Listing the "Blender Foundation" as copyright holder implied the Blender Foundation holds copyright to files which may include work from many developers. While keeping copyright on headers makes sense for isolated libraries, Blender's own code may be refactored or moved between files in a way that makes the per file copyright holders less meaningful. Copyright references to the "Blender Foundation" have been replaced with "Blender Authors", with the exception of `./extern/` since these this contains libraries which are more isolated, any changed to license headers there can be handled on a case-by-case basis. Some directories in `./intern/` have also been excluded: - `./intern/cycles/` it's own `AUTHORS` file is planned. - `./intern/opensubdiv/`. An "AUTHORS" file has been added, using the chromium projects authors file as a template. Design task: #110784 Ref !110783.
551 lines
18 KiB
Python
551 lines
18 KiB
Python
# SPDX-FileCopyrightText: 2011-2022 Blender Authors
|
|
#
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
|
|
# ./blender.bin --background -noaudio --python tests/python/bl_pyapi_mathutils.py -- --verbose
|
|
import unittest
|
|
from mathutils import Matrix, Vector, Quaternion, Euler
|
|
from mathutils import kdtree, geometry
|
|
import math
|
|
|
|
# keep globals immutable
|
|
vector_data = (
|
|
(1.0, 0.0, 0.0),
|
|
(0.0, 1.0, 0.0),
|
|
(0.0, 0.0, 1.0),
|
|
|
|
(1.0, 1.0, 1.0),
|
|
|
|
(0.33783, 0.715698, -0.611206),
|
|
(-0.944031, -0.326599, -0.045624),
|
|
(-0.101074, -0.416443, -0.903503),
|
|
(0.799286, 0.49411, -0.341949),
|
|
(-0.854645, 0.518036, 0.033936),
|
|
(0.42514, -0.437866, -0.792114),
|
|
(-0.358948, 0.597046, 0.717377),
|
|
(-0.985413, 0.144714, 0.089294),
|
|
)
|
|
|
|
# get data at different scales
|
|
vector_data = sum(
|
|
(tuple(tuple(a * scale for a in v) for v in vector_data)
|
|
for scale in (s * sign for s in (0.0001, 0.1, 1.0, 10.0, 1000.0, 100000.0)
|
|
for sign in (1.0, -1.0))), ()) + ((0.0, 0.0, 0.0),)
|
|
|
|
|
|
class MatrixTesting(unittest.TestCase):
|
|
def test_matrix_column_access(self):
|
|
# mat =
|
|
# [ 1 2 3 4 ]
|
|
# [ 1 2 3 4 ]
|
|
# [ 1 2 3 4 ]
|
|
mat = Matrix(((1, 11, 111),
|
|
(2, 22, 222),
|
|
(3, 33, 333),
|
|
(4, 44, 444)))
|
|
|
|
self.assertEqual(mat[0], Vector((1, 11, 111)))
|
|
self.assertEqual(mat[1], Vector((2, 22, 222)))
|
|
self.assertEqual(mat[2], Vector((3, 33, 333)))
|
|
self.assertEqual(mat[3], Vector((4, 44, 444)))
|
|
|
|
def test_item_access(self):
|
|
args = ((1, 4, 0, -1),
|
|
(2, -1, 2, -2),
|
|
(0, 3, 8, 3),
|
|
(-2, 9, 1, 0))
|
|
|
|
mat = Matrix(args)
|
|
|
|
for row in range(4):
|
|
for col in range(4):
|
|
self.assertEqual(mat[row][col], args[row][col])
|
|
|
|
self.assertEqual(mat[0][2], 0)
|
|
self.assertEqual(mat[3][1], 9)
|
|
self.assertEqual(mat[2][3], 3)
|
|
self.assertEqual(mat[0][0], 1)
|
|
self.assertEqual(mat[3][3], 0)
|
|
|
|
def test_item_assignment(self):
|
|
mat = Matrix() - Matrix()
|
|
indices = (0, 0), (1, 3), (2, 0), (3, 2), (3, 1)
|
|
checked_indices = []
|
|
for row, col in indices:
|
|
mat[row][col] = 1
|
|
|
|
for row in range(4):
|
|
for col in range(4):
|
|
if mat[row][col]:
|
|
checked_indices.append((row, col))
|
|
|
|
for item in checked_indices:
|
|
self.assertIn(item, indices)
|
|
|
|
def test_matrix_to_3x3(self):
|
|
# mat =
|
|
# [ 1 2 3 4 ]
|
|
# [ 2 4 6 8 ]
|
|
# [ 3 6 9 12 ]
|
|
# [ 4 8 12 16 ]
|
|
mat = Matrix(tuple((i, 2 * i, 3 * i, 4 * i) for i in range(1, 5)))
|
|
mat_correct = Matrix(((1, 2, 3), (2, 4, 6), (3, 6, 9)))
|
|
self.assertEqual(mat.to_3x3(), mat_correct)
|
|
|
|
def test_matrix_to_translation(self):
|
|
mat = Matrix()
|
|
mat[0][3] = 1
|
|
mat[1][3] = 2
|
|
mat[2][3] = 3
|
|
self.assertEqual(mat.to_translation(), Vector((1, 2, 3)))
|
|
|
|
def test_matrix_translation(self):
|
|
mat = Matrix()
|
|
mat.translation = Vector((1, 2, 3))
|
|
self.assertEqual(mat[0][3], 1)
|
|
self.assertEqual(mat[1][3], 2)
|
|
self.assertEqual(mat[2][3], 3)
|
|
|
|
def test_matrix_non_square_matmul(self):
|
|
mat1 = Matrix(((1, 2, 3),
|
|
(4, 5, 6)))
|
|
mat2 = Matrix(((1, 2),
|
|
(3, 4),
|
|
(5, 6)))
|
|
|
|
prod_mat1 = Matrix(((22, 28),
|
|
(49, 64)))
|
|
prod_mat2 = Matrix(((9, 12, 15),
|
|
(19, 26, 33),
|
|
(29, 40, 51)))
|
|
|
|
self.assertEqual(mat1 @ mat2, prod_mat1)
|
|
self.assertEqual(mat2 @ mat1, prod_mat2)
|
|
|
|
def test_mat4x4_vec3D_matmul(self):
|
|
mat = Matrix(((1, 0, 2, 0),
|
|
(0, 6, 0, 0),
|
|
(0, 0, 1, 1),
|
|
(0, 0, 0, 1)))
|
|
|
|
vec = Vector((1, 2, 3))
|
|
|
|
prod_mat_vec = Vector((7, 12, 4))
|
|
prod_vec_mat = Vector((1, 12, 5))
|
|
|
|
self.assertEqual(mat @ vec, prod_mat_vec)
|
|
self.assertEqual(vec @ mat, prod_vec_mat)
|
|
|
|
def test_mat_vec_matmul(self):
|
|
mat1 = Matrix()
|
|
|
|
vec = Vector((1, 2))
|
|
|
|
self.assertRaises(ValueError, mat1.__matmul__, vec)
|
|
self.assertRaises(ValueError, vec.__matmul__, mat1)
|
|
|
|
mat2 = Matrix(((1, 2),
|
|
(-2, 3)))
|
|
|
|
prod = Vector((5, 4))
|
|
|
|
self.assertEqual(mat2 @ vec, prod)
|
|
|
|
def test_matrix_square_matmul(self):
|
|
mat1 = Matrix(((1, 0),
|
|
(1, 2)))
|
|
mat2 = Matrix(((1, 2),
|
|
(-2, 3)))
|
|
|
|
prod1 = Matrix(((1, 2),
|
|
(-3, 8)))
|
|
prod2 = Matrix(((3, 4),
|
|
(1, 6)))
|
|
|
|
self.assertEqual(mat1 @ mat2, prod1)
|
|
self.assertEqual(mat2 @ mat1, prod2)
|
|
|
|
# tests for element-wise multiplication
|
|
|
|
def test_matrix_mul(self):
|
|
mat1 = Matrix(((1, 0),
|
|
(1, 2)))
|
|
mat2 = Matrix(((1, 2),
|
|
(-2, 3)))
|
|
mat3 = Matrix(((1, 0, 2, 0),
|
|
(0, 6, 0, 0),
|
|
(0, 0, 1, 1),
|
|
(0, 0, 0, 1)))
|
|
|
|
prod = Matrix(((1, 0),
|
|
(-2, 6)))
|
|
|
|
self.assertEqual(mat1 * mat2, prod)
|
|
self.assertEqual(mat2 * mat1, prod)
|
|
self.assertRaises(ValueError, mat1.__mul__, mat3)
|
|
|
|
def test_matrix_inverse(self):
|
|
mat = Matrix(((1, 4, 0, -1),
|
|
(2, -1, 2, -2),
|
|
(0, 3, 8, 3),
|
|
(-2, 9, 1, 0)))
|
|
|
|
inv_mat = (1 / 285) * Matrix(((195, -57, 27, -102),
|
|
(50, -19, 4, 6),
|
|
(-60, 57, 18, 27),
|
|
(110, -133, 43, -78)))
|
|
|
|
self.assertEqual(mat.inverted(), inv_mat)
|
|
|
|
def test_matrix_inverse_safe(self):
|
|
mat = Matrix(((1, 4, 0, -1),
|
|
(2, -1, 0, -2),
|
|
(0, 3, 0, 3),
|
|
(-2, 9, 0, 0)))
|
|
|
|
# Warning, if we change epsilon in py api we have to update this!!!
|
|
epsilon = 1e-8
|
|
inv_mat_safe = mat.copy()
|
|
inv_mat_safe[0][0] += epsilon
|
|
inv_mat_safe[1][1] += epsilon
|
|
inv_mat_safe[2][2] += epsilon
|
|
inv_mat_safe[3][3] += epsilon
|
|
inv_mat_safe.invert()
|
|
'''
|
|
inv_mat_safe = Matrix(((1.0, -0.5, 0.0, -0.5),
|
|
(0.222222, -0.111111, -0.0, 0.0),
|
|
(-333333344.0, 316666656.0, 100000000.0, 150000000.0),
|
|
(0.888888, -0.9444444, 0.0, -0.5)))
|
|
'''
|
|
|
|
self.assertEqual(mat.inverted_safe(), inv_mat_safe)
|
|
|
|
def test_matrix_matmult(self):
|
|
mat = Matrix(((1, 4, 0, -1),
|
|
(2, -1, 2, -2),
|
|
(0, 3, 8, 3),
|
|
(-2, 9, 1, 0)))
|
|
|
|
prod_mat = Matrix(((11, -9, 7, -9),
|
|
(4, -3, 12, 6),
|
|
(0, 48, 73, 18),
|
|
(16, -14, 26, -13)))
|
|
|
|
self.assertEqual(mat @ mat, prod_mat)
|
|
|
|
def test_loc_rot_scale(self):
|
|
euler = Euler((math.radians(90), 0, math.radians(90)), 'ZYX')
|
|
expected = Matrix(((0, -5, 0, 1),
|
|
(0, 0, -6, 2),
|
|
(4, 0, 0, 3),
|
|
(0, 0, 0, 1)))
|
|
|
|
result = Matrix.LocRotScale((1, 2, 3), euler, (4, 5, 6))
|
|
self.assertAlmostEqualMatrix(result, expected, 4)
|
|
|
|
result = Matrix.LocRotScale((1, 2, 3), euler.to_quaternion(), (4, 5, 6))
|
|
self.assertAlmostEqualMatrix(result, expected, 4)
|
|
|
|
result = Matrix.LocRotScale((1, 2, 3), euler.to_matrix(), (4, 5, 6))
|
|
self.assertAlmostEqualMatrix(result, expected, 4)
|
|
|
|
def assertAlmostEqualMatrix(self, first, second, size, *, places=6, msg=None, delta=None):
|
|
for i in range(size):
|
|
for j in range(size):
|
|
self.assertAlmostEqual(first[i][j], second[i][j], places=places, msg=msg, delta=delta)
|
|
|
|
|
|
class VectorTesting(unittest.TestCase):
|
|
|
|
def test_orthogonal(self):
|
|
|
|
angle_90d = math.pi / 2.0
|
|
for v in vector_data:
|
|
v = Vector(v)
|
|
if v.length_squared != 0.0:
|
|
self.assertAlmostEqual(v.angle(v.orthogonal()), angle_90d)
|
|
|
|
def test_vector_matmul(self):
|
|
# produces dot product for vectors
|
|
vec1 = Vector((1, 3, 5))
|
|
vec2 = Vector((1, 2))
|
|
|
|
self.assertRaises(ValueError, vec1.__matmul__, vec2)
|
|
self.assertEqual(vec1 @ vec1, 35)
|
|
self.assertEqual(vec2 @ vec2, 5)
|
|
|
|
def test_vector_imatmul(self):
|
|
vec = Vector((1, 3, 5))
|
|
|
|
with self.assertRaises(TypeError):
|
|
vec @= vec
|
|
|
|
# tests for element-wise multiplication
|
|
|
|
def test_vector_mul(self):
|
|
# element-wise multiplication
|
|
vec1 = Vector((1, 3, 5))
|
|
vec2 = Vector((1, 2))
|
|
|
|
prod1 = Vector((1, 9, 25))
|
|
prod2 = Vector((2, 6, 10))
|
|
|
|
self.assertRaises(ValueError, vec1.__mul__, vec2)
|
|
self.assertEqual(vec1 * vec1, prod1)
|
|
self.assertEqual(2 * vec1, prod2)
|
|
|
|
def test_vector_imul(self):
|
|
# inplace element-wise multiplication
|
|
vec = Vector((1, 3, 5))
|
|
prod1 = Vector((1, 9, 25))
|
|
prod2 = Vector((2, 18, 50))
|
|
|
|
vec *= vec
|
|
self.assertEqual(vec, prod1)
|
|
|
|
vec *= 2
|
|
self.assertEqual(vec, prod2)
|
|
|
|
|
|
class QuaternionTesting(unittest.TestCase):
|
|
|
|
def test_to_expmap(self):
|
|
q = Quaternion((0, 0, 1), math.radians(90))
|
|
|
|
e = q.to_exponential_map()
|
|
self.assertAlmostEqual(e.x, 0)
|
|
self.assertAlmostEqual(e.y, 0)
|
|
self.assertAlmostEqual(e.z, math.radians(90), 6)
|
|
|
|
def test_expmap_axis_normalization(self):
|
|
q = Quaternion((1, 1, 0), 2)
|
|
e = q.to_exponential_map()
|
|
|
|
self.assertAlmostEqual(e.x, 2 * math.sqrt(0.5), 6)
|
|
self.assertAlmostEqual(e.y, 2 * math.sqrt(0.5), 6)
|
|
self.assertAlmostEqual(e.z, 0)
|
|
|
|
def test_from_expmap(self):
|
|
e = Vector((1, 1, 0))
|
|
q = Quaternion(e)
|
|
axis, angle = q.to_axis_angle()
|
|
|
|
self.assertAlmostEqual(angle, math.sqrt(2), 6)
|
|
self.assertAlmostEqual(axis.x, math.sqrt(0.5), 6)
|
|
self.assertAlmostEqual(axis.y, math.sqrt(0.5), 6)
|
|
self.assertAlmostEqual(axis.z, 0)
|
|
|
|
|
|
class KDTreeTesting(unittest.TestCase):
|
|
@staticmethod
|
|
def kdtree_create_grid_3d_data(tot):
|
|
index = 0
|
|
mul = 1.0 / (tot - 1)
|
|
for x in range(tot):
|
|
for y in range(tot):
|
|
for z in range(tot):
|
|
yield (x * mul, y * mul, z * mul), index
|
|
index += 1
|
|
|
|
@staticmethod
|
|
def kdtree_create_grid_3d(tot, *, filter_fn=None):
|
|
k = kdtree.KDTree(tot * tot * tot)
|
|
for co, index in KDTreeTesting.kdtree_create_grid_3d_data(tot):
|
|
if (filter_fn is not None) and (not filter_fn(co, index)):
|
|
continue
|
|
k.insert(co, index)
|
|
k.balance()
|
|
return k
|
|
|
|
def assertAlmostEqualVector(self, first, second, places=7, msg=None, delta=None):
|
|
self.assertAlmostEqual(first[0], second[0], places=places, msg=msg, delta=delta)
|
|
self.assertAlmostEqual(first[1], second[1], places=places, msg=msg, delta=delta)
|
|
self.assertAlmostEqual(first[2], second[2], places=places, msg=msg, delta=delta)
|
|
|
|
def test_kdtree_single(self):
|
|
co = (0,) * 3
|
|
index = 2
|
|
|
|
k = kdtree.KDTree(1)
|
|
k.insert(co, index)
|
|
k.balance()
|
|
|
|
co_found, index_found, dist_found = k.find(co)
|
|
|
|
self.assertEqual(tuple(co_found), co)
|
|
self.assertEqual(index_found, index)
|
|
self.assertEqual(dist_found, 0.0)
|
|
|
|
def test_kdtree_empty(self):
|
|
co = (0,) * 3
|
|
|
|
k = kdtree.KDTree(0)
|
|
k.balance()
|
|
|
|
co_found, index_found, dist_found = k.find(co)
|
|
|
|
self.assertIsNone(co_found)
|
|
self.assertIsNone(index_found)
|
|
self.assertIsNone(dist_found)
|
|
|
|
def test_kdtree_line(self):
|
|
tot = 10
|
|
|
|
k = kdtree.KDTree(tot)
|
|
|
|
for i in range(tot):
|
|
k.insert((i,) * 3, i)
|
|
|
|
k.balance()
|
|
|
|
co_found, index_found, dist_found = k.find((-1,) * 3)
|
|
self.assertEqual(tuple(co_found), (0,) * 3)
|
|
|
|
co_found, index_found, dist_found = k.find((tot,) * 3)
|
|
self.assertEqual(tuple(co_found), (tot - 1,) * 3)
|
|
|
|
def test_kdtree_grid(self):
|
|
size = 10
|
|
k = self.kdtree_create_grid_3d(size)
|
|
|
|
# find_range
|
|
ret = k.find_range((0.5,) * 3, 2.0)
|
|
self.assertEqual(len(ret), size * size * size)
|
|
|
|
ret = k.find_range((1.0,) * 3, 1.0 / size)
|
|
self.assertEqual(len(ret), 1)
|
|
|
|
ret = k.find_range((1.0,) * 3, 2.0 / size)
|
|
self.assertEqual(len(ret), 8)
|
|
|
|
ret = k.find_range((10,) * 3, 0.5)
|
|
self.assertEqual(len(ret), 0)
|
|
|
|
# find_n
|
|
tot = 0
|
|
ret = k.find_n((1.0,) * 3, tot)
|
|
self.assertEqual(len(ret), tot)
|
|
|
|
tot = 10
|
|
ret = k.find_n((1.0,) * 3, tot)
|
|
self.assertEqual(len(ret), tot)
|
|
self.assertEqual(ret[0][2], 0.0)
|
|
|
|
tot = size * size * size
|
|
ret = k.find_n((1.0,) * 3, tot)
|
|
self.assertEqual(len(ret), tot)
|
|
|
|
def test_kdtree_grid_filter_simple(self):
|
|
size = 10
|
|
k = self.kdtree_create_grid_3d(size)
|
|
|
|
# filter exact index
|
|
ret_regular = k.find((1.0,) * 3)
|
|
ret_filter = k.find((1.0,) * 3, filter=lambda i: i == ret_regular[1])
|
|
self.assertEqual(ret_regular, ret_filter)
|
|
ret_filter = k.find((-1.0,) * 3, filter=lambda i: i == ret_regular[1])
|
|
self.assertEqual(ret_regular[:2], ret_filter[:2]) # ignore distance
|
|
|
|
def test_kdtree_grid_filter_pairs(self):
|
|
size = 10
|
|
k_all = self.kdtree_create_grid_3d(size)
|
|
k_odd = self.kdtree_create_grid_3d(size, filter_fn=lambda co, i: (i % 2) == 1)
|
|
k_evn = self.kdtree_create_grid_3d(size, filter_fn=lambda co, i: (i % 2) == 0)
|
|
|
|
samples = 5
|
|
mul = 1 / (samples - 1)
|
|
for x in range(samples):
|
|
for y in range(samples):
|
|
for z in range(samples):
|
|
co = (x * mul, y * mul, z * mul)
|
|
|
|
ret_regular = k_odd.find(co)
|
|
self.assertEqual(ret_regular[1] % 2, 1)
|
|
ret_filter = k_all.find(co, filter=lambda i: (i % 2) == 1)
|
|
self.assertAlmostEqualVector(ret_regular, ret_filter)
|
|
|
|
ret_regular = k_evn.find(co)
|
|
self.assertEqual(ret_regular[1] % 2, 0)
|
|
ret_filter = k_all.find(co, filter=lambda i: (i % 2) == 0)
|
|
self.assertAlmostEqualVector(ret_regular, ret_filter)
|
|
|
|
# filter out all values (search odd tree for even values and the reverse)
|
|
co = (0,) * 3
|
|
ret_filter = k_odd.find(co, filter=lambda i: (i % 2) == 0)
|
|
self.assertEqual(ret_filter[1], None)
|
|
|
|
ret_filter = k_evn.find(co, filter=lambda i: (i % 2) == 1)
|
|
self.assertEqual(ret_filter[1], None)
|
|
|
|
def test_kdtree_invalid_size(self):
|
|
with self.assertRaises(ValueError):
|
|
kdtree.KDTree(-1)
|
|
|
|
def test_kdtree_invalid_balance(self):
|
|
co = (0,) * 3
|
|
index = 2
|
|
|
|
k = kdtree.KDTree(2)
|
|
k.insert(co, index)
|
|
k.balance()
|
|
k.insert(co, index)
|
|
with self.assertRaises(RuntimeError):
|
|
k.find(co)
|
|
|
|
def test_kdtree_invalid_filter(self):
|
|
k = kdtree.KDTree(1)
|
|
k.insert((0,) * 3, 0)
|
|
k.balance()
|
|
# not callable
|
|
with self.assertRaises(TypeError):
|
|
k.find((0,) * 3, filter=None)
|
|
# no args
|
|
with self.assertRaises(TypeError):
|
|
k.find((0,) * 3, filter=lambda: None)
|
|
# bad return value
|
|
with self.assertRaises(ValueError):
|
|
k.find((0,) * 3, filter=lambda i: None)
|
|
|
|
|
|
class TesselatePolygon(unittest.TestCase):
|
|
def test_empty(self):
|
|
self.assertEqual([], geometry.tessellate_polygon([]))
|
|
|
|
def test_2d(self):
|
|
polyline = [
|
|
Vector((-0.14401324093341827, 0.1266411542892456)),
|
|
Vector((-0.14401324093341827, 0.13)),
|
|
Vector((0.13532273471355438, 0.1266411542892456)),
|
|
Vector((0.13532273471355438, 0.13)),
|
|
]
|
|
expect = [(0, 1, 2), (0, 3, 2)]
|
|
self.assertEqual(expect, geometry.tessellate_polygon([polyline]))
|
|
|
|
def test_3d(self):
|
|
polyline = [
|
|
Vector((-0.14401324093341827, 0.1266411542892456, -0.13966798782348633)),
|
|
Vector((-0.14401324093341827, 0.1266411542892456, 0.13966798782348633)),
|
|
Vector((0.13532273471355438, 0.1266411542892456, 0.13966798782348633)),
|
|
Vector((0.13532273471355438, 0.1266411542892456, -0.13966798782348633)),
|
|
]
|
|
expect = [(2, 3, 0), (2, 0, 1)]
|
|
self.assertEqual(expect, geometry.tessellate_polygon([polyline]))
|
|
|
|
def test_3d_degenerate(self):
|
|
polyline = [
|
|
Vector((-0.14401324093341827, -0.15269476175308228, -0.13966798782348633)),
|
|
Vector((0.13532273471355438, -0.15269476175308228, -0.13966798782348633)),
|
|
Vector((0.13532273471355438, -0.15269476175308228, -0.13966798782348633)),
|
|
Vector((-0.14401324093341827, -0.15269476175308228, -0.13966798782348633)),
|
|
]
|
|
# If this returns a proper result, rather than [(0, 0, 0)], it could mean that
|
|
# degenerate geometry is handled properly.
|
|
expect = [(0, 0, 0)]
|
|
self.assertEqual(expect, geometry.tessellate_polygon([polyline]))
|
|
|
|
|
|
if __name__ == '__main__':
|
|
import sys
|
|
sys.argv = [__file__] + (sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [])
|
|
unittest.main()
|