Browse Source

python: raise exceptions for Math API usage errors.

Originally those were assertions that were kept even in release builds,
which meant that calling math.angle() on non-normalized vectors aborted
the whole Python interpreted. Not great. But then the assertions were
made debug-only, which means invalid usage from Python (where the
bindings are usually only built as Release) now silently gives back a
wrong result, which is perhaps even worse.

Because the Python overhead is already massive due to all string lookup
and such, doing one more check in the implementations isn't really going
to slow down anything. Thus I'm mirroring all (debug-only) Magnum
assertions on the Python side, turning them into exceptions. With proper
messages as well, because those are extremely useful.
next
Vladimír Vondruš 2 years ago
parent
commit
7a35f405b4
  1. 140
      doc/python/magnum.math.rst
  2. 90
      src/python/magnum/math.cpp
  3. 144
      src/python/magnum/math.matrix.h
  4. 13
      src/python/magnum/math.vectorfloat.cpp
  5. 142
      src/python/magnum/test/test_math.py

140
doc/python/magnum.math.rst

@ -230,6 +230,146 @@
:py:`mat.translation` is a read-write property accessing the fourth column
of the matrix. Similarly for the :ref:`Matrix3` class.
.. py:function:: magnum.Matrix2x2.inverted_orthogonal
:raise ValueError: If the matrix is not orthogonal
.. py:function:: magnum.Matrix2x2d.inverted_orthogonal
:raise ValueError: If the matrix is not orthogonal
.. py:function:: magnum.Matrix3x3.inverted_orthogonal
:raise ValueError: If the matrix is not orthogonal
.. py:function:: magnum.Matrix3x3d.inverted_orthogonal
:raise ValueError: If the matrix is not orthogonal
.. py:function:: magnum.Matrix4x4.inverted_orthogonal
:raise ValueError: If the matrix is not orthogonal
.. py:function:: magnum.Matrix4x4d.inverted_orthogonal
:raise ValueError: If the matrix is not orthogonal
.. py:function:: magnum.Matrix3.inverted_orthogonal
:raise ValueError: If the matrix is not orthogonal
.. py:function:: magnum.Matrix3d.inverted_orthogonal
:raise ValueError: If the matrix is not orthogonal
.. py:function:: magnum.Matrix4.inverted_orthogonal
:raise ValueError: If the matrix is not orthogonal
.. py:function:: magnum.Matrix4d.inverted_orthogonal
:raise ValueError: If the matrix is not orthogonal
.. py:function:: magnum.Matrix3.reflection
:raise ValueError: If :p:`normal` is not normalized
.. py:function:: magnum.Matrix3d.reflection
:raise ValueError: If :p:`normal` is not normalized
.. py:function:: magnum.Matrix4.reflection
:raise ValueError: If :p:`normal` is not normalized
.. py:function:: magnum.Matrix4d.reflection
:raise ValueError: If :p:`normal` is not normalized
.. py:function:: magnum.Matrix3.rotation(self)
:raise ValueError: If the normalized rotation part is not orthogonal
.. py:function:: magnum.Matrix3d.rotation(self)
:raise ValueError: If the normalized rotation part is not orthogonal
.. py:function:: magnum.Matrix4.rotation(self)
:raise ValueError: If the normalized rotation part is not orthogonal
.. py:function:: magnum.Matrix4d.rotation(self)
:raise ValueError: If the normalized rotation part is not orthogonal
.. py:function:: magnum.Matrix3.rotation_normalized
:raise ValueError: If the rotation part is not orthogonal
.. py:function:: magnum.Matrix3d.rotation_normalized
:raise ValueError: If the rotation part is not orthogonal
.. py:function:: magnum.Matrix4.rotation_normalized
:raise ValueError: If the rotation part is not orthogonal
.. py:function:: magnum.Matrix4d.rotation_normalized
:raise ValueError: If the rotation part is not orthogonal
.. py:function:: magnum.Matrix3.uniform_scaling_squared
:raise ValueError: If the matrix doesn't have uniform scaling
.. py:function:: magnum.Matrix3d.uniform_scaling_squared
:raise ValueError: If the matrix doesn't have uniform scaling
.. py:function:: magnum.Matrix4.uniform_scaling_squared
:raise ValueError: If the matrix doesn't have uniform scaling
.. py:function:: magnum.Matrix4d.uniform_scaling_squared
:raise ValueError: If the matrix doesn't have uniform scaling
.. py:function:: magnum.Matrix3.uniform_scaling
:raise ValueError: If the matrix doesn't have uniform scaling
.. py:function:: magnum.Matrix3d.uniform_scaling
:raise ValueError: If the matrix doesn't have uniform scaling
.. py:function:: magnum.Matrix4.uniform_scaling
:raise ValueError: If the matrix doesn't have uniform scaling
.. py:function:: magnum.Matrix4d.uniform_scaling
:raise ValueError: If the matrix doesn't have uniform scaling
.. py:function:: magnum.Matrix3.inverted_rigid
:raise ValueError: If the matrix doesn't represent a rigid transformation
.. py:function:: magnum.Matrix3d.inverted_rigid
:raise ValueError: If the matrix doesn't represent a rigid transformation
.. py:function:: magnum.Matrix4.inverted_rigid
:raise ValueError: If the matrix doesn't represent a rigid transformation
.. py:function:: magnum.Matrix4d.inverted_rigid
:raise ValueError: If the matrix doesn't represent a rigid transformation
.. py:function:: magnum.math.half_angle(normalized_a: magnum.Quaternion, normalized_b: magnum.Quaternion)
:raise ValueError: If either of the quaternions is not normalized
.. py:function:: magnum.math.half_angle(normalized_a: magnum.Quaterniond, normalized_b: magnum.Quaterniond)
:raise ValueError: If either of the quaternions is not normalized
.. py:function:: magnum.math.lerp(normalized_a: magnum.Quaternion, normalized_b: magnum.Quaternion, t: float)
:raise ValueError: If either of the quaternions is not normalized
.. py:function:: magnum.math.lerp(normalized_a: magnum.Quaterniond, normalized_b: magnum.Quaterniond, t: float)
:raise ValueError: If either of the quaternions is not normalized
.. py:function:: magnum.math.lerp_shortest_path(normalized_a: magnum.Quaternion, normalized_b: magnum.Quaternion, t: float)
:raise ValueError: If either of the quaternions is not normalized
.. py:function:: magnum.math.lerp_shortest_path(normalized_a: magnum.Quaterniond, normalized_b: magnum.Quaterniond, t: float)
:raise ValueError: If either of the quaternions is not normalized
.. py:function:: magnum.math.slerp(normalized_a: magnum.Quaternion, normalized_b: magnum.Quaternion, t: float)
:raise ValueError: If either of the quaternions is not normalized
.. py:function:: magnum.math.slerp(normalized_a: magnum.Quaterniond, normalized_b: magnum.Quaterniond, t: float)
:raise ValueError: If either of the quaternions is not normalized
.. py:function:: magnum.math.slerp_shortest_path(normalized_a: magnum.Quaternion, normalized_b: magnum.Quaternion, t: float)
:raise ValueError: If either of the quaternions is not normalized
.. py:function:: magnum.math.slerp_shortest_path(normalized_a: magnum.Quaterniond, normalized_b: magnum.Quaterniond, t: float)
:raise ValueError: If either of the quaternions is not normalized
.. py:function:: magnum.Quaternion.rotation(angle: magnum.Rad, normalized_axis: magnum.Vector3)
:raise ValueError: If :p:`normalized_axis` is not normalized
.. py:function:: magnum.Quaterniond.rotation(angle: magnum.Rad, normalized_axis: magnum.Vector3d)
:raise ValueError: If :p:`normalized_axis` is not normalized
.. py:function:: magnum.Quaternion.from_matrix
:raise ValueError: If :p:`matrix` is not a rotation
.. py:function:: magnum.Quaterniond.from_matrix
:raise ValueError: If :p:`matrix` is not a rotation
.. py:function:: magnum.Quaternion.angle
:raise ValueError: If the quaternion is not normalized
.. py:function:: magnum.Quaterniond.angle
:raise ValueError: If the quaternion is not normalized
.. py:function:: magnum.Quaternion.axis
:raise ValueError: If the quaternion is not normalized
.. py:function:: magnum.Quaterniond.axis
:raise ValueError: If the quaternion is not normalized
.. py:function:: magnum.Quaternion.inverted_normalized
:raise ValueError: If the quaternion is not normalized
.. py:function:: magnum.Quaterniond.inverted_normalized
:raise ValueError: If the quaternion is not normalized
.. py:function:: magnum.Quaternion.transform_vector_normalized
:raise ValueError: If the quaternion is not normalized
.. py:function:: magnum.Quaterniond.transform_vector_normalized
:raise ValueError: If the quaternion is not normalized
.. py:function:: magnum.math.angle(normalized_a: magnum.Vector2, normalized_b: magnum.Vector2)
:raise ValueError: If either of the vectors is not normalized
.. py:function:: magnum.math.angle(normalized_a: magnum.Vector2d, normalized_b: magnum.Vector2d)
:raise ValueError: If either of the vectors is not normalized
.. py:function:: magnum.math.angle(normalized_a: magnum.Vector3, normalized_b: magnum.Vector3)
:raise ValueError: If either of the vectors is not normalized
.. py:function:: magnum.math.angle(normalized_a: magnum.Vector3d, normalized_b: magnum.Vector3d)
:raise ValueError: If either of the vectors is not normalized
.. py:function:: magnum.math.angle(normalized_a: magnum.Vector4, normalized_b: magnum.Vector4)
:raise ValueError: If either of the vectors is not normalized
.. py:function:: magnum.math.angle(normalized_a: magnum.Vector4d, normalized_b: magnum.Vector4d)
:raise ValueError: If either of the vectors is not normalized
.. py:function:: magnum.Vector2.projected_onto_normalized
:raise ValueError: If :p:`line` is not normalized
.. py:function:: magnum.Vector2d.projected_onto_normalized
:raise ValueError: If :p:`line` is not normalized
.. py:function:: magnum.Vector3.projected_onto_normalized
:raise ValueError: If :p:`line` is not normalized
.. py:function:: magnum.Vector3d.projected_onto_normalized
:raise ValueError: If :p:`line` is not normalized
.. py:function:: magnum.Vector4.projected_onto_normalized
:raise ValueError: If :p:`line` is not normalized
.. py:function:: magnum.Vector4d.projected_onto_normalized
:raise ValueError: If :p:`line` is not normalized
.. For pickling, because py::pickle() doesn't seem to have a way to set the
__setstate__ / __getstate__ docs directly FFS

