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 <mikael@hermansson.io>
Co-authored-by: Tomasz Chabora <kobewi4e@gmail.com>
Co-authored-by: Thaddeus Crews <repiteo@outlook.com>
This commit is contained in:
Malcolm Anderson 2025-12-02 15:46:49 -08:00
parent a3e84cc2af
commit 5c4500a236
6 changed files with 190 additions and 12 deletions

View file

@ -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<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);
@ -3919,14 +3926,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);
}
}
}
}
@ -4060,6 +4094,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);
@ -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<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);
}
@ -5334,6 +5378,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 {
@ -6380,6 +6431,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.