summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorRémi Verschelde <rverschelde@gmail.com>2023-05-29 10:28:53 +0200
committerRémi Verschelde <rverschelde@gmail.com>2023-05-29 10:28:53 +0200
commit990943782a80e47b4a46587c298a338e9a2c119c (patch)
treed4396e8bf6c3fa0093d9bf297d6be85a73f05553
parent9f05e16727a6776c0bf27c18d5ce0d016542494a (diff)
parenta3c4a4b039e9c0c5947744bbaf7f9a9bc123982c (diff)
downloadredot-engine-990943782a80e47b4a46587c298a338e9a2c119c.tar.gz
Merge pull request #74959 from MewPurPur/nice-curves
Overhaul the Curve Editor
-rw-r--r--editor/plugins/curve_editor_plugin.cpp1205
-rw-r--r--editor/plugins/curve_editor_plugin.h139
-rw-r--r--scene/resources/curve.cpp7
-rw-r--r--scene/resources/curve.h7
4 files changed, 834 insertions, 524 deletions
diff --git a/editor/plugins/curve_editor_plugin.cpp b/editor/plugins/curve_editor_plugin.cpp
index 7c87f009a6..1e5e15f6ae 100644
--- a/editor/plugins/curve_editor_plugin.cpp
+++ b/editor/plugins/curve_editor_plugin.cpp
@@ -33,276 +33,358 @@
#include "canvas_item_editor_plugin.h"
#include "core/core_string_names.h"
#include "core/input/input.h"
+#include "core/math/geometry_2d.h"
#include "core/os/keyboard.h"
#include "editor/editor_interface.h"
#include "editor/editor_node.h"
#include "editor/editor_scale.h"
#include "editor/editor_settings.h"
#include "editor/editor_undo_redo_manager.h"
+#include "editor/gui/editor_spin_slider.h"
+#include "scene/gui/flow_container.h"
+#include "scene/gui/menu_button.h"
#include "scene/gui/popup_menu.h"
+#include "scene/gui/separator.h"
-CurveEditor::CurveEditor() {
- _selected_point = -1;
- _hover_point = -1;
- _selected_tangent = TANGENT_NONE;
- _hover_radius = 6;
- _tangents_length = 40;
- _dragging = false;
- _has_undo_data = false;
- _gizmo_handle_scale = EDITOR_GET("interface/touchscreen/scale_gizmo_handles");
-
+CurveEdit::CurveEdit() {
set_focus_mode(FOCUS_ALL);
set_clip_contents(true);
+}
- _context_menu = memnew(PopupMenu);
- _context_menu->connect("id_pressed", callable_mp(this, &CurveEditor::on_context_menu_item_selected));
- add_child(_context_menu);
+void CurveEdit::_on_mouse_exited() {
+ hovered_index = -1;
+ hovered_tangent_index = TANGENT_NONE;
+ queue_redraw();
+}
- _presets_menu = memnew(PopupMenu);
- _presets_menu->set_name("_presets_menu");
- _presets_menu->add_item(TTR("Flat 0"), PRESET_FLAT0);
- _presets_menu->add_item(TTR("Flat 1"), PRESET_FLAT1);
- _presets_menu->add_item(TTR("Linear"), PRESET_LINEAR);
- _presets_menu->add_item(TTR("Ease In"), PRESET_EASE_IN);
- _presets_menu->add_item(TTR("Ease Out"), PRESET_EASE_OUT);
- _presets_menu->add_item(TTR("Smoothstep"), PRESET_SMOOTHSTEP);
- _presets_menu->connect("id_pressed", callable_mp(this, &CurveEditor::on_preset_item_selected));
- _context_menu->add_child(_presets_menu);
+void CurveEdit::_bind_methods() {
+ ClassDB::bind_method(D_METHOD("set_selected_index", "index"), &CurveEdit::set_selected_index);
}
-void CurveEditor::set_curve(Ref<Curve> curve) {
- if (curve == _curve_ref) {
+void CurveEdit::set_curve(Ref<Curve> p_curve) {
+ if (p_curve == curve) {
return;
}
- if (_curve_ref.is_valid()) {
- _curve_ref->disconnect(CoreStringNames::get_singleton()->changed, callable_mp(this, &CurveEditor::_curve_changed));
- _curve_ref->disconnect(Curve::SIGNAL_RANGE_CHANGED, callable_mp(this, &CurveEditor::_curve_changed));
+ if (curve.is_valid()) {
+ curve->disconnect(CoreStringNames::get_singleton()->changed, callable_mp(this, &CurveEdit::_curve_changed));
+ curve->disconnect(Curve::SIGNAL_RANGE_CHANGED, callable_mp(this, &CurveEdit::_curve_changed));
}
- _curve_ref = curve;
+ curve = p_curve;
- if (_curve_ref.is_valid()) {
- _curve_ref->connect(CoreStringNames::get_singleton()->changed, callable_mp(this, &CurveEditor::_curve_changed));
- _curve_ref->connect(Curve::SIGNAL_RANGE_CHANGED, callable_mp(this, &CurveEditor::_curve_changed));
+ if (curve.is_valid()) {
+ curve->connect(CoreStringNames::get_singleton()->changed, callable_mp(this, &CurveEdit::_curve_changed));
+ curve->connect(Curve::SIGNAL_RANGE_CHANGED, callable_mp(this, &CurveEdit::_curve_changed));
}
- _selected_point = -1;
- _hover_point = -1;
- _selected_tangent = TANGENT_NONE;
+ // Note: if you edit a curve, then set another, and try to undo,
+ // it will normally apply on the previous curve, but you won't see it.
+}
+Ref<Curve> CurveEdit::get_curve() {
+ return curve;
+}
+
+void CurveEdit::set_snap_enabled(bool p_enabled) {
+ snap_enabled = p_enabled;
queue_redraw();
+ if (curve.is_valid()) {
+ if (snap_enabled) {
+ curve->set_meta(SNAME("_snap_enabled"), true);
+ } else {
+ curve->remove_meta(SNAME("_snap_enabled"));
+ }
+ }
+}
- // Note: if you edit a curve, then set another, and try to undo,
- // it will normally apply on the previous curve, but you won't see it
+void CurveEdit::set_snap_count(int p_snap_count) {
+ snap_count = p_snap_count;
+ queue_redraw();
+ if (curve.is_valid()) {
+ if (snap_count != CurveEditor::DEFAULT_SNAP) {
+ curve->set_meta(SNAME("_snap_count"), snap_count);
+ } else {
+ curve->remove_meta(SNAME("_snap_count"));
+ }
+ }
}
-Size2 CurveEditor::get_minimum_size() const {
- return Vector2(64, 150) * EDSCALE;
+Size2 CurveEdit::get_minimum_size() const {
+ return Vector2(64, 135) * EDSCALE;
}
-void CurveEditor::_notification(int p_what) {
+void CurveEdit::_notification(int p_what) {
switch (p_what) {
- case NOTIFICATION_DRAW: {
- _draw();
+ case NOTIFICATION_ENTER_TREE: {
+ connect("mouse_exited", callable_mp(this, &CurveEdit::_on_mouse_exited));
} break;
+ case NOTIFICATION_THEME_CHANGED:
case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: {
- if (EditorSettings::get_singleton()->check_changed_settings_in_group("interface/touchscreen/scale_gizmo_handles")) {
- _gizmo_handle_scale = EDITOR_GET("interface/touchscreen/scale_gizmo_handles");
+ float gizmo_scale = EDITOR_GET("interface/touchscreen/scale_gizmo_handles");
+ point_radius = Math::round(BASE_POINT_RADIUS * get_theme_default_base_scale() * gizmo_scale);
+ hover_radius = Math::round(BASE_HOVER_RADIUS * get_theme_default_base_scale() * gizmo_scale);
+ tangent_radius = Math::round(BASE_TANGENT_RADIUS * get_theme_default_base_scale() * gizmo_scale);
+ tangent_hover_radius = Math::round(BASE_TANGENT_HOVER_RADIUS * get_theme_default_base_scale() * gizmo_scale);
+ tangent_length = Math::round(BASE_TANGENT_LENGTH * get_theme_default_base_scale());
+ } break;
+ case NOTIFICATION_DRAW: {
+ _redraw();
+ } break;
+ case NOTIFICATION_VISIBILITY_CHANGED: {
+ if (!is_visible()) {
+ grabbing = GRAB_NONE;
}
} break;
}
}
-void CurveEditor::gui_input(const Ref<InputEvent> &p_event) {
- Ref<InputEventMouseButton> mb_ref = p_event;
- if (mb_ref.is_valid()) {
- const InputEventMouseButton &mb = **mb_ref;
+void CurveEdit::gui_input(const Ref<InputEvent> &p_event) {
+ ERR_FAIL_COND(p_event.is_null());
+
+ Ref<InputEventKey> k = p_event;
+ if (k.is_valid()) {
+ // Deleting points or making tangents linear.
+ if (k->is_pressed() && k->get_keycode() == Key::KEY_DELETE) {
+ if (selected_tangent_index != TANGENT_NONE) {
+ toggle_linear(selected_index, selected_tangent_index);
+ } else if (selected_index != -1) {
+ if (grabbing == GRAB_ADD) {
+ curve->remove_point(selected_index); // Point is temporary, so remove directly from curve.
+ set_selected_index(-1);
+ } else {
+ remove_point(selected_index);
+ }
+ grabbing = GRAB_NONE;
+ hovered_index = -1;
+ hovered_tangent_index = TANGENT_NONE;
+ }
+ accept_event();
+ }
- if (mb.is_pressed() && !_dragging) {
- Vector2 mpos = mb.get_position();
+ if (k->get_keycode() == Key::SHIFT || k->get_keycode() == Key::ALT) {
+ queue_redraw(); // Redraw to show the axes or constraints.
+ }
+ }
- _selected_tangent = get_tangent_at(mpos);
- if (_selected_tangent == TANGENT_NONE) {
- set_selected_point(get_point_at(mpos));
+ Ref<InputEventMouseButton> mb = p_event;
+ if (mb.is_valid() && mb->is_pressed()) {
+ Vector2 mpos = mb->get_position();
+
+ if (mb->get_button_index() == MouseButton::RIGHT || mb->get_button_index() == MouseButton::MIDDLE) {
+ if (mb->get_button_index() == MouseButton::RIGHT && grabbing == GRAB_MOVE) {
+ // Move a point to its old position.
+ curve->set_point_value(selected_index, initial_grab_pos.y);
+ curve->set_point_offset(selected_index, initial_grab_pos.x);
+ set_selected_index(initial_grab_index);
+ hovered_index = get_point_at(mpos);
+ grabbing = GRAB_NONE;
+ } else {
+ // Remove a point or make a tangent linear.
+ selected_tangent_index = get_tangent_at(mpos);
+ if (selected_tangent_index != TANGENT_NONE) {
+ toggle_linear(selected_index, selected_tangent_index);
+ } else {
+ int point_to_remove = get_point_at(mpos);
+ if (point_to_remove != -1) {
+ if (grabbing == GRAB_ADD) {
+ curve->remove_point(point_to_remove); // Point is temporary, so remove directly from curve.
+ set_selected_index(-1);
+ } else {
+ remove_point(point_to_remove);
+ }
+ hovered_index = get_point_at(mpos);
+ grabbing = GRAB_NONE;
+ }
+ }
}
+ }
- switch (mb.get_button_index()) {
- case MouseButton::RIGHT:
- _context_click_pos = mpos;
- open_context_menu(get_screen_position() + mpos);
- break;
-
- case MouseButton::MIDDLE:
- remove_point(_hover_point);
- break;
-
- case MouseButton::LEFT:
- _dragging = true;
- break;
- default:
- break;
+ // Selecting or creating points.
+ if (mb->get_button_index() == MouseButton::LEFT) {
+ if (grabbing == GRAB_NONE) {
+ selected_tangent_index = get_tangent_at(mpos);
+ if (selected_tangent_index == TANGENT_NONE) {
+ set_selected_index(get_point_at(mpos));
+ }
+ queue_redraw();
}
- }
- if (!mb.is_pressed() && _dragging && mb.get_button_index() == MouseButton::LEFT) {
- _dragging = false;
- if (_has_undo_data) {
- EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
- undo_redo->create_action(_selected_tangent == TANGENT_NONE ? TTR("Modify Curve Point") : TTR("Modify Curve Tangent"));
- undo_redo->add_do_method(*_curve_ref, "_set_data", _curve_ref->get_data());
- undo_redo->add_undo_method(*_curve_ref, "_set_data", _undo_data);
- // Note: this will trigger one more "changed" signal even if nothing changes,
- // but it's ok since it would have fired every frame during the drag anyways
- undo_redo->commit_action();
-
- _has_undo_data = false;
+ if (selected_index != -1) {
+ // If an existing point/tangent was grabbed, remember a few things about it.
+ grabbing = GRAB_MOVE;
+ initial_grab_pos = curve->get_point_position(selected_index);
+ initial_grab_index = selected_index;
+ if (selected_index > 0) {
+ initial_grab_left_tangent = curve->get_point_left_tangent(selected_index);
+ }
+ if (selected_index < curve->get_point_count() - 1) {
+ initial_grab_right_tangent = curve->get_point_right_tangent(selected_index);
+ }
+ } else if (grabbing == GRAB_NONE) {
+ // Adding a new point. Insert a temporary point for the user to adjust, so it's not in the undo/redo.
+ Vector2 new_pos = get_world_pos(mpos).clamp(Vector2(0.0, curve->get_min_value()), Vector2(1.0, curve->get_max_value()));
+ if (snap_enabled || mb->is_ctrl_pressed()) {
+ new_pos.x = Math::snapped(new_pos.x, 1.0 / snap_count);
+ new_pos.y = Math::snapped(new_pos.y - curve->get_min_value(), curve->get_range() / snap_count) + curve->get_min_value();
+ }
+
+ new_pos.x = get_offset_without_collision(selected_index, new_pos.x, mpos.x >= get_view_pos(new_pos).x);
+
+ // Add a temporary point for the user to adjust before adding it permanently.
+ int new_idx = curve->add_point_no_update(new_pos);
+ set_selected_index(new_idx);
+ grabbing = GRAB_ADD;
+ initial_grab_pos = new_pos;
}
}
}
- Ref<InputEventMouseMotion> mm_ref = p_event;
- if (mm_ref.is_valid()) {
- const InputEventMouseMotion &mm = **mm_ref;
-
- Vector2 mpos = mm.get_position();
+ if (mb.is_valid() && mb->get_button_index() == MouseButton::LEFT && !mb->is_pressed()) {
+ if (selected_tangent_index != TANGENT_NONE) {
+ // Finish moving a tangent control.
+ if (selected_index == 0) {
+ set_point_right_tangent(selected_index, curve->get_point_right_tangent(selected_index));
+ } else if (selected_index == curve->get_point_count() - 1) {
+ set_point_left_tangent(selected_index, curve->get_point_left_tangent(selected_index));
+ } else {
+ set_point_tangents(selected_index, curve->get_point_left_tangent(selected_index), curve->get_point_right_tangent(selected_index));
+ }
+ grabbing = GRAB_NONE;
+ } else if (grabbing == GRAB_MOVE) {
+ // Finish moving a point.
+ set_point_position(selected_index, curve->get_point_position(selected_index));
+ grabbing = GRAB_NONE;
+ } else if (grabbing == GRAB_ADD) {
+ // Finish inserting a new point. Remove the temporary point and insert a permanent one in its place.
+ Vector2 new_pos = curve->get_point_position(selected_index);
+ curve->remove_point(selected_index);
+ add_point(new_pos);
+ grabbing = GRAB_NONE;
+ }
+ queue_redraw();
+ }
- if (_dragging && _curve_ref.is_valid()) {
- if (_selected_point != -1) {
- Curve &curve = **_curve_ref;
+ Ref<InputEventMouseMotion> mm = p_event;
+ if (mm.is_valid()) {
+ Vector2 mpos = mm->get_position();
- if (!_has_undo_data) {
- // Save full curve state before dragging points,
- // because this operation can modify their order
- _undo_data = curve.get_data();
- _has_undo_data = true;
- }
+ if (grabbing != GRAB_NONE && curve.is_valid()) {
+ if (selected_index != -1) {
+ if (selected_tangent_index == TANGENT_NONE) {
+ // Drag point.
+ Vector2 new_pos = get_world_pos(mpos).clamp(Vector2(0.0, curve->get_min_value()), Vector2(1.0, curve->get_max_value()));
- const float curve_amplitude = curve.get_max_value() - curve.get_min_value();
- // Snap to "round" coordinates when holding Ctrl.
- // Be more precise when holding Shift as well.
- float snap_threshold;
- if (mm.is_ctrl_pressed()) {
- snap_threshold = mm.is_shift_pressed() ? 0.025 : 0.1;
- } else {
- snap_threshold = 0.0;
- }
+ if (snap_enabled || mm->is_ctrl_pressed()) {
+ new_pos.x = Math::snapped(new_pos.x, 1.0 / snap_count);
+ new_pos.y = Math::snapped(new_pos.y - curve->get_min_value(), curve->get_range() / snap_count) + curve->get_min_value();
+ }
- if (_selected_tangent == TANGENT_NONE) {
- // Drag point
+ // Allow to snap to axes with Shift.
+ if (mm->is_shift_pressed()) {
+ Vector2 initial_mpos = get_view_pos(initial_grab_pos);
+ if (Math::abs(mpos.x - initial_mpos.x) > Math::abs(mpos.y - initial_mpos.y)) {
+ new_pos.y = initial_grab_pos.y;
+ } else {
+ new_pos.x = initial_grab_pos.x;
+ }
+ }
- Vector2 point_pos = get_world_pos(mpos).snapped(Vector2(snap_threshold, snap_threshold * curve_amplitude));
+ // Allow to constraint the point between the adjacent two with Alt.
+ if (mm->is_alt_pressed()) {
+ float prev_point_offset = (selected_index > 0) ? (curve->get_point_position(selected_index - 1).x + 0.00001) : 0.0;
+ float next_point_offset = (selected_index < curve->get_point_count() - 1) ? (curve->get_point_position(selected_index + 1).x - 0.00001) : 1.0;
+ new_pos.x = CLAMP(new_pos.x, prev_point_offset, next_point_offset);
+ }
- int i = curve.set_point_offset(_selected_point, point_pos.x);
- // The index may change if the point is dragged across another one
- set_hover_point_index(i);
- set_selected_point(i);
+ new_pos.x = get_offset_without_collision(selected_index, new_pos.x, mpos.x >= get_view_pos(new_pos).x);
- // This is to prevent the user from losing a point out of view.
- if (point_pos.y < curve.get_min_value()) {
- point_pos.y = curve.get_min_value();
- } else if (point_pos.y > curve.get_max_value()) {
- point_pos.y = curve.get_max_value();
- }
+ // The index may change if the point is dragged across another one.
+ int i = curve->set_point_offset(selected_index, new_pos.x);
+ hovered_index = i;
+ set_selected_index(i);
- curve.set_point_value(_selected_point, point_pos.y);
+ new_pos.y = CLAMP(new_pos.y, curve->get_min_value(), curve->get_max_value());
+ curve->set_point_value(selected_index, new_pos.y);
} else {
- // Drag tangent
-
- const Vector2 point_pos = curve.get_point_position(_selected_point);
- const Vector2 control_pos = get_world_pos(mpos).snapped(Vector2(snap_threshold, snap_threshold * curve_amplitude));
+ // Drag tangent.
- Vector2 dir = (control_pos - point_pos).normalized();
+ const Vector2 new_pos = curve->get_point_position(selected_index);
+ const Vector2 control_pos = get_world_pos(mpos);
- real_t tangent;
- if (!Math::is_zero_approx(dir.x)) {
- tangent = dir.y / dir.x;
- } else {
- tangent = 9999 * (dir.y >= 0 ? 1 : -1);
- }
+ Vector2 dir = (control_pos - new_pos).normalized();
+ real_t tangent = dir.y / (dir.x > 0 ? MAX(dir.x, 0.00001) : MIN(dir.x, -0.00001));
- bool link = !Input::get_singleton()->is_key_pressed(Key::SHIFT);
+ // Must keep track of the hovered index as the cursor might move outside of the editor while dragging.
+ hovered_tangent_index = selected_tangent_index;
- if (_selected_tangent == TANGENT_LEFT) {
- curve.set_point_left_tangent(_selected_point, tangent);
+ // Adjust the tangents.
+ if (selected_tangent_index == TANGENT_LEFT) {
+ curve->set_point_left_tangent(selected_index, tangent);
- // Note: if a tangent is set to linear, it shouldn't be linked to the other
- if (link && _selected_point != (curve.get_point_count() - 1) && curve.get_point_right_mode(_selected_point) != Curve::TANGENT_LINEAR) {
- curve.set_point_right_tangent(_selected_point, tangent);
+ // Align the other tangent if it isn't linear and Shift is not pressed.
+ // If Shift is pressed at any point, restore the initial angle of the other tangent.
+ if (selected_index != (curve->get_point_count() - 1) && curve->get_point_right_mode(selected_index) != Curve::TANGENT_LINEAR) {
+ curve->set_point_right_tangent(selected_index, mm->is_shift_pressed() ? initial_grab_right_tangent : tangent);
}
} else {
- curve.set_point_right_tangent(_selected_point, tangent);
+ curve->set_point_right_tangent(selected_index, tangent);
- if (link && _selected_point != 0 && curve.get_point_left_mode(_selected_point) != Curve::TANGENT_LINEAR) {
- curve.set_point_left_tangent(_selected_point, tangent);
+ if (selected_index != 0 && curve->get_point_left_mode(selected_index) != Curve::TANGENT_LINEAR) {
+ curve->set_point_left_tangent(selected_index, mm->is_shift_pressed() ? initial_grab_left_tangent : tangent);
}
}
}
}
-
} else {
- set_hover_point_index(get_point_at(mpos));
- }
- }
-
- Ref<InputEventKey> key_ref = p_event;
- if (key_ref.is_valid()) {
- const InputEventKey &key = **key_ref;
-
- if (key.is_pressed() && _selected_point != -1) {
- if (key.get_keycode() == Key::KEY_DELETE) {
- remove_point(_selected_point);
- }
+ // Grab mode is GRAB_NONE, so do hovering logic.
+ hovered_index = get_point_at(mpos);
+ hovered_tangent_index = get_tangent_at(mpos);
+ queue_redraw();
}
}
}
-void CurveEditor::on_preset_item_selected(int preset_id) {
- ERR_FAIL_COND(preset_id < 0 || preset_id >= PRESET_COUNT);
- ERR_FAIL_COND(_curve_ref.is_null());
+void CurveEdit::use_preset(int p_preset_id) {
+ ERR_FAIL_COND(p_preset_id < 0 || p_preset_id >= PRESET_COUNT);
+ ERR_FAIL_COND(curve.is_null());
- Curve &curve = **_curve_ref;
- Array previous_data = curve.get_data();
+ Array previous_data = curve->get_data();
+ curve->clear_points();
- curve.clear_points();
+ float min_value = curve->get_min_value();
+ float max_value = curve->get_max_value();
- switch (preset_id) {
- case PRESET_FLAT0:
- curve.add_point(Vector2(0, 0));
- curve.add_point(Vector2(1, 0));
- curve.set_point_right_mode(0, Curve::TANGENT_LINEAR);
- curve.set_point_left_mode(1, Curve::TANGENT_LINEAR);
- break;
-
- case PRESET_FLAT1:
- curve.add_point(Vector2(0, 1));
- curve.add_point(Vector2(1, 1));
- curve.set_point_right_mode(0, Curve::TANGENT_LINEAR);
- curve.set_point_left_mode(1, Curve::TANGENT_LINEAR);
+ switch (p_preset_id) {
+ case PRESET_CONSTANT:
+ curve->add_point(Vector2(0, (min_value + max_value) / 2.0));
+ curve->add_point(Vector2(1, (min_value + max_value) / 2.0));
+ curve->set_point_right_mode(0, Curve::TANGENT_LINEAR);
+ curve->set_point_left_mode(1, Curve::TANGENT_LINEAR);
break;
case PRESET_LINEAR:
- curve.add_point(Vector2(0, 0));
- curve.add_point(Vector2(1, 1));
- curve.set_point_right_mode(0, Curve::TANGENT_LINEAR);
- curve.set_point_left_mode(1, Curve::TANGENT_LINEAR);
+ curve->add_point(Vector2(0, min_value));
+ curve->add_point(Vector2(1, max_value));
+ curve->set_point_right_mode(0, Curve::TANGENT_LINEAR);
+ curve->set_point_left_mode(1, Curve::TANGENT_LINEAR);
break;
case PRESET_EASE_IN:
- curve.add_point(Vector2(0, 0));
- curve.add_point(Vector2(1, 1), (curve.get_max_value() - curve.get_min_value()) * 1.4, 0);
+ curve->add_point(Vector2(0, min_value));
+ curve->add_point(Vector2(1, max_value), curve->get_range() * 1.4, 0);
break;
case PRESET_EASE_OUT:
- curve.add_point(Vector2(0, 0), 0, (curve.get_max_value() - curve.get_min_value()) * 1.4);
- curve.add_point(Vector2(1, 1));
+ curve->add_point(Vector2(0, min_value), 0, curve->get_range() * 1.4);
+ curve->add_point(Vector2(1, max_value));
break;
case PRESET_SMOOTHSTEP:
- curve.add_point(Vector2(0, 0));
- curve.add_point(Vector2(1, 1));
+ curve->add_point(Vector2(0, min_value));
+ curve->add_point(Vector2(1, max_value));
break;
default:
@@ -311,236 +393,269 @@ void CurveEditor::on_preset_item_selected(int preset_id) {
EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
undo_redo->create_action(TTR("Load Curve Preset"));
- undo_redo->add_do_method(&curve, "_set_data", curve.get_data());
- undo_redo->add_undo_method(&curve, "_set_data", previous_data);
+ undo_redo->add_do_method(*curve, "_set_data", curve->get_data());
+ undo_redo->add_do_method(this, "set_selected_index", -1);
+ undo_redo->add_undo_method(*curve, "_set_data", previous_data);
+ undo_redo->add_undo_method(this, "set_selected_index", selected_index);
undo_redo->commit_action();
}
-void CurveEditor::_curve_changed() {
+void CurveEdit::_curve_changed() {
queue_redraw();
- // Point count can change in case of undo
- if (_selected_point >= _curve_ref->get_point_count()) {
- set_selected_point(-1);
+ // Point count can change in case of undo.
+ if (selected_index >= curve->get_point_count()) {
+ set_selected_index(-1);
}
}
-void CurveEditor::on_context_menu_item_selected(int action_id) {
- switch (action_id) {
- case CONTEXT_ADD_POINT:
- add_point(_context_click_pos);
- break;
-
- case CONTEXT_REMOVE_POINT:
- remove_point(_selected_point);
- break;
-
- case CONTEXT_LINEAR:
- toggle_linear();
- break;
+int CurveEdit::get_point_at(Vector2 p_pos) const {
+ if (curve.is_null()) {
+ return -1;
+ }
- case CONTEXT_LEFT_LINEAR:
- toggle_linear(TANGENT_LEFT);
- break;
+ // Use a square-shaped hover region. If hovering multiple points, pick the closer one.
+ const Rect2 hover_rect = Rect2(p_pos, Vector2(0, 0)).grow(hover_radius);
+ int closest_idx = -1;
+ float closest_dist_squared = hover_radius * hover_radius * 2;
- case CONTEXT_RIGHT_LINEAR:
- toggle_linear(TANGENT_RIGHT);
- break;
+ for (int i = 0; i < curve->get_point_count(); ++i) {
+ Vector2 p = get_view_pos(curve->get_point_position(i));
+ if (hover_rect.has_point(p) && p.distance_squared_to(p_pos) < closest_dist_squared) {
+ closest_dist_squared = p.distance_squared_to(p_pos);
+ closest_idx = i;
+ }
}
-}
-void CurveEditor::open_context_menu(Vector2 pos) {
- _context_menu->set_position(pos);
+ return closest_idx;
+}
- _context_menu->clear();
+CurveEdit::TangentIndex CurveEdit::get_tangent_at(Vector2 p_pos) const {
+ if (curve.is_null() || selected_index < 0) {
+ return TANGENT_NONE;
+ }
- if (_curve_ref.is_valid()) {
- _context_menu->add_item(TTR("Add Point"), CONTEXT_ADD_POINT);
+ const Rect2 hover_rect = Rect2(p_pos, Vector2(0, 0)).grow(tangent_hover_radius);
- if (_selected_point >= 0) {
- _context_menu->add_item(TTR("Remove Point"), CONTEXT_REMOVE_POINT);
+ if (selected_index != 0) {
+ Vector2 control_pos = get_tangent_view_pos(selected_index, TANGENT_LEFT);
+ if (hover_rect.has_point(control_pos)) {
+ return TANGENT_LEFT;
+ }
+ }
- if (_selected_tangent != TANGENT_NONE) {
- _context_menu->add_separator();
+ if (selected_index != curve->get_point_count() - 1) {
+ Vector2 control_pos = get_tangent_view_pos(selected_index, TANGENT_RIGHT);
+ if (hover_rect.has_point(control_pos)) {
+ return TANGENT_RIGHT;
+ }
+ }
- _context_menu->add_check_item(TTR("Linear"), CONTEXT_LINEAR);
+ return TANGENT_NONE;
+}
- bool is_linear = _selected_tangent == TANGENT_LEFT
- ? _curve_ref->get_point_left_mode(_selected_point) == Curve::TANGENT_LINEAR
- : _curve_ref->get_point_right_mode(_selected_point) == Curve::TANGENT_LINEAR;
+// FIXME: This function should be bounded better.
+float CurveEdit::get_offset_without_collision(int p_current_index, float p_offset, bool p_prioritize_right) {
+ float safe_offset = p_offset;
+ bool prioritizing_right = p_prioritize_right;
- _context_menu->set_item_checked(_context_menu->get_item_index(CONTEXT_LINEAR), is_linear);
+ for (int i = 0; i < curve->get_point_count(); i++) {
+ if (i == p_current_index) {
+ continue;
+ }
- } else {
- if (_selected_point > 0 || _selected_point + 1 < _curve_ref->get_point_count()) {
- _context_menu->add_separator();
- }
+ if (curve->get_point_position(i).x > safe_offset) {
+ break;
+ }
- if (_selected_point > 0) {
- _context_menu->add_check_item(TTR("Left Linear"), CONTEXT_LEFT_LINEAR);
- _context_menu->set_item_checked(_context_menu->get_item_index(CONTEXT_LEFT_LINEAR),
- _curve_ref->get_point_left_mode(_selected_point) == Curve::TANGENT_LINEAR);
+ if (curve->get_point_position(i).x == safe_offset) {
+ if (prioritizing_right) {
+ safe_offset += 0.00001;
+ if (safe_offset > 1.0) {
+ safe_offset = 1.0;
+ prioritizing_right = false;
}
- if (_selected_point + 1 < _curve_ref->get_point_count()) {
- _context_menu->add_check_item(TTR("Right Linear"), CONTEXT_RIGHT_LINEAR);
- _context_menu->set_item_checked(_context_menu->get_item_index(CONTEXT_RIGHT_LINEAR),
- _curve_ref->get_point_right_mode(_selected_point) == Curve::TANGENT_LINEAR);
+ } else {
+ safe_offset -= 0.00001;
+ if (safe_offset < 0.0) {
+ safe_offset = 0.0;
+ prioritizing_right = true;
}
}
+ i = -1;
}
-
- _context_menu->add_separator();
}
- _context_menu->add_submenu_item(TTR("Load Preset"), _presets_menu->get_name());
-
- _context_menu->reset_size();
- _context_menu->popup();
+ return safe_offset;
}
-int CurveEditor::get_point_at(Vector2 pos) const {
- if (_curve_ref.is_null()) {
- return -1;
- }
- const Curve &curve = **_curve_ref;
-
- const float true_hover_radius = Math::round(_hover_radius * _gizmo_handle_scale * EDSCALE);
- const float r = true_hover_radius * true_hover_radius;
+void CurveEdit::add_point(Vector2 p_pos) {
+ ERR_FAIL_COND(curve.is_null());
- for (int i = 0; i < curve.get_point_count(); ++i) {
- Vector2 p = get_view_pos(curve.get_point_position(i));
- if (p.distance_squared_to(pos) <= r) {
- return i;
- }
- }
+ // Add a point to get its index, then remove it immediately. Trick to feed the UndoRedo.
+ int new_idx = curve->add_point(p_pos);
+ curve->remove_point(new_idx);
- return -1;
+ EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
+ undo_redo->create_action(TTR("Add Curve Point"));
+ undo_redo->add_do_method(*curve, "add_point", p_pos);
+ undo_redo->add_do_method(this, "set_selected_index", new_idx);
+ undo_redo->add_undo_method(*curve, "remove_point", new_idx);
+ undo_redo->add_undo_method(this, "set_selected_index", -1);
+ undo_redo->commit_action();
}
-CurveEditor::TangentIndex CurveEditor::get_tangent_at(Vector2 pos) const {
- if (_curve_ref.is_null() || _selected_point < 0) {
- return TANGENT_NONE;
- }
+void CurveEdit::remove_point(int p_index) {
+ ERR_FAIL_COND(curve.is_null());
+ ERR_FAIL_INDEX_MSG(p_index, curve->get_point_count(), "Curve point is out of bounds.");
- if (_selected_point != 0) {
- Vector2 control_pos = get_tangent_view_pos(_selected_point, TANGENT_LEFT);
- if (control_pos.distance_to(pos) < _hover_radius * _gizmo_handle_scale) {
- return TANGENT_LEFT;
- }
- }
+ Curve::Point p = curve->get_point(p_index);
+ Vector2 old_pos = (grabbing == GRAB_MOVE) ? initial_grab_pos : p.position;
- if (_selected_point != _curve_ref->get_point_count() - 1) {
- Vector2 control_pos = get_tangent_view_pos(_selected_point, TANGENT_RIGHT);
- if (control_pos.distance_to(pos) < _hover_radius * _gizmo_handle_scale) {
- return TANGENT_RIGHT;
- }
+ int new_selected_index = selected_index;
+ // Reselect the old selected point if it's not the deleted one.
+ if (new_selected_index > p_index) {
+ new_selected_index -= 1;
+ } else if (new_selected_index == p_index) {
+ new_selected_index = -1;
}
- return TANGENT_NONE;
-}
-
-void CurveEditor::add_point(Vector2 pos) {
- ERR_FAIL_COND(_curve_ref.is_null());
-
EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
undo_redo->create_action(TTR("Remove Curve Point"));
+ undo_redo->add_do_method(*curve, "remove_point", p_index);
+ undo_redo->add_do_method(this, "set_selected_index", new_selected_index);
+ undo_redo->add_undo_method(*curve, "add_point", old_pos, p.left_tangent, p.right_tangent, p.left_mode, p.right_mode);
+ undo_redo->add_undo_method(this, "set_selected_index", selected_index);
+ undo_redo->commit_action();
+}
- Vector2 point_pos = get_world_pos(pos);
- if (point_pos.y < 0.0) {
- point_pos.y = 0.0;
- } else if (point_pos.y > 1.0) {
- point_pos.y = 1.0;
- }
+void CurveEdit::set_point_position(int p_index, Vector2 p_pos) {
+ ERR_FAIL_COND(curve.is_null());
+ ERR_FAIL_INDEX_MSG(p_index, curve->get_point_count(), "Curve point is out of bounds.");
- // Small trick to get the point index to feed the undo method
- int i = _curve_ref->add_point(point_pos);
- _curve_ref->remove_point(i);
+ if (initial_grab_pos == p_pos) {
+ return;
+ }
- undo_redo->add_do_method(*_curve_ref, "add_point", point_pos);
- undo_redo->add_undo_method(*_curve_ref, "remove_point", i);
+ // Pretend the point started from its old place.
+ curve->set_point_value(p_index, initial_grab_pos.y);
+ curve->set_point_offset(p_index, initial_grab_pos.x);
+ // Note: Changing the offset may modify the order.
+ EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
+ undo_redo->create_action(TTR("Modify Curve Point"));
+ undo_redo->add_do_method(*curve, "set_point_value", initial_grab_index, p_pos.y);
+ undo_redo->add_do_method(*curve, "set_point_offset", initial_grab_index, p_pos.x);
+ undo_redo->add_do_method(this, "set_selected_index", p_index);
+ undo_redo->add_undo_method(*curve, "set_point_value", p_index, initial_grab_pos.y);
+ undo_redo->add_undo_method(*curve, "set_point_offset", p_index, initial_grab_pos.x);
+ undo_redo->add_undo_method(this, "set_selected_index", initial_grab_index);
undo_redo->commit_action();
}
-void CurveEditor::remove_point(int index) {
- ERR_FAIL_COND(_curve_ref.is_null());
+void CurveEdit::set_point_tangents(int p_index, float p_left, float p_right) {
+ ERR_FAIL_COND(curve.is_null());
+ ERR_FAIL_INDEX_MSG(p_index, curve->get_point_count(), "Curve point is out of bounds.");
- EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
- undo_redo->create_action(TTR("Remove Curve Point"));
-
- Curve::Point p = _curve_ref->get_point(index);
+ if (initial_grab_left_tangent == p_left) {
+ set_point_right_tangent(p_index, p_right);
+ return;
+ } else if (initial_grab_right_tangent == p_right) {
+ set_point_left_tangent(p_index, p_left);
+ return;
+ }
- undo_redo->add_do_method(*_curve_ref, "remove_point", index);
- undo_redo->add_undo_method(*_curve_ref, "add_point", p.position, p.left_tangent, p.right_tangent, p.left_mode, p.right_mode);
+ curve->set_point_left_tangent(p_index, initial_grab_left_tangent);
+ curve->set_point_right_tangent(p_index, initial_grab_right_tangent);
+ EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
+ undo_redo->create_action(TTR("Modify Curve Point's Tangents"));
+ undo_redo->add_do_method(*curve, "set_point_left_tangent", p_index, p_left);
+ undo_redo->add_do_method(*curve, "set_point_right_tangent", p_index, p_right);
+ undo_redo->add_do_method(this, "set_selected_index", p_index);
+ undo_redo->add_undo_method(*curve, "set_point_left_tangent", p_index, initial_grab_left_tangent);
+ undo_redo->add_undo_method(*curve, "set_point_right_tangent", p_index, initial_grab_right_tangent);
+ undo_redo->add_undo_method(this, "set_selected_index", p_index);
+ undo_redo->commit_action();
+}
- if (index == _selected_point) {
- set_selected_point(-1);
- }
+void CurveEdit::set_point_left_tangent(int p_index, float p_tangent) {
+ ERR_FAIL_COND(curve.is_null());
+ ERR_FAIL_INDEX_MSG(p_index, curve->get_point_count(), "Curve point is out of bounds.");
- if (index == _hover_point) {
- set_hover_point_index(-1);
+ if (initial_grab_left_tangent == p_tangent) {
+ return;
}
+ curve->set_point_left_tangent(p_index, initial_grab_left_tangent);
+ EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
+ undo_redo->create_action(TTR("Modify Curve Point's Left Tangent"));
+ undo_redo->add_do_method(*curve, "set_point_left_tangent", p_index, p_tangent);
+ undo_redo->add_do_method(this, "set_selected_index", p_index);
+ undo_redo->add_undo_method(*curve, "set_point_left_tangent", p_index, initial_grab_left_tangent);
+ undo_redo->add_undo_method(this, "set_selected_index", p_index);
undo_redo->commit_action();
}
-void CurveEditor::toggle_linear(TangentIndex tangent) {
- ERR_FAIL_COND(_curve_ref.is_null());
+void CurveEdit::set_point_right_tangent(int p_index, float p_tangent) {
+ ERR_FAIL_COND(curve.is_null());
+ ERR_FAIL_INDEX_MSG(p_index, curve->get_point_count(), "Curve point is out of bounds.");
- EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
- undo_redo->create_action(TTR("Toggle Curve Linear Tangent"));
-
- if (tangent == TANGENT_NONE) {
- tangent = _selected_tangent;
+ if (initial_grab_right_tangent == p_tangent) {
+ return;
}
- if (tangent == TANGENT_LEFT) {
- bool is_linear = _curve_ref->get_point_left_mode(_selected_point) == Curve::TANGENT_LINEAR;
+ curve->set_point_right_tangent(p_index, initial_grab_right_tangent);
+ EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
+ undo_redo->create_action(TTR("Modify Curve Point's Right Tangent"));
+ undo_redo->add_do_method(*curve, "set_point_right_tangent", p_index, p_tangent);
+ undo_redo->add_do_method(this, "set_selected_index", p_index);
+ undo_redo->add_undo_method(*curve, "set_point_right_tangent", p_index, initial_grab_right_tangent);
+ undo_redo->add_undo_method(this, "set_selected_index", p_index);
+ undo_redo->commit_action();
+}
- Curve::TangentMode prev_mode = _curve_ref->get_point_left_mode(_selected_point);
- Curve::TangentMode mode = is_linear ? Curve::TANGENT_FREE : Curve::TANGENT_LINEAR;
+void CurveEdit::toggle_linear(int p_index, TangentIndex p_tangent) {
+ ERR_FAIL_COND(curve.is_null());
+ ERR_FAIL_INDEX_MSG(p_index, curve->get_point_count(), "Curve point is out of bounds.");
- undo_redo->add_do_method(*_curve_ref, "set_point_left_mode", _selected_point, mode);
- undo_redo->add_undo_method(*_curve_ref, "set_point_left_mode", _selected_point, prev_mode);
+ if (p_tangent == TANGENT_NONE) {
+ return;
+ }
- } else {
- bool is_linear = _curve_ref->get_point_right_mode(_selected_point) == Curve::TANGENT_LINEAR;
+ EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
+ undo_redo->create_action(TTR("Toggle Linear Curve Point's Tangent"));
- Curve::TangentMode prev_mode = _curve_ref->get_point_right_mode(_selected_point);
- Curve::TangentMode mode = is_linear ? Curve::TANGENT_FREE : Curve::TANGENT_LINEAR;
+ Curve::TangentMode prev_mode = (p_tangent == TANGENT_LEFT) ? curve->get_point_left_mode(p_index) : curve->get_point_right_mode(p_index);
+ Curve::TangentMode mode = (prev_mode == Curve::TANGENT_LINEAR) ? Curve::TANGENT_FREE : Curve::TANGENT_LINEAR;
+ float prev_angle = (p_tangent == TANGENT_LEFT) ? curve->get_point_left_tangent(p_index) : curve->get_point_right_tangent(p_index);
- undo_redo->add_do_method(*_curve_ref, "set_point_right_mode", _selected_point, mode);
- undo_redo->add_undo_method(*_curve_ref, "set_point_right_mode", _selected_point, prev_mode);
+ // Add different methods in the UndoRedo based on the tangent passed.
+ if (p_tangent == TANGENT_LEFT) {
+ undo_redo->add_do_method(*curve, "set_point_left_mode", p_index, mode);
+ undo_redo->add_undo_method(*curve, "set_point_left_mode", p_index, prev_mode);
+ undo_redo->add_undo_method(*curve, "set_point_left_tangent", p_index, prev_angle);
+ } else {
+ undo_redo->add_do_method(*curve, "set_point_right_mode", p_index, mode);
+ undo_redo->add_undo_method(*curve, "set_point_right_mode", p_index, prev_mode);
+ undo_redo->add_undo_method(*curve, "set_point_right_tangent", p_index, prev_angle);
}
undo_redo->commit_action();
}
-void CurveEditor::set_selected_point(int index) {
- if (index != _selected_point) {
- _selected_point = index;
+void CurveEdit::set_selected_index(int p_index) {
+ if (p_index != selected_index) {
+ selected_index = p_index;
queue_redraw();
}
}
-void CurveEditor::set_hover_point_index(int index) {
- if (index != _hover_point) {
- _hover_point = index;
- queue_redraw();
- }
-}
-
-void CurveEditor::update_view_transform() {
+void CurveEdit::update_view_transform() {
Ref<Font> font = get_theme_font(SNAME("font"), SNAME("Label"));
int font_size = get_theme_font_size(SNAME("font_size"), SNAME("Label"));
const real_t margin = font->get_height(font_size) + 2 * EDSCALE;
- float min_y = 0;
- float max_y = 1;
-
- if (_curve_ref.is_valid()) {
- min_y = _curve_ref->get_min_value();
- max_y = _curve_ref->get_max_value();
- }
+ float min_y = curve.is_valid() ? curve->get_min_value() : 0.0;
+ float max_y = curve.is_valid() ? curve->get_max_value() : 1.0;
const Rect2 world_rect = Rect2(Curve::MIN_X, min_y, Curve::MAX_X, max_y - min_y);
const Size2 view_margin(margin, margin);
@@ -557,43 +672,62 @@ void CurveEditor::update_view_transform() {
_world_to_view = view_trans * world_trans;
}
-Vector2 CurveEditor::get_tangent_view_pos(int i, TangentIndex tangent) const {
+Vector2 CurveEdit::get_tangent_view_pos(int p_index, TangentIndex p_tangent) const {
Vector2 dir;
- if (tangent == TANGENT_LEFT) {
- dir = -Vector2(1, _curve_ref->get_point_left_tangent(i));
+ if (p_tangent == TANGENT_LEFT) {
+ dir = -Vector2(1, curve->get_point_left_tangent(p_index));
} else {
- dir = Vector2(1, _curve_ref->get_point_right_tangent(i));
+ dir = Vector2(1, curve->get_point_right_tangent(p_index));
}
- Vector2 point_pos = get_view_pos(_curve_ref->get_point_position(i));
- Vector2 control_pos = get_view_pos(_curve_ref->get_point_position(i) + dir);
+ Vector2 point_pos = curve->get_point_position(p_index);
+ Vector2 point_view_pos = get_view_pos(point_pos);
+ Vector2 control_view_pos = get_view_pos(point_pos + dir);
- return point_pos + Math::round(_tangents_length * _gizmo_handle_scale * EDSCALE) * (control_pos - point_pos).normalized();
+ Vector2 distance_from_point = tangent_length * (control_view_pos - point_view_pos).normalized();
+ Vector2 tangent_view_pos = point_view_pos + distance_from_point;
+
+ // Since the tangent is long, it might slip outside of the area of the editor for points close to the domain/range boundaries.
+ // The code below shrinks the tangent control by up to 50% so it always stays inside the editor for points within the bounds.
+ float fraction_inside = 1.0;
+ if (distance_from_point.x != 0.0) {
+ fraction_inside = MIN(fraction_inside, ((distance_from_point.x > 0 ? get_rect().size.x : 0) - point_view_pos.x) / distance_from_point.x);
+ }
+ if (distance_from_point.y != 0.0) {
+ fraction_inside = MIN(fraction_inside, ((distance_from_point.y > 0 ? get_rect().size.y : 0) - point_view_pos.y) / distance_from_point.y);
+ }
+
+ if (fraction_inside < 1.0 && fraction_inside > 0.5) {
+ tangent_view_pos = point_view_pos + distance_from_point * fraction_inside;
+ }
+
+ return tangent_view_pos;
}
-Vector2 CurveEditor::get_view_pos(Vector2 world_pos) const {
- return _world_to_view.xform(world_pos);
+Vector2 CurveEdit::get_view_pos(Vector2 p_world_pos) const {
+ return _world_to_view.xform(p_world_pos);
}
-Vector2 CurveEditor::get_world_pos(Vector2 view_pos) const {
- return _world_to_view.affine_inverse().xform(view_pos);
+Vector2 CurveEdit::get_world_pos(Vector2 p_view_pos) const {
+ return _world_to_view.affine_inverse().xform(p_view_pos);
}
-// Uses non-baked points, but takes advantage of ordered iteration to be faster
+// Uses non-baked points, but takes advantage of ordered iteration to be faster.
template <typename T>
-static void plot_curve_accurate(const Curve &curve, float step, T plot_func) {
+static void plot_curve_accurate(const Curve &curve, float step, Vector2 scaling, T plot_func) {
if (curve.get_point_count() <= 1) {
- // Not enough points to make a curve, so it's just a straight line
+ // Not enough points to make a curve, so it's just a straight line.
+ // The added tiny vectors make the drawn line stay exactly within the bounds in practice.
float y = curve.sample(0);
- plot_func(Vector2(0, y), Vector2(1.f, y), true);
+ plot_func(Vector2(0, y) * scaling + Vector2(0.5, 0), Vector2(1.f, y) * scaling - Vector2(1.5, 0), true);
} else {
Vector2 first_point = curve.get_point_position(0);
Vector2 last_point = curve.get_point_position(curve.get_point_count() - 1);
// Edge lines
- plot_func(Vector2(0, first_point.y), first_point, false);
- plot_func(Vector2(Curve::MAX_X, last_point.y), last_point, false);
+ plot_func(Vector2(0, first_point.y) * scaling + Vector2(0.5, 0), first_point * scaling, false);
+ plot_func(Vector2(Curve::MAX_X, last_point.y) * scaling - Vector2(1.5, 0), last_point * scaling, false);
// Draw section by section, so that we get maximum precision near points.
// It's an accurate representation, but slower than using the baked one.
@@ -604,16 +738,18 @@ static void plot_curve_accurate(const Curve &curve, float step, T plot_func) {
Vector2 pos = a;
Vector2 prev_pos = a;
- float len = b.x - a.x;
+ float scaled_step = step / scaling.x;
+ float samples = (b.x - a.x) / scaled_step;
- for (float x = step; x < len; x += step) {
+ for (int j = 1; j < samples; j++) {
+ float x = j * scaled_step;
pos.x = a.x + x;
pos.y = curve.sample_local_nocheck(i - 1, x);
- plot_func(prev_pos, pos, true);
+ plot_func(prev_pos * scaling, pos * scaling, true);
prev_pos = pos;
}
- plot_func(prev_pos, b, true);
+ plot_func(prev_pos * scaling, b * scaling, true);
}
}
}
@@ -629,50 +765,50 @@ struct CanvasItemPlotCurve {
color2(p_color2) {}
void operator()(Vector2 pos0, Vector2 pos1, bool in_definition) {
- // FIXME: Using a quad line breaks curve rendering.
- ci.draw_line(pos0, pos1, in_definition ? color1 : color2, -1);
+ ci.draw_line(pos0, pos1, in_definition ? color1 : color2, 0.5, true);
}
};
-void CurveEditor::_draw() {
- if (_curve_ref.is_null()) {
+void CurveEdit::_redraw() {
+ if (curve.is_null()) {
return;
}
- Curve &curve = **_curve_ref;
update_view_transform();
- // Background
+ // Draw background.
Vector2 view_size = get_rect().size;
draw_style_box(get_theme_stylebox(SNAME("panel"), SNAME("Tree")), Rect2(Point2(), view_size));
- // Grid
-
+ // Draw snapping grid, then primary grid.
draw_set_transform_matrix(_world_to_view);
Vector2 min_edge = get_world_pos(Vector2(0, view_size.y));
Vector2 max_edge = get_world_pos(Vector2(view_size.x, 0));
- const Color grid_color0 = get_theme_color(SNAME("mono_color"), SNAME("Editor")) * Color(1, 1, 1, 0.15);
- const Color grid_color1 = get_theme_color(SNAME("mono_color"), SNAME("Editor")) * Color(1, 1, 1, 0.07);
- draw_line(Vector2(min_edge.x, curve.get_min_value()), Vector2(max_edge.x, curve.get_min_value()), grid_color0);
- draw_line(Vector2(max_edge.x, curve.get_max_value()), Vector2(min_edge.x, curve.get_max_value()), grid_color0);
- draw_line(Vector2(0, min_edge.y), Vector2(0, max_edge.y), grid_color0);
- draw_line(Vector2(1, max_edge.y), Vector2(1, min_edge.y), grid_color0);
+ const Color grid_color_primary = get_theme_color(SNAME("mono_color"), SNAME("Editor")) * Color(1, 1, 1, 0.25);
+ const Color grid_color = get_theme_color(SNAME("mono_color"), SNAME("Editor")) * Color(1, 1, 1, 0.1);
- float curve_height = (curve.get_max_value() - curve.get_min_value());
- const Vector2 grid_step(0.25, 0.5 * curve_height);
+ const Vector2i grid_steps = Vector2i(4, 2);
+ const Vector2 step_size = Vector2(1, curve->get_range()) / grid_steps;
- for (real_t x = 0; x < 1.0; x += grid_step.x) {
- draw_line(Vector2(x, min_edge.y), Vector2(x, max_edge.y), grid_color1);
- }
- for (real_t y = curve.get_min_value(); y < curve.get_max_value(); y += grid_step.y) {
- draw_line(Vector2(min_edge.x, y), Vector2(max_edge.x, y), grid_color1);
+ draw_line(Vector2(min_edge.x, curve->get_min_value()), Vector2(max_edge.x, curve->get_min_value()), grid_color_primary);
+ draw_line(Vector2(max_edge.x, curve->get_max_value()), Vector2(min_edge.x, curve->get_max_value()), grid_color_primary);
+ draw_line(Vector2(0, min_edge.y), Vector2(0, max_edge.y), grid_color_primary);
+ draw_line(Vector2(1, max_edge.y), Vector2(1, min_edge.y), grid_color_primary);
+
+ for (int i = 1; i < grid_steps.x; i++) {
+ real_t x = i * step_size.x;
+ draw_line(Vector2(x, min_edge.y), Vector2(x, max_edge.y), grid_color);
}
- // Markings
+ for (int i = 1; i < grid_steps.y; i++) {
+ real_t y = curve->get_min_value() + i * step_size.y;
+ draw_line(Vector2(min_edge.x, y), Vector2(max_edge.x, y), grid_color);
+ }
+ // Draw number markings.
draw_set_transform_matrix(Transform2D());
Ref<Font> font = get_theme_font(SNAME("font"), SNAME("Label"));
@@ -680,94 +816,218 @@ void CurveEditor::_draw() {
float font_height = font->get_height(font_size);
Color text_color = get_theme_color(SNAME("font_color"), SNAME("Editor"));
- {
- // X axis
- float y = curve.get_min_value();
- Vector2 off(0, font_height - 1);
- draw_string(font, get_view_pos(Vector2(0, y)) + off, "0.0", HORIZONTAL_ALIGNMENT_LEFT, -1, font_size, text_color);
- draw_string(font, get_view_pos(Vector2(0.25, y)) + off, "0.25", HORIZONTAL_ALIGNMENT_LEFT, -1, font_size, text_color);
- draw_string(font, get_view_pos(Vector2(0.5, y)) + off, "0.5", HORIZONTAL_ALIGNMENT_LEFT, -1, font_size, text_color);
- draw_string(font, get_view_pos(Vector2(0.75, y)) + off, "0.75", HORIZONTAL_ALIGNMENT_LEFT, -1, font_size, text_color);
- draw_string(font, get_view_pos(Vector2(1, y)) + off, "1.0", HORIZONTAL_ALIGNMENT_LEFT, -1, font_size, text_color);
+ for (int i = 0; i <= grid_steps.x; ++i) {
+ real_t x = i * step_size.x;
+ draw_string(font, get_view_pos(Vector2(x - step_size.x / 2, curve->get_min_value())) + Vector2(0, font_height - Math::round(2 * EDSCALE)), String::num(x, 2), HORIZONTAL_ALIGNMENT_CENTER, get_view_pos(Vector2(step_size.x, 0)).x, font_size, text_color);
}
- {
- // Y axis
- float m0 = curve.get_min_value();
- float m1 = 0.5 * (curve.get_min_value() + curve.get_max_value());
- float m2 = curve.get_max_value();
- Vector2 off(1, -1);
- draw_string(font, get_view_pos(Vector2(0, m0)) + off, String::num(m0, 2), HORIZONTAL_ALIGNMENT_LEFT, -1, font_size, text_color);
- draw_string(font, get_view_pos(Vector2(0, m1)) + off, String::num(m1, 2), HORIZONTAL_ALIGNMENT_LEFT, -1, font_size, text_color);
- draw_string(font, get_view_pos(Vector2(0, m2)) + off, String::num(m2, 3), HORIZONTAL_ALIGNMENT_LEFT, -1, font_size, text_color);
+ for (int i = 0; i <= grid_steps.y; ++i) {
+ real_t y = curve->get_min_value() + i * step_size.y;
+ draw_string(font, get_view_pos(Vector2(0, y)) + Vector2(2, -2), String::num(y, 2), HORIZONTAL_ALIGNMENT_LEFT, -1, font_size, text_color);
}
- // Draw tangents for current point
+ // Draw curve.
- if (_selected_point >= 0) {
- const Color tangent_color = get_theme_color(SNAME("accent_color"), SNAME("Editor"));
+ // An unusual transform so we can offset the curve before scaling it up, allowing the curve to be antialiased.
+ // The scaling up ensures that the curve rendering doesn't break when we use a quad line to draw it.
+ draw_set_transform_matrix(Transform2D(0, get_view_pos(Vector2(0, 0))));
- int i = _selected_point;
- Vector2 pos = curve.get_point_position(i);
+ const Color line_color = get_theme_color(SNAME("font_color"), SNAME("Editor"));
+ const Color edge_line_color = get_theme_color(SNAME("font_color"), SNAME("Editor")) * Color(1, 1, 1, 0.75);
- if (i != 0) {
- Vector2 control_pos = get_tangent_view_pos(i, TANGENT_LEFT);
- draw_line(get_view_pos(pos), control_pos, tangent_color, Math::round(EDSCALE));
- draw_rect(Rect2(control_pos, Vector2(1, 1)).grow(Math::round(2 * _gizmo_handle_scale * EDSCALE)), tangent_color);
- }
+ CanvasItemPlotCurve plot_func(*this, line_color, edge_line_color);
+ plot_curve_accurate(**curve, 2.f, (get_view_pos(Vector2(1, curve->get_max_value())) - get_view_pos(Vector2(0, curve->get_min_value()))) / Vector2(1, curve->get_range()), plot_func);
+
+ // Draw points, except for the selected one.
+ draw_set_transform_matrix(Transform2D());
+
+ bool shift_pressed = Input::get_singleton()->is_key_pressed(Key::SHIFT);
+
+ const Color point_color = get_theme_color(SNAME("font_color"), SNAME("Editor"));
- if (i != curve.get_point_count() - 1) {
- Vector2 control_pos = get_tangent_view_pos(i, TANGENT_RIGHT);
- draw_line(get_view_pos(pos), control_pos, tangent_color, Math::round(EDSCALE));
- draw_rect(Rect2(control_pos, Vector2(1, 1)).grow(Math::round(2 * _gizmo_handle_scale * EDSCALE)), tangent_color);
+ for (int i = 0; i < curve->get_point_count(); ++i) {
+ Vector2 pos = get_view_pos(curve->get_point_position(i));
+ if (selected_index != i) {
+ draw_rect(Rect2(pos, Vector2(0, 0)).grow(point_radius), point_color);
+ }
+ if (hovered_index == i && hovered_tangent_index == TANGENT_NONE) {
+ draw_rect(Rect2(pos, Vector2(0, 0)).grow(hover_radius - Math::round(3 * EDSCALE)), line_color, false, Math::round(1 * EDSCALE));
}
}
- // Draw lines
+ // Draw selected point and its tangents.
- draw_set_transform_matrix(_world_to_view);
+ if (selected_index >= 0) {
+ const Vector2 point_pos = curve->get_point_position(selected_index);
+ const Color selected_point_color = get_theme_color(SNAME("accent_color"), SNAME("Editor"));
- const Color line_color = get_theme_color(SNAME("font_color"), SNAME("Editor"));
- const Color edge_line_color = get_theme_color(SNAME("highlight_color"), SNAME("Editor"));
+ // Draw tangents if not dragging a point, or if holding a point without having moved it yet.
+ if (grabbing == GRAB_NONE || (grabbing != GRAB_NONE && (initial_grab_pos == point_pos || selected_tangent_index != TANGENT_NONE))) {
+ const Color selected_tangent_color = get_theme_color(SNAME("accent_color"), SNAME("Editor")).darkened(0.25);
+ const Color tangent_color = get_theme_color(SNAME("font_color"), SNAME("Editor")).darkened(0.25);
- CanvasItemPlotCurve plot_func(*this, line_color, edge_line_color);
- plot_curve_accurate(curve, 4.f / view_size.x, plot_func);
+ if (selected_index != 0) {
+ Vector2 control_pos = get_tangent_view_pos(selected_index, TANGENT_LEFT);
+ Color left_tangent_color = (selected_tangent_index == TANGENT_LEFT) ? selected_tangent_color : tangent_color;
- // Draw points
+ draw_line(get_view_pos(point_pos), control_pos, left_tangent_color, 0.5 * EDSCALE, true);
+ // Square for linear mode, circle otherwise.
+ if (curve->get_point_left_mode(selected_index) == Curve::TANGENT_FREE) {
+ draw_circle(control_pos, tangent_radius, left_tangent_color);
+ } else {
+ draw_rect(Rect2(control_pos, Vector2(0, 0)).grow(tangent_radius), left_tangent_color);
+ }
+ // Hover indicator.
+ if (hovered_tangent_index == TANGENT_LEFT || (hovered_tangent_index == TANGENT_RIGHT && !shift_pressed && curve->get_point_left_mode(selected_index) != Curve::TANGENT_LINEAR)) {
+ draw_rect(Rect2(control_pos, Vector2(0, 0)).grow(tangent_hover_radius - Math::round(3 * EDSCALE)), tangent_color, false, Math::round(1 * EDSCALE));
+ }
+ }
- draw_set_transform_matrix(Transform2D());
+ if (selected_index != curve->get_point_count() - 1) {
+ Vector2 control_pos = get_tangent_view_pos(selected_index, TANGENT_RIGHT);
+ Color right_tangent_color = (selected_tangent_index == TANGENT_RIGHT) ? selected_tangent_color : tangent_color;
- const Color point_color = get_theme_color(SNAME("font_color"), SNAME("Editor"));
- const Color selected_point_color = get_theme_color(SNAME("accent_color"), SNAME("Editor"));
+ draw_line(get_view_pos(point_pos), control_pos, right_tangent_color, 0.5 * EDSCALE, true);
+ // Square for linear mode, circle otherwise.
+ if (curve->get_point_right_mode(selected_index) == Curve::TANGENT_FREE) {
+ draw_circle(control_pos, tangent_radius, right_tangent_color);
+ } else {
+ draw_rect(Rect2(control_pos, Vector2(0, 0)).grow(tangent_radius), right_tangent_color);
+ }
+ // Hover indicator.
+ if (hovered_tangent_index == TANGENT_RIGHT || (hovered_tangent_index == TANGENT_LEFT && !shift_pressed && curve->get_point_right_mode(selected_index) != Curve::TANGENT_LINEAR)) {
+ draw_rect(Rect2(control_pos, Vector2(0, 0)).grow(tangent_hover_radius - Math::round(3 * EDSCALE)), tangent_color, false, Math::round(1 * EDSCALE));
+ }
+ }
+ }
- for (int i = 0; i < curve.get_point_count(); ++i) {
- Vector2 pos = curve.get_point_position(i);
- draw_rect(Rect2(get_view_pos(pos), Vector2(1, 1)).grow(Math::round(3 * _gizmo_handle_scale * EDSCALE)), i == _selected_point ? selected_point_color : point_color);
- // TODO Circles are prettier. Needs a fix! Or a texture
- //draw_circle(pos, 2, point_color);
+ draw_rect(Rect2(get_view_pos(point_pos), Vector2(0, 0)).grow(point_radius), selected_point_color);
}
- // Hover
+ // Draw help text.
+
+ if (selected_index > 0 && selected_index < curve->get_point_count() - 1 && selected_tangent_index == TANGENT_NONE && hovered_tangent_index != TANGENT_NONE && !shift_pressed) {
+ float width = view_size.x - 50 * EDSCALE;
+ text_color.a *= 0.4;
+
+ draw_multiline_string(font, Vector2(25 * EDSCALE, font_height - Math::round(2 * EDSCALE)), TTR("Hold Shift to edit tangents individually"), HORIZONTAL_ALIGNMENT_CENTER, width, font_size, -1, text_color);
- if (_hover_point != -1) {
- const Color hover_color = line_color;
- Vector2 pos = curve.get_point_position(_hover_point);
- draw_rect(Rect2(get_view_pos(pos), Vector2(1, 1)).grow(Math::round(_hover_radius * _gizmo_handle_scale * EDSCALE)), hover_color, false, Math::round(EDSCALE));
+ } else if (selected_index != -1 && selected_tangent_index == TANGENT_NONE) {
+ const Vector2 point_pos = curve->get_point_position(selected_index);
+ float width = view_size.x - 50 * EDSCALE;
+ text_color.a *= 0.8;
+
+ draw_string(font, Vector2(25 * EDSCALE, font_height - Math::round(2 * EDSCALE)), vformat("(%.2f, %.2f)", point_pos.x, point_pos.y), HORIZONTAL_ALIGNMENT_CENTER, width, font_size, text_color);
+
+ } else if (selected_index != -1 && selected_tangent_index != TANGENT_NONE) {
+ float width = view_size.x - 50 * EDSCALE;
+ text_color.a *= 0.8;
+ real_t theta = Math::rad_to_deg(Math::atan(selected_tangent_index == TANGENT_LEFT ? -1 * curve->get_point_left_tangent(selected_index) : curve->get_point_right_tangent(selected_index)));
+
+ draw_string(font, Vector2(25 * EDSCALE, font_height - Math::round(2 * EDSCALE)), String::num(theta, 1) + String::utf8(" °"), HORIZONTAL_ALIGNMENT_CENTER, width, font_size, text_color);
}
- // Help text
+ // Draw temporary constraints and snapping axes.
+ draw_set_transform_matrix(_world_to_view);
- float width = view_size.x - 60 * EDSCALE;
- if (_selected_point > 0 && _selected_point + 1 < curve.get_point_count()) {
- text_color.a *= 0.4;
- draw_multiline_string(font, Vector2(50 * EDSCALE, font_height), TTR("Hold Shift to edit tangents individually"), HORIZONTAL_ALIGNMENT_LEFT, width, font_size, -1, text_color);
- } else if (curve.get_point_count() == 0) {
- text_color.a *= 0.4;
- draw_multiline_string(font, Vector2(50 * EDSCALE, font_height), TTR("Right click to add point"), HORIZONTAL_ALIGNMENT_LEFT, width, font_size, -1, text_color);
+ if (Input::get_singleton()->is_key_pressed(Key::ALT) && grabbing != GRAB_NONE && selected_tangent_index == TANGENT_NONE) {
+ float prev_point_offset = (selected_index > 0) ? curve->get_point_position(selected_index - 1).x : 0.0;
+ float next_point_offset = (selected_index < curve->get_point_count() - 1) ? curve->get_point_position(selected_index + 1).x : 1.0;
+
+ draw_line(Vector2(prev_point_offset, curve->get_min_value()), Vector2(prev_point_offset, curve->get_max_value()), Color(point_color, 0.6));
+ draw_line(Vector2(next_point_offset, curve->get_min_value()), Vector2(next_point_offset, curve->get_max_value()), Color(point_color, 0.6));
+ }
+
+ if (shift_pressed && grabbing != GRAB_NONE && selected_tangent_index == TANGENT_NONE) {
+ draw_line(Vector2(initial_grab_pos.x, curve->get_min_value()), Vector2(initial_grab_pos.x, curve->get_max_value()), get_theme_color(SNAME("axis_x_color"), SNAME("Editor")).darkened(0.4));
+ draw_line(Vector2(0, initial_grab_pos.y), Vector2(1, initial_grab_pos.y), get_theme_color(SNAME("axis_y_color"), SNAME("Editor")).darkened(0.4));
}
}
-//---------------
+///////////////////////
+
+const int CurveEditor::DEFAULT_SNAP = 10;
+
+void CurveEditor::_set_snap_enabled(bool p_enabled) {
+ curve_editor_rect->set_snap_enabled(p_enabled);
+ snap_count_edit->set_visible(p_enabled);
+}
+
+void CurveEditor::_set_snap_count(int p_snap_count) {
+ curve_editor_rect->set_snap_count(CLAMP(p_snap_count, 2, 100));
+}
+
+void CurveEditor::_on_preset_item_selected(int p_preset_id) {
+ curve_editor_rect->use_preset(p_preset_id);
+}
+
+void CurveEditor::set_curve(const Ref<Curve> &p_curve) {
+ curve_editor_rect->set_curve(p_curve);
+}
+
+void CurveEditor::_notification(int p_what) {
+ switch (p_what) {
+ case NOTIFICATION_THEME_CHANGED: {
+ spacing = Math::round(BASE_SPACING * get_theme_default_base_scale());
+ snap_button->set_icon(get_theme_icon(SNAME("SnapGrid"), SNAME("EditorIcons")));
+ PopupMenu *p = presets_button->get_popup();
+ p->clear();
+ p->add_icon_item(get_theme_icon(SNAME("CurveConstant"), SNAME("EditorIcons")), TTR("Constant"), CurveEdit::PRESET_CONSTANT);
+ p->add_icon_item(get_theme_icon(SNAME("CurveLinear"), SNAME("EditorIcons")), TTR("Linear"), CurveEdit::PRESET_LINEAR);
+ p->add_icon_item(get_theme_icon(SNAME("CurveIn"), SNAME("EditorIcons")), TTR("Ease In"), CurveEdit::PRESET_EASE_IN);
+ p->add_icon_item(get_theme_icon(SNAME("CurveOut"), SNAME("EditorIcons")), TTR("Ease Out"), CurveEdit::PRESET_EASE_OUT);
+ p->add_icon_item(get_theme_icon(SNAME("CurveInOut"), SNAME("EditorIcons")), TTR("Smoothstep"), CurveEdit::PRESET_SMOOTHSTEP);
+ } break;
+ case NOTIFICATION_READY: {
+ Ref<Curve> curve = curve_editor_rect->get_curve();
+ // Set snapping settings based on the curve's meta.
+ snap_button->set_pressed(curve->get_meta("_snap_enabled", false));
+ snap_count_edit->set_value(curve->get_meta("_snap_count", DEFAULT_SNAP));
+ } break;
+ }
+}
+
+CurveEditor::CurveEditor() {
+ HFlowContainer *toolbar = memnew(HFlowContainer);
+ add_child(toolbar);
+
+ snap_button = memnew(Button);
+ snap_button->set_tooltip_text(TTR("Toggle Grid Snap"));
+ snap_button->set_toggle_mode(true);
+ toolbar->add_child(snap_button);
+ snap_button->connect("toggled", callable_mp(this, &CurveEditor::_set_snap_enabled));
+
+ toolbar->add_child(memnew(VSeparator));
+
+ snap_count_edit = memnew(EditorSpinSlider);
+ snap_count_edit->set_min(2);
+ snap_count_edit->set_max(100);
+ snap_count_edit->set_value(DEFAULT_SNAP);
+ snap_count_edit->set_custom_minimum_size(Size2(65 * EDSCALE, 0));
+ toolbar->add_child(snap_count_edit);
+ snap_count_edit->connect("value_changed", callable_mp(this, &CurveEditor::_set_snap_count));
+
+ presets_button = memnew(MenuButton);
+ presets_button->set_text(TTR("Presets"));
+ presets_button->set_switch_on_hover(true);
+ presets_button->set_h_size_flags(SIZE_EXPAND | SIZE_SHRINK_END);
+ toolbar->add_child(presets_button);
+ presets_button->get_popup()->connect("id_pressed", callable_mp(this, &CurveEditor::_on_preset_item_selected));
+
+ curve_editor_rect = memnew(CurveEdit);
+ add_child(curve_editor_rect);
+
+ // Some empty space below. Not a part of the curve editor so it can't draw in it.
+ Control *empty_space = memnew(Control);
+ empty_space->set_custom_minimum_size(Vector2(0, spacing));
+ add_child(empty_space);
+
+ set_mouse_filter(MOUSE_FILTER_STOP);
+ _set_snap_enabled(snap_button->is_pressed());
+ _set_snap_count(snap_count_edit->get_value());
+}
+
+///////////////////////
bool EditorInspectorPluginCurve::can_handle(Object *p_object) {
return Object::cast_to<Curve>(p_object) != nullptr;
@@ -779,73 +1039,58 @@ void EditorInspectorPluginCurve::parse_begin(Object *p_object) {
Ref<Curve> c(curve);
CurveEditor *editor = memnew(CurveEditor);
- editor->set_curve(curve);
+ editor->set_curve(c);
add_custom_control(editor);
}
CurveEditorPlugin::CurveEditorPlugin() {
- Ref<EditorInspectorPluginCurve> curve_plugin;
- curve_plugin.instantiate();
- EditorInspector::add_inspector_plugin(curve_plugin);
+ Ref<EditorInspectorPluginCurve> plugin;
+ plugin.instantiate();
+ add_inspector_plugin(plugin);
EditorInterface::get_singleton()->get_resource_previewer()->add_preview_generator(memnew(CurvePreviewGenerator));
}
-//-----------------------------------
-// Preview generator
+///////////////////////
bool CurvePreviewGenerator::handles(const String &p_type) const {
return p_type == "Curve";
}
Ref<Texture2D> CurvePreviewGenerator::generate(const Ref<Resource> &p_from, const Size2 &p_size, Dictionary &p_metadata) const {
- Ref<Curve> curve_ref = p_from;
- ERR_FAIL_COND_V_MSG(curve_ref.is_null(), Ref<Texture2D>(), "It's not a reference to a valid Resource object.");
- Curve &curve = **curve_ref;
+ Ref<Curve> curve = p_from;
+ if (curve.is_null()) {
+ return Ref<Texture2D>();
+ }
- // FIXME: Should be ported to use p_size as done in b2633a97
- int thumbnail_size = EDITOR_GET("filesystem/file_dialog/thumbnail_size");
- thumbnail_size *= EDSCALE;
+ Size2 thumbnail_size = p_size * EDSCALE;
Ref<Image> img_ref;
img_ref.instantiate();
Image &im = **img_ref;
-
- im.initialize_data(thumbnail_size, thumbnail_size / 2, false, Image::FORMAT_RGBA8);
+ im.initialize_data(thumbnail_size.x, thumbnail_size.y, false, Image::FORMAT_RGBA8);
Color bg_color(0.1, 0.1, 0.1, 1.0);
-
- im.fill(bg_color);
-
Color line_color(0.8, 0.8, 0.8, 1.0);
- float range_y = curve.get_max_value() - curve.get_min_value();
- int prev_y = 0;
- for (int x = 0; x < im.get_width(); ++x) {
+ im.fill(bg_color);
+ // Set the first pixel of the thumbnail.
+ float v = (curve->sample_baked(0) - curve->get_min_value()) / curve->get_range();
+ int y = CLAMP(im.get_height() - v * im.get_height(), 0, im.get_height() - 1);
+ im.set_pixel(0, y, line_color);
+
+ // Plot a line towards the next point.
+ int prev_y = y;
+ for (int x = 1; x < im.get_width(); ++x) {
float t = static_cast<float>(x) / im.get_width();
- float v = (curve.sample_baked(t) - curve.get_min_value()) / range_y;
- int y = CLAMP(im.get_height() - v * im.get_height(), 0, im.get_height());
+ v = (curve->sample_baked(t) - curve->get_min_value()) / curve->get_range();
+ y = CLAMP(im.get_height() - v * im.get_height(), 0, im.get_height() - 1);
- // Plot point
- if (y >= 0 && y < im.get_height()) {
- im.set_pixel(x, y, line_color);
+ Vector<Point2i> points = Geometry2D::bresenham_line(Point2i(x - 1, prev_y), Point2i(x, y));
+ for (Point2i point : points) {
+ im.set_pixelv(point, line_color);
}
-
- // Plot vertical line to fix discontinuity (not 100% correct but enough for a preview)
- if (x != 0 && Math::abs(y - prev_y) > 1) {
- int y0, y1;
- if (y < prev_y) {
- y0 = y;
- y1 = prev_y;
- } else {
- y0 = prev_y;
- y1 = y;
- }
- for (int ly = y0; ly < y1; ++ly) {
- im.set_pixel(x, ly, line_color);
- }
- }
-
prev_y = y;
}
+
return ImageTexture::create_from_image(img_ref);
}
diff --git a/editor/plugins/curve_editor_plugin.h b/editor/plugins/curve_editor_plugin.h
index 903f8d593e..0f582fca9a 100644
--- a/editor/plugins/curve_editor_plugin.h
+++ b/editor/plugins/curve_editor_plugin.h
@@ -36,22 +36,27 @@
#include "editor/editor_resource_preview.h"
#include "scene/resources/curve.h"
+class EditorSpinSlider;
+class MenuButton;
class PopupMenu;
-// Edits a y(x) curve
-class CurveEditor : public Control {
- GDCLASS(CurveEditor, Control);
+class CurveEdit : public Control {
+ GDCLASS(CurveEdit, Control);
public:
- CurveEditor();
+ CurveEdit();
- Size2 get_minimum_size() const override;
+ void set_snap_enabled(bool p_enabled);
+ void set_snap_count(int p_snap_count);
+ void use_preset(int p_preset_id);
+
+ void set_curve(Ref<Curve> p_curve);
+ Ref<Curve> get_curve();
- void set_curve(Ref<Curve> curve);
+ Size2 get_minimum_size() const override;
enum PresetID {
- PRESET_FLAT0 = 0,
- PRESET_FLAT1,
+ PRESET_CONSTANT = 0,
PRESET_LINEAR,
PRESET_EASE_IN,
PRESET_EASE_OUT,
@@ -59,14 +64,6 @@ public:
PRESET_COUNT
};
- enum ContextAction {
- CONTEXT_ADD_POINT = 0,
- CONTEXT_REMOVE_POINT,
- CONTEXT_LINEAR,
- CONTEXT_LEFT_LINEAR,
- CONTEXT_RIGHT_LINEAR
- };
-
enum TangentIndex {
TANGENT_NONE = -1,
TANGENT_LEFT = 0,
@@ -75,49 +72,103 @@ public:
protected:
void _notification(int p_what);
+ static void _bind_methods();
private:
virtual void gui_input(const Ref<InputEvent> &p_event) override;
- void on_preset_item_selected(int preset_id);
void _curve_changed();
- void on_context_menu_item_selected(int action_id);
-
- void open_context_menu(Vector2 pos);
- int get_point_at(Vector2 pos) const;
- TangentIndex get_tangent_at(Vector2 pos) const;
- void add_point(Vector2 pos);
- void remove_point(int index);
- void toggle_linear(TangentIndex tangent = TANGENT_NONE);
- void set_selected_point(int index);
- void set_hover_point_index(int index);
+
+ int get_point_at(Vector2 p_pos) const;
+ TangentIndex get_tangent_at(Vector2 p_pos) const;
+
+ float get_offset_without_collision(int p_current_index, float p_offset, bool p_prioritize_right = true);
+
+ void add_point(Vector2 p_pos);
+ void remove_point(int p_index);
+ void set_point_position(int p_index, Vector2 p_pos);
+
+ void set_point_tangents(int p_index, float p_left, float p_right);
+ void set_point_left_tangent(int p_index, float p_tangent);
+ void set_point_right_tangent(int p_index, float p_tangent);
+ void toggle_linear(int p_index, TangentIndex p_tangent = TANGENT_NONE);
+
void update_view_transform();
- Vector2 get_tangent_view_pos(int i, TangentIndex tangent) const;
- Vector2 get_view_pos(Vector2 world_pos) const;
- Vector2 get_world_pos(Vector2 view_pos) const;
+ void set_selected_index(int p_index);
+ void set_selected_tangent_index(TangentIndex p_tangent);
+
+ Vector2 get_tangent_view_pos(int p_index, TangentIndex p_tangent) const;
+ Vector2 get_view_pos(Vector2 p_world_pos) const;
+ Vector2 get_world_pos(Vector2 p_view_pos) const;
- void _draw();
+ void _on_mouse_exited();
+
+ void _redraw();
private:
Transform2D _world_to_view;
- Ref<Curve> _curve_ref;
- PopupMenu *_context_menu = nullptr;
+ Ref<Curve> curve;
PopupMenu *_presets_menu = nullptr;
- Array _undo_data;
- bool _has_undo_data;
+ int selected_index = -1;
+ int hovered_index = -1;
+ TangentIndex selected_tangent_index = TANGENT_NONE;
+ TangentIndex hovered_tangent_index = TANGENT_NONE;
+
+ // Make sure to use the scaled values below.
+ const int BASE_POINT_RADIUS = 4;
+ const int BASE_HOVER_RADIUS = 10;
+ const int BASE_TANGENT_RADIUS = 3;
+ const int BASE_TANGENT_HOVER_RADIUS = 8;
+ const int BASE_TANGENT_LENGTH = 36;
+
+ int point_radius = BASE_POINT_RADIUS;
+ int hover_radius = BASE_HOVER_RADIUS;
+ int tangent_radius = BASE_TANGENT_RADIUS;
+ int tangent_hover_radius = BASE_TANGENT_HOVER_RADIUS;
+ int tangent_length = BASE_TANGENT_LENGTH;
+
+ enum GrabMode {
+ GRAB_NONE,
+ GRAB_ADD,
+ GRAB_MOVE
+ };
+ GrabMode grabbing = GRAB_NONE;
+ Vector2 initial_grab_pos;
+ int initial_grab_index;
+ float initial_grab_left_tangent;
+ float initial_grab_right_tangent;
+
+ bool snap_enabled = false;
+ int snap_count = 10;
+};
+
+// CurveEdit + toolbar
+class CurveEditor : public VBoxContainer {
+ GDCLASS(CurveEditor, VBoxContainer);
+
+ // Make sure to use the scaled values below.
+ const int BASE_SPACING = 4;
+ int spacing = BASE_SPACING;
+
+ Button *snap_button = nullptr;
+ EditorSpinSlider *snap_count_edit = nullptr;
+ MenuButton *presets_button = nullptr;
+ CurveEdit *curve_editor_rect = nullptr;
- Vector2 _context_click_pos;
- int _selected_point;
- int _hover_point;
- TangentIndex _selected_tangent;
- bool _dragging;
+ void _set_snap_enabled(bool p_enabled);
+ void _set_snap_count(int p_snap_count);
+ void _on_preset_item_selected(int p_preset_id);
- // Constant
- float _hover_radius;
- float _tangents_length;
- float _gizmo_handle_scale = 1.0;
+protected:
+ void _notification(int p_what);
+
+public:
+ static const int DEFAULT_SNAP;
+ void set_curve(const Ref<Curve> &p_curve);
+
+ CurveEditor();
};
class EditorInspectorPluginCurve : public EditorInspectorPlugin {
diff --git a/scene/resources/curve.cpp b/scene/resources/curve.cpp
index 64e466cf75..5aaa41d518 100644
--- a/scene/resources/curve.cpp
+++ b/scene/resources/curve.cpp
@@ -114,6 +114,13 @@ int Curve::add_point(Vector2 p_position, real_t p_left_tangent, real_t p_right_t
return ret;
}
+// TODO: Needed to make the curve editor function properly until https://github.com/godotengine/godot/issues/76985 is fixed.
+int Curve::add_point_no_update(Vector2 p_position, real_t p_left_tangent, real_t p_right_tangent, TangentMode p_left_mode, TangentMode p_right_mode) {
+ int ret = _add_point(p_position, p_left_tangent, p_right_tangent, p_left_mode, p_right_mode);
+
+ return ret;
+}
+
int Curve::get_index(real_t p_offset) const {
// Lower-bound float binary search
diff --git a/scene/resources/curve.h b/scene/resources/curve.h
index 19af200ba7..005d42610b 100644
--- a/scene/resources/curve.h
+++ b/scene/resources/curve.h
@@ -83,6 +83,11 @@ public:
real_t right_tangent = 0,
TangentMode left_mode = TANGENT_FREE,
TangentMode right_mode = TANGENT_FREE);
+ int add_point_no_update(Vector2 p_position,
+ real_t left_tangent = 0,
+ real_t right_tangent = 0,
+ TangentMode left_mode = TANGENT_FREE,
+ TangentMode right_mode = TANGENT_FREE);
void remove_point(int p_index);
void clear_points();
@@ -100,6 +105,8 @@ public:
real_t get_max_value() const { return _max_value; }
void set_max_value(real_t p_max);
+ real_t get_range() const { return _max_value - _min_value; }
+
real_t sample(real_t p_offset) const;
real_t sample_local_nocheck(int p_index, real_t p_local_offset) const;