diff --git a/scripts/presets/keyconfig/keymap_data/blender_default.py b/scripts/presets/keyconfig/keymap_data/blender_default.py index 8a7563f92b7..201e99f3f45 100644 --- a/scripts/presets/keyconfig/keymap_data/blender_default.py +++ b/scripts/presets/keyconfig/keymap_data/blender_default.py @@ -6077,6 +6077,7 @@ def km_edit_curves(params): ("curves.set_selection_domain", {"type": 'TWO', "value": 'PRESS'}, {"properties": [("domain", 'CURVE')]}), ("curves.duplicate_move", {"type": 'D', "value": 'PRESS', "shift": True}, None), *_template_items_select_actions(params, "curves.select_all"), + ("curves.extrude_move", {"type": 'E', "value": 'PRESS'}, None), ("curves.select_linked", {"type": 'L', "value": 'PRESS', "ctrl": True}, None), ("curves.delete", {"type": 'X', "value": 'PRESS'}, None), ("curves.delete", {"type": 'DEL', "value": 'PRESS'}, None), diff --git a/source/blender/editors/curves/CMakeLists.txt b/source/blender/editors/curves/CMakeLists.txt index 27c401204d7..643a4c8bd6c 100644 --- a/source/blender/editors/curves/CMakeLists.txt +++ b/source/blender/editors/curves/CMakeLists.txt @@ -26,6 +26,7 @@ set(SRC intern/curves_data.cc intern/curves_draw.cc intern/curves_edit.cc + intern/curves_extrude.cc intern/curves_masks.cc intern/curves_ops.cc intern/curves_selection.cc diff --git a/source/blender/editors/curves/intern/curves_extrude.cc b/source/blender/editors/curves/intern/curves_extrude.cc new file mode 100644 index 00000000000..5829ffe5cff --- /dev/null +++ b/source/blender/editors/curves/intern/curves_extrude.cc @@ -0,0 +1,357 @@ +/* SPDX-FileCopyrightText: 2023 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +#include "BKE_attribute.hh" +#include "BKE_context.hh" +#include "BKE_curves_utils.hh" + +#include "WM_api.hh" +#include "WM_types.hh" + +#include "ED_curves.hh" + +#include "DEG_depsgraph.hh" + +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]. + * Leading to only two copy operations. + */ +static Span compress_intervals(const Span curve_interval_ranges, + MutableSpan intervals) +{ + const int *src = intervals.data(); + /* Skip the first curve, as all the data stays in the same place. */ + int *dst = intervals.data() + curve_interval_ranges[0].size(); + + for (const int curve : IndexRange(1, curve_interval_ranges.size() - 1)) { + const IndexRange range = curve_interval_ranges[curve]; + const int width = range.size() - 1; + std::copy_n(src + range.first() + 1, width, dst); + dst += width; + } + (*dst) = src[curve_interval_ranges[curve_interval_ranges.size() - 1].last() + 1]; + return {intervals.data(), dst - intervals.data() + 1}; +} + +/** + * Creates copy intervals for selection #range in the context of #curve_index. + * If part of the #range is outside given curve, slices it and returns false indicating remaining + * still needs to be handled. If whole #range was handled returns true. + */ +static bool handle_range(const int curve_index, + const int interval_offset, + const Span offsets, + int ¤t_interval, + IndexRange &range, + MutableSpan curve_intervals, + MutableSpan is_first_selected) +{ + const int first_elem = offsets[curve_index]; + const int last_elem = offsets[curve_index + 1] - 1; + + if (current_interval == 0) { + is_first_selected[curve_index] = range.first() == first_elem && range.size() == 1; + if (!is_first_selected[curve_index]) { + current_interval++; + } + } + curve_intervals[interval_offset + current_interval] = range.first(); + current_interval++; + + bool inside_curve = last_elem >= range.last(); + if (inside_curve) { + curve_intervals[interval_offset + current_interval] = range.last(); + } + else { + curve_intervals[interval_offset + current_interval] = last_elem; + range = IndexRange(last_elem + 1, range.last() - last_elem); + } + current_interval++; + return inside_curve; +} + +/** + * Calculates number of points in resulting curve denoted by #curve_index and sets it's + * #curve_offsets value. + */ +static void calc_curve_offset(const int curve_index, + int &interval_offset, + const Span offsets, + MutableSpan new_offsets, + MutableSpan curve_interval_ranges) +{ + const int points_in_curve = (offsets[curve_index + 1] - offsets[curve_index] + + curve_interval_ranges[curve_index].size() - 1); + new_offsets[curve_index + 1] = new_offsets[curve_index] + points_in_curve; + interval_offset += curve_interval_ranges[curve_index].size() + 1; +} + +static void finish_curve(int &curve_index, + int &interval_offset, + int last_interval, + int last_elem, + const Span offsets, + MutableSpan new_offsets, + MutableSpan curve_intervals, + MutableSpan curve_interval_ranges, + MutableSpan is_first_selected) +{ + if (curve_intervals[interval_offset + last_interval] != last_elem || + curve_intervals[interval_offset + last_interval - 1] != + curve_intervals[interval_offset + last_interval]) + { + /* Append last element of the current curve if it is not extruded or extruded together with + * preceding points. */ + last_interval++; + curve_intervals[interval_offset + last_interval] = last_elem; + } + else if (is_first_selected[curve_index] && last_interval == 1) { + /* Extrusion from one point. */ + curve_intervals[interval_offset + last_interval + 1] = + curve_intervals[interval_offset + last_interval]; + is_first_selected[curve_index] = false; + last_interval++; + } + curve_interval_ranges[curve_index] = IndexRange(interval_offset, last_interval); + calc_curve_offset(curve_index, interval_offset, offsets, new_offsets, curve_interval_ranges); + curve_index++; +} + +static void finish_curve_or_full_copy(int &curve_index, + int &interval_offset, + int current_interval, + const std::optional prev_range, + const Span offsets, + MutableSpan new_offsets, + MutableSpan curve_intervals, + MutableSpan curve_interval_ranges, + MutableSpan is_first_selected) +{ + const int last = offsets[curve_index + 1] - 1; + + if (prev_range.has_value() && prev_range.value().last() >= offsets[curve_index]) { + finish_curve(curve_index, + interval_offset, + current_interval - 1, + last, + offsets, + new_offsets, + curve_intervals, + curve_interval_ranges, + is_first_selected); + } + else { + /* Copy full curve if previous selected point vas not on this curve. */ + const int first = offsets[curve_index]; + curve_interval_ranges[curve_index] = IndexRange(interval_offset, 1); + is_first_selected[curve_index] = false; + curve_intervals[interval_offset] = first; + curve_intervals[interval_offset + 1] = last; + calc_curve_offset(curve_index, interval_offset, offsets, new_offsets, curve_interval_ranges); + curve_index++; + } +} + +static void calc_curves_extrusion(const IndexMask &selection, + const Span offsets, + MutableSpan new_offsets, + MutableSpan curve_intervals, + MutableSpan curve_interval_ranges, + MutableSpan is_first_selected) +{ + std::optional prev_range; + int current_interval = 0; + + int curve_index = 0; + int interval_offset = 0; + curve_intervals[interval_offset] = offsets[0]; + new_offsets[0] = offsets[0]; + + selection.foreach_range([&](const IndexRange range) { + /* Beginning of the range outside current curve. */ + if (range.first() > offsets[curve_index + 1] - 1) { + do { + finish_curve_or_full_copy(curve_index, + interval_offset, + current_interval, + prev_range, + offsets, + new_offsets, + curve_intervals, + curve_interval_ranges, + is_first_selected); + } while (range.first() > offsets[curve_index + 1] - 1); + current_interval = 0; + curve_intervals[interval_offset] = offsets[curve_index]; + } + + IndexRange range_to_handle = range; + while (!handle_range(curve_index, + interval_offset, + offsets, + current_interval, + range_to_handle, + curve_intervals, + is_first_selected)) + { + finish_curve(curve_index, + interval_offset, + current_interval - 1, + offsets[curve_index + 1] - 1, + offsets, + new_offsets, + curve_intervals, + curve_interval_ranges, + is_first_selected); + current_interval = 0; + curve_intervals[interval_offset] = offsets[curve_index]; + } + prev_range = range; + }); + + do { + finish_curve_or_full_copy(curve_index, + interval_offset, + current_interval, + prev_range, + offsets, + new_offsets, + curve_intervals, + curve_interval_ranges, + is_first_selected); + prev_range.reset(); + } while (curve_index < offsets.size() - 1); +} + +static void extrude_curves(Curves &curves_id) +{ + const bke::AttrDomain selection_domain = bke::AttrDomain(curves_id.selection_domain); + if (selection_domain != bke::AttrDomain::Point) { + return; + } + + IndexMaskMemory memory; + const IndexMask extruded_points = retrieve_selected_points(curves_id, memory); + if (extruded_points.is_empty()) { + return; + } + + const bke::CurvesGeometry &curves = curves_id.geometry.wrap(); + const Span old_offsets = curves.offsets(); + + bke::CurvesGeometry new_curves = bke::curves::copy_only_curve_domain(curves); + + const int curves_num = curves.curves_num(); + const int curve_intervals_size = extruded_points.size() * 2 + curves_num * 2; + + new_curves.resize(0, curves_num); + MutableSpan new_offsets = new_curves.offsets_for_write(); + + /* Buffer for intervals of all curves. Beginning and end of a curve can be determined only by + * #curve_interval_ranges. For ex. [0, 3, 4, 4, 4] indicates one copy interval for first curve + * [0, 3] and two for second [4, 4][4, 4]. The first curve will be copied as is without changes, + * in the second one (consisting only one point - 4) first point will be duplicated (extruded). + */ + Array curve_intervals(curve_intervals_size); + + /* Points to intervals for each curve in the curve_intervals array. + * For example above value would be [{0, 1}, {2, 2}] */ + Array 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. */ + Array is_first_selected(curves_num); + + calc_curves_extrusion(extruded_points, + old_offsets, + new_offsets, + curve_intervals, + curve_interval_ranges, + is_first_selected); + + 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); + const CPPType &src_selection_type = src_selection.type(); + bke::GSpanAttributeWriter dst_selection = ensure_selection_attribute( + new_curves, + bke::AttrDomain::Point, + src_selection_type.is() ? CD_PROP_BOOL : CD_PROP_FLOAT); + + threading::parallel_for(curves.curves_range(), 256, [&](IndexRange curves_range) { + for (const int curve : curves_range) { + const int first_index = curve_interval_ranges[curve].start(); + const int first_value = curve_intervals[first_index]; + bool is_selected = is_first_selected[curve]; + + for (const int i : curve_interval_ranges[curve]) { + const int dest_index = new_offsets[curve] + curve_intervals[i] - first_value + i - + first_index; + const int size = curve_intervals[i + 1] - curve_intervals[i] + 1; + GMutableSpan dst_span = dst_selection.span.slice(IndexRange(dest_index, size)); + if (is_selected) { + src_selection_type.copy_assign_n( + src_selection.slice(IndexRange(curve_intervals[i], size)).data(), + dst_span.data(), + size); + } + else { + fill_selection(dst_span, false); + } + + is_selected = !is_selected; + } + } + }); + dst_selection.finish(); + + const Span intervals = compress_intervals(curve_interval_ranges, curve_intervals); + + bke::MutableAttributeAccessor dst_attributes = new_curves.attributes_for_write(); + + for (auto &attribute : bke::retrieve_attributes_for_transfer( + src_attributes, dst_attributes, ATTR_DOMAIN_MASK_POINT, {}, {".selection"})) + { + const CPPType &type = attribute.src.type(); + threading::parallel_for(IndexRange(intervals.size() - 1), 512, [&](IndexRange range) { + for (const int i : range) { + const int first = intervals[i]; + const int size = intervals[i + 1] - first + 1; + const int dest_index = intervals[i] + i; + type.copy_assign_n(attribute.src.slice(IndexRange(first, size)).data(), + attribute.dst.span.slice(IndexRange(dest_index, size)).data(), + size); + } + }); + attribute.dst.finish(); + } + curves_id.geometry.wrap() = std::move(new_curves); + DEG_id_tag_update(&curves_id.id, ID_RECALC_GEOMETRY); +} + +static int curves_extrude_exec(bContext *C, wmOperator * /*op*/) +{ + for (Curves *curves_id : get_unique_editable_curves(*C)) { + extrude_curves(*curves_id); + } + return OPERATOR_FINISHED; +} + +void CURVES_OT_extrude(wmOperatorType *ot) +{ + ot->name = "Extrude"; + ot->description = "Extrude selected control point(s)"; + ot->idname = "CURVES_OT_extrude"; + + ot->exec = curves_extrude_exec; + ot->poll = editable_curves_in_edit_mode_poll; + + ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO; +} + +} // namespace blender::ed::curves diff --git a/source/blender/editors/curves/intern/curves_ops.cc b/source/blender/editors/curves/intern/curves_ops.cc index 21fce3bc477..64943171099 100644 --- a/source/blender/editors/curves/intern/curves_ops.cc +++ b/source/blender/editors/curves/intern/curves_ops.cc @@ -1332,6 +1332,7 @@ void ED_operatortypes_curves() WM_operatortype_append(CURVES_OT_convert_to_particle_system); WM_operatortype_append(CURVES_OT_convert_from_particle_system); WM_operatortype_append(CURVES_OT_draw); + WM_operatortype_append(CURVES_OT_extrude); WM_operatortype_append(CURVES_OT_snap_curves_to_surface); WM_operatortype_append(CURVES_OT_set_selection_domain); WM_operatortype_append(CURVES_OT_select_all); @@ -1360,6 +1361,16 @@ void ED_operatormacros_curves() 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); + + /* Extrude + Move */ + ot = WM_operatortype_append_macro("CURVES_OT_extrude_move", + "Extrude Curve and Move", + "Extrude curve and move result", + OPTYPE_UNDO | OPTYPE_REGISTER); + WM_operatortype_macro_define(ot, "CURVES_OT_extrude"); + 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) diff --git a/source/blender/editors/curves/intern/curves_selection.cc b/source/blender/editors/curves/intern/curves_selection.cc index e1c70f50ed6..5e5d52a9233 100644 --- a/source/blender/editors/curves/intern/curves_selection.cc +++ b/source/blender/editors/curves/intern/curves_selection.cc @@ -128,6 +128,16 @@ void fill_selection_true(GMutableSpan selection) } } +void fill_selection(GMutableSpan selection, bool value) +{ + if (selection.type().is()) { + selection.typed().fill(value); + } + else if (selection.type().is()) { + selection.typed().fill(value ? 1.0f : 0.0f); + } +} + void fill_selection_false(GMutableSpan selection, const IndexMask &mask) { if (selection.type().is()) { diff --git a/source/blender/editors/include/ED_curves.hh b/source/blender/editors/include/ED_curves.hh index d4ede1980aa..5fc662160d8 100644 --- a/source/blender/editors/include/ED_curves.hh +++ b/source/blender/editors/include/ED_curves.hh @@ -80,6 +80,7 @@ bool curves_poll(bContext *C); void CURVES_OT_attribute_set(wmOperatorType *ot); void CURVES_OT_draw(wmOperatorType *ot); +void CURVES_OT_extrude(wmOperatorType *ot); /** \} */ @@ -143,6 +144,7 @@ IndexMask random_mask(const bke::CurvesGeometry &curves, void fill_selection_false(GMutableSpan span); void fill_selection_true(GMutableSpan span); +void fill_selection(GMutableSpan selection, bool value); void fill_selection_false(GMutableSpan selection, const IndexMask &mask); void fill_selection_true(GMutableSpan selection, const IndexMask &mask);