From e922411371603a5be33f294fcd17587de2f03701 Mon Sep 17 00:00:00 2001 From: Vovodroid Date: Sun, 26 Oct 2025 10:13:14 +0200 Subject: [PATCH] Add instances (#6237) * Add instances * - Added a new menu item for converting instances to objects, --------- Co-authored-by: SoftFever --- resources/images/instance_add.svg | 68 +++++++++++++++++++++++ resources/images/instance_add_dark.svg | 68 +++++++++++++++++++++++ resources/images/instance_remove.svg | 58 +++++++++++++++++++ resources/images/instance_remove_dark.svg | 58 +++++++++++++++++++ src/slic3r/GUI/GLCanvas3D.cpp | 58 ++++++++++++++----- src/slic3r/GUI/GUI_Factories.cpp | 33 +++++++++-- src/slic3r/GUI/GUI_Factories.hpp | 2 +- src/slic3r/GUI/GUI_ObjectList.cpp | 12 ++-- src/slic3r/GUI/Jobs/FillBedJob.cpp | 35 ++++++++++-- src/slic3r/GUI/Jobs/FillBedJob.hpp | 4 +- src/slic3r/GUI/Plater.cpp | 25 ++++++--- src/slic3r/GUI/Plater.hpp | 1 + 12 files changed, 382 insertions(+), 40 deletions(-) create mode 100644 resources/images/instance_add.svg create mode 100644 resources/images/instance_add_dark.svg create mode 100644 resources/images/instance_remove.svg create mode 100644 resources/images/instance_remove_dark.svg diff --git a/resources/images/instance_add.svg b/resources/images/instance_add.svg new file mode 100644 index 0000000000..8c9ce6a2d1 --- /dev/null +++ b/resources/images/instance_add.svg @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + diff --git a/resources/images/instance_add_dark.svg b/resources/images/instance_add_dark.svg new file mode 100644 index 0000000000..e3f31a8bab --- /dev/null +++ b/resources/images/instance_add_dark.svg @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + diff --git a/resources/images/instance_remove.svg b/resources/images/instance_remove.svg new file mode 100644 index 0000000000..7f851bc023 --- /dev/null +++ b/resources/images/instance_remove.svg @@ -0,0 +1,58 @@ + + + + + + + + + + + + + diff --git a/resources/images/instance_remove_dark.svg b/resources/images/instance_remove_dark.svg new file mode 100644 index 0000000000..b6335df6c6 --- /dev/null +++ b/resources/images/instance_remove_dark.svg @@ -0,0 +1,58 @@ + + + + + + + + + + + + + diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 7523c04cbc..86e29afb24 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -3550,20 +3550,20 @@ void GLCanvas3D::on_char(wxKeyEvent& evt) break; } - //case '+': { - // if (dynamic_cast(m_canvas->GetParent()) != nullptr) - // post_event(wxKeyEvent(EVT_GLCANVAS_EDIT_COLOR_CHANGE, evt)); - // else - // post_event(Event(EVT_GLCANVAS_INCREASE_INSTANCES, +1)); - // break; - //} - //case '-': { - // if (dynamic_cast(m_canvas->GetParent()) != nullptr) - // post_event(wxKeyEvent(EVT_GLCANVAS_EDIT_COLOR_CHANGE, evt)); - // else - // post_event(Event(EVT_GLCANVAS_INCREASE_INSTANCES, -1)); - // break; - //} + case '+': { + if (dynamic_cast(m_canvas->GetParent()) != nullptr) + post_event(wxKeyEvent(EVT_GLCANVAS_EDIT_COLOR_CHANGE, evt)); + else + post_event(Event(EVT_GLCANVAS_INCREASE_INSTANCES, +1)); + break; + } + case '-': { + if (dynamic_cast(m_canvas->GetParent()) != nullptr) + post_event(wxKeyEvent(EVT_GLCANVAS_EDIT_COLOR_CHANGE, evt)); + else + post_event(Event(EVT_GLCANVAS_INCREASE_INSTANCES, -1)); + break; + } case '?': { post_event(SimpleEvent(EVT_GLCANVAS_QUESTION_MARK)); break; } case 'A': case 'a': @@ -6656,6 +6656,12 @@ void GLCanvas3D::_switch_toolbars_icon_filename() item = m_main_toolbar.get_item("arrange"); item->set_icon_filename(m_is_dark ? "toolbar_arrange_dark.svg" : "toolbar_arrange.svg"); + item = m_main_toolbar.get_item("more"); + item->set_icon_filename(m_is_dark ? "instance_add_dark.svg" : "instance_add.svg"); + + item = m_main_toolbar.get_item("fewer"); + item->set_icon_filename(m_is_dark ? "instance_remove_dark.svg" : "instance_remove.svg"); + item = m_main_toolbar.get_item("splitobjects"); item->set_icon_filename(m_is_dark ? "split_objects_dark.svg" : "split_objects.svg"); @@ -6805,6 +6811,30 @@ bool GLCanvas3D::_init_main_toolbar() if (!m_main_toolbar.add_separator()) return false; + item.name = "more"; + item.icon_filename = m_is_dark ? "instance_add_dark.svg" : "instance_add.svg"; + item.tooltip = _utf8(L("Add instance")) + " [+]"; + item.sprite_id++; + item.left.render_callback = nullptr; + item.left.action_callback = [this]() { if (m_canvas != nullptr) wxPostEvent(m_canvas, SimpleEvent(EVT_GLTOOLBAR_MORE)); }; + item.visibility_callback = GLToolbarItem::Default_Visibility_Callback; + item.left.toggable = false; + item.enabling_callback = []()->bool { return wxGetApp().plater()->can_increase_instances(); }; + if (!m_main_toolbar.add_item(item)) + return false; + + item.name = "fewer"; + item.icon_filename = m_is_dark ? "instance_remove_dark.svg" : "instance_remove.svg"; + item.tooltip = _utf8(L("Remove instance")) + " [-]"; + item.sprite_id++; + item.left.render_callback = nullptr; + item.left.action_callback = [this]() { if (m_canvas != nullptr) wxPostEvent(m_canvas, SimpleEvent(EVT_GLTOOLBAR_FEWER)); }; + item.visibility_callback = GLToolbarItem::Default_Visibility_Callback; + item.left.toggable = false; + item.enabling_callback = []()->bool { return wxGetApp().plater()->can_decrease_instances(); }; + if (!m_main_toolbar.add_item(item)) + return false; + item.name = "splitobjects"; item.icon_filename = m_is_dark ? "split_objects_dark.svg" : "split_objects.svg"; item.tooltip = _utf8(L("Split to objects")); diff --git a/src/slic3r/GUI/GUI_Factories.cpp b/src/slic3r/GUI/GUI_Factories.cpp index 2232e16f60..6a7ed26c34 100644 --- a/src/slic3r/GUI/GUI_Factories.cpp +++ b/src/slic3r/GUI/GUI_Factories.cpp @@ -782,7 +782,7 @@ void MenuFactory::append_menu_item_fill_bed(wxMenu *menu) { append_menu_item( menu, wxID_ANY, _L("Fill bed with copies"), _L("Fill the remaining area of bed with copies of the selected object"), - [](wxCommandEvent &) { plater()->fill_bed_with_instances(); }, "", nullptr, []() { return plater()->can_increase_instances(); }, m_parent); + [](wxCommandEvent &) { plater()->fill_bed_with_copies(); }, "", nullptr, []() { return plater()->can_increase_instances(); }, m_parent); } wxMenuItem* MenuFactory::append_menu_item_printable(wxMenu* menu) @@ -1259,14 +1259,12 @@ void MenuFactory::create_default_menu() void MenuFactory::create_common_object_menu(wxMenu* menu) { append_menu_item_rename(menu); - // BBS - //append_menu_items_instance_manipulation(menu); + append_menu_items_instance_manipulation(menu); + // Delete menu was moved to be after +/- instace to make it more difficult to be selected by mistake. append_menu_item_delete(menu); - //append_menu_item_instance_to_object(menu); menu->AppendSeparator(); - // BBS append_menu_item_reload_from_disk(menu); append_menu_item_export_stl(menu); // "Scale to print volume" makes a sense just for whole object @@ -1302,6 +1300,11 @@ void MenuFactory::create_object_menu() void MenuFactory::create_extra_object_menu() { + // Object instances / conversions + append_menu_items_instance_manipulation(&m_object_menu); + m_object_menu.AppendSeparator(); + append_menu_item_instance_to_object(&m_object_menu); + m_object_menu.AppendSeparator(); //append_menu_item_fill_bed(&m_object_menu); // Object Clone append_menu_item_clone(&m_object_menu); @@ -1796,6 +1799,26 @@ wxMenu* MenuFactory::assemble_multi_selection_menu() return menu; } + +//PS +void MenuFactory::append_menu_items_instance_manipulation(wxMenu* menu) +{ + MenuType type = menu == &m_object_menu ? mtObjectFFF : mtObjectSLA; + + items_increase[type] = append_menu_item(menu, wxID_ANY, _L("Add instance") + "\t+", _L("Add one more instance of the selected object"), + [](wxCommandEvent&) { plater()->increase_instances(); }, "", nullptr, + []() { return plater()->can_increase_instances(); }, m_parent); + items_decrease[type] = append_menu_item(menu, wxID_ANY, _L("Remove instance") + "\t-", _L("Remove one instance of the selected object"), + [](wxCommandEvent&) { plater()->decrease_instances(); }, "", nullptr, + []() { return plater()->can_decrease_instances(); }, m_parent); + items_set_number_of_copies[type] = append_menu_item(menu, wxID_ANY, _L("Set number of instances") + dots, _L("Change the number of instances of the selected object"), + [](wxCommandEvent&) { plater()->set_number_of_copies(); }, "", nullptr, + []() { return plater()->can_increase_instances(); }, m_parent); + append_menu_item(menu, wxID_ANY, _L("Fill bed with instances") + dots, _L("Fill the remaining area of bed with instances of the selected object"), + [](wxCommandEvent&) { plater()->fill_bed_with_instances(); }, "", nullptr, + []() { return plater()->can_increase_instances(); }, m_parent); +} + wxMenu *MenuFactory::filament_action_menu(int active_filament_menu_id) { create_filament_action_menu(false, active_filament_menu_id); return &m_filament_action_menu; diff --git a/src/slic3r/GUI/GUI_Factories.hpp b/src/slic3r/GUI/GUI_Factories.hpp index 45e50e2e32..17afce4fce 100644 --- a/src/slic3r/GUI/GUI_Factories.hpp +++ b/src/slic3r/GUI/GUI_Factories.hpp @@ -157,7 +157,7 @@ private: void append_menu_item_edit_text(wxMenu *menu); void append_menu_item_edit_svg(wxMenu *menu); - //void append_menu_items_instance_manipulation(wxMenu *menu); + void append_menu_items_instance_manipulation(wxMenu *menu); //void update_menu_items_instance_manipulation(MenuType type); //BBS add bbl menu item void append_menu_item_clone(wxMenu* menu); diff --git a/src/slic3r/GUI/GUI_ObjectList.cpp b/src/slic3r/GUI/GUI_ObjectList.cpp index 5ca5d0c28f..49c82ce50d 100644 --- a/src/slic3r/GUI/GUI_ObjectList.cpp +++ b/src/slic3r/GUI/GUI_ObjectList.cpp @@ -1735,12 +1735,12 @@ void ObjectList::key_event(wxKeyEvent& event) 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() == '+') + increase_instances(); + else if (event.GetUnicodeKey() == '-') + decrease_instances(); + else if (event.GetUnicodeKey() == 'p') + toggle_printable_state(); else if (filaments_count() > 1) { std::vector numbers = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' }; wxChar key_char = event.GetUnicodeKey(); diff --git a/src/slic3r/GUI/Jobs/FillBedJob.cpp b/src/slic3r/GUI/Jobs/FillBedJob.cpp index 91d6126a41..c1ff0b4584 100644 --- a/src/slic3r/GUI/Jobs/FillBedJob.cpp +++ b/src/slic3r/GUI/Jobs/FillBedJob.cpp @@ -169,16 +169,35 @@ void FillBedJob::prepare() ModelInstance *mi = model_object->instances[sel_id]; ArrangePolygon template_ap = get_instance_arrange_poly(mi, global_config); - for (int i = 0; i < needed_items; ++i) { + int obj_idx; + double offset_base, offset; + bool was_one_instance; + if (m_instances) { + obj_idx = m_plater->get_selected_object_idx(); + offset_base = m_plater->canvas3D()->get_size_proportional_to_max_bed_size(0.05); + offset = offset_base; + was_one_instance = model_object->instances.size()==1; + } + + for (int i = 0; i < needed_items; ++i, offset += offset_base) { ArrangePolygon ap = template_ap; ap.poly = m_selected.front().poly; ap.bed_idx = PartPlateList::MAX_PLATES_COUNT; ap.itemid = -1; - ap.setter = [this, mi](const ArrangePolygon &p) { + ap.setter = [this, mi, offset](const ArrangePolygon &p) { ModelObject *mo = m_plater->model().objects[m_object_idx]; - ModelObject* newObj = m_plater->model().add_object(*mo); - newObj->name = mo->name +" "+ std::to_string(p.itemid); - for (ModelInstance *newInst : newObj->instances) { newInst->apply_arrange_result(p.translation.cast(), p.rotation); } + ModelObject *obj; + if (m_instances) { + ModelInstance* model_instance = mo->instances.back(); + Vec3d offset_vec = model_instance->get_offset() + Vec3d(offset, offset, 0.0); + mo->add_instance(offset_vec, model_instance->get_scaling_factor(), model_instance->get_rotation(), model_instance->get_mirror()); + obj = mo; + } else { + ModelObject* newObj = m_plater->model().add_object(*mo); + newObj->name = mo->name +" "+ std::to_string(p.itemid); + obj = newObj; + } + for (ModelInstance *newInst : obj->instances) { newInst->apply_arrange_result(p.translation.cast(), p.rotation); } //m_plater->sidebar().obj_list()->paste_objects_into_list({m_plater->model().objects.size()-1}); }; m_selected.emplace_back(ap); @@ -256,7 +275,7 @@ void FillBedJob::process(Ctl &ctl) _u8L("Bed filling done.")); } -FillBedJob::FillBedJob() : m_plater{wxGetApp().plater()} {} +FillBedJob::FillBedJob(bool instances) : m_plater{wxGetApp().plater()}, m_instances{instances} {} void FillBedJob::finalize(bool canceled, std::exception_ptr &eptr) { @@ -323,6 +342,10 @@ void FillBedJob::finalize(bool canceled, std::exception_ptr &eptr) //model_object->ensure_on_bed(); //BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ": model_object->ensure_on_bed()"; + if (m_instances && wxGetApp().app_config->get("auto_arrange") == "true") { + m_plater->set_prepare_state(Job::PREPARE_STATE_MENU); + m_plater->arrange(); + } m_plater->update(); } } diff --git a/src/slic3r/GUI/Jobs/FillBedJob.hpp b/src/slic3r/GUI/Jobs/FillBedJob.hpp index 20672e0f37..28950aeac8 100644 --- a/src/slic3r/GUI/Jobs/FillBedJob.hpp +++ b/src/slic3r/GUI/Jobs/FillBedJob.hpp @@ -26,12 +26,14 @@ class FillBedJob : public Job int m_status_range = 0; Plater *m_plater; + bool m_instances; + public: void prepare(); void process(Ctl &ctl) override; - FillBedJob(); + FillBedJob(bool instances = false); int status_range() const { diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 778280970a..65cd061a12 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -13604,8 +13604,6 @@ void Plater::remove_selected() void Plater::increase_instances(size_t num) { - // BBS -#if 0 if (! can_increase_instances()) { return; } Plater::TakeSnapshot snapshot(this, "Increase Instances"); @@ -13638,13 +13636,14 @@ void Plater::increase_instances(size_t num) p->selection_changed(); this->p->schedule_background_process(); -#endif + if (wxGetApp().app_config->get("auto_arrange") == "true") { + this->set_prepare_state(Job::PREPARE_STATE_MENU); + this->arrange(); + } } void Plater::decrease_instances(size_t num) { - // BBS -#if 0 if (! can_decrease_instances()) { return; } Plater::TakeSnapshot snapshot(this, "Decrease Instances"); @@ -13668,7 +13667,10 @@ void Plater::decrease_instances(size_t num) p->selection_changed(); this->p->schedule_background_process(); -#endif + if (wxGetApp().app_config->get("auto_arrange") == "true") { + this->set_prepare_state(Job::PREPARE_STATE_MENU); + this->arrange(); + } } static long GetNumberFromUser( const wxString& msg, @@ -13713,7 +13715,7 @@ void Plater::set_number_of_copies(/*size_t num*/) decrease_instances(-diff); } -void Plater::fill_bed_with_instances() +void Plater::fill_bed_with_copies() { auto &w = get_ui_job_worker(); if (w.is_idle()) { @@ -13722,6 +13724,15 @@ void Plater::fill_bed_with_instances() } } +void Plater::fill_bed_with_instances() +{ + auto &w = get_ui_job_worker(); + if (w.is_idle()) { + p->take_snapshot(_u8L("Arrange")); + replace_job(w, std::make_unique(true)); + } +} + bool Plater::is_selection_empty() const { return p->get_selection().is_empty() || p->get_selection().is_wipe_tower(); diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index fa235a84d3..b110e46bbe 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -460,6 +460,7 @@ public: void increase_instances(size_t num = 1); void decrease_instances(size_t num = 1); void set_number_of_copies(/*size_t num*/); + void fill_bed_with_copies(); void fill_bed_with_instances(); bool is_selection_empty() const; void scale_selection_to_fit_print_volume();