Browse Source

python: expose almost all remaining mesh tools operating on a MeshData.

next
Vladimír Vondruš 3 years ago
parent
commit
0eabbebbae
  1. 1
      doc/python/conf.py
  2. 73
      doc/python/magnum.meshtools.rst
  3. 10
      doc/python/pages/changelog.rst
  4. 196
      src/python/magnum/meshtools.cpp
  5. BIN
      src/python/magnum/test/mesh-packed.bin
  6. 9
      src/python/magnum/test/mesh-packed.bin.in
  7. 104
      src/python/magnum/test/mesh-packed.gltf
  8. 258
      src/python/magnum/test/test_meshtools.py

1
doc/python/conf.py

@ -203,6 +203,7 @@ INPUT_DOCS = [
'magnum.rst', 'magnum.rst',
'magnum.gl.rst', 'magnum.gl.rst',
'magnum.math.rst', 'magnum.math.rst',
'magnum.meshtools.rst',
'magnum.platform.rst', 'magnum.platform.rst',
'magnum.scenegraph.rst', 'magnum.scenegraph.rst',
'magnum.scenetools.rst', 'magnum.scenetools.rst',

73
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š <mosra@centrum.cz>
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`

10
doc/python/pages/changelog.rst

@ -102,6 +102,16 @@ Changelog
- Fixed :ref:`platform.sdl2.Application.InputEvent.Modifier` and - Fixed :ref:`platform.sdl2.Application.InputEvent.Modifier` and
:ref:`platform.glfw.Application.InputEvent.Modifier` to behave properly :ref:`platform.glfw.Application.InputEvent.Modifier` to behave properly
as flags and not just as an enum 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 - Exposed :ref:`platform.sdl2.Application.viewport_event` and
:ref:`platform.glfw.Application.viewport_event` and a possibility :ref:`platform.glfw.Application.viewport_event` and a possibility
to make the window resizable on startup to make the window resizable on startup

196
src/python/magnum/meshtools.cpp

@ -24,8 +24,19 @@
*/ */
#include <pybind11/pybind11.h> #include <pybind11/pybind11.h>
#include <pybind11/stl.h> /* for std::vector */
#include <Corrade/Containers/Optional.h>
#include <Corrade/Containers/ArrayViewStl.h>
#include <Magnum/GL/Mesh.h> #include <Magnum/GL/Mesh.h>
#include <Magnum/MeshTools/Compile.h> #include <Magnum/MeshTools/Compile.h>
#include <Magnum/MeshTools/CompressIndices.h>
#include <Magnum/MeshTools/Duplicate.h>
#include <Magnum/MeshTools/FilterAttributes.h>
#include <Magnum/MeshTools/GenerateIndices.h>
#include <Magnum/MeshTools/Interleave.h>
#include <Magnum/MeshTools/RemoveDuplicates.h>
#include <Magnum/MeshTools/Reference.h>
#include <Magnum/MeshTools/Transform.h>
#include <Magnum/Trade/MeshData.h> #include <Magnum/Trade/MeshData.h>
#include "corrade/EnumOperators.h" #include "corrade/EnumOperators.h"
@ -50,10 +61,193 @@ void meshtools(py::module_& m) {
.value("GENERATE_SMOOTH_NORMALS", MeshTools::CompileFlag::GenerateSmoothNormals); .value("GENERATE_SMOOTH_NORMALS", MeshTools::CompileFlag::GenerateSmoothNormals);
corrade::enumOperators(compileFlags); corrade::enumOperators(compileFlags);
py::enum_<MeshTools::InterleaveFlag> 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 m
.def("compile", [](const Trade::MeshData& mesh, MeshTools::CompileFlag flags) { .def("compile", [](const Trade::MeshData& mesh, MeshTools::CompileFlag flags) {
return MeshTools::compile(mesh, 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<Trade::MeshAttribute> 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<Trade::MeshAttribute> 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<Trade::MeshData(*)(const Trade::MeshData&)>(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<Trade::MeshData(*)(const Trade::MeshData&)>(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<Float>::epsilon(),
py::arg("double_epsilon") = Math::TypeTraits<Double>::epsilon())
.def("transform2d", [](const Trade::MeshData& mesh, const Matrix3& transformation, UnsignedInt id, MeshTools::InterleaveFlag flags) {
const Containers::Optional<UnsignedInt> 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<UnsignedInt> 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<UnsignedInt> 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<UnsignedInt> 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<UnsignedInt> tangentAttributeId = mesh.findAttributeId(Trade::MeshAttribute::Tangent, id);
const Containers::Optional<UnsignedInt> bitangentAttributeId = mesh.findAttributeId(Trade::MeshAttribute::Bitangent, id);
const Containers::Optional<UnsignedInt> 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<UnsignedInt> 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<UnsignedInt> 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);
} }
} }

BIN
src/python/magnum/test/mesh-packed.bin

Binary file not shown.

9
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

104
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"
}
]
}

258
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š <mosra@centrum.cz>
#
# 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())
Loading…
Cancel
Save