Register Instance Copies and Moves with Their Plate (#15613)

# Description

Each plate keeps a registry of the instances it holds
(`PartPlate::obj_to_instance_set`). The plate's filament list
(`get_extruders`), its wipe tower preview and position clamp, the object
list grouping and the saved project's per-plate instance list all read
it. Two paths left it stale:

* `Plater::increase_instances` (the `+` key / toolbar) adds the copy to
the model but never registers it with any plate.
* `GLCanvas3D::do_move` (drag release and arrow keys) ended with
`notify_instance_update(-1, 0)`, so only instance 0 of each selected
object was re-registered. Rotate, scale and mirror already notify every
instance.

So a copy created with `+` and dragged onto another plate stayed unknown
to that plate: the project saved afterwards listed it on no plate, and a
multi-filament copy moved onto a single-filament plate drew no wipe
tower there and never got its tower position clamped. The Print side
selects instances by geometry, so the plate still sliced, which is why
this went unnoticed.

This PR

* registers new copies with their plate at creation;
* has `do_move` notify exactly the instances it moved (every instance of
the object when a part was moved in Volume mode), rather than instance 0
or all instances - notifying an instance that stayed put invalidates its
plate's slice result, so `(-1, -1)` as used by rotate would have
un-sliced every plate holding a sibling copy;
* drops the registry entry again when `decrease_instances` removes a
copy.

A second commit finishes the switch #15532 started with
`contain_any_instance_totally()`: `get_extruders_without_support()`,
`check_single_extruder_mixed_filament_risk()` and
`check_compatible_of_nozzle_and_filament()` still tested instance 0
only, so an object whose copy - not its original - sits on the plate was
skipped by all three.

No new options, no format change. The `is_new` flag is deliberately not
passed for the copies: a copy landing on a spiral-vase plate gets the
same "apply spiral mode settings?" prompt a dragged instance gets,
instead of a silent rewrite of the object's settings.

# Screenshots/Recordings/Graphs
Before:
<img width="1920" height="1080" alt="05-moved"
src="https://github.com/user-attachments/assets/3cf9f5a9-1a4e-41e8-8c57-578f849d8c29"
/>

After:
<img width="1920" height="1080" alt="05-moved"
src="https://github.com/user-attachments/assets/1b801a7e-b7cd-4ffb-bd1d-b701f90dade6"
/>


## Tests

Re-run after the rebase, both binaries driven through the same headless
harness (Xvfb 1920x1080, llvmpipe) on the same fixture: `cubeA`
(filament 1) alone on plate 1, `cubeB` (a two-part object, filaments 2
and 1) alone on plate 2, so plate 1 shows no wipe tower at load. Select
the plate-2 object, press `+`, walk the copy onto plate 1 with 36 x Left
(10 mm per press, one `do_move` each), save, slice plate 1.

Before is main `8af92214d0` - i.e. with #15532's
`contain_any_instance_totally()` already in place, so the only
difference is this PR.

* **Before:** the saved `model_settings.config` lists plate 1 with
`cubeA` only and plate 2 with `cubeB` instance 0. The copy (instance 1)
is listed **on no plate at all**, and plate 1 draws no wipe tower even
though a two-filament object is sitting on it.
* **After:** plate 1 lists `cubeA` **and** `cubeB` instance 1; plate 2
still lists instance 0. The plate-1 tower preview appears, and slicing
plate 1 succeeds with the tower actually generated - the filament panel
reports 1.10 m / 0.48 m in its Tower column over 51 filament changes,
and the G-code carries `EXCLUDE_OBJECT_END NAME=cubeB.stl_id_1_copy_0`.

Same camera and fixture on both runs, so the screenshots above are
directly comparable.
This commit is contained in:
HanifKoh
2026-09-11 11:18:36 +08:00
committed by GitHub
3 changed files with 25 additions and 5 deletions

View File

@@ -5059,7 +5059,21 @@ void GLCanvas3D::do_move(const std::string& snapshot_type)
} }
//BBS: notify instance updates to part plater list //BBS: notify instance updates to part plater list
m_selection.notify_instance_update(-1, 0); // Only what moved: the selected instances, or every instance of an object one of whose
// 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.
{
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());
if (notified.insert(key).second)
m_selection.notify_instance_update(key.first, key.second);
}
}
// Fixes sinking/flying instances (snaps object to buildplate) // Fixes sinking/flying instances (snaps object to buildplate)
for (const std::pair<int, int>& i : done) { for (const std::pair<int, int>& i : done) {

View File

@@ -1917,7 +1917,7 @@ std::vector<int> PartPlate::get_extruders_without_support(bool conside_custom_gc
const DynamicPrintConfig& glb_config = wxGetApp().preset_bundle->prints.get_edited_preset().config; const DynamicPrintConfig& glb_config = wxGetApp().preset_bundle->prints.get_edited_preset().config;
for (int obj_idx = 0; obj_idx < m_model->objects.size(); obj_idx++) { for (int obj_idx = 0; obj_idx < m_model->objects.size(); obj_idx++) {
if (!contain_instance_totally(obj_idx, 0)) if (!contain_any_instance_totally(obj_idx))
continue; continue;
ModelObject* mo = m_model->objects[obj_idx]; ModelObject* mo = m_model->objects[obj_idx];
@@ -2088,7 +2088,7 @@ bool PartPlate::check_single_extruder_mixed_filament_risk(const DynamicPrintConf
"which may significantly increase waste and the risk of nozzle / waste-chute clogging."); "which may significantly increase waste and the risk of nozzle / waste-chute clogging.");
for (int obj_idx = 0; obj_idx < (int)m_model->objects.size(); ++obj_idx) { for (int obj_idx = 0; obj_idx < (int)m_model->objects.size(); ++obj_idx) {
if (!contain_instance_totally(obj_idx, 0)) if (!contain_any_instance_totally(obj_idx))
continue; continue;
ModelObject *mo = m_model->objects[obj_idx]; ModelObject *mo = m_model->objects[obj_idx];
int obj_ext = mo->config.has("extruder") ? mo->config.extruder() : 1; int obj_ext = mo->config.has("extruder") ? mo->config.extruder() : 1;
@@ -2307,7 +2307,7 @@ bool PartPlate::check_compatible_of_nozzle_and_filament(const DynamicPrintConfig
return wipe_tower_size; return wipe_tower_size;
for (int obj_idx = 0; obj_idx < m_model->objects.size(); obj_idx++) { for (int obj_idx = 0; obj_idx < m_model->objects.size(); obj_idx++) {
if (!use_global_objects && !contain_instance_totally(obj_idx, 0)) if (!use_global_objects && !contain_any_instance_totally(obj_idx))
continue; continue;
BoundingBoxf3 bbox = m_model->objects[obj_idx]->bounding_box(); BoundingBoxf3 bbox = m_model->objects[obj_idx]->bounding_box();

View File

@@ -17839,6 +17839,10 @@ void Plater::increase_instances(size_t num)
model_object->add_instance(offset_vec, model_instance->get_scaling_factor(), model_instance->get_rotation(), model_instance->get_mirror()); model_object->add_instance(offset_vec, model_instance->get_scaling_factor(), model_instance->get_rotation(), model_instance->get_mirror());
// p->print.get_object(obj_idx)->add_copy(Slic3r::to_2d(offset_vec)); // p->print.get_object(obj_idx)->add_copy(Slic3r::to_2d(offset_vec));
} }
// Register the copies with the plate they land on before the scene reloads: the plate's
// filament list and wipe tower preview are read from that registry.
for (size_t i = model_object->instances.size() - num; i < model_object->instances.size(); ++i)
p->partplate_list.notify_instance_update(obj_idx, static_cast<int>(i));
#ifdef SUPPORT_AUTO_CENTER #ifdef SUPPORT_AUTO_CENTER
if (p->get_config("autocenter") == "true") if (p->get_config("autocenter") == "true")
@@ -17869,8 +17873,10 @@ void Plater::decrease_instances(size_t num)
ModelObject* model_object = p->model.objects[obj_idx]; ModelObject* model_object = p->model.objects[obj_idx];
if (model_object->instances.size() > num) { if (model_object->instances.size() > num) {
for (size_t i = 0; i < num; ++ i) for (size_t i = 0; i < num; ++ i) {
p->partplate_list.notify_instance_removed(obj_idx, static_cast<int>(model_object->instances.size()) - 1);
model_object->delete_last_instance(); model_object->delete_last_instance();
}
p->update(); p->update();
// Delete object from Sidebar list. Do it after update, so that the GLScene selection is updated with the modified model. // Delete object from Sidebar list. Do it after update, so that the GLScene selection is updated with the modified model.
sidebar().obj_list()->decrease_object_instances(obj_idx, num); sidebar().obj_list()->decrease_object_instances(obj_idx, num);