Curves: Add edit mode duplicate operator

Reuse the grease pencil implementation added in:
- fb275bc040
- 5799a26568819ce27e8c12df96b7ffba84cc00f9
This commit is contained in:
Hans Goudey
2023-12-11 15:44:59 -05:00
parent 7dc6a6bd9a
commit 9af176bfe6
8 changed files with 251 additions and 185 deletions

View File

@@ -6077,6 +6077,7 @@ def km_edit_curves(params):
("curves.set_selection_domain", {"type": 'TWO', "value": 'PRESS'}, {"properties": [("domain", 'CURVE')]}),
("curves.disable_selection", {"type": 'ONE', "value": 'PRESS', "alt": True}, None),
("curves.disable_selection", {"type": 'TWO', "value": 'PRESS', "alt": True}, None),
("curves.duplicate_move", {"type": 'D', "value": 'PRESS', "shift": True}, None),
*_template_items_select_actions(params, "curves.select_all"),
("curves.select_linked", {"type": 'L', "value": 'PRESS', "ctrl": True}, None),
("curves.delete", {"type": 'X', "value": 'PRESS'}, None),

View File

@@ -4119,6 +4119,7 @@ def km_curves(params):
("curves.set_selection_domain", {"type": 'TWO', "value": 'PRESS'}, {"properties": [("domain", 'CURVE')]}),
("curves.disable_selection", {"type": 'ONE', "value": 'PRESS', "alt": True}, None),
("curves.disable_selection", {"type": 'TWO', "value": 'PRESS', "alt": True}, None),
("curves.duplicate_move", {"type": 'D', "value": 'PRESS', "ctrl": True}, None),
# Selection Operators
("curves.select_all", {"type": 'A', "value": 'PRESS', "ctrl": True}, {"properties": [("action", 'SELECT')]}),
("curves.select_all", {"type": 'A', "value": 'PRESS', "shift": True,

View File

@@ -5887,6 +5887,8 @@ class VIEW3D_MT_edit_curves(Menu):
layout.menu("VIEW3D_MT_transform")
layout.separator()
layout.operator("curves.duplicate_move")
layout.separator()
layout.operator("curves.attribute_set")
layout.operator("curves.delete")
layout.template_node_operator_asset_menu_items(catalog_path=self.bl_label)

View File

@@ -6,6 +6,8 @@
* \ingroup edcurves
*/
#include "BLI_array_utils.hh"
#include "BKE_curves.hh"
#include "ED_curves.hh"
@@ -34,4 +36,187 @@ bool remove_selection(bke::CurvesGeometry &curves, const eAttrDomain selection_d
return attributes.domain_size(selection_domain) != domain_size_orig;
}
void duplicate_points(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_duplicate(curves.points_num());
mask.to_bools(points_to_duplicate.as_mutable_span());
const int num_points_to_add = mask.size();
int curr_dst_point_start = 0;
Array<int> dst_to_src_point(num_points_to_add);
Vector<int> dst_curve_counts;
Vector<int> dst_to_src_curve;
Vector<bool> dst_cyclic;
/* Add the duplicated curves and points. */
for (const int curve_i : curves.curves_range()) {
const IndexRange points = points_by_curve[curve_i];
const Span<bool> curve_points_to_duplicate = points_to_duplicate.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_duplicate = array_utils::find_all_ranges(
curve_points_to_duplicate, true);
if (ranges_to_duplicate.is_empty()) {
continue;
}
const bool is_last_segment_selected = curve_cyclic &&
ranges_to_duplicate.first().first() == 0 &&
ranges_to_duplicate.last().last() == points.size() - 1;
const bool is_curve_self_joined = is_last_segment_selected && ranges_to_duplicate.size() != 1;
const bool is_cyclic = ranges_to_duplicate.size() == 1 && is_last_segment_selected;
const IndexRange range_ids = ranges_to_duplicate.index_range();
/* Skip the first range because it is joined to the end of the last range. */
for (const int range_i : ranges_to_duplicate.index_range().drop_front(is_curve_self_joined)) {
const IndexRange range = ranges_to_duplicate[range_i];
array_utils::fill_index_range<int>(
dst_to_src_point.as_mutable_span().slice(curr_dst_point_start, range.size()),
range.start() + points.first());
curr_dst_point_start += range.size();
dst_curve_counts.append(range.size());
dst_to_src_curve.append(curve_i);
dst_cyclic.append(is_cyclic);
}
/* Join the first range to the end of the last range. */
if (is_curve_self_joined) {
const IndexRange first_range = ranges_to_duplicate[range_ids.first()];
array_utils::fill_index_range<int>(
dst_to_src_point.as_mutable_span().slice(curr_dst_point_start, first_range.size()),
first_range.start() + points.first());
curr_dst_point_start += first_range.size();
dst_curve_counts[dst_curve_counts.size() - 1] += first_range.size();
}
}
const int old_curves_num = curves.curves_num();
const int old_points_num = curves.points_num();
const int num_curves_to_add = dst_to_src_curve.size();
bke::MutableAttributeAccessor attributes = curves.attributes_for_write();
/* Delete selection attribute so that it will not have to be resized. */
attributes.remove(".selection");
curves.resize(old_points_num + num_points_to_add, old_curves_num + num_curves_to_add);
MutableSpan<int> new_curve_offsets = curves.offsets_for_write();
array_utils::copy(dst_curve_counts.as_span(),
new_curve_offsets.drop_front(old_curves_num).drop_back(1));
offset_indices::accumulate_counts_to_offsets(new_curve_offsets.drop_front(old_curves_num),
old_points_num);
/* Transfer curve and point attributes. */
attributes.for_all([&](const bke::AttributeIDRef &id, const bke::AttributeMetaData meta_data) {
bke::GSpanAttributeWriter attribute = attributes.lookup_for_write_span(id);
if (!attribute) {
return true;
}
switch (meta_data.domain) {
case ATTR_DOMAIN_CURVE: {
if (id.name() == "cyclic") {
return true;
}
bke::attribute_math::gather(
attribute.span,
dst_to_src_curve,
attribute.span.slice(IndexRange(old_curves_num, num_curves_to_add)));
break;
}
case ATTR_DOMAIN_POINT: {
bke::attribute_math::gather(
attribute.span,
dst_to_src_point,
attribute.span.slice(IndexRange(old_points_num, num_points_to_add)));
break;
}
default: {
attribute.finish();
BLI_assert_unreachable();
return true;
}
}
attribute.finish();
return true;
});
if (!(src_cyclic.is_single() && !src_cyclic.get_internal_single())) {
array_utils::copy(dst_cyclic.as_span(), curves.cyclic_for_write().drop_front(old_curves_num));
}
curves.update_curve_types();
curves.tag_topology_changed();
bke::SpanAttributeWriter<bool> selection = attributes.lookup_or_add_for_write_span<bool>(
".selection", ATTR_DOMAIN_POINT);
selection.span.take_back(num_points_to_add).fill(true);
selection.finish();
}
void duplicate_curves(bke::CurvesGeometry &curves, const IndexMask &mask)
{
const int orig_points_num = curves.points_num();
const int orig_curves_num = curves.curves_num();
bke::MutableAttributeAccessor attributes = curves.attributes_for_write();
/* Delete selection attribute so that it will not have to be resized. */
attributes.remove(".selection");
/* Resize the curves and copy the offsets of duplicated curves into the new offsets. */
curves.resize(curves.points_num(), orig_curves_num + mask.size());
const IndexRange orig_curves_range = curves.curves_range().take_front(orig_curves_num);
const IndexRange new_curves_range = curves.curves_range().drop_front(orig_curves_num);
MutableSpan<int> offset_data = curves.offsets_for_write();
offset_indices::gather_selected_offsets(
OffsetIndices<int>(offset_data.take_front(orig_curves_num + 1)),
mask,
orig_points_num,
offset_data.drop_front(orig_curves_num));
const OffsetIndices<int> points_by_curve = curves.points_by_curve();
/* Resize the points array to match the new total point count. */
curves.resize(points_by_curve.total_size(), curves.curves_num());
attributes.for_all([&](const bke::AttributeIDRef &id, const bke::AttributeMetaData meta_data) {
bke::GSpanAttributeWriter attribute = attributes.lookup_for_write_span(id);
switch (meta_data.domain) {
case ATTR_DOMAIN_POINT:
bke::attribute_math::gather_group_to_group(points_by_curve.slice(orig_curves_range),
points_by_curve.slice(new_curves_range),
mask,
attribute.span,
attribute.span);
break;
case ATTR_DOMAIN_CURVE:
array_utils::gather(attribute.span, mask, attribute.span.take_back(mask.size()));
break;
default:
BLI_assert_unreachable();
return true;
}
attribute.finish();
return true;
});
curves.update_curve_types();
curves.tag_topology_changed();
bke::SpanAttributeWriter<bool> selection = attributes.lookup_or_add_for_write_span<bool>(
".selection", ATTR_DOMAIN_CURVE);
selection.span.take_back(mask.size()).fill(true);
selection.finish();
}
} // namespace blender::ed::curves

View File

@@ -1244,6 +1244,44 @@ static void CURVES_OT_delete(wmOperatorType *ot)
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
}
namespace curves_duplicate {
static int delete_exec(bContext *C, wmOperator * /*op*/)
{
for (Curves *curves_id : get_unique_editable_curves(*C)) {
bke::CurvesGeometry &curves = curves_id->geometry.wrap();
IndexMaskMemory memory;
switch (eAttrDomain(curves_id->selection_domain)) {
case ATTR_DOMAIN_POINT:
duplicate_points(curves, retrieve_selected_points(*curves_id, memory));
break;
case ATTR_DOMAIN_CURVE:
duplicate_curves(curves, retrieve_selected_curves(*curves_id, memory));
break;
default:
BLI_assert_unreachable();
break;
}
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 curves_duplicate
static void CURVES_OT_duplicate(wmOperatorType *ot)
{
ot->name = "Duplicate";
ot->idname = __func__;
ot->description = "Copy selected points or curves";
ot->exec = curves_duplicate::delete_exec;
ot->poll = editable_curves_in_edit_mode_poll;
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
}
} // namespace blender::ed::curves
void ED_operatortypes_curves()
@@ -1263,6 +1301,23 @@ void ED_operatortypes_curves()
WM_operatortype_append(CURVES_OT_select_less);
WM_operatortype_append(CURVES_OT_surface_set);
WM_operatortype_append(CURVES_OT_delete);
WM_operatortype_append(CURVES_OT_duplicate);
}
void ED_operatormacros_curves()
{
wmOperatorType *ot;
wmOperatorTypeMacro *otmacro;
/* Duplicate + Move = Interactively place newly duplicated strokes */
ot = WM_operatortype_append_macro("CURVES_OT_duplicate_move",
"Duplicate",
"Make copies of selected elements and move them",
OPTYPE_UNDO | OPTYPE_REGISTER);
WM_operatortype_macro_define(ot, "CURVES_OT_duplicate");
otmacro = WM_operatortype_macro_define(ot, "TRANSFORM_OT_translate");
RNA_boolean_set(otmacro->ptr, "use_proportional_edit", false);
RNA_boolean_set(otmacro->ptr, "mirror", false);
}
void ED_keymap_curves(wmKeyConfig *keyconf)

View File

@@ -1530,189 +1530,6 @@ static void GREASE_PENCIL_OT_set_material(wmOperatorType *ot)
/** \name Duplicate Operator
* \{ */
static void duplicate_points(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_duplicate(curves.points_num());
mask.to_bools(points_to_duplicate.as_mutable_span());
const int num_points_to_add = mask.size();
int curr_dst_point_start = 0;
Array<int> dst_to_src_point(num_points_to_add);
Vector<int> dst_curve_counts;
Vector<int> dst_to_src_curve;
Vector<bool> dst_cyclic;
/* Add the duplicated curves and points. */
for (const int curve_i : curves.curves_range()) {
const IndexRange points = points_by_curve[curve_i];
const Span<bool> curve_points_to_duplicate = points_to_duplicate.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_duplicate = array_utils::find_all_ranges(
curve_points_to_duplicate, true);
if (ranges_to_duplicate.is_empty()) {
continue;
}
const bool is_last_segment_selected = curve_cyclic &&
ranges_to_duplicate.first().first() == 0 &&
ranges_to_duplicate.last().last() == points.size() - 1;
const bool is_curve_self_joined = is_last_segment_selected && ranges_to_duplicate.size() != 1;
const bool is_cyclic = ranges_to_duplicate.size() == 1 && is_last_segment_selected;
const IndexRange range_ids = ranges_to_duplicate.index_range();
/* Skip the first range because it is joined to the end of the last range. */
for (const int range_i : ranges_to_duplicate.index_range().drop_front(is_curve_self_joined)) {
const IndexRange range = ranges_to_duplicate[range_i];
array_utils::fill_index_range<int>(
dst_to_src_point.as_mutable_span().slice(curr_dst_point_start, range.size()),
range.start() + points.first());
curr_dst_point_start += range.size();
dst_curve_counts.append(range.size());
dst_to_src_curve.append(curve_i);
dst_cyclic.append(is_cyclic);
}
/* Join the first range to the end of the last range. */
if (is_curve_self_joined) {
const IndexRange first_range = ranges_to_duplicate[range_ids.first()];
array_utils::fill_index_range<int>(
dst_to_src_point.as_mutable_span().slice(curr_dst_point_start, first_range.size()),
first_range.start() + points.first());
curr_dst_point_start += first_range.size();
dst_curve_counts[dst_curve_counts.size() - 1] += first_range.size();
}
}
const int old_curves_num = curves.curves_num();
const int old_points_num = curves.points_num();
const int num_curves_to_add = dst_to_src_curve.size();
bke::MutableAttributeAccessor attributes = curves.attributes_for_write();
/* Delete selection attribute so that it will not have to be resized. */
attributes.remove(".selection");
curves.resize(old_points_num + num_points_to_add, old_curves_num + num_curves_to_add);
MutableSpan<int> new_curve_offsets = curves.offsets_for_write();
array_utils::copy(dst_curve_counts.as_span(),
new_curve_offsets.drop_front(old_curves_num).drop_back(1));
offset_indices::accumulate_counts_to_offsets(new_curve_offsets.drop_front(old_curves_num),
old_points_num);
/* Transfer curve and point attributes. */
attributes.for_all([&](const bke::AttributeIDRef &id, const bke::AttributeMetaData meta_data) {
bke::GSpanAttributeWriter attribute = attributes.lookup_for_write_span(id);
if (!attribute) {
return true;
}
switch (meta_data.domain) {
case ATTR_DOMAIN_CURVE: {
if (id.name() == "cyclic") {
return true;
}
bke::attribute_math::gather(
attribute.span,
dst_to_src_curve,
attribute.span.slice(IndexRange(old_curves_num, num_curves_to_add)));
break;
}
case ATTR_DOMAIN_POINT: {
bke::attribute_math::gather(
attribute.span,
dst_to_src_point,
attribute.span.slice(IndexRange(old_points_num, num_points_to_add)));
break;
}
default: {
attribute.finish();
BLI_assert_unreachable();
return true;
}
}
attribute.finish();
return true;
});
if (!(src_cyclic.is_single() && !src_cyclic.get_internal_single())) {
array_utils::copy(dst_cyclic.as_span(), curves.cyclic_for_write().drop_front(old_curves_num));
}
curves.update_curve_types();
curves.tag_topology_changed();
bke::SpanAttributeWriter<bool> selection = attributes.lookup_or_add_for_write_span<bool>(
".selection", ATTR_DOMAIN_POINT);
selection.span.take_back(num_points_to_add).fill(true);
selection.finish();
}
static void duplicate_curves(bke::CurvesGeometry &curves, const IndexMask &mask)
{
const int orig_points_num = curves.points_num();
const int orig_curves_num = curves.curves_num();
bke::MutableAttributeAccessor attributes = curves.attributes_for_write();
/* Delete selection attribute so that it will not have to be resized. */
attributes.remove(".selection");
/* Resize the curves and copy the offsets of duplicated curves into the new offsets. */
curves.resize(curves.points_num(), orig_curves_num + mask.size());
const IndexRange orig_curves_range = curves.curves_range().take_front(orig_curves_num);
const IndexRange new_curves_range = curves.curves_range().drop_front(orig_curves_num);
MutableSpan<int> offset_data = curves.offsets_for_write();
offset_indices::gather_selected_offsets(
OffsetIndices<int>(offset_data.take_front(orig_curves_num + 1)),
mask,
orig_points_num,
offset_data.drop_front(orig_curves_num));
const OffsetIndices<int> points_by_curve = curves.points_by_curve();
/* Resize the points array to match the new total point count. */
curves.resize(points_by_curve.total_size(), curves.curves_num());
attributes.for_all([&](const bke::AttributeIDRef &id, const bke::AttributeMetaData meta_data) {
bke::GSpanAttributeWriter attribute = attributes.lookup_for_write_span(id);
switch (meta_data.domain) {
case ATTR_DOMAIN_POINT:
bke::attribute_math::gather_group_to_group(points_by_curve.slice(orig_curves_range),
points_by_curve.slice(new_curves_range),
mask,
attribute.span,
attribute.span);
break;
case ATTR_DOMAIN_CURVE:
array_utils::gather(attribute.span, mask, attribute.span.take_back(mask.size()));
break;
default:
BLI_assert_unreachable();
return true;
}
attribute.finish();
return true;
});
curves.update_curve_types();
curves.tag_topology_changed();
bke::SpanAttributeWriter<bool> selection = attributes.lookup_or_add_for_write_span<bool>(
".selection", ATTR_DOMAIN_CURVE);
selection.span.take_back(mask.size()).fill(true);
selection.finish();
}
static int grease_pencil_duplicate_exec(bContext *C, wmOperator * /*op*/)
{
const Scene *scene = CTX_data_scene(C);
@@ -1733,10 +1550,10 @@ static int grease_pencil_duplicate_exec(bContext *C, wmOperator * /*op*/)
bke::CurvesGeometry &curves = info.drawing.strokes_for_write();
if (selection_domain == ATTR_DOMAIN_CURVE) {
duplicate_curves(curves, elements);
curves::duplicate_curves(curves, elements);
}
else if (selection_domain == ATTR_DOMAIN_POINT) {
duplicate_points(curves, elements);
curves::duplicate_points(curves, elements);
}
info.drawing.tag_topology_changed();
changed.store(true, std::memory_order_relaxed);

View File

@@ -32,6 +32,7 @@ struct wmKeyConfig;
* \{ */
void ED_operatortypes_curves();
void ED_operatormacros_curves();
void ED_curves_undosys_type(UndoType *ut);
void ED_keymap_curves(wmKeyConfig *keyconf);
@@ -286,6 +287,9 @@ bool select_circle(const ViewContext &vc,
*/
bool remove_selection(bke::CurvesGeometry &curves, eAttrDomain selection_domain);
void duplicate_points(bke::CurvesGeometry &curves, const IndexMask &mask);
void duplicate_curves(bke::CurvesGeometry &curves, const IndexMask &mask);
/** \} */
} // namespace blender::ed::curves

View File

@@ -162,6 +162,7 @@ void ED_spacemacros_init()
ED_operatormacros_action();
ED_operatormacros_clip();
ED_operatormacros_curve();
ED_operatormacros_curves();
ED_operatormacros_mask();
ED_operatormacros_sequencer();
ED_operatormacros_paint();