1233 lines
41 KiB
Python
1233 lines
41 KiB
Python
# SPDX-FileCopyrightText: 2011-2022 Blender Authors
|
|
#
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
|
|
# ./blender.bin --background --python tests/python/bl_pyapi_mathutils.py -- --verbose
|
|
import unittest
|
|
from mathutils import Matrix, Vector, Quaternion, Euler, Color
|
|
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),)
|
|
|
|
|
|
def _test_flat_buffer_protocol(self, ty, n):
|
|
expected = list(range(n))
|
|
data = ty(expected)
|
|
view = memoryview(data)
|
|
|
|
self.assertEqual(view.shape, (n,))
|
|
self.assertEqual(view.format, "f")
|
|
self.assertEqual(view.tolist(), expected)
|
|
|
|
# Check multiple simultaneous.
|
|
with self.assertRaises(BufferError):
|
|
memoryview(data)
|
|
|
|
# Check frozen.
|
|
with self.assertRaises(BufferError):
|
|
data.freeze()
|
|
|
|
# Check resize.
|
|
if ty is Vector:
|
|
with self.assertRaises(BufferError):
|
|
data.resize(100)
|
|
|
|
_incref = view # For potential changes in GC.
|
|
|
|
# Check for a release buffer call, GC releases the buffer if it's not referenced.
|
|
data = ty(expected)
|
|
memoryview(data)
|
|
memoryview(data)
|
|
|
|
vec = ty(expected)
|
|
vec.freeze()
|
|
view = memoryview(vec)
|
|
with self.assertRaises(TypeError):
|
|
view[0] = 1
|
|
|
|
|
|
class GenericSliceMixIn:
|
|
"""
|
|
Slice get/set tests parameterized by ``generic_len``
|
|
(the length of the slice-target sequence).
|
|
Sub-classes must:
|
|
- Set ``generic_len``.
|
|
- Implement ``generic_make(values)`` returning a fresh instance whose
|
|
contents equal ``values``. Required because some types (e.g. ``MatrixAccess``) can't be constructed by type.
|
|
- Inherit ``unittest.TestCase``.
|
|
"""
|
|
generic_len = 0
|
|
|
|
def generic_make(self, values):
|
|
raise NotImplementedError
|
|
|
|
def test_slice_get(self):
|
|
base = tuple(float(i + 1) for i in range(self.generic_len))
|
|
obj = self.generic_make(base)
|
|
self.assertIsInstance(obj[:], tuple)
|
|
# Step 1.
|
|
self.assertEqual(obj[:], base)
|
|
self.assertEqual(obj[1:], base[1:])
|
|
self.assertEqual(obj[:-1], base[:-1])
|
|
self.assertEqual(obj[-1:], base[-1:])
|
|
self.assertEqual(obj[1:1], ())
|
|
# Stepped.
|
|
self.assertEqual(obj[::2], base[::2])
|
|
self.assertEqual(obj[::-1], base[::-1])
|
|
self.assertEqual(obj[::-2], base[::-2])
|
|
self.assertEqual(obj[1:1:-1], ())
|
|
|
|
def test_slice_set(self):
|
|
base = tuple(float(i + 1) for i in range(self.generic_len))
|
|
# Forward full overwrite (fast path).
|
|
obj = self.generic_make(base)
|
|
new_full = tuple(10.0 * (i + 1) for i in range(self.generic_len))
|
|
obj[:] = new_full
|
|
self.assertEqual(tuple(obj), new_full)
|
|
# Reverse full overwrite (fast path).
|
|
obj[::-1] = base
|
|
self.assertEqual(tuple(obj), base[::-1])
|
|
# Partial step-1 (slow path).
|
|
obj = self.generic_make(base)
|
|
new_tail = tuple(50.0 * (i + 1) for i in range(self.generic_len - 1))
|
|
obj[1:] = new_tail
|
|
self.assertEqual(tuple(obj), (base[0],) + new_tail)
|
|
# Stepped partial (slow path).
|
|
obj = self.generic_make(base)
|
|
every_other = base[::2]
|
|
new_every_other = tuple(99.0 + i for i in range(len(every_other)))
|
|
obj[::2] = new_every_other
|
|
expected = list(base)
|
|
for i, v in zip(range(0, self.generic_len, 2), new_every_other):
|
|
expected[i] = v
|
|
self.assertEqual(tuple(obj), tuple(expected))
|
|
# Empty extended slice with empty seq is a no-op.
|
|
obj = self.generic_make(base)
|
|
obj[5:2:1] = ()
|
|
self.assertEqual(tuple(obj), base)
|
|
|
|
def test_slice_set_length_mismatch(self):
|
|
base = tuple(float(i + 1) for i in range(self.generic_len))
|
|
obj = self.generic_make(base)
|
|
with self.assertRaises(ValueError):
|
|
obj[:] = base[:-1]
|
|
with self.assertRaises(ValueError):
|
|
obj[::2] = base
|
|
with self.assertRaises(ValueError):
|
|
obj[5:2:1] = (1.0,)
|
|
self.assertEqual(tuple(obj), base)
|
|
|
|
def test_slice_set_frozen(self):
|
|
base = tuple(float(i + 1) for i in range(self.generic_len))
|
|
obj = self.generic_make(base)
|
|
obj.freeze()
|
|
# Fast path (full overwrite, frozen check only).
|
|
with self.assertRaises(TypeError):
|
|
obj[:] = base
|
|
# Slow path (partial, frozen check inside `ReadCallback_ForWrite`).
|
|
with self.assertRaises(TypeError):
|
|
obj[::2] = base[::2]
|
|
self.assertEqual(tuple(obj), base)
|
|
|
|
def test_slice_set_self_aliased_reverse(self):
|
|
# `obj[::-1] = obj` must reverse in place. Exercises the parse-through-seq
|
|
# sync path that silently undoes the `is_subset=false` fast-path skip.
|
|
base = tuple(float(i + 1) for i in range(self.generic_len))
|
|
obj = self.generic_make(base)
|
|
obj[::-1] = obj
|
|
self.assertEqual(tuple(obj), base[::-1])
|
|
|
|
def test_slice_set_self_aliased_full(self):
|
|
# `obj[:] = obj` writes original values back; effectively a no-op.
|
|
base = tuple(float(i + 1) for i in range(self.generic_len))
|
|
obj = self.generic_make(base)
|
|
obj[:] = obj
|
|
self.assertEqual(tuple(obj), base)
|
|
|
|
def test_slice_set_type_error_atomic(self):
|
|
# A bad element in seq must raise TypeError without partially mutating the object.
|
|
# Catches a future "optimization" that drops atomicity.
|
|
base = tuple(float(i + 1) for i in range(self.generic_len))
|
|
obj = self.generic_make(base)
|
|
# `obj[-2:]` is `slice_length=2` for any `generic_len >= 2`,
|
|
# so the parse failure is uniformly on the bad element rather
|
|
# than depending on whether a length-mismatch check fires first.
|
|
with self.assertRaises(TypeError):
|
|
obj[-2:] = [99.0, "not_a_float"]
|
|
self.assertEqual(tuple(obj), base)
|
|
|
|
def test_slice_set_identity_preserved(self):
|
|
# In-place slice assignment must not replace the underlying object.
|
|
base = tuple(float(i + 1) for i in range(self.generic_len))
|
|
obj = self.generic_make(base)
|
|
original_id = id(obj)
|
|
obj[:] = base
|
|
self.assertEqual(id(obj), original_id)
|
|
obj[::2] = base[::2]
|
|
self.assertEqual(id(obj), original_id)
|
|
|
|
def test_slice_set_seq_longer_than_slice(self):
|
|
# Assigning a sequence longer than the slice must raise ValueError,
|
|
# not silently truncate to the slice length.
|
|
base = tuple(float(i + 1) for i in range(self.generic_len))
|
|
obj = self.generic_make(base)
|
|
# `obj[0:1]` is a 1-length slice; pass 2 elements.
|
|
with self.assertRaises(ValueError):
|
|
obj[0:1] = [99.0, 98.0]
|
|
self.assertEqual(tuple(obj), base)
|
|
|
|
def test_slice_set_nonempty_seq_into_empty_slice(self):
|
|
# Assigning a non-empty sequence to an empty slice must raise ValueError,
|
|
# not silently no-op.
|
|
base = tuple(float(i + 1) for i in range(self.generic_len))
|
|
obj = self.generic_make(base)
|
|
with self.assertRaises(ValueError):
|
|
obj[1:1] = [99.0]
|
|
self.assertEqual(tuple(obj), base)
|
|
|
|
def test_slice_set_empty_slice_empty_seq(self):
|
|
# Empty slice with an empty sequence is a no-op.
|
|
base = tuple(float(i + 1) for i in range(self.generic_len))
|
|
obj = self.generic_make(base)
|
|
obj[0:0] = ()
|
|
self.assertEqual(tuple(obj), base)
|
|
obj[1:1] = ()
|
|
self.assertEqual(tuple(obj), base)
|
|
obj[-1:-1] = ()
|
|
self.assertEqual(tuple(obj), base)
|
|
|
|
|
|
class MatrixTesting(unittest.TestCase):
|
|
|
|
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)
|
|
|
|
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 test_matrix_freeze_is_read_only(self):
|
|
mat = Matrix(((1, 1, 1),) * 3)
|
|
mat.freeze()
|
|
with self.assertRaises(ValueError):
|
|
mat.resize_4x4()
|
|
with self.assertRaises(TypeError):
|
|
mat[0][0] = 0.0
|
|
|
|
def test_matrix_freeze_iter(self):
|
|
rows = (
|
|
(1.0, 2.0, 3.0),
|
|
(4.0, 5.0, 6.0),
|
|
(7.0, 8.0, 9.0),
|
|
)
|
|
cols = tuple(zip(*rows))
|
|
mat = Matrix(rows)
|
|
mat.freeze()
|
|
self.assertEqual(tuple(tuple(v) for v in mat), rows)
|
|
self.assertEqual(tuple(tuple(v) for v in mat.row), rows)
|
|
self.assertEqual(tuple(tuple(v) for v in mat.col), cols)
|
|
|
|
def test_buffer_protocol(self):
|
|
expected = [list(range(i * 4, (i * 4) + 4)) for i in range(4)]
|
|
m = Matrix(expected)
|
|
view = memoryview(m)
|
|
|
|
self.assertEqual(view.shape, (4, 4))
|
|
self.assertEqual(view.format, "f")
|
|
self.assertEqual(view.tolist(), expected)
|
|
|
|
|
|
class MatrixSliceMixIn:
|
|
"""
|
|
Slice get/set tests for the ``Matrix`` row sequence.
|
|
Items are row ``Vector`` of length ``matrix_size``.
|
|
|
|
``matrix_access_attr`` selects which sequence is being sliced:
|
|
``""`` for the matrix itself (``mat[i:j]``), or ``"row"`` / ``"col"``
|
|
for the ``MatrixAccess`` wrappers (``mat.row[i:j]`` / ``mat.col[i:j]``).
|
|
"""
|
|
matrix_size = 0
|
|
matrix_access_attr = ""
|
|
|
|
def _make_value(self, seed):
|
|
"""
|
|
A distinct ``Vector`` value for the given integer seed.
|
|
"""
|
|
return Vector(tuple(float(seed * 100 + j) for j in range(self.matrix_size)))
|
|
|
|
def _make_obj(self, values):
|
|
"""
|
|
Build a `Matrix` whose rows or columns equal ``values``,
|
|
and return ``(mat, accessor)`` where ``accessor`` is the
|
|
sequence being sliced (the matrix itself, or ``mat.row`` / ``mat.col``).
|
|
"""
|
|
n = self.matrix_size
|
|
if self.matrix_access_attr == "col":
|
|
rows = tuple(
|
|
tuple(values[c][r] for c in range(n))
|
|
for r in range(n)
|
|
)
|
|
else:
|
|
assert self.matrix_access_attr in {"", "row"}
|
|
rows = tuple(tuple(v) for v in values)
|
|
mat = Matrix(rows)
|
|
accessor = mat if self.matrix_access_attr == "" else getattr(mat, self.matrix_access_attr)
|
|
return mat, accessor
|
|
|
|
def test_slice_set_type_error_atomic(self):
|
|
# A non-sequence element must raise TypeError without mutating the matrix.
|
|
# `99.0` isn't a row/col-sequence so the parse fails on it.
|
|
n = self.matrix_size
|
|
base = tuple(self._make_value(i + 1) for i in range(n))
|
|
_mat, obj = self._make_obj(base)
|
|
# `obj[-2:]` is `slice_length=2` for any `generic_len >= 2`,
|
|
# so the parse failure is uniformly on the bad element rather
|
|
# than depending on whether a length-mismatch check fires first.
|
|
with self.assertRaises(TypeError):
|
|
obj[-2:] = [99.0, "not_a_float"]
|
|
self.assertEqual(tuple(obj), base)
|
|
|
|
def test_slice_get(self):
|
|
n = self.matrix_size
|
|
base = tuple(self._make_value(i + 1) for i in range(n))
|
|
_mat, obj = self._make_obj(base)
|
|
self.assertIsInstance(obj[:], tuple)
|
|
# Step 1.
|
|
self.assertEqual(obj[:], base)
|
|
self.assertEqual(obj[1:], base[1:])
|
|
self.assertEqual(obj[:-1], base[:-1])
|
|
self.assertEqual(obj[-1:], base[-1:])
|
|
self.assertEqual(obj[1:1], ())
|
|
# Stepped.
|
|
self.assertEqual(obj[::2], base[::2])
|
|
self.assertEqual(obj[::-1], base[::-1])
|
|
self.assertEqual(obj[::-2], base[::-2])
|
|
self.assertEqual(obj[1:1:-1], ())
|
|
|
|
def test_slice_set(self):
|
|
n = self.matrix_size
|
|
base = tuple(self._make_value(i + 1) for i in range(n))
|
|
# Forward full overwrite (fast path).
|
|
_mat, obj = self._make_obj(base)
|
|
new_full = tuple(self._make_value(10 * (i + 1)) for i in range(n))
|
|
obj[:] = new_full
|
|
self.assertEqual(tuple(obj), new_full)
|
|
# Reverse full overwrite (fast path).
|
|
obj[::-1] = base
|
|
self.assertEqual(tuple(obj), base[::-1])
|
|
# Partial step-1 (slow path).
|
|
_mat, obj = self._make_obj(base)
|
|
new_tail = tuple(self._make_value(50 * (i + 1)) for i in range(n - 1))
|
|
obj[1:] = new_tail
|
|
self.assertEqual(tuple(obj), (base[0],) + new_tail)
|
|
# Stepped partial (slow path).
|
|
_mat, obj = self._make_obj(base)
|
|
every_other = base[::2]
|
|
new_every_other = tuple(self._make_value(99 + i) for i in range(len(every_other)))
|
|
obj[::2] = new_every_other
|
|
expected = list(base)
|
|
for i, v in zip(range(0, n, 2), new_every_other):
|
|
expected[i] = v
|
|
self.assertEqual(tuple(obj), tuple(expected))
|
|
# Empty extended slice with empty seq is a no-op.
|
|
_mat, obj = self._make_obj(base)
|
|
obj[5:2:1] = ()
|
|
self.assertEqual(tuple(obj), base)
|
|
|
|
def test_slice_set_length_mismatch(self):
|
|
n = self.matrix_size
|
|
base = tuple(self._make_value(i + 1) for i in range(n))
|
|
_mat, obj = self._make_obj(base)
|
|
with self.assertRaises(ValueError):
|
|
obj[:] = base[:-1]
|
|
with self.assertRaises(ValueError):
|
|
obj[::2] = base
|
|
with self.assertRaises(ValueError):
|
|
obj[5:2:1] = (self._make_value(1),)
|
|
self.assertEqual(tuple(obj), base)
|
|
|
|
def test_slice_set_frozen(self):
|
|
n = self.matrix_size
|
|
base = tuple(self._make_value(i + 1) for i in range(n))
|
|
mat, obj = self._make_obj(base)
|
|
# `MatrixAccess` has no `freeze()`; freezing the matrix is enough either way.
|
|
mat.freeze()
|
|
# Fast path (full overwrite, frozen check only).
|
|
with self.assertRaises(TypeError):
|
|
obj[:] = base
|
|
# Slow path (partial, frozen check inside ReadCallback_ForWrite).
|
|
with self.assertRaises(TypeError):
|
|
obj[::2] = base[::2]
|
|
self.assertEqual(tuple(obj), base)
|
|
|
|
def test_slice_set_self_aliased_reverse(self):
|
|
# `obj[::-1] = obj` reverses rows/cols in place.
|
|
# Atomicity comes from staging all parsed rows/cols before assigning.
|
|
n = self.matrix_size
|
|
base = tuple(self._make_value(i + 1) for i in range(n))
|
|
_mat, obj = self._make_obj(base)
|
|
obj[::-1] = obj
|
|
self.assertEqual(tuple(obj), base[::-1])
|
|
|
|
def test_slice_set_self_aliased_full(self):
|
|
# `obj[:] = obj` writes original values back; effectively a no-op.
|
|
n = self.matrix_size
|
|
base = tuple(self._make_value(i + 1) for i in range(n))
|
|
_mat, obj = self._make_obj(base)
|
|
obj[:] = obj
|
|
self.assertEqual(tuple(obj), base)
|
|
|
|
def test_slice_set_identity_preserved(self):
|
|
# Slice assignment must not replace the matrix or its `MatrixAccess` wrapper.
|
|
n = self.matrix_size
|
|
base = tuple(self._make_value(i + 1) for i in range(n))
|
|
_mat, obj = self._make_obj(base)
|
|
original_id = id(obj)
|
|
obj[:] = base
|
|
self.assertEqual(id(obj), original_id)
|
|
obj[::2] = base[::2]
|
|
self.assertEqual(id(obj), original_id)
|
|
|
|
|
|
class Matrix3x3TestingSlice(MatrixSliceMixIn, unittest.TestCase):
|
|
matrix_size = 3
|
|
matrix_access_attr = ""
|
|
|
|
|
|
class Matrix4x4TestingSlice(MatrixSliceMixIn, unittest.TestCase):
|
|
matrix_size = 4
|
|
matrix_access_attr = ""
|
|
|
|
|
|
class MatrixAccess3x3RowTestingSlice(MatrixSliceMixIn, unittest.TestCase):
|
|
matrix_size = 3
|
|
matrix_access_attr = "row"
|
|
|
|
|
|
class MatrixAccess3x3ColTestingSlice(MatrixSliceMixIn, unittest.TestCase):
|
|
matrix_size = 3
|
|
matrix_access_attr = "col"
|
|
|
|
|
|
class MatrixAccess4x4RowTestingSlice(MatrixSliceMixIn, unittest.TestCase):
|
|
matrix_size = 4
|
|
matrix_access_attr = "row"
|
|
|
|
|
|
class MatrixAccess4x4ColTestingSlice(MatrixSliceMixIn, unittest.TestCase):
|
|
matrix_size = 4
|
|
matrix_access_attr = "col"
|
|
|
|
|
|
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)
|
|
|
|
def test_vector_freeze_is_read_only(self):
|
|
vec = Vector((1, 3, 5))
|
|
|
|
vec.freeze()
|
|
with self.assertRaises(ValueError):
|
|
vec.resize(2)
|
|
with self.assertRaises(TypeError):
|
|
vec[0] = 0.0
|
|
|
|
def test_buffer_protocol(self):
|
|
_test_flat_buffer_protocol(self, Vector, 10)
|
|
|
|
|
|
class VectorTestingSlice2(GenericSliceMixIn, unittest.TestCase):
|
|
generic_len = 2
|
|
|
|
def generic_make(self, values):
|
|
return Vector(values)
|
|
|
|
|
|
class VectorTestingSlice3(GenericSliceMixIn, unittest.TestCase):
|
|
generic_len = 3
|
|
|
|
def generic_make(self, values):
|
|
return Vector(values)
|
|
|
|
|
|
class VectorTestingSlice4(GenericSliceMixIn, unittest.TestCase):
|
|
generic_len = 4
|
|
|
|
def generic_make(self, values):
|
|
return Vector(values)
|
|
|
|
|
|
class VectorTestingSlice10(GenericSliceMixIn, unittest.TestCase):
|
|
generic_len = 10
|
|
|
|
def generic_make(self, values):
|
|
return Vector(values)
|
|
|
|
|
|
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)
|
|
|
|
def test_buffer_protocol(self):
|
|
_test_flat_buffer_protocol(self, Quaternion, 4)
|
|
|
|
|
|
class QuaternionTestingSlice(GenericSliceMixIn, unittest.TestCase):
|
|
generic_len = 4
|
|
|
|
def generic_make(self, values):
|
|
return Quaternion(values)
|
|
|
|
|
|
class EulerTesting(unittest.TestCase):
|
|
|
|
def test_buffer_protocol(self):
|
|
_test_flat_buffer_protocol(self, Euler, 3)
|
|
|
|
|
|
class EulerTestingSlice(GenericSliceMixIn, unittest.TestCase):
|
|
generic_len = 3
|
|
|
|
def generic_make(self, values):
|
|
return Euler(values)
|
|
|
|
|
|
class ColorTesting(unittest.TestCase):
|
|
|
|
def test_buffer_protocol(self):
|
|
_test_flat_buffer_protocol(self, Color, 3)
|
|
|
|
|
|
class ColorTestingSlice(GenericSliceMixIn, unittest.TestCase):
|
|
generic_len = 3
|
|
|
|
def generic_make(self, values):
|
|
return Color(values)
|
|
|
|
|
|
# Test features of `mathutils` types.
|
|
class TypeTesting(unittest.TestCase):
|
|
|
|
def test_keywords_unsupported_matrix(self):
|
|
with self.assertRaises(TypeError) as context:
|
|
Matrix(dummy=None)
|
|
self.assertEqual(str(context.exception), "Matrix(): takes no keyword args")
|
|
|
|
def test_keywords_unsupported_vector(self):
|
|
with self.assertRaises(TypeError) as context:
|
|
Vector(dummy=None)
|
|
self.assertEqual(str(context.exception), "Vector(): takes no keyword args")
|
|
|
|
def test_keywords_unsupported_color(self):
|
|
with self.assertRaises(TypeError) as context:
|
|
Color(dummy=None)
|
|
self.assertEqual(str(context.exception), "mathutils.Color(): takes no keyword args")
|
|
|
|
def test_keywords_unsupported_euler(self):
|
|
with self.assertRaises(TypeError) as context:
|
|
Euler(dummy=None)
|
|
self.assertEqual(str(context.exception), "mathutils.Euler(): takes no keyword args")
|
|
|
|
def test_subclass_matrix(self):
|
|
class MyMatrix(Matrix):
|
|
__slots__ = (
|
|
"offset",
|
|
)
|
|
|
|
def __new__(cls, *args, **kw):
|
|
del kw
|
|
return super().__new__(cls, *args)
|
|
|
|
def __init__(self, *args, offset=0.0):
|
|
self.offset = offset
|
|
|
|
def __str__(self):
|
|
return "{:s}({!r})".format(type(self).__name__, tuple(tuple(x) for x in self))
|
|
|
|
def my_sum_and_offset(self):
|
|
return sum(iter([i for v in self for i in v])) + self.offset
|
|
|
|
value = MyMatrix(offset=1.0)
|
|
self.assertEqual(
|
|
str(value),
|
|
"MyMatrix(((1.0, 0.0, 0.0, 0.0), (0.0, 1.0, 0.0, 0.0), (0.0, 0.0, 1.0, 0.0), (0.0, 0.0, 0.0, 1.0)))",
|
|
)
|
|
self.assertEqual(value.my_sum_and_offset(), 5.0)
|
|
value.offset += 1.0
|
|
self.assertEqual(value.my_sum_and_offset(), 6.0)
|
|
|
|
value = MyMatrix(((1.0, 2.0, 3.0), (4.0, 5.0, 6.0), (7.0, 8.0, 9.0)), offset=5.0)
|
|
self.assertEqual(str(value), "MyMatrix(((1.0, 2.0, 3.0), (4.0, 5.0, 6.0), (7.0, 8.0, 9.0)))")
|
|
self.assertEqual(value.my_sum_and_offset(), 50.0)
|
|
|
|
# Ensure `__slots__` is working.
|
|
with self.assertRaises(AttributeError):
|
|
value.offset_other = 5.0
|
|
|
|
def test_subclass_vector(self):
|
|
class MyVector(Vector):
|
|
__slots__ = (
|
|
"offset",
|
|
)
|
|
|
|
def __new__(cls, *args, **kw):
|
|
del kw
|
|
return super().__new__(cls, *args)
|
|
|
|
def __init__(self, *args, offset=0.0):
|
|
self.offset = offset
|
|
|
|
def __str__(self):
|
|
return "{:s}({!r})".format(type(self).__name__, tuple(self))
|
|
|
|
def my_sum_and_offset(self):
|
|
return sum(iter(self)) + self.offset
|
|
|
|
value = MyVector(offset=1.0)
|
|
self.assertEqual(str(value), "MyVector((0.0, 0.0, 0.0))")
|
|
self.assertEqual(value.my_sum_and_offset(), 1.0)
|
|
value.offset += 1.0
|
|
self.assertEqual(value.my_sum_and_offset(), 2.0)
|
|
|
|
value = MyVector((1.0, 2.0, 3.0), offset=4.0)
|
|
self.assertEqual(str(value), "MyVector((1.0, 2.0, 3.0))")
|
|
self.assertEqual(value.my_sum_and_offset(), 10.0)
|
|
|
|
# Ensure `__slots__` is working.
|
|
with self.assertRaises(AttributeError):
|
|
value.offset_other = 5.0
|
|
|
|
def test_subclass_color(self):
|
|
class MyColor(Color):
|
|
__slots__ = (
|
|
"offset",
|
|
)
|
|
|
|
def __new__(cls, *args, **kw):
|
|
del kw
|
|
return super().__new__(cls, *args)
|
|
|
|
def __init__(self, *args, offset=0.0):
|
|
self.offset = offset
|
|
|
|
def __str__(self):
|
|
return "{:s}({!r})".format(type(self).__name__, tuple(self))
|
|
|
|
def my_sum_and_offset(self):
|
|
return sum(iter(self)) + self.offset
|
|
|
|
value = MyColor(offset=1.0)
|
|
self.assertEqual(str(value), "MyColor((0.0, 0.0, 0.0))")
|
|
self.assertEqual(value.my_sum_and_offset(), 1.0)
|
|
|
|
value = MyColor((1.0, 2.0, 3.0), offset=4.0)
|
|
self.assertEqual(str(value), "MyColor((1.0, 2.0, 3.0))")
|
|
self.assertEqual(value.my_sum_and_offset(), 10.0)
|
|
|
|
# Ensure `__slots__` is working.
|
|
with self.assertRaises(AttributeError):
|
|
value.offset_other = 5.0
|
|
value.offset += 1.0
|
|
|
|
def test_subclass_quaternion(self):
|
|
class MyQuaternion(Quaternion):
|
|
__slots__ = (
|
|
"offset",
|
|
)
|
|
|
|
def __new__(cls, *args, **_kw):
|
|
return super().__new__(cls, *args)
|
|
|
|
def __init__(self, *args, offset=0.0):
|
|
self.offset = offset
|
|
|
|
def __str__(self):
|
|
return "{:s}({!r})".format(type(self).__name__, tuple(self))
|
|
|
|
def my_sum_and_offset(self):
|
|
return sum(iter(self)) + self.offset
|
|
|
|
value = MyQuaternion(offset=1.0)
|
|
self.assertEqual(str(value), "MyQuaternion((1.0, 0.0, 0.0, 0.0))")
|
|
self.assertEqual(value.my_sum_and_offset(), 2.0)
|
|
value.offset += 1.0
|
|
self.assertEqual(value.my_sum_and_offset(), 3.0)
|
|
|
|
value = MyQuaternion((1.0, 2.0, 3.0, 4.0), offset=4.0)
|
|
self.assertEqual(str(value), "MyQuaternion((1.0, 2.0, 3.0, 4.0))")
|
|
self.assertEqual(value.my_sum_and_offset(), 14.0)
|
|
|
|
# Ensure `__slots__` is working.
|
|
with self.assertRaises(AttributeError):
|
|
value.offset_other = 5.0
|
|
|
|
def test_subclass_euler(self):
|
|
class MyEuler(Euler):
|
|
__slots__ = (
|
|
"offset",
|
|
)
|
|
|
|
def __new__(cls, *args, **kw):
|
|
del kw
|
|
return super().__new__(cls, *args)
|
|
|
|
def __init__(self, *args, offset=0.0):
|
|
self.offset = offset
|
|
|
|
def __str__(self):
|
|
return "{:s}({!r})".format(type(self).__name__, tuple(self))
|
|
|
|
def my_sum_and_offset(self):
|
|
return sum(iter(self)) + self.offset
|
|
|
|
value = MyEuler(offset=1.0)
|
|
self.assertEqual(str(value), "MyEuler((0.0, 0.0, 0.0))")
|
|
self.assertEqual(value.my_sum_and_offset(), 1.0)
|
|
value.offset += 1.0
|
|
self.assertEqual(value.my_sum_and_offset(), 2.0)
|
|
|
|
value = MyEuler((1.0, 2.0, 3.0), offset=4.0)
|
|
self.assertEqual(str(value), "MyEuler((1.0, 2.0, 3.0))")
|
|
self.assertEqual(value.my_sum_and_offset(), 10.0)
|
|
|
|
# Ensure `__slots__` is working.
|
|
with self.assertRaises(AttributeError):
|
|
value.offset_other = 5.0
|
|
value.offset += 1.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_without_balance(self):
|
|
co = (0,) * 3
|
|
|
|
k = kdtree.KDTree(1)
|
|
k.insert(co, 1)
|
|
with self.assertRaises(RuntimeError):
|
|
k.find(co)
|
|
|
|
def test_kdtree_without_balance_and_insert(self):
|
|
co = (0,) * 3
|
|
|
|
k = kdtree.KDTree(1)
|
|
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=True)
|
|
# 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()
|