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