diff --git a/doc/python/magnum.math.rst b/doc/python/magnum.math.rst index 7b72f7e..376789b 100644 --- a/doc/python/magnum.math.rst +++ b/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 diff --git a/src/python/magnum/math.cpp b/src/python/magnum/math.cpp index 94d9432..36f4a46 100644 --- a/src/python/magnum/math.cpp +++ b/src/python/magnum/math.cpp @@ -302,27 +302,60 @@ template void quaternion(py::module_& m, py::class_& c) { .def("dot", static_cast(&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(&Math::lerp), - "Linear interpolation of two quaternions", py::arg("normalized_a"), py::arg("normalized_b"), py::arg("t")) - .def("lerp_shortest_path", static_cast(&Math::lerpShortestPath), - "Linear shortest-path interpolation of two quaternions", py::arg("normalized_a"), py::arg("normalized_b"), py::arg("t")) - .def("slerp", static_cast(&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(&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& axis) { - return T::rotation(Math::Rad(angle), axis); + .def_static("rotation", [](Radd angle, const Math::Vector3& 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(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& matrix) { + /* Same as the check in fromMatrix() */ + if(std::abs(matrix.determinant() - typename T::Type(1)) >= typename T::Type(3)*Math::TypeTraits::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 void quaternion(py::module_& m, py::class_& 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 void quaternion(py::module_& m, py::class_& 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& 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", diff --git a/src/python/magnum/math.matrix.h b/src/python/magnum/math.matrix.h index f97cd8a..9ecb3ca 100644 --- a/src/python/magnum/math.matrix.h +++ b/src/python/magnum/math.matrix.h @@ -266,7 +266,13 @@ template void everyMatrix(py::class_& 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 void matrices( matrix3 /* Constructors. The translation() / scaling() / rotation() are handled below as they conflict with member functions. */ - .def_static("reflection", &Math::Matrix3::reflection, - "2D reflection matrix", py::arg("normal")) + .def_static("reflection", [](const Math::Vector2& 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::reflection(normal); + }, "2D reflection matrix", py::arg("normal")) .def_static("shearing_x", &Math::Matrix3::shearingX, "2D shearing matrix along the X axis", py::arg("amount")) .def_static("shearing_y", &Math::Matrix3::shearingY, @@ -605,16 +616,43 @@ template void matrices( "2D rotation and scaling part of the matrix") .def("rotation_shear", &Math::Matrix3::rotationShear, "2D rotation and shear part of the matrix") - .def("rotation_normalized", &Math::Matrix3::rotationNormalized, - "2D rotation part of the matrix assuming there is no scaling") + .def("rotation_normalized", [](const Math::Matrix3& self) { + /* Same as implementation of rotationNormalized() */ + const Math::Matrix2x2 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::scalingSquared, "Non-uniform scaling part of the matrix, squared") - .def("uniform_scaling_squared", &Math::Matrix3::uniformScalingSquared, - "Uniform scaling part of the matrix, squared") - .def("uniform_scaling", &Math::Matrix3::uniformScaling, - "Uniform scaling part of the matrix") - .def("inverted_rigid", &Math::Matrix3::invertedRigid, - "Inverted rigid transformation matrix") + .def("uniform_scaling_squared", [](const Math::Matrix3& self) { + /* Same as implementation of uniformScalingSquared() */ + const T scalingSquared = self[0].xy().dot(); + if(!Math::TypeTraits::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& self) { + /* Same as implementation of uniformScalingSquared(), which + uniformScaling() delegates to */ + const T scalingSquared = self[0].xy().dot(); + if(!Math::TypeTraits::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& 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::transformVector, "Transform a 2D vector with the matrix", py::arg("vector")) .def("transform_point", &Math::Matrix3::transformPoint, @@ -719,7 +757,15 @@ Overloaded function. .def_static("_srotation", [](Radd angle) { return Math::Matrix3::rotation(Math::Rad(angle)); }) - .def("_irotation", static_cast(Math::Matrix3::*)() const>(&Math::Matrix3::rotation)) + .def("_irotation", [](const Math::Matrix3& self) { + /* Same as implementation of rotation() */ + const Math::Matrix2x2 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>(args[0])) { return matrix3.attr("_irotation")(*args, **kwargs); @@ -759,8 +805,13 @@ Overloaded function. .def_static("rotation_z", [](Radd angle) { return Math::Matrix4::rotationZ(Math::Rad(angle)); }, "3D rotation matrix around the Z axis", py::arg("angle")) - .def_static("reflection", &Math::Matrix4::reflection, - "3D reflection matrix", py::arg("normal")) + .def_static("reflection", [](const Math::Vector3& 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::reflection(normal); + }, "3D reflection matrix", py::arg("normal")) .def_static("shearing_xy", &Math::Matrix4::shearingXY, "3D shearing matrix along the XY plane", py::arg("amount_x"), py::arg("amount_y")) .def_static("shearing_xz", &Math::Matrix4::shearingXZ, @@ -809,18 +860,49 @@ Overloaded function. "3D rotation and scaling part of the matrix") .def("rotation_shear", &Math::Matrix4::rotationShear, "3D rotation and shear part of the matrix") - .def("rotation_normalized", &Math::Matrix4::rotationNormalized, - "3D rotation part of the matrix assuming there is no scaling") + .def("rotation_normalized", [](const Math::Matrix4& self) { + /* Same as implementation of rotationNormalized() */ + const Math::Matrix3x3 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::scalingSquared, "Non-uniform scaling part of the matrix, squared") - .def("uniform_scaling_squared", &Math::Matrix4::uniformScalingSquared, - "Uniform scaling part of the matrix, squared") - .def("uniform_scaling", &Math::Matrix4::uniformScaling, - "Uniform scaling part of the matrix") + .def("uniform_scaling_squared", [](const Math::Matrix4& self) { + /* Same as implementation of uniformScalingSquared() */ + const T scalingSquared = self[0].xyz().dot(); + if(!Math::TypeTraits::equals(self[1].xyz().dot(), scalingSquared) || + !Math::TypeTraits::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& self) { + /* Same as implementation of uniformScalingSquared(), which + uniformScaling() delegates to */ + const T scalingSquared = self[0].xyz().dot(); + if(!Math::TypeTraits::equals(self[1].xyz().dot(), scalingSquared) || + !Math::TypeTraits::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::normalMatrix, "Normal matrix") - .def("inverted_rigid", &Math::Matrix4::invertedRigid, - "Inverted rigid transformation matrix") + .def("inverted_rigid", [](const Math::Matrix4& 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::transformVector, "Transform a 3D vector with the matrix", py::arg("vector")) .def("transform_point", &Math::Matrix4::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& axis) { - return Math::Matrix4::rotation(Math::Rad(angle), axis); + .def_static("_srotation", [](Radd angle, const Math::Vector3& 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::rotation(Math::Rad(angle), normalizedAxis); + }) + .def("_irotation", [](const Math::Matrix4& self) { + /* Same as implementation of rotation() */ + const Math::Matrix3x3 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::Matrix4::*)() const>(&Math::Matrix4::rotation)) .def("rotation", [matrix4](const py::args& args, const py::kwargs& kwargs) { if(py::len(args) && py::isinstance>(args[0])) { return matrix4.attr("_irotation")(*args, **kwargs); diff --git a/src/python/magnum/math.vectorfloat.cpp b/src/python/magnum/math.vectorfloat.cpp index 2712ce2..972c7cc 100644 --- a/src/python/magnum/math.vectorfloat.cpp +++ b/src/python/magnum/math.vectorfloat.cpp @@ -57,8 +57,13 @@ template void vectorFloat(py::module_& m, py::class_& 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 void vectorFloat(py::module_& m, py::class_& 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")); } diff --git a/src/python/magnum/test/test_math.py b/src/python/magnum/test/test_math.py index 75a9075..d9e9072 100644 --- a/src/python/magnum/test/test_math.py +++ b/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)