From eb00a8546db91328a2bcb2e600ef6ca8b49018ec Mon Sep 17 00:00:00 2001 From: smix8 <52464204+smix8@users.noreply.github.com> Date: Sun, 17 May 2026 22:34:25 +0200 Subject: [PATCH] Split `Node3DEditorViewport` into a dedicated file Splits `Node3DEditorViewport` into a dedicated file. The main `Node3DEditorPlugin` file was one if the biggest files of the entire engine. It was getting way too big to be viewed, parsed and displayed effectively (e.g. GitHub, certain IDEs). While still big after this PR it at least splits it more or less in half. - Moves `Node3DEditorViewport` and related helper classes to its own file `node_3d_editor_viewport` file. - Moves the (shared) constexpr to its own file `node_3d_editor_constants` file. - Forward declares many more classes. - Removes unnecessary includes. - Fixes classes that needed the actual Node3DViewport class. --- editor/debugger/script_editor_debugger.cpp | 1 + editor/docks/scene_tree_dock.cpp | 1 + editor/editor_interface.cpp | 1 + editor/editor_node.cpp | 1 + editor/plugins/editor_plugin.cpp | 1 + editor/scene/3d/camera_3d_editor_plugin.cpp | 1 + editor/scene/3d/node_3d_editor_constants.h | 63 + editor/scene/3d/node_3d_editor_gizmos.cpp | 1 + editor/scene/3d/node_3d_editor_plugin.cpp | 7340 +---------------- editor/scene/3d/node_3d_editor_plugin.h | 602 +- editor/scene/3d/node_3d_editor_viewport.cpp | 7313 ++++++++++++++++ editor/scene/3d/node_3d_editor_viewport.h | 612 ++ editor/scene/3d/path_3d_editor_plugin.cpp | 1 + editor/scene/3d/polygon_3d_editor_plugin.cpp | 1 + editor/settings/editor_settings_dialog.cpp | 1 + .../gridmap/editor/grid_map_editor_plugin.cpp | 1 + 16 files changed, 8067 insertions(+), 7874 deletions(-) create mode 100644 editor/scene/3d/node_3d_editor_constants.h create mode 100644 editor/scene/3d/node_3d_editor_viewport.cpp create mode 100644 editor/scene/3d/node_3d_editor_viewport.h diff --git a/editor/debugger/script_editor_debugger.cpp b/editor/debugger/script_editor_debugger.cpp index d44a043b17..aff621f6f7 100644 --- a/editor/debugger/script_editor_debugger.cpp +++ b/editor/debugger/script_editor_debugger.cpp @@ -55,6 +55,7 @@ #include "editor/gui/editor_toaster.h" #include "editor/inspector/editor_property_name_processor.h" #include "editor/scene/3d/node_3d_editor_plugin.h" +#include "editor/scene/3d/node_3d_editor_viewport.h" #include "editor/scene/canvas_item_editor_plugin.h" #include "editor/settings/editor_settings.h" #include "editor/themes/editor_scale.h" diff --git a/editor/docks/scene_tree_dock.cpp b/editor/docks/scene_tree_dock.cpp index 8af97f0e02..ecab44c235 100644 --- a/editor/docks/scene_tree_dock.cpp +++ b/editor/docks/scene_tree_dock.cpp @@ -54,6 +54,7 @@ #include "editor/inspector/editor_context_menu_plugin.h" #include "editor/inspector/multi_node_edit.h" #include "editor/scene/3d/node_3d_editor_plugin.h" +#include "editor/scene/3d/node_3d_editor_viewport.h" #include "editor/scene/canvas_item_editor_plugin.h" #include "editor/scene/rename_dialog.h" #include "editor/scene/reparent_dialog.h" diff --git a/editor/editor_interface.cpp b/editor/editor_interface.cpp index dcb1c6151a..80324ff13c 100644 --- a/editor/editor_interface.cpp +++ b/editor/editor_interface.cpp @@ -50,6 +50,7 @@ #include "editor/inspector/property_selector.h" #include "editor/run/editor_run_bar.h" #include "editor/scene/3d/node_3d_editor_plugin.h" +#include "editor/scene/3d/node_3d_editor_viewport.h" #include "editor/scene/editor_scene_tabs.h" #include "editor/scene/scene_tree_editor.h" #include "editor/settings/editor_command_palette.h" diff --git a/editor/editor_node.cpp b/editor/editor_node.cpp index c3a4222cfc..50f4e8ddbb 100644 --- a/editor/editor_node.cpp +++ b/editor/editor_node.cpp @@ -127,6 +127,7 @@ #include "editor/scene/3d/material_3d_conversion_plugins.h" #include "editor/scene/3d/mesh_library_editor_plugin.h" #include "editor/scene/3d/node_3d_editor_plugin.h" +#include "editor/scene/3d/node_3d_editor_viewport.h" #include "editor/scene/3d/root_motion_editor_plugin.h" #include "editor/scene/canvas_item_editor_plugin.h" #include "editor/scene/editor_scene_tabs.h" diff --git a/editor/plugins/editor_plugin.cpp b/editor/plugins/editor_plugin.cpp index 95fba96b03..ecc4b8409c 100644 --- a/editor/plugins/editor_plugin.cpp +++ b/editor/plugins/editor_plugin.cpp @@ -54,6 +54,7 @@ #include "editor/plugins/editor_plugin_list.h" #include "editor/plugins/editor_resource_conversion_plugin.h" #include "editor/scene/3d/node_3d_editor_plugin.h" +#include "editor/scene/3d/node_3d_editor_viewport.h" #include "editor/scene/canvas_item_editor_plugin.h" #include "editor/script/script_editor_plugin.h" #include "editor/settings/project_settings_editor.h" diff --git a/editor/scene/3d/camera_3d_editor_plugin.cpp b/editor/scene/3d/camera_3d_editor_plugin.cpp index 43144ea7ef..b157bbdf13 100644 --- a/editor/scene/3d/camera_3d_editor_plugin.cpp +++ b/editor/scene/3d/camera_3d_editor_plugin.cpp @@ -34,6 +34,7 @@ #include "core/object/callable_mp.h" #include "editor/editor_node.h" #include "editor/scene/3d/node_3d_editor_plugin.h" +#include "editor/themes/editor_scale.h" #include "scene/gui/aspect_ratio_container.h" #include "scene/gui/foldable_container.h" #include "scene/gui/subviewport_container.h" diff --git a/editor/scene/3d/node_3d_editor_constants.h b/editor/scene/3d/node_3d_editor_constants.h new file mode 100644 index 0000000000..377f3ef687 --- /dev/null +++ b/editor/scene/3d/node_3d_editor_constants.h @@ -0,0 +1,63 @@ +/**************************************************************************/ +/* node_3d_editor_constants.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#pragma once + +#include "core/math/math_defs.h" + +namespace Node3DEditorConstants { + +constexpr real_t GIZMO_ARROW_SIZE = 0.35; +constexpr real_t GIZMO_RING_HALF_WIDTH = 0.1; +constexpr real_t GIZMO_PLANE_SIZE = 0.2; +constexpr real_t GIZMO_PLANE_DST = 0.3; +constexpr real_t GIZMO_CIRCLE_SIZE = 1.1; + +constexpr real_t GIZMO_SCALE_OFFSET = GIZMO_CIRCLE_SIZE + 0.3; +constexpr real_t GIZMO_ARROW_OFFSET = GIZMO_CIRCLE_SIZE + 0.3; + +constexpr real_t TRACKBALL_SENSITIVITY = 0.005; +constexpr int TRACKBALL_SPHERE_RINGS = 16; +constexpr int TRACKBALL_SPHERE_SECTORS = 32; +constexpr real_t TRACKBALL_HIGHLIGHT_ALPHA = 0.01; +constexpr int GIZMO_HIGHLIGHT_AXIS_VIEW_ROTATION = 15; + +constexpr float VERTEX_SNAP_THRESHOLD = 30.0f; +constexpr int GIZMO_HIGHLIGHT_AXIS_TRACKBALL = 16; + +constexpr real_t ZOOM_FREELOOK_INDICATOR_DELAY_S = 1.5; + +constexpr real_t MIN_Z = 0.01; +constexpr real_t MAX_Z = 1000000.0; + +constexpr real_t MIN_FOV = 0.01; +constexpr real_t MAX_FOV = 179; + +} // namespace Node3DEditorConstants diff --git a/editor/scene/3d/node_3d_editor_gizmos.cpp b/editor/scene/3d/node_3d_editor_gizmos.cpp index 11ba7e62ee..cbf4d418d9 100644 --- a/editor/scene/3d/node_3d_editor_gizmos.cpp +++ b/editor/scene/3d/node_3d_editor_gizmos.cpp @@ -36,6 +36,7 @@ #include "editor/editor_node.h" #include "editor/editor_string_names.h" #include "editor/scene/3d/node_3d_editor_plugin.h" +#include "editor/scene/3d/node_3d_editor_viewport.h" #include "editor/settings/editor_settings.h" #include "scene/main/scene_tree.h" #include "scene/resources/3d/primitive_meshes.h" diff --git a/editor/scene/3d/node_3d_editor_plugin.cpp b/editor/scene/3d/node_3d_editor_plugin.cpp index 3520d7c8e3..e9aa2377e0 100644 --- a/editor/scene/3d/node_3d_editor_plugin.cpp +++ b/editor/scene/3d/node_3d_editor_plugin.cpp @@ -32,8 +32,6 @@ #include "core/config/project_settings.h" #include "core/input/input.h" -#include "core/input/input_map.h" -#include "core/io/resource_loader.h" #include "core/math/geometry_3d.h" #include "core/math/math_funcs.h" #include "core/math/projection.h" @@ -41,8 +39,6 @@ #include "core/object/class_db.h" #include "core/os/keyboard.h" #include "core/os/os.h" -#include "core/string/translation_server.h" -#include "editor/animation/animation_player_editor_plugin.h" #include "editor/debugger/editor_debugger_node.h" #include "editor/docks/scene_tree_dock.h" #include "editor/editor_main_screen.h" @@ -86,7311 +82,37 @@ #include "editor/scene/3d/gizmos/two_bone_ik_3d_gizmo_plugin.h" #include "editor/scene/3d/gizmos/visible_on_screen_notifier_3d_gizmo_plugin.h" #include "editor/scene/3d/gizmos/voxel_gi_gizmo_plugin.h" +#include "editor/scene/3d/node_3d_editor_constants.h" #include "editor/scene/3d/node_3d_editor_gizmos.h" +#include "editor/scene/3d/node_3d_editor_viewport.h" #include "editor/settings/editor_settings.h" -#include "editor/translations/editor_translation_preview_button.h" #include "editor/translations/editor_translation_preview_menu.h" -#include "scene/3d/audio_stream_player_3d.h" #include "scene/3d/camera_3d.h" -#include "scene/3d/decal.h" #include "scene/3d/light_3d.h" -#include "scene/3d/mesh_instance_3d.h" #include "scene/3d/physics/collision_shape_3d.h" #include "scene/3d/physics/physics_body_3d.h" -#include "scene/3d/sprite_3d.h" -#include "scene/3d/visual_instance_3d.h" #include "scene/3d/world_environment.h" +#include "scene/gui/button.h" #include "scene/gui/center_container.h" #include "scene/gui/color_picker.h" #include "scene/gui/flow_container.h" +#include "scene/gui/menu_button.h" #include "scene/gui/rich_text_label.h" #include "scene/gui/separator.h" +#include "scene/gui/spin_box.h" #include "scene/gui/split_container.h" -#include "scene/gui/subviewport_container.h" #include "scene/main/scene_tree.h" #include "scene/resources/3d/sky_material.h" -#include "scene/resources/packed_scene.h" #include "scene/resources/sky.h" #include "scene/resources/surface_tool.h" #include "servers/rendering/rendering_server.h" -constexpr real_t GIZMO_ARROW_SIZE = 0.35; -constexpr real_t GIZMO_RING_HALF_WIDTH = 0.1; -constexpr real_t GIZMO_PLANE_SIZE = 0.2; -constexpr real_t GIZMO_PLANE_DST = 0.3; -constexpr real_t GIZMO_CIRCLE_SIZE = 1.1; - -constexpr real_t GIZMO_SCALE_OFFSET = GIZMO_CIRCLE_SIZE + 0.3; -constexpr real_t GIZMO_ARROW_OFFSET = GIZMO_CIRCLE_SIZE + 0.3; - -constexpr real_t TRACKBALL_SENSITIVITY = 0.005; -constexpr int TRACKBALL_SPHERE_RINGS = 16; -constexpr int TRACKBALL_SPHERE_SECTORS = 32; -constexpr real_t TRACKBALL_HIGHLIGHT_ALPHA = 0.01; -constexpr int GIZMO_HIGHLIGHT_AXIS_VIEW_ROTATION = 15; - -constexpr float VERTEX_SNAP_THRESHOLD = 30.0f; -constexpr int GIZMO_HIGHLIGHT_AXIS_TRACKBALL = 16; - -constexpr real_t ZOOM_FREELOOK_INDICATOR_DELAY_S = 1.5; - -constexpr real_t MIN_Z = 0.01; -constexpr real_t MAX_Z = 1000000.0; - -constexpr real_t MIN_FOV = 0.01; -constexpr real_t MAX_FOV = 179; - -void ViewportNavigationControl::_notification(int p_what) { - switch (p_what) { - case NOTIFICATION_DRAW: { - if (viewport != nullptr) { - _draw(); - _update_navigation(); - } - } break; - - case NOTIFICATION_MOUSE_ENTER: { - hovered = true; - queue_redraw(); - } break; - - case NOTIFICATION_MOUSE_EXIT: { - hovered = false; - queue_redraw(); - } break; - } -} - -void ViewportNavigationControl::_draw() { - if (nav_mode == View3DController::NAV_MODE_NONE) { - return; - } - - Vector2 center = get_size() / 2.0; - float radius = get_size().x / 2.0; - - const bool focused = focused_index != -1; - draw_circle(center, radius, Color(0.5, 0.5, 0.5, focused || hovered ? 0.35 : 0.15)); - - const Color c = focused ? Color(0.9, 0.9, 0.9, 0.9) : Color(0.5, 0.5, 0.5, 0.25); - - Vector2 circle_pos = focused ? center.move_toward(focused_pos, radius) : center; - - draw_circle(circle_pos, AXIS_CIRCLE_RADIUS, c); - draw_circle(circle_pos, AXIS_CIRCLE_RADIUS * 0.8, c.darkened(0.4)); -} - -void ViewportNavigationControl::_process_click(int p_index, Vector2 p_position, bool p_pressed) { - hovered = false; - queue_redraw(); - - if (focused_index != -1 && focused_index != p_index) { - return; - } - if (p_pressed) { - if (p_position.distance_to(get_size() / 2.0) < get_size().x / 2.0) { - focused_pos = p_position; - focused_index = p_index; - queue_redraw(); - } - } else { - focused_index = -1; - if (Input::get_singleton()->get_mouse_mode() == Input::MouseMode::MOUSE_MODE_CAPTURED) { - Input::get_singleton()->set_mouse_mode(Input::MouseMode::MOUSE_MODE_VISIBLE); - Input::get_singleton()->warp_mouse(focused_mouse_start); - } - } -} - -void ViewportNavigationControl::_process_drag(int p_index, Vector2 p_position, Vector2 p_relative_position) { - if (focused_index == p_index) { - if (Input::get_singleton()->get_mouse_mode() == Input::MouseMode::MOUSE_MODE_VISIBLE) { - Input::get_singleton()->set_mouse_mode(Input::MouseMode::MOUSE_MODE_CAPTURED); - focused_mouse_start = p_position; - } - focused_pos += p_relative_position; - queue_redraw(); - } -} - -void ViewportNavigationControl::gui_input(const Ref &p_event) { - // Mouse events - const Ref mouse_button = p_event; - if (mouse_button.is_valid() && mouse_button->get_button_index() == MouseButton::LEFT) { - _process_click(100, mouse_button->get_position(), mouse_button->is_pressed()); - } - - const Ref mouse_motion = p_event; - if (mouse_motion.is_valid()) { - _process_drag(100, mouse_motion->get_global_position(), viewport->view_3d_controller->get_warped_mouse_motion(mouse_motion, viewport->surface->get_global_rect())); - } - - // Touch events - const Ref screen_touch = p_event; - if (screen_touch.is_valid()) { - _process_click(screen_touch->get_index(), screen_touch->get_position(), screen_touch->is_pressed()); - } - - const Ref screen_drag = p_event; - if (screen_drag.is_valid()) { - _process_drag(screen_drag->get_index(), screen_drag->get_position(), screen_drag->get_relative()); - } -} - -void ViewportNavigationControl::_update_navigation() { - if (focused_index == -1) { - return; - } - - Vector2 delta = focused_pos - (get_size() / 2.0); - Vector2 delta_normalized = delta.normalized(); - switch (nav_mode) { - case View3DController::NAV_MODE_MOVE: { - real_t speed_multiplier = MIN(delta.length() / (get_size().x * 100.0), 3.0); - real_t speed = viewport->view_3d_controller->get_freelook_speed() * speed_multiplier; - - Vector3 forward; - if (viewport->view_3d_controller->get_freelook_scheme() == View3DController::FreelookScheme::FREELOOK_FULLY_AXIS_LOCKED) { - // Forward/backward keys will always go straight forward/backward, never moving on the Y axis. - forward = Vector3(0, 0, delta_normalized.y).rotated(Vector3(0, 1, 0), viewport->camera->get_rotation().y); - } else { - // Forward/backward keys will be relative to the camera pitch. - forward = viewport->camera->get_transform().basis.xform(Vector3(0, 0, delta_normalized.y)); - } - - const Vector3 right = viewport->camera->get_transform().basis.xform(Vector3(delta_normalized.x, 0, 0)); - - const Vector3 direction = forward + right; - const Vector3 motion = direction * speed; - viewport->view_3d_controller->cursor.pos += motion; - viewport->view_3d_controller->cursor.eye_pos += motion; - } break; - - case View3DController::NAV_MODE_LOOK: { - real_t speed_multiplier = MIN(delta.length() / (get_size().x * 2.5), 3.0); - real_t speed = viewport->view_3d_controller->get_freelook_speed() * speed_multiplier; - viewport->view_3d_controller->cursor_look(nullptr, delta_normalized * speed); - } break; - - case View3DController::NAV_MODE_PAN: { - real_t speed_multiplier = MIN(delta.length() / (get_size().x), 3.0); - real_t speed = viewport->view_3d_controller->get_freelook_speed() * speed_multiplier; - viewport->view_3d_controller->cursor_pan(nullptr, -delta_normalized * speed); - } break; - case View3DController::NAV_MODE_ZOOM: { - real_t speed_multiplier = MIN(delta.length() / (get_size().x), 3.0); - real_t speed = viewport->view_3d_controller->get_freelook_speed() * speed_multiplier; - viewport->view_3d_controller->cursor_zoom(nullptr, delta_normalized * speed); - } break; - case View3DController::NAV_MODE_ORBIT: { - real_t speed_multiplier = MIN(delta.length() / (get_size().x), 3.0); - real_t speed = viewport->view_3d_controller->get_freelook_speed() * speed_multiplier; - viewport->view_3d_controller->cursor_orbit(nullptr, delta_normalized * speed); - } break; - case View3DController::NAV_MODE_NONE: { - } break; - } -} - -void ViewportNavigationControl::set_navigation_mode(View3DController::NavigationMode p_nav_mode) { - nav_mode = p_nav_mode; -} - -void ViewportNavigationControl::set_viewport(Node3DEditorViewport *p_viewport) { - viewport = p_viewport; -} - -void ViewportRotationControl::_notification(int p_what) { - switch (p_what) { - case NOTIFICATION_ENTER_TREE: { - axis_menu_options.clear(); - axis_menu_options.push_back(Node3DEditorViewport::VIEW_RIGHT); - axis_menu_options.push_back(Node3DEditorViewport::VIEW_TOP); - axis_menu_options.push_back(Node3DEditorViewport::VIEW_FRONT); - axis_menu_options.push_back(Node3DEditorViewport::VIEW_LEFT); - axis_menu_options.push_back(Node3DEditorViewport::VIEW_BOTTOM); - axis_menu_options.push_back(Node3DEditorViewport::VIEW_REAR); - - axis_colors.clear(); - axis_colors.push_back(get_theme_color(SNAME("axis_x_color"), EditorStringName(Editor))); - axis_colors.push_back(get_theme_color(SNAME("axis_y_color"), EditorStringName(Editor))); - axis_colors.push_back(get_theme_color(SNAME("axis_z_color"), EditorStringName(Editor))); - queue_redraw(); - } break; - - case NOTIFICATION_DRAW: { - if (viewport != nullptr) { - _draw(); - } - } break; - - case NOTIFICATION_MOUSE_EXIT: { - focused_axis = -2; - queue_redraw(); - } break; - - case NOTIFICATION_WM_WINDOW_FOCUS_OUT: { - gizmo_activated = false; - } break; - } -} - -void ViewportRotationControl::_draw() { - const Vector2 center = get_size() / 2.0; - const real_t radius = get_size().x / 2.0; - - if (focused_axis > -2 || orbiting_index != -1) { - draw_circle(center, radius, Color(0.5, 0.5, 0.5, 0.25), true, -1.0, true); - } - - Vector axis_to_draw; - _get_sorted_axis(axis_to_draw); - for (int i = 0; i < axis_to_draw.size(); ++i) { - _draw_axis(axis_to_draw[i]); - } -} - -void ViewportRotationControl::_draw_axis(const Axis2D &p_axis) { - const bool focused = focused_axis == p_axis.axis; - const bool positive = p_axis.is_positive; - const int direction = p_axis.axis % 3; - - const Color axis_color = axis_colors[direction]; - const double min_alpha = 0.35; - const double alpha = focused ? 1.0 : Math::remap((p_axis.z_axis + 1.0) / 2.0, 0, 0.5, min_alpha, 1.0); - const Color c = focused ? Color(axis_color.lightened(0.25), 1.0) : Color(axis_color, alpha); - - // Highlight positive axis text when hovered. - const Color c_positive_axis = focused ? Color(1.0, 1.0, 1.0, alpha) : Color(0.0, 0.0, 0.0, alpha * 0.6); - - // Highlight negative axis text when hovered, but hide when not focused. - const Color c_negative_axis = focused ? Color(1.0, 1.0, 1.0, alpha) : Color(axis_color, 0); - - if (positive) { - // Draw axis lines for the positive axes. - const Vector2 center = get_size() / 2.0; - const Vector2 diff = p_axis.screen_point - center; - const float line_length = MAX(diff.length() - AXIS_CIRCLE_RADIUS - 0.5 * EDSCALE, 0); - - draw_line(center + diff.limit_length(0.5 * EDSCALE), center + diff.limit_length(line_length), c, 1.5 * EDSCALE, true); - - draw_circle(p_axis.screen_point, AXIS_CIRCLE_RADIUS, c, true, -1.0, true); - - // Draw the axis letter for the positive axes. - const String axis_name = direction == 0 ? "X" : (direction == 1 ? "Y" : "Z"); - const Ref &font = get_theme_font(SNAME("rotation_control"), EditorStringName(EditorFonts)); - const int font_size = get_theme_font_size(SNAME("rotation_control_size"), EditorStringName(EditorFonts)); - const Size2 char_size = font->get_char_size(axis_name[0], font_size); - const Vector2 char_offset = Vector2(-char_size.width / 2.0, char_size.height * 0.25); - draw_char(font, p_axis.screen_point + char_offset, axis_name, font_size, c_positive_axis); - } else { - // Draw an outline around the negative axes. - draw_circle(p_axis.screen_point, AXIS_CIRCLE_RADIUS, c, true, -1.0, true); - draw_circle(p_axis.screen_point, AXIS_CIRCLE_RADIUS * 0.8, c.darkened(0.4), true, -1.0, true); - - // Draw the text for the negative axes. - const String axis_name = direction == 0 ? "-X" : (direction == 1 ? "-Y" : "-Z"); - const Ref &font = get_theme_font(SNAME("rotation_control"), EditorStringName(EditorFonts)); - const int font_size = get_theme_font_size(SNAME("rotation_control_size"), EditorStringName(EditorFonts)); - const Size2 string_size = font->get_string_size(axis_name, HORIZONTAL_ALIGNMENT_LEFT, -1.0f, font_size); - const float font_ascent = font->get_ascent(font_size); - const float font_descent = font->get_descent(font_size); - const float string_height = font_ascent + font_descent; - const Vector2 offset(-string_size.width / 2.0, string_height * 0.25); - draw_string(font, p_axis.screen_point + offset, axis_name, HORIZONTAL_ALIGNMENT_LEFT, -1.0f, font_size, c_negative_axis); - } -} - -void ViewportRotationControl::_get_sorted_axis(Vector &r_axis) { - const Vector2 center = get_size() / 2.0; - const real_t radius = get_size().x / 2.0 - AXIS_CIRCLE_RADIUS - 2.0 * EDSCALE; - const Basis camera_basis = viewport->view_3d_controller->to_camera_transform().get_basis().inverse(); - - for (int i = 0; i < 3; ++i) { - Vector3 axis_3d = camera_basis.get_column(i); - Vector2 axis_vector = Vector2(axis_3d.x, -axis_3d.y) * radius; - - if (Math::abs(axis_3d.z) < 1.0) { - Axis2D pos_axis; - pos_axis.axis = i; - pos_axis.screen_point = center + axis_vector; - pos_axis.z_axis = axis_3d.z; - pos_axis.is_positive = true; - r_axis.push_back(pos_axis); - - Axis2D neg_axis; - neg_axis.axis = i + 3; - neg_axis.screen_point = center - axis_vector; - neg_axis.z_axis = -axis_3d.z; - neg_axis.is_positive = false; - r_axis.push_back(neg_axis); - } else { - // Special case when the camera is aligned with one axis. - Axis2D axis; - axis.axis = i + (axis_3d.z <= 0 ? 0 : 3); - axis.screen_point = center; - axis.z_axis = 1.0; - // Invert display style to fix aligned axis rendering. - axis.is_positive = (axis_3d.z > 0); - r_axis.push_back(axis); - } - } - - r_axis.sort_custom(); -} - -void ViewportRotationControl::_process_click(int p_index, Vector2 p_position, bool p_pressed) { - if (orbiting_index != -1 && orbiting_index != p_index) { - return; - } - if (p_pressed) { - if (p_position.distance_to(get_size() / 2.0) < get_size().x / 2.0) { - orbiting_index = p_index; - } - } else { - if (focused_axis > -1 && gizmo_activated) { - viewport->_menu_option(axis_menu_options[focused_axis]); - _update_focus(); - } - orbiting_index = -1; - if (Input::get_singleton()->get_mouse_mode() == Input::MouseMode::MOUSE_MODE_CAPTURED) { - Input::get_singleton()->set_mouse_mode(Input::MouseMode::MOUSE_MODE_VISIBLE); - Input::get_singleton()->warp_mouse(orbiting_mouse_start); - } - } -} - -void ViewportRotationControl::_process_drag(Ref p_event, int p_index, Vector2 p_position, Vector2 p_relative_position) { - Point2 mouse_pos = get_local_mouse_position(); - const bool movement_threshold_passed = original_mouse_pos.distance_to(mouse_pos) > 4 * EDSCALE; - if (orbiting_index == p_index && gizmo_activated && movement_threshold_passed) { - if (Input::get_singleton()->get_mouse_mode() == Input::MouseMode::MOUSE_MODE_VISIBLE) { - Input::get_singleton()->set_mouse_mode(Input::MouseMode::MOUSE_MODE_CAPTURED); - orbiting_mouse_start = p_position; - saved_cursor = viewport->view_3d_controller->cursor; - } - viewport->view_3d_controller->cursor_orbit(p_event, p_relative_position); - focused_axis = -1; - } else { - _update_focus(); - } -} - -void ViewportRotationControl::gui_input(const Ref &p_event) { - ERR_FAIL_COND(p_event.is_null()); - - // Key events - const Ref k = p_event; - - if (k.is_valid() && k->is_action_pressed(SNAME("ui_cancel"), false, true)) { - if (Input::get_singleton()->get_mouse_mode() == Input::MouseMode::MOUSE_MODE_CAPTURED) { - Input::get_singleton()->set_mouse_mode(Input::MouseMode::MOUSE_MODE_VISIBLE); - Input::get_singleton()->warp_mouse(orbiting_mouse_start); - viewport->view_3d_controller->cursor = saved_cursor; - gizmo_activated = false; - } - } - - // Mouse events - const Ref mb = p_event; - if (mb.is_valid()) { - if (mb->get_button_index() == MouseButton::LEFT) { - _process_click(100, mb->get_position(), mb->is_pressed()); - if (mb->is_pressed()) { - gizmo_activated = true; - original_mouse_pos = get_local_mouse_position(); - grab_focus(); - } - } else if (mb->get_button_index() == MouseButton::RIGHT) { - if (Input::get_singleton()->get_mouse_mode() == Input::MouseMode::MOUSE_MODE_CAPTURED) { - Input::get_singleton()->set_mouse_mode(Input::MouseMode::MOUSE_MODE_VISIBLE); - Input::get_singleton()->warp_mouse(orbiting_mouse_start); - viewport->view_3d_controller->cursor = saved_cursor; - gizmo_activated = false; - } - } - } - - const Ref mm = p_event; - if (mm.is_valid()) { - _process_drag(mm, 100, mm->get_global_position(), viewport->view_3d_controller->get_warped_mouse_motion(mm, viewport->surface->get_global_rect())); - } - - // Touch events - const Ref screen_touch = p_event; - if (screen_touch.is_valid()) { - _process_click(screen_touch->get_index(), screen_touch->get_position(), screen_touch->is_pressed()); - } - - const Ref screen_drag = p_event; - if (screen_drag.is_valid()) { - _process_drag(nullptr, screen_drag->get_index(), screen_drag->get_position(), screen_drag->get_relative()); - } -} - -void ViewportRotationControl::_update_focus() { - int original_focus = focused_axis; - focused_axis = -2; - Vector2 mouse_pos = get_local_mouse_position(); - - if (mouse_pos.distance_to(get_size() / 2.0) < get_size().x / 2.0) { - focused_axis = -1; - } - - Vector axes; - _get_sorted_axis(axes); - - for (int i = 0; i < axes.size(); i++) { - const Axis2D &axis = axes[i]; - if (mouse_pos.distance_to(axis.screen_point) < AXIS_CIRCLE_RADIUS) { - focused_axis = axis.axis; - } - } - - if (focused_axis != original_focus) { - queue_redraw(); - } -} - -void ViewportRotationControl::set_viewport(Node3DEditorViewport *p_viewport) { - viewport = p_viewport; -} - -void Node3DEditorViewport::_view_settings_confirmed(real_t p_interp_delta) { - // Set FOV override multiplier back to the default, so that the FOV - // setting specified in the View menu is correctly applied. - view_3d_controller->cursor.fov_scale = 1.0; - - view_3d_controller->update_camera(p_interp_delta); -} - -void Node3DEditorViewport::_update_navigation_controls_visibility() { - bool show_viewport_rotation_gizmo = EDITOR_GET("editors/3d/navigation/show_viewport_rotation_gizmo") && (!previewing_cinema && !previewing_camera); - rotation_control->set_visible(show_viewport_rotation_gizmo); - - bool show_viewport_navigation_gizmo = EDITOR_GET("editors/3d/navigation/show_viewport_navigation_gizmo") && (!previewing_cinema && !previewing_camera); - position_control->set_visible(show_viewport_navigation_gizmo); - look_control->set_visible(show_viewport_navigation_gizmo); -} - -bool Node3DEditorViewport::_is_rotation_arc_visible() const { - return _edit.mode == TRANSFORM_ROTATE && !Math::is_zero_approx(_edit.accumulated_rotation_angle) && _edit.gizmo_initiated; -} - -int Node3DEditorViewport::get_selected_count() const { - const HashMap &selection = editor_selection->get_selection(); - - int count = 0; - - for (const KeyValue &E : selection) { - Node3D *sp = ObjectDB::get_instance(E.key); - if (!sp) { - continue; - } - - Node3DEditorSelectedItem *se = editor_selection->get_node_editor_data(sp); - if (!se) { - continue; - } - - count++; - } - - return count; -} - -bool Node3DEditorViewport::_has_unlocked_selection() const { - const List &selection = editor_selection->get_top_selected_node_list(); - for (Node *E : selection) { - Node3D *sp = Object::cast_to(E); - if (sp && !_is_node_locked(sp)) { - return true; - } - } - return false; -} - -void Node3DEditorViewport::cancel_transform() { - const List &selection = editor_selection->get_top_selected_node_list(); - - for (Node *E : selection) { - Node3D *sp = Object::cast_to(E); - if (!sp) { - continue; - } - - Node3DEditorSelectedItem *se = editor_selection->get_node_editor_data(sp); - if (!se) { - continue; - } - - if (se && se->gizmo.is_valid()) { - Vector ids; - Vector restore; - - for (const KeyValue &GE : se->subgizmos) { - ids.push_back(GE.key); - restore.push_back(GE.value); - } - - se->gizmo->commit_subgizmos(ids, restore, true); - } - - sp->set_global_transform(se->original); - } - - for (const KeyValue &pair : _edit.children_original_globals) { - pair.key->set_global_transform(pair.value); - } - - collision_reposition = false; - finish_transform(); - set_message(TTRC("Transform Aborted."), 3); -} - -void Node3DEditorViewport::_update_shrink() { - const float scaling_3d_scale = GLOBAL_GET("rendering/scaling_3d/scale"); - const float shrink_factor = view_display_menu->get_popup()->is_item_checked(view_display_menu->get_popup()->get_item_index(VIEW_HALF_RESOLUTION)) ? 0.5 : 1.0; - viewport->set_scaling_3d_scale(MAX(0.25, scaling_3d_scale * shrink_factor)); -} - -float Node3DEditorViewport::get_znear() const { - return CLAMP(spatial_editor->get_znear(), MIN_Z, MAX_Z); -} - -float Node3DEditorViewport::get_zfar() const { - return CLAMP(spatial_editor->get_zfar(), MIN_Z, MAX_Z); -} - -float Node3DEditorViewport::get_fov() const { - return CLAMP(spatial_editor->get_fov() * view_3d_controller->cursor.fov_scale, MIN_FOV, MAX_FOV); -} - -Transform3D Node3DEditorViewport::_get_camera_transform() const { - return camera->get_global_transform(); -} - -Vector3 Node3DEditorViewport::_get_camera_position() const { - return _get_camera_transform().origin; -} - -Point2 Node3DEditorViewport::point_to_screen(const Vector3 &p_point) { - return camera->unproject_position(p_point); -} - -Vector3 Node3DEditorViewport::get_ray_pos(const Vector2 &p_pos) const { - return camera->project_ray_origin(p_pos); -} - -Vector3 Node3DEditorViewport::_get_camera_normal() const { - return -_get_camera_transform().basis.get_column(2); -} - -Vector3 Node3DEditorViewport::get_ray(const Vector2 &p_pos) const { - return camera->project_ray_normal(p_pos); -} - -void Node3DEditorViewport::_clear_selected() { - if (previewing) { - return; - } - - _edit.gizmo = Ref(); - _edit.gizmo_handle = -1; - _edit.gizmo_handle_secondary = false; - _edit.gizmo_initial_value = Variant(); - - Node3D *selected = spatial_editor->get_single_selected_node(); - Node3DEditorSelectedItem *se = selected ? editor_selection->get_node_editor_data(selected) : nullptr; - - if (se && se->gizmo.is_valid()) { - se->subgizmos.clear(); - se->gizmo->redraw(); - se->gizmo.unref(); - spatial_editor->update_transform_gizmo(); - } else { - editor_selection->clear(); - Node3DEditor::get_singleton()->edit(nullptr); - } -} - -void Node3DEditorViewport::_select_clicked(bool p_allow_locked) { - if (previewing) { - clicked = ObjectID(); - return; - } - - Node *node = ObjectDB::get_instance(clicked); - Node3D *selected = Object::cast_to(node); - clicked = ObjectID(); - - if (!selected) { - return; - } - - Node *edited_scene = EditorNode::get_singleton()->get_edited_scene(); - - // Prevent selection of nodes not owned by the edited scene. - while (node && node != edited_scene->get_parent()) { - Node *node_owner = node->get_owner(); - if (node_owner == edited_scene || node == edited_scene || (node_owner != nullptr && edited_scene->is_editable_instance(node_owner))) { - break; - } - node = node->get_parent(); - selected = Object::cast_to(node); - } - - if (!p_allow_locked) { - // Replace the node by the group if grouped - while (node && node != edited_scene->get_parent()) { - Node3D *selected_tmp = Object::cast_to(node); - if (selected_tmp && node->has_meta("_edit_group_")) { - selected = selected_tmp; - } - node = node->get_parent(); - } - } - - if (p_allow_locked || (selected != nullptr && !_is_node_locked(selected))) { - if (clicked_wants_append) { - const List &top_node_list = editor_selection->get_top_selected_node_list(); - const Node *active_node = top_node_list.is_empty() ? nullptr : top_node_list.back()->get(); - if (editor_selection->is_selected(selected)) { - editor_selection->remove_node(selected); - if (selected != active_node) { - editor_selection->add_node(selected); - } - } else { - editor_selection->add_node(selected); - } - } else { - if (!editor_selection->is_selected(selected)) { - editor_selection->clear(); - editor_selection->add_node(selected); - EditorNode::get_singleton()->edit_node(selected); - } - } - - const List &top_node_list = editor_selection->get_top_selected_node_list(); - if (top_node_list.size() == 1) { - EditorNode::get_singleton()->edit_node(top_node_list.front()->get()); - } - } -} - -ObjectID Node3DEditorViewport::_select_ray(const Point2 &p_pos) const { - Vector3 ray = get_ray(p_pos); - Vector3 pos = get_ray_pos(p_pos); - Vector2 shrinked_pos = p_pos; - - if (viewport->get_debug_draw() == Viewport::DEBUG_DRAW_SDFGI_PROBES) { - RS::get_singleton()->sdfgi_set_debug_probe_select(pos, ray); - } - - HashSet> found_gizmos; - - Node *edited_scene = get_tree()->get_edited_scene_root(); - ObjectID closest; - Node *item = nullptr; - float closest_dist = 1e20; - - Vector nodes_with_gizmos = Node3DEditor::get_singleton()->gizmo_bvh_ray_query(pos, pos + ray * camera->get_far()); - - for (Node3D *spat : nodes_with_gizmos) { - if (!spat || _is_node_locked(spat)) { - continue; - } - - Vector> gizmos = spat->get_gizmos(); - - for (int j = 0; j < gizmos.size(); j++) { - Ref seg = gizmos[j]; - - if (seg.is_null() || found_gizmos.has(seg)) { - continue; - } - - found_gizmos.insert(seg); - Vector3 point; - Vector3 normal; - - bool inters = seg->intersect_ray(camera, shrinked_pos, point, normal); - - if (!inters) { - continue; - } - - const real_t dist = pos.distance_to(point); - - if (dist < 0) { - continue; - } - - if (dist < closest_dist) { - item = Object::cast_to(spat); - if (item != edited_scene) { - item = edited_scene->get_deepest_editable_node(item); - } - - closest = item->get_instance_id(); - closest_dist = dist; - } - } - } - - if (!item) { - return ObjectID(); - } - - return closest; -} - -float Node3DEditorViewport::_min_screen_dist_to_aabb(const AABB &p_aabb, const Transform3D &p_transform, const Point2 &p_cursor) const { - Vector3 first_corner = p_transform.xform(p_aabb.get_endpoint(0)); - if (camera->is_position_behind(first_corner)) { - return 0.0f; - } - Point2 screen_min = camera->unproject_position(first_corner); - Point2 screen_max = screen_min; - - for (int i = 1; i < 8; i++) { - Vector3 world_corner = p_transform.xform(p_aabb.get_endpoint(i)); - if (camera->is_position_behind(world_corner)) { - return 0.0f; - } - Point2 s = camera->unproject_position(world_corner); - screen_min = screen_min.min(s); - screen_max = screen_max.max(s); - } - - float dx = MAX(screen_min.x - p_cursor.x, MAX(0.0f, p_cursor.x - screen_max.x)); - float dy = MAX(screen_min.y - p_cursor.y, MAX(0.0f, p_cursor.y - screen_max.y)); - return Math::sqrt(dx * dx + dy * dy); -} - -static bool _node_is_snap_source(Node *p_node, bool p_use_collision) { - if (!p_use_collision) { - return Object::cast_to(p_node); - } - if (Object::cast_to(p_node)) { - return true; - } - Node3D *n3d = Object::cast_to(p_node); - if (!n3d) { - return false; - } - for (const Ref &g : n3d->get_gizmos()) { - Ref seg = g; - if (seg.is_valid() && seg->get_collision_meshes_are_snap_source()) { - return true; - } - } - return false; -} - -static bool _node_has_snap_target(Node *p_node, bool p_use_collision) { - if (_node_is_snap_source(p_node, p_use_collision)) { - return true; - } - for (int i = 0; i < p_node->get_child_count(); i++) { - if (_node_has_snap_target(p_node->get_child(i), p_use_collision)) { - return true; - } - } - return false; -} - -bool Node3DEditorViewport::_find_closest_vertex_on_node(const Point2 &p_screen_pos, Node3D *p_node, float &r_closest_screen_dist, Vector3 &r_vertex_world) const { - bool found = false; - bool use_collision = Node3DEditor::get_singleton()->is_vertex_snap_use_collision(); - bool walk_collision_segments = use_collision && Object::cast_to(p_node); - - Transform3D gt = p_node->get_global_transform(); - Vector> gizmos = p_node->get_gizmos(); - - for (int i = 0; i < gizmos.size(); i++) { - Ref seg = gizmos[i]; - if (seg.is_null()) { - continue; - } - - if (walk_collision_segments) { - const Vector &segments = seg->get_collision_segments(); - for (int si = 0; si < segments.size(); si++) { - Vector3 world_v = gt.xform(segments[si]); - if (camera->is_position_behind(world_v)) { - continue; - } - Vector2 screen_v = camera->unproject_position(world_v); - float dist = screen_v.distance_to(p_screen_pos); - if (dist < r_closest_screen_dist) { - r_closest_screen_dist = dist; - r_vertex_world = world_v; - found = true; - } - } - } - - if (!use_collision || seg->get_collision_meshes_are_snap_source()) { - const LocalVector> &meshes = seg->get_collision_meshes(); - for (const Ref &tm : meshes) { - if (tm.is_null() || !tm->is_valid()) { - continue; - } - - const Vector &bvh = tm->get_bvh(); - const Vector &triangles = tm->get_triangles(); - const Vector &vertices = tm->get_vertices(); - - if (bvh.is_empty()) { - continue; - } - - // Traverse the TriangleMesh BVH, pruning branches whose screen-space - // AABB is farther than the current best. - LocalVector stack; - stack.push_back(bvh.size() - 1); - - while (!stack.is_empty()) { - int node_idx = stack[stack.size() - 1]; - stack.resize(stack.size() - 1); - - const TriangleMesh::BVH &b = bvh[node_idx]; - - if (_min_screen_dist_to_aabb(b.aabb, gt, p_screen_pos) >= r_closest_screen_dist) { - continue; - } - - if (b.face_index >= 0) { - const TriangleMesh::Triangle &tri = triangles[b.face_index]; - for (int vi = 0; vi < 3; vi++) { - Vector3 world_v = gt.xform(vertices[tri.indices[vi]]); - if (camera->is_position_behind(world_v)) { - continue; - } - Vector2 screen_v = camera->unproject_position(world_v); - float dist = screen_v.distance_to(p_screen_pos); - if (dist < r_closest_screen_dist) { - r_closest_screen_dist = dist; - r_vertex_world = world_v; - found = true; - } - } - } else { - stack.push_back(b.left); - stack.push_back(b.right); - } - } - } - } - } - - return found; -} - -bool Node3DEditorViewport::_find_closest_vertex_in_scene(const Point2 &p_screen_pos, float p_threshold, Vector3 &r_vertex_world, const HashMap *p_exclude) { - float closest_screen_dist = p_threshold; - bool found = false; - - Point2 min_pos(p_screen_pos.x - p_threshold, p_screen_pos.y - p_threshold); - Point2 max_pos(p_screen_pos.x + p_threshold, p_screen_pos.y + p_threshold); - Vector nodes_with_gizmos = Node3DEditor::get_singleton()->gizmo_bvh_frustum_query(_build_screen_frustum(min_pos, max_pos)); - - bool use_collision = Node3DEditor::get_singleton()->is_vertex_snap_use_collision(); - - for (Node3D *spat : nodes_with_gizmos) { - if (!spat) { - continue; - } - - if (!_node_is_snap_source(spat, use_collision)) { - continue; - } - - if (p_exclude) { - bool should_skip = false; - Node *current = spat; - while (current) { - if (p_exclude->has(current->get_instance_id())) { - should_skip = true; - break; - } - current = current->get_parent(); - } - if (should_skip) { - continue; - } - } - - if (_find_closest_vertex_on_node(p_screen_pos, spat, closest_screen_dist, r_vertex_world)) { - found = true; - } - } - - return found; -} - -void Node3DEditorViewport::_vertex_snap_commit() { - for (const KeyValue &E : vertex_snap_original_positions) { - Node3D *node = ObjectDB::get_instance(E.key); - if (node) { - vertex_snap_source += node->get_global_position() - E.value; - break; - } - } - - HashMap original_positions; - for (const KeyValue &E : vertex_snap_original_positions) { - original_positions[E.key] = E.value; - } - vertex_snap_dragging = false; - vertex_snap_has_target = false; - vertex_snap_original_positions.clear(); - if (!vertex_snap_mode) { - vertex_snap_has_source = false; - set_message(""); - } - surface->queue_redraw(); - - EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); - undo_redo->create_action(TTR("Vertex Snap")); - for (const KeyValue &E : original_positions) { - Node3D *node = ObjectDB::get_instance(E.key); - if (node) { - undo_redo->add_do_method(node, "set_global_position", node->get_global_position()); - undo_redo->add_undo_method(node, "set_global_position", E.value); - } - } - undo_redo->commit_action(false); - _reset_follow_mode_count(); -} - -void Node3DEditorViewport::_vertex_snap_cancel() { - vertex_snap_mode = false; - vertex_snap_keycode = Key::NONE; - vertex_snap_dragging = false; - vertex_snap_has_source = false; - vertex_snap_has_target = false; - - for (const KeyValue &E : vertex_snap_original_positions) { - Node3D *node = ObjectDB::get_instance(E.key); - if (node) { - node->set_global_position(E.value); - } - } - vertex_snap_original_positions.clear(); - set_message(TTR("Vertex Snap Canceled."), 3); - surface->queue_redraw(); -} - -bool Node3DEditorViewport::_is_vertex_occluded(const Vector3 &p_world_pos, const Vector2 &p_screen_pos) const { - Vector3 ray_pos = get_ray_pos(p_screen_pos); - float vertex_dist = ray_pos.distance_to(p_world_pos); - Vector hits = Node3DEditor::get_singleton()->gizmo_bvh_ray_query(ray_pos, ray_pos + get_ray(p_screen_pos) * camera->get_far()); - for (Node3D *spat : hits) { - if (!spat) { - continue; - } - Vector> gizmos = spat->get_gizmos(); - for (int i = 0; i < gizmos.size(); i++) { - Ref seg = gizmos[i]; - if (seg.is_null()) { - continue; - } - Vector3 point, normal; - if (seg->intersect_ray(camera, p_screen_pos, point, normal)) { - if (ray_pos.distance_to(point) < vertex_dist - 0.01f) { - return true; - } - } - } - } - return false; -} - -void Node3DEditorViewport::_vertex_snap_update_source(const Point2 &p_screen_pos) { - const List &selection = editor_selection->get_top_selected_node_list(); - if (selection.is_empty()) { - vertex_snap_has_source = false; - return; - } - - float threshold = VERTEX_SNAP_THRESHOLD * EDSCALE; - Vector3 vw; - bool found = false; - - if (spatial_editor->is_vertex_snap_origin_mode()) { - found = _find_closest_vertex_in_scene(p_screen_pos, threshold, vw); - } else { - bool use_collision = spatial_editor->is_vertex_snap_use_collision(); - bool selection_has_snap_target = false; - for (Node *E : selection) { - Node3D *sp = Object::cast_to(E); - if (!sp) { - continue; - } - - LocalVector descendants; - descendants.push_back(sp); - while (!descendants.is_empty()) { - Node3D *node = descendants[descendants.size() - 1]; - descendants.resize(descendants.size() - 1); - if (_node_is_snap_source(node, use_collision)) { - selection_has_snap_target = true; - if (_find_closest_vertex_on_node(p_screen_pos, node, threshold, vw)) { - found = true; - } - } - for (int i = 0; i < node->get_child_count(); i++) { - Node3D *child = Object::cast_to(node->get_child(i)); - if (child) { - descendants.push_back(child); - } - } - } - } - - if (!found && !selection_has_snap_target) { - found = _find_closest_vertex_in_scene(p_screen_pos, threshold, vw); - } - } - - if (found) { - vertex_snap_source = vw; - vertex_snap_has_source = true; - } else { - vertex_snap_has_source = false; - } -} - -void Node3DEditorViewport::_find_items_at_pos(const Point2 &p_pos, Vector<_RayResult> &r_results, bool p_include_locked_nodes) { - Vector3 ray = get_ray(p_pos); - Vector3 pos = get_ray_pos(p_pos); - - Vector nodes_with_gizmos = Node3DEditor::get_singleton()->gizmo_bvh_ray_query(pos, pos + ray * camera->get_far()); - - HashSet found_nodes; - - for (Node3D *spat : nodes_with_gizmos) { - if (!spat) { - continue; - } - - if (found_nodes.has(spat)) { - continue; - } - - if (!p_include_locked_nodes && _is_node_locked(spat)) { - continue; - } - - Vector> gizmos = spat->get_gizmos(); - for (int j = 0; j < gizmos.size(); j++) { - Ref seg = gizmos[j]; - - if (seg.is_null()) { - continue; - } - - Vector3 point; - Vector3 normal; - - bool inters = seg->intersect_ray(camera, p_pos, point, normal); - - if (!inters) { - continue; - } - - const real_t dist = pos.distance_to(point); - - if (dist < 0) { - continue; - } - - found_nodes.insert(spat); - - _RayResult res; - res.item = spat; - res.depth = dist; - r_results.push_back(res); - break; - } - } - - r_results.sort(); -} - -Vector3 Node3DEditorViewport::_get_screen_to_space(const Vector3 &p_vector3) { - Projection cm; - if (view_3d_controller->is_orthogonal()) { - cm.set_orthogonal(camera->get_size(), get_size().aspect(), get_znear() + p_vector3.z, get_zfar()); - } else { - cm.set_perspective(get_fov(), get_size().aspect(), get_znear() + p_vector3.z, get_zfar()); - } - Vector2 screen_he = cm.get_viewport_half_extents(); - - Transform3D camera_transform; - camera_transform.translate_local(view_3d_controller->cursor.pos); - camera_transform.basis.rotate(Vector3(1, 0, 0), -view_3d_controller->cursor.x_rot); - camera_transform.basis.rotate(Vector3(0, 1, 0), -view_3d_controller->cursor.y_rot); - camera_transform.translate_local(0, 0, view_3d_controller->cursor.distance); - - return camera_transform.xform(Vector3(((p_vector3.x / get_size().width) * 2.0 - 1.0) * screen_he.x, ((1.0 - (p_vector3.y / get_size().height)) * 2.0 - 1.0) * screen_he.y, -(get_znear() + p_vector3.z))); -} - -Vector Node3DEditorViewport::_build_screen_frustum(const Point2 &p_min, const Point2 &p_max) { - const real_t z_offset = MAX(0.0, 5.0 - get_znear()); - Vector3 box[4] = { - Vector3(p_min.x, p_min.y, z_offset), - Vector3(p_max.x, p_min.y, z_offset), - Vector3(p_max.x, p_max.y, z_offset), - Vector3(p_min.x, p_max.y, z_offset), - }; - - Vector frustum; - Vector3 cam_pos = _get_camera_position(); - for (int i = 0; i < 4; i++) { - Vector3 a = _get_screen_to_space(box[i]); - Vector3 b = _get_screen_to_space(box[(i + 1) % 4]); - if (view_3d_controller->is_orthogonal()) { - frustum.push_back(Plane((a - b).normalized(), a)); - } else { - frustum.push_back(Plane(a, b, cam_pos)); - } - } - Plane near_plane = Plane(-_get_camera_normal(), cam_pos); - near_plane.d -= get_znear(); - frustum.push_back(near_plane); - Plane far_plane = -near_plane; - far_plane.d += get_zfar(); - frustum.push_back(far_plane); - return frustum; -} - -void Node3DEditorViewport::_select_region() { - View3DController::Cursor cursor = view_3d_controller->cursor; - - if (cursor.region_begin == cursor.region_end) { - if (!clicked_wants_append) { - _clear_selected(); - } - return; - } - - Point2 region_min = cursor.region_begin.min(cursor.region_end); - Point2 region_max = cursor.region_begin.max(cursor.region_end); - Vector frustum = _build_screen_frustum(region_min, region_max); - - if (spatial_editor->get_single_selected_node()) { - Node3D *single_selected = spatial_editor->get_single_selected_node(); - Node3DEditorSelectedItem *se = editor_selection->get_node_editor_data(single_selected); - - if (se) { - Ref old_gizmo; - if (!clicked_wants_append) { - se->subgizmos.clear(); - old_gizmo = se->gizmo; - se->gizmo.unref(); - } - - bool found_subgizmos = false; - Vector> gizmos = single_selected->get_gizmos(); - for (int j = 0; j < gizmos.size(); j++) { - Ref seg = gizmos[j]; - if (seg.is_null()) { - continue; - } - - if (se->gizmo.is_valid() && se->gizmo != seg) { - continue; - } - - Vector subgizmos = seg->subgizmos_intersect_frustum(camera, frustum); - if (!subgizmos.is_empty()) { - se->gizmo = seg; - for (int i = 0; i < subgizmos.size(); i++) { - int subgizmo_id = subgizmos[i]; - if (!se->subgizmos.has(subgizmo_id)) { - se->subgizmos.insert(subgizmo_id, se->gizmo->get_subgizmo_transform(subgizmo_id)); - } - } - found_subgizmos = true; - break; - } - } - - if (!clicked_wants_append || found_subgizmos) { - if (se->gizmo.is_valid()) { - se->gizmo->redraw(); - } - - if (old_gizmo != se->gizmo && old_gizmo.is_valid()) { - old_gizmo->redraw(); - } - - spatial_editor->update_transform_gizmo(); - } - - if (found_subgizmos) { - return; - } - } - } - - if (!clicked_wants_append) { - _clear_selected(); - } - - Vector nodes_with_gizmos = Node3DEditor::get_singleton()->gizmo_bvh_frustum_query(frustum); - HashSet found_nodes; - Vector selected; - - Node *edited_scene = get_tree()->get_edited_scene_root(); - if (edited_scene == nullptr) { - return; - } - - for (Node3D *sp : nodes_with_gizmos) { - if (!sp || _is_node_locked(sp)) { - continue; - } - - if (found_nodes.has(sp)) { - continue; - } - found_nodes.insert(sp); - - Node *node = Object::cast_to(sp); - - // Selection requires that the node is the edited scene or its descendant, and has an owner. - if (node != edited_scene) { - if (!node->get_owner() || !edited_scene->is_ancestor_of(node)) { - continue; - } - node = edited_scene->get_deepest_editable_node(node); - while (node != edited_scene) { - Node *node_owner = node->get_owner(); - if (node_owner == edited_scene || (node_owner != nullptr && edited_scene->is_editable_instance(node_owner))) { - break; - } - node = node->get_parent(); - } - } - - // Replace the node by the group if grouped - if (node->is_class("Node3D")) { - Node3D *sel = Object::cast_to(node); - while (node && node != EditorNode::get_singleton()->get_edited_scene()->get_parent()) { - Node3D *selected_tmp = Object::cast_to(node); - if (selected_tmp && node->has_meta("_edit_group_")) { - sel = selected_tmp; - } - node = node->get_parent(); - } - node = sel; - } - - if (_is_node_locked(node)) { - continue; - } - - Vector> gizmos = sp->get_gizmos(); - for (int j = 0; j < gizmos.size(); j++) { - Ref seg = gizmos[j]; - if (seg.is_null()) { - continue; - } - - if (seg->intersect_frustum(camera, frustum)) { - selected.push_back(node); - } - } - } - - for (int i = 0; i < selected.size(); i++) { - if (!editor_selection->is_selected(selected[i])) { - editor_selection->add_node(selected[i]); - } - } - - const List &top_node_list = editor_selection->get_top_selected_node_list(); - if (top_node_list.size() == 1) { - EditorNode::get_singleton()->edit_node(top_node_list.front()->get()); - } -} - -void Node3DEditorViewport::_view_state_changed() { - if (view_3d_controller->get_orthogonal_mode() == View3DController::ORTHOGONAL_AUTO) { - _menu_option(VIEW_ORTHOGONAL); - view_3d_controller->force_auto_orthogonal(); - } else if (view_3d_controller->get_orthogonal_mode() == View3DController::ORTHOGONAL_DISABLED) { - _menu_option(VIEW_PERSPECTIVE); - } - _update_name(); -} - -void Node3DEditorViewport::_update_name() { - view_display_menu->set_text(view_3d_controller->get_view_type_name()); - view_display_menu->reset_size(); -} - -void Node3DEditorViewport::_compute_edit(const Point2 &p_point) { - _edit.original_local = spatial_editor->are_local_coords_enabled(); - _edit.click_ray = get_ray(p_point); - _edit.click_ray_pos = get_ray_pos(p_point); - _edit.plane = TRANSFORM_VIEW; - spatial_editor->update_transform_gizmo(); - _edit.center = spatial_editor->get_gizmo_transform().origin; - - Node3D *selected = spatial_editor->get_single_selected_node(); - Node3DEditorSelectedItem *se = selected ? editor_selection->get_node_editor_data(selected) : nullptr; - - if (se && se->gizmo.is_valid()) { - for (const KeyValue &E : se->subgizmos) { - int subgizmo_id = E.key; - se->subgizmos[subgizmo_id] = se->gizmo->get_subgizmo_transform(subgizmo_id); - } - se->original_local = selected->get_transform(); - se->original = selected->get_global_transform(); - } else { - const List &selection = editor_selection->get_top_selected_node_list(); - - for (Node *E : selection) { - Node3D *sp = Object::cast_to(E); - if (!sp) { - continue; - } - - Node3DEditorSelectedItem *sel_item = editor_selection->get_node_editor_data(sp); - - if (!sel_item) { - continue; - } - - sel_item->original_local = sel_item->sp->get_local_gizmo_transform(); - sel_item->original = sel_item->sp->get_global_gizmo_transform(); - } - } - - if (spatial_editor->is_preserve_children_transform_enabled() && _edit.children_original_globals.is_empty()) { - const List &selection = editor_selection->get_top_selected_node_list(); - for (Node *E : selection) { - Node3D *sp = Object::cast_to(E); - if (!sp) { - continue; - } - - int child_count = sp->get_child_count(); - for (int i = 0; i < child_count; i++) { - Node3D *child = Object::cast_to(sp->get_child(i)); - if (child) { - _edit.children_original_globals[child] = child->get_global_transform(); - } - } - } - } -} - -static Key _get_key_modifier_setting(const String &p_property) { - switch (EDITOR_GET(p_property).operator int()) { - case 0: - return Key::NONE; - case 1: - return Key::SHIFT; - case 2: - return Key::ALT; - case 3: - return Key::META; - case 4: - return Key::CTRL; - } - return Key::NONE; -} - -static Key _get_key_modifier(Ref e) { - if (e->is_shift_pressed()) { - return Key::SHIFT; - } - if (e->is_alt_pressed()) { - return Key::ALT; - } - if (e->is_ctrl_pressed()) { - return Key::CTRL; - } - if (e->is_meta_pressed()) { - return Key::META; - } - return Key::NONE; -} - -bool Node3DEditorViewport::_transform_gizmo_select(const Vector2 &p_screenpos, bool p_highlight_only) { - if (!spatial_editor->is_gizmo_visible()) { - return false; - } - if (get_selected_count() == 0) { - if (p_highlight_only) { - spatial_editor->select_gizmo_highlight_axis(-1); - } - return false; - } - - Vector3 ray_pos = get_ray_pos(p_screenpos); - Vector3 ray = get_ray(p_screenpos); - - Transform3D gt = spatial_editor->get_gizmo_transform(); - - if (spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_TRANSFORM || spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_MOVE) { - int col_axis = -1; - real_t col_d = 1e20; - - for (int i = 0; i < 3; i++) { - const Vector3 grabber_pos = gt.origin + gt.basis.get_column(i).normalized() * gizmo_scale * (GIZMO_ARROW_OFFSET + (GIZMO_ARROW_SIZE * 0.5)); - const real_t grabber_radius = gizmo_scale * GIZMO_ARROW_SIZE; - - Vector3 r; - - if (Geometry3D::segment_intersects_sphere(ray_pos, ray_pos + ray * MAX_Z, grabber_pos, grabber_radius, &r)) { - const real_t d = r.distance_to(ray_pos); - if (d < col_d) { - col_d = d; - col_axis = i; - } - } - } - - bool is_plane_translate = false; - // plane select - if (col_axis == -1) { - col_d = 1e20; - - for (int i = 0; i < 3; i++) { - Vector3 ivec2 = gt.basis.get_column((i + 1) % 3).normalized(); - Vector3 ivec3 = gt.basis.get_column((i + 2) % 3).normalized(); - - // Allow some tolerance to make the plane easier to click, - // even if the click is actually slightly outside the plane. - const Vector3 grabber_pos = gt.origin + (ivec2 + ivec3) * gizmo_scale * (GIZMO_PLANE_SIZE + GIZMO_PLANE_DST * 0.6667); - - Vector3 r; - Plane plane(gt.basis.get_column(i).normalized(), gt.origin); - - if (plane.intersects_ray(ray_pos, ray, &r)) { - const real_t dist = r.distance_to(grabber_pos); - // Allow some tolerance to make the plane easier to click, - // even if the click is actually slightly outside the plane. - if (dist < (gizmo_scale * GIZMO_PLANE_SIZE * 1.5)) { - const real_t d = ray_pos.distance_to(r); - if (d < col_d) { - col_d = d; - col_axis = i; - - is_plane_translate = true; - } - } - } - } - } - - if (col_axis != -1) { - if (p_highlight_only) { - spatial_editor->select_gizmo_highlight_axis(col_axis + (is_plane_translate ? 6 : 0)); - - } else { - //handle plane translate - _edit.mode = TRANSFORM_TRANSLATE; - _compute_edit(p_screenpos); - _edit.plane = TransformPlane(TRANSFORM_X_AXIS + col_axis + (is_plane_translate ? 3 : 0)); - } - return true; - } - } - - if (spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_TRANSFORM || spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_ROTATE) { - int col_axis = -1; - bool view_rotation_selected = false; - bool trackball_selected = false; - - Vector3 hit_position; - Vector3 hit_normal; - real_t ray_length = gt.origin.distance_to(ray_pos) + (GIZMO_CIRCLE_SIZE * gizmo_scale) * 4.0f; - if (Geometry3D::segment_intersects_sphere(ray_pos, ray_pos + ray * ray_length, gt.origin, gizmo_scale * (GIZMO_CIRCLE_SIZE), &hit_position, &hit_normal)) { - if (hit_normal.dot(_get_camera_normal()) < 0.05) { - hit_position = gt.xform_inv(hit_position).abs(); - int min_axis = hit_position.min_axis_index(); - if (hit_position[min_axis] < gizmo_scale * GIZMO_RING_HALF_WIDTH) { - col_axis = min_axis; - } - } - } - - if (col_axis == -1) { - float col_d = 1e20; - - for (int i = 0; i < 3; i++) { - Plane plane(gt.basis.get_column(i).normalized(), gt.origin); - Vector3 r; - if (!plane.intersects_ray(ray_pos, ray, &r)) { - continue; - } - - const real_t dist = r.distance_to(gt.origin); - const Vector3 r_dir = (r - gt.origin).normalized(); - - if (_get_camera_normal().dot(r_dir) <= 0.005) { - if (dist > gizmo_scale * (GIZMO_CIRCLE_SIZE - GIZMO_RING_HALF_WIDTH) && dist < gizmo_scale * (GIZMO_CIRCLE_SIZE + GIZMO_RING_HALF_WIDTH)) { - const real_t d = ray_pos.distance_to(r); - if (d < col_d) { - col_d = d; - col_axis = i; - } - } - } - } - } - - if (col_axis == -1) { - Vector3 ray_to_center = gt.origin - ray_pos; - real_t ray_length_to_center = ray_to_center.dot(ray); - Vector3 closest_point_on_ray = ray_pos + ray * ray_length_to_center; - real_t distance_ray_to_center = closest_point_on_ray.distance_to(gt.origin); - - real_t view_rotation_radius = gizmo_scale * spatial_editor->gizmo_view_rotation_scale; - real_t circumference_tolerance = gizmo_scale * GIZMO_RING_HALF_WIDTH; - - if (Math::abs(distance_ray_to_center - view_rotation_radius) < circumference_tolerance && - ray_length_to_center > 0) { - view_rotation_selected = true; - } else if (spatial_editor->is_trackball_enabled() && distance_ray_to_center < gizmo_scale * (GIZMO_CIRCLE_SIZE - GIZMO_RING_HALF_WIDTH) && ray_length_to_center > 0) { - trackball_selected = true; - } - } - - if (view_rotation_selected) { - if (p_highlight_only) { - spatial_editor->select_gizmo_highlight_axis(GIZMO_HIGHLIGHT_AXIS_VIEW_ROTATION); - } else { - _edit.mode = TRANSFORM_ROTATE; - _compute_edit(p_screenpos); - _edit.plane = TRANSFORM_VIEW; - _edit.accumulated_rotation_angle = 0.0; - _edit.rotation_angle = 0.0; - _edit.rotation_axis = _get_camera_normal(); - _edit.view_axis_local = spatial_editor->get_gizmo_transform().basis.xform_inv(_get_camera_normal()).normalized(); - _edit.gizmo_initiated = true; - } - return true; - } else if (trackball_selected) { - if (p_highlight_only) { - spatial_editor->select_gizmo_highlight_axis(GIZMO_HIGHLIGHT_AXIS_TRACKBALL); - } else { - _edit.mode = TRANSFORM_ROTATE; - _compute_edit(p_screenpos); - _edit.plane = TRANSFORM_VIEW; - _edit.is_trackball = true; - _edit.show_rotation_line = false; - _edit.accumulated_rotation_angle = 0.0; - _edit.rotation_angle = 0.0; - _edit.rotation_axis = _get_camera_normal(); - _edit.gizmo_initiated = true; - spatial_editor->select_gizmo_highlight_axis(-1); - } - return true; - } else if (col_axis != -1) { - if (p_highlight_only) { - spatial_editor->select_gizmo_highlight_axis(col_axis + 3); - } else { - //handle axis-specific rotate - _edit.mode = TRANSFORM_ROTATE; - _compute_edit(p_screenpos); - _edit.plane = TransformPlane(TRANSFORM_X_AXIS + col_axis); - _edit.accumulated_rotation_angle = 0.0; - _edit.rotation_angle = 0.0; - _edit.rotation_axis = gt.basis.get_column(col_axis).normalized(); - _edit.gizmo_initiated = true; - } - return true; - } - } - - if (spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_SCALE) { - int col_axis = -1; - float col_d = 1e20; - - for (int i = 0; i < 3; i++) { - const Vector3 grabber_pos = gt.origin + gt.basis.get_column(i).normalized() * gizmo_scale * GIZMO_SCALE_OFFSET; - const real_t grabber_radius = gizmo_scale * GIZMO_ARROW_SIZE; - - Vector3 r; - - if (Geometry3D::segment_intersects_sphere(ray_pos, ray_pos + ray * MAX_Z, grabber_pos, grabber_radius, &r)) { - const real_t d = r.distance_to(ray_pos); - if (d < col_d) { - col_d = d; - col_axis = i; - } - } - } - - bool is_plane_scale = false; - // plane select - if (col_axis == -1) { - col_d = 1e20; - - for (int i = 0; i < 3; i++) { - const Vector3 ivec2 = gt.basis.get_column((i + 1) % 3).normalized(); - const Vector3 ivec3 = gt.basis.get_column((i + 2) % 3).normalized(); - - // Allow some tolerance to make the plane easier to click, - // even if the click is actually slightly outside the plane. - const Vector3 grabber_pos = gt.origin + (ivec2 + ivec3) * gizmo_scale * (GIZMO_PLANE_SIZE + GIZMO_PLANE_DST * 0.6667); - - Vector3 r; - Plane plane(gt.basis.get_column(i).normalized(), gt.origin); - - if (plane.intersects_ray(ray_pos, ray, &r)) { - const real_t dist = r.distance_to(grabber_pos); - // Allow some tolerance to make the plane easier to click, - // even if the click is actually slightly outside the plane. - if (dist < (gizmo_scale * GIZMO_PLANE_SIZE * 1.5)) { - const real_t d = ray_pos.distance_to(r); - if (d < col_d) { - col_d = d; - col_axis = i; - - is_plane_scale = true; - } - } - } - } - } - - if (col_axis != -1) { - if (p_highlight_only) { - spatial_editor->select_gizmo_highlight_axis(col_axis + (is_plane_scale ? 12 : 9)); - - } else { - //handle scale - _edit.mode = TRANSFORM_SCALE; - _compute_edit(p_screenpos); - _edit.plane = TransformPlane(TRANSFORM_X_AXIS + col_axis + (is_plane_scale ? 3 : 0)); - } - return true; - } - } - - if (p_highlight_only) { - spatial_editor->select_gizmo_highlight_axis(-1); - } - - return false; -} - -void Node3DEditorViewport::_transform_gizmo_apply(Node3D *p_node, const Transform3D &p_transform, bool p_local) { - if (p_transform.basis.determinant() == 0) { - return; - } - - bool preserve_children = spatial_editor->is_preserve_children_transform_enabled(); - - Vector children_global_transforms; - Vector node3d_children; - - if (preserve_children) { - int child_count = p_node->get_child_count(); - for (int i = 0; i < child_count; i++) { - Node3D *child = Object::cast_to(p_node->get_child(i)); - if (child) { - children_global_transforms.push_back(child->get_global_transform()); - node3d_children.push_back(child); - } - } - } - - if (p_local) { - p_node->set_transform(p_transform); - } else { - p_node->set_global_transform(p_transform); - } - - if (preserve_children) { - for (int i = 0; i < node3d_children.size(); i++) { - node3d_children[i]->set_global_transform(children_global_transforms[i]); - } - } -} - -Transform3D Node3DEditorViewport::_compute_transform(TransformMode p_mode, const Transform3D &p_original, const Transform3D &p_original_local, Vector3 p_motion, double p_extra, bool p_local, bool p_orthogonal, bool p_view_axis) { - switch (p_mode) { - case TRANSFORM_SCALE: { - if (spatial_editor->is_snap_enabled()) { - p_motion.snapf(p_extra); - } - Transform3D s; - if (p_local) { - s.basis = p_original_local.basis.scaled_local(p_motion + Vector3(1, 1, 1)); - s.origin = p_original_local.origin; - } else { - s.basis.scale(p_motion + Vector3(1, 1, 1)); - Transform3D base = Transform3D(Basis(), _edit.center); - s = base * (s * (base.inverse() * p_original)); - - // Recalculate orthogonalized scale without moving origin. - if (p_orthogonal) { - s.basis = p_original.basis.scaled_orthogonal(p_motion + Vector3(1, 1, 1)); - } - } - - return s; - } - case TRANSFORM_TRANSLATE: { - if (spatial_editor->is_snap_enabled()) { - p_motion.snapf(p_extra); - } - - if (p_local) { - return p_original_local.translated_local(p_motion); - } - - return p_original.translated(p_motion); - } - case TRANSFORM_ROTATE: { - Transform3D r; - - Basis parent_global_basis = p_original.basis * p_original_local.basis.inverse(); - - Vector3 axis; - if (p_local && !p_view_axis) { - axis = p_original_local.basis.xform(p_motion); - } else { - axis = parent_global_basis.xform_inv(p_motion); - } - - if (p_local) { - r.basis = Basis(axis.normalized(), p_extra) * p_original_local.basis; - r.origin = p_original_local.origin; - } else { - r.basis = parent_global_basis * Basis(axis.normalized(), p_extra) * p_original_local.basis; - r.origin = Basis(p_motion, p_extra).xform(p_original.origin - _edit.center) + _edit.center; - } - - return r; - } - default: { - ERR_FAIL_V_MSG(Transform3D(), "Invalid mode in '_compute_transform'"); - } - } -} - -void Node3DEditorViewport::_reset_transform(TransformType p_type) { - List selection = editor_selection->get_full_selected_node_list(); - if (selection.is_empty()) { - return; - } - EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); - undo_redo->create_action(TTR("Reset Transform")); - for (Node *node : selection) { - Node3D *sp = Object::cast_to(node); - if (!sp) { - continue; - } - - switch (p_type) { - case TransformType::POSITION: - undo_redo->add_undo_method(sp, "set_position", sp->get_position()); - undo_redo->add_do_method(sp, "set_position", Vector3()); - break; - case TransformType::ROTATION: - undo_redo->add_undo_method(sp, "set_rotation", sp->get_rotation()); - undo_redo->add_do_method(sp, "set_rotation", Vector3()); - break; - case TransformType::SCALE: - undo_redo->add_undo_method(sp, "set_scale", sp->get_scale()); - undo_redo->add_do_method(sp, "set_scale", Vector3(1, 1, 1)); - break; - } - } - undo_redo->commit_action(); -} - -void Node3DEditorViewport::_surface_mouse_enter() { - if (Input::get_singleton()->get_mouse_mode() == Input::MouseMode::MOUSE_MODE_CAPTURED) { - return; - } - - if (!surface->has_focus() && (!get_viewport()->gui_get_focus_owner() || !get_viewport()->gui_get_focus_owner()->is_text_field())) { - surface->grab_focus(); - } -} - -void Node3DEditorViewport::_surface_mouse_exit() { - _remove_preview_node(); - _reset_preview_material(); - _remove_preview_material(); -} - -void Node3DEditorViewport::_surface_focus_enter() { - view_display_menu->set_disable_shortcuts(false); -} - -void Node3DEditorViewport::_surface_focus_exit() { - view_display_menu->set_disable_shortcuts(true); -} - -bool Node3DEditorViewport::_is_node_locked(const Node *p_node) const { - return p_node->get_meta("_edit_lock_", false); -} - -void Node3DEditorViewport::_list_select(Ref b) { - Vector<_RayResult> potential_selection_results; - _find_items_at_pos(b->get_position(), potential_selection_results, b->is_alt_pressed()); - - Node *edited_scene = EditorNode::get_singleton()->get_edited_scene(); - - // Filter to a list of nodes which include either the edited scene or nodes directly owned by the edited scene. - // If a node has an invalid owner, recursively check their parents until a valid node is found. - for (int i = 0; i < potential_selection_results.size(); i++) { - Node3D *node = potential_selection_results[i].item; - while (true) { - if (node == nullptr || node == edited_scene->get_parent()) { - break; - } else { - Node *node_owner = node->get_owner(); - if (node == edited_scene || node_owner == edited_scene || (node_owner != nullptr && edited_scene->is_editable_instance(node_owner))) { - if (!selection_results.has(node)) { - selection_results.append(node); - } - break; - } - } - node = Object::cast_to(node->get_parent()); - } - } - - clicked_wants_append = b->is_shift_pressed(); - - if (selection_results.size() == 1) { - clicked = selection_results[0]->get_instance_id(); - selection_results.clear(); - - if (clicked.is_valid()) { - _select_clicked(b->is_alt_pressed()); - } - } else if (!selection_results.is_empty()) { - NodePath root_path = get_tree()->get_edited_scene_root()->get_path(); - StringName root_name = root_path.get_name(root_path.get_name_count() - 1); - int icon_max_width = EditorNode::get_singleton()->get_editor_theme()->get_constant(SNAME("class_icon_size"), EditorStringName(Editor)); - - for (int i = 0; i < selection_results.size(); i++) { - Node3D *spat = selection_results[i]; - - Ref icon = EditorNode::get_singleton()->get_object_icon(spat); - - String node_path = "/" + root_name + "/" + String(root_path.rel_path_to(spat->get_path())); - - int locked = 0; - if (_is_node_locked(spat)) { - locked = 1; - } else { - Node *ed_scene = EditorNode::get_singleton()->get_edited_scene(); - Node *node = spat; - - while (node && node != ed_scene->get_parent()) { - Node3D *selected_tmp = Object::cast_to(node); - if (selected_tmp && node->has_meta("_edit_group_")) { - locked = 2; - } - node = node->get_parent(); - } - } - - String suffix; - if (locked == 1) { - suffix = " (" + TTR("Locked") + ")"; - } else if (locked == 2) { - suffix = " (" + TTR("Grouped") + ")"; - } - selection_menu->add_item((String)spat->get_name() + suffix); - selection_menu->set_item_icon(i, icon); - selection_menu->set_item_icon_max_width(i, icon_max_width); - selection_menu->set_item_metadata(i, node_path); - selection_menu->set_item_tooltip(i, String(spat->get_name()) + "\nType: " + spat->get_class() + "\nPath: " + node_path); - } - - selection_results_menu = selection_results; - selection_menu->set_position(get_screen_position() + b->get_position()); - selection_menu->reset_size(); - selection_menu->popup(); - } -} - -// Helper function to redirect mouse events to the active freelook viewport -static bool _redirect_freelook_input(const Ref &p_event, Node3DEditorViewport *p_exclude_viewport = nullptr) { - if (Input::get_singleton()->get_mouse_mode() != Input::MouseMode::MOUSE_MODE_CAPTURED) { - return false; - } - - Node3DEditor *editor = Node3DEditor::get_singleton(); - if (!editor->get_freelook_viewport()) { - return false; - } - - Node3DEditorViewport *freelook_vp = editor->get_freelook_viewport(); - if (freelook_vp == p_exclude_viewport) { - return false; - } - - Ref mouse_event = p_event; - if (!mouse_event.is_valid()) { - return false; - } - - Control *target_surface = freelook_vp->get_surface(); - - target_surface->emit_signal(SceneStringName(gui_input), p_event); - return true; -} - -// This is only active during instant transforms, -// to capture and wrap mouse events outside the control. -void Node3DEditorViewport::input(const Ref &p_event) { - ERR_FAIL_COND(!_edit.instant); - Ref m = p_event; - - if (m.is_valid()) { - _edit.mouse_pos += view_3d_controller->get_warped_mouse_motion(p_event, surface->get_global_rect()); - update_transform(_get_key_modifier(m) == Key::SHIFT); - } -} - -void Node3DEditorViewport::_sinput(const Ref &p_event) { - const Ref k = p_event; - - if (k.is_valid() && k->is_pressed()) { - const Key code = view_3d_controller->emulate_numpad_key(k->get_physical_keycode()); - if (code != k->get_physical_keycode()) { - k->set_keycode(code); - } - } - - if (get_viewport()->gui_get_drag_data()) { - // Disable all input actions during drag-and-drop. - return; - } - - if (k.is_valid()) { - Ref k_no_shift = k->duplicate(); - k_no_shift->set_shift_pressed(false); - if (!vertex_snap_mode && !vertex_snap_dragging && k->is_pressed() && _edit.mode == TRANSFORM_NONE && ED_IS_SHORTCUT("spatial_editor/vertex_snap", k_no_shift)) { - vertex_snap_mode = true; - vertex_snap_keycode = k->get_physical_keycode() != Key::NONE ? k->get_physical_keycode() : k->get_keycode(); - _disable_follow_mode(); - set_message(TTR("Vertex Snap")); - _vertex_snap_update_source(_edit.mouse_pos); - surface->queue_redraw(); - } else if (vertex_snap_mode && !k->is_pressed() && k->get_physical_keycode() == vertex_snap_keycode) { - vertex_snap_mode = false; - vertex_snap_keycode = Key::NONE; - if (!vertex_snap_dragging) { - vertex_snap_has_source = false; - vertex_snap_has_target = false; - set_message(""); - surface->queue_redraw(); - } - } else if ((vertex_snap_mode || vertex_snap_dragging) && k->is_pressed() && k->get_keycode() == Key::ESCAPE) { - if (vertex_snap_dragging) { - _vertex_snap_cancel(); - } else { - vertex_snap_has_source = false; - surface->queue_redraw(); - } - vertex_snap_mode = false; - vertex_snap_keycode = Key::NONE; - set_message(""); - return; - } - } - - if (_redirect_freelook_input(p_event, this)) { - return; - } - - { - Ref vb = p_event; - if ((vertex_snap_mode || vertex_snap_dragging) && vb.is_valid()) { - if (vb->get_button_index() == MouseButton::RIGHT && vb->is_pressed() && vertex_snap_dragging) { - _vertex_snap_cancel(); - return; - } - if (vb->get_button_index() == MouseButton::LEFT) { - if (vb->is_pressed()) { - const List &selection = editor_selection->get_top_selected_node_list(); - bool use_origin_snap = spatial_editor->is_vertex_snap_origin_mode(); - - Node3D *selected = spatial_editor->get_single_selected_node(); - Node3DEditorSelectedItem *se = selected ? editor_selection->get_node_editor_data(selected) : nullptr; - bool has_subgizmos = se && se->gizmo.is_valid() && !se->subgizmos.is_empty(); - - if (!use_origin_snap) { - bool has_snap_target = false; - for (Node *E : selection) { - if (_node_has_snap_target(E, spatial_editor->is_vertex_snap_use_collision())) { - has_snap_target = true; - break; - } - } - if (!has_snap_target) { - use_origin_snap = true; - } - } - - if (use_origin_snap || !vertex_snap_has_source) { - _vertex_snap_update_source(vb->get_position()); - } - - if (vertex_snap_has_source && !selection.is_empty()) { - vertex_snap_original_positions.clear(); - for (Node *E : selection) { - Node3D *sp = Object::cast_to(E); - if (sp) { - vertex_snap_original_positions[sp->get_instance_id()] = sp->get_global_position(); - } - } - - if (!use_origin_snap) { - vertex_snap_dragging = true; - Vector3 cam_normal = camera->get_global_transform().basis.get_column(2); - vertex_snap_drag_plane = Plane(cam_normal, vertex_snap_source); - } else { - if (has_subgizmos) { - Transform3D gi = selected->get_global_transform().affine_inverse(); - Vector3 local_target = gi.xform(vertex_snap_source); - - Vector ids; - Vector restores; - for (const KeyValue &GE : se->subgizmos) { - Transform3D original_xform = se->gizmo->get_subgizmo_transform(GE.key); - ids.push_back(GE.key); - restores.push_back(original_xform); - - Transform3D snapped_xform = original_xform; - snapped_xform.origin = local_target; - se->gizmo->set_subgizmo_transform(GE.key, snapped_xform); - } - se->gizmo->commit_subgizmos(ids, restores, false); - } else { - EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); - undo_redo->create_action(TTR("Vertex Snap")); - for (const KeyValue &E2 : vertex_snap_original_positions) { - Node3D *node = ObjectDB::get_instance(E2.key); - if (node) { - node->set_global_position(vertex_snap_source); - undo_redo->add_do_method(node, "set_global_position", vertex_snap_source); - undo_redo->add_undo_method(node, "set_global_position", E2.value); - } - } - undo_redo->commit_action(false); - } - vertex_snap_original_positions.clear(); - spatial_editor->update_transform_gizmo(); - _reset_follow_mode_count(); - } - surface->queue_redraw(); - } - } else { - if (vertex_snap_dragging) { - _vertex_snap_commit(); - } - } - return; - } - } - } - - EditorPlugin::AfterGUIInput after = EditorPlugin::AFTER_GUI_INPUT_PASS; - { - EditorNode *en = EditorNode::get_singleton(); - Camera3D *input_camera = previewing ? previewing : camera; - - switch (en->get_editor_plugins_force_input_forwarding()->forward_3d_gui_input(input_camera, p_event, true)) { - case EditorPlugin::AFTER_GUI_INPUT_PASS: { - // Continue processing. - } break; - - case EditorPlugin::AFTER_GUI_INPUT_STOP: { - return; // Stop processing. - } break; - - case EditorPlugin::AFTER_GUI_INPUT_CUSTOM: { - after = EditorPlugin::AFTER_GUI_INPUT_CUSTOM; - } break; - } - - switch (en->get_editor_plugins_over()->forward_3d_gui_input(input_camera, p_event, false)) { - case EditorPlugin::AFTER_GUI_INPUT_PASS: { - // Continue processing. - } break; - - case EditorPlugin::AFTER_GUI_INPUT_STOP: { - return; // Stop processing. - } break; - - case EditorPlugin::AFTER_GUI_INPUT_CUSTOM: { - after = EditorPlugin::AFTER_GUI_INPUT_CUSTOM; - } break; - } - } - - // Several parts of the 3D navigation are handled here. - bool was_navigating = view_3d_controller->is_navigating(); - view_3d_controller->gui_input(p_event, surface->get_global_rect()); - if (was_navigating && !view_3d_controller->is_navigating()) { - return; - } - - Ref b = p_event; - - if (b.is_valid()) { - emit_signal(SNAME("clicked")); - - View3DController::NavigationMouseButton orbit_mouse_preference = (View3DController::NavigationMouseButton)EDITOR_GET("editors/3d/navigation/orbit_mouse_button").operator int(); - View3DController::NavigationMouseButton pan_mouse_preference = (View3DController::NavigationMouseButton)EDITOR_GET("editors/3d/navigation/pan_mouse_button").operator int(); - View3DController::NavigationMouseButton zoom_mouse_preference = (View3DController::NavigationMouseButton)EDITOR_GET("editors/3d/navigation/zoom_mouse_button").operator int(); - - switch (b->get_button_index()) { - case MouseButton::RIGHT: { - if (b->is_pressed()) { - if (_edit.gizmo.is_valid()) { - // Restore. - _edit.gizmo->commit_handle(_edit.gizmo_handle, _edit.gizmo_handle_secondary, _edit.gizmo_initial_value, true); - _edit.gizmo = Ref(); - set_message(""); - } - - if (_edit.mode == TRANSFORM_NONE) { - if (orbit_mouse_preference == View3DController::NAV_MOUSE_BUTTON_RIGHT && _is_nav_modifier_pressed("spatial_editor/viewport_orbit_modifier_1") && _is_nav_modifier_pressed("spatial_editor/viewport_orbit_modifier_2")) { - break; - } else if (pan_mouse_preference == View3DController::NAV_MOUSE_BUTTON_RIGHT && _is_nav_modifier_pressed("spatial_editor/viewport_pan_modifier_1") && _is_nav_modifier_pressed("spatial_editor/viewport_pan_modifier_2")) { - break; - } else if (zoom_mouse_preference == View3DController::NAV_MOUSE_BUTTON_RIGHT && _is_nav_modifier_pressed("spatial_editor/viewport_zoom_modifier_1") && _is_nav_modifier_pressed("spatial_editor/viewport_zoom_modifier_2")) { - break; - } - } - - if (b->is_alt_pressed()) { - _list_select(b); - return; - } - - if (_edit.mode != TRANSFORM_NONE) { - cancel_transform(); - break; - } - - const Key mod = _get_key_modifier(b); - if (!view_3d_controller->is_orthogonal() && !(previewing && !pilot_preview_enabled)) { - if (mod == _get_key_modifier_setting("editors/3d/freelook/freelook_activation_modifier")) { - view_3d_controller->set_freelook_enabled(true); - } - } - } else { - view_3d_controller->set_freelook_enabled(false); - } - - if (view_3d_controller->is_freelook_enabled() && !surface->has_focus()) { - // Focus usually doesn't trigger on right-click, but in case of freelook it should, - // otherwise using keyboard navigation would misbehave - surface->grab_focus(); - } - - } break; - case MouseButton::MIDDLE: { - if (b->is_pressed() && _edit.mode != TRANSFORM_NONE) { - if (orbit_mouse_preference == View3DController::NAV_MOUSE_BUTTON_MIDDLE && _is_nav_modifier_pressed("spatial_editor/viewport_orbit_modifier_1") && _is_nav_modifier_pressed("spatial_editor/viewport_orbit_modifier_2")) { - break; - } else if (pan_mouse_preference == View3DController::NAV_MOUSE_BUTTON_MIDDLE && _is_nav_modifier_pressed("spatial_editor/viewport_pan_modifier_1") && _is_nav_modifier_pressed("spatial_editor/viewport_pan_modifier_2")) { - break; - } else if (zoom_mouse_preference == View3DController::NAV_MOUSE_BUTTON_MIDDLE && _is_nav_modifier_pressed("spatial_editor/viewport_zoom_modifier_1") && _is_nav_modifier_pressed("spatial_editor/viewport_zoom_modifier_2")) { - break; - } - - switch (_edit.plane) { - case TRANSFORM_VIEW: { - _edit.plane = TRANSFORM_X_AXIS; - set_message(TTR("X-Axis Transform."), 2); - view_3d_controller->set_view_type(View3DController::VIEW_TYPE_USER); - } break; - case TRANSFORM_X_AXIS: { - _edit.plane = TRANSFORM_Y_AXIS; - set_message(TTR("Y-Axis Transform."), 2); - - } break; - case TRANSFORM_Y_AXIS: { - _edit.plane = TRANSFORM_Z_AXIS; - set_message(TTR("Z-Axis Transform."), 2); - - } break; - case TRANSFORM_Z_AXIS: { - _edit.plane = TRANSFORM_VIEW; - // TRANSLATORS: This refers to the transform of the view plane. - set_message(TTR("View Plane Transform."), 2); - - } break; - case TRANSFORM_YZ: - case TRANSFORM_XZ: - case TRANSFORM_XY: { - } break; - } - } - } break; - case MouseButton::LEFT: { - if (b->is_pressed()) { - clicked_wants_append = b->is_shift_pressed(); - - if (_edit.mode != TRANSFORM_NONE && (_edit.instant || collision_reposition)) { - commit_transform(); - break; // just commit the edit, stop processing the event so we don't deselect the object - } - if (orbit_mouse_preference == View3DController::NAV_MOUSE_BUTTON_LEFT && _is_nav_modifier_pressed("spatial_editor/viewport_orbit_modifier_1") && _is_nav_modifier_pressed("spatial_editor/viewport_orbit_modifier_2")) { - break; - } else if (pan_mouse_preference == View3DController::NAV_MOUSE_BUTTON_LEFT && _is_nav_modifier_pressed("spatial_editor/viewport_pan_modifier_1") && _is_nav_modifier_pressed("spatial_editor/viewport_pan_modifier_2")) { - break; - } else if (zoom_mouse_preference == View3DController::NAV_MOUSE_BUTTON_LEFT && _is_nav_modifier_pressed("spatial_editor/viewport_zoom_modifier_1") && _is_nav_modifier_pressed("spatial_editor/viewport_zoom_modifier_2")) { - break; - } - - if (spatial_editor->get_tool_mode() == Node3DEditor::TOOL_RULER) { - EditorNode::get_singleton()->get_scene_root()->add_child(ruler); - collision_reposition = true; - break; - } - - if (spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_LIST_SELECT) { - _list_select(b); - break; - } - - _edit.mouse_pos = b->get_position(); - _edit.original_mouse_pos = b->get_position(); - _edit.mode = TRANSFORM_NONE; - _edit.original = spatial_editor->get_gizmo_transform(); // To prevent to break when flipping with scale. - - bool can_select_gizmos = spatial_editor->get_single_selected_node(); - - { - int idx = view_display_menu->get_popup()->get_item_index(VIEW_GIZMOS); - int idx2 = view_display_menu->get_popup()->get_item_index(VIEW_TRANSFORM_GIZMO); - can_select_gizmos = can_select_gizmos && view_display_menu->get_popup()->is_item_checked(idx); - transform_gizmo_visible = view_display_menu->get_popup()->is_item_checked(idx2); - } - - // Gizmo handles - if (can_select_gizmos) { - Vector> gizmos = spatial_editor->get_single_selected_node()->get_gizmos(); - - bool intersected_handle = false; - for (int i = 0; i < gizmos.size(); i++) { - Ref seg = gizmos[i]; - - if (seg.is_null()) { - continue; - } - - int gizmo_handle = -1; - bool gizmo_secondary = false; - seg->handles_intersect_ray(camera, _edit.mouse_pos, b->is_shift_pressed(), gizmo_handle, gizmo_secondary); - if (gizmo_handle != -1) { - _edit.gizmo = seg; - seg->begin_handle_action(gizmo_handle, gizmo_secondary); - _edit.gizmo_handle = gizmo_handle; - _edit.gizmo_handle_secondary = gizmo_secondary; - _edit.gizmo_initial_value = seg->get_handle_value(gizmo_handle, gizmo_secondary); - intersected_handle = true; - break; - } - } - - if (intersected_handle) { - break; - } - } - - // Transform gizmo - if (transform_gizmo_visible && _transform_gizmo_select(_edit.mouse_pos)) { - break; - } - - // Subgizmos - if (can_select_gizmos) { - Node3DEditorSelectedItem *se = editor_selection->get_node_editor_data(spatial_editor->get_single_selected_node()); - Vector> gizmos = spatial_editor->get_single_selected_node()->get_gizmos(); - - bool intersected_subgizmo = false; - for (int i = 0; i < gizmos.size(); i++) { - Ref seg = gizmos[i]; - - if (seg.is_null()) { - continue; - } - - int subgizmo_id = seg->subgizmos_intersect_ray(camera, _edit.mouse_pos); - if (subgizmo_id != -1) { - ERR_CONTINUE(!se); - if (b->is_shift_pressed()) { - if (se->subgizmos.has(subgizmo_id)) { - se->subgizmos.erase(subgizmo_id); - } else { - se->subgizmos.insert(subgizmo_id, seg->get_subgizmo_transform(subgizmo_id)); - } - } else { - se->subgizmos.clear(); - se->subgizmos.insert(subgizmo_id, seg->get_subgizmo_transform(subgizmo_id)); - } - - if (se->subgizmos.is_empty()) { - se->gizmo = Ref(); - } else { - se->gizmo = seg; - } - - seg->redraw(); - spatial_editor->update_transform_gizmo(); - _reset_follow_mode_count(); - intersected_subgizmo = true; - break; - } - } - - if (intersected_subgizmo) { - break; - } - } - - clicked = ObjectID(); - - bool node_selected = get_selected_count() > 0; - - if (after != EditorPlugin::AFTER_GUI_INPUT_CUSTOM) { - // Single item selection. - clicked = _select_ray(b->get_position()); - - if (clicked.is_valid() && !editor_selection->is_selected(ObjectDB::get_instance(clicked))) { - if (!node_selected || (!b->is_alt_pressed() && !(spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_TRANSFORM && b->is_command_or_control_pressed()))) { - selection_in_progress = true; - break; - } - } - - if (clicked.is_null()) { - if (node_selected) { - TransformMode mode = TRANSFORM_NONE; - - if (spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_TRANSFORM) { - if (b->is_command_or_control_pressed()) { - mode = TRANSFORM_ROTATE; - } else if (b->is_alt_pressed()) { - mode = TRANSFORM_TRANSLATE; - } - } else if (b->is_alt_pressed()) { - switch (spatial_editor->get_tool_mode()) { - case Node3DEditor::TOOL_MODE_ROTATE: - mode = TRANSFORM_ROTATE; - break; - case Node3DEditor::TOOL_MODE_MOVE: - mode = TRANSFORM_TRANSLATE; - break; - case Node3DEditor::TOOL_MODE_SCALE: - mode = TRANSFORM_SCALE; - break; - default: - break; - } - } - - if (mode != TRANSFORM_NONE) { - begin_transform(mode, false); - break; - } - } - - if (!previewing) { - // Default to region select. - view_3d_controller->cursor.region_select = true; - view_3d_controller->cursor.region_begin = b->get_position(); - view_3d_controller->cursor.region_end = b->get_position(); - } - break; - } - } - - if (clicked.is_valid() && !clicked_wants_append) { - bool is_clicked_node_selected = editor_selection->is_selected(ObjectDB::get_instance(clicked)); - TransformMode mode = TRANSFORM_NONE; - - switch (spatial_editor->get_tool_mode()) { - case Node3DEditor::TOOL_MODE_TRANSFORM: - if (b->is_command_or_control_pressed() && node_selected) { - mode = TRANSFORM_ROTATE; - } else if (b->is_alt_pressed() && node_selected) { - mode = TRANSFORM_TRANSLATE; - } else if (is_clicked_node_selected) { - mode = TRANSFORM_TRANSLATE; - } - break; - case Node3DEditor::TOOL_MODE_ROTATE: - if (is_clicked_node_selected || (b->is_alt_pressed() && node_selected)) { - mode = TRANSFORM_ROTATE; - } - break; - case Node3DEditor::TOOL_MODE_MOVE: - if (is_clicked_node_selected || (b->is_alt_pressed() && node_selected)) { - mode = TRANSFORM_TRANSLATE; - } - break; - case Node3DEditor::TOOL_MODE_SCALE: - if (is_clicked_node_selected || (b->is_alt_pressed() && node_selected)) { - mode = TRANSFORM_SCALE; - } - break; - default: - break; - } - - if (mode != TRANSFORM_NONE) { - begin_transform(mode, false); - break; - } - } - - surface->queue_redraw(); - } else { - if (ruler->is_inside_tree()) { - EditorNode::get_singleton()->get_scene_root()->remove_child(ruler); - ruler_start_point->set_visible(false); - ruler_end_point->set_visible(false); - ruler_label->set_visible(false); - ruler_label_x->set_visible(false); - ruler_label_y->set_visible(false); - ruler_label_z->set_visible(false); - collision_reposition = false; - break; - } - - if (_edit.gizmo.is_valid()) { - // Certain gizmo plugins should be able to commit handles without dragging them. - if (_edit.original_mouse_pos != _edit.mouse_pos || _edit.gizmo->get_plugin()->can_commit_handle_on_click()) { - _edit.gizmo->commit_handle(_edit.gizmo_handle, _edit.gizmo_handle_secondary, _edit.gizmo_initial_value, false); - } - Node3D *selected_node = spatial_editor->get_single_selected_node(); - if (selected_node) { - selected_node->update_gizmos(); - } - _edit.gizmo = Ref(); - set_message(""); - break; - } - - if (after != EditorPlugin::AFTER_GUI_INPUT_CUSTOM) { - selection_in_progress = false; - - if (clicked.is_valid() && _edit.mode == TRANSFORM_NONE) { - _select_clicked(false); - } else if (view_3d_controller->cursor.region_select) { - _select_region(); - surface->queue_redraw(); - } - - movement_threshold_passed = false; - view_3d_controller->cursor.region_select = false; - } - - if (!_edit.instant && _edit.mode != TRANSFORM_NONE) { - Node3D *selected = spatial_editor->get_single_selected_node(); - Node3DEditorSelectedItem *se = selected ? editor_selection->get_node_editor_data(selected) : nullptr; - - if (se && se->gizmo.is_valid()) { - Vector ids; - Vector restore; - - for (const KeyValue &GE : se->subgizmos) { - ids.push_back(GE.key); - restore.push_back(GE.value); - } - - se->gizmo->commit_subgizmos(ids, restore, false); - finish_transform(); - } else { - if (_edit.original_mouse_pos != _edit.mouse_pos) { - commit_transform(); - } - } - _edit.mode = TRANSFORM_NONE; - set_message(""); - spatial_editor->update_transform_gizmo(); - } - } - - } break; - default: - break; - } - } - - Ref m = p_event; - - // Instant transforms process mouse motion in input() to handle wrapping. - if (m.is_valid() && !_edit.instant) { - _edit.mouse_pos = m->get_position(); - - if (vertex_snap_mode || vertex_snap_dragging) { - if (!vertex_snap_dragging) { - _vertex_snap_update_source(m->get_position()); - } else { - const float vertex_snap_threshold = VERTEX_SNAP_THRESHOLD * EDSCALE; - bool has_displacement = false; - Vector3 displacement; - - Vector3 target; - if (_find_closest_vertex_in_scene(m->get_position(), vertex_snap_threshold, target, &vertex_snap_original_positions)) { - vertex_snap_target = target; - vertex_snap_has_target = true; - displacement = target - vertex_snap_source; - has_displacement = true; - set_message(TTR("Vertex Snap (Snapped)")); - } else { - vertex_snap_has_target = false; - Vector3 ray_pos = get_ray_pos(m->get_position()); - Vector3 ray_dir = get_ray(m->get_position()); - Vector3 intersection; - if (vertex_snap_drag_plane.intersects_ray(ray_pos, ray_dir, &intersection)) { - displacement = intersection - vertex_snap_source; - has_displacement = true; - } - set_message(TTR("Vertex Snap")); - } - - if (has_displacement) { - for (const KeyValue &E : vertex_snap_original_positions) { - Node3D *node = ObjectDB::get_instance(E.key); - if (node) { - node->set_global_position(E.value + displacement); - } - } - } - } - surface->queue_redraw(); - return; - } - - if (!view_3d_controller->is_freelook_enabled() && spatial_editor->get_single_selected_node()) { - Vector> gizmos = spatial_editor->get_single_selected_node()->get_gizmos(); - - Ref found_gizmo; - int found_handle = -1; - bool found_handle_secondary = false; - - for (int i = 0; i < gizmos.size(); i++) { - Ref seg = gizmos[i]; - if (seg.is_null()) { - continue; - } - - seg->handles_intersect_ray(camera, _edit.mouse_pos, false, found_handle, found_handle_secondary); - - if (found_handle != -1) { - found_gizmo = seg; - break; - } - } - - if (found_gizmo.is_valid()) { - spatial_editor->select_gizmo_highlight_axis(-1); - } - - bool current_hover_handle_secondary = false; - int current_hover_handle = spatial_editor->get_current_hover_gizmo_handle(current_hover_handle_secondary); - if (found_gizmo != spatial_editor->get_current_hover_gizmo() || found_handle != current_hover_handle || found_handle_secondary != current_hover_handle_secondary) { - spatial_editor->set_current_hover_gizmo(found_gizmo); - spatial_editor->set_current_hover_gizmo_handle(found_handle, found_handle_secondary); - spatial_editor->get_single_selected_node()->update_gizmos(); - } - } - - if (!view_3d_controller->is_freelook_enabled() && transform_gizmo_visible && spatial_editor->get_current_hover_gizmo().is_null() && !m->get_button_mask().has_flag(MouseButtonMask::LEFT) && _edit.gizmo.is_null()) { - _transform_gizmo_select(_edit.mouse_pos, true); - } - - if (_edit.gizmo.is_valid()) { - _edit.gizmo->set_handle(_edit.gizmo_handle, _edit.gizmo_handle_secondary, camera, m->get_position()); - Variant v = _edit.gizmo->get_handle_value(_edit.gizmo_handle, _edit.gizmo_handle_secondary); - String n = _edit.gizmo->get_handle_name(_edit.gizmo_handle, _edit.gizmo_handle_secondary); - set_message(n + ": " + String(v)); - - } else if (m->get_button_mask().has_flag(MouseButtonMask::LEFT)) { - movement_threshold_passed = _edit.original_mouse_pos.distance_to(_edit.mouse_pos) > 8 * EDSCALE; - - if ((selection_in_progress || clicked_wants_append || spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_SELECT) && movement_threshold_passed && clicked.is_valid() && !previewing) { - view_3d_controller->cursor.region_select = true; - view_3d_controller->cursor.region_begin = _edit.original_mouse_pos; - clicked = ObjectID(); - } - - if (view_3d_controller->cursor.region_select) { - view_3d_controller->cursor.region_end = m->get_position(); - surface->queue_redraw(); - return; - } - - if (clicked.is_valid() && movement_threshold_passed && (spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_TRANSFORM || spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_MOVE)) { - bool is_select_mode = (spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_TRANSFORM); - bool is_clicked_selected = editor_selection->is_selected(ObjectDB::get_instance(clicked)); - - if (_edit.mode == TRANSFORM_NONE && (is_select_mode || is_clicked_selected) && _has_unlocked_selection()) { - _compute_edit(_edit.original_mouse_pos); - clicked = ObjectID(); - _edit.mode = TRANSFORM_TRANSLATE; - } - } - - if (_edit.mode == TRANSFORM_NONE || _edit.numeric_input != 0 || _edit.numeric_next_decimal != 0) { - return; - } - - if (!selection_in_progress) { - update_transform(_get_key_modifier(m) == Key::SHIFT); - } - } - } - - if (k.is_valid()) { - if (!k->is_pressed()) { - return; - } - - if (_edit.instant) { - // In a Blender-style transform, numbers set the magnitude of the transform. - // E.g. pressing g4.5x means "translate 4.5 units along the X axis". - bool processed = true; - Key keycode = k->get_keycode(); - Key physical_keycode = k->get_physical_keycode(); - - // Use physical keycode for main keyboard numbers (for non-QWERTY layouts like AZERTY) - // but regular keycode for numpad numbers. - if ((physical_keycode >= Key::KEY_0 && physical_keycode <= Key::KEY_9) || (keycode >= Key::KP_0 && keycode <= Key::KP_9)) { - uint32_t value; - if (physical_keycode >= Key::KEY_0 && physical_keycode <= Key::KEY_9) { - value = uint32_t(physical_keycode - Key::KEY_0); - } else { - value = uint32_t(keycode - Key::KP_0); - } - - if (_edit.numeric_next_decimal < 0) { - _edit.numeric_input = _edit.numeric_input + value * Math::pow(10.0, _edit.numeric_next_decimal--); - } else { - _edit.numeric_input = _edit.numeric_input * 10 + value; - } - update_transform_numeric(); - } else if (keycode == Key::MINUS || keycode == Key::KP_SUBTRACT) { - _edit.numeric_negate = !_edit.numeric_negate; - update_transform_numeric(); - } else if (keycode == Key::PERIOD || physical_keycode == Key::KP_PERIOD) { - // Use physical keycode for KP_PERIOD to ensure numpad period works consistently - // across different keyboard layouts (like nordic keyboards). - if (_edit.numeric_next_decimal == 0) { - _edit.numeric_next_decimal = -1; - } - } else if (keycode == Key::ENTER || keycode == Key::KP_ENTER || keycode == Key::SPACE) { - commit_transform(); - } else { - processed = false; - } - - if (processed) { - // Ignore mouse inputs once we receive a numeric input. - set_process_input(false); - accept_event(); - return; - } - } - - Ref event_mod = p_event; - const Key code = view_3d_controller->emulate_numpad_key(k->get_physical_keycode()); - if (code != k->get_physical_keycode()) { - event_mod = p_event->duplicate(); - Ref k_mod = event_mod; - k_mod->set_keycode(code); - } - - if (_edit.mode == TRANSFORM_NONE) { - if (_edit.gizmo.is_null() && view_3d_controller->is_freelook_enabled() && k->get_keycode() == Key::ESCAPE) { - view_3d_controller->set_freelook_enabled(false); - return; - } - - if (_edit.gizmo.is_valid() && (k->get_keycode() == Key::ESCAPE || k->get_keycode() == Key::BACKSPACE)) { - // Restore. - _edit.gizmo->commit_handle(_edit.gizmo_handle, _edit.gizmo_handle_secondary, _edit.gizmo_initial_value, true); - _edit.gizmo = Ref(); - set_message(""); - } - if (k->get_keycode() == Key::ESCAPE && !view_3d_controller->cursor.region_select && !k->is_echo()) { - _clear_selected(); - return; - } - } else { - // We're actively transforming, handle keys specially - TransformPlane new_plane = TRANSFORM_VIEW; - if (ED_IS_SHORTCUT("spatial_editor/lock_transform_x", event_mod)) { - new_plane = TRANSFORM_X_AXIS; - } else if (ED_IS_SHORTCUT("spatial_editor/lock_transform_y", event_mod)) { - new_plane = TRANSFORM_Y_AXIS; - } else if (ED_IS_SHORTCUT("spatial_editor/lock_transform_z", event_mod)) { - new_plane = TRANSFORM_Z_AXIS; - } else if (_edit.mode != TRANSFORM_ROTATE) { // rotating on a plane doesn't make sense - if (ED_IS_SHORTCUT("spatial_editor/lock_transform_yz", event_mod)) { - new_plane = TRANSFORM_YZ; - } else if (ED_IS_SHORTCUT("spatial_editor/lock_transform_xz", event_mod)) { - new_plane = TRANSFORM_XZ; - } else if (ED_IS_SHORTCUT("spatial_editor/lock_transform_xy", event_mod)) { - new_plane = TRANSFORM_XY; - } - } - - if (new_plane != TRANSFORM_VIEW) { - if (new_plane != _edit.plane) { - // lock me once and get a global constraint - _edit.plane = new_plane; - spatial_editor->set_local_coords_enabled(false); - } else if (!spatial_editor->are_local_coords_enabled()) { - // lock me twice and get a local constraint - spatial_editor->set_local_coords_enabled(true); - } else { - // lock me thrice and we're back where we started - _edit.plane = TRANSFORM_VIEW; - spatial_editor->set_local_coords_enabled(false); - } - if (_edit.numeric_input != 0 || _edit.numeric_next_decimal != 0) { - update_transform_numeric(); - } else { - update_transform(Input::get_singleton()->is_key_pressed(Key::SHIFT)); - } - accept_event(); - return; - } - } - if (ED_IS_SHORTCUT("spatial_editor/bottom_view", event_mod)) { - _menu_option(VIEW_BOTTOM); - } - if (ED_IS_SHORTCUT("spatial_editor/top_view", event_mod)) { - _menu_option(VIEW_TOP); - } - if (ED_IS_SHORTCUT("spatial_editor/rear_view", event_mod)) { - _menu_option(VIEW_REAR); - } - if (ED_IS_SHORTCUT("spatial_editor/front_view", event_mod)) { - _menu_option(VIEW_FRONT); - } - if (ED_IS_SHORTCUT("spatial_editor/left_view", event_mod)) { - _menu_option(VIEW_LEFT); - } - if (ED_IS_SHORTCUT("spatial_editor/right_view", event_mod)) { - _menu_option(VIEW_RIGHT); - } - if (ED_IS_SHORTCUT("spatial_editor/orbit_view_down", event_mod)) { - // Clamp rotation to roughly -90..90 degrees so the user can't look upside-down and end up disoriented. - view_3d_controller->cursor.x_rot = CLAMP(view_3d_controller->cursor.x_rot - Math::PI / 12.0, -1.57, 1.57); - view_3d_controller->cursor.unsnapped_x_rot = view_3d_controller->cursor.x_rot; - view_3d_controller->set_view_type(View3DController::VIEW_TYPE_USER); - } - if (ED_IS_SHORTCUT("spatial_editor/orbit_view_up", event_mod)) { - // Clamp rotation to roughly -90..90 degrees so the user can't look upside-down and end up disoriented. - view_3d_controller->cursor.x_rot = CLAMP(view_3d_controller->cursor.x_rot + Math::PI / 12.0, -1.57, 1.57); - view_3d_controller->cursor.unsnapped_x_rot = view_3d_controller->cursor.x_rot; - view_3d_controller->set_view_type(View3DController::VIEW_TYPE_USER); - } - if (ED_IS_SHORTCUT("spatial_editor/orbit_view_right", event_mod)) { - view_3d_controller->cursor.y_rot -= Math::PI / 12.0; - view_3d_controller->cursor.unsnapped_y_rot = view_3d_controller->cursor.y_rot; - view_3d_controller->set_view_type(View3DController::VIEW_TYPE_USER); - } - if (ED_IS_SHORTCUT("spatial_editor/orbit_view_left", event_mod)) { - view_3d_controller->cursor.y_rot += Math::PI / 12.0; - view_3d_controller->cursor.unsnapped_y_rot = view_3d_controller->cursor.y_rot; - view_3d_controller->set_view_type(View3DController::VIEW_TYPE_USER); - } - if (ED_IS_SHORTCUT("spatial_editor/orbit_view_180", event_mod)) { - view_3d_controller->cursor.y_rot += Math::PI; - view_3d_controller->cursor.unsnapped_y_rot = view_3d_controller->cursor.y_rot; - view_3d_controller->set_view_type(View3DController::VIEW_TYPE_USER); - } - if (ED_IS_SHORTCUT("spatial_editor/focus_origin", event_mod)) { - _menu_option(VIEW_CENTER_TO_ORIGIN); - } - if (ED_IS_SHORTCUT("spatial_editor/focus_selection", event_mod)) { - _menu_option(VIEW_CENTER_TO_SELECTION); - times_focused_consecutively += 1; - } - if (ED_IS_SHORTCUT("spatial_editor/align_transform_with_view", event_mod)) { - _menu_option(VIEW_ALIGN_TRANSFORM_WITH_VIEW); - } - if (ED_IS_SHORTCUT("spatial_editor/align_rotation_with_view", event_mod)) { - _menu_option(VIEW_ALIGN_ROTATION_WITH_VIEW); - } - if (ED_IS_SHORTCUT("spatial_editor/insert_anim_key", event_mod)) { - if (!get_selected_count() || _edit.mode != TRANSFORM_NONE) { - return; - } - - if (!AnimationPlayerEditor::get_singleton()->get_track_editor()->has_keying()) { - set_message(TTR("Keying is disabled (no key inserted).")); - return; - } - - const List &selection = editor_selection->get_top_selected_node_list(); - - for (Node *E : selection) { - Node3D *sp = Object::cast_to(E); - if (!sp) { - continue; - } - - spatial_editor->emit_signal(SNAME("transform_key_request"), sp, "", sp->get_transform()); - } - - set_message(TTR("Animation Key Inserted.")); - } - if (ED_IS_SHORTCUT("spatial_editor/cancel_transform", event_mod) && _edit.mode != TRANSFORM_NONE) { - cancel_transform(); - } - if (!view_3d_controller->is_freelook_enabled() && !k->is_echo()) { - if (ED_IS_SHORTCUT("spatial_editor/reset_transform_position", event_mod)) { - _reset_transform(TransformType::POSITION); - } - if (ED_IS_SHORTCUT("spatial_editor/reset_transform_rotation", event_mod)) { - _reset_transform(TransformType::ROTATION); - } - if (ED_IS_SHORTCUT("spatial_editor/reset_transform_scale", event_mod)) { - _reset_transform(TransformType::SCALE); - } - if (ED_IS_SHORTCUT("spatial_editor/instant_translate", event_mod) && (_edit.mode != TRANSFORM_TRANSLATE || collision_reposition)) { - if (_edit.mode == TRANSFORM_NONE) { - begin_transform(TRANSFORM_TRANSLATE, true); - } else if (_edit.instant || collision_reposition) { - commit_transform(); - begin_transform(TRANSFORM_TRANSLATE, true); - } - } - if (ED_IS_SHORTCUT("spatial_editor/instant_rotate", event_mod)) { - if (_edit.mode == TRANSFORM_ROTATE && _edit.instant) { - _edit.is_trackball = !_edit.is_trackball; - _edit.show_rotation_line = !_edit.is_trackball; - _edit.plane = TRANSFORM_VIEW; - _edit.original_mouse_pos = _edit.mouse_pos; - if (_edit.is_trackball) { - set_message(TTR("Trackball Rotation")); - } else { - _edit.initial_click_vector = Vector3(); - _edit.previous_rotation_vector = Vector3(); - _edit.accumulated_rotation_angle = 0.0; - _edit.rotation_angle = 0.0; - set_message(vformat(TTR("Rotating %s degrees."), String::num(0, 0))); - } - surface->queue_redraw(); - } else if (_edit.mode != TRANSFORM_ROTATE) { - if (_edit.mode == TRANSFORM_NONE) { - begin_transform(TRANSFORM_ROTATE, true); - } else if (_edit.instant || collision_reposition) { - commit_transform(); - begin_transform(TRANSFORM_ROTATE, true); - } - } - } - if (ED_IS_SHORTCUT("spatial_editor/instant_scale", event_mod) && _edit.mode != TRANSFORM_SCALE) { - if (_edit.mode == TRANSFORM_NONE) { - begin_transform(TRANSFORM_SCALE, true); - } else if (_edit.instant || collision_reposition) { - commit_transform(); - begin_transform(TRANSFORM_SCALE, true); - } - } - if (ED_IS_SHORTCUT("spatial_editor/collision_reposition", event_mod) && editor_selection->get_top_selected_node_list().size() == 1 && !collision_reposition && _has_unlocked_selection()) { - if (_edit.mode == TRANSFORM_NONE || _edit.instant) { - if (_edit.mode == TRANSFORM_NONE) { - _compute_edit(_edit.mouse_pos); - } else { - commit_transform(); - _compute_edit(_edit.mouse_pos); - } - _edit.mode = TRANSFORM_TRANSLATE; - collision_reposition = true; - } - } - } - - // Freelook doesn't work in orthogonal mode or when previewing without pilot mode. - if (!view_3d_controller->is_orthogonal() && !(previewing && !pilot_preview_enabled) && ED_IS_SHORTCUT("spatial_editor/freelook_toggle", event_mod)) { - view_3d_controller->set_freelook_enabled(!view_3d_controller->is_freelook_enabled()); - } else if (k->get_keycode() == Key::ESCAPE) { - view_3d_controller->set_freelook_enabled(false); - } - - if (k->get_keycode() == Key::SPACE) { - if (!k->is_pressed()) { - emit_signal(SNAME("toggle_maximize_view"), this); - } - } - } - - // Freelook uses most of the useful shortcuts, like save, so its OK - // to consider freelook active as end of the line for future events. - if (view_3d_controller->is_freelook_enabled()) { - accept_event(); - } -} - -void Node3DEditorViewport::_cursor_interpolated() { - last_camera_transform = view_3d_controller->interp_to_camera_transform(); - - if (previewing_camera && previewing && pilot_preview_enabled) { - _pilot_ensure_undo_session(); - previewing->set_global_transform(last_camera_transform); - pilot_undo_idle_time = 0.0; - } else if (!previewing_camera) { - camera->set_global_transform(last_camera_transform); - } - - if (view_3d_controller->is_orthogonal()) { - float half_fov = Math::deg_to_rad(get_fov()) / 2.0; - float height = 2.0 * view_3d_controller->cursor.distance * Math::tan(half_fov); - camera->set_orthogonal(height, get_znear(), get_zfar()); - } else { - camera->set_perspective(get_fov(), get_znear(), get_zfar()); - } - - view_3d_controller->set_z_near(get_znear()); - view_3d_controller->set_z_far(get_zfar()); - - update_transform_gizmo_view(); - rotation_control->queue_redraw(); - position_control->queue_redraw(); - look_control->queue_redraw(); - surface->queue_redraw(); - spatial_editor->update_grid(); -} - -void Node3DEditorViewport::_cursor_distance_scaled() { - zoom_indicator_delay = ZOOM_FREELOOK_INDICATOR_DELAY_S; - surface->queue_redraw(); -} - -void Node3DEditorViewport::_freelook_changed() { - spatial_editor->set_freelook_viewport(view_3d_controller->is_freelook_enabled() ? this : nullptr); -} - -void Node3DEditorViewport::_pilot_ensure_undo_session() { - if (pilot_undo_session_active || !previewing) { - return; - } - pilot_undo_initial_transform = previewing->get_global_transform(); - pilot_undo_session_active = true; - pilot_undo_idle_time = 0.0; -} - -void Node3DEditorViewport::_pilot_commit_undo_session() { - if (!pilot_undo_session_active) { - return; - } - pilot_undo_session_active = false; - pilot_undo_idle_time = 0.0; - if (!previewing) { - return; - } - const Transform3D current_transform = previewing->get_global_transform(); - if (current_transform.is_equal_approx(pilot_undo_initial_transform)) { - return; - } - EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); - undo_redo->create_action(vformat(TTR("Move Camera \"%s\""), previewing->get_name()), UndoRedo::MERGE_ENDS); - undo_redo->add_do_method(previewing, "set_global_transform", current_transform); - undo_redo->add_undo_method(previewing, "set_global_transform", pilot_undo_initial_transform); - undo_redo->commit_action(false); -} - -void Node3DEditorViewport::_pilot_tick_undo_session(real_t p_delta) { - if (!pilot_undo_session_active) { - return; - } - pilot_undo_idle_time += p_delta; - if (pilot_undo_idle_time > 0.15) { - _pilot_commit_undo_session(); - } -} - -void Node3DEditorViewport::_freelook_speed_scaled() { - zoom_indicator_delay = ZOOM_FREELOOK_INDICATOR_DELAY_S; - surface->queue_redraw(); -} - -bool Node3DEditorViewport::_is_nav_modifier_pressed(const String &p_name) { - return _is_shortcut_empty(p_name) || Input::get_singleton()->is_action_pressed(p_name); -} - -bool Node3DEditorViewport::_is_shortcut_empty(const String &p_name) { - Ref check_shortcut = ED_GET_SHORTCUT(p_name); - - ERR_FAIL_COND_V_MSG(check_shortcut.is_null(), true, "The Shortcut was null, possible name mismatch."); - - return check_shortcut->get_events().is_empty(); -} - -void Node3DEditorViewport::set_message(const String &p_message, float p_time) { - message = p_message; - message_time = p_time; -} - -void Node3DEditorPlugin::edited_scene_changed() { - for (uint32_t i = 0; i < Node3DEditor::VIEWPORTS_COUNT; i++) { - Node3DEditorViewport *viewport = Node3DEditor::get_singleton()->get_editor_viewport(i); - if (viewport->is_visible()) { - viewport->notification(Control::NOTIFICATION_VISIBILITY_CHANGED); - } - } -} - -void Node3DEditorViewport::_project_settings_changed() { - // Update shadow atlas if changed. - int shadowmap_size = GLOBAL_GET("rendering/lights_and_shadows/positional_shadow/atlas_size"); - bool shadowmap_16_bits = GLOBAL_GET("rendering/lights_and_shadows/positional_shadow/atlas_16_bits"); - int atlas_q0 = GLOBAL_GET("rendering/lights_and_shadows/positional_shadow/atlas_quadrant_0_subdiv"); - int atlas_q1 = GLOBAL_GET("rendering/lights_and_shadows/positional_shadow/atlas_quadrant_1_subdiv"); - int atlas_q2 = GLOBAL_GET("rendering/lights_and_shadows/positional_shadow/atlas_quadrant_2_subdiv"); - int atlas_q3 = GLOBAL_GET("rendering/lights_and_shadows/positional_shadow/atlas_quadrant_3_subdiv"); - - viewport->set_positional_shadow_atlas_size(shadowmap_size); - viewport->set_positional_shadow_atlas_16_bits(shadowmap_16_bits); - viewport->set_positional_shadow_atlas_quadrant_subdiv(0, Viewport::PositionalShadowAtlasQuadrantSubdiv(atlas_q0)); - viewport->set_positional_shadow_atlas_quadrant_subdiv(1, Viewport::PositionalShadowAtlasQuadrantSubdiv(atlas_q1)); - viewport->set_positional_shadow_atlas_quadrant_subdiv(2, Viewport::PositionalShadowAtlasQuadrantSubdiv(atlas_q2)); - viewport->set_positional_shadow_atlas_quadrant_subdiv(3, Viewport::PositionalShadowAtlasQuadrantSubdiv(atlas_q3)); - - // Update MSAA, screen-space AA and debanding if changed - - const int msaa_mode = GLOBAL_GET("rendering/anti_aliasing/quality/msaa_3d"); - viewport->set_msaa_3d(Viewport::MSAA(msaa_mode)); - const int ssaa_mode = GLOBAL_GET("rendering/anti_aliasing/quality/screen_space_aa"); - viewport->set_screen_space_aa(Viewport::ScreenSpaceAA(ssaa_mode)); - const bool use_taa = GLOBAL_GET("rendering/anti_aliasing/quality/use_taa"); - viewport->set_use_taa(use_taa); - - const bool transparent_background = GLOBAL_GET("rendering/viewport/transparent_background"); - viewport->set_transparent_background(transparent_background); - - const bool use_debanding = GLOBAL_GET("rendering/anti_aliasing/quality/use_debanding"); - viewport->set_use_debanding(use_debanding); - - const bool use_occlusion_culling = GLOBAL_GET("rendering/occlusion_culling/use_occlusion_culling"); - viewport->set_use_occlusion_culling(use_occlusion_culling); - - const float mesh_lod_threshold = GLOBAL_GET("rendering/mesh_lod/lod_change/threshold_pixels"); - viewport->set_mesh_lod_threshold(mesh_lod_threshold); - - const Viewport::Scaling3DMode scaling_3d_mode = Viewport::Scaling3DMode(int(GLOBAL_GET("rendering/scaling_3d/mode"))); - viewport->set_scaling_3d_mode(scaling_3d_mode); - - _update_shrink(); - - const float fsr_sharpness = GLOBAL_GET("rendering/scaling_3d/fsr_sharpness"); - viewport->set_fsr_sharpness(fsr_sharpness); - - const float texture_mipmap_bias = GLOBAL_GET("rendering/textures/default_filters/texture_mipmap_bias"); - viewport->set_texture_mipmap_bias(texture_mipmap_bias); - - const Viewport::AnisotropicFiltering anisotropic_filtering_level = Viewport::AnisotropicFiltering(int(GLOBAL_GET("rendering/textures/default_filters/anisotropic_filtering_level"))); - viewport->set_anisotropic_filtering_level(anisotropic_filtering_level); -} - -static void override_label_colors(Control *p_control) { - p_control->begin_bulk_theme_override(); - p_control->add_theme_color_override(SceneStringName(font_color), p_control->get_theme_color(SNAME("font_dark_background_color"), EditorStringName(Editor))); - p_control->add_theme_color_override("font_hover_color", p_control->get_theme_color(SNAME("font_dark_background_hover_color"), EditorStringName(Editor))); - p_control->add_theme_color_override("font_focus_color", p_control->get_theme_color(SNAME("font_dark_background_focus_color"), EditorStringName(Editor))); - p_control->add_theme_color_override("font_pressed_color", p_control->get_theme_color(SNAME("font_dark_background_pressed_color"), EditorStringName(Editor))); - p_control->add_theme_color_override("font_hover_pressed_color", p_control->get_theme_color(SNAME("font_dark_background_hover_pressed_color"), EditorStringName(Editor))); - p_control->end_bulk_theme_override(); -} - -static void override_button_stylebox(Button *p_button, const Ref p_stylebox) { - p_button->begin_bulk_theme_override(); - p_button->add_theme_style_override(CoreStringName(normal), p_stylebox); - p_button->add_theme_style_override("normal_mirrored", p_stylebox); - p_button->add_theme_style_override(SceneStringName(hover), p_stylebox); - p_button->add_theme_style_override("hover_mirrored", p_stylebox); - p_button->add_theme_style_override("hover_pressed", p_stylebox); - p_button->add_theme_style_override("hover_pressed_mirrored", p_stylebox); - p_button->add_theme_style_override(SceneStringName(pressed), p_stylebox); - p_button->add_theme_style_override("pressed_mirrored", p_stylebox); - p_button->add_theme_style_override("focus", p_stylebox); - p_button->add_theme_style_override("focus_mirrored", p_stylebox); - p_button->add_theme_style_override("disabled", p_stylebox); - p_button->add_theme_style_override("disabled_mirrored", p_stylebox); - p_button->end_bulk_theme_override(); -} - -void Node3DEditorViewport::_notification(int p_what) { - switch (p_what) { - case NOTIFICATION_TRANSLATION_CHANGED: { - _update_name(); - _update_centered_labels(); - message_time = MIN(message_time, 0.001); // Make it disappear. - - const int item_count = display_submenu->get_item_count(); - for (int i = 0; i < item_count; i++) { - const Array item_data = display_submenu->get_item_metadata(i); - if (item_data.is_empty()) { - continue; - } - - SupportedRenderingMethods rendering_methods = item_data[0]; - String base_tooltip = item_data[1]; - - bool disabled = false; - String disabled_tooltip; - switch (rendering_methods) { - case SupportedRenderingMethods::ALL: - break; - case SupportedRenderingMethods::FORWARD_PLUS_MOBILE: - disabled = OS::get_singleton()->get_current_rendering_method() == "gl_compatibility"; - disabled_tooltip = TTR("This debug draw mode is only supported when using the Forward+ or Mobile renderer."); - break; - case SupportedRenderingMethods::FORWARD_PLUS: - disabled = OS::get_singleton()->get_current_rendering_method() == "gl_compatibility" || OS::get_singleton()->get_current_rendering_method() == "mobile"; - disabled_tooltip = TTR("This debug draw mode is only supported when using the Forward+ renderer."); - break; - } - - display_submenu->set_item_disabled(i, disabled); - String tooltip = TTR(base_tooltip); - if (disabled) { - if (tooltip.is_empty()) { - tooltip = disabled_tooltip; - } else { - tooltip += "\n\n" + disabled_tooltip; - } - } - display_submenu->set_item_tooltip(i, tooltip); - } - } break; - - case NOTIFICATION_READY: { - ProjectSettings::get_singleton()->connect("settings_changed", callable_mp(this, &Node3DEditorViewport::_project_settings_changed)); - _update_navigation_controls_visibility(); - } break; - - case NOTIFICATION_VISIBILITY_CHANGED: { - bool vp_visible = is_visible_in_tree(); - - set_process(vp_visible); - set_physics_process(vp_visible); - - if (vp_visible) { - view_3d_controller->set_orthogonal(view_display_menu->get_popup()->is_item_checked(view_display_menu->get_popup()->get_item_index(VIEW_ORTHOGONAL))); - view_3d_controller->update_camera(); - _update_name(); - } else { - view_3d_controller->set_freelook_enabled(false); - _pilot_commit_undo_session(); - } - callable_mp(this, &Node3DEditorViewport::update_transform_gizmo_view).call_deferred(); - } break; - - case NOTIFICATION_RESIZED: { - callable_mp(this, &Node3DEditorViewport::update_transform_gizmo_view).call_deferred(); - } break; - - case NOTIFICATION_PROCESS: { - if (ruler->is_inside_tree()) { - Vector3 start_pos = ruler_start_point->get_global_position(); - Vector3 end_pos = ruler_end_point->get_global_position(); - - geometry->clear_surfaces(); - geometry->surface_begin(Mesh::PRIMITIVE_LINES); - - Vector3 center = (start_pos + end_pos) / 2; - Vector3 camera_dir = (camera->get_transform().origin - center).normalized(); - real_t offset_distance = 0.01; - - geometry->surface_add_vertex(start_pos + camera_dir * offset_distance); - geometry->surface_add_vertex(end_pos + camera_dir * offset_distance); - geometry->surface_end(); - - geometry_xray->clear_surfaces(); - geometry_xray->surface_begin(Mesh::PRIMITIVE_LINES); - geometry_xray->surface_add_vertex(start_pos); - geometry_xray->surface_add_vertex(end_pos); - geometry_xray->surface_end(); - - float distance = start_pos.distance_to(end_pos); - if (distance < 0.001) { - distance = 0.0; - } - ruler_label->set_text(TranslationServer::get_singleton()->format_number(vformat("%.3f m", distance), _get_locale())); - - Vector2 screen_position = camera->unproject_position(center) - (ruler_label->get_custom_minimum_size() / 2); - ruler_label->set_position(screen_position); - - bool show_components = Input::get_singleton()->is_key_pressed(Key::SHIFT); - - if (show_components) { - Ref triangle_mesh = ruler_triangle_lines->get_mesh(); - Ref triangle_mesh_xray = ruler_triangle_lines_xray->get_mesh(); - - if (triangle_mesh.is_valid() && triangle_mesh_xray.is_valid()) { - Vector3 delta = end_pos - start_pos; - delta = delta.abs(); - const real_t threshold = 0.001; - if (delta.x < threshold) { - delta.x = 0.0; - } - if (delta.y < threshold) { - delta.y = 0.0; - } - if (delta.z < threshold) { - delta.z = 0.0; - } - - Vector3 corner_point; - Color first_line_color; - Color second_line_color; - - Color axis_x_color = get_theme_color(SNAME("axis_x_color"), EditorStringName(Editor)); - Color axis_y_color = get_theme_color(SNAME("axis_y_color"), EditorStringName(Editor)); - Color axis_z_color = get_theme_color(SNAME("axis_z_color"), EditorStringName(Editor)); - - if (delta.x > 0.0 && delta.y > 0.0 && delta.z == 0.0) { - // XY plane - corner_point = Vector3(end_pos.x, start_pos.y, start_pos.z); - first_line_color = axis_x_color; - second_line_color = axis_y_color; - } else if (delta.x > 0.0 && delta.z > 0.0 && delta.y == 0.0) { - // XZ plane - corner_point = Vector3(end_pos.x, start_pos.y, start_pos.z); - first_line_color = axis_x_color; - second_line_color = axis_z_color; - } else if (delta.y > 0.0 && delta.z > 0.0 && delta.x == 0.0) { - // YZ plane - corner_point = Vector3(start_pos.x, start_pos.y, end_pos.z); - first_line_color = axis_z_color; - second_line_color = axis_y_color; - } else if (delta.x > 0.0 && delta.y > 0.0 && delta.z > 0.0) { - // All three axes - corner_point = Vector3(end_pos.x, start_pos.y, start_pos.z); - first_line_color = axis_x_color; - second_line_color = axis_y_color; - } else { - corner_point = end_pos; - first_line_color = axis_x_color; - second_line_color = axis_x_color; - } - - triangle_mesh->clear_surfaces(); - triangle_mesh->surface_begin(Mesh::PRIMITIVE_LINES); - - Vector3 triangle_camera_dir = (camera->get_transform().origin - center).normalized(); - real_t triangle_offset_distance = 0.01; - - triangle_mesh->surface_set_color(first_line_color); - triangle_mesh->surface_add_vertex(start_pos + triangle_camera_dir * triangle_offset_distance); - triangle_mesh->surface_set_color(first_line_color); - triangle_mesh->surface_add_vertex(corner_point + triangle_camera_dir * triangle_offset_distance); - triangle_mesh->surface_set_color(second_line_color); - triangle_mesh->surface_add_vertex(corner_point + triangle_camera_dir * triangle_offset_distance); - triangle_mesh->surface_set_color(second_line_color); - triangle_mesh->surface_add_vertex(end_pos + triangle_camera_dir * triangle_offset_distance); - - triangle_mesh->surface_end(); - - Color first_line_color_xray = first_line_color; - Color second_line_color_xray = second_line_color; - first_line_color_xray.a = 0.15; - second_line_color_xray.a = 0.15; - - triangle_mesh_xray->clear_surfaces(); - triangle_mesh_xray->surface_begin(Mesh::PRIMITIVE_LINES); - - triangle_mesh_xray->surface_set_color(first_line_color_xray); - triangle_mesh_xray->surface_add_vertex(start_pos); - triangle_mesh_xray->surface_set_color(first_line_color_xray); - triangle_mesh_xray->surface_add_vertex(corner_point); - triangle_mesh_xray->surface_set_color(second_line_color_xray); - triangle_mesh_xray->surface_add_vertex(corner_point); - triangle_mesh_xray->surface_set_color(second_line_color_xray); - triangle_mesh_xray->surface_add_vertex(end_pos); - - triangle_mesh_xray->surface_end(); - } - } else { - Ref triangle_mesh = ruler_triangle_lines->get_mesh(); - Ref triangle_mesh_xray = ruler_triangle_lines_xray->get_mesh(); - - if (triangle_mesh.is_valid()) { - triangle_mesh->clear_surfaces(); - } - if (triangle_mesh_xray.is_valid()) { - triangle_mesh_xray->clear_surfaces(); - } - } - - if (show_components) { - Vector3 delta = end_pos - start_pos; - delta = delta.abs(); - const real_t threshold = 0.001; - if (delta.x < threshold) { - delta.x = 0.0; - } - if (delta.y < threshold) { - delta.y = 0.0; - } - if (delta.z < threshold) { - delta.z = 0.0; - } - - int active_axes = 0; - if (delta.x > 0.0) { - active_axes++; - } - if (delta.y > 0.0) { - active_axes++; - } - if (delta.z > 0.0) { - active_axes++; - } - - String x_text = delta.x > 0.0 ? TranslationServer::get_singleton()->format_number(vformat("X: %.3f m", delta.x), _get_locale()) : ""; - String y_text = delta.y > 0.0 ? TranslationServer::get_singleton()->format_number(vformat("Y: %.3f m", delta.y), _get_locale()) : ""; - String z_text = delta.z > 0.0 ? TranslationServer::get_singleton()->format_number(vformat("Z: %.3f m", delta.z), _get_locale()) : ""; - - Color axis_x_color = get_theme_color(SNAME("axis_x_color"), EditorStringName(Editor)); - Color axis_y_color = get_theme_color(SNAME("axis_y_color"), EditorStringName(Editor)); - Color axis_z_color = get_theme_color(SNAME("axis_z_color"), EditorStringName(Editor)); - - Vector3 corner_point; - - if (active_axes >= 2) { - if (delta.z == 0.0) { - // XY plane - corner_point = Vector3(end_pos.x, start_pos.y, start_pos.z); - - ruler_label_x->add_theme_color_override(SceneStringName(font_color), axis_x_color); - ruler_label_x->set_text(x_text); - ruler_label_x->set_visible(true); - Vector2 x_pos = camera->unproject_position((start_pos + corner_point) / 2) - (ruler_label_x->get_custom_minimum_size() / 2); - ruler_label_x->set_position(x_pos); - - ruler_label_y->add_theme_color_override(SceneStringName(font_color), axis_y_color); - ruler_label_y->set_text(y_text); - ruler_label_y->set_visible(true); - Vector2 y_pos = camera->unproject_position((corner_point + end_pos) / 2) - (ruler_label_y->get_custom_minimum_size() / 2); - ruler_label_y->set_position(y_pos); - - ruler_label_z->set_visible(false); - } else if (delta.y == 0.0) { - // XZ plane - corner_point = Vector3(end_pos.x, start_pos.y, start_pos.z); - - ruler_label_x->add_theme_color_override(SceneStringName(font_color), axis_x_color); - ruler_label_x->set_text(x_text); - ruler_label_x->set_visible(true); - Vector2 x_pos = camera->unproject_position((start_pos + corner_point) / 2) - (ruler_label_x->get_custom_minimum_size() / 2); - ruler_label_x->set_position(x_pos); - - ruler_label_y->add_theme_color_override(SceneStringName(font_color), axis_z_color); - ruler_label_y->set_text(z_text); - ruler_label_y->set_visible(true); - Vector2 z_pos = camera->unproject_position((corner_point + end_pos) / 2) - (ruler_label_y->get_custom_minimum_size() / 2); - ruler_label_y->set_position(z_pos); - - ruler_label_z->set_visible(false); - } else if (delta.x == 0.0) { - // YZ plane - corner_point = Vector3(start_pos.x, start_pos.y, end_pos.z); - - ruler_label_x->add_theme_color_override(SceneStringName(font_color), axis_z_color); - ruler_label_x->set_text(z_text); - ruler_label_x->set_visible(true); - Vector2 z_pos = camera->unproject_position((start_pos + corner_point) / 2) - (ruler_label_x->get_custom_minimum_size() / 2); - ruler_label_x->set_position(z_pos); - - ruler_label_y->add_theme_color_override(SceneStringName(font_color), axis_y_color); - ruler_label_y->set_text(y_text); - ruler_label_y->set_visible(true); - Vector2 y_pos = camera->unproject_position((corner_point + end_pos) / 2) - (ruler_label_y->get_custom_minimum_size() / 2); - ruler_label_y->set_position(y_pos); - - ruler_label_z->set_visible(false); - } else { - // All three axes - corner_point = Vector3(end_pos.x, start_pos.y, start_pos.z); - - ruler_label_x->add_theme_color_override(SceneStringName(font_color), axis_x_color); - ruler_label_x->set_text(x_text); - ruler_label_x->set_visible(true); - Vector2 x_pos = camera->unproject_position((start_pos + corner_point) / 2) - (ruler_label_x->get_custom_minimum_size() / 2); - ruler_label_x->set_position(x_pos); - - ruler_label_y->add_theme_color_override(SceneStringName(font_color), axis_y_color); - ruler_label_y->set_text(y_text); - ruler_label_y->set_visible(true); - Vector2 y_pos = camera->unproject_position((corner_point + end_pos) / 2) - (ruler_label_y->get_custom_minimum_size() / 2); - ruler_label_y->set_position(y_pos); - - ruler_label_z->add_theme_color_override(SceneStringName(font_color), axis_z_color); - ruler_label_z->set_text(z_text); - ruler_label_z->set_visible(true); - Vector2 z_pos = camera->unproject_position(center + Vector3(0, 0, -0.5)) - (ruler_label_z->get_custom_minimum_size() / 2); - ruler_label_z->set_position(z_pos); - } - } else if (active_axes == 1) { - corner_point = end_pos; - - if (delta.x > 0.0) { - ruler_label_x->add_theme_color_override(SceneStringName(font_color), axis_x_color); - ruler_label_x->set_text(x_text); - ruler_label_x->set_visible(true); - Vector2 pos = camera->unproject_position(center) - (ruler_label_x->get_custom_minimum_size() / 2); - ruler_label_x->set_position(pos); - ruler_label_y->set_visible(false); - ruler_label_z->set_visible(false); - } else if (delta.y > 0.0) { - ruler_label_x->add_theme_color_override(SceneStringName(font_color), axis_y_color); - ruler_label_x->set_text(y_text); - ruler_label_x->set_visible(true); - Vector2 pos = camera->unproject_position(center) - (ruler_label_x->get_custom_minimum_size() / 2); - ruler_label_x->set_position(pos); - ruler_label_y->set_visible(false); - ruler_label_z->set_visible(false); - } else { - ruler_label_x->add_theme_color_override(SceneStringName(font_color), axis_z_color); - ruler_label_x->set_text(z_text); - ruler_label_x->set_visible(true); - Vector2 pos = camera->unproject_position(center) - (ruler_label_x->get_custom_minimum_size() / 2); - ruler_label_x->set_position(pos); - ruler_label_y->set_visible(false); - ruler_label_z->set_visible(false); - } - } else { - ruler_label_x->set_visible(false); - ruler_label_y->set_visible(false); - ruler_label_z->set_visible(false); - } - - } else { - ruler_label_x->set_visible(false); - ruler_label_y->set_visible(false); - ruler_label_z->set_visible(false); - } - } - - real_t delta = get_process_delta_time(); - - if (zoom_indicator_delay > 0) { - zoom_indicator_delay -= delta; - if (zoom_indicator_delay <= 0) { - surface->queue_redraw(); - zoom_limit_label->hide(); - } - } - - if (view_3d_controller->is_freelook_enabled()) { - view_3d_controller->update_freelook(delta); - _disable_follow_mode(); - } - - if (focused_node_id.is_valid() && get_selected_count() > 0 && times_focused_consecutively >= 2 && times_focused_consecutively % 2 == 0) { - Node *focused_node = ObjectDB::get_instance(focused_node_id); - if (focused_node) { - follow_mode->set_text(vformat(TTR("Following %s"), focused_node->get_name())); - follow_mode->set_button_icon(get_editor_theme_icon(focused_node->get_class())); - follow_mode->show(); - focus_selection(); - } else { - _disable_follow_mode(); - } - } else { - follow_mode->hide(); - } - - Node *scene_root = SceneTreeDock::get_singleton()->get_editor_data()->get_edited_scene_root(); - if (previewing_cinema && scene_root != nullptr) { - Camera3D *cam = scene_root->get_viewport()->get_camera_3d(); - if (cam != nullptr && cam != previewing) { - //then switch the viewport's camera to the scene's viewport camera - if (previewing != nullptr) { - _pilot_commit_undo_session(); - previewing->disconnect(SceneStringName(tree_exited), callable_mp(this, &Node3DEditorViewport::_preview_exited_scene)); - previewing->disconnect(CoreStringName(property_list_changed), callable_mp(this, &Node3DEditorViewport::_preview_camera_property_changed)); - } - previewing = cam; - previewing->connect(SceneStringName(tree_exited), callable_mp(this, &Node3DEditorViewport::_preview_exited_scene)); - previewing->connect(CoreStringName(property_list_changed), callable_mp(this, &Node3DEditorViewport::_preview_camera_property_changed)); - RS::get_singleton()->viewport_attach_camera(viewport->get_viewport_rid(), cam->get_camera()); - surface->queue_redraw(); - } - } - - if (_camera_moved_externally()) { - // If camera moved after this plugin last set it, presumably a tool script has moved it, accept the new camera transform as the cursor position. - pilot_undo_session_active = false; - pilot_undo_idle_time = 0.0; - _apply_camera_transform_to_cursor(); - view_3d_controller->update_camera(); - } else { - view_3d_controller->update_camera(delta); - } - - _pilot_tick_undo_session(delta); - - const HashMap &selection = editor_selection->get_selection(); - - bool changed = false; - bool exist = false; - - for (const KeyValue &E : selection) { - Node3D *sp = ObjectDB::get_instance(E.key); - if (!sp) { - continue; - } - - Node3DEditorSelectedItem *se = editor_selection->get_node_editor_data(sp); - if (!se) { - continue; - } - - Transform3D t = sp->get_global_gizmo_transform(); - if (!t.is_finite()) { - continue; - } - AABB new_aabb = _calculate_spatial_bounds(sp); - - exist = true; - if (se->last_xform == t && se->aabb == new_aabb && !se->last_xform_dirty) { - continue; - } - changed = true; - se->last_xform_dirty = false; - se->last_xform = t; - - se->aabb = new_aabb; - - Transform3D t_offset = t; - - // apply AABB scaling before item's global transform - { - const Vector3 offset(0.005, 0.005, 0.005); - Basis aabb_s; - aabb_s.scale(se->aabb.size + offset); - t.translate_local(se->aabb.position - offset / 2); - t.basis = t.basis * aabb_s; - } - { - const Vector3 offset(0.01, 0.01, 0.01); - Basis aabb_s; - aabb_s.scale(se->aabb.size + offset); - t_offset.translate_local(se->aabb.position - offset / 2); - t_offset.basis = t_offset.basis * aabb_s; - } - - RenderingServer::get_singleton()->instance_set_transform(se->sbox_instance, t); - RenderingServer::get_singleton()->instance_set_transform(se->sbox_instance_offset, t_offset); - RenderingServer::get_singleton()->instance_set_transform(se->sbox_instance_xray, t); - RenderingServer::get_singleton()->instance_set_transform(se->sbox_instance_xray_offset, t_offset); - } - - if (changed || (spatial_editor->is_gizmo_visible() && !exist)) { - spatial_editor->update_transform_gizmo(); - } - - if (message_time > 0) { - if (message != last_message) { - surface->queue_redraw(); - last_message = message; - } - - message_time -= get_process_delta_time(); - if (message_time < 0) { - surface->queue_redraw(); - } - } - - bool show_info = view_display_menu->get_popup()->is_item_checked(view_display_menu->get_popup()->get_item_index(VIEW_INFORMATION)); - if (show_info != info_panel->is_visible()) { - info_panel->set_visible(show_info); - } - - Camera3D *current_camera; - - if (previewing) { - current_camera = previewing; - } else { - current_camera = camera; - } - - if (show_info) { - const String viewport_size = vformat(U"%d × %d", viewport->get_size().x * viewport->get_scaling_3d_scale(), viewport->get_size().y * viewport->get_scaling_3d_scale()); - String text; - text += vformat(TTR("X: %s"), rtos(current_camera->get_position().x).pad_decimals(1)) + "\n"; - text += vformat(TTR("Y: %s"), rtos(current_camera->get_position().y).pad_decimals(1)) + "\n"; - text += vformat(TTR("Z: %s"), rtos(current_camera->get_position().z).pad_decimals(1)) + "\n"; - text += "\n"; - text += vformat( - TTR("Size: %s (%.1fMP)") + "\n", - viewport_size, - viewport->get_size().x * viewport->get_size().y * Math::pow(viewport->get_scaling_3d_scale(), 2) * 0.000001); - - text += "\n"; - text += vformat(TTR("Objects: %d"), viewport->get_render_info(Viewport::RENDER_INFO_TYPE_VISIBLE, Viewport::RENDER_INFO_OBJECTS_IN_FRAME)) + "\n"; - text += vformat(TTR("Primitives: %d"), viewport->get_render_info(Viewport::RENDER_INFO_TYPE_VISIBLE, Viewport::RENDER_INFO_PRIMITIVES_IN_FRAME)) + "\n"; - text += vformat(TTR("Draw Calls: %d"), viewport->get_render_info(Viewport::RENDER_INFO_TYPE_VISIBLE, Viewport::RENDER_INFO_DRAW_CALLS_IN_FRAME)); - - info_label->set_text(text); - } - - // FPS Counter. - bool show_fps = view_display_menu->get_popup()->is_item_checked(view_display_menu->get_popup()->get_item_index(VIEW_FRAME_TIME)); - - if (show_fps != frame_time_panel->is_visible()) { - frame_time_panel->set_visible(show_fps); - RS::get_singleton()->viewport_set_measure_render_time(viewport->get_viewport_rid(), show_fps); - for (int i = 0; i < FRAME_TIME_HISTORY; i++) { - // Initialize to 120 FPS, so that the initial estimation until we get enough data is always reasonable. - cpu_time_history[i] = 8.333333; - gpu_time_history[i] = 8.333333; - } - cpu_time_history_index = 0; - gpu_time_history_index = 0; - } - if (show_fps) { - cpu_time_history[cpu_time_history_index] = RS::get_singleton()->viewport_get_measured_render_time_cpu(viewport->get_viewport_rid()); - cpu_time_history_index = (cpu_time_history_index + 1) % FRAME_TIME_HISTORY; - double cpu_time = 0.0; - for (int i = 0; i < FRAME_TIME_HISTORY; i++) { - cpu_time += cpu_time_history[i]; - } - cpu_time /= FRAME_TIME_HISTORY; - // Prevent unrealistically low values. - cpu_time = MAX(0.01, cpu_time); - - gpu_time_history[gpu_time_history_index] = RS::get_singleton()->viewport_get_measured_render_time_gpu(viewport->get_viewport_rid()); - gpu_time_history_index = (gpu_time_history_index + 1) % FRAME_TIME_HISTORY; - double gpu_time = 0.0; - for (int i = 0; i < FRAME_TIME_HISTORY; i++) { - gpu_time += gpu_time_history[i]; - } - gpu_time /= FRAME_TIME_HISTORY; - // Prevent division by zero for the FPS counter (and unrealistically low values). - // This limits the reported FPS to 100000. - gpu_time = MAX(0.01, gpu_time); - - // Color labels depending on performance level ("good" = green, "OK" = yellow, "bad" = red). - // Middle point is at 15 ms. - cpu_time_label->set_text(vformat(TTR("CPU Time: %s ms"), rtos(cpu_time).pad_decimals(2))); - cpu_time_label->add_theme_color_override( - SceneStringName(font_color), - frame_time_gradient->get_color_at_offset( - Math::remap(cpu_time, 0, 30, 0, 1))); - - gpu_time_label->set_text(vformat(TTR("GPU Time: %s ms"), rtos(gpu_time).pad_decimals(2))); - // Middle point is at 15 ms. - gpu_time_label->add_theme_color_override( - SceneStringName(font_color), - frame_time_gradient->get_color_at_offset( - Math::remap(gpu_time, 0, 30, 0, 1))); - - const double fps = 1000.0 / gpu_time; - fps_label->set_text(vformat(TTR("FPS: %d"), fps)); - // Middle point is at 60 FPS. - fps_label->add_theme_color_override( - SceneStringName(font_color), - frame_time_gradient->get_color_at_offset( - Math::remap(fps, 110, 10, 0, 1))); - } - } break; - - case NOTIFICATION_PHYSICS_PROCESS: { - if (collision_reposition) { - Node3D *selected_node = nullptr; - - if (ruler->is_inside_tree()) { - if (ruler_start_point->is_visible()) { - selected_node = ruler_end_point; - } else { - selected_node = ruler_start_point; - } - } else { - const List &selection = editor_selection->get_top_selected_node_list(); - if (selection.size() == 1) { - selected_node = Object::cast_to(selection.front()->get()); - } - } - - if (selected_node) { - if (!ruler->is_inside_tree()) { - double snap = EDITOR_GET("interface/inspector/default_float_step"); - int snap_step_decimals = Math::range_step_decimals(snap); - set_message(vformat(TTR("Translating: %s"), vformat("%.*v", snap_step_decimals, selected_node->get_global_position()))); - } - - selected_node->set_global_position(spatial_editor->snap_point(_get_instance_position(_edit.mouse_pos, selected_node))); - - if (ruler->is_inside_tree() && !ruler_start_point->is_visible()) { - ruler_end_point->set_global_position(ruler_start_point->get_global_position()); - ruler_start_point->set_visible(true); - ruler_end_point->set_visible(true); - ruler_label->set_visible(true); - ruler_label_x->set_visible(false); - ruler_label_y->set_visible(false); - ruler_label_z->set_visible(false); - } - } - } - - if (!update_preview_node) { - return; - } - if (preview_node->is_inside_tree()) { - preview_node_pos = spatial_editor->snap_point(_get_instance_position(preview_node_viewport_pos, preview_node)); - double snap = EDITOR_GET("interface/inspector/default_float_step"); - int snap_step_decimals = Math::range_step_decimals(snap); - set_message(vformat(TTR("Instantiating: %s"), vformat("%.*v", snap_step_decimals, preview_node_pos))); - Transform3D preview_gl_transform = Transform3D(Basis(), preview_node_pos); - preview_node->set_global_transform(preview_gl_transform); - if (!preview_node->is_visible()) { - preview_node->show(); - } - } - update_preview_node = false; - } break; - - case NOTIFICATION_APPLICATION_FOCUS_OUT: - case NOTIFICATION_WM_WINDOW_FOCUS_OUT: { - view_3d_controller->set_freelook_enabled(false); - view_3d_controller->cursor.region_select = false; - surface->queue_redraw(); - - // Commit the drag if the window is focused out. - if (_edit.mode != TRANSFORM_NONE) { - commit_transform(); - return; - } - - if (_edit.gizmo.is_valid()) { - // Certain gizmo plugins should be able to commit handles without dragging them. - if (_edit.original_mouse_pos != _edit.mouse_pos || _edit.gizmo->get_plugin()->can_commit_handle_on_click()) { - _edit.gizmo->commit_handle(_edit.gizmo_handle, _edit.gizmo_handle_secondary, _edit.gizmo_initial_value, false); - } - - spatial_editor->get_single_selected_node()->update_gizmos(); - _edit.gizmo = Ref(); - set_message(""); - } - } break; - - case NOTIFICATION_ENTER_TREE: { - surface->connect(SceneStringName(draw), callable_mp(this, &Node3DEditorViewport::_draw)); - surface->connect(SceneStringName(gui_input), callable_mp(this, &Node3DEditorViewport::_sinput)); - surface->connect(SceneStringName(mouse_entered), callable_mp(this, &Node3DEditorViewport::_surface_mouse_enter)); - surface->connect(SceneStringName(mouse_exited), callable_mp(this, &Node3DEditorViewport::_surface_mouse_exit)); - surface->connect(SceneStringName(focus_entered), callable_mp(this, &Node3DEditorViewport::_surface_focus_enter)); - surface->connect(SceneStringName(focus_exited), callable_mp(this, &Node3DEditorViewport::_surface_focus_exit)); - - _init_gizmo_instance(index); - } break; - - case NOTIFICATION_EXIT_TREE: { - _finish_gizmo_instances(); - } break; - - case NOTIFICATION_THEME_CHANGED: { - _update_centered_labels(); - - view_display_menu->set_button_icon(get_editor_theme_icon(SNAME("GuiTabMenuHlDarkBackground"))); - preview_camera->set_button_icon(get_editor_theme_icon(SNAME("Camera3DDarkBackground"))); - pilot_camera->set_button_icon(get_editor_theme_icon(SNAME("3D"))); - Control *gui_base = EditorNode::get_singleton()->get_gui_base(); - - const Ref &information_3d_stylebox = gui_base->get_theme_stylebox(SNAME("Information3dViewport"), EditorStringName(EditorStyles)); - - override_button_stylebox(view_display_menu, information_3d_stylebox); - override_label_colors(view_display_menu); - override_button_stylebox(translation_preview_button, information_3d_stylebox); - override_label_colors(translation_preview_button); - override_button_stylebox(follow_mode, information_3d_stylebox); - override_label_colors(follow_mode); - override_button_stylebox(preview_camera, information_3d_stylebox); - override_label_colors(preview_camera); - - frame_time_gradient->set_color(0, get_theme_color(SNAME("success_color_dark_background"), EditorStringName(Editor))); - frame_time_gradient->set_color(1, get_theme_color(SNAME("warning_color_dark_background"), EditorStringName(Editor))); - frame_time_gradient->set_color(2, get_theme_color(SNAME("error_color_dark_background"), EditorStringName(Editor))); - - override_button_stylebox(pilot_camera, information_3d_stylebox); - override_label_colors(pilot_camera); - - info_panel->add_theme_style_override(SceneStringName(panel), information_3d_stylebox); - override_label_colors(info_label); - tooltip_panel->add_theme_style_override(CoreStringName(normal), information_3d_stylebox); - - frame_time_panel->add_theme_style_override(SceneStringName(panel), information_3d_stylebox); - // Set a minimum width to prevent the width from changing all the time - // when numbers vary rapidly. This minimum width is set based on a - // GPU time of 999.99 ms in the current editor language. - const float min_width = get_theme_font(SNAME("main"), EditorStringName(EditorFonts))->get_string_size(vformat(TTR("GPU Time: %s ms"), 999.99)).x; - frame_time_panel->set_custom_minimum_size(Size2(min_width, 0) * EDSCALE); - frame_time_vbox->add_theme_constant_override("separation", Math::round(-1 * EDSCALE)); - - cinema_label->add_theme_style_override(CoreStringName(normal), information_3d_stylebox); - locked_label->add_theme_style_override(CoreStringName(normal), information_3d_stylebox); - - ruler_label->add_theme_color_override(SceneStringName(font_color), Color(1.0, 0.9, 0.0, 1.0)); - ruler_label->add_theme_color_override("font_outline_color", Color(0.0, 0.0, 0.0, 1.0)); - ruler_label->add_theme_constant_override("outline_size", 4 * EDSCALE); - ruler_label->add_theme_font_size_override(SceneStringName(font_size), 15 * EDSCALE); - ruler_label->add_theme_font_override(SceneStringName(font), get_theme_font(SNAME("bold"), EditorStringName(EditorFonts))); - - ruler_label_x->add_theme_color_override("font_outline_color", Color(0.0, 0.0, 0.0, 1.0)); - ruler_label_x->add_theme_constant_override("outline_size", 4 * EDSCALE); - ruler_label_x->add_theme_font_size_override(SceneStringName(font_size), 15 * EDSCALE); - ruler_label_x->add_theme_font_override(SceneStringName(font), get_theme_font(SNAME("bold"), EditorStringName(EditorFonts))); - - ruler_label_y->add_theme_color_override("font_outline_color", Color(0.0, 0.0, 0.0, 1.0)); - ruler_label_y->add_theme_constant_override("outline_size", 4 * EDSCALE); - ruler_label_y->add_theme_font_size_override(SceneStringName(font_size), 15 * EDSCALE); - ruler_label_y->add_theme_font_override(SceneStringName(font), get_theme_font(SNAME("bold"), EditorStringName(EditorFonts))); - - ruler_label_z->add_theme_color_override("font_outline_color", Color(0.0, 0.0, 0.0, 1.0)); - ruler_label_z->add_theme_constant_override("outline_size", 4 * EDSCALE); - ruler_label_z->add_theme_font_size_override(SceneStringName(font_size), 15 * EDSCALE); - ruler_label_z->add_theme_font_override(SceneStringName(font), get_theme_font(SNAME("bold"), EditorStringName(EditorFonts))); - } break; - - case NOTIFICATION_DRAG_END: { - // Clear preview material when dropped outside applicable object. - if (spatial_editor->get_preview_material().is_valid() && !is_drag_successful()) { - _reset_preview_material(); - _remove_preview_material(); - } else { - _remove_preview_node(); - } - } break; - - case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: { - _update_view_3d_controller(); - - if (EditorSettings::get_singleton()->check_changed_settings_in_group("editors/3d")) { - _update_navigation_controls_visibility(); - } - } break; - } -} - -void Node3DEditorViewport::_update_view_3d_controller(bool p_update_all) { - view_3d_controller->set_shortcut(View3DController::SHORTCUT_FOV_DECREASE, ED_GET_SHORTCUT("spatial_editor/decrease_fov")); - view_3d_controller->set_shortcut(View3DController::SHORTCUT_FOV_INCREASE, ED_GET_SHORTCUT("spatial_editor/increase_fov")); - view_3d_controller->set_shortcut(View3DController::SHORTCUT_FOV_RESET, ED_GET_SHORTCUT("spatial_editor/reset_fov")); - - view_3d_controller->set_shortcut(View3DController::SHORTCUT_PAN_MOD_1, ED_GET_SHORTCUT("spatial_editor/viewport_pan_modifier_1")); - view_3d_controller->set_shortcut(View3DController::SHORTCUT_PAN_MOD_2, ED_GET_SHORTCUT("spatial_editor/viewport_pan_modifier_2")); - - view_3d_controller->set_shortcut(View3DController::SHORTCUT_ORBIT_MOD_1, ED_GET_SHORTCUT("spatial_editor/viewport_orbit_modifier_1")); - view_3d_controller->set_shortcut(View3DController::SHORTCUT_ORBIT_MOD_2, ED_GET_SHORTCUT("spatial_editor/viewport_orbit_modifier_2")); - view_3d_controller->set_shortcut(View3DController::SHORTCUT_ORBIT_SNAP_MOD_1, ED_GET_SHORTCUT("spatial_editor/viewport_orbit_snap_modifier_1")); - view_3d_controller->set_shortcut(View3DController::SHORTCUT_ORBIT_SNAP_MOD_2, ED_GET_SHORTCUT("spatial_editor/viewport_orbit_snap_modifier_2")); - - view_3d_controller->set_shortcut(View3DController::SHORTCUT_FREELOOK_FORWARD, ED_GET_SHORTCUT("spatial_editor/freelook_forward")); - view_3d_controller->set_shortcut(View3DController::SHORTCUT_FREELOOK_BACKWARDS, ED_GET_SHORTCUT("spatial_editor/freelook_backwards")); - view_3d_controller->set_shortcut(View3DController::SHORTCUT_FREELOOK_LEFT, ED_GET_SHORTCUT("spatial_editor/freelook_left")); - view_3d_controller->set_shortcut(View3DController::SHORTCUT_FREELOOK_RIGHT, ED_GET_SHORTCUT("spatial_editor/freelook_right")); - view_3d_controller->set_shortcut(View3DController::SHORTCUT_FREELOOK_UP, ED_GET_SHORTCUT("spatial_editor/freelook_up")); - view_3d_controller->set_shortcut(View3DController::SHORTCUT_FREELOOK_DOWN, ED_GET_SHORTCUT("spatial_editor/freelook_down")); - view_3d_controller->set_shortcut(View3DController::SHORTCUT_FREELOOK_SPEED_MOD, ED_GET_SHORTCUT("spatial_editor/freelook_speed_modifier")); - view_3d_controller->set_shortcut(View3DController::SHORTCUT_FREELOOK_SLOW_MOD, ED_GET_SHORTCUT("spatial_editor/freelook_slow_modifier")); - - view_3d_controller->set_shortcut(View3DController::SHORTCUT_ZOOM_MOD_1, ED_GET_SHORTCUT("spatial_editor/viewport_zoom_modifier_1")); - view_3d_controller->set_shortcut(View3DController::SHORTCUT_ZOOM_MOD_2, ED_GET_SHORTCUT("spatial_editor/viewport_zoom_modifier_2")); - - if (p_update_all || EditorSettings::get_singleton()->check_changed_settings_in_group("editors/3d")) { - view_3d_controller->set_pan_mouse_button((View3DController::NavigationMouseButton)EDITOR_GET("editors/3d/navigation/pan_mouse_button").operator int()); - - view_3d_controller->set_orbit_sensitivity(EDITOR_GET("editors/3d/navigation_feel/orbit_sensitivity")); - view_3d_controller->set_orbit_inertia(EDITOR_GET("editors/3d/navigation_feel/orbit_inertia")); - view_3d_controller->set_orbit_mouse_button((View3DController::NavigationMouseButton)EDITOR_GET("editors/3d/navigation/orbit_mouse_button").operator int()); - - view_3d_controller->set_zoom_style((View3DController::ZoomStyle)EDITOR_GET("editors/3d/navigation/zoom_style").operator int()); - view_3d_controller->set_zoom_inertia(EDITOR_GET("editors/3d/navigation_feel/zoom_inertia")); - view_3d_controller->set_zoom_mouse_button((View3DController::NavigationMouseButton)EDITOR_GET("editors/3d/navigation/zoom_mouse_button").operator int()); - - view_3d_controller->set_freelook_scheme((View3DController::FreelookScheme)EDITOR_GET("editors/3d/freelook/freelook_navigation_scheme").operator int()); - view_3d_controller->set_freelook_base_speed(EDITOR_GET("editors/3d/freelook/freelook_base_speed")); - view_3d_controller->set_freelook_sensitivity(EDITOR_GET("editors/3d/freelook/freelook_sensitivity")); - view_3d_controller->set_freelook_inertia(EDITOR_GET("editors/3d/freelook/freelook_inertia")); - view_3d_controller->set_freelook_speed_zoom_link(EDITOR_GET("editors/3d/freelook/freelook_speed_zoom_link")); - view_3d_controller->set_freelook_invert_y_axis(EDITOR_GET("editors/3d/freelook/freelook_invert_y_axis")); - - view_3d_controller->set_translation_sensitivity(EDITOR_GET("editors/3d/navigation_feel/translation_sensitivity")); - view_3d_controller->set_translation_inertia(EDITOR_GET("editors/3d/navigation_feel/translation_inertia")); - - view_3d_controller->set_angle_snap_threshold(EDITOR_GET("editors/3d/navigation_feel/angle_snap_threshold")); - - view_3d_controller->set_emulate_3_button_mouse(EDITOR_GET("editors/3d/navigation/emulate_3_button_mouse")); - view_3d_controller->set_emulate_numpad(EDITOR_GET("editors/3d/navigation/emulate_numpad")); - - view_3d_controller->set_invert_x_axis(EDITOR_GET("editors/3d/navigation/invert_x_axis")); - view_3d_controller->set_invert_y_axis(EDITOR_GET("editors/3d/navigation/invert_y_axis")); - - view_3d_controller->set_warped_mouse_panning((View3DController::NavigationMouseButton)EDITOR_GET("editors/3d/navigation/warped_mouse_panning").operator int()); - } -} - -static void draw_indicator_bar(Control &p_surface, real_t p_fill, const Ref p_icon, const Ref p_font, int p_font_size, const String &p_text, const Color &p_color) { - // Adjust bar size from control height - const Vector2 surface_size = p_surface.get_size(); - const real_t h = surface_size.y / 2.0; - const real_t y = (surface_size.y - h) / 2.0; - - const Rect2 r(10 * EDSCALE, y, 6 * EDSCALE, h); - const real_t sy = r.size.y * p_fill; - - // Note: because this bar appears over the viewport, it has to stay readable for any background color - // Draw both neutral dark and bright colors to account this - p_surface.draw_rect(r, p_color * Color(1, 1, 1, 0.2)); - p_surface.draw_rect(Rect2(r.position.x, r.position.y + r.size.y - sy, r.size.x, sy), p_color * Color(1, 1, 1, 0.6)); - p_surface.draw_rect(r.grow(1), Color(0, 0, 0, 0.7), false, Math::round(EDSCALE)); - - const Vector2 icon_size = p_icon->get_size(); - const Vector2 icon_pos = Vector2(r.position.x - (icon_size.x - r.size.x) / 2, r.position.y + r.size.y + 2 * EDSCALE); - p_surface.draw_texture(p_icon, icon_pos, p_color); - - // Draw text below the bar (for speed/zoom information). - p_surface.draw_string_outline(p_font, Vector2(icon_pos.x, icon_pos.y + icon_size.y + 16 * EDSCALE), p_text, HORIZONTAL_ALIGNMENT_LEFT, -1.f, p_font_size, Math::round(4 * EDSCALE), Color(0, 0, 0)); - p_surface.draw_string(p_font, Vector2(icon_pos.x, icon_pos.y + icon_size.y + 16 * EDSCALE), p_text, HORIZONTAL_ALIGNMENT_LEFT, -1.f, p_font_size, p_color); -} - -void Node3DEditorViewport::_draw() { - EditorNode::get_singleton()->get_editor_plugins_over()->forward_3d_draw_over_viewport(surface); - EditorNode::get_singleton()->get_editor_plugins_force_over()->forward_3d_force_draw_over_viewport(surface); - - if (surface->has_focus() || rotation_control->has_focus()) { - Size2 size = surface->get_size(); - Rect2 r = Rect2(Point2(), size); - get_theme_stylebox(SNAME("FocusViewport"), EditorStringName(EditorStyles))->draw(surface->get_canvas_item(), r); - } - - View3DController::Cursor cursor = view_3d_controller->cursor; - - if (cursor.region_select && movement_threshold_passed) { - const Rect2 selection_rect = Rect2(cursor.region_begin, cursor.region_end - cursor.region_begin); - - surface->draw_rect( - selection_rect, - get_theme_color(SNAME("box_selection_fill_color"), EditorStringName(Editor))); - - surface->draw_rect( - selection_rect, - get_theme_color(SNAME("box_selection_stroke_color"), EditorStringName(Editor)), - false, - Math::round(EDSCALE)); - } - - RID ci = surface->get_canvas_item(); - - if (message_time > 0) { - Ref font = get_theme_font(SceneStringName(font), SNAME("Label")); - int font_size = get_theme_font_size(SceneStringName(font_size), SNAME("Label")); - Point2 msgpos = Point2(10 * EDSCALE, get_size().y - 14 * EDSCALE); - font->draw_string(ci, msgpos + Point2(1, 1), message, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size, Color(0, 0, 0, 0.8)); - font->draw_string(ci, msgpos + Point2(-1, -1), message, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size, Color(0, 0, 0, 0.8)); - font->draw_string(ci, msgpos, message, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size, Color(1, 1, 1, 1)); - } - - if ((vertex_snap_mode || vertex_snap_dragging) && vertex_snap_has_source) { - const float circle_radius = 6.0f * EDSCALE; - const float outline_width = 2.0f * EDSCALE; - const float occluded_alpha = 0.30f; - - Vector3 source_display = vertex_snap_source; - if (vertex_snap_dragging) { - for (const KeyValue &E : vertex_snap_original_positions) { - Node3D *node = ObjectDB::get_instance(E.key); - if (node) { - source_display = vertex_snap_source + (node->get_global_position() - E.value); - break; - } - } - } - - if (!camera->is_position_behind(source_display)) { - Vector2 screen_pos = camera->unproject_position(source_display); - bool occluded = _is_vertex_occluded(source_display, screen_pos); - float alpha = occluded ? occluded_alpha : 1.0f; - surface->draw_circle(screen_pos, circle_radius + outline_width, Color(0, 0, 0, 0.6 * alpha), true, -1.0, true); - surface->draw_circle(screen_pos, circle_radius, Color(1, 1, 0, alpha), true, -1.0, true); - } - - if (vertex_snap_dragging && vertex_snap_has_target && !camera->is_position_behind(vertex_snap_target)) { - Vector2 screen_pos = camera->unproject_position(vertex_snap_target); - bool occluded = _is_vertex_occluded(vertex_snap_target, screen_pos); - float alpha = occluded ? occluded_alpha : 1.0f; - surface->draw_circle(screen_pos, circle_radius + outline_width, Color(0, 0, 0, 0.6 * alpha), true, -1.0, true); - surface->draw_circle(screen_pos, circle_radius, Color(0, 1, 0, alpha), true, -1.0, true); - } - } - - if (_edit.mode == TRANSFORM_ROTATE) { - Point2 center = point_to_screen(_edit.center); - - Color handle_color; - switch (_edit.plane) { - case TRANSFORM_VIEW: - handle_color = get_theme_color(SNAME("axis_view_plane_color"), EditorStringName(Editor)); - break; - case TRANSFORM_X_AXIS: - handle_color = get_theme_color(SNAME("axis_x_color"), EditorStringName(Editor)); - break; - case TRANSFORM_Y_AXIS: - handle_color = get_theme_color(SNAME("axis_y_color"), EditorStringName(Editor)); - break; - case TRANSFORM_Z_AXIS: - handle_color = get_theme_color(SNAME("axis_z_color"), EditorStringName(Editor)); - break; - default: - handle_color = get_theme_color(SNAME("accent_color"), EditorStringName(Editor)); - break; - } - - if (!_edit.is_trackball && _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); - - real_t rotation_radius = (_edit.plane == TRANSFORM_VIEW) ? spatial_editor->gizmo_view_rotation_scale : GIZMO_CIRCLE_SIZE; - - 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 * rotation_radius * (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(), handle_color.get_s() * 0.6, 1.0, 0.8); - Vector circle_colors; - circle_colors.resize(circle_points.size()); - circle_colors.fill(circle_color); - RenderingServer::get_singleton()->canvas_item_add_polyline(ci, circle_points, circle_colors, Math::round(2 * EDSCALE), true); - - const int segments = 64; - float display_angle = _edit.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 * rotation_radius * (right * Math::cos(angle1) + forward * Math::sin(angle1)); - Vector3 point2_3d = _edit.center + gizmo_scale * rotation_radius * (right * Math::cos(angle2) + forward * Math::sin(angle2)); - - Point2 point1_2d = point_to_screen(point1_3d); - Point2 point2_2d = point_to_screen(point2_3d); - - Vector triangle_points; - triangle_points.push_back(center); - 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(), handle_color.get_s() * 0.8, 1.0, 0.7); - - Vector3 start_point_3d = _edit.center + gizmo_scale * rotation_radius * 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), - true); - - Vector3 end_point_3d = _edit.center + gizmo_scale * rotation_radius * (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), - true); - } - - if (_edit.show_rotation_line) { - handle_color = handle_color.from_hsv(handle_color.get_h(), handle_color.get_s() * 0.25, 1.0, handle_color.a); - 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")); - float aspect = ss.aspect(); - Size2 s = get_size(); - - Rect2 draw_rect; - - switch (previewing->get_keep_aspect_mode()) { - case Camera3D::KEEP_WIDTH: { - draw_rect.size = Size2(s.width, s.width / aspect); - draw_rect.position.x = 0; - draw_rect.position.y = (s.height - draw_rect.size.y) * 0.5; - - } break; - case Camera3D::KEEP_HEIGHT: { - draw_rect.size = Size2(s.height * aspect, s.height); - draw_rect.position.y = 0; - draw_rect.position.x = (s.width - draw_rect.size.x) * 0.5; - - } break; - } - - draw_rect = Rect2(Vector2(), s).intersection(draw_rect); - - surface->draw_rect(draw_rect, Color(0.6, 0.6, 0.1, 0.5), false, Math::round(2 * EDSCALE)); - - } else { - if (zoom_indicator_delay > 0.0) { - if (view_3d_controller->is_freelook_enabled()) { - // Show speed. - - real_t min_speed = MAX(camera->get_near() * 4, View3DControllerConsts::ZOOM_FREELOOK_MIN); - real_t max_speed = MIN(camera->get_far() / 4, View3DControllerConsts::ZOOM_FREELOOK_MAX); - real_t scale_length = (max_speed - min_speed); - - if (!Math::is_zero_approx(scale_length)) { - float freelook_speed = view_3d_controller->get_freelook_speed(); - real_t logscale_t = 1.0 - Math::log1p(freelook_speed - min_speed) / Math::log1p(scale_length); - - // Display the freelook speed to help the user get a better sense of scale. - const int precision = freelook_speed < 1.0 ? 2 : 1; - draw_indicator_bar( - *surface, - 1.0 - logscale_t, - get_editor_theme_icon(SNAME("ViewportSpeed")), - get_theme_font("bold", EditorStringName(EditorFonts)), - get_theme_font_size(SceneStringName(font_size), SNAME("Label")), - vformat("%s m/s", String::num(freelook_speed).pad_decimals(precision)), - Color(1.0, 0.95, 0.7)); - } - } else { - // Show zoom - zoom_limit_label->set_visible(zoom_failed_attempts_count > 15); - - real_t min_distance = MAX(camera->get_near() * 4, View3DControllerConsts::ZOOM_FREELOOK_MIN); - real_t max_distance = MIN(camera->get_far() / 4, View3DControllerConsts::ZOOM_FREELOOK_MAX); - real_t scale_length = (max_distance - min_distance); - - if (!Math::is_zero_approx(scale_length)) { - real_t logscale_t = 1.0 - Math::log1p(cursor.distance - min_distance) / Math::log1p(scale_length); - - // Display the zoom center distance to help the user get a better sense of scale. - const int precision = cursor.distance < 1.0 ? 2 : 1; - draw_indicator_bar( - *surface, - logscale_t, - get_editor_theme_icon(SNAME("ViewportZoom")), - get_theme_font("bold", EditorStringName(EditorFonts)), - get_theme_font_size(SceneStringName(font_size), SNAME("Label")), - vformat("%s m", String::num(cursor.distance).pad_decimals(precision)), - Color(0.7, 0.95, 1.0)); - } - } - } - } -} - -bool Node3DEditorViewport::_camera_moved_externally() { - if (previewing_camera && previewing) { - if (pilot_preview_enabled) { - Transform3D t = previewing->get_global_transform(); - return !t.is_equal_approx(last_camera_transform); - } - return false; - } - Transform3D t = camera->get_global_transform(); - return !t.is_equal_approx(last_camera_transform); -} - -void Node3DEditorViewport::_apply_camera_transform_to_cursor() { - if (previewing_camera && previewing) { - if (pilot_preview_enabled) { - _sync_cursor_from_transform(previewing->get_global_transform()); - } - return; - } - _sync_cursor_from_transform(camera->get_camera_transform()); -} - -void Node3DEditorViewport::_menu_option(int p_option) { - EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); - switch (p_option) { - case VIEW_TOP: { - view_3d_controller->cursor.y_rot = 0; - view_3d_controller->cursor.x_rot = Math::PI / 2.0; - view_3d_controller->cursor.unsnapped_y_rot = view_3d_controller->cursor.y_rot; - view_3d_controller->cursor.unsnapped_x_rot = view_3d_controller->cursor.x_rot; - set_message(TTR("Top View."), 2); - view_3d_controller->set_view_type(View3DController::VIEW_TYPE_TOP); - - } break; - case VIEW_BOTTOM: { - view_3d_controller->cursor.y_rot = 0; - view_3d_controller->cursor.x_rot = -Math::PI / 2.0; - view_3d_controller->cursor.unsnapped_y_rot = view_3d_controller->cursor.y_rot; - view_3d_controller->cursor.unsnapped_x_rot = view_3d_controller->cursor.x_rot; - set_message(TTR("Bottom View."), 2); - view_3d_controller->set_view_type(View3DController::VIEW_TYPE_BOTTOM); - - } break; - case VIEW_LEFT: { - view_3d_controller->cursor.x_rot = 0; - view_3d_controller->cursor.y_rot = Math::PI / 2.0; - view_3d_controller->cursor.unsnapped_x_rot = view_3d_controller->cursor.x_rot; - view_3d_controller->cursor.unsnapped_y_rot = view_3d_controller->cursor.y_rot; - set_message(TTR("Left View."), 2); - view_3d_controller->set_view_type(View3DController::VIEW_TYPE_LEFT); - - } break; - case VIEW_RIGHT: { - view_3d_controller->cursor.x_rot = 0; - view_3d_controller->cursor.y_rot = -Math::PI / 2.0; - view_3d_controller->cursor.unsnapped_x_rot = view_3d_controller->cursor.x_rot; - view_3d_controller->cursor.unsnapped_y_rot = view_3d_controller->cursor.y_rot; - set_message(TTR("Right View."), 2); - view_3d_controller->set_view_type(View3DController::VIEW_TYPE_RIGHT); - - } break; - case VIEW_FRONT: { - view_3d_controller->cursor.x_rot = 0; - view_3d_controller->cursor.y_rot = 0; - view_3d_controller->cursor.unsnapped_x_rot = view_3d_controller->cursor.x_rot; - view_3d_controller->cursor.unsnapped_y_rot = view_3d_controller->cursor.y_rot; - set_message(TTR("Front View."), 2); - view_3d_controller->set_view_type(View3DController::VIEW_TYPE_FRONT); - - } break; - case VIEW_REAR: { - view_3d_controller->cursor.x_rot = 0; - view_3d_controller->cursor.y_rot = Math::PI; - view_3d_controller->cursor.unsnapped_x_rot = view_3d_controller->cursor.x_rot; - view_3d_controller->cursor.unsnapped_y_rot = view_3d_controller->cursor.y_rot; - set_message(TTR("Rear View."), 2); - view_3d_controller->set_view_type(View3DController::VIEW_TYPE_REAR); - - } break; - case VIEW_CENTER_TO_ORIGIN: { - view_3d_controller->cursor.pos = Vector3(0, 0, 0); - _disable_follow_mode(); - - } break; - case VIEW_CENTER_TO_SELECTION: { - focus_selection(); - - } break; - case VIEW_ALIGN_TRANSFORM_WITH_VIEW: { - if (!get_selected_count()) { - break; - } - - Transform3D camera_transform = camera->get_global_transform(); - - const List &selection = editor_selection->get_top_selected_node_list(); - - undo_redo->create_action(TTR("Align Transform with View")); - for (Node *E : selection) { - Node3D *sp = Object::cast_to(E); - if (!sp) { - continue; - } - - Node3DEditorSelectedItem *se = editor_selection->get_node_editor_data(sp); - if (!se) { - continue; - } - - Transform3D xform = camera_transform; - if (view_3d_controller->is_orthogonal()) { - Vector3 offset = camera_transform.basis.xform(Vector3(0, 0, view_3d_controller->cursor.distance)); - xform.origin = view_3d_controller->cursor.pos + offset; - } else { - xform.scale_basis(sp->get_scale()); - } - - if (Object::cast_to(E)) { - // Adjust rotation to match Decal's default orientation. - // This makes the decal "look" in the same direction as the camera, - // rather than pointing down relative to the camera orientation. - xform.basis.rotate_local(Vector3(1, 0, 0), Math::TAU * 0.25); - } - - Node3D *parent = sp->get_parent_node_3d(); - Transform3D local_xform = parent ? parent->get_global_transform().affine_inverse() * xform : xform; - undo_redo->add_do_method(sp, "set_transform", local_xform); - undo_redo->add_undo_method(sp, "set_transform", sp->get_local_gizmo_transform()); - } - undo_redo->commit_action(); - - } break; - case VIEW_ALIGN_ROTATION_WITH_VIEW: { - if (!get_selected_count()) { - break; - } - - Transform3D camera_transform = camera->get_global_transform(); - - const List &selection = editor_selection->get_top_selected_node_list(); - - undo_redo->create_action(TTR("Align Rotation with View")); - for (Node *E : selection) { - Node3D *sp = Object::cast_to(E); - if (!sp) { - continue; - } - - Node3DEditorSelectedItem *se = editor_selection->get_node_editor_data(sp); - if (!se) { - continue; - } - - Basis basis = camera_transform.basis; - - if (Object::cast_to(E)) { - // Adjust rotation to match Decal's default orientation. - // This makes the decal "look" in the same direction as the camera, - // rather than pointing down relative to the camera orientation. - basis.rotate_local(Vector3(1, 0, 0), Math::TAU * 0.25); - } - - undo_redo->add_do_method(sp, "set_rotation", basis.get_euler_normalized()); - undo_redo->add_undo_method(sp, "set_rotation", sp->get_rotation()); - } - undo_redo->commit_action(); - - } break; - case VIEW_ENVIRONMENT: { - int idx = view_display_menu->get_popup()->get_item_index(VIEW_ENVIRONMENT); - bool current = view_display_menu->get_popup()->is_item_checked(idx); - current = !current; - if (current) { - camera->set_environment(Ref()); - } else { - camera->set_environment(Node3DEditor::get_singleton()->get_viewport_environment()); - } - - view_display_menu->get_popup()->set_item_checked(idx, current); - - } break; - case VIEW_PERSPECTIVE: { - view_display_menu->get_popup()->set_item_checked(view_display_menu->get_popup()->get_item_index(VIEW_PERSPECTIVE), true); - view_display_menu->get_popup()->set_item_checked(view_display_menu->get_popup()->get_item_index(VIEW_ORTHOGONAL), false); - view_3d_controller->set_orthogonal(false); - callable_mp(this, &Node3DEditorViewport::update_transform_gizmo_view).call_deferred(); - view_3d_controller->update_camera(); - _update_name(); - - } break; - case VIEW_ORTHOGONAL: { - view_display_menu->get_popup()->set_item_checked(view_display_menu->get_popup()->get_item_index(VIEW_PERSPECTIVE), false); - view_display_menu->get_popup()->set_item_checked(view_display_menu->get_popup()->get_item_index(VIEW_ORTHOGONAL), true); - view_3d_controller->set_orthogonal(true); - callable_mp(this, &Node3DEditorViewport::update_transform_gizmo_view).call_deferred(); - view_3d_controller->update_camera(); - _update_name(); - } break; - case VIEW_SWITCH_PERSPECTIVE_ORTHOGONAL: { - _menu_option(view_3d_controller->is_orthogonal() ? VIEW_PERSPECTIVE : VIEW_ORTHOGONAL); - - } break; - case VIEW_AUTO_ORTHOGONAL: { - int idx = view_display_menu->get_popup()->get_item_index(VIEW_AUTO_ORTHOGONAL); - bool current = view_display_menu->get_popup()->is_item_checked(idx); - current = !current; - view_display_menu->get_popup()->set_item_checked(idx, current); - view_3d_controller->set_auto_orthogonal_allowed(current); - _update_name(); - } break; - case VIEW_LOCK_ROTATION: { - int idx = view_display_menu->get_popup()->get_item_index(VIEW_LOCK_ROTATION); - bool current = view_display_menu->get_popup()->is_item_checked(idx); - _set_lock_view_rotation(!current); - - } break; - case VIEW_AUDIO_LISTENER: { - int idx = view_display_menu->get_popup()->get_item_index(VIEW_AUDIO_LISTENER); - bool current = view_display_menu->get_popup()->is_item_checked(idx); - current = !current; - viewport->set_as_audio_listener_3d(current); - view_display_menu->get_popup()->set_item_checked(idx, current); - - } break; - case VIEW_AUDIO_DOPPLER: { - int idx = view_display_menu->get_popup()->get_item_index(VIEW_AUDIO_DOPPLER); - bool current = view_display_menu->get_popup()->is_item_checked(idx); - current = !current; - camera->set_doppler_tracking(current ? Camera3D::DOPPLER_TRACKING_IDLE_STEP : Camera3D::DOPPLER_TRACKING_DISABLED); - view_display_menu->get_popup()->set_item_checked(idx, current); - - } break; - case VIEW_CINEMATIC_PREVIEW: { - int idx = view_display_menu->get_popup()->get_item_index(VIEW_CINEMATIC_PREVIEW); - bool current = view_display_menu->get_popup()->is_item_checked(idx); - current = !current; - view_display_menu->get_popup()->set_item_checked(idx, current); - _toggle_cinema_preview(current); - - cinema_label->set_visible(current); - _update_centered_labels(); - surface->queue_redraw(); - - if (current) { - preview_camera->hide(); - } else { - if (previewing != nullptr) { - preview_camera->show(); - } - } - } break; - case VIEW_GIZMOS: { - int idx = view_display_menu->get_popup()->get_item_index(VIEW_GIZMOS); - bool current = view_display_menu->get_popup()->is_item_checked(idx); - current = !current; - uint32_t layers = camera->get_cull_mask(); - layers &= ~(1 << GIZMO_EDIT_LAYER); - if (current) { - layers |= (1 << GIZMO_EDIT_LAYER); - } - camera->set_cull_mask(layers); - view_display_menu->get_popup()->set_item_checked(idx, current); - - } break; - case VIEW_TRANSFORM_GIZMO: { - int idx = view_display_menu->get_popup()->get_item_index(VIEW_TRANSFORM_GIZMO); - bool current = view_display_menu->get_popup()->is_item_checked(idx); - current = !current; - transform_gizmo_visible = current; - - spatial_editor->update_transform_gizmo(); - view_display_menu->get_popup()->set_item_checked(idx, current); - } break; - case VIEW_HALF_RESOLUTION: { - int idx = view_display_menu->get_popup()->get_item_index(VIEW_HALF_RESOLUTION); - bool current = view_display_menu->get_popup()->is_item_checked(idx); - view_display_menu->get_popup()->set_item_checked(idx, !current); - _update_shrink(); - } break; - case VIEW_INFORMATION: { - int idx = view_display_menu->get_popup()->get_item_index(VIEW_INFORMATION); - bool current = view_display_menu->get_popup()->is_item_checked(idx); - view_display_menu->get_popup()->set_item_checked(idx, !current); - - } break; - case VIEW_FRAME_TIME: { - int idx = view_display_menu->get_popup()->get_item_index(VIEW_FRAME_TIME); - bool current = view_display_menu->get_popup()->is_item_checked(idx); - view_display_menu->get_popup()->set_item_checked(idx, !current); - } break; - case VIEW_GRID: { - int idx = view_display_menu->get_popup()->get_item_index(VIEW_GRID); - bool current = view_display_menu->get_popup()->is_item_checked(idx); - current = !current; - uint32_t layers = camera->get_cull_mask(); - layers &= ~(1 << GIZMO_GRID_LAYER); - if (current) { - layers |= (1 << GIZMO_GRID_LAYER); - } - camera->set_cull_mask(layers); - view_display_menu->get_popup()->set_item_checked(idx, current); - } break; - case VIEW_DISPLAY_NORMAL: - case VIEW_DISPLAY_WIREFRAME: - case VIEW_DISPLAY_OVERDRAW: - case VIEW_DISPLAY_UNSHADED: - case VIEW_DISPLAY_LIGHTING: - case VIEW_DISPLAY_NORMAL_BUFFER: - case VIEW_DISPLAY_DEBUG_SHADOW_ATLAS: - case VIEW_DISPLAY_DEBUG_DIRECTIONAL_SHADOW_ATLAS: - case VIEW_DISPLAY_DEBUG_VOXEL_GI_ALBEDO: - case VIEW_DISPLAY_DEBUG_VOXEL_GI_LIGHTING: - case VIEW_DISPLAY_DEBUG_VOXEL_GI_EMISSION: - case VIEW_DISPLAY_DEBUG_SCENE_LUMINANCE: - case VIEW_DISPLAY_DEBUG_SSAO: - case VIEW_DISPLAY_DEBUG_SSIL: - case VIEW_DISPLAY_DEBUG_PSSM_SPLITS: - case VIEW_DISPLAY_DEBUG_DECAL_ATLAS: - case VIEW_DISPLAY_DEBUG_AREA_LIGHT_ATLAS: - case VIEW_DISPLAY_DEBUG_SDFGI: - case VIEW_DISPLAY_DEBUG_SDFGI_PROBES: - case VIEW_DISPLAY_DEBUG_GI_BUFFER: - case VIEW_DISPLAY_DEBUG_DISABLE_LOD: - case VIEW_DISPLAY_DEBUG_CLUSTER_OMNI_LIGHTS: - case VIEW_DISPLAY_DEBUG_CLUSTER_SPOT_LIGHTS: - case VIEW_DISPLAY_DEBUG_CLUSTER_AREA_LIGHTS: - case VIEW_DISPLAY_DEBUG_CLUSTER_DECALS: - case VIEW_DISPLAY_DEBUG_CLUSTER_REFLECTION_PROBES: - case VIEW_DISPLAY_DEBUG_OCCLUDERS: - case VIEW_DISPLAY_MOTION_VECTORS: - case VIEW_DISPLAY_INTERNAL_BUFFER: { - static const int display_options[] = { - VIEW_DISPLAY_NORMAL, - VIEW_DISPLAY_WIREFRAME, - VIEW_DISPLAY_OVERDRAW, - VIEW_DISPLAY_UNSHADED, - VIEW_DISPLAY_LIGHTING, - VIEW_DISPLAY_NORMAL_BUFFER, - VIEW_DISPLAY_DEBUG_SHADOW_ATLAS, - VIEW_DISPLAY_DEBUG_DIRECTIONAL_SHADOW_ATLAS, - VIEW_DISPLAY_DEBUG_VOXEL_GI_ALBEDO, - VIEW_DISPLAY_DEBUG_VOXEL_GI_LIGHTING, - VIEW_DISPLAY_DEBUG_VOXEL_GI_EMISSION, - VIEW_DISPLAY_DEBUG_SCENE_LUMINANCE, - VIEW_DISPLAY_DEBUG_SSAO, - VIEW_DISPLAY_DEBUG_SSIL, - VIEW_DISPLAY_DEBUG_GI_BUFFER, - VIEW_DISPLAY_DEBUG_DISABLE_LOD, - VIEW_DISPLAY_DEBUG_PSSM_SPLITS, - VIEW_DISPLAY_DEBUG_DECAL_ATLAS, - VIEW_DISPLAY_DEBUG_AREA_LIGHT_ATLAS, - VIEW_DISPLAY_DEBUG_SDFGI, - VIEW_DISPLAY_DEBUG_SDFGI_PROBES, - VIEW_DISPLAY_DEBUG_CLUSTER_OMNI_LIGHTS, - VIEW_DISPLAY_DEBUG_CLUSTER_SPOT_LIGHTS, - VIEW_DISPLAY_DEBUG_CLUSTER_AREA_LIGHTS, - VIEW_DISPLAY_DEBUG_CLUSTER_DECALS, - VIEW_DISPLAY_DEBUG_CLUSTER_REFLECTION_PROBES, - VIEW_DISPLAY_DEBUG_OCCLUDERS, - VIEW_DISPLAY_MOTION_VECTORS, - VIEW_DISPLAY_INTERNAL_BUFFER, - VIEW_MAX - }; - static const Viewport::DebugDraw debug_draw_modes[] = { - Viewport::DEBUG_DRAW_DISABLED, - Viewport::DEBUG_DRAW_WIREFRAME, - Viewport::DEBUG_DRAW_OVERDRAW, - Viewport::DEBUG_DRAW_UNSHADED, - Viewport::DEBUG_DRAW_LIGHTING, - Viewport::DEBUG_DRAW_NORMAL_BUFFER, - Viewport::DEBUG_DRAW_SHADOW_ATLAS, - Viewport::DEBUG_DRAW_DIRECTIONAL_SHADOW_ATLAS, - Viewport::DEBUG_DRAW_VOXEL_GI_ALBEDO, - Viewport::DEBUG_DRAW_VOXEL_GI_LIGHTING, - Viewport::DEBUG_DRAW_VOXEL_GI_EMISSION, - Viewport::DEBUG_DRAW_SCENE_LUMINANCE, - Viewport::DEBUG_DRAW_SSAO, - Viewport::DEBUG_DRAW_SSIL, - Viewport::DEBUG_DRAW_GI_BUFFER, - Viewport::DEBUG_DRAW_DISABLE_LOD, - Viewport::DEBUG_DRAW_PSSM_SPLITS, - Viewport::DEBUG_DRAW_DECAL_ATLAS, - Viewport::DEBUG_DRAW_AREA_LIGHT_ATLAS, - Viewport::DEBUG_DRAW_SDFGI, - Viewport::DEBUG_DRAW_SDFGI_PROBES, - Viewport::DEBUG_DRAW_CLUSTER_OMNI_LIGHTS, - Viewport::DEBUG_DRAW_CLUSTER_SPOT_LIGHTS, - Viewport::DEBUG_DRAW_CLUSTER_AREA_LIGHTS, - Viewport::DEBUG_DRAW_CLUSTER_DECALS, - Viewport::DEBUG_DRAW_CLUSTER_REFLECTION_PROBES, - Viewport::DEBUG_DRAW_OCCLUDERS, - Viewport::DEBUG_DRAW_MOTION_VECTORS, - Viewport::DEBUG_DRAW_INTERNAL_BUFFER, - }; - - for (int idx = 0; display_options[idx] != VIEW_MAX; idx++) { - int id = display_options[idx]; - int item_idx = view_display_menu->get_popup()->get_item_index(id); - if (item_idx != -1) { - view_display_menu->get_popup()->set_item_checked(item_idx, id == p_option); - } - item_idx = display_submenu->get_item_index(id); - if (item_idx != -1) { - display_submenu->set_item_checked(item_idx, id == p_option); - } - - if (id == p_option) { - viewport->set_debug_draw(debug_draw_modes[idx]); - } - } - } break; - } -} - -void Node3DEditorViewport::_preview_exited_scene() { - preview_camera->disconnect(SceneStringName(toggled), callable_mp(this, &Node3DEditorViewport::_toggle_camera_preview)); - preview_camera->set_pressed(false); - _toggle_camera_preview(false); - preview_camera->connect(SceneStringName(toggled), callable_mp(this, &Node3DEditorViewport::_toggle_camera_preview)); - view_display_menu->show(); -} - -void Node3DEditorViewport::_preview_camera_property_changed() { - if (previewing) { - surface->queue_redraw(); - } -} - -void Node3DEditorViewport::_sync_cursor_from_transform(const Transform3D &p_transform) { - const Basis basis = p_transform.basis; - - view_3d_controller->cursor.eye_pos = p_transform.origin; - view_3d_controller->cursor.x_rot = -basis.get_euler().x; - view_3d_controller->cursor.y_rot = -basis.get_euler().y; - view_3d_controller->cursor.unsnapped_x_rot = view_3d_controller->cursor.x_rot; - view_3d_controller->cursor.unsnapped_y_rot = view_3d_controller->cursor.y_rot; - - real_t distance = view_3d_controller->cursor.distance; - if (view_3d_controller->is_orthogonal()) { - distance = (get_zfar() - get_znear()) / 2.0; - } - view_3d_controller->cursor.pos = p_transform.origin - basis.get_column(2) * distance; -} - -void Node3DEditorViewport::_update_centered_labels() { - if (cinema_label->is_visible()) { - cinema_label->reset_size(); - float cinema_half_width = cinema_label->get_size().width / 2.0f; - cinema_label->set_anchor_and_offset(SIDE_LEFT, 0.5f, -cinema_half_width); - } - - if (locked_label->is_visible()) { - locked_label->reset_size(); - float locked_half_width = locked_label->get_size().width / 2.0f; - locked_label->set_anchor_and_offset(SIDE_LEFT, 0.5f, -locked_half_width); - } -} - -void Node3DEditorViewport::_init_gizmo_instance(int p_idx) { - uint32_t layer = 1 << (GIZMO_BASE_LAYER + p_idx); - - for (int i = 0; i < 3; i++) { - move_gizmo_instance[i] = RS::get_singleton()->instance_create(); - RS::get_singleton()->instance_set_base(move_gizmo_instance[i], spatial_editor->get_move_gizmo(i)->get_rid()); - RS::get_singleton()->instance_set_scenario(move_gizmo_instance[i], get_tree()->get_root()->get_world_3d()->get_scenario()); - RS::get_singleton()->instance_set_visible(move_gizmo_instance[i], false); - RS::get_singleton()->instance_geometry_set_cast_shadows_setting(move_gizmo_instance[i], RSE::SHADOW_CASTING_SETTING_OFF); - RS::get_singleton()->instance_set_layer_mask(move_gizmo_instance[i], layer); - RS::get_singleton()->instance_geometry_set_flag(move_gizmo_instance[i], RSE::INSTANCE_FLAG_IGNORE_OCCLUSION_CULLING, true); - RS::get_singleton()->instance_geometry_set_flag(move_gizmo_instance[i], RSE::INSTANCE_FLAG_USE_BAKED_LIGHT, false); - - move_plane_gizmo_instance[i] = RS::get_singleton()->instance_create(); - RS::get_singleton()->instance_set_base(move_plane_gizmo_instance[i], spatial_editor->get_move_plane_gizmo(i)->get_rid()); - RS::get_singleton()->instance_set_scenario(move_plane_gizmo_instance[i], get_tree()->get_root()->get_world_3d()->get_scenario()); - RS::get_singleton()->instance_set_visible(move_plane_gizmo_instance[i], false); - RS::get_singleton()->instance_geometry_set_cast_shadows_setting(move_plane_gizmo_instance[i], RSE::SHADOW_CASTING_SETTING_OFF); - RS::get_singleton()->instance_set_layer_mask(move_plane_gizmo_instance[i], layer); - RS::get_singleton()->instance_geometry_set_flag(move_plane_gizmo_instance[i], RSE::INSTANCE_FLAG_IGNORE_OCCLUSION_CULLING, true); - RS::get_singleton()->instance_geometry_set_flag(move_plane_gizmo_instance[i], RSE::INSTANCE_FLAG_USE_BAKED_LIGHT, false); - - scale_gizmo_instance[i] = RS::get_singleton()->instance_create(); - RS::get_singleton()->instance_set_base(scale_gizmo_instance[i], spatial_editor->get_scale_gizmo(i)->get_rid()); - RS::get_singleton()->instance_set_scenario(scale_gizmo_instance[i], get_tree()->get_root()->get_world_3d()->get_scenario()); - RS::get_singleton()->instance_set_visible(scale_gizmo_instance[i], false); - RS::get_singleton()->instance_geometry_set_cast_shadows_setting(scale_gizmo_instance[i], RSE::SHADOW_CASTING_SETTING_OFF); - RS::get_singleton()->instance_set_layer_mask(scale_gizmo_instance[i], layer); - RS::get_singleton()->instance_geometry_set_flag(scale_gizmo_instance[i], RSE::INSTANCE_FLAG_IGNORE_OCCLUSION_CULLING, true); - RS::get_singleton()->instance_geometry_set_flag(scale_gizmo_instance[i], RSE::INSTANCE_FLAG_USE_BAKED_LIGHT, false); - - scale_plane_gizmo_instance[i] = RS::get_singleton()->instance_create(); - RS::get_singleton()->instance_set_base(scale_plane_gizmo_instance[i], spatial_editor->get_scale_plane_gizmo(i)->get_rid()); - RS::get_singleton()->instance_set_scenario(scale_plane_gizmo_instance[i], get_tree()->get_root()->get_world_3d()->get_scenario()); - RS::get_singleton()->instance_set_visible(scale_plane_gizmo_instance[i], false); - RS::get_singleton()->instance_geometry_set_cast_shadows_setting(scale_plane_gizmo_instance[i], RSE::SHADOW_CASTING_SETTING_OFF); - RS::get_singleton()->instance_set_layer_mask(scale_plane_gizmo_instance[i], layer); - RS::get_singleton()->instance_geometry_set_flag(scale_plane_gizmo_instance[i], RSE::INSTANCE_FLAG_IGNORE_OCCLUSION_CULLING, true); - RS::get_singleton()->instance_geometry_set_flag(scale_plane_gizmo_instance[i], RSE::INSTANCE_FLAG_USE_BAKED_LIGHT, false); - - axis_gizmo_instance[i] = RS::get_singleton()->instance_create(); - } - - for (int i = 0; i < 3; i++) { - RS::get_singleton()->instance_set_base(axis_gizmo_instance[i], spatial_editor->get_axis_gizmo(i)->get_rid()); - RS::get_singleton()->instance_set_scenario(axis_gizmo_instance[i], get_tree()->get_root()->get_world_3d()->get_scenario()); - RS::get_singleton()->instance_set_visible(axis_gizmo_instance[i], true); - RS::get_singleton()->instance_geometry_set_cast_shadows_setting(axis_gizmo_instance[i], RSE::SHADOW_CASTING_SETTING_OFF); - RS::get_singleton()->instance_set_layer_mask(axis_gizmo_instance[i], layer); - RS::get_singleton()->instance_geometry_set_flag(axis_gizmo_instance[i], RSE::INSTANCE_FLAG_IGNORE_OCCLUSION_CULLING, true); - RS::get_singleton()->instance_geometry_set_flag(axis_gizmo_instance[i], RSE::INSTANCE_FLAG_USE_BAKED_LIGHT, false); - } - - for (int i = 0; i < 4; i++) { - rotate_gizmo_instance[i] = RS::get_singleton()->instance_create(); - RS::get_singleton()->instance_set_base(rotate_gizmo_instance[i], spatial_editor->get_rotate_gizmo(i)->get_rid()); - RS::get_singleton()->instance_set_scenario(rotate_gizmo_instance[i], get_tree()->get_root()->get_world_3d()->get_scenario()); - RS::get_singleton()->instance_set_visible(rotate_gizmo_instance[i], false); - RS::get_singleton()->instance_geometry_set_cast_shadows_setting(rotate_gizmo_instance[i], RSE::SHADOW_CASTING_SETTING_OFF); - RS::get_singleton()->instance_set_layer_mask(rotate_gizmo_instance[i], layer); - RS::get_singleton()->instance_geometry_set_flag(rotate_gizmo_instance[i], RSE::INSTANCE_FLAG_IGNORE_OCCLUSION_CULLING, true); - RS::get_singleton()->instance_geometry_set_flag(rotate_gizmo_instance[i], RSE::INSTANCE_FLAG_USE_BAKED_LIGHT, false); - } - - // Create trackball sphere instance - trackball_sphere_instance = RS::get_singleton()->instance_create(); - RS::get_singleton()->instance_set_base(trackball_sphere_instance, spatial_editor->get_trackball_sphere_gizmo()->get_rid()); - RS::get_singleton()->instance_set_scenario(trackball_sphere_instance, get_tree()->get_root()->get_world_3d()->get_scenario()); - RS::get_singleton()->instance_set_visible(trackball_sphere_instance, false); - RS::get_singleton()->instance_geometry_set_cast_shadows_setting(trackball_sphere_instance, RSE::SHADOW_CASTING_SETTING_OFF); - RS::get_singleton()->instance_set_layer_mask(trackball_sphere_instance, layer); - RS::get_singleton()->instance_geometry_set_flag(trackball_sphere_instance, RSE::INSTANCE_FLAG_IGNORE_OCCLUSION_CULLING, true); - RS::get_singleton()->instance_geometry_set_flag(trackball_sphere_instance, RSE::INSTANCE_FLAG_USE_BAKED_LIGHT, false); -} - -void Node3DEditorViewport::_finish_gizmo_instances() { - ERR_FAIL_NULL(RenderingServer::get_singleton()); - for (int i = 0; i < 3; i++) { - RS::get_singleton()->free_rid(move_gizmo_instance[i]); - RS::get_singleton()->free_rid(move_plane_gizmo_instance[i]); - RS::get_singleton()->free_rid(rotate_gizmo_instance[i]); - RS::get_singleton()->free_rid(scale_gizmo_instance[i]); - RS::get_singleton()->free_rid(scale_plane_gizmo_instance[i]); - RS::get_singleton()->free_rid(axis_gizmo_instance[i]); - } - // Rotation white outline - RS::get_singleton()->free_rid(rotate_gizmo_instance[3]); - - RS::get_singleton()->free_rid(trackball_sphere_instance); -} - -void Node3DEditorViewport::_disable_follow_mode() { - // Exit follow mode by resetting the number of times the follow shortcut was used consecutively. - times_focused_consecutively = 0; -} - -void Node3DEditorViewport::_reset_follow_mode_count() { - bool is_in_follow_mode = times_focused_consecutively >= 2 && times_focused_consecutively % 2 == 0; - if (!is_in_follow_mode) { - times_focused_consecutively = 0; - } -} - -void Node3DEditorViewport::_toggle_camera_preview(bool p_activate) { - ERR_FAIL_COND(p_activate && !preview); - ERR_FAIL_COND(!p_activate && !previewing); - - emit_signal(SNAME("clicked")); - previewing_camera = p_activate; - _update_navigation_controls_visibility(); - - if (!p_activate) { - _pilot_commit_undo_session(); - previewing->disconnect(SceneStringName(tree_exiting), callable_mp(this, &Node3DEditorViewport::_preview_exited_scene)); - previewing->disconnect(CoreStringName(property_list_changed), callable_mp(this, &Node3DEditorViewport::_preview_camera_property_changed)); - previewing = nullptr; - RS::get_singleton()->viewport_attach_camera(viewport->get_viewport_rid(), camera->get_camera()); //restore - if (!preview) { - preview_camera->hide(); - } - pilot_camera->hide(); - pilot_preview_enabled = false; - pilot_camera->set_pressed(false); - - _apply_camera_transform_to_cursor(); - view_3d_controller->update_camera(0); - last_camera_transform = camera->get_global_transform(); - - surface->queue_redraw(); - - } else { - previewing = preview; - previewing->connect(SceneStringName(tree_exiting), callable_mp(this, &Node3DEditorViewport::_preview_exited_scene)); - previewing->connect(CoreStringName(property_list_changed), callable_mp(this, &Node3DEditorViewport::_preview_camera_property_changed)); - RS::get_singleton()->viewport_attach_camera(viewport->get_viewport_rid(), preview->get_camera()); //replace - - _sync_cursor_from_transform(preview->get_global_transform()); - view_3d_controller->update_camera(0); - - pilot_camera->show(); - - if (Input::get_singleton()->is_key_pressed(Key::SHIFT)) { - pilot_camera->set_pressed(true); - } - - surface->queue_redraw(); - } -} - -void Node3DEditorViewport::_toggle_pilot_preview(bool p_activate) { - if (!p_activate) { - _pilot_commit_undo_session(); - } - pilot_preview_enabled = p_activate; - if (p_activate && previewing) { - _sync_cursor_from_transform(previewing->get_global_transform()); - view_3d_controller->update_camera(0); - } -} - -void Node3DEditorViewport::_toggle_cinema_preview(bool p_activate) { - previewing_cinema = p_activate; - _update_navigation_controls_visibility(); - - if (!previewing_cinema) { - if (previewing != nullptr) { - previewing->disconnect(SceneStringName(tree_exited), callable_mp(this, &Node3DEditorViewport::_preview_exited_scene)); - previewing->disconnect(CoreStringName(property_list_changed), callable_mp(this, &Node3DEditorViewport::_preview_camera_property_changed)); - } - - previewing = nullptr; - RS::get_singleton()->viewport_attach_camera(viewport->get_viewport_rid(), camera->get_camera()); //restore - preview_camera->set_pressed(false); - if (!preview) { - preview_camera->hide(); - } else { - preview_camera->show(); - } - view_display_menu->show(); - surface->queue_redraw(); - } -} - -void Node3DEditorViewport::_selection_result_pressed(int p_result) { - if (selection_results_menu.size() <= p_result) { - return; - } - - clicked = selection_results_menu[p_result]->get_instance_id(); - - if (clicked.is_valid()) { - _select_clicked(true); - } - - selection_results_menu.clear(); -} - -void Node3DEditorViewport::_selection_menu_hide() { - selection_results.clear(); - selection_menu->clear(); - selection_menu->reset_size(); -} - -void Node3DEditorViewport::set_can_preview(Camera3D *p_preview) { - preview = p_preview; - - if (!preview_camera->is_pressed() && !previewing_cinema) { - preview_camera->set_visible(p_preview); - } -} - -void Node3DEditorViewport::switch_preview_camera(Camera3D *p_new_camera) { - if (!previewing_camera || !previewing || !p_new_camera || p_new_camera == previewing) { - return; - } - - _pilot_commit_undo_session(); - - previewing->disconnect(SceneStringName(tree_exiting), callable_mp(this, &Node3DEditorViewport::_preview_exited_scene)); - previewing->disconnect(CoreStringName(property_list_changed), callable_mp(this, &Node3DEditorViewport::_preview_camera_property_changed)); - - previewing = p_new_camera; - previewing->connect(SceneStringName(tree_exiting), callable_mp(this, &Node3DEditorViewport::_preview_exited_scene)); - previewing->connect(CoreStringName(property_list_changed), callable_mp(this, &Node3DEditorViewport::_preview_camera_property_changed)); - RS::get_singleton()->viewport_attach_camera(viewport->get_viewport_rid(), p_new_camera->get_camera()); - - _sync_cursor_from_transform(p_new_camera->get_global_transform()); - view_3d_controller->update_camera(0); - - surface->queue_redraw(); -} - -void Node3DEditorViewport::update_transform_gizmo_view() { - if (!is_visible_in_tree()) { - return; - } - - Transform3D xform = spatial_editor->get_gizmo_transform(); - - Transform3D camera_xform = camera->get_transform(); - - if (xform.origin.is_equal_approx(camera_xform.origin)) { - for (int i = 0; i < 3; i++) { - RenderingServer::get_singleton()->instance_set_visible(move_gizmo_instance[i], false); - RenderingServer::get_singleton()->instance_set_visible(move_plane_gizmo_instance[i], false); - RenderingServer::get_singleton()->instance_set_visible(rotate_gizmo_instance[i], false); - RenderingServer::get_singleton()->instance_set_visible(scale_gizmo_instance[i], false); - RenderingServer::get_singleton()->instance_set_visible(scale_plane_gizmo_instance[i], false); - RenderingServer::get_singleton()->instance_set_visible(axis_gizmo_instance[i], false); - } - RenderingServer::get_singleton()->instance_set_visible(rotate_gizmo_instance[3], false); - return; - } - - const Vector3 camz = -camera_xform.get_basis().get_column(2).normalized(); - const Vector3 camy = -camera_xform.get_basis().get_column(1).normalized(); - const Plane p = Plane(camz, camera_xform.origin); - const real_t gizmo_d = MAX(Math::abs(p.distance_to(xform.origin)), CMP_EPSILON); - const real_t d0 = camera->unproject_position(camera_xform.origin + camz * gizmo_d).y; - const real_t d1 = camera->unproject_position(camera_xform.origin + camz * gizmo_d + camy).y; - const real_t dd = MAX(Math::abs(d0 - d1), CMP_EPSILON); - - const real_t gizmo_size = EDITOR_GET("editors/3d/manipulator_gizmo_size"); - // At low viewport heights, multiply the gizmo scale based on the viewport height. - // This prevents the gizmo from growing very large and going outside the viewport. - const int viewport_base_height = 400 * MAX(1, EDSCALE); - gizmo_scale = - (gizmo_size / Math::abs(dd)) * MAX(1, EDSCALE) * - MIN(viewport_base_height, subviewport_container->get_size().height) / viewport_base_height; - Vector3 scale = Vector3(1, 1, 1) * gizmo_scale; - - // If the determinant is zero, we should disable the gizmo from being rendered, - // this prevents supplying bad values to the renderer and then having to filter it out again. - if (xform.basis.determinant() == 0) { - for (int i = 0; i < 3; i++) { - RenderingServer::get_singleton()->instance_set_visible(move_gizmo_instance[i], false); - RenderingServer::get_singleton()->instance_set_visible(move_plane_gizmo_instance[i], false); - RenderingServer::get_singleton()->instance_set_visible(rotate_gizmo_instance[i], false); - RenderingServer::get_singleton()->instance_set_visible(scale_gizmo_instance[i], false); - RenderingServer::get_singleton()->instance_set_visible(scale_plane_gizmo_instance[i], false); - RenderingServer::get_singleton()->instance_set_visible(axis_gizmo_instance[i], false); - } - RenderingServer::get_singleton()->instance_set_visible(rotate_gizmo_instance[3], false); - return; - } - - bool local_coords = spatial_editor->are_local_coords_enabled(); - bool arc_visible = _is_rotation_arc_visible(); - int show_gizmo_flags = EDITOR_GET("editors/3d/show_gizmo_during_rotation"); - - bool keep_gizmo_visible = arc_visible && ((local_coords && (show_gizmo_flags & Node3DEditor::TRANSFORM_MODE_LOCAL)) || (!local_coords && (show_gizmo_flags & Node3DEditor::TRANSFORM_MODE_GLOBAL))); - bool hide_gizmo_during_rotation = arc_visible && !keep_gizmo_visible; - bool hide_gizmo_during_trackball = (_edit.mode == TRANSFORM_ROTATE && _edit.is_trackball); - - int arc_replaces_ring = -1; - if (keep_gizmo_visible) { - switch (_edit.plane) { - case TRANSFORM_X_AXIS: - arc_replaces_ring = 0; - break; - case TRANSFORM_Y_AXIS: - arc_replaces_ring = 1; - break; - case TRANSFORM_Z_AXIS: - arc_replaces_ring = 2; - break; - case TRANSFORM_VIEW: - arc_replaces_ring = 3; - break; - default: - break; - } - } - - bool show_gizmo = spatial_editor->is_gizmo_visible() && !_edit.instant && transform_gizmo_visible && !collision_reposition && !hide_gizmo_during_rotation && !hide_gizmo_during_trackball; - bool show_rotate_gizmo = show_gizmo && (spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_TRANSFORM || spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_ROTATE); - - 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) { - axis_angle = axis_angle.looking_at(xform.basis.get_column(i).normalized(), xform.basis.get_column((i + 1) % 3).normalized()); - } - axis_angle.basis.scale(scale); - axis_angle.origin = xform.origin; - RenderingServer::get_singleton()->instance_set_transform(move_gizmo_instance[i], axis_angle); - RenderingServer::get_singleton()->instance_set_visible(move_gizmo_instance[i], show_gizmo && (spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_TRANSFORM || spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_MOVE)); - 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_TRANSFORM || 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_rotate_gizmo && i != arc_replaces_ring); - 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); - RenderingServer::get_singleton()->instance_set_visible(scale_plane_gizmo_instance[i], show_gizmo && (spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_SCALE)); - RenderingServer::get_singleton()->instance_set_transform(axis_gizmo_instance[i], xform); - } - - Transform3D view_rotation_xform = xform; - view_rotation_xform.orthonormalize(); - - bool can_show_trackball = spatial_editor->is_gizmo_visible() && !_edit.instant && transform_gizmo_visible && !collision_reposition && !hide_gizmo_during_rotation; - bool show_trackball_sphere = can_show_trackball && (spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_TRANSFORM || spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_ROTATE) && !hide_gizmo_during_trackball; - Transform3D trackball_xform = view_rotation_xform; - trackball_xform.basis.scale(scale); - RenderingServer::get_singleton()->instance_set_transform(trackball_sphere_instance, trackball_xform); - RenderingServer::get_singleton()->instance_set_visible(trackball_sphere_instance, show_trackball_sphere); - - bool shrink_view_ring = arc_replaces_ring >= 0 && arc_replaces_ring < 3; - Vector3 view_ring_scale = shrink_view_ring ? scale : scale * (spatial_editor->gizmo_view_rotation_scale / GIZMO_CIRCLE_SIZE); - view_rotation_xform.basis.scale(view_ring_scale); - RenderingServer::get_singleton()->instance_set_transform(rotate_gizmo_instance[3], view_rotation_xform); - RenderingServer::get_singleton()->instance_set_visible(rotate_gizmo_instance[3], show_rotate_gizmo && arc_replaces_ring != 3); - - bool show_axes = spatial_editor->is_gizmo_visible() && _edit.mode != TRANSFORM_NONE && !hide_gizmo_during_trackball; - RenderingServer *rs = RenderingServer::get_singleton(); - rs->instance_set_visible(axis_gizmo_instance[0], show_axes && (_edit.plane == TRANSFORM_X_AXIS || _edit.plane == TRANSFORM_XY || _edit.plane == TRANSFORM_XZ)); - rs->instance_set_visible(axis_gizmo_instance[1], show_axes && (_edit.plane == TRANSFORM_Y_AXIS || _edit.plane == TRANSFORM_XY || _edit.plane == TRANSFORM_YZ)); - rs->instance_set_visible(axis_gizmo_instance[2], show_axes && (_edit.plane == TRANSFORM_Z_AXIS || _edit.plane == TRANSFORM_XZ || _edit.plane == TRANSFORM_YZ)); -} - -void Node3DEditorViewport::update_transform_gizmo_highlight() { - if (!is_visible_in_tree() || !Rect2(Vector2(), surface->get_size()).has_point(surface->get_local_mouse_position())) { - return; - } - _transform_gizmo_select(surface->get_local_mouse_position(), true); -} - -void Node3DEditorViewport::set_state(const Dictionary &p_state) { - if (p_state.has("position")) { - view_3d_controller->cursor.pos = p_state["position"]; - } - if (p_state.has("x_rotation")) { - view_3d_controller->cursor.x_rot = p_state["x_rotation"]; - view_3d_controller->cursor.unsnapped_x_rot = view_3d_controller->cursor.x_rot; - } - if (p_state.has("y_rotation")) { - view_3d_controller->cursor.y_rot = p_state["y_rotation"]; - view_3d_controller->cursor.unsnapped_y_rot = view_3d_controller->cursor.y_rot; - } - if (p_state.has("distance")) { - view_3d_controller->cursor.distance = p_state["distance"]; - } - if (p_state.has("orthogonal")) { - bool orth = p_state["orthogonal"]; - _menu_option(orth ? VIEW_ORTHOGONAL : VIEW_PERSPECTIVE); - } - if (p_state.has("view_type")) { - view_3d_controller->set_view_type(View3DController::ViewType(p_state["view_type"].operator int())); - } - if (p_state.has("auto_orthogonal_enabled")) { - bool enabled = p_state["auto_orthogonal_enabled"]; - view_display_menu->get_popup()->set_item_checked(view_display_menu->get_popup()->get_item_index(VIEW_AUTO_ORTHOGONAL), enabled); - view_3d_controller->set_auto_orthogonal_allowed(enabled); - } - if (p_state.has("auto_orthogonal")) { - if (p_state["auto_orthogonal"]) { - view_3d_controller->force_auto_orthogonal(); - _update_name(); - } - } - if (p_state.has("display_mode")) { - int display = p_state["display_mode"]; - - int idx = view_display_menu->get_popup()->get_item_index(display); - if (idx != -1 && !view_display_menu->get_popup()->is_item_checked(idx)) { - _menu_option(display); - } else { - idx = display_submenu->get_item_index(display); - if (idx != -1 && !display_submenu->is_item_checked(idx)) { - _menu_option(display); - } - } - } - if (p_state.has("lock_rotation")) { - _set_lock_view_rotation(p_state["lock_rotation"]); - } - if (p_state.has("use_environment")) { - bool env = p_state["use_environment"]; - - if (env != camera->get_environment().is_valid()) { - _menu_option(VIEW_ENVIRONMENT); - } - } - if (p_state.has("listener")) { - bool listener = p_state["listener"]; - - int idx = view_display_menu->get_popup()->get_item_index(VIEW_AUDIO_LISTENER); - viewport->set_as_audio_listener_3d(listener); - view_display_menu->get_popup()->set_item_checked(idx, listener); - } - if (p_state.has("doppler")) { - bool doppler = p_state["doppler"]; - - int idx = view_display_menu->get_popup()->get_item_index(VIEW_AUDIO_DOPPLER); - camera->set_doppler_tracking(doppler ? Camera3D::DOPPLER_TRACKING_IDLE_STEP : Camera3D::DOPPLER_TRACKING_DISABLED); - view_display_menu->get_popup()->set_item_checked(idx, doppler); - } - if (p_state.has("gizmos")) { - bool gizmos = p_state["gizmos"]; - - int idx = view_display_menu->get_popup()->get_item_index(VIEW_GIZMOS); - if (view_display_menu->get_popup()->is_item_checked(idx) != gizmos) { - _menu_option(VIEW_GIZMOS); - } - } - if (p_state.has("transform_gizmo")) { - bool transform_gizmo = p_state["transform_gizmo"]; - - int idx = view_display_menu->get_popup()->get_item_index(VIEW_TRANSFORM_GIZMO); - if (view_display_menu->get_popup()->is_item_checked(idx) != transform_gizmo) { - _menu_option(VIEW_TRANSFORM_GIZMO); - } - } - if (p_state.has("grid")) { - bool grid = p_state["grid"]; - - int idx = view_display_menu->get_popup()->get_item_index(VIEW_GRID); - if (view_display_menu->get_popup()->is_item_checked(idx) != grid) { - _menu_option(VIEW_GRID); - } - } - if (p_state.has("information")) { - bool information = p_state["information"]; - - int idx = view_display_menu->get_popup()->get_item_index(VIEW_INFORMATION); - if (view_display_menu->get_popup()->is_item_checked(idx) != information) { - _menu_option(VIEW_INFORMATION); - } - } - if (p_state.has("frame_time")) { - bool fps = p_state["frame_time"]; - - int idx = view_display_menu->get_popup()->get_item_index(VIEW_FRAME_TIME); - if (view_display_menu->get_popup()->is_item_checked(idx) != fps) { - _menu_option(VIEW_FRAME_TIME); - } - } - if (p_state.has("half_res")) { - bool half_res = p_state["half_res"]; - - int idx = view_display_menu->get_popup()->get_item_index(VIEW_HALF_RESOLUTION); - view_display_menu->get_popup()->set_item_checked(idx, half_res); - _update_shrink(); - } - if (p_state.has("cinematic_preview")) { - previewing_cinema = p_state["cinematic_preview"]; - - int idx = view_display_menu->get_popup()->get_item_index(VIEW_CINEMATIC_PREVIEW); - view_display_menu->get_popup()->set_item_checked(idx, previewing_cinema); - - cinema_label->set_visible(previewing_cinema); - if (previewing_cinema) { - _update_centered_labels(); - surface->queue_redraw(); - } - } - - if (preview_camera->is_connected(SceneStringName(toggled), callable_mp(this, &Node3DEditorViewport::_toggle_camera_preview))) { - preview_camera->disconnect(SceneStringName(toggled), callable_mp(this, &Node3DEditorViewport::_toggle_camera_preview)); - } - if (p_state.has("previewing")) { - Node *pv = EditorNode::get_singleton()->get_edited_scene()->get_node(p_state["previewing"]); - if (Object::cast_to(pv)) { - previewing = Object::cast_to(pv); - previewing->connect(SceneStringName(tree_exiting), callable_mp(this, &Node3DEditorViewport::_preview_exited_scene)); - previewing->connect(CoreStringName(property_list_changed), callable_mp(this, &Node3DEditorViewport::_preview_camera_property_changed)); - RS::get_singleton()->viewport_attach_camera(viewport->get_viewport_rid(), previewing->get_camera()); //replace - surface->queue_redraw(); - previewing_camera = true; - _update_navigation_controls_visibility(); - preview_camera->set_pressed(true); - preview_camera->show(); - pilot_camera->show(); - - camera->set_global_transform(view_3d_controller->to_camera_transform()); - - _sync_cursor_from_transform(previewing->get_global_transform()); - view_3d_controller->update_camera(0); - } - } - preview_camera->connect(SceneStringName(toggled), callable_mp(this, &Node3DEditorViewport::_toggle_camera_preview)); -} - -Dictionary Node3DEditorViewport::get_state() const { - Dictionary d; - d["position"] = view_3d_controller->cursor.pos; - d["x_rotation"] = view_3d_controller->cursor.x_rot; - d["y_rotation"] = view_3d_controller->cursor.y_rot; - d["distance"] = view_3d_controller->cursor.distance; - d["use_environment"] = camera->get_environment().is_valid(); - d["orthogonal"] = camera->get_projection() == Camera3D::PROJECTION_ORTHOGONAL; - d["view_type"] = view_3d_controller->get_view_type(); - d["auto_orthogonal"] = view_3d_controller->get_orthogonal_mode() == View3DController::ORTHOGONAL_AUTO; - d["auto_orthogonal_enabled"] = view_display_menu->get_popup()->is_item_checked(view_display_menu->get_popup()->get_item_index(VIEW_AUTO_ORTHOGONAL)); - - // Find selected display mode. - int display_mode = VIEW_DISPLAY_NORMAL; - for (int i = VIEW_DISPLAY_NORMAL; i < VIEW_DISPLAY_ADVANCED; i++) { - if (view_display_menu->get_popup()->is_item_checked(view_display_menu->get_popup()->get_item_index(i))) { - display_mode = i; - break; - } - } - for (int i = VIEW_DISPLAY_ADVANCED + 1; i < VIEW_DISPLAY_MAX; i++) { - if (display_submenu->is_item_checked(display_submenu->get_item_index(i))) { - display_mode = i; - break; - } - } - d["display_mode"] = display_mode; - - d["listener"] = viewport->is_audio_listener_3d(); - d["doppler"] = view_display_menu->get_popup()->is_item_checked(view_display_menu->get_popup()->get_item_index(VIEW_AUDIO_DOPPLER)); - d["gizmos"] = view_display_menu->get_popup()->is_item_checked(view_display_menu->get_popup()->get_item_index(VIEW_GIZMOS)); - d["transform_gizmo"] = view_display_menu->get_popup()->is_item_checked(view_display_menu->get_popup()->get_item_index(VIEW_TRANSFORM_GIZMO)); - d["grid"] = view_display_menu->get_popup()->is_item_checked(view_display_menu->get_popup()->get_item_index(VIEW_GRID)); - d["information"] = view_display_menu->get_popup()->is_item_checked(view_display_menu->get_popup()->get_item_index(VIEW_INFORMATION)); - d["frame_time"] = view_display_menu->get_popup()->is_item_checked(view_display_menu->get_popup()->get_item_index(VIEW_FRAME_TIME)); - d["half_res"] = view_display_menu->get_popup()->is_item_checked(view_display_menu->get_popup()->get_item_index(VIEW_HALF_RESOLUTION)); - d["cinematic_preview"] = view_display_menu->get_popup()->is_item_checked(view_display_menu->get_popup()->get_item_index(VIEW_CINEMATIC_PREVIEW)); - if (previewing) { - d["previewing"] = EditorNode::get_singleton()->get_edited_scene()->get_path_to(previewing); - } - d["lock_rotation"] = view_3d_controller->is_locking_rotation(); - - return d; -} - -void Node3DEditorViewport::_bind_methods() { - ADD_SIGNAL(MethodInfo("toggle_maximize_view", PropertyInfo(Variant::OBJECT, "viewport"))); - ADD_SIGNAL(MethodInfo("clicked")); -} - -void Node3DEditorViewport::reset() { - view_3d_controller->set_orthogonal(false); - view_3d_controller->set_view_type(View3DController::VIEW_TYPE_USER); - message_time = 0; - message = ""; - last_message = ""; - - view_3d_controller->cursor = View3DController::Cursor(); -} - -void Node3DEditorViewport::focus_selection() { - Vector3 center; - int count = 0; - - const List &selection = editor_selection->get_top_selected_node_list(); - focused_node_id = ObjectID(); - if (!selection.is_empty()) { - focused_node_id = selection.front()->get()->get_instance_id(); - } - - for (Node *node : selection) { - Node3D *node_3d = Object::cast_to(node); - if (!node_3d) { - continue; - } - - Node3DEditorSelectedItem *se = editor_selection->get_node_editor_data(node_3d); - if (!se) { - continue; - } - - if (se->gizmo.is_valid()) { - for (const KeyValue &GE : se->subgizmos) { - const Vector3 pos = node_3d->get_global_gizmo_transform().xform(se->gizmo->get_subgizmo_transform(GE.key).origin); - if (pos.is_finite()) { - center += pos; - count++; - } - } - } else { - const Vector3 pos = node_3d->get_global_gizmo_transform().origin; - if (pos.is_finite()) { - center += pos; - count++; - } - } - } - - if (count > 1) { - center /= count; - } - - view_3d_controller->cursor.pos = center; -} - -void Node3DEditorViewport::assign_pending_data_pointers(Node3D *p_preview_node, AABB *p_preview_bounds, AcceptDialog *p_accept) { - preview_node = p_preview_node; - preview_bounds = p_preview_bounds; - accept = p_accept; -} - -void _insert_rid_recursive(Node *node, HashSet &rids) { - CollisionObject3D *co = Object::cast_to(node); - - if (co) { - rids.insert(co->get_rid()); - } else if (node->is_class("CSGShape3D")) { // HACK: We should avoid referencing module logic. - rids.insert(node->call("_get_root_collision_instance")); - } - - for (int i = 0; i < node->get_child_count(); i++) { - Node *child = node->get_child(i); - _insert_rid_recursive(child, rids); - } -} - -Vector3 Node3DEditorViewport::_get_instance_position(const Point2 &p_pos, Node3D *p_node) const { - const float MAX_DISTANCE = 50.0; - const float FALLBACK_DISTANCE = 5.0; - - Vector3 world_ray = get_ray(p_pos); - Vector3 world_pos = get_ray_pos(p_pos); - - PhysicsDirectSpaceState3D *ss = get_tree()->get_root()->get_world_3d()->get_direct_space_state(); - - HashSet rids; - - if (preview_node && preview_node->get_child_count() > 0) { - _insert_rid_recursive(preview_node, rids); - } else if (!preview_node->is_inside_tree() && !ruler->is_inside_tree()) { - const List &selection = editor_selection->get_top_selected_node_list(); - - Node3D *first_selected_node = Object::cast_to(selection.front()->get()); - - if (first_selected_node) { - _insert_rid_recursive(first_selected_node, rids); - } - } - - PhysicsDirectSpaceState3D::RayParameters ray_params; - ray_params.exclude = rids; - ray_params.from = world_pos; - ray_params.to = world_pos + world_ray * camera->get_far(); - - PhysicsDirectSpaceState3D::RayResult result; - if (ss->intersect_ray(ray_params, result) && (preview_node->get_child_count() > 0 || !preview_node->is_inside_tree())) { - // Calculate an offset for the `p_node` such that the its bounding box is on top of and touching the contact surface's plane. - - // Use the Gram-Schmidt process to get an orthonormal Basis aligned with the surface normal. - const Vector3 bb_basis_x = result.normal; - Vector3 bb_basis_y = Vector3(0, 1, 0); - bb_basis_y = bb_basis_y - bb_basis_y.project(bb_basis_x); - if (bb_basis_y.is_zero_approx()) { - bb_basis_y = Vector3(0, 0, 1); - bb_basis_y = bb_basis_y - bb_basis_y.project(bb_basis_x); - } - bb_basis_y = bb_basis_y.normalized(); - const Vector3 bb_basis_z = bb_basis_x.cross(bb_basis_y); - const Basis bb_basis = Basis(bb_basis_x, bb_basis_y, bb_basis_z); - - // This normal-aligned Basis allows us to create an AABB that can fit on the surface plane as snugly as possible. - const Transform3D bb_transform = Transform3D(bb_basis, p_node->get_global_transform().origin); - const AABB p_node_bb = _calculate_spatial_bounds(p_node, true, &bb_transform); - // The x-axis's alignment with the surface normal also makes it trivial to get the distance from `p_node`'s origin at (0, 0, 0) to the correct AABB face. - const float offset_distance = -p_node_bb.position.x; - - // `result_offset` is in global space. - const Vector3 result_offset = result.position + result.normal * offset_distance; - - return result_offset; - } - - const bool is_orthogonal = camera->get_projection() == Camera3D::PROJECTION_ORTHOGONAL; - - // The XZ plane. - Vector3 intersection; - Plane plane(Vector3(0, 1, 0)); - if (plane.intersects_ray(world_pos, world_ray, &intersection)) { - if (is_orthogonal || world_pos.distance_to(intersection) <= MAX_DISTANCE) { - return intersection; - } - } - - // Plane facing the camera using fallback distance. - if (is_orthogonal) { - plane = Plane(world_ray, view_3d_controller->cursor.pos - world_ray * (view_3d_controller->cursor.distance - FALLBACK_DISTANCE)); - } else { - plane = Plane(world_ray, world_pos + world_ray * FALLBACK_DISTANCE); - } - if (plane.intersects_ray(world_pos, world_ray, &intersection)) { - return intersection; - } - - // Not likely, but just in case... - return world_pos + world_ray * FALLBACK_DISTANCE; -} - -AABB Node3DEditorViewport::_calculate_spatial_bounds(const Node3D *p_parent, bool p_omit_top_level, const Transform3D *p_bounds_orientation) { - if (!p_parent) { - return AABB(Vector3(-0.2, -0.2, -0.2), Vector3(0.4, 0.4, 0.4)); - } - const Transform3D parent_transform = p_parent->get_global_transform(); - if (!parent_transform.is_finite()) { - return AABB(); - } - AABB bounds; - - Transform3D bounds_orientation; - Transform3D xform_to_top_level_parent_space; - if (p_bounds_orientation) { - bounds_orientation = *p_bounds_orientation; - xform_to_top_level_parent_space = bounds_orientation.affine_inverse() * parent_transform; - } else { - bounds_orientation = parent_transform; - } - - const VisualInstance3D *visual_instance = Object::cast_to(p_parent); - if (visual_instance) { - bounds = visual_instance->get_aabb(); - } else { - bounds = AABB(); - } - bounds = xform_to_top_level_parent_space.xform(bounds); - - for (int i = 0; i < p_parent->get_child_count(); i++) { - const Node3D *child = Object::cast_to(p_parent->get_child(i)); - if (child && !(p_omit_top_level && child->is_set_as_top_level())) { - const AABB child_bounds = _calculate_spatial_bounds(child, p_omit_top_level, &bounds_orientation); - bounds.merge_with(child_bounds); - } - } - - return bounds; -} - -Node *Node3DEditorViewport::_sanitize_preview_node(Node *p_node) const { - Node3D *node_3d = Object::cast_to(p_node); - if (node_3d == nullptr) { - Node3D *replacement_node = memnew(Node3D); - replacement_node->set_name(p_node->get_name()); - p_node->replace_by(replacement_node); - memdelete(p_node); - p_node = replacement_node; - } else { - VisualInstance3D *visual_instance = Object::cast_to(node_3d); - if (visual_instance == nullptr) { - Node3D *replacement_node = memnew(Node3D); - replacement_node->set_name(node_3d->get_name()); - replacement_node->set_visible(node_3d->is_visible()); - replacement_node->set_transform(node_3d->get_transform()); - replacement_node->set_rotation_edit_mode(node_3d->get_rotation_edit_mode()); - replacement_node->set_rotation_order(node_3d->get_rotation_order()); - replacement_node->set_as_top_level(node_3d->is_set_as_top_level()); - p_node->replace_by(replacement_node); - memdelete(p_node); - p_node = replacement_node; - } - } - - for (int i = 0; i < p_node->get_child_count(); i++) { - _sanitize_preview_node(p_node->get_child(i)); - } - - return p_node; -} - -void Node3DEditorViewport::_create_preview_node(const Vector &files) const { - bool add_preview = false; - for (const String &path : files) { - Ref res = ResourceLoader::load(path); - ERR_CONTINUE(res.is_null()); - - Ref scene = res; - if (scene.is_valid()) { - Node *instance = scene->instantiate(); - if (instance) { - instance = _sanitize_preview_node(instance); - preview_node->add_child(instance); - Node3D *node_3d = Object::cast_to(instance); - if (node_3d) { - node_3d->set_as_top_level(false); - } - } - add_preview = true; - } - - Ref mesh = res; - if (mesh.is_valid()) { - MeshInstance3D *mesh_instance = memnew(MeshInstance3D); - mesh_instance->set_mesh(mesh); - preview_node->add_child(mesh_instance); - add_preview = true; - } - - Ref audio = res; - if (audio.is_valid()) { - Sprite3D *sprite = memnew(Sprite3D); - sprite->set_texture(get_editor_theme_icon(SNAME("Gizmo3DSamplePlayer"))); - sprite->set_billboard_mode(StandardMaterial3D::BILLBOARD_ENABLED); - sprite->set_pixel_size(0.005); - preview_node->add_child(sprite); - add_preview = true; - } - } - if (add_preview) { - EditorNode::get_singleton()->get_scene_root()->add_child(preview_node); - *preview_bounds = _calculate_spatial_bounds(preview_node); - } -} - -void Node3DEditorViewport::_remove_preview_node() { - tooltip_panel->hide(); - - set_message(""); - if (preview_node->get_parent()) { - for (int i = preview_node->get_child_count() - 1; i >= 0; i--) { - Node *node = preview_node->get_child(i); - node->queue_free(); - preview_node->remove_child(node); - } - EditorNode::get_singleton()->get_scene_root()->remove_child(preview_node); - } -} - -bool Node3DEditorViewport::_apply_preview_material(ObjectID p_target, const Point2 &p_point) const { - _reset_preview_material(); - - if (p_target.is_null()) { - return false; - } - - spatial_editor->set_preview_material_target(p_target); - - Object *target_inst = ObjectDB::get_instance(p_target); - - bool is_ctrl = Input::get_singleton()->is_key_pressed(Key::CTRL); - - MeshInstance3D *mesh_instance = Object::cast_to(target_inst); - if (is_ctrl && mesh_instance) { - Ref mesh = mesh_instance->get_mesh(); - int surface_count = mesh->get_surface_count(); - - Vector3 world_ray = get_ray(p_point); - Vector3 world_pos = get_ray_pos(p_point); - - int closest_surface = -1; - float closest_dist = 1e20; - - Transform3D gt = mesh_instance->get_global_transform(); - - Transform3D ai = gt.affine_inverse(); - Vector3 xform_ray = ai.basis.xform(world_ray).normalized(); - Vector3 xform_pos = ai.xform(world_pos); - - for (int surface_idx = 0; surface_idx < surface_count; surface_idx++) { - Ref surface_mesh = mesh->generate_surface_triangle_mesh(surface_idx); - - Vector3 rpos, rnorm; - if (surface_mesh->intersect_ray(xform_pos, xform_ray, rpos, rnorm)) { - Vector3 hitpos = gt.xform(rpos); - - const real_t dist = world_pos.distance_to(hitpos); - - if (dist < 0) { - continue; - } - - if (dist < closest_dist) { - closest_surface = surface_idx; - closest_dist = dist; - } - } - } - - if (closest_surface == -1) { - return false; - } - - spatial_editor->set_preview_material_surface(closest_surface); - spatial_editor->set_preview_reset_material(mesh_instance->get_surface_override_material(closest_surface)); - mesh_instance->set_surface_override_material(closest_surface, spatial_editor->get_preview_material()); - - return true; - } - - GeometryInstance3D *geometry_instance = Object::cast_to(target_inst); - if (geometry_instance) { - spatial_editor->set_preview_material_surface(-1); - spatial_editor->set_preview_reset_material(geometry_instance->get_material_override()); - geometry_instance->set_material_override(spatial_editor->get_preview_material()); - return true; - } - - return false; -} - -void Node3DEditorViewport::_reset_preview_material() const { - ObjectID last_target = spatial_editor->get_preview_material_target(); - if (last_target.is_null()) { - return; - } - Object *last_target_inst = ObjectDB::get_instance(last_target); - - MeshInstance3D *mesh_instance = Object::cast_to(last_target_inst); - GeometryInstance3D *geometry_instance = Object::cast_to(last_target_inst); - if (mesh_instance && spatial_editor->get_preview_material_surface() != -1) { - mesh_instance->set_surface_override_material(spatial_editor->get_preview_material_surface(), spatial_editor->get_preview_reset_material()); - } else if (geometry_instance) { - geometry_instance->set_material_override(spatial_editor->get_preview_reset_material()); - } -} - -void Node3DEditorViewport::_remove_preview_material() { - tooltip_panel->hide(); - - spatial_editor->set_preview_material(Ref()); - spatial_editor->set_preview_reset_material(Ref()); - spatial_editor->set_preview_material_target(ObjectID()); - spatial_editor->set_preview_material_surface(-1); -} - -bool Node3DEditorViewport::_cyclical_dependency_exists(const String &p_target_scene_path, Node *p_desired_node) const { - if (p_desired_node->get_scene_file_path() == p_target_scene_path) { - return true; - } - - int childCount = p_desired_node->get_child_count(); - for (int i = 0; i < childCount; i++) { - Node *child = p_desired_node->get_child(i); - if (_cyclical_dependency_exists(p_target_scene_path, child)) { - return true; - } - } - return false; -} - -bool Node3DEditorViewport::_create_instance(Node *p_parent, const String &p_path, const Point2 &p_point) { - Ref res = ResourceLoader::load(p_path); - ERR_FAIL_COND_V(res.is_null(), false); - - Ref scene = res; - Ref mesh = res; - - Node *instantiated_scene = nullptr; - - if (mesh.is_valid() || scene.is_valid()) { - if (mesh.is_valid()) { - MeshInstance3D *mesh_instance = memnew(MeshInstance3D); - mesh_instance->set_mesh(mesh); - - // Adjust casing according to project setting. The file name is expected to be in snake_case, but will work for others. - const String &node_name = Node::adjust_name_casing(p_path.get_file().get_basename()); - if (!node_name.is_empty()) { - mesh_instance->set_name(node_name); - } - - instantiated_scene = mesh_instance; - } else { - if (scene.is_null()) { // invalid scene - return false; - } else { - instantiated_scene = scene->instantiate(PackedScene::GEN_EDIT_STATE_INSTANCE); - } - } - } - - if (instantiated_scene == nullptr) { - return false; - } - - if (!EditorNode::get_singleton()->get_edited_scene()->get_scene_file_path().is_empty()) { // Cyclic instantiation. - if (_cyclical_dependency_exists(EditorNode::get_singleton()->get_edited_scene()->get_scene_file_path(), instantiated_scene)) { - memdelete(instantiated_scene); - return false; - } - } - - if (scene.is_valid()) { - instantiated_scene->set_scene_file_path(ProjectSettings::get_singleton()->localize_path(p_path)); - } - - EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); - undo_redo->add_do_method(p_parent, "add_child", instantiated_scene, true); - undo_redo->add_do_method(instantiated_scene, "set_owner", EditorNode::get_singleton()->get_edited_scene()); - undo_redo->add_do_reference(instantiated_scene); - undo_redo->add_undo_method(p_parent, "remove_child", instantiated_scene); - undo_redo->add_do_method(editor_selection, "add_node", instantiated_scene); - - String new_name = p_parent->validate_child_name(instantiated_scene); - EditorDebuggerNode *ed = EditorDebuggerNode::get_singleton(); - undo_redo->add_do_method(ed, "live_debug_instantiate_node", EditorNode::get_singleton()->get_edited_scene()->get_path_to(p_parent), p_path, new_name); - undo_redo->add_undo_method(ed, "live_debug_remove_node", NodePath(String(EditorNode::get_singleton()->get_edited_scene()->get_path_to(p_parent)) + "/" + new_name)); - - Node3D *node3d = Object::cast_to(instantiated_scene); - if (node3d) { - Transform3D parent_tf; - Node3D *parent_node3d = Object::cast_to(p_parent); - if (parent_node3d) { - parent_tf = parent_node3d->get_global_gizmo_transform(); - } - - Transform3D new_tf = node3d->get_transform(); - if (node3d->is_set_as_top_level()) { - new_tf.origin += preview_node_pos; - } else { - new_tf.origin = parent_tf.affine_inverse().xform(preview_node_pos + node3d->get_position()); - new_tf.basis = parent_tf.affine_inverse().basis * new_tf.basis; - } - - undo_redo->add_do_method(instantiated_scene, "set_transform", new_tf); - } - - return true; -} - -bool Node3DEditorViewport::_create_audio_node(Node *p_parent, const String &p_path, const Point2 &p_point) { - Ref audio = ResourceLoader::load(p_path); - ERR_FAIL_COND_V(audio.is_null(), false); - - AudioStreamPlayer3D *audio_player = memnew(AudioStreamPlayer3D); - audio_player->set_stream(audio); - - // Adjust casing according to project setting. The file name is expected to be in snake_case, but will work for others. - const String &node_name = Node::adjust_name_casing(p_path.get_file().get_basename()); - if (!node_name.is_empty()) { - audio_player->set_name(node_name); - } - - EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); - undo_redo->add_do_method(p_parent, "add_child", audio_player, true); - undo_redo->add_do_method(audio_player, "set_owner", EditorNode::get_singleton()->get_edited_scene()); - undo_redo->add_do_reference(audio_player); - undo_redo->add_undo_method(p_parent, "remove_child", audio_player); - undo_redo->add_do_method(editor_selection, "add_node", audio_player); - - const String new_name = p_parent->validate_child_name(audio_player); - EditorDebuggerNode *ed = EditorDebuggerNode::get_singleton(); - undo_redo->add_do_method(ed, "live_debug_create_node", EditorNode::get_singleton()->get_edited_scene()->get_path_to(p_parent), audio_player->get_class(), new_name); - undo_redo->add_undo_method(ed, "live_debug_remove_node", NodePath(String(EditorNode::get_singleton()->get_edited_scene()->get_path_to(p_parent)) + "/" + new_name)); - - Transform3D parent_tf; - Node3D *parent_node3d = Object::cast_to(p_parent); - if (parent_node3d) { - parent_tf = parent_node3d->get_global_gizmo_transform(); - } - - Transform3D new_tf = audio_player->get_transform(); - new_tf.origin = parent_tf.affine_inverse().xform(preview_node_pos + audio_player->get_position()); - new_tf.basis = parent_tf.affine_inverse().basis * new_tf.basis; - - undo_redo->add_do_method(audio_player, "set_transform", new_tf); - - return true; -} - -void Node3DEditorViewport::_perform_drop_data() { - EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); - if (spatial_editor->get_preview_material_target().is_valid()) { - GeometryInstance3D *geometry_instance = ObjectDB::get_instance(spatial_editor->get_preview_material_target()); - MeshInstance3D *mesh_instance = ObjectDB::get_instance(spatial_editor->get_preview_material_target()); - if (mesh_instance && spatial_editor->get_preview_material_surface() != -1) { - undo_redo->create_action(vformat(TTR("Set Surface %d Override Material"), spatial_editor->get_preview_material_surface())); - undo_redo->add_do_method(geometry_instance, "set_surface_override_material", spatial_editor->get_preview_material_surface(), spatial_editor->get_preview_material()); - undo_redo->add_undo_method(geometry_instance, "set_surface_override_material", spatial_editor->get_preview_material_surface(), spatial_editor->get_preview_reset_material()); - undo_redo->commit_action(); - } else if (geometry_instance) { - undo_redo->create_action(TTR("Set Material Override")); - undo_redo->add_do_method(geometry_instance, "set_material_override", spatial_editor->get_preview_material()); - undo_redo->add_undo_method(geometry_instance, "set_material_override", spatial_editor->get_preview_reset_material()); - undo_redo->commit_action(); - } - - _remove_preview_material(); - return; - } - - _remove_preview_node(); - - PackedStringArray error_files; - - undo_redo->create_action(TTR("Create Node"), UndoRedo::MERGE_DISABLE, target_node); - undo_redo->add_do_method(editor_selection, "clear"); - - for (int i = 0; i < selected_files.size(); i++) { - String path = selected_files[i]; - Ref res = ResourceLoader::load(path); - if (res.is_null()) { - continue; - } - - Ref scene = res; - Ref mesh = res; - if (mesh.is_valid() || scene.is_valid()) { - if (!_create_instance(target_node, path, drop_pos)) { - error_files.push_back(path.get_file()); - } - } - - Ref audio = res; - if (audio.is_valid()) { - if (!_create_audio_node(target_node, path, drop_pos)) { - error_files.push_back(path.get_file()); - } - } - } - - undo_redo->commit_action(); - - if (error_files.size() > 0) { - accept->set_text(vformat(TTR("Error instantiating scene from %s."), String(", ").join(error_files))); - accept->popup_centered(); - } -} - -void Node3DEditorViewport::_show_tooltip(const String &p_title, const String &p_description) const { - tooltip_panel->set_text( - vformat("[font_size=%s][b][color=%s]%s[/color][/b][/font_size]\n%s", - get_theme_default_font_size() + 2, - get_theme_color(SNAME("accent_color"), EditorStringName(Editor)).to_html(false), - p_title, p_description)); - tooltip_panel->show(); -} - -bool Node3DEditorViewport::can_drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) { - if (previewing) { - return false; - } - - if (p_point == Vector2(Math::INF, Math::INF)) { - tooltip_panel->hide(); - return false; - } - preview_node_viewport_pos = p_point; - - Dictionary d = p_data; - if (!d.has("type") || String(d["type"]) != "files") { - tooltip_panel->hide(); - return false; - } - Vector files = d["files"]; - - // If we already have a preview material or preview node, - // We just need to update them. - if (spatial_editor->get_preview_material().is_valid()) { - ObjectID new_preview_material_target = _select_ray(p_point); - return _apply_preview_material(new_preview_material_target, p_point); - } - - if (preview_node->is_inside_tree()) { - preview_node_viewport_pos = p_point; - update_preview_node = true; - return true; - } - - // If we don't already have a preview material or preview node, - // it means that this is the first time we are visiting this function. - // In that case, we need to check that the file(s) are droppable. - bool is_cyclical_dep = false; - String error_file; - - enum { - SCENE = 1 << 0, - TEXTURE = 1 << 1, - AUDIO = 1 << 2, - MESH = 1 << 3, - MATERIAL = 1 << 4, - }; - int instantiate_type = 0; - - // Track whether a type other than PackedScene is valid to stop checking them and only - // continue to check if the rest of the scenes are valid (don't have cyclic dependencies). - bool is_other_valid = false; - // Check if at least one of the dragged files is a mesh, material, texture, or scene. - for (int i = 0; i < files.size(); i++) { - const String &res_type = ResourceLoader::get_resource_type(files[i]); - bool is_scene = ClassDB::is_parent_class(res_type, "PackedScene"); - bool is_mesh = ClassDB::is_parent_class(res_type, "Mesh"); - bool is_material = ClassDB::is_parent_class(res_type, "Material"); - bool is_texture = ClassDB::is_parent_class(res_type, "Texture"); - bool is_audio = ClassDB::is_parent_class(res_type, "AudioStream"); - - if (is_mesh || is_scene || is_material || is_texture || is_audio) { - Ref res = ResourceLoader::load(files[i]); - if (res.is_null()) { - continue; - } - Ref scn = res; - Ref mesh = res; - Ref mat = res; - Ref tex = res; - Ref audio = res; - if (scn.is_valid()) { - Node *instantiated_scene = scn->instantiate(PackedScene::GEN_EDIT_STATE_INSTANCE); - if (!instantiated_scene) { - continue; - } - Node *edited_scene = EditorNode::get_singleton()->get_edited_scene(); - if (edited_scene && !edited_scene->get_scene_file_path().is_empty() && _cyclical_dependency_exists(edited_scene->get_scene_file_path(), instantiated_scene)) { - memdelete(instantiated_scene); - is_cyclical_dep = true; - error_file = files[i].get_file(); - break; - } - memdelete(instantiated_scene); - instantiate_type |= SCENE; - } else if (!is_other_valid && mat.is_valid()) { - Ref base_mat = res; - Ref shader_mat = res; - - if (base_mat.is_null() && shader_mat.is_null()) { - continue; - } - - spatial_editor->set_preview_material(mat); - is_other_valid = true; - instantiate_type |= MATERIAL; - continue; - } else if (!is_other_valid && mesh.is_valid()) { - // Let the mesh pass. - is_other_valid = true; - instantiate_type |= MESH; - } else if (!is_other_valid && tex.is_valid()) { - Ref new_mat; - new_mat.instantiate(); - new_mat->set_texture(BaseMaterial3D::TEXTURE_ALBEDO, tex); - - spatial_editor->set_preview_material(new_mat); - is_other_valid = true; - instantiate_type |= TEXTURE; - continue; - } else if (!is_other_valid && audio.is_valid()) { - is_other_valid = true; - instantiate_type |= AUDIO; - } else { - continue; - } - } - } - - String title = TTRN("Can't drop the file...", "Can't drop the files...", files.size()); - if (is_cyclical_dep) { - _show_tooltip(title, vformat(TTR("Circular dependency found at %s."), error_file)); - return false; - } - - if (instantiate_type == 0) { - _show_tooltip(title, TTR("File format is not supported.")); - return false; - } - - // Only droppable file(s), on first frame, will make it to this point. - // Hence it is a good place to create the previews and tooltips. - _create_preview_node(files); - preview_node->hide(); - - String desc = "[ul]" + - TTRN("[b]Default:[/b] Add as sibling of selected node (except when root is selected).", - "[b]Default:[/b] Add as siblings of selected node (except when root is selected).", - files.size()) + - "\n" + - TTRN("[b]Hold Shift:[/b] Add as child of selected node.", - "[b]Hold Shift:[/b] Add as children of selected node.", - files.size()) + - "\n" + - TTRN("[b]Hold Alt:[/b] Add as child of root node.", - "[b]Hold Alt:[/b] Add as children of root node.", - files.size()); - - if (files.size() > 1) { - title = TTR("Dropping multiple files..."); - } else if (instantiate_type & SCENE) { - title = TTR("Dropping a Scene file..."); - } else if (instantiate_type & MESH) { - title = TTR("Dropping a Mesh file..."); - } else if (instantiate_type & AUDIO) { - title = TTR("Dropping an Audio file..."); - } else if (instantiate_type & MATERIAL || instantiate_type & TEXTURE) { - title = TTR("Dropping a Material..."); - desc = "[ul]"; - desc += vformat(TTR("[b]Default:[/b] Place in Geometry's Material Override slot.") + - "\n" + TTR("[b]Hold %s:[/b] Place in Mesh's Surface Material Override slot."), - keycode_get_string((Key)KeyModifierMask::CMD_OR_CTRL)); - } - desc += "[/ul]"; - - _show_tooltip(title, desc); - - return true; -} - -void Node3DEditorViewport::drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) { - if (!can_drop_data_fw(p_point, p_data, p_from)) { - return; - } - - bool is_shift = Input::get_singleton()->is_key_pressed(Key::SHIFT); - bool is_alt = Input::get_singleton()->is_key_pressed(Key::ALT); - - selected_files.clear(); - Dictionary d = p_data; - if (d.has("type") && String(d["type"]) == "files") { - selected_files = d["files"]; - } - - const List &selected_nodes = EditorNode::get_singleton()->get_editor_selection()->get_top_selected_node_list(); - Node *root_node = EditorNode::get_singleton()->get_edited_scene(); - if (selected_nodes.size() > 0) { - Node *selected_node = selected_nodes.front()->get(); - if (is_alt) { - target_node = root_node; - } else if (is_shift) { - target_node = selected_node; - } else { // Default behavior. - target_node = (selected_node != root_node) ? selected_node->get_parent() : root_node; - } - } else { - if (root_node) { - target_node = root_node; - } else { - // Create a root node so we can add child nodes to it. - SceneTreeDock::get_singleton()->add_root_node(memnew(Node3D)); - target_node = get_tree()->get_edited_scene_root(); - } - } - - drop_pos = p_point; - - _perform_drop_data(); -} - -void Node3DEditorViewport::begin_transform(TransformMode p_mode, bool instant) { - if (previewing) { - return; - } - - if (get_selected_count() > 0) { - if (!_has_unlocked_selection()) { - return; - } - - _edit.children_original_globals.clear(); - - _edit.mode = p_mode; - _compute_edit(_edit.mouse_pos); - _edit.instant = instant; - _edit.initial_click_vector = Vector3(); - _edit.previous_rotation_vector = Vector3(); - _edit.accumulated_rotation_angle = 0.0; - _edit.rotation_angle = 0.0; - _edit.gizmo_initiated = false; - switch (p_mode) { - case TRANSFORM_ROTATE: - _edit.show_rotation_line = true; - set_message(vformat(TTR("Rotating %s degrees."), String::num(0, 0))); - break; - case TRANSFORM_TRANSLATE: - set_message(vformat(TTR("Translating: %s"), vformat("%.0v", Vector3()))); - break; - case TRANSFORM_SCALE: - set_message(vformat(TTR("Scaling: %s"), vformat("%.0v", Vector3()))); - break; - default: - break; - } - update_transform_gizmo_view(); - set_process_input(instant); - surface->queue_redraw(); - } -} - -// Apply the current transform operation. -void Node3DEditorViewport::commit_transform() { - ERR_FAIL_COND(_edit.mode == TRANSFORM_NONE); - static const char *_transform_name[4] = { - TTRC("None"), - TTRC("Rotate"), - // TRANSLATORS: This refers to the movement that changes the position of an object. - TTRC("Translate"), - TTRC("Scale"), - }; - EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); - undo_redo->create_action(_transform_name[_edit.mode]); - - const List &selection = editor_selection->get_top_selected_node_list(); - - for (Node *E : selection) { - Node3D *sp = Object::cast_to(E); - if (!sp) { - continue; - } - - Node3DEditorSelectedItem *se = editor_selection->get_node_editor_data(sp); - if (!se) { - continue; - } - - undo_redo->add_do_method(sp, "set_transform", sp->get_local_gizmo_transform()); - undo_redo->add_undo_method(sp, "set_transform", se->original_local); - } - - if (!_edit.children_original_globals.is_empty()) { - for (const KeyValue &pair : _edit.children_original_globals) { - Node3D *child = pair.key; - Transform3D original_global = pair.value; - Transform3D current_global = child->get_global_transform(); - - undo_redo->add_do_method(child, "set_global_transform", current_global); - undo_redo->add_undo_method(child, "set_global_transform", original_global); - } - } - - undo_redo->commit_action(); - - collision_reposition = false; - finish_transform(); - _reset_follow_mode_count(); - set_message(""); -} - -void Node3DEditorViewport::apply_transform(Vector3 p_motion, double p_snap) { - // View-plane translate/scale always uses global coords; rotation and axis operations respect local/global preference. - bool local_coords = spatial_editor->are_local_coords_enabled() && - !(_edit.plane == TRANSFORM_VIEW && _edit.mode != TRANSFORM_ROTATE) && - !_edit.is_trackball; - - bool is_global_view_plane = (_edit.plane == TRANSFORM_VIEW) && - ((_edit.mode != TRANSFORM_ROTATE) || !spatial_editor->are_local_coords_enabled()); - - const List &selection = editor_selection->get_top_selected_node_list(); - for (Node *E : selection) { - Node3D *sp = Object::cast_to(E); - if (!sp) { - continue; - } - - Node3DEditorSelectedItem *se = editor_selection->get_node_editor_data(sp); - if (!se) { - continue; - } - - if (sp->has_meta("_edit_lock_") && !spatial_editor->is_gizmo_visible()) { - continue; - } - - if (se->gizmo.is_valid()) { - for (KeyValue &GE : se->subgizmos) { - Transform3D xform = GE.value; - Transform3D new_xform = _compute_transform(_edit.mode, se->original * xform, xform, p_motion, p_snap, local_coords, _edit.plane != TRANSFORM_VIEW, is_global_view_plane); // Force orthogonal with subgizmo. - if (!local_coords) { - new_xform = se->original.affine_inverse() * new_xform; - } - se->gizmo->set_subgizmo_transform(GE.key, new_xform); - } - } else { - Transform3D new_xform = _compute_transform(_edit.mode, se->original, se->original_local, p_motion, p_snap, local_coords, sp->get_rotation_edit_mode() != Node3D::ROTATION_EDIT_MODE_BASIS && _edit.plane != TRANSFORM_VIEW, is_global_view_plane); - _transform_gizmo_apply(se->sp, new_xform, local_coords); - } - } - - spatial_editor->update_transform_gizmo(); - surface->queue_redraw(); -} - -// Update the current transform operation in response to an input. -void Node3DEditorViewport::update_transform(bool p_shift) { - Vector3 ray_pos = get_ray_pos(_edit.mouse_pos); - Vector3 ray = get_ray(_edit.mouse_pos); - double snap = EDITOR_GET("interface/inspector/default_float_step"); - int snap_step_decimals = Math::range_step_decimals(snap); - - // View-plane translate/scale always uses global coords; rotation and axis operations respect local/global preference. - bool local_coords = spatial_editor->are_local_coords_enabled() && - !(_edit.plane == TRANSFORM_VIEW && _edit.mode != TRANSFORM_ROTATE); - - switch (_edit.mode) { - case TRANSFORM_SCALE: { - Vector3 motion_mask; - Plane plane; - bool plane_mv = false; - - switch (_edit.plane) { - case TRANSFORM_VIEW: - motion_mask = Vector3(0, 0, 0); - plane = Plane(_get_camera_normal(), _edit.center); - break; - case TRANSFORM_X_AXIS: - motion_mask = spatial_editor->get_gizmo_transform().basis.get_column(0).normalized(); - plane = Plane(motion_mask.cross(motion_mask.cross(_get_camera_normal())).normalized(), _edit.center); - break; - case TRANSFORM_Y_AXIS: - motion_mask = spatial_editor->get_gizmo_transform().basis.get_column(1).normalized(); - plane = Plane(motion_mask.cross(motion_mask.cross(_get_camera_normal())).normalized(), _edit.center); - break; - case TRANSFORM_Z_AXIS: - motion_mask = spatial_editor->get_gizmo_transform().basis.get_column(2).normalized(); - plane = Plane(motion_mask.cross(motion_mask.cross(_get_camera_normal())).normalized(), _edit.center); - break; - case TRANSFORM_YZ: - motion_mask = spatial_editor->get_gizmo_transform().basis.get_column(2).normalized() + spatial_editor->get_gizmo_transform().basis.get_column(1).normalized(); - plane = Plane(spatial_editor->get_gizmo_transform().basis.get_column(0).normalized(), _edit.center); - plane_mv = true; - break; - case TRANSFORM_XZ: - motion_mask = spatial_editor->get_gizmo_transform().basis.get_column(2).normalized() + spatial_editor->get_gizmo_transform().basis.get_column(0).normalized(); - plane = Plane(spatial_editor->get_gizmo_transform().basis.get_column(1).normalized(), _edit.center); - plane_mv = true; - break; - case TRANSFORM_XY: - motion_mask = spatial_editor->get_gizmo_transform().basis.get_column(0).normalized() + spatial_editor->get_gizmo_transform().basis.get_column(1).normalized(); - plane = Plane(spatial_editor->get_gizmo_transform().basis.get_column(2).normalized(), _edit.center); - plane_mv = true; - break; - } - - Vector3 intersection; - if (!plane.intersects_ray(ray_pos, ray, &intersection)) { - break; - } - - Vector3 click; - if (!plane.intersects_ray(_edit.click_ray_pos, _edit.click_ray, &click)) { - break; - } - - Vector3 motion = intersection - click; - if (_edit.plane != TRANSFORM_VIEW) { - if (!plane_mv) { - motion = motion_mask.dot(motion) * motion_mask; - - } else { - // Alternative planar scaling mode - if (p_shift) { - motion = motion_mask.dot(motion) * motion_mask; - } - } - - } else { - const real_t center_click_dist = click.distance_to(_edit.center); - const real_t center_inters_dist = intersection.distance_to(_edit.center); - if (center_click_dist == 0) { - break; - } - - const real_t scale = center_inters_dist - center_click_dist; - motion = Vector3(scale, scale, scale); - } - - motion /= click.distance_to(_edit.center); - - if (spatial_editor->is_snap_enabled()) { - snap = spatial_editor->get_scale_snap() / 100; - } - Vector3 motion_snapped = motion; - motion_snapped.snapf(snap); - // This might not be necessary anymore after issue #288 is solved (in 4.0?). - // TRANSLATORS: Refers to changing the scale of a node in the 3D editor. - set_message(vformat(TTR("Scaling: %s"), vformat("%.*v", snap_step_decimals, motion_snapped))); - if (local_coords) { - // TODO: needed? - motion = _edit.original.basis.inverse().xform(motion); - } - - apply_transform(motion, snap); - } break; - - case TRANSFORM_TRANSLATE: { - Vector3 motion_mask; - Plane plane; - bool plane_mv = false; - - switch (_edit.plane) { - case TRANSFORM_VIEW: - plane = Plane(_get_camera_normal(), _edit.center); - break; - case TRANSFORM_X_AXIS: - motion_mask = spatial_editor->get_gizmo_transform().basis.get_column(0).normalized(); - plane = Plane(motion_mask.cross(motion_mask.cross(_get_camera_normal())).normalized(), _edit.center); - break; - case TRANSFORM_Y_AXIS: - motion_mask = spatial_editor->get_gizmo_transform().basis.get_column(1).normalized(); - plane = Plane(motion_mask.cross(motion_mask.cross(_get_camera_normal())).normalized(), _edit.center); - break; - case TRANSFORM_Z_AXIS: - motion_mask = spatial_editor->get_gizmo_transform().basis.get_column(2).normalized(); - plane = Plane(motion_mask.cross(motion_mask.cross(_get_camera_normal())).normalized(), _edit.center); - break; - case TRANSFORM_YZ: - plane = Plane(spatial_editor->get_gizmo_transform().basis.get_column(0).normalized(), _edit.center); - plane_mv = true; - break; - case TRANSFORM_XZ: - plane = Plane(spatial_editor->get_gizmo_transform().basis.get_column(1).normalized(), _edit.center); - plane_mv = true; - break; - case TRANSFORM_XY: - plane = Plane(spatial_editor->get_gizmo_transform().basis.get_column(2).normalized(), _edit.center); - plane_mv = true; - break; - } - - Vector3 intersection; - if (!plane.intersects_ray(ray_pos, ray, &intersection)) { - break; - } - - Vector3 click; - if (!plane.intersects_ray(_edit.click_ray_pos, _edit.click_ray, &click)) { - break; - } - - Vector3 motion = intersection - click; - if (_edit.plane != TRANSFORM_VIEW) { - if (!plane_mv) { - motion = motion_mask.dot(motion) * motion_mask; - } - } - - if (spatial_editor->is_snap_enabled()) { - snap = spatial_editor->get_translate_snap(); - } - Vector3 motion_snapped = motion; - motion_snapped.snapf(snap); - // TRANSLATORS: Refers to changing the position of a node in the 3D editor. - set_message(vformat(TTR("Translating: %s"), vformat("%.*v", snap_step_decimals, motion_snapped))); - if (local_coords) { - motion = spatial_editor->get_gizmo_transform().basis.inverse().xform(motion); - } - - apply_transform(motion, snap); - } break; - - case TRANSFORM_ROTATE: { - Plane plane; - if (camera->get_projection() == Camera3D::PROJECTION_PERSPECTIVE) { - Vector3 cam_to_obj = _edit.center - _get_camera_position(); - if (!cam_to_obj.is_zero_approx()) { - plane = Plane(cam_to_obj.normalized(), _edit.center); - } else { - plane = Plane(_get_camera_normal(), _edit.center); - } - } else { - plane = Plane(_get_camera_normal(), _edit.center); - } - - if (_edit.is_trackball) { - Vector2 motion_delta = _edit.mouse_pos - _edit.original_mouse_pos; - real_t sensitivity = TRACKBALL_SENSITIVITY * EDSCALE; - Vector2 rotation_input = motion_delta * sensitivity; - - Transform3D cam_transform = view_3d_controller->to_camera_transform(); - Vector3 cam_right = cam_transform.basis.get_column(0).normalized(); - Vector3 cam_up = cam_transform.basis.get_column(1).normalized(); - Vector3 rotation_axis = cam_up * rotation_input.x + cam_right * rotation_input.y; - - real_t rotation_angle = rotation_axis.length(); - if (rotation_angle > 0.0f) { - rotation_axis /= rotation_angle; - - if (spatial_editor->is_snap_enabled()) { - double snap_step = spatial_editor->get_rotate_snap(); - double angle_deg = Math::rad_to_deg(rotation_angle); - angle_deg = Math::snapped(angle_deg, snap_step); - rotation_angle = Math::deg_to_rad(angle_deg); - } - - double angle_deg = Math::rad_to_deg(rotation_angle); - set_message(vformat(TTR("Rotating %s degrees."), String::num(angle_deg, 2))); - - apply_transform(rotation_axis, rotation_angle); - } - break; - } - - Vector3 local_axis; - Vector3 global_axis; - switch (_edit.plane) { - case TRANSFORM_VIEW: - local_axis = _edit.view_axis_local; - global_axis = _get_camera_normal(); - break; - case TRANSFORM_X_AXIS: - local_axis = Vector3(1, 0, 0); - break; - case TRANSFORM_Y_AXIS: - local_axis = Vector3(0, 1, 0); - break; - case TRANSFORM_Z_AXIS: - local_axis = Vector3(0, 0, 1); - break; - case TRANSFORM_YZ: - case TRANSFORM_XZ: - case TRANSFORM_XY: - break; - } - - if (_edit.plane != TRANSFORM_VIEW) { - global_axis = spatial_editor->get_gizmo_transform().basis.xform(local_axis).normalized(); - } - - Vector3 intersection; - if (!plane.intersects_ray(ray_pos, ray, &intersection)) { - break; - } - - Vector3 click; - if (!plane.intersects_ray(_edit.click_ray_pos, _edit.click_ray, &click)) { - break; - } - - Vector3 current_rotation_vector = (intersection - _edit.center).normalized(); - - if (_edit.initial_click_vector == Vector3()) { - Plane rotation_plane(global_axis, _edit.center); - Vector3 click_on_rotation_plane; - if (rotation_plane.intersects_ray(_edit.click_ray_pos, _edit.click_ray, &click_on_rotation_plane)) { - _edit.initial_click_vector = (click_on_rotation_plane - _edit.center).normalized(); - } else { - _edit.initial_click_vector = (click - _edit.center).normalized(); - } - _edit.previous_rotation_vector = current_rotation_vector; - _edit.accumulated_rotation_angle = 0.0; - _edit.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; - - 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; - - if (spatial_editor->is_snap_enabled()) { - snap = spatial_editor->get_rotate_snap(); - snap_step_decimals = Math::range_step_decimals(snap); - } - - if (axis_is_orthogonal) { - _edit.show_rotation_line = false; - Vector3 projection_axis = plane.normal.cross(global_axis); - Vector3 delta = intersection - click; - float projection = delta.dot(projection_axis); - double orth_angle = (projection * (Math::PI / 2.0f)) / (gizmo_scale * GIZMO_CIRCLE_SIZE); - _edit.rotation_angle = spatial_editor->is_snap_enabled() - ? Math::deg_to_rad(Math::snapped(Math::rad_to_deg(orth_angle), snap)) - : orth_angle; - } else { - _edit.show_rotation_line = true; - _edit.rotation_angle = spatial_editor->is_snap_enabled() - ? Math::deg_to_rad(Math::snapped(Math::rad_to_deg(_edit.accumulated_rotation_angle), snap)) - : _edit.accumulated_rotation_angle; - } - set_message(vformat(TTR("Rotating %s degrees."), String::num(Math::rad_to_deg(_edit.rotation_angle), snap_step_decimals))); - - Vector3 compute_axis = local_coords ? local_axis : global_axis; - apply_transform(compute_axis, _edit.rotation_angle); - } break; - default: { - } - } -} - -void Node3DEditorViewport::update_transform_numeric() { - Vector3 motion; - switch (_edit.plane) { - case TRANSFORM_VIEW: { - switch (_edit.mode) { - case TRANSFORM_TRANSLATE: - motion = Vector3(1, 0, 0); - break; - case TRANSFORM_ROTATE: - motion = _edit.view_axis_local; - break; - case TRANSFORM_SCALE: - motion = Vector3(1, 1, 1); - break; - case TRANSFORM_NONE: - ERR_FAIL_MSG("_edit.mode cannot be TRANSFORM_NONE in update_transform_numeric."); - } - break; - } - case TRANSFORM_X_AXIS: - motion = Vector3(1, 0, 0); - break; - case TRANSFORM_Y_AXIS: - motion = Vector3(0, 1, 0); - break; - case TRANSFORM_Z_AXIS: - motion = Vector3(0, 0, 1); - break; - case TRANSFORM_XY: - motion = Vector3(1, 1, 0); - break; - case TRANSFORM_XZ: - motion = Vector3(1, 0, 1); - break; - case TRANSFORM_YZ: - motion = Vector3(0, 1, 1); - break; - } - - double value = _edit.numeric_input * (_edit.numeric_negate ? -1 : 1); - double extra = 0.0; - switch (_edit.mode) { - case TRANSFORM_TRANSLATE: - motion *= value; - set_message(vformat(TTR("Translating %s."), motion)); - break; - case TRANSFORM_ROTATE: - extra = Math::deg_to_rad(value); - set_message(vformat(TTR("Rotating %f degrees."), value)); - break; - case TRANSFORM_SCALE: - // To halve the size of an object in Blender, you scale it by 0.5. - // Doing the same in Godot is considered scaling it by -0.5. - motion *= (value - 1.0); - set_message(vformat(TTR("Scaling %s."), motion)); - break; - case TRANSFORM_NONE: - ERR_FAIL_MSG("_edit.mode cannot be TRANSFORM_NONE in update_transform_numeric."); - } - - apply_transform(motion, extra); -} - -// Perform cleanup after a transform operation is committed or canceled. -void Node3DEditorViewport::finish_transform() { - _edit.mode = TRANSFORM_NONE; - _edit.instant = false; - _edit.numeric_input = 0; - _edit.numeric_next_decimal = 0; - _edit.numeric_negate = false; - _edit.is_trackball = false; - _edit.initial_click_vector = Vector3(); - _edit.previous_rotation_vector = Vector3(); - _edit.accumulated_rotation_angle = 0.0; - _edit.rotation_angle = 0.0; - _edit.gizmo_initiated = false; - _edit.children_original_globals.clear(); - spatial_editor->set_local_coords_enabled(_edit.original_local); - spatial_editor->update_transform_gizmo(); - surface->queue_redraw(); - set_process_input(false); - clicked = ObjectID(); -} - -// Register a shortcut and also add it as an input action with the same events. -void Node3DEditorViewport::register_shortcut_action(const String &p_path, const String &p_name, Key p_keycode, bool p_physical) { - Ref sc = ED_SHORTCUT(p_path, p_name, p_keycode, p_physical); - shortcut_changed_callback(sc, p_path); - // Connect to the change event on the shortcut so the input binding can be updated. - sc->connect_changed(callable_mp(this, &Node3DEditorViewport::shortcut_changed_callback).bind(sc, p_path)); -} - -// Update the action in the InputMap to the provided shortcut events. -void Node3DEditorViewport::shortcut_changed_callback(const Ref p_shortcut, const String &p_shortcut_path) { - InputMap *im = InputMap::get_singleton(); - if (im->has_action(p_shortcut_path)) { - im->action_erase_events(p_shortcut_path); - } else { - im->add_action(p_shortcut_path); - } - - for (int i = 0; i < p_shortcut->get_events().size(); i++) { - im->action_add_event(p_shortcut_path, p_shortcut->get_events()[i]); - } - - if (view_3d_controller.is_valid()) { - _update_view_3d_controller(); - } -} - -void Node3DEditorViewport::_set_lock_view_rotation(bool p_lock_rotation) { - view_3d_controller->set_lock_rotation(p_lock_rotation); - int idx = view_display_menu->get_popup()->get_item_index(VIEW_LOCK_ROTATION); - view_display_menu->get_popup()->set_item_checked(idx, p_lock_rotation); - if (p_lock_rotation) { - locked_label->show(); - } else { - locked_label->hide(); - } -} - -void Node3DEditorViewport::_add_advanced_debug_draw_mode_item(PopupMenu *p_popup, const String &p_name, int p_value, SupportedRenderingMethods p_rendering_methods, const String &p_tooltip) { - display_submenu->add_radio_check_item(p_name, p_value); - Array item_data = { p_rendering_methods, p_tooltip }; - display_submenu->set_item_metadata(-1, item_data); // Tooltip is assigned in NOTIFICATION_TRANSLATION_CHANGED. -} - -void Node3DEditorViewport::_load_viewport_inputs() { - // Registering with Key::NONE intentionally creates an empty Array. - register_shortcut_action("spatial_editor/viewport_orbit_modifier_1", TTRC("Viewport Orbit Modifier 1"), Key::NONE); - register_shortcut_action("spatial_editor/viewport_orbit_modifier_2", TTRC("Viewport Orbit Modifier 2"), Key::NONE); - register_shortcut_action("spatial_editor/viewport_orbit_snap_modifier_1", TTRC("Viewport Orbit Snap Modifier 1"), Key::ALT); - register_shortcut_action("spatial_editor/viewport_orbit_snap_modifier_2", TTRC("Viewport Orbit Snap Modifier 2"), Key::NONE); - register_shortcut_action("spatial_editor/viewport_pan_modifier_1", TTRC("Viewport Pan Modifier 1"), Key::SHIFT); - register_shortcut_action("spatial_editor/viewport_pan_modifier_2", TTRC("Viewport Pan Modifier 2"), Key::NONE); - register_shortcut_action("spatial_editor/viewport_zoom_modifier_1", TTRC("Viewport Zoom Modifier 1"), Key::CTRL); - register_shortcut_action("spatial_editor/viewport_zoom_modifier_2", TTRC("Viewport Zoom Modifier 2"), Key::NONE); - - register_shortcut_action("spatial_editor/freelook_left", TTRC("Freelook Left"), Key::A, true); - register_shortcut_action("spatial_editor/freelook_right", TTRC("Freelook Right"), Key::D, true); - register_shortcut_action("spatial_editor/freelook_forward", TTRC("Freelook Forward"), Key::W, true); - register_shortcut_action("spatial_editor/freelook_backwards", TTRC("Freelook Backwards"), Key::S, true); - register_shortcut_action("spatial_editor/freelook_up", TTRC("Freelook Up"), Key::E, true); - register_shortcut_action("spatial_editor/freelook_down", TTRC("Freelook Down"), Key::Q, true); - register_shortcut_action("spatial_editor/freelook_speed_modifier", TTRC("Freelook Speed Modifier"), Key::SHIFT); - register_shortcut_action("spatial_editor/freelook_slow_modifier", TTRC("Freelook Slow Modifier"), Key::ALT); -} - -Node3DEditorViewport::Node3DEditorViewport(Node3DEditor *p_spatial_editor, int p_index) { - cpu_time_history_index = 0; - gpu_time_history_index = 0; - - _edit.mode = TRANSFORM_NONE; - _edit.plane = TRANSFORM_VIEW; - _edit.show_rotation_line = true; - _edit.instant = false; - _edit.gizmo_handle = -1; - _edit.gizmo_handle_secondary = false; - - index = p_index; - editor_selection = EditorNode::get_singleton()->get_editor_selection(); - editor_selection->connect("selection_changed", callable_mp(this, &Node3DEditorViewport::_reset_follow_mode_count)); - - message_time = 0; - zoom_indicator_delay = 0.0; - - spatial_editor = p_spatial_editor; - SubViewportContainer *c = memnew(SubViewportContainer); - subviewport_container = c; - c->set_stretch(true); - add_child(c); - c->set_anchors_and_offsets_preset(Control::PRESET_FULL_RECT); - viewport = memnew(SubViewport); - viewport->set_disable_input(true); - - c->add_child(viewport); - surface = memnew(Control); - SET_DRAG_FORWARDING_CD(surface, Node3DEditorViewport); - add_child(surface); - surface->set_anchors_and_offsets_preset(Control::PRESET_FULL_RECT); - surface->set_clip_contents(true); - camera = memnew(Camera3D); - camera->set_disable_gizmos(true); - camera->set_cull_mask(((1 << 20) - 1) | (1 << (GIZMO_BASE_LAYER + p_index)) | (1 << GIZMO_EDIT_LAYER) | (1 << GIZMO_GRID_LAYER) | (1 << MISC_TOOL_LAYER)); - viewport->add_child(camera); - camera->make_current(); - surface->set_focus_mode(FOCUS_ALL); - - VBoxContainer *vbox = memnew(VBoxContainer); - surface->add_child(vbox); - vbox->set_offset(SIDE_LEFT, 10 * EDSCALE); - vbox->set_offset(SIDE_TOP, 10 * EDSCALE); - - HBoxContainer *hbox = memnew(HBoxContainer); - vbox->add_child(hbox); - - view_display_menu = memnew(MenuButton); - view_display_menu->set_flat(false); - view_display_menu->set_h_size_flags(0); - view_display_menu->set_shortcut_context(this); - view_display_menu->set_accessibility_name(TTRC("View")); - view_display_menu->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED); - view_display_menu->get_popup()->set_auto_translate_mode(AUTO_TRANSLATE_MODE_ALWAYS); - hbox->add_child(view_display_menu); - - view_display_menu->get_popup()->set_hide_on_checkable_item_selection(false); - - view_display_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("spatial_editor/top_view"), VIEW_TOP); - view_display_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("spatial_editor/bottom_view"), VIEW_BOTTOM); - view_display_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("spatial_editor/left_view"), VIEW_LEFT); - view_display_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("spatial_editor/right_view"), VIEW_RIGHT); - view_display_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("spatial_editor/front_view"), VIEW_FRONT); - view_display_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("spatial_editor/rear_view"), VIEW_REAR); - view_display_menu->get_popup()->add_separator(); - view_display_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("spatial_editor/switch_perspective_orthogonal"), VIEW_SWITCH_PERSPECTIVE_ORTHOGONAL); - view_display_menu->get_popup()->add_radio_check_item(TTRC("Perspective"), VIEW_PERSPECTIVE); - view_display_menu->get_popup()->add_radio_check_item(TTRC("Orthogonal"), VIEW_ORTHOGONAL); - view_display_menu->get_popup()->set_item_checked(view_display_menu->get_popup()->get_item_index(VIEW_PERSPECTIVE), true); - view_display_menu->get_popup()->add_check_item(TTRC("Auto Orthogonal Enabled"), VIEW_AUTO_ORTHOGONAL); - view_display_menu->get_popup()->set_item_checked(view_display_menu->get_popup()->get_item_index(VIEW_AUTO_ORTHOGONAL), true); - view_display_menu->get_popup()->add_separator(); - view_display_menu->get_popup()->add_check_shortcut(ED_SHORTCUT("spatial_editor/view_lock_rotation", TTRC("Lock View Rotation")), VIEW_LOCK_ROTATION); - view_display_menu->get_popup()->add_separator(); - // TRANSLATORS: "Normal" as in "normal life", not "normal vector". - view_display_menu->get_popup()->add_radio_check_shortcut(ED_SHORTCUT("spatial_editor/view_display_normal", TTRC("Display Normal")), VIEW_DISPLAY_NORMAL); - view_display_menu->get_popup()->add_radio_check_shortcut(ED_SHORTCUT("spatial_editor/view_display_wireframe", TTRC("Display Wireframe")), VIEW_DISPLAY_WIREFRAME); - view_display_menu->get_popup()->add_radio_check_shortcut(ED_SHORTCUT("spatial_editor/view_display_overdraw", TTRC("Display Overdraw")), VIEW_DISPLAY_OVERDRAW); - view_display_menu->get_popup()->add_radio_check_shortcut(ED_SHORTCUT("spatial_editor/view_display_lighting", TTRC("Display Lighting")), VIEW_DISPLAY_LIGHTING); - view_display_menu->get_popup()->add_radio_check_shortcut(ED_SHORTCUT("spatial_editor/view_display_unshaded", TTRC("Display Unshaded")), VIEW_DISPLAY_UNSHADED); - view_display_menu->get_popup()->set_item_checked(view_display_menu->get_popup()->get_item_index(VIEW_DISPLAY_NORMAL), true); - - display_submenu = memnew(PopupMenu); - display_submenu->set_hide_on_checkable_item_selection(false); - _add_advanced_debug_draw_mode_item(display_submenu, TTRC("Directional Shadow Splits"), VIEW_DISPLAY_DEBUG_PSSM_SPLITS, SupportedRenderingMethods::FORWARD_PLUS_MOBILE, - TTRC("Displays directional shadow splits in different colors to make adjusting split thresholds easier. \nRed: 1st split (closest to the camera), Green: 2nd split, Blue: 3rd split, Yellow: 4th split (furthest from the camera)")); - display_submenu->add_separator(); - // TRANSLATORS: "Normal" as in "normal vector", not "normal life". - _add_advanced_debug_draw_mode_item(display_submenu, TTRC("Normal Buffer"), VIEW_DISPLAY_NORMAL_BUFFER, SupportedRenderingMethods::FORWARD_PLUS); - display_submenu->add_separator(); - _add_advanced_debug_draw_mode_item(display_submenu, TTRC("Shadow Atlas"), VIEW_DISPLAY_DEBUG_SHADOW_ATLAS, SupportedRenderingMethods::ALL, - TTRC("Displays the shadow atlas used for positional (omni/spot) shadow mapping.\nRequires a visible OmniLight3D or SpotLight3D node with shadows enabled to have a visible effect.")); - _add_advanced_debug_draw_mode_item(display_submenu, TTRC("Directional Shadow Map"), VIEW_DISPLAY_DEBUG_DIRECTIONAL_SHADOW_ATLAS, SupportedRenderingMethods::ALL, - TTRC("Displays the shadow map used for directional shadow mapping.\nRequires a visible DirectionalLight3D node with shadows enabled to have a visible effect.")); - display_submenu->add_separator(); - _add_advanced_debug_draw_mode_item(display_submenu, TTRC("Decal Atlas"), VIEW_DISPLAY_DEBUG_DECAL_ATLAS, SupportedRenderingMethods::FORWARD_PLUS_MOBILE); - _add_advanced_debug_draw_mode_item(display_submenu, TTRC("AreaLight3D Atlas"), VIEW_DISPLAY_DEBUG_AREA_LIGHT_ATLAS, SupportedRenderingMethods::FORWARD_PLUS_MOBILE); - display_submenu->add_separator(); - _add_advanced_debug_draw_mode_item(display_submenu, TTRC("VoxelGI Lighting"), VIEW_DISPLAY_DEBUG_VOXEL_GI_LIGHTING, SupportedRenderingMethods::FORWARD_PLUS, - TTRC("Requires a visible VoxelGI node that has been baked to have a visible effect.")); - _add_advanced_debug_draw_mode_item(display_submenu, TTRC("VoxelGI Albedo"), VIEW_DISPLAY_DEBUG_VOXEL_GI_ALBEDO, SupportedRenderingMethods::FORWARD_PLUS, - TTRC("Requires a visible VoxelGI node that has been baked to have a visible effect.")); - _add_advanced_debug_draw_mode_item(display_submenu, TTRC("VoxelGI Emission"), VIEW_DISPLAY_DEBUG_VOXEL_GI_EMISSION, SupportedRenderingMethods::FORWARD_PLUS, - TTRC("Requires a visible VoxelGI node that has been baked to have a visible effect.")); - display_submenu->add_separator(); - _add_advanced_debug_draw_mode_item(display_submenu, TTRC("SDFGI Cascades"), VIEW_DISPLAY_DEBUG_SDFGI, SupportedRenderingMethods::FORWARD_PLUS, - TTRC("Requires SDFGI to be enabled in Environment to have a visible effect.")); - _add_advanced_debug_draw_mode_item(display_submenu, TTRC("SDFGI Probes"), VIEW_DISPLAY_DEBUG_SDFGI_PROBES, SupportedRenderingMethods::FORWARD_PLUS, - TTRC("Left-click a SDFGI probe to display its occlusion information (white = not occluded, red = fully occluded).\nRequires SDFGI to be enabled in Environment to have a visible effect.")); - display_submenu->add_separator(); - _add_advanced_debug_draw_mode_item(display_submenu, TTRC("Scene Luminance"), VIEW_DISPLAY_DEBUG_SCENE_LUMINANCE, SupportedRenderingMethods::FORWARD_PLUS_MOBILE, - TTRC("Displays the scene luminance computed from the 3D buffer. This is used for Auto Exposure calculation.\nRequires Auto Exposure to be enabled in CameraAttributes to have a visible effect.")); - display_submenu->add_separator(); - _add_advanced_debug_draw_mode_item(display_submenu, TTRC("SSAO"), VIEW_DISPLAY_DEBUG_SSAO, SupportedRenderingMethods::FORWARD_PLUS, - TTRC("Displays the screen-space ambient occlusion buffer. Requires SSAO to be enabled in Environment to have a visible effect.")); - _add_advanced_debug_draw_mode_item(display_submenu, TTRC("SSIL"), VIEW_DISPLAY_DEBUG_SSIL, SupportedRenderingMethods::FORWARD_PLUS, - TTRC("Displays the screen-space indirect lighting buffer. Requires SSIL to be enabled in Environment to have a visible effect.")); - display_submenu->add_separator(); - _add_advanced_debug_draw_mode_item(display_submenu, TTRC("VoxelGI/SDFGI Buffer"), VIEW_DISPLAY_DEBUG_GI_BUFFER, SupportedRenderingMethods::FORWARD_PLUS, - TTRC("Requires SDFGI or VoxelGI to be enabled to have a visible effect.")); - display_submenu->add_separator(); - _add_advanced_debug_draw_mode_item(display_submenu, TTRC("Disable Mesh LOD"), VIEW_DISPLAY_DEBUG_DISABLE_LOD, SupportedRenderingMethods::ALL, - TTRC("Renders all meshes with their highest level of detail regardless of their distance from the camera.")); - display_submenu->add_separator(); - _add_advanced_debug_draw_mode_item(display_submenu, TTRC("OmniLight3D Cluster"), VIEW_DISPLAY_DEBUG_CLUSTER_OMNI_LIGHTS, SupportedRenderingMethods::FORWARD_PLUS, - TTRC("Highlights tiles of pixels that are affected by at least one OmniLight3D.")); - _add_advanced_debug_draw_mode_item(display_submenu, TTRC("SpotLight3D Cluster"), VIEW_DISPLAY_DEBUG_CLUSTER_SPOT_LIGHTS, SupportedRenderingMethods::FORWARD_PLUS, - TTRC("Highlights tiles of pixels that are affected by at least one SpotLight3D.")); - _add_advanced_debug_draw_mode_item(display_submenu, TTRC("AreaLight3D Cluster"), VIEW_DISPLAY_DEBUG_CLUSTER_AREA_LIGHTS, SupportedRenderingMethods::FORWARD_PLUS, - TTRC("Highlights tiles of pixels that are affected by at least one AreaLight3D.")); - _add_advanced_debug_draw_mode_item(display_submenu, TTRC("Decal Cluster"), VIEW_DISPLAY_DEBUG_CLUSTER_DECALS, SupportedRenderingMethods::FORWARD_PLUS, - TTRC("Highlights tiles of pixels that are affected by at least one Decal.")); - _add_advanced_debug_draw_mode_item(display_submenu, TTRC("ReflectionProbe Cluster"), VIEW_DISPLAY_DEBUG_CLUSTER_REFLECTION_PROBES, SupportedRenderingMethods::FORWARD_PLUS, - TTRC("Highlights tiles of pixels that are affected by at least one ReflectionProbe.")); - _add_advanced_debug_draw_mode_item(display_submenu, TTRC("Occlusion Culling Buffer"), VIEW_DISPLAY_DEBUG_OCCLUDERS, SupportedRenderingMethods::FORWARD_PLUS_MOBILE, - TTRC("Represents occluders with black pixels. Requires occlusion culling to be enabled to have a visible effect.")); - _add_advanced_debug_draw_mode_item(display_submenu, TTRC("Motion Vectors"), VIEW_DISPLAY_MOTION_VECTORS, SupportedRenderingMethods::FORWARD_PLUS, - TTRC("Represents motion vectors with colored lines in the direction of motion. Gray dots represent areas with no per-pixel motion.")); - _add_advanced_debug_draw_mode_item(display_submenu, TTRC("Internal Buffer"), VIEW_DISPLAY_INTERNAL_BUFFER, SupportedRenderingMethods::FORWARD_PLUS_MOBILE, - TTRC("Shows the scene rendered in linear colorspace before any tonemapping or post-processing.")); - view_display_menu->get_popup()->add_submenu_node_item(TTRC("Display Advanced..."), display_submenu, VIEW_DISPLAY_ADVANCED); - - view_display_menu->get_popup()->add_separator(); - view_display_menu->get_popup()->add_check_shortcut(ED_SHORTCUT("spatial_editor/view_environment", TTRC("View Environment")), VIEW_ENVIRONMENT); - view_display_menu->get_popup()->add_check_shortcut(ED_SHORTCUT("spatial_editor/view_gizmos", TTRC("View Gizmos")), VIEW_GIZMOS); - view_display_menu->get_popup()->add_check_shortcut(ED_SHORTCUT("spatial_editor/view_transform_gizmo", TTRC("View Transform Gizmo")), VIEW_TRANSFORM_GIZMO); - view_display_menu->get_popup()->add_check_shortcut(ED_SHORTCUT("spatial_editor/view_grid_lines", TTRC("View Grid")), VIEW_GRID); - view_display_menu->get_popup()->add_check_shortcut(ED_SHORTCUT("spatial_editor/view_information", TTRC("View Information")), VIEW_INFORMATION); - view_display_menu->get_popup()->add_check_shortcut(ED_SHORTCUT("spatial_editor/view_fps", TTRC("View Frame Time")), VIEW_FRAME_TIME); - view_display_menu->get_popup()->set_item_checked(view_display_menu->get_popup()->get_item_index(VIEW_ENVIRONMENT), true); - view_display_menu->get_popup()->add_separator(); - view_display_menu->get_popup()->add_check_shortcut(ED_SHORTCUT("spatial_editor/view_half_resolution", TTRC("Half Resolution")), VIEW_HALF_RESOLUTION); - view_display_menu->get_popup()->add_separator(); - view_display_menu->get_popup()->add_check_shortcut(ED_SHORTCUT("spatial_editor/view_audio_listener", TTRC("Audio Listener")), VIEW_AUDIO_LISTENER); - view_display_menu->get_popup()->add_check_shortcut(ED_SHORTCUT("spatial_editor/view_audio_doppler", TTRC("Enable Doppler")), VIEW_AUDIO_DOPPLER); - view_display_menu->get_popup()->set_item_checked(view_display_menu->get_popup()->get_item_index(VIEW_GIZMOS), true); - view_display_menu->get_popup()->set_item_checked(view_display_menu->get_popup()->get_item_index(VIEW_TRANSFORM_GIZMO), true); - view_display_menu->get_popup()->set_item_checked(view_display_menu->get_popup()->get_item_index(VIEW_GRID), true); - - view_display_menu->get_popup()->add_separator(); - view_display_menu->get_popup()->add_check_shortcut(ED_SHORTCUT("spatial_editor/view_cinematic_preview", TTRC("Cinematic Preview")), VIEW_CINEMATIC_PREVIEW); - - view_display_menu->get_popup()->add_separator(); - view_display_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("spatial_editor/focus_origin"), VIEW_CENTER_TO_ORIGIN); - view_display_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("spatial_editor/focus_selection"), VIEW_CENTER_TO_SELECTION); - view_display_menu->get_popup()->set_item_tooltip(-1, TTR("Press Focus Selection twice to start following the selection as it moves. Press it yet another time to stop following the selection.")); - view_display_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("spatial_editor/align_transform_with_view"), VIEW_ALIGN_TRANSFORM_WITH_VIEW); - view_display_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("spatial_editor/align_rotation_with_view"), VIEW_ALIGN_ROTATION_WITH_VIEW); - view_display_menu->get_popup()->connect(SceneStringName(id_pressed), callable_mp(this, &Node3DEditorViewport::_menu_option)); - display_submenu->connect(SceneStringName(id_pressed), callable_mp(this, &Node3DEditorViewport::_menu_option)); - view_display_menu->set_disable_shortcuts(true); - - _load_viewport_inputs(); - InputMap::get_singleton()->connect("project_settings_loaded", callable_mp(this, &Node3DEditorViewport::_load_viewport_inputs)); - - ED_SHORTCUT("spatial_editor/lock_transform_x", TTRC("Lock Transformation to X axis"), Key::X); - ED_SHORTCUT("spatial_editor/lock_transform_y", TTRC("Lock Transformation to Y axis"), Key::Y); - ED_SHORTCUT("spatial_editor/lock_transform_z", TTRC("Lock Transformation to Z axis"), Key::Z); - ED_SHORTCUT("spatial_editor/lock_transform_yz", TTRC("Lock Transformation to YZ plane"), KeyModifierMask::SHIFT | Key::X); - ED_SHORTCUT("spatial_editor/lock_transform_xz", TTRC("Lock Transformation to XZ plane"), KeyModifierMask::SHIFT | Key::Y); - ED_SHORTCUT("spatial_editor/lock_transform_xy", TTRC("Lock Transformation to XY plane"), KeyModifierMask::SHIFT | Key::Z); - ED_SHORTCUT("spatial_editor/cancel_transform", TTRC("Cancel Transformation"), Key::ESCAPE); - ED_SHORTCUT("spatial_editor/instant_translate", TTRC("Begin Translate Transformation")); - ED_SHORTCUT("spatial_editor/instant_rotate", TTRC("Begin Rotate Transformation")); - ED_SHORTCUT("spatial_editor/instant_scale", TTRC("Begin Scale Transformation")); - ED_SHORTCUT("spatial_editor/collision_reposition", TTRC("Reposition Using Collisions"), KeyModifierMask::SHIFT | Key::G); - ED_SHORTCUT("spatial_editor/reset_transform_position", TTRC("Reset Position"), KeyModifierMask::ALT + Key::W); - ED_SHORTCUT("spatial_editor/reset_transform_rotation", TTRC("Reset Rotation"), KeyModifierMask::ALT + Key::E); - ED_SHORTCUT("spatial_editor/reset_transform_scale", TTRC("Reset Scale"), KeyModifierMask::ALT + Key::R); - - translation_preview_button = memnew(EditorTranslationPreviewButton); - hbox->add_child(translation_preview_button); - - follow_mode = memnew(Button); - follow_mode->set_tooltip_text(TTR("Click to stop following this node as it moves.")); - follow_mode->hide(); - vbox->add_child(follow_mode); - follow_mode->connect(SceneStringName(pressed), callable_mp(this, &Node3DEditorViewport::_disable_follow_mode)); - - preview_camera = memnew(CheckBox); - preview_camera->set_text(TTRC("Preview")); - preview_camera->set_tooltip_text(TTRC("Preview through the selected camera.\nHold Shift while clicking to also enable Pilot mode.")); - // Using Control even on macOS to avoid conflict with Quick Open shortcut. - preview_camera->set_shortcut(ED_SHORTCUT("spatial_editor/toggle_camera_preview", TTRC("Toggle Camera Preview"), KeyModifierMask::CTRL | Key::P)); - vbox->add_child(preview_camera); - preview_camera->set_h_size_flags(0); - preview_camera->set_theme_type_variation("CheckBoxNoIconTint"); - preview_camera->hide(); - preview_camera->connect(SceneStringName(toggled), callable_mp(this, &Node3DEditorViewport::_toggle_camera_preview)); - - pilot_camera = memnew(CheckBox); - pilot_camera->set_text(TTRC("Pilot")); - pilot_camera->set_tooltip_text(TTRC("Enable pilot mode for the preview camera.\nAllows WASD movement and mouse look when in preview mode.")); - pilot_camera->set_shortcut(ED_SHORTCUT("spatial_editor/toggle_pilot_preview", TTRC("Toggle Pilot Mode in Preview"))); - vbox->add_child(pilot_camera); - pilot_camera->set_h_size_flags(0); - pilot_camera->set_theme_type_variation("CheckBoxNoIconTint"); - pilot_camera->hide(); - pilot_camera->connect(SceneStringName(toggled), callable_mp(this, &Node3DEditorViewport::_toggle_pilot_preview)); - previewing = nullptr; - gizmo_scale = 1.0; - - preview_node = nullptr; - - bottom_center_vbox = memnew(VBoxContainer); - bottom_center_vbox->set_anchors_preset(LayoutPreset::PRESET_CENTER); - bottom_center_vbox->set_anchor_and_offset(SIDE_TOP, ANCHOR_END, -20 * EDSCALE); - bottom_center_vbox->set_anchor_and_offset(SIDE_BOTTOM, ANCHOR_END, -10 * EDSCALE); - bottom_center_vbox->set_h_grow_direction(GROW_DIRECTION_BOTH); - bottom_center_vbox->set_v_grow_direction(GROW_DIRECTION_BEGIN); - surface->add_child(bottom_center_vbox); - - info_panel = memnew(PanelContainer); - info_panel->set_anchor_and_offset(SIDE_LEFT, ANCHOR_END, -90 * EDSCALE); - info_panel->set_anchor_and_offset(SIDE_TOP, ANCHOR_END, -90 * EDSCALE); - info_panel->set_anchor_and_offset(SIDE_RIGHT, ANCHOR_END, -10 * EDSCALE); - info_panel->set_anchor_and_offset(SIDE_BOTTOM, ANCHOR_END, -10 * EDSCALE); - info_panel->set_h_grow_direction(GROW_DIRECTION_BEGIN); - info_panel->set_v_grow_direction(GROW_DIRECTION_BEGIN); - info_panel->set_mouse_filter(MOUSE_FILTER_IGNORE); - surface->add_child(info_panel); - info_panel->hide(); - - info_label = memnew(Label); - info_label->set_focus_mode(FOCUS_ACCESSIBILITY); - info_panel->add_child(info_label); - - cinema_label = memnew(Label); - cinema_label->set_anchor_and_offset(SIDE_TOP, ANCHOR_BEGIN, 10 * EDSCALE); - cinema_label->set_h_grow_direction(GROW_DIRECTION_END); - cinema_label->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER); - cinema_label->set_vertical_alignment(VERTICAL_ALIGNMENT_CENTER); - surface->add_child(cinema_label); - cinema_label->set_text(TTRC("Cinematic Preview")); - cinema_label->hide(); - previewing_cinema = false; - - locked_label = memnew(Label); - locked_label->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER); - locked_label->set_vertical_alignment(VERTICAL_ALIGNMENT_CENTER); - locked_label->set_h_size_flags(SIZE_SHRINK_CENTER); - bottom_center_vbox->add_child(locked_label); - locked_label->set_text(TTRC("View Rotation Locked")); - locked_label->hide(); - - zoom_limit_label = memnew(Label); - zoom_limit_label->set_text(TTRC(U"To zoom further, change the camera's clipping planes (View → Settings...)")); - zoom_limit_label->set_name("ZoomLimitMessageLabel"); - zoom_limit_label->add_theme_color_override(SceneStringName(font_color), Color(1, 1, 1, 1)); - zoom_limit_label->hide(); - bottom_center_vbox->add_child(zoom_limit_label); - - tooltip_panel = memnew(RichTextLabel); - vbox->add_child(tooltip_panel); - tooltip_panel->hide(); - tooltip_panel->set_h_grow_direction(GROW_DIRECTION_BEGIN); - tooltip_panel->set_v_grow_direction(GROW_DIRECTION_BEGIN); - tooltip_panel->set_mouse_filter(MOUSE_FILTER_IGNORE); - tooltip_panel->set_focus_mode(FOCUS_ACCESSIBILITY); - tooltip_panel->set_use_bbcode(true); - tooltip_panel->set_fit_content(true); - tooltip_panel->set_scroll_active(false); - tooltip_panel->set_tab_size(1); - tooltip_panel->set_autowrap_mode(TextServer::AUTOWRAP_OFF); - tooltip_panel->set_anchors_and_offsets_preset(LayoutPreset::PRESET_TOP_LEFT); - tooltip_panel->add_theme_color_override(SceneStringName(font_color), Color(0.8f, 0.8f, 0.8f, 1)); - tooltip_panel->add_theme_constant_override("paragraph_separation", 5); - - frame_time_gradient = memnew(Gradient); - // The color is set when the theme changes. - frame_time_gradient->add_point(0.5, Color()); - - top_right_vbox = memnew(VBoxContainer); - top_right_vbox->add_theme_constant_override("separation", 10.0 * EDSCALE); - top_right_vbox->set_anchors_and_offsets_preset(PRESET_TOP_RIGHT, PRESET_MODE_MINSIZE, 10.0 * EDSCALE); - top_right_vbox->set_h_grow_direction(GROW_DIRECTION_BEGIN); - - const int navigation_control_size = 150; - - position_control = memnew(ViewportNavigationControl); - position_control->set_navigation_mode(View3DController::NAV_MODE_MOVE); - position_control->set_custom_minimum_size(Size2(navigation_control_size, navigation_control_size) * EDSCALE); - position_control->set_h_size_flags(SIZE_SHRINK_END); - position_control->set_anchor_and_offset(SIDE_LEFT, ANCHOR_BEGIN, 0); - position_control->set_anchor_and_offset(SIDE_TOP, ANCHOR_END, -navigation_control_size * EDSCALE); - position_control->set_anchor_and_offset(SIDE_RIGHT, ANCHOR_BEGIN, navigation_control_size * EDSCALE); - position_control->set_anchor_and_offset(SIDE_BOTTOM, ANCHOR_END, 0); - position_control->set_viewport(this); - surface->add_child(position_control); - - look_control = memnew(ViewportNavigationControl); - look_control->set_navigation_mode(View3DController::NAV_MODE_LOOK); - look_control->set_custom_minimum_size(Size2(navigation_control_size, navigation_control_size) * EDSCALE); - look_control->set_h_size_flags(SIZE_SHRINK_END); - look_control->set_anchor_and_offset(SIDE_LEFT, ANCHOR_END, -navigation_control_size * EDSCALE); - look_control->set_anchor_and_offset(SIDE_TOP, ANCHOR_END, -navigation_control_size * EDSCALE); - look_control->set_anchor_and_offset(SIDE_RIGHT, ANCHOR_END, 0); - look_control->set_anchor_and_offset(SIDE_BOTTOM, ANCHOR_END, 0); - look_control->set_viewport(this); - surface->add_child(look_control); - - rotation_control = memnew(ViewportRotationControl); - rotation_control->set_custom_minimum_size(Size2(80, 80) * EDSCALE); - rotation_control->set_h_size_flags(SIZE_SHRINK_END); - rotation_control->set_viewport(this); - rotation_control->set_focus_mode(FOCUS_CLICK); - top_right_vbox->add_child(rotation_control); - - frame_time_panel = memnew(PanelContainer); - frame_time_panel->set_mouse_filter(MOUSE_FILTER_IGNORE); - top_right_vbox->add_child(frame_time_panel); - frame_time_panel->hide(); - - frame_time_vbox = memnew(VBoxContainer); - frame_time_panel->add_child(frame_time_vbox); - - // Individual Labels are used to allow coloring each label with its own color. - cpu_time_label = memnew(Label); - frame_time_vbox->add_child(cpu_time_label); - - gpu_time_label = memnew(Label); - frame_time_vbox->add_child(gpu_time_label); - - fps_label = memnew(Label); - frame_time_vbox->add_child(fps_label); - - surface->add_child(top_right_vbox); - - accept = nullptr; - - selection_menu = memnew(PopupMenu); - add_child(selection_menu); - selection_menu->set_min_size(Size2(100, 0) * EDSCALE); - selection_menu->connect(SceneStringName(id_pressed), callable_mp(this, &Node3DEditorViewport::_selection_result_pressed)); - selection_menu->connect("popup_hide", callable_mp(this, &Node3DEditorViewport::_selection_menu_hide)); - - if (p_index == 0) { - view_display_menu->get_popup()->set_item_checked(view_display_menu->get_popup()->get_item_index(VIEW_AUDIO_LISTENER), true); - viewport->set_as_audio_listener_3d(true); - } - - ruler = memnew(Node); - - ruler_start_point = memnew(Node3D); - ruler_start_point->set_visible(false); - - ruler_end_point = memnew(Node3D); - ruler_end_point->set_visible(false); - - ruler_material.instantiate(); - ruler_material->set_albedo(Color(1.0, 0.9, 0.0, 1.0)); - ruler_material->set_flag(BaseMaterial3D::FLAG_DISABLE_FOG, true); - ruler_material->set_shading_mode(BaseMaterial3D::SHADING_MODE_UNSHADED); - ruler_material->set_depth_draw_mode(BaseMaterial3D::DEPTH_DRAW_DISABLED); - - ruler_material_xray.instantiate(); - ruler_material_xray->set_albedo(Color(1.0, 0.9, 0.0, 0.15)); - ruler_material_xray->set_flag(BaseMaterial3D::FLAG_DISABLE_FOG, true); - ruler_material_xray->set_shading_mode(BaseMaterial3D::SHADING_MODE_UNSHADED); - ruler_material_xray->set_flag(BaseMaterial3D::FLAG_DISABLE_DEPTH_TEST, true); - ruler_material_xray->set_transparency(BaseMaterial3D::TRANSPARENCY_ALPHA); - ruler_material_xray->set_render_priority(BaseMaterial3D::RENDER_PRIORITY_MAX); - - geometry.instantiate(); - - geometry_xray.instantiate(); - - ruler_line = memnew(MeshInstance3D); - ruler_line->set_mesh(geometry); - ruler_line->set_material_override(ruler_material); - - ruler_line_xray = memnew(MeshInstance3D); - ruler_line_xray->set_mesh(geometry_xray); - ruler_line_xray->set_material_override(ruler_material_xray); - - ruler_triangle_material.instantiate(); - ruler_triangle_material->set_albedo(Color(1.0, 1.0, 1.0, 1.0)); - ruler_triangle_material->set_flag(BaseMaterial3D::FLAG_DISABLE_FOG, true); - ruler_triangle_material->set_shading_mode(BaseMaterial3D::SHADING_MODE_UNSHADED); - ruler_triangle_material->set_depth_draw_mode(BaseMaterial3D::DEPTH_DRAW_DISABLED); - ruler_triangle_material->set_flag(BaseMaterial3D::FLAG_ALBEDO_FROM_VERTEX_COLOR, true); - - ruler_triangle_material_xray.instantiate(); - ruler_triangle_material_xray->set_albedo(Color(1.0, 1.0, 1.0, 0.15)); - ruler_triangle_material_xray->set_flag(BaseMaterial3D::FLAG_DISABLE_FOG, true); - ruler_triangle_material_xray->set_shading_mode(BaseMaterial3D::SHADING_MODE_UNSHADED); - ruler_triangle_material_xray->set_flag(BaseMaterial3D::FLAG_DISABLE_DEPTH_TEST, true); - ruler_triangle_material_xray->set_transparency(BaseMaterial3D::TRANSPARENCY_ALPHA); - ruler_triangle_material_xray->set_render_priority(BaseMaterial3D::RENDER_PRIORITY_MAX); - ruler_triangle_material_xray->set_flag(BaseMaterial3D::FLAG_ALBEDO_FROM_VERTEX_COLOR, true); - - ruler_triangle_lines = memnew(MeshInstance3D); - Ref triangle_mesh; - triangle_mesh.instantiate(); - ruler_triangle_lines->set_mesh(triangle_mesh); - ruler_triangle_lines->set_material_override(ruler_triangle_material); - - ruler_triangle_lines_xray = memnew(MeshInstance3D); - Ref triangle_mesh_xray; - triangle_mesh_xray.instantiate(); - ruler_triangle_lines_xray->set_mesh(triangle_mesh_xray); - ruler_triangle_lines_xray->set_material_override(ruler_triangle_material_xray); - - ruler_label = memnew(Label); - ruler_label->set_visible(false); - - ruler_label_x = memnew(Label); - ruler_label_x->set_visible(false); - - ruler_label_y = memnew(Label); - ruler_label_y->set_visible(false); - - ruler_label_z = memnew(Label); - ruler_label_z->set_visible(false); - - ruler->add_child(ruler_start_point); - ruler->add_child(ruler_end_point); - ruler->add_child(ruler_line); - ruler->add_child(ruler_line_xray); - ruler->add_child(ruler_triangle_lines); - ruler->add_child(ruler_triangle_lines_xray); - - viewport->add_child(ruler_label); - viewport->add_child(ruler_label_x); - viewport->add_child(ruler_label_y); - viewport->add_child(ruler_label_z); - - view_3d_controller.instantiate(); - view_3d_controller->connect("view_state_changed", callable_mp(this, &Node3DEditorViewport::_view_state_changed)); - view_3d_controller->connect("fov_scaled", callable_mp((CanvasItem *)surface, &CanvasItem::queue_redraw)); - view_3d_controller->connect("freelook_changed", callable_mp(this, &Node3DEditorViewport::_freelook_changed)); - view_3d_controller->connect("freelook_speed_scaled", callable_mp(this, &Node3DEditorViewport::_freelook_speed_scaled)); - view_3d_controller->connect("cursor_panned", callable_mp(this, &Node3DEditorViewport::_disable_follow_mode)); - view_3d_controller->connect("cursor_interpolated", callable_mp(this, &Node3DEditorViewport::_cursor_interpolated)); - view_3d_controller->connect("cursor_distance_scaled", callable_mp(this, &Node3DEditorViewport::_cursor_distance_scaled)); - _update_view_3d_controller(true); - - _update_name(); - - EditorNode::get_singleton()->register_hdr_viewport(viewport); -} - -Node3DEditorViewport::~Node3DEditorViewport() { - memdelete(ruler); -} - -////////////////////////////////////////////////////////////// - -void Node3DEditorViewportContainer::_update_split_drag_margin() { - if (view != VIEW_USE_4_VIEWPORTS && view != VIEW_USE_3_VIEWPORTS) { - return; - } - // Also for 3 viewports view since the first split container is used to remember the offset. - first_split->set_split_offset(second_split->get_split_offset()); - - if (view == VIEW_USE_4_VIEWPORTS) { - // Extend to cover the first split on top. - second_split->set_drag_area_margin_begin(second_split->get_size().y - get_size().y); - } -} - -void Node3DEditorViewportContainer::_notification(int p_what) { - switch (p_what) { - case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: { - if (!EditorSettings::get_singleton()->check_changed_settings_in_group("interface/touchscreen")) { - return; - } - [[fallthrough]]; - } - case NOTIFICATION_READY: { - bool touch_optimizations = EDITOR_GET("interface/touchscreen/enable_touch_optimizations"); - main_split->set_touch_dragger_enabled(touch_optimizations); - first_split->set_touch_dragger_enabled(touch_optimizations); - second_split->set_touch_dragger_enabled(touch_optimizations); - } break; - } -} - -void Node3DEditorViewportContainer::set_view(View p_view) { - view = p_view; - - Node3DEditorViewport *viewports[4]; - for (uint32_t i = 0; i < 4; i++) { - viewports[i] = Node3DEditor::get_singleton()->get_editor_viewport(i); - ERR_FAIL_NULL(viewports[i]); - } - - const bool previous_main_vertical = !first_split->is_vertical(); - const float horizontal_offset = previous_main_vertical ? first_split->get_split_offset() : main_split->get_split_offset(); - const float vertical_offset = previous_main_vertical ? main_split->get_split_offset() : first_split->get_split_offset(); - - first_split->set_dragging_enabled(true); - second_split->set_drag_area_margin_begin(0); - viewports[0]->show(); - - switch (view) { - case VIEW_USE_1_VIEWPORT: { - for (int i = 1; i < 4; i++) { - viewports[i]->hide(); - } - second_split->hide(); - } break; - case VIEW_USE_2_VIEWPORTS: - case VIEW_USE_2_VIEWPORTS_ALT: { - viewports[1]->show(); - viewports[2]->hide(); - viewports[3]->hide(); - second_split->hide(); - const bool is_vertical = view == VIEW_USE_2_VIEWPORTS; - if (first_split->is_vertical() != is_vertical) { - first_split->set_vertical(is_vertical); - first_split->set_split_offset(is_vertical ? vertical_offset : horizontal_offset); - main_split->set_split_offset(is_vertical ? horizontal_offset : vertical_offset); // Store the other offset here for later. - } - } break; - case VIEW_USE_3_VIEWPORTS: - case VIEW_USE_3_VIEWPORTS_ALT: { - // Default mode has two on bottom (second_split). Alt mode has two on the left (first_split). - const bool main_vertical = view == VIEW_USE_3_VIEWPORTS; - viewports[1]->set_visible(!main_vertical); - viewports[2]->show(); - viewports[3]->set_visible(main_vertical); - second_split->show(); - main_split->set_vertical(main_vertical); - main_split->set_split_offset(main_vertical ? vertical_offset : horizontal_offset); - first_split->set_vertical(!main_vertical); - first_split->set_split_offset(main_vertical ? horizontal_offset : vertical_offset); - second_split->set_split_offset(main_vertical ? horizontal_offset : vertical_offset); - } break; - case VIEW_USE_4_VIEWPORTS: { - for (int i = 1; i < 4; i++) { - viewports[i]->show(); - } - second_split->show(); - main_split->set_vertical(true); - main_split->set_split_offset(vertical_offset); - first_split->set_vertical(false); - first_split->set_split_offset(horizontal_offset); - second_split->set_split_offset(horizontal_offset); - - first_split->set_dragging_enabled(false); - _update_split_drag_margin(); - } break; - } -} - -Node3DEditorViewportContainer::View Node3DEditorViewportContainer::get_view() { - return view; -} - -void Node3DEditorViewportContainer::add_viewport(Node3DEditorViewport *p_viewport, int p_index) { - if (p_index <= 1) { - first_split->add_child(p_viewport); - } else { - second_split->add_child(p_viewport); - if (p_index == 3) { - // Connect to the second split's child to update the drag margin immediately whenever the split offset changes. - p_viewport->connect(SceneStringName(resized), callable_mp(this, &Node3DEditorViewportContainer::_update_split_drag_margin)); - } - } -} - -Dictionary Node3DEditorViewportContainer::get_split_state() const { - Dictionary state; - state["main"] = Math::round(main_split->get_split_offset() / EDSCALE); - state["first"] = Math::round(first_split->get_split_offset() / EDSCALE); - state["second"] = Math::round(second_split->get_split_offset() / EDSCALE); - return state; -} - -void Node3DEditorViewportContainer::set_split_state(const Dictionary &p_state) { - if (p_state.has("main")) { - main_split->set_split_offset(int(p_state["main"]) * EDSCALE); - } - if (p_state.has("first")) { - first_split->set_split_offset(int(p_state["first"]) * EDSCALE); - } - if (p_state.has("second")) { - second_split->set_split_offset(int(p_state["second"]) * EDSCALE); - } -} - -Node3DEditorViewportContainer::Node3DEditorViewportContainer() { - set_clip_contents(true); - - main_split = memnew(SplitContainer); - main_split->set_drag_nested_intersections(true); - - first_split = memnew(SplitContainer); - first_split->set_h_size_flags(SIZE_EXPAND_FILL); - first_split->set_v_size_flags(SIZE_EXPAND_FILL); - first_split->set_drag_nested_intersections(true); - - second_split = memnew(SplitContainer); - second_split->set_h_size_flags(SIZE_EXPAND_FILL); - second_split->set_v_size_flags(SIZE_EXPAND_FILL); - second_split->set_drag_nested_intersections(true); - - main_split->add_child(first_split); - main_split->add_child(second_split); - add_child(main_split); -} +using namespace Node3DEditorConstants; /////////////////////////////////////////////////////////////////// Node3DEditor *Node3DEditor::singleton = nullptr; -Node3DEditorSelectedItem::~Node3DEditorSelectedItem() { - ERR_FAIL_NULL(RenderingServer::get_singleton()); - if (sbox_instance.is_valid()) { - RenderingServer::get_singleton()->free_rid(sbox_instance); - } - if (sbox_instance_offset.is_valid()) { - RenderingServer::get_singleton()->free_rid(sbox_instance_offset); - } - if (sbox_instance_xray.is_valid()) { - RenderingServer::get_singleton()->free_rid(sbox_instance_xray); - } - if (sbox_instance_xray_offset.is_valid()) { - RenderingServer::get_singleton()->free_rid(sbox_instance_xray_offset); - } -} - void Node3DEditor::select_gizmo_highlight_axis(int p_axis) { for (int i = 0; i < 3; i++) { move_gizmo[i]->surface_set_material(0, i == p_axis ? gizmo_color_hl[i] : gizmo_color[i]); @@ -7581,6 +303,7 @@ void Node3DEditor::_generate_selection_boxes() { const Color selection_box_color = EDITOR_GET("editors/3d/selection_box_color"); const Color active_selection_box_color = EDITOR_GET("editors/3d/active_selection_box_color"); + selection_box_mat.instantiate(); selection_box_mat->set_shading_mode(StandardMaterial3D::SHADING_MODE_UNSHADED); selection_box_mat->set_flag(StandardMaterial3D::FLAG_DISABLE_FOG, true); selection_box_mat->set_albedo(selection_box_color); @@ -7588,6 +311,7 @@ void Node3DEditor::_generate_selection_boxes() { st->set_material(selection_box_mat); selection_box = st->commit(); + selection_box_mat_xray.instantiate(); selection_box_mat_xray->set_shading_mode(StandardMaterial3D::SHADING_MODE_UNSHADED); selection_box_mat_xray->set_flag(StandardMaterial3D::FLAG_DISABLE_FOG, true); selection_box_mat_xray->set_flag(StandardMaterial3D::FLAG_DISABLE_DEPTH_TEST, true); @@ -7596,6 +320,7 @@ void Node3DEditor::_generate_selection_boxes() { st_xray->set_material(selection_box_mat_xray); selection_box_xray = st_xray->commit(); + active_selection_box_mat.instantiate(); active_selection_box_mat->set_shading_mode(StandardMaterial3D::SHADING_MODE_UNSHADED); active_selection_box_mat->set_flag(StandardMaterial3D::FLAG_DISABLE_FOG, true); active_selection_box_mat->set_albedo(active_selection_box_color); @@ -7603,6 +328,7 @@ void Node3DEditor::_generate_selection_boxes() { active_st->set_material(active_selection_box_mat); active_selection_box = active_st->commit(); + active_selection_box_mat_xray.instantiate(); active_selection_box_mat_xray->set_shading_mode(StandardMaterial3D::SHADING_MODE_UNSHADED); active_selection_box_mat_xray->set_flag(StandardMaterial3D::FLAG_DISABLE_FOG, true); active_selection_box_mat_xray->set_flag(StandardMaterial3D::FLAG_DISABLE_DEPTH_TEST, true); @@ -9769,9 +2495,6 @@ void Node3DEditor::_notification(int p_what) { update_all_gizmos(); } _update_vertex_snap_tooltips(); - if (EditorSettings::get_singleton()->check_changed_settings_in_group("interface/inspector")) { - snap_translate->set_step(EDITOR_GET("interface/inspector/default_float_step")); - } } break; case NOTIFICATION_PHYSICS_PROCESS: { @@ -9881,6 +2604,14 @@ Node3DEditorViewport *Node3DEditor::get_last_used_viewport() { return viewports[last_used_viewport]; } +void Node3DEditor::set_freelook_viewport(Node3DEditorViewport *p_viewport) { + freelook_viewport = p_viewport; +} + +Node3DEditorViewport *Node3DEditor::get_freelook_viewport() const { + return freelook_viewport; +} + void Node3DEditor::add_control_to_left_panel(Control *p_control) { left_panel_split->add_child(p_control); left_panel_split->move_child(p_control, 0); @@ -10846,7 +3577,7 @@ Node3DEditor::Node3DEditor() { snap_translate = memnew(EditorSpinSlider); snap_translate->set_min(0.0); - snap_translate->set_step(EDITOR_GET("interface/inspector/default_float_step")); + snap_translate->set_step(0.001); snap_translate->set_max(10.0); snap_translate->set_suffix("m"); snap_translate->set_allow_greater(true); @@ -11187,6 +3918,15 @@ Node3DEditor::~Node3DEditor() { } } +void Node3DEditorPlugin::edited_scene_changed() { + for (uint32_t i = 0; i < Node3DEditor::VIEWPORTS_COUNT; i++) { + Node3DEditorViewport *viewport = Node3DEditor::get_singleton()->get_editor_viewport(i); + if (viewport->is_visible()) { + viewport->notification(Control::NOTIFICATION_VISIBILITY_CHANGED); + } + } +} + void Node3DEditorPlugin::make_visible(bool p_visible) { if (p_visible) { spatial_editor->show(); @@ -11242,6 +3982,18 @@ Vector3 Node3DEditor::snap_point(Vector3 p_target, Vector3 p_start) const { return p_target; } +float Node3DEditor::get_znear() const { + return settings_znear->get_value(); +} + +float Node3DEditor::get_zfar() const { + return settings_zfar->get_value(); +} + +float Node3DEditor::get_fov() const { + return settings_fov->get_value(); +} + bool Node3DEditor::is_gizmo_visible() const { if (selected) { return gizmo.visible && selected->is_transform_gizmo_visible(); @@ -11249,6 +4001,18 @@ bool Node3DEditor::is_gizmo_visible() const { return gizmo.visible; } +bool Node3DEditor::are_local_coords_enabled() const { + return tool_option_button[Node3DEditor::TOOL_OPT_LOCAL_COORDS]->is_pressed(); +} + +void Node3DEditor::set_local_coords_enabled(bool on) const { + tool_option_button[Node3DEditor::TOOL_OPT_LOCAL_COORDS]->set_pressed(on); +} + +bool Node3DEditor::is_preserve_children_transform_enabled() const { + return tool_option_button[Node3DEditor::TOOL_OPT_PRESERVE_CHILDREN_TRANSFORM]->is_pressed(); +} + bool Node3DEditor::is_vertex_snap_use_collision() const { return vertex_snap_use_collision != Input::get_singleton()->is_key_pressed(Key::SHIFT); } diff --git a/editor/scene/3d/node_3d_editor_plugin.h b/editor/scene/3d/node_3d_editor_plugin.h index d1071568ac..cee1c161d6 100644 --- a/editor/scene/3d/node_3d_editor_plugin.h +++ b/editor/scene/3d/node_3d_editor_plugin.h @@ -33,574 +33,29 @@ #include "core/math/dynamic_bvh.h" #include "editor/plugins/editor_plugin.h" #include "editor/scene/3d/node_3d_editor_gizmos.h" -#include "editor/themes/editor_scale.h" #include "scene/debugger/view_3d_controller.h" #include "scene/gui/box_container.h" -#include "scene/gui/button.h" -#include "scene/gui/margin_container.h" -#include "scene/gui/spin_box.h" -#include "scene/resources/gradient.h" -#include "scene/resources/immediate_mesh.h" +#include "scene/gui/popup.h" class AcceptDialog; -class CheckBox; +class Button; class ColorPickerButton; class ConfirmationDialog; class DirectionalLight3D; -class EditorData; class EditorSelection; class EditorSpinSlider; class HSplitContainer; class LineEdit; class MenuButton; -class Node3DEditor; class Node3DEditorViewport; +class Node3DEditorViewportContainer; class OptionButton; class PanelContainer; class ProceduralSkyMaterial; -class RichTextLabel; -class SplitContainer; -class SubViewport; -class SubViewportContainer; +class SpinBox; class VSeparator; class VSplitContainer; -class ViewportNavigationControl; class WorldEnvironment; -class MeshInstance3D; - -class ViewportRotationControl : public Control { - GDCLASS(ViewportRotationControl, Control); - - struct Axis2D { - Vector2 screen_point; - float z_axis = -99.0; - int axis = -1; - bool is_positive = true; - }; - - struct Axis2DCompare { - _FORCE_INLINE_ bool operator()(const Axis2D &l, const Axis2D &r) const { - return l.z_axis < r.z_axis; - } - }; - - Node3DEditorViewport *viewport = nullptr; - Vector axis_colors; - Vector axis_menu_options; - Vector2i orbiting_mouse_start; - Point2 original_mouse_pos; - View3DController::Cursor saved_cursor; - int orbiting_index = -1; - int focused_axis = -2; - bool gizmo_activated = false; - - const float AXIS_CIRCLE_RADIUS = 8.0f * EDSCALE; - -protected: - void _notification(int p_what); - virtual void gui_input(const Ref &p_event) override; - void _draw(); - void _draw_axis(const Axis2D &p_axis); - void _get_sorted_axis(Vector &r_axis); - void _update_focus(); - void _process_click(int p_index, Vector2 p_position, bool p_pressed); - void _process_drag(Ref p_event, int p_index, Vector2 p_position, Vector2 p_relative_position); - -public: - void set_viewport(Node3DEditorViewport *p_viewport); -}; - -class Node3DEditorViewport : public Control { - GDCLASS(Node3DEditorViewport, Control); - friend class Node3DEditor; - friend class ViewportNavigationControl; - friend class ViewportRotationControl; - enum { - VIEW_TOP, - VIEW_BOTTOM, - VIEW_LEFT, - VIEW_RIGHT, - VIEW_FRONT, - VIEW_REAR, - VIEW_CENTER_TO_ORIGIN, - VIEW_CENTER_TO_SELECTION, - VIEW_ALIGN_TRANSFORM_WITH_VIEW, - VIEW_ALIGN_ROTATION_WITH_VIEW, - VIEW_PERSPECTIVE, - VIEW_ENVIRONMENT, - VIEW_ORTHOGONAL, - VIEW_SWITCH_PERSPECTIVE_ORTHOGONAL, - VIEW_HALF_RESOLUTION, - VIEW_AUDIO_LISTENER, - VIEW_AUDIO_DOPPLER, - VIEW_GIZMOS, - VIEW_TRANSFORM_GIZMO, - VIEW_GRID, - VIEW_INFORMATION, - VIEW_FRAME_TIME, - - // < Keep in sync with menu. - VIEW_DISPLAY_NORMAL, - VIEW_DISPLAY_WIREFRAME, - VIEW_DISPLAY_OVERDRAW, - VIEW_DISPLAY_LIGHTING, - VIEW_DISPLAY_UNSHADED, - VIEW_DISPLAY_ADVANCED, - // Advanced menu: - VIEW_DISPLAY_DEBUG_PSSM_SPLITS, - VIEW_DISPLAY_NORMAL_BUFFER, - VIEW_DISPLAY_DEBUG_SHADOW_ATLAS, - VIEW_DISPLAY_DEBUG_DIRECTIONAL_SHADOW_ATLAS, - VIEW_DISPLAY_DEBUG_DECAL_ATLAS, - VIEW_DISPLAY_DEBUG_AREA_LIGHT_ATLAS, - VIEW_DISPLAY_DEBUG_VOXEL_GI_ALBEDO, - VIEW_DISPLAY_DEBUG_VOXEL_GI_LIGHTING, - VIEW_DISPLAY_DEBUG_VOXEL_GI_EMISSION, - VIEW_DISPLAY_DEBUG_SDFGI, - VIEW_DISPLAY_DEBUG_SDFGI_PROBES, - VIEW_DISPLAY_DEBUG_SCENE_LUMINANCE, - VIEW_DISPLAY_DEBUG_SSAO, - VIEW_DISPLAY_DEBUG_SSIL, - VIEW_DISPLAY_DEBUG_GI_BUFFER, - VIEW_DISPLAY_DEBUG_DISABLE_LOD, - VIEW_DISPLAY_DEBUG_CLUSTER_OMNI_LIGHTS, - VIEW_DISPLAY_DEBUG_CLUSTER_SPOT_LIGHTS, - VIEW_DISPLAY_DEBUG_CLUSTER_AREA_LIGHTS, - VIEW_DISPLAY_DEBUG_CLUSTER_DECALS, - VIEW_DISPLAY_DEBUG_CLUSTER_REFLECTION_PROBES, - VIEW_DISPLAY_DEBUG_OCCLUDERS, - VIEW_DISPLAY_MOTION_VECTORS, - VIEW_DISPLAY_INTERNAL_BUFFER, - VIEW_DISPLAY_MAX, - // > Keep in sync with menu. - - VIEW_LOCK_ROTATION, - VIEW_CINEMATIC_PREVIEW, - VIEW_AUTO_ORTHOGONAL, - VIEW_MAX - }; - -public: - static constexpr int32_t GIZMO_BASE_LAYER = 27; - static constexpr int32_t GIZMO_EDIT_LAYER = 26; - static constexpr int32_t GIZMO_GRID_LAYER = 25; - static constexpr int32_t MISC_TOOL_LAYER = 24; - - static constexpr int32_t FRAME_TIME_HISTORY = 20; - -private: - double cpu_time_history[FRAME_TIME_HISTORY]; - int cpu_time_history_index; - double gpu_time_history[FRAME_TIME_HISTORY]; - int gpu_time_history_index; - - Node *ruler = nullptr; - Node3D *ruler_start_point = nullptr; - Node3D *ruler_end_point = nullptr; - Ref geometry; - Ref geometry_xray; - MeshInstance3D *ruler_line = nullptr; - MeshInstance3D *ruler_line_xray = nullptr; - Label *ruler_label = nullptr; - Ref ruler_material; - Ref ruler_material_xray; - Ref ruler_triangle_material; - Ref ruler_triangle_material_xray; - MeshInstance3D *ruler_triangle_lines = nullptr; - MeshInstance3D *ruler_triangle_lines_xray = nullptr; - Label *ruler_label_x = nullptr; - Label *ruler_label_y = nullptr; - Label *ruler_label_z = nullptr; - - int index; - void _menu_option(int p_option); - Node3D *preview_node = nullptr; - bool update_preview_node = false; - Point2 preview_node_viewport_pos; - Vector3 preview_node_pos; - AABB *preview_bounds = nullptr; - Vector selected_files; - AcceptDialog *accept = nullptr; - - Node *target_node = nullptr; - Point2 drop_pos; - - ObjectID focused_node_id; - - EditorSelection *editor_selection = nullptr; - - Button *translation_preview_button = nullptr; - Button *follow_mode = nullptr; - CheckBox *preview_camera = nullptr; - CheckBox *pilot_camera = nullptr; - SubViewportContainer *subviewport_container = nullptr; - - MenuButton *view_display_menu = nullptr; - PopupMenu *display_submenu = nullptr; - - Control *surface = nullptr; - SubViewport *viewport = nullptr; - Camera3D *camera = nullptr; - bool transforming = false; - bool transform_gizmo_visible = true; - bool collision_reposition = false; - real_t gizmo_scale; - - bool vertex_snap_mode = false; - Key vertex_snap_keycode = Key::NONE; - bool vertex_snap_dragging = false; - Vector3 vertex_snap_source; - Plane vertex_snap_drag_plane; - Vector3 vertex_snap_target; - bool vertex_snap_has_target = false; - bool vertex_snap_has_source = false; - HashMap vertex_snap_original_positions; - - PanelContainer *info_panel = nullptr; - Label *info_label = nullptr; - Label *cinema_label = nullptr; - Label *locked_label = nullptr; - Label *zoom_limit_label = nullptr; - - RichTextLabel *tooltip_panel = nullptr; - - VBoxContainer *top_right_vbox = nullptr; - VBoxContainer *bottom_center_vbox = nullptr; - ViewportNavigationControl *position_control = nullptr; - ViewportNavigationControl *look_control = nullptr; - ViewportRotationControl *rotation_control = nullptr; - Ref frame_time_gradient; - PanelContainer *frame_time_panel = nullptr; - VBoxContainer *frame_time_vbox = nullptr; - Label *cpu_time_label = nullptr; - Label *gpu_time_label = nullptr; - Label *fps_label = nullptr; - - struct _RayResult { - Node3D *item = nullptr; - real_t depth = 0; - _FORCE_INLINE_ bool operator<(const _RayResult &p_rr) const { return depth < p_rr.depth; } - }; - - void _view_state_changed(); - - 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); - - float _min_screen_dist_to_aabb(const AABB &p_aabb, const Transform3D &p_transform, const Point2 &p_cursor) const; - bool _find_closest_vertex_on_node(const Point2 &p_screen_pos, Node3D *p_node, float &r_closest_screen_dist, Vector3 &r_vertex_world) const; - bool _find_closest_vertex_in_scene(const Point2 &p_screen_pos, float p_threshold, Vector3 &r_vertex_world, const HashMap *p_exclude = nullptr); - void _vertex_snap_update_source(const Point2 &p_screen_pos); - void _vertex_snap_commit(); - void _vertex_snap_cancel(); - bool _is_vertex_occluded(const Vector3 &p_world_pos, const Vector2 &p_screen_pos) const; - - Transform3D _get_camera_transform() const; - int get_selected_count() const; - bool _has_unlocked_selection() const; - void cancel_transform(); - void _update_shrink(); - - Vector3 _get_camera_position() const; - Vector3 _get_camera_normal() const; - Vector3 _get_screen_to_space(const Vector3 &p_vector3); - Vector _build_screen_frustum(const Point2 &p_min, const Point2 &p_max); - - void _select_region(); - bool _transform_gizmo_select(const Vector2 &p_screenpos, bool p_highlight_only = false); - void _transform_gizmo_apply(Node3D *p_node, const Transform3D &p_transform, bool p_local); - - bool _is_shortcut_empty(const String &p_name); - bool _is_nav_modifier_pressed(const String &p_name); - - float get_znear() const; - float get_zfar() const; - float get_fov() const; - - void _show_tooltip(const String &p_title, const String &p_description) const; - - ObjectID clicked; - ObjectID material_target; - Vector selection_results; - Vector selection_results_menu; - bool clicked_wants_append = false; - bool selection_in_progress = false; - bool movement_threshold_passed = false; - - PopupMenu *selection_menu = nullptr; - - enum TransformMode { - TRANSFORM_NONE, - TRANSFORM_ROTATE, - TRANSFORM_TRANSLATE, - TRANSFORM_SCALE - }; - enum TransformPlane { - TRANSFORM_VIEW, - TRANSFORM_X_AXIS, - TRANSFORM_Y_AXIS, - TRANSFORM_Z_AXIS, - TRANSFORM_YZ, - TRANSFORM_XZ, - TRANSFORM_XY, - }; - enum TransformType { - POSITION, - ROTATION, - SCALE, - }; - - struct EditData { - TransformMode mode; - TransformPlane plane; - Transform3D original; - Vector3 click_ray; - Vector3 click_ray_pos; - Vector3 center; - Point2 mouse_pos; - Point2 original_mouse_pos; - bool show_rotation_line = false; - bool is_trackball = false; - Ref gizmo; - int gizmo_handle = 0; - bool gizmo_handle_secondary = false; - Variant gizmo_initial_value; - bool original_local; - bool instant; - - // Numeric blender-style transforms (e.g. 'g5x'). - // numeric_input tracks the current input value, e.g. 1.23. - // numeric_negate indicates whether '-' has been pressed to negate the value - // while numeric_next_decimal is 0, numbers are input before the decimal point - // after pressing '.', numeric next decimal changes to -1, and decrements after each press. - double numeric_input = 0.0; - bool numeric_negate = false; - int numeric_next_decimal = 0; - - Vector3 rotation_axis; - Vector3 view_axis_local; - double accumulated_rotation_angle = 0.0; - double rotation_angle = 0.0; - Vector3 initial_click_vector; - Vector3 previous_rotation_vector; - bool gizmo_initiated = false; - - HashMap children_original_globals; - } _edit; - - Ref view_3d_controller; - void _update_view_3d_controller(bool p_update_all = true); - - void _cursor_interpolated(); - void _cursor_distance_scaled(); - - void _freelook_changed(); - void _freelook_speed_scaled(); - - real_t zoom_indicator_delay; - int zoom_failed_attempts_count = 0; - - RID move_gizmo_instance[3], move_plane_gizmo_instance[3], rotate_gizmo_instance[4], scale_gizmo_instance[3], scale_plane_gizmo_instance[3], axis_gizmo_instance[3]; - RID trackball_sphere_instance; - - String last_message; - String message; - double message_time; - - void set_message(const String &p_message, float p_time = 5); - - void _view_settings_confirmed(real_t p_interp_delta); - void _update_navigation_controls_visibility(); - void _draw(); - - // These allow tool scripts to set the 3D cursor location by updating the camera transform. - Transform3D last_camera_transform; - bool _camera_moved_externally(); - void _apply_camera_transform_to_cursor(); - - void _surface_mouse_enter(); - void _surface_mouse_exit(); - void _surface_focus_enter(); - void _surface_focus_exit(); - - void input(const Ref &p_event) override; - void _sinput(const Ref &p_event); - Node3DEditor *spatial_editor = nullptr; - - Camera3D *previewing = nullptr; - Camera3D *preview = nullptr; - - bool previewing_camera = false; - bool previewing_cinema = false; - int times_focused_consecutively = 0; - bool pilot_preview_enabled = false; - - bool pilot_undo_session_active = false; - real_t pilot_undo_idle_time = 0.0; - Transform3D pilot_undo_initial_transform; - void _pilot_ensure_undo_session(); - void _pilot_commit_undo_session(); - void _pilot_tick_undo_session(real_t p_delta); - - bool _is_node_locked(const Node *p_node) const; - void _preview_exited_scene(); - void _preview_camera_property_changed(); - void _sync_cursor_from_transform(const Transform3D &p_transform); - void _update_centered_labels(); - void _disable_follow_mode(); - void _reset_follow_mode_count(); - void _toggle_camera_preview(bool); - void _toggle_pilot_preview(bool); - void _toggle_cinema_preview(bool); - void _init_gizmo_instance(int p_idx); - void _finish_gizmo_instances(); - void _selection_result_pressed(int); - void _selection_menu_hide(); - void _list_select(Ref b); - - Vector3 _get_instance_position(const Point2 &p_pos, Node3D *p_node) const; - static AABB _calculate_spatial_bounds(const Node3D *p_parent, bool p_omit_top_level = false, const Transform3D *p_bounds_orientation = nullptr); - - Node *_sanitize_preview_node(Node *p_node) const; - - void _create_preview_node(const Vector &files) const; - void _remove_preview_node(); - bool _apply_preview_material(ObjectID p_target, const Point2 &p_point) const; - void _reset_preview_material() const; - void _remove_preview_material(); - bool _cyclical_dependency_exists(const String &p_target_scene_path, Node *p_desired_node) const; - bool _create_instance(Node *p_parent, const String &p_path, const Point2 &p_point); - bool _create_audio_node(Node *p_parent, const String &p_path, const Point2 &p_point); - void _perform_drop_data(); - - bool can_drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from); - void drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from); - - void _project_settings_changed(); - - Transform3D _compute_transform(TransformMode p_mode, const Transform3D &p_original, const Transform3D &p_original_local, Vector3 p_motion, double p_extra, bool p_local, bool p_orthogonal, bool p_view_axis); - - void _reset_transform(TransformType p_type); - - void begin_transform(TransformMode p_mode, bool instant); - void commit_transform(); - void apply_transform(Vector3 p_motion, double p_snap); - void update_transform(bool p_shift); - void update_transform_numeric(); - void finish_transform(); - - void _load_viewport_inputs(); - void register_shortcut_action(const String &p_path, const String &p_name, Key p_keycode, bool p_physical = false); - void shortcut_changed_callback(const Ref p_shortcut, const String &p_shortcut_path); - - // Supported rendering methods for advanced debug draw mode items. - enum SupportedRenderingMethods { - ALL, - FORWARD_PLUS, - FORWARD_PLUS_MOBILE, - }; - - void _set_lock_view_rotation(bool p_lock_rotation); - void _add_advanced_debug_draw_mode_item(PopupMenu *p_popup, const String &p_name, int p_value, SupportedRenderingMethods p_rendering_methods = SupportedRenderingMethods::ALL, const String &p_tooltip = ""); - -protected: - void _notification(int p_what); - static void _bind_methods(); - -public: - void update_surface() { surface->queue_redraw(); } - void update_transform_gizmo_view(); - void update_transform_gizmo_highlight(); - - void set_can_preview(Camera3D *p_preview); - void switch_preview_camera(Camera3D *p_new_camera); - void set_state(const Dictionary &p_state); - Dictionary get_state() const; - void reset(); - - Vector3 get_ray_pos(const Vector2 &p_pos) const; - Vector3 get_ray(const Vector2 &p_pos) const; - Point2 point_to_screen(const Vector3 &p_point); - - void focus_selection(); - - void assign_pending_data_pointers( - Node3D *p_preview_node, - AABB *p_preview_bounds, - AcceptDialog *p_accept); - - SubViewport *get_viewport_node() { return viewport; } - Camera3D *get_camera_3d() { return camera; } // return the default camera object. - Control *get_surface() { return surface; } - - Node3DEditorViewport(Node3DEditor *p_spatial_editor, int p_index); - ~Node3DEditorViewport(); -}; - -class Node3DEditorSelectedItem : public Object { - GDCLASS(Node3DEditorSelectedItem, Object); - -public: - AABB aabb; - Transform3D original; // original location when moving - Transform3D original_local; - Transform3D last_xform; // last transform - bool last_xform_dirty; - Node3D *sp = nullptr; - RID sbox_instance; - RID sbox_instance_offset; - RID sbox_instance_xray; - RID sbox_instance_xray_offset; - Ref gizmo; - HashMap subgizmos; // Key: Subgizmo ID, Value: Initial subgizmo transform. - - Node3DEditorSelectedItem() { - sp = nullptr; - last_xform_dirty = true; - } - ~Node3DEditorSelectedItem(); -}; - -class Node3DEditorViewportContainer : public MarginContainer { - GDCLASS(Node3DEditorViewportContainer, MarginContainer); - -public: - enum View { - VIEW_USE_1_VIEWPORT, - VIEW_USE_2_VIEWPORTS, - VIEW_USE_2_VIEWPORTS_ALT, - VIEW_USE_3_VIEWPORTS, - VIEW_USE_3_VIEWPORTS_ALT, - VIEW_USE_4_VIEWPORTS, - }; - -private: - View view = VIEW_USE_1_VIEWPORT; - SplitContainer *main_split = nullptr; - SplitContainer *first_split = nullptr; - SplitContainer *second_split = nullptr; - - void _update_split_drag_margin(); - -protected: - void _notification(int p_what); - -public: - void set_view(View p_view); - View get_view(); - - void add_viewport(Node3DEditorViewport *p_viewport, int p_index); - - Dictionary get_split_state() const; - void set_split_state(const Dictionary &p_state); - - Node3DEditorViewportContainer(); -}; class Node3DEditor : public VBoxContainer { GDCLASS(Node3DEditor, VBoxContainer); @@ -692,10 +147,10 @@ private: Ref selection_box_xray; Ref selection_box; - Ref selection_box_mat = memnew(StandardMaterial3D); - Ref selection_box_mat_xray = memnew(StandardMaterial3D); - Ref active_selection_box_mat = memnew(StandardMaterial3D); - Ref active_selection_box_mat_xray = memnew(StandardMaterial3D); + Ref selection_box_mat; + Ref selection_box_mat_xray; + Ref active_selection_box_mat; + Ref active_selection_box_mat_xray; RID indicators; RID indicators_instance; @@ -945,17 +400,17 @@ public: Vector3 snap_point(Vector3 p_target, Vector3 p_start = Vector3(0, 0, 0)) const; - float get_znear() const { return settings_znear->get_value(); } - float get_zfar() const { return settings_zfar->get_value(); } - float get_fov() const { return settings_fov->get_value(); } + float get_znear() const; + float get_zfar() const; + float get_fov() const; Transform3D get_gizmo_transform() const { return gizmo.transform; } bool is_gizmo_visible() const; ToolMode get_tool_mode() const { return tool_mode; } - bool are_local_coords_enabled() const { return tool_option_button[Node3DEditor::TOOL_OPT_LOCAL_COORDS]->is_pressed(); } - void set_local_coords_enabled(bool on) const { tool_option_button[Node3DEditor::TOOL_OPT_LOCAL_COORDS]->set_pressed(on); } - bool is_preserve_children_transform_enabled() const { return tool_option_button[Node3DEditor::TOOL_OPT_PRESERVE_CHILDREN_TRANSFORM]->is_pressed(); } + bool are_local_coords_enabled() const; + void set_local_coords_enabled(bool on) const; + bool is_preserve_children_transform_enabled() const; bool is_snap_enabled() const { return snap_enabled ^ snap_key_enabled; } bool is_vertex_snap_origin_mode() const { return vertex_snap_origin_mode; } bool is_vertex_snap_use_collision() const; @@ -1037,8 +492,8 @@ public: } Node3DEditorViewport *get_last_used_viewport(); - void set_freelook_viewport(Node3DEditorViewport *p_viewport) { freelook_viewport = p_viewport; } - Node3DEditorViewport *get_freelook_viewport() const { return freelook_viewport; } + void set_freelook_viewport(Node3DEditorViewport *p_viewport); + Node3DEditorViewport *get_freelook_viewport() const; void add_gizmo_plugin(Ref p_plugin); void remove_gizmo_plugin(Ref p_plugin); @@ -1077,28 +532,3 @@ public: Node3DEditorPlugin(); }; - -class ViewportNavigationControl : public Control { - GDCLASS(ViewportNavigationControl, Control); - - Node3DEditorViewport *viewport = nullptr; - Vector2i focused_mouse_start; - Vector2 focused_pos; - bool hovered = false; - int focused_index = -1; - View3DController::NavigationMode nav_mode = View3DController::NavigationMode::NAV_MODE_NONE; - - const float AXIS_CIRCLE_RADIUS = 30.0f * EDSCALE; - -protected: - void _notification(int p_what); - virtual void gui_input(const Ref &p_event) override; - void _draw(); - void _process_click(int p_index, Vector2 p_position, bool p_pressed); - void _process_drag(int p_index, Vector2 p_position, Vector2 p_relative_position); - void _update_navigation(); - -public: - void set_navigation_mode(View3DController::NavigationMode p_nav_mode); - void set_viewport(Node3DEditorViewport *p_viewport); -}; diff --git a/editor/scene/3d/node_3d_editor_viewport.cpp b/editor/scene/3d/node_3d_editor_viewport.cpp new file mode 100644 index 0000000000..9ab17877dd --- /dev/null +++ b/editor/scene/3d/node_3d_editor_viewport.cpp @@ -0,0 +1,7313 @@ +/**************************************************************************/ +/* node_3d_editor_viewport.cpp */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#include "node_3d_editor_viewport.h" + +#include "core/config/project_settings.h" +#include "core/input/input.h" +#include "core/input/input_map.h" +#include "core/io/resource_loader.h" +#include "core/math/geometry_3d.h" +#include "core/math/math_funcs.h" +#include "core/math/projection.h" +#include "core/object/callable_mp.h" +#include "core/object/class_db.h" +#include "core/os/keyboard.h" +#include "core/os/os.h" +#include "core/string/translation_server.h" +#include "editor/animation/animation_player_editor_plugin.h" +#include "editor/debugger/editor_debugger_node.h" +#include "editor/docks/scene_tree_dock.h" +#include "editor/editor_main_screen.h" +#include "editor/editor_node.h" +#include "editor/editor_string_names.h" +#include "editor/editor_undo_redo_manager.h" +#include "editor/plugins/editor_plugin_list.h" +#include "editor/run/editor_run_bar.h" +#include "editor/scene/3d/node_3d_editor_constants.h" +#include "editor/scene/3d/node_3d_editor_gizmos.h" +#include "editor/scene/3d/node_3d_editor_plugin.h" +#include "editor/settings/editor_settings.h" +#include "editor/translations/editor_translation_preview_button.h" +#include "scene/3d/audio_stream_player_3d.h" +#include "scene/3d/camera_3d.h" +#include "scene/3d/decal.h" +#include "scene/3d/mesh_instance_3d.h" +#include "scene/3d/physics/collision_object_3d.h" +#include "scene/3d/physics/collision_shape_3d.h" +#include "scene/3d/sprite_3d.h" +#include "scene/gui/box_container.h" +#include "scene/gui/color_picker.h" +#include "scene/gui/rich_text_label.h" +#include "scene/gui/split_container.h" +#include "scene/gui/subviewport_container.h" +#include "scene/main/scene_tree.h" +#include "scene/resources/gradient.h" +#include "scene/resources/immediate_mesh.h" +#include "scene/resources/packed_scene.h" +#include "servers/rendering/rendering_server.h" + +using namespace Node3DEditorConstants; + +Node3DEditorSelectedItem::~Node3DEditorSelectedItem() { + ERR_FAIL_NULL(RenderingServer::get_singleton()); + if (sbox_instance.is_valid()) { + RenderingServer::get_singleton()->free_rid(sbox_instance); + } + if (sbox_instance_offset.is_valid()) { + RenderingServer::get_singleton()->free_rid(sbox_instance_offset); + } + if (sbox_instance_xray.is_valid()) { + RenderingServer::get_singleton()->free_rid(sbox_instance_xray); + } + if (sbox_instance_xray_offset.is_valid()) { + RenderingServer::get_singleton()->free_rid(sbox_instance_xray_offset); + } +} + +void ViewportNavigationControl::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_DRAW: { + if (viewport != nullptr) { + _draw(); + _update_navigation(); + } + } break; + + case NOTIFICATION_MOUSE_ENTER: { + hovered = true; + queue_redraw(); + } break; + + case NOTIFICATION_MOUSE_EXIT: { + hovered = false; + queue_redraw(); + } break; + } +} + +void ViewportNavigationControl::_draw() { + if (nav_mode == View3DController::NAV_MODE_NONE) { + return; + } + + Vector2 center = get_size() / 2.0; + float radius = get_size().x / 2.0; + + const bool focused = focused_index != -1; + draw_circle(center, radius, Color(0.5, 0.5, 0.5, focused || hovered ? 0.35 : 0.15)); + + const Color c = focused ? Color(0.9, 0.9, 0.9, 0.9) : Color(0.5, 0.5, 0.5, 0.25); + + Vector2 circle_pos = focused ? center.move_toward(focused_pos, radius) : center; + + draw_circle(circle_pos, AXIS_CIRCLE_RADIUS, c); + draw_circle(circle_pos, AXIS_CIRCLE_RADIUS * 0.8, c.darkened(0.4)); +} + +void ViewportNavigationControl::_process_click(int p_index, Vector2 p_position, bool p_pressed) { + hovered = false; + queue_redraw(); + + if (focused_index != -1 && focused_index != p_index) { + return; + } + if (p_pressed) { + if (p_position.distance_to(get_size() / 2.0) < get_size().x / 2.0) { + focused_pos = p_position; + focused_index = p_index; + queue_redraw(); + } + } else { + focused_index = -1; + if (Input::get_singleton()->get_mouse_mode() == Input::MouseMode::MOUSE_MODE_CAPTURED) { + Input::get_singleton()->set_mouse_mode(Input::MouseMode::MOUSE_MODE_VISIBLE); + Input::get_singleton()->warp_mouse(focused_mouse_start); + } + } +} + +void ViewportNavigationControl::_process_drag(int p_index, Vector2 p_position, Vector2 p_relative_position) { + if (focused_index == p_index) { + if (Input::get_singleton()->get_mouse_mode() == Input::MouseMode::MOUSE_MODE_VISIBLE) { + Input::get_singleton()->set_mouse_mode(Input::MouseMode::MOUSE_MODE_CAPTURED); + focused_mouse_start = p_position; + } + focused_pos += p_relative_position; + queue_redraw(); + } +} + +void ViewportNavigationControl::gui_input(const Ref &p_event) { + // Mouse events + const Ref mouse_button = p_event; + if (mouse_button.is_valid() && mouse_button->get_button_index() == MouseButton::LEFT) { + _process_click(100, mouse_button->get_position(), mouse_button->is_pressed()); + } + + const Ref mouse_motion = p_event; + if (mouse_motion.is_valid()) { + _process_drag(100, mouse_motion->get_global_position(), viewport->view_3d_controller->get_warped_mouse_motion(mouse_motion, viewport->surface->get_global_rect())); + } + + // Touch events + const Ref screen_touch = p_event; + if (screen_touch.is_valid()) { + _process_click(screen_touch->get_index(), screen_touch->get_position(), screen_touch->is_pressed()); + } + + const Ref screen_drag = p_event; + if (screen_drag.is_valid()) { + _process_drag(screen_drag->get_index(), screen_drag->get_position(), screen_drag->get_relative()); + } +} + +void ViewportNavigationControl::_update_navigation() { + if (focused_index == -1) { + return; + } + + Vector2 delta = focused_pos - (get_size() / 2.0); + Vector2 delta_normalized = delta.normalized(); + switch (nav_mode) { + case View3DController::NAV_MODE_MOVE: { + real_t speed_multiplier = MIN(delta.length() / (get_size().x * 100.0), 3.0); + real_t speed = viewport->view_3d_controller->get_freelook_speed() * speed_multiplier; + + Vector3 forward; + if (viewport->view_3d_controller->get_freelook_scheme() == View3DController::FreelookScheme::FREELOOK_FULLY_AXIS_LOCKED) { + // Forward/backward keys will always go straight forward/backward, never moving on the Y axis. + forward = Vector3(0, 0, delta_normalized.y).rotated(Vector3(0, 1, 0), viewport->camera->get_rotation().y); + } else { + // Forward/backward keys will be relative to the camera pitch. + forward = viewport->camera->get_transform().basis.xform(Vector3(0, 0, delta_normalized.y)); + } + + const Vector3 right = viewport->camera->get_transform().basis.xform(Vector3(delta_normalized.x, 0, 0)); + + const Vector3 direction = forward + right; + const Vector3 motion = direction * speed; + viewport->view_3d_controller->cursor.pos += motion; + viewport->view_3d_controller->cursor.eye_pos += motion; + } break; + + case View3DController::NAV_MODE_LOOK: { + real_t speed_multiplier = MIN(delta.length() / (get_size().x * 2.5), 3.0); + real_t speed = viewport->view_3d_controller->get_freelook_speed() * speed_multiplier; + viewport->view_3d_controller->cursor_look(nullptr, delta_normalized * speed); + } break; + + case View3DController::NAV_MODE_PAN: { + real_t speed_multiplier = MIN(delta.length() / (get_size().x), 3.0); + real_t speed = viewport->view_3d_controller->get_freelook_speed() * speed_multiplier; + viewport->view_3d_controller->cursor_pan(nullptr, -delta_normalized * speed); + } break; + case View3DController::NAV_MODE_ZOOM: { + real_t speed_multiplier = MIN(delta.length() / (get_size().x), 3.0); + real_t speed = viewport->view_3d_controller->get_freelook_speed() * speed_multiplier; + viewport->view_3d_controller->cursor_zoom(nullptr, delta_normalized * speed); + } break; + case View3DController::NAV_MODE_ORBIT: { + real_t speed_multiplier = MIN(delta.length() / (get_size().x), 3.0); + real_t speed = viewport->view_3d_controller->get_freelook_speed() * speed_multiplier; + viewport->view_3d_controller->cursor_orbit(nullptr, delta_normalized * speed); + } break; + case View3DController::NAV_MODE_NONE: { + } break; + } +} + +void ViewportNavigationControl::set_navigation_mode(View3DController::NavigationMode p_nav_mode) { + nav_mode = p_nav_mode; +} + +void ViewportNavigationControl::set_viewport(Node3DEditorViewport *p_viewport) { + viewport = p_viewport; +} + +void ViewportRotationControl::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_ENTER_TREE: { + axis_menu_options.clear(); + axis_menu_options.push_back(Node3DEditorViewport::VIEW_RIGHT); + axis_menu_options.push_back(Node3DEditorViewport::VIEW_TOP); + axis_menu_options.push_back(Node3DEditorViewport::VIEW_FRONT); + axis_menu_options.push_back(Node3DEditorViewport::VIEW_LEFT); + axis_menu_options.push_back(Node3DEditorViewport::VIEW_BOTTOM); + axis_menu_options.push_back(Node3DEditorViewport::VIEW_REAR); + + axis_colors.clear(); + axis_colors.push_back(get_theme_color(SNAME("axis_x_color"), EditorStringName(Editor))); + axis_colors.push_back(get_theme_color(SNAME("axis_y_color"), EditorStringName(Editor))); + axis_colors.push_back(get_theme_color(SNAME("axis_z_color"), EditorStringName(Editor))); + queue_redraw(); + } break; + + case NOTIFICATION_DRAW: { + if (viewport != nullptr) { + _draw(); + } + } break; + + case NOTIFICATION_MOUSE_EXIT: { + focused_axis = -2; + queue_redraw(); + } break; + + case NOTIFICATION_WM_WINDOW_FOCUS_OUT: { + gizmo_activated = false; + } break; + } +} + +void ViewportRotationControl::_draw() { + const Vector2 center = get_size() / 2.0; + const real_t radius = get_size().x / 2.0; + + if (focused_axis > -2 || orbiting_index != -1) { + draw_circle(center, radius, Color(0.5, 0.5, 0.5, 0.25), true, -1.0, true); + } + + Vector axis_to_draw; + _get_sorted_axis(axis_to_draw); + for (int i = 0; i < axis_to_draw.size(); ++i) { + _draw_axis(axis_to_draw[i]); + } +} + +void ViewportRotationControl::_draw_axis(const Axis2D &p_axis) { + const bool focused = focused_axis == p_axis.axis; + const bool positive = p_axis.is_positive; + const int direction = p_axis.axis % 3; + + const Color axis_color = axis_colors[direction]; + const double min_alpha = 0.35; + const double alpha = focused ? 1.0 : Math::remap((p_axis.z_axis + 1.0) / 2.0, 0, 0.5, min_alpha, 1.0); + const Color c = focused ? Color(axis_color.lightened(0.25), 1.0) : Color(axis_color, alpha); + + // Highlight positive axis text when hovered. + const Color c_positive_axis = focused ? Color(1.0, 1.0, 1.0, alpha) : Color(0.0, 0.0, 0.0, alpha * 0.6); + + // Highlight negative axis text when hovered, but hide when not focused. + const Color c_negative_axis = focused ? Color(1.0, 1.0, 1.0, alpha) : Color(axis_color, 0); + + if (positive) { + // Draw axis lines for the positive axes. + const Vector2 center = get_size() / 2.0; + const Vector2 diff = p_axis.screen_point - center; + const float line_length = MAX(diff.length() - AXIS_CIRCLE_RADIUS - 0.5 * EDSCALE, 0); + + draw_line(center + diff.limit_length(0.5 * EDSCALE), center + diff.limit_length(line_length), c, 1.5 * EDSCALE, true); + + draw_circle(p_axis.screen_point, AXIS_CIRCLE_RADIUS, c, true, -1.0, true); + + // Draw the axis letter for the positive axes. + const String axis_name = direction == 0 ? "X" : (direction == 1 ? "Y" : "Z"); + const Ref &font = get_theme_font(SNAME("rotation_control"), EditorStringName(EditorFonts)); + const int font_size = get_theme_font_size(SNAME("rotation_control_size"), EditorStringName(EditorFonts)); + const Size2 char_size = font->get_char_size(axis_name[0], font_size); + const Vector2 char_offset = Vector2(-char_size.width / 2.0, char_size.height * 0.25); + draw_char(font, p_axis.screen_point + char_offset, axis_name, font_size, c_positive_axis); + } else { + // Draw an outline around the negative axes. + draw_circle(p_axis.screen_point, AXIS_CIRCLE_RADIUS, c, true, -1.0, true); + draw_circle(p_axis.screen_point, AXIS_CIRCLE_RADIUS * 0.8, c.darkened(0.4), true, -1.0, true); + + // Draw the text for the negative axes. + const String axis_name = direction == 0 ? "-X" : (direction == 1 ? "-Y" : "-Z"); + const Ref &font = get_theme_font(SNAME("rotation_control"), EditorStringName(EditorFonts)); + const int font_size = get_theme_font_size(SNAME("rotation_control_size"), EditorStringName(EditorFonts)); + const Size2 string_size = font->get_string_size(axis_name, HORIZONTAL_ALIGNMENT_LEFT, -1.0f, font_size); + const float font_ascent = font->get_ascent(font_size); + const float font_descent = font->get_descent(font_size); + const float string_height = font_ascent + font_descent; + const Vector2 offset(-string_size.width / 2.0, string_height * 0.25); + draw_string(font, p_axis.screen_point + offset, axis_name, HORIZONTAL_ALIGNMENT_LEFT, -1.0f, font_size, c_negative_axis); + } +} + +void ViewportRotationControl::_get_sorted_axis(Vector &r_axis) { + const Vector2 center = get_size() / 2.0; + const real_t radius = get_size().x / 2.0 - AXIS_CIRCLE_RADIUS - 2.0 * EDSCALE; + const Basis camera_basis = viewport->view_3d_controller->to_camera_transform().get_basis().inverse(); + + for (int i = 0; i < 3; ++i) { + Vector3 axis_3d = camera_basis.get_column(i); + Vector2 axis_vector = Vector2(axis_3d.x, -axis_3d.y) * radius; + + if (Math::abs(axis_3d.z) < 1.0) { + Axis2D pos_axis; + pos_axis.axis = i; + pos_axis.screen_point = center + axis_vector; + pos_axis.z_axis = axis_3d.z; + pos_axis.is_positive = true; + r_axis.push_back(pos_axis); + + Axis2D neg_axis; + neg_axis.axis = i + 3; + neg_axis.screen_point = center - axis_vector; + neg_axis.z_axis = -axis_3d.z; + neg_axis.is_positive = false; + r_axis.push_back(neg_axis); + } else { + // Special case when the camera is aligned with one axis. + Axis2D axis; + axis.axis = i + (axis_3d.z <= 0 ? 0 : 3); + axis.screen_point = center; + axis.z_axis = 1.0; + // Invert display style to fix aligned axis rendering. + axis.is_positive = (axis_3d.z > 0); + r_axis.push_back(axis); + } + } + + r_axis.sort_custom(); +} + +void ViewportRotationControl::_process_click(int p_index, Vector2 p_position, bool p_pressed) { + if (orbiting_index != -1 && orbiting_index != p_index) { + return; + } + if (p_pressed) { + if (p_position.distance_to(get_size() / 2.0) < get_size().x / 2.0) { + orbiting_index = p_index; + } + } else { + if (focused_axis > -1 && gizmo_activated) { + viewport->_menu_option(axis_menu_options[focused_axis]); + _update_focus(); + } + orbiting_index = -1; + if (Input::get_singleton()->get_mouse_mode() == Input::MouseMode::MOUSE_MODE_CAPTURED) { + Input::get_singleton()->set_mouse_mode(Input::MouseMode::MOUSE_MODE_VISIBLE); + Input::get_singleton()->warp_mouse(orbiting_mouse_start); + } + } +} + +void ViewportRotationControl::_process_drag(Ref p_event, int p_index, Vector2 p_position, Vector2 p_relative_position) { + Point2 mouse_pos = get_local_mouse_position(); + const bool movement_threshold_passed = original_mouse_pos.distance_to(mouse_pos) > 4 * EDSCALE; + if (orbiting_index == p_index && gizmo_activated && movement_threshold_passed) { + if (Input::get_singleton()->get_mouse_mode() == Input::MouseMode::MOUSE_MODE_VISIBLE) { + Input::get_singleton()->set_mouse_mode(Input::MouseMode::MOUSE_MODE_CAPTURED); + orbiting_mouse_start = p_position; + saved_cursor = viewport->view_3d_controller->cursor; + } + viewport->view_3d_controller->cursor_orbit(p_event, p_relative_position); + focused_axis = -1; + } else { + _update_focus(); + } +} + +void ViewportRotationControl::gui_input(const Ref &p_event) { + ERR_FAIL_COND(p_event.is_null()); + + // Key events + const Ref k = p_event; + + if (k.is_valid() && k->is_action_pressed(SNAME("ui_cancel"), false, true)) { + if (Input::get_singleton()->get_mouse_mode() == Input::MouseMode::MOUSE_MODE_CAPTURED) { + Input::get_singleton()->set_mouse_mode(Input::MouseMode::MOUSE_MODE_VISIBLE); + Input::get_singleton()->warp_mouse(orbiting_mouse_start); + viewport->view_3d_controller->cursor = saved_cursor; + gizmo_activated = false; + } + } + + // Mouse events + const Ref mb = p_event; + if (mb.is_valid()) { + if (mb->get_button_index() == MouseButton::LEFT) { + _process_click(100, mb->get_position(), mb->is_pressed()); + if (mb->is_pressed()) { + gizmo_activated = true; + original_mouse_pos = get_local_mouse_position(); + grab_focus(); + } + } else if (mb->get_button_index() == MouseButton::RIGHT) { + if (Input::get_singleton()->get_mouse_mode() == Input::MouseMode::MOUSE_MODE_CAPTURED) { + Input::get_singleton()->set_mouse_mode(Input::MouseMode::MOUSE_MODE_VISIBLE); + Input::get_singleton()->warp_mouse(orbiting_mouse_start); + viewport->view_3d_controller->cursor = saved_cursor; + gizmo_activated = false; + } + } + } + + const Ref mm = p_event; + if (mm.is_valid()) { + _process_drag(mm, 100, mm->get_global_position(), viewport->view_3d_controller->get_warped_mouse_motion(mm, viewport->surface->get_global_rect())); + } + + // Touch events + const Ref screen_touch = p_event; + if (screen_touch.is_valid()) { + _process_click(screen_touch->get_index(), screen_touch->get_position(), screen_touch->is_pressed()); + } + + const Ref screen_drag = p_event; + if (screen_drag.is_valid()) { + _process_drag(nullptr, screen_drag->get_index(), screen_drag->get_position(), screen_drag->get_relative()); + } +} + +void ViewportRotationControl::_update_focus() { + int original_focus = focused_axis; + focused_axis = -2; + Vector2 mouse_pos = get_local_mouse_position(); + + if (mouse_pos.distance_to(get_size() / 2.0) < get_size().x / 2.0) { + focused_axis = -1; + } + + Vector axes; + _get_sorted_axis(axes); + + for (int i = 0; i < axes.size(); i++) { + const Axis2D &axis = axes[i]; + if (mouse_pos.distance_to(axis.screen_point) < AXIS_CIRCLE_RADIUS) { + focused_axis = axis.axis; + } + } + + if (focused_axis != original_focus) { + queue_redraw(); + } +} + +void ViewportRotationControl::set_viewport(Node3DEditorViewport *p_viewport) { + viewport = p_viewport; +} + +void Node3DEditorViewport::_view_settings_confirmed(real_t p_interp_delta) { + // Set FOV override multiplier back to the default, so that the FOV + // setting specified in the View menu is correctly applied. + view_3d_controller->cursor.fov_scale = 1.0; + + view_3d_controller->update_camera(p_interp_delta); +} + +void Node3DEditorViewport::_update_navigation_controls_visibility() { + bool show_viewport_rotation_gizmo = EDITOR_GET("editors/3d/navigation/show_viewport_rotation_gizmo") && (!previewing_cinema && !previewing_camera); + rotation_control->set_visible(show_viewport_rotation_gizmo); + + bool show_viewport_navigation_gizmo = EDITOR_GET("editors/3d/navigation/show_viewport_navigation_gizmo") && (!previewing_cinema && !previewing_camera); + position_control->set_visible(show_viewport_navigation_gizmo); + look_control->set_visible(show_viewport_navigation_gizmo); +} + +bool Node3DEditorViewport::_is_rotation_arc_visible() const { + return _edit.mode == TRANSFORM_ROTATE && !Math::is_zero_approx(_edit.accumulated_rotation_angle) && _edit.gizmo_initiated; +} + +int Node3DEditorViewport::get_selected_count() const { + const HashMap &selection = editor_selection->get_selection(); + + int count = 0; + + for (const KeyValue &E : selection) { + Node3D *sp = ObjectDB::get_instance(E.key); + if (!sp) { + continue; + } + + Node3DEditorSelectedItem *se = editor_selection->get_node_editor_data(sp); + if (!se) { + continue; + } + + count++; + } + + return count; +} + +bool Node3DEditorViewport::_has_unlocked_selection() const { + const List &selection = editor_selection->get_top_selected_node_list(); + for (Node *E : selection) { + Node3D *sp = Object::cast_to(E); + if (sp && !_is_node_locked(sp)) { + return true; + } + } + return false; +} + +void Node3DEditorViewport::cancel_transform() { + const List &selection = editor_selection->get_top_selected_node_list(); + + for (Node *E : selection) { + Node3D *sp = Object::cast_to(E); + if (!sp) { + continue; + } + + Node3DEditorSelectedItem *se = editor_selection->get_node_editor_data(sp); + if (!se) { + continue; + } + + if (se && se->gizmo.is_valid()) { + Vector ids; + Vector restore; + + for (const KeyValue &GE : se->subgizmos) { + ids.push_back(GE.key); + restore.push_back(GE.value); + } + + se->gizmo->commit_subgizmos(ids, restore, true); + } + + sp->set_global_transform(se->original); + } + + for (const KeyValue &pair : _edit.children_original_globals) { + pair.key->set_global_transform(pair.value); + } + + collision_reposition = false; + finish_transform(); + set_message(TTRC("Transform Aborted."), 3); +} + +void Node3DEditorViewport::_update_shrink() { + const float scaling_3d_scale = GLOBAL_GET("rendering/scaling_3d/scale"); + const float shrink_factor = view_display_menu->get_popup()->is_item_checked(view_display_menu->get_popup()->get_item_index(VIEW_HALF_RESOLUTION)) ? 0.5 : 1.0; + viewport->set_scaling_3d_scale(MAX(0.25, scaling_3d_scale * shrink_factor)); +} + +float Node3DEditorViewport::get_znear() const { + return CLAMP(spatial_editor->get_znear(), MIN_Z, MAX_Z); +} + +float Node3DEditorViewport::get_zfar() const { + return CLAMP(spatial_editor->get_zfar(), MIN_Z, MAX_Z); +} + +float Node3DEditorViewport::get_fov() const { + return CLAMP(spatial_editor->get_fov() * view_3d_controller->cursor.fov_scale, MIN_FOV, MAX_FOV); +} + +Transform3D Node3DEditorViewport::_get_camera_transform() const { + return camera->get_global_transform(); +} + +Vector3 Node3DEditorViewport::_get_camera_position() const { + return _get_camera_transform().origin; +} + +Point2 Node3DEditorViewport::point_to_screen(const Vector3 &p_point) { + return camera->unproject_position(p_point); +} + +Vector3 Node3DEditorViewport::get_ray_pos(const Vector2 &p_pos) const { + return camera->project_ray_origin(p_pos); +} + +Vector3 Node3DEditorViewport::_get_camera_normal() const { + return -_get_camera_transform().basis.get_column(2); +} + +Vector3 Node3DEditorViewport::get_ray(const Vector2 &p_pos) const { + return camera->project_ray_normal(p_pos); +} + +void Node3DEditorViewport::_clear_selected() { + if (previewing) { + return; + } + + _edit.gizmo = Ref(); + _edit.gizmo_handle = -1; + _edit.gizmo_handle_secondary = false; + _edit.gizmo_initial_value = Variant(); + + Node3D *selected = spatial_editor->get_single_selected_node(); + Node3DEditorSelectedItem *se = selected ? editor_selection->get_node_editor_data(selected) : nullptr; + + if (se && se->gizmo.is_valid()) { + se->subgizmos.clear(); + se->gizmo->redraw(); + se->gizmo.unref(); + spatial_editor->update_transform_gizmo(); + } else { + editor_selection->clear(); + Node3DEditor::get_singleton()->edit(nullptr); + } +} + +void Node3DEditorViewport::_select_clicked(bool p_allow_locked) { + if (previewing) { + clicked = ObjectID(); + return; + } + + Node *node = ObjectDB::get_instance(clicked); + Node3D *selected = Object::cast_to(node); + clicked = ObjectID(); + + if (!selected) { + return; + } + + Node *edited_scene = EditorNode::get_singleton()->get_edited_scene(); + + // Prevent selection of nodes not owned by the edited scene. + while (node && node != edited_scene->get_parent()) { + Node *node_owner = node->get_owner(); + if (node_owner == edited_scene || node == edited_scene || (node_owner != nullptr && edited_scene->is_editable_instance(node_owner))) { + break; + } + node = node->get_parent(); + selected = Object::cast_to(node); + } + + if (!p_allow_locked) { + // Replace the node by the group if grouped + while (node && node != edited_scene->get_parent()) { + Node3D *selected_tmp = Object::cast_to(node); + if (selected_tmp && node->has_meta("_edit_group_")) { + selected = selected_tmp; + } + node = node->get_parent(); + } + } + + if (p_allow_locked || (selected != nullptr && !_is_node_locked(selected))) { + if (clicked_wants_append) { + const List &top_node_list = editor_selection->get_top_selected_node_list(); + const Node *active_node = top_node_list.is_empty() ? nullptr : top_node_list.back()->get(); + if (editor_selection->is_selected(selected)) { + editor_selection->remove_node(selected); + if (selected != active_node) { + editor_selection->add_node(selected); + } + } else { + editor_selection->add_node(selected); + } + } else { + if (!editor_selection->is_selected(selected)) { + editor_selection->clear(); + editor_selection->add_node(selected); + EditorNode::get_singleton()->edit_node(selected); + } + } + + const List &top_node_list = editor_selection->get_top_selected_node_list(); + if (top_node_list.size() == 1) { + EditorNode::get_singleton()->edit_node(top_node_list.front()->get()); + } + } +} + +ObjectID Node3DEditorViewport::_select_ray(const Point2 &p_pos) const { + Vector3 ray = get_ray(p_pos); + Vector3 pos = get_ray_pos(p_pos); + Vector2 shrinked_pos = p_pos; + + if (viewport->get_debug_draw() == Viewport::DEBUG_DRAW_SDFGI_PROBES) { + RS::get_singleton()->sdfgi_set_debug_probe_select(pos, ray); + } + + HashSet> found_gizmos; + + Node *edited_scene = get_tree()->get_edited_scene_root(); + ObjectID closest; + Node *item = nullptr; + float closest_dist = 1e20; + + Vector nodes_with_gizmos = Node3DEditor::get_singleton()->gizmo_bvh_ray_query(pos, pos + ray * camera->get_far()); + + for (Node3D *spat : nodes_with_gizmos) { + if (!spat || _is_node_locked(spat)) { + continue; + } + + Vector> gizmos = spat->get_gizmos(); + + for (int j = 0; j < gizmos.size(); j++) { + Ref seg = gizmos[j]; + + if (seg.is_null() || found_gizmos.has(seg)) { + continue; + } + + found_gizmos.insert(seg); + Vector3 point; + Vector3 normal; + + bool inters = seg->intersect_ray(camera, shrinked_pos, point, normal); + + if (!inters) { + continue; + } + + const real_t dist = pos.distance_to(point); + + if (dist < 0) { + continue; + } + + if (dist < closest_dist) { + item = Object::cast_to(spat); + if (item != edited_scene) { + item = edited_scene->get_deepest_editable_node(item); + } + + closest = item->get_instance_id(); + closest_dist = dist; + } + } + } + + if (!item) { + return ObjectID(); + } + + return closest; +} + +float Node3DEditorViewport::_min_screen_dist_to_aabb(const AABB &p_aabb, const Transform3D &p_transform, const Point2 &p_cursor) const { + Vector3 first_corner = p_transform.xform(p_aabb.get_endpoint(0)); + if (camera->is_position_behind(first_corner)) { + return 0.0f; + } + Point2 screen_min = camera->unproject_position(first_corner); + Point2 screen_max = screen_min; + + for (int i = 1; i < 8; i++) { + Vector3 world_corner = p_transform.xform(p_aabb.get_endpoint(i)); + if (camera->is_position_behind(world_corner)) { + return 0.0f; + } + Point2 s = camera->unproject_position(world_corner); + screen_min = screen_min.min(s); + screen_max = screen_max.max(s); + } + + float dx = MAX(screen_min.x - p_cursor.x, MAX(0.0f, p_cursor.x - screen_max.x)); + float dy = MAX(screen_min.y - p_cursor.y, MAX(0.0f, p_cursor.y - screen_max.y)); + return Math::sqrt(dx * dx + dy * dy); +} + +static bool _node_is_snap_source(Node *p_node, bool p_use_collision) { + if (!p_use_collision) { + return Object::cast_to(p_node); + } + if (Object::cast_to(p_node)) { + return true; + } + Node3D *n3d = Object::cast_to(p_node); + if (!n3d) { + return false; + } + for (const Ref &g : n3d->get_gizmos()) { + Ref seg = g; + if (seg.is_valid() && seg->get_collision_meshes_are_snap_source()) { + return true; + } + } + return false; +} + +static bool _node_has_snap_target(Node *p_node, bool p_use_collision) { + if (_node_is_snap_source(p_node, p_use_collision)) { + return true; + } + for (int i = 0; i < p_node->get_child_count(); i++) { + if (_node_has_snap_target(p_node->get_child(i), p_use_collision)) { + return true; + } + } + return false; +} + +bool Node3DEditorViewport::_find_closest_vertex_on_node(const Point2 &p_screen_pos, Node3D *p_node, float &r_closest_screen_dist, Vector3 &r_vertex_world) const { + bool found = false; + bool use_collision = Node3DEditor::get_singleton()->is_vertex_snap_use_collision(); + bool walk_collision_segments = use_collision && Object::cast_to(p_node); + + Transform3D gt = p_node->get_global_transform(); + Vector> gizmos = p_node->get_gizmos(); + + for (int i = 0; i < gizmos.size(); i++) { + Ref seg = gizmos[i]; + if (seg.is_null()) { + continue; + } + + if (walk_collision_segments) { + const Vector &segments = seg->get_collision_segments(); + for (int si = 0; si < segments.size(); si++) { + Vector3 world_v = gt.xform(segments[si]); + if (camera->is_position_behind(world_v)) { + continue; + } + Vector2 screen_v = camera->unproject_position(world_v); + float dist = screen_v.distance_to(p_screen_pos); + if (dist < r_closest_screen_dist) { + r_closest_screen_dist = dist; + r_vertex_world = world_v; + found = true; + } + } + } + + if (!use_collision || seg->get_collision_meshes_are_snap_source()) { + const LocalVector> &meshes = seg->get_collision_meshes(); + for (const Ref &tm : meshes) { + if (tm.is_null() || !tm->is_valid()) { + continue; + } + + const Vector &bvh = tm->get_bvh(); + const Vector &triangles = tm->get_triangles(); + const Vector &vertices = tm->get_vertices(); + + if (bvh.is_empty()) { + continue; + } + + // Traverse the TriangleMesh BVH, pruning branches whose screen-space + // AABB is farther than the current best. + LocalVector stack; + stack.push_back(bvh.size() - 1); + + while (!stack.is_empty()) { + int node_idx = stack[stack.size() - 1]; + stack.resize(stack.size() - 1); + + const TriangleMesh::BVH &b = bvh[node_idx]; + + if (_min_screen_dist_to_aabb(b.aabb, gt, p_screen_pos) >= r_closest_screen_dist) { + continue; + } + + if (b.face_index >= 0) { + const TriangleMesh::Triangle &tri = triangles[b.face_index]; + for (int vi = 0; vi < 3; vi++) { + Vector3 world_v = gt.xform(vertices[tri.indices[vi]]); + if (camera->is_position_behind(world_v)) { + continue; + } + Vector2 screen_v = camera->unproject_position(world_v); + float dist = screen_v.distance_to(p_screen_pos); + if (dist < r_closest_screen_dist) { + r_closest_screen_dist = dist; + r_vertex_world = world_v; + found = true; + } + } + } else { + stack.push_back(b.left); + stack.push_back(b.right); + } + } + } + } + } + + return found; +} + +bool Node3DEditorViewport::_find_closest_vertex_in_scene(const Point2 &p_screen_pos, float p_threshold, Vector3 &r_vertex_world, const HashMap *p_exclude) { + float closest_screen_dist = p_threshold; + bool found = false; + + Point2 min_pos(p_screen_pos.x - p_threshold, p_screen_pos.y - p_threshold); + Point2 max_pos(p_screen_pos.x + p_threshold, p_screen_pos.y + p_threshold); + Vector nodes_with_gizmos = Node3DEditor::get_singleton()->gizmo_bvh_frustum_query(_build_screen_frustum(min_pos, max_pos)); + + bool use_collision = Node3DEditor::get_singleton()->is_vertex_snap_use_collision(); + + for (Node3D *spat : nodes_with_gizmos) { + if (!spat) { + continue; + } + + if (!_node_is_snap_source(spat, use_collision)) { + continue; + } + + if (p_exclude) { + bool should_skip = false; + Node *current = spat; + while (current) { + if (p_exclude->has(current->get_instance_id())) { + should_skip = true; + break; + } + current = current->get_parent(); + } + if (should_skip) { + continue; + } + } + + if (_find_closest_vertex_on_node(p_screen_pos, spat, closest_screen_dist, r_vertex_world)) { + found = true; + } + } + + return found; +} + +void Node3DEditorViewport::_vertex_snap_commit() { + for (const KeyValue &E : vertex_snap_original_positions) { + Node3D *node = ObjectDB::get_instance(E.key); + if (node) { + vertex_snap_source += node->get_global_position() - E.value; + break; + } + } + + HashMap original_positions; + for (const KeyValue &E : vertex_snap_original_positions) { + original_positions[E.key] = E.value; + } + vertex_snap_dragging = false; + vertex_snap_has_target = false; + vertex_snap_original_positions.clear(); + if (!vertex_snap_mode) { + vertex_snap_has_source = false; + set_message(""); + } + surface->queue_redraw(); + + EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); + undo_redo->create_action(TTR("Vertex Snap")); + for (const KeyValue &E : original_positions) { + Node3D *node = ObjectDB::get_instance(E.key); + if (node) { + undo_redo->add_do_method(node, "set_global_position", node->get_global_position()); + undo_redo->add_undo_method(node, "set_global_position", E.value); + } + } + undo_redo->commit_action(false); + _reset_follow_mode_count(); +} + +void Node3DEditorViewport::_vertex_snap_cancel() { + vertex_snap_mode = false; + vertex_snap_keycode = Key::NONE; + vertex_snap_dragging = false; + vertex_snap_has_source = false; + vertex_snap_has_target = false; + + for (const KeyValue &E : vertex_snap_original_positions) { + Node3D *node = ObjectDB::get_instance(E.key); + if (node) { + node->set_global_position(E.value); + } + } + vertex_snap_original_positions.clear(); + set_message(TTR("Vertex Snap Canceled."), 3); + surface->queue_redraw(); +} + +bool Node3DEditorViewport::_is_vertex_occluded(const Vector3 &p_world_pos, const Vector2 &p_screen_pos) const { + Vector3 ray_pos = get_ray_pos(p_screen_pos); + float vertex_dist = ray_pos.distance_to(p_world_pos); + Vector hits = Node3DEditor::get_singleton()->gizmo_bvh_ray_query(ray_pos, ray_pos + get_ray(p_screen_pos) * camera->get_far()); + for (Node3D *spat : hits) { + if (!spat) { + continue; + } + Vector> gizmos = spat->get_gizmos(); + for (int i = 0; i < gizmos.size(); i++) { + Ref seg = gizmos[i]; + if (seg.is_null()) { + continue; + } + Vector3 point, normal; + if (seg->intersect_ray(camera, p_screen_pos, point, normal)) { + if (ray_pos.distance_to(point) < vertex_dist - 0.01f) { + return true; + } + } + } + } + return false; +} + +void Node3DEditorViewport::_vertex_snap_update_source(const Point2 &p_screen_pos) { + const List &selection = editor_selection->get_top_selected_node_list(); + if (selection.is_empty()) { + vertex_snap_has_source = false; + return; + } + + float threshold = VERTEX_SNAP_THRESHOLD * EDSCALE; + Vector3 vw; + bool found = false; + + if (spatial_editor->is_vertex_snap_origin_mode()) { + found = _find_closest_vertex_in_scene(p_screen_pos, threshold, vw); + } else { + bool use_collision = spatial_editor->is_vertex_snap_use_collision(); + bool selection_has_snap_target = false; + for (Node *E : selection) { + Node3D *sp = Object::cast_to(E); + if (!sp) { + continue; + } + + LocalVector descendants; + descendants.push_back(sp); + while (!descendants.is_empty()) { + Node3D *node = descendants[descendants.size() - 1]; + descendants.resize(descendants.size() - 1); + if (_node_is_snap_source(node, use_collision)) { + selection_has_snap_target = true; + if (_find_closest_vertex_on_node(p_screen_pos, node, threshold, vw)) { + found = true; + } + } + for (int i = 0; i < node->get_child_count(); i++) { + Node3D *child = Object::cast_to(node->get_child(i)); + if (child) { + descendants.push_back(child); + } + } + } + } + + if (!found && !selection_has_snap_target) { + found = _find_closest_vertex_in_scene(p_screen_pos, threshold, vw); + } + } + + if (found) { + vertex_snap_source = vw; + vertex_snap_has_source = true; + } else { + vertex_snap_has_source = false; + } +} + +void Node3DEditorViewport::_find_items_at_pos(const Point2 &p_pos, Vector<_RayResult> &r_results, bool p_include_locked_nodes) { + Vector3 ray = get_ray(p_pos); + Vector3 pos = get_ray_pos(p_pos); + + Vector nodes_with_gizmos = Node3DEditor::get_singleton()->gizmo_bvh_ray_query(pos, pos + ray * camera->get_far()); + + HashSet found_nodes; + + for (Node3D *spat : nodes_with_gizmos) { + if (!spat) { + continue; + } + + if (found_nodes.has(spat)) { + continue; + } + + if (!p_include_locked_nodes && _is_node_locked(spat)) { + continue; + } + + Vector> gizmos = spat->get_gizmos(); + for (int j = 0; j < gizmos.size(); j++) { + Ref seg = gizmos[j]; + + if (seg.is_null()) { + continue; + } + + Vector3 point; + Vector3 normal; + + bool inters = seg->intersect_ray(camera, p_pos, point, normal); + + if (!inters) { + continue; + } + + const real_t dist = pos.distance_to(point); + + if (dist < 0) { + continue; + } + + found_nodes.insert(spat); + + _RayResult res; + res.item = spat; + res.depth = dist; + r_results.push_back(res); + break; + } + } + + r_results.sort(); +} + +Vector3 Node3DEditorViewport::_get_screen_to_space(const Vector3 &p_vector3) { + Projection cm; + if (view_3d_controller->is_orthogonal()) { + cm.set_orthogonal(camera->get_size(), get_size().aspect(), get_znear() + p_vector3.z, get_zfar()); + } else { + cm.set_perspective(get_fov(), get_size().aspect(), get_znear() + p_vector3.z, get_zfar()); + } + Vector2 screen_he = cm.get_viewport_half_extents(); + + Transform3D camera_transform; + camera_transform.translate_local(view_3d_controller->cursor.pos); + camera_transform.basis.rotate(Vector3(1, 0, 0), -view_3d_controller->cursor.x_rot); + camera_transform.basis.rotate(Vector3(0, 1, 0), -view_3d_controller->cursor.y_rot); + camera_transform.translate_local(0, 0, view_3d_controller->cursor.distance); + + return camera_transform.xform(Vector3(((p_vector3.x / get_size().width) * 2.0 - 1.0) * screen_he.x, ((1.0 - (p_vector3.y / get_size().height)) * 2.0 - 1.0) * screen_he.y, -(get_znear() + p_vector3.z))); +} + +Vector Node3DEditorViewport::_build_screen_frustum(const Point2 &p_min, const Point2 &p_max) { + const real_t z_offset = MAX(0.0, 5.0 - get_znear()); + Vector3 box[4] = { + Vector3(p_min.x, p_min.y, z_offset), + Vector3(p_max.x, p_min.y, z_offset), + Vector3(p_max.x, p_max.y, z_offset), + Vector3(p_min.x, p_max.y, z_offset), + }; + + Vector frustum; + Vector3 cam_pos = _get_camera_position(); + for (int i = 0; i < 4; i++) { + Vector3 a = _get_screen_to_space(box[i]); + Vector3 b = _get_screen_to_space(box[(i + 1) % 4]); + if (view_3d_controller->is_orthogonal()) { + frustum.push_back(Plane((a - b).normalized(), a)); + } else { + frustum.push_back(Plane(a, b, cam_pos)); + } + } + Plane near_plane = Plane(-_get_camera_normal(), cam_pos); + near_plane.d -= get_znear(); + frustum.push_back(near_plane); + Plane far_plane = -near_plane; + far_plane.d += get_zfar(); + frustum.push_back(far_plane); + return frustum; +} + +void Node3DEditorViewport::_select_region() { + View3DController::Cursor cursor = view_3d_controller->cursor; + + if (cursor.region_begin == cursor.region_end) { + if (!clicked_wants_append) { + _clear_selected(); + } + return; + } + + Point2 region_min = cursor.region_begin.min(cursor.region_end); + Point2 region_max = cursor.region_begin.max(cursor.region_end); + Vector frustum = _build_screen_frustum(region_min, region_max); + + if (spatial_editor->get_single_selected_node()) { + Node3D *single_selected = spatial_editor->get_single_selected_node(); + Node3DEditorSelectedItem *se = editor_selection->get_node_editor_data(single_selected); + + if (se) { + Ref old_gizmo; + if (!clicked_wants_append) { + se->subgizmos.clear(); + old_gizmo = se->gizmo; + se->gizmo.unref(); + } + + bool found_subgizmos = false; + Vector> gizmos = single_selected->get_gizmos(); + for (int j = 0; j < gizmos.size(); j++) { + Ref seg = gizmos[j]; + if (seg.is_null()) { + continue; + } + + if (se->gizmo.is_valid() && se->gizmo != seg) { + continue; + } + + Vector subgizmos = seg->subgizmos_intersect_frustum(camera, frustum); + if (!subgizmos.is_empty()) { + se->gizmo = seg; + for (int i = 0; i < subgizmos.size(); i++) { + int subgizmo_id = subgizmos[i]; + if (!se->subgizmos.has(subgizmo_id)) { + se->subgizmos.insert(subgizmo_id, se->gizmo->get_subgizmo_transform(subgizmo_id)); + } + } + found_subgizmos = true; + break; + } + } + + if (!clicked_wants_append || found_subgizmos) { + if (se->gizmo.is_valid()) { + se->gizmo->redraw(); + } + + if (old_gizmo != se->gizmo && old_gizmo.is_valid()) { + old_gizmo->redraw(); + } + + spatial_editor->update_transform_gizmo(); + } + + if (found_subgizmos) { + return; + } + } + } + + if (!clicked_wants_append) { + _clear_selected(); + } + + Vector nodes_with_gizmos = Node3DEditor::get_singleton()->gizmo_bvh_frustum_query(frustum); + HashSet found_nodes; + Vector selected; + + Node *edited_scene = get_tree()->get_edited_scene_root(); + if (edited_scene == nullptr) { + return; + } + + for (Node3D *sp : nodes_with_gizmos) { + if (!sp || _is_node_locked(sp)) { + continue; + } + + if (found_nodes.has(sp)) { + continue; + } + found_nodes.insert(sp); + + Node *node = Object::cast_to(sp); + + // Selection requires that the node is the edited scene or its descendant, and has an owner. + if (node != edited_scene) { + if (!node->get_owner() || !edited_scene->is_ancestor_of(node)) { + continue; + } + node = edited_scene->get_deepest_editable_node(node); + while (node != edited_scene) { + Node *node_owner = node->get_owner(); + if (node_owner == edited_scene || (node_owner != nullptr && edited_scene->is_editable_instance(node_owner))) { + break; + } + node = node->get_parent(); + } + } + + // Replace the node by the group if grouped + if (node->is_class("Node3D")) { + Node3D *sel = Object::cast_to(node); + while (node && node != EditorNode::get_singleton()->get_edited_scene()->get_parent()) { + Node3D *selected_tmp = Object::cast_to(node); + if (selected_tmp && node->has_meta("_edit_group_")) { + sel = selected_tmp; + } + node = node->get_parent(); + } + node = sel; + } + + if (_is_node_locked(node)) { + continue; + } + + Vector> gizmos = sp->get_gizmos(); + for (int j = 0; j < gizmos.size(); j++) { + Ref seg = gizmos[j]; + if (seg.is_null()) { + continue; + } + + if (seg->intersect_frustum(camera, frustum)) { + selected.push_back(node); + } + } + } + + for (int i = 0; i < selected.size(); i++) { + if (!editor_selection->is_selected(selected[i])) { + editor_selection->add_node(selected[i]); + } + } + + const List &top_node_list = editor_selection->get_top_selected_node_list(); + if (top_node_list.size() == 1) { + EditorNode::get_singleton()->edit_node(top_node_list.front()->get()); + } +} + +void Node3DEditorViewport::_view_state_changed() { + if (view_3d_controller->get_orthogonal_mode() == View3DController::ORTHOGONAL_AUTO) { + _menu_option(VIEW_ORTHOGONAL); + view_3d_controller->force_auto_orthogonal(); + } else if (view_3d_controller->get_orthogonal_mode() == View3DController::ORTHOGONAL_DISABLED) { + _menu_option(VIEW_PERSPECTIVE); + } + _update_name(); +} + +void Node3DEditorViewport::_update_name() { + view_display_menu->set_text(view_3d_controller->get_view_type_name()); + view_display_menu->reset_size(); +} + +void Node3DEditorViewport::_compute_edit(const Point2 &p_point) { + _edit.original_local = spatial_editor->are_local_coords_enabled(); + _edit.click_ray = get_ray(p_point); + _edit.click_ray_pos = get_ray_pos(p_point); + _edit.plane = TRANSFORM_VIEW; + spatial_editor->update_transform_gizmo(); + _edit.center = spatial_editor->get_gizmo_transform().origin; + + Node3D *selected = spatial_editor->get_single_selected_node(); + Node3DEditorSelectedItem *se = selected ? editor_selection->get_node_editor_data(selected) : nullptr; + + if (se && se->gizmo.is_valid()) { + for (const KeyValue &E : se->subgizmos) { + int subgizmo_id = E.key; + se->subgizmos[subgizmo_id] = se->gizmo->get_subgizmo_transform(subgizmo_id); + } + se->original_local = selected->get_transform(); + se->original = selected->get_global_transform(); + } else { + const List &selection = editor_selection->get_top_selected_node_list(); + + for (Node *E : selection) { + Node3D *sp = Object::cast_to(E); + if (!sp) { + continue; + } + + Node3DEditorSelectedItem *sel_item = editor_selection->get_node_editor_data(sp); + + if (!sel_item) { + continue; + } + + sel_item->original_local = sel_item->sp->get_local_gizmo_transform(); + sel_item->original = sel_item->sp->get_global_gizmo_transform(); + } + } + + if (spatial_editor->is_preserve_children_transform_enabled() && _edit.children_original_globals.is_empty()) { + const List &selection = editor_selection->get_top_selected_node_list(); + for (Node *E : selection) { + Node3D *sp = Object::cast_to(E); + if (!sp) { + continue; + } + + int child_count = sp->get_child_count(); + for (int i = 0; i < child_count; i++) { + Node3D *child = Object::cast_to(sp->get_child(i)); + if (child) { + _edit.children_original_globals[child] = child->get_global_transform(); + } + } + } + } +} + +static Key _get_key_modifier_setting(const String &p_property) { + switch (EDITOR_GET(p_property).operator int()) { + case 0: + return Key::NONE; + case 1: + return Key::SHIFT; + case 2: + return Key::ALT; + case 3: + return Key::META; + case 4: + return Key::CTRL; + } + return Key::NONE; +} + +static Key _get_key_modifier(Ref e) { + if (e->is_shift_pressed()) { + return Key::SHIFT; + } + if (e->is_alt_pressed()) { + return Key::ALT; + } + if (e->is_ctrl_pressed()) { + return Key::CTRL; + } + if (e->is_meta_pressed()) { + return Key::META; + } + return Key::NONE; +} + +bool Node3DEditorViewport::_transform_gizmo_select(const Vector2 &p_screenpos, bool p_highlight_only) { + if (!spatial_editor->is_gizmo_visible()) { + return false; + } + if (get_selected_count() == 0) { + if (p_highlight_only) { + spatial_editor->select_gizmo_highlight_axis(-1); + } + return false; + } + + Vector3 ray_pos = get_ray_pos(p_screenpos); + Vector3 ray = get_ray(p_screenpos); + + Transform3D gt = spatial_editor->get_gizmo_transform(); + + if (spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_TRANSFORM || spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_MOVE) { + int col_axis = -1; + real_t col_d = 1e20; + + for (int i = 0; i < 3; i++) { + const Vector3 grabber_pos = gt.origin + gt.basis.get_column(i).normalized() * gizmo_scale * (GIZMO_ARROW_OFFSET + (GIZMO_ARROW_SIZE * 0.5)); + const real_t grabber_radius = gizmo_scale * GIZMO_ARROW_SIZE; + + Vector3 r; + + if (Geometry3D::segment_intersects_sphere(ray_pos, ray_pos + ray * MAX_Z, grabber_pos, grabber_radius, &r)) { + const real_t d = r.distance_to(ray_pos); + if (d < col_d) { + col_d = d; + col_axis = i; + } + } + } + + bool is_plane_translate = false; + // plane select + if (col_axis == -1) { + col_d = 1e20; + + for (int i = 0; i < 3; i++) { + Vector3 ivec2 = gt.basis.get_column((i + 1) % 3).normalized(); + Vector3 ivec3 = gt.basis.get_column((i + 2) % 3).normalized(); + + // Allow some tolerance to make the plane easier to click, + // even if the click is actually slightly outside the plane. + const Vector3 grabber_pos = gt.origin + (ivec2 + ivec3) * gizmo_scale * (GIZMO_PLANE_SIZE + GIZMO_PLANE_DST * 0.6667); + + Vector3 r; + Plane plane(gt.basis.get_column(i).normalized(), gt.origin); + + if (plane.intersects_ray(ray_pos, ray, &r)) { + const real_t dist = r.distance_to(grabber_pos); + // Allow some tolerance to make the plane easier to click, + // even if the click is actually slightly outside the plane. + if (dist < (gizmo_scale * GIZMO_PLANE_SIZE * 1.5)) { + const real_t d = ray_pos.distance_to(r); + if (d < col_d) { + col_d = d; + col_axis = i; + + is_plane_translate = true; + } + } + } + } + } + + if (col_axis != -1) { + if (p_highlight_only) { + spatial_editor->select_gizmo_highlight_axis(col_axis + (is_plane_translate ? 6 : 0)); + + } else { + //handle plane translate + _edit.mode = TRANSFORM_TRANSLATE; + _compute_edit(p_screenpos); + _edit.plane = TransformPlane(TRANSFORM_X_AXIS + col_axis + (is_plane_translate ? 3 : 0)); + } + return true; + } + } + + if (spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_TRANSFORM || spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_ROTATE) { + int col_axis = -1; + bool view_rotation_selected = false; + bool trackball_selected = false; + + Vector3 hit_position; + Vector3 hit_normal; + real_t ray_length = gt.origin.distance_to(ray_pos) + (GIZMO_CIRCLE_SIZE * gizmo_scale) * 4.0f; + if (Geometry3D::segment_intersects_sphere(ray_pos, ray_pos + ray * ray_length, gt.origin, gizmo_scale * (GIZMO_CIRCLE_SIZE), &hit_position, &hit_normal)) { + if (hit_normal.dot(_get_camera_normal()) < 0.05) { + hit_position = gt.xform_inv(hit_position).abs(); + int min_axis = hit_position.min_axis_index(); + if (hit_position[min_axis] < gizmo_scale * GIZMO_RING_HALF_WIDTH) { + col_axis = min_axis; + } + } + } + + if (col_axis == -1) { + float col_d = 1e20; + + for (int i = 0; i < 3; i++) { + Plane plane(gt.basis.get_column(i).normalized(), gt.origin); + Vector3 r; + if (!plane.intersects_ray(ray_pos, ray, &r)) { + continue; + } + + const real_t dist = r.distance_to(gt.origin); + const Vector3 r_dir = (r - gt.origin).normalized(); + + if (_get_camera_normal().dot(r_dir) <= 0.005) { + if (dist > gizmo_scale * (GIZMO_CIRCLE_SIZE - GIZMO_RING_HALF_WIDTH) && dist < gizmo_scale * (GIZMO_CIRCLE_SIZE + GIZMO_RING_HALF_WIDTH)) { + const real_t d = ray_pos.distance_to(r); + if (d < col_d) { + col_d = d; + col_axis = i; + } + } + } + } + } + + if (col_axis == -1) { + Vector3 ray_to_center = gt.origin - ray_pos; + real_t ray_length_to_center = ray_to_center.dot(ray); + Vector3 closest_point_on_ray = ray_pos + ray * ray_length_to_center; + real_t distance_ray_to_center = closest_point_on_ray.distance_to(gt.origin); + + real_t view_rotation_radius = gizmo_scale * spatial_editor->gizmo_view_rotation_scale; + real_t circumference_tolerance = gizmo_scale * GIZMO_RING_HALF_WIDTH; + + if (Math::abs(distance_ray_to_center - view_rotation_radius) < circumference_tolerance && + ray_length_to_center > 0) { + view_rotation_selected = true; + } else if (spatial_editor->is_trackball_enabled() && distance_ray_to_center < gizmo_scale * (GIZMO_CIRCLE_SIZE - GIZMO_RING_HALF_WIDTH) && ray_length_to_center > 0) { + trackball_selected = true; + } + } + + if (view_rotation_selected) { + if (p_highlight_only) { + spatial_editor->select_gizmo_highlight_axis(GIZMO_HIGHLIGHT_AXIS_VIEW_ROTATION); + } else { + _edit.mode = TRANSFORM_ROTATE; + _compute_edit(p_screenpos); + _edit.plane = TRANSFORM_VIEW; + _edit.accumulated_rotation_angle = 0.0; + _edit.rotation_angle = 0.0; + _edit.rotation_axis = _get_camera_normal(); + _edit.view_axis_local = spatial_editor->get_gizmo_transform().basis.xform_inv(_get_camera_normal()).normalized(); + _edit.gizmo_initiated = true; + } + return true; + } else if (trackball_selected) { + if (p_highlight_only) { + spatial_editor->select_gizmo_highlight_axis(GIZMO_HIGHLIGHT_AXIS_TRACKBALL); + } else { + _edit.mode = TRANSFORM_ROTATE; + _compute_edit(p_screenpos); + _edit.plane = TRANSFORM_VIEW; + _edit.is_trackball = true; + _edit.show_rotation_line = false; + _edit.accumulated_rotation_angle = 0.0; + _edit.rotation_angle = 0.0; + _edit.rotation_axis = _get_camera_normal(); + _edit.gizmo_initiated = true; + spatial_editor->select_gizmo_highlight_axis(-1); + } + return true; + } else if (col_axis != -1) { + if (p_highlight_only) { + spatial_editor->select_gizmo_highlight_axis(col_axis + 3); + } else { + //handle axis-specific rotate + _edit.mode = TRANSFORM_ROTATE; + _compute_edit(p_screenpos); + _edit.plane = TransformPlane(TRANSFORM_X_AXIS + col_axis); + _edit.accumulated_rotation_angle = 0.0; + _edit.rotation_angle = 0.0; + _edit.rotation_axis = gt.basis.get_column(col_axis).normalized(); + _edit.gizmo_initiated = true; + } + return true; + } + } + + if (spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_SCALE) { + int col_axis = -1; + float col_d = 1e20; + + for (int i = 0; i < 3; i++) { + const Vector3 grabber_pos = gt.origin + gt.basis.get_column(i).normalized() * gizmo_scale * GIZMO_SCALE_OFFSET; + const real_t grabber_radius = gizmo_scale * GIZMO_ARROW_SIZE; + + Vector3 r; + + if (Geometry3D::segment_intersects_sphere(ray_pos, ray_pos + ray * MAX_Z, grabber_pos, grabber_radius, &r)) { + const real_t d = r.distance_to(ray_pos); + if (d < col_d) { + col_d = d; + col_axis = i; + } + } + } + + bool is_plane_scale = false; + // plane select + if (col_axis == -1) { + col_d = 1e20; + + for (int i = 0; i < 3; i++) { + const Vector3 ivec2 = gt.basis.get_column((i + 1) % 3).normalized(); + const Vector3 ivec3 = gt.basis.get_column((i + 2) % 3).normalized(); + + // Allow some tolerance to make the plane easier to click, + // even if the click is actually slightly outside the plane. + const Vector3 grabber_pos = gt.origin + (ivec2 + ivec3) * gizmo_scale * (GIZMO_PLANE_SIZE + GIZMO_PLANE_DST * 0.6667); + + Vector3 r; + Plane plane(gt.basis.get_column(i).normalized(), gt.origin); + + if (plane.intersects_ray(ray_pos, ray, &r)) { + const real_t dist = r.distance_to(grabber_pos); + // Allow some tolerance to make the plane easier to click, + // even if the click is actually slightly outside the plane. + if (dist < (gizmo_scale * GIZMO_PLANE_SIZE * 1.5)) { + const real_t d = ray_pos.distance_to(r); + if (d < col_d) { + col_d = d; + col_axis = i; + + is_plane_scale = true; + } + } + } + } + } + + if (col_axis != -1) { + if (p_highlight_only) { + spatial_editor->select_gizmo_highlight_axis(col_axis + (is_plane_scale ? 12 : 9)); + + } else { + //handle scale + _edit.mode = TRANSFORM_SCALE; + _compute_edit(p_screenpos); + _edit.plane = TransformPlane(TRANSFORM_X_AXIS + col_axis + (is_plane_scale ? 3 : 0)); + } + return true; + } + } + + if (p_highlight_only) { + spatial_editor->select_gizmo_highlight_axis(-1); + } + + return false; +} + +void Node3DEditorViewport::_transform_gizmo_apply(Node3D *p_node, const Transform3D &p_transform, bool p_local) { + if (p_transform.basis.determinant() == 0) { + return; + } + + bool preserve_children = spatial_editor->is_preserve_children_transform_enabled(); + + Vector children_global_transforms; + Vector node3d_children; + + if (preserve_children) { + int child_count = p_node->get_child_count(); + for (int i = 0; i < child_count; i++) { + Node3D *child = Object::cast_to(p_node->get_child(i)); + if (child) { + children_global_transforms.push_back(child->get_global_transform()); + node3d_children.push_back(child); + } + } + } + + if (p_local) { + p_node->set_transform(p_transform); + } else { + p_node->set_global_transform(p_transform); + } + + if (preserve_children) { + for (int i = 0; i < node3d_children.size(); i++) { + node3d_children[i]->set_global_transform(children_global_transforms[i]); + } + } +} + +Transform3D Node3DEditorViewport::_compute_transform(TransformMode p_mode, const Transform3D &p_original, const Transform3D &p_original_local, Vector3 p_motion, double p_extra, bool p_local, bool p_orthogonal, bool p_view_axis) { + switch (p_mode) { + case TRANSFORM_SCALE: { + if (spatial_editor->is_snap_enabled()) { + p_motion.snapf(p_extra); + } + Transform3D s; + if (p_local) { + s.basis = p_original_local.basis.scaled_local(p_motion + Vector3(1, 1, 1)); + s.origin = p_original_local.origin; + } else { + s.basis.scale(p_motion + Vector3(1, 1, 1)); + Transform3D base = Transform3D(Basis(), _edit.center); + s = base * (s * (base.inverse() * p_original)); + + // Recalculate orthogonalized scale without moving origin. + if (p_orthogonal) { + s.basis = p_original.basis.scaled_orthogonal(p_motion + Vector3(1, 1, 1)); + } + } + + return s; + } + case TRANSFORM_TRANSLATE: { + if (spatial_editor->is_snap_enabled()) { + p_motion.snapf(p_extra); + } + + if (p_local) { + return p_original_local.translated_local(p_motion); + } + + return p_original.translated(p_motion); + } + case TRANSFORM_ROTATE: { + Transform3D r; + + Basis parent_global_basis = p_original.basis * p_original_local.basis.inverse(); + + Vector3 axis; + if (p_local && !p_view_axis) { + axis = p_original_local.basis.xform(p_motion); + } else { + axis = parent_global_basis.xform_inv(p_motion); + } + + if (p_local) { + r.basis = Basis(axis.normalized(), p_extra) * p_original_local.basis; + r.origin = p_original_local.origin; + } else { + r.basis = parent_global_basis * Basis(axis.normalized(), p_extra) * p_original_local.basis; + r.origin = Basis(p_motion, p_extra).xform(p_original.origin - _edit.center) + _edit.center; + } + + return r; + } + default: { + ERR_FAIL_V_MSG(Transform3D(), "Invalid mode in '_compute_transform'"); + } + } +} + +void Node3DEditorViewport::_reset_transform(TransformType p_type) { + List selection = editor_selection->get_full_selected_node_list(); + if (selection.is_empty()) { + return; + } + EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); + undo_redo->create_action(TTR("Reset Transform")); + for (Node *node : selection) { + Node3D *sp = Object::cast_to(node); + if (!sp) { + continue; + } + + switch (p_type) { + case TransformType::POSITION: + undo_redo->add_undo_method(sp, "set_position", sp->get_position()); + undo_redo->add_do_method(sp, "set_position", Vector3()); + break; + case TransformType::ROTATION: + undo_redo->add_undo_method(sp, "set_rotation", sp->get_rotation()); + undo_redo->add_do_method(sp, "set_rotation", Vector3()); + break; + case TransformType::SCALE: + undo_redo->add_undo_method(sp, "set_scale", sp->get_scale()); + undo_redo->add_do_method(sp, "set_scale", Vector3(1, 1, 1)); + break; + } + } + undo_redo->commit_action(); +} + +void Node3DEditorViewport::_surface_mouse_enter() { + if (Input::get_singleton()->get_mouse_mode() == Input::MouseMode::MOUSE_MODE_CAPTURED) { + return; + } + + if (!surface->has_focus() && (!get_viewport()->gui_get_focus_owner() || !get_viewport()->gui_get_focus_owner()->is_text_field())) { + surface->grab_focus(); + } +} + +void Node3DEditorViewport::_surface_mouse_exit() { + _remove_preview_node(); + _reset_preview_material(); + _remove_preview_material(); +} + +void Node3DEditorViewport::_surface_focus_enter() { + view_display_menu->set_disable_shortcuts(false); +} + +void Node3DEditorViewport::_surface_focus_exit() { + view_display_menu->set_disable_shortcuts(true); +} + +bool Node3DEditorViewport::_is_node_locked(const Node *p_node) const { + return p_node->get_meta("_edit_lock_", false); +} + +void Node3DEditorViewport::_list_select(Ref b) { + Vector<_RayResult> potential_selection_results; + _find_items_at_pos(b->get_position(), potential_selection_results, b->is_alt_pressed()); + + Node *edited_scene = EditorNode::get_singleton()->get_edited_scene(); + + // Filter to a list of nodes which include either the edited scene or nodes directly owned by the edited scene. + // If a node has an invalid owner, recursively check their parents until a valid node is found. + for (int i = 0; i < potential_selection_results.size(); i++) { + Node3D *node = potential_selection_results[i].item; + while (true) { + if (node == nullptr || node == edited_scene->get_parent()) { + break; + } else { + Node *node_owner = node->get_owner(); + if (node == edited_scene || node_owner == edited_scene || (node_owner != nullptr && edited_scene->is_editable_instance(node_owner))) { + if (!selection_results.has(node)) { + selection_results.append(node); + } + break; + } + } + node = Object::cast_to(node->get_parent()); + } + } + + clicked_wants_append = b->is_shift_pressed(); + + if (selection_results.size() == 1) { + clicked = selection_results[0]->get_instance_id(); + selection_results.clear(); + + if (clicked.is_valid()) { + _select_clicked(b->is_alt_pressed()); + } + } else if (!selection_results.is_empty()) { + NodePath root_path = get_tree()->get_edited_scene_root()->get_path(); + StringName root_name = root_path.get_name(root_path.get_name_count() - 1); + int icon_max_width = EditorNode::get_singleton()->get_editor_theme()->get_constant(SNAME("class_icon_size"), EditorStringName(Editor)); + + for (int i = 0; i < selection_results.size(); i++) { + Node3D *spat = selection_results[i]; + + Ref icon = EditorNode::get_singleton()->get_object_icon(spat); + + String node_path = "/" + root_name + "/" + String(root_path.rel_path_to(spat->get_path())); + + int locked = 0; + if (_is_node_locked(spat)) { + locked = 1; + } else { + Node *ed_scene = EditorNode::get_singleton()->get_edited_scene(); + Node *node = spat; + + while (node && node != ed_scene->get_parent()) { + Node3D *selected_tmp = Object::cast_to(node); + if (selected_tmp && node->has_meta("_edit_group_")) { + locked = 2; + } + node = node->get_parent(); + } + } + + String suffix; + if (locked == 1) { + suffix = " (" + TTR("Locked") + ")"; + } else if (locked == 2) { + suffix = " (" + TTR("Grouped") + ")"; + } + selection_menu->add_item((String)spat->get_name() + suffix); + selection_menu->set_item_icon(i, icon); + selection_menu->set_item_icon_max_width(i, icon_max_width); + selection_menu->set_item_metadata(i, node_path); + selection_menu->set_item_tooltip(i, String(spat->get_name()) + "\nType: " + spat->get_class() + "\nPath: " + node_path); + } + + selection_results_menu = selection_results; + selection_menu->set_position(get_screen_position() + b->get_position()); + selection_menu->reset_size(); + selection_menu->popup(); + } +} + +// Helper function to redirect mouse events to the active freelook viewport +static bool _redirect_freelook_input(const Ref &p_event, Node3DEditorViewport *p_exclude_viewport = nullptr) { + if (Input::get_singleton()->get_mouse_mode() != Input::MouseMode::MOUSE_MODE_CAPTURED) { + return false; + } + + Node3DEditor *editor = Node3DEditor::get_singleton(); + if (!editor->get_freelook_viewport()) { + return false; + } + + Node3DEditorViewport *freelook_vp = editor->get_freelook_viewport(); + if (freelook_vp == p_exclude_viewport) { + return false; + } + + Ref mouse_event = p_event; + if (!mouse_event.is_valid()) { + return false; + } + + Control *target_surface = freelook_vp->get_surface(); + + target_surface->emit_signal(SceneStringName(gui_input), p_event); + return true; +} + +// This is only active during instant transforms, +// to capture and wrap mouse events outside the control. +void Node3DEditorViewport::input(const Ref &p_event) { + ERR_FAIL_COND(!_edit.instant); + Ref m = p_event; + + if (m.is_valid()) { + _edit.mouse_pos += view_3d_controller->get_warped_mouse_motion(p_event, surface->get_global_rect()); + update_transform(_get_key_modifier(m) == Key::SHIFT); + } +} + +void Node3DEditorViewport::_sinput(const Ref &p_event) { + const Ref k = p_event; + + if (k.is_valid() && k->is_pressed()) { + const Key code = view_3d_controller->emulate_numpad_key(k->get_physical_keycode()); + if (code != k->get_physical_keycode()) { + k->set_keycode(code); + } + } + + if (get_viewport()->gui_get_drag_data()) { + // Disable all input actions during drag-and-drop. + return; + } + + if (k.is_valid()) { + Ref k_no_shift = k->duplicate(); + k_no_shift->set_shift_pressed(false); + if (!vertex_snap_mode && !vertex_snap_dragging && k->is_pressed() && _edit.mode == TRANSFORM_NONE && ED_IS_SHORTCUT("spatial_editor/vertex_snap", k_no_shift)) { + vertex_snap_mode = true; + vertex_snap_keycode = k->get_physical_keycode() != Key::NONE ? k->get_physical_keycode() : k->get_keycode(); + _disable_follow_mode(); + set_message(TTR("Vertex Snap")); + _vertex_snap_update_source(_edit.mouse_pos); + surface->queue_redraw(); + } else if (vertex_snap_mode && !k->is_pressed() && k->get_physical_keycode() == vertex_snap_keycode) { + vertex_snap_mode = false; + vertex_snap_keycode = Key::NONE; + if (!vertex_snap_dragging) { + vertex_snap_has_source = false; + vertex_snap_has_target = false; + set_message(""); + surface->queue_redraw(); + } + } else if ((vertex_snap_mode || vertex_snap_dragging) && k->is_pressed() && k->get_keycode() == Key::ESCAPE) { + if (vertex_snap_dragging) { + _vertex_snap_cancel(); + } else { + vertex_snap_has_source = false; + surface->queue_redraw(); + } + vertex_snap_mode = false; + vertex_snap_keycode = Key::NONE; + set_message(""); + return; + } + } + + if (_redirect_freelook_input(p_event, this)) { + return; + } + + { + Ref vb = p_event; + if ((vertex_snap_mode || vertex_snap_dragging) && vb.is_valid()) { + if (vb->get_button_index() == MouseButton::RIGHT && vb->is_pressed() && vertex_snap_dragging) { + _vertex_snap_cancel(); + return; + } + if (vb->get_button_index() == MouseButton::LEFT) { + if (vb->is_pressed()) { + const List &selection = editor_selection->get_top_selected_node_list(); + bool use_origin_snap = spatial_editor->is_vertex_snap_origin_mode(); + + Node3D *selected = spatial_editor->get_single_selected_node(); + Node3DEditorSelectedItem *se = selected ? editor_selection->get_node_editor_data(selected) : nullptr; + bool has_subgizmos = se && se->gizmo.is_valid() && !se->subgizmos.is_empty(); + + if (!use_origin_snap) { + bool has_snap_target = false; + for (Node *E : selection) { + if (_node_has_snap_target(E, spatial_editor->is_vertex_snap_use_collision())) { + has_snap_target = true; + break; + } + } + if (!has_snap_target) { + use_origin_snap = true; + } + } + + if (use_origin_snap || !vertex_snap_has_source) { + _vertex_snap_update_source(vb->get_position()); + } + + if (vertex_snap_has_source && !selection.is_empty()) { + vertex_snap_original_positions.clear(); + for (Node *E : selection) { + Node3D *sp = Object::cast_to(E); + if (sp) { + vertex_snap_original_positions[sp->get_instance_id()] = sp->get_global_position(); + } + } + + if (!use_origin_snap) { + vertex_snap_dragging = true; + Vector3 cam_normal = camera->get_global_transform().basis.get_column(2); + vertex_snap_drag_plane = Plane(cam_normal, vertex_snap_source); + } else { + if (has_subgizmos) { + Transform3D gi = selected->get_global_transform().affine_inverse(); + Vector3 local_target = gi.xform(vertex_snap_source); + + Vector ids; + Vector restores; + for (const KeyValue &GE : se->subgizmos) { + Transform3D original_xform = se->gizmo->get_subgizmo_transform(GE.key); + ids.push_back(GE.key); + restores.push_back(original_xform); + + Transform3D snapped_xform = original_xform; + snapped_xform.origin = local_target; + se->gizmo->set_subgizmo_transform(GE.key, snapped_xform); + } + se->gizmo->commit_subgizmos(ids, restores, false); + } else { + EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); + undo_redo->create_action(TTR("Vertex Snap")); + for (const KeyValue &E2 : vertex_snap_original_positions) { + Node3D *node = ObjectDB::get_instance(E2.key); + if (node) { + node->set_global_position(vertex_snap_source); + undo_redo->add_do_method(node, "set_global_position", vertex_snap_source); + undo_redo->add_undo_method(node, "set_global_position", E2.value); + } + } + undo_redo->commit_action(false); + } + vertex_snap_original_positions.clear(); + spatial_editor->update_transform_gizmo(); + _reset_follow_mode_count(); + } + surface->queue_redraw(); + } + } else { + if (vertex_snap_dragging) { + _vertex_snap_commit(); + } + } + return; + } + } + } + + EditorPlugin::AfterGUIInput after = EditorPlugin::AFTER_GUI_INPUT_PASS; + { + EditorNode *en = EditorNode::get_singleton(); + Camera3D *input_camera = previewing ? previewing : camera; + + switch (en->get_editor_plugins_force_input_forwarding()->forward_3d_gui_input(input_camera, p_event, true)) { + case EditorPlugin::AFTER_GUI_INPUT_PASS: { + // Continue processing. + } break; + + case EditorPlugin::AFTER_GUI_INPUT_STOP: { + return; // Stop processing. + } break; + + case EditorPlugin::AFTER_GUI_INPUT_CUSTOM: { + after = EditorPlugin::AFTER_GUI_INPUT_CUSTOM; + } break; + } + + switch (en->get_editor_plugins_over()->forward_3d_gui_input(input_camera, p_event, false)) { + case EditorPlugin::AFTER_GUI_INPUT_PASS: { + // Continue processing. + } break; + + case EditorPlugin::AFTER_GUI_INPUT_STOP: { + return; // Stop processing. + } break; + + case EditorPlugin::AFTER_GUI_INPUT_CUSTOM: { + after = EditorPlugin::AFTER_GUI_INPUT_CUSTOM; + } break; + } + } + + // Several parts of the 3D navigation are handled here. + bool was_navigating = view_3d_controller->is_navigating(); + view_3d_controller->gui_input(p_event, surface->get_global_rect()); + if (was_navigating && !view_3d_controller->is_navigating()) { + return; + } + + Ref b = p_event; + + if (b.is_valid()) { + emit_signal(SNAME("clicked")); + + View3DController::NavigationMouseButton orbit_mouse_preference = (View3DController::NavigationMouseButton)EDITOR_GET("editors/3d/navigation/orbit_mouse_button").operator int(); + View3DController::NavigationMouseButton pan_mouse_preference = (View3DController::NavigationMouseButton)EDITOR_GET("editors/3d/navigation/pan_mouse_button").operator int(); + View3DController::NavigationMouseButton zoom_mouse_preference = (View3DController::NavigationMouseButton)EDITOR_GET("editors/3d/navigation/zoom_mouse_button").operator int(); + + switch (b->get_button_index()) { + case MouseButton::RIGHT: { + if (b->is_pressed()) { + if (_edit.gizmo.is_valid()) { + // Restore. + _edit.gizmo->commit_handle(_edit.gizmo_handle, _edit.gizmo_handle_secondary, _edit.gizmo_initial_value, true); + _edit.gizmo = Ref(); + set_message(""); + } + + if (_edit.mode == TRANSFORM_NONE) { + if (orbit_mouse_preference == View3DController::NAV_MOUSE_BUTTON_RIGHT && _is_nav_modifier_pressed("spatial_editor/viewport_orbit_modifier_1") && _is_nav_modifier_pressed("spatial_editor/viewport_orbit_modifier_2")) { + break; + } else if (pan_mouse_preference == View3DController::NAV_MOUSE_BUTTON_RIGHT && _is_nav_modifier_pressed("spatial_editor/viewport_pan_modifier_1") && _is_nav_modifier_pressed("spatial_editor/viewport_pan_modifier_2")) { + break; + } else if (zoom_mouse_preference == View3DController::NAV_MOUSE_BUTTON_RIGHT && _is_nav_modifier_pressed("spatial_editor/viewport_zoom_modifier_1") && _is_nav_modifier_pressed("spatial_editor/viewport_zoom_modifier_2")) { + break; + } + } + + if (b->is_alt_pressed()) { + _list_select(b); + return; + } + + if (_edit.mode != TRANSFORM_NONE) { + cancel_transform(); + break; + } + + const Key mod = _get_key_modifier(b); + if (!view_3d_controller->is_orthogonal() && !(previewing && !pilot_preview_enabled)) { + if (mod == _get_key_modifier_setting("editors/3d/freelook/freelook_activation_modifier")) { + view_3d_controller->set_freelook_enabled(true); + } + } + } else { + view_3d_controller->set_freelook_enabled(false); + } + + if (view_3d_controller->is_freelook_enabled() && !surface->has_focus()) { + // Focus usually doesn't trigger on right-click, but in case of freelook it should, + // otherwise using keyboard navigation would misbehave + surface->grab_focus(); + } + + } break; + case MouseButton::MIDDLE: { + if (b->is_pressed() && _edit.mode != TRANSFORM_NONE) { + if (orbit_mouse_preference == View3DController::NAV_MOUSE_BUTTON_MIDDLE && _is_nav_modifier_pressed("spatial_editor/viewport_orbit_modifier_1") && _is_nav_modifier_pressed("spatial_editor/viewport_orbit_modifier_2")) { + break; + } else if (pan_mouse_preference == View3DController::NAV_MOUSE_BUTTON_MIDDLE && _is_nav_modifier_pressed("spatial_editor/viewport_pan_modifier_1") && _is_nav_modifier_pressed("spatial_editor/viewport_pan_modifier_2")) { + break; + } else if (zoom_mouse_preference == View3DController::NAV_MOUSE_BUTTON_MIDDLE && _is_nav_modifier_pressed("spatial_editor/viewport_zoom_modifier_1") && _is_nav_modifier_pressed("spatial_editor/viewport_zoom_modifier_2")) { + break; + } + + switch (_edit.plane) { + case TRANSFORM_VIEW: { + _edit.plane = TRANSFORM_X_AXIS; + set_message(TTR("X-Axis Transform."), 2); + view_3d_controller->set_view_type(View3DController::VIEW_TYPE_USER); + } break; + case TRANSFORM_X_AXIS: { + _edit.plane = TRANSFORM_Y_AXIS; + set_message(TTR("Y-Axis Transform."), 2); + + } break; + case TRANSFORM_Y_AXIS: { + _edit.plane = TRANSFORM_Z_AXIS; + set_message(TTR("Z-Axis Transform."), 2); + + } break; + case TRANSFORM_Z_AXIS: { + _edit.plane = TRANSFORM_VIEW; + // TRANSLATORS: This refers to the transform of the view plane. + set_message(TTR("View Plane Transform."), 2); + + } break; + case TRANSFORM_YZ: + case TRANSFORM_XZ: + case TRANSFORM_XY: { + } break; + } + } + } break; + case MouseButton::LEFT: { + if (b->is_pressed()) { + clicked_wants_append = b->is_shift_pressed(); + + if (_edit.mode != TRANSFORM_NONE && (_edit.instant || collision_reposition)) { + commit_transform(); + break; // just commit the edit, stop processing the event so we don't deselect the object + } + if (orbit_mouse_preference == View3DController::NAV_MOUSE_BUTTON_LEFT && _is_nav_modifier_pressed("spatial_editor/viewport_orbit_modifier_1") && _is_nav_modifier_pressed("spatial_editor/viewport_orbit_modifier_2")) { + break; + } else if (pan_mouse_preference == View3DController::NAV_MOUSE_BUTTON_LEFT && _is_nav_modifier_pressed("spatial_editor/viewport_pan_modifier_1") && _is_nav_modifier_pressed("spatial_editor/viewport_pan_modifier_2")) { + break; + } else if (zoom_mouse_preference == View3DController::NAV_MOUSE_BUTTON_LEFT && _is_nav_modifier_pressed("spatial_editor/viewport_zoom_modifier_1") && _is_nav_modifier_pressed("spatial_editor/viewport_zoom_modifier_2")) { + break; + } + + if (spatial_editor->get_tool_mode() == Node3DEditor::TOOL_RULER) { + EditorNode::get_singleton()->get_scene_root()->add_child(ruler); + collision_reposition = true; + break; + } + + if (spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_LIST_SELECT) { + _list_select(b); + break; + } + + _edit.mouse_pos = b->get_position(); + _edit.original_mouse_pos = b->get_position(); + _edit.mode = TRANSFORM_NONE; + _edit.original = spatial_editor->get_gizmo_transform(); // To prevent to break when flipping with scale. + + bool can_select_gizmos = spatial_editor->get_single_selected_node(); + + { + int idx = view_display_menu->get_popup()->get_item_index(VIEW_GIZMOS); + int idx2 = view_display_menu->get_popup()->get_item_index(VIEW_TRANSFORM_GIZMO); + can_select_gizmos = can_select_gizmos && view_display_menu->get_popup()->is_item_checked(idx); + transform_gizmo_visible = view_display_menu->get_popup()->is_item_checked(idx2); + } + + // Gizmo handles + if (can_select_gizmos) { + Vector> gizmos = spatial_editor->get_single_selected_node()->get_gizmos(); + + bool intersected_handle = false; + for (int i = 0; i < gizmos.size(); i++) { + Ref seg = gizmos[i]; + + if (seg.is_null()) { + continue; + } + + int gizmo_handle = -1; + bool gizmo_secondary = false; + seg->handles_intersect_ray(camera, _edit.mouse_pos, b->is_shift_pressed(), gizmo_handle, gizmo_secondary); + if (gizmo_handle != -1) { + _edit.gizmo = seg; + seg->begin_handle_action(gizmo_handle, gizmo_secondary); + _edit.gizmo_handle = gizmo_handle; + _edit.gizmo_handle_secondary = gizmo_secondary; + _edit.gizmo_initial_value = seg->get_handle_value(gizmo_handle, gizmo_secondary); + intersected_handle = true; + break; + } + } + + if (intersected_handle) { + break; + } + } + + // Transform gizmo + if (transform_gizmo_visible && _transform_gizmo_select(_edit.mouse_pos)) { + break; + } + + // Subgizmos + if (can_select_gizmos) { + Node3DEditorSelectedItem *se = editor_selection->get_node_editor_data(spatial_editor->get_single_selected_node()); + Vector> gizmos = spatial_editor->get_single_selected_node()->get_gizmos(); + + bool intersected_subgizmo = false; + for (int i = 0; i < gizmos.size(); i++) { + Ref seg = gizmos[i]; + + if (seg.is_null()) { + continue; + } + + int subgizmo_id = seg->subgizmos_intersect_ray(camera, _edit.mouse_pos); + if (subgizmo_id != -1) { + ERR_CONTINUE(!se); + if (b->is_shift_pressed()) { + if (se->subgizmos.has(subgizmo_id)) { + se->subgizmos.erase(subgizmo_id); + } else { + se->subgizmos.insert(subgizmo_id, seg->get_subgizmo_transform(subgizmo_id)); + } + } else { + se->subgizmos.clear(); + se->subgizmos.insert(subgizmo_id, seg->get_subgizmo_transform(subgizmo_id)); + } + + if (se->subgizmos.is_empty()) { + se->gizmo = Ref(); + } else { + se->gizmo = seg; + } + + seg->redraw(); + spatial_editor->update_transform_gizmo(); + _reset_follow_mode_count(); + intersected_subgizmo = true; + break; + } + } + + if (intersected_subgizmo) { + break; + } + } + + clicked = ObjectID(); + + bool node_selected = get_selected_count() > 0; + + if (after != EditorPlugin::AFTER_GUI_INPUT_CUSTOM) { + // Single item selection. + clicked = _select_ray(b->get_position()); + + if (clicked.is_valid() && !editor_selection->is_selected(ObjectDB::get_instance(clicked))) { + if (!node_selected || (!b->is_alt_pressed() && !(spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_TRANSFORM && b->is_command_or_control_pressed()))) { + selection_in_progress = true; + break; + } + } + + if (clicked.is_null()) { + if (node_selected) { + TransformMode mode = TRANSFORM_NONE; + + if (spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_TRANSFORM) { + if (b->is_command_or_control_pressed()) { + mode = TRANSFORM_ROTATE; + } else if (b->is_alt_pressed()) { + mode = TRANSFORM_TRANSLATE; + } + } else if (b->is_alt_pressed()) { + switch (spatial_editor->get_tool_mode()) { + case Node3DEditor::TOOL_MODE_ROTATE: + mode = TRANSFORM_ROTATE; + break; + case Node3DEditor::TOOL_MODE_MOVE: + mode = TRANSFORM_TRANSLATE; + break; + case Node3DEditor::TOOL_MODE_SCALE: + mode = TRANSFORM_SCALE; + break; + default: + break; + } + } + + if (mode != TRANSFORM_NONE) { + begin_transform(mode, false); + break; + } + } + + if (!previewing) { + // Default to region select. + view_3d_controller->cursor.region_select = true; + view_3d_controller->cursor.region_begin = b->get_position(); + view_3d_controller->cursor.region_end = b->get_position(); + } + break; + } + } + + if (clicked.is_valid() && !clicked_wants_append) { + bool is_clicked_node_selected = editor_selection->is_selected(ObjectDB::get_instance(clicked)); + TransformMode mode = TRANSFORM_NONE; + + switch (spatial_editor->get_tool_mode()) { + case Node3DEditor::TOOL_MODE_TRANSFORM: + if (b->is_command_or_control_pressed() && node_selected) { + mode = TRANSFORM_ROTATE; + } else if (b->is_alt_pressed() && node_selected) { + mode = TRANSFORM_TRANSLATE; + } else if (is_clicked_node_selected) { + mode = TRANSFORM_TRANSLATE; + } + break; + case Node3DEditor::TOOL_MODE_ROTATE: + if (is_clicked_node_selected || (b->is_alt_pressed() && node_selected)) { + mode = TRANSFORM_ROTATE; + } + break; + case Node3DEditor::TOOL_MODE_MOVE: + if (is_clicked_node_selected || (b->is_alt_pressed() && node_selected)) { + mode = TRANSFORM_TRANSLATE; + } + break; + case Node3DEditor::TOOL_MODE_SCALE: + if (is_clicked_node_selected || (b->is_alt_pressed() && node_selected)) { + mode = TRANSFORM_SCALE; + } + break; + default: + break; + } + + if (mode != TRANSFORM_NONE) { + begin_transform(mode, false); + break; + } + } + + surface->queue_redraw(); + } else { + if (ruler->is_inside_tree()) { + EditorNode::get_singleton()->get_scene_root()->remove_child(ruler); + ruler_start_point->set_visible(false); + ruler_end_point->set_visible(false); + ruler_label->set_visible(false); + ruler_label_x->set_visible(false); + ruler_label_y->set_visible(false); + ruler_label_z->set_visible(false); + collision_reposition = false; + break; + } + + if (_edit.gizmo.is_valid()) { + // Certain gizmo plugins should be able to commit handles without dragging them. + if (_edit.original_mouse_pos != _edit.mouse_pos || _edit.gizmo->get_plugin()->can_commit_handle_on_click()) { + _edit.gizmo->commit_handle(_edit.gizmo_handle, _edit.gizmo_handle_secondary, _edit.gizmo_initial_value, false); + } + Node3D *selected_node = spatial_editor->get_single_selected_node(); + if (selected_node) { + selected_node->update_gizmos(); + } + _edit.gizmo = Ref(); + set_message(""); + break; + } + + if (after != EditorPlugin::AFTER_GUI_INPUT_CUSTOM) { + selection_in_progress = false; + + if (clicked.is_valid() && _edit.mode == TRANSFORM_NONE) { + _select_clicked(false); + } else if (view_3d_controller->cursor.region_select) { + _select_region(); + surface->queue_redraw(); + } + + movement_threshold_passed = false; + view_3d_controller->cursor.region_select = false; + } + + if (!_edit.instant && _edit.mode != TRANSFORM_NONE) { + Node3D *selected = spatial_editor->get_single_selected_node(); + Node3DEditorSelectedItem *se = selected ? editor_selection->get_node_editor_data(selected) : nullptr; + + if (se && se->gizmo.is_valid()) { + Vector ids; + Vector restore; + + for (const KeyValue &GE : se->subgizmos) { + ids.push_back(GE.key); + restore.push_back(GE.value); + } + + se->gizmo->commit_subgizmos(ids, restore, false); + finish_transform(); + } else { + if (_edit.original_mouse_pos != _edit.mouse_pos) { + commit_transform(); + } + } + _edit.mode = TRANSFORM_NONE; + set_message(""); + spatial_editor->update_transform_gizmo(); + } + } + + } break; + default: + break; + } + } + + Ref m = p_event; + + // Instant transforms process mouse motion in input() to handle wrapping. + if (m.is_valid() && !_edit.instant) { + _edit.mouse_pos = m->get_position(); + + if (vertex_snap_mode || vertex_snap_dragging) { + if (!vertex_snap_dragging) { + _vertex_snap_update_source(m->get_position()); + } else { + const float vertex_snap_threshold = VERTEX_SNAP_THRESHOLD * EDSCALE; + bool has_displacement = false; + Vector3 displacement; + + Vector3 target; + if (_find_closest_vertex_in_scene(m->get_position(), vertex_snap_threshold, target, &vertex_snap_original_positions)) { + vertex_snap_target = target; + vertex_snap_has_target = true; + displacement = target - vertex_snap_source; + has_displacement = true; + set_message(TTR("Vertex Snap (Snapped)")); + } else { + vertex_snap_has_target = false; + Vector3 ray_pos = get_ray_pos(m->get_position()); + Vector3 ray_dir = get_ray(m->get_position()); + Vector3 intersection; + if (vertex_snap_drag_plane.intersects_ray(ray_pos, ray_dir, &intersection)) { + displacement = intersection - vertex_snap_source; + has_displacement = true; + } + set_message(TTR("Vertex Snap")); + } + + if (has_displacement) { + for (const KeyValue &E : vertex_snap_original_positions) { + Node3D *node = ObjectDB::get_instance(E.key); + if (node) { + node->set_global_position(E.value + displacement); + } + } + } + } + surface->queue_redraw(); + return; + } + + if (!view_3d_controller->is_freelook_enabled() && spatial_editor->get_single_selected_node()) { + Vector> gizmos = spatial_editor->get_single_selected_node()->get_gizmos(); + + Ref found_gizmo; + int found_handle = -1; + bool found_handle_secondary = false; + + for (int i = 0; i < gizmos.size(); i++) { + Ref seg = gizmos[i]; + if (seg.is_null()) { + continue; + } + + seg->handles_intersect_ray(camera, _edit.mouse_pos, false, found_handle, found_handle_secondary); + + if (found_handle != -1) { + found_gizmo = seg; + break; + } + } + + if (found_gizmo.is_valid()) { + spatial_editor->select_gizmo_highlight_axis(-1); + } + + bool current_hover_handle_secondary = false; + int current_hover_handle = spatial_editor->get_current_hover_gizmo_handle(current_hover_handle_secondary); + if (found_gizmo != spatial_editor->get_current_hover_gizmo() || found_handle != current_hover_handle || found_handle_secondary != current_hover_handle_secondary) { + spatial_editor->set_current_hover_gizmo(found_gizmo); + spatial_editor->set_current_hover_gizmo_handle(found_handle, found_handle_secondary); + spatial_editor->get_single_selected_node()->update_gizmos(); + } + } + + if (!view_3d_controller->is_freelook_enabled() && transform_gizmo_visible && spatial_editor->get_current_hover_gizmo().is_null() && !m->get_button_mask().has_flag(MouseButtonMask::LEFT) && _edit.gizmo.is_null()) { + _transform_gizmo_select(_edit.mouse_pos, true); + } + + if (_edit.gizmo.is_valid()) { + _edit.gizmo->set_handle(_edit.gizmo_handle, _edit.gizmo_handle_secondary, camera, m->get_position()); + Variant v = _edit.gizmo->get_handle_value(_edit.gizmo_handle, _edit.gizmo_handle_secondary); + String n = _edit.gizmo->get_handle_name(_edit.gizmo_handle, _edit.gizmo_handle_secondary); + set_message(n + ": " + String(v)); + + } else if (m->get_button_mask().has_flag(MouseButtonMask::LEFT)) { + movement_threshold_passed = _edit.original_mouse_pos.distance_to(_edit.mouse_pos) > 8 * EDSCALE; + + if ((selection_in_progress || clicked_wants_append || spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_SELECT) && movement_threshold_passed && clicked.is_valid() && !previewing) { + view_3d_controller->cursor.region_select = true; + view_3d_controller->cursor.region_begin = _edit.original_mouse_pos; + clicked = ObjectID(); + } + + if (view_3d_controller->cursor.region_select) { + view_3d_controller->cursor.region_end = m->get_position(); + surface->queue_redraw(); + return; + } + + if (clicked.is_valid() && movement_threshold_passed && (spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_TRANSFORM || spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_MOVE)) { + bool is_select_mode = (spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_TRANSFORM); + bool is_clicked_selected = editor_selection->is_selected(ObjectDB::get_instance(clicked)); + + if (_edit.mode == TRANSFORM_NONE && (is_select_mode || is_clicked_selected) && _has_unlocked_selection()) { + _compute_edit(_edit.original_mouse_pos); + clicked = ObjectID(); + _edit.mode = TRANSFORM_TRANSLATE; + } + } + + if (_edit.mode == TRANSFORM_NONE || _edit.numeric_input != 0 || _edit.numeric_next_decimal != 0) { + return; + } + + if (!selection_in_progress) { + update_transform(_get_key_modifier(m) == Key::SHIFT); + } + } + } + + if (k.is_valid()) { + if (!k->is_pressed()) { + return; + } + + if (_edit.instant) { + // In a Blender-style transform, numbers set the magnitude of the transform. + // E.g. pressing g4.5x means "translate 4.5 units along the X axis". + bool processed = true; + Key keycode = k->get_keycode(); + Key physical_keycode = k->get_physical_keycode(); + + // Use physical keycode for main keyboard numbers (for non-QWERTY layouts like AZERTY) + // but regular keycode for numpad numbers. + if ((physical_keycode >= Key::KEY_0 && physical_keycode <= Key::KEY_9) || (keycode >= Key::KP_0 && keycode <= Key::KP_9)) { + uint32_t value; + if (physical_keycode >= Key::KEY_0 && physical_keycode <= Key::KEY_9) { + value = uint32_t(physical_keycode - Key::KEY_0); + } else { + value = uint32_t(keycode - Key::KP_0); + } + + if (_edit.numeric_next_decimal < 0) { + _edit.numeric_input = _edit.numeric_input + value * Math::pow(10.0, _edit.numeric_next_decimal--); + } else { + _edit.numeric_input = _edit.numeric_input * 10 + value; + } + update_transform_numeric(); + } else if (keycode == Key::MINUS || keycode == Key::KP_SUBTRACT) { + _edit.numeric_negate = !_edit.numeric_negate; + update_transform_numeric(); + } else if (keycode == Key::PERIOD || physical_keycode == Key::KP_PERIOD) { + // Use physical keycode for KP_PERIOD to ensure numpad period works consistently + // across different keyboard layouts (like nordic keyboards). + if (_edit.numeric_next_decimal == 0) { + _edit.numeric_next_decimal = -1; + } + } else if (keycode == Key::ENTER || keycode == Key::KP_ENTER || keycode == Key::SPACE) { + commit_transform(); + } else { + processed = false; + } + + if (processed) { + // Ignore mouse inputs once we receive a numeric input. + set_process_input(false); + accept_event(); + return; + } + } + + Ref event_mod = p_event; + const Key code = view_3d_controller->emulate_numpad_key(k->get_physical_keycode()); + if (code != k->get_physical_keycode()) { + event_mod = p_event->duplicate(); + Ref k_mod = event_mod; + k_mod->set_keycode(code); + } + + if (_edit.mode == TRANSFORM_NONE) { + if (_edit.gizmo.is_null() && view_3d_controller->is_freelook_enabled() && k->get_keycode() == Key::ESCAPE) { + view_3d_controller->set_freelook_enabled(false); + return; + } + + if (_edit.gizmo.is_valid() && (k->get_keycode() == Key::ESCAPE || k->get_keycode() == Key::BACKSPACE)) { + // Restore. + _edit.gizmo->commit_handle(_edit.gizmo_handle, _edit.gizmo_handle_secondary, _edit.gizmo_initial_value, true); + _edit.gizmo = Ref(); + set_message(""); + } + if (k->get_keycode() == Key::ESCAPE && !view_3d_controller->cursor.region_select && !k->is_echo()) { + _clear_selected(); + return; + } + } else { + // We're actively transforming, handle keys specially + TransformPlane new_plane = TRANSFORM_VIEW; + if (ED_IS_SHORTCUT("spatial_editor/lock_transform_x", event_mod)) { + new_plane = TRANSFORM_X_AXIS; + } else if (ED_IS_SHORTCUT("spatial_editor/lock_transform_y", event_mod)) { + new_plane = TRANSFORM_Y_AXIS; + } else if (ED_IS_SHORTCUT("spatial_editor/lock_transform_z", event_mod)) { + new_plane = TRANSFORM_Z_AXIS; + } else if (_edit.mode != TRANSFORM_ROTATE) { // rotating on a plane doesn't make sense + if (ED_IS_SHORTCUT("spatial_editor/lock_transform_yz", event_mod)) { + new_plane = TRANSFORM_YZ; + } else if (ED_IS_SHORTCUT("spatial_editor/lock_transform_xz", event_mod)) { + new_plane = TRANSFORM_XZ; + } else if (ED_IS_SHORTCUT("spatial_editor/lock_transform_xy", event_mod)) { + new_plane = TRANSFORM_XY; + } + } + + if (new_plane != TRANSFORM_VIEW) { + if (new_plane != _edit.plane) { + // lock me once and get a global constraint + _edit.plane = new_plane; + spatial_editor->set_local_coords_enabled(false); + } else if (!spatial_editor->are_local_coords_enabled()) { + // lock me twice and get a local constraint + spatial_editor->set_local_coords_enabled(true); + } else { + // lock me thrice and we're back where we started + _edit.plane = TRANSFORM_VIEW; + spatial_editor->set_local_coords_enabled(false); + } + if (_edit.numeric_input != 0 || _edit.numeric_next_decimal != 0) { + update_transform_numeric(); + } else { + update_transform(Input::get_singleton()->is_key_pressed(Key::SHIFT)); + } + accept_event(); + return; + } + } + if (ED_IS_SHORTCUT("spatial_editor/bottom_view", event_mod)) { + _menu_option(VIEW_BOTTOM); + } + if (ED_IS_SHORTCUT("spatial_editor/top_view", event_mod)) { + _menu_option(VIEW_TOP); + } + if (ED_IS_SHORTCUT("spatial_editor/rear_view", event_mod)) { + _menu_option(VIEW_REAR); + } + if (ED_IS_SHORTCUT("spatial_editor/front_view", event_mod)) { + _menu_option(VIEW_FRONT); + } + if (ED_IS_SHORTCUT("spatial_editor/left_view", event_mod)) { + _menu_option(VIEW_LEFT); + } + if (ED_IS_SHORTCUT("spatial_editor/right_view", event_mod)) { + _menu_option(VIEW_RIGHT); + } + if (ED_IS_SHORTCUT("spatial_editor/orbit_view_down", event_mod)) { + // Clamp rotation to roughly -90..90 degrees so the user can't look upside-down and end up disoriented. + view_3d_controller->cursor.x_rot = CLAMP(view_3d_controller->cursor.x_rot - Math::PI / 12.0, -1.57, 1.57); + view_3d_controller->cursor.unsnapped_x_rot = view_3d_controller->cursor.x_rot; + view_3d_controller->set_view_type(View3DController::VIEW_TYPE_USER); + } + if (ED_IS_SHORTCUT("spatial_editor/orbit_view_up", event_mod)) { + // Clamp rotation to roughly -90..90 degrees so the user can't look upside-down and end up disoriented. + view_3d_controller->cursor.x_rot = CLAMP(view_3d_controller->cursor.x_rot + Math::PI / 12.0, -1.57, 1.57); + view_3d_controller->cursor.unsnapped_x_rot = view_3d_controller->cursor.x_rot; + view_3d_controller->set_view_type(View3DController::VIEW_TYPE_USER); + } + if (ED_IS_SHORTCUT("spatial_editor/orbit_view_right", event_mod)) { + view_3d_controller->cursor.y_rot -= Math::PI / 12.0; + view_3d_controller->cursor.unsnapped_y_rot = view_3d_controller->cursor.y_rot; + view_3d_controller->set_view_type(View3DController::VIEW_TYPE_USER); + } + if (ED_IS_SHORTCUT("spatial_editor/orbit_view_left", event_mod)) { + view_3d_controller->cursor.y_rot += Math::PI / 12.0; + view_3d_controller->cursor.unsnapped_y_rot = view_3d_controller->cursor.y_rot; + view_3d_controller->set_view_type(View3DController::VIEW_TYPE_USER); + } + if (ED_IS_SHORTCUT("spatial_editor/orbit_view_180", event_mod)) { + view_3d_controller->cursor.y_rot += Math::PI; + view_3d_controller->cursor.unsnapped_y_rot = view_3d_controller->cursor.y_rot; + view_3d_controller->set_view_type(View3DController::VIEW_TYPE_USER); + } + if (ED_IS_SHORTCUT("spatial_editor/focus_origin", event_mod)) { + _menu_option(VIEW_CENTER_TO_ORIGIN); + } + if (ED_IS_SHORTCUT("spatial_editor/focus_selection", event_mod)) { + _menu_option(VIEW_CENTER_TO_SELECTION); + times_focused_consecutively += 1; + } + if (ED_IS_SHORTCUT("spatial_editor/align_transform_with_view", event_mod)) { + _menu_option(VIEW_ALIGN_TRANSFORM_WITH_VIEW); + } + if (ED_IS_SHORTCUT("spatial_editor/align_rotation_with_view", event_mod)) { + _menu_option(VIEW_ALIGN_ROTATION_WITH_VIEW); + } + if (ED_IS_SHORTCUT("spatial_editor/insert_anim_key", event_mod)) { + if (!get_selected_count() || _edit.mode != TRANSFORM_NONE) { + return; + } + + if (!AnimationPlayerEditor::get_singleton()->get_track_editor()->has_keying()) { + set_message(TTR("Keying is disabled (no key inserted).")); + return; + } + + const List &selection = editor_selection->get_top_selected_node_list(); + + for (Node *E : selection) { + Node3D *sp = Object::cast_to(E); + if (!sp) { + continue; + } + + spatial_editor->emit_signal(SNAME("transform_key_request"), sp, "", sp->get_transform()); + } + + set_message(TTR("Animation Key Inserted.")); + } + if (ED_IS_SHORTCUT("spatial_editor/cancel_transform", event_mod) && _edit.mode != TRANSFORM_NONE) { + cancel_transform(); + } + if (!view_3d_controller->is_freelook_enabled() && !k->is_echo()) { + if (ED_IS_SHORTCUT("spatial_editor/reset_transform_position", event_mod)) { + _reset_transform(TransformType::POSITION); + } + if (ED_IS_SHORTCUT("spatial_editor/reset_transform_rotation", event_mod)) { + _reset_transform(TransformType::ROTATION); + } + if (ED_IS_SHORTCUT("spatial_editor/reset_transform_scale", event_mod)) { + _reset_transform(TransformType::SCALE); + } + if (ED_IS_SHORTCUT("spatial_editor/instant_translate", event_mod) && (_edit.mode != TRANSFORM_TRANSLATE || collision_reposition)) { + if (_edit.mode == TRANSFORM_NONE) { + begin_transform(TRANSFORM_TRANSLATE, true); + } else if (_edit.instant || collision_reposition) { + commit_transform(); + begin_transform(TRANSFORM_TRANSLATE, true); + } + } + if (ED_IS_SHORTCUT("spatial_editor/instant_rotate", event_mod)) { + if (_edit.mode == TRANSFORM_ROTATE && _edit.instant) { + _edit.is_trackball = !_edit.is_trackball; + _edit.show_rotation_line = !_edit.is_trackball; + _edit.plane = TRANSFORM_VIEW; + _edit.original_mouse_pos = _edit.mouse_pos; + if (_edit.is_trackball) { + set_message(TTR("Trackball Rotation")); + } else { + _edit.initial_click_vector = Vector3(); + _edit.previous_rotation_vector = Vector3(); + _edit.accumulated_rotation_angle = 0.0; + _edit.rotation_angle = 0.0; + set_message(vformat(TTR("Rotating %s degrees."), String::num(0, 0))); + } + surface->queue_redraw(); + } else if (_edit.mode != TRANSFORM_ROTATE) { + if (_edit.mode == TRANSFORM_NONE) { + begin_transform(TRANSFORM_ROTATE, true); + } else if (_edit.instant || collision_reposition) { + commit_transform(); + begin_transform(TRANSFORM_ROTATE, true); + } + } + } + if (ED_IS_SHORTCUT("spatial_editor/instant_scale", event_mod) && _edit.mode != TRANSFORM_SCALE) { + if (_edit.mode == TRANSFORM_NONE) { + begin_transform(TRANSFORM_SCALE, true); + } else if (_edit.instant || collision_reposition) { + commit_transform(); + begin_transform(TRANSFORM_SCALE, true); + } + } + if (ED_IS_SHORTCUT("spatial_editor/collision_reposition", event_mod) && editor_selection->get_top_selected_node_list().size() == 1 && !collision_reposition && _has_unlocked_selection()) { + if (_edit.mode == TRANSFORM_NONE || _edit.instant) { + if (_edit.mode == TRANSFORM_NONE) { + _compute_edit(_edit.mouse_pos); + } else { + commit_transform(); + _compute_edit(_edit.mouse_pos); + } + _edit.mode = TRANSFORM_TRANSLATE; + collision_reposition = true; + } + } + } + + // Freelook doesn't work in orthogonal mode or when previewing without pilot mode. + if (!view_3d_controller->is_orthogonal() && !(previewing && !pilot_preview_enabled) && ED_IS_SHORTCUT("spatial_editor/freelook_toggle", event_mod)) { + view_3d_controller->set_freelook_enabled(!view_3d_controller->is_freelook_enabled()); + } else if (k->get_keycode() == Key::ESCAPE) { + view_3d_controller->set_freelook_enabled(false); + } + + if (k->get_keycode() == Key::SPACE) { + if (!k->is_pressed()) { + emit_signal(SNAME("toggle_maximize_view"), this); + } + } + } + + // Freelook uses most of the useful shortcuts, like save, so its OK + // to consider freelook active as end of the line for future events. + if (view_3d_controller->is_freelook_enabled()) { + accept_event(); + } +} + +void Node3DEditorViewport::_cursor_interpolated() { + last_camera_transform = view_3d_controller->interp_to_camera_transform(); + + if (previewing_camera && previewing && pilot_preview_enabled) { + _pilot_ensure_undo_session(); + previewing->set_global_transform(last_camera_transform); + pilot_undo_idle_time = 0.0; + } else if (!previewing_camera) { + camera->set_global_transform(last_camera_transform); + } + + if (view_3d_controller->is_orthogonal()) { + float half_fov = Math::deg_to_rad(get_fov()) / 2.0; + float height = 2.0 * view_3d_controller->cursor.distance * Math::tan(half_fov); + camera->set_orthogonal(height, get_znear(), get_zfar()); + } else { + camera->set_perspective(get_fov(), get_znear(), get_zfar()); + } + + view_3d_controller->set_z_near(get_znear()); + view_3d_controller->set_z_far(get_zfar()); + + update_transform_gizmo_view(); + rotation_control->queue_redraw(); + position_control->queue_redraw(); + look_control->queue_redraw(); + surface->queue_redraw(); + spatial_editor->update_grid(); +} + +void Node3DEditorViewport::_cursor_distance_scaled() { + zoom_indicator_delay = ZOOM_FREELOOK_INDICATOR_DELAY_S; + surface->queue_redraw(); +} + +void Node3DEditorViewport::_freelook_changed() { + spatial_editor->set_freelook_viewport(view_3d_controller->is_freelook_enabled() ? this : nullptr); +} + +void Node3DEditorViewport::_pilot_ensure_undo_session() { + if (pilot_undo_session_active || !previewing) { + return; + } + pilot_undo_initial_transform = previewing->get_global_transform(); + pilot_undo_session_active = true; + pilot_undo_idle_time = 0.0; +} + +void Node3DEditorViewport::_pilot_commit_undo_session() { + if (!pilot_undo_session_active) { + return; + } + pilot_undo_session_active = false; + pilot_undo_idle_time = 0.0; + if (!previewing) { + return; + } + const Transform3D current_transform = previewing->get_global_transform(); + if (current_transform.is_equal_approx(pilot_undo_initial_transform)) { + return; + } + EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); + undo_redo->create_action(vformat(TTR("Move Camera \"%s\""), previewing->get_name()), UndoRedo::MERGE_ENDS); + undo_redo->add_do_method(previewing, "set_global_transform", current_transform); + undo_redo->add_undo_method(previewing, "set_global_transform", pilot_undo_initial_transform); + undo_redo->commit_action(false); +} + +void Node3DEditorViewport::_pilot_tick_undo_session(real_t p_delta) { + if (!pilot_undo_session_active) { + return; + } + pilot_undo_idle_time += p_delta; + if (pilot_undo_idle_time > 0.15) { + _pilot_commit_undo_session(); + } +} + +void Node3DEditorViewport::_freelook_speed_scaled() { + zoom_indicator_delay = ZOOM_FREELOOK_INDICATOR_DELAY_S; + surface->queue_redraw(); +} + +bool Node3DEditorViewport::_is_nav_modifier_pressed(const String &p_name) { + return _is_shortcut_empty(p_name) || Input::get_singleton()->is_action_pressed(p_name); +} + +bool Node3DEditorViewport::_is_shortcut_empty(const String &p_name) { + Ref check_shortcut = ED_GET_SHORTCUT(p_name); + + ERR_FAIL_COND_V_MSG(check_shortcut.is_null(), true, "The Shortcut was null, possible name mismatch."); + + return check_shortcut->get_events().is_empty(); +} + +void Node3DEditorViewport::set_message(const String &p_message, float p_time) { + message = p_message; + message_time = p_time; +} + +void Node3DEditorViewport::_project_settings_changed() { + // Update shadow atlas if changed. + int shadowmap_size = GLOBAL_GET("rendering/lights_and_shadows/positional_shadow/atlas_size"); + bool shadowmap_16_bits = GLOBAL_GET("rendering/lights_and_shadows/positional_shadow/atlas_16_bits"); + int atlas_q0 = GLOBAL_GET("rendering/lights_and_shadows/positional_shadow/atlas_quadrant_0_subdiv"); + int atlas_q1 = GLOBAL_GET("rendering/lights_and_shadows/positional_shadow/atlas_quadrant_1_subdiv"); + int atlas_q2 = GLOBAL_GET("rendering/lights_and_shadows/positional_shadow/atlas_quadrant_2_subdiv"); + int atlas_q3 = GLOBAL_GET("rendering/lights_and_shadows/positional_shadow/atlas_quadrant_3_subdiv"); + + viewport->set_positional_shadow_atlas_size(shadowmap_size); + viewport->set_positional_shadow_atlas_16_bits(shadowmap_16_bits); + viewport->set_positional_shadow_atlas_quadrant_subdiv(0, Viewport::PositionalShadowAtlasQuadrantSubdiv(atlas_q0)); + viewport->set_positional_shadow_atlas_quadrant_subdiv(1, Viewport::PositionalShadowAtlasQuadrantSubdiv(atlas_q1)); + viewport->set_positional_shadow_atlas_quadrant_subdiv(2, Viewport::PositionalShadowAtlasQuadrantSubdiv(atlas_q2)); + viewport->set_positional_shadow_atlas_quadrant_subdiv(3, Viewport::PositionalShadowAtlasQuadrantSubdiv(atlas_q3)); + + // Update MSAA, screen-space AA and debanding if changed + + const int msaa_mode = GLOBAL_GET("rendering/anti_aliasing/quality/msaa_3d"); + viewport->set_msaa_3d(Viewport::MSAA(msaa_mode)); + const int ssaa_mode = GLOBAL_GET("rendering/anti_aliasing/quality/screen_space_aa"); + viewport->set_screen_space_aa(Viewport::ScreenSpaceAA(ssaa_mode)); + const bool use_taa = GLOBAL_GET("rendering/anti_aliasing/quality/use_taa"); + viewport->set_use_taa(use_taa); + + const bool transparent_background = GLOBAL_GET("rendering/viewport/transparent_background"); + viewport->set_transparent_background(transparent_background); + + const bool use_debanding = GLOBAL_GET("rendering/anti_aliasing/quality/use_debanding"); + viewport->set_use_debanding(use_debanding); + + const bool use_occlusion_culling = GLOBAL_GET("rendering/occlusion_culling/use_occlusion_culling"); + viewport->set_use_occlusion_culling(use_occlusion_culling); + + const float mesh_lod_threshold = GLOBAL_GET("rendering/mesh_lod/lod_change/threshold_pixels"); + viewport->set_mesh_lod_threshold(mesh_lod_threshold); + + const Viewport::Scaling3DMode scaling_3d_mode = Viewport::Scaling3DMode(int(GLOBAL_GET("rendering/scaling_3d/mode"))); + viewport->set_scaling_3d_mode(scaling_3d_mode); + + _update_shrink(); + + const float fsr_sharpness = GLOBAL_GET("rendering/scaling_3d/fsr_sharpness"); + viewport->set_fsr_sharpness(fsr_sharpness); + + const float texture_mipmap_bias = GLOBAL_GET("rendering/textures/default_filters/texture_mipmap_bias"); + viewport->set_texture_mipmap_bias(texture_mipmap_bias); + + const Viewport::AnisotropicFiltering anisotropic_filtering_level = Viewport::AnisotropicFiltering(int(GLOBAL_GET("rendering/textures/default_filters/anisotropic_filtering_level"))); + viewport->set_anisotropic_filtering_level(anisotropic_filtering_level); +} + +static void override_label_colors(Control *p_control) { + p_control->begin_bulk_theme_override(); + p_control->add_theme_color_override(SceneStringName(font_color), p_control->get_theme_color(SNAME("font_dark_background_color"), EditorStringName(Editor))); + p_control->add_theme_color_override("font_hover_color", p_control->get_theme_color(SNAME("font_dark_background_hover_color"), EditorStringName(Editor))); + p_control->add_theme_color_override("font_focus_color", p_control->get_theme_color(SNAME("font_dark_background_focus_color"), EditorStringName(Editor))); + p_control->add_theme_color_override("font_pressed_color", p_control->get_theme_color(SNAME("font_dark_background_pressed_color"), EditorStringName(Editor))); + p_control->add_theme_color_override("font_hover_pressed_color", p_control->get_theme_color(SNAME("font_dark_background_hover_pressed_color"), EditorStringName(Editor))); + p_control->end_bulk_theme_override(); +} + +static void override_button_stylebox(Button *p_button, const Ref p_stylebox) { + p_button->begin_bulk_theme_override(); + p_button->add_theme_style_override(CoreStringName(normal), p_stylebox); + p_button->add_theme_style_override("normal_mirrored", p_stylebox); + p_button->add_theme_style_override(SceneStringName(hover), p_stylebox); + p_button->add_theme_style_override("hover_mirrored", p_stylebox); + p_button->add_theme_style_override("hover_pressed", p_stylebox); + p_button->add_theme_style_override("hover_pressed_mirrored", p_stylebox); + p_button->add_theme_style_override(SceneStringName(pressed), p_stylebox); + p_button->add_theme_style_override("pressed_mirrored", p_stylebox); + p_button->add_theme_style_override("focus", p_stylebox); + p_button->add_theme_style_override("focus_mirrored", p_stylebox); + p_button->add_theme_style_override("disabled", p_stylebox); + p_button->add_theme_style_override("disabled_mirrored", p_stylebox); + p_button->end_bulk_theme_override(); +} + +void Node3DEditorViewport::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_TRANSLATION_CHANGED: { + _update_name(); + _update_centered_labels(); + message_time = MIN(message_time, 0.001); // Make it disappear. + + const int item_count = display_submenu->get_item_count(); + for (int i = 0; i < item_count; i++) { + const Array item_data = display_submenu->get_item_metadata(i); + if (item_data.is_empty()) { + continue; + } + + SupportedRenderingMethods rendering_methods = item_data[0]; + String base_tooltip = item_data[1]; + + bool disabled = false; + String disabled_tooltip; + switch (rendering_methods) { + case SupportedRenderingMethods::ALL: + break; + case SupportedRenderingMethods::FORWARD_PLUS_MOBILE: + disabled = OS::get_singleton()->get_current_rendering_method() == "gl_compatibility"; + disabled_tooltip = TTR("This debug draw mode is only supported when using the Forward+ or Mobile renderer."); + break; + case SupportedRenderingMethods::FORWARD_PLUS: + disabled = OS::get_singleton()->get_current_rendering_method() == "gl_compatibility" || OS::get_singleton()->get_current_rendering_method() == "mobile"; + disabled_tooltip = TTR("This debug draw mode is only supported when using the Forward+ renderer."); + break; + } + + display_submenu->set_item_disabled(i, disabled); + String tooltip = TTR(base_tooltip); + if (disabled) { + if (tooltip.is_empty()) { + tooltip = disabled_tooltip; + } else { + tooltip += "\n\n" + disabled_tooltip; + } + } + display_submenu->set_item_tooltip(i, tooltip); + } + } break; + + case NOTIFICATION_READY: { + ProjectSettings::get_singleton()->connect("settings_changed", callable_mp(this, &Node3DEditorViewport::_project_settings_changed)); + _update_navigation_controls_visibility(); + } break; + + case NOTIFICATION_VISIBILITY_CHANGED: { + bool vp_visible = is_visible_in_tree(); + + set_process(vp_visible); + set_physics_process(vp_visible); + + if (vp_visible) { + view_3d_controller->set_orthogonal(view_display_menu->get_popup()->is_item_checked(view_display_menu->get_popup()->get_item_index(VIEW_ORTHOGONAL))); + view_3d_controller->update_camera(); + _update_name(); + } else { + view_3d_controller->set_freelook_enabled(false); + _pilot_commit_undo_session(); + } + callable_mp(this, &Node3DEditorViewport::update_transform_gizmo_view).call_deferred(); + } break; + + case NOTIFICATION_RESIZED: { + callable_mp(this, &Node3DEditorViewport::update_transform_gizmo_view).call_deferred(); + } break; + + case NOTIFICATION_PROCESS: { + if (ruler->is_inside_tree()) { + Vector3 start_pos = ruler_start_point->get_global_position(); + Vector3 end_pos = ruler_end_point->get_global_position(); + + geometry->clear_surfaces(); + geometry->surface_begin(Mesh::PRIMITIVE_LINES); + + Vector3 center = (start_pos + end_pos) / 2; + Vector3 camera_dir = (camera->get_transform().origin - center).normalized(); + real_t offset_distance = 0.01; + + geometry->surface_add_vertex(start_pos + camera_dir * offset_distance); + geometry->surface_add_vertex(end_pos + camera_dir * offset_distance); + geometry->surface_end(); + + geometry_xray->clear_surfaces(); + geometry_xray->surface_begin(Mesh::PRIMITIVE_LINES); + geometry_xray->surface_add_vertex(start_pos); + geometry_xray->surface_add_vertex(end_pos); + geometry_xray->surface_end(); + + float distance = start_pos.distance_to(end_pos); + if (distance < 0.001) { + distance = 0.0; + } + ruler_label->set_text(TranslationServer::get_singleton()->format_number(vformat("%.3f m", distance), _get_locale())); + + Vector2 screen_position = camera->unproject_position(center) - (ruler_label->get_custom_minimum_size() / 2); + ruler_label->set_position(screen_position); + + bool show_components = Input::get_singleton()->is_key_pressed(Key::SHIFT); + + if (show_components) { + Ref triangle_mesh = ruler_triangle_lines->get_mesh(); + Ref triangle_mesh_xray = ruler_triangle_lines_xray->get_mesh(); + + if (triangle_mesh.is_valid() && triangle_mesh_xray.is_valid()) { + Vector3 delta = end_pos - start_pos; + delta = delta.abs(); + const real_t threshold = 0.001; + if (delta.x < threshold) { + delta.x = 0.0; + } + if (delta.y < threshold) { + delta.y = 0.0; + } + if (delta.z < threshold) { + delta.z = 0.0; + } + + Vector3 corner_point; + Color first_line_color; + Color second_line_color; + + Color axis_x_color = get_theme_color(SNAME("axis_x_color"), EditorStringName(Editor)); + Color axis_y_color = get_theme_color(SNAME("axis_y_color"), EditorStringName(Editor)); + Color axis_z_color = get_theme_color(SNAME("axis_z_color"), EditorStringName(Editor)); + + if (delta.x > 0.0 && delta.y > 0.0 && delta.z == 0.0) { + // XY plane + corner_point = Vector3(end_pos.x, start_pos.y, start_pos.z); + first_line_color = axis_x_color; + second_line_color = axis_y_color; + } else if (delta.x > 0.0 && delta.z > 0.0 && delta.y == 0.0) { + // XZ plane + corner_point = Vector3(end_pos.x, start_pos.y, start_pos.z); + first_line_color = axis_x_color; + second_line_color = axis_z_color; + } else if (delta.y > 0.0 && delta.z > 0.0 && delta.x == 0.0) { + // YZ plane + corner_point = Vector3(start_pos.x, start_pos.y, end_pos.z); + first_line_color = axis_z_color; + second_line_color = axis_y_color; + } else if (delta.x > 0.0 && delta.y > 0.0 && delta.z > 0.0) { + // All three axes + corner_point = Vector3(end_pos.x, start_pos.y, start_pos.z); + first_line_color = axis_x_color; + second_line_color = axis_y_color; + } else { + corner_point = end_pos; + first_line_color = axis_x_color; + second_line_color = axis_x_color; + } + + triangle_mesh->clear_surfaces(); + triangle_mesh->surface_begin(Mesh::PRIMITIVE_LINES); + + Vector3 triangle_camera_dir = (camera->get_transform().origin - center).normalized(); + real_t triangle_offset_distance = 0.01; + + triangle_mesh->surface_set_color(first_line_color); + triangle_mesh->surface_add_vertex(start_pos + triangle_camera_dir * triangle_offset_distance); + triangle_mesh->surface_set_color(first_line_color); + triangle_mesh->surface_add_vertex(corner_point + triangle_camera_dir * triangle_offset_distance); + triangle_mesh->surface_set_color(second_line_color); + triangle_mesh->surface_add_vertex(corner_point + triangle_camera_dir * triangle_offset_distance); + triangle_mesh->surface_set_color(second_line_color); + triangle_mesh->surface_add_vertex(end_pos + triangle_camera_dir * triangle_offset_distance); + + triangle_mesh->surface_end(); + + Color first_line_color_xray = first_line_color; + Color second_line_color_xray = second_line_color; + first_line_color_xray.a = 0.15; + second_line_color_xray.a = 0.15; + + triangle_mesh_xray->clear_surfaces(); + triangle_mesh_xray->surface_begin(Mesh::PRIMITIVE_LINES); + + triangle_mesh_xray->surface_set_color(first_line_color_xray); + triangle_mesh_xray->surface_add_vertex(start_pos); + triangle_mesh_xray->surface_set_color(first_line_color_xray); + triangle_mesh_xray->surface_add_vertex(corner_point); + triangle_mesh_xray->surface_set_color(second_line_color_xray); + triangle_mesh_xray->surface_add_vertex(corner_point); + triangle_mesh_xray->surface_set_color(second_line_color_xray); + triangle_mesh_xray->surface_add_vertex(end_pos); + + triangle_mesh_xray->surface_end(); + } + } else { + Ref triangle_mesh = ruler_triangle_lines->get_mesh(); + Ref triangle_mesh_xray = ruler_triangle_lines_xray->get_mesh(); + + if (triangle_mesh.is_valid()) { + triangle_mesh->clear_surfaces(); + } + if (triangle_mesh_xray.is_valid()) { + triangle_mesh_xray->clear_surfaces(); + } + } + + if (show_components) { + Vector3 delta = end_pos - start_pos; + delta = delta.abs(); + const real_t threshold = 0.001; + if (delta.x < threshold) { + delta.x = 0.0; + } + if (delta.y < threshold) { + delta.y = 0.0; + } + if (delta.z < threshold) { + delta.z = 0.0; + } + + int active_axes = 0; + if (delta.x > 0.0) { + active_axes++; + } + if (delta.y > 0.0) { + active_axes++; + } + if (delta.z > 0.0) { + active_axes++; + } + + String x_text = delta.x > 0.0 ? TranslationServer::get_singleton()->format_number(vformat("X: %.3f m", delta.x), _get_locale()) : ""; + String y_text = delta.y > 0.0 ? TranslationServer::get_singleton()->format_number(vformat("Y: %.3f m", delta.y), _get_locale()) : ""; + String z_text = delta.z > 0.0 ? TranslationServer::get_singleton()->format_number(vformat("Z: %.3f m", delta.z), _get_locale()) : ""; + + Color axis_x_color = get_theme_color(SNAME("axis_x_color"), EditorStringName(Editor)); + Color axis_y_color = get_theme_color(SNAME("axis_y_color"), EditorStringName(Editor)); + Color axis_z_color = get_theme_color(SNAME("axis_z_color"), EditorStringName(Editor)); + + Vector3 corner_point; + + if (active_axes >= 2) { + if (delta.z == 0.0) { + // XY plane + corner_point = Vector3(end_pos.x, start_pos.y, start_pos.z); + + ruler_label_x->add_theme_color_override(SceneStringName(font_color), axis_x_color); + ruler_label_x->set_text(x_text); + ruler_label_x->set_visible(true); + Vector2 x_pos = camera->unproject_position((start_pos + corner_point) / 2) - (ruler_label_x->get_custom_minimum_size() / 2); + ruler_label_x->set_position(x_pos); + + ruler_label_y->add_theme_color_override(SceneStringName(font_color), axis_y_color); + ruler_label_y->set_text(y_text); + ruler_label_y->set_visible(true); + Vector2 y_pos = camera->unproject_position((corner_point + end_pos) / 2) - (ruler_label_y->get_custom_minimum_size() / 2); + ruler_label_y->set_position(y_pos); + + ruler_label_z->set_visible(false); + } else if (delta.y == 0.0) { + // XZ plane + corner_point = Vector3(end_pos.x, start_pos.y, start_pos.z); + + ruler_label_x->add_theme_color_override(SceneStringName(font_color), axis_x_color); + ruler_label_x->set_text(x_text); + ruler_label_x->set_visible(true); + Vector2 x_pos = camera->unproject_position((start_pos + corner_point) / 2) - (ruler_label_x->get_custom_minimum_size() / 2); + ruler_label_x->set_position(x_pos); + + ruler_label_y->add_theme_color_override(SceneStringName(font_color), axis_z_color); + ruler_label_y->set_text(z_text); + ruler_label_y->set_visible(true); + Vector2 z_pos = camera->unproject_position((corner_point + end_pos) / 2) - (ruler_label_y->get_custom_minimum_size() / 2); + ruler_label_y->set_position(z_pos); + + ruler_label_z->set_visible(false); + } else if (delta.x == 0.0) { + // YZ plane + corner_point = Vector3(start_pos.x, start_pos.y, end_pos.z); + + ruler_label_x->add_theme_color_override(SceneStringName(font_color), axis_z_color); + ruler_label_x->set_text(z_text); + ruler_label_x->set_visible(true); + Vector2 z_pos = camera->unproject_position((start_pos + corner_point) / 2) - (ruler_label_x->get_custom_minimum_size() / 2); + ruler_label_x->set_position(z_pos); + + ruler_label_y->add_theme_color_override(SceneStringName(font_color), axis_y_color); + ruler_label_y->set_text(y_text); + ruler_label_y->set_visible(true); + Vector2 y_pos = camera->unproject_position((corner_point + end_pos) / 2) - (ruler_label_y->get_custom_minimum_size() / 2); + ruler_label_y->set_position(y_pos); + + ruler_label_z->set_visible(false); + } else { + // All three axes + corner_point = Vector3(end_pos.x, start_pos.y, start_pos.z); + + ruler_label_x->add_theme_color_override(SceneStringName(font_color), axis_x_color); + ruler_label_x->set_text(x_text); + ruler_label_x->set_visible(true); + Vector2 x_pos = camera->unproject_position((start_pos + corner_point) / 2) - (ruler_label_x->get_custom_minimum_size() / 2); + ruler_label_x->set_position(x_pos); + + ruler_label_y->add_theme_color_override(SceneStringName(font_color), axis_y_color); + ruler_label_y->set_text(y_text); + ruler_label_y->set_visible(true); + Vector2 y_pos = camera->unproject_position((corner_point + end_pos) / 2) - (ruler_label_y->get_custom_minimum_size() / 2); + ruler_label_y->set_position(y_pos); + + ruler_label_z->add_theme_color_override(SceneStringName(font_color), axis_z_color); + ruler_label_z->set_text(z_text); + ruler_label_z->set_visible(true); + Vector2 z_pos = camera->unproject_position(center + Vector3(0, 0, -0.5)) - (ruler_label_z->get_custom_minimum_size() / 2); + ruler_label_z->set_position(z_pos); + } + } else if (active_axes == 1) { + corner_point = end_pos; + + if (delta.x > 0.0) { + ruler_label_x->add_theme_color_override(SceneStringName(font_color), axis_x_color); + ruler_label_x->set_text(x_text); + ruler_label_x->set_visible(true); + Vector2 pos = camera->unproject_position(center) - (ruler_label_x->get_custom_minimum_size() / 2); + ruler_label_x->set_position(pos); + ruler_label_y->set_visible(false); + ruler_label_z->set_visible(false); + } else if (delta.y > 0.0) { + ruler_label_x->add_theme_color_override(SceneStringName(font_color), axis_y_color); + ruler_label_x->set_text(y_text); + ruler_label_x->set_visible(true); + Vector2 pos = camera->unproject_position(center) - (ruler_label_x->get_custom_minimum_size() / 2); + ruler_label_x->set_position(pos); + ruler_label_y->set_visible(false); + ruler_label_z->set_visible(false); + } else { + ruler_label_x->add_theme_color_override(SceneStringName(font_color), axis_z_color); + ruler_label_x->set_text(z_text); + ruler_label_x->set_visible(true); + Vector2 pos = camera->unproject_position(center) - (ruler_label_x->get_custom_minimum_size() / 2); + ruler_label_x->set_position(pos); + ruler_label_y->set_visible(false); + ruler_label_z->set_visible(false); + } + } else { + ruler_label_x->set_visible(false); + ruler_label_y->set_visible(false); + ruler_label_z->set_visible(false); + } + + } else { + ruler_label_x->set_visible(false); + ruler_label_y->set_visible(false); + ruler_label_z->set_visible(false); + } + } + + real_t delta = get_process_delta_time(); + + if (zoom_indicator_delay > 0) { + zoom_indicator_delay -= delta; + if (zoom_indicator_delay <= 0) { + surface->queue_redraw(); + zoom_limit_label->hide(); + } + } + + if (view_3d_controller->is_freelook_enabled()) { + view_3d_controller->update_freelook(delta); + _disable_follow_mode(); + } + + if (focused_node_id.is_valid() && get_selected_count() > 0 && times_focused_consecutively >= 2 && times_focused_consecutively % 2 == 0) { + Node *focused_node = ObjectDB::get_instance(focused_node_id); + if (focused_node) { + follow_mode->set_text(vformat(TTR("Following %s"), focused_node->get_name())); + follow_mode->set_button_icon(get_editor_theme_icon(focused_node->get_class())); + follow_mode->show(); + focus_selection(); + } else { + _disable_follow_mode(); + } + } else { + follow_mode->hide(); + } + + Node *scene_root = SceneTreeDock::get_singleton()->get_editor_data()->get_edited_scene_root(); + if (previewing_cinema && scene_root != nullptr) { + Camera3D *cam = scene_root->get_viewport()->get_camera_3d(); + if (cam != nullptr && cam != previewing) { + //then switch the viewport's camera to the scene's viewport camera + if (previewing != nullptr) { + _pilot_commit_undo_session(); + previewing->disconnect(SceneStringName(tree_exited), callable_mp(this, &Node3DEditorViewport::_preview_exited_scene)); + previewing->disconnect(CoreStringName(property_list_changed), callable_mp(this, &Node3DEditorViewport::_preview_camera_property_changed)); + } + previewing = cam; + previewing->connect(SceneStringName(tree_exited), callable_mp(this, &Node3DEditorViewport::_preview_exited_scene)); + previewing->connect(CoreStringName(property_list_changed), callable_mp(this, &Node3DEditorViewport::_preview_camera_property_changed)); + RS::get_singleton()->viewport_attach_camera(viewport->get_viewport_rid(), cam->get_camera()); + surface->queue_redraw(); + } + } + + if (_camera_moved_externally()) { + // If camera moved after this plugin last set it, presumably a tool script has moved it, accept the new camera transform as the cursor position. + pilot_undo_session_active = false; + pilot_undo_idle_time = 0.0; + _apply_camera_transform_to_cursor(); + view_3d_controller->update_camera(); + } else { + view_3d_controller->update_camera(delta); + } + + _pilot_tick_undo_session(delta); + + const HashMap &selection = editor_selection->get_selection(); + + bool changed = false; + bool exist = false; + + for (const KeyValue &E : selection) { + Node3D *sp = ObjectDB::get_instance(E.key); + if (!sp) { + continue; + } + + Node3DEditorSelectedItem *se = editor_selection->get_node_editor_data(sp); + if (!se) { + continue; + } + + Transform3D t = sp->get_global_gizmo_transform(); + if (!t.is_finite()) { + continue; + } + AABB new_aabb = _calculate_spatial_bounds(sp); + + exist = true; + if (se->last_xform == t && se->aabb == new_aabb && !se->last_xform_dirty) { + continue; + } + changed = true; + se->last_xform_dirty = false; + se->last_xform = t; + + se->aabb = new_aabb; + + Transform3D t_offset = t; + + // apply AABB scaling before item's global transform + { + const Vector3 offset(0.005, 0.005, 0.005); + Basis aabb_s; + aabb_s.scale(se->aabb.size + offset); + t.translate_local(se->aabb.position - offset / 2); + t.basis = t.basis * aabb_s; + } + { + const Vector3 offset(0.01, 0.01, 0.01); + Basis aabb_s; + aabb_s.scale(se->aabb.size + offset); + t_offset.translate_local(se->aabb.position - offset / 2); + t_offset.basis = t_offset.basis * aabb_s; + } + + RenderingServer::get_singleton()->instance_set_transform(se->sbox_instance, t); + RenderingServer::get_singleton()->instance_set_transform(se->sbox_instance_offset, t_offset); + RenderingServer::get_singleton()->instance_set_transform(se->sbox_instance_xray, t); + RenderingServer::get_singleton()->instance_set_transform(se->sbox_instance_xray_offset, t_offset); + } + + if (changed || (spatial_editor->is_gizmo_visible() && !exist)) { + spatial_editor->update_transform_gizmo(); + } + + if (message_time > 0) { + if (message != last_message) { + surface->queue_redraw(); + last_message = message; + } + + message_time -= get_process_delta_time(); + if (message_time < 0) { + surface->queue_redraw(); + } + } + + bool show_info = view_display_menu->get_popup()->is_item_checked(view_display_menu->get_popup()->get_item_index(VIEW_INFORMATION)); + if (show_info != info_panel->is_visible()) { + info_panel->set_visible(show_info); + } + + Camera3D *current_camera; + + if (previewing) { + current_camera = previewing; + } else { + current_camera = camera; + } + + if (show_info) { + const String viewport_size = vformat(U"%d × %d", viewport->get_size().x * viewport->get_scaling_3d_scale(), viewport->get_size().y * viewport->get_scaling_3d_scale()); + String text; + text += vformat(TTR("X: %s"), rtos(current_camera->get_position().x).pad_decimals(1)) + "\n"; + text += vformat(TTR("Y: %s"), rtos(current_camera->get_position().y).pad_decimals(1)) + "\n"; + text += vformat(TTR("Z: %s"), rtos(current_camera->get_position().z).pad_decimals(1)) + "\n"; + text += "\n"; + text += vformat( + TTR("Size: %s (%.1fMP)") + "\n", + viewport_size, + viewport->get_size().x * viewport->get_size().y * Math::pow(viewport->get_scaling_3d_scale(), 2) * 0.000001); + + text += "\n"; + text += vformat(TTR("Objects: %d"), viewport->get_render_info(Viewport::RENDER_INFO_TYPE_VISIBLE, Viewport::RENDER_INFO_OBJECTS_IN_FRAME)) + "\n"; + text += vformat(TTR("Primitives: %d"), viewport->get_render_info(Viewport::RENDER_INFO_TYPE_VISIBLE, Viewport::RENDER_INFO_PRIMITIVES_IN_FRAME)) + "\n"; + text += vformat(TTR("Draw Calls: %d"), viewport->get_render_info(Viewport::RENDER_INFO_TYPE_VISIBLE, Viewport::RENDER_INFO_DRAW_CALLS_IN_FRAME)); + + info_label->set_text(text); + } + + // FPS Counter. + bool show_fps = view_display_menu->get_popup()->is_item_checked(view_display_menu->get_popup()->get_item_index(VIEW_FRAME_TIME)); + + if (show_fps != frame_time_panel->is_visible()) { + frame_time_panel->set_visible(show_fps); + RS::get_singleton()->viewport_set_measure_render_time(viewport->get_viewport_rid(), show_fps); + for (int i = 0; i < FRAME_TIME_HISTORY; i++) { + // Initialize to 120 FPS, so that the initial estimation until we get enough data is always reasonable. + cpu_time_history[i] = 8.333333; + gpu_time_history[i] = 8.333333; + } + cpu_time_history_index = 0; + gpu_time_history_index = 0; + } + if (show_fps) { + cpu_time_history[cpu_time_history_index] = RS::get_singleton()->viewport_get_measured_render_time_cpu(viewport->get_viewport_rid()); + cpu_time_history_index = (cpu_time_history_index + 1) % FRAME_TIME_HISTORY; + double cpu_time = 0.0; + for (int i = 0; i < FRAME_TIME_HISTORY; i++) { + cpu_time += cpu_time_history[i]; + } + cpu_time /= FRAME_TIME_HISTORY; + // Prevent unrealistically low values. + cpu_time = MAX(0.01, cpu_time); + + gpu_time_history[gpu_time_history_index] = RS::get_singleton()->viewport_get_measured_render_time_gpu(viewport->get_viewport_rid()); + gpu_time_history_index = (gpu_time_history_index + 1) % FRAME_TIME_HISTORY; + double gpu_time = 0.0; + for (int i = 0; i < FRAME_TIME_HISTORY; i++) { + gpu_time += gpu_time_history[i]; + } + gpu_time /= FRAME_TIME_HISTORY; + // Prevent division by zero for the FPS counter (and unrealistically low values). + // This limits the reported FPS to 100000. + gpu_time = MAX(0.01, gpu_time); + + // Color labels depending on performance level ("good" = green, "OK" = yellow, "bad" = red). + // Middle point is at 15 ms. + cpu_time_label->set_text(vformat(TTR("CPU Time: %s ms"), rtos(cpu_time).pad_decimals(2))); + cpu_time_label->add_theme_color_override( + SceneStringName(font_color), + frame_time_gradient->get_color_at_offset( + Math::remap(cpu_time, 0, 30, 0, 1))); + + gpu_time_label->set_text(vformat(TTR("GPU Time: %s ms"), rtos(gpu_time).pad_decimals(2))); + // Middle point is at 15 ms. + gpu_time_label->add_theme_color_override( + SceneStringName(font_color), + frame_time_gradient->get_color_at_offset( + Math::remap(gpu_time, 0, 30, 0, 1))); + + const double fps = 1000.0 / gpu_time; + fps_label->set_text(vformat(TTR("FPS: %d"), fps)); + // Middle point is at 60 FPS. + fps_label->add_theme_color_override( + SceneStringName(font_color), + frame_time_gradient->get_color_at_offset( + Math::remap(fps, 110, 10, 0, 1))); + } + } break; + + case NOTIFICATION_PHYSICS_PROCESS: { + if (collision_reposition) { + Node3D *selected_node = nullptr; + + if (ruler->is_inside_tree()) { + if (ruler_start_point->is_visible()) { + selected_node = ruler_end_point; + } else { + selected_node = ruler_start_point; + } + } else { + const List &selection = editor_selection->get_top_selected_node_list(); + if (selection.size() == 1) { + selected_node = Object::cast_to(selection.front()->get()); + } + } + + if (selected_node) { + if (!ruler->is_inside_tree()) { + double snap = EDITOR_GET("interface/inspector/default_float_step"); + int snap_step_decimals = Math::range_step_decimals(snap); + set_message(vformat(TTR("Translating: %s"), vformat("%.*v", snap_step_decimals, selected_node->get_global_position()))); + } + + selected_node->set_global_position(spatial_editor->snap_point(_get_instance_position(_edit.mouse_pos, selected_node))); + + if (ruler->is_inside_tree() && !ruler_start_point->is_visible()) { + ruler_end_point->set_global_position(ruler_start_point->get_global_position()); + ruler_start_point->set_visible(true); + ruler_end_point->set_visible(true); + ruler_label->set_visible(true); + ruler_label_x->set_visible(false); + ruler_label_y->set_visible(false); + ruler_label_z->set_visible(false); + } + } + } + + if (!update_preview_node) { + return; + } + if (preview_node->is_inside_tree()) { + preview_node_pos = spatial_editor->snap_point(_get_instance_position(preview_node_viewport_pos, preview_node)); + double snap = EDITOR_GET("interface/inspector/default_float_step"); + int snap_step_decimals = Math::range_step_decimals(snap); + set_message(vformat(TTR("Instantiating: %s"), vformat("%.*v", snap_step_decimals, preview_node_pos))); + Transform3D preview_gl_transform = Transform3D(Basis(), preview_node_pos); + preview_node->set_global_transform(preview_gl_transform); + if (!preview_node->is_visible()) { + preview_node->show(); + } + } + update_preview_node = false; + } break; + + case NOTIFICATION_APPLICATION_FOCUS_OUT: + case NOTIFICATION_WM_WINDOW_FOCUS_OUT: { + view_3d_controller->set_freelook_enabled(false); + view_3d_controller->cursor.region_select = false; + surface->queue_redraw(); + + // Commit the drag if the window is focused out. + if (_edit.mode != TRANSFORM_NONE) { + commit_transform(); + return; + } + + if (_edit.gizmo.is_valid()) { + // Certain gizmo plugins should be able to commit handles without dragging them. + if (_edit.original_mouse_pos != _edit.mouse_pos || _edit.gizmo->get_plugin()->can_commit_handle_on_click()) { + _edit.gizmo->commit_handle(_edit.gizmo_handle, _edit.gizmo_handle_secondary, _edit.gizmo_initial_value, false); + } + + spatial_editor->get_single_selected_node()->update_gizmos(); + _edit.gizmo = Ref(); + set_message(""); + } + } break; + + case NOTIFICATION_ENTER_TREE: { + surface->connect(SceneStringName(draw), callable_mp(this, &Node3DEditorViewport::_draw)); + surface->connect(SceneStringName(gui_input), callable_mp(this, &Node3DEditorViewport::_sinput)); + surface->connect(SceneStringName(mouse_entered), callable_mp(this, &Node3DEditorViewport::_surface_mouse_enter)); + surface->connect(SceneStringName(mouse_exited), callable_mp(this, &Node3DEditorViewport::_surface_mouse_exit)); + surface->connect(SceneStringName(focus_entered), callable_mp(this, &Node3DEditorViewport::_surface_focus_enter)); + surface->connect(SceneStringName(focus_exited), callable_mp(this, &Node3DEditorViewport::_surface_focus_exit)); + + _init_gizmo_instance(index); + } break; + + case NOTIFICATION_EXIT_TREE: { + _finish_gizmo_instances(); + } break; + + case NOTIFICATION_THEME_CHANGED: { + _update_centered_labels(); + + view_display_menu->set_button_icon(get_editor_theme_icon(SNAME("GuiTabMenuHlDarkBackground"))); + preview_camera->set_button_icon(get_editor_theme_icon(SNAME("Camera3DDarkBackground"))); + pilot_camera->set_button_icon(get_editor_theme_icon(SNAME("3D"))); + Control *gui_base = EditorNode::get_singleton()->get_gui_base(); + + const Ref &information_3d_stylebox = gui_base->get_theme_stylebox(SNAME("Information3dViewport"), EditorStringName(EditorStyles)); + + override_button_stylebox(view_display_menu, information_3d_stylebox); + override_label_colors(view_display_menu); + override_button_stylebox(translation_preview_button, information_3d_stylebox); + override_label_colors(translation_preview_button); + override_button_stylebox(follow_mode, information_3d_stylebox); + override_label_colors(follow_mode); + override_button_stylebox(preview_camera, information_3d_stylebox); + override_label_colors(preview_camera); + + frame_time_gradient->set_color(0, get_theme_color(SNAME("success_color_dark_background"), EditorStringName(Editor))); + frame_time_gradient->set_color(1, get_theme_color(SNAME("warning_color_dark_background"), EditorStringName(Editor))); + frame_time_gradient->set_color(2, get_theme_color(SNAME("error_color_dark_background"), EditorStringName(Editor))); + + override_button_stylebox(pilot_camera, information_3d_stylebox); + override_label_colors(pilot_camera); + + info_panel->add_theme_style_override(SceneStringName(panel), information_3d_stylebox); + override_label_colors(info_label); + tooltip_panel->add_theme_style_override(CoreStringName(normal), information_3d_stylebox); + + frame_time_panel->add_theme_style_override(SceneStringName(panel), information_3d_stylebox); + // Set a minimum width to prevent the width from changing all the time + // when numbers vary rapidly. This minimum width is set based on a + // GPU time of 999.99 ms in the current editor language. + const float min_width = get_theme_font(SNAME("main"), EditorStringName(EditorFonts))->get_string_size(vformat(TTR("GPU Time: %s ms"), 999.99)).x; + frame_time_panel->set_custom_minimum_size(Size2(min_width, 0) * EDSCALE); + frame_time_vbox->add_theme_constant_override("separation", Math::round(-1 * EDSCALE)); + + cinema_label->add_theme_style_override(CoreStringName(normal), information_3d_stylebox); + locked_label->add_theme_style_override(CoreStringName(normal), information_3d_stylebox); + + ruler_label->add_theme_color_override(SceneStringName(font_color), Color(1.0, 0.9, 0.0, 1.0)); + ruler_label->add_theme_color_override("font_outline_color", Color(0.0, 0.0, 0.0, 1.0)); + ruler_label->add_theme_constant_override("outline_size", 4 * EDSCALE); + ruler_label->add_theme_font_size_override(SceneStringName(font_size), 15 * EDSCALE); + ruler_label->add_theme_font_override(SceneStringName(font), get_theme_font(SNAME("bold"), EditorStringName(EditorFonts))); + + ruler_label_x->add_theme_color_override("font_outline_color", Color(0.0, 0.0, 0.0, 1.0)); + ruler_label_x->add_theme_constant_override("outline_size", 4 * EDSCALE); + ruler_label_x->add_theme_font_size_override(SceneStringName(font_size), 15 * EDSCALE); + ruler_label_x->add_theme_font_override(SceneStringName(font), get_theme_font(SNAME("bold"), EditorStringName(EditorFonts))); + + ruler_label_y->add_theme_color_override("font_outline_color", Color(0.0, 0.0, 0.0, 1.0)); + ruler_label_y->add_theme_constant_override("outline_size", 4 * EDSCALE); + ruler_label_y->add_theme_font_size_override(SceneStringName(font_size), 15 * EDSCALE); + ruler_label_y->add_theme_font_override(SceneStringName(font), get_theme_font(SNAME("bold"), EditorStringName(EditorFonts))); + + ruler_label_z->add_theme_color_override("font_outline_color", Color(0.0, 0.0, 0.0, 1.0)); + ruler_label_z->add_theme_constant_override("outline_size", 4 * EDSCALE); + ruler_label_z->add_theme_font_size_override(SceneStringName(font_size), 15 * EDSCALE); + ruler_label_z->add_theme_font_override(SceneStringName(font), get_theme_font(SNAME("bold"), EditorStringName(EditorFonts))); + } break; + + case NOTIFICATION_DRAG_END: { + // Clear preview material when dropped outside applicable object. + if (spatial_editor->get_preview_material().is_valid() && !is_drag_successful()) { + _reset_preview_material(); + _remove_preview_material(); + } else { + _remove_preview_node(); + } + } break; + + case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: { + _update_view_3d_controller(); + + if (EditorSettings::get_singleton()->check_changed_settings_in_group("editors/3d")) { + _update_navigation_controls_visibility(); + } + } break; + } +} + +void Node3DEditorViewport::_update_view_3d_controller(bool p_update_all) { + view_3d_controller->set_shortcut(View3DController::SHORTCUT_FOV_DECREASE, ED_GET_SHORTCUT("spatial_editor/decrease_fov")); + view_3d_controller->set_shortcut(View3DController::SHORTCUT_FOV_INCREASE, ED_GET_SHORTCUT("spatial_editor/increase_fov")); + view_3d_controller->set_shortcut(View3DController::SHORTCUT_FOV_RESET, ED_GET_SHORTCUT("spatial_editor/reset_fov")); + + view_3d_controller->set_shortcut(View3DController::SHORTCUT_PAN_MOD_1, ED_GET_SHORTCUT("spatial_editor/viewport_pan_modifier_1")); + view_3d_controller->set_shortcut(View3DController::SHORTCUT_PAN_MOD_2, ED_GET_SHORTCUT("spatial_editor/viewport_pan_modifier_2")); + + view_3d_controller->set_shortcut(View3DController::SHORTCUT_ORBIT_MOD_1, ED_GET_SHORTCUT("spatial_editor/viewport_orbit_modifier_1")); + view_3d_controller->set_shortcut(View3DController::SHORTCUT_ORBIT_MOD_2, ED_GET_SHORTCUT("spatial_editor/viewport_orbit_modifier_2")); + view_3d_controller->set_shortcut(View3DController::SHORTCUT_ORBIT_SNAP_MOD_1, ED_GET_SHORTCUT("spatial_editor/viewport_orbit_snap_modifier_1")); + view_3d_controller->set_shortcut(View3DController::SHORTCUT_ORBIT_SNAP_MOD_2, ED_GET_SHORTCUT("spatial_editor/viewport_orbit_snap_modifier_2")); + + view_3d_controller->set_shortcut(View3DController::SHORTCUT_FREELOOK_FORWARD, ED_GET_SHORTCUT("spatial_editor/freelook_forward")); + view_3d_controller->set_shortcut(View3DController::SHORTCUT_FREELOOK_BACKWARDS, ED_GET_SHORTCUT("spatial_editor/freelook_backwards")); + view_3d_controller->set_shortcut(View3DController::SHORTCUT_FREELOOK_LEFT, ED_GET_SHORTCUT("spatial_editor/freelook_left")); + view_3d_controller->set_shortcut(View3DController::SHORTCUT_FREELOOK_RIGHT, ED_GET_SHORTCUT("spatial_editor/freelook_right")); + view_3d_controller->set_shortcut(View3DController::SHORTCUT_FREELOOK_UP, ED_GET_SHORTCUT("spatial_editor/freelook_up")); + view_3d_controller->set_shortcut(View3DController::SHORTCUT_FREELOOK_DOWN, ED_GET_SHORTCUT("spatial_editor/freelook_down")); + view_3d_controller->set_shortcut(View3DController::SHORTCUT_FREELOOK_SPEED_MOD, ED_GET_SHORTCUT("spatial_editor/freelook_speed_modifier")); + view_3d_controller->set_shortcut(View3DController::SHORTCUT_FREELOOK_SLOW_MOD, ED_GET_SHORTCUT("spatial_editor/freelook_slow_modifier")); + + view_3d_controller->set_shortcut(View3DController::SHORTCUT_ZOOM_MOD_1, ED_GET_SHORTCUT("spatial_editor/viewport_zoom_modifier_1")); + view_3d_controller->set_shortcut(View3DController::SHORTCUT_ZOOM_MOD_2, ED_GET_SHORTCUT("spatial_editor/viewport_zoom_modifier_2")); + + if (p_update_all || EditorSettings::get_singleton()->check_changed_settings_in_group("editors/3d")) { + view_3d_controller->set_pan_mouse_button((View3DController::NavigationMouseButton)EDITOR_GET("editors/3d/navigation/pan_mouse_button").operator int()); + + view_3d_controller->set_orbit_sensitivity(EDITOR_GET("editors/3d/navigation_feel/orbit_sensitivity")); + view_3d_controller->set_orbit_inertia(EDITOR_GET("editors/3d/navigation_feel/orbit_inertia")); + view_3d_controller->set_orbit_mouse_button((View3DController::NavigationMouseButton)EDITOR_GET("editors/3d/navigation/orbit_mouse_button").operator int()); + + view_3d_controller->set_zoom_style((View3DController::ZoomStyle)EDITOR_GET("editors/3d/navigation/zoom_style").operator int()); + view_3d_controller->set_zoom_inertia(EDITOR_GET("editors/3d/navigation_feel/zoom_inertia")); + view_3d_controller->set_zoom_mouse_button((View3DController::NavigationMouseButton)EDITOR_GET("editors/3d/navigation/zoom_mouse_button").operator int()); + + view_3d_controller->set_freelook_scheme((View3DController::FreelookScheme)EDITOR_GET("editors/3d/freelook/freelook_navigation_scheme").operator int()); + view_3d_controller->set_freelook_base_speed(EDITOR_GET("editors/3d/freelook/freelook_base_speed")); + view_3d_controller->set_freelook_sensitivity(EDITOR_GET("editors/3d/freelook/freelook_sensitivity")); + view_3d_controller->set_freelook_inertia(EDITOR_GET("editors/3d/freelook/freelook_inertia")); + view_3d_controller->set_freelook_speed_zoom_link(EDITOR_GET("editors/3d/freelook/freelook_speed_zoom_link")); + view_3d_controller->set_freelook_invert_y_axis(EDITOR_GET("editors/3d/freelook/freelook_invert_y_axis")); + + view_3d_controller->set_translation_sensitivity(EDITOR_GET("editors/3d/navigation_feel/translation_sensitivity")); + view_3d_controller->set_translation_inertia(EDITOR_GET("editors/3d/navigation_feel/translation_inertia")); + + view_3d_controller->set_angle_snap_threshold(EDITOR_GET("editors/3d/navigation_feel/angle_snap_threshold")); + + view_3d_controller->set_emulate_3_button_mouse(EDITOR_GET("editors/3d/navigation/emulate_3_button_mouse")); + view_3d_controller->set_emulate_numpad(EDITOR_GET("editors/3d/navigation/emulate_numpad")); + + view_3d_controller->set_invert_x_axis(EDITOR_GET("editors/3d/navigation/invert_x_axis")); + view_3d_controller->set_invert_y_axis(EDITOR_GET("editors/3d/navigation/invert_y_axis")); + + view_3d_controller->set_warped_mouse_panning((View3DController::NavigationMouseButton)EDITOR_GET("editors/3d/navigation/warped_mouse_panning").operator int()); + } +} + +static void draw_indicator_bar(Control &p_surface, real_t p_fill, const Ref p_icon, const Ref p_font, int p_font_size, const String &p_text, const Color &p_color) { + // Adjust bar size from control height + const Vector2 surface_size = p_surface.get_size(); + const real_t h = surface_size.y / 2.0; + const real_t y = (surface_size.y - h) / 2.0; + + const Rect2 r(10 * EDSCALE, y, 6 * EDSCALE, h); + const real_t sy = r.size.y * p_fill; + + // Note: because this bar appears over the viewport, it has to stay readable for any background color + // Draw both neutral dark and bright colors to account this + p_surface.draw_rect(r, p_color * Color(1, 1, 1, 0.2)); + p_surface.draw_rect(Rect2(r.position.x, r.position.y + r.size.y - sy, r.size.x, sy), p_color * Color(1, 1, 1, 0.6)); + p_surface.draw_rect(r.grow(1), Color(0, 0, 0, 0.7), false, Math::round(EDSCALE)); + + const Vector2 icon_size = p_icon->get_size(); + const Vector2 icon_pos = Vector2(r.position.x - (icon_size.x - r.size.x) / 2, r.position.y + r.size.y + 2 * EDSCALE); + p_surface.draw_texture(p_icon, icon_pos, p_color); + + // Draw text below the bar (for speed/zoom information). + p_surface.draw_string_outline(p_font, Vector2(icon_pos.x, icon_pos.y + icon_size.y + 16 * EDSCALE), p_text, HORIZONTAL_ALIGNMENT_LEFT, -1.f, p_font_size, Math::round(4 * EDSCALE), Color(0, 0, 0)); + p_surface.draw_string(p_font, Vector2(icon_pos.x, icon_pos.y + icon_size.y + 16 * EDSCALE), p_text, HORIZONTAL_ALIGNMENT_LEFT, -1.f, p_font_size, p_color); +} + +void Node3DEditorViewport::_draw() { + EditorNode::get_singleton()->get_editor_plugins_over()->forward_3d_draw_over_viewport(surface); + EditorNode::get_singleton()->get_editor_plugins_force_over()->forward_3d_force_draw_over_viewport(surface); + + if (surface->has_focus() || rotation_control->has_focus()) { + Size2 size = surface->get_size(); + Rect2 r = Rect2(Point2(), size); + get_theme_stylebox(SNAME("FocusViewport"), EditorStringName(EditorStyles))->draw(surface->get_canvas_item(), r); + } + + View3DController::Cursor cursor = view_3d_controller->cursor; + + if (cursor.region_select && movement_threshold_passed) { + const Rect2 selection_rect = Rect2(cursor.region_begin, cursor.region_end - cursor.region_begin); + + surface->draw_rect( + selection_rect, + get_theme_color(SNAME("box_selection_fill_color"), EditorStringName(Editor))); + + surface->draw_rect( + selection_rect, + get_theme_color(SNAME("box_selection_stroke_color"), EditorStringName(Editor)), + false, + Math::round(EDSCALE)); + } + + RID ci = surface->get_canvas_item(); + + if (message_time > 0) { + Ref font = get_theme_font(SceneStringName(font), SNAME("Label")); + int font_size = get_theme_font_size(SceneStringName(font_size), SNAME("Label")); + Point2 msgpos = Point2(10 * EDSCALE, get_size().y - 14 * EDSCALE); + font->draw_string(ci, msgpos + Point2(1, 1), message, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size, Color(0, 0, 0, 0.8)); + font->draw_string(ci, msgpos + Point2(-1, -1), message, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size, Color(0, 0, 0, 0.8)); + font->draw_string(ci, msgpos, message, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size, Color(1, 1, 1, 1)); + } + + if ((vertex_snap_mode || vertex_snap_dragging) && vertex_snap_has_source) { + const float circle_radius = 6.0f * EDSCALE; + const float outline_width = 2.0f * EDSCALE; + const float occluded_alpha = 0.30f; + + Vector3 source_display = vertex_snap_source; + if (vertex_snap_dragging) { + for (const KeyValue &E : vertex_snap_original_positions) { + Node3D *node = ObjectDB::get_instance(E.key); + if (node) { + source_display = vertex_snap_source + (node->get_global_position() - E.value); + break; + } + } + } + + if (!camera->is_position_behind(source_display)) { + Vector2 screen_pos = camera->unproject_position(source_display); + bool occluded = _is_vertex_occluded(source_display, screen_pos); + float alpha = occluded ? occluded_alpha : 1.0f; + surface->draw_circle(screen_pos, circle_radius + outline_width, Color(0, 0, 0, 0.6 * alpha), true, -1.0, true); + surface->draw_circle(screen_pos, circle_radius, Color(1, 1, 0, alpha), true, -1.0, true); + } + + if (vertex_snap_dragging && vertex_snap_has_target && !camera->is_position_behind(vertex_snap_target)) { + Vector2 screen_pos = camera->unproject_position(vertex_snap_target); + bool occluded = _is_vertex_occluded(vertex_snap_target, screen_pos); + float alpha = occluded ? occluded_alpha : 1.0f; + surface->draw_circle(screen_pos, circle_radius + outline_width, Color(0, 0, 0, 0.6 * alpha), true, -1.0, true); + surface->draw_circle(screen_pos, circle_radius, Color(0, 1, 0, alpha), true, -1.0, true); + } + } + + if (_edit.mode == TRANSFORM_ROTATE) { + Point2 center = point_to_screen(_edit.center); + + Color handle_color; + switch (_edit.plane) { + case TRANSFORM_VIEW: + handle_color = get_theme_color(SNAME("axis_view_plane_color"), EditorStringName(Editor)); + break; + case TRANSFORM_X_AXIS: + handle_color = get_theme_color(SNAME("axis_x_color"), EditorStringName(Editor)); + break; + case TRANSFORM_Y_AXIS: + handle_color = get_theme_color(SNAME("axis_y_color"), EditorStringName(Editor)); + break; + case TRANSFORM_Z_AXIS: + handle_color = get_theme_color(SNAME("axis_z_color"), EditorStringName(Editor)); + break; + default: + handle_color = get_theme_color(SNAME("accent_color"), EditorStringName(Editor)); + break; + } + + if (!_edit.is_trackball && _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); + + real_t rotation_radius = (_edit.plane == TRANSFORM_VIEW) ? spatial_editor->gizmo_view_rotation_scale : GIZMO_CIRCLE_SIZE; + + 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 * rotation_radius * (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(), handle_color.get_s() * 0.6, 1.0, 0.8); + Vector circle_colors; + circle_colors.resize(circle_points.size()); + circle_colors.fill(circle_color); + RenderingServer::get_singleton()->canvas_item_add_polyline(ci, circle_points, circle_colors, Math::round(2 * EDSCALE), true); + + const int segments = 64; + float display_angle = _edit.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 * rotation_radius * (right * Math::cos(angle1) + forward * Math::sin(angle1)); + Vector3 point2_3d = _edit.center + gizmo_scale * rotation_radius * (right * Math::cos(angle2) + forward * Math::sin(angle2)); + + Point2 point1_2d = point_to_screen(point1_3d); + Point2 point2_2d = point_to_screen(point2_3d); + + Vector triangle_points; + triangle_points.push_back(center); + 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(), handle_color.get_s() * 0.8, 1.0, 0.7); + + Vector3 start_point_3d = _edit.center + gizmo_scale * rotation_radius * 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), + true); + + Vector3 end_point_3d = _edit.center + gizmo_scale * rotation_radius * (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), + true); + } + + if (_edit.show_rotation_line) { + handle_color = handle_color.from_hsv(handle_color.get_h(), handle_color.get_s() * 0.25, 1.0, handle_color.a); + 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")); + float aspect = ss.aspect(); + Size2 s = get_size(); + + Rect2 draw_rect; + + switch (previewing->get_keep_aspect_mode()) { + case Camera3D::KEEP_WIDTH: { + draw_rect.size = Size2(s.width, s.width / aspect); + draw_rect.position.x = 0; + draw_rect.position.y = (s.height - draw_rect.size.y) * 0.5; + + } break; + case Camera3D::KEEP_HEIGHT: { + draw_rect.size = Size2(s.height * aspect, s.height); + draw_rect.position.y = 0; + draw_rect.position.x = (s.width - draw_rect.size.x) * 0.5; + + } break; + } + + draw_rect = Rect2(Vector2(), s).intersection(draw_rect); + + surface->draw_rect(draw_rect, Color(0.6, 0.6, 0.1, 0.5), false, Math::round(2 * EDSCALE)); + + } else { + if (zoom_indicator_delay > 0.0) { + if (view_3d_controller->is_freelook_enabled()) { + // Show speed. + + real_t min_speed = MAX(camera->get_near() * 4, View3DControllerConsts::ZOOM_FREELOOK_MIN); + real_t max_speed = MIN(camera->get_far() / 4, View3DControllerConsts::ZOOM_FREELOOK_MAX); + real_t scale_length = (max_speed - min_speed); + + if (!Math::is_zero_approx(scale_length)) { + float freelook_speed = view_3d_controller->get_freelook_speed(); + real_t logscale_t = 1.0 - Math::log1p(freelook_speed - min_speed) / Math::log1p(scale_length); + + // Display the freelook speed to help the user get a better sense of scale. + const int precision = freelook_speed < 1.0 ? 2 : 1; + draw_indicator_bar( + *surface, + 1.0 - logscale_t, + get_editor_theme_icon(SNAME("ViewportSpeed")), + get_theme_font("bold", EditorStringName(EditorFonts)), + get_theme_font_size(SceneStringName(font_size), SNAME("Label")), + vformat("%s m/s", String::num(freelook_speed).pad_decimals(precision)), + Color(1.0, 0.95, 0.7)); + } + } else { + // Show zoom + zoom_limit_label->set_visible(zoom_failed_attempts_count > 15); + + real_t min_distance = MAX(camera->get_near() * 4, View3DControllerConsts::ZOOM_FREELOOK_MIN); + real_t max_distance = MIN(camera->get_far() / 4, View3DControllerConsts::ZOOM_FREELOOK_MAX); + real_t scale_length = (max_distance - min_distance); + + if (!Math::is_zero_approx(scale_length)) { + real_t logscale_t = 1.0 - Math::log1p(cursor.distance - min_distance) / Math::log1p(scale_length); + + // Display the zoom center distance to help the user get a better sense of scale. + const int precision = cursor.distance < 1.0 ? 2 : 1; + draw_indicator_bar( + *surface, + logscale_t, + get_editor_theme_icon(SNAME("ViewportZoom")), + get_theme_font("bold", EditorStringName(EditorFonts)), + get_theme_font_size(SceneStringName(font_size), SNAME("Label")), + vformat("%s m", String::num(cursor.distance).pad_decimals(precision)), + Color(0.7, 0.95, 1.0)); + } + } + } + } +} + +bool Node3DEditorViewport::_camera_moved_externally() { + if (previewing_camera && previewing) { + if (pilot_preview_enabled) { + Transform3D t = previewing->get_global_transform(); + return !t.is_equal_approx(last_camera_transform); + } + return false; + } + Transform3D t = camera->get_global_transform(); + return !t.is_equal_approx(last_camera_transform); +} + +void Node3DEditorViewport::_apply_camera_transform_to_cursor() { + if (previewing_camera && previewing) { + if (pilot_preview_enabled) { + _sync_cursor_from_transform(previewing->get_global_transform()); + } + return; + } + _sync_cursor_from_transform(camera->get_camera_transform()); +} + +void Node3DEditorViewport::_menu_option(int p_option) { + EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); + switch (p_option) { + case VIEW_TOP: { + view_3d_controller->cursor.y_rot = 0; + view_3d_controller->cursor.x_rot = Math::PI / 2.0; + view_3d_controller->cursor.unsnapped_y_rot = view_3d_controller->cursor.y_rot; + view_3d_controller->cursor.unsnapped_x_rot = view_3d_controller->cursor.x_rot; + set_message(TTR("Top View."), 2); + view_3d_controller->set_view_type(View3DController::VIEW_TYPE_TOP); + + } break; + case VIEW_BOTTOM: { + view_3d_controller->cursor.y_rot = 0; + view_3d_controller->cursor.x_rot = -Math::PI / 2.0; + view_3d_controller->cursor.unsnapped_y_rot = view_3d_controller->cursor.y_rot; + view_3d_controller->cursor.unsnapped_x_rot = view_3d_controller->cursor.x_rot; + set_message(TTR("Bottom View."), 2); + view_3d_controller->set_view_type(View3DController::VIEW_TYPE_BOTTOM); + + } break; + case VIEW_LEFT: { + view_3d_controller->cursor.x_rot = 0; + view_3d_controller->cursor.y_rot = Math::PI / 2.0; + view_3d_controller->cursor.unsnapped_x_rot = view_3d_controller->cursor.x_rot; + view_3d_controller->cursor.unsnapped_y_rot = view_3d_controller->cursor.y_rot; + set_message(TTR("Left View."), 2); + view_3d_controller->set_view_type(View3DController::VIEW_TYPE_LEFT); + + } break; + case VIEW_RIGHT: { + view_3d_controller->cursor.x_rot = 0; + view_3d_controller->cursor.y_rot = -Math::PI / 2.0; + view_3d_controller->cursor.unsnapped_x_rot = view_3d_controller->cursor.x_rot; + view_3d_controller->cursor.unsnapped_y_rot = view_3d_controller->cursor.y_rot; + set_message(TTR("Right View."), 2); + view_3d_controller->set_view_type(View3DController::VIEW_TYPE_RIGHT); + + } break; + case VIEW_FRONT: { + view_3d_controller->cursor.x_rot = 0; + view_3d_controller->cursor.y_rot = 0; + view_3d_controller->cursor.unsnapped_x_rot = view_3d_controller->cursor.x_rot; + view_3d_controller->cursor.unsnapped_y_rot = view_3d_controller->cursor.y_rot; + set_message(TTR("Front View."), 2); + view_3d_controller->set_view_type(View3DController::VIEW_TYPE_FRONT); + + } break; + case VIEW_REAR: { + view_3d_controller->cursor.x_rot = 0; + view_3d_controller->cursor.y_rot = Math::PI; + view_3d_controller->cursor.unsnapped_x_rot = view_3d_controller->cursor.x_rot; + view_3d_controller->cursor.unsnapped_y_rot = view_3d_controller->cursor.y_rot; + set_message(TTR("Rear View."), 2); + view_3d_controller->set_view_type(View3DController::VIEW_TYPE_REAR); + + } break; + case VIEW_CENTER_TO_ORIGIN: { + view_3d_controller->cursor.pos = Vector3(0, 0, 0); + _disable_follow_mode(); + + } break; + case VIEW_CENTER_TO_SELECTION: { + focus_selection(); + + } break; + case VIEW_ALIGN_TRANSFORM_WITH_VIEW: { + if (!get_selected_count()) { + break; + } + + Transform3D camera_transform = camera->get_global_transform(); + + const List &selection = editor_selection->get_top_selected_node_list(); + + undo_redo->create_action(TTR("Align Transform with View")); + for (Node *E : selection) { + Node3D *sp = Object::cast_to(E); + if (!sp) { + continue; + } + + Node3DEditorSelectedItem *se = editor_selection->get_node_editor_data(sp); + if (!se) { + continue; + } + + Transform3D xform = camera_transform; + if (view_3d_controller->is_orthogonal()) { + Vector3 offset = camera_transform.basis.xform(Vector3(0, 0, view_3d_controller->cursor.distance)); + xform.origin = view_3d_controller->cursor.pos + offset; + } else { + xform.scale_basis(sp->get_scale()); + } + + if (Object::cast_to(E)) { + // Adjust rotation to match Decal's default orientation. + // This makes the decal "look" in the same direction as the camera, + // rather than pointing down relative to the camera orientation. + xform.basis.rotate_local(Vector3(1, 0, 0), Math::TAU * 0.25); + } + + Node3D *parent = sp->get_parent_node_3d(); + Transform3D local_xform = parent ? parent->get_global_transform().affine_inverse() * xform : xform; + undo_redo->add_do_method(sp, "set_transform", local_xform); + undo_redo->add_undo_method(sp, "set_transform", sp->get_local_gizmo_transform()); + } + undo_redo->commit_action(); + + } break; + case VIEW_ALIGN_ROTATION_WITH_VIEW: { + if (!get_selected_count()) { + break; + } + + Transform3D camera_transform = camera->get_global_transform(); + + const List &selection = editor_selection->get_top_selected_node_list(); + + undo_redo->create_action(TTR("Align Rotation with View")); + for (Node *E : selection) { + Node3D *sp = Object::cast_to(E); + if (!sp) { + continue; + } + + Node3DEditorSelectedItem *se = editor_selection->get_node_editor_data(sp); + if (!se) { + continue; + } + + Basis basis = camera_transform.basis; + + if (Object::cast_to(E)) { + // Adjust rotation to match Decal's default orientation. + // This makes the decal "look" in the same direction as the camera, + // rather than pointing down relative to the camera orientation. + basis.rotate_local(Vector3(1, 0, 0), Math::TAU * 0.25); + } + + undo_redo->add_do_method(sp, "set_rotation", basis.get_euler_normalized()); + undo_redo->add_undo_method(sp, "set_rotation", sp->get_rotation()); + } + undo_redo->commit_action(); + + } break; + case VIEW_ENVIRONMENT: { + int idx = view_display_menu->get_popup()->get_item_index(VIEW_ENVIRONMENT); + bool current = view_display_menu->get_popup()->is_item_checked(idx); + current = !current; + if (current) { + camera->set_environment(Ref()); + } else { + camera->set_environment(Node3DEditor::get_singleton()->get_viewport_environment()); + } + + view_display_menu->get_popup()->set_item_checked(idx, current); + + } break; + case VIEW_PERSPECTIVE: { + view_display_menu->get_popup()->set_item_checked(view_display_menu->get_popup()->get_item_index(VIEW_PERSPECTIVE), true); + view_display_menu->get_popup()->set_item_checked(view_display_menu->get_popup()->get_item_index(VIEW_ORTHOGONAL), false); + view_3d_controller->set_orthogonal(false); + callable_mp(this, &Node3DEditorViewport::update_transform_gizmo_view).call_deferred(); + view_3d_controller->update_camera(); + _update_name(); + + } break; + case VIEW_ORTHOGONAL: { + view_display_menu->get_popup()->set_item_checked(view_display_menu->get_popup()->get_item_index(VIEW_PERSPECTIVE), false); + view_display_menu->get_popup()->set_item_checked(view_display_menu->get_popup()->get_item_index(VIEW_ORTHOGONAL), true); + view_3d_controller->set_orthogonal(true); + callable_mp(this, &Node3DEditorViewport::update_transform_gizmo_view).call_deferred(); + view_3d_controller->update_camera(); + _update_name(); + } break; + case VIEW_SWITCH_PERSPECTIVE_ORTHOGONAL: { + _menu_option(view_3d_controller->is_orthogonal() ? VIEW_PERSPECTIVE : VIEW_ORTHOGONAL); + + } break; + case VIEW_AUTO_ORTHOGONAL: { + int idx = view_display_menu->get_popup()->get_item_index(VIEW_AUTO_ORTHOGONAL); + bool current = view_display_menu->get_popup()->is_item_checked(idx); + current = !current; + view_display_menu->get_popup()->set_item_checked(idx, current); + view_3d_controller->set_auto_orthogonal_allowed(current); + _update_name(); + } break; + case VIEW_LOCK_ROTATION: { + int idx = view_display_menu->get_popup()->get_item_index(VIEW_LOCK_ROTATION); + bool current = view_display_menu->get_popup()->is_item_checked(idx); + _set_lock_view_rotation(!current); + + } break; + case VIEW_AUDIO_LISTENER: { + int idx = view_display_menu->get_popup()->get_item_index(VIEW_AUDIO_LISTENER); + bool current = view_display_menu->get_popup()->is_item_checked(idx); + current = !current; + viewport->set_as_audio_listener_3d(current); + view_display_menu->get_popup()->set_item_checked(idx, current); + + } break; + case VIEW_AUDIO_DOPPLER: { + int idx = view_display_menu->get_popup()->get_item_index(VIEW_AUDIO_DOPPLER); + bool current = view_display_menu->get_popup()->is_item_checked(idx); + current = !current; + camera->set_doppler_tracking(current ? Camera3D::DOPPLER_TRACKING_IDLE_STEP : Camera3D::DOPPLER_TRACKING_DISABLED); + view_display_menu->get_popup()->set_item_checked(idx, current); + + } break; + case VIEW_CINEMATIC_PREVIEW: { + int idx = view_display_menu->get_popup()->get_item_index(VIEW_CINEMATIC_PREVIEW); + bool current = view_display_menu->get_popup()->is_item_checked(idx); + current = !current; + view_display_menu->get_popup()->set_item_checked(idx, current); + _toggle_cinema_preview(current); + + cinema_label->set_visible(current); + _update_centered_labels(); + surface->queue_redraw(); + + if (current) { + preview_camera->hide(); + } else { + if (previewing != nullptr) { + preview_camera->show(); + } + } + } break; + case VIEW_GIZMOS: { + int idx = view_display_menu->get_popup()->get_item_index(VIEW_GIZMOS); + bool current = view_display_menu->get_popup()->is_item_checked(idx); + current = !current; + uint32_t layers = camera->get_cull_mask(); + layers &= ~(1 << GIZMO_EDIT_LAYER); + if (current) { + layers |= (1 << GIZMO_EDIT_LAYER); + } + camera->set_cull_mask(layers); + view_display_menu->get_popup()->set_item_checked(idx, current); + + } break; + case VIEW_TRANSFORM_GIZMO: { + int idx = view_display_menu->get_popup()->get_item_index(VIEW_TRANSFORM_GIZMO); + bool current = view_display_menu->get_popup()->is_item_checked(idx); + current = !current; + transform_gizmo_visible = current; + + spatial_editor->update_transform_gizmo(); + view_display_menu->get_popup()->set_item_checked(idx, current); + } break; + case VIEW_HALF_RESOLUTION: { + int idx = view_display_menu->get_popup()->get_item_index(VIEW_HALF_RESOLUTION); + bool current = view_display_menu->get_popup()->is_item_checked(idx); + view_display_menu->get_popup()->set_item_checked(idx, !current); + _update_shrink(); + } break; + case VIEW_INFORMATION: { + int idx = view_display_menu->get_popup()->get_item_index(VIEW_INFORMATION); + bool current = view_display_menu->get_popup()->is_item_checked(idx); + view_display_menu->get_popup()->set_item_checked(idx, !current); + + } break; + case VIEW_FRAME_TIME: { + int idx = view_display_menu->get_popup()->get_item_index(VIEW_FRAME_TIME); + bool current = view_display_menu->get_popup()->is_item_checked(idx); + view_display_menu->get_popup()->set_item_checked(idx, !current); + } break; + case VIEW_GRID: { + int idx = view_display_menu->get_popup()->get_item_index(VIEW_GRID); + bool current = view_display_menu->get_popup()->is_item_checked(idx); + current = !current; + uint32_t layers = camera->get_cull_mask(); + layers &= ~(1 << GIZMO_GRID_LAYER); + if (current) { + layers |= (1 << GIZMO_GRID_LAYER); + } + camera->set_cull_mask(layers); + view_display_menu->get_popup()->set_item_checked(idx, current); + } break; + case VIEW_DISPLAY_NORMAL: + case VIEW_DISPLAY_WIREFRAME: + case VIEW_DISPLAY_OVERDRAW: + case VIEW_DISPLAY_UNSHADED: + case VIEW_DISPLAY_LIGHTING: + case VIEW_DISPLAY_NORMAL_BUFFER: + case VIEW_DISPLAY_DEBUG_SHADOW_ATLAS: + case VIEW_DISPLAY_DEBUG_DIRECTIONAL_SHADOW_ATLAS: + case VIEW_DISPLAY_DEBUG_VOXEL_GI_ALBEDO: + case VIEW_DISPLAY_DEBUG_VOXEL_GI_LIGHTING: + case VIEW_DISPLAY_DEBUG_VOXEL_GI_EMISSION: + case VIEW_DISPLAY_DEBUG_SCENE_LUMINANCE: + case VIEW_DISPLAY_DEBUG_SSAO: + case VIEW_DISPLAY_DEBUG_SSIL: + case VIEW_DISPLAY_DEBUG_PSSM_SPLITS: + case VIEW_DISPLAY_DEBUG_DECAL_ATLAS: + case VIEW_DISPLAY_DEBUG_AREA_LIGHT_ATLAS: + case VIEW_DISPLAY_DEBUG_SDFGI: + case VIEW_DISPLAY_DEBUG_SDFGI_PROBES: + case VIEW_DISPLAY_DEBUG_GI_BUFFER: + case VIEW_DISPLAY_DEBUG_DISABLE_LOD: + case VIEW_DISPLAY_DEBUG_CLUSTER_OMNI_LIGHTS: + case VIEW_DISPLAY_DEBUG_CLUSTER_SPOT_LIGHTS: + case VIEW_DISPLAY_DEBUG_CLUSTER_AREA_LIGHTS: + case VIEW_DISPLAY_DEBUG_CLUSTER_DECALS: + case VIEW_DISPLAY_DEBUG_CLUSTER_REFLECTION_PROBES: + case VIEW_DISPLAY_DEBUG_OCCLUDERS: + case VIEW_DISPLAY_MOTION_VECTORS: + case VIEW_DISPLAY_INTERNAL_BUFFER: { + static const int display_options[] = { + VIEW_DISPLAY_NORMAL, + VIEW_DISPLAY_WIREFRAME, + VIEW_DISPLAY_OVERDRAW, + VIEW_DISPLAY_UNSHADED, + VIEW_DISPLAY_LIGHTING, + VIEW_DISPLAY_NORMAL_BUFFER, + VIEW_DISPLAY_DEBUG_SHADOW_ATLAS, + VIEW_DISPLAY_DEBUG_DIRECTIONAL_SHADOW_ATLAS, + VIEW_DISPLAY_DEBUG_VOXEL_GI_ALBEDO, + VIEW_DISPLAY_DEBUG_VOXEL_GI_LIGHTING, + VIEW_DISPLAY_DEBUG_VOXEL_GI_EMISSION, + VIEW_DISPLAY_DEBUG_SCENE_LUMINANCE, + VIEW_DISPLAY_DEBUG_SSAO, + VIEW_DISPLAY_DEBUG_SSIL, + VIEW_DISPLAY_DEBUG_GI_BUFFER, + VIEW_DISPLAY_DEBUG_DISABLE_LOD, + VIEW_DISPLAY_DEBUG_PSSM_SPLITS, + VIEW_DISPLAY_DEBUG_DECAL_ATLAS, + VIEW_DISPLAY_DEBUG_AREA_LIGHT_ATLAS, + VIEW_DISPLAY_DEBUG_SDFGI, + VIEW_DISPLAY_DEBUG_SDFGI_PROBES, + VIEW_DISPLAY_DEBUG_CLUSTER_OMNI_LIGHTS, + VIEW_DISPLAY_DEBUG_CLUSTER_SPOT_LIGHTS, + VIEW_DISPLAY_DEBUG_CLUSTER_AREA_LIGHTS, + VIEW_DISPLAY_DEBUG_CLUSTER_DECALS, + VIEW_DISPLAY_DEBUG_CLUSTER_REFLECTION_PROBES, + VIEW_DISPLAY_DEBUG_OCCLUDERS, + VIEW_DISPLAY_MOTION_VECTORS, + VIEW_DISPLAY_INTERNAL_BUFFER, + VIEW_MAX + }; + static const Viewport::DebugDraw debug_draw_modes[] = { + Viewport::DEBUG_DRAW_DISABLED, + Viewport::DEBUG_DRAW_WIREFRAME, + Viewport::DEBUG_DRAW_OVERDRAW, + Viewport::DEBUG_DRAW_UNSHADED, + Viewport::DEBUG_DRAW_LIGHTING, + Viewport::DEBUG_DRAW_NORMAL_BUFFER, + Viewport::DEBUG_DRAW_SHADOW_ATLAS, + Viewport::DEBUG_DRAW_DIRECTIONAL_SHADOW_ATLAS, + Viewport::DEBUG_DRAW_VOXEL_GI_ALBEDO, + Viewport::DEBUG_DRAW_VOXEL_GI_LIGHTING, + Viewport::DEBUG_DRAW_VOXEL_GI_EMISSION, + Viewport::DEBUG_DRAW_SCENE_LUMINANCE, + Viewport::DEBUG_DRAW_SSAO, + Viewport::DEBUG_DRAW_SSIL, + Viewport::DEBUG_DRAW_GI_BUFFER, + Viewport::DEBUG_DRAW_DISABLE_LOD, + Viewport::DEBUG_DRAW_PSSM_SPLITS, + Viewport::DEBUG_DRAW_DECAL_ATLAS, + Viewport::DEBUG_DRAW_AREA_LIGHT_ATLAS, + Viewport::DEBUG_DRAW_SDFGI, + Viewport::DEBUG_DRAW_SDFGI_PROBES, + Viewport::DEBUG_DRAW_CLUSTER_OMNI_LIGHTS, + Viewport::DEBUG_DRAW_CLUSTER_SPOT_LIGHTS, + Viewport::DEBUG_DRAW_CLUSTER_AREA_LIGHTS, + Viewport::DEBUG_DRAW_CLUSTER_DECALS, + Viewport::DEBUG_DRAW_CLUSTER_REFLECTION_PROBES, + Viewport::DEBUG_DRAW_OCCLUDERS, + Viewport::DEBUG_DRAW_MOTION_VECTORS, + Viewport::DEBUG_DRAW_INTERNAL_BUFFER, + }; + + for (int idx = 0; display_options[idx] != VIEW_MAX; idx++) { + int id = display_options[idx]; + int item_idx = view_display_menu->get_popup()->get_item_index(id); + if (item_idx != -1) { + view_display_menu->get_popup()->set_item_checked(item_idx, id == p_option); + } + item_idx = display_submenu->get_item_index(id); + if (item_idx != -1) { + display_submenu->set_item_checked(item_idx, id == p_option); + } + + if (id == p_option) { + viewport->set_debug_draw(debug_draw_modes[idx]); + } + } + } break; + } +} + +void Node3DEditorViewport::_preview_exited_scene() { + preview_camera->disconnect(SceneStringName(toggled), callable_mp(this, &Node3DEditorViewport::_toggle_camera_preview)); + preview_camera->set_pressed(false); + _toggle_camera_preview(false); + preview_camera->connect(SceneStringName(toggled), callable_mp(this, &Node3DEditorViewport::_toggle_camera_preview)); + view_display_menu->show(); +} + +void Node3DEditorViewport::_preview_camera_property_changed() { + if (previewing) { + surface->queue_redraw(); + } +} + +void Node3DEditorViewport::_sync_cursor_from_transform(const Transform3D &p_transform) { + const Basis basis = p_transform.basis; + + view_3d_controller->cursor.eye_pos = p_transform.origin; + view_3d_controller->cursor.x_rot = -basis.get_euler().x; + view_3d_controller->cursor.y_rot = -basis.get_euler().y; + view_3d_controller->cursor.unsnapped_x_rot = view_3d_controller->cursor.x_rot; + view_3d_controller->cursor.unsnapped_y_rot = view_3d_controller->cursor.y_rot; + + real_t distance = view_3d_controller->cursor.distance; + if (view_3d_controller->is_orthogonal()) { + distance = (get_zfar() - get_znear()) / 2.0; + } + view_3d_controller->cursor.pos = p_transform.origin - basis.get_column(2) * distance; +} + +void Node3DEditorViewport::_update_centered_labels() { + if (cinema_label->is_visible()) { + cinema_label->reset_size(); + float cinema_half_width = cinema_label->get_size().width / 2.0f; + cinema_label->set_anchor_and_offset(SIDE_LEFT, 0.5f, -cinema_half_width); + } + + if (locked_label->is_visible()) { + locked_label->reset_size(); + float locked_half_width = locked_label->get_size().width / 2.0f; + locked_label->set_anchor_and_offset(SIDE_LEFT, 0.5f, -locked_half_width); + } +} + +void Node3DEditorViewport::_init_gizmo_instance(int p_idx) { + uint32_t layer = 1 << (GIZMO_BASE_LAYER + p_idx); + + for (int i = 0; i < 3; i++) { + move_gizmo_instance[i] = RS::get_singleton()->instance_create(); + RS::get_singleton()->instance_set_base(move_gizmo_instance[i], spatial_editor->get_move_gizmo(i)->get_rid()); + RS::get_singleton()->instance_set_scenario(move_gizmo_instance[i], get_tree()->get_root()->get_world_3d()->get_scenario()); + RS::get_singleton()->instance_set_visible(move_gizmo_instance[i], false); + RS::get_singleton()->instance_geometry_set_cast_shadows_setting(move_gizmo_instance[i], RSE::SHADOW_CASTING_SETTING_OFF); + RS::get_singleton()->instance_set_layer_mask(move_gizmo_instance[i], layer); + RS::get_singleton()->instance_geometry_set_flag(move_gizmo_instance[i], RSE::INSTANCE_FLAG_IGNORE_OCCLUSION_CULLING, true); + RS::get_singleton()->instance_geometry_set_flag(move_gizmo_instance[i], RSE::INSTANCE_FLAG_USE_BAKED_LIGHT, false); + + move_plane_gizmo_instance[i] = RS::get_singleton()->instance_create(); + RS::get_singleton()->instance_set_base(move_plane_gizmo_instance[i], spatial_editor->get_move_plane_gizmo(i)->get_rid()); + RS::get_singleton()->instance_set_scenario(move_plane_gizmo_instance[i], get_tree()->get_root()->get_world_3d()->get_scenario()); + RS::get_singleton()->instance_set_visible(move_plane_gizmo_instance[i], false); + RS::get_singleton()->instance_geometry_set_cast_shadows_setting(move_plane_gizmo_instance[i], RSE::SHADOW_CASTING_SETTING_OFF); + RS::get_singleton()->instance_set_layer_mask(move_plane_gizmo_instance[i], layer); + RS::get_singleton()->instance_geometry_set_flag(move_plane_gizmo_instance[i], RSE::INSTANCE_FLAG_IGNORE_OCCLUSION_CULLING, true); + RS::get_singleton()->instance_geometry_set_flag(move_plane_gizmo_instance[i], RSE::INSTANCE_FLAG_USE_BAKED_LIGHT, false); + + scale_gizmo_instance[i] = RS::get_singleton()->instance_create(); + RS::get_singleton()->instance_set_base(scale_gizmo_instance[i], spatial_editor->get_scale_gizmo(i)->get_rid()); + RS::get_singleton()->instance_set_scenario(scale_gizmo_instance[i], get_tree()->get_root()->get_world_3d()->get_scenario()); + RS::get_singleton()->instance_set_visible(scale_gizmo_instance[i], false); + RS::get_singleton()->instance_geometry_set_cast_shadows_setting(scale_gizmo_instance[i], RSE::SHADOW_CASTING_SETTING_OFF); + RS::get_singleton()->instance_set_layer_mask(scale_gizmo_instance[i], layer); + RS::get_singleton()->instance_geometry_set_flag(scale_gizmo_instance[i], RSE::INSTANCE_FLAG_IGNORE_OCCLUSION_CULLING, true); + RS::get_singleton()->instance_geometry_set_flag(scale_gizmo_instance[i], RSE::INSTANCE_FLAG_USE_BAKED_LIGHT, false); + + scale_plane_gizmo_instance[i] = RS::get_singleton()->instance_create(); + RS::get_singleton()->instance_set_base(scale_plane_gizmo_instance[i], spatial_editor->get_scale_plane_gizmo(i)->get_rid()); + RS::get_singleton()->instance_set_scenario(scale_plane_gizmo_instance[i], get_tree()->get_root()->get_world_3d()->get_scenario()); + RS::get_singleton()->instance_set_visible(scale_plane_gizmo_instance[i], false); + RS::get_singleton()->instance_geometry_set_cast_shadows_setting(scale_plane_gizmo_instance[i], RSE::SHADOW_CASTING_SETTING_OFF); + RS::get_singleton()->instance_set_layer_mask(scale_plane_gizmo_instance[i], layer); + RS::get_singleton()->instance_geometry_set_flag(scale_plane_gizmo_instance[i], RSE::INSTANCE_FLAG_IGNORE_OCCLUSION_CULLING, true); + RS::get_singleton()->instance_geometry_set_flag(scale_plane_gizmo_instance[i], RSE::INSTANCE_FLAG_USE_BAKED_LIGHT, false); + + axis_gizmo_instance[i] = RS::get_singleton()->instance_create(); + } + + for (int i = 0; i < 3; i++) { + RS::get_singleton()->instance_set_base(axis_gizmo_instance[i], spatial_editor->get_axis_gizmo(i)->get_rid()); + RS::get_singleton()->instance_set_scenario(axis_gizmo_instance[i], get_tree()->get_root()->get_world_3d()->get_scenario()); + RS::get_singleton()->instance_set_visible(axis_gizmo_instance[i], true); + RS::get_singleton()->instance_geometry_set_cast_shadows_setting(axis_gizmo_instance[i], RSE::SHADOW_CASTING_SETTING_OFF); + RS::get_singleton()->instance_set_layer_mask(axis_gizmo_instance[i], layer); + RS::get_singleton()->instance_geometry_set_flag(axis_gizmo_instance[i], RSE::INSTANCE_FLAG_IGNORE_OCCLUSION_CULLING, true); + RS::get_singleton()->instance_geometry_set_flag(axis_gizmo_instance[i], RSE::INSTANCE_FLAG_USE_BAKED_LIGHT, false); + } + + for (int i = 0; i < 4; i++) { + rotate_gizmo_instance[i] = RS::get_singleton()->instance_create(); + RS::get_singleton()->instance_set_base(rotate_gizmo_instance[i], spatial_editor->get_rotate_gizmo(i)->get_rid()); + RS::get_singleton()->instance_set_scenario(rotate_gizmo_instance[i], get_tree()->get_root()->get_world_3d()->get_scenario()); + RS::get_singleton()->instance_set_visible(rotate_gizmo_instance[i], false); + RS::get_singleton()->instance_geometry_set_cast_shadows_setting(rotate_gizmo_instance[i], RSE::SHADOW_CASTING_SETTING_OFF); + RS::get_singleton()->instance_set_layer_mask(rotate_gizmo_instance[i], layer); + RS::get_singleton()->instance_geometry_set_flag(rotate_gizmo_instance[i], RSE::INSTANCE_FLAG_IGNORE_OCCLUSION_CULLING, true); + RS::get_singleton()->instance_geometry_set_flag(rotate_gizmo_instance[i], RSE::INSTANCE_FLAG_USE_BAKED_LIGHT, false); + } + + // Create trackball sphere instance + trackball_sphere_instance = RS::get_singleton()->instance_create(); + RS::get_singleton()->instance_set_base(trackball_sphere_instance, spatial_editor->get_trackball_sphere_gizmo()->get_rid()); + RS::get_singleton()->instance_set_scenario(trackball_sphere_instance, get_tree()->get_root()->get_world_3d()->get_scenario()); + RS::get_singleton()->instance_set_visible(trackball_sphere_instance, false); + RS::get_singleton()->instance_geometry_set_cast_shadows_setting(trackball_sphere_instance, RSE::SHADOW_CASTING_SETTING_OFF); + RS::get_singleton()->instance_set_layer_mask(trackball_sphere_instance, layer); + RS::get_singleton()->instance_geometry_set_flag(trackball_sphere_instance, RSE::INSTANCE_FLAG_IGNORE_OCCLUSION_CULLING, true); + RS::get_singleton()->instance_geometry_set_flag(trackball_sphere_instance, RSE::INSTANCE_FLAG_USE_BAKED_LIGHT, false); +} + +void Node3DEditorViewport::_finish_gizmo_instances() { + ERR_FAIL_NULL(RenderingServer::get_singleton()); + for (int i = 0; i < 3; i++) { + RS::get_singleton()->free_rid(move_gizmo_instance[i]); + RS::get_singleton()->free_rid(move_plane_gizmo_instance[i]); + RS::get_singleton()->free_rid(rotate_gizmo_instance[i]); + RS::get_singleton()->free_rid(scale_gizmo_instance[i]); + RS::get_singleton()->free_rid(scale_plane_gizmo_instance[i]); + RS::get_singleton()->free_rid(axis_gizmo_instance[i]); + } + // Rotation white outline + RS::get_singleton()->free_rid(rotate_gizmo_instance[3]); + + RS::get_singleton()->free_rid(trackball_sphere_instance); +} + +void Node3DEditorViewport::_disable_follow_mode() { + // Exit follow mode by resetting the number of times the follow shortcut was used consecutively. + times_focused_consecutively = 0; +} + +void Node3DEditorViewport::_reset_follow_mode_count() { + bool is_in_follow_mode = times_focused_consecutively >= 2 && times_focused_consecutively % 2 == 0; + if (!is_in_follow_mode) { + times_focused_consecutively = 0; + } +} + +void Node3DEditorViewport::_toggle_camera_preview(bool p_activate) { + ERR_FAIL_COND(p_activate && !preview); + ERR_FAIL_COND(!p_activate && !previewing); + + emit_signal(SNAME("clicked")); + previewing_camera = p_activate; + _update_navigation_controls_visibility(); + + if (!p_activate) { + _pilot_commit_undo_session(); + previewing->disconnect(SceneStringName(tree_exiting), callable_mp(this, &Node3DEditorViewport::_preview_exited_scene)); + previewing->disconnect(CoreStringName(property_list_changed), callable_mp(this, &Node3DEditorViewport::_preview_camera_property_changed)); + previewing = nullptr; + RS::get_singleton()->viewport_attach_camera(viewport->get_viewport_rid(), camera->get_camera()); //restore + if (!preview) { + preview_camera->hide(); + } + pilot_camera->hide(); + pilot_preview_enabled = false; + pilot_camera->set_pressed(false); + + _apply_camera_transform_to_cursor(); + view_3d_controller->update_camera(0); + last_camera_transform = camera->get_global_transform(); + + surface->queue_redraw(); + + } else { + previewing = preview; + previewing->connect(SceneStringName(tree_exiting), callable_mp(this, &Node3DEditorViewport::_preview_exited_scene)); + previewing->connect(CoreStringName(property_list_changed), callable_mp(this, &Node3DEditorViewport::_preview_camera_property_changed)); + RS::get_singleton()->viewport_attach_camera(viewport->get_viewport_rid(), preview->get_camera()); //replace + + _sync_cursor_from_transform(preview->get_global_transform()); + view_3d_controller->update_camera(0); + + pilot_camera->show(); + + if (Input::get_singleton()->is_key_pressed(Key::SHIFT)) { + pilot_camera->set_pressed(true); + } + + surface->queue_redraw(); + } +} + +void Node3DEditorViewport::_toggle_pilot_preview(bool p_activate) { + if (!p_activate) { + _pilot_commit_undo_session(); + } + pilot_preview_enabled = p_activate; + if (p_activate && previewing) { + _sync_cursor_from_transform(previewing->get_global_transform()); + view_3d_controller->update_camera(0); + } +} + +void Node3DEditorViewport::_toggle_cinema_preview(bool p_activate) { + previewing_cinema = p_activate; + _update_navigation_controls_visibility(); + + if (!previewing_cinema) { + if (previewing != nullptr) { + previewing->disconnect(SceneStringName(tree_exited), callable_mp(this, &Node3DEditorViewport::_preview_exited_scene)); + previewing->disconnect(CoreStringName(property_list_changed), callable_mp(this, &Node3DEditorViewport::_preview_camera_property_changed)); + } + + previewing = nullptr; + RS::get_singleton()->viewport_attach_camera(viewport->get_viewport_rid(), camera->get_camera()); //restore + preview_camera->set_pressed(false); + if (!preview) { + preview_camera->hide(); + } else { + preview_camera->show(); + } + view_display_menu->show(); + surface->queue_redraw(); + } +} + +void Node3DEditorViewport::_selection_result_pressed(int p_result) { + if (selection_results_menu.size() <= p_result) { + return; + } + + clicked = selection_results_menu[p_result]->get_instance_id(); + + if (clicked.is_valid()) { + _select_clicked(true); + } + + selection_results_menu.clear(); +} + +void Node3DEditorViewport::_selection_menu_hide() { + selection_results.clear(); + selection_menu->clear(); + selection_menu->reset_size(); +} + +void Node3DEditorViewport::set_can_preview(Camera3D *p_preview) { + preview = p_preview; + + if (!preview_camera->is_pressed() && !previewing_cinema) { + preview_camera->set_visible(p_preview); + } +} + +void Node3DEditorViewport::switch_preview_camera(Camera3D *p_new_camera) { + if (!previewing_camera || !previewing || !p_new_camera || p_new_camera == previewing) { + return; + } + + _pilot_commit_undo_session(); + + previewing->disconnect(SceneStringName(tree_exiting), callable_mp(this, &Node3DEditorViewport::_preview_exited_scene)); + previewing->disconnect(CoreStringName(property_list_changed), callable_mp(this, &Node3DEditorViewport::_preview_camera_property_changed)); + + previewing = p_new_camera; + previewing->connect(SceneStringName(tree_exiting), callable_mp(this, &Node3DEditorViewport::_preview_exited_scene)); + previewing->connect(CoreStringName(property_list_changed), callable_mp(this, &Node3DEditorViewport::_preview_camera_property_changed)); + RS::get_singleton()->viewport_attach_camera(viewport->get_viewport_rid(), p_new_camera->get_camera()); + + _sync_cursor_from_transform(p_new_camera->get_global_transform()); + view_3d_controller->update_camera(0); + + surface->queue_redraw(); +} + +void Node3DEditorViewport::update_transform_gizmo_view() { + if (!is_visible_in_tree()) { + return; + } + + Transform3D xform = spatial_editor->get_gizmo_transform(); + + Transform3D camera_xform = camera->get_transform(); + + if (xform.origin.is_equal_approx(camera_xform.origin)) { + for (int i = 0; i < 3; i++) { + RenderingServer::get_singleton()->instance_set_visible(move_gizmo_instance[i], false); + RenderingServer::get_singleton()->instance_set_visible(move_plane_gizmo_instance[i], false); + RenderingServer::get_singleton()->instance_set_visible(rotate_gizmo_instance[i], false); + RenderingServer::get_singleton()->instance_set_visible(scale_gizmo_instance[i], false); + RenderingServer::get_singleton()->instance_set_visible(scale_plane_gizmo_instance[i], false); + RenderingServer::get_singleton()->instance_set_visible(axis_gizmo_instance[i], false); + } + RenderingServer::get_singleton()->instance_set_visible(rotate_gizmo_instance[3], false); + return; + } + + const Vector3 camz = -camera_xform.get_basis().get_column(2).normalized(); + const Vector3 camy = -camera_xform.get_basis().get_column(1).normalized(); + const Plane p = Plane(camz, camera_xform.origin); + const real_t gizmo_d = MAX(Math::abs(p.distance_to(xform.origin)), CMP_EPSILON); + const real_t d0 = camera->unproject_position(camera_xform.origin + camz * gizmo_d).y; + const real_t d1 = camera->unproject_position(camera_xform.origin + camz * gizmo_d + camy).y; + const real_t dd = MAX(Math::abs(d0 - d1), CMP_EPSILON); + + const real_t gizmo_size = EDITOR_GET("editors/3d/manipulator_gizmo_size"); + // At low viewport heights, multiply the gizmo scale based on the viewport height. + // This prevents the gizmo from growing very large and going outside the viewport. + const int viewport_base_height = 400 * MAX(1, EDSCALE); + gizmo_scale = + (gizmo_size / Math::abs(dd)) * MAX(1, EDSCALE) * + MIN(viewport_base_height, subviewport_container->get_size().height) / viewport_base_height; + Vector3 scale = Vector3(1, 1, 1) * gizmo_scale; + + // If the determinant is zero, we should disable the gizmo from being rendered, + // this prevents supplying bad values to the renderer and then having to filter it out again. + if (xform.basis.determinant() == 0) { + for (int i = 0; i < 3; i++) { + RenderingServer::get_singleton()->instance_set_visible(move_gizmo_instance[i], false); + RenderingServer::get_singleton()->instance_set_visible(move_plane_gizmo_instance[i], false); + RenderingServer::get_singleton()->instance_set_visible(rotate_gizmo_instance[i], false); + RenderingServer::get_singleton()->instance_set_visible(scale_gizmo_instance[i], false); + RenderingServer::get_singleton()->instance_set_visible(scale_plane_gizmo_instance[i], false); + RenderingServer::get_singleton()->instance_set_visible(axis_gizmo_instance[i], false); + } + RenderingServer::get_singleton()->instance_set_visible(rotate_gizmo_instance[3], false); + return; + } + + bool local_coords = spatial_editor->are_local_coords_enabled(); + bool arc_visible = _is_rotation_arc_visible(); + int show_gizmo_flags = EDITOR_GET("editors/3d/show_gizmo_during_rotation"); + + bool keep_gizmo_visible = arc_visible && ((local_coords && (show_gizmo_flags & Node3DEditor::TRANSFORM_MODE_LOCAL)) || (!local_coords && (show_gizmo_flags & Node3DEditor::TRANSFORM_MODE_GLOBAL))); + bool hide_gizmo_during_rotation = arc_visible && !keep_gizmo_visible; + bool hide_gizmo_during_trackball = (_edit.mode == TRANSFORM_ROTATE && _edit.is_trackball); + + int arc_replaces_ring = -1; + if (keep_gizmo_visible) { + switch (_edit.plane) { + case TRANSFORM_X_AXIS: + arc_replaces_ring = 0; + break; + case TRANSFORM_Y_AXIS: + arc_replaces_ring = 1; + break; + case TRANSFORM_Z_AXIS: + arc_replaces_ring = 2; + break; + case TRANSFORM_VIEW: + arc_replaces_ring = 3; + break; + default: + break; + } + } + + bool show_gizmo = spatial_editor->is_gizmo_visible() && !_edit.instant && transform_gizmo_visible && !collision_reposition && !hide_gizmo_during_rotation && !hide_gizmo_during_trackball; + bool show_rotate_gizmo = show_gizmo && (spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_TRANSFORM || spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_ROTATE); + + 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) { + axis_angle = axis_angle.looking_at(xform.basis.get_column(i).normalized(), xform.basis.get_column((i + 1) % 3).normalized()); + } + axis_angle.basis.scale(scale); + axis_angle.origin = xform.origin; + RenderingServer::get_singleton()->instance_set_transform(move_gizmo_instance[i], axis_angle); + RenderingServer::get_singleton()->instance_set_visible(move_gizmo_instance[i], show_gizmo && (spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_TRANSFORM || spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_MOVE)); + 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_TRANSFORM || 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_rotate_gizmo && i != arc_replaces_ring); + 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); + RenderingServer::get_singleton()->instance_set_visible(scale_plane_gizmo_instance[i], show_gizmo && (spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_SCALE)); + RenderingServer::get_singleton()->instance_set_transform(axis_gizmo_instance[i], xform); + } + + Transform3D view_rotation_xform = xform; + view_rotation_xform.orthonormalize(); + + bool can_show_trackball = spatial_editor->is_gizmo_visible() && !_edit.instant && transform_gizmo_visible && !collision_reposition && !hide_gizmo_during_rotation; + bool show_trackball_sphere = can_show_trackball && (spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_TRANSFORM || spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_ROTATE) && !hide_gizmo_during_trackball; + Transform3D trackball_xform = view_rotation_xform; + trackball_xform.basis.scale(scale); + RenderingServer::get_singleton()->instance_set_transform(trackball_sphere_instance, trackball_xform); + RenderingServer::get_singleton()->instance_set_visible(trackball_sphere_instance, show_trackball_sphere); + + bool shrink_view_ring = arc_replaces_ring >= 0 && arc_replaces_ring < 3; + Vector3 view_ring_scale = shrink_view_ring ? scale : scale * (spatial_editor->gizmo_view_rotation_scale / GIZMO_CIRCLE_SIZE); + view_rotation_xform.basis.scale(view_ring_scale); + RenderingServer::get_singleton()->instance_set_transform(rotate_gizmo_instance[3], view_rotation_xform); + RenderingServer::get_singleton()->instance_set_visible(rotate_gizmo_instance[3], show_rotate_gizmo && arc_replaces_ring != 3); + + bool show_axes = spatial_editor->is_gizmo_visible() && _edit.mode != TRANSFORM_NONE && !hide_gizmo_during_trackball; + RenderingServer *rs = RenderingServer::get_singleton(); + rs->instance_set_visible(axis_gizmo_instance[0], show_axes && (_edit.plane == TRANSFORM_X_AXIS || _edit.plane == TRANSFORM_XY || _edit.plane == TRANSFORM_XZ)); + rs->instance_set_visible(axis_gizmo_instance[1], show_axes && (_edit.plane == TRANSFORM_Y_AXIS || _edit.plane == TRANSFORM_XY || _edit.plane == TRANSFORM_YZ)); + rs->instance_set_visible(axis_gizmo_instance[2], show_axes && (_edit.plane == TRANSFORM_Z_AXIS || _edit.plane == TRANSFORM_XZ || _edit.plane == TRANSFORM_YZ)); +} + +void Node3DEditorViewport::update_transform_gizmo_highlight() { + if (!is_visible_in_tree() || !Rect2(Vector2(), surface->get_size()).has_point(surface->get_local_mouse_position())) { + return; + } + _transform_gizmo_select(surface->get_local_mouse_position(), true); +} + +void Node3DEditorViewport::set_state(const Dictionary &p_state) { + if (p_state.has("position")) { + view_3d_controller->cursor.pos = p_state["position"]; + } + if (p_state.has("x_rotation")) { + view_3d_controller->cursor.x_rot = p_state["x_rotation"]; + view_3d_controller->cursor.unsnapped_x_rot = view_3d_controller->cursor.x_rot; + } + if (p_state.has("y_rotation")) { + view_3d_controller->cursor.y_rot = p_state["y_rotation"]; + view_3d_controller->cursor.unsnapped_y_rot = view_3d_controller->cursor.y_rot; + } + if (p_state.has("distance")) { + view_3d_controller->cursor.distance = p_state["distance"]; + } + if (p_state.has("orthogonal")) { + bool orth = p_state["orthogonal"]; + _menu_option(orth ? VIEW_ORTHOGONAL : VIEW_PERSPECTIVE); + } + if (p_state.has("view_type")) { + view_3d_controller->set_view_type(View3DController::ViewType(p_state["view_type"].operator int())); + } + if (p_state.has("auto_orthogonal_enabled")) { + bool enabled = p_state["auto_orthogonal_enabled"]; + view_display_menu->get_popup()->set_item_checked(view_display_menu->get_popup()->get_item_index(VIEW_AUTO_ORTHOGONAL), enabled); + view_3d_controller->set_auto_orthogonal_allowed(enabled); + } + if (p_state.has("auto_orthogonal")) { + if (p_state["auto_orthogonal"]) { + view_3d_controller->force_auto_orthogonal(); + _update_name(); + } + } + if (p_state.has("display_mode")) { + int display = p_state["display_mode"]; + + int idx = view_display_menu->get_popup()->get_item_index(display); + if (idx != -1 && !view_display_menu->get_popup()->is_item_checked(idx)) { + _menu_option(display); + } else { + idx = display_submenu->get_item_index(display); + if (idx != -1 && !display_submenu->is_item_checked(idx)) { + _menu_option(display); + } + } + } + if (p_state.has("lock_rotation")) { + _set_lock_view_rotation(p_state["lock_rotation"]); + } + if (p_state.has("use_environment")) { + bool env = p_state["use_environment"]; + + if (env != camera->get_environment().is_valid()) { + _menu_option(VIEW_ENVIRONMENT); + } + } + if (p_state.has("listener")) { + bool listener = p_state["listener"]; + + int idx = view_display_menu->get_popup()->get_item_index(VIEW_AUDIO_LISTENER); + viewport->set_as_audio_listener_3d(listener); + view_display_menu->get_popup()->set_item_checked(idx, listener); + } + if (p_state.has("doppler")) { + bool doppler = p_state["doppler"]; + + int idx = view_display_menu->get_popup()->get_item_index(VIEW_AUDIO_DOPPLER); + camera->set_doppler_tracking(doppler ? Camera3D::DOPPLER_TRACKING_IDLE_STEP : Camera3D::DOPPLER_TRACKING_DISABLED); + view_display_menu->get_popup()->set_item_checked(idx, doppler); + } + if (p_state.has("gizmos")) { + bool gizmos = p_state["gizmos"]; + + int idx = view_display_menu->get_popup()->get_item_index(VIEW_GIZMOS); + if (view_display_menu->get_popup()->is_item_checked(idx) != gizmos) { + _menu_option(VIEW_GIZMOS); + } + } + if (p_state.has("transform_gizmo")) { + bool transform_gizmo = p_state["transform_gizmo"]; + + int idx = view_display_menu->get_popup()->get_item_index(VIEW_TRANSFORM_GIZMO); + if (view_display_menu->get_popup()->is_item_checked(idx) != transform_gizmo) { + _menu_option(VIEW_TRANSFORM_GIZMO); + } + } + if (p_state.has("grid")) { + bool grid = p_state["grid"]; + + int idx = view_display_menu->get_popup()->get_item_index(VIEW_GRID); + if (view_display_menu->get_popup()->is_item_checked(idx) != grid) { + _menu_option(VIEW_GRID); + } + } + if (p_state.has("information")) { + bool information = p_state["information"]; + + int idx = view_display_menu->get_popup()->get_item_index(VIEW_INFORMATION); + if (view_display_menu->get_popup()->is_item_checked(idx) != information) { + _menu_option(VIEW_INFORMATION); + } + } + if (p_state.has("frame_time")) { + bool fps = p_state["frame_time"]; + + int idx = view_display_menu->get_popup()->get_item_index(VIEW_FRAME_TIME); + if (view_display_menu->get_popup()->is_item_checked(idx) != fps) { + _menu_option(VIEW_FRAME_TIME); + } + } + if (p_state.has("half_res")) { + bool half_res = p_state["half_res"]; + + int idx = view_display_menu->get_popup()->get_item_index(VIEW_HALF_RESOLUTION); + view_display_menu->get_popup()->set_item_checked(idx, half_res); + _update_shrink(); + } + if (p_state.has("cinematic_preview")) { + previewing_cinema = p_state["cinematic_preview"]; + + int idx = view_display_menu->get_popup()->get_item_index(VIEW_CINEMATIC_PREVIEW); + view_display_menu->get_popup()->set_item_checked(idx, previewing_cinema); + + cinema_label->set_visible(previewing_cinema); + if (previewing_cinema) { + _update_centered_labels(); + surface->queue_redraw(); + } + } + + if (preview_camera->is_connected(SceneStringName(toggled), callable_mp(this, &Node3DEditorViewport::_toggle_camera_preview))) { + preview_camera->disconnect(SceneStringName(toggled), callable_mp(this, &Node3DEditorViewport::_toggle_camera_preview)); + } + if (p_state.has("previewing")) { + Node *pv = EditorNode::get_singleton()->get_edited_scene()->get_node(p_state["previewing"]); + if (Object::cast_to(pv)) { + previewing = Object::cast_to(pv); + previewing->connect(SceneStringName(tree_exiting), callable_mp(this, &Node3DEditorViewport::_preview_exited_scene)); + previewing->connect(CoreStringName(property_list_changed), callable_mp(this, &Node3DEditorViewport::_preview_camera_property_changed)); + RS::get_singleton()->viewport_attach_camera(viewport->get_viewport_rid(), previewing->get_camera()); //replace + surface->queue_redraw(); + previewing_camera = true; + _update_navigation_controls_visibility(); + preview_camera->set_pressed(true); + preview_camera->show(); + pilot_camera->show(); + + camera->set_global_transform(view_3d_controller->to_camera_transform()); + + _sync_cursor_from_transform(previewing->get_global_transform()); + view_3d_controller->update_camera(0); + } + } + preview_camera->connect(SceneStringName(toggled), callable_mp(this, &Node3DEditorViewport::_toggle_camera_preview)); +} + +Dictionary Node3DEditorViewport::get_state() const { + Dictionary d; + d["position"] = view_3d_controller->cursor.pos; + d["x_rotation"] = view_3d_controller->cursor.x_rot; + d["y_rotation"] = view_3d_controller->cursor.y_rot; + d["distance"] = view_3d_controller->cursor.distance; + d["use_environment"] = camera->get_environment().is_valid(); + d["orthogonal"] = camera->get_projection() == Camera3D::PROJECTION_ORTHOGONAL; + d["view_type"] = view_3d_controller->get_view_type(); + d["auto_orthogonal"] = view_3d_controller->get_orthogonal_mode() == View3DController::ORTHOGONAL_AUTO; + d["auto_orthogonal_enabled"] = view_display_menu->get_popup()->is_item_checked(view_display_menu->get_popup()->get_item_index(VIEW_AUTO_ORTHOGONAL)); + + // Find selected display mode. + int display_mode = VIEW_DISPLAY_NORMAL; + for (int i = VIEW_DISPLAY_NORMAL; i < VIEW_DISPLAY_ADVANCED; i++) { + if (view_display_menu->get_popup()->is_item_checked(view_display_menu->get_popup()->get_item_index(i))) { + display_mode = i; + break; + } + } + for (int i = VIEW_DISPLAY_ADVANCED + 1; i < VIEW_DISPLAY_MAX; i++) { + if (display_submenu->is_item_checked(display_submenu->get_item_index(i))) { + display_mode = i; + break; + } + } + d["display_mode"] = display_mode; + + d["listener"] = viewport->is_audio_listener_3d(); + d["doppler"] = view_display_menu->get_popup()->is_item_checked(view_display_menu->get_popup()->get_item_index(VIEW_AUDIO_DOPPLER)); + d["gizmos"] = view_display_menu->get_popup()->is_item_checked(view_display_menu->get_popup()->get_item_index(VIEW_GIZMOS)); + d["transform_gizmo"] = view_display_menu->get_popup()->is_item_checked(view_display_menu->get_popup()->get_item_index(VIEW_TRANSFORM_GIZMO)); + d["grid"] = view_display_menu->get_popup()->is_item_checked(view_display_menu->get_popup()->get_item_index(VIEW_GRID)); + d["information"] = view_display_menu->get_popup()->is_item_checked(view_display_menu->get_popup()->get_item_index(VIEW_INFORMATION)); + d["frame_time"] = view_display_menu->get_popup()->is_item_checked(view_display_menu->get_popup()->get_item_index(VIEW_FRAME_TIME)); + d["half_res"] = view_display_menu->get_popup()->is_item_checked(view_display_menu->get_popup()->get_item_index(VIEW_HALF_RESOLUTION)); + d["cinematic_preview"] = view_display_menu->get_popup()->is_item_checked(view_display_menu->get_popup()->get_item_index(VIEW_CINEMATIC_PREVIEW)); + if (previewing) { + d["previewing"] = EditorNode::get_singleton()->get_edited_scene()->get_path_to(previewing); + } + d["lock_rotation"] = view_3d_controller->is_locking_rotation(); + + return d; +} + +void Node3DEditorViewport::_bind_methods() { + ADD_SIGNAL(MethodInfo("toggle_maximize_view", PropertyInfo(Variant::OBJECT, "viewport"))); + ADD_SIGNAL(MethodInfo("clicked")); +} + +void Node3DEditorViewport::reset() { + view_3d_controller->set_orthogonal(false); + view_3d_controller->set_view_type(View3DController::VIEW_TYPE_USER); + message_time = 0; + message = ""; + last_message = ""; + + view_3d_controller->cursor = View3DController::Cursor(); +} + +void Node3DEditorViewport::focus_selection() { + Vector3 center; + int count = 0; + + const List &selection = editor_selection->get_top_selected_node_list(); + focused_node_id = ObjectID(); + if (!selection.is_empty()) { + focused_node_id = selection.front()->get()->get_instance_id(); + } + + for (Node *node : selection) { + Node3D *node_3d = Object::cast_to(node); + if (!node_3d) { + continue; + } + + Node3DEditorSelectedItem *se = editor_selection->get_node_editor_data(node_3d); + if (!se) { + continue; + } + + if (se->gizmo.is_valid()) { + for (const KeyValue &GE : se->subgizmos) { + const Vector3 pos = node_3d->get_global_gizmo_transform().xform(se->gizmo->get_subgizmo_transform(GE.key).origin); + if (pos.is_finite()) { + center += pos; + count++; + } + } + } else { + const Vector3 pos = node_3d->get_global_gizmo_transform().origin; + if (pos.is_finite()) { + center += pos; + count++; + } + } + } + + if (count > 1) { + center /= count; + } + + view_3d_controller->cursor.pos = center; +} + +void Node3DEditorViewport::assign_pending_data_pointers(Node3D *p_preview_node, AABB *p_preview_bounds, AcceptDialog *p_accept) { + preview_node = p_preview_node; + preview_bounds = p_preview_bounds; + accept = p_accept; +} + +void _insert_rid_recursive(Node *node, HashSet &rids) { + CollisionObject3D *co = Object::cast_to(node); + + if (co) { + rids.insert(co->get_rid()); + } else if (node->is_class("CSGShape3D")) { // HACK: We should avoid referencing module logic. + rids.insert(node->call("_get_root_collision_instance")); + } + + for (int i = 0; i < node->get_child_count(); i++) { + Node *child = node->get_child(i); + _insert_rid_recursive(child, rids); + } +} + +Vector3 Node3DEditorViewport::_get_instance_position(const Point2 &p_pos, Node3D *p_node) const { + const float MAX_DISTANCE = 50.0; + const float FALLBACK_DISTANCE = 5.0; + + Vector3 world_ray = get_ray(p_pos); + Vector3 world_pos = get_ray_pos(p_pos); + + PhysicsDirectSpaceState3D *ss = get_tree()->get_root()->get_world_3d()->get_direct_space_state(); + + HashSet rids; + + if (preview_node && preview_node->get_child_count() > 0) { + _insert_rid_recursive(preview_node, rids); + } else if (!preview_node->is_inside_tree() && !ruler->is_inside_tree()) { + const List &selection = editor_selection->get_top_selected_node_list(); + + Node3D *first_selected_node = Object::cast_to(selection.front()->get()); + + if (first_selected_node) { + _insert_rid_recursive(first_selected_node, rids); + } + } + + PhysicsDirectSpaceState3D::RayParameters ray_params; + ray_params.exclude = rids; + ray_params.from = world_pos; + ray_params.to = world_pos + world_ray * camera->get_far(); + + PhysicsDirectSpaceState3D::RayResult result; + if (ss->intersect_ray(ray_params, result) && (preview_node->get_child_count() > 0 || !preview_node->is_inside_tree())) { + // Calculate an offset for the `p_node` such that the its bounding box is on top of and touching the contact surface's plane. + + // Use the Gram-Schmidt process to get an orthonormal Basis aligned with the surface normal. + const Vector3 bb_basis_x = result.normal; + Vector3 bb_basis_y = Vector3(0, 1, 0); + bb_basis_y = bb_basis_y - bb_basis_y.project(bb_basis_x); + if (bb_basis_y.is_zero_approx()) { + bb_basis_y = Vector3(0, 0, 1); + bb_basis_y = bb_basis_y - bb_basis_y.project(bb_basis_x); + } + bb_basis_y = bb_basis_y.normalized(); + const Vector3 bb_basis_z = bb_basis_x.cross(bb_basis_y); + const Basis bb_basis = Basis(bb_basis_x, bb_basis_y, bb_basis_z); + + // This normal-aligned Basis allows us to create an AABB that can fit on the surface plane as snugly as possible. + const Transform3D bb_transform = Transform3D(bb_basis, p_node->get_global_transform().origin); + const AABB p_node_bb = _calculate_spatial_bounds(p_node, true, &bb_transform); + // The x-axis's alignment with the surface normal also makes it trivial to get the distance from `p_node`'s origin at (0, 0, 0) to the correct AABB face. + const float offset_distance = -p_node_bb.position.x; + + // `result_offset` is in global space. + const Vector3 result_offset = result.position + result.normal * offset_distance; + + return result_offset; + } + + const bool is_orthogonal = camera->get_projection() == Camera3D::PROJECTION_ORTHOGONAL; + + // The XZ plane. + Vector3 intersection; + Plane plane(Vector3(0, 1, 0)); + if (plane.intersects_ray(world_pos, world_ray, &intersection)) { + if (is_orthogonal || world_pos.distance_to(intersection) <= MAX_DISTANCE) { + return intersection; + } + } + + // Plane facing the camera using fallback distance. + if (is_orthogonal) { + plane = Plane(world_ray, view_3d_controller->cursor.pos - world_ray * (view_3d_controller->cursor.distance - FALLBACK_DISTANCE)); + } else { + plane = Plane(world_ray, world_pos + world_ray * FALLBACK_DISTANCE); + } + if (plane.intersects_ray(world_pos, world_ray, &intersection)) { + return intersection; + } + + // Not likely, but just in case... + return world_pos + world_ray * FALLBACK_DISTANCE; +} + +AABB Node3DEditorViewport::_calculate_spatial_bounds(const Node3D *p_parent, bool p_omit_top_level, const Transform3D *p_bounds_orientation) { + if (!p_parent) { + return AABB(Vector3(-0.2, -0.2, -0.2), Vector3(0.4, 0.4, 0.4)); + } + const Transform3D parent_transform = p_parent->get_global_transform(); + if (!parent_transform.is_finite()) { + return AABB(); + } + AABB bounds; + + Transform3D bounds_orientation; + Transform3D xform_to_top_level_parent_space; + if (p_bounds_orientation) { + bounds_orientation = *p_bounds_orientation; + xform_to_top_level_parent_space = bounds_orientation.affine_inverse() * parent_transform; + } else { + bounds_orientation = parent_transform; + } + + const VisualInstance3D *visual_instance = Object::cast_to(p_parent); + if (visual_instance) { + bounds = visual_instance->get_aabb(); + } else { + bounds = AABB(); + } + bounds = xform_to_top_level_parent_space.xform(bounds); + + for (int i = 0; i < p_parent->get_child_count(); i++) { + const Node3D *child = Object::cast_to(p_parent->get_child(i)); + if (child && !(p_omit_top_level && child->is_set_as_top_level())) { + const AABB child_bounds = _calculate_spatial_bounds(child, p_omit_top_level, &bounds_orientation); + bounds.merge_with(child_bounds); + } + } + + return bounds; +} + +Node *Node3DEditorViewport::_sanitize_preview_node(Node *p_node) const { + Node3D *node_3d = Object::cast_to(p_node); + if (node_3d == nullptr) { + Node3D *replacement_node = memnew(Node3D); + replacement_node->set_name(p_node->get_name()); + p_node->replace_by(replacement_node); + memdelete(p_node); + p_node = replacement_node; + } else { + VisualInstance3D *visual_instance = Object::cast_to(node_3d); + if (visual_instance == nullptr) { + Node3D *replacement_node = memnew(Node3D); + replacement_node->set_name(node_3d->get_name()); + replacement_node->set_visible(node_3d->is_visible()); + replacement_node->set_transform(node_3d->get_transform()); + replacement_node->set_rotation_edit_mode(node_3d->get_rotation_edit_mode()); + replacement_node->set_rotation_order(node_3d->get_rotation_order()); + replacement_node->set_as_top_level(node_3d->is_set_as_top_level()); + p_node->replace_by(replacement_node); + memdelete(p_node); + p_node = replacement_node; + } + } + + for (int i = 0; i < p_node->get_child_count(); i++) { + _sanitize_preview_node(p_node->get_child(i)); + } + + return p_node; +} + +void Node3DEditorViewport::_create_preview_node(const Vector &files) const { + bool add_preview = false; + for (const String &path : files) { + Ref res = ResourceLoader::load(path); + ERR_CONTINUE(res.is_null()); + + Ref scene = res; + if (scene.is_valid()) { + Node *instance = scene->instantiate(); + if (instance) { + instance = _sanitize_preview_node(instance); + preview_node->add_child(instance); + Node3D *node_3d = Object::cast_to(instance); + if (node_3d) { + node_3d->set_as_top_level(false); + } + } + add_preview = true; + } + + Ref mesh = res; + if (mesh.is_valid()) { + MeshInstance3D *mesh_instance = memnew(MeshInstance3D); + mesh_instance->set_mesh(mesh); + preview_node->add_child(mesh_instance); + add_preview = true; + } + + Ref audio = res; + if (audio.is_valid()) { + Sprite3D *sprite = memnew(Sprite3D); + sprite->set_texture(get_editor_theme_icon(SNAME("Gizmo3DSamplePlayer"))); + sprite->set_billboard_mode(StandardMaterial3D::BILLBOARD_ENABLED); + sprite->set_pixel_size(0.005); + preview_node->add_child(sprite); + add_preview = true; + } + } + if (add_preview) { + EditorNode::get_singleton()->get_scene_root()->add_child(preview_node); + *preview_bounds = _calculate_spatial_bounds(preview_node); + } +} + +void Node3DEditorViewport::_remove_preview_node() { + tooltip_panel->hide(); + + set_message(""); + if (preview_node->get_parent()) { + for (int i = preview_node->get_child_count() - 1; i >= 0; i--) { + Node *node = preview_node->get_child(i); + node->queue_free(); + preview_node->remove_child(node); + } + EditorNode::get_singleton()->get_scene_root()->remove_child(preview_node); + } +} + +bool Node3DEditorViewport::_apply_preview_material(ObjectID p_target, const Point2 &p_point) const { + _reset_preview_material(); + + if (p_target.is_null()) { + return false; + } + + spatial_editor->set_preview_material_target(p_target); + + Object *target_inst = ObjectDB::get_instance(p_target); + + bool is_ctrl = Input::get_singleton()->is_key_pressed(Key::CTRL); + + MeshInstance3D *mesh_instance = Object::cast_to(target_inst); + if (is_ctrl && mesh_instance) { + Ref mesh = mesh_instance->get_mesh(); + int surface_count = mesh->get_surface_count(); + + Vector3 world_ray = get_ray(p_point); + Vector3 world_pos = get_ray_pos(p_point); + + int closest_surface = -1; + float closest_dist = 1e20; + + Transform3D gt = mesh_instance->get_global_transform(); + + Transform3D ai = gt.affine_inverse(); + Vector3 xform_ray = ai.basis.xform(world_ray).normalized(); + Vector3 xform_pos = ai.xform(world_pos); + + for (int surface_idx = 0; surface_idx < surface_count; surface_idx++) { + Ref surface_mesh = mesh->generate_surface_triangle_mesh(surface_idx); + + Vector3 rpos, rnorm; + if (surface_mesh->intersect_ray(xform_pos, xform_ray, rpos, rnorm)) { + Vector3 hitpos = gt.xform(rpos); + + const real_t dist = world_pos.distance_to(hitpos); + + if (dist < 0) { + continue; + } + + if (dist < closest_dist) { + closest_surface = surface_idx; + closest_dist = dist; + } + } + } + + if (closest_surface == -1) { + return false; + } + + spatial_editor->set_preview_material_surface(closest_surface); + spatial_editor->set_preview_reset_material(mesh_instance->get_surface_override_material(closest_surface)); + mesh_instance->set_surface_override_material(closest_surface, spatial_editor->get_preview_material()); + + return true; + } + + GeometryInstance3D *geometry_instance = Object::cast_to(target_inst); + if (geometry_instance) { + spatial_editor->set_preview_material_surface(-1); + spatial_editor->set_preview_reset_material(geometry_instance->get_material_override()); + geometry_instance->set_material_override(spatial_editor->get_preview_material()); + return true; + } + + return false; +} + +void Node3DEditorViewport::_reset_preview_material() const { + ObjectID last_target = spatial_editor->get_preview_material_target(); + if (last_target.is_null()) { + return; + } + Object *last_target_inst = ObjectDB::get_instance(last_target); + + MeshInstance3D *mesh_instance = Object::cast_to(last_target_inst); + GeometryInstance3D *geometry_instance = Object::cast_to(last_target_inst); + if (mesh_instance && spatial_editor->get_preview_material_surface() != -1) { + mesh_instance->set_surface_override_material(spatial_editor->get_preview_material_surface(), spatial_editor->get_preview_reset_material()); + } else if (geometry_instance) { + geometry_instance->set_material_override(spatial_editor->get_preview_reset_material()); + } +} + +void Node3DEditorViewport::_remove_preview_material() { + tooltip_panel->hide(); + + spatial_editor->set_preview_material(Ref()); + spatial_editor->set_preview_reset_material(Ref()); + spatial_editor->set_preview_material_target(ObjectID()); + spatial_editor->set_preview_material_surface(-1); +} + +bool Node3DEditorViewport::_cyclical_dependency_exists(const String &p_target_scene_path, Node *p_desired_node) const { + if (p_desired_node->get_scene_file_path() == p_target_scene_path) { + return true; + } + + int childCount = p_desired_node->get_child_count(); + for (int i = 0; i < childCount; i++) { + Node *child = p_desired_node->get_child(i); + if (_cyclical_dependency_exists(p_target_scene_path, child)) { + return true; + } + } + return false; +} + +bool Node3DEditorViewport::_create_instance(Node *p_parent, const String &p_path, const Point2 &p_point) { + Ref res = ResourceLoader::load(p_path); + ERR_FAIL_COND_V(res.is_null(), false); + + Ref scene = res; + Ref mesh = res; + + Node *instantiated_scene = nullptr; + + if (mesh.is_valid() || scene.is_valid()) { + if (mesh.is_valid()) { + MeshInstance3D *mesh_instance = memnew(MeshInstance3D); + mesh_instance->set_mesh(mesh); + + // Adjust casing according to project setting. The file name is expected to be in snake_case, but will work for others. + const String &node_name = Node::adjust_name_casing(p_path.get_file().get_basename()); + if (!node_name.is_empty()) { + mesh_instance->set_name(node_name); + } + + instantiated_scene = mesh_instance; + } else { + if (scene.is_null()) { // invalid scene + return false; + } else { + instantiated_scene = scene->instantiate(PackedScene::GEN_EDIT_STATE_INSTANCE); + } + } + } + + if (instantiated_scene == nullptr) { + return false; + } + + if (!EditorNode::get_singleton()->get_edited_scene()->get_scene_file_path().is_empty()) { // Cyclic instantiation. + if (_cyclical_dependency_exists(EditorNode::get_singleton()->get_edited_scene()->get_scene_file_path(), instantiated_scene)) { + memdelete(instantiated_scene); + return false; + } + } + + if (scene.is_valid()) { + instantiated_scene->set_scene_file_path(ProjectSettings::get_singleton()->localize_path(p_path)); + } + + EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); + undo_redo->add_do_method(p_parent, "add_child", instantiated_scene, true); + undo_redo->add_do_method(instantiated_scene, "set_owner", EditorNode::get_singleton()->get_edited_scene()); + undo_redo->add_do_reference(instantiated_scene); + undo_redo->add_undo_method(p_parent, "remove_child", instantiated_scene); + undo_redo->add_do_method(editor_selection, "add_node", instantiated_scene); + + String new_name = p_parent->validate_child_name(instantiated_scene); + EditorDebuggerNode *ed = EditorDebuggerNode::get_singleton(); + undo_redo->add_do_method(ed, "live_debug_instantiate_node", EditorNode::get_singleton()->get_edited_scene()->get_path_to(p_parent), p_path, new_name); + undo_redo->add_undo_method(ed, "live_debug_remove_node", NodePath(String(EditorNode::get_singleton()->get_edited_scene()->get_path_to(p_parent)) + "/" + new_name)); + + Node3D *node3d = Object::cast_to(instantiated_scene); + if (node3d) { + Transform3D parent_tf; + Node3D *parent_node3d = Object::cast_to(p_parent); + if (parent_node3d) { + parent_tf = parent_node3d->get_global_gizmo_transform(); + } + + Transform3D new_tf = node3d->get_transform(); + if (node3d->is_set_as_top_level()) { + new_tf.origin += preview_node_pos; + } else { + new_tf.origin = parent_tf.affine_inverse().xform(preview_node_pos + node3d->get_position()); + new_tf.basis = parent_tf.affine_inverse().basis * new_tf.basis; + } + + undo_redo->add_do_method(instantiated_scene, "set_transform", new_tf); + } + + return true; +} + +bool Node3DEditorViewport::_create_audio_node(Node *p_parent, const String &p_path, const Point2 &p_point) { + Ref audio = ResourceLoader::load(p_path); + ERR_FAIL_COND_V(audio.is_null(), false); + + AudioStreamPlayer3D *audio_player = memnew(AudioStreamPlayer3D); + audio_player->set_stream(audio); + + // Adjust casing according to project setting. The file name is expected to be in snake_case, but will work for others. + const String &node_name = Node::adjust_name_casing(p_path.get_file().get_basename()); + if (!node_name.is_empty()) { + audio_player->set_name(node_name); + } + + EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); + undo_redo->add_do_method(p_parent, "add_child", audio_player, true); + undo_redo->add_do_method(audio_player, "set_owner", EditorNode::get_singleton()->get_edited_scene()); + undo_redo->add_do_reference(audio_player); + undo_redo->add_undo_method(p_parent, "remove_child", audio_player); + undo_redo->add_do_method(editor_selection, "add_node", audio_player); + + const String new_name = p_parent->validate_child_name(audio_player); + EditorDebuggerNode *ed = EditorDebuggerNode::get_singleton(); + undo_redo->add_do_method(ed, "live_debug_create_node", EditorNode::get_singleton()->get_edited_scene()->get_path_to(p_parent), audio_player->get_class(), new_name); + undo_redo->add_undo_method(ed, "live_debug_remove_node", NodePath(String(EditorNode::get_singleton()->get_edited_scene()->get_path_to(p_parent)) + "/" + new_name)); + + Transform3D parent_tf; + Node3D *parent_node3d = Object::cast_to(p_parent); + if (parent_node3d) { + parent_tf = parent_node3d->get_global_gizmo_transform(); + } + + Transform3D new_tf = audio_player->get_transform(); + new_tf.origin = parent_tf.affine_inverse().xform(preview_node_pos + audio_player->get_position()); + new_tf.basis = parent_tf.affine_inverse().basis * new_tf.basis; + + undo_redo->add_do_method(audio_player, "set_transform", new_tf); + + return true; +} + +void Node3DEditorViewport::_perform_drop_data() { + EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); + if (spatial_editor->get_preview_material_target().is_valid()) { + GeometryInstance3D *geometry_instance = ObjectDB::get_instance(spatial_editor->get_preview_material_target()); + MeshInstance3D *mesh_instance = ObjectDB::get_instance(spatial_editor->get_preview_material_target()); + if (mesh_instance && spatial_editor->get_preview_material_surface() != -1) { + undo_redo->create_action(vformat(TTR("Set Surface %d Override Material"), spatial_editor->get_preview_material_surface())); + undo_redo->add_do_method(geometry_instance, "set_surface_override_material", spatial_editor->get_preview_material_surface(), spatial_editor->get_preview_material()); + undo_redo->add_undo_method(geometry_instance, "set_surface_override_material", spatial_editor->get_preview_material_surface(), spatial_editor->get_preview_reset_material()); + undo_redo->commit_action(); + } else if (geometry_instance) { + undo_redo->create_action(TTR("Set Material Override")); + undo_redo->add_do_method(geometry_instance, "set_material_override", spatial_editor->get_preview_material()); + undo_redo->add_undo_method(geometry_instance, "set_material_override", spatial_editor->get_preview_reset_material()); + undo_redo->commit_action(); + } + + _remove_preview_material(); + return; + } + + _remove_preview_node(); + + PackedStringArray error_files; + + undo_redo->create_action(TTR("Create Node"), UndoRedo::MERGE_DISABLE, target_node); + undo_redo->add_do_method(editor_selection, "clear"); + + for (int i = 0; i < selected_files.size(); i++) { + String path = selected_files[i]; + Ref res = ResourceLoader::load(path); + if (res.is_null()) { + continue; + } + + Ref scene = res; + Ref mesh = res; + if (mesh.is_valid() || scene.is_valid()) { + if (!_create_instance(target_node, path, drop_pos)) { + error_files.push_back(path.get_file()); + } + } + + Ref audio = res; + if (audio.is_valid()) { + if (!_create_audio_node(target_node, path, drop_pos)) { + error_files.push_back(path.get_file()); + } + } + } + + undo_redo->commit_action(); + + if (error_files.size() > 0) { + accept->set_text(vformat(TTR("Error instantiating scene from %s."), String(", ").join(error_files))); + accept->popup_centered(); + } +} + +void Node3DEditorViewport::_show_tooltip(const String &p_title, const String &p_description) const { + tooltip_panel->set_text( + vformat("[font_size=%s][b][color=%s]%s[/color][/b][/font_size]\n%s", + get_theme_default_font_size() + 2, + get_theme_color(SNAME("accent_color"), EditorStringName(Editor)).to_html(false), + p_title, p_description)); + tooltip_panel->show(); +} + +bool Node3DEditorViewport::can_drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) { + if (previewing) { + return false; + } + + if (p_point == Vector2(Math::INF, Math::INF)) { + tooltip_panel->hide(); + return false; + } + preview_node_viewport_pos = p_point; + + Dictionary d = p_data; + if (!d.has("type") || String(d["type"]) != "files") { + tooltip_panel->hide(); + return false; + } + Vector files = d["files"]; + + // If we already have a preview material or preview node, + // We just need to update them. + if (spatial_editor->get_preview_material().is_valid()) { + ObjectID new_preview_material_target = _select_ray(p_point); + return _apply_preview_material(new_preview_material_target, p_point); + } + + if (preview_node->is_inside_tree()) { + preview_node_viewport_pos = p_point; + update_preview_node = true; + return true; + } + + // If we don't already have a preview material or preview node, + // it means that this is the first time we are visiting this function. + // In that case, we need to check that the file(s) are droppable. + bool is_cyclical_dep = false; + String error_file; + + enum { + SCENE = 1 << 0, + TEXTURE = 1 << 1, + AUDIO = 1 << 2, + MESH = 1 << 3, + MATERIAL = 1 << 4, + }; + int instantiate_type = 0; + + // Track whether a type other than PackedScene is valid to stop checking them and only + // continue to check if the rest of the scenes are valid (don't have cyclic dependencies). + bool is_other_valid = false; + // Check if at least one of the dragged files is a mesh, material, texture, or scene. + for (int i = 0; i < files.size(); i++) { + const String &res_type = ResourceLoader::get_resource_type(files[i]); + bool is_scene = ClassDB::is_parent_class(res_type, "PackedScene"); + bool is_mesh = ClassDB::is_parent_class(res_type, "Mesh"); + bool is_material = ClassDB::is_parent_class(res_type, "Material"); + bool is_texture = ClassDB::is_parent_class(res_type, "Texture"); + bool is_audio = ClassDB::is_parent_class(res_type, "AudioStream"); + + if (is_mesh || is_scene || is_material || is_texture || is_audio) { + Ref res = ResourceLoader::load(files[i]); + if (res.is_null()) { + continue; + } + Ref scn = res; + Ref mesh = res; + Ref mat = res; + Ref tex = res; + Ref audio = res; + if (scn.is_valid()) { + Node *instantiated_scene = scn->instantiate(PackedScene::GEN_EDIT_STATE_INSTANCE); + if (!instantiated_scene) { + continue; + } + Node *edited_scene = EditorNode::get_singleton()->get_edited_scene(); + if (edited_scene && !edited_scene->get_scene_file_path().is_empty() && _cyclical_dependency_exists(edited_scene->get_scene_file_path(), instantiated_scene)) { + memdelete(instantiated_scene); + is_cyclical_dep = true; + error_file = files[i].get_file(); + break; + } + memdelete(instantiated_scene); + instantiate_type |= SCENE; + } else if (!is_other_valid && mat.is_valid()) { + Ref base_mat = res; + Ref shader_mat = res; + + if (base_mat.is_null() && shader_mat.is_null()) { + continue; + } + + spatial_editor->set_preview_material(mat); + is_other_valid = true; + instantiate_type |= MATERIAL; + continue; + } else if (!is_other_valid && mesh.is_valid()) { + // Let the mesh pass. + is_other_valid = true; + instantiate_type |= MESH; + } else if (!is_other_valid && tex.is_valid()) { + Ref new_mat; + new_mat.instantiate(); + new_mat->set_texture(BaseMaterial3D::TEXTURE_ALBEDO, tex); + + spatial_editor->set_preview_material(new_mat); + is_other_valid = true; + instantiate_type |= TEXTURE; + continue; + } else if (!is_other_valid && audio.is_valid()) { + is_other_valid = true; + instantiate_type |= AUDIO; + } else { + continue; + } + } + } + + String title = TTRN("Can't drop the file...", "Can't drop the files...", files.size()); + if (is_cyclical_dep) { + _show_tooltip(title, vformat(TTR("Circular dependency found at %s."), error_file)); + return false; + } + + if (instantiate_type == 0) { + _show_tooltip(title, TTR("File format is not supported.")); + return false; + } + + // Only droppable file(s), on first frame, will make it to this point. + // Hence it is a good place to create the previews and tooltips. + _create_preview_node(files); + preview_node->hide(); + + String desc = "[ul]" + + TTRN("[b]Default:[/b] Add as sibling of selected node (except when root is selected).", + "[b]Default:[/b] Add as siblings of selected node (except when root is selected).", + files.size()) + + "\n" + + TTRN("[b]Hold Shift:[/b] Add as child of selected node.", + "[b]Hold Shift:[/b] Add as children of selected node.", + files.size()) + + "\n" + + TTRN("[b]Hold Alt:[/b] Add as child of root node.", + "[b]Hold Alt:[/b] Add as children of root node.", + files.size()); + + if (files.size() > 1) { + title = TTR("Dropping multiple files..."); + } else if (instantiate_type & SCENE) { + title = TTR("Dropping a Scene file..."); + } else if (instantiate_type & MESH) { + title = TTR("Dropping a Mesh file..."); + } else if (instantiate_type & AUDIO) { + title = TTR("Dropping an Audio file..."); + } else if (instantiate_type & MATERIAL || instantiate_type & TEXTURE) { + title = TTR("Dropping a Material..."); + desc = "[ul]"; + desc += vformat(TTR("[b]Default:[/b] Place in Geometry's Material Override slot.") + + "\n" + TTR("[b]Hold %s:[/b] Place in Mesh's Surface Material Override slot."), + keycode_get_string((Key)KeyModifierMask::CMD_OR_CTRL)); + } + desc += "[/ul]"; + + _show_tooltip(title, desc); + + return true; +} + +void Node3DEditorViewport::drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) { + if (!can_drop_data_fw(p_point, p_data, p_from)) { + return; + } + + bool is_shift = Input::get_singleton()->is_key_pressed(Key::SHIFT); + bool is_alt = Input::get_singleton()->is_key_pressed(Key::ALT); + + selected_files.clear(); + Dictionary d = p_data; + if (d.has("type") && String(d["type"]) == "files") { + selected_files = d["files"]; + } + + const List &selected_nodes = EditorNode::get_singleton()->get_editor_selection()->get_top_selected_node_list(); + Node *root_node = EditorNode::get_singleton()->get_edited_scene(); + if (selected_nodes.size() > 0) { + Node *selected_node = selected_nodes.front()->get(); + if (is_alt) { + target_node = root_node; + } else if (is_shift) { + target_node = selected_node; + } else { // Default behavior. + target_node = (selected_node != root_node) ? selected_node->get_parent() : root_node; + } + } else { + if (root_node) { + target_node = root_node; + } else { + // Create a root node so we can add child nodes to it. + SceneTreeDock::get_singleton()->add_root_node(memnew(Node3D)); + target_node = get_tree()->get_edited_scene_root(); + } + } + + drop_pos = p_point; + + _perform_drop_data(); +} + +void Node3DEditorViewport::begin_transform(TransformMode p_mode, bool instant) { + if (previewing) { + return; + } + + if (get_selected_count() > 0) { + if (!_has_unlocked_selection()) { + return; + } + _edit.children_original_globals.clear(); + + _edit.mode = p_mode; + _compute_edit(_edit.mouse_pos); + _edit.instant = instant; + _edit.initial_click_vector = Vector3(); + _edit.previous_rotation_vector = Vector3(); + _edit.accumulated_rotation_angle = 0.0; + _edit.rotation_angle = 0.0; + _edit.gizmo_initiated = false; + switch (p_mode) { + case TRANSFORM_ROTATE: + _edit.show_rotation_line = true; + set_message(vformat(TTR("Rotating %s degrees."), String::num(0, 0))); + break; + case TRANSFORM_TRANSLATE: + set_message(vformat(TTR("Translating: %s"), vformat("%.0v", Vector3()))); + break; + case TRANSFORM_SCALE: + set_message(vformat(TTR("Scaling: %s"), vformat("%.0v", Vector3()))); + break; + default: + break; + } + update_transform_gizmo_view(); + set_process_input(instant); + surface->queue_redraw(); + } +} + +// Apply the current transform operation. +void Node3DEditorViewport::commit_transform() { + ERR_FAIL_COND(_edit.mode == TRANSFORM_NONE); + static const char *_transform_name[4] = { + TTRC("None"), + TTRC("Rotate"), + // TRANSLATORS: This refers to the movement that changes the position of an object. + TTRC("Translate"), + TTRC("Scale"), + }; + EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); + undo_redo->create_action(_transform_name[_edit.mode]); + + const List &selection = editor_selection->get_top_selected_node_list(); + + for (Node *E : selection) { + Node3D *sp = Object::cast_to(E); + if (!sp) { + continue; + } + + Node3DEditorSelectedItem *se = editor_selection->get_node_editor_data(sp); + if (!se) { + continue; + } + + undo_redo->add_do_method(sp, "set_transform", sp->get_local_gizmo_transform()); + undo_redo->add_undo_method(sp, "set_transform", se->original_local); + } + + if (!_edit.children_original_globals.is_empty()) { + for (const KeyValue &pair : _edit.children_original_globals) { + Node3D *child = pair.key; + Transform3D original_global = pair.value; + Transform3D current_global = child->get_global_transform(); + + undo_redo->add_do_method(child, "set_global_transform", current_global); + undo_redo->add_undo_method(child, "set_global_transform", original_global); + } + } + + undo_redo->commit_action(); + + collision_reposition = false; + finish_transform(); + _reset_follow_mode_count(); + set_message(""); +} + +void Node3DEditorViewport::apply_transform(Vector3 p_motion, double p_snap) { + // View-plane translate/scale always uses global coords; rotation and axis operations respect local/global preference. + bool local_coords = spatial_editor->are_local_coords_enabled() && + !(_edit.plane == TRANSFORM_VIEW && _edit.mode != TRANSFORM_ROTATE); + + bool is_global_view_plane = (_edit.plane == TRANSFORM_VIEW) && + ((_edit.mode != TRANSFORM_ROTATE) || !spatial_editor->are_local_coords_enabled()); + + const List &selection = editor_selection->get_top_selected_node_list(); + for (Node *E : selection) { + Node3D *sp = Object::cast_to(E); + if (!sp) { + continue; + } + + Node3DEditorSelectedItem *se = editor_selection->get_node_editor_data(sp); + if (!se) { + continue; + } + + if (sp->has_meta("_edit_lock_") && !spatial_editor->is_gizmo_visible()) { + continue; + } + + if (se->gizmo.is_valid()) { + for (KeyValue &GE : se->subgizmos) { + Transform3D xform = GE.value; + Transform3D new_xform = _compute_transform(_edit.mode, se->original * xform, xform, p_motion, p_snap, local_coords, _edit.plane != TRANSFORM_VIEW, is_global_view_plane); // Force orthogonal with subgizmo. + if (!local_coords) { + new_xform = se->original.affine_inverse() * new_xform; + } + se->gizmo->set_subgizmo_transform(GE.key, new_xform); + } + } else { + Transform3D new_xform = _compute_transform(_edit.mode, se->original, se->original_local, p_motion, p_snap, local_coords, sp->get_rotation_edit_mode() != Node3D::ROTATION_EDIT_MODE_BASIS && _edit.plane != TRANSFORM_VIEW, is_global_view_plane); + _transform_gizmo_apply(se->sp, new_xform, local_coords); + } + } + + spatial_editor->update_transform_gizmo(); + surface->queue_redraw(); +} + +// Update the current transform operation in response to an input. +void Node3DEditorViewport::update_transform(bool p_shift) { + Vector3 ray_pos = get_ray_pos(_edit.mouse_pos); + Vector3 ray = get_ray(_edit.mouse_pos); + double snap = EDITOR_GET("interface/inspector/default_float_step"); + int snap_step_decimals = Math::range_step_decimals(snap); + + // View-plane translate/scale always uses global coords; rotation and axis operations respect local/global preference. + bool local_coords = spatial_editor->are_local_coords_enabled() && + !(_edit.plane == TRANSFORM_VIEW && _edit.mode != TRANSFORM_ROTATE); + + switch (_edit.mode) { + case TRANSFORM_SCALE: { + Vector3 motion_mask; + Plane plane; + bool plane_mv = false; + + switch (_edit.plane) { + case TRANSFORM_VIEW: + motion_mask = Vector3(0, 0, 0); + plane = Plane(_get_camera_normal(), _edit.center); + break; + case TRANSFORM_X_AXIS: + motion_mask = spatial_editor->get_gizmo_transform().basis.get_column(0).normalized(); + plane = Plane(motion_mask.cross(motion_mask.cross(_get_camera_normal())).normalized(), _edit.center); + break; + case TRANSFORM_Y_AXIS: + motion_mask = spatial_editor->get_gizmo_transform().basis.get_column(1).normalized(); + plane = Plane(motion_mask.cross(motion_mask.cross(_get_camera_normal())).normalized(), _edit.center); + break; + case TRANSFORM_Z_AXIS: + motion_mask = spatial_editor->get_gizmo_transform().basis.get_column(2).normalized(); + plane = Plane(motion_mask.cross(motion_mask.cross(_get_camera_normal())).normalized(), _edit.center); + break; + case TRANSFORM_YZ: + motion_mask = spatial_editor->get_gizmo_transform().basis.get_column(2).normalized() + spatial_editor->get_gizmo_transform().basis.get_column(1).normalized(); + plane = Plane(spatial_editor->get_gizmo_transform().basis.get_column(0).normalized(), _edit.center); + plane_mv = true; + break; + case TRANSFORM_XZ: + motion_mask = spatial_editor->get_gizmo_transform().basis.get_column(2).normalized() + spatial_editor->get_gizmo_transform().basis.get_column(0).normalized(); + plane = Plane(spatial_editor->get_gizmo_transform().basis.get_column(1).normalized(), _edit.center); + plane_mv = true; + break; + case TRANSFORM_XY: + motion_mask = spatial_editor->get_gizmo_transform().basis.get_column(0).normalized() + spatial_editor->get_gizmo_transform().basis.get_column(1).normalized(); + plane = Plane(spatial_editor->get_gizmo_transform().basis.get_column(2).normalized(), _edit.center); + plane_mv = true; + break; + } + + Vector3 intersection; + if (!plane.intersects_ray(ray_pos, ray, &intersection)) { + break; + } + + Vector3 click; + if (!plane.intersects_ray(_edit.click_ray_pos, _edit.click_ray, &click)) { + break; + } + + Vector3 motion = intersection - click; + if (_edit.plane != TRANSFORM_VIEW) { + if (!plane_mv) { + motion = motion_mask.dot(motion) * motion_mask; + + } else { + // Alternative planar scaling mode + if (p_shift) { + motion = motion_mask.dot(motion) * motion_mask; + } + } + + } else { + const real_t center_click_dist = click.distance_to(_edit.center); + const real_t center_inters_dist = intersection.distance_to(_edit.center); + if (center_click_dist == 0) { + break; + } + + const real_t scale = center_inters_dist - center_click_dist; + motion = Vector3(scale, scale, scale); + } + + motion /= click.distance_to(_edit.center); + + if (spatial_editor->is_snap_enabled()) { + snap = spatial_editor->get_scale_snap() / 100; + } + Vector3 motion_snapped = motion; + motion_snapped.snapf(snap); + // This might not be necessary anymore after issue #288 is solved (in 4.0?). + // TRANSLATORS: Refers to changing the scale of a node in the 3D editor. + set_message(vformat(TTR("Scaling: %s"), vformat("%.*v", snap_step_decimals, motion_snapped))); + if (local_coords) { + // TODO: needed? + motion = _edit.original.basis.inverse().xform(motion); + } + + apply_transform(motion, snap); + } break; + + case TRANSFORM_TRANSLATE: { + Vector3 motion_mask; + Plane plane; + bool plane_mv = false; + + switch (_edit.plane) { + case TRANSFORM_VIEW: + plane = Plane(_get_camera_normal(), _edit.center); + break; + case TRANSFORM_X_AXIS: + motion_mask = spatial_editor->get_gizmo_transform().basis.get_column(0).normalized(); + plane = Plane(motion_mask.cross(motion_mask.cross(_get_camera_normal())).normalized(), _edit.center); + break; + case TRANSFORM_Y_AXIS: + motion_mask = spatial_editor->get_gizmo_transform().basis.get_column(1).normalized(); + plane = Plane(motion_mask.cross(motion_mask.cross(_get_camera_normal())).normalized(), _edit.center); + break; + case TRANSFORM_Z_AXIS: + motion_mask = spatial_editor->get_gizmo_transform().basis.get_column(2).normalized(); + plane = Plane(motion_mask.cross(motion_mask.cross(_get_camera_normal())).normalized(), _edit.center); + break; + case TRANSFORM_YZ: + plane = Plane(spatial_editor->get_gizmo_transform().basis.get_column(0).normalized(), _edit.center); + plane_mv = true; + break; + case TRANSFORM_XZ: + plane = Plane(spatial_editor->get_gizmo_transform().basis.get_column(1).normalized(), _edit.center); + plane_mv = true; + break; + case TRANSFORM_XY: + plane = Plane(spatial_editor->get_gizmo_transform().basis.get_column(2).normalized(), _edit.center); + plane_mv = true; + break; + } + + Vector3 intersection; + if (!plane.intersects_ray(ray_pos, ray, &intersection)) { + break; + } + + Vector3 click; + if (!plane.intersects_ray(_edit.click_ray_pos, _edit.click_ray, &click)) { + break; + } + + Vector3 motion = intersection - click; + if (_edit.plane != TRANSFORM_VIEW) { + if (!plane_mv) { + motion = motion_mask.dot(motion) * motion_mask; + } + } + + if (spatial_editor->is_snap_enabled()) { + snap = spatial_editor->get_translate_snap(); + } + Vector3 motion_snapped = motion; + motion_snapped.snapf(snap); + // TRANSLATORS: Refers to changing the position of a node in the 3D editor. + set_message(vformat(TTR("Translating: %s"), vformat("%.*v", snap_step_decimals, motion_snapped))); + if (local_coords) { + motion = spatial_editor->get_gizmo_transform().basis.inverse().xform(motion); + } + + apply_transform(motion, snap); + } break; + + case TRANSFORM_ROTATE: { + Plane plane; + if (camera->get_projection() == Camera3D::PROJECTION_PERSPECTIVE) { + Vector3 cam_to_obj = _edit.center - _get_camera_position(); + if (!cam_to_obj.is_zero_approx()) { + plane = Plane(cam_to_obj.normalized(), _edit.center); + } else { + plane = Plane(_get_camera_normal(), _edit.center); + } + } else { + plane = Plane(_get_camera_normal(), _edit.center); + } + + if (_edit.is_trackball) { + Vector2 motion_delta = _edit.mouse_pos - _edit.original_mouse_pos; + real_t sensitivity = TRACKBALL_SENSITIVITY * EDSCALE; + Vector2 rotation_input = motion_delta * sensitivity; + + Transform3D cam_transform = view_3d_controller->to_camera_transform(); + Vector3 cam_right = cam_transform.basis.get_column(0).normalized(); + Vector3 cam_up = cam_transform.basis.get_column(1).normalized(); + Vector3 rotation_axis = cam_up * rotation_input.x + cam_right * rotation_input.y; + + real_t rotation_angle = rotation_axis.length(); + if (rotation_angle > 0.0f) { + rotation_axis /= rotation_angle; + + if (spatial_editor->is_snap_enabled()) { + double snap_step = spatial_editor->get_rotate_snap(); + double angle_deg = Math::rad_to_deg(rotation_angle); + angle_deg = Math::snapped(angle_deg, snap_step); + rotation_angle = Math::deg_to_rad(angle_deg); + } + + double angle_deg = Math::rad_to_deg(rotation_angle); + set_message(vformat(TTR("Rotating %s degrees."), String::num(angle_deg, 2))); + + apply_transform(rotation_axis, rotation_angle); + } + break; + } + + Vector3 local_axis; + Vector3 global_axis; + switch (_edit.plane) { + case TRANSFORM_VIEW: + local_axis = _edit.view_axis_local; + global_axis = _get_camera_normal(); + break; + case TRANSFORM_X_AXIS: + local_axis = Vector3(1, 0, 0); + break; + case TRANSFORM_Y_AXIS: + local_axis = Vector3(0, 1, 0); + break; + case TRANSFORM_Z_AXIS: + local_axis = Vector3(0, 0, 1); + break; + case TRANSFORM_YZ: + case TRANSFORM_XZ: + case TRANSFORM_XY: + break; + } + + if (_edit.plane != TRANSFORM_VIEW) { + global_axis = spatial_editor->get_gizmo_transform().basis.xform(local_axis).normalized(); + } + + Vector3 intersection; + if (!plane.intersects_ray(ray_pos, ray, &intersection)) { + break; + } + + Vector3 click; + if (!plane.intersects_ray(_edit.click_ray_pos, _edit.click_ray, &click)) { + break; + } + + Vector3 current_rotation_vector = (intersection - _edit.center).normalized(); + + if (_edit.initial_click_vector == Vector3()) { + Plane rotation_plane(global_axis, _edit.center); + Vector3 click_on_rotation_plane; + if (rotation_plane.intersects_ray(_edit.click_ray_pos, _edit.click_ray, &click_on_rotation_plane)) { + _edit.initial_click_vector = (click_on_rotation_plane - _edit.center).normalized(); + } else { + _edit.initial_click_vector = (click - _edit.center).normalized(); + } + _edit.previous_rotation_vector = current_rotation_vector; + _edit.accumulated_rotation_angle = 0.0; + _edit.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; + + 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; + + if (spatial_editor->is_snap_enabled()) { + snap = spatial_editor->get_rotate_snap(); + snap_step_decimals = Math::range_step_decimals(snap); + } + + if (axis_is_orthogonal) { + _edit.show_rotation_line = false; + Vector3 projection_axis = plane.normal.cross(global_axis); + Vector3 delta = intersection - click; + float projection = delta.dot(projection_axis); + double orth_angle = (projection * (Math::PI / 2.0f)) / (gizmo_scale * GIZMO_CIRCLE_SIZE); + _edit.rotation_angle = spatial_editor->is_snap_enabled() + ? Math::deg_to_rad(Math::snapped(Math::rad_to_deg(orth_angle), snap)) + : orth_angle; + } else { + _edit.show_rotation_line = true; + _edit.rotation_angle = spatial_editor->is_snap_enabled() + ? Math::deg_to_rad(Math::snapped(Math::rad_to_deg(_edit.accumulated_rotation_angle), snap)) + : _edit.accumulated_rotation_angle; + } + set_message(vformat(TTR("Rotating %s degrees."), String::num(Math::rad_to_deg(_edit.rotation_angle), snap_step_decimals))); + + Vector3 compute_axis = local_coords ? local_axis : global_axis; + apply_transform(compute_axis, _edit.rotation_angle); + } break; + default: { + } + } +} + +void Node3DEditorViewport::update_transform_numeric() { + Vector3 motion; + switch (_edit.plane) { + case TRANSFORM_VIEW: { + switch (_edit.mode) { + case TRANSFORM_TRANSLATE: + motion = Vector3(1, 0, 0); + break; + case TRANSFORM_ROTATE: + motion = _edit.view_axis_local; + break; + case TRANSFORM_SCALE: + motion = Vector3(1, 1, 1); + break; + case TRANSFORM_NONE: + ERR_FAIL_MSG("_edit.mode cannot be TRANSFORM_NONE in update_transform_numeric."); + } + break; + } + case TRANSFORM_X_AXIS: + motion = Vector3(1, 0, 0); + break; + case TRANSFORM_Y_AXIS: + motion = Vector3(0, 1, 0); + break; + case TRANSFORM_Z_AXIS: + motion = Vector3(0, 0, 1); + break; + case TRANSFORM_XY: + motion = Vector3(1, 1, 0); + break; + case TRANSFORM_XZ: + motion = Vector3(1, 0, 1); + break; + case TRANSFORM_YZ: + motion = Vector3(0, 1, 1); + break; + } + + double value = _edit.numeric_input * (_edit.numeric_negate ? -1 : 1); + double extra = 0.0; + switch (_edit.mode) { + case TRANSFORM_TRANSLATE: + motion *= value; + set_message(vformat(TTR("Translating %s."), motion)); + break; + case TRANSFORM_ROTATE: + extra = Math::deg_to_rad(value); + set_message(vformat(TTR("Rotating %f degrees."), value)); + break; + case TRANSFORM_SCALE: + // To halve the size of an object in Blender, you scale it by 0.5. + // Doing the same in Godot is considered scaling it by -0.5. + motion *= (value - 1.0); + set_message(vformat(TTR("Scaling %s."), motion)); + break; + case TRANSFORM_NONE: + ERR_FAIL_MSG("_edit.mode cannot be TRANSFORM_NONE in update_transform_numeric."); + } + + apply_transform(motion, extra); +} + +// Perform cleanup after a transform operation is committed or canceled. +void Node3DEditorViewport::finish_transform() { + _edit.mode = TRANSFORM_NONE; + _edit.instant = false; + _edit.numeric_input = 0; + _edit.numeric_next_decimal = 0; + _edit.numeric_negate = false; + _edit.is_trackball = false; + _edit.initial_click_vector = Vector3(); + _edit.previous_rotation_vector = Vector3(); + _edit.accumulated_rotation_angle = 0.0; + _edit.rotation_angle = 0.0; + _edit.gizmo_initiated = false; + _edit.children_original_globals.clear(); + spatial_editor->set_local_coords_enabled(_edit.original_local); + spatial_editor->update_transform_gizmo(); + surface->queue_redraw(); + set_process_input(false); + clicked = ObjectID(); +} + +// Register a shortcut and also add it as an input action with the same events. +void Node3DEditorViewport::register_shortcut_action(const String &p_path, const String &p_name, Key p_keycode, bool p_physical) { + Ref sc = ED_SHORTCUT(p_path, p_name, p_keycode, p_physical); + shortcut_changed_callback(sc, p_path); + // Connect to the change event on the shortcut so the input binding can be updated. + sc->connect_changed(callable_mp(this, &Node3DEditorViewport::shortcut_changed_callback).bind(sc, p_path)); +} + +// Update the action in the InputMap to the provided shortcut events. +void Node3DEditorViewport::shortcut_changed_callback(const Ref p_shortcut, const String &p_shortcut_path) { + InputMap *im = InputMap::get_singleton(); + if (im->has_action(p_shortcut_path)) { + im->action_erase_events(p_shortcut_path); + } else { + im->add_action(p_shortcut_path); + } + + for (int i = 0; i < p_shortcut->get_events().size(); i++) { + im->action_add_event(p_shortcut_path, p_shortcut->get_events()[i]); + } + + if (view_3d_controller.is_valid()) { + _update_view_3d_controller(); + } +} + +void Node3DEditorViewport::_set_lock_view_rotation(bool p_lock_rotation) { + view_3d_controller->set_lock_rotation(p_lock_rotation); + int idx = view_display_menu->get_popup()->get_item_index(VIEW_LOCK_ROTATION); + view_display_menu->get_popup()->set_item_checked(idx, p_lock_rotation); + if (p_lock_rotation) { + locked_label->show(); + } else { + locked_label->hide(); + } +} + +void Node3DEditorViewport::_add_advanced_debug_draw_mode_item(PopupMenu *p_popup, const String &p_name, int p_value, SupportedRenderingMethods p_rendering_methods, const String &p_tooltip) { + display_submenu->add_radio_check_item(p_name, p_value); + Array item_data = { p_rendering_methods, p_tooltip }; + display_submenu->set_item_metadata(-1, item_data); // Tooltip is assigned in NOTIFICATION_TRANSLATION_CHANGED. +} + +void Node3DEditorViewport::_load_viewport_inputs() { + // Registering with Key::NONE intentionally creates an empty Array. + register_shortcut_action("spatial_editor/viewport_orbit_modifier_1", TTRC("Viewport Orbit Modifier 1"), Key::NONE); + register_shortcut_action("spatial_editor/viewport_orbit_modifier_2", TTRC("Viewport Orbit Modifier 2"), Key::NONE); + register_shortcut_action("spatial_editor/viewport_orbit_snap_modifier_1", TTRC("Viewport Orbit Snap Modifier 1"), Key::ALT); + register_shortcut_action("spatial_editor/viewport_orbit_snap_modifier_2", TTRC("Viewport Orbit Snap Modifier 2"), Key::NONE); + register_shortcut_action("spatial_editor/viewport_pan_modifier_1", TTRC("Viewport Pan Modifier 1"), Key::SHIFT); + register_shortcut_action("spatial_editor/viewport_pan_modifier_2", TTRC("Viewport Pan Modifier 2"), Key::NONE); + register_shortcut_action("spatial_editor/viewport_zoom_modifier_1", TTRC("Viewport Zoom Modifier 1"), Key::CTRL); + register_shortcut_action("spatial_editor/viewport_zoom_modifier_2", TTRC("Viewport Zoom Modifier 2"), Key::NONE); + + register_shortcut_action("spatial_editor/freelook_left", TTRC("Freelook Left"), Key::A, true); + register_shortcut_action("spatial_editor/freelook_right", TTRC("Freelook Right"), Key::D, true); + register_shortcut_action("spatial_editor/freelook_forward", TTRC("Freelook Forward"), Key::W, true); + register_shortcut_action("spatial_editor/freelook_backwards", TTRC("Freelook Backwards"), Key::S, true); + register_shortcut_action("spatial_editor/freelook_up", TTRC("Freelook Up"), Key::E, true); + register_shortcut_action("spatial_editor/freelook_down", TTRC("Freelook Down"), Key::Q, true); + register_shortcut_action("spatial_editor/freelook_speed_modifier", TTRC("Freelook Speed Modifier"), Key::SHIFT); + register_shortcut_action("spatial_editor/freelook_slow_modifier", TTRC("Freelook Slow Modifier"), Key::ALT); +} + +Node3DEditorViewport::Node3DEditorViewport(Node3DEditor *p_spatial_editor, int p_index) { + cpu_time_history_index = 0; + gpu_time_history_index = 0; + + _edit.mode = TRANSFORM_NONE; + _edit.plane = TRANSFORM_VIEW; + _edit.show_rotation_line = true; + _edit.instant = false; + _edit.gizmo_handle = -1; + _edit.gizmo_handle_secondary = false; + + index = p_index; + editor_selection = EditorNode::get_singleton()->get_editor_selection(); + editor_selection->connect("selection_changed", callable_mp(this, &Node3DEditorViewport::_reset_follow_mode_count)); + + message_time = 0; + zoom_indicator_delay = 0.0; + + spatial_editor = p_spatial_editor; + SubViewportContainer *c = memnew(SubViewportContainer); + subviewport_container = c; + c->set_stretch(true); + add_child(c); + c->set_anchors_and_offsets_preset(Control::PRESET_FULL_RECT); + viewport = memnew(SubViewport); + viewport->set_disable_input(true); + + c->add_child(viewport); + surface = memnew(Control); + SET_DRAG_FORWARDING_CD(surface, Node3DEditorViewport); + add_child(surface); + surface->set_anchors_and_offsets_preset(Control::PRESET_FULL_RECT); + surface->set_clip_contents(true); + camera = memnew(Camera3D); + camera->set_disable_gizmos(true); + camera->set_cull_mask(((1 << 20) - 1) | (1 << (GIZMO_BASE_LAYER + p_index)) | (1 << GIZMO_EDIT_LAYER) | (1 << GIZMO_GRID_LAYER) | (1 << MISC_TOOL_LAYER)); + viewport->add_child(camera); + camera->make_current(); + surface->set_focus_mode(FOCUS_ALL); + + VBoxContainer *vbox = memnew(VBoxContainer); + surface->add_child(vbox); + vbox->set_offset(SIDE_LEFT, 10 * EDSCALE); + vbox->set_offset(SIDE_TOP, 10 * EDSCALE); + + HBoxContainer *hbox = memnew(HBoxContainer); + vbox->add_child(hbox); + + view_display_menu = memnew(MenuButton); + view_display_menu->set_flat(false); + view_display_menu->set_h_size_flags(0); + view_display_menu->set_shortcut_context(this); + view_display_menu->set_accessibility_name(TTRC("View")); + view_display_menu->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED); + view_display_menu->get_popup()->set_auto_translate_mode(AUTO_TRANSLATE_MODE_ALWAYS); + hbox->add_child(view_display_menu); + + view_display_menu->get_popup()->set_hide_on_checkable_item_selection(false); + + view_display_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("spatial_editor/top_view"), VIEW_TOP); + view_display_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("spatial_editor/bottom_view"), VIEW_BOTTOM); + view_display_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("spatial_editor/left_view"), VIEW_LEFT); + view_display_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("spatial_editor/right_view"), VIEW_RIGHT); + view_display_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("spatial_editor/front_view"), VIEW_FRONT); + view_display_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("spatial_editor/rear_view"), VIEW_REAR); + view_display_menu->get_popup()->add_separator(); + view_display_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("spatial_editor/switch_perspective_orthogonal"), VIEW_SWITCH_PERSPECTIVE_ORTHOGONAL); + view_display_menu->get_popup()->add_radio_check_item(TTRC("Perspective"), VIEW_PERSPECTIVE); + view_display_menu->get_popup()->add_radio_check_item(TTRC("Orthogonal"), VIEW_ORTHOGONAL); + view_display_menu->get_popup()->set_item_checked(view_display_menu->get_popup()->get_item_index(VIEW_PERSPECTIVE), true); + view_display_menu->get_popup()->add_check_item(TTRC("Auto Orthogonal Enabled"), VIEW_AUTO_ORTHOGONAL); + view_display_menu->get_popup()->set_item_checked(view_display_menu->get_popup()->get_item_index(VIEW_AUTO_ORTHOGONAL), true); + view_display_menu->get_popup()->add_separator(); + view_display_menu->get_popup()->add_check_shortcut(ED_SHORTCUT("spatial_editor/view_lock_rotation", TTRC("Lock View Rotation")), VIEW_LOCK_ROTATION); + view_display_menu->get_popup()->add_separator(); + // TRANSLATORS: "Normal" as in "normal life", not "normal vector". + view_display_menu->get_popup()->add_radio_check_shortcut(ED_SHORTCUT("spatial_editor/view_display_normal", TTRC("Display Normal")), VIEW_DISPLAY_NORMAL); + view_display_menu->get_popup()->add_radio_check_shortcut(ED_SHORTCUT("spatial_editor/view_display_wireframe", TTRC("Display Wireframe")), VIEW_DISPLAY_WIREFRAME); + view_display_menu->get_popup()->add_radio_check_shortcut(ED_SHORTCUT("spatial_editor/view_display_overdraw", TTRC("Display Overdraw")), VIEW_DISPLAY_OVERDRAW); + view_display_menu->get_popup()->add_radio_check_shortcut(ED_SHORTCUT("spatial_editor/view_display_lighting", TTRC("Display Lighting")), VIEW_DISPLAY_LIGHTING); + view_display_menu->get_popup()->add_radio_check_shortcut(ED_SHORTCUT("spatial_editor/view_display_unshaded", TTRC("Display Unshaded")), VIEW_DISPLAY_UNSHADED); + view_display_menu->get_popup()->set_item_checked(view_display_menu->get_popup()->get_item_index(VIEW_DISPLAY_NORMAL), true); + + display_submenu = memnew(PopupMenu); + display_submenu->set_hide_on_checkable_item_selection(false); + _add_advanced_debug_draw_mode_item(display_submenu, TTRC("Directional Shadow Splits"), VIEW_DISPLAY_DEBUG_PSSM_SPLITS, SupportedRenderingMethods::FORWARD_PLUS_MOBILE, + TTRC("Displays directional shadow splits in different colors to make adjusting split thresholds easier. \nRed: 1st split (closest to the camera), Green: 2nd split, Blue: 3rd split, Yellow: 4th split (furthest from the camera)")); + display_submenu->add_separator(); + // TRANSLATORS: "Normal" as in "normal vector", not "normal life". + _add_advanced_debug_draw_mode_item(display_submenu, TTRC("Normal Buffer"), VIEW_DISPLAY_NORMAL_BUFFER, SupportedRenderingMethods::FORWARD_PLUS); + display_submenu->add_separator(); + _add_advanced_debug_draw_mode_item(display_submenu, TTRC("Shadow Atlas"), VIEW_DISPLAY_DEBUG_SHADOW_ATLAS, SupportedRenderingMethods::ALL, + TTRC("Displays the shadow atlas used for positional (omni/spot) shadow mapping.\nRequires a visible OmniLight3D or SpotLight3D node with shadows enabled to have a visible effect.")); + _add_advanced_debug_draw_mode_item(display_submenu, TTRC("Directional Shadow Map"), VIEW_DISPLAY_DEBUG_DIRECTIONAL_SHADOW_ATLAS, SupportedRenderingMethods::ALL, + TTRC("Displays the shadow map used for directional shadow mapping.\nRequires a visible DirectionalLight3D node with shadows enabled to have a visible effect.")); + display_submenu->add_separator(); + _add_advanced_debug_draw_mode_item(display_submenu, TTRC("Decal Atlas"), VIEW_DISPLAY_DEBUG_DECAL_ATLAS, SupportedRenderingMethods::FORWARD_PLUS_MOBILE); + _add_advanced_debug_draw_mode_item(display_submenu, TTRC("AreaLight3D Atlas"), VIEW_DISPLAY_DEBUG_AREA_LIGHT_ATLAS, SupportedRenderingMethods::FORWARD_PLUS_MOBILE); + display_submenu->add_separator(); + _add_advanced_debug_draw_mode_item(display_submenu, TTRC("VoxelGI Lighting"), VIEW_DISPLAY_DEBUG_VOXEL_GI_LIGHTING, SupportedRenderingMethods::FORWARD_PLUS, + TTRC("Requires a visible VoxelGI node that has been baked to have a visible effect.")); + _add_advanced_debug_draw_mode_item(display_submenu, TTRC("VoxelGI Albedo"), VIEW_DISPLAY_DEBUG_VOXEL_GI_ALBEDO, SupportedRenderingMethods::FORWARD_PLUS, + TTRC("Requires a visible VoxelGI node that has been baked to have a visible effect.")); + _add_advanced_debug_draw_mode_item(display_submenu, TTRC("VoxelGI Emission"), VIEW_DISPLAY_DEBUG_VOXEL_GI_EMISSION, SupportedRenderingMethods::FORWARD_PLUS, + TTRC("Requires a visible VoxelGI node that has been baked to have a visible effect.")); + display_submenu->add_separator(); + _add_advanced_debug_draw_mode_item(display_submenu, TTRC("SDFGI Cascades"), VIEW_DISPLAY_DEBUG_SDFGI, SupportedRenderingMethods::FORWARD_PLUS, + TTRC("Requires SDFGI to be enabled in Environment to have a visible effect.")); + _add_advanced_debug_draw_mode_item(display_submenu, TTRC("SDFGI Probes"), VIEW_DISPLAY_DEBUG_SDFGI_PROBES, SupportedRenderingMethods::FORWARD_PLUS, + TTRC("Left-click a SDFGI probe to display its occlusion information (white = not occluded, red = fully occluded).\nRequires SDFGI to be enabled in Environment to have a visible effect.")); + display_submenu->add_separator(); + _add_advanced_debug_draw_mode_item(display_submenu, TTRC("Scene Luminance"), VIEW_DISPLAY_DEBUG_SCENE_LUMINANCE, SupportedRenderingMethods::FORWARD_PLUS_MOBILE, + TTRC("Displays the scene luminance computed from the 3D buffer. This is used for Auto Exposure calculation.\nRequires Auto Exposure to be enabled in CameraAttributes to have a visible effect.")); + display_submenu->add_separator(); + _add_advanced_debug_draw_mode_item(display_submenu, TTRC("SSAO"), VIEW_DISPLAY_DEBUG_SSAO, SupportedRenderingMethods::FORWARD_PLUS, + TTRC("Displays the screen-space ambient occlusion buffer. Requires SSAO to be enabled in Environment to have a visible effect.")); + _add_advanced_debug_draw_mode_item(display_submenu, TTRC("SSIL"), VIEW_DISPLAY_DEBUG_SSIL, SupportedRenderingMethods::FORWARD_PLUS, + TTRC("Displays the screen-space indirect lighting buffer. Requires SSIL to be enabled in Environment to have a visible effect.")); + display_submenu->add_separator(); + _add_advanced_debug_draw_mode_item(display_submenu, TTRC("VoxelGI/SDFGI Buffer"), VIEW_DISPLAY_DEBUG_GI_BUFFER, SupportedRenderingMethods::FORWARD_PLUS, + TTRC("Requires SDFGI or VoxelGI to be enabled to have a visible effect.")); + display_submenu->add_separator(); + _add_advanced_debug_draw_mode_item(display_submenu, TTRC("Disable Mesh LOD"), VIEW_DISPLAY_DEBUG_DISABLE_LOD, SupportedRenderingMethods::ALL, + TTRC("Renders all meshes with their highest level of detail regardless of their distance from the camera.")); + display_submenu->add_separator(); + _add_advanced_debug_draw_mode_item(display_submenu, TTRC("OmniLight3D Cluster"), VIEW_DISPLAY_DEBUG_CLUSTER_OMNI_LIGHTS, SupportedRenderingMethods::FORWARD_PLUS, + TTRC("Highlights tiles of pixels that are affected by at least one OmniLight3D.")); + _add_advanced_debug_draw_mode_item(display_submenu, TTRC("SpotLight3D Cluster"), VIEW_DISPLAY_DEBUG_CLUSTER_SPOT_LIGHTS, SupportedRenderingMethods::FORWARD_PLUS, + TTRC("Highlights tiles of pixels that are affected by at least one SpotLight3D.")); + _add_advanced_debug_draw_mode_item(display_submenu, TTRC("AreaLight3D Cluster"), VIEW_DISPLAY_DEBUG_CLUSTER_AREA_LIGHTS, SupportedRenderingMethods::FORWARD_PLUS, + TTRC("Highlights tiles of pixels that are affected by at least one AreaLight3D.")); + _add_advanced_debug_draw_mode_item(display_submenu, TTRC("Decal Cluster"), VIEW_DISPLAY_DEBUG_CLUSTER_DECALS, SupportedRenderingMethods::FORWARD_PLUS, + TTRC("Highlights tiles of pixels that are affected by at least one Decal.")); + _add_advanced_debug_draw_mode_item(display_submenu, TTRC("ReflectionProbe Cluster"), VIEW_DISPLAY_DEBUG_CLUSTER_REFLECTION_PROBES, SupportedRenderingMethods::FORWARD_PLUS, + TTRC("Highlights tiles of pixels that are affected by at least one ReflectionProbe.")); + _add_advanced_debug_draw_mode_item(display_submenu, TTRC("Occlusion Culling Buffer"), VIEW_DISPLAY_DEBUG_OCCLUDERS, SupportedRenderingMethods::FORWARD_PLUS_MOBILE, + TTRC("Represents occluders with black pixels. Requires occlusion culling to be enabled to have a visible effect.")); + _add_advanced_debug_draw_mode_item(display_submenu, TTRC("Motion Vectors"), VIEW_DISPLAY_MOTION_VECTORS, SupportedRenderingMethods::FORWARD_PLUS, + TTRC("Represents motion vectors with colored lines in the direction of motion. Gray dots represent areas with no per-pixel motion.")); + _add_advanced_debug_draw_mode_item(display_submenu, TTRC("Internal Buffer"), VIEW_DISPLAY_INTERNAL_BUFFER, SupportedRenderingMethods::FORWARD_PLUS_MOBILE, + TTRC("Shows the scene rendered in linear colorspace before any tonemapping or post-processing.")); + view_display_menu->get_popup()->add_submenu_node_item(TTRC("Display Advanced..."), display_submenu, VIEW_DISPLAY_ADVANCED); + + view_display_menu->get_popup()->add_separator(); + view_display_menu->get_popup()->add_check_shortcut(ED_SHORTCUT("spatial_editor/view_environment", TTRC("View Environment")), VIEW_ENVIRONMENT); + view_display_menu->get_popup()->add_check_shortcut(ED_SHORTCUT("spatial_editor/view_gizmos", TTRC("View Gizmos")), VIEW_GIZMOS); + view_display_menu->get_popup()->add_check_shortcut(ED_SHORTCUT("spatial_editor/view_transform_gizmo", TTRC("View Transform Gizmo")), VIEW_TRANSFORM_GIZMO); + view_display_menu->get_popup()->add_check_shortcut(ED_SHORTCUT("spatial_editor/view_grid_lines", TTRC("View Grid")), VIEW_GRID); + view_display_menu->get_popup()->add_check_shortcut(ED_SHORTCUT("spatial_editor/view_information", TTRC("View Information")), VIEW_INFORMATION); + view_display_menu->get_popup()->add_check_shortcut(ED_SHORTCUT("spatial_editor/view_fps", TTRC("View Frame Time")), VIEW_FRAME_TIME); + view_display_menu->get_popup()->set_item_checked(view_display_menu->get_popup()->get_item_index(VIEW_ENVIRONMENT), true); + view_display_menu->get_popup()->add_separator(); + view_display_menu->get_popup()->add_check_shortcut(ED_SHORTCUT("spatial_editor/view_half_resolution", TTRC("Half Resolution")), VIEW_HALF_RESOLUTION); + view_display_menu->get_popup()->add_separator(); + view_display_menu->get_popup()->add_check_shortcut(ED_SHORTCUT("spatial_editor/view_audio_listener", TTRC("Audio Listener")), VIEW_AUDIO_LISTENER); + view_display_menu->get_popup()->add_check_shortcut(ED_SHORTCUT("spatial_editor/view_audio_doppler", TTRC("Enable Doppler")), VIEW_AUDIO_DOPPLER); + view_display_menu->get_popup()->set_item_checked(view_display_menu->get_popup()->get_item_index(VIEW_GIZMOS), true); + view_display_menu->get_popup()->set_item_checked(view_display_menu->get_popup()->get_item_index(VIEW_TRANSFORM_GIZMO), true); + view_display_menu->get_popup()->set_item_checked(view_display_menu->get_popup()->get_item_index(VIEW_GRID), true); + + view_display_menu->get_popup()->add_separator(); + view_display_menu->get_popup()->add_check_shortcut(ED_SHORTCUT("spatial_editor/view_cinematic_preview", TTRC("Cinematic Preview")), VIEW_CINEMATIC_PREVIEW); + + view_display_menu->get_popup()->add_separator(); + view_display_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("spatial_editor/focus_origin"), VIEW_CENTER_TO_ORIGIN); + view_display_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("spatial_editor/focus_selection"), VIEW_CENTER_TO_SELECTION); + view_display_menu->get_popup()->set_item_tooltip(-1, TTR("Press Focus Selection twice to start following the selection as it moves. Press it yet another time to stop following the selection.")); + view_display_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("spatial_editor/align_transform_with_view"), VIEW_ALIGN_TRANSFORM_WITH_VIEW); + view_display_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("spatial_editor/align_rotation_with_view"), VIEW_ALIGN_ROTATION_WITH_VIEW); + view_display_menu->get_popup()->connect(SceneStringName(id_pressed), callable_mp(this, &Node3DEditorViewport::_menu_option)); + display_submenu->connect(SceneStringName(id_pressed), callable_mp(this, &Node3DEditorViewport::_menu_option)); + view_display_menu->set_disable_shortcuts(true); + + _load_viewport_inputs(); + InputMap::get_singleton()->connect("project_settings_loaded", callable_mp(this, &Node3DEditorViewport::_load_viewport_inputs)); + + ED_SHORTCUT("spatial_editor/lock_transform_x", TTRC("Lock Transformation to X axis"), Key::X); + ED_SHORTCUT("spatial_editor/lock_transform_y", TTRC("Lock Transformation to Y axis"), Key::Y); + ED_SHORTCUT("spatial_editor/lock_transform_z", TTRC("Lock Transformation to Z axis"), Key::Z); + ED_SHORTCUT("spatial_editor/lock_transform_yz", TTRC("Lock Transformation to YZ plane"), KeyModifierMask::SHIFT | Key::X); + ED_SHORTCUT("spatial_editor/lock_transform_xz", TTRC("Lock Transformation to XZ plane"), KeyModifierMask::SHIFT | Key::Y); + ED_SHORTCUT("spatial_editor/lock_transform_xy", TTRC("Lock Transformation to XY plane"), KeyModifierMask::SHIFT | Key::Z); + ED_SHORTCUT("spatial_editor/cancel_transform", TTRC("Cancel Transformation"), Key::ESCAPE); + ED_SHORTCUT("spatial_editor/instant_translate", TTRC("Begin Translate Transformation")); + ED_SHORTCUT("spatial_editor/instant_rotate", TTRC("Begin Rotate Transformation")); + ED_SHORTCUT("spatial_editor/instant_scale", TTRC("Begin Scale Transformation")); + ED_SHORTCUT("spatial_editor/collision_reposition", TTRC("Reposition Using Collisions"), KeyModifierMask::SHIFT | Key::G); + ED_SHORTCUT("spatial_editor/reset_transform_position", TTRC("Reset Position"), KeyModifierMask::ALT + Key::W); + ED_SHORTCUT("spatial_editor/reset_transform_rotation", TTRC("Reset Rotation"), KeyModifierMask::ALT + Key::E); + ED_SHORTCUT("spatial_editor/reset_transform_scale", TTRC("Reset Scale"), KeyModifierMask::ALT + Key::R); + + translation_preview_button = memnew(EditorTranslationPreviewButton); + hbox->add_child(translation_preview_button); + + follow_mode = memnew(Button); + follow_mode->set_tooltip_text(TTR("Click to stop following this node as it moves.")); + follow_mode->hide(); + vbox->add_child(follow_mode); + follow_mode->connect(SceneStringName(pressed), callable_mp(this, &Node3DEditorViewport::_disable_follow_mode)); + + preview_camera = memnew(CheckBox); + preview_camera->set_text(TTRC("Preview")); + preview_camera->set_tooltip_text(TTRC("Preview through the selected camera.\nHold Shift while clicking to also enable Pilot mode.")); + // Using Control even on macOS to avoid conflict with Quick Open shortcut. + preview_camera->set_shortcut(ED_SHORTCUT("spatial_editor/toggle_camera_preview", TTRC("Toggle Camera Preview"), KeyModifierMask::CTRL | Key::P)); + vbox->add_child(preview_camera); + preview_camera->set_h_size_flags(0); + preview_camera->set_theme_type_variation("CheckBoxNoIconTint"); + preview_camera->hide(); + preview_camera->connect(SceneStringName(toggled), callable_mp(this, &Node3DEditorViewport::_toggle_camera_preview)); + + pilot_camera = memnew(CheckBox); + pilot_camera->set_text(TTRC("Pilot")); + pilot_camera->set_tooltip_text(TTRC("Enable pilot mode for the preview camera.\nAllows WASD movement and mouse look when in preview mode.")); + pilot_camera->set_shortcut(ED_SHORTCUT("spatial_editor/toggle_pilot_preview", TTRC("Toggle Pilot Mode in Preview"))); + vbox->add_child(pilot_camera); + pilot_camera->set_h_size_flags(0); + pilot_camera->set_theme_type_variation("CheckBoxNoIconTint"); + pilot_camera->hide(); + pilot_camera->connect(SceneStringName(toggled), callable_mp(this, &Node3DEditorViewport::_toggle_pilot_preview)); + previewing = nullptr; + gizmo_scale = 1.0; + + preview_node = nullptr; + + bottom_center_vbox = memnew(VBoxContainer); + bottom_center_vbox->set_anchors_preset(LayoutPreset::PRESET_CENTER); + bottom_center_vbox->set_anchor_and_offset(SIDE_TOP, ANCHOR_END, -20 * EDSCALE); + bottom_center_vbox->set_anchor_and_offset(SIDE_BOTTOM, ANCHOR_END, -10 * EDSCALE); + bottom_center_vbox->set_h_grow_direction(GROW_DIRECTION_BOTH); + bottom_center_vbox->set_v_grow_direction(GROW_DIRECTION_BEGIN); + surface->add_child(bottom_center_vbox); + + info_panel = memnew(PanelContainer); + info_panel->set_anchor_and_offset(SIDE_LEFT, ANCHOR_END, -90 * EDSCALE); + info_panel->set_anchor_and_offset(SIDE_TOP, ANCHOR_END, -90 * EDSCALE); + info_panel->set_anchor_and_offset(SIDE_RIGHT, ANCHOR_END, -10 * EDSCALE); + info_panel->set_anchor_and_offset(SIDE_BOTTOM, ANCHOR_END, -10 * EDSCALE); + info_panel->set_h_grow_direction(GROW_DIRECTION_BEGIN); + info_panel->set_v_grow_direction(GROW_DIRECTION_BEGIN); + info_panel->set_mouse_filter(MOUSE_FILTER_IGNORE); + surface->add_child(info_panel); + info_panel->hide(); + + info_label = memnew(Label); + info_label->set_focus_mode(FOCUS_ACCESSIBILITY); + info_panel->add_child(info_label); + + cinema_label = memnew(Label); + cinema_label->set_anchor_and_offset(SIDE_TOP, ANCHOR_BEGIN, 10 * EDSCALE); + cinema_label->set_h_grow_direction(GROW_DIRECTION_END); + cinema_label->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER); + cinema_label->set_vertical_alignment(VERTICAL_ALIGNMENT_CENTER); + surface->add_child(cinema_label); + cinema_label->set_text(TTRC("Cinematic Preview")); + cinema_label->hide(); + previewing_cinema = false; + + locked_label = memnew(Label); + locked_label->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER); + locked_label->set_vertical_alignment(VERTICAL_ALIGNMENT_CENTER); + locked_label->set_h_size_flags(SIZE_SHRINK_CENTER); + bottom_center_vbox->add_child(locked_label); + locked_label->set_text(TTRC("View Rotation Locked")); + locked_label->hide(); + + zoom_limit_label = memnew(Label); + zoom_limit_label->set_text(TTRC(U"To zoom further, change the camera's clipping planes (View → Settings...)")); + zoom_limit_label->set_name("ZoomLimitMessageLabel"); + zoom_limit_label->add_theme_color_override(SceneStringName(font_color), Color(1, 1, 1, 1)); + zoom_limit_label->hide(); + bottom_center_vbox->add_child(zoom_limit_label); + + tooltip_panel = memnew(RichTextLabel); + vbox->add_child(tooltip_panel); + tooltip_panel->hide(); + tooltip_panel->set_h_grow_direction(GROW_DIRECTION_BEGIN); + tooltip_panel->set_v_grow_direction(GROW_DIRECTION_BEGIN); + tooltip_panel->set_mouse_filter(MOUSE_FILTER_IGNORE); + tooltip_panel->set_focus_mode(FOCUS_ACCESSIBILITY); + tooltip_panel->set_use_bbcode(true); + tooltip_panel->set_fit_content(true); + tooltip_panel->set_scroll_active(false); + tooltip_panel->set_tab_size(1); + tooltip_panel->set_autowrap_mode(TextServer::AUTOWRAP_OFF); + tooltip_panel->set_anchors_and_offsets_preset(LayoutPreset::PRESET_TOP_LEFT); + tooltip_panel->add_theme_color_override(SceneStringName(font_color), Color(0.8f, 0.8f, 0.8f, 1)); + tooltip_panel->add_theme_constant_override("paragraph_separation", 5); + + frame_time_gradient = memnew(Gradient); + // The color is set when the theme changes. + frame_time_gradient->add_point(0.5, Color()); + + top_right_vbox = memnew(VBoxContainer); + top_right_vbox->add_theme_constant_override("separation", 10.0 * EDSCALE); + top_right_vbox->set_anchors_and_offsets_preset(PRESET_TOP_RIGHT, PRESET_MODE_MINSIZE, 10.0 * EDSCALE); + top_right_vbox->set_h_grow_direction(GROW_DIRECTION_BEGIN); + + const int navigation_control_size = 150; + + position_control = memnew(ViewportNavigationControl); + position_control->set_navigation_mode(View3DController::NAV_MODE_MOVE); + position_control->set_custom_minimum_size(Size2(navigation_control_size, navigation_control_size) * EDSCALE); + position_control->set_h_size_flags(SIZE_SHRINK_END); + position_control->set_anchor_and_offset(SIDE_LEFT, ANCHOR_BEGIN, 0); + position_control->set_anchor_and_offset(SIDE_TOP, ANCHOR_END, -navigation_control_size * EDSCALE); + position_control->set_anchor_and_offset(SIDE_RIGHT, ANCHOR_BEGIN, navigation_control_size * EDSCALE); + position_control->set_anchor_and_offset(SIDE_BOTTOM, ANCHOR_END, 0); + position_control->set_viewport(this); + surface->add_child(position_control); + + look_control = memnew(ViewportNavigationControl); + look_control->set_navigation_mode(View3DController::NAV_MODE_LOOK); + look_control->set_custom_minimum_size(Size2(navigation_control_size, navigation_control_size) * EDSCALE); + look_control->set_h_size_flags(SIZE_SHRINK_END); + look_control->set_anchor_and_offset(SIDE_LEFT, ANCHOR_END, -navigation_control_size * EDSCALE); + look_control->set_anchor_and_offset(SIDE_TOP, ANCHOR_END, -navigation_control_size * EDSCALE); + look_control->set_anchor_and_offset(SIDE_RIGHT, ANCHOR_END, 0); + look_control->set_anchor_and_offset(SIDE_BOTTOM, ANCHOR_END, 0); + look_control->set_viewport(this); + surface->add_child(look_control); + + rotation_control = memnew(ViewportRotationControl); + rotation_control->set_custom_minimum_size(Size2(80, 80) * EDSCALE); + rotation_control->set_h_size_flags(SIZE_SHRINK_END); + rotation_control->set_viewport(this); + rotation_control->set_focus_mode(FOCUS_CLICK); + top_right_vbox->add_child(rotation_control); + + frame_time_panel = memnew(PanelContainer); + frame_time_panel->set_mouse_filter(MOUSE_FILTER_IGNORE); + top_right_vbox->add_child(frame_time_panel); + frame_time_panel->hide(); + + frame_time_vbox = memnew(VBoxContainer); + frame_time_panel->add_child(frame_time_vbox); + + // Individual Labels are used to allow coloring each label with its own color. + cpu_time_label = memnew(Label); + frame_time_vbox->add_child(cpu_time_label); + + gpu_time_label = memnew(Label); + frame_time_vbox->add_child(gpu_time_label); + + fps_label = memnew(Label); + frame_time_vbox->add_child(fps_label); + + surface->add_child(top_right_vbox); + + accept = nullptr; + + selection_menu = memnew(PopupMenu); + add_child(selection_menu); + selection_menu->set_min_size(Size2(100, 0) * EDSCALE); + selection_menu->connect(SceneStringName(id_pressed), callable_mp(this, &Node3DEditorViewport::_selection_result_pressed)); + selection_menu->connect("popup_hide", callable_mp(this, &Node3DEditorViewport::_selection_menu_hide)); + + if (p_index == 0) { + view_display_menu->get_popup()->set_item_checked(view_display_menu->get_popup()->get_item_index(VIEW_AUDIO_LISTENER), true); + viewport->set_as_audio_listener_3d(true); + } + + ruler = memnew(Node); + + ruler_start_point = memnew(Node3D); + ruler_start_point->set_visible(false); + + ruler_end_point = memnew(Node3D); + ruler_end_point->set_visible(false); + + ruler_material.instantiate(); + ruler_material->set_albedo(Color(1.0, 0.9, 0.0, 1.0)); + ruler_material->set_flag(BaseMaterial3D::FLAG_DISABLE_FOG, true); + ruler_material->set_shading_mode(BaseMaterial3D::SHADING_MODE_UNSHADED); + ruler_material->set_depth_draw_mode(BaseMaterial3D::DEPTH_DRAW_DISABLED); + + ruler_material_xray.instantiate(); + ruler_material_xray->set_albedo(Color(1.0, 0.9, 0.0, 0.15)); + ruler_material_xray->set_flag(BaseMaterial3D::FLAG_DISABLE_FOG, true); + ruler_material_xray->set_shading_mode(BaseMaterial3D::SHADING_MODE_UNSHADED); + ruler_material_xray->set_flag(BaseMaterial3D::FLAG_DISABLE_DEPTH_TEST, true); + ruler_material_xray->set_transparency(BaseMaterial3D::TRANSPARENCY_ALPHA); + ruler_material_xray->set_render_priority(BaseMaterial3D::RENDER_PRIORITY_MAX); + + geometry.instantiate(); + + geometry_xray.instantiate(); + + ruler_line = memnew(MeshInstance3D); + ruler_line->set_mesh(geometry); + ruler_line->set_material_override(ruler_material); + + ruler_line_xray = memnew(MeshInstance3D); + ruler_line_xray->set_mesh(geometry_xray); + ruler_line_xray->set_material_override(ruler_material_xray); + + ruler_triangle_material.instantiate(); + ruler_triangle_material->set_albedo(Color(1.0, 1.0, 1.0, 1.0)); + ruler_triangle_material->set_flag(BaseMaterial3D::FLAG_DISABLE_FOG, true); + ruler_triangle_material->set_shading_mode(BaseMaterial3D::SHADING_MODE_UNSHADED); + ruler_triangle_material->set_depth_draw_mode(BaseMaterial3D::DEPTH_DRAW_DISABLED); + ruler_triangle_material->set_flag(BaseMaterial3D::FLAG_ALBEDO_FROM_VERTEX_COLOR, true); + + ruler_triangle_material_xray.instantiate(); + ruler_triangle_material_xray->set_albedo(Color(1.0, 1.0, 1.0, 0.15)); + ruler_triangle_material_xray->set_flag(BaseMaterial3D::FLAG_DISABLE_FOG, true); + ruler_triangle_material_xray->set_shading_mode(BaseMaterial3D::SHADING_MODE_UNSHADED); + ruler_triangle_material_xray->set_flag(BaseMaterial3D::FLAG_DISABLE_DEPTH_TEST, true); + ruler_triangle_material_xray->set_transparency(BaseMaterial3D::TRANSPARENCY_ALPHA); + ruler_triangle_material_xray->set_render_priority(BaseMaterial3D::RENDER_PRIORITY_MAX); + ruler_triangle_material_xray->set_flag(BaseMaterial3D::FLAG_ALBEDO_FROM_VERTEX_COLOR, true); + + ruler_triangle_lines = memnew(MeshInstance3D); + Ref triangle_mesh; + triangle_mesh.instantiate(); + ruler_triangle_lines->set_mesh(triangle_mesh); + ruler_triangle_lines->set_material_override(ruler_triangle_material); + + ruler_triangle_lines_xray = memnew(MeshInstance3D); + Ref triangle_mesh_xray; + triangle_mesh_xray.instantiate(); + ruler_triangle_lines_xray->set_mesh(triangle_mesh_xray); + ruler_triangle_lines_xray->set_material_override(ruler_triangle_material_xray); + + ruler_label = memnew(Label); + ruler_label->set_visible(false); + + ruler_label_x = memnew(Label); + ruler_label_x->set_visible(false); + + ruler_label_y = memnew(Label); + ruler_label_y->set_visible(false); + + ruler_label_z = memnew(Label); + ruler_label_z->set_visible(false); + + ruler->add_child(ruler_start_point); + ruler->add_child(ruler_end_point); + ruler->add_child(ruler_line); + ruler->add_child(ruler_line_xray); + ruler->add_child(ruler_triangle_lines); + ruler->add_child(ruler_triangle_lines_xray); + + viewport->add_child(ruler_label); + viewport->add_child(ruler_label_x); + viewport->add_child(ruler_label_y); + viewport->add_child(ruler_label_z); + + view_3d_controller.instantiate(); + view_3d_controller->connect("view_state_changed", callable_mp(this, &Node3DEditorViewport::_view_state_changed)); + view_3d_controller->connect("fov_scaled", callable_mp((CanvasItem *)surface, &CanvasItem::queue_redraw)); + view_3d_controller->connect("freelook_changed", callable_mp(this, &Node3DEditorViewport::_freelook_changed)); + view_3d_controller->connect("freelook_speed_scaled", callable_mp(this, &Node3DEditorViewport::_freelook_speed_scaled)); + view_3d_controller->connect("cursor_panned", callable_mp(this, &Node3DEditorViewport::_disable_follow_mode)); + view_3d_controller->connect("cursor_interpolated", callable_mp(this, &Node3DEditorViewport::_cursor_interpolated)); + view_3d_controller->connect("cursor_distance_scaled", callable_mp(this, &Node3DEditorViewport::_cursor_distance_scaled)); + _update_view_3d_controller(true); + + _update_name(); + + EditorNode::get_singleton()->register_hdr_viewport(viewport); +} + +Node3DEditorViewport::~Node3DEditorViewport() { + memdelete(ruler); +} + +////////////////////////////////////////////////////////////// + +void Node3DEditorViewportContainer::_update_split_drag_margin() { + if (view != VIEW_USE_4_VIEWPORTS && view != VIEW_USE_3_VIEWPORTS) { + return; + } + // Also for 3 viewports view since the first split container is used to remember the offset. + first_split->set_split_offset(second_split->get_split_offset()); + + if (view == VIEW_USE_4_VIEWPORTS) { + // Extend to cover the first split on top. + second_split->set_drag_area_margin_begin(second_split->get_size().y - get_size().y); + } +} + +void Node3DEditorViewportContainer::_notification(int p_what) { + switch (p_what) { + case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: { + if (!EditorSettings::get_singleton()->check_changed_settings_in_group("interface/touchscreen")) { + return; + } + [[fallthrough]]; + } + case NOTIFICATION_READY: { + bool touch_optimizations = EDITOR_GET("interface/touchscreen/enable_touch_optimizations"); + main_split->set_touch_dragger_enabled(touch_optimizations); + first_split->set_touch_dragger_enabled(touch_optimizations); + second_split->set_touch_dragger_enabled(touch_optimizations); + } break; + } +} + +void Node3DEditorViewportContainer::set_view(View p_view) { + view = p_view; + + Node3DEditorViewport *viewports[4]; + for (uint32_t i = 0; i < 4; i++) { + viewports[i] = Node3DEditor::get_singleton()->get_editor_viewport(i); + ERR_FAIL_NULL(viewports[i]); + } + + const bool previous_main_vertical = !first_split->is_vertical(); + const float horizontal_offset = previous_main_vertical ? first_split->get_split_offset() : main_split->get_split_offset(); + const float vertical_offset = previous_main_vertical ? main_split->get_split_offset() : first_split->get_split_offset(); + + first_split->set_dragging_enabled(true); + second_split->set_drag_area_margin_begin(0); + viewports[0]->show(); + + switch (view) { + case VIEW_USE_1_VIEWPORT: { + for (int i = 1; i < 4; i++) { + viewports[i]->hide(); + } + second_split->hide(); + } break; + case VIEW_USE_2_VIEWPORTS: + case VIEW_USE_2_VIEWPORTS_ALT: { + viewports[1]->show(); + viewports[2]->hide(); + viewports[3]->hide(); + second_split->hide(); + const bool is_vertical = view == VIEW_USE_2_VIEWPORTS; + if (first_split->is_vertical() != is_vertical) { + first_split->set_vertical(is_vertical); + first_split->set_split_offset(is_vertical ? vertical_offset : horizontal_offset); + main_split->set_split_offset(is_vertical ? horizontal_offset : vertical_offset); // Store the other offset here for later. + } + } break; + case VIEW_USE_3_VIEWPORTS: + case VIEW_USE_3_VIEWPORTS_ALT: { + // Default mode has two on bottom (second_split). Alt mode has two on the left (first_split). + const bool main_vertical = view == VIEW_USE_3_VIEWPORTS; + viewports[1]->set_visible(!main_vertical); + viewports[2]->show(); + viewports[3]->set_visible(main_vertical); + second_split->show(); + main_split->set_vertical(main_vertical); + main_split->set_split_offset(main_vertical ? vertical_offset : horizontal_offset); + first_split->set_vertical(!main_vertical); + first_split->set_split_offset(main_vertical ? horizontal_offset : vertical_offset); + second_split->set_split_offset(main_vertical ? horizontal_offset : vertical_offset); + } break; + case VIEW_USE_4_VIEWPORTS: { + for (int i = 1; i < 4; i++) { + viewports[i]->show(); + } + second_split->show(); + main_split->set_vertical(true); + main_split->set_split_offset(vertical_offset); + first_split->set_vertical(false); + first_split->set_split_offset(horizontal_offset); + second_split->set_split_offset(horizontal_offset); + + first_split->set_dragging_enabled(false); + _update_split_drag_margin(); + } break; + } +} + +Node3DEditorViewportContainer::View Node3DEditorViewportContainer::get_view() { + return view; +} + +void Node3DEditorViewportContainer::add_viewport(Node3DEditorViewport *p_viewport, int p_index) { + if (p_index <= 1) { + first_split->add_child(p_viewport); + } else { + second_split->add_child(p_viewport); + if (p_index == 3) { + // Connect to the second split's child to update the drag margin immediately whenever the split offset changes. + p_viewport->connect(SceneStringName(resized), callable_mp(this, &Node3DEditorViewportContainer::_update_split_drag_margin)); + } + } +} + +Dictionary Node3DEditorViewportContainer::get_split_state() const { + Dictionary state; + state["main"] = Math::round(main_split->get_split_offset() / EDSCALE); + state["first"] = Math::round(first_split->get_split_offset() / EDSCALE); + state["second"] = Math::round(second_split->get_split_offset() / EDSCALE); + return state; +} + +void Node3DEditorViewportContainer::set_split_state(const Dictionary &p_state) { + if (p_state.has("main")) { + main_split->set_split_offset(int(p_state["main"]) * EDSCALE); + } + if (p_state.has("first")) { + first_split->set_split_offset(int(p_state["first"]) * EDSCALE); + } + if (p_state.has("second")) { + second_split->set_split_offset(int(p_state["second"]) * EDSCALE); + } +} + +Node3DEditorViewportContainer::Node3DEditorViewportContainer() { + set_clip_contents(true); + + main_split = memnew(SplitContainer); + main_split->set_drag_nested_intersections(true); + + first_split = memnew(SplitContainer); + first_split->set_h_size_flags(SIZE_EXPAND_FILL); + first_split->set_v_size_flags(SIZE_EXPAND_FILL); + first_split->set_drag_nested_intersections(true); + + second_split = memnew(SplitContainer); + second_split->set_h_size_flags(SIZE_EXPAND_FILL); + second_split->set_v_size_flags(SIZE_EXPAND_FILL); + second_split->set_drag_nested_intersections(true); + + main_split->add_child(first_split); + main_split->add_child(second_split); + add_child(main_split); +} diff --git a/editor/scene/3d/node_3d_editor_viewport.h b/editor/scene/3d/node_3d_editor_viewport.h new file mode 100644 index 0000000000..4d4bcb9068 --- /dev/null +++ b/editor/scene/3d/node_3d_editor_viewport.h @@ -0,0 +1,612 @@ +/**************************************************************************/ +/* node_3d_editor_viewport.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#pragma once + +#include "editor/plugins/editor_plugin.h" +#include "editor/scene/3d/node_3d_editor_gizmos.h" +#include "editor/themes/editor_scale.h" +#include "scene/debugger/view_3d_controller.h" +#include "scene/gui/margin_container.h" + +class AcceptDialog; +class CheckBox; +class EditorSelection; +class Gradient; +class ImmediateMesh; +class MenuButton; +class MeshInstance3D; +class Node3DEditor; +class Node3DEditorViewport; +class PanelContainer; +class RichTextLabel; +class SplitContainer; +class SubViewport; +class SubViewportContainer; +class VBoxContainer; + +class Node3DEditorSelectedItem : public Object { + GDCLASS(Node3DEditorSelectedItem, Object); + +public: + AABB aabb; + Transform3D original; // original location when moving + Transform3D original_local; + Transform3D last_xform; // last transform + bool last_xform_dirty; + Node3D *sp = nullptr; + RID sbox_instance; + RID sbox_instance_offset; + RID sbox_instance_xray; + RID sbox_instance_xray_offset; + Ref gizmo; + HashMap subgizmos; // Key: Subgizmo ID, Value: Initial subgizmo transform. + + Node3DEditorSelectedItem() { + sp = nullptr; + last_xform_dirty = true; + } + ~Node3DEditorSelectedItem(); +}; + +class ViewportNavigationControl : public Control { + GDCLASS(ViewportNavigationControl, Control); + + Node3DEditorViewport *viewport = nullptr; + Vector2i focused_mouse_start; + Vector2 focused_pos; + bool hovered = false; + int focused_index = -1; + View3DController::NavigationMode nav_mode = View3DController::NavigationMode::NAV_MODE_NONE; + + const float AXIS_CIRCLE_RADIUS = 30.0f * EDSCALE; + +protected: + void _notification(int p_what); + virtual void gui_input(const Ref &p_event) override; + void _draw(); + void _process_click(int p_index, Vector2 p_position, bool p_pressed); + void _process_drag(int p_index, Vector2 p_position, Vector2 p_relative_position); + void _update_navigation(); + +public: + void set_navigation_mode(View3DController::NavigationMode p_nav_mode); + void set_viewport(Node3DEditorViewport *p_viewport); +}; + +class ViewportRotationControl : public Control { + GDCLASS(ViewportRotationControl, Control); + + struct Axis2D { + Vector2 screen_point; + float z_axis = -99.0; + int axis = -1; + bool is_positive = true; + }; + + struct Axis2DCompare { + _FORCE_INLINE_ bool operator()(const Axis2D &l, const Axis2D &r) const { + return l.z_axis < r.z_axis; + } + }; + + Node3DEditorViewport *viewport = nullptr; + Vector axis_colors; + Vector axis_menu_options; + Vector2i orbiting_mouse_start; + Point2 original_mouse_pos; + View3DController::Cursor saved_cursor; + int orbiting_index = -1; + int focused_axis = -2; + bool gizmo_activated = false; + + const float AXIS_CIRCLE_RADIUS = 8.0f * EDSCALE; + +protected: + void _notification(int p_what); + virtual void gui_input(const Ref &p_event) override; + void _draw(); + void _draw_axis(const Axis2D &p_axis); + void _get_sorted_axis(Vector &r_axis); + void _update_focus(); + void _process_click(int p_index, Vector2 p_position, bool p_pressed); + void _process_drag(Ref p_event, int p_index, Vector2 p_position, Vector2 p_relative_position); + +public: + void set_viewport(Node3DEditorViewport *p_viewport); +}; + +class Node3DEditorViewport : public Control { + GDCLASS(Node3DEditorViewport, Control); + friend class Node3DEditor; + friend class ViewportNavigationControl; + friend class ViewportRotationControl; + enum { + VIEW_TOP, + VIEW_BOTTOM, + VIEW_LEFT, + VIEW_RIGHT, + VIEW_FRONT, + VIEW_REAR, + VIEW_CENTER_TO_ORIGIN, + VIEW_CENTER_TO_SELECTION, + VIEW_ALIGN_TRANSFORM_WITH_VIEW, + VIEW_ALIGN_ROTATION_WITH_VIEW, + VIEW_PERSPECTIVE, + VIEW_ENVIRONMENT, + VIEW_ORTHOGONAL, + VIEW_SWITCH_PERSPECTIVE_ORTHOGONAL, + VIEW_HALF_RESOLUTION, + VIEW_AUDIO_LISTENER, + VIEW_AUDIO_DOPPLER, + VIEW_GIZMOS, + VIEW_TRANSFORM_GIZMO, + VIEW_GRID, + VIEW_INFORMATION, + VIEW_FRAME_TIME, + + // < Keep in sync with menu. + VIEW_DISPLAY_NORMAL, + VIEW_DISPLAY_WIREFRAME, + VIEW_DISPLAY_OVERDRAW, + VIEW_DISPLAY_LIGHTING, + VIEW_DISPLAY_UNSHADED, + VIEW_DISPLAY_ADVANCED, + // Advanced menu: + VIEW_DISPLAY_DEBUG_PSSM_SPLITS, + VIEW_DISPLAY_NORMAL_BUFFER, + VIEW_DISPLAY_DEBUG_SHADOW_ATLAS, + VIEW_DISPLAY_DEBUG_DIRECTIONAL_SHADOW_ATLAS, + VIEW_DISPLAY_DEBUG_DECAL_ATLAS, + VIEW_DISPLAY_DEBUG_AREA_LIGHT_ATLAS, + VIEW_DISPLAY_DEBUG_VOXEL_GI_ALBEDO, + VIEW_DISPLAY_DEBUG_VOXEL_GI_LIGHTING, + VIEW_DISPLAY_DEBUG_VOXEL_GI_EMISSION, + VIEW_DISPLAY_DEBUG_SDFGI, + VIEW_DISPLAY_DEBUG_SDFGI_PROBES, + VIEW_DISPLAY_DEBUG_SCENE_LUMINANCE, + VIEW_DISPLAY_DEBUG_SSAO, + VIEW_DISPLAY_DEBUG_SSIL, + VIEW_DISPLAY_DEBUG_GI_BUFFER, + VIEW_DISPLAY_DEBUG_DISABLE_LOD, + VIEW_DISPLAY_DEBUG_CLUSTER_OMNI_LIGHTS, + VIEW_DISPLAY_DEBUG_CLUSTER_SPOT_LIGHTS, + VIEW_DISPLAY_DEBUG_CLUSTER_AREA_LIGHTS, + VIEW_DISPLAY_DEBUG_CLUSTER_DECALS, + VIEW_DISPLAY_DEBUG_CLUSTER_REFLECTION_PROBES, + VIEW_DISPLAY_DEBUG_OCCLUDERS, + VIEW_DISPLAY_MOTION_VECTORS, + VIEW_DISPLAY_INTERNAL_BUFFER, + VIEW_DISPLAY_MAX, + // > Keep in sync with menu. + + VIEW_LOCK_ROTATION, + VIEW_CINEMATIC_PREVIEW, + VIEW_AUTO_ORTHOGONAL, + VIEW_MAX + }; + +public: + static constexpr int32_t GIZMO_BASE_LAYER = 27; + static constexpr int32_t GIZMO_EDIT_LAYER = 26; + static constexpr int32_t GIZMO_GRID_LAYER = 25; + static constexpr int32_t MISC_TOOL_LAYER = 24; + + static constexpr int32_t FRAME_TIME_HISTORY = 20; + +private: + double cpu_time_history[FRAME_TIME_HISTORY]; + int cpu_time_history_index; + double gpu_time_history[FRAME_TIME_HISTORY]; + int gpu_time_history_index; + + Node *ruler = nullptr; + Node3D *ruler_start_point = nullptr; + Node3D *ruler_end_point = nullptr; + Ref geometry; + Ref geometry_xray; + MeshInstance3D *ruler_line = nullptr; + MeshInstance3D *ruler_line_xray = nullptr; + Label *ruler_label = nullptr; + Ref ruler_material; + Ref ruler_material_xray; + Ref ruler_triangle_material; + Ref ruler_triangle_material_xray; + MeshInstance3D *ruler_triangle_lines = nullptr; + MeshInstance3D *ruler_triangle_lines_xray = nullptr; + Label *ruler_label_x = nullptr; + Label *ruler_label_y = nullptr; + Label *ruler_label_z = nullptr; + + int index; + void _menu_option(int p_option); + Node3D *preview_node = nullptr; + bool update_preview_node = false; + Point2 preview_node_viewport_pos; + Vector3 preview_node_pos; + AABB *preview_bounds = nullptr; + Vector selected_files; + AcceptDialog *accept = nullptr; + + Node *target_node = nullptr; + Point2 drop_pos; + + ObjectID focused_node_id; + + EditorSelection *editor_selection = nullptr; + + Button *translation_preview_button = nullptr; + Button *follow_mode = nullptr; + CheckBox *preview_camera = nullptr; + CheckBox *pilot_camera = nullptr; + SubViewportContainer *subviewport_container = nullptr; + + MenuButton *view_display_menu = nullptr; + PopupMenu *display_submenu = nullptr; + + Control *surface = nullptr; + SubViewport *viewport = nullptr; + Camera3D *camera = nullptr; + bool transforming = false; + bool transform_gizmo_visible = true; + bool collision_reposition = false; + real_t gizmo_scale; + + bool vertex_snap_mode = false; + Key vertex_snap_keycode = Key::NONE; + bool vertex_snap_dragging = false; + Vector3 vertex_snap_source; + Plane vertex_snap_drag_plane; + Vector3 vertex_snap_target; + bool vertex_snap_has_target = false; + bool vertex_snap_has_source = false; + HashMap vertex_snap_original_positions; + + PanelContainer *info_panel = nullptr; + Label *info_label = nullptr; + Label *cinema_label = nullptr; + Label *locked_label = nullptr; + Label *zoom_limit_label = nullptr; + + RichTextLabel *tooltip_panel = nullptr; + + VBoxContainer *top_right_vbox = nullptr; + VBoxContainer *bottom_center_vbox = nullptr; + ViewportNavigationControl *position_control = nullptr; + ViewportNavigationControl *look_control = nullptr; + ViewportRotationControl *rotation_control = nullptr; + Ref frame_time_gradient; + PanelContainer *frame_time_panel = nullptr; + VBoxContainer *frame_time_vbox = nullptr; + Label *cpu_time_label = nullptr; + Label *gpu_time_label = nullptr; + Label *fps_label = nullptr; + + struct _RayResult { + Node3D *item = nullptr; + real_t depth = 0; + _FORCE_INLINE_ bool operator<(const _RayResult &p_rr) const { return depth < p_rr.depth; } + }; + + void _view_state_changed(); + + 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); + + float _min_screen_dist_to_aabb(const AABB &p_aabb, const Transform3D &p_transform, const Point2 &p_cursor) const; + bool _find_closest_vertex_on_node(const Point2 &p_screen_pos, Node3D *p_node, float &r_closest_screen_dist, Vector3 &r_vertex_world) const; + bool _find_closest_vertex_in_scene(const Point2 &p_screen_pos, float p_threshold, Vector3 &r_vertex_world, const HashMap *p_exclude = nullptr); + void _vertex_snap_update_source(const Point2 &p_screen_pos); + void _vertex_snap_commit(); + void _vertex_snap_cancel(); + bool _is_vertex_occluded(const Vector3 &p_world_pos, const Vector2 &p_screen_pos) const; + + Transform3D _get_camera_transform() const; + int get_selected_count() const; + bool _has_unlocked_selection() const; + void cancel_transform(); + void _update_shrink(); + + Vector3 _get_camera_position() const; + Vector3 _get_camera_normal() const; + Vector3 _get_screen_to_space(const Vector3 &p_vector3); + Vector _build_screen_frustum(const Point2 &p_min, const Point2 &p_max); + + void _select_region(); + bool _transform_gizmo_select(const Vector2 &p_screenpos, bool p_highlight_only = false); + void _transform_gizmo_apply(Node3D *p_node, const Transform3D &p_transform, bool p_local); + + bool _is_shortcut_empty(const String &p_name); + bool _is_nav_modifier_pressed(const String &p_name); + + float get_znear() const; + float get_zfar() const; + float get_fov() const; + + void _show_tooltip(const String &p_title, const String &p_description) const; + + ObjectID clicked; + ObjectID material_target; + Vector selection_results; + Vector selection_results_menu; + bool clicked_wants_append = false; + bool selection_in_progress = false; + bool movement_threshold_passed = false; + + PopupMenu *selection_menu = nullptr; + + enum TransformMode { + TRANSFORM_NONE, + TRANSFORM_ROTATE, + TRANSFORM_TRANSLATE, + TRANSFORM_SCALE + }; + enum TransformPlane { + TRANSFORM_VIEW, + TRANSFORM_X_AXIS, + TRANSFORM_Y_AXIS, + TRANSFORM_Z_AXIS, + TRANSFORM_YZ, + TRANSFORM_XZ, + TRANSFORM_XY, + }; + enum TransformType { + POSITION, + ROTATION, + SCALE, + }; + + struct EditData { + TransformMode mode; + TransformPlane plane; + Transform3D original; + Vector3 click_ray; + Vector3 click_ray_pos; + Vector3 center; + Point2 mouse_pos; + Point2 original_mouse_pos; + bool show_rotation_line = false; + bool is_trackball = false; + Ref gizmo; + int gizmo_handle = 0; + bool gizmo_handle_secondary = false; + Variant gizmo_initial_value; + bool original_local; + bool instant; + + // Numeric blender-style transforms (e.g. 'g5x'). + // numeric_input tracks the current input value, e.g. 1.23. + // numeric_negate indicates whether '-' has been pressed to negate the value + // while numeric_next_decimal is 0, numbers are input before the decimal point + // after pressing '.', numeric next decimal changes to -1, and decrements after each press. + double numeric_input = 0.0; + bool numeric_negate = false; + int numeric_next_decimal = 0; + + Vector3 rotation_axis; + Vector3 view_axis_local; + double accumulated_rotation_angle = 0.0; + double rotation_angle = 0.0; + Vector3 initial_click_vector; + Vector3 previous_rotation_vector; + bool gizmo_initiated = false; + + HashMap children_original_globals; + } _edit; + + Ref view_3d_controller; + void _update_view_3d_controller(bool p_update_all = true); + + void _cursor_interpolated(); + void _cursor_distance_scaled(); + + void _freelook_changed(); + void _freelook_speed_scaled(); + + real_t zoom_indicator_delay; + int zoom_failed_attempts_count = 0; + + RID move_gizmo_instance[3], move_plane_gizmo_instance[3], rotate_gizmo_instance[4], scale_gizmo_instance[3], scale_plane_gizmo_instance[3], axis_gizmo_instance[3]; + RID trackball_sphere_instance; + + String last_message; + String message; + double message_time; + + void set_message(const String &p_message, float p_time = 5); + + void _view_settings_confirmed(real_t p_interp_delta); + void _update_navigation_controls_visibility(); + void _draw(); + + // These allow tool scripts to set the 3D cursor location by updating the camera transform. + Transform3D last_camera_transform; + bool _camera_moved_externally(); + void _apply_camera_transform_to_cursor(); + + void _surface_mouse_enter(); + void _surface_mouse_exit(); + void _surface_focus_enter(); + void _surface_focus_exit(); + + void input(const Ref &p_event) override; + void _sinput(const Ref &p_event); + Node3DEditor *spatial_editor = nullptr; + + Camera3D *previewing = nullptr; + Camera3D *preview = nullptr; + + bool previewing_camera = false; + bool previewing_cinema = false; + int times_focused_consecutively = 0; + bool pilot_preview_enabled = false; + + bool pilot_undo_session_active = false; + real_t pilot_undo_idle_time = 0.0; + Transform3D pilot_undo_initial_transform; + void _pilot_ensure_undo_session(); + void _pilot_commit_undo_session(); + void _pilot_tick_undo_session(real_t p_delta); + + bool _is_node_locked(const Node *p_node) const; + void _preview_exited_scene(); + void _preview_camera_property_changed(); + void _sync_cursor_from_transform(const Transform3D &p_transform); + void _update_centered_labels(); + void _disable_follow_mode(); + void _reset_follow_mode_count(); + void _toggle_camera_preview(bool); + void _toggle_pilot_preview(bool); + void _toggle_cinema_preview(bool); + void _init_gizmo_instance(int p_idx); + void _finish_gizmo_instances(); + void _selection_result_pressed(int); + void _selection_menu_hide(); + void _list_select(Ref b); + + Vector3 _get_instance_position(const Point2 &p_pos, Node3D *p_node) const; + static AABB _calculate_spatial_bounds(const Node3D *p_parent, bool p_omit_top_level = false, const Transform3D *p_bounds_orientation = nullptr); + + Node *_sanitize_preview_node(Node *p_node) const; + + void _create_preview_node(const Vector &files) const; + void _remove_preview_node(); + bool _apply_preview_material(ObjectID p_target, const Point2 &p_point) const; + void _reset_preview_material() const; + void _remove_preview_material(); + bool _cyclical_dependency_exists(const String &p_target_scene_path, Node *p_desired_node) const; + bool _create_instance(Node *p_parent, const String &p_path, const Point2 &p_point); + bool _create_audio_node(Node *p_parent, const String &p_path, const Point2 &p_point); + void _perform_drop_data(); + + bool can_drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from); + void drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from); + + void _project_settings_changed(); + + Transform3D _compute_transform(TransformMode p_mode, const Transform3D &p_original, const Transform3D &p_original_local, Vector3 p_motion, double p_extra, bool p_local, bool p_orthogonal, bool p_view_axis); + + void _reset_transform(TransformType p_type); + + void begin_transform(TransformMode p_mode, bool instant); + void commit_transform(); + void apply_transform(Vector3 p_motion, double p_snap); + void update_transform(bool p_shift); + void update_transform_numeric(); + void finish_transform(); + + void _load_viewport_inputs(); + void register_shortcut_action(const String &p_path, const String &p_name, Key p_keycode, bool p_physical = false); + void shortcut_changed_callback(const Ref p_shortcut, const String &p_shortcut_path); + + // Supported rendering methods for advanced debug draw mode items. + enum SupportedRenderingMethods { + ALL, + FORWARD_PLUS, + FORWARD_PLUS_MOBILE, + }; + + void _set_lock_view_rotation(bool p_lock_rotation); + void _add_advanced_debug_draw_mode_item(PopupMenu *p_popup, const String &p_name, int p_value, SupportedRenderingMethods p_rendering_methods = SupportedRenderingMethods::ALL, const String &p_tooltip = ""); + +protected: + void _notification(int p_what); + static void _bind_methods(); + +public: + void update_surface() { surface->queue_redraw(); } + void update_transform_gizmo_view(); + void update_transform_gizmo_highlight(); + + void set_can_preview(Camera3D *p_preview); + void switch_preview_camera(Camera3D *p_new_camera); + void set_state(const Dictionary &p_state); + Dictionary get_state() const; + void reset(); + + Vector3 get_ray_pos(const Vector2 &p_pos) const; + Vector3 get_ray(const Vector2 &p_pos) const; + Point2 point_to_screen(const Vector3 &p_point); + + void focus_selection(); + + void assign_pending_data_pointers( + Node3D *p_preview_node, + AABB *p_preview_bounds, + AcceptDialog *p_accept); + + SubViewport *get_viewport_node() { return viewport; } + Camera3D *get_camera_3d() { return camera; } // return the default camera object. + Control *get_surface() { return surface; } + + Node3DEditorViewport(Node3DEditor *p_spatial_editor, int p_index); + ~Node3DEditorViewport(); +}; + +class Node3DEditorViewportContainer : public MarginContainer { + GDCLASS(Node3DEditorViewportContainer, MarginContainer); + +public: + enum View { + VIEW_USE_1_VIEWPORT, + VIEW_USE_2_VIEWPORTS, + VIEW_USE_2_VIEWPORTS_ALT, + VIEW_USE_3_VIEWPORTS, + VIEW_USE_3_VIEWPORTS_ALT, + VIEW_USE_4_VIEWPORTS, + }; + +private: + View view = VIEW_USE_1_VIEWPORT; + SplitContainer *main_split = nullptr; + SplitContainer *first_split = nullptr; + SplitContainer *second_split = nullptr; + + void _update_split_drag_margin(); + +protected: + void _notification(int p_what); + +public: + void set_view(View p_view); + View get_view(); + + void add_viewport(Node3DEditorViewport *p_viewport, int p_index); + + Dictionary get_split_state() const; + void set_split_state(const Dictionary &p_state); + + Node3DEditorViewportContainer(); +}; diff --git a/editor/scene/3d/path_3d_editor_plugin.cpp b/editor/scene/3d/path_3d_editor_plugin.cpp index b1f82f948b..f5f13f1f19 100644 --- a/editor/scene/3d/path_3d_editor_plugin.cpp +++ b/editor/scene/3d/path_3d_editor_plugin.cpp @@ -39,6 +39,7 @@ #include "editor/editor_string_names.h" #include "editor/editor_undo_redo_manager.h" #include "editor/scene/3d/node_3d_editor_plugin.h" +#include "editor/scene/3d/node_3d_editor_viewport.h" #include "editor/settings/editor_settings.h" #include "scene/debugger/view_3d_controller.h" #include "scene/gui/dialogs.h" diff --git a/editor/scene/3d/polygon_3d_editor_plugin.cpp b/editor/scene/3d/polygon_3d_editor_plugin.cpp index 5e719c5353..55087c5929 100644 --- a/editor/scene/3d/polygon_3d_editor_plugin.cpp +++ b/editor/scene/3d/polygon_3d_editor_plugin.cpp @@ -43,6 +43,7 @@ #include "editor/settings/editor_settings.h" #include "scene/3d/camera_3d.h" #include "scene/3d/mesh_instance_3d.h" +#include "scene/gui/button.h" #include "scene/main/scene_tree.h" #include "scene/resources/immediate_mesh.h" #include "scene/resources/material.h" diff --git a/editor/settings/editor_settings_dialog.cpp b/editor/settings/editor_settings_dialog.cpp index 8306e51758..24e69db554 100644 --- a/editor/settings/editor_settings_dialog.cpp +++ b/editor/settings/editor_settings_dialog.cpp @@ -56,6 +56,7 @@ #include "scene/gui/tab_container.h" #include "scene/gui/texture_rect.h" #include "scene/gui/tree.h" +#include "scene/main/timer.h" void EditorSettingsDialog::ok_pressed() { if (!EditorSettings::get_singleton()) { diff --git a/modules/gridmap/editor/grid_map_editor_plugin.cpp b/modules/gridmap/editor/grid_map_editor_plugin.cpp index c38ab156be..b5a337ef7a 100644 --- a/modules/gridmap/editor/grid_map_editor_plugin.cpp +++ b/modules/gridmap/editor/grid_map_editor_plugin.cpp @@ -45,6 +45,7 @@ #include "editor/inspector/editor_resource_preview.h" #include "editor/scene/3d/mesh_library_editor_plugin.h" #include "editor/scene/3d/node_3d_editor_plugin.h" +#include "editor/scene/3d/node_3d_editor_viewport.h" #include "editor/settings/editor_command_palette.h" #include "editor/settings/editor_settings.h" #include "editor/themes/editor_scale.h"