mirror of
https://github.com/blender/blender
synced 2026-09-29 04:37:17 +03:00
Cleanup: Reorganize sculpt operator tests
Moves sculpt operator related tests into the `sculpt_paint` folder and reorganizes them based on function. Pull Request: https://projects.blender.org/blender/blender/pulls/153155
This commit is contained in:
parent
1b1b0daff0
commit
7a646cb88d
7 changed files with 194 additions and 250 deletions
|
|
@ -553,13 +553,6 @@ if(TEST_SRC_DIR_EXISTS)
|
|||
--
|
||||
--testdir "${TEST_SRC_DIR}/constraints"
|
||||
)
|
||||
|
||||
add_blender_test(
|
||||
multires
|
||||
--python ${TEST_PYTHON_DIR}/bl_multires.py
|
||||
--
|
||||
--testdir "${TEST_SRC_DIR}/sculpting"
|
||||
)
|
||||
endif()
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
|
|
@ -658,7 +651,7 @@ endif()
|
|||
# BRUSH TESTS
|
||||
add_blender_test(
|
||||
bl_brush
|
||||
--python ${CMAKE_CURRENT_LIST_DIR}/bl_brush_test.py
|
||||
--python ${CMAKE_CURRENT_LIST_DIR}/sculpt_paint/brush_asset_test.py
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
|
|
@ -1436,8 +1429,15 @@ endif()
|
|||
|
||||
if(TEST_SRC_DIR_EXISTS)
|
||||
add_blender_test(
|
||||
bl_sculpt_operators
|
||||
--python ${CMAKE_CURRENT_LIST_DIR}/bl_sculpt.py
|
||||
bl_sculpt_mask
|
||||
--python ${CMAKE_CURRENT_LIST_DIR}/sculpt_paint/mask_test.py
|
||||
--
|
||||
--testdir "${TEST_SRC_DIR}/sculpting"
|
||||
)
|
||||
|
||||
add_blender_test(
|
||||
bl_multires
|
||||
--python ${CMAKE_CURRENT_LIST_DIR}/sculpt_paint/multires_operators_test.py
|
||||
--
|
||||
--testdir "${TEST_SRC_DIR}/sculpting"
|
||||
)
|
||||
|
|
@ -1469,20 +1469,6 @@ if(TEST_SRC_DIR_EXISTS)
|
|||
--
|
||||
--testdir "${TEST_SRC_DIR}/sculpting"
|
||||
)
|
||||
|
||||
add_blender_test(
|
||||
bl_sculpt_mask_filter
|
||||
--python ${CMAKE_CURRENT_LIST_DIR}/sculpt_paint/mask_filter_test.py
|
||||
--
|
||||
--testdir "${TEST_SRC_DIR}/sculpting"
|
||||
)
|
||||
|
||||
add_blender_test(
|
||||
bl_sculpt_mask_flood_fill
|
||||
--python ${CMAKE_CURRENT_LIST_DIR}/sculpt_paint/mask_flood_fill_test.py
|
||||
--
|
||||
--testdir "${TEST_SRC_DIR}/sculpting"
|
||||
)
|
||||
endif()
|
||||
|
||||
if(WITH_GPU_MESH_PAINT_TESTS AND TEST_SRC_DIR_EXISTS)
|
||||
|
|
|
|||
82
tests/python/sculpt_paint/dyntopo_test.py
Normal file
82
tests/python/sculpt_paint/dyntopo_test.py
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
# SPDX-FileCopyrightText: 2026 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
__all__ = (
|
||||
"main",
|
||||
)
|
||||
|
||||
import unittest
|
||||
import sys
|
||||
import pathlib
|
||||
import numpy as np
|
||||
|
||||
import bpy
|
||||
|
||||
"""
|
||||
blender -b --factory-startup --python tests/python/dyntopo_test.py -- --testdir tests/files/sculpting/
|
||||
"""
|
||||
|
||||
args = None
|
||||
|
||||
|
||||
class DetailFloodFillTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
bpy.ops.ed.undo_push()
|
||||
|
||||
bpy.ops.mesh.primitive_cube_add()
|
||||
bpy.ops.sculpt.sculptmode_toggle()
|
||||
bpy.ops.sculpt.dynamic_topology_toggle()
|
||||
|
||||
def test_operator_subdivides_mesh(self):
|
||||
"""Test that the operator generates a mesh with appropriately sized edges."""
|
||||
|
||||
max_edge_length = 1.0
|
||||
# Based on the detail_size::EDGE_LENGTH_MIN_FACTOR constant
|
||||
min_edge_length = max_edge_length * 0.4
|
||||
|
||||
bpy.context.scene.tool_settings.sculpt.detail_type_method = 'CONSTANT'
|
||||
bpy.context.scene.tool_settings.sculpt.constant_detail_resolution = max_edge_length
|
||||
|
||||
ret_val = bpy.ops.sculpt.detail_flood_fill()
|
||||
self.assertEqual({'FINISHED'}, ret_val)
|
||||
|
||||
# Toggle to ensure the mesh data is refreshed.
|
||||
bpy.ops.sculpt.dynamic_topology_toggle()
|
||||
|
||||
mesh = bpy.context.object.data
|
||||
for edge in mesh.edges:
|
||||
v0 = mesh.vertices[edge.vertices[0]]
|
||||
v1 = mesh.vertices[edge.vertices[1]]
|
||||
|
||||
length = (v0.co - v1.co).length
|
||||
|
||||
self.assertGreaterEqual(
|
||||
length,
|
||||
min_edge_length,
|
||||
f"Edge between {v0.index} and {v1.index} should be longer than minimum length")
|
||||
self.assertLessEqual(
|
||||
length,
|
||||
max_edge_length,
|
||||
f"Edge between {v0.index} and {v1.index} should be shorter than maximum length")
|
||||
|
||||
|
||||
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()
|
||||
|
|
@ -1,85 +0,0 @@
|
|||
# SPDX-FileCopyrightText: 2025 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
__all__ = (
|
||||
"main",
|
||||
)
|
||||
|
||||
import math
|
||||
import unittest
|
||||
import sys
|
||||
import pathlib
|
||||
import numpy as np
|
||||
|
||||
import bpy
|
||||
|
||||
"""
|
||||
blender -b --factory-startup --python tests/python/sculpt_paint/mask_filter_test.py -- --testdir tests/files/sculpting/
|
||||
"""
|
||||
|
||||
args = None
|
||||
|
||||
|
||||
class GrowMaskTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
bpy.ops.wm.open_mainfile(filepath=str(args.testdir / "partially_masked_sphere.blend"), load_ui=False)
|
||||
bpy.ops.ed.undo_push()
|
||||
|
||||
def test_grow_increases_number_of_masked_vertices(self):
|
||||
mesh = bpy.context.object.data
|
||||
mask_attr = mesh.attributes['.sculpt_mask']
|
||||
|
||||
num_vertices = mesh.attributes.domain_size('POINT')
|
||||
|
||||
old_mask_data = np.zeros(num_vertices, dtype=np.float32)
|
||||
mask_attr.data.foreach_get('value', old_mask_data)
|
||||
|
||||
bpy.ops.sculpt.mask_filter(filter_type='GROW')
|
||||
|
||||
new_mask_data = np.zeros(num_vertices, dtype=np.float32)
|
||||
mask_attr.data.foreach_get('value', new_mask_data)
|
||||
|
||||
self.assertGreater(np.count_nonzero(new_mask_data), np.count_nonzero(old_mask_data))
|
||||
|
||||
|
||||
class ShrinkMaskTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
bpy.ops.wm.open_mainfile(filepath=str(args.testdir / "partially_masked_sphere.blend"), load_ui=False)
|
||||
bpy.ops.ed.undo_push()
|
||||
|
||||
def test_shrink_decreases_number_of_masked_vertices(self):
|
||||
mesh = bpy.context.object.data
|
||||
mask_attr = mesh.attributes['.sculpt_mask']
|
||||
|
||||
num_vertices = mesh.attributes.domain_size('POINT')
|
||||
|
||||
old_mask_data = np.zeros(num_vertices, dtype=np.float32)
|
||||
mask_attr.data.foreach_get('value', old_mask_data)
|
||||
|
||||
bpy.ops.sculpt.mask_filter(filter_type='SHRINK')
|
||||
|
||||
new_mask_data = np.zeros(num_vertices, dtype=np.float32)
|
||||
mask_attr.data.foreach_get('value', new_mask_data)
|
||||
|
||||
self.assertLess(np.count_nonzero(new_mask_data), np.count_nonzero(old_mask_data))
|
||||
|
||||
|
||||
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()
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
# SPDX-FileCopyrightText: 2025 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
__all__ = (
|
||||
"main",
|
||||
)
|
||||
|
||||
import math
|
||||
import unittest
|
||||
import sys
|
||||
import pathlib
|
||||
import numpy as np
|
||||
|
||||
import bpy
|
||||
|
||||
"""
|
||||
blender -b --factory-startup --python tests/python/sculpt_paint/mask_flood_fill_test.py -- --testdir tests/files/sculpting/
|
||||
"""
|
||||
|
||||
args = None
|
||||
|
||||
|
||||
class ClearMaskTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
bpy.ops.wm.open_mainfile(filepath=str(args.testdir / "partially_masked_sphere.blend"), load_ui=False)
|
||||
bpy.ops.ed.undo_push()
|
||||
|
||||
def test_value_removes_attribute(self):
|
||||
mesh = bpy.context.object.data
|
||||
|
||||
bpy.ops.paint.mask_flood_fill(mode='VALUE', value=0)
|
||||
|
||||
self.assertFalse(mesh.attributes.get('.sculpt_mask'))
|
||||
|
||||
|
||||
class InvertMaskTest(unittest.TestCase):
|
||||
def test_invert_applies_correct_values(self):
|
||||
bpy.ops.wm.open_mainfile(filepath=str(args.testdir / "partially_masked_sphere.blend"), load_ui=False)
|
||||
bpy.ops.ed.undo_push()
|
||||
|
||||
mesh = bpy.context.object.data
|
||||
mask_attr = mesh.attributes['.sculpt_mask']
|
||||
|
||||
num_vertices = mesh.attributes.domain_size('POINT')
|
||||
|
||||
old_mask_data = np.zeros(num_vertices, dtype=np.float32)
|
||||
mask_attr.data.foreach_get('value', old_mask_data)
|
||||
|
||||
bpy.ops.paint.mask_flood_fill(mode='INVERT')
|
||||
|
||||
new_mask_data = np.zeros(num_vertices, dtype=np.float32)
|
||||
mask_attr.data.foreach_get('value', new_mask_data)
|
||||
|
||||
expected_mask_data = np.array([1.0 - m for m in old_mask_data])
|
||||
|
||||
self.assertEqual(expected_mask_data.tolist(), new_mask_data.tolist())
|
||||
|
||||
def test_invert_on_empty_fills_mesh(self):
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
bpy.ops.ed.undo_push()
|
||||
|
||||
bpy.ops.mesh.primitive_monkey_add()
|
||||
bpy.ops.sculpt.sculptmode_toggle()
|
||||
|
||||
bpy.ops.paint.mask_flood_fill(mode='INVERT')
|
||||
|
||||
mesh = bpy.context.object.data
|
||||
mask_attr = mesh.attributes['.sculpt_mask']
|
||||
|
||||
num_vertices = mesh.attributes.domain_size('POINT')
|
||||
|
||||
new_mask_data = np.zeros(num_vertices, dtype=np.float32)
|
||||
mask_attr.data.foreach_get('value', new_mask_data)
|
||||
|
||||
expected_mask_data = np.ones(num_vertices, dtype=np.float32)
|
||||
|
||||
self.assertEqual(expected_mask_data.tolist(), new_mask_data.tolist())
|
||||
|
||||
|
||||
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()
|
||||
|
|
@ -14,7 +14,7 @@ import numpy as np
|
|||
import bpy
|
||||
|
||||
"""
|
||||
blender -b --factory-startup --python tests/python/bl_sculpt.py -- --testdir tests/files/sculpting/
|
||||
blender -b --factory-startup --python tests/python/mask_test.py -- --testdir tests/files/sculpting/
|
||||
"""
|
||||
|
||||
args = None
|
||||
|
|
@ -41,6 +41,107 @@ def set_view3d_context_override(context_override):
|
|||
context_override["region"] = region
|
||||
|
||||
|
||||
class GrowMaskTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
bpy.ops.wm.open_mainfile(filepath=str(args.testdir / "partially_masked_sphere.blend"), load_ui=False)
|
||||
bpy.ops.ed.undo_push()
|
||||
|
||||
def test_grow_increases_number_of_masked_vertices(self):
|
||||
mesh = bpy.context.object.data
|
||||
mask_attr = mesh.attributes['.sculpt_mask']
|
||||
|
||||
num_vertices = mesh.attributes.domain_size('POINT')
|
||||
|
||||
old_mask_data = np.zeros(num_vertices, dtype=np.float32)
|
||||
mask_attr.data.foreach_get('value', old_mask_data)
|
||||
|
||||
bpy.ops.sculpt.mask_filter(filter_type='GROW')
|
||||
|
||||
new_mask_data = np.zeros(num_vertices, dtype=np.float32)
|
||||
mask_attr.data.foreach_get('value', new_mask_data)
|
||||
|
||||
self.assertGreater(np.count_nonzero(new_mask_data), np.count_nonzero(old_mask_data))
|
||||
|
||||
|
||||
class ShrinkMaskTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
bpy.ops.wm.open_mainfile(filepath=str(args.testdir / "partially_masked_sphere.blend"), load_ui=False)
|
||||
bpy.ops.ed.undo_push()
|
||||
|
||||
def test_shrink_decreases_number_of_masked_vertices(self):
|
||||
mesh = bpy.context.object.data
|
||||
mask_attr = mesh.attributes['.sculpt_mask']
|
||||
|
||||
num_vertices = mesh.attributes.domain_size('POINT')
|
||||
|
||||
old_mask_data = np.zeros(num_vertices, dtype=np.float32)
|
||||
mask_attr.data.foreach_get('value', old_mask_data)
|
||||
|
||||
bpy.ops.sculpt.mask_filter(filter_type='SHRINK')
|
||||
|
||||
new_mask_data = np.zeros(num_vertices, dtype=np.float32)
|
||||
mask_attr.data.foreach_get('value', new_mask_data)
|
||||
|
||||
self.assertLess(np.count_nonzero(new_mask_data), np.count_nonzero(old_mask_data))
|
||||
|
||||
|
||||
class ClearMaskTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
bpy.ops.wm.open_mainfile(filepath=str(args.testdir / "partially_masked_sphere.blend"), load_ui=False)
|
||||
bpy.ops.ed.undo_push()
|
||||
|
||||
def test_value_removes_attribute(self):
|
||||
mesh = bpy.context.object.data
|
||||
|
||||
bpy.ops.paint.mask_flood_fill(mode='VALUE', value=0)
|
||||
|
||||
self.assertFalse(mesh.attributes.get('.sculpt_mask'))
|
||||
|
||||
|
||||
class InvertMaskTest(unittest.TestCase):
|
||||
def test_invert_applies_correct_values(self):
|
||||
bpy.ops.wm.open_mainfile(filepath=str(args.testdir / "partially_masked_sphere.blend"), load_ui=False)
|
||||
bpy.ops.ed.undo_push()
|
||||
|
||||
mesh = bpy.context.object.data
|
||||
mask_attr = mesh.attributes['.sculpt_mask']
|
||||
|
||||
num_vertices = mesh.attributes.domain_size('POINT')
|
||||
|
||||
old_mask_data = np.zeros(num_vertices, dtype=np.float32)
|
||||
mask_attr.data.foreach_get('value', old_mask_data)
|
||||
|
||||
bpy.ops.paint.mask_flood_fill(mode='INVERT')
|
||||
|
||||
new_mask_data = np.zeros(num_vertices, dtype=np.float32)
|
||||
mask_attr.data.foreach_get('value', new_mask_data)
|
||||
|
||||
expected_mask_data = np.array([1.0 - m for m in old_mask_data])
|
||||
|
||||
self.assertEqual(expected_mask_data.tolist(), new_mask_data.tolist())
|
||||
|
||||
def test_invert_on_empty_fills_mesh(self):
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
bpy.ops.ed.undo_push()
|
||||
|
||||
bpy.ops.mesh.primitive_monkey_add()
|
||||
bpy.ops.sculpt.sculptmode_toggle()
|
||||
|
||||
bpy.ops.paint.mask_flood_fill(mode='INVERT')
|
||||
|
||||
mesh = bpy.context.object.data
|
||||
mask_attr = mesh.attributes['.sculpt_mask']
|
||||
|
||||
num_vertices = mesh.attributes.domain_size('POINT')
|
||||
|
||||
new_mask_data = np.zeros(num_vertices, dtype=np.float32)
|
||||
mask_attr.data.foreach_get('value', new_mask_data)
|
||||
|
||||
expected_mask_data = np.ones(num_vertices, dtype=np.float32)
|
||||
|
||||
self.assertEqual(expected_mask_data.tolist(), new_mask_data.tolist())
|
||||
|
||||
|
||||
class MaskByColorTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
bpy.ops.wm.open_mainfile(filepath=str(args.testdir / "plane_with_red_circle.blend"), load_ui=False)
|
||||
|
|
@ -127,48 +228,6 @@ class MaskFromCavityTest(unittest.TestCase):
|
|||
f"Vertex {i} should not be fully masked ({position_data[i]}) -> {mask_data[i]}")
|
||||
|
||||
|
||||
class DetailFloodFillTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
bpy.ops.ed.undo_push()
|
||||
|
||||
bpy.ops.mesh.primitive_cube_add()
|
||||
bpy.ops.sculpt.sculptmode_toggle()
|
||||
bpy.ops.sculpt.dynamic_topology_toggle()
|
||||
|
||||
def test_operator_subdivides_mesh(self):
|
||||
"""Test that the operator generates a mesh with appropriately sized edges."""
|
||||
|
||||
max_edge_length = 1.0
|
||||
# Based on the detail_size::EDGE_LENGTH_MIN_FACTOR constant
|
||||
min_edge_length = max_edge_length * 0.4
|
||||
|
||||
bpy.context.scene.tool_settings.sculpt.detail_type_method = 'CONSTANT'
|
||||
bpy.context.scene.tool_settings.sculpt.constant_detail_resolution = max_edge_length
|
||||
|
||||
ret_val = bpy.ops.sculpt.detail_flood_fill()
|
||||
self.assertEqual({'FINISHED'}, ret_val)
|
||||
|
||||
# Toggle to ensure the mesh data is refreshed.
|
||||
bpy.ops.sculpt.dynamic_topology_toggle()
|
||||
|
||||
mesh = bpy.context.object.data
|
||||
for edge in mesh.edges:
|
||||
v0 = mesh.vertices[edge.vertices[0]]
|
||||
v1 = mesh.vertices[edge.vertices[1]]
|
||||
|
||||
length = (v0.co - v1.co).length
|
||||
|
||||
self.assertGreaterEqual(
|
||||
length,
|
||||
min_edge_length,
|
||||
f"Edge between {v0.index} and {v1.index} should be longer than minimum length")
|
||||
self.assertLessEqual(
|
||||
length,
|
||||
max_edge_length,
|
||||
f"Edge between {v0.index} and {v1.index} should be shorter than maximum length")
|
||||
|
||||
|
||||
def main():
|
||||
global args
|
||||
import argparse
|
||||
Loading…
Add table
Add a link
Reference in a new issue