diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 1392e564a0..6567f97470 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -7556,24 +7556,14 @@ void GLCanvas3D::_render_imex_ghosts() PartPlateList& ppl = wxGetApp().plater()->get_partplate_list(); PartPlate* active_plate = ppl.get_curr_plate(); if (active_plate) { - // Refresh per-frame so ghost positions reflect the primary's live drag state. + // Refresh per-frame so ghost positions reflect the primary's live drag state, + // and restamp ghost RGB from the live filament palette so color-only changes + // (palette edits, late-loading project colors) never leave stale ghosts. active_plate->update_imex_ghost_transforms(primary_live_xf); + active_plate->update_imex_ghost_colors(); const auto& ghosts = active_plate->get_imex_ghost_volumes(); - // Re-resolve ghost colors live instead of trusting the bake: the ghost cache key - // covers mesh-shaping inputs (mode, pem, object set) but not filament_colour, so - // a palette edit or a late-loading project color would leave stale — or - // UNPRINTABLE-black — ghosts until an unrelated rebuild. This is the same - // resolution the hover tooltip runs, so the two can never disagree. - std::map head_colors; for (const auto& g : ghosts) { if (!g || !g->is_active) continue; - const int head = PartPlate::imex_ghost_head_from_composite_id(g->composite_id.object_id); - auto hc = head_colors.find(head); - if (hc == head_colors.end()) - hc = head_colors.emplace(head, active_plate->get_imex_head_filament_color(head)).first; - ColorRGBA color = hc->second; - color.a(g->color.a()); // keep the ghost alpha baked by calc_imex_ghosts - g->color = color; const Transform3d model_matrix = g->world_matrix(); shader->set_uniform("volume_world_matrix", model_matrix); shader->set_uniform("slope.volume_world_normal_matrix", diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index 57a326e125..0b58dabcc4 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -438,14 +438,14 @@ void PartPlate::set_imex_head_filament_map(const std::map& m) m_config.set_key_value("imex_head_filament_map", new ConfigOptionString(os.str())); } update_slice_result_valid_state(false); - m_imex_ghost_cache_key = "\x01"; // force ghost rebuild + // No ghost rebuild: routing only affects color, which update_imex_ghost_colors + // restamps live every frame. } void PartPlate::reset_imex_head_filament_map() { m_config.erase("imex_head_filament_map"); update_slice_result_valid_state(false); - m_imex_ghost_cache_key = "\x01"; } ColorRGBA PartPlate::get_imex_head_filament_color(int physical_head) const @@ -454,8 +454,19 @@ ColorRGBA PartPlate::get_imex_head_filament_color(int physical_head) const if (!pb) return GLVolume::UNPRINTABLE_COLOR; - const ConfigOptionInts pem = effective_physical_extruder_map(*pb); - const auto plate_map = get_imex_head_filament_map(); + return get_imex_head_filament_color(physical_head, + effective_physical_extruder_map(*pb), + get_imex_head_filament_map()); +} + +ColorRGBA PartPlate::get_imex_head_filament_color(int physical_head, + const ConfigOptionInts& pem, + const std::map& plate_map) const +{ + auto* pb = wxGetApp().preset_bundle; + if (!pb) + return GLVolume::UNPRINTABLE_COLOR; + const int logical = resolve_filament_for_head(plate_map, pem, physical_head); if (logical < 0) return GLVolume::UNPRINTABLE_COLOR; @@ -1035,23 +1046,16 @@ void PartPlate::ensure_imex_zones() std::string PartPlate::build_imex_ghost_cache_key() const { - // Ghost shape depends on: mode topology (same inputs as zone key) + pem + - // set of objects on plate + each primary instance's transform. + // Ghost shape depends on: mode topology (same inputs as zone key) + set of + // objects on plate + each primary instance's transform. Filament routing and + // colors are deliberately NOT keyed: ghost RGB is restamped live every frame + // by update_imex_ghost_colors, so color-only changes never need a mesh rebuild. std::string k = build_imex_cache_key(); if (k.empty()) return ""; // ghost-off when zones-off - if (auto* pb = wxGetApp().preset_bundle) { - // Keyed on the effective pem (project → printer → pei-derived) so a printer - // swap that only changes printer_extruder_id still invalidates the ghost cache. - const ConfigOptionInts pem = effective_physical_extruder_map(*pb); - k += "|pem"; - for (int v : pem.values) { k += ':'; k += std::to_string(v); } - } k += "|obj"; for (const auto& oi : obj_to_instance_set) k += std::to_string(oi.first) + "." + std::to_string(oi.second) + ","; - for (const auto& kv : get_imex_head_filament_map()) - k += "|m" + std::to_string(kv.first) + "=" + std::to_string(kv.second); return k; } @@ -1095,6 +1099,10 @@ bool PartPlate::resolve_active_mode_tools(std::string& out_tools_str, int& out_p return true; } +// Ghost opacity. RGB is owned by update_imex_ghost_colors (restamped per frame); +// calc_imex_ghosts only bakes this alpha into the volume. +static constexpr float IMEX_GHOST_ALPHA = 0.55f; + void PartPlate::calc_imex_ghosts() { m_imex_ghost_volumes.clear(); @@ -1184,7 +1192,6 @@ void PartPlate::calc_imex_ghosts() return {aggregated_primary, aggregated_target - aggregated_primary}; }; - constexpr float GHOST_ALPHA = 0.55f; // Mesh is object-local and identical across all instances and heads of a given object. // Build the merged TriangleMesh once per obj_idx and reuse across the inner head loop. @@ -1231,8 +1238,9 @@ void PartPlate::calc_imex_ghosts() primary_phys, phys, role, gantry, pri_center, mirror_axis_for(phys)); const Transform3d ghost_xf = head_xf * inst_world; - ColorRGBA color = get_imex_head_filament_color(phys); - color.a(GHOST_ALPHA); + // RGB here is a placeholder — update_imex_ghost_colors restamps every + // ghost from the live filament palette each frame; only alpha matters. + const ColorRGBA color = {0.5f, 0.5f, 0.5f, IMEX_GHOST_ALPHA}; auto ghost = std::make_unique(color); ghost->set_instance_transformation(ghost_xf); @@ -1346,6 +1354,34 @@ void PartPlate::update_imex_ghost_transforms( } } +// Restamp every ghost's RGB from the current filament palette. Called per frame +// from the render loop, so ghosts can never go stale against filament preset or +// color changes — the ghost cache key deliberately ignores color-only inputs. +// This is the same resolution the hover tooltip runs (resolve_filament_for_head +// over the effective pem + per-plate override map), so the two cannot disagree. +void PartPlate::update_imex_ghost_colors() +{ + if (m_imex_ghost_volumes.empty() || !m_plater) return; + auto* pb = wxGetApp().preset_bundle; + if (!pb) return; + + // Plate-level inputs hoisted once; per-head results memoized across instances. + const ConfigOptionInts pem = effective_physical_extruder_map(*pb); + const auto plate_map = get_imex_head_filament_map(); + + std::map head_colors; + for (auto& ghost : m_imex_ghost_volumes) { + if (!ghost) continue; + const int head = imex_ghost_head_from_composite_id(ghost->composite_id.object_id); + auto hc = head_colors.find(head); + if (hc == head_colors.end()) + hc = head_colors.emplace(head, get_imex_head_filament_color(head, pem, plate_map)).first; + ColorRGBA color = hc->second; + color.a(IMEX_GHOST_ALPHA); + ghost->color = color; + } +} + // True if `hull` (scaled, plate-list coordinates) overlaps a secondary zone or a // carriage danger strip. Shared by the object and prime-tower checks so both use one // definition of "overlap" — see imex_hull_violates_zones() for those semantics. diff --git a/src/slic3r/GUI/PartPlate.hpp b/src/slic3r/GUI/PartPlate.hpp index def9d99293..3d629f0fdf 100644 --- a/src/slic3r/GUI/PartPlate.hpp +++ b/src/slic3r/GUI/PartPlate.hpp @@ -343,6 +343,11 @@ public: // respecting any imex_head_filament_map override. Falls back to GLVolume::UNPRINTABLE_COLOR // when no filament routes to the head. ColorRGBA get_imex_head_filament_color(int physical_head) const; + // Same resolution against caller-hoisted plate-level inputs (hot paths that + // resolve several heads should hoist pem/plate_map once and use this form). + ColorRGBA get_imex_head_filament_color(int physical_head, + const ConfigOptionInts& pem, + const std::map& plate_map) const; // Visual ghosts of every object on this plate, one per active secondary head. // Non-selectable, non-draggable. See IMEX ghost renderer spec. @@ -357,6 +362,10 @@ public: void update_imex_ghost_transforms( const std::function(int, int)>& primary_live_xf = {}); + // Restamp ghost RGB from the live filament palette (per frame, from the render + // loop). Color is deliberately not part of the ghost cache key — see the .cpp. + void update_imex_ghost_colors(); + // Invalidate the ghost cache so the next render_imex_zones() call rebuilds fully. void invalidate_imex_ghosts() { m_imex_ghost_cache_key = "\x01"; }