Fix Align/Distribute and add it to the Move gizmo (#16479)

# Description

Follow-up to #13373, which added Align/Distribute to the object context
menu. This fixes the problems found in its review and brings
BambuStudio's align and distribute controls to the Move gizmo.

Align and distribute now work when all copies of an object are selected,
on parts of rotated, scaled or mirrored objects, and on objects with
several copies, whose other copies used to lose the part move. Aligning
a single object arranges its solid parts and leaves modifiers and
negative volumes in place. Moves in Z are offered only when Auto-Drop
would not undo them. Commands that would do nothing are disabled, each
operation is a single undo step, and other plates keep their slicing
result. The same fix stops the automatic drop to the bed after a move,
rotate, scale or mirror from also moving copies of other selected
objects.

The Move gizmo gets BambuStudio's align row. It aligns or distributes
the selected objects or parts, or aligns them to their object or to the
plate, and previews the target while you hover an icon. While the Move
gizmo is open, Ctrl+click adds objects to the selection, and Alt+click
adds or removes parts of the selected object.

No change to slicing output or project files.

# Screenshots/Recordings/Graphs



https://github.com/user-attachments/assets/44c308c3-faff-4f51-a3b7-88f264f03743



## Tests

<!--
> Please describe the tests that you have conducted to verify the
changes made in this PR.
-->

<!--
> A guide for users on how to download the artifacts from this PR.
-->

[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
This commit is contained in:
SoftFever
2026-10-14 23:39:36 +08:00
committed by GitHub
39 changed files with 629 additions and 589 deletions
+1
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><line x1="7.5" y1=".5" x2="7.5" y2="2.5" style="fill:none; stroke:#009688; stroke-linecap:square; stroke-miterlimit:10;"/><line x1="7.5" y1="6.5" x2="7.5" y2="8.5" style="fill:none; stroke:#009688; stroke-linecap:square; stroke-miterlimit:10;"/><line x1="7.5" y1="12.5" x2="7.5" y2="14.5" style="fill:none; stroke:#009688; stroke-linecap:square; stroke-miterlimit:10;"/><rect x="5.5" y="2.5" width="4" height="4" style="fill:none; stroke:#009688; stroke-miterlimit:10;"/><rect x="1.5" y="8.5" width="12" height="4" style="fill:none; stroke:#7c8282; stroke-miterlimit:10;"/></svg>

After

Width:  |  Height:  |  Size: 684 B

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><line x1="7.5" y1=".5" x2="7.5" y2="2.5" style="fill:none; stroke:#009688; stroke-linecap:square; stroke-miterlimit:10;"/><line x1="7.5" y1="6.5" x2="7.5" y2="8.5" style="fill:none; stroke:#009688; stroke-linecap:square; stroke-miterlimit:10;"/><line x1="7.5" y1="12.5" x2="7.5" y2="14.5" style="fill:none; stroke:#009688; stroke-linecap:square; stroke-miterlimit:10;"/><rect x="5.5" y="2.5" width="4" height="4" style="fill:none; stroke:#009688; stroke-miterlimit:10;"/><rect x="1.5" y="8.5" width="12" height="4" style="fill:none; stroke:#949494; stroke-miterlimit:10;"/></svg>

After

Width:  |  Height:  |  Size: 684 B

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><rect x="2.5" y="8.5" width="10" height="4" style="fill:none; stroke:#7c8282; stroke-miterlimit:10;"/><line x1="14.5" y1=".5" x2="14.5" y2="14.5" style="fill:none; stroke:#009688; stroke-linecap:square; stroke-miterlimit:10;"/><rect x="8.5" y="2.5" width="4" height="4" style="fill:none; stroke:#009688; stroke-miterlimit:10;"/></svg>

After

Width:  |  Height:  |  Size: 439 B

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><rect x="2.5" y="8.5" width="10" height="4" style="fill:none; stroke:#949494; stroke-miterlimit:10;"/><line x1="14.5" y1=".5" x2="14.5" y2="14.5" style="fill:none; stroke:#009688; stroke-linecap:square; stroke-miterlimit:10;"/><rect x="8.5" y="2.5" width="4" height="4" style="fill:none; stroke:#009688; stroke-miterlimit:10;"/></svg>

After

Width:  |  Height:  |  Size: 439 B

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><rect x="2.5" y="8.5" width="10" height="4" style="fill:none; stroke:#7c8282; stroke-miterlimit:10;"/><line x1=".5" y1=".5" x2=".5" y2="14.5" style="fill:none; stroke:#009688; stroke-linecap:square; stroke-miterlimit:10;"/><rect x="2.5" y="2.5" width="4" height="4" style="fill:none; stroke:#009688; stroke-miterlimit:10;"/></svg>

After

Width:  |  Height:  |  Size: 435 B

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><rect x="2.5" y="8.5" width="10" height="4" style="fill:none; stroke:#949494; stroke-miterlimit:10;"/><line x1=".5" y1=".5" x2=".5" y2="14.5" style="fill:none; stroke:#009688; stroke-linecap:square; stroke-miterlimit:10;"/><rect x="2.5" y="2.5" width="4" height="4" style="fill:none; stroke:#009688; stroke-miterlimit:10;"/></svg>

After

Width:  |  Height:  |  Size: 435 B

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><line x1=".5" y1="7.5" x2="2.5" y2="7.5" style="fill:none; stroke:#009688; stroke-linecap:square; stroke-miterlimit:10;"/><line x1="6.5" y1="7.5" x2="8.5" y2="7.5" style="fill:none; stroke:#009688; stroke-linecap:square; stroke-miterlimit:10;"/><line x1="12.5" y1="7.5" x2="14.5" y2="7.5" style="fill:none; stroke:#009688; stroke-linecap:square; stroke-miterlimit:10;"/><rect x="2.5" y="5.5" width="4" height="4" transform="translate(-3 12) rotate(-90)" style="fill:none; stroke:#009688; stroke-miterlimit:10;"/><rect x="5.5" y="5.5" width="10" height="4" transform="translate(3 18) rotate(-90)" style="fill:none; stroke:#7c8282; stroke-miterlimit:10;"/></svg>

After

Width:  |  Height:  |  Size: 765 B

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><line x1=".5" y1="7.5" x2="2.5" y2="7.5" style="fill:none; stroke:#009688; stroke-linecap:square; stroke-miterlimit:10;"/><line x1="6.5" y1="7.5" x2="8.5" y2="7.5" style="fill:none; stroke:#009688; stroke-linecap:square; stroke-miterlimit:10;"/><line x1="12.5" y1="7.5" x2="14.5" y2="7.5" style="fill:none; stroke:#009688; stroke-linecap:square; stroke-miterlimit:10;"/><rect x="2.5" y="5.5" width="4" height="4" transform="translate(-3 12) rotate(-90)" style="fill:none; stroke:#009688; stroke-miterlimit:10;"/><rect x="5.5" y="5.5" width="10" height="4" transform="translate(3 18) rotate(-90)" style="fill:none; stroke:#949494; stroke-miterlimit:10;"/></svg>

After

Width:  |  Height:  |  Size: 765 B

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><line x1=".5" y1=".5" x2="14.5" y2=".5" style="fill:none; stroke:#009688; stroke-linecap:square; stroke-miterlimit:10;"/><rect x="2.5" y="2.5" width="4" height="4" transform="translate(0 9) rotate(-90)" style="fill:none; stroke:#009688; stroke-miterlimit:10;"/><rect x="5.5" y="5.5" width="10" height="4" transform="translate(3 18) rotate(-90)" style="fill:none; stroke:#7c8282; stroke-miterlimit:10;"/></svg>

After

Width:  |  Height:  |  Size: 514 B

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><line x1=".5" y1=".5" x2="14.5" y2=".5" style="fill:none; stroke:#009688; stroke-linecap:square; stroke-miterlimit:10;"/><rect x="2.5" y="2.5" width="4" height="4" transform="translate(0 9) rotate(-90)" style="fill:none; stroke:#009688; stroke-miterlimit:10;"/><rect x="5.5" y="5.5" width="10" height="4" transform="translate(3 18) rotate(-90)" style="fill:none; stroke:#949494; stroke-miterlimit:10;"/></svg>

After

Width:  |  Height:  |  Size: 514 B

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><line x1=".5" y1="14.5" x2="14.5" y2="14.5" style="fill:none; stroke:#009688; stroke-linecap:square; stroke-miterlimit:10;"/><rect x="2.5" y="8.5" width="4" height="4" transform="translate(-6 15) rotate(-90)" style="fill:none; stroke:#009688; stroke-miterlimit:10;"/><rect x="5.5" y="5.5" width="10" height="4" transform="translate(3 18) rotate(-90)" style="fill:none; stroke:#7c8282; stroke-miterlimit:10;"/></svg>

After

Width:  |  Height:  |  Size: 520 B

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><line x1=".5" y1="14.5" x2="14.5" y2="14.5" style="fill:none; stroke:#009688; stroke-linecap:square; stroke-miterlimit:10;"/><rect x="2.5" y="8.5" width="4" height="4" transform="translate(-6 15) rotate(-90)" style="fill:none; stroke:#009688; stroke-miterlimit:10;"/><rect x="5.5" y="5.5" width="10" height="4" transform="translate(3 18) rotate(-90)" style="fill:none; stroke:#949494; stroke-miterlimit:10;"/></svg>

After

Width:  |  Height:  |  Size: 520 B

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><polygon points="14.5 7.5 7.5 10.5 .5 7.5 7.5 4.5 14.5 7.5" style="fill:none; stroke:#7c8282; stroke-linejoin:round;"/><polyline points="9.5 .5 7.5 2.5 5.5 .5" style="fill:none; stroke:#009688; stroke-linecap:round; stroke-linejoin:round;"/><polyline points="5.5 14.5 7.5 12.5 9.5 14.5" style="fill:none; stroke:#009688; stroke-linecap:round; stroke-linejoin:round;"/></svg>

After

Width:  |  Height:  |  Size: 479 B

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><polygon points="14.5 7.5 7.5 10.5 .5 7.5 7.5 4.5 14.5 7.5" style="fill:none; stroke:#949494; stroke-linejoin:round;"/><polyline points="9.5 .5 7.5 2.5 5.5 .5" style="fill:none; stroke:#009688; stroke-linecap:round; stroke-linejoin:round;"/><polyline points="5.5 14.5 7.5 12.5 9.5 14.5" style="fill:none; stroke:#009688; stroke-linecap:round; stroke-linejoin:round;"/></svg>

After

Width:  |  Height:  |  Size: 479 B

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><polygon points="14.5 3.5 7.5 6.5 .5 3.5 7.5 .5 14.5 3.5" style="fill:none; stroke:#7c8282; stroke-linejoin:round;"/><line x1="7.5" y1="8.5" x2="7.5" y2="14.5" style="fill:none; stroke:#009688; stroke-linecap:square; stroke-miterlimit:10;"/><polyline points="5.5 10.5 7.5 8.5 9.5 10.5" style="fill:none; stroke:#009688; stroke-linecap:round; stroke-linejoin:round;"/></svg>

After

Width:  |  Height:  |  Size: 478 B

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><polygon points="14.5 3.5 7.5 6.5 .5 3.5 7.5 .5 14.5 3.5" style="fill:none; stroke:#949494; stroke-linejoin:round;"/><line x1="7.5" y1="8.5" x2="7.5" y2="14.5" style="fill:none; stroke:#009688; stroke-linecap:square; stroke-miterlimit:10;"/><polyline points="5.5 10.5 7.5 8.5 9.5 10.5" style="fill:none; stroke:#009688; stroke-linecap:round; stroke-linejoin:round;"/></svg>

After

Width:  |  Height:  |  Size: 478 B

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><polygon points="14.5 11.5 7.5 14.5 .5 11.5 7.5 8.5 14.5 11.5" style="fill:none; stroke:#7c8282; stroke-linejoin:round;"/><line x1="7.5" y1=".5" x2="7.5" y2="6.5" style="fill:none; stroke:#009688; stroke-linecap:square; stroke-miterlimit:10;"/><polyline points="9.5 4.5 7.5 6.5 5.5 4.5" style="fill:none; stroke:#009688; stroke-linecap:round; stroke-linejoin:round;"/></svg>

After

Width:  |  Height:  |  Size: 479 B

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><polygon points="14.5 11.5 7.5 14.5 .5 11.5 7.5 8.5 14.5 11.5" style="fill:none; stroke:#949494; stroke-linejoin:round;"/><line x1="7.5" y1=".5" x2="7.5" y2="6.5" style="fill:none; stroke:#009688; stroke-linecap:square; stroke-miterlimit:10;"/><polyline points="9.5 4.5 7.5 6.5 5.5 4.5" style="fill:none; stroke:#009688; stroke-linecap:round; stroke-linejoin:round;"/></svg>

After

Width:  |  Height:  |  Size: 479 B

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><rect x="-2.5" y="5.5" width="10" height="4" transform="translate(-5 10) rotate(-90)" style="fill:none; stroke:#009688; stroke-linejoin:round;"/><rect x="7.5" y="5.5" width="10" height="4" transform="translate(5 20) rotate(-90)" style="fill:none; stroke:#009688; stroke-linejoin:round;"/><line x1="7.5" y1="5.5" x2="7.5" y2="9.5" style="fill:none; stroke:#7c8282; stroke-linecap:square; stroke-miterlimit:10;"/></svg>

After

Width:  |  Height:  |  Size: 522 B

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><rect x="-2.5" y="5.5" width="10" height="4" transform="translate(-5 10) rotate(-90)" style="fill:none; stroke:#009688; stroke-linejoin:round;"/><rect x="7.5" y="5.5" width="10" height="4" transform="translate(5 20) rotate(-90)" style="fill:none; stroke:#009688; stroke-linejoin:round;"/><line x1="7.5" y1="5.5" x2="7.5" y2="9.5" style="fill:none; stroke:#949494; stroke-linecap:square; stroke-miterlimit:10;"/></svg>

After

Width:  |  Height:  |  Size: 522 B

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><rect x="2.5" y=".5" width="10" height="4" style="fill:none; stroke:#009688; stroke-linejoin:round;"/><rect x="2.5" y="10.5" width="10" height="4" style="fill:none; stroke:#009688; stroke-linejoin:round;"/><line x1="9.5" y1="7.5" x2="5.5" y2="7.5" style="fill:none; stroke:#7c8282; stroke-linecap:square; stroke-miterlimit:10;"/></svg>

After

Width:  |  Height:  |  Size: 440 B

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><rect x="2.5" y=".5" width="10" height="4" style="fill:none; stroke:#009688; stroke-linejoin:round;"/><rect x="2.5" y="10.5" width="10" height="4" style="fill:none; stroke:#009688; stroke-linejoin:round;"/><line x1="9.5" y1="7.5" x2="5.5" y2="7.5" style="fill:none; stroke:#949494; stroke-linecap:square; stroke-miterlimit:10;"/></svg>

After

Width:  |  Height:  |  Size: 440 B

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><polygon points="14.5 3.5 7.5 6.5 .5 3.5 7.5 .5 14.5 3.5" style="fill:none; stroke:#009688; stroke-linejoin:round;"/><polyline points="2.83 10.5 .5 11.5 7.5 14.5 14.5 11.5 12.17 10.5" style="fill:none; stroke:#009688; stroke-linejoin:round;"/><polyline points="2.83 6.5 .5 7.5 7.5 10.5 14.5 7.5 12.17 6.5" style="fill:none; stroke:#7c8282; stroke-linejoin:round;"/></svg>

After

Width:  |  Height:  |  Size: 476 B

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><polygon points="14.5 3.5 7.5 6.5 .5 3.5 7.5 .5 14.5 3.5" style="fill:none; stroke:#009688; stroke-linejoin:round;"/><polyline points="2.83 10.5 .5 11.5 7.5 14.5 14.5 11.5 12.17 10.5" style="fill:none; stroke:#009688; stroke-linejoin:round;"/><polyline points="2.83 6.5 .5 7.5 7.5 10.5 14.5 7.5 12.17 6.5" style="fill:none; stroke:#949494; stroke-linejoin:round;"/></svg>

After

Width:  |  Height:  |  Size: 476 B

+16 -4
View File
@@ -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<unsigned int> 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<std::pair<int, int>> 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<int>(m_model->objects.size()))
continue;
const std::pair<int, int> key(object_idx, selection_mode == Selection::Volume ? -1 : v->instance_idx());
const std::pair<int, int> 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);
}
+51 -234
View File
@@ -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)
+184 -161
View File
@@ -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 <algorithm>
#include <cmath>
#include <cstddef>
#include <string>
#include <vector>
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<typename GetCoordFunc, typename SetCoordFunc>
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<const GLVolume *> 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<const GLVolume *> 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<const GLVolume*> 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<const GLVolume*> 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<typename GetCoordFunc>
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<const GLVolume*> 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<const GLVolume*> 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::ObjectInfo> GLGizmoAlignment::get_selected_objects_info(BoundingBoxf3 &big_bb) const
{
std::vector<ObjectInfo> objects;
@@ -633,8 +570,74 @@ std::vector<GLGizmoAlignment::ObjectInfo> 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<const GLVolume *> GLGizmoAlignment::get_selected_part_volumes() const
{
std::vector<const GLVolume *> 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<int> 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
+18 -11
View File
@@ -4,8 +4,9 @@
#include "libslic3r/Point.hpp"
#include "libslic3r/BoundingBox.hpp"
#include "slic3r/GUI/Selection.hpp"
#include <cstddef>
#include <string>
#include <vector>
#include <functional>
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<ObjectInfo> 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<const GLVolume *> 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
+96
View File
@@ -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 <algorithm>
#include <cstddef>
#include <vector>
#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<double> positions;
if (type >= AlignType::DISTRIBUTE_X) {
const size_t count = std::min<size_t>(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);
+3
View File
@@ -34,6 +34,8 @@ class GLGizmoMove3D : public GLGizmoBase
Vec3d old_center{ Vec3d::Zero() };
};
std::array<GrabberConnection, 3> 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;
+26 -31
View File
@@ -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<std::pair<MENU_ICON_NAME, const char*>>{
{ 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<ClippingPlane> GLGizmosManager::get_clipping_plane() const
{
if (! m_common_gizmos_data || ! m_common_gizmos_data->object_clipper())
+25 -1
View File
@@ -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<ClippingPlane> get_clipping_plane() const;
bool wants_reslice_supports_on_undo() const;
@@ -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<std::string> 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();
@@ -13,6 +13,7 @@
#include <wx/string.h>
#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<std::pair<wxString, wxString>> m_shortcuts_move;
// Contains all shortcuts in the format of {shortcut, description}, e.g. {alt + _L("Left mouse button"), _L("Part_selection")}
-97
View File
@@ -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); }
-12
View File
@@ -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);
+12 -35
View File
@@ -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<unsigned int> 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();
+2 -1
View File
@@ -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);