Blender V5.0
node_geo_blur_attribute.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
5#include "BLI_array.hh"
7#include "BLI_index_mask.hh"
8#include "BLI_index_range.hh"
9#include "BLI_span.hh"
10#include "BLI_task.hh"
11#include "BLI_virtual_array.hh"
12
13#include "BKE_attribute_math.hh"
14#include "BKE_curves.hh"
16#include "BKE_mesh.hh"
17#include "BKE_mesh_mapping.hh"
18
19#include "NOD_rna_define.hh"
20
22#include "UI_resources.hh"
23
24#include "RNA_enum_types.hh"
25
27
28#include "node_geometry_util.hh"
29
31
33{
34 b.use_custom_socket_order();
35 b.allow_any_socket_order();
36 b.add_default_layout();
37 const bNode *node = b.node_or_null();
38
39 if (node != nullptr) {
40 const eCustomDataType data_type = eCustomDataType(node->custom1);
41 b.add_input(data_type, "Value").supports_field().hide_value().is_default_link_socket();
42 b.add_output(data_type, "Value").field_source_reference_all().align_with_previous();
43 }
44 b.add_input<decl::Int>("Iterations")
45 .default_value(1)
46 .min(0)
47 .description("How many times to blur the values for all elements");
48 b.add_input<decl::Float>("Weight")
49 .default_value(1.0f)
51 .min(0.0f)
52 .max(1.0f)
53 .supports_field()
54 .description("Relative mix weight of neighboring elements");
55}
56
57static void node_layout(uiLayout *layout, bContext * /*C*/, PointerRNA *ptr)
58{
59 layout->prop(ptr, "data_type", UI_ITEM_NONE, "", ICON_NONE);
60}
61
62static void node_init(bNodeTree * /*tree*/, bNode *node)
63{
64 node->custom1 = CD_PROP_FLOAT;
65}
66
68{
69 const blender::bke::bNodeType &node_type = params.node_type();
70 const NodeDeclaration &declaration = *node_type.static_declaration;
71
72 /* Weight and Iterations inputs don't change based on the data type. */
74
75 const std::optional<eCustomDataType> new_node_type = bke::socket_type_to_custom_data_type(
76 eNodeSocketDatatype(params.other_socket().type));
77 if (!new_node_type.has_value()) {
78 return;
79 }
80 eCustomDataType fixed_data_type = *new_node_type;
81 if (fixed_data_type == CD_PROP_STRING) {
82 return;
83 }
84 if (fixed_data_type == CD_PROP_QUATERNION) {
85 fixed_data_type = CD_PROP_FLOAT3;
86 }
87 if (fixed_data_type == CD_PROP_FLOAT4X4) {
88 /* Don't implement matrix blurring for now. */
89 return;
90 }
91 if (fixed_data_type == CD_PROP_BOOL) {
92 /* This node does not support boolean sockets, use integer instead. */
93 fixed_data_type = CD_PROP_INT32;
94 }
95 params.add_item(IFACE_("Value"), [node_type, fixed_data_type](LinkSearchOpParams &params) {
96 bNode &node = params.add_node(node_type);
97 node.custom1 = fixed_data_type;
98 params.update_and_connect_available_socket(node, "Value");
99 });
100}
101
103 const int verts_num,
104 Array<int> &r_offsets,
105 Array<int> &r_indices)
106{
107 bke::mesh::build_vert_to_edge_map(edges, verts_num, r_offsets, r_indices);
108 const OffsetIndices<int> offsets(r_offsets);
109 threading::parallel_for(IndexRange(verts_num), 2048, [&](const IndexRange range) {
110 for (const int vert : range) {
111 MutableSpan<int> neighbors = r_indices.as_mutable_span().slice(offsets[vert]);
112 for (const int i : neighbors.index_range()) {
113 neighbors[i] = bke::mesh::edge_other_vert(edges[neighbors[i]], vert);
114 }
115 }
116 });
117}
118
120 const int verts_num,
121 Array<int> &r_offsets,
122 Array<int> &r_indices)
123{
124 Array<int> vert_to_edge_offset_data;
125 Array<int> vert_to_edge_indices;
127 edges, verts_num, vert_to_edge_offset_data, vert_to_edge_indices);
128 const OffsetIndices<int> vert_to_edge_offsets(vert_to_edge_offset_data);
129
130 r_offsets = Array<int>(edges.size() + 1, 0);
131 threading::parallel_for(edges.index_range(), 1024, [&](const IndexRange range) {
132 for (const int edge_i : range) {
133 const int2 edge = edges[edge_i];
134 r_offsets[edge_i] = vert_to_edge_offsets[edge[0]].size() - 1 +
135 vert_to_edge_offsets[edge[1]].size() - 1;
136 }
137 });
139 r_indices.reinitialize(offsets.total_size());
140
141 threading::parallel_for(edges.index_range(), 1024, [&](const IndexRange range) {
142 for (const int edge_i : range) {
143 const int2 edge = edges[edge_i];
144 MutableSpan<int> neighbors = r_indices.as_mutable_span().slice(offsets[edge_i]);
145 int count = 0;
146 for (const Span<int> neighbor_edges : {vert_to_edge[edge[0]], vert_to_edge[edge[1]]}) {
147 for (const int neighbor_edge : neighbor_edges) {
148 if (neighbor_edge != edge_i) {
149 neighbors[count] = neighbor_edge;
150 count++;
151 }
152 }
153 }
154 }
155 });
156}
157
159 const Span<int> corner_edges,
160 const int edges_num,
161 Array<int> &r_offsets,
162 Array<int> &r_indices)
163{
164 Array<int> edge_to_face_offset_data;
165 Array<int> edge_to_face_indices;
167 faces, corner_edges, edges_num, edge_to_face_offset_data, edge_to_face_indices);
168 const OffsetIndices<int> edge_to_face_offsets(edge_to_face_offset_data);
169
170 r_offsets = Array<int>(faces.size() + 1, 0);
171 threading::parallel_for(faces.index_range(), 4096, [&](const IndexRange range) {
172 for (const int face_i : range) {
173 for (const int edge : corner_edges.slice(faces[face_i])) {
174 /* Subtract face itself from the number of faces connected to the edge. */
175 r_offsets[face_i] += edge_to_face_offsets[edge].size() - 1;
176 }
177 }
178 });
180 r_indices.reinitialize(offsets.total_size());
181
182 threading::parallel_for(faces.index_range(), 1024, [&](IndexRange range) {
183 for (const int face_i : range) {
184 MutableSpan<int> neighbors = r_indices.as_mutable_span().slice(offsets[face_i]);
185 if (neighbors.is_empty()) {
186 continue;
187 }
188 int count = 0;
189 for (const int edge : corner_edges.slice(faces[face_i])) {
190 for (const int neighbor : edge_to_face_map[edge]) {
191 if (neighbor != face_i) {
192 neighbors[count] = neighbor;
193 count++;
194 }
195 }
196 }
197 }
198 });
199}
200
202 const AttrDomain domain,
203 Array<int> &r_offsets,
204 Array<int> &r_indices)
205{
206 switch (domain) {
207 case AttrDomain::Point:
208 build_vert_to_vert_by_edge_map(mesh.edges(), mesh.verts_num, r_offsets, r_indices);
209 break;
210 case AttrDomain::Edge:
211 build_edge_to_edge_by_vert_map(mesh.edges(), mesh.verts_num, r_offsets, r_indices);
212 break;
213 case AttrDomain::Face:
215 mesh.faces(), mesh.corner_edges(), mesh.edges_num, r_offsets, r_indices);
216 break;
217 default:
219 break;
220 }
221 return {OffsetIndices<int>(r_offsets), r_indices};
222}
223
224template<typename T>
225static Span<T> blur_on_mesh_exec(const Span<float> neighbor_weights,
226 const GroupedSpan<int> neighbors_map,
227 const int iterations,
228 const MutableSpan<T> buffer_a,
229 const MutableSpan<T> buffer_b)
230{
231 /* Source is set to buffer_b even though it is actually in buffer_a because the loop below starts
232 * with swapping both. */
233 MutableSpan<T> src = buffer_b;
234 MutableSpan<T> dst = buffer_a;
235
236 for ([[maybe_unused]] const int64_t iteration : IndexRange(iterations)) {
237 std::swap(src, dst);
239 threading::parallel_for(dst.index_range(), 1024, [&](const IndexRange range) {
240 for (const int64_t index : range) {
241 const Span<int> neighbors = neighbors_map[index];
242 const float neighbor_weight = neighbor_weights[index];
243 mixer.set(index, src[index], 1.0f);
244 for (const int neighbor : neighbors) {
245 mixer.mix_in(index, src[neighbor], neighbor_weight);
246 }
247 }
248 mixer.finalize(range);
249 });
250 }
251
252 return dst;
253}
254
255template<typename Func> static void to_static_type_for_blur(const CPPType &type, const Func &func)
256{
257 type.to_static_type_tag<int, float, float3, ColorGeometry4f>([&](auto type_tag) {
258 using T = typename decltype(type_tag)::type;
259 if constexpr (!std::is_same_v<T, void>) {
260 func(T());
261 }
262 else {
264 }
265 });
266}
267
268static GSpan blur_on_mesh(const Mesh &mesh,
269 const AttrDomain domain,
270 const int iterations,
271 const Span<float> neighbor_weights,
272 const GMutableSpan buffer_a,
273 const GMutableSpan buffer_b)
274{
275 Array<int> neighbor_offsets;
276 Array<int> neighbor_indices;
277 const GroupedSpan<int> neighbors_map = create_mesh_map(
278 mesh, domain, neighbor_offsets, neighbor_indices);
279
280 GSpan result_buffer;
281 to_static_type_for_blur(buffer_a.type(), [&](auto dummy) {
282 using T = decltype(dummy);
283 result_buffer = blur_on_mesh_exec<T>(
284 neighbor_weights, neighbors_map, iterations, buffer_a.typed<T>(), buffer_b.typed<T>());
285 });
286 return result_buffer;
287}
288
289template<typename T>
291 const Span<float> neighbor_weights,
292 const int iterations,
293 const MutableSpan<T> buffer_a,
294 const MutableSpan<T> buffer_b)
295{
296 MutableSpan<T> src = buffer_b;
297 MutableSpan<T> dst = buffer_a;
298
299 const OffsetIndices points_by_curve = curves.points_by_curve();
300 const VArray<bool> cyclic = curves.cyclic();
301
302 for ([[maybe_unused]] const int iteration : IndexRange(iterations)) {
303 std::swap(src, dst);
305 threading::parallel_for(curves.curves_range(), 256, [&](const IndexRange range) {
306 for (const int curve_i : range) {
307 const IndexRange points = points_by_curve[curve_i];
308 if (points.size() == 1) {
309 /* No mixing possible. */
310 const int point_i = points[0];
311 mixer.set(point_i, src[point_i], 1.0f);
312 continue;
313 }
314 /* Inner points. */
315 for (const int point_i : points.drop_front(1).drop_back(1)) {
316 const float neighbor_weight = neighbor_weights[point_i];
317 mixer.set(point_i, src[point_i], 1.0f);
318 mixer.mix_in(point_i, src[point_i - 1], neighbor_weight);
319 mixer.mix_in(point_i, src[point_i + 1], neighbor_weight);
320 }
321 const int first_i = points[0];
322 const float first_neighbor_weight = neighbor_weights[first_i];
323 const int last_i = points.last();
324 const float last_neighbor_weight = neighbor_weights[last_i];
325
326 /* First point. */
327 mixer.set(first_i, src[first_i], 1.0f);
328 mixer.mix_in(first_i, src[first_i + 1], first_neighbor_weight);
329 /* Last point. */
330 mixer.set(last_i, src[last_i], 1.0f);
331 mixer.mix_in(last_i, src[last_i - 1], last_neighbor_weight);
332
333 if (cyclic[curve_i]) {
334 /* First point. */
335 mixer.mix_in(first_i, src[last_i], first_neighbor_weight);
336 /* Last point. */
337 mixer.mix_in(last_i, src[first_i], last_neighbor_weight);
338 }
339 }
340 mixer.finalize(points_by_curve[range]);
341 });
342 }
343
344 return dst;
345}
346
348 const int iterations,
349 const Span<float> neighbor_weights,
350 const GMutableSpan buffer_a,
351 const GMutableSpan buffer_b)
352{
353 GSpan result_buffer;
354 to_static_type_for_blur(buffer_a.type(), [&](auto dummy) {
355 using T = decltype(dummy);
356 result_buffer = blur_on_curve_exec<T>(
357 curves, neighbor_weights, iterations, buffer_a.typed<T>(), buffer_b.typed<T>());
358 });
359 return result_buffer;
360}
361
363 private:
364 const Field<float> weight_field_;
365 const GField value_field_;
366 const int iterations_;
367
368 public:
369 BlurAttributeFieldInput(Field<float> weight_field, GField value_field, const int iterations)
370 : bke::GeometryFieldInput(value_field.cpp_type(), "Blur Attribute"),
371 weight_field_(std::move(weight_field)),
372 value_field_(std::move(value_field)),
373 iterations_(iterations)
374 {
375 }
376
378 const IndexMask & /*mask*/) const final
379 {
380 const int64_t domain_size = context.attributes()->domain_size(context.domain());
381
382 GArray<> buffer_a(*type_, domain_size);
383
384 FieldEvaluator evaluator(context, domain_size);
385
386 evaluator.add_with_destination(value_field_, buffer_a.as_mutable_span());
387 evaluator.add(weight_field_);
388 evaluator.evaluate();
389
390 /* Blurring does not make sense with a less than 2 elements. */
391 if (domain_size <= 1) {
392 return GVArray::from_garray(std::move(buffer_a));
393 }
394
395 if (iterations_ <= 0) {
396 return GVArray::from_garray(std::move(buffer_a));
397 }
398
399 VArraySpan<float> neighbor_weights = evaluator.get_evaluated<float>(1);
400 GArray<> buffer_b(*type_, domain_size);
401
402 GSpan result_buffer = buffer_a.as_span();
403 switch (context.type()) {
404 case GeometryComponent::Type::Mesh:
405 if (ELEM(context.domain(), AttrDomain::Point, AttrDomain::Edge, AttrDomain::Face)) {
406 if (const Mesh *mesh = context.mesh()) {
407 result_buffer = blur_on_mesh(
408 *mesh, context.domain(), iterations_, neighbor_weights, buffer_a, buffer_b);
409 }
410 }
411 break;
412 case GeometryComponent::Type::Curve:
413 case GeometryComponent::Type::GreasePencil:
414 if (context.domain() == AttrDomain::Point) {
415 if (const bke::CurvesGeometry *curves = context.curves_or_strokes()) {
416 result_buffer = blur_on_curves(
417 *curves, iterations_, neighbor_weights, buffer_a, buffer_b);
418 }
419 }
420 break;
421 default:
422 break;
423 }
424
425 BLI_assert(ELEM(result_buffer.data(), buffer_a.data(), buffer_b.data()));
426 if (result_buffer.data() == buffer_a.data()) {
427 return GVArray::from_garray(std::move(buffer_a));
428 }
429 return GVArray::from_garray(std::move(buffer_b));
430 }
431
432 void for_each_field_input_recursive(FunctionRef<void(const FieldInput &)> fn) const override
433 {
434 weight_field_.node().for_each_field_input_recursive(fn);
435 value_field_.node().for_each_field_input_recursive(fn);
436 }
437
438 uint64_t hash() const override
439 {
440 return get_default_hash(iterations_, weight_field_, value_field_);
441 }
442
443 bool is_equal_to(const fn::FieldNode &other) const override
444 {
445 if (const BlurAttributeFieldInput *other_blur = dynamic_cast<const BlurAttributeFieldInput *>(
446 &other))
447 {
448 return weight_field_ == other_blur->weight_field_ &&
449 value_field_ == other_blur->value_field_ && iterations_ == other_blur->iterations_;
450 }
451 return false;
452 }
453
454 std::optional<AttrDomain> preferred_domain(const GeometryComponent &component) const override
455 {
456 const std::optional<AttrDomain> domain = bke::try_detect_field_domain(component, value_field_);
457 if (domain.has_value() && *domain == AttrDomain::Corner) {
458 return AttrDomain::Point;
459 }
460 return domain;
461 }
462};
463
465{
466 const int iterations = params.extract_input<int>("Iterations");
467 Field<float> weight_field = params.extract_input<Field<float>>("Weight");
468
469 GField value_field = params.extract_input<GField>("Value");
470 GField output_field{std::make_shared<BlurAttributeFieldInput>(
471 std::move(weight_field), std::move(value_field), iterations)};
472 params.set_output<GField>("Value", std::move(output_field));
473}
474
475static void node_rna(StructRNA *srna)
476{
478 srna,
479 "data_type",
480 "Data Type",
481 "",
485 [](bContext * /*C*/, PointerRNA * /*ptr*/, PropertyRNA * /*prop*/, bool *r_free) {
486 *r_free = true;
489 });
490 });
491}
492
493static void node_register()
494{
495 static blender::bke::bNodeType ntype;
496 geo_node_type_base(&ntype, "GeometryNodeBlurAttribute", GEO_NODE_BLUR_ATTRIBUTE);
497 ntype.ui_name = "Blur Attribute";
498 ntype.ui_description = "Mix attribute values of neighboring elements";
499 ntype.enum_name_legacy = "BLUR_ATTRIBUTE";
501 ntype.initfunc = node_init;
502 ntype.declare = node_declare;
507
508 node_rna(ntype.rna_ext.srna);
509}
510NOD_REGISTER_NODE(node_register)
511
512} // namespace blender::nodes::node_geo_blur_attribute_cc
Low-level operations for curves.
#define NODE_CLASS_ATTRIBUTE
Definition BKE_node.hh:462
#define GEO_NODE_BLUR_ATTRIBUTE
#define BLI_assert_unreachable()
Definition BLI_assert.h:93
#define BLI_assert(a)
Definition BLI_assert.h:46
#define final(a, b, c)
Definition BLI_hash.h:19
#define ELEM(...)
#define IFACE_(msgid)
@ CD_PROP_FLOAT
@ CD_PROP_FLOAT3
@ CD_PROP_COLOR
@ CD_PROP_QUATERNION
@ CD_PROP_INT32
@ CD_PROP_STRING
@ CD_PROP_FLOAT4X4
eNodeSocketDatatype
#define NOD_REGISTER_NODE(REGISTER_FUNC)
#define NOD_inline_enum_accessors(member)
@ PROP_FACTOR
Definition RNA_types.hh:251
#define UI_ITEM_NONE
long long int int64_t
unsigned long long int uint64_t
MutableSpan< T > as_mutable_span()
Definition BLI_array.hh:248
void to_static_type_tag(const Fn &fn) const
GMutableSpan as_mutable_span()
GSpan as_span() const
const void * data() const
const CPPType & type() const
const void * data() const
static GVArray from_garray(GArray<> array)
constexpr IndexRange index_range() const
Definition BLI_span.hh:670
constexpr int64_t size() const
Definition BLI_span.hh:252
constexpr IndexRange index_range() const
Definition BLI_span.hh:401
OffsetIndices< int > points_by_curve() const
IndexRange curves_range() const
VArray< bool > cyclic() const
FieldInput(const CPPType &type, std::string debug_name="")
Definition field.cc:677
int add(GField field, GVArray *varray_ptr)
Definition field.cc:751
int add_with_destination(GField field, GVMutableArray dst)
Definition field.cc:738
const GVArray & get_evaluated(const int field_index) const
Definition FN_field.hh:448
const CPPType * type_
Definition FN_field.hh:270
const CPPType & cpp_type() const
Definition FN_field.hh:632
Vector< SocketDeclaration * > inputs
std::optional< AttrDomain > preferred_domain(const GeometryComponent &component) const override
BlurAttributeFieldInput(Field< float > weight_field, GField value_field, const int iterations)
void for_each_field_input_recursive(FunctionRef< void(const FieldInput &)> fn) const override
GVArray get_varray_for_context(const bke::GeometryFieldContext &context, const IndexMask &) const final
nullptr float
uiWidgetBaseParameters params[MAX_WIDGET_BASE_BATCH]
#define T
static char faces[256]
typename DefaultMixerStruct< T >::type DefaultMixer
int edge_other_vert(const int2 edge, const int vert)
Definition BKE_mesh.hh:370
GroupedSpan< int > build_edge_to_face_map(OffsetIndices< int > faces, Span< int > corner_edges, int edges_num, Array< int > &r_offsets, Array< int > &r_indices)
GroupedSpan< int > build_vert_to_edge_map(Span< int2 > edges, int verts_num, Array< int > &r_offsets, Array< int > &r_indices)
std::optional< AttrDomain > try_detect_field_domain(const GeometryComponent &component, const fn::GField &field)
void node_register_type(bNodeType &ntype)
Definition node.cc:2416
std::optional< eCustomDataType > socket_type_to_custom_data_type(eNodeSocketDatatype type)
Definition node.cc:5144
static void node_geo_exec(GeoNodeExecParams params)
static Span< T > blur_on_curve_exec(const bke::CurvesGeometry &curves, const Span< float > neighbor_weights, const int iterations, const MutableSpan< T > buffer_a, const MutableSpan< T > buffer_b)
static void node_gather_link_searches(GatherLinkSearchOpParams &params)
static void build_vert_to_vert_by_edge_map(const Span< int2 > edges, const int verts_num, Array< int > &r_offsets, Array< int > &r_indices)
static void to_static_type_for_blur(const CPPType &type, const Func &func)
static GSpan blur_on_mesh(const Mesh &mesh, const AttrDomain domain, const int iterations, const Span< float > neighbor_weights, const GMutableSpan buffer_a, const GMutableSpan buffer_b)
static GSpan blur_on_curves(const bke::CurvesGeometry &curves, const int iterations, const Span< float > neighbor_weights, const GMutableSpan buffer_a, const GMutableSpan buffer_b)
static GroupedSpan< int > create_mesh_map(const Mesh &mesh, const AttrDomain domain, Array< int > &r_offsets, Array< int > &r_indices)
static void node_layout(uiLayout *layout, bContext *, PointerRNA *ptr)
static void build_face_to_face_by_edge_map(const OffsetIndices< int > faces, const Span< int > corner_edges, const int edges_num, Array< int > &r_offsets, Array< int > &r_indices)
static void build_edge_to_edge_by_vert_map(const Span< int2 > edges, const int verts_num, Array< int > &r_offsets, Array< int > &r_indices)
static void node_init(bNodeTree *, bNode *node)
static void node_declare(NodeDeclarationBuilder &b)
static Span< T > blur_on_mesh_exec(const Span< float > neighbor_weights, const GroupedSpan< int > neighbors_map, const int iterations, const MutableSpan< T > buffer_a, const MutableSpan< T > buffer_b)
void search_link_ops_for_declarations(GatherLinkSearchOpParams &params, Span< SocketDeclaration * > declarations)
PropertyRNA * RNA_def_node_enum(StructRNA *srna, const char *identifier, const char *ui_name, const char *ui_description, const EnumPropertyItem *static_items, const EnumRNAAccessors accessors, std::optional< int > default_value, const EnumPropertyItemFunc item_func, const bool allow_animation)
const EnumPropertyItem * enum_items_filter(const EnumPropertyItem *original_item_array, FunctionRef< bool(const EnumPropertyItem &item)> fn)
OffsetIndices< int > accumulate_counts_to_offsets(MutableSpan< int > counts_to_offsets, int start_offset=0)
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
uint64_t get_default_hash(const T &v, const Args &...args)
Definition BLI_hash.hh:233
ColorSceneLinear4f< eAlpha::Premultiplied > ColorGeometry4f
VecBase< float, 3 > float3
void geo_node_type_base(blender::bke::bNodeType *ntype, std::string idname, const std::optional< int16_t > legacy_type)
const EnumPropertyItem rna_enum_attribute_type_items[]
StructRNA * srna
int edges_num
int verts_num
int16_t custom1
Defines a node type.
Definition BKE_node.hh:238
blender::nodes::NodeDeclaration * static_declaration
Definition BKE_node.hh:371
std::string ui_description
Definition BKE_node.hh:244
void(* initfunc)(bNodeTree *ntree, bNode *node)
Definition BKE_node.hh:289
NodeGeometryExecFunction geometry_node_execute
Definition BKE_node.hh:354
const char * enum_name_legacy
Definition BKE_node.hh:247
void(* draw_buttons)(uiLayout *, bContext *C, PointerRNA *ptr)
Definition BKE_node.hh:259
NodeGatherSocketLinkOperationsFunction gather_link_search_ops
Definition BKE_node.hh:378
NodeDeclareFunction declare
Definition BKE_node.hh:362
void prop(PointerRNA *ptr, PropertyRNA *prop, int index, int value, eUI_Item_Flag flag, std::optional< blender::StringRef > name_opt, int icon, std::optional< blender::StringRef > placeholder=std::nullopt)
i
Definition text_draw.cc:230
PointerRNA * ptr
Definition wm_files.cc:4238