Path: blob/master/editor/scene/curve_editor_plugin.cpp
9896 views
/**************************************************************************/1/* curve_editor_plugin.cpp */2/**************************************************************************/3/* This file is part of: */4/* GODOT ENGINE */5/* https://godotengine.org */6/**************************************************************************/7/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */8/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */9/* */10/* Permission is hereby granted, free of charge, to any person obtaining */11/* a copy of this software and associated documentation files (the */12/* "Software"), to deal in the Software without restriction, including */13/* without limitation the rights to use, copy, modify, merge, publish, */14/* distribute, sublicense, and/or sell copies of the Software, and to */15/* permit persons to whom the Software is furnished to do so, subject to */16/* the following conditions: */17/* */18/* The above copyright notice and this permission notice shall be */19/* included in all copies or substantial portions of the Software. */20/* */21/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */22/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */23/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */24/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */25/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */26/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */27/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */28/**************************************************************************/2930#include "curve_editor_plugin.h"3132#include "canvas_item_editor_plugin.h"33#include "core/input/input.h"34#include "core/math/geometry_2d.h"35#include "core/os/keyboard.h"36#include "editor/editor_interface.h"37#include "editor/editor_node.h"38#include "editor/editor_string_names.h"39#include "editor/editor_undo_redo_manager.h"40#include "editor/gui/editor_spin_slider.h"41#include "editor/settings/editor_settings.h"42#include "editor/themes/editor_scale.h"43#include "scene/gui/flow_container.h"44#include "scene/gui/menu_button.h"45#include "scene/gui/popup_menu.h"46#include "scene/gui/separator.h"47#include "scene/resources/image_texture.h"4849CurveEdit::CurveEdit() {50set_focus_mode(FOCUS_ALL);51set_clip_contents(true);52}5354void CurveEdit::_bind_methods() {55ClassDB::bind_method(D_METHOD("set_selected_index", "index"), &CurveEdit::set_selected_index);56}5758void CurveEdit::set_curve(Ref<Curve> p_curve) {59if (p_curve == curve) {60return;61}6263if (curve.is_valid()) {64curve->disconnect_changed(callable_mp(this, &CurveEdit::_curve_changed));65curve->disconnect(Curve::SIGNAL_RANGE_CHANGED, callable_mp(this, &CurveEdit::_curve_changed));66curve->disconnect(Curve::SIGNAL_DOMAIN_CHANGED, callable_mp(this, &CurveEdit::_curve_changed));67}6869curve = p_curve;7071if (curve.is_valid()) {72curve->connect_changed(callable_mp(this, &CurveEdit::_curve_changed));73curve->connect(Curve::SIGNAL_RANGE_CHANGED, callable_mp(this, &CurveEdit::_curve_changed));74curve->connect(Curve::SIGNAL_DOMAIN_CHANGED, callable_mp(this, &CurveEdit::_curve_changed));75}7677// Note: if you edit a curve, then set another, and try to undo,78// it will normally apply on the previous curve, but you won't see it.79}8081Ref<Curve> CurveEdit::get_curve() {82return curve;83}8485void CurveEdit::set_snap_enabled(bool p_enabled) {86snap_enabled = p_enabled;87queue_redraw();88if (curve.is_valid()) {89if (snap_enabled) {90curve->set_meta(SNAME("_snap_enabled"), true);91} else {92curve->remove_meta(SNAME("_snap_enabled"));93}94}95}9697void CurveEdit::set_snap_count(int p_snap_count) {98snap_count = p_snap_count;99queue_redraw();100if (curve.is_valid()) {101if (snap_count != CurveEditor::DEFAULT_SNAP) {102curve->set_meta(SNAME("_snap_count"), snap_count);103} else {104curve->remove_meta(SNAME("_snap_count"));105}106}107}108109Size2 CurveEdit::get_minimum_size() const {110return Vector2(64, MAX(135, get_size().x * ASPECT_RATIO)) * EDSCALE;111}112113void CurveEdit::_notification(int p_what) {114switch (p_what) {115case NOTIFICATION_MOUSE_EXIT: {116if (hovered_index != -1 || hovered_tangent_index != TANGENT_NONE) {117hovered_index = -1;118hovered_tangent_index = TANGENT_NONE;119queue_redraw();120}121} break;122case NOTIFICATION_THEME_CHANGED: {123float gizmo_scale = EDITOR_GET("interface/touchscreen/scale_gizmo_handles");124point_radius = Math::round(BASE_POINT_RADIUS * get_theme_default_base_scale() * gizmo_scale);125hover_radius = Math::round(BASE_HOVER_RADIUS * get_theme_default_base_scale() * gizmo_scale);126tangent_radius = Math::round(BASE_TANGENT_RADIUS * get_theme_default_base_scale() * gizmo_scale);127tangent_hover_radius = Math::round(BASE_TANGENT_HOVER_RADIUS * get_theme_default_base_scale() * gizmo_scale);128tangent_length = Math::round(BASE_TANGENT_LENGTH * get_theme_default_base_scale());129} break;130case NOTIFICATION_ACCESSIBILITY_UPDATE: {131RID ae = get_accessibility_element();132ERR_FAIL_COND(ae.is_null());133134//TODO135DisplayServer::get_singleton()->accessibility_update_set_role(ae, DisplayServer::AccessibilityRole::ROLE_STATIC_TEXT);136DisplayServer::get_singleton()->accessibility_update_set_value(ae, TTR(vformat("The %s is not accessible at this time.", "Curve editor")));137} break;138case NOTIFICATION_DRAW: {139_redraw();140} break;141case NOTIFICATION_VISIBILITY_CHANGED: {142if (!is_visible()) {143grabbing = GRAB_NONE;144}145} break;146}147}148149void CurveEdit::gui_input(const Ref<InputEvent> &p_event) {150ERR_FAIL_COND(p_event.is_null());151if (curve.is_null()) {152return;153}154155Ref<InputEventKey> k = p_event;156if (k.is_valid()) {157// Deleting points or making tangents linear.158if (k->is_pressed() && k->get_keycode() == Key::KEY_DELETE) {159if (selected_tangent_index != TANGENT_NONE) {160toggle_linear(selected_index, selected_tangent_index);161} else if (selected_index != -1) {162if (grabbing == GRAB_ADD) {163curve->remove_point(selected_index); // Point is temporary, so remove directly from curve.164set_selected_index(-1);165} else {166remove_point(selected_index);167}168grabbing = GRAB_NONE;169hovered_index = -1;170hovered_tangent_index = TANGENT_NONE;171}172accept_event();173}174175if (k->get_keycode() == Key::SHIFT || k->get_keycode() == Key::ALT) {176queue_redraw(); // Redraw to show the axes or constraints.177}178}179180Ref<InputEventMouseButton> mb = p_event;181if (mb.is_valid() && mb->is_pressed()) {182Vector2 mpos = mb->get_position();183184if (mb->get_button_index() == MouseButton::RIGHT || mb->get_button_index() == MouseButton::MIDDLE) {185if (mb->get_button_index() == MouseButton::RIGHT && grabbing == GRAB_MOVE) {186// Move a point to its old position.187curve->set_point_value(selected_index, initial_grab_pos.y);188curve->set_point_offset(selected_index, initial_grab_pos.x);189set_selected_index(initial_grab_index);190hovered_index = get_point_at(mpos);191grabbing = GRAB_NONE;192} else {193// Remove a point or make a tangent linear.194selected_tangent_index = get_tangent_at(mpos);195if (selected_tangent_index != TANGENT_NONE) {196toggle_linear(selected_index, selected_tangent_index);197} else {198int point_to_remove = get_point_at(mpos);199if (point_to_remove == -1) {200set_selected_index(-1); // Nothing on the place of the click, just deselect the point.201} else {202if (grabbing == GRAB_ADD) {203curve->remove_point(point_to_remove); // Point is temporary, so remove directly from curve.204set_selected_index(-1);205} else {206remove_point(point_to_remove);207}208hovered_index = get_point_at(mpos);209grabbing = GRAB_NONE;210}211}212}213}214215// Selecting or creating points.216if (mb->get_button_index() == MouseButton::LEFT) {217if (grabbing == GRAB_NONE) {218selected_tangent_index = get_tangent_at(mpos);219if (selected_tangent_index == TANGENT_NONE) {220set_selected_index(get_point_at(mpos));221}222queue_redraw();223}224225if (selected_index != -1) {226// If an existing point/tangent was grabbed, remember a few things about it.227grabbing = GRAB_MOVE;228initial_grab_pos = curve->get_point_position(selected_index);229initial_grab_index = selected_index;230if (selected_index > 0) {231initial_grab_left_tangent = curve->get_point_left_tangent(selected_index);232}233if (selected_index < curve->get_point_count() - 1) {234initial_grab_right_tangent = curve->get_point_right_tangent(selected_index);235}236} else if (grabbing == GRAB_NONE) {237// Adding a new point. Insert a temporary point for the user to adjust, so it's not in the undo/redo.238Vector2 new_pos = get_world_pos(mpos).clamp(Vector2(curve->get_min_domain(), curve->get_min_value()), Vector2(curve->get_max_domain(), curve->get_max_value()));239if (snap_enabled || mb->is_command_or_control_pressed()) {240new_pos.x = Math::snapped(new_pos.x - curve->get_min_domain(), curve->get_domain_range() / snap_count) + curve->get_min_domain();241new_pos.y = Math::snapped(new_pos.y - curve->get_min_value(), curve->get_value_range() / snap_count) + curve->get_min_value();242}243244new_pos.x = get_offset_without_collision(selected_index, new_pos.x, mpos.x >= get_view_pos(new_pos).x);245246// Add a temporary point for the user to adjust before adding it permanently.247int new_idx = curve->add_point_no_update(new_pos);248set_selected_index(new_idx);249grabbing = GRAB_ADD;250initial_grab_pos = new_pos;251}252}253}254255if (mb.is_valid() && mb->get_button_index() == MouseButton::LEFT && !mb->is_pressed()) {256if (selected_tangent_index != TANGENT_NONE) {257// Finish moving a tangent control.258if (selected_index == 0) {259set_point_right_tangent(selected_index, curve->get_point_right_tangent(selected_index));260} else if (selected_index == curve->get_point_count() - 1) {261set_point_left_tangent(selected_index, curve->get_point_left_tangent(selected_index));262} else {263set_point_tangents(selected_index, curve->get_point_left_tangent(selected_index), curve->get_point_right_tangent(selected_index));264}265grabbing = GRAB_NONE;266} else if (grabbing == GRAB_MOVE) {267// Finish moving a point.268set_point_position(selected_index, curve->get_point_position(selected_index));269grabbing = GRAB_NONE;270} else if (grabbing == GRAB_ADD) {271// Finish inserting a new point. Remove the temporary point and insert a permanent one in its place.272Vector2 new_pos = curve->get_point_position(selected_index);273curve->remove_point(selected_index);274add_point(new_pos);275grabbing = GRAB_NONE;276}277queue_redraw();278}279280Ref<InputEventMouseMotion> mm = p_event;281if (mm.is_valid()) {282Vector2 mpos = mm->get_position();283284if (grabbing != GRAB_NONE && curve.is_valid()) {285if (selected_index != -1) {286if (selected_tangent_index == TANGENT_NONE) {287// Drag point.288Vector2 new_pos = get_world_pos(mpos).clamp(Vector2(curve->get_min_domain(), curve->get_min_value()), Vector2(curve->get_max_domain(), curve->get_max_value()));289290if (snap_enabled || mm->is_command_or_control_pressed()) {291new_pos.x = Math::snapped(new_pos.x - curve->get_min_domain(), curve->get_domain_range() / snap_count) + curve->get_min_domain();292new_pos.y = Math::snapped(new_pos.y - curve->get_min_value(), curve->get_value_range() / snap_count) + curve->get_min_value();293}294295// Allow to snap to axes with Shift.296if (mm->is_shift_pressed()) {297Vector2 initial_mpos = get_view_pos(initial_grab_pos);298if (Math::abs(mpos.x - initial_mpos.x) > Math::abs(mpos.y - initial_mpos.y)) {299new_pos.y = initial_grab_pos.y;300} else {301new_pos.x = initial_grab_pos.x;302}303}304305// Allow to constraint the point between the adjacent two with Alt.306if (mm->is_alt_pressed()) {307float prev_point_offset = (selected_index > 0) ? (curve->get_point_position(selected_index - 1).x + 0.00001) : curve->get_min_domain();308float next_point_offset = (selected_index < curve->get_point_count() - 1) ? (curve->get_point_position(selected_index + 1).x - 0.00001) : curve->get_max_domain();309new_pos.x = CLAMP(new_pos.x, prev_point_offset, next_point_offset);310}311312new_pos.x = get_offset_without_collision(selected_index, new_pos.x, mpos.x >= get_view_pos(new_pos).x);313314// The index may change if the point is dragged across another one.315int i = curve->set_point_offset(selected_index, new_pos.x);316hovered_index = i;317set_selected_index(i);318319new_pos.y = CLAMP(new_pos.y, curve->get_min_value(), curve->get_max_value());320curve->set_point_value(selected_index, new_pos.y);321322} else {323// Drag tangent.324325const Vector2 new_pos = curve->get_point_position(selected_index);326const Vector2 control_pos = get_world_pos(mpos);327328Vector2 dir = (control_pos - new_pos).normalized();329real_t tangent = dir.y / (dir.x > 0 ? MAX(dir.x, 0.00001) : MIN(dir.x, -0.00001));330331// Must keep track of the hovered index as the cursor might move outside of the editor while dragging.332hovered_tangent_index = selected_tangent_index;333334// Adjust the tangents.335if (selected_tangent_index == TANGENT_LEFT) {336curve->set_point_left_tangent(selected_index, tangent);337338// Align the other tangent if it isn't linear and Shift is not pressed.339// If Shift is pressed at any point, restore the initial angle of the other tangent.340if (selected_index != (curve->get_point_count() - 1) && curve->get_point_right_mode(selected_index) != Curve::TANGENT_LINEAR) {341curve->set_point_right_tangent(selected_index, mm->is_shift_pressed() ? initial_grab_right_tangent : tangent);342}343344} else {345curve->set_point_right_tangent(selected_index, tangent);346347if (selected_index != 0 && curve->get_point_left_mode(selected_index) != Curve::TANGENT_LINEAR) {348curve->set_point_left_tangent(selected_index, mm->is_shift_pressed() ? initial_grab_left_tangent : tangent);349}350}351}352}353} else {354// Grab mode is GRAB_NONE, so do hovering logic.355hovered_index = get_point_at(mpos);356hovered_tangent_index = get_tangent_at(mpos);357queue_redraw();358}359}360}361362void CurveEdit::use_preset(int p_preset_id) {363ERR_FAIL_COND(p_preset_id < 0 || p_preset_id >= PRESET_COUNT);364ERR_FAIL_COND(curve.is_null());365366Array previous_data = curve->get_data();367curve->clear_points();368369const float min_y = curve->get_min_value();370const float max_y = curve->get_max_value();371const float min_x = curve->get_min_domain();372const float max_x = curve->get_max_domain();373374switch (p_preset_id) {375case PRESET_CONSTANT:376curve->add_point(Vector2(min_x, (min_y + max_y) / 2.0));377curve->add_point(Vector2(max_x, (min_y + max_y) / 2.0));378curve->set_point_right_mode(0, Curve::TANGENT_LINEAR);379curve->set_point_left_mode(1, Curve::TANGENT_LINEAR);380break;381382case PRESET_LINEAR:383curve->add_point(Vector2(min_x, min_y));384curve->add_point(Vector2(max_x, max_y));385curve->set_point_right_mode(0, Curve::TANGENT_LINEAR);386curve->set_point_left_mode(1, Curve::TANGENT_LINEAR);387break;388389case PRESET_EASE_IN:390curve->add_point(Vector2(min_x, min_y));391curve->add_point(Vector2(max_x, max_y), curve->get_value_range() / curve->get_domain_range() * 1.4, 0);392break;393394case PRESET_EASE_OUT:395curve->add_point(Vector2(min_x, min_y), 0, curve->get_value_range() / curve->get_domain_range() * 1.4);396curve->add_point(Vector2(max_x, max_y));397break;398399case PRESET_SMOOTHSTEP:400curve->add_point(Vector2(min_x, min_y));401curve->add_point(Vector2(max_x, max_y));402break;403404default:405break;406}407408EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();409undo_redo->create_action(TTR("Load Curve Preset"));410undo_redo->add_do_method(*curve, "_set_data", curve->get_data());411undo_redo->add_do_method(this, "set_selected_index", -1);412undo_redo->add_undo_method(*curve, "_set_data", previous_data);413undo_redo->add_undo_method(this, "set_selected_index", selected_index);414undo_redo->commit_action();415}416417void CurveEdit::_curve_changed() {418queue_redraw();419// Point count can change in case of undo.420if (selected_index >= curve->get_point_count()) {421set_selected_index(-1);422}423}424425int CurveEdit::get_point_at(const Vector2 &p_pos) const {426if (curve.is_null()) {427return -1;428}429430// Use a square-shaped hover region. If hovering multiple points, pick the closer one.431const Rect2 hover_rect = Rect2(p_pos, Vector2(0, 0)).grow(hover_radius);432int closest_idx = -1;433float closest_dist_squared = hover_radius * hover_radius * 2;434435for (int i = 0; i < curve->get_point_count(); ++i) {436Vector2 p = get_view_pos(curve->get_point_position(i));437if (hover_rect.has_point(p) && p.distance_squared_to(p_pos) < closest_dist_squared) {438closest_dist_squared = p.distance_squared_to(p_pos);439closest_idx = i;440}441}442443return closest_idx;444}445446CurveEdit::TangentIndex CurveEdit::get_tangent_at(const Vector2 &p_pos) const {447if (curve.is_null() || selected_index < 0) {448return TANGENT_NONE;449}450451const Rect2 hover_rect = Rect2(p_pos, Vector2(0, 0)).grow(tangent_hover_radius);452453if (selected_index != 0) {454Vector2 control_pos = get_tangent_view_pos(selected_index, TANGENT_LEFT);455if (hover_rect.has_point(control_pos)) {456return TANGENT_LEFT;457}458}459460if (selected_index != curve->get_point_count() - 1) {461Vector2 control_pos = get_tangent_view_pos(selected_index, TANGENT_RIGHT);462if (hover_rect.has_point(control_pos)) {463return TANGENT_RIGHT;464}465}466467return TANGENT_NONE;468}469470// FIXME: This function should be bounded better.471float CurveEdit::get_offset_without_collision(int p_current_index, float p_offset, bool p_prioritize_right) {472float safe_offset = p_offset;473bool prioritizing_right = p_prioritize_right;474475for (int i = 0; i < curve->get_point_count(); i++) {476if (i == p_current_index) {477continue;478}479480if (curve->get_point_position(i).x > safe_offset) {481break;482}483484if (curve->get_point_position(i).x == safe_offset) {485if (prioritizing_right) {486safe_offset += 0.00001;487if (safe_offset > 1.0) {488safe_offset = 1.0;489prioritizing_right = false;490}491} else {492safe_offset -= 0.00001;493if (safe_offset < 0.0) {494safe_offset = 0.0;495prioritizing_right = true;496}497}498i = -1;499}500}501502return safe_offset;503}504505void CurveEdit::add_point(const Vector2 &p_pos) {506ERR_FAIL_COND(curve.is_null());507508// Add a point to get its index, then remove it immediately. Trick to feed the UndoRedo.509int new_idx = curve->add_point(p_pos);510curve->remove_point(new_idx);511512EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();513undo_redo->create_action(TTR("Add Curve Point"));514undo_redo->add_do_method(*curve, "add_point", p_pos);515undo_redo->add_do_method(this, "set_selected_index", new_idx);516undo_redo->add_undo_method(*curve, "remove_point", new_idx);517undo_redo->add_undo_method(this, "set_selected_index", -1);518undo_redo->commit_action();519}520521void CurveEdit::remove_point(int p_index) {522ERR_FAIL_COND(curve.is_null());523ERR_FAIL_INDEX_MSG(p_index, curve->get_point_count(), "Curve point is out of bounds.");524525Curve::Point p = curve->get_point(p_index);526Vector2 old_pos = (grabbing == GRAB_MOVE) ? initial_grab_pos : p.position;527528int new_selected_index = selected_index;529// Reselect the old selected point if it's not the deleted one.530if (new_selected_index > p_index) {531new_selected_index -= 1;532} else if (new_selected_index == p_index) {533new_selected_index = -1;534}535536EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();537undo_redo->create_action(TTR("Remove Curve Point"));538undo_redo->add_do_method(*curve, "remove_point", p_index);539undo_redo->add_do_method(this, "set_selected_index", new_selected_index);540undo_redo->add_undo_method(*curve, "add_point", old_pos, p.left_tangent, p.right_tangent, p.left_mode, p.right_mode);541undo_redo->add_undo_method(this, "set_selected_index", selected_index);542undo_redo->commit_action();543}544545void CurveEdit::set_point_position(int p_index, const Vector2 &p_pos) {546ERR_FAIL_COND(curve.is_null());547ERR_FAIL_INDEX_MSG(p_index, curve->get_point_count(), "Curve point is out of bounds.");548549if (initial_grab_pos == p_pos) {550return;551}552553// Pretend the point started from its old place.554curve->set_point_value(p_index, initial_grab_pos.y);555curve->set_point_offset(p_index, initial_grab_pos.x);556// Note: Changing the offset may modify the order.557EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();558undo_redo->create_action(TTR("Modify Curve Point"));559undo_redo->add_do_method(*curve, "set_point_value", initial_grab_index, p_pos.y);560undo_redo->add_do_method(*curve, "set_point_offset", initial_grab_index, p_pos.x);561undo_redo->add_do_method(this, "set_selected_index", p_index);562undo_redo->add_undo_method(*curve, "set_point_value", p_index, initial_grab_pos.y);563undo_redo->add_undo_method(*curve, "set_point_offset", p_index, initial_grab_pos.x);564undo_redo->add_undo_method(this, "set_selected_index", initial_grab_index);565undo_redo->commit_action();566}567568void CurveEdit::set_point_tangents(int p_index, float p_left, float p_right) {569ERR_FAIL_COND(curve.is_null());570ERR_FAIL_INDEX_MSG(p_index, curve->get_point_count(), "Curve point is out of bounds.");571572if (initial_grab_left_tangent == p_left) {573set_point_right_tangent(p_index, p_right);574return;575} else if (initial_grab_right_tangent == p_right) {576set_point_left_tangent(p_index, p_left);577return;578}579580curve->set_point_left_tangent(p_index, initial_grab_left_tangent);581curve->set_point_right_tangent(p_index, initial_grab_right_tangent);582EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();583undo_redo->create_action(TTR("Modify Curve Point's Tangents"));584undo_redo->add_do_method(*curve, "set_point_left_tangent", p_index, p_left);585undo_redo->add_do_method(*curve, "set_point_right_tangent", p_index, p_right);586undo_redo->add_do_method(this, "set_selected_index", p_index);587undo_redo->add_undo_method(*curve, "set_point_left_tangent", p_index, initial_grab_left_tangent);588undo_redo->add_undo_method(*curve, "set_point_right_tangent", p_index, initial_grab_right_tangent);589undo_redo->add_undo_method(this, "set_selected_index", p_index);590undo_redo->commit_action();591}592593void CurveEdit::set_point_left_tangent(int p_index, float p_tangent) {594ERR_FAIL_COND(curve.is_null());595ERR_FAIL_INDEX_MSG(p_index, curve->get_point_count(), "Curve point is out of bounds.");596597if (initial_grab_left_tangent == p_tangent) {598return;599}600601curve->set_point_left_tangent(p_index, initial_grab_left_tangent);602EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();603undo_redo->create_action(TTR("Modify Curve Point's Left Tangent"));604undo_redo->add_do_method(*curve, "set_point_left_tangent", p_index, p_tangent);605undo_redo->add_do_method(this, "set_selected_index", p_index);606undo_redo->add_undo_method(*curve, "set_point_left_tangent", p_index, initial_grab_left_tangent);607undo_redo->add_undo_method(this, "set_selected_index", p_index);608undo_redo->commit_action();609}610611void CurveEdit::set_point_right_tangent(int p_index, float p_tangent) {612ERR_FAIL_COND(curve.is_null());613ERR_FAIL_INDEX_MSG(p_index, curve->get_point_count(), "Curve point is out of bounds.");614615if (initial_grab_right_tangent == p_tangent) {616return;617}618619curve->set_point_right_tangent(p_index, initial_grab_right_tangent);620EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();621undo_redo->create_action(TTR("Modify Curve Point's Right Tangent"));622undo_redo->add_do_method(*curve, "set_point_right_tangent", p_index, p_tangent);623undo_redo->add_do_method(this, "set_selected_index", p_index);624undo_redo->add_undo_method(*curve, "set_point_right_tangent", p_index, initial_grab_right_tangent);625undo_redo->add_undo_method(this, "set_selected_index", p_index);626undo_redo->commit_action();627}628629void CurveEdit::toggle_linear(int p_index, TangentIndex p_tangent) {630ERR_FAIL_COND(curve.is_null());631ERR_FAIL_INDEX_MSG(p_index, curve->get_point_count(), "Curve point is out of bounds.");632633if (p_tangent == TANGENT_NONE) {634return;635}636637EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();638undo_redo->create_action(TTR("Toggle Linear Curve Point's Tangent"));639640Curve::TangentMode prev_mode = (p_tangent == TANGENT_LEFT) ? curve->get_point_left_mode(p_index) : curve->get_point_right_mode(p_index);641Curve::TangentMode mode = (prev_mode == Curve::TANGENT_LINEAR) ? Curve::TANGENT_FREE : Curve::TANGENT_LINEAR;642float prev_angle = (p_tangent == TANGENT_LEFT) ? curve->get_point_left_tangent(p_index) : curve->get_point_right_tangent(p_index);643644// Add different methods in the UndoRedo based on the tangent passed.645if (p_tangent == TANGENT_LEFT) {646undo_redo->add_do_method(*curve, "set_point_left_mode", p_index, mode);647undo_redo->add_undo_method(*curve, "set_point_left_mode", p_index, prev_mode);648undo_redo->add_undo_method(*curve, "set_point_left_tangent", p_index, prev_angle);649} else {650undo_redo->add_do_method(*curve, "set_point_right_mode", p_index, mode);651undo_redo->add_undo_method(*curve, "set_point_right_mode", p_index, prev_mode);652undo_redo->add_undo_method(*curve, "set_point_right_tangent", p_index, prev_angle);653}654655undo_redo->commit_action();656}657658void CurveEdit::set_selected_index(int p_index) {659if (p_index != selected_index) {660selected_index = p_index;661queue_redraw();662}663}664665void CurveEdit::update_view_transform() {666Ref<Font> font = get_theme_font(SceneStringName(font), SNAME("Label"));667int font_size = get_theme_font_size(SceneStringName(font_size), SNAME("Label"));668669const real_t margin = font->get_height(font_size) + 2 * EDSCALE;670671float min_x = curve.is_valid() ? curve->get_min_domain() : 0.0;672float max_x = curve.is_valid() ? curve->get_max_domain() : 1.0;673float min_y = curve.is_valid() ? curve->get_min_value() : 0.0;674float max_y = curve.is_valid() ? curve->get_max_value() : 1.0;675676const Rect2 world_rect = Rect2(min_x, min_y, max_x - min_x, max_y - min_y);677const Size2 view_margin(margin, margin);678const Size2 view_size = get_size() - view_margin * 2;679const Vector2 scale = view_size / world_rect.size;680681Transform2D world_trans;682world_trans.translate_local(-world_rect.position - Vector2(0, world_rect.size.y));683world_trans.scale(Vector2(scale.x, -scale.y));684685Transform2D view_trans;686view_trans.translate_local(view_margin);687688_world_to_view = view_trans * world_trans;689}690691Vector2 CurveEdit::get_tangent_view_pos(int p_index, TangentIndex p_tangent) const {692Vector2 dir;693if (p_tangent == TANGENT_LEFT) {694dir = -Vector2(1, curve->get_point_left_tangent(p_index));695} else {696dir = Vector2(1, curve->get_point_right_tangent(p_index));697}698699Vector2 point_pos = curve->get_point_position(p_index);700Vector2 point_view_pos = get_view_pos(point_pos);701Vector2 control_view_pos = get_view_pos(point_pos + dir);702703Vector2 distance_from_point = tangent_length * (control_view_pos - point_view_pos).normalized();704Vector2 tangent_view_pos = point_view_pos + distance_from_point;705706// Since the tangent is long, it might slip outside of the area of the editor for points close to the domain/range boundaries.707// The code below shrinks the tangent control by up to 50% so it always stays inside the editor for points within the bounds.708float fraction_inside = 1.0;709if (distance_from_point.x != 0.0) {710fraction_inside = MIN(fraction_inside, ((distance_from_point.x > 0 ? get_rect().size.x : 0) - point_view_pos.x) / distance_from_point.x);711}712if (distance_from_point.y != 0.0) {713fraction_inside = MIN(fraction_inside, ((distance_from_point.y > 0 ? get_rect().size.y : 0) - point_view_pos.y) / distance_from_point.y);714}715716if (fraction_inside < 1.0 && fraction_inside > 0.5) {717tangent_view_pos = point_view_pos + distance_from_point * fraction_inside;718}719720return tangent_view_pos;721}722723Vector2 CurveEdit::get_view_pos(const Vector2 &p_world_pos) const {724return _world_to_view.xform(p_world_pos);725}726727Vector2 CurveEdit::get_world_pos(const Vector2 &p_view_pos) const {728return _world_to_view.affine_inverse().xform(p_view_pos);729}730731// Uses non-baked points, but takes advantage of ordered iteration to be faster.732void CurveEdit::plot_curve_accurate(float p_step, const Color &p_line_color, const Color &p_edge_line_color) {733const real_t min_x = curve->get_min_domain();734const real_t max_x = curve->get_max_domain();735if (curve->get_point_count() <= 1) { // Draw single line through entire plot.736real_t y = curve->sample(0);737draw_line(get_view_pos(Vector2(min_x, y)) + Vector2(0.5, 0), get_view_pos(Vector2(max_x, y)) - Vector2(1.5, 0), p_line_color, LINE_WIDTH, true);738return;739}740741Vector2 first_point = curve->get_point_position(0);742Vector2 last_point = curve->get_point_position(curve->get_point_count() - 1);743744// Transform pixels-per-step into curve domain. Only works for non-rotated transforms.745const float world_step_size = p_step / _world_to_view.get_scale().x;746747// Edge lines.748draw_line(get_view_pos(Vector2(min_x, first_point.y)) + Vector2(0.5, 0), get_view_pos(first_point), p_edge_line_color, LINE_WIDTH, true);749draw_line(get_view_pos(last_point), get_view_pos(Vector2(max_x, last_point.y)) - Vector2(1.5, 0), p_edge_line_color, LINE_WIDTH, true);750751// Draw section by section, so that we get maximum precision near points.752// It's an accurate representation, but slower than using the baked one.753for (int i = 1; i < curve->get_point_count(); ++i) {754Vector2 a = curve->get_point_position(i - 1);755Vector2 b = curve->get_point_position(i);756757Vector2 pos = a;758Vector2 prev_pos = a;759760float samples = (b.x - a.x) / world_step_size;761762for (int j = 1; j < samples; j++) {763float x = j * world_step_size;764pos.x = a.x + x;765pos.y = curve->sample_local_nocheck(i - 1, x);766draw_line(get_view_pos(prev_pos), get_view_pos(pos), p_line_color, LINE_WIDTH, true);767prev_pos = pos;768}769770draw_line(get_view_pos(prev_pos), get_view_pos(b), p_line_color, LINE_WIDTH, true);771}772}773774void CurveEdit::_redraw() {775if (curve.is_null()) {776return;777}778779update_view_transform();780781// Draw background.782783Vector2 view_size = get_rect().size;784draw_style_box(get_theme_stylebox(SceneStringName(panel), SNAME("Tree")), Rect2(Point2(), view_size));785786// Draw primary grid.787draw_set_transform_matrix(_world_to_view);788789Vector2 min_edge = get_world_pos(Vector2(0, view_size.y));790Vector2 max_edge = get_world_pos(Vector2(view_size.x, 0));791792const Color grid_color_primary = get_theme_color(SNAME("mono_color"), EditorStringName(Editor)) * Color(1, 1, 1, 0.25);793const Color grid_color = get_theme_color(SNAME("mono_color"), EditorStringName(Editor)) * Color(1, 1, 1, 0.1);794795const Vector2i grid_steps = Vector2i(4, 2);796const Vector2 step_size = Vector2(curve->get_domain_range(), curve->get_value_range()) / grid_steps;797798draw_line(Vector2(min_edge.x, curve->get_min_value()), Vector2(max_edge.x, curve->get_min_value()), grid_color_primary);799draw_line(Vector2(max_edge.x, curve->get_max_value()), Vector2(min_edge.x, curve->get_max_value()), grid_color_primary);800draw_line(Vector2(curve->get_min_domain(), min_edge.y), Vector2(curve->get_min_domain(), max_edge.y), grid_color_primary);801draw_line(Vector2(curve->get_max_domain(), max_edge.y), Vector2(curve->get_max_domain(), min_edge.y), grid_color_primary);802803for (int i = 1; i < grid_steps.x; i++) {804real_t x = curve->get_min_domain() + i * step_size.x;805draw_line(Vector2(x, min_edge.y), Vector2(x, max_edge.y), grid_color);806}807808for (int i = 1; i < grid_steps.y; i++) {809real_t y = curve->get_min_value() + i * step_size.y;810draw_line(Vector2(min_edge.x, y), Vector2(max_edge.x, y), grid_color);811}812813// Draw number markings.814draw_set_transform_matrix(Transform2D());815816Ref<Font> font = get_theme_font(SceneStringName(font), SNAME("Label"));817int font_size = get_theme_font_size(SceneStringName(font_size), SNAME("Label"));818float font_height = font->get_height(font_size);819Color text_color = get_theme_color(SceneStringName(font_color), EditorStringName(Editor));820821int pad = Math::round(2 * EDSCALE);822823for (int i = 0; i <= grid_steps.x; ++i) {824real_t x = curve->get_min_domain() + i * step_size.x;825draw_string(font, get_view_pos(Vector2(x, curve->get_min_value())) + Vector2(pad, font_height - pad), String::num(x, 2), HORIZONTAL_ALIGNMENT_CENTER, -1, font_size, text_color);826}827828for (int i = 0; i <= grid_steps.y; ++i) {829real_t y = curve->get_min_value() + i * step_size.y;830draw_string(font, get_view_pos(Vector2(curve->get_min_domain(), y)) + Vector2(pad, -pad), String::num(y, 2), HORIZONTAL_ALIGNMENT_LEFT, -1, font_size, text_color);831}832833// Draw curve in view coordinates. Curve world-to-view point conversion happens in plot_curve_accurate().834835const Color line_color = get_theme_color(SceneStringName(font_color), EditorStringName(Editor));836const Color edge_line_color = get_theme_color(SceneStringName(font_color), EditorStringName(Editor)) * Color(1, 1, 1, 0.75);837838plot_curve_accurate(STEP_SIZE, line_color, edge_line_color);839840// Draw points, except for the selected one.841842bool shift_pressed = Input::get_singleton()->is_key_pressed(Key::SHIFT);843844const Color point_color = get_theme_color(SceneStringName(font_color), EditorStringName(Editor));845846for (int i = 0; i < curve->get_point_count(); ++i) {847Vector2 pos = get_view_pos(curve->get_point_position(i));848if (selected_index != i) {849draw_rect(Rect2(pos, Vector2(0, 0)).grow(point_radius), point_color);850}851if (hovered_index == i && hovered_tangent_index == TANGENT_NONE) {852draw_rect(Rect2(pos, Vector2(0, 0)).grow(hover_radius - Math::round(3 * EDSCALE)), line_color, false, Math::round(1 * EDSCALE));853}854}855856// Draw selected point and its tangents.857858if (selected_index >= 0) {859const Vector2 point_pos = curve->get_point_position(selected_index);860const Color selected_point_color = get_theme_color(SNAME("accent_color"), EditorStringName(Editor));861862// Draw tangents if not dragging a point, or if holding a point without having moved it yet.863if (grabbing == GRAB_NONE || initial_grab_pos == point_pos || selected_tangent_index != TANGENT_NONE) {864const Color selected_tangent_color = get_theme_color(SNAME("accent_color"), EditorStringName(Editor)).darkened(0.25);865const Color tangent_color = get_theme_color(SceneStringName(font_color), EditorStringName(Editor)).darkened(0.25);866867if (selected_index != 0) {868Vector2 control_pos = get_tangent_view_pos(selected_index, TANGENT_LEFT);869Color left_tangent_color = (selected_tangent_index == TANGENT_LEFT) ? selected_tangent_color : tangent_color;870871draw_line(get_view_pos(point_pos), control_pos, left_tangent_color, 0.5 * EDSCALE, true);872// Square for linear mode, circle otherwise.873if (curve->get_point_left_mode(selected_index) == Curve::TANGENT_FREE) {874draw_circle(control_pos, tangent_radius, left_tangent_color);875} else {876draw_rect(Rect2(control_pos, Vector2(0, 0)).grow(tangent_radius), left_tangent_color);877}878// Hover indicator.879if (hovered_tangent_index == TANGENT_LEFT || (hovered_tangent_index == TANGENT_RIGHT && !shift_pressed && curve->get_point_left_mode(selected_index) != Curve::TANGENT_LINEAR)) {880draw_rect(Rect2(control_pos, Vector2(0, 0)).grow(tangent_hover_radius - Math::round(3 * EDSCALE)), tangent_color, false, Math::round(1 * EDSCALE));881}882}883884if (selected_index != curve->get_point_count() - 1) {885Vector2 control_pos = get_tangent_view_pos(selected_index, TANGENT_RIGHT);886Color right_tangent_color = (selected_tangent_index == TANGENT_RIGHT) ? selected_tangent_color : tangent_color;887888draw_line(get_view_pos(point_pos), control_pos, right_tangent_color, 0.5 * EDSCALE, true);889// Square for linear mode, circle otherwise.890if (curve->get_point_right_mode(selected_index) == Curve::TANGENT_FREE) {891draw_circle(control_pos, tangent_radius, right_tangent_color);892} else {893draw_rect(Rect2(control_pos, Vector2(0, 0)).grow(tangent_radius), right_tangent_color);894}895// Hover indicator.896if (hovered_tangent_index == TANGENT_RIGHT || (hovered_tangent_index == TANGENT_LEFT && !shift_pressed && curve->get_point_right_mode(selected_index) != Curve::TANGENT_LINEAR)) {897draw_rect(Rect2(control_pos, Vector2(0, 0)).grow(tangent_hover_radius - Math::round(3 * EDSCALE)), tangent_color, false, Math::round(1 * EDSCALE));898}899}900}901902draw_rect(Rect2(get_view_pos(point_pos), Vector2(0, 0)).grow(point_radius), selected_point_color);903}904905// Draw help text.906907if (selected_index > 0 && selected_index < curve->get_point_count() - 1 && selected_tangent_index == TANGENT_NONE && hovered_tangent_index != TANGENT_NONE && !shift_pressed) {908float width = view_size.x - 50 * EDSCALE;909text_color.a *= 0.4;910911draw_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);912913} else if (selected_index != -1 && selected_tangent_index == TANGENT_NONE) {914const Vector2 point_pos = curve->get_point_position(selected_index);915float width = view_size.x - 50 * EDSCALE;916text_color.a *= 0.8;917918draw_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);919920} else if (selected_index != -1 && selected_tangent_index != TANGENT_NONE) {921float width = view_size.x - 50 * EDSCALE;922text_color.a *= 0.8;923real_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)));924925draw_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);926}927928// Draw temporary constraints and snapping axes.929draw_set_transform_matrix(_world_to_view);930931if (Input::get_singleton()->is_key_pressed(Key::ALT) && grabbing != GRAB_NONE && selected_tangent_index == TANGENT_NONE) {932float prev_point_offset = (selected_index > 0) ? curve->get_point_position(selected_index - 1).x : curve->get_min_domain();933float next_point_offset = (selected_index < curve->get_point_count() - 1) ? curve->get_point_position(selected_index + 1).x : curve->get_max_domain();934935draw_line(Vector2(prev_point_offset, curve->get_min_value()), Vector2(prev_point_offset, curve->get_max_value()), Color(point_color, 0.6));936draw_line(Vector2(next_point_offset, curve->get_min_value()), Vector2(next_point_offset, curve->get_max_value()), Color(point_color, 0.6));937}938939if (shift_pressed && grabbing != GRAB_NONE && selected_tangent_index == TANGENT_NONE) {940draw_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"), EditorStringName(Editor)).darkened(0.4));941draw_line(Vector2(curve->get_min_domain(), initial_grab_pos.y), Vector2(curve->get_max_domain(), initial_grab_pos.y), get_theme_color(SNAME("axis_y_color"), EditorStringName(Editor)).darkened(0.4));942}943}944945///////////////////////946947const int CurveEditor::DEFAULT_SNAP = 10;948949void CurveEditor::_set_snap_enabled(bool p_enabled) {950curve_editor_rect->set_snap_enabled(p_enabled);951snap_count_edit->set_visible(p_enabled);952}953954void CurveEditor::_set_snap_count(int p_snap_count) {955curve_editor_rect->set_snap_count(CLAMP(p_snap_count, 2, 100));956}957958void CurveEditor::_on_preset_item_selected(int p_preset_id) {959curve_editor_rect->use_preset(p_preset_id);960}961962void CurveEditor::set_curve(const Ref<Curve> &p_curve) {963curve_editor_rect->set_curve(p_curve);964}965966void CurveEditor::_notification(int p_what) {967switch (p_what) {968case NOTIFICATION_THEME_CHANGED: {969spacing = Math::round(BASE_SPACING * get_theme_default_base_scale());970snap_button->set_button_icon(get_editor_theme_icon(SNAME("SnapGrid")));971PopupMenu *p = presets_button->get_popup();972p->clear();973p->add_icon_item(get_editor_theme_icon(SNAME("CurveConstant")), TTR("Constant"), CurveEdit::PRESET_CONSTANT);974p->add_icon_item(get_editor_theme_icon(SNAME("CurveLinear")), TTR("Linear"), CurveEdit::PRESET_LINEAR);975p->add_icon_item(get_editor_theme_icon(SNAME("CurveIn")), TTR("Ease In"), CurveEdit::PRESET_EASE_IN);976p->add_icon_item(get_editor_theme_icon(SNAME("CurveOut")), TTR("Ease Out"), CurveEdit::PRESET_EASE_OUT);977p->add_icon_item(get_editor_theme_icon(SNAME("CurveInOut")), TTR("Smoothstep"), CurveEdit::PRESET_SMOOTHSTEP);978} break;979case NOTIFICATION_READY: {980Ref<Curve> curve = curve_editor_rect->get_curve();981if (curve.is_valid()) {982// Set snapping settings based on the curve's meta.983snap_button->set_pressed(curve->get_meta("_snap_enabled", false));984snap_count_edit->set_value(curve->get_meta("_snap_count", DEFAULT_SNAP));985}986} break;987case NOTIFICATION_RESIZED:988curve_editor_rect->update_minimum_size();989break;990}991}992993CurveEditor::CurveEditor() {994HFlowContainer *toolbar = memnew(HFlowContainer);995add_child(toolbar);996997snap_button = memnew(Button);998snap_button->set_tooltip_text(TTR("Toggle Grid Snap"));999snap_button->set_toggle_mode(true);1000toolbar->add_child(snap_button);1001snap_button->connect(SceneStringName(toggled), callable_mp(this, &CurveEditor::_set_snap_enabled));10021003toolbar->add_child(memnew(VSeparator));10041005snap_count_edit = memnew(EditorSpinSlider);1006snap_count_edit->set_min(2);1007snap_count_edit->set_max(100);1008snap_count_edit->set_accessibility_name(TTRC("Snap Step"));1009snap_count_edit->set_value(DEFAULT_SNAP);1010snap_count_edit->set_custom_minimum_size(Size2(65 * EDSCALE, 0));1011toolbar->add_child(snap_count_edit);1012snap_count_edit->connect(SceneStringName(value_changed), callable_mp(this, &CurveEditor::_set_snap_count));10131014presets_button = memnew(MenuButton);1015presets_button->set_text(TTR("Presets"));1016presets_button->set_switch_on_hover(true);1017presets_button->set_h_size_flags(SIZE_EXPAND | SIZE_SHRINK_END);1018toolbar->add_child(presets_button);1019presets_button->get_popup()->connect(SceneStringName(id_pressed), callable_mp(this, &CurveEditor::_on_preset_item_selected));10201021curve_editor_rect = memnew(CurveEdit);1022add_child(curve_editor_rect);10231024// Some empty space below. Not a part of the curve editor so it can't draw in it.1025Control *empty_space = memnew(Control);1026empty_space->set_custom_minimum_size(Vector2(0, spacing));1027add_child(empty_space);10281029set_mouse_filter(MOUSE_FILTER_STOP);1030_set_snap_enabled(snap_button->is_pressed());1031_set_snap_count(snap_count_edit->get_value());1032}10331034///////////////////////10351036bool EditorInspectorPluginCurve::can_handle(Object *p_object) {1037return Object::cast_to<Curve>(p_object) != nullptr;1038}10391040void EditorInspectorPluginCurve::parse_begin(Object *p_object) {1041Curve *curve = Object::cast_to<Curve>(p_object);1042ERR_FAIL_NULL(curve);1043Ref<Curve> c(curve);10441045CurveEditor *editor = memnew(CurveEditor);1046editor->set_curve(c);1047add_custom_control(editor);1048}10491050CurveEditorPlugin::CurveEditorPlugin() {1051Ref<EditorInspectorPluginCurve> plugin;1052plugin.instantiate();1053add_inspector_plugin(plugin);10541055EditorInterface::get_singleton()->get_resource_previewer()->add_preview_generator(memnew(CurvePreviewGenerator));1056}10571058///////////////////////10591060bool CurvePreviewGenerator::handles(const String &p_type) const {1061return p_type == "Curve";1062}10631064Ref<Texture2D> CurvePreviewGenerator::generate(const Ref<Resource> &p_from, const Size2 &p_size, Dictionary &p_metadata) const {1065Ref<Curve> curve = p_from;1066if (curve.is_null()) {1067return Ref<Texture2D>();1068}10691070Ref<Image> img_ref;1071img_ref.instantiate();1072Image &im = **img_ref;1073im.initialize_data(p_size.x, p_size.y, false, Image::FORMAT_RGBA8);10741075Color line_color = EditorInterface::get_singleton()->get_editor_theme()->get_color(SceneStringName(font_color), EditorStringName(Editor));10761077// Set the first pixel of the thumbnail.1078float v = (curve->sample_baked(curve->get_min_domain()) - curve->get_min_value()) / curve->get_value_range();1079int y = CLAMP(im.get_height() - v * im.get_height(), 0, im.get_height() - 1);1080im.set_pixel(0, y, line_color);10811082// Plot a line towards the next point.1083int prev_y = y;1084for (int x = 1; x < im.get_width(); ++x) {1085float t = static_cast<float>(x) / im.get_width() * curve->get_domain_range() + curve->get_min_domain();1086v = (curve->sample_baked(t) - curve->get_min_value()) / curve->get_value_range();1087y = CLAMP(im.get_height() - v * im.get_height(), 0, im.get_height() - 1);10881089Vector<Point2i> points = Geometry2D::bresenham_line(Point2i(x - 1, prev_y), Point2i(x, y));1090for (Point2i point : points) {1091im.set_pixelv(point, line_color);1092}1093prev_y = y;1094}10951096return ImageTexture::create_from_image(img_ref);1097}109810991100