@@ -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+
30803173void 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+
35993865bool ObjectList::can_merge_to_multipart_object () const
36003866{
36013867 if (has_selected_cut_object ())
0 commit comments