Geometry Nodes: Menu Switch Node #113445

Merged
Lukas Tönne merged 190 commits from LukasTonne/blender:nodes-menu-switch into main 2024-01-26 12:40:14 +01:00
36 changed files with 1867 additions and 19 deletions

View File

@ -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,
)

View File

@ -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")

View File

@ -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),

View File

@ -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
/** \} */

View File

@ -0,0 +1,49 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include <string>
#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),
};
LukasTonne marked this conversation as resolved Outdated

This file should go in the blender::bke namespace

This file should go in the `blender::bke` namespace

I tried that, problem is we can't reference types in a namespace from DNA ...

I tried that, problem is we can't reference types in a namespace from DNA ...

You can, it's done a fair amount actually. See MeshRuntimeHandle for an example.

You can, it's done a fair amount actually. See `MeshRuntimeHandle` for an example.

Ah forgot about that trick.

Ah forgot about that trick.
/* -------------------------------------------------------------------- */
/** \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 {
LukasTonne marked this conversation as resolved Outdated

No need for blender:: here

No need for `blender::` here
Vector<RuntimeNodeEnumItem> items;
void delete_self() override
{
delete this;
}
};
/** \} */
} // namespace blender::bke

View File

@ -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<typename Fn> bool socket_data_to_static_type(const eNodeSocketDatatype type, const Fn &fn)
@ -199,6 +200,9 @@ template<typename Fn> bool socket_data_to_static_type(const eNodeSocketDatatype
case SOCK_MATERIAL:
fn.template operator()<bNodeSocketValueMaterial>();
return true;
case SOCK_MENU:
fn.template operator()<bNodeSocketValueMenu>();
return true;
case SOCK_CUSTOM:
case SOCK_SHADER:

View File

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

View File

@ -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 */
};
/* -------------------------------------------------------------------- */

View File

