Files
test2/source/blender/blenkernel/intern/geometry_compare.cc

1026 lines
38 KiB
C++

/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include <algorithm>
#include <numeric>
#include "BLI_array.hh"
#include "BLI_math_base.h"
#include "BLI_ordered_edge.hh"
#include "BLI_span.hh"
#include "BKE_anonymous_attribute_id.hh"
#include "BKE_attribute.hh"
#include "BKE_attribute_math.hh"
#include "BKE_mesh.hh"
#include "BKE_mesh_mapping.hh"
#include "BKE_geometry_compare.hh"
namespace blender::bke::compare_geometry {
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. */
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 geometries are the same up to a change of indices. */
};
const char *mismatch_to_string(const GeoMismatch &mismatch)
{
switch (mismatch) {
case GeoMismatch::NumPoints:
return "The number of points is different";
case GeoMismatch::NumEdges:
return "The number of edges is different";
case GeoMismatch::NumCorners:
return "The number of corners is different";
case GeoMismatch::NumFaces:
return "The number of faces is different";
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 GeoMismatch::CornerAttributes:
return "Some values of the corner attributes are different";
case GeoMismatch::FaceAttributes:
return "Some values of the face attributes are different";
case GeoMismatch::CurveAttributes:
return "Some values of the curve attributes are different";
case GeoMismatch::EdgeTopology:
return "The edge topology is different";
case GeoMismatch::FaceTopology:
return "The face topology is different";
case GeoMismatch::CurveTopology:
return "The curve topology is different";
case GeoMismatch::Attributes:
return "The sets of attribute ids are different";
case GeoMismatch::AttributeTypes:
return "Some attributes with the same name have different types";
case GeoMismatch::Indices:
return "The geometries 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_quat = float4(value1);
const float4 value2_quat = float4(value2);
return value1_quat[component_i] < value2_quat[component_i];
}
if constexpr (std::is_same_v<T, float4x4>) {
return value1.base_ptr()[component_i] < value2.base_ptr()[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();
return false;
});
}
/**
* 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
* threshold.
*/
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, short2, int2, bool, int8_t, OrderedEdge, ColorGeometry4b>) {
/* These types already have a good implementation. */
return value1 != value2;
}
/* 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);
}
if constexpr (std::is_same_v<T, float4x4>) {
return compare_threshold_relative(
value1.base_ptr()[component_i], value2.base_ptr()[component_i], threshold);
}
BLI_assert_unreachable();
}
/**
* Split the sets into smaller sets based on the sorted attribute values.
*
* \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()) {
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]];
smallest = std::min(corner_id, smallest);
}
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 StringRef id)
{
return attribute_name_is_anonymous(id) || id.startswith(".vs.") || id.startswith(".es.") ||
id.startswith(".pn.");
}
/**
* Verify that both geometries have the same attributes:
* - Same names
* - Same domains
* - Same types
*/
static std::optional<GeoMismatch> verify_attributes_compatible(
const AttributeAccessor &attributes1, const AttributeAccessor &attributes2)
{
Set<StringRefNull> attribute_ids1 = attributes1.all_ids();
Set<StringRefNull> attribute_ids2 = attributes2.all_ids();
attribute_ids1.remove_if(ignored_attribute);
attribute_ids2.remove_if(ignored_attribute);
if (attribute_ids1 != attribute_ids2) {
/* Disabled for now due to tests not being up to date. */
// return GeoMismatch::Attributes;
}
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 GeoMismatch::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 geometries.
*/
static std::optional<GeoMismatch> sort_domain_using_attributes(
const AttributeAccessor &attributes1,
const AttributeAccessor &attributes2,
const AttrDomain domain,
const Span<StringRef> excluded_attributes,
IndexMapping &maps,
const float threshold)
{
/* We only need the ids from one geometry, since we know they have the same attributes. */
Set<StringRefNull> 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 (!attributes2.contains(id)) {
/* Only needed right now since some test meshes don't have the same attributes. */
return GeoMismatch::Attributes;
}
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<GeoMismatch> 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;
}
else if constexpr (is_same_any_v<T, float4x4>) {
num_loops = 16;
}
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 AttrDomain::Point:
mismatch = GeoMismatch::PointAttributes;
return;
case AttrDomain::Edge:
mismatch = GeoMismatch::EdgeAttributes;
return;
case AttrDomain::Corner:
mismatch = GeoMismatch::CornerAttributes;
return;
case AttrDomain::Face:
mismatch = GeoMismatch::FaceAttributes;
return;
case AttrDomain::Curve:
mismatch = GeoMismatch::CurveAttributes;
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 occurred if the mapping couldn't be constructed.
*/
static std::optional<GeoMismatch> 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. */
return std::nullopt;
}
/* Since we are not yet able to distinguish 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.verts_num, 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.verts_num, 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;
}
}
if (vertex_matches) {
matching_verts.append(index_in_set);
}
}
if (matching_verts.is_empty()) {
return GeoMismatch::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<GeoMismatch> 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 GeoMismatch::NumPoints;
}
if (mesh1.edges_num != mesh2.edges_num) {
return GeoMismatch::NumEdges;
}
if (mesh1.corners_num != mesh2.corners_num) {
return GeoMismatch::NumCorners;
}
if (mesh1.faces_num != mesh2.faces_num) {
return GeoMismatch::NumFaces;
}
std::optional<GeoMismatch> 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.verts_num);
mismatch = sort_domain_using_attributes(
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 GeoMismatch::EdgeTopology;
}
mismatch = sort_domain_using_attributes(
mesh1_attributes, mesh2_attributes, AttrDomain::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.corners_num);
if (!sort_corners_based_on_domain(mesh1.corner_verts(), mesh2.corner_verts(), verts, corners)) {
return GeoMismatch::FaceTopology;
}
if (!sort_corners_based_on_domain(mesh1.corner_edges(), mesh2.corner_edges(), edges, corners)) {
return GeoMismatch::FaceTopology;
}
mismatch = sort_domain_using_attributes(mesh1_attributes,
mesh2_attributes,
AttrDomain::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 GeoMismatch::FaceTopology;
}
mismatch = sort_domain_using_attributes(
mesh1_attributes, mesh2_attributes, AttrDomain::Face, {}, faces, threshold);
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 GeoMismatch::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 GeoMismatch::FaceTopology;
}
if (!sort_corners_based_on_domain(mesh1.corner_edges(), mesh2.corner_edges(), edges, corners)) {
return GeoMismatch::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 GeoMismatch::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 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 GeoMismatch::Indices;
}
}
for (const int sorted_i : faces.from_sorted1.index_range()) {
if (faces.from_sorted1[sorted_i] != faces.from_sorted2[sorted_i]) {
return GeoMismatch::Indices;
}
}
/* No mismatches found. */
return std::nullopt;
}
/**
* Sort curves based on their sizes.
*/
static bool sort_curves(const OffsetIndices<int> offset_indices1,
const OffsetIndices<int> offset_indices2,
IndexMapping &curves)
{
Array<int> curve_point_counts1(offset_indices1.size());
Array<int> 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<GeoMismatch> 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<GeoMismatch> 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