Curves: add Split operator

Adds Split operator to curves. It should have the same behavior as the
corresponding operator for legacy curves.

Pull Request: https://projects.blender.org/blender/blender/pulls/131788
This commit is contained in:
Laurynas Duburas
2025-02-24 11:32:59 +01:00
committed by Jacques Lucke
parent fc6a15b2eb
commit 2c42294557
9 changed files with 465 additions and 40 deletions

View File

@@ -5781,6 +5781,7 @@ def km_edit_curves(params):
("curves.separate", {"type": 'P', "value": 'PRESS'}, None),
("curves.select_more", {"type": 'NUMPAD_PLUS', "value": 'PRESS', "ctrl": True, "repeat": True}, None),
("curves.select_less", {"type": 'NUMPAD_MINUS', "value": 'PRESS', "ctrl": True, "repeat": True}, None),
("curves.split", {"type": 'Y', "value": 'PRESS'}, None),
*_template_items_proportional_editing(
params, connected=True, toggle_data_path="tool_settings.use_proportional_edit"),
("curves.tilt_clear", {"type": 'T', "value": 'PRESS', "alt": True}, None),

View File

@@ -5901,6 +5901,10 @@ class VIEW3D_MT_edit_curves_context_menu(Menu):
layout.operator_menu_enum("curves.handle_type_set", "type")
layout.separator()
layout.operator("curves.split")
class VIEW3D_MT_edit_pointcloud(Menu):
bl_label = "Point Cloud"

View File

