mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-13 10:51:12 +00:00
Merge upstream/main into belt-printer
Brings belt-printer up to main4b4a261787. Resolutions: - G-code header (#15897, #15915): main moved the header, config and thumbnail block later in _do_export; write_belt_header() moves with it, still after the thumbnails and outside the BTT_TFT gate. - _extrude: first-layer acceleration keeps the per-path first-layer plane test with main's cached nozzle index (#16028); main's set_speed out-param form (#16108) everywhere else. - GCodeWriter (#16108): the arc-to-polyline fallback for machine mappings that cannot express G2/G3 now runs in the out-param extrude_arc_to_xy, which is the overload GCode calls, and appends to the caller's string. - GCodeProcessorResult: the belt fields join main's forwarding assign. - Clipper2 (#15969): belt arrange helpers take Slic3r::Point; the tree support join types lose their ClipperLib qualifier. - CLI arrange (#15837): belt printers still reserve no wipe tower. - Wipe tower options (#15841): the two new sparse-layer toggles are hidden for belt printers like the rest of the tower options. - Keyboard shortcuts (#15706): main's registry replaces the old key switch; the belt view toggle is re-registered in the next commit. - Print::process: the belt purge-plan undo runs before main's SliceStarted event. - scripts/filament_id_snapshot.json: deleted on main (a77209af8f). - Includes and appended tests: union of both sides.
This commit is contained in:
@@ -1,14 +1,17 @@
|
||||
#include "libslic3r/libslic3r.h"
|
||||
#include "libslic3r/PresetBundle.hpp"
|
||||
#include "libslic3r/LifecycleEvents.hpp"
|
||||
#include "GUI_ObjectList.hpp"
|
||||
#include "GUI_Factories.hpp"
|
||||
//#include "GUI_ObjectLayers.hpp"
|
||||
#include "GUI_App.hpp"
|
||||
#include "Shortcuts.hpp"
|
||||
#include "I18N.hpp"
|
||||
#include "Plater.hpp"
|
||||
#include "BitmapComboBox.hpp"
|
||||
#include "MainFrame.hpp"
|
||||
#include "slic3r/Utils/UndoRedo.hpp"
|
||||
#include "slic3r/plugin/PluginManager.hpp"
|
||||
|
||||
#include "OptionsGroup.hpp"
|
||||
#include "Tab.hpp"
|
||||
@@ -25,16 +28,68 @@
|
||||
#include "StepMeshDialog.hpp"
|
||||
|
||||
|
||||
#include "slic3r/GUI/Event.hpp"
|
||||
#include "libslic3r/Config.hpp"
|
||||
#include <string>
|
||||
#include "slic3r/GUI/Widgets/StateColor.hpp"
|
||||
#include "slic3r/GUI/Widgets/Label.hpp"
|
||||
#include "slic3r/GUI/ObjectDataViewModel.hpp"
|
||||
#include <cstddef>
|
||||
#include "slic3r/GUI/ExtraRenderers.hpp"
|
||||
#include <cassert>
|
||||
#include <algorithm>
|
||||
#include "libslic3r/TriangleMesh.hpp"
|
||||
#include "slic3r/GUI/GUI.hpp"
|
||||
#include <memory>
|
||||
#include <iterator>
|
||||
#include "slic3r/GUI/GLToolbar.hpp"
|
||||
#include <optional>
|
||||
#include <cstdlib>
|
||||
#include <utility>
|
||||
#include "libslic3r/BoundingBox.hpp"
|
||||
#include "libslic3r/Geometry.hpp"
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include <boost/algorithm/string/predicate.hpp>
|
||||
#include "libslic3r/LocalesUtils.hpp"
|
||||
#include "libslic3r/Format/STEP.hpp"
|
||||
#include <exception>
|
||||
#include "slic3r/GUI/ParamsPanel.hpp"
|
||||
#include "slic3r/GUI/GUI_ObjectLayers.hpp"
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include "libslic3r/BrimEarsPoint.hpp"
|
||||
#include "libslic3r/ObjectID.hpp"
|
||||
#include "slic3r/GUI/GUI_ObjectSettings.hpp"
|
||||
#include <cmath>
|
||||
#include "slic3r/GUI/GUI_Utils.hpp"
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
#include <functional>
|
||||
#include <boost/algorithm/string.hpp>
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <wx/arrstr.h>
|
||||
#include <wx/dc.h>
|
||||
#include <wx/gdicmn.h>
|
||||
#include <wx/colour.h>
|
||||
#include <wx/dataview.h>
|
||||
#include <wx/event.h>
|
||||
#include <wx/dvrenderers.h>
|
||||
#include <wx/chartype.h>
|
||||
#include <wx/dataobj.h>
|
||||
#include <wx/dnd.h>
|
||||
#include <wx/busycursor.h>
|
||||
#include <wx/msgdlg.h>
|
||||
#include <wx/accel.h>
|
||||
#include <wx/app.h>
|
||||
#include <wx/progdlg.h>
|
||||
#include <wx/listbook.h>
|
||||
#include <wx/numformatter.h>
|
||||
#include <wx/string.h>
|
||||
#include <wx/setup.h>
|
||||
#include <wx/settings.h>
|
||||
#include <wx/utils.h>
|
||||
#include <wx/headerctrl.h>
|
||||
#include <wx/window.h>
|
||||
|
||||
#include "slic3r/Utils/FixModelByCgal.hpp"
|
||||
#include "libslic3r/Format/bbs_3mf.hpp"
|
||||
@@ -245,56 +300,15 @@ ObjectList::ObjectList(wxWindow* parent) :
|
||||
// Key events are not correctly processed by the wxDataViewCtrl on OSX.
|
||||
// Our patched wxWidgets process the keyboard accelerators.
|
||||
// On the other hand, using accelerators will break in-place editing on Windows & Linux/GTK (there is no in-place editing working on OSX for wxDataViewCtrl for now).
|
||||
// Bind(wxEVT_KEY_DOWN, &ObjectList::OnChar, this);
|
||||
{
|
||||
// Accelerators
|
||||
// wxAcceleratorEntry entries[25];
|
||||
wxAcceleratorEntry entries[26];
|
||||
int index = 0;
|
||||
entries[index++].Set(wxACCEL_CTRL, (int)'C', wxID_COPY);
|
||||
entries[index++].Set(wxACCEL_CTRL, (int)'X', wxID_CUT);
|
||||
entries[index++].Set(wxACCEL_CTRL, (int)'V', wxID_PASTE);
|
||||
entries[index++].Set(wxACCEL_CTRL, (int)'M', wxID_DUPLICATE);
|
||||
entries[index++].Set(wxACCEL_CTRL, (int)'A', wxID_SELECTALL);
|
||||
entries[index++].Set(wxACCEL_CTRL, (int)'Z', wxID_UNDO);
|
||||
entries[index++].Set(wxACCEL_CTRL, (int)'Y', wxID_REDO);
|
||||
entries[index++].Set(wxACCEL_NORMAL, WXK_BACK, wxID_DELETE);
|
||||
//entries[index++].Set(wxACCEL_NORMAL, int('+'), wxID_ADD);
|
||||
//entries[index++].Set(wxACCEL_NORMAL, WXK_NUMPAD_ADD, wxID_ADD);
|
||||
//entries[index++].Set(wxACCEL_NORMAL, int('-'), wxID_REMOVE);
|
||||
//entries[index++].Set(wxACCEL_NORMAL, WXK_NUMPAD_SUBTRACT, wxID_REMOVE);
|
||||
//entries[index++].Set(wxACCEL_NORMAL, int('p'), wxID_PRINT);
|
||||
|
||||
int numbers_cnt = 0;
|
||||
for (auto char_number : { '1', '2', '3', '4', '5', '6', '7', '8', '9' }) {
|
||||
entries[index + numbers_cnt].Set(wxACCEL_NORMAL, int(char_number), wxID_LAST + numbers_cnt+1);
|
||||
entries[index + 9 + numbers_cnt].Set(wxACCEL_NORMAL, WXK_NUMPAD0 + numbers_cnt - 1, wxID_LAST + numbers_cnt+1);
|
||||
numbers_cnt++;
|
||||
// index++;
|
||||
}
|
||||
wxAcceleratorTable accel(26, entries);
|
||||
SetAcceleratorTable(accel);
|
||||
|
||||
this->Bind(wxEVT_MENU, [this](wxCommandEvent &evt) { this->copy(); }, wxID_COPY);
|
||||
this->Bind(wxEVT_MENU, [this](wxCommandEvent &evt) { this->paste(); }, wxID_PASTE);
|
||||
this->Bind(wxEVT_MENU, [this](wxCommandEvent &evt) { this->select_item_all_children(); }, wxID_SELECTALL);
|
||||
this->Bind(wxEVT_MENU, [this](wxCommandEvent &evt) { this->remove(); }, wxID_DELETE);
|
||||
this->Bind(wxEVT_MENU, [this](wxCommandEvent &evt) { this->undo(); }, wxID_UNDO);
|
||||
this->Bind(wxEVT_MENU, [this](wxCommandEvent &evt) { this->redo(); }, wxID_REDO);
|
||||
this->Bind(wxEVT_MENU, [this](wxCommandEvent &evt) { this->cut(); }, wxID_CUT);
|
||||
this->Bind(wxEVT_MENU, [this](wxCommandEvent &evt) { this->clone(); }, wxID_DUPLICATE);
|
||||
//this->Bind(wxEVT_MENU, [this](wxCommandEvent &evt) { this->increase_instances(); }, wxID_ADD);
|
||||
//this->Bind(wxEVT_MENU, [this](wxCommandEvent &evt) { this->decrease_instances(); }, wxID_REMOVE);
|
||||
//this->Bind(wxEVT_MENU, [this](wxCommandEvent &evt) { this->toggle_printable_state(); }, wxID_PRINT);
|
||||
|
||||
for (int i = 1; i < 10; i++)
|
||||
this->Bind(wxEVT_MENU, [this, i](wxCommandEvent &evt) {
|
||||
if (filaments_count() > 1 && i <= filaments_count())
|
||||
this->set_extruder_for_selected_items(i);
|
||||
}, wxID_LAST+i);
|
||||
|
||||
m_accel = accel;
|
||||
}
|
||||
m_shortcut_id_base = wxWindow::NewControlId(int(Shortcut::Count));
|
||||
for (size_t i = 0; i < size_t(Shortcut::Count); ++i)
|
||||
this->Bind(wxEVT_MENU, [this, shortcut = Shortcut(i)](wxCommandEvent&) { dispatch_shortcut(shortcut); }, m_shortcut_id_base + int(i));
|
||||
for (int i = 1; i < 10; i++)
|
||||
this->Bind(wxEVT_MENU, [this, i](wxCommandEvent &evt) {
|
||||
if (filaments_count() > 1 && i <= filaments_count())
|
||||
this->set_extruder_for_selected_items(i);
|
||||
}, wxID_LAST+i);
|
||||
update_shortcut_accelerators();
|
||||
#else //__WXOSX__
|
||||
Bind(wxEVT_CHAR, [this](wxKeyEvent& event) { key_event(event); }); // doesn't work on OSX
|
||||
#endif
|
||||
@@ -1199,17 +1213,39 @@ void ObjectList::update_name_in_model(const wxDataViewItem& item) const
|
||||
if (m_objects_model->GetItemType(item) & itObject) {
|
||||
std::string name = m_objects_model->GetName(item).ToUTF8().data();
|
||||
if (obj->name != name) {
|
||||
const std::string previous_name = obj->name;
|
||||
obj->name = name;
|
||||
// if object has just one volume, rename this volume too
|
||||
if (obj->volumes.size() == 1)
|
||||
obj->volumes[0]->name = obj->name;
|
||||
Slic3r::save_object_mesh(*obj);
|
||||
|
||||
LifecycleEventContext ctx;
|
||||
ctx.name = name;
|
||||
ctx.previous_name = previous_name;
|
||||
ctx.id = std::to_string(obj->id().id);
|
||||
ctx.index = obj_idx;
|
||||
ctx.source = "object";
|
||||
fire_lifecycle_event(LifecycleEvent::ObjectRenamed, ctx);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (volume_id < 0) return;
|
||||
obj->volumes[volume_id]->name = m_objects_model->GetName(item).ToUTF8().data();
|
||||
std::string name = m_objects_model->GetName(item).ToUTF8().data();
|
||||
if (obj->volumes[volume_id]->name == name)
|
||||
return;
|
||||
|
||||
const std::string previous_name = obj->volumes[volume_id]->name;
|
||||
obj->volumes[volume_id]->name = name;
|
||||
|
||||
LifecycleEventContext ctx;
|
||||
ctx.name = name;
|
||||
ctx.previous_name = previous_name;
|
||||
ctx.id = std::to_string(obj->volumes[volume_id]->id().id);
|
||||
ctx.index = volume_id;
|
||||
ctx.source = "volume";
|
||||
fire_lifecycle_event(LifecycleEvent::ObjectRenamed, ctx);
|
||||
}
|
||||
|
||||
void ObjectList::update_name_in_list(int obj_idx, int vol_idx) const
|
||||
@@ -1796,36 +1832,10 @@ void ObjectList::decrease_instances()
|
||||
#ifndef __WXOSX__
|
||||
void ObjectList::key_event(wxKeyEvent& event)
|
||||
{
|
||||
//if (event.GetKeyCode() == WXK_TAB)
|
||||
// Navigate(event.ShiftDown() ? wxNavigationKeyEvent::IsBackward : wxNavigationKeyEvent::IsForward);
|
||||
//else
|
||||
if (event.GetKeyCode() == WXK_DELETE /*|| event.GetKeyCode() == WXK_BACK*/ )
|
||||
remove();
|
||||
//else if (event.GetKeyCode() == WXK_F5)
|
||||
// wxGetApp().plater()->reload_all_from_disk();
|
||||
else if (wxGetKeyState(wxKeyCode('A')) && wxGetKeyState(WXK_CONTROL/*WXK_SHIFT*/))
|
||||
select_item_all_children();
|
||||
else if (wxGetKeyState(wxKeyCode('C')) && wxGetKeyState(WXK_CONTROL))
|
||||
copy();
|
||||
else if (wxGetKeyState(wxKeyCode('V')) && wxGetKeyState(WXK_CONTROL))
|
||||
paste();
|
||||
else if (wxGetKeyState(wxKeyCode('Y')) && wxGetKeyState(WXK_CONTROL))
|
||||
redo();
|
||||
else if (wxGetKeyState(wxKeyCode('Z')) && wxGetKeyState(WXK_CONTROL))
|
||||
undo();
|
||||
else if (wxGetKeyState(wxKeyCode('X')) && wxGetKeyState(WXK_CONTROL))
|
||||
cut();
|
||||
else if (wxGetKeyState(wxKeyCode('K')) && wxGetKeyState(WXK_CONTROL))
|
||||
clone();
|
||||
else if (event.GetUnicodeKey() == '+')
|
||||
increase_instances();
|
||||
else if (event.GetUnicodeKey() == '-')
|
||||
decrease_instances();
|
||||
else if (event.GetUnicodeKey() == 'p')
|
||||
toggle_printable_state();
|
||||
else if (event.GetUnicodeKey() == 'd')
|
||||
toggle_auto_drop();
|
||||
else if (filaments_count() > 1) {
|
||||
const std::optional<Shortcut> shortcut = wxGetApp().shortcuts().lookup(ShortcutContext::ObjectList, KeyChord::from_event(event));
|
||||
if (shortcut.has_value() && dispatch_shortcut(*shortcut))
|
||||
return;
|
||||
if (filaments_count() > 1) {
|
||||
std::vector<wxChar> numbers = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
|
||||
wxChar key_char = event.GetUnicodeKey();
|
||||
if (std::find(numbers.begin(), numbers.end(), key_char) != numbers.end()) {
|
||||
@@ -1842,6 +1852,43 @@ void ObjectList::key_event(wxKeyEvent& event)
|
||||
}
|
||||
#endif /* __WXOSX__ */
|
||||
|
||||
#ifdef __WXOSX__
|
||||
void ObjectList::update_shortcut_accelerators()
|
||||
{
|
||||
std::vector<wxAcceleratorEntry> entries;
|
||||
const ShortcutRegistry& shortcuts = wxGetApp().shortcuts();
|
||||
for (Shortcut shortcut : shortcuts_in(ShortcutContext::ObjectList))
|
||||
if (const KeyChord chord = shortcuts.binding(shortcut); chord.valid())
|
||||
entries.push_back(chord.to_accelerator_entry(m_shortcut_id_base + int(shortcut)));
|
||||
for (int i = 1; i < 10; ++i) {
|
||||
entries.emplace_back(wxACCEL_NORMAL, '0' + i, wxID_LAST + i);
|
||||
entries.emplace_back(wxACCEL_NORMAL, WXK_NUMPAD0 + i, wxID_LAST + i);
|
||||
}
|
||||
m_accel = wxAcceleratorTable(int(entries.size()), entries.data());
|
||||
SetAcceleratorTable(m_accel);
|
||||
}
|
||||
#endif /* __WXOSX__ */
|
||||
|
||||
bool ObjectList::dispatch_shortcut(Shortcut shortcut)
|
||||
{
|
||||
switch (shortcut) {
|
||||
case Shortcut::DeleteSelected: remove(); break;
|
||||
case Shortcut::SelectAll: select_item_all_children(); break;
|
||||
case Shortcut::Copy: copy(); break;
|
||||
case Shortcut::Paste: paste(); break;
|
||||
case Shortcut::Cut: cut(); break;
|
||||
case Shortcut::Undo: undo(); break;
|
||||
case Shortcut::Redo: redo(); break;
|
||||
case Shortcut::CloneSelected: clone(); break;
|
||||
case Shortcut::AddInstance: increase_instances(); break;
|
||||
case Shortcut::RemoveInstance: decrease_instances(); break;
|
||||
case Shortcut::TogglePrintable: toggle_printable_state(); break;
|
||||
case Shortcut::ToggleAutoDrop: toggle_auto_drop(); break;
|
||||
default: return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void ObjectList::OnBeginDrag(wxDataViewEvent &event)
|
||||
{
|
||||
const bool mult_sel = multiple_selection();
|
||||
@@ -1949,6 +1996,12 @@ bool ObjectList::can_drop(const wxDataViewItem& item, int& src_obj_id, int& src_
|
||||
|
||||
if (dragged_item_v_type == item_v_type && dragged_item_v_type != ModelVolumeType::MODEL_PART)
|
||||
return true;
|
||||
|
||||
// Use tree item types: hidden cut connectors make tree indices differ from volumes indices.
|
||||
if (is_precise_seam(dragged_item_v_type) && is_precise_seam(item_v_type))
|
||||
// Allow reordering only within the strong or weak modifier group.
|
||||
return is_precise_seam_strong(dragged_item_v_type) == is_precise_seam_strong(item_v_type);
|
||||
|
||||
if ((dragged_item_v_type != item_v_type) || // we can't reorder volumes outside of types
|
||||
item_v_type >= ModelVolumeType::SUPPORT_BLOCKER) // support blockers/enforcers can't change its place
|
||||
return false;
|
||||
@@ -2038,10 +2091,43 @@ void ObjectList::OnDrop(wxDataViewEvent &event)
|
||||
int to_volume_id = m_objects_model->GetVolumeIdByItem(item);
|
||||
int delta = to_volume_id < from_volume_id ? -1 : 1;
|
||||
|
||||
auto &volumes = (*m_objects)[m_dragged_data.obj_idx()]->volumes;
|
||||
const int obj_idx = m_dragged_data.obj_idx();
|
||||
// Object-indexed UI maps may be stale after another object is removed or reordered.
|
||||
if (obj_idx < 0 || size_t(obj_idx) >= m_objects->size()) {
|
||||
event.Veto();
|
||||
m_dragged_data.clear();
|
||||
return;
|
||||
}
|
||||
const ModelObject *object = (*m_objects)[obj_idx];
|
||||
auto &volumes = (*m_objects)[obj_idx]->volumes;
|
||||
std::vector<size_t> visible_volume_indices;
|
||||
visible_volume_indices.reserve(volumes.size());
|
||||
for (size_t idx = 0; idx < volumes.size(); ++idx)
|
||||
// Match add_volumes_to_object_in_list: only connectors of cut objects are hidden.
|
||||
if (!(object->is_cut() && volumes[idx]->is_cut_connector()))
|
||||
visible_volume_indices.push_back(idx);
|
||||
|
||||
int cnt = 0;
|
||||
for (int id = from_volume_id; cnt < abs(from_volume_id - to_volume_id); id += delta, cnt++) std::swap(volumes[id], volumes[id + delta]);
|
||||
// Validate the entire move before any swap; these checks must also protect Release builds.
|
||||
if (from_volume_id < 0 || to_volume_id < 0 ||
|
||||
size_t(from_volume_id) >= visible_volume_indices.size() || size_t(to_volume_id) >= visible_volume_indices.size()) {
|
||||
event.Veto();
|
||||
m_dragged_data.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
// Move through visible slots only, keeping hidden cut connectors at their original indices.
|
||||
// The local mapping stays valid because no hidden volume changes slots during the move.
|
||||
for (int id = from_volume_id; id != to_volume_id; id += delta) {
|
||||
const size_t current_idx = visible_volume_indices[id];
|
||||
const size_t next_idx = visible_volume_indices[id + delta];
|
||||
std::swap(volumes[current_idx], volumes[next_idx]);
|
||||
}
|
||||
|
||||
// Later selection/filament handlers use this cache; repair it for the moved object too.
|
||||
auto &ui_to_model = m_objects_model->get_ui_and_3d_volume_map()[obj_idx];
|
||||
ui_to_model.clear();
|
||||
for (size_t idx = 0; idx < visible_volume_indices.size(); ++idx)
|
||||
ui_to_model[int(idx)] = int(visible_volume_indices[idx]);
|
||||
|
||||
select_item(m_objects_model->ReorganizeChildren(from_volume_id, to_volume_id, m_objects_model->GetParent(item)));
|
||||
|
||||
@@ -3548,7 +3634,14 @@ void ObjectList::delete_all_connectors_for_object(int obj_idx)
|
||||
obj->delete_connectors();
|
||||
|
||||
if (obj->volumes.empty() || !obj->has_solid_mesh()) {
|
||||
const std::string deleted_obj_name = obj->name;
|
||||
model.delete_object(idx);
|
||||
{
|
||||
Slic3r::LifecycleEventContext ctx;
|
||||
ctx.name = deleted_obj_name;
|
||||
ctx.code = Slic3r::LifecycleEvtCode::Ok;
|
||||
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectDeleted, ctx);
|
||||
}
|
||||
m_objects_model->Delete(m_objects_model->GetItemById(idx));
|
||||
continue;
|
||||
}
|
||||
@@ -3853,7 +3946,8 @@ wxDataViewItem ObjectList::add_settings_item(wxDataViewItem parent_item, const D
|
||||
const bool is_layer_settings = m_objects_model->GetItemType(parent_item) == itLayer;
|
||||
if (!is_object_settings) {
|
||||
ModelVolumeType volume_type = m_objects_model->GetVolumeType(parent_item);
|
||||
if (volume_type == ModelVolumeType::NEGATIVE_VOLUME || volume_type == ModelVolumeType::SUPPORT_BLOCKER || volume_type == ModelVolumeType::SUPPORT_ENFORCER)
|
||||
// Precise Seam is non-printing helper geometry — no per-volume settings
|
||||
if (volume_type == ModelVolumeType::NEGATIVE_VOLUME || volume_type == ModelVolumeType::SUPPORT_BLOCKER || volume_type == ModelVolumeType::SUPPORT_ENFORCER || is_precise_seam(volume_type))
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -4117,6 +4211,13 @@ void ObjectList::add_object_to_list(size_t obj_idx, bool call_selection_changed,
|
||||
const auto item = m_objects_model->AddObject(model_object, warning_bitmap, model_object->is_cut());
|
||||
Expand(m_objects_model->GetParent(item));
|
||||
|
||||
{
|
||||
Slic3r::LifecycleEventContext ctx;
|
||||
ctx.name = model_object->name;
|
||||
ctx.code = Slic3r::LifecycleEvtCode::Ok;
|
||||
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectAdded, ctx);
|
||||
}
|
||||
|
||||
if (!do_info_update)
|
||||
return;
|
||||
|
||||
@@ -5712,13 +5813,49 @@ ModelVolumeType ObjectList::get_selected_volume_type()
|
||||
return ModelVolumeType::INVALID;
|
||||
}
|
||||
|
||||
void ObjectList::set_volume_type(ModelVolumeType new_type)
|
||||
// ---------------- Helpers for Precise Seam group-aware type changes ----------------
|
||||
// Used by set_volume_type() and the "Precise Seam Type" subtype submenu handler.
|
||||
|
||||
// Detects whether a type change crosses a Precise Seam "group boundary" that requires
|
||||
// manual repositioning inside ModelObject::volumes[]:
|
||||
// - between any non-PS type and any PS subtype, or
|
||||
// - between strong PS (CENTER/LEFT/RIGHT) and weak PS (ENFORCED/BLOCKED/NEUTRAL).
|
||||
// sort_volumes() is stable and groups strong PS before weak PS. When a volume crosses
|
||||
// a group, moving it to the end of volumes[] lets the subsequent stable sort place it
|
||||
// at the end of its new group. Without this, a strong→weak transition would leave the
|
||||
// volume stuck in the strong segment and break the modifier-application order.
|
||||
static bool precise_seam_group_changed(ModelVolumeType old_type, ModelVolumeType new_type)
|
||||
{
|
||||
const bool old_is_ps = is_precise_seam(old_type);
|
||||
const bool new_is_ps = is_precise_seam(new_type);
|
||||
if (old_is_ps != new_is_ps)
|
||||
return true; // non-PS ↔ PS transition
|
||||
if (!old_is_ps)
|
||||
return false; // both non-PS — regular enum ordering is enough
|
||||
return is_precise_seam_strong(old_type) != is_precise_seam_strong(new_type);
|
||||
}
|
||||
|
||||
// Moves a volume to the end of ModelObject::volumes[]. Combined with stable sort_volumes(),
|
||||
// this places it at the end of its new group while preserving relative order of others.
|
||||
static void move_volume_to_end(ModelObject* obj, ModelVolume* volume)
|
||||
{
|
||||
if (obj == nullptr || volume == nullptr)
|
||||
return;
|
||||
auto it = std::find(obj->volumes.begin(), obj->volumes.end(), volume);
|
||||
if (it != obj->volumes.end()) {
|
||||
obj->volumes.erase(it);
|
||||
obj->volumes.push_back(volume);
|
||||
}
|
||||
}
|
||||
|
||||
void ObjectList::set_volume_type(ModelVolumeType new_type, bool preserve_ps_subtype)
|
||||
{
|
||||
struct VolumeSelection {
|
||||
int object_idx;
|
||||
ModelVolume* volume;
|
||||
};
|
||||
|
||||
// --- Collect selected volumes from the object tree, falling back to the 3D canvas ---
|
||||
std::vector<VolumeSelection> volumes;
|
||||
auto add_volume = [&volumes](int obj_idx, ModelVolume* volume) {
|
||||
if (volume == nullptr)
|
||||
@@ -5783,25 +5920,49 @@ void ObjectList::set_volume_type(ModelVolumeType new_type)
|
||||
// and historically crashed (originally fixed in the now-disabled change_part_type() by hiding
|
||||
// Support entries in the old choice dialog; the UI-side guard for the current submenu lives
|
||||
// in MenuFactory::append_menu_item_change_type, see #13120).
|
||||
// The same rationale applies to any Precise Seam subtype.
|
||||
// This block must never be reachable under a healthy UI; if it ever logs, the UI guard has
|
||||
// been bypassed (new entry point, refactor, plugin, etc.) and should be investigated.
|
||||
if (new_type == ModelVolumeType::SUPPORT_BLOCKER || new_type == ModelVolumeType::SUPPORT_ENFORCER) {
|
||||
if (new_type == ModelVolumeType::SUPPORT_BLOCKER || new_type == ModelVolumeType::SUPPORT_ENFORCER
|
||||
|| is_precise_seam(new_type)) {
|
||||
const bool has_text_or_svg = std::any_of(volumes.begin(), volumes.end(),
|
||||
[](const VolumeSelection& sel) { return sel.volume->is_svg() || sel.volume->is_text(); });
|
||||
if (has_text_or_svg) {
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__
|
||||
<< ": blocked attempt to set SUPPORT_BLOCKER/ENFORCER on SVG/text volume; "
|
||||
<< ": blocked attempt to set SUPPORT_BLOCKER/ENFORCER or Precise Seam on SVG/text volume; "
|
||||
<< "UI guard should have prevented this -- possible regression in the Change Type menu";
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Subtype preservation for the generic "Precise Seam" entry ---
|
||||
// The Change Type submenu uses PRECISE_SEAM_CENTER as the single "Precise Seam" entry
|
||||
// and calls with preserve_ps_subtype=true: volumes that are already Precise Seam keep
|
||||
// their subtype (LEFT/RIGHT/etc.); only non-PS volumes get converted to the default
|
||||
// CENTER subtype.
|
||||
//
|
||||
// The "Precise Seam Type" subtype submenu calls with preserve_ps_subtype=false: the
|
||||
// user explicitly picked CENTER and all selected PS volumes must be set to CENTER
|
||||
// verbatim (otherwise CENTER would become unreachable for mixed-subtype selections,
|
||||
// since preservation would keep every volume at its current subtype and any_diff would
|
||||
// be false — the reason for this two-parameter split).
|
||||
auto effective_new_type = [new_type, preserve_ps_subtype](const ModelVolume* v) -> ModelVolumeType {
|
||||
if (preserve_ps_subtype && new_type == ModelVolumeType::PRECISE_SEAM_CENTER && v->is_precise_seam())
|
||||
return v->type();
|
||||
return new_type;
|
||||
};
|
||||
|
||||
// --- Any-change check using the effective target type ---
|
||||
const bool any_diff = std::any_of(volumes.begin(), volumes.end(),
|
||||
[new_type](const VolumeSelection& sel) { return sel.volume->type() != new_type; });
|
||||
[&effective_new_type](const VolumeSelection& sel) {
|
||||
return sel.volume->type() != effective_new_type(sel.volume);
|
||||
});
|
||||
|
||||
if (!any_diff)
|
||||
return;
|
||||
|
||||
// --- Last-solid-part guard (pre-existing behavior) ---
|
||||
// When converting away from MODEL_PART, ensure at least one solid part remains per object.
|
||||
if (new_type != ModelVolumeType::MODEL_PART) {
|
||||
std::map<int, int> total_part_cnt;
|
||||
std::map<int, int> selected_part_cnt;
|
||||
@@ -5829,14 +5990,36 @@ void ObjectList::set_volume_type(ModelVolumeType new_type)
|
||||
|
||||
take_snapshot(_u8L("Change part type"));
|
||||
|
||||
// --- Apply type changes with Precise Seam group-aware repositioning ---
|
||||
// Note: `changed_volumes` / `touched_objects` track every processed volume, including
|
||||
// subtype-preservation no-ops. Tracking no-ops is intentional — it ensures such
|
||||
// volumes remain selected after the post-apply rebuild. Without that, a cross-object
|
||||
// mixed selection like [Part, PS_LEFT] clicking "Change type → Precise Seam" would
|
||||
// silently deselect the preserved PS_LEFT: only the Part's object would enter
|
||||
// touched_objects, and the final SetSelections(new_selection) would drop PS_LEFT.
|
||||
std::set<const ModelVolume*> changed_volumes;
|
||||
std::set<int> touched_objects;
|
||||
for (const auto& sel : volumes) {
|
||||
sel.volume->set_type(new_type);
|
||||
const ModelVolumeType target = effective_new_type(sel.volume);
|
||||
const ModelVolumeType old_type = sel.volume->type();
|
||||
|
||||
// Record the volume for reselection before the no-op short-circuit (see comment above).
|
||||
changed_volumes.insert(sel.volume);
|
||||
touched_objects.insert(sel.object_idx);
|
||||
|
||||
if (old_type == target)
|
||||
continue; // subtype-preservation no-op — nothing to write/move
|
||||
|
||||
sel.volume->set_type(target);
|
||||
|
||||
// If the change crosses a PS group boundary, move the volume to the end of volumes[]
|
||||
// so the stable sort in reorder_volumes_and_get_selection() places it at the end of
|
||||
// its new group (see precise_seam_group_changed() for details).
|
||||
if (precise_seam_group_changed(old_type, target))
|
||||
move_volume_to_end((*m_objects)[sel.object_idx], sel.volume);
|
||||
}
|
||||
|
||||
// --- Rebuild selection to follow the changed volumes ---
|
||||
wxDataViewItemArray new_selection;
|
||||
for (int obj_idx : touched_objects) {
|
||||
wxDataViewItemArray sel_items = reorder_volumes_and_get_selection(obj_idx, [&changed_volumes](const ModelVolume* volume) {
|
||||
|
||||
Reference in New Issue
Block a user