Skip to content

Commit ea5d2bf

Browse files
committed
feat: link identical copies as instances
(cherry picked from commit 791e15e)
1 parent 8ca1488 commit ea5d2bf

4 files changed

Lines changed: 291 additions & 0 deletions

File tree

src/slic3r/GUI/GUI_Factories.cpp

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1144,6 +1144,24 @@ void MenuFactory::append_menu_items_convert_unit(wxMenu* menu)
11441144
}
11451145
}
11461146

1147+
void MenuFactory::append_menu_item_link_copies_as_instances(wxMenu *menu)
1148+
{
1149+
append_menu_item(
1150+
menu,
1151+
wxID_ANY,
1152+
_L("Link selected copies as instances"),
1153+
_L("Convert the selected identical object copies into linked instances"),
1154+
[](wxCommandEvent &) {
1155+
obj_list()->link_selected_copies_as_instances();
1156+
},
1157+
"",
1158+
menu,
1159+
[]() {
1160+
return obj_list()->can_link_selected_copies_as_instances();
1161+
},
1162+
m_parent);
1163+
}
1164+
11471165
void MenuFactory::append_menu_item_merge_to_multipart_object(wxMenu* menu)
11481166
{
11491167
append_menu_item(menu, wxID_ANY, _L("Merge"), _L("Assemble the selected objects to an object with multiple parts"),
@@ -1822,6 +1840,10 @@ wxMenu* MenuFactory::multi_selection_menu()
18221840
wxMenu* menu = new MenuWithSeparators();
18231841
if (!multi_volume) {
18241842
int index = 0;
1843+
if (obj_list()->can_link_selected_copies_as_instances()) {
1844+
append_menu_item_link_copies_as_instances(menu);
1845+
index++;
1846+
}
18251847
if (obj_list()->can_merge_to_multipart_object()) {
18261848
append_menu_item_merge_to_multipart_object(menu);
18271849
index++;

src/slic3r/GUI/GUI_Factories.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ class MenuFactory
157157
void append_menu_item_scale_selection_to_fit_print_volume(wxMenu* menu);
158158
void append_menu_items_convert_unit(wxMenu* menu); // Add "Conver/Revert..." menu items (from/to inches/meters) after "Reload From Disk"
159159
void append_menu_items_flush_options(wxMenu* menu);
160+
void append_menu_item_link_copies_as_instances(wxMenu *menu);
160161
void append_menu_item_merge_to_multipart_object(wxMenu *menu);
161162
void append_menu_item_merge_to_single_object(wxMenu* menu);
162163
void append_menu_item_merge_parts_to_single_part(wxMenu *menu);

src/slic3r/GUI/GUI_ObjectList.cpp

Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3077,6 +3077,99 @@ void ObjectList::split()
30773077
update_info_items(obj_idx);
30783078
}
30793079

3080+
void ObjectList::link_selected_copies_as_instances()
3081+
{
3082+
if (!can_link_selected_copies_as_instances())
3083+
return;
3084+
3085+
wxDataViewItemArray selections;
3086+
GetSelections(selections);
3087+
3088+
std::vector<int> object_indices;
3089+
object_indices.reserve(selections.size());
3090+
3091+
for (const wxDataViewItem &item : selections)
3092+
object_indices.emplace_back(
3093+
m_objects_model->GetIdByItem(item));
3094+
3095+
std::sort(object_indices.begin(), object_indices.end());
3096+
object_indices.erase(
3097+
std::unique(object_indices.begin(), object_indices.end()),
3098+
object_indices.end());
3099+
3100+
const size_t master_idx =
3101+
static_cast<size_t>(object_indices.front());
3102+
3103+
ModelObject *master = (*m_objects)[master_idx];
3104+
Model *model = master->get_model();
3105+
3106+
Plater::TakeSnapshot snapshot(
3107+
wxGetApp().plater(),
3108+
"Link Copies as Instances");
3109+
3110+
wxBusyCursor wait;
3111+
3112+
for (size_t idx = 1; idx < object_indices.size(); ++idx) {
3113+
const ModelObject *source =
3114+
(*m_objects)[object_indices[idx]];
3115+
3116+
master->add_instance(*source->instances.front());
3117+
}
3118+
3119+
PartPlateList &partplate_list =
3120+
wxGetApp().plater()->get_partplate_list();
3121+
3122+
const bool previous_prevent_list_events =
3123+
m_prevent_list_events;
3124+
m_prevent_list_events = true;
3125+
3126+
// Remove sources from highest to lowest index so the master index and all
3127+
// remaining source indices stay valid throughout the operation.
3128+
for (auto it = object_indices.rbegin();
3129+
it != object_indices.rend();
3130+
++it) {
3131+
const size_t source_idx = static_cast<size_t>(*it);
3132+
3133+
if (source_idx == master_idx)
3134+
continue;
3135+
3136+
model->delete_object(source_idx);
3137+
partplate_list.notify_instance_removed(source_idx, -1);
3138+
}
3139+
3140+
// Register every instance on its current plate before the scene reload.
3141+
notify_instance_updated(static_cast<int>(master_idx));
3142+
3143+
// Reload the scene while the sidebar still contains the old object rows.
3144+
// This follows the existing object-deletion path and lets the GL selection
3145+
// synchronize with the modified model before the rows are removed.
3146+
wxGetApp().plater()->update();
3147+
3148+
for (auto it = object_indices.rbegin();
3149+
it != object_indices.rend();
3150+
++it) {
3151+
const size_t source_idx = static_cast<size_t>(*it);
3152+
3153+
if (source_idx != master_idx)
3154+
delete_object_from_list(source_idx);
3155+
}
3156+
3157+
m_prevent_list_events = previous_prevent_list_events;
3158+
3159+
// A single-instance object has no instance children in the sidebar.
3160+
// Create entries for the original instance and every converted copy.
3161+
increase_object_instances(
3162+
master_idx,
3163+
master->instances.size());
3164+
3165+
wxGetApp().plater()
3166+
->get_view3D_canvas3D()
3167+
->update_instance_printable_state_for_object(master_idx);
3168+
3169+
wxGetApp().plater()->object_list_changed();
3170+
update_info_items(master_idx);
3171+
}
3172+
30803173
void ObjectList::merge(bool to_multipart_object)
30813174
{
30823175
wxBusyCursor wait;
@@ -3596,6 +3689,179 @@ bool ObjectList::can_split_instances()
35963689
return selection.is_multiple_full_instance() || selection.is_single_full_instance();
35973690
}
35983691

3692+
static bool can_link_model_objects_as_instances(
3693+
const ModelObject &master,
3694+
const ModelObject &candidate)
3695+
{
3696+
if (master.instances.size() != 1 ||
3697+
candidate.instances.size() != 1)
3698+
return false;
3699+
3700+
if (master.is_cut() || candidate.is_cut())
3701+
return false;
3702+
3703+
if (master.volumes.size() != candidate.volumes.size())
3704+
return false;
3705+
3706+
if (master.module_name != candidate.module_name)
3707+
return false;
3708+
3709+
if (master.printable != candidate.printable)
3710+
return false;
3711+
3712+
if (!master.origin_translation.isApprox(
3713+
candidate.origin_translation))
3714+
return false;
3715+
3716+
if (master.config.get() != candidate.config.get())
3717+
return false;
3718+
3719+
if (master.layer_config_ranges !=
3720+
candidate.layer_config_ranges)
3721+
return false;
3722+
3723+
if (master.layer_height_profile.get() !=
3724+
candidate.layer_height_profile.get())
3725+
return false;
3726+
3727+
// These object-specific structures cannot be represented independently
3728+
// after the objects have been converted into instances.
3729+
if (!master.brim_points.empty() ||
3730+
!candidate.brim_points.empty() ||
3731+
!master.cut_connectors.empty() ||
3732+
!candidate.cut_connectors.empty() ||
3733+
!master.sla_support_points.empty() ||
3734+
!candidate.sla_support_points.empty() ||
3735+
!master.sla_drain_holes.empty() ||
3736+
!candidate.sla_drain_holes.empty())
3737+
return false;
3738+
3739+
for (size_t volume_idx = 0;
3740+
volume_idx < master.volumes.size();
3741+
++volume_idx) {
3742+
const ModelVolume &master_volume =
3743+
*master.volumes[volume_idx];
3744+
const ModelVolume &candidate_volume =
3745+
*candidate.volumes[volume_idx];
3746+
3747+
// Editable text and SVG metadata is object-specific and must not be
3748+
// silently discarded when the source object is removed.
3749+
if (master_volume.is_text() ||
3750+
candidate_volume.is_text() ||
3751+
master_volume.is_svg() ||
3752+
candidate_volume.is_svg())
3753+
return false;
3754+
3755+
if (master_volume.name != candidate_volume.name)
3756+
return false;
3757+
3758+
if (master_volume.type() != candidate_volume.type())
3759+
return false;
3760+
3761+
if (master_volume.material_id() !=
3762+
candidate_volume.material_id())
3763+
return false;
3764+
3765+
// Bambu object copies share the immutable TriangleMesh allocation.
3766+
// Pointer equality intentionally avoids treating separately imported,
3767+
// merely similar meshes as linked copies.
3768+
if (master_volume.mesh_ptr() != candidate_volume.mesh_ptr())
3769+
return false;
3770+
3771+
if (!(master_volume.get_transformation() ==
3772+
candidate_volume.get_transformation()))
3773+
return false;
3774+
3775+
if (master_volume.is_assemble_initialized() !=
3776+
candidate_volume.is_assemble_initialized())
3777+
return false;
3778+
3779+
if (master_volume.is_assemble_initialized() &&
3780+
!(master_volume.get_assemble_transformation() ==
3781+
candidate_volume.get_assemble_transformation()))
3782+
return false;
3783+
3784+
if (master_volume.get_origin_mesh_or_vertice_render() !=
3785+
candidate_volume.get_origin_mesh_or_vertice_render())
3786+
return false;
3787+
3788+
if (master_volume.config.get() !=
3789+
candidate_volume.config.get())
3790+
return false;
3791+
3792+
if (!master_volume.supported_facets.equals(
3793+
candidate_volume.supported_facets))
3794+
return false;
3795+
3796+
if (!master_volume.fuzzy_skin_facets.equals(
3797+
candidate_volume.fuzzy_skin_facets))
3798+
return false;
3799+
3800+
if (!master_volume.seam_facets.equals(
3801+
candidate_volume.seam_facets))
3802+
return false;
3803+
3804+
if (!master_volume.mmu_segmentation_facets.equals(
3805+
candidate_volume.mmu_segmentation_facets))
3806+
return false;
3807+
3808+
if (!master_volume.exterior_facets.equals(
3809+
candidate_volume.exterior_facets))
3810+
return false;
3811+
}
3812+
3813+
return true;
3814+
}
3815+
3816+
bool ObjectList::can_link_selected_copies_as_instances() const
3817+
{
3818+
if (printer_technology() == ptSLA)
3819+
return false;
3820+
3821+
wxDataViewItemArray selections;
3822+
GetSelections(selections);
3823+
3824+
if (selections.size() < 2)
3825+
return false;
3826+
3827+
std::vector<int> object_indices;
3828+
object_indices.reserve(selections.size());
3829+
3830+
for (const wxDataViewItem &item : selections) {
3831+
if (m_objects_model->GetItemType(item) != itObject)
3832+
return false;
3833+
3834+
const int object_idx = m_objects_model->GetIdByItem(item);
3835+
3836+
if (object_idx < 0 ||
3837+
object_idx >= static_cast<int>(m_objects->size()))
3838+
return false;
3839+
3840+
object_indices.emplace_back(object_idx);
3841+
}
3842+
3843+
std::sort(object_indices.begin(), object_indices.end());
3844+
object_indices.erase(
3845+
std::unique(object_indices.begin(), object_indices.end()),
3846+
object_indices.end());
3847+
3848+
if (object_indices.size() < 2)
3849+
return false;
3850+
3851+
const ModelObject &master =
3852+
*(*m_objects)[object_indices.front()];
3853+
3854+
for (size_t idx = 1; idx < object_indices.size(); ++idx) {
3855+
const ModelObject &candidate =
3856+
*(*m_objects)[object_indices[idx]];
3857+
3858+
if (!can_link_model_objects_as_instances(master, candidate))
3859+
return false;
3860+
}
3861+
3862+
return true;
3863+
}
3864+
35993865
bool ObjectList::can_merge_to_multipart_object() const
36003866
{
36013867
if (has_selected_cut_object())

src/slic3r/GUI/GUI_ObjectList.hpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,7 @@ class ObjectList : public wxDataViewCtrl
319319
bool del_subobject_from_object(const int obj_idx, const int idx, const int type);
320320
void del_info_item(const int obj_idx, InfoItemType type);
321321
void split();
322+
void link_selected_copies_as_instances();
322323
void merge(bool to_multipart_object);
323324
void merge_volumes(); // BBS: merge parts to single part
324325
void layers_editing();
@@ -332,6 +333,7 @@ class ObjectList : public wxDataViewCtrl
332333
bool is_splittable(bool to_objects);
333334
bool selected_instances_of_same_object();
334335
bool can_split_instances();
336+
bool can_link_selected_copies_as_instances() const;
335337
bool can_merge_to_multipart_object() const;
336338
bool can_merge_to_single_object() const;
337339
bool can_mesh_boolean() const;

0 commit comments

Comments
 (0)