diff --git a/source/blender/geometry/GEO_resample_curves.hh b/source/blender/geometry/GEO_resample_curves.hh index 6199bf2bd29..1d3c4c5f6ca 100644 --- a/source/blender/geometry/GEO_resample_curves.hh +++ b/source/blender/geometry/GEO_resample_curves.hh @@ -38,16 +38,22 @@ CurvesGeometry resample_to_count(const CurvesGeometry &src_curves, * Create new curves resampled to make each segment have the length specified by the * #segment_length field input, rounded to make the length of each segment the same. * The accuracy will depend on the curve's resolution parameter. + * + * \param keep_last_segment: If false, curves that are too short are collapsed to a single point. + * If true, they will have at least one segment after resampling. This mainly exists for + * compatibility. */ CurvesGeometry resample_to_length(const CurvesGeometry &src_curves, const IndexMask &selection, const VArray &sample_lengths, - const ResampleCurvesOutputAttributeIDs &output_ids = {}); + const ResampleCurvesOutputAttributeIDs &output_ids = {}, + bool keep_last_segment = false); CurvesGeometry resample_to_length(const CurvesGeometry &src_curves, const fn::FieldContext &field_context, const fn::Field &selection_field, const fn::Field &segment_length_field, - const ResampleCurvesOutputAttributeIDs &output_ids = {}); + const ResampleCurvesOutputAttributeIDs &output_ids = {}, + bool keep_last_segment = false); /** * Evaluate each selected curve to its implicit evaluated points. diff --git a/source/blender/geometry/intern/resample_curves.cc b/source/blender/geometry/intern/resample_curves.cc index d8e63e08c30..ad87163debf 100644 --- a/source/blender/geometry/intern/resample_curves.cc +++ b/source/blender/geometry/intern/resample_curves.cc @@ -32,24 +32,32 @@ static fn::Field get_count_input_max_one(const fn::Field &count_field) return fn::Field(fn::FieldOperation::Create(max_one_fn, {count_field})); } -static fn::Field get_count_input_from_length(const fn::Field &length_field) +static int get_count_from_length(const float curve_length, + const float sample_length, + const bool keep_last_segment) { - static auto get_count_fn = mf::build::SI2_SO( + /* Find the number of sampled segments by dividing the total length by + * the sample length. Then there is one more sampled point than segment. */ + if (UNLIKELY(sample_length == 0.0f)) { + return 1; + } + const int count = int(curve_length / sample_length) + 1; + return std::max(keep_last_segment ? 2 : 1, count); +} + +static fn::Field get_count_input_from_length(const fn::Field &length_field, + const bool keep_last_segment) +{ + static auto get_count_fn = mf::build::SI3_SO( "Length Input to Count", - [](const float curve_length, const float sample_length) { - /* Find the number of sampled segments by dividing the total length by - * the sample length. Then there is one more sampled point than segment. */ - if (UNLIKELY(sample_length == 0.0f)) { - return 1; - } - const int count = int(curve_length / sample_length) + 1; - return std::max(1, count); - }, - mf::build::exec_presets::AllSpanOrSingle()); + get_count_from_length, + mf::build::exec_presets::SomeSpanOrSingle<0, 1>()); auto get_count_op = fn::FieldOperation::Create( get_count_fn, - {fn::Field(std::make_shared()), length_field}); + {fn::Field(std::make_shared()), + length_field, + fn::make_constant_field(keep_last_segment)}); return fn::Field(std::move(get_count_op)); } @@ -486,7 +494,8 @@ CurvesGeometry resample_to_count(const CurvesGeometry &src_curves, CurvesGeometry resample_to_length(const CurvesGeometry &src_curves, const IndexMask &selection, const VArray &sample_lengths, - const ResampleCurvesOutputAttributeIDs &output_ids) + const ResampleCurvesOutputAttributeIDs &output_ids, + const bool keep_last_segment) { if (src_curves.curves_range().is_empty()) { return {}; @@ -503,7 +512,8 @@ CurvesGeometry resample_to_length(const CurvesGeometry &src_curves, selection.foreach_index(GrainSize(1024), [&](const int curve_i) { const float curve_length = src_curves.evaluated_length_total_for_curve(curve_i, curves_cyclic[curve_i]); - dst_offsets[curve_i] = int(curve_length / sample_lengths[curve_i]) + 1; + dst_offsets[curve_i] = get_count_from_length( + curve_length, sample_lengths[curve_i], keep_last_segment); }); IndexMaskMemory memory; @@ -523,12 +533,13 @@ CurvesGeometry resample_to_length(const CurvesGeometry &src_curves, const fn::FieldContext &field_context, const fn::Field &selection_field, const fn::Field &segment_length_field, - const ResampleCurvesOutputAttributeIDs &output_ids) + const ResampleCurvesOutputAttributeIDs &output_ids, + const bool keep_last_segment) { return resample_to_uniform(src_curves, field_context, selection_field, - get_count_input_from_length(segment_length_field), + get_count_input_from_length(segment_length_field, keep_last_segment), output_ids); } diff --git a/source/blender/makesdna/DNA_node_types.h b/source/blender/makesdna/DNA_node_types.h index 74a1643937e..5ff8266dc60 100644 --- a/source/blender/makesdna/DNA_node_types.h +++ b/source/blender/makesdna/DNA_node_types.h @@ -1833,6 +1833,11 @@ typedef struct NodeGeometryCurvePrimitiveQuad { typedef struct NodeGeometryCurveResample { /** #GeometryNodeCurveResampleMode. */ uint8_t mode; + /** + * If false, curves may be collapsed to a single point. This is unexpected and is only supported + * for compatibility reasons (#102598). + */ + uint8_t keep_last_segment; } NodeGeometryCurveResample; typedef struct NodeGeometryCurveFillet { diff --git a/source/blender/nodes/NOD_rna_define.hh b/source/blender/nodes/NOD_rna_define.hh index 37c1465906d..33307573750 100644 --- a/source/blender/nodes/NOD_rna_define.hh +++ b/source/blender/nodes/NOD_rna_define.hh @@ -58,6 +58,46 @@ struct EnumRNAAccessors { node_storage(node).member = value; \ }) +struct BooleanRNAAccessors { + BooleanPropertyGetFunc getter; + BooleanPropertySetFunc setter; + + BooleanRNAAccessors(BooleanPropertyGetFunc getter, BooleanPropertySetFunc setter) + : getter(getter), setter(setter) + { + } +}; + +/** + * Generates accessor methods for a property stored directly in the `bNode`, typically + * `bNode->custom1` or similar. + */ +#define NOD_inline_boolean_accessors(member, flag) \ + BooleanRNAAccessors( \ + [](PointerRNA *ptr, PropertyRNA * /*prop*/) -> bool { \ + const bNode &node = *static_cast(ptr->data); \ + return node.member & (flag); \ + }, \ + [](PointerRNA *ptr, PropertyRNA * /*prop*/, const bool value) { \ + bNode &node = *static_cast(ptr->data); \ + SET_FLAG_FROM_TEST(node.member, value, (flag)); \ + }) + +/** + * Generates accessor methods for a property stored in `bNode->storage`. This is expected to be + * used in a node file that uses #NODE_STORAGE_FUNCS. + */ +#define NOD_storage_boolean_accessors(member, flag) \ + BooleanRNAAccessors( \ + [](PointerRNA *ptr, PropertyRNA * /*prop*/) -> bool { \ + const bNode &node = *static_cast(ptr->data); \ + return node_storage(node).member & (flag); \ + }, \ + [](PointerRNA *ptr, PropertyRNA * /*prop*/, const bool value) { \ + bNode &node = *static_cast(ptr->data); \ + SET_FLAG_FROM_TEST(node_storage(node).member, value, (flag)); \ + }) + const EnumPropertyItem *enum_items_filter(const EnumPropertyItem *original_item_array, FunctionRef fn); @@ -71,4 +111,12 @@ PropertyRNA *RNA_def_node_enum(StructRNA *srna, const EnumPropertyItemFunc item_func = nullptr, bool allow_animation = false); +PropertyRNA *RNA_def_node_boolean(StructRNA *srna, + const char *identifier, + const char *ui_name, + const char *ui_description, + const BooleanRNAAccessors accessors, + std::optional default_value = std::nullopt, + bool allow_animation = false); + } // namespace blender::nodes diff --git a/source/blender/nodes/geometry/nodes/node_geo_curve_resample.cc b/source/blender/nodes/geometry/nodes/node_geo_curve_resample.cc index fdc4e69459d..e614f0e0a3e 100644 --- a/source/blender/nodes/geometry/nodes/node_geo_curve_resample.cc +++ b/source/blender/nodes/geometry/nodes/node_geo_curve_resample.cc @@ -47,11 +47,17 @@ static void node_layout(uiLayout *layout, bContext * /*C*/, PointerRNA *ptr) uiItemR(layout, ptr, "mode", UI_ITEM_NONE, "", ICON_NONE); } +static void node_layout_ex(uiLayout *layout, bContext * /*C*/, PointerRNA *ptr) +{ + uiItemR(layout, ptr, "keep_last_segment", UI_ITEM_NONE, std::nullopt, ICON_NONE); +} + static void node_init(bNodeTree * /*tree*/, bNode *node) { NodeGeometryCurveResample *data = MEM_cnew(__func__); data->mode = GEO_NODE_CURVE_RESAMPLE_COUNT; + data->keep_last_segment = true; node->storage = data; } @@ -106,7 +112,7 @@ static void node_geo_exec(GeoNodeExecParams params) const bke::CurvesGeometry &src_curves = src_curves_id->geometry.wrap(); const bke::CurvesFieldContext field_context{*src_curves_id, AttrDomain::Curve}; bke::CurvesGeometry dst_curves = geometry::resample_to_length( - src_curves, field_context, selection, length); + src_curves, field_context, selection, length, {}, storage.keep_last_segment); Curves *dst_curves_id = bke::curves_new_nomain(std::move(dst_curves)); bke::curves_copy_parameters(*src_curves_id, *dst_curves_id); geometry.replace_curves(dst_curves_id); @@ -122,7 +128,7 @@ static void node_geo_exec(GeoNodeExecParams params) const bke::GreasePencilLayerFieldContext field_context( *grease_pencil, AttrDomain::Curve, layer_index); bke::CurvesGeometry dst_curves = geometry::resample_to_length( - src_curves, field_context, selection, length); + src_curves, field_context, selection, length, {}, storage.keep_last_segment); drawing->strokes_for_write() = std::move(dst_curves); drawing->tag_topology_changed(); } @@ -193,6 +199,13 @@ static void node_rna(StructRNA *srna) "How to specify the amount of samples", mode_items, NOD_storage_enum_accessors(mode)); + + RNA_def_node_boolean(srna, + "keep_last_segment", + "Keep Last Segment", + "Don't collapse a curves to single points if they are shorter than the " + "given length. The collapsing behavior exists for compatibility reasons.", + NOD_storage_boolean_accessors(keep_last_segment, 1)); } static void node_register() @@ -206,6 +219,7 @@ static void node_register() ntype.nclass = NODE_CLASS_GEOMETRY; ntype.declare = node_declare; ntype.draw_buttons = node_layout; + ntype.draw_buttons_ex = node_layout_ex; blender::bke::node_type_storage( &ntype, "NodeGeometryCurveResample", node_free_standard_storage, node_copy_standard_storage); ntype.initfunc = node_init; diff --git a/source/blender/nodes/intern/node_rna_define.cc b/source/blender/nodes/intern/node_rna_define.cc index 3d01a16f922..14c919ce117 100644 --- a/source/blender/nodes/intern/node_rna_define.cc +++ b/source/blender/nodes/intern/node_rna_define.cc @@ -50,4 +50,26 @@ PropertyRNA *RNA_def_node_enum(StructRNA *srna, return prop; } +PropertyRNA *RNA_def_node_boolean(StructRNA *srna, + const char *identifier, + const char *ui_name, + const char *ui_description, + const BooleanRNAAccessors accessors, + std::optional default_value, + bool allow_animation) +{ + PropertyRNA *prop = RNA_def_property(srna, identifier, PROP_BOOLEAN, PROP_NONE); + RNA_def_property_boolean_funcs_runtime(prop, accessors.getter, accessors.setter); + if (default_value.has_value()) { + RNA_def_property_boolean_default(prop, *default_value); + } + RNA_def_property_ui_text(prop, ui_name, ui_description); + if (!allow_animation) { + RNA_def_property_clear_flag(prop, PROP_ANIMATABLE); + } + RNA_def_property_update_runtime(prop, rna_Node_socket_update); + RNA_def_property_update_notifier(prop, NC_NODE | NA_EDITED); + return prop; +} + } // namespace blender::nodes