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.annotation.FloatRange 20 import androidx.collection.FloatFloatPair 21 import androidx.collection.FloatList 22 import androidx.collection.MutableFloatList 23 24 internal class MeasuredPolygon : AbstractList<MeasuredPolygon.MeasuredCubic> { 25 private val measurer: Measurer 26 private val cubics: List<MeasuredCubic> 27 val features: List<ProgressableFeature> 28 29 private constructor( 30 measurer: Measurer, 31 features: List<ProgressableFeature>, 32 cubics: List<Cubic>, 33 outlineProgress: FloatList 34 ) { 35 require(outlineProgress.size == cubics.size + 1) { 36 "Outline progress size is expected to be the cubics size + 1" 37 } 38 require(outlineProgress.first() == 0f) { 39 "First outline progress value is expected to be zero" 40 } 41 require(outlineProgress.last() == 1f) { 42 "Last outline progress value is expected to be one" 43 } 44 this.measurer = measurer 45 this.features = features 46 47 if (DEBUG) { 48 debugLog(LOG_TAG) { 49 "CTOR: cubics = " + 50 cubics.joinToString() + 51 "\nCTOR: op = " + 52 outlineProgress.joinToString() 53 } 54 } 55 val measuredCubics = mutableListOf<MeasuredCubic>() 56 var startOutlineProgress = 0f 57 for (index in cubics.indices) { 58 // Filter out "empty" cubics 59 if ((outlineProgress[index + 1] - outlineProgress[index]) > DistanceEpsilon) { 60 measuredCubics.add( 61 MeasuredCubic(cubics[index], startOutlineProgress, outlineProgress[index + 1]) 62 ) 63 // The next measured cubic will start exactly where this one ends. 64 startOutlineProgress = outlineProgress[index + 1] 65 } 66 } 67 // We could have removed empty cubics at the end. Ensure the last measured cubic ends at 1f 68 measuredCubics[measuredCubics.lastIndex].updateProgressRange(endOutlineProgress = 1f) 69 this.cubics = measuredCubics 70 } 71 72 /** 73 * A MeasuredCubic holds information about the cubic itself, the feature (if any) associated 74 * with it, and the outline progress values (start and end) for the cubic. This information is 75 * used to match cubics between shapes that lie at similar outline progress positions along 76 * their respective shapes (after matching features and shifting). 77 * 78 * Outline progress is a value in [0..1) that represents the distance traveled along the overall 79 * outline path of the shape. 80 */ 81 internal inner class MeasuredCubic( 82 val cubic: Cubic, 83 @FloatRange(from = 0.0, to = 1.0) startOutlineProgress: Float, 84 @FloatRange(from = 0.0, to = 1.0) endOutlineProgress: Float, 85 ) { 86 init { 87 require(endOutlineProgress >= startOutlineProgress) { 88 "endOutlineProgress is expected to be equal or greater than startOutlineProgress" 89 } 90 } 91 92 val measuredSize = measurer.measureCubic(cubic) 93 94 var startOutlineProgress = startOutlineProgress 95 private set 96 97 var endOutlineProgress = endOutlineProgress 98 private set 99 100 internal fun updateProgressRange( 101 startOutlineProgress: Float = this.startOutlineProgress, 102 endOutlineProgress: Float = this.endOutlineProgress 103 ) { 104 require(endOutlineProgress >= startOutlineProgress) { 105 "endOutlineProgress is expected to be equal or greater than startOutlineProgress" 106 } 107 this.startOutlineProgress = startOutlineProgress 108 this.endOutlineProgress = endOutlineProgress 109 } 110 111 /** Cut this MeasuredCubic into two MeasuredCubics at the given outline progress value. */ 112 fun cutAtProgress(cutOutlineProgress: Float): Pair<MeasuredCubic, MeasuredCubic> { 113 // Floating point errors further up can cause cutOutlineProgress to land just 114 // slightly outside of the start/end progress for this cubic, so we limit it 115 // to those bounds to avoid further errors later 116 val boundedCutOutlineProgress = 117 cutOutlineProgress.coerceIn(startOutlineProgress, endOutlineProgress) 118 val outlineProgressSize = endOutlineProgress - startOutlineProgress 119 val progressFromStart = boundedCutOutlineProgress - startOutlineProgress 120 121 // Note that in earlier parts of the computation, we have empty MeasuredCubics (cubics 122 // with progressSize == 0f), but those cubics are filtered out before this method is 123 // called. 124 val relativeProgress = progressFromStart / outlineProgressSize 125 val t = measurer.findCubicCutPoint(cubic, relativeProgress * measuredSize) 126 require(t in 0f..1f) { "Cubic cut point is expected to be between 0 and 1" } 127 128 debugLog(LOG_TAG) { 129 "cutAtProgress: progress = $boundedCutOutlineProgress / " + 130 "this = [$startOutlineProgress .. $endOutlineProgress] / " + 131 "ps = $progressFromStart / rp = $relativeProgress / t = $t" 132 } 133 134 // c1/c2 are the two new cubics, then we return MeasuredCubics created from them 135 val (c1, c2) = cubic.split(t) 136 return MeasuredCubic(c1, startOutlineProgress, boundedCutOutlineProgress) to 137 MeasuredCubic(c2, boundedCutOutlineProgress, endOutlineProgress) 138 } 139 140 override fun toString(): String { 141 return "MeasuredCubic(outlineProgress=" + 142 "[$startOutlineProgress .. $endOutlineProgress], " + 143 "size=$measuredSize, cubic=$cubic)" 144 } 145 } 146 147 /** 148 * Finds the point in the input list of measured cubics that pass the given outline progress, 149 * and generates a new MeasuredPolygon (equivalent to this), that starts at that point. This 150 * usually means cutting the cubic that crosses the outline progress (unless the cut is at one 151 * of its ends) For example, given outline progress 0.4f and measured cubics on these outline 152 * progress ranges: 153 * 154 * c1 [0 -> 0.2] c2 [0.2 -> 0.5] c3 [0.5 -> 1.0] 155 * 156 * c2 will be cut in two, at the given outline progress, we can name these c2a [0.2 -> 0.4] and 157 * c2b [0.4 -> 0.5] 158 * 159 * The return then will have measured cubics [c2b, c3, c1, c2a], and they will have their 160 * outline progress ranges adjusted so the new list starts at 0. c2b [0 -> 0.1] c3 [0.1 -> 0.6] 161 * c1 [0.6 -> 0.8] c2a [0.8 -> 1.0] 162 */ 163 fun cutAndShift(cuttingPoint: Float): MeasuredPolygon { 164 require(cuttingPoint in 0f..1f) { "Cutting point is expected to be between 0 and 1" } 165 if (cuttingPoint < DistanceEpsilon) return this 166 167 // Find the index of cubic we want to cut 168 val targetIndex = 169 cubics.indexOfFirst { cuttingPoint in it.startOutlineProgress..it.endOutlineProgress } 170 val target = cubics[targetIndex] 171 if (DEBUG) { 172 cubics.forEachIndexed { index, cubic -> 173 debugLog(LOG_TAG) { "cut&Shift | cubic #$index : $cubic " } 174 } 175 debugLog(LOG_TAG) { 176 "cut&Shift, cuttingPoint = $cuttingPoint, target = ($targetIndex) $target" 177 } 178 } 179 // Cut the target cubic. 180 // b1, b2 are two resulting cubics after cut 181 val (b1, b2) = target.cutAtProgress(cuttingPoint) 182 debugLog(LOG_TAG) { "Split | $target -> $b1 & $b2" } 183 184 // Construct the list of the cubics we need: 185 // * The second part of the target cubic (after the cut) 186 // * All cubics after the target, until the end + All cubics from the start, before the 187 // target cubic 188 // * The first part of the target cubic (before the cut) 189 val retCubics = mutableListOf(b2.cubic) 190 for (i in 1 until cubics.size) { 191 retCubics.add(cubics[(i + targetIndex) % cubics.size].cubic) 192 } 193 retCubics.add(b1.cubic) 194 195 // Construct the array of outline progress. 196 // For example, if we have 3 cubics with outline progress [0 .. 0.3], [0.3 .. 0.8] & 197 // [0.8 .. 1.0], and we cut + shift at 0.6: 198 // 0. 0123456789 199 // |--|--/-|-| 200 // The outline progresses will start at 0 (the cutting point, that shifs to 0.0), 201 // then 0.8 - 0.6 = 0.2, then 1 - 0.6 = 0.4, then 0.3 - 0.6 + 1 = 0.7, 202 // then 1 (the cutting point again), 203 // all together: (0.0, 0.2, 0.4, 0.7, 1.0) 204 val retOutlineProgress = MutableFloatList(cubics.size + 2) 205 for (index in 0 until cubics.size + 2) { 206 retOutlineProgress.add( 207 when (index) { 208 0 -> 0f 209 cubics.size + 1 -> 1f 210 else -> { 211 val cubicIndex = (targetIndex + index - 1) % cubics.size 212 positiveModulo(cubics[cubicIndex].endOutlineProgress - cuttingPoint, 1f) 213 } 214 } 215 ) 216 } 217 218 // Shift the feature's outline progress too. 219 val newFeatures = buildList { 220 for (i in features.indices) { 221 add( 222 ProgressableFeature( 223 positiveModulo(features[i].progress - cuttingPoint, 1f), 224 features[i].feature 225 ) 226 ) 227 } 228 } 229 230 // Filter out all empty cubics (i.e. start and end anchor are (almost) the same point.) 231 return MeasuredPolygon(measurer, newFeatures, retCubics, retOutlineProgress) 232 } 233 234 // Implementation of AbstractList. 235 override val size 236 get() = cubics.size 237 238 override fun get(index: Int) = cubics[index] 239 240 companion object { 241 internal fun measurePolygon(measurer: Measurer, polygon: RoundedPolygon): MeasuredPolygon { 242 val cubics = mutableListOf<Cubic>() 243 val featureToCubic = mutableListOf<Pair<Feature, Int>>() 244 245 // Get the cubics from the polygon, at the same time, extract the features and keep a 246 // reference to the representative cubic we will use. 247 for (featureIndex in polygon.features.indices) { 248 val feature = polygon.features[featureIndex] 249 for (cubicIndex in feature.cubics.indices) { 250 if (feature is Feature.Corner && cubicIndex == feature.cubics.size / 2) { 251 featureToCubic.add(feature to cubics.size) 252 } 253 cubics.add(feature.cubics[cubicIndex]) 254 } 255 } 256 // TODO(performance): Make changes to satisfy the lint warnings for unnecessary 257 // iterators creation. 258 val measures = 259 cubics.scan(0f) { measure, cubic -> 260 measure + 261 measurer.measureCubic(cubic).also { 262 require(it >= 0f) { 263 "Measured cubic is expected to be greater or equal to zero" 264 } 265 } 266 } 267 val totalMeasure = measures.last() 268 269 // Equivalent to `measures.map { it / totalMeasure }` but without Iterator allocation. 270 val outlineProgress = MutableFloatList(measures.size) 271 for (i in measures.indices) { 272 outlineProgress.add(measures[i] / totalMeasure) 273 } 274 275 debugLog(LOG_TAG) { "Total size: $totalMeasure" } 276 277 val features = buildList { 278 for (i in featureToCubic.indices) { 279 val ix = featureToCubic[i].second 280 add( 281 ProgressableFeature( 282 positiveModulo((outlineProgress[ix] + outlineProgress[ix + 1]) / 2, 1f), 283 featureToCubic[i].first 284 ) 285 ) 286 } 287 } 288 289 return MeasuredPolygon(measurer, features, cubics, outlineProgress) 290 } 291 } 292 } 293 294 /** 295 * Interface for measuring a cubic. Implementations can use whatever algorithm desired to produce 296 * these measurement values. 297 */ 298 internal interface Measurer { 299 300 /** 301 * Returns size of given cubic, according to however the implementation wants to measure the 302 * size (angle, length, etc). It has to be greater or equal to 0. 303 */ measureCubicnull304 fun measureCubic(c: Cubic): Float 305 306 /** 307 * Given a cubic and a measure that should be between 0 and the value returned by measureCubic 308 * (If not, it will be capped), finds the parameter t of the cubic at which that measure is 309 * reached. 310 */ 311 fun findCubicCutPoint(c: Cubic, m: Float): Float 312 } 313 314 /** 315 * Approximates the arc lengths of cubics by splitting the arc into segments and calculating their 316 * sizes. The more segments, the more accurate the result will be to the true arc length. The 317 * default implementation has at least 98.5% accuracy on the case of a circular arc, which is the 318 * worst case for our standard shapes. 319 */ 320 internal class LengthMeasurer() : Measurer { 321 // The minimum number needed to achieve up to 98.5% accuracy from the true arc length 322 // See PolygonMeasureTest.measureCircle 323 private val segments = 3 324 325 override fun measureCubic(c: Cubic): Float { 326 return closestProgressTo(c, Float.POSITIVE_INFINITY).second 327 } 328 329 override fun findCubicCutPoint(c: Cubic, m: Float): Float { 330 return closestProgressTo(c, m).first 331 } 332 333 private fun closestProgressTo(cubic: Cubic, threshold: Float): FloatFloatPair { 334 var total = 0f 335 var remainder = threshold 336 var prev = Point(cubic.anchor0X, cubic.anchor0Y) 337 338 for (i in 1..segments) { 339 val progress = i.toFloat() / segments 340 val point = cubic.pointOnCurve(progress) 341 val segment = (point - prev).getDistance() 342 343 if (segment >= remainder) { 344 return FloatFloatPair(progress - (1.0f - remainder / segment) / segments, threshold) 345 } 346 347 remainder -= segment 348 total += segment 349 prev = point 350 } 351 352 return FloatFloatPair(1.0f, total) 353 } 354 } 355 356 private val LOG_TAG = "PolygonMeasure" 357