Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
9f67b3b
wip curve smoothing
Christakou Feb 12, 2026
d157a06
Wip curve smoothing pt2
Christakou Feb 16, 2026
1bb3ce1
Add smooth curve button and implement curve smoothing functionality
Christakou Feb 16, 2026
ef74e1f
Use cat-mull rom smoothing to make it depend only on the point positions
Christakou Feb 16, 2026
50d2408
Fix some issues handling closed curves
Christakou Feb 16, 2026
f6c8bfd
Stop multiplying by smoothing_ratio twice and adjust handling of clos…
Christakou Feb 16, 2026
3c3c347
smooth_ratio looks better at 0.33
Christakou Feb 16, 2026
2b223ce
Make the in and out points depend on distance to next and previous po…
Christakou Feb 17, 2026
d107ecc
Reset point_in and point_out when curve is closed
Christakou Feb 17, 2026
90c521b
Remove outdated comment
Christakou Feb 17, 2026
424eec7
Update smooth_ratio to 0.5 for improved curve smoothing
Christakou Feb 18, 2026
981e82a
Implement smoothing in Path2D (wip)
Christakou Feb 19, 2026
da5b313
Fix next_p and prev_p being swapped and improve handling for closed p…
Christakou Feb 21, 2026
7d07970
Rename to auto-tangent and add icon svg
Christakou Feb 21, 2026
228dc90
Rename smooth points button to auto tangent and update tooltip
Christakou Feb 21, 2026
ccfe43c
Address PR suggestions and rework closed curve logic in 3D to align w…
Christakou Feb 21, 2026
1957239
Clean up left over comments
Christakou Feb 21, 2026
19a3c02
Add torsion spinbox for autosmooth button
Christakou Mar 26, 2026
2e6772a
Implement auto-tangent and torsion controls for path2d
Christakou Mar 26, 2026
925a566
Add spinbox declaration in header file
Christakou Mar 26, 2026
c59bb47
Apply suggestion from @TokageItLab
Christakou Mar 27, 2026
7fceb78
Refactor autotangent to unify logic and support application of smooth…
Christakou Apr 4, 2026
b97904c
Make autotangent a toggle like delete toggle
Christakou May 15, 2026
4936f9a
Implement auto tangent toggle button for Path2D
Christakou May 17, 2026
9fe92fd
Tidy up logic and fix toggle bug
Christakou May 17, 2026
2dd1971
Refactor and lint
Christakou May 17, 2026
93da14d
Fix toggle buttons bug
Christakou May 17, 2026
1a9be50
Move clear points and apply autotangent to all to options dropdown
Christakou May 17, 2026
c9202d4
Apply clang format
Christakou May 17, 2026
95f44fc
Early return if path or curve are null
Christakou May 17, 2026
b2138f8
Early return if path or curve are null
Christakou May 17, 2026
df83fa4
Remove unused include
Christakou May 17, 2026
6892add
Rearrange toolbar to use EditorSpinSlider for auto-tangent torsion an…
Christakou Jun 1, 2026
aef6af5
Add shortcuts for applying auto-tangent
Christakou Jun 1, 2026
4cd0fcd
Merge branch 'master' into add_curve_smooth_button
Christakou Jun 1, 2026
1bbb1c9
Add forward declarations for EditorSpinSlider and VSeparator
Christakou Jun 1, 2026
f30b9cd
Add missing includes
Christakou Jun 1, 2026
6f6481a
Remove unecessary include
Christakou Jun 1, 2026
6b13960
Remove another unecessary include, (missed by local prek?)
Christakou Jun 1, 2026
c4653a4
Remove duplicate connection
Christakou Jun 1, 2026
4ba1c2f
Update editor/scene/2d/path_2d_editor_plugin.cpp
Christakou Jun 2, 2026
ba06b8c
Update editor/scene/3d/path_3d_editor_plugin.cpp
Christakou Jun 2, 2026
1961ed0
Update editor/scene/2d/path_2d_editor_plugin.cpp
Christakou Jun 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions editor/icons/CurveAutoTangent.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
141 changes: 131 additions & 10 deletions editor/scene/2d/path_2d_editor_plugin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,13 @@
#include "core/os/keyboard.h"
#include "editor/editor_node.h"
#include "editor/editor_undo_redo_manager.h"
#include "editor/gui/editor_spin_slider.h"
#include "editor/scene/canvas_item_editor_plugin.h"
#include "editor/settings/editor_settings.h"
#include "editor/themes/editor_scale.h"
#include "scene/gui/dialogs.h"
#include "scene/gui/menu_button.h"
#include "scene/gui/separator.h"
#include "scene/resources/mesh.h"
#include "servers/rendering/rendering_server.h"

