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