Mesh: add index-independent test for mesh equality #112794

Merged
Hans Goudey merged 46 commits from wannes.malfait/blender:mesh_isomorphism into main 2023-11-27 16:10:52 +01:00
8 changed files with 984 additions and 380 deletions

View File

@ -0,0 +1,38 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "BKE_mesh_types.hh"
/** \file
* \ingroup bke
*/
namespace blender::bke::compare_meshes {
wannes.malfait marked this conversation as resolved Outdated

So far I've just used the mesh namespace for functions that take mesh data spans, rather than a full Mesh. Think it's nice to keep it like that (so blender::bke::compare_meshes) unless there's a solid reason to change it later.

So far I've just used the `mesh` namespace for functions that take mesh data spans, rather than a full `Mesh`. Think it's nice to keep it like that (so `blender::bke::compare_meshes`) unless there's a solid reason to change it later.
enum class MeshMismatch : int8_t;
/**
* Convert the mismatch to a human-readable string for display.
*/
const char *mismatch_to_string(const MeshMismatch &mismatch);
wannes.malfait marked this conversation as resolved
Review

It would be nice to add a function that returns whether a MeshMismatch is considered a test failure. That would be the best way for this code to replace BKE_mesh_cmp

It would be nice to add a function that returns whether a `MeshMismatch` is considered a test failure. That would be the best way for this code to replace `BKE_mesh_cmp`
Review

I think any of these mismatches should be considered test failures. If any of these occur, then there is no way to re-index the meshes such that they become equal. So, to test for failures it would be like this:

const optional<MeshMismatch> mismatch = meshes_isomorphic(mesh1, mesh2);
if (mismatch) {
    std::cout << "Test failed: "<<mismatch_to_string(mismatch) << std::endl;
}
I think any of these mismatches should be considered test failures. If any of these occur, then there is no way to re-index the meshes such that they become equal. So, to test for failures it would be like this: ```cpp const optional<MeshMismatch> mismatch = meshes_isomorphic(mesh1, mesh2); if (mismatch) { std::cout << "Test failed: "<<mismatch_to_string(mismatch) << std::endl; } ```
Review

Oh, I missed that, but obvious in retrospect, thanks!

Oh, I missed that, but obvious in retrospect, thanks!
/**
* \brief Checks if the two meshes are different, returning the type of mismatch if any. Changes in
* index order are detected, but treated as a mismatch.
*
* \details Instead of just blindly comparing the two meshes, the code tries to determine if they
* are isomorphic. Two meshes are considered isomorphic, if, for each domain, there is a bijection
* between the two meshes such that the bijections preserve connectivity.
*
* In general, determining if two graphs are isomorphic is a very difficult problem (no polynomial
* time algorithm is known). Because we have more information than just connectivity (attributes),
* we can compute it in a more reasonable time in most cases.
wannes.malfait marked this conversation as resolved Outdated

Is this something you are checking for?

Is this something you are checking for?
*
* \returns The type of mismatch that was detected, if there is any.
wannes.malfait marked this conversation as resolved Outdated

I find the return value a bit backwards, because the function name sounds like it returns something truthy if the meshes are isomorphic, which is not the case here.

I find the return value a bit backwards, because the function name sounds like it returns something truthy if the meshes are isomorphic, which is not the case here.

I really wanted to use something like Result<(), MeshMismatch> in rust, but ended up with this instead. I completely agree with you, and will change the name to meshes_unisomorphic which fits better with the semantics of the return type.

I really wanted to use something like `Result<(), MeshMismatch>` in rust, but ended up with this instead. I completely agree with you, and will change the name to `meshes_unisomorphic` which fits better with the semantics of the return type.
*/
std::optional<MeshMismatch> compare_meshes(const Mesh &mesh1, const Mesh &mesh2, float threshold);
} // namespace blender::bke::compare_meshes

View File

