diff --git a/localization/i18n/list.txt b/localization/i18n/list.txt index aab1c50ec9..f3a78f894a 100644 --- a/localization/i18n/list.txt +++ b/localization/i18n/list.txt @@ -82,6 +82,7 @@ src/slic3r/GUI/Gizmos/GLGizmoMeasure.cpp src/slic3r/GUI/Gizmos/GLGizmoMeasure.hpp src/slic3r/GUI/Gizmos/GLGizmoAssembly.cpp src/slic3r/GUI/Gizmos/GLGizmoAssembly.hpp +src/slic3r/GUI/Gizmos/GLGizmoAlignment.cpp src/slic3r/GUI/GUI.cpp src/slic3r/GUI/GUI_App.cpp src/slic3r/GUI/GUI_AuxiliaryList.cpp diff --git a/resources/images/align_x_center.svg b/resources/images/align_x_center.svg new file mode 100644 index 0000000000..a33ce81073 --- /dev/null +++ b/resources/images/align_x_center.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/images/align_x_center_dark.svg b/resources/images/align_x_center_dark.svg new file mode 100644 index 0000000000..cbf9a58f0d --- /dev/null +++ b/resources/images/align_x_center_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/images/align_x_max.svg b/resources/images/align_x_max.svg new file mode 100644 index 0000000000..d6f3603d92 --- /dev/null +++ b/resources/images/align_x_max.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/images/align_x_max_dark.svg b/resources/images/align_x_max_dark.svg new file mode 100644 index 0000000000..d33f84c409 --- /dev/null +++ b/resources/images/align_x_max_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/images/align_x_min.svg b/resources/images/align_x_min.svg new file mode 100644 index 0000000000..505c18899b --- /dev/null +++ b/resources/images/align_x_min.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/images/align_x_min_dark.svg b/resources/images/align_x_min_dark.svg new file mode 100644 index 0000000000..0392c91e46 --- /dev/null +++ b/resources/images/align_x_min_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/images/align_y_center.svg b/resources/images/align_y_center.svg new file mode 100644 index 0000000000..970aea781b --- /dev/null +++ b/resources/images/align_y_center.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/images/align_y_center_dark.svg b/resources/images/align_y_center_dark.svg new file mode 100644 index 0000000000..099b20b073 --- /dev/null +++ b/resources/images/align_y_center_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/images/align_y_max.svg b/resources/images/align_y_max.svg new file mode 100644 index 0000000000..a325ba35cd --- /dev/null +++ b/resources/images/align_y_max.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/images/align_y_max_dark.svg b/resources/images/align_y_max_dark.svg new file mode 100644 index 0000000000..4993cfd3d0 --- /dev/null +++ b/resources/images/align_y_max_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/images/align_y_min.svg b/resources/images/align_y_min.svg new file mode 100644 index 0000000000..6b4f666033 --- /dev/null +++ b/resources/images/align_y_min.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/images/align_y_min_dark.svg b/resources/images/align_y_min_dark.svg new file mode 100644 index 0000000000..420174006d --- /dev/null +++ b/resources/images/align_y_min_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/images/align_z_center.svg b/resources/images/align_z_center.svg new file mode 100644 index 0000000000..09cd8f6b5f --- /dev/null +++ b/resources/images/align_z_center.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/resources/images/align_z_center_dark.svg b/resources/images/align_z_center_dark.svg new file mode 100644 index 0000000000..42084b17b6 --- /dev/null +++ b/resources/images/align_z_center_dark.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/resources/images/align_z_max.svg b/resources/images/align_z_max.svg new file mode 100644 index 0000000000..0ffbb75a8b --- /dev/null +++ b/resources/images/align_z_max.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/resources/images/align_z_max_dark.svg b/resources/images/align_z_max_dark.svg new file mode 100644 index 0000000000..bad4d83a34 --- /dev/null +++ b/resources/images/align_z_max_dark.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/resources/images/align_z_min.svg b/resources/images/align_z_min.svg new file mode 100644 index 0000000000..8adae8932a --- /dev/null +++ b/resources/images/align_z_min.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/resources/images/align_z_min_dark.svg b/resources/images/align_z_min_dark.svg new file mode 100644 index 0000000000..0fcf55fc24 --- /dev/null +++ b/resources/images/align_z_min_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/resources/images/distribute_x.svg b/resources/images/distribute_x.svg new file mode 100644 index 0000000000..50252dc411 --- /dev/null +++ b/resources/images/distribute_x.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/resources/images/distribute_x_dark.svg b/resources/images/distribute_x_dark.svg new file mode 100644 index 0000000000..f9c9ea3fbf --- /dev/null +++ b/resources/images/distribute_x_dark.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/resources/images/distribute_y.svg b/resources/images/distribute_y.svg new file mode 100644 index 0000000000..38da79e822 --- /dev/null +++ b/resources/images/distribute_y.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/resources/images/distribute_y_dark.svg b/resources/images/distribute_y_dark.svg new file mode 100644 index 0000000000..9133148e0d --- /dev/null +++ b/resources/images/distribute_y_dark.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/resources/images/distribute_z.svg b/resources/images/distribute_z.svg new file mode 100644 index 0000000000..8ffe8b9d53 --- /dev/null +++ b/resources/images/distribute_z.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/resources/images/distribute_z_dark.svg b/resources/images/distribute_z_dark.svg new file mode 100644 index 0000000000..fd34062e62 --- /dev/null +++ b/resources/images/distribute_z_dark.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 88dc228ac5..73bd498656 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -4978,7 +4978,7 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) // Don't deselect a volume if layer editing is enabled or any gizmo is active. We want the object to stay selected // during the scene manipulation. - if (m_picking_enabled && (!any_gizmo_active || !evt.CmdDown()) && (!m_hover_volume_idxs.empty())) { + if (m_picking_enabled && (m_gizmos.is_allow_multi_select_parts_or_objects() || !evt.CmdDown()) && (!m_hover_volume_idxs.empty())) { if (evt.LeftDown() && !m_hover_volume_idxs.empty()) { int volume_idx = get_first_hover_volume_idx(); bool already_selected = m_selection.contains_volume(volume_idx); @@ -4988,7 +4988,18 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) if (already_selected && ctrl_down) m_selection.remove(volume_idx); + else if (alt_down && m_gizmos.is_allow_multi_select_parts_or_objects() && m_selection.get_mode() == Selection::Volume && + m_selection.get_object_idx() == m_volumes.volumes[volume_idx]->object_idx() && + m_selection.get_instance_idx() == m_volumes.volumes[volume_idx]->instance_idx()) { + // Alt+click on another part of the selected parts' instance adds it, and on a selected + // part removes it, unless it is the only one. + if (!already_selected) + m_selection.add_volumes(Selection::Volume, {(unsigned int) volume_idx}, false); + else if (curr_idxs.size() > 1) + m_selection.remove_volumes(Selection::Volume, {(unsigned int) volume_idx}); + } else if (alt_down) { + // Select just the clicked part, also when an object is selected. Selection::EMode mode = Selection::Volume; if (already_selected) { std::vector volume_idxs; @@ -7963,8 +7974,8 @@ void GLCanvas3D::_picking_pass() if (0 <= hit.raycaster_id && hit.raycaster_id < (int)m_volumes.volumes.size()) { const GLVolume* volume = m_volumes.volumes[hit.raycaster_id]; if (volume->is_active && !volume->disabled && (volume->composite_id.volume_id >= 0 || m_render_sla_auxiliaries)) { - // do not add the volume id if any gizmo is active and CTRL is pressed - if (m_gizmos.get_current_type() == GLGizmosManager::EType::Undefined || !wxGetKeyState(WXK_CONTROL)) + // do not add the volume id if CTRL is pressed while a gizmo that does not allow Ctrl+click selection is active + if (m_gizmos.is_allow_multi_select_parts_or_objects() || !wxGetKeyState(WXK_CONTROL)) m_hover_volume_idxs.emplace_back(hit.raycaster_id); m_gizmos.set_hover_id(-1); } diff --git a/src/slic3r/GUI/Gizmos/GLGizmoAlignment.cpp b/src/slic3r/GUI/Gizmos/GLGizmoAlignment.cpp index 3b1c5f350e..96b85215a1 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoAlignment.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoAlignment.cpp @@ -2,6 +2,7 @@ #include "slic3r/GUI/3DScene.hpp" #include "slic3r/GUI/GLCanvas3D.hpp" #include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/I18N.hpp" #include "slic3r/GUI/PartPlate.hpp" #include "slic3r/GUI/Plater.hpp" #include "slic3r/GUI/Selection.hpp" @@ -627,8 +628,10 @@ BoundingBoxf3 GLGizmoAlignment::get_parent_box() const PartPlate *plate = plate_list.get_plate(plate_list.find_instance(obj_idx, *inst_idxs.begin())); if (plate == nullptr) break; + BoundingBoxf3 instances_box; + get_selected_objects_info(instances_box); box = plate->get_plate_box(); - box.max.z() = get_items_box().max.z(); + box.max.z() = instances_box.max.z(); // Keep the aligned objects off the plate edge. box.min += Vec3d(0.1, 0.1, 0.1); box.max -= Vec3d(0.1, 0.1, 0.1); @@ -663,10 +666,18 @@ void GLGizmoAlignment::finish_operation(bool force_volume_move) m_canvas.do_move("", force_volume_move); } -size_t GLGizmoAlignment::get_items_count(AlignType type, bool align_parent) const +size_t GLGizmoAlignment::get_items_count(AlignType type, bool align_parent, std::string *why_not) const { - if (is_parts_selection(align_parent)) - return get_selected_part_volumes().size(); + auto cannot_move = [why_not](const std::string &reason) { + if (why_not != nullptr) + *why_not = reason; + return size_t(0); + }; + + if (is_parts_selection(align_parent)) { + const size_t count = get_selected_part_volumes().size(); + return count > 0 ? count : cannot_move(_u8L("Parts of a cut object cannot be moved")); + } // Instances are moved through their instance offsets, which do_move() commits only from an Instance selection. const Selection &selection = get_selection(); @@ -677,7 +688,7 @@ size_t GLGizmoAlignment::get_items_count(AlignType type, bool align_parent) cons type == AlignType::DISTRIBUTE_Z; // The plate gives no Z target, and aligning to its bottom would duplicate Drop. if (is_z && align_parent) - return 0; + return cannot_move(_u8L("Aligning to the plate is available in X and Y only")); PartPlateList &plate_list = wxGetApp().plater()->get_partplate_list(); int plate_idx = -1; @@ -687,13 +698,13 @@ size_t GLGizmoAlignment::get_items_count(AlignType type, bool align_parent) cons continue; // Auto-drop would put a raised object back on the bed and leave a lowered one sunk. if (is_z && selection.get_model()->objects[obj_idx]->auto_drop) - return 0; + return cannot_move(_u8L("Turn off Auto-Drop for the selected objects to move them in Z")); if (align_parent) { // The plate holding the selection is the target, so it must hold all of it. for (int inst_idx : inst_idxs) { const int idx = plate_list.find_instance(obj_idx, inst_idx); if (idx == -1 || (plate_idx != -1 && idx != plate_idx)) - return 0; + return cannot_move(_u8L("All selected objects must be on the same plate")); plate_idx = idx; } } diff --git a/src/slic3r/GUI/Gizmos/GLGizmoAlignment.hpp b/src/slic3r/GUI/Gizmos/GLGizmoAlignment.hpp index 5300cc3a99..daca519034 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoAlignment.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoAlignment.hpp @@ -77,6 +77,8 @@ public: // World bounding box aligned to with align_parent: the instance of a parts selection, or the plate holding the // selected instances, up to their top. BoundingBoxf3 get_parent_box() const; + // Number of items the operation would move, or 0 when it cannot move them; why_not then gets the reason, if any. + size_t get_items_count(AlignType type, bool align_parent, std::string *why_not = nullptr) const; private: GLCanvas3D& m_canvas; @@ -92,7 +94,6 @@ private: bool take_snapshot(const std::string& name); void apply_transformation(int obj_idx, int inst_idx, const Vec3d& displacement); void finish_operation(bool force_volume_move = false); - size_t get_items_count(AlignType type, bool align_parent) const; }; } // namespace GUI diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMove.cpp b/src/slic3r/GUI/Gizmos/GLGizmoMove.cpp index 58fd0ce583..6968e81c7e 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMove.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMove.cpp @@ -31,6 +31,11 @@ #include "slic3r/GUI/Camera.hpp" #include "slic3r/GUI/GLModel.hpp" #include "slic3r/GUI/Gizmos/GizmoObjectManipulation.hpp" +#include "slic3r/GUI/Gizmos/GLGizmoAlignment.hpp" +#include "libslic3r/BoundingBox.hpp" +#include +#include +#include #include "slic3r/GUI/I18N.hpp" namespace Slic3r { @@ -109,6 +114,8 @@ void GLGizmoMove3D::on_set_state() { m_last_selected_volume_idx = -1; change_cs_by_selection(); } + if (m_object_manipulation) + m_object_manipulation->reset_align_state(); } void GLGizmoMove3D::on_start_dragging() @@ -157,6 +164,8 @@ void GLGizmoMove3D::on_render() glsafe(::glClear(GL_DEPTH_BUFFER_BIT)); glsafe(::glEnable(GL_DEPTH_TEST)); + render_align_preview(); + const auto &[box, box_trafo] = selection.get_bounding_box_in_current_reference_system(); m_bounding_box = box; m_center = box_trafo.translation(); @@ -340,6 +349,93 @@ double GLGizmoMove3D::calc_projection(const UpdateData& data) const return projection; } +void GLGizmoMove3D::render_align_preview() +{ + using AlignType = GLGizmoAlignment::AlignType; + if (m_object_manipulation == nullptr || m_object_manipulation->get_hovered_align_type() == AlignType::NONE) + return; + + // The box the hovered operation aligns to, in world coordinates like the operation itself. + const AlignType type = m_object_manipulation->get_hovered_align_type(); + const GLGizmoAlignment &helper = m_object_manipulation->get_alignment_helper(); + const BoundingBoxf3 box = m_object_manipulation->get_align_to_parent() ? helper.get_parent_box() : helper.get_items_box(); + if (!box.defined) + return; + m_parent.get_selection().render_bounding_box(box, {0.0f, 150.0f / 255.0f, 136.0f / 255.0f}, m_parent.get_scale()); + + GLShaderProgram* shader = wxGetApp().get_shader("flat"); + if (shader == nullptr) + return; + + if (!m_align_plane.is_initialized()) { + GLModel::Geometry init_data; + init_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3 }; + init_data.reserve_vertices(4); + init_data.reserve_indices(6); + init_data.add_vertex(Vec3f(-0.5f, -0.5f, 0.0f)); + init_data.add_vertex(Vec3f(0.5f, -0.5f, 0.0f)); + init_data.add_vertex(Vec3f(0.5f, 0.5f, 0.0f)); + init_data.add_vertex(Vec3f(-0.5f, 0.5f, 0.0f)); + init_data.add_triangle(0, 1, 2); + init_data.add_triangle(0, 2, 3); + m_align_plane.init_from(std::move(init_data)); + m_align_plane.set_color({ 1.0f, 1.0f, 1.0f, 0.5f }); + } + + // A plane across the box at its min, center or max on the hovered axis. Distributing shows up to six evenly + // spaced planes. + int axis = Z; + double t = 0.5; + switch (type) { + case AlignType::X_MIN: axis = X; t = 0.0; break; + case AlignType::X_MAX: axis = X; t = 1.0; break; + case AlignType::CENTER_X: + case AlignType::DISTRIBUTE_X: axis = X; break; + case AlignType::Y_MIN: axis = Y; t = 0.0; break; + case AlignType::Y_MAX: axis = Y; t = 1.0; break; + case AlignType::CENTER_Y: + case AlignType::DISTRIBUTE_Y: axis = Y; break; + case AlignType::Z_MIN: t = 0.0; break; + case AlignType::Z_MAX: t = 1.0; break; + default: break; + } + const Vec3d size = box.size(); + std::vector positions; + if (type >= AlignType::DISTRIBUTE_X) { + const size_t count = std::min(helper.get_items_count(type, false), 6); + for (size_t i = 1; i <= count; ++i) + positions.push_back(box.min[axis] + i * size[axis] / (count + 1)); + } else + positions.push_back(box.min[axis] + t * size[axis]); + + const int u = (axis + 1) % 3; + const int v = (axis + 2) % 3; + Transform3d plane_matrix = Transform3d::Identity(); + plane_matrix.linear().col(0) = size[u] * Vec3d::Unit(u); + plane_matrix.linear().col(1) = size[v] * Vec3d::Unit(v); + plane_matrix.linear().col(2) = Vec3d::Unit(axis); + Vec3d plane_center = box.center(); + + glsafe(::glDisable(GL_CULL_FACE)); + glsafe(::glEnable(GL_BLEND)); + glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)); + // Translucent planes must hide neither each other nor the grabbers drawn after them. + glsafe(::glDepthMask(GL_FALSE)); + shader->start_using(); + const Camera& camera = wxGetApp().plater()->get_camera(); + shader->set_uniform("projection_matrix", camera.get_projection_matrix()); + for (double position : positions) { + plane_center[axis] = position; + plane_matrix.translation() = plane_center; + shader->set_uniform("view_model_matrix", camera.get_view_matrix() * plane_matrix); + m_align_plane.render(); + } + shader->stop_using(); + glsafe(::glDepthMask(GL_TRUE)); + glsafe(::glEnable(GL_CULL_FACE)); + glsafe(::glDisable(GL_BLEND)); +} + void GLGizmoMove3D::change_cs_by_selection() { int obejct_idx, volume_idx; ModelVolume *model_volume = m_parent.get_selection().get_selected_single_volume(obejct_idx, volume_idx); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMove.hpp b/src/slic3r/GUI/Gizmos/GLGizmoMove.hpp index 8296ba7c5e..3eb5522156 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMove.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMove.hpp @@ -34,6 +34,8 @@ class GLGizmoMove3D : public GLGizmoBase Vec3d old_center{ Vec3d::Zero() }; }; std::array m_grabber_connections; + // Unit square previewing the target plane of the hovered align or distribute icon. + GLModel m_align_plane; //BBS: add size adjust related GizmoObjectManipulation* m_object_manipulation; @@ -76,6 +78,7 @@ protected: private: double calc_projection(const UpdateData& data) const; + void render_align_preview(); void change_cs_by_selection(); //cs mean Coordinate System private: int m_last_selected_obejct_idx, m_last_selected_volume_idx; diff --git a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp index 41d4811664..a934c12bf2 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp @@ -367,7 +367,19 @@ bool GLGizmosManager::init_icon_textures() { IC_CANVAS_SECTION, "canvas_section" }, { IC_CANVAS_SECTION_HOVER, "canvas_section_hover" }, { IC_CANVAS_SECTION_DARK, "canvas_section_dark" }, { IC_CANVAS_SECTION_DARK_HOVER, "canvas_section_dark_hover" }, { IC_CANVAS_SECTION_ACTIVE, "canvas_section_active" }, { IC_CANVAS_SECTION_ACTIVE_HOVER, "canvas_section_active_hover" }, - { IC_CANVAS_SECTION_ACTIVE_DARK, "canvas_section_active_dark" }, { IC_CANVAS_SECTION_ACTIVE_DARK_HOVER, "canvas_section_active_dark_hover" } }) { + { IC_CANVAS_SECTION_ACTIVE_DARK, "canvas_section_active_dark" }, { IC_CANVAS_SECTION_ACTIVE_DARK_HOVER, "canvas_section_active_dark_hover" }, + { IC_ALIGN_X_MIN, "align_x_min" }, { IC_ALIGN_X_MIN_DARK, "align_x_min_dark" }, + { IC_ALIGN_X_CENTER, "align_x_center" }, { IC_ALIGN_X_CENTER_DARK, "align_x_center_dark" }, + { IC_ALIGN_X_MAX, "align_x_max" }, { IC_ALIGN_X_MAX_DARK, "align_x_max_dark" }, + { IC_ALIGN_Y_MIN, "align_y_min" }, { IC_ALIGN_Y_MIN_DARK, "align_y_min_dark" }, + { IC_ALIGN_Y_CENTER, "align_y_center" }, { IC_ALIGN_Y_CENTER_DARK, "align_y_center_dark" }, + { IC_ALIGN_Y_MAX, "align_y_max" }, { IC_ALIGN_Y_MAX_DARK, "align_y_max_dark" }, + { IC_ALIGN_Z_MIN, "align_z_min" }, { IC_ALIGN_Z_MIN_DARK, "align_z_min_dark" }, + { IC_ALIGN_Z_CENTER, "align_z_center" }, { IC_ALIGN_Z_CENTER_DARK, "align_z_center_dark" }, + { IC_ALIGN_Z_MAX, "align_z_max" }, { IC_ALIGN_Z_MAX_DARK, "align_z_max_dark" }, + { IC_DISTRIBUTE_X, "distribute_x" }, { IC_DISTRIBUTE_X_DARK, "distribute_x_dark" }, + { IC_DISTRIBUTE_Y, "distribute_y" }, { IC_DISTRIBUTE_Y_DARK, "distribute_y_dark" }, + { IC_DISTRIBUTE_Z, "distribute_z" }, { IC_DISTRIBUTE_Z_DARK, "distribute_z_dark" } }) { if (!IMTexture::load_from_svg_file(Slic3r::resources_dir() + "/images/" + name + ".svg", 72, 72, texture_id)) return false; icon_list.insert(std::make_pair((int) icon, texture_id)); @@ -617,6 +629,13 @@ bool GLGizmosManager::is_allow_select_all() { return false; } +// Ctrl+click and Alt+click grow the selection only where that serves the gizmo: the Move gizmo aligns and distributes +// several objects or parts, while Scale uses Ctrl for single sided scaling. +bool GLGizmosManager::is_allow_multi_select_parts_or_objects() const +{ + return m_current == Undefined || m_current == EType::Move; +} + std::optional GLGizmosManager::get_clipping_plane() const { if (! m_common_gizmos_data || ! m_common_gizmos_data->object_clipper()) diff --git a/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp b/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp index 0914f8d958..d08b50f61d 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosManager.hpp @@ -201,6 +201,30 @@ public: IC_CANVAS_SECTION_ACTIVE_HOVER, IC_CANVAS_SECTION_ACTIVE_DARK, IC_CANVAS_SECTION_ACTIVE_DARK_HOVER, + IC_ALIGN_X_MIN, + IC_ALIGN_X_CENTER, + IC_ALIGN_X_MAX, + IC_ALIGN_Y_MIN, + IC_ALIGN_Y_CENTER, + IC_ALIGN_Y_MAX, + IC_ALIGN_Z_MIN, + IC_ALIGN_Z_CENTER, + IC_ALIGN_Z_MAX, + IC_DISTRIBUTE_X, + IC_DISTRIBUTE_Y, + IC_DISTRIBUTE_Z, + IC_ALIGN_X_MIN_DARK, + IC_ALIGN_X_CENTER_DARK, + IC_ALIGN_X_MAX_DARK, + IC_ALIGN_Y_MIN_DARK, + IC_ALIGN_Y_CENTER_DARK, + IC_ALIGN_Y_MAX_DARK, + IC_ALIGN_Z_MIN_DARK, + IC_ALIGN_Z_CENTER_DARK, + IC_ALIGN_Z_MAX_DARK, + IC_DISTRIBUTE_X_DARK, + IC_DISTRIBUTE_Y_DARK, + IC_DISTRIBUTE_Z_DARK, }; explicit GLGizmosManager(GLCanvas3D& parent); @@ -301,6 +325,7 @@ public: bool is_paint_gizmo(); bool is_allow_select_all(); + bool is_allow_multi_select_parts_or_objects() const; // Empty when the open gizmo has no object clipper. std::optional get_clipping_plane() const; bool wants_reslice_supports_on_undo() const; diff --git a/src/slic3r/GUI/Gizmos/GizmoObjectManipulation.cpp b/src/slic3r/GUI/Gizmos/GizmoObjectManipulation.cpp index be3e961e09..d9bd585a72 100644 --- a/src/slic3r/GUI/Gizmos/GizmoObjectManipulation.cpp +++ b/src/slic3r/GUI/Gizmos/GizmoObjectManipulation.cpp @@ -11,6 +11,7 @@ #include "slic3r/GUI/GUI_ObjectList.hpp" //#include "I18N.hpp" #include "GLGizmosManager.hpp" +#include "GLGizmoAlignment.hpp" #include "slic3r/GUI/GLCanvas3D.hpp" #include "slic3r/GUI/GUI_App.hpp" #include "slic3r/Utils/UndoRedo.hpp" @@ -75,7 +76,7 @@ static double get_volume_min_z(const GLVolume* volume) } GizmoObjectManipulation::GizmoObjectManipulation(GLCanvas3D& glcanvas) - : m_glcanvas(glcanvas) + : m_glcanvas(glcanvas), m_alignment_helper(glcanvas) { m_imperial_units = wxGetApp().app_config->get("use_inches") == "1"; m_new_unit_string = m_imperial_units ? L_CONTEXT("in", "inches") : L("mm"); @@ -86,6 +87,7 @@ GizmoObjectManipulation::GizmoObjectManipulation(GLCanvas3D& glcanvas) m_shortcuts_move = { {alt + _L("Left mouse button"), _L("Part selection")}, + {ctrl + _L("Left mouse button"), _L("Select multiple objects")}, {shift + _L("Left mouse button"), _L("Fixed step drag")}, {_L("Context Menu"), _L("Toggle Auto-Drop")} }; @@ -761,6 +763,12 @@ void GizmoObjectManipulation::set_init_rotation(const Geometry::Transformation & m_init_rotation = value.get_rotation(); } +void GizmoObjectManipulation::reset_align_state() +{ + m_hovered_align_type = GLGizmoAlignment::AlignType::NONE; + m_align_to_parent = !m_alignment_helper.can_align(GLGizmoAlignment::AlignType::X_MIN); +} + void GizmoObjectManipulation::do_render_move_window(ImGuiWrapper *imgui_wrapper, std::string window_name, float x, float y, float bottom_limit) { // BBS: GUI refactor: move gizmo to the right @@ -798,16 +806,26 @@ void GizmoObjectManipulation::do_render_move_window(ImGuiWrapper *imgui_wrapper, }; float space_size = imgui_wrapper->get_style_scaling() * 8; + // The align section shares the combo width, the caption column and the input columns of the position row. + const bool show_align = m_glcanvas.get_canvas_type() == GLCanvas3D::ECanvasType::CanvasView3D && + !m_glcanvas.get_selection().is_wipe_tower(); //ORCA float coord_combo_width = std::max({ imgui_wrapper->calc_text_size(_L("World")).x, imgui_wrapper->calc_text_size(_L("Object")).x, imgui_wrapper->calc_text_size(_L("Part")).x - }) + imgui_wrapper->calc_text_size("xxx"sv).x + imgui_wrapper->scaled(3.5f); + }); float label_max = std::max({ imgui_wrapper->calc_text_size(_L("Position")).x, imgui_wrapper->calc_text_size(_L("Relative")).x }); + if (show_align) { + coord_combo_width = std::max({coord_combo_width, imgui_wrapper->calc_text_size(_L("Align selected")).x, + imgui_wrapper->calc_text_size(_L("Align to object")).x, + imgui_wrapper->calc_text_size(_L("Align to plate")).x}); + label_max = std::max(label_max, imgui_wrapper->calc_text_size(_L("Align/Distribute")).x); + } + coord_combo_width += imgui_wrapper->calc_text_size("xxx"sv).x + imgui_wrapper->scaled(3.5f); float caption_max = std::max(label_max, coord_combo_width - 3 * space_size); float end_text_size = imgui_wrapper->calc_text_size(this->m_new_unit_string).x; @@ -821,6 +839,11 @@ void GizmoObjectManipulation::do_render_move_window(ImGuiWrapper *imgui_wrapper, // Rotation float unit_size = imgui_wrapper->calc_text_size(MAX_SIZE).x + space_size; + // Four align buttons sit under each axis' input, a space before the next axis, so the inputs widen to hold them. + const float align_icon_size = ImGui::GetFrameHeight() * 1.2f; + const float align_button_size = align_icon_size + 2 * ImGui::GetStyle().FramePadding.x; + if (show_align) + unit_size = std::max(unit_size, 4 * align_button_size); int index = 1; int index_unit = 1; @@ -909,6 +932,114 @@ void GizmoObjectManipulation::do_render_move_window(ImGuiWrapper *imgui_wrapper, } if (!focued_on_text) m_glcanvas.handle_sidebar_focus_event("", false); + // Align and distribute along the world axes, within the selection or to the object or plate holding it. + using AlignType = GLGizmoAlignment::AlignType; + const bool old_align_to_parent = m_align_to_parent; + AlignType hovered_align_type = AlignType::NONE; + if (show_align) { + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + // The last mode aligns to the parent. Aligning within the selection needs two items. + std::vector align_modes = {_u8L("Align selected"), + m_alignment_helper.is_parts_selection(true) ? _u8L("Align to object") : _u8L("Align to plate")}; + if (!m_alignment_helper.can_align(AlignType::X_MIN)) { + align_modes.erase(align_modes.begin()); + m_align_to_parent = true; + } + size_t align_mode_idx = m_align_to_parent ? align_modes.size() - 1 : 0; + ImGuiWrapper::push_combo_style(m_glcanvas.get_scale()); + // render_combo() takes its ImGui id from the empty label, which the coordinate combo uses too. + ImGui::PushID("align_mode"); + if (render_combo(imgui_wrapper, "", align_modes, align_mode_idx, 0, coord_combo_width)) + m_align_to_parent = align_mode_idx + 1 == align_modes.size(); + ImGui::PopID(); + ImGuiWrapper::pop_combo_style(); + + // The label in the caption column and each axis' buttons from the left edge of its input, both centered + // vertically in the row. + const float row_y = ImGui::GetCursorPosY(); + const float button_height = align_icon_size + 2 * ImGui::GetStyle().FramePadding.y; + const float row_height = std::max(ImGui::GetTextLineHeight(), button_height); + ImGui::SetCursorPosY(row_y + (row_height - ImGui::GetTextLineHeight()) * 0.5f); + imgui_wrapper->text(_L("Align/Distribute")); + + const bool dark_mode = wxGetApp().dark_mode(); + int current_axis = -1; + auto align_icon = [&](int axis, AlignType type, GLGizmosManager::MENU_ICON_NAME icon, + GLGizmosManager::MENU_ICON_NAME dark_icon, const wxString &tip) { + const bool distribute = type >= AlignType::DISTRIBUTE_X; + const bool enabled = m_align_to_parent ? m_alignment_helper.can_align(type, true) : + distribute ? m_alignment_helper.can_distribute(type) : + m_alignment_helper.can_align(type); + if (axis != current_axis) { + ImGui::SameLine(caption_max + (axis + 1) * space_size + axis * unit_size); + if (current_axis < 0) + ImGui::SetCursorPosY(row_y + (row_height - button_height) * 0.5f); + current_axis = axis; + } else + ImGui::SameLine(0, 0); + + if (!enabled) { + ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 0.3f); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::GetStyle().Colors[ImGuiCol_Button]); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImGui::GetStyle().Colors[ImGuiCol_Button]); + } + ImTextureID texture = m_glcanvas.get_gizmos_manager().get_icon_texture_id(dark_mode ? dark_icon : icon); + if (ImGui::ImageButton3(texture, texture, ImVec2(align_icon_size, align_icon_size)) && enabled) { + if (distribute) + m_alignment_helper.distribute_objects(type); + else + m_alignment_helper.align_objects(type, m_align_to_parent); + } + if (!enabled) { + ImGui::PopStyleColor(2); + ImGui::PopStyleVar(); + } + + if (ImGui::IsItemHovered()) { + wxString text = tip; + if (enabled) + hovered_align_type = type; + else { + // Too few items is the reason when there are items to move (only distribute needs three); + // otherwise the helper names it. + std::string why_not; + if (m_alignment_helper.get_items_count(type, m_align_to_parent, &why_not) > 0) + why_not = _u8L("Select at least 3 parts or objects"); + if (!why_not.empty()) + text += "\n" + from_u8(why_not); + } + imgui_wrapper->tooltip(text, imgui_wrapper->calc_text_size(text).x + 3 * space_size); + } + }; + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); + align_icon(0, AlignType::X_MIN, GLGizmosManager::IC_ALIGN_X_MIN, GLGizmosManager::IC_ALIGN_X_MIN_DARK, _L("Align left") + " (-X)"); + align_icon(0, AlignType::CENTER_X, GLGizmosManager::IC_ALIGN_X_CENTER, GLGizmosManager::IC_ALIGN_X_CENTER_DARK, _L("Align left-right center") + " (X)"); + align_icon(0, AlignType::X_MAX, GLGizmosManager::IC_ALIGN_X_MAX, GLGizmosManager::IC_ALIGN_X_MAX_DARK, _L("Align right") + " (+X)"); + if (!m_align_to_parent) + align_icon(0, AlignType::DISTRIBUTE_X, GLGizmosManager::IC_DISTRIBUTE_X, GLGizmosManager::IC_DISTRIBUTE_X_DARK, _L("Distribute left-right") + " (X)"); + align_icon(1, AlignType::Y_MIN, GLGizmosManager::IC_ALIGN_Y_MIN, GLGizmosManager::IC_ALIGN_Y_MIN_DARK, _L("Align front") + " (-Y)"); + align_icon(1, AlignType::CENTER_Y, GLGizmosManager::IC_ALIGN_Y_CENTER, GLGizmosManager::IC_ALIGN_Y_CENTER_DARK, _L("Align front-back center") + " (Y)"); + align_icon(1, AlignType::Y_MAX, GLGizmosManager::IC_ALIGN_Y_MAX, GLGizmosManager::IC_ALIGN_Y_MAX_DARK, _L("Align back") + " (+Y)"); + if (!m_align_to_parent) + align_icon(1, AlignType::DISTRIBUTE_Y, GLGizmosManager::IC_DISTRIBUTE_Y, GLGizmosManager::IC_DISTRIBUTE_Y_DARK, _L("Distribute front-back") + " (Y)"); + align_icon(2, AlignType::Z_MIN, GLGizmosManager::IC_ALIGN_Z_MIN, GLGizmosManager::IC_ALIGN_Z_MIN_DARK, _L("Align bottom") + " (-Z)"); + align_icon(2, AlignType::CENTER_Z, GLGizmosManager::IC_ALIGN_Z_CENTER, GLGizmosManager::IC_ALIGN_Z_CENTER_DARK, _L("Align top-bottom center") + " (Z)"); + align_icon(2, AlignType::Z_MAX, GLGizmosManager::IC_ALIGN_Z_MAX, GLGizmosManager::IC_ALIGN_Z_MAX_DARK, _L("Align top") + " (+Z)"); + if (!m_align_to_parent) + align_icon(2, AlignType::DISTRIBUTE_Z, GLGizmosManager::IC_DISTRIBUTE_Z, GLGizmosManager::IC_DISTRIBUTE_Z_DARK, _L("Distribute top-bottom") + " (Z)"); + ImGui::PopStyleVar(); + ImGui::SetCursorPosY(row_y + row_height + ImGui::GetStyle().ItemSpacing.y); + } + // The Move gizmo draws the preview with the scene, which an ImGui-only frame does not redraw. + if (hovered_align_type != m_hovered_align_type || m_align_to_parent != old_align_to_parent) { + m_hovered_align_type = hovered_align_type; + m_glcanvas.set_as_dirty(); + m_glcanvas.request_extra_frame(); + } + ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); diff --git a/src/slic3r/GUI/Gizmos/GizmoObjectManipulation.hpp b/src/slic3r/GUI/Gizmos/GizmoObjectManipulation.hpp index e43a129734..400f7b7405 100644 --- a/src/slic3r/GUI/Gizmos/GizmoObjectManipulation.hpp +++ b/src/slic3r/GUI/Gizmos/GizmoObjectManipulation.hpp @@ -13,6 +13,7 @@ #include #include "slic3r/GUI/GUI_Geometry.hpp" +#include "GLGizmoAlignment.hpp" //#include "slic3r/GUI/GLCanvas3D.hpp" @@ -151,6 +152,13 @@ public: void set_init_rotation(const Geometry::Transformation &value); + // Align/distribute state of the Move panel, which the Move gizmo previews. + const GLGizmoAlignment &get_alignment_helper() const { return m_alignment_helper; } + GLGizmoAlignment::AlignType get_hovered_align_type() const { return m_hovered_align_type; } + bool get_align_to_parent() const { return m_align_to_parent; } + // When the Move gizmo opens or closes: no preview, and "Align selected" whenever the selection allows it. + void reset_align_state(); + private: void reset_settings_value(); void update_settings_value(const Selection& selection); @@ -175,6 +183,12 @@ private: GLCanvas3D& m_glcanvas; unsigned int m_last_active_item { 0 }; + GLGizmoAlignment m_alignment_helper; + // Operation of the enabled align icon under the mouse. + GLGizmoAlignment::AlignType m_hovered_align_type{GLGizmoAlignment::AlignType::NONE}; + // Align to the object or plate holding the selection rather than within the selection. + bool m_align_to_parent{false}; + // Contains all shortcuts in the format of {shortcut, description}, e.g. {alt + _L("Left mouse button"), _L("Part_selection")} std::vector> m_shortcuts_move; // Contains all shortcuts in the format of {shortcut, description}, e.g. {alt + _L("Left mouse button"), _L("Part_selection")}