From 5ad49f41429177d9ec0572f97bb9575fec34f152 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20T=C3=B6nne?= Date: Fri, 26 Jan 2024 12:40:01 +0100 Subject: [PATCH] Geometry Nodes: Menu Switch Node This patch adds support for _Menu Switch_ nodes and enum definitions in node trees more generally. The design is based on the outcome of the [2022 Nodes Workshop](https://code.blender.org/2022/11/geometry-nodes-workshop-2022/#menu-switch). The _Menu Switch_ node is an advanced version of the _Switch_ node which has a customizable **menu input socket** instead of a simple boolean. The _items_ of this menu are owned by the node itself. Each item has a name and description and unique identifier that is used internally. A menu _socket_ represents a concrete value out of the list of items. To enable selection of an enum value for unconnected sockets the menu is presented as a dropdown list like built-in enums. When the socket is connected a shared pointer to the enum definition is propagated along links and stored in socket default values. This allows node groups to expose a menu from an internal menu switch as a parameter. The enum definition is a runtime copy of the enum items in DNA that allows sharing. A menu socket can have multiple connections, which can lead to ambiguity. If two or more different menu source nodes are connected to a socket it gets marked as _undefined_. Any connection to an undefined menu socket is invalid as a hint to users that there is a problem. A warning/error is also shown on nodes with undefined menu sockets. At runtime the value of a menu socket is the simple integer identifier. This can also be a field in geometry nodes. The identifier is unique within each enum definition, and it is persistent even when items are added, removed, or changed. Changing the name of an item does not affect the internal identifier, so users can rename enum items without breaking existing input values. This also persists if, for example, a linked node group is temporarily unavailable. Pull Request: https://projects.blender.org/blender/blender/pulls/113445 --- scripts/startup/bl_operators/node.py | 58 +++ .../startup/bl_ui/node_add_menu_geometry.py | 1 + scripts/startup/bl_ui/space_node.py | 56 +++ source/blender/blenkernel/BKE_node.h | 1 + source/blender/blenkernel/BKE_node_enum.hh | 49 ++ .../blenkernel/BKE_node_tree_interface.hh | 4 + source/blender/blenkernel/CMakeLists.txt | 2 + source/blender/blenkernel/intern/idprop.cc | 1 + source/blender/blenkernel/intern/node.cc | 65 +++ .../blenkernel/intern/node_enum_definition.cc | 149 ++++++ .../blenkernel/intern/node_tree_interface.cc | 33 ++ .../blenkernel/intern/node_tree_update.cc | 242 +++++++++- source/blender/editors/space_node/drawnode.cc | 28 ++ .../editors/space_node/node_relationships.cc | 4 +- .../editors/space_node/node_templates.cc | 6 + source/blender/makesdna/DNA_array_utils.hh | 1 + source/blender/makesdna/DNA_node_types.h | 62 ++- source/blender/makesrna/RNA_enum_types.hh | 6 + .../makesrna/intern/rna_node_socket.cc | 105 +++++ .../intern/rna_node_tree_interface.cc | 19 + .../blender/makesrna/intern/rna_nodetree.cc | 238 ++++++++++ .../nodes/NOD_geometry_nodes_lazy_function.hh | 3 + .../blender/nodes/NOD_socket_declarations.hh | 38 ++ source/blender/nodes/NOD_static_types.h | 1 + source/blender/nodes/geometry/CMakeLists.txt | 1 + .../nodes/geometry/node_geometry_tree.cc | 3 +- .../geometry/nodes/node_geo_index_switch.cc | 3 +- .../geometry/nodes/node_geo_menu_switch.cc | 435 ++++++++++++++++++ .../nodes/geometry/nodes/node_geo_switch.cc | 3 +- .../nodes/intern/geometry_nodes_execute.cc | 66 +++ .../intern/geometry_nodes_lazy_function.cc | 75 +++ .../nodes/intern/geometry_nodes_log.cc | 26 +- source/blender/nodes/intern/node_common.cc | 7 + .../blender/nodes/intern/node_declaration.cc | 6 + source/blender/nodes/intern/node_socket.cc | 42 +- .../nodes/intern/node_socket_declarations.cc | 47 ++ 36 files changed, 1867 insertions(+), 19 deletions(-) create mode 100644 source/blender/blenkernel/BKE_node_enum.hh create mode 100644 source/blender/blenkernel/intern/node_enum_definition.cc create mode 100644 source/blender/nodes/geometry/nodes/node_geo_menu_switch.cc diff --git a/scripts/startup/bl_operators/node.py b/scripts/startup/bl_operators/node.py index c9d22b55aa4..9a49cc8467e 100644 --- a/scripts/startup/bl_operators/node.py +++ b/scripts/startup/bl_operators/node.py @@ -386,6 +386,61 @@ class NODE_OT_interface_item_remove(NodeInterfaceOperator, Operator): return {'FINISHED'} +class NODE_OT_enum_definition_item_add(Operator): + '''Add an enum item to the definition''' + bl_idname = "node.enum_definition_item_add" + bl_label = "Add Item" + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context): + node = context.active_node + enum_def = node.enum_definition + item = enum_def.enum_items.new("Item") + enum_def.active_index = enum_def.enum_items[:].index(item) + return {'FINISHED'} + + +class NODE_OT_enum_definition_item_remove(Operator): + '''Remove the selected enum item from the definition''' + bl_idname = "node.enum_definition_item_remove" + bl_label = "Remove Item" + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context): + node = context.active_node + enum_def = node.enum_definition + item = enum_def.active_item + if item: + enum_def.enum_items.remove(item) + enum_def.active_index = min(max(enum_def.active_index, 0), len(enum_def.enum_items) - 1) + return {'FINISHED'} + + +class NODE_OT_enum_definition_item_move(Operator): + '''Remove the selected enum item from the definition''' + bl_idname = "node.enum_definition_item_move" + bl_label = "Move Item" + bl_options = {'REGISTER', 'UNDO'} + + direction: EnumProperty( + name="Direction", + description="Move up or down", + items=[("UP", "Up", ""), ("DOWN", "Down", "")] + ) + + def execute(self, context): + node = context.active_node + enum_def = node.enum_definition + index = enum_def.active_index + if self.direction == 'UP': + enum_def.enum_items.move(index, index - 1) + enum_def.active_index = min(max(index - 1, 0), len(enum_def.enum_items) - 1) + else: + enum_def.enum_items.move(index, index + 1) + enum_def.active_index = min(max(index + 1, 0), len(enum_def.enum_items) - 1) + return {'FINISHED'} + + classes = ( NodeSetting, @@ -397,4 +452,7 @@ classes = ( NODE_OT_interface_item_duplicate, NODE_OT_interface_item_remove, NODE_OT_tree_path_parent, + NODE_OT_enum_definition_item_add, + NODE_OT_enum_definition_item_remove, + NODE_OT_enum_definition_item_move, ) diff --git a/scripts/startup/bl_ui/node_add_menu_geometry.py b/scripts/startup/bl_ui/node_add_menu_geometry.py index 4aa1cdb33fc..15fae7534ee 100644 --- a/scripts/startup/bl_ui/node_add_menu_geometry.py +++ b/scripts/startup/bl_ui/node_add_menu_geometry.py @@ -543,6 +543,7 @@ class NODE_MT_category_GEO_UTILITIES(Menu): layout.menu("NODE_MT_category_GEO_UTILITIES_MATH") layout.menu("NODE_MT_category_GEO_UTILITIES_ROTATION") layout.separator() + node_add_menu.add_node_type(layout, "GeometryNodeMenuSwitch") node_add_menu.add_node_type(layout, "FunctionNodeRandomValue") node_add_menu.add_repeat_zone(layout, label="Repeat Zone") node_add_menu.add_node_type(layout, "GeometryNodeSwitch") diff --git a/scripts/startup/bl_ui/space_node.py b/scripts/startup/bl_ui/space_node.py index 25729b384ed..b69e5de6d5b 100644 --- a/scripts/startup/bl_ui/space_node.py +++ b/scripts/startup/bl_ui/space_node.py @@ -1213,6 +1213,60 @@ class NODE_PT_index_switch_node_items(Panel): row.operator("node.index_switch_item_remove", icon='REMOVE', text="").index = i +class NODE_UL_enum_definition_items(bpy.types.UIList): + def draw_item(self, _context, layout, _data, item, icon, _active_data, _active_propname, _index): + layout.prop(item, "name", text="", emboss=False, icon_value=icon) + + +class NODE_PT_menu_switch_items(Panel): + bl_space_type = 'NODE_EDITOR' + bl_region_type = 'UI' + bl_category = "Node" + bl_label = "Menu Switch" + + @classmethod + def poll(cls, context): + snode = context.space_data + if snode is None: + return False + node = context.active_node + if node is None or node.bl_idname != "GeometryNodeMenuSwitch": + return False + return True + + def draw(self, context): + node = context.active_node + layout = self.layout + split = layout.row() + split.template_list( + "NODE_UL_enum_definition_items", + "", + node.enum_definition, + "enum_items", + node.enum_definition, + "active_index") + + ops_col = split.column() + + add_remove_col = ops_col.column(align=True) + add_remove_col.operator("node.enum_definition_item_add", icon='ADD', text="") + add_remove_col.operator("node.enum_definition_item_remove", icon='REMOVE', text="") + + ops_col.separator() + + up_down_col = ops_col.column(align=True) + props = up_down_col.operator("node.enum_definition_item_move", icon='TRIA_UP', text="") + props.direction = 'UP' + props = up_down_col.operator("node.enum_definition_item_move", icon='TRIA_DOWN', text="") + props.direction = 'DOWN' + + active_item = node.enum_definition.active_item + if active_item is not None: + layout.use_property_split = True + layout.use_property_decorate = False + layout.prop(active_item, "description") + + # Grease Pencil properties class NODE_PT_annotation(AnnotationDataPanel, Panel): bl_space_type = 'NODE_EDITOR' @@ -1285,6 +1339,8 @@ classes = ( NODE_PT_bake_node_items, NODE_PT_index_switch_node_items, NODE_PT_repeat_zone_items, + NODE_UL_enum_definition_items, + NODE_PT_menu_switch_items, NODE_PT_active_node_properties, node_panel(EEVEE_MATERIAL_PT_settings), diff --git a/source/blender/blenkernel/BKE_node.h b/source/blender/blenkernel/BKE_node.h index 3b72cf45ad6..1d80d45172c 100644 --- a/source/blender/blenkernel/BKE_node.h +++ b/source/blender/blenkernel/BKE_node.h @@ -1328,6 +1328,7 @@ void BKE_nodetree_remove_layer_n(struct bNodeTree *ntree, struct Scene *scene, i #define GEO_NODE_GET_NAMED_GRID 2121 #define GEO_NODE_STORE_NAMED_GRID 2122 #define GEO_NODE_SORT_ELEMENTS 2123 +#define GEO_NODE_MENU_SWITCH 2124 /** \} */ diff --git a/source/blender/blenkernel/BKE_node_enum.hh b/source/blender/blenkernel/BKE_node_enum.hh new file mode 100644 index 00000000000..a1c3a5df97e --- /dev/null +++ b/source/blender/blenkernel/BKE_node_enum.hh @@ -0,0 +1,49 @@ +/* SPDX-FileCopyrightText: 2023 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +#pragma once + +#include + +#include "BLI_implicit_sharing.hh" +#include "BLI_vector.hh" + +namespace blender::bke { + +/* Flags for #bNodeSocketValueMenu. */ +enum NodeSocketValueMenuRuntimeFlag { + /* Socket has conflicting menu connections and cannot resolve items. */ + NODE_MENU_ITEMS_CONFLICT = (1 << 0), +}; + +/* -------------------------------------------------------------------- */ +/** \name Runtime enum items list. + * \{ */ + +/** + * Runtime copy of #NodeEnumItem for use in #RuntimeNodeEnumItems. + */ +struct RuntimeNodeEnumItem { + std::string name; + std::string description; + /* Immutable unique identifier. */ + int identifier; +}; + +/** + * Shared immutable list of enum items. + * These are owned by a node and can be referenced by node sockets. + */ +struct RuntimeNodeEnumItems : ImplicitSharingMixin { + Vector items; + + void delete_self() override + { + delete this; + } +}; + +/** \} */ + +} // namespace blender::bke diff --git a/source/blender/blenkernel/BKE_node_tree_interface.hh b/source/blender/blenkernel/BKE_node_tree_interface.hh index f8434095d88..c699df7f31a 100644 --- a/source/blender/blenkernel/BKE_node_tree_interface.hh +++ b/source/blender/blenkernel/BKE_node_tree_interface.hh @@ -158,6 +158,7 @@ static const bNodeSocketStaticTypeInfo node_socket_subtypes[] = { {"NodeSocketCollection", "NodeTreeInterfaceSocketCollection", SOCK_COLLECTION, PROP_NONE}, {"NodeSocketTexture", "NodeTreeInterfaceSocketTexture", SOCK_TEXTURE, PROP_NONE}, {"NodeSocketMaterial", "NodeTreeInterfaceSocketMaterial", SOCK_MATERIAL, PROP_NONE}, + {"NodeSocketMenu", "NodeTreeInterfaceSocketMenu", SOCK_MENU, PROP_NONE}, }; template bool socket_data_to_static_type(const eNodeSocketDatatype type, const Fn &fn) @@ -199,6 +200,9 @@ template bool socket_data_to_static_type(const eNodeSocketDatatype case SOCK_MATERIAL: fn.template operator()(); return true; + case SOCK_MENU: + fn.template operator()(); + return true; case SOCK_CUSTOM: case SOCK_SHADER: diff --git a/source/blender/blenkernel/CMakeLists.txt b/source/blender/blenkernel/CMakeLists.txt index 2086d571d02..cd9e6032ea7 100644 --- a/source/blender/blenkernel/CMakeLists.txt +++ b/source/blender/blenkernel/CMakeLists.txt @@ -232,6 +232,7 @@ set(SRC intern/multires_versioning.cc intern/nla.cc intern/node.cc + intern/node_enum_definition.cc intern/node_runtime.cc intern/node_socket_value.cc intern/node_tree_anonymous_attributes.cc @@ -456,6 +457,7 @@ set(SRC BKE_nla.h BKE_node.h BKE_node.hh + BKE_node_enum.hh BKE_node_runtime.hh BKE_node_socket_value.hh BKE_node_tree_anonymous_attributes.hh diff --git a/source/blender/blenkernel/intern/idprop.cc b/source/blender/blenkernel/intern/idprop.cc index 213c4ee87ca..0e5804055c9 100644 --- a/source/blender/blenkernel/intern/idprop.cc +++ b/source/blender/blenkernel/intern/idprop.cc @@ -57,6 +57,7 @@ static size_t idp_size_table[] = { sizeof(double), /* #IDP_DOUBLE */ 0, /* #IDP_IDPARRAY (no fixed size). */ sizeof(int8_t), /* #IDP_BOOLEAN */ + sizeof(int), /* #IDP_ENUM */ }; /* -------------------------------------------------------------------- */ diff --git a/source/blender/blenkernel/intern/node.cc b/source/blender/blenkernel/intern/node.cc index c1b30c59dc4..6d722807fd9 100644 --- a/source/blender/blenkernel/intern/node.cc +++ b/source/blender/blenkernel/intern/node.cc @@ -67,6 +67,7 @@ #include "BKE_lib_query.hh" #include "BKE_main.hh" #include "BKE_node.hh" +#include "BKE_node_enum.hh" #include "BKE_node_runtime.hh" #include "BKE_node_tree_anonymous_attributes.hh" #include "BKE_node_tree_interface.hh" @@ -357,6 +358,7 @@ static void library_foreach_node_socket(LibraryForeachIDData *data, bNodeSocket case SOCK_CUSTOM: case SOCK_SHADER: case SOCK_GEOMETRY: + case SOCK_MENU: break; } } @@ -701,6 +703,10 @@ static void write_node_socket_default_value(BlendWriter *writer, const bNodeSock case SOCK_ROTATION: BLO_write_struct(writer, bNodeSocketValueRotation, sock->default_value); break; + case SOCK_MENU: { + BLO_write_struct(writer, bNodeSocketValueMenu, sock->default_value); + break; + } case SOCK_CUSTOM: /* Custom node sockets where default_value is defined uses custom properties for storage. */ break; @@ -853,6 +859,17 @@ void ntreeBlendWrite(BlendWriter *writer, bNodeTree *ntree) if (node->type == GEO_NODE_BAKE) { blender::nodes::BakeItemsAccessor::blend_write(writer, *node); } + if (node->type == GEO_NODE_MENU_SWITCH) { + const NodeMenuSwitch &storage = *static_cast(node->storage); + BLO_write_struct_array(writer, + NodeEnumItem, + storage.enum_definition.items_num, + storage.enum_definition.items_array); + for (const NodeEnumItem &item : storage.enum_definition.items()) { + BLO_write_string(writer, item.name); + BLO_write_string(writer, item.description); + } + } } LISTBASE_FOREACH (bNodeLink *, link, &ntree->links) { @@ -921,6 +938,7 @@ static bool is_node_socket_supported(const bNodeSocket *sock) case SOCK_TEXTURE: case SOCK_MATERIAL: case SOCK_ROTATION: + case SOCK_MENU: return true; } return false; @@ -937,6 +955,18 @@ static void direct_link_node_socket(BlendDataReader *reader, bNodeSocket *sock) BLO_read_data_address(reader, &sock->default_value); BLO_read_data_address(reader, &sock->default_attribute_name); sock->runtime = MEM_new(__func__); + + switch (eNodeSocketDatatype(sock->type)) { + case SOCK_MENU: { + bNodeSocketValueMenu &default_value = *sock->default_value_typed(); + /* Clear runtime data. */ + default_value.enum_items = nullptr; + default_value.runtime_flag = 0; + break; + } + default: + break; + } } static void direct_link_node_socket_list(BlendDataReader *reader, ListBase *socket_list) @@ -1099,6 +1129,15 @@ void ntreeBlendReadData(BlendDataReader *reader, ID *owner_id, bNodeTree *ntree) blender::nodes::BakeItemsAccessor::blend_read_data(reader, *node); break; } + case GEO_NODE_MENU_SWITCH: { + NodeMenuSwitch &storage = *static_cast(node->storage); + BLO_read_data_address(reader, &storage.enum_definition.items_array); + for (const NodeEnumItem &item : storage.enum_definition.items()) { + BLO_read_data_address(reader, &item.name); + BLO_read_data_address(reader, &item.description); + } + break; + } default: break; @@ -1826,6 +1865,7 @@ static void socket_id_user_increment(bNodeSocket *sock) case SOCK_ROTATION: case SOCK_INT: case SOCK_STRING: + case SOCK_MENU: case SOCK_CUSTOM: case SOCK_SHADER: case SOCK_GEOMETRY: @@ -1872,6 +1912,7 @@ static bool socket_id_user_decrement(bNodeSocket *sock) case SOCK_ROTATION: case SOCK_INT: case SOCK_STRING: + case SOCK_MENU: case SOCK_CUSTOM: case SOCK_SHADER: case SOCK_GEOMETRY: @@ -1931,6 +1972,7 @@ void nodeModifySocketType(bNodeTree *ntree, case SOCK_COLLECTION: case SOCK_TEXTURE: case SOCK_MATERIAL: + case SOCK_MENU: break; } } @@ -2065,6 +2107,8 @@ const char *nodeStaticSocketType(const int type, const int subtype) return "NodeSocketTexture"; case SOCK_MATERIAL: return "NodeSocketMaterial"; + case SOCK_MENU: + return "NodeSocketMenu"; case SOCK_CUSTOM: break; } @@ -2146,6 +2190,8 @@ const char *nodeStaticSocketInterfaceTypeNew(const int type, const int subtype) return "NodeTreeInterfaceSocketTexture"; case SOCK_MATERIAL: return "NodeTreeInterfaceSocketMaterial"; + case SOCK_MENU: + return "NodeTreeInterfaceSocketMenu"; case SOCK_CUSTOM: break; } @@ -2183,6 +2229,8 @@ const char *nodeStaticSocketLabel(const int type, const int /*subtype*/) return "Texture"; case SOCK_MATERIAL: return "Material"; + case SOCK_MENU: + return "Menu"; case SOCK_CUSTOM: break; } @@ -2222,6 +2270,13 @@ static void node_socket_free(bNodeSocket *sock, const bool do_id_user) if (do_id_user) { socket_id_user_decrement(sock); } + if (sock->type == SOCK_MENU) { + auto &default_value_menu = *sock->default_value_typed(); + if (default_value_menu.enum_items) { + /* Release shared data pointer. */ + default_value_menu.enum_items->remove_user_and_delete_if_last(); + } + } MEM_freeN(sock->default_value); } if (sock->default_attribute_name) { @@ -2551,6 +2606,14 @@ static void node_socket_copy(bNodeSocket *sock_dst, const bNodeSocket *sock_src, if ((flag & LIB_ID_CREATE_NO_USER_REFCOUNT) == 0) { socket_id_user_increment(sock_dst); } + + if (sock_src->type == SOCK_MENU) { + auto &default_value_menu = *sock_dst->default_value_typed(); + if (default_value_menu.enum_items) { + /* Copy of shared data pointer. */ + default_value_menu.enum_items->add_user(); + } + } } sock_dst->default_attribute_name = static_cast( @@ -2691,6 +2754,8 @@ static void *socket_value_storage(bNodeSocket &socket) return &socket.default_value_typed()->value; case SOCK_ROTATION: return &socket.default_value_typed()->value_euler; + case SOCK_MENU: + return &socket.default_value_typed()->value; case SOCK_STRING: /* We don't want do this now! */ return nullptr; diff --git a/source/blender/blenkernel/intern/node_enum_definition.cc b/source/blender/blenkernel/intern/node_enum_definition.cc new file mode 100644 index 00000000000..510d70c1d10 --- /dev/null +++ b/source/blender/blenkernel/intern/node_enum_definition.cc @@ -0,0 +1,149 @@ +/* SPDX-FileCopyrightText: 2023 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +#include "BLI_string.h" +#include "BLI_string_utils.hh" + +#include "DNA_array_utils.hh" +#include "DNA_node_types.h" + +#include "BKE_node.h" +#include "BKE_node_enum.hh" +#include "BKE_node_runtime.hh" + +using blender::bke::NodeSocketValueMenuRuntimeFlag; + +bool bNodeSocketValueMenu::has_conflict() const +{ + return this->runtime_flag & NodeSocketValueMenuRuntimeFlag::NODE_MENU_ITEMS_CONFLICT; +} + +blender::Span NodeEnumDefinition::items() const +{ + return {this->items_array, this->items_num}; +} + +blender::MutableSpan NodeEnumDefinition::items_for_write() +{ + return {this->items_array, this->items_num}; +} + +NodeEnumItem *NodeEnumDefinition::add_item(blender::StringRef name) +{ + const int insert_index = this->items_num; + NodeEnumItem *old_items = this->items_array; + + this->items_array = MEM_cnew_array(this->items_num + 1, __func__); + std::copy_n(old_items, insert_index, this->items_array); + NodeEnumItem &new_item = this->items_array[insert_index]; + std::copy_n(old_items + insert_index + 1, + this->items_num - insert_index, + this->items_array + insert_index + 1); + + new_item.identifier = this->next_identifier++; + this->set_item_name(new_item, name); + + this->items_num++; + MEM_SAFE_FREE(old_items); + + return &new_item; +} + +static void free_enum_item(NodeEnumItem *item) +{ + MEM_SAFE_FREE(item->name); + MEM_SAFE_FREE(item->description); +} + +bool NodeEnumDefinition::remove_item(NodeEnumItem &item) +{ + if (!this->items().contains_ptr(&item)) { + return false; + } + const int remove_index = &item - this->items().begin(); + /* DNA fields are 16 bits, can't use directly. */ + int items_num = this->items_num; + int active_index = this->active_index; + blender::dna::array::remove_index( + &this->items_array, &items_num, &active_index, remove_index, free_enum_item); + this->items_num = int16_t(items_num); + this->active_index = int16_t(active_index); + return true; +} + +void NodeEnumDefinition::clear() +{ + /* DNA fields are 16 bits, can't use directly. */ + int items_num = this->items_num; + int active_index = this->active_index; + blender::dna::array::clear(&this->items_array, &items_num, &active_index, free_enum_item); + this->items_num = int16_t(items_num); + this->active_index = int16_t(active_index); +} + +bool NodeEnumDefinition::move_item(uint16_t from_index, uint16_t to_index) +{ + if (to_index < this->items_num) { + int items_num = this->items_num; + int active_index = this->active_index; + blender::dna::array::move_index(this->items_array, items_num, from_index, to_index); + this->items_num = int16_t(items_num); + this->active_index = int16_t(active_index); + } + return true; +} + +const NodeEnumItem *NodeEnumDefinition::active_item() const +{ + if (blender::IndexRange(this->items_num).contains(this->active_index)) { + return &this->items()[this->active_index]; + } + return nullptr; +} + +NodeEnumItem *NodeEnumDefinition::active_item() +{ + if (blender::IndexRange(this->items_num).contains(this->active_index)) { + return &this->items_for_write()[this->active_index]; + } + return nullptr; +} + +void NodeEnumDefinition::active_item_set(NodeEnumItem *item) +{ + this->active_index = this->items().contains_ptr(item) ? item - this->items_array : -1; +} + +void NodeEnumDefinition::set_item_name(NodeEnumItem &item, blender::StringRef name) +{ + char unique_name[MAX_NAME + 4]; + STRNCPY(unique_name, name.data()); + + struct Args { + NodeEnumDefinition *enum_def; + const NodeEnumItem *item; + } args = {this, &item}; + + const char *default_name = items().is_empty() ? "Name" : items().last().name; + BLI_uniquename_cb( + [](void *arg, const char *name) { + const Args &args = *static_cast(arg); + for (const NodeEnumItem &item : args.enum_def->items()) { + if (&item != args.item) { + if (STREQ(item.name, name)) { + return true; + } + } + } + return false; + }, + &args, + default_name, + '.', + unique_name, + ARRAY_SIZE(unique_name)); + + MEM_SAFE_FREE(item.name); + item.name = BLI_strdup(unique_name); +} diff --git a/source/blender/blenkernel/intern/node_tree_interface.cc b/source/blender/blenkernel/intern/node_tree_interface.cc index 05ff47839a2..eca2c2c2f22 100644 --- a/source/blender/blenkernel/intern/node_tree_interface.cc +++ b/source/blender/blenkernel/intern/node_tree_interface.cc @@ -6,6 +6,7 @@ #include "BKE_lib_id.hh" #include "BKE_lib_query.hh" #include "BKE_node.hh" +#include "BKE_node_enum.hh" #include "BKE_node_tree_interface.hh" #include "BLI_math_vector.h" @@ -171,6 +172,12 @@ template<> void socket_data_init_impl(bNodeSocketValueMaterial &data) { data.value = nullptr; } +template<> void socket_data_init_impl(bNodeSocketValueMenu &data) +{ + data.value = -1; + data.enum_items = nullptr; + data.runtime_flag = 0; +} static void *make_socket_data(const StringRef socket_type) { @@ -191,6 +198,13 @@ static void *make_socket_data(const StringRef socket_type) * \{ */ template void socket_data_free_impl(T & /*data*/, const bool /*do_id_user*/) {} +template<> void socket_data_free_impl(bNodeSocketValueMenu &dst, const bool /*do_id_user*/) +{ + if (dst.enum_items) { + /* Release shared data pointer. */ + dst.enum_items->remove_user_and_delete_if_last(); + } +} static void socket_data_free(bNodeTreeInterfaceSocket &socket, const bool do_id_user) { @@ -210,6 +224,14 @@ static void socket_data_free(bNodeTreeInterfaceSocket &socket, const bool do_id_ * \{ */ template void socket_data_copy_impl(T & /*dst*/, const T & /*src*/) {} +template<> +void socket_data_copy_impl(bNodeSocketValueMenu &dst, const bNodeSocketValueMenu & /*src*/) +{ + /* Copy of shared data pointer. */ + if (dst.enum_items) { + dst.enum_items->add_user(); + } +} static void socket_data_copy(bNodeTreeInterfaceSocket &dst, const bNodeTreeInterfaceSocket &src, @@ -304,6 +326,10 @@ inline void socket_data_write_impl(BlendWriter *writer, bNodeSocketValueMaterial { BLO_write_struct(writer, bNodeSocketValueMaterial, &data); } +inline void socket_data_write_impl(BlendWriter *writer, bNodeSocketValueMenu &data) +{ + BLO_write_struct(writer, bNodeSocketValueMenu, &data); +} static void socket_data_write(BlendWriter *writer, bNodeTreeInterfaceSocket &socket) { @@ -323,6 +349,13 @@ template void socket_data_read_data_impl(BlendDataReader *reader, T { BLO_read_data_address(reader, data); } +template<> void socket_data_read_data_impl(BlendDataReader *reader, bNodeSocketValueMenu **data) +{ + BLO_read_data_address(reader, data); + /* Clear runtime data. */ + (*data)->enum_items = nullptr; + (*data)->runtime_flag = 0; +} static void socket_data_read_data(BlendDataReader *reader, bNodeTreeInterfaceSocket &socket) { diff --git a/source/blender/blenkernel/intern/node_tree_update.cc b/source/blender/blenkernel/intern/node_tree_update.cc index 99f13876891..539667b57e5 100644 --- a/source/blender/blenkernel/intern/node_tree_update.cc +++ b/source/blender/blenkernel/intern/node_tree_update.cc @@ -19,6 +19,7 @@ #include "BKE_image.h" #include "BKE_main.hh" #include "BKE_node.hh" +#include "BKE_node_enum.hh" #include "BKE_node_runtime.hh" #include "BKE_node_tree_anonymous_attributes.hh" #include "BKE_node_tree_update.hh" @@ -485,6 +486,9 @@ class NodeTreeMainUpdater { this->propagate_runtime_flags(ntree); if (ntree.type == NTREE_GEOMETRY) { + if (this->propagate_enum_definitions(ntree)) { + result.interface_changed = true; + } if (node_field_inferencing::update_field_inferencing(ntree)) { result.interface_changed = true; } @@ -799,6 +803,209 @@ class NodeTreeMainUpdater { } } + bool propagate_enum_definitions(bNodeTree &ntree) + { + ntree.ensure_interface_cache(); + + /* Propagation from right to left to determine which enum + * definition to use for menu sockets. */ + for (bNode *node : ntree.toposort_right_to_left()) { + const bool node_updated = this->should_update_individual_node(ntree, *node); + + if (node->typeinfo->type == GEO_NODE_MENU_SWITCH) { + /* Generate new enum items when the node has changed, otherwise keep existing items. */ + if (node_updated) { + const NodeMenuSwitch &storage = *static_cast(node->storage); + const RuntimeNodeEnumItems *enum_items = this->create_runtime_enum_items( + storage.enum_definition); + + bNodeSocket &input = *node->input_sockets()[0]; + BLI_assert(input.is_available() && input.type == SOCK_MENU); + this->set_enum_ptr(*input.default_value_typed(), enum_items); + } + continue; + } + else { + /* Clear current enum references. */ + for (bNodeSocket *socket : node->input_sockets()) { + if (socket->is_available() && socket->type == SOCK_MENU) { + clear_enum_reference(*socket); + } + } + for (bNodeSocket *socket : node->output_sockets()) { + if (socket->is_available() && socket->type == SOCK_MENU) { + clear_enum_reference(*socket); + } + } + } + + /* Propagate enum references from output links. */ + for (bNodeSocket *output : node->output_sockets()) { + if (output->is_available() && output->type == SOCK_MENU) { + for (const bNodeSocket *input : output->directly_linked_sockets()) { + this->update_socket_enum_definition( + *output->default_value_typed(), + *input->default_value_typed()); + } + } + } + + if (node->is_group()) { + /* Node groups expose internal enum definitions. */ + if (node->id == nullptr) { + continue; + } + const bNodeTree *group_tree = reinterpret_cast(node->id); + group_tree->ensure_interface_cache(); + + for (const int socket_i : group_tree->interface_inputs().index_range()) { + bNodeSocket &input = *node->input_sockets()[socket_i]; + const bNodeTreeInterfaceSocket &iosocket = *group_tree->interface_inputs()[socket_i]; + BLI_assert(STREQ(input.identifier, iosocket.identifier)); + if (input.is_available() && input.type == SOCK_MENU) { + BLI_assert(STREQ(iosocket.socket_type, "NodeSocketMenu")); + this->update_socket_enum_definition( + *input.default_value_typed(), + *static_cast(iosocket.socket_data)); + } + } + } + else if (node->type == GEO_NODE_MENU_SWITCH) { + /* First input is always the node's own menu, propagate only to the enum case inputs. */ + const bNodeSocket *output = node->output_sockets().first(); + for (bNodeSocket *input : node->input_sockets().drop_front(1)) { + if (input->is_available() && input->type == SOCK_MENU) { + this->update_socket_enum_definition( + *input->default_value_typed(), + *output->default_value_typed()); + } + } + } + else { + /* Propagate over internal relations. */ + /* XXX Placeholder implementation just propagates all outputs + * to all inputs for built-in nodes This could perhaps use + * input/output relations to handle propagation generically? */ + for (bNodeSocket *input : node->input_sockets()) { + if (input->is_available() && input->type == SOCK_MENU) { + for (const bNodeSocket *output : node->output_sockets()) { + if (output->is_available() && output->type == SOCK_MENU) { + this->update_socket_enum_definition( + *input->default_value_typed(), + *output->default_value_typed()); + } + } + } + } + } + } + + /* Build list of new enum items for the node tree interface. */ + Vector interface_enum_items(ntree.interface_inputs().size(), {0}); + for (const bNode *group_input_node : ntree.group_input_nodes()) { + for (const int socket_i : ntree.interface_inputs().index_range()) { + const bNodeSocket &output = *group_input_node->output_sockets()[socket_i]; + + if (output.is_available() && output.type == SOCK_MENU) { + this->update_socket_enum_definition(interface_enum_items[socket_i], + *output.default_value_typed()); + } + } + } + + /* Move enum items to the interface and detect if anything changed. */ + bool changed = false; + for (const int socket_i : ntree.interface_inputs().index_range()) { + bNodeTreeInterfaceSocket &iosocket = *ntree.interface_inputs()[socket_i]; + if (STREQ(iosocket.socket_type, "NodeSocketMenu")) { + bNodeSocketValueMenu &dst = *static_cast(iosocket.socket_data); + const bNodeSocketValueMenu &src = interface_enum_items[socket_i]; + if (dst.enum_items != src.enum_items || dst.has_conflict() != src.has_conflict()) { + changed = true; + if (dst.enum_items) { + dst.enum_items->remove_user_and_delete_if_last(); + } + /* Items are moved, no need to change user count. */ + dst.enum_items = src.enum_items; + SET_FLAG_FROM_TEST(dst.runtime_flag, src.has_conflict(), NODE_MENU_ITEMS_CONFLICT); + } + } + } + + return changed; + } + + /** + * Make a runtime copy of the DNA enum items. + * The runtime items list is shared by sockets. + */ + const RuntimeNodeEnumItems *create_runtime_enum_items(const NodeEnumDefinition &enum_def) + { + RuntimeNodeEnumItems *enum_items = new RuntimeNodeEnumItems(); + enum_items->items.reinitialize(enum_def.items_num); + for (const int i : enum_def.items().index_range()) { + const NodeEnumItem &src = enum_def.items()[i]; + RuntimeNodeEnumItem &dst = enum_items->items[i]; + + dst.identifier = src.identifier; + dst.name = src.name ? src.name : ""; + dst.description = src.description ? src.description : ""; + } + return enum_items; + } + + void clear_enum_reference(bNodeSocket &socket) + { + BLI_assert(socket.is_available() && socket.type == SOCK_MENU); + bNodeSocketValueMenu &default_value = *socket.default_value_typed(); + this->reset_enum_ptr(default_value); + default_value.runtime_flag &= ~NODE_MENU_ITEMS_CONFLICT; + } + + void update_socket_enum_definition(bNodeSocketValueMenu &dst, const bNodeSocketValueMenu &src) + { + if (dst.has_conflict()) { + /* Target enum already has a conflict. */ + BLI_assert(dst.enum_items == nullptr); + return; + } + + if (src.has_conflict()) { + /* Target conflict if any source enum has a conflict. */ + this->reset_enum_ptr(dst); + dst.runtime_flag |= NODE_MENU_ITEMS_CONFLICT; + } + else if (!dst.enum_items) { + /* First connection, set the reference. */ + this->set_enum_ptr(dst, src.enum_items); + } + else if (src.enum_items && dst.enum_items != src.enum_items) { + /* Error if enum ref does not match other connections. */ + this->reset_enum_ptr(dst); + dst.runtime_flag |= NODE_MENU_ITEMS_CONFLICT; + } + } + + void reset_enum_ptr(bNodeSocketValueMenu &dst) + { + if (dst.enum_items) { + dst.enum_items->remove_user_and_delete_if_last(); + dst.enum_items = nullptr; + } + } + + void set_enum_ptr(bNodeSocketValueMenu &dst, const RuntimeNodeEnumItems *enum_items) + { + if (dst.enum_items) { + dst.enum_items->remove_user_and_delete_if_last(); + dst.enum_items = nullptr; + } + if (enum_items) { + enum_items->add_user(); + dst.enum_items = enum_items; + } + } + void update_link_validation(bNodeTree &ntree) { const Span toposort = ntree.toposort_left_to_right(); @@ -810,12 +1017,24 @@ class NodeTreeMainUpdater { toposort_indices[node.index()] = i; } + /* Tests if enum references are undefined. */ + const auto is_invalid_enum_ref = [](const bNodeSocket &socket) -> bool { + if (socket.type == SOCK_MENU) { + return socket.default_value_typed()->enum_items == nullptr; + } + return false; + }; + LISTBASE_FOREACH (bNodeLink *, link, &ntree.links) { link->flag |= NODE_LINK_VALID; if (!link->fromsock->is_available() || !link->tosock->is_available()) { link->flag &= ~NODE_LINK_VALID; continue; } + if (is_invalid_enum_ref(*link->fromsock) || is_invalid_enum_ref(*link->tosock)) { + link->flag &= ~NODE_LINK_VALID; + continue; + } const bNode &from_node = *link->fromnode; const bNode &to_node = *link->tonode; if (toposort_indices[from_node.index()] > toposort_indices[to_node.index()]) { @@ -837,8 +1056,8 @@ class NodeTreeMainUpdater { { tree.ensure_topology_cache(); - /* Compute a hash that represents the node topology connected to the output. This always has to - * be updated even if it is not used to detect changes right now. Otherwise + /* Compute a hash that represents the node topology connected to the output. This always has + * to be updated even if it is not used to detect changes right now. Otherwise * #btree.runtime.output_topology_hash will go out of date. */ const Vector tree_output_sockets = this->find_output_sockets(tree); const uint32_t old_topology_hash = tree.runtime->output_topology_hash; @@ -852,8 +1071,8 @@ class NodeTreeMainUpdater { * be used without causing updates all the time currently. In the future we could try to * handle other drivers better as well. * Note that this optimization only works in practice when the depsgraph didn't also get a - * copy-on-write tag for the node tree (which happens when changing node properties). It does - * work in a few situations like adding reroutes and duplicating nodes though. */ + * copy-on-write tag for the node tree (which happens when changing node properties). It + * does work in a few situations like adding reroutes and duplicating nodes though. */ LISTBASE_FOREACH (const FCurve *, fcurve, &adt->drivers) { const ChannelDriver *driver = fcurve->driver; const StringRef expression = driver->expression; @@ -876,7 +1095,8 @@ class NodeTreeMainUpdater { return true; } - /* The topology hash can only be used when only topology-changing operations have been done. */ + /* The topology hash can only be used when only topology-changing operations have been done. + */ if (tree.runtime->changed_flag == (tree.runtime->changed_flag & (NTREE_CHANGED_LINK | NTREE_CHANGED_REMOVED_NODE))) { @@ -929,15 +1149,15 @@ class NodeTreeMainUpdater { } /** - * Computes a hash that changes when the node tree topology connected to an output node changes. - * Adding reroutes does not have an effect on the hash. + * Computes a hash that changes when the node tree topology connected to an output node + * changes. Adding reroutes does not have an effect on the hash. */ uint32_t get_combined_socket_topology_hash(const bNodeTree &tree, Span sockets) { if (tree.has_available_link_cycle()) { - /* Return dummy value when the link has any cycles. The algorithm below could be improved to - * handle cycles more gracefully. */ + /* Return dummy value when the link has any cycles. The algorithm below could be improved + * to handle cycles more gracefully. */ return 0; } Array hashes = this->get_socket_topology_hashes(tree, sockets); @@ -1121,8 +1341,8 @@ class NodeTreeMainUpdater { } } } - /* The Normal node has a special case, because the value stored in the first output socket - * is used as input in the node. */ + /* The Normal node has a special case, because the value stored in the first output + * socket is used as input in the node. */ if (node.type == SH_NODE_NORMAL && socket.index() == 1) { BLI_assert(STREQ(socket.name, "Dot")); const bNodeSocket &normal_output = node.output_socket(0); diff --git a/source/blender/editors/space_node/drawnode.cc b/source/blender/editors/space_node/drawnode.cc index fd6cfa18c93..35ac7ece4b1 100644 --- a/source/blender/editors/space_node/drawnode.cc +++ b/source/blender/editors/space_node/drawnode.cc @@ -23,6 +23,7 @@ #include "BKE_image.h" #include "BKE_main.hh" #include "BKE_node.hh" +#include "BKE_node_enum.hh" #include "BKE_node_runtime.hh" #include "BKE_node_tree_update.hh" #include "BKE_scene.h" @@ -1199,6 +1200,7 @@ static const float std_node_socket_colors[][4] = { {0.62, 0.31, 0.64, 1.0}, /* SOCK_TEXTURE */ {0.92, 0.46, 0.51, 1.0}, /* SOCK_MATERIAL */ {0.65, 0.39, 0.78, 1.0}, /* SOCK_ROTATION */ + {0.40, 0.40, 0.40, 1.0}, /* SOCK_MENU */ }; /* Callback for colors that does not depend on the socket pointer argument to get the type. */ @@ -1233,6 +1235,7 @@ static const SocketColorFn std_node_socket_color_funcs[] = { std_node_socket_color_fn, std_node_socket_color_fn, std_node_socket_color_fn, + std_node_socket_color_fn, }; /* draw function for file output node sockets, @@ -1371,6 +1374,27 @@ static void std_node_socket_draw( break; } + case SOCK_MENU: { + const bNodeSocketValueMenu *default_value = + sock->default_value_typed(); + if (default_value->enum_items) { + if (default_value->enum_items->items.is_empty()) { + uiLayout *row = uiLayoutSplit(layout, 0.4f, false); + uiItemL(row, text, ICON_NONE); + uiItemL(row, "No Items", ICON_NONE); + } + else { + uiItemR(layout, ptr, "default_value", DEFAULT_FLAGS, "", ICON_NONE); + } + } + else if (default_value->has_conflict()) { + uiItemL(layout, IFACE_("Menu Error"), ICON_ERROR); + } + else { + uiItemL(layout, IFACE_("Menu Undefined"), ICON_QUESTION); + } + break; + } case SOCK_OBJECT: { uiItemR(layout, ptr, "default_value", DEFAULT_FLAGS, text, ICON_NONE); break; @@ -1498,6 +1522,10 @@ static void std_node_socket_interface_draw(ID *id, uiItemR(col, &ptr, "default_value", DEFAULT_FLAGS, IFACE_("Default"), ICON_NONE); break; } + case SOCK_MENU: { + uiItemR(col, &ptr, "default_value", DEFAULT_FLAGS, IFACE_("Default"), ICON_NONE); + break; + } case SOCK_SHADER: case SOCK_GEOMETRY: break; diff --git a/source/blender/editors/space_node/node_relationships.cc b/source/blender/editors/space_node/node_relationships.cc index fba0258bd45..af83bec6b6b 100644 --- a/source/blender/editors/space_node/node_relationships.cc +++ b/source/blender/editors/space_node/node_relationships.cc @@ -434,7 +434,8 @@ static bool socket_can_be_viewed(const bNode &node, const bNodeSocket &socket) SOCK_INT, SOCK_BOOLEAN, SOCK_ROTATION, - SOCK_RGBA); + SOCK_RGBA, + SOCK_MENU); } /** @@ -2245,6 +2246,7 @@ static int get_main_socket_priority(const bNodeSocket *socket) case SOCK_COLLECTION: case SOCK_TEXTURE: case SOCK_MATERIAL: + case SOCK_MENU: return 6; } return -1; diff --git a/source/blender/editors/space_node/node_templates.cc b/source/blender/editors/space_node/node_templates.cc index 2edea184a72..aa7174507d3 100644 --- a/source/blender/editors/space_node/node_templates.cc +++ b/source/blender/editors/space_node/node_templates.cc @@ -394,6 +394,9 @@ static Vector ui_node_link_items(NodeLinkArg *arg, else if (dynamic_cast(&socket_decl)) { item.socket_type = SOCK_STRING; } + else if (dynamic_cast(&socket_decl)) { + item.socket_type = SOCK_MENU; + } else if (dynamic_cast(&socket_decl)) { item.socket_type = SOCK_IMAGE; } @@ -994,6 +997,9 @@ static void ui_node_draw_input(uiLayout &layout, split_wrapper.decorate_column, &inputptr, "default_value", RNA_NO_INDEX); break; } + case SOCK_MENU: + uiItemL(sub, RPT_("Unsupported Menu Socket"), ICON_NONE); + break; case SOCK_CUSTOM: input.typeinfo->draw(&C, sub, &inputptr, &nodeptr, input.name); break; diff --git a/source/blender/makesdna/DNA_array_utils.hh b/source/blender/makesdna/DNA_array_utils.hh index eb89b021aa6..cf4e303195d 100644 --- a/source/blender/makesdna/DNA_array_utils.hh +++ b/source/blender/makesdna/DNA_array_utils.hh @@ -13,6 +13,7 @@ #include "MEM_guardedalloc.h" #include "BLI_index_range.hh" +#include "BLI_utildefines.h" namespace blender::dna::array { diff --git a/source/blender/makesdna/DNA_node_types.h b/source/blender/makesdna/DNA_node_types.h index e4f69e90bdf..1d354653adb 100644 --- a/source/blender/makesdna/DNA_node_types.h +++ b/source/blender/makesdna/DNA_node_types.h @@ -40,17 +40,22 @@ namespace blender::bke { class bNodeTreeZones; class bNodeTreeZone; } // namespace blender::bke +namespace blender::bke { +struct RuntimeNodeEnumItems; +} // namespace blender::bke using NodeDeclarationHandle = blender::nodes::NodeDeclaration; using SocketDeclarationHandle = blender::nodes::SocketDeclaration; using bNodeTreeRuntimeHandle = blender::bke::bNodeTreeRuntime; using bNodeRuntimeHandle = blender::bke::bNodeRuntime; using bNodeSocketRuntimeHandle = blender::bke::bNodeSocketRuntime; +using RuntimeNodeEnumItemsHandle = blender::bke::RuntimeNodeEnumItems; #else typedef struct NodeDeclarationHandle NodeDeclarationHandle; typedef struct SocketDeclarationHandle SocketDeclarationHandle; typedef struct bNodeTreeRuntimeHandle bNodeTreeRuntimeHandle; typedef struct bNodeRuntimeHandle bNodeRuntimeHandle; typedef struct bNodeSocketRuntimeHandle bNodeSocketRuntimeHandle; +typedef struct RuntimeNodeEnumItemsHandle RuntimeNodeEnumItemsHandle; #endif struct AnimData; @@ -69,6 +74,7 @@ struct bNodeLink; struct bNodePreview; struct bNodeType; struct bNode; +struct NodeEnumDefinition; #define NODE_MAXSTR 64 @@ -256,6 +262,7 @@ typedef enum eNodeSocketDatatype { SOCK_TEXTURE = 12, SOCK_MATERIAL = 13, SOCK_ROTATION = 14, + SOCK_MENU = 15, } eNodeSocketDatatype; /** Socket shape. */ @@ -919,6 +926,19 @@ typedef struct bNodeSocketValueMaterial { struct Material *value; } bNodeSocketValueMaterial; +typedef struct bNodeSocketValueMenu { + /* Default input enum identifier. */ + int value; + /* #NodeSocketValueMenuRuntimeFlag */ + int runtime_flag; + /* Immutable runtime enum definition. */ + const RuntimeNodeEnumItemsHandle *enum_items; + +#ifdef __cplusplus + bool has_conflict() const; +#endif +} bNodeSocketValueMenu; + typedef struct GeometryNodeAssetTraits { int flag; } GeometryNodeAssetTraits; @@ -1613,10 +1633,50 @@ typedef struct NodeGeometryMeshLine { } NodeGeometryMeshLine; typedef struct NodeSwitch { - /** #NodeSwitch. */ + /** #eNodeSocketDatatype. */ uint8_t input_type; } NodeSwitch; +typedef struct NodeEnumItem { + char *name; + char *description; + /* Immutable unique identifier. */ + int32_t identifier; + char _pad[4]; +} NodeEnumItem; + +typedef struct NodeEnumDefinition { + /* User-defined enum items owned and managed by this node. */ + NodeEnumItem *items_array; + int16_t items_num; + int16_t active_index; + uint32_t next_identifier; + +#ifdef __cplusplus + blender::Span items() const; + blender::MutableSpan items_for_write(); + + NodeEnumItem *add_item(blender::StringRef name); + bool remove_item(NodeEnumItem &item); + void clear(); + bool move_item(uint16_t from_index, uint16_t to_index); + + const NodeEnumItem *active_item() const; + NodeEnumItem *active_item(); + void active_item_set(NodeEnumItem *item); + + void set_item_name(NodeEnumItem &item, blender::StringRef name); +#endif +} NodeEnumDefinition; + +typedef struct NodeMenuSwitch { + NodeEnumDefinition enum_definition; + + /** #eNodeSocketDatatype. */ + uint8_t data_type; + char _pad[7]; +} NodeMenuSwitch; + typedef struct NodeGeometryCurveSplineType { /** #GeometryNodeSplineType. */ uint8_t spline_type; diff --git a/source/blender/makesrna/RNA_enum_types.hh b/source/blender/makesrna/RNA_enum_types.hh index 4a2236b1425..ee0cd006441 100644 --- a/source/blender/makesrna/RNA_enum_types.hh +++ b/source/blender/makesrna/RNA_enum_types.hh @@ -17,6 +17,9 @@ struct bNodeType; struct PointerRNA; struct PropertyRNA; struct bContext; +namespace blender::bke { +struct RuntimeNodeEnumItems; +} /* Types */ #define DEF_ENUM(id) extern const EnumPropertyItem id[]; @@ -121,3 +124,6 @@ const EnumPropertyItem *RNA_mask_local_itemf(bContext *C, /* Non confirming, utility function. */ const EnumPropertyItem *RNA_enum_node_tree_types_itemf_impl(bContext *C, bool *r_free); + +const EnumPropertyItem *RNA_node_enum_definition_itemf( + const blender::bke::RuntimeNodeEnumItems &enum_items, bool *r_free); diff --git a/source/blender/makesrna/intern/rna_node_socket.cc b/source/blender/makesrna/intern/rna_node_socket.cc index 4ed195d2154..15194b28e96 100644 --- a/source/blender/makesrna/intern/rna_node_socket.cc +++ b/source/blender/makesrna/intern/rna_node_socket.cc @@ -34,6 +34,7 @@ const EnumPropertyItem rna_enum_node_socket_type_items[] = { {SOCK_COLLECTION, "COLLECTION", 0, "Collection", ""}, {SOCK_TEXTURE, "TEXTURE", 0, "Texture", ""}, {SOCK_MATERIAL, "MATERIAL", 0, "Material", ""}, + {SOCK_MENU, "MENU", 0, "Menu", ""}, {0, nullptr, 0, nullptr, nullptr}, }; @@ -42,6 +43,7 @@ const EnumPropertyItem rna_enum_node_socket_type_items[] = { # include "DNA_material_types.h" # include "BKE_node.h" +# include "BKE_node_enum.hh" # include "BKE_node_runtime.hh" # include "BKE_node_tree_update.hh" @@ -414,6 +416,56 @@ bool rna_NodeSocketMaterial_default_value_poll(PointerRNA * /*ptr*/, PointerRNA return ma->gp_style == nullptr; } +const EnumPropertyItem *RNA_node_enum_definition_itemf( + const blender::bke::RuntimeNodeEnumItems &enum_items, bool *r_free) +{ + EnumPropertyItem tmp = {0}; + EnumPropertyItem *result = nullptr; + int totitem = 0; + + for (const blender::bke::RuntimeNodeEnumItem &item : enum_items.items) { + tmp.value = item.identifier; + /* Item name is unique and used as the RNA identitifier as well. + * The integer value is persistent and unique and should be used + * when storing the enum value. */ + tmp.identifier = item.name.c_str(); + /* TODO support icons in enum definition. */ + tmp.icon = ICON_NONE; + tmp.name = item.name.c_str(); + tmp.description = item.description.c_str(); + + RNA_enum_item_add(&result, &totitem, &tmp); + } + + if (totitem == 0) { + *r_free = false; + return rna_enum_dummy_NULL_items; + } + + RNA_enum_item_end(&result, &totitem); + *r_free = true; + + return result; +} + +const EnumPropertyItem *RNA_node_socket_menu_itemf(bContext * /*C*/, + PointerRNA *ptr, + PropertyRNA * /*prop*/, + bool *r_free) +{ + const bNodeSocket *socket = static_cast(ptr->data); + if (!socket) { + *r_free = false; + return rna_enum_dummy_NULL_items; + } + const bNodeSocketValueMenu *data = static_cast(socket->default_value); + if (!data->enum_items) { + *r_free = false; + return rna_enum_dummy_NULL_items; + } + return RNA_node_enum_definition_itemf(*data->enum_items, r_free); +} + #else static void rna_def_node_socket(BlenderRNA *brna) @@ -1129,6 +1181,52 @@ static void rna_def_node_socket_interface_string(BlenderRNA *brna, const char *i rna_def_node_tree_interface_socket_builtin(srna); } +static void rna_def_node_socket_menu(BlenderRNA *brna, const char *identifier) +{ + StructRNA *srna; + PropertyRNA *prop; + + srna = RNA_def_struct(brna, identifier, "NodeSocketStandard"); + RNA_def_struct_ui_text(srna, "Menu Node Socket", "Menu socket of a node"); + RNA_def_struct_sdna(srna, "bNodeSocket"); + + RNA_def_struct_sdna_from(srna, "bNodeSocketValueMenu", "default_value"); + + prop = RNA_def_property(srna, "default_value", PROP_ENUM, PROP_NONE); + RNA_def_property_enum_sdna(prop, nullptr, "value"); + RNA_def_property_enum_items(prop, rna_enum_dummy_NULL_items); + RNA_def_property_enum_funcs(prop, nullptr, nullptr, "RNA_node_socket_menu_itemf"); + RNA_def_property_ui_text(prop, "Default Value", "Input value used for unconnected socket"); + RNA_def_property_update(prop, NC_NODE | NA_EDITED, "rna_NodeSocketStandard_value_update"); + RNA_def_property_flag(prop, PROP_CONTEXT_UPDATE); + + RNA_def_struct_sdna_from(srna, "bNodeSocket", nullptr); +} + +static void rna_def_node_socket_interface_menu(BlenderRNA *brna, const char *identifier) +{ + StructRNA *srna; + PropertyRNA *prop; + + srna = RNA_def_struct(brna, identifier, "NodeTreeInterfaceSocket"); + RNA_def_struct_ui_text(srna, "Menu Node Socket Interface", "Menu socket of a node"); + RNA_def_struct_sdna(srna, "bNodeTreeInterfaceSocket"); + + RNA_def_struct_sdna_from(srna, "bNodeSocketValueMenu", "socket_data"); + + prop = RNA_def_property(srna, "default_value", PROP_ENUM, PROP_NONE); + RNA_def_property_enum_sdna(prop, nullptr, "value"); + RNA_def_property_enum_items(prop, rna_enum_dummy_NULL_items); + RNA_def_property_enum_funcs(prop, nullptr, nullptr, "RNA_node_tree_interface_socket_menu_itemf"); + RNA_def_property_clear_flag(prop, PROP_ANIMATABLE); + RNA_def_property_ui_text(prop, "Default Value", "Input value used for unconnected socket"); + RNA_def_property_update(prop, NC_NODE | NA_EDITED, "rna_NodeTreeInterfaceSocket_value_update"); + + RNA_def_struct_sdna_from(srna, "bNodeTreeInterfaceSocket", nullptr); + + rna_def_node_tree_interface_socket_builtin(srna); +} + static void rna_def_node_socket_shader(BlenderRNA *brna, const char *identifier) { StructRNA *srna; @@ -1458,6 +1556,7 @@ static const bNodeSocketStaticTypeInfo node_socket_subtypes[] = { {"NodeSocketCollection", "NodeTreeInterfaceSocketCollection", SOCK_COLLECTION, PROP_NONE}, {"NodeSocketTexture", "NodeTreeInterfaceSocketTexture", SOCK_TEXTURE, PROP_NONE}, {"NodeSocketMaterial", "NodeTreeInterfaceSocketMaterial", SOCK_MATERIAL, PROP_NONE}, + {"NodeSocketMenu", "NodeTreeInterfaceSocketMenu", SOCK_MENU, PROP_NONE}, }; static void rna_def_node_socket_subtypes(BlenderRNA *brna) @@ -1508,6 +1607,9 @@ static void rna_def_node_socket_subtypes(BlenderRNA *brna) case SOCK_MATERIAL: rna_def_node_socket_material(brna, identifier); break; + case SOCK_MENU: + rna_def_node_socket_menu(brna, identifier); + break; case SOCK_CUSTOM: break; @@ -1547,6 +1649,9 @@ void rna_def_node_socket_interface_subtypes(BlenderRNA *brna) case SOCK_STRING: rna_def_node_socket_interface_string(brna, identifier); break; + case SOCK_MENU: + rna_def_node_socket_interface_menu(brna, identifier); + break; case SOCK_SHADER: rna_def_node_socket_interface_shader(brna, identifier); break; diff --git a/source/blender/makesrna/intern/rna_node_tree_interface.cc b/source/blender/makesrna/intern/rna_node_tree_interface.cc index 4ac8edfe4bc..716636a83f5 100644 --- a/source/blender/makesrna/intern/rna_node_tree_interface.cc +++ b/source/blender/makesrna/intern/rna_node_tree_interface.cc @@ -30,6 +30,7 @@ static const EnumPropertyItem node_tree_interface_socket_in_out_items[] = { # include "BKE_attribute.hh" # include "BKE_node.h" +# include "BKE_node_enum.hh" # include "BKE_node_runtime.hh" # include "BKE_node_tree_interface.hh" # include "BKE_node_tree_update.hh" @@ -883,6 +884,24 @@ static int rna_NodeTreeInterface_items_lookup_string(PointerRNA *ptr, return false; } +const EnumPropertyItem *RNA_node_tree_interface_socket_menu_itemf(bContext * /*C*/, + PointerRNA *ptr, + PropertyRNA * /*prop*/, + bool *r_free) +{ + const bNodeTreeInterfaceSocket *socket = static_cast(ptr->data); + if (!socket) { + *r_free = false; + return rna_enum_dummy_NULL_items; + } + const bNodeSocketValueMenu *data = static_cast(socket->socket_data); + if (!data->enum_items) { + *r_free = false; + return rna_enum_dummy_NULL_items; + } + return RNA_node_enum_definition_itemf(*data->enum_items, r_free); +} + #else static void rna_def_node_interface_item(BlenderRNA *brna) diff --git a/source/blender/makesrna/intern/rna_nodetree.cc b/source/blender/makesrna/intern/rna_nodetree.cc index 6279d6f9235..38dba742d25 100644 --- a/source/blender/makesrna/intern/rna_nodetree.cc +++ b/source/blender/makesrna/intern/rna_nodetree.cc @@ -78,6 +78,7 @@ const EnumPropertyItem rna_enum_node_socket_data_type_items[] = { {SOCK_VECTOR, "VECTOR", 0, "Vector", ""}, {SOCK_ROTATION, "ROTATION", 0, "Rotation", ""}, {SOCK_STRING, "STRING", 0, "String", ""}, + {SOCK_MENU, "MENU", 0, "Menu", ""}, {SOCK_RGBA, "RGBA", 0, "Color", ""}, {SOCK_OBJECT, "OBJECT", 0, "Object", ""}, {SOCK_IMAGE, "IMAGE", 0, "Image", ""}, @@ -3916,6 +3917,133 @@ static const EnumPropertyItem *rna_NodeConvertColorSpace_color_space_itemf(bCont return items; } +static bNode *find_node_by_enum_definition(bNodeTree *ntree, const NodeEnumDefinition *enum_def) +{ + ntree->ensure_topology_cache(); + for (bNode *node : ntree->nodes_by_type("GeometryNodeMenuSwitch")) { + NodeMenuSwitch *storage = static_cast(node->storage); + if (&storage->enum_definition == enum_def) { + return node; + } + } + return nullptr; +} + +static bNode *find_node_by_enum_item(bNodeTree *ntree, const NodeEnumItem *item) +{ + ntree->ensure_topology_cache(); + for (bNode *node : ntree->nodes_by_type("GeometryNodeMenuSwitch")) { + NodeMenuSwitch *storage = static_cast(node->storage); + if (storage->enum_definition.items().contains_ptr(item)) { + return node; + } + } + return nullptr; +} + +static NodeEnumDefinition *find_enum_definition_by_item(bNodeTree *ntree, const NodeEnumItem *item) +{ + ntree->ensure_topology_cache(); + for (bNode *node : ntree->nodes_by_type("GeometryNodeMenuSwitch")) { + NodeMenuSwitch *storage = static_cast(node->storage); + if (storage->enum_definition.items().contains_ptr(item)) { + return &storage->enum_definition; + } + } + return nullptr; +} + +/* Tag the node owning the enum definition to ensure propagation of the enum. */ +static void rna_NodeEnumDefinition_tag_changed(bNodeTree *ntree, NodeEnumDefinition *enum_def) +{ + bNode *node = find_node_by_enum_definition(ntree, enum_def); + BLI_assert(node != nullptr); + BKE_ntree_update_tag_node_property(ntree, node); +} + +static void rna_NodeEnumItem_update(Main *bmain, Scene * /*scene*/, PointerRNA *ptr) +{ + bNodeTree *ntree = reinterpret_cast(ptr->owner_id); + const NodeEnumItem *item = static_cast(ptr->data); + BLI_assert(find_node_by_enum_item(ntree, item) != nullptr); + ED_node_tree_propagate_change(nullptr, bmain, ntree); +} + +static void rna_NodeEnumItem_name_set(PointerRNA *ptr, const char *value) +{ + bNodeTree *ntree = reinterpret_cast(ptr->owner_id); + NodeEnumItem *item = static_cast(ptr->data); + NodeEnumDefinition *enum_def = find_enum_definition_by_item(ntree, item); + enum_def->set_item_name(*item, value); + rna_NodeEnumDefinition_tag_changed(ntree, enum_def); +} + +static NodeEnumItem *rna_NodeEnumDefinition_new( + ID *id, NodeEnumDefinition *enum_def, Main *bmain, ReportList *reports, const char *name) +{ + NodeEnumItem *item = enum_def->add_item(name); + if (item == nullptr) { + BKE_report(reports, RPT_ERROR, "Unable to create enum item"); + } + else { + bNodeTree *ntree = reinterpret_cast(id); + rna_NodeEnumDefinition_tag_changed(ntree, enum_def); + ED_node_tree_propagate_change(nullptr, bmain, ntree); + WM_main_add_notifier(NC_NODE | NA_EDITED, ntree); + } + return item; +} + +static void rna_NodeEnumDefinition_remove( + ID *id, NodeEnumDefinition *enum_def, Main *bmain, ReportList *reports, NodeEnumItem *item) +{ + if (!enum_def->remove_item(*item)) { + BKE_reportf(reports, RPT_ERROR, "Unable to remove item '%s' from enum definition", item->name); + } + else { + bNodeTree *ntree = reinterpret_cast(id); + rna_NodeEnumDefinition_tag_changed(ntree, enum_def); + ED_node_tree_propagate_change(nullptr, bmain, ntree); + WM_main_add_notifier(NC_NODE | NA_EDITED, ntree); + } +} + +static void rna_NodeEnumDefinition_clear(ID *id, NodeEnumDefinition *enum_def, Main *bmain) +{ + enum_def->clear(); + bNodeTree *ntree = reinterpret_cast(id); + rna_NodeEnumDefinition_tag_changed(ntree, enum_def); + ED_node_tree_propagate_change(nullptr, bmain, ntree); + WM_main_add_notifier(NC_NODE | NA_EDITED, ntree); +} + +static void rna_NodeEnumDefinition_move( + ID *id, NodeEnumDefinition *enum_def, Main *bmain, int from_index, int to_index) +{ + enum_def->move_item(from_index, to_index); + bNodeTree *ntree = reinterpret_cast(id); + rna_NodeEnumDefinition_tag_changed(ntree, enum_def); + ED_node_tree_propagate_change(nullptr, bmain, ntree); + WM_main_add_notifier(NC_NODE | NA_EDITED, ntree); +} + +static PointerRNA rna_NodeEnumDefinition_active_item_get(PointerRNA *ptr) +{ + NodeEnumDefinition *enum_def = static_cast(ptr->data); + NodeEnumItem *item = enum_def->active_item(); + PointerRNA r_ptr = RNA_pointer_create(ptr->owner_id, &RNA_NodeEnumItem, item); + return r_ptr; +} + +static void rna_NodeEnumDefinition_active_item_set(PointerRNA *ptr, + PointerRNA value, + ReportList * /*reports*/) +{ + NodeEnumDefinition *enum_def = static_cast(ptr->data); + NodeEnumItem *item = static_cast(value.data); + enum_def->active_item_set(item); +} + #else static const EnumPropertyItem prop_image_layer_items[] = { @@ -9520,6 +9648,113 @@ static void def_geo_string_to_curves(StructRNA *srna) RNA_def_property_update(prop, NC_NODE | NA_EDITED, "rna_Node_update"); } +static void rna_def_node_enum_item(BlenderRNA *brna) +{ + PropertyRNA *prop; + + StructRNA *srna = RNA_def_struct(brna, "NodeEnumItem", nullptr); + RNA_def_struct_ui_text(srna, "Enum Item", ""); + RNA_def_struct_sdna(srna, "NodeEnumItem"); + + prop = RNA_def_property(srna, "name", PROP_STRING, PROP_NONE); + RNA_def_property_string_funcs(prop, nullptr, nullptr, "rna_NodeEnumItem_name_set"); + RNA_def_property_ui_text(prop, "Name", ""); + RNA_def_struct_name_property(srna, prop); + RNA_def_property_update(prop, NC_NODE | NA_EDITED, "rna_NodeEnumItem_update"); + + prop = RNA_def_property(srna, "description", PROP_STRING, PROP_NONE); + RNA_def_property_string_sdna(prop, nullptr, "description"); + RNA_def_property_ui_text(prop, "Description", ""); + RNA_def_property_update(prop, NC_NODE | NA_EDITED, "rna_NodeEnumItem_update"); +} + +static void rna_def_node_enum_definition_items(BlenderRNA *brna) +{ + StructRNA *srna; + PropertyRNA *parm; + FunctionRNA *func; + + srna = RNA_def_struct(brna, "NodeEnumDefinitionItems", nullptr); + RNA_def_struct_sdna(srna, "NodeEnumDefinition"); + RNA_def_struct_ui_text( + srna, "Enum Definition Items", "Collection of items that make up an enum"); + + func = RNA_def_function(srna, "new", "rna_NodeEnumDefinition_new"); + RNA_def_function_ui_description(func, "Add an a new enum item"); + RNA_def_function_flag(func, FUNC_USE_SELF_ID | FUNC_USE_MAIN | FUNC_USE_REPORTS); + parm = RNA_def_string(func, "name", nullptr, MAX_NAME, "Name", ""); + RNA_def_parameter_flags(parm, PropertyFlag(0), PARM_REQUIRED); + /* return value */ + parm = RNA_def_pointer(func, "item", "NodeEnumItem", "Item", "New item"); + RNA_def_function_return(func, parm); + + func = RNA_def_function(srna, "remove", "rna_NodeEnumDefinition_remove"); + RNA_def_function_ui_description(func, "Remove an item from this enum"); + RNA_def_function_flag(func, FUNC_USE_SELF_ID | FUNC_USE_MAIN | FUNC_USE_REPORTS); + parm = RNA_def_pointer(func, "item", "NodeEnumItem", "Item", "The item to remove"); + RNA_def_parameter_flags(parm, PROP_NEVER_NULL, PARM_REQUIRED); + + func = RNA_def_function(srna, "clear", "rna_NodeEnumDefinition_clear"); + RNA_def_function_ui_description(func, "Remove all items from this enum"); + RNA_def_function_flag(func, FUNC_USE_SELF_ID | FUNC_USE_MAIN); + + func = RNA_def_function(srna, "move", "rna_NodeEnumDefinition_move"); + RNA_def_function_ui_description(func, "Move an item to another position"); + RNA_def_function_flag(func, FUNC_USE_SELF_ID | FUNC_USE_MAIN); + parm = RNA_def_int( + func, "from_index", -1, 0, INT_MAX, "From Index", "Index of the item to move", 0, 10000); + RNA_def_parameter_flags(parm, PropertyFlag(0), PARM_REQUIRED); + parm = RNA_def_int( + func, "to_index", -1, 0, INT_MAX, "To Index", "Target index for the item", 0, 10000); + RNA_def_parameter_flags(parm, PropertyFlag(0), PARM_REQUIRED); +} + +static void rna_def_node_enum_definition(BlenderRNA *brna) +{ + StructRNA *srna; + PropertyRNA *prop; + + srna = RNA_def_struct(brna, "NodeEnumDefinition", nullptr); + RNA_def_struct_sdna(srna, "NodeEnumDefinition"); + RNA_def_struct_ui_text(srna, "Enum Definition", "Definition of an enumeration for nodes"); + + prop = RNA_def_property(srna, "enum_items", PROP_COLLECTION, PROP_NONE); + RNA_def_property_collection_sdna(prop, nullptr, "items_array", "items_num"); + RNA_def_property_struct_type(prop, "NodeEnumItem"); + RNA_def_property_ui_text(prop, "Items", ""); + RNA_def_property_srna(prop, "NodeEnumDefinitionItems"); + + prop = RNA_def_property(srna, "active_index", PROP_INT, PROP_UNSIGNED); + RNA_def_property_int_sdna(prop, nullptr, "active_index"); + RNA_def_property_ui_text(prop, "Active Item Index", "Index of the active item"); + RNA_def_property_clear_flag(prop, PROP_ANIMATABLE); + RNA_def_property_update(prop, NC_NODE, nullptr); + + prop = RNA_def_property(srna, "active_item", PROP_POINTER, PROP_NONE); + RNA_def_property_struct_type(prop, "NodeEnumItem"); + RNA_def_property_pointer_funcs(prop, + "rna_NodeEnumDefinition_active_item_get", + "rna_NodeEnumDefinition_active_item_set", + nullptr, + nullptr); + RNA_def_property_flag(prop, PROP_EDITABLE); + RNA_def_property_ui_text(prop, "Active Item", "Active item"); + RNA_def_property_update(prop, NC_NODE, nullptr); +} + +static void def_geo_menu_switch(StructRNA *srna) +{ + PropertyRNA *prop; + + RNA_def_struct_sdna_from(srna, "NodeMenuSwitch", "storage"); + + prop = RNA_def_property(srna, "enum_definition", PROP_POINTER, PROP_NONE); + RNA_def_property_pointer_sdna(prop, nullptr, "enum_definition"); + RNA_def_property_struct_type(prop, "NodeEnumDefinition"); + RNA_def_property_clear_flag(prop, PROP_EDITABLE); + RNA_def_property_ui_text(prop, "Enum Definition", "Definition of enum items"); +} + static void rna_def_shader_node(BlenderRNA *brna) { StructRNA *srna; @@ -10663,6 +10898,9 @@ void RNA_def_nodetree(BlenderRNA *brna) rna_def_geo_repeat_output_items(brna); rna_def_geo_index_switch_items(brna); rna_def_bake_items(brna); + rna_def_node_enum_item(brna); + rna_def_node_enum_definition_items(brna); + rna_def_node_enum_definition(brna); rna_def_node_instance_hash(brna); } diff --git a/source/blender/nodes/NOD_geometry_nodes_lazy_function.hh b/source/blender/nodes/NOD_geometry_nodes_lazy_function.hh index 98999e6c09e..2c259ee7250 100644 --- a/source/blender/nodes/NOD_geometry_nodes_lazy_function.hh +++ b/source/blender/nodes/NOD_geometry_nodes_lazy_function.hh @@ -398,6 +398,9 @@ std::unique_ptr get_index_switch_node_lazy_function( const bNode &node, GeometryNodesLazyFunctionGraphInfo &lf_graph_info); std::unique_ptr get_bake_lazy_function( const bNode &node, GeometryNodesLazyFunctionGraphInfo &own_lf_graph_info); +std::unique_ptr get_menu_switch_node_lazy_function( + const bNode &node, GeometryNodesLazyFunctionGraphInfo &lf_graph_info); +std::unique_ptr get_menu_switch_node_socket_usage_lazy_function(const bNode &node); /** * Outputs the default value of each output socket that has not been output yet. This needs the diff --git a/source/blender/nodes/NOD_socket_declarations.hh b/source/blender/nodes/NOD_socket_declarations.hh index 2ceeded54b8..033584c4fe2 100644 --- a/source/blender/nodes/NOD_socket_declarations.hh +++ b/source/blender/nodes/NOD_socket_declarations.hh @@ -179,6 +179,27 @@ class StringBuilder : public SocketDeclarationBuilder { StringBuilder &default_value(const std::string value); }; +class MenuBuilder; + +class Menu : public SocketDeclaration { + public: + int32_t default_value; + + friend MenuBuilder; + + using Builder = MenuBuilder; + + bNodeSocket &build(bNodeTree &ntree, bNode &node) const override; + bool matches(const bNodeSocket &socket) const override; + bNodeSocket &update_or_build(bNodeTree &ntree, bNode &node, bNodeSocket &socket) const override; + bool can_connect(const bNodeSocket &socket) const override; +}; + +class MenuBuilder : public SocketDeclarationBuilder { + public: + MenuBuilder &default_value(int32_t value); +}; + class IDSocketDeclaration : public SocketDeclaration { public: const char *idname; @@ -488,6 +509,23 @@ inline StringBuilder &StringBuilder::default_value(std::string value) /** \} */ +/* -------------------------------------------------------------------- */ +/** \name #MenuBuilder Inline Methods + * \{ */ + +inline MenuBuilder &MenuBuilder::default_value(const int32_t value) +{ + if (decl_in_) { + decl_in_->default_value = value; + } + if (decl_out_) { + decl_out_->default_value = value; + } + return *this; +} + +/** \} */ + /* -------------------------------------------------------------------- */ /** \name #RotationBuilder Inline Methods * \{ */ diff --git a/source/blender/nodes/NOD_static_types.h b/source/blender/nodes/NOD_static_types.h index 28c934bae7c..cfe80875e4f 100644 --- a/source/blender/nodes/NOD_static_types.h +++ b/source/blender/nodes/NOD_static_types.h @@ -372,6 +372,7 @@ DefNode(GeometryNode, GEO_NODE_INSTANCES_TO_POINTS, 0, "INSTANCES_TO_POINTS",Ins DefNode(GeometryNode, GEO_NODE_IS_VIEWPORT, 0, "IS_VIEWPORT", IsViewport, "Is Viewport", "Retrieve whether the nodes are being evaluated for the viewport rather than the final render") DefNode(GeometryNode, GEO_NODE_JOIN_GEOMETRY, 0, "JOIN_GEOMETRY", JoinGeometry, "Join Geometry", "Merge separately generated geometries into a single one") DefNode(GeometryNode, GEO_NODE_MATERIAL_SELECTION, 0, "MATERIAL_SELECTION", MaterialSelection, "Material Selection", "Provide a selection of faces that use the specified material") +DefNode(GeometryNode, GEO_NODE_MENU_SWITCH, def_geo_menu_switch, "MENU_SWITCH", MenuSwitch, "Menu Switch", "Select from multiple inputs by name") DefNode(GeometryNode, GEO_NODE_MERGE_BY_DISTANCE, 0, "MERGE_BY_DISTANCE", MergeByDistance, "Merge by Distance", "Merge vertices or points within a given distance") DefNode(GeometryNode, GEO_NODE_MESH_BOOLEAN, 0, "MESH_BOOLEAN", MeshBoolean, "Mesh Boolean", "Cut, subtract, or join multiple mesh inputs") DefNode(GeometryNode, GEO_NODE_MESH_FACE_GROUP_BOUNDARIES, 0, "MESH_FACE_SET_BOUNDARIES", MeshFaceSetBoundaries, "Face Group Boundaries", "Find edges on the boundaries between groups of faces with the same ID value") diff --git a/source/blender/nodes/geometry/CMakeLists.txt b/source/blender/nodes/geometry/CMakeLists.txt index 5c588ccdb23..ada7ebf8f0a 100644 --- a/source/blender/nodes/geometry/CMakeLists.txt +++ b/source/blender/nodes/geometry/CMakeLists.txt @@ -122,6 +122,7 @@ set(SRC nodes/node_geo_material_replace.cc nodes/node_geo_material_selection.cc nodes/node_geo_merge_by_distance.cc + nodes/node_geo_menu_switch.cc nodes/node_geo_mesh_face_group_boundaries.cc nodes/node_geo_mesh_primitive_circle.cc nodes/node_geo_mesh_primitive_cone.cc diff --git a/source/blender/nodes/geometry/node_geometry_tree.cc b/source/blender/nodes/geometry/node_geometry_tree.cc index 178ed2fcb4c..f2fbb363c55 100644 --- a/source/blender/nodes/geometry/node_geometry_tree.cc +++ b/source/blender/nodes/geometry/node_geometry_tree.cc @@ -120,7 +120,8 @@ static bool geometry_node_tree_socket_type_valid(bNodeTreeType * /*treetype*/, SOCK_COLLECTION, SOCK_TEXTURE, SOCK_IMAGE, - SOCK_MATERIAL); + SOCK_MATERIAL, + SOCK_MENU); } void register_node_tree_type_geo() diff --git a/source/blender/nodes/geometry/nodes/node_geo_index_switch.cc b/source/blender/nodes/geometry/nodes/node_geo_index_switch.cc index 31e95e511cf..cec273a8708 100644 --- a/source/blender/nodes/geometry/nodes/node_geo_index_switch.cc +++ b/source/blender/nodes/geometry/nodes/node_geo_index_switch.cc @@ -304,7 +304,8 @@ static void node_rna(StructRNA *srna) SOCK_OBJECT, SOCK_COLLECTION, SOCK_MATERIAL, - SOCK_IMAGE); + SOCK_IMAGE, + SOCK_MENU); }); }); } diff --git a/source/blender/nodes/geometry/nodes/node_geo_menu_switch.cc b/source/blender/nodes/geometry/nodes/node_geo_menu_switch.cc new file mode 100644 index 00000000000..02d0b29078a --- /dev/null +++ b/source/blender/nodes/geometry/nodes/node_geo_menu_switch.cc @@ -0,0 +1,435 @@ +/* SPDX-FileCopyrightText: 2023 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +#include + +#include "node_geometry_util.hh" + +#include "DNA_node_types.h" + +#include "BLI_array_utils.hh" +#include "BLI_string.h" + +#include "FN_multi_function.hh" + +#include "UI_interface.hh" +#include "UI_resources.hh" + +#include "NOD_rna_define.hh" +#include "NOD_socket.hh" +#include "NOD_socket_search_link.hh" + +#include "RNA_access.hh" +#include "RNA_enum_types.hh" + +#include "WM_api.hh" + +namespace blender::nodes::node_geo_menu_switch_cc { + +NODE_STORAGE_FUNCS(NodeMenuSwitch) + +static bool is_supported_socket_type(const eNodeSocketDatatype data_type) +{ + return ELEM(data_type, + SOCK_FLOAT, + SOCK_INT, + SOCK_BOOLEAN, + SOCK_ROTATION, + SOCK_VECTOR, + SOCK_STRING, + SOCK_RGBA, + SOCK_GEOMETRY, + SOCK_OBJECT, + SOCK_COLLECTION, + SOCK_MATERIAL, + SOCK_IMAGE); +} + +static void node_declare(blender::nodes::NodeDeclarationBuilder &b) +{ + const bNode *node = b.node_or_null(); + if (node == nullptr) { + return; + } + const NodeMenuSwitch &storage = node_storage(*node); + const eNodeSocketDatatype data_type = eNodeSocketDatatype(storage.data_type); + const bool supports_fields = socket_type_supports_fields(data_type); + + auto &menu = b.add_input("Menu"); + if (supports_fields) { + menu.supports_field(); + } + + for (const NodeEnumItem &enum_item : storage.enum_definition.items()) { + const std::string identifier = "Item_" + std::to_string(enum_item.identifier); + auto &input = b.add_input(data_type, enum_item.name, std::move(identifier)); + if (supports_fields) { + input.supports_field(); + } + /* Labels are ugly in combination with data-block pickers and are usually disabled. */ + input.hide_label(ELEM(data_type, SOCK_OBJECT, SOCK_IMAGE, SOCK_COLLECTION, SOCK_MATERIAL)); + } + + auto &output = b.add_output(data_type, "Output"); + if (supports_fields) { + output.dependent_field().reference_pass_all(); + } + else if (data_type == SOCK_GEOMETRY) { + output.propagate_all(); + } +} + +static void node_layout(uiLayout *layout, bContext * /*C*/, PointerRNA *ptr) +{ + uiItemR(layout, ptr, "data_type", UI_ITEM_NONE, "", ICON_NONE); +} + +static void node_enum_definition_init(NodeEnumDefinition &enum_def) +{ + enum_def.next_identifier = 0; + enum_def.items_array = nullptr; + enum_def.items_num = 0; +} + +static void node_enum_definition_free(NodeEnumDefinition &enum_def) +{ + for (NodeEnumItem &item : enum_def.items_for_write()) { + MEM_SAFE_FREE(item.name); + MEM_SAFE_FREE(item.description); + } + MEM_SAFE_FREE(enum_def.items_array); +} + +static void node_enum_definition_copy(NodeEnumDefinition &dst_enum_def, + const NodeEnumDefinition &src_enum_def) +{ + dst_enum_def.items_array = MEM_cnew_array(src_enum_def.items_num, __func__); + dst_enum_def.items_num = src_enum_def.items_num; + dst_enum_def.active_index = src_enum_def.active_index; + dst_enum_def.next_identifier = src_enum_def.next_identifier; + for (const int i : IndexRange(src_enum_def.items_num)) { + dst_enum_def.items_array[i].identifier = src_enum_def.items_array[i].identifier; + if (char *name = src_enum_def.items_array[i].name) { + dst_enum_def.items_array[i].name = BLI_strdup(name); + } + if (char *desc = src_enum_def.items_array[i].description) { + dst_enum_def.items_array[i].description = BLI_strdup(desc); + } + } +} + +static void node_init(bNodeTree * /*tree*/, bNode *node) +{ + NodeMenuSwitch *data = MEM_cnew(__func__); + node_enum_definition_init(data->enum_definition); + data->data_type = SOCK_GEOMETRY; + node->storage = data; +} + +static void node_free_storage(bNode *node) +{ + NodeMenuSwitch &storage = node_storage(*node); + node_enum_definition_free(storage.enum_definition); + MEM_freeN(node->storage); +} + +static void node_copy_storage(bNodeTree * /*dst_tree*/, bNode *dst_node, const bNode *src_node) +{ + const NodeMenuSwitch &src_storage = node_storage(*src_node); + NodeMenuSwitch *dst_storage = MEM_cnew(__func__); + + node_enum_definition_copy(dst_storage->enum_definition, src_storage.enum_definition); + dst_storage->data_type = src_storage.data_type; + + dst_node->storage = dst_storage; +} + +static void node_update(bNodeTree * /*ntree*/, bNode * /*node*/) {} + +static void node_gather_link_searches(GatherLinkSearchOpParams ¶ms) +{ + if (params.in_out() == SOCK_IN) { + const eNodeSocketDatatype data_type = eNodeSocketDatatype(params.other_socket().type); + if (data_type == SOCK_MENU) { + params.add_item(IFACE_("Menu"), [](LinkSearchOpParams ¶ms) { + bNode &node = params.add_node("GeometryNodeMenuSwitch"); + params.update_and_connect_available_socket(node, "Menu"); + }); + } + } + else { + params.add_item(IFACE_("Output"), [](LinkSearchOpParams ¶ms) { + bNode &node = params.add_node("GeometryNodeMenuSwitch"); + node_storage(node).data_type = params.socket.type; + params.update_and_connect_available_socket(node, "Output"); + }); + } +} + +/** + * Multifunction which evaluates the switch input for each enum item and partially fills the output + * array with values from the input array where the identifier matches. + */ +class MenuSwitchFn : public mf::MultiFunction { + const NodeEnumDefinition &enum_def_; + const CPPType &type_; + mf::Signature signature_; + + public: + MenuSwitchFn(const NodeEnumDefinition &enum_def, const CPPType &type) + : enum_def_(enum_def), type_(type) + { + mf::SignatureBuilder builder{"Menu Switch", signature_}; + builder.single_input("Menu"); + for (const NodeEnumItem &enum_item : enum_def.items()) { + builder.single_input(enum_item.name, type); + } + builder.single_output("Output", type); + + this->set_signature(&signature_); + } + + void call(const IndexMask &mask, mf::Params params, mf::Context /*context*/) const + { + const int value_inputs_start = 1; + const int inputs_num = enum_def_.items_num; + const VArray values = params.readonly_single_input(0, "Menu"); + /* Use one extra mask at the end for invalid indices. */ + const int invalid_index = inputs_num; + + GMutableSpan output = params.uninitialized_single_output( + signature_.params.index_range().last(), "Output"); + + auto find_item_index = [&](const int value) -> int { + for (const int i : enum_def_.items().index_range()) { + const NodeEnumItem &item = enum_def_.items()[i]; + if (item.identifier == value) { + return i; + } + } + return invalid_index; + }; + + if (const std::optional value = values.get_if_single()) { + const int index = find_item_index(*value); + if (index < inputs_num) { + const GVArray inputs = params.readonly_single_input(value_inputs_start + index); + inputs.materialize_to_uninitialized(mask, output.data()); + } + else { + type_.fill_construct_indices(type_.default_value(), output.data(), mask); + } + return; + } + + IndexMaskMemory memory; + Array masks(inputs_num + 1); + IndexMask::from_groups( + mask, memory, [&](const int64_t i) { return find_item_index(values[i]); }, masks); + + for (const int i : IndexRange(inputs_num)) { + if (!masks[i].is_empty()) { + const GVArray inputs = params.readonly_single_input(value_inputs_start + i); + inputs.materialize_to_uninitialized(masks[i], output.data()); + } + } + + type_.fill_construct_indices(type_.default_value(), output.data(), masks[invalid_index]); + } +}; + +class LazyFunctionForMenuSwitchNode : public LazyFunction { + private: + const bNode &node_; + bool can_be_field_ = false; + const NodeEnumDefinition &enum_def_; + const CPPType *cpp_type_; + const CPPType *field_base_type_; + + public: + LazyFunctionForMenuSwitchNode(const bNode &node, + GeometryNodesLazyFunctionGraphInfo &lf_graph_info) + : node_(node), enum_def_(node_storage(node).enum_definition) + { + const NodeMenuSwitch &storage = node_storage(node); + const eNodeSocketDatatype data_type = eNodeSocketDatatype(storage.data_type); + can_be_field_ = socket_type_supports_fields(data_type); + const bNodeSocketType *socket_type = nodeSocketTypeFind( + nodeStaticSocketType(data_type, PROP_NONE)); + BLI_assert(socket_type != nullptr); + cpp_type_ = socket_type->geometry_nodes_cpp_type; + field_base_type_ = socket_type->base_cpp_type; + + MutableSpan lf_index_by_bsocket = lf_graph_info.mapping.lf_index_by_bsocket; + debug_name_ = node.name; + lf_index_by_bsocket[node.input_socket(0).index_in_tree()] = inputs_.append_and_get_index_as( + "Switch", CPPType::get(), lf::ValueUsage::Used); + for (const int i : enum_def_.items().index_range()) { + const NodeEnumItem &enum_item = enum_def_.items()[i]; + lf_index_by_bsocket[node.input_socket(i + 1).index_in_tree()] = + inputs_.append_and_get_index_as(enum_item.name, *cpp_type_, lf::ValueUsage::Maybe); + } + lf_index_by_bsocket[node.output_socket(0).index_in_tree()] = outputs_.append_and_get_index_as( + "Value", *cpp_type_); + } + + void execute_impl(lf::Params ¶ms, const lf::Context & /*context*/) const override + { + SocketValueVariant condition_variant = params.get_input(0); + if (condition_variant.is_context_dependent_field() && can_be_field_) { + this->execute_field(condition_variant.get>(), params); + } + else { + this->execute_single(condition_variant.get(), params); + } + } + + void execute_single(const int condition, lf::Params ¶ms) const + { + for (const int i : IndexRange(enum_def_.items_num)) { + const NodeEnumItem &enum_item = enum_def_.items_array[i]; + const int input_index = i + 1; + if (enum_item.identifier == condition) { + void *value_to_forward = params.try_get_input_data_ptr_or_request(input_index); + if (value_to_forward == nullptr) { + /* Try again when the value is available. */ + return; + } + + void *output_ptr = params.get_output_data_ptr(0); + cpp_type_->move_construct(value_to_forward, output_ptr); + params.output_set(0); + } + else { + params.set_input_unused(input_index); + } + } + /* No guarantee that the switch input matches any enum, + * set default outputs to ensure valid state. */ + set_default_remaining_node_outputs(params, node_); + } + + void execute_field(Field condition, lf::Params ¶ms) const + { + /* When the condition is a non-constant field, we need all inputs. */ + const int values_num = this->enum_def_.items_num; + Array input_values(values_num); + for (const int i : IndexRange(values_num)) { + const int input_index = i + 1; + input_values[i] = params.try_get_input_data_ptr_or_request(input_index); + } + if (input_values.as_span().contains(nullptr)) { + /* Try again when inputs are available. */ + return; + } + + Vector item_fields(enum_def_.items_num + 1); + item_fields[0] = std::move(condition); + for (const int i : IndexRange(enum_def_.items_num)) { + item_fields[i + 1] = input_values[i]->extract(); + } + std::unique_ptr multi_function = std::make_unique( + enum_def_, *field_base_type_); + GField output_field{FieldOperation::Create(std::move(multi_function), std::move(item_fields))}; + + void *output_ptr = params.get_output_data_ptr(0); + new (output_ptr) SocketValueVariant(std::move(output_field)); + params.output_set(0); + } +}; + +/** + * Outputs booleans that indicate which inputs of a menu switch node + * are used. Note that it's possible that multiple inputs are used + * when the condition is a field. + */ +class LazyFunctionForMenuSwitchSocketUsage : public lf::LazyFunction { + const NodeEnumDefinition &enum_def_; + + public: + LazyFunctionForMenuSwitchSocketUsage(const bNode &node) + : enum_def_(node_storage(node).enum_definition) + { + debug_name_ = "Menu Switch Socket Usage"; + inputs_.append_as("Condition", CPPType::get()); + for (const int i : IndexRange(enum_def_.items_num)) { + const NodeEnumItem &enum_item = enum_def_.items()[i]; + outputs_.append_as(enum_item.name, CPPType::get()); + } + } + + void execute_impl(lf::Params ¶ms, const lf::Context & /*context*/) const override + { + const SocketValueVariant &condition_variant = params.get_input(0); + if (condition_variant.is_context_dependent_field()) { + for (const int i : IndexRange(enum_def_.items_num)) { + params.set_output(i, true); + } + } + else { + const int32_t value = condition_variant.get(); + for (const int i : IndexRange(enum_def_.items_num)) { + const NodeEnumItem &enum_item = enum_def_.items()[i]; + params.set_output(i, value == enum_item.identifier); + } + } + } +}; + +static void node_rna(StructRNA *srna) +{ + RNA_def_node_enum( + srna, + "data_type", + "Data Type", + "", + rna_enum_node_socket_data_type_items, + NOD_storage_enum_accessors(data_type), + SOCK_GEOMETRY, + [](bContext * /*C*/, PointerRNA * /*ptr*/, PropertyRNA * /*prop*/, bool *r_free) { + *r_free = true; + return enum_items_filter( + rna_enum_node_socket_data_type_items, [](const EnumPropertyItem &item) -> bool { + return is_supported_socket_type(eNodeSocketDatatype(item.value)); + }); + }); +} + +static void register_node() +{ + static bNodeType ntype; + + geo_node_type_base(&ntype, GEO_NODE_MENU_SWITCH, "Menu Switch", NODE_CLASS_CONVERTER); + ntype.declare = node_declare; + ntype.initfunc = node_init; + ntype.updatefunc = node_update; + node_type_storage(&ntype, "NodeMenuSwitch", node_free_storage, node_copy_storage); + ntype.gather_link_search_ops = node_gather_link_searches; + ntype.draw_buttons = node_layout; + nodeRegisterType(&ntype); + + node_rna(ntype.rna_ext.srna); +} +NOD_REGISTER_NODE(register_node) + +} // namespace blender::nodes::node_geo_menu_switch_cc + +namespace blender::nodes { + +std::unique_ptr get_menu_switch_node_lazy_function( + const bNode &node, GeometryNodesLazyFunctionGraphInfo &lf_graph_info) +{ + using namespace node_geo_menu_switch_cc; + BLI_assert(node.type == GEO_NODE_MENU_SWITCH); + return std::make_unique(node, lf_graph_info); +} + +std::unique_ptr get_menu_switch_node_socket_usage_lazy_function(const bNode &node) +{ + using namespace node_geo_menu_switch_cc; + BLI_assert(node.type == GEO_NODE_MENU_SWITCH); + return std::make_unique(node); +} + +} // namespace blender::nodes diff --git a/source/blender/nodes/geometry/nodes/node_geo_switch.cc b/source/blender/nodes/geometry/nodes/node_geo_switch.cc index c9d23befaba..9fa89f0cc2b 100644 --- a/source/blender/nodes/geometry/nodes/node_geo_switch.cc +++ b/source/blender/nodes/geometry/nodes/node_geo_switch.cc @@ -234,7 +234,8 @@ static void node_rna(StructRNA *srna) SOCK_OBJECT, SOCK_COLLECTION, SOCK_MATERIAL, - SOCK_IMAGE); + SOCK_IMAGE, + SOCK_MENU); }); }); } diff --git a/source/blender/nodes/intern/geometry_nodes_execute.cc b/source/blender/nodes/intern/geometry_nodes_execute.cc index d53dd9150d8..a0a856a7f79 100644 --- a/source/blender/nodes/intern/geometry_nodes_execute.cc +++ b/source/blender/nodes/intern/geometry_nodes_execute.cc @@ -21,12 +21,15 @@ #include "BKE_geometry_fields.hh" #include "BKE_geometry_set.hh" #include "BKE_idprop.hh" +#include "BKE_node_enum.hh" #include "BKE_node_runtime.hh" #include "BKE_node_socket_value.hh" #include "BKE_type_conversions.hh" #include "FN_lazy_function_execute.hh" +#include "UI_resources.hh" + namespace lf = blender::fn::lazy_function; namespace geo_log = blender::nodes::geo_eval_log; @@ -157,12 +160,60 @@ bool socket_type_has_attribute_toggle(const eNodeSocketDatatype type) bool input_has_attribute_toggle(const bNodeTree &node_tree, const int socket_index) { + node_tree.ensure_interface_cache(); + const bNodeSocketType *typeinfo = node_tree.interface_inputs()[socket_index]->socket_typeinfo(); + if (ELEM(typeinfo->type, SOCK_MENU)) { + return false; + } + BLI_assert(node_tree.runtime->field_inferencing_interface); const nodes::FieldInferencingInterface &field_interface = *node_tree.runtime->field_inferencing_interface; return field_interface.inputs[socket_index] != nodes::InputSocketFieldType::None; } +static void id_property_int_update_enum_items(const bNodeSocketValueMenu *value, + IDPropertyUIDataInt *ui_data) +{ + int idprop_items_num = 0; + IDPropertyUIDataEnumItem *idprop_items = nullptr; + + if (value->enum_items) { + const Span items = value->enum_items->items; + idprop_items_num = items.size(); + idprop_items = MEM_cnew_array(items.size(), __func__); + for (const int i : items.index_range()) { + const bke::RuntimeNodeEnumItem &item = items[i]; + IDPropertyUIDataEnumItem &idprop_item = idprop_items[i]; + idprop_item.value = item.identifier; + /* TODO: The name may not be unique! + * We require a unique identifier string for IDProperty and RNA enums, + * so node enums should probably have this too. */ + idprop_item.identifier = BLI_strdup_null(item.name.c_str()); + idprop_item.name = BLI_strdup_null(item.name.c_str()); + idprop_item.description = BLI_strdup_null(item.description.c_str()); + idprop_item.icon = ICON_NONE; + } + } + + /* Fallback: if no items are defined, use a dummy item so the id property is not shown as a plain + * int value. */ + if (idprop_items_num == 0) { + idprop_items_num = 1; + idprop_items = MEM_cnew_array(1, __func__); + idprop_items->value = 0; + idprop_items->identifier = BLI_strdup("DUMMY"); + idprop_items->name = BLI_strdup(""); + idprop_items->description = BLI_strdup(""); + idprop_items->icon = ICON_NONE; + } + + /* Node enum definitions should already be valid. */ + BLI_assert(IDP_EnumItemsValidate(idprop_items, idprop_items_num, nullptr)); + ui_data->enum_items = idprop_items; + ui_data->enum_items_num = idprop_items_num; +} + std::unique_ptr id_property_create_from_socket( const bNodeTreeInterfaceSocket &socket) { @@ -259,6 +310,14 @@ std::unique_ptr id_property_create_f ui_data->default_value = BLI_strdup(value->value); return property; } + case SOCK_MENU: { + const bNodeSocketValueMenu *value = static_cast( + socket.socket_data); + auto property = bke::idprop::create(identifier, value->value); + IDPropertyUIDataInt *ui_data = (IDPropertyUIDataInt *)IDP_ui_data_ensure(property.get()); + id_property_int_update_enum_items(value, ui_data); + return property; + } case SOCK_OBJECT: { const bNodeSocketValueObject *value = static_cast( socket.socket_data); @@ -319,6 +378,8 @@ bool id_property_type_matches_socket(const bNodeTreeInterfaceSocket &socket, return property.type == IDP_BOOLEAN; case SOCK_STRING: return property.type == IDP_STRING; + case SOCK_MENU: + return property.type == IDP_INT; case SOCK_OBJECT: case SOCK_COLLECTION: case SOCK_TEXTURE: @@ -415,6 +476,11 @@ static void init_socket_cpp_value_from_property(const IDProperty &property, new (r_value) bke::SocketValueVariant(std::move(value)); break; } + case SOCK_MENU: { + int value = IDP_Int(&property); + new (r_value) bke::SocketValueVariant(std::move(value)); + break; + } case SOCK_OBJECT: { ID *id = IDP_Id(&property); Object *object = (id && GS(id->name) == ID_OB) ? (Object *)id : nullptr; diff --git a/source/blender/nodes/intern/geometry_nodes_lazy_function.cc b/source/blender/nodes/intern/geometry_nodes_lazy_function.cc index 24c930a8c1d..6ec0f7a07d2 100644 --- a/source/blender/nodes/intern/geometry_nodes_lazy_function.cc +++ b/source/blender/nodes/intern/geometry_nodes_lazy_function.cc @@ -3080,6 +3080,10 @@ struct GeometryNodesLazyFunctionBuilder { this->build_bake_node(bnode, graph_params); break; } + case GEO_NODE_MENU_SWITCH: { + this->build_menu_switch_node(bnode, graph_params); + break; + } default: { if (node_type->geometry_node_execute) { this->build_geometry_node(bnode, graph_params); @@ -3677,6 +3681,77 @@ struct GeometryNodesLazyFunctionBuilder { } } + void build_menu_switch_node(const bNode &bnode, BuildGraphParams &graph_params) + { + std::unique_ptr lazy_function = get_menu_switch_node_lazy_function( + bnode, *lf_graph_info_); + lf::FunctionNode &lf_node = graph_params.lf_graph.add_function(*lazy_function); + scope_.add(std::move(lazy_function)); + + int input_index = 0; + for (const bNodeSocket *bsocket : bnode.input_sockets()) { + if (bsocket->is_available()) { + lf::InputSocket &lf_socket = lf_node.input(input_index); + graph_params.lf_inputs_by_bsocket.add(bsocket, &lf_socket); + mapping_->bsockets_by_lf_socket_map.add(&lf_socket, bsocket); + input_index++; + } + } + for (const bNodeSocket *bsocket : bnode.output_sockets()) { + if (bsocket->is_available()) { + lf::OutputSocket &lf_socket = lf_node.output(0); + graph_params.lf_output_by_bsocket.add(bsocket, &lf_socket); + mapping_->bsockets_by_lf_socket_map.add(&lf_socket, bsocket); + break; + } + } + + this->build_menu_switch_node_socket_usage(bnode, graph_params); + } + + void build_menu_switch_node_socket_usage(const bNode &bnode, BuildGraphParams &graph_params) + { + const NodeMenuSwitch &storage = *static_cast(bnode.storage); + const NodeEnumDefinition &enum_def = storage.enum_definition; + + const bNodeSocket *switch_input_bsocket = bnode.input_sockets()[0]; + Vector input_bsockets(enum_def.items_num); + for (const int i : IndexRange(enum_def.items_num)) { + input_bsockets[i] = bnode.input_sockets()[i + 1]; + } + const bNodeSocket *output_bsocket = bnode.output_sockets()[0]; + + lf::OutputSocket *output_is_used_socket = graph_params.usage_by_bsocket.lookup_default( + output_bsocket, nullptr); + if (output_is_used_socket == nullptr) { + return; + } + graph_params.usage_by_bsocket.add(switch_input_bsocket, output_is_used_socket); + if (switch_input_bsocket->is_directly_linked()) { + /* The condition input is dynamic, so the usage of the other inputs is as well. */ + std::unique_ptr lazy_function = + get_menu_switch_node_socket_usage_lazy_function(bnode); + lf::FunctionNode &lf_node = graph_params.lf_graph.add_function(*lazy_function); + scope_.add(std::move(lazy_function)); + + graph_params.lf_inputs_by_bsocket.add(switch_input_bsocket, &lf_node.input(0)); + for (const int i : IndexRange(enum_def.items_num)) { + graph_params.usage_by_bsocket.add(input_bsockets[i], &lf_node.output(i)); + } + } + else { + const int condition = + switch_input_bsocket->default_value_typed()->value; + for (const int i : IndexRange(enum_def.items_num)) { + const NodeEnumItem &enum_item = enum_def.items()[i]; + if (enum_item.identifier == condition) { + graph_params.usage_by_bsocket.add(input_bsockets[i], output_is_used_socket); + break; + } + } + } + } + void build_undefined_node(const bNode &bnode, BuildGraphParams &graph_params) { auto &lazy_function = scope_.construct( diff --git a/source/blender/nodes/intern/geometry_nodes_log.cc b/source/blender/nodes/intern/geometry_nodes_log.cc index 3fbd68faf6a..25fabb0a00c 100644 --- a/source/blender/nodes/intern/geometry_nodes_log.cc +++ b/source/blender/nodes/intern/geometry_nodes_log.cc @@ -7,6 +7,7 @@ #include "BKE_compute_contexts.hh" #include "BKE_curves.hh" +#include "BKE_node_enum.hh" #include "BKE_node_runtime.hh" #include "BKE_node_socket_value.hh" #include "BKE_viewer_path.hh" @@ -176,6 +177,16 @@ void GeoTreeLogger::log_value(const bNode &node, const bNodeSocket &socket, cons store_logged_value(this->allocator->construct(GMutablePointer{type, buffer})); }; + auto log_menu_value = [&](Span enum_items, const int identifier) { + for (const bke::RuntimeNodeEnumItem &item : enum_items) { + if (item.identifier == identifier) { + log_generic_value(CPPType::get(), &item.name); + return; + } + } + log_generic_value(CPPType::get(), &identifier); + }; + if (type.is()) { const bke::GeometrySet &geometry = *value.get(); store_logged_value(this->allocator->construct(geometry)); @@ -189,7 +200,20 @@ void GeoTreeLogger::log_value(const bNode &node, const bNodeSocket &socket, cons else { value_variant.convert_to_single(); const GPointer value = value_variant.get_single_ptr(); - log_generic_value(*value.type(), value.get()); + if (socket.type == SOCK_MENU) { + const bNodeSocketValueMenu &default_value = + *socket.default_value_typed(); + if (default_value.enum_items) { + const int identifier = *value.get(); + log_menu_value(default_value.enum_items->items, identifier); + } + else { + log_generic_value(*value.type(), value.get()); + } + } + else { + log_generic_value(*value.type(), value.get()); + } } } else { diff --git a/source/blender/nodes/intern/node_common.cc b/source/blender/nodes/intern/node_common.cc index 172e5f854fb..8cb16a2d594 100644 --- a/source/blender/nodes/intern/node_common.cc +++ b/source/blender/nodes/intern/node_common.cc @@ -266,6 +266,13 @@ static SocketDeclarationPtr declaration_for_interface_socket( dst = std::move(decl); break; } + case SOCK_MENU: { + const auto &value = node_interface::get_socket_data_as(io_socket); + std::unique_ptr decl = std::make_unique(); + decl->default_value = value.value; + dst = std::move(decl); + break; + } case SOCK_OBJECT: { auto value = std::make_unique(); value->default_value_fn = get_default_id_getter(ntree.tree_interface, io_socket); diff --git a/source/blender/nodes/intern/node_declaration.cc b/source/blender/nodes/intern/node_declaration.cc index df5d1561484..0d8291cb0b8 100644 --- a/source/blender/nodes/intern/node_declaration.cc +++ b/source/blender/nodes/intern/node_declaration.cc @@ -428,6 +428,8 @@ std::unique_ptr make_declaration_for_socket_type( return std::make_unique(); case SOCK_MATERIAL: return std::make_unique(); + case SOCK_MENU: + return std::make_unique(); default: return {}; } @@ -461,6 +463,8 @@ BaseSocketDeclarationBuilder &NodeDeclarationBuilder::add_input( return this->add_input(name, identifier); case SOCK_MATERIAL: return this->add_input(name, identifier); + case SOCK_MENU: + return this->add_input(name, identifier); default: BLI_assert_unreachable(); return this->add_input("", ""); @@ -502,6 +506,8 @@ BaseSocketDeclarationBuilder &NodeDeclarationBuilder::add_output( return this->add_output(name, identifier); case SOCK_MATERIAL: return this->add_output(name, identifier); + case SOCK_MENU: + return this->add_output(name, identifier); default: BLI_assert_unreachable(); return this->add_output("", ""); diff --git a/source/blender/nodes/intern/node_socket.cc b/source/blender/nodes/intern/node_socket.cc index cad312e2859..9f25624c3fd 100644 --- a/source/blender/nodes/intern/node_socket.cc +++ b/source/blender/nodes/intern/node_socket.cc @@ -548,8 +548,14 @@ void update_node_declaration_and_sockets(bNodeTree &ntree, bNode &node) bool socket_type_supports_fields(const eNodeSocketDatatype socket_type) { - return ELEM( - socket_type, SOCK_FLOAT, SOCK_VECTOR, SOCK_RGBA, SOCK_BOOLEAN, SOCK_INT, SOCK_ROTATION); + return ELEM(socket_type, + SOCK_FLOAT, + SOCK_VECTOR, + SOCK_RGBA, + SOCK_BOOLEAN, + SOCK_INT, + SOCK_ROTATION, + SOCK_MENU); } } // namespace blender::nodes @@ -646,6 +652,13 @@ void node_socket_init_default_value_data(eNodeSocketDatatype datatype, int subty *data = dval; break; } + case SOCK_MENU: { + bNodeSocketValueMenu *dval = MEM_cnew("node socket value menu"); + dval->value = -1; + + *data = dval; + break; + } case SOCK_OBJECT: { bNodeSocketValueObject *dval = MEM_cnew("node socket value object"); dval->value = nullptr; @@ -741,6 +754,12 @@ void node_socket_copy_default_value_data(eNodeSocketDatatype datatype, void *to, *toval = *fromval; break; } + case SOCK_MENU: { + bNodeSocketValueMenu *toval = (bNodeSocketValueMenu *)to; + bNodeSocketValueMenu *fromval = (bNodeSocketValueMenu *)from; + *toval = *fromval; + break; + } case SOCK_OBJECT: { bNodeSocketValueObject *toval = (bNodeSocketValueObject *)to; bNodeSocketValueObject *fromval = (bNodeSocketValueObject *)from; @@ -1045,6 +1064,23 @@ static bNodeSocketType *make_socket_type_string() return socktype; } +static bNodeSocketType *make_socket_type_menu() +{ + bNodeSocketType *socktype = make_standard_socket_type(SOCK_MENU, PROP_NONE); + socktype->base_cpp_type = &blender::CPPType::get(); + socktype->get_base_cpp_value = [](const void *socket_value, void *r_value) { + *(int *)r_value = ((bNodeSocketValueMenu *)socket_value)->value; + }; + socktype->geometry_nodes_cpp_type = &blender::CPPType::get(); + socktype->get_geometry_nodes_cpp_value = [](const void *socket_value, void *r_value) { + const int value = ((bNodeSocketValueMenu *)socket_value)->value; + new (r_value) SocketValueVariant(value); + }; + static SocketValueVariant default_value{0}; + socktype->geometry_nodes_default_cpp_value = &default_value; + return socktype; +} + static bNodeSocketType *make_socket_type_object() { bNodeSocketType *socktype = make_standard_socket_type(SOCK_OBJECT, PROP_NONE); @@ -1150,6 +1186,8 @@ void register_standard_node_socket_types() nodeRegisterSocketType(make_socket_type_string()); + nodeRegisterSocketType(make_socket_type_menu()); + nodeRegisterSocketType(make_standard_socket_type(SOCK_SHADER, PROP_NONE)); nodeRegisterSocketType(make_socket_type_object()); diff --git a/source/blender/nodes/intern/node_socket_declarations.cc b/source/blender/nodes/intern/node_socket_declarations.cc index 10f2b8b474c..8c72b107971 100644 --- a/source/blender/nodes/intern/node_socket_declarations.cc +++ b/source/blender/nodes/intern/node_socket_declarations.cc @@ -481,6 +481,53 @@ bNodeSocket &String::update_or_build(bNodeTree &ntree, bNode &node, bNodeSocket /** \} */ +/* -------------------------------------------------------------------- */ +/** \name #Menu + * \{ */ + +bNodeSocket &Menu::build(bNodeTree &ntree, bNode &node) const +{ + bNodeSocket &socket = *nodeAddStaticSocket(&ntree, + &node, + this->in_out, + SOCK_MENU, + PROP_NONE, + this->identifier.c_str(), + this->name.c_str()); + + ((bNodeSocketValueMenu *)socket.default_value)->value = this->default_value; + this->set_common_flags(socket); + return socket; +} + +bool Menu::matches(const bNodeSocket &socket) const +{ + if (!this->matches_common_data(socket)) { + return false; + } + if (socket.type != SOCK_MENU) { + return false; + } + return true; +} + +bool Menu::can_connect(const bNodeSocket &socket) const +{ + return sockets_can_connect(*this, socket) && socket.type == SOCK_MENU; +} + +bNodeSocket &Menu::update_or_build(bNodeTree &ntree, bNode &node, bNodeSocket &socket) const +{ + if (socket.type != SOCK_MENU) { + BLI_assert(socket.in_out == this->in_out); + return this->build(ntree, node); + } + this->set_common_flags(socket); + return socket; +} + +/** \} */ + /* -------------------------------------------------------------------- */ /** \name #IDSocketDeclaration * \{ */