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..32012712a9 --- /dev/null +++ b/resources/images/align_x_center.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/images/align_x_center_dark.svg b/resources/images/align_x_center_dark.svg new file mode 100644 index 0000000000..2ed90436b1 --- /dev/null +++ b/resources/images/align_x_center_dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/images/align_x_max.svg b/resources/images/align_x_max.svg new file mode 100644 index 0000000000..3668d32792 --- /dev/null +++ b/resources/images/align_x_max.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/images/align_x_max_dark.svg b/resources/images/align_x_max_dark.svg new file mode 100644 index 0000000000..81e2e236b5 --- /dev/null +++ b/resources/images/align_x_max_dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/images/align_x_min.svg b/resources/images/align_x_min.svg new file mode 100644 index 0000000000..b35da87f31 --- /dev/null +++ b/resources/images/align_x_min.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/images/align_x_min_dark.svg b/resources/images/align_x_min_dark.svg new file mode 100644 index 0000000000..9e32867fad --- /dev/null +++ b/resources/images/align_x_min_dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/images/align_y_center.svg b/resources/images/align_y_center.svg new file mode 100644 index 0000000000..5bc5e0a2e4 --- /dev/null +++ b/resources/images/align_y_center.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/images/align_y_center_dark.svg b/resources/images/align_y_center_dark.svg new file mode 100644 index 0000000000..cea517a3a3 --- /dev/null +++ b/resources/images/align_y_center_dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/images/align_y_max.svg b/resources/images/align_y_max.svg new file mode 100644 index 0000000000..9a4bf46b0a --- /dev/null +++ b/resources/images/align_y_max.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/images/align_y_max_dark.svg b/resources/images/align_y_max_dark.svg new file mode 100644 index 0000000000..cf91f9dc26 --- /dev/null +++ b/resources/images/align_y_max_dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/images/align_y_min.svg b/resources/images/align_y_min.svg new file mode 100644 index 0000000000..3e6bea962e --- /dev/null +++ b/resources/images/align_y_min.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/images/align_y_min_dark.svg b/resources/images/align_y_min_dark.svg new file mode 100644 index 0000000000..800a202aef --- /dev/null +++ b/resources/images/align_y_min_dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/images/align_z_center.svg b/resources/images/align_z_center.svg new file mode 100644 index 0000000000..5436259956 --- /dev/null +++ b/resources/images/align_z_center.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/images/align_z_center_dark.svg b/resources/images/align_z_center_dark.svg new file mode 100644 index 0000000000..9bcb13d5e0 --- /dev/null +++ b/resources/images/align_z_center_dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/images/align_z_max.svg b/resources/images/align_z_max.svg new file mode 100644 index 0000000000..8446d3a8ac --- /dev/null +++ b/resources/images/align_z_max.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/images/align_z_max_dark.svg b/resources/images/align_z_max_dark.svg new file mode 100644 index 0000000000..518c48f648 --- /dev/null +++ b/resources/images/align_z_max_dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/images/align_z_min.svg b/resources/images/align_z_min.svg new file mode 100644 index 0000000000..1988fb1c27 --- /dev/null +++ b/resources/images/align_z_min.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/images/align_z_min_dark.svg b/resources/images/align_z_min_dark.svg new file mode 100644 index 0000000000..8bfdcc0027 --- /dev/null +++ b/resources/images/align_z_min_dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/images/distribute_x.svg b/resources/images/distribute_x.svg new file mode 100644 index 0000000000..20f505f61f --- /dev/null +++ b/resources/images/distribute_x.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/images/distribute_x_dark.svg b/resources/images/distribute_x_dark.svg new file mode 100644 index 0000000000..a87cf75a05 --- /dev/null +++ b/resources/images/distribute_x_dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/images/distribute_y.svg b/resources/images/distribute_y.svg new file mode 100644 index 0000000000..5ddd1742d0 --- /dev/null +++ b/resources/images/distribute_y.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/images/distribute_y_dark.svg b/resources/images/distribute_y_dark.svg new file mode 100644 index 0000000000..ac72dda332 --- /dev/null +++ b/resources/images/distribute_y_dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/images/distribute_z.svg b/resources/images/distribute_z.svg new file mode 100644 index 0000000000..ee29156e21 --- /dev/null +++ b/resources/images/distribute_z.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/images/distribute_z_dark.svg b/resources/images/distribute_z_dark.svg new file mode 100644 index 0000000000..6ba375f364 --- /dev/null +++ b/resources/images/distribute_z_dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 3ac4fc2f78..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; @@ -5588,13 +5599,14 @@ void GLCanvas3D::do_move(const std::string& snapshot_type, bool force_volume_mov // parts moved. Notifying a plate about an instance that stayed put invalidates its slice // result, and notifying instance 0 alone left a moved copy unregistered on its new plate. { + const bool parts_moved = force_volume_move || selection_mode == Selection::Volume; std::set> notified; for (unsigned int i : m_selection.get_volume_idxs()) { const GLVolume* v = m_volumes.volumes[i]; const int object_idx = v->object_idx(); if (object_idx < 0 || object_idx >= static_cast(m_model->objects.size())) continue; - const std::pair key(object_idx, selection_mode == Selection::Volume ? -1 : v->instance_idx()); + const std::pair key(object_idx, parts_moved ? -1 : v->instance_idx()); if (notified.insert(key).second) m_selection.notify_instance_update(key.first, key.second); } @@ -7962,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/GUI_Factories.cpp b/src/slic3r/GUI/GUI_Factories.cpp index ca487ec3a5..c85ff2db60 100644 --- a/src/slic3r/GUI/GUI_Factories.cpp +++ b/src/slic3r/GUI/GUI_Factories.cpp @@ -2334,243 +2334,60 @@ void MenuFactory::append_menu_item_center(wxMenu* menu) void MenuFactory::append_menu_item_align_distribute(wxMenu *menu) { + using AlignType = GLGizmoAlignment::AlignType; + auto can = [](AlignType type) { + if (plater()->canvas3D()->get_canvas_type() != GLCanvas3D::ECanvasType::CanvasView3D) + return false; + GLGizmoAlignment alignment_helper(*plater()->get_view3D_canvas3D()); + return type >= AlignType::DISTRIBUTE_X ? alignment_helper.can_distribute(type) : alignment_helper.can_align(type); + }; + auto run = [](AlignType type) { + GLGizmoAlignment alignment_helper(*plater()->get_view3D_canvas3D()); + if (type >= AlignType::DISTRIBUTE_X) + alignment_helper.distribute_objects(type); + else + alignment_helper.align_objects(type); + }; + + struct AlignMenuItem + { + wxString label; + AlignType type; + std::string icon; + }; + const AlignMenuItem items[] = { + // icons shared with caanvas. BitmapCache converts colors dark to light. so dark variations used for proper colors + {_L("Distribute left-right") + " (X)" , AlignType::DISTRIBUTE_X, "distribute_x_dark"}, + {_L("Distribute front-back") + " (Y)" , AlignType::DISTRIBUTE_Y, "distribute_y_dark"}, + {_L("Distribute top-bottom") + " (Z)" , AlignType::DISTRIBUTE_Z, "distribute_z_dark"}, + {wxString(), AlignType::NONE, ""}, + {_L("Align left") + " (-X)", AlignType::X_MIN, "align_x_min_dark"}, + {_L("Align left-right center") + " (X)" , AlignType::CENTER_X, "align_x_center_dark"}, + {_L("Align right") + " (+X)", AlignType::X_MAX, "align_x_max_dark"}, + {wxString(), AlignType::NONE, ""}, + {_L("Align front") + " (-Y)", AlignType::Y_MIN, "align_y_min_dark"}, + {_L("Align front-back center") + " (Y)" , AlignType::CENTER_Y, "align_y_center_dark"}, + {_L("Align back") + " (+Y)", AlignType::Y_MAX, "align_y_max_dark"}, + {wxString(), AlignType::NONE, ""}, + {_L("Align bottom") + " (-Z)", AlignType::Z_MIN, "align_z_min_dark"}, + {_L("Align top-bottom center") + " (Z)" , AlignType::CENTER_Z, "align_z_center_dark"}, + {_L("Align top") + " (+Z)", AlignType::Z_MAX, "align_z_max_dark"}, + }; + wxMenu* align_distribute_menu = new wxMenu(); - if (!align_distribute_menu) - return; - - append_menu_item( - align_distribute_menu, wxID_ANY, _L("Distribute left-right") + " (X)", "", - [](wxCommandEvent &) { - auto canvas3d = plater()->get_view3D_canvas3D(); - canvas3d->get_gizmos_manager().check_object_located_outside_plate(true); - plater()->distribute_selection_x(); - }, - "", nullptr, - []() { - if (plater()->canvas3D()->get_canvas_type() != GLCanvas3D::ECanvasType::CanvasView3D) - return false; - auto canvas3d = plater()->get_view3D_canvas3D(); - GLGizmoAlignment alignment_helper(*canvas3d); - return alignment_helper.can_distribute(GLGizmoAlignment::AlignType::DISTRIBUTE_X); - }, - m_parent); - - append_menu_item( - align_distribute_menu, wxID_ANY, _L("Distribute front-back") + " (Y)", "", - [](wxCommandEvent &) { - auto canvas3d = plater()->get_view3D_canvas3D(); - canvas3d->get_gizmos_manager().check_object_located_outside_plate(true); - plater()->distribute_selection_y(); - }, - "", nullptr, - []() { - if (plater()->canvas3D()->get_canvas_type() != GLCanvas3D::ECanvasType::CanvasView3D) - return false; - auto canvas3d = plater()->get_view3D_canvas3D(); - GLGizmoAlignment alignment_helper(*canvas3d); - return alignment_helper.can_distribute(GLGizmoAlignment::AlignType::DISTRIBUTE_Y); - }, - m_parent); - - append_menu_item( - align_distribute_menu, wxID_ANY, _L("Distribute top-bottom") + " (Z)", "", - [](wxCommandEvent &) { - auto canvas3d = plater()->get_view3D_canvas3D(); - canvas3d->get_gizmos_manager().check_object_located_outside_plate(true); - plater()->distribute_selection_z(); - }, - "", nullptr, - []() { - if (plater()->canvas3D()->get_canvas_type() != GLCanvas3D::ECanvasType::CanvasView3D) - return false; - auto canvas3d = plater()->get_view3D_canvas3D(); - GLGizmoAlignment alignment_helper(*canvas3d); - return alignment_helper.can_distribute(GLGizmoAlignment::AlignType::DISTRIBUTE_Z); - }, - m_parent); - - align_distribute_menu->AppendSeparator(); - - append_menu_item( - align_distribute_menu, wxID_ANY, _L("Align left") + " (-X)", "", - [](wxCommandEvent &) { - auto canvas3d = plater()->get_view3D_canvas3D(); - canvas3d->get_gizmos_manager().check_object_located_outside_plate(true); - plater()->align_selection_x_min(); - }, - "", nullptr, - []() { - if (plater()->canvas3D()->get_canvas_type() != GLCanvas3D::ECanvasType::CanvasView3D) - return false; - auto canvas3d = plater()->get_view3D_canvas3D(); - GLGizmoAlignment alignment_helper(*canvas3d); - return alignment_helper.can_align(GLGizmoAlignment::AlignType::X_MIN); - }, - m_parent); - - append_menu_item( - align_distribute_menu, wxID_ANY, _L("Align left-right center") + " (X)", "", - [](wxCommandEvent &) { - auto canvas3d = plater()->get_view3D_canvas3D(); - canvas3d->get_gizmos_manager().check_object_located_outside_plate(true); - plater()->align_selection_x_center(); - }, - "", nullptr, - []() { - if (plater()->canvas3D()->get_canvas_type() != GLCanvas3D::ECanvasType::CanvasView3D) - return false; - auto canvas3d = plater()->get_view3D_canvas3D(); - GLGizmoAlignment alignment_helper(*canvas3d); - return alignment_helper.can_align(GLGizmoAlignment::AlignType::CENTER_X); - }, - m_parent); - - append_menu_item( - align_distribute_menu, wxID_ANY, _L("Align right") + " (+X)", "", - [](wxCommandEvent &) { - auto canvas3d = plater()->get_view3D_canvas3D(); - canvas3d->get_gizmos_manager().check_object_located_outside_plate(true); - plater()->align_selection_x_max(); - }, - "", nullptr, - []() { - if (plater()->canvas3D()->get_canvas_type() != GLCanvas3D::ECanvasType::CanvasView3D) - return false; - auto canvas3d = plater()->get_view3D_canvas3D(); - GLGizmoAlignment alignment_helper(*canvas3d); - return alignment_helper.can_align(GLGizmoAlignment::AlignType::X_MAX); - }, - m_parent); - - align_distribute_menu->AppendSeparator(); - - append_menu_item( - align_distribute_menu, wxID_ANY, _L("Align front") + " (-Y)", "", - [](wxCommandEvent &) { - auto canvas3d = plater()->get_view3D_canvas3D(); - canvas3d->get_gizmos_manager().check_object_located_outside_plate(true); - plater()->align_selection_y_min(); - }, - "", nullptr, - []() { - if (plater()->canvas3D()->get_canvas_type() != GLCanvas3D::ECanvasType::CanvasView3D) - return false; - auto canvas3d = plater()->get_view3D_canvas3D(); - GLGizmoAlignment alignment_helper(*canvas3d); - return alignment_helper.can_align(GLGizmoAlignment::AlignType::Y_MIN); - }, - m_parent); - - append_menu_item( - align_distribute_menu, wxID_ANY, _L("Align front-back center") + " (Y)", "", - [](wxCommandEvent &) { - auto canvas3d = plater()->get_view3D_canvas3D(); - canvas3d->get_gizmos_manager().check_object_located_outside_plate(true); - plater()->align_selection_y_center(); - }, - "", nullptr, - []() { - if (plater()->canvas3D()->get_canvas_type() != GLCanvas3D::ECanvasType::CanvasView3D) - return false; - auto canvas3d = plater()->get_view3D_canvas3D(); - GLGizmoAlignment alignment_helper(*canvas3d); - return alignment_helper.can_align(GLGizmoAlignment::AlignType::CENTER_Y); - }, - m_parent); - - append_menu_item( - align_distribute_menu, wxID_ANY, _L("Align back") + " (+Y)", "", - [](wxCommandEvent &) { - auto canvas3d = plater()->get_view3D_canvas3D(); - canvas3d->get_gizmos_manager().check_object_located_outside_plate(true); - plater()->align_selection_y_max(); - }, - "", nullptr, - []() { - if (plater()->canvas3D()->get_canvas_type() != GLCanvas3D::ECanvasType::CanvasView3D) - return false; - auto canvas3d = plater()->get_view3D_canvas3D(); - GLGizmoAlignment alignment_helper(*canvas3d); - return alignment_helper.can_align(GLGizmoAlignment::AlignType::Y_MAX); - }, - m_parent); - - align_distribute_menu->AppendSeparator(); - - append_menu_item( - align_distribute_menu, wxID_ANY, _L("Align bottom") + " (-Z)", "", - [](wxCommandEvent &) { - auto canvas3d = plater()->get_view3D_canvas3D(); - canvas3d->get_gizmos_manager().check_object_located_outside_plate(true); - plater()->align_selection_z_min(); - }, - "", nullptr, - []() { - if (plater()->canvas3D()->get_canvas_type() != GLCanvas3D::ECanvasType::CanvasView3D) - return false; - auto canvas3d = plater()->get_view3D_canvas3D(); - GLGizmoAlignment alignment_helper(*canvas3d); - return alignment_helper.can_align(GLGizmoAlignment::AlignType::Z_MIN); - }, - m_parent); - - append_menu_item( - align_distribute_menu, wxID_ANY, _L("Align top-bottom center") + " (Z)", "", - [](wxCommandEvent &) { - auto canvas3d = plater()->get_view3D_canvas3D(); - canvas3d->get_gizmos_manager().check_object_located_outside_plate(true); - plater()->align_selection_z_center(); - }, - "", nullptr, - []() { - if (plater()->canvas3D()->get_canvas_type() != GLCanvas3D::ECanvasType::CanvasView3D) - return false; - auto canvas3d = plater()->get_view3D_canvas3D(); - GLGizmoAlignment alignment_helper(*canvas3d); - return alignment_helper.can_align(GLGizmoAlignment::AlignType::CENTER_Z); - }, - m_parent); - - append_menu_item( - align_distribute_menu, wxID_ANY, _L("Align top") + " (+Z)", "", - [](wxCommandEvent &) { - auto canvas3d = plater()->get_view3D_canvas3D(); - canvas3d->get_gizmos_manager().check_object_located_outside_plate(true); - plater()->align_selection_z_max(); - }, - "", nullptr, - []() { - if (plater()->canvas3D()->get_canvas_type() != GLCanvas3D::ECanvasType::CanvasView3D) - return false; - auto canvas3d = plater()->get_view3D_canvas3D(); - GLGizmoAlignment alignment_helper(*canvas3d); - return alignment_helper.can_align(GLGizmoAlignment::AlignType::Z_MAX); - }, - m_parent); + for (const AlignMenuItem &item : items) { + const AlignType type = item.type; + if (type == AlignType::NONE) + align_distribute_menu->AppendSeparator(); + else + append_menu_item( + align_distribute_menu, wxID_ANY, item.label, "", [run, type](wxCommandEvent &) { run(type); }, item.icon, nullptr, + [can, type]() { return can(type); }, m_parent); + } + // Anything that can be distributed can also be aligned, and aligning in X has no extra condition. append_submenu(menu, align_distribute_menu, wxID_ANY, _L("Align/Distribute"), _L("Align and distribute objects"), "", - []() { - if (plater()->canvas3D()->get_canvas_type() != GLCanvas3D::ECanvasType::CanvasView3D) - return false; - auto canvas3d = plater()->get_view3D_canvas3D(); - GLGizmoAlignment alignment_helper(*canvas3d); - - bool has_any_align = alignment_helper.can_align(GLGizmoAlignment::AlignType::CENTER_X) || - alignment_helper.can_align(GLGizmoAlignment::AlignType::CENTER_Y) || - alignment_helper.can_align(GLGizmoAlignment::AlignType::CENTER_Z) || - alignment_helper.can_align(GLGizmoAlignment::AlignType::X_MAX) || - alignment_helper.can_align(GLGizmoAlignment::AlignType::X_MIN) || - alignment_helper.can_align(GLGizmoAlignment::AlignType::Y_MAX) || - alignment_helper.can_align(GLGizmoAlignment::AlignType::Y_MIN) || - alignment_helper.can_align(GLGizmoAlignment::AlignType::Z_MAX) || - alignment_helper.can_align(GLGizmoAlignment::AlignType::Z_MIN); - - bool has_any_distribute = alignment_helper.can_distribute(GLGizmoAlignment::AlignType::DISTRIBUTE_X) || - alignment_helper.can_distribute(GLGizmoAlignment::AlignType::DISTRIBUTE_Y) || - alignment_helper.can_distribute(GLGizmoAlignment::AlignType::DISTRIBUTE_Z); - - return has_any_align || has_any_distribute; - }, m_parent); + [can]() { return can(AlignType::X_MIN); }, m_parent); } void MenuFactory::append_menu_item_drop(wxMenu* menu) diff --git a/src/slic3r/GUI/Gizmos/GLGizmoAlignment.cpp b/src/slic3r/GUI/Gizmos/GLGizmoAlignment.cpp index 7e97d64940..96b85215a1 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoAlignment.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoAlignment.cpp @@ -1,8 +1,20 @@ #include "GLGizmoAlignment.hpp" +#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" +#include "libslic3r/BoundingBox.hpp" #include "libslic3r/Model.hpp" +#include "libslic3r/Point.hpp" + +#include +#include +#include +#include +#include namespace Slic3r { namespace GUI { @@ -13,7 +25,7 @@ GLGizmoAlignment::GLGizmoAlignment(GLCanvas3D& canvas) : m_canvas(canvas) bool GLGizmoAlignment::align_objects(AlignType type, bool align_parent) { - if (!validate_selection_for_align()) { + if (!can_align(type, align_parent)) { return false; } @@ -92,32 +104,29 @@ bool GLGizmoAlignment::align_to_x_min(bool align_parent) bool GLGizmoAlignment::align_to_center(AlignType type,bool align_parent) { - const Selection& selection = get_selection(); std::string type_str = type == AlignType::CENTER_X ? "X" : (type == AlignType::CENTER_Y ? "Y" : "Z"); std::string operation_name = type == AlignType::CENTER_X ? "X Center" : (type == AlignType::CENTER_Y ? "Y Center" : "Z Center"); double selection_center = 0.f; if (align_parent) { - selection_center = get_current_coord(operation_name, m_parent_box); + BoundingBoxf3 parent_box = get_parent_box(); + selection_center = get_current_coord(operation_name, parent_box); // Handle multiple volume selection - if (is_part_align_parent()) { + if (is_parts_selection(true)) { if (!take_snapshot("Align to parent center " + type_str)) { return false; } get_selection().setup_cache(); - auto selection_bbox = selection.get_bounding_box(); - double current_coord = get_current_coord(operation_name, selection_bbox); - Vec3d displacement = generate_displacement(operation_name, selection_center - current_coord); - // Move each selected volume to align to the center of their collective bounding box - for (unsigned int idx : selection.get_volume_idxs()) { - const GLVolume *volume = selection.get_volume(idx); - if (volume) { - get_selection().translate(volume->object_idx(), volume->instance_idx(), volume->volume_idx(), displacement); - } + BoundingBoxf3 selection_bbox = get_items_box(); + double current_coord = get_current_coord(operation_name, selection_bbox); + Vec3d displacement = generate_displacement(operation_name, selection_center - current_coord); + // Move the selected volumes together so that their collective bounding box is centered in the parent + for (const GLVolume *volume : get_selected_part_volumes()) { + get_selection().translate(volume->object_idx(), volume->instance_idx(), volume->volume_idx(), displacement, false); } - finish_operation("Align to parent center " + type_str); + finish_operation(true); return true; } BoundingBoxf3 big_bb; @@ -125,7 +134,6 @@ bool GLGizmoAlignment::align_to_center(AlignType type,bool align_parent) if (objects.empty()) return false; // Handle multiple objects or parts selection - BoundingBoxf3 selection_bbox = selection.get_bounding_box(); if (!take_snapshot("Align to parent center " + type_str)) { return false; } @@ -138,60 +146,52 @@ bool GLGizmoAlignment::align_to_center(AlignType type,bool align_parent) apply_transformation(obj.object_idx, obj.instance_idx, displacement); } - finish_operation("Align to parent center " + type_str); + finish_operation(); return true; } - // Handle single object selection// Handle multiple volume selection - if (selection.is_single_full_object() || - selection.is_multiple_volume() || selection.is_multiple_modifier() || selection.is_single_volume() || selection.is_single_modifier()) { + // Handle the parts of a single instance + if (is_parts_selection(false)) { if (!take_snapshot("Align Parts Center " + type_str)){ return false; } get_selection().setup_cache(); - BoundingBoxf3 selection_bbox = selection.get_bounding_box(); + BoundingBoxf3 selection_bbox = get_items_box(); selection_center = get_current_coord(operation_name, selection_bbox); - // Move each selected volume to align to the center of their collective bounding box - for (unsigned int idx : selection.get_volume_idxs()) { - const GLVolume* volume = selection.get_volume(idx); - if (volume) { - auto temp_box = volume->transformed_convex_hull_bounding_box(); - double current = get_current_coord(operation_name, temp_box); - double offset = selection_center - current; - Vec3d displacement = generate_displacement(operation_name, offset); - get_selection().translate(volume->object_idx(), volume->instance_idx(), volume->volume_idx(), displacement); - } + // Move each part to align to the center of their collective bounding box + for (const GLVolume *volume : get_selected_part_volumes()) { + auto temp_box = volume->transformed_convex_hull_bounding_box(); + double current = get_current_coord(operation_name, temp_box); + double offset = selection_center - current; + Vec3d displacement = generate_displacement(operation_name, offset); + get_selection().translate(volume->object_idx(), volume->instance_idx(), volume->volume_idx(), displacement, false); } - finish_operation("Align Parts Center" + type_str, true); + finish_operation(true); return true; } BoundingBoxf3 big_bb; auto objects = get_selected_objects_info(big_bb); if (objects.empty()) return false; - // Handle multiple objects or parts selection - if (selection.is_multiple_full_object()) { - BoundingBoxf3 selection_bbox = selection.get_bounding_box(); - double selection_center = get_current_coord(operation_name, selection_bbox); + // Handle multiple instances + selection_center = get_current_coord(operation_name, big_bb); - if (!take_snapshot("Align Objects Center" + type_str)) { - return false; - } - - get_selection().setup_cache(); - // Multiple objects: use selection bounding box for alignment - for (const auto& obj : objects) { - BoundingBoxf3 temp_bbox = obj.bbox; - double current_coord = get_current_coord(operation_name, temp_bbox); - Vec3d displacement = generate_displacement(operation_name, selection_center - current_coord); - apply_transformation(obj.object_idx, obj.instance_idx, displacement); - } - finish_operation("Align Objects Center" + type_str); - return true; + if (!take_snapshot("Align Objects Center" + type_str)) { + return false; } - return false; + + get_selection().setup_cache(); + // Multiple objects: use selection bounding box for alignment + for (const auto& obj : objects) { + BoundingBoxf3 temp_bbox = obj.bbox; + double current_coord = get_current_coord(operation_name, temp_bbox); + Vec3d displacement = generate_displacement(operation_name, selection_center - current_coord); + apply_transformation(obj.object_idx, obj.instance_idx, displacement); + } + finish_operation(); + return true; } bool GLGizmoAlignment::align_to_z_max(bool align_parent) @@ -244,37 +244,31 @@ bool GLGizmoAlignment::distribute_z() template bool GLGizmoAlignment::align_objects_generic(GetCoordFunc get_coord, SetCoordFunc set_coord, const std::string &operation_name ,bool align_parent) { - const Selection& selection = get_selection(); if (align_parent) { // Find reference coordinate - double reference_coord = get_current_coord(operation_name, m_parent_box); + BoundingBoxf3 parent_box = get_parent_box(); + double reference_coord = get_current_coord(operation_name, parent_box); // Handle parts selection differently - if (is_part_align_parent()) { + if (is_parts_selection(true)) { if (!take_snapshot("align to parent node " + operation_name)) { return false; } get_selection().setup_cache(); - // Get all selected volumes - std::vector volumes; - for (unsigned int idx : selection.get_volume_idxs()) { - const GLVolume *volume = selection.get_volume(idx); - if (volume) { volumes.push_back(volume); } - } - + const std::vector volumes = get_selected_part_volumes(); if (volumes.empty()) return false; - BoundingBoxf3 selection_bbox = selection.get_bounding_box(); + BoundingBoxf3 selection_bbox = get_items_box(); double current_coord = get_current_coord(operation_name, selection_bbox); Vec3d displacement = generate_displacement(operation_name, reference_coord - current_coord); for (const GLVolume *volume : volumes) { if (displacement.norm() > 1e-6) { - get_selection().translate(volume->object_idx(), volume->instance_idx(), volume->volume_idx(), displacement); + get_selection().translate(volume->object_idx(), volume->instance_idx(), volume->volume_idx(), displacement, false); } } - finish_operation("align to parent node " + operation_name); + finish_operation(true); return true; } @@ -296,26 +290,18 @@ bool GLGizmoAlignment::align_objects_generic(GetCoordFunc get_coord, SetCoordFun } } - finish_operation("align to parent node " + operation_name); + finish_operation(); return true; } // Handle parts selection differently - if (selection.is_single_full_object() || selection.is_multiple_volume() || selection.is_multiple_modifier()) { + if (is_parts_selection(false)) { if (!take_snapshot(operation_name)) { return false; } get_selection().setup_cache(); - // Get all selected volumes - std::vector volumes; - for (unsigned int idx : selection.get_volume_idxs()) { - const GLVolume* volume = selection.get_volume(idx); - if (volume) { - volumes.push_back(volume); - } - } - + const std::vector volumes = get_selected_part_volumes(); if (volumes.empty()) return false; // Find reference coordinate @@ -381,11 +367,11 @@ bool GLGizmoAlignment::align_objects_generic(GetCoordFunc get_coord, SetCoordFun } if (displacement.norm() > 1e-6) { - get_selection().translate(volume->object_idx(), volume->instance_idx(), volume->volume_idx(), displacement); + get_selection().translate(volume->object_idx(), volume->instance_idx(), volume->volume_idx(), displacement, false); } } - finish_operation(operation_name,true); + finish_operation(true); return true; } @@ -420,22 +406,15 @@ bool GLGizmoAlignment::align_objects_generic(GetCoordFunc get_coord, SetCoordFun } } - finish_operation(operation_name); + finish_operation(); return true; } template bool GLGizmoAlignment::distribute_objects_generic(GetCoordFunc get_coord, int axis, const std::string& operation_name) { - const Selection& selection = get_selection(); - - if (selection.is_single_full_object() || selection.is_multiple_volume() || selection.is_multiple_modifier()) { - std::vector volumes; - for (unsigned int idx : selection.get_volume_idxs()) { - const GLVolume* v = selection.get_volume(idx); - if (v) volumes.push_back(v); - } - + if (is_parts_selection(false)) { + const std::vector volumes = get_selected_part_volumes(); if (volumes.size() < 3) return false; if (!take_snapshot(operation_name)) { return false; @@ -454,7 +433,7 @@ bool GLGizmoAlignment::distribute_objects_generic(GetCoordFunc get_coord, int ax std::sort(entries.begin(), entries.end(), [](const VolEntry& a, const VolEntry& b){ return a.coord < b.coord; }); - BoundingBoxf3 selection_bbox = selection.get_bounding_box(); + BoundingBoxf3 selection_bbox = get_items_box(); BoundingBoxf3 left_bbox = entries.front().v->transformed_convex_hull_bounding_box(); BoundingBoxf3 right_bbox = entries.back().v->transformed_convex_hull_bounding_box(); auto axis_extent = [&](const BoundingBoxf3 &bb) { return (axis == 0 ? (bb.max.x() - bb.min.x()) : axis == 1 ? (bb.max.y() - bb.min.y()) : (bb.max.z() - bb.min.z())); }; @@ -475,11 +454,11 @@ bool GLGizmoAlignment::distribute_objects_generic(GetCoordFunc get_coord, int ax Vec3d displacement = Vec3d::Zero(); displacement[axis] = offset; const GLVolume *v = entries[i].v; - get_selection().translate(v->object_idx(), v->instance_idx(), v->volume_idx(), displacement); + get_selection().translate(v->object_idx(), v->instance_idx(), v->volume_idx(), displacement, false); } } - finish_operation(operation_name,true); + finish_operation(true); return true; } BoundingBoxf3 big_bb; @@ -513,52 +492,19 @@ bool GLGizmoAlignment::distribute_objects_generic(GetCoordFunc get_coord, int ax } } - finish_operation(operation_name); + finish_operation(); return true; } -bool GLGizmoAlignment::can_align(AlignType type) const +bool GLGizmoAlignment::can_align(AlignType type, bool align_parent) const { - const Selection& selection = get_selection(); - - bool is_single_object = selection.is_single_full_object(); - bool is_multiple_objects = selection.is_multiple_full_object(); - bool is_single_part = selection.is_single_volume() || selection.is_single_modifier(); - bool is_multiple_parts = selection.is_multiple_volume() || selection.is_multiple_modifier(); - - - return validate_selection_for_align(); + return get_items_count(type, align_parent) >= (align_parent ? 1 : 2); } bool GLGizmoAlignment::can_distribute(AlignType type) const { - const Selection& selection = get_selection(); - if (selection.is_single_full_object()) { - size_t count = selection.get_volume_idxs().size(); - if (count >= 3) - return true; - } - if (selection.is_single_volume() || - selection.is_single_modifier()) { - return false; - } - - if (selection.is_multiple_volume() || selection.is_multiple_modifier()) { - size_t count = selection.get_volume_idxs().size(); - if (count < 3) return false; - return (type == AlignType::DISTRIBUTE_X || - type == AlignType::DISTRIBUTE_Y || - type == AlignType::DISTRIBUTE_Z); - } - - if (selection.is_multiple_full_object()) { - BoundingBoxf3 big_bb; - auto objects = get_selected_objects_info(big_bb); - if (objects.size() < 3) return false; - return (type == AlignType::DISTRIBUTE_X || type == AlignType::DISTRIBUTE_Y || type == AlignType::DISTRIBUTE_Z); - } - - return validate_selection_for_distribute(); + return (type == AlignType::DISTRIBUTE_X || type == AlignType::DISTRIBUTE_Y || type == AlignType::DISTRIBUTE_Z) && + get_items_count(type, false) >= 3; } double GLGizmoAlignment::get_current_coord(std::string operation_name, BoundingBoxf3 &bbox) @@ -598,15 +544,6 @@ Vec3d GLGizmoAlignment::generate_displacement(std::string operation_name, double return displacement; } -bool GLGizmoAlignment::is_part_align_parent() -{ - Selection &selection = get_selection(); - if (selection.is_multiple_volume() || selection.is_multiple_modifier() || selection.is_single_volume() || selection.is_single_modifier()) { - return true; - } - return false; -} - std::vector GLGizmoAlignment::get_selected_objects_info(BoundingBoxf3 &big_bb) const { std::vector objects; @@ -633,8 +570,74 @@ std::vector GLGizmoAlignment::get_selected_objects return objects; } -void GLGizmoAlignment::set_parent_box(const BoundingBoxf3 &bb) { - m_parent_box = bb; +bool GLGizmoAlignment::is_parts_selection(bool align_parent) const +{ + const Selection &selection = get_selection(); + const auto &content = selection.get_content(); + if (content.size() != 1 || content.begin()->first >= 1000 || content.begin()->second.size() != 1) + return false; + return !align_parent || selection.get_mode() == Selection::Volume; +} + +std::vector GLGizmoAlignment::get_selected_part_volumes() const +{ + std::vector volumes; + if (!is_parts_selection(false)) + return volumes; + + const Selection &selection = get_selection(); + const ModelObject *object = selection.get_model()->objects[selection.get_object_idx()]; + if (object->is_cut()) + return volumes; + + for (unsigned int idx : selection.get_volume_idxs()) { + const GLVolume *volume = selection.get_volume(idx); + if (volume->volume_idx() < 0) + continue; + // Moving a whole instance's parts leaves its modifiers, negative volumes and support blockers in place, + // as dragging a part does. + if (selection.get_mode() == Selection::Volume || object->volumes[volume->volume_idx()]->is_model_part()) + volumes.push_back(volume); + } + return volumes; +} + +BoundingBoxf3 GLGizmoAlignment::get_items_box() const +{ + BoundingBoxf3 box; + if (is_parts_selection(false)) { + for (const GLVolume *volume : get_selected_part_volumes()) + box.merge(volume->transformed_convex_hull_bounding_box()); + } else + get_selected_objects_info(box); + return box; +} + +BoundingBoxf3 GLGizmoAlignment::get_parent_box() const +{ + const Selection &selection = get_selection(); + if (is_parts_selection(true)) + return selection.get_model()->objects[selection.get_object_idx()]->instance_convex_hull_bounding_box(selection.get_instance_idx()); + + // can_align() requires every selected instance on one plate, so the first one gives the plate. + BoundingBoxf3 box; + PartPlateList &plate_list = wxGetApp().plater()->get_partplate_list(); + for (const auto &[obj_idx, inst_idxs] : selection.get_content()) { + if (obj_idx >= 1000 || inst_idxs.empty()) + continue; + 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() = 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); + break; + } + return box; } Selection& GLGizmoAlignment::get_selection() const @@ -653,41 +656,61 @@ void GLGizmoAlignment::apply_transformation(int obj_idx, int inst_idx, const Vec get_selection().translate(obj_idx, inst_idx, displacement); } -void GLGizmoAlignment::finish_operation(const std::string &operation_name, bool force_volume_move) +void GLGizmoAlignment::finish_operation(bool force_volume_move) { - get_selection().notify_instance_update(-1, -1); - m_canvas.do_move(operation_name, force_volume_move); + // A part move changes the ModelVolume shared by every copy of the object, so the other copies' volumes must show + // it too, or do_move() would write their stale transformation back to the model. + if (force_volume_move) + get_selection().synchronize_unselected_volumes(); + // The operation already took its snapshot. + m_canvas.do_move("", force_volume_move); } -bool GLGizmoAlignment::validate_selection_for_align() const +size_t GLGizmoAlignment::get_items_count(AlignType type, bool align_parent, std::string *why_not) const { - const Selection& selection = get_selection(); + auto cannot_move = [why_not](const std::string &reason) { + if (why_not != nullptr) + *why_not = reason; + return size_t(0); + }; - if (selection.is_single_full_object() || - selection.is_single_volume() || - selection.is_single_modifier()) { - return true; + 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")); } - return selection.get_volume_idxs().size() >= 2; -} + // Instances are moved through their instance offsets, which do_move() commits only from an Instance selection. + const Selection &selection = get_selection(); + if (selection.get_mode() != Selection::Instance) + return 0; -bool GLGizmoAlignment::validate_selection_for_distribute() const -{ - const Selection& selection = get_selection(); + const bool is_z = type == AlignType::Z_MIN || type == AlignType::CENTER_Z || type == AlignType::Z_MAX || + type == AlignType::DISTRIBUTE_Z; + // The plate gives no Z target, and aligning to its bottom would duplicate Drop. + if (is_z && align_parent) + return cannot_move(_u8L("Aligning to the plate is available in X and Y only")); - if (selection.is_single_full_object() || selection.is_multiple_full_object()) { - std::set object_indices; - for (int volume_idx : selection.get_volume_idxs()) { - const GLVolume* volume = selection.get_volume(volume_idx); - if (volume) { - object_indices.insert(volume->object_idx()); + PartPlateList &plate_list = wxGetApp().plater()->get_partplate_list(); + int plate_idx = -1; + size_t count = 0; + for (const auto &[obj_idx, inst_idxs] : selection.get_content()) { + if (obj_idx >= 1000) + 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 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 cannot_move(_u8L("All selected objects must be on the same plate")); + plate_idx = idx; } } - return object_indices.size() >= 3; + count += inst_idxs.size(); } - - return selection.get_volume_idxs().size() >= 3; + return count; } } // namespace GUI diff --git a/src/slic3r/GUI/Gizmos/GLGizmoAlignment.hpp b/src/slic3r/GUI/Gizmos/GLGizmoAlignment.hpp index 5bbeceb7a6..daca519034 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoAlignment.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoAlignment.hpp @@ -4,8 +4,9 @@ #include "libslic3r/Point.hpp" #include "libslic3r/BoundingBox.hpp" #include "slic3r/GUI/Selection.hpp" +#include +#include #include -#include namespace Slic3r { namespace GUI { @@ -58,14 +59,26 @@ public: bool distribute_y(); bool distribute_z(); - bool can_align(AlignType type) const; + bool can_align(AlignType type, bool align_parent = false) const; bool can_distribute(AlignType type) const; std::vector get_selected_objects_info(BoundingBoxf3 &big_bb) const; double get_current_coord(std::string operation_name, BoundingBoxf3 &bbox); Vec3d generate_displacement(std::string operation_name, double offset); - bool is_part_align_parent(); - void set_parent_box(const BoundingBoxf3 &bb); + + // A parts selection (one instance of one object) aligns the parts inside it; anything else aligns whole instances. + // With align_parent, only a Volume-mode selection counts as parts: a whole instance aligns to its plate instead. + bool is_parts_selection(bool align_parent) const; + // The volumes a parts operation moves: the selected volumes, or only the model parts when a whole instance is + // selected. Empty for a cut object, whose parts must keep their place. + std::vector get_selected_part_volumes() const; + // World bounding box of the items an operation moves, the reference of every non-parent operation. + BoundingBoxf3 get_items_box() const; + // 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; @@ -80,13 +93,7 @@ private: Selection& get_selection() const; bool take_snapshot(const std::string& name); void apply_transformation(int obj_idx, int inst_idx, const Vec3d& displacement); - void finish_operation(const std::string &operation_name, bool force_volume_move = false); - - bool validate_selection_for_align() const; - bool validate_selection_for_distribute() const; - -private: - BoundingBoxf3 m_parent_box; + void finish_operation(bool force_volume_move = false); }; } // 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 a5a3122b85..3193916125 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp @@ -373,6 +373,25 @@ bool GLGizmosManager::init_icon_textures() icon_list.insert(std::make_pair((int) icon, texture_id)); } + for (const auto& [icon, name] : std::initializer_list>{ + { 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", 16, 16, texture_id)) + return false; + icon_list.insert(std::make_pair((int) icon, texture_id)); + } + return true; } @@ -470,37 +489,6 @@ bool GLGizmosManager::check_gizmos_closed_except(EType type) const return true; } -// Switches the active plate to the one containing the first selected object, -// so that subsequent operations (align/distribute) act on a plate the user -// can actually see. -void GLGizmosManager::check_object_located_outside_plate(bool change_plate) -{ - PartPlateList& plate_list = wxGetApp().plater()->get_partplate_list(); - auto curr_plate_index = plate_list.get_curr_plate_index(); - Selection& selection = m_parent.get_selection(); - auto idxs = selection.get_volume_idxs(); - if (idxs.empty()) - return; - - const GLVolume* v = selection.get_volume(*idxs.begin()); - int object_idx = v->object_idx(); - const Model* model = m_parent.get_model(); - if (object_idx < 0 || object_idx >= (int) model->objects.size()) - return; - - ModelObject* model_object = model->objects[object_idx]; - for (size_t i = 0; i < plate_list.get_plate_count(); ++i) { - auto plate = plate_list.get_plate(i); - for (auto object : plate->get_objects_on_this_plate()) { - if (model_object == object) { - if (change_plate && curr_plate_index != (int) i) - plate_list.select_plate(i); - return; - } - } - } -} - void GLGizmosManager::set_hover_id(int id) { // Measure and assembly handles hover by itself @@ -648,6 +636,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 cba59de9f1..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); @@ -261,7 +285,6 @@ public: void reset_all_states(); bool open_gizmo(EType type); bool check_gizmos_closed_except(EType) const; - void check_object_located_outside_plate(bool change_plate = true); void set_hover_id(int id); @@ -302,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..406c7e258c 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 @@ -782,6 +790,8 @@ void GizmoObjectManipulation::do_render_move_window(ImGuiWrapper *imgui_wrapper, ImGuiWrapper::push_toolbar_style(m_glcanvas.get_scale()); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0.0, 6.0)); + const ImGuiStyle &style = ImGui::GetStyle(); + std::string name = this->m_new_title_string + "##" + window_name; imgui_wrapper->begin(_L(name), ImGuiWrapper::TOOLBAR_WINDOW_FLAGS); @@ -798,16 +808,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("Selection")).x, + imgui_wrapper->calc_text_size(_L("Object")).x, + imgui_wrapper->calc_text_size(_L("Plate")).x}); + label_max = std::max(label_max, imgui_wrapper->calc_text_size(_L("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 +841,13 @@ 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 = 16.f * m_glcanvas.get_scale(); // ORCA use 16px icons + const float align_button_size = align_icon_size + 2.f * style.FramePadding.x; // ORCA + if (show_align) { + unit_size = std::max(unit_size, 3.f * align_button_size); + unit_size = std::round(unit_size); // ORCA keep columns on whole pixels + } int index = 1; int index_unit = 1; @@ -909,6 +936,134 @@ 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(); + + // Each row is row_height tall, separated by ItemSpacing.y. Every control is centered vertically in its row. + const float row_y = ImGui::GetCursorPosY(); + const float button_height = align_icon_size + 2.f * style.FramePadding.y; + ImGuiWrapper::push_combo_style(m_glcanvas.get_scale()); + const float combo_height = ImGui::GetFrameHeight(); + ImGuiWrapper::pop_combo_style(); + const float row_height = std::max({ImGui::GetTextLineHeight(), button_height, combo_height}); + const float row_gap = style.ItemSpacing.y; + int row = 0; + auto row_top = [&](int r) { return row_y + r * (row_height + row_gap); }; + // Snap to whole pixels to avoid blurry icons + auto center_in_row = [&](int r, float item_height) { return std::floor(row_top(r) + (row_height - item_height) * 0.5f); }; + + // Row 0: align mode combo in the caption column. + ImGui::SetCursorPosY(center_in_row(row, combo_height)); + + // The last mode aligns to the parent. Aligning within the selection needs two items. + std::vector align_modes = { + _u8L("Selection"), + m_alignment_helper.is_parts_selection(true) ? _u8L("Object") : _u8L("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(); + if (ImGui::IsItemHovered()) { + wxString tooltip_str = _L("Reference for alignment"); + imgui_wrapper->tooltip(tooltip_str, imgui_wrapper->calc_text_size(tooltip_str).x + 3 * space_size); + } + ImGuiWrapper::pop_combo_style(); + + 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(int(caption_max + (axis + 1) * space_size + axis * unit_size)); // ORCA ensure placement snaps to pixel to fix blur + if (current_axis < 0) + ImGui::SetCursorPosY(center_in_row(row, button_height)); + 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)"); + + 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)"); + + 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) { + row = 1; + current_axis = -1; + ImGui::SetCursorPosY(center_in_row(row, ImGui::GetTextLineHeight())); + imgui_wrapper->text(_L("Distribute")); + align_icon(0, AlignType::DISTRIBUTE_X, GLGizmosManager::IC_DISTRIBUTE_X, GLGizmosManager::IC_DISTRIBUTE_X_DARK, _L("Distribute left-right") + " (X)"); + align_icon(1, AlignType::DISTRIBUTE_Y, GLGizmosManager::IC_DISTRIBUTE_Y, GLGizmosManager::IC_DISTRIBUTE_Y_DARK, _L("Distribute front-back") + " (Y)"); + 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_top(row + 1)); + } + // 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")} diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index da828527f2..cf9dddeee7 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -245,7 +245,6 @@ #include "Widgets/MultiNozzleSync.hpp" // NozzleOption, tryPopUpMultiNozzleDialog, setExtruderNozzleCount #include "DeviceCore/DevNozzleSystem.h" // DevNozzle, GetExtNozzles / GetRackNozzles #include "ProjectDirtyStateManager.hpp" -#include "Gizmos/GLGizmoAlignment.hpp" #include "Gizmos/GLGizmoSimplify.hpp" // create suggestion notification #include "Gizmos/GLGizmoSVG.hpp" // Drop SVG file #include "Gizmos/GizmoObjectManipulation.hpp" @@ -7485,18 +7484,6 @@ struct Plater::priv void delete_all_objects_from_model(); void reset(bool apply_presets_change = false, bool reload_presets = true); void center_selection(); - void distribute_selection_x(); - void distribute_selection_y(); - void distribute_selection_z(); - void align_selection_x_min(); - void align_selection_x_center(); - void align_selection_x_max(); - void align_selection_y_min(); - void align_selection_y_center(); - void align_selection_y_max(); - void align_selection_z_min(); - void align_selection_z_center(); - void align_selection_z_max(); void drop_selection(); void mirror(Axis axis); void split_object(bool auto_drop = true); @@ -10926,78 +10913,6 @@ void Plater::priv::center_selection() view3D->center_selected(); } -void Plater::priv::distribute_selection_x() -{ - GLGizmoAlignment alignment_helper(*view3D->get_canvas3d()); - alignment_helper.distribute_objects(GLGizmoAlignment::AlignType::DISTRIBUTE_X); -} - -void Plater::priv::distribute_selection_y() -{ - GLGizmoAlignment alignment_helper(*view3D->get_canvas3d()); - alignment_helper.distribute_objects(GLGizmoAlignment::AlignType::DISTRIBUTE_Y); -} - -void Plater::priv::distribute_selection_z() -{ - GLGizmoAlignment alignment_helper(*view3D->get_canvas3d()); - alignment_helper.distribute_objects(GLGizmoAlignment::AlignType::DISTRIBUTE_Z); -} - -void Plater::priv::align_selection_x_min() -{ - GLGizmoAlignment alignment_helper(*view3D->get_canvas3d()); - alignment_helper.align_objects(GLGizmoAlignment::AlignType::X_MIN); -} - -void Plater::priv::align_selection_x_center() -{ - GLGizmoAlignment alignment_helper(*view3D->get_canvas3d()); - alignment_helper.align_objects(GLGizmoAlignment::AlignType::CENTER_X); -} - -void Plater::priv::align_selection_x_max() -{ - GLGizmoAlignment alignment_helper(*view3D->get_canvas3d()); - alignment_helper.align_objects(GLGizmoAlignment::AlignType::X_MAX); -} - -void Plater::priv::align_selection_y_min() -{ - GLGizmoAlignment alignment_helper(*view3D->get_canvas3d()); - alignment_helper.align_objects(GLGizmoAlignment::AlignType::Y_MIN); -} - -void Plater::priv::align_selection_y_center() -{ - GLGizmoAlignment alignment_helper(*view3D->get_canvas3d()); - alignment_helper.align_objects(GLGizmoAlignment::AlignType::CENTER_Y); -} - -void Plater::priv::align_selection_y_max() -{ - GLGizmoAlignment alignment_helper(*view3D->get_canvas3d()); - alignment_helper.align_objects(GLGizmoAlignment::AlignType::Y_MAX); -} - -void Plater::priv::align_selection_z_min() -{ - GLGizmoAlignment alignment_helper(*view3D->get_canvas3d()); - alignment_helper.align_objects(GLGizmoAlignment::AlignType::Z_MIN); -} - -void Plater::priv::align_selection_z_center() -{ - GLGizmoAlignment alignment_helper(*view3D->get_canvas3d()); - alignment_helper.align_objects(GLGizmoAlignment::AlignType::CENTER_Z); -} - -void Plater::priv::align_selection_z_max() -{ - GLGizmoAlignment alignment_helper(*view3D->get_canvas3d()); - alignment_helper.align_objects(GLGizmoAlignment::AlignType::Z_MAX); -} - void Plater::priv::drop_selection() { view3D->drop_selected(); @@ -22311,18 +22226,6 @@ void Plater::suppress_background_process(const bool stop_background_process) BackgroundSlicingProcess& Plater::background_process() { return p->background_process; } void Plater::center_selection() { p->center_selection(); } -void Plater::distribute_selection_x() { p->distribute_selection_x(); } -void Plater::distribute_selection_y() { p->distribute_selection_y(); } -void Plater::distribute_selection_z() { p->distribute_selection_z(); } -void Plater::align_selection_x_min() { p->align_selection_x_min(); } -void Plater::align_selection_x_center() { p->align_selection_x_center(); } -void Plater::align_selection_x_max() { p->align_selection_x_max(); } -void Plater::align_selection_y_min() { p->align_selection_y_min(); } -void Plater::align_selection_y_center() { p->align_selection_y_center(); } -void Plater::align_selection_y_max() { p->align_selection_y_max(); } -void Plater::align_selection_z_min() { p->align_selection_z_min(); } -void Plater::align_selection_z_center() { p->align_selection_z_center(); } -void Plater::align_selection_z_max() { p->align_selection_z_max(); } void Plater::drop_selection() { p->drop_selection(); } void Plater::mirror(Axis axis) { p->mirror(axis); } void Plater::split_object(bool auto_drop) { p->split_object(auto_drop); } diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index a270eea1c3..0bb2d65add 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -762,18 +762,6 @@ public: //BBS: add clone logic void clone_selection(); void center_selection(); - void distribute_selection_x(); - void distribute_selection_y(); - void distribute_selection_z(); - void align_selection_x_min(); - void align_selection_x_center(); - void align_selection_x_max(); - void align_selection_y_min(); - void align_selection_y_center(); - void align_selection_y_max(); - void align_selection_z_min(); - void align_selection_z_center(); - void align_selection_z_max(); void drop_selection(); void search(bool plater_is_active, Preset::Type type, wxWindow *tag, TextInput *etag, wxWindow *stag); void mirror(Axis axis); diff --git a/src/slic3r/GUI/Selection.cpp b/src/slic3r/GUI/Selection.cpp index db27a05ac6..8978f165b9 100644 --- a/src/slic3r/GUI/Selection.cpp +++ b/src/slic3r/GUI/Selection.cpp @@ -1757,50 +1757,27 @@ void Selection::translate(unsigned int object_idx, unsigned int instance_idx, co //BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": obj %1%, instance %2%") % object_idx % instance_idx; //BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": %1%, displacement {%2%, %3%, %4%}") % __LINE__ % displacement(X) % displacement(Y) % displacement(Z); - for (unsigned int i : m_list) { - GLVolume& v = *(*m_volumes)[i]; - if (v.object_idx() == (int)object_idx && v.instance_idx() == (int)instance_idx) - v.set_instance_offset(v.get_instance_offset() + displacement); - } - - std::set done; // prevent processing volumes twice - done.insert(m_list.begin(), m_list.end()); - - for (unsigned int i : m_list) { - if (done.size() == m_volumes->size()) - break; - - int object_idx = (*m_volumes)[i]->object_idx(); - if (object_idx >= 1000) - continue; - - // Process unselected volumes of the object. - for (unsigned int j = 0; j < (unsigned int)m_volumes->size(); ++j) { - if (done.size() == m_volumes->size()) - break; - - if (done.find(j) != done.end()) - continue; - - GLVolume& v = *(*m_volumes)[j]; - if (v.object_idx() != object_idx || v.instance_idx() != (int)instance_idx) - continue; - - v.set_instance_offset(v.get_instance_offset() + displacement); - done.insert(j); - } + // Every volume of the instance, selected or not, so that the instance moves as a whole. + for (GLVolume* v : *m_volumes) { + if (v->object_idx() == (int)object_idx && v->instance_idx() == (int)instance_idx) + v->set_instance_offset(v->get_instance_offset() + displacement); } this->set_bounding_boxes_dirty(); } -void Selection::translate(unsigned int object_idx, unsigned int instance_idx, unsigned int volume_idx, const Vec3d &displacement) { +void Selection::translate(unsigned int object_idx, unsigned int instance_idx, unsigned int volume_idx, const Vec3d &displacement, bool local) +{ if (!m_valid) return; for (unsigned int i : m_list) { GLVolume &v = *(*m_volumes)[i]; - if (v.object_idx() == (int) object_idx && v.instance_idx() == (int) instance_idx && v.volume_idx() == (int) volume_idx) - v.set_volume_offset(v.get_volume_offset() + displacement); + if (v.object_idx() == (int) object_idx && v.instance_idx() == (int) instance_idx && v.volume_idx() == (int) volume_idx) { + Vec3d local_displacement = displacement; + if (!local) + local_displacement = v.get_instance_transformation().get_matrix_no_offset().inverse() * displacement; + v.set_volume_offset(v.get_volume_offset() + local_displacement); + } } this->set_bounding_boxes_dirty(); diff --git a/src/slic3r/GUI/Selection.hpp b/src/slic3r/GUI/Selection.hpp index 35ba184d08..c777c3c291 100644 --- a/src/slic3r/GUI/Selection.hpp +++ b/src/slic3r/GUI/Selection.hpp @@ -360,7 +360,8 @@ public: void translate(unsigned int object_idx, const Vec3d& displacement); void translate(unsigned int object_idx, unsigned int instance_idx, const Vec3d& displacement); - void translate(unsigned int object_idx, unsigned int instance_idx, unsigned int volume_idx, const Vec3d &displacement); + // local: true (default) if displacement is already in instance-local space; false treats it as world-space delta (L^-1 applied). + void translate(unsigned int object_idx, unsigned int instance_idx, unsigned int volume_idx, const Vec3d &displacement, bool local = true); void rotate(unsigned int object_idx, unsigned int instance_idx, const Transform3d &overwrite_tran); void rotate(unsigned int object_idx, unsigned int instance_idx, unsigned int volume_idx, const Transform3d &overwrite_tran);