diff --git a/editor/animation/animation_track_editor.cpp b/editor/animation/animation_track_editor.cpp index 3f0103c92765..5d7b59029bba 100644 --- a/editor/animation/animation_track_editor.cpp +++ b/editor/animation/animation_track_editor.cpp @@ -2331,21 +2331,28 @@ void AnimationTrackEdit::_notification(int p_what) { // Pre-calculate the actual order of the keys. This is needed as a move might be happening // which might cause the keys to be in a different order than their current indices. - Vector> sorted_keys; + LocalVector> sorted_keys; for (int i = 0; i < animation->track_get_key_count(track); i++) { float time_offset = animation->track_get_key_time(track, i) - timeline->get_value(); + if (editor->is_key_selected(track, i) && editor->is_moving_selection()) { time_offset += editor->get_moving_selection_offset(); } + if (editor->is_key_selected(track, i) && editor->is_scaling_selection()) { + float dynamic_scaling = editor->get_scaling_selection_drag() - editor->get_scaling_selection_pivot(); + dynamic_scaling /= editor->get_scaling_selection_drag_start() - editor->get_scaling_selection_pivot(); + time_offset = (time_offset - editor->get_scaling_selection_pivot()) * dynamic_scaling + editor->get_scaling_selection_pivot(); + } + float screen_pos = time_offset * scale + limit; sorted_keys.push_back(Pair(screen_pos, i)); } sorted_keys.sort(); - for (int i = 0; i < sorted_keys.size(); i++) { + for (size_t i = 0; i < sorted_keys.size(); i++) { float offset = sorted_keys[i].first; int original_index = sorted_keys[i].second; bool selected = editor->is_key_selected(track, original_index); @@ -2876,6 +2883,11 @@ Control::CursorShape AnimationTrackEdit::get_cursor_shape(const Point2 &p_pos) c if (command_or_control_pressed && animation->track_get_type(track) == Animation::TYPE_METHOD && hovering_key_idx != -1) { return Control::CURSOR_POINTING_HAND; } + + if (editor->is_position_over_scale_handle(get_global_position() + p_pos)) { + return Control::CURSOR_HSIZE; + } + return get_default_cursor_shape(); } @@ -3239,7 +3251,7 @@ void AnimationTrackEdit::gui_input(const Ref &p_event) { } } - if (!moving_selection && mb.is_valid() && mb->is_pressed() && mb->get_button_index() == MouseButton::RIGHT) { + if (!moving_selection && !editor->is_scaling_selection() && mb.is_valid() && mb->is_pressed() && mb->get_button_index() == MouseButton::RIGHT) { Point2 pos = mb->get_position(); if (pos.x >= timeline->get_name_limit() && pos.x <= get_size().width - timeline->get_buttons_width()) { // Can do something with menu too! show insert key. @@ -3379,7 +3391,7 @@ void AnimationTrackEdit::gui_input(const Ref &p_event) { } bool AnimationTrackEdit::_try_select_at_ui_pos(const Point2 &p_pos, bool p_aggregate, bool p_deselectable) { - if (!animation->track_is_compressed(track)) { // Selecting compressed keyframes for editing is not possible. + if (!animation->track_is_compressed(track) && !editor->is_position_over_scale_handle(get_global_position() + p_pos)) { // Selecting compressed keyframes for editing is not possible. float scale = timeline->get_zoom_scale(); int limit = timeline->get_name_limit(); int limit_end = get_size().width - timeline->get_buttons_width(); @@ -4068,7 +4080,13 @@ AnimationTrackEditGroup::AnimationTrackEditGroup() { set_mouse_filter(MOUSE_FILTER_PASS); } -////////////////////////////////////// +Control::CursorShape AnimationTrackEditGroup::get_cursor_shape(const Point2 &p_pos) const { + if (editor->is_position_over_scale_handle(get_global_position() + p_pos)) { + return Control::CURSOR_HSIZE; + } + + return get_default_cursor_shape(); +} void AnimationTrackEditor::add_track_edit_plugin(const Ref &p_plugin) { if (track_edit_plugins.has(p_plugin)) { @@ -4186,6 +4204,10 @@ Ref AnimationTrackEditor::get_current_animation() const { return animation; } +Control *AnimationTrackEditor::get_scale_control() const { + return scale_control; +} + void AnimationTrackEditor::_root_removed() { root = nullptr; } @@ -4283,12 +4305,21 @@ void AnimationTrackEditor::cleanup() { void AnimationTrackEditor::_name_limit_changed() { _redraw_tracks(); + if (selection.size() > 1) { + _update_scale_control(); + } } void AnimationTrackEditor::_timeline_changed(float p_new_pos, bool p_timeline_only) { emit_signal(SNAME("timeline_changed"), p_new_pos, p_timeline_only, false); } +void AnimationTrackEditor::_zoom_changed() { + if (selection.size() > 1) { + _update_scale_control(); + } +} + void AnimationTrackEditor::_track_remove_request(int p_track) { _animation_track_remove_request(p_track, animation); } @@ -6219,6 +6250,25 @@ void AnimationTrackEditor::_move_selection_begin() { void AnimationTrackEditor::_move_selection(float p_offset) { moving_selection_offset = p_offset; + scale_control->queue_redraw(); + _redraw_tracks(); +} + +void AnimationTrackEditor::_scale_selection_begin(float p_pivot, float p_drag_start) { + scaling_selection_pivot = p_pivot; + scaling_selection_drag = p_drag_start; + scaling_selection_drag_start = p_drag_start; + scaling_selection = true; +} + +void AnimationTrackEditor::_scale_selection(float p_drag_time) { + scaling_selection_drag = p_drag_time; + _redraw_tracks(); +} + +void AnimationTrackEditor::_scale_selection_cancel() { + scaling_selection_drag = scaling_selection_drag_start; + _update_scale_control(); _redraw_tracks(); } @@ -6254,6 +6304,7 @@ void AnimationTrackEditor::_clear_key_edit() { void AnimationTrackEditor::_clear_selection(bool p_update) { selection.clear(); + scale_control->hide(); if (p_update) { _redraw_tracks(); @@ -6264,6 +6315,7 @@ void AnimationTrackEditor::_clear_selection(bool p_update) { void AnimationTrackEditor::_update_key_edit() { _clear_key_edit(); + scale_control->hide(); if (animation.is_null()) { return; } @@ -6323,10 +6375,45 @@ void AnimationTrackEditor::_update_key_edit() { multi_key_edit->use_fps = timeline->is_using_fps(); multi_key_edit->root_path = root; + if (_update_scale_control()) { + scale_control->show(); + } + EditorNode::get_singleton()->push_item(multi_key_edit); } } +bool AnimationTrackEditor::_update_scale_control() { + if (read_only) { + return false; + } + const float timeline_margin_left = timeline_mc->get_theme_constant(SNAME("margin_left"), SNAME("AnimationTrackMargins")); + Rect2 bounds = Rect2(); + bool first_key = true; + + for (const KeyValue &E : selection) { + int track = E.key.track; + int key_id = E.key.key; + Rect2 key_rect = Rect2(); + key_rect.size.height = track_edits[track]->get_size().height; + float offset = animation->track_get_key_time(track, key_id) - timeline->get_value(); + offset = offset * timeline->get_zoom_scale() + timeline->get_name_limit(); + key_rect.position.x = offset + timeline_margin_left; + key_rect.position.y = track_edits[track]->get_global_position().y - scroll->get_global_position().y; + + if (first_key) { + bounds = key_rect; + first_key = false; + } else { + bounds = bounds.merge(key_rect); + } + } + + scale_control->set_rect(bounds); + + return !Math::is_zero_approx(bounds.size.x); +} + void AnimationTrackEditor::_clear_selection_for_anim(const Ref &p_anim) { if (animation != p_anim) { return; @@ -6359,7 +6446,7 @@ void AnimationTrackEditor::_move_selection_commit() { EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); undo_redo->create_action(TTR("Animation Move Keys")); - List<_AnimMoveRestore> to_restore; + LocalVector<_AnimMoveRestore> to_restore; float motion = moving_selection_offset; // 1 - remove the keys. @@ -6441,6 +6528,7 @@ void AnimationTrackEditor::_move_selection_commit() { void AnimationTrackEditor::_move_selection_cancel() { moving_selection = false; + scale_control->queue_redraw(); _redraw_tracks(); } @@ -6452,12 +6540,85 @@ float AnimationTrackEditor::get_moving_selection_offset() const { return moving_selection_offset; } +bool AnimationTrackEditor::is_scaling_selection() const { + return scaling_selection; +} + +bool AnimationTrackEditor::is_position_over_scale_handle(const Point2 &p_global_pos) const { + if (!get_scale_control()->is_visible()) { + return false; + } + static const int SCALE_HANDLE_WIDTH = 24; + static const int KEY_ICON_HALF_WIDTH = 5; + + Vector2 local_pos = box_selection_container->get_global_transform().xform_inv(p_global_pos); + Rect2 scale_rect = scale_control->get_rect(); + + Rect2 left_edge = Rect2( + scale_rect.position - Vector2(Math::round(SCALE_HANDLE_WIDTH * EDSCALE), 0), + Vector2(Vector2(Math::round((SCALE_HANDLE_WIDTH - KEY_ICON_HALF_WIDTH) * EDSCALE), scale_rect.size.y))); + Rect2 right_edge = Rect2( + scale_rect.position + Vector2(scale_rect.size.x + Math::round(KEY_ICON_HALF_WIDTH * EDSCALE), 0), + Vector2(Vector2(Math::round((SCALE_HANDLE_WIDTH - KEY_ICON_HALF_WIDTH) * EDSCALE), scale_rect.size.y))); + + if (left_edge.has_point(local_pos) || right_edge.has_point(local_pos)) { + return true; + } + + return false; +} + +float AnimationTrackEditor::get_scaling_selection_pivot() const { + return scaling_selection_pivot; +} + +float AnimationTrackEditor::get_scaling_selection_drag_start() const { + return scaling_selection_drag_start; +} + +float AnimationTrackEditor::get_scaling_selection_drag() const { + return scaling_selection_drag; +} + void AnimationTrackEditor::_box_selection_draw() { const Rect2 selection_rect = Rect2(Point2(), box_selection->get_size()); box_selection->draw_rect(selection_rect, get_theme_color(SNAME("box_selection_fill_color"), EditorStringName(Editor))); box_selection->draw_rect(selection_rect, get_theme_color(SNAME("box_selection_stroke_color"), EditorStringName(Editor)), false, Math::round(EDSCALE)); } +void AnimationTrackEditor::_scale_control_draw() { + static const int HALF_SCALE_HANDLE_WIDTH = 12; + static const int SCALE_HANDLE_LINE_WIDTH = 3; + + const float timeline_margin_left = timeline_mc->get_theme_constant(SNAME("margin_left"), SNAME("AnimationTrackMargins")); + + Rect2 scale_rect = Rect2(Point2(), scale_control->get_size()); + scale_rect = scale_rect.grow_individual(HALF_SCALE_HANDLE_WIDTH * EDSCALE, 0, HALF_SCALE_HANDLE_WIDTH * EDSCALE, 0); + + Point2 move_offset = moving_selection ? Point2(moving_selection_offset * timeline->get_zoom_scale(), 0) : Point2(); + + int left_limit = timeline->get_name_limit() + timeline_margin_left; + int right_limit = timeline->get_size().width - timeline->get_buttons_width() + timeline_margin_left; + + float left_line_x = scale_control->get_position().x + move_offset.x; + float right_line_x = left_line_x + scale_control->get_size().x; + + Color accent_color = get_theme_color(SNAME("accent_color"), EditorStringName(Editor)); + float line_width = Math::round(SCALE_HANDLE_LINE_WIDTH * EDSCALE); + + if (left_line_x >= left_limit && left_line_x <= right_limit) { + Point2 line_start = scale_rect.position + move_offset; + Point2 line_end = line_start + Point2(0, scale_rect.size.y); + scale_control->draw_line(line_start, line_end, accent_color, line_width); + } + + if (right_line_x >= left_limit && right_line_x <= right_limit) { + Point2 line_start = scale_rect.position + move_offset + Point2(scale_rect.size.x, 0); + Point2 line_end = line_start - Point2(0, -scale_rect.size.y); + scale_control->draw_line(line_start, line_end, accent_color, line_width); + } +} + void AnimationTrackEditor::_scroll_input(const Ref &p_event) { if (!box_selecting) { if (panner->gui_input(p_event, scroll->get_global_rect())) { @@ -6468,8 +6629,48 @@ void AnimationTrackEditor::_scroll_input(const Ref &p_event) { Ref mb = p_event; + if (mb.is_valid() && is_scaling_selection()) { + if (!mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT) { + _scale_selection_commit(); + scaling_selection = false; + return; + } + + if (mb->is_pressed() && mb->get_button_index() == MouseButton::RIGHT) { + _scale_selection_cancel(); + scaling_selection = false; + return; + } + } + if (mb.is_valid() && mb->get_button_index() == MouseButton::LEFT) { if (mb->is_pressed()) { + if (scale_control->is_visible_in_tree()) { + static const int SCALE_HANDLE_WIDTH = 24; + static const int KEY_ICON_HALF_WIDTH = 5; + + Rect2 scale_rect = scale_control->get_rect(); + + Rect2 left_edge = Rect2( + scale_rect.position - Vector2(Math::round(SCALE_HANDLE_WIDTH * EDSCALE), 0), + Vector2(Vector2(Math::round((SCALE_HANDLE_WIDTH - KEY_ICON_HALF_WIDTH) * EDSCALE), scale_rect.size.y))); + Rect2 right_edge = Rect2( + scale_rect.position + Vector2(scale_rect.size.x + Math::round(KEY_ICON_HALF_WIDTH * EDSCALE), 0), + Vector2(Vector2(Math::round((SCALE_HANDLE_WIDTH - KEY_ICON_HALF_WIDTH) * EDSCALE), scale_rect.size.y))); + + const float timeline_margin_left = timeline_mc->get_theme_constant(SNAME("margin_left"), SNAME("AnimationTrackMargins")); + float right_edge_time = (scale_control->get_end().x - timeline->get_name_limit() - timeline_margin_left) / timeline->get_zoom_scale(); + float left_edge_time = (scale_control->get_begin().x - timeline->get_name_limit() - timeline_margin_left) / timeline->get_zoom_scale(); + + if (left_edge.has_point(mb->get_position())) { + _scale_selection_begin(right_edge_time, left_edge_time); + return; + } else if (right_edge.has_point(mb->get_position())) { + _scale_selection_begin(left_edge_time, right_edge_time); + return; + } + } + box_selecting = true; box_selecting_from = scroll->get_global_transform().xform(mb->get_position()); box_select_rect = Rect2(); @@ -6499,6 +6700,21 @@ void AnimationTrackEditor::_scroll_input(const Ref &p_event) { Ref mm = p_event; + if (mm.is_valid() && is_scaling_selection()) { + static const int HALF_SCALE_HANDLE_WIDTH = 12; + const float timeline_margin_left = timeline_mc->get_theme_constant(SNAME("margin_left"), SNAME("AnimationTrackMargins")); + float raw_drag_time = (mm->get_position().x - timeline->get_name_limit() - timeline_margin_left) / timeline->get_zoom_scale(); + float scale_dir_offset = CLAMP((raw_drag_time - scaling_selection_pivot) * timeline->get_zoom_scale(), -HALF_SCALE_HANDLE_WIDTH, HALF_SCALE_HANDLE_WIDTH); + float drag_time = (mm->get_position().x - timeline->get_name_limit() - timeline_margin_left - Math::round(scale_dir_offset * EDSCALE)) / timeline->get_zoom_scale(); + float snapped_time = snap_time(drag_time); + _scale_selection(snapped_time); + Rect2 new_control = Rect2(scaling_selection_pivot * timeline->get_zoom_scale() + timeline->get_name_limit() + timeline_margin_left, scale_control->get_begin().y, 0.0, scale_control->get_size().y); + float snapped_pixel_x = snapped_time * timeline->get_zoom_scale() + timeline->get_name_limit() + timeline_margin_left; + new_control = new_control.expand(Point2(snapped_pixel_x, scale_control->get_begin().y)); + scale_control->set_rect(new_control); + return; + } + if (mm.is_valid() && box_selecting) { if (!mm->get_button_mask().has_flag(MouseButtonMask::LEFT)) { // No longer. @@ -6548,9 +6764,8 @@ void AnimationTrackEditor::_toggle_bezier_edit() { } void AnimationTrackEditor::_scroll_changed(const Vector2 &p_val) { + const Vector2 scroll_difference = p_val - prev_scroll_position; if (box_selecting) { - const Vector2 scroll_difference = p_val - prev_scroll_position; - Vector2 from = box_selecting_from - scroll_difference; Vector2 to = box_selecting_to; @@ -6568,8 +6783,10 @@ void AnimationTrackEditor::_scroll_changed(const Vector2 &p_val) { box_selection->set_rect(Rect2(from - scroll->get_global_position(), rect.get_size())); box_select_rect = rect; } - + Rect2 offset_scale_rect = Rect2(scale_control->get_rect().position - scroll_difference, scale_control->get_rect().size); + scale_control->set_rect(offset_scale_rect); prev_scroll_position = p_val; + scale_control->queue_redraw(); } void AnimationTrackEditor::_v_scroll_changed(float p_val) { @@ -6577,7 +6794,7 @@ void AnimationTrackEditor::_v_scroll_changed(float p_val) { } void AnimationTrackEditor::_h_scroll_changed(float p_val) { - _scroll_changed(Vector2(p_val, prev_scroll_position.y)); + _scroll_changed(Vector2(p_val * timeline->get_zoom_scale(), prev_scroll_position.y)); } void AnimationTrackEditor::_pan_callback(Vector2 p_scroll_vec, Ref p_event) { @@ -7182,106 +7399,7 @@ void AnimationTrackEditor::_edit_menu_pressed(int p_option) { if (selection.is_empty()) { return; } - - float from_t = 1e20; - float to_t = -1e20; - float len = -1e20; - float pivot = 0; - - for (const KeyValue &E : selection) { - float t = animation->track_get_key_time(E.key.track, E.key.key); - if (t < from_t) { - from_t = t; - } - if (t > to_t) { - to_t = t; - } - } - - len = to_t - from_t; - if (scale_from_cursor) { - pivot = timeline->get_play_position(); - } else { - pivot = from_t; - } - - float s = scale->get_value(); - ERR_FAIL_COND_MSG(s == 0, "Can't scale to 0."); - - EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); - undo_redo->create_action(TTR("Animation Scale Keys")); - - List<_AnimMoveRestore> to_restore; - - // 1 - Remove the keys. - for (RBMap::Element *E = selection.back(); E; E = E->prev()) { - undo_redo->add_do_method(animation.ptr(), "track_remove_key", E->key().track, E->key().key); - } - // 2 - Remove overlapped keys. - for (RBMap::Element *E = selection.back(); E; E = E->prev()) { - float newtime = (E->get().pos - from_t) * s + from_t; - int idx = animation->track_find_key(E->key().track, newtime, Animation::FIND_MODE_APPROX); - if (idx == -1) { - continue; - } - SelectedKey sk; - sk.key = idx; - sk.track = E->key().track; - if (selection.has(sk)) { - continue; // Already in selection, don't save. - } - - undo_redo->add_do_method(animation.ptr(), "track_remove_key_at_time", E->key().track, newtime); - _AnimMoveRestore amr; - - amr.key = animation->track_get_key_value(E->key().track, idx); - amr.track = E->key().track; - amr.time = newtime; - amr.transition = animation->track_get_key_transition(E->key().track, idx); - - to_restore.push_back(amr); - } - -#define NEW_POS(m_ofs) (((s > 0) ? m_ofs : from_t + (len - (m_ofs - from_t))) - pivot) * Math::abs(s) + pivot - // 3 - Move the keys (re insert them). - for (RBMap::Element *E = selection.back(); E; E = E->prev()) { - float newpos = NEW_POS(E->get().pos); - undo_redo->add_do_method(animation.ptr(), "track_insert_key", E->key().track, newpos, animation->track_get_key_value(E->key().track, E->key().key), animation->track_get_key_transition(E->key().track, E->key().key)); - } - - // 4 - (Undo) Remove inserted keys. - for (RBMap::Element *E = selection.back(); E; E = E->prev()) { - float newpos = NEW_POS(E->get().pos); - undo_redo->add_undo_method(animation.ptr(), "track_remove_key_at_time", E->key().track, newpos); - } - - // 5 - (Undo) Reinsert keys. - for (RBMap::Element *E = selection.back(); E; E = E->prev()) { - undo_redo->add_undo_method(animation.ptr(), "track_insert_key", E->key().track, E->get().pos, animation->track_get_key_value(E->key().track, E->key().key), animation->track_get_key_transition(E->key().track, E->key().key)); - } - - // 6 - (Undo) Reinsert overlapped keys. - for (_AnimMoveRestore &amr : to_restore) { - undo_redo->add_undo_method(animation.ptr(), "track_insert_key", amr.track, amr.time, amr.key, amr.transition); - } - - undo_redo->add_do_method(this, "_clear_selection_for_anim", animation); - undo_redo->add_undo_method(this, "_clear_selection_for_anim", animation); - - // 7 - Reselect. - for (RBMap::Element *E = selection.back(); E; E = E->prev()) { - float oldpos = E->get().pos; - float newpos = NEW_POS(oldpos); - if (newpos >= 0) { - undo_redo->add_do_method(this, "_select_at_anim", animation, E->key().track, newpos); - } - undo_redo->add_undo_method(this, "_select_at_anim", animation, E->key().track, oldpos); - } -#undef NEW_POS - - undo_redo->add_do_method(this, "_redraw_tracks"); - undo_redo->add_undo_method(this, "_redraw_tracks"); - undo_redo->commit_action(); + _scale_selection_commit(false); } break; case EDIT_SET_START_OFFSET: { @@ -7880,6 +7998,9 @@ void AnimationTrackEditor::_view_group_toggle() { _update_tracks(); view_group->set_button_icon(get_editor_theme_icon(view_group->is_pressed() ? SNAME("AnimationTrackList") : SNAME("AnimationTrackGroup"))); bezier_edit->set_filtered(selected_filter->is_pressed()); + if (selection.size() > 1) { + callable_mp(this, &AnimationTrackEditor::_update_scale_control).call_deferred(); + } } bool AnimationTrackEditor::is_grouping_tracks() { @@ -8073,6 +8194,120 @@ void AnimationTrackEditor::_bind_methods() { ADD_SIGNAL(MethodInfo("animation_step_changed", PropertyInfo(Variant::FLOAT, "step"))); } +void AnimationTrackEditor::_scale_selection_commit(bool p_dynamic) { + if (selection.is_empty()) { + return; + } + + EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); + undo_redo->create_action(TTR("Animation Scale Keys")); + + LocalVector<_AnimMoveRestore> to_restore; + + float from_t = get_scaling_selection_pivot(); + float to_t = get_scaling_selection_drag_start(); + float len = to_t - from_t; + float scale_len = get_scaling_selection_drag() - get_scaling_selection_pivot(); + float scale_factor = scale_len / len; + float pivot = get_scaling_selection_pivot() + timeline->get_value(); + + if (!p_dynamic) { + from_t = 1e20; + to_t = -1e20; + for (const KeyValue &E : selection) { + float t = animation->track_get_key_time(E.key.track, E.key.key); + from_t = MIN(from_t, t); + to_t = MAX(to_t, t); + } + scale_factor = scale->get_value(); + if (scale_from_cursor) { + pivot = timeline->get_play_position(); + } else { + pivot = from_t; + } + } + + // 1 - Remove the keys + for (RBMap::Element *E = selection.back(); E; E = E->prev()) { + undo_redo->add_do_method(animation.ptr(), "track_remove_key", E->key().track, E->key().key); + } + + // 2 - Remove overlapped keys + for (RBMap::Element *E = selection.back(); E; E = E->prev()) { + float newtime = (E->get().pos - from_t) * scale_factor + from_t; + int idx = animation->track_find_key(E->key().track, newtime, Animation::FIND_MODE_APPROX); + if (idx == -1) { + continue; + } + SelectedKey sk; + sk.key = idx; + sk.track = E->key().track; + if (selection.has(sk)) { + continue; + } + + undo_redo->add_do_method(animation.ptr(), "track_remove_key_at_time", E->key().track, newtime); + _AnimMoveRestore amr; + amr.key = animation->track_get_key_value(E->key().track, idx); + amr.track = E->key().track; + amr.time = newtime; + amr.transition = animation->track_get_key_transition(E->key().track, idx); + to_restore.push_back(amr); + } + + // 3 - Move the keys (reinsert them) + for (RBMap::Element *E = selection.back(); E; E = E->prev()) { + float newpos = ((E->get().pos - pivot) * scale_factor) + pivot; + if (!p_dynamic && scale_factor < 0 && !scale_from_cursor) { + newpos = newpos + (to_t - from_t) * Math::abs(scale_factor); + } + undo_redo->add_do_method(animation.ptr(), "track_insert_key", E->key().track, newpos, + animation->track_get_key_value(E->key().track, E->key().key), + animation->track_get_key_transition(E->key().track, E->key().key)); + } + + // 4 - (Undo) Remove inserted keys + for (RBMap::Element *E = selection.back(); E; E = E->prev()) { + float newpos = ((E->get().pos - pivot) * scale_factor) + pivot; + if (!p_dynamic && scale_factor < 0 && !scale_from_cursor) { + newpos = newpos + (to_t - from_t) * Math::abs(scale_factor); + } + undo_redo->add_undo_method(animation.ptr(), "track_remove_key_at_time", E->key().track, newpos); + } + + // 5 - (Undo) Reinsert keys + for (RBMap::Element *E = selection.back(); E; E = E->prev()) { + undo_redo->add_undo_method(animation.ptr(), "track_insert_key", E->key().track, E->get().pos, + animation->track_get_key_value(E->key().track, E->key().key), + animation->track_get_key_transition(E->key().track, E->key().key)); + } + + // 6 - (Undo) Reinsert overlapped keys + for (_AnimMoveRestore &amr : to_restore) { + undo_redo->add_undo_method(animation.ptr(), "track_insert_key", amr.track, amr.time, amr.key, amr.transition); + } + + undo_redo->add_do_method(this, "_clear_selection_for_anim", animation); + undo_redo->add_undo_method(this, "_clear_selection_for_anim", animation); + + // 7 - Reselect + for (RBMap::Element *E = selection.back(); E; E = E->prev()) { + float oldpos = E->get().pos; + float newpos = ((E->get().pos - pivot) * scale_factor) + pivot; + if (!p_dynamic && scale_factor < 0 && !scale_from_cursor) { + newpos = newpos + (to_t - from_t) * Math::abs(scale_factor); + } + if (newpos >= 0) { + undo_redo->add_do_method(this, "_select_at_anim", animation, E->key().track, newpos); + } + undo_redo->add_undo_method(this, "_select_at_anim", animation, E->key().track, oldpos); + } + + undo_redo->add_do_method(this, "_redraw_tracks"); + undo_redo->add_undo_method(this, "_redraw_tracks"); + undo_redo->commit_action(); +} + void AnimationTrackEditor::_pick_track_filter_text_changed(const String &p_newtext) { TreeItem *root_item = pick_track->get_scene_tree()->get_scene_tree()->get_root(); @@ -8178,6 +8413,7 @@ AnimationTrackEditor::AnimationTrackEditor() { timeline->connect("track_added", callable_mp(this, &AnimationTrackEditor::_add_track)); timeline->connect(SceneStringName(value_changed), callable_mp(this, &AnimationTrackEditor::_timeline_value_changed)); timeline->connect("length_changed", callable_mp(this, &AnimationTrackEditor::_update_length)); + timeline->connect("zoom_changed", callable_mp(this, &AnimationTrackEditor::_zoom_changed)); timeline->connect("filter_changed", callable_mp(this, &AnimationTrackEditor::_update_tracks)); panner.instantiate(); @@ -8222,7 +8458,6 @@ AnimationTrackEditor::AnimationTrackEditor() { scroll->connect(SceneStringName(theme_changed), callable_mp(this, &AnimationTrackEditor::_update_timeline_margins), CONNECT_DEFERRED); scroll->get_v_scroll_bar()->connect(SceneStringName(visibility_changed), callable_mp(this, &AnimationTrackEditor::_update_timeline_margins)); scroll->get_v_scroll_bar()->connect(SceneStringName(value_changed), callable_mp(this, &AnimationTrackEditor::_v_scroll_changed)); - scroll->get_h_scroll_bar()->connect(SceneStringName(value_changed), callable_mp(this, &AnimationTrackEditor::_h_scroll_changed)); timeline_vbox->set_custom_minimum_size(Size2(0, 150) * EDSCALE); @@ -8230,6 +8465,7 @@ AnimationTrackEditor::AnimationTrackEditor() { hscroll->share(timeline); hscroll->hide(); hscroll->connect(SceneStringName(value_changed), callable_mp(this, &AnimationTrackEditor::_update_scroll)); + hscroll->connect(SceneStringName(value_changed), callable_mp(this, &AnimationTrackEditor::_h_scroll_changed)); timeline_vbox->add_child(hscroll); timeline->set_hscroll(hscroll); @@ -8510,6 +8746,12 @@ AnimationTrackEditor::AnimationTrackEditor() { box_selection->hide(); box_selection->connect(SceneStringName(draw), callable_mp(this, &AnimationTrackEditor::_box_selection_draw)); + scale_control = memnew(Control); + box_selection_container->add_child(scale_control); + scale_control->set_mouse_filter(MOUSE_FILTER_IGNORE); + scale_control->hide(); + scale_control->connect(SceneStringName(draw), callable_mp(this, &AnimationTrackEditor::_scale_control_draw)); + // Default Plugins. Ref def_plugin; diff --git a/editor/animation/animation_track_editor.h b/editor/animation/animation_track_editor.h index 1d2fe00d8f87..035004f92126 100644 --- a/editor/animation/animation_track_editor.h +++ b/editor/animation/animation_track_editor.h @@ -599,6 +599,7 @@ class AnimationTrackEditGroup : public Control { void set_root(Node *p_root); void set_editor(AnimationTrackEditor *p_editor); String get_node_name() const; + virtual CursorShape get_cursor_shape(const Point2 &p_pos) const override; AnimationTrackEditGroup(); }; @@ -775,15 +776,26 @@ class AnimationTrackEditor : public VBoxContainer { void _move_selection_commit(); void _move_selection_cancel(); + bool scaling_selection = false; + float scaling_selection_pivot = 0.0f; + float scaling_selection_drag_start = 0.0f; + float scaling_selection_drag = 0.0f; + void _scale_selection_begin(float p_pivot, float p_drag_start); + void _scale_selection(float p_offset); + void _scale_selection_commit(bool p_dynamic = true); + void _scale_selection_cancel(); + AnimationTrackKeyEdit *key_edit = nullptr; AnimationMultiTrackKeyEdit *multi_key_edit = nullptr; void _update_key_edit(); + bool _update_scale_control(); void _clear_key_edit(); Control *box_selection_container = nullptr; Control *box_selection = nullptr; void _box_selection_draw(); + void _scale_control_draw(); bool box_selecting = false; Vector2 box_selecting_from; Vector2 box_selecting_to; @@ -791,6 +803,8 @@ class AnimationTrackEditor : public VBoxContainer { Vector2 prev_scroll_position; void _scroll_input(const Ref &p_event); + Control *scale_control = nullptr; + Vector> track_edit_plugins; void _toggle_bezier_edit(); @@ -917,6 +931,7 @@ class AnimationTrackEditor : public VBoxContainer { void _clear_selection(bool p_update = false); void _key_selected(int p_key, bool p_single, int p_track); void _key_deselected(int p_key, int p_track); + void _zoom_changed(); enum { EDIT_COPY_TRACKS, @@ -958,6 +973,7 @@ class AnimationTrackEditor : public VBoxContainer { void set_animation(const Ref &p_anim, bool p_read_only); Ref get_current_animation() const; + Control *get_scale_control() const; void set_root(Node *p_root); Node *get_root() const; void update_keying(); @@ -985,6 +1001,9 @@ class AnimationTrackEditor : public VBoxContainer { bool is_selection_active() const; bool is_key_clipboard_active() const; bool is_moving_selection() const; + bool is_scaling_selection() const; + bool is_position_over_scale_handle(const Point2 &p_global_pos) const; + bool is_snap_timeline_enabled() const; bool is_snap_keys_enabled() const; bool is_insert_at_current_time_enabled() const; @@ -993,6 +1012,9 @@ class AnimationTrackEditor : public VBoxContainer { bool can_add_reset_key() const; void _on_filter_updated(const String &p_filter); float get_moving_selection_offset() const; + float get_scaling_selection_pivot() const; + float get_scaling_selection_drag_start() const; + float get_scaling_selection_drag() const; float snap_time(float p_value, bool p_relative = false); float get_snap_unit(); bool is_grouping_tracks(); diff --git a/editor/animation/animation_track_editor_plugins.cpp b/editor/animation/animation_track_editor_plugins.cpp index 61a5d0a50305..21efd82e6767 100644 --- a/editor/animation/animation_track_editor_plugins.cpp +++ b/editor/animation/animation_track_editor_plugins.cpp @@ -1175,10 +1175,10 @@ void AnimationTrackEditTypeAudio::gui_input(const Ref &p_event) { } Control::CursorShape AnimationTrackEditTypeAudio::get_cursor_shape(const Point2 &p_pos) const { - if (over_drag_position || len_resizing) { + if ((over_drag_position || len_resizing) && !get_editor()->get_scale_control()->is_visible()) { return Control::CURSOR_HSIZE; } else { - return get_default_cursor_shape(); + return AnimationTrackEdit::get_cursor_shape(p_pos); } }