Files
test/tests/python/mesh_join.py
Hans Goudey 340f9d7ff3 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.
2025-10-03 01:57:49 +02:00

221 lines
8.4 KiB
Python

# SPDX-FileCopyrightText: 2025 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later
import unittest
import bpy
import sys
import pathlib
from mathutils import Vector
class TestMeshJoin(unittest.TestCase):
def test_simple(self):
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete()
bpy.ops.mesh.primitive_cube_add()
bpy.ops.mesh.primitive_cube_add()
mesh = bpy.data.objects['Cube'].data
bpy.ops.object.select_all(action='SELECT')
bpy.context.view_layer.objects.active = bpy.data.objects['Cube']
bpy.ops.object.join()
self.assertEqual(len(mesh.vertices), 16)
def test_face_sets(self):
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete()
bpy.ops.mesh.primitive_cube_add()
bpy.ops.mesh.primitive_monkey_add()
bpy.ops.mesh.primitive_uv_sphere_add()
cube_obj = bpy.data.objects['Cube']
cube = cube_obj.data
monkey = bpy.data.objects['Suzanne'].data
sphere = bpy.data.objects['Sphere'].data
attr = cube.attributes.new(name=".sculpt_face_set", type='INT', domain='FACE')
attr.data[0].value = 1
attr.data[1].value = 1
attr.data[2].value = 45
attr.data[3].value = 45
attr.data[4].value = 45
attr.data[5].value = 45
b = monkey.attributes.new(name=".sculpt_face_set", type='INT', domain='FACE')
b.data[0].value = 52
b.data[1].value = 52
b.data[2].value = 52
b.data[3].value = 52
b.data[4].value = 52
b.data[5].value = 52
sphere.attributes.new(name=".sculpt_face_set", type='INT', domain='FACE')
bpy.ops.object.select_all(action='SELECT')
bpy.context.view_layer.objects.active = cube_obj
bpy.ops.object.join()
joined_attr = cube.attributes[".sculpt_face_set"]
self.assertEqual(len(joined_attr.data), 1018)
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, 98)
self.assertEqual(joined_attr.data[20].value, 46)
def test_materials_simple(self):
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete()
mat_1 = bpy.data.materials.new('mat_1')
bpy.ops.mesh.primitive_cube_add()
cube_1 = bpy.context.view_layer.objects.active
cube_1.data.materials.append(mat_1)
bpy.ops.mesh.primitive_cube_add()
cube_2 = bpy.context.view_layer.objects.active
cube_2.data.materials.append(mat_1)
bpy.ops.object.select_all(action='SELECT')
bpy.context.view_layer.objects.active = cube_1
bpy.ops.object.join()
result_mesh = cube_1.data
self.assertEqual(len(result_mesh.materials), 1)
self.assertFalse(result_mesh.attributes.get("material_index"))
def test_no_materials_with_indices(self):
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete()
mat_1 = bpy.data.materials.new('mat_1')
bpy.ops.mesh.primitive_cube_add()
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')
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
bpy.ops.object.join()
result_mesh = cube_1.data
self.assertEqual(len(result_mesh.materials), 2)
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] * 6 + [0, 1, 1, 700, 1, 2])
def test_materials(self):
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete()
mat_1 = bpy.data.materials.new('mat_1')
mat_2 = bpy.data.materials.new('mat_2')
mat_3 = bpy.data.materials.new('mat_3')
mat_4 = bpy.data.materials.new('mat_4')
bpy.ops.mesh.primitive_cube_add()
cube_no_mats = bpy.context.view_layer.objects.active
bpy.ops.mesh.primitive_cube_add()
cube_2 = bpy.context.view_layer.objects.active
cube_2.data.materials.append(mat_1)
cube_2.data.materials.append(mat_2)
cube_2.data.materials.append(mat_3)
material_indices = cube_2.data.attributes.new(name="material_index", type='INT', domain='FACE')
material_indices.data.foreach_set('value', [0, 1, 2, 0, 1, 2])
bpy.ops.mesh.primitive_cube_add()
cube_2_mats = bpy.context.view_layer.objects.active
cube_2_mats.data.materials.append(mat_3)
cube_2_mats.data.materials.append(mat_4)
material_indices = cube_2_mats.data.attributes.new(name="material_index", type='INT', domain='FACE')
material_indices.data.foreach_set('value', [0, 0, 0, 1, 1, 1])
bpy.ops.object.select_all(action='SELECT')
bpy.context.view_layer.objects.active = cube_no_mats
bpy.ops.object.join()
result_mesh = cube_no_mats.data
self.assertEqual(len(result_mesh.materials), 5)
material_indices = result_mesh.attributes["material_index"]
material_index_data = [m.value for m in material_indices.data]
self.assertEqual(material_index_data, [0] * 6 + [1, 2, 3, 1, 2, 3] + [3, 3, 3, 4, 4, 4])
def test_shared_object_data(self):
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete()
bpy.ops.mesh.primitive_cube_add()
cube_1 = bpy.context.view_layer.objects.active
bpy.ops.object.duplicate(linked=True)
cube_2 = bpy.context.view_layer.objects.active
self.assertEqual(cube_1.data.name, cube_2.data.name)
bpy.ops.mesh.primitive_cube_add()
bpy.ops.object.select_all(action='SELECT')
bpy.context.view_layer.objects.active = cube_1
bpy.ops.object.join()
self.assertEqual(len(cube_1.data.vertices), 24)
def test_shape_keys(self):
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete()
bpy.ops.mesh.primitive_cube_add()
cube_1 = bpy.context.view_layer.objects.active
cube_1.shape_key_add(name="Basis")
key = cube_1.shape_key_add(name="A")
key = cube_1.shape_key_add(name="B")
cube_1.data.shape_keys.key_blocks["B"].data[0].co = Vector((-1, -4, -1))
bpy.ops.mesh.primitive_cube_add()
cube_2 = bpy.context.view_layer.objects.active
cube_2.shape_key_add(name="Basis")
key = cube_2.shape_key_add(name="A")
key = cube_2.shape_key_add(name="B")
key = cube_2.shape_key_add(name="C")
key = cube_2.shape_key_add(name="D")
cube_2.data.shape_keys.key_blocks["B"].data[5].co = Vector((1, -3, 1))
bpy.ops.object.select_all(action='SELECT')
bpy.context.view_layer.objects.active = cube_1
bpy.ops.object.join()
self.assertEqual(len(cube_1.data.vertices), 16)
self.assertEqual(len(cube_1.data.shape_keys.key_blocks), 5)
self.assertEqual(cube_1.data.shape_keys.key_blocks["B"].data[0].co, Vector((-1.0, -4.0, -1.0)))
self.assertEqual(cube_1.data.shape_keys.key_blocks["B"].data[13].co, Vector((1.0, -3.0, 1.0)))
def test_shape_keys_not_active(self):
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete()
bpy.ops.mesh.primitive_cube_add()
cube_1 = bpy.context.object
cube_1.shape_key_add(name="Basis")
key = cube_1.shape_key_add(name="A")
key = cube_1.shape_key_add(name="B")
key = cube_1.shape_key_add(name="C")
bpy.ops.mesh.primitive_cube_add()
cube_2 = bpy.context.object
bpy.ops.object.select_all(action='SELECT')
bpy.context.view_layer.objects.active = cube_2
bpy.ops.object.join()
self.assertEqual(len(cube_2.data.vertices), 16)
self.assertEqual(len(cube_2.data.shape_keys.key_blocks), 4)
if __name__ == '__main__':
import sys
sys.argv = [__file__] + (sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [])
unittest.main()