Expand All @@ -50,8 +53,7 @@ void Path2DEditor::_notification(int p_what) {
curve_create->set_button_icon(get_editor_theme_icon(SNAME("CurveCreate")));
curve_del->set_button_icon(get_editor_theme_icon(SNAME("CurveDelete")));
curve_close->set_button_icon(get_editor_theme_icon(SNAME("CurveClose")));
curve_clear_points->set_button_icon(get_editor_theme_icon(SNAME("Clear")));

curve_auto_tangent_toggle->set_button_icon(get_editor_theme_icon(SNAME("CurveAutoTangent")));
create_curve_button->set_button_icon(get_editor_theme_icon(SNAME("Curve2D")));
} break;
}
Expand Down Expand Up @@ -136,9 +138,16 @@ bool Path2DEditor::forward_gui_input(const Ref<InputEvent> &p_event) {
}
}
}
EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
// Check for auto tangent click.
if ((mb->get_button_index() == MouseButton::LEFT && mode == MODE_AUTO_TANGENT) && dist_to_p < grab_threshold) {
undo_redo->create_action(TTR("Auto Tangent Point"));
_auto_tangent_point(i);
undo_redo->commit_action();
return true;
}

// Check for point deletion.
EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
if ((mb->get_button_index() == MouseButton::RIGHT && (mode == MODE_EDIT || mode == MODE_CREATE)) || (mb->get_button_index() == MouseButton::LEFT && mode == MODE_DELETE)) {
if (dist_to_p < grab_threshold) {
undo_redo->create_action(TTR("Remove Point from Curve"));
Expand Down Expand Up @@ -670,21 +679,31 @@ void Path2DEditor::_mode_selected(int p_mode) {
curve_edit->set_pressed(false);
curve_edit_curve->set_pressed(false);
curve_del->set_pressed(false);
curve_auto_tangent_toggle->set_pressed(false);
} else if (p_mode == MODE_EDIT) {
curve_create->set_pressed(false);
curve_edit->set_pressed(true);
curve_edit_curve->set_pressed(false);
curve_del->set_pressed(false);
curve_auto_tangent_toggle->set_pressed(false);
} else if (p_mode == MODE_EDIT_CURVE) {
curve_create->set_pressed(false);
curve_edit->set_pressed(false);
curve_edit_curve->set_pressed(true);
curve_del->set_pressed(false);
curve_auto_tangent_toggle->set_pressed(false);
Comment thread
Christakou marked this conversation as resolved.
} else if (p_mode == MODE_DELETE) {
curve_create->set_pressed(false);
curve_edit->set_pressed(false);
curve_edit_curve->set_pressed(false);
curve_del->set_pressed(true);
curve_auto_tangent_toggle->set_pressed(false);
} else if (p_mode == MODE_AUTO_TANGENT) {
curve_edit->set_pressed(false);
curve_edit_curve->set_pressed(false);
curve_del->set_pressed(false);
curve_create->set_pressed(false);
curve_auto_tangent_toggle->set_pressed(true);
} else if (p_mode == MODE_CLOSE) {
if (node->get_curve().is_null()) {
return;
Expand Down Expand Up @@ -744,6 +763,12 @@ void Path2DEditor::_handle_option_pressed(int p_option) {
mirror_handle_length = !is_checked;
pm->set_item_checked(HANDLE_OPTION_LENGTH, mirror_handle_length);
} break;
case HANDLE_OPTION_AUTO_TANGENT: {
_auto_tangent();
} break;
case HANDLE_OPTION_CLEAR_POINTS: {
_confirm_clear_points();
} break;
}
}

Expand Down Expand Up @@ -809,6 +834,84 @@ void Path2DEditor::_confirm_clear_points() {
clear_points_dialog->popup_centered();
}

void Path2DEditor::_auto_tangent_point(int p_index) {
if (!node || node->get_curve().is_null() || node->get_curve()->get_point_count() == 0) {
return;
}
const Ref<Curve2D> curve = node->get_curve();
int point_count = curve->get_point_count();

EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
Vector2 begin = node->get_curve()->get_point_position(0);
Vector2 end = node->get_curve()->get_point_position(node->get_curve()->get_point_count() - 1);
bool is_closed = begin.is_equal_approx(end);
const float smooth_ratio = auto_tangent_torsion->get_value();
const bool has_prev = is_closed || p_index > 0;
const bool has_next = is_closed || p_index < point_count - 1;
if (!has_prev && !has_next) {
return; // Single point curve is noop
}
Vector2 curr_p = curve->get_point_position(p_index);

if (has_prev && has_next) {
if (p_index == point_count - 1 || p_index == 0) {
Vector2 next_p = curve->get_point_position(1);
Vector2 prev_p = curve->get_point_position(point_count - 2);
Vector2 first_last_tangent = (next_p - prev_p).normalized();

undo_redo->add_undo_method(curve.ptr(), "set_point_in", point_count - 1, curve->get_point_in(point_count - 1));
undo_redo->add_do_method(curve.ptr(), "set_point_in", point_count - 1, -first_last_tangent * end.distance_to(prev_p) * smooth_ratio);

undo_redo->add_undo_method(curve.ptr(), "set_point_out", 0, curve->get_point_out(0));
undo_redo->add_do_method(curve.ptr(), "set_point_out", 0, first_last_tangent * begin.distance_to(next_p) * smooth_ratio);
} else {
int prev_p_index = is_closed ? (p_index - 1 + point_count) % point_count : p_index - 1;
int next_p_index = is_closed ? (p_index + 1) % point_count : p_index + 1;

Vector2 prev_p = curve->get_point_position(prev_p_index);
Vector2 next_p = curve->get_point_position(next_p_index);
Vector2 tangent = (next_p - prev_p).normalized();
undo_redo->add_undo_method(curve.ptr(), "set_point_in", p_index, curve->get_point_in(p_index));
undo_redo->add_do_method(curve.ptr(), "set_point_in", p_index, -tangent * curr_p.distance_to(prev_p) * smooth_ratio);
undo_redo->add_undo_method(curve.ptr(), "set_point_out", p_index, curve->get_point_out(p_index));
undo_redo->add_do_method(curve.ptr(), "set_point_out", p_index, tangent * curr_p.distance_to(next_p) * smooth_ratio);
}
} else if (has_next) {
Vector2 next_p = curve->get_point_position(p_index + 1);
Vector2 tangent = (next_p - curr_p).normalized();
undo_redo->add_undo_method(curve.ptr(), "set_point_in", p_index, curve->get_point_in(p_index));
undo_redo->add_do_method(curve.ptr(), "set_point_in", p_index, Vector2());
undo_redo->add_undo_method(curve.ptr(), "set_point_out", p_index, curve->get_point_out(p_index));
undo_redo->add_do_method(curve.ptr(), "set_point_out", p_index, tangent * curr_p.distance_to(next_p) * smooth_ratio);
} else {
Vector2 prev_p = curve->get_point_position(p_index - 1);
Vector2 tangent = (curr_p - prev_p).normalized();
undo_redo->add_undo_method(curve.ptr(), "set_point_in", p_index, curve->get_point_in(p_index));
undo_redo->add_do_method(curve.ptr(), "set_point_in", p_index, -tangent * curr_p.distance_to(prev_p) * smooth_ratio);
undo_redo->add_undo_method(curve.ptr(), "set_point_out", p_index, curve->get_point_out(p_index));
undo_redo->add_do_method(curve.ptr(), "set_point_out", p_index, Vector2());
}
}

void Path2DEditor::_auto_tangent() {
if (!node || node->get_curve().is_null() || node->get_curve()->get_point_count() <= 2) {
return;
}
EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
Ref<Curve2D> curve = node->get_curve();
undo_redo->create_action(TTR("Auto Tangent"));
int point_count = curve->get_point_count();

Vector2 begin = curve->get_point_position(0);
Vector2 end = curve->get_point_position(point_count - 1);
bool is_closed = begin.is_equal_approx(end);
int end_index = is_closed ? point_count - 1 : point_count;
for (int i = 0; i < end_index; i++) {
_auto_tangent_point(i);
}
undo_redo->commit_action();
}

void Path2DEditor::_clear_curve_points(Path2D *p_path2d) {
if (!p_path2d || p_path2d->get_curve().is_null()) {
return;
Expand Down Expand Up @@ -883,20 +986,35 @@ Path2DEditor::Path2DEditor() {
curve_del->connect(SceneStringName(pressed), callable_mp(this, &Path2DEditor::_mode_selected).bind(MODE_DELETE));
toolbar->add_child(curve_del);

curve_auto_tangent_toggle = memnew(Button);
curve_auto_tangent_toggle->set_theme_type_variation(SceneStringName(FlatButton));
curve_auto_tangent_toggle->set_toggle_mode(true);
curve_auto_tangent_toggle->set_focus_mode(Control::FOCUS_ACCESSIBILITY);
curve_auto_tangent_toggle->set_tooltip_text(TTR("Toggle Auto Tangent Mode"));
curve_auto_tangent_toggle->connect(SceneStringName(pressed), callable_mp(this, &Path2DEditor::_mode_selected).bind(MODE_AUTO_TANGENT));
toolbar->add_child(curve_auto_tangent_toggle);

auto_tangent_torsion = memnew(EditorSpinSlider);
auto_tangent_torsion->set_min(0.0);
auto_tangent_torsion->set_max(1.0);
auto_tangent_torsion->set_step(0.05);
auto_tangent_torsion->set_h_size_flags(Control::SIZE_EXPAND);
auto_tangent_torsion->set_custom_minimum_size(Size2(65 * EDSCALE, 0));
Comment thread
AThousandShips marked this conversation as resolved.
auto_tangent_torsion->set_focus_mode(Control::FOCUS_ACCESSIBILITY);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to produce warnings

  WARNING: C:\godot_source\scene/gui/control.cpp:3000 - This control can grab focus only when screen reader is active. Use set_focus_mode() and set_focus_behavior_recursive() to allow a control to get focus. Use get_tree().is_accessibility_enabled() to check screen-reader state.

auto_tangent_torsion->set_tooltip_text(TTRC("Auto Tangent Torsion"));
toolbar->add_child(auto_tangent_torsion);
auto_tangent_torsion->set_value(0.5);

v_separator = memnew(VSeparator);
toolbar->add_child(v_separator);

curve_close = memnew(Button);
curve_close->set_theme_type_variation(SceneStringName(FlatButton));
curve_close->set_focus_mode(Control::FOCUS_ACCESSIBILITY);
curve_close->set_tooltip_text(TTR("Close Curve"));
curve_close->connect(SceneStringName(pressed), callable_mp(this, &Path2DEditor::_mode_selected).bind(MODE_CLOSE));
toolbar->add_child(curve_close);

curve_clear_points = memnew(Button);
curve_clear_points->set_theme_type_variation(SceneStringName(FlatButton));
curve_clear_points->set_focus_mode(Control::FOCUS_ACCESSIBILITY);
curve_clear_points->set_tooltip_text(TTR("Clear Points"));
curve_clear_points->connect(SceneStringName(pressed), callable_mp(this, &Path2DEditor::_confirm_clear_points));
toolbar->add_child(curve_clear_points);

clear_points_dialog = memnew(ConfirmationDialog);
clear_points_dialog->set_title(TTR("Please Confirm..."));
clear_points_dialog->set_text(TTR("Remove all curve points?"));
Expand All @@ -914,6 +1032,9 @@ Path2DEditor::Path2DEditor() {
menu->set_item_checked(HANDLE_OPTION_ANGLE, mirror_handle_angle);
menu->add_check_item(TTR("Mirror Handle Lengths"));
menu->set_item_checked(HANDLE_OPTION_LENGTH, mirror_handle_length);
menu->add_separator();
menu->add_shortcut(ED_SHORTCUT("path_editor/apply_auto_tangent_to_all_points", TTRC("Apply Auto Tangent to All Points"), KeyModifierMask::CMD_OR_CTRL | Key::T), HANDLE_OPTION_AUTO_TANGENT);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I forgot that this shortcut is supposed to be used in two editors. You can't use ED_SHORTCUT this way, because it defines a shortcut. While it does work, defining a shortcut in multiple places can lead to inconsistency.

You should use ED_GET_SHORTCUT("path_editor/apply_auto_tangent_to_all_points") instead. For now you can leave ED_SHORTCUT in the Path3DEditor, because it's registered first, but this is rather fragile, so it should be moved to a better place eventually (not sure where yet).

menu->add_shortcut(ED_SHORTCUT("path_editor/clear_points", TTRC("Clear All Points")), HANDLE_OPTION_CLEAR_POINTS);
menu->connect(SceneStringName(id_pressed), callable_mp(this, &Path2DEditor::_handle_option_pressed));

add_child(toolbar);
Expand Down
12 changes: 11 additions & 1 deletion editor/scene/2d/path_2d_editor_plugin.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
class CanvasItemEditor;
class ConfirmationDialog;
class MenuButton;
class EditorSpinSlider;
class VSeparator;

class Path2DEditor : public HBoxContainer {
GDCLASS(Path2DEditor, HBoxContainer);
Expand All @@ -54,16 +56,19 @@ class Path2DEditor : public HBoxContainer {
MODE_DELETE,
MODE_CLOSE,
MODE_CLEAR_POINTS,
MODE_AUTO_TANGENT,
};

Mode mode = MODE_EDIT;
HBoxContainer *toolbar = nullptr;
Button *curve_clear_points = nullptr;
Button *curve_close = nullptr;
Button *curve_create = nullptr;
Button *curve_del = nullptr;
Button *curve_edit = nullptr;
Button *curve_edit_curve = nullptr;
Button *curve_auto_tangent_toggle = nullptr;
VSeparator *v_separator = nullptr;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The v_separator can be a local variable.

EditorSpinSlider *auto_tangent_torsion = nullptr;
MenuButton *handle_menu = nullptr;

Button *create_curve_button = nullptr;
Expand All @@ -76,6 +81,8 @@ class Path2DEditor : public HBoxContainer {
enum HandleOption {
HANDLE_OPTION_ANGLE,
HANDLE_OPTION_LENGTH,
HANDLE_OPTION_AUTO_TANGENT,
HANDLE_OPTION_CLEAR_POINTS,
};

enum Action {
Expand Down Expand Up @@ -107,8 +114,11 @@ class Path2DEditor : public HBoxContainer {
void _node_visibility_changed();
void _update_toolbar();

void _auto_tangent();

void _create_curve();
void _confirm_clear_points();
void _auto_tangent_point(int p_index);
void _clear_curve_points(Path2D *p_path2d);
void _restore_curve_points(Path2D *p_path2d, const PackedVector2Array &p_points);

Expand Down
Loading
Loading