mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-30 12:21:05 +00:00
feat(libslic3r): multi-nozzle slicing engine for H2C/A2L
Port BambuStudio's dual-nozzle slicing core: H2C-era config keys, filament-to-nozzle grouping with per-layer dynamic regrouping, filament/nozzle/hotend gcode placeholder vocabulary, multi-nozzle wipe tower pre-heat/pre-cool, the two-pass pre-cooling injector, and corexy farthest-point timelapse.
This commit is contained in:
@@ -800,6 +800,10 @@ std::string AppConfig::load()
|
|||||||
preset_info.nozzle_volume_type = NozzleVolumeType(cali_it.value()["nozzle_volume_type"].get<int>());
|
preset_info.nozzle_volume_type = NozzleVolumeType(cali_it.value()["nozzle_volume_type"].get<int>());
|
||||||
if (cali_it.value().contains("bed_type"))
|
if (cali_it.value().contains("bed_type"))
|
||||||
preset_info.bed_type = BedType(cali_it.value()["bed_type"].get<int>());
|
preset_info.bed_type = BedType(cali_it.value()["bed_type"].get<int>());
|
||||||
|
if (cali_it.value().contains("nozzle_pos_id"))
|
||||||
|
preset_info.nozzle_pos_id = cali_it.value()["nozzle_pos_id"].get<int>();
|
||||||
|
if (cali_it.value().contains("nozzle_sn"))
|
||||||
|
preset_info.nozzle_sn = cali_it.value()["nozzle_sn"].get<std::string>();
|
||||||
cali_info.selected_presets.push_back(preset_info);
|
cali_info.selected_presets.push_back(preset_info);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -957,6 +961,8 @@ void AppConfig::save()
|
|||||||
preset_json["extruder_id"] = filament_preset.extruder_id;
|
preset_json["extruder_id"] = filament_preset.extruder_id;
|
||||||
preset_json["nozzle_volume_type"] = int(filament_preset.nozzle_volume_type);
|
preset_json["nozzle_volume_type"] = int(filament_preset.nozzle_volume_type);
|
||||||
preset_json["bed_type"] = int(filament_preset.bed_type);
|
preset_json["bed_type"] = int(filament_preset.bed_type);
|
||||||
|
preset_json["nozzle_pos_id"] = filament_preset.nozzle_pos_id;
|
||||||
|
preset_json["nozzle_sn"] = filament_preset.nozzle_sn;
|
||||||
preset_json["nozzle_diameter"] = filament_preset.nozzle_diameter;
|
preset_json["nozzle_diameter"] = filament_preset.nozzle_diameter;
|
||||||
preset_json["filament_id"] = filament_preset.filament_id;
|
preset_json["filament_id"] = filament_preset.filament_id;
|
||||||
preset_json["setting_id"] = filament_preset.setting_id;
|
preset_json["setting_id"] = filament_preset.setting_id;
|
||||||
|
|||||||
@@ -471,6 +471,8 @@ set(lisbslic3r_sources
|
|||||||
FilamentGroup.cpp
|
FilamentGroup.cpp
|
||||||
FilamentGroupUtils.hpp
|
FilamentGroupUtils.hpp
|
||||||
FilamentGroupUtils.cpp
|
FilamentGroupUtils.cpp
|
||||||
|
MultiNozzleUtils.hpp
|
||||||
|
MultiNozzleUtils.cpp
|
||||||
GCode/ToolOrderUtils.hpp
|
GCode/ToolOrderUtils.hpp
|
||||||
GCode/ToolOrderUtils.cpp
|
GCode/ToolOrderUtils.cpp
|
||||||
FlushVolPredictor.hpp
|
FlushVolPredictor.hpp
|
||||||
|
|||||||
@@ -51,6 +51,10 @@ public:
|
|||||||
double retracted() const { return m_retracted; }
|
double retracted() const { return m_retracted; }
|
||||||
// Get extra retraction planned after
|
// Get extra retraction planned after
|
||||||
double restart_extra() const { return m_restart_extra; }
|
double restart_extra() const { return m_restart_extra; }
|
||||||
|
// Share-aware retracted-length readers (for extruders shared between filaments), consumed by GCodeWriter::get_extruder_retracted_length.
|
||||||
|
bool is_share_extruder() const { return m_share_extruder; }
|
||||||
|
double get_single_retracted_length() const { return m_retracted; }
|
||||||
|
double get_share_retracted_length() const { return m_share_retracted[extruder_id()]; }
|
||||||
// Setters for the PlaceholderParser.
|
// Setters for the PlaceholderParser.
|
||||||
// Set current extruder position. Only applicable with absolute extruder addressing.
|
// Set current extruder position. Only applicable with absolute extruder addressing.
|
||||||
void set_position(double e) { m_E = e; }
|
void set_position(double e) { m_E = e; }
|
||||||
|
|||||||
+923
-338
File diff suppressed because it is too large
Load Diff
@@ -13,7 +13,8 @@
|
|||||||
|
|
||||||
const static int DEFAULT_CLUSTER_SIZE = 16;
|
const static int DEFAULT_CLUSTER_SIZE = 16;
|
||||||
|
|
||||||
const static int ABSOLUTE_FLUSH_GAP_TOLERANCE = 5;
|
const static int ABSOLUTE_FLUSH_GAP_TOLERANCE = 10;
|
||||||
|
|
||||||
|
|
||||||
namespace Slic3r
|
namespace Slic3r
|
||||||
{
|
{
|
||||||
@@ -52,12 +53,12 @@ namespace Slic3r
|
|||||||
|
|
||||||
struct MemoryedGroup {
|
struct MemoryedGroup {
|
||||||
MemoryedGroup() = default;
|
MemoryedGroup() = default;
|
||||||
MemoryedGroup(const std::vector<int>& group_, const int cost_, const int prefer_level_) :group(group_), cost(cost_), prefer_level(prefer_level_) {}
|
MemoryedGroup(const std::vector<int>& group_, const double cost_, const int prefer_level_) :group(group_), cost(cost_), prefer_level(prefer_level_) {}
|
||||||
bool operator>(const MemoryedGroup& other) const {
|
bool operator>(const MemoryedGroup& other) const {
|
||||||
return prefer_level < other.prefer_level || (prefer_level == other.prefer_level && cost > other.cost);
|
return prefer_level < other.prefer_level || (prefer_level == other.prefer_level && cost > other.cost);
|
||||||
}
|
}
|
||||||
|
|
||||||
int cost{ 0 };
|
double cost{ 0 };
|
||||||
int prefer_level{ 0 };
|
int prefer_level{ 0 };
|
||||||
std::vector<int>group;
|
std::vector<int>group;
|
||||||
};
|
};
|
||||||
@@ -75,6 +76,7 @@ namespace Slic3r
|
|||||||
std::vector<FilamentGroupUtils::FilamentInfo> filament_info;
|
std::vector<FilamentGroupUtils::FilamentInfo> filament_info;
|
||||||
std::vector<std::string> filament_ids;
|
std::vector<std::string> filament_ids;
|
||||||
std::vector<std::set<int>> unprintable_filaments;
|
std::vector<std::set<int>> unprintable_filaments;
|
||||||
|
std::map<int, std::set<NozzleVolumeType>> unprintable_volumes;
|
||||||
} model_info;
|
} model_info;
|
||||||
|
|
||||||
struct GroupInfo {
|
struct GroupInfo {
|
||||||
@@ -82,40 +84,64 @@ namespace Slic3r
|
|||||||
double max_gap_threshold;
|
double max_gap_threshold;
|
||||||
FGMode mode;
|
FGMode mode;
|
||||||
FGStrategy strategy;
|
FGStrategy strategy;
|
||||||
bool ignore_ext_filament; //wai gua filament
|
bool ignore_ext_filament;
|
||||||
|
bool has_filament_switcher = false;
|
||||||
|
std::vector<int> filament_volume_map;
|
||||||
} group_info;
|
} group_info;
|
||||||
|
|
||||||
struct MachineInfo {
|
struct MachineInfo {
|
||||||
std::vector<int> max_group_size;
|
std::vector<int> max_group_size;
|
||||||
std::vector<std::vector<FilamentGroupUtils::MachineFilamentInfo>> machine_filament_info;
|
std::vector<std::vector<FilamentGroupUtils::MachineFilamentInfo>> machine_filament_info;
|
||||||
std::vector<std::pair<std::set<int>, int>> extruder_group_size;
|
std::vector<bool> prefer_non_model_filament;
|
||||||
int master_extruder_id;
|
int master_extruder_id;
|
||||||
} machine_info;
|
} machine_info;
|
||||||
|
|
||||||
|
struct SpeedInfo{
|
||||||
|
std::unordered_map<int,std::unordered_map<int,double>> filament_print_time;
|
||||||
|
double extruder_change_time;
|
||||||
|
double filament_change_time;
|
||||||
|
bool group_with_time;
|
||||||
|
MultiNozzleUtils::FilamentChangeTimeParams change_time_params;
|
||||||
|
std::vector<bool> ams_preload_enabled;
|
||||||
|
} speed_info;
|
||||||
|
|
||||||
|
struct NozzleInfo {
|
||||||
|
std::map<int, std::vector<int>> extruder_nozzle_list;
|
||||||
|
std::vector<MultiNozzleUtils::NozzleInfo> nozzle_list;
|
||||||
|
std::unordered_map<int, int> nozzle_status;
|
||||||
|
} nozzle_info;
|
||||||
};
|
};
|
||||||
|
|
||||||
std::vector<int> select_best_group_for_ams(const std::vector<std::vector<int>>& map_lists,
|
std::vector<int> select_best_group_for_ams(const std::vector<std::vector<int>> &filament_to_nozzles,
|
||||||
|
const std::vector<MultiNozzleUtils::NozzleInfo>& nozzle_list,
|
||||||
const std::vector<unsigned int>& used_filaments,
|
const std::vector<unsigned int>& used_filaments,
|
||||||
const std::vector<FilamentGroupUtils::FilamentInfo>& used_filament_info,
|
const std::vector<FilamentGroupUtils::FilamentInfo>& used_filament_info,
|
||||||
const std::vector<std::vector<FilamentGroupUtils::MachineFilamentInfo>>& machine_filament_info,
|
const std::vector<std::vector<FilamentGroupUtils::MachineFilamentInfo>>& machine_filament_info,
|
||||||
|
const bool has_filament_switcher = false,
|
||||||
const double color_delta_threshold = 20);
|
const double color_delta_threshold = 20);
|
||||||
|
|
||||||
std::vector<int> optimize_group_for_master_extruder(const std::vector<unsigned int>& used_filaments, const FilamentGroupContext& ctx, const std::vector<int>& filament_map);
|
|
||||||
|
|
||||||
bool can_swap_groups(const int extruder_id_0, const std::set<int>& group_0, const int extruder_id_1, const std::set<int>& group_1, const FilamentGroupContext& ctx);
|
|
||||||
|
|
||||||
std::vector<int> calc_filament_group_for_tpu(const std::set<int>& tpu_filaments, const int filament_nums, const int master_extruder_id);
|
|
||||||
|
|
||||||
class FlushDistanceEvaluator
|
class FlushDistanceEvaluator
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
FlushDistanceEvaluator(const FlushMatrix& flush_matrix,const std::vector<unsigned int>&used_filaments,const std::vector<std::vector<unsigned int>>& layer_filaments, double p = 0.65);
|
FlushDistanceEvaluator(const std::vector<FlushMatrix>& flush_matrix,const std::vector<unsigned int>&used_filaments,const std::vector<std::vector<unsigned int>>& layer_filaments, double p = 0.65);
|
||||||
~FlushDistanceEvaluator() = default;
|
~FlushDistanceEvaluator() = default;
|
||||||
double get_distance(int idx_a, int idx_b) const;
|
double get_distance(int idx_a, int idx_b, int extruder_id) const;
|
||||||
private:
|
private:
|
||||||
std::vector<std::vector<float>>m_distance_matrix;
|
std::vector<std::vector<std::vector<float>>>m_distance_matrix;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
class TimeEvaluator
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
TimeEvaluator(const FilamentGroupContext::SpeedInfo& speed_info) : m_speed_info(speed_info) {}
|
||||||
|
double get_estimated_time(const std::vector<int>& filament_map) const;
|
||||||
|
private:
|
||||||
|
FilamentGroupContext::SpeedInfo m_speed_info;
|
||||||
|
};
|
||||||
|
|
||||||
class FilamentGroup
|
class FilamentGroup
|
||||||
{
|
{
|
||||||
using MemoryedGroup = FilamentGroupUtils::MemoryedGroup;
|
using MemoryedGroup = FilamentGroupUtils::MemoryedGroup;
|
||||||
@@ -129,11 +155,17 @@ namespace Slic3r
|
|||||||
public:
|
public:
|
||||||
std::vector<int> calc_filament_group_for_match(int* cost = nullptr);
|
std::vector<int> calc_filament_group_for_match(int* cost = nullptr);
|
||||||
std::vector<int> calc_filament_group_for_flush(int* cost = nullptr);
|
std::vector<int> calc_filament_group_for_flush(int* cost = nullptr);
|
||||||
|
std::vector<int> calc_filament_group_for_tpu(int* cost = nullptr);
|
||||||
private:
|
private:
|
||||||
std::vector<int> calc_min_flush_group(int* cost = nullptr);
|
std::vector<int> calc_min_flush_group(int* cost = nullptr);
|
||||||
std::vector<int> calc_min_flush_group_by_enum(const std::vector<unsigned int>& used_filaments, int* cost = nullptr);
|
|
||||||
std::vector<int> calc_min_flush_group_by_pam2(const std::vector<unsigned int>& used_filaments, int* cost = nullptr, int timeout_ms = 300);
|
std::vector<int> calc_group_by_enum(int k, const std::vector<unsigned int>& used_filaments,
|
||||||
|
const std::unordered_map<int, std::vector<int>>& unplaceable_limits, int* cost = nullptr);
|
||||||
|
std::vector<int> calc_group_by_kmedoids(int k, const std::vector<unsigned int>& used_filaments,
|
||||||
|
const std::unordered_map<int, std::vector<int>>& unplaceable_limits, int* cost = nullptr, int timeout_ms = 500);
|
||||||
|
|
||||||
|
std::map<int, int> rebuild_unprintables(const std::vector<unsigned int>& used_filaments, const std::map<int,int>& extruder_unprintables);
|
||||||
|
std::unordered_map<int, std::vector<int>> rebuild_nozzle_unprintables(const std::vector<unsigned int>& used_filaments, const std::unordered_map<int, std::vector<int>>& extruder_unprintables, const std::vector<int>& filament_volume_map);
|
||||||
|
|
||||||
std::unordered_map<int, std::vector<int>> try_merge_filaments();
|
std::unordered_map<int, std::vector<int>> try_merge_filaments();
|
||||||
void rebuild_context(const std::unordered_map<int, std::vector<int>>& merged_filaments);
|
void rebuild_context(const std::unordered_map<int, std::vector<int>>& merged_filaments);
|
||||||
@@ -141,57 +173,78 @@ namespace Slic3r
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
FilamentGroupContext ctx;
|
FilamentGroupContext ctx;
|
||||||
|
MemoryedGroupHeap m_memoryed_heap;
|
||||||
std::vector<std::vector<int>> m_memoryed_groups;
|
std::vector<std::vector<int>> m_memoryed_groups;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
std::optional<std::function<bool(int, std::vector<int>&)>> get_custom_seq;
|
std::optional<std::function<bool(int, std::vector<int>&)>> get_custom_seq;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
class KMediods2
|
std::vector<int> calc_filament_group_for_manual_multi_nozzle(const std::vector<int>& filament_map_manual,const FilamentGroupContext& ctx);
|
||||||
|
|
||||||
|
std::vector<int> calc_filament_group_for_match_multi_nozzle(const FilamentGroupContext& ctx);
|
||||||
|
|
||||||
|
struct FilamentPlanRes
|
||||||
{
|
{
|
||||||
|
std::vector<int> fil_order;
|
||||||
|
std::vector<int> fil_nozzle_match;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<FilamentPlanRes> plan_filament_nozzle_mapping_and_order(const FilamentGroupContext& ctx);
|
||||||
|
|
||||||
|
|
||||||
|
class KMediods
|
||||||
|
{
|
||||||
|
protected:
|
||||||
using MemoryedGroupHeap = FilamentGroupUtils::MemoryedGroupHeap;
|
using MemoryedGroupHeap = FilamentGroupUtils::MemoryedGroupHeap;
|
||||||
using MemoryedGroup = FilamentGroupUtils::MemoryedGroup;
|
using MemoryedGroup = FilamentGroupUtils::MemoryedGroup;
|
||||||
|
|
||||||
enum INIT_TYPE
|
|
||||||
{
|
|
||||||
Random = 0,
|
|
||||||
Farthest
|
|
||||||
};
|
|
||||||
public:
|
public:
|
||||||
KMediods2(const int elem_count, const std::shared_ptr<FlushDistanceEvaluator>& evaluator, int default_group_id = 0) :
|
KMediods(const int k, const int elem_count, const std::shared_ptr<FlushDistanceEvaluator>& evaluator, int default_group_id = 0) {
|
||||||
m_evaluator{ evaluator },
|
m_k = k;
|
||||||
m_elem_count{ elem_count },
|
m_evaluator = evaluator;
|
||||||
m_default_group_id{ default_group_id }
|
m_max_cluster_size = std::vector<int>(k, DEFAULT_CLUSTER_SIZE);
|
||||||
{
|
m_elem_count = elem_count;
|
||||||
m_max_cluster_size = std::vector<int>(m_k, DEFAULT_CLUSTER_SIZE);
|
m_default_group_id = default_group_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
// set max group size
|
// set max group size
|
||||||
void set_max_cluster_size(const std::vector<int>& group_size) { m_max_cluster_size = group_size; }
|
void set_max_cluster_size(const std::vector<int>& group_size) { m_max_cluster_size = group_size; }
|
||||||
|
|
||||||
// key stores elem idx, value stores the cluster id that elem cnanot be placed
|
void set_cluster_group_size(const std::vector<std::pair<std::set<int>,int>>& cluster_group_size);
|
||||||
void set_unplaceable_limits(const std::map<int, int>& placeable_limits) { m_unplaceable_limits = placeable_limits; }
|
|
||||||
|
|
||||||
void do_clustering(const FGStrategy& g_strategy,int timeout_ms = 100);
|
// key stores elem, value stores the cluster id that the elem must be placed
|
||||||
|
void set_placable_limits(const std::unordered_map<int, std::vector<int>>& placable_limits) { m_placeable_limits = placable_limits; }
|
||||||
|
|
||||||
|
// key stores elem, value stores the cluster id that the elem cannot be placed
|
||||||
|
void set_unplacable_limits(const std::unordered_map<int, std::vector<int>>& unplacable_limits) { m_unplaceable_limits = unplacable_limits; }
|
||||||
|
|
||||||
void set_memory_threshold(double threshold) { memory_threshold = threshold; }
|
void set_memory_threshold(double threshold) { memory_threshold = threshold; }
|
||||||
MemoryedGroupHeap get_memoryed_groups()const { return memoryed_groups; }
|
MemoryedGroupHeap get_memoryed_groups()const { return memoryed_groups; }
|
||||||
|
|
||||||
std::vector<int>get_cluster_labels()const { return m_cluster_labels; }
|
void do_clustering(const FilamentGroupContext& context, int timeout_ms = 100, int retry = 10);
|
||||||
|
std::vector<int> get_cluster_labels()const { return m_cluster_labels; }
|
||||||
|
|
||||||
private:
|
|
||||||
std::vector<int>cluster_small_data(const std::map<int, int>& unplaceable_limits, const std::vector<int>& group_size);
|
|
||||||
std::vector<int>assign_cluster_label(const std::vector<int>& center, const std::map<int, int>& unplaceable_limits, const std::vector<int>& group_size, const FGStrategy& strategy);
|
|
||||||
int calc_cost(const std::vector<int>& labels, const std::vector<int>& medoids);
|
|
||||||
protected:
|
protected:
|
||||||
FilamentGroupUtils::MemoryedGroupHeap memoryed_groups;
|
bool have_enough_size(const std::vector<int>& cluster_size, const std::vector<std::pair<std::set<int>, int>>& cluster_group_size,int elem_count);
|
||||||
std::shared_ptr<FlushDistanceEvaluator> m_evaluator;
|
// calculate cluster distance
|
||||||
std::map<int, int>m_unplaceable_limits;
|
int calc_cost(const std::vector<int>& clusters, const std::vector<int>& cluster_centers, int cluster_id = -1);
|
||||||
std::vector<int>m_cluster_labels;
|
|
||||||
std::vector<int>m_max_cluster_size;
|
|
||||||
|
|
||||||
const int m_k = 2;
|
// get initial cluster center
|
||||||
|
std::vector<int>init_cluster_center(const std::unordered_map<int, std::vector<int>>& placeable_limits, const std::unordered_map<int, std::vector<int>>& unplaceable_limits, const std::vector<int>& cluster_size, const std::vector<std::pair<std::set<int>, int>>& cluster_group_size, int seed);
|
||||||
|
// assign each elem to the cluster
|
||||||
|
std::vector<int> assign_cluster_label(const std::vector<int>& center, const std::unordered_map<int, std::vector<int>>& placeable_limits, const std::unordered_map<int, std::vector<int>>& unplaceable_limits, const std::vector<int>& group_size, const std::vector<std::pair<std::set<int>, int>>& cluster_group_size);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
MemoryedGroupHeap memoryed_groups;
|
||||||
|
std::shared_ptr<FlushDistanceEvaluator>m_evaluator;
|
||||||
|
std::unordered_map<int, std::vector<int>> m_unplaceable_limits; // key: filament, value: nozzle ids it cannot be assigned to
|
||||||
|
std::unordered_map<int, std::vector<int>> m_placeable_limits; // key: filament, value: nozzle ids it must be assigned to
|
||||||
|
std::vector<int>m_max_cluster_size; // max number of filaments each nozzle can hold
|
||||||
|
std::vector<int>m_cluster_labels; // assignment result, resolved down to nozzle id
|
||||||
|
std::vector<std::pair<std::set<int>,int>> m_cluster_group_size;
|
||||||
|
std::vector<int> m_nozzle_to_extruder;
|
||||||
|
|
||||||
|
|
||||||
|
int m_k;
|
||||||
int m_elem_count;
|
int m_elem_count;
|
||||||
int m_default_group_id{ 0 };
|
int m_default_group_id{ 0 };
|
||||||
double memory_threshold{ 0 };
|
double memory_threshold{ 0 };
|
||||||
|
|||||||
@@ -274,5 +274,66 @@ namespace FilamentGroupUtils
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int get_estimate_extruder_change_count(const std::vector<std::vector<unsigned int>> &layer_filaments, const MultiNozzleUtils::LayeredNozzleGroupResult &extruder_nozzle_info)
|
||||||
|
{
|
||||||
|
int ret = 0;
|
||||||
|
for (size_t layer_id = 0; layer_id < layer_filaments.size(); ++layer_id) {
|
||||||
|
int extruder_count = extruder_nozzle_info.get_used_extruders(layer_id).size();
|
||||||
|
ret += (extruder_count - 1);
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
int get_estimate_nozzle_change_count(const std::vector<std::vector<unsigned int>> &layer_filaments, const MultiNozzleUtils::LayeredNozzleGroupResult &extruder_nozzle_info)
|
||||||
|
{
|
||||||
|
int ret = 0;
|
||||||
|
for (size_t layer_id = 0; layer_id < layer_filaments.size(); ++layer_id) {
|
||||||
|
auto extruder_list = extruder_nozzle_info.get_used_extruders(layer_id);
|
||||||
|
for (auto extruder_id : extruder_list) {
|
||||||
|
int nozzle_count = extruder_nozzle_info.get_used_nozzles_in_extruder(extruder_id, layer_id).size();
|
||||||
|
if (nozzle_count > 1) ret += (nozzle_count - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::pair<int, int> get_estimate_extruder_filament_change_count(const MultiNozzleUtils::LayeredNozzleGroupResult &extruder_nozzle_info)
|
||||||
|
{
|
||||||
|
std::pair<int, int> ret{0,0};
|
||||||
|
int layer_nums = extruder_nozzle_info.get_layer_filament_sequences().size();
|
||||||
|
for (int layer_id = 0; layer_id < layer_nums; layer_id++) {
|
||||||
|
std::vector<int> extruders = extruder_nozzle_info.get_used_extruders(layer_id);
|
||||||
|
ret.first = extruders.size() - 1;
|
||||||
|
|
||||||
|
for (auto ext_id : extruders) {
|
||||||
|
int nozzles = extruder_nozzle_info.get_used_nozzles_in_extruder(ext_id, layer_id).size();
|
||||||
|
ret.second += nozzles;
|
||||||
|
}
|
||||||
|
ret.second = std::max(0, ret.second - ret.first);
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::map<int,std::vector<int>> build_extruder_nozzle_list(const std::vector<MultiNozzleUtils::NozzleInfo>& nozzle_list)
|
||||||
|
{
|
||||||
|
std::map<int, std::vector<int>> ret;
|
||||||
|
for (auto& nozzle : nozzle_list) {
|
||||||
|
ret[nozzle.extruder_id].emplace_back(nozzle.group_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto& elem : ret)
|
||||||
|
std::sort(elem.second.begin(), elem.second.end());
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<int> update_used_filament_values(const std::vector<int>& old_values, const std::vector<int>& new_values, const std::vector<unsigned int>& used_filaments)
|
||||||
|
{
|
||||||
|
std::vector<int> res = old_values;
|
||||||
|
for (size_t i = 0; i < used_filaments.size(); ++i) {
|
||||||
|
res[used_filaments[i]] = new_values[used_filaments[i]];
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
#include <exception>
|
#include <exception>
|
||||||
|
|
||||||
#include "PrintConfig.hpp"
|
#include "PrintConfig.hpp"
|
||||||
|
#include "MultiNozzleUtils.hpp"
|
||||||
|
|
||||||
namespace Slic3r
|
namespace Slic3r
|
||||||
{
|
{
|
||||||
@@ -31,6 +32,10 @@ namespace Slic3r
|
|||||||
Color color;
|
Color color;
|
||||||
std::string type;
|
std::string type;
|
||||||
bool is_support;
|
bool is_support;
|
||||||
|
// How this filament is used across the model. Orca's shipping grouping
|
||||||
|
// algorithm does not read it yet; defaulted so a default-built FilamentInfo
|
||||||
|
// is deterministic. The nozzle-centric engine consumes it later.
|
||||||
|
FilamentUsageType usage_type = FilamentUsageType::ModelOnly;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct MachineFilamentInfo: public FilamentInfo {
|
struct MachineFilamentInfo: public FilamentInfo {
|
||||||
@@ -80,6 +85,20 @@ namespace Slic3r
|
|||||||
void extract_unprintable_limit_indices(const std::vector<std::set<int>>& unprintable_elems, const std::vector<unsigned int>& used_filaments, std::unordered_map<int, std::vector<int>>& unplaceable_limits);
|
void extract_unprintable_limit_indices(const std::vector<std::set<int>>& unprintable_elems, const std::vector<unsigned int>& used_filaments, std::unordered_map<int, std::vector<int>>& unplaceable_limits);
|
||||||
|
|
||||||
bool check_printable(const std::vector<std::set<int>>& groups, const std::map<int, int>& unprintable);
|
bool check_printable(const std::vector<std::set<int>>& groups, const std::map<int, int>& unprintable);
|
||||||
|
|
||||||
|
// Nozzle-centric grouping helpers. The estimate helpers read a LayeredNozzleGroupResult's
|
||||||
|
// per-layer extruder/nozzle usage; the two builders support building the grouping context
|
||||||
|
// (extruder->nozzle inventory) and writing back a resolved map onto only the used-filament
|
||||||
|
// slots.
|
||||||
|
int get_estimate_extruder_change_count(const std::vector<std::vector<unsigned int>>& layer_filaments, const MultiNozzleUtils::LayeredNozzleGroupResult& extruder_nozzle_info);
|
||||||
|
|
||||||
|
int get_estimate_nozzle_change_count(const std::vector<std::vector<unsigned int>>& layer_filaments, const MultiNozzleUtils::LayeredNozzleGroupResult& extruder_nozzle_info);
|
||||||
|
|
||||||
|
std::pair<int, int> get_estimate_extruder_filament_change_count(const MultiNozzleUtils::LayeredNozzleGroupResult& extruder_nozzle_info);
|
||||||
|
|
||||||
|
std::map<int, std::vector<int>> build_extruder_nozzle_list(const std::vector<MultiNozzleUtils::NozzleInfo>& nozzle_list);
|
||||||
|
|
||||||
|
std::vector<int> update_used_filament_values(const std::vector<int>& old_values, const std::vector<int>& new_values, const std::vector<unsigned int>& used_filaments);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -347,6 +347,7 @@ static constexpr const char* OTHER_LAYERS_PRINT_SEQUENCE_NUMS_ATTR = "other_laye
|
|||||||
static constexpr const char* SPIRAL_VASE_MODE = "spiral_mode";
|
static constexpr const char* SPIRAL_VASE_MODE = "spiral_mode";
|
||||||
static constexpr const char* FILAMENT_MAP_MODE_ATTR = "filament_map_mode";
|
static constexpr const char* FILAMENT_MAP_MODE_ATTR = "filament_map_mode";
|
||||||
static constexpr const char* FILAMENT_MAP_ATTR = "filament_maps";
|
static constexpr const char* FILAMENT_MAP_ATTR = "filament_maps";
|
||||||
|
static constexpr const char* FILAMENT_VOL_MAP_ATTR = "filament_volume_maps";
|
||||||
static constexpr const char* LIMIT_FILAMENT_MAP_ATTR = "limit_filament_maps";
|
static constexpr const char* LIMIT_FILAMENT_MAP_ATTR = "limit_filament_maps";
|
||||||
static constexpr const char* GCODE_FILE_ATTR = "gcode_file";
|
static constexpr const char* GCODE_FILE_ATTR = "gcode_file";
|
||||||
static constexpr const char* THUMBNAIL_FILE_ATTR = "thumbnail_file";
|
static constexpr const char* THUMBNAIL_FILE_ATTR = "thumbnail_file";
|
||||||
@@ -699,6 +700,43 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
|||||||
info.id = it->first;
|
info.id = it->first;
|
||||||
info.used_g = used_filament_g;
|
info.used_g = used_filament_g;
|
||||||
info.used_m = used_filament_m;
|
info.used_m = used_filament_m;
|
||||||
|
|
||||||
|
// Stamp each filament's logical-nozzle assignment onto the saved 3mf so the
|
||||||
|
// device/monitor can reconstruct it. NOTE: this block runs for the whole fleet, not just
|
||||||
|
// multi-nozzle prints: reorder_extruders_for_minimum_flush_volume runs unconditionally and
|
||||||
|
// set_nozzle_group_result stores a (non-null) 1-nozzle result even for a single-extruder print,
|
||||||
|
// so result->nozzle_group_result is non-null here for X1/P1/A1/H2S too. Saved-3mf byte-identity
|
||||||
|
// vs the older filament_maps path therefore holds by VALUE-COINCIDENCE, not by skipping: for a
|
||||||
|
// standard 0.4/0.2/0.6/0.8 nozzle the stamped group_id/nozzle_diameter/volume_type render the same
|
||||||
|
// bytes as the filament_maps-derived fallback below (empirically confirmed on an X1C 0.4 saved 3mf:
|
||||||
|
// slice_info.config emits group_id="0" nozzle_diameter="0.40" volume_type="Standard" and
|
||||||
|
// <nozzle id="0" extruder_id="1" nozzle_diameter="0.4" volume_type="Standard"> == the else-path).
|
||||||
|
// The one divergence is a non-standard nozzle (e.g. 0.3/0.5): format_diameter_to_str snaps the
|
||||||
|
// stamped diameter to the nearest of {0.2,0.4,0.6,0.8} (0.5 -> "0.4"), whereas the older path wrote
|
||||||
|
// the raw config value — a metadata-only carry. No g-code effect; the raw nozzle_diameters plate
|
||||||
|
// metadata is unchanged.
|
||||||
|
if (result && result->nozzle_group_result) {
|
||||||
|
auto nozzles_for_filament = result->nozzle_group_result->get_nozzles_for_filament(it->first);
|
||||||
|
if (!nozzles_for_filament.empty()) {
|
||||||
|
info.group_id.reserve(nozzles_for_filament.size());
|
||||||
|
std::set<double> diameters;
|
||||||
|
std::set<NozzleVolumeType> volume_types;
|
||||||
|
for (const auto& nozzle : nozzles_for_filament) {
|
||||||
|
info.group_id.emplace_back(nozzle.group_id);
|
||||||
|
diameters.insert(string_to_double_decimal_point(nozzle.diameter));
|
||||||
|
volume_types.insert(nozzle.volume_type);
|
||||||
|
}
|
||||||
|
std::sort(info.group_id.begin(), info.group_id.end());
|
||||||
|
info.group_id.erase(std::unique(info.group_id.begin(), info.group_id.end()), info.group_id.end());
|
||||||
|
if (!diameters.empty())
|
||||||
|
info.nozzle_diameter = *diameters.begin();
|
||||||
|
if (volume_types.size() > 1)
|
||||||
|
info.nozzle_volume_type = get_nozzle_volume_type_string(nvtHybrid);
|
||||||
|
else if (!volume_types.empty())
|
||||||
|
info.nozzle_volume_type = get_nozzle_volume_type_string(*volume_types.begin());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
auto model_volume_it = ps.model_volumes_per_extruder.find(it->first);
|
auto model_volume_it = ps.model_volumes_per_extruder.find(it->first);
|
||||||
auto support_volume_it = ps.support_volumes_per_extruder.find(it->first);
|
auto support_volume_it = ps.support_volumes_per_extruder.find(it->first);
|
||||||
info.used_for_object = model_volume_it != ps.model_volumes_per_extruder.end() && model_volume_it->second > EPSILON;
|
info.used_for_object = model_volume_it != ps.model_volumes_per_extruder.end() && model_volume_it->second > EPSILON;
|
||||||
@@ -706,6 +744,13 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
|||||||
slice_filaments_info.push_back(info);
|
slice_filaments_info.push_back(info);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Carry the layer-aware grouping result into the plate so the 3mf writer can emit the <nozzle> tags
|
||||||
|
// and the enable_filament_dynamic_map flag. Only a LayeredNozzleGroupResult (the slicer output) is
|
||||||
|
// stored; a device-side StaticNozzleGroupResult loaded from a 3mf is not re-serialized here.
|
||||||
|
auto layered_group_result = std::dynamic_pointer_cast<MultiNozzleUtils::LayeredNozzleGroupResult>(result->nozzle_group_result);
|
||||||
|
if (layered_group_result)
|
||||||
|
nozzle_group_result = *layered_group_result;
|
||||||
|
|
||||||
/* only for test
|
/* only for test
|
||||||
GCodeProcessorResult::SliceWarning sw;
|
GCodeProcessorResult::SliceWarning sw;
|
||||||
sw.msg = BED_TEMP_TOO_HIGH_THAN_FILAMENT;
|
sw.msg = BED_TEMP_TOO_HIGH_THAN_FILAMENT;
|
||||||
@@ -1283,6 +1328,9 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
|||||||
bool _handle_start_config_warning(const char** attributes, unsigned int num_attributes);
|
bool _handle_start_config_warning(const char** attributes, unsigned int num_attributes);
|
||||||
bool _handle_end_config_warning();
|
bool _handle_end_config_warning();
|
||||||
|
|
||||||
|
bool _handle_start_config_nozzle(const char** attributes, unsigned int num_attributes);
|
||||||
|
bool _handle_end_config_nozzle();
|
||||||
|
|
||||||
//BBS: add plater config parse functions
|
//BBS: add plater config parse functions
|
||||||
bool _handle_start_config_plater(const char** attributes, unsigned int num_attributes);
|
bool _handle_start_config_plater(const char** attributes, unsigned int num_attributes);
|
||||||
bool _handle_end_config_plater();
|
bool _handle_end_config_plater();
|
||||||
@@ -1618,8 +1666,10 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
|||||||
plate->is_label_object_enabled = it->second->is_label_object_enabled;
|
plate->is_label_object_enabled = it->second->is_label_object_enabled;
|
||||||
plate->skipped_objects = it->second->skipped_objects;
|
plate->skipped_objects = it->second->skipped_objects;
|
||||||
plate->slice_filaments_info = it->second->slice_filaments_info;
|
plate->slice_filaments_info = it->second->slice_filaments_info;
|
||||||
|
plate->nozzles_info = it->second->nozzles_info;
|
||||||
plate->printer_model_id = it->second->printer_model_id;
|
plate->printer_model_id = it->second->printer_model_id;
|
||||||
plate->nozzle_diameters = it->second->nozzle_diameters;
|
plate->nozzle_diameters = it->second->nozzle_diameters;
|
||||||
|
plate->nozzle_volume_types = it->second->nozzle_volume_types;
|
||||||
plate->filament_maps = it->second->filament_maps;
|
plate->filament_maps = it->second->filament_maps;
|
||||||
plate->filament_change_sequence = it->second->filament_change_sequence;
|
plate->filament_change_sequence = it->second->filament_change_sequence;
|
||||||
plate->nozzle_change_sequence = it->second->nozzle_change_sequence;
|
plate->nozzle_change_sequence = it->second->nozzle_change_sequence;
|
||||||
@@ -2289,9 +2339,11 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
|||||||
plate_data_list[it->first-1]->is_support_used = it->second->is_support_used;
|
plate_data_list[it->first-1]->is_support_used = it->second->is_support_used;
|
||||||
plate_data_list[it->first-1]->is_label_object_enabled = it->second->is_label_object_enabled;
|
plate_data_list[it->first-1]->is_label_object_enabled = it->second->is_label_object_enabled;
|
||||||
plate_data_list[it->first-1]->slice_filaments_info = it->second->slice_filaments_info;
|
plate_data_list[it->first-1]->slice_filaments_info = it->second->slice_filaments_info;
|
||||||
|
plate_data_list[it->first-1]->nozzles_info = it->second->nozzles_info;
|
||||||
plate_data_list[it->first-1]->skipped_objects = it->second->skipped_objects;
|
plate_data_list[it->first-1]->skipped_objects = it->second->skipped_objects;
|
||||||
plate_data_list[it->first-1]->printer_model_id = it->second->printer_model_id;
|
plate_data_list[it->first-1]->printer_model_id = it->second->printer_model_id;
|
||||||
plate_data_list[it->first-1]->nozzle_diameters = it->second->nozzle_diameters;
|
plate_data_list[it->first-1]->nozzle_diameters = it->second->nozzle_diameters;
|
||||||
|
plate_data_list[it->first-1]->nozzle_volume_types = it->second->nozzle_volume_types;
|
||||||
plate_data_list[it->first-1]->filament_maps = it->second->filament_maps;
|
plate_data_list[it->first-1]->filament_maps = it->second->filament_maps;
|
||||||
plate_data_list[it->first-1]->filament_change_sequence = it->second->filament_change_sequence;
|
plate_data_list[it->first-1]->filament_change_sequence = it->second->filament_change_sequence;
|
||||||
plate_data_list[it->first-1]->nozzle_change_sequence = it->second->nozzle_change_sequence;
|
plate_data_list[it->first-1]->nozzle_change_sequence = it->second->nozzle_change_sequence;
|
||||||
@@ -3469,6 +3521,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
|||||||
res = _handle_start_config_filament(attributes, num_attributes);
|
res = _handle_start_config_filament(attributes, num_attributes);
|
||||||
else if (::strcmp(SLICE_WARNING_TAG, name) == 0)
|
else if (::strcmp(SLICE_WARNING_TAG, name) == 0)
|
||||||
res = _handle_start_config_warning(attributes, num_attributes);
|
res = _handle_start_config_warning(attributes, num_attributes);
|
||||||
|
else if (::strcmp(NOZZLE_TAG, name) == 0)
|
||||||
|
res = _handle_start_config_nozzle(attributes, num_attributes);
|
||||||
else if (::strcmp(ASSEMBLE_TAG, name) == 0)
|
else if (::strcmp(ASSEMBLE_TAG, name) == 0)
|
||||||
res = _handle_start_assemble(attributes, num_attributes);
|
res = _handle_start_assemble(attributes, num_attributes);
|
||||||
else if (::strcmp(ASSEMBLE_ITEM_TAG, name) == 0)
|
else if (::strcmp(ASSEMBLE_ITEM_TAG, name) == 0)
|
||||||
@@ -3503,6 +3557,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
|||||||
res = _handle_end_config_plater();
|
res = _handle_end_config_plater();
|
||||||
else if (::strcmp(FILAMENT_TAG, name) == 0)
|
else if (::strcmp(FILAMENT_TAG, name) == 0)
|
||||||
res = _handle_end_config_filament();
|
res = _handle_end_config_filament();
|
||||||
|
else if (::strcmp(NOZZLE_TAG, name) == 0)
|
||||||
|
res = _handle_end_config_nozzle();
|
||||||
else if (::strcmp(INSTANCE_TAG, name) == 0)
|
else if (::strcmp(INSTANCE_TAG, name) == 0)
|
||||||
res = _handle_end_config_plater_instance();
|
res = _handle_end_config_plater_instance();
|
||||||
else if (::strcmp(ASSEMBLE_TAG, name) == 0)
|
else if (::strcmp(ASSEMBLE_TAG, name) == 0)
|
||||||
@@ -4460,6 +4516,19 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
|||||||
m_curr_plater->config.set_key_value("filament_map", new ConfigOptionInts(filament_map));
|
m_curr_plater->config.set_key_value("filament_map", new ConfigOptionInts(filament_map));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (key == FILAMENT_VOL_MAP_ATTR) {
|
||||||
|
if (m_curr_plater){
|
||||||
|
auto filament_volume_map = get_vector_from_string(value);
|
||||||
|
for (size_t idx = 0; idx < filament_volume_map.size(); ++idx) {
|
||||||
|
// Only Standard(0)/High Flow(1) are currently supported; clamp any higher
|
||||||
|
// volume-type (TPU High Flow/Hybrid) back to Standard until they are wired in.
|
||||||
|
if (filament_volume_map[idx] > 1) {
|
||||||
|
filament_volume_map[idx] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m_curr_plater->config.set_key_value("filament_volume_map", new ConfigOptionInts(filament_volume_map));
|
||||||
|
}
|
||||||
|
}
|
||||||
else if (key == GCODE_FILE_ATTR)
|
else if (key == GCODE_FILE_ATTR)
|
||||||
{
|
{
|
||||||
m_curr_plater->gcode_file = value;
|
m_curr_plater->gcode_file = value;
|
||||||
@@ -4569,6 +4638,11 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
|||||||
if (m_curr_plater)
|
if (m_curr_plater)
|
||||||
m_curr_plater->printer_model_id = value;
|
m_curr_plater->printer_model_id = value;
|
||||||
}
|
}
|
||||||
|
else if (key == NOZZLE_VOLUME_TYPE_ATTR)
|
||||||
|
{
|
||||||
|
if (m_curr_plater)
|
||||||
|
m_curr_plater->nozzle_volume_types = value;
|
||||||
|
}
|
||||||
else if (key == NOZZLE_DIAMETERS_ATTR)
|
else if (key == NOZZLE_DIAMETERS_ATTR)
|
||||||
{
|
{
|
||||||
if (m_curr_plater)
|
if (m_curr_plater)
|
||||||
@@ -4622,6 +4696,41 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool _BBS_3MF_Importer::_handle_start_config_nozzle(const char** attributes, unsigned int num_attributes)
|
||||||
|
{
|
||||||
|
// Read the per-plate <nozzle> tags. Older 3mf without <nozzle> tags leave nozzles_info
|
||||||
|
// empty; load_nozzle_infos_with_compatibility then rebuilds the list from the per-filament
|
||||||
|
// group_id / filament_map on the device side.
|
||||||
|
if (m_curr_plater) {
|
||||||
|
// id="0" extruder_id="1" nozzle_diameter="0.4" volume_type="Standard"
|
||||||
|
std::string id = bbs_get_attribute_value_string(attributes, num_attributes, "id");
|
||||||
|
std::string extruder_id = bbs_get_attribute_value_string(attributes, num_attributes, "extruder_id");
|
||||||
|
std::string nozzle_diameter= bbs_get_attribute_value_string(attributes, num_attributes, "nozzle_diameter");
|
||||||
|
std::string volume_type = bbs_get_attribute_value_string(attributes, num_attributes, "volume_type");
|
||||||
|
|
||||||
|
auto volume_type_str_to_enum = ConfigOptionEnum<NozzleVolumeType>::get_enum_values();
|
||||||
|
|
||||||
|
MultiNozzleUtils::NozzleInfo nozzle_info;
|
||||||
|
nozzle_info.group_id = atoi(id.c_str());
|
||||||
|
nozzle_info.extruder_id = atoi(extruder_id.c_str()) - 1;
|
||||||
|
nozzle_info.diameter = nozzle_diameter;
|
||||||
|
|
||||||
|
if (volume_type_str_to_enum.count(volume_type))
|
||||||
|
nozzle_info.volume_type = NozzleVolumeType(volume_type_str_to_enum.at(volume_type));
|
||||||
|
else
|
||||||
|
nozzle_info.volume_type = NozzleVolumeType::nvtStandard;
|
||||||
|
|
||||||
|
m_curr_plater->nozzles_info.push_back(nozzle_info);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _BBS_3MF_Importer::_handle_end_config_nozzle()
|
||||||
|
{
|
||||||
|
// do nothing
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
bool _BBS_3MF_Importer::_handle_start_config_warning(const char** attributes, unsigned int num_attributes)
|
bool _BBS_3MF_Importer::_handle_start_config_warning(const char** attributes, unsigned int num_attributes)
|
||||||
{
|
{
|
||||||
if (m_curr_plater) {
|
if (m_curr_plater) {
|
||||||
@@ -7980,6 +8089,18 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
|||||||
stream << "\"/>\n";
|
stream << "\"/>\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ConfigOptionInts* filament_volume_maps_opt = plate_data->config.option<ConfigOptionInts>("filament_volume_map");
|
||||||
|
if (filament_map_mode_opt != nullptr && filament_volume_maps_opt != nullptr) {
|
||||||
|
stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << FILAMENT_VOL_MAP_ATTR << "\" " << VALUE_ATTR << "=\"";
|
||||||
|
const std::vector<int>& volume_values = filament_volume_maps_opt->values;
|
||||||
|
for (int i = 0; i < volume_values.size(); ++i) {
|
||||||
|
stream << volume_values[i];
|
||||||
|
if (i != (volume_values.size() - 1))
|
||||||
|
stream << " ";
|
||||||
|
}
|
||||||
|
stream << "\"/>\n";
|
||||||
|
}
|
||||||
|
|
||||||
if (save_gcode)
|
if (save_gcode)
|
||||||
stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << GCODE_FILE_ATTR << "\" " << VALUE_ATTR << "=\"" << std::boolalpha << xml_escape(plate_data->gcode_file) << "\"/>\n";
|
stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << GCODE_FILE_ATTR << "\" " << VALUE_ATTR << "=\"" << std::boolalpha << xml_escape(plate_data->gcode_file) << "\"/>\n";
|
||||||
if (!plate_data->gcode_file.empty()) {
|
if (!plate_data->gcode_file.empty()) {
|
||||||
@@ -8119,7 +8240,12 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
|||||||
[](unsigned int filament_id) { return filament_id + 1; });
|
[](unsigned int filament_id) { return filament_id + 1; });
|
||||||
|
|
||||||
const std::string plate_key = "plate_" + std::to_string(idx + 1);
|
const std::string plate_key = "plate_" + std::to_string(idx + 1);
|
||||||
sequence_json[plate_key]["sequence"] = filament_sequence;
|
// Dynamic-map plates write the sequence under "filament_sequence"; every other plate (the
|
||||||
|
// whole shipping fleet + H2C static mode) keeps the "sequence" key, so the saved 3mf is
|
||||||
|
// byte-identical to the older format. The reader accepts both.
|
||||||
|
const bool enable_dynamic_map = plate_data->nozzle_group_result && plate_data->nozzle_group_result->is_support_dynamic_nozzle_map();
|
||||||
|
const std::string seq_key = enable_dynamic_map ? "filament_sequence" : "sequence";
|
||||||
|
sequence_json[plate_key][seq_key] = filament_sequence;
|
||||||
sequence_json[plate_key]["nozzle_sequence"] = plate_data->nozzle_change_sequence;
|
sequence_json[plate_key]["nozzle_sequence"] = plate_data->nozzle_change_sequence;
|
||||||
sequence_json[plate_key]["optimal_assignment"] = plate_data->optimal_assignment;
|
sequence_json[plate_key]["optimal_assignment"] = plate_data->optimal_assignment;
|
||||||
}
|
}
|
||||||
@@ -8214,6 +8340,14 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
|||||||
stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << OUTSIDE_ATTR << "\" " << VALUE_ATTR << "=\"" << std::boolalpha<< plate_data->toolpath_outside << "\"/>\n";
|
stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << OUTSIDE_ATTR << "\" " << VALUE_ATTR << "=\"" << std::boolalpha<< plate_data->toolpath_outside << "\"/>\n";
|
||||||
stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << SUPPORT_USED_ATTR << "\" " << VALUE_ATTR << "=\"" << std::boolalpha<< plate_data->is_support_used << "\"/>\n";
|
stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << SUPPORT_USED_ATTR << "\" " << VALUE_ATTR << "=\"" << std::boolalpha<< plate_data->is_support_used << "\"/>\n";
|
||||||
stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << LABEL_OBJECT_ENABLED_ATTR << "\" " << VALUE_ATTR << "=\"" << std::boolalpha<< plate_data->is_label_object_enabled << "\"/>\n";
|
stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << LABEL_OBJECT_ENABLED_ATTR << "\" " << VALUE_ATTR << "=\"" << std::boolalpha<< plate_data->is_label_object_enabled << "\"/>\n";
|
||||||
|
// Report the plate's dynamic-map state from the grouping result. The result is present
|
||||||
|
// for the whole fleet (a static single-nozzle result too), so this if-branch is normally
|
||||||
|
// taken; is_support_dynamic_nozzle_map() is false for any non-dynamic (static /
|
||||||
|
// single-extruder) result ⇒ byte-identical to the previously hard-coded value. The else
|
||||||
|
// is a defensive fallback for a missing result.
|
||||||
|
if (plate_data && plate_data->nozzle_group_result)
|
||||||
|
stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << ENABLE_FILAMENT_DYNAMIC_MAP_ATTR << "\" " << VALUE_ATTR << "=\"" << std::boolalpha << plate_data->nozzle_group_result->is_support_dynamic_nozzle_map() << "\"/>\n";
|
||||||
|
else
|
||||||
stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << ENABLE_FILAMENT_DYNAMIC_MAP_ATTR << "\" " << VALUE_ATTR << "=\"" << std::boolalpha << false << "\"/>\n";
|
stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << ENABLE_FILAMENT_DYNAMIC_MAP_ATTR << "\" " << VALUE_ATTR << "=\"" << std::boolalpha << false << "\"/>\n";
|
||||||
{
|
{
|
||||||
bool has_filament_switcher = config.has("has_filament_switcher") ? config.opt_bool("has_filament_switcher") : false;
|
bool has_filament_switcher = config.has("has_filament_switcher") ? config.opt_bool("has_filament_switcher") : false;
|
||||||
@@ -8349,6 +8483,20 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
|||||||
stream << " <" << SLICE_WARNING_TAG << " msg=\"" << it->msg << "\" level=\"" << std::to_string(it->level) << "\" error_code =\"" << it->error_code << "\" />\n";
|
stream << " <" << SLICE_WARNING_TAG << " msg=\"" << it->msg << "\" level=\"" << std::to_string(it->level) << "\" error_code =\"" << it->error_code << "\" />\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Emit the <nozzle> tags from the grouping result when present. The result is present for
|
||||||
|
// the whole fleet (see the value-coincidence note in parse_filament_info), so this if-branch
|
||||||
|
// is normally taken; for single-nozzle-per-extruder prints get_used_nozzles_in_extruder()
|
||||||
|
// yields one nozzle per used extruder whose serialize() output is byte-identical to the
|
||||||
|
// filament_maps-derived else-path below for a standard nozzle diameter (a non-standard
|
||||||
|
// diameter snaps via format_diameter_to_str — the metadata-only carry). Only genuinely
|
||||||
|
// multi-nozzle prints emit per-nozzle tags the extruder map cannot express. The else
|
||||||
|
// is a defensive fallback for a missing result.
|
||||||
|
if (plate_data->nozzle_group_result) {
|
||||||
|
auto used_nozzle_list = plate_data->nozzle_group_result->get_used_nozzles_in_extruder();
|
||||||
|
for (auto& used_nozzle : used_nozzle_list) {
|
||||||
|
stream << " <" << NOZZLE_TAG << " " << used_nozzle.serialize() << "/>\n";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
for (int nozzle_group_id : used_nozzle_groups) {
|
for (int nozzle_group_id : used_nozzle_groups) {
|
||||||
stream << " <" << NOZZLE_TAG << " "
|
stream << " <" << NOZZLE_TAG << " "
|
||||||
<< "id=\"" << nozzle_group_id << "\" "
|
<< "id=\"" << nozzle_group_id << "\" "
|
||||||
@@ -8356,6 +8504,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
|||||||
<< "nozzle_diameter=\"" << get_nozzle_diameter_str(nozzle_group_id) << "\" "
|
<< "nozzle_diameter=\"" << get_nozzle_diameter_str(nozzle_group_id) << "\" "
|
||||||
<< "volume_type=\"" << get_nozzle_volume_type(nozzle_group_id) << "\"/>\n";
|
<< "volume_type=\"" << get_nozzle_volume_type(nozzle_group_id) << "\"/>\n";
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!plate_data->layer_filaments.empty()) {
|
if (!plate_data->layer_filaments.empty()) {
|
||||||
stream << " <" << LAYER_FILAMENT_LISTS_TAG << ">\n";
|
stream << " <" << LAYER_FILAMENT_LISTS_TAG << ">\n";
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ struct PlateData
|
|||||||
std::map<int, std::pair<int, int>> obj_inst_map;
|
std::map<int, std::pair<int, int>> obj_inst_map;
|
||||||
std::string printer_model_id;
|
std::string printer_model_id;
|
||||||
std::string nozzle_diameters;
|
std::string nozzle_diameters;
|
||||||
|
std::string nozzle_volume_types;
|
||||||
std::string gcode_file;
|
std::string gcode_file;
|
||||||
std::string gcode_file_md5;
|
std::string gcode_file_md5;
|
||||||
std::string thumbnail_file;
|
std::string thumbnail_file;
|
||||||
@@ -102,6 +103,13 @@ struct PlateData
|
|||||||
std::vector<unsigned int> nozzle_change_sequence;
|
std::vector<unsigned int> nozzle_change_sequence;
|
||||||
std::vector<int> optimal_assignment;
|
std::vector<int> optimal_assignment;
|
||||||
|
|
||||||
|
// Multi-nozzle grouping surface. nozzles_info accumulates the <nozzle> tags read from a
|
||||||
|
// gcode.3mf; nozzle_group_result is the slicer's per-filament→nozzle assignment carried into the
|
||||||
|
// saved 3mf metadata (write) and reconstructed on load. Both are empty/nullopt for single-nozzle
|
||||||
|
// prints, so the saved-3mf output for single-nozzle printers is byte-identical.
|
||||||
|
std::vector<MultiNozzleUtils::NozzleInfo> nozzles_info;
|
||||||
|
std::optional<MultiNozzleUtils::LayeredNozzleGroupResult> nozzle_group_result;
|
||||||
|
|
||||||
// Hexadecimal number,
|
// Hexadecimal number,
|
||||||
// the 0th digit corresponds to extruder 1
|
// the 0th digit corresponds to extruder 1
|
||||||
// the 1th digit corresponds to extruder 2
|
// the 1th digit corresponds to extruder 2
|
||||||
|
|||||||
+782
-80
File diff suppressed because it is too large
Load Diff
+48
-1
@@ -252,7 +252,8 @@ public:
|
|||||||
std::string travel_to(const Point& point, ExtrusionRole role, std::string comment, double z = DBL_MAX);
|
std::string travel_to(const Point& point, ExtrusionRole role, std::string comment, double z = DBL_MAX);
|
||||||
bool needs_retraction(const Polyline& travel, ExtrusionRole role, LiftType& lift_type);
|
bool needs_retraction(const Polyline& travel, ExtrusionRole role, LiftType& lift_type);
|
||||||
std::string retract(bool toolchange = false, bool is_last_retraction = false, LiftType lift_type = LiftType::NormalLift, bool apply_instantly = false, ExtrusionRole role = erNone);
|
std::string retract(bool toolchange = false, bool is_last_retraction = false, LiftType lift_type = LiftType::NormalLift, bool apply_instantly = false, ExtrusionRole role = erNone);
|
||||||
std::string unretract() { return m_writer.unlift() + m_writer.unretract(); }
|
// extra_retract forwards a PETG pre-extrusion over-extrusion; default 0 -> identical to the plain deretract.
|
||||||
|
std::string unretract(float extra_retract = 0.f) { return m_writer.unlift() + m_writer.unretract(extra_retract); }
|
||||||
std::string set_extruder(unsigned int extruder_id, double print_z, bool by_object=false, int toolchange_temp_override = -1);
|
std::string set_extruder(unsigned int extruder_id, double print_z, bool by_object=false, int toolchange_temp_override = -1);
|
||||||
bool is_BBL_Printer();
|
bool is_BBL_Printer();
|
||||||
WipeTowerType wipe_tower_type();
|
WipeTowerType wipe_tower_type();
|
||||||
@@ -402,6 +403,11 @@ private:
|
|||||||
std::string preamble();
|
std::string preamble();
|
||||||
// BBS
|
// BBS
|
||||||
std::string change_layer(coordf_t print_z);
|
std::string change_layer(coordf_t print_z);
|
||||||
|
// Bedslinger model: derive the Y-axis acceleration limit from the machine force/bed-mass config
|
||||||
|
// and the mass already printed. Yields the min machine Y acceleration when the A2L config keys are
|
||||||
|
// unset (i.e. every existing printer), so it is inert for them.
|
||||||
|
void mass_load_limited_machine_acceleration(const PrintStatistics &curr_print_statistics, const Print &print,
|
||||||
|
double &y_acceleration_limit_res, double &accumulated_mass_res);
|
||||||
// Orca: pass the complete collection of region perimeters to the extrude loop to check whether the wipe before external loop
|
// Orca: pass the complete collection of region perimeters to the extrude loop to check whether the wipe before external loop
|
||||||
// should be executed
|
// should be executed
|
||||||
std::string extrude_entity(const ExtrusionEntity& entity,
|
std::string extrude_entity(const ExtrusionEntity& entity,
|
||||||
@@ -500,6 +506,18 @@ private:
|
|||||||
std::string extrude_infill(const Print& print, const std::vector<ObjectByExtruder::Island::Region>& by_region, bool ironing);
|
std::string extrude_infill(const Print& print, const std::vector<ObjectByExtruder::Island::Region>& by_region, bool ironing);
|
||||||
std::string extrude_support(const ExtrusionEntityCollection& support_fills, const ExtrusionRole support_extrusion_role);
|
std::string extrude_support(const ExtrusionEntityCollection& support_fills, const ExtrusionRole support_extrusion_role);
|
||||||
|
|
||||||
|
// Farthest-point timelapse: find the extrusion point farthest from camera (0,0)
|
||||||
|
void compute_farthest_point(const std::vector<LayerToPrint> &layers, int most_used_extruder,
|
||||||
|
const std::map<std::pair<const SupportLayer *, ExtrusionRole>, unsigned int> &support_filaments);
|
||||||
|
// Build the per-layer timelapse snapshot g-code (safe-position or, when skip_pos_pick,
|
||||||
|
// an inline photo at the current head position). Extracted from the former process_layer lambda so the
|
||||||
|
// per-extrusion farthest-point hook (_extrude) can call it too. Identical to the old lambda when the
|
||||||
|
// farthest-point subsystem is disabled (skip_pos_pick=false, m_farthest_point_timelapse.enabled=false).
|
||||||
|
std::string generate_timelapse_gcode(const Print &print, coordf_t print_z, int most_used_extruder,
|
||||||
|
const std::set<size_t> *layer_object_label_ids,
|
||||||
|
const std::vector<const PrintObject*> *printed_objects,
|
||||||
|
bool skip_pos_pick = false);
|
||||||
|
|
||||||
// BBS
|
// BBS
|
||||||
LiftType to_lift_type(ZHopType z_hop_types);
|
LiftType to_lift_type(ZHopType z_hop_types);
|
||||||
|
|
||||||
@@ -556,6 +574,32 @@ private:
|
|||||||
AvoidCrossingPerimeters m_avoid_crossing_perimeters;
|
AvoidCrossingPerimeters m_avoid_crossing_perimeters;
|
||||||
RetractWhenCrossingPerimeters m_retract_when_crossing_perimeters;
|
RetractWhenCrossingPerimeters m_retract_when_crossing_perimeters;
|
||||||
TimelapsePosPicker m_timelapse_pos_picker;
|
TimelapsePosPicker m_timelapse_pos_picker;
|
||||||
|
|
||||||
|
// Farthest-point timelapse context. Corexy-only refinement layered on top of the existing
|
||||||
|
// timelapse_type. All fields default to the inert state; `enabled` is (re)computed each layer in
|
||||||
|
// process_layer and is false whenever the farthest_point_timelapse config toggle is off, the printer
|
||||||
|
// is i3 (psI3), or timelapse_type is not traditional — so every shipping printer that does not set the
|
||||||
|
// toggle is identical to the previous path.
|
||||||
|
struct FarthestPointTimelapseContext {
|
||||||
|
// Whether farthest-point timelapse is active for this layer
|
||||||
|
bool enabled{false};
|
||||||
|
// The farthest extrusion point from camera (0,0) in global scaled coordinates (includes plate origin + inst.shift)
|
||||||
|
Point farthest_point;
|
||||||
|
// farthest_point converted to mm (gcode coordinate space, includes plate origin)
|
||||||
|
Vec2d farthest_gcode_pos{0, 0};
|
||||||
|
// Extruder index (0-based) that prints the farthest point
|
||||||
|
int farthest_extruder_id{0};
|
||||||
|
// Whether the farthest point is printed by the photo head (most_used_extruder)
|
||||||
|
bool farthest_is_photo_head{false};
|
||||||
|
// Whether inline timelapse gcode has already been inserted on this layer
|
||||||
|
bool inserted_this_layer{false};
|
||||||
|
// The extruder used most on this layer, chosen as the photo head
|
||||||
|
int most_used_extruder{0};
|
||||||
|
// Object labels for the current layer, used when inline timelapse is inserted from extrusion code.
|
||||||
|
std::set<size_t> layer_object_label_ids;
|
||||||
|
};
|
||||||
|
FarthestPointTimelapseContext m_farthest_point_timelapse;
|
||||||
|
|
||||||
bool m_enable_loop_clipping;
|
bool m_enable_loop_clipping;
|
||||||
//resonance avoidance
|
//resonance avoidance
|
||||||
bool m_resonance_avoidance;
|
bool m_resonance_avoidance;
|
||||||
@@ -595,6 +639,9 @@ private:
|
|||||||
float m_last_layer_z{ 0.0f };
|
float m_last_layer_z{ 0.0f };
|
||||||
float m_max_layer_z{ 0.0f };
|
float m_max_layer_z{ 0.0f };
|
||||||
float m_last_width{ 0.0f };
|
float m_last_width{ 0.0f };
|
||||||
|
// Bedslinger mass model: cumulative printed mass at the previous layer, used to derive
|
||||||
|
// the current layer mass for the per-layer Y acceleration limit (curr_y_acceleration_limit).
|
||||||
|
double m_last_layer_accumulated_mass{ 0.0 };
|
||||||
|
|
||||||
// Always check gcode placeholders when building in debug mode.
|
// Always check gcode placeholders when building in debug mode.
|
||||||
#if !defined(NDEBUG)
|
#if !defined(NDEBUG)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -6,6 +6,7 @@
|
|||||||
#include "libslic3r/ExtrusionEntity.hpp"
|
#include "libslic3r/ExtrusionEntity.hpp"
|
||||||
#include "libslic3r/PrintConfig.hpp"
|
#include "libslic3r/PrintConfig.hpp"
|
||||||
#include "libslic3r/CustomGCode.hpp"
|
#include "libslic3r/CustomGCode.hpp"
|
||||||
|
#include "libslic3r/MultiNozzleUtils.hpp"
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <array>
|
#include <array>
|
||||||
@@ -43,6 +44,23 @@ class Print;
|
|||||||
Count
|
Count
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Classifies why a wipe-tower / change_filament / time-lapse region is safe to relocate a
|
||||||
|
// pre-heat M104 into, for the pre-heat/pre-cool injector. The shipping time_lapse_gcode
|
||||||
|
// template (timelapse-on by default) emits SKIPPABLE_* on essentially every slice, so the
|
||||||
|
// "timelapse" payload -> stTimelapse classification is exercised widely.
|
||||||
|
enum SkipType
|
||||||
|
{
|
||||||
|
stTimelapse,
|
||||||
|
stHeadWrapDetect,
|
||||||
|
stOther,
|
||||||
|
stNone
|
||||||
|
};
|
||||||
|
|
||||||
|
const std::unordered_map<std::string_view, SkipType> skip_type_map{
|
||||||
|
{"timelapse", SkipType::stTimelapse},
|
||||||
|
{"head_wrap_detect", SkipType::stHeadWrapDetect}
|
||||||
|
};
|
||||||
|
|
||||||
struct PrintEstimatedStatistics
|
struct PrintEstimatedStatistics
|
||||||
{
|
{
|
||||||
enum class ETimeMode : unsigned char
|
enum class ETimeMode : unsigned char
|
||||||
@@ -77,6 +95,10 @@ class Print;
|
|||||||
|
|
||||||
std::array<Mode, static_cast<size_t>(ETimeMode::Count)> modes;
|
std::array<Mode, static_cast<size_t>(ETimeMode::Count)> modes;
|
||||||
unsigned int total_filament_changes;
|
unsigned int total_filament_changes;
|
||||||
|
// Number of filament changes that actually re-flush a nozzle (a filament-in-nozzle change
|
||||||
|
// onto a non-empty nozzle), tracked only by the richer multi-nozzle hotend-change time model.
|
||||||
|
// Stays 0 for single-nozzle printers (X1/P1/A1/H2S/A2L), which never enter the two-arg model.
|
||||||
|
unsigned int total_flush_filament_changes;
|
||||||
unsigned int total_extruder_changes;
|
unsigned int total_extruder_changes;
|
||||||
float total_filament_load_time;
|
float total_filament_load_time;
|
||||||
float total_filament_unload_time;
|
float total_filament_unload_time;
|
||||||
@@ -101,6 +123,7 @@ class Print;
|
|||||||
flush_per_filament.clear();
|
flush_per_filament.clear();
|
||||||
used_filaments_per_role.clear();
|
used_filaments_per_role.clear();
|
||||||
total_filament_changes = 0;
|
total_filament_changes = 0;
|
||||||
|
total_flush_filament_changes = 0;
|
||||||
total_extruder_changes = 0;
|
total_extruder_changes = 0;
|
||||||
total_filament_load_time = 0.0f;
|
total_filament_load_time = 0.0f;
|
||||||
total_filament_unload_time = 0.0f;
|
total_filament_unload_time = 0.0f;
|
||||||
@@ -166,6 +189,14 @@ class Print;
|
|||||||
ConflictResultOpt conflict_result;
|
ConflictResultOpt conflict_result;
|
||||||
GCodeCheckResult gcode_check_result;
|
GCodeCheckResult gcode_check_result;
|
||||||
FilamentPrintableResult filament_printable_reuslt;
|
FilamentPrintableResult filament_printable_reuslt;
|
||||||
|
// The per-filament -> logical-nozzle grouping the slicer computed for this
|
||||||
|
// result, surfaced onto the object the device GUI reads
|
||||||
|
// (plater->background_process().get_current_gcode_result()). Populated only from
|
||||||
|
// Print::get_layered_nozzle_group_result() (ToolOrdering's static L/R + rack subset);
|
||||||
|
// default-empty (null) and read by no g-code emitter, so it is invisible in the emitted
|
||||||
|
// g-code. Consumed by the print-dispatch nozzle mapping (DevNozzleMappingCtrl) via
|
||||||
|
// DevUtilBackend::GetNozzleGroupResult.
|
||||||
|
std::shared_ptr<MultiNozzleUtils::NozzleGroupResultBase> nozzle_group_result;
|
||||||
float initial_layer_time;
|
float initial_layer_time;
|
||||||
|
|
||||||
struct SettingsIds
|
struct SettingsIds
|
||||||
@@ -263,6 +294,10 @@ class Print;
|
|||||||
std::vector<SliceWarning> warnings;
|
std::vector<SliceWarning> warnings;
|
||||||
int nozzle_hrc;
|
int nozzle_hrc;
|
||||||
std::vector<NozzleType> nozzle_type;
|
std::vector<NozzleType> nozzle_type;
|
||||||
|
// Per-extruder physical hotend type. Fed to the pre-heat injector's TimeProcessContext
|
||||||
|
// (mixed-type X2D workaround). Populated in apply_config; unused until the injector side-pass
|
||||||
|
// consumes it.
|
||||||
|
std::vector<ExtruderType> extruder_types;
|
||||||
// first key stores filaments, second keys stores the layer ranges(enclosed) that use the filaments
|
// first key stores filaments, second keys stores the layer ranges(enclosed) that use the filaments
|
||||||
std::unordered_map<std::vector<unsigned int>, std::vector<std::pair<int, int>>,FilamentSequenceHash> layer_filaments;
|
std::unordered_map<std::vector<unsigned int>, std::vector<std::pair<int, int>>,FilamentSequenceHash> layer_filaments;
|
||||||
std::vector<unsigned int> nozzle_change_sequence;
|
std::vector<unsigned int> nozzle_change_sequence;
|
||||||
@@ -271,6 +306,11 @@ class Print;
|
|||||||
// first key stores `from` filament, second keys stores the `to` filament
|
// first key stores `from` filament, second keys stores the `to` filament
|
||||||
std::map<std::pair<int,int>, int > filament_change_count_map;
|
std::map<std::pair<int,int>, int > filament_change_count_map;
|
||||||
|
|
||||||
|
// Accumulated print time spent inside SKIPPABLE regions, per skip type. Populated by the time
|
||||||
|
// estimator; consumed only downstream. The shipping time_lapse_gcode template emits SKIPPABLE_*
|
||||||
|
// widely, so this is typically populated (stTimelapse) on most slices.
|
||||||
|
std::unordered_map<SkipType, float> skippable_part_time;
|
||||||
|
|
||||||
BedType bed_type = BedType::btCount;
|
BedType bed_type = BedType::btCount;
|
||||||
void reset();
|
void reset();
|
||||||
|
|
||||||
@@ -304,11 +344,18 @@ class Print;
|
|||||||
gcode_check_result = other.gcode_check_result;
|
gcode_check_result = other.gcode_check_result;
|
||||||
limit_filament_maps = other.limit_filament_maps;
|
limit_filament_maps = other.limit_filament_maps;
|
||||||
filament_printable_reuslt = other.filament_printable_reuslt;
|
filament_printable_reuslt = other.filament_printable_reuslt;
|
||||||
|
// Orca: copy the shared grouping result so a copied result keeps it (shared_ptr =>
|
||||||
|
// memory-safe), rather than leaving a stale pointer on the target. No g-code effect either way.
|
||||||
|
nozzle_group_result = other.nozzle_group_result;
|
||||||
|
// Keep the per-extruder hotend types on a copied result (injector input).
|
||||||
|
extruder_types = other.extruder_types;
|
||||||
layer_filaments = other.layer_filaments;
|
layer_filaments = other.layer_filaments;
|
||||||
filament_change_sequence = other.filament_change_sequence;
|
filament_change_sequence = other.filament_change_sequence;
|
||||||
nozzle_change_sequence = other.nozzle_change_sequence;
|
nozzle_change_sequence = other.nozzle_change_sequence;
|
||||||
optimal_assignment = other.optimal_assignment;
|
optimal_assignment = other.optimal_assignment;
|
||||||
filament_change_count_map = other.filament_change_count_map;
|
filament_change_count_map = other.filament_change_count_map;
|
||||||
|
// Keep the SKIPPABLE per-type time on a copied result.
|
||||||
|
skippable_part_time = other.skippable_part_time;
|
||||||
initial_layer_time = other.initial_layer_time;
|
initial_layer_time = other.initial_layer_time;
|
||||||
#if ENABLE_GCODE_VIEWER_STATISTICS
|
#if ENABLE_GCODE_VIEWER_STATISTICS
|
||||||
time = other.time;
|
time = other.time;
|
||||||
@@ -319,6 +366,75 @@ class Print;
|
|||||||
void unlock() const { result_mutex.unlock(); }
|
void unlock() const { result_mutex.unlock(); }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// First-pass usage-block descriptors for the pre-heat/pre-cool injector. FilamentUsageBlock
|
||||||
|
// records the [lower,upper) output-line-id span a single filament occupies; ExtruderUsageBlcok
|
||||||
|
// (the "Blcok" typo is intentional) records the span an extruder is active in, with the start/end
|
||||||
|
// filament + logical-nozzle ids and the post-extrusion (pre-switch) partial-free sub-range. Built
|
||||||
|
// during run_post_process, consumed only by the injector side-pass under the enable_pre_heating gate.
|
||||||
|
namespace ExtruderPreHeating
|
||||||
|
{
|
||||||
|
struct FilamentUsageBlock
|
||||||
|
{
|
||||||
|
int filament_id;
|
||||||
|
int extruder_id;
|
||||||
|
int nozzle_id;
|
||||||
|
unsigned int lower_gcode_id;
|
||||||
|
unsigned int upper_gcode_id; // [lower_gcode_id,upper_gcode_id) uses current filament , upper gcode id will be set after finding next block
|
||||||
|
FilamentUsageBlock(int filament_id_, int extruder_id_, int nozzle_id_, unsigned int lower_gcode_id_, unsigned int upper_gcode_id_) :filament_id(filament_id_), extruder_id(extruder_id_), nozzle_id(nozzle_id_), lower_gcode_id(lower_gcode_id_), upper_gcode_id(upper_gcode_id_) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Describle the usage of a exturder in a section
|
||||||
|
*
|
||||||
|
* The strucutre stores the start and end lines of the sections as well as
|
||||||
|
* the filament used at the beginning and end of the section.
|
||||||
|
* Post extrusion means the final extrusion before switching to the next extruder.
|
||||||
|
*
|
||||||
|
* Simplified GCode Flow:
|
||||||
|
* 1.Extruder Change Block (ext0 switch to ext1)
|
||||||
|
* 2.Extruder Usage Block (use ext1 to print)
|
||||||
|
* 3.Extruder Change Block (ext1 switch to ext0)
|
||||||
|
* 4.Extruder Usage Block (use ext0 to print)
|
||||||
|
* 5.Extruder Change Block (ext0 switch to ex1)
|
||||||
|
* ...
|
||||||
|
*
|
||||||
|
* So the construct of extruder usage block relys on two extruder change block
|
||||||
|
*/
|
||||||
|
struct ExtruderUsageBlcok
|
||||||
|
{
|
||||||
|
int extruder_id = -1;
|
||||||
|
unsigned int start_id = -1;
|
||||||
|
unsigned int end_id = -1;
|
||||||
|
int start_filament = -1;
|
||||||
|
int end_filament = -1;
|
||||||
|
int start_nozzle_id = -1;
|
||||||
|
int end_nozzle_id = -1;
|
||||||
|
unsigned int post_extrusion_start_id = -1;
|
||||||
|
unsigned int post_extrusion_end_id = -1;
|
||||||
|
bool ignore_cooling_before_tower = false;
|
||||||
|
|
||||||
|
void initialize_step_1(int extruder_id_, int start_id_, int start_filament_, int start_nozzle_id_) {
|
||||||
|
extruder_id = extruder_id_;
|
||||||
|
start_id = start_id_;
|
||||||
|
start_filament = start_filament_;
|
||||||
|
start_nozzle_id = start_nozzle_id_;
|
||||||
|
};
|
||||||
|
void initialize_step_2(int post_extrusion_start_id_) {
|
||||||
|
post_extrusion_start_id = post_extrusion_start_id_;
|
||||||
|
}
|
||||||
|
void initialize_step_3(int end_id_, int end_filament_, int post_extrusion_end_id_, int end_nozzle_id_) {
|
||||||
|
end_id = end_id_;
|
||||||
|
end_filament = end_filament_;
|
||||||
|
post_extrusion_end_id = post_extrusion_end_id_;
|
||||||
|
end_nozzle_id = end_nozzle_id_;
|
||||||
|
}
|
||||||
|
void reset() {
|
||||||
|
*this = ExtruderUsageBlcok();
|
||||||
|
}
|
||||||
|
ExtruderUsageBlcok() = default;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class CommandProcessor {
|
class CommandProcessor {
|
||||||
public:
|
public:
|
||||||
@@ -347,6 +463,24 @@ class Print;
|
|||||||
static const std::string VFlush_Start_Tag;
|
static const std::string VFlush_Start_Tag;
|
||||||
static const std::string VFlush_End_Tag;
|
static const std::string VFlush_End_Tag;
|
||||||
static const std::string External_Purge_Tag;
|
static const std::string External_Purge_Tag;
|
||||||
|
public:
|
||||||
|
// Orca: SKIPPABLE region tags, stored as static strings (the FLUSH idiom above) rather than
|
||||||
|
// a CustomETags/CustomTags array. Public so the emission sites (WipeTower / change_filament
|
||||||
|
// path) can reference them single-sourced.
|
||||||
|
static const std::string Skippable_Start_Tag;
|
||||||
|
static const std::string Skippable_End_Tag;
|
||||||
|
static const std::string Skippable_Type_Tag;
|
||||||
|
// Orca: usage-block builder markers (MACHINE_START_GCODE_END / MACHINE_END_GCODE_START /
|
||||||
|
// NOZZLE_CHANGE_START / NOZZLE_CHANGE_END / CP_TOOLCHANGE_WIPE), stored as static strings (the
|
||||||
|
// FLUSH/SKIPPABLE idiom above) rather than extending the Reserved_Tags arrays — these are
|
||||||
|
// multi-nozzle markers only ever emitted by BBL-printer paths. Public so the emission sites can
|
||||||
|
// reference them single-sourced. The MACHINE_*_GCODE_* emission (GCode.cpp, gated
|
||||||
|
// enable_pre_heating) activates the usage-block builder.
|
||||||
|
static const std::string Machine_Start_GCode_End_Tag;
|
||||||
|
static const std::string Machine_End_GCode_Start_Tag;
|
||||||
|
static const std::string Nozzle_Change_Start_Tag;
|
||||||
|
static const std::string Nozzle_Change_End_Tag;
|
||||||
|
static const std::string Toolchange_Wipe_Tag;
|
||||||
public:
|
public:
|
||||||
enum class ETags : unsigned char
|
enum class ETags : unsigned char
|
||||||
{
|
{
|
||||||
@@ -455,6 +589,9 @@ class Print;
|
|||||||
|
|
||||||
EMoveType move_type{ EMoveType::Noop };
|
EMoveType move_type{ EMoveType::Noop };
|
||||||
ExtrusionRole role{ erNone };
|
ExtrusionRole role{ erNone };
|
||||||
|
// SKIPPABLE tag classification stamped onto each time block. Feeds skippable_part_time
|
||||||
|
// and the injector's SKIPPABLE relocation. stNone unless inside a SKIPPABLE_* region.
|
||||||
|
SkipType skippable_type{ SkipType::stNone };
|
||||||
unsigned int move_id{ 0 };
|
unsigned int move_id{ 0 };
|
||||||
unsigned int g1_line_id{ 0 };
|
unsigned int g1_line_id{ 0 };
|
||||||
unsigned int remaining_internal_g1_lines{ 0 };
|
unsigned int remaining_internal_g1_lines{ 0 };
|
||||||
@@ -624,6 +761,25 @@ class Print;
|
|||||||
|
|
||||||
struct TimeProcessor
|
struct TimeProcessor
|
||||||
{
|
{
|
||||||
|
// Orca: the insert-line taxonomy + the ordered map of lines the pre-heat/pre-cool injector
|
||||||
|
// splices into the finished g-code, keyed by output-line id. Orca keeps its single-pass
|
||||||
|
// run_post_process (M73 / filament stats / ActualSpeedMove / Backtrace /
|
||||||
|
// machine_tool_change_time) intact and applies this map in a separate, gated ADDITIVE
|
||||||
|
// second file-rewrite pass (run_second_pass_injection); with an empty map that pass is a
|
||||||
|
// byte-for-byte identity rewrite. The map is populated by the PreCoolingInjector.
|
||||||
|
enum InsertLineType
|
||||||
|
{
|
||||||
|
PlaceholderReplace,
|
||||||
|
TimePredict,
|
||||||
|
FilamentChangePredict,
|
||||||
|
ExtruderChangePredict,
|
||||||
|
PreCooling,
|
||||||
|
PreHeating,
|
||||||
|
};
|
||||||
|
|
||||||
|
// first key is line id, second key is content
|
||||||
|
using InsertedLinesMap = std::map<unsigned int, std::vector<std::pair<std::string, InsertLineType>>>;
|
||||||
|
|
||||||
struct Planner
|
struct Planner
|
||||||
{
|
{
|
||||||
// Size of the firmware planner queue. The old 8-bit Marlins usually just managed 16 trapezoidal blocks.
|
// Size of the firmware planner queue. The old 8-bit Marlins usually just managed 16 trapezoidal blocks.
|
||||||
@@ -651,6 +807,117 @@ class Print;
|
|||||||
|
|
||||||
void reset();
|
void reset();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// The pre-cool / pre-heat injection engine. It consumes the already-computed per-move time
|
||||||
|
// substrate (moves[i].time[valid_machine_id] / .gcode_id) and the first-pass usage blocks to
|
||||||
|
// locate idle-hotend windows, then emits M632/M400/M104/M633 lines into a
|
||||||
|
// TimeProcessor::InsertedLinesMap that the additive second file-rewrite pass
|
||||||
|
// (run_second_pass_injection) splices into the finished g-code. It is constructed and run ONLY
|
||||||
|
// when m_enable_pre_heating — single-nozzle printers (X1/P1/A1/H2S, flag false) never reach it.
|
||||||
|
// Every input is a const reference bundled from GCodeProcessor members; the injector never
|
||||||
|
// mutates GCodeProcessor state.
|
||||||
|
class PreCoolingInjector {
|
||||||
|
public:
|
||||||
|
struct ExtruderFreeBlock {
|
||||||
|
unsigned int free_lower_gcode_id;
|
||||||
|
unsigned int free_upper_gcode_id;
|
||||||
|
unsigned int partial_free_lower_id; // range of extrusion in wipe tower; without a wipe tower
|
||||||
|
unsigned int partial_free_upper_id; // partial_free lower/upper equal free_lower_gcode_id
|
||||||
|
int last_filament_id;
|
||||||
|
int next_filament_id;
|
||||||
|
int last_nozzle_id;
|
||||||
|
int next_nozzle_id;
|
||||||
|
int extruder_id; // partition key for the pre-heat/pre-cool region (extruder or hotend), not
|
||||||
|
// necessarily a real extruder id
|
||||||
|
bool ignore_cooling_before_tower = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
void process_pre_cooling_and_heating(TimeProcessor::InsertedLinesMap& inserted_operation_lines);
|
||||||
|
void build_extruder_free_blocks(const std::vector<ExtruderPreHeating::FilamentUsageBlock>& filament_usage_blocks, const std::vector<ExtruderPreHeating::ExtruderUsageBlcok>& extruder_usage_blocks);
|
||||||
|
|
||||||
|
PreCoolingInjector(
|
||||||
|
const std::vector<GCodeProcessorResult::MoveVertex>& moves_,
|
||||||
|
const std::vector<std::string>& filament_types_,
|
||||||
|
const MultiNozzleUtils::LayeredNozzleGroupResult& nozzle_group_result_,
|
||||||
|
const std::vector<int>& filament_nozzle_temps_,
|
||||||
|
const std::vector<int>& filament_nozzle_temps_initial_layer_,
|
||||||
|
const std::vector<int>& physical_extruder_map_,
|
||||||
|
int valid_machine_id_,
|
||||||
|
float inject_time_threshold_,
|
||||||
|
bool handle_hotend_as_extruder_,
|
||||||
|
bool has_filament_switcher_,
|
||||||
|
const std::vector<int>& pre_cooling_temp_,
|
||||||
|
const std::vector<double>& cooling_rate_,
|
||||||
|
const std::vector<double>& heating_rate_,
|
||||||
|
const std::vector<std::pair<unsigned int, unsigned int>>& skippable_blocks_,
|
||||||
|
const std::vector<int>& extruder_max_nozzle_count_,
|
||||||
|
const std::vector<double>& filament_preheat_temperature_delta_,
|
||||||
|
const std::vector<double>& filament_max_temperature_drop_when_ec_,
|
||||||
|
unsigned int machine_start_gcode_end_id_,
|
||||||
|
unsigned int machine_end_gcode_start_id_,
|
||||||
|
const std::vector<ExtruderType>& extruder_types_,
|
||||||
|
const std::vector<double>& nozzle_diameter_
|
||||||
|
) :
|
||||||
|
moves(moves_),
|
||||||
|
filament_types(filament_types_),
|
||||||
|
nozzle_group_result(nozzle_group_result_),
|
||||||
|
filament_nozzle_temps(filament_nozzle_temps_),
|
||||||
|
filament_nozzle_temps_initial_layer(filament_nozzle_temps_initial_layer_),
|
||||||
|
physical_extruder_map(physical_extruder_map_),
|
||||||
|
valid_machine_id(valid_machine_id_),
|
||||||
|
inject_time_threshold(inject_time_threshold_),
|
||||||
|
handle_hotend_as_extruder(handle_hotend_as_extruder_),
|
||||||
|
has_filament_switcher(has_filament_switcher_),
|
||||||
|
filament_pre_cooling_temps(pre_cooling_temp_),
|
||||||
|
cooling_rate(cooling_rate_),
|
||||||
|
heating_rate(heating_rate_),
|
||||||
|
skippable_blocks(skippable_blocks_),
|
||||||
|
extruder_max_nozzle_count(extruder_max_nozzle_count_),
|
||||||
|
filament_preheat_temperature_delta(filament_preheat_temperature_delta_),
|
||||||
|
filament_max_temperature_drop_when_ec(filament_max_temperature_drop_when_ec_),
|
||||||
|
machine_start_gcode_end_id(machine_start_gcode_end_id_),
|
||||||
|
machine_end_gcode_start_id(machine_end_gcode_start_id_),
|
||||||
|
extruder_types(extruder_types_),
|
||||||
|
nozzle_diameter(nozzle_diameter_)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::vector<ExtruderFreeBlock> m_extruder_free_blocks;
|
||||||
|
const std::vector<GCodeProcessorResult::MoveVertex>& moves;
|
||||||
|
const std::vector<std::string>& filament_types;
|
||||||
|
const MultiNozzleUtils::LayeredNozzleGroupResult& nozzle_group_result;
|
||||||
|
const std::vector<int>& filament_nozzle_temps;
|
||||||
|
const std::vector<int>& filament_nozzle_temps_initial_layer;
|
||||||
|
const std::vector<int>& physical_extruder_map;
|
||||||
|
const int valid_machine_id;
|
||||||
|
const float inject_time_threshold;
|
||||||
|
const bool handle_hotend_as_extruder;
|
||||||
|
const bool has_filament_switcher;
|
||||||
|
const std::vector<double>& cooling_rate;
|
||||||
|
const std::vector<double>& heating_rate;
|
||||||
|
const std::vector<int>& filament_pre_cooling_temps; // target cooling temp during post extrusion
|
||||||
|
const std::vector<std::pair<unsigned int, unsigned int>>& skippable_blocks;
|
||||||
|
const std::vector<int>& extruder_max_nozzle_count;
|
||||||
|
const std::vector<double>& filament_preheat_temperature_delta;
|
||||||
|
const std::vector<double>& filament_max_temperature_drop_when_ec;
|
||||||
|
const unsigned int machine_start_gcode_end_id;
|
||||||
|
const unsigned int machine_end_gcode_start_id;
|
||||||
|
const std::vector<ExtruderType>& extruder_types;
|
||||||
|
const std::vector<double>& nozzle_diameter;
|
||||||
|
|
||||||
|
void inject_cooling_heating_command(
|
||||||
|
TimeProcessor::InsertedLinesMap& inserted_operation_lines,
|
||||||
|
const ExtruderFreeBlock& free_block,
|
||||||
|
float curr_temp,
|
||||||
|
float target_temp,
|
||||||
|
bool pre_cooling,
|
||||||
|
bool pre_heating
|
||||||
|
);
|
||||||
|
|
||||||
|
void build_by_filament_blocks(const std::vector<ExtruderPreHeating::FilamentUsageBlock>& filament_usage_blocks);
|
||||||
|
void build_by_extruder_blocks(const std::vector<ExtruderPreHeating::ExtruderUsageBlcok>& extruder_usage_blocks);
|
||||||
|
};
|
||||||
public:
|
public:
|
||||||
class SeamsDetector
|
class SeamsDetector
|
||||||
{
|
{
|
||||||
@@ -795,12 +1062,51 @@ class Print;
|
|||||||
bool m_flushing; // mark a section with real flush
|
bool m_flushing; // mark a section with real flush
|
||||||
bool m_virtual_flushing; // mark a section with virtual flush, only for statistics
|
bool m_virtual_flushing; // mark a section with virtual flush, only for statistics
|
||||||
bool m_wipe_tower;
|
bool m_wipe_tower;
|
||||||
|
// Current-section SKIPPABLE state. Set by process_tags when inside a SKIPPABLE_* region;
|
||||||
|
// stamped onto each TimeBlock. The shipping time_lapse_gcode template emits SKIPPABLE_*
|
||||||
|
// widely, so these commonly go active (true / stTimelapse) and stamp blocks on most slices.
|
||||||
|
bool m_skippable{false};
|
||||||
|
SkipType m_skippable_type{SkipType::stNone};
|
||||||
int m_object_label_id{-1};
|
int m_object_label_id{-1};
|
||||||
float m_print_z{0.0f};
|
float m_print_z{0.0f};
|
||||||
std::vector<float> m_remaining_volume;
|
std::vector<float> m_remaining_volume;
|
||||||
ExtruderTemps m_filament_nozzle_temp;
|
ExtruderTemps m_filament_nozzle_temp;
|
||||||
ExtruderTemps m_filament_nozzle_temp_first_layer;
|
ExtruderTemps m_filament_nozzle_temp_first_layer;
|
||||||
std::vector<int> m_physical_extruder_map;
|
std::vector<int> m_physical_extruder_map;
|
||||||
|
// Multi-nozzle context state. Per-extruder max (sub-)nozzle count; >1 marks a multi-nozzle
|
||||||
|
// extruder. Input for the pre-heat/filament-change-time injection model; not yet consumed by
|
||||||
|
// Orca's time estimator, so it is inert for existing printers.
|
||||||
|
std::vector<int> m_extruder_max_nozzle_count{1};
|
||||||
|
// Pre-heat / pre-cool injector estimator inputs. Populated from the config in apply_config
|
||||||
|
// (both overloads) and cleared in reset(), so the PreCoolingInjector has its inputs in place.
|
||||||
|
// Consumed only by the injector two-pass side-pass, gated on m_enable_pre_heating.
|
||||||
|
std::vector<std::string> m_filament_types;
|
||||||
|
std::vector<double> m_nozzle_diameter;
|
||||||
|
std::vector<double> m_hotend_cooling_rate{ 2.f };
|
||||||
|
std::vector<double> m_hotend_heating_rate{ 2.f };
|
||||||
|
std::vector<int> m_filament_pre_cooling_temp{ 0 };
|
||||||
|
std::vector<double> m_filament_preheat_temperature_delta;
|
||||||
|
bool m_enable_pre_heating{ false };
|
||||||
|
bool m_handle_hotend_as_extruder{ false };
|
||||||
|
bool m_has_filament_switcher{ false };
|
||||||
|
// [start,end] output-line-id ranges of each SKIPPABLE region, collected during
|
||||||
|
// run_post_process. The injector relocates pre-heat M104s out of these ranges. The shipping
|
||||||
|
// time_lapse_gcode template emits SKIPPABLE_* widely, so on a timelapse-on slice this is
|
||||||
|
// populated with many timelapse ranges (not empty) — the consumer must expect the common
|
||||||
|
// timelapse case, not only H2C/A2L wipe-tower ranges.
|
||||||
|
std::vector<std::pair<unsigned int, unsigned int>> m_skippable_blocks;
|
||||||
|
// First-pass usage blocks, built in run_post_process and stored on the member so the
|
||||||
|
// injector side-pass can consume them. Filled only when m_enable_pre_heating — single-nozzle
|
||||||
|
// printers (X1/P1/A1/H2S) never build them. They depend on the MACHINE_*_GCODE_* /
|
||||||
|
// NOZZLE_CHANGE_* emission the builder keys off.
|
||||||
|
std::vector<ExtruderPreHeating::FilamentUsageBlock> m_filament_blocks;
|
||||||
|
std::vector<ExtruderPreHeating::ExtruderUsageBlcok> m_extruder_blocks;
|
||||||
|
unsigned int m_machine_start_gcode_end_line_id{ (unsigned int) (-1) };
|
||||||
|
unsigned int m_machine_end_gcode_start_line_id{ (unsigned int) (-1) };
|
||||||
|
// Tracks, during the stream, which filament sits in each physical nozzle and which nozzle each
|
||||||
|
// extruder currently carries. Consumed ONLY by the richer two-arg process_filament_change
|
||||||
|
// model, which single-nozzle printers (X1/P1/A1/H2S/A2L) never enter.
|
||||||
|
MultiNozzleUtils::NozzleStatusRecorder m_nozzle_status_recorder;
|
||||||
bool m_manual_filament_change;
|
bool m_manual_filament_change;
|
||||||
|
|
||||||
//BBS: x, y offset for gcode generated
|
//BBS: x, y offset for gcode generated
|
||||||
@@ -1094,22 +1400,43 @@ class Print;
|
|||||||
// Processes T line (Select Tool)
|
// Processes T line (Select Tool)
|
||||||
void process_T(const GCodeReader::GCodeLine& line);
|
void process_T(const GCodeReader::GCodeLine& line);
|
||||||
void process_T(const std::string_view command);
|
void process_T(const std::string_view command);
|
||||||
|
// T variant carrying the H<nozzle> logical-nozzle id parsed off the command line. -1 = absent.
|
||||||
|
void process_T(const std::string_view command, int nozzle_id);
|
||||||
void process_M1020(const GCodeReader::GCodeLine &line);
|
void process_M1020(const GCodeReader::GCodeLine &line);
|
||||||
|
|
||||||
void process_M622(const GCodeReader::GCodeLine &line);
|
void process_M622(const GCodeReader::GCodeLine &line);
|
||||||
void process_M623(const GCodeReader::GCodeLine &line);
|
void process_M623(const GCodeReader::GCodeLine &line);
|
||||||
|
|
||||||
void process_filament_change(int id);
|
void process_filament_change(int id);
|
||||||
|
// Richer hotend-change time model distinguishing extruder-switch / nozzle-in-extruder change /
|
||||||
|
// filament-in-nozzle change. Self-gated: for single-nozzle printers it delegates to
|
||||||
|
// process_filament_change(int) so their time estimate — hence exported g-code — is unchanged.
|
||||||
|
void process_filament_change(int id, int nozzle_id);
|
||||||
|
// True only for multi-nozzle-capable printers (H2C cluster, or a dual/multi-extruder machine
|
||||||
|
// like H2D/X2D): the gate that admits the richer two-arg hotend-change time model. False for
|
||||||
|
// every single-extruder single-nozzle printer (X1/P1/A1/H2S/A2L).
|
||||||
|
bool use_multi_nozzle_change_time_model() const;
|
||||||
|
|
||||||
// post process the file with the given filename to:
|
// post process the file with the given filename to:
|
||||||
// 1) add remaining time lines M73 and update moves' gcode ids accordingly
|
// 1) add remaining time lines M73 and update moves' gcode ids accordingly
|
||||||
// 2) update used filament data
|
// 2) update used filament data
|
||||||
void run_post_process();
|
void run_post_process();
|
||||||
|
|
||||||
|
// Additive second file-rewrite pass. Splices the pre-heat/pre-cool injector's InsertedLinesMap
|
||||||
|
// into the finished g-code and re-shifts every move's gcode_id by the number of inserted lines
|
||||||
|
// before it. Runs only when m_enable_pre_heating, AFTER run_post_process, so single-nozzle
|
||||||
|
// printers (X1/P1/A1/H2S) never enter it; with an empty map it is a byte-for-byte identity rewrite.
|
||||||
|
void run_second_pass_injection();
|
||||||
|
// Shift each move's gcode_id by the count of injector lines inserted before it. No-op when the
|
||||||
|
// map is empty.
|
||||||
|
void handle_offsets_of_second_process(const TimeProcessor::InsertedLinesMap& inserted_operation_lines);
|
||||||
|
|
||||||
//BBS: different path_type is only used for arc move
|
//BBS: different path_type is only used for arc move
|
||||||
void store_move_vertex(EMoveType type, EMovePathType path_type = EMovePathType::Noop_move, bool internal_only = false);
|
void store_move_vertex(EMoveType type, EMovePathType path_type = EMovePathType::Noop_move, bool internal_only = false);
|
||||||
|
|
||||||
void set_extrusion_role(ExtrusionRole role);
|
void set_extrusion_role(ExtrusionRole role);
|
||||||
|
// Resolve the SKIPPABLE_TYPE payload to a SkipType.
|
||||||
|
void set_skippable_type(const std::string_view type);
|
||||||
|
|
||||||
float minimum_feedrate(PrintEstimatedStatistics::ETimeMode mode, float feedrate) const;
|
float minimum_feedrate(PrintEstimatedStatistics::ETimeMode mode, float feedrate) const;
|
||||||
float minimum_travel_feedrate(PrintEstimatedStatistics::ETimeMode mode, float feedrate) const;
|
float minimum_travel_feedrate(PrintEstimatedStatistics::ETimeMode mode, float feedrate) const;
|
||||||
|
|||||||
@@ -361,7 +361,8 @@ namespace Slic3r {
|
|||||||
* @param safe_areas A collection of extended polygons defining the safe areas.
|
* @param safe_areas A collection of extended polygons defining the safe areas.
|
||||||
* @return Point The nearest point within the safe areas or the default timelapse position if no safe areas exist.
|
* @return Point The nearest point within the safe areas or the default timelapse position if no safe areas exist.
|
||||||
*/
|
*/
|
||||||
Point pick_pos_internal(const Point& curr_pos, const ExPolygons& safe_areas, const ExPolygons& path_collision_area, bool detect_path_collision)
|
Point pick_pos_internal(const Point& curr_pos, const ExPolygons& safe_areas, const ExPolygons& path_collision_area, bool detect_path_collision,
|
||||||
|
const std::optional<Point>& farthest_point = std::nullopt)
|
||||||
{
|
{
|
||||||
struct CandidatePoint
|
struct CandidatePoint
|
||||||
{
|
{
|
||||||
@@ -381,7 +382,11 @@ namespace Slic3r {
|
|||||||
std::priority_queue<CandidatePoint> max_heap;
|
std::priority_queue<CandidatePoint> max_heap;
|
||||||
|
|
||||||
const double candidate_point_segment = scale_(5), weight_of_camera=1./3.;
|
const double candidate_point_segment = scale_(5), weight_of_camera=1./3.;
|
||||||
auto penaltyFunc = [&weight_of_camera](const Point &curr_post, const Point &CameraPos, const Point &candidatet) -> double {
|
auto penaltyFunc = [&weight_of_camera, &farthest_point](const Point &curr_post, const Point &CameraPos, const Point &candidatet) -> double {
|
||||||
|
if (farthest_point.has_value()) {
|
||||||
|
// Farthest-point timelapse: prefer candidate closest to the farthest point (L1 norm)
|
||||||
|
return (farthest_point.value() - candidatet).cwiseAbs().sum();
|
||||||
|
}
|
||||||
// move distance + Camera occlusion penalty function
|
// move distance + Camera occlusion penalty function
|
||||||
double ret_pen = (curr_post - candidatet).cwiseAbs().sum() - weight_of_camera * (CameraPos - candidatet).cwiseAbs().sum();
|
double ret_pen = (curr_post - candidatet).cwiseAbs().sum() - weight_of_camera * (CameraPos - candidatet).cwiseAbs().sum();
|
||||||
return ret_pen;
|
return ret_pen;
|
||||||
@@ -523,7 +528,7 @@ namespace Slic3r {
|
|||||||
path_collision_area = union_ex(layer_slices_without_curr, rod_limit_areas);
|
path_collision_area = union_ex(layer_slices_without_curr, rod_limit_areas);
|
||||||
}
|
}
|
||||||
|
|
||||||
return pick_pos_internal(center_p, safe_area,path_collision_area, by_object);
|
return pick_pos_internal(center_p, safe_area,path_collision_area, by_object, ctx.farthest_point);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -610,4 +615,50 @@ namespace Slic3r {
|
|||||||
return *m_all_layer_pos;
|
return *m_all_layer_pos;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Whether the head can travel to X0 without crossing any other instance that is
|
||||||
|
// taller than the current print position.
|
||||||
|
bool TimelapsePosPicker::get_is_clear_to_x0(const PosPickCtx &ctx)
|
||||||
|
{
|
||||||
|
bool by_object = m_print_seq == PrintSequence::ByObject;
|
||||||
|
std::vector<const PrintObject *> object_list = get_object_list(ctx.printed_objects);
|
||||||
|
|
||||||
|
auto range_intersect = [](int left1, int right1, int left2, int right2) {
|
||||||
|
if (left1 <= left2 && left2 <= right1) return true;
|
||||||
|
if (left2 <= left1 && left1 <= right2) return true;
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
ExPolygons unclear_area;
|
||||||
|
const Layer *layer = ctx.curr_layer;
|
||||||
|
float z_target = layer->print_z;
|
||||||
|
float z_low = layer->print_z - 0.5;
|
||||||
|
float z_high = layer->print_z + 0.5;
|
||||||
|
|
||||||
|
for (auto &obj : object_list) {
|
||||||
|
for (auto &instance : obj->instances()) {
|
||||||
|
auto instance_bbox = get_real_instance_bbox(instance);
|
||||||
|
bool is_curr_obj = ( obj == object_list.back() ) || ( !by_object ),
|
||||||
|
higher_than_curr_pos = instance_bbox.max.z() > z_target;
|
||||||
|
if (!is_curr_obj && range_intersect(instance_bbox.min.z(), instance_bbox.max.z(), z_low, z_high)) {
|
||||||
|
ExPolygon expoly;
|
||||||
|
expoly.contour = {{scale_(instance_bbox.min.x()), scale_(instance_bbox.min.y())},
|
||||||
|
{scale_(instance_bbox.max.x()), scale_(instance_bbox.min.y())},
|
||||||
|
{scale_(instance_bbox.max.x()), scale_(instance_bbox.max.y())},
|
||||||
|
{scale_(instance_bbox.min.x()), scale_(instance_bbox.max.y())}};
|
||||||
|
expoly.contour = expand_object_projection(expoly.contour, by_object, higher_than_curr_pos);
|
||||||
|
unclear_area.emplace_back(std::move(expoly));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Point curr_pos_in_plate = {ctx.curr_pos.x() - scale_(m_plate_offset.x()), ctx.curr_pos.y() - scale_(m_plate_offset.y())};
|
||||||
|
for (const ExPolygon &expoly : unclear_area) {
|
||||||
|
BoundingBox bbox = expoly.contour.bounding_box();
|
||||||
|
if (curr_pos_in_plate.y() < bbox.min.y() || curr_pos_in_plate.y() > bbox.max.y()) continue;
|
||||||
|
if (bbox.min.x() <= curr_pos_in_plate.x()) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -22,6 +22,9 @@ namespace Slic3r {
|
|||||||
int picture_extruder_id; // the extruder id to take picture
|
int picture_extruder_id; // the extruder id to take picture
|
||||||
int curr_extruder_id;
|
int curr_extruder_id;
|
||||||
std::optional<std::vector<const PrintObject*>> printed_objects; // printed objects, only have value in by object mode
|
std::optional<std::vector<const PrintObject*>> printed_objects; // printed objects, only have value in by object mode
|
||||||
|
// Farthest-point timelapse: plate-relative scaled point; when set, pick_pos_internal
|
||||||
|
// biases the picked snapshot position toward this point (nullopt → legacy camera-occlusion loss).
|
||||||
|
std::optional<Point> farthest_point;
|
||||||
};
|
};
|
||||||
|
|
||||||
// data are stored without plate offset
|
// data are stored without plate offset
|
||||||
@@ -32,6 +35,9 @@ namespace Slic3r {
|
|||||||
~TimelapsePosPicker() = default;
|
~TimelapsePosPicker() = default;
|
||||||
|
|
||||||
Point pick_pos(const PosPickCtx& ctx);
|
Point pick_pos(const PosPickCtx& ctx);
|
||||||
|
// Is the path to X0 clear of other (taller) instances? Drives the
|
||||||
|
// `clear_to_x0` timelapse-gcode variable (g39 clamping detection).
|
||||||
|
bool get_is_clear_to_x0(const PosPickCtx& ctx);
|
||||||
void init(const Print* print, const Point& plate_offset);
|
void init(const Print* print, const Point& plate_offset);
|
||||||
void reset();
|
void reset();
|
||||||
private:
|
private:
|
||||||
|
|||||||
@@ -7,13 +7,100 @@
|
|||||||
|
|
||||||
namespace Slic3r
|
namespace Slic3r
|
||||||
{
|
{
|
||||||
struct MinCostMaxFlow {
|
// ==================== MaxFlowWithLowerBounds ====================
|
||||||
|
struct MaxFlowWithLowerBounds {
|
||||||
public:
|
public:
|
||||||
struct Edge {
|
|
||||||
int from, to, capacity, cost, flow;
|
void add_edge(int from, int to, int capacity);
|
||||||
Edge(int u, int v, int cap, int cst) : from(u), to(v), capacity(cap), cost(cst), flow(0) {}
|
|
||||||
|
bool bfs();
|
||||||
|
int dfs(int u, int f);
|
||||||
|
int solve(std::vector<int>& matching);
|
||||||
|
|
||||||
|
public:
|
||||||
|
std::vector<int> l_nodes;
|
||||||
|
std::vector<int> r_nodes;
|
||||||
|
std::vector<Edge> edges;
|
||||||
|
std::vector<std::vector<int>> adj;
|
||||||
|
std::vector<int> level;
|
||||||
|
std::vector<int> it;
|
||||||
|
|
||||||
|
int total_nodes{ -1 };
|
||||||
|
int source_id{ -1 };
|
||||||
|
int sink_id{ -1 };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
void MaxFlowWithLowerBounds::add_edge(int from, int to, int capacity)
|
||||||
|
{
|
||||||
|
adj[from].emplace_back(edges.size());
|
||||||
|
edges.emplace_back(from, to, capacity, 0);
|
||||||
|
// also add the reverse residual edge with zero capacity
|
||||||
|
adj[to].emplace_back(edges.size());
|
||||||
|
edges.emplace_back(to, from, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MaxFlowWithLowerBounds::bfs() {
|
||||||
|
level.assign(total_nodes, -1);
|
||||||
|
std::queue<int> q;
|
||||||
|
q.push(source_id);
|
||||||
|
level[source_id] = 0;
|
||||||
|
|
||||||
|
while (!q.empty()) {
|
||||||
|
int u = q.front(); q.pop();
|
||||||
|
for (int eid : adj[u]) {
|
||||||
|
Edge &e = edges[eid];
|
||||||
|
if (e.flow < e.capacity && level[e.to] == -1) {
|
||||||
|
level[e.to] = level[u] + 1;
|
||||||
|
q.push(e.to);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return level[sink_id] != -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int MaxFlowWithLowerBounds::dfs(int u, int f) {
|
||||||
|
if (u == sink_id) return f;
|
||||||
|
for (int &i = it[u]; i < (int)adj[u].size(); ++i) {
|
||||||
|
int eid = adj[u][i];
|
||||||
|
Edge &e = edges[eid];
|
||||||
|
if (e.flow < e.capacity && level[e.to] == level[u] + 1) {
|
||||||
|
int pushed = dfs(e.to, std::min(f, e.capacity - e.flow));
|
||||||
|
if (pushed > 0) {
|
||||||
|
e.flow += pushed;
|
||||||
|
edges[eid ^ 1].flow -= pushed;
|
||||||
|
return pushed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int MaxFlowWithLowerBounds::solve(std::vector<int>& matching) {
|
||||||
|
int flow = 0;
|
||||||
|
while (bfs()) {
|
||||||
|
it.assign(total_nodes, 0);
|
||||||
|
while (int pushed = dfs(source_id, MaxFlowGraph::INF))
|
||||||
|
flow += pushed;
|
||||||
|
}
|
||||||
|
|
||||||
|
int L = l_nodes.size();
|
||||||
|
int R = r_nodes.size();
|
||||||
|
// collect l-r matches
|
||||||
|
matching.resize(l_nodes.size(), MaxFlowGraph::INVALID_ID);
|
||||||
|
for (int u = 0; u < L; ++u) {
|
||||||
|
for (int eid : adj[u]) {
|
||||||
|
Edge &e = edges[eid];
|
||||||
|
if (e.flow > 0 && e.to >= L && e.to < L + R) {
|
||||||
|
matching[e.from] = e.to - L;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return flow;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== MinCostMaxFlow ====================
|
||||||
|
struct MinCostMaxFlow {
|
||||||
|
public:
|
||||||
std::vector<int> solve();
|
std::vector<int> solve();
|
||||||
void add_edge(int from, int to, int capacity, int cost);
|
void add_edge(int from, int to, int capacity, int cost);
|
||||||
bool spfa(int source, int sink);
|
bool spfa(int source, int sink);
|
||||||
@@ -107,15 +194,10 @@ namespace Slic3r
|
|||||||
{
|
{
|
||||||
if (l_nodes[idx_in_left] == -1) {
|
if (l_nodes[idx_in_left] == -1) {
|
||||||
return 0;
|
return 0;
|
||||||
//TODO: test more here
|
|
||||||
int sum = 0;
|
|
||||||
for (int i = 0; i < matrix.size(); ++i)
|
|
||||||
sum += matrix[i][idx_in_right];
|
|
||||||
sum /= matrix.size();
|
|
||||||
return -sum;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return matrix[l_nodes[idx_in_left]][r_nodes[idx_in_right]];
|
float val = matrix[l_nodes[idx_in_left]][r_nodes[idx_in_right]];
|
||||||
|
return std::min(static_cast<int>(val), MaxFlowGraph::MCMF_MAX_EDGE_COST);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -123,27 +205,40 @@ namespace Slic3r
|
|||||||
const std::unordered_map<int, std::vector<int>>& uv_link_limits,
|
const std::unordered_map<int, std::vector<int>>& uv_link_limits,
|
||||||
const std::unordered_map<int, std::vector<int>>& uv_unlink_limits,
|
const std::unordered_map<int, std::vector<int>>& uv_unlink_limits,
|
||||||
const std::vector<int>& u_capacity,
|
const std::vector<int>& u_capacity,
|
||||||
const std::vector<int>& v_capacity)
|
const std::vector<int>& v_capacity,
|
||||||
|
const std::vector<std::pair<std::set<int>,int>>& v_group_capacity)
|
||||||
{
|
{
|
||||||
assert(u_capacity.empty() || u_capacity.size() == u_nodes.size());
|
assert(u_capacity.empty() || u_capacity.size() == u_nodes.size());
|
||||||
assert(v_capacity.empty() || v_capacity.size() == v_nodes.size());
|
assert(v_capacity.empty() || v_capacity.size() == v_nodes.size());
|
||||||
l_nodes = u_nodes;
|
l_nodes = u_nodes;
|
||||||
r_nodes = v_nodes;
|
r_nodes = v_nodes;
|
||||||
total_nodes = u_nodes.size() + v_nodes.size() + 2;
|
total_nodes = u_nodes.size() + v_nodes.size() + v_group_capacity.size() + 2;
|
||||||
source_id = total_nodes - 2;
|
source_id = total_nodes - 2;
|
||||||
sink_id = total_nodes - 1;
|
sink_id = total_nodes - 1;
|
||||||
|
|
||||||
adj.resize(total_nodes);
|
adj.resize(total_nodes);
|
||||||
|
|
||||||
|
std::vector<int>v_node_to(v_nodes.size(), sink_id);
|
||||||
|
for (size_t gid = 0; gid < v_group_capacity.size(); ++gid) {
|
||||||
|
for (auto vid : v_group_capacity[gid].first)
|
||||||
|
v_node_to[vid] = l_nodes.size() + r_nodes.size() + gid;
|
||||||
|
}
|
||||||
|
|
||||||
// add edge from source to left nodes
|
// add edge from source to left nodes
|
||||||
for (int idx = 0; idx < l_nodes.size(); ++idx) {
|
for (int idx = 0; idx < l_nodes.size(); ++idx) {
|
||||||
int capacity = u_capacity.empty() ? 1 : u_capacity[idx];
|
int capacity = u_capacity.empty() ? 1 : u_capacity[idx];
|
||||||
add_edge(source_id, idx, capacity);
|
add_edge(source_id, idx, capacity);
|
||||||
}
|
}
|
||||||
// add edge from right nodes to sink node
|
// add edge from right nodes to v_node_to(sink node or temp group node)
|
||||||
for (int idx = 0; idx < r_nodes.size(); ++idx) {
|
for (int idx = 0; idx < r_nodes.size(); ++idx) {
|
||||||
int capacity = v_capacity.empty() ? 1 : v_capacity[idx];
|
int capacity = v_capacity.empty() ? 1 : v_capacity[idx];
|
||||||
add_edge(l_nodes.size() + idx, sink_id, capacity);
|
add_edge(l_nodes.size() + idx, v_node_to[idx], capacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
// add edge from temp group node to sink node
|
||||||
|
for (int idx = 0; idx < v_group_capacity.size(); ++idx) {
|
||||||
|
int capacity = v_group_capacity[idx].second;
|
||||||
|
add_edge(l_nodes.size() + r_nodes.size() + idx, sink_id, capacity);
|
||||||
}
|
}
|
||||||
|
|
||||||
// add edge from left nodes to right nodes
|
// add edge from left nodes to right nodes
|
||||||
@@ -269,6 +364,301 @@ namespace Slic3r
|
|||||||
return m_solver->solve();
|
return m_solver->solve();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== GeneralMinCostLowerBoundsSolver ====================
|
||||||
|
GeneralMinCostLowerBoundsSolver::~GeneralMinCostLowerBoundsSolver() = default;
|
||||||
|
|
||||||
|
GeneralMinCostLowerBoundsSolver::GeneralMinCostLowerBoundsSolver(const std::vector<FlushMatrix> &matrix_,
|
||||||
|
const std::vector<int> &u_nodes,
|
||||||
|
const std::vector<int> &v_nodes,
|
||||||
|
const std::vector<int> &v_nodes_group,
|
||||||
|
const std::unordered_map<int, std::vector<int>> &uv_link_limits,
|
||||||
|
const std::unordered_map<int, std::vector<int>> &uv_unlink_limits)
|
||||||
|
{
|
||||||
|
flush_matrix = matrix_;
|
||||||
|
l_nodes = u_nodes;
|
||||||
|
r_nodes = v_nodes;
|
||||||
|
r_nodes_group = v_nodes_group;
|
||||||
|
m_uv_link_limits = uv_link_limits;
|
||||||
|
m_uv_unlink_limits = uv_unlink_limits;
|
||||||
|
num_groups = *std::max_element(r_nodes_group.begin(), r_nodes_group.end()) + 1;
|
||||||
|
|
||||||
|
m_solver_lower_bounds = std::make_unique<MaxFlowWithLowerBounds>();
|
||||||
|
m_solver_min_cost = std::make_unique<MinCostMaxFlow>();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<int> GeneralMinCostLowerBoundsSolver::solve()
|
||||||
|
{
|
||||||
|
// group nodes that do not need a lower-bound constraint
|
||||||
|
std::unordered_set<int> no_lower_group;
|
||||||
|
for (int i = 0; i < r_nodes.size(); i++) {
|
||||||
|
if (r_nodes[i] >= 0)
|
||||||
|
no_lower_group.insert(r_nodes_group[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. build the lower-bound network graph
|
||||||
|
build_feasible_graph(no_lower_group);
|
||||||
|
|
||||||
|
// 2. compute the max flow
|
||||||
|
int need = 0;
|
||||||
|
for (int d : demand)
|
||||||
|
if (d > 0) need += d;
|
||||||
|
std::vector<int> feasible_matching;
|
||||||
|
int pushed_flow = m_solver_lower_bounds->solve(feasible_matching);
|
||||||
|
assert(need == pushed_flow);
|
||||||
|
|
||||||
|
// 3. convert the lower-bound max-flow network into a min-cost-max-flow network
|
||||||
|
build_graph_with_feasible_result();
|
||||||
|
// 4. compute the min-cost max-flow
|
||||||
|
auto min_cost_matching = m_solver_min_cost->solve();
|
||||||
|
|
||||||
|
return min_cost_matching;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GeneralMinCostLowerBoundsSolver::build_feasible_graph(const std::unordered_set<int> &no_lower_groups)
|
||||||
|
{
|
||||||
|
m_solver_lower_bounds->l_nodes = l_nodes;
|
||||||
|
m_solver_lower_bounds->r_nodes = r_nodes;
|
||||||
|
m_solver_lower_bounds->total_nodes = l_nodes.size() + r_nodes.size() + num_groups + 2;
|
||||||
|
|
||||||
|
m_solver_lower_bounds->source_id = m_solver_lower_bounds->total_nodes - 2;
|
||||||
|
m_solver_lower_bounds->sink_id = m_solver_lower_bounds->total_nodes - 1;
|
||||||
|
m_solver_lower_bounds->adj.resize(m_solver_lower_bounds->total_nodes);
|
||||||
|
demand.resize(m_solver_lower_bounds->total_nodes, 0);
|
||||||
|
|
||||||
|
const int L = m_solver_lower_bounds->l_nodes.size();
|
||||||
|
const int R = m_solver_lower_bounds->r_nodes.size();
|
||||||
|
|
||||||
|
// source -> l
|
||||||
|
for (int i = 0; i < L; ++i)
|
||||||
|
m_solver_lower_bounds->add_edge(m_solver_lower_bounds->source_id, i, 1);
|
||||||
|
|
||||||
|
// u -> v (with link/unlink limits)
|
||||||
|
for (int i = 0; i < L; ++i) {
|
||||||
|
if (auto it = m_uv_link_limits.find(i); it != m_uv_link_limits.end()) {
|
||||||
|
for (int j : it->second)
|
||||||
|
m_solver_lower_bounds->add_edge(i, L + j, 1);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<std::vector<int>> unlink_limits;
|
||||||
|
if (auto it = m_uv_unlink_limits.find(i); it != m_uv_unlink_limits.end())
|
||||||
|
unlink_limits = it->second;
|
||||||
|
|
||||||
|
for (int j = 0; j < R; ++j) {
|
||||||
|
if (unlink_limits.has_value() && std::find(unlink_limits->begin(), unlink_limits->end(), j) != unlink_limits->end())
|
||||||
|
continue;
|
||||||
|
m_solver_lower_bounds->add_edge(i, L + j, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// r -> group
|
||||||
|
for (int j = 0; j < R; ++j) {
|
||||||
|
int g = r_nodes_group[j];
|
||||||
|
m_solver_lower_bounds->add_edge(L + j, L + R + g, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// group -> sink (lower bound = 1)
|
||||||
|
for (int g = 0; g < num_groups; ++g) {
|
||||||
|
if (no_lower_groups.count(g))
|
||||||
|
m_solver_lower_bounds->add_edge(L + R + g, m_solver_lower_bounds->sink_id, R);
|
||||||
|
else
|
||||||
|
add_edge_with_lower_bound(L + R + g, m_solver_lower_bounds->sink_id, 1, R, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
max_flow_edges = m_solver_lower_bounds->edges.size();
|
||||||
|
|
||||||
|
// support lower bounds, add super source super sink
|
||||||
|
super_source = m_solver_lower_bounds->total_nodes++;
|
||||||
|
super_sink = m_solver_lower_bounds->total_nodes++;
|
||||||
|
|
||||||
|
m_solver_lower_bounds->adj.resize(m_solver_lower_bounds->total_nodes);
|
||||||
|
demand.resize(m_solver_lower_bounds->total_nodes, 0);
|
||||||
|
|
||||||
|
for (int i = 0; i < super_source; ++i) {
|
||||||
|
if (demand[i] > 0) {
|
||||||
|
m_solver_lower_bounds->add_edge(super_source, i, demand[i]);
|
||||||
|
} else if (demand[i] < 0) {
|
||||||
|
m_solver_lower_bounds->add_edge(i, super_sink, -demand[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m_solver_lower_bounds->add_edge(m_solver_lower_bounds->sink_id, m_solver_lower_bounds->source_id, MaxFlowGraph::INF);
|
||||||
|
source_id = m_solver_lower_bounds->source_id;
|
||||||
|
sink_id = m_solver_lower_bounds->sink_id;
|
||||||
|
m_solver_lower_bounds->source_id = super_source;
|
||||||
|
m_solver_lower_bounds->sink_id = super_sink;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GeneralMinCostLowerBoundsSolver::build_graph_with_feasible_result()
|
||||||
|
{
|
||||||
|
for (auto&lb:lower_bound_edges){
|
||||||
|
m_solver_lower_bounds->edges[lb.edge_id].flow += lb.lower;
|
||||||
|
m_solver_lower_bounds->edges[lb.edge_id ^ 1].flow -= lb.lower;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_solver_min_cost->l_nodes = m_solver_lower_bounds->l_nodes;
|
||||||
|
m_solver_min_cost->r_nodes = m_solver_lower_bounds->r_nodes;
|
||||||
|
|
||||||
|
m_solver_min_cost->source_id = source_id;
|
||||||
|
m_solver_min_cost->sink_id = sink_id;
|
||||||
|
m_solver_min_cost->total_nodes = sink_id + 1;
|
||||||
|
|
||||||
|
m_solver_min_cost->edges = m_solver_lower_bounds->edges;
|
||||||
|
m_solver_min_cost->edges.erase(m_solver_min_cost->edges.begin() + max_flow_edges, m_solver_min_cost->edges.end());
|
||||||
|
|
||||||
|
m_solver_min_cost->adj = m_solver_lower_bounds->adj;
|
||||||
|
m_solver_min_cost->adj.resize(m_solver_min_cost->total_nodes);
|
||||||
|
for (auto &node_edges : m_solver_min_cost->adj) {
|
||||||
|
node_edges.erase(std::remove_if(node_edges.begin(), node_edges.end(), [this](int val) {return val >= this->max_flow_edges;}), node_edges.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
for (auto& e : m_solver_min_cost->edges) {
|
||||||
|
int L = m_solver_min_cost->l_nodes.size();
|
||||||
|
int R = m_solver_min_cost->r_nodes.size();
|
||||||
|
|
||||||
|
if (e.from < L && e.to >= L && e.to < L + R) {
|
||||||
|
int idx_in_left = e.from;
|
||||||
|
int idx_in_right = e.to - L;
|
||||||
|
int group_id = r_nodes_group[idx_in_right];
|
||||||
|
|
||||||
|
if (r_nodes[idx_in_right] == -1) continue;
|
||||||
|
e.cost = flush_matrix[group_id][l_nodes[idx_in_left]][r_nodes[idx_in_right]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void GeneralMinCostLowerBoundsSolver::add_edge_with_lower_bound(int from, int to, int lower, int upper, int cost)
|
||||||
|
{
|
||||||
|
int eid = m_solver_lower_bounds->edges.size();
|
||||||
|
m_solver_lower_bounds->add_edge(from, to, upper - lower);
|
||||||
|
|
||||||
|
lower_bound_edges.push_back({eid, lower});
|
||||||
|
demand[from] -= lower;
|
||||||
|
demand[to] += lower;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== GroupMinCostFlowSolver ====================
|
||||||
|
GroupMinCostFlowSolver::~GroupMinCostFlowSolver() = default;
|
||||||
|
|
||||||
|
GroupMinCostFlowSolver::GroupMinCostFlowSolver(const std::vector<FlushMatrix> &matrix_,
|
||||||
|
const std::vector<int> &u_nodes,
|
||||||
|
const std::vector<int> &v_nodes,
|
||||||
|
const std::vector<int> &v_nodes_group,
|
||||||
|
const std::unordered_map<int, std::vector<int>> &uv_link_limits,
|
||||||
|
const std::unordered_map<int, std::vector<int>> &uv_unlink_limits)
|
||||||
|
{
|
||||||
|
flush_matrix = matrix_;
|
||||||
|
l_nodes = u_nodes;
|
||||||
|
r_nodes = v_nodes;
|
||||||
|
r_nodes_group = v_nodes_group;
|
||||||
|
m_uv_link_limits = uv_link_limits;
|
||||||
|
m_uv_unlink_limits = uv_unlink_limits;
|
||||||
|
num_groups = *std::max_element(r_nodes_group.begin(), r_nodes_group.end()) + 1;
|
||||||
|
|
||||||
|
m_solver = std::make_unique<MinCostMaxFlow>();
|
||||||
|
build_graph();
|
||||||
|
}
|
||||||
|
|
||||||
|
int GroupMinCostFlowSolver::get_flush_cost(int l_idx, int r_idx)
|
||||||
|
{
|
||||||
|
if (r_nodes[r_idx] == -1)
|
||||||
|
return 0;
|
||||||
|
int group_id = r_nodes_group[r_idx];
|
||||||
|
return (int)flush_matrix[group_id][l_nodes[l_idx]][r_nodes[r_idx]];
|
||||||
|
}
|
||||||
|
|
||||||
|
void GroupMinCostFlowSolver::build_graph()
|
||||||
|
{
|
||||||
|
const int L = (int)l_nodes.size();
|
||||||
|
const int R = (int)r_nodes.size();
|
||||||
|
const int G = num_groups;
|
||||||
|
|
||||||
|
m_solver->l_nodes = l_nodes;
|
||||||
|
m_solver->r_nodes = r_nodes;
|
||||||
|
m_solver->total_nodes = L + R + G + 2;
|
||||||
|
m_solver->source_id = L + R + G;
|
||||||
|
m_solver->sink_id = L + R + G + 1;
|
||||||
|
m_solver->adj.resize(m_solver->total_nodes);
|
||||||
|
|
||||||
|
int max_flush = 0;
|
||||||
|
for (const auto &mat : flush_matrix)
|
||||||
|
for (const auto &row : mat)
|
||||||
|
for (float v : row)
|
||||||
|
max_flush = std::max(max_flush, (int)v);
|
||||||
|
int bonus = max_flush * L + 1;
|
||||||
|
|
||||||
|
// source -> l_i
|
||||||
|
for (int i = 0; i < L; ++i)
|
||||||
|
m_solver->add_edge(m_solver->source_id, i, 1, 0);
|
||||||
|
|
||||||
|
// l_i -> r_j (with link/unlink limits)
|
||||||
|
for (int i = 0; i < L; ++i) {
|
||||||
|
if (auto it = m_uv_link_limits.find(i); it != m_uv_link_limits.end()) {
|
||||||
|
for (int j : it->second)
|
||||||
|
m_solver->add_edge(i, L + j, 1, get_flush_cost(i, j));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<std::vector<int>> unlink_limits;
|
||||||
|
if (auto it = m_uv_unlink_limits.find(i); it != m_uv_unlink_limits.end())
|
||||||
|
unlink_limits = it->second;
|
||||||
|
|
||||||
|
for (int j = 0; j < R; ++j) {
|
||||||
|
if (unlink_limits.has_value() && std::find(unlink_limits->begin(), unlink_limits->end(), j) != unlink_limits->end())
|
||||||
|
continue;
|
||||||
|
m_solver->add_edge(i, L + j, 1, get_flush_cost(i, j));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// r_j -> group_g
|
||||||
|
// Compute per-nozzle incoming edge count as capacity upper bound.
|
||||||
|
// When unlink_limits restrict multiple filaments to the same nozzle,
|
||||||
|
// capacity=1 would block valid assignments. Using the actual in-degree
|
||||||
|
// allows the necessary flow while still preserving nozzle-level balance
|
||||||
|
// (a nozzle with fewer forced filaments keeps a tighter cap).
|
||||||
|
// The first unit carries a small nozzle-bonus to encourage spreading
|
||||||
|
// filaments across distinct nozzles within the same group.
|
||||||
|
int nozzle_bonus = max_flush + 1;
|
||||||
|
std::vector<int> r_in_degree(R, 0);
|
||||||
|
for (int i = 0; i < L; ++i) {
|
||||||
|
if (auto it = m_uv_link_limits.find(i); it != m_uv_link_limits.end()) {
|
||||||
|
for (int j : it->second)
|
||||||
|
r_in_degree[j]++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
std::optional<std::vector<int>> unlink_limits;
|
||||||
|
if (auto it = m_uv_unlink_limits.find(i); it != m_uv_unlink_limits.end())
|
||||||
|
unlink_limits = it->second;
|
||||||
|
for (int j = 0; j < R; ++j) {
|
||||||
|
if (unlink_limits.has_value() && std::find(unlink_limits->begin(), unlink_limits->end(), j) != unlink_limits->end())
|
||||||
|
continue;
|
||||||
|
r_in_degree[j]++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int j = 0; j < R; ++j) {
|
||||||
|
int g = r_nodes_group[j];
|
||||||
|
int cap = std::max(r_in_degree[j], 1);
|
||||||
|
// First unit gets -nozzle_bonus to prefer using distinct nozzles
|
||||||
|
m_solver->add_edge(L + j, L + R + g, 1, -nozzle_bonus);
|
||||||
|
if (cap > 1)
|
||||||
|
m_solver->add_edge(L + j, L + R + g, cap - 1, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// group_g -> sink (split: first unit gets -bonus, rest gets 0)
|
||||||
|
// bonus >> nozzle_bonus, so group coverage always takes priority
|
||||||
|
for (int g = 0; g < G; ++g) {
|
||||||
|
m_solver->add_edge(L + R + g, m_solver->sink_id, 1, -bonus);
|
||||||
|
if (L > 1)
|
||||||
|
m_solver->add_edge(L + R + g, m_solver->sink_id, L - 1, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<int> GroupMinCostFlowSolver::solve()
|
||||||
|
{
|
||||||
|
return m_solver->solve();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== MinFlushFlowSolver ====================
|
||||||
MinFlushFlowSolver::~MinFlushFlowSolver()
|
MinFlushFlowSolver::~MinFlushFlowSolver()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@@ -277,7 +667,8 @@ namespace Slic3r
|
|||||||
const std::unordered_map<int, std::vector<int>>& uv_link_limits,
|
const std::unordered_map<int, std::vector<int>>& uv_link_limits,
|
||||||
const std::unordered_map<int, std::vector<int>>& uv_unlink_limits,
|
const std::unordered_map<int, std::vector<int>>& uv_unlink_limits,
|
||||||
const std::vector<int>& u_capacity,
|
const std::vector<int>& u_capacity,
|
||||||
const std::vector<int>& v_capacity)
|
const std::vector<int>& v_capacity,
|
||||||
|
const std::vector<std::pair<std::set<int>,int>>&v_group_capacity)
|
||||||
{
|
{
|
||||||
assert(u_capacity.empty() || u_capacity.size() == u_nodes.size());
|
assert(u_capacity.empty() || u_capacity.size() == u_nodes.size());
|
||||||
assert(v_capacity.empty() || v_capacity.size() == v_nodes.size());
|
assert(v_capacity.empty() || v_capacity.size() == v_nodes.size());
|
||||||
@@ -286,13 +677,19 @@ namespace Slic3r
|
|||||||
m_solver->l_nodes = u_nodes;
|
m_solver->l_nodes = u_nodes;
|
||||||
m_solver->r_nodes = v_nodes;
|
m_solver->r_nodes = v_nodes;
|
||||||
|
|
||||||
m_solver->total_nodes = u_nodes.size() + v_nodes.size() + 2;
|
m_solver->total_nodes = u_nodes.size() + v_nodes.size() + v_group_capacity.size() + 2;
|
||||||
|
|
||||||
m_solver->source_id =m_solver->total_nodes - 2;
|
m_solver->source_id =m_solver->total_nodes - 2;
|
||||||
m_solver->sink_id = m_solver->total_nodes - 1;
|
m_solver->sink_id = m_solver->total_nodes - 1;
|
||||||
|
|
||||||
m_solver->adj.resize(m_solver->total_nodes);
|
m_solver->adj.resize(m_solver->total_nodes);
|
||||||
|
|
||||||
|
std::vector<int> v_node_to(v_nodes.size(), m_solver->sink_id);
|
||||||
|
for (size_t gid = 0; gid < v_group_capacity.size(); ++gid) {
|
||||||
|
for (auto vid : v_group_capacity[gid].first)
|
||||||
|
v_node_to[vid] = m_solver->l_nodes.size() + m_solver->r_nodes.size() + gid;
|
||||||
|
}
|
||||||
|
|
||||||
// add edge from source to left nodes,cost to 0
|
// add edge from source to left nodes,cost to 0
|
||||||
for (int i = 0; i < m_solver->l_nodes.size(); ++i) {
|
for (int i = 0; i < m_solver->l_nodes.size(); ++i) {
|
||||||
int capacity = u_capacity.empty() ? 1 : u_capacity[i];
|
int capacity = u_capacity.empty() ? 1 : u_capacity[i];
|
||||||
@@ -301,7 +698,12 @@ namespace Slic3r
|
|||||||
// add edge from right nodes to sink,cost to 0
|
// add edge from right nodes to sink,cost to 0
|
||||||
for (int i = 0; i < m_solver->r_nodes.size(); ++i) {
|
for (int i = 0; i < m_solver->r_nodes.size(); ++i) {
|
||||||
int capacity = v_capacity.empty() ? 1 : v_capacity[i];
|
int capacity = v_capacity.empty() ? 1 : v_capacity[i];
|
||||||
m_solver->add_edge(m_solver->l_nodes.size() + i, m_solver->sink_id, capacity, 0);
|
m_solver->add_edge(m_solver->l_nodes.size() + i, v_node_to[i], capacity, 0);
|
||||||
|
}
|
||||||
|
// add edge from temp group node to sink node
|
||||||
|
for(int i=0;i<v_group_capacity.size();++i){
|
||||||
|
int capacity = v_group_capacity[i].second;
|
||||||
|
m_solver->add_edge(m_solver->l_nodes.size() + m_solver->r_nodes.size() + i, m_solver->sink_id, capacity, 0);
|
||||||
}
|
}
|
||||||
// add edge from left node to right nodes
|
// add edge from left node to right nodes
|
||||||
for (int i = 0; i < m_solver->l_nodes.size(); ++i) {
|
for (int i = 0; i < m_solver->l_nodes.size(); ++i) {
|
||||||
@@ -602,12 +1004,125 @@ namespace Slic3r
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Single-nozzle flush-minimizing reorder over one filament set / one flush matrix, with an
|
||||||
|
// optional seed filament. Extracted from the group loop so the multi-nozzle reorder can call it
|
||||||
|
// per physical nozzle.
|
||||||
|
// TODO: add custom sequence
|
||||||
|
static int reorder_filaments_for_minimum_flush_volume_base(const std::vector<unsigned int>& filament_lists,
|
||||||
|
const std::vector<std::vector<unsigned int>>& layer_filaments,
|
||||||
|
const FlushMatrix& flush_matrix,
|
||||||
|
const std::function<bool(int, std::vector<int>&)> get_custom_seq,
|
||||||
|
std::vector<std::vector<unsigned int>>* filament_sequences,
|
||||||
|
std::optional<unsigned int> initial_filament_id = std::nullopt)
|
||||||
|
{
|
||||||
|
constexpr int max_n_with_forcast = 5;
|
||||||
|
using uint128_t = boost::multiprecision::uint128_t;
|
||||||
|
|
||||||
|
if (filament_sequences) {
|
||||||
|
filament_sequences->clear();
|
||||||
|
filament_sequences->reserve(layer_filaments.size());
|
||||||
|
}
|
||||||
|
auto filament_list_to_hash_key = [](const std::vector<unsigned int>& curr_layer_filaments, const std::vector<unsigned int>& next_layer_filaments,
|
||||||
|
const std::optional<unsigned int>& prev_filament, bool use_forcast) -> uint128_t {
|
||||||
|
uint128_t hash_key = 0;
|
||||||
|
// 31-0 bit define current layer extruder,63-32 bit define next layer extruder,95~64 define prev extruder
|
||||||
|
if (prev_filament) hash_key |= (uint128_t(1) << (64 + *prev_filament));
|
||||||
|
|
||||||
|
if (use_forcast) {
|
||||||
|
for (auto item : next_layer_filaments) { hash_key |= (uint128_t(1) << (32 + item)); }
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto item : curr_layer_filaments) { hash_key |= (uint128_t(1) << item); }
|
||||||
|
return hash_key;
|
||||||
|
};
|
||||||
|
|
||||||
|
int cost = 0;
|
||||||
|
std::map<size_t, std::vector<unsigned int>> custom_layer_sequence_map;
|
||||||
|
std::unordered_map<uint128_t, std::pair<float, std::vector<unsigned int>>> caches;
|
||||||
|
std::unordered_set<unsigned int> filament_sets(filament_lists.begin(), filament_lists.end());
|
||||||
|
std::optional<unsigned int> curr_filament_id;
|
||||||
|
// use the provided initial filament id as the starting state when it is valid
|
||||||
|
if (initial_filament_id.has_value() && *initial_filament_id < flush_matrix.size()) {
|
||||||
|
curr_filament_id = initial_filament_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (size_t layer = 0; layer < layer_filaments.size(); ++layer){
|
||||||
|
const auto& curr_lf = layer_filaments[layer];
|
||||||
|
std::vector<int> custom_filament_seq;
|
||||||
|
if (get_custom_seq && get_custom_seq(layer, custom_filament_seq) && !custom_filament_seq.empty()) {
|
||||||
|
std::vector<unsigned int> unsign_custom_extruder_seq;
|
||||||
|
for (int extruder : custom_filament_seq) {
|
||||||
|
unsigned int unsign_extruder = static_cast<unsigned int>(extruder) - 1;
|
||||||
|
auto it = std::find(layer_filaments[layer].begin(), layer_filaments[layer].end(), unsign_extruder);
|
||||||
|
if (it != layer_filaments[layer].end())
|
||||||
|
unsign_custom_extruder_seq.emplace_back(unsign_extruder);
|
||||||
|
}
|
||||||
|
assert(layer_filaments[layer].size() == unsign_custom_extruder_seq.size());
|
||||||
|
|
||||||
|
custom_layer_sequence_map[layer] = unsign_custom_extruder_seq;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (size_t layer = 0; layer < layer_filaments.size(); ++layer) {
|
||||||
|
const auto& curr_lf = layer_filaments[layer];
|
||||||
|
|
||||||
|
if(auto iter = custom_layer_sequence_map.find(layer); iter != custom_layer_sequence_map.end()){
|
||||||
|
auto sequence_in_group = collect_filaments_in_groups<unsigned int>(std::unordered_set<unsigned int>(filament_lists.begin(),filament_lists.end()), iter->second);
|
||||||
|
|
||||||
|
std::optional<unsigned int> prev = curr_filament_id;
|
||||||
|
for (auto& f: sequence_in_group){
|
||||||
|
if(prev)
|
||||||
|
cost += flush_matrix[*prev][f];
|
||||||
|
prev = f;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!sequence_in_group.empty()){
|
||||||
|
curr_filament_id = sequence_in_group.back();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(filament_sequences)
|
||||||
|
filament_sequences->emplace_back(sequence_in_group);
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<unsigned int> filament_used = collect_filaments_in_groups<unsigned int>(filament_sets, curr_lf);
|
||||||
|
std::vector<unsigned int> next_lf;
|
||||||
|
if (layer + 1 < layer_filaments.size()) next_lf = layer_filaments[layer + 1];
|
||||||
|
std::vector<unsigned int> filament_used_next_layer = collect_filaments_in_groups<unsigned int>(filament_sets, next_lf);
|
||||||
|
|
||||||
|
bool use_forcast = false;
|
||||||
|
float tmp_cost = 0;
|
||||||
|
std::vector<unsigned int> sequence;
|
||||||
|
uint128_t hash_key = filament_list_to_hash_key(filament_used, filament_used_next_layer, curr_filament_id, use_forcast);
|
||||||
|
if (auto iter = caches.find(hash_key); iter != caches.end()) {
|
||||||
|
tmp_cost = iter->second.first;
|
||||||
|
sequence = iter->second.second;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
sequence = get_extruders_order(flush_matrix, filament_used, filament_used_next_layer, curr_filament_id, use_forcast, &tmp_cost);
|
||||||
|
caches[hash_key] = { tmp_cost,sequence };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filament_sequences)
|
||||||
|
filament_sequences->emplace_back(sequence);
|
||||||
|
|
||||||
|
if (!sequence.empty())
|
||||||
|
curr_filament_id = sequence.back();
|
||||||
|
|
||||||
|
cost += tmp_cost;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cost;
|
||||||
|
}
|
||||||
|
|
||||||
int reorder_filaments_for_minimum_flush_volume(const std::vector<unsigned int>& filament_lists,
|
int reorder_filaments_for_minimum_flush_volume(const std::vector<unsigned int>& filament_lists,
|
||||||
const std::vector<int>& filament_maps,
|
const std::vector<int>& filament_maps,
|
||||||
const std::vector<std::vector<unsigned int>>& layer_filaments,
|
const std::vector<std::vector<unsigned int>>& layer_filaments,
|
||||||
const std::vector<FlushMatrix>& flush_matrix,
|
const std::vector<FlushMatrix>& flush_matrix,
|
||||||
std::optional<std::function<bool(int, std::vector<int>&)>> get_custom_seq,
|
std::optional<std::function<bool(int, std::vector<int>&)>> get_custom_seq,
|
||||||
std::vector<std::vector<unsigned int>>* filament_sequences)
|
std::vector<std::vector<unsigned int>>* filament_sequences,
|
||||||
|
const std::unordered_map<int, int>& nozzle_status)
|
||||||
{
|
{
|
||||||
//only when layer filament num <= 5,we do forcast
|
//only when layer filament num <= 5,we do forcast
|
||||||
constexpr int max_n_with_forcast = 5;
|
constexpr int max_n_with_forcast = 5;
|
||||||
@@ -670,6 +1185,12 @@ namespace Slic3r
|
|||||||
if (groups[idx].empty())
|
if (groups[idx].empty())
|
||||||
continue;
|
continue;
|
||||||
std::optional<unsigned int>current_extruder_id;
|
std::optional<unsigned int>current_extruder_id;
|
||||||
|
// seed the group (nozzle) with the filament already loaded, if nozzle_status supplies one
|
||||||
|
if (auto it = nozzle_status.find(static_cast<int>(idx)); it != nozzle_status.end() && it->second >= 0) {
|
||||||
|
unsigned int initial_fil = static_cast<unsigned int>(it->second);
|
||||||
|
if (initial_fil < flush_matrix[idx].size())
|
||||||
|
current_extruder_id = initial_fil;
|
||||||
|
}
|
||||||
|
|
||||||
std::unordered_map<uint128_t, std::pair<float, std::vector<unsigned int>>> caches;
|
std::unordered_map<uint128_t, std::pair<float, std::vector<unsigned int>>> caches;
|
||||||
|
|
||||||
@@ -775,4 +1296,174 @@ namespace Slic3r
|
|||||||
|
|
||||||
return cost;
|
return cost;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int reorder_filaments_for_multi_nozzle_extruder(const std::vector<unsigned int>& filament_lists,
|
||||||
|
const MultiNozzleUtils::LayeredNozzleGroupResult& nozzle_group_result,
|
||||||
|
const std::vector<std::vector<unsigned int>>& layer_filaments,
|
||||||
|
const std::vector<FlushMatrix>& flush_matrix,
|
||||||
|
const std::function<bool(int, std::vector<int>&)> get_custom_seq,
|
||||||
|
std::vector<std::vector<unsigned int>>* filament_sequences,
|
||||||
|
const MultiNozzleUtils::NozzleStatusRecorder& initial_status)
|
||||||
|
{
|
||||||
|
std::map<int,std::set<unsigned int>> nozzle_filament_groups;
|
||||||
|
std::map<int,std::set<int>> extruder_to_nozzle;
|
||||||
|
|
||||||
|
for(auto filament_idx : filament_lists){
|
||||||
|
auto nozzle_info = nozzle_group_result.get_nozzle_for_filament(filament_idx, -1);
|
||||||
|
if (!nozzle_info)
|
||||||
|
continue;
|
||||||
|
nozzle_filament_groups[nozzle_info->group_id].insert(filament_idx);
|
||||||
|
extruder_to_nozzle[nozzle_info->extruder_id].insert(nozzle_info->group_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::map<size_t, std::vector<unsigned int>>custom_layer_sequence_map;// save the filament sequences of custom layer
|
||||||
|
for (size_t layer = 0; layer < layer_filaments.size(); ++layer){
|
||||||
|
const auto& curr_lf = layer_filaments[layer];
|
||||||
|
std::vector<int> custom_filament_seq;
|
||||||
|
if (get_custom_seq && get_custom_seq(layer, custom_filament_seq) && !custom_filament_seq.empty()) {
|
||||||
|
std::vector<unsigned int> unsign_custom_extruder_seq;
|
||||||
|
for (int extruder : custom_filament_seq) {
|
||||||
|
unsigned int unsign_extruder = static_cast<unsigned int>(extruder) - 1;
|
||||||
|
auto it = std::find(layer_filaments[layer].begin(), layer_filaments[layer].end(), unsign_extruder);
|
||||||
|
if (it != layer_filaments[layer].end())
|
||||||
|
unsign_custom_extruder_seq.emplace_back(unsign_extruder);
|
||||||
|
}
|
||||||
|
assert(layer_filaments[layer].size() == unsign_custom_extruder_seq.size());
|
||||||
|
|
||||||
|
custom_layer_sequence_map[layer] = unsign_custom_extruder_seq;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
std::map<int, std::vector<std::vector<unsigned int>>> nozzle_filament_sequences;
|
||||||
|
bool store_sequence = filament_sequences != nullptr;
|
||||||
|
|
||||||
|
int cost = 0;
|
||||||
|
for(auto& group : nozzle_filament_groups){
|
||||||
|
int nozzle_id = group.first;
|
||||||
|
auto& filament_in_nozzle = group.second;
|
||||||
|
|
||||||
|
int extruder_id = 0;
|
||||||
|
for(auto& [ext, nozzle_set] : extruder_to_nozzle){
|
||||||
|
if(nozzle_set.count(nozzle_id)){
|
||||||
|
extruder_id = ext;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(filament_in_nozzle.empty())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
std::vector<unsigned int> filament_vec_in_nozzle(filament_in_nozzle.begin(), filament_in_nozzle.end());
|
||||||
|
|
||||||
|
int initial_fil = initial_status.get_filament_in_nozzle(nozzle_id);
|
||||||
|
std::optional<unsigned int> initial_fil_id = (initial_fil >= 0 && initial_fil < flush_matrix[extruder_id].size())? std::optional<unsigned int>(initial_fil) : std::nullopt;
|
||||||
|
|
||||||
|
std::vector<std::vector<unsigned int>> filament_seq;
|
||||||
|
cost += reorder_filaments_for_minimum_flush_volume_base(filament_vec_in_nozzle, layer_filaments, flush_matrix[extruder_id], get_custom_seq,
|
||||||
|
store_sequence ? &filament_seq : nullptr, initial_fil_id);
|
||||||
|
if(store_sequence)
|
||||||
|
nozzle_filament_sequences.emplace(nozzle_id, std::move(filament_seq));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!store_sequence)
|
||||||
|
return cost;
|
||||||
|
|
||||||
|
std::vector<int> extruders;
|
||||||
|
std::map<int, std::vector<int>> nozzles_per_extruder;
|
||||||
|
for (auto& [extruder_id, nozzle_set] : extruder_to_nozzle) {
|
||||||
|
extruders.push_back(extruder_id);
|
||||||
|
nozzles_per_extruder[extruder_id] = std::vector<int>(
|
||||||
|
nozzle_set.begin(), nozzle_set.end()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
filament_sequences->clear();
|
||||||
|
filament_sequences->resize(layer_filaments.size());
|
||||||
|
|
||||||
|
// No filament in filament_lists resolved to a nozzle in nozzle_group_result
|
||||||
|
// (e.g. a degenerate input where a layer references a filament index outside the range's
|
||||||
|
// grouping map). Emit each layer's filaments in their given order so the caller still gets a
|
||||||
|
// valid per-layer sequence, and skip the cross-nozzle reorder. Guards the unchecked
|
||||||
|
// max_element(extruders) below, which would dereference end() on an empty range.
|
||||||
|
if (extruders.empty()) {
|
||||||
|
for (size_t layer = 0; layer < layer_filaments.size(); ++layer)
|
||||||
|
(*filament_sequences)[layer] = layer_filaments[layer];
|
||||||
|
return cost;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto get_extruder_for_filament = [nozzle_group_result](unsigned int filament_idx) {
|
||||||
|
auto nozzle = nozzle_group_result.get_nozzle_for_filament(filament_idx, -1);
|
||||||
|
if (!nozzle)
|
||||||
|
return -1;
|
||||||
|
return nozzle->extruder_id;
|
||||||
|
};
|
||||||
|
|
||||||
|
auto get_nozzle_idx_for_filament = [nozzles_per_extruder, nozzle_group_result](unsigned int filament_idx)->int {
|
||||||
|
auto nozzle = nozzle_group_result.get_nozzle_for_filament(filament_idx, -1);
|
||||||
|
if (!nozzle)
|
||||||
|
return -1;
|
||||||
|
return std::find(nozzles_per_extruder.at(nozzle->extruder_id).begin(), nozzles_per_extruder.at(nozzle->extruder_id).end(), nozzle->group_id) - nozzles_per_extruder.at(nozzle->extruder_id).begin();
|
||||||
|
};
|
||||||
|
|
||||||
|
int initial_extruder = initial_status.get_current_extruder_id();
|
||||||
|
int last_extruder_idx = (initial_extruder >= 0 && initial_extruder < extruders.size())? initial_extruder : 0;
|
||||||
|
// set size to max extruder_id in case extruder_id is not continuous
|
||||||
|
std::vector<int> last_nozzle_idx(*std::max_element(extruders.begin(),extruders.end()) + 1,0);
|
||||||
|
for (int ext_id = 0; ext_id < static_cast<int>(last_nozzle_idx.size()); ext_id++) {
|
||||||
|
int initial_nozzle = initial_status.get_nozzle_in_extruder(ext_id);
|
||||||
|
auto ext_nozzles = nozzles_per_extruder[ext_id];
|
||||||
|
auto it = std::find(ext_nozzles.begin(), ext_nozzles.end(), initial_nozzle);
|
||||||
|
if (it != ext_nozzles.end())
|
||||||
|
last_nozzle_idx[ext_id] = static_cast<int>(std::distance(ext_nozzles.begin(), it));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (size_t layer = 0; layer < layer_filaments.size(); ++layer) {
|
||||||
|
auto& out_seq = (*filament_sequences)[layer];
|
||||||
|
|
||||||
|
if (custom_layer_sequence_map.find(layer) != custom_layer_sequence_map.end()) {
|
||||||
|
out_seq = custom_layer_sequence_map[layer];
|
||||||
|
if (!out_seq.empty()) {
|
||||||
|
last_extruder_idx = get_extruder_for_filament(out_seq.back());
|
||||||
|
for (auto filament : out_seq) {
|
||||||
|
int cur_ext_id = get_extruder_for_filament(filament);
|
||||||
|
last_nozzle_idx[cur_ext_id] = get_nozzle_idx_for_filament(filament);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (last_extruder_idx == -1)
|
||||||
|
last_extruder_idx = 0;
|
||||||
|
|
||||||
|
int curr_last_extruder_idx = last_extruder_idx;
|
||||||
|
auto curr_last_nozzle_idx = last_nozzle_idx;
|
||||||
|
for (int i = 0; i < extruders.size(); ++i) {
|
||||||
|
int extruder_id = extruders[(last_extruder_idx + i) % extruders.size()];
|
||||||
|
auto& base_nozzles = nozzles_per_extruder[extruder_id];
|
||||||
|
|
||||||
|
bool has_seq = false;
|
||||||
|
if (last_nozzle_idx[extruder_id] == -1)
|
||||||
|
last_nozzle_idx[extruder_id] = 0;
|
||||||
|
|
||||||
|
for (int j = 0; j < base_nozzles.size(); ++j) {
|
||||||
|
int nozzle_idx = (last_nozzle_idx[extruder_id] + j) % base_nozzles.size();
|
||||||
|
int nozzle_id = base_nozzles[nozzle_idx];
|
||||||
|
const auto& frag = nozzle_filament_sequences[nozzle_id][layer];
|
||||||
|
if (frag.empty())
|
||||||
|
continue;
|
||||||
|
has_seq = true;
|
||||||
|
curr_last_nozzle_idx[extruder_id] = nozzle_idx;
|
||||||
|
out_seq.insert(out_seq.end(), frag.begin(), frag.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (has_seq)
|
||||||
|
curr_last_extruder_idx = extruder_id;
|
||||||
|
}
|
||||||
|
last_extruder_idx = curr_last_extruder_idx;
|
||||||
|
last_nozzle_idx = curr_last_nozzle_idx;
|
||||||
|
}
|
||||||
|
return cost;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,10 @@
|
|||||||
#include <functional>
|
#include <functional>
|
||||||
#include <limits>
|
#include <limits>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <set>
|
||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include "../MultiNozzleUtils.hpp"
|
||||||
|
|
||||||
namespace Slic3r {
|
namespace Slic3r {
|
||||||
|
|
||||||
@@ -15,21 +18,27 @@ using FlushMatrix = std::vector<std::vector<float>>;
|
|||||||
namespace MaxFlowGraph {
|
namespace MaxFlowGraph {
|
||||||
const int INF = std::numeric_limits<int>::max();
|
const int INF = std::numeric_limits<int>::max();
|
||||||
const int INVALID_ID = -1;
|
const int INVALID_ID = -1;
|
||||||
|
// Upper bound for MCMF edge cost to prevent int overflow in SPFA causing infinite loops
|
||||||
|
constexpr int MCMF_MAX_EDGE_COST = 10000000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Namespace-scope edge shared by the max-flow / min-cost-max-flow solvers below.
|
||||||
|
// The default cost keeps the plain max-flow solvers (which never read cost) source-compatible.
|
||||||
|
struct Edge
|
||||||
|
{
|
||||||
|
int from, to, capacity, cost, flow;
|
||||||
|
Edge(int u, int v, int cap, int cst = 0) : from(u), to(v), capacity(cap), cost(cst), flow(0) {}
|
||||||
|
};
|
||||||
|
|
||||||
class MaxFlowSolver
|
class MaxFlowSolver
|
||||||
{
|
{
|
||||||
private:
|
|
||||||
struct Edge {
|
|
||||||
int from, to, capacity, flow;
|
|
||||||
Edge(int u, int v, int cap) :from(u), to(v), capacity(cap), flow(0) {}
|
|
||||||
};
|
|
||||||
public:
|
public:
|
||||||
MaxFlowSolver(const std::vector<int>& u_nodes, const std::vector<int>& v_nodes,
|
MaxFlowSolver(const std::vector<int>& u_nodes, const std::vector<int>& v_nodes,
|
||||||
const std::unordered_map<int, std::vector<int>>& uv_link_limits = {},
|
const std::unordered_map<int, std::vector<int>>& uv_link_limits = {},
|
||||||
const std::unordered_map<int, std::vector<int>>& uv_unlink_limits = {},
|
const std::unordered_map<int, std::vector<int>>& uv_unlink_limits = {},
|
||||||
const std::vector<int>& u_capacity = {},
|
const std::vector<int>& u_capacity = {},
|
||||||
const std::vector<int>& v_capacity = {}
|
const std::vector<int>& v_capacity = {},
|
||||||
|
const std::vector<std::pair<std::set<int>, int>>& v_group_capacity = {}
|
||||||
);
|
);
|
||||||
std::vector<int> solve();
|
std::vector<int> solve();
|
||||||
|
|
||||||
@@ -47,6 +56,7 @@ private:
|
|||||||
|
|
||||||
|
|
||||||
struct MinCostMaxFlow;
|
struct MinCostMaxFlow;
|
||||||
|
struct MaxFlowWithLowerBounds;
|
||||||
|
|
||||||
class GeneralMinCostSolver
|
class GeneralMinCostSolver
|
||||||
{
|
{
|
||||||
@@ -61,6 +71,84 @@ private:
|
|||||||
std::unique_ptr<MinCostMaxFlow> m_solver;
|
std::unique_ptr<MinCostMaxFlow> m_solver;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class GeneralMinCostLowerBoundsSolver
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
GeneralMinCostLowerBoundsSolver(
|
||||||
|
const std::vector<FlushMatrix> &matrix_,
|
||||||
|
const std::vector<int>& u_nodes,
|
||||||
|
const std::vector<int>& v_nodes,
|
||||||
|
const std::vector<int>& v_nodes_group,
|
||||||
|
const std::unordered_map<int, std::vector<int>>& uv_link_limits = {},
|
||||||
|
const std::unordered_map<int, std::vector<int>>& uv_unlink_limits = {});
|
||||||
|
|
||||||
|
std::vector<int> solve();
|
||||||
|
~GeneralMinCostLowerBoundsSolver();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void build_feasible_graph(const std::unordered_set<int>& no_lower_groups);
|
||||||
|
|
||||||
|
void build_graph_with_feasible_result();
|
||||||
|
|
||||||
|
void add_edge_with_lower_bound(int from, int to, int lower, int upper, int cost);
|
||||||
|
|
||||||
|
int get_distance(const int idx_in_left,const int idx_in_right);
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::unique_ptr<MaxFlowWithLowerBounds> m_solver_lower_bounds;
|
||||||
|
std::unique_ptr<MinCostMaxFlow> m_solver_min_cost;
|
||||||
|
|
||||||
|
std::vector<FlushMatrix> flush_matrix;
|
||||||
|
std::vector<int> l_nodes;
|
||||||
|
std::vector<int> r_nodes;
|
||||||
|
std::vector<int> r_nodes_group;
|
||||||
|
std::unordered_map<int, std::vector<int>> m_uv_link_limits;
|
||||||
|
std::unordered_map<int, std::vector<int>> m_uv_unlink_limits;
|
||||||
|
int num_groups = 0;
|
||||||
|
|
||||||
|
// support lower bounds
|
||||||
|
struct LowerBoundEdge{
|
||||||
|
int edge_id;
|
||||||
|
int lower;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<int> demand;
|
||||||
|
std::vector<LowerBoundEdge> lower_bound_edges;
|
||||||
|
|
||||||
|
int super_source = -1;
|
||||||
|
int super_sink = -1;
|
||||||
|
int source_id = -1;
|
||||||
|
int sink_id = -1;
|
||||||
|
int max_flow_edges = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class GroupMinCostFlowSolver
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
GroupMinCostFlowSolver(
|
||||||
|
const std::vector<FlushMatrix> &matrix_,
|
||||||
|
const std::vector<int> &u_nodes,
|
||||||
|
const std::vector<int> &v_nodes,
|
||||||
|
const std::vector<int> &v_nodes_group,
|
||||||
|
const std::unordered_map<int, std::vector<int>> &uv_link_limits = {},
|
||||||
|
const std::unordered_map<int, std::vector<int>> &uv_unlink_limits = {});
|
||||||
|
|
||||||
|
std::vector<int> solve();
|
||||||
|
~GroupMinCostFlowSolver();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void build_graph();
|
||||||
|
int get_flush_cost(int l_idx, int r_idx);
|
||||||
|
|
||||||
|
std::unique_ptr<MinCostMaxFlow> m_solver;
|
||||||
|
std::vector<FlushMatrix> flush_matrix;
|
||||||
|
std::vector<int> l_nodes;
|
||||||
|
std::vector<int> r_nodes;
|
||||||
|
std::vector<int> r_nodes_group;
|
||||||
|
std::unordered_map<int, std::vector<int>> m_uv_link_limits;
|
||||||
|
std::unordered_map<int, std::vector<int>> m_uv_unlink_limits;
|
||||||
|
int num_groups = 0;
|
||||||
|
};
|
||||||
|
|
||||||
class MinFlushFlowSolver
|
class MinFlushFlowSolver
|
||||||
{
|
{
|
||||||
@@ -71,7 +159,8 @@ public:
|
|||||||
const std::unordered_map<int, std::vector<int>>& uv_link_limits = {},
|
const std::unordered_map<int, std::vector<int>>& uv_link_limits = {},
|
||||||
const std::unordered_map<int, std::vector<int>>& uv_unlink_limits = {},
|
const std::unordered_map<int, std::vector<int>>& uv_unlink_limits = {},
|
||||||
const std::vector<int>& u_capacity = {},
|
const std::vector<int>& u_capacity = {},
|
||||||
const std::vector<int>& v_capacity = {}
|
const std::vector<int>& v_capacity = {},
|
||||||
|
const std::vector<std::pair<std::set<int>, int>>& v_group_capacity = {}
|
||||||
);
|
);
|
||||||
std::vector<int> solve();
|
std::vector<int> solve();
|
||||||
~MinFlushFlowSolver();
|
~MinFlushFlowSolver();
|
||||||
@@ -108,7 +197,19 @@ int reorder_filaments_for_minimum_flush_volume(const std::vector<unsigned int> &
|
|||||||
const std::vector<std::vector<unsigned int>> &layer_filaments,
|
const std::vector<std::vector<unsigned int>> &layer_filaments,
|
||||||
const std::vector<FlushMatrix> &flush_matrix,
|
const std::vector<FlushMatrix> &flush_matrix,
|
||||||
std::optional<std::function<bool(int, std::vector<int> &)>> get_custom_seq,
|
std::optional<std::function<bool(int, std::vector<int> &)>> get_custom_seq,
|
||||||
std::vector<std::vector<unsigned int>> *filament_sequences);
|
std::vector<std::vector<unsigned int>> *filament_sequences,
|
||||||
|
const std::unordered_map<int, int>& nozzle_status = {});
|
||||||
|
|
||||||
|
// Order filaments within a per-nozzle grouping result (multi-nozzle extruders). Threads a
|
||||||
|
// NozzleStatusRecorder describing the initial physical nozzle occupancy so the reorder can reward
|
||||||
|
// keeping an already-loaded filament in place.
|
||||||
|
int reorder_filaments_for_multi_nozzle_extruder(const std::vector<unsigned int>& filament_lists,
|
||||||
|
const MultiNozzleUtils::LayeredNozzleGroupResult& nozzle_group_result,
|
||||||
|
const std::vector<std::vector<unsigned int>>& layer_filaments,
|
||||||
|
const std::vector<FlushMatrix>& flush_matrix,
|
||||||
|
const std::function<bool(int,std::vector<int>&)> get_custom_seq,
|
||||||
|
std::vector<std::vector<unsigned int>> * filament_sequences,
|
||||||
|
const MultiNozzleUtils::NozzleStatusRecorder& initial_status = {});
|
||||||
|
|
||||||
}
|
}
|
||||||
#endif // !TOOL_ORDER_UTILS_HPP
|
#endif // !TOOL_ORDER_UTILS_HPP
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
#include <boost/container/small_vector.hpp>
|
#include <boost/container/small_vector.hpp>
|
||||||
#include "../FilamentGroup.hpp"
|
#include "../FilamentGroup.hpp"
|
||||||
|
#include "../MultiNozzleUtils.hpp"
|
||||||
#include "../ExtrusionEntity.hpp"
|
#include "../ExtrusionEntity.hpp"
|
||||||
#include "../PrintConfig.hpp"
|
#include "../PrintConfig.hpp"
|
||||||
|
|
||||||
@@ -98,19 +99,23 @@ private:
|
|||||||
struct FilamentChangeStats
|
struct FilamentChangeStats
|
||||||
{
|
{
|
||||||
int filament_flush_weight{0};
|
int filament_flush_weight{0};
|
||||||
|
// flush_filament_change_count counts filament changes that actually flush a physical nozzle.
|
||||||
|
// It replaces the former (dead, never populated) extruder_change_count. For single-nozzle-per-
|
||||||
|
// extruder printers it equals the per-extruder filament_change_count, so GUI stat displays are
|
||||||
|
// unchanged.
|
||||||
|
int flush_filament_change_count{0};
|
||||||
int filament_change_count{0};
|
int filament_change_count{0};
|
||||||
int extruder_change_count{0};
|
|
||||||
|
|
||||||
void clear(){
|
void clear(){
|
||||||
filament_flush_weight = 0;
|
filament_flush_weight = 0;
|
||||||
filament_change_count = 0;
|
filament_change_count = 0;
|
||||||
extruder_change_count = 0;
|
flush_filament_change_count = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
FilamentChangeStats& operator+=(const FilamentChangeStats& other) {
|
FilamentChangeStats& operator+=(const FilamentChangeStats& other) {
|
||||||
this->filament_flush_weight += other.filament_flush_weight;
|
this->filament_flush_weight += other.filament_flush_weight;
|
||||||
this->filament_change_count += other.filament_change_count;
|
this->filament_change_count += other.filament_change_count;
|
||||||
this->extruder_change_count += other.extruder_change_count;
|
this->flush_filament_change_count += other.flush_filament_change_count;
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,7 +123,7 @@ struct FilamentChangeStats
|
|||||||
FilamentChangeStats ret;
|
FilamentChangeStats ret;
|
||||||
ret.filament_flush_weight = this->filament_flush_weight + other.filament_flush_weight;
|
ret.filament_flush_weight = this->filament_flush_weight + other.filament_flush_weight;
|
||||||
ret.filament_change_count = this->filament_change_count + other.filament_change_count;
|
ret.filament_change_count = this->filament_change_count + other.filament_change_count;
|
||||||
ret.extruder_change_count = this->extruder_change_count + other.extruder_change_count;
|
ret.flush_filament_change_count = this->flush_filament_change_count + other.flush_filament_change_count;
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,12 +241,21 @@ public:
|
|||||||
bool has_wipe_tower() const { return ! m_layer_tools.empty() && m_first_printing_extruder != (unsigned int)-1 && m_layer_tools.front().has_wipe_tower; }
|
bool has_wipe_tower() const { return ! m_layer_tools.empty() && m_first_printing_extruder != (unsigned int)-1 && m_layer_tools.front().has_wipe_tower; }
|
||||||
|
|
||||||
int get_most_used_extruder() const { return most_used_extruder; }
|
int get_most_used_extruder() const { return most_used_extruder; }
|
||||||
|
|
||||||
|
// Logical (extruder, nozzle) grouping of the used filaments, built during reorder.
|
||||||
|
// For single-nozzle printers this is one logical nozzle per extruder (nozzle id == extruder id).
|
||||||
|
// Consumed by GCode (get_nozzle_id / get_first_nozzle_for_filament).
|
||||||
|
const MultiNozzleUtils::LayeredNozzleGroupResult &get_layered_nozzle_group_result() const { return m_nozzle_group_result; }
|
||||||
/*
|
/*
|
||||||
* called in single extruder mode, the value in map are all 0
|
* called in single extruder mode, the value in map are all 0
|
||||||
* called in dual extruder mode, the value in map will be 0 or 1
|
* called in dual extruder mode, the value in map will be 0 or 1
|
||||||
* 0 based group id
|
* 0 based group id
|
||||||
*/
|
*/
|
||||||
static std::vector<int> get_recommended_filament_maps(const std::vector<std::vector<unsigned int>>& layer_filaments, const Print* print,const FilamentMapMode mode, const std::vector<std::set<int>>& physical_unprintables, const std::vector<std::set<int>>& geometric_unprintables);
|
// Nozzle-centric grouping. Returns a nozzle-aware LayeredNozzleGroupResult instead of a plain
|
||||||
|
// extruder-level std::vector<int>. Callers derive the 0/1-based extruder map via
|
||||||
|
// result.get_extruder_map(). unprintable_volumes / nozzle_status default empty for the static
|
||||||
|
// path; the per-layer engine supplies non-empty values.
|
||||||
|
static MultiNozzleUtils::LayeredNozzleGroupResult get_recommended_filament_maps(const std::vector<std::vector<unsigned int>>& layer_filaments, const Print* print,const FilamentMapMode mode, const std::vector<std::set<int>>& physical_unprintables, const std::vector<std::set<int>>& geometric_unprintables, const std::map<int, std::set<NozzleVolumeType>>& unprintable_volumes = {}, const std::unordered_map<int, int>& nozzle_status = {});
|
||||||
|
|
||||||
// should be called after doing reorder
|
// should be called after doing reorder
|
||||||
FilamentChangeStats get_filament_change_stats(FilamentChangeMode mode);
|
FilamentChangeStats get_filament_change_stats(FilamentChangeMode mode);
|
||||||
@@ -283,6 +297,13 @@ private:
|
|||||||
FilamentChangeStats m_stats_by_single_extruder;
|
FilamentChangeStats m_stats_by_single_extruder;
|
||||||
FilamentChangeStats m_stats_by_multi_extruder_curr;
|
FilamentChangeStats m_stats_by_multi_extruder_curr;
|
||||||
FilamentChangeStats m_stats_by_multi_extruder_best;
|
FilamentChangeStats m_stats_by_multi_extruder_best;
|
||||||
|
MultiNozzleUtils::LayeredNozzleGroupResult m_nozzle_group_result;
|
||||||
|
// Physical nozzle occupancy threaded through the per-layer selector regroup.
|
||||||
|
// m_initial_nozzle_status seeds the first combo range (empty for a fresh slice — there is no
|
||||||
|
// device continuation state); m_nozzle_status carries the running state out of the plan. Inert
|
||||||
|
// for every printer except an H2C profile that enables the filament selector (is_dynamic_group_reorder).
|
||||||
|
MultiNozzleUtils::NozzleStatusRecorder m_initial_nozzle_status;
|
||||||
|
MultiNozzleUtils::NozzleStatusRecorder m_nozzle_status;
|
||||||
|
|
||||||
int most_used_extruder;
|
int most_used_extruder;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#include <cassert>
|
#include <cassert>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
#include <array>
|
||||||
#include <numeric>
|
#include <numeric>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <iomanip>
|
#include <iomanip>
|
||||||
@@ -1493,6 +1494,21 @@ WipeTower::WipeTower(const PrintConfig& config, int plate_idx, Vec3d plate_origi
|
|||||||
m_enable_tower_interface_cooldown_during_tower(config.enable_tower_interface_cooldown_during_tower.value)
|
m_enable_tower_interface_cooldown_during_tower(config.enable_tower_interface_cooldown_during_tower.value)
|
||||||
{
|
{
|
||||||
m_flat_ironing = (m_flat_ironing && m_use_gap_wall);
|
m_flat_ironing = (m_flat_ironing && m_use_gap_wall);
|
||||||
|
|
||||||
|
// Prime-tower heating during wipe. m_is_multiple_nozzle mirrors the gate used in ToolOrdering/GCode
|
||||||
|
// (std::any_of extruder_max_nozzle_count > 1); it is false for every current printer, so the
|
||||||
|
// heating-during-wipe logic in toolchange_wipe_new is inert.
|
||||||
|
m_hotend_heating_rate = config.hotend_heating_rate.values;
|
||||||
|
m_physical_extruder_map = config.physical_extruder_map.values;
|
||||||
|
m_is_multiple_nozzle = std::any_of(config.extruder_max_nozzle_count.values.begin(),
|
||||||
|
config.extruder_max_nozzle_count.values.end(),
|
||||||
|
[](int v) { return v > 1; });
|
||||||
|
|
||||||
|
// Per-extruder printable-height clamp. Empty for single-extruder printers
|
||||||
|
// (extruder_printable_height = []), so is_valid_last_layer is inert there.
|
||||||
|
m_printable_height = config.extruder_printable_height.values;
|
||||||
|
m_last_layer_id.assign(config.nozzle_diameter.size(), -1);
|
||||||
|
|
||||||
// Read absolute value of first layer speed, if given as percentage,
|
// Read absolute value of first layer speed, if given as percentage,
|
||||||
// it is taken over following default. Speeds from config are not
|
// it is taken over following default. Speeds from config are not
|
||||||
// easily accessible here.
|
// easily accessible here.
|
||||||
@@ -1558,8 +1574,12 @@ void WipeTower::set_extruder(size_t idx, const PrintConfig& config)
|
|||||||
}
|
}
|
||||||
m_filpar[idx].tower_interface_pre_extrusion_dist = config.filament_tower_interface_pre_extrusion_dist.get_at(idx);
|
m_filpar[idx].tower_interface_pre_extrusion_dist = config.filament_tower_interface_pre_extrusion_dist.get_at(idx);
|
||||||
m_filpar[idx].tower_interface_pre_extrusion_length = config.filament_tower_interface_pre_extrusion_length.get_at(idx);
|
m_filpar[idx].tower_interface_pre_extrusion_length = config.filament_tower_interface_pre_extrusion_length.get_at(idx);
|
||||||
|
// PETG pre-extrusion offset reuses the tower-interface pre-extrusion distance. Only read by the
|
||||||
|
// has_filament_switcher-gated PETG branch in get_next_pos (inert fleet-wide).
|
||||||
|
m_filpar[idx].petg_pre_extrusion_offset_dist = config.filament_tower_interface_pre_extrusion_dist.get_at(idx);
|
||||||
m_filpar[idx].tower_ironing_area = config.filament_tower_ironing_area.get_at(idx);
|
m_filpar[idx].tower_ironing_area = config.filament_tower_ironing_area.get_at(idx);
|
||||||
m_filpar[idx].tower_interface_purge_length = config.filament_tower_interface_purge_volume.get_at(idx);
|
m_filpar[idx].tower_interface_purge_length = config.filament_tower_interface_purge_volume.get_at(idx);
|
||||||
|
m_filpar[idx].filament_cooling_before_tower = config.filament_cooling_before_tower.get_at(idx);
|
||||||
|
|
||||||
// If this is a single extruder MM printer, we will use all the SE-specific config values.
|
// If this is a single extruder MM printer, we will use all the SE-specific config values.
|
||||||
// Otherwise, the defaults will be used to turn off the SE stuff.
|
// Otherwise, the defaults will be used to turn off the SE stuff.
|
||||||
@@ -1651,6 +1671,27 @@ Vec2f WipeTower::get_next_pos(const WipeTower::box_coordinates &cleaning_box, fl
|
|||||||
break;
|
break;
|
||||||
default: break;
|
default: break;
|
||||||
}
|
}
|
||||||
|
// Shift the wipe start outward for a PETG pre-extrusion on filament-switcher devices, clamped to the
|
||||||
|
// shared printable bed. Gated on m_has_filament_switcher, which is false for the whole shipping fleet
|
||||||
|
// (no profile sets the key), so is_petg_pre_extrusion is always false and res is returned unchanged.
|
||||||
|
// The tower-interface contact branch is deliberately NOT applied here (enable_tower_interface_features
|
||||||
|
// DOES ship on H2C/X2D; applying it would change their g-code); is_contact_pre_extrusion is computed
|
||||||
|
// only as the guard that gives the contact path priority over PETG.
|
||||||
|
bool is_contact_pre_extrusion = interface_layer && m_enable_tower_interface_features;
|
||||||
|
bool is_petg_pre_extrusion = !is_contact_pre_extrusion && is_petg_filament(m_current_tool) && m_has_filament_switcher;
|
||||||
|
if (is_petg_pre_extrusion) {
|
||||||
|
Vec2f stop_pos = res;
|
||||||
|
float offset_dist = m_filpar[m_current_tool].petg_pre_extrusion_offset_dist;
|
||||||
|
auto printer_bbx = unscaled(get_extents(m_shared_print_bed)); // BoundingBoxBase<Vec2d>
|
||||||
|
printer_bbx.translate((-m_wipe_tower_pos - m_rib_offset).cast<double>());
|
||||||
|
if (stop_pos.x() < m_wipe_tower_width / 2.f)
|
||||||
|
stop_pos = Vec2f(stop_pos.x() - offset_dist, stop_pos.y());
|
||||||
|
else
|
||||||
|
stop_pos = Vec2f(stop_pos.x() + offset_dist, stop_pos.y());
|
||||||
|
if (stop_pos.x() < printer_bbx.min[0]) stop_pos.x() = printer_bbx.min[0];
|
||||||
|
if (stop_pos.x() > printer_bbx.max[0]) stop_pos.x() = printer_bbx.max[0];
|
||||||
|
res = stop_pos;
|
||||||
|
}
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2645,6 +2686,11 @@ bool WipeTower::is_tpu_filament(int filament_id) const
|
|||||||
return m_filpar[filament_id].material == "TPU";
|
return m_filpar[filament_id].material == "TPU";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool WipeTower::is_petg_filament(int filament_id) const
|
||||||
|
{
|
||||||
|
return m_filpar[filament_id].material == "PETG";
|
||||||
|
}
|
||||||
|
|
||||||
// BBS: consider both soluable and support properties
|
// BBS: consider both soluable and support properties
|
||||||
// Return index of first toolchange that switches to non-soluble and non-support extruder
|
// Return index of first toolchange that switches to non-soluble and non-support extruder
|
||||||
// ot -1 if there is no such toolchange.
|
// ot -1 if there is no such toolchange.
|
||||||
@@ -2717,6 +2763,9 @@ void WipeTower::get_wall_skip_points(const WipeTowerInfo &layer)
|
|||||||
float spacing = m_layer_info->extra_spacing;
|
float spacing = m_layer_info->extra_spacing;
|
||||||
if (has_tpu_filament() && m_layer_info->extra_spacing < m_tpu_fixed_spacing) spacing = 1;
|
if (has_tpu_filament() && m_layer_info->extra_spacing < m_tpu_fixed_spacing) spacing = 1;
|
||||||
float nozzle_change_depth = tool_change.nozzle_change_depth * spacing;
|
float nozzle_change_depth = tool_change.nozzle_change_depth * spacing;
|
||||||
|
// Drop the nozzle-change depth on an extruder's final layer above its printable height
|
||||||
|
// (inert unless is_valid_last_layer clamps, i.e. multi-extruder near Z-max).
|
||||||
|
if (!is_valid_last_layer(old_filament, m_cur_layer_id, layer.z)) nozzle_change_depth = 0.f;
|
||||||
//float nozzle_change_depth = tool_change.nozzle_change_depth * (has_tpu_filament() ? m_tpu_fixed_spacing : layer.extra_spacing);
|
//float nozzle_change_depth = tool_change.nozzle_change_depth * (has_tpu_filament() ? m_tpu_fixed_spacing : layer.extra_spacing);
|
||||||
auto* block = get_block_by_category(m_filpar[new_filament].category, false);
|
auto* block = get_block_by_category(m_filpar[new_filament].category, false);
|
||||||
if (!block)
|
if (!block)
|
||||||
@@ -2760,7 +2809,10 @@ void WipeTower::get_wall_skip_points(const WipeTowerInfo &layer)
|
|||||||
WipeTower::ToolChangeResult WipeTower::tool_change_new(size_t new_tool, bool solid_toolchange,bool solid_nozzlechange)
|
WipeTower::ToolChangeResult WipeTower::tool_change_new(size_t new_tool, bool solid_toolchange,bool solid_nozzlechange)
|
||||||
{
|
{
|
||||||
m_nozzle_change_result.gcode.clear();
|
m_nozzle_change_result.gcode.clear();
|
||||||
if (!m_filament_map.empty() && new_tool < m_filament_map.size() && m_filament_map[m_current_tool] != m_filament_map[new_tool]) {
|
// Skip the cross-extruder nozzle change (ramming) on an extruder's final layer above its printable
|
||||||
|
// height. is_valid_last_layer is inert unless multi-extruder near Z-max.
|
||||||
|
if (!m_filament_map.empty() && new_tool < m_filament_map.size() && m_filament_map[m_current_tool] != m_filament_map[new_tool]
|
||||||
|
&& is_valid_last_layer(m_current_tool, m_cur_layer_id, m_z_pos)) {
|
||||||
m_nozzle_change_result = nozzle_change_new(m_current_tool, new_tool, solid_nozzlechange);
|
m_nozzle_change_result = nozzle_change_new(m_current_tool, new_tool, solid_nozzlechange);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3411,23 +3463,103 @@ void WipeTower::toolchange_wipe_new(WipeTowerWriter &writer, const box_coordinat
|
|||||||
x_to_wipe = solid_tool_toolchange ? std::numeric_limits<float>::max(): x_to_wipe;
|
x_to_wipe = solid_tool_toolchange ? std::numeric_limits<float>::max(): x_to_wipe;
|
||||||
float target_speed = is_first_layer() ? std::min(m_first_layer_speed * 60.f, 4800.f) : 4800.f;
|
float target_speed = is_first_layer() ? std::min(m_first_layer_speed * 60.f, 4800.f) : 4800.f;
|
||||||
target_speed = solid_tool_toolchange ? 20.f * 60.f : target_speed;
|
target_speed = solid_tool_toolchange ? 20.f * 60.f : target_speed;
|
||||||
float wipe_speed = 0.33f * target_speed;
|
// Nominal wipe-speed schedule. The applied wipe_speed is nominal_speed * speed_factor; speed_factor
|
||||||
|
// stays 1.0 unless the H2C prime-tower heating-during-wipe model below slows the wipe so the hotend
|
||||||
|
// can reach temperature (nominal_speed == wipe_speed when speed_factor == 1, i.e. single-nozzle).
|
||||||
|
float nominal_speed = 0.33f * target_speed;
|
||||||
|
|
||||||
m_left_to_right = ((m_cur_layer_id + 3) % 4 >= 2);
|
m_left_to_right = ((m_cur_layer_id + 3) % 4 >= 2);
|
||||||
|
|
||||||
bool is_from_up = (m_cur_layer_id % 2 == 1);
|
bool is_from_up = (m_cur_layer_id % 2 == 1);
|
||||||
|
|
||||||
|
// Prime-tower heating during wipe. Everything here is gated on m_is_multiple_nozzle (false for every
|
||||||
|
// current printer); the lambdas emit nothing until add_M104_by_requirement's gate opens, so the
|
||||||
|
// single-nozzle wipe is untouched.
|
||||||
|
// WipeSpeedMap mirrors the nominal schedule above and is read only by estimate_wipe_time. It is a
|
||||||
|
// std::array (stack, no per-call heap allocation); values depend on runtime target_speed so it
|
||||||
|
// cannot be static const.
|
||||||
|
const std::array<float, 5> WipeSpeedMap{0.33f * target_speed, 0.375f * target_speed, 0.458f * target_speed,
|
||||||
|
0.875f * target_speed, std::min(target_speed, 0.875f * target_speed + 50.f)};
|
||||||
|
auto estimate_wipe_time = [&cleaning_box, &x_to_wipe, &xr, &xl, &dy, &WipeSpeedMap, &solid_tool_toolchange]() -> float {
|
||||||
|
int n = std::ceil(x_to_wipe / (xr - xl));
|
||||||
|
if (solid_tool_toolchange) n = (cleaning_box.lu[1] - cleaning_box.ld[1]) / dy;
|
||||||
|
float one_line_len = xr - xl;
|
||||||
|
float time = std::numeric_limits<float>::max();
|
||||||
|
if (n <= 1)
|
||||||
|
time = one_line_len / WipeSpeedMap[0];
|
||||||
|
else if (n <= 2)
|
||||||
|
time = one_line_len / WipeSpeedMap[0] + one_line_len / WipeSpeedMap[1];
|
||||||
|
else if (n <= 3)
|
||||||
|
time = one_line_len / WipeSpeedMap[0] + one_line_len / WipeSpeedMap[1] + one_line_len / WipeSpeedMap[2];
|
||||||
|
else if (n <= 4)
|
||||||
|
time = one_line_len / WipeSpeedMap[0] + one_line_len / WipeSpeedMap[1] + one_line_len / WipeSpeedMap[2] + one_line_len / WipeSpeedMap[3];
|
||||||
|
else {
|
||||||
|
time = one_line_len / WipeSpeedMap[0] + one_line_len / WipeSpeedMap[1] + one_line_len / WipeSpeedMap[2] + one_line_len / WipeSpeedMap[3];
|
||||||
|
time += (n - 4) * one_line_len / WipeSpeedMap[4];
|
||||||
|
}
|
||||||
|
return time * 60.f;
|
||||||
|
};
|
||||||
|
// Emit the arriving-hotend pre-heat inside the M632/M633 nozzle-change barrier. `M632 S<tool>[ H<nozzle>]
|
||||||
|
// M N` opens the barrier (M = firmware nozzle-change flag, N = slicer generated), the M104 sets the
|
||||||
|
// arriving hotend temp, and `M633` closes it. H2C's grouping is static (no dynamic nozzle map), so the
|
||||||
|
// H<nozzle> field is omitted (a dynamic nozzle map would supply a real nozzle id, a static map -1 => no
|
||||||
|
// H). The counterproductive fan-on (M106 S255) used for departing-tool cooldown is intentionally
|
||||||
|
// omitted, since this is a pre-HEAT of the arriving tool. The whole helper is only ever called from
|
||||||
|
// add_M104_by_requirement, which is gated on m_is_multiple_nozzle (extruder_max_nozzle_count>1) => H2C
|
||||||
|
// only; every other printer's wipe tower is untouched. The M632 M-flag is itself a firmware barrier, so
|
||||||
|
// a preceding M400 wait is subsumed.
|
||||||
|
auto format_line_M104 = [this](int target_temp, int target_extruder = -1, bool wait_for_moves = true, const std::string &comment = "") {
|
||||||
|
std::string buffer;
|
||||||
|
buffer += "M632 S" + std::to_string(m_current_tool) + " M N\n";
|
||||||
|
buffer += "M104";
|
||||||
|
if (target_extruder != -1 && target_extruder < (int) m_physical_extruder_map.size())
|
||||||
|
buffer += (" T" + std::to_string(m_physical_extruder_map[target_extruder]));
|
||||||
|
buffer += " S" + std::to_string(target_temp) + " N0"; // N0 means the gcode is generated by the slicer
|
||||||
|
if (!comment.empty()) buffer += " ;" + comment;
|
||||||
|
buffer += '\n';
|
||||||
|
buffer += "M633\n";
|
||||||
|
(void) wait_for_moves; // the M632 M-flag barrier replaces the former M400 wait
|
||||||
|
return buffer;
|
||||||
|
};
|
||||||
|
// Suppress the pre-heat M104 on the first layer and on solid (contact) toolchanges (should_heating).
|
||||||
|
// m_is_multiple_nozzle folds in the H2C gate so single-nozzle output is untouched.
|
||||||
|
// Orca: the arriving extruder id is resolved as m_filament_map[tool]-1 (layer-static) because Orca's
|
||||||
|
// wipe tower is extruder-level rather than tracking a per-layer nozzle map.
|
||||||
|
bool should_heating = m_is_multiple_nozzle && m_filpar[m_current_tool].filament_cooling_before_tower > EPSILON &&
|
||||||
|
!solid_tool_toolchange && !is_first_layer();
|
||||||
|
auto add_M104_by_requirement = [&writer, &format_line_M104, &should_heating, this]() {
|
||||||
|
if (m_filpar[m_current_tool].filament_cooling_before_tower < EPSILON) return;
|
||||||
|
if (!should_heating) return;
|
||||||
|
float target_temp = is_first_layer() ? m_filpar[m_current_tool].nozzle_temperature_initial_layer : m_filpar[m_current_tool].nozzle_temperature;
|
||||||
|
writer.append(format_line_M104(target_temp, m_filament_map[m_current_tool] - 1));
|
||||||
|
};
|
||||||
|
float speed_factor = 1.f;
|
||||||
|
if (should_heating) {
|
||||||
|
// The heating-slowdown scaling is disabled — no additional heating time is required, so
|
||||||
|
// speed_factor stays 1.0. The structure and estimate_wipe_time/WipeSpeedMap are retained for
|
||||||
|
// future H2C tuning; the divide-by-zero/bounds guard is preserved in the commented body below.
|
||||||
|
// int extruder_id = m_filament_map[m_current_tool] - 1;
|
||||||
|
// if (extruder_id >= 0 && extruder_id < (int) m_hotend_heating_rate.size() && m_hotend_heating_rate[extruder_id] > 0.) {
|
||||||
|
// float estimate_time = estimate_wipe_time();
|
||||||
|
// float heat_time = m_filpar[m_current_tool].filament_cooling_before_tower / m_hotend_heating_rate[extruder_id];
|
||||||
|
// if (estimate_time < heat_time) speed_factor = estimate_time / heat_time;
|
||||||
|
// }
|
||||||
|
(void) estimate_wipe_time; // retain scaffolding above without an unused-lambda warning
|
||||||
|
}
|
||||||
|
float wipe_speed = nominal_speed * speed_factor;
|
||||||
|
|
||||||
// now the wiping itself:
|
// now the wiping itself:
|
||||||
for (int i = 0; true; ++i) {
|
for (int i = 0; true; ++i) {
|
||||||
if (i != 0) {
|
if (i != 0) {
|
||||||
if (wipe_speed < 0.34f * target_speed)
|
if (nominal_speed < 0.34f * target_speed)
|
||||||
wipe_speed = 0.375f * target_speed;
|
nominal_speed = 0.375f * target_speed;
|
||||||
else if (wipe_speed < 0.377 * target_speed)
|
else if (nominal_speed < 0.377 * target_speed)
|
||||||
wipe_speed = 0.458f * target_speed;
|
nominal_speed = 0.458f * target_speed;
|
||||||
else if (wipe_speed < 0.46f * target_speed)
|
else if (nominal_speed < 0.46f * target_speed)
|
||||||
wipe_speed = 0.875f * target_speed;
|
nominal_speed = 0.875f * target_speed;
|
||||||
else
|
else
|
||||||
wipe_speed = std::min(target_speed, wipe_speed + 50.f);
|
nominal_speed = std::min(target_speed, nominal_speed + 50.f);
|
||||||
|
wipe_speed = nominal_speed * speed_factor;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool need_change_flow = need_thick_bridge_flow(writer.y());
|
bool need_change_flow = need_thick_bridge_flow(writer.y());
|
||||||
@@ -3453,6 +3585,7 @@ void WipeTower::toolchange_wipe_new(WipeTowerWriter &writer, const box_coordinat
|
|||||||
} else
|
} else
|
||||||
writer.travel(writer.x() + 1.5 * ironing_length, writer.y(), 240.);
|
writer.travel(writer.x() + 1.5 * ironing_length, writer.y(), 240.);
|
||||||
writer.retract(-retract_length, retract_speed);
|
writer.retract(-retract_length, retract_speed);
|
||||||
|
add_M104_by_requirement(); // Pre-heat the arriving hotend during the wipe
|
||||||
writer.extrude(xr + wipe_tower_wall_infill_overlap * m_perimeter_width, writer.y(), wipe_speed);
|
writer.extrude(xr + wipe_tower_wall_infill_overlap * m_perimeter_width, writer.y(), wipe_speed);
|
||||||
} else {
|
} else {
|
||||||
float dx = xl - wipe_tower_wall_infill_overlap * m_perimeter_width - writer.pos().x();
|
float dx = xl - wipe_tower_wall_infill_overlap * m_perimeter_width - writer.pos().x();
|
||||||
@@ -3468,9 +3601,11 @@ void WipeTower::toolchange_wipe_new(WipeTowerWriter &writer, const box_coordinat
|
|||||||
}else
|
}else
|
||||||
writer.travel(writer.x() - 1.5 * ironing_length, writer.y(), 240.);
|
writer.travel(writer.x() - 1.5 * ironing_length, writer.y(), 240.);
|
||||||
writer.retract(-retract_length, retract_speed);
|
writer.retract(-retract_length, retract_speed);
|
||||||
|
add_M104_by_requirement(); // Pre-heat the arriving hotend during the wipe
|
||||||
writer.extrude(xl - wipe_tower_wall_infill_overlap * m_perimeter_width, writer.y(), wipe_speed);
|
writer.extrude(xl - wipe_tower_wall_infill_overlap * m_perimeter_width, writer.y(), wipe_speed);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
if (i == 0) add_M104_by_requirement(); // Pre-heat the arriving hotend during the wipe
|
||||||
if (m_left_to_right)
|
if (m_left_to_right)
|
||||||
writer.extrude(xr + wipe_tower_wall_infill_overlap * m_perimeter_width, writer.y(), wipe_speed);
|
writer.extrude(xr + wipe_tower_wall_infill_overlap * m_perimeter_width, writer.y(), wipe_speed);
|
||||||
else
|
else
|
||||||
@@ -3571,6 +3706,47 @@ bool WipeTower::is_in_same_extruder(int filament_id_1, int filament_id_2)
|
|||||||
return m_filament_map[filament_id_1] == m_filament_map[filament_id_2];
|
return m_filament_map[filament_id_1] == m_filament_map[filament_id_2];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Per-extruder printable-height clamp: is an extruder still allowed to print on this wipe-tower layer,
|
||||||
|
// or is it its final layer above the extruder's printable height?
|
||||||
|
// Orca: the arriving extruder id is resolved as m_filament_map[tool]-1 (1-based map, layer-static),
|
||||||
|
// because Orca's wipe tower is extruder-level rather than tracking a per-layer nozzle map (the same
|
||||||
|
// idiom the pre-heat path uses in toolchange_wipe_new). Gated on m_is_multi_extruder so that
|
||||||
|
// single-extruder printers (including ones whose extruder_printable_height defaults to {0}) always
|
||||||
|
// return true and leave wipe-tower g-code unchanged.
|
||||||
|
bool WipeTower::is_valid_last_layer(int tool, int layer_id, double layer_z) const
|
||||||
|
{
|
||||||
|
if (!m_is_multi_extruder)
|
||||||
|
return true;
|
||||||
|
int extruder_id = (tool >= 0 && tool < (int) m_filament_map.size()) ? m_filament_map[tool] - 1 : -1;
|
||||||
|
if (extruder_id < 0 || extruder_id >= (int) m_printable_height.size() || extruder_id >= (int) m_last_layer_id.size())
|
||||||
|
return true;
|
||||||
|
if (m_last_layer_id[extruder_id] == layer_id && layer_z > m_printable_height[extruder_id])
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Records, per extruder, the last wipe-tower layer index that uses it, so is_valid_last_layer can
|
||||||
|
// recognise the extruder's final layer. Inert for single-extruder printers (early return);
|
||||||
|
// bounds-checked because m_filament_map may be empty/short.
|
||||||
|
void WipeTower::set_nozzle_last_layer_id()
|
||||||
|
{
|
||||||
|
if (!m_is_multi_extruder)
|
||||||
|
return;
|
||||||
|
for (int idx = 0; idx < (int) m_plan.size(); ++idx) {
|
||||||
|
const auto &info = m_plan[idx];
|
||||||
|
for (const auto &tc : info.tool_changes) {
|
||||||
|
int old_tool = (int) tc.old_tool;
|
||||||
|
int new_tool = (int) tc.new_tool;
|
||||||
|
int old_ext = (old_tool >= 0 && old_tool < (int) m_filament_map.size()) ? m_filament_map[old_tool] - 1 : -1;
|
||||||
|
int new_ext = (new_tool >= 0 && new_tool < (int) m_filament_map.size()) ? m_filament_map[new_tool] - 1 : -1;
|
||||||
|
if (old_ext >= 0 && old_ext < (int) m_last_layer_id.size())
|
||||||
|
m_last_layer_id[old_ext] = idx;
|
||||||
|
if (new_ext >= 0 && new_ext < (int) m_last_layer_id.size())
|
||||||
|
m_last_layer_id[new_ext] = idx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void WipeTower::reset_block_status()
|
void WipeTower::reset_block_status()
|
||||||
{
|
{
|
||||||
for (auto &block : m_wipe_tower_blocks) {
|
for (auto &block : m_wipe_tower_blocks) {
|
||||||
@@ -3797,6 +3973,7 @@ void WipeTower::plan_tower_new()
|
|||||||
}
|
}
|
||||||
|
|
||||||
update_all_layer_depth(max_depth);
|
update_all_layer_depth(max_depth);
|
||||||
|
set_nozzle_last_layer_id(); // record per-extruder last layer for is_valid_last_layer
|
||||||
float diagonal = sqrt(m_wipe_tower_depth * m_wipe_tower_depth + m_wipe_tower_width * m_wipe_tower_width);
|
float diagonal = sqrt(m_wipe_tower_depth * m_wipe_tower_depth + m_wipe_tower_width * m_wipe_tower_width);
|
||||||
m_rib_length = std::max({m_rib_length, diagonal});
|
m_rib_length = std::max({m_rib_length, diagonal});
|
||||||
m_rib_length += m_extra_rib_length;
|
m_rib_length += m_extra_rib_length;
|
||||||
@@ -3916,9 +4093,12 @@ void WipeTower::generate_new(std::vector<std::vector<WipeTower::ToolChangeResult
|
|||||||
int candidate_id = -1;
|
int candidate_id = -1;
|
||||||
for (size_t idx = 0; idx < layer.tool_changes.size(); ++idx) {
|
for (size_t idx = 0; idx < layer.tool_changes.size(); ++idx) {
|
||||||
if (idx == 0) {
|
if (idx == 0) {
|
||||||
if (layer.tool_changes[idx].old_tool == wall_filament_id)
|
// An extruder's last-layer filament above its printable height cannot supply the
|
||||||
|
// outer wall. is_valid_last_layer is inert unless it clamps.
|
||||||
|
if (layer.tool_changes[idx].old_tool == wall_filament_id && is_valid_last_layer(layer.tool_changes[idx].old_tool, m_cur_layer_id, layer.z))
|
||||||
return wall_filament_id;
|
return wall_filament_id;
|
||||||
else if (m_filpar[layer.tool_changes[idx].old_tool].category == m_filpar[wall_filament_id].category) {
|
else if (m_filpar[layer.tool_changes[idx].old_tool].category == m_filpar[wall_filament_id].category &&
|
||||||
|
is_valid_last_layer(layer.tool_changes[idx].old_tool, m_cur_layer_id, layer.z)) {
|
||||||
candidate_id = layer.tool_changes[idx].old_tool;
|
candidate_id = layer.tool_changes[idx].old_tool;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4017,6 +4197,10 @@ void WipeTower::generate_new(std::vector<std::vector<WipeTower::ToolChangeResult
|
|||||||
finish_layer_filament = wall_idx;
|
finish_layer_filament = wall_idx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cancel a block on the last layer above its extruder's printable height.
|
||||||
|
// is_valid_last_layer is inert unless multi-extruder near Z-max.
|
||||||
|
if (!is_valid_last_layer(finish_layer_filament, m_cur_layer_id, layer.z)) continue;
|
||||||
|
|
||||||
ToolChangeResult finish_block_tcr;
|
ToolChangeResult finish_block_tcr;
|
||||||
if (interface_solid || (block.solid_infill[m_cur_layer_id] && block.filament_adhesiveness_category != m_filament_categories[finish_layer_filament])) {
|
if (interface_solid || (block.solid_infill[m_cur_layer_id] && block.filament_adhesiveness_category != m_filament_categories[finish_layer_filament])) {
|
||||||
interface_solid = interface_solid && !((block.solid_infill[m_cur_layer_id] && block.filament_adhesiveness_category != m_filament_categories[finish_layer_filament]));//noly reduce speed when
|
interface_solid = interface_solid && !((block.solid_infill[m_cur_layer_id] && block.filament_adhesiveness_category != m_filament_categories[finish_layer_filament]));//noly reduce speed when
|
||||||
|
|||||||
@@ -313,6 +313,13 @@ public:
|
|||||||
void set_has_tpu_filament(bool has_tpu) { m_has_tpu_filament = has_tpu; }
|
void set_has_tpu_filament(bool has_tpu) { m_has_tpu_filament = has_tpu; }
|
||||||
bool has_tpu_filament() const { return m_has_tpu_filament; }
|
bool has_tpu_filament() const { return m_has_tpu_filament; }
|
||||||
|
|
||||||
|
// Orca: has_filament_switcher is not a static PrintConfig member, so it is pushed in from Print
|
||||||
|
// via a setter rather than read in the ctor. Device-set only.
|
||||||
|
void set_has_filament_switcher(bool v) { m_has_filament_switcher = v; }
|
||||||
|
// The region every extruder can reach, used to clamp the PETG pre-extrusion offset to the
|
||||||
|
// printable bed.
|
||||||
|
void set_shared_print_bed(const Polygons &bed) { m_shared_print_bed = bed; }
|
||||||
|
|
||||||
struct FilamentParameters {
|
struct FilamentParameters {
|
||||||
std::string material = "PLA";
|
std::string material = "PLA";
|
||||||
int category;
|
int category;
|
||||||
@@ -341,8 +348,14 @@ public:
|
|||||||
float wipe_dist;
|
float wipe_dist;
|
||||||
float tower_interface_pre_extrusion_dist = 0.f;
|
float tower_interface_pre_extrusion_dist = 0.f;
|
||||||
float tower_interface_pre_extrusion_length = 0.f;
|
float tower_interface_pre_extrusion_length = 0.f;
|
||||||
|
// Outward shift of the wipe start for a PETG pre-extrusion on filament-switcher devices;
|
||||||
|
// set from filament_tower_interface_pre_extrusion_dist.
|
||||||
|
float petg_pre_extrusion_offset_dist = 0.f;
|
||||||
float tower_ironing_area = 4.f;
|
float tower_ironing_area = 4.f;
|
||||||
float tower_interface_purge_length = 0.f;
|
float tower_interface_purge_length = 0.f;
|
||||||
|
// Distance (in mm of filament) that a hotend is allowed to pre-cool before the
|
||||||
|
// tower is reached; drives the prime-tower heating-during-wipe model (multi-nozzle only).
|
||||||
|
float filament_cooling_before_tower = 0.f;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -462,6 +475,22 @@ private:
|
|||||||
bool m_adhesion = true;
|
bool m_adhesion = true;
|
||||||
GCodeFlavor m_gcode_flavor;
|
GCodeFlavor m_gcode_flavor;
|
||||||
|
|
||||||
|
// Multi-nozzle prime-tower heating during wipe. m_is_multiple_nozzle gates the whole
|
||||||
|
// feature; it is false for every current (single-nozzle) printer (extruder_max_nozzle_count
|
||||||
|
// defaults to 1), so the pre-heat/pre-cool path is inert and wipe-tower g-code is unchanged.
|
||||||
|
bool m_is_multiple_nozzle = false;
|
||||||
|
std::vector<double> m_hotend_heating_rate; // config.hotend_heating_rate (deg/s per extruder)
|
||||||
|
std::vector<int> m_physical_extruder_map; // logical extruder -> physical tool number (M104 T param)
|
||||||
|
|
||||||
|
// Per-extruder printable-height clamp. m_printable_height = config.extruder_printable_height
|
||||||
|
// (per-extruder Z limit; empty for single-extruder printers, [320,325] for H2D). m_last_layer_id
|
||||||
|
// records, per extruder, the last wipe-tower layer that uses it. is_valid_last_layer() is gated on
|
||||||
|
// m_is_multi_extruder so single-extruder wipe-tower g-code is unchanged; the clamp only bites a
|
||||||
|
// multi-extruder wipe tower whose final per-extruder layer exceeds that extruder's printable
|
||||||
|
// height (near the Z limit).
|
||||||
|
std::vector<double> m_printable_height;
|
||||||
|
std::vector<int> m_last_layer_id;
|
||||||
|
|
||||||
// Bed properties
|
// Bed properties
|
||||||
enum {
|
enum {
|
||||||
RectangularBed,
|
RectangularBed,
|
||||||
@@ -501,6 +530,11 @@ private:
|
|||||||
bool m_flat_ironing=false;
|
bool m_flat_ironing=false;
|
||||||
bool m_enable_tower_interface_features=false;
|
bool m_enable_tower_interface_features=false;
|
||||||
bool m_enable_tower_interface_cooldown_during_tower=false;
|
bool m_enable_tower_interface_cooldown_during_tower=false;
|
||||||
|
// Filament-switcher device flag + shared printable bed for the PETG pre-extrusion offset.
|
||||||
|
// m_has_filament_switcher is false for the whole shipping fleet (no profile sets the key), so
|
||||||
|
// the PETG branch in get_next_pos never runs -> no change fleet-wide.
|
||||||
|
bool m_has_filament_switcher=false;
|
||||||
|
Polygons m_shared_print_bed;
|
||||||
bool m_prev_layer_had_interface=false;
|
bool m_prev_layer_had_interface=false;
|
||||||
bool m_current_layer_has_interface=false;
|
bool m_current_layer_has_interface=false;
|
||||||
// Calculates length of extrusion line to extrude given volume
|
// Calculates length of extrusion line to extrude given volume
|
||||||
@@ -520,6 +554,7 @@ private:
|
|||||||
void save_on_last_wipe();
|
void save_on_last_wipe();
|
||||||
|
|
||||||
bool is_tpu_filament(int filament_id) const;
|
bool is_tpu_filament(int filament_id) const;
|
||||||
|
bool is_petg_filament(int filament_id) const;
|
||||||
|
|
||||||
// BBS
|
// BBS
|
||||||
box_coordinates align_perimeter(const box_coordinates& perimeter_box);
|
box_coordinates align_perimeter(const box_coordinates& perimeter_box);
|
||||||
@@ -586,6 +621,12 @@ private:
|
|||||||
const box_coordinates &cleaning_box,
|
const box_coordinates &cleaning_box,
|
||||||
float wipe_volume);
|
float wipe_volume);
|
||||||
void get_wall_skip_points(const WipeTowerInfo &layer);
|
void get_wall_skip_points(const WipeTowerInfo &layer);
|
||||||
|
|
||||||
|
// Per-extruder printable-height clamp (see m_printable_height). is_valid_last_layer returns
|
||||||
|
// false only for a multi-extruder wipe tower's final per-extruder layer that exceeds that
|
||||||
|
// extruder's printable height; returns true (no clamp) in every other case.
|
||||||
|
bool is_valid_last_layer(int tool, int layer_id, double layer_z) const;
|
||||||
|
void set_nozzle_last_layer_id();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -632,6 +632,25 @@ std::string GCodeWriter::toolchange(unsigned int filament_id)
|
|||||||
return gcode.str();
|
return gcode.str();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Current parked-retract length of the filament's extruder, share-aware. m_filament_extruders is
|
||||||
|
// sorted by id (see toolchange), so a lower_bound lookup finds the entry; unknown filament ids
|
||||||
|
// degrade to 0 rather than dereferencing end().
|
||||||
|
double GCodeWriter::get_extruder_retracted_length(const int filament_id)
|
||||||
|
{
|
||||||
|
double res = 0.0;
|
||||||
|
auto filament_extruder_iter = Slic3r::lower_bound_by_predicate(m_filament_extruders.begin(), m_filament_extruders.end(),
|
||||||
|
[filament_id](const Extruder &e) { return (int) e.id() < filament_id; });
|
||||||
|
if (filament_extruder_iter == m_filament_extruders.end() || (int) filament_extruder_iter->id() != filament_id)
|
||||||
|
return res;
|
||||||
|
|
||||||
|
if (filament_extruder_iter->is_share_extruder())
|
||||||
|
res = filament_extruder_iter->get_share_retracted_length();
|
||||||
|
else
|
||||||
|
res = filament_extruder_iter->get_single_retracted_length();
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
std::string GCodeWriter::set_speed(double F, const std::string &comment, const std::string &cooling_marker)
|
std::string GCodeWriter::set_speed(double F, const std::string &comment, const std::string &cooling_marker)
|
||||||
{
|
{
|
||||||
assert(F > 0.);
|
assert(F > 0.);
|
||||||
@@ -1101,7 +1120,7 @@ std::string GCodeWriter::_retract(double length, double restart_extra, const std
|
|||||||
return gcode;
|
return gcode;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string GCodeWriter::unretract()
|
std::string GCodeWriter::unretract(float extra_retract)
|
||||||
{
|
{
|
||||||
std::string gcode;
|
std::string gcode;
|
||||||
|
|
||||||
@@ -1117,7 +1136,9 @@ std::string GCodeWriter::unretract()
|
|||||||
//BBS
|
//BBS
|
||||||
// use G1 instead of G0 because G0 will blend the restart with the previous travel move
|
// use G1 instead of G0 because G0 will blend the restart with the previous travel move
|
||||||
GCodeG1Formatter w;
|
GCodeG1Formatter w;
|
||||||
w.emit_e(filament()->E());
|
// extra_retract over-extrudes for the PETG pre-extrusion; 0 by
|
||||||
|
// default -> identical to the plain deretract E position.
|
||||||
|
w.emit_e(filament()->E() + extra_retract);
|
||||||
w.emit_f(filament()->deretract_speed() * 60.);
|
w.emit_f(filament()->deretract_speed() * 60.);
|
||||||
//BBS
|
//BBS
|
||||||
w.emit_comment(GCodeWriter::full_gcode_comment, " ; unretract");
|
w.emit_comment(GCodeWriter::full_gcode_comment, " ; unretract");
|
||||||
|
|||||||
@@ -66,6 +66,9 @@ public:
|
|||||||
bool need_toolchange(unsigned int filament_id) const;
|
bool need_toolchange(unsigned int filament_id) const;
|
||||||
std::string set_extruder(unsigned int filament_id);
|
std::string set_extruder(unsigned int filament_id);
|
||||||
void init_extruder(unsigned int filament_id);
|
void init_extruder(unsigned int filament_id);
|
||||||
|
// Current parked-retract length of a filament's extruder (share-aware). Used for the
|
||||||
|
// new_extruder_retracted_length change-filament placeholder. Returns 0 if the filament is unknown.
|
||||||
|
double get_extruder_retracted_length(const int filament_id);
|
||||||
// Prefix of the toolchange G-code line, to be used by the CoolingBuffer to separate sections of the G-code
|
// Prefix of the toolchange G-code line, to be used by the CoolingBuffer to separate sections of the G-code
|
||||||
// printed with the same extruder.
|
// printed with the same extruder.
|
||||||
std::string toolchange_prefix() const;
|
std::string toolchange_prefix() const;
|
||||||
@@ -83,7 +86,9 @@ public:
|
|||||||
std::string extrude_to_xyz(const Vec3d &point, double dE, const std::string &comment = std::string(), bool force_no_extrusion = false);
|
std::string extrude_to_xyz(const Vec3d &point, double dE, const std::string &comment = std::string(), bool force_no_extrusion = false);
|
||||||
std::string retract(bool before_wipe = false, double retract_length = 0);
|
std::string retract(bool before_wipe = false, double retract_length = 0);
|
||||||
std::string retract_for_toolchange(bool before_wipe = false, double retract_length = 0);
|
std::string retract_for_toolchange(bool before_wipe = false, double retract_length = 0);
|
||||||
std::string unretract();
|
// extra_retract adds a small over-extrusion to the deretract move (PETG pre-extrusion).
|
||||||
|
// Default 0 -> byte-identical to the plain deretract.
|
||||||
|
std::string unretract(float extra_retract = 0.f);
|
||||||
// do lift instantly
|
// do lift instantly
|
||||||
std::string eager_lift(const LiftType type);
|
std::string eager_lift(const LiftType type);
|
||||||
// record a lift request, do realy lift in next travel
|
// record a lift request, do realy lift in next travel
|
||||||
|
|||||||
@@ -0,0 +1,922 @@
|
|||||||
|
#include "MultiNozzleUtils.hpp"
|
||||||
|
#include "Utils.hpp"
|
||||||
|
#include "ProjectTask.hpp" // Slic3r::FilamentInfo (StaticNozzleGroupResult / load_nozzle_infos_with_compatibility)
|
||||||
|
#include <algorithm>
|
||||||
|
#include <iomanip>
|
||||||
|
#include <sstream>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <unordered_set>
|
||||||
|
#include <boost/log/trivial.hpp>
|
||||||
|
|
||||||
|
// Multi-nozzle support.
|
||||||
|
|
||||||
|
namespace Slic3r { namespace MultiNozzleUtils {
|
||||||
|
// ==================== tool function implementations ====================
|
||||||
|
std::vector<NozzleInfo> build_nozzle_list(std::vector<NozzleGroupInfo> nozzle_groups)
|
||||||
|
{
|
||||||
|
std::vector<NozzleInfo> ret;
|
||||||
|
std::sort(nozzle_groups.begin(), nozzle_groups.end());
|
||||||
|
int nozzle_id = 0;
|
||||||
|
for (auto& group : nozzle_groups) {
|
||||||
|
for (int i = 0; i < group.nozzle_count; ++i) {
|
||||||
|
NozzleInfo tmp;
|
||||||
|
tmp.diameter = group.diameter;
|
||||||
|
tmp.extruder_id = group.extruder_id;
|
||||||
|
tmp.volume_type = group.volume_type;
|
||||||
|
tmp.group_id = nozzle_id++;
|
||||||
|
ret.emplace_back(std::move(tmp));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<NozzleInfo> build_nozzle_list(double diameter, const std::vector<int>& filament_nozzle_map, const std::vector<int>& filament_volume_map, const std::vector<int>& filament_map)
|
||||||
|
{
|
||||||
|
std::string diameter_str = format_diameter_to_str(diameter);
|
||||||
|
std::map<int, std::vector<int>> nozzle_to_filaments;
|
||||||
|
for(size_t idx = 0; idx < filament_nozzle_map.size(); ++idx){
|
||||||
|
int nozzle_id = filament_nozzle_map[idx];
|
||||||
|
nozzle_to_filaments[nozzle_id].emplace_back(static_cast<int>(idx));
|
||||||
|
}
|
||||||
|
std::vector<NozzleInfo> ret;
|
||||||
|
for(auto& elem : nozzle_to_filaments){
|
||||||
|
int nozzle_id = elem.first;
|
||||||
|
auto& filaments = elem.second;
|
||||||
|
NozzleInfo info;
|
||||||
|
info.diameter = diameter_str;
|
||||||
|
info.group_id = nozzle_id;
|
||||||
|
info.extruder_id = filament_map[filaments.front()];
|
||||||
|
info.volume_type = NozzleVolumeType(filament_volume_map[filaments.front()]);
|
||||||
|
ret.emplace_back(std::move(info));
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== LayeredNozzleGroupResult ====================
|
||||||
|
static bool has_filament_mapped_to_multiple_nozzles(const std::vector<std::vector<int>> &layer_filament_nozzle_maps,
|
||||||
|
const std::vector<unsigned int> &used_filaments)
|
||||||
|
{
|
||||||
|
if (layer_filament_nozzle_maps.empty() || used_filaments.empty())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
for (auto filament_id_u : used_filaments) {
|
||||||
|
int filament_id = static_cast<int>(filament_id_u);
|
||||||
|
std::set<int> nozzle_ids;
|
||||||
|
|
||||||
|
for (size_t layer_id = 0; layer_id < layer_filament_nozzle_maps.size(); ++layer_id) {
|
||||||
|
const auto &map = layer_filament_nozzle_maps[layer_id];
|
||||||
|
if (filament_id < 0 || filament_id >= static_cast<int>(map.size()))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
int nozzle_id = map[filament_id];
|
||||||
|
if (nozzle_id < 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
nozzle_ids.insert(nozzle_id);
|
||||||
|
if (nozzle_ids.size() > 1)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<LayeredNozzleGroupResult> LayeredNozzleGroupResult::create(
|
||||||
|
const std::vector<int>& filament_nozzle_map,
|
||||||
|
const std::vector<NozzleInfo>& nozzle_list,
|
||||||
|
const std::vector<unsigned int>& used_filaments)
|
||||||
|
{
|
||||||
|
if (filament_nozzle_map.empty() || nozzle_list.empty()) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
LayeredNozzleGroupResult result(false);
|
||||||
|
result._default_filament_nozzle_map = filament_nozzle_map;
|
||||||
|
result._nozzle_list = nozzle_list;
|
||||||
|
result._used_filaments = used_filaments;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<LayeredNozzleGroupResult> LayeredNozzleGroupResult::create(
|
||||||
|
const std::vector<std::vector<int>>& layer_filament_nozzle_maps,
|
||||||
|
const std::vector<NozzleInfo>& nozzle_list,
|
||||||
|
const std::vector<unsigned int>& used_filaments,
|
||||||
|
const std::vector<std::vector<unsigned int>>& layer_filament_sequences)
|
||||||
|
{
|
||||||
|
if (layer_filament_nozzle_maps.empty() || nozzle_list.empty()) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool support_dynamic_nozzle_map = has_filament_mapped_to_multiple_nozzles(layer_filament_nozzle_maps, used_filaments);
|
||||||
|
LayeredNozzleGroupResult result(support_dynamic_nozzle_map);
|
||||||
|
result._layer_filament_nozzle_maps = layer_filament_nozzle_maps;
|
||||||
|
result._layer_filament_sequences = layer_filament_sequences;
|
||||||
|
result._nozzle_list = nozzle_list;
|
||||||
|
result._used_filaments = used_filaments;
|
||||||
|
|
||||||
|
if (!layer_filament_nozzle_maps.empty()) {
|
||||||
|
result._default_filament_nozzle_map = layer_filament_nozzle_maps[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<LayeredNozzleGroupResult> LayeredNozzleGroupResult::create(
|
||||||
|
const std::vector<unsigned int>& used_filaments,
|
||||||
|
const std::vector<int>& filament_map,
|
||||||
|
const std::vector<int>& filament_volume_map,
|
||||||
|
const std::vector<int>& filament_nozzle_map,
|
||||||
|
const std::vector<std::map<NozzleVolumeType, int>> &nozzle_count,
|
||||||
|
float diameter)
|
||||||
|
{
|
||||||
|
std::vector<NozzleGroupInfo> nozzle_groups;
|
||||||
|
for (size_t extruder_id = 0; extruder_id < nozzle_count.size(); ++extruder_id) {
|
||||||
|
for (auto elem : nozzle_count[extruder_id]) {
|
||||||
|
NozzleGroupInfo group_info;
|
||||||
|
group_info.diameter = format_diameter_to_str(diameter);
|
||||||
|
group_info.volume_type = elem.first;
|
||||||
|
group_info.nozzle_count = elem.second;
|
||||||
|
group_info.extruder_id = static_cast<int>(extruder_id);
|
||||||
|
nozzle_groups.emplace_back(group_info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto nozzle_list = build_nozzle_list(nozzle_groups);
|
||||||
|
std::vector<bool> used_nozzle(nozzle_list.size(), false);
|
||||||
|
std::map<int, int> input_nozzle_id_to_output;
|
||||||
|
std::vector<int> output_nozzle_map(filament_nozzle_map.size(), 0);
|
||||||
|
|
||||||
|
for (auto filament_idx : used_filaments) {
|
||||||
|
NozzleVolumeType req_type = NozzleVolumeType(filament_volume_map[filament_idx]);
|
||||||
|
int req_extruder = filament_map[filament_idx];
|
||||||
|
int input_nozzle_idx = filament_nozzle_map[filament_idx];
|
||||||
|
|
||||||
|
if (input_nozzle_id_to_output.find(input_nozzle_idx) != input_nozzle_id_to_output.end()) {
|
||||||
|
output_nozzle_map[filament_idx] = input_nozzle_id_to_output[input_nozzle_idx];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int output_nozzle_idx = -1;
|
||||||
|
for (size_t nozzle_idx = 0; nozzle_idx < nozzle_list.size(); ++nozzle_idx) {
|
||||||
|
if (used_nozzle[nozzle_idx]) continue;
|
||||||
|
|
||||||
|
auto &nozzle_info = nozzle_list[nozzle_idx];
|
||||||
|
if (!(nozzle_info.extruder_id == req_extruder && nozzle_info.volume_type == req_type)) continue;
|
||||||
|
|
||||||
|
output_nozzle_idx = static_cast<int>(nozzle_idx);
|
||||||
|
input_nozzle_id_to_output[input_nozzle_idx] = output_nozzle_idx;
|
||||||
|
used_nozzle[nozzle_idx] = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (output_nozzle_idx == -1) { return std::nullopt; }
|
||||||
|
output_nozzle_map[filament_idx] = output_nozzle_idx;
|
||||||
|
}
|
||||||
|
|
||||||
|
return create(output_nozzle_map, nozzle_list, used_filaments);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool LayeredNozzleGroupResult::are_filaments_same_extruder(int filament_id1, int filament_id2, int layer_id) const
|
||||||
|
{
|
||||||
|
std::optional<NozzleInfo> nozzle_info1 = get_nozzle_for_filament(filament_id1, layer_id);
|
||||||
|
std::optional<NozzleInfo> nozzle_info2 = get_nozzle_for_filament(filament_id2, layer_id);
|
||||||
|
|
||||||
|
if (!nozzle_info1 || !nozzle_info2) return false;
|
||||||
|
|
||||||
|
return nozzle_info1->extruder_id == nozzle_info2->extruder_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool LayeredNozzleGroupResult::are_filaments_same_nozzle(int filament_id1, int filament_id2, int layer_id) const
|
||||||
|
{
|
||||||
|
std::optional<NozzleInfo> nozzle_info1 = get_nozzle_for_filament(filament_id1, layer_id);
|
||||||
|
std::optional<NozzleInfo> nozzle_info2 = get_nozzle_for_filament(filament_id2, layer_id);
|
||||||
|
if (!nozzle_info1 || !nozzle_info2) return false;
|
||||||
|
|
||||||
|
return nozzle_info1->group_id == nozzle_info2->group_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
int LayeredNozzleGroupResult::get_extruder_count() const
|
||||||
|
{
|
||||||
|
std::set<int> extruder_ids;
|
||||||
|
for (const auto &nozzle : _nozzle_list) { extruder_ids.insert(nozzle.extruder_id); }
|
||||||
|
return static_cast<int>(extruder_ids.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<NozzleInfo> LayeredNozzleGroupResult::get_used_nozzles_in_extruder(int target_extruder_id) const
|
||||||
|
{
|
||||||
|
return get_used_nozzles_in_extruder(target_extruder_id, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<NozzleInfo> LayeredNozzleGroupResult::get_used_nozzles_in_extruder(int target_extruder_id, int layer_id) const
|
||||||
|
{
|
||||||
|
std::set<int> nozzle_ids;
|
||||||
|
std::vector<NozzleInfo> result;
|
||||||
|
|
||||||
|
std::vector<unsigned int> target_filaments = get_used_filaments(layer_id);
|
||||||
|
|
||||||
|
for (unsigned int filament_id : target_filaments) {
|
||||||
|
if (layer_id != -1) {
|
||||||
|
auto nozzle_opt = get_nozzle_for_filament(static_cast<int>(filament_id), layer_id);
|
||||||
|
if (nozzle_opt) {
|
||||||
|
if (target_extruder_id == -1 || nozzle_opt->extruder_id == target_extruder_id) { nozzle_ids.insert(nozzle_opt->group_id); }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
auto nozzles = get_nozzles_for_filament(static_cast<int>(filament_id));
|
||||||
|
for (const auto &nozzle : nozzles) {
|
||||||
|
if (target_extruder_id == -1 || nozzle.extruder_id == target_extruder_id) { nozzle_ids.insert(nozzle.group_id); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int nozzle_id : nozzle_ids) {
|
||||||
|
if (nozzle_id >= 0 && nozzle_id < static_cast<int>(_nozzle_list.size())) { result.push_back(_nozzle_list[nozzle_id]); }
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<int> LayeredNozzleGroupResult::get_used_extruders() const
|
||||||
|
{
|
||||||
|
return get_used_extruders(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<int> LayeredNozzleGroupResult::get_used_extruders(int layer_id) const
|
||||||
|
{
|
||||||
|
std::set<int> used_extruders;
|
||||||
|
// used filaments on the given layer (or globally)
|
||||||
|
std::vector<unsigned int> target_filaments = get_used_filaments(layer_id);
|
||||||
|
for (auto filament_id : target_filaments) {
|
||||||
|
if (layer_id != -1) {
|
||||||
|
// single-layer: nozzle used by this filament on this layer
|
||||||
|
auto nozzle_opt = get_nozzle_for_filament(static_cast<int>(filament_id), layer_id);
|
||||||
|
if (nozzle_opt) { used_extruders.insert(nozzle_opt->extruder_id); }
|
||||||
|
} else {
|
||||||
|
// global: every nozzle this filament uses across all layers
|
||||||
|
auto nozzles = get_nozzles_for_filament(static_cast<int>(filament_id));
|
||||||
|
for (const auto &nozzle : nozzles) { used_extruders.insert(nozzle.extruder_id); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return std::vector<int>(used_extruders.begin(), used_extruders.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<int> LayeredNozzleGroupResult::get_extruder_map(bool zero_based, int layer_id) const
|
||||||
|
{
|
||||||
|
const std::vector<int> &filament_nozzle_map = get_layer_filament_nozzle_map(layer_id);
|
||||||
|
std::vector<int> extruder_map(filament_nozzle_map.size());
|
||||||
|
for (size_t idx = 0; idx < filament_nozzle_map.size(); ++idx) {
|
||||||
|
int nozzle_id = filament_nozzle_map[idx];
|
||||||
|
if (nozzle_id >= 0 && nozzle_id < static_cast<int>(_nozzle_list.size())) {
|
||||||
|
extruder_map[idx] = _nozzle_list[nozzle_id].extruder_id;
|
||||||
|
} else {
|
||||||
|
extruder_map[idx] = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (zero_based) return extruder_map;
|
||||||
|
|
||||||
|
auto new_filament_map = extruder_map;
|
||||||
|
std::transform(new_filament_map.begin(), new_filament_map.end(), new_filament_map.begin(), [](int val) { return val + 1; });
|
||||||
|
return new_filament_map;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<int> LayeredNozzleGroupResult::get_nozzle_map(int layer_id) const
|
||||||
|
{
|
||||||
|
const std::vector<int> &filament_nozzle_map = get_layer_filament_nozzle_map(layer_id);
|
||||||
|
std::vector<int> nozzle_map(filament_nozzle_map.size());
|
||||||
|
for (size_t idx = 0; idx < filament_nozzle_map.size(); ++idx) {
|
||||||
|
int nozzle_id = filament_nozzle_map[idx];
|
||||||
|
if (nozzle_id >= 0 && nozzle_id < static_cast<int>(_nozzle_list.size())) {
|
||||||
|
nozzle_map[idx] = _nozzle_list[nozzle_id].group_id;
|
||||||
|
} else {
|
||||||
|
nozzle_map[idx] = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nozzle_map;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<int> LayeredNozzleGroupResult::get_volume_map(int layer_id) const
|
||||||
|
{
|
||||||
|
const std::vector<int> &filament_nozzle_map = get_layer_filament_nozzle_map(layer_id);
|
||||||
|
std::vector<int> volume_map(filament_nozzle_map.size());
|
||||||
|
for (size_t idx = 0; idx < filament_nozzle_map.size(); ++idx) {
|
||||||
|
int nozzle_id = filament_nozzle_map[idx];
|
||||||
|
if (nozzle_id >= 0 && nozzle_id < static_cast<int>(_nozzle_list.size())) {
|
||||||
|
volume_map[idx] = _nozzle_list[nozzle_id].volume_type;
|
||||||
|
} else {
|
||||||
|
volume_map[idx] = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return volume_map;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<unsigned int> LayeredNozzleGroupResult::get_used_filaments(int layer_id) const
|
||||||
|
{
|
||||||
|
if (layer_id < 0) { return _used_filaments; }
|
||||||
|
if (layer_id >= static_cast<int>(_layer_filament_nozzle_maps.size())) { return _used_filaments; }
|
||||||
|
|
||||||
|
if (!_layer_filament_sequences.empty() && layer_id < static_cast<int>(_layer_filament_sequences.size())) {
|
||||||
|
return _layer_filament_sequences[layer_id];
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<NozzleInfo> LayeredNozzleGroupResult::get_nozzle_for_filament(int filament_id, int layer_id) const
|
||||||
|
{
|
||||||
|
const std::vector<int> &filament_nozzle_map = get_layer_filament_nozzle_map(layer_id);
|
||||||
|
|
||||||
|
if (filament_id < 0 || filament_id >= static_cast<int>(filament_nozzle_map.size())) { return std::nullopt; }
|
||||||
|
|
||||||
|
int nozzle_id = filament_nozzle_map[filament_id];
|
||||||
|
return get_nozzle_from_id(nozzle_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<NozzleInfo> LayeredNozzleGroupResult::get_nozzles_for_filament(int filament_id) const
|
||||||
|
{
|
||||||
|
std::set<int> nozzle_ids;
|
||||||
|
|
||||||
|
if (!support_dynamic_nozzle_map) {
|
||||||
|
if (filament_id >= 0 && filament_id < static_cast<int>(_default_filament_nozzle_map.size())) {
|
||||||
|
nozzle_ids.insert(_default_filament_nozzle_map[filament_id]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
int start_layer = 0;
|
||||||
|
int end_layer = static_cast<int>(_layer_filament_nozzle_maps.size());
|
||||||
|
|
||||||
|
for (int i = start_layer; i < end_layer; ++i) {
|
||||||
|
const auto &map = _layer_filament_nozzle_maps[i];
|
||||||
|
if (filament_id >= 0 && filament_id < static_cast<int>(map.size())) {
|
||||||
|
nozzle_ids.insert(map[filament_id]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<NozzleInfo> result;
|
||||||
|
for (int id : nozzle_ids) {
|
||||||
|
if (id >= 0 && id < static_cast<int>(_nozzle_list.size())) { result.push_back(_nozzle_list[id]); }
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<NozzleInfo> LayeredNozzleGroupResult::get_first_nozzle_for_filament(int filament_id) const
|
||||||
|
{
|
||||||
|
if (filament_id < 0) return std::nullopt;
|
||||||
|
|
||||||
|
if (!support_dynamic_nozzle_map) {
|
||||||
|
if (filament_id >= static_cast<int>(_default_filament_nozzle_map.size())) return std::nullopt;
|
||||||
|
return get_nozzle_from_id(_default_filament_nozzle_map[filament_id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (size_t layer = 0; layer < _layer_filament_nozzle_maps.size(); ++layer) {
|
||||||
|
auto layer_used_filaments = get_used_filaments(layer);
|
||||||
|
if (std::find(layer_used_filaments.begin(), layer_used_filaments.end(), static_cast<unsigned int>(filament_id)) == layer_used_filaments.end()){
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const auto &map = _layer_filament_nozzle_maps[layer];
|
||||||
|
if (filament_id >= 0 && filament_id < static_cast<int>(map.size())) {
|
||||||
|
int nozzle_id = map[filament_id];
|
||||||
|
auto nozzle = get_nozzle_from_id(nozzle_id);
|
||||||
|
if (nozzle) return nozzle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<NozzleInfo> LayeredNozzleGroupResult::get_nozzle_from_id(int nozzle_id) const
|
||||||
|
{
|
||||||
|
if (nozzle_id < 0 || nozzle_id >= static_cast<int>(_nozzle_list.size())) { return std::nullopt; }
|
||||||
|
return _nozzle_list[nozzle_id];
|
||||||
|
}
|
||||||
|
|
||||||
|
int LayeredNozzleGroupResult::get_extruder_id(int filament_id, int layer_id) const
|
||||||
|
{
|
||||||
|
auto nozzle_info = get_nozzle_for_filament(filament_id, layer_id);
|
||||||
|
return nozzle_info ? nozzle_info->extruder_id : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int LayeredNozzleGroupResult::get_nozzle_id(int filament_id, int layer_id) const
|
||||||
|
{
|
||||||
|
auto nozzle_info = get_nozzle_for_filament(filament_id, layer_id);
|
||||||
|
return nozzle_info ? nozzle_info->group_id : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<int> &LayeredNozzleGroupResult::get_layer_filament_nozzle_map(int layer_id) const
|
||||||
|
{
|
||||||
|
if (layer_id >= 0 && layer_id < static_cast<int>(_layer_filament_nozzle_maps.size())) { return _layer_filament_nozzle_maps[layer_id]; }
|
||||||
|
return _default_filament_nozzle_map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== filament-change-time model ====================
|
||||||
|
FilamentChangeSimResult simulate_filament_change_time(
|
||||||
|
const std::vector<int>& logical_filaments,
|
||||||
|
const std::vector<NozzleInfo>& nozzle_list,
|
||||||
|
const std::vector<int>& filament_change_seq,
|
||||||
|
const std::vector<int>& nozzle_change_seq,
|
||||||
|
const std::vector<int>& group_of_filament,
|
||||||
|
const FilamentChangeTimeParams& time_params,
|
||||||
|
const std::vector<bool>& ams_preload_enabled,
|
||||||
|
bool calc_sliced_time)
|
||||||
|
{
|
||||||
|
FilamentChangeSimResult result;
|
||||||
|
if (logical_filaments.empty() || nozzle_list.empty() || filament_change_seq.empty() || nozzle_change_seq.empty())
|
||||||
|
return result;
|
||||||
|
|
||||||
|
// Re-map the parameter semantics:
|
||||||
|
// standard = AMS -> selector -> extruder (full path), selector = selector -> extruder (short path)
|
||||||
|
// so AMS -> selector = standard - selector
|
||||||
|
const float load_ams_to_selector = time_params.standard_load_time - time_params.selector_load_time;
|
||||||
|
const float unload_ams_to_selector = time_params.standard_unload_time - time_params.selector_unload_time;
|
||||||
|
const float load_selector_to_ext = time_params.selector_load_time;
|
||||||
|
const float unload_ext_to_selector = time_params.selector_unload_time;
|
||||||
|
|
||||||
|
// nozzle_id -> extruder_id
|
||||||
|
std::unordered_map<int, int> nozzle_to_extruder;
|
||||||
|
nozzle_to_extruder.reserve(nozzle_list.size());
|
||||||
|
for (const auto& nozzle : nozzle_list)
|
||||||
|
nozzle_to_extruder[nozzle.group_id] = nozzle.extruder_id;
|
||||||
|
|
||||||
|
// filament_id -> AMS group
|
||||||
|
std::unordered_map<int, int> filament_to_group;
|
||||||
|
filament_to_group.reserve(logical_filaments.size());
|
||||||
|
for (size_t i = 0; i < logical_filaments.size(); ++i)
|
||||||
|
filament_to_group[logical_filaments[i]] = group_of_filament[i];
|
||||||
|
|
||||||
|
const auto get_group = [&](int filament_id) -> int {
|
||||||
|
auto it = filament_to_group.find(filament_id);
|
||||||
|
return it != filament_to_group.end() ? it->second : -1;
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto is_preload_enabled = [&](int group_id) -> bool {
|
||||||
|
if (group_id < 0 || group_id >= static_cast<int>(ams_preload_enabled.size()))
|
||||||
|
return false;
|
||||||
|
return ams_preload_enabled[group_id];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Filament location states
|
||||||
|
enum class Location { IN_AMS, IN_SELECTOR, IN_EXTRUDER };
|
||||||
|
std::unordered_map<int, Location> filament_location; // filament_id -> current location
|
||||||
|
std::unordered_map<int, int> filament_extruder; // filament_id -> extruder it sits in (only valid when IN_EXTRUDER)
|
||||||
|
std::unordered_map<int, int> extruder_filament; // extruder_id -> currently loaded filament
|
||||||
|
// group_id -> filaments currently occupying that AMS channel (IN_SELECTOR or IN_EXTRUDER)
|
||||||
|
std::unordered_map<int, std::unordered_set<int>> ams_group_occupied;
|
||||||
|
|
||||||
|
filament_location.reserve(logical_filaments.size());
|
||||||
|
filament_extruder.reserve(logical_filaments.size());
|
||||||
|
|
||||||
|
// Initial state: every filament is in the AMS, every extruder is empty
|
||||||
|
for (int f : logical_filaments)
|
||||||
|
filament_location[f] = Location::IN_AMS;
|
||||||
|
|
||||||
|
// Slicer-estimate simulator: use NozzleStatusRecorder to track what each nozzle/extruder holds during slicing
|
||||||
|
NozzleStatusRecorder sliced_recorder;
|
||||||
|
|
||||||
|
const size_t seq_len = std::min(filament_change_seq.size(), nozzle_change_seq.size());
|
||||||
|
double actual_time = 0.0;
|
||||||
|
double sliced_time = 0.0;
|
||||||
|
|
||||||
|
for (size_t i = 0; i < seq_len; ++i) {
|
||||||
|
int B = filament_change_seq[i];
|
||||||
|
int nozzle_id = nozzle_change_seq[i];
|
||||||
|
|
||||||
|
auto nozzle_iter = nozzle_to_extruder.find(nozzle_id);
|
||||||
|
if (nozzle_iter == nozzle_to_extruder.end()) continue;
|
||||||
|
|
||||||
|
int E = nozzle_iter->second; // target extruder
|
||||||
|
|
||||||
|
// Step 0: compute the slicer-estimated time
|
||||||
|
// Slicer estimate: simulate the slicer's view (no selector awareness);
|
||||||
|
// count a load/unload when nozzle_in_extruder_change || filament_in_nozzle_change
|
||||||
|
if (calc_sliced_time) {
|
||||||
|
int old_nozzle_in_E = sliced_recorder.get_nozzle_in_extruder(E);
|
||||||
|
int old_filament_in_nozzle = sliced_recorder.get_filament_in_nozzle(nozzle_id);
|
||||||
|
int old_filament_in_ext = sliced_recorder.get_filament_in_nozzle(old_nozzle_in_E);
|
||||||
|
|
||||||
|
bool nozzle_change = (old_nozzle_in_E != nozzle_id);
|
||||||
|
bool filament_change = (old_filament_in_nozzle != B);
|
||||||
|
|
||||||
|
if (nozzle_change || filament_change) {
|
||||||
|
if (old_filament_in_ext != -1)
|
||||||
|
sliced_time += time_params.standard_unload_time;
|
||||||
|
sliced_time += time_params.standard_load_time;
|
||||||
|
}
|
||||||
|
sliced_recorder.set_nozzle_status(nozzle_id, B, E);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 1: find the filament A currently loaded in the target extruder E
|
||||||
|
int A = -1;
|
||||||
|
{
|
||||||
|
auto it = extruder_filament.find(E);
|
||||||
|
if (it != extruder_filament.end())
|
||||||
|
A = it->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
int group_B = get_group(B);
|
||||||
|
int group_A = (A != -1) ? get_group(A) : -1;
|
||||||
|
|
||||||
|
// Step 2: clear B's AMS-channel occupancy
|
||||||
|
auto group_it = ams_group_occupied.find(group_B);
|
||||||
|
if (group_it != ams_group_occupied.end()) {
|
||||||
|
for (int X : group_it->second) {
|
||||||
|
if (X == B) continue;
|
||||||
|
// X shares B's AMS channel, retreat it to the AMS to make way
|
||||||
|
Location loc_X = filament_location[X];
|
||||||
|
if (loc_X == Location::IN_EXTRUDER) {
|
||||||
|
actual_time += unload_ext_to_selector + unload_ams_to_selector;
|
||||||
|
int E2 = filament_extruder[X];
|
||||||
|
extruder_filament.erase(E2);
|
||||||
|
filament_extruder.erase(X);
|
||||||
|
} else if (loc_X == Location::IN_SELECTOR) {
|
||||||
|
actual_time += unload_ams_to_selector;
|
||||||
|
}
|
||||||
|
filament_location[X] = Location::IN_AMS;
|
||||||
|
}
|
||||||
|
group_it->second.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3: A exits E (while A is still in the extruder)
|
||||||
|
// Step 3.5: pre-load B (in parallel with Step 3)
|
||||||
|
// actual time = max(Step 3, Step 3.5)
|
||||||
|
bool step3_executed = false;
|
||||||
|
float step3_time = 0.0f;
|
||||||
|
if (A != -1 && A != B && filament_location[A] == Location::IN_EXTRUDER) {
|
||||||
|
if (is_preload_enabled(group_A) && group_A != group_B) {
|
||||||
|
step3_time = unload_ext_to_selector;
|
||||||
|
filament_location[A] = Location::IN_SELECTOR;
|
||||||
|
} else {
|
||||||
|
step3_time = unload_ext_to_selector + unload_ams_to_selector;
|
||||||
|
filament_location[A] = Location::IN_AMS;
|
||||||
|
ams_group_occupied[group_A].erase(A);
|
||||||
|
}
|
||||||
|
extruder_filament.erase(E);
|
||||||
|
filament_extruder.erase(A);
|
||||||
|
step3_executed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
float step3_5_time = 0.0f;
|
||||||
|
if (step3_executed &&
|
||||||
|
filament_location[B] == Location::IN_AMS &&
|
||||||
|
group_A != group_B &&
|
||||||
|
is_preload_enabled(group_B)) {
|
||||||
|
step3_5_time = load_ams_to_selector;
|
||||||
|
filament_location[B] = Location::IN_SELECTOR;
|
||||||
|
ams_group_occupied[group_B].insert(B);
|
||||||
|
}
|
||||||
|
|
||||||
|
actual_time += std::max(step3_time, step3_5_time);
|
||||||
|
|
||||||
|
// Step 4: push B into E
|
||||||
|
// Step 6: pre-load the next filament C (in parallel with Step 4)
|
||||||
|
// actual time = max(Step 4, Step 6)
|
||||||
|
float step4_time = 0.0f;
|
||||||
|
Location loc_B = filament_location[B];
|
||||||
|
if (loc_B == Location::IN_AMS) {
|
||||||
|
step4_time = load_ams_to_selector + load_selector_to_ext;
|
||||||
|
} else if (loc_B == Location::IN_SELECTOR) {
|
||||||
|
step4_time = load_selector_to_ext;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 5: update state
|
||||||
|
extruder_filament[E] = B;
|
||||||
|
filament_location[B] = Location::IN_EXTRUDER;
|
||||||
|
filament_extruder[B] = E;
|
||||||
|
ams_group_occupied[group_B].insert(B);
|
||||||
|
|
||||||
|
float step6_time = 0.0f;
|
||||||
|
if (i + 1 < seq_len) {
|
||||||
|
int C = filament_change_seq[i + 1];
|
||||||
|
int group_C = get_group(C);
|
||||||
|
if (filament_location[C] == Location::IN_AMS &&
|
||||||
|
group_C != group_B &&
|
||||||
|
is_preload_enabled(group_C) &&
|
||||||
|
ams_group_occupied[group_C].empty()) {
|
||||||
|
step6_time = load_ams_to_selector;
|
||||||
|
filament_location[C] = Location::IN_SELECTOR;
|
||||||
|
ams_group_occupied[group_C].insert(C);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
actual_time += std::max(step4_time, step6_time);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.actual_time = actual_time;
|
||||||
|
result.sliced_time = sliced_time;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== NozzleStatusRecorder implementation ====================
|
||||||
|
|
||||||
|
bool NozzleStatusRecorder::is_nozzle_empty(int nozzle_id) const
|
||||||
|
{
|
||||||
|
auto iter = nozzle_filament_status.find(nozzle_id);
|
||||||
|
if (iter == nozzle_filament_status.end()) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int NozzleStatusRecorder::get_filament_in_nozzle(int nozzle_id) const
|
||||||
|
{
|
||||||
|
auto iter = nozzle_filament_status.find(nozzle_id);
|
||||||
|
if (iter == nozzle_filament_status.end()) return -1;
|
||||||
|
return iter->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
int NozzleStatusRecorder::get_nozzle_in_extruder(int extruder_id) const
|
||||||
|
{
|
||||||
|
auto iter = extruder_nozzle_status.find(extruder_id);
|
||||||
|
if (iter == extruder_nozzle_status.end()) return -1;
|
||||||
|
return iter->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
void NozzleStatusRecorder::set_nozzle_status(int nozzle_id, int filament_id, int extruder_id)
|
||||||
|
{
|
||||||
|
nozzle_filament_status[nozzle_id] = filament_id;
|
||||||
|
if (extruder_id != -1) {
|
||||||
|
extruder_nozzle_status[extruder_id] = nozzle_id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void NozzleStatusRecorder::clear_nozzle_status(int nozzle_id)
|
||||||
|
{
|
||||||
|
auto iter = nozzle_filament_status.find(nozzle_id);
|
||||||
|
if (iter == nozzle_filament_status.end()) return;
|
||||||
|
nozzle_filament_status.erase(iter);
|
||||||
|
}
|
||||||
|
|
||||||
|
int LayeredNozzleGroupResult::estimate_seq_flush_weight(const std::vector<std::vector<std::vector<float>>>& flush_matrix, const std::vector<int>& filament_change_seq) const
|
||||||
|
{
|
||||||
|
auto get_weight_from_volume = [](float volume){
|
||||||
|
return static_cast<int>(volume * 1.26 * 0.01);
|
||||||
|
};
|
||||||
|
|
||||||
|
float total_flush_volume = 0;
|
||||||
|
NozzleStatusRecorder recorder;
|
||||||
|
for(auto filament: filament_change_seq){
|
||||||
|
auto nozzle = get_nozzle_for_filament(filament, -1);
|
||||||
|
if(!nozzle)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
int extruder_id = nozzle->extruder_id;
|
||||||
|
int nozzle_id = nozzle->group_id;
|
||||||
|
int last_filament = recorder.get_filament_in_nozzle(nozzle_id);
|
||||||
|
|
||||||
|
if(last_filament!= -1 && last_filament != filament){
|
||||||
|
// bounds check to avoid out-of-range access
|
||||||
|
if (extruder_id >= 0 && extruder_id < static_cast<int>(flush_matrix.size()) &&
|
||||||
|
last_filament >= 0 && last_filament < static_cast<int>(flush_matrix[extruder_id].size()) &&
|
||||||
|
filament >= 0 && filament < static_cast<int>(flush_matrix[extruder_id][last_filament].size())) {
|
||||||
|
float flush_volume = flush_matrix[extruder_id][last_filament][filament];
|
||||||
|
total_flush_volume += flush_volume;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
recorder.set_nozzle_status(nozzle_id, filament);
|
||||||
|
}
|
||||||
|
|
||||||
|
return get_weight_from_volume(total_flush_volume);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== StaticNozzleGroupResult ====================
|
||||||
|
|
||||||
|
std::optional<StaticNozzleGroupResult> StaticNozzleGroupResult::create(
|
||||||
|
const std::vector<FilamentInfo>& filaments_info,
|
||||||
|
const std::vector<NozzleInfo>& nozzles_info,
|
||||||
|
const std::vector<int>& filament_change_seq,
|
||||||
|
const std::vector<int>& nozzle_change_seq,
|
||||||
|
bool support_dynamic_nozzle_map)
|
||||||
|
{
|
||||||
|
if (filaments_info.empty() || nozzles_info.empty()) return std::nullopt;
|
||||||
|
|
||||||
|
std::map<int, NozzleInfo> nozzle_list_map;
|
||||||
|
std::map<int, std::set<int>> filament_to_nozzles;
|
||||||
|
|
||||||
|
for (auto nozzle_info : nozzles_info)
|
||||||
|
nozzle_list_map[nozzle_info.group_id] = nozzle_info;
|
||||||
|
|
||||||
|
for (auto filament_info : filaments_info) {
|
||||||
|
auto fil_id = filament_info.id;
|
||||||
|
auto nozzles_id = filament_info.group_id;
|
||||||
|
std::set<int> nozzles_set(nozzles_id.begin(), nozzles_id.end());
|
||||||
|
// Backward compat with older (single-nozzle) gcode.3mf: filament has no group_id, avoid an empty map.
|
||||||
|
if (nozzles_set.empty()) {
|
||||||
|
for (const auto& nozzle_entry : nozzle_list_map)
|
||||||
|
nozzles_set.insert(nozzle_entry.first);
|
||||||
|
}
|
||||||
|
filament_to_nozzles[fil_id] = nozzles_set;
|
||||||
|
}
|
||||||
|
|
||||||
|
StaticNozzleGroupResult result(support_dynamic_nozzle_map);
|
||||||
|
result._filament_to_nozzles = filament_to_nozzles;
|
||||||
|
result._nozzle_list_map = nozzle_list_map;
|
||||||
|
result._filament_change_seq = filament_change_seq;
|
||||||
|
result._nozzle_change_seq = nozzle_change_seq;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<NozzleInfo> StaticNozzleGroupResult::get_nozzle_from_id(int nozzle_id) const
|
||||||
|
{
|
||||||
|
auto iter = _nozzle_list_map.find(nozzle_id);
|
||||||
|
if (iter == _nozzle_list_map.end()) { return std::nullopt; }
|
||||||
|
return iter->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
int StaticNozzleGroupResult::get_extruder_count() const
|
||||||
|
{
|
||||||
|
std::set<int> extruder_ids;
|
||||||
|
for (const auto &elem : _nozzle_list_map) { extruder_ids.insert(elem.second.extruder_id); }
|
||||||
|
return static_cast<int>(extruder_ids.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<NozzleInfo> StaticNozzleGroupResult::get_used_nozzles_in_extruder(int target_extruder_id) const
|
||||||
|
{
|
||||||
|
std::vector<NozzleInfo> result;
|
||||||
|
for (const auto &elem : _nozzle_list_map) {
|
||||||
|
const auto &nozzle = elem.second;
|
||||||
|
if (target_extruder_id == -1 || nozzle.extruder_id == target_extruder_id) {
|
||||||
|
result.push_back(nozzle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<int> StaticNozzleGroupResult::get_used_extruders() const
|
||||||
|
{
|
||||||
|
std::set<int> used_extruders;
|
||||||
|
for (const auto &elem : _nozzle_list_map) { used_extruders.insert(elem.second.extruder_id); }
|
||||||
|
return std::vector<int>(used_extruders.begin(), used_extruders.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<unsigned int> StaticNozzleGroupResult::get_used_filaments() const
|
||||||
|
{
|
||||||
|
std::vector<unsigned int> used_filaments;
|
||||||
|
used_filaments.reserve(_filament_to_nozzles.size());
|
||||||
|
for (const auto &elem : _filament_to_nozzles) {
|
||||||
|
if (elem.first >= 0) {
|
||||||
|
used_filaments.push_back(static_cast<unsigned int>(elem.first));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return used_filaments;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<NozzleInfo> StaticNozzleGroupResult::get_nozzles_for_filament(int filament_id) const
|
||||||
|
{
|
||||||
|
auto iter = _filament_to_nozzles.find(filament_id);
|
||||||
|
if (iter == _filament_to_nozzles.end()) { return std::vector<NozzleInfo>(); }
|
||||||
|
|
||||||
|
std::vector<NozzleInfo> result;
|
||||||
|
for (int nozzle_id : iter->second) {
|
||||||
|
auto nozzle_iter = _nozzle_list_map.find(nozzle_id);
|
||||||
|
if (nozzle_iter != _nozzle_list_map.end()) {
|
||||||
|
result.push_back(nozzle_iter->second);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<NozzleInfo> StaticNozzleGroupResult::get_first_nozzle_for_filament(int filament_id) const
|
||||||
|
{
|
||||||
|
if (filament_id < 0) return std::nullopt;
|
||||||
|
|
||||||
|
if (!_filament_change_seq.empty() && _filament_change_seq.size() == _nozzle_change_seq.size()) {
|
||||||
|
for (size_t idx = 0; idx < _filament_change_seq.size(); ++idx) {
|
||||||
|
if (_filament_change_seq[idx] == filament_id) {
|
||||||
|
int nozzle_id = _nozzle_change_seq[idx];
|
||||||
|
auto nozzle = get_nozzle_from_id(nozzle_id);
|
||||||
|
if (nozzle) return nozzle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto iter = _filament_to_nozzles.find(filament_id);
|
||||||
|
if (iter == _filament_to_nozzles.end()) return std::nullopt;
|
||||||
|
|
||||||
|
for (int nozzle_id : iter->second) {
|
||||||
|
auto nozzle = get_nozzle_from_id(nozzle_id);
|
||||||
|
if (nozzle) return nozzle;
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== serialization ====================
|
||||||
|
|
||||||
|
std::string NozzleInfo::serialize() const
|
||||||
|
{
|
||||||
|
std::ostringstream oss;
|
||||||
|
oss << "id=\"" << group_id << "\" "
|
||||||
|
<< "extruder_id=\"" << extruder_id + 1 << "\" "
|
||||||
|
<< "nozzle_diameter=\"" << diameter << "\" "
|
||||||
|
<< "volume_type=\"" << get_nozzle_volume_type_string(volume_type) << "\"";
|
||||||
|
return oss.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string NozzleGroupInfo::serialize() const
|
||||||
|
{
|
||||||
|
std::ostringstream oss;
|
||||||
|
oss << extruder_id << "-"
|
||||||
|
<< std::setprecision(2) << diameter << "-"
|
||||||
|
<< get_nozzle_volume_type_string(volume_type) << "-"
|
||||||
|
<< nozzle_count;
|
||||||
|
return oss.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<NozzleGroupInfo> NozzleGroupInfo::deserialize(const std::string &str)
|
||||||
|
{
|
||||||
|
std::istringstream iss(str);
|
||||||
|
std::string token;
|
||||||
|
std::vector<std::string> tokens;
|
||||||
|
|
||||||
|
while (std::getline(iss, token, '-')) { tokens.push_back(token); }
|
||||||
|
|
||||||
|
if (tokens.size() != 4) { return std::nullopt; }
|
||||||
|
|
||||||
|
try {
|
||||||
|
int extruder_id = std::stoi(tokens[0]);
|
||||||
|
std::string diameter = tokens[1];
|
||||||
|
NozzleVolumeType volume_type = NozzleVolumeType(ConfigOptionEnum<NozzleVolumeType>::get_enum_values().at(tokens[2]));
|
||||||
|
int nozzle_count = std::stoi(tokens[3]);
|
||||||
|
|
||||||
|
return NozzleGroupInfo(diameter, volume_type, extruder_id, nozzle_count);
|
||||||
|
} catch (const std::exception &) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<NozzleInfo> load_nozzle_infos_with_compatibility(
|
||||||
|
const std::vector<NozzleInfo>& nozzle_infos,
|
||||||
|
const std::vector<FilamentInfo>& filament_infos,
|
||||||
|
const std::vector<int>& filament_map,
|
||||||
|
const std::vector<NozzleVolumeType>& extruder_volume_types,
|
||||||
|
const std::vector<double>& nozzle_diameter
|
||||||
|
)
|
||||||
|
{
|
||||||
|
bool has_nozzle_info = !nozzle_infos.empty();
|
||||||
|
bool has_valid_filament_info = !filament_infos.empty() && std::all_of(filament_infos.begin(), filament_infos.end(), [](const FilamentInfo& info){
|
||||||
|
return info.group_id.size() == 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
if(!has_nozzle_info && !has_valid_filament_info){
|
||||||
|
BOOST_LOG_TRIVIAL(warning)<<__FUNCTION__ << ": building nozzle list from filament map and volume types";
|
||||||
|
|
||||||
|
// Backward compatibility for older gcode.3mf:
|
||||||
|
// - nozzle_diameter is always present and its size defines extruder count.
|
||||||
|
// - filament_map may be missing; treat it as [0, 0, ...] for each extruder.
|
||||||
|
// - extruder_volume_types may be missing; treat it as all Standard.
|
||||||
|
const size_t extruder_count = nozzle_diameter.size();
|
||||||
|
|
||||||
|
std::vector<NozzleVolumeType> volume_types_fixed = extruder_volume_types;
|
||||||
|
volume_types_fixed.resize(extruder_count, NozzleVolumeType::nvtStandard);
|
||||||
|
|
||||||
|
std::vector<NozzleInfo> result;
|
||||||
|
result.reserve(extruder_count);
|
||||||
|
for (size_t extruder_id = 0; extruder_id < extruder_count; ++extruder_id) {
|
||||||
|
NozzleInfo info;
|
||||||
|
info.diameter = format_diameter_to_str(nozzle_diameter[extruder_id]);
|
||||||
|
info.group_id = static_cast<int>(extruder_id);
|
||||||
|
info.extruder_id = static_cast<int>(extruder_id);
|
||||||
|
info.volume_type = volume_types_fixed[extruder_id];
|
||||||
|
result.emplace_back(std::move(info));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!has_nozzle_info){
|
||||||
|
BOOST_LOG_TRIVIAL(info)<<__FUNCTION__ << ": building nozzle list from filament info";
|
||||||
|
std::map<int, NozzleInfo> nozzle_map; // group_id -> NozzleInfo
|
||||||
|
for(auto& filament : filament_infos){
|
||||||
|
int group_id = filament.group_id.front();
|
||||||
|
if(group_id < 0 || nozzle_map.find(group_id) != nozzle_map.end()){
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto volume_type_str_to_enum = ConfigOptionEnum<NozzleVolumeType>::get_enum_values();
|
||||||
|
|
||||||
|
NozzleInfo info;
|
||||||
|
info.diameter = format_diameter_to_str(filament.nozzle_diameter);
|
||||||
|
info.group_id = group_id;
|
||||||
|
// Orca: bounds-check filament_map[filament.id] so a malformed 3mf (filament id
|
||||||
|
// beyond the map) degrades to extruder 0 instead of dereferencing out of range.
|
||||||
|
info.extruder_id = (filament.id >= 0 && filament.id < static_cast<int>(filament_map.size()))
|
||||||
|
? filament_map[filament.id] - 1
|
||||||
|
: 0; // to 0-based
|
||||||
|
|
||||||
|
if (volume_type_str_to_enum.count(filament.nozzle_volume_type))
|
||||||
|
info.volume_type = NozzleVolumeType(volume_type_str_to_enum.at(filament.nozzle_volume_type));
|
||||||
|
else {
|
||||||
|
info.volume_type = NozzleVolumeType::nvtStandard;
|
||||||
|
}
|
||||||
|
|
||||||
|
nozzle_map[group_id] = std::move(info);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<NozzleInfo> ret;
|
||||||
|
for(auto& elem : nozzle_map){
|
||||||
|
ret.emplace_back(elem.second);
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto result = nozzle_infos;
|
||||||
|
std::sort(result.begin(), result.end());
|
||||||
|
BOOST_LOG_TRIVIAL(info)<<__FUNCTION__ << ": using new 3mf format with " << result.size() << " nozzle infos.";
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
}} // namespace Slic3r::MultiNozzleUtils
|
||||||
@@ -0,0 +1,288 @@
|
|||||||
|
#ifndef MULTI_NOZZLE_UTILS_HPP
|
||||||
|
#define MULTI_NOZZLE_UTILS_HPP
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
#include <map>
|
||||||
|
#include <optional>
|
||||||
|
#include <set>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include "PrintConfig.hpp"
|
||||||
|
|
||||||
|
// Multi-nozzle support types.
|
||||||
|
// Declares the filament-grouping result types the slicing pipeline needs, plus the analytic
|
||||||
|
// filament-change-time model (FilamentChangeTimeParams, NozzleStatusRecorder,
|
||||||
|
// FilamentChangeSimResult, simulate_filament_change_time) — self-contained analytic code that
|
||||||
|
// never touches the time estimator; its first consumer is the filament_group golden harness.
|
||||||
|
// The gcode.3mf serialization surface lives here too: NozzleInfo/NozzleGroupInfo
|
||||||
|
// serialize+deserialize, the device-side StaticNozzleGroupResult,
|
||||||
|
// load_nozzle_infos_with_compatibility (the backward-compat 3mf reader) and
|
||||||
|
// LayeredNozzleGroupResult::estimate_seq_flush_weight. The change-time-tuning helpers
|
||||||
|
// calc_filament_change_gap_for_assignment / find_optimal_physical_assignment (used only by the
|
||||||
|
// AMS pre-load optimizer, a later feature) are not implemented here.
|
||||||
|
|
||||||
|
namespace Slic3r {
|
||||||
|
struct FilamentInfo; // Slic3r::FilamentInfo (ProjectTask.hpp) — consumed by StaticNozzleGroupResult / the 3mf reader
|
||||||
|
namespace MultiNozzleUtils {
|
||||||
|
|
||||||
|
// Information about a single logical nozzle.
|
||||||
|
struct NozzleInfo
|
||||||
|
{
|
||||||
|
std::string diameter;
|
||||||
|
NozzleVolumeType volume_type;
|
||||||
|
int extruder_id{-1}; // logical extruder id
|
||||||
|
int group_id{-1}; // logical nozzle id
|
||||||
|
|
||||||
|
std::string serialize() const;
|
||||||
|
|
||||||
|
bool operator<(const NozzleInfo& other) const {
|
||||||
|
if(group_id != other.group_id) return group_id < other.group_id;
|
||||||
|
if(extruder_id != other.extruder_id) return extruder_id < other.extruder_id;
|
||||||
|
if(volume_type != other.volume_type) return volume_type < other.volume_type;
|
||||||
|
return diameter < other.diameter;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// A group of identical nozzles on one extruder (diameter + volume type + count).
|
||||||
|
struct NozzleGroupInfo
|
||||||
|
{
|
||||||
|
std::string diameter;
|
||||||
|
NozzleVolumeType volume_type;
|
||||||
|
int extruder_id;
|
||||||
|
int nozzle_count;
|
||||||
|
|
||||||
|
NozzleGroupInfo() = default;
|
||||||
|
|
||||||
|
NozzleGroupInfo(const std::string& nozzle_diameter_, const NozzleVolumeType volume_type_, const int extruder_id_, const int nozzle_count_)
|
||||||
|
: diameter(nozzle_diameter_), volume_type(volume_type_), extruder_id(extruder_id_), nozzle_count(nozzle_count_)
|
||||||
|
{}
|
||||||
|
|
||||||
|
inline bool operator<(const NozzleGroupInfo &rhs) const
|
||||||
|
{
|
||||||
|
if (extruder_id != rhs.extruder_id) return extruder_id < rhs.extruder_id;
|
||||||
|
if (diameter != rhs.diameter) return diameter < rhs.diameter;
|
||||||
|
if (volume_type != rhs.volume_type) return volume_type < rhs.volume_type;
|
||||||
|
return nozzle_count < rhs.nozzle_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool is_same_type(const NozzleGroupInfo &rhs) const
|
||||||
|
{
|
||||||
|
return diameter == rhs.diameter && volume_type == rhs.volume_type && extruder_id == rhs.extruder_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool operator==(const NozzleGroupInfo &rhs) const
|
||||||
|
{
|
||||||
|
return diameter == rhs.diameter && volume_type == rhs.volume_type && extruder_id == rhs.extruder_id && nozzle_count == rhs.nozzle_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string serialize() const;
|
||||||
|
static std::optional<NozzleGroupInfo> deserialize(const std::string& str);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load/unload time constants used by the filament-change-time model.
|
||||||
|
// Consumed by simulate_filament_change_time() below and carried by the grouping-context
|
||||||
|
// substrate (FilamentGroupContext::SpeedInfo).
|
||||||
|
struct FilamentChangeTimeParams
|
||||||
|
{
|
||||||
|
float selector_load_time{0.0f};
|
||||||
|
float selector_unload_time{0.0f};
|
||||||
|
float standard_load_time{0.0f};
|
||||||
|
float standard_unload_time{0.0f};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Abstract base for a nozzle-grouping result.
|
||||||
|
*/
|
||||||
|
class NozzleGroupResultBase
|
||||||
|
{
|
||||||
|
protected:
|
||||||
|
bool support_dynamic_nozzle_map{false}; // whether dynamic (selector) mapping is used
|
||||||
|
|
||||||
|
public:
|
||||||
|
NozzleGroupResultBase(bool support_dynamic_map = false) : support_dynamic_nozzle_map(support_dynamic_map) {}
|
||||||
|
virtual ~NozzleGroupResultBase() = default;
|
||||||
|
|
||||||
|
virtual std::optional<NozzleInfo> get_nozzle_from_id(int nozzle_id) const = 0;
|
||||||
|
virtual std::optional<NozzleInfo> get_first_nozzle_for_filament(int filament_id) const = 0; // logical nozzle a filament first uses
|
||||||
|
|
||||||
|
virtual std::vector<NozzleInfo> get_nozzles_for_filament(int filament_id) const = 0; // every nozzle a filament may use (across all layers)
|
||||||
|
|
||||||
|
bool is_support_dynamic_nozzle_map() const { return support_dynamic_nozzle_map; }
|
||||||
|
|
||||||
|
virtual int get_extruder_count() const = 0;
|
||||||
|
|
||||||
|
virtual std::vector<NozzleInfo> get_used_nozzles_in_extruder(int extruder_id =-1) const = 0;
|
||||||
|
virtual std::vector<int> get_used_extruders() const = 0;
|
||||||
|
virtual std::vector<unsigned int> get_used_filaments() const = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Layer-aware nozzle-grouping result.
|
||||||
|
* Used by the back-end slicing code; supports per-layer nozzle mapping.
|
||||||
|
*/
|
||||||
|
class LayeredNozzleGroupResult : public NozzleGroupResultBase
|
||||||
|
{
|
||||||
|
private:
|
||||||
|
std::vector<std::vector<int>> _layer_filament_nozzle_maps; // per-layer filament -> nozzle map
|
||||||
|
std::vector<std::vector<unsigned int>> _layer_filament_sequences; // per-layer filament print order
|
||||||
|
std::vector<int> _default_filament_nozzle_map; // global filament -> nozzle map
|
||||||
|
std::vector<unsigned int> _used_filaments; // all used filament indices
|
||||||
|
std::vector<NozzleInfo> _nozzle_list; // global nozzle list
|
||||||
|
|
||||||
|
public:
|
||||||
|
LayeredNozzleGroupResult(bool support_dynamic_map = false) : NozzleGroupResultBase(support_dynamic_map) {}
|
||||||
|
|
||||||
|
// No selector: one global filament->nozzle map.
|
||||||
|
static std::optional<LayeredNozzleGroupResult> create(
|
||||||
|
const std::vector<int>& filament_nozzle_map,
|
||||||
|
const std::vector<NozzleInfo>& nozzle_list,
|
||||||
|
const std::vector<unsigned int>& used_filaments);
|
||||||
|
|
||||||
|
// Selector: built from per-layer maps (each layer may differ).
|
||||||
|
static std::optional<LayeredNozzleGroupResult> create(
|
||||||
|
const std::vector<std::vector<int>>& layer_filament_nozzle_maps,
|
||||||
|
const std::vector<NozzleInfo>& nozzle_list,
|
||||||
|
const std::vector<unsigned int>& used_filaments,
|
||||||
|
const std::vector<std::vector<unsigned int>>& layer_filament_sequences);
|
||||||
|
|
||||||
|
// Multi-nozzle without selector: resolve each requested logical nozzle to a physical nozzle.
|
||||||
|
static std::optional<LayeredNozzleGroupResult> create(
|
||||||
|
const std::vector<unsigned int>& used_filaments,
|
||||||
|
const std::vector<int>& filament_map,
|
||||||
|
const std::vector<int>& filament_volume_map,
|
||||||
|
const std::vector<int>& filament_nozzle_map,
|
||||||
|
const std::vector<std::map<NozzleVolumeType, int>>& nozzle_count,
|
||||||
|
float diameter);
|
||||||
|
|
||||||
|
bool are_filaments_same_extruder(int filament_id1, int filament_id2, int layer_id = -1) const;
|
||||||
|
bool are_filaments_same_nozzle(int filament_id1, int filament_id2, int layer_id = -1) const;
|
||||||
|
int get_extruder_count() const override;
|
||||||
|
|
||||||
|
std::vector<NozzleInfo> get_used_nozzles_in_extruder(int target_extruder_id = -1) const override;
|
||||||
|
std::vector<NozzleInfo> get_used_nozzles_in_extruder(int target_extruder_id, int layer_id) const; // layer_id=-1 uses default map
|
||||||
|
std::vector<int> get_used_extruders() const override;
|
||||||
|
std::vector<int> get_used_extruders(int layer_id) const; // layer_id=-1 returns global extruders
|
||||||
|
|
||||||
|
std::vector<int> get_extruder_map(bool zero_based = true, int layer_id = -1) const;
|
||||||
|
std::vector<int> get_nozzle_map(int layer_id = -1) const;
|
||||||
|
std::vector<int> get_volume_map(int layer_id = -1) const;
|
||||||
|
|
||||||
|
std::vector<unsigned int> get_used_filaments() const override { return _used_filaments; }
|
||||||
|
std::vector<unsigned int> get_used_filaments(int layer_id) const;
|
||||||
|
|
||||||
|
std::optional<NozzleInfo> get_nozzle_for_filament(int filament_id, int layer_id = -1) const;
|
||||||
|
std::vector<NozzleInfo> get_nozzles_for_filament(int filament_id) const override;
|
||||||
|
|
||||||
|
std::optional<NozzleInfo> get_nozzle_from_id(int nozzle_id) const override;
|
||||||
|
std::optional<NozzleInfo> get_first_nozzle_for_filament(int filament_id) const override;
|
||||||
|
int get_extruder_id(int filament_id, int layer_id = -1) const;
|
||||||
|
int get_nozzle_id(int filament_id, int layer_id = -1) const;
|
||||||
|
|
||||||
|
size_t get_layer_count() const { return _layer_filament_nozzle_maps.size(); }
|
||||||
|
const std::vector<int>& get_layer_filament_nozzle_map(int layer_id) const;
|
||||||
|
const std::vector<std::vector<int>> &get_layer_filament_nozzle_maps() const { return _layer_filament_nozzle_maps; }
|
||||||
|
const std::vector<std::vector<unsigned int>>& get_layer_filament_sequences() const { return _layer_filament_sequences; }
|
||||||
|
|
||||||
|
// Estimate the flush weight of a filament-change sequence given the per-extruder flush matrix
|
||||||
|
// (extruder -> from-filament -> to-filament).
|
||||||
|
int estimate_seq_flush_weight(const std::vector<std::vector<std::vector<float>>>& flush_matrix, const std::vector<int>& filament_change_seq) const;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Layer-less nozzle-grouping result for the device side (static nozzle mapping only).
|
||||||
|
* Reconstructed from a loaded gcode.3mf together with the filament/nozzle change sequences.
|
||||||
|
*/
|
||||||
|
class StaticNozzleGroupResult : public NozzleGroupResultBase
|
||||||
|
{
|
||||||
|
private:
|
||||||
|
std::map<int, std::set<int>> _filament_to_nozzles; // every nozzle a filament may map to
|
||||||
|
std::map<int, NozzleInfo> _nozzle_list_map; // used nozzles, keyed by logical nozzle id
|
||||||
|
std::vector<int> _filament_change_seq; // filament sequence used to resolve first-use
|
||||||
|
std::vector<int> _nozzle_change_seq; // logical-nozzle sequence paired with the filament sequence
|
||||||
|
|
||||||
|
public:
|
||||||
|
StaticNozzleGroupResult(bool support_dynamic_map) : NozzleGroupResultBase(support_dynamic_map) {}
|
||||||
|
// Build from a loaded 3mf, with the filament/nozzle change sequences.
|
||||||
|
static std::optional<StaticNozzleGroupResult> create(
|
||||||
|
const std::vector<FilamentInfo>& filaments_info,
|
||||||
|
const std::vector<NozzleInfo>& nozzles_info,
|
||||||
|
const std::vector<int>& filament_change_seq,
|
||||||
|
const std::vector<int>& nozzle_change_seq,
|
||||||
|
bool support_dynamic_map);
|
||||||
|
|
||||||
|
int get_extruder_count() const override;
|
||||||
|
std::vector<NozzleInfo> get_used_nozzles_in_extruder(int extruder_id = -1) const override;
|
||||||
|
std::vector<int> get_used_extruders() const override;
|
||||||
|
std::vector<unsigned int> get_used_filaments() const override;
|
||||||
|
|
||||||
|
std::optional<NozzleInfo> get_nozzle_from_id(int nozzle_id) const override;
|
||||||
|
|
||||||
|
std::vector<NozzleInfo> get_nozzles_for_filament(int filament_id) const override;
|
||||||
|
std::optional<NozzleInfo> get_first_nozzle_for_filament(int filament_id) const override;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Tracks, during the filament-change simulation, which filament sits in each physical nozzle
|
||||||
|
// and which nozzle each extruder currently carries.
|
||||||
|
class NozzleStatusRecorder
|
||||||
|
{
|
||||||
|
private:
|
||||||
|
std::unordered_map<int, int> nozzle_filament_status; // Track filament in each nozzle
|
||||||
|
std::unordered_map<int, int> extruder_nozzle_status; // Track nozzle for each extruder
|
||||||
|
int current_extruder_id_ = -1; // Track current extruder id
|
||||||
|
|
||||||
|
public:
|
||||||
|
NozzleStatusRecorder() = default;
|
||||||
|
bool is_nozzle_empty(int nozzle_id) const;
|
||||||
|
int get_filament_in_nozzle(int nozzle_id) const;
|
||||||
|
int get_nozzle_in_extruder(int extruder_id) const;
|
||||||
|
int get_current_extruder_id() const { return current_extruder_id_; }
|
||||||
|
|
||||||
|
void clear_nozzle_status(int nozzle_id);
|
||||||
|
void set_current_extruder_id(int extruder_id) { current_extruder_id_ = extruder_id; }
|
||||||
|
|
||||||
|
// Update the status of a nozzle with new filament and extruder information
|
||||||
|
void set_nozzle_status(int nozzle_id, int filament_id, int extruder_id = -1);
|
||||||
|
|
||||||
|
// key: nozzle id, value: filament id (-1 = the nozzle carries no filament)
|
||||||
|
const std::unordered_map<int, int>& get_nozzle_filament_map() const { return nozzle_filament_status; }
|
||||||
|
// key: extruder id, value: nozzle id (-1 = the extruder carries no nozzle)
|
||||||
|
const std::unordered_map<int, int>& get_extruder_nozzle_map() const { return extruder_nozzle_status; }
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FilamentChangeSimResult {
|
||||||
|
double actual_time = 0.0;
|
||||||
|
double sliced_time = 0.0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Analytic filament-change-time model. Given the used filaments, the nozzle
|
||||||
|
// list, the filament/nozzle change sequences, each filament's AMS group and the load/unload time
|
||||||
|
// constants, it simulates AMS->selector->extruder transport (with optional AMS pre-load overlap)
|
||||||
|
// and returns the actual print time plus the slicer-estimated time. Self-contained: it never
|
||||||
|
// touches the g-code time estimator.
|
||||||
|
FilamentChangeSimResult simulate_filament_change_time(
|
||||||
|
const std::vector<int>& logical_filaments,
|
||||||
|
const std::vector<NozzleInfo>& nozzle_list,
|
||||||
|
const std::vector<int>& filament_change_seq,
|
||||||
|
const std::vector<int>& nozzle_change_seq,
|
||||||
|
const std::vector<int>& group_of_filament,
|
||||||
|
const FilamentChangeTimeParams& time_params,
|
||||||
|
const std::vector<bool>& ams_preload_enabled = {},
|
||||||
|
bool calc_sliced_time = false);
|
||||||
|
|
||||||
|
// ==================== tool functions ====================
|
||||||
|
std::vector<NozzleInfo> build_nozzle_list(std::vector<NozzleGroupInfo> info);
|
||||||
|
std::vector<NozzleInfo> build_nozzle_list(double diameter, const std::vector<int>& filament_nozzle_map,
|
||||||
|
const std::vector<int>& filament_volume_map, const std::vector<int>& filament_map);
|
||||||
|
// Load nozzle infos from a gcode.3mf, handling backward compatibility with older 3mf that did not
|
||||||
|
// record standalone <nozzle> tags: falls back to the per-filament group_id/diameter/volume_type, and
|
||||||
|
// (for the oldest single-nozzle 3mf) to the filament_map + extruder volume types + nozzle diameters.
|
||||||
|
std::vector<NozzleInfo> load_nozzle_infos_with_compatibility(
|
||||||
|
const std::vector<NozzleInfo>& nozzle_infos,
|
||||||
|
const std::vector<FilamentInfo>& filament_infos,
|
||||||
|
const std::vector<int>& filament_map,
|
||||||
|
const std::vector<NozzleVolumeType>& extruder_volume_types,
|
||||||
|
const std::vector<double>& nozzle_diameter
|
||||||
|
);
|
||||||
|
} // namespace MultiNozzleUtils
|
||||||
|
} // namespace Slic3r
|
||||||
|
|
||||||
|
#endif // MULTI_NOZZLE_UTILS_HPP
|
||||||
@@ -252,7 +252,7 @@ void extend_default_config_length(DynamicPrintConfig& config, const bool set_nil
|
|||||||
auto replace_nil_and_resize = [&](const std::string & key, int length){
|
auto replace_nil_and_resize = [&](const std::string & key, int length){
|
||||||
ConfigOption* raw_ptr = config.option(key);
|
ConfigOption* raw_ptr = config.option(key);
|
||||||
ConfigOptionVectorBase* opt_vec = static_cast<ConfigOptionVectorBase *>(raw_ptr);
|
ConfigOptionVectorBase* opt_vec = static_cast<ConfigOptionVectorBase *>(raw_ptr);
|
||||||
if(set_nil_to_default && raw_ptr->is_nil() && defaults.has(key) && std::find(filament_extruder_override_keys.begin(), filament_extruder_override_keys.end(), key) == filament_extruder_override_keys.end()){
|
if(set_nil_to_default && raw_ptr->is_nil() && defaults.has(key) && !is_filament_extruder_override_key(key)){
|
||||||
opt_vec->clear();
|
opt_vec->clear();
|
||||||
opt_vec->resize(length, defaults.option(key));
|
opt_vec->resize(length, defaults.option(key));
|
||||||
}
|
}
|
||||||
@@ -1312,7 +1312,7 @@ static std::vector<std::string> s_Preset_print_options{
|
|||||||
};
|
};
|
||||||
|
|
||||||
static std::vector<std::string> s_Preset_filament_options {/*"filament_colour", */ "default_filament_colour", "required_nozzle_HRC", "filament_diameter", "pellet_flow_coefficient", "volumetric_speed_coefficients", "filament_type",
|
static std::vector<std::string> s_Preset_filament_options {/*"filament_colour", */ "default_filament_colour", "required_nozzle_HRC", "filament_diameter", "pellet_flow_coefficient", "volumetric_speed_coefficients", "filament_type",
|
||||||
"filament_soluble", "filament_is_support", "filament_printable",
|
"filament_soluble", "filament_is_support", "filament_printable", "filament_extruder_compatibility",
|
||||||
"filament_max_volumetric_speed", "filament_adaptive_volumetric_speed",
|
"filament_max_volumetric_speed", "filament_adaptive_volumetric_speed",
|
||||||
"filament_flow_ratio", "filament_density", "filament_adhesiveness_category", "filament_cost", "filament_minimal_purge_on_wipe_tower",
|
"filament_flow_ratio", "filament_density", "filament_adhesiveness_category", "filament_cost", "filament_minimal_purge_on_wipe_tower",
|
||||||
"filament_tower_interface_pre_extrusion_dist", "filament_tower_interface_pre_extrusion_length", "filament_tower_ironing_area", "filament_tower_interface_purge_volume",
|
"filament_tower_interface_pre_extrusion_dist", "filament_tower_interface_pre_extrusion_length", "filament_tower_ironing_area", "filament_tower_interface_purge_volume",
|
||||||
@@ -1347,7 +1347,13 @@ static std::vector<std::string> s_Preset_filament_options {/*"filament_colour",
|
|||||||
"filament_multitool_ramming", "filament_multitool_ramming_volume", "filament_multitool_ramming_flow", "activate_chamber_temp_control", "chamber_minimal_temperature",
|
"filament_multitool_ramming", "filament_multitool_ramming_volume", "filament_multitool_ramming_flow", "activate_chamber_temp_control", "chamber_minimal_temperature",
|
||||||
"filament_long_retractions_when_cut","filament_retraction_distances_when_cut", "idle_temperature",
|
"filament_long_retractions_when_cut","filament_retraction_distances_when_cut", "idle_temperature",
|
||||||
//BBS filament change length while the extruder color
|
//BBS filament change length while the extruder color
|
||||||
"filament_change_length","filament_flush_volumetric_speed","filament_flush_temp", "filament_cooling_before_tower",
|
"filament_change_length","filament_flush_volumetric_speed","filament_flush_temp","filament_flush_temp_fast", "filament_cooling_before_tower",
|
||||||
|
// Multi-nozzle pre-cooling / ramming / nozzle-change (nc) filament overrides
|
||||||
|
"filament_ramming_volumetric_speed", "filament_ramming_volumetric_speed_nc",
|
||||||
|
"filament_ramming_travel_time", "filament_ramming_travel_time_nc",
|
||||||
|
"filament_pre_cooling_temperature", "filament_pre_cooling_temperature_nc",
|
||||||
|
"filament_preheat_temperature_delta", "filament_retract_length_nc",
|
||||||
|
"filament_change_length_nc", "filament_prime_volume_nc",
|
||||||
"long_retractions_when_ec", "retraction_distances_when_ec"
|
"long_retractions_when_ec", "retraction_distances_when_ec"
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1358,6 +1364,8 @@ static std::vector<std::string> s_Preset_machine_limits_options {
|
|||||||
"machine_min_extruding_rate", "machine_min_travel_rate",
|
"machine_min_extruding_rate", "machine_min_travel_rate",
|
||||||
"machine_max_jerk_x", "machine_max_jerk_y", "machine_max_jerk_z", "machine_max_jerk_e",
|
"machine_max_jerk_x", "machine_max_jerk_y", "machine_max_jerk_z", "machine_max_jerk_e",
|
||||||
"machine_max_junction_deviation",
|
"machine_max_junction_deviation",
|
||||||
|
// Bedslinger mass/force limits
|
||||||
|
"machine_max_force_Y", "machine_bed_mass_Y", "machine_max_printed_mass",
|
||||||
//resonance avoidance ported from qidi slicer
|
//resonance avoidance ported from qidi slicer
|
||||||
"resonance_avoidance", "min_resonance_avoidance_speed", "max_resonance_avoidance_speed",
|
"resonance_avoidance", "min_resonance_avoidance_speed", "max_resonance_avoidance_speed",
|
||||||
// Orca: input shaping
|
// Orca: input shaping
|
||||||
@@ -1375,7 +1383,7 @@ static std::vector<std::string> s_Preset_printer_options {
|
|||||||
"default_print_profile", "inherits",
|
"default_print_profile", "inherits",
|
||||||
"silent_mode",
|
"silent_mode",
|
||||||
"scan_first_layer", "enable_power_loss_recovery", "wrapping_detection_layers", "wrapping_exclude_area", "machine_load_filament_time", "machine_unload_filament_time", "machine_tool_change_time", "time_cost", "machine_pause_gcode", "template_custom_gcode",
|
"scan_first_layer", "enable_power_loss_recovery", "wrapping_detection_layers", "wrapping_exclude_area", "machine_load_filament_time", "machine_unload_filament_time", "machine_tool_change_time", "time_cost", "machine_pause_gcode", "template_custom_gcode",
|
||||||
"nozzle_type", "nozzle_hrc","auxiliary_fan", "nozzle_volume","upward_compatible_machine", "z_hop_types", "travel_slope", "retract_lift_enforce","support_chamber_temp_control","support_air_filtration","printer_structure",
|
"nozzle_type", "nozzle_hrc","auxiliary_fan", "nozzle_volume","upward_compatible_machine", "z_hop_types", "travel_slope", "retract_lift_enforce","support_chamber_temp_control","support_air_filtration","cooling_filter_enabled","printer_structure","farthest_point_timelapse",
|
||||||
"best_object_pos", "head_wrap_detect_zone",
|
"best_object_pos", "head_wrap_detect_zone",
|
||||||
"host_type", "print_host", "printhost_apikey", "flashforge_serial_number", "bbl_use_printhost", "printer_agent",
|
"host_type", "print_host", "printhost_apikey", "flashforge_serial_number", "bbl_use_printhost", "printer_agent",
|
||||||
"print_host_webui",
|
"print_host_webui",
|
||||||
@@ -1387,7 +1395,13 @@ static std::vector<std::string> s_Preset_printer_options {
|
|||||||
"cooling_tube_length", "high_current_on_filament_swap", "parking_pos_retraction", "extra_loading_move", "wipe_tower_type", "purge_in_prime_tower", "enable_filament_ramming", "tool_change_on_wipe_tower",
|
"cooling_tube_length", "high_current_on_filament_swap", "parking_pos_retraction", "extra_loading_move", "wipe_tower_type", "purge_in_prime_tower", "enable_filament_ramming", "tool_change_on_wipe_tower",
|
||||||
"z_offset",
|
"z_offset",
|
||||||
"disable_m73", "preferred_orientation", "emit_machine_limits_to_gcode", "pellet_modded_printer", "support_multi_bed_types", "use_3mf", "default_bed_type", "bed_mesh_min","bed_mesh_max","bed_mesh_probe_distance", "adaptive_bed_mesh_margin", "enable_long_retraction_when_cut","long_retractions_when_cut","retraction_distances_when_cut",
|
"disable_m73", "preferred_orientation", "emit_machine_limits_to_gcode", "pellet_modded_printer", "support_multi_bed_types", "use_3mf", "default_bed_type", "bed_mesh_min","bed_mesh_max","bed_mesh_probe_distance", "adaptive_bed_mesh_margin", "enable_long_retraction_when_cut","long_retractions_when_cut","retraction_distances_when_cut",
|
||||||
"bed_temperature_formula", "nozzle_flush_dataset"
|
"bed_temperature_formula", "nozzle_flush_dataset",
|
||||||
|
// Multi-nozzle count + pre-heat model printer options
|
||||||
|
"extruder_max_nozzle_count", "group_algo_with_time", "enable_pre_heating", "hotend_heating_rate", "hotend_cooling_rate",
|
||||||
|
"machine_hotend_change_time", "machine_prepare_compensation_time",
|
||||||
|
// Fast-purge printer flag + device/firmware-facing per-variant extruder-change
|
||||||
|
// deretraction speed (unconsumed by the slicer; carried by H2D/A2L/X2D/P2S machine profiles).
|
||||||
|
"support_fast_purge_mode", "deretract_speed_extruder_change"
|
||||||
};
|
};
|
||||||
|
|
||||||
static std::vector<std::string> s_Preset_sla_print_options {
|
static std::vector<std::string> s_Preset_sla_print_options {
|
||||||
|
|||||||
@@ -72,6 +72,7 @@
|
|||||||
#define BBL_JSON_KEY_BOTTOM_TEXTURE_END_NAME "bottom_texture_end_name"
|
#define BBL_JSON_KEY_BOTTOM_TEXTURE_END_NAME "bottom_texture_end_name"
|
||||||
#define BBL_JSON_KEY_USE_DOUBLE_EXTRUDER_DEFAULT_TEXTURE "use_double_extruder_default_texture"
|
#define BBL_JSON_KEY_USE_DOUBLE_EXTRUDER_DEFAULT_TEXTURE "use_double_extruder_default_texture"
|
||||||
#define BBL_JSON_KEY_BOTTOM_TEXTURE_RECT "bottom_texture_rect"
|
#define BBL_JSON_KEY_BOTTOM_TEXTURE_RECT "bottom_texture_rect"
|
||||||
|
#define BBL_JSON_KEY_BOTTOM_TEXTURE_RECT_LONGER "bottom_texture_rect_longer"
|
||||||
#define BBL_JSON_KEY_MIDDLE_TEXTURE_RECT "middle_texture_rect"
|
#define BBL_JSON_KEY_MIDDLE_TEXTURE_RECT "middle_texture_rect"
|
||||||
|
|
||||||
#define BBL_JSON_KEY_HOTEND_MODEL "hotend_model"
|
#define BBL_JSON_KEY_HOTEND_MODEL "hotend_model"
|
||||||
@@ -150,6 +151,7 @@ public:
|
|||||||
std::string bottom_texture_end_name;
|
std::string bottom_texture_end_name;
|
||||||
std::string use_double_extruder_default_texture;
|
std::string use_double_extruder_default_texture;
|
||||||
std::string bottom_texture_rect;
|
std::string bottom_texture_rect;
|
||||||
|
std::string bottom_texture_rect_longer;
|
||||||
std::string middle_texture_rect;
|
std::string middle_texture_rect;
|
||||||
std::string hotend_model;
|
std::string hotend_model;
|
||||||
PrinterVariant* variant(const std::string &name) {
|
PrinterVariant* variant(const std::string &name) {
|
||||||
|
|||||||
@@ -52,6 +52,9 @@ static std::vector<std::string> s_project_options {
|
|||||||
"wipe_tower_rotation_angle",
|
"wipe_tower_rotation_angle",
|
||||||
"curr_bed_type",
|
"curr_bed_type",
|
||||||
"flush_multiplier",
|
"flush_multiplier",
|
||||||
|
// Fast-purge mode: project-level purge control, inert at Default.
|
||||||
|
"flush_multiplier_fast",
|
||||||
|
"prime_volume_mode",
|
||||||
"nozzle_volume_type",
|
"nozzle_volume_type",
|
||||||
"filament_map_mode",
|
"filament_map_mode",
|
||||||
"filament_map"
|
"filament_map"
|
||||||
@@ -348,7 +351,7 @@ PresetBundle::PresetBundle()
|
|||||||
auto& default_config = this->filaments.default_preset().config;
|
auto& default_config = this->filaments.default_preset().config;
|
||||||
for(const std::string& opt_key : default_config.keys()){
|
for(const std::string& opt_key : default_config.keys()){
|
||||||
ConfigOption* opt = default_config.optptr(opt_key, false);
|
ConfigOption* opt = default_config.optptr(opt_key, false);
|
||||||
bool is_override_key = std::find(filament_extruder_override_keys.begin(),filament_extruder_override_keys.end(), opt_key) != filament_extruder_override_keys.end();
|
bool is_override_key = is_filament_extruder_override_key(opt_key);
|
||||||
if(!is_override_key || !opt->nullable())
|
if(!is_override_key || !opt->nullable())
|
||||||
continue;
|
continue;
|
||||||
opt->deserialize("nil",ForwardCompatibilitySubstitutionRule::Disable);
|
opt->deserialize("nil",ForwardCompatibilitySubstitutionRule::Disable);
|
||||||
@@ -721,6 +724,8 @@ std::optional<FilamentBaseInfo> PresetBundle::get_filament_by_filament_id(const
|
|||||||
auto iter = std::find(compatible_printers.begin(), compatible_printers.end(), printer_name);
|
auto iter = std::find(compatible_printers.begin(), compatible_printers.end(), printer_name);
|
||||||
if (iter != compatible_printers.end() && config.has("filament_printable")) {
|
if (iter != compatible_printers.end() && config.has("filament_printable")) {
|
||||||
info.filament_printable = config.option<ConfigOptionInts>("filament_printable")->values[0];
|
info.filament_printable = config.option<ConfigOptionInts>("filament_printable")->values[0];
|
||||||
|
if (config.has("filament_extruder_compatibility"))
|
||||||
|
info.set_filament_extruder_compatibility(config.option<ConfigOptionInts>("filament_extruder_compatibility")->values[0]);
|
||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3876,6 +3881,42 @@ DynamicPrintConfig PresetBundle::full_config_secure(std::optional<std::vector<in
|
|||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<std::vector<std::vector<float>>> PresetBundle::get_full_flush_matrix(bool with_multiplier) const
|
||||||
|
{
|
||||||
|
auto full_config = this->full_config();
|
||||||
|
int extruder_nums = full_config.option<ConfigOptionFloats>("nozzle_diameter")->values.size();
|
||||||
|
std::vector<double> flush_volume_value = full_config.option<ConfigOptionFloats>("flush_volumes_matrix")->values;
|
||||||
|
int filament_nums = full_config.option<ConfigOptionStrings>("filament_type")->values.size();
|
||||||
|
|
||||||
|
std::vector<std::vector<std::vector<float>>> matrix;
|
||||||
|
for (size_t extruder_id = 0; extruder_id < extruder_nums; ++extruder_id) {
|
||||||
|
std::vector<float> flush_matrix(cast<float>(get_flush_volumes_matrix(flush_volume_value, extruder_id, extruder_nums)));
|
||||||
|
std::vector<std::vector<float>> wipe_volumes;
|
||||||
|
for (unsigned int i = 0; i < filament_nums; ++i)
|
||||||
|
wipe_volumes.push_back(std::vector<float>(flush_matrix.begin() + i * filament_nums, flush_matrix.begin() + (i + 1) * filament_nums));
|
||||||
|
|
||||||
|
matrix.emplace_back(wipe_volumes);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (with_multiplier) {
|
||||||
|
// Fast purge mode uses flush_multiplier_fast; the default prime_volume_mode==Default
|
||||||
|
// (or the key absent) reads flush_multiplier, so this is inert.
|
||||||
|
auto* mode_opt = project_config.option<ConfigOptionEnum<PrimeVolumeMode>>("prime_volume_mode");
|
||||||
|
const bool use_fast = mode_opt && mode_opt->value == PrimeVolumeMode::pvmFast;
|
||||||
|
auto* mult_opt = project_config.option<ConfigOptionFloats>(use_fast ? "flush_multiplier_fast" : "flush_multiplier");
|
||||||
|
auto flush_multiplies = mult_opt ? mult_opt->values : project_config.option<ConfigOptionFloats>("flush_multiplier")->values;
|
||||||
|
flush_multiplies.resize(extruder_nums, 1);
|
||||||
|
for (size_t extruder_id = 0; extruder_id < extruder_nums; ++extruder_id) {
|
||||||
|
for (auto& vec : matrix[extruder_id]) {
|
||||||
|
for (auto& v : vec)
|
||||||
|
v *= flush_multiplies[extruder_id];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return matrix;
|
||||||
|
}
|
||||||
|
|
||||||
const std::set<std::string> ignore_settings_list ={
|
const std::set<std::string> ignore_settings_list ={
|
||||||
"inherits",
|
"inherits",
|
||||||
"print_settings_id", "filament_settings_id", "printer_settings_id"
|
"print_settings_id", "filament_settings_id", "printer_settings_id"
|
||||||
@@ -4770,6 +4811,8 @@ std::pair<PresetsConfigSubstitutions, size_t> PresetBundle::load_vendor_configs_
|
|||||||
model.use_double_extruder_default_texture = it.value();
|
model.use_double_extruder_default_texture = it.value();
|
||||||
} else if (boost::iequals(it.key(), BBL_JSON_KEY_BOTTOM_TEXTURE_RECT)) {
|
} else if (boost::iequals(it.key(), BBL_JSON_KEY_BOTTOM_TEXTURE_RECT)) {
|
||||||
model.bottom_texture_rect = it.value();
|
model.bottom_texture_rect = it.value();
|
||||||
|
} else if (boost::iequals(it.key(), BBL_JSON_KEY_BOTTOM_TEXTURE_RECT_LONGER)) {
|
||||||
|
model.bottom_texture_rect_longer = it.value();
|
||||||
} else if (boost::iequals(it.key(), BBL_JSON_KEY_MIDDLE_TEXTURE_RECT)) {
|
} else if (boost::iequals(it.key(), BBL_JSON_KEY_MIDDLE_TEXTURE_RECT)) {
|
||||||
model.middle_texture_rect = it.value();
|
model.middle_texture_rect = it.value();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,6 +72,25 @@ struct FilamentBaseInfo
|
|||||||
bool is_support{ false };
|
bool is_support{ false };
|
||||||
bool is_system{ true };
|
bool is_system{ true };
|
||||||
int filament_printable = 3;
|
int filament_printable = 3;
|
||||||
|
|
||||||
|
// filament_extruder_compatibility packs one compatibility level per extruder into a single
|
||||||
|
// 32-bit int, 3 bits per extruder (up to 10 extruders). Levels: 0 = printable, 1 = error,
|
||||||
|
// 2 = critical warning, 3 = warning (4-7 reserved). extruder_id is 0-based.
|
||||||
|
int get_extruder_compatibility(int extruder_id) const {
|
||||||
|
constexpr int bits_per_extruder = 3;
|
||||||
|
constexpr int extruder_mask = (1 << bits_per_extruder) - 1; // 0x7
|
||||||
|
constexpr int max_extruder_count = 32 / bits_per_extruder; // 10
|
||||||
|
|
||||||
|
if (extruder_id < 0 || extruder_id >= max_extruder_count)
|
||||||
|
return 0;
|
||||||
|
return (m_filament_extruder_compatibility >> (bits_per_extruder * extruder_id)) & extruder_mask;
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_filament_extruder_compatibility(int value) { m_filament_extruder_compatibility = value; }
|
||||||
|
int get_filament_extruder_compatibility() const { return m_filament_extruder_compatibility; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
int m_filament_extruder_compatibility = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
enum BundleType{
|
enum BundleType{
|
||||||
@@ -368,6 +387,11 @@ public:
|
|||||||
// full_config() with the some "useless" config removed.
|
// full_config() with the some "useless" config removed.
|
||||||
DynamicPrintConfig full_config_secure(std::optional<std::vector<int>>filament_maps = std::nullopt) const;
|
DynamicPrintConfig full_config_secure(std::optional<std::vector<int>>filament_maps = std::nullopt) const;
|
||||||
|
|
||||||
|
// Per-extruder flush matrix [extruder_id][from_filament][to_filament] in mm^3, optionally scaled
|
||||||
|
// by the per-extruder flush_multiplier (or flush_multiplier_fast when prime_volume_mode==Fast).
|
||||||
|
// Used by the print-dispatch nozzle-mapping flush-weight estimate.
|
||||||
|
std::vector<std::vector<std::vector<float>>> get_full_flush_matrix(bool with_multiplier = true) const;
|
||||||
|
|
||||||
//BBS: add some functions for multiple extruders
|
//BBS: add some functions for multiple extruders
|
||||||
int get_printer_extruder_count() const;
|
int get_printer_extruder_count() const;
|
||||||
bool support_different_extruders() const;
|
bool support_different_extruders() const;
|
||||||
|
|||||||
+70
-5
@@ -2506,10 +2506,15 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
|
|||||||
auto map_mode = get_filament_map_mode();
|
auto map_mode = get_filament_map_mode();
|
||||||
// get recommended filament map
|
// get recommended filament map
|
||||||
if (map_mode < FilamentMapMode::fmmManual) {
|
if (map_mode < FilamentMapMode::fmmManual) {
|
||||||
filament_maps = ToolOrdering::get_recommended_filament_maps(all_filaments, this, map_mode, physical_unprintables, geometric_unprintables);
|
// Grouping returns a nozzle-aware result; the 1-based extruder map
|
||||||
std::transform(filament_maps.begin(), filament_maps.end(), filament_maps.begin(), [](int value) { return value + 1; });
|
// for the by-object path is derived from it.
|
||||||
|
auto grouping_result = ToolOrdering::get_recommended_filament_maps(all_filaments, this, map_mode, physical_unprintables, geometric_unprintables);
|
||||||
|
auto derived_maps = grouping_result.get_extruder_map(false);
|
||||||
|
if (!derived_maps.empty()) {
|
||||||
|
filament_maps = derived_maps;
|
||||||
update_filament_maps_to_config(filament_maps);
|
update_filament_maps_to_config(filament_maps);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// check map valid both in auto and mannual mode
|
// check map valid both in auto and mannual mode
|
||||||
std::transform(filament_maps.begin(), filament_maps.end(), filament_maps.begin(), [](int value) {return value - 1; });
|
std::transform(filament_maps.begin(), filament_maps.end(), filament_maps.begin(), [](int value) {return value - 1; });
|
||||||
|
|
||||||
@@ -2658,8 +2663,14 @@ std::string Print::export_gcode(const std::string& path_template, GCodeProcessor
|
|||||||
gcode.do_export(this, path.c_str(), result, thumbnail_cb);
|
gcode.do_export(this, path.c_str(), result, thumbnail_cb);
|
||||||
gcode.export_layer_filaments(result);
|
gcode.export_layer_filaments(result);
|
||||||
//BBS
|
//BBS
|
||||||
if (result != nullptr)
|
if (result != nullptr) {
|
||||||
result->conflict_result = m_conflict_result;
|
result->conflict_result = m_conflict_result;
|
||||||
|
// Surface the slicer's per-filament nozzle grouping onto the post-slice result
|
||||||
|
// the device GUI reads. This is the static L/R + rack subset the multi-nozzle path computes;
|
||||||
|
// null for single-nozzle prints where nothing computes it. It is assigned after g-code
|
||||||
|
// generation and read by no emitter, so it does not affect the emitted g-code.
|
||||||
|
result->nozzle_group_result = this->get_layered_nozzle_group_result();
|
||||||
|
}
|
||||||
return path.c_str();
|
return path.c_str();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3306,6 +3317,47 @@ size_t Print::get_extruder_id(unsigned int filament_id) const
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Region reachable by every extruder = intersection of all per-extruder printable areas.
|
||||||
|
// For single-nozzle printers, or whenever extruder_printable_area is unpopulated / degenerate (all
|
||||||
|
// current single/dual profiles), fall back to the full printable_area so the wipe-tower-center clamp
|
||||||
|
// is identical to the previous full-bed clamp.
|
||||||
|
Polygons Print::get_extruder_shared_printable_polygon() const
|
||||||
|
{
|
||||||
|
const std::vector<Vec2ds>& extruder_printable_areas = m_config.extruder_printable_area.values;
|
||||||
|
if (m_config.nozzle_diameter.size() < 2 || extruder_printable_areas.empty())
|
||||||
|
return {Polygon::new_scale(m_config.printable_area.values)};
|
||||||
|
for (const Vec2ds& area : extruder_printable_areas)
|
||||||
|
if (area.size() < 3)
|
||||||
|
return {Polygon::new_scale(m_config.printable_area.values)};
|
||||||
|
|
||||||
|
Polygons shared_printable_polys = {Polygon::new_scale(extruder_printable_areas.front())};
|
||||||
|
for (size_t i = 1; i < extruder_printable_areas.size(); ++i)
|
||||||
|
shared_printable_polys = intersection(shared_printable_polys, Polygons{Polygon::new_scale(extruder_printable_areas[i])});
|
||||||
|
return shared_printable_polys;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Narrow the stored grouping result to the layer-aware type the slicing pipeline uses.
|
||||||
|
std::shared_ptr<MultiNozzleUtils::LayeredNozzleGroupResult> Print::get_layered_nozzle_group_result() const
|
||||||
|
{
|
||||||
|
return std::dynamic_pointer_cast<MultiNozzleUtils::LayeredNozzleGroupResult>(m_nozzle_group_result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dynamic (per-layer selector) regroup predicate.
|
||||||
|
// Orca: enable_filament_dynamic_map is a develop-only config key registered in the ConfigDef but NOT
|
||||||
|
// a static PrintConfig member, so it is read from the applied full config; it is absent for every
|
||||||
|
// shipping printer/profile -> nullptr -> false, which keeps the static grouping path (identical
|
||||||
|
// output) the only one the current fleet takes. There is no mixed-colour-filament guard (mixed-colour
|
||||||
|
// filaments are not supported). The remaining gates (auto-for-flush mode, multi-extruder machine)
|
||||||
|
// read the static PrintConfig members.
|
||||||
|
bool Print::is_dynamic_group_reorder() const
|
||||||
|
{
|
||||||
|
const auto *opt = m_full_print_config.option<ConfigOptionBool>("enable_filament_dynamic_map");
|
||||||
|
const bool enabled = opt && opt->value;
|
||||||
|
if (!enabled || m_config.filament_map_mode != FilamentMapMode::fmmAutoForFlush || m_config.nozzle_diameter.size() <= 1)
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// Wipe tower support.
|
// Wipe tower support.
|
||||||
bool Print::has_wipe_tower() const
|
bool Print::has_wipe_tower() const
|
||||||
{
|
{
|
||||||
@@ -3466,6 +3518,14 @@ void Print::_make_wipe_tower()
|
|||||||
m_wipe_tower_data.tool_ordering.empty() ? 0.f : m_wipe_tower_data.tool_ordering.back().print_z, m_wipe_tower_data.tool_ordering.all_extruders());
|
m_wipe_tower_data.tool_ordering.empty() ? 0.f : m_wipe_tower_data.tool_ordering.back().print_z, m_wipe_tower_data.tool_ordering.all_extruders());
|
||||||
wipe_tower.set_has_tpu_filament(this->has_tpu_filament());
|
wipe_tower.set_has_tpu_filament(this->has_tpu_filament());
|
||||||
wipe_tower.set_filament_map(this->get_filament_maps());
|
wipe_tower.set_filament_map(this->get_filament_maps());
|
||||||
|
// Feed the has_filament_switcher device flag (develop-only dynamic key, read defensively from
|
||||||
|
// the full config — no shipping profile sets it) and the shared printable bed used by the PETG
|
||||||
|
// pre-extrusion offset clamp. Both are inert unless has_filament_switcher is set.
|
||||||
|
{
|
||||||
|
const ConfigOptionBool* hfs = m_full_print_config.option<ConfigOptionBool>("has_filament_switcher");
|
||||||
|
wipe_tower.set_has_filament_switcher(hfs && hfs->value);
|
||||||
|
}
|
||||||
|
wipe_tower.set_shared_print_bed(this->get_extruder_shared_printable_polygon());
|
||||||
// Set the extruder & material properties at the wipe tower object.
|
// Set the extruder & material properties at the wipe tower object.
|
||||||
for (size_t i = 0; i < number_of_extruders; ++i)
|
for (size_t i = 0; i < number_of_extruders; ++i)
|
||||||
wipe_tower.set_extruder(i, m_config);
|
wipe_tower.set_extruder(i, m_config);
|
||||||
@@ -3517,7 +3577,10 @@ void Print::_make_wipe_tower()
|
|||||||
float volume_to_purge = 0;
|
float volume_to_purge = 0;
|
||||||
if (pre_filament_id != (unsigned int)(-1) && pre_filament_id != filament_id) {
|
if (pre_filament_id != (unsigned int)(-1) && pre_filament_id != filament_id) {
|
||||||
volume_to_purge = multi_extruder_flush[nozzle_id][pre_filament_id][filament_id];
|
volume_to_purge = multi_extruder_flush[nozzle_id][pre_filament_id][filament_id];
|
||||||
volume_to_purge *= m_config.flush_multiplier.get_at(nozzle_id);
|
// Fast purge mode uses flush_multiplier_fast; Default is inert.
|
||||||
|
float flush_multiplier = (m_config.prime_volume_mode == PrimeVolumeMode::pvmFast) ? m_config.flush_multiplier_fast.get_at(nozzle_id)
|
||||||
|
: m_config.flush_multiplier.get_at(nozzle_id);
|
||||||
|
volume_to_purge *= flush_multiplier;
|
||||||
volume_to_purge = pre_filament_id == -1 ? 0 :
|
volume_to_purge = pre_filament_id == -1 ? 0 :
|
||||||
layer_tools.wiping_extrusions().mark_wiping_extrusions(*this, current_filament_id, filament_id, volume_to_purge);
|
layer_tools.wiping_extrusions().mark_wiping_extrusions(*this, current_filament_id, filament_id, volume_to_purge);
|
||||||
}
|
}
|
||||||
@@ -3526,8 +3589,10 @@ void Print::_make_wipe_tower()
|
|||||||
float grab_purge_volume = m_config.grab_length.get_at(nozzle_id) * 2.4; //(diameter/2)^2*PI=2.4
|
float grab_purge_volume = m_config.grab_length.get_at(nozzle_id) * 2.4; //(diameter/2)^2*PI=2.4
|
||||||
volume_to_purge = std::max(0.f, volume_to_purge - grab_purge_volume);
|
volume_to_purge = std::max(0.f, volume_to_purge - grab_purge_volume);
|
||||||
|
|
||||||
|
// Saving mode reduces the prime volume to 15 mm3; Default is inert.
|
||||||
|
float prime_volume = (m_config.prime_volume_mode == PrimeVolumeMode::pvmSaving) ? 15.f : (float) m_config.prime_volume;
|
||||||
wipe_tower.plan_toolchange((float)layer_tools.print_z, (float)layer_tools.wipe_tower_layer_height, current_filament_id, filament_id,
|
wipe_tower.plan_toolchange((float)layer_tools.print_z, (float)layer_tools.wipe_tower_layer_height, current_filament_id, filament_id,
|
||||||
m_config.prime_volume, volume_to_purge);
|
prime_volume, volume_to_purge);
|
||||||
current_filament_id = filament_id;
|
current_filament_id = filament_id;
|
||||||
nozzle_cur_filament_ids[nozzle_id] = filament_id;
|
nozzle_cur_filament_ids[nozzle_id] = filament_id;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ class SupportLayer;
|
|||||||
class TreeSupportData;
|
class TreeSupportData;
|
||||||
class TreeSupport;
|
class TreeSupport;
|
||||||
class ExtrusionLayers;
|
class ExtrusionLayers;
|
||||||
|
namespace MultiNozzleUtils { class NozzleGroupResultBase; class LayeredNozzleGroupResult; }
|
||||||
|
|
||||||
#define MAX_OUTER_NOZZLE_DIAMETER 4
|
#define MAX_OUTER_NOZZLE_DIAMETER 4
|
||||||
// BBS: move from PrintObjectSlice.cpp
|
// BBS: move from PrintObjectSlice.cpp
|
||||||
@@ -1012,6 +1013,25 @@ public:
|
|||||||
// get the group label of filament
|
// get the group label of filament
|
||||||
size_t get_extruder_id(unsigned int filament_id) const;
|
size_t get_extruder_id(unsigned int filament_id) const;
|
||||||
|
|
||||||
|
// The region every extruder can reach,
|
||||||
|
// i.e. the intersection of all per-extruder printable areas. Falls back to the full printable_area
|
||||||
|
// for single-nozzle printers and whenever extruder_printable_area is not populated (all current
|
||||||
|
// single/dual profiles), so the wipe-tower-center clamp is byte-identical to full-bed clamping there.
|
||||||
|
Polygons get_extruder_shared_printable_polygon() const;
|
||||||
|
|
||||||
|
// Logical (extruder, nozzle) grouping result produced by ToolOrdering during reorder.
|
||||||
|
// Consumed by GCode via get_layered_nozzle_group_result()->get_nozzle_id(filament, layer) etc.
|
||||||
|
void set_nozzle_group_result(std::shared_ptr<MultiNozzleUtils::NozzleGroupResultBase> result) { m_nozzle_group_result = result; }
|
||||||
|
std::shared_ptr<MultiNozzleUtils::NozzleGroupResultBase> get_nozzle_group_result() const { return m_nozzle_group_result; }
|
||||||
|
std::shared_ptr<MultiNozzleUtils::LayeredNozzleGroupResult> get_layered_nozzle_group_result() const;
|
||||||
|
|
||||||
|
// True only when the printer opts into the per-layer filament selector
|
||||||
|
// (enable_filament_dynamic_map) in auto-for-flush mode on a multi-extruder machine. Gates the
|
||||||
|
// dynamic (per-layer) regroup branch in ToolOrdering::reorder_extruders_for_minimum_flush_volume.
|
||||||
|
// Closed for every current profile, so the static grouping path (byte-identical output) is the
|
||||||
|
// only one taken by the shipping fleet.
|
||||||
|
bool is_dynamic_group_reorder() const;
|
||||||
|
|
||||||
const std::vector<std::vector<DynamicPrintConfig>>& get_extruder_filament_info() const { return m_extruder_filament_info; }
|
const std::vector<std::vector<DynamicPrintConfig>>& get_extruder_filament_info() const { return m_extruder_filament_info; }
|
||||||
void set_extruder_filament_info(const std::vector<std::vector<DynamicPrintConfig>>& filament_info) { m_extruder_filament_info = filament_info; }
|
void set_extruder_filament_info(const std::vector<std::vector<DynamicPrintConfig>>& filament_info) { m_extruder_filament_info = filament_info; }
|
||||||
|
|
||||||
@@ -1176,6 +1196,9 @@ private:
|
|||||||
|
|
||||||
std::vector<std::vector<DynamicPrintConfig>> m_extruder_filament_info;
|
std::vector<std::vector<DynamicPrintConfig>> m_extruder_filament_info;
|
||||||
|
|
||||||
|
// Logical (extruder, nozzle) grouping result, set by ToolOrdering during reorder.
|
||||||
|
std::shared_ptr<MultiNozzleUtils::NozzleGroupResultBase> m_nozzle_group_result;
|
||||||
|
|
||||||
// Following section will be consumed by the GCodeGenerator.
|
// Following section will be consumed by the GCodeGenerator.
|
||||||
ToolOrdering m_tool_ordering;
|
ToolOrdering m_tool_ordering;
|
||||||
WipeTowerData m_wipe_tower_data {m_tool_ordering};
|
WipeTowerData m_wipe_tower_data {m_tool_ordering};
|
||||||
|
|||||||
@@ -83,6 +83,15 @@ const std::vector<std::string> filament_extruder_override_keys = {
|
|||||||
"filament_retraction_distances_when_cut"
|
"filament_retraction_distances_when_cut"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Some filament override parameters are generated from filament_extruder_override_keys,
|
||||||
|
// while filament_retract_length_nc is defined separately. Keep the generator list
|
||||||
|
// unchanged and use this helper for behavior checks that need the full override set.
|
||||||
|
bool is_filament_extruder_override_key(const std::string &opt_key)
|
||||||
|
{
|
||||||
|
return std::find(filament_extruder_override_keys.begin(), filament_extruder_override_keys.end(), opt_key) != filament_extruder_override_keys.end() ||
|
||||||
|
opt_key == "filament_retract_length_nc";
|
||||||
|
}
|
||||||
|
|
||||||
size_t get_extruder_index(const GCodeConfig& config, unsigned int filament_id)
|
size_t get_extruder_index(const GCodeConfig& config, unsigned int filament_id)
|
||||||
{
|
{
|
||||||
if (filament_id < config.filament_map.size()) {
|
if (filament_id < config.filament_map.size()) {
|
||||||
@@ -569,17 +578,28 @@ CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(ExtruderType)
|
|||||||
|
|
||||||
static const t_config_enum_values s_keys_map_NozzleVolumeType = {
|
static const t_config_enum_values s_keys_map_NozzleVolumeType = {
|
||||||
{ "Standard", nvtStandard },
|
{ "Standard", nvtStandard },
|
||||||
{ "High Flow", nvtHighFlow }
|
{ "High Flow", nvtHighFlow },
|
||||||
|
{ "TPU High Flow", nvtTPUHighFlow },
|
||||||
|
{ "Hybrid", nvtHybrid }
|
||||||
};
|
};
|
||||||
CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(NozzleVolumeType)
|
CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(NozzleVolumeType)
|
||||||
|
|
||||||
static const t_config_enum_values s_keys_map_FilamentMapMode = {
|
static const t_config_enum_values s_keys_map_FilamentMapMode = {
|
||||||
{ "Auto For Flush", fmmAutoForFlush },
|
{ "Auto For Flush", fmmAutoForFlush },
|
||||||
{ "Auto For Match", fmmAutoForMatch },
|
{ "Auto For Match", fmmAutoForMatch },
|
||||||
{ "Manual", fmmManual }
|
{ "Manual", fmmManual },
|
||||||
|
{ "Nozzle Manual", fmmNozzleManual }
|
||||||
};
|
};
|
||||||
CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(FilamentMapMode)
|
CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(FilamentMapMode)
|
||||||
|
|
||||||
|
// PrimeVolumeMode. Serialized string keys must stay stable; they round-trip through .3mf.
|
||||||
|
static const t_config_enum_values s_keys_map_PrimeVolumeMode = {
|
||||||
|
{ "Default", pvmDefault },
|
||||||
|
{ "Saving", pvmSaving },
|
||||||
|
{ "Fast", pvmFast }
|
||||||
|
};
|
||||||
|
CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(PrimeVolumeMode)
|
||||||
|
|
||||||
|
|
||||||
//BBS
|
//BBS
|
||||||
std::string get_extruder_variant_string(ExtruderType extruder_type, NozzleVolumeType nozzle_volume_type)
|
std::string get_extruder_variant_string(ExtruderType extruder_type, NozzleVolumeType nozzle_volume_type)
|
||||||
@@ -650,6 +670,60 @@ std::vector<std::string> save_extruder_ams_count_to_string(const std::vector<std
|
|||||||
return extruder_ams_count_str;
|
return extruder_ams_count_str;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parses the extruder_nozzle_stats option: one "|"-separated token list per
|
||||||
|
// extruder, each token
|
||||||
|
// "<volume_type_name>#<count>". Empty string => no per-type stats for that extruder.
|
||||||
|
std::vector<std::map<NozzleVolumeType, int>> get_extruder_nozzle_stats(const std::vector<std::string> &strs)
|
||||||
|
{
|
||||||
|
std::vector<std::map<NozzleVolumeType, int>> extruder_nozzle_counts;
|
||||||
|
for (const std::string &str : strs) {
|
||||||
|
std::map<NozzleVolumeType, int> nozzle_count_map;
|
||||||
|
if (str.empty()) {
|
||||||
|
extruder_nozzle_counts.emplace_back(nozzle_count_map);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
std::vector<std::string> nozzle_infos;
|
||||||
|
boost::algorithm::split(nozzle_infos, str, boost::algorithm::is_any_of("|"));
|
||||||
|
for (auto &nozzle_info : nozzle_infos) {
|
||||||
|
std::vector<std::string> attr;
|
||||||
|
boost::algorithm::split(attr, nozzle_info, boost::algorithm::is_any_of("#"));
|
||||||
|
// Guard against malformed device/project data: skip tokens without a "<type>#<count>"
|
||||||
|
// shape or with an unknown volume-type key instead of throwing (.at) or reading attr[1]
|
||||||
|
// out of bounds. Corrupt tokens are dropped and logged; the rest still parse.
|
||||||
|
if (attr.size() < 2) {
|
||||||
|
BOOST_LOG_TRIVIAL(warning) << "get_extruder_nozzle_stats: skipping malformed token '" << nozzle_info << "'";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
auto it = s_keys_map_NozzleVolumeType.find(attr[0]);
|
||||||
|
if (it == s_keys_map_NozzleVolumeType.end()) {
|
||||||
|
BOOST_LOG_TRIVIAL(warning) << "get_extruder_nozzle_stats: skipping unknown nozzle volume type '" << attr[0] << "'";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
NozzleVolumeType volume_type = NozzleVolumeType(it->second);
|
||||||
|
int nozzle_count = std::atoi(attr[1].c_str());
|
||||||
|
nozzle_count_map[volume_type] = nozzle_count;
|
||||||
|
}
|
||||||
|
extruder_nozzle_counts.emplace_back(nozzle_count_map);
|
||||||
|
}
|
||||||
|
return extruder_nozzle_counts;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> save_extruder_nozzle_stats_to_string(const std::vector<std::map<NozzleVolumeType, int>> &extruder_nozzle_stats)
|
||||||
|
{
|
||||||
|
std::vector<std::string> extruder_nozzle_count_str;
|
||||||
|
for (size_t idx = 0; idx < extruder_nozzle_stats.size(); ++idx) {
|
||||||
|
std::ostringstream oss;
|
||||||
|
const auto &item = extruder_nozzle_stats[idx];
|
||||||
|
for (auto it = item.begin(); it != item.end(); ++it) {
|
||||||
|
oss << get_nozzle_volume_type_string(it->first) << "#" << it->second;
|
||||||
|
if (std::next(it) != item.end())
|
||||||
|
oss << "|";
|
||||||
|
}
|
||||||
|
extruder_nozzle_count_str.emplace_back(oss.str());
|
||||||
|
}
|
||||||
|
return extruder_nozzle_count_str;
|
||||||
|
}
|
||||||
|
|
||||||
static void assign_printer_technology_to_unknown(t_optiondef_map &options, PrinterTechnology printer_technology)
|
static void assign_printer_technology_to_unknown(t_optiondef_map &options, PrinterTechnology printer_technology)
|
||||||
{
|
{
|
||||||
for (std::pair<const t_config_option_key, ConfigOptionDef> &kvp : options)
|
for (std::pair<const t_config_option_key, ConfigOptionDef> &kvp : options)
|
||||||
@@ -2548,6 +2622,25 @@ void PrintConfigDef::init_fff_params()
|
|||||||
def->mode = comDevelop;
|
def->mode = comDevelop;
|
||||||
def->set_default_value(new ConfigOptionInts{1});
|
def->set_default_value(new ConfigOptionInts{1});
|
||||||
|
|
||||||
|
// Multi-nozzle: per-filament map to the config slot identified by (extruder, nozzle_volume_type).
|
||||||
|
// Forward-compat-only registration with no consumer yet.
|
||||||
|
// Orca: resolves per-variant slots via get_index_for_extruder at PrintApply time rather than a
|
||||||
|
// read-time filament-config-index lookup, so nothing in src/ reads this key. Kept registered so an
|
||||||
|
// H2C project/config that carries it loads without an unknown-option substitution warning.
|
||||||
|
// Internal use only, no translation.
|
||||||
|
def = this->add("filament_map_2", coInts);
|
||||||
|
def->label = "Filament map plus for multi nozzle";
|
||||||
|
def->tooltip = "Filament map to the index identified by extruder and nozzle_volume_type";
|
||||||
|
def->mode = comDevelop;
|
||||||
|
def->set_default_value(new ConfigOptionInts{1});
|
||||||
|
|
||||||
|
// Per-filament nozzle-volume-type override (multi-volume/Hybrid). Registered so the value
|
||||||
|
// round-trips through .3mf plate metadata (filament_volume_maps); load/save-only for now and
|
||||||
|
// inert for existing printers (unread by slicing until the multi-volume pipeline lands).
|
||||||
|
def = this->add("filament_volume_map", coInts);
|
||||||
|
def->mode = comDevelop;
|
||||||
|
def->set_default_value(new ConfigOptionInts{(int)(NozzleVolumeType::nvtStandard)});
|
||||||
|
|
||||||
def = this->add("physical_extruder_map",coInts);
|
def = this->add("physical_extruder_map",coInts);
|
||||||
// internal use only, don't need translation
|
// internal use only, don't need translation
|
||||||
def->label = "Map the logical extruder to physical extruder";
|
def->label = "Map the logical extruder to physical extruder";
|
||||||
@@ -2563,14 +2656,22 @@ void PrintConfigDef::init_fff_params()
|
|||||||
def->enum_values.push_back("Auto For Flush");
|
def->enum_values.push_back("Auto For Flush");
|
||||||
def->enum_values.push_back("Auto For Match");
|
def->enum_values.push_back("Auto For Match");
|
||||||
def->enum_values.push_back("Manual");
|
def->enum_values.push_back("Manual");
|
||||||
|
def->enum_values.push_back("Nozzle Manual");
|
||||||
def->enum_values.push_back("Default");
|
def->enum_values.push_back("Default");
|
||||||
def->enum_labels.push_back(L("Auto For Flush"));
|
def->enum_labels.push_back(L("Auto For Flush"));
|
||||||
def->enum_labels.push_back(L("Auto For Match"));
|
def->enum_labels.push_back(L("Auto For Match"));
|
||||||
def->enum_labels.push_back(L("Manual"));
|
def->enum_labels.push_back(L("Manual"));
|
||||||
|
def->enum_labels.push_back(L("Nozzle Manual"));
|
||||||
def->enum_labels.push_back(L("Default"));
|
def->enum_labels.push_back(L("Default"));
|
||||||
def->mode = comAdvanced;
|
def->mode = comAdvanced;
|
||||||
def->set_default_value(new ConfigOptionEnum<FilamentMapMode>(fmmAutoForFlush));
|
def->set_default_value(new ConfigOptionEnum<FilamentMapMode>(fmmAutoForFlush));
|
||||||
|
|
||||||
|
// Fully-manual mode (fmmNozzleManual): per-filament -> physical-nozzle assignment.
|
||||||
|
// Inert until the nozzle-assignment engine consumes it. Internal use only, no translation.
|
||||||
|
def = this->add("filament_nozzle_map", coInts);
|
||||||
|
def->mode = comDevelop;
|
||||||
|
def->set_default_value(new ConfigOptionInts{1});
|
||||||
|
|
||||||
def = this->add("filament_flush_temp", coInts);
|
def = this->add("filament_flush_temp", coInts);
|
||||||
def->label = L("Flush temperature");
|
def->label = L("Flush temperature");
|
||||||
def->tooltip = L("Temperature when flushing filament. 0 indicates the upper bound of the recommended nozzle temperature range.");
|
def->tooltip = L("Temperature when flushing filament. 0 indicates the upper bound of the recommended nozzle temperature range.");
|
||||||
@@ -2581,6 +2682,19 @@ void PrintConfigDef::init_fff_params()
|
|||||||
def->sidetext = L(u8"\u2103" /* °C */); // degrees Celsius, CIS languages need translation
|
def->sidetext = L(u8"\u2103" /* °C */); // degrees Celsius, CIS languages need translation
|
||||||
def->set_default_value(new ConfigOptionIntsNullable{0});
|
def->set_default_value(new ConfigOptionIntsNullable{0});
|
||||||
|
|
||||||
|
// Fast-purge flush temperature: used only when prime_volume_mode==Fast.
|
||||||
|
// Default 0 (fall back to the recommended-range upper bound, identical to filament_flush_temp),
|
||||||
|
// so the key is inert for the shipping fleet. Kept out of the g-code config block (banned_keys).
|
||||||
|
def = this->add("filament_flush_temp_fast", coInts);
|
||||||
|
def->label = L("Flush temperature");
|
||||||
|
def->tooltip = L("Flush temperature used in fast purge mode.");
|
||||||
|
def->mode = comAdvanced;
|
||||||
|
def->nullable = true;
|
||||||
|
def->min = 0;
|
||||||
|
def->max = max_temp;
|
||||||
|
def->sidetext = L(u8"℃" /* °C */); // degrees Celsius, CIS languages need translation
|
||||||
|
def->set_default_value(new ConfigOptionIntsNullable{0});
|
||||||
|
|
||||||
def = this->add("filament_flush_volumetric_speed", coFloats);
|
def = this->add("filament_flush_volumetric_speed", coFloats);
|
||||||
def->label = L("Flush volumetric speed");
|
def->label = L("Flush volumetric speed");
|
||||||
def->tooltip = L("Volumetric speed when flushing filament. 0 indicates the max volumetric speed.");
|
def->tooltip = L("Volumetric speed when flushing filament. 0 indicates the max volumetric speed.");
|
||||||
@@ -2954,6 +3068,18 @@ void PrintConfigDef::init_fff_params()
|
|||||||
def->mode = comDevelop;
|
def->mode = comDevelop;
|
||||||
def->set_default_value(new ConfigOptionInts{3});
|
def->set_default_value(new ConfigOptionInts{3});
|
||||||
|
|
||||||
|
// A single 32-bit int encodes the compatibility level of a filament across all extruders (up to 10).
|
||||||
|
// Every 3 bits represent one extruder: bits [3*i, 3*i+2] -> extruder i.
|
||||||
|
// Compatibility levels: 0 = printable, 1 = error, 2 = critical warning, 3 = warning (4-7 reserved).
|
||||||
|
// Default 0 (printable) keeps the key inert for filaments/printers that do not declare it.
|
||||||
|
def = this->add("filament_extruder_compatibility", coInts);
|
||||||
|
def->label = L("Filament-extruder compatibility");
|
||||||
|
def->tooltip = L("A single 32-bit int encoding the compatibility level of a filament across all extruders (up to 10). "
|
||||||
|
"Every 3 bits represent one extruder (bits [3*i, 3*i+2] for extruder i). "
|
||||||
|
"0: printable, 1: error, 2: critical warning, 3: warning, 4-7: reserved.");
|
||||||
|
def->mode = comDevelop;
|
||||||
|
def->set_default_value(new ConfigOptionInts{0});
|
||||||
|
|
||||||
// BBS
|
// BBS
|
||||||
def = this->add("temperature_vitrification", coInts);
|
def = this->add("temperature_vitrification", coInts);
|
||||||
def->label = L("Softening temperature");
|
def->label = L("Softening temperature");
|
||||||
@@ -3857,6 +3983,12 @@ void PrintConfigDef::init_fff_params()
|
|||||||
def->mode=comDevelop;
|
def->mode=comDevelop;
|
||||||
def->set_default_value(new ConfigOptionBool(true));
|
def->set_default_value(new ConfigOptionBool(true));
|
||||||
|
|
||||||
|
def = this->add("cooling_filter_enabled", coBool);
|
||||||
|
def->label = L("Use cooling filter");
|
||||||
|
def->tooltip = L("Enable this if printer support cooling filter");
|
||||||
|
def->mode = comAdvanced;
|
||||||
|
def->set_default_value(new ConfigOptionBool(false));
|
||||||
|
|
||||||
def = this->add("gcode_flavor", coEnum);
|
def = this->add("gcode_flavor", coEnum);
|
||||||
def->label = L("G-code flavor");
|
def->label = L("G-code flavor");
|
||||||
def->tooltip = L("What kind of G-code the printer is compatible with.");
|
def->tooltip = L("What kind of G-code the printer is compatible with.");
|
||||||
@@ -4659,6 +4791,39 @@ void PrintConfigDef::init_fff_params()
|
|||||||
def->mode = comDevelop;
|
def->mode = comDevelop;
|
||||||
def->set_default_value(new ConfigOptionFloats{ 0., 0. });
|
def->set_default_value(new ConfigOptionFloats{ 0., 0. });
|
||||||
|
|
||||||
|
// Bedslinger mass/force limits.
|
||||||
|
// Consumed by GCode::mass_load_limited_machine_acceleration (curr_y_acceleration_limit)
|
||||||
|
// and the printed-mass check. Default 0 keeps them inactive for existing printers.
|
||||||
|
def = this->add("machine_max_force_Y", coFloat);
|
||||||
|
def->full_label = L("Maximum force of the Y axis");
|
||||||
|
def->category = L("Machine limits");
|
||||||
|
def->readonly = false;
|
||||||
|
def->tooltip = L("The allowed maximum output force of Y axis");
|
||||||
|
def->sidetext = L("N");
|
||||||
|
def->min = 0;
|
||||||
|
def->mode = comDevelop;
|
||||||
|
def->set_default_value(new ConfigOptionFloat(0));
|
||||||
|
|
||||||
|
def = this->add("machine_bed_mass_Y", coFloat);
|
||||||
|
def->full_label = L("Bed mass of the Y axis");
|
||||||
|
def->category = L("Machine limits");
|
||||||
|
def->readonly = false;
|
||||||
|
def->tooltip = L("The machine bed mass load of Y axis");
|
||||||
|
def->sidetext = L("g");
|
||||||
|
def->min = 0;
|
||||||
|
def->mode = comDevelop;
|
||||||
|
def->set_default_value(new ConfigOptionFloat(0));
|
||||||
|
|
||||||
|
def = this->add("machine_max_printed_mass", coFloat);
|
||||||
|
def->full_label = L("The allowed max printed mass");
|
||||||
|
def->category = L("Machine limits");
|
||||||
|
def->readonly = false;
|
||||||
|
def->tooltip = L("The allowed max printed mass on a plate");
|
||||||
|
def->sidetext = L("g");
|
||||||
|
def->min = 0;
|
||||||
|
def->mode = comDevelop;
|
||||||
|
def->set_default_value(new ConfigOptionFloat(0));
|
||||||
|
|
||||||
// M204 P... [mm/sec^2]
|
// M204 P... [mm/sec^2]
|
||||||
def = this->add("machine_max_acceleration_extruding", coFloats);
|
def = this->add("machine_max_acceleration_extruding", coFloats);
|
||||||
def->full_label = L("Maximum acceleration for extruding");
|
def->full_label = L("Maximum acceleration for extruding");
|
||||||
@@ -5404,10 +5569,15 @@ void PrintConfigDef::init_fff_params()
|
|||||||
def->label = "Nozzle Volume Type";
|
def->label = "Nozzle Volume Type";
|
||||||
def->tooltip = "Nozzle volume type for extruders.";
|
def->tooltip = "Nozzle volume type for extruders.";
|
||||||
def->enum_keys_map = &ConfigOptionEnum<NozzleVolumeType>::get_enum_values();
|
def->enum_keys_map = &ConfigOptionEnum<NozzleVolumeType>::get_enum_values();
|
||||||
|
// Order must match the NozzleVolumeType enum values (Standard=0, High Flow=1, Hybrid=2, TPU High Flow=3).
|
||||||
def->enum_values.push_back(L("Standard"));
|
def->enum_values.push_back(L("Standard"));
|
||||||
def->enum_values.push_back(L("High Flow"));
|
def->enum_values.push_back(L("High Flow"));
|
||||||
|
def->enum_values.push_back(L("Hybrid"));
|
||||||
|
def->enum_values.push_back(L("TPU High Flow"));
|
||||||
def->enum_labels.push_back(L("Standard"));
|
def->enum_labels.push_back(L("Standard"));
|
||||||
def->enum_labels.push_back(L("High Flow"));
|
def->enum_labels.push_back(L("High Flow"));
|
||||||
|
def->enum_labels.push_back(L("Hybrid"));
|
||||||
|
def->enum_labels.push_back(L("TPU High Flow"));
|
||||||
def->mode = comSimple;
|
def->mode = comSimple;
|
||||||
def->set_default_value(new ConfigOptionEnumsGeneric{ NozzleVolumeType::nvtStandard });
|
def->set_default_value(new ConfigOptionEnumsGeneric{ NozzleVolumeType::nvtStandard });
|
||||||
|
|
||||||
@@ -5418,8 +5588,12 @@ void PrintConfigDef::init_fff_params()
|
|||||||
def->enum_keys_map = &ConfigOptionEnum<NozzleVolumeType>::get_enum_values();
|
def->enum_keys_map = &ConfigOptionEnum<NozzleVolumeType>::get_enum_values();
|
||||||
def->enum_values.push_back(L("Standard"));
|
def->enum_values.push_back(L("Standard"));
|
||||||
def->enum_values.push_back(L("High Flow"));
|
def->enum_values.push_back(L("High Flow"));
|
||||||
|
def->enum_values.push_back(L("Hybrid"));
|
||||||
|
def->enum_values.push_back(L("TPU High Flow"));
|
||||||
def->enum_labels.push_back(L("Standard"));
|
def->enum_labels.push_back(L("Standard"));
|
||||||
def->enum_labels.push_back(L("High Flow"));
|
def->enum_labels.push_back(L("High Flow"));
|
||||||
|
def->enum_labels.push_back(L("Hybrid"));
|
||||||
|
def->enum_labels.push_back(L("TPU High Flow"));
|
||||||
def->mode = comDevelop;
|
def->mode = comDevelop;
|
||||||
def->set_default_value(new ConfigOptionEnumsGeneric{ NozzleVolumeType::nvtStandard });
|
def->set_default_value(new ConfigOptionEnumsGeneric{ NozzleVolumeType::nvtStandard });
|
||||||
|
|
||||||
@@ -5436,6 +5610,43 @@ void PrintConfigDef::init_fff_params()
|
|||||||
def->tooltip = "AMS counts per extruder.";
|
def->tooltip = "AMS counts per extruder.";
|
||||||
def->set_default_value(new ConfigOptionStrings { });
|
def->set_default_value(new ConfigOptionStrings { });
|
||||||
|
|
||||||
|
// Multi-nozzle: per-extruder physical nozzle inventory by volume type,
|
||||||
|
// "<volume_type>#<count>" tokens joined by "|". Internal use only, no translation.
|
||||||
|
def = this->add("extruder_nozzle_stats", coStrings);
|
||||||
|
def->label = "Extruder nozzle stats";
|
||||||
|
def->tooltip = "Physical nozzle counts per extruder, keyed by nozzle volume type.";
|
||||||
|
def->set_default_value(new ConfigOptionStrings { });
|
||||||
|
|
||||||
|
// Multi-nozzle: number of physical nozzles per extruder. Forward-compat-only registration
|
||||||
|
// with no slicing consumer: the nozzle-assignment engine derives its per-extruder physical nozzle
|
||||||
|
// inventory from the richer `extruder_nozzle_stats` key, not from this scalar count, so nothing in
|
||||||
|
// src/ reads it. Kept registered so an H2C project/config that carries the key loads without an
|
||||||
|
// unknown-option substitution warning. Default {1} == today's single-nozzle-per-extruder assumption,
|
||||||
|
// so absent/default is a no-op for every shipping printer. Internal use only, no translation.
|
||||||
|
def = this->add("extruder_nozzle_count", coInts);
|
||||||
|
def->label = "extruder nozzle count";
|
||||||
|
def->tooltip = "extruder nozzle count";
|
||||||
|
def->mode = comDevelop;
|
||||||
|
def->set_default_value(new ConfigOptionInts{1});
|
||||||
|
|
||||||
|
// Per-nozzle volume type. Forward-compat-only registration with no slicing consumer — nothing in
|
||||||
|
// src/ reads it; the engine resolves per-nozzle volume types from `extruder_nozzle_stats` tokens
|
||||||
|
// instead. Kept registered so a project/config carrying it loads without an unknown-option
|
||||||
|
// substitution warning. Registers Standard/High Flow/TPU High Flow only (no Hybrid).
|
||||||
|
// Internal use only, no translation.
|
||||||
|
def = this->add("extruder_nozzle_volume_type", coEnums);
|
||||||
|
def->label = "Extruder nozzle volume type";
|
||||||
|
def->tooltip = "Nozzle volume type per physical nozzle of an extruder.";
|
||||||
|
def->enum_keys_map = &ConfigOptionEnum<NozzleVolumeType>::get_enum_values();
|
||||||
|
def->enum_values.push_back("Standard");
|
||||||
|
def->enum_values.push_back("High Flow");
|
||||||
|
def->enum_values.push_back("TPU High Flow");
|
||||||
|
def->enum_labels.push_back("Standard");
|
||||||
|
def->enum_labels.push_back("High Flow");
|
||||||
|
def->enum_labels.push_back("TPU High Flow");
|
||||||
|
def->mode = comDevelop;
|
||||||
|
def->set_default_value(new ConfigOptionEnumsGeneric{ NozzleVolumeType::nvtStandard });
|
||||||
|
|
||||||
def = this->add("enable_filament_dynamic_map", coBool);
|
def = this->add("enable_filament_dynamic_map", coBool);
|
||||||
def->label = L("Enable filament dynamic map");
|
def->label = L("Enable filament dynamic map");
|
||||||
def->tooltip = L("Enable dynamic filament mapping during print.");
|
def->tooltip = L("Enable dynamic filament mapping during print.");
|
||||||
@@ -5534,6 +5745,19 @@ void PrintConfigDef::init_fff_params()
|
|||||||
def->mode = comAdvanced;
|
def->mode = comAdvanced;
|
||||||
def->set_default_value(new ConfigOptionFloats { 0. });
|
def->set_default_value(new ConfigOptionFloats { 0. });
|
||||||
|
|
||||||
|
// Per-(extruder,variant) deretraction speed at an extruder change. This value
|
||||||
|
// ships in multi-extruder machine profiles but has no slicer consumer — it is a device/firmware-facing
|
||||||
|
// profile key. Registered here (ConfigDef-only, no FullPrintConfig static member) so X2D/P2S profiles
|
||||||
|
// that already carry it — and the H2D/A2L leaves — validate as a known key; unread by the slicer.
|
||||||
|
// nullable so absent leaves stay nil (excluded from the g-code config block).
|
||||||
|
def = this->add("deretract_speed_extruder_change", coFloats);
|
||||||
|
def->label = L("Deretraction speed (extruder change)");
|
||||||
|
def->tooltip = L("Speed for reloading filament into the nozzle when switching extruder.");
|
||||||
|
def->sidetext = L("mm/s"); // millimeters per second, CIS languages need translation
|
||||||
|
def->mode = comDevelop;
|
||||||
|
def->nullable = true;
|
||||||
|
def->set_default_value(new ConfigOptionFloatsNullable { 0. });
|
||||||
|
|
||||||
def = this->add("use_firmware_retraction", coBool);
|
def = this->add("use_firmware_retraction", coBool);
|
||||||
def->label = L("Use firmware retraction");
|
def->label = L("Use firmware retraction");
|
||||||
def->tooltip = L("This experimental setting uses G10 and G11 commands to have the firmware "
|
def->tooltip = L("This experimental setting uses G10 and G11 commands to have the firmware "
|
||||||
@@ -5952,6 +6176,20 @@ void PrintConfigDef::init_fff_params()
|
|||||||
def->mode = comSimple;
|
def->mode = comSimple;
|
||||||
def->set_default_value(new ConfigOptionEnum<TimelapseType>(tlTraditional));
|
def->set_default_value(new ConfigOptionEnum<TimelapseType>(tlTraditional));
|
||||||
|
|
||||||
|
// Farthest-point timelapse. Corexy-only refinement layered on top of the existing
|
||||||
|
// timelapse_type: when enabled the traditional-mode snapshot is
|
||||||
|
// taken at the extrusion point farthest from the camera (0,0) instead of traveling to a safe/chute
|
||||||
|
// position. Gated in GCode::process_layer by timelapse_type==tlTraditional && printer_structure!=psI3,
|
||||||
|
// so it is inert for i3 (A1/A2L) printers and whenever the toggle is off (default false → every
|
||||||
|
// existing printer's g-code is byte-identical). Only H2C/H2D machine profiles set it =1.
|
||||||
|
def = this->add("farthest_point_timelapse", coBool);
|
||||||
|
def->label = L("Farthest point timelapse");
|
||||||
|
def->tooltip = L("When enabled, the timelapse snapshot is taken at the farthest point from camera "
|
||||||
|
"instead of traveling to the wipe tower or excess chute. "
|
||||||
|
"Only effective in traditional timelapse mode on non-I3 printers.");
|
||||||
|
def->mode = comSimple;
|
||||||
|
def->set_default_value(new ConfigOptionBool(false));
|
||||||
|
|
||||||
def = this->add("standby_temperature_delta", coInt);
|
def = this->add("standby_temperature_delta", coInt);
|
||||||
def->label = L("Temperature variation");
|
def->label = L("Temperature variation");
|
||||||
// TRN PrintSettings : "Ooze prevention" > "Temperature variation"
|
// TRN PrintSettings : "Ooze prevention" > "Temperature variation"
|
||||||
@@ -6875,6 +7113,14 @@ void PrintConfigDef::init_fff_params()
|
|||||||
//def->sidetext = "";
|
//def->sidetext = "";
|
||||||
def->set_default_value(new ConfigOptionFloats{0.3});
|
def->set_default_value(new ConfigOptionFloats{0.3});
|
||||||
|
|
||||||
|
// Fast-purge mode: the flush multiplier used when prime_volume_mode==Fast.
|
||||||
|
// Only consumed on the pvmFast branch (default prime_volume_mode==Default reads flush_multiplier),
|
||||||
|
// so this key is inert for the shipping fleet. Kept out of the g-code config block (banned_keys).
|
||||||
|
def = this->add("flush_multiplier_fast", coFloats);
|
||||||
|
def->label = L("Flush multiplier (Fast mode)");
|
||||||
|
def->tooltip = L("The flush multiplier used in fast purge mode.");
|
||||||
|
def->set_default_value(new ConfigOptionFloats{1.2});
|
||||||
|
|
||||||
// BBS
|
// BBS
|
||||||
def = this->add("prime_volume", coFloat);
|
def = this->add("prime_volume", coFloat);
|
||||||
def->label = L("Prime volume");
|
def->label = L("Prime volume");
|
||||||
@@ -6884,6 +7130,22 @@ void PrintConfigDef::init_fff_params()
|
|||||||
def->mode = comSimple;
|
def->mode = comSimple;
|
||||||
def->set_default_value(new ConfigOptionFloat(45.));
|
def->set_default_value(new ConfigOptionFloat(45.));
|
||||||
|
|
||||||
|
// Dual-extruder purge control: Default reproduces current behaviour
|
||||||
|
// (per-extruder flush_multiplier + filament_prime_volume); Saving reduces the prime volume,
|
||||||
|
// Fast selects flush_multiplier_fast + filament_flush_temp_fast. Default pvmDefault = inert.
|
||||||
|
def = this->add("prime_volume_mode", coEnum);
|
||||||
|
def->label = L("Prime volume mode");
|
||||||
|
def->tooltip = L("Selects how the wipe-tower prime and flush volumes are computed on multi-extruder printers.");
|
||||||
|
def->enum_keys_map = &ConfigOptionEnum<PrimeVolumeMode>::get_enum_values();
|
||||||
|
def->enum_values.push_back("Default");
|
||||||
|
def->enum_values.push_back("Saving");
|
||||||
|
def->enum_values.push_back("Fast");
|
||||||
|
def->enum_labels.push_back(L("Default"));
|
||||||
|
def->enum_labels.push_back(L("Saving"));
|
||||||
|
def->enum_labels.push_back(L("Fast"));
|
||||||
|
def->mode = comDevelop;
|
||||||
|
def->set_default_value(new ConfigOptionEnum<PrimeVolumeMode>(pvmDefault));
|
||||||
|
|
||||||
def = this->add("wipe_tower_x", coFloats);
|
def = this->add("wipe_tower_x", coFloats);
|
||||||
//def->label = L("Position X");
|
//def->label = L("Position X");
|
||||||
//def->tooltip = L("X coordinate of the left front corner of a wipe tower.");
|
//def->tooltip = L("X coordinate of the left front corner of a wipe tower.");
|
||||||
@@ -7340,6 +7602,162 @@ void PrintConfigDef::init_fff_params()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Multi-nozzle + pre-heat + nozzle-change (nc) config keys ----
|
||||||
|
// These back 6-nozzle cluster grouping, the pre-heat/pre-cool time model and wipe-tower
|
||||||
|
// nozzle-change handling. Defaults are no-ops for existing single-nozzle printers (all new
|
||||||
|
// readers gate on extruder_max_nozzle_count > 1).
|
||||||
|
def = this->add("machine_hotend_change_time", coFloat);
|
||||||
|
def->label = L("Hotend change time");
|
||||||
|
def->tooltip = L("Time to change hotend.");
|
||||||
|
def->sidetext = L("s");
|
||||||
|
def->min = 0;
|
||||||
|
def->mode = comAdvanced;
|
||||||
|
def->set_default_value(new ConfigOptionFloat(0.0));
|
||||||
|
|
||||||
|
// Printer-owned toggle selecting the time-aware grouping objective (flush + change/print
|
||||||
|
// time) over the flush-only objective. Default false == today's flush-only grouping, so
|
||||||
|
// absent/default is inert. Consumed by the grouping solver (SpeedInfo).
|
||||||
|
// Orca: pinned to comDevelop (rather than the default comSimple) to match the sibling multi-nozzle
|
||||||
|
// dev keys and keep it out of user selectors.
|
||||||
|
def = this->add("group_algo_with_time", coBool);
|
||||||
|
def->mode = comDevelop;
|
||||||
|
def->set_default_value(new ConfigOptionBool(false));
|
||||||
|
|
||||||
|
def = this->add("machine_prepare_compensation_time", coFloat);
|
||||||
|
def->mode = comDevelop;
|
||||||
|
def->set_default_value(new ConfigOptionFloat(260));
|
||||||
|
|
||||||
|
def = this->add("enable_pre_heating", coBool);
|
||||||
|
def->set_default_value(new ConfigOptionBool(false));
|
||||||
|
|
||||||
|
// Pre-heat injector context key. handle_hotend_as_extruder is read defensively by the injector;
|
||||||
|
// registered here (comDevelop, default off) as a known inert key.
|
||||||
|
def = this->add("handle_hotend_as_extruder", coBool);
|
||||||
|
def->mode = comDevelop;
|
||||||
|
def->set_default_value(new ConfigOptionBool(false));
|
||||||
|
|
||||||
|
// filament_max_temperature_drop_when_ec: inert (default {0}, passed into the injector context but
|
||||||
|
// never consumed). Registered inert-only (comDevelop); not wired into any estimator read, and not
|
||||||
|
// added to any per-variant expansion list.
|
||||||
|
def = this->add("filament_max_temperature_drop_when_ec", coFloats);
|
||||||
|
def->mode = comDevelop;
|
||||||
|
def->set_default_value(new ConfigOptionFloats{0});
|
||||||
|
|
||||||
|
def = this->add("hotend_cooling_rate", coFloats);
|
||||||
|
def->nullable = true;
|
||||||
|
def->set_default_value(new ConfigOptionFloatsNullable{2});
|
||||||
|
|
||||||
|
def = this->add("hotend_heating_rate", coFloats);
|
||||||
|
def->nullable = true;
|
||||||
|
def->set_default_value(new ConfigOptionFloatsNullable{2});
|
||||||
|
|
||||||
|
def = this->add("filament_change_length_nc", coFloats);
|
||||||
|
def->label = L("Hotend change");
|
||||||
|
def->tooltip = L("When changing the hotend, it is recommended to extrude a certain length of filament from the original nozzle. This helps minimize nozzle oozing.");
|
||||||
|
def->sidetext = L("mm");
|
||||||
|
def->min = 0;
|
||||||
|
def->mode = comAdvanced;
|
||||||
|
def->set_default_value(new ConfigOptionFloats{10});
|
||||||
|
|
||||||
|
def = this->add("filament_ramming_travel_time", coFloats);
|
||||||
|
def->label = L("Extruder change");
|
||||||
|
def->tooltip = L("To prevent oozing, the nozzle will perform a reverse travel movement for a certain period after "
|
||||||
|
"the ramming is complete. The setting define the travel time.");
|
||||||
|
def->mode = comAdvanced;
|
||||||
|
def->sidetext = "s";
|
||||||
|
def->min = 0;
|
||||||
|
def->nullable = true;
|
||||||
|
def->set_default_value(new ConfigOptionFloatsNullable{0});
|
||||||
|
|
||||||
|
def = this->add("filament_pre_cooling_temperature", coInts);
|
||||||
|
def->label = L("Extruder change");
|
||||||
|
def->tooltip = L("To prevent oozing, the nozzle temperature will be cooled during ramming. Therefore, the ramming time must be greater than the cooldown time. 0 means disabled.");
|
||||||
|
def->mode = comAdvanced;
|
||||||
|
def->sidetext = "°C";
|
||||||
|
def->min = 0;
|
||||||
|
def->nullable = true;
|
||||||
|
def->set_default_value(new ConfigOptionIntsNullable{0});
|
||||||
|
|
||||||
|
def = this->add("filament_ramming_volumetric_speed", coFloats);
|
||||||
|
def->label = L("Extruder change");
|
||||||
|
def->tooltip = L("The maximum volumetric speed for ramming before extruder change, where -1 means using the maximum volumetric speed.");
|
||||||
|
def->sidetext = L("mm³/s");
|
||||||
|
def->min = -1;
|
||||||
|
def->max = 200;
|
||||||
|
def->mode = comAdvanced;
|
||||||
|
def->nullable = true;
|
||||||
|
def->set_default_value(new ConfigOptionFloatsNullable{-1});
|
||||||
|
|
||||||
|
def = this->add("filament_ramming_travel_time_nc", coFloats);
|
||||||
|
def->label = L("Hotend change");
|
||||||
|
def->tooltip = L("To prevent oozing, the nozzle will perform a reverse travel movement for a certain period after "
|
||||||
|
"the ramming is complete. The setting define the travel time.");
|
||||||
|
def->mode = comAdvanced;
|
||||||
|
def->sidetext = "s";
|
||||||
|
def->min = 0;
|
||||||
|
def->nullable = true;
|
||||||
|
def->set_default_value(new ConfigOptionFloatsNullable{0});
|
||||||
|
|
||||||
|
def = this->add("filament_pre_cooling_temperature_nc", coInts);
|
||||||
|
def->label = L("Hotend change");
|
||||||
|
def->tooltip = L(
|
||||||
|
"To prevent oozing, the nozzle temperature will be cooled during ramming. Note: only a cooldown command and fan activation are triggered, reaching the target temperature is not guaranteed. 0 means disabled.");
|
||||||
|
def->mode = comAdvanced;
|
||||||
|
def->sidetext = "°C";
|
||||||
|
def->min = 0;
|
||||||
|
def->nullable = true;
|
||||||
|
def->set_default_value(new ConfigOptionIntsNullable{0});
|
||||||
|
|
||||||
|
def = this->add("filament_ramming_volumetric_speed_nc", coFloats);
|
||||||
|
def->label = L("Hotend change");
|
||||||
|
def->tooltip = L("The maximum volumetric speed for ramming before a hotend change, where -1 means using the maximum volumetric speed.");
|
||||||
|
def->sidetext = L("mm³/s");
|
||||||
|
def->min = -1;
|
||||||
|
def->max = 200;
|
||||||
|
def->mode = comAdvanced;
|
||||||
|
def->nullable = true;
|
||||||
|
def->set_default_value(new ConfigOptionFloatsNullable{-1});
|
||||||
|
|
||||||
|
def = this->add("filament_retract_length_nc", coFloats);
|
||||||
|
def->label = L("length when change hotend");
|
||||||
|
def->tooltip = L("When this retraction value is modified, it will be used as the amount of filament retracted "
|
||||||
|
"inside the hotend before changing hotends.");
|
||||||
|
def->sidetext = L("mm");
|
||||||
|
def->mode = comDevelop;
|
||||||
|
def->nullable = true;
|
||||||
|
def->min = 0;
|
||||||
|
def->max = 18;
|
||||||
|
def->set_default_value(new ConfigOptionFloatsNullable { 10. });
|
||||||
|
|
||||||
|
def = this->add("extruder_max_nozzle_count", coInts);
|
||||||
|
def->mode = comDevelop;
|
||||||
|
def->nullable = true;
|
||||||
|
def->set_default_value(new ConfigOptionIntsNullable{ 1 });
|
||||||
|
|
||||||
|
// Printer flag gating the fast-purge mode selector in the Multi-Filament
|
||||||
|
// UI. comDevelop, default false; no shipping profile sets it, so the UI stays hidden.
|
||||||
|
def = this->add("support_fast_purge_mode", coBool);
|
||||||
|
def->label = L("Support fast purge mode");
|
||||||
|
def->tooltip = L("Whether this printer supports fast purge mode with optimized temperature and multiplier.");
|
||||||
|
def->mode = comDevelop;
|
||||||
|
def->set_default_value(new ConfigOptionBool(false));
|
||||||
|
|
||||||
|
def = this->add("filament_prime_volume_nc", coFloats);
|
||||||
|
def->label = L("Hotend change");
|
||||||
|
def->tooltip = L("The volume of material required to prime the extruder for a hotend change on the tower.");
|
||||||
|
def->sidetext = L("mm³");
|
||||||
|
def->min = 1.0;
|
||||||
|
def->mode = comSimple;
|
||||||
|
def->set_default_value(new ConfigOptionFloats{60.});
|
||||||
|
|
||||||
|
def = this->add("filament_preheat_temperature_delta", coFloats);
|
||||||
|
def->label = L("Preheat temperature delta");
|
||||||
|
def->tooltip = L("Temperature delta applied during pre-heating before tool change.");
|
||||||
|
def->sidetext = "°C";
|
||||||
|
def->mode = comDevelop;
|
||||||
|
def->nullable = true;
|
||||||
|
def->set_default_value(new ConfigOptionFloatsNullable{0});
|
||||||
|
|
||||||
def = this->add("detect_narrow_internal_solid_infill", coBool);
|
def = this->add("detect_narrow_internal_solid_infill", coBool);
|
||||||
def->label = L("Detect narrow internal solid infills");
|
def->label = L("Detect narrow internal solid infills");
|
||||||
def->category = L("Strength");
|
def->category = L("Strength");
|
||||||
@@ -7389,13 +7807,14 @@ void PrintConfigDef::init_filament_option_keys()
|
|||||||
m_filament_option_keys = {
|
m_filament_option_keys = {
|
||||||
"filament_diameter", "min_layer_height", "max_layer_height","volumetric_speed_coefficients",
|
"filament_diameter", "min_layer_height", "max_layer_height","volumetric_speed_coefficients",
|
||||||
"retraction_length", "z_hop", "z_hop_types", "retract_lift_above", "retract_lift_below", "retract_lift_enforce", "retraction_speed", "deretraction_speed",
|
"retraction_length", "z_hop", "z_hop_types", "retract_lift_above", "retract_lift_below", "retract_lift_enforce", "retraction_speed", "deretraction_speed",
|
||||||
"retract_before_wipe", "retract_restart_extra", "retraction_minimum_travel", "wipe", "wipe_distance",
|
"retract_before_wipe", "filament_retract_length_nc", "retract_restart_extra", "retraction_minimum_travel", "wipe", "wipe_distance",
|
||||||
"retract_when_changing_layer", "retract_length_toolchange", "retract_restart_extra_toolchange", "filament_colour",
|
"retract_when_changing_layer", "retract_length_toolchange", "retract_restart_extra_toolchange", "filament_colour",
|
||||||
"default_filament_profile","retraction_distances_when_cut","long_retractions_when_cut"/*,"filament_seam_gap"*/
|
"default_filament_profile","retraction_distances_when_cut","long_retractions_when_cut"/*,"filament_seam_gap"*/
|
||||||
};
|
};
|
||||||
|
|
||||||
m_filament_retract_keys = {
|
m_filament_retract_keys = {
|
||||||
"deretraction_speed",
|
"deretraction_speed",
|
||||||
|
"filament_retract_length_nc",
|
||||||
"long_retractions_when_cut",
|
"long_retractions_when_cut",
|
||||||
"retract_before_wipe",
|
"retract_before_wipe",
|
||||||
"retract_lift_above",
|
"retract_lift_above",
|
||||||
@@ -8414,6 +8833,15 @@ std::set<std::string> print_options_with_variant = {
|
|||||||
std::set<std::string> filament_options_with_variant = {
|
std::set<std::string> filament_options_with_variant = {
|
||||||
"filament_flow_ratio",
|
"filament_flow_ratio",
|
||||||
"filament_max_volumetric_speed",
|
"filament_max_volumetric_speed",
|
||||||
|
// Per-variant ramming / pre-cooling / nozzle-change filament overrides
|
||||||
|
"filament_ramming_volumetric_speed",
|
||||||
|
"filament_pre_cooling_temperature",
|
||||||
|
"filament_ramming_travel_time",
|
||||||
|
"filament_ramming_volumetric_speed_nc",
|
||||||
|
"filament_pre_cooling_temperature_nc",
|
||||||
|
"filament_ramming_travel_time_nc",
|
||||||
|
"filament_retract_length_nc",
|
||||||
|
"filament_preheat_temperature_delta",
|
||||||
//"filament_extruder_id",
|
//"filament_extruder_id",
|
||||||
"filament_extruder_variant",
|
"filament_extruder_variant",
|
||||||
"filament_retraction_length",
|
"filament_retraction_length",
|
||||||
@@ -8461,7 +8889,8 @@ std::set<std::string> printer_extruder_options = {
|
|||||||
"extruder_printable_area",
|
"extruder_printable_area",
|
||||||
"extruder_printable_height",
|
"extruder_printable_height",
|
||||||
"min_layer_height",
|
"min_layer_height",
|
||||||
"max_layer_height"
|
"max_layer_height",
|
||||||
|
"extruder_max_nozzle_count" // Per-extruder max (sub-)nozzle count
|
||||||
};
|
};
|
||||||
|
|
||||||
std::set<std::string> printer_options_with_variant_1 = {
|
std::set<std::string> printer_options_with_variant_1 = {
|
||||||
@@ -8489,6 +8918,9 @@ std::set<std::string> printer_options_with_variant_1 = {
|
|||||||
"nozzle_type",
|
"nozzle_type",
|
||||||
"printer_extruder_id",
|
"printer_extruder_id",
|
||||||
"printer_extruder_variant",
|
"printer_extruder_variant",
|
||||||
|
// Per-variant hotend heat-up / cool-down rates (pre-heat time model)
|
||||||
|
"hotend_cooling_rate",
|
||||||
|
"hotend_heating_rate",
|
||||||
"nozzle_flush_dataset"
|
"nozzle_flush_dataset"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,14 @@ enum GCodeFlavor : unsigned char {
|
|||||||
gcfNoExtrusion
|
gcfNoExtrusion
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// How a filament is used across the model. Part of the multi-nozzle grouping data; not yet
|
||||||
|
// read by the shipping slicer — the nozzle-centric FilamentGroup engine consumes it.
|
||||||
|
enum FilamentUsageType {
|
||||||
|
SupportOnly,
|
||||||
|
ModelOnly,
|
||||||
|
Hybrid
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
enum class FuzzySkinType {
|
enum class FuzzySkinType {
|
||||||
None,
|
None,
|
||||||
@@ -418,24 +426,37 @@ enum ExtruderType {
|
|||||||
enum NozzleVolumeType {
|
enum NozzleVolumeType {
|
||||||
nvtStandard = 0,
|
nvtStandard = 0,
|
||||||
nvtHighFlow,
|
nvtHighFlow,
|
||||||
nvtMaxNozzleVolumeType = nvtHighFlow
|
nvtHybrid, // match-any / mixed sentinel; hidden from user selectors, kept out of the shipping slicer
|
||||||
|
nvtTPUHighFlow, // physical variant, used on H2D/H2DP 0.4 nozzles only
|
||||||
|
// Integer values are serialized as raw ints in 3mf plate metadata and device MQTT, so they MUST stay stable.
|
||||||
|
nvtMaxNozzleVolumeType = nvtTPUHighFlow
|
||||||
};
|
};
|
||||||
|
|
||||||
enum FilamentMapMode {
|
enum FilamentMapMode {
|
||||||
fmmAutoForFlush,
|
fmmAutoForFlush,
|
||||||
fmmAutoForMatch,
|
fmmAutoForMatch,
|
||||||
fmmManual,
|
fmmManual,
|
||||||
|
fmmNozzleManual, // Fully-manual filament->physical-nozzle mapping (filament_nozzle_map). Kept ordered right after fmmManual so every `< fmmManual` "is-auto" check stays correct.
|
||||||
fmmDefault
|
fmmDefault
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Dual-extruder purge control. Default reproduces the current
|
||||||
|
// per-extruder flush_multiplier + filament_prime_volume behaviour, so absent/default is inert.
|
||||||
|
// Saving -> reduce prime volume to 15 mm3; Fast -> use flush_multiplier_fast + filament_flush_temp_fast.
|
||||||
|
enum PrimeVolumeMode {
|
||||||
|
pvmDefault = 0,
|
||||||
|
pvmSaving,
|
||||||
|
pvmFast
|
||||||
|
};
|
||||||
|
|
||||||
extern std::string get_extruder_variant_string(ExtruderType extruder_type, NozzleVolumeType nozzle_volume_type);
|
extern std::string get_extruder_variant_string(ExtruderType extruder_type, NozzleVolumeType nozzle_volume_type);
|
||||||
|
|
||||||
static std::set<NozzleVolumeType> get_valid_nozzle_volume_type() {
|
static std::set<NozzleVolumeType> get_valid_nozzle_volume_type() {
|
||||||
std::set<NozzleVolumeType> type;
|
std::set<NozzleVolumeType> type;
|
||||||
for (int i = 0; i <= nvtMaxNozzleVolumeType; ++i) {
|
for (int i = 0; i <= nvtMaxNozzleVolumeType; ++i) {
|
||||||
auto t = static_cast<NozzleVolumeType>(i);
|
auto t = static_cast<NozzleVolumeType>(i);
|
||||||
// TODO: Orca: Support hybrid
|
// Hybrid is a "match-any / mixed" sentinel, never a user-selectable physical type.
|
||||||
//if (t == nvtHybrid) continue;
|
if (t == nvtHybrid) continue;
|
||||||
type.insert(t);
|
type.insert(t);
|
||||||
}
|
}
|
||||||
return type;
|
return type;
|
||||||
@@ -521,11 +542,17 @@ static std::string get_bed_temp_1st_layer_key(const BedType type)
|
|||||||
}
|
}
|
||||||
|
|
||||||
extern const std::vector<std::string> filament_extruder_override_keys;
|
extern const std::vector<std::string> filament_extruder_override_keys;
|
||||||
|
// Full override-key check incl. filament_retract_length_nc (defined outside the generator list).
|
||||||
|
extern bool is_filament_extruder_override_key(const std::string &opt_key);
|
||||||
|
|
||||||
// for parse extruder_ams_count
|
// for parse extruder_ams_count
|
||||||
extern std::vector<std::map<int, int>> get_extruder_ams_count(const std::vector<std::string> &strs);
|
extern std::vector<std::map<int, int>> get_extruder_ams_count(const std::vector<std::string> &strs);
|
||||||
extern std::vector<std::string> save_extruder_ams_count_to_string(const std::vector<std::map<int, int>> &extruder_ams_count);
|
extern std::vector<std::string> save_extruder_ams_count_to_string(const std::vector<std::map<int, int>> &extruder_ams_count);
|
||||||
|
|
||||||
|
// for parse extruder_nozzle_stats (per-extruder physical nozzle inventory by volume type)
|
||||||
|
extern std::vector<std::map<NozzleVolumeType, int>> get_extruder_nozzle_stats(const std::vector<std::string> &strs);
|
||||||
|
extern std::vector<std::string> save_extruder_nozzle_stats_to_string(const std::vector<std::map<NozzleVolumeType, int>> &extruder_nozzle_stats);
|
||||||
|
|
||||||
#define CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(NAME) \
|
#define CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(NAME) \
|
||||||
template<> const t_config_enum_names& ConfigOptionEnum<NAME>::get_enum_names(); \
|
template<> const t_config_enum_names& ConfigOptionEnum<NAME>::get_enum_names(); \
|
||||||
template<> const t_config_enum_values& ConfigOptionEnum<NAME>::get_enum_values();
|
template<> const t_config_enum_values& ConfigOptionEnum<NAME>::get_enum_values();
|
||||||
@@ -1297,6 +1324,12 @@ PRINT_CONFIG_CLASS_DEFINE(
|
|||||||
((ConfigOptionFloats, machine_min_travel_rate))
|
((ConfigOptionFloats, machine_min_travel_rate))
|
||||||
// M205 S... [mm/sec]
|
// M205 S... [mm/sec]
|
||||||
((ConfigOptionFloats, machine_min_extruding_rate))
|
((ConfigOptionFloats, machine_min_extruding_rate))
|
||||||
|
// Bedslinger mass/force model: drive the per-layer Y-axis
|
||||||
|
// acceleration limit (curr_y_acceleration_limit) and the printed-mass check.
|
||||||
|
// Default 0 => inactive for every existing printer (mass model reads them as disabled).
|
||||||
|
((ConfigOptionFloat, machine_max_force_Y))
|
||||||
|
((ConfigOptionFloat, machine_bed_mass_Y))
|
||||||
|
((ConfigOptionFloat, machine_max_printed_mass))
|
||||||
|
|
||||||
//resonance avoidance ported from qidi slicer
|
//resonance avoidance ported from qidi slicer
|
||||||
((ConfigOptionBool, resonance_avoidance))
|
((ConfigOptionBool, resonance_avoidance))
|
||||||
@@ -1351,6 +1384,7 @@ PRINT_CONFIG_CLASS_DEFINE(
|
|||||||
((ConfigOptionStrings, filament_vendor))
|
((ConfigOptionStrings, filament_vendor))
|
||||||
((ConfigOptionBools, filament_is_support))
|
((ConfigOptionBools, filament_is_support))
|
||||||
((ConfigOptionInts, filament_printable))
|
((ConfigOptionInts, filament_printable))
|
||||||
|
((ConfigOptionInts, filament_extruder_compatibility))
|
||||||
((ConfigOptionFloats, filament_change_length))
|
((ConfigOptionFloats, filament_change_length))
|
||||||
((ConfigOptionFloats, filament_cost))
|
((ConfigOptionFloats, filament_cost))
|
||||||
((ConfigOptionStrings, default_filament_colour))
|
((ConfigOptionStrings, default_filament_colour))
|
||||||
@@ -1367,6 +1401,8 @@ PRINT_CONFIG_CLASS_DEFINE(
|
|||||||
((ConfigOptionIntsNullable, nozzle_flush_dataset))
|
((ConfigOptionIntsNullable, nozzle_flush_dataset))
|
||||||
((ConfigOptionFloatsNullable, filament_flush_volumetric_speed))
|
((ConfigOptionFloatsNullable, filament_flush_volumetric_speed))
|
||||||
((ConfigOptionIntsNullable, filament_flush_temp))
|
((ConfigOptionIntsNullable, filament_flush_temp))
|
||||||
|
// Fast-purge flush temperature; consumed only when prime_volume_mode==pvmFast.
|
||||||
|
((ConfigOptionIntsNullable, filament_flush_temp_fast))
|
||||||
// BBS
|
// BBS
|
||||||
((ConfigOptionBool, scan_first_layer))
|
((ConfigOptionBool, scan_first_layer))
|
||||||
((ConfigOptionEnum<PowerLossRecoveryMode>, enable_power_loss_recovery))
|
((ConfigOptionEnum<PowerLossRecoveryMode>, enable_power_loss_recovery))
|
||||||
@@ -1428,11 +1464,13 @@ PRINT_CONFIG_CLASS_DEFINE(
|
|||||||
((ConfigOptionInt, nozzle_hrc))
|
((ConfigOptionInt, nozzle_hrc))
|
||||||
((ConfigOptionBool, auxiliary_fan))
|
((ConfigOptionBool, auxiliary_fan))
|
||||||
((ConfigOptionBool, support_air_filtration))
|
((ConfigOptionBool, support_air_filtration))
|
||||||
|
((ConfigOptionBool, cooling_filter_enabled))
|
||||||
((ConfigOptionEnum<PrinterStructure>,printer_structure))
|
((ConfigOptionEnum<PrinterStructure>,printer_structure))
|
||||||
((ConfigOptionBool, support_chamber_temp_control))
|
((ConfigOptionBool, support_chamber_temp_control))
|
||||||
((ConfigOptionEnumsGeneric, extruder_type))
|
((ConfigOptionEnumsGeneric, extruder_type))
|
||||||
((ConfigOptionEnumsGeneric, nozzle_volume_type))
|
((ConfigOptionEnumsGeneric, nozzle_volume_type))
|
||||||
((ConfigOptionStrings, extruder_ams_count))
|
((ConfigOptionStrings, extruder_ams_count))
|
||||||
|
((ConfigOptionStrings, extruder_nozzle_stats))
|
||||||
((ConfigOptionInts, printer_extruder_id))
|
((ConfigOptionInts, printer_extruder_id))
|
||||||
((ConfigOptionInt, master_extruder_id))
|
((ConfigOptionInt, master_extruder_id))
|
||||||
((ConfigOptionStrings, printer_extruder_variant))
|
((ConfigOptionStrings, printer_extruder_variant))
|
||||||
@@ -1490,6 +1528,26 @@ PRINT_CONFIG_CLASS_DEFINE(
|
|||||||
((ConfigOptionStrings, small_area_infill_flow_compensation_model))
|
((ConfigOptionStrings, small_area_infill_flow_compensation_model))
|
||||||
|
|
||||||
((ConfigOptionBool, has_scarf_joint_seam))
|
((ConfigOptionBool, has_scarf_joint_seam))
|
||||||
|
|
||||||
|
// Multi-nozzle + pre-heating + nozzle-change (nc) keys. Defaults are no-ops for existing
|
||||||
|
// single-nozzle printers; new slicing paths gate on extruder_max_nozzle_count > 1.
|
||||||
|
((ConfigOptionFloat, machine_hotend_change_time))
|
||||||
|
((ConfigOptionFloat, machine_prepare_compensation_time))
|
||||||
|
((ConfigOptionBool, enable_pre_heating))
|
||||||
|
((ConfigOptionFloatsNullable, hotend_cooling_rate))
|
||||||
|
((ConfigOptionFloatsNullable, hotend_heating_rate))
|
||||||
|
((ConfigOptionFloats, filament_change_length_nc))
|
||||||
|
((ConfigOptionFloatsNullable, filament_ramming_travel_time))
|
||||||
|
((ConfigOptionIntsNullable, filament_pre_cooling_temperature))
|
||||||
|
((ConfigOptionFloatsNullable, filament_ramming_volumetric_speed))
|
||||||
|
((ConfigOptionFloatsNullable, filament_ramming_travel_time_nc))
|
||||||
|
((ConfigOptionIntsNullable, filament_pre_cooling_temperature_nc))
|
||||||
|
((ConfigOptionFloatsNullable, filament_ramming_volumetric_speed_nc))
|
||||||
|
((ConfigOptionFloatsNullable, filament_retract_length_nc))
|
||||||
|
((ConfigOptionIntsNullable, extruder_max_nozzle_count))
|
||||||
|
// Printer flag: whether the printer offers the fast-purge mode selector.
|
||||||
|
// Default false; no shipping profile sets it, so the fast-purge UI stays hidden.
|
||||||
|
((ConfigOptionBool, support_fast_purge_mode))
|
||||||
)
|
)
|
||||||
|
|
||||||
// This object is mapped to Perl as Slic3r::Config::Print.
|
// This object is mapped to Perl as Slic3r::Config::Print.
|
||||||
@@ -1636,7 +1694,15 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE(
|
|||||||
|
|
||||||
// BBS: wipe tower is only used for priming
|
// BBS: wipe tower is only used for priming
|
||||||
((ConfigOptionFloat, prime_volume))
|
((ConfigOptionFloat, prime_volume))
|
||||||
|
// Nozzle-change (nc) prime volume + pre-heat delta
|
||||||
|
((ConfigOptionFloats, filament_prime_volume_nc))
|
||||||
|
((ConfigOptionFloatsNullable, filament_preheat_temperature_delta))
|
||||||
((ConfigOptionFloats, flush_multiplier))
|
((ConfigOptionFloats, flush_multiplier))
|
||||||
|
// Fast-purge mode. Kept out of the g-code config block (banned_keys in
|
||||||
|
// GCode::append_full_config) so registering them leaves the shipping fleet's g-code byte-identical;
|
||||||
|
// consumed only on the prime_volume_mode==pvmFast / pvmSaving branch (default pvmDefault = inert).
|
||||||
|
((ConfigOptionEnum<PrimeVolumeMode>, prime_volume_mode))
|
||||||
|
((ConfigOptionFloats, flush_multiplier_fast))
|
||||||
((ConfigOptionFloat, z_offset))
|
((ConfigOptionFloat, z_offset))
|
||||||
// BBS: project filaments
|
// BBS: project filaments
|
||||||
((ConfigOptionFloats, filament_colour_new))
|
((ConfigOptionFloats, filament_colour_new))
|
||||||
@@ -1644,6 +1710,8 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE(
|
|||||||
((ConfigOptionFloatsNullable, nozzle_volume))
|
((ConfigOptionFloatsNullable, nozzle_volume))
|
||||||
((ConfigOptionPoints, start_end_points))
|
((ConfigOptionPoints, start_end_points))
|
||||||
((ConfigOptionEnum<TimelapseType>, timelapse_type))
|
((ConfigOptionEnum<TimelapseType>, timelapse_type))
|
||||||
|
// Corexy farthest-point timelapse (default false → inert for existing printers)
|
||||||
|
((ConfigOptionBool, farthest_point_timelapse))
|
||||||
((ConfigOptionString, thumbnails))
|
((ConfigOptionString, thumbnails))
|
||||||
// BBS: move from PrintObjectConfig
|
// BBS: move from PrintObjectConfig
|
||||||
((ConfigOptionBool, independent_support_layer_height))
|
((ConfigOptionBool, independent_support_layer_height))
|
||||||
|
|||||||
@@ -102,6 +102,9 @@ const std::string& var_dir();
|
|||||||
// Return a full resource path for a file_name.
|
// Return a full resource path for a file_name.
|
||||||
std::string var(const std::string &file_name);
|
std::string var(const std::string &file_name);
|
||||||
|
|
||||||
|
// Snap a nozzle diameter to the closest supported value and format it as a string (e.g. 0.4 -> "0.4").
|
||||||
|
std::string format_diameter_to_str(double diameter, int precision = 1);
|
||||||
|
|
||||||
// Set a path with various static definition data (for example the initial config bundles).
|
// Set a path with various static definition data (for example the initial config bundles).
|
||||||
void set_resources_dir(const std::string &path);
|
void set_resources_dir(const std::string &path);
|
||||||
// Return a full path to the resources directory.
|
// Return a full path to the resources directory.
|
||||||
|
|||||||
@@ -83,6 +83,8 @@ public:
|
|||||||
NozzleVolumeType nozzle_volume_type;
|
NozzleVolumeType nozzle_volume_type;
|
||||||
BedType bed_type;
|
BedType bed_type;
|
||||||
float nozzle_diameter;
|
float nozzle_diameter;
|
||||||
|
int nozzle_pos_id{-1};
|
||||||
|
std::string nozzle_sn;
|
||||||
std::string filament_id;
|
std::string filament_id;
|
||||||
std::string setting_id;
|
std::string setting_id;
|
||||||
std::string name;
|
std::string name;
|
||||||
@@ -93,6 +95,8 @@ public:
|
|||||||
this->extruder_id = other.extruder_id;
|
this->extruder_id = other.extruder_id;
|
||||||
this->nozzle_volume_type = other.nozzle_volume_type;
|
this->nozzle_volume_type = other.nozzle_volume_type;
|
||||||
this->nozzle_diameter = other.nozzle_diameter;
|
this->nozzle_diameter = other.nozzle_diameter;
|
||||||
|
this->nozzle_pos_id = other.nozzle_pos_id;
|
||||||
|
this->nozzle_sn = other.nozzle_sn;
|
||||||
this->filament_id = other.filament_id;
|
this->filament_id = other.filament_id;
|
||||||
this->setting_id = other.setting_id;
|
this->setting_id = other.setting_id;
|
||||||
this->name = other.name;
|
this->name = other.name;
|
||||||
@@ -123,7 +127,9 @@ public:
|
|||||||
int ams_id = 0;
|
int ams_id = 0;
|
||||||
int slot_id = 0;
|
int slot_id = 0;
|
||||||
int cali_idx = -1;
|
int cali_idx = -1;
|
||||||
|
int nozzle_pos_id = -1; //-1 means no nozzle pos
|
||||||
float nozzle_diameter;
|
float nozzle_diameter;
|
||||||
|
std::string nozzle_sn;
|
||||||
std::string filament_id;
|
std::string filament_id;
|
||||||
std::string setting_id;
|
std::string setting_id;
|
||||||
std::string name;
|
std::string name;
|
||||||
@@ -140,7 +146,9 @@ struct PACalibIndexInfo
|
|||||||
int ams_id = 0;
|
int ams_id = 0;
|
||||||
int slot_id = 0;
|
int slot_id = 0;
|
||||||
int cali_idx = -1; // -1 means default
|
int cali_idx = -1; // -1 means default
|
||||||
|
int nozzle_pos_id = -1; //-1 means no nozzle pos
|
||||||
float nozzle_diameter;
|
float nozzle_diameter;
|
||||||
|
std::string nozzle_sn;
|
||||||
std::string filament_id;
|
std::string filament_id;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -148,7 +156,9 @@ struct PACalibExtruderInfo
|
|||||||
{
|
{
|
||||||
int extruder_id = 0;
|
int extruder_id = 0;
|
||||||
NozzleVolumeType nozzle_volume_type;
|
NozzleVolumeType nozzle_volume_type;
|
||||||
|
int nozzle_pos_id = -1; //-1 means no nozzle pos
|
||||||
float nozzle_diameter;
|
float nozzle_diameter;
|
||||||
|
std::string nozzle_sn;
|
||||||
std::string filament_id = "";
|
std::string filament_id = "";
|
||||||
bool use_extruder_id{true};
|
bool use_extruder_id{true};
|
||||||
bool use_nozzle_volume_type{true};
|
bool use_nozzle_volume_type{true};
|
||||||
|
|||||||
@@ -8,6 +8,10 @@
|
|||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
#include <sstream>
|
||||||
|
#include <iomanip>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
#include "format.hpp"
|
#include "format.hpp"
|
||||||
#include "Platform.hpp"
|
#include "Platform.hpp"
|
||||||
@@ -1497,6 +1501,15 @@ std::string format_memsize(size_t bytes, unsigned int decimals)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string format_diameter_to_str(double diameter, int precision)
|
||||||
|
{
|
||||||
|
double candidates[] = {0.2, 0.4, 0.6, 0.8};
|
||||||
|
double best = *std::min_element(std::begin(candidates), std::end(candidates), [diameter](double a, double b) { return std::abs(a - diameter) < std::abs(b - diameter); });
|
||||||
|
std::ostringstream oss;
|
||||||
|
oss << std::fixed << std::setprecision(precision) << best;
|
||||||
|
return oss.str();
|
||||||
|
}
|
||||||
|
|
||||||
// Returns platform-specific string to be used as log output or parsed in SysInfoDialog.
|
// Returns platform-specific string to be used as log output or parsed in SysInfoDialog.
|
||||||
// The latter parses the string with (semi)colons as separators, it should look about as
|
// The latter parses the string with (semi)colons as separators, it should look about as
|
||||||
// "desc1: value1; desc2: value2" or similar (spaces should not matter).
|
// "desc1: value1; desc2: value2" or similar (spaces should not matter).
|
||||||
|
|||||||
Reference in New Issue
Block a user