From 340f9d7ff31f1bf1dce92485d2aa1e322b7ad472 Mon Sep 17 00:00:00 2001 From: Hans Goudey Date: Thu, 2 Oct 2025 13:41:44 -0400 Subject: [PATCH] Refactor: Use separate result for mesh joining, fix multires data join Instead of modifying the active mesh in place, which means we can't use the size of its data arrays when copying its data, and its caches are immediately invalidated, copy data to a separate out-of-main result mesh first. The only downside is that for a moment during the operator the shape key array sizes will be out of sync with the mesh size. Also the custom data for multires layers wasn't copied properly after the recent refactor that rewrote this code. Take the opportunity to fix that too. The motivation for this change is an improvement to copy different kinds of custom normals properly to the joined mesh, which never worked since free custom normals were introduced. This contains a few changes to the expected results in the tests. Those are edge cases, and the new results make more sense. --- source/blender/blenkernel/BKE_customdata.hh | 4 +- .../blender/blenkernel/intern/customdata.cc | 8 +- source/blender/editors/mesh/mesh_join.cc | 256 ++++++++++-------- tests/python/mesh_join.py | 8 +- 4 files changed, 152 insertions(+), 124 deletions(-) diff --git a/source/blender/blenkernel/BKE_customdata.hh b/source/blender/blenkernel/BKE_customdata.hh index 17e19b3c71c..a8878985ad6 100644 --- a/source/blender/blenkernel/BKE_customdata.hh +++ b/source/blender/blenkernel/BKE_customdata.hh @@ -343,8 +343,8 @@ void CustomData_copy_data_layer(const CustomData *source, int dst_index, int count); void CustomData_copy_elements(eCustomDataType type, - void *src_data_ofs, - void *dst_data_ofs, + const void *src_data, + void *dst_data, int count); /** diff --git a/source/blender/blenkernel/intern/customdata.cc b/source/blender/blenkernel/intern/customdata.cc index 30d74888261..602597ed52c 100644 --- a/source/blender/blenkernel/intern/customdata.cc +++ b/source/blender/blenkernel/intern/customdata.cc @@ -3257,17 +3257,17 @@ void CustomData_set_only_copy(const CustomData *data, const eCustomDataMask mask } void CustomData_copy_elements(const eCustomDataType type, - void *src_data_ofs, - void *dst_data_ofs, + const void *src_data, + void *dst_data, const int count) { const LayerTypeInfo *typeInfo = layerType_getInfo(type); if (typeInfo->copy) { - typeInfo->copy(src_data_ofs, dst_data_ofs, count); + typeInfo->copy(src_data, dst_data, count); } else { - memcpy(dst_data_ofs, src_data_ofs, size_t(count) * typeInfo->size); + memcpy(dst_data, src_data, size_t(count) * typeInfo->size); } } diff --git a/source/blender/editors/mesh/mesh_join.cc b/source/blender/editors/mesh/mesh_join.cc index 6e9c2bec508..5e6ce5dce8b 100644 --- a/source/blender/editors/mesh/mesh_join.cc +++ b/source/blender/editors/mesh/mesh_join.cc @@ -59,12 +59,8 @@ static VectorSet join_vertex_groups(const Span obje Mesh &dst_mesh) { VectorSet vertex_group_names; - LISTBASE_FOREACH (const bDeformGroup *, dg, &dst_mesh.vertex_group_names) { - vertex_group_names.add_new(dg->name); - } - bool any_vertex_group_data = false; - for (const int i : objects_to_join.index_range().drop_front(1)) { + for (const int i : objects_to_join.index_range()) { const Mesh &mesh = *static_cast(objects_to_join[i]->data); any_vertex_group_data |= CustomData_has_layer(&mesh.vert_data, CD_MDEFORMVERT); LISTBASE_FOREACH (const bDeformGroup *, dg, &mesh.vertex_group_names) { @@ -81,9 +77,9 @@ static VectorSet join_vertex_groups(const Span obje MDeformVert *dvert = (MDeformVert *)CustomData_add_layer( &dst_mesh.vert_data, CD_MDEFORMVERT, CD_CONSTRUCT, dst_mesh.verts_num); - for (const int i : objects_to_join.index_range().drop_front(1)) { + for (const int i : objects_to_join.index_range()) { const Mesh &src_mesh = *static_cast(objects_to_join[i]->data); - const Span src_dverts = src_mesh.deform_verts().take_front(vert_ranges[i].size()); + const Span src_dverts = src_mesh.deform_verts(); if (src_dverts.is_empty()) { continue; } @@ -106,29 +102,48 @@ static VectorSet join_vertex_groups(const Span obje return vertex_group_names; } -static void join_positions_and_shape_keys(Main *bmain, - const Span objects_to_join, - const OffsetIndices vert_ranges, - const float4x4 &world_to_dst_mesh, - Mesh &dst_mesh) +static void join_positions(const Span objects_to_join, + const OffsetIndices vert_ranges, + const float4x4 &world_to_dst_mesh, + Mesh &dst_mesh) { + MutableSpan dst_positions = dst_mesh.vert_positions_for_write(); + for (const int i : objects_to_join.index_range()) { + const Object &src_object = *objects_to_join[i]; + const IndexRange dst_range = vert_ranges[i]; + const Mesh &src_mesh = *static_cast(src_object.data); + const Span src_positions = src_mesh.vert_positions(); + const float4x4 transform = world_to_dst_mesh * src_object.object_to_world(); + math::transform_points(src_positions, transform, dst_positions.slice(dst_range)); + } +} + +static void join_shape_keys(Main *bmain, + const Span objects_to_join, + const OffsetIndices vert_ranges, + const float4x4 &world_to_active_mesh, + Mesh &active_mesh) +{ + const int dst_verts_num = vert_ranges.total_size(); Vector key_blocks; VectorSet key_names; - if (Key *key = dst_mesh.key) { + if (Key *key = active_mesh.key) { LISTBASE_FOREACH (KeyBlock *, kb, &key->block) { + kb->data = MEM_reallocN(kb->data, sizeof(float3) * dst_verts_num); + kb->totelem = dst_verts_num; key_names.add_new(kb->name); key_blocks.append(kb); } } const auto ensure_dst_key = [&]() { - if (!dst_mesh.key) { - dst_mesh.key = BKE_key_add(bmain, &dst_mesh.id); - dst_mesh.key->type = KEY_RELATIVE; + if (!active_mesh.key) { + active_mesh.key = BKE_key_add(bmain, &active_mesh.id); + active_mesh.key->type = KEY_RELATIVE; } }; - MutableSpan dst_positions = dst_mesh.vert_positions_for_write(); + const Span active_mesh_positions = active_mesh.vert_positions(); for (const int i : objects_to_join.index_range().drop_front(1)) { const Key *src_key = static_cast(objects_to_join[i]->data)->key; @@ -138,15 +153,14 @@ static void join_positions_and_shape_keys(Main *bmain, ensure_dst_key(); LISTBASE_FOREACH (const KeyBlock *, src_kb, &src_key->block) { if (key_names.add_as(src_kb->name)) { - KeyBlock *dst_kb = BKE_keyblock_add(dst_mesh.key, src_kb->name); + KeyBlock *dst_kb = BKE_keyblock_add(active_mesh.key, src_kb->name); BKE_keyblock_copy_settings(dst_kb, src_kb); - dst_kb->data = MEM_malloc_arrayN(dst_mesh.verts_num, __func__); - dst_kb->totelem = dst_mesh.verts_num; + dst_kb->data = MEM_malloc_arrayN(dst_verts_num, __func__); + dst_kb->totelem = dst_verts_num; /* Initialize the new shape key data with the base positions for the active object. */ MutableSpan key_data(static_cast(dst_kb->data), dst_kb->totelem); - key_data.take_front(vert_ranges[0].size()) - .copy_from(dst_positions.take_front(vert_ranges[0].size())); + key_data.take_front(vert_ranges[0].size()).copy_from(active_mesh_positions); /* Remap `KeyBlock::relative`. */ if (const KeyBlock *src_kb_relative = static_cast( @@ -158,27 +172,28 @@ static void join_positions_and_shape_keys(Main *bmain, } } + Key *dst_key = active_mesh.key; + if (!dst_key) { + return; + } + for (const int i : objects_to_join.index_range().drop_front(1)) { const Object &src_object = *objects_to_join[i]; const IndexRange dst_range = vert_ranges[i]; const Mesh &src_mesh = *static_cast(src_object.data); - const Span src_positions = src_mesh.vert_positions().take_front(dst_range.size()); - const float4x4 transform = world_to_dst_mesh * src_object.object_to_world(); - math::transform_points(src_positions, transform, dst_positions.slice(dst_range)); + const Span src_positions = src_mesh.vert_positions(); + const float4x4 transform = world_to_active_mesh * src_object.object_to_world(); - if (Key *dst_key = dst_mesh.key) { - LISTBASE_FOREACH (KeyBlock *, kb, &dst_key->block) { - MutableSpan key_data(static_cast(kb->data), kb->totelem); - if (const KeyBlock *src_kb = src_mesh.key ? - BKE_keyblock_find_name(src_mesh.key, kb->name) : - nullptr) - { - const Span src_kb_data(static_cast(src_kb->data), dst_range.size()); - math::transform_points(src_kb_data, transform, key_data.slice(dst_range)); - } - else { - key_data.slice(dst_range).copy_from(dst_positions.slice(dst_range)); - } + LISTBASE_FOREACH (KeyBlock *, kb, &dst_key->block) { + MutableSpan key_data(static_cast(kb->data), kb->totelem); + if (const KeyBlock *src_kb = src_mesh.key ? BKE_keyblock_find_name(src_mesh.key, kb->name) : + nullptr) + { + const Span src_kb_data(static_cast(src_kb->data), dst_range.size()); + math::transform_points(src_kb_data, transform, key_data.slice(dst_range)); + } + else { + math::transform_points(src_positions, transform, key_data.slice(dst_range)); } } } @@ -247,7 +262,7 @@ static void join_generic_attributes(const Span objects_to_join, const bke::AttrType data_type = kinds[attr_i].data_type; bke::GSpanAttributeWriter dst = dst_attributes.lookup_for_write_span(name); - for (const int i : objects_to_join.index_range().drop_front(1)) { + for (const int i : objects_to_join.index_range()) { const Mesh &src_mesh = *static_cast(objects_to_join[i]->data); const bke::AttributeAccessor src_attributes = src_mesh.attributes(); const GVArray src = *src_attributes.lookup_or_default(name, domain, data_type); @@ -268,7 +283,7 @@ static void join_generic_attributes(const Span objects_to_join, } }(); - src.materialize(IndexRange(dst_range.size()), dst.span.slice(dst_range).data()); + src.materialize(dst.span.slice(dst_range).data()); } dst.finish(); } @@ -301,13 +316,13 @@ static VectorSet join_materials(const Span objects_t return materials; } - bke::SpanAttributeWriter dst_material_indices = dst_attributes.lookup_or_add_for_write_span( + bke::SpanAttributeWriter dst_attr = dst_attributes.lookup_or_add_for_write_only_span( "material_index", bke::AttrDomain::Face); - if (!dst_material_indices) { + if (!dst_attr) { return {}; } - for (const int i : objects_to_join.index_range().drop_front(1)) { + for (const int i : objects_to_join.index_range()) { const Object &src_object = *objects_to_join[i]; const IndexRange dst_range = face_ranges[i]; const Mesh &src_mesh = *static_cast(src_object.data); @@ -319,14 +334,13 @@ static VectorSet join_materials(const Span objects_t Material *first_material = src_mesh.totcol == 0 ? nullptr : BKE_object_material_get(&const_cast(src_object), 1); - dst_material_indices.span.slice(dst_range).fill(materials.index_of(first_material)); + dst_attr.span.slice(dst_range).fill(materials.index_of(first_material)); continue; } if (src_mesh.totcol == 0) { /* These material indices are invalid, but copy them anyway to avoid destroying user data. */ - material_indices.materialize(dst_range.index_range(), - dst_material_indices.span.slice(dst_range)); + material_indices.materialize(dst_range.index_range(), dst_attr.span.slice(dst_range)); continue; } @@ -341,11 +355,11 @@ static VectorSet join_materials(const Span objects_t const int max = src_mesh.totcol - 1; for (const int face : dst_range.index_range()) { const int src = std::clamp(material_indices[face], 0, max); - dst_material_indices.span[dst_range[face]] = index_map[src]; + dst_attr.span[dst_range[face]] = index_map[src]; } } - dst_material_indices.finish(); + dst_attr.finish(); return materials; } @@ -357,18 +371,23 @@ static void join_face_sets(const Span objects_to_join, const OffsetIndices face_ranges, Mesh &dst_mesh) { - bke::MutableAttributeAccessor dst_attributes = dst_mesh.attributes_for_write(); - bke::SpanAttributeWriter dst_face_sets = dst_attributes.lookup_for_write_span( - ".sculpt_face_set"); - if (!dst_face_sets) { - return; - } - if (dst_face_sets.domain != bke::AttrDomain::Face) { + if (std::none_of(objects_to_join.begin(), objects_to_join.end(), [](const Object *object) { + const Mesh &mesh = *static_cast(object->data); + return mesh.attributes().contains(".sculpt_face_set"); + })) + { return; } - int max_face_set = 1; - for (const int i : objects_to_join.index_range().drop_front(1)) { + bke::MutableAttributeAccessor dst_attributes = dst_mesh.attributes_for_write(); + bke::SpanAttributeWriter dst_face_sets = dst_attributes.lookup_or_add_for_write_span( + ".sculpt_face_set", bke::AttrDomain::Face); + if (!dst_face_sets) { + return; + } + + int max_face_set = 0; + for (const int i : objects_to_join.index_range()) { const Object &src_object = *objects_to_join[i]; const IndexRange dst_range = face_ranges[i]; const Mesh &src_mesh = *static_cast(src_object.data); @@ -463,10 +482,9 @@ wmOperatorStatus join_objects_exec(bContext *C, wmOperator *op) /* Only join meshes if there are verts to join, * there aren't too many, and we only had one mesh selected. */ - Mesh *dst_mesh = (Mesh *)active_object->data; - Key *key = dst_mesh->key; + Mesh *active_mesh = (Mesh *)active_object->data; - if (ELEM(vert_ranges.total_size(), 0, dst_mesh->verts_num)) { + if (ELEM(vert_ranges.total_size(), 0, active_mesh->verts_num)) { BKE_report(op->reports, RPT_WARNING, "No mesh data to join"); return OPERATOR_CANCELLED; } @@ -480,39 +498,21 @@ wmOperatorStatus join_objects_exec(bContext *C, wmOperator *op) return OPERATOR_CANCELLED; } - CustomData_realloc(&dst_mesh->vert_data, dst_mesh->verts_num, vert_ranges.total_size()); - CustomData_realloc(&dst_mesh->edge_data, dst_mesh->edges_num, edge_ranges.total_size()); - CustomData_realloc(&dst_mesh->face_data, dst_mesh->faces_num, face_ranges.total_size()); - CustomData_realloc(&dst_mesh->corner_data, dst_mesh->corners_num, corner_ranges.total_size()); - if (face_ranges.total_size() != dst_mesh->faces_num) { - implicit_sharing::resize_trivial_array(&dst_mesh->face_offset_indices, - &dst_mesh->runtime->face_offsets_sharing_info, - dst_mesh->faces_num, - face_ranges.total_size() + 1); - } - dst_mesh->verts_num = vert_ranges.total_size(); - dst_mesh->edges_num = edge_ranges.total_size(); - dst_mesh->faces_num = face_ranges.total_size(); - dst_mesh->corners_num = corner_ranges.total_size(); - if (Key *key = dst_mesh->key) { - LISTBASE_FOREACH (KeyBlock *, kb, &key->block) { - kb->data = MEM_reallocN(kb->data, sizeof(float3) * dst_mesh->verts_num); - kb->totelem = dst_mesh->verts_num; - } - } - - BKE_mesh_runtime_clear_geometry(dst_mesh); + Mesh *dst_mesh = BKE_mesh_new_nomain(vert_ranges.total_size(), + edge_ranges.total_size(), + face_ranges.total_size(), + corner_ranges.total_size()); /* Inverse transform for all selected meshes in this object, * See #object_join_exec for detailed comment on why the safe version is used. */ float4x4 world_to_active_object; invert_m4_m4_safe_ortho(world_to_active_object.ptr(), active_object->object_to_world().ptr()); - join_positions_and_shape_keys( - bmain, objects_to_join, vert_ranges, world_to_active_object, *dst_mesh); + join_shape_keys(bmain, objects_to_join, vert_ranges, world_to_active_object, *active_mesh); + join_positions(objects_to_join, vert_ranges, world_to_active_object, *dst_mesh); MutableSpan dst_edges = dst_mesh->edges_for_write(); - for (const int i : objects_to_join.index_range().drop_front(1)) { + for (const int i : objects_to_join.index_range()) { const Object &src_object = *objects_to_join[i]; const IndexRange dst_range = edge_ranges[i]; const Mesh &src_mesh = *static_cast(src_object.data); @@ -523,7 +523,7 @@ wmOperatorStatus join_objects_exec(bContext *C, wmOperator *op) } MutableSpan dst_corner_verts = dst_mesh->corner_verts_for_write(); - for (const int i : objects_to_join.index_range().drop_front(1)) { + for (const int i : objects_to_join.index_range()) { const Object &src_object = *objects_to_join[i]; const IndexRange dst_range = corner_ranges[i]; const Mesh &src_mesh = *static_cast(src_object.data); @@ -534,7 +534,7 @@ wmOperatorStatus join_objects_exec(bContext *C, wmOperator *op) } MutableSpan dst_corner_edges = dst_mesh->corner_edges_for_write(); - for (const int i : objects_to_join.index_range().drop_front(1)) { + for (const int i : objects_to_join.index_range()) { const Object &src_object = *objects_to_join[i]; const IndexRange dst_range = corner_ranges[i]; const Mesh &src_mesh = *static_cast(src_object.data); @@ -545,7 +545,7 @@ wmOperatorStatus join_objects_exec(bContext *C, wmOperator *op) } MutableSpan dst_face_offsets = dst_mesh->face_offsets_for_write(); - for (const int i : objects_to_join.index_range().drop_front(1)) { + for (const int i : objects_to_join.index_range()) { const Object &src_object = *objects_to_join[i]; const IndexRange dst_range = face_ranges[i]; const Mesh &src_mesh = *static_cast(src_object.data); @@ -556,24 +556,6 @@ wmOperatorStatus join_objects_exec(bContext *C, wmOperator *op) } dst_face_offsets.last() = dst_mesh->corners_num; - for (const int i : objects_to_join.index_range().drop_front(1)) { - const Object &src_object = *objects_to_join[i]; - const Mesh &src_mesh = *static_cast(src_object.data); - const Key *src_key = src_mesh.key; - if (!src_key) { - continue; - } - } - - for (const int i : objects_to_join.index_range().drop_front(1)) { - Object &src_object = *objects_to_join[i]; - multiresModifier_prepare_join(depsgraph, scene, &src_object, active_object); - if (MultiresModifierData *mmd = get_multires_modifier(scene, &src_object, true)) { - object::iter_other( - bmain, &src_object, true, object::multires_update_totlevels, &mmd->totlvl); - } - } - join_face_sets(objects_to_join, face_ranges, *dst_mesh); VectorSet materials = join_materials(objects_to_join, face_ranges, *dst_mesh); @@ -589,6 +571,50 @@ wmOperatorStatus join_objects_exec(bContext *C, wmOperator *op) corner_ranges, *dst_mesh); + /* Copy multires data to the out-of-main mesh. */ + if (get_multires_modifier(scene, active_object, true)) { + if (std::any_of(objects_to_join.begin(), objects_to_join.end(), [](const Object *object) { + const Mesh &src_mesh = *static_cast(object->data); + return CustomData_has_layer(&src_mesh.corner_data, CD_MDISPS); + })) + { + MDisps *dst = static_cast(CustomData_add_layer( + &dst_mesh->corner_data, CD_MDISPS, CD_CONSTRUCT, dst_mesh->corners_num)); + for (const int i : objects_to_join.index_range()) { + const Mesh &src_mesh = *static_cast(objects_to_join[i]->data); + if (const void *src = CustomData_get_layer(&src_mesh.corner_data, CD_MDISPS)) { + CustomData_copy_elements( + CD_MDISPS, src, &dst[corner_ranges[i].first()], src_mesh.corners_num); + } + } + } + if (std::any_of(objects_to_join.begin(), objects_to_join.end(), [](const Object *object) { + const Mesh &src_mesh = *static_cast(object->data); + return CustomData_has_layer(&src_mesh.corner_data, CD_GRID_PAINT_MASK); + })) + { + GridPaintMask *dst = static_cast(CustomData_add_layer( + &dst_mesh->corner_data, CD_GRID_PAINT_MASK, CD_CONSTRUCT, dst_mesh->corners_num)); + for (const int i : objects_to_join.index_range()) { + const Mesh &src_mesh = *static_cast(objects_to_join[i]->data); + if (const void *src = CustomData_get_layer(&src_mesh.corner_data, CD_GRID_PAINT_MASK)) { + CustomData_copy_elements( + CD_GRID_PAINT_MASK, src, &dst[corner_ranges[i].first()], src_mesh.corners_num); + } + } + } + } + for (const int i : objects_to_join.index_range().drop_front(1)) { + Object &src_object = *objects_to_join[i]; + multiresModifier_prepare_join(depsgraph, scene, &src_object, active_object); + if (MultiresModifierData *mmd = get_multires_modifier(scene, &src_object, true)) { + object::iter_other( + bmain, &src_object, true, object::multires_update_totlevels, &mmd->totlvl); + } + } + + BKE_mesh_nomain_to_mesh(dst_mesh, active_mesh, active_object, false); + for (Object *object : objects_to_join.as_span().drop_front(1)) { object::base_free_and_unlink(bmain, scene, object); } @@ -600,13 +626,13 @@ wmOperatorStatus join_objects_exec(bContext *C, wmOperator *op) } } for (const int a : IndexRange(active_object->totcol)) { - if (Material *ma = dst_mesh->mat[a]) { + if (Material *ma = active_mesh->mat[a]) { id_us_min(&ma->id); } } MEM_SAFE_FREE(active_object->mat); MEM_SAFE_FREE(active_object->matbits); - MEM_SAFE_FREE(dst_mesh->mat); + MEM_SAFE_FREE(active_mesh->mat); /* If the object had no slots, don't add an empty one. */ if (active_object->totcol == 0 && materials.size() == 1 && materials[0] == nullptr) { @@ -616,9 +642,9 @@ wmOperatorStatus join_objects_exec(bContext *C, wmOperator *op) const int totcol = materials.size(); if (totcol) { VectorData data = materials.extract_vector().release(); - dst_mesh->mat = data.data; + active_mesh->mat = data.data; for (const int i : IndexRange(totcol)) { - if (Material *ma = dst_mesh->mat[i]) { + if (Material *ma = active_mesh->mat[i]) { id_us_plus((ID *)ma); } } @@ -626,14 +652,16 @@ wmOperatorStatus join_objects_exec(bContext *C, wmOperator *op) active_object->matbits = MEM_calloc_arrayN(totcol, __func__); } - active_object->totcol = dst_mesh->totcol = totcol; + active_object->totcol = active_mesh->totcol = totcol; /* other mesh users */ - BKE_objects_materials_sync_length_all(bmain, (ID *)dst_mesh); + BKE_objects_materials_sync_length_all(bmain, &active_mesh->id); /* ensure newly inserted keys are time sorted */ - if (key && (key->type != KEY_RELATIVE)) { - BKE_key_sort(key); + if (Key *key = active_mesh->key) { + if (key->type != KEY_RELATIVE) { + BKE_key_sort(key); + } } /* Due to dependency cycle some other object might access old derived data. */ diff --git a/tests/python/mesh_join.py b/tests/python/mesh_join.py index cbe20a22f3a..abfc07580bc 100644 --- a/tests/python/mesh_join.py +++ b/tests/python/mesh_join.py @@ -58,8 +58,8 @@ class TestMeshJoin(unittest.TestCase): self.assertEqual(joined_attr.data[0].value, 1) self.assertEqual(joined_attr.data[1].value, 1) self.assertEqual(joined_attr.data[2].value, 45) - self.assertEqual(joined_attr.data[9].value, 53) - self.assertEqual(joined_attr.data[20].value, 1) + self.assertEqual(joined_attr.data[9].value, 98) + self.assertEqual(joined_attr.data[20].value, 46) def test_materials_simple(self): bpy.ops.object.select_all(action='SELECT') @@ -91,11 +91,11 @@ class TestMeshJoin(unittest.TestCase): cube_1 = bpy.context.view_layer.objects.active cube_1.data.materials.append(mat_1) material_indices = cube_1.data.attributes.new(name="material_index", type='INT', domain='FACE') - material_indices.data.foreach_set('value', [0, 1, 1, 700, 1, 2]) bpy.ops.mesh.primitive_cube_add() cube_2 = bpy.context.view_layer.objects.active material_indices = cube_2.data.attributes.new(name="material_index", type='INT', domain='FACE') + material_indices.data.foreach_set('value', [0, 1, 1, 700, 1, 2]) bpy.ops.object.select_all(action='SELECT') bpy.context.view_layer.objects.active = cube_1 @@ -106,7 +106,7 @@ class TestMeshJoin(unittest.TestCase): material_indices = cube_1.data.attributes["material_index"] self.assertTrue(material_indices) material_index_data = [m.value for m in material_indices.data] - self.assertEqual(material_index_data, [0, 1, 1, 700, 1, 2] + [0] * 6) + self.assertEqual(material_index_data, [0] * 6 + [0, 1, 1, 700, 1, 2]) def test_materials(self): bpy.ops.object.select_all(action='SELECT')