Merge pull request #113479 from Meorge/feat/collapse-anim-groups

Collapse groups in animation track editor
This commit is contained in:
Thaddeus Crews 2026-02-24 09:29:42 -06:00
commit 15a4311583
No known key found for this signature in database
GPG key ID: 8C6E5FEB5FC03CCC
6 changed files with 190 additions and 12 deletions

View file

@ -3911,7 +3911,14 @@ void AnimationTrackEditGroup::_notification(int p_what) {
draw_line(Point2(get_size().width - timeline->get_buttons_width(), 0), Point2(get_size().width - timeline->get_buttons_width(), get_size().height), v_line_color, Math::round(EDSCALE));
int ofs = stylebox_header->get_margin(SIDE_LEFT);
bool is_group_folded = editor->get_current_animation()->editor_is_group_folded(node_name);
Ref<Texture2D> fold_icon = get_theme_icon(is_group_folded ? SNAME("arrow_collapsed") : SNAME("arrow"), SNAME("Tree"));
Size2 fold_icon_size = fold_icon->get_size();
draw_texture_rect(fold_icon, Rect2(Point2(ofs, (get_size().height - fold_icon_size.y) / 2 + v_margin_offset).round(), fold_icon_size));
ofs += h_separation + fold_icon_size.x;
draw_texture_rect(icon, Rect2(Point2(ofs, (get_size().height - icon_size.y) / 2 + v_margin_offset).round(), icon_size));
ofs += h_separation + icon_size.x;
draw_string(font, Point2(ofs, (get_size().height - font->get_height(font_size)) / 2 + font->get_ascent(font_size) + v_margin_offset).round(), node_name, HORIZONTAL_ALIGNMENT_LEFT, timeline->get_name_limit() - ofs, font_size, color);
@ -3938,14 +3945,41 @@ void AnimationTrackEditGroup::gui_input(const Ref<InputEvent> &p_event) {
Ref<InputEventMouseButton> mb = p_event;
if (mb.is_valid() && mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT) {
Point2 pos = mb->get_position();
Rect2 node_name_rect = Rect2(0, 0, timeline->get_name_limit(), get_size().height);
if (node_name_rect.has_point(pos)) {
EditorSelection *editor_selection = EditorNode::get_singleton()->get_editor_selection();
editor_selection->clear();
Node *n = root->get_node_or_null(node);
if (n) {
editor_selection->add_node(n);
int left_ofs = get_theme_stylebox(SNAME("header"), SNAME("AnimationTrackEditGroup"))->get_margin(SIDE_LEFT);
bool is_group_folded = editor->get_current_animation()->editor_is_group_folded(node_name);
Ref<Texture2D> fold_icon = get_theme_icon(is_group_folded ? SNAME("arrow_collapsed") : SNAME("arrow"), SNAME("Tree"));
int fold_icon_width = fold_icon->get_size().width;
Rect2 fold_area_rect = Rect2(0, 0, left_ofs + fold_icon_width, get_size().height);
if (fold_area_rect.has_point(pos)) {
bool current_group_folded = !editor->get_current_animation()->editor_is_group_folded(node_name);
editor->get_current_animation()->editor_set_group_folded(node_name, current_group_folded);
if (!editor->get_current_animation()->get_path().is_resource_file()) {
EditorNode::get_editor_folding().save_scene_folding(
EditorNode::get_singleton()->get_edited_scene(),
EditorNode::get_singleton()->get_edited_scene()->get_scene_file_path());
} else {
EditorNode::get_editor_folding().save_resource_folding(
editor->get_current_animation(),
editor->get_current_animation()->get_path());
}
for (AnimationTrackEdit *i : track_edits) {
i->set_visible(!current_group_folded);
}
queue_redraw();
} else {
Rect2 node_name_rect = Rect2(0, 0, timeline->get_name_limit(), get_size().height);
if (node_name_rect.has_point(pos)) {
EditorSelection *editor_selection = EditorNode::get_singleton()->get_editor_selection();
editor_selection->clear();
Node *n = root->get_node_or_null(node);
if (n) {
editor_selection->add_node(n);
}
}
}
}
@ -4079,6 +4113,12 @@ void AnimationTrackEditor::set_animation(const Ref<Animation> &p_anim, bool p_re
}
}
}
if (animation->get_path().is_resource_file()) {
EditorNode::get_editor_folding().load_resource_folding(
animation,
animation->get_path());
}
} else {
hscroll->hide();
edit->set_disabled(true);
@ -5305,6 +5345,10 @@ void AnimationTrackEditor::_update_tracks() {
track_edit->set_in_group(true);
group_sort[base_path]->add_child(track_edit);
AnimationTrackEditGroup *g = Object::cast_to<AnimationTrackEditGroup>(group_sort[base_path]->get_child(0));
ERR_FAIL_NULL_MSG(g, "The first child of this group's VBoxContainer isn't an AnimationTrackEditGroup. Collapsing this animation group may not work.");
g->track_edits.push_back(track_edit);
} else {
track_edit->set_in_group(false);
}
@ -5353,6 +5397,13 @@ void AnimationTrackEditor::_update_tracks() {
for (VBoxContainer *vb : group_containers) {
track_vbox->add_child(vb);
AnimationTrackEditGroup *g = Object::cast_to<AnimationTrackEditGroup>(vb->get_child(0));
if (g) {
for (AnimationTrackEdit *i : g->track_edits) {
i->set_visible(!animation->editor_is_group_folded(g->node_name));
}
}
}
} else {
@ -6399,6 +6450,9 @@ void AnimationTrackEditor::_scroll_input(const Ref<InputEvent> &p_event) {
if (box_selection->is_visible_in_tree()) {
// Only if moved.
for (int i = 0; i < track_edits.size(); i++) {
if (!track_edits[i]->is_visible_in_tree()) {
continue; // Skip collapsed track edits.
}
Rect2 local_rect = box_select_rect;
local_rect.position -= track_edits[i]->get_global_position();
track_edits[i]->append_to_selection(local_rect, mb->is_command_or_control_pressed());

View file

@ -568,6 +568,8 @@ class AnimationMultiTrackKeyEdit;
class AnimationBezierTrackEdit;
class AnimationTrackEditGroup : public Control {
friend class AnimationTrackEditor;
GDCLASS(AnimationTrackEditGroup, Control);
Ref<Texture2D> icon;
Vector2 icon_size;
@ -578,7 +580,7 @@ class AnimationTrackEditGroup : public Control {
AnimationTrackEditor *editor = nullptr;
bool hovered = false;
LocalVector<AnimationTrackEdit *> track_edits;
void _zoom_changed();
protected:

View file

@ -2316,6 +2316,32 @@ void SceneTreeDock::perform_node_renames(Node *p_base, HashMap<Node *, NodePath>
}
}
}
// key.get_path() of p_renames is like:
// /root/@EditorNode@18033/@Panel@14/.../Scene/TheOldName
// value of p_renames is like:
// /root/@EditorNode@18033/@Panel@14/.../Scene/TheNewName
for (const KeyValue<Node *, NodePath> &rename : *p_renames) {
NodePath old_path = rename.key->get_path();
NodePath new_path = rename.value;
Vector<StringName> rel_path = old_path.rel_path_to(new_path).get_names();
StringName old_node_name = rename.key->get_name();
StringName new_node_name = rel_path[rel_path.size() - 1];
anim->editor_set_group_folded(new_node_name, anim->editor_is_group_folded(old_node_name));
anim->editor_set_group_folded(old_node_name, false);
}
if (!anim->get_path().is_resource_file()) {
EditorNode::get_editor_folding().save_scene_folding(
EditorNode::get_singleton()->get_edited_scene(),
EditorNode::get_singleton()->get_edited_scene()->get_scene_file_path());
} else {
EditorNode::get_editor_folding().save_resource_folding(
anim,
anim->get_path());
}
}
}
}

View file

@ -34,6 +34,8 @@
#include "core/io/file_access.h"
#include "editor/file_system/editor_paths.h"
#include "editor/inspector/editor_inspector.h"
#include "scene/animation/animation_mixer.h"
#include "scene/resources/animation.h"
Vector<String> EditorFolding::_get_unfolds(const Object *p_object) {
Vector<String> sections;
@ -55,6 +57,12 @@ void EditorFolding::save_resource_folding(const Ref<Resource> &p_resource, const
Vector<String> unfolds = _get_unfolds(p_resource.ptr());
config->set_value("folding", "sections_unfolded", unfolds);
Ref<Animation> as_anim = p_resource;
if (as_anim.is_valid()) {
Vector<String> folded_groups = _get_animation_folds(as_anim.ptr());
config->set_value("folding", "animation_groups_folded", folded_groups);
}
String file = p_path.get_file() + "-folding-" + p_path.md5_text() + ".cfg";
file = EditorPaths::get_singleton()->get_project_settings_dir().path_join(file);
config->save(file);
@ -86,9 +94,15 @@ void EditorFolding::load_resource_folding(Ref<Resource> p_resource, const String
unfolds = config->get_value("folding", "sections_unfolded");
}
_set_unfolds(p_resource.ptr(), unfolds);
Ref<Animation> anim = p_resource;
if (anim.is_valid() && config->has_section_key("folding", "animation_groups_folded")) {
Vector<String> folded_groups = config->get_value("folding", "animation_groups_folded");
_set_animation_folds(anim.ptr(), folded_groups);
}
}
void EditorFolding::_fill_folds(const Node *p_root, const Node *p_node, Array &p_folds, Array &resource_folds, Array &nodes_folded, HashSet<Ref<Resource>> &resources) {
void EditorFolding::_fill_folds(const Node *p_root, const Node *p_node, Array &p_folds, Array &resource_folds, Array &nodes_folded, HashSet<Ref<Resource>> &resources, HashSet<Ref<Animation>> &animations, Array &anim_groups_folded) {
if (p_root != p_node) {
if (!p_node->get_owner()) {
return; //not owned, bye
@ -108,6 +122,21 @@ void EditorFolding::_fill_folds(const Node *p_root, const Node *p_node, Array &p
p_folds.push_back(unfolds);
}
const AnimationMixer *anim_mixer = Object::cast_to<AnimationMixer>(p_node);
if (anim_mixer) {
List<StringName> anim_names;
anim_mixer->get_animation_list(&anim_names);
for (const StringName &anim_name : anim_names) {
Ref<Animation> anim = anim_mixer->get_animation(anim_name);
if (anim.is_valid() && !animations.has(anim) && !anim->get_path().is_empty() && !anim->get_path().is_resource_file()) {
Vector<String> anim_folds = _get_animation_folds(anim.ptr());
anim_groups_folded.push_back(anim->get_path());
anim_groups_folded.push_back(anim_folds);
animations.insert(anim);
}
}
}
List<PropertyInfo> plist;
p_node->get_property_list(&plist);
for (const PropertyInfo &E : plist) {
@ -125,7 +154,7 @@ void EditorFolding::_fill_folds(const Node *p_root, const Node *p_node, Array &p
}
for (int i = 0; i < p_node->get_child_count(); i++) {
_fill_folds(p_root, p_node->get_child(i), p_folds, resource_folds, nodes_folded, resources);
_fill_folds(p_root, p_node->get_child(i), p_folds, resource_folds, nodes_folded, resources, animations, anim_groups_folded);
}
}
@ -143,11 +172,14 @@ void EditorFolding::save_scene_folding(const Node *p_scene, const String &p_path
Array unfolds, res_unfolds;
HashSet<Ref<Resource>> resources;
Array nodes_folded;
_fill_folds(p_scene, p_scene, unfolds, res_unfolds, nodes_folded, resources);
HashSet<Ref<Animation>> animations;
Array anim_groups_folded;
_fill_folds(p_scene, p_scene, unfolds, res_unfolds, nodes_folded, resources, animations, anim_groups_folded);
config->set_value("folding", "node_unfolds", unfolds);
config->set_value("folding", "resource_unfolds", res_unfolds);
config->set_value("folding", "nodes_folded", nodes_folded);
config->set_value("folding", "animation_groups_folded", anim_groups_folded);
String file = p_path.get_file() + "-folding-" + p_path.md5_text() + ".cfg";
file = EditorPaths::get_singleton()->get_project_settings_dir().path_join(file);
@ -178,9 +210,14 @@ void EditorFolding::load_scene_folding(Node *p_scene, const String &p_path) {
if (config->has_section_key("folding", "nodes_folded")) {
nodes_folded = config->get_value("folding", "nodes_folded");
}
Array animation_groups_folded;
if (config->has_section_key("folding", "animation_groups_folded")) {
animation_groups_folded = config->get_value("folding", "animation_groups_folded");
}
ERR_FAIL_COND(unfolds.size() & 1);
ERR_FAIL_COND(res_unfolds.size() & 1);
ERR_FAIL_COND(animation_groups_folded.size() & 1);
for (int i = 0; i < unfolds.size(); i += 2) {
NodePath path2 = unfolds[i];
@ -210,6 +247,17 @@ void EditorFolding::load_scene_folding(Node *p_scene, const String &p_path) {
node->set_display_folded(true);
}
}
for (int i = 0; i < animation_groups_folded.size(); i += 2) {
String path2 = animation_groups_folded[i];
Ref<Animation> anim = ResourceCache::get_ref(path2);
if (anim.is_null()) {
continue;
}
Vector<String> folded_groups = animation_groups_folded[i + 1];
_set_animation_folds(anim.ptr(), folded_groups);
}
}
bool EditorFolding::has_folding_data(const String &p_path) {
@ -294,3 +342,24 @@ void EditorFolding::unfold_scene(Node *p_scene) {
HashSet<Ref<Resource>> resources;
_do_node_unfolds(p_scene, p_scene, resources);
}
Vector<String> EditorFolding::_get_animation_folds(const Animation *p_animation) {
Vector<String> folded_groups;
folded_groups.resize(p_animation->editor_get_folded_groups().size());
if (folded_groups.size()) {
String *w = folded_groups.ptrw();
int idx = 0;
for (const StringName &group_name : p_animation->editor_get_folded_groups()) {
w[idx++] = group_name;
}
}
return folded_groups;
}
void EditorFolding::_set_animation_folds(Animation *p_animation, const Vector<String> &p_folds) {
p_animation->editor_clear_folded_groups();
for (const String &group_name : p_folds) {
p_animation->editor_add_folded_group(group_name);
}
}

View file

@ -32,15 +32,20 @@
#include "scene/main/node.h"
class Animation;
class EditorFolding {
Vector<String> _get_unfolds(const Object *p_object);
void _set_unfolds(Object *p_object, const Vector<String> &p_unfolds);
void _fill_folds(const Node *p_root, const Node *p_node, Array &p_folds, Array &resource_folds, Array &nodes_folded, HashSet<Ref<Resource>> &resources);
void _fill_folds(const Node *p_root, const Node *p_node, Array &p_folds, Array &resource_folds, Array &nodes_folded, HashSet<Ref<Resource>> &resources, HashSet<Ref<Animation>> &animations, Array &anim_groups_folded);
void _do_object_unfolds(Object *p_object, HashSet<Ref<Resource>> &resources);
void _do_node_unfolds(Node *p_root, Node *p_node, HashSet<Ref<Resource>> &resources);
Vector<String> _get_animation_folds(const Animation *p_animation);
void _set_animation_folds(Animation *p_animation, const Vector<String> &p_unfolds);
public:
void save_resource_folding(const Ref<Resource> &p_resource, const String &p_path);
void load_resource_folding(Ref<Resource> p_resource, const String &p_path);
@ -48,6 +53,9 @@ public:
void save_scene_folding(const Node *p_scene, const String &p_path);
void load_scene_folding(Node *p_scene, const String &p_path);
void save_animation_folding(const Ref<Animation> &p_animation, const String &p_path);
void load_animation_folding(Ref<Animation> p_animation, const String &p_path);
void unfold_scene(Node *p_scene);
bool has_folding_data(const String &p_path);

View file

@ -251,6 +251,10 @@ private:
LocalVector<Track *> tracks;
#ifdef TOOLS_ENABLED
HashSet<StringName> folded_groups;
#endif // TOOLS_ENABLED
template <typename T, typename V>
int _insert(double p_time, T &p_keys, const V &p_value);
@ -539,6 +543,21 @@ public:
void optimize(real_t p_allowed_velocity_err = 0.01, real_t p_allowed_angular_err = 0.01, int p_precision = 3);
void compress(uint32_t p_page_size = 8192, uint32_t p_fps = 120, float p_split_tolerance = 4.0); // 4.0 seems to be the split tolerance sweet spot from many tests.
#ifdef TOOLS_ENABLED
const HashSet<StringName> &editor_get_folded_groups() const { return folded_groups; }
void editor_clear_folded_groups() { folded_groups.clear(); }
void editor_add_folded_group(const StringName &p_group_name) { folded_groups.insert(p_group_name); }
void editor_remove_folded_group(const StringName &p_group_name) { folded_groups.erase(p_group_name); }
bool editor_is_group_folded(const StringName &p_group_name) const { return folded_groups.has(p_group_name); }
void editor_set_group_folded(const StringName &p_group_name, bool p_folded) {
if (p_folded) {
editor_add_folded_group(p_group_name);
} else {
editor_remove_folded_group(p_group_name);
}
}
#endif // TOOLS_ENABLED
// Helper functions for Rotation.
static double interpolate_via_rest(double p_from, double p_to, double p_weight, double p_rest = 0.0); // Deterministic slerp to prevent to cross the inverted rest axis.
static Quaternion interpolate_via_rest(const Quaternion &p_from, const Quaternion &p_to, real_t p_weight, const Quaternion &p_rest = Quaternion()); // Deterministic slerp to prevent to cross the inverted rest axis.