1 /*
2  * Copyright 2024 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  * Convert cubics to Features in a 1:1 mapping of [Cubic.asFeature] unless
21  * - two subsequent cubics are not continuous, in which case an empty corner needs to be added in
22  *   between. Example for C1, C2: /C1\/C2\ -> /C\C/C\.
23  * - multiple subsequent cubics can be expressed as a single feature. Example for C1, C2:
24  *   --C1----C2-- -> -----E----. One exception to the latter rule is for the first and last cubic,
25  *   that remain the same in order to persist the start position. Assumes the list of cubics is
26  *   continuous.
27  */
detectFeaturesnull28 internal fun detectFeatures(cubics: List<Cubic>): List<Feature> {
29     if (cubics.isEmpty()) return emptyList()
30 
31     // TODO: b/372651969 Try different heuristics for corner grouping
32     return buildList {
33         var current = cubics.first()
34 
35         // Do one roundabout in which (current == last, next == first) is the last iteration.
36         // Just like a snowball, subsequent cubics that align to one feature merge until
37         // the streak breaks, the result is added, and a new streak starts.
38         for (i in cubics.indices) {
39             val next = cubics[(i + 1) % (cubics.size)]
40 
41             if (i < cubics.lastIndex && current.alignsIshWith(next)) {
42                 current = Cubic.extend(current, next)
43                 continue
44             }
45 
46             add(current.asFeature(next))
47 
48             if (!current.smoothesIntoIsh(next)) {
49                 add(Cubic.empty(current.anchor1X, current.anchor1Y).asFeature(next))
50             }
51 
52             current = next
53         }
54     }
55 }
56 
57 /**
58  * Convert to [Feature.Edge] if this cubic describes a straight line, otherwise to a
59  * [Feature.Corner]. Corner convexity is determined by [convex].
60  */
asFeaturenull61 internal fun Cubic.asFeature(next: Cubic): Feature =
62     if (straightIsh()) Feature.Edge(listOf(this)) else Feature.Corner(listOf(this), convexTo(next))
63 
64 /** Determine if the cubic is close to a straight line. Empty cubics don't count as straightIsh. */
65 internal fun Cubic.straightIsh(): Boolean =
66     !zeroLength() &&
67         collinearIsh(
68             anchor0X,
69             anchor0Y,
70             anchor1X,
71             anchor1Y,
72             control0X,
73             control0Y,
74             RelaxedDistanceEpsilon
75         ) &&
76         collinearIsh(
77             anchor0X,
78             anchor0Y,
79             anchor1X,
80             anchor1Y,
81             control1X,
82             control1Y,
83             RelaxedDistanceEpsilon
84         )
85 
86 /**
87  * Determines if next is a smooth continuation of this cubic. Smooth meaning that the first control
88  * point of next is a reflection of this' second control point, similar to the S/s or t/T command in
89  * svg paths https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths#b%C3%A9zier_curves
90  */
91 internal fun Cubic.smoothesIntoIsh(next: Cubic): Boolean =
92     collinearIsh(
93         control1X,
94         control1Y,
95         next.control0X,
96         next.control0Y,
97         anchor1X,
98         anchor1Y,
99         RelaxedDistanceEpsilon
100     )
101 
102 /**
103  * Determines if all of this' points align with next's points. For straight lines, this is the same
104  * as if next was a continuation of this.
105  */
106 internal fun Cubic.alignsIshWith(next: Cubic): Boolean =
107     straightIsh() && next.straightIsh() && smoothesIntoIsh(next) ||
108         zeroLength() ||
109         next.zeroLength()
110 
111 /** Create a new cubic by extending A to B's second anchor point */
112 private fun Cubic.Companion.extend(a: Cubic, b: Cubic): Cubic {
113     return if (a.zeroLength())
114         Cubic(
115             a.anchor0X,
116             a.anchor0Y,
117             b.control0X,
118             b.control0Y,
119             b.control1X,
120             b.control1Y,
121             b.anchor1X,
122             b.anchor1Y
123         )
124     else
125         Cubic(
126             a.anchor0X,
127             a.anchor0Y,
128             a.control0X,
129             a.control0Y,
130             a.control1X,
131             a.control1Y,
132             b.anchor1X,
133             b.anchor1Y
134         )
135 }
136