@ -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<const NodeMenuSwitch *>(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<bNodeSocketRuntime>(__func__);
switch (eNodeSocketDatatype(sock->type)) {
case SOCK_MENU: {
bNodeSocketValueMenu &default_value = *sock->default_value_typed<bNodeSocketValueMenu>();
/* 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<NodeMenuSwitch *>(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:
LukasTonne marked this conversation as resolved Outdated

Outdated comment, same below.

Outdated comment, same below.
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<bNodeSocketValueMenu>();
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<bNodeSocketValueMenu>();
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<char *>(
@ -2691,6 +2754,8 @@ static void *socket_value_storage(bNodeSocket &socket)
return &socket.default_value_typed<bNodeSocketValueMaterial>()->value;
case SOCK_ROTATION:
return &socket.default_value_typed<bNodeSocketValueRotation>()->value_euler;
case SOCK_MENU:
return &socket.default_value_typed<bNodeSocketValueMenu>()->value;
case SOCK_STRING:
/* We don't want do this now! */
return nullptr;

View File

@ -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<NodeEnumItem> NodeEnumDefinition::items() const
{
return {this->items_array, this->items_num};
}
blender::MutableSpan<NodeEnumItem> 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<NodeEnumItem>(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);
}
LukasTonne marked this conversation as resolved Outdated

Maybe you want to use the utility in DNA_array_utils.hh. Same below.

Maybe you want to use the utility in` DNA_array_utils.hh`. Same below.
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;
LukasTonne marked this conversation as resolved Outdated

Looks like this does not free everything in the items. Can just use the utility in DNA_array_utils.hh.

Looks like this does not free everything in the items. Can just use the utility in `DNA_array_utils.hh`.
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<Args *>(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);
}

View File

@ -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<typename T> 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<typename T> 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<typename T> 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)
{

View File

@ -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) {
LukasTonne marked this conversation as resolved Outdated

Since this is geometry nodes only for now, it can also be moved one line down.

Since this is geometry nodes only for now, it can also be moved one line down.
if (this->propagate_enum_definitions(ntree)) {
result.interface_changed = true;
}
if (node_field_inferencing::update_field_inferencing(ntree)) {
result.interface_changed = true;
LukasTonne marked this conversation as resolved Outdated

I think it's important that this can detect whether the enum definitions on the interface changed (by just comparing the old and new state). This is also done below in a few cases: result.interface_changed = true;.

This is important because it allows more selectively updating parent node groups and the modifier. Setting interface_changed to true will also tag the group nodes that invoke the current group as changed (see add_node_tag(pair.first, pair.second, NTREE_CHANGED_NODE_PROPERTY);).

I think it's important that this can detect whether the enum definitions on the interface changed (by just comparing the old and new state). This is also done below in a few cases: `result.interface_changed = true;`. This is important because it allows more selectively updating parent node groups and the modifier. Setting `interface_changed` to true will also tag the group nodes that invoke the current group as changed (see `add_node_tag(pair.first, pair.second, NTREE_CHANGED_NODE_PROPERTY);`).

I think i need to add another update flag NTREE_CHANGED_ENUM to keep track of which nodes are actually affected by enum changes. RNA would also set this more specifically, instead of the generic NTREE_CHANGED_NODE_PROPERTY. Otherwise it wouldn't be possible to tell genuine enum pointer conflicts apart from pointers that simply haven't been updated yet and get replaced. Other parts of the updater seem to do the same, e.g. BKE_ntree_update_tag_node_internal_link is called both from the API as well as the update_internal_links_in_node propagation function.

I think i need to add another update flag `NTREE_CHANGED_ENUM` to keep track of which nodes are actually affected by enum changes. RNA would also set this more specifically, instead of the generic `NTREE_CHANGED_NODE_PROPERTY`. Otherwise it wouldn't be possible to tell genuine enum pointer conflicts apart from pointers that simply haven't been updated yet and get replaced. Other parts of the updater seem to do the same, e.g. `BKE_ntree_update_tag_node_internal_link` is called both from the API as well as the `update_internal_links_in_node` propagation function.

I didn't need to add another update flag after all. I just make sure the enum items of the menu switch are only replaced when the node changes, and then compare the new node tree interface pointers at the end.

I didn't need to add another update flag after all. I just make sure the enum items of the menu switch are only replaced when the node changes, and then compare the new node tree interface pointers at the end.
}
@ -799,6 +803,209 @@ class NodeTreeMainUpdater {
}
}
bool propagate_enum_definitions(bNodeTree &ntree)
{
ntree.ensure_interface_cache();
LukasTonne marked this conversation as resolved Outdated

Minor note, since we are in a class here, it's possible to order the functions so that the higher level functions are at the top and they call the functions below. That's done in the rest of the update code here.

Minor note, since we are in a `class` here, it's possible to order the functions so that the higher level functions are at the top and they call the functions below. That's done in the rest of the update code here.
/* 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<NodeMenuSwitch *>(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<bNodeSocketValueMenu>(), 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<bNodeSocketValueMenu>(),
*input->default_value_typed<bNodeSocketValueMenu>());
}
}
}
if (node->is_group()) {
/* Node groups expose internal enum definitions. */
if (node->id == nullptr) {
continue;
}
const bNodeTree *group_tree = reinterpret_cast<bNodeTree *>(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));
LukasTonne marked this conversation as resolved Outdated

Please use this-> for method calls in this file.

Please use `this->` for method calls in this file.
if (input.is_available() && input.type == SOCK_MENU) {
BLI_assert(STREQ(iosocket.socket_type, "NodeSocketMenu"));
this->update_socket_enum_definition(
*input.default_value_typed<bNodeSocketValueMenu>(),
*static_cast<bNodeSocketValueMenu *>(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<bNodeSocketValueMenu>(),
*output->default_value_typed<bNodeSocketValueMenu>());
}
}
}
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<bNodeSocketValueMenu>(),
*output->default_value_typed<bNodeSocketValueMenu>());
LukasTonne marked this conversation as resolved Outdated

Shouldn't type check for output socket be here too?

Shouldn't type check for output socket be here too?

Yes, good catch.

