import numpy as np
import weakref
from functools import reduce
_epsilon = 1e-6
[docs]def get_bounds_fit_matrix(xmin, xmax, ymin, ymax, zmin, zmax):
"""Return a 4x4 transform matrix.
Map the default bounding box [-0.5, 0.5, -0.5, 0.5, -0.5, 0.5] into
a custom bounding box [xmin, xmax, ymin, ymax, zmin, zmax].
Parameters
----------
xmin : float
Lower x bound.
xmax : float
Upper x bound.
ymin : float
Lower y bound.
ymax : float
Upper y bound.
zmin : float
Lower z bound.
zmax : float
Upper z bound.
Returns
-------
ndarray
Transform matrix.
Raises
------
TypeError
Expected float.
"""
for name, value in locals().copy().items():
try:
float(value)
except (TypeError, ValueError):
raise TypeError('%s: expected float, %s given' %
(name, type(value).__name__))
matrix = np.diagflat(
np.array((xmax - xmin, ymax - ymin, zmax - zmin, 1.0), np.float32, order='C'))
matrix[0:3, 3] = ((xmax + xmin) / 2.0, (ymax + ymin) /
2.0, (zmax + zmin) / 2.0)
return matrix
class Transform(object):
"""
Abstraction of a 4x4 model transformation matrix with hierarchy support.
"""
def __init__(self, bounds=None, translation=None, rotation=None, scaling=None, custom_matrix=None, parent=None):
"""
Transform constructor.
:param bounds: List[float] (xmin, xmax, ymin, ymax, zmin, zmax)
:param translation: List[float] (dx, dy, dz) - translation vector
:param rotation: List[float] (gamma, axis_x, axis_y, axis_z) - angle in radians, then rotation axis vector
:param scaling: List[float] (s_x, s_y, s_z) - 3 scaling coefficients
:param custom_matrix: np.array - 4x4 arbitrary transform matrix
:param parent: `Transform` optional parent transform, which is applied before this transform
"""
self.bounds = bounds
assert translation is None or len(translation) == 3
self.translation = translation
assert rotation is None or len(rotation) == 4
self.rotation = rotation
assert scaling is None or len(scaling) == 3
self.scaling = scaling
self.parent = parent
if parent is not None:
parent._add_child(self)
self.drawables = []
self.children = []
self.parent_matrix = parent.model_matrix if parent else np.identity(
4, dtype=np.float32)
self.custom_matrix = custom_matrix if custom_matrix is not None else np.identity(
4, dtype=np.float32)
self.model_matrix = np.identity(4, dtype=np.float32)
self._recompute_matrix()
def __setattr__(self, key, value):
"""Set attributes with conversion to ndarray where needed."""
is_set = hasattr(self, key) # == False in constructor
# parameter canonicalization and some validation via reshaping
if value is None:
# TODO: maybe forbid for some fields
pass
elif key == 'translation':
value = np.array(value, dtype=np.float32).reshape(3, 1)
elif key == 'rotation':
value = np.array(value, dtype=np.float32).reshape(4)
value[0] = np.fmod(value[0], 2.0 * np.pi)
if value[0] < 0.0:
value[0] += 2.0 * np.pi
value[0] = np.cos(value[0] / 2)
norm = np.linalg.norm(value[1:4])
needed_norm = np.sqrt(1 - value[0] * value[0])
if abs(norm - needed_norm) > _epsilon:
if norm < _epsilon:
raise ValueError(
'Norm of (x, y, z) part of quaternion too close to zero')
value[1:4] = value[1:4] / norm * needed_norm
# assert abs(np.linalg.norm(value) - 1.0) < _epsilon
elif key == 'scaling':
value = np.array(value, dtype=np.float32).reshape(3)
elif key in ['parent_matrix', 'custom_matrix', 'model_matrix']:
value = np.array(value, dtype=np.float32).reshape((4, 4))
super(Transform, self).__setattr__(key, value)
if is_set and key != 'model_matrix':
self._recompute_matrix()
self._notify_dependants()
def __repr__(self):
return 'Transform(bounds={!r}, translation={!r}, rotation={!r}, scaling={!r})'.format(
self.bounds, self.translation, self.rotation, self.scaling
)
def _recompute_matrix(self):
# this method shouldn't modify any fields except self.model_matrix
if self.bounds is None or len(self.bounds) == 0:
fit_matrix = np.identity(4)
else:
if len(self.bounds) == 6:
xmin, xmax, ymin, ymax, zmin, zmax = self.bounds
elif len(self.bounds) == 4:
xmin, xmax, ymin, ymax = self.bounds
zmin, zmax = -0.5, 0.5
elif len(self.bounds) == 2:
# 1-D bounds for a 2D strip - why not?
xmin, xmax = self.bounds
ymin, ymax, zmin, zmax = -0.5, 0.5, -0.5, 0.5
else:
raise ValueError('Wrong size of bounds array ({}), should be 4 for 2D or 6 for 3D bounds.'.format(
self.bounds
))
fit_matrix = get_bounds_fit_matrix(
xmin, xmax, ymin, ymax, zmin, zmax)
if self.translation is not None:
translation_matrix = np.vstack((
np.hstack((np.identity(3), np.array(
self.translation).reshape(3, 1))),
np.array([0., 0., 0., 1.]).reshape(1, 4)
))
else:
translation_matrix = np.identity(4)
if self.rotation is not None:
a, b, c, d = self.rotation
rotation_matrix = np.array([
[a * a + b * b - c * c - d * d, 2 *
(b * c - a * d), 2 * (b * d + a * c), 0.],
[2 * (b * c + a * d), a * a - b * b + c *
c - d * d, 2 * (c * d - a * b), 0.],
[2 * (b * d - a * c), 2 * (c * d + a * b),
a * a - b * b - c * c + d * d, 0.],
[0., 0., 0., 1.]
])
else:
rotation_matrix = np.identity(4)
if self.scaling is not None:
scaling_matrix = np.diag(np.append(self.scaling, 1.0))
else:
scaling_matrix = np.identity(4)
self.model_matrix = reduce(np.dot, [
translation_matrix, rotation_matrix, scaling_matrix, fit_matrix, self.custom_matrix, self.parent_matrix
])
def _add_child(self, transform):
self.children.append(weakref.ref(transform))
def add_drawable(self, drawable):
"""Register a Drawable to have its model_matrix overwritten after changes to the transform or its parent."""
self.drawables.append(weakref.ref(drawable))
def parent_updated(self):
"""Read updated parent transform matrix and update own model_matrix.
This method should be normally only called by parent Transform to notify its children.
"""
if self.parent is not None:
self.parent_matrix = self.parent.model_matrix
self._recompute_matrix()
self._notify_dependants()
def _notify_dependants(self):
extant_children = []
for child_ref in self.children:
child = child_ref()
if child is not None:
child.parent_updated()
extant_children.append(child_ref)
self.children[:] = extant_children
extant_drawables = []
for drawable_ref in self.drawables:
drawable = drawable_ref()
if drawable is not None:
drawable.model_matrix = self.model_matrix
extant_drawables.append(drawable_ref)
self.drawables[:] = extant_drawables