Skip to content

Commit d739700

Browse files
committed
Implement orbit snapping in 3D viewport
1 parent cb7cd81 commit d739700

File tree

4 files changed

+88
-17
lines changed

4 files changed

+88
-17
lines changed

doc/classes/EditorSettings.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,9 @@
407407
<member name="editors/3d/navigation/zoom_style" type="int" setter="" getter="">
408408
The mouse cursor movement direction to use when zooming by moving the mouse. This does not affect zooming with the mouse wheel.
409409
</member>
410+
<member name="editors/3d/navigation_feel/angle_snap_threshold" type="float" setter="" getter="">
411+
The angle threshold for snapping camera rotation to 45-degree angles while orbiting with [kbd]Alt[/kbd] held.
412+
</member>
410413
<member name="editors/3d/navigation_feel/orbit_inertia" type="float" setter="" getter="">
411414
The inertia to use when orbiting in the 3D editor. Higher values make the camera start and stop slower, which looks smoother but adds latency.
412415
</member>

editor/scene/3d/node_3d_editor_plugin.cpp

Lines changed: 81 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1213,7 +1213,7 @@ void Node3DEditorViewport::_update_name() {
12131213
} break;
12141214
}
12151215

1216-
if (auto_orthogonal) {
1216+
if (orthogonal && auto_orthogonal) {
12171217
// TRANSLATORS: This will be appended to the view name when Auto Orthogonal is enabled.
12181218
name += " " + TTR("[auto]");
12191219
}
@@ -2510,27 +2510,32 @@ void Node3DEditorViewport::_sinput(const Ref<InputEvent> &p_event) {
25102510
if (ED_IS_SHORTCUT("spatial_editor/orbit_view_down", p_event)) {
25112511
// Clamp rotation to roughly -90..90 degrees so the user can't look upside-down and end up disoriented.
25122512
cursor.x_rot = CLAMP(cursor.x_rot - Math::PI / 12.0, -1.57, 1.57);
2513+
cursor.unsnapped_x_rot = cursor.x_rot;
25132514
view_type = VIEW_TYPE_USER;
25142515
_update_name();
25152516
}
25162517
if (ED_IS_SHORTCUT("spatial_editor/orbit_view_up", p_event)) {
25172518
// Clamp rotation to roughly -90..90 degrees so the user can't look upside-down and end up disoriented.
25182519
cursor.x_rot = CLAMP(cursor.x_rot + Math::PI / 12.0, -1.57, 1.57);
2520+
cursor.unsnapped_x_rot = cursor.x_rot;
25192521
view_type = VIEW_TYPE_USER;
25202522
_update_name();
25212523
}
25222524
if (ED_IS_SHORTCUT("spatial_editor/orbit_view_right", p_event)) {
25232525
cursor.y_rot -= Math::PI / 12.0;
2526+
cursor.unsnapped_y_rot = cursor.y_rot;
25242527
view_type = VIEW_TYPE_USER;
25252528
_update_name();
25262529
}
25272530
if (ED_IS_SHORTCUT("spatial_editor/orbit_view_left", p_event)) {
25282531
cursor.y_rot += Math::PI / 12.0;
2532+
cursor.unsnapped_y_rot = cursor.y_rot;
25292533
view_type = VIEW_TYPE_USER;
25302534
_update_name();
25312535
}
25322536
if (ED_IS_SHORTCUT("spatial_editor/orbit_view_180", p_event)) {
25332537
cursor.y_rot += Math::PI;
2538+
cursor.unsnapped_y_rot = cursor.y_rot;
25342539
view_type = VIEW_TYPE_USER;
25352540
_update_name();
25362541
}
@@ -2726,30 +2731,69 @@ void Node3DEditorViewport::_nav_orbit(Ref<InputEventWithModifiers> p_event, cons
27262731
return;
27272732
}
27282733

2729-
if (orthogonal && auto_orthogonal) {
2730-
_menu_option(VIEW_PERSPECTIVE);
2731-
}
2732-
27332734
const real_t degrees_per_pixel = EDITOR_GET("editors/3d/navigation_feel/orbit_sensitivity");
27342735
const real_t radians_per_pixel = Math::deg_to_rad(degrees_per_pixel);
27352736
const bool invert_y_axis = EDITOR_GET("editors/3d/navigation/invert_y_axis");
27362737
const bool invert_x_axis = EDITOR_GET("editors/3d/navigation/invert_x_axis");
27372738

2738-
if (invert_y_axis) {
2739-
cursor.x_rot -= p_relative.y * radians_per_pixel;
2740-
} else {
2741-
cursor.x_rot += p_relative.y * radians_per_pixel;
2742-
}
2743-
// Clamp the Y rotation to roughly -90..90 degrees so the user can't look upside-down and end up disoriented.
2744-
cursor.x_rot = CLAMP(cursor.x_rot, -1.57, 1.57);
2739+
cursor.unsnapped_x_rot += p_relative.y * radians_per_pixel * (invert_y_axis ? -1 : 1);
2740+
cursor.unsnapped_x_rot = CLAMP(cursor.unsnapped_x_rot, -1.57, 1.57);
2741+
cursor.unsnapped_y_rot += p_relative.x * radians_per_pixel * (invert_x_axis ? -1 : 1);
27452742

2746-
if (invert_x_axis) {
2747-
cursor.y_rot -= p_relative.x * radians_per_pixel;
2748-
} else {
2749-
cursor.y_rot += p_relative.x * radians_per_pixel;
2743+
cursor.x_rot = cursor.unsnapped_x_rot;
2744+
cursor.y_rot = cursor.unsnapped_y_rot;
2745+
2746+
if (_is_nav_modifier_pressed("spatial_editor/viewport_orbit_snap_modifier_1") &&
2747+
_is_nav_modifier_pressed("spatial_editor/viewport_orbit_snap_modifier_2")) {
2748+
const real_t snap_angle = Math::deg_to_rad(45.0);
2749+
const real_t snap_threshold = Math::deg_to_rad((real_t)EDITOR_GET("editors/3d/navigation_feel/angle_snap_threshold"));
2750+
2751+
real_t x_rot_snapped = Math::snapped(cursor.unsnapped_x_rot, snap_angle);
2752+
real_t y_rot_snapped = Math::snapped(cursor.unsnapped_y_rot, snap_angle);
2753+
2754+
real_t x_dist = Math::abs(cursor.unsnapped_x_rot - x_rot_snapped);
2755+
real_t y_dist = Math::abs(cursor.unsnapped_y_rot - y_rot_snapped);
2756+
2757+
if (x_dist < snap_threshold && y_dist < snap_threshold) {
2758+
cursor.x_rot = x_rot_snapped;
2759+
cursor.y_rot = y_rot_snapped;
2760+
2761+
real_t y_rot_wrapped = Math::wrapf(y_rot_snapped, (real_t)-Math::PI, (real_t)Math::PI);
2762+
2763+
if (Math::abs(x_rot_snapped) < snap_threshold) {
2764+
if (Math::abs(y_rot_wrapped) < snap_threshold) {
2765+
view_type = VIEW_TYPE_FRONT;
2766+
} else if (Math::abs(Math::abs(y_rot_wrapped) - Math::PI) < snap_threshold) {
2767+
view_type = VIEW_TYPE_REAR;
2768+
} else if (Math::abs(y_rot_wrapped - Math::PI / 2.0) < snap_threshold) {
2769+
view_type = VIEW_TYPE_LEFT;
2770+
} else if (Math::abs(y_rot_wrapped + Math::PI / 2.0) < snap_threshold) {
2771+
view_type = VIEW_TYPE_RIGHT;
2772+
} else {
2773+
// Only switch to ortho for 90-degree views.
2774+
return;
2775+
}
2776+
_set_auto_orthogonal();
2777+
_update_name();
2778+
} else if (Math::abs(Math::abs(x_rot_snapped) - Math::PI / 2.0) < snap_threshold) {
2779+
if (Math::abs(y_rot_wrapped) < snap_threshold ||
2780+
Math::abs(Math::abs(y_rot_wrapped) - Math::PI) < snap_threshold ||
2781+
Math::abs(y_rot_wrapped - Math::PI / 2.0) < snap_threshold ||
2782+
Math::abs(y_rot_wrapped + Math::PI / 2.0) < snap_threshold) {
2783+
view_type = x_rot_snapped > 0 ? VIEW_TYPE_TOP : VIEW_TYPE_BOTTOM;
2784+
_set_auto_orthogonal();
2785+
_update_name();
2786+
}
2787+
}
2788+
2789+
return;
2790+
}
27502791
}
2792+
27512793
view_type = VIEW_TYPE_USER;
2752-
_update_name();
2794+
if (orthogonal && auto_orthogonal) {
2795+
_menu_option(VIEW_PERSPECTIVE);
2796+
}
27532797
}
27542798

27552799
void Node3DEditorViewport::_nav_look(Ref<InputEventWithModifiers> p_event, const Vector2 &p_relative) {
@@ -2777,8 +2821,10 @@ void Node3DEditorViewport::_nav_look(Ref<InputEventWithModifiers> p_event, const
27772821
}
27782822
// Clamp the Y rotation to roughly -90..90 degrees so the user can't look upside-down and end up disoriented.
27792823
cursor.x_rot = CLAMP(cursor.x_rot, -1.57, 1.57);
2824+
cursor.unsnapped_x_rot = cursor.x_rot;
27802825

27812826
cursor.y_rot += p_relative.x * radians_per_pixel;
2827+
cursor.unsnapped_y_rot = cursor.y_rot;
27822828

27832829
// Look is like the opposite of Orbit: the focus point rotates around the camera
27842830
Transform3D camera_transform = to_camera_transform(cursor);
@@ -3763,6 +3809,8 @@ void Node3DEditorViewport::_apply_camera_transform_to_cursor() {
37633809

37643810
cursor.x_rot = -camera_transform.basis.get_euler().x;
37653811
cursor.y_rot = -camera_transform.basis.get_euler().y;
3812+
cursor.unsnapped_x_rot = cursor.x_rot;
3813+
cursor.unsnapped_y_rot = cursor.y_rot;
37663814
}
37673815

37683816
void Node3DEditorViewport::_menu_option(int p_option) {
@@ -3771,6 +3819,8 @@ void Node3DEditorViewport::_menu_option(int p_option) {
37713819
case VIEW_TOP: {
37723820
cursor.y_rot = 0;
37733821
cursor.x_rot = Math::PI / 2.0;
3822+
cursor.unsnapped_y_rot = cursor.y_rot;
3823+
cursor.unsnapped_x_rot = cursor.x_rot;
37743824
set_message(TTR("Top View."), 2);
37753825
view_type = VIEW_TYPE_TOP;
37763826
_set_auto_orthogonal();
@@ -3780,6 +3830,8 @@ void Node3DEditorViewport::_menu_option(int p_option) {
37803830
case VIEW_BOTTOM: {
37813831
cursor.y_rot = 0;
37823832
cursor.x_rot = -Math::PI / 2.0;
3833+
cursor.unsnapped_y_rot = cursor.y_rot;
3834+
cursor.unsnapped_x_rot = cursor.x_rot;
37833835
set_message(TTR("Bottom View."), 2);
37843836
view_type = VIEW_TYPE_BOTTOM;
37853837
_set_auto_orthogonal();
@@ -3789,6 +3841,8 @@ void Node3DEditorViewport::_menu_option(int p_option) {
37893841
case VIEW_LEFT: {
37903842
cursor.x_rot = 0;
37913843
cursor.y_rot = Math::PI / 2.0;
3844+
cursor.unsnapped_x_rot = cursor.x_rot;
3845+
cursor.unsnapped_y_rot = cursor.y_rot;
37923846
set_message(TTR("Left View."), 2);
37933847
view_type = VIEW_TYPE_LEFT;
37943848
_set_auto_orthogonal();
@@ -3798,6 +3852,8 @@ void Node3DEditorViewport::_menu_option(int p_option) {
37983852
case VIEW_RIGHT: {
37993853
cursor.x_rot = 0;
38003854
cursor.y_rot = -Math::PI / 2.0;
3855+
cursor.unsnapped_x_rot = cursor.x_rot;
3856+
cursor.unsnapped_y_rot = cursor.y_rot;
38013857
set_message(TTR("Right View."), 2);
38023858
view_type = VIEW_TYPE_RIGHT;
38033859
_set_auto_orthogonal();
@@ -3807,6 +3863,8 @@ void Node3DEditorViewport::_menu_option(int p_option) {
38073863
case VIEW_FRONT: {
38083864
cursor.x_rot = 0;
38093865
cursor.y_rot = 0;
3866+
cursor.unsnapped_x_rot = cursor.x_rot;
3867+
cursor.unsnapped_y_rot = cursor.y_rot;
38103868
set_message(TTR("Front View."), 2);
38113869
view_type = VIEW_TYPE_FRONT;
38123870
_set_auto_orthogonal();
@@ -3816,6 +3874,8 @@ void Node3DEditorViewport::_menu_option(int p_option) {
38163874
case VIEW_REAR: {
38173875
cursor.x_rot = 0;
38183876
cursor.y_rot = Math::PI;
3877+
cursor.unsnapped_x_rot = cursor.x_rot;
3878+
cursor.unsnapped_y_rot = cursor.y_rot;
38193879
set_message(TTR("Rear View."), 2);
38203880
view_type = VIEW_TYPE_REAR;
38213881
_set_auto_orthogonal();
@@ -4445,9 +4505,11 @@ void Node3DEditorViewport::set_state(const Dictionary &p_state) {
44454505
}
44464506
if (p_state.has("x_rotation")) {
44474507
cursor.x_rot = p_state["x_rotation"];
4508+
cursor.unsnapped_x_rot = cursor.x_rot;
44484509
}
44494510
if (p_state.has("y_rotation")) {
44504511
cursor.y_rot = p_state["y_rotation"];
4512+
cursor.unsnapped_y_rot = cursor.y_rot;
44514513
}
44524514
if (p_state.has("distance")) {
44534515
cursor.distance = p_state["distance"];
@@ -6016,6 +6078,8 @@ Node3DEditorViewport::Node3DEditorViewport(Node3DEditor *p_spatial_editor, int p
60166078
// Registering with Key::NONE intentionally creates an empty Array.
60176079
register_shortcut_action("spatial_editor/viewport_orbit_modifier_1", TTRC("Viewport Orbit Modifier 1"), Key::NONE);
60186080
register_shortcut_action("spatial_editor/viewport_orbit_modifier_2", TTRC("Viewport Orbit Modifier 2"), Key::NONE);
6081+
register_shortcut_action("spatial_editor/viewport_orbit_snap_modifier_1", TTRC("Viewport Orbit Snap Modifier 1"), Key::ALT);
6082+
register_shortcut_action("spatial_editor/viewport_orbit_snap_modifier_2", TTRC("Viewport Orbit Snap Modifier 2"), Key::NONE);
60196083
register_shortcut_action("spatial_editor/viewport_pan_modifier_1", TTRC("Viewport Pan Modifier 1"), Key::SHIFT);
60206084
register_shortcut_action("spatial_editor/viewport_pan_modifier_2", TTRC("Viewport Pan Modifier 2"), Key::NONE);
60216085
register_shortcut_action("spatial_editor/viewport_zoom_modifier_1", TTRC("Viewport Zoom Modifier 1"), Key::SHIFT);

editor/scene/3d/node_3d_editor_plugin.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,7 @@ class Node3DEditorViewport : public Control {
403403
struct Cursor {
404404
Vector3 pos;
405405
real_t x_rot, y_rot, distance, fov_scale;
406+
real_t unsnapped_x_rot, unsnapped_y_rot;
406407
Vector3 eye_pos; // Used in freelook mode
407408
bool region_select;
408409
Point2 region_begin, region_end;
@@ -411,6 +412,8 @@ class Node3DEditorViewport : public Control {
411412
// These rotations place the camera in +X +Y +Z, aka south east, facing north west.
412413
x_rot = 0.5;
413414
y_rot = -0.5;
415+
unsnapped_x_rot = x_rot;
416+
unsnapped_y_rot = y_rot;
414417
distance = 4;
415418
fov_scale = 1.0;
416419
region_select = false;

editor/settings/editor_settings.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -936,6 +936,7 @@ void EditorSettings::_load_defaults(Ref<ConfigFile> p_extra_config) {
936936
EDITOR_SETTING(Variant::FLOAT, PROPERTY_HINT_RANGE, "editors/3d/navigation_feel/orbit_inertia", 0.0, "0,1,0.001")
937937
EDITOR_SETTING(Variant::FLOAT, PROPERTY_HINT_RANGE, "editors/3d/navigation_feel/translation_inertia", 0.05, "0,1,0.001")
938938
EDITOR_SETTING(Variant::FLOAT, PROPERTY_HINT_RANGE, "editors/3d/navigation_feel/zoom_inertia", 0.05, "0,1,0.001")
939+
EDITOR_SETTING(Variant::FLOAT, PROPERTY_HINT_RANGE, "editors/3d/navigation_feel/angle_snap_threshold", 10.0, "1,20,0.1,degrees")
939940
_initial_set("editors/3d/navigation/show_viewport_rotation_gizmo", true);
940941
_initial_set("editors/3d/navigation/show_viewport_navigation_gizmo", DisplayServer::get_singleton()->is_touchscreen_available());
941942

0 commit comments

Comments
 (0)