Yes, good catch.
}
}
}
}
}
}
/* Build list of new enum items for the node tree interface. */
Vector<bNodeSocketValueMenu> 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<bNodeSocketValueMenu>());
}
}
}
/* 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<bNodeSocketValueMenu *>(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<bNodeSocketValueMenu>();
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<const bNode *> 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<bNodeSocketValueMenu>()->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<const bNodeSocket *> 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<const bNodeSocket *> 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<uint32_t> 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);

View File

@ -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 */
LukasTonne marked this conversation as resolved Outdated

Wrong comment.

Wrong comment.
{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<SOCK_TEXTURE>,
std_node_socket_color_fn<SOCK_MATERIAL>,
std_node_socket_color_fn<SOCK_ROTATION>,
std_node_socket_color_fn<SOCK_MENU>,
};
/* 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<bNodeSocketValueMenu>();
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;

View File

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

View File

@ -394,6 +394,9 @@ static Vector<NodeLinkItem> ui_node_link_items(NodeLinkArg *arg,
else if (dynamic_cast<const decl::String *>(&socket_decl)) {
item.socket_type = SOCK_STRING;
}
else if (dynamic_cast<const decl::Menu *>(&socket_decl)) {
item.socket_type = SOCK_MENU;
}
else if (dynamic_cast<const decl::Image *>(&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;
}

Implement menu dropdown for material views.

Implement menu dropdown for material views.
case SOCK_MENU:
uiItemL(sub, RPT_("Unsupported Menu Socket"), ICON_NONE);
LukasTonne marked this conversation as resolved Outdated

Change to something like RPT_("Unsupported Menu Socket")

Change to something like `RPT_("Unsupported Menu Socket")`
break;
case SOCK_CUSTOM:
input.typeinfo->draw(&C, sub, &inputptr, &nodeptr, input.name);
break;

View File

@ -13,6 +13,7 @@
#include "MEM_guardedalloc.h"
#include "BLI_index_range.hh"
#include "BLI_utildefines.h"
namespace blender::dna::array {

View File

@ -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;
LukasTonne marked this conversation as resolved
Review

This seems like runtime data, since it's found by doing static analysis on the tree. Could it be stored outside of DNA?

This seems like runtime data, since it's found by doing static analysis on the tree. Could it be stored outside of DNA?
Review

Yes this is a runtime flag. Moving the flag into RuntimeNodeEnumItems doesn't work though, because when there is a conflict the enum_items pointer is set to null. The items themselves are fine, the updater just can't decide which of them to assign to the socket. So it's the socket itself that should be marked as "conflicted".

There isn't a good place to put this flag in the generic bNodeSocketRuntime, so rather than adding yet another complicated runtime pointer thingy in bNodeSocketValueMenu just for a flag, i'm going to rename it to runtime_flag and move the flag enum definition into BKE_node_enum.hh.

Yes this is a runtime flag. Moving the flag into `RuntimeNodeEnumItems` doesn't work though, because when there is a conflict the `enum_items` pointer is set to null. The items themselves are fine, the updater just can't decide which of them to assign to the socket. So it's the socket itself that should be marked as "conflicted". There isn't a good place to put this flag in the generic `bNodeSocketRuntime`, so rather than adding yet another complicated runtime pointer thingy in `bNodeSocketValueMenu` just for a flag, i'm going to rename it to `runtime_flag` and move the flag enum definition into `BKE_node_enum.hh`.
#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<NodeEnumItem> items() const;
blender::MutableSpan<NodeEnumItem> 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;

View File

@ -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(
LukasTonne marked this conversation as resolved Outdated

Unused definition

Unused definition
const blender::bke::RuntimeNodeEnumItems &enum_items, bool *r_free);

View File

@ -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<bNodeSocket *>(ptr->data);
if (!socket) {
*r_free = false;
return rna_enum_dummy_NULL_items;
}
const bNodeSocketValueMenu *data = static_cast<bNodeSocketValueMenu *>(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;

View File

@ -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<bNodeTreeInterfaceSocket *>(ptr->data);
if (!socket) {
*r_free = false;
return rna_enum_dummy_NULL_items;
}
const bNodeSocketValueMenu *data = static_cast<bNodeSocketValueMenu *>(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)

View File

@ -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<NodeMenuSwitch *>(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<NodeMenuSwitch *>(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<NodeMenuSwitch *>(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<bNodeTree *>(ptr->owner_id);
const NodeEnumItem *item = static_cast<NodeEnumItem *>(ptr->data);
BLI_assert(find_node_by_enum_item(ntree, item) != nullptr);
LukasTonne marked this conversation as resolved Outdated

Unused variable (in release builds anyway)

Unused variable (in release builds anyway)
ED_node_tree_propagate_change(nullptr, bmain, ntree);
}
static void rna_NodeEnumItem_name_set(PointerRNA *ptr, const char *value)
{
bNodeTree *ntree = reinterpret_cast<bNodeTree *>(ptr->owner_id);
NodeEnumItem *item = static_cast<NodeEnumItem *>(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<bNodeTree *>(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<bNodeTree *>(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<bNodeTree *>(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<bNodeTree *>(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<NodeEnumDefinition *>(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<NodeEnumDefinition *>(ptr->data);
NodeEnumItem *item = static_cast<NodeEnumItem *>(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);
}

View File

@ -398,6 +398,9 @@ std::unique_ptr<LazyFunction> get_index_switch_node_lazy_function(
const bNode &node, GeometryNodesLazyFunctionGraphInfo &lf_graph_info);
std::unique_ptr<LazyFunction> get_bake_lazy_function(
const bNode &node, GeometryNodesLazyFunctionGraphInfo &own_lf_graph_info);
std::unique_ptr<LazyFunction> get_menu_switch_node_lazy_function(
const bNode &node, GeometryNodesLazyFunctionGraphInfo &lf_graph_info);
std::unique_ptr<LazyFunction> 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

View File

@ -179,6 +179,27 @@ class StringBuilder : public SocketDeclarationBuilder<String> {
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<Menu> {
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
* \{ */

