238 lines
7.6 KiB
Python
238 lines
7.6 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))
|
|
|
|
def test_negative_intermediate_face_offset(self):
|
|
# An intermediate face's start offset is negative.
|
|
verts = [(0, 0, 0), (1, 0, 0), (1, 1, 0), (0, 1, 0),
|
|
(2, 0, 0), (3, 0, 0), (3, 1, 0), (2, 1, 0)]
|
|
edges = []
|
|
faces = [(0, 1, 2, 3), (4, 5, 6, 7)]
|
|
|
|
mesh = bpy.data.meshes.new("test_mesh")
|
|
mesh.from_pydata(verts, edges, faces)
|
|
|
|
mesh.polygons[1].loop_start = -4
|
|
|
|
self.assertTrue(mesh.validate(verbose=True))
|
|
self.assertFalse(mesh.validate(verbose=True))
|
|
|
|
def test_intermediate_face_offset_past_corners(self):
|
|
# An intermediate face's start offset is beyond the number of corners.
|
|
verts = [(0, 0, 0), (1, 0, 0), (1, 1, 0), (0, 1, 0),
|
|
(2, 0, 0), (3, 0, 0), (3, 1, 0), (2, 1, 0)]
|
|
edges = []
|
|
faces = [(0, 1, 2, 3), (4, 5, 6, 7)]
|
|
|
|
mesh = bpy.data.meshes.new("test_mesh")
|
|
mesh.from_pydata(verts, edges, faces)
|
|
|
|
mesh.polygons[1].loop_start = 1000
|
|
|
|
self.assertTrue(mesh.validate(verbose=True))
|
|
self.assertFalse(mesh.validate(verbose=True))
|
|
|
|
def test_corner_edge_to_duplicate_edge(self):
|
|
# A corner referencing a duplicate edge that gets removed must be
|
|
# redirected to the kept edge.
|
|
verts = [(0, 0, 0), (1, 0, 0), (1, 1, 0), (0, 1, 0)]
|
|
edges = [(0, 1), (1, 2), (2, 3), (3, 0), (0, 1)]
|
|
faces = [(0, 1, 2, 3)]
|
|
|
|
mesh = bpy.data.meshes.new("test_mesh")
|
|
mesh.from_pydata(verts, edges, faces)
|
|
|
|
# Point the first corner's edge at the duplicate.
|
|
corner_edges = mesh.attributes[".corner_edge"].data
|
|
corner_edges[0].value = 4
|
|
|
|
self.assertTrue(mesh.validate(verbose=True))
|
|
self.assertFalse(mesh.validate(verbose=True))
|
|
|
|
def test_bad_corner_edge_after_bad_vert_edge(self):
|
|
# When earlier edges have been removed (e.g. for out-of-range verts), the
|
|
# index used by the bad-edge fix-up must adapt.
|
|
verts = [(0, 0, 0), (1, 0, 0), (1, 1, 0), (0, 1, 0)]
|
|
faces = [(0, 1, 2, 3)]
|
|
|
|
mesh = bpy.data.meshes.new("test_mesh")
|
|
mesh.from_pydata(verts, [], faces)
|
|
|
|
# Give an edge an out-of-range vertex so it gets removed by validation.
|
|
mesh.attributes[".edge_verts"].data[0].value = (0, 99)
|
|
|
|
self.assertTrue(mesh.validate(verbose=True))
|
|
self.assertFalse(mesh.validate(verbose=True))
|
|
|
|
def test_missing_edge_with_oob_corner_edge(self):
|
|
# A face with a missing edge and an out-of-range corner_edge index.
|
|
verts = [(0, 0, 0), (1, 0, 0), (1, 1, 0), (0, 1, 0)]
|
|
faces = [(0, 1, 2, 3)]
|
|
|
|
mesh = bpy.data.meshes.new("test_mesh")
|
|
mesh.from_pydata(verts, [], faces)
|
|
|
|
mesh.attributes[".edge_verts"].data[0].value = (0, 99)
|
|
mesh.attributes[".corner_edge"].data[0].value = 999
|
|
|
|
self.assertTrue(mesh.validate(verbose=True))
|
|
self.assertFalse(mesh.validate(verbose=True))
|
|
|
|
def test_overlapping_face_offsets(self):
|
|
# Non-monotonic face offsets.
|
|
verts = [(float(i), 0.0, 0.0) for i in range(12)]
|
|
faces = [(0, 1, 2, 3), (4, 5, 6, 7), (8, 9, 10, 11)]
|
|
|
|
mesh = bpy.data.meshes.new("test_mesh")
|
|
mesh.from_pydata(verts, [], faces)
|
|
|
|
mesh.polygons[1].loop_start = 5
|
|
mesh.polygons[2].loop_start = 3
|
|
|
|
self.assertTrue(mesh.validate(verbose=True))
|
|
self.assertEqual(len(mesh.polygons), 0)
|
|
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()
|