fix(imex): scope ghost render + picking to the active plate

_render_imex_ghosts and _picking_pass_imex_ghosts both walked every
plate in the partplate list and rendered/hit-tested all ghosts found —
which meant background plates' ghosts bled through into the active
scene whenever GL state was shared (most visibly when entering paint
mode), and clicks could land on a ghost that belonged to a plate the
user wasn't actually looking at.

Switch both paths to read get_curr_plate() and skip the per-plate loop.
Per-plate ghost volumes still live on each PartPlate so 3MF
round-trips work and switching plates picks up the new active plate's
ghosts cleanly; we just don't draw or hit-test the ones whose plate
isn't the active one.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Clifford Garwood
2026-04-30 23:01:15 -04:00
co-authored by Claude Opus 4.7
parent 2c604173d3
commit a8dbdf90de
+17 -12
View File
@@ -7305,18 +7305,19 @@ void GLCanvas3D::_picking_pass_imex_ghosts()
return tmax >= 0.0;
};
// Match _render_imex_ghosts: only hit-test ghosts on the active plate. Picking
// through ghosts on background plates would hand the user a stale plate index
// for the head-filament popover and let them edit a plate they aren't looking at.
PartPlateList& ppl = wxGetApp().plater()->get_partplate_list();
for (int pi = 0; pi < ppl.get_plate_count(); ++pi) {
PartPlate* plate = ppl.get_plate(pi);
if (!plate) continue;
const auto& ghosts = plate->get_imex_ghost_volumes();
if (ghosts.empty()) continue;
PartPlate* active_plate = ppl.get_curr_plate();
if (active_plate) {
const auto& ghosts = active_plate->get_imex_ghost_volumes();
for (const auto& g : ghosts) {
if (!g || !g->is_active || !g->picking) continue;
const BoundingBoxf3 bbox = g->transformed_bounding_box();
if (ray_hits_bbox(ray_origin, ray_dir, bbox)) {
m_hover_ghost_head = PartPlate::imex_ghost_head_from_composite_id(g->composite_id.object_id);
m_hover_ghost_plate = pi;
m_hover_ghost_plate = ppl.get_curr_plate_index();
return; // first hit wins
}
}
@@ -7355,14 +7356,18 @@ void GLCanvas3D::_render_imex_ghosts()
shader->set_uniform("print_volume.type", -1);
shader->set_uniform("slope.actived", false);
// Only render ghosts for the active plate. Iterating every plate here causes
// ghosts from background plates to bleed through into the active scene
// (e.g. when entering paint mode), since the GL state is shared across the
// whole canvas. Per-plate ghost volumes still live on each PartPlate so they
// round-trip through 3MF saves; we just don't draw them when their plate
// isn't the one the user is currently looking at.
PartPlateList& ppl = wxGetApp().plater()->get_partplate_list();
for (int pi = 0; pi < ppl.get_plate_count(); ++pi) {
PartPlate* plate = ppl.get_plate(pi);
if (!plate) continue;
PartPlate* active_plate = ppl.get_curr_plate();
if (active_plate) {
// Refresh per-frame so ghost positions reflect the primary's live drag state.
plate->update_imex_ghost_transforms(primary_live_xf);
const auto& ghosts = plate->get_imex_ghost_volumes();
if (ghosts.empty()) continue;
active_plate->update_imex_ghost_transforms(primary_live_xf);
const auto& ghosts = active_plate->get_imex_ghost_volumes();
for (const auto& g : ghosts) {
if (!g || !g->is_active) continue;
const Transform3d model_matrix = g->world_matrix();