fix(imex): guard IMEX zone refresh against headless CLI slicing

`check_outside` (placement check) calls `ensure_imex_zones()` to make sure
zone geometry is current before deciding if an instance is in-bounds. In
the GUI path that's fine, but the CLI / headless 3MF-load path also reaches
this through `PartPlateList::load_from_3mf_structure -> reload_all_objects
-> add_instance -> check_outside`, and CLI mode has no GUI_App initialized.

`build_imex_cache_key` and `calc_imex_zones` both dereference
`wxGetApp().preset_bundle` — without a GUI_App, `wxGetApp()` returns memory
that segfaults on member access, killing the slicer with SIGSEGV before
any G-code is produced.

Latent since 461c69c83e (Apr 9), surfaced now that upstream's main carries
the headless regression-test CI step (#13353) that exercises CLI slicing
on every PR build.

Fix: short-circuit `ensure_imex_zones()` when `m_plater` is null (already
the GUI/CLI marker used by `calc_imex_ghosts`). Also tighten the existing
`build_imex_cache_key` null check to consult `m_plater` first as defence
in depth, so the function stays safe if reached from another headless
caller.

Verified locally against the upstream regression suite — klipper /
p1s_multicolor / toolchanger_4_color all slice cleanly within the
20% baseline tolerance.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Clifford Garwood
2026-05-01 08:14:07 -04:00
co-authored by Claude Opus 4.7
parent b452a8a28f
commit 6c7bd484db
+9 -1
View File
@@ -954,7 +954,11 @@ void PartPlate::calc_imex_zones()
// adjust the scale.
std::string PartPlate::build_imex_cache_key() const
{
if (!wxGetApp().preset_bundle)
// CLI / headless slice: no GUI_App is initialized, so `wxGetApp()` returns
// memory that segfaults on member access. `m_plater` is set only by the GUI
// construction path; treat its absence as "no IMEX state to compute" and
// return an empty key. Same guard pattern used by calc_imex_ghosts.
if (!m_plater || !wxGetApp().preset_bundle)
return "";
const DynamicPrintConfig& printer_cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config;
auto* is_imex_opt = printer_cfg.option<ConfigOptionBool>("is_imex");
@@ -1002,6 +1006,10 @@ void PartPlate::refresh_imex_icon()
// Called before any collision check AND before rendering so both paths share the same data.
void PartPlate::ensure_imex_zones()
{
// CLI / headless slice: no GUI_App, no plater, no IMEX state to track. Bail out
// before touching `wxGetApp()` (which segfaults without a GUI_App) — placement
// checks reach this from the headless 3MF-load path through `check_outside`.
if (!m_plater) return;
std::string cache_key = build_imex_cache_key();
if (cache_key != m_imex_zones_mode_cache) {
m_imex_zones_mode_cache = cache_key;