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 /**
20  * MeasuredFeatures contains a list of all features in a polygon along with the [0..1] progress at
21  * that feature
22  */
23 internal typealias MeasuredFeatures = List<ProgressableFeature>
24 
25 internal data class ProgressableFeature(val progress: Float, val feature: Feature)
26 
27 /** featureMapper creates a mapping between the "features" (rounded corners) of two shapes */
28 internal fun featureMapper(features1: MeasuredFeatures, features2: MeasuredFeatures): DoubleMapper {
29     // We only use corners for this mapping.
30     val filteredFeatures1 = buildList {
31         // Performance: Builds the list by avoiding creating an unnecessary Iterator to iterate
32         // through the features1 List.
33         for (i in features1.indices) {
34             if (features1[i].feature is Feature.Corner) {
35                 add(features1[i])
36             }
37         }
38     }
39     val filteredFeatures2 = buildList {
40         // Performance: Builds the list by avoiding creating an unnecessary Iterator to iterate
41         // through the features2 List.
42         for (i in features2.indices) {
43             if (features2[i].feature is Feature.Corner) {
44                 add(features2[i])
45             }
46         }
47     }
48 
49     val featureProgressMapping = doMapping(filteredFeatures1, filteredFeatures2)
50 
51     debugLog(LOG_TAG) { featureProgressMapping.joinToString { "${it.first} -> ${it.second}" } }
52     return DoubleMapper(*featureProgressMapping.toTypedArray()).also { dm ->
53         debugLog(LOG_TAG) {
54             val N = 10
55             "Map: " +
56                 (0..N).joinToString { i -> (dm.map(i.toFloat() / N)).toStringWithLessPrecision() } +
57                 "\nMb : " +
58                 (0..N).joinToString { i ->
59                     (dm.mapBack(i.toFloat() / N)).toStringWithLessPrecision()
60                 }
61         }
62     }
63 }
64 
65 internal data class DistanceVertex(
66     val distance: Float,
67     val f1: ProgressableFeature,
68     val f2: ProgressableFeature
69 )
70 
71 /**
72  * Returns a mapping of the features between features1 and features2. The return is a list of pairs
73  * in which the first element is the progress of a feature in features1 and the second element is
74  * the progress of the feature in features2 that we mapped it to. The list is sorted by the first
75  * element. To do this:
76  * 1) Compute the distance for all pairs of features in (features1 x features2),
77  * 2) Sort ascending by by such distance
78  * 3) Try to add them, from smallest distance to biggest, ensuring that: a) The features we are
79  *    mapping haven't been mapped yet. b) We are not adding a crossing in the mapping. Since the
80  *    mapping is sorted by the first element of each pair, this means that the second elements of
81  *    each pair are monotonically increasing, except maybe one time (Counting all pair of
82  *    consecutive elements, and the last element to first element).
83  */
doMappingnull84 internal fun doMapping(
85     features1: List<ProgressableFeature>,
86     features2: List<ProgressableFeature>
87 ): List<Pair<Float, Float>> {
88     debugLog(LOG_TAG) { "Shape1 progresses: " + features1.map { it.progress }.joinToString() }
89     debugLog(LOG_TAG) { "Shape2 progresses: " + features2.map { it.progress }.joinToString() }
90     val distanceVertexList =
91         buildList {
92                 for (f1 in features1) {
93                     for (f2 in features2) {
94                         val d = featureDistSquared(f1.feature, f2.feature)
95                         if (d != Float.MAX_VALUE) add(DistanceVertex(d, f1, f2))
96                     }
97                 }
98             }
99             .sortedBy { it.distance }
100 
101     // Special cases.
102     if (distanceVertexList.isEmpty()) return IdentityMapping
103     if (distanceVertexList.size == 1)
104         return distanceVertexList.first().let {
105             val f1 = it.f1.progress
106             val f2 = it.f2.progress
107             listOf(f1 to f2, (f1 + 0.5f) % 1f to (f2 + 0.5f) % 1f)
108         }
109 
110     return MappingHelper().apply { distanceVertexList.forEach { addMapping(it.f1, it.f2) } }.mapping
111 }
112 
113 private val IdentityMapping = listOf(0f to 0f, 0.5f to 0.5f)
114 
115 private class MappingHelper() {
116     // List of mappings from progress in the start shape to progress in the end shape.
117     // We keep this list sorted by the first element.
118     val mapping = mutableListOf<Pair<Float, Float>>()
119 
120     // Which features in the start shape have we used and which in the end shape.
121     private val usedF1 = mutableSetOf<ProgressableFeature>()
122     private val usedF2 = mutableSetOf<ProgressableFeature>()
123 
addMappingnull124     fun addMapping(f1: ProgressableFeature, f2: ProgressableFeature) {
125         // We don't want to map the same feature twice.
126         if (f1 in usedF1 || f2 in usedF2) return
127 
128         // Ret is sorted, find where we need to insert this new mapping.
129         val index = mapping.binarySearchBy(f1.progress) { it.first }
130         require(index < 0) { "There can't be two features with the same progress" }
131 
132         val insertionIndex = -index - 1
133         val n = mapping.size
134 
135         // We can always add the first 1 element
136         if (n >= 1) {
137             val (before1, before2) = mapping[(insertionIndex + n - 1) % n]
138             val (after1, after2) = mapping[insertionIndex % n]
139 
140             // We don't want features that are way too close to each other, that will make the
141             // DoubleMapper unstable
142             if (
143                 progressDistance(f1.progress, before1) < DistanceEpsilon ||
144                     progressDistance(f1.progress, after1) < DistanceEpsilon ||
145                     progressDistance(f2.progress, before2) < DistanceEpsilon ||
146                     progressDistance(f2.progress, after2) < DistanceEpsilon
147             ) {
148                 return
149             }
150 
151             // When we have 2 or more elements, we need to ensure we are not adding extra crossings.
152             if (n > 1 && !progressInRange(f2.progress, before2, after2)) return
153         }
154 
155         // All good, we can add the mapping.
156         mapping.add(insertionIndex, f1.progress to f2.progress)
157         usedF1.add(f1)
158         usedF2.add(f2)
159     }
160 }
161 
162 /**
163  * Returns distance along overall shape between two Features on the two different shapes. This
164  * information is used to determine how to map features (and the curves that make up those
165  * features).
166  */
featureDistSquarednull167 internal fun featureDistSquared(f1: Feature, f2: Feature): Float {
168     // TODO: We might want to enable concave-convex matching in some situations. If so, the
169     //  approach below will not work
170     if (f1 is Feature.Corner && f2 is Feature.Corner && f1.convex != f2.convex) {
171         // Simple hack to force all features to map only to features of the same concavity, by
172         // returning an infinitely large distance in that case
173         debugLog(LOG_TAG) { "*** Feature distance ∞ for convex-vs-concave corners" }
174         return Float.MAX_VALUE
175     }
176     return (featureRepresentativePoint(f1) - featureRepresentativePoint(f2)).getDistanceSquared()
177 }
178 
179 // TODO: b/378441547 - Move to explicit parameter / expose?
featureRepresentativePointnull180 internal fun featureRepresentativePoint(feature: Feature): Point {
181     val x = (feature.cubics.first().anchor0X + feature.cubics.last().anchor1X) / 2f
182     val y = (feature.cubics.first().anchor0Y + feature.cubics.last().anchor1Y) / 2f
183     return Point(x, y)
184 }
185 
186 private val LOG_TAG = "FeatureMapping"
187