From 0eabbebbae6d141fdaaac91291709eb809f2b7d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Mon, 10 Apr 2023 16:53:53 +0200 Subject: [PATCH] python: expose almost all remaining mesh tools operating on a MeshData. --- doc/python/conf.py | 1 + doc/python/magnum.meshtools.rst | 73 ++++++ doc/python/pages/changelog.rst | 10 + src/python/magnum/meshtools.cpp | 196 +++++++++++++++- src/python/magnum/test/mesh-packed.bin | Bin 0 -> 78 bytes src/python/magnum/test/mesh-packed.bin.in | 9 + src/python/magnum/test/mesh-packed.gltf | 104 +++++++++ src/python/magnum/test/test_meshtools.py | 258 ++++++++++++++++++++++ 8 files changed, 650 insertions(+), 1 deletion(-) create mode 100644 doc/python/magnum.meshtools.rst create mode 100644 src/python/magnum/test/mesh-packed.bin create mode 100644 src/python/magnum/test/mesh-packed.bin.in create mode 100644 src/python/magnum/test/mesh-packed.gltf create mode 100644 src/python/magnum/test/test_meshtools.py diff --git a/doc/python/conf.py b/doc/python/conf.py index 589bc12..9677d56 100644 --- a/doc/python/conf.py +++ b/doc/python/conf.py @@ -203,6 +203,7 @@ INPUT_DOCS = [ 'magnum.rst', 'magnum.gl.rst', 'magnum.math.rst', + 'magnum.meshtools.rst', 'magnum.platform.rst', 'magnum.scenegraph.rst', 'magnum.scenetools.rst', diff --git a/doc/python/magnum.meshtools.rst b/doc/python/magnum.meshtools.rst new file mode 100644 index 0000000..a2695ae --- /dev/null +++ b/doc/python/magnum.meshtools.rst @@ -0,0 +1,73 @@ +.. + 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:function:: magnum.meshtools.compress_indices + :raise AssertionError: If :p:`mesh` is not indexed + +.. py:function:: magnum.meshtools.duplicate + :raise AssertionError: If :p:`mesh` is not indexed + +.. py:function:: magnum.meshtools.generate_indices + :raise AssertionError: If :p:`mesh` is not :ref:`MeshPrimitive.LINE_STRIP`, + :ref:`MeshPrimitive.LINE_LOOP`, :ref:`MeshPrimitive.TRIANGLE_STRIP` or + :ref:`MeshPrimitive.TRIANGLE_FAN` + +.. py:function:: magnum.meshtools.transform2d + :raise KeyError: If :p:`mesh` doesn't have + :ref:`trade.MeshAttribute.POSITION` of index :p:`id` + :raise AssertionError: If :ref:`trade.MeshAttribute.POSITION` are not 2D + +.. py:function:: magnum.meshtools.transform2d_in_place + :raise AssertionError: If :p:`mesh` vertex data aren't + :ref:`trade.DataFlags.MUTABLE` + :raise KeyError: If :p:`mesh` doesn't have + :ref:`trade.MeshAttribute.POSITION` of index :p:`id` + :raise AssertionError: If :ref:`trade.MeshAttribute.POSITION` are not + :ref:`VertexFormat.VECTOR2` + +.. py:function:: magnum.meshtools.transform3d + :raise KeyError: If :p:`mesh` doesn't have + :ref:`trade.MeshAttribute.POSITION` of index :p:`id` + :raise AssertionError: If :ref:`trade.MeshAttribute.POSITION` are not 3D + +.. py:function:: magnum.meshtools.transform3d_in_place + :raise AssertionError: If :p:`mesh` vertex data aren't + :ref:`trade.DataFlags.MUTABLE` + :raise KeyError: If :p:`mesh` doesn't have + :ref:`trade.MeshAttribute.POSITION` of index :p:`id` + :raise AssertionError: If :ref:`trade.MeshAttribute.POSITION` are not + :ref:`VertexFormat.VECTOR3` + +.. py:function:: magnum.meshtools.transform_texture_coordinates2d + :raise KeyError: If :p:`mesh` doesn't have + :ref:`trade.MeshAttribute.TEXTURE_COORDINATES` of index :p:`id` + +.. py:function:: magnum.meshtools.transform_texture_coordinates2d_in_place + :raise AssertionError: If :p:`mesh` vertex data aren't + :ref:`trade.DataFlags.MUTABLE` + :raise KeyError: If :p:`mesh` doesn't have + :ref:`trade.MeshAttribute.TEXTURE_COORDINATES` of index :p:`id` + :raise AssertionError: If :ref:`trade.MeshAttribute.TEXTURE_COORDINATES` + are not :ref:`VertexFormat.VECTOR2` diff --git a/doc/python/pages/changelog.rst b/doc/python/pages/changelog.rst index 949d4ef..5630558 100644 --- a/doc/python/pages/changelog.rst +++ b/doc/python/pages/changelog.rst @@ -102,6 +102,16 @@ Changelog - Fixed :ref:`platform.sdl2.Application.InputEvent.Modifier` and :ref:`platform.glfw.Application.InputEvent.Modifier` to behave properly as flags and not just as an enum +- Exposed :ref:`meshtools.compress_indices()`, :ref:`meshtools.duplicate()`, + :ref:`meshtools.filter_except_attributes()`, + :ref:`meshtools.filter_only_attributes()`, + :ref:`meshtools.generate_indices()`, :ref:`meshtools.interleave()`, + :ref:`meshtools.owned()`, :ref:`meshtools.remove_duplicates()`, + :ref:`meshtools.remove_duplicates_fuzzy()`, :ref:`meshtools.transform2d()`, + :ref:`meshtools.transform2d_in_place()`, :ref:`meshtools.transform3d()`, + :ref:`meshtools.transform3d_in_place()`, + :ref:`meshtools.transform_texture_coordinates2d()` and + :ref:`meshtools.transform_texture_coordinates2d_in_place()` - Exposed :ref:`platform.sdl2.Application.viewport_event` and :ref:`platform.glfw.Application.viewport_event` and a possibility to make the window resizable on startup diff --git a/src/python/magnum/meshtools.cpp b/src/python/magnum/meshtools.cpp index 3b7556e..9e52cf4 100644 --- a/src/python/magnum/meshtools.cpp +++ b/src/python/magnum/meshtools.cpp @@ -24,8 +24,19 @@ */ #include +#include /* for std::vector */ +#include +#include #include #include +#include +#include +#include +#include +#include +#include +#include +#include #include #include "corrade/EnumOperators.h" @@ -50,10 +61,193 @@ void meshtools(py::module_& m) { .value("GENERATE_SMOOTH_NORMALS", MeshTools::CompileFlag::GenerateSmoothNormals); corrade::enumOperators(compileFlags); + py::enum_ interleaveFlags{m, "InterleaveFlags", "Interleaving behavior flags"}; + interleaveFlags + .value("NONE", MeshTools::InterleaveFlag{}) + .value("PRESERVE_INTERLEAVED_ATTRIBUTES", MeshTools::InterleaveFlag::PreserveInterleavedAttributes) + .value("PRESERVE_STRIDED_INDICES", MeshTools::InterleaveFlag::PreserveStridedIndices); + corrade::enumOperators(interleaveFlags); + m .def("compile", [](const Trade::MeshData& mesh, MeshTools::CompileFlag flags) { return MeshTools::compile(mesh, flags); - }, "Compile 3D mesh data", py::arg("mesh"), py::arg("flags") = MeshTools::CompileFlag{}); + }, "Compile 3D mesh data", py::arg("mesh"), py::arg("flags") = MeshTools::CompileFlag{}) + .def("compress_indices", [](const Trade::MeshData& mesh, MeshIndexType atLeast) { + if(!mesh.isIndexed()) { + PyErr_SetString(PyExc_AssertionError, "the mesh is not indexed"); + throw py::error_already_set{}; + } + /** @todo check that the indices aren't impl-specific once it's + possible to test */ + + return MeshTools::compressIndices(mesh, atLeast); + }, "Compress mesh data indices", py::arg("mesh"), + #if PYBIND11_VERSION_MAJOR*100 + PYBIND11_VERSION_MINOR >= 206 + py::kw_only{}, /* new in pybind11 2.6 */ + #endif + py::arg("at_least") = MeshIndexType::UnsignedShort) + .def("duplicate", [](const Trade::MeshData& mesh) { + if(!mesh.isIndexed()) { + PyErr_SetString(PyExc_AssertionError, "the mesh is not indexed"); + throw py::error_already_set{}; + } + /** @todo check that the indices aren't impl-specific once it's + possible to test */ + + return MeshTools::duplicate(mesh); + }, "Duplicate indexed mesh data", py::arg("mesh")) + .def("filter_except_attributes", [](const Trade::MeshData& mesh, const std::vector attributes) { + return MeshTools::filterExceptAttributes(mesh, attributes); + }, "Filter a mesh to contain everything except the selected subset of named attributes", py::arg("mesh"), py::arg("attributes")) + .def("filter_only_attributes", [](const Trade::MeshData& mesh, const std::vector attributes) { + return MeshTools::filterOnlyAttributes(mesh, attributes); + }, "Filter a mesh to contain only the selected subset of named attributes", py::arg("mesh"), py::arg("attributes")) + .def("generate_indices", [](const Trade::MeshData& mesh) { + if(mesh.primitive() != MeshPrimitive::LineStrip && + mesh.primitive() != MeshPrimitive::LineLoop && + mesh.primitive() != MeshPrimitive::TriangleStrip && + mesh.primitive() != MeshPrimitive::TriangleFan) + { + PyErr_SetString(PyExc_AssertionError, "invalid mesh primitive"); + throw py::error_already_set{}; + } + /** @todo check that the indices aren't impl-specific once it's + possible to test */ + + return MeshTools::generateIndices(mesh); + }, "Convert a mesh to plain indexed lines or triangles", py::arg("mesh")) + .def("interleave", [](const Trade::MeshData& mesh, MeshTools::InterleaveFlag flags) { + /** @todo check that the vertices/indices aren't impl-specific if + the interleaved preservation is disabled, once it's possible to + test */ + return MeshTools::interleave(mesh, {}, flags); + }, "Interleave mesh data", py::arg("mesh"), py::arg("flags") = MeshTools::InterleaveFlag::PreserveInterleavedAttributes) + .def("owned", static_cast(MeshTools::owned), "Create an owned mesh data", py::arg("mesh")) + /** @todo check that the indices/vertices aren't impl-specific once + it's possible to test */ + .def("remove_duplicates", static_cast(MeshTools::removeDuplicates), "Remove mesh data duplicates", py::arg("mesh")) + /** @todo check that the indices/vertices aren't impl-specific once + it's possible to test */ + .def("remove_duplicates_fuzzy", MeshTools::removeDuplicatesFuzzy, "Remove mesh data duplicates", py::arg("mesh"), + #if PYBIND11_VERSION_MAJOR*100 + PYBIND11_VERSION_MINOR >= 206 + py::kw_only{}, /* new in pybind11 2.6 */ + #endif + py::arg("float_epsilon") = Math::TypeTraits::epsilon(), + py::arg("double_epsilon") = Math::TypeTraits::epsilon()) + .def("transform2d", [](const Trade::MeshData& mesh, const Matrix3& transformation, UnsignedInt id, MeshTools::InterleaveFlag flags) { + const Containers::Optional positionAttributeId = mesh.findAttributeId(Trade::MeshAttribute::Position, id); + if(!positionAttributeId) { + PyErr_SetString(PyExc_KeyError, "position attribute not found"); + throw py::error_already_set{}; + } + if(vertexFormatComponentCount(mesh.attributeFormat(*positionAttributeId)) != 2) { + PyErr_SetString(PyExc_AssertionError, "positions are not 2D"); + throw py::error_already_set{}; + } + /** @todo check that the positions aren't impl-specific once + it's possible to test */ + + return MeshTools::transform2D(mesh, transformation, id, flags); + }, "Transform 2D positions in a mesh data", py::arg("mesh"), py::arg("transformation"), py::arg("id") = 0, py::arg("flags") = MeshTools::InterleaveFlag::PreserveInterleavedAttributes) + .def("transform2d_in_place", [](Trade::MeshData& mesh, const Matrix3& transformation, UnsignedInt id) { + if(!(mesh.vertexDataFlags() & Trade::DataFlag::Mutable)) { + PyErr_SetString(PyExc_AssertionError, "vertex data not mutable"); + throw py::error_already_set{}; + } + + const Containers::Optional positionAttributeId = mesh.findAttributeId(Trade::MeshAttribute::Position, id); + if(!positionAttributeId) { + PyErr_SetString(PyExc_KeyError, "position attribute not found"); + throw py::error_already_set{}; + } + if(mesh.attributeFormat(*positionAttributeId) != VertexFormat::Vector2) { + PyErr_SetString(PyExc_AssertionError, "positions are not VECTOR2"); + throw py::error_already_set{}; + } + + MeshTools::transform2DInPlace(mesh, transformation, id); + }, "Transform 2D positions in a mesh data in-place", py::arg("mesh"), py::arg("transformation"), py::arg("id") = 0) + .def("transform3d", [](const Trade::MeshData& mesh, const Matrix4& transformation, UnsignedInt id, MeshTools::InterleaveFlag flags) { + const Containers::Optional positionAttributeId = mesh.findAttributeId(Trade::MeshAttribute::Position, id); + if(!positionAttributeId) { + PyErr_SetString(PyExc_KeyError, "position attribute not found"); + throw py::error_already_set{}; + } + if(vertexFormatComponentCount(mesh.attributeFormat(*positionAttributeId)) != 3) { + PyErr_SetString(PyExc_AssertionError, "mesh positions are not 3D"); + throw py::error_already_set{}; + } + /** @todo check that the positions, normals, ... aren't + impl-specific once it's possible to test */ + + return MeshTools::transform3D(mesh, transformation, id, flags); + }, "Transform 3D positions, normals, tangents and bitangents in a mesh data", py::arg("mesh"), py::arg("transformation"), py::arg("id") = 0, py::arg("flags") = MeshTools::InterleaveFlag::PreserveInterleavedAttributes) + .def("transform3d_in_place", [](Trade::MeshData& mesh, const Matrix4& transformation, UnsignedInt id) { + if(!(mesh.vertexDataFlags() & Trade::DataFlag::Mutable)) { + PyErr_SetString(PyExc_AssertionError, "vertex data not mutable"); + throw py::error_already_set{}; + } + + const Containers::Optional positionAttributeId = mesh.findAttributeId(Trade::MeshAttribute::Position, id); + if(!positionAttributeId) { + PyErr_SetString(PyExc_KeyError, "position attribute not found"); + throw py::error_already_set{}; + } + if(mesh.attributeFormat(*positionAttributeId) != VertexFormat::Vector3) { + PyErr_SetString(PyExc_AssertionError, "positions are not VECTOR3"); + throw py::error_already_set{}; + } + + const Containers::Optional tangentAttributeId = mesh.findAttributeId(Trade::MeshAttribute::Tangent, id); + const Containers::Optional bitangentAttributeId = mesh.findAttributeId(Trade::MeshAttribute::Bitangent, id); + const Containers::Optional normalAttributeId = mesh.findAttributeId(Trade::MeshAttribute::Normal, id); + if(tangentAttributeId && + (mesh.attributeFormat(*tangentAttributeId) != VertexFormat::Vector3 && + mesh.attributeFormat(*tangentAttributeId) != VertexFormat::Vector4)) + { + PyErr_SetString(PyExc_AssertionError, "tangents are not VECTOR3 or VECTOR4"); + throw py::error_already_set{}; + } + if(bitangentAttributeId && mesh.attributeFormat(*bitangentAttributeId) != VertexFormat::Vector3) { + PyErr_SetString(PyExc_AssertionError, "bitangents are not VECTOR3"); + throw py::error_already_set{}; + } + if(normalAttributeId && mesh.attributeFormat(*normalAttributeId) != VertexFormat::Vector3) { + PyErr_SetString(PyExc_AssertionError, "normals are not VECTOR3"); + throw py::error_already_set{}; + } + + MeshTools::transform3DInPlace(mesh, transformation, id); + }, "Transform 3D position, normals, tangents and bitangents in a mesh data in-place", py::arg("mesh"), py::arg("transformation"), py::arg("id") = 0) + .def("transform_texture_coordinates2d", [](const Trade::MeshData& mesh, const Matrix3& transformation, UnsignedInt id, MeshTools::InterleaveFlag flags) { + const Containers::Optional textureCoordinateAttributeId = mesh.findAttributeId(Trade::MeshAttribute::TextureCoordinates, id); + if(!textureCoordinateAttributeId) { + PyErr_SetString(PyExc_KeyError, "texture coordinates attribute not found"); + throw py::error_already_set{}; + } + /** @todo check that the texture coordinates aren't impl-specific + once it's possible to test */ + + return MeshTools::transformTextureCoordinates2D(mesh, transformation, id, flags); + }, "Transform 2D texture coordinates in a mesh data", py::arg("mesh"), py::arg("transformation"), py::arg("id") = 0, py::arg("flags") = MeshTools::InterleaveFlag::PreserveInterleavedAttributes) + .def("transform_texture_coordinates2d_in_place", [](Trade::MeshData& mesh, const Matrix3& transformation, UnsignedInt id) { + if(!(mesh.vertexDataFlags() & Trade::DataFlag::Mutable)) { + PyErr_SetString(PyExc_AssertionError, "vertex data not mutable"); + throw py::error_already_set{}; + } + + const Containers::Optional textureCoordinateAttributeId = mesh.findAttributeId(Trade::MeshAttribute::TextureCoordinates, id); + if(!textureCoordinateAttributeId) { + PyErr_SetString(PyExc_KeyError, "texture coordinates attribute not found"); + throw py::error_already_set{}; + } + if(mesh.attributeFormat(*textureCoordinateAttributeId) != VertexFormat::Vector2) { + PyErr_SetString(PyExc_AssertionError, "texture coordinates are not VECTOR2"); + throw py::error_already_set{}; + } + + MeshTools::transformTextureCoordinates2DInPlace(mesh, transformation, id); + }, "Transform 2D texture coordinates in a mesh data in-place", py::arg("mesh"), py::arg("transformation"), py::arg("id") = 0); } } diff --git a/src/python/magnum/test/mesh-packed.bin b/src/python/magnum/test/mesh-packed.bin new file mode 100644 index 0000000000000000000000000000000000000000..e002277f66a2cbe8b36cc4d2137431be4336147d GIT binary patch literal 78 zcmXwu!4ZHU5Cc~Pk)IuLRK}G;DV9Pfl5vJi^4JY{1x&Srz{%Ief81bbuSF7<-2SF4 M4W5>{DFVe|0hUY&djJ3c literal 0 HcmV?d00001 diff --git a/src/python/magnum/test/mesh-packed.bin.in b/src/python/magnum/test/mesh-packed.bin.in new file mode 100644 index 0000000..f11b811 --- /dev/null +++ b/src/python/magnum/test/mesh-packed.bin.in @@ -0,0 +1,9 @@ +type = '<3f3H4h 3f3H4h 3f3H4h' +input = [ + # float positions, 16-bit positions, 16-bit normals/tangents/texcoords + 1.0, 2.0, 3.0, 1, 2, 3, 32767, 0, 0, 0, + 4.0, 5.0, 6.0, 4, 5, 6, 0, 32767, 0, 0, + 7.0, 8.0, 9.0, 7, 8, 9, 0, 0, -32768, 0 +] + +# kate: hl python diff --git a/src/python/magnum/test/mesh-packed.gltf b/src/python/magnum/test/mesh-packed.gltf new file mode 100644 index 0000000..253f3df --- /dev/null +++ b/src/python/magnum/test/mesh-packed.gltf @@ -0,0 +1,104 @@ +{ + "asset": { + "version": "2.0" + }, + "meshes": [ + { + "name": "packed positions", + "primitives": [ + { + "attributes": { + "POSITION": 1 + } + } + ] + }, + { + "name": "packed normals", + "primitives": [ + { + "attributes": { + "POSITION": 0, + "NORMAL": 2 + } + } + ] + }, + { + "name": "packed tangents", + "primitives": [ + { + "attributes": { + "POSITION": 0, + "TANGENT": 3 + } + } + ] + }, + { + "name": "packed texcoords", + "primitives": [ + { + "attributes": { + "POSITION": 0, + "TEXCOORD_0": 4 + } + } + ] + } + ], + "accessors": [ + { + "bufferView": 0, + "componentType": 5126, + "count": 3, + "type": "VEC3" + }, + { + "bufferView": 0, + "byteOffset": 12, + "componentType": 5123, + "count": 3, + "type": "VEC3" + }, + { + "bufferView": 0, + "byteOffset": 18, + "componentType": 5122, + "normalized": true, + "count": 3, + "type": "VEC3" + }, + { + "bufferView": 0, + "byteOffset": 18, + "componentType": 5122, + "normalized": true, + "count": 3, + "type": "VEC4" + }, + { + "bufferView": 0, + "byteOffset": 18, + "componentType": 5123, + "normalized": true, + "count": 3, + "type": "VEC2" + } + ], + "bufferViews": [ + { + "buffer": 0, + "byteOffset": 0, + "byteLength": 78, + "byteStride": 26 + } + ], + "buffers": [ + { + "byteLength": 78, + "uri": "mesh-packed.bin" + } + ] +} + diff --git a/src/python/magnum/test/test_meshtools.py b/src/python/magnum/test/test_meshtools.py new file mode 100644 index 0000000..e56a56b --- /dev/null +++ b/src/python/magnum/test/test_meshtools.py @@ -0,0 +1,258 @@ +# +# 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 unittest + +from magnum import * +from magnum import meshtools, primitives, trade + +class CompressIndices(unittest.TestCase): + def test(self): + mesh = primitives.cube_solid() + self.assertTrue(mesh.is_indexed) + self.assertEqual(mesh.index_type, MeshIndexType.UNSIGNED_SHORT) + + compressed = meshtools.compress_indices(mesh, at_least=MeshIndexType.UNSIGNED_BYTE) + self.assertEqual(compressed.index_type, MeshIndexType.UNSIGNED_BYTE) + + def test_not_indexed(self): + mesh = primitives.line2d() + self.assertFalse(mesh.is_indexed) + + with self.assertRaisesRegex(AssertionError, "the mesh is not indexed"): + meshtools.compress_indices(mesh) + +class Duplicate(unittest.TestCase): + def test(self): + mesh = primitives.cube_solid() + self.assertTrue(mesh.is_indexed) + + duplicated = meshtools.duplicate(mesh) + self.assertFalse(duplicated.is_indexed) + + def test_not_indexed(self): + mesh = primitives.line2d() + self.assertFalse(mesh.is_indexed) + + with self.assertRaisesRegex(AssertionError, "the mesh is not indexed"): + meshtools.duplicate(mesh) + +class GenerateIndices(unittest.TestCase): + def test(self): + mesh = primitives.cube_solid_strip() + self.assertFalse(mesh.is_indexed) + self.assertEqual(mesh.primitive, MeshPrimitive.TRIANGLE_STRIP) + + indexed = meshtools.generate_indices(mesh) + self.assertTrue(indexed.is_indexed) + self.assertEqual(indexed.primitive, MeshPrimitive.TRIANGLES) + + def test_invalid_primitive(self): + mesh = primitives.cube_solid() + self.assertEqual(mesh.primitive, MeshPrimitive.TRIANGLES) + + with self.assertRaisesRegex(AssertionError, "invalid mesh primitive"): + meshtools.generate_indices(mesh) + +class FilterAttributes(unittest.TestCase): + def test_only(self): + mesh = primitives.cube_solid() + self.assertEqual(mesh.attribute_count(), 2) + self.assertTrue(mesh.has_attribute(trade.MeshAttribute.NORMAL)) + + # Currently it doesn't blow up if unknown attributes are listed + filtered = meshtools.filter_only_attributes(mesh, [trade.MeshAttribute.TEXTURE_COORDINATES, trade.MeshAttribute.NORMAL]) + self.assertEqual(filtered.attribute_count(), 1) + self.assertTrue(filtered.has_attribute(trade.MeshAttribute.NORMAL)) + + def test_except(self): + mesh = primitives.cube_solid() + self.assertEqual(mesh.attribute_count(), 2) + self.assertTrue(mesh.has_attribute(trade.MeshAttribute.NORMAL)) + + # Currently it doesn't blow up if unknown attributes are listed + filtered = meshtools.filter_except_attributes(mesh, [trade.MeshAttribute.TEXTURE_COORDINATES, trade.MeshAttribute.NORMAL]) + self.assertEqual(filtered.attribute_count(), 1) + self.assertFalse(filtered.has_attribute(trade.MeshAttribute.NORMAL)) + +class Interleave(unittest.TestCase): + def test(self): + mesh = meshtools.filter_except_attributes(primitives.circle3d_solid(3, primitives.Circle3DFlags.TEXTURE_COORDINATES), [trade.MeshAttribute.NORMAL]) + # Position + gap after normals + texture coordinates + self.assertEqual(mesh.attribute_count(), 2) + self.assertEqual(mesh.attribute_stride(trade.MeshAttribute.POSITION), 12 + 12 + 8) + + interleaved = meshtools.interleave(mesh) + self.assertEqual(interleaved.attribute_count(), 2) + # Gap after normals not removed + self.assertEqual(interleaved.attribute_stride(trade.MeshAttribute.POSITION), 12 + 12 + 8) + + interleaved_packed = meshtools.interleave(mesh, meshtools.InterleaveFlags.NONE) + self.assertEqual(interleaved_packed.attribute_count(), 2) + # Gap after normals removed + self.assertEqual(interleaved_packed.attribute_stride(trade.MeshAttribute.POSITION), 12 + 8) + +class Owned(unittest.TestCase): + def test(self): + mesh = primitives.square_solid() + self.assertEqual(mesh.vertex_data_flags, trade.DataFlags.NONE) + + owned = meshtools.owned(mesh) + self.assertEqual(owned.vertex_data_flags, trade.DataFlags.OWNED|trade.DataFlags.MUTABLE) + +class RemoveDuplicates(unittest.TestCase): + def test(self): + mesh = meshtools.duplicate(primitives.cube_solid()) + self.assertFalse(mesh.is_indexed) + self.assertEqual(mesh.vertex_count, 36) + + deduplicated = meshtools.remove_duplicates(mesh) + self.assertTrue(deduplicated.is_indexed) + self.assertEqual(deduplicated.vertex_count, 24) + + def test_fuzzy(self): + mesh = meshtools.duplicate(primitives.cube_solid()) + self.assertFalse(mesh.is_indexed) + self.assertEqual(mesh.vertex_count, 36) + + deduplicated = meshtools.remove_duplicates_fuzzy(mesh) + self.assertTrue(deduplicated.is_indexed) + self.assertEqual(deduplicated.vertex_count, 24) + + # Haha + single_point = meshtools.remove_duplicates_fuzzy(mesh, float_epsilon=1e6) + self.assertEqual(single_point.vertex_count, 1) + +class Transform(unittest.TestCase): + def test_2d(self): + mesh = primitives.line2d() + self.assertEqual(mesh.attribute(trade.MeshAttribute.POSITION)[0], (0.0, 0.0)) + + transformed = meshtools.transform2d(mesh, Matrix3.translation(Vector2.x_axis(100.0))) + self.assertEqual(transformed.attribute(trade.MeshAttribute.POSITION)[0], (100.0, 0.0)) + + def test_2d_in_place(self): + mesh = primitives.line2d() + self.assertEqual(mesh.attribute(trade.MeshAttribute.POSITION)[0], (0.0, 0.0)) + + meshtools.transform2d_in_place(mesh, Matrix3.translation(Vector2.x_axis(100.0))) + self.assertEqual(mesh.attribute(trade.MeshAttribute.POSITION)[0], (100.0, 0.0)) + + def test_3d(self): + mesh = primitives.line3d() + self.assertEqual(mesh.attribute(trade.MeshAttribute.POSITION)[0], (0.0, 0.0, 0.0)) + + transformed = meshtools.transform3d(mesh, Matrix4.translation(Vector3.x_axis(100.0))) + self.assertEqual(transformed.attribute(trade.MeshAttribute.POSITION)[0], (100.0, 0.0, 0.0)) + + def test_3d_in_place(self): + mesh = primitives.line3d() + self.assertEqual(mesh.attribute(trade.MeshAttribute.POSITION)[0], (0.0, 0.0, 0.0)) + + meshtools.transform3d_in_place(mesh, Matrix4.translation(Vector3.x_axis(100.0))) + self.assertEqual(mesh.attribute(trade.MeshAttribute.POSITION)[0], (100.0, 0.0, 0.0)) + + def test_texture_coordinates2d(self): + mesh = primitives.square_solid(primitives.SquareFlags.TEXTURE_COORDINATES) + self.assertEqual(mesh.attribute(trade.MeshAttribute.TEXTURE_COORDINATES)[0], (1.0, 0.0)) + + transformed = meshtools.transform_texture_coordinates2d(mesh, Matrix3.translation(Vector2.x_axis(100.0))) + self.assertEqual(transformed.attribute(trade.MeshAttribute.TEXTURE_COORDINATES)[0], (101.0, 0.0)) + + def test_texture_coordinates2d_in_place(self): + mesh = meshtools.owned(primitives.square_solid(primitives.SquareFlags.TEXTURE_COORDINATES)) + self.assertEqual(mesh.attribute(trade.MeshAttribute.TEXTURE_COORDINATES)[0], (1.0, 0.0)) + + meshtools.transform_texture_coordinates2d_in_place(mesh, Matrix3.translation(Vector2.x_axis(100.0))) + self.assertEqual(mesh.attribute(trade.MeshAttribute.TEXTURE_COORDINATES)[0], (101.0, 0.0)) + + def test_no_attribute(self): + mesh = meshtools.owned(primitives.square_solid(primitives.SquareFlags.TEXTURE_COORDINATES)) + + with self.assertRaisesRegex(KeyError, "position attribute not found"): + meshtools.transform2d(mesh, Matrix3(), 1) + with self.assertRaisesRegex(KeyError, "position attribute not found"): + meshtools.transform2d_in_place(mesh, Matrix3(), 1) + with self.assertRaisesRegex(KeyError, "position attribute not found"): + meshtools.transform3d(mesh, Matrix4(), 1) + with self.assertRaisesRegex(KeyError, "position attribute not found"): + meshtools.transform3d_in_place(mesh, Matrix4(), 1) + with self.assertRaisesRegex(KeyError, "texture coordinates attribute not found"): + meshtools.transform_texture_coordinates2d(mesh, Matrix3(), 1) + with self.assertRaisesRegex(KeyError, "texture coordinates attribute not found"): + meshtools.transform_texture_coordinates2d_in_place(mesh, Matrix3(), 1) + + def test_not_2d_not_3d(self): + mesh2d = primitives.line2d() + mesh3d = primitives.line3d() + + with self.assertRaisesRegex(AssertionError, "positions are not 2D"): + meshtools.transform2d(mesh3d, Matrix3()) + with self.assertRaisesRegex(AssertionError, "positions are not VECTOR2"): + meshtools.transform2d_in_place(mesh3d, Matrix3()) + with self.assertRaisesRegex(AssertionError, "positions are not 3D"): + meshtools.transform3d(mesh2d, Matrix4()) + with self.assertRaisesRegex(AssertionError, "positions are not VECTOR3"): + meshtools.transform3d_in_place(mesh2d, Matrix4()) + + def test_not_float(self): + importer = trade.ImporterManager().load_and_instantiate('GltfImporter') + importer.open_file(os.path.join(os.path.dirname(__file__), 'mesh-packed.gltf')) + + # Non-in-place should work + # TODO test 2D positions + # TODO test bitangents and three-component tangents once there's a tool + # to convert from/to four-component (don't want to use Assimp) + packed_positions = meshtools.transform3d(importer.mesh('packed positions'), Matrix4.rotation_x(Deg(90.0))) + packed_normals = meshtools.transform3d(importer.mesh('packed normals'), Matrix4.rotation_x(Deg(90.0))) + packed_tangents = meshtools.transform3d(importer.mesh('packed tangents'), Matrix4.rotation_x(Deg(90.0))) + packed_texcoords = meshtools.transform_texture_coordinates2d(importer.mesh('packed texcoords'), Matrix3.rotation(Deg(90.0))) + self.assertEqual(packed_positions.attribute(trade.MeshAttribute.POSITION)[1], (4.0, -6.0, 5.0)) + self.assertEqual(packed_normals.attribute(trade.MeshAttribute.NORMAL)[1], (0.0, 0.0, 1.0)) + self.assertEqual(packed_tangents.attribute(trade.MeshAttribute.TANGENT)[1], (0.0, 0.0, 1.0, 0.0)) + self.assertEqual(packed_texcoords.attribute(trade.MeshAttribute.TEXTURE_COORDINATES)[1], (-0.5, 0.0)) + + # TODO test 2D position with something that's actually 2D + with self.assertRaisesRegex(AssertionError, "positions are not VECTOR2"): + meshtools.transform2d_in_place(importer.mesh('packed positions'), Matrix3()) + with self.assertRaisesRegex(AssertionError, "positions are not VECTOR3"): + meshtools.transform3d_in_place(importer.mesh('packed positions'), Matrix4()) + with self.assertRaisesRegex(AssertionError, "normals are not VECTOR3"): + meshtools.transform3d_in_place(importer.mesh('packed normals'), Matrix4()) + with self.assertRaisesRegex(AssertionError, "tangents are not VECTOR3 or VECTOR4"): + meshtools.transform3d_in_place(importer.mesh('packed tangents'), Matrix4()) + with self.assertRaisesRegex(AssertionError, "texture coordinates are not VECTOR2"): + meshtools.transform_texture_coordinates2d_in_place(importer.mesh('packed texcoords'), Matrix3()) + + def test_in_place_not_mutable(self): + mesh = primitives.square_solid(primitives.SquareFlags.TEXTURE_COORDINATES) + + with self.assertRaisesRegex(AssertionError, "vertex data not mutable"): + meshtools.transform2d_in_place(mesh, Matrix3()) + with self.assertRaisesRegex(AssertionError, "vertex data not mutable"): + meshtools.transform3d_in_place(mesh, Matrix4()) + with self.assertRaisesRegex(AssertionError, "vertex data not mutable"): + meshtools.transform_texture_coordinates2d_in_place(mesh, Matrix3())