1 /*
2  * Copyright 2024 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 // TODO: b/372000685 b/372003785 b/372004969
20 //  Make class public as soon as all validations
21 //  are implemented and mention in [RoundedPolygon] constructor
22 
23 /**
24  * Utility class to fix invalid [RoundedPolygon]s that will otherwise break [Morph]s in one way or
25  * another, as [RoundedPolygon] assumes correct input. Correct input meaning:
26  * - Closed geometry
27  * - Clockwise orientation of points
28  * - No self-intersections
29  * - No holes
30  * - Single Polygon
31  */
32 internal class PolygonValidator() {
33 
34     companion object {
35 
36         // TODO: b/372000685 b/372003785 b/372004969
37         // Update docs when other validations are implemented
38         /**
39          * Validates whether this [RoundedPolygon]'s orientation is clockwise and fixes it if
40          * necessary.
41          *
42          * @param polygon The [RoundedPolygon] to validate
43          * @return A new [RoundedPolygon] with fixed orientation, or the same [RoundedPolygon] as
44          *   given when it was already valid
45          */
fixnull46         fun fix(polygon: RoundedPolygon): RoundedPolygon {
47             var result = polygon
48 
49             debugLog(LOG_TAG) { "Validating polygon..." }
50 
51             if (isCWOriented(polygon)) {
52                 debugLog(LOG_TAG) { "Passed clockwise validation!" }
53             } else {
54                 debugLog(LOG_TAG) { "Polygon is oriented anti-clockwise, fixing orientation..." }
55                 result = fixCWOrientation(polygon)
56             }
57 
58             return result
59         }
60 
isCWOrientednull61         private fun isCWOriented(polygon: RoundedPolygon): Boolean {
62             var signedArea = 0.0f
63 
64             for (i in polygon.cubics.indices) {
65                 val cubic = polygon.cubics[i]
66                 signedArea += (cubic.anchor1X - cubic.anchor0X) * (cubic.anchor1Y + cubic.anchor0Y)
67             }
68 
69             return signedArea < 0
70         }
71 
fixCWOrientationnull72         private fun fixCWOrientation(polygon: RoundedPolygon): RoundedPolygon {
73             val reversedFeatures = buildList {
74                 // Persist first feature to stay a Corner
75                 add(polygon.features.first().reversed())
76 
77                 for (i in polygon.features.lastIndex downTo 1) {
78                     add(polygon.features[i].reversed())
79                 }
80             }
81 
82             return RoundedPolygon(reversedFeatures, polygon.center)
83         }
84     }
85 }
86 
87 private const val LOG_TAG = "PolygonValidation"
88