1 /*
<lambda>null2  * Copyright 2023 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.test.filters.SmallTest
20 import kotlin.math.PI
21 import kotlin.math.sqrt
22 import org.junit.Assert.assertEquals
23 import org.junit.Assert.assertTrue
24 import org.junit.Test
25 
26 @SmallTest
27 class PolygonMeasureTest {
28     private val measurer = LengthMeasurer()
29 
30     @Test fun measureSharpTriangle() = regularPolygonMeasure(3)
31 
32     @Test fun measureSharpPentagon() = regularPolygonMeasure(5)
33 
34     @Test fun measureSharpOctagon() = regularPolygonMeasure(8)
35 
36     @Test fun measureSharpDodecagon() = regularPolygonMeasure(12)
37 
38     @Test fun measureSharpIcosagon() = regularPolygonMeasure(20)
39 
40     @Test
41     fun measureSlightlyRoundedHexagon() {
42         irregularPolygonMeasure(RoundedPolygon(6, rounding = CornerRounding(0.15f)))
43     }
44 
45     @Test
46     fun measureMediumRoundedHexagon() {
47         irregularPolygonMeasure(RoundedPolygon(6, rounding = CornerRounding(0.5f)))
48     }
49 
50     @Test
51     fun measureMaximumRoundedHexagon() {
52         irregularPolygonMeasure(RoundedPolygon(6, rounding = CornerRounding(1f)))
53     }
54 
55     @Test
56     fun measureCircle() {
57         // White box test: As the length measurer approximates arcs by linear segments,
58         // this test validates if the chosen segment count approximates the arc length up to
59         // an error of 1.5% from the true length
60         val vertices = 4
61         val polygon = RoundedPolygon.circle(numVertices = vertices)
62 
63         val actualLength = polygon.cubics.sumOf { LengthMeasurer().measureCubic(it).toDouble() }
64         val expectedLength = 2 * PI
65 
66         assertEquals(expectedLength, actualLength, 0.015f * expectedLength)
67     }
68 
69     @Test
70     fun irregularTriangleAngleMeasure() =
71         irregularPolygonMeasure(
72             RoundedPolygon(
73                 vertices = floatArrayOf(0f, -1f, 1f, 1f, 0f, 0.5f, -1f, 1f),
74                 perVertexRounding =
75                     listOf(
76                         CornerRounding(0.2f, 0.5f),
77                         CornerRounding(0.2f, 0.5f),
78                         CornerRounding(0.4f, 0f),
79                         CornerRounding(0.2f, 0.5f),
80                     )
81             )
82         )
83 
84     @Test
85     fun quarterAngleMeasure() =
86         irregularPolygonMeasure(
87             RoundedPolygon(
88                 vertices = floatArrayOf(-1f, -1f, 1f, -1f, 1f, 1f, -1f, 1f),
89                 perVertexRounding =
90                     listOf(
91                         CornerRounding.Unrounded,
92                         CornerRounding.Unrounded,
93                         CornerRounding(0.5f, 0.5f),
94                         CornerRounding.Unrounded,
95                     )
96             )
97         )
98 
99     @Test
100     fun hourGlassMeasure() {
101         // Regression test: Legacy measurer (AngleMeasurer) would skip the diagonal sides
102         // as they are 0 degrees from the center.
103         val unit = 1f
104         val coordinates =
105             floatArrayOf(
106                 // lower glass
107                 0f,
108                 0f,
109                 unit,
110                 unit,
111                 -unit,
112                 unit,
113 
114                 // upper glass
115                 0f,
116                 0f,
117                 -unit,
118                 -unit,
119                 unit,
120                 -unit,
121             )
122 
123         val diagonal = sqrt(unit * unit + unit * unit)
124         val horizontal = 2 * unit
125         val total = 4 * diagonal + 2 * horizontal
126 
127         val polygon = RoundedPolygon(coordinates)
128         customPolygonMeasure(
129             polygon,
130             floatArrayOf(
131                 diagonal / total,
132                 horizontal / total,
133                 diagonal / total,
134                 diagonal / total,
135                 horizontal / total,
136                 diagonal / total,
137             )
138         )
139     }
140 
141     @Test
142     fun handlesEmptyFeatureLast() {
143         val triangle =
144             RoundedPolygon(
145                 listOf(
146                     Feature.buildConvexCorner(listOf(Cubic.straightLine(0f, 0f, 1f, 1f))),
147                     Feature.buildConvexCorner(listOf(Cubic.straightLine(1f, 1f, 1f, 0f))),
148                     Feature.buildConvexCorner(listOf(Cubic.straightLine(1f, 0f, 0f, 0f))),
149                     // Empty feature at the end.
150                     Feature.buildConvexCorner(listOf(Cubic.straightLine(0f, 0f, 0f, 0f))),
151                 )
152             )
153 
154         irregularPolygonMeasure(triangle)
155     }
156 
157     private fun regularPolygonMeasure(
158         sides: Int,
159         rounding: CornerRounding = CornerRounding.Unrounded
160     ) {
161         irregularPolygonMeasure(RoundedPolygon(sides, rounding = rounding)) { measuredPolygon ->
162             assertEquals(sides, measuredPolygon.size)
163 
164             measuredPolygon.forEachIndexed { index, measuredCubic ->
165                 assertEqualish(index.toFloat() / sides, measuredCubic.startOutlineProgress)
166             }
167         }
168     }
169 
170     private fun customPolygonMeasure(polygon: RoundedPolygon, progresses: FloatArray) =
171         irregularPolygonMeasure(polygon) { measuredPolygon ->
172             require(measuredPolygon.size == progresses.size)
173 
174             measuredPolygon.forEachIndexed { index, measuredCubic ->
175                 assertEqualish(
176                     progresses[index],
177                     measuredCubic.endOutlineProgress - measuredCubic.startOutlineProgress
178                 )
179             }
180         }
181 
182     private fun irregularPolygonMeasure(
183         polygon: RoundedPolygon,
184         extraChecks: (MeasuredPolygon) -> Unit = {}
185     ) {
186         val measuredPolygon = MeasuredPolygon.measurePolygon(measurer, polygon)
187 
188         assertEquals(0f, measuredPolygon.first().startOutlineProgress)
189         assertEquals(1f, measuredPolygon.last().endOutlineProgress)
190 
191         measuredPolygon.forEachIndexed { index, measuredCubic ->
192             if (index > 0) {
193                 assertEquals(
194                     measuredPolygon[index - 1].endOutlineProgress,
195                     measuredCubic.startOutlineProgress
196                 )
197             }
198             assertTrue(measuredCubic.endOutlineProgress >= measuredCubic.startOutlineProgress)
199         }
200 
201         measuredPolygon.features.forEachIndexed { index, progressableFeature ->
202             assert(progressableFeature.progress >= 0f && progressableFeature.progress < 1f) {
203                 "Feature #$index has invalid progress: ${progressableFeature.progress}"
204             }
205         }
206 
207         extraChecks(measuredPolygon)
208     }
209 }
210