diff --git a/source/blender/blenkernel/BKE_mesh_compare.hh b/source/blender/blenkernel/BKE_geometry_compare.hh similarity index 57% rename from source/blender/blenkernel/BKE_mesh_compare.hh rename to source/blender/blenkernel/BKE_geometry_compare.hh index aba6755f77a..5af51f6cd68 100644 --- a/source/blender/blenkernel/BKE_mesh_compare.hh +++ b/source/blender/blenkernel/BKE_geometry_compare.hh @@ -4,20 +4,21 @@ #pragma once +#include "BKE_curves.hh" #include "BKE_mesh_types.hh" /** \file * \ingroup bke */ -namespace blender::bke::compare_meshes { +namespace blender::bke::compare_geometry { -enum class MeshMismatch : int8_t; +enum class GeoMismatch : int8_t; /** * Convert the mismatch to a human-readable string for display. */ -const char *mismatch_to_string(const MeshMismatch &mismatch); +const char *mismatch_to_string(const GeoMismatch &mismatch); /** * \brief Checks if the two meshes are different, returning the type of mismatch if any. Changes in @@ -33,6 +34,16 @@ const char *mismatch_to_string(const MeshMismatch &mismatch); * * \returns The type of mismatch that was detected, if there is any. */ -std::optional compare_meshes(const Mesh &mesh1, const Mesh &mesh2, float threshold); +std::optional compare_meshes(const Mesh &mesh1, const Mesh &mesh2, float threshold); -} // namespace blender::bke::compare_meshes +/** + * \brief Checks if the two curves geometries are different, returning the type of mismatch if any. + * Changes in index order are detected, but treated as a mismatch. + * + * \returns The type of mismatch that was detected, if there is any. + */ +std::optional compare_curves(const CurvesGeometry &curves1, + const CurvesGeometry &curves2, + float threshold); + +} // namespace blender::bke::compare_geometry diff --git a/source/blender/blenkernel/CMakeLists.txt b/source/blender/blenkernel/CMakeLists.txt index 46bc5988d22..7834bcf095c 100644 --- a/source/blender/blenkernel/CMakeLists.txt +++ b/source/blender/blenkernel/CMakeLists.txt @@ -200,7 +200,7 @@ set(SRC intern/mesh.cc intern/mesh_attributes.cc intern/mesh_calc_edges.cc - intern/mesh_compare.cc + intern/geometry_compare.cc intern/mesh_convert.cc intern/mesh_data_update.cc intern/mesh_debug.cc @@ -449,7 +449,7 @@ set(SRC BKE_mball_tessellate.hh BKE_mesh.h BKE_mesh.hh - BKE_mesh_compare.hh + BKE_geometry_compare.hh BKE_mesh_fair.hh BKE_mesh_iterators.hh BKE_mesh_legacy_convert.hh diff --git a/source/blender/blenkernel/intern/mesh_compare.cc b/source/blender/blenkernel/intern/geometry_compare.cc similarity index 80% rename from source/blender/blenkernel/intern/mesh_compare.cc rename to source/blender/blenkernel/intern/geometry_compare.cc index 52988b0a202..e0e13908f80 100644 --- a/source/blender/blenkernel/intern/mesh_compare.cc +++ b/source/blender/blenkernel/intern/geometry_compare.cc @@ -12,55 +12,64 @@ #include "BKE_mesh.hh" #include "BKE_mesh_mapping.hh" -#include "BKE_mesh_compare.hh" +#include "BKE_geometry_compare.hh" -namespace blender::bke::compare_meshes { +namespace blender::bke::compare_geometry { -enum class MeshMismatch : int8_t { - NumVerts, /* The number of vertices is different. */ +enum class GeoMismatch : int8_t { + NumPoints, /* The number of points is different. */ NumEdges, /* The number of edges is different. */ NumCorners, /* The number of corners is different. */ NumFaces, /* The number of faces is different. */ - VertexAttributes, /* Some values of the vertex attributes are different. */ + NumCurves, /* The number of curves is different. */ + PointAttributes, /* Some values of the point attributes are different. */ EdgeAttributes, /* Some values of the edge attributes are different. */ CornerAttributes, /* Some values of the corner attributes are different. */ FaceAttributes, /* Some values of the face attributes are different. */ + CurveAttributes, /* Some values of the curve attributes are different. */ EdgeTopology, /* The edge topology is different. */ FaceTopology, /* The face topology is different. */ + CurveTopology, /* The curve topology is different. */ Attributes, /* The sets of attribute ids are different. */ AttributeTypes, /* Some attributes with the same name have different types. */ - Indices, /* The meshes are the same up to a change of indices. */ + Indices, /* The geometries are the same up to a change of indices. */ }; -const char *mismatch_to_string(const MeshMismatch &mismatch) +const char *mismatch_to_string(const GeoMismatch &mismatch) { switch (mismatch) { - case MeshMismatch::NumVerts: - return "The number of vertices is different"; - case MeshMismatch::NumEdges: + case GeoMismatch::NumPoints: + return "The number of points is different"; + case GeoMismatch::NumEdges: return "The number of edges is different"; - case MeshMismatch::NumCorners: + case GeoMismatch::NumCorners: return "The number of corners is different"; - case MeshMismatch::NumFaces: + case GeoMismatch::NumFaces: return "The number of faces is different"; - case MeshMismatch::VertexAttributes: - return "Some values of the vertex attributes are different"; - case MeshMismatch::EdgeAttributes: + case GeoMismatch::NumCurves: + return "The number of curves is different"; + case GeoMismatch::PointAttributes: + return "Some values of the point attributes are different"; + case GeoMismatch::EdgeAttributes: return "Some values of the edge attributes are different"; - case MeshMismatch::CornerAttributes: + case GeoMismatch::CornerAttributes: return "Some values of the corner attributes are different"; - case MeshMismatch::FaceAttributes: + case GeoMismatch::FaceAttributes: return "Some values of the face attributes are different"; - case MeshMismatch::EdgeTopology: + case GeoMismatch::CurveAttributes: + return "Some values of the curve attributes are different"; + case GeoMismatch::EdgeTopology: return "The edge topology is different"; - case MeshMismatch::FaceTopology: + case GeoMismatch::FaceTopology: return "The face topology is different"; - case MeshMismatch::Attributes: + case GeoMismatch::CurveTopology: + return "The curve topology is different"; + case GeoMismatch::Attributes: return "The sets of attribute ids are different"; - case MeshMismatch::AttributeTypes: + case GeoMismatch::AttributeTypes: return "Some attributes with the same name have different types"; - case MeshMismatch::Indices: - return "The meshes are the same up to a change of indices"; + case GeoMismatch::Indices: + return "The geometries are the same up to a change of indices"; } BLI_assert_unreachable(); return ""; @@ -502,32 +511,32 @@ static bool ignored_attribute(const StringRef id) } /** - * Verify that both meshes have the same attributes: + * Verify that both geometries have the same attributes: * - Same names * - Same domains * - Same types */ -static std::optional verify_attributes_compatible( - const AttributeAccessor &mesh1_attributes, const AttributeAccessor &mesh2_attributes) +static std::optional verify_attributes_compatible( + const AttributeAccessor &attributes1, const AttributeAccessor &attributes2) { - Set mesh1_attribute_ids = mesh1_attributes.all_ids(); - Set mesh2_attribute_ids = mesh2_attributes.all_ids(); - mesh1_attribute_ids.remove_if(ignored_attribute); - mesh2_attribute_ids.remove_if(ignored_attribute); + Set attribute_ids1 = attributes1.all_ids(); + Set attribute_ids2 = attributes2.all_ids(); + attribute_ids1.remove_if(ignored_attribute); + attribute_ids2.remove_if(ignored_attribute); - if (mesh1_attribute_ids != mesh2_attribute_ids) { + if (attribute_ids1 != attribute_ids2) { /* Disabled for now due to tests not being up to date. */ - // return MeshMismatch::Attributes; + // return GeoMismatch::Attributes; } - for (const StringRef id : mesh1_attribute_ids) { - GAttributeReader reader1 = mesh1_attributes.lookup(id); - GAttributeReader reader2 = mesh2_attributes.lookup(id); + for (const StringRef id : attribute_ids1) { + GAttributeReader reader1 = attributes1.lookup(id); + GAttributeReader reader2 = attributes2.lookup(id); if (!reader1 || !reader2) { /* Necessary because of previous disabled return. */ continue; } if (reader1.domain != reader2.domain || reader1.varray.type() != reader2.varray.type()) { - return MeshMismatch::AttributeTypes; + return GeoMismatch::AttributeTypes; } } return std::nullopt; @@ -536,38 +545,38 @@ static std::optional verify_attributes_compatible( /** * Sort the domain using all the attributes on that domain except the ones in excluded_attributes * - * \returns A mismatch if one of the attributes has different values between the two meshes. + * \returns A mismatch if one of the attributes has different values between the two geometries. */ -static std::optional sort_domain_using_attributes( - const AttributeAccessor &mesh1_attributes, - const AttributeAccessor &mesh2_attributes, +static std::optional sort_domain_using_attributes( + const AttributeAccessor &attributes1, + const AttributeAccessor &attributes2, const AttrDomain domain, const Span excluded_attributes, IndexMapping &maps, const float threshold) { - /* We only need the ids from one mesh, since we know they have the same attributes. */ - Set attribute_ids = mesh1_attributes.all_ids(); + /* We only need the ids from one geometry, since we know they have the same attributes. */ + Set attribute_ids = attributes1.all_ids(); for (const StringRef name : excluded_attributes) { attribute_ids.remove_as(name); } attribute_ids.remove_if(ignored_attribute); for (const StringRef id : attribute_ids) { - if (!mesh2_attributes.contains(id)) { + if (!attributes2.contains(id)) { /* Only needed right now since some test meshes don't have the same attributes. */ - return MeshMismatch::Attributes; + return GeoMismatch::Attributes; } - GAttributeReader reader1 = mesh1_attributes.lookup(id); - GAttributeReader reader2 = mesh2_attributes.lookup(id); + GAttributeReader reader1 = attributes1.lookup(id); + GAttributeReader reader2 = attributes2.lookup(id); if (reader1.domain != domain) { /* We only look at attributes of the given domain. */ continue; } - std::optional mismatch = {}; + std::optional mismatch = {}; attribute_math::convert_to_static_type(reader1.varray.type(), [&](auto dummy) { using T = decltype(dummy); @@ -602,16 +611,19 @@ static std::optional sort_domain_using_attributes( if (!attributes_line_up) { switch (domain) { case AttrDomain::Point: - mismatch = MeshMismatch::VertexAttributes; + mismatch = GeoMismatch::PointAttributes; return; case AttrDomain::Edge: - mismatch = MeshMismatch::EdgeAttributes; + mismatch = GeoMismatch::EdgeAttributes; return; case AttrDomain::Corner: - mismatch = MeshMismatch::CornerAttributes; + mismatch = GeoMismatch::CornerAttributes; return; case AttrDomain::Face: - mismatch = MeshMismatch::FaceAttributes; + mismatch = GeoMismatch::FaceAttributes; + return; + case AttrDomain::Curve: + mismatch = GeoMismatch::CurveAttributes; return; default: BLI_assert_unreachable(); @@ -685,10 +697,10 @@ static bool all_set_sizes_one(const Span set_sizes) * * \returns the type of mismatch that occurred if the mapping couldn't be constructed. */ -static std::optional construct_vertex_mapping(const Mesh &mesh1, - const Mesh &mesh2, - IndexMapping &verts, - IndexMapping &edges) +static std::optional construct_vertex_mapping(const Mesh &mesh1, + const Mesh &mesh2, + IndexMapping &verts, + IndexMapping &edges) { if (all_set_sizes_one(verts.set_sizes)) { /* The vertices are already in one-to-one correspondence. */ @@ -739,7 +751,7 @@ static std::optional construct_vertex_mapping(const Mesh &mesh1, } if (matching_verts.is_empty()) { - return MeshMismatch::EdgeTopology; + return GeoMismatch::EdgeTopology; } /* Update the maps. */ @@ -778,26 +790,26 @@ static std::optional construct_vertex_mapping(const Mesh &mesh1, return std::nullopt; } -std::optional compare_meshes(const Mesh &mesh1, - const Mesh &mesh2, - const float threshold) +std::optional compare_meshes(const Mesh &mesh1, + const Mesh &mesh2, + const float threshold) { /* These will be assumed implicitly later on. */ if (mesh1.verts_num != mesh2.verts_num) { - return MeshMismatch::NumVerts; + return GeoMismatch::NumPoints; } if (mesh1.edges_num != mesh2.edges_num) { - return MeshMismatch::NumEdges; + return GeoMismatch::NumEdges; } if (mesh1.corners_num != mesh2.corners_num) { - return MeshMismatch::NumCorners; + return GeoMismatch::NumCorners; } if (mesh1.faces_num != mesh2.faces_num) { - return MeshMismatch::NumFaces; + return GeoMismatch::NumFaces; } - std::optional mismatch = {}; + std::optional mismatch = {}; const AttributeAccessor mesh1_attributes = mesh1.attributes(); const AttributeAccessor mesh2_attributes = mesh2.attributes(); @@ -811,14 +823,14 @@ std::optional compare_meshes(const Mesh &mesh1, mesh1_attributes, mesh2_attributes, AttrDomain::Point, {}, verts, threshold); if (mismatch) { return mismatch; - }; + } /* We need the maps going the other way as well. */ verts.recalculate_inverse_maps(); IndexMapping edges(mesh1.edges_num); if (!sort_edges(mesh1.edges(), mesh2.edges(), verts, edges)) { - return MeshMismatch::EdgeTopology; + return GeoMismatch::EdgeTopology; } mismatch = sort_domain_using_attributes( @@ -832,11 +844,11 @@ std::optional compare_meshes(const Mesh &mesh1, IndexMapping corners(mesh1.corners_num); if (!sort_corners_based_on_domain(mesh1.corner_verts(), mesh2.corner_verts(), verts, corners)) { - return MeshMismatch::FaceTopology; + return GeoMismatch::FaceTopology; } if (!sort_corners_based_on_domain(mesh1.corner_edges(), mesh2.corner_edges(), edges, corners)) { - return MeshMismatch::FaceTopology; + return GeoMismatch::FaceTopology; } mismatch = sort_domain_using_attributes(mesh1_attributes, @@ -854,7 +866,7 @@ std::optional compare_meshes(const Mesh &mesh1, IndexMapping faces(mesh1.faces_num); if (!sort_faces_based_on_corners(corners, mesh1.face_offsets(), mesh2.face_offsets(), faces)) { - return MeshMismatch::FaceTopology; + return GeoMismatch::FaceTopology; } mismatch = sort_domain_using_attributes( @@ -871,7 +883,7 @@ std::optional compare_meshes(const Mesh &mesh1, /* Now we double check that the other topology maps agree with this vertex mapping. */ if (!sort_edges(mesh1.edges(), mesh2.edges(), verts, edges)) { - return MeshMismatch::EdgeTopology; + return GeoMismatch::EdgeTopology; } make_set_sizes_one(edges); @@ -879,11 +891,11 @@ std::optional compare_meshes(const Mesh &mesh1, edges.recalculate_inverse_maps(); if (!sort_corners_based_on_domain(mesh1.corner_verts(), mesh2.corner_verts(), verts, corners)) { - return MeshMismatch::FaceTopology; + return GeoMismatch::FaceTopology; } if (!sort_corners_based_on_domain(mesh1.corner_edges(), mesh2.corner_edges(), edges, corners)) { - return MeshMismatch::FaceTopology; + return GeoMismatch::FaceTopology; } make_set_sizes_one(corners); @@ -891,7 +903,7 @@ std::optional compare_meshes(const Mesh &mesh1, corners.recalculate_inverse_maps(); if (!sort_faces_based_on_corners(corners, mesh1.face_offsets(), mesh2.face_offsets(), faces)) { - return MeshMismatch::FaceTopology; + return GeoMismatch::FaceTopology; } make_set_sizes_one(faces); @@ -900,19 +912,19 @@ std::optional compare_meshes(const Mesh &mesh1, * are the same. */ for (const int sorted_i : verts.from_sorted1.index_range()) { if (verts.from_sorted1[sorted_i] != verts.from_sorted2[sorted_i]) { - return MeshMismatch::Indices; + return GeoMismatch::Indices; } } /* Skip the test for edges, since a lot of tests actually have different edge indices. *TODO: remove this once those tests have been updated. */ for (const int sorted_i : corners.from_sorted1.index_range()) { if (corners.from_sorted1[sorted_i] != corners.from_sorted2[sorted_i]) { - return MeshMismatch::Indices; + return GeoMismatch::Indices; } } for (const int sorted_i : faces.from_sorted1.index_range()) { if (faces.from_sorted1[sorted_i] != faces.from_sorted2[sorted_i]) { - return MeshMismatch::Indices; + return GeoMismatch::Indices; } } @@ -920,4 +932,92 @@ std::optional compare_meshes(const Mesh &mesh1, return std::nullopt; } -} // namespace blender::bke::compare_meshes +/** + * Sort curves based on their sizes. + */ +static bool sort_curves(const OffsetIndices offset_indices1, + const OffsetIndices offset_indices2, + IndexMapping &curves) +{ + Array curve_point_counts1(offset_indices1.size()); + Array curve_point_counts2(offset_indices2.size()); + offset_indices::copy_group_sizes( + offset_indices1, offset_indices1.index_range(), curve_point_counts1.as_mutable_span()); + offset_indices::copy_group_sizes( + offset_indices2, offset_indices2.index_range(), curve_point_counts2.as_mutable_span()); + sort_per_set_based_on_attributes(curves.set_sizes, + curves.from_sorted1, + curves.from_sorted2, + curve_point_counts1.as_span(), + curve_point_counts2.as_span(), + 0); + const bool curves_sizes_match = update_set_ids(curves.set_ids, + curve_point_counts1.as_span(), + curve_point_counts2.as_span(), + curves.from_sorted1, + curves.from_sorted2, + 0, + 0); + if (!curves_sizes_match) { + return false; + } + update_set_sizes(curves.set_ids, curves.set_sizes); + return true; +} + +std::optional compare_curves(const CurvesGeometry &curves1, + const CurvesGeometry &curves2, + const float threshold) +{ + /* These will be assumed implicitly later on. */ + if (curves1.points_num() != curves2.points_num()) { + return GeoMismatch::NumPoints; + } + if (curves1.curves_num() != curves2.curves_num()) { + return GeoMismatch::NumCurves; + } + + std::optional mismatch = {}; + + const AttributeAccessor curves1_attributes = curves1.attributes(); + const AttributeAccessor curves2_attributes = curves2.attributes(); + mismatch = verify_attributes_compatible(curves1_attributes, curves2_attributes); + if (mismatch) { + return mismatch; + } + + IndexMapping points(curves1.points_num()); + mismatch = sort_domain_using_attributes( + curves1_attributes, curves2_attributes, AttrDomain::Point, {}, points, threshold); + if (mismatch) { + return mismatch; + } + + IndexMapping curves(curves1.curves_num()); + if (!sort_curves(curves1.offsets(), curves2.offsets(), curves)) { + return GeoMismatch::CurveTopology; + } + + mismatch = sort_domain_using_attributes( + curves1_attributes, curves2_attributes, AttrDomain::Curve, {}, curves, threshold); + if (mismatch) { + return mismatch; + } + + for (const int sorted_i : points.from_sorted1.index_range()) { + if (points.from_sorted1[sorted_i] != points.from_sorted2[sorted_i]) { + return GeoMismatch::Indices; + } + } + + for (const int sorted_i : curves.from_sorted1.index_range()) { + if (curves.from_sorted1[sorted_i] != curves.from_sorted2[sorted_i]) { + return GeoMismatch::Indices; + } + } + + /* No mismatches found. */ + return std::nullopt; +} + +} // namespace blender::bke::compare_geometry diff --git a/source/blender/makesrna/intern/rna_curves_api.cc b/source/blender/makesrna/intern/rna_curves_api.cc index e7e8d08747a..989d24cf4ae 100644 --- a/source/blender/makesrna/intern/rna_curves_api.cc +++ b/source/blender/makesrna/intern/rna_curves_api.cc @@ -17,6 +17,7 @@ #ifdef RNA_RUNTIME # include "BKE_curves.hh" +# include "BKE_geometry_compare.hh" # include "BKE_report.hh" # include "BLI_index_mask.hh" @@ -215,6 +216,20 @@ static void rna_Curves_set_types(Curves *curves_id, } } +static const char *rna_Curves_unit_test_compare(Curves *curves1, Curves *curves2, float threshold) +{ + using namespace blender::bke::compare_geometry; + + const std::optional mismatch = compare_curves( + curves1->geometry.wrap(), curves2->geometry.wrap(), threshold); + + if (!mismatch) { + return "Same"; + } + + return mismatch_to_string(mismatch.value()); +} + #else void RNA_api_curves(StructRNA *srna) @@ -300,6 +315,22 @@ void RNA_api_curves(StructRNA *srna) 0, INT_MAX); RNA_def_parameter_flags(parm, PROP_DYNAMIC, ParameterFlag(0)); + + func = RNA_def_function(srna, "unit_test_compare", "rna_Curves_unit_test_compare"); + RNA_def_pointer(func, "curves", "Curves", "", "Curves to compare to"); + RNA_def_float_factor(func, + "threshold", + FLT_EPSILON * 60, + 0.0f, + FLT_MAX, + "Threshold", + "Comparison tolerance threshold", + 0.0f, + FLT_MAX); + /* return value */ + parm = RNA_def_string( + func, "result", "nothing", 64, "Return value", "String description of result of comparison"); + RNA_def_function_return(func, parm); } #endif diff --git a/source/blender/makesrna/intern/rna_mesh_api.cc b/source/blender/makesrna/intern/rna_mesh_api.cc index 206c3fb5ab0..d1fa85ede2a 100644 --- a/source/blender/makesrna/intern/rna_mesh_api.cc +++ b/source/blender/makesrna/intern/rna_mesh_api.cc @@ -25,9 +25,9 @@ # include "BKE_anim_data.hh" # include "BKE_attribute.hh" +# include "BKE_geometry_compare.hh" # include "BKE_mesh.h" # include "BKE_mesh.hh" -# include "BKE_mesh_compare.hh" # include "BKE_mesh_mapping.hh" # include "BKE_mesh_runtime.hh" # include "BKE_mesh_tangent.hh" @@ -41,8 +41,8 @@ static const char *rna_Mesh_unit_test_compare(Mesh *mesh, Mesh *mesh2, float threshold) { - using namespace blender::bke::compare_meshes; - const std::optional mismatch = compare_meshes(*mesh, *mesh2, threshold); + using namespace blender::bke::compare_geometry; + const std::optional mismatch = compare_meshes(*mesh, *mesh2, threshold); if (!mismatch) { return "Same"; diff --git a/tests/python/modules/mesh_test.py b/tests/python/modules/mesh_test.py index a544417d766..e4f27c800ec 100644 --- a/tests/python/modules/mesh_test.py +++ b/tests/python/modules/mesh_test.py @@ -407,7 +407,7 @@ class MeshTest(ABC): if result_mesh == "Same": result_codes['Mesh Comparison'] = (True, result_mesh) - elif allow_index_change and result_mesh == "The meshes are the same up to a change of indices": + elif allow_index_change and result_mesh == "The geometries are the same up to a change of indices": result_codes['Mesh Comparison'] = (True, result_mesh) else: result_codes['Mesh Comparison'] = (False, result_mesh)