Geometry Nodes: support wildcard in Remove Named Attribute node

This adds support for removing multiple named attributes from a geometry at once
using a string pattern that can contain a single wildcard character (`*`).

Using the pattern `sim_*` removes all attributes with the `sim_` prefix for example.

The most difficult design issue here is the decision of what pattern matching language
to use. In the end we only settled on the lowest common denominator for now. This
is already very useful and does not limit us much in the future if we want to support
more complex pattern matching. Once we support lists, some of the use-cases can
also be solved more generally without extra functionality in the Remove Named Attribute
node.

Different pattern matching languages have been discussed before:
https://devtalk.blender.org/t/string-pattern-matching-language-in-blender-and-geometry-nodes/27410

Pull Request: https://projects.blender.org/blender/blender/pulls/118258
This commit is contained in:
Jacques Lucke
2024-02-19 21:21:18 +01:00
parent 4ed6daf490
commit 6c46178a7f

View File

@@ -6,8 +6,18 @@
#include <fmt/format.h>
#include "NOD_rna_define.hh"
#include "UI_interface.hh"
#include "UI_resources.hh"
namespace blender::nodes::node_geo_remove_attribute_cc {
enum class PatternMode {
Exact,
Wildcard,
};
static void node_declare(NodeDeclarationBuilder &b)
{
b.add_input<decl::Geometry>("Geometry");
@@ -15,22 +25,44 @@ static void node_declare(NodeDeclarationBuilder &b)
b.add_output<decl::Geometry>("Geometry").propagate_all();
}
static void node_layout(uiLayout *layout, bContext * /*C*/, PointerRNA *ptr)
{
uiItemR(layout, ptr, "pattern_mode", UI_ITEM_NONE, "", ICON_NONE);
}
static void node_geo_exec(GeoNodeExecParams params)
{
GeometrySet geometry_set = params.extract_input<GeometrySet>("Geometry");
const std::string name = params.extract_input<std::string>("Name");
if (name.empty()) {
const std::string pattern = params.extract_input<std::string>("Name");
if (pattern.empty()) {
params.set_output("Geometry", std::move(geometry_set));
return;
}
if (!bke::allow_procedural_attribute_access(name)) {
params.error_message_add(NodeWarningType::Info, TIP_(bke::no_procedural_access_message));
params.set_output("Geometry", std::move(geometry_set));
return;
const bNode &node = params.node();
PatternMode pattern_mode = PatternMode(node.custom1);
if (pattern_mode == PatternMode::Wildcard) {
const int wildcard_count = Span(pattern.c_str(), pattern.size()).count('*');
if (wildcard_count == 0) {
pattern_mode = PatternMode::Exact;
}
else if (wildcard_count >= 2) {
params.error_message_add(NodeWarningType::Info,
TIP_("Only one * is supported in the pattern"));
params.set_output("Geometry", std::move(geometry_set));
return;
}
}
std::atomic<bool> attribute_exists = false;
std::atomic<bool> cannot_delete = false;
StringRef wildcard_prefix;
StringRef wildcard_suffix;
if (pattern_mode == PatternMode::Wildcard) {
const int wildcard_index = pattern.find('*');
wildcard_prefix = StringRef(pattern).substr(0, wildcard_index);
wildcard_suffix = StringRef(pattern).substr(wildcard_index + 1);
}
Set<std::string> removed_attributes;
Set<std::string> failed_attributes;
geometry_set.modify_geometry_sets([&](GeometrySet &geometry_set) {
for (const GeometryComponent::Type type : {GeometryComponent::Type::Mesh,
@@ -38,42 +70,103 @@ static void node_geo_exec(GeoNodeExecParams params)
GeometryComponent::Type::Curve,
GeometryComponent::Type::Instance})
{
if (geometry_set.has(type)) {
/* First check if the attribute exists before getting write access,
* to avoid potentially expensive unnecessary copies. */
const GeometryComponent &read_only_component = *geometry_set.get_component(type);
if (read_only_component.attributes()->contains(name)) {
attribute_exists = true;
if (!geometry_set.has(type)) {
continue;
}
/* First check if the attribute exists before getting write access,
* to avoid potentially expensive unnecessary copies. */
const GeometryComponent &read_only_component = *geometry_set.get_component(type);
Vector<std::string> attributes_to_remove;
switch (pattern_mode) {
case PatternMode::Exact: {
if (read_only_component.attributes()->contains(pattern)) {
attributes_to_remove.append(pattern);
}
break;
}
else {
case PatternMode::Wildcard: {
read_only_component.attributes()->for_all(
[&](const blender::bke::AttributeIDRef &id,
const blender::bke::AttributeMetaData /*meta_data*/) {
if (id.is_anonymous()) {
return true;
}
const StringRef attribute_name = id.name();
if (attribute_name.startswith(wildcard_prefix) &&
attribute_name.endswith(wildcard_suffix))
{
attributes_to_remove.append(attribute_name);
}
return true;
});
break;
}
}
if (attributes_to_remove.is_empty()) {
break;
}
GeometryComponent &component = geometry_set.get_component_for_write(type);
for (const StringRef attribute_name : attributes_to_remove) {
if (!bke::allow_procedural_attribute_access(attribute_name)) {
continue;
}
GeometryComponent &component = geometry_set.get_component_for_write(type);
if (!component.attributes_for_write()->remove(name)) {
cannot_delete = true;
if (component.attributes_for_write()->remove(attribute_name)) {
removed_attributes.add(attribute_name);
}
else {
failed_attributes.add(attribute_name);
}
}
}
});
if (attribute_exists && !cannot_delete) {
params.used_named_attribute(name, NamedAttributeUsage::Remove);
for (const StringRef attribute_name : removed_attributes) {
params.used_named_attribute(attribute_name, NamedAttributeUsage::Remove);
}
if (!attribute_exists) {
const std::string message = fmt::format(TIP_("Attribute does not exist: \"{}\""), name);
if (!failed_attributes.is_empty()) {
Vector<std::string> quoted_attribute_names;
for (const StringRef attribute_name : failed_attributes) {
quoted_attribute_names.append(fmt::format("\"{}\"", attribute_name));
}
const std::string message = fmt::format(TIP_("Cannot remove built-in attributes: {}"),
fmt::join(quoted_attribute_names, ", "));
params.error_message_add(NodeWarningType::Warning, message);
}
if (cannot_delete) {
const std::string message = fmt::format(TIP_("Cannot delete built-in attribute: \"{}\""),
name);
else if (removed_attributes.is_empty() && pattern_mode == PatternMode::Exact) {
const std::string message = fmt::format(TIP_("Attribute does not exist: \"{}\""), pattern);
params.error_message_add(NodeWarningType::Warning, message);
}
params.set_output("Geometry", std::move(geometry_set));
}
static void node_rna(StructRNA *srna)
{
static const EnumPropertyItem pattern_mode_items[] = {
{int(PatternMode::Exact),
"EXACT",
0,
"Exact",
"Remove the one attribute with the given name"},
{int(PatternMode::Wildcard),
"WILDCARD",
0,
"Wildcard",
"Remove all attributes that match the pattern which is allowed to contain a single "
"wildcard (*)."},
{0, nullptr, 0, nullptr, nullptr},
};
RNA_def_node_enum(srna,
"pattern_mode",
"Pattern Mode",
"How the attributes to remove are chosen",
pattern_mode_items,
NOD_inline_enum_accessors(custom1));
}
static void node_register()
{
static bNodeType ntype;
@@ -81,9 +174,12 @@ static void node_register()
geo_node_type_base(
&ntype, GEO_NODE_REMOVE_ATTRIBUTE, "Remove Named Attribute", NODE_CLASS_ATTRIBUTE);
ntype.declare = node_declare;
ntype.draw_buttons = node_layout;
bke::node_type_size(&ntype, 170, 100, 700);
ntype.geometry_node_execute = node_geo_exec;
nodeRegisterType(&ntype);
node_rna(ntype.rna_ext.srna);
}
NOD_REGISTER_NODE(node_register)