mirror of
https://projects.blender.org/blender/blender.git
synced 2025-01-22 15:32:15 -05:00
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:
parent
bff51ae66c
commit
a6fd1f5034
3 changed files with 208 additions and 3 deletions
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
192
tests/python/curves_extrude.py
Normal file
192
tests/python/curves_extrude.py
Normal 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()
|
Loading…
Reference in a new issue