1 /*
2  * Copyright (C) 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.ink.geometry
18 
19 import androidx.ink.brush.Brush
20 import androidx.ink.brush.StockBrushes
21 import androidx.ink.strokes.Stroke
22 import androidx.ink.strokes.testing.buildStrokeInputBatchFromPoints
23 import com.google.common.truth.Truth.assertThat
24 import kotlin.test.assertFailsWith
25 import kotlin.test.assertNotNull
26 import org.junit.Test
27 import org.junit.runner.RunWith
28 import org.junit.runners.JUnit4
29 
30 @RunWith(JUnit4::class)
31 class PartitionedMeshTest {
32 
33     @Test
computeBoundingBox_shouldBeEmptynull34     fun computeBoundingBox_shouldBeEmpty() {
35         val partitionedMesh = PartitionedMesh()
36 
37         assertThat(partitionedMesh.computeBoundingBox()).isNull()
38     }
39 
40     @Test
computeBoundingBox_reusesAllocationsnull41     fun computeBoundingBox_reusesAllocations() {
42         val partitionedMesh = buildTestStrokeShape()
43 
44         val boundingBox = partitionedMesh.computeBoundingBox()
45         assertThat(partitionedMesh.computeBoundingBox()).isSameInstanceAs(boundingBox)
46     }
47 
48     @Test
getRenderGroupCount_whenEmptyShape_shouldBeZeronull49     fun getRenderGroupCount_whenEmptyShape_shouldBeZero() {
50         val partitionedMesh = PartitionedMesh()
51 
52         assertThat(partitionedMesh.getRenderGroupCount()).isEqualTo(0)
53     }
54 
55     @Test
getOutlineCount_whenEmptyShape_shouldThrownull56     fun getOutlineCount_whenEmptyShape_shouldThrow() {
57         val partitionedMesh = PartitionedMesh()
58 
59         assertFailsWith<IllegalArgumentException> {
60             @Suppress("Range") partitionedMesh.getOutlineCount(-1)
61         }
62         assertFailsWith<IllegalArgumentException> { partitionedMesh.getOutlineCount(0) }
63         assertFailsWith<IllegalArgumentException> { partitionedMesh.getOutlineCount(1) }
64     }
65 
66     @Test
getOutlineVertexCount_whenEmptyShape_shouldThrownull67     fun getOutlineVertexCount_whenEmptyShape_shouldThrow() {
68         val partitionedMesh = PartitionedMesh()
69 
70         assertFailsWith<IllegalArgumentException> {
71             @Suppress("Range") partitionedMesh.getOutlineVertexCount(-1, 0)
72         }
73         assertFailsWith<IllegalArgumentException> { partitionedMesh.getOutlineVertexCount(0, 0) }
74         assertFailsWith<IllegalArgumentException> { partitionedMesh.getOutlineVertexCount(1, 0) }
75     }
76 
77     @Test
populateOutlinePosition_whenEmptyShape_shouldThrownull78     fun populateOutlinePosition_whenEmptyShape_shouldThrow() {
79         val partitionedMesh = PartitionedMesh()
80 
81         assertFailsWith<IllegalArgumentException> {
82             @Suppress("Range") partitionedMesh.populateOutlinePosition(-1, 0, 0, MutableVec())
83         }
84         assertFailsWith<IllegalArgumentException> {
85             partitionedMesh.populateOutlinePosition(0, 0, 0, MutableVec())
86         }
87         assertFailsWith<IllegalArgumentException> {
88             partitionedMesh.populateOutlinePosition(1, 0, 0, MutableVec())
89         }
90     }
91 
92     @Test
toString_returnsAStringnull93     fun toString_returnsAString() {
94         val string = PartitionedMesh().toString()
95 
96         // Not elaborate checks - this test mainly exists to ensure that toString doesn't crash.
97         assertThat(string).contains("PartitionedMesh")
98         assertThat(string).contains("bounds")
99         assertThat(string).contains("meshes")
100         assertThat(string).contains("nativePointer")
101     }
102 
103     @Test
populateOutlinePosition_withStrokeShape_shouldBeWithinBoundsnull104     fun populateOutlinePosition_withStrokeShape_shouldBeWithinBounds() {
105         val shape = buildTestStrokeShape()
106 
107         assertThat(shape.getRenderGroupCount()).isEqualTo(1)
108         assertThat(shape.getOutlineCount(0)).isEqualTo(1)
109         assertThat(shape.getOutlineVertexCount(0, 0)).isGreaterThan(2)
110 
111         val bounds = assertNotNull(shape.computeBoundingBox())
112 
113         val p = MutableVec()
114         for (outlineVertexIndex in 0 until shape.getOutlineVertexCount(0, 0)) {
115             shape.populateOutlinePosition(groupIndex = 0, outlineIndex = 0, outlineVertexIndex, p)
116             assertThat(p.x).isAtLeast(bounds.xMin)
117             assertThat(p.y).isAtLeast(bounds.yMin)
118             assertThat(p.x).isAtMost(bounds.xMax)
119             assertThat(p.y).isAtMost(bounds.yMax)
120         }
121     }
122 
123     @Test
populateOutlinePosition_whenBadIndex_shouldThrownull124     fun populateOutlinePosition_whenBadIndex_shouldThrow() {
125         val shape = buildTestStrokeShape()
126 
127         val p = MutableVec()
128         assertFailsWith<IllegalArgumentException> {
129             @Suppress("Range") shape.populateOutlinePosition(-1, 0, 0, p)
130         }
131         assertFailsWith<IllegalArgumentException> { shape.populateOutlinePosition(5, 0, 0, p) }
132         assertFailsWith<IllegalArgumentException> {
133             @Suppress("Range") shape.populateOutlinePosition(0, -1, 0, p)
134         }
135         assertFailsWith<IllegalArgumentException> { shape.populateOutlinePosition(0, 5, 0, p) }
136         assertFailsWith<IllegalArgumentException> {
137             @Suppress("Range") shape.populateOutlinePosition(0, 0, -1, p)
138         }
139         assertFailsWith<IllegalArgumentException> { shape.populateOutlinePosition(0, 1, 999, p) }
140     }
141 
142     @Test
meshFormat_forTestShape_isEquivalentToMeshFormatOfFirstMeshnull143     fun meshFormat_forTestShape_isEquivalentToMeshFormatOfFirstMesh() {
144         val partitionedMesh = buildTestStrokeShape()
145         assertThat(partitionedMesh.getRenderGroupCount()).isEqualTo(1)
146         val shapeFormat = partitionedMesh.renderGroupFormat(0)
147         val meshes = partitionedMesh.renderGroupMeshes(0)
148         assertThat(meshes).isNotEmpty()
149         assertThat(shapeFormat).isNotNull()
150         assertThat(meshes[0].format.isUnpackedEquivalent(shapeFormat)).isTrue()
151     }
152 
153     /**
154      * Verifies that [PartitionedMesh.computeCoverage] calls the correct JNI method for
155      * [PartitionedMesh] and [Triangle].
156      */
157     @Test
computeCoverage_forPartitionedMeshAndTriangle_callsJniAndReturnsFloatnull158     fun computeCoverage_forPartitionedMeshAndTriangle_callsJniAndReturnsFloat() {
159         val partitionedMesh = buildTestStrokeShape()
160         val intersectingTriangle =
161             ImmutableTriangle(
162                 p0 = ImmutableVec(15f, 4f),
163                 p1 = ImmutableVec(20f, 4f),
164                 p2 = ImmutableVec(20f, 5f),
165             )
166         val externalTriangle =
167             ImmutableTriangle(
168                 p0 = ImmutableVec(100f, 200f),
169                 p1 = ImmutableVec(300f, 400f),
170                 p2 = ImmutableVec(100f, 700f),
171             )
172 
173         assertThat(partitionedMesh.computeCoverage(intersectingTriangle)).isGreaterThan(0f)
174         assertThat(partitionedMesh.computeCoverage(externalTriangle)).isEqualTo(0f)
175         assertThat(partitionedMesh.computeCoverage(externalTriangle, SCALE_TRANSFORM)).isEqualTo(0f)
176     }
177 
178     /**
179      * Verifies that [PartitionedMesh.computeCoverage] calls the correct JNI method for
180      * [PartitionedMesh] and [Box].
181      */
182     @Test
computeCoverage_forPartitionedMeshAndBox_callsJniAndReturnsFloatnull183     fun computeCoverage_forPartitionedMeshAndBox_callsJniAndReturnsFloat() {
184         val partitionedMesh = buildTestStrokeShape()
185         val intersectingBox =
186             ImmutableBox.fromTwoPoints(ImmutableVec(0f, 0f), ImmutableVec(100f, 100f))
187         val externalBox =
188             ImmutableBox.fromTwoPoints(ImmutableVec(100f, 200f), ImmutableVec(300f, 400f))
189 
190         assertThat(partitionedMesh.computeCoverage(intersectingBox)).isGreaterThan(0f)
191         assertThat(partitionedMesh.computeCoverage(externalBox)).isEqualTo(0f)
192         assertThat(partitionedMesh.computeCoverage(externalBox, SCALE_TRANSFORM)).isEqualTo(0f)
193     }
194 
195     /**
196      * Verifies that [PartitionedMesh.computeCoverage] calls the correct JNI method for
197      * [PartitionedMesh] and [Parallelogram].
198      */
199     @Test
computeCoverage_forPartitionedMeshAndParallelogram_callsJniAndReturnsFloatnull200     fun computeCoverage_forPartitionedMeshAndParallelogram_callsJniAndReturnsFloat() {
201         val partitionedMesh = buildTestStrokeShape()
202         val intersectingParallelogram =
203             ImmutableParallelogram.fromCenterAndDimensions(
204                 center = ImmutableVec(15f, 4f),
205                 width = 20f,
206                 height = 9f,
207             )
208         val externalParallelogram =
209             ImmutableParallelogram.fromCenterDimensionsRotationAndShear(
210                 center = ImmutableVec(100f, 200f),
211                 width = 3f,
212                 height = 4f,
213                 rotation = Angle.QUARTER_TURN_RADIANS,
214                 shearFactor = 2f,
215             )
216 
217         assertThat(partitionedMesh.computeCoverage(intersectingParallelogram)).isGreaterThan(0f)
218         assertThat(partitionedMesh.computeCoverage(externalParallelogram)).isEqualTo(0f)
219         assertThat(partitionedMesh.computeCoverage(externalParallelogram, SCALE_TRANSFORM))
220             .isEqualTo(0f)
221     }
222 
223     /**
224      * Verifies that [PartitionedMesh.computeCoverage] calls the correct JNI method for two
225      * [PartitionedMesh]es.
226      */
227     @Test
computeCoverage_forTwoPartitionedMeshes_callsJniAndReturnsFloatnull228     fun computeCoverage_forTwoPartitionedMeshes_callsJniAndReturnsFloat() {
229         val partitionedMesh = buildTestStrokeShape()
230         val intersectingShape =
231             Stroke(
232                     TEST_BRUSH,
233                     buildStrokeInputBatchFromPoints(floatArrayOf(15f, 3f, 15f, 5f)).asImmutable(),
234                 )
235                 .shape
236         val externalShape =
237             Stroke(
238                     TEST_BRUSH,
239                     buildStrokeInputBatchFromPoints(floatArrayOf(100f, 3f, 200f, 5f)).asImmutable(),
240                 )
241                 .shape
242 
243         assertThat(partitionedMesh.computeCoverage(intersectingShape)).isGreaterThan(0f)
244         assertThat(partitionedMesh.computeCoverage(externalShape)).isEqualTo(0f)
245         assertThat(partitionedMesh.computeCoverage(externalShape, SCALE_TRANSFORM)).isEqualTo(0f)
246     }
247 
248     /**
249      * Verifies that [PartitionedMesh.computeCoverageIsGreaterThan] calls the correct JNI method for
250      * [PartitionedMesh] and [Triangle].
251      */
252     @Test
computeCoverageIsGreaterThan_forPartitionedMeshAndTriangle_callsJniAndReturnsFloatnull253     fun computeCoverageIsGreaterThan_forPartitionedMeshAndTriangle_callsJniAndReturnsFloat() {
254         val partitionedMesh = buildTestStrokeShape()
255         val intersectingTriangle =
256             ImmutableTriangle(
257                 p0 = ImmutableVec(15f, 4f),
258                 p1 = ImmutableVec(20f, 4f),
259                 p2 = ImmutableVec(20f, 5f),
260             )
261         val externalTriangle =
262             ImmutableTriangle(
263                 p0 = ImmutableVec(100f, 200f),
264                 p1 = ImmutableVec(300f, 400f),
265                 p2 = ImmutableVec(100f, 700f),
266             )
267 
268         assertThat(partitionedMesh.computeCoverageIsGreaterThan(intersectingTriangle, 0f)).isTrue()
269         assertThat(partitionedMesh.computeCoverageIsGreaterThan(externalTriangle, 0f)).isFalse()
270         assertThat(
271                 partitionedMesh.computeCoverageIsGreaterThan(externalTriangle, 0f, SCALE_TRANSFORM)
272             )
273             .isFalse()
274     }
275 
276     /**
277      * Verifies that [PartitionedMesh.computeCoverageIsGreaterThan] calls the correct JNI method for
278      * [PartitionedMesh] and [Box].
279      *
280      * For this test, `partitionedMesh` consists of triangulation of a straight line [Stroke] from
281      * (10, 3) to (20, 5), with the total area of all triangles equal to 180.471. `intersectingBox`
282      * intersects three of these triangles with the total area of 103.05. It has a coverage of
283      * 103.05 / 180.471 ≈ 0.57. `externalBox` does not intersect with any of the triangles and has a
284      * coverage of zero.
285      */
286     @Test
computeCoverageIsGreaterThan_forPartitionedMeshAndBox_callsJniAndReturnsBooleannull287     fun computeCoverageIsGreaterThan_forPartitionedMeshAndBox_callsJniAndReturnsBoolean() {
288         val partitionedMesh = buildTestStrokeShape()
289         val intersectingBox =
290             ImmutableBox.fromTwoPoints(ImmutableVec(10f, 3f), ImmutableVec(15f, 5f))
291         val externalBox =
292             ImmutableBox.fromTwoPoints(ImmutableVec(100f, 200f), ImmutableVec(300f, 400f))
293 
294         assertThat(partitionedMesh.computeCoverageIsGreaterThan(intersectingBox, 0f)).isTrue()
295         assertThat(partitionedMesh.computeCoverageIsGreaterThan(externalBox, 0f)).isFalse()
296         assertThat(partitionedMesh.computeCoverageIsGreaterThan(externalBox, 0f, SCALE_TRANSFORM))
297             .isFalse()
298     }
299 
300     /**
301      * Verifies that [PartitionedMesh.computeCoverageIsGreaterThan] calls the correct JNI method for
302      * [PartitionedMesh] and [Parallelogram].
303      */
304     @Test
computeCoverageIsGreaterThan_forPartitionedMeshAndParallelogram_callsJniAndReturnsBooleannull305     fun computeCoverageIsGreaterThan_forPartitionedMeshAndParallelogram_callsJniAndReturnsBoolean() {
306         val partitionedMesh = buildTestStrokeShape()
307         val intersectingParallelogram =
308             ImmutableParallelogram.fromCenterAndDimensions(
309                 center = ImmutableVec(15f, 4f),
310                 width = 20f,
311                 height = 9f,
312             )
313         val externalParallelogram =
314             ImmutableParallelogram.fromCenterDimensionsRotationAndShear(
315                 center = ImmutableVec(100f, 200f),
316                 width = 3f,
317                 height = 4f,
318                 rotation = Angle.QUARTER_TURN_RADIANS,
319                 shearFactor = 2f,
320             )
321 
322         assertThat(partitionedMesh.computeCoverageIsGreaterThan(intersectingParallelogram, 0f))
323             .isTrue()
324         assertThat(partitionedMesh.computeCoverageIsGreaterThan(externalParallelogram, 0f))
325             .isFalse()
326         assertThat(
327                 partitionedMesh.computeCoverageIsGreaterThan(
328                     externalParallelogram,
329                     0f,
330                     SCALE_TRANSFORM
331                 )
332             )
333             .isFalse()
334     }
335 
336     /**
337      * Verifies that [PartitionedMesh.computeCoverage] calls the correct JNI method for two
338      * [PartitionedMesh]s.
339      *
340      * For this test, `partitionedMesh` consists of triangulation of a straight line [Stroke] from
341      * (10, 3) to (20, 5), with the total area of all triangles equal to 180.471.
342      * `intersectingShape` consists of the triangulation of a straight vertical line [Stroke] from
343      * [15, 3] to [15, 5], and intersects with 6 of these triangles with the total area of 106.95.
344      * It has a coverage of (106.95) / 180.471 ≈ 0.59. `externalShape` does not intersect with
345      * `partitionedMesh`, and has zero coverage.
346      */
347     @Test
computeCoverageIsGreaterThan_forTwoPartitionedMeshes_callsJniAndReturnsBooleannull348     fun computeCoverageIsGreaterThan_forTwoPartitionedMeshes_callsJniAndReturnsBoolean() {
349         val partitionedMesh = buildTestStrokeShape()
350         val intersectingShape =
351             Stroke(
352                     TEST_BRUSH,
353                     buildStrokeInputBatchFromPoints(floatArrayOf(15f, 3f, 15f, 5f)).asImmutable(),
354                 )
355                 .shape
356         val externalShape =
357             Stroke(
358                     TEST_BRUSH,
359                     buildStrokeInputBatchFromPoints(floatArrayOf(100f, 3f, 200f, 5f)).asImmutable(),
360                 )
361                 .shape
362 
363         assertThat(partitionedMesh.computeCoverageIsGreaterThan(intersectingShape, 0f)).isTrue()
364         assertThat(partitionedMesh.computeCoverageIsGreaterThan(externalShape, 0f)).isFalse()
365         assertThat(partitionedMesh.computeCoverageIsGreaterThan(externalShape, 0f, SCALE_TRANSFORM))
366             .isFalse()
367     }
368 
369     @Test
initializeSpatialIndexnull370     fun initializeSpatialIndex() {
371         val partitionedMesh = buildTestStrokeShape()
372         assertThat(partitionedMesh.isSpatialIndexInitialized()).isFalse()
373 
374         partitionedMesh.initializeSpatialIndex()
375 
376         assertThat(partitionedMesh.isSpatialIndexInitialized()).isTrue()
377     }
378 
379     @Test
isSpatialIndexInitialized_afterGeometryQuery_returnsTruenull380     fun isSpatialIndexInitialized_afterGeometryQuery_returnsTrue() {
381         val partitionedMesh = buildTestStrokeShape()
382         val triangle =
383             ImmutableTriangle(
384                 p0 = ImmutableVec(15f, 4f),
385                 p1 = ImmutableVec(20f, 4f),
386                 p2 = ImmutableVec(20f, 5f),
387             )
388         assertThat(partitionedMesh.isSpatialIndexInitialized()).isFalse()
389 
390         assertThat(partitionedMesh.computeCoverage(triangle)).isNotNaN()
391 
392         assertThat(partitionedMesh.isSpatialIndexInitialized()).isTrue()
393     }
394 
buildTestStrokeShapenull395     private fun buildTestStrokeShape(): PartitionedMesh {
396         return Stroke(
397                 TEST_BRUSH,
398                 buildStrokeInputBatchFromPoints(floatArrayOf(10f, 3f, 20f, 5f)).asImmutable(),
399             )
400             .shape
401     }
402 
403     companion object {
404         private val SCALE_TRANSFORM = ImmutableAffineTransform(1.2f, 0f, 0f, 0f, 0.4f, 0f)
405 
406         private val TEST_BRUSH =
407             Brush(family = StockBrushes.markerLatest, size = 10f, epsilon = 0.1f)
408     }
409 }
410