diff --git a/core/string/node_path.cpp b/core/string/node_path.cpp index 32e4564c5e1..8ae2efb7875 100644 --- a/core/string/node_path.cpp +++ b/core/string/node_path.cpp @@ -92,6 +92,14 @@ StringName NodePath::get_subname(int p_idx) const { return data->subpath[p_idx]; } +int NodePath::get_total_name_count() const { + if (!data) { + return 0; + } + + return data->path.size() + data->subpath.size(); +} + void NodePath::unref() { if (data && data->refcount.unref()) { memdelete(data); @@ -229,6 +237,27 @@ StringName NodePath::get_concatenated_subnames() const { return data->concatenated_subpath; } +NodePath NodePath::slice(int p_begin, int p_end) const { + const int name_count = get_name_count(); + const int total_count = get_total_name_count(); + + int begin = CLAMP(p_begin, -total_count, total_count); + if (begin < 0) { + begin += total_count; + } + int end = CLAMP(p_end, -total_count, total_count); + if (end < 0) { + end += total_count; + } + const int sub_begin = MAX(begin - name_count - 1, 0); + const int sub_end = MAX(end - name_count, 0); + + const Vector names = get_names().slice(begin, end); + const Vector sub_names = get_subnames().slice(sub_begin, sub_end); + const bool absolute = is_absolute() && (begin == 0); + return NodePath(names, sub_names, absolute); +} + NodePath NodePath::rel_path_to(const NodePath &p_np) const { ERR_FAIL_COND_V(!is_absolute(), NodePath()); ERR_FAIL_COND_V(!p_np.is_absolute(), NodePath()); @@ -331,7 +360,7 @@ NodePath NodePath::simplified() const { } NodePath::NodePath(const Vector &p_path, bool p_absolute) { - if (p_path.size() == 0) { + if (p_path.size() == 0 && !p_absolute) { return; } @@ -343,7 +372,7 @@ NodePath::NodePath(const Vector &p_path, bool p_absolute) { } NodePath::NodePath(const Vector &p_path, const Vector &p_subpath, bool p_absolute) { - if (p_path.size() == 0 && p_subpath.size() == 0) { + if (p_path.size() == 0 && p_subpath.size() == 0 && !p_absolute) { return; } diff --git a/core/string/node_path.h b/core/string/node_path.h index 876d69924e0..56799839d76 100644 --- a/core/string/node_path.h +++ b/core/string/node_path.h @@ -57,10 +57,12 @@ public: StringName get_name(int p_idx) const; int get_subname_count() const; StringName get_subname(int p_idx) const; + int get_total_name_count() const; Vector get_names() const; Vector get_subnames() const; StringName get_concatenated_names() const; StringName get_concatenated_subnames() const; + NodePath slice(int p_begin, int p_end = INT_MAX) const; NodePath rel_path_to(const NodePath &p_np) const; NodePath get_as_property_path() const; diff --git a/core/variant/variant_call.cpp b/core/variant/variant_call.cpp index f041d2c95ee..6bb00556e8f 100644 --- a/core/variant/variant_call.cpp +++ b/core/variant/variant_call.cpp @@ -2016,6 +2016,7 @@ static void _register_variant_builtin_methods() { bind_method(NodePath, get_subname, sarray("idx"), varray()); bind_method(NodePath, get_concatenated_names, sarray(), varray()); bind_method(NodePath, get_concatenated_subnames, sarray(), varray()); + bind_method(NodePath, slice, sarray("begin", "end"), varray(INT_MAX)); bind_method(NodePath, get_as_property_path, sarray(), varray()); bind_method(NodePath, is_empty, sarray(), varray()); diff --git a/doc/classes/NodePath.xml b/doc/classes/NodePath.xml index 553a3913d86..a13813aa70b 100644 --- a/doc/classes/NodePath.xml +++ b/doc/classes/NodePath.xml @@ -183,6 +183,16 @@ Returns [code]true[/code] if the node path is empty. + + + + + + Returns the slice of the [NodePath], from [param begin] (inclusive) to [param end] (exclusive), as a new [NodePath]. + The absolute value of [param begin] and [param end] will be clamped to the sum of [method get_name_count] and [method get_subname_count], so the default value for [param end] makes it slice to the end of the [NodePath] by default (i.e. [code]path.slice(1)[/code] is a shorthand for [code]path.slice(1, path.get_name_count() + path.get_subname_count())[/code]). + If either [param begin] or [param end] are negative, they will be relative to the end of the [NodePath] (i.e. [code]path.slice(0, -2)[/code] is a shorthand for [code]path.slice(0, path.get_name_count() + path.get_subname_count() - 2)[/code]). + + diff --git a/tests/core/string/test_node_path.h b/tests/core/string/test_node_path.h index b0c810bb549..9afda386115 100644 --- a/tests/core/string/test_node_path.h +++ b/tests/core/string/test_node_path.h @@ -167,6 +167,59 @@ TEST_CASE("[NodePath] Empty path") { node_path_empty.is_empty(), "The node path should be considered empty."); } + +TEST_CASE("[NodePath] Slice") { + const NodePath node_path_relative = NodePath("Parent/Child:prop"); + const NodePath node_path_absolute = NodePath("/root/Parent/Child:prop"); + CHECK_MESSAGE( + node_path_relative.slice(0, 2) == NodePath("Parent/Child"), + "The slice lower bound should be inclusive and the slice upper bound should be exclusive."); + CHECK_MESSAGE( + node_path_relative.slice(3) == NodePath(":prop"), + "Slicing on the length of the path should return the last entry."); + CHECK_MESSAGE( + node_path_relative.slice(1, 3) == NodePath("Child:prop"), + "Slicing should include names and subnames."); + CHECK_MESSAGE( + node_path_relative.slice(-1) == NodePath(":prop"), + "Slicing on -1 should return the last entry."); + CHECK_MESSAGE( + node_path_relative.slice(0, -1) == NodePath("Parent/Child"), + "Slicing up to -1 should include the second-to-last entry."); + CHECK_MESSAGE( + node_path_relative.slice(-2, -1) == NodePath("Child"), + "Slicing from negative to negative should treat lower bound as inclusive and upper bound as exclusive."); + CHECK_MESSAGE( + node_path_relative.slice(0, 10) == NodePath("Parent/Child:prop"), + "Slicing past the length of the path should work like slicing up to the last entry."); + CHECK_MESSAGE( + node_path_relative.slice(-10, 2) == NodePath("Parent/Child"), + "Slicing negatively past the length of the path should work like slicing from the first entry."); + CHECK_MESSAGE( + node_path_relative.slice(1, 1) == NodePath(""), + "Slicing with a lower bound equal to upper bound should return empty path."); + + CHECK_MESSAGE( + node_path_absolute.slice(0, 2) == NodePath("/root/Parent"), + "Slice from beginning of an absolute path should be an absolute path."); + CHECK_MESSAGE( + node_path_absolute.slice(1, 4) == NodePath("Parent/Child:prop"), + "Slice of an absolute path that does not start at the beginning should be a relative path."); + CHECK_MESSAGE( + node_path_absolute.slice(3, 4) == NodePath(":prop"), + "Slice of an absolute path that does not start at the beginning should be a relative path."); + + CHECK_MESSAGE( + NodePath("").slice(0, 1) == NodePath(""), + "Slice of an empty path should be an empty path."); + CHECK_MESSAGE( + NodePath("").slice(-1, 2) == NodePath(""), + "Slice of an empty path should be an empty path."); + CHECK_MESSAGE( + NodePath("/").slice(-1, 2) == NodePath("/"), + "Slice of an empty absolute path should be an empty absolute path."); +} + } // namespace TestNodePath #endif // TEST_NODE_PATH_H