Blender V4.5
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_grease_pencil.hh"
12#include "BKE_report.hh"
13
14#include "DNA_scene_types.h"
15
16#include "DEG_depsgraph.hh"
17
18#include "ED_curves.hh"
19#include "ED_grease_pencil.hh"
20#include "RNA_access.hh"
21
22#include "WM_api.hh"
23
24#include "RNA_define.hh"
25
26#include <algorithm>
27
29
30namespace {
31
36struct PointsRange {
37 bke::greasepencil::Drawing *from_drawing;
38 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 int64_t &r_total_points_selected,
54 IndexMaskMemory &memory)
55{
56 Vector<PointsRange> selected_ranges{};
57 r_total_points_selected = 0;
58
59 for (const MutableDrawingInfo &info : drawings) {
60 IndexMask points_selection = retrieve_editable_and_selected_points(
61 object, info.drawing, info.layer_index, memory);
62 if (points_selection.is_empty()) {
63 continue;
64 }
65 r_total_points_selected += points_selection.size();
66
67 const Vector<IndexRange> initial_ranges = points_selection.to_ranges();
68
74 Vector<IndexRange> ranges{};
75 const Array<int> points_map = info.drawing.strokes().point_to_curve_map();
76 for (const IndexRange initial_range : initial_ranges) {
77 if (points_map[initial_range.first()] == points_map[initial_range.last()]) {
78 selected_ranges.append({&info.drawing, initial_range});
79 continue;
80 }
81
82 IndexRange range = {initial_range.start(), 1};
83 int previous_curve = points_map[range.start()];
84 for (const int64_t index : initial_range.drop_front(1)) {
85 const int current_curve = points_map[index];
86 if (previous_curve != current_curve) {
87 selected_ranges.append({&info.drawing, range});
88 range = {index, 1};
89 previous_curve = current_curve;
90 }
91 else {
92 range = {range.start(), range.size() + 1};
93 }
94 }
95
96 selected_ranges.append({&info.drawing, range});
97 }
98 }
99
100 return selected_ranges;
101}
102
103template<typename T> void reverse_point_data(const IndexRange point_range, MutableSpan<T> data)
104{
105 data.slice(point_range.first(), point_range.size()).reverse();
106}
107
108template<typename T>
109void swap_handle_attributes(MutableSpan<T> handles_left, MutableSpan<T> handles_right)
110{
111 BLI_assert(handles_left.size() == handles_right.size());
112 threading::parallel_for(handles_left.index_range(), 8192, [&](const IndexRange range) {
113 for (const int point : range) {
114 std::swap(handles_left[point], handles_right[point]);
115 }
116 });
117};
118
123void reverse_points_of(bke::CurvesGeometry &dst_curves, const IndexRange points_to_reverse)
124{
125 bke::MutableAttributeAccessor attributes = dst_curves.attributes_for_write();
126
127 attributes.foreach_attribute([&](const bke::AttributeIter &iter) {
128 if (iter.domain != bke::AttrDomain::Point) {
129 return;
130 }
131 if (iter.data_type == CD_PROP_STRING) {
132 return;
133 }
134
135 bke::GSpanAttributeWriter attribute = attributes.lookup_for_write_span(iter.name);
136 bke::attribute_math::convert_to_static_type(attribute.span.type(), [&](auto dummy) {
137 using T = decltype(dummy);
138 reverse_point_data<T>(points_to_reverse, attribute.span.typed<T>());
139 });
140 attribute.finish();
141 });
142
143 /* Also needs to swap left/right bezier handles if handle attributes exist. */
144 if (attributes.contains("handle_left") && attributes.contains("handle_right")) {
145 MutableSpan<float3> handles_left = dst_curves.handle_positions_left_for_write().slice(
146 points_to_reverse);
147 MutableSpan<float3> handles_right = dst_curves.handle_positions_right_for_write().slice(
148 points_to_reverse);
149 swap_handle_attributes<float3>(handles_left, handles_right);
150 }
151 if (attributes.contains(".selection_handle_left") &&
152 attributes.contains(".selection_handle_right"))
153 {
154 bke::SpanAttributeWriter<bool> writer_left = attributes.lookup_for_write_span<bool>(
155 ".selection_handle_left");
156 bke::SpanAttributeWriter<bool> writer_right = attributes.lookup_for_write_span<bool>(
157 ".selection_handle_right");
158 const MutableSpan<bool> selection_left = writer_left.span.slice(points_to_reverse);
159 const MutableSpan<bool> selection_right = writer_right.span.slice(points_to_reverse);
160 swap_handle_attributes<bool>(selection_left, selection_right);
161 writer_left.finish();
162 writer_right.finish();
163 }
164 if (attributes.contains("handle_type_left") && attributes.contains("handle_type_right")) {
165 MutableSpan<int8_t> types_left = dst_curves.handle_types_left_for_write().slice(
166 points_to_reverse);
167 MutableSpan<int8_t> types_right = dst_curves.handle_types_right_for_write().slice(
168 points_to_reverse);
169 swap_handle_attributes<int8_t>(types_left, types_right);
170 }
171}
172
173void apply_action(ActionOnNextRange action,
174 const IndexRange working_range,
175 const IndexRange adding_range,
176 bke::CurvesGeometry &dst_curves)
177{
189
190 switch (action) {
191 case ActionOnNextRange::Nothing:
192 return;
193 case ActionOnNextRange::ReverseExisting: {
194 reverse_points_of(dst_curves, working_range);
195 break;
196 }
197 case ActionOnNextRange::ReverseAddition: {
198 const IndexRange src_range_on_dst = {working_range.last() + 1, adding_range.size()};
199 reverse_points_of(dst_curves, src_range_on_dst);
200 break;
201 }
202 case ActionOnNextRange::ReverseBoth: {
203 apply_action(ActionOnNextRange::ReverseExisting, working_range, adding_range, dst_curves);
204 apply_action(ActionOnNextRange::ReverseAddition, working_range, adding_range, dst_curves);
205 break;
206 }
207 }
208}
209
217int64_t compute_closest_range_to(PointsRange &range,
218 const Span<PointsRange> &ranges,
219 int64_t starting_from,
220 ActionOnNextRange &r_action)
221{
222 auto get_range_begin_end = [](const PointsRange &points_range) -> std::pair<float3, float3> {
223 const Span<float3> current_range_positions = points_range.from_drawing->strokes().positions();
224 const float3 range_begin = current_range_positions[points_range.range.first()];
225 const float3 range_end = current_range_positions[points_range.range.last()];
226
227 return {range_begin, range_end};
228 };
229
230 const auto [cur_range_begin, cur_range_end] = get_range_begin_end(range);
231 float min_dist = FLT_MAX;
232
233 int64_t ret_value = starting_from;
234 ActionOnNextRange action = ActionOnNextRange::Nothing;
235
236 const int64_t iterations = ranges.size() - starting_from;
237 for (const int64_t i : IndexRange(starting_from, iterations)) {
238 const auto [range_begin, range_end] = get_range_begin_end(ranges[i]);
239
240 float dist = math::distance_squared(cur_range_end, range_begin);
241 if (dist < min_dist) {
242 action = ActionOnNextRange::Nothing;
243 ret_value = i;
244 min_dist = dist;
245 }
246
247 dist = math::distance_squared(cur_range_begin, range_begin);
248 if (dist < min_dist) {
249 action = ActionOnNextRange::ReverseExisting;
250 ret_value = i;
251 min_dist = dist;
252 }
253
254 dist = math::distance_squared(cur_range_end, range_end);
255 if (dist < min_dist) {
256 action = ActionOnNextRange::ReverseAddition;
257 ret_value = i;
258 min_dist = dist;
259 }
260
261 dist = math::distance_squared(cur_range_begin, range_end);
262 if (dist < min_dist) {
263 action = ActionOnNextRange::ReverseBoth;
264 ret_value = i;
265 min_dist = dist;
266 }
267 }
268
269 r_action = action;
270 return ret_value;
271}
272
273void copy_range_to_dst(const PointsRange &points_range,
274 int &dst_starting_point,
275 bke::CurvesGeometry &dst_curves)
276{
277 Array<int> src_raw_offsets(2);
278 Array<int> dst_raw_offsets(2);
279
280 const int64_t selection_size = points_range.range.size();
281 src_raw_offsets[0] = points_range.range.first();
282 src_raw_offsets[1] = points_range.range.last() + 1;
283
284 dst_raw_offsets[0] = dst_starting_point;
285 dst_starting_point += selection_size;
286 dst_raw_offsets[1] = dst_starting_point;
287
288 OffsetIndices<int> src_offsets{src_raw_offsets};
289 OffsetIndices<int> dst_offsets{dst_raw_offsets};
290
291 copy_attributes_group_to_group(points_range.from_drawing->strokes().attributes(),
293 {},
294 {},
295 src_offsets,
296 dst_offsets,
297 IndexMask{1},
298 dst_curves.attributes_for_write());
299}
300
301PointsRange copy_point_attributes(MutableSpan<PointsRange> selected_ranges,
302 bke::CurvesGeometry &dst_curves,
303 bke::greasepencil::Drawing &dst_drawing)
304{
305 /* The algorithm for joining the points goes as follows:
306 * 1. Pick the first range of the selected ranges of points, which will be the working range
307 * 2. Copy the attributes of this range to dst_curves
308 * 3. Lookup in the remaining ranges for the one closer to the working range
309 * 4. Copy its attributes
310 * 5. In order to minimize the length of the stroke connecting them, reverse their points as
311 * needed
312 * 6. Extend the working range with the new range
313 * 7. Remove the new range from the list of remaining ranges. Lookup for the next one and
314 * continue
315 */
316
317 const PointsRange &first_range = selected_ranges.first();
318 PointsRange working_range = {&dst_drawing, {0, first_range.range.size()}};
319
320 int next_point_index = 0;
321 copy_range_to_dst(first_range, next_point_index, dst_curves);
322
323 const int64_t ranges = selected_ranges.size() - 1;
324 for (const int64_t i : IndexRange(1, ranges)) {
325 ActionOnNextRange action;
326 const int64_t closest_range = compute_closest_range_to(
327 working_range, selected_ranges, i, action);
328 std::swap(selected_ranges[i], selected_ranges[closest_range]);
329 PointsRange &next_range = selected_ranges[i];
330 copy_range_to_dst(next_range, next_point_index, dst_curves);
331 apply_action(action, working_range.range, next_range.range, dst_curves);
332 working_range.range = {0, next_point_index};
333 }
334
335 return working_range;
336}
337
338void copy_curve_attributes(Span<PointsRange> ranges_selected,
339 bke::CurvesGeometry &dst_curves,
340 bke::greasepencil::Drawing &dst_drawing)
341{
342 /* The decision of which stroke use to copy the curve attributes is a bit arbitrary, since the
343 * original selection may embrace several strokes. The criteria is as follows:
344 * - If the selection contained points from the active layer, the first selected stroke from it
345 * is used.
346 * - Otherwise, the first selected stroke is used.
347 * Reasoning behind is that the user will probably want to keep similar curve parameters for
348 * all the strokes in a layer.
349 * Also, the "cyclic" attribute is deliberately set to false, since user
350 * probably wants to set it manually
351 */
352
353 auto src_range = [&]() -> const PointsRange & {
354 const auto *it = std::find_if(
355 ranges_selected.begin(), ranges_selected.end(), [dst_drawing](const PointsRange &range) {
356 return range.from_drawing == &dst_drawing;
357 });
358
359 return it != ranges_selected.end() ? *it : ranges_selected.first();
360 }();
361
362 const bke::CurvesGeometry &src_curves = src_range.from_drawing->strokes();
363 const Array<int> points_map = src_curves.point_to_curve_map();
364 const int first_selected_curve = points_map[src_range.range.first()];
365
366 const int final_curve_index = dst_curves.curves_num() - 1;
367 const Array<int> dst_curves_raw_offsets = {final_curve_index, dst_curves.curves_num()};
368 const OffsetIndices<int> dst_curve_offsets{dst_curves_raw_offsets};
369
370 gather_attributes_to_groups(src_curves.attributes(),
374 dst_curve_offsets,
375 IndexMask({first_selected_curve, 1}),
376 dst_curves.attributes_for_write());
377 dst_curves.cyclic_for_write().first() = false;
378}
379
384void clear_selection_attribute(Span<PointsRange> ranges_selected)
385{
386 for (const PointsRange &range : ranges_selected) {
387 bke::CurvesGeometry &curves = range.from_drawing->strokes_for_write();
388 bke::MutableAttributeAccessor attributes = curves.attributes_for_write();
389 if (bke::GSpanAttributeWriter selection = attributes.lookup_for_write_span(".selection")) {
390 ed::curves::fill_selection_false(selection.span);
391 selection.finish();
392 }
393 }
394}
395
396void remove_selected_points(Span<PointsRange> ranges_selected)
397{
398 /* Removing points from a drawing invalidates subsequent ranges for the same drawing.
399 * Combine all ranges for the same drawings first to prevent removing the wrong points. */
400 using RangesMap = Map<bke::greasepencil::Drawing *, Vector<IndexMask>>;
401 RangesMap ranges_by_drawing;
402 for (const PointsRange &points_range : ranges_selected) {
403 BLI_assert(points_range.from_drawing != nullptr);
404 Vector<IndexMask> &ranges = ranges_by_drawing.lookup_or_add(points_range.from_drawing, {});
405 ranges.append(points_range.range);
406 }
407
408 for (const RangesMap::Item &item : ranges_by_drawing.items()) {
409 bke::CurvesGeometry &dst_curves = item.key->strokes_for_write();
410 IndexMaskMemory memory;
411 const IndexMask combined_mask = IndexMask::from_union(item.value, memory);
412 dst_curves.remove_points(combined_mask, {});
413 }
414}
415
416void append_strokes_from(bke::CurvesGeometry &&other, bke::CurvesGeometry &dst)
417{
418 const int initial_points_num = dst.points_num();
419 const int initial_curves_num = dst.curves_num();
420 const int other_points_num = other.points_num();
421 const int other_curves_num = other.curves_num();
422
423 dst.resize(initial_points_num + other_points_num, initial_curves_num + other_curves_num);
424
425 Array<int> other_raw_offsets{0, other_points_num};
426 Array<int> dst_raw_offsets{initial_points_num, initial_points_num + other_points_num};
427
428 OffsetIndices<int> other_point_offsets{other_raw_offsets};
429 OffsetIndices<int> dst_point_offsets{dst_raw_offsets};
430
431 copy_attributes_group_to_group(other.attributes(),
434 {},
435 other_point_offsets,
436 dst_point_offsets,
437 IndexMask{1},
438 dst.attributes_for_write());
439
440 other_raw_offsets = {0, other_curves_num};
441 dst_raw_offsets = {initial_curves_num, initial_curves_num + other_curves_num};
442
443 OffsetIndices<int> other_curve_offsets{other_raw_offsets};
444 OffsetIndices<int> dst_curve_offsets{dst_raw_offsets};
445
446 copy_attributes_group_to_group(other.attributes(),
449 {},
450 other_curve_offsets,
451 dst_curve_offsets,
452 IndexMask{1},
453 dst.attributes_for_write());
454}
455
456/* -------------------------------------------------------------------- */
459
464wmOperatorStatus grease_pencil_join_selection_exec(bContext *C, wmOperator *op)
465{
466 using namespace bke::greasepencil;
467
468 const Scene *scene = CTX_data_scene(C);
471 scene->toolsettings, object);
472 GreasePencil &grease_pencil = *static_cast<GreasePencil *>(object->data);
473 if (!grease_pencil.has_active_layer()) {
474 BKE_report(op->reports, RPT_ERROR, "No active layer");
475 return OPERATOR_CANCELLED;
476 }
477
478 const ActiveLayerBehavior active_layer_behavior = static_cast<ActiveLayerBehavior>(
479 RNA_enum_get(op->ptr, "type"));
480 const Layer &active_layer = *grease_pencil.get_active_layer();
481
482 Drawing *dst_drawing = grease_pencil.get_editable_drawing_at(active_layer, scene->r.cfra);
483 if (dst_drawing == nullptr) {
484 return OPERATOR_CANCELLED;
485 }
486
487 IndexMaskMemory memory;
488 int64_t selected_points_count;
489 const Vector<MutableDrawingInfo> editable_drawings = retrieve_editable_drawings(*scene,
490 grease_pencil);
491 Vector<PointsRange> ranges_selected = retrieve_selection_ranges(
492 *object, editable_drawings, selected_points_count, memory);
493 if (ranges_selected.size() <= 1) {
494 /* Nothing to join */
495 return OPERATOR_FINISHED;
496 }
497
498 /* Temporary geometry where to perform the logic
499 * Once it gets stable, it is appended all at once to the destination curves */
500 bke::CurvesGeometry tmp_curves(selected_points_count, 1);
501
502 const PointsRange working_range = copy_point_attributes(
503 ranges_selected, tmp_curves, *dst_drawing);
504 copy_curve_attributes(ranges_selected, tmp_curves, *dst_drawing);
505
506 clear_selection_attribute(ranges_selected);
507
508 Array<PointsRange> working_range_collection = {working_range};
509 clear_selection_attribute(working_range_collection);
510
511 bke::CurvesGeometry &dst_curves = dst_drawing->strokes_for_write();
512 if (active_layer_behavior == ActiveLayerBehavior::JoinSelection) {
513 remove_selected_points(ranges_selected);
514 }
515
516 append_strokes_from(std::move(tmp_curves), dst_curves);
517
518 bke::GSpanAttributeWriter selection = ed::curves::ensure_selection_attribute(
519 dst_curves, selection_domain, CD_PROP_BOOL);
520
521 if (selection_domain == bke::AttrDomain::Curve) {
522 ed::curves::fill_selection_true(selection.span.take_back(tmp_curves.curves_num()));
523 }
524 else {
525 ed::curves::fill_selection_true(selection.span.take_back(tmp_curves.points_num()));
526 }
527 selection.finish();
528
529 dst_curves.update_curve_types();
530 dst_curves.tag_topology_changed();
531 dst_drawing->tag_topology_changed();
532
533 DEG_id_tag_update(&grease_pencil.id, ID_RECALC_GEOMETRY);
534 WM_event_add_notifier(C, NC_GEOM | ND_DATA, &grease_pencil);
535
536 return OPERATOR_FINISHED;
537}
538
539void GREASE_PENCIL_OT_join_selection(wmOperatorType *ot)
540{
541 static const EnumPropertyItem active_layer_behavior[] = {
542 {int(ActiveLayerBehavior::JoinAndCopySelection),
543 "JOINCOPY",
544 0,
545 "Join and Copy",
546 "Copy the selection in the new stroke"},
547 {int(ActiveLayerBehavior::JoinSelection),
548 "JOIN",
549 0,
550 "Join",
551 "Move the selection to the new stroke"},
552 {0, nullptr, 0, nullptr, nullptr},
553 };
554
555 /* identifiers. */
556 ot->name = "Join Selection";
557 ot->idname = "GREASE_PENCIL_OT_join_selection";
558 ot->description = "New stroke from selected points/strokes";
559
560 /* callbacks. */
562 ot->exec = grease_pencil_join_selection_exec;
564
566
568 ot->srna,
569 "type",
570 active_layer_behavior,
571 int(ActiveLayerBehavior::JoinSelection),
572 "Type",
573 "Defines how the operator will behave on the selection in the active layer");
574}
575
576} // namespace
577
579
580} // namespace blender::ed::greasepencil
581
583{
584 using namespace blender::ed::greasepencil;
585
586 WM_operatortype_append(GREASE_PENCIL_OT_join_selection);
587}
Object * CTX_data_active_object(const bContext *C)
Scene * CTX_data_scene(const bContext *C)
Low-level operations for grease pencil.
void BKE_report(ReportList *reports, eReportType type, const char *message)
Definition report.cc:126
#define BLI_assert(a)
Definition BLI_assert.h:46
float[3] Vector
void DEG_id_tag_update(ID *id, unsigned int flags)
@ ID_RECALC_GEOMETRY
Definition DNA_ID.h:982
@ CD_PROP_STRING
struct GreasePencil GreasePencil
struct Object Object
struct Scene Scene
@ OPERATOR_CANCELLED
@ OPERATOR_FINISHED
struct wmOperator wmOperator
#define C
Definition RandGen.cpp:29
#define NC_GEOM
Definition WM_types.hh:390
#define ND_DATA
Definition WM_types.hh:506
@ OPTYPE_UNDO
Definition WM_types.hh:182
@ OPTYPE_REGISTER
Definition WM_types.hh:180
BMesh const char void * data
long long int int64_t
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:493
constexpr T & first() const
Definition BLI_span.hh:679
constexpr IndexRange index_range() const
Definition BLI_span.hh:670
constexpr const T & first() const
Definition BLI_span.hh:315
constexpr int64_t size() const
Definition BLI_span.hh:252
constexpr const T * end() const
Definition BLI_span.hh:224
constexpr const T * begin() const
Definition BLI_span.hh:220
int64_t size() const
void append(const T &value)
static IndexMask from_union(const IndexMask &mask_a, const IndexMask &mask_b, IndexMaskMemory &memory)
void ED_operatortypes_grease_pencil_join()
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)
void fill_selection_false(GMutableSpan selection)
void fill_selection_true(GMutableSpan selection)
bke::GSpanAttributeWriter ensure_selection_attribute(bke::CurvesGeometry &curves, bke::AttrDomain selection_domain, eCustomDataType create_type, StringRef attribute_name)
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)
T distance_squared(const VecBase< T, Size > &a, const VecBase< T, Size > &b)
void parallel_for(const IndexRange range, const int64_t grain_size, const Function &function, const TaskSizeHints &size_hints=detail::TaskSizeHints_Static(1))
Definition BLI_task.hh:93
VecBase< float, 3 > float3
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
struct ToolSettings * toolsettings
struct RenderData r
const char * name
Definition WM_types.hh:1030
wmOperatorStatus(* exec)(bContext *C, wmOperator *op) ATTR_WARN_UNUSED_RESULT
Definition WM_types.hh:1046
bool(* poll)(bContext *C) ATTR_WARN_UNUSED_RESULT
Definition WM_types.hh:1086
const char * idname
Definition WM_types.hh:1032
const char * description
Definition WM_types.hh:1036
wmOperatorStatus(* invoke)(bContext *C, wmOperator *op, const wmEvent *event) ATTR_WARN_UNUSED_RESULT
Definition WM_types.hh:1062
PropertyRNA * prop
Definition WM_types.hh:1136
StructRNA * srna
Definition WM_types.hh:1124
struct ReportList * reports
struct PointerRNA * ptr
i
Definition text_draw.cc:230
void WM_event_add_notifier(const bContext *C, uint type, void *reference)
wmOperatorType * ot
Definition wm_files.cc:4225
void WM_operatortype_append(void(*opfunc)(wmOperatorType *))
wmOperatorStatus WM_menu_invoke(bContext *C, wmOperator *op, const wmEvent *)