diff --git a/py3dm/__init__.py b/py3dm/__init__.py index ffbf747..8cc4b91 100644 --- a/py3dm/__init__.py +++ b/py3dm/__init__.py @@ -7,6 +7,9 @@ LineCurve, LineCurveView, LineCurveTable, + Mesh, + MeshTable, + MeshView, ModelComponent, ObjectAttributes, ObjectColorSource, diff --git a/py3dm/__init__.pyi b/py3dm/__init__.pyi index 76e8373..a0c51de 100644 --- a/py3dm/__init__.pyi +++ b/py3dm/__init__.pyi @@ -613,6 +613,398 @@ class LineCurveTable: ... +class Mesh(Geometry): + """Python bindings for the openNURBS ``ON_Mesh`` class. + """ + @overload + def __init__(self) -> None: ... + + @overload + def __init__( + self, + initial_face_array_capacity: int, + initial_vertex_array_capacity: int, + has_vertex_normals: bool, + has_texture_coordinates: bool + ) -> None: ... + + @overload + def __init__(self, mesh: Mesh) -> None: ... + + # query methods + def is_corrupt( + self, + repair: bool, + silent_error: bool, + text_log: TextLog | None = None + ) -> bool: + """Check for corrupt data values that are likely to cause crashes. + + Parameters + ---------- + repair: bool + If ``True``, ``const_cast<>`` will be used to change the corrupt + data so that crashes are less likely. + + silent_error: bool + If ``True``, ON_ERROR will not be called when corruption is + detected. + + text_log: TextLog + If text_log is not null, then a description of corruption is + printed using text_log. + + Notes + ----- + Ideally, ``is_corrupt`` would be a virtual function on ``ON_Object``, + but doing that at this point would break the public SDK. + """ + ... + + def is_deformable(self) -> bool: + """Returns ``True`` if object can be accurately modified with "squishy" + transformations like projections, shears, an non-uniform scaling. + """ + ... + + def is_empty(self) -> bool: + """Returns ``True`` if there are zero vertices or zero faces. + """ + ... + + def is_not_empty(self) -> bool: + """Returns ``True`` if there are vertices or faces. + """ + ... + + def is_valid(self, text_log: TextLog | None = None) -> bool: + """Tests an object to see if its data members are correctly + initialized. + + Parameters + ---------- + text_log: `TextLog`, optional + If the object is not valid and ``text_log`` is not ``None``, then a + brief english description of the reason the object is not valid is + appended to the log. The information appended to ``text_log`` is + suitable for low-level debugging purposes by programmers and is not + intended to be useful as a high level user interface tool. + + Returns + ------- + success: bool + ``True`` if the object is valid or ``False`` if the object is + invalid, uninitialized, etc. + """ + ... + + # count methods + def face_count(self) -> int: + """Return the number of faces. + """ + ... + + def hidden_vertex_count(self) -> int: + """Return the number of hidden vertices. + """ + ... + + def invalid_face_count(self) -> int: + """Return the number of faces that are invalid. + """ + ... + + def quad_count(self) -> int: + """Return the number of faces that are quads. + """ + ... + + def triangle_count(self) -> int: + """Return the number of faces that are triangles. + """ + ... + + def vertex_count(self) -> int: + """Return the number of vertices. + """ + ... + + # has methods + def has_face_normals(self) -> bool: ... + + def has_ngons(self) -> bool: ... + + def has_principal_curvatures(self) -> bool: ... + + def has_surface_parameters(self) -> bool: ... + + def has_vertex_colors(self) -> bool: ... + + def has_vertex_normals(self) -> bool: ... + + # other methods + def clear_vertex_colors(self) -> None: ... + + def flip(self) -> None: + """Reverses the order of the vertices. The first vertex is not changed. + """ + ... + + def reserve_vertex_capacity(self, new_vertex_capacity: int) -> bool: + """Increases the capacity of arrays to be at least the requested capacity. + + Parameters + ---------- + new_vertex_capacity: int + The desired capacity + + Notes + ----- + This function is useful if you are getting ready to add a known number + of vertices and want to increase the dynamic array capacities before + you begin adding vertices. + """ + ... + + def set_quad(self, face_index: int, a: int, b: int, c: int, d: int) -> bool: + """Returns `True` if the quad has been successfully set. + + Parameters + ---------- + face_index: int + The face index + a: int + First vertex index + b: int + Second vertex index + c: int + Third vertex index + d: int + Fourth vertex index + """ + ... + + def set_triangle(self, face_index: int, a: int, b: int, c: int) -> bool: + """Returns `True` if the triangle has been successfully set. + + Parameters + ---------- + face_index: int + The face index + a: int + First vertex index + b: int + Second vertex index + c: int + Third vertex index + """ + ... + + def set_vertex(self, vertex_index: int, vertex_location: Point3d) -> bool: + """Returns `True` if the vertex has been successfully set. + + Parameters + ---------- + vertex_index: int + The vertex index + vertex_location: int + New vertex location + """ + ... + + def swap_coordinates(self, i: int, j: int) -> bool: + """Returns `True` if the vertices with indices ``i`` and ``j`` has been + successfully swap. + """ + ... + + +class MeshTable: + """Python wrapper providing access to ``ON_Mesh`` objects stored in an + ``ONX_Model``. + + This class offers a convenient interface for adding, retrieving, counting, + and iterating over mesh objects. + + ``MeshTable`` does not own the underlying data; it operates on the + associated ``ONX_Model`` instance. + """ + # dunder methods + def __iter__(self) -> Iterator[MeshView]: ... + + def __len__(self) -> int: ... + + # public methods + @overload + def add(self, obj_attr: ObjectAttributes) -> UUID: + """Returns the ``UUID`` of the mesh in case of successful addition, or + an empty ``UUID`` otherwise. If the mesh is in the model, then the + ``UUID`` is unique for all components in the model and is locked. + """ + ... + + @overload + def add(self, mesh: Mesh, obj_attr: None | ObjectAttributes = None) -> UUID: + """Returns the ``UUID`` of the mesh in case of successful addition, or + an empty ``UUID`` otherwise. If the mesh is in the model, then the + ``UUID`` is unique for all components in the model and is locked. + """ + ... + + @overload + def add( + self, + initial_face_array_capacity: int, + initial_vertex_array_capacity: int, + has_vertex_normals: bool, + has_texture_coordinates: bool, + obj_attr: None | ObjectAttributes = None + ) -> UUID: + """Returns the ``UUID`` of the mesh in case of successful addition, or + an empty ``UUID`` otherwise. If the mesh is in the model, then the + ``UUID`` is unique for all components in the model and is locked. + """ + ... + + def count(self) -> int: + """Returns the number of objects of type ``ON::mesh_object`` in the + model. + """ + ... + + def get_by_uuid(self, object_uuid: UUID) -> Mesh | None: + """Returns the object with the given ``object_uuid`` or ``None`` if + ``object_uuid`` is not found. + """ + ... + + def get_by_uuid_exclusive(self, object_uuid: UUID) -> MeshView | None: + """Returns the object with the given ``object_uuid`` or ``None`` if + ``object_uuid`` is not found. + + Notes + ----- + From opennurbs documentation + (``ON_ModelGeometryComponent::ExclusiveGeometry()``): + Get a pointer to geometry that can be used to modify the geometry. The + returned pointer is not shared at the time it is returned and will not + be shared until a copy of this ``ON_ModelGeometryComponent`` is + created. If this ``ON_ModelGeometryComponent`` is the only reference to + the geometry, then a pointer to the geometry is returned. Otherwise, + ``nullptr`` is returned. + """ + ... + + +class MeshView: + """Tiny wrapper to read-only access `Mesh` (``ON_Mesh``) objects. + """ + # query methods + def is_corrupt( + self, + repair: bool, + silent_error: bool, + text_log: TextLog | None = None + ) -> bool: + """Check for corrupt data values that are likely to cause crashes. + + Parameters + ---------- + repair: bool + If ``True``, ``const_cast<>`` will be used to change the corrupt + data so that crashes are less likely. + + silent_error: bool + If ``True``, ON_ERROR will not be called when corruption is + detected. + + text_log: TextLog + If text_log is not null, then a description of corruption is + printed using text_log. + + Notes + ----- + Ideally, ``is_corrupt`` would be a virtual function on ``ON_Object``, + but doing that at this point would break the public SDK. + """ + ... + + def is_empty(self) -> bool: + """Returns ``True`` if there are zero vertices or zero faces. + """ + ... + + def is_not_empty(self) -> bool: + """Returns ``True`` if there are vertices or faces. + """ + ... + + def is_valid(self, text_log: TextLog | None = None) -> bool: + """Tests an object to see if its data members are correctly + initialized. + + Parameters + ---------- + text_log: `TextLog`, optional + If the object is not valid and ``text_log`` is not ``None``, then a + brief english description of the reason the object is not valid is + appended to the log. The information appended to ``text_log`` is + suitable for low-level debugging purposes by programmers and is not + intended to be useful as a high level user interface tool. + + Returns + ------- + success: bool + ``True`` if the object is valid or ``False`` if the object is + invalid, uninitialized, etc. + """ + ... + + # count methods + def face_count(self) -> int: + """Return the number of faces. + """ + ... + + def hidden_vertex_count(self) -> int: + """Return the number of hidden vertices. + """ + ... + + def invalid_face_count(self) -> int: + """Return the number of faces that are invalid. + """ + ... + + def quad_count(self) -> int: + """Return the number of faces that are quads. + """ + ... + + def triangle_count(self) -> int: + """Return the number of faces that are triangles. + """ + ... + + def vertex_count(self) -> int: + """Return the number of vertices. + """ + ... + + # has methods + def has_face_normals(self) -> bool: ... + + def has_ngons(self) -> bool: ... + + def has_principal_curvatures(self) -> bool: ... + + def has_surface_parameters(self) -> bool: ... + + def has_vertex_colors(self) -> bool: ... + + def has_vertex_normals(self) -> bool: ... + + class Model: """Python bindings for the openNURBS ``ONX_Model`` class. diff --git a/src/bindings/bindings.cpp b/src/bindings/bindings.cpp index 5a2febb..bd25c9e 100644 --- a/src/bindings/bindings.cpp +++ b/src/bindings/bindings.cpp @@ -13,6 +13,9 @@ #include "line_curve_bindings.h" #include "line_curve_table_bindings.h" #include "line_curve_view_bindings.h" +#include "mesh_bindings.h" +#include "mesh_table_bindings.h" +#include "mesh_view_bindings.h" #include "model_bindings.h" #include "model_component_bindings.h" #include "object_attributes_bindings.h" @@ -41,6 +44,9 @@ NB_MODULE(_py3dm, m) { LayerTableBindings(m); LineCurveBindings(m); LineCurveViewBindings(m); + MeshBindings(m); + MeshTableBindings(m); + MeshViewBindings(m); ModelBindings(m); PointBindings(m); TextLogBindings(m); diff --git a/src/bindings/mesh_bindings.cpp b/src/bindings/mesh_bindings.cpp new file mode 100644 index 0000000..856e028 --- /dev/null +++ b/src/bindings/mesh_bindings.cpp @@ -0,0 +1,54 @@ +// System includes + +// External includes + +// Project includes +#include "mesh_bindings.h" + + +void MeshBindings(nb::module_& m) { + nb::class_(m, "Mesh") + /*magic methods*/ + .def(nb::init<>()) + .def(nb::init()) + .def(nb::init()) + + /*query methods*/ + .def( + "is_corrupt", + &ON_Mesh::IsCorrupt, + nb::arg("repair"), + nb::arg("silent_error") , + nb::arg("text_log") = nullptr + ) + .def("is_deformable", &ON_Mesh::IsDeformable) + .def("is_empty", &ON_Mesh::IsEmpty) + .def("is_not_empty", &ON_Mesh::IsNotEmpty) + .def("is_valid", &ON_Mesh::IsValid, nb::arg("text_log") = nullptr) + + /*count methods*/ + .def("face_count", &ON_Mesh::FaceCount) + .def("hidden_vertex_count", &ON_Mesh::HiddenVertexCount) + .def("invalid_face_count", &ON_Mesh::InvalidFaceCount) + .def("quad_count", &ON_Mesh::QuadCount) + .def("triangle_count", &ON_Mesh::TriangleCount) + .def("vertex_count", &ON_Mesh::VertexCount) + + /*has methods*/ + .def("has_face_normals", &ON_Mesh::HasFaceNormals) + .def("has_ngons", &ON_Mesh::HasNgons) + .def("has_principal_curvatures", &ON_Mesh::HasPrincipalCurvatures) + .def("has_surface_parameters", &ON_Mesh::HasSurfaceParameters) + .def("has_vertex_colors", nb::overload_cast<>(&ON_Mesh::HasVertexColors, nb::const_)) + .def("has_vertex_normals", &ON_Mesh::HasVertexNormals) + + /*other methods*/ + .def("clear_vertex_colors", &ON_Mesh::ClearVertexColors) + .def("flip", &ON_Mesh::Flip) + .def("reserve_vertex_capacity", &ON_Mesh::ReserveVertexCapacity) + .def("set_quad", &ON_Mesh::SetQuad) + .def("set_triangle", &ON_Mesh::SetTriangle) + .def("set_vertex", nb::overload_cast(&ON_Mesh::SetVertex)) + .def("swap_coordinates", &ON_Mesh::SwapCoordinates) + ; +} diff --git a/src/bindings/mesh_bindings.h b/src/bindings/mesh_bindings.h new file mode 100644 index 0000000..aa3f329 --- /dev/null +++ b/src/bindings/mesh_bindings.h @@ -0,0 +1,19 @@ +/* + src/bindings/mesh_bindings.h: Exposing ON_Mesh to Python + + Copyright (c) 2026 Studio W Engineers + + All rights reserved. Use of this source code is governed by a + BSD-style license that can be found in the LICENSE file. +*/ +#pragma once + +// System includes + +// External includes + +// Project includes +#include "bindings.h" + + +void MeshBindings(nb::module_& m); diff --git a/src/bindings/mesh_table_bindings.cpp b/src/bindings/mesh_table_bindings.cpp new file mode 100644 index 0000000..79f26e6 --- /dev/null +++ b/src/bindings/mesh_table_bindings.cpp @@ -0,0 +1,62 @@ +// System includes + +// External includes + +// Project includes +#include "../mesh_table.h" +#include "casters/uuid_caster.h" + + +void MeshTableBindings(nb::module_& m) { + nb::class_(m, "__MeshTableIterator") + .def("__iter__", [](MeshTable::Iterator& it) -> MeshTable::Iterator& { + return it; + }) + .def("__next__", [](MeshTable::Iterator& it) { + while (!it.IsOver()) { + MeshView* object = *it; + ++it; + + if (object != nullptr) { + return object; + } + } + throw nb::stop_iteration(); + }, nb::rv_policy::reference_internal + ) + ; + + nb::class_(m, "MeshTable") + /*magic methods*/ + .def("__iter__", [](MeshTable& self) {return self.Begin();}, nb::keep_alive<0, 1>()) + .def("__len__", &MeshTable::Count) + + /*add methods*/ + .def( + "add", + nb::overload_cast(&MeshTable::Add, nb::const_), + nb::arg("obj_attr") = nullptr + ) + .def( + "add", + nb::overload_cast(&MeshTable::Add, nb::const_), + nb::arg("mesh"), + nb::arg("obj_attr") = nullptr + ) + .def( + "add", + nb::overload_cast(&MeshTable::Add, nb::const_), + nb::arg("initial_face_array_capacity"), + nb::arg("initial_vertex_array_capacity"), + nb::arg("has_vertex_normals") = false, + nb::arg("has_texture_coordinates") = false, + nb::arg("obj_attr") = nullptr + ) + + /*getters*/ + .def("get_by_uuid", &MeshTable::GetbyUUID, nb::rv_policy::reference_internal) + + /*other methods*/ + .def("count", &MeshTable::Count) + ; +} diff --git a/src/bindings/mesh_table_bindings.h b/src/bindings/mesh_table_bindings.h new file mode 100644 index 0000000..167eab7 --- /dev/null +++ b/src/bindings/mesh_table_bindings.h @@ -0,0 +1,19 @@ +/* + src/bindings/mesh_table_bindings.h: Exposing helper class MeshTable to Python + + Copyright (c) 2026 Studio W Engineers + + All rights reserved. Use of this source code is governed by a + BSD-style license that can be found in the LICENSE file. +*/ +#pragma once + +// System includes + +// External includes + +// Project includes +#include "bindings.h" + + +void MeshTableBindings(nb::module_& m); diff --git a/src/bindings/mesh_view_bindings.cpp b/src/bindings/mesh_view_bindings.cpp new file mode 100644 index 0000000..83b0713 --- /dev/null +++ b/src/bindings/mesh_view_bindings.cpp @@ -0,0 +1,35 @@ +// System includes + +// External includes + +// Project includes +#include "../views/mesh_view.h" +#include "casters/uuid_caster.h" +#include "mesh_view_bindings.h" + + +void MeshViewBindings(nb::module_& m) { + nb::class_(m, "MeshView") + /*query methods*/ + .def("is_corrupt", &MeshView::IsCorrupt) + .def("is_empty", &MeshView::IsEmpty) + .def("is_not_empty", &MeshView::IsNotEmpty) + .def("is_valid", &MeshView::IsValid) + + /*count methods*/ + .def("face_count", &MeshView::FaceCount) + .def("hidden_vertex_count", &MeshView::HiddenVertexCount) + .def("invalid_face_count", &MeshView::InvalidFaceCount) + .def("quad_count", &MeshView::QuadCount) + .def("triangle_count", &MeshView::TriangleCount) + .def("vertex_count", &MeshView::VertexCount) + + /*has methods*/ + .def("has_face_normals", &MeshView::HasFaceNormals) + .def("has_ngons", &MeshView::HasNgons) + .def("has_principal_curvatures", &MeshView::HasPrincipalCurvatures) + .def("has_surface_parameters", &MeshView::HasSurfaceParameters) + .def("has_vertex_colors", &MeshView::HasVertexColors) + .def("has_vertex_normals", &MeshView::HasVertexNormals) + ; +} diff --git a/src/bindings/mesh_view_bindings.h b/src/bindings/mesh_view_bindings.h new file mode 100644 index 0000000..d9120eb --- /dev/null +++ b/src/bindings/mesh_view_bindings.h @@ -0,0 +1,19 @@ +/* + src/bindings/mesh_view_bindings.h: Exposing MeshView to Python + + Copyright (c) 2026 Studio W Engineers + + All rights reserved. Use of this source code is governed by a + BSD-style license that can be found in the LICENSE file. +*/ +#pragma once + +// System includes + +// External includes + +// Project includes +#include "bindings.h" + + +void MeshViewBindings(nb::module_& m); diff --git a/src/bindings/model_bindings.cpp b/src/bindings/model_bindings.cpp index e51f3a0..2228ac8 100644 --- a/src/bindings/model_bindings.cpp +++ b/src/bindings/model_bindings.cpp @@ -34,6 +34,7 @@ void ModelBindings(nb::module_& m) { /*tables*/ .def_prop_ro("layer_table", &Model::ModelLayerTable) .def_prop_ro("line_curve_table", &Model::ModelLineCurveTable) + .def_prop_ro("mesh_table", &Model::ModelMeshTable) .def_prop_ro("point_table", &Model::ModelPointTable) ; } diff --git a/src/mesh_table.cpp b/src/mesh_table.cpp new file mode 100644 index 0000000..995d9aa --- /dev/null +++ b/src/mesh_table.cpp @@ -0,0 +1,114 @@ +#include "mesh_table.h" + +/*constructors*/ +MeshTable::MeshTable(std::shared_ptr model) { + m_model = model; +} + +/*add methods*/ +ON_UUID MeshTable::Add(const ON_3dmObjectAttributes* obj_attr) const { + ON_Mesh mesh; + const ON_ModelComponent* mc = m_model->AddModelGeometryComponent(&mesh, obj_attr).ModelComponent(); + return (mc != nullptr) ? mc->Id() : ON_nil_uuid; +} + +ON_UUID MeshTable::Add(const ON_Mesh& mesh, const ON_3dmObjectAttributes* obj_attr) const { + const ON_ModelComponent* mc = m_model->AddModelGeometryComponent(&mesh, obj_attr).ModelComponent(); + return (mc != nullptr) ? mc->Id() : ON_nil_uuid; +} + +ON_UUID MeshTable::Add( + int initial_face_array_capacity, + int initial_vertex_array_capacity, + bool has_vertex_normals, + bool has_texture_coordinates, + const ON_3dmObjectAttributes* obj_attr +) const { + ON_Mesh mesh( + initial_face_array_capacity, + initial_vertex_array_capacity, + has_vertex_normals, + has_texture_coordinates + ); + const ON_ModelComponent* mc = m_model->AddModelGeometryComponent(&mesh, obj_attr).ModelComponent(); + return (mc != nullptr) ? mc->Id() : ON_nil_uuid; +} + +/*getters*/ +MeshView* MeshTable::GetbyUUID(const ON_UUID on_uuid) const { + const ON_ModelComponent* mc = m_model->ComponentFromId(ON_ModelComponent::Type::ModelGeometry, on_uuid).ModelComponent(); + + if (!IsMesh(mc)) { + return nullptr; + } + + const ON_ModelGeometryComponent* mgc = ON_ModelGeometryComponent::Cast(mc); + return new MeshView(ON_Mesh::Cast(mgc->Geometry(nullptr)), mgc->ModelObjectId()); +} + +ON_Mesh* MeshTable::GetbyUUIDExclusive(const ON_UUID obj_uuid) const { + const ON_ModelComponentReference& mcr = m_model->ComponentFromRuntimeSerialNumber( + GetRuntimeSerialNumber(obj_uuid) + ); + ON_ModelGeometryComponent* mgc = ON_ModelGeometryComponent::Cast( + mcr.ExclusiveModelComponent() + ); + + return mgc? ON_Mesh::Cast(mgc->ExclusiveGeometry()) : nullptr; +} + +ON__UINT64 MeshTable::GetRuntimeSerialNumber(const ON_UUID obj_uuid) const { + return m_model->Manifest().ItemFromId( + ON_ModelComponent::Type::ModelGeometry, + obj_uuid + ).ComponentRuntimeSerialNumber(); +} + +/*other methods*/ +int MeshTable::Count() const { + int count = 0; + ONX_ModelComponentIterator mci(*m_model.get(), ON_ModelComponent::Type::ModelGeometry); + ON_ModelComponentReference mcr = mci.FirstComponentReference(); + while (!mcr.IsEmpty()) { + if (MeshTable::IsMesh(mcr.ModelComponent())) { + ++count; + } + mcr = mci.NextComponentReference(); + } + + return count; +} + +bool MeshTable::IsMesh(const ON_ModelComponent* mc) { + const ON_ModelGeometryComponent* mgc = ON_ModelGeometryComponent::Cast(mc); + if (mgc == nullptr) { + return false; + } + + const ON_Geometry* geom = mgc->Geometry(nullptr); + return (geom && geom->ObjectType() == ON::mesh_object); +} + +/*MeshTable Iterator*/ +MeshTable::Iterator::Iterator(MeshTable* table) + : m_table(table), + m_iterator(*table->m_model.get(), ON_ModelComponent::Type::ModelGeometry) { + m_current = m_iterator.FirstComponentReference(); +} + +MeshView* MeshTable::Iterator::operator*() const { + return m_table->GetbyUUID(m_current.ModelComponentId()); +} + +MeshTable::Iterator& MeshTable::Iterator::operator++() { + m_current = m_iterator.NextComponentReference(); + return *this; +} + +bool MeshTable::Iterator::IsOver() const { + return m_current.IsEmpty(); +} + +MeshTable::Iterator MeshTable::Begin() { + return Iterator(this); +} diff --git a/src/mesh_table.h b/src/mesh_table.h new file mode 100644 index 0000000..0f87649 --- /dev/null +++ b/src/mesh_table.h @@ -0,0 +1,67 @@ +/* + src/point_table.h: Implementation details related to MeshTable. + + Copyright (c) 2026 Studio W Engineers + + All rights reserved. Use of this source code is governed by a + BSD-style license that can be found in the LICENSE file. +*/ +#pragma once + +// System includes + +// External includes + +// Project includes +#include "../lib/opennurbs/opennurbs.h" +#include "views/mesh_view.h" + + +class MeshTable { +public: + /*constructors*/ + MeshTable(std::shared_ptr model); + + /*destructor*/ + ~MeshTable() = default; + + /*add methods*/ + ON_UUID Add(const ON_3dmObjectAttributes* obj_attr) const; + ON_UUID Add(const ON_Mesh& mesh, const ON_3dmObjectAttributes* obj_attr) const; + ON_UUID Add( + int initial_face_array_capacity, + int initial_vertex_array_capacity, + bool has_vertex_normals, + bool has_texture_coordinates, + const ON_3dmObjectAttributes* obj_attr + ) const; + + /*getters*/ + MeshView* GetbyUUID(const ON_UUID obj_uuid) const; + ON_Mesh* GetbyUUIDExclusive(const ON_UUID obj_uuid) const; + ON__UINT64 GetRuntimeSerialNumber(const ON_UUID obj_uuid) const; + + /*other methods*/ + int Count() const; + static bool IsMesh(const ON_ModelComponent* mc); + + /*MeshTable Iterator*/ + class Iterator { + public: + Iterator(MeshTable* table); + + MeshView* operator*() const; + Iterator& operator++(); + bool IsOver() const; + + private: + MeshTable* m_table; + ONX_ModelComponentIterator m_iterator; + mutable ON_ModelComponentReference m_current; + }; + + Iterator Begin(); + +private: + std::shared_ptr m_model; +}; diff --git a/src/model.cpp b/src/model.cpp index 98a77df..f7c34f3 100644 --- a/src/model.cpp +++ b/src/model.cpp @@ -115,6 +115,10 @@ LineCurveTable Model::ModelLineCurveTable() { return LineCurveTable(p_model); } +MeshTable Model::ModelMeshTable() { + return MeshTable(p_model); +} + PointTable Model::ModelPointTable() { return PointTable(p_model); } diff --git a/src/model.h b/src/model.h index ad275f0..76818f8 100644 --- a/src/model.h +++ b/src/model.h @@ -15,6 +15,7 @@ // Project includes #include "line_curve_table.h" #include "layer_table.h" +#include "mesh_table.h" #include "point_table.h" @@ -46,8 +47,9 @@ class Model { bool Write(ON_wString path, int version); /*tables*/ - LayerTable ModelLayerTable(); LineCurveTable ModelLineCurveTable(); + LayerTable ModelLayerTable(); + MeshTable ModelMeshTable(); PointTable ModelPointTable(); private: diff --git a/src/views/mesh_view.cpp b/src/views/mesh_view.cpp new file mode 100644 index 0000000..197af89 --- /dev/null +++ b/src/views/mesh_view.cpp @@ -0,0 +1,79 @@ +#include "mesh_view.h" + +/*constructor*/ +MeshView::MeshView( + const ON_Mesh* source, + const ON_UUID on_uuid +) : m_mesh(source), m_uuid(on_uuid) {} + +/*query methods*/ +bool MeshView::IsCorrupt() const { + return m_mesh->IsCorrupt(false, true, nullptr); +} + +bool MeshView::IsEmpty() const { + return m_mesh->IsEmpty(); +} + +bool MeshView::IsNotEmpty() const { + return m_mesh->IsNotEmpty(); +} + +bool MeshView::IsValid() const { + return m_mesh->IsValid(nullptr); +} + +/*count methods*/ +int MeshView::FaceCount() const { + return m_mesh->FaceCount(); +} + +int MeshView::HiddenVertexCount() const { + return m_mesh->HiddenVertexCount(); +} + +int MeshView::InvalidFaceCount() const { + return m_mesh->InvalidFaceCount(); +} + +int MeshView::QuadCount() const { + return m_mesh->QuadCount(); +} + +int MeshView::TriangleCount() const { + return m_mesh->TriangleCount(); +} + +int MeshView::VertexCount() const { + return m_mesh->VertexCount(); +} + +/*has methods*/ +bool MeshView::HasFaceNormals() const { + return m_mesh->HasFaceNormals(); +} + +bool MeshView::HasNgons() const { + return m_mesh->HasNgons(); +} + +bool MeshView::HasPrincipalCurvatures() const { + return m_mesh->HasPrincipalCurvatures(); +} + +bool MeshView::HasSurfaceParameters() const { + return m_mesh->HasSurfaceParameters(); +} + +bool MeshView::HasVertexColors() const { + return m_mesh->HasVertexColors(); +} + +bool MeshView::HasVertexNormals() const { + return m_mesh->HasVertexNormals(); +} + +/*other methods*/ +ON_UUID MeshView::GetUUID() const { + return m_uuid; +} diff --git a/src/views/mesh_view.h b/src/views/mesh_view.h new file mode 100644 index 0000000..a33d180 --- /dev/null +++ b/src/views/mesh_view.h @@ -0,0 +1,56 @@ +/* + src/mesh_view.h: Tiny wrapper to read-only access ON_Mesh objects + + Copyright (c) 2026 Studio W Engineers + + All rights reserved. Use of this source code is governed by a + BSD-style license that can be found in the LICENSE file. +*/ +#pragma once + +// System includes + +// External includes + +// Project includes +#include "../../lib/opennurbs/opennurbs.h" + + +class MeshView { +public: + /*constructor*/ + MeshView(const ON_Mesh* source, const ON_UUID on_uuid); + + /*destructor*/ + ~MeshView() = default; + + /*query methods*/ + bool IsCorrupt() const; + bool IsEmpty() const ; + bool IsNotEmpty() const ; + bool IsValid() const ; + + /*count methods*/ + int FaceCount() const; + int HiddenVertexCount() const; + int InvalidFaceCount() const; + int QuadCount() const; + int TriangleCount() const; + int VertexCount() const; + + /*has methods*/ + bool HasFaceNormals() const; + bool HasNgons() const; + bool HasPrincipalCurvatures() const; + bool HasSurfaceParameters() const; + bool HasVertexColors() const; + bool HasVertexNormals() const; + + /*other methods*/ + ON_UUID GetUUID() const; + +private: + /*member variables*/ + const ON_Mesh* m_mesh; + const ON_UUID m_uuid; +}; \ No newline at end of file diff --git a/tests/test_mesh.py b/tests/test_mesh.py new file mode 100644 index 0000000..bcaa21e --- /dev/null +++ b/tests/test_mesh.py @@ -0,0 +1,40 @@ +# standard library imports +from unittest import TestCase + +# third party library imports +from py3dm import Mesh, Point3d + +# local library specific imports + + +class MeshTestSuite(TestCase): + def test_set_quad(self) -> None: + mesh = Mesh(1, 4, False, False) + with self.subTest(msg="Empty mesh"): + self.assertEqual(mesh.quad_count(), 0) + + mesh.set_vertex(0, Point3d(0, 0, 0)) + mesh.set_vertex(1, Point3d(1, 0, 0)) + mesh.set_vertex(2, Point3d(0, 1, 0)) + mesh.set_vertex(3, Point3d(0, 0, 1)) + mesh.set_quad(0, 0, 1, 2, 3) + + with self.subTest(msg="Non-empty mesh"): + self.assertEqual(mesh.quad_count(), 1) + + with self.subTest(msg="Valid mesh"): + self.assertTrue(mesh.is_valid()) + + def test_set_triangle(self) -> None: + mesh = Mesh() + mesh.reserve_vertex_capacity(3) + with self.subTest(msg="Empty mesh"): + self.assertEqual(mesh.triangle_count(), 0) + + mesh.set_vertex(0, Point3d(0, 0, 0)) + mesh.set_vertex(1, Point3d(1, 0, 0)) + mesh.set_vertex(2, Point3d(0, 1, 0)) + mesh.set_triangle(0, 0, 1, 2) + + with self.subTest(msg="Non-empty mesh"): + self.assertEqual(mesh.triangle_count(), 1) diff --git a/tests/test_mesh_table.py b/tests/test_mesh_table.py new file mode 100644 index 0000000..f573664 --- /dev/null +++ b/tests/test_mesh_table.py @@ -0,0 +1,51 @@ +# standard library imports +from unittest import TestCase +from uuid import UUID + +# third party library imports +from py3dm import Mesh, Model, ObjectAttributes, Point3d + +# local library specific imports + + +class MeshTableTestSuite(TestCase): + def setUp(self) -> None: + model = Model() + self.table = model.mesh_table + + def test_add_with_attributes(self) -> None: + mesh = Mesh() + mesh.reserve_vertex_capacity(3) + mesh.set_vertex(0, Point3d(0, 0, 0)) + mesh.set_vertex(1, Point3d(1, 0, 0)) + mesh.set_vertex(2, Point3d(0, 1, 0)) + mesh.set_triangle(0, 0, 1, 2) + mesh_uuid = self.table.add(mesh) + + with self.subTest(): + self.assertNotEqual(mesh_uuid, UUID(int=0)) + + with self.subTest(): + retrieved_mesh = self.table.get_by_uuid(mesh_uuid) + self.assertEqual(retrieved_mesh.face_count(), 1) + + with self.subTest(): + retrieved_mesh = self.table.get_by_uuid(mesh_uuid) + self.assertEqual(retrieved_mesh.triangle_count(), 1) + + def test_count(self) -> None: + with self.subTest(msg="empty table"): + self.assertEqual(self.table.count(), 0) + + self.table.add(Mesh()) + + with self.subTest(msg="non-empty table"): + self.assertEqual(self.table.count(), 1) + + with self.subTest(msg="non-empty table"): + self.assertEqual(len(self.table), 1) + + def test_get_by_uuid(self) -> None: + obj_uuid = self.table.add(Mesh()) + mesh = self.table.get_by_uuid(obj_uuid) + self.assertTrue(mesh.is_empty())