From 5c4500a23610d23c82513f506ad8a7e81a7319d1 Mon Sep 17 00:00:00 2001 From: Malcolm Anderson Date: Tue, 2 Dec 2025 15:46:49 -0800 Subject: [PATCH] Allow animation groups to be collapsed by clicking disclosure chevron on left Save group collapsed state during editing session Save collapsed groups in Animation resource so they persist across sessions Update editor/animation/animation_track_editor.h Remove data duplication and unnecessary method Prevent error about negative-sized Rect2 Move animation group folding to editor cfg files Clean up length of some lines of code Keep fold state of groups when renamed Update scene/resources/animation.h Make fold_area_rect calculation more accurate Improve animation includes Store animation fold state in scene folding file Fix animation fold saving for independent resource animations Apply suggestions from code review Update scene/resources/animation.h Co-authored-by: A Thousand Ships <96648715+AThousandShips@users.noreply.github.com> Co-authored-by: Mikael Hermansson Co-authored-by: Tomasz Chabora Co-authored-by: Thaddeus Crews --- editor/animation/animation_track_editor.cpp | 68 +++++++++++++++++-- editor/animation/animation_track_editor.h | 4 +- editor/docks/scene_tree_dock.cpp | 26 +++++++ editor/settings/editor_folding.cpp | 75 ++++++++++++++++++++- editor/settings/editor_folding.h | 10 ++- scene/resources/animation.h | 19 ++++++ 6 files changed, 190 insertions(+), 12 deletions(-) diff --git a/editor/animation/animation_track_editor.cpp b/editor/animation/animation_track_editor.cpp index f984c0e456..203c7e1c79 100644 --- a/editor/animation/animation_track_editor.cpp +++ b/editor/animation/animation_track_editor.cpp @@ -3892,7 +3892,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 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); @@ -3919,14 +3926,41 @@ void AnimationTrackEditGroup::gui_input(const Ref &p_event) { Ref 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 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); + } } } } @@ -4060,6 +4094,12 @@ void AnimationTrackEditor::set_animation(const Ref &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); @@ -5286,6 +5326,10 @@ void AnimationTrackEditor::_update_tracks() { track_edit->set_in_group(true); group_sort[base_path]->add_child(track_edit); + AnimationTrackEditGroup *g = Object::cast_to(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); } @@ -5334,6 +5378,13 @@ void AnimationTrackEditor::_update_tracks() { for (VBoxContainer *vb : group_containers) { track_vbox->add_child(vb); + + AnimationTrackEditGroup *g = Object::cast_to(vb->get_child(0)); + if (g) { + for (AnimationTrackEdit *i : g->track_edits) { + i->set_visible(!animation->editor_is_group_folded(g->node_name)); + } + } } } else { @@ -6380,6 +6431,9 @@ void AnimationTrackEditor::_scroll_input(const Ref &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()); diff --git a/editor/animation/animation_track_editor.h b/editor/animation/animation_track_editor.h index d32e33f2ee..f31e14d091 100644 --- a/editor/animation/animation_track_editor.h +++ b/editor/animation/animation_track_editor.h @@ -568,6 +568,8 @@ class AnimationMultiTrackKeyEdit; class AnimationBezierTrackEdit; class AnimationTrackEditGroup : public Control { + friend class AnimationTrackEditor; + GDCLASS(AnimationTrackEditGroup, Control); Ref icon; Vector2 icon_size; @@ -578,7 +580,7 @@ class AnimationTrackEditGroup : public Control { AnimationTrackEditor *editor = nullptr; bool hovered = false; - + LocalVector track_edits; void _zoom_changed(); protected: diff --git a/editor/docks/scene_tree_dock.cpp b/editor/docks/scene_tree_dock.cpp index 37743a5298..98c63f47b9 100644 --- a/editor/docks/scene_tree_dock.cpp +++ b/editor/docks/scene_tree_dock.cpp @@ -2316,6 +2316,32 @@ void SceneTreeDock::perform_node_renames(Node *p_base, HashMap } } } + + // 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 &rename : *p_renames) { + NodePath old_path = rename.key->get_path(); + NodePath new_path = rename.value; + Vector 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()); + } } } } diff --git a/editor/settings/editor_folding.cpp b/editor/settings/editor_folding.cpp index f65948c2fc..0d93d6d3be 100644 --- a/editor/settings/editor_folding.cpp +++ b/editor/settings/editor_folding.cpp @@ -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 EditorFolding::_get_unfolds(const Object *p_object) { Vector sections; @@ -55,6 +57,12 @@ void EditorFolding::save_resource_folding(const Ref &p_resource, const Vector unfolds = _get_unfolds(p_resource.ptr()); config->set_value("folding", "sections_unfolded", unfolds); + Ref as_anim = p_resource; + if (as_anim.is_valid()) { + Vector 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 p_resource, const String unfolds = config->get_value("folding", "sections_unfolded"); } _set_unfolds(p_resource.ptr(), unfolds); + + Ref anim = p_resource; + if (anim.is_valid() && config->has_section_key("folding", "animation_groups_folded")) { + Vector 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> &resources) { +void EditorFolding::_fill_folds(const Node *p_root, const Node *p_node, Array &p_folds, Array &resource_folds, Array &nodes_folded, HashSet> &resources, HashSet> &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(p_node); + if (anim_mixer) { + List anim_names; + anim_mixer->get_animation_list(&anim_names); + for (const StringName &anim_name : anim_names) { + Ref 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 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 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> resources; Array nodes_folded; - _fill_folds(p_scene, p_scene, unfolds, res_unfolds, nodes_folded, resources); + HashSet> 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 anim = ResourceCache::get_ref(path2); + if (anim.is_null()) { + continue; + } + + Vector 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> resources; _do_node_unfolds(p_scene, p_scene, resources); } + +Vector EditorFolding::_get_animation_folds(const Animation *p_animation) { + Vector 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 &p_folds) { + p_animation->editor_clear_folded_groups(); + for (const String &group_name : p_folds) { + p_animation->editor_add_folded_group(group_name); + } +} diff --git a/editor/settings/editor_folding.h b/editor/settings/editor_folding.h index cc41dbd71a..fc0a3cdbf7 100644 --- a/editor/settings/editor_folding.h +++ b/editor/settings/editor_folding.h @@ -32,15 +32,20 @@ #include "scene/main/node.h" +class Animation; + class EditorFolding { Vector _get_unfolds(const Object *p_object); void _set_unfolds(Object *p_object, const Vector &p_unfolds); - void _fill_folds(const Node *p_root, const Node *p_node, Array &p_folds, Array &resource_folds, Array &nodes_folded, HashSet> &resources); + void _fill_folds(const Node *p_root, const Node *p_node, Array &p_folds, Array &resource_folds, Array &nodes_folded, HashSet> &resources, HashSet> &animations, Array &anim_groups_folded); void _do_object_unfolds(Object *p_object, HashSet> &resources); void _do_node_unfolds(Node *p_root, Node *p_node, HashSet> &resources); + Vector _get_animation_folds(const Animation *p_animation); + void _set_animation_folds(Animation *p_animation, const Vector &p_unfolds); + public: void save_resource_folding(const Ref &p_resource, const String &p_path); void load_resource_folding(Ref 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 &p_animation, const String &p_path); + void load_animation_folding(Ref p_animation, const String &p_path); + void unfold_scene(Node *p_scene); bool has_folding_data(const String &p_path); diff --git a/scene/resources/animation.h b/scene/resources/animation.h index 71bc1ec6a3..a2725f294e 100644 --- a/scene/resources/animation.h +++ b/scene/resources/animation.h @@ -251,6 +251,10 @@ private: LocalVector tracks; +#ifdef TOOLS_ENABLED + HashSet folded_groups; +#endif // TOOLS_ENABLED + template 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 &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.