From 2e48d331010614141eca9449a46090ffc6ce3393 Mon Sep 17 00:00:00 2001 From: Jacques Lucke Date: Wed, 23 Jul 2025 11:23:32 +0200 Subject: [PATCH] Geometry Nodes: improve internal bundle api This extends the API for bundles in two main ways: * Adds support for working with paths to reference nested bundles directly. * Adds support for typed add/lookup, also taking into account implicit conversions. Some unit tests have been added as well. Pull Request: https://projects.blender.org/blender/blender/pulls/142942 --- source/blender/blenkernel/intern/node.cc | 19 ++ source/blender/nodes/CMakeLists.txt | 1 + source/blender/nodes/NOD_geometry_exec.hh | 23 +-- .../nodes/NOD_geometry_nodes_bundle.hh | 188 +++++++++++++++++- .../nodes/NOD_geometry_nodes_lazy_function.hh | 17 -- .../nodes/NOD_geometry_nodes_values.hh | 61 ++++++ .../nodes/intern/geometry_nodes_bundle.cc | 120 ++++++++++- .../intern/geometry_nodes_bundle_tests.cc | 106 ++++++++++ .../intern/geometry_nodes_closure_zone.cc | 1 + 9 files changed, 487 insertions(+), 49 deletions(-) create mode 100644 source/blender/nodes/NOD_geometry_nodes_values.hh create mode 100644 source/blender/nodes/intern/geometry_nodes_bundle_tests.cc diff --git a/source/blender/blenkernel/intern/node.cc b/source/blender/blenkernel/intern/node.cc index 8010ee916b0..3c7a4b71331 100644 --- a/source/blender/blenkernel/intern/node.cc +++ b/source/blender/blenkernel/intern/node.cc @@ -4883,6 +4883,25 @@ std::optional geo_nodes_base_cpp_type_to_socket_type(const if (type.is()) { return SOCK_CLOSURE; } + if (type.is()) { + return SOCK_GEOMETRY; + } + if (type.is()) { + return SOCK_MATERIAL; + } + if (type.is()) { + return SOCK_TEXTURE; + } + if (type.is()) { + return SOCK_OBJECT; + } + if (type.is()) { + return SOCK_COLLECTION; + } + if (type.is()) { + return SOCK_IMAGE; + } + return std::nullopt; } diff --git a/source/blender/nodes/CMakeLists.txt b/source/blender/nodes/CMakeLists.txt index 12358beeeca..5cd780c79bc 100644 --- a/source/blender/nodes/CMakeLists.txt +++ b/source/blender/nodes/CMakeLists.txt @@ -240,6 +240,7 @@ if(WITH_GTESTS) ) set(TEST_SRC intern/node_iterator_tests.cc + intern/geometry_nodes_bundle_tests.cc ) set(TEST_LIB bf_nodes diff --git a/source/blender/nodes/NOD_geometry_exec.hh b/source/blender/nodes/NOD_geometry_exec.hh index 3cddc7dbace..781f374c029 100644 --- a/source/blender/nodes/NOD_geometry_exec.hh +++ b/source/blender/nodes/NOD_geometry_exec.hh @@ -24,6 +24,7 @@ #include "NOD_derived_node_tree.hh" #include "NOD_geometry_nodes_lazy_function.hh" +#include "NOD_geometry_nodes_values.hh" namespace blender::nodes { @@ -96,22 +97,6 @@ class GeoNodeExecParams { { } - template - static constexpr bool is_field_base_type_v = is_same_any_v; - - template - static constexpr bool stored_as_SocketValueVariant_v = - is_field_base_type_v || fn::is_field_v || bke::is_VolumeGrid_v || - is_same_any_v; - /** * Get the input value for the input socket with the given identifier. * @@ -122,7 +107,7 @@ class GeoNodeExecParams { if constexpr (std::is_enum_v) { return T(this->extract_input(identifier)); } - else if constexpr (stored_as_SocketValueVariant_v) { + else if constexpr (geo_nodes_type_stored_as_SocketValueVariant_v) { SocketValueVariant value_variant = this->extract_input(identifier); return value_variant.extract(); } @@ -154,7 +139,7 @@ class GeoNodeExecParams { if constexpr (std::is_enum_v) { return T(this->get_input(identifier)); } - else if constexpr (stored_as_SocketValueVariant_v) { + else if constexpr (geo_nodes_type_stored_as_SocketValueVariant_v) { auto value_variant = this->get_input(identifier); return value_variant.extract(); } @@ -191,7 +176,7 @@ class GeoNodeExecParams { template void set_output(StringRef identifier, T &&value) { using StoredT = std::decay_t; - if constexpr (stored_as_SocketValueVariant_v) { + if constexpr (geo_nodes_type_stored_as_SocketValueVariant_v) { this->set_output(identifier, SocketValueVariant::From(std::forward(value))); } else { diff --git a/source/blender/nodes/NOD_geometry_nodes_bundle.hh b/source/blender/nodes/NOD_geometry_nodes_bundle.hh index a6d6b8317a4..b8d03c6d758 100644 --- a/source/blender/nodes/NOD_geometry_nodes_bundle.hh +++ b/source/blender/nodes/NOD_geometry_nodes_bundle.hh @@ -6,7 +6,9 @@ #include "BKE_node.hh" +#include "BKE_node_socket_value.hh" #include "NOD_geometry_nodes_bundle_fwd.hh" +#include "NOD_geometry_nodes_values.hh" #include "NOD_socket_interface_key.hh" #include "DNA_node_types.h" @@ -16,6 +18,8 @@ namespace blender::nodes { /** * A bundle is a map containing keys and their corresponding values. Values are stored as the type * they have in Geometry Nodes (#bNodeSocketType::geometry_nodes_cpp_type). + * + * The API also supports working with paths in nested bundles like `root/child/data`. */ class Bundle : public ImplicitSharingMixin { public: @@ -33,6 +37,12 @@ class Bundle : public ImplicitSharingMixin { struct Item { const bke::bNodeSocketType *type; const void *value; + + /** + * Attempts to cast the stored value to the given type. This may do implicit conversions. + */ + template std::optional as(const bke::bNodeSocketType &socket_type) const; + template std::optional as() const; }; Bundle(); @@ -42,25 +52,183 @@ class Bundle : public ImplicitSharingMixin { Bundle &operator=(Bundle &&other) noexcept; ~Bundle(); - static BundlePtr create() - { - return BundlePtr(MEM_new(__func__)); - } + static BundlePtr create(); - void add_new(SocketInterfaceKey key, const bke::bNodeSocketType &type, const void *value); bool add(const SocketInterfaceKey &key, const bke::bNodeSocketType &type, const void *value); - bool add(SocketInterfaceKey &&key, const bke::bNodeSocketType &type, const void *value); + void add_new(SocketInterfaceKey key, const bke::bNodeSocketType &type, const void *value); + void add_override(const SocketInterfaceKey &key, + const bke::bNodeSocketType &type, + const void *value); + bool add_path(StringRef path, const bke::bNodeSocketType &type, const void *value); + void add_path_new(StringRef path, const bke::bNodeSocketType &type, const void *value); + void add_path_override(StringRef path, const bke::bNodeSocketType &type, const void *value); + + template void add(const SocketInterfaceKey &key, T value); + template void add_override(const SocketInterfaceKey &key, T value); + template void add_path(StringRef path, T value); + template void add_path_override(StringRef path, T value); + bool remove(const SocketInterfaceKey &key); bool contains(const SocketInterfaceKey &key) const; + bool contains_path(StringRef path) const; std::optional lookup(const SocketInterfaceKey &key) const; + std::optional lookup_path(Span path) const; + std::optional lookup_path(StringRef path) const; + template std::optional lookup(const SocketInterfaceKey &key) const; + template std::optional lookup_path(StringRef path) const; - Span items() const - { - return items_; - } + bool is_empty() const; + int64_t size() const; + + Span items() const; + + BundlePtr copy() const; void delete_self() override; }; +template +inline std::optional Bundle::Item::as(const bke::bNodeSocketType &socket_type) const +{ + if (!this->value || !this->type) { + return std::nullopt; + } + const void *converted_value = this->value; + BUFFER_FOR_CPP_TYPE_VALUE(*socket_type.geometry_nodes_cpp_type, buffer); + if (this->type != &socket_type) { + if (!implicitly_convert_socket_value(*this->type, this->value, socket_type, buffer)) { + return std::nullopt; + } + converted_value = buffer; + } + if constexpr (geo_nodes_type_stored_as_SocketValueVariant_v) { + const auto &value_variant = *static_cast(converted_value); + return value_variant.get(); + } + return *static_cast(converted_value); +} + +template constexpr bool is_valid_static_bundle_item_type() +{ + if (geo_nodes_is_field_base_type_v) { + return true; + } + if constexpr (fn::is_field_v) { + return geo_nodes_is_field_base_type_v; + } + if constexpr (is_same_any_v) { + return true; + } + return !geo_nodes_type_stored_as_SocketValueVariant_v; +} + +template inline const bke::bNodeSocketType *socket_type_info_by_static_type() +{ + if constexpr (fn::is_field_v) { + if constexpr (geo_nodes_is_field_base_type_v) { + const std::optional socket_type = + bke::geo_nodes_base_cpp_type_to_socket_type(CPPType::get()); + BLI_assert(socket_type); + const bke::bNodeSocketType *socket_type_info = bke::node_socket_type_find_static( + *socket_type); + BLI_assert(socket_type_info); + return socket_type_info; + } + } + const std::optional socket_type = + bke::geo_nodes_base_cpp_type_to_socket_type(CPPType::get()); + if (!socket_type) { + return nullptr; + } + return bke::node_socket_type_find_static(*socket_type); +} + +template inline std::optional Bundle::Item::as() const +{ + static_assert(is_valid_static_bundle_item_type()); + if (const bke::bNodeSocketType *socket_type = socket_type_info_by_static_type()) { + return this->as(*socket_type); + } + /* Can't lookup this type directly currently. */ + BLI_assert_unreachable(); + return std::nullopt; +} + +template inline std::optional Bundle::lookup(const SocketInterfaceKey &key) const +{ + const std::optional item = this->lookup(key); + if (!item) { + return std::nullopt; + } + return item->as(); +} + +template inline std::optional Bundle::lookup_path(const StringRef path) const +{ + const std::optional item = this->lookup_path(path); + if (!item) { + return std::nullopt; + } + return item->as(); +} + +template inline void to_stored_type(T &&value, Fn &&fn) +{ + using DecayT = std::decay_t; + static_assert(is_valid_static_bundle_item_type()); + const bke::bNodeSocketType *socket_type = socket_type_info_by_static_type(); + BLI_assert(socket_type); + if constexpr (geo_nodes_type_stored_as_SocketValueVariant_v) { + auto value_variant = bke::SocketValueVariant::From(std::forward(value)); + fn(*socket_type, &value_variant); + } + else { + fn(*socket_type, &value); + } +} + +template inline void Bundle::add(const SocketInterfaceKey &key, T value) +{ + to_stored_type(value, [&](const bke::bNodeSocketType &type, const void *value) { + this->add(key, type, value); + }); +} + +template inline void Bundle::add_path(const StringRef path, T value) +{ + to_stored_type(value, [&](const bke::bNodeSocketType &type, const void *value) { + this->add_path(path, type, value); + }); +} + +template inline void Bundle::add_override(const SocketInterfaceKey &key, T value) +{ + to_stored_type(value, [&](const bke::bNodeSocketType &type, const void *value) { + this->add_override(key, type, value); + }); +} + +template inline void Bundle::add_path_override(const StringRef path, T value) +{ + to_stored_type(value, [&](const bke::bNodeSocketType &type, const void *value) { + this->add_path_override(path, type, value); + }); +} + +inline Span Bundle::items() const +{ + return items_; +} + +inline bool Bundle::is_empty() const +{ + return items_.is_empty(); +} + +inline int64_t Bundle::size() const +{ + return items_.size(); +} + } // namespace blender::nodes diff --git a/source/blender/nodes/NOD_geometry_nodes_lazy_function.hh b/source/blender/nodes/NOD_geometry_nodes_lazy_function.hh index 39cfe4a00a7..c1c201b311d 100644 --- a/source/blender/nodes/NOD_geometry_nodes_lazy_function.hh +++ b/source/blender/nodes/NOD_geometry_nodes_lazy_function.hh @@ -629,23 +629,6 @@ std::string zone_wrapper_output_name(const ZoneBuildInfo &zone_info, const Span outputs, const int lf_socket_i); -/** - * Performs implicit conversion between socket types. Returns false if the conversion is not - * possible. In that case, r_to_value is left uninitialized. - */ -[[nodiscard]] bool implicitly_convert_socket_value(const bke::bNodeSocketType &from_type, - const void *from_value, - const bke::bNodeSocketType &to_type, - void *r_to_value); - -/** - * Builds a lazy-function that can convert between socket types. Returns null if the conversion is - * never possible. - */ -const LazyFunction *build_implicit_conversion_lazy_function(const bke::bNodeSocketType &from_type, - const bke::bNodeSocketType &to_type, - ResourceScope &scope); - /** * Report an error from a multi-function evaluation within a Geometry Nodes evaluation. * diff --git a/source/blender/nodes/NOD_geometry_nodes_values.hh b/source/blender/nodes/NOD_geometry_nodes_values.hh new file mode 100644 index 00000000000..f97bed5295e --- /dev/null +++ b/source/blender/nodes/NOD_geometry_nodes_values.hh @@ -0,0 +1,61 @@ +/* SPDX-FileCopyrightText: 2025 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +#pragma once + +#include "BKE_node.hh" +#include "BKE_volume_grid_fwd.hh" + +#include "BLI_color.hh" +#include "BLI_math_quaternion_types.hh" +#include "BLI_math_vector_types.hh" +#include "BLI_memory_utils.hh" + +#include "FN_field.hh" +#include "FN_lazy_function.hh" + +#include "NOD_geometry_nodes_bundle_fwd.hh" +#include "NOD_geometry_nodes_closure_fwd.hh" + +namespace blender::nodes { + +/** True if a static type can also exist as field in Geometry Nodes. */ +template +static constexpr bool geo_nodes_is_field_base_type_v = is_same_any_v; + +/** True if Geometry Nodes sockets can store values of the given type and the type is stored + * embedded in a #SocketValueVariant. */ +template +static constexpr bool geo_nodes_type_stored_as_SocketValueVariant_v = + std::is_enum_v || geo_nodes_is_field_base_type_v || fn::is_field_v || + bke::is_VolumeGrid_v || + is_same_any_v; + +/** + * Performs implicit conversion between socket types. Returns false if the conversion is not + * possible. In that case, r_to_value is left uninitialized. + */ +[[nodiscard]] bool implicitly_convert_socket_value(const bke::bNodeSocketType &from_type, + const void *from_value, + const bke::bNodeSocketType &to_type, + void *r_to_value); + +/** + * Builds a lazy-function that can convert between socket types. Returns null if the conversion is + * never possible. + */ +const fn::lazy_function::LazyFunction *build_implicit_conversion_lazy_function( + const bke::bNodeSocketType &from_type, + const bke::bNodeSocketType &to_type, + ResourceScope &scope); + +} // namespace blender::nodes diff --git a/source/blender/nodes/intern/geometry_nodes_bundle.cc b/source/blender/nodes/intern/geometry_nodes_bundle.cc index 73859ea50e5..fdf11e6adc7 100644 --- a/source/blender/nodes/intern/geometry_nodes_bundle.cc +++ b/source/blender/nodes/intern/geometry_nodes_bundle.cc @@ -2,6 +2,7 @@ * * SPDX-License-Identifier: GPL-2.0-or-later */ +#include "BKE_node_socket_value.hh" #include "BLI_cpp_type.hh" #include "BKE_node_runtime.hh" @@ -104,6 +105,11 @@ Bundle::Bundle(Bundle &&other) noexcept { } +BundlePtr Bundle::create() +{ + return BundlePtr(MEM_new(__func__)); +} + Bundle &Bundle::operator=(const Bundle &other) { if (this == &other) { @@ -135,6 +141,14 @@ void Bundle::add_new(SocketInterfaceKey key, const bke::bNodeSocketType &type, c buffers_.append(buffer); } +void Bundle::add_override(const SocketInterfaceKey &key, + const bke::bNodeSocketType &type, + const void *value) +{ + this->remove(key); + this->add_new(key, type, value); +} + bool Bundle::add(const SocketInterfaceKey &key, const bke::bNodeSocketType &type, const void *value) @@ -146,15 +160,56 @@ bool Bundle::add(const SocketInterfaceKey &key, return true; } -bool Bundle::add(SocketInterfaceKey &&key, const bke::bNodeSocketType &type, const void *value) +void Bundle::add_path_override(const StringRef path, + const bke::bNodeSocketType &type, + const void *value) { - if (this->contains(key)) { + BLI_assert(!path.is_empty()); + BLI_assert(!path.endswith("/")); + BLI_assert(this->is_mutable()); + const int sep = path.find_first_of('/'); + if (sep == StringRef::not_found) { + this->remove(SocketInterfaceKey{path}); + this->add_new(SocketInterfaceKey{path}, type, value); + return; + } + const StringRef first_part = path.substr(0, sep); + BundlePtr child_bundle; + const std::optional item = this->lookup(SocketInterfaceKey{first_part}); + if (item && item->type->type == SOCK_BUNDLE) { + child_bundle = static_cast(item->value)->get(); + } + if (!child_bundle) { + child_bundle = Bundle::create(); + } + this->remove(SocketInterfaceKey{first_part}); + if (!child_bundle->is_mutable()) { + child_bundle = child_bundle->copy(); + } + child_bundle->tag_ensured_mutable(); + const_cast(*child_bundle).add_path_override(path.substr(sep + 1), type, value); + bke::SocketValueVariant child_bundle_value = bke::SocketValueVariant::From( + std::move(child_bundle)); + this->add(SocketInterfaceKey{first_part}, + *bke::node_socket_type_find_static(SOCK_BUNDLE), + &child_bundle_value); +} + +bool Bundle::add_path(StringRef path, const bke::bNodeSocketType &type, const void *value) +{ + if (this->contains_path(path)) { return false; } - this->add_new(std::move(key), type, value); + this->add_path_new(path, type, value); return true; } +void Bundle::add_path_new(StringRef path, const bke::bNodeSocketType &type, const void *value) +{ + BLI_assert(!this->contains_path(path)); + this->add_path_override(path, type, value); +} + std::optional Bundle::lookup(const SocketInterfaceKey &key) const { for (const StoredItem &item : items_) { @@ -165,6 +220,60 @@ std::optional Bundle::lookup(const SocketInterfaceKey &key) const return std::nullopt; } +std::optional Bundle::lookup_path(const Span path) const +{ + BLI_assert(!path.is_empty()); + const StringRef first_elem = path[0]; + const std::optional item = this->lookup(SocketInterfaceKey(first_elem)); + if (!item) { + return std::nullopt; + } + if (path.size() == 1) { + return item; + } + if (item->type->type != SOCK_BUNDLE) { + return std::nullopt; + } + const BundlePtr child_bundle = + static_cast(item->value)->get(); + if (!child_bundle) { + return std::nullopt; + } + return child_bundle->lookup_path(path.drop_front(1)); +} + +static Vector split_path(const StringRef path) +{ + Vector path_elems; + StringRef remaining = path; + while (!remaining.is_empty()) { + const int sep = remaining.find_first_of('/'); + if (sep == StringRef::not_found) { + path_elems.append(remaining); + break; + } + path_elems.append(remaining.substr(0, sep)); + remaining = remaining.substr(sep + 1); + } + return path_elems; +} + +std::optional Bundle::lookup_path(const StringRef path) const +{ + const Vector path_elems = split_path(path); + return this->lookup_path(path_elems); +} + +BundlePtr Bundle::copy() const +{ + BundlePtr copy_ptr = Bundle::create(); + Bundle © = const_cast(*copy_ptr); + for (const StoredItem &item : items_) { + copy.add_new(item.key, *item.type, item.value); + } + return copy_ptr; +} + bool Bundle::remove(const SocketInterfaceKey &key) { const int removed_num = items_.remove_if([&key](StoredItem &item) { @@ -187,6 +296,11 @@ bool Bundle::contains(const SocketInterfaceKey &key) const return false; } +bool Bundle::contains_path(const StringRef path) const +{ + return this->lookup_path(path).has_value(); +} + void Bundle::delete_self() { MEM_delete(this); diff --git a/source/blender/nodes/intern/geometry_nodes_bundle_tests.cc b/source/blender/nodes/intern/geometry_nodes_bundle_tests.cc new file mode 100644 index 00000000000..d82cf22e2dd --- /dev/null +++ b/source/blender/nodes/intern/geometry_nodes_bundle_tests.cc @@ -0,0 +1,106 @@ +/* SPDX-FileCopyrightText: 2025 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ +#include "testing/testing.h" + +#include "CLG_log.h" + +#include "BKE_appdir.hh" +#include "BKE_context.hh" +#include "BKE_global.hh" +#include "BKE_idtype.hh" +#include "BKE_main.hh" +#include "BKE_material.hh" +#include "BKE_node.hh" +#include "BKE_scene.hh" + +#include "IMB_imbuf.hh" + +#include "RNA_define.hh" + +#include "NOD_geometry_nodes_bundle.hh" + +namespace blender::nodes::tests { + +class BundleTest : public ::testing::Test { + + protected: + static void SetUpTestSuite() + { + CLG_init(); + BKE_idtype_init(); + RNA_init(); + blender::bke::node_system_init(); + BKE_appdir_init(); + IMB_init(); + BKE_materials_init(); + } + + static void TearDownTestSuite() + { + BKE_materials_exit(); + bke::node_system_exit(); + RNA_exit(); + BKE_appdir_exit(); + IMB_exit(); + CLG_exit(); + } +}; + +TEST_F(BundleTest, DefaultBundle) +{ + BundlePtr bundle = Bundle::create(); + EXPECT_TRUE(bundle); + EXPECT_TRUE(bundle->is_empty()); +} + +TEST_F(BundleTest, AddItems) +{ + BundlePtr bundle_ptr = Bundle::create(); + Bundle &bundle = const_cast(*bundle_ptr); + bundle.add(SocketInterfaceKey{"a"}, 3); + EXPECT_EQ(bundle.size(), 1); + EXPECT_TRUE(bundle.contains(SocketInterfaceKey{"a"})); + EXPECT_EQ(bundle.lookup(SocketInterfaceKey{"a"}), 3); +} + +TEST_F(BundleTest, AddLookupPath) +{ + BundlePtr bundle_ptr = Bundle::create(); + Bundle &bundle = const_cast(*bundle_ptr); + bundle.add_path("a/b/c", 3); + bundle.add_path("a/b/d", 4); + EXPECT_EQ(bundle.size(), 1); + EXPECT_EQ((*bundle.lookup_path("a"))->size(), 1); + EXPECT_EQ((*bundle.lookup_path("a/b"))->size(), 2); + EXPECT_EQ(bundle.lookup_path("a/b/c"), 3); + EXPECT_EQ(bundle.lookup_path("a/b/d"), 4); + EXPECT_EQ(bundle.lookup_path("a/b/c"), std::nullopt); + EXPECT_EQ(bundle.lookup_path("a/b/x"), std::nullopt); +} + +TEST_F(BundleTest, LookupConversion) +{ + BundlePtr bundle_ptr = Bundle::create(); + Bundle &bundle = const_cast(*bundle_ptr); + bundle.add_path("a/b", -3.4f); + EXPECT_EQ(bundle.lookup_path("a/b"), -3.4f); + EXPECT_EQ(bundle.lookup_path("a/b"), -3); + EXPECT_EQ(bundle.lookup_path("a/b"), false); + EXPECT_EQ(bundle.lookup_path("a/b"), float3(-3.4f)); + EXPECT_EQ(bundle.lookup_path("a/b"), std::nullopt); +} + +TEST_F(BundleTest, AddOverride) +{ + BundlePtr bundle_ptr = Bundle::create(); + Bundle &bundle = const_cast(*bundle_ptr); + bundle.add_path("a/b", 4); + EXPECT_EQ(bundle.lookup_path("a/b"), 4); + bundle.add_path_override("a/b", 10); + EXPECT_EQ(bundle.lookup_path("a/b"), 10); + bundle.add_path("a/b", 15); + EXPECT_EQ(bundle.lookup_path("a/b"), 10); +} + +} // namespace blender::nodes::tests diff --git a/source/blender/nodes/intern/geometry_nodes_closure_zone.cc b/source/blender/nodes/intern/geometry_nodes_closure_zone.cc index ef305a6eb3d..f1abfa73219 100644 --- a/source/blender/nodes/intern/geometry_nodes_closure_zone.cc +++ b/source/blender/nodes/intern/geometry_nodes_closure_zone.cc @@ -15,6 +15,7 @@ #include "NOD_geo_closure.hh" #include "NOD_geometry_nodes_closure.hh" +#include "NOD_geometry_nodes_values.hh" #include "DEG_depsgraph_query.hh"