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