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 :py:`mat.translation` is a read-write property accessing the fourth column
of the matrix. Similarly for the :ref:`Matrix3` class. 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 .. For pickling, because py::pickle() doesn't seem to have a way to set the
__setstate__ / __getstate__ docs directly FFS __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), .def("dot", static_cast<typename T::Type(*)(const T&, const T&)>(&Math::dot),
"Dot product between two quaternions") "Dot product between two quaternions")
.def("half_angle", [](const T& normalizedA, const T& normalizedB) { .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 /** @todo switch back to angle() once it's reintroduced with the
correct output again */ correct output again */
return Radd(Math::halfAngle(normalizedA, normalizedB)); return Radd(Math::halfAngle(normalizedA, normalizedB));
}, "Angle between normalized quaternions", py::arg("normalized_a"), py::arg("normalized_b")) }, "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), .def("lerp", [](const T& normalizedA, const T& normalizedB, typename T::Type t) {
"Linear interpolation of two quaternions", py::arg("normalized_a"), py::arg("normalized_b"), py::arg("t")) if(!normalizedA.isNormalized() || !normalizedB.isNormalized()) {
.def("lerp_shortest_path", static_cast<T(*)(const T&, const T&, typename T::Type)>(&Math::lerpShortestPath), PyErr_Format(PyExc_ValueError, "quaternions %S and %S are not normalized", py::cast(normalizedA).ptr(), py::cast(normalizedB).ptr());
"Linear shortest-path interpolation of two quaternions", py::arg("normalized_a"), py::arg("normalized_b"), py::arg("t")) throw py::error_already_set{};
.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")) return Math::lerp(normalizedA, normalizedB, t);
.def("slerp_shortest_path", static_cast<T(*)(const T&, const T&, typename T::Type)>(&Math::slerpShortestPath), }, "Linear interpolation of two quaternions", py::arg("normalized_a"), py::arg("normalized_b"), py::arg("t"))
"Spherical linear shortest-path 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 c
/* Constructors */ /* Constructors */
.def_static("rotation", [](Radd angle, const Math::Vector3<typename T::Type>& axis) { .def_static("rotation", [](Radd angle, const Math::Vector3<typename T::Type>& normalizedAxis) {
return T::rotation(Math::Rad<typename T::Type>(angle), axis); 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")) }, "Rotation quaternion", py::arg("angle"), py::arg("normalized_axis"))
.def_static("from_matrix", &T::fromMatrix, .def_static("from_matrix", [](const Math::Matrix3x3<typename T::Type>& matrix) {
"Create a quaternion from rotation matrix", py::arg("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", []() { .def_static("zero_init", []() {
return T{Math::ZeroInit}; return T{Math::ZeroInit};
}, "Construct a zero-initialized quaternion") }, "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, .def("is_normalized", &T::isNormalized,
"Whether the quaternion is normalized") "Whether the quaternion is normalized")
.def("angle", [](const T& self) { .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()); return Radd(self.angle());
}, "Rotation angle of a unit quaternion") }, "Rotation angle of a unit quaternion")
.def("axis", &T::axis, .def("axis", [](const T& self) {
"Rotation axis of a unit quaternion") 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, .def("to_matrix", &T::toMatrix,
"Convert to a rotation matrix") "Convert to a rotation matrix")
.def("dot", &T::dot, .def("dot", &T::dot,
@ -401,12 +443,22 @@ template<class T> void quaternion(py::module_& m, py::class_<T>& c) {
"Conjugated quaternion") "Conjugated quaternion")
.def("inverted", &T::inverted, .def("inverted", &T::inverted,
"Inverted quaternion") "Inverted quaternion")
.def("inverted_normalized", &T::invertedNormalized, .def("inverted_normalized", [](const T& self) {
"Inverted normalized quaternion") 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, .def("transform_vector", &T::transformVector,
"Rotate a vector with a quaternion", py::arg("vector")) "Rotate a vector with a quaternion", py::arg("vector"))
.def("transform_vector_normalized", &T::transformVectorNormalized, .def("transform_vector_normalized", [](const T& self, const Math::Vector3<typename T::Type>& vector) {
"Rotate a vector with a normalized quaternion", py::arg("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 */ /* Properties */
.def_property("vector", .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(); return self.adjugate();
}, "Adjugate matrix") }, "Adjugate matrix")
.def("inverted", &T::inverted, "Inverted 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 { .def("__matmul__", [](const T& self, const T& other) -> T {
return self*other; return self*other;
}, "Multiply a matrix") }, "Multiply a matrix")
@ -570,8 +576,13 @@ template<class T> void matrices(
matrix3 matrix3
/* Constructors. The translation() / scaling() / rotation() are handled /* Constructors. The translation() / scaling() / rotation() are handled
below as they conflict with member functions. */ below as they conflict with member functions. */
.def_static("reflection", &Math::Matrix3<T>::reflection, .def_static("reflection", [](const Math::Vector2<T>& normal) {
"2D reflection matrix", py::arg("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, .def_static("shearing_x", &Math::Matrix3<T>::shearingX,
"2D shearing matrix along the X axis", py::arg("amount")) "2D shearing matrix along the X axis", py::arg("amount"))
.def_static("shearing_y", &Math::Matrix3<T>::shearingY, .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") "2D rotation and scaling part of the matrix")
.def("rotation_shear", &Math::Matrix3<T>::rotationShear, .def("rotation_shear", &Math::Matrix3<T>::rotationShear,
"2D rotation and shear part of the matrix") "2D rotation and shear part of the matrix")
.def("rotation_normalized", &Math::Matrix3<T>::rotationNormalized, .def("rotation_normalized", [](const Math::Matrix3<T>& self) {
"2D rotation part of the matrix assuming there is no scaling") /* 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, .def("scaling_squared", &Math::Matrix3<T>::scalingSquared,
"Non-uniform scaling part of the matrix, squared") "Non-uniform scaling part of the matrix, squared")
.def("uniform_scaling_squared", &Math::Matrix3<T>::uniformScalingSquared, .def("uniform_scaling_squared", [](const Math::Matrix3<T>& self) {
"Uniform scaling part of the matrix, squared") /* Same as implementation of uniformScalingSquared() */
.def("uniform_scaling", &Math::Matrix3<T>::uniformScaling, const T scalingSquared = self[0].xy().dot();
"Uniform scaling part of the matrix") if(!Math::TypeTraits<T>::equals(self[1].xy().dot(), scalingSquared)) {
.def("inverted_rigid", &Math::Matrix3<T>::invertedRigid, PyErr_Format(PyExc_ValueError, "the matrix doesn't have uniform scaling:\n%S", py::cast(self.rotationScaling()).ptr());
"Inverted rigid transformation matrix") 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, .def("transform_vector", &Math::Matrix3<T>::transformVector,
"Transform a 2D vector with the matrix", py::arg("vector")) "Transform a 2D vector with the matrix", py::arg("vector"))
.def("transform_point", &Math::Matrix3<T>::transformPoint, .def("transform_point", &Math::Matrix3<T>::transformPoint,
@ -719,7 +757,15 @@ Overloaded function.
.def_static("_srotation", [](Radd angle) { .def_static("_srotation", [](Radd angle) {
return Math::Matrix3<T>::rotation(Math::Rad<T>(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) { .def("rotation", [matrix3](const py::args& args, const py::kwargs& kwargs) {
if(py::len(args) && py::isinstance<Math::Matrix3<T>>(args[0])) { if(py::len(args) && py::isinstance<Math::Matrix3<T>>(args[0])) {
return matrix3.attr("_irotation")(*args, **kwargs); return matrix3.attr("_irotation")(*args, **kwargs);
@ -759,8 +805,13 @@ Overloaded function.
.def_static("rotation_z", [](Radd angle) { .def_static("rotation_z", [](Radd angle) {
return Math::Matrix4<T>::rotationZ(Math::Rad<T>(angle)); return Math::Matrix4<T>::rotationZ(Math::Rad<T>(angle));
}, "3D rotation matrix around the Z axis", py::arg("angle")) }, "3D rotation matrix around the Z axis", py::arg("angle"))
.def_static("reflection", &Math::Matrix4<T>::reflection, .def_static("reflection", [](const Math::Vector3<T>& normal) {
"3D reflection matrix", py::arg("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, .def_static("shearing_xy", &Math::Matrix4<T>::shearingXY,
"3D shearing matrix along the XY plane", py::arg("amount_x"), py::arg("amount_y")) "3D shearing matrix along the XY plane", py::arg("amount_x"), py::arg("amount_y"))
.def_static("shearing_xz", &Math::Matrix4<T>::shearingXZ, .def_static("shearing_xz", &Math::Matrix4<T>::shearingXZ,
@ -809,18 +860,49 @@ Overloaded function.
"3D rotation and scaling part of the matrix") "3D rotation and scaling part of the matrix")
.def("rotation_shear", &Math::Matrix4<T>::rotationShear, .def("rotation_shear", &Math::Matrix4<T>::rotationShear,
"3D rotation and shear part of the matrix") "3D rotation and shear part of the matrix")
.def("rotation_normalized", &Math::Matrix4<T>::rotationNormalized, .def("rotation_normalized", [](const Math::Matrix4<T>& self) {
"3D rotation part of the matrix assuming there is no scaling") /* 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, .def("scaling_squared", &Math::Matrix4<T>::scalingSquared,
"Non-uniform scaling part of the matrix, squared") "Non-uniform scaling part of the matrix, squared")
.def("uniform_scaling_squared", &Math::Matrix4<T>::uniformScalingSquared, .def("uniform_scaling_squared", [](const Math::Matrix4<T>& self) {
"Uniform scaling part of the matrix, squared") /* Same as implementation of uniformScalingSquared() */
.def("uniform_scaling", &Math::Matrix4<T>::uniformScaling, const T scalingSquared = self[0].xyz().dot();
"Uniform scaling part of the matrix") 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, .def("normal_matrix", &Math::Matrix4<T>::normalMatrix,
"Normal matrix") "Normal matrix")
.def("inverted_rigid", &Math::Matrix4<T>::invertedRigid, .def("inverted_rigid", [](const Math::Matrix4<T>& self) {
"Inverted rigid transformation matrix") 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, .def("transform_vector", &Math::Matrix4<T>::transformVector,
"Transform a 3D vector with the matrix", py::arg("vector")) "Transform a 3D vector with the matrix", py::arg("vector"))
.def("transform_point", &Math::Matrix4<T>::transformPoint, .def("transform_point", &Math::Matrix4<T>::transformPoint,
@ -928,10 +1010,22 @@ Overloaded function.
/* Static/member rotation(). Pybind doesn't support that natively, /* Static/member rotation(). Pybind doesn't support that natively,
so we create a rotation(*args, **kwargs) and dispatch ourselves. */ so we create a rotation(*args, **kwargs) and dispatch ourselves. */
.def_static("_srotation", [](Radd angle, const Math::Vector3<T>& axis) { .def_static("_srotation", [](Radd angle, const Math::Vector3<T>& normalizedAxis) {
return Math::Matrix4<T>::rotation(Math::Rad<T>(angle), axis); 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) { .def("rotation", [matrix4](const py::args& args, const py::kwargs& kwargs) {
if(py::len(args) && py::isinstance<Math::Matrix4<T>>(args[0])) { if(py::len(args) && py::isinstance<Math::Matrix4<T>>(args[0])) {
return matrix4.attr("_irotation")(*args, **kwargs); 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)}; return T{Math::fma(a, b, c)};
}, "Fused multiply-add") }, "Fused multiply-add")
.def("angle", [](const T& a, const T& b) { return Radd(Math::angle(a, b)); }, .def("angle", [](const T& normalizedA, const T& normalizedB) {
"Angle between normalized vectors", py::arg("normalized_a"), py::arg("normalized_b")); 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 c
.def("is_normalized", &T::isNormalized, "Whether the vector is normalized") .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); return self.projected(line);
}, "Vector projected onto a line", py::arg("line")) }, "Vector projected onto a line", py::arg("line"))
.def("projected_onto_normalized", [](const T& self, const T& 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); return self.projectedOntoNormalized(line);
}, "Vector projected onto a normalized line", py::arg("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)) self.assertEqual(a.minmax(), (-13.5, 3.5))
def test_ops(self): 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(math.dot(Vector2(0.5, 3.0), Vector2(2.0, 0.5)), 2.5)
self.assertEqual(Deg(math.angle( self.assertEqual(Deg(math.angle(a, b)), Deg(44.9999807616716))
Vector2(0.5, 3.0).normalized(),
Vector2(2.0, 0.5).normalized())), Deg(66.5014333443446))
self.assertEqual(Vector3(1.0, 2.0, 0.3).projected(Vector3.y_axis()), self.assertEqual(Vector3(1.0, 2.0, 0.3).projected(Vector3.y_axis()),
Vector3.y_axis(2.0)) Vector3.y_axis(2.0))
self.assertEqual(Vector3(1.0, 2.0, 0.3).projected_onto_normalized(Vector3.y_axis()), self.assertEqual(Vector3(1.0, 2.0, 0.3).projected_onto_normalized(Vector3.y_axis()),
Vector3.y_axis(2.0)) 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): def test_ops_number_on_the_left(self):
self.assertEqual(2.0*Vector2(1.0, -3.0), Vector2(2.0, -6.0)) 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)) 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), (0.0, -1.0),
(1.0, 0.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): def test_repr(self):
a = Matrix2x3((1.0, 2.0, 3.0), a = Matrix2x3((1.0, 2.0, 3.0),
(4.0, 5.0, 6.0)) (4.0, 5.0, 6.0))
@ -1039,6 +1058,10 @@ class Matrix3_(unittest.TestCase):
Vector3(4.0, 5.0, 0.0), Vector3(4.0, 5.0, 0.0),
Vector3(7.0, 8.0, 1.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): def test_pickle(self):
data = pickle.dumps(Matrix3((1.0, 2.0, 3.0), data = pickle.dumps(Matrix3((1.0, 2.0, 3.0),
(4.0, 5.0, 6.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(b.inverted(), Matrix3.scaling(Vector2(1/3.0)))
self.assertEqual(a.inverted_orthogonal(), Matrix3.rotation(Deg(-45.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): def test_methods_return_type(self):
self.assertIsInstance(Matrix3.zero_init(), Matrix3) self.assertIsInstance(Matrix3.zero_init(), Matrix3)
self.assertIsInstance(Matrix3.from_diagonal((3.0, 1.0, 1.0)), 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(9.0, 10.0, 11.0, 0.0),
Vector4(13.0, 14.0, 15.0, 1.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): def test_pickle(self):
data = pickle.dumps(Matrix4((1.0, 2.0, 3.0, 4.0), data = pickle.dumps(Matrix4((1.0, 2.0, 3.0, 4.0),
(5.0, 6.0, 7.0, 8.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().transposed(), Matrix4)
self.assertIsInstance(Matrix4().inverted(), 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 # 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 # 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()) b = Quaternion.from_matrix(Matrix4.rotation_x(Deg(45.0)).rotation_scaling())
self.assertEqual(a, Quaternion((0.382683, 0.0, 0.0), 0.92388)) 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): def test_pickle(self):
data = pickle.dumps(Quaternion((1.0, 2.0, 3.0), 4.0)) 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)) 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(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)) 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): def test_functions(self):
a = Quaternion.rotation(Deg(45.0), Vector3d.x_axis()) a = Quaternion.rotation(Deg(45.0), Vector3d.x_axis())
b = Quaternion.rotation(Deg(-145.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(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)) 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): def test_properties(self):
a = Quaternion() a = Quaternion()
a.vector = (1.0, 2.0, 3.0) a.vector = (1.0, 2.0, 3.0)

Loading…
Cancel
Save