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