From 14f7810870c1586f41ff0c15a73cb2237eaf9fe1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Sun, 8 Sep 2019 14:42:53 +0200 Subject: [PATCH] python: finish the magic and docs for Matrix[34].scaling() and friends. This is so ugly it's beautiful. The translation needed a metaclass to work properly, but the undoubtedly worst/best is making those exposed nicely in the docs. --- doc/python/magnum.math.rst | 21 +- src/python/magnum/bootstrap.h | 6 +- src/python/magnum/math.cpp | 71 ++++- src/python/magnum/math.matrix.h | 313 ++++++++++++++++------ src/python/magnum/math.matrixdouble.cpp | 10 +- src/python/magnum/math.matrixfloat.cpp | 12 +- src/python/magnum/test/test_math.py | 12 +- src/python/magnum/test/test_math_numpy.py | 4 +- 8 files changed, 337 insertions(+), 112 deletions(-) diff --git a/doc/python/magnum.math.rst b/doc/python/magnum.math.rst index 4213d38..73fc4d5 100644 --- a/doc/python/magnum.math.rst +++ b/doc/python/magnum.math.rst @@ -207,17 +207,14 @@ implemented as a *real* swizzle, allowing for convenient expressions like :py:`vec.xz = (3.5, 0.1)`. - `Static constructors and instance method overloads`_ - ---------------------------------------------------- + `Static constructors and instance method / property overloads`_ + --------------------------------------------------------------- While not common in Python, the `Matrix4.scaling()` / `Matrix4.rotation()` - methods mimic the C++ equivalent --- calling `Matrix4.scaling()` will - return a scaling matrix, while :py:`mat.scaling()` returns the 3x3 scaling - part of the matrix. Similarly for the `Matrix3` class. - - .. block-warning:: Subject to change - - On the other hand, there's currently just `Matrix3.translation()` and - the corresponding :py:`mat.translation` property is temporarily - available as an underscored `Matrix3._translation`. This will change - later. + methods mimic the C++ equivalent --- calling :py:`Matrix4.scaling(vec)` + will return a scaling matrix, while :py:`mat.scaling()` returns the 3x3 + scaling part of the matrix. With `Matrix4.translation`, it's a bit more + involved --- calling :py:`Matrix4.translation(vec)` will return a + translation matrix, while :py:`mat.translation` is a read-write property + accessing the fourth column of the matrix. Similarly for the `Matrix3` + class. diff --git a/src/python/magnum/bootstrap.h b/src/python/magnum/bootstrap.h index 530cf74..cd642c9 100644 --- a/src/python/magnum/bootstrap.h +++ b/src/python/magnum/bootstrap.h @@ -25,6 +25,8 @@ DEALINGS IN THE SOFTWARE. */ +#include + namespace pybind11 { class module; } namespace Magnum {} @@ -36,8 +38,8 @@ namespace py = pybind11; void math(py::module& root, py::module& m); void mathVectorFloat(py::module& root, py::module& m); void mathVectorIntegral(py::module& root, py::module& m); -void mathMatrixFloat(py::module& root); -void mathMatrixDouble(py::module& root); +void mathMatrixFloat(py::module& root, PyTypeObject* metaclass); +void mathMatrixDouble(py::module& root, PyTypeObject* metaclass); void mathRange(py::module& root, py::module& m); void gl(py::module& m); diff --git a/src/python/magnum/math.cpp b/src/python/magnum/math.cpp index efc908f..cdb02c5 100644 --- a/src/python/magnum/math.cpp +++ b/src/python/magnum/math.cpp @@ -341,6 +341,67 @@ template void quaternion(py::module& m, py::class_& c) { .def("__repr__", repr, "Object representation"); } +/* Behaves exactly like Py_Type_Type.tp_getattro but redirects access to the + translation attribute to _stranslation in order to make it behave like a + function when called on an object */ +PyObject* transformationMatrixGettattro(PyObject* const obj, PyObject* const name) { + if(PyUnicode_Check(name) && PyUnicode_CompareWithASCIIString(name, "translation") == 0) { + /* TODO: this means one allocation per every attribute access, any + chance we could minimize that? Storing a global reference to this + is crappy :/ Maybe allocate and store this inside + transformationMatrixMetaclass? But who would be responsible for + Py_DECREF then? Pybind's module destructors are kinda overdone: + https://pybind11.readthedocs.io/en/stable/advanced/misc.html#module-destructors */ + PyObject* const _stranslation = PyUnicode_FromString("_stranslation"); + PyObject* const ret = PyType_Type.tp_getattro(obj, _stranslation); + Py_DECREF(_stranslation); + return ret; + } + + return PyType_Type.tp_getattro(obj, name); +} + +/* Based off pybind11:detail::make_default_metaclass(), but with Python < 3.3 + support and unneeded pybind specifics removed. In particular, we don't need + any static attribute access modifications from pybind's own metaclass, as + Matrix[34] doesn't need to support assignment to static attributes. */ +PyTypeObject* transformationMatrixMetaclass() { + constexpr auto *name = "TransformationMatrixType"; + auto name_obj = py::reinterpret_steal(PyUnicode_FromString(name)); + + /* Danger zone: from now (and until PyType_Ready), make sure to + issue no Python C API calls which could potentially invoke the + garbage collector (the GC will call type_traverse(), which will in + turn find the newly constructed type in an invalid state) */ + auto heap_type = reinterpret_cast(PyType_Type.tp_alloc(&PyType_Type, 0)); + if(!heap_type) + py::pybind11_fail("magnum::transformationMatrixMetaclass(): error allocating metaclass!"); + + heap_type->ht_name = name_obj.inc_ref().ptr(); + heap_type->ht_qualname = name_obj.inc_ref().ptr(); + + auto type = &heap_type->ht_type; + type->tp_name = name; + type->tp_base = py::detail::type_incref(&PyType_Type); + type->tp_flags = Py_TPFLAGS_DEFAULT|Py_TPFLAGS_BASETYPE|Py_TPFLAGS_HEAPTYPE; + + type->tp_setattro = PyType_Type.tp_setattro; + /* In order to create reasonable docs for this, we can't override the + translation attribute at that time --- the _stranslation will be then + used for documentation. */ + if(std::getenv("MCSS_GENERATING_OUTPUT")) + type->tp_getattro = PyType_Type.tp_getattro; + else + type->tp_getattro = transformationMatrixGettattro; + + if(PyType_Ready(type) < 0) + py::pybind11_fail("magnum::transformationMatrixMetaclass(): failure in PyType_Ready()!"); + + py::setattr(reinterpret_cast(type), "__module__", py::str("magnum_builtins")); + + return type; +} + } void math(py::module& root, py::module& m) { @@ -391,9 +452,15 @@ void math(py::module& root, py::module& m) { .def("acos", [](Double angle) { return Math::acos(angle); }, "Arc cosine") .def("atan", [](Double angle) { return Math::atan(angle); }, "Arc tangent"); - /* These are needed for the quaternion, so register them before */ + /* These are needed for the quaternion, so register them before. Double + versions are called from inside these. */ magnum::mathVectorFloat(root, m); - magnum::mathMatrixFloat(root); + /* Matrices need a metaclass in order to support the magic translation + attribute, so allocate it here, just once. TODO: I'm not sure who's + responsible for deleting the object, actually -- however neither pybind + seems to be destructing the metaclasses in any way, so in the worst case + it's being done wrong in a consistent way. */ + magnum::mathMatrixFloat(root, transformationMatrixMetaclass()); /* Quaternion */ py::class_ quaternion_(root, "Quaternion", "Float quaternion"); diff --git a/src/python/magnum/math.matrix.h b/src/python/magnum/math.matrix.h index a6a4d37..f212b44 100644 --- a/src/python/magnum/math.matrix.h +++ b/src/python/magnum/math.matrix.h @@ -531,17 +531,14 @@ template void matrices( return self*other; }, "Multiply a matrix"); - /* 3x3 transformation matrix. Buffer constructors need to be *before* tuple constructors so numpy buffer protocol gets extracted correctly. */ py::implicitly_convertible, Math::Matrix3>(); everyRectangularMatrix(matrix3); everyMatrix(matrix3); matrix3 - /* Constructors. The scaling() / rotation() are handled below - as they conflict with member functions. */ - .def_static("translation", static_cast(*)(const Math::Vector2&)>(&Math::Matrix3::translation), - "2D translation matrix") + /* Constructors. The translation() / scaling() / rotation() are handled + below as they conflict with member functions. */ .def_static("reflection", &Math::Matrix3::reflection, "2D reflection matrix") .def_static("shearing_x", &Math::Matrix3::shearingX, @@ -590,7 +587,8 @@ template void matrices( .def("transform_point", &Math::Matrix3::transformPoint, "Transform a 2D point with the matrix") - /* Properties */ + /* Properties. The translation is handled below together with a static + translation(). */ .def_property("right", static_cast(Math::Matrix3::*)() const>(&Math::Matrix3::right), [](Math::Matrix3& self, const Math::Vector2& value) { self.right() = value; }, @@ -598,40 +596,118 @@ template void matrices( .def_property("up", static_cast(Math::Matrix3::*)() const>(&Math::Matrix3::up), [](Math::Matrix3& self, const Math::Vector2& value) { self.up() = value; }, - "Up-pointing 2D vector") - .def_property("_translation", // TODO - static_cast(Math::Matrix3::*)() const>(&Math::Matrix3::translation), - [](Math::Matrix3& self, const Math::Vector2& value) { self.translation() = value; }, - "2D translation part of the matrix") - - /* Static/member scaling(). Pybind doesn't support that natively, so - we create a scaling(*args, **kwargs) and dispatch ourselves. */ - .def_static("_sscaling", static_cast(*)(const Math::Vector2&)>(&Math::Matrix3::scaling), - "2D scaling matrix") - .def("_iscaling", static_cast(Math::Matrix3::*)() const>(&Math::Matrix3::scaling), - "Non-uniform scaling part of the matrix") - .def("scaling", [matrix3](py::args args, py::kwargs kwargs) { - if(py::len(args) && py::isinstance>(args[0])) { - return matrix3.attr("_iscaling")(*args, **kwargs); - } else { - return matrix3.attr("_sscaling")(*args, **kwargs); - } - }) - - /* Static/member rotation(). Pybind doesn't support that natively, so - we create a rotation(*args, **kwargs) and dispatch ourselves. */ - .def_static("_srotation", [](Radd angle) { - return Math::Matrix3::rotation(Math::Rad(angle)); - }, "2D rotation matrix") - .def("_irotation", static_cast(Math::Matrix3::*)() const>(&Math::Matrix3::rotation), - "2D rotation part of the matrix") - .def("rotation", [matrix3](py::args args, py::kwargs kwargs) { - if(py::len(args) && py::isinstance>(args[0])) { - return matrix3.attr("_irotation")(*args, **kwargs); - } else { - return matrix3.attr("_srotation")(*args, **kwargs); - } - }); + "Up-pointing 2D vector"); + + /* "Magic" static/member functions and properties. In order to have + reasonable docs, we need to disable pybind's function signatures and + supply ours faked instead. */ + { + py::options options; + options.disable_function_signatures(); + + constexpr const char* ScalingDocstring[] { + R"(scaling(*args, **kwargs) +Overloaded function. + +1. scaling(arg0: _magnum.Vector2) -> _magnum.Matrix3 + +2D scaling matrix + +2. scaling(self: _magnum.Matrix3) -> _magnum.Vector2 + +Non-uniform scaling part of the matrix +)", + R"(scaling(*args, **kwargs) +Overloaded function. + +1. scaling(arg0: _magnum.Vector2d) -> _magnum.Matrix3d + +2D scaling matrix + +2. scaling(self: _magnum.Matrix3d) -> _magnum.Vector2d + +Non-uniform scaling part of the matrix +)"}; + constexpr const char* RotationDocstring[] { + R"(rotation(*args, **kwargs) +Overloaded function. + +1. rotation(arg0: _magnum.Rad) -> _magnum.Matrix3 + +2D rotation matrix + +2. rotation(self: _magnum.Matrix3) -> _magnum.Matrix2x2 + +2D rotation part of the matrix +)", + R"(rotation(*args, **kwargs) +Overloaded function. + +1. rotation(arg0: _magnum.Rad) -> _magnum.Matrix3d + +2D rotation matrix + +2. rotation(self: _magnum.Matrix3d) -> _magnum.Matrix2x2d + +2D rotation part of the matrix +)"}; + /* This one is special, as it renames the function */ + constexpr const char* TranslationDocstring[] { + R"(_stranslation(*args, **kwargs) +Overloaded function. + +1. translation(arg0: _magnum.Vector2) -> _magnum.Matrix3 + +2D translation matrix +)", + R"(_stranslation(*args, **kwargs) +Overloaded function. + +1. translation(arg0: _magnum.Vector2d) -> _magnum.Matrix3d + +2D translation matrix +)"}; + + matrix3 + /* Static/member scaling(). Pybind doesn't support that natively, + so we create a scaling(*args, **kwargs) and dispatch ourselves. */ + .def_static("_sscaling", static_cast(*)(const Math::Vector2&)>(&Math::Matrix3::scaling)) + .def("_iscaling", static_cast(Math::Matrix3::*)() const>(&Math::Matrix3::scaling)) + .def("scaling", [matrix3](py::args args, py::kwargs kwargs) { + if(py::len(args) && py::isinstance>(args[0])) { + return matrix3.attr("_iscaling")(*args, **kwargs); + } else { + return matrix3.attr("_sscaling")(*args, **kwargs); + } + }, ScalingDocstring[sizeof(T)/4 - 1]) + + /* Static/member rotation(). Pybind doesn't support that natively, + so we create a rotation(*args, **kwargs) and dispatch ourselves. */ + .def_static("_srotation", [](Radd angle) { + return Math::Matrix3::rotation(Math::Rad(angle)); + }) + .def("_irotation", static_cast(Math::Matrix3::*)() const>(&Math::Matrix3::rotation)) + .def("rotation", [matrix3](py::args args, py::kwargs kwargs) { + if(py::len(args) && py::isinstance>(args[0])) { + return matrix3.attr("_irotation")(*args, **kwargs); + } else { + return matrix3.attr("_srotation")(*args, **kwargs); + } + }, RotationDocstring[sizeof(T)/4 - 1]) + + /* Static translation function, member translation property. This + one is tricky and can't be done without supplying a special + metaclass that replaces static access to `translation` with + `_stranslation`. */ + .def_static("_stranslation", static_cast(*)(const Math::Vector2&)>(&Math::Matrix3::translation), std::getenv("MCSS_GENERATING_OUTPUT") ? TranslationDocstring[sizeof(T)/4 - 1] : ""); + } + + /* The translation property again needs a pybind signature so we can + extract its type */ + matrix3.def_property("translation", + static_cast(Math::Matrix3::*)() const>(&Math::Matrix3::translation), + [](Math::Matrix3& self, const Math::Vector2& value) { self.translation() = value; }, + "2D translation part of the matrix"); /* 4x4 transformation matrix. Buffer constructors need to be *before* tuple constructors so numpy buffer protocol gets extracted correctly. */ @@ -639,10 +715,8 @@ template void matrices( everyRectangularMatrix(matrix4); everyMatrix(matrix4); matrix4 - /* Constructors. The scaling() / rotation() are handled below - as they conflict with member functions. */ - .def_static("translation", static_cast(*)(const Math::Vector3&)>(&Math::Matrix4::translation), - "3D translation matrix") + /* Constructors. The translation() / scaling() / rotation() are handled + below as they conflict with member functions. */ .def_static("rotation_x", [](Radd angle) { return Math::Matrix4::rotationX(Math::Rad(angle)); }, "3D rotation matrix around the X axis") @@ -715,7 +789,8 @@ template void matrices( .def("transform_point", &Math::Matrix4::transformPoint, "Transform a 3D point with the matrix") - /* Properties */ + /* Properties. The translation is handled below together with a static + translation(). */ .def_property("right", static_cast(Math::Matrix4::*)() const>(&Math::Matrix4::right), [](Math::Matrix4& self, const Math::Vector3& value) { self.right() = value; }, @@ -727,40 +802,120 @@ template void matrices( .def_property("backward", static_cast(Math::Matrix4::*)() const>(&Math::Matrix4::backward), [](Math::Matrix4& self, const Math::Vector3& value) { self.backward() = value; }, - "Backward-pointing 3D vector") - .def_property("_translation", // TODO - static_cast(Math::Matrix4::*)() const>(&Math::Matrix4::translation), - [](Math::Matrix4& self, const Math::Vector3& value) { self.translation() = value; }, - "3D translation part of the matrix") - - /* Static/member scaling(). Pybind doesn't support that natively, so - we create a scaling(*args, **kwargs) and dispatch ourselves. */ - .def_static("_sscaling", static_cast(*)(const Math::Vector3&)>(&Math::Matrix4::scaling), - "3D scaling matrix") - .def("_iscaling", static_cast(Math::Matrix4::*)() const>(&Math::Matrix4::scaling), - "Non-uniform scaling part of the matrix") - .def("scaling", [matrix4](py::args args, py::kwargs kwargs) { - if(py::len(args) && py::isinstance>(args[0])) { - return matrix4.attr("_iscaling")(*args, **kwargs); - } else { - return matrix4.attr("_sscaling")(*args, **kwargs); - } - }) - - /* 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); - }, "3D rotation matrix around arbitrary axis") - .def("_irotation", static_cast(Math::Matrix4::*)() const>(&Math::Matrix4::rotation), - "3D rotation part of the matrix") - .def("rotation", [matrix4](py::args args, py::kwargs kwargs) { - if(py::len(args) && py::isinstance>(args[0])) { - return matrix4.attr("_irotation")(*args, **kwargs); - } else { - return matrix4.attr("_srotation")(*args, **kwargs); - } - }); + "Backward-pointing 3D vector"); + + /* "Magic" static/member functions and properties. In order to have + reasonable docs, we need to disable pybind's function signatures and + supply ours faked instead. */ + { + py::options options; + options.disable_function_signatures(); + + constexpr const char* ScalingDocstring[] { + R"(scaling(*args, **kwargs) +Overloaded function. + +1. scaling(arg0: _magnum.Vector3) -> _magnum.Matrix4 + +3D scaling matrix + +2. scaling(self: _magnum.Matrix4) -> _magnum.Vector3 + +Non-uniform scaling part of the matrix +)", + R"(scaling(*args, **kwargs) +Overloaded function. + +1. scaling(arg0: _magnum.Vector3d) -> _magnum.Matrix4d + +2D scaling matrix + +2. scaling(self: _magnum.Matrix3d) -> _magnum.Vector3d + +Non-uniform scaling part of the matrix +)" + }; + constexpr const char* RotationDocstring[] { + R"(rotation(*args, **kwargs) +Overloaded function. + +1. rotation(arg0: _magnum.Rad, arg1: _magnum.Vector3) -> _magnum.Matrix4 + +3D rotation matrix + +2. rotation(self: _magnum.Matrix3) -> _magnum.Matrix3x3 + +3D rotation part of the matrix +)", + R"(rotation(*args, **kwargs) +Overloaded function. + +1. rotation(arg0: _magnum.Rad, arg1: _magnum.Vector3d) -> _magnum.Matrix4d + +3D rotation matrix + +2. rotation(self: _magnum.Matrix4d) -> _magnum.Matrix3x3d + +3D rotation part of the matrix +)", + }; + /* This one is special, as it renames the function */ + constexpr const char* TranslationDocstring[] { + R"(_stranslation(*args, **kwargs) +Overloaded function. + +1. translation(arg0: _magnum.Vector3) -> _magnum.Matrix4 + +3D translation matrix +)", + R"(_stranslation(*args, **kwargs) +Overloaded function. + +1. translation(arg0: _magnum.Vector3d) -> _magnum.Matrix4d + +3D translation matrix +)"}; + + matrix4 + /* Static/member scaling(). Pybind doesn't support that natively, + so we create a scaling(*args, **kwargs) and dispatch ourselves. */ + .def_static("_sscaling", static_cast(*)(const Math::Vector3&)>(&Math::Matrix4::scaling)) + .def("_iscaling", static_cast(Math::Matrix4::*)() const>(&Math::Matrix4::scaling)) + .def("scaling", [matrix4](py::args args, py::kwargs kwargs) { + if(py::len(args) && py::isinstance>(args[0])) { + return matrix4.attr("_iscaling")(*args, **kwargs); + } else { + return matrix4.attr("_sscaling")(*args, **kwargs); + } + }, ScalingDocstring[sizeof(T)/4 - 1]) + + /* 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("_irotation", static_cast(Math::Matrix4::*)() const>(&Math::Matrix4::rotation)) + .def("rotation", [matrix4](py::args args, py::kwargs kwargs) { + if(py::len(args) && py::isinstance>(args[0])) { + return matrix4.attr("_irotation")(*args, **kwargs); + } else { + return matrix4.attr("_srotation")(*args, **kwargs); + } + }, RotationDocstring[sizeof(T)/4 - 1]) + + /* Static translation function, member translation property. This + one is tricky and can't be done without supplying a special + metaclass that replaces static access to `translation` with + `_stranslation`. */ + .def_static("_stranslation", static_cast(*)(const Math::Vector3&)>(&Math::Matrix4::translation), std::getenv("MCSS_GENERATING_OUTPUT") ? TranslationDocstring[sizeof(T)/4 - 1] : ""); + } + + /* The translation property again needs a pybind signature so we can + extract its type */ + matrix4.def_property("translation", + static_cast(Math::Matrix4::*)() const>(&Math::Matrix4::translation), + [](Math::Matrix4& self, const Math::Vector3& value) { self.translation() = value; }, + "3D translation part of the matrix"); } } diff --git a/src/python/magnum/math.matrixdouble.cpp b/src/python/magnum/math.matrixdouble.cpp index 0375016..f84a170 100644 --- a/src/python/magnum/math.matrixdouble.cpp +++ b/src/python/magnum/math.matrixdouble.cpp @@ -27,7 +27,7 @@ namespace magnum { -void mathMatrixDouble(py::module& root) { +void mathMatrixDouble(py::module& root, PyTypeObject* const metaclass) { py::class_ matrix2x2d{root, "Matrix2x2d", "2x2 double matrix", py::buffer_protocol{}}; py::class_ matrix2x3d{root, "Matrix2x3d", "2x3 double matrix", py::buffer_protocol{}}; py::class_ matrix2x4d{root, "Matrix2x4d", "2x4 double matrix", py::buffer_protocol{}}; @@ -43,9 +43,11 @@ void mathMatrixDouble(py::module& root) { /* The subclasses don't have buffer protocol enabled, as that's already done by the base classes. Moreover, just adding py::buffer_protocol{} would cause it to not find the buffer functions as we don't add them - anywhere, thus failing with `pybind11_getbuffer(): Internal error`. */ - py::class_ matrix3d{root, "Matrix3d", "2D double transformation matrix"}; - py::class_ matrix4d{root, "Matrix4d", "3D double transformation matrix"}; + anywhere, thus failing with `pybind11_getbuffer(): Internal error`. The + metaclasses are needed for supporting the magic translation attribute, + see transformationMatrixMetaclass() in math.cpp for more information. */ + py::class_ matrix3d{root, "Matrix3d", "2D double transformation matrix", py::metaclass(reinterpret_cast(metaclass))}; + py::class_ matrix4d{root, "Matrix4d", "3D double transformation matrix", py::metaclass(reinterpret_cast(metaclass))}; /* Register type conversions as soon as possible as those should have a priority over buffer and list constructors. These need all the types to diff --git a/src/python/magnum/math.matrixfloat.cpp b/src/python/magnum/math.matrixfloat.cpp index de2700d..b9be744 100644 --- a/src/python/magnum/math.matrixfloat.cpp +++ b/src/python/magnum/math.matrixfloat.cpp @@ -27,7 +27,7 @@ namespace magnum { -void mathMatrixFloat(py::module& root) { +void mathMatrixFloat(py::module& root, PyTypeObject* const metaclass) { py::class_ matrix2x2{root, "Matrix2x2", "2x2 float matrix", py::buffer_protocol{}}; py::class_ matrix2x3{root, "Matrix2x3", "2x3 float matrix", py::buffer_protocol{}}; py::class_ matrix2x4{root, "Matrix2x4", "2x4 float matrix", py::buffer_protocol{}}; @@ -43,13 +43,15 @@ void mathMatrixFloat(py::module& root) { /* The subclasses don't have buffer protocol enabled, as that's already done by the base classes. Moreover, just adding py::buffer_protocol{} would cause it to not find the buffer functions as we don't add them - anywhere, thus failing with `pybind11_getbuffer(): Internal error`. */ - py::class_ matrix3{root, "Matrix3", "2D float transformation matrix"}; - py::class_ matrix4{root, "Matrix4", "3D float transformation matrix"}; + anywhere, thus failing with `pybind11_getbuffer(): Internal error`. The + metaclasses are needed for supporting the magic translation attribute, + see transformationMatrixMetaclass() in math.cpp for more information. */ + py::class_ matrix3{root, "Matrix3", "2D float transformation matrix", py::metaclass(reinterpret_cast(metaclass))}; + py::class_ matrix4{root, "Matrix4", "3D float transformation matrix", py::metaclass(reinterpret_cast(metaclass))}; /* Register the double types as well, only after that register type conversions because they need all the types */ - mathMatrixDouble(root); + mathMatrixDouble(root, metaclass); /* Register type conversions as soon as possible as those should have a priority over buffer and list constructors. These need all the types to diff --git a/src/python/magnum/test/test_math.py b/src/python/magnum/test/test_math.py index 1551f8e..537ffe5 100644 --- a/src/python/magnum/test/test_math.py +++ b/src/python/magnum/test/test_math.py @@ -680,7 +680,7 @@ class Matrix3_(unittest.TestCase): def test_static_methods(self): a = Matrix3.translation((0.0, -1.0)) self.assertEqual(a[2].xy, Vector2(0.0, -1.0)) - self.assertEqual(a._translation, Vector2(0.0, -1.0)) # TODO + self.assertEqual(a.translation, Vector2(0.0, -1.0)) b = Matrix3.rotation(Deg(45.0)) self.assertEqual(b.rotation(), Matrix2x2( @@ -694,11 +694,11 @@ class Matrix3_(unittest.TestCase): a = Matrix3.translation(Vector2.y_axis(-5.0))@Matrix3.rotation(Deg(45.0)) self.assertEqual(a.right, Vector2(0.707107, 0.707107)) self.assertEqual(a.up, Vector2(-0.707107, 0.707107)) - self.assertEqual(a._translation, Vector2.y_axis(-5.0)) # TODO + self.assertEqual(a.translation, Vector2.y_axis(-5.0)) a.right = Vector2.x_axis(2.0) a.up = -Vector2.y_axis() - a._translation = Vector2(0.0) # TODO + a.translation = Vector2(0.0) self.assertEqual(a, Matrix3.from_diagonal((2.0, -1.0, 1.0))) def test_methods(self): @@ -796,7 +796,7 @@ class Matrix4_(unittest.TestCase): def test_static_methods(self): a = Matrix4.translation((0.0, -1.0, 2.0)) self.assertEqual(a[3].xyz, Vector3(0.0, -1.0, 2.0)) - self.assertEqual(a._translation, Vector3(0.0, -1.0, 2.0)) # TODO + self.assertEqual(a.translation, Vector3(0.0, -1.0, 2.0)) b = Matrix4.rotation(Deg(45.0), Vector3.x_axis()) self.assertEqual(b.rotation(), Matrix3x3( @@ -812,12 +812,12 @@ class Matrix4_(unittest.TestCase): self.assertEqual(a.right, Vector3(0.707107, 0.707107, 0.0)) self.assertEqual(a.up, Vector3(-0.707107, 0.707107, 0.0)) self.assertEqual(a.backward, Vector3(0.0, 0.0, 1.0)) - self.assertEqual(a._translation, Vector3.y_axis(-5.0)) # TODO + self.assertEqual(a.translation, Vector3.y_axis(-5.0)) a.right = Vector3.x_axis(3.0) a.up = -Vector3.y_axis() a.backward = Vector3.z_axis(2.0) - a._translation = Vector3(0.0) # TODO + a.translation = Vector3(0.0) self.assertEqual(a, Matrix4.from_diagonal((3.0, -1.0, 2.0, 1.0))) def test_methods(self): diff --git a/src/python/magnum/test/test_math_numpy.py b/src/python/magnum/test/test_math_numpy.py index 71de4d9..012eeed 100644 --- a/src/python/magnum/test/test_math_numpy.py +++ b/src/python/magnum/test/test_math_numpy.py @@ -48,7 +48,7 @@ class Vector(unittest.TestCase): a.xyz = np.array([1.0, 2.0, 3.0]) b = Matrix4.translation(np.array([1.0, 2.0, 3.0])) - self.assertEqual(b._translation, Vector3(1.0, 2.0, 3.0)) + self.assertEqual(b.translation, Vector3(1.0, 2.0, 3.0)) def test_from_numpy_implicit_typed(self): # But this doesn't, works only if buffer protocol is defined @@ -56,7 +56,7 @@ class Vector(unittest.TestCase): a.xyz = np.array([1.0, 2.0, 3.0], dtype='float32') a = Matrix4.translation(np.array([1.0, 2.0, 3.0], dtype='float32')) - self.assertEqual(a._translation, Vector3(1.0, 2.0, 3.0)) + self.assertEqual(a.translation, Vector3(1.0, 2.0, 3.0)) def test_from_numpy_invalid_dimensions(self): a = np.array([[1, 2], [3, 4]])