Blender V4.3
grease_pencil_join_selection.cc
Go to the documentation of this file.
1/* SPDX-FileCopyrightText: 2023 Blender Authors
2 *
3 * SPDX-License-Identifier: GPL-2.0-or-later */
4
9#include "BKE_attribute.hh"
10#include "BKE_context.hh"
11#include "BKE_curves_utils.hh"
12#include "BKE_grease_pencil.hh"
13
14#include "DNA_scene_types.h"
15
16#include "DEG_depsgraph.hh"
17
18#include "ED_grease_pencil.hh"
19#include "RNA_access.hh"
20
21#include "WM_api.hh"
22
23#include "RNA_define.hh"
24
25#include <algorithm>
26
28
29namespace {
30
35struct PointsRange {
36 bke::CurvesGeometry *owning_curves;
37 IndexRange range;
39};
40
41enum class ActionOnNextRange { Nothing, ReverseExisting, ReverseAddition, ReverseBoth };
42
43enum class ActiveLayerBehavior { JoinAndCopySelection, JoinSelection };
44
51Vector<PointsRange> retrieve_selection_ranges(Object &object,
52 const Span<MutableDrawingInfo> drawings,
53 const int active_layer_index,
54 int64_t &r_total_points_selected,
55 IndexMaskMemory &memory)
56{
57 Vector<PointsRange> selected_ranges{};
58 r_total_points_selected = 0;
59
60 for (const MutableDrawingInfo &info : drawings) {
61 IndexMask points_selection = retrieve_editable_and_selected_points(
62 object, info.drawing, info.layer_index, memory);
63 if (points_selection.is_empty()) {
64 continue;
65 }
66 r_total_points_selected += points_selection.size();
67
68 const Vector<IndexRange> initial_ranges = points_selection.to_ranges();
69 const bool is_active_layer = info.layer_index == active_layer_index;
70
76 Vector<IndexRange> ranges{};
77 const Array<int> points_map = info.drawing.strokes().point_to_curve_map();
78 for (const IndexRange initial_range : initial_ranges) {
79 if (points_map[initial_range.first()] == points_map[initial_range.last()]) {
80 selected_ranges.append(
81 {&info.drawing.strokes_for_write(), initial_range, is_active_layer});
82 continue;
83 }
84
85 IndexRange range = {initial_range.start(), 1};
86 int previous_curve = points_map[range.start()];
87 for (const int64_t index : initial_range.drop_front(1)) {
88 const int current_curve = points_map[index];
89 if (previous_curve != current_curve) {
90 selected_ranges.append({&info.drawing.strokes_for_write(), range, is_active_layer});
91 range = {index, 1};
92 previous_curve = current_curve;
93 }
94 else {
95 range = {range.start(), range.size() + 1};
96 }
97 }
98
99 selected_ranges.append({&info.drawing.strokes_for_write(), range, is_active_layer});
100 }
101 }
102
103 return selected_ranges;
104}
105
106template<typename T> void reverse_point_data(const IndexRange point_range, MutableSpan<T> data)
107{
108 data.slice(point_range.first(), point_range.size()).reverse();
109}
110
115void reverse_points_of(bke::CurvesGeometry &dst_curves, const IndexRange points_to_reverse)
116{
117 bke::MutableAttributeAccessor attributes = dst_curves.attributes_for_write();
118
119 attributes.foreach_attribute([&](const bke::AttributeIter &iter) {
120 if (iter.domain != bke::AttrDomain::Point) {
121 return;
122 }
123 if (iter.data_type == CD_PROP_STRING) {
124 return;
125 }
126
127 bke::GSpanAttributeWriter attribute = attributes.lookup_for_write_span(iter.name);
128 bke::attribute_math::convert_to_static_type(attribute.span.type(), [&](auto dummy) {
129 using T = decltype(dummy);
130 reverse_point_data<T>(points_to_reverse, attribute.span.typed<T>());
131 });
132 attribute.finish();
133 });
134}
135
136void apply_action(ActionOnNextRange action,
137 const IndexRange working_range,
138 const IndexRange adding_range,
139 bke::CurvesGeometry &dst_curves)
140{
153 switch (action) {
154 case ActionOnNextRange::Nothing:
155 return;
156 case ActionOnNextRange::ReverseExisting: {
157 reverse_points_of(dst_curves, working_range);
158 break;
159 }
160 case ActionOnNextRange::ReverseAddition: {
161 const IndexRange src_range_on_dst = {working_range.last() + 1, adding_range.size()};
162 reverse_points_of(dst_curves, src_range_on_dst);
163 break;
164 }
165 case ActionOnNextRange::ReverseBoth: {
166 apply_action(ActionOnNextRange::ReverseExisting, working_range, adding_range, dst_curves);
167 apply_action(ActionOnNextRange::ReverseAddition, working_range, adding_range, dst_curves);
168 break;
169 }
170 }
171}
172
180int64_t compute_closest_range_to(PointsRange &range,
181 const Span<PointsRange> &ranges,
182 int64_t starting_from,
183 ActionOnNextRange &r_action)
184{
185 auto get_range_begin_end = [](const PointsRange &points_range) -> std::pair<float3, float3> {
186 const Span<float3> current_range_positions = points_range.owning_curves->positions();
187 const float3 range_begin = current_range_positions[points_range.range.first()];
188 const float3 range_end = current_range_positions[points_range.range.last()];
189
190 return {range_begin, range_end};
191 };
192
193 const auto [cur_range_begin, cur_range_end] = get_range_begin_end(range);
194 float min_dist = FLT_MAX;
195
196 int64_t ret_value = starting_from;
197 ActionOnNextRange action = ActionOnNextRange::Nothing;
198
199 const int64_t iterations = ranges.size() - starting_from;
200 for (const int64_t i : IndexRange(starting_from, iterations)) {
201 const auto [range_begin, range_end] = get_range_begin_end(ranges[i]);
202
203 float dist = math::distance_squared(cur_range_end, range_begin);
204 if (dist < min_dist) {
205 action = ActionOnNextRange::Nothing;
206 ret_value = i;
207 min_dist = dist;
208 }
209
210 dist = math::distance_squared(cur_range_begin, range_begin);
211 if (dist < min_dist) {
212 action = ActionOnNextRange::ReverseExisting;
213 ret_value = i;
214 min_dist = dist;
215 }
216
217 dist = math::distance_squared(cur_range_end, range_end);
218 if (dist < min_dist) {
219 action = ActionOnNextRange::ReverseAddition;
220 ret_value = i;
221 min_dist = dist;
222 }
223
224 dist = math::distance_squared(cur_range_begin, range_end);
225 if (dist < min_dist) {
226 action = ActionOnNextRange::ReverseBoth;
227 ret_value = i;
228 min_dist = dist;
229 }
230 }
231
232 r_action = action;
233 return ret_value;
234}
235
236void copy_range_to_dst(const PointsRange &points_range,
237 int &dst_starting_point,
238 bke::CurvesGeometry &dst_curves)
239{
240 Array<int> src_raw_offsets(2);
241 Array<int> dst_raw_offsets(2);
242
243 const int64_t selection_size = points_range.range.size();
244 src_raw_offsets[0] = points_range.range.first();
245 src_raw_offsets[1] = points_range.range.last() + 1;
246
247 dst_raw_offsets[0] = dst_starting_point;
248 dst_starting_point += selection_size;
249 dst_raw_offsets[1] = dst_starting_point;
250
251 OffsetIndices<int> src_offsets{src_raw_offsets};
252 OffsetIndices<int> dst_offsets{dst_raw_offsets};
253
254 copy_attributes_group_to_group(points_range.owning_curves->attributes(),
256 {},
257 {},
258 src_offsets,
259 dst_offsets,
260 IndexMask{1},
261 dst_curves.attributes_for_write());
262}
263
264PointsRange copy_point_attributes(MutableSpan<PointsRange> selected_ranges,
265 bke::CurvesGeometry &dst_curves)
266{
267 /* The algorithm for joining the points goes as follows:
268 * 1. Pick the first range of the selected ranges of points, which will be the working range
269 * 2. Copy the attributes of this range to dst_curves
270 * 3. Lookup in the remaining ranges for the one closer to the working range
271 * 4. Copy its attributes
272 * 5. In order to minimize the length of the stroke connecting them, reverse their points as
273 * needed
274 * 6. Extend the working range with the new range
275 * 7. Remove the new range from the list of remaining ranges. Lookup for the next one and
276 * continue
277 */
278
279 const PointsRange &first_range = selected_ranges.first();
280 PointsRange working_range = {&dst_curves, {0, first_range.range.size()}, true};
281
282 int next_point_index = 0;
283 copy_range_to_dst(first_range, next_point_index, dst_curves);
284
285 const int64_t ranges = selected_ranges.size() - 1;
286 for (const int64_t i : IndexRange(1, ranges)) {
287 ActionOnNextRange action;
288 const int64_t closest_range = compute_closest_range_to(
289 working_range, selected_ranges, i, action);
290 std::swap(selected_ranges[i], selected_ranges[closest_range]);
291 PointsRange &next_range = selected_ranges[i];
292 copy_range_to_dst(next_range, next_point_index, dst_curves);
293 apply_action(action, working_range.range, next_range.range, dst_curves);
294 working_range.range = {0, next_point_index};
295 }
296
297 return working_range;
298}
299
300void copy_curve_attributes(Span<PointsRange> ranges_selected, bke::CurvesGeometry &dst_curves)
301{
302 /* The decision of which stroke use to copy the curve attributes is a bit arbitrary, since the
303 * original selection may embrace several strokes. The criteria is as follows:
304 * - If the selection contained points from the active layer, the first selected stroke from it
305 * is used.
306 * - Otherwise, the first selected stroke is used.
307 * Reasoning behind is that the user will probably want to keep similar curve parameters for
308 * all the strokes in a layer.
309 * Also, the "cyclic" attribute is deliberately set to false, since user
310 * probably wants to set it manually
311 */
312
313 auto src_range = [&]() -> const PointsRange & {
314 auto it = std::find_if(ranges_selected.begin(),
315 ranges_selected.end(),
316 [](const PointsRange &range) { return range.belongs_to_active_layer; });
317
318 return it != ranges_selected.end() ? *it : ranges_selected.first();
319 }();
320
321 const bke::CurvesGeometry &src_curves = *src_range.owning_curves;
322 const Array<int> points_map = src_curves.point_to_curve_map();
323 const int first_selected_curve = points_map[src_range.range.first()];
324
325 const int final_curve_index = dst_curves.curves_num() - 1;
326 const Array<int> dst_curves_raw_offsets = {final_curve_index, dst_curves.curves_num()};
327 const OffsetIndices<int> dst_curve_offsets{dst_curves_raw_offsets};
328
329 gather_attributes_to_groups(src_curves.attributes(),
333 dst_curve_offsets,
334 IndexMask({first_selected_curve, 1}),
335 dst_curves.attributes_for_write());
336 dst_curves.cyclic_for_write().first() = false;
337}
338
343void clear_selection_attribute(Span<PointsRange> ranges_selected,
344 const bke::AttrDomain selection_domain)
345{
346 for (const PointsRange &range : ranges_selected) {
347 bke::CurvesGeometry &curves = *range.owning_curves;
348 bke::MutableAttributeAccessor attributes = curves.attributes_for_write();
349 bke::SpanAttributeWriter<bool> selection = attributes.lookup_or_add_for_write_span<bool>(
350 ".selection", selection_domain);
351
352 const IndexMask mask = selection_domain == bke::AttrDomain::Point ?
353 IndexMask{curves.points_num()} :
354 IndexMask{curves.curves_num()};
355
356 masked_fill(selection.span, false, mask);
357 selection.finish();
358 }
359}
360
361void remove_selected_points_in_active_layer(Span<PointsRange> ranges_selected,
362 bke::CurvesGeometry &dst_curves)
363{
364 IndexMaskMemory memory;
365 Vector<int64_t> mask_content;
366 for (const PointsRange &points_range : ranges_selected) {
367 if (!points_range.belongs_to_active_layer) {
368 continue;
369 }
370
371 Array<int64_t> range_content(points_range.range.size());
372 IndexMask(points_range.range).to_indices(range_content.as_mutable_span());
373 mask_content.extend(range_content);
374 }
375
376 /* remove_points requires the the indices in the mask to be sorted */
377 std::sort(mask_content.begin(), mask_content.end());
378 IndexMask mask = IndexMask::from_indices(mask_content.as_span(), memory);
379
380 dst_curves.remove_points(mask, {});
381}
382
383void append_strokes_from(bke::CurvesGeometry &&other, bke::CurvesGeometry &dst)
384{
385 const int initial_points_num = dst.points_num();
386 const int initial_curves_num = dst.curves_num();
387 const int other_points_num = other.points_num();
388 const int other_curves_num = other.curves_num();
389
390 dst.resize(initial_points_num + other_points_num, initial_curves_num + other_curves_num);
391
392 Array<int> other_raw_offsets{0, other_points_num};
393 Array<int> dst_raw_offsets{initial_points_num, initial_points_num + other_points_num};
394
395 OffsetIndices<int> other_point_offsets{other_raw_offsets};
396 OffsetIndices<int> dst_point_offsets{dst_raw_offsets};
397
398 copy_attributes_group_to_group(other.attributes(),
401 {},
402 other_point_offsets,
403 dst_point_offsets,
404 IndexMask{1},
405 dst.attributes_for_write());
406
407 other_raw_offsets = {0, other_curves_num};
408 dst_raw_offsets = {initial_curves_num, initial_curves_num + other_curves_num};
409
410 OffsetIndices<int> other_curve_offsets{other_raw_offsets};
411 OffsetIndices<int> dst_curve_offsets{dst_raw_offsets};
412
413 copy_attributes_group_to_group(other.attributes(),
416 {},
417 other_curve_offsets,
418 dst_curve_offsets,
419 IndexMask{1},
420 dst.attributes_for_write());
421}
422
423/* -------------------------------------------------------------------- */
431int grease_pencil_join_selection_exec(bContext *C, wmOperator *op)
432{
433 using namespace bke::greasepencil;
434
435 const Scene *scene = CTX_data_scene(C);
436 Object *object = CTX_data_active_object(C);
438 scene->toolsettings, object);
439 GreasePencil &grease_pencil = *static_cast<GreasePencil *>(object->data);
440 if (!grease_pencil.has_active_layer()) {
441 return OPERATOR_CANCELLED;
442 }
443
444 const ActiveLayerBehavior active_layer_behavior = static_cast<ActiveLayerBehavior>(
445 RNA_enum_get(op->ptr, "type"));
446 const Layer &active_layer = *grease_pencil.get_active_layer();
447
448 const std::optional<int> opt_layer_index = grease_pencil.get_layer_index(active_layer);
449 BLI_assert(opt_layer_index.has_value());
450 const int active_layer_index = *opt_layer_index;
451
452 Drawing *dst_drawing = grease_pencil.get_editable_drawing_at(*grease_pencil.get_active_layer(),
453 scene->r.cfra);
454 if (dst_drawing == nullptr) {
455 return OPERATOR_CANCELLED;
456 }
457
458 IndexMaskMemory memory;
459 int64_t selected_points_count;
460 const Vector<MutableDrawingInfo> editable_drawings = retrieve_editable_drawings(*scene,
461 grease_pencil);
462 Vector<PointsRange> ranges_selected = retrieve_selection_ranges(
463 *object, editable_drawings, active_layer_index, selected_points_count, memory);
464 if (ranges_selected.size() <= 1) {
465 /* Nothing to join */
466 return OPERATOR_FINISHED;
467 }
468
469 /* Temporary geometry where to perform the logic
470 * Once it gets stable, it is appended all at once to the destination curves */
471 bke::CurvesGeometry tmp_curves(selected_points_count, 1);
472
473 const PointsRange working_range = copy_point_attributes(ranges_selected, tmp_curves);
474 copy_curve_attributes(ranges_selected, tmp_curves);
475
476 clear_selection_attribute(ranges_selected, selection_domain);
477
478 Array<PointsRange> working_range_collection = {working_range};
479 clear_selection_attribute(working_range_collection, selection_domain);
480
481 bke::CurvesGeometry &dst_curves = dst_drawing->strokes_for_write();
482 if (active_layer_behavior == ActiveLayerBehavior::JoinSelection) {
483 remove_selected_points_in_active_layer(ranges_selected, dst_curves);
484 }
485
486 append_strokes_from(std::move(tmp_curves), dst_curves);
487
488 dst_curves.update_curve_types();
489 dst_curves.tag_topology_changed();
490 dst_drawing->tag_topology_changed();
491
492 DEG_id_tag_update(&grease_pencil.id, ID_RECALC_GEOMETRY);
493 WM_event_add_notifier(C, NC_GEOM | ND_DATA, &grease_pencil);
494
495 return OPERATOR_FINISHED;
496}
497
498void GREASE_PENCIL_OT_join_selection(wmOperatorType *ot)
499{
500 static const EnumPropertyItem active_layer_behavior[] = {
501 {int(ActiveLayerBehavior::JoinAndCopySelection),
502 "JOINCOPY",
503 0,
504 "Join and Copy",
505 "Copy the selection in the new stroke"},
506 {int(ActiveLayerBehavior::JoinSelection),
507 "JOIN",
508 0,
509 "Join",
510 "Move the selection to the new stroke"},
511 {0, nullptr, 0, nullptr, nullptr},
512 };
513
514 /* identifiers. */
515 ot->name = "Join Selection";
516 ot->idname = "GREASE_PENCIL_OT_join_selection";
517 ot->description = "New stroke from selected points/strokes";
518
519 /* callbacks. */
521 ot->exec = grease_pencil_join_selection_exec;
523
525
527 ot->srna,
528 "type",
529 active_layer_behavior,
530 int(ActiveLayerBehavior::JoinSelection),
531 "Type",
532 "Defines how the operator will behave on the selection in the active layer");
533}
534
535} // namespace
536
539} // namespace blender::ed::greasepencil
540
542{
543 using namespace blender::ed::greasepencil;
544
545 WM_operatortype_append(GREASE_PENCIL_OT_join_selection);
546}
Object * CTX_data_active_object(const bContext *C)
Scene * CTX_data_scene(const bContext *C)
Low-level operations for curves.
Low-level operations for grease pencil.
#define BLI_assert(a)
Definition BLI_assert.h:50
void DEG_id_tag_update(ID *id, unsigned int flags)
@ ID_RECALC_GEOMETRY
Definition DNA_ID.h:1041
@ CD_PROP_STRING
@ OPTYPE_UNDO
Definition WM_types.hh:162
@ OPTYPE_REGISTER
Definition WM_types.hh:160
#define NC_GEOM
Definition WM_types.hh:360
#define ND_DATA
Definition WM_types.hh:475
static IndexMask from_indices(Span< T > indices, IndexMaskMemory &memory)
draw_view push_constant(Type::INT, "radiance_src") .push_constant(Type capture_info_buf storage_buf(1, Qualifier::READ, "ObjectBounds", "bounds_buf[]") .push_constant(Type draw_view int
void ED_operatortypes_grease_pencil_join()
bool belongs_to_active_layer
bke::CurvesGeometry * owning_curves
blender::bke::AttrDomain ED_grease_pencil_selection_domain_get(const ToolSettings *tool_settings, const Object *object)
void convert_to_static_type(const CPPType &cpp_type, const Func &func)
auto attribute_filter_from_skip_ref(const Span< StringRef > skip)
void gather_attributes_to_groups(AttributeAccessor src_attributes, AttrDomain src_domain, AttrDomain dst_domain, const AttributeFilter &attribute_filter, OffsetIndices< int > dst_offsets, const IndexMask &src_selection, MutableAttributeAccessor dst_attributes)
void copy_attributes_group_to_group(AttributeAccessor src_attributes, AttrDomain src_domain, AttrDomain dst_domain, const AttributeFilter &attribute_filter, OffsetIndices< int > src_offsets, OffsetIndices< int > dst_offsets, const IndexMask &selection, MutableAttributeAccessor dst_attributes)
bool editable_grease_pencil_poll(bContext *C)
IndexMask retrieve_editable_and_selected_points(Object &object, const bke::greasepencil::Drawing &drawing, int layer_index, IndexMaskMemory &memory)
Vector< MutableDrawingInfo > retrieve_editable_drawings(const Scene &scene, GreasePencil &grease_pencil)
void masked_fill(MutableSpan< T > data, const T &value, const IndexMask &mask)
T distance_squared(const VecBase< T, Size > &a, const VecBase< T, Size > &b)
int RNA_enum_get(PointerRNA *ptr, const char *name)
PropertyRNA * RNA_def_enum(StructOrFunctionRNA *cont_, const char *identifier, const EnumPropertyItem *items, const int default_value, const char *ui_name, const char *ui_description)
#define FLT_MAX
Definition stdcycles.h:14
__int64 int64_t
Definition stdint.h:89
const char * name
Definition WM_types.hh:990
bool(* poll)(bContext *C) ATTR_WARN_UNUSED_RESULT
Definition WM_types.hh:1042
const char * idname
Definition WM_types.hh:992
int(* invoke)(bContext *C, wmOperator *op, const wmEvent *event) ATTR_WARN_UNUSED_RESULT
Definition WM_types.hh:1022
int(* exec)(bContext *C, wmOperator *op) ATTR_WARN_UNUSED_RESULT
Definition WM_types.hh:1006
const char * description
Definition WM_types.hh:996
PropertyRNA * prop
Definition WM_types.hh:1092
StructRNA * srna
Definition WM_types.hh:1080
struct PointerRNA * ptr
void WM_event_add_notifier(const bContext *C, uint type, void *reference)
wmOperatorType * ot
Definition wm_files.cc:4125
void WM_operatortype_append(void(*opfunc)(wmOperatorType *))
int WM_menu_invoke(bContext *C, wmOperator *op, const wmEvent *)