refactor(imex): make render the sole author of ghost RGB; scope cache key to mesh inputs

Follow-up to the live ghost-color fix, addressing review findings: with the
render loop restamping ghost colors every frame, the bake-time resolution in
calc_imex_ghosts was dead code (its RGB was displayed for zero frames), and
the pem + head-filament-map entries in the ghost cache key had become
color-only inputs that forced a full mesh re-bake — including a visible
hitch on every ghost-picker selection — for what is now a pure recolor.

- calc_imex_ghosts bakes an alpha-only placeholder; IMEX_GHOST_ALPHA is
  hoisted to file scope as the single opacity authority (no more reading
  alpha back out of the field the restamp overwrites).
- build_imex_ghost_cache_key drops pem and the head-filament map; the
  forced invalidations in set/reset_imex_head_filament_map go with them.
  Picker selections now recolor live with no rebuild.
- The restamp moves into PartPlate::update_imex_ghost_colors(), beside the
  transform refresh, so PartPlate owns its volumes' colors and the canvas
  calls one hook. Plate-level inputs (pem, override map) are hoisted once
  per frame via a new get_imex_head_filament_color overload that the
  single-head form delegates to, keeping tooltip parity by construction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Clifford Garwood
2026-08-14 16:39:14 -04:00
co-authored by Claude Fable 5
parent f41752546c
commit 28c0c681fe
3 changed files with 67 additions and 32 deletions
+4 -14
View File
@@ -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<int, ColorRGBA> 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",
+54 -18
View File
@@ -438,14 +438,14 @@ void PartPlate::set_imex_head_filament_map(const std::map<int,int>& 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<int, int>& 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<GLVolume>(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<int, ColorRGBA> 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.
+9
View File
@@ -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<int, int>& 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<std::optional<Transform3d>(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"; }