Curves: Separate to object operator

Use the existing "remove_points_and_split" utility added
for grease pencil to implement the operator for the curves
object type. The whole structure is similar to the recently
added point cloud separate operator (4cd3540579).

Pull Request: https://projects.blender.org/blender/blender/pulls/134763
This commit is contained in:
Hans Goudey
2025-02-18 19:31:30 +01:00
committed by Hans Goudey
parent e2c1922c60
commit a99f9496a0
11 changed files with 273 additions and 112 deletions

View File

@@ -5778,6 +5778,7 @@ def km_edit_curves(params):
"shift": True}, {"properties": [("deselect", True)]}),
("curves.delete", {"type": 'X', "value": 'PRESS'}, None),
("curves.delete", {"type": 'DEL', "value": 'PRESS'}, None),
("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),
*_template_items_proportional_editing(

View File

@@ -25,6 +25,7 @@ set(SRC
intern/curves_selection.cc
intern/curves_undo.cc
intern/select_linked_pick.cc
intern/separate.cc
)
set(LIB

View File

@@ -1771,6 +1771,7 @@ void operatortypes_curves()
WM_operatortype_append(CURVES_OT_select_linked_pick);
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_surface_set);
WM_operatortype_append(CURVES_OT_delete);
WM_operatortype_append(CURVES_OT_duplicate);

View File

@@ -0,0 +1,128 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_index_mask.hh"
#include "BLI_task.hh"
#include "BLI_vector_set.hh"
#include "BKE_context.hh"
#include "BKE_layer.hh"
#include "BKE_lib_id.hh"
#include "BKE_curves.hh"
#include "ED_curves.hh"
#include "ED_object.hh"
#include "DNA_layer_types.h"
#include "DEG_depsgraph.hh"
#include "DEG_depsgraph_build.hh"
#include "GEO_curves_remove_and_split.hh"
#include "WM_api.hh"
namespace blender::ed::curves {
static int separate_exec(bContext *C, wmOperator * /*op*/)
{
Main *bmain = CTX_data_main(C);
Scene *scene = CTX_data_scene(C);
ViewLayer *view_layer = CTX_data_view_layer(C);
Vector<Base *> bases = BKE_view_layer_array_from_bases_in_edit_mode(
scene, view_layer, CTX_wm_view3d(C));
VectorSet<Curves *> src_curves;
for (Base *base_src : bases) {
src_curves.add(static_cast<Curves *>(base_src->object->data));
}
/* Modify new curves and generate new curves in parallel. */
Array<std::optional<bke::CurvesGeometry>> dst_geometry(src_curves.size());
threading::parallel_for(dst_geometry.index_range(), 1, [&](const IndexRange range) {
for (const int i : range) {
Curves &src = *src_curves[i];
IndexMaskMemory memory;
switch (bke::AttrDomain(src.selection_domain)) {
case bke::AttrDomain::Point: {
const IndexMask selection = retrieve_selected_points(src, memory);
if (selection.is_empty()) {
continue;
}
dst_geometry[i] = bke::curves_copy_point_selection(src.geometry.wrap(), selection, {});
src.geometry.wrap() = geometry::remove_points_and_split(src.geometry.wrap(), selection);
break;
}
case bke::AttrDomain::Curve: {
const IndexMask selection = retrieve_selected_curves(src, memory);
if (selection.is_empty()) {
continue;
}
dst_geometry[i] = bke::curves_copy_curve_selection(src.geometry.wrap(), selection, {});
src.geometry.wrap().remove_curves(selection, {});
break;
}
default:
BLI_assert_unreachable();
break;
}
}
});
/* Move new curves into main data-base. */
Array<Curves *> dst_curves(src_curves.size(), nullptr);
for (const int i : dst_curves.index_range()) {
if (std::optional<bke::CurvesGeometry> &dst = dst_geometry[i]) {
dst_curves[i] = BKE_curves_add(bmain, BKE_id_name(src_curves[i]->id));
dst_curves[i]->geometry.wrap() = std::move(*dst);
bke::curves_copy_parameters(*src_curves[i], *dst_curves[i]);
}
}
/* Skip processing objects with no selected elements. */
bases.remove_if([&](Base *base) {
Curves *curves = static_cast<Curves *>(base->object->data);
return dst_curves[src_curves.index_of(curves)] == nullptr;
});
if (bases.is_empty()) {
return OPERATOR_CANCELLED;
}
/* Add new objects for the new curves. */
for (Base *base_src : bases) {
Curves *src = static_cast<Curves *>(base_src->object->data);
Curves *dst = dst_curves[src_curves.index_of(src)];
Base *base_dst = object::add_duplicate(
bmain, scene, view_layer, base_src, eDupli_ID_Flags(U.dupflag) & USER_DUP_ACT);
Object *object_dst = base_dst->object;
object_dst->mode = OB_MODE_OBJECT;
object_dst->data = dst;
DEG_id_tag_update(&src->id, ID_RECALC_GEOMETRY);
DEG_id_tag_update(&dst->id, ID_RECALC_GEOMETRY);
WM_event_add_notifier(C, NC_OBJECT | ND_DRAW, base_src->object);
WM_event_add_notifier(C, NC_OBJECT | ND_DRAW, object_dst);
}
DEG_relations_tag_update(bmain);
return OPERATOR_FINISHED;
}
void CURVES_OT_separate(wmOperatorType *ot)
{
ot->name = "Separate";
ot->idname = "CURVES_OT_separate";
ot->description = "Separate selected geometry into a new object";
ot->exec = separate_exec;
ot->poll = editable_curves_in_edit_mode_poll;
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
}
} // namespace blender::ed::curves

View File

@@ -62,6 +62,7 @@
#include "ED_transform_snap_object_context.hh"
#include "ED_view3d.hh"
#include "GEO_curves_remove_and_split.hh"
#include "GEO_join_geometries.hh"
#include "GEO_realize_instances.hh"
#include "GEO_reorder.hh"
@@ -418,106 +419,6 @@ static void GREASE_PENCIL_OT_stroke_simplify(wmOperatorType *ot)
/** \name Delete Operator
* \{ */
bke::CurvesGeometry remove_points_and_split(const bke::CurvesGeometry &curves,
const IndexMask &mask)
{
const OffsetIndices<int> points_by_curve = curves.points_by_curve();
const VArray<bool> src_cyclic = curves.cyclic();
Array<bool> points_to_delete(curves.points_num());
mask.to_bools(points_to_delete.as_mutable_span());
const int total_points = points_to_delete.as_span().count(false);
/* Return if deleting everything. */
if (total_points == 0) {
return {};
}
int curr_dst_point_id = 0;
Array<int> dst_to_src_point(total_points);
Vector<int> dst_curve_counts;
Vector<int> dst_to_src_curve;
Vector<bool> dst_cyclic;
for (const int curve_i : curves.curves_range()) {
const IndexRange points = points_by_curve[curve_i];
const Span<bool> curve_points_to_delete = points_to_delete.as_span().slice(points);
const bool curve_cyclic = src_cyclic[curve_i];
/* Note, these ranges start at zero and needed to be shifted by `points.first()` */
const Vector<IndexRange> ranges_to_keep = array_utils::find_all_ranges(curve_points_to_delete,
false);
if (ranges_to_keep.is_empty()) {
continue;
}
const bool is_last_segment_selected = curve_cyclic && ranges_to_keep.first().first() == 0 &&
ranges_to_keep.last().last() == points.size() - 1;
const bool is_curve_self_joined = is_last_segment_selected && ranges_to_keep.size() != 1;
const bool is_cyclic = ranges_to_keep.size() == 1 && is_last_segment_selected;
IndexRange range_ids = ranges_to_keep.index_range();
/* Skip the first range because it is joined to the end of the last range. */
for (const int range_i : ranges_to_keep.index_range().drop_front(is_curve_self_joined)) {
const IndexRange range = ranges_to_keep[range_i];
int count = range.size();
for (const int src_point : range.shift(points.first())) {
dst_to_src_point[curr_dst_point_id++] = src_point;
}
/* Join the first range to the end of the last range. */
if (is_curve_self_joined && range_i == range_ids.last()) {
const IndexRange first_range = ranges_to_keep[range_ids.first()];
for (const int src_point : first_range.shift(points.first())) {
dst_to_src_point[curr_dst_point_id++] = src_point;
}
count += first_range.size();
}
dst_curve_counts.append(count);
dst_to_src_curve.append(curve_i);
dst_cyclic.append(is_cyclic);
}
}
const int total_curves = dst_to_src_curve.size();
bke::CurvesGeometry dst_curves(total_points, total_curves);
BKE_defgroup_copy_list(&dst_curves.vertex_group_names, &curves.vertex_group_names);
MutableSpan<int> new_curve_offsets = dst_curves.offsets_for_write();
array_utils::copy(dst_curve_counts.as_span(), new_curve_offsets.drop_back(1));
offset_indices::accumulate_counts_to_offsets(new_curve_offsets);
bke::MutableAttributeAccessor dst_attributes = dst_curves.attributes_for_write();
const bke::AttributeAccessor src_attributes = curves.attributes();
/* Transfer curve attributes. */
gather_attributes(src_attributes,
bke::AttrDomain::Curve,
bke::AttrDomain::Curve,
bke::attribute_filter_from_skip_ref({"cyclic"}),
dst_to_src_curve,
dst_attributes);
array_utils::copy(dst_cyclic.as_span(), dst_curves.cyclic_for_write());
/* Transfer point attributes. */
gather_attributes(src_attributes,
bke::AttrDomain::Point,
bke::AttrDomain::Point,
{},
dst_to_src_point,
dst_attributes);
dst_curves.update_curve_types();
dst_curves.remove_attributes_based_on_types();
return dst_curves;
}
static int grease_pencil_delete_exec(bContext *C, wmOperator * /*op*/)
{
const Scene *scene = CTX_data_scene(C);
@@ -542,7 +443,7 @@ static int grease_pencil_delete_exec(bContext *C, wmOperator * /*op*/)
curves.remove_curves(elements, {});
}
else if (selection_domain == bke::AttrDomain::Point) {
curves = remove_points_and_split(curves, elements);
curves = geometry::remove_points_and_split(curves, elements);
}
info.drawing.tag_topology_changed();
changed = true;
@@ -2165,7 +2066,7 @@ static bool grease_pencil_separate_selected(bContext &C,
/* Copy strokes to new CurvesGeometry. */
drawing_dst->strokes_for_write() = bke::curves_copy_point_selection(
curves_src, selected_points, {});
curves_src = remove_points_and_split(curves_src, selected_points);
curves_src = geometry::remove_points_and_split(curves_src, selected_points);
info.drawing.tag_topology_changed();
drawing_dst->tag_topology_changed();
@@ -2592,7 +2493,7 @@ static int grease_pencil_copy_strokes_exec(bContext *C, wmOperator *op)
}
else if (selection_domain == bke::AttrDomain::Point) {
const IndexMask selected_points = ed::curves::retrieve_selected_points(curves, memory);
copied_curves = remove_points_and_split(
copied_curves = geometry::remove_points_and_split(
curves, selected_points.complement(curves.points_range(), memory));
num_elements_copied += copied_curves.points_num();
}

View File

@@ -140,6 +140,7 @@ void CURVES_OT_attribute_set(wmOperatorType *ot);
void CURVES_OT_draw(wmOperatorType *ot);
void CURVES_OT_extrude(wmOperatorType *ot);
void CURVES_OT_select_linked_pick(wmOperatorType *ot);
void CURVES_OT_separate(wmOperatorType *ot);
/** \} */

View File

@@ -914,13 +914,6 @@ GreasePencilLineartModifierData *get_first_lineart_modifier(const Object &ob);
GreasePencil *from_context(bContext &C);
/**
* Remove the points in the \a point_mask and split each curve at the points that are removed (if
* necessary).
*/
bke::CurvesGeometry remove_points_and_split(const bke::CurvesGeometry &curves,
const IndexMask &point_mask);
/* Make sure selection domain is updated to match the current selection mode. */
bool ensure_selection_domain(ToolSettings *ts, Object *object);

View File

@@ -39,6 +39,7 @@
#include "DEG_depsgraph_query.hh"
#include "GEO_curves_remove_and_split.hh"
#include "GEO_join_geometries.hh"
#include "GEO_smooth_curves.hh"
@@ -1800,8 +1801,8 @@ static bool remove_points_and_split_from_drawings(
if (Drawing *drawing = get_current_drawing_or_duplicate_for_autokey(
scene, grease_pencil, info.layer_index))
{
drawing->strokes_for_write() = ed::greasepencil::remove_points_and_split(drawing->strokes(),
points_to_remove);
drawing->strokes_for_write() = geometry::remove_points_and_split(drawing->strokes(),
points_to_remove);
drawing->tag_topology_changed();
changed = true;
}

View File

@@ -14,6 +14,7 @@ set(INC_SYS
set(SRC
intern/add_curves_on_mesh.cc
intern/curve_constraints.cc
intern/curves_remove_and_split.cc
intern/extend_curves.cc
intern/extract_elements.cc
intern/fillet_curves.cc

View File

@@ -0,0 +1,18 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "BKE_curves.hh"
namespace blender::geometry {
/**
* Remove the points in the \a point_mask and split each curve at the points that are removed (if
* necessary).
*/
bke::CurvesGeometry remove_points_and_split(const bke::CurvesGeometry &curves,
const IndexMask &mask);
} // namespace blender::geometry

View File

@@ -0,0 +1,115 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_array_utils.hh"
#include "BKE_attribute.hh"
#include "BKE_curves.hh"
#include "BKE_deform.hh"
#include "GEO_curves_remove_and_split.hh"
namespace blender::geometry {
bke::CurvesGeometry remove_points_and_split(const bke::CurvesGeometry &curves,
const IndexMask &mask)
{
const OffsetIndices<int> points_by_curve = curves.points_by_curve();
const VArray<bool> src_cyclic = curves.cyclic();
Array<bool> points_to_delete(curves.points_num());
mask.to_bools(points_to_delete.as_mutable_span());
const int total_points = points_to_delete.as_span().count(false);
/* Return if deleting everything. */
if (total_points == 0) {
return {};
}
int curr_dst_point_id = 0;
Array<int> dst_to_src_point(total_points);
Vector<int> dst_curve_counts;
Vector<int> dst_to_src_curve;
Vector<bool> dst_cyclic;
for (const int curve_i : curves.curves_range()) {
const IndexRange points = points_by_curve[curve_i];
const Span<bool> curve_points_to_delete = points_to_delete.as_span().slice(points);
const bool curve_cyclic = src_cyclic[curve_i];
/* Note, these ranges start at zero and needed to be shifted by `points.first()` */
const Vector<IndexRange> ranges_to_keep = array_utils::find_all_ranges(curve_points_to_delete,
false);
if (ranges_to_keep.is_empty()) {
continue;
}
const bool is_last_segment_selected = curve_cyclic && ranges_to_keep.first().first() == 0 &&
ranges_to_keep.last().last() == points.size() - 1;
const bool is_curve_self_joined = is_last_segment_selected && ranges_to_keep.size() != 1;
const bool is_cyclic = ranges_to_keep.size() == 1 && is_last_segment_selected;
IndexRange range_ids = ranges_to_keep.index_range();
/* Skip the first range because it is joined to the end of the last range. */
for (const int range_i : ranges_to_keep.index_range().drop_front(is_curve_self_joined)) {
const IndexRange range = ranges_to_keep[range_i];
int count = range.size();
for (const int src_point : range.shift(points.first())) {
dst_to_src_point[curr_dst_point_id++] = src_point;
}
/* Join the first range to the end of the last range. */
if (is_curve_self_joined && range_i == range_ids.last()) {
const IndexRange first_range = ranges_to_keep[range_ids.first()];
for (const int src_point : first_range.shift(points.first())) {
dst_to_src_point[curr_dst_point_id++] = src_point;
}
count += first_range.size();
}
dst_curve_counts.append(count);
dst_to_src_curve.append(curve_i);
dst_cyclic.append(is_cyclic);
}
}
const int total_curves = dst_to_src_curve.size();
bke::CurvesGeometry dst_curves(total_points, total_curves);
BKE_defgroup_copy_list(&dst_curves.vertex_group_names, &curves.vertex_group_names);
MutableSpan<int> new_curve_offsets = dst_curves.offsets_for_write();
array_utils::copy(dst_curve_counts.as_span(), new_curve_offsets.drop_back(1));
offset_indices::accumulate_counts_to_offsets(new_curve_offsets);
bke::MutableAttributeAccessor dst_attributes = dst_curves.attributes_for_write();
const bke::AttributeAccessor src_attributes = curves.attributes();
/* Transfer curve attributes. */
gather_attributes(src_attributes,
bke::AttrDomain::Curve,
bke::AttrDomain::Curve,
bke::attribute_filter_from_skip_ref({"cyclic"}),
dst_to_src_curve,
dst_attributes);
array_utils::copy(dst_cyclic.as_span(), dst_curves.cyclic_for_write());
/* Transfer point attributes. */
gather_attributes(src_attributes,
bke::AttrDomain::Point,
bke::AttrDomain::Point,
{},
dst_to_src_point,
dst_attributes);
dst_curves.update_curve_types();
dst_curves.remove_attributes_based_on_types();
return dst_curves;
}
} // namespace blender::geometry