mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-10 17:21:10 +00:00
feat(imex): effective_physical_extruder_map helper + diagnostic no-filament tooltip
Centralize the project→printer→printer_extruder_id fallback for the physical extruder map. PrintApply, PartPlate (ghost color + cache key), Plater (tooltip + click gate) all previously open-coded the three-step lookup, and each handled the "pem unset, derive from pei" case slightly differently — an IDEX printer without an explicit pem could paint an UNPRINTABLE_COLOR ghost even though the slicer would have derived a valid mapping. - IMEXHelpers: add effective_physical_extruder_map(explicit_pem, pei) and a PresetBundle overload that wraps the project→printer precedence. - PrintApply: use the helper in place of the inline pei→pem normalization. - PartPlate / Plater: call the PresetBundle overload at every ghost-color, ghost-cache-key, tooltip, and click-gate site. - Plater::format_imex_ghost_tooltip: when no filament resolves to a head, surface an actionable message directing the user to extend the extruder count in the Machine tab, instead of the generic "(no filament routed)". - IMEXFilamentPickerPopover: hold m_pem by value so callers can pass a stack-local derived pem without lifetime worries. - Tests: 5 new cases covering explicit-wins, default-pem fallback, null inputs, and the IDEX ghost-color regression that motivated this. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
b2128cc330
commit
6a3de6a28f
@@ -6,8 +6,33 @@
|
||||
#include <sstream>
|
||||
#include <unordered_set>
|
||||
|
||||
#include "libslic3r/PresetBundle.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
ConfigOptionInts effective_physical_extruder_map(const ConfigOptionInts* explicit_pem,
|
||||
const ConfigOptionInts* printer_extruder_id)
|
||||
{
|
||||
if (explicit_pem && explicit_pem->values.size() >= 2)
|
||||
return *explicit_pem;
|
||||
ConfigOptionInts derived;
|
||||
if (printer_extruder_id) {
|
||||
derived.values.reserve(printer_extruder_id->values.size());
|
||||
for (int v : printer_extruder_id->values)
|
||||
derived.values.push_back(v - 1);
|
||||
}
|
||||
return derived;
|
||||
}
|
||||
|
||||
ConfigOptionInts effective_physical_extruder_map(const PresetBundle& pb)
|
||||
{
|
||||
const ConfigOptionInts* explicit_pem = pb.project_config.option<ConfigOptionInts>("physical_extruder_map");
|
||||
if (!explicit_pem || explicit_pem->values.size() < 2)
|
||||
explicit_pem = pb.printers.get_edited_preset().config.option<ConfigOptionInts>("physical_extruder_map");
|
||||
const ConfigOptionInts* pei = pb.printers.get_edited_preset().config.option<ConfigOptionInts>("printer_extruder_id");
|
||||
return effective_physical_extruder_map(explicit_pem, pei);
|
||||
}
|
||||
|
||||
int first_filament_for_physical_head(const ConfigOptionInts& pem, int physical)
|
||||
{
|
||||
const auto& v = pem.values;
|
||||
|
||||
@@ -11,6 +11,25 @@
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
class PresetBundle;
|
||||
|
||||
// Returns the effective physical_extruder_map given an optionally-explicit map and
|
||||
// the printer's `printer_extruder_id`. If `explicit_pem` has size >= 2 the caller
|
||||
// authored one, and it is returned verbatim. Otherwise the map is auto-derived
|
||||
// from `printer_extruder_id` by converting each 1-indexed value to 0-indexed.
|
||||
// Returns an empty ConfigOptionInts if neither source yields any values.
|
||||
// Slice-time (PrintApply) and GUI ghost-color paths both call this so a printer
|
||||
// profile without an explicit pem still gets a consistent mapping.
|
||||
ConfigOptionInts effective_physical_extruder_map(const ConfigOptionInts* explicit_pem,
|
||||
const ConfigOptionInts* printer_extruder_id);
|
||||
|
||||
// GUI overload: resolves the effective pem from a live PresetBundle using the
|
||||
// project_config → printer preset fallback, then derives from printer_extruder_id
|
||||
// if neither holds a user-authored map (size >= 2). Use this instead of open-coding
|
||||
// the lookup at ghost-color, tooltip, click-gate, and cache-key call sites so they
|
||||
// all agree on what the slicer will see.
|
||||
ConfigOptionInts effective_physical_extruder_map(const PresetBundle& pb);
|
||||
|
||||
// Returns the lowest 0-based logical filament index L such that pem[L] == physical.
|
||||
// Returns -1 if no filament routes to `physical`.
|
||||
// Degenerate case: an empty pem returns 0 when `physical == 0` (identity-on-head-0
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "ClipperUtils.hpp"
|
||||
#include "IMEXHelpers.hpp"
|
||||
#include "Model.hpp"
|
||||
#include "Print.hpp"
|
||||
|
||||
@@ -1168,11 +1169,8 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
|
||||
{
|
||||
auto* pem = new_full_config.option<ConfigOptionInts>("physical_extruder_map", true);
|
||||
const auto* pei = new_full_config.option<ConfigOptionInts>("printer_extruder_id");
|
||||
if (pem && pei && !pei->values.empty() && pem->values.size() <= 1) {
|
||||
pem->values.clear();
|
||||
for (int v : pei->values)
|
||||
pem->values.push_back(v - 1); // convert 1-indexed extruder IDs to 0-indexed
|
||||
}
|
||||
if (pem)
|
||||
pem->values = effective_physical_extruder_map(pem, pei).values;
|
||||
}
|
||||
|
||||
m_ori_full_print_config = new_full_config;
|
||||
|
||||
@@ -30,7 +30,10 @@ private:
|
||||
void on_filament_selected(int slot_1_based);
|
||||
|
||||
PartPlate* m_plate;
|
||||
const ConfigOptionInts& m_pem;
|
||||
// Owned by value: callers may pass a stack-local derived pem
|
||||
// (e.g. effective_physical_extruder_map result) that won't outlive
|
||||
// the popover's async lifetime.
|
||||
ConfigOptionInts m_pem;
|
||||
int m_physical_head;
|
||||
CommitCallback m_on_commit;
|
||||
|
||||
|
||||
@@ -438,14 +438,9 @@ ColorRGBA PartPlate::get_imex_head_filament_color(int physical_head) const
|
||||
if (!pb)
|
||||
return GLVolume::UNPRINTABLE_COLOR;
|
||||
|
||||
const ConfigOptionInts* pem = pb->project_config.option<ConfigOptionInts>("physical_extruder_map");
|
||||
if (!pem || pem->values.size() < 2)
|
||||
pem = pb->printers.get_edited_preset().config.option<ConfigOptionInts>("physical_extruder_map");
|
||||
if (!pem)
|
||||
return GLVolume::UNPRINTABLE_COLOR;
|
||||
|
||||
const ConfigOptionInts pem = effective_physical_extruder_map(*pb);
|
||||
const auto plate_map = get_imex_head_filament_map();
|
||||
const int logical = resolve_filament_for_head(plate_map, *pem, physical_head);
|
||||
const int logical = resolve_filament_for_head(plate_map, pem, physical_head);
|
||||
if (logical < 0)
|
||||
return GLVolume::UNPRINTABLE_COLOR;
|
||||
|
||||
@@ -979,13 +974,11 @@ std::string PartPlate::build_imex_ghost_cache_key() const
|
||||
if (k.empty()) return ""; // ghost-off when zones-off
|
||||
|
||||
if (auto* pb = wxGetApp().preset_bundle) {
|
||||
const ConfigOptionInts* pem = pb->project_config.option<ConfigOptionInts>("physical_extruder_map");
|
||||
if (!pem || pem->values.size() < 2)
|
||||
pem = pb->printers.get_edited_preset().config.option<ConfigOptionInts>("physical_extruder_map");
|
||||
if (pem) {
|
||||
k += "|pem";
|
||||
for (int v : pem->values) { k += ':'; k += std::to_string(v); }
|
||||
}
|
||||
// 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)
|
||||
|
||||
+10
-12
@@ -18543,14 +18543,15 @@ Plater::ImexGhostTooltip Plater::format_imex_ghost_tooltip(int physical_head) co
|
||||
return t;
|
||||
}
|
||||
|
||||
const ConfigOptionInts* pem = wxGetApp().preset_bundle->project_config.option<ConfigOptionInts>("physical_extruder_map");
|
||||
if (!pem || pem->values.size() < 2)
|
||||
pem = wxGetApp().preset_bundle->printers.get_edited_preset().config.option<ConfigOptionInts>("physical_extruder_map");
|
||||
|
||||
const ConfigOptionInts pem = effective_physical_extruder_map(*wxGetApp().preset_bundle);
|
||||
const auto map = plate->get_imex_head_filament_map();
|
||||
const int logical = pem ? resolve_filament_for_head(map, *pem, physical_head) : -1;
|
||||
const int logical = resolve_filament_for_head(map, pem, physical_head);
|
||||
if (logical < 0) {
|
||||
t.label = "T" + std::to_string(physical_head) + " -> (no filament routed)";
|
||||
// No filament slot resolves to this physical head. Usually means the printer's
|
||||
// extruder count is understated relative to the filament palette (user added a
|
||||
// filament without extending printer_extruder_id in the Machine tab).
|
||||
t.label = "T" + std::to_string(physical_head) + " — no filament routed\n"
|
||||
"Add an extruder in the Machine tab so this head has a filament slot.";
|
||||
return t;
|
||||
}
|
||||
t.filament_slot_1based = logical + 1;
|
||||
@@ -18563,13 +18564,10 @@ Plater::ImexGhostTooltip Plater::format_imex_ghost_tooltip(int physical_head) co
|
||||
|
||||
void Plater::on_imex_ghost_click(int physical_head)
|
||||
{
|
||||
const ConfigOptionInts* pem = wxGetApp().preset_bundle->project_config.option<ConfigOptionInts>("physical_extruder_map");
|
||||
if (!pem || pem->values.size() < 2)
|
||||
pem = wxGetApp().preset_bundle->printers.get_edited_preset().config.option<ConfigOptionInts>("physical_extruder_map");
|
||||
if (!pem) return;
|
||||
const ConfigOptionInts pem = effective_physical_extruder_map(*wxGetApp().preset_bundle);
|
||||
|
||||
int lane_count = 0;
|
||||
for (int pv : pem->values) if (pv == physical_head) ++lane_count;
|
||||
for (int pv : pem.values) if (pv == physical_head) ++lane_count;
|
||||
if (lane_count < 2) return; // single-lane head -> click is a no-op, tooltip conveyed status
|
||||
|
||||
PartPlate* plate = p->partplate_list.get_curr_plate();
|
||||
@@ -18577,7 +18575,7 @@ void Plater::on_imex_ghost_click(int physical_head)
|
||||
|
||||
auto* picker = new IMEXFilamentPickerPopover(
|
||||
p->view3D->get_canvas3d()->get_wxglcanvas(),
|
||||
plate, *pem, physical_head,
|
||||
plate, pem, physical_head,
|
||||
[this]() {
|
||||
take_snapshot("edit imex head filament");
|
||||
update_project_dirty_from_presets();
|
||||
|
||||
@@ -13,6 +13,42 @@ static ConfigOptionInts make_pem(std::vector<int> v) {
|
||||
return o;
|
||||
}
|
||||
|
||||
TEST_CASE("effective_physical_extruder_map — explicit wins", "[IMEX]") {
|
||||
auto explicit_pem = make_pem({0, 0, 0, 0, 1, 2, 3});
|
||||
auto pei = make_pem({1, 2, 3, 4}); // would derive to {0,1,2,3}
|
||||
auto out = effective_physical_extruder_map(&explicit_pem, &pei);
|
||||
REQUIRE(out.values == std::vector<int>{0, 0, 0, 0, 1, 2, 3});
|
||||
}
|
||||
|
||||
TEST_CASE("effective_physical_extruder_map — default pem falls back to pei derive", "[IMEX]") {
|
||||
auto default_pem = make_pem({0}); // size 1, the PrintConfig default
|
||||
auto pei = make_pem({1, 2}); // 1-indexed IDEX
|
||||
auto out = effective_physical_extruder_map(&default_pem, &pei);
|
||||
REQUIRE(out.values == std::vector<int>{0, 1}); // 1-indexed → 0-indexed
|
||||
}
|
||||
|
||||
TEST_CASE("effective_physical_extruder_map — null explicit, pei present", "[IMEX]") {
|
||||
auto pei = make_pem({1, 2, 3});
|
||||
auto out = effective_physical_extruder_map(nullptr, &pei);
|
||||
REQUIRE(out.values == std::vector<int>{0, 1, 2});
|
||||
}
|
||||
|
||||
TEST_CASE("effective_physical_extruder_map — both absent yields empty", "[IMEX]") {
|
||||
auto out = effective_physical_extruder_map(nullptr, nullptr);
|
||||
REQUIRE(out.values.empty());
|
||||
}
|
||||
|
||||
TEST_CASE("effective_physical_extruder_map — IDEX ghost-color regression guard", "[IMEX]") {
|
||||
// Printer with printer_extruder_id = [1, 2] and no explicit pem (default {0}).
|
||||
// Before the GUI fix, this scenario produced a black ghost on T1 because
|
||||
// first_filament_for_physical_head({0}, 1) == -1.
|
||||
auto default_pem = make_pem({0});
|
||||
auto pei = make_pem({1, 2});
|
||||
auto pem = effective_physical_extruder_map(&default_pem, &pei);
|
||||
REQUIRE(first_filament_for_physical_head(pem, 0) == 0);
|
||||
REQUIRE(first_filament_for_physical_head(pem, 1) == 1); // no longer -1
|
||||
}
|
||||
|
||||
TEST_CASE("first_filament_for_physical_head — identity pem", "[IMEX]") {
|
||||
auto pem = make_pem({0, 1, 2, 3});
|
||||
REQUIRE(first_filament_for_physical_head(pem, 0) == 0);
|
||||
|
||||
Reference in New Issue
Block a user