@@ -507,6 +507,36 @@ void foreach_curve_by_type(const VArray<int8_t> &types,
FunctionRef<void(IndexMask)> poly_fn,
FunctionRef<void(IndexMask)> bezier_fn,
FunctionRef<void(IndexMask)> nurbs_fn);
using SelectedCallback = FunctionRef<void(
int curve_i, IndexRange curve_points, Span<IndexRange> selected_point_ranges)>;
using UnselectedCallback = FunctionRef<void(IndexRange curves, IndexRange unselected_points)>;
/**
* Calls callback function for each curve having selected points.
*
* \param mask: selected points.
* \param points_by_curve: The offsets of every curve into arrays on the points domain.
* \param selected_fn: callback function called for each curve with at least one point selected.
*/
void foreach_selected_point_ranges_per_curve(const IndexMask &mask,
const OffsetIndices<int> points_by_curve,
SelectedCallback selected_fn);
/**
* Calls callback function for each curve having selected points.
* Calls second callback for groups of curves with no points selected.
*
* \param mask: selected points.
* \param points_by_curve: The offsets of every curve into arrays on the points domain.
* \param selected_fn: callback function called for each curve with at least one point selected.
* \param unselected_fn: callback function called for groups of curves with no selected points.
*/
void foreach_selected_point_ranges_per_curve(const IndexMask &mask,
const OffsetIndices<int> points_by_curve,
SelectedCallback selected_fn,
UnselectedCallback unselected_fn);
namespace bezier {
/**

View File

@@ -83,6 +83,81 @@ void foreach_curve_by_type(const VArray<int8_t> &types,
call_if_not_empty(CURVE_TYPE_NURBS, nurbs_fn);
}
static void if_has_data_call_callback(const Span<int> offset_data,
const int begin,
const int end,
UnselectedCallback callback)
{
if (begin < end) {
const IndexRange curves = IndexRange::from_begin_end(begin, end);
const IndexRange points = IndexRange::from_begin_end(offset_data[begin], offset_data[end]);
callback(curves, points);
}
};
template<typename Fn>
static void foreach_selected_point_ranges_per_curve_(const IndexMask &mask,
const OffsetIndices<int> points_by_curve,
SelectedCallback selected_fn,
Fn unselected_fn)
{
Vector<IndexRange> ranges;
Span<int> offset_data = points_by_curve.data();
int curve_i = mask.is_empty() ? -1 : 0;
int range_first = mask.is_empty() ? 0 : mask.first();
int range_last = range_first - 1;
mask.foreach_index([&](const int64_t index) {
if (offset_data[curve_i + 1] <= index) {
int first_unselected_curve = curve_i;
if (range_last >= range_first) {
ranges.append(IndexRange::from_begin_end_inclusive(range_first, range_last));
selected_fn(curve_i, points_by_curve[curve_i], ranges);
ranges.clear();
first_unselected_curve++;
}
do {
++curve_i;
} while (offset_data[curve_i + 1] <= index);
if constexpr (std::is_invocable_r_v<void, Fn, IndexRange, IndexRange>) {
if_has_data_call_callback(offset_data, first_unselected_curve, curve_i, unselected_fn);
}
range_first = index;
}
else if (range_last + 1 != index) {
ranges.append(IndexRange::from_begin_end_inclusive(range_first, range_last));
range_first = index;
}
range_last = index;
});
if (range_last - range_first >= 0) {
ranges.append(IndexRange::from_begin_end_inclusive(range_first, range_last));
selected_fn(curve_i, points_by_curve[curve_i], ranges);
}
if constexpr (std::is_invocable_r_v<void, Fn, IndexRange, IndexRange>) {
if_has_data_call_callback(offset_data, curve_i + 1, points_by_curve.size(), unselected_fn);
}
}
void foreach_selected_point_ranges_per_curve(const IndexMask &mask,
const OffsetIndices<int> offset_indices,
SelectedCallback selected_fn)
{
foreach_selected_point_ranges_per_curve_<void()>(mask, offset_indices, selected_fn, nullptr);
}
void foreach_selected_point_ranges_per_curve(const IndexMask &mask,
const OffsetIndices<int> offset_indices,
SelectedCallback selected_fn,
UnselectedCallback unselected_fn)
{
foreach_selected_point_ranges_per_curve_<UnselectedCallback>(
mask, offset_indices, selected_fn, unselected_fn);
}
namespace bezier {
Array<float3> retrieve_all_positions(const bke::CurvesGeometry &curves,

View File

@@ -41,44 +41,6 @@ bool remove_selection(bke::CurvesGeometry &curves, const bke::AttrDomain selecti
return attributes.domain_size(selection_domain) != domain_size_orig;
}
static void foreach_content_slice_by_offsets(
const IndexMask &mask,
const OffsetIndices<int> offset_indices,
FunctionRef<void(Span<IndexRange> selected_points, IndexRange slice_points, int slice)> fn)
{
Vector<IndexRange> ranges;
Span<int> offset_data = offset_indices.data();
int slice = 0;
int range_first = mask.first();
int range_last = mask.first() - 1;
mask.foreach_index([&](const int64_t index) {
if (offset_data[slice + 1] <= index) {
if (range_last - range_first >= 0) {
ranges.append(IndexRange::from_begin_end_inclusive(range_first, range_last));
fn(ranges, offset_indices[slice], slice);
ranges.clear();
}
do {
++slice;
} while (offset_data[slice + 1] <= index);
range_first = index;
}
else if (range_last + 1 != index) {
ranges.append(IndexRange::from_begin_end_inclusive(range_first, range_last));
range_first = index;
}
range_last = index;
});
if (range_last - range_first >= 0) {
ranges.append(IndexRange::from_begin_end_inclusive(range_first, range_last));
fn(ranges, offset_indices[slice], slice);
}
}
static void curve_offsets_from_selection(const Span<IndexRange> selected_points,
const IndexRange points,
const int curve,
@@ -131,10 +93,10 @@ void duplicate_points(bke::CurvesGeometry &curves, const IndexMask &mask)
dst_cyclic.reserve(curves.curves_num());
/* Add the duplicated curves and points. */
foreach_content_slice_by_offsets(
bke::curves::foreach_selected_point_ranges_per_curve(
mask,
points_by_curve,
[&](Span<IndexRange> ranges_to_duplicate, IndexRange points, int curve) {
[&](const int curve, const IndexRange points, Span<IndexRange> ranges_to_duplicate) {
curve_offsets_from_selection(ranges_to_duplicate,
points,
curve,
@@ -269,6 +231,189 @@ void duplicate_curves(bke::CurvesGeometry &curves, const IndexMask &mask)
}
}
static void invert_ranges(const IndexRange universe,
const Span<IndexRange> ranges,
Array<IndexRange> &inverted)
{
const bool contains_first = ranges.first().first() == universe.first();
const bool contains_last = ranges.last().last() == universe.last();
inverted.reinitialize(ranges.size() - 1 + !contains_first + !contains_last);
int64_t start = contains_first ? ranges.first().one_after_last() : universe.first();
int i = 0;
for (const IndexRange range : ranges.drop_front(contains_first)) {
inverted[i++] = IndexRange::from_begin_end(start, range.first());
start = range.one_after_last();
}
if (!contains_last) {
inverted.last() = IndexRange::from_begin_end(start, universe.one_after_last());
}
}
static IndexRange extend_range(const IndexRange range, const IndexRange universe)
{
return IndexRange::from_begin_end_inclusive(math::max(range.start() - 1, universe.start()),
math::min(range.one_after_last(), universe.last()));
}
/**
* Extends each range by one point at both ends of it. Merges adjacent ranges if intersections
* occur.
*/
static void extend_range_by_1_within_bounds(const IndexRange universe,
const bool cyclic,
const Span<IndexRange> ranges,
Vector<IndexRange> &extended_ranges)
{
extended_ranges.clear();
if (ranges.is_empty()) {
return;
}
const bool first_match = ranges.first().first() == universe.first();
const bool last_match = ranges.last().last() == universe.last();
const bool add_first = cyclic && last_match && !first_match;
const bool add_last = cyclic && first_match && !last_match;
IndexRange current = add_first ? IndexRange::from_single(universe.first()) :
extend_range(ranges.first(), universe);
for (const IndexRange range : ranges.drop_front(!add_first)) {
const IndexRange extended = extend_range(range, universe);
if (extended.first() <= current.last()) {
current = IndexRange::from_begin_end_inclusive(current.start(), extended.last());
}
else {
extended_ranges.append(current);
current = extended;
}
}
extended_ranges.append(current);
if (add_last) {
extended_ranges.append(IndexRange::from_single(universe.last()));
}
}
static void copy_data_to_geometry(const bke::CurvesGeometry &src_curves,
const Span<int> dst_to_src_curve,
const Span<int> offsets,
const Span<bool> cyclic,
const Span<IndexRange> src_ranges,
const OffsetIndices<int> dst_offsets,
bke::CurvesGeometry &dst_curves)
{
dst_curves.resize(offsets.last(), dst_to_src_curve.size());
array_utils::copy(offsets, dst_curves.offsets_for_write());
dst_curves.cyclic_for_write().copy_from(cyclic);
const bke::AttributeAccessor src_attributes = src_curves.attributes();
bke::MutableAttributeAccessor dst_attributes = dst_curves.attributes_for_write();
bke::gather_attributes(src_attributes,
bke::AttrDomain::Curve,
bke::AttrDomain::Curve,
bke::attribute_filter_from_skip_ref({"cyclic"}),
dst_to_src_curve,
dst_attributes);
for (auto &attribute : bke::retrieve_attributes_for_transfer(
src_attributes,
dst_attributes,
ATTR_DOMAIN_MASK_POINT,
bke::attribute_filter_from_skip_ref(
ed::curves::get_curves_selection_attribute_names(src_curves))))
{
bke::attribute_math::gather_ranges_to_groups(
src_ranges, dst_offsets, attribute.src, attribute.dst.span);
attribute.dst.finish();
};
dst_curves.update_curve_types();
dst_curves.tag_topology_changed();
}
bke::CurvesGeometry split_points(const bke::CurvesGeometry &curves,
const IndexMask &points_to_split)
{
const OffsetIndices points_by_curve = curves.points_by_curve();
const VArray<bool> cyclic = curves.cyclic();
Vector<int> curve_map;
Vector<int> new_offsets({0});
Vector<IndexRange> src_ranges;
Vector<int> dst_offsets({0});
Vector<bool> new_cyclic;
Vector<IndexRange> deselect;
Array<IndexRange> unselected_curve_points;
Vector<IndexRange> curve_points_to_preserve;
bke::curves::foreach_selected_point_ranges_per_curve(
points_to_split,
points_by_curve,
[&](const int curve, const IndexRange points, const Span<IndexRange> selected_curve_points) {
const int points_start = new_offsets.last();
curve_offsets_from_selection(selected_curve_points,
points,
curve,
cyclic[curve],
new_offsets,
new_cyclic,
src_ranges,
dst_offsets,
curve_map);
const int split_points_num = new_offsets.last() - points_start;
/* Invert ranges to get non selected points. */
invert_ranges(points, selected_curve_points, unselected_curve_points);
/* Extended every range to left and right by one point. Any resulting intersection is
* merged. */
extend_range_by_1_within_bounds(
points, cyclic[curve], unselected_curve_points, curve_points_to_preserve);
const int size_before = curve_map.size();
curve_offsets_from_selection(curve_points_to_preserve,
points,
curve,
cyclic[curve] &&
(split_points_num <= curve_points_to_preserve.size()),
new_offsets,
new_cyclic,
src_ranges,
dst_offsets,
curve_map);
deselect.append(IndexRange::from_begin_end(size_before, curve_map.size()));
},
[&](const IndexRange curves, const IndexRange points) {
deselect.append(IndexRange::from_begin_size(curve_map.size(), curves.size()));
src_ranges.append(points);
dst_offsets.append(dst_offsets.last() + points.size());
int last_offset = new_offsets.last();
for (const int curve : curves) {
last_offset += points_by_curve[curve].size();
new_offsets.append(last_offset);
curve_map.append(curve);
new_cyclic.append(cyclic[curve]);
}
});
bke::CurvesGeometry new_curves;
copy_data_to_geometry(
curves, curve_map, new_offsets, new_cyclic, src_ranges, dst_offsets.as_span(), new_curves);
OffsetIndices<int> new_points_by_curve = new_curves.points_by_curve();
foreach_selection_attribute_writer(
new_curves, bke::AttrDomain::Point, [&](bke::GSpanAttributeWriter &selection) {
for (const IndexRange curves : deselect) {
for (const int curve : curves) {
fill_selection_false(selection.span.slice(new_points_by_curve[curve]));
}
}
});
return new_curves;
}
void add_curves(bke::CurvesGeometry &curves, const Span<int> new_sizes)
{
const int orig_points_num = curves.points_num();

View File

@@ -1137,6 +1137,43 @@ static void CURVES_OT_select_less(wmOperatorType *ot)
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
}
namespace split {
static int split_exec(bContext *C, wmOperator * /*op*/)
{
VectorSet<Curves *> unique_curves = get_unique_editable_curves(*C);
for (Curves *curves_id : unique_curves) {
CurvesGeometry &curves = curves_id->geometry.wrap();
IndexMaskMemory memory;
const IndexMask points_to_split = retrieve_all_selected_points(curves, memory);
if (points_to_split.is_empty()) {
continue;
}
curves = split_points(curves, points_to_split);
curves.calculate_bezier_auto_handles();
DEG_id_tag_update(&curves_id->id, ID_RECALC_GEOMETRY);
WM_event_add_notifier(C, NC_GEOM | ND_DATA, curves_id);
}
return OPERATOR_FINISHED;
}
} // namespace split
static void CURVES_OT_split(wmOperatorType *ot)
{
ot->name = "Split";
ot->idname = __func__;
ot->description = "Split selected points";
ot->exec = split::split_exec;
ot->poll = editable_curves_point_domain_poll;
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
}
namespace surface_set {
static bool surface_set_poll(bContext *C)
@@ -1771,6 +1808,7 @@ void operatortypes_curves()
WM_operatortype_append(CURVES_OT_select_more);
WM_operatortype_append(CURVES_OT_select_less);
WM_operatortype_append(CURVES_OT_separate);
WM_operatortype_append(CURVES_OT_split);
WM_operatortype_append(CURVES_OT_surface_set);
WM_operatortype_append(CURVES_OT_delete);
WM_operatortype_append(CURVES_OT_duplicate);

View File

@@ -66,6 +66,16 @@ IndexMask retrieve_selected_points(const bke::CurvesGeometry &curves, IndexMaskM
return retrieve_selected_points(curves, ".selection", memory);
}
IndexMask retrieve_all_selected_points(const bke::CurvesGeometry &curves, IndexMaskMemory &memory)
{
Vector<IndexMask> selection_by_attribute;
for (const StringRef selection_name : ed::curves::get_curves_selection_attribute_names(curves)) {
selection_by_attribute.append(
ed::curves::retrieve_selected_points(curves, selection_name, memory));
}
return IndexMask::from_union(selection_by_attribute, memory);
}
IndexMask retrieve_selected_points(const bke::CurvesGeometry &curves,
StringRef attribute_name,
IndexMaskMemory &memory)

View File

@@ -47,6 +47,19 @@ static bke::CurvesGeometry create_curves(const Vector<float3> positions,
return create_curves(Span<Vector<float3>>(&positions, 1), order, is_cyclic);
}
static void validate_positions(const Span<Vector<float3>> expected_positions,
const OffsetIndices<int> points_by_curve,
const Span<float3> positions)
{
for (const int curve : expected_positions.index_range()) {
const Span<float3> expected_curve_positions = expected_positions[curve];
const IndexRange points = points_by_curve[curve];
for (const int point : expected_curve_positions.index_range()) {
EXPECT_EQ(positions[points[point]], expected_curve_positions[point]);
}
}
}
TEST(curves_editors, DuplicatePointsTwoSingle)
{
/* Two points from single curve. */
@@ -135,4 +148,104 @@ TEST(curves_editors, DuplicatePointsTwoCyclic)
EXPECT_TRUE(positions[15] == expected_positions[2][0]);
}
TEST(curves_editors, SplitPointsTwoSingle)
{
/* Split two points from single curve. */
const Vector<float3> positions = {{-1.5, 0, 0}, {-1, 1, 0}, {1, 1, 0}, {1.5, 0, 0}};
bke::CurvesGeometry curves = create_curves(positions, 4, {});
IndexMaskMemory memory;
const IndexMask mask = IndexMask::from_indices<int>({1, 2}, memory);
bke::CurvesGeometry new_curves = split_points(curves, mask);
const Vector<Vector<float3>> expected_positions = {
{{-1, 1, 0}, {1, 1, 0}}, {{-1.5, 0, 0}, {-1, 1, 0}}, {{1, 1, 0}, {1.5, 0, 0}}};
EXPECT_EQ(new_curves.curves_num(), expected_positions.size());
validate_positions(expected_positions, new_curves.points_by_curve(), new_curves.positions());
}
TEST(curves_editors, SplitPointsFourThree)
{
/* Four points from three curves. One curve has one point. */
const Vector<Vector<float3>> positions = {
{{-1.5, 0, 0}, {-1, 1, 0}, {1, 1, 0}, {1.5, 0, 0}},
{{0, 0, 0}},
{{-1.5, 0, 0}, {-1, 1, 0}, {1, 1, 0}, {1.5, 0, 0}, {1, -1, 0}}};
bke::CurvesGeometry curves = create_curves(positions, 4, {});
IndexMaskMemory memory;
const IndexMask mask = IndexMask::from_indices(Array<int>{0, 1, 4, 9}.as_span(), memory);
bke::CurvesGeometry new_curves = split_points(curves, mask);
const Vector<Vector<float3>> expected_positions = {
{{-1.5, 0, 0}, {-1, 1, 0}},
{{-1, 1, 0}, {1, 1, 0}, {1.5, 0, 0}},
{{0, 0, 0}},
{{1, -1, 0}},
{{-1.5, 0, 0}, {-1, 1, 0}, {1, 1, 0}, {1.5, 0, 0}, {1, -1, 0}}};
EXPECT_EQ(new_curves.curves_num(), expected_positions.size());
validate_positions(expected_positions, new_curves.points_by_curve(), new_curves.positions());
}
TEST(curves_editors, SplitPointsTwoCyclic)
{
/* Two points from cyclic curve. Points are on cycle. */
const Vector<Vector<float3>> positions = {
{{-1.5, 0, 0}, {-1, 1, 0}, {1, 1, 0}, {1.5, 0, 0}},
{{0, 0, 0}},
{{1, 1, 0}, {1, -1, 0}, {-1, -1, 0}, {-1, 1, 0}},
{{-1.5, 0, 0}, {-1, 1, 0}, {1, 1, 0}, {1.5, 0, 0}, {1, -1, 0}}};
bke::CurvesGeometry curves = create_curves(positions, 4, {2});
IndexMaskMemory memory;
const IndexMask mask = IndexMask::from_indices(Array<int>{5, 8}.as_span(), memory);
bke::CurvesGeometry new_curves = split_points(curves, mask);
const Vector<Vector<float3>> expected_positions = {
{{-1.5, 0, 0}, {-1, 1, 0}, {1, 1, 0}, {1.5, 0, 0}},
{{0, 0, 0}},
{{-1, 1, 0}, {1, 1, 0}},
{{1, 1, 0}, {1, -1, 0}, {-1, -1, 0}, {-1, 1, 0}},
{{-1.5, 0, 0}, {-1, 1, 0}, {1, 1, 0}, {1.5, 0, 0}, {1, -1, 0}}};
EXPECT_EQ(new_curves.curves_num(), expected_positions.size());
validate_positions(expected_positions, new_curves.points_by_curve(), new_curves.positions());
Array<bool> expected_cyclic = {false, false, false, false, false};
VArray<bool> cyclic = new_curves.cyclic();
for (const int i : expected_cyclic.index_range()) {
EXPECT_EQ(expected_cyclic[i], cyclic[i]);
}
}
TEST(curves_editors, SplitPointsTwoTouchCyclic)
{
/* Two points from cyclic curve. Points are touching cycle. */
const Vector<Vector<float3>> positions = {
{{-1.5, 0, 0}, {-1, 1, 0}, {1, 1, 0}, {1.5, 0, 0}},
{{0, 0, 0}},
{{1, 1, 0}, {1, -1, 0}, {-1, -1, 0}, {-1, 1, 0}},
{{-1.5, 0, 0}, {-1, 1, 0}, {1, 1, 0}, {1.5, 0, 0}, {1, -1, 0}}};
bke::CurvesGeometry curves = create_curves(positions, 4, {2});
IndexMaskMemory memory;
const IndexMask mask = IndexMask::from_indices(Array<int>{5, 6}.as_span(), memory);
bke::CurvesGeometry new_curves = split_points(curves, mask);
const Vector<Vector<float3>> expected_positions = {
{{-1.5, 0, 0}, {-1, 1, 0}, {1, 1, 0}, {1.5, 0, 0}},
{{0, 0, 0}},
{{1, 1, 0}, {1, -1, 0}},
{{1, -1, 0}, {-1, -1, 0}, {-1, 1, 0}, {1, 1, 0}},
{{-1.5, 0, 0}, {-1, 1, 0}, {1, 1, 0}, {1.5, 0, 0}, {1, -1, 0}}};
EXPECT_EQ(new_curves.curves_num(), expected_positions.size());
validate_positions(expected_positions, new_curves.points_by_curve(), new_curves.positions());
}
} // namespace blender::ed::curves::tests

View File

@@ -230,6 +230,12 @@ IndexMask retrieve_selected_points(const bke::CurvesGeometry &curves,
IndexMaskMemory &memory);
IndexMask retrieve_selected_points(const Curves &curves_id, IndexMaskMemory &memory);
/**
* Find points that are selected (a selection factor greater than zero) or have
* any of their Bezier handle selected.
*/
IndexMask retrieve_all_selected_points(const bke::CurvesGeometry &curves, IndexMaskMemory &memory);
/**
* If the selection_id attribute doesn't exist, create it with the requested type (bool or float).
*/
@@ -419,6 +425,9 @@ bool remove_selection(bke::CurvesGeometry &curves, bke::AttrDomain selection_dom
void duplicate_points(bke::CurvesGeometry &curves, const IndexMask &mask);
void duplicate_curves(bke::CurvesGeometry &curves, const IndexMask &mask);
bke::CurvesGeometry split_points(const bke::CurvesGeometry &curves,
const IndexMask &points_to_split);
/**
* Adds new curves to \a curves.
* \param new_sizes: The new size for each curve. Sizes must be > 0.