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