Jelajahi Sumber

Add selection box movement/scaling to the animation bezier editor

Michael Alexsander 8 bulan lalu
induk
melakukan
14c42162ae
2 mengubah file dengan 354 tambahan dan 34 penghapusan
  1. 344 33
      editor/animation_bezier_editor.cpp
  2. 10 1
      editor/animation_bezier_editor.h

+ 344 - 33
editor/animation_bezier_editor.cpp

@@ -60,8 +60,12 @@ void AnimationBezierTrackEdit::_draw_track(int p_track, const Color &p_color) {
 
 	for (int i = 0; i < animation->track_get_key_count(p_track); i++) {
 		real_t ofs = animation->track_get_key_time(p_track, i);
-		if (moving_selection && selection.has(IntPair(p_track, i))) {
-			ofs += moving_selection_offset.x;
+		if (selection.has(IntPair(p_track, i))) {
+			if (moving_selection) {
+				ofs += moving_selection_offset.x;
+			} else if (scaling_selection) {
+				ofs += -scaling_selection_offset.x + (ofs - scaling_selection_pivot.x) * (scaling_selection_scale.x - 1);
+			}
 		}
 
 		key_order[ofs] = i;
@@ -83,9 +87,14 @@ void AnimationBezierTrackEdit::_draw_track(int p_track, const Color &p_color) {
 			out_handle = moving_handle_right;
 		}
 
-		if (moving_selection && selection.has(IntPair(p_track, i))) {
-			offset += moving_selection_offset.x;
-			height += moving_selection_offset.y;
+		if (selection.has(IntPair(p_track, i))) {
+			if (moving_selection) {
+				offset += moving_selection_offset.x;
+				height += moving_selection_offset.y;
+			} else if (scaling_selection) {
+				offset += -scaling_selection_offset.x + (offset - scaling_selection_pivot.x) * (scaling_selection_scale.x - 1);
+				height += -scaling_selection_offset.y + (height - scaling_selection_pivot.y) * (scaling_selection_scale.y - 1);
+			}
 		}
 
 		out_handle += Vector2(offset, height);
@@ -97,9 +106,14 @@ void AnimationBezierTrackEdit::_draw_track(int p_track, const Color &p_color) {
 			in_handle = moving_handle_left;
 		}
 
-		if (moving_selection && selection.has(IntPair(p_track, i_n))) {
-			offset_n += moving_selection_offset.x;
-			height_n += moving_selection_offset.y;
+		if (selection.has(IntPair(p_track, i_n))) {
+			if (moving_selection) {
+				offset_n += moving_selection_offset.x;
+				height_n += moving_selection_offset.y;
+			} else if (scaling_selection) {
+				offset_n += -scaling_selection_offset.x + (offset_n - scaling_selection_pivot.x) * (scaling_selection_scale.x - 1);
+				height_n += -scaling_selection_offset.y + (height_n - scaling_selection_pivot.y) * (scaling_selection_scale.y - 1);
+			}
 		}
 
 		in_handle += Vector2(offset_n, height_n);
@@ -505,29 +519,46 @@ void AnimationBezierTrackEdit::_notification(int p_what) {
 				}
 			}
 
+			const bool draw_selection_handles = selection.size() > 1;
+			LocalVector<Point2> selected_pos;
+
 			// Draw editor handles.
 			{
 				edit_points.clear();
 				float scale = timeline->get_zoom_scale();
 
 				for (int i = 0; i < track_count; ++i) {
-					if (!_is_track_curves_displayed(i) || locked_tracks.has(i)) {
+					bool draw_track = _is_track_curves_displayed(i) && !locked_tracks.has(i);
+					if (!draw_selection_handles && !draw_track) {
 						continue;
 					}
 
 					int key_count = animation->track_get_key_count(i);
-
 					for (int j = 0; j < key_count; ++j) {
 						float offset = animation->track_get_key_time(i, j);
 						float value = animation->bezier_track_get_key_value(i, j);
-
-						if (moving_selection && selection.has(IntPair(i, j))) {
-							offset += moving_selection_offset.x;
-							value += moving_selection_offset.y;
+						bool is_selected = selection.has(IntPair(i, j));
+
+						if (is_selected) {
+							if (moving_selection) {
+								offset += moving_selection_offset.x;
+								value += moving_selection_offset.y;
+							} else if (scaling_selection) {
+								offset += -scaling_selection_offset.x + (offset - scaling_selection_pivot.x) * (scaling_selection_scale.x - 1);
+								value += -scaling_selection_offset.y + (value - scaling_selection_pivot.y) * (scaling_selection_scale.y - 1);
+							}
 						}
 
 						Vector2 pos((offset - timeline->get_value()) * scale + limit, _bezier_h_to_pixel(value));
 
+						if (draw_selection_handles && is_selected) {
+							selected_pos.push_back(pos);
+						}
+
+						if (!draw_track) {
+							continue;
+						}
+
 						Vector2 in_vec = animation->bezier_track_get_key_in_handle(i, j);
 
 						if ((moving_handle == 1 || moving_handle == -1) && moving_handle_track == i && moving_handle_key == j) {
@@ -543,7 +574,7 @@ void AnimationBezierTrackEdit::_notification(int p_what) {
 
 						Vector2 pos_out(((offset + out_vec.x) - timeline->get_value()) * scale + limit, _bezier_h_to_pixel(value + out_vec.y));
 
-						if (i == selected_track || selection.has(IntPair(i, j))) {
+						if (i == selected_track || is_selected) {
 							_draw_line_clipped(pos, pos_in, accent, limit, right_limit);
 							_draw_line_clipped(pos, pos_out, accent, limit, right_limit);
 						}
@@ -554,7 +585,7 @@ void AnimationBezierTrackEdit::_notification(int p_what) {
 						if (pos.x >= limit && pos.x <= right_limit) {
 							ep.point_rect.position = (pos - bezier_icon->get_size() / 2.0).floor();
 							ep.point_rect.size = bezier_icon->get_size();
-							if (selection.has(IntPair(i, j))) {
+							if (is_selected) {
 								draw_texture(selected_icon, ep.point_rect.position);
 								draw_string(font, ep.point_rect.position + Vector2(8, -font->get_height(font_size) - 8), TTR("Time:") + " " + TS->format_number(rtos(Math::snapped(offset, 0.0001))), HORIZONTAL_ALIGNMENT_LEFT, -1, font_size, accent);
 								draw_string(font, ep.point_rect.position + Vector2(8, -8), TTR("Value:") + " " + TS->format_number(rtos(Math::snapped(value, 0.001))), HORIZONTAL_ALIGNMENT_LEFT, -1, font_size, accent);
@@ -569,7 +600,7 @@ void AnimationBezierTrackEdit::_notification(int p_what) {
 						}
 						ep.point_rect = ep.point_rect.grow(ep.point_rect.size.width * 0.5);
 
-						if (i == selected_track || selection.has(IntPair(i, j))) {
+						if (i == selected_track || is_selected) {
 							if (animation->bezier_track_get_key_handle_mode(i, j) != Animation::HANDLE_MODE_LINEAR) {
 								if (pos_in.x >= limit && pos_in.x <= right_limit) {
 									ep.in_rect.position = (pos_in - bezier_handle_icon->get_size() / 2.0).floor();
@@ -600,6 +631,34 @@ void AnimationBezierTrackEdit::_notification(int p_what) {
 				}
 			}
 
+			selection_rect = Rect2();
+			selection_handles_rect = Rect2();
+			// Draw scale handles.
+			if (draw_selection_handles) {
+				selection_rect.position = selected_pos[0];
+				selected_pos.remove_at(0);
+				for (const Point2 &pos : selected_pos) {
+					selection_rect = selection_rect.expand(pos);
+				}
+
+				const int outer_ofs = Math::round(12 * EDSCALE);
+				const int inner_ofs = Math::round(outer_ofs / 2.0);
+
+				// Draw horizontal handles.
+				if (selection_rect.size.height > CMP_EPSILON) {
+					_draw_line_clipped(selection_rect.position - Vector2(inner_ofs, inner_ofs), selection_rect.position + Vector2(selection_rect.size.width + inner_ofs, -inner_ofs), accent, limit, right_limit);
+					_draw_line_clipped(selection_rect.position + Vector2(-inner_ofs, selection_rect.size.height + inner_ofs), selection_rect.position + selection_rect.size + Vector2(inner_ofs, inner_ofs), accent, limit, right_limit);
+				}
+				// Draw vertical handles.
+				if (selection_rect.size.width > CMP_EPSILON) {
+					_draw_line_clipped(selection_rect.position - Vector2(inner_ofs, inner_ofs), selection_rect.position + Vector2(-inner_ofs, selection_rect.size.height + inner_ofs), accent, limit, right_limit);
+					_draw_line_clipped(selection_rect.position + Vector2(selection_rect.size.width + inner_ofs, -inner_ofs), selection_rect.position + selection_rect.size + Vector2(inner_ofs, inner_ofs), accent, limit, right_limit);
+				}
+
+				selection_handles_rect.position = selection_rect.position - Vector2(outer_ofs, outer_ofs);
+				selection_handles_rect.size = selection_rect.size + Vector2(outer_ofs, outer_ofs) * 2;
+			}
+
 			if (box_selecting) {
 				Vector2 bs_from = box_selection_from;
 				Vector2 bs_to = box_selection_to;
@@ -1192,15 +1251,17 @@ void AnimationBezierTrackEdit::gui_input(const Ref<InputEvent> &p_event) {
 			}
 		}
 
+		// Check this first, to allow manipulating key handles while ignoring keyframes before scaling/moving.
+		bool inside_selection_handles_rect = !read_only && selection_handles_rect.has_point(mb->get_position());
+
 		// First, check keyframe.
 		// Command/Control makes it ignore the keyframe, so control point editors can be force-edited.
-		if (!mb->is_command_or_control_pressed()) {
+		if (!inside_selection_handles_rect && !mb->is_command_or_control_pressed()) {
 			if (_try_select_at_ui_pos(mb->get_position(), mb->is_shift_pressed(), true)) {
 				return;
 			}
 		}
-
-		// Second, check handles.
+		// Second, check key handles.
 		for (int i = 0; i < edit_points.size(); i++) {
 			if (!read_only) {
 				if (edit_points[i].in_rect.has_point(mb->get_position())) {
@@ -1225,6 +1286,50 @@ void AnimationBezierTrackEdit::gui_input(const Ref<InputEvent> &p_event) {
 			}
 		}
 
+		// Box scaling/movement.
+		if (inside_selection_handles_rect) {
+			const Vector2i rel_pos = mb->get_position() - selection_rect.position;
+			scaling_selection_handles = Vector2i();
+
+			// Check which scaling handles are available.
+			if (selection_rect.size.width > CMP_EPSILON) {
+				if (rel_pos.x <= 0) {
+					scaling_selection_handles.x = -1;
+				} else if (rel_pos.x >= selection_rect.size.width) {
+					scaling_selection_handles.x = 1;
+				}
+			}
+			if (selection_rect.size.height > CMP_EPSILON) {
+				if (rel_pos.y <= 0) {
+					scaling_selection_handles.y = -1;
+				} else if (rel_pos.y >= selection_rect.size.height) {
+					scaling_selection_handles.y = 1;
+				}
+			}
+
+			if (scaling_selection_handles != Vector2i()) {
+				scaling_selection = true;
+
+				const float time = ((selection_rect.position.x - limit) / timeline->get_zoom_scale()) + timeline->get_value();
+				const float h = (get_size().height / 2.0 - selection_rect.position.y) * timeline_v_zoom + timeline_v_scroll;
+				scaling_selection_pivot = Point2(time, h);
+
+				return;
+			}
+
+			// If not scaling, that means we're moving.
+			moving_selection_attempt = true;
+			moving_selection = false;
+			moving_selection_mouse_begin = mb->get_position();
+			// The pivot will be from the mouse click location, not a specific key.
+			moving_selection_from_key = -1;
+			moving_selection_from_track = selected_track;
+			moving_selection_offset = Vector2();
+			select_single_attempt = IntPair(-1, -1);
+
+			return;
+		}
+
 		// Insert new point.
 		if (mb->get_position().x >= limit && mb->get_position().x < get_size().width && mb->is_command_or_control_pressed()) {
 			float h = (get_size().height / 2.0 - mb->get_position().y) * timeline_v_zoom + timeline_v_scroll;
@@ -1249,7 +1354,7 @@ void AnimationBezierTrackEdit::gui_input(const Ref<InputEvent> &p_event) {
 
 			moving_selection_attempt = true;
 			moving_selection = false;
-			moving_selection_mouse_begin_x = mb->get_position().x;
+			moving_selection_mouse_begin = mb->get_position();
 			moving_selection_from_key = index;
 			moving_selection_from_track = selected_track;
 			moving_selection_offset = Vector2();
@@ -1284,12 +1389,12 @@ void AnimationBezierTrackEdit::gui_input(const Ref<InputEvent> &p_event) {
 			if (bs_from.y > bs_to.y) {
 				SWAP(bs_from.y, bs_to.y);
 			}
-			Rect2 selection_rect(bs_from, bs_to - bs_from);
+			Rect2 rect(bs_from, bs_to - bs_from);
 
 			bool track_set = false;
 			int j = 0;
 			for (int i = 0; i < edit_points.size(); i++) {
-				if (edit_points[i].point_rect.intersects(selection_rect)) {
+				if (edit_points[i].point_rect.intersects(rect)) {
 					_select_at_anim(animation, edit_points[i].track, animation->track_get_key_time(edit_points[i].track, edit_points[i].key), j == 0 && !box_selecting_add);
 					if (!track_set) {
 						track_set = true;
@@ -1334,8 +1439,7 @@ void AnimationBezierTrackEdit::gui_input(const Ref<InputEvent> &p_event) {
 	if (moving_selection_attempt && mb.is_valid() && !mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT) {
 		if (!read_only) {
 			if (moving_selection && (abs(moving_selection_offset.x) > CMP_EPSILON || abs(moving_selection_offset.y) > CMP_EPSILON)) {
-				//combit it
-
+				// Commit it.
 				EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
 				undo_redo->create_action(TTR("Move Bezier Points"));
 
@@ -1458,11 +1562,141 @@ void AnimationBezierTrackEdit::gui_input(const Ref<InputEvent> &p_event) {
 
 			moving_selection = false;
 			moving_selection_attempt = false;
-			moving_selection_mouse_begin_x = 0.0;
+			moving_selection_mouse_begin = Point2();
 			queue_redraw();
 		}
 	}
 
+	if (scaling_selection && mb.is_valid() && !read_only && !mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT) {
+		if (abs(scaling_selection_scale.x - 1) > CMP_EPSILON || abs(scaling_selection_scale.y - 1) > CMP_EPSILON) {
+			// Scale it.
+			EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
+			undo_redo->create_action(TTR("Scale Bezier Points"));
+
+			List<AnimMoveRestore> to_restore;
+			List<Animation::HandleMode> to_restore_handle_modes;
+			// 1 - Remove the keys.
+			for (SelectionSet::Element *E = selection.back(); E; E = E->prev()) {
+				undo_redo->add_do_method(animation.ptr(), "track_remove_key", E->get().first, E->get().second);
+			}
+			// 2 - Remove overlapped keys.
+			for (SelectionSet::Element *E = selection.back(); E; E = E->prev()) {
+				real_t newtime = animation->track_get_key_time(E->get().first, E->get().second);
+				newtime += -scaling_selection_offset.x + (newtime - scaling_selection_pivot.x) * (scaling_selection_scale.x - 1);
+
+				int idx = animation->track_find_key(E->get().first, newtime, Animation::FIND_MODE_APPROX);
+				if (idx == -1) {
+					continue;
+				}
+
+				if (selection.has(IntPair(E->get().first, idx))) {
+					continue; // Already in selection, don't save.
+				}
+
+				undo_redo->add_do_method(animation.ptr(), "track_remove_key_at_time", E->get().first, newtime);
+				AnimMoveRestore amr;
+
+				amr.key = animation->track_get_key_value(E->get().first, idx);
+				amr.track = E->get().first;
+				amr.time = newtime;
+
+				to_restore.push_back(amr);
+				to_restore_handle_modes.push_back(animation->bezier_track_get_key_handle_mode(E->get().first, idx));
+			}
+
+			// 3 - Scale the keys (re-insert them).
+			for (SelectionSet::Element *E = selection.back(); E; E = E->prev()) {
+				real_t newpos = animation->track_get_key_time(E->get().first, E->get().second);
+				newpos += -scaling_selection_offset.x + (newpos - scaling_selection_pivot.x) * (scaling_selection_scale.x - 1);
+
+				Array key = animation->track_get_key_value(E->get().first, E->get().second);
+				real_t h = key[0];
+				h += -scaling_selection_offset.y + (h - scaling_selection_pivot.y) * (scaling_selection_scale.y - 1);
+				key[0] = h;
+
+				undo_redo->add_do_method(
+						this,
+						"_bezier_track_insert_key_at_anim",
+						animation,
+						E->get().first,
+						newpos,
+						key[0],
+						Vector2(key[1], key[2]),
+						Vector2(key[3], key[4]),
+						animation->bezier_track_get_key_handle_mode(E->get().first, E->get().second));
+			}
+
+			// 4 - (undo) Remove inserted keys.
+			for (SelectionSet::Element *E = selection.back(); E; E = E->prev()) {
+				real_t newpos = animation->track_get_key_time(E->get().first, E->get().second);
+				newpos += -scaling_selection_offset.x + (newpos - scaling_selection_pivot.x) * (scaling_selection_scale.x - 1);
+				undo_redo->add_undo_method(animation.ptr(), "track_remove_key_at_time", E->get().first, newpos);
+			}
+
+			// 5 - (undo) Reinsert keys.
+			for (SelectionSet::Element *E = selection.back(); E; E = E->prev()) {
+				real_t oldpos = animation->track_get_key_time(E->get().first, E->get().second);
+				Array key = animation->track_get_key_value(E->get().first, E->get().second);
+				undo_redo->add_undo_method(
+						this,
+						"_bezier_track_insert_key_at_anim",
+						animation,
+						E->get().first,
+						oldpos,
+						key[0],
+						Vector2(key[1], key[2]),
+						Vector2(key[3], key[4]),
+						animation->bezier_track_get_key_handle_mode(E->get().first, E->get().second));
+			}
+
+			// 6 - (undo) Reinsert overlapped keys.
+			List<AnimMoveRestore>::ConstIterator restore_itr = to_restore.begin();
+			List<Animation::HandleMode>::ConstIterator handle_itr = to_restore_handle_modes.begin();
+			for (; restore_itr != to_restore.end() && handle_itr != to_restore_handle_modes.end(); ++restore_itr, ++handle_itr) {
+				const AnimMoveRestore &amr = *restore_itr;
+				Array key = amr.key;
+				undo_redo->add_undo_method(animation.ptr(), "track_insert_key", amr.track, amr.time, amr.key, 1);
+				undo_redo->add_undo_method(
+						this,
+						"_bezier_track_insert_key_at_anim",
+						animation,
+						amr.track,
+						amr.time,
+						key[0],
+						Vector2(key[1], key[2]),
+						Vector2(key[3], key[4]),
+						*handle_itr);
+			}
+
+			undo_redo->add_do_method(this, "_clear_selection_for_anim", animation);
+			undo_redo->add_undo_method(this, "_clear_selection_for_anim", animation);
+
+			// 7 - Reselect.
+			int i = 0;
+			for (SelectionSet::Element *E = selection.back(); E; E = E->prev()) {
+				real_t oldpos = animation->track_get_key_time(E->get().first, E->get().second);
+				real_t newpos = animation->track_get_key_time(E->get().first, E->get().second);
+				newpos += -scaling_selection_offset.x + (newpos - scaling_selection_pivot.x) * (scaling_selection_scale.x - 1);
+
+				undo_redo->add_do_method(this, "_select_at_anim", animation, E->get().first, newpos, i == 0);
+				undo_redo->add_undo_method(this, "_select_at_anim", animation, E->get().first, oldpos, i == 0);
+				i++;
+			}
+
+			AnimationPlayerEditor *ape = AnimationPlayerEditor::get_singleton();
+			if (ape) {
+				undo_redo->add_do_method(ape, "_animation_update_key_frame");
+				undo_redo->add_undo_method(ape, "_animation_update_key_frame");
+			}
+			undo_redo->commit_action();
+		}
+
+		scaling_selection = false;
+		scaling_selection_scale = Vector2(1, 1);
+		scaling_selection_offset = Vector2();
+		queue_redraw();
+	}
+
 	Ref<InputEventMouseMotion> mm = p_event;
 	if (moving_selection_attempt && mm.is_valid()) {
 		if (!moving_selection) {
@@ -1472,9 +1706,9 @@ void AnimationBezierTrackEdit::gui_input(const Ref<InputEvent> &p_event) {
 
 		if (!read_only) {
 			float y = (get_size().height / 2.0 - mm->get_position().y) * timeline_v_zoom + timeline_v_scroll;
-			float moving_selection_begin_time = ((moving_selection_mouse_begin_x - limit) / timeline->get_zoom_scale()) + timeline->get_value();
+			float moving_selection_begin_time = ((moving_selection_mouse_begin.x - limit) / timeline->get_zoom_scale()) + timeline->get_value();
 			float new_time = ((mm->get_position().x - limit) / timeline->get_zoom_scale()) + timeline->get_value();
-			float moving_selection_pivot = animation->track_get_key_time(moving_selection_from_track, moving_selection_from_key);
+			float moving_selection_pivot = moving_selection_from_key != -1 ? animation->track_get_key_time(moving_selection_from_track, moving_selection_from_key) : 0;
 			float time_delta = new_time - moving_selection_begin_time;
 
 			float snapped_time = editor->snap_time(moving_selection_pivot + time_delta);
@@ -1482,9 +1716,15 @@ void AnimationBezierTrackEdit::gui_input(const Ref<InputEvent> &p_event) {
 			if (abs(moving_selection_offset.x) > CMP_EPSILON || (snapped_time > moving_selection_pivot && time_delta > CMP_EPSILON) || (snapped_time < moving_selection_pivot && time_delta < -CMP_EPSILON)) {
 				time_offset = snapped_time - moving_selection_pivot;
 			}
-			float moving_selection_begin_value = animation->bezier_track_get_key_value(moving_selection_from_track, moving_selection_from_key);
-			float y_offset = y - moving_selection_begin_value;
 
+			float moving_selection_begin_value;
+			if (moving_selection_from_key == -1) {
+				moving_selection_begin_value = (get_size().height / 2.0 - moving_selection_mouse_begin.y) * timeline_v_zoom + timeline_v_scroll;
+			} else {
+				moving_selection_begin_value = animation->bezier_track_get_key_value(moving_selection_from_track, moving_selection_from_key);
+			}
+
+			float y_offset = y - moving_selection_begin_value;
 			moving_selection_offset = Vector2(time_offset, y_offset);
 		}
 
@@ -1504,11 +1744,82 @@ void AnimationBezierTrackEdit::gui_input(const Ref<InputEvent> &p_event) {
 		queue_redraw();
 	}
 
+	if (scaling_selection && mm.is_valid() && !read_only) {
+		Point2 mp = mm->get_position();
+		const int handle_length = Math::round((selection_handles_rect.size.width - selection_rect.size.width) / 4.0);
+		Point2 rel_pos;
+
+		// Calculate the scale according with the distance between the mouse's position (adjusted so that the cursor appears inside the handles)
+		// and the opposite end of the `selection_rect`.
+
+		if (scaling_selection_handles.x != 0) {
+			if (scaling_selection_handles.x == 1) { // Right Handle
+				const int handle_adjust = Math::round(mp.x - (scaling_selection_scale.x >= 0 ? selection_rect.position.x : (selection_rect.position.x + selection_rect.size.width)));
+				mp.x -= MIN(Math::abs(handle_adjust), handle_length) * scaling_selection_handles.x * SIGN(handle_adjust);
+
+				if (editor->is_snap_keys_enabled()) {
+					mp.x = editor->snap_time((mp.x - limit) / timeline->get_zoom_scale(), true) + timeline->get_value();
+					mp.x = (mp.x - timeline->get_value()) * timeline->get_zoom_scale() + limit;
+				}
+
+				rel_pos.x = scaling_selection_scale.x >= 0 ? (mp.x - selection_rect.position.x) : selection_rect.position.x + selection_rect.size.width - mp.x;
+			} else { // Left Handle
+				const int handle_adjust = Math::round((scaling_selection_scale.x >= 0 ? (selection_rect.position.x + selection_rect.size.width) : selection_rect.position.x) - mp.x);
+				mp.x -= MIN(Math::abs(handle_adjust), handle_length) * scaling_selection_handles.x * SIGN(handle_adjust);
+
+				const float x = editor->snap_time((mp.x - limit) / timeline->get_zoom_scale(), true) + timeline->get_value();
+				if (editor->is_snap_keys_enabled()) {
+					mp.x = (x - timeline->get_value()) * timeline->get_zoom_scale() + limit;
+				}
+
+				rel_pos.x = scaling_selection_scale.x >= 0 ? (selection_rect.position.x + selection_rect.size.width - mp.x) : (mp.x - selection_rect.position.x);
+				scaling_selection_offset.x = scaling_selection_pivot.x - x;
+			}
+
+			scaling_selection_scale.x *= rel_pos.x / selection_rect.size.width;
+			if (scaling_selection_scale.x == 0) {
+				scaling_selection_scale.x = CMP_EPSILON;
+			}
+		}
+
+		if (scaling_selection_handles.y != 0) {
+			if (scaling_selection_handles.y == 1) { // Bottom Handle
+				const int handle_adjust = Math::round(mp.y - (scaling_selection_scale.y >= 0 ? selection_rect.position.y : (selection_rect.position.y + selection_rect.size.height)));
+				mp.y -= MIN(Math::abs(handle_adjust), handle_length) * scaling_selection_handles.y * SIGN(handle_adjust);
+
+				if (scaling_selection_scale.y >= 0) {
+					rel_pos.y = mp.y - selection_rect.position.y;
+				} else {
+					rel_pos.y = selection_rect.position.y + selection_rect.size.height - mp.y;
+				}
+			} else { // Top Handle
+				const int handle_adjust = Math::round((scaling_selection_scale.y >= 0 ? (selection_rect.position.y + selection_rect.size.height) : selection_rect.position.y) - mp.y);
+				mp.y -= MIN(Math::abs(handle_adjust), handle_length) * scaling_selection_handles.y * SIGN(handle_adjust);
+
+				if (scaling_selection_scale.y >= 0) {
+					rel_pos.y = selection_rect.position.y + selection_rect.size.height - mp.y;
+				} else {
+					rel_pos.y = mp.y - selection_rect.position.y;
+				}
+
+				const float h = (get_size().height / 2.0 - mp.y) * timeline_v_zoom + timeline_v_scroll;
+				scaling_selection_offset.y = scaling_selection_pivot.y - h;
+			}
+
+			scaling_selection_scale.y *= rel_pos.y / selection_rect.size.height;
+			if (scaling_selection_scale.y == 0) {
+				scaling_selection_scale.y = CMP_EPSILON;
+			}
+		}
+
+		queue_redraw();
+	}
+
 	if ((moving_handle == 1 || moving_handle == -1) && mm.is_valid()) {
 		float y = (get_size().height / 2.0 - mm->get_position().y) * timeline_v_zoom + timeline_v_scroll;
-		float x = editor->snap_time((mm->get_position().x - timeline->get_name_limit()) / timeline->get_zoom_scale()) + timeline->get_value();
+		float x = editor->snap_time((mm->get_position().x - limit) / timeline->get_zoom_scale()) + timeline->get_value();
 
-		Vector2 key_pos = Vector2(animation->track_get_key_time(selected_track, moving_handle_key), animation->bezier_track_get_key_value(selected_track, moving_handle_key));
+		Vector2 key_pos = Vector2(animation->track_get_key_time(moving_handle_track, moving_handle_key), animation->bezier_track_get_key_value(moving_handle_track, moving_handle_key));
 
 		Vector2 moving_handle_value = Vector2(x, y) - key_pos;
 
@@ -1600,7 +1911,7 @@ bool AnimationBezierTrackEdit::_try_select_at_ui_pos(const Point2 &p_pos, bool p
 					moving_selection_attempt = true;
 					moving_selection_from_key = pair.second;
 					moving_selection_from_track = pair.first;
-					moving_selection_mouse_begin_x = p_pos.x;
+					moving_selection_mouse_begin = p_pos;
 					moving_selection_offset = Vector2();
 					moving_handle_track = pair.first;
 					moving_handle_left = animation->bezier_track_get_key_in_handle(pair.first, pair.second);

+ 10 - 1
editor/animation_bezier_editor.h

@@ -109,7 +109,7 @@ class AnimationBezierTrackEdit : public Control {
 	typedef Pair<int, int> IntPair;
 
 	bool moving_selection_attempt = false;
-	float moving_selection_mouse_begin_x = 0.0;
+	Point2 moving_selection_mouse_begin;
 	IntPair select_single_attempt;
 	bool moving_selection = false;
 	int moving_selection_from_key = 0;
@@ -123,6 +123,15 @@ class AnimationBezierTrackEdit : public Control {
 	Vector2 box_selection_from;
 	Vector2 box_selection_to;
 
+	Rect2 selection_rect;
+	Rect2 selection_handles_rect;
+
+	bool scaling_selection = false;
+	Vector2i scaling_selection_handles;
+	Vector2 scaling_selection_scale = Vector2(1, 1);
+	Vector2 scaling_selection_offset;
+	Point2 scaling_selection_pivot;
+
 	int moving_handle = 0; //0 no move -1 or +1 out, 2 both (drawing only)
 	int moving_handle_key = 0;
 	int moving_handle_track = 0;