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