1/**************************************************************************/
2/* curve_editor_plugin.cpp */
3/**************************************************************************/
4/* This file is part of: */
5/* GODOT ENGINE */
6/* https://godotengine.org */
7/**************************************************************************/
8/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
9/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
10/* */
11/* Permission is hereby granted, free of charge, to any person obtaining */
12/* a copy of this software and associated documentation files (the */
13/* "Software"), to deal in the Software without restriction, including */
14/* without limitation the rights to use, copy, modify, merge, publish, */
15/* distribute, sublicense, and/or sell copies of the Software, and to */
16/* permit persons to whom the Software is furnished to do so, subject to */
17/* the following conditions: */
18/* */
19/* The above copyright notice and this permission notice shall be */
20/* included in all copies or substantial portions of the Software. */
21/* */
22/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
23/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
24/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
25/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
26/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
27/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
28/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
29/**************************************************************************/
30
31#include "curve_editor_plugin.h"
32
33#include "canvas_item_editor_plugin.h"
34#include "core/input/input.h"
35#include "core/math/geometry_2d.h"
36#include "core/os/keyboard.h"
37#include "editor/editor_interface.h"
38#include "editor/editor_node.h"
39#include "editor/editor_scale.h"
40#include "editor/editor_settings.h"
41#include "editor/editor_string_names.h"
42#include "editor/editor_undo_redo_manager.h"
43#include "editor/gui/editor_spin_slider.h"
44#include "scene/gui/flow_container.h"
45#include "scene/gui/menu_button.h"
46#include "scene/gui/popup_menu.h"
47#include "scene/gui/separator.h"
48#include "scene/resources/image_texture.h"
49
50CurveEdit::CurveEdit() {
51 set_focus_mode(FOCUS_ALL);
52 set_clip_contents(true);
53}
54
55void CurveEdit::_bind_methods() {
56 ClassDB::bind_method(D_METHOD("set_selected_index", "index"), &CurveEdit::set_selected_index);
57}
58
59void CurveEdit::set_curve(Ref<Curve> p_curve) {
60 if (p_curve == curve) {
61 return;
62 }
63
64 if (curve.is_valid()) {
65 curve->disconnect_changed(callable_mp(this, &CurveEdit::_curve_changed));
66 curve->disconnect(Curve::SIGNAL_RANGE_CHANGED, callable_mp(this, &CurveEdit::_curve_changed));
67 }
68
69 curve = p_curve;
70
71 if (curve.is_valid()) {
72 curve->connect_changed(callable_mp(this, &CurveEdit::_curve_changed));
73 curve->connect(Curve::SIGNAL_RANGE_CHANGED, callable_mp(this, &CurveEdit::_curve_changed));
74 }
75
76 // Note: if you edit a curve, then set another, and try to undo,
77 // it will normally apply on the previous curve, but you won't see it.
78}
79
80Ref<Curve> CurveEdit::get_curve() {
81 return curve;
82}
83
84void CurveEdit::set_snap_enabled(bool p_enabled) {
85 snap_enabled = p_enabled;
86 queue_redraw();
87 if (curve.is_valid()) {
88 if (snap_enabled) {
89 curve->set_meta(SNAME("_snap_enabled"), true);
90 } else {
91 curve->remove_meta(SNAME("_snap_enabled"));
92 }
93 }
94}
95
96void CurveEdit::set_snap_count(int p_snap_count) {
97 snap_count = p_snap_count;
98 queue_redraw();
99 if (curve.is_valid()) {
100 if (snap_count != CurveEditor::DEFAULT_SNAP) {
101 curve->set_meta(SNAME("_snap_count"), snap_count);
102 } else {
103 curve->remove_meta(SNAME("_snap_count"));
104 }
105 }
106}
107
108Size2 CurveEdit::get_minimum_size() const {
109 return Vector2(64, MAX(135, get_size().x * ASPECT_RATIO)) * EDSCALE;
110}
111
112void CurveEdit::_notification(int p_what) {
113 switch (p_what) {
114 case NOTIFICATION_MOUSE_EXIT: {
115 if (hovered_index != -1 || hovered_tangent_index != TANGENT_NONE) {
116 hovered_index = -1;
117 hovered_tangent_index = TANGENT_NONE;
118 queue_redraw();
119 }
120 } break;
121 case NOTIFICATION_THEME_CHANGED:
122 case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: {
123 float gizmo_scale = EDITOR_GET("interface/touchscreen/scale_gizmo_handles");
124 point_radius = Math::round(BASE_POINT_RADIUS * get_theme_default_base_scale() * gizmo_scale);
125 hover_radius = Math::round(BASE_HOVER_RADIUS * get_theme_default_base_scale() * gizmo_scale);
126 tangent_radius = Math::round(BASE_TANGENT_RADIUS * get_theme_default_base_scale() * gizmo_scale);
127 tangent_hover_radius = Math::round(BASE_TANGENT_HOVER_RADIUS * get_theme_default_base_scale() * gizmo_scale);
128 tangent_length = Math::round(BASE_TANGENT_LENGTH * get_theme_default_base_scale());
129 } break;
130 case NOTIFICATION_DRAW: {
131 _redraw();
132 } break;
133 case NOTIFICATION_VISIBILITY_CHANGED: {
134 if (!is_visible()) {
135 grabbing = GRAB_NONE;
136 }
137 } break;
138 }
139}
140
141void CurveEdit::gui_input(const Ref<InputEvent> &p_event) {
142 ERR_FAIL_COND(p_event.is_null());
143
144 Ref<InputEventKey> k = p_event;
145 if (k.is_valid()) {
146 // Deleting points or making tangents linear.
147 if (k->is_pressed() && k->get_keycode() == Key::KEY_DELETE) {
148 if (selected_tangent_index != TANGENT_NONE) {
149 toggle_linear(selected_index, selected_tangent_index);
150 } else if (selected_index != -1) {
151 if (grabbing == GRAB_ADD) {
152 curve->remove_point(selected_index); // Point is temporary, so remove directly from curve.
153 set_selected_index(-1);
154 } else {
155 remove_point(selected_index);
156 }
157 grabbing = GRAB_NONE;
158 hovered_index = -1;
159 hovered_tangent_index = TANGENT_NONE;
160 }
161 accept_event();
162 }
163
164 if (k->get_keycode() == Key::SHIFT || k->get_keycode() == Key::ALT) {
165 queue_redraw(); // Redraw to show the axes or constraints.
166 }
167 }
168
169 Ref<InputEventMouseButton> mb = p_event;
170 if (mb.is_valid() && mb->is_pressed()) {
171 Vector2 mpos = mb->get_position();
172
173 if (mb->get_button_index() == MouseButton::RIGHT || mb->get_button_index() == MouseButton::MIDDLE) {
174 if (mb->get_button_index() == MouseButton::RIGHT && grabbing == GRAB_MOVE) {
175 // Move a point to its old position.
176 curve->set_point_value(selected_index, initial_grab_pos.y);
177 curve->set_point_offset(selected_index, initial_grab_pos.x);
178 set_selected_index(initial_grab_index);
179 hovered_index = get_point_at(mpos);
180 grabbing = GRAB_NONE;
181 } else {
182 // Remove a point or make a tangent linear.
183 selected_tangent_index = get_tangent_at(mpos);
184 if (selected_tangent_index != TANGENT_NONE) {
185 toggle_linear(selected_index, selected_tangent_index);
186 } else {
187 int point_to_remove = get_point_at(mpos);
188 if (point_to_remove == -1) {
189 set_selected_index(-1); // Nothing on the place of the click, just deselect the point.
190 } else {
191 if (grabbing == GRAB_ADD) {
192 curve->remove_point(point_to_remove); // Point is temporary, so remove directly from curve.
193 set_selected_index(-1);
194 } else {
195 remove_point(point_to_remove);
196 }
197 hovered_index = get_point_at(mpos);
198 grabbing = GRAB_NONE;
199 }
200 }
201 }
202 }
203
204 // Selecting or creating points.
205 if (mb->get_button_index() == MouseButton::LEFT) {
206 if (grabbing == GRAB_NONE) {
207 selected_tangent_index = get_tangent_at(mpos);
208 if (selected_tangent_index == TANGENT_NONE) {
209 set_selected_index(get_point_at(mpos));
210 }
211 queue_redraw();
212 }
213
214 if (selected_index != -1) {
215 // If an existing point/tangent was grabbed, remember a few things about it.
216 grabbing = GRAB_MOVE;
217 initial_grab_pos = curve->get_point_position(selected_index);
218 initial_grab_index = selected_index;
219 if (selected_index > 0) {
220 initial_grab_left_tangent = curve->get_point_left_tangent(selected_index);
221 }
222 if (selected_index < curve->get_point_count() - 1) {
223 initial_grab_right_tangent = curve->get_point_right_tangent(selected_index);
224 }
225 } else if (grabbing == GRAB_NONE) {
226 // Adding a new point. Insert a temporary point for the user to adjust, so it's not in the undo/redo.
227 Vector2 new_pos = get_world_pos(mpos).clamp(Vector2(0.0, curve->get_min_value()), Vector2(1.0, curve->get_max_value()));
228 if (snap_enabled || mb->is_ctrl_pressed()) {
229 new_pos.x = Math::snapped(new_pos.x, 1.0 / snap_count);
230 new_pos.y = Math::snapped(new_pos.y - curve->get_min_value(), curve->get_range() / snap_count) + curve->get_min_value();
231 }
232
233 new_pos.x = get_offset_without_collision(selected_index, new_pos.x, mpos.x >= get_view_pos(new_pos).x);
234
235 // Add a temporary point for the user to adjust before adding it permanently.
236 int new_idx = curve->add_point_no_update(new_pos);
237 set_selected_index(new_idx);
238 grabbing = GRAB_ADD;
239 initial_grab_pos = new_pos;
240 }
241 }
242 }
243
244 if (mb.is_valid() && mb->get_button_index() == MouseButton::LEFT && !mb->is_pressed()) {
245 if (selected_tangent_index != TANGENT_NONE) {
246 // Finish moving a tangent control.
247 if (selected_index == 0) {
248 set_point_right_tangent(selected_index, curve->get_point_right_tangent(selected_index));
249 } else if (selected_index == curve->get_point_count() - 1) {
250 set_point_left_tangent(selected_index, curve->get_point_left_tangent(selected_index));
251 } else {
252 set_point_tangents(selected_index, curve->get_point_left_tangent(selected_index), curve->get_point_right_tangent(selected_index));
253 }
254 grabbing = GRAB_NONE;
255 } else if (grabbing == GRAB_MOVE) {
256 // Finish moving a point.
257 set_point_position(selected_index, curve->get_point_position(selected_index));
258 grabbing = GRAB_NONE;
259 } else if (grabbing == GRAB_ADD) {
260 // Finish inserting a new point. Remove the temporary point and insert a permanent one in its place.
261 Vector2 new_pos = curve->get_point_position(selected_index);
262 curve->remove_point(selected_index);
263 add_point(new_pos);
264 grabbing = GRAB_NONE;
265 }
266 queue_redraw();
267 }
268
269 Ref<InputEventMouseMotion> mm = p_event;
270 if (mm.is_valid()) {
271 Vector2 mpos = mm->get_position();
272
273 if (grabbing != GRAB_NONE && curve.is_valid()) {
274 if (selected_index != -1) {
275 if (selected_tangent_index == TANGENT_NONE) {
276 // Drag point.
277 Vector2 new_pos = get_world_pos(mpos).clamp(Vector2(0.0, curve->get_min_value()), Vector2(1.0, curve->get_max_value()));
278
279 if (snap_enabled || mm->is_ctrl_pressed()) {
280 new_pos.x = Math::snapped(new_pos.x, 1.0 / snap_count);
281 new_pos.y = Math::snapped(new_pos.y - curve->get_min_value(), curve->get_range() / snap_count) + curve->get_min_value();
282 }
283
284 // Allow to snap to axes with Shift.
285 if (mm->is_shift_pressed()) {
286 Vector2 initial_mpos = get_view_pos(initial_grab_pos);
287 if (Math::abs(mpos.x - initial_mpos.x) > Math::abs(mpos.y - initial_mpos.y)) {
288 new_pos.y = initial_grab_pos.y;
289 } else {
290 new_pos.x = initial_grab_pos.x;
291 }
292 }
293
294 // Allow to constraint the point between the adjacent two with Alt.
295 if (mm->is_alt_pressed()) {
296 float prev_point_offset = (selected_index > 0) ? (curve->get_point_position(selected_index - 1).x + 0.00001) : 0.0;
297 float next_point_offset = (selected_index < curve->get_point_count() - 1) ? (curve->get_point_position(selected_index + 1).x - 0.00001) : 1.0;
298 new_pos.x = CLAMP(new_pos.x, prev_point_offset, next_point_offset);
299 }
300
301 new_pos.x = get_offset_without_collision(selected_index, new_pos.x, mpos.x >= get_view_pos(new_pos).x);
302
303 // The index may change if the point is dragged across another one.
304 int i = curve->set_point_offset(selected_index, new_pos.x);
305 hovered_index = i;
306 set_selected_index(i);
307
308 new_pos.y = CLAMP(new_pos.y, curve->get_min_value(), curve->get_max_value());
309 curve->set_point_value(selected_index, new_pos.y);
310
311 } else {
312 // Drag tangent.
313
314 const Vector2 new_pos = curve->get_point_position(selected_index);
315 const Vector2 control_pos = get_world_pos(mpos);
316
317 Vector2 dir = (control_pos - new_pos).normalized();
318 real_t tangent = dir.y / (dir.x > 0 ? MAX(dir.x, 0.00001) : MIN(dir.x, -0.00001));
319
320 // Must keep track of the hovered index as the cursor might move outside of the editor while dragging.
321 hovered_tangent_index = selected_tangent_index;
322
323 // Adjust the tangents.
324 if (selected_tangent_index == TANGENT_LEFT) {
325 curve->set_point_left_tangent(selected_index, tangent);
326
327 // Align the other tangent if it isn't linear and Shift is not pressed.
328 // If Shift is pressed at any point, restore the initial angle of the other tangent.
329 if (selected_index != (curve->get_point_count() - 1) && curve->get_point_right_mode(selected_index) != Curve::TANGENT_LINEAR) {
330 curve->set_point_right_tangent(selected_index, mm->is_shift_pressed() ? initial_grab_right_tangent : tangent);
331 }
332
333 } else {
334 curve->set_point_right_tangent(selected_index, tangent);
335
336 if (selected_index != 0 && curve->get_point_left_mode(selected_index) != Curve::TANGENT_LINEAR) {
337 curve->set_point_left_tangent(selected_index, mm->is_shift_pressed() ? initial_grab_left_tangent : tangent);
338 }
339 }
340 }
341 }
342 } else {
343 // Grab mode is GRAB_NONE, so do hovering logic.
344 hovered_index = get_point_at(mpos);
345 hovered_tangent_index = get_tangent_at(mpos);
346 queue_redraw();
347 }
348 }
349}
350
351void CurveEdit::use_preset(int p_preset_id) {
352 ERR_FAIL_COND(p_preset_id < 0 || p_preset_id >= PRESET_COUNT);
353 ERR_FAIL_COND(curve.is_null());
354
355 Array previous_data = curve->get_data();
356 curve->clear_points();
357
358 float min_value = curve->get_min_value();
359 float max_value = curve->get_max_value();
360
361 switch (p_preset_id) {
362 case PRESET_CONSTANT:
363 curve->add_point(Vector2(0, (min_value + max_value) / 2.0));
364 curve->add_point(Vector2(1, (min_value + max_value) / 2.0));
365 curve->set_point_right_mode(0, Curve::TANGENT_LINEAR);
366 curve->set_point_left_mode(1, Curve::TANGENT_LINEAR);
367 break;
368
369 case PRESET_LINEAR:
370 curve->add_point(Vector2(0, min_value));
371 curve->add_point(Vector2(1, max_value));
372 curve->set_point_right_mode(0, Curve::TANGENT_LINEAR);
373 curve->set_point_left_mode(1, Curve::TANGENT_LINEAR);
374 break;
375
376 case PRESET_EASE_IN:
377 curve->add_point(Vector2(0, min_value));
378 curve->add_point(Vector2(1, max_value), curve->get_range() * 1.4, 0);
379 break;
380
381 case PRESET_EASE_OUT:
382 curve->add_point(Vector2(0, min_value), 0, curve->get_range() * 1.4);
383 curve->add_point(Vector2(1, max_value));
384 break;
385
386 case PRESET_SMOOTHSTEP:
387 curve->add_point(Vector2(0, min_value));
388 curve->add_point(Vector2(1, max_value));
389 break;
390
391 default:
392 break;
393 }
394
395 EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
396 undo_redo->create_action(TTR("Load Curve Preset"));
397 undo_redo->add_do_method(*curve, "_set_data", curve->get_data());
398 undo_redo->add_do_method(this, "set_selected_index", -1);
399 undo_redo->add_undo_method(*curve, "_set_data", previous_data);
400 undo_redo->add_undo_method(this, "set_selected_index", selected_index);
401 undo_redo->commit_action();
402}
403
404void CurveEdit::_curve_changed() {
405 queue_redraw();
406 // Point count can change in case of undo.
407 if (selected_index >= curve->get_point_count()) {
408 set_selected_index(-1);
409 }
410}
411
412int CurveEdit::get_point_at(Vector2 p_pos) const {
413 if (curve.is_null()) {
414 return -1;
415 }
416
417 // Use a square-shaped hover region. If hovering multiple points, pick the closer one.
418 const Rect2 hover_rect = Rect2(p_pos, Vector2(0, 0)).grow(hover_radius);
419 int closest_idx = -1;
420 float closest_dist_squared = hover_radius * hover_radius * 2;
421
422 for (int i = 0; i < curve->get_point_count(); ++i) {
423 Vector2 p = get_view_pos(curve->get_point_position(i));
424 if (hover_rect.has_point(p) && p.distance_squared_to(p_pos) < closest_dist_squared) {
425 closest_dist_squared = p.distance_squared_to(p_pos);
426 closest_idx = i;
427 }
428 }
429
430 return closest_idx;
431}
432
433CurveEdit::TangentIndex CurveEdit::get_tangent_at(Vector2 p_pos) const {
434 if (curve.is_null() || selected_index < 0) {
435 return TANGENT_NONE;
436 }
437
438 const Rect2 hover_rect = Rect2(p_pos, Vector2(0, 0)).grow(tangent_hover_radius);
439
440 if (selected_index != 0) {
441 Vector2 control_pos = get_tangent_view_pos(selected_index, TANGENT_LEFT);
442 if (hover_rect.has_point(control_pos)) {
443 return TANGENT_LEFT;
444 }
445 }
446
447 if (selected_index != curve->get_point_count() - 1) {
448 Vector2 control_pos = get_tangent_view_pos(selected_index, TANGENT_RIGHT);
449 if (hover_rect.has_point(control_pos)) {
450 return TANGENT_RIGHT;
451 }
452 }
453
454 return TANGENT_NONE;
455}
456
457// FIXME: This function should be bounded better.
458float CurveEdit::get_offset_without_collision(int p_current_index, float p_offset, bool p_prioritize_right) {
459 float safe_offset = p_offset;
460 bool prioritizing_right = p_prioritize_right;
461
462 for (int i = 0; i < curve->get_point_count(); i++) {
463 if (i == p_current_index) {
464 continue;
465 }
466
467 if (curve->get_point_position(i).x > safe_offset) {
468 break;
469 }
470
471 if (curve->get_point_position(i).x == safe_offset) {
472 if (prioritizing_right) {
473 safe_offset += 0.00001;
474 if (safe_offset > 1.0) {
475 safe_offset = 1.0;
476 prioritizing_right = false;
477 }
478 } else {
479 safe_offset -= 0.00001;
480 if (safe_offset < 0.0) {
481 safe_offset = 0.0;
482 prioritizing_right = true;
483 }
484 }
485 i = -1;
486 }
487 }
488
489 return safe_offset;
490}
491
492void CurveEdit::add_point(Vector2 p_pos) {
493 ERR_FAIL_COND(curve.is_null());
494
495 // Add a point to get its index, then remove it immediately. Trick to feed the UndoRedo.
496 int new_idx = curve->add_point(p_pos);
497 curve->remove_point(new_idx);
498
499 EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
500 undo_redo->create_action(TTR("Add Curve Point"));
501 undo_redo->add_do_method(*curve, "add_point", p_pos);
502 undo_redo->add_do_method(this, "set_selected_index", new_idx);
503 undo_redo->add_undo_method(*curve, "remove_point", new_idx);
504 undo_redo->add_undo_method(this, "set_selected_index", -1);
505 undo_redo->commit_action();
506}
507
508void CurveEdit::remove_point(int p_index) {
509 ERR_FAIL_COND(curve.is_null());
510 ERR_FAIL_INDEX_MSG(p_index, curve->get_point_count(), "Curve point is out of bounds.");
511
512 Curve::Point p = curve->get_point(p_index);
513 Vector2 old_pos = (grabbing == GRAB_MOVE) ? initial_grab_pos : p.position;
514
515 int new_selected_index = selected_index;
516 // Reselect the old selected point if it's not the deleted one.
517 if (new_selected_index > p_index) {
518 new_selected_index -= 1;
519 } else if (new_selected_index == p_index) {
520 new_selected_index = -1;
521 }
522
523 EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
524 undo_redo->create_action(TTR("Remove Curve Point"));
525 undo_redo->add_do_method(*curve, "remove_point", p_index);
526 undo_redo->add_do_method(this, "set_selected_index", new_selected_index);
527 undo_redo->add_undo_method(*curve, "add_point", old_pos, p.left_tangent, p.right_tangent, p.left_mode, p.right_mode);
528 undo_redo->add_undo_method(this, "set_selected_index", selected_index);
529 undo_redo->commit_action();
530}
531
532void CurveEdit::set_point_position(int p_index, Vector2 p_pos) {
533 ERR_FAIL_COND(curve.is_null());
534 ERR_FAIL_INDEX_MSG(p_index, curve->get_point_count(), "Curve point is out of bounds.");
535
536 if (initial_grab_pos == p_pos) {
537 return;
538 }
539
540 // Pretend the point started from its old place.
541 curve->set_point_value(p_index, initial_grab_pos.y);
542 curve->set_point_offset(p_index, initial_grab_pos.x);
543 // Note: Changing the offset may modify the order.
544 EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
545 undo_redo->create_action(TTR("Modify Curve Point"));
546 undo_redo->add_do_method(*curve, "set_point_value", initial_grab_index, p_pos.y);
547 undo_redo->add_do_method(*curve, "set_point_offset", initial_grab_index, p_pos.x);
548 undo_redo->add_do_method(this, "set_selected_index", p_index);
549 undo_redo->add_undo_method(*curve, "set_point_value", p_index, initial_grab_pos.y);
550 undo_redo->add_undo_method(*curve, "set_point_offset", p_index, initial_grab_pos.x);
551 undo_redo->add_undo_method(this, "set_selected_index", initial_grab_index);
552 undo_redo->commit_action();
553}
554
555void CurveEdit::set_point_tangents(int p_index, float p_left, float p_right) {
556 ERR_FAIL_COND(curve.is_null());
557 ERR_FAIL_INDEX_MSG(p_index, curve->get_point_count(), "Curve point is out of bounds.");
558
559 if (initial_grab_left_tangent == p_left) {
560 set_point_right_tangent(p_index, p_right);
561 return;
562 } else if (initial_grab_right_tangent == p_right) {
563 set_point_left_tangent(p_index, p_left);
564 return;
565 }
566
567 curve->set_point_left_tangent(p_index, initial_grab_left_tangent);
568 curve->set_point_right_tangent(p_index, initial_grab_right_tangent);
569 EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
570 undo_redo->create_action(TTR("Modify Curve Point's Tangents"));
571 undo_redo->add_do_method(*curve, "set_point_left_tangent", p_index, p_left);
572 undo_redo->add_do_method(*curve, "set_point_right_tangent", p_index, p_right);
573 undo_redo->add_do_method(this, "set_selected_index", p_index);
574 undo_redo->add_undo_method(*curve, "set_point_left_tangent", p_index, initial_grab_left_tangent);
575 undo_redo->add_undo_method(*curve, "set_point_right_tangent", p_index, initial_grab_right_tangent);
576 undo_redo->add_undo_method(this, "set_selected_index", p_index);
577 undo_redo->commit_action();
578}
579
580void CurveEdit::set_point_left_tangent(int p_index, float p_tangent) {
581 ERR_FAIL_COND(curve.is_null());
582 ERR_FAIL_INDEX_MSG(p_index, curve->get_point_count(), "Curve point is out of bounds.");
583
584 if (initial_grab_left_tangent == p_tangent) {
585 return;
586 }
587
588 curve->set_point_left_tangent(p_index, initial_grab_left_tangent);
589 EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
590 undo_redo->create_action(TTR("Modify Curve Point's Left Tangent"));
591 undo_redo->add_do_method(*curve, "set_point_left_tangent", p_index, p_tangent);
592 undo_redo->add_do_method(this, "set_selected_index", p_index);
593 undo_redo->add_undo_method(*curve, "set_point_left_tangent", p_index, initial_grab_left_tangent);
594 undo_redo->add_undo_method(this, "set_selected_index", p_index);
595 undo_redo->commit_action();
596}
597
598void CurveEdit::set_point_right_tangent(int p_index, float p_tangent) {
599 ERR_FAIL_COND(curve.is_null());
600 ERR_FAIL_INDEX_MSG(p_index, curve->get_point_count(), "Curve point is out of bounds.");
601
602 if (initial_grab_right_tangent == p_tangent) {
603 return;
604 }
605
606 curve->set_point_right_tangent(p_index, initial_grab_right_tangent);
607 EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
608 undo_redo->create_action(TTR("Modify Curve Point's Right Tangent"));
609 undo_redo->add_do_method(*curve, "set_point_right_tangent", p_index, p_tangent);
610 undo_redo->add_do_method(this, "set_selected_index", p_index);
611 undo_redo->add_undo_method(*curve, "set_point_right_tangent", p_index, initial_grab_right_tangent);
612 undo_redo->add_undo_method(this, "set_selected_index", p_index);
613 undo_redo->commit_action();
614}
615
616void CurveEdit::toggle_linear(int p_index, TangentIndex p_tangent) {
617 ERR_FAIL_COND(curve.is_null());
618 ERR_FAIL_INDEX_MSG(p_index, curve->get_point_count(), "Curve point is out of bounds.");
619
620 if (p_tangent == TANGENT_NONE) {
621 return;
622 }
623
624 EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
625 undo_redo->create_action(TTR("Toggle Linear Curve Point's Tangent"));
626
627 Curve::TangentMode prev_mode = (p_tangent == TANGENT_LEFT) ? curve->get_point_left_mode(p_index) : curve->get_point_right_mode(p_index);
628 Curve::TangentMode mode = (prev_mode == Curve::TANGENT_LINEAR) ? Curve::TANGENT_FREE : Curve::TANGENT_LINEAR;
629 float prev_angle = (p_tangent == TANGENT_LEFT) ? curve->get_point_left_tangent(p_index) : curve->get_point_right_tangent(p_index);
630
631 // Add different methods in the UndoRedo based on the tangent passed.
632 if (p_tangent == TANGENT_LEFT) {
633 undo_redo->add_do_method(*curve, "set_point_left_mode", p_index, mode);
634 undo_redo->add_undo_method(*curve, "set_point_left_mode", p_index, prev_mode);
635 undo_redo->add_undo_method(*curve, "set_point_left_tangent", p_index, prev_angle);
636 } else {
637 undo_redo->add_do_method(*curve, "set_point_right_mode", p_index, mode);
638 undo_redo->add_undo_method(*curve, "set_point_right_mode", p_index, prev_mode);
639 undo_redo->add_undo_method(*curve, "set_point_right_tangent", p_index, prev_angle);
640 }
641
642 undo_redo->commit_action();
643}
644
645void CurveEdit::set_selected_index(int p_index) {
646 if (p_index != selected_index) {
647 selected_index = p_index;
648 queue_redraw();
649 }
650}
651
652void CurveEdit::update_view_transform() {
653 Ref<Font> font = get_theme_font(SNAME("font"), SNAME("Label"));
654 int font_size = get_theme_font_size(SNAME("font_size"), SNAME("Label"));
655
656 const real_t margin = font->get_height(font_size) + 2 * EDSCALE;
657
658 float min_y = curve.is_valid() ? curve->get_min_value() : 0.0;
659 float max_y = curve.is_valid() ? curve->get_max_value() : 1.0;
660
661 const Rect2 world_rect = Rect2(Curve::MIN_X, min_y, Curve::MAX_X, max_y - min_y);
662 const Size2 view_margin(margin, margin);
663 const Size2 view_size = get_size() - view_margin * 2;
664 const Vector2 scale = view_size / world_rect.size;
665
666 Transform2D world_trans;
667 world_trans.translate_local(-world_rect.position - Vector2(0, world_rect.size.y));
668 world_trans.scale(Vector2(scale.x, -scale.y));
669
670 Transform2D view_trans;
671 view_trans.translate_local(view_margin);
672
673 _world_to_view = view_trans * world_trans;
674}
675
676Vector2 CurveEdit::get_tangent_view_pos(int p_index, TangentIndex p_tangent) const {
677 Vector2 dir;
678 if (p_tangent == TANGENT_LEFT) {
679 dir = -Vector2(1, curve->get_point_left_tangent(p_index));
680 } else {
681 dir = Vector2(1, curve->get_point_right_tangent(p_index));
682 }
683
684 Vector2 point_pos = curve->get_point_position(p_index);
685 Vector2 point_view_pos = get_view_pos(point_pos);
686 Vector2 control_view_pos = get_view_pos(point_pos + dir);
687
688 Vector2 distance_from_point = tangent_length * (control_view_pos - point_view_pos).normalized();
689 Vector2 tangent_view_pos = point_view_pos + distance_from_point;
690
691 // Since the tangent is long, it might slip outside of the area of the editor for points close to the domain/range boundaries.
692 // The code below shrinks the tangent control by up to 50% so it always stays inside the editor for points within the bounds.
693 float fraction_inside = 1.0;
694 if (distance_from_point.x != 0.0) {
695 fraction_inside = MIN(fraction_inside, ((distance_from_point.x > 0 ? get_rect().size.x : 0) - point_view_pos.x) / distance_from_point.x);
696 }
697 if (distance_from_point.y != 0.0) {
698 fraction_inside = MIN(fraction_inside, ((distance_from_point.y > 0 ? get_rect().size.y : 0) - point_view_pos.y) / distance_from_point.y);
699 }
700
701 if (fraction_inside < 1.0 && fraction_inside > 0.5) {
702 tangent_view_pos = point_view_pos + distance_from_point * fraction_inside;
703 }
704
705 return tangent_view_pos;
706}
707
708Vector2 CurveEdit::get_view_pos(Vector2 p_world_pos) const {
709 return _world_to_view.xform(p_world_pos);
710}
711
712Vector2 CurveEdit::get_world_pos(Vector2 p_view_pos) const {
713 return _world_to_view.affine_inverse().xform(p_view_pos);
714}
715
716// Uses non-baked points, but takes advantage of ordered iteration to be faster.
717template <typename T>
718static void plot_curve_accurate(const Curve &curve, float step, Vector2 scaling, T plot_func) {
719 if (curve.get_point_count() <= 1) {
720 // Not enough points to make a curve, so it's just a straight line.
721 // The added tiny vectors make the drawn line stay exactly within the bounds in practice.
722 float y = curve.sample(0);
723 plot_func(Vector2(0, y) * scaling + Vector2(0.5, 0), Vector2(1.f, y) * scaling - Vector2(1.5, 0), true);
724
725 } else {
726 Vector2 first_point = curve.get_point_position(0);
727 Vector2 last_point = curve.get_point_position(curve.get_point_count() - 1);
728
729 // Edge lines
730 plot_func(Vector2(0, first_point.y) * scaling + Vector2(0.5, 0), first_point * scaling, false);
731 plot_func(Vector2(Curve::MAX_X, last_point.y) * scaling - Vector2(1.5, 0), last_point * scaling, false);
732
733 // Draw section by section, so that we get maximum precision near points.
734 // It's an accurate representation, but slower than using the baked one.
735 for (int i = 1; i < curve.get_point_count(); ++i) {
736 Vector2 a = curve.get_point_position(i - 1);
737 Vector2 b = curve.get_point_position(i);
738
739 Vector2 pos = a;
740 Vector2 prev_pos = a;
741
742 float scaled_step = step / scaling.x;
743 float samples = (b.x - a.x) / scaled_step;
744
745 for (int j = 1; j < samples; j++) {
746 float x = j * scaled_step;
747 pos.x = a.x + x;
748 pos.y = curve.sample_local_nocheck(i - 1, x);
749 plot_func(prev_pos * scaling, pos * scaling, true);
750 prev_pos = pos;
751 }
752
753 plot_func(prev_pos * scaling, b * scaling, true);
754 }
755 }
756}
757
758struct CanvasItemPlotCurve {
759 CanvasItem &ci;
760 Color color1;
761 Color color2;
762
763 CanvasItemPlotCurve(CanvasItem &p_ci, Color p_color1, Color p_color2) :
764 ci(p_ci),
765 color1(p_color1),
766 color2(p_color2) {}
767
768 void operator()(Vector2 pos0, Vector2 pos1, bool in_definition) {
769 ci.draw_line(pos0, pos1, in_definition ? color1 : color2, 0.5, true);
770 }
771};
772
773void CurveEdit::_redraw() {
774 if (curve.is_null()) {
775 return;
776 }
777
778 update_view_transform();
779
780 // Draw background.
781
782 Vector2 view_size = get_rect().size;
783 draw_style_box(get_theme_stylebox(SNAME("panel"), SNAME("Tree")), Rect2(Point2(), view_size));
784
785 // Draw snapping grid, then primary grid.
786 draw_set_transform_matrix(_world_to_view);
787
788 Vector2 min_edge = get_world_pos(Vector2(0, view_size.y));
789 Vector2 max_edge = get_world_pos(Vector2(view_size.x, 0));
790
791 const Color grid_color_primary = get_theme_color(SNAME("mono_color"), EditorStringName(Editor)) * Color(1, 1, 1, 0.25);
792 const Color grid_color = get_theme_color(SNAME("mono_color"), EditorStringName(Editor)) * Color(1, 1, 1, 0.1);
793
794 const Vector2i grid_steps = Vector2i(4, 2);
795 const Vector2 step_size = Vector2(1, curve->get_range()) / grid_steps;
796
797 draw_line(Vector2(min_edge.x, curve->get_min_value()), Vector2(max_edge.x, curve->get_min_value()), grid_color_primary);
798 draw_line(Vector2(max_edge.x, curve->get_max_value()), Vector2(min_edge.x, curve->get_max_value()), grid_color_primary);
799 draw_line(Vector2(0, min_edge.y), Vector2(0, max_edge.y), grid_color_primary);
800 draw_line(Vector2(1, max_edge.y), Vector2(1, min_edge.y), grid_color_primary);
801
802 for (int i = 1; i < grid_steps.x; i++) {
803 real_t x = i * step_size.x;
804 draw_line(Vector2(x, min_edge.y), Vector2(x, max_edge.y), grid_color);
805 }
806
807 for (int i = 1; i < grid_steps.y; i++) {
808 real_t y = curve->get_min_value() + i * step_size.y;
809 draw_line(Vector2(min_edge.x, y), Vector2(max_edge.x, y), grid_color);
810 }
811
812 // Draw number markings.
813 draw_set_transform_matrix(Transform2D());
814
815 Ref<Font> font = get_theme_font(SNAME("font"), SNAME("Label"));
816 int font_size = get_theme_font_size(SNAME("font_size"), SNAME("Label"));
817 float font_height = font->get_height(font_size);
818 Color text_color = get_theme_color(SNAME("font_color"), EditorStringName(Editor));
819
820 for (int i = 0; i <= grid_steps.x; ++i) {
821 real_t x = i * step_size.x;
822 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);
823 }
824
825 for (int i = 0; i <= grid_steps.y; ++i) {
826 real_t y = curve->get_min_value() + i * step_size.y;
827 draw_string(font, get_view_pos(Vector2(0, y)) + Vector2(2, -2), String::num(y, 2), HORIZONTAL_ALIGNMENT_LEFT, -1, font_size, text_color);
828 }
829
830 // Draw curve.
831
832 // An unusual transform so we can offset the curve before scaling it up, allowing the curve to be antialiased.
833 // The scaling up ensures that the curve rendering doesn't break when we use a quad line to draw it.
834 draw_set_transform_matrix(Transform2D(0, get_view_pos(Vector2(0, 0))));
835
836 const Color line_color = get_theme_color(SNAME("font_color"), EditorStringName(Editor));
837 const Color edge_line_color = get_theme_color(SNAME("font_color"), EditorStringName(Editor)) * Color(1, 1, 1, 0.75);
838
839 CanvasItemPlotCurve plot_func(*this, line_color, edge_line_color);
840 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);
841
842 // Draw points, except for the selected one.
843 draw_set_transform_matrix(Transform2D());
844
845 bool shift_pressed = Input::get_singleton()->is_key_pressed(Key::SHIFT);
846
847 const Color point_color = get_theme_color(SNAME("font_color"), EditorStringName(Editor));
848
849 for (int i = 0; i < curve->get_point_count(); ++i) {
850 Vector2 pos = get_view_pos(curve->get_point_position(i));
851 if (selected_index != i) {
852 draw_rect(Rect2(pos, Vector2(0, 0)).grow(point_radius), point_color);
853 }
854 if (hovered_index == i && hovered_tangent_index == TANGENT_NONE) {
855 draw_rect(Rect2(pos, Vector2(0, 0)).grow(hover_radius - Math::round(3 * EDSCALE)), line_color, false, Math::round(1 * EDSCALE));
856 }
857 }
858
859 // Draw selected point and its tangents.
860
861 if (selected_index >= 0) {
862 const Vector2 point_pos = curve->get_point_position(selected_index);
863 const Color selected_point_color = get_theme_color(SNAME("accent_color"), EditorStringName(Editor));
864
865 // Draw tangents if not dragging a point, or if holding a point without having moved it yet.
866 if (grabbing == GRAB_NONE || initial_grab_pos == point_pos || selected_tangent_index != TANGENT_NONE) {
867 const Color selected_tangent_color = get_theme_color(SNAME("accent_color"), EditorStringName(Editor)).darkened(0.25);
868 const Color tangent_color = get_theme_color(SNAME("font_color"), EditorStringName(Editor)).darkened(0.25);
869
870 if (selected_index != 0) {
871 Vector2 control_pos = get_tangent_view_pos(selected_index, TANGENT_LEFT);
872 Color left_tangent_color = (selected_tangent_index == TANGENT_LEFT) ? selected_tangent_color : tangent_color;
873
874 draw_line(get_view_pos(point_pos), control_pos, left_tangent_color, 0.5 * EDSCALE, true);
875 // Square for linear mode, circle otherwise.
876 if (curve->get_point_left_mode(selected_index) == Curve::TANGENT_FREE) {
877 draw_circle(control_pos, tangent_radius, left_tangent_color);
878 } else {
879 draw_rect(Rect2(control_pos, Vector2(0, 0)).grow(tangent_radius), left_tangent_color);
880 }
881 // Hover indicator.
882 if (hovered_tangent_index == TANGENT_LEFT || (hovered_tangent_index == TANGENT_RIGHT && !shift_pressed && curve->get_point_left_mode(selected_index) != Curve::TANGENT_LINEAR)) {
883 draw_rect(Rect2(control_pos, Vector2(0, 0)).grow(tangent_hover_radius - Math::round(3 * EDSCALE)), tangent_color, false, Math::round(1 * EDSCALE));
884 }
885 }
886
887 if (selected_index != curve->get_point_count() - 1) {
888 Vector2 control_pos = get_tangent_view_pos(selected_index, TANGENT_RIGHT);
889 Color right_tangent_color = (selected_tangent_index == TANGENT_RIGHT) ? selected_tangent_color : tangent_color;
890
891 draw_line(get_view_pos(point_pos), control_pos, right_tangent_color, 0.5 * EDSCALE, true);
892 // Square for linear mode, circle otherwise.
893 if (curve->get_point_right_mode(selected_index) == Curve::TANGENT_FREE) {
894 draw_circle(control_pos, tangent_radius, right_tangent_color);
895 } else {
896 draw_rect(Rect2(control_pos, Vector2(0, 0)).grow(tangent_radius), right_tangent_color);
897 }
898 // Hover indicator.
899 if (hovered_tangent_index == TANGENT_RIGHT || (hovered_tangent_index == TANGENT_LEFT && !shift_pressed && curve->get_point_right_mode(selected_index) != Curve::TANGENT_LINEAR)) {
900 draw_rect(Rect2(control_pos, Vector2(0, 0)).grow(tangent_hover_radius - Math::round(3 * EDSCALE)), tangent_color, false, Math::round(1 * EDSCALE));
901 }
902 }
903 }
904
905 draw_rect(Rect2(get_view_pos(point_pos), Vector2(0, 0)).grow(point_radius), selected_point_color);
906 }
907
908 // Draw help text.
909
910 if (selected_index > 0 && selected_index < curve->get_point_count() - 1 && selected_tangent_index == TANGENT_NONE && hovered_tangent_index != TANGENT_NONE && !shift_pressed) {
911 float width = view_size.x - 50 * EDSCALE;
912 text_color.a *= 0.4;
913
914 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);
915
916 } else if (selected_index != -1 && selected_tangent_index == TANGENT_NONE) {
917 const Vector2 point_pos = curve->get_point_position(selected_index);
918 float width = view_size.x - 50 * EDSCALE;
919 text_color.a *= 0.8;
920
921 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);
922
923 } else if (selected_index != -1 && selected_tangent_index != TANGENT_NONE) {
924 float width = view_size.x - 50 * EDSCALE;
925 text_color.a *= 0.8;
926 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)));
927
928 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);
929 }
930
931 // Draw temporary constraints and snapping axes.
932 draw_set_transform_matrix(_world_to_view);
933
934 if (Input::get_singleton()->is_key_pressed(Key::ALT) && grabbing != GRAB_NONE && selected_tangent_index == TANGENT_NONE) {
935 float prev_point_offset = (selected_index > 0) ? curve->get_point_position(selected_index - 1).x : 0.0;
936 float next_point_offset = (selected_index < curve->get_point_count() - 1) ? curve->get_point_position(selected_index + 1).x : 1.0;
937
938 draw_line(Vector2(prev_point_offset, curve->get_min_value()), Vector2(prev_point_offset, curve->get_max_value()), Color(point_color, 0.6));
939 draw_line(Vector2(next_point_offset, curve->get_min_value()), Vector2(next_point_offset, curve->get_max_value()), Color(point_color, 0.6));
940 }
941
942 if (shift_pressed && grabbing != GRAB_NONE && selected_tangent_index == TANGENT_NONE) {
943 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"), EditorStringName(Editor)).darkened(0.4));
944 draw_line(Vector2(0, initial_grab_pos.y), Vector2(1, initial_grab_pos.y), get_theme_color(SNAME("axis_y_color"), EditorStringName(Editor)).darkened(0.4));
945 }
946}
947
948///////////////////////
949
950const int CurveEditor::DEFAULT_SNAP = 10;
951
952void CurveEditor::_set_snap_enabled(bool p_enabled) {
953 curve_editor_rect->set_snap_enabled(p_enabled);
954 snap_count_edit->set_visible(p_enabled);
955}
956
957void CurveEditor::_set_snap_count(int p_snap_count) {
958 curve_editor_rect->set_snap_count(CLAMP(p_snap_count, 2, 100));
959}
960
961void CurveEditor::_on_preset_item_selected(int p_preset_id) {
962 curve_editor_rect->use_preset(p_preset_id);
963}
964
965void CurveEditor::set_curve(const Ref<Curve> &p_curve) {
966 curve_editor_rect->set_curve(p_curve);
967}
968
969void CurveEditor::_notification(int p_what) {
970 switch (p_what) {
971 case NOTIFICATION_THEME_CHANGED: {
972 spacing = Math::round(BASE_SPACING * get_theme_default_base_scale());
973 snap_button->set_icon(get_editor_theme_icon(SNAME("SnapGrid")));
974 PopupMenu *p = presets_button->get_popup();
975 p->clear();
976 p->add_icon_item(get_editor_theme_icon(SNAME("CurveConstant")), TTR("Constant"), CurveEdit::PRESET_CONSTANT);
977 p->add_icon_item(get_editor_theme_icon(SNAME("CurveLinear")), TTR("Linear"), CurveEdit::PRESET_LINEAR);
978 p->add_icon_item(get_editor_theme_icon(SNAME("CurveIn")), TTR("Ease In"), CurveEdit::PRESET_EASE_IN);
979 p->add_icon_item(get_editor_theme_icon(SNAME("CurveOut")), TTR("Ease Out"), CurveEdit::PRESET_EASE_OUT);
980 p->add_icon_item(get_editor_theme_icon(SNAME("CurveInOut")), TTR("Smoothstep"), CurveEdit::PRESET_SMOOTHSTEP);
981 } break;
982 case NOTIFICATION_READY: {
983 Ref<Curve> curve = curve_editor_rect->get_curve();
984 if (curve.is_valid()) {
985 // Set snapping settings based on the curve's meta.
986 snap_button->set_pressed(curve->get_meta("_snap_enabled", false));
987 snap_count_edit->set_value(curve->get_meta("_snap_count", DEFAULT_SNAP));
988 }
989 } break;
990 case NOTIFICATION_RESIZED:
991 curve_editor_rect->update_minimum_size();
992 break;
993 }
994}
995
996CurveEditor::CurveEditor() {
997 HFlowContainer *toolbar = memnew(HFlowContainer);
998 add_child(toolbar);
999
1000 snap_button = memnew(Button);
1001 snap_button->set_tooltip_text(TTR("Toggle Grid Snap"));
1002 snap_button->set_toggle_mode(true);
1003 toolbar->add_child(snap_button);
1004 snap_button->connect("toggled", callable_mp(this, &CurveEditor::_set_snap_enabled));
1005
1006 toolbar->add_child(memnew(VSeparator));
1007
1008 snap_count_edit = memnew(EditorSpinSlider);
1009 snap_count_edit->set_min(2);
1010 snap_count_edit->set_max(100);
1011 snap_count_edit->set_value(DEFAULT_SNAP);
1012 snap_count_edit->set_custom_minimum_size(Size2(65 * EDSCALE, 0));
1013 toolbar->add_child(snap_count_edit);
1014 snap_count_edit->connect("value_changed", callable_mp(this, &CurveEditor::_set_snap_count));
1015
1016 presets_button = memnew(MenuButton);
1017 presets_button->set_text(TTR("Presets"));
1018 presets_button->set_switch_on_hover(true);
1019 presets_button->set_h_size_flags(SIZE_EXPAND | SIZE_SHRINK_END);
1020 toolbar->add_child(presets_button);
1021 presets_button->get_popup()->connect("id_pressed", callable_mp(this, &CurveEditor::_on_preset_item_selected));
1022
1023 curve_editor_rect = memnew(CurveEdit);
1024 add_child(curve_editor_rect);
1025
1026 // Some empty space below. Not a part of the curve editor so it can't draw in it.
1027 Control *empty_space = memnew(Control);
1028 empty_space->set_custom_minimum_size(Vector2(0, spacing));
1029 add_child(empty_space);
1030
1031 set_mouse_filter(MOUSE_FILTER_STOP);
1032 _set_snap_enabled(snap_button->is_pressed());
1033 _set_snap_count(snap_count_edit->get_value());
1034}
1035
1036///////////////////////
1037
1038bool EditorInspectorPluginCurve::can_handle(Object *p_object) {
1039 return Object::cast_to<Curve>(p_object) != nullptr;
1040}
1041
1042void EditorInspectorPluginCurve::parse_begin(Object *p_object) {
1043 Curve *curve = Object::cast_to<Curve>(p_object);
1044 ERR_FAIL_NULL(curve);
1045 Ref<Curve> c(curve);
1046
1047 CurveEditor *editor = memnew(CurveEditor);
1048 editor->set_curve(c);
1049 add_custom_control(editor);
1050}
1051
1052CurveEditorPlugin::CurveEditorPlugin() {
1053 Ref<EditorInspectorPluginCurve> plugin;
1054 plugin.instantiate();
1055 add_inspector_plugin(plugin);
1056
1057 EditorInterface::get_singleton()->get_resource_previewer()->add_preview_generator(memnew(CurvePreviewGenerator));
1058}
1059
1060///////////////////////
1061
1062bool CurvePreviewGenerator::handles(const String &p_type) const {
1063 return p_type == "Curve";
1064}
1065
1066Ref<Texture2D> CurvePreviewGenerator::generate(const Ref<Resource> &p_from, const Size2 &p_size, Dictionary &p_metadata) const {
1067 Ref<Curve> curve = p_from;
1068 if (curve.is_null()) {
1069 return Ref<Texture2D>();
1070 }
1071
1072 Size2 thumbnail_size = p_size * EDSCALE;
1073 Ref<Image> img_ref;
1074 img_ref.instantiate();
1075 Image &im = **img_ref;
1076 im.initialize_data(thumbnail_size.x, thumbnail_size.y, false, Image::FORMAT_RGBA8);
1077
1078 Color bg_color(0.1, 0.1, 0.1, 1.0);
1079 Color line_color(0.8, 0.8, 0.8, 1.0);
1080
1081 im.fill(bg_color);
1082 // Set the first pixel of the thumbnail.
1083 float v = (curve->sample_baked(0) - curve->get_min_value()) / curve->get_range();
1084 int y = CLAMP(im.get_height() - v * im.get_height(), 0, im.get_height() - 1);
1085 im.set_pixel(0, y, line_color);
1086
1087 // Plot a line towards the next point.
1088 int prev_y = y;
1089 for (int x = 1; x < im.get_width(); ++x) {
1090 float t = static_cast<float>(x) / im.get_width();
1091 v = (curve->sample_baked(t) - curve->get_min_value()) / curve->get_range();
1092 y = CLAMP(im.get_height() - v * im.get_height(), 0, im.get_height() - 1);
1093
1094 Vector<Point2i> points = Geometry2D::bresenham_line(Point2i(x - 1, prev_y), Point2i(x, y));
1095 for (Point2i point : points) {
1096 im.set_pixelv(point, line_color);
1097 }
1098 prev_y = y;
1099 }
1100
1101 return ImageTexture::create_from_image(img_ref);
1102}
1103