1# Apache License, Version 2.0 2 3# ./blender.bin --background -noaudio --python tests/python/bl_pyapi_mathutils.py -- --verbose 4import unittest 5from mathutils import Matrix, Vector, Quaternion 6from mathutils import kdtree, geometry 7import math 8 9# keep globals immutable 10vector_data = ( 11 (1.0, 0.0, 0.0), 12 (0.0, 1.0, 0.0), 13 (0.0, 0.0, 1.0), 14 15 (1.0, 1.0, 1.0), 16 17 (0.33783, 0.715698, -0.611206), 18 (-0.944031, -0.326599, -0.045624), 19 (-0.101074, -0.416443, -0.903503), 20 (0.799286, 0.49411, -0.341949), 21 (-0.854645, 0.518036, 0.033936), 22 (0.42514, -0.437866, -0.792114), 23 (-0.358948, 0.597046, 0.717377), 24 (-0.985413, 0.144714, 0.089294), 25) 26 27# get data at different scales 28vector_data = sum( 29 (tuple(tuple(a * scale for a in v) for v in vector_data) 30 for scale in (s * sign for s in (0.0001, 0.1, 1.0, 10.0, 1000.0, 100000.0) 31 for sign in (1.0, -1.0))), ()) + ((0.0, 0.0, 0.0),) 32 33 34class MatrixTesting(unittest.TestCase): 35 def test_matrix_column_access(self): 36 # mat = 37 # [ 1 2 3 4 ] 38 # [ 1 2 3 4 ] 39 # [ 1 2 3 4 ] 40 mat = Matrix(((1, 11, 111), 41 (2, 22, 222), 42 (3, 33, 333), 43 (4, 44, 444))) 44 45 self.assertEqual(mat[0], Vector((1, 11, 111))) 46 self.assertEqual(mat[1], Vector((2, 22, 222))) 47 self.assertEqual(mat[2], Vector((3, 33, 333))) 48 self.assertEqual(mat[3], Vector((4, 44, 444))) 49 50 def test_item_access(self): 51 args = ((1, 4, 0, -1), 52 (2, -1, 2, -2), 53 (0, 3, 8, 3), 54 (-2, 9, 1, 0)) 55 56 mat = Matrix(args) 57 58 for row in range(4): 59 for col in range(4): 60 self.assertEqual(mat[row][col], args[row][col]) 61 62 self.assertEqual(mat[0][2], 0) 63 self.assertEqual(mat[3][1], 9) 64 self.assertEqual(mat[2][3], 3) 65 self.assertEqual(mat[0][0], 1) 66 self.assertEqual(mat[3][3], 0) 67 68 def test_item_assignment(self): 69 mat = Matrix() - Matrix() 70 indices = (0, 0), (1, 3), (2, 0), (3, 2), (3, 1) 71 checked_indices = [] 72 for row, col in indices: 73 mat[row][col] = 1 74 75 for row in range(4): 76 for col in range(4): 77 if mat[row][col]: 78 checked_indices.append((row, col)) 79 80 for item in checked_indices: 81 self.assertIn(item, indices) 82 83 def test_matrix_to_3x3(self): 84 # mat = 85 # [ 1 2 3 4 ] 86 # [ 2 4 6 8 ] 87 # [ 3 6 9 12 ] 88 # [ 4 8 12 16 ] 89 mat = Matrix(tuple((i, 2 * i, 3 * i, 4 * i) for i in range(1, 5))) 90 mat_correct = Matrix(((1, 2, 3), (2, 4, 6), (3, 6, 9))) 91 self.assertEqual(mat.to_3x3(), mat_correct) 92 93 def test_matrix_to_translation(self): 94 mat = Matrix() 95 mat[0][3] = 1 96 mat[1][3] = 2 97 mat[2][3] = 3 98 self.assertEqual(mat.to_translation(), Vector((1, 2, 3))) 99 100 def test_matrix_translation(self): 101 mat = Matrix() 102 mat.translation = Vector((1, 2, 3)) 103 self.assertEqual(mat[0][3], 1) 104 self.assertEqual(mat[1][3], 2) 105 self.assertEqual(mat[2][3], 3) 106 107 def test_matrix_non_square_matmul(self): 108 mat1 = Matrix(((1, 2, 3), 109 (4, 5, 6))) 110 mat2 = Matrix(((1, 2), 111 (3, 4), 112 (5, 6))) 113 114 prod_mat1 = Matrix(((22, 28), 115 (49, 64))) 116 prod_mat2 = Matrix(((9, 12, 15), 117 (19, 26, 33), 118 (29, 40, 51))) 119 120 self.assertEqual(mat1 @ mat2, prod_mat1) 121 self.assertEqual(mat2 @ mat1, prod_mat2) 122 123 def test_mat4x4_vec3D_matmul(self): 124 mat = Matrix(((1, 0, 2, 0), 125 (0, 6, 0, 0), 126 (0, 0, 1, 1), 127 (0, 0, 0, 1))) 128 129 vec = Vector((1, 2, 3)) 130 131 prod_mat_vec = Vector((7, 12, 4)) 132 prod_vec_mat = Vector((1, 12, 5)) 133 134 self.assertEqual(mat @ vec, prod_mat_vec) 135 self.assertEqual(vec @ mat, prod_vec_mat) 136 137 def test_mat_vec_matmul(self): 138 mat1 = Matrix() 139 140 vec = Vector((1, 2)) 141 142 self.assertRaises(ValueError, mat1.__matmul__, vec) 143 self.assertRaises(ValueError, vec.__matmul__, mat1) 144 145 mat2 = Matrix(((1, 2), 146 (-2, 3))) 147 148 prod = Vector((5, 4)) 149 150 self.assertEqual(mat2 @ vec, prod) 151 152 def test_matrix_square_matmul(self): 153 mat1 = Matrix(((1, 0), 154 (1, 2))) 155 mat2 = Matrix(((1, 2), 156 (-2, 3))) 157 158 prod1 = Matrix(((1, 2), 159 (-3, 8))) 160 prod2 = Matrix(((3, 4), 161 (1, 6))) 162 163 self.assertEqual(mat1 @ mat2, prod1) 164 self.assertEqual(mat2 @ mat1, prod2) 165 166 """ 167 # tests for element-wise multiplication 168 169 def test_matrix_mul(self): 170 mat1 = Matrix(((1, 0), 171 (1, 2))) 172 mat2 = Matrix(((1, 2), 173 (-2, 3))) 174 mat3 = Matrix(((1, 0, 2, 0), 175 (0, 6, 0, 0), 176 (0, 0, 1, 1), 177 (0, 0, 0, 1))) 178 179 prod = Matrix(((1, 0), 180 (-2, 6))) 181 182 self.assertEqual(mat1 * mat2, prod) 183 self.assertEqual(mat2 * mat1, prod) 184 self.assertRaises(ValueError, mat1.__mul__, mat3) 185 """ 186 187 def test_matrix_inverse(self): 188 mat = Matrix(((1, 4, 0, -1), 189 (2, -1, 2, -2), 190 (0, 3, 8, 3), 191 (-2, 9, 1, 0))) 192 193 inv_mat = (1 / 285) * Matrix(((195, -57, 27, -102), 194 (50, -19, 4, 6), 195 (-60, 57, 18, 27), 196 (110, -133, 43, -78))) 197 198 self.assertEqual(mat.inverted(), inv_mat) 199 200 def test_matrix_inverse_safe(self): 201 mat = Matrix(((1, 4, 0, -1), 202 (2, -1, 0, -2), 203 (0, 3, 0, 3), 204 (-2, 9, 0, 0))) 205 206 # Warning, if we change epsilon in py api we have to update this!!! 207 epsilon = 1e-8 208 inv_mat_safe = mat.copy() 209 inv_mat_safe[0][0] += epsilon 210 inv_mat_safe[1][1] += epsilon 211 inv_mat_safe[2][2] += epsilon 212 inv_mat_safe[3][3] += epsilon 213 inv_mat_safe.invert() 214 ''' 215 inv_mat_safe = Matrix(((1.0, -0.5, 0.0, -0.5), 216 (0.222222, -0.111111, -0.0, 0.0), 217 (-333333344.0, 316666656.0, 100000000.0, 150000000.0), 218 (0.888888, -0.9444444, 0.0, -0.5))) 219 ''' 220 221 self.assertEqual(mat.inverted_safe(), inv_mat_safe) 222 223 def test_matrix_matmult(self): 224 mat = Matrix(((1, 4, 0, -1), 225 (2, -1, 2, -2), 226 (0, 3, 8, 3), 227 (-2, 9, 1, 0))) 228 229 prod_mat = Matrix(((11, -9, 7, -9), 230 (4, -3, 12, 6), 231 (0, 48, 73, 18), 232 (16, -14, 26, -13))) 233 234 self.assertEqual(mat @ mat, prod_mat) 235 236 237class VectorTesting(unittest.TestCase): 238 239 def test_orthogonal(self): 240 241 angle_90d = math.pi / 2.0 242 for v in vector_data: 243 v = Vector(v) 244 if v.length_squared != 0.0: 245 self.assertAlmostEqual(v.angle(v.orthogonal()), angle_90d) 246 247 def test_vector_matmul(self): 248 # produces dot product for vectors 249 vec1 = Vector((1, 3, 5)) 250 vec2 = Vector((1, 2)) 251 252 self.assertRaises(ValueError, vec1.__matmul__, vec2) 253 self.assertEqual(vec1 @ vec1, 35) 254 self.assertEqual(vec2 @ vec2, 5) 255 256 def test_vector_imatmul(self): 257 vec = Vector((1, 3, 5)) 258 259 with self.assertRaises(TypeError): 260 vec @= vec 261 262 """ 263 # tests for element-wise multiplication 264 265 def test_vector_mul(self): 266 # element-wise multiplication 267 vec1 = Vector((1, 3, 5)) 268 vec2 = Vector((1, 2)) 269 270 prod1 = Vector((1, 9, 25)) 271 prod2 = Vector((2, 6, 10)) 272 273 self.assertRaises(ValueError, vec1.__mul__, vec2) 274 self.assertEqual(vec1 * vec1, prod1) 275 self.assertEqual(2 * vec1, prod2) 276 277 def test_vector_imul(self): 278 # inplace element-wise multiplication 279 vec = Vector((1, 3, 5)) 280 prod1 = Vector((1, 9, 25)) 281 prod2 = Vector((2, 18, 50)) 282 283 vec *= vec 284 self.assertEqual(vec, prod1) 285 286 vec *= 2 287 self.assertEqual(vec, prod2) 288 """ 289 290 291class QuaternionTesting(unittest.TestCase): 292 293 def test_to_expmap(self): 294 q = Quaternion((0, 0, 1), math.radians(90)) 295 296 e = q.to_exponential_map() 297 self.assertAlmostEqual(e.x, 0) 298 self.assertAlmostEqual(e.y, 0) 299 self.assertAlmostEqual(e.z, math.radians(90), 6) 300 301 def test_expmap_axis_normalization(self): 302 q = Quaternion((1, 1, 0), 2) 303 e = q.to_exponential_map() 304 305 self.assertAlmostEqual(e.x, 2 * math.sqrt(0.5), 6) 306 self.assertAlmostEqual(e.y, 2 * math.sqrt(0.5), 6) 307 self.assertAlmostEqual(e.z, 0) 308 309 def test_from_expmap(self): 310 e = Vector((1, 1, 0)) 311 q = Quaternion(e) 312 axis, angle = q.to_axis_angle() 313 314 self.assertAlmostEqual(angle, math.sqrt(2), 6) 315 self.assertAlmostEqual(axis.x, math.sqrt(0.5), 6) 316 self.assertAlmostEqual(axis.y, math.sqrt(0.5), 6) 317 self.assertAlmostEqual(axis.z, 0) 318 319 320class KDTreeTesting(unittest.TestCase): 321 @staticmethod 322 def kdtree_create_grid_3d_data(tot): 323 index = 0 324 mul = 1.0 / (tot - 1) 325 for x in range(tot): 326 for y in range(tot): 327 for z in range(tot): 328 yield (x * mul, y * mul, z * mul), index 329 index += 1 330 331 @staticmethod 332 def kdtree_create_grid_3d(tot, *, filter_fn=None): 333 k = kdtree.KDTree(tot * tot * tot) 334 for co, index in KDTreeTesting.kdtree_create_grid_3d_data(tot): 335 if (filter_fn is not None) and (not filter_fn(co, index)): 336 continue 337 k.insert(co, index) 338 k.balance() 339 return k 340 341 def assertAlmostEqualVector(self, first, second, places=7, msg=None, delta=None): 342 self.assertAlmostEqual(first[0], second[0], places=places, msg=msg, delta=delta) 343 self.assertAlmostEqual(first[1], second[1], places=places, msg=msg, delta=delta) 344 self.assertAlmostEqual(first[2], second[2], places=places, msg=msg, delta=delta) 345 346 def test_kdtree_single(self): 347 co = (0,) * 3 348 index = 2 349 350 k = kdtree.KDTree(1) 351 k.insert(co, index) 352 k.balance() 353 354 co_found, index_found, dist_found = k.find(co) 355 356 self.assertEqual(tuple(co_found), co) 357 self.assertEqual(index_found, index) 358 self.assertEqual(dist_found, 0.0) 359 360 def test_kdtree_empty(self): 361 co = (0,) * 3 362 363 k = kdtree.KDTree(0) 364 k.balance() 365 366 co_found, index_found, dist_found = k.find(co) 367 368 self.assertIsNone(co_found) 369 self.assertIsNone(index_found) 370 self.assertIsNone(dist_found) 371 372 def test_kdtree_line(self): 373 tot = 10 374 375 k = kdtree.KDTree(tot) 376 377 for i in range(tot): 378 k.insert((i,) * 3, i) 379 380 k.balance() 381 382 co_found, index_found, dist_found = k.find((-1,) * 3) 383 self.assertEqual(tuple(co_found), (0,) * 3) 384 385 co_found, index_found, dist_found = k.find((tot,) * 3) 386 self.assertEqual(tuple(co_found), (tot - 1,) * 3) 387 388 def test_kdtree_grid(self): 389 size = 10 390 k = self.kdtree_create_grid_3d(size) 391 392 # find_range 393 ret = k.find_range((0.5,) * 3, 2.0) 394 self.assertEqual(len(ret), size * size * size) 395 396 ret = k.find_range((1.0,) * 3, 1.0 / size) 397 self.assertEqual(len(ret), 1) 398 399 ret = k.find_range((1.0,) * 3, 2.0 / size) 400 self.assertEqual(len(ret), 8) 401 402 ret = k.find_range((10,) * 3, 0.5) 403 self.assertEqual(len(ret), 0) 404 405 # find_n 406 tot = 0 407 ret = k.find_n((1.0,) * 3, tot) 408 self.assertEqual(len(ret), tot) 409 410 tot = 10 411 ret = k.find_n((1.0,) * 3, tot) 412 self.assertEqual(len(ret), tot) 413 self.assertEqual(ret[0][2], 0.0) 414 415 tot = size * size * size 416 ret = k.find_n((1.0,) * 3, tot) 417 self.assertEqual(len(ret), tot) 418 419 def test_kdtree_grid_filter_simple(self): 420 size = 10 421 k = self.kdtree_create_grid_3d(size) 422 423 # filter exact index 424 ret_regular = k.find((1.0,) * 3) 425 ret_filter = k.find((1.0,) * 3, filter=lambda i: i == ret_regular[1]) 426 self.assertEqual(ret_regular, ret_filter) 427 ret_filter = k.find((-1.0,) * 3, filter=lambda i: i == ret_regular[1]) 428 self.assertEqual(ret_regular[:2], ret_filter[:2]) # ignore distance 429 430 def test_kdtree_grid_filter_pairs(self): 431 size = 10 432 k_all = self.kdtree_create_grid_3d(size) 433 k_odd = self.kdtree_create_grid_3d(size, filter_fn=lambda co, i: (i % 2) == 1) 434 k_evn = self.kdtree_create_grid_3d(size, filter_fn=lambda co, i: (i % 2) == 0) 435 436 samples = 5 437 mul = 1 / (samples - 1) 438 for x in range(samples): 439 for y in range(samples): 440 for z in range(samples): 441 co = (x * mul, y * mul, z * mul) 442 443 ret_regular = k_odd.find(co) 444 self.assertEqual(ret_regular[1] % 2, 1) 445 ret_filter = k_all.find(co, lambda i: (i % 2) == 1) 446 self.assertAlmostEqualVector(ret_regular, ret_filter) 447 448 ret_regular = k_evn.find(co) 449 self.assertEqual(ret_regular[1] % 2, 0) 450 ret_filter = k_all.find(co, lambda i: (i % 2) == 0) 451 self.assertAlmostEqualVector(ret_regular, ret_filter) 452 453 # filter out all values (search odd tree for even values and the reverse) 454 co = (0,) * 3 455 ret_filter = k_odd.find(co, lambda i: (i % 2) == 0) 456 self.assertEqual(ret_filter[1], None) 457 458 ret_filter = k_evn.find(co, lambda i: (i % 2) == 1) 459 self.assertEqual(ret_filter[1], None) 460 461 def test_kdtree_invalid_size(self): 462 with self.assertRaises(ValueError): 463 kdtree.KDTree(-1) 464 465 def test_kdtree_invalid_balance(self): 466 co = (0,) * 3 467 index = 2 468 469 k = kdtree.KDTree(2) 470 k.insert(co, index) 471 k.balance() 472 k.insert(co, index) 473 with self.assertRaises(RuntimeError): 474 k.find(co) 475 476 def test_kdtree_invalid_filter(self): 477 k = kdtree.KDTree(1) 478 k.insert((0,) * 3, 0) 479 k.balance() 480 # not callable 481 with self.assertRaises(TypeError): 482 k.find((0,) * 3, filter=None) 483 # no args 484 with self.assertRaises(TypeError): 485 k.find((0,) * 3, filter=lambda: None) 486 # bad return value 487 with self.assertRaises(ValueError): 488 k.find((0,) * 3, filter=lambda i: None) 489 490 491class TesselatePolygon(unittest.TestCase): 492 def test_empty(self): 493 self.assertEqual([], geometry.tessellate_polygon([])) 494 495 def test_2d(self): 496 polyline = [ 497 Vector((-0.14401324093341827, 0.1266411542892456)), 498 Vector((-0.14401324093341827, 0.13)), 499 Vector((0.13532273471355438, 0.1266411542892456)), 500 Vector((0.13532273471355438, 0.13)), 501 ] 502 expect = [(0, 1, 2), (0, 3, 2)] 503 self.assertEqual(expect, geometry.tessellate_polygon([polyline])) 504 505 def test_3d(self): 506 polyline = [ 507 Vector((-0.14401324093341827, 0.1266411542892456, -0.13966798782348633)), 508 Vector((-0.14401324093341827, 0.1266411542892456, 0.13966798782348633)), 509 Vector((0.13532273471355438, 0.1266411542892456, 0.13966798782348633)), 510 Vector((0.13532273471355438, 0.1266411542892456, -0.13966798782348633)), 511 ] 512 expect = [(2, 3, 0), (2, 0, 1)] 513 self.assertEqual(expect, geometry.tessellate_polygon([polyline])) 514 515 def test_3d_degenerate(self): 516 polyline = [ 517 Vector((-0.14401324093341827, -0.15269476175308228, -0.13966798782348633)), 518 Vector((0.13532273471355438, -0.15269476175308228, -0.13966798782348633)), 519 Vector((0.13532273471355438, -0.15269476175308228, -0.13966798782348633)), 520 Vector((-0.14401324093341827, -0.15269476175308228, -0.13966798782348633)), 521 ] 522 # If this returns a proper result, rather than [(0, 0, 0)], it could mean that 523 # degenerate geometry is handled properly. 524 expect = [(0, 0, 0)] 525 self.assertEqual(expect, geometry.tessellate_polygon([polyline])) 526 527 528if __name__ == '__main__': 529 import sys 530 sys.argv = [__file__] + (sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []) 531 unittest.main() 532