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