fix(imex): resolve ghost colors live at render time, not at bake time

Ghost GLVolumes baked their filament color once in calc_imex_ghosts and
only rebuilt when build_imex_ghost_cache_key changed. The key covers the
mesh-shaping inputs (mode topology, pem, object set, per-plate head map)
but not filament_colour, and no invalidation hook fires on filament
preset or color changes — so a ghost baked under a transient palette
(late-loading project colors, a subsequently edited filament color)
kept the stale color forever. A failed lookup at bake time baked
GLVolume::UNPRINTABLE_COLOR, which renders as a jet-black ghost while
the hover tooltip — which re-resolves live — shows the correct color.

Re-stamp each ghost's color in _render_imex_ghosts from
get_imex_head_filament_color, the same resolution the tooltip runs, so
the two can never disagree. Resolution is hoisted into a per-head map
so instances sharing a head resolve once per frame; the alpha baked by
calc_imex_ghosts is preserved. The render loop already stamps color per
ghost per frame (set_render_color + model.set_color), so the added cost
is a couple of map lookups. The cache key stays scoped to what it
actually protects: the expensive mesh bake and transforms.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Clifford Garwood
2026-08-10 07:45:50 -04:00
co-authored by Claude Fable 5
parent ec309e1cc6
commit bdbdd8e222
+13
View File
@@ -7559,8 +7559,21 @@ void GLCanvas3D::_render_imex_ghosts()
// Refresh per-frame so ghost positions reflect the primary's live drag state.
active_plate->update_imex_ghost_transforms(primary_live_xf);
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",