Fix #158280: Node Tools: Menu inputs broken since recent refactor

Caused by 1561c1ea4a
There were a few problems. First, the enum items generation callback
didn't get the pointer I had expected when writing this. Instead, generate
them when registering the operator. Second, the asset meta-data stored
things in a confusing way, resulting in the Python-accessible enum items
using numbers as the identifiers instead of the names like similar properties
on modifiers. Add a test to keep this all working in the future.
This commit is contained in:
Hans Goudey 2026-05-07 16:54:56 -04:00 • committed by Hans Goudey
parent 8f7d756880
commit b68a2f3921
3 changed files with 101 additions and 37 deletions

View file

@ -10,6 +10,7 @@
#include "MEM_guardedalloc.h"
#include <algorithm>
#include <cstddef>
#include <cstdlib>
#include <cstring>
@ -2231,14 +2232,21 @@ IDProperty *node_create_asset_meta_data_properties(const bNodeTree &node_tree)
}
case SOCK_MENU: {
const auto &value = node_interface::get_socket_data_as<bNodeSocketValueMenu>(*socket);
IDP_AddToGroup(input.get(), idprop::create("default_value", value.value).release());
if (value.enum_items) {
if (std::ranges::any_of(value.enum_items->items, [&](const RuntimeNodeEnumItem &item) {
return item.identifier == value.value;
}))
{
/* Only add the default value property if it's contained in the enum items. */
IDP_AddToGroup(input.get(), idprop::create("default_value", value.value).release());
}
auto items = idprop::create_group("items");
for (const RuntimeNodeEnumItem &enum_item : value.enum_items->items) {
auto item = idprop::create_group(std::to_string(enum_item.identifier));
IDP_AddToGroup(item.get(), idprop::create("name", enum_item.name).release());
auto item = idprop::create_group(enum_item.name);
IDP_AddToGroup(item.get(), idprop::create("value", enum_item.identifier).release());
IDP_AddToGroup(item.get(),
idprop::create("description", enum_item.description).release());
IDP_AddToGroup(items.get(), item.release());
}
IDP_AddToGroup(input.get(), items.release());
}

View file

@ -127,6 +127,7 @@ struct OperatorTypeData : public wmOperatorType::TypeData {
std::unique_ptr<IDProperty, bke::idprop::IDPropertyDeleter> asset_meta_data_properties;
Vector<StructRNA *> generated_structs;
Vector<Array<EnumPropertyItem, 0>> enum_item_storage;
struct LocalRef {
uint32_t session_uid;
@ -1115,35 +1116,31 @@ static bool run_node_group_poll(bContext *C, wmOperatorType *ot)
return true;
}
static const EnumPropertyItem *enum_input_items_fn(bContext * /*C*/,
PointerRNA *ptr,
PropertyRNA *prop,
bool *r_free)
static Array<EnumPropertyItem, 0> get_input_enum_items(const IDProperty &input_idprop)
{
const wmOperator *op = ptr->data_as<wmOperator>();
const OperatorTypeData &type_data = *static_cast<const OperatorTypeData *>(op->customdata);
const IDProperty &inputs_props = *IDP_GetPropertyFromGroup(
type_data.asset_meta_data_properties.get(), "inputs");
const IDProperty &input_idprop = *IDP_GetPropertyFromGroup(&inputs_props,
RNA_property_identifier(prop));
const IDProperty *items_idprop = IDP_GetPropertyFromGroup(&input_idprop, "items");
if (!items_idprop || items_idprop->type != IDP_GROUP) {
return rna_enum_dummy_NULL_items;
return {rna_enum_dummy_NULL_items[0]};
}
int totitem = 0;
EnumPropertyItem *items = nullptr;
for (IDProperty &item_idprop : items_idprop->data.group) {
EnumPropertyItem item;
item.identifier = item_idprop.name;
item.name = IDP_group_lookup_string(item_idprop, "name").value_or("").c_str();
item.description = IDP_group_lookup_string(item_idprop, "description").value_or("").c_str();
item.value = std::stoi(item_idprop.name);
RNA_enum_item_add(&items, &totitem, &item);
if (!items_idprop->data.children_map || items_idprop->data.children_map->children.is_empty()) {
return {rna_enum_dummy_NULL_items[0]};
}
*r_free = true;
const int items_num = items_idprop->data.children_map->children.size();
Array<EnumPropertyItem, 0> items(items_num + 1);
for (const auto [i, item_idprop] : items_idprop->data.group.enumerate()) {
items[i] = EnumPropertyItem{
.value = IDP_group_lookup_int(item_idprop, "value").value_or(0),
.identifier = item_idprop.name,
.icon = ICON_NONE,
.name = item_idprop.name,
.description = IDP_group_lookup_string(item_idprop, "description").value_or("").c_str(),
};
}
items.last() = {0, nullptr, 0, nullptr, nullptr};
return items;
}
@ -1188,9 +1185,8 @@ static void make_common_value_props(StructRNA &srna)
}
static StructRNA *get_input_socket_struct_rna(IDProperty &input_idprop,
Vector<StructRNA *> &r_generated)
OperatorTypeData &type_data)
{
const StringRefNull identifier = input_idprop.name;
const std::optional<int> type = IDP_group_lookup_int(input_idprop, "type");
if (!type) {
@ -1199,7 +1195,7 @@ static StructRNA *get_input_socket_struct_rna(IDProperty &input_idprop,
StructRNA *srna = RNA_def_struct_ptr(
&RNA_blender_rna_get(), identifier.c_str(), RNA_PropertyGroup);
BLI_assert(!RNA_struct_in_public_namespace(srna));
r_generated.append(srna);
type_data.generated_structs.append(srna);
// RNA_def_struct_path_func_runtime(srna, rna_NodesModifierPropertyInput_path);
const StringRefNull name = IDP_group_lookup_string(input_idprop, "name").value_or(identifier);
const StringRefNull description =
@ -1333,9 +1329,21 @@ static StructRNA *get_input_socket_struct_rna(IDProperty &input_idprop,
break;
}
case SOCK_MENU: {
PropertyRNA *prop = RNA_def_enum(
srna, "value", rna_enum_dummy_NULL_items, 0, name.c_str(), description.c_str());
RNA_def_enum_funcs(prop, enum_input_items_fn);
type_data.enum_item_storage.append_as(get_input_enum_items(input_idprop));
int default_value = IDP_group_lookup_int(input_idprop, "default_value").value_or(0);
if (std::ranges::none_of(
type_data.enum_item_storage.last(),
[&](const EnumPropertyItem &item) { return item.value == default_value; }))
{
/* Default value must be used by one of the enum items. */
default_value = 0;
}
RNA_def_enum(srna,
"value",
type_data.enum_item_storage.last().data(),
IDP_group_lookup_int(input_idprop, "default_value").value_or(0),
name.c_str(),
description.c_str());
make_common_value_props(*srna);
break;
}
@ -1346,13 +1354,12 @@ static StructRNA *get_input_socket_struct_rna(IDProperty &input_idprop,
return srna;
}
static StructRNA *create_inputs_srna(const IDProperty &properties,
Vector<StructRNA *> &r_generated)
static StructRNA *create_inputs_srna(const IDProperty &properties, OperatorTypeData &type_data)
{
StructRNA *srna = RNA_def_struct_ptr(
&RNA_blender_rna_get(), "GeometryNodesInterfaceInputs", RNA_PropertyGroup);
BLI_assert(!RNA_struct_in_public_namespace(srna));
r_generated.append(srna);
type_data.generated_structs.append(srna);
const IDProperty &inputs_props = *IDP_GetPropertyFromGroup(&properties, "inputs");
@ -1360,7 +1367,7 @@ static StructRNA *create_inputs_srna(const IDProperty &properties,
if (input_idprop.type != IDP_GROUP) {
continue;
}
StructRNA *input_srna = get_input_socket_struct_rna(input_idprop, r_generated);
StructRNA *input_srna = get_input_socket_struct_rna(input_idprop, type_data);
if (!input_srna) {
continue;
}
@ -1413,8 +1420,7 @@ static void register_node_tool(wmOperatorType *ot,
ot->flag |= OPTYPE_DEPENDS_ON_CURSOR;
}
StructRNA *inputs_srna = create_inputs_srna(*type_data.asset_meta_data_properties,
type_data.generated_structs);
StructRNA *inputs_srna = create_inputs_srna(*type_data.asset_meta_data_properties, type_data);
RNA_def_pointer_runtime(ot->srna, "inputs", inputs_srna, "Inputs", "Settings for input sockets");
if (StructRNA *panels_srna = create_panels_srna(*type_data.asset_meta_data_properties,
type_data.generated_structs))

View file

@ -115,6 +115,56 @@ class TestNodeTool(unittest.TestCase):
self.assertIn("attr_with_default", attribute_names)
self.assertIn("attr_without_default", attribute_names)
def test_menu_input(self):
from bpy.types import WindowManager
bpy.ops.mesh.primitive_cube_add()
cube = bpy.context.active_object
tree = create_object_mode_mesh_tool_tree(
"TestNodeToolMenu", "geometry.test_node_tool_menu"
)
tree.interface.new_socket("Geometry", in_out='INPUT', socket_type='NodeSocketGeometry')
mode_socket = tree.interface.new_socket(
"Mode", in_out='INPUT', socket_type='NodeSocketMenu')
tree.interface.new_socket("Geometry", in_out='OUTPUT', socket_type='NodeSocketGeometry')
group_input = tree.nodes.new("NodeGroupInput")
group_output = tree.nodes.new("NodeGroupOutput")
menu_switch = tree.nodes.new("GeometryNodeMenuSwitch")
menu_switch.data_type = 'VECTOR'
transform = tree.nodes.new("GeometryNodeTransform")
# The menu switch starts with default items "A" and "B". Rename and add a third so the items become X, Y, Z.
menu_switch.enum_items[0].name = "X"
menu_switch.enum_items[1].name = "Y"
menu_switch.enum_items.new("Z")
menu_switch.inputs["X"].default_value = (10.0, 0.0, 0.0)
menu_switch.inputs["Y"].default_value = (0.0, 10.0, 0.0)
menu_switch.inputs["Z"].default_value = (0.0, 0.0, 10.0)
tree.links.new(group_input.outputs["Geometry"], transform.inputs["Geometry"])
tree.links.new(group_input.outputs["Mode"], menu_switch.inputs["Menu"])
tree.links.new(menu_switch.outputs["Output"], transform.inputs["Translation"])
tree.links.new(transform.outputs["Geometry"], group_output.inputs["Geometry"])
WindowManager.register_node_group_operators()
bpy.ops.geometry.test_node_tool_menu(
'EXEC_DEFAULT',
inputs={mode_socket.identifier: {"value": "Y"}},
)
verts = cube.data.vertices
n = len(verts)
center_x = sum(v.co.x for v in verts) / n
center_y = sum(v.co.y for v in verts) / n
center_z = sum(v.co.z for v in verts) / n
self.assertAlmostEqual(center_x, 0.0, places=4)
self.assertAlmostEqual(center_y, 10.0, places=4)
self.assertAlmostEqual(center_z, 0.0, places=4)
if __name__ == "__main__":
import sys