90
src/python/magnum/math.cpp

@ -302,27 +302,60 @@ template<class T> void quaternion(py::module_& m, py::class_<T>& c) {
.def("dot", static_cast<typename T::Type(*)(const T&, const T&)>(&Math::dot),
"Dot product between two quaternions")
.def("half_angle", [](const T& normalizedA, const T& normalizedB) {
if(!normalizedA.isNormalized() || !normalizedB.isNormalized()) {
PyErr_Format(PyExc_ValueError, "quaternions %S and %S are not normalized", py::cast(normalizedA).ptr(), py::cast(normalizedB).ptr());
throw py::error_already_set{};
}
/** @todo switch back to angle() once it's reintroduced with the
correct output again */
return Radd(Math::halfAngle(normalizedA, normalizedB));
}, "Angle between normalized quaternions", py::arg("normalized_a"), py::arg("normalized_b"))
.def("lerp", static_cast<T(*)(const T&, const T&, typename T::Type)>(&Math::lerp),
"Linear interpolation of two quaternions", py::arg("normalized_a"), py::arg("normalized_b"), py::arg("t"))
.def("lerp_shortest_path", static_cast<T(*)(const T&, const T&, typename T::Type)>(&Math::lerpShortestPath),
"Linear shortest-path interpolation of two quaternions", py::arg("normalized_a"), py::arg("normalized_b"), py::arg("t"))
.def("slerp", static_cast<T(*)(const T&, const T&, typename T::Type)>(&Math::slerp),
"Spherical linear interpolation of two quaternions", py::arg("normalized_a"), py::arg("normalized_b"), py::arg("t"))
.def("slerp_shortest_path", static_cast<T(*)(const T&, const T&, typename T::Type)>(&Math::slerpShortestPath),
"Spherical linear shortest-path interpolation of two quaternions", py::arg("normalized_a"), py::arg("normalized_b"), py::arg("t"))
;
.def("lerp", [](const T& normalizedA, const T& normalizedB, typename T::Type t) {
if(!normalizedA.isNormalized() || !normalizedB.isNormalized()) {
PyErr_Format(PyExc_ValueError, "quaternions %S and %S are not normalized", py::cast(normalizedA).ptr(), py::cast(normalizedB).ptr());
throw py::error_already_set{};
}
return Math::lerp(normalizedA, normalizedB, t);
}, "Linear interpolation of two quaternions", py::arg("normalized_a"), py::arg("normalized_b"), py::arg("t"))
.def("lerp_shortest_path", [](const T& normalizedA, const T& normalizedB, typename T::Type t) {
if(!normalizedA.isNormalized() || !normalizedB.isNormalized()) {
PyErr_Format(PyExc_ValueError, "quaternions %S and %S are not normalized", py::cast(normalizedA).ptr(), py::cast(normalizedB).ptr());
throw py::error_already_set{};
}
return Math::lerpShortestPath(normalizedA, normalizedB, t);
}, "Linear shortest-path interpolation of two quaternions", py::arg("normalized_a"), py::arg("normalized_b"), py::arg("t"))
.def("slerp", [](const T& normalizedA, const T& normalizedB, typename T::Type t) {
if(!normalizedA.isNormalized() || !normalizedB.isNormalized()) {
PyErr_Format(PyExc_ValueError, "quaternions %S and %S are not normalized", py::cast(normalizedA).ptr(), py::cast(normalizedB).ptr());
throw py::error_already_set{};
}
return Math::slerp(normalizedA, normalizedB, t);
}, "Spherical linear interpolation of two quaternions", py::arg("normalized_a"), py::arg("normalized_b"), py::arg("t"))
.def("slerp_shortest_path", [](const T& normalizedA, const T& normalizedB, typename T::Type t) {
if(!normalizedA.isNormalized() || !normalizedB.isNormalized()) {
PyErr_Format(PyExc_ValueError, "quaternions %S and %S are not normalized", py::cast(normalizedA).ptr(), py::cast(normalizedB).ptr());
throw py::error_already_set{};
}
return Math::slerpShortestPath(normalizedA, normalizedB, t);
}, "Spherical linear shortest-path interpolation of two quaternions", py::arg("normalized_a"), py::arg("normalized_b"), py::arg("t"));
c
/* Constructors */
.def_static("rotation", [](Radd angle, const Math::Vector3<typename T::Type>& axis) {
return T::rotation(Math::Rad<typename T::Type>(angle), axis);
.def_static("rotation", [](Radd angle, const Math::Vector3<typename T::Type>& normalizedAxis) {
if(!normalizedAxis.isNormalized()) {
PyErr_Format(PyExc_ValueError, "axis %S is not normalized", py::cast(normalizedAxis).ptr());
throw py::error_already_set{};
}
return T::rotation(Math::Rad<typename T::Type>(angle), normalizedAxis);
}, "Rotation quaternion", py::arg("angle"), py::arg("normalized_axis"))
.def_static("from_matrix", &T::fromMatrix,
"Create a quaternion from rotation matrix", py::arg("matrix"))
.def_static("from_matrix", [](const Math::Matrix3x3<typename T::Type>& matrix) {
/* Same as the check in fromMatrix() */
if(std::abs(matrix.determinant() - typename T::Type(1)) >= typename T::Type(3)*Math::TypeTraits<typename T::Type>::epsilon()) {
PyErr_Format(PyExc_ValueError, "the matrix is not a rotation:\n%S", py::cast(matrix).ptr());
throw py::error_already_set{};
}
return T::fromMatrix(matrix);
}, "Create a quaternion from rotation matrix", py::arg("matrix"))
.def_static("zero_init", []() {
return T{Math::ZeroInit};
}, "Construct a zero-initialized quaternion")
@ -385,10 +418,19 @@ template<class T> void quaternion(py::module_& m, py::class_<T>& c) {
.def("is_normalized", &T::isNormalized,
"Whether the quaternion is normalized")
.def("angle", [](const T& self) {
if(!self.isNormalized()) {
PyErr_Format(PyExc_ValueError, "%S is not normalized", py::cast(self).ptr());
throw py::error_already_set{};
}
return Radd(self.angle());
}, "Rotation angle of a unit quaternion")
.def("axis", &T::axis,
"Rotation axis of a unit quaternion")
.def("axis", [](const T& self) {
if(!self.isNormalized()) {
PyErr_Format(PyExc_ValueError, "%S is not normalized", py::cast(self).ptr());
throw py::error_already_set{};
}
return self.axis();
}, "Rotation axis of a unit quaternion")
.def("to_matrix", &T::toMatrix,
"Convert to a rotation matrix")
.def("dot", &T::dot,
@ -401,12 +443,22 @@ template<class T> void quaternion(py::module_& m, py::class_<T>& c) {
"Conjugated quaternion")
.def("inverted", &T::inverted,
"Inverted quaternion")
.def("inverted_normalized", &T::invertedNormalized,
"Inverted normalized quaternion")
.def("inverted_normalized", [](const T& self) {
if(!self.isNormalized()) {
PyErr_Format(PyExc_ValueError, "%S is not normalized", py::cast(self).ptr());
throw py::error_already_set{};
}
return self.invertedNormalized();
}, "Inverted normalized quaternion")
.def("transform_vector", &T::transformVector,
"Rotate a vector with a quaternion", py::arg("vector"))
.def("transform_vector_normalized", &T::transformVectorNormalized,
"Rotate a vector with a normalized quaternion", py::arg("vector"))
.def("transform_vector_normalized", [](const T& self, const Math::Vector3<typename T::Type>& vector) {
if(!self.isNormalized()) {
PyErr_Format(PyExc_ValueError, "%S is not normalized", py::cast(self).ptr());
throw py::error_already_set{};
}
return self.transformVectorNormalized(vector);
}, "Rotate a vector with a normalized quaternion", py::arg("vector"))
/* Properties */
.def_property("vector",

144
src/python/magnum/math.matrix.h

@ -266,7 +266,13 @@ template<class T, class ...Args> void everyMatrix(py::class_<T, Args...>& c) {
return self.adjugate();
}, "Adjugate matrix")
.def("inverted", &T::inverted, "Inverted matrix")
.def("inverted_orthogonal", &T::invertedOrthogonal, "Inverted orthogonal matrix")
.def("inverted_orthogonal", [](const T& self) {
if(!self.isOrthogonal()) {
PyErr_Format(PyExc_ValueError, "the matrix is not orthogonal:\n%S", py::cast(self).ptr());
throw py::error_already_set{};
}
return self.invertedOrthogonal();
}, "Inverted orthogonal matrix")
.def("__matmul__", [](const T& self, const T& other) -> T {
return self*other;
}, "Multiply a matrix")
@ -570,8 +576,13 @@ template<class T> void matrices(
matrix3
/* Constructors. The translation() / scaling() / rotation() are handled
below as they conflict with member functions. */
.def_static("reflection", &Math::Matrix3<T>::reflection,
"2D reflection matrix", py::arg("normal"))
.def_static("reflection", [](const Math::Vector2<T>& normal) {
if(!normal.isNormalized()) {
PyErr_Format(PyExc_ValueError, "normal %S is not normalized", py::cast(normal).ptr());
throw py::error_already_set{};
}
return Math::Matrix3<T>::reflection(normal);
}, "2D reflection matrix", py::arg("normal"))
.def_static("shearing_x", &Math::Matrix3<T>::shearingX,
"2D shearing matrix along the X axis", py::arg("amount"))
.def_static("shearing_y", &Math::Matrix3<T>::shearingY,
@ -605,16 +616,43 @@ template<class T> void matrices(
"2D rotation and scaling part of the matrix")
.def("rotation_shear", &Math::Matrix3<T>::rotationShear,
"2D rotation and shear part of the matrix")
.def("rotation_normalized", &Math::Matrix3<T>::rotationNormalized,
"2D rotation part of the matrix assuming there is no scaling")
.def("rotation_normalized", [](const Math::Matrix3<T>& self) {
/* Same as implementation of rotationNormalized() */
const Math::Matrix2x2<T> rotationScaling = self.rotationScaling();
if(!rotationScaling.isOrthogonal()) {
PyErr_Format(PyExc_ValueError, "the rotation part is not orthogonal:\n%S", py::cast(rotationScaling).ptr());
throw py::error_already_set{};
}
return rotationScaling;
}, "2D rotation part of the matrix assuming there is no scaling")
.def("scaling_squared", &Math::Matrix3<T>::scalingSquared,
"Non-uniform scaling part of the matrix, squared")
.def("uniform_scaling_squared", &Math::Matrix3<T>::uniformScalingSquared,
"Uniform scaling part of the matrix, squared")
.def("uniform_scaling", &Math::Matrix3<T>::uniformScaling,
"Uniform scaling part of the matrix")
.def("inverted_rigid", &Math::Matrix3<T>::invertedRigid,
"Inverted rigid transformation matrix")
.def("uniform_scaling_squared", [](const Math::Matrix3<T>& self) {
/* Same as implementation of uniformScalingSquared() */
const T scalingSquared = self[0].xy().dot();
if(!Math::TypeTraits<T>::equals(self[1].xy().dot(), scalingSquared)) {
PyErr_Format(PyExc_ValueError, "the matrix doesn't have uniform scaling:\n%S", py::cast(self.rotationScaling()).ptr());
throw py::error_already_set{};
}
return scalingSquared;
}, "Uniform scaling part of the matrix, squared")
.def("uniform_scaling", [](const Math::Matrix3<T>& self) {
/* Same as implementation of uniformScalingSquared(), which
uniformScaling() delegates to */
const T scalingSquared = self[0].xy().dot();
if(!Math::TypeTraits<T>::equals(self[1].xy().dot(), scalingSquared)) {
PyErr_Format(PyExc_ValueError, "the matrix doesn't have uniform scaling:\n%S", py::cast(self.rotationScaling()).ptr());
throw py::error_already_set{};
};
return std::sqrt(scalingSquared);
}, "Uniform scaling part of the matrix")
.def("inverted_rigid", [](const Math::Matrix3<T>& self) {
if(!self.isRigidTransformation()) {
PyErr_Format(PyExc_ValueError, "the matrix doesn't represent a rigid transformation:\n%S", py::cast(self).ptr());
throw py::error_already_set{};
}
return self.invertedRigid();
}, "Inverted rigid transformation matrix")
.def("transform_vector", &Math::Matrix3<T>::transformVector,
"Transform a 2D vector with the matrix", py::arg("vector"))
.def("transform_point", &Math::Matrix3<T>::transformPoint,
@ -719,7 +757,15 @@ Overloaded function.
.def_static("_srotation", [](Radd angle) {
return Math::Matrix3<T>::rotation(Math::Rad<T>(angle));
})
.def("_irotation", static_cast<Math::Matrix2x2<T>(Math::Matrix3<T>::*)() const>(&Math::Matrix3<T>::rotation))
.def("_irotation", [](const Math::Matrix3<T>& self) {
/* Same as implementation of rotation() */
const Math::Matrix2x2<T> rotationShear = self.rotationShear();
if(!rotationShear.isOrthogonal()) {
PyErr_Format(PyExc_ValueError, "the normalized rotation part is not orthogonal:\n%S", py::cast(rotationShear).ptr());
throw py::error_already_set{};
}
return rotationShear;
})
.def("rotation", [matrix3](const py::args& args, const py::kwargs& kwargs) {
if(py::len(args) && py::isinstance<Math::Matrix3<T>>(args[0])) {
return matrix3.attr("_irotation")(*args, **kwargs);
@ -759,8 +805,13 @@ Overloaded function.
.def_static("rotation_z", [](Radd angle) {
return Math::Matrix4<T>::rotationZ(Math::Rad<T>(angle));
}, "3D rotation matrix around the Z axis", py::arg("angle"))
.def_static("reflection", &Math::Matrix4<T>::reflection,
"3D reflection matrix", py::arg("normal"))
.def_static("reflection", [](const Math::Vector3<T>& normal) {
if(!normal.isNormalized()) {
PyErr_Format(PyExc_ValueError, "normal %S is not normalized", py::cast(normal).ptr());
throw py::error_already_set{};
}
return Math::Matrix4<T>::reflection(normal);
}, "3D reflection matrix", py::arg("normal"))
.def_static("shearing_xy", &Math::Matrix4<T>::shearingXY,
"3D shearing matrix along the XY plane", py::arg("amount_x"), py::arg("amount_y"))
.def_static("shearing_xz", &Math::Matrix4<T>::shearingXZ,
@ -809,18 +860,49 @@ Overloaded function.
"3D rotation and scaling part of the matrix")
.def("rotation_shear", &Math::Matrix4<T>::rotationShear,
"3D rotation and shear part of the matrix")
.def("rotation_normalized", &Math::Matrix4<T>::rotationNormalized,
"3D rotation part of the matrix assuming there is no scaling")
.def("rotation_normalized", [](const Math::Matrix4<T>& self) {
/* Same as implementation of rotationNormalized() */
const Math::Matrix3x3<T> rotationScaling = self.rotationScaling();
if(!rotationScaling.isOrthogonal()) {
PyErr_Format(PyExc_ValueError, "the rotation part is not orthogonal:\n%S", py::cast(rotationScaling).ptr());
throw py::error_already_set{};
}
return rotationScaling;
}, "3D rotation part of the matrix assuming there is no scaling")
.def("scaling_squared", &Math::Matrix4<T>::scalingSquared,
"Non-uniform scaling part of the matrix, squared")
.def("uniform_scaling_squared", &Math::Matrix4<T>::uniformScalingSquared,
"Uniform scaling part of the matrix, squared")
.def("uniform_scaling", &Math::Matrix4<T>::uniformScaling,
"Uniform scaling part of the matrix")
.def("uniform_scaling_squared", [](const Math::Matrix4<T>& self) {
/* Same as implementation of uniformScalingSquared() */
const T scalingSquared = self[0].xyz().dot();
if(!Math::TypeTraits<T>::equals(self[1].xyz().dot(), scalingSquared) ||
!Math::TypeTraits<T>::equals(self[2].xyz().dot(), scalingSquared)
) {
PyErr_Format(PyExc_ValueError, "the matrix doesn't have uniform scaling:\n%S", py::cast(self.rotationScaling()).ptr());
throw py::error_already_set{};
}
return scalingSquared;
}, "Uniform scaling part of the matrix, squared")
.def("uniform_scaling", [](const Math::Matrix4<T>& self) {
/* Same as implementation of uniformScalingSquared(), which
uniformScaling() delegates to */
const T scalingSquared = self[0].xyz().dot();
if(!Math::TypeTraits<T>::equals(self[1].xyz().dot(), scalingSquared) ||
!Math::TypeTraits<T>::equals(self[2].xyz().dot(), scalingSquared)
) {
PyErr_Format(PyExc_ValueError, "the matrix doesn't have uniform scaling:\n%S", py::cast(self.rotationScaling()).ptr());
throw py::error_already_set{};
}
return std::sqrt(scalingSquared);
}, "Uniform scaling part of the matrix")
.def("normal_matrix", &Math::Matrix4<T>::normalMatrix,
"Normal matrix")
.def("inverted_rigid", &Math::Matrix4<T>::invertedRigid,
"Inverted rigid transformation matrix")
.def("inverted_rigid", [](const Math::Matrix4<T>& self) {
if(!self.isRigidTransformation()) {
PyErr_Format(PyExc_ValueError, "the matrix doesn't represent a rigid transformation:\n%S", py::cast(self).ptr());
throw py::error_already_set{};
}
return self.invertedRigid();
}, "Inverted rigid transformation matrix")
.def("transform_vector", &Math::Matrix4<T>::transformVector,
"Transform a 3D vector with the matrix", py::arg("vector"))
.def("transform_point", &Math::Matrix4<T>::transformPoint,
@ -928,10 +1010,22 @@ Overloaded function.
/* Static/member rotation(). Pybind doesn't support that natively,
so we create a rotation(*args, **kwargs) and dispatch ourselves. */
.def_static("_srotation", [](Radd angle, const Math::Vector3<T>& axis) {
return Math::Matrix4<T>::rotation(Math::Rad<T>(angle), axis);
.def_static("_srotation", [](Radd angle, const Math::Vector3<T>& normalizedAxis) {
if(!normalizedAxis.isNormalized()) {
PyErr_Format(PyExc_ValueError, "axis %S is not normalized", py::cast(normalizedAxis).ptr());
throw py::error_already_set{};
}
return Math::Matrix4<T>::rotation(Math::Rad<T>(angle), normalizedAxis);
})
.def("_irotation", [](const Math::Matrix4<T>& self) {
/* Same as implementation of rotation() */
const Math::Matrix3x3<T> rotationShear = self.rotationShear();
if(!rotationShear.isOrthogonal()) {
PyErr_Format(PyExc_ValueError, "the normalized rotation part is not orthogonal:\n%S", py::cast(rotationShear).ptr());
throw py::error_already_set{};
}
return rotationShear;
})
.def("_irotation", static_cast<Math::Matrix3x3<T>(Math::Matrix4<T>::*)() const>(&Math::Matrix4<T>::rotation))
.def("rotation", [matrix4](const py::args& args, const py::kwargs& kwargs) {
if(py::len(args) && py::isinstance<Math::Matrix4<T>>(args[0])) {
return matrix4.attr("_irotation")(*args, **kwargs);

13
src/python/magnum/math.vectorfloat.cpp

@ -57,8 +57,13 @@ template<class T> void vectorFloat(py::module_& m, py::class_<T>& c) {
return T{Math::fma(a, b, c)};
}, "Fused multiply-add")
.def("angle", [](const T& a, const T& b) { return Radd(Math::angle(a, b)); },
"Angle between normalized vectors", py::arg("normalized_a"), py::arg("normalized_b"));
.def("angle", [](const T& normalizedA, const T& normalizedB) {
if(!normalizedA.isNormalized() || !normalizedB.isNormalized()) {
PyErr_Format(PyExc_ValueError, "vectors %S and %S are not normalized", py::cast(normalizedA).ptr(), py::cast(normalizedB).ptr());
throw py::error_already_set{};
}
return Radd(Math::angle(normalizedA, normalizedB));
}, "Angle between normalized vectors", py::arg("normalized_a"), py::arg("normalized_b"));
c
.def("is_normalized", &T::isNormalized, "Whether the vector is normalized")
@ -74,6 +79,10 @@ template<class T> void vectorFloat(py::module_& m, py::class_<T>& c) {
return self.projected(line);
}, "Vector projected onto a line", py::arg("line"))
.def("projected_onto_normalized", [](const T& self, const T& line) {
if(!line.isNormalized()) {
PyErr_Format(PyExc_ValueError, "line %S is not normalized", py::cast(line).ptr());
throw py::error_already_set{};
}
return self.projectedOntoNormalized(line);
}, "Vector projected onto a normalized line", py::arg("line"));
}

142
src/python/magnum/test/test_math.py

@ -402,15 +402,27 @@ class Vector(unittest.TestCase):
self.assertEqual(a.minmax(), (-13.5, 3.5))
def test_ops(self):
a = Vector2(0.707107, 0.707107)
b = Vector2(1.0, 0.0)
self.assertEqual(math.dot(Vector2(0.5, 3.0), Vector2(2.0, 0.5)), 2.5)
self.assertEqual(Deg(math.angle(
Vector2(0.5, 3.0).normalized(),
Vector2(2.0, 0.5).normalized())), Deg(66.5014333443446))
self.assertEqual(Deg(math.angle(a, b)), Deg(44.9999807616716))
self.assertEqual(Vector3(1.0, 2.0, 0.3).projected(Vector3.y_axis()),
Vector3.y_axis(2.0))
self.assertEqual(Vector3(1.0, 2.0, 0.3).projected_onto_normalized(Vector3.y_axis()),
Vector3.y_axis(2.0))
def test_ops_invalid(self):
a = Vector2(0.707107, 0.707107)
b = Vector2(1.0, 0.0)
with self.assertRaisesRegex(ValueError, "vectors Vector\\(1.41421, 1.41421\\) and Vector\\(1, 0\\) are not normalized"):
math.angle(a*2.0, b)
with self.assertRaisesRegex(ValueError, "vectors Vector\\(0.707107, 0.707107\\) and Vector\\(2, 0\\) are not normalized"):
math.angle(a, b*2.0)
with self.assertRaisesRegex(ValueError, "line Vector\\(2, 0.5\\) is not normalized"):
Vector2(0.5, 3.0).projected_onto_normalized(Vector2(2.0, 0.5))
def test_ops_number_on_the_left(self):
self.assertEqual(2.0*Vector2(1.0, -3.0), Vector2(2.0, -6.0))
self.assertEqual(6.0/Vector2(2.0, -3.0), Vector2(3.0, -2.0))
@ -938,6 +950,13 @@ class Matrix(unittest.TestCase):
(0.0, -1.0),
(1.0, 0.0)))
def test_methods_invalid(self):
with self.assertRaisesRegex(ValueError, """the matrix is not orthogonal:
Matrix\\(4, 0,
0, 2\\)"""):
Matrix2x2((4.0, 0.0),
(0.0, 2.0)).inverted_orthogonal()
def test_repr(self):
a = Matrix2x3((1.0, 2.0, 3.0),
(4.0, 5.0, 6.0))
@ -1039,6 +1058,10 @@ class Matrix3_(unittest.TestCase):
Vector3(4.0, 5.0, 0.0),
Vector3(7.0, 8.0, 1.0)))
def test_static_methods_invalid(self):
with self.assertRaisesRegex(ValueError, "normal Vector\\(2, 0\\) is not normalized"):
Matrix3.reflection(Vector2(2.0, 0.0))
def test_pickle(self):
data = pickle.dumps(Matrix3((1.0, 2.0, 3.0),
(4.0, 5.0, 6.0),
@ -1091,6 +1114,29 @@ class Matrix3_(unittest.TestCase):
self.assertEqual(b.inverted(), Matrix3.scaling(Vector2(1/3.0)))
self.assertEqual(a.inverted_orthogonal(), Matrix3.rotation(Deg(-45.0)))
def test_methods_invalid(self):
with self.assertRaisesRegex(ValueError, """the rotation part is not orthogonal:
Matrix\\(3, 0,
0, 3\\)"""):
Matrix3.scaling(Vector2(3.0)).rotation_normalized()
with self.assertRaisesRegex(ValueError, """the matrix doesn't have uniform scaling:
Matrix\\(3, 0,
0, 2\\)"""):
Matrix3.scaling((3.0, 2.0)).uniform_scaling_squared()
with self.assertRaisesRegex(ValueError, """the matrix doesn't have uniform scaling:
Matrix\\(3, 0,
0, 2\\)"""):
Matrix3.scaling((3.0, 2.0)).uniform_scaling()
with self.assertRaisesRegex(ValueError, """the matrix doesn't represent a rigid transformation:
Matrix\\(3, 0, 0,
0, 3, 0,
0, 0, 1\\)"""):
Matrix3.scaling(Vector2(3.0)).inverted_rigid()
with self.assertRaisesRegex(ValueError, """the normalized rotation part is not orthogonal:
Matrix\\(1, 0.894427,
0, 0.447214\\)"""):
Matrix3.shearing_x(2.0).rotation()
def test_methods_return_type(self):
self.assertIsInstance(Matrix3.zero_init(), Matrix3)
self.assertIsInstance(Matrix3.from_diagonal((3.0, 1.0, 1.0)), Matrix3)
@ -1244,6 +1290,10 @@ class Matrix4_(unittest.TestCase):
Vector4(9.0, 10.0, 11.0, 0.0),
Vector4(13.0, 14.0, 15.0, 1.0)))
def test_static_methods_invalid(self):
with self.assertRaisesRegex(ValueError, "normal Vector\\(2, 0, 0\\) is not normalized"):
Matrix4.reflection(Vector3(2.0, 0.0, 0.0))
def test_pickle(self):
data = pickle.dumps(Matrix4((1.0, 2.0, 3.0, 4.0),
(5.0, 6.0, 7.0, 8.0),
@ -1316,6 +1366,44 @@ class Matrix4_(unittest.TestCase):
self.assertIsInstance(Matrix4().transposed(), Matrix4)
self.assertIsInstance(Matrix4().inverted(), Matrix4)
def test_methods_invalid(self):
with self.assertRaisesRegex(ValueError, """the rotation part is not orthogonal:
Matrix\\(3, 0, 0,
0, 3, 0,
0, 0, 3\\)"""):
Matrix4.scaling(Vector3(3.0)).rotation_normalized()
with self.assertRaisesRegex(ValueError, """the matrix doesn't have uniform scaling:
Matrix\\(3, 0, 0,
0, 2, 0,
0, 0, 3\\)"""):
Matrix4.scaling((3.0, 2.0, 3.0)).uniform_scaling_squared()
with self.assertRaisesRegex(ValueError, """the matrix doesn't have uniform scaling:
Matrix\\(3, 0, 0,
0, 3, 0,
0, 0, 2\\)"""):
Matrix4.scaling((3.0, 3.0, 2.0)).uniform_scaling_squared()
with self.assertRaisesRegex(ValueError, """the matrix doesn't have uniform scaling:
Matrix\\(3, 0, 0,
0, 2, 0,
0, 0, 3\\)"""):
Matrix4.scaling((3.0, 2.0, 3.0)).uniform_scaling()
with self.assertRaisesRegex(ValueError, """the matrix doesn't have uniform scaling:
Matrix\\(3, 0, 0,
0, 3, 0,
0, 0, 2\\)"""):
Matrix4.scaling((3.0, 3.0, 2.0)).uniform_scaling()
with self.assertRaisesRegex(ValueError, """the matrix doesn't represent a rigid transformation:
Matrix\\(3, 0, 0, 0,
0, 3, 0, 0,
0, 0, 3, 0,
0, 0, 0, 1\\)"""):
Matrix4.scaling(Vector3(3.0)).inverted_rigid()
with self.assertRaisesRegex(ValueError, """the normalized rotation part is not orthogonal:
Matrix\\(1, 0.816497, 0,
0, 0.408248, 0,
0, 0.408248, 1\\)"""):
Matrix4.shearing_xz(2.0, 1.0).rotation()
# conversion from buffer is tested in test_math_numpy, array.array is
# one-dimensional and I don't want to drag numpy here just for one test
@ -1366,6 +1454,15 @@ class Quaternion_(unittest.TestCase):
b = Quaternion.from_matrix(Matrix4.rotation_x(Deg(45.0)).rotation_scaling())
self.assertEqual(a, Quaternion((0.382683, 0.0, 0.0), 0.92388))
def test_static_methods_invalid(self):
with self.assertRaisesRegex(ValueError, "axis Vector\\(2, 0, 1\\) is not normalized"):
Quaternion.rotation(Deg(35.0), Vector3(2.0, 0.0, 1.0))
with self.assertRaisesRegex(ValueError, """the matrix is not a rotation:
Matrix\\(2, 0, 0,
0, 2, 0,
0, 0, 2\\)"""):
Quaternion.from_matrix(Matrix4.scaling(Vector3(2.0)).rotation_scaling())
def test_pickle(self):
data = pickle.dumps(Quaternion((1.0, 2.0, 3.0), 4.0))
self.assertEqual(pickle.loads(data), Quaternion((1.0, 2.0, 3.0), 4.0))
@ -1386,6 +1483,18 @@ class Quaternion_(unittest.TestCase):
self.assertEqual(a.transform_vector(Vector3.y_axis()), Vector3(0.0, 0.707107, 0.707107))
self.assertEqual(a.transform_vector_normalized(Vector3.y_axis()), Vector3(0.0, 0.707107, 0.707107))
def test_methods_invalid(self):
a = Quaternion.rotation(Deg(45.0), Vector3.x_axis())*3.0
with self.assertRaisesRegex(ValueError, "Quaternion\\({1.14805, 0, 0}, 2.77164\\) is not normalized"):
a.angle()
with self.assertRaisesRegex(ValueError, "Quaternion\\({1.14805, 0, 0}, 2.77164\\) is not normalized"):
a.axis()
with self.assertRaisesRegex(ValueError, "Quaternion\\({1.14805, 0, 0}, 2.77164\\) is not normalized"):
a.inverted_normalized()
with self.assertRaisesRegex(ValueError, "Quaternion\\({1.14805, 0, 0}, 2.77164\\) is not normalized"):
a.transform_vector_normalized(Vector3())
def test_functions(self):
a = Quaternion.rotation(Deg(45.0), Vector3d.x_axis())
b = Quaternion.rotation(Deg(-145.0), Vector3d.x_axis())
@ -1396,6 +1505,33 @@ class Quaternion_(unittest.TestCase):
self.assertEqual(math.slerp(a, b, 0.25), Quaternion((-0.0218149, 0.0, 0.0), 0.99976))
self.assertEqual(math.slerp_shortest_path(a, b, 0.25), Quaternion((-0.691513, 0.0, 0.0), -0.722364))
def test_functions_invalid(self):
a = Quaternion.rotation(Deg(45.0), Vector3d.x_axis())
b = Quaternion.rotation(Deg(-145.0), Vector3d.x_axis())
a_invalid = a*3.0
b_invalid = b*0.5
with self.assertRaisesRegex(ValueError, "quaternions Quaternion\\({1.14805, 0, 0}, 2.77164\\) and Quaternion\\({-0.953717, -0, -0}, 0.300706\\) are not normalized"):
math.half_angle(a_invalid, b)
with self.assertRaisesRegex(ValueError, "quaternions Quaternion\\({0.382683, 0, 0}, 0.92388\\) and Quaternion\\({-0.476858, -0, -0}, 0.150353\\) are not normalized"):
math.half_angle(a, b_invalid)
with self.assertRaisesRegex(ValueError, "quaternions Quaternion\\({1.14805, 0, 0}, 2.77164\\) and Quaternion\\({-0.953717, -0, -0}, 0.300706\\) are not normalized"):
math.lerp(a_invalid, b, 0.25)
with self.assertRaisesRegex(ValueError, "quaternions Quaternion\\({0.382683, 0, 0}, 0.92388\\) and Quaternion\\({-0.476858, -0, -0}, 0.150353\\) are not normalized"):
math.lerp(a, b_invalid, 0.25)
with self.assertRaisesRegex(ValueError, "quaternions Quaternion\\({1.14805, 0, 0}, 2.77164\\) and Quaternion\\({-0.953717, -0, -0}, 0.300706\\) are not normalized"):
math.lerp_shortest_path(a_invalid, b, 0.25)
with self.assertRaisesRegex(ValueError, "quaternions Quaternion\\({0.382683, 0, 0}, 0.92388\\) and Quaternion\\({-0.476858, -0, -0}, 0.150353\\) are not normalized"):
math.lerp_shortest_path(a, b_invalid, 0.25)
with self.assertRaisesRegex(ValueError, "quaternions Quaternion\\({1.14805, 0, 0}, 2.77164\\) and Quaternion\\({-0.953717, -0, -0}, 0.300706\\) are not normalized"):
math.slerp(a_invalid, b, 0.25)
with self.assertRaisesRegex(ValueError, "quaternions Quaternion\\({0.382683, 0, 0}, 0.92388\\) and Quaternion\\({-0.476858, -0, -0}, 0.150353\\) are not normalized"):
math.slerp(a, b_invalid, 0.25)
with self.assertRaisesRegex(ValueError, "quaternions Quaternion\\({1.14805, 0, 0}, 2.77164\\) and Quaternion\\({-0.953717, -0, -0}, 0.300706\\) are not normalized"):
math.slerp_shortest_path(a_invalid, b, 0.25)
with self.assertRaisesRegex(ValueError, "quaternions Quaternion\\({0.382683, 0, 0}, 0.92388\\) and Quaternion\\({-0.476858, -0, -0}, 0.150353\\) are not normalized"):
math.slerp_shortest_path(a, b_invalid, 0.25)
def test_properties(self):
a = Quaternion()
a.vector = (1.0, 2.0, 3.0)

Loading…
Cancel
Save