blender/tests/python/mesh_validate.py
Sean Kim b72b204390 Mesh: Add validation of CD_MDISPS tangent vector data
This commit adds support for validating and correcting Multires tangent
data stored in CD_MDISPS when called via `BKE_mesh_validate` when
importing or on demand when users call `mesh.validate()` via the Python
API.

Any non-finite values are set to 0. While this will result in likely odd
looking data on a multires mesh, it will prevent further propagation of
bad float data and will allow users to restore their mesh to a workable
state in the case of data corruption.

Ref: #149205

Pull Request: https://projects.blender.org/blender/blender/pulls/149320
2025-11-04 22:42:09 +01:00

146 lines
4.2 KiB
Python

# SPDX-FileCopyrightText: 2025 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later
import unittest
import bpy
import sys
import pathlib
"""
blender -b --factory-startup --python tests/python/mesh_validate.py -- --testdir tests/files/
"""
class TestMeshValidate(unittest.TestCase):
def setUp(self):
if bpy.context.object and bpy.context.object.mode != 'OBJECT':
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete()
for mesh in list(bpy.data.meshes):
bpy.data.meshes.remove(mesh)
def tearDown(self):
for mesh in list(bpy.data.meshes):
bpy.data.meshes.remove(mesh)
def test_invalid_edge_vertex_indices(self):
verts = [(0, 0, 0), (1, 1, 1)]
edges = [(0, 99)] # Invalid vertex 99
mesh = bpy.data.meshes.new("test_mesh")
mesh.from_pydata(verts, edges, [])
self.assertTrue(mesh.validate(verbose=True))
self.assertFalse(mesh.validate(verbose=True))
def test_duplicate_edge_vertex_indices(self):
verts = [(0, 0, 0), (1, 1, 1)]
edges = [(0, 0)] # Invalid edge from a vertex to itself
mesh = bpy.data.meshes.new("test_mesh")
mesh.from_pydata(verts, edges, [])
self.assertTrue(mesh.validate(verbose=True))
self.assertFalse(mesh.validate(verbose=True))
def test_bad_face_offsets(self):
bpy.ops.mesh.primitive_cube_add()
mesh = bpy.context.active_object.data
mesh.polygons[0].loop_start = 100
self.assertTrue(mesh.validate(verbose=True))
self.assertFalse(mesh.validate(verbose=True))
def test_bad_material_indices(self):
bpy.ops.mesh.primitive_plane_add()
obj = bpy.context.active_object
mesh = obj.data
attr = mesh.attributes.new(name="material_index", type='INT', domain='FACE')
attr.data[0].value = -4
self.assertTrue(mesh.validate(verbose=True))
self.assertFalse(mesh.validate(verbose=True))
def test_duplicate_faces(self):
verts = [(0, 0, 0), (1, 0, 0), (1, 1, 0)]
faces = [(0, 1, 2), (2, 0, 1)]
mesh = bpy.data.meshes.new("test_mesh")
mesh.from_pydata(verts, [], faces)
self.assertTrue(mesh.validate(verbose=True))
self.assertFalse(mesh.validate(verbose=True))
def test_invalid_float_attributes(self):
bpy.ops.mesh.primitive_plane_add()
mesh = bpy.context.active_object.data
mesh.vertices[0].co.x = float('nan')
self.assertTrue(mesh.validate(verbose=True))
self.assertFalse(mesh.validate(verbose=True))
def test_duplicate_edges(self):
verts = [(0, 0, 0), (1, 1, 1)]
edges = [(0, 1), (1, 0)]
mesh = bpy.data.meshes.new("test_mesh")
mesh.from_pydata(verts, edges, [])
self.assertTrue(mesh.validate(verbose=True))
self.assertFalse(mesh.validate(verbose=True))
def test_faces_with_bad_edge_references(self):
verts = [(0, 0, 0), (1, 0, 0), (1, 1, 0), (0, 1, 0)]
edges = []
faces = [(0, 1, 2, 3)]
mesh = bpy.data.meshes.new("test_mesh")
mesh.from_pydata(verts, edges, faces)
corner_edges = mesh.attributes[".corner_edge"].data
corner_edges[2].value = 0
self.assertTrue(mesh.validate(verbose=True))
self.assertFalse(mesh.validate(verbose=True))
args = None
class TestMeshValidateExistingFile(unittest.TestCase):
def setUp(self):
bpy.ops.wm.open_mainfile(filepath=str(args.testdir / "sculpting/invalid_mdisp_cube.blend"), load_ui=False)
def test_mesh_with_bad_multires_displacements(self):
mesh = bpy.context.active_object.data
self.assertTrue(mesh.validate(verbose=True))
self.assertFalse(mesh.validate(verbose=True))
def main():
global args
import argparse
argv = [sys.argv[0]]
if '--' in sys.argv:
argv += sys.argv[sys.argv.index('--') + 1:]
parser = argparse.ArgumentParser()
parser.add_argument('--testdir', required=True, type=pathlib.Path)
args, remaining = parser.parse_known_args(argv)
unittest.main(argv=remaining)
if __name__ == '__main__':
main()