From 6daaea47805731c9145d97afecd657bbfdf043ba Mon Sep 17 00:00:00 2001 From: robert yevdokimov Date: Sun, 13 Jul 2025 17:47:26 +0400 Subject: [PATCH] Create a rotation arc showing accumulated rotation when using transform gizmo --- editor/scene/3d/node_3d_editor_plugin.cpp | 163 ++++++++++++++++++++-- editor/scene/3d/node_3d_editor_plugin.h | 8 ++ 2 files changed, 157 insertions(+), 14 deletions(-) diff --git a/editor/scene/3d/node_3d_editor_plugin.cpp b/editor/scene/3d/node_3d_editor_plugin.cpp index 55505477cde..8cfab30435a 100644 --- a/editor/scene/3d/node_3d_editor_plugin.cpp +++ b/editor/scene/3d/node_3d_editor_plugin.cpp @@ -537,6 +537,10 @@ void Node3DEditorViewport::_update_navigation_controls_visibility() { look_control->set_visible(show_viewport_navigation_gizmo); } +bool Node3DEditorViewport::_is_rotation_arc_visible() const { + return _edit.mode == TRANSFORM_ROTATE && _edit.accumulated_rotation_angle != 0.0 && _edit.gizmo_initiated; +} + void Node3DEditorViewport::_update_camera(real_t p_interp_delta) { bool is_orthogonal = camera->get_projection() == Camera3D::PROJECTION_ORTHOGONAL; @@ -1416,6 +1420,9 @@ bool Node3DEditorViewport::_transform_gizmo_select(const Vector2 &p_screenpos, b _edit.mode = TRANSFORM_ROTATE; _compute_edit(p_screenpos); _edit.plane = TransformPlane(TRANSFORM_X_AXIS + col_axis); + _edit.accumulated_rotation_angle = 0.0; + _edit.rotation_axis = gt.basis.get_column(col_axis).normalized(); + _edit.gizmo_initiated = true; } return true; } @@ -3471,7 +3478,7 @@ void Node3DEditorViewport::_draw() { font->draw_string(ci, msgpos, message, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size, Color(1, 1, 1, 1)); } - if (_edit.mode == TRANSFORM_ROTATE && _edit.show_rotation_line) { + if (_edit.mode == TRANSFORM_ROTATE) { Point2 center = point_to_screen(_edit.center); Color handle_color; @@ -3489,14 +3496,110 @@ void Node3DEditorViewport::_draw() { handle_color = get_theme_color(SNAME("accent_color"), EditorStringName(Editor)); break; } - handle_color = handle_color.from_hsv(handle_color.get_h(), 0.25, 1.0, 1); - RenderingServer::get_singleton()->canvas_item_add_line( - ci, - _edit.mouse_pos, - center, - handle_color, - Math::round(2 * EDSCALE)); + if (_is_rotation_arc_visible() && !_edit.initial_click_vector.is_zero_approx()) { + Vector3 up = _edit.rotation_axis; + Vector3 right = _edit.initial_click_vector; + + right = right - up * up.dot(right); + right.normalize(); + Vector3 forward = up.cross(right); + + const int circle_segments = 64; + Vector circle_points; + for (int i = 0; i <= circle_segments; i++) { + float angle = (float(i) / float(circle_segments)) * Math::TAU; + Vector3 point_3d = _edit.center + gizmo_scale * GIZMO_CIRCLE_SIZE * (right * Math::cos(angle) + forward * Math::sin(angle)); + Point2 point_2d = point_to_screen(point_3d); + circle_points.push_back(point_2d); + } + + Color circle_color = handle_color.from_hsv(handle_color.get_h(), 0.6, 1.0, 0.8); + for (int i = 0; i < circle_points.size() - 1; i++) { + RenderingServer::get_singleton()->canvas_item_add_line( + ci, + circle_points[i], + circle_points[i + 1], + circle_color, + Math::round(2 * EDSCALE)); + } + + const int segments = 64; + float display_angle = _edit.display_rotation_angle; + + float abs_angle = Math::abs(display_angle); + if (abs_angle > Math::TAU) { + float remainder = Math::fmod((double)abs_angle, Math::TAU); + remainder = remainder < 0.01 ? Math::TAU : remainder; + display_angle = SIGN(display_angle) * remainder; + abs_angle = remainder; + } + + int num_segments = MAX(8, int(abs_angle / (Math::TAU / segments) * segments)); + num_segments = MIN(num_segments, segments); + + Color fill_color = Color(1.0, 1.0, 1.0, 0.2); + + bool is_counterclockwise = display_angle > 0; + float start_angle = is_counterclockwise ? 0.0f : display_angle; + float end_angle = is_counterclockwise ? display_angle : 0.0f; + + for (int i = 0; i < num_segments; i++) { + float t1 = float(i) / float(num_segments); + float t2 = float(i + 1) / float(num_segments); + float angle1 = Math::lerp(start_angle, end_angle, t1); + float angle2 = Math::lerp(start_angle, end_angle, t2); + + Vector3 point1_3d = _edit.center + gizmo_scale * GIZMO_CIRCLE_SIZE * (right * Math::cos(angle1) + forward * Math::sin(angle1)); + Vector3 point2_3d = _edit.center + gizmo_scale * GIZMO_CIRCLE_SIZE * (right * Math::cos(angle2) + forward * Math::sin(angle2)); + + Point2 center_2d = center; + Point2 point1_2d = point_to_screen(point1_3d); + Point2 point2_2d = point_to_screen(point2_3d); + + Vector triangle_points; + triangle_points.push_back(center_2d); + triangle_points.push_back(point1_2d); + triangle_points.push_back(point2_2d); + + Vector triangle_colors; + triangle_colors.push_back(fill_color); + triangle_colors.push_back(fill_color); + triangle_colors.push_back(fill_color); + + RenderingServer::get_singleton()->canvas_item_add_polygon(ci, triangle_points, triangle_colors); + } + + Color edge_color = handle_color.from_hsv(handle_color.get_h(), 0.8, 1.0, 0.7); + + Vector3 start_point_3d = _edit.center + gizmo_scale * GIZMO_CIRCLE_SIZE * right; + Point2 start_point_2d = point_to_screen(start_point_3d); + RenderingServer::get_singleton()->canvas_item_add_line( + ci, + center, + start_point_2d, + edge_color, + Math::round(2 * EDSCALE)); + + Vector3 end_point_3d = _edit.center + gizmo_scale * GIZMO_CIRCLE_SIZE * (right * Math::cos(display_angle) + forward * Math::sin(display_angle)); + Point2 end_point_2d = point_to_screen(end_point_3d); + RenderingServer::get_singleton()->canvas_item_add_line( + ci, + center, + end_point_2d, + edge_color, + Math::round(2 * EDSCALE)); + } + + if (_edit.show_rotation_line) { + handle_color = handle_color.from_hsv(handle_color.get_h(), 0.25, 1.0, 1); + RenderingServer::get_singleton()->canvas_item_add_line( + ci, + _edit.mouse_pos, + center, + handle_color, + Math::round(2 * EDSCALE)); + } } if (previewing) { Size2 ss = Size2(GLOBAL_GET("display/window/size/viewport_width"), GLOBAL_GET("display/window/size/viewport_height")); @@ -4213,7 +4316,9 @@ void Node3DEditorViewport::update_transform_gizmo_view() { return; } - bool show_gizmo = spatial_editor->is_gizmo_visible() && !_edit.instant && transform_gizmo_visible && !collision_reposition; + bool hide_during_rotation = _is_rotation_arc_visible(); + + bool show_gizmo = spatial_editor->is_gizmo_visible() && !_edit.instant && transform_gizmo_visible && !collision_reposition && !hide_during_rotation; for (int i = 0; i < 3; i++) { Transform3D axis_angle; if (xform.basis.get_column(i).normalized().dot(xform.basis.get_column((i + 1) % 3).normalized()) < 1.0) { @@ -4226,7 +4331,8 @@ void Node3DEditorViewport::update_transform_gizmo_view() { RenderingServer::get_singleton()->instance_set_transform(move_plane_gizmo_instance[i], axis_angle); RenderingServer::get_singleton()->instance_set_visible(move_plane_gizmo_instance[i], show_gizmo && (spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_SELECT || spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_MOVE)); RenderingServer::get_singleton()->instance_set_transform(rotate_gizmo_instance[i], axis_angle); - RenderingServer::get_singleton()->instance_set_visible(rotate_gizmo_instance[i], show_gizmo && (spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_SELECT || spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_ROTATE)); + bool show_rotate_gizmo = show_gizmo && (spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_SELECT || spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_ROTATE); + RenderingServer::get_singleton()->instance_set_visible(rotate_gizmo_instance[i], show_rotate_gizmo); RenderingServer::get_singleton()->instance_set_transform(scale_gizmo_instance[i], axis_angle); RenderingServer::get_singleton()->instance_set_visible(scale_gizmo_instance[i], show_gizmo && (spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_SCALE)); RenderingServer::get_singleton()->instance_set_transform(scale_plane_gizmo_instance[i], axis_angle); @@ -4244,7 +4350,8 @@ void Node3DEditorViewport::update_transform_gizmo_view() { xform.orthonormalize(); xform.basis.scale(scale); RenderingServer::get_singleton()->instance_set_transform(rotate_gizmo_instance[3], xform); - RenderingServer::get_singleton()->instance_set_visible(rotate_gizmo_instance[3], spatial_editor->is_gizmo_visible() && !_edit.instant && transform_gizmo_visible && !collision_reposition && (spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_SELECT || spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_ROTATE)); + bool show_white_outline = spatial_editor->is_gizmo_visible() && !_edit.instant && transform_gizmo_visible && !collision_reposition && (spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_SELECT || spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_ROTATE); + RenderingServer::get_singleton()->instance_set_visible(rotate_gizmo_instance[3], show_white_outline && !hide_during_rotation); } void Node3DEditorViewport::set_state(const Dictionary &p_state) { @@ -5165,6 +5272,11 @@ void Node3DEditorViewport::begin_transform(TransformMode p_mode, bool instant) { _compute_edit(_edit.mouse_pos); _edit.instant = instant; _edit.snap = spatial_editor->is_snap_enabled(); + _edit.initial_click_vector = Vector3(); + _edit.previous_rotation_vector = Vector3(); + _edit.accumulated_rotation_angle = 0.0; + _edit.display_rotation_angle = 0.0; + _edit.gizmo_initiated = false; update_transform_gizmo_view(); set_process_input(instant); } @@ -5464,6 +5576,15 @@ void Node3DEditorViewport::update_transform(bool p_shift) { break; } + Vector3 current_rotation_vector = (intersection - _edit.center).normalized(); + + if (_edit.initial_click_vector == Vector3()) { + _edit.initial_click_vector = (click - _edit.center).normalized(); + _edit.previous_rotation_vector = current_rotation_vector; + _edit.accumulated_rotation_angle = 0.0; + _edit.display_rotation_angle = 0.0; + } + static const float orthogonal_threshold = Math::cos(Math::deg_to_rad(85.0f)); bool axis_is_orthogonal = Math::abs(plane.normal.dot(global_axis)) < orthogonal_threshold; @@ -5477,12 +5598,21 @@ void Node3DEditorViewport::update_transform(bool p_shift) { } else { _edit.show_rotation_line = true; Vector3 click_axis = (click - _edit.center).normalized(); - Vector3 current_axis = (intersection - _edit.center).normalized(); - angle = click_axis.signed_angle_to(current_axis, global_axis); + angle = click_axis.signed_angle_to(current_rotation_vector, global_axis); } - if (_edit.snap || spatial_editor->is_snap_enabled()) { + if (_edit.previous_rotation_vector != Vector3()) { + double delta_angle = _edit.previous_rotation_vector.signed_angle_to(current_rotation_vector, global_axis); + _edit.accumulated_rotation_angle += delta_angle; + } + _edit.previous_rotation_vector = current_rotation_vector; + + bool snapping = _edit.snap || spatial_editor->is_snap_enabled(); + if (snapping) { snap = spatial_editor->get_rotate_snap(); + _edit.display_rotation_angle = Math::deg_to_rad(Math::snapped(Math::rad_to_deg(_edit.accumulated_rotation_angle), snap)); + } else { + _edit.display_rotation_angle = _edit.accumulated_rotation_angle; } angle = Math::snapped(Math::rad_to_deg(angle), snap); set_message(vformat(TTR("Rotating %s degrees."), String::num(angle, snap_step_decimals))); @@ -5568,6 +5698,11 @@ void Node3DEditorViewport::finish_transform() { _edit.numeric_input = 0; _edit.numeric_next_decimal = 0; _edit.numeric_negate = false; + _edit.initial_click_vector = Vector3(); + _edit.previous_rotation_vector = Vector3(); + _edit.accumulated_rotation_angle = 0.0; + _edit.display_rotation_angle = 0.0; + _edit.gizmo_initiated = false; spatial_editor->set_local_coords_enabled(_edit.original_local); spatial_editor->update_transform_gizmo(); surface->queue_redraw(); diff --git a/editor/scene/3d/node_3d_editor_plugin.h b/editor/scene/3d/node_3d_editor_plugin.h index 28e7c750ca4..62fec45d2b1 100644 --- a/editor/scene/3d/node_3d_editor_plugin.h +++ b/editor/scene/3d/node_3d_editor_plugin.h @@ -296,6 +296,7 @@ private: void _update_name(); void _compute_edit(const Point2 &p_point); void _clear_selected(); + bool _is_rotation_arc_visible() const; void _select_clicked(bool p_allow_locked); ObjectID _select_ray(const Point2 &p_pos) const; void _find_items_at_pos(const Point2 &p_pos, Vector<_RayResult> &r_results, bool p_include_locked); @@ -390,6 +391,13 @@ private: double numeric_input = 0.0; bool numeric_negate = false; int numeric_next_decimal = 0; + + Vector3 rotation_axis; + double accumulated_rotation_angle = 0.0; + double display_rotation_angle = 0.0; + Vector3 initial_click_vector; + Vector3 previous_rotation_vector; + bool gizmo_initiated = false; } _edit; struct Cursor {