diff --git a/doc/python/conf.py b/doc/python/conf.py index 0e63b78..0e9631f 100644 --- a/doc/python/conf.py +++ b/doc/python/conf.py @@ -19,12 +19,13 @@ import magnum.platform.sdl2 import magnum.primitives import magnum.shaders import magnum.scenegraph +import magnum.text import magnum.trade # So the doc see everything # TODO: use just +=, m.css should reorder this on its own corrade.__all__ = ['containers', 'pluginmanager', 'BUILD_STATIC', 'BUILD_MULTITHREADED', 'TARGET_UNIX', 'TARGET_APPLE', 'TARGET_IOS', 'TARGET_IOS_SIMULATOR', 'TARGET_WINDOWS', 'TARGET_WINDOWS_RT', 'TARGET_EMSCRIPTEN', 'TARGET_ANDROID'] -magnum.__all__ = ['math', 'gl', 'meshtools', 'platform', 'primitives', 'shaders', 'scenegraph', 'trade', 'BUILD_STATIC', 'TARGET_GL', 'TARGET_GLES', 'TARGET_GLES2', 'TARGET_WEBGL', 'TARGET_VK'] + magnum.__all__ +magnum.__all__ = ['math', 'gl', 'meshtools', 'platform', 'primitives', 'shaders', 'scenegraph', 'text', 'trade', 'BUILD_STATIC', 'TARGET_GL', 'TARGET_GLES', 'TARGET_GLES2', 'TARGET_WEBGL', 'TARGET_VK'] + magnum.__all__ # hide values of the preprocessor defines to avoid confusion by assigning a # class without __repr__ to them @@ -126,6 +127,7 @@ INPUT_DOCS = [ 'magnum.platform.rst', 'magnum.scenegraph.rst', 'magnum.shaders.rst', + 'magnum.text.rst', 'magnum.trade.rst', ] diff --git a/doc/python/magnum.text.rst b/doc/python/magnum.text.rst new file mode 100644 index 0000000..5488806 --- /dev/null +++ b/doc/python/magnum.text.rst @@ -0,0 +1,69 @@ +.. + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, + 2020, 2021, 2022 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:class:: magnum.text.FontManager + :summary: Manager for :ref:`AbstractFont` plugin instances + + Each plugin returned by :ref:`instantiate()` or :ref:`load_and_instantiate()` + references its owning :ref:`FontManager` through + :ref:`AbstractFont.manager`, ensuring the manager is not deleted before the + plugin instances are. + +.. py:class:: magnum.text.AbstractFont + + Similarly to C++, font plugins are loaded through :ref:`FontManager`: + + .. + >>> from magnum import text + + .. code:: py + + >>> manager = text.FontManager() + >>> font = manager.load_and_instantiate('StbTrueTypeFont') + + Unlike C++, errors in both API usage and file parsing are reported by + raising an exception. See particular function documentation for detailed + behavior. + +.. py:function:: magnum.text.AbstractFont.open_data + :raise RuntimeError: If file opening fails + +.. py:function:: magnum.text.AbstractFont.open_file + :raise RuntimeError: If file opening fails + +.. py:property:: magnum.text.AbstractFont.size + :raise AssertionError: If no file is opened +.. py:property:: magnum.text.AbstractFont.ascent + :raise AssertionError: If no file is opened +.. py:property:: magnum.text.AbstractFont.descent + :raise AssertionError: If no file is opened +.. py:property:: magnum.text.AbstractFont.line_height + :raise AssertionError: If no file is opened +.. py:function:: magnum.text.AbstractFont.glyph_id + :raise AssertionError: If no file is opened +.. py:function:: magnum.text.AbstractFont.glyph_advance + :raise AssertionError: If no file is opened +.. py:function:: magnum.text.AbstractFont.fill_glyph_cache + :raise AssertionError: If no file is opened diff --git a/doc/python/pages/changelog.rst b/doc/python/pages/changelog.rst index 2191f61..ec83b42 100644 --- a/doc/python/pages/changelog.rst +++ b/doc/python/pages/changelog.rst @@ -95,6 +95,7 @@ Changelog :ref:`trade.AbstractSceneConverter` - Exposed :ref:`Color3.red()` and other convenience constructors (see :gh:`mosra/magnum-bindings#12`) +- Exposed the :ref:`text` library - Fixed issues with an in-source build (see :gh:`mosra/magnum-bindings#13`) - All CMake build options are now prefixed with ``MAGNUM_``. For backwards compatibility, unless ``MAGNUM_BUILD_DEPRECATED`` is disabled and unless a diff --git a/src/python/CMakeLists.txt b/src/python/CMakeLists.txt index f831e89..7356df0 100644 --- a/src/python/CMakeLists.txt +++ b/src/python/CMakeLists.txt @@ -71,6 +71,7 @@ foreach(target magnum_platform_glx magnum_platform_glfw magnum_platform_sdl2 + magnum_text magnum_trade) if(TARGET ${target}) set(${target}_file $) diff --git a/src/python/magnum/CMakeLists.txt b/src/python/magnum/CMakeLists.txt index b4d72e9..09e3432 100644 --- a/src/python/magnum/CMakeLists.txt +++ b/src/python/magnum/CMakeLists.txt @@ -34,6 +34,7 @@ find_package(Magnum COMPONENTS Primitives SceneGraph Shaders + Text Trade GlfwApplication @@ -80,6 +81,9 @@ set(magnum_scenegraph_SRCS set(magnum_shaders_SRCS shaders.cpp) +set(magnum_text_SRCS + text.cpp) + set(magnum_trade_SRCS trade.cpp) @@ -137,6 +141,17 @@ if(NOT MAGNUM_BUILD_STATIC) LIBRARY_OUTPUT_DIRECTORY ${output_dir}/magnum) endif() + if(Magnum_Text_FOUND) + pybind11_add_module(magnum_text ${pybind11_add_module_SYSTEM} ${magnum_text_SRCS}) + target_include_directories(magnum_text PRIVATE + ${PROJECT_SOURCE_DIR}/src + ${PROJECT_SOURCE_DIR}/src/python) + target_link_libraries(magnum_text PRIVATE Magnum::Text) + set_target_properties(magnum_text PROPERTIES + OUTPUT_NAME "text" + LIBRARY_OUTPUT_DIRECTORY ${output_dir}/magnum) + endif() + if(Magnum_Trade_FOUND) pybind11_add_module(magnum_trade ${pybind11_add_module_SYSTEM} ${magnum_trade_SRCS}) target_include_directories(magnum_trade PRIVATE @@ -180,6 +195,11 @@ else() list(APPEND magnum_LIBS Magnum::Shaders) endif() + if(Magnum_Text_FOUND) + list(APPEND magnum_SRCS ${magnum_text_SRCS}) + list(APPEND magnum_LIBS Magnum::Text) + endif() + if(Magnum_Trade_FOUND) list(APPEND magnum_SRCS ${magnum_trade_SRCS}) list(APPEND magnum_LIBS Magnum::Trade) diff --git a/src/python/magnum/__init__.py b/src/python/magnum/__init__.py index 399b1ad..dbe3878 100644 --- a/src/python/magnum/__init__.py +++ b/src/python/magnum/__init__.py @@ -35,7 +35,7 @@ sys.modules['magnum.math'] = math # In case Magnum is built statically, the whole core project is put into # _magnum. Then we need to do the same as above but for all modules. -for i in ['gl', 'meshtools', 'platform', 'primitives', 'scenegraph', 'shaders', 'trade']: +for i in ['gl', 'meshtools', 'platform', 'primitives', 'scenegraph', 'shaders', 'text', 'trade']: if i in globals(): sys.modules['magnum.' + i] = globals()[i] # Platform has subpackages diff --git a/src/python/magnum/bootstrap.h b/src/python/magnum/bootstrap.h index 75a96d6..8765409 100644 --- a/src/python/magnum/bootstrap.h +++ b/src/python/magnum/bootstrap.h @@ -74,6 +74,7 @@ void meshtools(py::module_& m); void primitives(py::module_& m); void scenegraph(py::module_& m); void shaders(py::module_& m); +void text(py::module_& m); void trade(py::module_& m); namespace platform { diff --git a/src/python/magnum/magnum.cpp b/src/python/magnum/magnum.cpp index 4508d85..8b7e86e 100644 --- a/src/python/magnum/magnum.cpp +++ b/src/python/magnum/magnum.cpp @@ -367,6 +367,11 @@ PYBIND11_MODULE(_magnum, m) { magnum::scenegraph(scenegraph); #endif + #ifdef Magnum_Text_FOUND + py::module_ text = m.def_submodule("text"); + magnum::text(text); + #endif + #ifdef Magnum_Trade_FOUND py::module_ trade = m.def_submodule("trade"); magnum::trade(trade); diff --git a/src/python/magnum/staticconfigure.h.cmake b/src/python/magnum/staticconfigure.h.cmake index 44142e5..4642636 100644 --- a/src/python/magnum/staticconfigure.h.cmake +++ b/src/python/magnum/staticconfigure.h.cmake @@ -31,6 +31,7 @@ #cmakedefine Magnum_Primitives_FOUND #cmakedefine Magnum_SceneGraph_FOUND #cmakedefine Magnum_Shaders_FOUND +#cmakedefine Magnum_Text_FOUND #cmakedefine Magnum_Trade_FOUND #cmakedefine Magnum_GlfwApplication_FOUND diff --git a/src/python/magnum/test/Oxygen.ttf b/src/python/magnum/test/Oxygen.ttf new file mode 100644 index 0000000..9737cec Binary files /dev/null and b/src/python/magnum/test/Oxygen.ttf differ diff --git a/src/python/magnum/test/test_text.py b/src/python/magnum/test/test_text.py new file mode 100644 index 0000000..ef6b4c0 --- /dev/null +++ b/src/python/magnum/test/test_text.py @@ -0,0 +1,104 @@ +# +# This file is part of Magnum. +# +# Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, +# 2020, 2021, 2022 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 os +import sys +import unittest + +from corrade import pluginmanager +from magnum import * +from magnum import text + +class Font(unittest.TestCase): + def test(self): + manager = text.FontManager() + self.assertIn('StbTrueTypeFont', manager.alias_list) + self.assertEqual(manager.load_state('StbTrueTypeFont'), pluginmanager.LoadState.NOT_LOADED) + + self.assertTrue(manager.load('StbTrueTypeFont') & pluginmanager.LoadState.LOADED) + self.assertEqual(manager.unload('StbTrueTypeFont'), pluginmanager.LoadState.NOT_LOADED) + + with self.assertRaisesRegex(RuntimeError, "can't load plugin"): + manager.load('NonexistentFont') + with self.assertRaisesRegex(RuntimeError, "can't unload plugin"): + manager.unload('NonexistentFont') + + def test_no_file_opened(self): + font = text.FontManager().load_and_instantiate('StbTrueTypeFont') + self.assertFalse(font.is_opened) + + with self.assertRaisesRegex(AssertionError, "no file opened"): + font.size + with self.assertRaisesRegex(AssertionError, "no file opened"): + font.ascent + with self.assertRaisesRegex(AssertionError, "no file opened"): + font.descent + with self.assertRaisesRegex(AssertionError, "no file opened"): + font.line_height + with self.assertRaisesRegex(AssertionError, "no file opened"): + font.glyph_id('A') + with self.assertRaisesRegex(AssertionError, "no file opened"): + font.glyph_advance(0) + # fill_glyph_cache() not tested as it needs a GL context; assuming it's + # correct + + def test_open_failed(self): + font = text.FontManager().load_and_instantiate('StbTrueTypeFont') + + with self.assertRaisesRegex(RuntimeError, "opening nonexistent.ttf failed"): + font.open_file('nonexistent.ttf', 16.0) + with self.assertRaisesRegex(RuntimeError, "opening data failed"): + font.open_data(b'', 16.0) + + def test_open(self): + manager = text.FontManager() + manager_refcount = sys.getrefcount(manager) + + # Font references the manager to ensure it doesn't get GC'd before the + # plugin instances + font = manager.load_and_instantiate('StbTrueTypeFont') + self.assertIs(font.manager, manager) + self.assertEqual(sys.getrefcount(manager), manager_refcount + 1) + + font.open_file(os.path.join(os.path.dirname(__file__), 'Oxygen.ttf'), 16.0) + self.assertTrue(font.is_opened) + self.assertEqual(font.size, 16.0) + self.assertEqual(font.ascent, 17.011186599731445) + self.assertEqual(font.descent, -4.322147846221924) + self.assertEqual(font.line_height, 21.33333396911621) + self.assertEqual(font.glyph_id('A'), 36) + self.assertEqual(font.glyph_advance(36), (11.7136, 0.0)) + + # Deleting the font should decrease manager refcount again + del font + self.assertEqual(sys.getrefcount(manager), manager_refcount) + + def test_open_data(self): + font = text.FontManager().load_and_instantiate('StbTrueTypeFont') + + with open(os.path.join(os.path.dirname(__file__), 'Oxygen.ttf'), 'rb') as f: + font.open_data(f.read(), 16.0) + + self.assertEqual(font.size, 16.0) diff --git a/src/python/magnum/test/test_text_gl.py b/src/python/magnum/test/test_text_gl.py new file mode 100644 index 0000000..f8d9288 --- /dev/null +++ b/src/python/magnum/test/test_text_gl.py @@ -0,0 +1,75 @@ +# +# This file is part of Magnum. +# +# Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, +# 2020, 2021, 2022 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 os +import sys +import unittest + +# setUpModule gets called before everything else, skipping if GL tests can't +# be run +from . import GLTestCase, setUpModule + +from corrade import pluginmanager +from magnum import * +from magnum import gl, text + +class GlyphCache(GLTestCase): + def test(self): + cache = text.GlyphCache((128, 128), (2, 2)) + + self.assertEqual(cache.texture_size, (128, 128)) + self.assertEqual(cache.padding, (2, 2)) + + cache_refcount = sys.getrefcount(cache) + + # Returned texture references the cache to ensure it doesn't get GC'd + # before the texture instances + texture = cache.texture + self.assertEqual(sys.getrefcount(cache), cache_refcount + 1) + + # Deleting the texture should decrease cache refcount again + del texture + self.assertEqual(sys.getrefcount(cache), cache_refcount) + +class DistanceFieldGlyphCache(GLTestCase): + def test(self): + cache = text.DistanceFieldGlyphCache((1024, 1024), (128, 128), 2) + + self.assertEqual(cache.texture_size, (1024, 1024)) + self.assertEqual(cache.padding, (2, 2)) + +class Renderer2D(GLTestCase): + def test(self): + font = text.FontManager().load_and_instantiate('StbTrueTypeFont') + font.open_file(os.path.join(os.path.dirname(__file__), 'Oxygen.ttf'), 16.0) + + cache = text.GlyphCache((128, 128)) + font.fill_glyph_cache(cache, "hello") + + renderer = text.Renderer2D(font, cache, 1.0) + renderer.reserve(16) + renderer.render("hello") + + self.assertEqual(renderer.rectangle, Range2D((0.0625, -0.0625), (2.4807, 0.875))) diff --git a/src/python/magnum/text.cpp b/src/python/magnum/text.cpp new file mode 100644 index 0000000..8d3b8a8 --- /dev/null +++ b/src/python/magnum/text.cpp @@ -0,0 +1,181 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, + 2020, 2021, 2022 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 /** @todo drop once we have our string casters */ +#include +#include +#include +#include + +#include "corrade/pluginmanager.h" +#include "magnum/bootstrap.h" + +namespace magnum { + +namespace { + +/* For some reason having ...Args as the second (and not last) template + argument does not work. So I'm listing all variants used more than once. */ +template R checkOpened(Text::AbstractFont& self) { + if(!self.isOpened()) { + PyErr_SetString(PyExc_AssertionError, "no file opened"); + throw py::error_already_set{}; + } + return (self.*f)(); +} +template R checkOpened(Text::AbstractFont& self, Arg1 arg1) { + if(!self.isOpened()) { + PyErr_SetString(PyExc_AssertionError, "no file opened"); + throw py::error_already_set{}; + } + return (self.*f)(arg1); +} + +} + +void text(py::module_& m) { + m.doc() = "Text rendering"; + + /* AbstractFont depends on this */ + py::module_::import("corrade.pluginmanager"); + + #ifndef MAGNUM_BUILD_STATIC + /* These are a part of the same module in the static build, no need to + import (also can't import because there it's _magnum.*) */ + py::module_::import("magnum.gl"); + #endif + + /* Glyph caches */ + py::class_ abstractGlyphCache{m, "AbstractGlyphCache", "Base for glyph caches"}; + abstractGlyphCache + /** @todo features */ + .def_property_readonly("texture_size", &Text::AbstractGlyphCache::textureSize, "Glyph cache texture size") + .def_property_readonly("padding", &Text::AbstractGlyphCache::padding, "Glyph padding") + /** @todo glyph iteration and population */ + ; + py::class_ glyphCache{m, "GlyphCache", "Glyph cache"}; + glyphCache + .def(py::init(), "Constructor", py::arg("internal_format"), py::arg("original_size"), py::arg("size"), py::arg("padding")) + .def(py::init(), "Constructor", py::arg("internal_format"), py::arg("size"), py::arg("padding") = Vector2i{}) + .def(py::init(), "Constructor", py::arg("original_size"), py::arg("size"), py::arg("padding")) + .def(py::init(), "Constructor", py::arg("size"), py::arg("padding") = Vector2i{}) + /* The default behavior when returning a reference seems to be that it + increfs the originating instance and decrefs it again after the + variable gets deleted. This is verified in test_text_gl.py to be + extra sure. */ + .def_property_readonly("texture", &Text::GlyphCache::texture, "Cache texture"); + py::class_ distanceFieldGlyphCache{m, "DistanceFieldGlyphCache", "Glyph cache with distance field rendering"}; + distanceFieldGlyphCache + .def(py::init(), "Constructor", py::arg("original_size"), py::arg("size"), py::arg("radius")) + /** @todo setDistanceFieldImage, once needed for anything */ + ; + + /* Font */ + py::class_> abstractFont{m, "AbstractFont", "Interface for font plugins"}; + abstractFont + /** @todo features */ + .def_property_readonly("is_opened", &Text::AbstractFont::isOpened, "Whether any file is opened") + .def("open_data", [](Text::AbstractFont& self, Containers::ArrayView data, Float size) { + /** @todo log redirection -- but we'd need assertions to not be + part of that so when it dies, the user can still see why */ + if(self.openData(data, size)) return; + + PyErr_SetString(PyExc_RuntimeError, "opening data failed"); + throw py::error_already_set{}; + }, "Open raw data", py::arg("data"), py::arg("size")) + .def("open_file", [](Text::AbstractFont& self, const std::string& filename, Float size) { + /** @todo log redirection -- but we'd need assertions to not be + part of that so when it dies, the user can still see why */ + if(self.openFile(filename, size)) return; + + PyErr_Format(PyExc_RuntimeError, "opening %s failed", filename.data()); + throw py::error_already_set{}; + }, "Open a file", py::arg("filename"), py::arg("size")) + .def("close", &Text::AbstractFont::close, "Close currently opened file") + + .def_property_readonly("size", checkOpened, "Font size") + .def_property_readonly("ascent", checkOpened, "Font ascent") + .def_property_readonly("descent", checkOpened, "Font descent") + .def_property_readonly("line_height", checkOpened, "Line height") + .def("glyph_id", checkOpened, "Glyph ID for given character", py::arg("character")) + .def("glyph_advance", checkOpened, "Glyph advance", py::arg("glyph")) + .def("fill_glyph_cache", [](Text::AbstractFont& self, Text::AbstractGlyphCache& cache, const std::string& characters) { + if(!self.isOpened()) { + PyErr_SetString(PyExc_AssertionError, "no file opened"); + throw py::error_already_set{}; + } + return self.fillGlyphCache(cache, characters); + }, "Fill glyph cache with given character set", py::arg("cache"), py::arg("characters")) + /** @todo createGlyphCache() */ + /** @todo layout and AbstractLayouter, once needed for anything */ + ; + corrade::plugin(abstractFont); + + py::class_, PluginManager::AbstractManager> fontManager{m, "FontManager", "Manager for font plugins"}; + corrade::manager(fontManager); + + py::enum_{m, "Alignment", "Text rendering alignment"} + .value("LINE_LEFT", Text::Alignment::LineLeft) + .value("LINE_CENTER", Text::Alignment::LineCenter) + .value("LINE_RIGHT", Text::Alignment::LineRight) + .value("MIDDLE_LEFT", Text::Alignment::MiddleLeft) + .value("MIDDLE_CENTER", Text::Alignment::MiddleCenter) + .value("MIDDLE_RIGHT", Text::Alignment::MiddleRight) + .value("TOP_LEFT", Text::Alignment::TopLeft) + .value("TOP_CENTER", Text::Alignment::TopCenter) + .value("TOP_RIGHT", Text::Alignment::TopRight) + .value("LINE_CENTER_INTEGRAL", Text::Alignment::LineCenterIntegral) + .value("MIDDLE_LEFT_INTEGRAL", Text::Alignment::MiddleLeftIntegral) + .value("MIDDLE_CENTER_INTEGRAL", Text::Alignment::MiddleCenterIntegral) + .value("MIDDLE_RIGHT_INTEGRAL", Text::Alignment::MiddleRightIntegral); + + /** @todo any reason to expose a 3D renderer? it isn't any different + currently */ + py::class_ renderer2D{m, "Renderer2D", "2D text renderer"}; + renderer2D + .def(py::init(), "Constructor", py::arg("font"), py::arg("cache"), py::arg("size"), py::arg("alignment") = Text::Alignment::LineLeft) + .def_property_readonly("capacity", &Text::Renderer2D::capacity, "Capacity for rendered glyphs") + .def_property_readonly("rectangle", &Text::Renderer2D::rectangle, "Rectangle spanning the rendered text") + /** @todo are the buffers useful for anything? */ + /* The default behavior when returning a reference seems to be that it + increfs the originating instance and decrefs it again after the + variable gets deleted. This is verified in test_text.py to be extra + sure. */ + .def_property_readonly("mesh", &Text::Renderer2D::mesh, "Mesh") + .def("reserve", &Text::Renderer2D::reserve, "Reserve capacity for renderered glyphs", py::arg("glyph_count"), py::arg("vertex_buffer_usage") = GL::BufferUsage::StaticDraw, py::arg("index_buffer_usage") = GL::BufferUsage::StaticDraw) + .def("render", static_cast(&Text::Renderer2D::render), "Render text", py::arg("text")); +} + +} + +#ifndef MAGNUM_BUILD_STATIC +/* TODO: remove declaration when https://github.com/pybind/pybind11/pull/1863 + is released */ +extern "C" PYBIND11_EXPORT PyObject* PyInit_text(); +PYBIND11_MODULE(text, m) { + magnum::text(m); +} +#endif diff --git a/src/python/setup.py.cmake b/src/python/setup.py.cmake index a1ceb9e..e2cc7dd 100644 --- a/src/python/setup.py.cmake +++ b/src/python/setup.py.cmake @@ -46,6 +46,7 @@ extension_paths = { 'magnum.platform.glx': '${magnum_platform_glx_file}', 'magnum.platform.glfw': '${magnum_platform_glfw_file}', 'magnum.platform.sdl2': '${magnum_platform_sdl2_file}', + 'magnum.text': '${magnum_text_file}', 'magnum.trade': '${magnum_trade_file}', }