View File

@ -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")
LukasTonne marked this conversation as resolved Outdated

Should be moved one line up.

Should be moved one line up.
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")

View File

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

View File

@ -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()

View File

@ -304,7 +304,8 @@ static void node_rna(StructRNA *srna)
SOCK_OBJECT,
SOCK_COLLECTION,
SOCK_MATERIAL,
SOCK_IMAGE);
SOCK_IMAGE,
SOCK_MENU);
});
});
}

View File

@ -0,0 +1,435 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include <algorithm>
#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);
LukasTonne marked this conversation as resolved Outdated

No need to specify the default, it's already the default for the default

No need to specify the default, it's already the default for the default
}
LukasTonne marked this conversation as resolved Outdated

In my simple test, the menu switch didn't work with menu sockets yet (shows red link). If that's too difficult to fix right now, just remove it from the list here.

In my simple test, the menu switch didn't work with menu sockets yet (shows red link). If that's too difficult to fix right now, just remove it from the list here.

Yeah, can be made to work, but i really don't want to spend more time on this PR.

Yeah, can be made to work, but i really don't want to spend more time on this PR.
static void node_declare(blender::nodes::NodeDeclarationBuilder &b)
LukasTonne marked this conversation as resolved Outdated

Seems possible to write this code generically for all types without the switch statement, like how it's done in the index switch node. Same with the outputs.

Seems possible to write this code generically for all types without the switch statement, like how it's done in the index switch node. Same with the outputs.
{
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);
LukasTonne marked this conversation as resolved Outdated

Any particular reason to define this range? Seems unnecessary

Any particular reason to define this range? Seems unnecessary
const bool supports_fields = socket_type_supports_fields(data_type);
auto &menu = b.add_input<decl::Menu>("Menu");
if (supports_fields) {
LukasTonne marked this conversation as resolved
Review

Bit confusing to have a default_value of false. Probably shouldn't have a default value at all.

Bit confusing to have a `default_value` of `false`. Probably shouldn't have a default value at all.
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));
LukasTonne marked this conversation as resolved
Review

Use "Item_", just to be a little bit more consistent with other such lists (e.g. in NOD_zone_socket_items.hh).

Use `"Item_"`, just to be a little bit more consistent with other such lists (e.g. in `NOD_zone_socket_items.hh`).
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)

I don't really see a good reason for not supporting this now. It seems like something that should just work, or is there any complication?

