From 8b3be68b3d358860f2d5757ce61ebf43799bde0c Mon Sep 17 00:00:00 2001 From: Brady Johnston Date: Wed, 1 Oct 2025 09:20:11 +0200 Subject: [PATCH] Geometry Nodes: new Join Bundle Node Adds a Join Bundle node with a multi-input Bundle socket. Bundles are iterated on the top level of each input bundle and items added to the resulting single bundle. While creating the final bundle existing items have priority and new items with an already existing name are discarded. There is an info message when there are duplicate keys in the input bundles. Co-authored-by: Brady Johnston Co-authored-by: Jacques Lucke Pull Request: https://projects.blender.org/blender/blender/pulls/146750 --- .../startup/bl_ui/node_add_menu_geometry.py | 1 + .../blender/makesrna/intern/rna_nodetree.cc | 1 + source/blender/nodes/geometry/CMakeLists.txt | 1 + .../geometry/nodes/node_geo_join_bundle.cc | 89 +++++++++++++++++++ source/blender/nodes/intern/trace_values.cc | 41 +++++++-- .../utilities/join_bundle.blend | 3 + 6 files changed, 130 insertions(+), 6 deletions(-) create mode 100644 source/blender/nodes/geometry/nodes/node_geo_join_bundle.cc create mode 100644 tests/files/modeling/geometry_nodes/utilities/join_bundle.blend diff --git a/scripts/startup/bl_ui/node_add_menu_geometry.py b/scripts/startup/bl_ui/node_add_menu_geometry.py index cd845d46722..d522f1f3f21 100644 --- a/scripts/startup/bl_ui/node_add_menu_geometry.py +++ b/scripts/startup/bl_ui/node_add_menu_geometry.py @@ -790,6 +790,7 @@ class NODE_MT_category_utilities_bundle_base(node_add_menu.NodeMenu): layout = self.layout self.node_operator(layout, "NodeCombineBundle") self.node_operator(layout, "NodeSeparateBundle") + self.node_operator(layout, "NodeJoinBundle") self.draw_assets_for_catalog(layout, self.menu_path) diff --git a/source/blender/makesrna/intern/rna_nodetree.cc b/source/blender/makesrna/intern/rna_nodetree.cc index 78f7b9b4571..5bc5a6b3c28 100644 --- a/source/blender/makesrna/intern/rna_nodetree.cc +++ b/source/blender/makesrna/intern/rna_nodetree.cc @@ -9743,6 +9743,7 @@ static void rna_def_nodes(BlenderRNA *brna) define("NodeInternal", "NodeCombineBundle", def_combine_bundle); define("NodeInternal", "NodeEnableOutput"); define("NodeInternal", "NodeEvaluateClosure", def_evaluate_closure); + define("NodeInternal", "NodeJoinBundle"); define("NodeInternal", "NodeSeparateBundle", def_separate_bundle); define("ShaderNode", "ShaderNodeAddShader"); diff --git a/source/blender/nodes/geometry/CMakeLists.txt b/source/blender/nodes/geometry/CMakeLists.txt index 4b455791989..b178b6e0f37 100644 --- a/source/blender/nodes/geometry/CMakeLists.txt +++ b/source/blender/nodes/geometry/CMakeLists.txt @@ -146,6 +146,7 @@ set(SRC nodes/node_geo_instances_to_points.cc nodes/node_geo_interpolate_curves.cc nodes/node_geo_is_viewport.cc + nodes/node_geo_join_bundle.cc nodes/node_geo_join_geometry.cc nodes/node_geo_list.cc nodes/node_geo_list_get_item.cc diff --git a/source/blender/nodes/geometry/nodes/node_geo_join_bundle.cc b/source/blender/nodes/geometry/nodes/node_geo_join_bundle.cc new file mode 100644 index 00000000000..5c8e2f03b43 --- /dev/null +++ b/source/blender/nodes/geometry/nodes/node_geo_join_bundle.cc @@ -0,0 +1,89 @@ +/* SPDX-FileCopyrightText: 2025 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +#include + +#include "NOD_geometry_nodes_bundle.hh" + +#include "node_geometry_util.hh" + +namespace blender::nodes::node_geo_join_bundle { + +static void node_declare(NodeDeclarationBuilder &b) +{ + b.use_custom_socket_order(); + b.allow_any_socket_order(); + b.add_input("Bundle").multi_input().description( + "Bundles to join together on the top level for each bundle. When there are duplicates, only " + "the first occurence is used"); + b.add_output("Bundle").align_with_previous(); +} + +static void node_geo_exec(GeoNodeExecParams params) +{ + GeoNodesMultiInput bundles = params.extract_input>( + "Bundle"); + + if (bundles.values.is_empty()) { + params.set_default_remaining_outputs(); + return; + } + + BundlePtr output_bundle; + int bundle_i = 0; + for (; bundle_i < bundles.values.size(); bundle_i++) { + BundlePtr &bundle = bundles.values[bundle_i]; + if (bundle) { + output_bundle = std::move(bundle); + bundle_i++; + break; + } + } + if (!output_bundle) { + output_bundle = Bundle::create(); + } + else if (!output_bundle->is_mutable()) { + output_bundle = output_bundle->copy(); + } + else { + output_bundle->tag_ensured_mutable(); + } + Bundle &mutable_output_bundle = const_cast(*output_bundle); + + VectorSet overridden_keys; + for (; bundle_i < bundles.values.size(); bundle_i++) { + BundlePtr &bundle = bundles.values[bundle_i]; + if (!bundle) { + continue; + } + for (const Bundle::StoredItem &item : bundle->items()) { + if (!mutable_output_bundle.add(item.key, item.value)) { + overridden_keys.add(item.key); + } + } + } + + if (!overridden_keys.is_empty()) { + std::string message = fmt::format( + "{}: {}", TIP_("Duplicate keys"), fmt::join(overridden_keys, ", ")); + params.error_message_add(NodeWarningType::Info, std::move(message)); + } + + params.set_output("Bundle", output_bundle); +} + +static void node_register() +{ + static blender::bke::bNodeType ntype; + geo_node_type_base(&ntype, "NodeJoinBundle"); + ntype.ui_name = "Join Bundle"; + ntype.ui_description = "Join multiple bundles together"; + ntype.nclass = NODE_CLASS_CONVERTER; + ntype.geometry_node_execute = node_geo_exec; + ntype.declare = node_declare; + blender::bke::node_register_type(ntype); +} +NOD_REGISTER_NODE(node_register) + +} // namespace blender::nodes::node_geo_join_bundle diff --git a/source/blender/nodes/intern/trace_values.cc b/source/blender/nodes/intern/trace_values.cc index 2a371397543..6c297fbf6f5 100644 --- a/source/blender/nodes/intern/trace_values.cc +++ b/source/blender/nodes/intern/trace_values.cc @@ -446,6 +446,10 @@ static Vector find_origin_sockets_through_contexts( } continue; } + if (node->is_type("NodeJoinBundle")) { + add_if_new(node.input_socket(0), bundle_path); + continue; + } if (node->is_type("NodeEvaluateClosure")) { const auto &evaluate_storage = *static_cast(node->storage); const StringRef key = evaluate_storage.output_items.items[socket->index()].name; @@ -611,12 +615,37 @@ LinkedBundleSignatures gather_linked_origin_bundle_signatures( {bundle_socket_context, &bundle_socket}, compute_context_cache, [&](const SocketInContext &socket) { - const bNode &node = socket->owner_node(); - if (socket->is_output() && node.is_type("NodeCombineBundle")) { - const auto &storage = *static_cast(node.storage); - result.items.append({BundleSignature::from_combine_bundle_node(node, false), - bool(storage.flag & NODE_COMBINE_BUNDLE_FLAG_DEFINE_SIGNATURE), - socket}); + const NodeInContext node = socket.owner_node(); + if (socket->is_output()) { + if (node->is_type("NodeCombineBundle")) { + const auto &storage = *static_cast(node->storage); + result.items.append({BundleSignature::from_combine_bundle_node(*node, false), + bool(storage.flag & NODE_COMBINE_BUNDLE_FLAG_DEFINE_SIGNATURE), + socket}); + return true; + } + } + if (node->is_type("NodeJoinBundle")) { + const SocketInContext input_socket = node.input_socket(0); + BundleSignature joined_signature; + bool is_signature_definition = true; + for (const bNodeLink *link : input_socket->directly_linked_links()) { + if (!link->is_used()) { + continue; + } + const bNodeSocket *socket_from = link->fromsock; + const LinkedBundleSignatures sub_signatures = gather_linked_origin_bundle_signatures( + node.context, *socket_from, compute_context_cache); + for (const LinkedBundleSignatures::Item &sub_signature : sub_signatures.items) { + if (!sub_signature.is_signature_definition) { + is_signature_definition = false; + } + for (const BundleSignature::Item &item : sub_signature.signature.items) { + joined_signature.items.add(item); + } + } + } + result.items.append({joined_signature, is_signature_definition, socket}); return true; } return false; diff --git a/tests/files/modeling/geometry_nodes/utilities/join_bundle.blend b/tests/files/modeling/geometry_nodes/utilities/join_bundle.blend new file mode 100644 index 00000000000..92f39a60646 --- /dev/null +++ b/tests/files/modeling/geometry_nodes/utilities/join_bundle.blend @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:344ee9e5991b7b5ea9a965e5d4382136d2295fc70f919c9d318fca6b096c3c88 +size 868815