diff --git a/doc/classes/Curve2D.xml b/doc/classes/Curve2D.xml index 6ce9616fb61..cf64bb1e26f 100644 --- a/doc/classes/Curve2D.xml +++ b/doc/classes/Curve2D.xml @@ -83,6 +83,19 @@ Deletes the point [param idx] from the curve. Sends an error to the console if [param idx] is out of bounds. + + + + All control points are reset, effectively removing all "smoothness." + + + + + + + Sets the control handles for the specified point to 0. + + @@ -149,6 +162,12 @@ Sets the position for the vertex [param idx]. If the index is out of bounds, the function sends an error to the console. + + + + Automatically interpolates control handles for all points, creating a smooth curve. + + diff --git a/editor/scene/2d/path_2d_editor_plugin.cpp b/editor/scene/2d/path_2d_editor_plugin.cpp index 53d0a7e1580..c12d6ba7aa0 100644 --- a/editor/scene/2d/path_2d_editor_plugin.cpp +++ b/editor/scene/2d/path_2d_editor_plugin.cpp @@ -40,6 +40,7 @@ #include "core/os/keyboard.h" #include "editor/editor_node.h" +#include "editor/editor_string_names.h" #include "editor/editor_undo_redo_manager.h" #include "editor/scene/canvas_item_editor_plugin.h" #include "editor/settings/editor_settings.h" @@ -56,6 +57,8 @@ void Path2DEditor::_notification(int p_what) { 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_smooth_points->set_button_icon(get_editor_theme_icon(SNAME("CurveInOut"))); + curve_reset_handles->set_button_icon(get_editor_theme_icon(SNAME("CurveLinear"))); create_curve_button->set_button_icon(get_editor_theme_icon(SNAME("Curve2D"))); } break; @@ -89,6 +92,14 @@ bool Path2DEditor::forward_gui_input(const Ref &p_event) { real_t grab_threshold = EDITOR_GET("editors/polygon_editor/point_grab_radius"); + Ref k = p_event; + if (k.is_valid() && k->is_pressed() && action == ACTION_NONE && !box_selecting && + !selected_points.is_empty() && + (k->get_keycode() == Key::KEY_DELETE || k->get_keycode() == Key::BACKSPACE)) { + _delete_selection(); + return true; + } + Ref mb = p_event; if (mb.is_valid()) { Transform2D xform = canvas_item_editor->get_canvas_transform() * node->get_screen_transform(); @@ -112,10 +123,28 @@ bool Path2DEditor::forward_gui_input(const Ref &p_event) { if (mode == MODE_EDIT && !mb->is_shift_pressed() && dist_to_p < grab_threshold) { // Points can only be moved in edit mode. + bool ctrl_or_cmd = mb->is_command_or_control_pressed(); + if (ctrl_or_cmd && selected_points.has(i)) { + // Ctrl-click on an already-selected point toggles it off. + selected_points.erase(i); + canvas_item_editor->update_viewport(); + return true; + } + if (!selected_points.has(i)) { + _select_point(i, ctrl_or_cmd, false); + } + action = ACTION_MOVING_POINT; action_point = i; moving_from = curve->get_point_position(i); moving_screen_from = gpoint; + + multi_move_start_positions.clear(); + if (selected_points.size() > 1) { + for (int sel : selected_points) { + multi_move_start_positions[sel] = curve->get_point_position(sel); + } + } return true; } else if (mode == MODE_EDIT || mode == MODE_EDIT_CURVE) { control_points_in_range = 0; @@ -149,6 +178,8 @@ bool Path2DEditor::forward_gui_input(const Ref &p_event) { undo_redo->create_action(TTR("Remove Point from Curve")); undo_redo->add_do_method(curve.ptr(), "remove_point", i); undo_redo->add_undo_method(curve.ptr(), "add_point", curve->get_point_position(i), curve->get_point_in(i), curve->get_point_out(i), i); + undo_redo->add_do_method(this, "_clear_point_selection"); + undo_redo->add_undo_method(this, "_clear_point_selection"); undo_redo->add_do_method(canvas_item_editor, "update_viewport"); undo_redo->add_undo_method(canvas_item_editor, "update_viewport"); undo_redo->commit_action(); @@ -172,6 +203,21 @@ bool Path2DEditor::forward_gui_input(const Ref &p_event) { } } } + + // Nothing was hit by the loop above — start box selection instead of + // falling through to CanvasItemEditor's node box-select. + if (mb->get_button_index() == MouseButton::LEFT && mode == MODE_EDIT && + !mb->is_command_or_control_pressed() && !on_edge) { + box_selecting = true; + box_select_from = gpoint; + box_select_to = gpoint; + box_select_additive = mb->is_shift_pressed(); + if (!box_select_additive) { + selected_points.clear(); + } + canvas_item_editor->update_viewport(); + return true; + } } if (action != ACTION_NONE && mb->is_pressed() && mb->get_button_index() == MouseButton::RIGHT) { @@ -180,7 +226,8 @@ bool Path2DEditor::forward_gui_input(const Ref &p_event) { } // Check for point creation. - if (mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT && ((mb->is_command_or_control_pressed() && mode == MODE_EDIT) || mode == MODE_CREATE)) { + if (mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT && ((mb->is_command_or_control_pressed() && mode == MODE_EDIT) || mode == MODE_CREATE) && + action == ACTION_NONE) { Ref curve = node->get_curve(); curve->add_point(cpoint); moving_from = cpoint; @@ -196,7 +243,8 @@ bool Path2DEditor::forward_gui_input(const Ref &p_event) { } // Check for segment split. - if (mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT && mode == MODE_EDIT && on_edge) { + if (mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT && mode == MODE_EDIT && on_edge && + action == ACTION_NONE) { Vector2 gpoint2 = mb->get_position(); Ref curve = node->get_curve(); @@ -241,13 +289,26 @@ bool Path2DEditor::forward_gui_input(const Ref &p_event) { case ACTION_MOVING_POINT: if (original_mouse_pos != gpoint) { - undo_redo->create_action(TTR("Move Point in Curve")); - undo_redo->add_undo_method(curve.ptr(), "set_point_position", action_point, moving_from); - undo_redo->add_do_method(curve.ptr(), "set_point_position", action_point, cpoint); - undo_redo->add_do_method(canvas_item_editor, "update_viewport"); - undo_redo->add_undo_method(canvas_item_editor, "update_viewport"); - undo_redo->commit_action(false); + if (multi_move_start_positions.size() > 1) { + Vector2 delta = cpoint - moving_from; + undo_redo->create_action(TTR("Move Points in Curve")); + for (const KeyValue &kv : multi_move_start_positions) { + undo_redo->add_do_method(curve.ptr(), "set_point_position", kv.key, kv.value + delta); + undo_redo->add_undo_method(curve.ptr(), "set_point_position", kv.key, kv.value); + } + undo_redo->add_do_method(canvas_item_editor, "update_viewport"); + undo_redo->add_undo_method(canvas_item_editor, "update_viewport"); + undo_redo->commit_action(false); + } else { + undo_redo->create_action(TTR("Move Point in Curve")); + undo_redo->add_undo_method(curve.ptr(), "set_point_position", action_point, moving_from); + undo_redo->add_do_method(curve.ptr(), "set_point_position", action_point, cpoint); + undo_redo->add_do_method(canvas_item_editor, "update_viewport"); + undo_redo->add_undo_method(canvas_item_editor, "update_viewport"); + undo_redo->commit_action(false); + } } + multi_move_start_positions.clear(); break; case ACTION_MOVING_NEW_POINT: { undo_redo->create_action(TTR("Add Point to Curve")); @@ -264,6 +325,10 @@ bool Path2DEditor::forward_gui_input(const Ref &p_event) { undo_redo->add_do_method(curve.ptr(), "add_point", Vector2(), Vector2(), Vector2(), action_point); undo_redo->add_do_method(curve.ptr(), "set_point_position", action_point, cpoint); undo_redo->add_undo_method(curve.ptr(), "remove_point", action_point); + undo_redo->add_do_method(this, "_clear_point_selection"); + undo_redo->add_undo_method(this, "_clear_point_selection"); + _clear_point_selection(); + canvas_item_editor->update_viewport(); undo_redo->add_do_method(canvas_item_editor, "update_viewport"); undo_redo->add_undo_method(canvas_item_editor, "update_viewport"); undo_redo->commit_action(false); @@ -306,11 +371,25 @@ bool Path2DEditor::forward_gui_input(const Ref &p_event) { return true; } + + // End of the drag selection box + if (!mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT && box_selecting) { + box_selecting = false; + _box_select_confirm(box_select_additive); + canvas_item_editor->update_viewport(); + return true; + } } Ref mm = p_event; if (mm.is_valid()) { + if (box_selecting) { + box_select_to = mm->get_position(); + canvas_item_editor->update_viewport(); + return true; + } + // When both control points were in range of click, // pick the point that drags the curve outwards. if (control_points_in_range == 2) { @@ -398,7 +477,14 @@ bool Path2DEditor::forward_gui_input(const Ref &p_event) { case ACTION_MOVING_POINT: case ACTION_MOVING_NEW_POINT: case ACTION_MOVING_NEW_POINT_FROM_SPLIT: { - curve->set_point_position(action_point, cpoint); + if (action == ACTION_MOVING_POINT && multi_move_start_positions.size() > 1) { + Vector2 delta = cpoint - moving_from; + for (const KeyValue &kv : multi_move_start_positions) { + curve->set_point_position(kv.key, kv.value + delta); + } + } else { + curve->set_point_position(action_point, cpoint); + } } break; case ACTION_MOVING_IN: { @@ -454,7 +540,9 @@ void Path2DEditor::forward_canvas_draw_over_viewport(Control *p_overlay) { debug_handle_lines.clear(); debug_handle_curve_transforms.clear(); debug_handle_sharp_transforms.clear(); + debug_handle_sharp_indices.clear(); debug_handle_smooth_transforms.clear(); + debug_handle_smooth_indices.clear(); Transform2D handle_curve_transform = Transform2D().scaled(curve_handle_size * 0.5); Transform2D handle_point_transform = Transform2D().scaled(handle_size * 0.5); @@ -489,8 +577,10 @@ void Path2DEditor::forward_canvas_draw_over_viewport(Control *p_overlay) { handle_point_transform.set_origin(point); if (smooth) { debug_handle_smooth_transforms.push_back(handle_point_transform); + debug_handle_smooth_indices.push_back(i); } else { debug_handle_sharp_transforms.push_back(handle_point_transform); + debug_handle_sharp_indices.push_back(i); } } @@ -523,26 +613,31 @@ void Path2DEditor::forward_canvas_draw_over_viewport(Control *p_overlay) { rs->multimesh_set_visible_instances(debug_handle_curve_multimesh_rid, 0); if (handle_curve_count > 0) { if (rs->multimesh_get_instance_count(debug_handle_curve_multimesh_rid) != int(handle_curve_count)) { - rs->multimesh_allocate_data(debug_handle_curve_multimesh_rid, handle_curve_count, RS::MULTIMESH_TRANSFORM_2D); + rs->multimesh_allocate_data(debug_handle_curve_multimesh_rid, handle_curve_count, RS::MULTIMESH_TRANSFORM_2D, true /* p_use_colors */); } Vector multimesh_buffer; - multimesh_buffer.resize(8 * handle_curve_count); + multimesh_buffer.resize(12 * handle_curve_count); float *multimesh_buffer_ptrw = multimesh_buffer.ptrw(); const Transform2D *debug_handle_transforms_ptr = debug_handle_curve_transforms.ptr(); + const Color control_point_color = Color(0.85, 0.65, 0.3, 0.85); // Muted amber; blue-yellow axis, low saturation. for (uint32_t i = 0; i < handle_curve_count; i++) { const Transform2D &handle_transform = debug_handle_transforms_ptr[i]; - multimesh_buffer_ptrw[i * 8 + 0] = handle_transform[0][0]; - multimesh_buffer_ptrw[i * 8 + 1] = handle_transform[1][0]; - multimesh_buffer_ptrw[i * 8 + 2] = 0; - multimesh_buffer_ptrw[i * 8 + 3] = handle_transform[2][0]; - multimesh_buffer_ptrw[i * 8 + 4] = handle_transform[0][1]; - multimesh_buffer_ptrw[i * 8 + 5] = handle_transform[1][1]; - multimesh_buffer_ptrw[i * 8 + 6] = 0; - multimesh_buffer_ptrw[i * 8 + 7] = handle_transform[2][1]; + multimesh_buffer_ptrw[i * 12 + 0] = handle_transform[0][0]; + multimesh_buffer_ptrw[i * 12 + 1] = handle_transform[1][0]; + multimesh_buffer_ptrw[i * 12 + 2] = 0; + multimesh_buffer_ptrw[i * 12 + 3] = handle_transform[2][0]; + multimesh_buffer_ptrw[i * 12 + 4] = handle_transform[0][1]; + multimesh_buffer_ptrw[i * 12 + 5] = handle_transform[1][1]; + multimesh_buffer_ptrw[i * 12 + 6] = 0; + multimesh_buffer_ptrw[i * 12 + 7] = handle_transform[2][1]; + multimesh_buffer_ptrw[i * 12 + 8] = control_point_color.r; + multimesh_buffer_ptrw[i * 12 + 9] = control_point_color.g; + multimesh_buffer_ptrw[i * 12 + 10] = control_point_color.b; + multimesh_buffer_ptrw[i * 12 + 11] = control_point_color.a; } rs->multimesh_set_buffer(debug_handle_curve_multimesh_rid, multimesh_buffer); @@ -556,26 +651,35 @@ void Path2DEditor::forward_canvas_draw_over_viewport(Control *p_overlay) { rs->multimesh_set_visible_instances(debug_handle_sharp_multimesh_rid, 0); if (handle_sharp_count > 0) { if (rs->multimesh_get_instance_count(debug_handle_sharp_multimesh_rid) != int(handle_sharp_count)) { - rs->multimesh_allocate_data(debug_handle_sharp_multimesh_rid, handle_sharp_count, RS::MULTIMESH_TRANSFORM_2D); + rs->multimesh_allocate_data(debug_handle_sharp_multimesh_rid, handle_sharp_count, RS::MULTIMESH_TRANSFORM_2D, true /* p_use_colors */); } Vector multimesh_buffer; - multimesh_buffer.resize(8 * handle_sharp_count); + multimesh_buffer.resize(12 * handle_sharp_count); // 8 transform floats + 4 color floats. float *multimesh_buffer_ptrw = multimesh_buffer.ptrw(); const Transform2D *debug_handle_transforms_ptr = debug_handle_sharp_transforms.ptr(); + const int *debug_handle_indices_ptr = debug_handle_sharp_indices.ptr(); + + const Color selected_color = Color(0.4, 1, 1); + const Color unselected_color = Color(1, 1, 1); for (uint32_t i = 0; i < handle_sharp_count; i++) { const Transform2D &handle_transform = debug_handle_transforms_ptr[i]; - - multimesh_buffer_ptrw[i * 8 + 0] = handle_transform[0][0]; - multimesh_buffer_ptrw[i * 8 + 1] = handle_transform[1][0]; - multimesh_buffer_ptrw[i * 8 + 2] = 0; - multimesh_buffer_ptrw[i * 8 + 3] = handle_transform[2][0]; - multimesh_buffer_ptrw[i * 8 + 4] = handle_transform[0][1]; - multimesh_buffer_ptrw[i * 8 + 5] = handle_transform[1][1]; - multimesh_buffer_ptrw[i * 8 + 6] = 0; - multimesh_buffer_ptrw[i * 8 + 7] = handle_transform[2][1]; + const Color &c = selected_points.has(debug_handle_indices_ptr[i]) ? selected_color : unselected_color; + + multimesh_buffer_ptrw[i * 12 + 0] = handle_transform[0][0]; + multimesh_buffer_ptrw[i * 12 + 1] = handle_transform[1][0]; + multimesh_buffer_ptrw[i * 12 + 2] = 0; + multimesh_buffer_ptrw[i * 12 + 3] = handle_transform[2][0]; + multimesh_buffer_ptrw[i * 12 + 4] = handle_transform[0][1]; + multimesh_buffer_ptrw[i * 12 + 5] = handle_transform[1][1]; + multimesh_buffer_ptrw[i * 12 + 6] = 0; + multimesh_buffer_ptrw[i * 12 + 7] = handle_transform[2][1]; + multimesh_buffer_ptrw[i * 12 + 8] = c.r; + multimesh_buffer_ptrw[i * 12 + 9] = c.g; + multimesh_buffer_ptrw[i * 12 + 10] = c.b; + multimesh_buffer_ptrw[i * 12 + 11] = c.a; } rs->multimesh_set_buffer(debug_handle_sharp_multimesh_rid, multimesh_buffer); @@ -589,26 +693,35 @@ void Path2DEditor::forward_canvas_draw_over_viewport(Control *p_overlay) { rs->multimesh_set_visible_instances(debug_handle_smooth_multimesh_rid, 0); if (handle_smooth_count > 0) { if (rs->multimesh_get_instance_count(debug_handle_smooth_multimesh_rid) != int(handle_smooth_count)) { - rs->multimesh_allocate_data(debug_handle_smooth_multimesh_rid, handle_smooth_count, RS::MULTIMESH_TRANSFORM_2D); + rs->multimesh_allocate_data(debug_handle_smooth_multimesh_rid, handle_smooth_count, RS::MULTIMESH_TRANSFORM_2D, true /* p_use_colors */); } Vector multimesh_buffer; - multimesh_buffer.resize(8 * handle_smooth_count); + multimesh_buffer.resize(12 * handle_smooth_count); float *multimesh_buffer_ptrw = multimesh_buffer.ptrw(); const Transform2D *debug_handle_transforms_ptr = debug_handle_smooth_transforms.ptr(); + const int *debug_handle_indices_ptr = debug_handle_smooth_indices.ptr(); + + const Color selected_color = Color(0.4, 1, 1); + const Color unselected_color = Color(1, 1, 1); for (uint32_t i = 0; i < handle_smooth_count; i++) { const Transform2D &handle_transform = debug_handle_transforms_ptr[i]; - - multimesh_buffer_ptrw[i * 8 + 0] = handle_transform[0][0]; - multimesh_buffer_ptrw[i * 8 + 1] = handle_transform[1][0]; - multimesh_buffer_ptrw[i * 8 + 2] = 0; - multimesh_buffer_ptrw[i * 8 + 3] = handle_transform[2][0]; - multimesh_buffer_ptrw[i * 8 + 4] = handle_transform[0][1]; - multimesh_buffer_ptrw[i * 8 + 5] = handle_transform[1][1]; - multimesh_buffer_ptrw[i * 8 + 6] = 0; - multimesh_buffer_ptrw[i * 8 + 7] = handle_transform[2][1]; + const Color &c = selected_points.has(debug_handle_indices_ptr[i]) ? selected_color : unselected_color; + + multimesh_buffer_ptrw[i * 12 + 0] = handle_transform[0][0]; + multimesh_buffer_ptrw[i * 12 + 1] = handle_transform[1][0]; + multimesh_buffer_ptrw[i * 12 + 2] = 0; + multimesh_buffer_ptrw[i * 12 + 3] = handle_transform[2][0]; + multimesh_buffer_ptrw[i * 12 + 4] = handle_transform[0][1]; + multimesh_buffer_ptrw[i * 12 + 5] = handle_transform[1][1]; + multimesh_buffer_ptrw[i * 12 + 6] = 0; + multimesh_buffer_ptrw[i * 12 + 7] = handle_transform[2][1]; + multimesh_buffer_ptrw[i * 12 + 8] = c.r; + multimesh_buffer_ptrw[i * 12 + 9] = c.g; + multimesh_buffer_ptrw[i * 12 + 10] = c.b; + multimesh_buffer_ptrw[i * 12 + 11] = c.a; } rs->multimesh_set_buffer(debug_handle_smooth_multimesh_rid, multimesh_buffer); @@ -616,6 +729,25 @@ void Path2DEditor::forward_canvas_draw_over_viewport(Control *p_overlay) { rs->canvas_item_add_multimesh(vpc->get_canvas_item(), debug_handle_smooth_multimesh_rid, curve_handle->get_rid()); } + + if (box_selecting) { + Point2 bsfrom = box_select_from; + Point2 bsto = box_select_to; + if (bsfrom.x > bsto.x) { + SWAP(bsfrom.x, bsto.x); + } + if (bsfrom.y > bsto.y) { + SWAP(bsfrom.y, bsto.y); + } + p_overlay->draw_rect( + Rect2(bsfrom, bsto - bsfrom), + get_theme_color(SNAME("box_selection_fill_color"), EditorStringName(Editor))); + p_overlay->draw_rect( + Rect2(bsfrom, bsto - bsfrom), + get_theme_color(SNAME("box_selection_stroke_color"), EditorStringName(Editor)), + false, + Math::round(EDSCALE)); + } } void Path2DEditor::_node_visibility_changed() { @@ -637,13 +769,14 @@ void Path2DEditor::_update_toolbar() { } void Path2DEditor::edit(Node *p_path2d) { - if (!canvas_item_editor) { - canvas_item_editor = CanvasItemEditor::get_singleton(); - } - if (action != ACTION_NONE) { _cancel_current_action(); } + _clear_selection(); + + if (!canvas_item_editor) { + canvas_item_editor = CanvasItemEditor::get_singleton(); + } if (p_path2d) { node = Object::cast_to(p_path2d); @@ -667,6 +800,7 @@ void Path2DEditor::_bind_methods() { ClassDB::bind_method(D_METHOD("_update_toolbar"), &Path2DEditor::_update_toolbar); ClassDB::bind_method(D_METHOD("_clear_curve_points"), &Path2DEditor::_clear_curve_points); ClassDB::bind_method(D_METHOD("_restore_curve_points"), &Path2DEditor::_restore_curve_points); + ClassDB::bind_method(D_METHOD("_clear_point_selection"), &Path2DEditor::_clear_point_selection); } void Path2DEditor::_mode_selected(int p_mode) { @@ -725,6 +859,42 @@ void Path2DEditor::_mode_selected(int p_mode) { undo_redo->create_action(TTR("Clear Curve Points"), UndoRedo::MERGE_DISABLE, node); undo_redo->add_do_method(this, "_clear_curve_points", node); undo_redo->add_undo_method(this, "_restore_curve_points", node, points); + undo_redo->add_do_method(this, "_clear_point_selection"); + undo_redo->add_undo_method(this, "_clear_point_selection"); + undo_redo->add_do_method(canvas_item_editor, "update_viewport"); + undo_redo->add_undo_method(canvas_item_editor, "update_viewport"); + undo_redo->commit_action(); + return; + } else if (p_mode == MODE_SMOOTH_ALL_POINTS) { + if (node->get_curve().is_null() || node->get_curve()->get_point_count() < 3) { + return; + } + Ref curve = node->get_curve(); + EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); + Dictionary before = curve->call("_get_data"); + curve->smooth_all_points(); + Dictionary after = curve->call("_get_data"); + + undo_redo->create_action(TTR("Smooth All Points")); + undo_redo->add_do_method(curve.ptr(), "_set_data", after); + undo_redo->add_undo_method(curve.ptr(), "_set_data", before); + undo_redo->add_do_method(canvas_item_editor, "update_viewport"); + undo_redo->add_undo_method(canvas_item_editor, "update_viewport"); + undo_redo->commit_action(); + return; + } else if (p_mode == MODE_RESET_ALL_HANDLES) { + if (node->get_curve().is_null() || node->get_curve()->get_point_count() == 0) { + return; + } + Ref curve = node->get_curve(); + EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); + Dictionary before = curve->call("_get_data"); + curve->reset_all_points_handles(); + Dictionary after = curve->call("_get_data"); + + undo_redo->create_action(TTR("Reset All Handles")); + undo_redo->add_do_method(curve.ptr(), "_set_data", after); + undo_redo->add_undo_method(curve.ptr(), "_set_data", before); undo_redo->add_do_method(canvas_item_editor, "update_viewport"); undo_redo->add_undo_method(canvas_item_editor, "update_viewport"); undo_redo->commit_action(); @@ -759,7 +929,13 @@ void Path2DEditor::_cancel_current_action() { switch (action) { case ACTION_MOVING_POINT: { - curve->set_point_position(action_point, moving_from); + if (multi_move_start_positions.size() > 1) { + for (const KeyValue &kv : multi_move_start_positions) { + curve->set_point_position(kv.key, kv.value); + } + } else { + curve->set_point_position(action_point, moving_from); + } } break; case ACTION_MOVING_NEW_POINT: { @@ -785,9 +961,102 @@ void Path2DEditor::_cancel_current_action() { } canvas_item_editor->update_viewport(); + multi_move_start_positions.clear(); action = ACTION_NONE; } +void Path2DEditor::_select_point(int p_idx, bool p_add_to_selection, bool p_toggle) { + if (!p_add_to_selection) { + selected_points.clear(); + } + if (p_toggle && selected_points.has(p_idx)) { + selected_points.erase(p_idx); + } else { + selected_points.insert(p_idx); + } + canvas_item_editor->update_viewport(); +} + +void Path2DEditor::_box_select_confirm(bool p_additive) { + Ref curve = node->get_curve(); + if (curve.is_null()) { + return; + } + Transform2D xform = canvas_item_editor->get_canvas_transform() * node->get_screen_transform(); + + // Normalize the rect: Rect2::has_point() requires a non-negative size. + Point2 bsfrom = box_select_from; + Point2 bsto = box_select_to; + if (bsfrom.x > bsto.x) { + SWAP(bsfrom.x, bsto.x); + } + if (bsfrom.y > bsto.y) { + SWAP(bsfrom.y, bsto.y); + } + Rect2 rect(bsfrom, bsto - bsfrom); + + if (!p_additive) { + selected_points.clear(); + } + for (int i = 0; i < curve->get_point_count(); i++) { + Vector2 screen_pos = xform.xform(curve->get_point_position(i)); + if (rect.has_point(screen_pos)) { + selected_points.insert(i); + } + } + canvas_item_editor->update_viewport(); +} + +void Path2DEditor::_delete_selection() { + if (selected_points.is_empty()) { + return; + } + + Ref curve = node->get_curve(); + EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); + + // Sort ascending once; iterate it both ways. + Vector indices; + for (int idx : selected_points) { + indices.push_back(idx); + } + indices.sort(); + + undo_redo->create_action(TTR("Delete Points from Curve")); + + // DO: remove from highest index to lowest, so earlier removals + // don't shift the indices of points not yet removed. + for (int i = indices.size() - 1; i >= 0; i--) { + undo_redo->add_do_method(curve.ptr(), "remove_point", indices[i]); + } + + // UNDO: add back from lowest index to highest. UndoRedo runs undo_ops + // in the SAME order they were added (not reversed), so this order + // must independently reconstruct the original point list correctly, + // which is to say, the lowest missing index first. + for (int i = 0; i < indices.size(); i++) { + int idx = indices[i]; + undo_redo->add_undo_method(curve.ptr(), "add_point", + curve->get_point_position(idx), curve->get_point_in(idx), curve->get_point_out(idx), idx); + } + + undo_redo->add_do_method(this, "_clear_point_selection"); + undo_redo->add_undo_method(this, "_clear_point_selection"); + undo_redo->add_do_method(canvas_item_editor, "update_viewport"); + undo_redo->add_undo_method(canvas_item_editor, "update_viewport"); + undo_redo->commit_action(); +} + +void Path2DEditor::_clear_selection() { + selected_points.clear(); + box_selecting = false; + multi_move_start_positions.clear(); +} + +void Path2DEditor::_clear_point_selection() { + selected_points.clear(); +} + void Path2DEditor::_create_curve() { ERR_FAIL_NULL(node); @@ -857,7 +1126,10 @@ Path2DEditor::Path2DEditor() { curve_edit->set_toggle_mode(true); curve_edit->set_pressed(true); curve_edit->set_focus_mode(Control::FOCUS_ACCESSIBILITY); - curve_edit->set_tooltip_text(TTR("Select Points") + "\n" + TTR("Shift+Drag: Select Control Points") + "\n" + keycode_get_string((Key)KeyModifierMask::CMD_OR_CTRL) + TTR("Click: Add Point") + "\n" + TTR("Left Click: Split Segment (in curve)") + "\n" + TTR("Right Click: Delete Point")); + curve_edit->set_tooltip_text(TTR("Select Points") + "\n" + TTR("Click: Select single point | (In Curve): Split Segment") + "\n" + + vformat(TTR("%sClick: Add Point | (On Point): Add/remove point from selection"), keycode_get_string((Key)KeyModifierMask::CMD_OR_CTRL)) + "\n" + + TTR("Drag on empty space: Box-select points") + "\n" + TTR("Shift+Drag: Add to box selection") + "\n" + + TTR("Delete/Backspace: Delete selected points") + "\n" + TTR("Right Click: Delete Point")); curve_edit->set_accessibility_name(TTRC("Select Points")); curve_edit->connect(SceneStringName(pressed), callable_mp(this, &Path2DEditor::_mode_selected).bind(MODE_EDIT)); toolbar->add_child(curve_edit); @@ -902,6 +1174,22 @@ Path2DEditor::Path2DEditor() { curve_clear_points->connect(SceneStringName(pressed), callable_mp(this, &Path2DEditor::_confirm_clear_points)); toolbar->add_child(curve_clear_points); + curve_smooth_points = memnew(Button); + curve_smooth_points->set_theme_type_variation(SceneStringName(FlatButton)); + curve_smooth_points->set_focus_mode(Control::FOCUS_ACCESSIBILITY); + curve_smooth_points->set_tooltip_text(TTR("Smooth All Points")); + curve_smooth_points->set_accessibility_name(TTRC("Smooth All Points")); + curve_smooth_points->connect(SceneStringName(pressed), callable_mp(this, &Path2DEditor::_mode_selected).bind(MODE_SMOOTH_ALL_POINTS)); + toolbar->add_child(curve_smooth_points); + + curve_reset_handles = memnew(Button); + curve_reset_handles->set_theme_type_variation(SceneStringName(FlatButton)); + curve_reset_handles->set_focus_mode(Control::FOCUS_ACCESSIBILITY); + curve_reset_handles->set_tooltip_text(TTR("Reset All Handles")); + curve_reset_handles->set_accessibility_name(TTRC("Reset All Handles")); + curve_reset_handles->connect(SceneStringName(pressed), callable_mp(this, &Path2DEditor::_mode_selected).bind(MODE_RESET_ALL_HANDLES)); + toolbar->add_child(curve_reset_handles); + clear_points_dialog = memnew(ConfirmationDialog); clear_points_dialog->set_title(TTR("Please Confirm...")); clear_points_dialog->set_text(TTR("Remove all curve points?")); diff --git a/editor/scene/2d/path_2d_editor_plugin.h b/editor/scene/2d/path_2d_editor_plugin.h index a679050f486..fa81f5ca43b 100644 --- a/editor/scene/2d/path_2d_editor_plugin.h +++ b/editor/scene/2d/path_2d_editor_plugin.h @@ -62,11 +62,15 @@ class Path2DEditor : public HBoxContainer { MODE_DELETE, MODE_CLOSE, MODE_CLEAR_POINTS, + MODE_SMOOTH_ALL_POINTS, + MODE_RESET_ALL_HANDLES, }; Mode mode = MODE_EDIT; HBoxContainer *toolbar = nullptr; Button *curve_clear_points = nullptr; + Button *curve_smooth_points = nullptr; + Button *curve_reset_handles = nullptr; Button *curve_close = nullptr; Button *curve_create = nullptr; Button *curve_del = nullptr; @@ -108,6 +112,23 @@ class Path2DEditor : public HBoxContainer { /// 0, 1, or 2. int control_points_in_range = 0; + /// @name Multi-selection state + /// @{ + HashSet selected_points; + HashMap multi_move_start_positions; ///< Snapshot of positions at drag-start, for group-move. + + bool box_selecting = false; + bool box_select_additive = false; ///< Track when holding shift to add to the selection instead of replacing it. + Point2 box_select_from; + Point2 box_select_to; + + void _select_point(int p_idx, bool p_add_to_selection, bool p_toggle); + void _box_select_confirm(bool p_additive); + void _delete_selection(); + void _clear_selection(); + void _clear_point_selection(); + /// @} + void _mode_selected(int p_mode); void _handle_option_pressed(int p_option); void _cancel_current_action(); @@ -131,7 +152,9 @@ class Path2DEditor : public HBoxContainer { LocalVector debug_handle_lines; LocalVector debug_handle_curve_transforms; LocalVector debug_handle_sharp_transforms; + LocalVector debug_handle_sharp_indices; LocalVector debug_handle_smooth_transforms; + LocalVector debug_handle_smooth_indices; protected: void _notification(int p_what); diff --git a/scene/resources/curve.cpp b/scene/resources/curve.cpp index cecde9a0eb9..2e119f3f861 100644 --- a/scene/resources/curve.cpp +++ b/scene/resources/curve.cpp @@ -797,6 +797,41 @@ Vector2 Curve2D::get_point_out(int p_index) const { return points[p_index].out; } +void Curve2D::reset_point_handles(int p_index) { + ERR_FAIL_INDEX(p_index, (int)points.size()); + set_point_in(p_index, Vector2()); + set_point_out(p_index, Vector2()); +} + +void Curve2D::reset_all_points_handles() { + int pc = points.size(); + for (int i = 0; i < pc; i++) { + reset_point_handles(i); // reuses the single-point reset instead of duplicating logic + } +} + +void Curve2D::smooth_all_points() { + int pc = points.size(); + if (pc < 3) { + return; // nothing to smooth with fewer than 3 points + } + + for (int i = 0; i < pc; i++) { + if (i == 0 || i == pc - 1) { + // Endpoints: leave sharp (no previous/next neighbor on both sides). + set_point_in(i, Vector2()); + set_point_out(i, Vector2()); + continue; + } + Vector2 prev = get_point_position(i - 1); + Vector2 next = get_point_position(i + 1); + // Catmull-Rom -> Bezier tangent conversion (1/6 chord factor). + Vector2 tangent = (next - prev) / 6.0; + set_point_in(i, -tangent); + set_point_out(i, tangent); + } +} + void Curve2D::_remove_point(int p_index) { ERR_FAIL_UNSIGNED_INDEX((uint32_t)p_index, points.size()); points.remove_at(p_index); @@ -1415,6 +1450,9 @@ void Curve2D::_bind_methods() { ClassDB::bind_method(D_METHOD("get_point_in", "idx"), &Curve2D::get_point_in); ClassDB::bind_method(D_METHOD("set_point_out", "idx", "position"), &Curve2D::set_point_out); ClassDB::bind_method(D_METHOD("get_point_out", "idx"), &Curve2D::get_point_out); + ClassDB::bind_method(D_METHOD("reset_point_handles", "idx"), &Curve2D::reset_point_handles); + ClassDB::bind_method(D_METHOD("reset_all_points_handles"), &Curve2D::reset_all_points_handles); + ClassDB::bind_method(D_METHOD("smooth_all_points"), &Curve2D::smooth_all_points); ClassDB::bind_method(D_METHOD("remove_point", "idx"), &Curve2D::remove_point); ClassDB::bind_method(D_METHOD("clear_points"), &Curve2D::clear_points); ClassDB::bind_method(D_METHOD("sample", "idx", "t"), &Curve2D::sample); @@ -2488,6 +2526,7 @@ void Curve3D::_bind_methods() { ClassDB::bind_method(D_METHOD("get_point_in", "idx"), &Curve3D::get_point_in); ClassDB::bind_method(D_METHOD("set_point_out", "idx", "position"), &Curve3D::set_point_out); ClassDB::bind_method(D_METHOD("get_point_out", "idx"), &Curve3D::get_point_out); + ClassDB::bind_method(D_METHOD("remove_point", "idx"), &Curve3D::remove_point); ClassDB::bind_method(D_METHOD("clear_points"), &Curve3D::clear_points); ClassDB::bind_method(D_METHOD("sample", "idx", "t"), &Curve3D::sample); diff --git a/scene/resources/curve.h b/scene/resources/curve.h index 43a802a4f1f..6fcf0ab090a 100644 --- a/scene/resources/curve.h +++ b/scene/resources/curve.h @@ -241,6 +241,11 @@ class Curve2D : public Resource { Vector2 get_point_in(int p_index) const; void set_point_out(int p_index, const Vector2 &p_out); Vector2 get_point_out(int p_index) const; + + void reset_point_handles(int p_index); ///< wraps existing "zero a single point" logic + void reset_all_points_handles(); ///< the "opposite" bulk operation + void smooth_all_points(); ///< the auto-smooth bulk operation + void remove_point(int p_index); void clear_points();