I don't really see a good reason for not supporting this now. It seems like something that should just work, or is there any complication?

For some socket only one enum definition can be exist in context of evaluation, right?

For some socket only one enum definition can be exist in context of evaluation, right?

I still need to implement better right-to-left propagation within the node for this to work. If the menu switch is set to "Menu" socket type the output is a menu. Initially this output is an empty menu. Connecting the output to an existing enum definition (i.e. another menu switch node) should propagate the enum definition to all "case" inputs but not to the condition input (first input).

Right now there is not much flexibility in how nodes propagate from outputs to inputs, it just copies the first menu output items to all the inputs, thereby creating a conflict with the source enum definition of the node itself. This method works fine for simple nodes like reroutes and boolean/index switches. We can implement this on a case-by-case basis for now.

e0a158e5db/source/blender/blenkernel/intern/node_tree_update.cc (L973-L979)

I still need to implement better right-to-left propagation within the node for this to work. If the menu switch is set to "Menu" socket type the output is a menu. Initially this output is an empty menu. Connecting the output to an existing enum definition (i.e. another menu switch node) _should_ propagate the enum definition to all "case" inputs _but not to the condition input_ (first input). Right now there is not much flexibility in how nodes propagate from outputs to inputs, it just copies the first menu output items to all the inputs, thereby creating a conflict with the source enum definition of the node itself. This method works fine for simple nodes like reroutes and boolean/index switches. We can implement this on a case-by-case basis for now. https://projects.blender.org/blender/blender/src/commit/e0a158e5db4564fbf0e03a6e9c100f6ed010783c/source/blender/blenkernel/intern/node_tree_update.cc#L973-L979
{
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<NodeEnumItem>(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<NodeMenuSwitch>(__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<NodeMenuSwitch>(__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 &params)
{
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 &params) {
bNode &node = params.add_node("GeometryNodeMenuSwitch");
params.update_and_connect_available_socket(node, "Menu");
});
}
}
else {
params.add_item(IFACE_("Output"), [](LinkSearchOpParams &params) {
bNode &node = params.add_node("GeometryNodeMenuSwitch");
LukasTonne marked this conversation as resolved Outdated

There's a socket_type_supports_fields function for this already

There's a `socket_type_supports_fields` function for this already
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<int>("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<int> values = params.readonly_single_input<int>(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<int> 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<IndexMask> masks(inputs_num + 1);
IndexMask::from_groups<int64_t>(
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));
LukasTonne marked this conversation as resolved Outdated

socket_type_supports_fields(data_type)?

`socket_type_supports_fields(data_type)`?
BLI_assert(socket_type != nullptr);
cpp_type_ = socket_type->geometry_nodes_cpp_type;
field_base_type_ = socket_type->base_cpp_type;
LukasTonne marked this conversation as resolved Outdated

Broken English here, and I don't really get the limitation currently

Broken English here, and I don't really get the limitation currently
MutableSpan<int> 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<SocketValueVariant>(), lf::ValueUsage::Used);
for (const int i : enum_def_.items().index_range()) {
LukasTonne marked this conversation as resolved Outdated

use doxygen syntax

use doxygen syntax
const NodeEnumItem &enum_item = enum_def_.items()[i];
lf_index_by_bsocket[node.input_socket(i + 1).index_in_tree()] =
LukasTonne marked this conversation as resolved Outdated

This class shouldn't need to be templated, it's fairly simple to handle different types generically inside.
Also, I think the implementation could look exactly the same as the index switch node, except with a different callback in the IndexMask::from_groups lambda. Currently it's looping over all items for every enum item, which is a bit bad in the worst case

This class shouldn't need to be templated, it's fairly simple to handle different types generically inside. Also, I think the implementation could look exactly the same as the index switch node, except with a different callback in the `IndexMask::from_groups` lambda. Currently it's looping over all items for every enum item, which is a bit bad in the worst case

Did IndexMask construction change? I implemented this months ago, and IIRC back then constructing a full index mask for each enum case was basically doing the same thing, looping over the full range to construct a mask, only to apply it once and discard.

The difference to the Index Switch case is that the index node only has to construct one mask and can used the index itself to find the right input array to copy from for each output index. The Menu Switch has to do a search for a matching enum identifier for each menu element if we want to only do one loop (some hash table might make that faster but probably comes with some constant cost as well). My current solution avoids this by handling each enum case separately and writing only those output elements that match the current case. That way it only has to check for equality instead of doing a linear search for each element.

Did IndexMask construction change? I implemented this months ago, and IIRC back then constructing a full index mask for each enum case was basically doing the same thing, looping over the full range to construct a mask, only to apply it once and discard. The difference to the `Index Switch` case is that the index node only has to construct one mask and can used the index itself to find the right input array to copy from for each output index. The Menu Switch has to do a search for a matching enum _identifier_ for each menu element if we want to only do one loop (some hash table might make that faster but probably comes with some constant cost as well). My current solution avoids this by handling each enum case separately and writing only those output elements that match the current case. That way it only has to check for equality instead of doing a linear search for each element.

Ok, IndexMask::from_groups looks like it could work here. I originally swapped the inner and outer loops to be able to devirtualize the input arrays one at a time. The implementation in Index Switch seems to just access the input GVArrays by index, which i thought devirtualization was supposed to avoid? There isn't a devirtualization for GVArrays. I'm ok with that, if anyone feels like it they can optimize this later.

Ok, `IndexMask::from_groups` looks like it could work here. I originally swapped the inner and outer loops to be able to devirtualize the input arrays one at a time. The implementation in `Index Switch` seems to just access the input GVArrays by index, which i thought devirtualization was supposed to avoid? There isn't a devirtualization for GVArrays. I'm ok with that, if anyone feels like it they can optimize this later.

Think we can relatively easily optimize IndexMask::from_groups later.

The implementation in Index Switch seems to just access the input GVArrays by index, which i thought devirtualization was supposed to avoid?

The index switch node calls materialize_to_uninitialized for each GVArray-- it doesn't access each element by index itself. Can't get much better than that in terms of devirtualization.

Think we can relatively easily optimize `IndexMask::from_groups` later. >The implementation in Index Switch seems to just access the input GVArrays by index, which i thought devirtualization was supposed to avoid? The index switch node calls `materialize_to_uninitialized` for each `GVArray`-- it doesn't access each element by index itself. Can't get much better than that in terms of devirtualization.
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 &params, const lf::Context & /*context*/) const override
{
SocketValueVariant condition_variant = params.get_input<SocketValueVariant>(0);
if (condition_variant.is_context_dependent_field() && can_be_field_) {
this->execute_field(condition_variant.get<Field<int>>(), params);
}
else {
this->execute_single(condition_variant.get<int>(), params);
}
}
void execute_single(const int condition, lf::Params &params) 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<int> condition, lf::Params &params) const
{
/* When the condition is a non-constant field, we need all inputs. */
const int values_num = this->enum_def_.items_num;
LukasTonne marked this conversation as resolved Outdated

@JacquesLucke There is a failing assert in SocketValueVariant::convert_to_single when using set_default_remaining_node_outputs on field outputs. The SocketValueVariant default value is created with a None kind, which is not supported. This fails during GeoTreeLogger::log_value.

@JacquesLucke There is a failing assert in `SocketValueVariant::convert_to_single` when using `set_default_remaining_node_outputs` on field outputs. The `SocketValueVariant` default value is created with a `None` kind, which is not supported. This fails during `GeoTreeLogger::log_value`.
Array<SocketValueVariant *, 8> 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<SocketValueVariant>(input_index);
LukasTonne marked this conversation as resolved Outdated

Any particular reason to store the multifunction here rather creating locally and passing ownership in FieldOperation::Create?

Any particular reason to store the multifunction here rather creating locally and passing ownership in `FieldOperation::Create`?

Not really. I wasn't sure about the lifetime of the lazy function and whether execute_impl would be called more than once. Constructing during the execute call is fine.

Not really. I wasn't sure about the lifetime of the lazy function and whether `execute_impl` would be called more than once. Constructing during the execute call is fine.
}
if (input_values.as_span().contains(nullptr)) {
/* Try again when inputs are available. */
return;
}
Vector<GField> 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<GField>();
}
std::unique_ptr<MultiFunction> multi_function = std::make_unique<MenuSwitchFn>(
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<SocketValueVariant>());
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<bool>());
}
}
void execute_impl(lf::Params &params, const lf::Context & /*context*/) const override
{
const SocketValueVariant &condition_variant = params.get_input<SocketValueVariant>(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<int>();
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,
LukasTonne marked this conversation as resolved Outdated

Probably more semantically helpful to use set_default_remaining_node_outputs even if the result is the same

Probably more semantically helpful to use `set_default_remaining_node_outputs` even if the result is the same

There is a bug in both the Index Switch and Menu Switch nodes here: If you remove all items the set_default_remaining_node_outputs call will crash, because the lf_index_by_bsocket for the output is never defined (-1). This stuff is hard to follow but i think lazy_function_interface_from_node must be called at some point, according to the generic lazy function types.

There is a bug in both the Index Switch and Menu Switch nodes here: If you remove all items the `set_default_remaining_node_outputs` call will crash, because the `lf_index_by_bsocket` for the output is never defined (-1). This stuff is hard to follow but i think `lazy_function_interface_from_node` must be called at some point, according to the generic lazy function types.

Passing the graph info to the constructor now, same as get_bake_lazy_function, and then insert the socket mapping in the constructor. Would be nice to have some kind of correctness check in debug to avoid having to figure this out every time. Maybe not all nodes need the indices and should check only when accessing the index.

Passing the graph info to the constructor now, same as `get_bake_lazy_function`, and then insert the socket mapping in the constructor. Would be nice to have some kind of correctness check in debug to avoid having to figure this out every time. Maybe not all nodes need the indices and should check only when accessing the index.

Reported separately for the Index Switch node #116885

Reported separately for the Index Switch node #116885
"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) {
LukasTonne marked this conversation as resolved Outdated

Pretty sure this check is redundant with the earlier is_context_dependent_field check.

Pretty sure this check is redundant with the earlier `is_context_dependent_field` check.
*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<LazyFunction> 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<LazyFunctionForMenuSwitchNode>(node, lf_graph_info);
}
std::unique_ptr<LazyFunction> 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<LazyFunctionForMenuSwitchSocketUsage>(node);
}
} // namespace blender::nodes

