diff --git a/doc/python/conf.py b/doc/python/conf.py index 80944829..8f17123f 100644 --- a/doc/python/conf.py +++ b/doc/python/conf.py @@ -15,10 +15,11 @@ import magnum.platform.glfw import magnum.platform.sdl2 import magnum.shaders +import magnum.scenegraph # So the doc see everything # TODO: use just +=, m.css should reorder this on its own -magnum.__all__ = ['math', 'gl', 'platform', 'shaders'] + magnum.__all__ +magnum.__all__ = ['math', 'gl', 'platform', 'shaders', 'scenegraph'] + magnum.__all__ # TODO ugh... can this be expressed directly in pybind? magnum.gl.__annotations__ = {} @@ -43,7 +44,8 @@ 'magnum.rst', 'magnum.gl.rst', 'magnum.math.rst', - 'magnum.platform.rst' + 'magnum.platform.rst', + 'magnum.scenegraph.rst' ] LINKS_NAVBAR2 = [ diff --git a/doc/python/magnum.scenegraph.rst b/doc/python/magnum.scenegraph.rst new file mode 100644 index 00000000..7ee32af6 --- /dev/null +++ b/doc/python/magnum.scenegraph.rst @@ -0,0 +1,83 @@ +.. + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019 + Vladimír Vondruš + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +.. + +.. py:module:: magnum.scenegraph + + The Python API for :dox:`SceneGraph` provides, similarly to C++, multiple + different transformation implementations. Recommended usage is importing + desired implementation akin to :cpp:`typedef`\ ing the types in C++: + + .. code-figure:: + + .. code:: c++ + + #include + #include + #include + + typedef SceneGraph::Scene Scene3D; + typedef SceneGraph::Object Object3D; + + C++ + + .. code-figure:: + + .. code:: py + + from magnum import scenegraph + from magnum.scenegraph.matrix import Scene3D, Object3D + + Python + + `Scene vs Object`_ + ================== + + In C++, the Scene is a subclass of Object. However, because the Scene + object is not transformable nor it's possible to attach features to it, + most of the inherited API is unusable. This could be considered a wart of + the C++ API, so the Python bindings expose Scene and Object as two + unrelated types and all APIs that can take either a Scene or an Object + have corresponding overloads. + + `Reference counting`_ + ===================== + + Compared to C++, the following is done with all Object instances created + on Python side: + + - the object is additionally referenced by its parent (if there's any) + so objects created in local scope stay alive even after exiting the + scope + - deleting its parent (either due to it going out of scope in C++ or + using :py:`del` in Python) will cause it to have no parent (instead of + being cascade deleted as well) + - in order to actually destroy an object, it has to have no parent + + For features it's a bit different, as it's not possible to have a feature + that's not attached to any object: + + .. note-danger:: + + UH OH cyclic refs in pybind HOW diff --git a/package/ci/travis-desktop.sh b/package/ci/travis-desktop.sh index 812aa68a..6e70cab8 100755 --- a/package/ci/travis-desktop.sh +++ b/package/ci/travis-desktop.sh @@ -30,7 +30,7 @@ cmake .. \ -DWITH_GL=ON \ -DWITH_MESHTOOLS=OFF \ -DWITH_PRIMITIVES=OFF \ - -DWITH_SCENEGRAPH=OFF \ + -DWITH_SCENEGRAPH=ON \ -DWITH_SHADERS=ON \ -DWITH_TEXT=OFF \ -DWITH_TEXTURETOOLS=OFF \ diff --git a/src/python/CMakeLists.txt b/src/python/CMakeLists.txt index 7c5831d2..e51f6089 100644 --- a/src/python/CMakeLists.txt +++ b/src/python/CMakeLists.txt @@ -41,6 +41,7 @@ add_subdirectory(magnum) # configure_file() and then replacing generator expressions with file(GENERATE) foreach(target magnum_gl + magnum_scenegraph magnum_shaders magnum_platform_egl magnum_platform_glx diff --git a/src/python/magnum/CMakeLists.txt b/src/python/magnum/CMakeLists.txt index bbf2a9b3..28f3453f 100644 --- a/src/python/magnum/CMakeLists.txt +++ b/src/python/magnum/CMakeLists.txt @@ -24,7 +24,7 @@ # # *Not* REQUIRED -find_package(Magnum COMPONENTS GL Shaders) +find_package(Magnum COMPONENTS GL Shaders SceneGraph) set(magnum_SRCS magnum.cpp @@ -55,6 +55,19 @@ if(Magnum_GL_FOUND) LIBRARY_OUTPUT_DIRECTORY ${output_dir}/magnum) endif() +if(Magnum_SceneGraph_FOUND) + set(magnum_scenegraph_SRCS + scenegraph.cpp) + + pybind11_add_module(magnum_scenegraph ${magnum_scenegraph_SRCS}) + target_include_directories(magnum_scenegraph PRIVATE ${PROJECT_SOURCE_DIR}/src/python) + target_link_libraries(magnum_scenegraph PRIVATE Magnum::SceneGraph) + set_target_properties(magnum_scenegraph PROPERTIES + FOLDER "python" + OUTPUT_NAME "scenegraph" + LIBRARY_OUTPUT_DIRECTORY ${output_dir}/magnum) +endif() + if(Magnum_Shaders_FOUND) set(magnum_shaders_SRCS shaders.cpp) diff --git a/src/python/magnum/scenegraph.cpp b/src/python/magnum/scenegraph.cpp new file mode 100644 index 00000000..95f3c0a6 --- /dev/null +++ b/src/python/magnum/scenegraph.cpp @@ -0,0 +1,181 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019 + Vladimír Vondruš + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#include +#include +#include +#include +#include + +#include "magnum/bootstrap.h" + +namespace magnum { namespace { + +template class PyObject: public SceneGraph::Object { + public: + PyObject(SceneGraph::Object* parent): SceneGraph::Object{parent} {} + + private: + void doErase() override {} +}; + +template void scene(py::class_>& c) { + c.def(py::init(), "Constructor"); +} + +template void object(py::class_>& c) { + c + .def(py::init([](PyObject* parent) { + return new PyObject{parent}; + }), "Constructor", py::arg("parent") = nullptr) + .def(py::init([](SceneGraph::Scene* parent) { + return new PyObject{parent}; + }), "Constructor", py::arg("parent") = nullptr) + + /* Properties */ + .def_property_readonly("scene", [](PyObject& self) { + return static_cast*>(self.scene()); + }, "Scene or None if the object is not a part of any scene") + .def_property("parent", [](PyObject& self) { + return static_cast*>(self.parent()); + }, [](PyObject& self, PyObject* parent) { + self.setParent(parent); + }, "Parent object or None if this is the root object") + + /* Matrix transformation APIs */ + .def("transformation_matrix", &PyObject::transformationMatrix, + "Transformation matrix") + .def("absolute_transformation_matrix", &PyObject::absoluteTransformationMatrix, + "Transformation matrix relative to the root object") + + /* Transformation APIs common to all implementations */ + .def_property("transformation", + &PyObject::transformation, + &PyObject::setTransformation, + "Object transformation") + .def("absolute_transformation", &PyObject::absoluteTransformation, + "Transformation relative to the root object") + .def("reset_transformation", [](PyObject& self) { + self.resetTransformation(); + }, "Reset the transformation") + .def("transform", [](PyObject& self, const typename Transformation::DataType& transformation) { + self.transform(transformation); + }, "Transform the object") + .def("transform_local", [](PyObject& self, const typename Transformation::DataType& transformation) { + self.transformLocal(transformation); + }, "Transform the object as a local transformation") + .def("translate", [](PyObject& self, const VectorTypeFor& vector) { + self.translate(vector); + }, "Translate the object") + .def("translate_local", [](PyObject& self, const VectorTypeFor& vector) { + self.translateLocal(vector); + }, "Translate the object as a local transformation"); +} + +template void object2D(py::class_>& c) { + c + .def("rotate", [](PyObject& self, const Radd angle) { + self.rotate(static_cast>(angle)); + }, "Rotate the object") + .def("rotate_local", [](PyObject& self, const Radd angle) { + self.rotateLocal(static_cast>(angle)); + }, "Rotate the object as a local transformation"); +} + +template void object3D(py::class_>& c) { + c + .def("rotate", [](PyObject& self, const Radd angle, const Math::Vector3& normalizedAxis) { + self.rotate(static_cast>(angle), normalizedAxis); + }, "Rotate the object as a local transformation", py::arg("angle"), py::arg("normalized_axis")) + .def("rotate_local", [](PyObject& self, const Radd angle, const Math::Vector3& normalizedAxis) { + self.rotateLocal(static_cast>(angle), normalizedAxis); + }, "Rotate the object as a local transformation", py::arg("angle"), py::arg("normalized_axis")) + .def("rotateX", [](PyObject& self, const Radd angle) { + self.rotateX(static_cast>(angle)); + }, "Rotate the object around X axis") + .def("rotate_x_local", [](PyObject& self, const Radd angle) { + self.rotateXLocal(static_cast>(angle)); + }, "Rotate the object around X axis as a local transformation") + .def("rotate_y", [](PyObject& self, const Radd angle) { + self.rotateY(static_cast>(angle)); + }, "Rotate the object around Y axis") + .def("rotate_y_local", [](PyObject& self, const Radd angle) { + self.rotateYLocal(static_cast>(angle)); + }, "Rotate the object around Y axis as a local transformation") + .def("rotate_z", [](PyObject& self, const Radd angle) { + self.rotateZ(static_cast>(angle)); + }, "Rotate the object around Z axis") + .def("rotate_z_local", [](PyObject& self, const Radd angle) { + self.rotateZLocal(static_cast>(angle)); + }, "Rotate the object around Z axis as a local transformation"); +} + +template void objectScale(py::class_>& c) { + c + .def("scale", [](PyObject& self, const VectorTypeFor& vector) { + self.scale(vector); + }, "Scale the object") + .def("scale_local", [](PyObject& self, const VectorTypeFor& vector) { + self.scaleLocal(vector); + }, "Scale the object as a local transformation") + .def("reflect", [](PyObject& self, const VectorTypeFor& vector) { + self.reflect(vector); + }, "Reflect the object") + .def("reflect_local", [](PyObject& self, const VectorTypeFor& vector) { + self.reflectLocal(vector); + }, "Reflect the object as a local transformation"); +} + +void scenegraph(py::module& m) { + { + py::module matrix = m.def_submodule("matrix"); + matrix.doc() = "General matrix-based scene graph implementation"; + + py::class_> scene2D_{matrix, "Scene2D", "Two-dimensional scene with matrix-based transformation implementation"}; + scene(scene2D_); + + py::class_> scene3D_{matrix, "Scene3D", "Three-dimensional scene with matrix-based transformation implementation"}; + scene(scene3D_); + + py::class_> object2D_{matrix, "Object2D", "Two-dimensional object with matrix-based transformation implementation"}; + object<2>(object2D_); + object2D(object2D_); + objectScale<2>(object2D_); + + py::class_> object3D_{matrix, "Object3D", "Three-dimensional object with matrix-based transformation implementation"}; + object<3>(object3D_); + object3D(object3D_); + objectScale<3>(object3D_); + } +} + +}} + +PYBIND11_MODULE(scenegraph, m) { + m.doc() = "Scene graph library"; + + magnum::scenegraph(m); +} + diff --git a/src/python/magnum/test/test_scenegraph.py b/src/python/magnum/test/test_scenegraph.py new file mode 100644 index 00000000..2630dcb4 --- /dev/null +++ b/src/python/magnum/test/test_scenegraph.py @@ -0,0 +1,57 @@ +# +# This file is part of Magnum. +# +# Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019 +# Vladimír Vondruš +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. +# + +import unittest + +from magnum import * +from magnum import scenegraph +from magnum.scenegraph.matrix import Object3D, Scene3D + +class Object(unittest.TestCase): + def test_hierarchy(self): + scene = Scene3D() + a = Object3D() + self.assertIs(a.scene, None) + + b = Object3D(parent=scene) + self.assertIs(b.scene, scene) + self.assertIs(b.parent, scene) + + c = Object3D(parent=b) + self.assertIs(c.scene, scene) + self.assertIs(c.parent, b) + + def test_transformation(self): + scene = Scene3D() + + a = Object3D(scene) + self.assertEqual(a.transformation, Matrix4.identity_init()) + self.assertEqual(a.absolute_transformation(), Matrix4.identity_init()) + + b = Object3D(a) + a.translate((3.0, 4.0, 5.0)) + self.assertEqual(a.transformation, Matrix4.translation([3.0, 4.0, 5.0])) + self.assertEqual(b.transformation, Matrix4.identity_init()) + self.assertEqual(b.absolute_transformation(), Matrix4.translation([3.0, 4.0, 5.0])) diff --git a/src/python/setup.py.cmake b/src/python/setup.py.cmake index 138a2896..ed7ee5e6 100644 --- a/src/python/setup.py.cmake +++ b/src/python/setup.py.cmake @@ -34,6 +34,7 @@ extension_paths = { 'corrade.containers': '$', 'magnum._magnum': '$', 'magnum.gl': '${magnum_gl_file}', + 'magnum.scenegraph': '${magnum_scenegraph_file}', 'magnum.shaders': '${magnum_shaders_file}', 'magnum.platform.egl': '${magnum_platform_egl_file}', 'magnum.platform.glx': '${magnum_platform_glx_file}',