fix(imex): key the resolved tool roster and fw-managed flag in the zone/ghost cache

Review follow-up. Removing pem from the ghost cache key also removed its
accidental role as the key's only printer-identity signal: the resolved
active-tools string (roster + primary) and imex_firmware_managed_zones both
shape the baked zone/ghost set but were never keyed directly, so a printer
swap between presets with matching mode names and topology could leave a
stale ghost set. Key all three in build_imex_cache_key, which also hardens
the zone cache against the same pre-existing gap.

Also from review: the tooltip swatch reuses the pem/map its label already
hoisted (one resolution, not two); the bake constructs ghosts with no color
at all, making update_imex_ghost_colors the sole color author; the headless
!m_plater guard is documented as the wxGetApp sentinel it is.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Clifford Garwood
2026-08-14 16:54:48 -04:00
co-authored by Claude Fable 5
parent 28c0c681fe
commit 8a5bc75e7b
2 changed files with 23 additions and 10 deletions
+20 -9
View File
@@ -1004,12 +1004,24 @@ std::string PartPlate::build_imex_cache_key() const
auto* cw_opt = printer_cfg.option<ConfigOptionFloat>("imex_nozzle_clearance_x");
auto* ch_opt = printer_cfg.option<ConfigOptionFloat>("imex_nozzle_clearance_y");
auto* mgn_opt = printer_cfg.option<ConfigOptionFloat>("imex_carriage_margin");
// Mode NAME alone is not printer identity: two presets can define the same mode
// name with different tool rosters/primary, and imex_firmware_managed_zones
// suppresses ghost generation entirely. Both shape the baked zone/ghost set, so
// key them directly (previously caught only incidentally, via the ghost key's
// since-removed pem entry).
std::string active_tools;
int primary_phys = -1;
resolve_active_mode_tools(active_tools, primary_phys); // empty on failure — keyed as such
auto* fw_opt = printer_cfg.option<ConfigOptionBool>("imex_firmware_managed_zones");
return active_mode
+ "|" + std::to_string(n_col_opt ? n_col_opt->value : 2)
+ "x" + std::to_string(n_row_opt ? n_row_opt->value : 1)
+ "|cw" + std::to_string(cw_opt ? (int)(cw_opt->value * 10) : 0)
+ "|ch" + std::to_string(ch_opt ? (int)(ch_opt->value * 10) : 0)
+ "|mg" + std::to_string(mgn_opt ? (int)(mgn_opt->value * 10) : 0);
+ "|mg" + std::to_string(mgn_opt ? (int)(mgn_opt->value * 10) : 0)
+ "|t" + active_tools
+ "|p" + std::to_string(primary_phys)
+ "|fw" + ((fw_opt && fw_opt->value) ? "1" : "0");
}
// Reposition the IMEX mode icon without requiring a full set_shape() rebuild.
@@ -1099,8 +1111,8 @@ 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.
// Ghost opacity, stamped together with RGB by update_imex_ghost_colors every
// frame — the sole author of ghost color.
static constexpr float IMEX_GHOST_ALPHA = 0.55f;
void PartPlate::calc_imex_ghosts()
@@ -1192,7 +1204,6 @@ void PartPlate::calc_imex_ghosts()
return {aggregated_primary, aggregated_target - aggregated_primary};
};
// 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.
// (Per-ghost GLModel::init_from still runs once each, since GLVolume owns its GLModel by value;
@@ -1238,11 +1249,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;
// 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);
// No color at bake time: update_imex_ghost_colors is the sole author of
// ghost color, restamping RGB + alpha from the live palette every frame.
auto ghost = std::make_unique<GLVolume>();
ghost->set_instance_transformation(ghost_xf);
ghost->force_transparent = 1;
ghost->force_native_color = 1;
@@ -1361,6 +1370,8 @@ void PartPlate::update_imex_ghost_transforms(
// over the effective pem + per-plate override map), so the two cannot disagree.
void PartPlate::update_imex_ghost_colors()
{
// !m_plater is the headless sentinel: without a GUI_App, calling wxGetApp()
// below is unsafe — same guard pattern as ensure_imex_zones.
if (m_imex_ghost_volumes.empty() || !m_plater) return;
auto* pb = wxGetApp().preset_bundle;
if (!pb) return;
+3 -1
View File
@@ -20293,7 +20293,9 @@ Plater::ImexGhostTooltip Plater::format_imex_ghost_tooltip(int physical_head) co
return t;
}
t.filament_slot_1based = logical + 1;
t.swatch = plate->get_imex_head_filament_color(physical_head);
// Reuse the pem/map already hoisted above so label and swatch are derived
// from one resolution, not two.
t.swatch = plate->get_imex_head_filament_color(physical_head, pem, map);
t.swatch.a(1.0f); // tooltip swatch opaque
t.label = "T" + std::to_string(physical_head) +
" -> filament " + std::to_string(t.filament_slot_1based);