View File

@ -234,7 +234,8 @@ static void node_rna(StructRNA *srna)
SOCK_OBJECT,
SOCK_COLLECTION,
SOCK_MATERIAL,
SOCK_IMAGE);
SOCK_IMAGE,
SOCK_MENU);
});
});
}

View File

@ -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<bke::RuntimeNodeEnumItem> items = value->enum_items->items;
idprop_items_num = items.size();
idprop_items = MEM_cnew_array<IDPropertyUIDataEnumItem>(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<IDPropertyUIDataEnumItem>(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<IDProperty, bke::idprop::IDPropertyDeleter> id_property_create_from_socket(
const bNodeTreeInterfaceSocket &socket)
{
@ -259,6 +310,14 @@ std::unique_ptr<IDProperty, bke::idprop::IDPropertyDeleter> id_property_create_f
ui_data->default_value = BLI_strdup(value->value);
return property;
}
case SOCK_MENU: {
const bNodeSocketValueMenu *value = static_cast<const bNodeSocketValueMenu *>(
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<const bNodeSocketValueObject *>(
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;

View File

@ -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<LazyFunction> 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<NodeMenuSwitch *>(bnode.storage);
const NodeEnumDefinition &enum_def = storage.enum_definition;
const bNodeSocket *switch_input_bsocket = bnode.input_sockets()[0];
Vector<const bNodeSocket *> 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<LazyFunction> 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<bNodeSocketValueMenu>()->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<LazyFunctionForUndefinedNode>(

View File

@ -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<GenericValueLog>(GMutablePointer{type, buffer}));
};
auto log_menu_value = [&](Span<bke::RuntimeNodeEnumItem> enum_items, const int identifier) {
for (const bke::RuntimeNodeEnumItem &item : enum_items) {
if (item.identifier == identifier) {
log_generic_value(CPPType::get<std::string>(), &item.name);
return;
}
}
log_generic_value(CPPType::get<int>(), &identifier);
};
if (type.is<bke::GeometrySet>()) {
const bke::GeometrySet &geometry = *value.get<bke::GeometrySet>();
store_logged_value(this->allocator->construct<GeometryInfoLog>(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<bNodeSocketValueMenu>();
if (default_value.enum_items) {
const int identifier = *value.get<int>();
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 {

View File

@ -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<bNodeSocketValueMenu>(io_socket);
std::unique_ptr<decl::Menu> decl = std::make_unique<decl::Menu>();
decl->default_value = value.value;
dst = std::move(decl);
break;
}
case SOCK_OBJECT: {
auto value = std::make_unique<decl::Object>();
value->default_value_fn = get_default_id_getter(ntree.tree_interface, io_socket);

View File

@ -428,6 +428,8 @@ std::unique_ptr<SocketDeclaration> make_declaration_for_socket_type(
return std::make_unique<decl::Collection>();
case SOCK_MATERIAL:
return std::make_unique<decl::Material>();
case SOCK_MENU:
return std::make_unique<decl::Menu>();
default:
return {};
}
@ -461,6 +463,8 @@ BaseSocketDeclarationBuilder &NodeDeclarationBuilder::add_input(
return this->add_input<decl::Collection>(name, identifier);
case SOCK_MATERIAL:
return this->add_input<decl::Material>(name, identifier);
case SOCK_MENU:
return this->add_input<decl::Menu>(name, identifier);
default:
BLI_assert_unreachable();
return this->add_input<decl::Float>("", "");
@ -502,6 +506,8 @@ BaseSocketDeclarationBuilder &NodeDeclarationBuilder::add_output(
return this->add_output<decl::Collection>(name, identifier);
case SOCK_MATERIAL:
return this->add_output<decl::Material>(name, identifier);
case SOCK_MENU:
return this->add_output<decl::Menu>(name, identifier);
default:
BLI_assert_unreachable();
return this->add_output<decl::Float>("", "");

View File

@ -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<bNodeSocketValueMenu>("node socket value menu");
dval->value = -1;
*data = dval;
break;
}
case SOCK_OBJECT: {
bNodeSocketValueObject *dval = MEM_cnew<bNodeSocketValueObject>("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<int>();
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<SocketValueVariant>();
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());

View File

@ -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;
Review

Any chance this could get a smarter check so that it only connects to the same enum type? Maybe that's out of scope for now, but it would be really nice.

Any chance this could get a smarter check so that it only connects to the same enum type? Maybe that's out of scope for now, but it would be really nice.
Review

It might work under certain circumstances, but it has limitations and changes workflow somewhat:

  1. Inputs only allow one connection, so checking for matching connections is pointless. The existing menu is replaced anyway.
  2. Outputs allow multiple connections, but users may want to replace the existing menu. Prohibiting connections would force users to first remove the existing connection, which could become annoying.
  3. Node group outputs don't know about the concrete type, this is currently an issue with type resolution more generally. A node group will not propagate the menu from outputs to inputs. We'd need some kind of internal placeholder for enum definitions to make this work. Each output then would still be treated as a separate enum and we still can't infer conflicts, since the concrete enum definitions may be compatible after all.

I suggest we keep this as a possible later feature.

It might work under certain circumstances, but it has limitations and changes workflow somewhat: 1. Inputs only allow one connection, so checking for matching connections is pointless. The existing menu is replaced anyway. 2. Outputs allow multiple connections, but users may want to replace the existing menu. Prohibiting connections would force users to first remove the existing connection, which could become annoying. 3. Node group outputs don't know about the concrete type, this is currently an issue with type resolution more generally. A node group will not propagate the menu from outputs to inputs. We'd need some kind of internal placeholder for enum definitions to make this work. Each output then would still be treated as a separate enum and we still can't infer conflicts, since the concrete enum definitions may be compatible after all. I suggest we keep this as a possible later feature.
}
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
* \{ */