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:
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user