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