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
8
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;
38 bool belongs_to_active_layer;
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{
152
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/* -------------------------------------------------------------------- */
426
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);
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
538
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
struct GreasePencil GreasePencil
struct Object Object
struct Scene Scene
struct wmOperator wmOperator
in reality light always falls off quadratically Particle Retrieve the data of the particle that spawned the object for example to give variation to multiple instances of an object Point Retrieve information about points in a point cloud Retrieve the edges of an object as it appears to Cycles topology will always appear triangulated Convert a blackbody temperature to an RGB value Normal Generate a perturbed normal from an RGB normal map image Typically used for faking highly detailed surfaces Generate an OSL shader from a file or text data block Image Sample an image file as a texture Gabor Generate Gabor noise Gradient Generate interpolated color and intensity values based on the input vector Magic Generate a psychedelic color texture Voronoi Generate Worley noise based on the distance to random points Typically used to generate textures such as or biological cells Brick Generate a procedural texture producing bricks Texture Retrieve multiple types of texture coordinates nTypically used as inputs for texture nodes Vector Convert a or normal between and object coordinate space Combine Create a color from its and value channels Color Retrieve a color attribute
#define C
Definition RandGen.cpp:29
#define NC_GEOM
Definition WM_types.hh:360
#define ND_DATA
Definition WM_types.hh:475
@ OPTYPE_UNDO
Definition WM_types.hh:162
@ OPTYPE_REGISTER
Definition WM_types.hh:160
bke::CurvesGeometry & strokes_for_write()
void tag_topology_changed()
Vector< IndexRange > to_ranges() const
int64_t size() const
bool is_empty() const
constexpr int64_t first() const
constexpr int64_t last(const int64_t n=0) const
constexpr int64_t size() const
constexpr int64_t start() const
constexpr int64_t size() const
Definition BLI_span.hh:494
constexpr T & first() const
Definition BLI_span.hh:680
constexpr const T & first() const
Definition BLI_span.hh:316
constexpr int64_t size() const
Definition BLI_span.hh:253
constexpr const T * end() const
Definition BLI_span.hh:225
constexpr const T * begin() const
Definition BLI_span.hh:221
int64_t size() const
void append(const T &value)
void extend(Span< T > array)
T * end()
Span< T > as_span() const
T * begin()
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()
blender::bke::AttrDomain ED_grease_pencil_selection_domain_get(const ToolSettings *tool_settings, const Object *object)
ccl_device_inline float4 mask(const int4 mask, const float4 a)
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)
void index(const bNode &, void *r_value)
VecBase< float, 3 > float3
GPU_SHADER_INTERFACE_INFO(overlay_edit_curve_handle_iface, "vert").flat(Type pos vertex_in(1, Type::UINT, "data") .vertex_out(overlay_edit_curve_handle_iface) .geometry_layout(PrimitiveIn Frequency::GEOMETRY storage_buf(1, Qualifier::READ, "uint", "data[]", Frequency::GEOMETRY) .push_constant(Type Frequency::GEOMETRY selection[]
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
struct ToolSettings * toolsettings
struct RenderData r
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 *)