Fix #102598: Resample Curve node collapses curves to a single point

Collapsing curves to a single point when just resampling is unexpected. This
patch changes it so that non-zero-length curves keep at least one segment.

The fix is fairly straight forward, but a bunch of additional code is added to
support the legacy option to avoid breaking backward compatibility.

Pull Request: https://projects.blender.org/blender/blender/pulls/133659
This commit is contained in:
Jacques Lucke
2025-02-13 16:47:10 +01:00
parent 39a5dc3edd
commit 808635e52a
6 changed files with 127 additions and 21 deletions

View File

@@ -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<float> &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<bool> &selection_field,
const fn::Field<float> &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.

View File

@@ -32,24 +32,32 @@ static fn::Field<int> get_count_input_max_one(const fn::Field<int> &count_field)
return fn::Field<int>(fn::FieldOperation::Create(max_one_fn, {count_field}));
}
static fn::Field<int> get_count_input_from_length(const fn::Field<float> &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<float, float, int>(
/* 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<int> get_count_input_from_length(const fn::Field<float> &length_field,
const bool keep_last_segment)
{
static auto get_count_fn = mf::build::SI3_SO<float, float, bool, int>(
"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<float>(std::make_shared<bke::CurveLengthFieldInput>()), length_field});
{fn::Field<float>(std::make_shared<bke::CurveLengthFieldInput>()),
length_field,
fn::make_constant_field(keep_last_segment)});
return fn::Field<int>(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<float> &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<bool> &selection_field,
const fn::Field<float> &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);
}

View File

@@ -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 {

View File

@@ -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<const bNode *>(ptr->data); \
return node.member & (flag); \
}, \
[](PointerRNA *ptr, PropertyRNA * /*prop*/, const bool value) { \
bNode &node = *static_cast<bNode *>(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<const bNode *>(ptr->data); \
return node_storage(node).member & (flag); \
}, \
[](PointerRNA *ptr, PropertyRNA * /*prop*/, const bool value) { \
bNode &node = *static_cast<bNode *>(ptr->data); \
SET_FLAG_FROM_TEST(node_storage(node).member, value, (flag)); \
})
const EnumPropertyItem *enum_items_filter(const EnumPropertyItem *original_item_array,
FunctionRef<bool(const EnumPropertyItem &item)> 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<bool> default_value = std::nullopt,
bool allow_animation = false);
} // namespace blender::nodes

View File

@@ -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<NodeGeometryCurveResample>(__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;

View File

@@ -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<bool> 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