From d818e406983651dec85caafd55efdee07fab4387 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Sat, 14 Sep 2019 01:00:51 +0200 Subject: [PATCH] python: implemented vector swizzles. --- doc/python/magnum.math.rst | 18 ++++--- src/python/magnum/math.vector.h | 78 +++++++++++++++++++++++++++++ src/python/magnum/test/test_math.py | 60 ++++++++++++++++++++++ 3 files changed, 149 insertions(+), 7 deletions(-) diff --git a/doc/python/magnum.math.rst b/doc/python/magnum.math.rst index 73fc4d5..8e083f8 100644 --- a/doc/python/magnum.math.rst +++ b/doc/python/magnum.math.rst @@ -192,6 +192,17 @@ - All vector and matrix classes implement :py:`len()`, which is used instead of e.g. :dox:`Math::Vector::Size`. Works on both classes and instances. + - :cpp:`Math::gather()` and :cpp:`Math::scatter()` operations are + implemented as real swizzles: + + .. code:: pycon + + >>> a = Vector4(1.5, 0.3, -1.0, 1.0) + >>> b = Vector4(7.2, 2.3, 1.1, 0.0) + >>> a.wxy = b.xwz + >>> a + Vector(0, 1.1, -1, 7.2) + - :py:`mat[a][b] = c` on matrices doesn't do the expected thing, use :py:`mat[a, b] = c` instead - :cpp:`Math::BoolVector::set()` doesn't exist, use ``[]`` instead @@ -200,13 +211,6 @@ possible to do in Python. Here the boolean operations behave like if :py:`any()` was applied before doing the operation. - .. block-warning:: Subject to change - - The :dox:`Math::swizzle()` operation is not yet available in the Python - API. Thanks to better flexibility of the Python language this will get - implemented as a *real* swizzle, allowing for convenient expressions - like :py:`vec.xz = (3.5, 0.1)`. - `Static constructors and instance method / property overloads`_ --------------------------------------------------------------- diff --git a/src/python/magnum/math.vector.h b/src/python/magnum/math.vector.h index afb8875..7b425af 100644 --- a/src/python/magnum/math.vector.h +++ b/src/python/magnum/math.vector.h @@ -228,6 +228,84 @@ template void vector(py::module& m, py::class_& c) { return self[i]; }, "Value at given position") + /* Swizzle */ + /* TODO: both of these could be *way* more efficiently implemented + directly on PyObject (no need to throw, no need to do string + conversions...) but then these wouldn't be visible to docs I fear */ + .def("__getattr__", [](T& self, const std::string& name) -> py::object { + if(name.size() > 4) { + PyErr_SetString(PyExc_AttributeError, "only four-component swizzles are supported at most"); + throw pybind11::error_already_set{}; + } + + Math::Vector4 out; + for(std::size_t i = 0; i != name.size(); ++i) { + if(name[i] == 'x' || name[i] == 'r') out[i] = self[0]; + else if(name[i] == 'y' || name[i] == 'g') out[i] = self[1]; + else if(T::Size > 2 && (name[i] == 'z' || name[i] == 'b')) out[i] = self[2]; + else if(T::Size > 3 && (name[i] == 'w' || name[i] == 'a')) out[i] = self[3]; + else { + PyErr_SetString(PyExc_AttributeError, "invalid swizzle"); + throw pybind11::error_already_set{}; + } + } + + if(name.size() == 4) return py::cast(out); + else if(name.size() == 3) return py::cast(out.xyz()); + else if(name.size() == 2) return py::cast(out.xy()); + /* this should be handled by the x/y/z/w/r/g/b/a properties instead */ + else CORRADE_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */ + }, "Vector swizzle") + .def("__setattr__", [](T& self, py::str nameO, py::object valueO) { + std::string name = py::cast(nameO); + /* If the name is just one character, this is better handled by + dedicated properties (and if not, it'll provide a better + diagnostic than we can). Same for xy / xyz / ... when + applicable, and when the name contains non-swizzle characters */ + if(name.size() == 1 || + (name.compare("xy") == 0 && T::Size > 2) || + (name.compare("xyz") == 0 && T::Size > 3) || + (name.compare("rgb") == 0 && T::Size > 3) || + name.find_first_not_of("xyzwrgba") != std::string::npos) { + if(PySuper_Type.tp_setattro(py::cast(self).ptr(), nameO.ptr(), valueO.ptr()) != 0) + throw pybind11::error_already_set{}; + return; + } + + /* Here we can be certain it's a swizzle attempt, so throw clear + error messages */ + const typename T::Type* data; + std::size_t size; + if(py::isinstance>(valueO)) { + data = py::cast&>(valueO).data(); + size = 2; + } else if(py::isinstance>(valueO)) { + data = py::cast&>(valueO).data(); + size = 3; + } else if(py::isinstance>(valueO)) { + data = py::cast&>(valueO).data(); + size = 4; + } else { + PyErr_SetString(PyExc_TypeError, "unrecognized swizzle type"); + throw pybind11::error_already_set{}; + } + + if(name.size() != size) { + PyErr_SetString(PyExc_TypeError, "swizzle doesn't match passed vector component count"); + throw pybind11::error_already_set{}; + } + for(std::size_t i = 0; i != name.size(); ++i) { + if(name[i] == 'x' || name[i] == 'r') self[0] = data[i]; + else if(name[i] == 'y' || name[i] == 'g') self[1] = data[i]; + else if(T::Size > 2 && (name[i] == 'z' || name[i] == 'b')) self[2] = data[i]; + else if(T::Size > 3 && (name[i] == 'w' || name[i] == 'a')) self[3] = data[i]; + else { + PyErr_SetString(PyExc_AttributeError, "invalid swizzle"); + throw pybind11::error_already_set{}; + } + } + }, "Vector swizzle") + /* Member functions common for floating-point and integer types */ .def("is_zero", &T::isZero, "Whether the vector is zero") .def("dot", static_cast(&T::dot), "Dot product of the vector") diff --git a/src/python/magnum/test/test_math.py b/src/python/magnum/test/test_math.py index 537ffe5..5e7df25 100644 --- a/src/python/magnum/test/test_math.py +++ b/src/python/magnum/test/test_math.py @@ -267,6 +267,66 @@ class Vector(unittest.TestCase): 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)) + def test_swizzle(self): + self.assertEqual(Vector3(3.0, 1.5, 0.4).yzxz, Vector4(1.5, 0.4, 3.0, 0.4)) + self.assertEqual(Vector3(3.0, 1.5, 0.4).gbrb, Vector4(1.5, 0.4, 3.0, 0.4)) + self.assertEqual(Vector4(3.0, 1.5, 0.4, 1.2).wyx, Vector3(1.2, 1.5, 3.0)) + self.assertEqual(Vector4(3.0, 1.5, 0.4, 1.2).agr, Vector3(1.2, 1.5, 3.0)) + self.assertEqual(Vector2(3.0, 1.5).yx, Vector2(1.5, 3.0)) + self.assertEqual(Vector2(3.0, 1.5).gr, Vector2(1.5, 3.0)) + + with self.assertRaisesRegex(AttributeError, "only four-component swizzles are supported at most"): + Vector4().xyzwx + with self.assertRaisesRegex(AttributeError, "invalid swizzle"): + Vector3().xyzw + with self.assertRaisesRegex(AttributeError, "invalid swizzle"): + Vector2().xyz + with self.assertRaisesRegex(AttributeError, "invalid swizzle"): + Vector4().c + + def test_swizzle_set(self): + a1 = Vector3(3.0, 1.5, 0.4) + a2 = Vector3(3.0, 1.5, 0.4) + a1.zy = Vector2(0.5, 1.3) + a2.bg = Vector2(0.5, 1.3) + self.assertEqual(a1, Vector3(3.0, 1.3, 0.5)) + self.assertEqual(a1, Vector3(3.0, 1.3, 0.5)) + + b1 = Vector4(3.0, 1.5, 0.4, 1.2) + b2 = Vector4(3.0, 1.5, 0.4, 1.2) + b1.wxz = Vector3(1.1, 0.0, -1.3) + b2.arb = Vector3(1.1, 0.0, -1.3) + self.assertEqual(b1, Vector4(0.0, 1.5, -1.3, 1.1)) + self.assertEqual(b2, Vector4(0.0, 1.5, -1.3, 1.1)) + + # Not sure if this should be supported, but also why not + c = Vector2(1.1, 0.4) + c.xyyx = Vector4(0.1, 0.2, 0.3, 0.7) + self.assertEqual(c, Vector2(0.7, 0.3)) + + # Passing derived types should work too + d = Vector4(3.0, 1.5, 0.4, 1.2) + d.wxz = Color3(1.1, 0.0, -1.3) + self.assertEqual(d, Vector4(0.0, 1.5, -1.3, 1.1)) + + # Handled by pybind / python as a fallback directly + with self.assertRaises(AttributeError): + Vector4().xc = Vector2() + with self.assertRaisesRegex(TypeError, "incompatible function arguments"): + Vector4().xyz = 3 + with self.assertRaisesRegex(TypeError, "incompatible function arguments"): + Vector4().xy = 3 + + # Handled by the swizzle implementation + with self.assertRaisesRegex(TypeError, "unrecognized swizzle type"): + Vector4().xzy = 3 + with self.assertRaisesRegex(TypeError, "swizzle doesn't match passed vector component count"): + Vector2().yx = Vector3() + with self.assertRaisesRegex(AttributeError, "invalid swizzle"): + Vector3().xyzw = Vector4() + with self.assertRaisesRegex(AttributeError, "invalid swizzle"): + Vector2().xyz = Vector3() + def test_repr(self): self.assertEqual(repr(Vector3(1.0, 3.14, -13.37)), 'Vector(1, 3.14, -13.37)')