@ -200,6 +200,7 @@ set(SRC
intern/mesh_boolean_convert.cc
intern/mesh_calc_edges.cc
intern/mesh_convert.cc
intern/mesh_compare.cc
intern/mesh_debug.cc
intern/mesh_evaluate.cc
intern/mesh_fair.cc
@ -436,6 +437,7 @@ set(SRC
BKE_mesh.h
BKE_mesh.hh
BKE_mesh_boolean_convert.hh
BKE_mesh_compare.hh
BKE_mesh_fair.hh
BKE_mesh_iterators.hh
BKE_mesh_legacy_convert.hh

View File

@ -396,380 +396,6 @@ IDTypeInfo IDType_ID_ME = {
/*lib_override_apply_post*/ nullptr,
};
enum {
MESHCMP_DVERT_WEIGHTMISMATCH = 1,
MESHCMP_DVERT_GROUPMISMATCH,
MESHCMP_DVERT_TOTGROUPMISMATCH,
MESHCMP_LOOPCOLMISMATCH,
MESHCMP_LOOPUVMISMATCH,
MESHCMP_LOOPMISMATCH,
MESHCMP_POLYVERTMISMATCH,
MESHCMP_POLYMISMATCH,
MESHCMP_EDGEUNKNOWN,
MESHCMP_VERTCOMISMATCH,
MESHCMP_CDLAYERS_MISMATCH,
MESHCMP_ATTRIBUTE_VALUE_MISMATCH,
};
static const char *cmpcode_to_str(int code)
{
switch (code) {
case MESHCMP_DVERT_WEIGHTMISMATCH:
return "Vertex Weight Mismatch";
case MESHCMP_DVERT_GROUPMISMATCH:
return "Vertex Group Mismatch";
case MESHCMP_DVERT_TOTGROUPMISMATCH:
return "Vertex Doesn't Belong To Same Number Of Groups";
case MESHCMP_LOOPCOLMISMATCH:
return "Color Attribute Mismatch";
case MESHCMP_LOOPUVMISMATCH:
return "UV Mismatch";
case MESHCMP_LOOPMISMATCH:
return "Loop Mismatch";
case MESHCMP_POLYVERTMISMATCH:
return "Loop Vert Mismatch In Poly Test";
case MESHCMP_POLYMISMATCH:
return "Loop Vert Mismatch";
case MESHCMP_EDGEUNKNOWN:
return "Edge Mismatch";
case MESHCMP_VERTCOMISMATCH:
return "Vertex Coordinate Mismatch";
case MESHCMP_CDLAYERS_MISMATCH:
return "CustomData Layer Count Mismatch";
case MESHCMP_ATTRIBUTE_VALUE_MISMATCH:
return "Attribute Value Mismatch";
default:
return "Mesh Comparison Code Unknown";
}
}
static bool is_sublayer_name(char const *sublayer_name, char const *name)
{
BLI_assert(strlen(sublayer_name) == 2);
if (name[1] != sublayer_name[0]) {
return false;
}
if (name[2] != sublayer_name[1]) {
return false;
}
if (name[3] != '.') {
return false;
}
return true;
}
static bool is_uv_bool_sublayer(const CustomDataLayer &layer)
{
char const *name = layer.name;
if (name[0] != '.') {
return false;
}
return is_sublayer_name(UV_VERTSEL_NAME, name) || is_sublayer_name(UV_EDGESEL_NAME, name) ||
is_sublayer_name(UV_PINNED_NAME, name);
}
/** Thresh is threshold for comparing vertices, UVs, vertex colors, weights, etc. */
static int customdata_compare(
CustomData *c1, CustomData *c2, const int total_length, Mesh *m1, const float thresh)
{
using namespace blender;
CustomDataLayer *l1, *l2;
int layer_count1 = 0, layer_count2 = 0, j;
const uint64_t cd_mask_non_generic = CD_MASK_MDEFORMVERT;
const uint64_t cd_mask_all_attr = CD_MASK_PROP_ALL | cd_mask_non_generic;
/* The uv selection / pin layers are ignored in the comparisons because
* the original flags they replace were ignored as well. Because of the
* lazy creation of these layers it would need careful handling of the
* test files to compare these layers. For now it has been decided to
* skip them.
*/
for (int i = 0; i < c1->totlayer; i++) {
l1 = &c1->layers[i];
if ((CD_TYPE_AS_MASK(l1->type) & cd_mask_all_attr) && l1->anonymous_id == nullptr &&
!is_uv_bool_sublayer(*l1))
{
layer_count1++;
}
}
for (int i = 0; i < c2->totlayer; i++) {
l2 = &c2->layers[i];
if ((CD_TYPE_AS_MASK(l2->type) & cd_mask_all_attr) && l2->anonymous_id == nullptr &&
!is_uv_bool_sublayer(*l2))
{
layer_count2++;
}
}
if (layer_count1 != layer_count2) {
/* TODO(@HooglyBoogly): Re-enable after tests are updated for material index refactor and UV as
* generic attribute refactor. */
// return MESHCMP_CDLAYERS_MISMATCH;
}
l1 = c1->layers;
l2 = c2->layers;
for (int i1 = 0; i1 < c1->totlayer; i1++) {
l1 = c1->layers + i1;
if (l1->anonymous_id != nullptr || is_uv_bool_sublayer(*l1)) {
continue;
}
bool found_corresponding_layer = false;
for (int i2 = 0; i2 < c2->totlayer; i2++) {
l2 = c2->layers + i2;
if (l1->type != l2->type || !STREQ(l1->name, l2->name) || l2->anonymous_id != nullptr) {
continue;
}
/* At this point `l1` and `l2` have the same name and type, so they should be compared. */
found_corresponding_layer = true;
if (StringRef(l1->name) == ".corner_edge") {
/* TODO(Hans): This attribute wasn't tested before loops were refactored into separate
* corner edges and corner verts attributes. Remove after updating tests. */
continue;
}
switch (l1->type) {
case CD_PROP_INT32_2D: {
blender::int2 *e1 = (blender::int2 *)l1->data;
blender::int2 *e2 = (blender::int2 *)l2->data;
if (StringRef(l1->name) == ".edge_verts") {
int etot = m1->totedge;
Set<OrderedEdge> ordered_edges;
ordered_edges.reserve(etot);
for (const int2 value : Span(e1, etot)) {
ordered_edges.add(value);
}
for (j = 0; j < etot; j++) {
if (!ordered_edges.contains(e2[j])) {
return MESHCMP_EDGEUNKNOWN;
}
}
}
else {
for (j = 0; j < total_length; j++) {
if (e1[j] != e2[j]) {
return MESHCMP_ATTRIBUTE_VALUE_MISMATCH;
}
}
}
break;
}
case CD_PROP_BYTE_COLOR: {
MLoopCol *lp1 = (MLoopCol *)l1->data;
MLoopCol *lp2 = (MLoopCol *)l2->data;
int ltot = m1->totloop;
for (j = 0; j < ltot; j++, lp1++, lp2++) {
if (lp1->r != lp2->r || lp1->g != lp2->g || lp1->b != lp2->b || lp1->a != lp2->a) {
return MESHCMP_LOOPCOLMISMATCH;
}
}
break;
}
case CD_MDEFORMVERT: {
MDeformVert *dv1 = (MDeformVert *)l1->data;
MDeformVert *dv2 = (MDeformVert *)l2->data;
int dvtot = m1->totvert;
for (j = 0; j < dvtot; j++, dv1++, dv2++) {
int k;
MDeformWeight *dw1 = dv1->dw, *dw2 = dv2->dw;
if (dv1->totweight != dv2->totweight) {
return MESHCMP_DVERT_TOTGROUPMISMATCH;
}
for (k = 0; k < dv1->totweight; k++, dw1++, dw2++) {
if (dw1->def_nr != dw2->def_nr) {
return MESHCMP_DVERT_GROUPMISMATCH;
}
if (fabsf(dw1->weight - dw2->weight) > thresh) {
return MESHCMP_DVERT_WEIGHTMISMATCH;
}
}
}
break;
}
case CD_PROP_FLOAT: {
const float *l1_data = (float *)l1->data;
const float *l2_data = (float *)l2->data;
for (int i = 0; i < total_length; i++) {
if (compare_threshold_relative(l1_data[i], l2_data[i], thresh)) {
return MESHCMP_ATTRIBUTE_VALUE_MISMATCH;
}
}
break;
}
case CD_PROP_FLOAT2: {
const float(*l1_data)[2] = (float(*)[2])l1->data;
const float(*l2_data)[2] = (float(*)[2])l2->data;
for (int i = 0; i < total_length; i++) {
if (compare_threshold_relative(l1_data[i][0], l2_data[i][0], thresh)) {
return MESHCMP_ATTRIBUTE_VALUE_MISMATCH;
}
if (compare_threshold_relative(l1_data[i][1], l2_data[i][1], thresh)) {
return MESHCMP_ATTRIBUTE_VALUE_MISMATCH;
}
}
break;
}
case CD_PROP_FLOAT3: {
const float(*l1_data)[3] = (float(*)[3])l1->data;
const float(*l2_data)[3] = (float(*)[3])l2->data;
for (int i = 0; i < total_length; i++) {
if (compare_threshold_relative(l1_data[i][0], l2_data[i][0], thresh)) {
return MESHCMP_ATTRIBUTE_VALUE_MISMATCH;
}
if (compare_threshold_relative(l1_data[i][1], l2_data[i][1], thresh)) {
return MESHCMP_ATTRIBUTE_VALUE_MISMATCH;
}
if (compare_threshold_relative(l1_data[i][2], l2_data[i][2], thresh)) {
return MESHCMP_ATTRIBUTE_VALUE_MISMATCH;
}
}
break;
}
case CD_PROP_QUATERNION: {
const float(*l1_data)[4] = (float(*)[4])l1->data;
const float(*l2_data)[4] = (float(*)[4])l2->data;
for (int i = 0; i < total_length; i++) {
if (compare_threshold_relative(l1_data[i][0], l2_data[i][0], thresh)) {
return MESHCMP_ATTRIBUTE_VALUE_MISMATCH;
}
if (compare_threshold_relative(l1_data[i][1], l2_data[i][1], thresh)) {
return MESHCMP_ATTRIBUTE_VALUE_MISMATCH;
}
if (compare_threshold_relative(l1_data[i][2], l2_data[i][2], thresh)) {
return MESHCMP_ATTRIBUTE_VALUE_MISMATCH;
}
if (compare_threshold_relative(l1_data[i][3], l2_data[i][3], thresh)) {
return MESHCMP_ATTRIBUTE_VALUE_MISMATCH;
}
}
break;
}
case CD_PROP_INT32: {
const int *l1_data = (int *)l1->data;
const int *l2_data = (int *)l2->data;
for (int i = 0; i < total_length; i++) {
if (l1_data[i] != l2_data[i]) {
return MESHCMP_ATTRIBUTE_VALUE_MISMATCH;
}
}
break;
}
case CD_PROP_INT8: {
const int8_t *l1_data = (int8_t *)l1->data;
const int8_t *l2_data = (int8_t *)l2->data;
for (int i = 0; i < total_length; i++) {
if (l1_data[i] != l2_data[i]) {
return MESHCMP_ATTRIBUTE_VALUE_MISMATCH;
}
}
break;
}
case CD_PROP_BOOL: {
const bool *l1_data = (bool *)l1->data;
const bool *l2_data = (bool *)l2->data;
for (int i = 0; i < total_length; i++) {
if (l1_data[i] != l2_data[i]) {
return MESHCMP_ATTRIBUTE_VALUE_MISMATCH;
}
}
break;
}
case CD_PROP_COLOR: {
const MPropCol *l1_data = (MPropCol *)l1->data;
const MPropCol *l2_data = (MPropCol *)l2->data;
for (int i = 0; i < total_length; i++) {
for (j = 0; j < 4; j++) {
if (compare_threshold_relative(l1_data[i].color[j], l2_data[i].color[j], thresh)) {
return MESHCMP_ATTRIBUTE_VALUE_MISMATCH;
}
}
}
break;
}
default: {
break;
}
}
}
if (!found_corresponding_layer) {
if ((uint64_t(1) << l1->type) & CD_MASK_PROP_ALL) {
return MESHCMP_CDLAYERS_MISMATCH;
}
}
}
return 0;
}
const char *BKE_mesh_cmp(Mesh *me1, Mesh *me2, float thresh)
{
int c;
if (!me1 || !me2) {
return "Requires two input meshes";
}
if (me1->totvert != me2->totvert) {
return "Number of verts don't match";
}
if (me1->totedge != me2->totedge) {
return "Number of edges don't match";
}
if (me1->faces_num != me2->faces_num) {
return "Number of faces don't match";
}
if (me1->totloop != me2->totloop) {
return "Number of loops don't match";
}
if (!std::equal(
me1->face_offsets().begin(), me1->face_offsets().end(), me2->face_offsets().begin()))
{
return "Face sizes don't match";
}
if ((c = customdata_compare(&me1->vert_data, &me2->vert_data, me1->totvert, me1, thresh))) {
return cmpcode_to_str(c);
}
if ((c = customdata_compare(&me1->edge_data, &me2->edge_data, me1->totedge, me1, thresh))) {
return cmpcode_to_str(c);
}
if ((c = customdata_compare(&me1->loop_data, &me2->loop_data, me1->totloop, me1, thresh))) {
return cmpcode_to_str(c);
}
if ((c = customdata_compare(&me1->face_data, &me2->face_data, me1->faces_num, me1, thresh))) {
return cmpcode_to_str(c);
}
return nullptr;
}
bool BKE_mesh_attribute_required(const char *name)
{
return ELEM(StringRef(name), "position", ".corner_vert", ".corner_edge", ".edge_verts");

View File

@ -0,0 +1,908 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_array.hh"
#include "BLI_math_base.h"
#include "BLI_ordered_edge.hh"
#include "BLI_span.hh"
#include "BKE_attribute.hh"
#include "BKE_attribute_math.hh"
#include "BKE_mesh.hh"
#include "BKE_mesh_mapping.hh"
#include "BKE_mesh_compare.hh"
namespace blender::bke::compare_meshes {
enum class MeshMismatch : int8_t {
NumVerts, /* The number of vertices 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. */
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. */
EdgeTopology, /* The edge topology is different. */
FaceTopology, /* The face 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. */
};
const char *mismatch_to_string(const MeshMismatch &mismatch)
{
switch (mismatch) {
case MeshMismatch::NumVerts:
return "The number of vertices is different";
case MeshMismatch::NumEdges:
return "The number of edges is different";
case MeshMismatch::NumCorners:
return "The number of corners is different";
case MeshMismatch::NumFaces:
return "The number of faces is different";
case MeshMismatch::VertexAttributes:
return "Some values of the vertex attributes are different";
case MeshMismatch::EdgeAttributes:
return "Some values of the edge attributes are different";
case MeshMismatch::CornerAttributes:
return "Some values of the corner attributes are different";
case MeshMismatch::FaceAttributes:
return "Some values of the face attributes are different";
case MeshMismatch::EdgeTopology:
return "The edge topology is different";
case MeshMismatch::FaceTopology:
return "The face topology is different";
case MeshMismatch::Attributes:
return "The sets of attribute ids are different";
case MeshMismatch::AttributeTypes:
return "Some attributes with the same name have different types";
wannes.malfait marked this conversation as resolved
Review

Don't use a default case if you want to get compiler warnings when cases are missing.

Don't use a `default` case if you want to get compiler warnings when cases are missing.
case MeshMismatch::Indices:
return "The meshes are the same up to a change of indices";
}
BLI_assert_unreachable();
return "";
}
class IndexMapping {
private:
void calculate_inverse_map(const Span<int> map, MutableSpan<int> inverted_map)
{
for (const int i : map.index_range()) {
inverted_map[map[i]] = i;
}
}
public:
Array<int> from_sorted1;
Array<int> from_sorted2;
Array<int> to_sorted1;
Array<int> to_sorted2;
Array<int> set_ids;
Array<int> set_sizes;
IndexMapping(const int64_t domain_size)
{
to_sorted1 = Array<int>(domain_size);
to_sorted2 = Array<int>(domain_size);
from_sorted1 = Array<int>(domain_size);
from_sorted2 = Array<int>(domain_size);
set_ids = Array<int>(domain_size);
set_sizes = Array<int>(domain_size);
std::iota(from_sorted1.begin(), from_sorted1.end(), 0);
std::iota(from_sorted2.begin(), from_sorted2.end(), 0);
std::iota(to_sorted1.begin(), to_sorted1.end(), 0);
std::iota(to_sorted2.begin(), to_sorted2.end(), 0);
set_ids.fill(0);
set_sizes.fill(set_ids.size());
}
/**
* Update the "to_sorted" maps by inverting the "from_sorted" maps.
*/
void recalculate_inverse_maps()
{
calculate_inverse_map(from_sorted1, to_sorted1);
calculate_inverse_map(from_sorted2, to_sorted2);
}
};
/**
* Sort the indices using the values. For vectors of floats, the sorting happens based on the given
* component.
*/
template<typename T>
static void sort_indices(MutableSpan<int> indices, const Span<T> values, const int component_i)
{
/* We need to have an appropriate comparison function, depending on the type. */
std::stable_sort(indices.begin(), indices.end(), [&](int i1, int i2) {
const T value1 = values[i1];
const T value2 = values[i2];
if constexpr (is_same_any_v<T, int, float, bool, int8_t, OrderedEdge>) {
/* These types are already comparable. */
return value1 < value2;
}
if constexpr (is_same_any_v<T, float2, float3, ColorGeometry4f>) {
return value1[component_i] < value2[component_i];
}
if constexpr (std::is_same_v<T, math::Quaternion>) {
const float4 value1 = float4(value1);
const float4 value2 = float4(value2);
return value1[component_i] < value2[component_i];
}
if constexpr (std::is_same_v<T, int2>) {
for (int i = 0; i < 2; i++) {
if (value1[i] != value2[i]) {
return value1[i] < value2[i];
}
}
return false;
}
if constexpr (std::is_same_v<T, ColorGeometry4b>) {
for (int i = 0; i < 4; i++) {
if (value1[i] != value2[i]) {
return value1[i] < value2[i];
}
}
return false;
}
BLI_assert_unreachable();
});
}
/**
* Sort the indices using the set ids of the values.
*/
static void sort_indices_with_id_maps(MutableSpan<int> indices,
const Span<int> values,
const Span<int> values_to_sorted,
const Span<int> set_ids)
{
std::stable_sort(indices.begin(), indices.end(), [&](int i1, int i2) {
return set_ids[values_to_sorted[values[i1]]] < set_ids[values_to_sorted[values[i2]]];
});
}
/* Sort the elements in each set based on the attribute values. */
template<typename T>
static void sort_per_set_based_on_attributes(const Span<int> set_sizes,
MutableSpan<int> sorted_to_domain1,
MutableSpan<int> sorted_to_domain2,
const Span<T> values1,
const Span<T> values2,
const int component_i)
{
int i = 0;
while (i < set_sizes.size()) {
const int set_size = set_sizes[i];
if (set_size == 1) {
/* No need to sort anymore. */
i += 1;
continue;
}
sort_indices(sorted_to_domain1.slice(IndexRange(i, set_size)), values1, component_i);
sort_indices(sorted_to_domain2.slice(IndexRange(i, set_size)), values2, component_i);
i += set_size;
}
}
/* Sort the elements in each set based on the set ids of the values. */
static void sort_per_set_with_id_maps(const Span<int> set_sizes,
const Span<int> values1,
const Span<int> values2,
const Span<int> values1_to_sorted,
const Span<int> values2_to_sorted,
const Span<int> value_set_ids,
MutableSpan<int> sorted_to_domain1,
MutableSpan<int> sorted_to_domain2)
{
int i = 0;
while (i < sorted_to_domain1.size()) {
const int set_size = set_sizes[i];
if (set_size == 1) {
/* No need to sort anymore. */
i += 1;
continue;
}
sort_indices_with_id_maps(sorted_to_domain1.slice(IndexRange(i, set_size)),
values1,
values1_to_sorted,
value_set_ids);
sort_indices_with_id_maps(sorted_to_domain2.slice(IndexRange(i, set_size)),
values2,
values2_to_sorted,
value_set_ids);
i += set_size;
}
}
/**
* Checks if the two values are different. For float types, the equality is checked based on a
* treshold.
*/
template<typename T>
static bool values_different(const T value1,
const T value2,
const float threshold,
const int component_i)
{
if constexpr (is_same_any_v<T, int, int2, bool, int8_t, OrderedEdge, ColorGeometry4b>) {
/* These types already have a good implementation. */
return value1 != value2;
wannes.malfait marked this conversation as resolved Outdated

We have an is_same_any_v utility that you might want to use.

We have an `is_same_any_v` utility that you might want to use.
}
/* The other types are based on floats. */
if constexpr (std::is_same_v<T, float>) {
return compare_threshold_relative(value1, value2, threshold);
}
if constexpr (is_same_any_v<T, float2, float3, ColorGeometry4f>) {
return compare_threshold_relative(value1[component_i], value2[component_i], threshold);
}
if constexpr (std::is_same_v<T, math::Quaternion>) {
const float4 value1_f = float4(value1);
const float4 value2_f = float4(value2);
return compare_threshold_relative(value1_f[component_i], value2_f[component_i], threshold);
}
BLI_assert_unreachable();
}
/**
* Split the sets into smaller sets based on the sorted attribute values.
wannes.malfait marked this conversation as resolved Outdated

Lots of else after return in this function

Lots of else after return in this function
*
* \returns false if the attributes don't line up.
*/
template<typename T>
static bool update_set_ids(MutableSpan<int> set_ids,
const Span<T> values1,
const Span<T> values2,
const Span<int> sorted_to_values1,
MutableSpan<int> sorted_to_values2,
const float threshold,
const int component_i)
{
/* Due to the way the sorting works, there could be a slightly bigger difference. */
const float value_threshold = 5 * threshold;
if (set_ids.is_empty()) {
return true;
}
T previous = values1[0];
int set_id = 0;
for (const int i : values1.index_range()) {
wannes.malfait marked this conversation as resolved Outdated

Use functional style casts here: float4(value1);

Use functional style casts here: `float4(value1);`
const T value1 = values1[sorted_to_values1[i]];
const T value2 = values2[sorted_to_values2[i]];
if (values_different(value1, value2, value_threshold, component_i)) {
/* They should be the same after sorting. */
return false;
}
if ((values_different(previous, value1, value_threshold, component_i) &&
values_different(previous, value2, value_threshold, component_i)) ||
set_ids[i] == i)
{
/* Different value, or this was already a different set. */
set_id = i;
previous = value1;
}
set_ids[i] = set_id;
}
return true;
}
/**
* Split the sets into smaller sets based on the set ids of the sorted values.
*
* \returns false if the attributes don't line up.
*/
static bool update_set_ids_with_id_maps(MutableSpan<int> set_ids,
const Span<int> domain_to_values1,
const Span<int> domain_to_values2,
const Span<int> values1_to_sorted,
const Span<int> values2_to_sorted,
const Span<int> value_set_ids,
const Span<int> sorted_to_domain1,
const Span<int> sorted_to_domain2)
{
if (set_ids.is_empty()) {
return true;
}
int previous = value_set_ids[values1_to_sorted[domain_to_values1[sorted_to_domain1[0]]]];
int set_id = 0;
for (const int i : sorted_to_domain1.index_range()) {
const int value_id1 =
value_set_ids[values1_to_sorted[domain_to_values1[sorted_to_domain1[i]]]];
const int value_id2 =
value_set_ids[values2_to_sorted[domain_to_values2[sorted_to_domain2[i]]]];
if (value_id1 != value_id2) {
/* They should be the same after sorting. */
return false;
}
if (value_id1 != previous || set_ids[i] == i) {
/* Different value, or this was already a different set. */
set_id = i;
previous = value_id1;
}
set_ids[i] = set_id;
}
return true;
}
/**
* Update set sizes, using the updated set ids.
*/
static void update_set_sizes(const Span<int> set_ids, MutableSpan<int> set_sizes)
{
int i = set_ids.size() - 1;
while (i >= 0) {
/* The id of a set is the index of its first element, so the size can be computed as the index
* of the last element minus the id (== index of first element) + 1. */
int set_size = i - set_ids[i] + 1;
/* Set the set size for each element in the set. */
for (int k = i - set_size + 1; k <= i; k++) {
set_sizes[k] = set_size;
}
i -= set_size;
}
}
static void edges_from_vertex_sets(const Span<int2> edges,
const Span<int> verts_to_sorted,
const Span<int> vertex_set_ids,
MutableSpan<OrderedEdge> r_edges)
{
for (const int i : r_edges.index_range()) {
const int2 e = edges[i];
r_edges[i] = OrderedEdge(vertex_set_ids[verts_to_sorted[e.x]],
vertex_set_ids[verts_to_sorted[e.y]]);
}
}
/**
* Sort the edges based on the sorted vertex set ids.
*/
static bool sort_edges(const Span<int2> edges1,
const Span<int2> edges2,
const IndexMapping &verts,
IndexMapping &edges)
{
/* Need `NoInitialization()` because OrderedEdge is not default constructible. */
Array<OrderedEdge> ordered_edges1(edges1.size(), NoInitialization());
Array<OrderedEdge> ordered_edges2(edges2.size(), NoInitialization());
edges_from_vertex_sets(edges1, verts.to_sorted1, verts.set_ids, ordered_edges1);
edges_from_vertex_sets(edges2, verts.to_sorted2, verts.set_ids, ordered_edges2);
sort_per_set_based_on_attributes(edges.set_sizes,
edges.from_sorted1,
edges.from_sorted2,
ordered_edges1.as_span(),
ordered_edges2.as_span(),
0);
const bool edges_match = update_set_ids(edges.set_ids,
ordered_edges1.as_span(),
ordered_edges2.as_span(),
edges.from_sorted1,
edges.from_sorted2,
0,
0);
if (!edges_match) {
return false;
}
update_set_sizes(edges.set_ids, edges.set_sizes);
return true;
}
/**
* Sort the corners based on the sorted vertex/edge set ids.
*/
static bool sort_corners_based_on_domain(const Span<int> corner_domain1,
const Span<int> corner_domain2,
const IndexMapping &domain,
IndexMapping &corners)
{
sort_per_set_with_id_maps(corners.set_sizes,
corner_domain1,
corner_domain2,
domain.to_sorted1,
domain.to_sorted2,
domain.set_ids,
corners.from_sorted1,
corners.from_sorted2);
const bool corners_line_up = update_set_ids_with_id_maps(corners.set_ids,
corner_domain1,
corner_domain2,
domain.to_sorted1,
domain.to_sorted2,
domain.set_ids,
corners.from_sorted1,
corners.from_sorted2);
if (!corners_line_up) {
return false;
}
update_set_sizes(corners.set_ids, corners.set_sizes);
return true;
}
static void calc_smallest_corner_ids(const Span<int> face_offsets,
const Span<int> corners_to_sorted,
const Span<int> corner_set_ids,
MutableSpan<int> smallest_corner_ids)
{
for (const int face_i : smallest_corner_ids.index_range()) {
const int face_start = face_offsets[face_i];
const int face_end = face_offsets[face_i + 1];
int smallest = corner_set_ids[corners_to_sorted[face_start]];
const IndexRange corners = IndexRange(face_start, face_end - face_start);
for (const int corner_i : corners.drop_front(1)) {
const int corner_id = corner_set_ids[corners_to_sorted[corner_i]];
if (corner_id < smallest) {
smallest = corner_id;
}
}
smallest_corner_ids[face_i] = smallest;
}
}
/**
* Sort the faces using the sorted corner set ids.
*/
static bool sort_faces_based_on_corners(const IndexMapping &corners,
const Span<int> face_offsets1,
const Span<int> face_offsets2,
IndexMapping &faces)
{
/* The smallest corner set id, per face. */
Array<int> smallest_corner_ids1(faces.from_sorted1.size());
Array<int> smallest_corner_ids2(faces.from_sorted2.size());
calc_smallest_corner_ids(
face_offsets1, corners.to_sorted1, corners.set_ids, smallest_corner_ids1);
calc_smallest_corner_ids(
face_offsets2, corners.to_sorted2, corners.set_ids, smallest_corner_ids2);
sort_per_set_based_on_attributes(faces.set_sizes,
faces.from_sorted1,
faces.from_sorted2,
smallest_corner_ids1.as_span(),
smallest_corner_ids2.as_span(),
0);
const bool faces_line_up = update_set_ids(faces.set_ids,
smallest_corner_ids1.as_span(),
smallest_corner_ids2.as_span(),
faces.from_sorted1,
faces.from_sorted2,
0,
0);
if (!faces_line_up) {
return false;
}
update_set_sizes(faces.set_ids, faces.set_sizes);
return true;
}
/*
* The uv selection / pin layers are ignored in the comparisons because
* the original flags they replace were ignored as well. Because of the
* lazy creation of these layers it would need careful handling of the
* test files to compare these layers. For now it has been decided to
* skip them.
*/
static bool ignored_attribute(const AttributeIDRef &id)
{
return id.is_anonymous() || id.name().startswith(".vs.") || id.name().startswith(".es.") ||
id.name().startswith(".pn.");
}
/**
* Verify that both meshes have the same attributes:
* - Same names
* - Same domains
* - Same types
*/
static std::optional<MeshMismatch> verify_attributes_compatible(
const AttributeAccessor &mesh1_attributes, const AttributeAccessor &mesh2_attributes)
{
Set<AttributeIDRef> mesh1_attribute_ids = mesh1_attributes.all_ids();
Set<AttributeIDRef> mesh2_attribute_ids = mesh2_attributes.all_ids();
mesh1_attribute_ids.remove_if(ignored_attribute);
mesh2_attribute_ids.remove_if(ignored_attribute);
if (mesh1_attribute_ids != mesh2_attribute_ids) {
/* Disabled for now due to tests not being up to date. */
// return MeshMismatch::Attributes;
}
for (const AttributeIDRef &id : mesh1_attribute_ids) {
GAttributeReader reader1 = mesh1_attributes.lookup(id);
GAttributeReader reader2 = mesh2_attributes.lookup(id);
if (reader1.domain != reader2.domain || reader1.varray.type() != reader2.varray.type()) {
return MeshMismatch::AttributeTypes;
}
}
return std::nullopt;
}
/**
* 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.
*/
static std::optional<MeshMismatch> sort_domain_using_attributes(
const AttributeAccessor &mesh1_attributes,
const AttributeAccessor &mesh2_attributes,
const eAttrDomain domain,
const Span<StringRef> excluded_attributes,
IndexMapping &maps,
const float threshold)
{
/* We only need the ids from one mesh, since we know they have the same attributes. */
Set<AttributeIDRef> attribute_ids = mesh1_attributes.all_ids();
for (const StringRef name : excluded_attributes) {
attribute_ids.remove(name);
}
attribute_ids.remove_if(ignored_attribute);
for (const AttributeIDRef &id : attribute_ids) {
if (!mesh2_attributes.contains(id)) {
/* Only needed right now since some test meshes don't have the same attributes. */
return MeshMismatch::Attributes;
}
GAttributeReader reader1 = mesh1_attributes.lookup(id);
GAttributeReader reader2 = mesh2_attributes.lookup(id);
if (reader1.domain != domain) {
/* We only look at attributes of the given domain. */
continue;
}
std::optional<MeshMismatch> mismatch = {};
attribute_math::convert_to_static_type(reader1.varray.type(), [&](auto dummy) {
using T = decltype(dummy);
const VArraySpan<T> values1 = reader1.varray.typed<T>();
const VArraySpan<T> values2 = reader2.varray.typed<T>();
/* Because sorting of float vectors is not very stable, we do a separate sort per component,
* re-computing the set ids each time. */
int num_loops = 1;
if constexpr (std::is_same_v<T, float2>) {
num_loops = 2;
}
else if constexpr (std::is_same_v<T, float3>) {
num_loops = 3;
}
else if constexpr (is_same_any_v<T, math::Quaternion, ColorGeometry4f>) {
num_loops = 4;
}
for (const int component_i : IndexRange(num_loops)) {
sort_per_set_based_on_attributes(
maps.set_sizes, maps.from_sorted1, maps.from_sorted2, values1, values2, component_i);
const bool attributes_line_up = update_set_ids(maps.set_ids,
values1,
values2,
maps.from_sorted1,
maps.from_sorted2,
threshold,
component_i);
if (!attributes_line_up) {
switch (domain) {
case ATTR_DOMAIN_POINT:
mismatch = MeshMismatch::VertexAttributes;
return;
case ATTR_DOMAIN_EDGE:
mismatch = MeshMismatch::EdgeAttributes;
return;
case ATTR_DOMAIN_CORNER:
mismatch = MeshMismatch::CornerAttributes;
return;
case ATTR_DOMAIN_FACE:
mismatch = MeshMismatch::FaceAttributes;
return;
default:
BLI_assert_unreachable();
break;
}
return;
}
update_set_sizes(maps.set_ids, maps.set_sizes);
}
});
if (mismatch) {
return mismatch;
}
}
return std::nullopt;
}
/* When all checks are done, it's possible that some set sizes are still not one e.g, when you have
* two loose verts at the same position they are indistinguishable. This makes all the set ID's one
* by choosing a match. If possible, the match is chosen such that they have the same unsorted
* index.
*/
static void make_set_sizes_one(IndexMapping &indices)
{
for (const int sorted_i : indices.set_sizes.index_range()) {
if (indices.set_sizes[sorted_i] == 1) {
continue;
}
int match = sorted_i;
for (const int other_index :
IndexRange(indices.set_ids[sorted_i], indices.set_sizes[sorted_i]))
{
if (indices.from_sorted1[sorted_i] == indices.from_sorted2[other_index]) {
match = other_index;
break;
}
}
std::swap(indices.from_sorted2[sorted_i], indices.from_sorted2[match]);
for (const int other_set_i :
IndexRange(indices.set_ids[sorted_i], indices.set_sizes[sorted_i]))
{
/* New first element, since this one is now in a new set. */
indices.set_ids[other_set_i] = sorted_i + 1;
indices.set_sizes[other_set_i] -= 1;
}
indices.set_ids[sorted_i] = sorted_i;
indices.set_sizes[sorted_i] = 1;
}
}
static bool all_set_sizes_one(const Span<int> set_sizes)
{
for (const int size : set_sizes) {
if (size != 1) {
return false;
}
}
return true;
}
/**
* Tries to construct a (bijective) mapping from the vertices of the first mesh to the
* vertices of the second mesh, such that:
* - Edge topology is preserved under this mapping, i.e. if v_1 and v_2 are on an edge in mesh1
* then f(v_1) and f(v_2) are on an edge in mesh2.
* - Face topology is preserved under this mapping, i.e. if v_1, ..., v_n form a face in mesh1,
* then f(v_1), ..., f(v_n) form a face in mesh2.
* - The mapping preserves all vertex attributes, i.e. if attr is some vertex attribute on mesh1,
* then for every vertex v of mesh1, attr(v) = attr(f(v)).
*
* \returns the type of mismatch that occured if the mapping couldn't be constructed.
*/
static std::optional<MeshMismatch> construct_vertex_mapping(const Mesh &mesh1,
const Mesh &mesh2,
IndexMapping &verts,
IndexMapping &edges)
{
wannes.malfait marked this conversation as resolved Outdated

BLI_assert_unreachable

`BLI_assert_unreachable`
if (all_set_sizes_one(verts.set_sizes)) {
/* The vertices are already in one-to-one correspondence. */
return std::nullopt;
}
/* Since we are not yet able to distiniguish all vertices based on their attributes alone, we
need to use the edge topology. */
Array<int> vert_to_edge_offsets1;
Array<int> vert_to_edge_indices1;
const GroupedSpan<int> vert_to_edge_map1 = mesh::build_vert_to_edge_map(
mesh1.edges(), mesh1.totvert, vert_to_edge_offsets1, vert_to_edge_indices1);
Array<int> vert_to_edge_offsets2;
Array<int> vert_to_edge_indices2;
const GroupedSpan<int> vert_to_edge_map2 = mesh::build_vert_to_edge_map(
mesh2.edges(), mesh2.totvert, vert_to_edge_offsets2, vert_to_edge_indices2);
for (const int sorted_i : verts.from_sorted1.index_range()) {
const int vert1 = verts.from_sorted1[sorted_i];
Vector<int> matching_verts;
const Span<int> edges1 = vert_to_edge_map1[vert1];
/* Try to find all matching vertices. We know that it will be in the same vertex set, if it
* exists. */
for (const int index_in_set : IndexRange(verts.set_sizes[sorted_i])) {
/* The set id is the index of its first element. */
const int vert2 = verts.from_sorted2[verts.set_ids[sorted_i] + index_in_set];
const Span<int> edges2 = vert_to_edge_map2[vert2];
if (edges1.size() != edges2.size()) {
continue;
}
bool vertex_matches = true;
for (const int edge1 : edges1) {
bool found_matching_edge = false;
for (const int edge2 : edges2) {
if (edges.set_ids[edges.to_sorted1[edge1]] == edges.set_ids[edges.to_sorted2[edge2]]) {
found_matching_edge = true;
break;
}
}
if (!found_matching_edge) {
vertex_matches = false;
break;
}
}
wannes.malfait marked this conversation as resolved Outdated

I this printing can be removed

I this printing can be removed
if (vertex_matches) {
matching_verts.append(index_in_set);
}
}
if (matching_verts.is_empty()) {
return MeshMismatch::EdgeTopology;
}
/* Update the maps. */
/* In principle, we should make sure that there is exactly one matching vertex. If the mesh is
* of good enough quality, that will always be the case. In other cases we just assume that any
* choice will be valid. Otherwise, the logic becomes a lot more difficult. Because we want to
* test for mesh equality as well, we try to pick the matching vert with the same index. */
int index_in_set = matching_verts.first();
for (const int other_index_in_set : matching_verts) {
const int other_sorted_index = verts.set_ids[sorted_i] + other_index_in_set;
if (verts.from_sorted1[sorted_i] == verts.from_sorted2[other_sorted_index]) {
index_in_set = other_index_in_set;
break;
}
}
std::swap(verts.from_sorted2[sorted_i],
verts.from_sorted2[verts.set_ids[sorted_i] + index_in_set]);
for (const int other_set_i : IndexRange(verts.set_ids[sorted_i], verts.set_sizes[sorted_i])) {
/* New first element, since this one is now in a new set. */
verts.set_ids[other_set_i] = sorted_i + 1;
verts.set_sizes[other_set_i] -= 1;
}
verts.set_ids[sorted_i] = sorted_i;
verts.set_sizes[sorted_i] = 1;
}
BLI_assert(all_set_sizes_one(verts.set_sizes));
verts.recalculate_inverse_maps();
/* The bijective mapping is now given by composing `verts.to_sorted1` with `verts.from_sorted2`,
* or vice versa. Since we don't actually need the mapping (we just care that it exists), we
* don't construct it here. */
return std::nullopt;
}
std::optional<MeshMismatch> compare_meshes(const Mesh &mesh1,
const Mesh &mesh2,
const float threshold)
{
/* These will be assumed implicitly later on. */
if (mesh1.totvert != mesh2.totvert) {
return MeshMismatch::NumVerts;
}
if (mesh1.totedge != mesh2.totedge) {
return MeshMismatch::NumEdges;
}
if (mesh1.totloop != mesh2.totloop) {
return MeshMismatch::NumCorners;
}
if (mesh1.faces_num != mesh2.faces_num) {
return MeshMismatch::NumFaces;
}
std::optional<MeshMismatch> mismatch = {};
const AttributeAccessor mesh1_attributes = mesh1.attributes();
const AttributeAccessor mesh2_attributes = mesh2.attributes();
mismatch = verify_attributes_compatible(mesh1_attributes, mesh2_attributes);
if (mismatch) {
return mismatch;
}
IndexMapping verts(mesh1.totvert);
mismatch = sort_domain_using_attributes(
mesh1_attributes, mesh2_attributes, ATTR_DOMAIN_POINT, {}, verts, threshold);
if (mismatch) {
return mismatch;
};
/* We need the maps going the other way as well. */
verts.recalculate_inverse_maps();
IndexMapping edges(mesh1.totedge);
if (!sort_edges(mesh1.edges(), mesh2.edges(), verts, edges)) {
return MeshMismatch::EdgeTopology;
}
mismatch = sort_domain_using_attributes(
mesh1_attributes, mesh2_attributes, ATTR_DOMAIN_EDGE, {".edge_verts"}, edges, threshold);
if (mismatch) {
return mismatch;
};
/* We need the maps going the other way as well. */
edges.recalculate_inverse_maps();
IndexMapping corners(mesh1.totloop);
if (!sort_corners_based_on_domain(mesh1.corner_verts(), mesh2.corner_verts(), verts, corners)) {
return MeshMismatch::FaceTopology;
}
if (!sort_corners_based_on_domain(mesh1.corner_edges(), mesh2.corner_edges(), edges, corners)) {
return MeshMismatch::FaceTopology;
}
mismatch = sort_domain_using_attributes(mesh1_attributes,
mesh2_attributes,
ATTR_DOMAIN_CORNER,
{".corner_vert", ".corner_edge"},
corners,
threshold);
if (mismatch) {
return mismatch;
};
/* We need the maps going the other way as well. */
corners.recalculate_inverse_maps();
IndexMapping faces(mesh1.faces_num);
if (!sort_faces_based_on_corners(corners, mesh1.face_offsets(), mesh2.face_offsets(), faces)) {
return MeshMismatch::FaceTopology;
}
mismatch = sort_domain_using_attributes(
mesh1_attributes, mesh2_attributes, ATTR_DOMAIN_FACE, {}, faces, threshold);
wannes.malfait marked this conversation as resolved Outdated

std::nullopt

`std::nullopt`
if (mismatch) {
return mismatch;
};
mismatch = construct_vertex_mapping(mesh1, mesh2, verts, edges);
if (mismatch) {
return mismatch;
}
/* 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;
}
make_set_sizes_one(edges);
edges.recalculate_inverse_maps();
if (!sort_corners_based_on_domain(mesh1.corner_verts(), mesh2.corner_verts(), verts, corners)) {
return MeshMismatch::FaceTopology;
}
if (!sort_corners_based_on_domain(mesh1.corner_edges(), mesh2.corner_edges(), edges, corners)) {
return MeshMismatch::FaceTopology;
}
make_set_sizes_one(corners);
corners.recalculate_inverse_maps();
if (!sort_faces_based_on_corners(corners, mesh1.face_offsets(), mesh2.face_offsets(), faces)) {
return MeshMismatch::FaceTopology;
}
make_set_sizes_one(faces);
/* The meshes are isomorphic, we now just need to determine if they are equal i.e., the indices
* 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;
}
}
/* 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;
}
}
for (const int sorted_i : faces.from_sorted1.index_range()) {
if (faces.from_sorted1[sorted_i] != faces.from_sorted2[sorted_i]) {
return MeshMismatch::Indices;
}
}
/* No mismatches found. */
return std::nullopt;
}
} // namespace blender::bke::mesh

View File

@ -38,10 +38,23 @@ struct OrderedEdge {
friend bool operator==(const OrderedEdge &e1, const OrderedEdge &e2)
wannes.malfait marked this conversation as resolved Outdated

The comparison operators should not be moved out of the header because they need to be inlined.
The operator<< should be moved to the implementation file though.

The comparison operators should not be moved out of the header because they need to be inlined. The `operator<<` should be moved to the implementation file though.
{
BLI_assert(e1.v_low < e1.v_high);
BLI_assert(e2.v_low < e2.v_high);
return e1.v_low == e2.v_low && e1.v_high == e2.v_high;
}
friend bool operator!=(const OrderedEdge &e1, const OrderedEdge &e2)
{
return !(e1 == e2);
}
friend bool operator<(const OrderedEdge &e1, const OrderedEdge &e2)
{
if (e1.v_low != e2.v_low) {
return e1.v_low < e2.v_low;
}
wannes.malfait marked this conversation as resolved Outdated

Typically we'd define these in a .cc file to avoid the need to include everywhere. Just parsing that was significantly slowing down compile times.

Typically we'd define these in a .cc file to avoid the need to include <ostream> everywhere. Just parsing that was significantly slowing down compile times.

I moved the implementation of these operators to a new ordered_edge.cc file. Not 100% sure if that is what you meant. I had to declare them in the header anyway, because otherwise I got linker errors and "missing declaration" warnings.

I moved the implementation of these operators to a new `ordered_edge.cc` file. Not 100% sure if that is what you meant. I had to declare them in the header anyway, because otherwise I got linker errors and "missing declaration" warnings.
return e1.v_high < e2.v_high;
}
friend std::ostream &operator<<(std::ostream &stream, const OrderedEdge &e);
};
} // namespace blender

View File

@ -120,6 +120,7 @@ set(SRC
intern/noise.c
intern/noise.cc
intern/offset_indices.cc
intern/ordered_edge.cc
intern/path_util.cc
intern/polyfill_2d.c
intern/polyfill_2d_beautify.c

View File

@ -0,0 +1,14 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_ordered_edge.hh"
namespace blender {
std::ostream &operator<<(std::ostream &stream, const OrderedEdge &e)
{
return stream << "OrderedEdge(" << e.v_low << ", " << e.v_high << ")";
}
} // namespace blender

View File

@ -27,6 +27,7 @@
# include "BKE_attribute.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"
@ -40,13 +41,14 @@
static const char *rna_Mesh_unit_test_compare(Mesh *mesh, Mesh *mesh2, float threshold)
wannes.malfait marked this conversation as resolved Outdated

Not quite sure I fully understand. I think it's still very important that we are table to check two meshes are exactly the same, not just isomorphic. Regression tests should fail when they are not exactly the same. Checking for an isomorphism is mostly just a utility that helps us track down the source of the issue more easily.

Changing indices should never be done accidentally.

Not quite sure I fully understand. I think it's still very important that we are table to check two meshes are exactly the same, not just isomorphic. Regression tests should fail when they are not exactly the same. Checking for an isomorphism is mostly just a utility that helps us track down the source of the issue more easily. Changing indices should never be done accidentally.

I think you are right. I will change it so that the normal comparison is run, but if the test fails, then a check is done to see if they are isomorphic.

I think you are right. I will change it so that the normal comparison is run, but if the test fails, then a check is done to see if they are isomorphic.
{
const char *ret = BKE_mesh_cmp(mesh, mesh2, threshold);
using namespace blender::bke::compare_meshes;
const std::optional<MeshMismatch> mismatch = compare_meshes(*mesh, *mesh2, threshold);
if (!ret) {
ret = "Same";
if (!mismatch) {
return "Same";
}
return ret;
return mismatch_to_string(mismatch.value());
}
static void rna_Mesh_sharp_from_angle_set(Mesh *mesh, const float angle)