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.test.filters.SmallTest
20 import org.junit.Assert.assertEquals
21 import org.junit.Assert.assertThrows
22 import org.junit.Test
23 
24 @SmallTest
25 class RoundedPolygonTest {
26 
27     val rounding = CornerRounding(.1f)
28     val perVtxRounded = listOf(rounding, rounding, rounding, rounding)
29 
30     @Test
31     fun numVertsConstructorTest() {
32         assertThrows(IllegalArgumentException::class.java) { RoundedPolygon(2) }
33 
34         val square = RoundedPolygon(4)
35         var min = Point(-1f, -1f)
36         var max = Point(1f, 1f)
37         assertInBounds(square.cubics, min, max)
38 
39         val doubleSquare = RoundedPolygon(4, 2f)
40         min *= 2f
41         max *= 2f
42         assertInBounds(doubleSquare.cubics, min, max)
43 
44         val squareRounded = RoundedPolygon(4, rounding = rounding)
45         min = Point(-1f, -1f)
46         max = Point(1f, 1f)
47         assertInBounds(squareRounded.cubics, min, max)
48 
49         val squarePVRounded = RoundedPolygon(4, perVertexRounding = perVtxRounded)
50         min = Point(-1f, -1f)
51         max = Point(1f, 1f)
52         assertInBounds(squarePVRounded.cubics, min, max)
53     }
54 
55     @Test
56     fun verticesConstructorTest() {
57         val p0 = Point(1f, 0f)
58         val p1 = Point(0f, 1f)
59         val p2 = Point(-1f, 0f)
60         val p3 = Point(0f, -1f)
61         val verts = floatArrayOf(p0.x, p0.y, p1.x, p1.y, p2.x, p2.y, p3.x, p3.y)
62 
63         assertThrows(IllegalArgumentException::class.java) {
64             RoundedPolygon(floatArrayOf(p0.x, p0.y, p1.x, p1.y))
65         }
66 
67         val manualSquare = RoundedPolygon(verts)
68         var min = Point(-1f, -1f)
69         var max = Point(1f, 1f)
70         assertInBounds(manualSquare.cubics, min, max)
71 
72         val offset = Point(1f, 2f)
73         val offsetVerts =
74             floatArrayOf(
75                 p0.x + offset.x,
76                 p0.y + offset.y,
77                 p1.x + offset.x,
78                 p1.y + offset.y,
79                 p2.x + offset.x,
80                 p2.y + offset.y,
81                 p3.x + offset.x,
82                 p3.y + offset.y
83             )
84         val manualSquareOffset = RoundedPolygon(offsetVerts, centerX = offset.x, centerY = offset.y)
85         min = Point(0f, 1f)
86         max = Point(2f, 3f)
87         assertInBounds(manualSquareOffset.cubics, min, max)
88 
89         val manualSquareRounded = RoundedPolygon(verts, rounding = rounding)
90         min = Point(-1f, -1f)
91         max = Point(1f, 1f)
92         assertInBounds(manualSquareRounded.cubics, min, max)
93 
94         val manualSquarePVRounded = RoundedPolygon(verts, perVertexRounding = perVtxRounded)
95         min = Point(-1f, -1f)
96         max = Point(1f, 1f)
97         assertInBounds(manualSquarePVRounded.cubics, min, max)
98     }
99 
100     @Test
101     fun featuresConstructorThrowsForTooFewFeatures() {
102         assertThrows(IllegalArgumentException::class.java) { RoundedPolygon(listOf()) }
103         val corner = Feature.Corner(listOf(Cubic.empty(0f, 0f)))
104         assertThrows(IllegalArgumentException::class.java) { RoundedPolygon(listOf(corner)) }
105     }
106 
107     @Test
108     fun featuresConstructorThrowsForNonContinuousFeatures() {
109         val cubic1 = Cubic.straightLine(0f, 0f, 1f, 0f)
110         val cubic2 = Cubic.straightLine(10f, 10f, 20f, 20f)
111         assertThrows(IllegalArgumentException::class.java) {
112             RoundedPolygon(listOf(Feature.buildEdge(cubic1), Feature.buildEdge(cubic2)))
113         }
114     }
115 
116     @Test
117     fun featuresConstructorReconstructsSquare() {
118         val base = RoundedPolygon.rectangle()
119         val actual = RoundedPolygon(base.features)
120         assertPolygonsEqualish(base, actual)
121     }
122 
123     @Test
124     fun featuresConstructorReconstructsRoundedSquare() {
125         val base = RoundedPolygon.rectangle(rounding = CornerRounding(0.5f, 0.2f))
126         val actual = RoundedPolygon(base.features)
127         assertPolygonsEqualish(base, actual)
128     }
129 
130     @Test
131     fun featuresConstructorReconstructsCircles() {
132         for (i in 3..20) {
133             val base = RoundedPolygon.circle(i)
134             val actual = RoundedPolygon(base.features)
135             assertPolygonsEqualish(base, actual)
136         }
137     }
138 
139     @Test
140     fun featuresConstructorReconstructsStars() {
141         for (i in 3..20) {
142             val base = RoundedPolygon.star(i)
143             val actual = RoundedPolygon(base.features)
144             assertPolygonsEqualish(base, actual)
145         }
146     }
147 
148     @Test
149     fun featuresConstructorReconstructsRoundedStars() {
150         for (i in 3..20) {
151             val base = RoundedPolygon.star(i, rounding = CornerRounding(0.5f, 0.2f))
152             val actual = RoundedPolygon(base.features)
153             assertPolygonsEqualish(base, actual)
154         }
155     }
156 
157     @Test
158     fun featuresConstructorReconstructsPill() {
159         val base = RoundedPolygon.pill()
160         val actual = RoundedPolygon(base.features)
161         assertPolygonsEqualish(base, actual)
162     }
163 
164     @Test
165     fun featuresConstructorReconstructsPillStar() {
166         val base = RoundedPolygon.pillStar(rounding = CornerRounding(0.5f, 0.2f))
167         val actual = RoundedPolygon(base.features)
168         assertPolygonsEqualish(base, actual)
169     }
170 
171     @Test
172     fun computeCenterTest() {
173         val polygon = RoundedPolygon(floatArrayOf(0f, 0f, 1f, 0f, 0f, 1f, 1f, 1f))
174 
175         assertEquals(0.5f, polygon.centerX, 1e-4f)
176         assertEquals(0.5f, polygon.centerY, 1e-4f)
177     }
178 
179     private fun pointsToFloats(points: List<Point>): FloatArray {
180         val result = FloatArray(points.size * 2)
181         var index = 0
182         for (point in points) {
183             result[index++] = point.x
184             result[index++] = point.y
185         }
186         return result
187     }
188 
189     @Test
190     fun roundingSpaceUsageTest() {
191         val p0 = Point(0f, 0f)
192         val p1 = Point(1f, 0f)
193         val p2 = Point(0.5f, 1f)
194         val pvRounding =
195             listOf(
196                 CornerRounding(1f, 0f),
197                 CornerRounding(1f, 1f),
198                 CornerRounding.Unrounded,
199             )
200         val polygon =
201             RoundedPolygon(
202                 vertices = pointsToFloats(listOf(p0, p1, p2)),
203                 perVertexRounding = pvRounding
204             )
205 
206         // Since there is not enough room in the p0 -> p1 side even for the roundings, we shouldn't
207         // take smoothing into account, so the corners should end in the middle point.
208         val lowerEdgeFeature = polygon.features.first { it is Feature.Edge } as Feature.Edge
209         assertEquals(1, lowerEdgeFeature.cubics.size)
210 
211         val lowerEdge = lowerEdgeFeature.cubics.first()
212         assertEqualish(0.5f, lowerEdge.anchor0X)
213         assertEqualish(0.0f, lowerEdge.anchor0Y)
214         assertEqualish(0.5f, lowerEdge.anchor1X)
215         assertEqualish(0.0f, lowerEdge.anchor1Y)
216     }
217 
218     /*
219      * In the following tests, we check how much was cut for the top left (vertex 0) and bottom
220      * left corner (vertex 3).
221      * In particular, both vertex are competing for space in the left side.
222      *
223      *   Vertex 0            Vertex 1
224      *      *---------------------*
225      *      |                     |
226      *      *---------------------*
227      *   Vertex 3            Vertex 2
228      */
229     private val points = 20
230 
231     @Test
232     fun unevenSmoothingTest() {
233         // Vertex 3 has the default 0.5 radius, 0 smoothing.
234         // Vertex 0 has 0.4 radius, and smoothing varying from 0 to 1.
235         repeat(points + 1) {
236             val smooth = it.toFloat() / points
237             doUnevenSmoothTest(
238                 CornerRounding(0.4f, smooth),
239                 expectedV0SX = 0.4f * (1 + smooth),
240                 expectedV0SY = (0.4f * (1 + smooth)).coerceAtMost(0.5f),
241                 expectedV3SY = 0.5f,
242             )
243         }
244     }
245 
246     @Test
247     fun unevenSmoothingTest2() {
248         // Vertex 3 has 0.2f radius and 0.2f smoothing, so it takes at most 0.4f
249         // Vertex 0 has 0.4f radius and smoothing varies from 0 to 1, when it reaches 0.5 it starts
250         // competing with vertex 3 for space.
251         repeat(points + 1) {
252             val smooth = it.toFloat() / points
253 
254             val smoothWantedV0 = 0.4f * smooth
255             val smoothWantedV3 = 0.2f
256 
257             // There is 0.4f room for smoothing
258             val factor = (0.4f / (smoothWantedV0 + smoothWantedV3)).coerceAtMost(1f)
259             doUnevenSmoothTest(
260                 CornerRounding(0.4f, smooth),
261                 expectedV0SX = 0.4f * (1 + smooth),
262                 expectedV0SY = 0.4f + factor * smoothWantedV0,
263                 expectedV3SY = 0.2f + factor * smoothWantedV3,
264                 rounding3 = CornerRounding(0.2f, 1f)
265             )
266         }
267     }
268 
269     @Test
270     fun unevenSmoothingTest3() {
271         // Vertex 3 has 0.6f radius.
272         // Vertex 0 has 0.4f radius and smoothing varies from 0 to 1. There is no room for smoothing
273         // on the segment between these vertices, but vertex 0 can still have smoothing on the top
274         // side.
275         repeat(points + 1) {
276             val smooth = it.toFloat() / points
277 
278             doUnevenSmoothTest(
279                 CornerRounding(0.4f, smooth),
280                 expectedV0SX = 0.4f * (1 + smooth),
281                 expectedV0SY = 0.4f,
282                 expectedV3SY = 0.6f,
283                 rounding3 = CornerRounding(0.6f)
284             )
285         }
286     }
287 
288     @Test
289     fun creatingFullSizeTest() {
290         val radius = 400f
291         val innerRadiusFactor = 0.35f
292         val innerRadius = radius * innerRadiusFactor
293         val roundingFactor = 0.32f
294 
295         val fullSizeShape =
296             RoundedPolygon.star(
297                     numVerticesPerRadius = 4,
298                     radius,
299                     innerRadius,
300                     rounding = CornerRounding(radius * roundingFactor),
301                     innerRounding = CornerRounding(radius * roundingFactor),
302                     centerX = radius,
303                     centerY = radius
304                 )
305                 .transformed { x, y ->
306                     TransformResult((x - radius) / radius, (y - radius) / radius)
307                 }
308 
309         val canonicalShape =
310             RoundedPolygon.star(
311                 numVerticesPerRadius = 4,
312                 1f,
313                 innerRadiusFactor,
314                 rounding = CornerRounding(roundingFactor),
315                 innerRounding = CornerRounding(roundingFactor)
316             )
317 
318         val cubics = canonicalShape.cubics
319         val cubics1 = fullSizeShape.cubics
320         assertEquals(cubics.size, cubics1.size)
321         cubics.zip(cubics1).forEach { (cubic, cubic1) ->
322             assertEqualish(cubic.anchor0X, cubic1.anchor0X)
323             assertEqualish(cubic.anchor0Y, cubic1.anchor0Y)
324             assertEqualish(cubic.anchor1X, cubic1.anchor1X)
325             assertEqualish(cubic.anchor1Y, cubic1.anchor1Y)
326             assertEqualish(cubic.control0X, cubic1.control0X)
327             assertEqualish(cubic.control0Y, cubic1.control0Y)
328             assertEqualish(cubic.control1X, cubic1.control1X)
329             assertEqualish(cubic.control1Y, cubic1.control1Y)
330         }
331     }
332 
333     private fun doUnevenSmoothTest(
334         // Corner rounding parameter for vertex 0 (top left)
335         rounding0: CornerRounding,
336         expectedV0SX: Float, // Expected total cut from vertex 0 towards vertex 1
337         expectedV0SY: Float, // Expected total cut from vertex 0 towards vertex 3
338         expectedV3SY: Float, // Expected total cut from vertex 3 towards vertex 0
339         // Corner rounding parameter for vertex 3 (bottom left)
340         rounding3: CornerRounding = CornerRounding(0.5f)
341     ) {
342         val p0 = Point(0f, 0f)
343         val p1 = Point(5f, 0f)
344         val p2 = Point(5f, 1f)
345         val p3 = Point(0f, 1f)
346 
347         val pvRounding =
348             listOf(
349                 rounding0,
350                 CornerRounding.Unrounded,
351                 CornerRounding.Unrounded,
352                 rounding3,
353             )
354         val polygon =
355             RoundedPolygon(
356                 vertices = pointsToFloats(listOf(p0, p1, p2, p3)),
357                 perVertexRounding = pvRounding
358             )
359         val (e01, _, _, e30) = polygon.features.filterIsInstance<Feature.Edge>()
360         val msg = "r0 = ${show(rounding0)}, r3 = ${show(rounding3)}"
361         assertEqualish(expectedV0SX, e01.cubics.first().anchor0X, msg)
362         assertEqualish(expectedV0SY, e30.cubics.first().anchor1Y, msg)
363         assertEqualish(expectedV3SY, 1f - e30.cubics.first().anchor0Y, msg)
364     }
365 
366     private fun show(cr: CornerRounding) = "(r=${cr.radius}, s=${cr.smoothing})"
367 }
368