1 /*
<lambda>null2  * Copyright 2022 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.graphics.shapes
18 
19 import androidx.annotation.IntRange
20 import androidx.collection.MutableFloatList
21 import kotlin.jvm.JvmOverloads
22 import kotlin.math.abs
23 import kotlin.math.max
24 import kotlin.math.min
25 import kotlin.math.sqrt
26 
27 /**
28  * The RoundedPolygon class allows simple construction of polygonal shapes with optional rounding at
29  * the vertices. Polygons can be constructed with either the number of vertices desired or an
30  * ordered list of vertices.
31  */
32 class RoundedPolygon internal constructor(val features: List<Feature>, internal val center: Point) {
33     val centerX
34         get() = center.x
35 
36     val centerY
37         get() = center.y
38 
39     /** A flattened version of the [Feature]s, as a List<Cubic>. */
40     val cubics = buildList {
41         // The first/last mechanism here ensures that the final anchor point in the shape
42         // exactly matches the first anchor point. There can be rendering artifacts introduced
43         // by those points being slightly off, even by much less than a pixel
44         var firstCubic: Cubic? = null
45         var lastCubic: Cubic? = null
46         var firstFeatureSplitStart: List<Cubic>? = null
47         var firstFeatureSplitEnd: List<Cubic>? = null
48         if (features.size > 0 && features[0].cubics.size == 3) {
49             val centerCubic = features[0].cubics[1]
50             val (start, end) = centerCubic.split(.5f)
51             firstFeatureSplitStart = mutableListOf(features[0].cubics[0], start)
52             firstFeatureSplitEnd = mutableListOf(end, features[0].cubics[2])
53         }
54         // iterating one past the features list size allows us to insert the initial split
55         // cubic if it exists
56         for (i in 0..features.size) {
57             val featureCubics =
58                 if (i == 0 && firstFeatureSplitEnd != null) firstFeatureSplitEnd
59                 else if (i == features.size) {
60                     if (firstFeatureSplitStart != null) firstFeatureSplitStart else break
61                 } else features[i].cubics
62             for (j in featureCubics.indices) {
63                 // Skip zero-length curves; they add nothing and can trigger rendering artifacts
64                 val cubic = featureCubics[j]
65                 if (!cubic.zeroLength()) {
66                     if (lastCubic != null) add(lastCubic)
67                     lastCubic = cubic
68                     if (firstCubic == null) firstCubic = cubic
69                 } else {
70                     if (lastCubic != null) {
71                         // Dropping several zero-ish length curves in a row can lead to
72                         // enough discontinuity to throw an exception later, even though the
73                         // distances are quite small. Account for that by making the last
74                         // cubic use the latest anchor point, always.
75                         lastCubic = Cubic(lastCubic.points.copyOf()) // Make a copy before mutating
76                         lastCubic.points[6] = cubic.anchor1X
77                         lastCubic.points[7] = cubic.anchor1Y
78                     }
79                 }
80             }
81         }
82         if (lastCubic != null && firstCubic != null) {
83             add(
84                 Cubic(
85                     lastCubic.anchor0X,
86                     lastCubic.anchor0Y,
87                     lastCubic.control0X,
88                     lastCubic.control0Y,
89                     lastCubic.control1X,
90                     lastCubic.control1Y,
91                     firstCubic.anchor0X,
92                     firstCubic.anchor0Y
93                 )
94             )
95         } else {
96             // Empty / 0-sized polygon.
97             add(
98                 Cubic(
99                     centerX,
100                     centerY,
101                     centerX,
102                     centerY,
103                     centerX,
104                     centerY,
105                     centerX,
106                     centerY,
107                 )
108             )
109         }
110     }
111 
112     init {
113         var prevCubic = cubics[cubics.size - 1]
114         debugLog("RoundedPolygon") { "Cubic-1 = $prevCubic" }
115         for (index in cubics.indices) {
116             val cubic = cubics[index]
117             debugLog("RoundedPolygon") { "Cubic = $cubic" }
118             if (
119                 abs(cubic.anchor0X - prevCubic.anchor1X) > DistanceEpsilon ||
120                     abs(cubic.anchor0Y - prevCubic.anchor1Y) > DistanceEpsilon
121             ) {
122                 debugLog("RoundedPolygon") {
123                     "Ix: $index | (${cubic.anchor0X},${cubic.anchor0Y}) vs $prevCubic"
124                 }
125                 throw IllegalArgumentException(
126                     "RoundedPolygon must be contiguous, with the anchor points of all curves " +
127                         "matching the anchor points of the preceding and succeeding cubics"
128                 )
129             }
130             prevCubic = cubic
131         }
132     }
133 
134     /**
135      * Transforms (scales/translates/etc.) this [RoundedPolygon] with the given [PointTransformer]
136      * and returns a new [RoundedPolygon]. This is a low level API and there should be more platform
137      * idiomatic ways to transform a [RoundedPolygon] provided by the platform specific wrapper.
138      *
139      * @param f The [PointTransformer] used to transform this [RoundedPolygon]
140      */
141     fun transformed(f: PointTransformer): RoundedPolygon {
142         val center = center.transformed(f)
143         return RoundedPolygon(
144             buildList {
145                 for (i in features.indices) {
146                     add(features[i].transformed(f))
147                 }
148             },
149             center
150         )
151     }
152 
153     /**
154      * Creates a new RoundedPolygon, moving and resizing this one, so it's completely inside the
155      * (0, 0) -> (1, 1) square, centered if there extra space in one direction
156      */
157     fun normalized(): RoundedPolygon {
158         val bounds = calculateBounds()
159         val width = bounds[2] - bounds[0]
160         val height = bounds[3] - bounds[1]
161         val side = max(width, height)
162         // Center the shape if bounds are not a square
163         val offsetX = (side - width) / 2 - bounds[0] /* left */
164         val offsetY = (side - height) / 2 - bounds[1] /* top */
165         return transformed { x, y -> TransformResult((x + offsetX) / side, (y + offsetY) / side) }
166     }
167 
168     override fun toString(): String =
169         "[RoundedPolygon." +
170             " Cubics = " +
171             cubics.joinToString() +
172             " || Features = " +
173             features.joinToString() +
174             " || Center = ($centerX, $centerY)]"
175 
176     /**
177      * Like [calculateBounds], this function calculates the axis-aligned bounds of the object and
178      * returns that rectangle. But this function determines the max dimension of the shape (by
179      * calculating the distance from its center to the start and midpoint of each curve) and returns
180      * a square which can be used to hold the object in any rotation. This function can be used, for
181      * example, to calculate the max size of a UI element meant to hold this shape in any rotation.
182      *
183      * @param bounds a buffer to hold the results. If not supplied, a temporary buffer will be
184      *   created.
185      * @return The axis-aligned max bounding box for this object, where the rectangles left, top,
186      *   right, and bottom values will be stored in entries 0, 1, 2, and 3, in that order.
187      */
188     fun calculateMaxBounds(bounds: FloatArray = FloatArray(4)): FloatArray {
189         require(bounds.size >= 4) { "Required bounds size of 4" }
190         var maxDistSquared = 0f
191         for (i in cubics.indices) {
192             val cubic = cubics[i]
193             val anchorDistance = distanceSquared(cubic.anchor0X - centerX, cubic.anchor0Y - centerY)
194             val middlePoint = cubic.pointOnCurve(.5f)
195             val middleDistance = distanceSquared(middlePoint.x - centerX, middlePoint.y - centerY)
196             maxDistSquared = max(maxDistSquared, max(anchorDistance, middleDistance))
197         }
198         val distance = sqrt(maxDistSquared)
199         bounds[0] = centerX - distance
200         bounds[1] = centerY - distance
201         bounds[2] = centerX + distance
202         bounds[3] = centerY + distance
203         return bounds
204     }
205 
206     /**
207      * Calculates the axis-aligned bounds of the object.
208      *
209      * @param approximate when true, uses a faster calculation to create the bounding box based on
210      *   the min/max values of all anchor and control points that make up the shape. Default value
211      *   is true.
212      * @param bounds a buffer to hold the results. If not supplied, a temporary buffer will be
213      *   created.
214      * @return The axis-aligned bounding box for this object, where the rectangles left, top, right,
215      *   and bottom values will be stored in entries 0, 1, 2, and 3, in that order.
216      */
217     @JvmOverloads
218     fun calculateBounds(
219         bounds: FloatArray = FloatArray(4),
220         approximate: Boolean = true
221     ): FloatArray {
222         require(bounds.size >= 4) { "Required bounds size of 4" }
223         var minX = Float.MAX_VALUE
224         var minY = Float.MAX_VALUE
225         var maxX = Float.MIN_VALUE
226         var maxY = Float.MIN_VALUE
227         for (i in cubics.indices) {
228             val cubic = cubics[i]
229             cubic.calculateBounds(bounds, approximate = approximate)
230             minX = min(minX, bounds[0])
231             minY = min(minY, bounds[1])
232             maxX = max(maxX, bounds[2])
233             maxY = max(maxY, bounds[3])
234         }
235         bounds[0] = minX
236         bounds[1] = minY
237         bounds[2] = maxX
238         bounds[3] = maxY
239         return bounds
240     }
241 
242     companion object {}
243 
244     override fun equals(other: Any?): Boolean {
245         if (this === other) return true
246         if (other !is RoundedPolygon) return false
247 
248         return features == other.features
249     }
250 
251     override fun hashCode(): Int {
252         return features.hashCode()
253     }
254 }
255 
256 /**
257  * This constructor takes the number of vertices in the resulting polygon. These vertices are
258  * positioned on a virtual circle around a given center with each vertex positioned [radius]
259  * distance from that center, equally spaced (with equal angles between them). If no radius is
260  * supplied, the shape will be created with a default radius of 1, resulting in a shape whose
261  * vertices lie on a unit circle, with width/height of 2. That default polygon will probably need to
262  * be rescaled using [transformed] into the appropriate size for the UI in which it will be drawn.
263  *
264  * The [rounding] and [perVertexRounding] parameters are optional. If not supplied, the result will
265  * be a regular polygon with straight edges and unrounded corners.
266  *
267  * @param numVertices The number of vertices in this polygon.
268  * @param radius The radius of the polygon, in pixels. This radius determines the initial size of
269  *   the object, but it can be transformed later by using the [transformed] function.
270  * @param centerX The X coordinate of the center of the polygon, around which all vertices will be
271  *   placed. The default center is at (0,0).
272  * @param centerY The Y coordinate of the center of the polygon, around which all vertices will be
273  *   placed. The default center is at (0,0).
274  * @param rounding The [CornerRounding] properties of all vertices. If some vertices should have
275  *   different rounding properties, then use [perVertexRounding] instead. The default rounding value
276  *   is [CornerRounding.Unrounded], meaning that the polygon will use the vertices themselves in the
277  *   final shape and not curves rounded around the vertices.
278  * @param perVertexRounding The [CornerRounding] properties of every vertex. If this parameter is
279  *   not null, then it must have [numVertices] elements. If this parameter is null, then the polygon
280  *   will use the [rounding] parameter for every vertex instead. The default value is null.
281  * @throws IllegalArgumentException If [perVertexRounding] is not null and its size is not equal to
282  *   [numVertices].
283  * @throws IllegalArgumentException [numVertices] must be at least 3.
284  */
285 @JvmOverloads
RoundedPolygonnull286 fun RoundedPolygon(
287     @IntRange(from = 3) numVertices: Int,
288     radius: Float = 1f,
289     centerX: Float = 0f,
290     centerY: Float = 0f,
291     rounding: CornerRounding = CornerRounding.Unrounded,
292     perVertexRounding: List<CornerRounding>? = null
293 ) =
294     RoundedPolygon(
295         verticesFromNumVerts(numVertices, radius, centerX, centerY),
296         rounding = rounding,
297         perVertexRounding = perVertexRounding,
298         centerX = centerX,
299         centerY = centerY
300     )
301 
302 /** Creates a copy of the given [RoundedPolygon] */
303 fun RoundedPolygon(source: RoundedPolygon) = RoundedPolygon(source.features, source.center)
304 
305 /**
306  * This function takes the vertices (either supplied or calculated, depending on the constructor
307  * called), plus [CornerRounding] parameters, and creates the actual [RoundedPolygon] shape,
308  * rounding around the vertices (or not) as specified. The result is a list of [Cubic] curves which
309  * represent the geometry of the final shape.
310  *
311  * @param vertices The list of vertices in this polygon specified as pairs of x/y coordinates in
312  *   this FloatArray. This should be an ordered list (with the outline of the shape going from each
313  *   vertex to the next in order of this list), otherwise the results will be undefined.
314  * @param rounding The [CornerRounding] properties of all vertices. If some vertices should have
315  *   different rounding properties, then use [perVertexRounding] instead. The default rounding value
316  *   is [CornerRounding.Unrounded], meaning that the polygon will use the vertices themselves in the
317  *   final shape and not curves rounded around the vertices.
318  * @param perVertexRounding The [CornerRounding] properties of all vertices. If this parameter is
319  *   not null, then it must have the same size as [vertices]. If this parameter is null, then the
320  *   polygon will use the [rounding] parameter for every vertex instead. The default value is null.
321  * @param centerX The X coordinate of the center of the polygon, around which all vertices will be
322  *   placed. The default center is at (0,0).
323  * @param centerY The Y coordinate of the center of the polygon, around which all vertices will be
324  *   placed. The default center is at (0,0).
325  * @throws IllegalArgumentException if the number of vertices is less than 3 (the [vertices]
326  *   parameter has less than 6 Floats). Or if the [perVertexRounding] parameter is not null and the
327  *   size doesn't match the number vertices.
328  */
329 // TODO(performance): Update the map calls to more efficient code that doesn't allocate Iterators
330 //  unnecessarily.
331 @JvmOverloads
332 fun RoundedPolygon(
333     vertices: FloatArray,
334     rounding: CornerRounding = CornerRounding.Unrounded,
335     perVertexRounding: List<CornerRounding>? = null,
336     centerX: Float = Float.MIN_VALUE,
337     centerY: Float = Float.MIN_VALUE
338 ): RoundedPolygon {
339     if (vertices.size < 6) {
340         throw IllegalArgumentException("Polygons must have at least 3 vertices")
341     }
342     if (vertices.size % 2 == 1) {
343         throw IllegalArgumentException("The vertices array should have even size")
344     }
345     if (perVertexRounding != null && perVertexRounding.size * 2 != vertices.size) {
346         throw IllegalArgumentException(
347             "perVertexRounding list should be either null or " +
348                 "the same size as the number of vertices (vertices.size / 2)"
349         )
350     }
351     val corners = mutableListOf<List<Cubic>>()
352     val n = vertices.size / 2
353     val roundedCorners = mutableListOf<RoundedCorner>()
354     for (i in 0 until n) {
355         val vtxRounding = perVertexRounding?.get(i) ?: rounding
356         val prevIndex = ((i + n - 1) % n) * 2
357         val nextIndex = ((i + 1) % n) * 2
358         roundedCorners.add(
359             RoundedCorner(
360                 Point(vertices[prevIndex], vertices[prevIndex + 1]),
361                 Point(vertices[i * 2], vertices[i * 2 + 1]),
362                 Point(vertices[nextIndex], vertices[nextIndex + 1]),
363                 vtxRounding
364             )
365         )
366     }
367 
368     // For each side, check if we have enough space to do the cuts needed, and if not split
369     // the available space, first for round cuts, then for smoothing if there is space left.
370     // Each element in this list is a pair, that represent how much we can do of the cut for
371     // the given side (side i goes from corner i to corner i+1), the elements of the pair are:
372     // first is how much we can use of expectedRoundCut, second how much of expectedCut
373     val cutAdjusts =
374         (0 until n).map { ix ->
375             val expectedRoundCut =
376                 roundedCorners[ix].expectedRoundCut + roundedCorners[(ix + 1) % n].expectedRoundCut
377             val expectedCut =
378                 roundedCorners[ix].expectedCut + roundedCorners[(ix + 1) % n].expectedCut
379             val vtxX = vertices[ix * 2]
380             val vtxY = vertices[ix * 2 + 1]
381             val nextVtxX = vertices[((ix + 1) % n) * 2]
382             val nextVtxY = vertices[((ix + 1) % n) * 2 + 1]
383             val sideSize = distance(vtxX - nextVtxX, vtxY - nextVtxY)
384 
385             // Check expectedRoundCut first, and ensure we fulfill rounding needs first for
386             // both corners before using space for smoothing
387             if (expectedRoundCut > sideSize) {
388                 // Not enough room for fully rounding, see how much we can actually do.
389                 sideSize / expectedRoundCut to 0f
390             } else if (expectedCut > sideSize) {
391                 // We can do full rounding, but not full smoothing.
392                 1f to (sideSize - expectedRoundCut) / (expectedCut - expectedRoundCut)
393             } else {
394                 // There is enough room for rounding & smoothing.
395                 1f to 1f
396             }
397         }
398     // Create and store list of beziers for each [potentially] rounded corner
399     for (i in 0 until n) {
400         // allowedCuts[0] is for the side from the previous corner to this one,
401         // allowedCuts[1] is for the side from this corner to the next one.
402         val allowedCuts = MutableFloatList(2)
403         for (delta in 0..1) {
404             val (roundCutRatio, cutRatio) = cutAdjusts[(i + n - 1 + delta) % n]
405             allowedCuts.add(
406                 roundedCorners[i].expectedRoundCut * roundCutRatio +
407                     (roundedCorners[i].expectedCut - roundedCorners[i].expectedRoundCut) * cutRatio
408             )
409         }
410         corners.add(
411             roundedCorners[i].getCubics(allowedCut0 = allowedCuts[0], allowedCut1 = allowedCuts[1])
412         )
413     }
414     // Finally, store the calculated cubics. This includes all of the rounded corners
415     // from above, along with new cubics representing the edges between those corners.
416     val tempFeatures = mutableListOf<Feature>()
417     for (i in 0 until n) {
418         // Note that these indices are for pairs of values (points), they need to be
419         // doubled to access the xy values in the vertices float array
420         val prevVtxIndex = (i + n - 1) % n
421         val nextVtxIndex = (i + 1) % n
422         val currVertex = Point(vertices[i * 2], vertices[i * 2 + 1])
423         val prevVertex = Point(vertices[prevVtxIndex * 2], vertices[prevVtxIndex * 2 + 1])
424         val nextVertex = Point(vertices[nextVtxIndex * 2], vertices[nextVtxIndex * 2 + 1])
425         val convex = convex(prevVertex, currVertex, nextVertex)
426         tempFeatures.add(Feature.Corner(corners[i], convex))
427         tempFeatures.add(
428             Feature.Edge(
429                 listOf(
430                     Cubic.straightLine(
431                         corners[i].last().anchor1X,
432                         corners[i].last().anchor1Y,
433                         corners[(i + 1) % n].first().anchor0X,
434                         corners[(i + 1) % n].first().anchor0Y
435                     )
436                 )
437             )
438         )
439     }
440 
441     val (cx, cy) =
442         if (centerX == Float.MIN_VALUE || centerY == Float.MIN_VALUE) {
443             calculateCenter(vertices)
444         } else {
445             Point(centerX, centerY)
446         }
447     return RoundedPolygon(tempFeatures, cx, cy)
448 }
449 
450 /**
451  * This constructor takes a list of [Feature] objects that define the polygon's shape and curves. By
452  * specifying the features directly, the summarization of [Cubic] objects to curves can be precisely
453  * controlled. This affects [Morph]'s default mapping, as curves with the same type (convex or
454  * concave) are mapped with each other. For example, if you have a convex curve in your start
455  * polygon, [Morph] will map it to another convex curve in the end polygon.
456  *
457  * The [centerX] and [centerY] parameters are optional. If not supplied, they will be estimated by
458  * calculating the average of all cubic anchor points.
459  *
460  * @param features The [Feature]s that describe the characteristics of each outline segment of the
461  *   polygon.
462  * @param centerX The X coordinate of the center of the polygon, around which all vertices will be
463  *   placed. If none provided, the center will be averaged.
464  * @param centerY The Y coordinate of the center of the polygon, around which all vertices will be
465  *   placed. If none provided, the center will be averaged.
466  * @throws IllegalArgumentException [features] must be at least specify 2 features and describe a
467  *   closed shape.
468  */
469 @JvmOverloads
RoundedPolygonnull470 fun RoundedPolygon(
471     features: List<Feature>,
472     centerX: Float = Float.NaN,
473     centerY: Float = Float.NaN
474 ): RoundedPolygon {
475     require(features.size >= 2) { "Polygons must have at least 2 features" }
476 
477     val vertices =
478         buildList {
479                 for (feature in features) {
480                     for (cubic in feature.cubics) {
481                         add(cubic.anchor0X)
482                         add(cubic.anchor0Y)
483                     }
484                 }
485             }
486             .toFloatArray()
487 
488     val cX = if (centerX.isNaN()) calculateCenter(vertices).first else centerX
489     val cY = if (centerY.isNaN()) calculateCenter(vertices).second else centerY
490 
491     return RoundedPolygon(features, Point(cX, cY))
492 }
493 
494 /**
495  * Calculates an estimated center position for the polygon, returning it. This function should only
496  * be called if the center is not already calculated or provided. The Polygon constructor which
497  * takes `numVertices` calculates its own center, since it knows exactly where it is centered, at
498  * (0, 0).
499  *
500  * Note that this center will be transformed whenever the shape itself is transformed. Any
501  * transforms that occur before the center is calculated will be taken into account automatically
502  * since the center calculation is an average of the current location of all cubic anchor points.
503  */
calculateCenternull504 internal fun calculateCenter(vertices: FloatArray): Point {
505     var cumulativeX = 0f
506     var cumulativeY = 0f
507     var index = 0
508     while (index < vertices.size) {
509         cumulativeX += vertices[index++]
510         cumulativeY += vertices[index++]
511     }
512     return Point(cumulativeX / (vertices.size / 2), cumulativeY / (vertices.size / 2))
513 }
514 
515 /**
516  * Private utility class that holds the information about each corner in a polygon. The shape of the
517  * corner can be returned by calling the [getCubics] function, which will return a list of curves
518  * representing the corner geometry. The shape of the corner depends on the [rounding] constructor
519  * parameter.
520  *
521  * If rounding is null, there is no rounding; the corner will simply be a single point at [p1]. This
522  * point will be represented by a [Cubic] of length 0 at that point.
523  *
524  * If rounding is not null, the corner will be rounded either with a curve approximating a circular
525  * arc of the radius specified in [rounding], or with three curves if [rounding] has a nonzero
526  * smoothing parameter. These three curves are a circular arc in the middle and two symmetrical
527  * flanking curves on either side. The smoothing parameter determines the curvature of the flanking
528  * curves.
529  *
530  * This is a class because we usually need to do the work in 2 steps, and prefer to keep state
531  * between: first we determine how much we want to cut to comply with the parameters, then we are
532  * given how much we can actually cut (because of space restrictions outside this corner)
533  *
534  * @param p0 the vertex before the one being rounded
535  * @param p1 the vertex of this rounded corner
536  * @param p2 the vertex after the one being rounded
537  * @param rounding the optional parameters specifying how this corner should be rounded
538  */
539 private class RoundedCorner(
540     val p0: Point,
541     val p1: Point,
542     val p2: Point,
543     val rounding: CornerRounding? = null
544 ) {
545     val d1: Point
546     val d2: Point
547     val cornerRadius: Float
548     val smoothing: Float
549     val cosAngle: Float
550     val sinAngle: Float
551     val expectedRoundCut: Float
552 
553     init {
554         val v01 = p0 - p1
555         val v21 = p2 - p1
556         val d01 = v01.getDistance()
557         val d21 = v21.getDistance()
558         if (d01 > 0f && d21 > 0f) {
559             d1 = v01 / d01
560             d2 = v21 / d21
561             cornerRadius = rounding?.radius ?: 0f
562             smoothing = rounding?.smoothing ?: 0f
563 
564             // cosine of angle at p1 is dot product of unit vectors to the other two vertices
565             cosAngle = d1.dotProduct(d2)
566 
567             // identity: sin^2 + cos^2 = 1
568             // sinAngle gives us the intersection
569             sinAngle = sqrt(1 - square(cosAngle))
570             // How much we need to cut, as measured on a side, to get the required radius
571             // calculating where the rounding circle hits the edge
572             // This uses the identity of tan(A/2) = sinA/(1 + cosA), where tan(A/2) = radius/cut
573             expectedRoundCut =
574                 if (sinAngle > 1e-3) {
575                     cornerRadius * (cosAngle + 1) / sinAngle
576                 } else {
577                     0f
578                 }
579         } else {
580             // One (or both) of the sides is empty, not much we can do.
581             d1 = Point(0f, 0f)
582             d2 = Point(0f, 0f)
583             cornerRadius = 0f
584             smoothing = 0f
585             cosAngle = 0f
586             sinAngle = 0f
587             expectedRoundCut = 0f
588         }
589     }
590 
591     // smoothing changes the actual cut. 0 is same as expectedRoundCut, 1 doubles it
592     val expectedCut: Float
593         get() = ((1 + smoothing) * expectedRoundCut)
594 
595     // the center of the circle approximated by the rounding curve (or the middle of the three
596     // curves if smoothing is requested). The center is the same as p0 if there is no rounding.
597     var center: Point = Point(0f, 0f)
598 
599     @JvmOverloads
getCubicsnull600     fun getCubics(allowedCut0: Float, allowedCut1: Float = allowedCut0): List<Cubic> {
601         // We use the minimum of both cuts to determine the radius, but if there is more space
602         // in one side we can use it for smoothing.
603         val allowedCut = min(allowedCut0, allowedCut1)
604         // Nothing to do, just use lines, or a point
605         if (
606             expectedRoundCut < DistanceEpsilon ||
607                 allowedCut < DistanceEpsilon ||
608                 cornerRadius < DistanceEpsilon
609         ) {
610             center = p1
611             return listOf(Cubic.straightLine(p1.x, p1.y, p1.x, p1.y))
612         }
613         // How much of the cut is required for the rounding part.
614         val actualRoundCut = min(allowedCut, expectedRoundCut)
615         // We have two smoothing values, one for each side of the vertex
616         // Space is used for rounding values first. If there is space left over, then we
617         // apply smoothing, if it was requested
618         val actualSmoothing0 = calculateActualSmoothingValue(allowedCut0)
619         val actualSmoothing1 = calculateActualSmoothingValue(allowedCut1)
620         // Scale the radius if needed
621         val actualR = cornerRadius * actualRoundCut / expectedRoundCut
622         // Distance from the corner (p1) to the center
623         val centerDistance = sqrt(square(actualR) + square(actualRoundCut))
624         // Center of the arc we will use for rounding
625         center = p1 + ((d1 + d2) / 2f).getDirection() * centerDistance
626         val circleIntersection0 = p1 + d1 * actualRoundCut
627         val circleIntersection2 = p1 + d2 * actualRoundCut
628         val flanking0 =
629             computeFlankingCurve(
630                 actualRoundCut,
631                 actualSmoothing0,
632                 p1,
633                 p0,
634                 circleIntersection0,
635                 circleIntersection2,
636                 center,
637                 actualR
638             )
639         val flanking2 =
640             computeFlankingCurve(
641                     actualRoundCut,
642                     actualSmoothing1,
643                     p1,
644                     p2,
645                     circleIntersection2,
646                     circleIntersection0,
647                     center,
648                     actualR
649                 )
650                 .reverse()
651         return listOf(
652             flanking0,
653             Cubic.circularArc(
654                 center.x,
655                 center.y,
656                 flanking0.anchor1X,
657                 flanking0.anchor1Y,
658                 flanking2.anchor0X,
659                 flanking2.anchor0Y
660             ),
661             flanking2
662         )
663     }
664 
665     /**
666      * If allowedCut (the amount we are able to cut) is greater than the expected cut (without
667      * smoothing applied yet), then there is room to apply smoothing and we calculate the actual
668      * smoothing value here.
669      */
calculateActualSmoothingValuenull670     private fun calculateActualSmoothingValue(allowedCut: Float): Float {
671         return if (allowedCut > expectedCut) {
672             smoothing
673         } else if (allowedCut > expectedRoundCut) {
674             smoothing * (allowedCut - expectedRoundCut) / (expectedCut - expectedRoundCut)
675         } else {
676             0f
677         }
678     }
679 
680     /**
681      * Compute a Bezier to connect the linear segment defined by corner and sideStart with the
682      * circular segment defined by circleCenter, circleSegmentIntersection,
683      * otherCircleSegmentIntersection and actualR. The bezier will start at the linear segment and
684      * end on the circular segment.
685      *
686      * @param actualRoundCut How much we are cutting of the corner to add the circular segment (this
687      *   is before smoothing, that will cut some more).
688      * @param actualSmoothingValues How much we want to smooth (this is the smooth parameter,
689      *   adjusted down if there is not enough room).
690      * @param corner The point at which the linear side ends
691      * @param sideStart The point at which the linear side starts
692      * @param circleSegmentIntersection The point at which the linear side and the circle intersect.
693      * @param otherCircleSegmentIntersection The point at which the opposing linear side and the
694      *   circle intersect.
695      * @param circleCenter The center of the circle.
696      * @param actualR The radius of the circle.
697      * @return a Bezier cubic curve that connects from the (cut) linear side and the (cut) circular
698      *   segment in a smooth way.
699      */
computeFlankingCurvenull700     private fun computeFlankingCurve(
701         actualRoundCut: Float,
702         actualSmoothingValues: Float,
703         corner: Point,
704         sideStart: Point,
705         circleSegmentIntersection: Point,
706         otherCircleSegmentIntersection: Point,
707         circleCenter: Point,
708         actualR: Float
709     ): Cubic {
710         // sideStart is the anchor, 'anchor' is actual control point
711         val sideDirection = (sideStart - corner).getDirection()
712         val curveStart = corner + sideDirection * actualRoundCut * (1 + actualSmoothingValues)
713         // We use an approximation to cut a part of the circle section proportional to 1 - smooth,
714         // When smooth = 0, we take the full section, when smooth = 1, we take nothing.
715         // TODO: revisit this, it can be problematic as it approaches 180 degrees
716         val p =
717             interpolate(
718                 circleSegmentIntersection,
719                 (circleSegmentIntersection + otherCircleSegmentIntersection) / 2f,
720                 actualSmoothingValues
721             )
722         // The flanking curve ends on the circle
723         val curveEnd =
724             circleCenter + directionVector(p.x - circleCenter.x, p.y - circleCenter.y) * actualR
725         // The anchor on the circle segment side is in the intersection between the tangent to the
726         // circle in the circle/flanking curve boundary and the linear segment.
727         val circleTangent = (curveEnd - circleCenter).rotate90()
728         val anchorEnd =
729             lineIntersection(sideStart, sideDirection, curveEnd, circleTangent)
730                 ?: circleSegmentIntersection
731         // From what remains, we pick a point for the start anchor.
732         // 2/3 seems to come from design tools?
733         val anchorStart = (curveStart + anchorEnd * 2f) / 3f
734         return Cubic(curveStart, anchorStart, anchorEnd, curveEnd)
735     }
736 
737     /**
738      * Returns the intersection point of the two lines d0->d1 and p0->p1, or null if the lines do
739      * not intersect
740      */
lineIntersectionnull741     private fun lineIntersection(p0: Point, d0: Point, p1: Point, d1: Point): Point? {
742         val rotatedD1 = d1.rotate90()
743         val den = d0.dotProduct(rotatedD1)
744         if (abs(den) < DistanceEpsilon) return null
745         val num = (p1 - p0).dotProduct(rotatedD1)
746         // Also check the relative value. This is equivalent to abs(den/num) < DistanceEpsilon,
747         // but avoid doing a division
748         if (abs(den) < DistanceEpsilon * abs(num)) return null
749         val k = num / den
750         return p0 + d0 * k
751     }
752 }
753 
verticesFromNumVertsnull754 private fun verticesFromNumVerts(
755     numVertices: Int,
756     radius: Float,
757     centerX: Float,
758     centerY: Float
759 ): FloatArray {
760     val result = FloatArray(numVertices * 2)
761     var arrayIndex = 0
762     for (i in 0 until numVertices) {
763         val vertex =
764             radialToCartesian(radius, (FloatPi / numVertices * 2 * i)) + Point(centerX, centerY)
765         result[arrayIndex++] = vertex.x
766         result[arrayIndex++] = vertex.y
767     }
768     return result
769 }
770