diff --git a/src/libslic3r/AppConfig.cpp b/src/libslic3r/AppConfig.cpp index b340bcee63..d8d1a57cf8 100644 --- a/src/libslic3r/AppConfig.cpp +++ b/src/libslic3r/AppConfig.cpp @@ -159,18 +159,6 @@ void AppConfig::set_defaults() #endif // _WIN32 } - if (get("use_perspective_camera").empty()) - set_bool("use_perspective_camera", true); - -#ifdef SUPPORT_FREE_CAMERA - if (get("use_free_camera").empty()) - set_bool("use_free_camera", false); -#endif - -#ifdef SUPPORT_REVERSE_MOUSE_ZOOM - if (get("reverse_mouse_wheel_zoom").empty()) - set_bool("reverse_mouse_wheel_zoom", false); -#endif if (get("enable_append_color_by_sync_ams").empty()) set_bool("enable_append_color_by_sync_ams", true); if (get("enable_merge_color_by_sync_ams").empty()) @@ -183,8 +171,6 @@ void AppConfig::set_defaults() if (get("export_sources_full_pathnames").empty()) set_bool("export_sources_full_pathnames", false); - if (get("zoom_to_mouse").empty()) - set_bool("zoom_to_mouse", false); if (get("show_shells_in_preview").empty()) set_bool("show_shells_in_preview", true); if (get("enable_text_styles").empty()) @@ -217,6 +203,48 @@ void AppConfig::set_defaults() set("3d_middle_tooltip_offset_y", "0.0"); if (get("cancel_glmultidraw").empty()) set_bool("cancel_glmultidraw", false); + + // 3D camera view options + + if (get("use_perspective_camera").empty()) + set_bool("use_perspective_camera", true); + if (get("use_free_camera").empty()) + set_bool("use_free_camera", false); + if (get("zoom_to_mouse").empty()) + set_bool("zoom_to_mouse", false); + if (get("reverse_mouse_wheel_zoom").empty()) + set_bool("reverse_mouse_wheel_zoom", false); + // Rotation speed adjustment ratio + if (get("view_rotate_speed_factor").empty()) + set("view_rotate_speed_factor", "0.8"); + // Which mouse button is used to rotate 3D view: left|mid|right|aux1|aux2 + if (get("view_rotate_mb").empty()) + set("view_rotate_mb", "left"); + // Which mouse button is used to pan 3D view: any|left|mid|right|aux1|aux2 + // Value is exclusive with "view_rotate_mb" (should not be set to same button). + // "any" legacy setting means either right and/or mid btns, whichever aren't used for rotation. + if (get("view_pan_mb").empty()) + set("view_pan_mb", "any"); + // Which camera rotation type is used when no modifiers are pressed: plate|center|selection|cursor|target + // \sa Camera::RotationFocusType + if (get("view_rotate_mode_nomod").empty()) + set("view_rotate_mode_nomod", "plate"); + // Which camera rotation type is used when CTRL (Win/Linux) or CMD (Mac) is pressed. + if (get("view_rotate_mode_ctrl").empty()) + set("view_rotate_mode_ctrl", "center"); + // Which camera rotation type is used when ALT/OPT is pressed. + if (get("view_rotate_mode_alt").empty()) + set("view_rotate_mode_alt", "selection"); + + // Remove legacy camera control options + erase("app", "keyboard_supported"); + erase("app", "mouse_supported"); + erase("app", "rotate_view"); + erase("app", "move_view"); + erase("app", "zoom_view"); + erase("app", "precise_control"); + erase("app", "mouse_wheel"); + //#ifdef SUPPORT_SHOW_HINTS if (get("show_hints").empty()) set_bool("show_hints", false); @@ -370,41 +398,14 @@ void AppConfig::set_defaults() set_bool("sync_user_preset", false); } - if (get("keyboard_supported").empty()) { - set("keyboard_supported", std::string("none/alt/control/shift")); - } - - if (get("mouse_supported").empty()) { - set("mouse_supported", "mouse left/mouse middle/mouse right"); - } - if (get("privacy_version").empty()) { set("privacy_version", "00.00.00.00"); } - if (get("rotate_view").empty()) { - set("rotate_view", "none/mouse left"); - } - - if (get("move_view").empty()) { - set("move_view", "none/mouse left"); - } - - if (get("zoom_view").empty()) { - set("zoom_view", "none/mouse left"); - } - - if (get("precise_control").empty()) { - set("precise_control", "none/mouse left"); - } - if (get("download_path").empty()) { set("download_path", ""); } - if (get("mouse_wheel").empty()) { - set("mouse_wheel", "0"); } - // helio options if (get("helio_enable").empty()) { set_bool("helio_enable", false); diff --git a/src/slic3r/GUI/Camera.cpp b/src/slic3r/GUI/Camera.cpp index fc0b399782..795a09be57 100644 --- a/src/slic3r/GUI/Camera.cpp +++ b/src/slic3r/GUI/Camera.cpp @@ -425,14 +425,12 @@ void Camera::debug_render() imgui.begin(std::string("Camera statistics"), ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse); std::string type = get_type_as_string(); - if (wxGetApp().plater()->get_mouse3d_controller().connected() -#ifdef SUPPORT_FREE_CAMERA - || (wxGetApp().app_config->get("use_free_camera") == "1") -#endif - ) + if (wxGetApp().app_config->get_bool("use_free_camera")) type += "/free"; else type += "/constrained"; + if (wxGetApp().plater()->get_mouse3d_controller().connected()) + type += " (3D mouse)"; Vec3f position = get_position().cast(); Vec3f target = m_target.cast(); @@ -478,7 +476,7 @@ void Camera::debug_render() ImGui::PopStyleVar(3); } -void Camera::rotate_on_sphere_with_target(double delta_azimut_rad, double delta_zenit_rad, bool apply_limits, Vec3d target) +void Camera::rotate_on_sphere_with_target(double delta_azimut_rad, double delta_zenit_rad, bool apply_limits, const Vec3d& target) { m_zenit += Geometry::rad2deg(delta_zenit_rad); if (apply_limits) { @@ -492,8 +490,8 @@ void Camera::rotate_on_sphere_with_target(double delta_azimut_rad, double delta_ } } - Vec3d translation = m_view_matrix.translation() + m_view_rotation * target; - auto rot_z = Eigen::AngleAxisd(delta_azimut_rad, Vec3d::UnitZ()); + const Vec3d translation = m_view_matrix.translation() + m_view_rotation * target; + const auto rot_z = Eigen::AngleAxisd(delta_azimut_rad, Vec3d::UnitZ()); m_view_rotation *= rot_z * Eigen::AngleAxisd(delta_zenit_rad, rot_z.inverse() * get_dir_right()); m_view_rotation.normalize(); m_view_matrix.fromPositionOrientationScale(m_view_rotation * (-target) + translation, m_view_rotation, Vec3d(1., 1., 1.)); @@ -502,37 +500,37 @@ void Camera::rotate_on_sphere_with_target(double delta_azimut_rad, double delta_ void Camera::rotate_on_sphere(double delta_azimut_rad, double delta_zenit_rad, bool apply_limits) { - m_zenit += Geometry::rad2deg(delta_zenit_rad); - if (apply_limits) { - if (m_zenit > 90.0f) { - delta_zenit_rad -= Geometry::deg2rad(m_zenit - 90.0f); - m_zenit = 90.0f; - } - else if (m_zenit < -90.0f) { - delta_zenit_rad -= Geometry::deg2rad(m_zenit + 90.0f); - m_zenit = -90.0f; - } - } - - const Vec3d translation = m_view_matrix.translation() + m_view_rotation * m_target; - const auto rot_z = Eigen::AngleAxisd(delta_azimut_rad, Vec3d::UnitZ()); - m_view_rotation *= rot_z * Eigen::AngleAxisd(delta_zenit_rad, rot_z.inverse() * get_dir_right()); - m_view_rotation.normalize(); - m_view_matrix.fromPositionOrientationScale(m_view_rotation * (- m_target) + translation, m_view_rotation, Vec3d(1., 1., 1.)); + rotate_on_sphere_with_target(delta_azimut_rad, delta_zenit_rad, apply_limits, m_target); } -//BBS rotate with target -void Camera::rotate_local_with_target(const Vec3d& rotation_rad, Vec3d target) +// Virtual trackball, rotate around an axis while looking at given target, where the eucledian norm of the axis gives the rotation angle in radians. +void Camera::rotate_local_with_target(const Vec3d& rotation_rad, const Vec3d& target) { double angle = rotation_rad.norm(); if (std::abs(angle) > EPSILON) { - Vec3d translation = m_view_matrix.translation() + m_view_rotation * target; - Vec3d axis = m_view_rotation.conjugate() * rotation_rad.normalized(); + const Vec3d translation = m_view_matrix.translation() + m_view_rotation * target; + const Vec3d axis = m_view_rotation.conjugate() * rotation_rad.normalized(); m_view_rotation *= Eigen::Quaterniond(Eigen::AngleAxisd(angle, axis)); m_view_rotation.normalize(); - m_view_matrix.fromPositionOrientationScale(m_view_rotation * (-target) + translation, m_view_rotation, Vec3d(1., 1., 1.)); - update_zenit(); - } + m_view_matrix.fromPositionOrientationScale(m_view_rotation * (-target) + translation, m_view_rotation, Vec3d(1., 1., 1.)); + update_zenit(); + } +} + +// Virtual trackball, rotate around current camera target. +// This overload is currently only used by 3D mouse input. +void Camera::rotate_local_around_target(const Vec3d& rotation_rad) +{ + rotate_local_with_target(rotation_rad, m_target); + + // If tilt exceeds constrained camera limits, automatically enable the free camera UI option. + // This will smooth out any future transition to a different control method, like mouse. + // The view and option can be reset by the user with a shortcut or menu selection. + if (m_update_config_on_free_rot_change && get_dir_up()(2) < -EPSILON) { + m_update_config_on_free_rot_change = false; + wxGetApp().app_config->set_bool("use_free_camera", true); + wxGetApp().plater()->update_camera_manipulation_settings(); + } } void Camera::calc_horizontal_rotate_rad(float &rotation_rad) { @@ -547,20 +545,6 @@ void Camera::calc_horizontal_rotate_rad(float &rotation_rad) { } } -// Virtual trackball, rotate around an axis, where the eucledian norm of the axis gives the rotation angle in radians. -void Camera::rotate_local_around_target(const Vec3d& rotation_rad) -{ - const double angle = rotation_rad.norm(); - if (std::abs(angle) > EPSILON) { - const Vec3d translation = m_view_matrix.translation() + m_view_rotation * m_target; - const Vec3d axis = m_view_rotation.conjugate() * rotation_rad.normalized(); - m_view_rotation *= Eigen::Quaterniond(Eigen::AngleAxisd(angle, axis)); - m_view_rotation.normalize(); - m_view_matrix.fromPositionOrientationScale(m_view_rotation * (-m_target) + translation, m_view_rotation, Vec3d(1., 1., 1.)); - update_zenit(); - } -} - void Camera::set_rotation(const Transform3d &rotation) { const Vec3d translation = m_view_matrix.translation() + m_view_rotation * m_target; diff --git a/src/slic3r/GUI/Camera.hpp b/src/slic3r/GUI/Camera.hpp index 7fbfc56088..df13f4540f 100644 --- a/src/slic3r/GUI/Camera.hpp +++ b/src/slic3r/GUI/Camera.hpp @@ -55,6 +55,7 @@ struct Camera private: EType m_type{ EType::Perspective }; bool m_update_config_on_type_change_enabled{ false }; + bool m_update_config_on_free_rot_change{ true }; Vec3d m_target{ Vec3d::Zero() }; float m_zenit{ 45.0f }; double m_zoom{ 1.0 }; @@ -144,8 +145,8 @@ struct Camera void translate_world(const Vec3d& displacement) { set_target(m_target + displacement); } // BBS rotate the camera on a sphere having center == target - void rotate_on_sphere_with_target(double delta_azimut_rad, double delta_zenit_rad, bool apply_limits, Vec3d target); - void rotate_local_with_target(const Vec3d& rotation_rad, Vec3d target); + void rotate_on_sphere_with_target(double delta_azimut_rad, double delta_zenit_rad, bool apply_limits, const Vec3d& target); + void rotate_local_with_target(const Vec3d& rotation_rad, const Vec3d& target); void calc_horizontal_rotate_rad(float &rotation_rad); // rotate the camera on a sphere having center == m_target and radius == m_distance // using the given variations of spherical coordinates @@ -160,8 +161,14 @@ struct Camera bool is_looking_front() const { return abs(get_dir_up().dot(Vec3d::UnitZ())-1) < 0.001; } // forces camera right vector to be parallel to XY plane void recover_from_free_camera() { - if (std::abs(get_dir_right()(2)) > EPSILON) + // First condition happens with unconstrained mouse rotations when going past the constrained stopping points. + // Second condition may happen after using a 3D mouse controller. + if (get_dir_up()(2) < -EPSILON || std::abs(get_dir_right()(2)) > EPSILON) { + update_target(); look_at(get_position(), m_target, Vec3d::UnitZ()); + } + // Set flag to monitor 3D mouse movements for toggling to free camera mode. \sa rotate_local_around_target() + m_update_config_on_free_rot_change = true; } void look_at(const Vec3d& position, const Vec3d& target, const Vec3d& up); diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 9196f3a62c..8e7bd29d7d 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -954,6 +954,7 @@ GLCanvas3D::Mouse::Drag::Drag() , move_volume_idx(-1) , move_requires_threshold(false) , move_start_threshold_position_2D(Invalid_2D_Point) + , last_modifiers(0) { } @@ -962,7 +963,6 @@ GLCanvas3D::Mouse::Mouse() , position(DBL_MAX, DBL_MAX) , scene_position(DBL_MAX, DBL_MAX, DBL_MAX) , ignore_left_up(false) - , ignore_right_up(false) { } @@ -1305,6 +1305,40 @@ void GLCanvas3D::SequentialPrintClearance::reset() m_perimeter.reset(); } + +// GLCanvas3D::CameraManipulationConf + +// static +GLCanvas3D::CameraRotationMode GLCanvas3D::CameraManipulationConf::camera_rot_mode_config_to_enum(const std::string& name) +{ + if (!name.compare("plate")) return GLCanvas3D::CameraRotationMode::SelectedPlate; + if (!name.compare("center")) return GLCanvas3D::CameraRotationMode::ViewCenter; + if (!name.compare("selection")) return GLCanvas3D::CameraRotationMode::SelectionOrCursor; + if (!name.compare("cursor")) return GLCanvas3D::CameraRotationMode::Cursor; + if (!name.compare("target")) return GLCanvas3D::CameraRotationMode::CameraTarget; + return GLCanvas3D::CameraRotationMode::Default; +} + +void GLCanvas3D::CameraManipulationConf::update_from_app_config() +{ + const auto *cfg = wxGetApp().app_config; + free_camera = cfg->get_bool("use_free_camera"); + zoom_to_mouse = cfg->get_bool("zoom_to_mouse"); + reverse_zoom = cfg->get_bool("reverse_mouse_wheel_zoom"); + rot_button = GUI::mouse_button_name_to_wx_enum(cfg->get("view_rotate_mb")); + pan_button = GUI::mouse_button_name_to_wx_enum(cfg->get("view_pan_mb")); + rot_mode_nomod = camera_rot_mode_config_to_enum(cfg->get("view_rotate_mode_nomod")); + rot_mode_ctrl = camera_rot_mode_config_to_enum(cfg->get("view_rotate_mode_ctrl")); + rot_mode_alt = camera_rot_mode_config_to_enum(cfg->get("view_rotate_mode_alt")); + rot_speed_factor = std::atof(cfg->get("view_rotate_speed_factor").c_str()); + if (rot_speed_factor < EPSILON) + rot_speed_factor = TRACKBALLSIZE; + rot_speed_factor = (PI * rot_speed_factor / 180.0f); +} + + +// GLCanvas3D + wxDEFINE_EVENT(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS, SimpleEvent); wxDEFINE_EVENT(EVT_GLCANVAS_OBJECT_SELECT, SimpleEvent); wxDEFINE_EVENT(EVT_GLCANVAS_PLATE_NAME_CHANGE, SimpleEvent); @@ -1356,6 +1390,8 @@ const double GLCanvas3D::DefaultCameraZoomToBoxMarginFactor = 1.25; const double GLCanvas3D::DefaultCameraZoomToBedMarginFactor = 2.00; const double GLCanvas3D::DefaultCameraZoomToPlateMarginFactor = 1.25; +GLCanvas3D::CameraManipulationConf GLCanvas3D::m_cam_manip_conf{ }; + void GLCanvas3D::load_arrange_settings() { std::string dist_fff_str = @@ -1544,6 +1580,12 @@ GLCanvas3D::GLCanvas3D(wxGLCanvas* canvas, Bed3D &bed) m_assembly_view_desc["number_key"] = _L("number keys can quickly change the color of objects"); m_render_pipeline_stage_stack.push(ERenderPipelineStage::Normal); + + // init static member only once + if (!m_cam_manip_conf.init) { + m_cam_manip_conf.init = true; + m_cam_manip_conf.update_from_app_config(); + } } GLCanvas3D::~GLCanvas3D() @@ -1555,9 +1597,7 @@ GLCanvas3D::~GLCanvas3D() s_full_screen_mesh.reset(); m_unit_cube.reset(); -#if ENABLE_SHOW_CAMERA_TARGET m_camera_target_mark.reset(); -#endif // ENABLE_SHOW_CAMERA_TARGET m_sequential_print_clearance.reset(); } @@ -2950,9 +2990,6 @@ void GLCanvas3D::render(bool only_init) } #endif // ENABLE_RENDER_PICKING_PASS -#if ENABLE_SHOW_CAMERA_TARGET - _render_camera_target(); -#endif // ENABLE_SHOW_CAMERA_TARGET if (m_picking_enabled && m_rectangle_selection.is_dragging()) m_rectangle_selection.render(*this); @@ -2986,6 +3023,16 @@ void GLCanvas3D::render(bool only_init) ImGui::PopStyleColor(2); ImGui::PopStyleVar(3); + + camera.debug_render(); + camera.debug_frustum(); + + if (m_rotation_center != Mouse::Drag::Invalid_3D_Point) + _render_camera_target(m_rotation_center); +#if ENABLE_SHOW_CAMERA_TARGET + else + _render_camera_target(camera.get_target()); +#endif } #if ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW @@ -2993,11 +3040,6 @@ void GLCanvas3D::render(bool only_init) wxGetApp().plater()->render_project_state_debug_window(); #endif // ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW - if (wxGetApp().plater()->is_render_statistic_dialog_visible()) { - camera.debug_render(); - camera.debug_frustum(); - } - #if ENABLE_IMGUI_STYLE_EDITOR if (wxGetApp().get_mode() == ConfigOptionMode::comDevelop) _render_style_editor(); @@ -4208,9 +4250,14 @@ void GLCanvas3D::bind_event_handlers() m_canvas->Bind(wxEVT_MIDDLE_UP, &GLCanvas3D::on_mouse, this); m_canvas->Bind(wxEVT_RIGHT_DOWN, &GLCanvas3D::on_mouse, this); m_canvas->Bind(wxEVT_RIGHT_UP, &GLCanvas3D::on_mouse, this); + // m_canvas->Bind(wxEVT_AUX1_DOWN, &GLCanvas3D::on_mouse, this); + m_canvas->Bind(wxEVT_AUX1_UP, &GLCanvas3D::on_mouse, this); + // m_canvas->Bind(wxEVT_AUX2_DOWN, &GLCanvas3D::on_mouse, this); + m_canvas->Bind(wxEVT_AUX2_UP, &GLCanvas3D::on_mouse, this); m_canvas->Bind(wxEVT_MOTION, &GLCanvas3D::on_mouse, this); m_canvas->Bind(wxEVT_ENTER_WINDOW, &GLCanvas3D::on_mouse, this); m_canvas->Bind(wxEVT_LEAVE_WINDOW, &GLCanvas3D::on_mouse, this); + m_canvas->Bind(wxEVT_MOUSE_CAPTURE_LOST, &GLCanvas3D::on_mouse_capture_lost, this); m_canvas->Bind(wxEVT_LEFT_DCLICK, &GLCanvas3D::on_mouse, this); m_canvas->Bind(wxEVT_MIDDLE_DCLICK, &GLCanvas3D::on_mouse, this); m_canvas->Bind(wxEVT_RIGHT_DCLICK, &GLCanvas3D::on_mouse, this); @@ -4246,9 +4293,14 @@ void GLCanvas3D::unbind_event_handlers() m_canvas->Unbind(wxEVT_MIDDLE_UP, &GLCanvas3D::on_mouse, this); m_canvas->Unbind(wxEVT_RIGHT_DOWN, &GLCanvas3D::on_mouse, this); m_canvas->Unbind(wxEVT_RIGHT_UP, &GLCanvas3D::on_mouse, this); + // m_canvas->Unbind(wxEVT_AUX1_DOWN, &GLCanvas3D::on_mouse, this); + m_canvas->Unbind(wxEVT_AUX1_UP, &GLCanvas3D::on_mouse, this); + // m_canvas->Unbind(wxEVT_AUX2_DOWN, &GLCanvas3D::on_mouse, this); + m_canvas->Unbind(wxEVT_AUX2_UP, &GLCanvas3D::on_mouse, this); m_canvas->Unbind(wxEVT_MOTION, &GLCanvas3D::on_mouse, this); m_canvas->Unbind(wxEVT_ENTER_WINDOW, &GLCanvas3D::on_mouse, this); m_canvas->Unbind(wxEVT_LEAVE_WINDOW, &GLCanvas3D::on_mouse, this); + m_canvas->Unbind(wxEVT_MOUSE_CAPTURE_LOST, &GLCanvas3D::on_mouse_capture_lost, this); m_canvas->Unbind(wxEVT_LEFT_DCLICK, &GLCanvas3D::on_mouse, this); m_canvas->Unbind(wxEVT_MIDDLE_DCLICK, &GLCanvas3D::on_mouse, this); m_canvas->Unbind(wxEVT_RIGHT_DCLICK, &GLCanvas3D::on_mouse, this); @@ -5056,7 +5108,9 @@ void GLCanvas3D::on_key(wxKeyEvent& evt) && keyCode != WXK_LEFT && keyCode != WXK_UP && keyCode != WXK_RIGHT - && keyCode != WXK_DOWN) { + && keyCode != WXK_DOWN + && keyCode != WXK_ALT // prevent possible focus loss on ALT UP + ) { evt.Skip(); // Needed to have EVT_CHAR generated as well } } @@ -5144,20 +5198,14 @@ void GLCanvas3D::on_mouse_wheel(wxMouseEvent& evt) return; } // Calculate the zoom delta and apply it to the current zoom factor -#ifdef SUPPORT_REVERSE_MOUSE_ZOOM - double direction_factor = (wxGetApp().app_config->get("reverse_mouse_wheel_zoom") == "1") ? -1.0 : 1.0; -#else - double direction_factor = 1.0; -#endif - auto delta = direction_factor * (double)evt.GetWheelRotation() / (double)evt.GetWheelDelta(); - bool zoom_to_mouse = wxGetApp().app_config->get("zoom_to_mouse") == "true"; - if (!zoom_to_mouse) {// zoom to center + const float direction_factor = m_cam_manip_conf.reverse_zoom ? -1.0f : 1.0f; + const double delta = direction_factor * evt.GetWheelRotation() / (float)evt.GetWheelDelta(); + if (!m_cam_manip_conf.zoom_to_mouse) { // zoom to center _update_camera_zoom(delta); } else { - auto cnv_size = get_canvas_size(); Camera& camera = get_active_camera(); - auto screen_center_3d_pos = _mouse_to_3d(camera, { cnv_size.get_width() * 0.5, cnv_size.get_height() * 0.5 }); + auto screen_center_3d_pos = _mouse_to_3d(camera, get_canvas_size().center()); auto mouse_3d_pos = _mouse_to_3d(camera, {evt.GetX(), evt.GetY()}); Vec3d displacement = mouse_3d_pos - screen_center_3d_pos; camera.translate(displacement); @@ -5438,7 +5486,6 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) if (p_main_toolbar->on_mouse(evt, *this)) { if (evt.LeftUp() || evt.MiddleUp() || evt.RightUp()) mouse_up_cleanup(); - m_mouse.set_start_position_3D_as_invalid(); return; } } @@ -5446,7 +5493,6 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) if (wxGetApp().plater()->get_collapse_toolbar().on_mouse(evt, *this)) { if (evt.LeftUp() || evt.MiddleUp() || evt.RightUp()) mouse_up_cleanup(); - m_mouse.set_start_position_3D_as_invalid(); return; } @@ -5455,7 +5501,6 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) if (wxGetApp().plater()->get_view_toolbar().on_mouse(evt, *this)) { if (evt.LeftUp() || evt.MiddleUp() || evt.RightUp()) mouse_up_cleanup(); - m_mouse.set_start_position_3D_as_invalid(); return; } #endif @@ -5496,7 +5541,6 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) if (evt.LeftUp() || evt.MiddleUp() || evt.RightUp()) mouse_up_cleanup(); - m_mouse.set_start_position_3D_as_invalid(); m_mouse.position = pos.cast(); if (evt.Dragging() && current_printer_technology() == ptFFF && (fff_print()->config().print_sequence == PrintSequence::ByObject)) { @@ -5532,7 +5576,7 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) int layer_editing_object_idx = is_layers_editing_enabled() ? selected_object_idx : -1; - if (m_mouse.drag.move_requires_threshold && m_mouse.is_move_start_threshold_position_2D_defined() && m_mouse.is_move_threshold_met(pos)) { + if (m_mouse.drag.move_requires_threshold && m_mouse.is_move_threshold_met(pos)) { m_mouse.drag.move_requires_threshold = false; m_mouse.set_move_start_threshold_position_2D_as_invalid(); } @@ -5564,12 +5608,14 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) _refresh_if_shown_on_screen(); m_tooltip_enabled = true; } - m_mouse.set_start_position_2D_as_invalid(); //#endif } else if (evt.Leaving()) { // to remove hover on objects when the mouse goes out of this canvas m_mouse.position = Vec2d(-1.0, -1.0); + // Allow pan and rotate mouse drag actions to continue outside of current window bounds. + if ((m_mouse.rotating || m_mouse.panning) && !m_canvas->HasCapture()) + m_canvas->CaptureMouse(); m_dirty = true; } else if (evt.LeftDClick()) { @@ -5593,7 +5639,7 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) else post_event(SimpleEvent(EVT_GLCANVAS_SWITCH_TO_GLOBAL)); } - else if (evt.LeftDown() || evt.RightDown() || evt.MiddleDown()) { + else if (!m_mouse.dragging && evt.ButtonDown()) { m_show_assembly_view_preview_menu = false; //BBS: add orient deactivate logic if (!m_gizmos.on_mouse(evt)) { @@ -5797,6 +5843,10 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) } } else if (evt.Dragging()) { + // Track initial drag starting position for rotate/pan hysterisis. + if (!m_mouse.dragging) + m_mouse.drag.move_start_threshold_position_2D = pos; + m_mouse.dragging = true; if (m_layers_editing.state != LayersEditing::Unknown && layer_editing_object_idx != -1) { @@ -5805,107 +5855,109 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) m_mouse.position = pos.cast(); } } - // do not process the dragging if the left mouse was set down in another canvas - else if (evt.LeftIsDown()) { - // if dragging over blank area with left button, rotate - if ((any_gizmo_active || m_hover_volume_idxs.empty()) && m_mouse.is_start_position_3D_defined()) { - const Vec3d rot = (Vec3d(pos.x(), pos.y(), 0.) - m_mouse.drag.start_position_3D) * (PI * TRACKBALLSIZE / 180.); - if (this->m_canvas_type == ECanvasType::CanvasAssembleView || m_gizmos.is_paint_gizmo()) { - //BBS rotate around target + else { + const int drag_mods = evt.GetModifiers() & ~wxMOD_SHIFT; // dont track shift state changes + // Rotate if dragging over blank area with configured rotation button. + if (GUI::wx_mouse_button_is_down(evt, m_cam_manip_conf.rot_button)) { + const Vec3d pos_3D((double)pos.x(), (double)pos.y(), 0.0); + // Do rotation if hysteresis tolerance has already been met on last drag event. + if (m_mouse.rotating) { + const float rot_factor = evt.ShiftDown() ? m_cam_manip_conf.rot_speed_factor * .5f : m_cam_manip_conf.rot_speed_factor; + const Vec3d rot = (pos_3D - m_mouse.drag.start_position_3D) * rot_factor; Camera& camera = get_active_camera(); - Vec3d rotate_target = Vec3d::Zero(); - if (!m_selection.is_empty()) - rotate_target = m_selection.get_bounding_box().center(); - else - rotate_target = volumes_bounding_box(is_volumes_limit_to_expand_plate()).center(); - //BBS do not limit rotate in assemble view - camera.rotate_local_with_target(Vec3d(rot.y(), rot.x(), 0.), rotate_target); - //camera.rotate_on_sphere_with_target(rot.x(), rot.y(), false, rotate_target); - } - else { -#ifdef SUPPORT_FEEE_CAMERA - if (wxGetApp().app_config->get("use_free_camera") == "1") - // Virtual track ball (similar to the 3DConnexion mouse). - get_active_camera().rotate_local_around_target(Vec3d(rot.y(), rot.x(), 0.)); - else { -#endif - // Forces camera right vector to be parallel to XY plane in case it has been misaligned using the 3D mouse free rotation. - // It is cheaper to call this function right away instead of testing wxGetApp().plater()->get_mouse3d_controller().connected(), - // which checks an atomics (flushes CPU caches). - // See GH issue #3816. - Camera& camera = get_active_camera(); - - bool rotate_limit = current_printer_technology() != ptSLA; - Vec3d rotate_target = m_selection.get_bounding_box().center(); - - camera.recover_from_free_camera(); - //BBS modify rotation - if (evt.ControlDown() || evt.CmdDown()) { - if ((m_rotation_center.x() == 0.f) && (m_rotation_center.y() == 0.f) && (m_rotation_center.z() == 0.f)) { - auto canvas_w = float(get_canvas_size().get_width()); - auto canvas_h = float(get_canvas_size().get_height()); - Point screen_center(canvas_w/2, canvas_h/2); - //camera.rotate_on_sphere_with_target(rot.x(), rot.y(), rotate_limit, wxGetApp().plater()->get_partplate_list().get_bounding_box().center()); - m_rotation_center = _mouse_to_3d(camera, screen_center); - m_rotation_center(2) = 0.f; - } - camera.rotate_on_sphere_with_target(rot.x(), rot.y(), rotate_limit, m_rotation_center); - } else { - //BBS rotate with current plate center - PartPlate* plate = wxGetApp().plater()->get_partplate_list().get_curr_plate(); - if (plate) - camera.rotate_on_sphere_with_target(rot.x(), rot.y(), rotate_limit, plate->get_bounding_box().center()); - else - camera.rotate_on_sphere(rot.x(), rot.y(), rotate_limit); + + // Update cached rotation reference point, if needed + if (drag_mods != m_mouse.drag.last_modifiers || m_rotation_center == Mouse::Drag::Invalid_3D_Point) { + // Set rotation target based on view mode or user preferences and any modifier keys. + CameraRotationMode rot_mode; + // Always rotate around selection in paint tool + if (m_gizmos.is_paint_gizmo()) + rot_mode = CameraRotationMode::SelectionOrCursor; + else if (drag_mods & wxMOD_CONTROL) + rot_mode = m_cam_manip_conf.rot_mode_ctrl; + else if (drag_mods & wxMOD_ALT) + rot_mode = m_cam_manip_conf.rot_mode_alt; + else + rot_mode = m_cam_manip_conf.rot_mode_nomod; + + // There's no "current plate" in assembly view, so switch to section/cursor style instead. + if (this->m_canvas_type == ECanvasType::CanvasAssembleView && rot_mode == CameraRotationMode::SelectedPlate) + rot_mode = CameraRotationMode::SelectionOrCursor; + + switch (rot_mode) { + case CameraRotationMode::ViewCenter: + // Rotate around center of canvas. + m_rotation_center = _mouse_to_3d(camera, get_canvas_size().center()); + m_rotation_center(2) = .0; + break; + case CameraRotationMode::SelectionOrCursor: + // Rotate around selection, if any. + if (!m_selection.is_empty()) { + m_rotation_center = m_selection.get_bounding_box().center(); + break; + } + [[fallthrough]]; + case CameraRotationMode::Cursor: + // Rotate around current cursor position. + m_rotation_center = _mouse_to_3d(camera, pos); + break; + case CameraRotationMode::SelectedPlate: + // Rotate around current plate center + if (PartPlate* plate = wxGetApp().plater()->get_partplate_list().get_curr_plate()) { + m_rotation_center = plate->get_bounding_box().center(); + break; + } + [[fallthrough]]; + case CameraRotationMode::CameraTarget: + default: + // Use current camera target + m_rotation_center = camera.get_target(); + break; } -#ifdef SUPPORT_FEEE_CAMERA - } -#endif - } + } // update m_rotation_center - m_dirty = true; - } - if (m_mouse.is_move_threshold_met(pos)) { - m_mouse.rotating = true; - } - m_mouse.drag.start_position_3D = Vec3d((double)pos(0), (double)pos(1), 0.0); - m_mouse.drag.move_start_threshold_position_2D = pos; - } - else if (evt.MiddleIsDown() || evt.RightIsDown()) { - // If dragging over blank area with right button, pan. - if (m_mouse.is_start_position_2D_defined()) { - // get point in model space at Z = 0 - float z = 0.0f; - Camera& camera = get_active_camera(); - const Vec3d& cur_pos = _mouse_to_3d(camera, pos, &z); - Vec3d orig = _mouse_to_3d(camera, m_mouse.drag.start_position_2D, &z); -#ifdef SUPPORT_FREE_CAMERA - if (this->m_canvas_type != ECanvasType::CanvasAssembleView) { - if (wxGetApp().app_config->get("use_free_camera") != "1") - // Forces camera right vector to be parallel to XY plane in case it has been misaligned using the 3D mouse free rotation. - // It is cheaper to call this function right away instead of testing wxGetApp().plater()->get_mouse3d_controller().connected(), - // which checks an atomics (flushes CPU caches). - // See GH issue #3816. - camera.recover_from_free_camera(); + //BBS do not limit rotate in paint tool views + const bool rot_constrained = !m_gizmos.is_paint_gizmo() && !m_cam_manip_conf.free_camera; + camera.rotate_on_sphere_with_target(rot.x(), rot.y(), rot_constrained, m_rotation_center); + m_dirty = true; } -#endif - - camera.set_target(camera.get_target() + orig - cur_pos); - m_dirty = true; - m_mouse.ignore_right_up = true; - } - if (m_mouse.is_move_threshold_met(pos)) { - m_mouse.panning = true; + // Start rotating on the next drag event once movement threshold is met. + else if (m_mouse.is_move_threshold_met(pos)) { + m_mouse.rotating = true; + } + // Track last position for movement delta calculation on next drag event. + m_mouse.drag.start_position_3D = pos_3D; + } + + // Pan if dragging over blank area with configured pan button(s). + // wxMouseButton::wxMOUSE_BTN_ANY is a special exception for legacy controls which accepts either middle or right buttons. + if ( ( m_cam_manip_conf.pan_button == wxMouseButton::wxMOUSE_BTN_ANY && (evt.MiddleIsDown() || evt.RightIsDown()) ) + || ( m_cam_manip_conf.pan_button != wxMouseButton::wxMOUSE_BTN_ANY && GUI::wx_mouse_button_is_down(evt, m_cam_manip_conf.pan_button) ) + ) { + // Do panning if hysteresis tolerance has already been met on last drag event. + if (m_mouse.panning) { + // get point in model space at Z = 0 + float z = 0.0f; + Camera& camera = get_active_camera(); + const Vec3d cur_pos = _mouse_to_3d(camera, pos, &z); + const Vec3d orig = _mouse_to_3d(camera, m_mouse.drag.start_position_2D, &z); + camera.set_target(camera.get_target() + orig - cur_pos); + m_dirty = true; + } + else if (m_mouse.is_move_threshold_met(pos)) { + // Start panning on the next drag event once movement threshold is met. + m_mouse.panning = true; + } + m_mouse.drag.start_position_2D = pos; } - m_mouse.drag.start_position_2D = pos; - m_mouse.drag.move_start_threshold_position_2D = pos; + + m_mouse.drag.last_modifiers = drag_mods; } } - else if (evt.LeftUp() || evt.MiddleUp() || evt.RightUp()) { + else if (evt.ButtonUp()) { if (evt.LeftUp()) { m_selection.stop_dragging(); - m_rotation_center(0) = m_rotation_center(1) = m_rotation_center(2) = 0.f; } const bool left_click_on_blank = evt.LeftUp() && !m_mouse.ignore_left_up && !m_mouse.dragging @@ -5964,7 +6016,7 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) } } } - else if (evt.RightUp() && !is_layers_editing_enabled()) { + else if (evt.RightUp() && !is_layers_editing_enabled() && !m_mouse.rotating && !m_mouse.panning) { m_mouse.position = pos.cast(); // forces a frame render to ensure that m_hover_volume_idxs is updated even when the user right clicks while // the context menu is already shown @@ -6000,28 +6052,37 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) render(); } - Vec2d logical_pos = pos.cast(); + if (m_gizmos.get_current_type() == GLGizmosManager::EType::Undefined) { + //BBS post right click event + Vec2d logical_pos = pos.cast(); #if ENABLE_RETINA_GL - const float factor = m_retina_helper->get_scale_factor(); - logical_pos = logical_pos.cwiseQuotient(Vec2d(factor, factor)); + const float factor = m_retina_helper->get_scale_factor(); + logical_pos = logical_pos.cwiseQuotient(Vec2d(factor, factor)); #endif // ENABLE_RETINA_GL - - if (!m_mouse.ignore_right_up && m_gizmos.get_current_type() == GLGizmosManager::EType::Undefined) { - //BBS post right click event - if (!m_hover_plate_idxs.empty()) { + if (!m_hover_plate_idxs.empty()) post_event(RBtnPlateEvent(EVT_GLCANVAS_PLATE_RIGHT_CLICK, { logical_pos, m_hover_plate_idxs.front() })); - } - else { - // do not post the event if the user is panning the scene - // or if right click was done over the wipe tower - bool post_right_click_event = m_hover_volume_idxs.empty() || !m_volumes.volumes[get_first_hover_volume_idx()]->is_wipe_tower; - if (post_right_click_event) - post_event(RBtnEvent(EVT_GLCANVAS_RIGHT_CLICK, { logical_pos, m_hover_volume_idxs.empty() })); - } + // do not post the event if right click was done over a volume or the wipe tower + else if (m_hover_volume_idxs.empty() || !m_volumes.volumes[get_first_hover_volume_idx()]->is_wipe_tower) + post_event(RBtnEvent(EVT_GLCANVAS_RIGHT_CLICK, { logical_pos, m_hover_volume_idxs.empty() })); } } - mouse_up_cleanup(); + // Only partially reset mouse state if still dragging (with other button(s) down) + if (m_mouse.dragging && GUI::wx_mouse_button_is_down(evt, wxMOUSE_BTN_ANY)) { + if (!GUI::wx_mouse_button_is_down(evt, m_cam_manip_conf.rot_button)) { + m_mouse.rotating = false; + if (m_cam_manip_conf.rot_button == wxMouseButton::wxMOUSE_BTN_LEFT) + m_mouse.ignore_left_up = false; + } + // Special exception for controls setup which accepts either middle or right buttons. + if (m_cam_manip_conf.pan_button == wxMouseButton::wxMOUSE_BTN_ANY) + m_mouse.panning = evt.MiddleIsDown() || evt.RightIsDown(); + else + m_mouse.panning = GUI::wx_mouse_button_is_down(evt, m_cam_manip_conf.pan_button); + } + else { + mouse_up_cleanup(); + } } else if (evt.Moving()) { m_mouse.position = pos.cast(); @@ -6836,14 +6897,13 @@ void GLCanvas3D::export_toolpaths_to_obj(const char* filename) const void GLCanvas3D::mouse_up_cleanup() { m_moving = false; + m_rotation_center = Mouse::Drag::Invalid_3D_Point; m_mouse.drag.move_volume_idx = -1; - m_mouse.set_start_position_3D_as_invalid(); - m_mouse.set_start_position_2D_as_invalid(); + m_mouse.drag.last_modifiers = 0; m_mouse.dragging = false; m_mouse.rotating = false; m_mouse.panning = false; m_mouse.ignore_left_up = false; - m_mouse.ignore_right_up = false; m_dirty = true; if (m_canvas->HasCapture()) @@ -10642,8 +10702,7 @@ void GLCanvas3D::_render_assemble_info() const ImGuiWrapper::pop_toolbar_style(); } -#if ENABLE_SHOW_CAMERA_TARGET -void GLCanvas3D::_render_camera_target() const +void GLCanvas3D::_render_camera_target(const Vec3d& target) const { const auto& p_flat_shader = wxGetApp().get_shader("flat"); if (!p_flat_shader) @@ -10673,10 +10732,8 @@ void GLCanvas3D::_render_camera_target() const glsafe(::glDisable(GL_DEPTH_TEST)); - const auto& p_ogl_manager = wxGetApp().get_opengl_manager(); - p_ogl_manager.set_line_width(2.0f); - - const Vec3d& target = get_active_camera().get_target(); + if (const auto* p_ogl_manager = wxGetApp().get_opengl_manager().get()) + p_ogl_manager->set_line_width(2.0f); const float scale = 2.0f * half_length; Transform3d model_matrix{ Transform3d::Identity() }; @@ -10710,7 +10767,6 @@ void GLCanvas3D::_render_camera_target() const wxGetApp().unbind_shader(); } -#endif // ENABLE_SHOW_CAMERA_TARGET void GLCanvas3D::_render_sla_slices() { diff --git a/src/slic3r/GUI/GLCanvas3D.hpp b/src/slic3r/GUI/GLCanvas3D.hpp index 8773dce2ac..82e89c611d 100644 --- a/src/slic3r/GUI/GLCanvas3D.hpp +++ b/src/slic3r/GUI/GLCanvas3D.hpp @@ -25,6 +25,7 @@ namespace Slic3r { namespace GUI { class AssemblyStepsUtils; } } #include +#include #include class wxSizeEvent; @@ -87,6 +88,8 @@ class Size int get_scale_factor() const; void set_scale_factor(int height); + + inline Point center() const { return { m_width / 2, m_height / 2 }; } }; @@ -350,6 +353,7 @@ class GLCanvas3D int move_volume_idx; bool move_requires_threshold; Point move_start_threshold_position_2D; + int last_modifiers; public: Drag(); @@ -360,22 +364,17 @@ class GLCanvas3D Vec3d scene_position; Drag drag; bool ignore_left_up; - bool ignore_right_up; bool rotating{ false }; bool panning{ false }; Mouse(); - void set_start_position_2D_as_invalid() { drag.start_position_2D = Drag::Invalid_2D_Point; } - void set_start_position_3D_as_invalid() { drag.start_position_3D = Drag::Invalid_3D_Point; } void set_move_start_threshold_position_2D_as_invalid() { drag.move_start_threshold_position_2D = Drag::Invalid_2D_Point; } - - bool is_start_position_2D_defined() const { return (drag.start_position_2D != Drag::Invalid_2D_Point); } - bool is_start_position_3D_defined() const { return (drag.start_position_3D != Drag::Invalid_3D_Point); } bool is_move_start_threshold_position_2D_defined() const { return (drag.move_start_threshold_position_2D != Drag::Invalid_2D_Point); } bool is_move_threshold_met(const Point& mouse_pos) const { - return (std::abs(mouse_pos(0) - drag.move_start_threshold_position_2D(0)) > Drag::MoveThresholdPx) - || (std::abs(mouse_pos(1) - drag.move_start_threshold_position_2D(1)) > Drag::MoveThresholdPx); + return is_move_start_threshold_position_2D_defined() && + ( (std::abs(mouse_pos(0) - drag.move_start_threshold_position_2D(0)) > Drag::MoveThresholdPx) + || (std::abs(mouse_pos(1) - drag.move_start_threshold_position_2D(1)) > Drag::MoveThresholdPx) ); } }; @@ -545,6 +544,39 @@ class GLCanvas3D ERenderPipelineStage m_stage; }; + + enum class CameraRotationMode : unsigned char + { + Default, // situation dependent + CameraTarget, // rotate around current camera target + SelectedPlate, // rotate around center of currently selected plate + ViewCenter, // around center point of current canvas + Cursor, // around cursor position + SelectionOrCursor, // around currently selected object(s) or cursor position if no selection + }; + + // Stored cached app config options pertaining to camera movement, to avoid expensive lookups during active user interactions. + struct CameraManipulationConf + { + bool init{ false }; + bool free_camera{ false }; + bool zoom_to_mouse{ false }; + bool reverse_zoom{ false }; + wxMouseButton rot_button{ wxMouseButton::wxMOUSE_BTN_LEFT }; + wxMouseButton pan_button{ wxMouseButton::wxMOUSE_BTN_ANY }; + CameraRotationMode rot_mode_nomod{ CameraRotationMode::SelectedPlate }; + CameraRotationMode rot_mode_ctrl{ CameraRotationMode::ViewCenter }; + CameraRotationMode rot_mode_alt{ CameraRotationMode::SelectionOrCursor }; + float rot_speed_factor{ 0.8f }; + + void update_from_app_config(); + static CameraRotationMode camera_rot_mode_config_to_enum(const std::string& name); + }; + + // Cached runtime camera manipulation control configuration based on user button/modifier preferences. + // static because there is only one global app config which applies to all instances of GLCanvas3D. + static CameraManipulationConf m_cam_manip_conf; + public: enum ECursorType : unsigned char { @@ -691,7 +723,7 @@ class GLCanvas3D std::vector m_hover_plate_idxs; //BBS if explosion_ratio is changed, need to update volume bounding box mutable float m_explosion_ratio = 1.0; - mutable Vec3d m_rotation_center{ 0.0, 0.0, 0.0}; + Vec3d m_rotation_center{ Mouse::Drag::Invalid_3D_Point }; // Following variable is obsolete and it should be safe to remove it. // I just don't want to do it now before a release (Lukas Matena 24.3.2019) @@ -740,9 +772,7 @@ class GLCanvas3D using FrameCallback = std::function; std::vector m_frame_callback_list; -#if ENABLE_SHOW_CAMERA_TARGET mutable GLModel m_camera_target_mark; -#endif // ENABLE_SHOW_CAMERA_TARGET public: OrientSettings& get_orient_settings() @@ -1139,6 +1169,7 @@ class GLCanvas3D void on_render_fallback_timer(wxTimerEvent& evt); void on_set_color_timer(wxTimerEvent& evt); void on_mouse(wxMouseEvent& evt); + void on_mouse_capture_lost(wxMouseCaptureLostEvent&) { mouse_up_cleanup(); } void on_gesture(wxGestureEvent& evt); void on_paint(wxPaintEvent& evt); void on_kill_focus(wxFocusEvent &evt); @@ -1173,6 +1204,7 @@ class GLCanvas3D void handle_layers_data_focus_event(const t_layer_height_range range, const EditorType type); void update_ui_from_settings(); + static void update_camera_manipulation_settings() { m_cam_manip_conf.update_from_app_config(); } int get_move_volume_id() const { return m_mouse.drag.move_volume_idx; } int get_first_hover_volume_idx() const { return m_hover_volume_idxs.empty() ? -1 : m_hover_volume_idxs.front(); } @@ -1387,9 +1419,7 @@ class GLCanvas3D float _show_assembly_tooltip_information(float caption_max, float x, float y) const; void _render_assemble_control(); void _render_assemble_info() const; -#if ENABLE_SHOW_CAMERA_TARGET - void _render_camera_target() const; -#endif // ENABLE_SHOW_CAMERA_TARGET + void _render_camera_target(const Vec3d& target) const; void _render_sla_slices(); void _render_selection_sidebar_hints() const; //BBS: GUI refactor: adjust main toolbar position diff --git a/src/slic3r/GUI/KBShortcutsDialog.cpp b/src/slic3r/GUI/KBShortcutsDialog.cpp index fae1dd0bcc..cdc9ae994c 100644 --- a/src/slic3r/GUI/KBShortcutsDialog.cpp +++ b/src/slic3r/GUI/KBShortcutsDialog.cpp @@ -251,6 +251,7 @@ void KBShortcutsDialog::fill_shortcuts() {ctrl + "6", L("Camera view - Right")}, {ctrl + "7", L("Camera view - Isometric")}, { "Z", L("Camera view - Fit to scene or selection")}, + {ctrl + "U", L("Camera movement - Toggle free/constrained rotation")}, {ctrl + "W", L("Reset Window Layout")}, {ctrl + "E", L("Show Labels by Layer")}, {L("Shift+E"), L("Show Labels by Object")}, diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 9da8cff1f9..2c5204c03a 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -769,6 +769,11 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_ return; } + if (evt.CmdDown() && evt.GetKeyCode() == 'U') { + view_set_free_camera(!wxGetApp().app_config->get_bool("use_free_camera")); + return; + } + if (!evt.HasAnyModifiers() && (evt.GetKeyCode() == 'Z' || evt.GetKeyCode() == 'z')) { if (!should_skip_fit_camera_shortcut(m_plater)) view_zoom_to_fit(); @@ -3253,19 +3258,26 @@ void MainFrame::init_menubar_as_editor() auto perspective_item = append_menu_radio_item(viewMenu, wxID_CAMERA_PERSPECTIVE + camera_id_base, _L("Use Perspective View"), _L("Use Perspective View"), [this](wxCommandEvent&) { wxGetApp().app_config->set_bool("use_perspective_camera", true); - wxGetApp().update_ui_from_settings(); + m_plater->update_camera_from_settings(); }, nullptr); //BBS orthogonal view auto orthogonal_item = append_menu_radio_item(viewMenu, wxID_CAMERA_ORTHOGONAL + camera_id_base, _L("Use Orthogonal View"), _L("Use Orthogonal View"), [this](wxCommandEvent&) { wxGetApp().app_config->set_bool("use_perspective_camera", false); - wxGetApp().update_ui_from_settings(); + m_plater->update_camera_from_settings(); }, nullptr); if (wxGetApp().app_config->get("use_perspective_camera").compare("true") == 0) viewMenu->Check(wxID_CAMERA_PERSPECTIVE + camera_id_base, true); else viewMenu->Check(wxID_CAMERA_ORTHOGONAL + camera_id_base, true); viewMenu->AppendSeparator(); + + append_menu_check_item( + viewMenu, wxID_ANY, _L("Free Camera Rotation") + "\t" + ctrl + "U", _L("Allow unconstrained camera rotation in 3D views."), + [this](wxCommandEvent &e) { view_set_free_camera(e.GetInt() == 1); }, + this, [this]() { return m_tabpanel->GetSelection() == TabPosition::tp3DEditor || m_tabpanel->GetSelection() == TabPosition::tpPreview; }, + [this]() { return wxGetApp().app_config->get_bool("use_free_camera"); }, this); + append_menu_check_item( viewMenu, wxID_ANY, _L("Show 3D Navigator"), _L("Show 3D navigator in Prepare and Preview scene"), [this](wxCommandEvent &) { @@ -4193,6 +4205,16 @@ void MainFrame::view_zoom_to_fit() const canvas->zoom_to_fit(); } +void MainFrame::view_set_free_camera(bool enabled) const +{ + if (wxGetApp().app_config->get_bool("use_free_camera") != enabled) { + wxGetApp().app_config->set_bool("use_free_camera", enabled); + if (m_plater) + m_plater->update_camera_from_settings(); + } +} + + // #ys_FIXME_to_delete void MainFrame::on_presets_changed(SimpleEvent &event) { diff --git a/src/slic3r/GUI/MainFrame.hpp b/src/slic3r/GUI/MainFrame.hpp index e9221544c8..e486e5a1b8 100644 --- a/src/slic3r/GUI/MainFrame.hpp +++ b/src/slic3r/GUI/MainFrame.hpp @@ -336,6 +336,7 @@ class MainFrame : public DPIFrame int get_calibration_curr_tab(); void select_view(const std::string& direction); void view_zoom_to_fit() const; + void view_set_free_camera(bool enabled) const; // Propagate changed configuration from the Tab to the Plater and save changes to the AppConfig void on_config_changed(DynamicPrintConfig* cfg) const ; void set_print_button_to_default(PrintSelectType select_type); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index b137e2858c..95a52c224b 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -7824,16 +7824,11 @@ wxString Plater::get_slice_warning_string(GCodeProcessorResult::SliceWarning& wa void Plater::priv::apply_free_camera_correction(bool apply/* = true*/) { - bool use_perspective_camera = get_config("use_perspective_camera").compare("true") == 0; - if (use_perspective_camera) + if (wxGetApp().app_config->get_bool("use_perspective_camera")) camera.set_type(Camera::EType::Perspective); else camera.set_type(Camera::EType::Ortho); - if (apply -#ifdef SUPPORT_FREE_CAMERA - && wxGetApp().app_config->get("use_free_camera") != "1" -#endif - ) + if (apply && !wxGetApp().app_config->get_bool("use_free_camera")) camera.recover_from_free_camera(); } @@ -21014,6 +21009,18 @@ bool Plater::is_any_job_running() const void Plater::update_ui_from_settings() { p->update_ui_from_settings(); } +void Plater::update_camera_from_settings() const +{ + update_camera_manipulation_settings(); + p->apply_free_camera_correction(); + p->set_current_canvas_as_dirty(); +} + +void Plater::update_camera_manipulation_settings() const { + // Passthrough to static method, to avoid needing to include GLCanvas3D declarations in other units just for this. + GLCanvas3D::update_camera_manipulation_settings(); +} + void Plater::select_view(const std::string& direction) { p->select_view(direction); } void Plater::set_slice_from_slice_btn(bool flag) { p->set_slice_from_slice_btn(flag); } diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index ee76d204fc..842afdade5 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -472,6 +472,10 @@ class Plater: public wxPanel // Called after the Preferences dialog is closed and the program settings are saved. // Update the UI based on the current preferences. void update_ui_from_settings(); + // Called to update camera type and constraint based on current settings. + void update_camera_from_settings() const; + // Called to update cached camera manipulation settings based on user preferences. + void update_camera_manipulation_settings() const; //BBS void select_curr_plate_all(); diff --git a/src/slic3r/GUI/Preferences.cpp b/src/slic3r/GUI/Preferences.cpp index e0c2160737..4270bb6555 100644 --- a/src/slic3r/GUI/Preferences.cpp +++ b/src/slic3r/GUI/Preferences.cpp @@ -80,13 +80,17 @@ class ResetWarningsDialog : public DPIDialog bool m_expanded = false; }; -wxBoxSizer *PreferencesDialog::create_item_title(wxString title, wxWindow *parent, wxString tooltip) +wxBoxSizer *PreferencesDialog::create_item_title(const wxString &title, wxWindow *parent, const wxString &tooltip, short level /* = 0 */) { - wxBoxSizer *m_sizer_title = new wxBoxSizer(wxHORIZONTAL); + auto text = new wxStaticText(parent, wxID_ANY, title, wxDefaultPosition, wxDefaultSize, 0); + text->SetForegroundColour(ThemeColor::TextSecondary); + text->SetFont(level < 2 ? Label::Head_13 : Label::Head_12); + if (!tooltip.IsEmpty() && tooltip != title) + text->SetToolTip(tooltip); - auto m_title = new wxStaticText(parent, wxID_ANY, title, wxDefaultPosition, wxDefaultSize, 0); - m_title->SetForegroundColour(ThemeColor::TextSecondary); - m_title->SetFont(::Label::Head_13); + wxBoxSizer *sizer = new wxBoxSizer(wxHORIZONTAL); + sizer->AddSpacer(FromDIP(TITLE_PADDING)); + sizer->Add(text, wxSizerFlags().CenterVertical()); // The Preferences dialog has no native default push button (every visible button // is a custom-drawn ::Button, i.e. a plain wxWindow, not a Win32 BUTTON control). @@ -98,12 +102,10 @@ wxBoxSizer *PreferencesDialog::create_item_title(wxString title, wxWindow *paren // hidden, zero-size native button as a stable in-thread default so the walk finds its // target immediately and never leaves our own windows. // The hang is first found on windows, but keep it on other platforms is no harm - auto line = new wxPanel(parent, wxID_ANY, wxDefaultPosition, wxSize(-1, 1)); - - m_sizer_title->AddSpacer(FromDIP(TITLE_PADDING)); - m_sizer_title->Add(m_title, wxSizerFlags().CenterVertical()); + if (level == 0) + auto line = new wxPanel(parent, wxID_ANY, wxDefaultPosition, wxSize(-1, 1)); - return m_sizer_title; + return sizer; } wxBoxSizer *PreferencesDialog::create_item_combobox(wxString title, wxWindow *parent, wxString tooltip, std::string param, const std::vector& label_list, const std::vector& value_list, std::function callback, int title_width, int combox_width) @@ -800,7 +802,7 @@ void PreferencesDialog::set_dark_mode() #endif } -wxBoxSizer *PreferencesDialog::create_item_checkbox(wxString title, wxWindow *parent, wxString tooltip, int padding_left, std::string param) +wxBoxSizer *PreferencesDialog::create_item_checkbox(const wxString &title, wxWindow *parent, const wxString &tooltip, int padding_left, const std::string ¶m, std::function callback /* = nullptr */) { wxBoxSizer *m_sizer_checkbox = new wxBoxSizer(wxHORIZONTAL); m_sizer_checkbox->SetMinSize(wxSize(-1, FromDIP(ITEM_MIN_HEIGHT))); @@ -828,7 +830,7 @@ wxBoxSizer *PreferencesDialog::create_item_checkbox(wxString title, wxWindow *pa m_sizer_checkbox->Add(checkbox, wxSizerFlags().CenterVertical().Border(wxRIGHT, FromDIP(ITEM_RIGHT_PADDING))); //// save config - checkbox->Bind(wxEVT_TOGGLEBUTTON, [this, checkbox, param](wxCommandEvent &e) { + checkbox->Bind(wxEVT_TOGGLEBUTTON, [=](wxCommandEvent &e) { if (param == "privacyuse") { app_config->set("firstguide", param, checkbox->GetValue()); NetworkAgent* agent = GUI::wxGetApp().getAgent(); @@ -849,6 +851,10 @@ wxBoxSizer *PreferencesDialog::create_item_checkbox(wxString title, wxWindow *pa app_config->save(); } + if (callback) { + callback(e.GetInt()); + } + if (param == "staff_pick_switch") { bool pbool = app_config->get("staff_pick_switch") == "true"; wxGetApp().switch_staff_pick(pbool); @@ -1506,9 +1512,6 @@ wxWindow *PreferencesDialog::create_3d_tab() auto title_3d = create_item_title(_L("3D Settings"), scrolled, _L("3D Settings")); - auto item_zoom_to_mouse = create_item_checkbox(_L("Zoom to mouse position"), scrolled, - _L("Zoom in towards the mouse pointer's position in the 3D view, rather than the 2D window center."), 50, "zoom_to_mouse"); - std::vector assemble_view_preview_options = {_L("Auto"), _L("Open"), _L("Close")}; auto enable_assemble_view_preview = create_item_combobox( _L("Display overview"), scrolled, _L("Display overview"), "enable_assemble_view_preview", assemble_view_preview_options, {"Auto", "Open", "Close"}, @@ -1574,7 +1577,6 @@ wxWindow *PreferencesDialog::create_3d_tab() sizer->Add(item_grabber_size, flags); sizer->Add(item_tooltip_offset, flags); sizer->Add(item_toolbar_style, flags); - sizer->Add(item_zoom_to_mouse, flags); sizer->Add(item_show_shells, flags); #if !BBL_RELEASE_TO_PUBLIC auto item_show_bvh_bounds = create_item_checkbox(_L("Show assembly BVH primary bounds"), scrolled, _L("Display the BVH primary bounding box wireframe in assembly view."), 50, @@ -1595,6 +1597,8 @@ wxWindow *PreferencesDialog::create_3d_tab() "camera_fullscreen_active_monitor_only"); sizer->Add(item_camera_fullscreen, flags); // [refactor-review] + sizer->Add(create_camera_page(scrolled), 0, wxEXPAND, FromDIP(10)); + sizer->AddSpacer(FromDIP(20)); scrolled->SetSizer(sizer); scrolled->FitInside(); @@ -1634,7 +1638,7 @@ wxWindow *PreferencesDialog::create_other_tab() sizer->Add(item_gcodes_warning, flags); // ---- Online Models (visible only when has_model_mall()) ---- - auto title_modelmall = create_item_title(_L("Online Models"), scrolled, _L("Online Models")); + auto title_modelmall = create_item_title(_L("Online Models"), scrolled, _L("Online Models"), 1); auto item_modelmall = create_item_checkbox(_L("Show online staff-picked models on the home page"), scrolled, _L("Show online staff-picked models on the home page"), 50, "staff_pick_switch"); auto item_show_history = create_item_checkbox(_L("Show history on the home page"), scrolled, _L("Show history on the home page"), 50, "show_print_history"); @@ -1655,7 +1659,7 @@ wxWindow *PreferencesDialog::create_other_tab() update_modelmall(dummy); // ---- Developer Mode (Figma keeps these two here, in the Other tab) ---- - auto title_dev = create_item_title(_L("Developer Mode"), scrolled, _L("Developer Mode")); + auto title_dev = create_item_title(_L("Developer Mode"), scrolled, _L("Developer Mode"), 1); auto item_dev_mode = create_item_checkbox(_L("Develop mode"), scrolled, _L("Develop mode"), 50, "developer_mode"); auto item_skip_blacklist = create_item_checkbox(_L("Skip AMS blacklist check"), scrolled, _L("Skip AMS blacklist check"), 50, "skip_ams_blacklist_check"); sizer->Add(title_dev, wxSizerFlags().Expand().Border(wxTOP, FromDIP(16))); @@ -1664,7 +1668,7 @@ wxWindow *PreferencesDialog::create_other_tab() #ifdef _WIN32 // ---- Associate Files To Bambu Studio (Windows only) ---- - auto title_associate_file = create_item_title(_L("Associate Files To Bambu Studio"), scrolled, _L("Associate Files To Bambu Studio")); + auto title_associate_file = create_item_title(_L("Associate Files To Bambu Studio"), scrolled, _L("Associate Files To Bambu Studio"), 1); auto item_associate_3mf = create_item_checkbox(_L("Associate .3mf files to Bambu Studio"), scrolled, _L("If enabled, sets Bambu Studio as default application to open .3mf files"), 50, "associate_3mf"); auto item_associate_stl = create_item_checkbox(_L("Associate .stl files to Bambu Studio"), scrolled, @@ -1683,6 +1687,198 @@ wxWindow *PreferencesDialog::create_other_tab() return scrolled; } +wxWindow* PreferencesDialog::create_camera_page(wxWindow *parent) +{ + auto page = new wxWindow(parent, wxID_ANY); + page->SetBackgroundColour(*wxWHITE); + + auto title_view_control = create_item_title(_L("Camera Controls"), page, _L("These settings adjust how to interact with 3D view camera controls."), 1); + + static const auto update_cam_manip_settings = [](int) { + if (const Plater *p = wxGetApp().plater()) + p->update_camera_manipulation_settings(); + }; + + auto item_mouse_zoom_settings = create_item_checkbox( + _L("Zoom to mouse position"), page, + _L("Zoom in towards the mouse pointer's position in the 3D view, rather than the 2D window center."), + 50, "zoom_to_mouse", update_cam_manip_settings + ); + + auto item_mouse_zoom_reverse = create_item_checkbox( + _L("Invert Zoom Direction"), page, + _L("Reverses mouse wheel zoom action so that scrolling down/back will zoom in instead of out."), + 50, "reverse_mouse_wheel_zoom", update_cam_manip_settings + ); + + auto item_rot_factor = create_item_range_input(_L("Rotation Speed Scale"), page, _L( + "This setting determines how sensitive the camera rotation (orbit) speed is in relation to mouse movement. Lower values increase precision and slow down the rotation.\n" + "Range: 0.01 - 3.00; Default: 0.8.\n\n" + "Note that holding down the Shift key while rotating will reduce the scaling to half of this value, temporarily allowing finer control." + ), + "view_rotate_speed_factor", 0.01f, 3.0f, 2, [](wxString) { update_cam_manip_settings(0); } + ); + + auto title_mouse_buttons = create_item_title(_L("Mouse Button Assignment"), page, _L( + "These settings determine which actions are preformed when the corresponding mouse button is pressed while dragging across the 3D view.\n" + "The left mouse button is always used to select and manipulate items, regardless of the choices selected here." + ), 2); + + static const std::vector mouse_btn_labels = { + _CTX(L_CONTEXT("Left", "Mouse Button"), "Mouse Button"), + _CTX(L_CONTEXT("Middle", "Mouse Button"), "Mouse Button"), + _CTX(L_CONTEXT("Right", "Mouse Button"), "Mouse Button"), + _CTX(L_CONTEXT("Aux 1", "Mouse Button"), "Mouse Button"), + _CTX(L_CONTEXT("Aux 2", "Mouse Button"), "Mouse Button"), + }; + static const std::vector mouse_btn_names = { "left", "mid", "right", "aux1", "aux2" }; + + // Pan control has the legacy default of using either middle or right button. + static const std::vector mouse_btn_pan_labels = [&]() { + auto tmp(mouse_btn_labels); + tmp.push_back(_CTX(L_CONTEXT("Right or Middle", "Mouse Button"), "Mouse Button")); + return tmp; + }(); + static const std::vector mouse_btn_pan_names = [&]() { + auto tmp(mouse_btn_names); + tmp.push_back("any"); + return tmp; + }(); + + auto item_rot_btn = create_item_combobox( + _L("Mouse Rotate Button"), page, + _L("Which mouse button to hold while dragging to control camera rotation."), + "view_rotate_mb", mouse_btn_labels, mouse_btn_names, + update_cam_manip_settings + ); + ::ComboBox *item_rot_btn_cbox = m_combobox_list[m_combobox_list.size()-1]; + + auto item_pan_btn = create_item_combobox( + _L("Mouse Pan Button"), page, + _L("Which mouse button to hold while dragging to control camera panning."), + "view_pan_mb", mouse_btn_pan_labels, mouse_btn_pan_names, + update_cam_manip_settings + ); + ::ComboBox *item_pan_btn_cbox = m_combobox_list[m_combobox_list.size()-1]; + + // Ensures that rotate and pan button choices must be exclusive, changes other action choice if needed. + // eg. if pan button is changed to be same as rotate, the rotate button choice is changed to compensate. + const auto validate_mouse_btns = [this, item_rot_btn_cbox, item_pan_btn_cbox](char idx) + { + const std::string rot_mb = app_config->get("view_rotate_mb"); + const std::string pan_mb = app_config->get("view_pan_mb"); + if (rot_mb != pan_mb && pan_mb != "any") + return; + const std::string &this_btn = (idx == 'r' ? rot_mb : pan_mb); + std::string other_btn; + if (this_btn == "any") { + if (rot_mb == "mid" || rot_mb == "right") + other_btn = "left"; + else + return; + } + else if (idx == 'r' && pan_mb == "any") { + if (this_btn == "mid") + other_btn = "right"; + else if (this_btn == "right") + other_btn = "mid"; + else + return; + } + else if (this_btn == "left" || this_btn == "mid") + other_btn = "right"; + else if (this_btn == "right") + other_btn = "mid"; + else if (this_btn == "aux1") + other_btn = "aux2"; + else if (this_btn == "aux2") + other_btn = "aux1"; + else + other_btn = "left"; + if (auto *other_cbox = (idx == 'r' ? item_pan_btn_cbox : item_rot_btn_cbox)) + other_cbox->SetSelection((int)std::distance(mouse_btn_names.cbegin(), std::find(mouse_btn_names.cbegin(), mouse_btn_names.cend(), other_btn))); + // ComboBox::SetSelection() doesn't trigger any callbacks and neither does SelectAndNotify() (and that also causes recursion here). + app_config->set(idx == 'r' ? "view_pan_mb" : "view_rotate_mb", other_btn); + app_config->save(); + update_cam_manip_settings(0); + }; + // These handlers must be added after creating the combos because the callback's capture must have the instances already created. + item_rot_btn_cbox->Bind(wxEVT_COMBOBOX, [=](wxEvent &e) { validate_mouse_btns('r'); e.Skip(); }); + item_pan_btn_cbox->Bind(wxEVT_COMBOBOX, [=](wxEvent &e) { validate_mouse_btns('p'); e.Skip(); }); + + auto title_rot_mode = create_item_title(_L("Rotation Mode"), page, _L( + "Select camera rotation modes to use based on keyboard modifier(s) pressed while dragging the mouse.\n\n" + "Selection or Cursor - Rotate around the center of selected object(s), or around cursor position when no selection and in Preview mode.\n" + "Cursor (always) - Always around where the cursor was positioned on the initial click, regardless of selection.\n" + "View Center - Rotate around the center of the current view window, clamped at zero Z height.\n" + "Selected Plate - Rotate around the center of the currently active build plate.\n" + "Camera Target - Rotate around whatever the camera is currently focused on. This acts somewhat like a 3D mouse and may be useful when transitioning between devices.\n" + ), 2); + + static const std::vector camera_rotation_type_labels = { + _CTX(L_CONTEXT("Selection or Cursor", "Camera rotation mode"), "Camera rotation mode"), + _CTX(L_CONTEXT("Cursor (always)", "Camera rotation mode"), "Camera rotation mode"), + _CTX(L_CONTEXT("View Center", "Camera rotation mode"), "Camera rotation mode"), + _CTX(L_CONTEXT("Selected Plate", "Camera rotation mode"), "Camera rotation mode"), + _CTX(L_CONTEXT("Camera Target", "Camera rotation mode"), "Camera rotation mode"), + }; + static const std::vector camera_rotation_type_names = { "selection", "cursor", "center", "plate", "target", }; + + auto item_rot_mode_nomod = create_item_combobox( + _L("Default (no key)"), page, + _L("The camera rotation type to use when none of the other key choices below are held down."), + "view_rotate_mode_nomod", camera_rotation_type_labels, camera_rotation_type_names, + update_cam_manip_settings + ); + + auto item_rot_mode_ctrl = create_item_combobox( +#ifdef __APPLE__ + _L("With Command key"), page, + _L("The camera rotation type to use when the Command key is held."), +#else + _L("With Ctrl key"), page, + _L("The camera rotation type to use when the Control key is held."), +#endif + "view_rotate_mode_ctrl", camera_rotation_type_labels, camera_rotation_type_names, + update_cam_manip_settings + ); + + auto item_rot_mode_alt = create_item_combobox( +#ifdef __APPLE__ + _L("With Option key"), page, + _L("The camera rotation type to use when the Option key is held."), +#else + _L("With Alt key"), page, + _L("The camera rotation type to use when the Alt key is held."), +#endif + "view_rotate_mode_alt", camera_rotation_type_labels, camera_rotation_type_names, + update_cam_manip_settings + ); + + wxBoxSizer *sizer = new wxBoxSizer(wxVERTICAL); + auto flags = wxSizerFlags().Expand().Border(wxTOP, FromDIP(4)); + + sizer->Add(title_view_control, 0, wxTOP | wxEXPAND, FromDIP(20)); + sizer->Add(item_mouse_zoom_settings, flags); + sizer->Add(item_mouse_zoom_reverse, flags); + sizer->Add(item_rot_factor, flags); + + sizer->Add(title_mouse_buttons, 0, wxTOP | wxLEFT | wxEXPAND, FromDIP(5)); + sizer->Add(item_rot_btn, flags); + sizer->Add(item_pan_btn, flags); + + sizer->Add(title_rot_mode, 0, wxTOP | wxLEFT | wxEXPAND, FromDIP(5)); + sizer->Add(item_rot_mode_nomod, flags); + sizer->Add(item_rot_mode_ctrl, flags); + sizer->Add(item_rot_mode_alt, flags); + + page->SetSizer(sizer); + page->Layout(); + sizer->Fit(page); + + return page; +} + wxWindow *PreferencesDialog::create_developer_tab() { auto scrolled = new ScrollPanel(m_book); @@ -1702,12 +1898,12 @@ wxWindow *PreferencesDialog::create_developer_tab() sizer->AddSpacer(FromDIP(4)); sizer->Add(item_log, flags); - auto title_dev = create_item_title(_L("Developer Tools"), scrolled, _L("Developer Tools")); + auto title_dev = create_item_title(_L("Developer Tools"), scrolled, _L("Developer Tools"), 1); auto item_internal_dev = create_item_checkbox(_L("Internal developer mode"), scrolled, _L("Internal developer mode"), 50, "internal_developer_mode"); auto item_ssl_mqtt = create_item_checkbox(_L("Enable SSL(MQTT)"), scrolled, _L("Enable SSL(MQTT)"), 50, "enable_ssl_for_mqtt"); auto item_ssl_ftp = create_item_checkbox(_L("Enable SSL(FTP)"), scrolled, _L("Enable SSL(FTP)"), 50, "enable_ssl_for_ftp"); - auto title_host = create_item_title(_L("Host Setting"), scrolled, _L("Host Setting")); + auto title_host = create_item_title(_L("Host Setting"), scrolled, _L("Host Setting"), 1); auto radio1 = create_item_radiobox(_L("DEV host: api-dev.bambu-lab.com/v1"), scrolled, wxEmptyString, 50, 1, "dev_host"); auto radio2 = create_item_radiobox(_L("QA host: api-qa.bambu-lab.com/v1"), scrolled, wxEmptyString, 50, 1, "qa_host"); auto radio3 = create_item_radiobox(_L("PRE host: api-pre.bambu-lab.com/v1"), scrolled, wxEmptyString, 50, 1, "pre_host"); diff --git a/src/slic3r/GUI/Preferences.hpp b/src/slic3r/GUI/Preferences.hpp index b0c3287e75..46cbaf5a17 100644 --- a/src/slic3r/GUI/Preferences.hpp +++ b/src/slic3r/GUI/Preferences.hpp @@ -90,12 +90,12 @@ class PreferencesDialog : public DPIDialog RadioSelectorList m_radio_group; // ComboBoxSelectorList m_comxbo_group; - wxBoxSizer *create_item_title(wxString title, wxWindow *parent, wxString tooltip); + wxBoxSizer *create_item_title(const wxString &title, wxWindow *parent, const wxString &tooltip, short level = 0); wxBoxSizer *create_item_combobox(wxString title, wxWindow *parent, wxString tooltip, std::string param,const std::vector& label_list, const std::vector& value_list, std::function callback = nullptr, int title_width = 0, int combox_width = 0); wxBoxSizer *create_item_region_combobox(wxString title, wxWindow *parent, wxString tooltip, std::vector vlist); wxBoxSizer *create_item_language_combobox(wxString title, wxWindow *parent, wxString tooltip, int padding_left, std::string param, std::vector vlist); wxBoxSizer *create_item_loglevel_combobox(wxString title, wxWindow *parent, wxString tooltip, std::vector vlist); - wxBoxSizer *create_item_checkbox(wxString title, wxWindow *parent, wxString tooltip, int padding_left, std::string param); + wxBoxSizer *create_item_checkbox(const wxString &title, wxWindow *parent, const wxString &tooltip, int padding_left, const std::string ¶m, std::function callback = nullptr); wxBoxSizer *create_item_darkmode_checkbox(wxString title, wxWindow *parent, wxString tooltip, int padding_left, std::string param); void set_dark_mode(); wxWindow* create_item_downloads(wxWindow* parent, int padding_left, std::string param); @@ -120,6 +120,7 @@ class PreferencesDialog : public DPIDialog wxWindow* create_general_tab(); wxWindow* create_user_tab(); wxWindow* create_3d_tab(); + wxWindow* create_camera_page(wxWindow *parent); wxWindow* create_other_tab(); wxWindow* create_developer_tab(); wxBoxSizer *create_bottom_buttons(); diff --git a/src/slic3r/GUI/wxExtensions.hpp b/src/slic3r/GUI/wxExtensions.hpp index 4681fb4822..4242090e52 100644 --- a/src/slic3r/GUI/wxExtensions.hpp +++ b/src/slic3r/GUI/wxExtensions.hpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include @@ -82,9 +83,44 @@ wxColourData show_sys_picker_dialog(wxWindow *parent, const wxColourData &clr_da namespace Slic3r { namespace GUI { + class BitmapComboBox; + +/// Checks pressed status of a mouse button within a given `wxMouseState` structure (typically from a mouse event). Returns: +/// - When `button` is in `wxMOUSE_BTN_LEFT` - `wxMOUSE_BTN_AUX2` range, then the corresponding `state.*IsDown()` result for given `button`; +/// - When `button == wxMOUSE_BTN_ANY` then return `true` if any button returns `true` for `state.*IsDown()`; +/// - When `button == wxMOUSE_BTN_NONE` then return `true` if all buttons return `false` for `state.*IsDown()` (opposite of `wxMOUSE_BTN_ANY`); +/// - `false` if `button` value is out of range. +inline bool wx_mouse_button_is_down(const wxMouseState& state, wxMouseButton button) { + switch (button) { + case wxMouseButton::wxMOUSE_BTN_LEFT: return state.LeftIsDown(); + case wxMouseButton::wxMOUSE_BTN_MIDDLE: return state.MiddleIsDown(); + case wxMouseButton::wxMOUSE_BTN_RIGHT: return state.RightIsDown(); + case wxMouseButton::wxMOUSE_BTN_AUX1: return state.Aux1IsDown(); + case wxMouseButton::wxMOUSE_BTN_AUX2: return state.Aux2IsDown(); + case wxMouseButton::wxMOUSE_BTN_ANY: + return state.LeftIsDown() || state.MiddleIsDown() || state.RightIsDown() || state.Aux1IsDown() || state.Aux2IsDown(); + case wxMouseButton::wxMOUSE_BTN_NONE: + return !wx_mouse_button_is_down(state, wxMOUSE_BTN_ANY); + default: return false; + } } + +/// Returns a corresponding `wsMouseButton` enum value for given `name` if one is matched, or `wxMOUSE_BTN_NONE` otherwise. +/// Recognized strings: "any", "left", "mid", "right", "aux1", "aux2" +inline wxMouseButton mouse_button_name_to_wx_enum(const std::string& name) { + if (!name.compare("left")) return wxMouseButton::wxMOUSE_BTN_LEFT; + if (!name.compare("right")) return wxMouseButton::wxMOUSE_BTN_RIGHT; + if (!name.compare("mid")) return wxMouseButton::wxMOUSE_BTN_MIDDLE; + if (!name.compare("aux1")) return wxMouseButton::wxMOUSE_BTN_AUX1; + if (!name.compare("aux2")) return wxMouseButton::wxMOUSE_BTN_AUX2; + if (!name.compare("any")) return wxMouseButton::wxMOUSE_BTN_ANY; + return wxMouseButton::wxMOUSE_BTN_NONE; } + +} // ns GUI +} // ns Slic3r + void apply_extruder_selector(Slic3r::GUI::BitmapComboBox** ctrl, wxWindow* parent, const std::string& first_item = "",