Skip to content

Commit 94818a5

Browse files
committed
Merge pull request #111509 from passivestar/orbit-snapping
Implement orbit snapping in 3D viewport
2 parents 026efbb + d739700 commit 94818a5

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
}
@@ -2541,27 +2541,32 @@ void Node3DEditorViewport::_sinput(const Ref<InputEvent> &p_event) {
25412541
if (ED_IS_SHORTCUT("spatial_editor/orbit_view_down", p_event)) {
25422542
// Clamp rotation to roughly -90..90 degrees so the user can't look upside-down and end up disoriented.
25432543
cursor.x_rot = CLAMP(cursor.x_rot - Math::PI / 12.0, -1.57, 1.57);
2544+
cursor.unsnapped_x_rot = cursor.x_rot;
25442545
view_type = VIEW_TYPE_USER;
25452546
_update_name();
25462547
}
25472548
if (ED_IS_SHORTCUT("spatial_editor/orbit_view_up", p_event)) {
25482549
// Clamp rotation to roughly -90..90 degrees so the user can't look upside-down and end up disoriented.
25492550
cursor.x_rot = CLAMP(cursor.x_rot + Math::PI / 12.0, -1.57, 1.57);
2551+
cursor.unsnapped_x_rot = cursor.x_rot;
25502552
view_type = VIEW_TYPE_USER;
25512553
_update_name();
25522554
}
25532555
if (ED_IS_SHORTCUT("spatial_editor/orbit_view_right", p_event)) {
25542556
cursor.y_rot -= Math::PI / 12.0;
2557+
cursor.unsnapped_y_rot = cursor.y_rot;
25552558
view_type = VIEW_TYPE_USER;
25562559
_update_name();
25572560
}
25582561
if (ED_IS_SHORTCUT("spatial_editor/orbit_view_left", p_event)) {
25592562
cursor.y_rot += Math::PI / 12.0;
2563+
cursor.unsnapped_y_rot = cursor.y_rot;
25602564
view_type = VIEW_TYPE_USER;
25612565
_update_name();
25622566
}
25632567
if (ED_IS_SHORTCUT("spatial_editor/orbit_view_180", p_event)) {
25642568
cursor.y_rot += Math::PI;
2569+
cursor.unsnapped_y_rot = cursor.y_rot;
25652570
view_type = VIEW_TYPE_USER;
25662571
_update_name();
25672572
}
@@ -2766,30 +2771,69 @@ void Node3DEditorViewport::_nav_orbit(Ref<InputEventWithModifiers> p_event, cons
27662771
return;
27672772
}
27682773

2769-
if (orthogonal && auto_orthogonal) {
2770-
_menu_option(VIEW_PERSPECTIVE);
2771-
}
2772-
27732774
const real_t degrees_per_pixel = EDITOR_GET("editors/3d/navigation_feel/orbit_sensitivity");
27742775
const real_t radians_per_pixel = Math::deg_to_rad(degrees_per_pixel);
27752776
const bool invert_y_axis = EDITOR_GET("editors/3d/navigation/invert_y_axis");
27762777
const bool invert_x_axis = EDITOR_GET("editors/3d/navigation/invert_x_axis");
27772778

2778-
if (invert_y_axis) {
2779-
cursor.x_rot -= p_relative.y * radians_per_pixel;
2780-
} else {
2781-
cursor.x_rot += p_relative.y * radians_per_pixel;
2782-
}
2783-
// Clamp the Y rotation to roughly -90..90 degrees so the user can't look upside-down and end up disoriented.
2784-
cursor.x_rot = CLAMP(cursor.x_rot, -1.57, 1.57);
2779+
cursor.unsnapped_x_rot += p_relative.y * radians_per_pixel * (invert_y_axis ? -1 : 1);
2780+
cursor.unsnapped_x_rot = CLAMP(cursor.unsnapped_x_rot, -1.57, 1.57);
2781+
cursor.unsnapped_y_rot += p_relative.x * radians_per_pixel * (invert_x_axis ? -1 : 1);
27852782

2786-
if (invert_x_axis) {
2787-
cursor.y_rot -= p_relative.x * radians_per_pixel;
2788-
} else {
2789-
cursor.y_rot += p_relative.x * radians_per_pixel;
2783+
cursor.x_rot = cursor.unsnapped_x_rot;
2784+
cursor.y_rot = cursor.unsnapped_y_rot;
2785+
2786+
if (_is_nav_modifier_pressed("spatial_editor/viewport_orbit_snap_modifier_1") &&
2787+
_is_nav_modifier_pressed("spatial_editor/viewport_orbit_snap_modifier_2")) {
2788+
const real_t snap_angle = Math::deg_to_rad(45.0);
2789+
const real_t snap_threshold = Math::deg_to_rad((real_t)EDITOR_GET("editors/3d/navigation_feel/angle_snap_threshold"));
2790+
2791+
real_t x_rot_snapped = Math::snapped(cursor.unsnapped_x_rot, snap_angle);
2792+
real_t y_rot_snapped = Math::snapped(cursor.unsnapped_y_rot, snap_angle);
2793+
2794+
real_t x_dist = Math::abs(cursor.unsnapped_x_rot - x_rot_snapped);
2795+
real_t y_dist = Math::abs(cursor.unsnapped_y_rot - y_rot_snapped);
2796+
2797+
if (x_dist < snap_threshold && y_dist < snap_threshold) {
2798+
cursor.x_rot = x_rot_snapped;
2799+
cursor.y_rot = y_rot_snapped;
2800+
2801+
real_t y_rot_wrapped = Math::wrapf(y_rot_snapped, (real_t)-Math::PI, (real_t)Math::PI);
2802+
2803+
if (Math::abs(x_rot_snapped) < snap_threshold) {
2804+
if (Math::abs(y_rot_wrapped) < snap_threshold) {
2805+
view_type = VIEW_TYPE_FRONT;
2806+
} else if (Math::abs(Math::abs(y_rot_wrapped) - Math::PI) < snap_threshold) {
2807+
view_type = VIEW_TYPE_REAR;
2808+
} else if (Math::abs(y_rot_wrapped - Math::PI / 2.0) < snap_threshold) {
2809+
view_type = VIEW_TYPE_LEFT;
2810+
} else if (Math::abs(y_rot_wrapped + Math::PI / 2.0) < snap_threshold) {
2811+
view_type = VIEW_TYPE_RIGHT;
2812+
} else {
2813+
// Only switch to ortho for 90-degree views.
2814+
return;
2815+
}
2816+
_set_auto_orthogonal();
2817+
_update_name();
2818+
} else if (Math::abs(Math::abs(x_rot_snapped) - Math::PI / 2.0) < snap_threshold) {
2819+
if (Math::abs(y_rot_wrapped) < snap_threshold ||
2820+
Math::abs(Math::abs(y_rot_wrapped) - Math::PI) < snap_threshold ||
2821+
Math::abs(y_rot_wrapped - Math::PI / 2.0) < snap_threshold ||
2822+
Math::abs(y_rot_wrapped + Math::PI / 2.0) < snap_threshold) {
2823+
view_type = x_rot_snapped > 0 ? VIEW_TYPE_TOP : VIEW_TYPE_BOTTOM;
2824+
_set_auto_orthogonal();
2825+
_update_name();
2826+
}
2827+
}
2828+
2829+
return;
2830+
}
27902831
}
2832+
27912833
view_type = VIEW_TYPE_USER;
2792-
_update_name();
2834+
if (orthogonal && auto_orthogonal) {
2835+
_menu_option(VIEW_PERSPECTIVE);
2836+
}
27932837
}
27942838

27952839
void Node3DEditorViewport::_nav_look(Ref<InputEventWithModifiers> p_event, const Vector2 &p_relative) {
@@ -2817,8 +2861,10 @@ void Node3DEditorViewport::_nav_look(Ref<InputEventWithModifiers> p_event, const
28172861
}
28182862
// Clamp the Y rotation to roughly -90..90 degrees so the user can't look upside-down and end up disoriented.
28192863
cursor.x_rot = CLAMP(cursor.x_rot, -1.57, 1.57);
2864+
cursor.unsnapped_x_rot = cursor.x_rot;
28202865

28212866
cursor.y_rot += p_relative.x * radians_per_pixel;
2867+
cursor.unsnapped_y_rot = cursor.y_rot;
28222868

28232869
// Look is like the opposite of Orbit: the focus point rotates around the camera
28242870
Transform3D camera_transform = to_camera_transform(cursor);
@@ -3803,6 +3849,8 @@ void Node3DEditorViewport::_apply_camera_transform_to_cursor() {
38033849

38043850
cursor.x_rot = -camera_transform.basis.get_euler().x;
38053851
cursor.y_rot = -camera_transform.basis.get_euler().y;
3852+
cursor.unsnapped_x_rot = cursor.x_rot;
3853+
cursor.unsnapped_y_rot = cursor.y_rot;
38063854
}
38073855

38083856
void Node3DEditorViewport::_menu_option(int p_option) {
@@ -3811,6 +3859,8 @@ void Node3DEditorViewport::_menu_option(int p_option) {
38113859
case VIEW_TOP: {
38123860
cursor.y_rot = 0;
38133861
cursor.x_rot = Math::PI / 2.0;
3862+
cursor.unsnapped_y_rot = cursor.y_rot;
3863+
cursor.unsnapped_x_rot = cursor.x_rot;
38143864
set_message(TTR("Top View."), 2);
38153865
view_type = VIEW_TYPE_TOP;
38163866
_set_auto_orthogonal();
@@ -3820,6 +3870,8 @@ void Node3DEditorViewport::_menu_option(int p_option) {
38203870
case VIEW_BOTTOM: {
38213871
cursor.y_rot = 0;
38223872
cursor.x_rot = -Math::PI / 2.0;
3873+
cursor.unsnapped_y_rot = cursor.y_rot;
3874+
cursor.unsnapped_x_rot = cursor.x_rot;
38233875
set_message(TTR("Bottom View."), 2);
38243876
view_type = VIEW_TYPE_BOTTOM;
38253877
_set_auto_orthogonal();
@@ -3829,6 +3881,8 @@ void Node3DEditorViewport::_menu_option(int p_option) {
38293881
case VIEW_LEFT: {
38303882
cursor.x_rot = 0;
38313883
cursor.y_rot = Math::PI / 2.0;
3884+
cursor.unsnapped_x_rot = cursor.x_rot;
3885+
cursor.unsnapped_y_rot = cursor.y_rot;
38323886
set_message(TTR("Left View."), 2);
38333887
view_type = VIEW_TYPE_LEFT;
38343888
_set_auto_orthogonal();
@@ -3838,6 +3892,8 @@ void Node3DEditorViewport::_menu_option(int p_option) {
38383892
case VIEW_RIGHT: {
38393893
cursor.x_rot = 0;
38403894
cursor.y_rot = -Math::PI / 2.0;
3895+
cursor.unsnapped_x_rot = cursor.x_rot;
3896+
cursor.unsnapped_y_rot = cursor.y_rot;
38413897
set_message(TTR("Right View."), 2);
38423898
view_type = VIEW_TYPE_RIGHT;
38433899
_set_auto_orthogonal();
@@ -3847,6 +3903,8 @@ void Node3DEditorViewport::_menu_option(int p_option) {
38473903
case VIEW_FRONT: {
38483904
cursor.x_rot = 0;
38493905
cursor.y_rot = 0;
3906+
cursor.unsnapped_x_rot = cursor.x_rot;
3907+
cursor.unsnapped_y_rot = cursor.y_rot;
38503908
set_message(TTR("Front View."), 2);
38513909
view_type = VIEW_TYPE_FRONT;
38523910
_set_auto_orthogonal();
@@ -3856,6 +3914,8 @@ void Node3DEditorViewport::_menu_option(int p_option) {
38563914
case VIEW_REAR: {
38573915
cursor.x_rot = 0;
38583916
cursor.y_rot = Math::PI;
3917+
cursor.unsnapped_x_rot = cursor.x_rot;
3918+
cursor.unsnapped_y_rot = cursor.y_rot;
38593919
set_message(TTR("Rear View."), 2);
38603920
view_type = VIEW_TYPE_REAR;
38613921
_set_auto_orthogonal();
@@ -4485,9 +4545,11 @@ void Node3DEditorViewport::set_state(const Dictionary &p_state) {
44854545
}
44864546
if (p_state.has("x_rotation")) {
44874547
cursor.x_rot = p_state["x_rotation"];
4548+
cursor.unsnapped_x_rot = cursor.x_rot;
44884549
}
44894550
if (p_state.has("y_rotation")) {
44904551
cursor.y_rot = p_state["y_rotation"];
4552+
cursor.unsnapped_y_rot = cursor.y_rot;
44914553
}
44924554
if (p_state.has("distance")) {
44934555
cursor.distance = p_state["distance"];
@@ -6056,6 +6118,8 @@ Node3DEditorViewport::Node3DEditorViewport(Node3DEditor *p_spatial_editor, int p
60566118
// Registering with Key::NONE intentionally creates an empty Array.
60576119
register_shortcut_action("spatial_editor/viewport_orbit_modifier_1", TTRC("Viewport Orbit Modifier 1"), Key::NONE);
60586120
register_shortcut_action("spatial_editor/viewport_orbit_modifier_2", TTRC("Viewport Orbit Modifier 2"), Key::NONE);
6121+
register_shortcut_action("spatial_editor/viewport_orbit_snap_modifier_1", TTRC("Viewport Orbit Snap Modifier 1"), Key::ALT);
6122+
register_shortcut_action("spatial_editor/viewport_orbit_snap_modifier_2", TTRC("Viewport Orbit Snap Modifier 2"), Key::NONE);
60596123
register_shortcut_action("spatial_editor/viewport_pan_modifier_1", TTRC("Viewport Pan Modifier 1"), Key::SHIFT);
60606124
register_shortcut_action("spatial_editor/viewport_pan_modifier_2", TTRC("Viewport Pan Modifier 2"), Key::NONE);
60616125
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
@@ -409,6 +409,7 @@ class Node3DEditorViewport : public Control {
409409
struct Cursor {
410410
Vector3 pos;
411411
real_t x_rot, y_rot, distance, fov_scale;
412+
real_t unsnapped_x_rot, unsnapped_y_rot;
412413
Vector3 eye_pos; // Used in freelook mode
413414
bool region_select;
414415
Point2 region_begin, region_end;
@@ -417,6 +418,8 @@ class Node3DEditorViewport : public Control {
417418
// These rotations place the camera in +X +Y +Z, aka south east, facing north west.
418419
x_rot = 0.5;
419420
y_rot = -0.5;
421+
unsnapped_x_rot = x_rot;
422+
unsnapped_y_rot = y_rot;
420423
distance = 4;
421424
fov_scale = 1.0;
422425
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)