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);