Fix: Curves extrude with all points selected

CurvesGeometry has no ".selection" attribute when all control points
are selected. The earlier code assumed that the attribute always exists.
Also Python tests are added for the "extrude" operator.

Pull Request: https://projects.blender.org/blender/blender/pulls/117095
This commit is contained in:
laurynas 2024-01-16 21:59:15 +01:00 committed by Hans Goudey
parent bff51ae66c
commit a6fd1f5034
3 changed files with 208 additions and 3 deletions

View file

@ -17,7 +17,7 @@ namespace blender::ed::curves {
/**
* Merges copy intervals at curve endings to minimize number of copy operations.
* For example above intervals [0, 3, 4, 4, 4] became [0, 4, 4].
* For example given in function 'extrude_curves' intervals [0, 3, 4, 4, 4] became [0, 4, 4].
* Leading to only two copy operations.
*/
static Span<int> compress_intervals(const Span<IndexRange> curve_interval_ranges,
@ -263,7 +263,8 @@ static void extrude_curves(Curves &curves_id)
Array<IndexRange> curve_interval_ranges(curves_num);
/* Per curve boolean indicating if first interval in a curve is selected.
* Other can be calculated as in a curve two adjacent intervals can have same selection state. */
* Other can be calculated as in a curve two adjacent intervals can not have same selection
* state. */
Array<bool> is_first_selected(curves_num);
calc_curves_extrusion(extruded_points,
@ -276,7 +277,11 @@ static void extrude_curves(Curves &curves_id)
new_curves.resize(new_offsets.last(), new_curves.curves_num());
const bke::AttributeAccessor src_attributes = curves.attributes();
const GVArraySpan src_selection = *src_attributes.lookup(".selection", bke::AttrDomain::Point);
GVArray src_selection_array = *src_attributes.lookup(".selection", bke::AttrDomain::Point);
if (!src_selection_array) {
src_selection_array = VArray<bool>::ForSingle(true, curves.points_num());
}
const GVArraySpan src_selection = src_selection_array;
const CPPType &src_selection_type = src_selection.type();
bke::GSpanAttributeWriter dst_selection = ensure_selection_attribute(
new_curves,

View file

@ -265,6 +265,14 @@ add_blender_test(
--run-all-tests
)
add_blender_test(
curves_extrude
${TEST_SRC_DIR}/modeling/curves_extrude.blend
--python ${TEST_PYTHON_DIR}/curves_extrude.py
--
--run-all-tests
)
# ------------------------------------------------------------------------------
# MODIFIERS TESTS
add_blender_test(

View file

@ -0,0 +1,192 @@
# SPDX-FileCopyrightText: 2020-2023 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later
from abc import ABC, abstractmethod
import bpy
import os
import sys
sys.path.append(os.path.dirname(os.path.realpath(__file__)))
from modules.mesh_test import RunTest
class CurvesTest(ABC):
def __init__(self, test_object_name, exp_object_name, test_name=None):
self.test_object_name = test_object_name
self.exp_object_name = exp_object_name
self.test_object = bpy.data.objects[self.test_object_name]
self.expected_object = bpy.data.objects[self.exp_object_name]
self.verbose = os.getenv("BLENDER_VERBOSE") is not None
if test_name:
self.test_name = test_name
else:
filepath = bpy.data.filepath
self.test_name = bpy.path.display_name_from_filepath(filepath)
self._failed_tests_list = []
def create_evaluated_object(self):
"""
Creates an evaluated object.
"""
bpy.context.view_layer.objects.active = self.test_object
# Duplicate test object.
bpy.ops.object.mode_set(mode="OBJECT")
bpy.ops.object.select_all(action="DESELECT")
bpy.context.view_layer.objects.active = self.test_object
self.test_object.select_set(True)
bpy.ops.object.duplicate()
self.evaluated_object = bpy.context.active_object
self.evaluated_object.name = "evaluated_object"
@staticmethod
def _print_result(result):
"""
Prints the comparison, selection and validation result.
"""
print("Results:")
for key in result:
print("{} : {}".format(key, result[key][1]))
print()
def run_test(self):
"""
Runs a single test, runs it again if test file is updated.
"""
print("\nSTART {} test.".format(self.test_name))
self.create_evaluated_object()
self.apply_operations()
result = self.compare_objects(self.evaluated_object, self.expected_object)
# Initializing with True to get correct resultant of result_code booleans.
success = True
inside_loop_flag = False
for key in result:
inside_loop_flag = True
success = success and result[key][0]
# Check "success" is actually evaluated and is not the default True value.
if not inside_loop_flag:
success = False
if success:
self.print_passed_test_result(result)
# Clean up.
if self.verbose:
print("Cleaning up...")
# Delete evaluated_test_object.
bpy.ops.object.delete()
return True
else:
self.print_failed_test_result(result)
return False
@abstractmethod
def apply_operations(self, evaluated_test_object_name):
pass
@staticmethod
def compare_curves(evaluated_curves, expected_curves):
if len(evaluated_curves.attributes.items()) != len(expected_curves.attributes.items()):
print("Attribute count doesn't match")
for a_idx, attribute in evaluated_curves.attributes.items():
expected_attribute = expected_curves.attributes[a_idx]
if len(attribute.data.items()) != len(expected_attribute.data.items()):
print("Attribute data length doesn't match")
value_attr_name = ('vector' if attribute.data_type == 'FLOAT_VECTOR'
or attribute.data_type == 'FLOAT2' else
'color' if attribute.data_type == 'FLOAT_COLOR' else 'value')
for v_idx, attribute_value in attribute.data.items():
if getattr(
attribute_value,
value_attr_name) != getattr(
expected_attribute.data[v_idx],
value_attr_name):
print("Attribute '{}' values do not match".format(attribute.name))
return False
return True
def compare_objects(self, evaluated_object, expected_object):
result_codes = {}
equal = self.compare_curves(evaluated_object.data, expected_object.data)
result_codes['Curves Comparison'] = (equal, evaluated_object.data)
return result_codes
def print_failed_test_result(self, result):
"""
Print results for failed test.
"""
print("FAILED {} test with the following: ".format(self.test_name))
def print_passed_test_result(self, result):
"""
Print results for passing test.
"""
print("PASSED {} test successfully.".format(self.test_name))
class CurvesOpTest(CurvesTest):
def __init__(self, test_name, test_object_name, exp_object_name, operators_stack):
super().__init__(test_object_name, exp_object_name, test_name)
self.operators_stack = operators_stack
def apply_operations(self):
for operator_name in self.operators_stack:
bpy.ops.object.mode_set(mode='EDIT')
curves_operator = getattr(bpy.ops.curves, operator_name)
try:
retval = curves_operator()
except AttributeError:
raise AttributeError("bpy.ops.curves has no attribute {}".format(operator_name))
except TypeError as ex:
raise TypeError("Incorrect operator parameters {!r} raised {!r}".format([], ex))
if retval != {'FINISHED'}:
raise RuntimeError("Unexpected operator return value: {}".format(operator_name))
bpy.ops.object.mode_set(mode='OBJECT')
def main():
tests = [
CurvesOpTest("Extrude 1 Point Curve", "a_test1PointCurve", "a_test1PointCurveExpected", ['extrude']),
CurvesOpTest("Extrude Middle Points", "b_testMiddlePoints", "b_testMiddlePointsExpected", ['extrude']),
CurvesOpTest("Extrude End Points", "c_testEndPoints", "c_testEndPointsExpected", ['extrude']),
CurvesOpTest("Extrude Neighbors In Separate Curves", "d_testNeighborsInCurves", "d_testNeighborsInCurvesExpected", ['extrude']),
CurvesOpTest("Extrude Edge Curves", "e_testEdgeCurves", "e_testEdgeCurvesExpected", ['extrude']),
CurvesOpTest("Extrude Middle Curve", "f_testMiddleCurve", "f_testMiddleCurveExpected", ['extrude']),
CurvesOpTest("Extrude All Points", "g_testAllPoints", "g_testAllPointsExpected", ['extrude'])
]
curves_extrude_test = RunTest(tests)
command = list(sys.argv)
for i, cmd in enumerate(command):
if cmd == "--run-all-tests":
curves_extrude_test.do_compare = True
curves_extrude_test.run_all_tests()
break
elif cmd == "--run-test":
curves_extrude_test.do_compare = False
name = command[i + 1]
curves_extrude_test.run_test(name)
break
if __name__ == "__main__":
main()