Browse Source

Merge pull request #87078 from Hilderin/animation-autofit

Add auto fit timeline and bezier scale on animation editor
Rémi Verschelde 1 year ago
parent
commit
0ebcba8583

+ 94 - 35
editor/animation_bezier_editor.cpp

@@ -266,23 +266,11 @@ void AnimationBezierTrackEdit::_notification(int p_what) {
 			RBMap<String, Vector<int>> track_indices;
 			int track_count = animation->get_track_count();
 			for (int i = 0; i < track_count; ++i) {
-				if (animation->track_get_type(i) != Animation::TrackType::TYPE_BEZIER) {
+				if (!_is_track_displayed(i)) {
 					continue;
 				}
 
 				String base_path = animation->track_get_path(i);
-				if (is_filtered) {
-					if (root && root->has_node(base_path)) {
-						Node *node = root->get_node(base_path);
-						if (!node) {
-							continue; // No node, no filter.
-						}
-						if (!EditorNode::get_singleton()->get_editor_selection()->is_selected(node)) {
-							continue; // Skip track due to not selected.
-						}
-					}
-				}
-
 				int end = base_path.find(":");
 				if (end != -1) {
 					base_path = base_path.substr(0, end + 1);
@@ -520,28 +508,11 @@ void AnimationBezierTrackEdit::_notification(int p_what) {
 				float scale = timeline->get_zoom_scale();
 
 				for (int i = 0; i < track_count; ++i) {
-					if (animation->track_get_type(i) != Animation::TrackType::TYPE_BEZIER || hidden_tracks.has(i)) {
-						continue;
-					}
-
-					if (hidden_tracks.has(i) || locked_tracks.has(i)) {
+					if (!_is_track_curves_displayed(i) || locked_tracks.has(i)) {
 						continue;
 					}
 
 					int key_count = animation->track_get_key_count(i);
-					String path = animation->track_get_path(i);
-
-					if (is_filtered) {
-						if (root && root->has_node(path)) {
-							Node *node = root->get_node(path);
-							if (!node) {
-								continue; // No node, no filter.
-							}
-							if (!EditorNode::get_singleton()->get_editor_selection()->is_selected(node)) {
-								continue; // Skip track due to not selected.
-							}
-						}
-					}
 
 					for (int j = 0; j < key_count; ++j) {
 						float offset = animation->track_get_key_time(i, j);
@@ -648,6 +619,43 @@ void AnimationBezierTrackEdit::_notification(int p_what) {
 	}
 }
 
+// Check if a track is displayed in the bezier editor (track type = bezier and track not filtered).
+bool AnimationBezierTrackEdit::_is_track_displayed(int p_track_index) {
+	if (animation->track_get_type(p_track_index) != Animation::TrackType::TYPE_BEZIER) {
+		return false;
+	}
+
+	if (is_filtered) {
+		String path = animation->track_get_path(p_track_index);
+		if (root && root->has_node(path)) {
+			Node *node = root->get_node(path);
+			if (!node) {
+				return false; // No node, no filter.
+			}
+			if (!EditorNode::get_singleton()->get_editor_selection()->is_selected(node)) {
+				return false; // Skip track due to not selected.
+			}
+		}
+	}
+
+	return true;
+}
+
+// Check if the curves for a track are displayed in the editor (not hidden). Includes the check on the track visibility.
+bool AnimationBezierTrackEdit::_is_track_curves_displayed(int p_track_index) {
+	//Is the track is visible in the editor?
+	if (!_is_track_displayed(p_track_index)) {
+		return false;
+	}
+
+	//And curves visible?
+	if (hidden_tracks.has(p_track_index)) {
+		return false;
+	}
+
+	return true;
+}
+
 Ref<Animation> AnimationBezierTrackEdit::get_animation() const {
 	return animation;
 }
@@ -741,6 +749,60 @@ void AnimationBezierTrackEdit::set_filtered(bool p_filtered) {
 	queue_redraw();
 }
 
+void AnimationBezierTrackEdit::auto_fit_vertically() {
+	int track_count = animation->get_track_count();
+	real_t minimum_value = INFINITY;
+	real_t maximum_value = -INFINITY;
+
+	int nb_track_visible = 0;
+	for (int i = 0; i < track_count; ++i) {
+		if (!_is_track_curves_displayed(i) || locked_tracks.has(i)) {
+			continue;
+		}
+
+		int key_count = animation->track_get_key_count(i);
+
+		for (int j = 0; j < key_count; ++j) {
+			real_t value = animation->bezier_track_get_key_value(i, j);
+
+			minimum_value = MIN(value, minimum_value);
+			maximum_value = MAX(value, maximum_value);
+
+			// We also want to includes the handles...
+			Vector2 in_vec = animation->bezier_track_get_key_in_handle(i, j);
+			Vector2 out_vec = animation->bezier_track_get_key_out_handle(i, j);
+
+			minimum_value = MIN(value + in_vec.y, minimum_value);
+			maximum_value = MAX(value + in_vec.y, maximum_value);
+			minimum_value = MIN(value + out_vec.y, minimum_value);
+			maximum_value = MAX(value + out_vec.y, maximum_value);
+		}
+
+		nb_track_visible++;
+	}
+
+	if (nb_track_visible == 0) {
+		// No visible track... we will not adjust the vertical zoom
+		return;
+	}
+
+	if (Math::is_finite(minimum_value) && Math::is_finite(maximum_value)) {
+		_zoom_vertically(minimum_value, maximum_value);
+		queue_redraw();
+	}
+}
+
+void AnimationBezierTrackEdit::_zoom_vertically(real_t p_minimum_value, real_t p_maximum_value) {
+	real_t target_height = p_maximum_value - p_minimum_value;
+	if (target_height <= CMP_EPSILON) {
+		timeline_v_scroll = p_maximum_value;
+		return;
+	}
+
+	timeline_v_scroll = (p_maximum_value + p_minimum_value) / 2.0;
+	timeline_v_zoom = target_height / ((get_size().height - timeline->get_size().height) * 0.9);
+}
+
 void AnimationBezierTrackEdit::_zoom_changed() {
 	queue_redraw();
 	play_position->queue_redraw();
@@ -931,10 +993,7 @@ void AnimationBezierTrackEdit::gui_input(const Ref<InputEvent> &p_event) {
 			}
 
 			if (Math::is_finite(minimum_value) && Math::is_finite(maximum_value)) {
-				timeline_v_scroll = (maximum_value + minimum_value) / 2.0;
-				if (maximum_value - minimum_value > CMP_EPSILON) {
-					timeline_v_zoom = (maximum_value - minimum_value) / ((get_size().height - timeline->get_size().height) * 0.9);
-				}
+				_zoom_vertically(minimum_value, maximum_value);
 			}
 
 			queue_redraw();

+ 4 - 0
editor/animation_bezier_editor.h

@@ -101,6 +101,8 @@ class AnimationBezierTrackEdit : public Control {
 	void _menu_selected(int p_index);
 
 	void _play_position_draw();
+	bool _is_track_displayed(int p_track_index);
+	bool _is_track_curves_displayed(int p_track_index);
 
 	Vector2 insert_at_pos;
 
@@ -188,6 +190,7 @@ class AnimationBezierTrackEdit : public Control {
 	void _draw_track(int p_track, const Color &p_color);
 
 	float _bezier_h_to_pixel(float p_h);
+	void _zoom_vertically(real_t p_minimum_value, real_t p_maximum_value);
 
 protected:
 	static void _bind_methods();
@@ -208,6 +211,7 @@ public:
 	void set_editor(AnimationTrackEditor *p_editor);
 	void set_root(Node *p_root);
 	void set_filtered(bool p_filtered);
+	void auto_fit_vertically();
 
 	void set_play_position(real_t p_pos);
 	void update_play_position();

+ 102 - 1
editor/animation_track_editor.cpp

@@ -1305,7 +1305,11 @@ void AnimationTimelineEdit::_zoom_changed(double) {
 }
 
 float AnimationTimelineEdit::get_zoom_scale() const {
-	float zv = zoom->get_max() - zoom->get_value();
+	return _get_zoom_scale(zoom->get_value());
+}
+
+float AnimationTimelineEdit::_get_zoom_scale(double p_zoom_value) const {
+	float zv = zoom->get_max() - p_zoom_value;
 	if (zv < 1) {
 		zv = 1.0 - zv;
 		return Math::pow(1.0f + zv, 8.0f) * 100;
@@ -1633,6 +1637,68 @@ void AnimationTimelineEdit::set_zoom(Range *p_zoom) {
 	zoom->connect("value_changed", callable_mp(this, &AnimationTimelineEdit::_zoom_changed));
 }
 
+void AnimationTimelineEdit::auto_fit() {
+	if (!animation.is_valid()) {
+		return;
+	}
+
+	float anim_end = animation->get_length();
+	float anim_start = 0;
+
+	// Search for keyframe outside animation boundaries to include keyframes before animation start and after animation length.
+	int track_count = animation->get_track_count();
+	for (int track = 0; track < track_count; ++track) {
+		for (int i = 0; i < animation->track_get_key_count(track); i++) {
+			float key_time = animation->track_get_key_time(track, i);
+			if (key_time > anim_end) {
+				anim_end = key_time;
+			}
+			if (key_time < anim_start) {
+				anim_start = key_time;
+			}
+		}
+	}
+
+	float anim_length = anim_end - anim_start;
+	int timeline_width_pixels = get_size().width - get_buttons_width() - get_name_limit();
+
+	// I want a little buffer at the end... (5% looks nice and we should keep some space for the bezier handles)
+	timeline_width_pixels *= 0.95;
+
+	// The technique is to reuse the _get_zoom_scale function directly to be sure that the auto_fit is always calculated
+	// the same way as the zoom slider. It's a little bit more calculation then doing the inverse of get_zoom_scale but
+	// it's really easier to understand and should always be accurate.
+	float new_zoom = zoom->get_max();
+	while (true) {
+		double test_zoom_scale = _get_zoom_scale(new_zoom);
+
+		if (anim_length * test_zoom_scale <= timeline_width_pixels) {
+			// It fits...
+			break;
+		}
+
+		new_zoom -= zoom->get_step();
+
+		if (new_zoom <= zoom->get_min()) {
+			new_zoom = zoom->get_min();
+			break;
+		}
+	}
+
+	// Horizontal scroll to get_min which should include keyframes that are before the animation start.
+	hscroll->set_value(hscroll->get_min());
+	// Set the zoom value... the signal value_changed will be emitted and the timeline will be refreshed correctly!
+	zoom->set_value(new_zoom);
+	// The new zoom value must be applied correctly so the scrollbar are updated before we move the scrollbar to
+	// the beginning of the animation, hence the call deferred.
+	callable_mp(this, &AnimationTimelineEdit::_scroll_to_start).call_deferred();
+}
+
+void AnimationTimelineEdit::_scroll_to_start() {
+	// Horizontal scroll to get_min which should include keyframes that are before the animation start.
+	hscroll->set_value(hscroll->get_min());
+}
+
 void AnimationTimelineEdit::set_track_edit(AnimationTrackEdit *p_track_edit) {
 	track_edit = p_track_edit;
 }
@@ -3446,6 +3512,8 @@ void AnimationTrackEditor::set_animation(const Ref<Animation> &p_anim, bool p_re
 		step->set_read_only(false);
 		snap->set_disabled(false);
 		snap_mode->set_disabled(false);
+		auto_fit->set_disabled(false);
+		auto_fit_bezier->set_disabled(false);
 
 		imported_anim_warning->hide();
 		for (int i = 0; i < animation->get_track_count(); i++) {
@@ -3466,6 +3534,8 @@ void AnimationTrackEditor::set_animation(const Ref<Animation> &p_anim, bool p_re
 		snap->set_disabled(true);
 		snap_mode->set_disabled(true);
 		bezier_edit_icon->set_disabled(true);
+		auto_fit->set_disabled(true);
+		auto_fit_bezier->set_disabled(true);
 	}
 }
 
@@ -4763,6 +4833,8 @@ void AnimationTrackEditor::_notification(int p_what) {
 			inactive_player_warning->set_icon(get_editor_theme_icon(SNAME("NodeWarning")));
 			main_panel->add_theme_style_override("panel", get_theme_stylebox(SNAME("panel"), SNAME("Tree")));
 			edit->get_popup()->set_item_icon(edit->get_popup()->get_item_index(EDIT_APPLY_RESET), get_editor_theme_icon(SNAME("Reload")));
+			auto_fit->set_icon(get_editor_theme_icon(SNAME("AnimationAutoFit")));
+			auto_fit_bezier->set_icon(get_editor_theme_icon(SNAME("AnimationAutoFitBezier")));
 		} break;
 
 		case NOTIFICATION_READY: {
@@ -5617,6 +5689,8 @@ void AnimationTrackEditor::_cancel_bezier_edit() {
 	bezier_edit->hide();
 	scroll->show();
 	bezier_edit_icon->set_pressed(false);
+	auto_fit->show();
+	auto_fit_bezier->hide();
 }
 
 void AnimationTrackEditor::_bezier_edit(int p_for_track) {
@@ -5625,6 +5699,8 @@ void AnimationTrackEditor::_bezier_edit(int p_for_track) {
 	bezier_edit->set_animation_and_track(animation, p_for_track, read_only);
 	scroll->hide();
 	bezier_edit->show();
+	auto_fit->hide();
+	auto_fit_bezier->show();
 	// Search everything within the track and curve - edit it.
 }
 
@@ -6865,6 +6941,18 @@ bool AnimationTrackEditor::is_grouping_tracks() {
 	return !view_group->is_pressed();
 }
 
+void AnimationTrackEditor::_auto_fit() {
+	timeline->auto_fit();
+}
+
+void AnimationTrackEditor::_auto_fit_bezier() {
+	timeline->auto_fit();
+
+	if (bezier_edit->is_visible()) {
+		bezier_edit->auto_fit_vertically();
+	}
+}
+
 void AnimationTrackEditor::_selection_changed() {
 	if (selected_filter->is_pressed()) {
 		_update_tracks(); // Needs updatin.
@@ -7179,6 +7267,19 @@ AnimationTrackEditor::AnimationTrackEditor() {
 	bottom_hb->add_child(zoom);
 	timeline->set_zoom(zoom);
 
+	auto_fit = memnew(Button);
+	auto_fit->set_flat(true);
+	auto_fit->connect("pressed", callable_mp(this, &AnimationTrackEditor::_auto_fit));
+	auto_fit->set_shortcut(ED_SHORTCUT("animation_editor/auto_fit", TTR("Fit to panel"), KeyModifierMask::ALT | Key::F));
+	bottom_hb->add_child(auto_fit);
+
+	auto_fit_bezier = memnew(Button);
+	auto_fit_bezier->set_flat(true);
+	auto_fit_bezier->set_visible(false);
+	auto_fit_bezier->connect("pressed", callable_mp(this, &AnimationTrackEditor::_auto_fit_bezier));
+	auto_fit_bezier->set_shortcut(ED_SHORTCUT("animation_editor/auto_fit", TTR("Fit to panel"), KeyModifierMask::ALT | Key::F));
+	bottom_hb->add_child(auto_fit_bezier);
+
 	edit = memnew(MenuButton);
 	edit->set_shortcut_context(this);
 	edit->set_text(TTR("Edit"));

+ 9 - 0
editor/animation_track_editor.h

@@ -182,6 +182,9 @@ class AnimationTimelineEdit : public Range {
 	virtual void gui_input(const Ref<InputEvent> &p_event) override;
 	void _track_added(int p_track);
 
+	float _get_zoom_scale(double p_zoom_value) const;
+	void _scroll_to_start();
+
 protected:
 	static void _bind_methods();
 	void _notification(int p_what);
@@ -197,6 +200,7 @@ public:
 	void set_track_edit(AnimationTrackEdit *p_track_edit);
 	void set_zoom(Range *p_zoom);
 	Range *get_zoom() const { return zoom; }
+	void auto_fit();
 
 	void set_play_position(float p_pos);
 	float get_play_position() const;
@@ -404,6 +408,8 @@ class AnimationTrackEditor : public VBoxContainer {
 	Button *snap = nullptr;
 	Button *bezier_edit_icon = nullptr;
 	OptionButton *snap_mode = nullptr;
+	Button *auto_fit = nullptr;
+	Button *auto_fit_bezier = nullptr;
 
 	Button *imported_anim_warning = nullptr;
 	void _show_imported_anim_warning();
@@ -591,6 +597,9 @@ class AnimationTrackEditor : public VBoxContainer {
 	Button *view_group = nullptr;
 	Button *selected_filter = nullptr;
 
+	void _auto_fit();
+	void _auto_fit_bezier();
+
 	void _selection_changed();
 
 	ConfirmationDialog *track_copy_dialog = nullptr;

+ 2 - 0
editor/icons/AnimationAutoFit.svg

@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="m15.477 0.99862v14h-2.1017v-14zm-14.954 14.003v-14h2.1017v14zm11.946-6.823-3.5-3v6zm-8.9343 0.023985 3.5 3v-2h2.1464l0.00376-2h-2.1501v-2zm4.6005-7.0028c8.9077 15.09 8.9077 15.09 0 0zm-0.23085 14.003c-8.9077-15.09-8.9077-15.09 0 0z" fill="#e0e0e0" stroke-width="1.0251"/></svg>

+ 2 - 0
editor/icons/AnimationAutoFitBezier.svg

@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="m12.469 8.1784-3.5-3v6zm-8.9343 0.023985 3.5 3v-2h2.1464l0.00376-2h-2.1501v-2zm9.4532 0.53338c-10.763 8.9077-10.763 8.9077 0 0zm0 7h-9.9859v-2h9.9859zm-9.9806-8.5564c10.763-8.9077 10.763-8.9077 0 0zm0-7h9.9859v2h-9.9859zm5.4684 2.8277c8.9077 10.763 8.9077 10.763 0 0zm7 0v9.9859h-2v-9.9859zm-7.8862 9.9859c-8.9077-10.763-8.9077-10.763 0 0zm-7 0v-9.9859h2v9.9859z" fill="#e0e0e0"/></svg>