Merge branch 'upstream/main' into harktech/k2-discovery

Consolidates #13744 (CFS filament sync) into this PR per maintainer
request and resyncs with main. Conflicts resolved:

* src/slic3r/GUI/Plater.cpp -- parallel else-if added by both sides
  (htCrealityPrint + flashforge_local_api branches); kept both.
* src/slic3r/GUI/PhysicalPrinterDialog.cpp -- both sides added a
  host-specific Browse dialog (Creality DNS-SD + Flashforge); kept
  both as early-return branches, fall through to BonjourDialog.
* src/slic3r/GUI/PrintHostDialogs.hpp -- parallel class declarations
  (CrealityPrintHostSendDialog + FlashforgePrintHostSendDialog);
  kept both.
This commit is contained in:
grant0013 committed 2026-05-28 07:56:06 +00:00
commit 8dfd480c52
1008 files changed
+31791 -25415

No files matched your search

+8
View File
@@ -23,6 +23,7 @@
#include <cstring>
#include <iostream>
#include <math.h>
#include <csignal>
#if defined(__linux__) || defined(__LINUX__)
#include <condition_variable>
@@ -7459,6 +7460,13 @@ extern "C" {
#else /* _MSC_VER */
int main(int argc, char **argv)
{
#ifndef _WIN32
// Ignore SIGPIPE so a write to a closed socket (e.g. a dropped printer
// network connection) returns EPIPE to the caller instead of terminating
// the whole process. Without this, losing the printer link kills
// OrcaSlicer with SIGPIPE (exit 141) and produces no crash report.
std::signal(SIGPIPE, SIG_IGN);
#endif
return CLI().run(argc, argv);
}
#endif /* _MSC_VER */
+10 -3
View File
@@ -118,6 +118,9 @@ public:
virtual void reverse() = 0;
virtual Point first_point() const = 0;
virtual Point last_point() const = 0;
virtual const Point3& first_point3() const = 0;
virtual const Point3& last_point3() const = 0;
// Produce a list of 2D polygons covered by the extruded paths, offsetted by the extrusion width.
// Increase the offset by scaled_epsilon to achieve an overlap, so a union will produce no gaps.
virtual void polygons_covered_by_width(Polygons &out, const float scaled_epsilon) const = 0;
@@ -253,9 +256,9 @@ public:
ExtrusionEntity* clone_move() override { return new ExtrusionPath(std::move(*this)); }
void reverse() override { this->polyline.reverse(); }
Point first_point() const override { return this->polyline.points.front().to_point(); }
Point3 first_point3() const { return this->polyline.points.front(); }
const Point3& first_point3() const override { return this->polyline.points.front(); }
Point last_point() const override { return this->polyline.points.back().to_point(); }
Point3 last_point3() const { return this->polyline.points.back(); }
const Point3& last_point3() const override { return this->polyline.points.back(); }
size_t size() const { return this->polyline.size(); }
bool empty() const { return this->polyline.empty(); }
bool is_closed() const { return ! this->empty() && this->polyline.points.front() == this->polyline.points.back(); }
@@ -403,7 +406,9 @@ public:
ExtrusionEntity* clone_move() override { return new ExtrusionMultiPath(std::move(*this)); }
void reverse() override;
Point first_point() const override { return this->paths.front().polyline.points.front().to_point(); }
const Point3& first_point3() const override { return this->paths.front().polyline.points.front(); }
Point last_point() const override { return this->paths.back().polyline.points.back().to_point(); }
const Point3& last_point3() const override { return this->paths.back().polyline.points.back(); }
size_t size() const { return this->paths.size(); }
bool empty() const { return this->paths.empty(); }
double length() const override;
@@ -459,7 +464,9 @@ public:
bool is_counter_clockwise() { return this->polygon().is_counter_clockwise(); }
void reverse() override;
Point first_point() const override { return this->paths.front().polyline.points.front().to_point(); }
const Point3& first_point3() const override { return this->paths.front().polyline.points.front(); }
Point last_point() const override { assert(this->first_point() == this->paths.back().polyline.points.back().to_point()); return this->first_point(); }
const Point3& last_point3() const override { assert(this->first_point3() == this->paths.back().polyline.points.back()); return this->first_point3(); }
Polygon polygon() const;
double length() const override;
bool split_at_vertex(const Point &point, const double scaled_epsilon = scaled<double>(0.001));
@@ -510,7 +517,7 @@ public:
#ifndef NDEBUG
bool validate() const {
assert(this->first_point() == this->paths.back().polyline.points.back());
assert(this->first_point3() == this->paths.back().polyline.points.back());
for (size_t i = 1; i < paths.size(); ++ i)
assert(this->paths[i - 1].polyline.points.back() == this->paths[i].polyline.points.front());
return true;
@@ -119,7 +119,10 @@ public:
{ return this->no_sort ? *this : chained_path_from(this->entities, start_near, role); }
void reverse() override;
Point first_point() const override { return this->entities.front()->first_point(); }
const Point3& first_point3() const override { return this->entities.front()->first_point3(); }
Point last_point() const override { return this->entities.back()->last_point(); }
const Point3& last_point3() const override { return this->entities.back()->last_point3(); }
// Produce a list of 2D polygons covered by the extruded paths, offsetted by the extrusion width.
// Increase the offset by scaled_epsilon to achieve an overlap, so a union will produce no gaps.
void polygons_covered_by_width(Polygons &out, const float scaled_epsilon) const override;
+9
View File
@@ -5726,6 +5726,9 @@ std::string GCode::extrude_loop(const ExtrusionLoop& loop_ref,
// if polyline was shorter than the clipping distance we'd get a null polyline, so
// we discard it in that case
const double seam_gap = scale_(m_config.seam_gap.get_abs_value(nozzle_diameter));
const bool seam_gap_applied = enable_seam_slope || m_enable_loop_clipping;
const double seam_gap_distance_mm = seam_gap_applied ? unscale_(seam_gap) : 0.0;
double seam_scarf_distance_mm = 0.0;
const double clip_length = m_enable_loop_clipping && !enable_seam_slope ? seam_gap : 0;
// get paths
@@ -5874,6 +5877,7 @@ std::string GCode::extrude_loop(const ExtrusionLoop& loop_ref,
const double slope_min_length = slope_entire_loop ? loop_length : std::min(m_config.seam_slope_min_length.value, loop_length);
const int slope_steps = m_config.seam_slope_steps;
const double slope_max_segment_length = scale_(slope_min_length / slope_steps);
seam_scarf_distance_mm = slope_min_length;
// Calculate the sloped loop
ExtrusionLoopSloped new_loop(paths, seam_gap, slope_min_length, slope_max_segment_length, start_slope_ratio, loop.loop_role());
@@ -5898,6 +5902,11 @@ std::string GCode::extrude_loop(const ExtrusionLoop& loop_ref,
}
}
if (description == "perimeter") {
m_processor.result().print_statistics.total_seam_gap_distance += static_cast<float>(seam_gap_distance_mm);
m_processor.result().print_statistics.total_seam_scarf_distance += static_cast<float>(seam_scarf_distance_mm);
}
// BBS
if (m_wipe.enable && FILAMENT_CONFIG(wipe)) {
m_wipe.path = Polyline();
+48 -18
View File
@@ -3821,9 +3821,10 @@ void GCodeProcessor::process_G1(const std::array<std::optional<double>, 4>& axes
return;
EMoveType type = move_type(delta_pos);
const float delta_xyz = std::sqrt(sqr(delta_pos[X]) + sqr(delta_pos[Y]) + sqr(delta_pos[Z]));
m_travel_dist = delta_xyz;
if (type == EMoveType::Extrude) {
const float delta_xyz = std::sqrt(sqr(delta_pos[X]) + sqr(delta_pos[Y]) + sqr(delta_pos[Z]));
m_travel_dist = delta_xyz;
float volume_extruded_filament = area_filament_cross_section * delta_pos[E];
float area_toolpath_cross_section = volume_extruded_filament / delta_xyz;
@@ -5445,6 +5446,11 @@ void GCodeProcessor::process_filament_change(int id)
int next_extruder_id = m_filament_maps[id];
int next_filament_id = id;
float extra_time = 0;
unsigned int filament_changes_delta = 0;
unsigned int extruder_changes_delta = 0;
float filament_load_time_delta = 0.0f;
float filament_unload_time_delta = 0.0f;
float tool_change_time_delta = 0.0f;
if (prev_filament_id == next_filament_id)
return;
@@ -5457,12 +5463,14 @@ void GCodeProcessor::process_filament_change(int id)
assert(prev_extruder_id != -1);
process_filaments(CustomGCode::ToolChange);
m_filament_id[next_extruder_id] = next_filament_id;
m_result.lock();
m_result.print_statistics.total_filament_changes += 1;
m_result.unlock();
extra_time += get_filament_unload_time(static_cast<size_t>(prev_filament_id));
filament_changes_delta += 1;
const float filament_unload_time = get_filament_unload_time(static_cast<size_t>(prev_filament_id));
extra_time += filament_unload_time;
filament_unload_time_delta += filament_unload_time;
m_time_processor.extruder_unloaded = false;
extra_time += get_filament_load_time(static_cast<size_t>(next_filament_id));
const float filament_load_time = get_filament_load_time(static_cast<size_t>(next_filament_id));
extra_time += filament_load_time;
filament_load_time_delta += filament_load_time;
}
else {
if (prev_extruder_id == -1) {
@@ -5470,7 +5478,9 @@ void GCodeProcessor::process_filament_change(int id)
m_extruder_id = next_extruder_id;
m_filament_id[next_extruder_id] = next_filament_id;
m_time_processor.extruder_unloaded = false;
extra_time += get_filament_load_time(static_cast<size_t>(next_filament_id));
const float filament_load_time = get_filament_load_time(static_cast<size_t>(next_filament_id));
extra_time += filament_load_time;
filament_load_time_delta += filament_load_time;
}
else {
//first process cache generated by last extruder
@@ -5481,24 +5491,39 @@ void GCodeProcessor::process_filament_change(int id)
//no filament in current extruder
m_filament_id[next_extruder_id] = next_filament_id;
m_time_processor.extruder_unloaded = false;
extra_time += get_filament_load_time(static_cast<size_t>(next_filament_id));
const float filament_load_time = get_filament_load_time(static_cast<size_t>(next_filament_id));
extra_time += filament_load_time;
filament_load_time_delta += filament_load_time;
}
else if (m_last_filament_id[next_extruder_id] != next_filament_id) {
//need to change filament
m_filament_id[next_extruder_id] = next_filament_id;
m_result.lock();
m_result.print_statistics.total_filament_changes += 1;
m_result.unlock();
extra_time += get_filament_unload_time(static_cast<size_t>(prev_filament_id));
filament_changes_delta += 1;
const float filament_unload_time = get_filament_unload_time(static_cast<size_t>(prev_filament_id));
extra_time += filament_unload_time;
filament_unload_time_delta += filament_unload_time;
m_time_processor.extruder_unloaded = false;
extra_time += get_filament_load_time(static_cast<size_t>(next_filament_id));
const float filament_load_time = get_filament_load_time(static_cast<size_t>(next_filament_id));
extra_time += filament_load_time;
filament_load_time_delta += filament_load_time;
}
m_result.lock();
m_result.print_statistics.total_extruder_changes++;
m_result.unlock();
extra_time += get_extruder_change_time(next_extruder_id);
extruder_changes_delta += 1;
const float tool_change_time = get_extruder_change_time(next_extruder_id);
extra_time += tool_change_time;
tool_change_time_delta += tool_change_time;
}
}
if (filament_changes_delta > 0 || extruder_changes_delta > 0 || filament_load_time_delta > 0.0f || filament_unload_time_delta > 0.0f || tool_change_time_delta > 0.0f) {
m_result.lock();
m_result.print_statistics.total_filament_changes += filament_changes_delta;
m_result.print_statistics.total_extruder_changes += extruder_changes_delta;
m_result.print_statistics.total_filament_load_time += filament_load_time_delta;
m_result.print_statistics.total_filament_unload_time += filament_unload_time_delta;
m_result.print_statistics.total_tool_change_time += tool_change_time_delta;
m_result.unlock();
}
m_cp_color.current = m_extruder_colors[next_filament_id];
simulate_st_synchronize(extra_time);
// store tool change move
@@ -5543,6 +5568,11 @@ void GCodeProcessor::store_move_vertex(EMoveType type, EMovePathType path_type,
m_line_id + 1 :
((type == EMoveType::Seam) ? m_last_line_id : m_line_id);
if (type == EMoveType::Travel) {
m_result.print_statistics.total_travel_moves++;
m_result.print_statistics.total_travel_distance += m_travel_dist;
}
m_result.moves.push_back({
m_last_line_id,
type,
+14
View File
@@ -78,6 +78,13 @@ class Print;
std::array<Mode, static_cast<size_t>(ETimeMode::Count)> modes;
unsigned int total_filament_changes;
unsigned int total_extruder_changes;
float total_filament_load_time;
float total_filament_unload_time;
float total_tool_change_time;
float total_travel_distance;
unsigned int total_travel_moves;
float total_seam_gap_distance;
float total_seam_scarf_distance;
PrintEstimatedStatistics() { reset(); }
@@ -95,6 +102,13 @@ class Print;
used_filaments_per_role.clear();
total_filament_changes = 0;
total_extruder_changes = 0;
total_filament_load_time = 0.0f;
total_filament_unload_time = 0.0f;
total_tool_change_time = 0.0f;
total_travel_distance = 0.0f;
total_travel_moves = 0;
total_seam_gap_distance = 0.0f;
total_seam_scarf_distance = 0.0f;
}
};
+3 -3
View File
@@ -864,10 +864,10 @@ std::string GCodeWriter::_spiral_travel_to_z(double z, const Vec2d &ij_offset, c
// Determine number of segments based on Resolution
// --------------------------------------------------------------------
const double ref_resolution = 0.01; // reference resolution in mm
const double ref_segments = 16.0; // reference number of segments at reference resolution
const double ref_segments = 8.0; // reference number of segments at reference resolution
// number of linear segments to use for approximating the arc, clamp between 4 and 24
const int segments = std::clamp(int(std::round(ref_segments * (ref_resolution / m_resolution))), 4, 24);
// number of linear segments to use for approximating the arc, clamp between 4 and 16
const int segments = std::clamp(int(std::round(ref_segments * (ref_resolution / m_resolution))), 4, 16);
// --------------------------------------------------------------------
const double px = m_pos(0) - m_x_offset; // take plate offset into consideration
+35 -30
View File
@@ -581,14 +581,6 @@ void Preset::load_info(const std::string& file)
catch (...) {
return;
}
//TODO: workaround for current info file convert, will remove it later
if (this->updated_time == 0) {
this->updated_time = (long long)Slic3r::Utils::get_current_time_utc();
//this->sync_info = "update";
BOOST_LOG_TRIVIAL(info) << boost::format("old info file, updated time to %1%") % this->updated_time;
save_info();
}
}
void Preset::save_info(std::string file)
@@ -1337,7 +1329,7 @@ static std::vector<std::string> s_Preset_printer_options {
"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",
"best_object_pos", "head_wrap_detect_zone",
"host_type", "print_host", "printhost_apikey", "bbl_use_printhost", "printer_agent",
"host_type", "print_host", "printhost_apikey", "flashforge_serial_number", "bbl_use_printhost", "printer_agent",
"print_host_webui",
"printhost_cafile","printhost_port","printhost_authorization_type",
"printhost_user", "printhost_password", "printhost_ssl_ignore_revoke", "thumbnails", "thumbnails_format",
@@ -2186,19 +2178,29 @@ bool PresetCollection::load_user_preset(std::string name, std::map<std::string,
}
}
// base_id
if (preset_values.find(BBL_JSON_KEY_BASE_ID) == preset_values.end()) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format("can not find base_id, not loading for user preset %1%") % canonical_name;
unlock();
return false;
// base_id is only required for presets inheriting from a parent. Root presets
// with an empty "inherits" field intentionally have no base_id.
std::string based_id;
const auto base_id = preset_values.find(BBL_JSON_KEY_BASE_ID);
if (base_id != preset_values.end()) {
based_id = base_id->second;
} else {
const auto inherits_iter = preset_values.find(BBL_JSON_KEY_INHERITS);
const bool preset_inherits_from_parent = inherits_iter != preset_values.end() && !inherits_iter->second.empty();
if (preset_inherits_from_parent) {
// This indicates that there is inherits exists but there is no base_id
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__
<< boost::format("can not find base_id, not loading for user preset %1%") % canonical_name;
unlock();
return false;
}
}
std::string cloud_base_id = preset_values[BBL_JSON_KEY_BASE_ID];
//filament_id
std::string cloud_filament_id;
if ((m_type == Preset::TYPE_FILAMENT) && preset_values.find(BBL_JSON_KEY_FILAMENT_ID) != preset_values.end()) {
cloud_filament_id = preset_values[BBL_JSON_KEY_FILAMENT_ID];
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " " << canonical_name << " filament_id: " << cloud_filament_id << " base_id: " << cloud_base_id;
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " " << canonical_name << " filament_id: " << cloud_filament_id << " base_id: " << based_id;
}
DynamicPrintConfig new_config, cloud_config;
@@ -2271,7 +2273,7 @@ bool PresetCollection::load_user_preset(std::string name, std::map<std::string,
iter->version = cloud_version.value();
iter->user_id = cloud_user_id;
iter->setting_id = cloud_setting_id;
iter->base_id = cloud_base_id;
iter->base_id = based_id;
iter->filament_id = cloud_filament_id;
update_alias(*iter);
//presets_loaded.emplace_back(*it->second);
@@ -2290,7 +2292,7 @@ bool PresetCollection::load_user_preset(std::string name, std::map<std::string,
preset.version = cloud_version.value();
preset.user_id = cloud_user_id;
preset.setting_id = cloud_setting_id;
preset.base_id = cloud_base_id;
preset.base_id = based_id;
preset.filament_id = cloud_filament_id;
update_alias(preset);
@@ -3651,20 +3653,22 @@ void PresetCollection::set_custom_preset_alias(Preset &preset)
// For printers, there is nothing to remove
// For prints AKA processes, the postfix should be kept
// Alias should be set here, as the preset name may be augmented further later (i.e., prefixing relative path for bundles)
std::string alias_name;
std::string preset_name = get_preset_bare_name(preset.name);
if (m_type == Preset::Type::TYPE_FILAMENT && preset.config.has(BBL_JSON_KEY_INHERITS) && preset.config.option<ConfigOptionString>(BBL_JSON_KEY_INHERITS)->value.empty()) {
if (alias_name.empty()) {
size_t end_pos = preset_name.find_first_of("@");
if (end_pos != std::string::npos) {
alias_name = preset_name.substr(0, end_pos);
boost::trim_right(alias_name);
}
std::string bare_preset_name = get_preset_bare_name(preset.name);
std::string alias_name = bare_preset_name;
const bool is_root_filament_preset =
m_type == Preset::Type::TYPE_FILAMENT &&
preset.config.has(BBL_JSON_KEY_INHERITS) &&
preset.config.option<ConfigOptionString>(BBL_JSON_KEY_INHERITS)->value.empty();
if (is_root_filament_preset) {
const size_t suffix_separator_pos = bare_preset_name.find_first_of("@");
if (suffix_separator_pos != std::string::npos) {
alias_name = bare_preset_name.substr(0, suffix_separator_pos);
boost::trim_right(alias_name);
if (alias_name.empty())
alias_name = bare_preset_name;
}
}
else {
alias_name = preset_name;
}
preset.alias = std::move(alias_name);
m_map_alias_to_profile_name[preset.alias].push_back(preset.name);
@@ -3835,6 +3839,7 @@ static std::vector<std::string> s_PhysicalPrinter_opts {
"print_host",
"print_host_webui",
"printhost_apikey",
"flashforge_serial_number",
"printhost_cafile",
"printhost_port",
"printhost_authorization_type",
+25 -4
View File
@@ -606,13 +606,24 @@ VendorType PresetBundle::get_current_vendor_type()
{
auto t = VendorType::Unknown;
auto config = &printers.get_edited_preset().config;
const auto* printer_model = config->opt<ConfigOptionString>("printer_model");
if (printer_model == nullptr) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": printer_model is "
<< (config->has("printer_model") ? "not a string" : "missing")
<< ", vendor type is Unknown";
return t;
}
std::string vendor_name;
for (auto vendor_profile : vendors) {
for (auto vendor_model : vendor_profile.second.models)
if (vendor_model.name == config->opt_string("printer_model")) {
for (const auto& vendor_profile : vendors) {
for (const auto& vendor_model : vendor_profile.second.models) {
if (vendor_model.name == printer_model->value) {
vendor_name = vendor_profile.first;
break;
}
}
if (!vendor_name.empty())
break;
}
if (!vendor_name.empty())
{
@@ -3779,7 +3790,17 @@ int PresetBundle::get_printer_extruder_count() const
{
const Preset& printer_preset = this->printers.get_edited_preset();
int count = printer_preset.config.option<ConfigOptionFloats>("nozzle_diameter")->values.size();
const auto* nozzle_diameter = printer_preset.config.option<ConfigOptionFloats>("nozzle_diameter");
if (nozzle_diameter == nullptr) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": nozzle_diameter is missing, using 1 extruder";
return 1;
}
if (nozzle_diameter->values.empty()) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": nozzle_diameter is empty, using 1 extruder";
return 1;
}
int count = int(nozzle_diameter->values.size());
return count;
}
+18
View File
@@ -3499,6 +3499,24 @@ std::string Print::output_filename(const std::string &filename_base) const
config.set_key_value("plate_number", new ConfigOptionString(get_plate_number_formatted()));
config.set_key_value("model_name", new ConfigOptionString(get_model_name()));
// the same type of filament contains multiple names, support exporting according to the filament name
auto full_print_config = this->full_print_config();
const ConfigOptionStrings* filament_settings_id = full_print_config.option<ConfigOptionStrings>("filament_settings_id");
std::string filament_name = "";
auto extruders = this->extruders(true);
if(!extruders.empty()) {
// first extruder is the default extruder
int extruder_id = extruders.front();
if(filament_settings_id->values.size() > extruder_id) {
filament_name = filament_settings_id->values[extruder_id];
}
}
size_t end_pos = filament_name.find_first_of("@");
if (end_pos != std::string::npos) {
filament_name = filament_name.substr(0, end_pos);
}
config.set_key_value("filament_name", new ConfigOptionString(filament_name));
return this->PrintBase::output_filename(m_config.filename_format.value, ".gcode", filename_base, &config);
}
+9 -2
View File
@@ -828,6 +828,13 @@ void PrintConfigDef::init_common_params()
def->cli = ConfigOptionDef::nocli;
def->set_default_value(new ConfigOptionString());
def = this->add("flashforge_serial_number", coString);
def->label = L("Serial Number");
def->tooltip = L("Flashforge local API requires the printer serial number.");
def->mode = comAdvanced;
def->cli = ConfigOptionDef::nocli;
def->set_default_value(new ConfigOptionString());
def = this->add("printhost_port", coString);
def->label = L("Printer");
def->tooltip = L("Name of the printer.");
@@ -4272,7 +4279,7 @@ void PrintConfigDef::init_fff_params()
def->min = 0;
def->max = 90;
def->mode = comExpert;
def->set_default_value(new ConfigOptionFloat(0));
def->set_default_value(new ConfigOptionFloat(35));
def = this->add("zaa_dont_alternate_fill_direction", coBool);
def->label = L("Don't alternate fill direction");
@@ -6770,7 +6777,7 @@ void PrintConfigDef::init_fff_params()
def->enum_labels.emplace_back(L("Cone"));
def->enum_labels.emplace_back(L("Rib"));
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionEnum<WipeTowerWallType>(wtwRectangle));
def->set_default_value(new ConfigOptionEnum<WipeTowerWallType>(wtwRib));
def = this->add("wipe_tower_extra_rib_length", coFloat);
def->label = L("Extra rib length");
+187 -53
View File
@@ -1703,30 +1703,46 @@ void PrintObject::detect_surfaces_type()
// Only iterate to the second-to-last layer, since we look at layer i+1.
if( (this->config().enable_extra_bridge_layer.value == eblApplyToAll) || (this->config().enable_extra_bridge_layer.value == eblExternalBridgeOnly)){
const size_t last = (m_layers.empty() ? 0 : m_layers.size() - 1);
tbb::parallel_for( tbb::blocked_range<size_t>(0, last), [this, region_id](const tbb::blocked_range<size_t> &range) {
// ORCA: Two-phase split (collect-then-apply) to eliminate a data race in the
// original single-phase parallel_for, where iteration `i` rewrote
// m_layers[i+1]->slices.surfaces via std::move while iteration `i+1` (running
// on an adjacent TBB block on another worker thread) was iterating that same
// Surfaces vector as its bot_surfs. Splitting into a read-only collect pass
// followed by a write-only apply pass removes the cross-iteration aliasing.
//
// Phase 1: read-only pass — collect each layer's stBottomBridge polygons into a
// per-layer cache. No surfaces are mutated, so concurrent reads are safe.
std::vector<Polygons> bridge_polys_per_layer(last);
tbb::parallel_for(tbb::blocked_range<size_t>(0, last), [this, region_id, &bridge_polys_per_layer](const tbb::blocked_range<size_t> &range) {
for (size_t i = range.begin(); i < range.end(); ++i) {
m_print->throw_if_canceled();
// Step 1: Find bridge polygons
// Current layer (i): Search for stBottomBridge polygons.
const Surfaces &bot_surfs = m_layers[i]->m_regions[region_id]->slices.surfaces;
// Next layer (i+1): The layer where stInternal polygons may be re-classified.
Surfaces &top_surfs = m_layers[i + 1]->m_regions[region_id]->slices.surfaces;
// Step 2: Collect the bridge polygons in the current layer region
Polygons polygons_bridge;
for (const Surface &sbot : bot_surfs) {
if (sbot.surface_type == stBottomBridge) {
polygons_append(polygons_bridge, to_polygons(sbot));
polygons_append(bridge_polys_per_layer[i], to_polygons(sbot));
}
}
}
});
// Phase 2: write pass — each iteration mutates only m_layers[i+1]->slices.surfaces
// and reads its bridge polygons from the precomputed cache. Different iterations
// never share a write target, so there is no aliasing between worker threads.
tbb::parallel_for( tbb::blocked_range<size_t>(0, last), [this, region_id, &bridge_polys_per_layer](const tbb::blocked_range<size_t> &range) {
for (size_t i = range.begin(); i < range.end(); ++i) {
m_print->throw_if_canceled();
// Step 1 + 2: pull the precomputed bridge polygons for the current source layer.
const Polygons &polygons_bridge = bridge_polys_per_layer[i];
// Step 3: Early termination of loop if no meaningfull bridge found
// No bridge polygons found, continue to the next layer
if (polygons_bridge.empty())
continue;
// Step 4: Bottom bridge polygons found - scan and create layer+1 bridge polygon
Surfaces &top_surfs = m_layers[i + 1]->m_regions[region_id]->slices.surfaces;
Surfaces new_surfaces;
new_surfaces.reserve(top_surfs.size());
@@ -1740,7 +1756,50 @@ void PrintObject::detect_surfaces_type()
// This would also skip generation of very short dual bridge layers (that are shorter than N perimeters), but these are unecessary as the bridge distance is
// We could reduce this slightly to account for innacurcies in the clipping operation.
// TODO: Monitor GitHub issues to check whether second bridge layers are ommited where they should be generated. If yes, reduce the filtering distance
// ORCA: Same-layer-top guard.
//
// Collect every stTop polygon present at layer i+1 (this region) and
// expand it by the same `offset_distance` used by the bridge filter
// above. Note that `offset_distance` here is the full wall-band
// distance for the region (external wall width + all configured
// internal wall widths, i.e. external + (wall_loops - 1) × internal),
// not a single perimeter width. Any candidate second-bridge area that
// falls under this expanded mask will be subtracted out below.
//
// Why this exists: detect_surfaces_type() classifies a layer's "top"
// surfaces as the geometry that is not covered by the layer above. Those
// stTop regions often have small stInternal islands embedded in them.
// The pre-existing wall-band filter (shrink_ex/offset_ex by
// offset_distance) is supposed to throw those tiny islands away, but
// its result is sensitive to Clipper's floating-point order of
// operations: on macOS ARM the filter eats them, on Windows/Intel it
// doesn't. Visible bridges then show up scattered across the printed
// top surface.
//
// Expanding stTop by offset_distance and subtracting it from the
// overlap makes the decision platform-independent: an island fully
// surrounded by stTop disappears regardless of which Clipper happens
// to be doing the math, while large stInternal regions away from the
// top survive intact (the expansion only nibbles the wall-band depth
// inward).
//
// Keep ExPolygons throughout so that any holes inside an stTop surface
// are offset with the correct sign (positive offset shrinks holes /
// grows the solid region). Using Polygons + expand() would treat the
// contour and each hole as independent polygons and could distort the
// mask.
ExPolygons same_layer_top_expanded;
{
ExPolygons same_layer_top;
for (const Surface &s : top_surfs) {
if (s.surface_type == stTop)
same_layer_top.push_back(s.expolygon);
}
if (! same_layer_top.empty())
same_layer_top_expanded = offset_ex(same_layer_top, offset_distance);
}
// For each surface in the layer above
for (Surface &s_up : top_surfs) {
// Only reclassify stInternal polygons (i.e. what will become later solid and sparse infill)
@@ -1755,7 +1814,13 @@ void PrintObject::detect_surfaces_type()
// Filter out the resulting candidate bridges based on size. First perform a shrink operation...
// ...followed by an expand operation to bring them back to the original size (positive offset)
overlap = offset_ex(shrink_ex(overlap, offset_distance), offset_distance);
// ORCA: subtract the expanded same-layer stTop mask (see comment above
// the mask construction). Drops stInternal islands fully surrounded by
// stTop at i+1 without affecting bridges that lie away from the top.
if (! same_layer_top_expanded.empty() && ! overlap.empty())
overlap = diff_ex(overlap, same_layer_top_expanded, ApplySafetyOffset::Yes);
// Now subtract the filtered new bridge layer from the remaining internal surfaces to create the new internal surface
ExPolygons remainder = diff_ex(p_up, overlap, ApplySafetyOffset::Yes);
@@ -2500,29 +2565,76 @@ void PrintObject::bridge_over_infill()
backup_surfaces[lidx] = {};
}
tbb::parallel_for(tbb::blocked_range<size_t>(0, this->layers().size()), [po = this, &backup_surfaces,
&surfaces_by_layer](tbb::blocked_range<size_t> r) {
// ORCA: Two-phase split (collect-then-apply) to eliminate a data race in
// the original single-phase parallel_for, where iteration `lidx` read
// m_layers[lidx-1]->regions()->fill_surfaces (its lower_layer) to compute
// `lightning_fill`, while iteration `lidx-1`, on an adjacent TBB block,
// was concurrently std::move-ing / emplace_back-ing into that same
// SurfaceCollection.
//
// Semantic choice — read ORIGINAL surfaces in Phase 1:
// The lower_layer that iteration `lidx` looks at is the *current* layer
// for iteration `lidx-1`, which Phase 2 will modify. We therefore have to
// pick whether Phase 1 sees that layer's pre-modification or
// post-modification state. We deliberately use the original (pre-modification)
// state, for two reasons:
// 1. The gate is asking "does the layer below use lightning sparse
// infill?" — that's a property of the layer's configuration plus its
// original sparse-infill classification. Phase 2's edits only carve
// a small overhang-aligned slice of sparse into solid; they do not
// change whether the layer is using lightning. The realistic gate
// answer is the same either way.
// 2. Each layer's solid expansion is meant to give its OWN lower_layer
// something to anchor lightning lines onto. Cascading the gate
// across layers ("skip mine because the layer below already did
// some") would invert that intent and force a serial Phase 2.
// The original racy code didn't actually implement either choice
// consistently — it returned whichever bytes happened to be in the
// vector when the thread arrived. This split makes the behaviour
// defined, deterministic across runs and platforms, and equivalent to
// a clean sequential implementation that gathered all gates first and
// then applied modifications.
//
// Phase 1: read-only — for each layer, determine whether its lower_layer
// has any stInternal area inside a lightning-infill region. That's the
// sole purpose of `lightning_fill` in the original code: a gate. Capture
// it once into a per-layer bool, derived from the original (unmodified)
// surfaces, so the gate is platform-independent and order-independent.
std::vector<char> needs_lightning_expansion(this->layers().size(), 0);
tbb::parallel_for(tbb::blocked_range<size_t>(0, this->layers().size()), [po = this, &surfaces_by_layer,
&needs_lightning_expansion](tbb::blocked_range<size_t> r) {
PRINT_OBJECT_TIME_LIMIT_MILLIS(PRINT_OBJECT_TIME_LIMIT_DEFAULT);
for (size_t lidx = r.begin(); lidx < r.end(); lidx++) {
if (surfaces_by_layer.find(lidx) == surfaces_by_layer.end())
continue;
Layer *layer = po->get_layer(lidx);
const Layer *layer = po->get_layer(lidx);
const Layer *lower_layer = layer->lower_layer;
if (lower_layer == nullptr)
continue;
Polygons lightning_fill;
for (const LayerRegion *region : lower_layer->regions()) {
if (region->region().config().sparse_infill_pattern == ipLightning) {
Polygons lf = to_polygons(region->fill_surfaces.filter_by_type(stInternal));
lightning_fill.insert(lightning_fill.end(), lf.begin(), lf.end());
if (region->region().config().sparse_infill_pattern == ipLightning
&& ! region->fill_surfaces.filter_by_type(stInternal).empty()) {
needs_lightning_expansion[lidx] = 1;
break;
}
}
}
});
if (lightning_fill.empty())
// Phase 2: write-only — each iteration mutates only m_layers[lidx]'s
// fill_surfaces and never reads any other layer's surfaces. Different
// iterations write to disjoint LayerRegion::fill_surfaces vectors, so
// there is no aliasing between worker threads.
tbb::parallel_for(tbb::blocked_range<size_t>(0, this->layers().size()), [po = this, &backup_surfaces,
&surfaces_by_layer,
&needs_lightning_expansion](tbb::blocked_range<size_t> r) {
PRINT_OBJECT_TIME_LIMIT_MILLIS(PRINT_OBJECT_TIME_LIMIT_DEFAULT);
for (size_t lidx = r.begin(); lidx < r.end(); lidx++) {
if (! needs_lightning_expansion[lidx])
continue;
Layer *layer = po->get_layer(lidx);
for (LayerRegion *region : layer->regions()) {
backup_surfaces[lidx][region] = std::move(
region->fill_surfaces); // Make backup copy by move!! so that pointers in candidate surfaces stay valid
@@ -3232,53 +3344,75 @@ void PrintObject::bridge_over_infill()
// === ORCA: Create a second internal bridge layer above the first bridge layer. ========================================================
// ======================================================================================================================================
if ( this->m_config.enable_extra_bridge_layer == eblApplyToAll || this->m_config.enable_extra_bridge_layer == eblInternalBridgeOnly) {
// Process layers in parallel up to second-to-last
tbb::parallel_for( tbb::blocked_range<size_t>(0, this->layers().size() - 1), [this](const tbb::blocked_range<size_t>& r) {
for (size_t lidx = r.begin(); lidx < r.end(); ++lidx)
{
// ORCA: Two-phase to eliminate the same data race as the external-bridge
// pass in detect_surfaces_type().
//
// Phase 1: read-only — for each layer, collect its stInternalBridge polygons and
// the matching bridge angle into a per-layer cache.
struct LayerBridgeCache {
ExPolygons polys;
double angle = 0.0;
float offset_distance = 0.0f;
bool has_bridge = false;
};
// Guard against size_t underflow when the object has 0 or 1 layers — there is
// no "layer above" to receive an extra bridge, so the whole pass is a no-op.
const size_t last = (this->layers().size() < 2) ? 0 : this->layers().size() - 1;
std::vector<LayerBridgeCache> caches(this->layers().size());
tbb::parallel_for( tbb::blocked_range<size_t>(0, last), [this, &caches](const tbb::blocked_range<size_t>& r) {
for (size_t lidx = r.begin(); lidx < r.end(); ++lidx) {
Layer* layer = this->get_layer(lidx);
// (A) Gather internal bridging surfaces in the current layer
ExPolygons bridging_current_layer;
double bridging_angle_current = 0.0;
bool found_any_bridge = false;
float offset_distance = 0.0f;
// Pick a region from which to retrieve the flow width
LayerBridgeCache &cache = caches[lidx];
if (!layer->regions().empty())
offset_distance = layer->regions().front()->flow(frSolidInfill).scaled_width();
cache.offset_distance = layer->regions().front()->flow(frSolidInfill).scaled_width();
for (LayerRegion *region : layer->regions()) {
for (const Surface &surf : region->fill_surfaces.surfaces) {
if (surf.surface_type == stInternalBridge) {
bridging_current_layer.push_back(surf.expolygon);
bridging_angle_current = surf.bridge_angle; // Store the last bridging angle of the current print object
found_any_bridge = true;
cache.polys.push_back(surf.expolygon);
cache.angle = surf.bridge_angle; // last bridge angle on this layer wins, matching prior behaviour
cache.has_bridge = true;
}
}
}
// If no bridging in this layer, continue with the next
if (!found_any_bridge || bridging_current_layer.empty())
if (!cache.has_bridge || cache.polys.empty()) {
cache.has_bridge = false;
continue;
// (B) Shrink-expand to remove trivial bridging areas
bridging_current_layer = offset_ex( shrink_ex(bridging_current_layer, offset_distance), offset_distance );
if (bridging_current_layer.empty())
continue; // all bridging was trivial, continue with the next layer
}
// Shrink-expand to remove trivial bridging areas
cache.polys = offset_ex(shrink_ex(cache.polys, cache.offset_distance), cache.offset_distance);
if (cache.polys.empty())
cache.has_bridge = false;
}
});
// Phase 2: write — each iteration mutates only m_layers[lidx+1]->fill_surfaces and
// pulls its bridge polygons from the precomputed cache. Different iterations never
// touch the same fill_surfaces vector, so there is no aliasing between workers.
tbb::parallel_for( tbb::blocked_range<size_t>(0, last), [this, &caches](const tbb::blocked_range<size_t>& r) {
for (size_t lidx = r.begin(); lidx < r.end(); ++lidx)
{
const LayerBridgeCache &cache = caches[lidx];
// If no bridging in this layer, continue with the next
if (!cache.has_bridge || cache.polys.empty())
continue;
// (C) If there is a next layer, identify overlapping stInternal & stInternalSolid areas and convert the overlap to stSecondInternalBridge
if (lidx + 1 < this->layers().size()) {
Layer* next_layer = this->get_layer(lidx + 1);
// second bridging angle is 90 degrees offset
double bridging_angle_second = bridging_angle_current + M_PI / 2.0;
double bridging_angle_second = cache.angle + M_PI / 2.0;
// Union the bridging polygons
ExPolygons bridging_union = union_safety_offset_ex(bridging_current_layer);
ExPolygons bridging_union = union_safety_offset_ex(cache.polys);
const float offset_distance = cache.offset_distance;
for (LayerRegion *next_region : next_layer->regions()) {
Surfaces next_new_surfaces;
Surfaces keep_surfaces;
+28 -18
View File
@@ -21,6 +21,32 @@ namespace Slic3r {
bool PrintObject::clip_multipart_objects = true;
bool PrintObject::infill_only_where_needed = false;
static coordf_t compute_slice_z(PrintObject* print_object, size_t i_layer, coordf_t lo, coordf_t hi)
{
bool zaa_active = false;
coordf_t z_offset = 0.0;
size_t num_regions = print_object->num_printing_regions();
for (size_t rid = 0; rid < num_regions; ++rid) {
const auto& rcfg = print_object->printing_region(rid).config();
if (rcfg.zaa_enabled) {
if (!zaa_active || rcfg.zaa_min_z < z_offset)
z_offset = rcfg.zaa_min_z;
zaa_active = true;
}
}
if (!zaa_active || i_layer == 0) {
return 0.5 * (lo + hi);
}
coordf_t slice_z = lo + z_offset;
if ((slice_z < lo && !is_approx(slice_z, lo)) || (slice_z > hi && !is_approx(slice_z, hi))) {
throw RuntimeError("Bad min Z value");
}
return slice_z;
}
LayerPtrs new_layers(
PrintObject *print_object,
// Object layers (pairs of bottom/top Z coordinate), without the raft.
@@ -34,24 +60,8 @@ LayerPtrs new_layers(
for (size_t i_layer = 0; i_layer < object_layers.size(); i_layer += 2) {
coordf_t lo = object_layers[i_layer];
coordf_t hi = object_layers[i_layer + 1];
coordf_t slice_z = 0.5 * (lo + hi);
bool zaa_active = false;
coordf_t z_offset = 0.0;
size_t num_regions = print_object->num_printing_regions();
for (size_t rid = 0; rid < num_regions; ++rid) {
const auto &rcfg = print_object->printing_region(rid).config();
if (rcfg.zaa_enabled) {
if (!zaa_active || rcfg.zaa_min_z < z_offset)
z_offset = rcfg.zaa_min_z;
zaa_active = true;
}
}
if (zaa_active) {
slice_z = lo + z_offset;
if ((slice_z < lo && !is_approx(slice_z, lo)) || (slice_z > hi && !is_approx(slice_z, hi))) {
throw RuntimeError("Bad min Z value");
}
}
coordf_t slice_z = compute_slice_z(print_object, i_layer, lo, hi);
Layer *layer = new Layer(id ++, print_object, hi - lo, hi + zmin, slice_z);
out.emplace_back(layer);
if (prev != nullptr) {
+17 -5
View File
@@ -46,11 +46,23 @@ void name_tbb_thread_pool_threads_set_locale();
template<class Fn>
inline boost::thread create_thread(boost::thread::attributes &attrs, Fn &&fn)
{
// Duplicating the stack allocation size of Thread Building Block worker
// threads of the thread pool: allocate 4MB on a 64bit system, allocate 2MB
// on a 32bit system by default.
attrs.set_stack_size((sizeof(void*) == 4) ? (2048 * 1024) : (4096 * 1024));
// Stack size for our worker threads. Originally duplicated TBB's pool
// default (4 MB), but the Emboss text-cut path calls into CGAL's
// Polygon_mesh_processing::corefine, which falls back from filtered
// interval arithmetic to exact rational arithmetic (mpq_class) on
// near-degenerate input, and the constrained 2D triangulation walker
// (Triangulation_2::march_locate_2D) can recurse deeply enough to
// exceed 4 MB on real models -- producing a SIGBUS at the next thread's
// stack guard page on macOS / Linux.
//
// 16 MB chosen as 4x defensive headroom over the observed crash
// threshold (n=1 reproducer at exactly 4 MB on macOS arm64). All three
// platforms defer-commit reserved stack pages: macOS / Linux mmap the
// stack and only fault in pages on touch; Boost.Thread on Win32 passes
// STACK_SIZE_PARAM_IS_A_RESERVATION to _beginthreadex, so the value is
// a reserve, not the initial commit. Resident memory therefore stays
// proportional to actual stack depth on every target.
attrs.set_stack_size(16 * 1024 * 1024);
return boost::thread{attrs, std::forward<Fn>(fn)};
}
+47 -11
View File
@@ -978,12 +978,51 @@ GLVolumeWithIdAndZList volumes_to_render(const GLVolumePtrs& volumes, GLVolumeCo
return list;
}
int GLVolumeCollection::get_selection_support_threshold_angle(bool &enable_support) const
// ORCA: Compute slope.normal_z for 3D overhang highlight directly from support settings.
// If support_threshold_angle is 0, use tree fallback angle (30 deg) for tree supports,
// and derive an equivalent angle from threshold overlap for normal supports.
float GLVolumeCollection::get_selection_support_normal_z() const
{
const DynamicPrintConfig& glb_cfg = GUI::wxGetApp().preset_bundle->prints.get_edited_preset().config;
enable_support = glb_cfg.opt_bool("enable_support");
int support_threshold_angle = glb_cfg.opt_int("support_threshold_angle");
return support_threshold_angle ;
const DynamicPrintConfig& glb_cfg = GUI::wxGetApp().preset_bundle->prints.get_edited_preset().config;
const auto& full_cfg = GUI::wxGetApp().preset_bundle->full_config();
const auto support_type = glb_cfg.opt_enum<SupportType>("support_type");
const int support_threshold_angle = glb_cfg.opt_int("support_threshold_angle");
double angle_rad;
if (support_threshold_angle > 0) {
// Match support generation: explicit threshold angles are treated as inclusive.
const int effective_support_threshold_angle = std::min(support_threshold_angle + 1, 89);
angle_rad = Geometry::deg2rad(static_cast<double>(effective_support_threshold_angle));
} else if (is_tree(support_type)) {
angle_rad = Geometry::deg2rad(30.0); // fallback value for tree supports
} else { // For normal supports, if the angle is set to 0, calculate normal_z from overlap.
const double layer_height = full_cfg.opt_float("layer_height");
const auto* nozzle_diameter_opt = full_cfg.option<ConfigOptionFloats>("nozzle_diameter");
const int wall_filament = full_cfg.opt_int("wall_filament");
const size_t nozzle_count = nozzle_diameter_opt->values.size();
const size_t wall_extruder_idx = (wall_filament > 0 && wall_filament <= static_cast<int>(nozzle_count))
? static_cast<size_t>(wall_filament - 1)
: 0; // Invalid extruder index falls back to extruder 1.
// Use wall extruder's nozzle diameter for better estimation of external perimeter width,
// which is more relevant to overhang printing than the default nozzle diameter.
const double nozzle_diameter = nozzle_diameter_opt->values[wall_extruder_idx];
double external_perimeter_width = full_cfg.get_abs_value("outer_wall_line_width", nozzle_diameter);
if (external_perimeter_width <= 0.0) {
external_perimeter_width = full_cfg.get_abs_value("line_width", nozzle_diameter);
if (external_perimeter_width <= 0.0)
external_perimeter_width = nozzle_diameter;
}
const double overlap_width = full_cfg.get_abs_value("support_threshold_overlap", external_perimeter_width);
const double lower_layer_offset = std::max(0.0, external_perimeter_width - overlap_width);
angle_rad = lower_layer_offset <= EPSILON ? Geometry::deg2rad(89.0) : std::atan(layer_height / lower_layer_offset);
}
return static_cast<float>(-std::cos(std::clamp(angle_rad, 0.0, Geometry::deg2rad(89.0))));
}
//BBS: add outline drawing logic
@@ -1019,6 +1058,8 @@ void GLVolumeCollection::render(GLVolumeCollection::ERenderType type,
if (disable_cullface)
glsafe(::glDisable(GL_CULL_FACE));
const float support_normal_z = get_selection_support_normal_z();
for (GLVolumeWithIdAndZ& volume : to_render) {
#if ENABLE_MODIFIERS_ALWAYS_TRANSPARENT
if (type == ERenderType::Transparent) {
@@ -1075,16 +1116,11 @@ void GLVolumeCollection::render(GLVolumeCollection::ERenderType type,
//use -1 ad a invalid type
shader->set_uniform("print_volume.type", -1);
}
bool enable_support;
int support_threshold_angle = get_selection_support_threshold_angle(enable_support);
float normal_z = -::cos(Geometry::deg2rad((float) support_threshold_angle));
shader->set_uniform("volume_world_matrix", volume.first->world_matrix());
shader->set_uniform("slope.actived", m_slope.isGlobalActive && !volume.first->is_modifier && !volume.first->is_wipe_tower);
shader->set_uniform("slope.volume_world_normal_matrix", static_cast<Matrix3f>(volume.first->world_matrix().matrix().block(0, 0, 3, 3).inverse().transpose().cast<float>()));
shader->set_uniform("slope.normal_z", normal_z);
shader->set_uniform("slope.normal_z", support_normal_z);
#if ENABLE_ENVIRONMENT_MAP
unsigned int environment_texture_id = GUI::wxGetApp().plater()->get_environment_texture_id();
+1 -1
View File
@@ -488,7 +488,7 @@ public:
GLVolume* new_toolpath_volume(const ColorRGBA& rgba);
GLVolume* new_nontoolpath_volume(const ColorRGBA& rgba);
int get_selection_support_threshold_angle(bool&) const;
float get_selection_support_normal_z() const;
// Render the volumes by OpenGL.
//BBS: add outline drawing logic
void render(ERenderType type,
+20 -3
View File
@@ -863,7 +863,15 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi
std::set<std::string> filament_id_set;
PresetBundle * preset_bundle = wxGetApp().preset_bundle;
std::ostringstream stream;
stream << std::fixed << std::setprecision(1) << obj->GetExtderSystem()->GetNozzleDiameter(0);
// Defensive: this dialog is opened only from StatusPanel (BBL-only) today, so the fallback fires
// only during the brief BBL startup window before firmware reports nozzle info. Without this,
// the "0.0" lookup string returns an empty set and the filament dropdown goes blank.
float machine_diameter = obj->GetExtderSystem()->GetNozzleDiameter(0);
if (machine_diameter == 0.0f && preset_bundle) {
const ConfigOption *opt = preset_bundle->printers.get_selected_preset().config.option("nozzle_diameter");
if (opt) machine_diameter = static_cast<const ConfigOptionFloats *>(opt)->values[0];
}
stream << std::fixed << std::setprecision(1) << machine_diameter;
std::string nozzle_diameter_str = stream.str();
std::set<std::string> printer_names = preset_bundle->get_printer_names_by_printer_type_and_nozzle(DevPrinterConfigUtil::get_printer_display_name(obj->printer_type), nozzle_diameter_str);
@@ -1101,8 +1109,17 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
PresetBundle* preset_bundle = wxGetApp().preset_bundle;
if (preset_bundle) {
std::ostringstream stream;
if (obj)
stream << std::fixed << std::setprecision(1) << obj->GetExtderSystem()->GetNozzleDiameter(0);
if (obj) {
// Defensive: this dialog is opened only from StatusPanel (BBL-only) today, so the fallback fires
// only during the brief BBL startup window before firmware reports nozzle info. Without this,
// the "0.0" lookup string returns an empty set and filament lookup yields no results.
float machine_diameter = obj->GetExtderSystem()->GetNozzleDiameter(0);
if (machine_diameter == 0.0f) {
const ConfigOption *opt = preset_bundle->printers.get_selected_preset().config.option("nozzle_diameter");
if (opt) machine_diameter = static_cast<const ConfigOptionFloats *>(opt)->values[0];
}
stream << std::fixed << std::setprecision(1) << machine_diameter;
}
std::string nozzle_diameter_str = stream.str();
std::set<std::string> printer_names = preset_bundle->get_printer_names_by_printer_type_and_nozzle(DevPrinterConfigUtil::get_printer_display_name(obj->printer_type),
nozzle_diameter_str);
+8 -1
View File
@@ -644,7 +644,14 @@ wxArrayString NewCalibrationHistoryDialog::get_all_filaments(const MachineObject
std::set<std::string> filament_id_set;
std::set<std::string> printer_names;
std::ostringstream stream;
stream << std::fixed << std::setprecision(1) << obj->GetExtderSystem()->GetNozzleDiameter(0);
// If the machine didn't report a nozzle diameter (0.0 = unknown), fall back to the currently
// selected printer preset so the filament list isn't empty.
float machine_diameter = obj->GetExtderSystem()->GetNozzleDiameter(0);
if (machine_diameter == 0.0f && preset_bundle) {
const ConfigOption *opt = preset_bundle->printers.get_selected_preset().config.option("nozzle_diameter");
if (opt) machine_diameter = static_cast<const ConfigOptionFloats *>(opt)->values[0];
}
stream << std::fixed << std::setprecision(1) << machine_diameter;
std::string nozzle_diameter_str = stream.str();
for (auto printer_it = preset_bundle->printers.begin(); printer_it != preset_bundle->printers.end(); printer_it++) {
+3
View File
@@ -761,6 +761,7 @@ void PressureAdvanceWizard::on_cali_start()
calib_info.extruder_id = preset_page->get_extruder_id(calib_info.ams_id);
calib_info.extruder_type = preset_page->get_extruder_type(calib_info.extruder_id);
calib_info.nozzle_volume_type = preset_page->get_nozzle_volume_type(calib_info.extruder_id);
calib_info.nozzle_diameter = preset_page->get_nozzle_diameter(calib_info.extruder_id);
calib_info.select_ams = std::to_string(selected_tray_id);
Preset *preset = selected_filaments.begin()->second;
Preset * temp_filament_preset = new Preset(preset->type, preset->name + "_temp");
@@ -1293,6 +1294,7 @@ void FlowRateWizard::on_cali_start(CaliPresetStage stage, float cali_value, Flow
calib_info.extruder_id = preset_page->get_extruder_id(calib_info.ams_id);
calib_info.extruder_type = preset_page->get_extruder_type(calib_info.extruder_id);
calib_info.nozzle_volume_type = preset_page->get_nozzle_volume_type(calib_info.extruder_id);
calib_info.nozzle_diameter = preset_page->get_nozzle_diameter(calib_info.extruder_id);
Preset* preset = selected_filaments.begin()->second;
temp_filament_preset = new Preset(preset->type, preset->name + "_temp");
temp_filament_preset->config = preset->config;
@@ -1708,6 +1710,7 @@ void MaxVolumetricSpeedWizard::on_cali_start()
calib_info.extruder_id = preset_page->get_extruder_id(calib_info.ams_id);
calib_info.extruder_type = preset_page->get_extruder_type(calib_info.extruder_id);
calib_info.nozzle_volume_type = preset_page->get_nozzle_volume_type(calib_info.extruder_id);
calib_info.nozzle_diameter = preset_page->get_nozzle_diameter(calib_info.extruder_id);
calib_info.filament_prest = selected_filaments.begin()->second;
std::map<int, DynamicPrintConfig> filament_list = preset_page->get_filament_ams_list();
calib_info.filament_color = filament_list[selected_filaments.begin()->first].opt_string("filament_colour", 0u);
+1 -1
View File
@@ -138,7 +138,7 @@ static const std::unordered_map<std::string, std::vector<std::string>> printer_m
"RatRig V-Core 4 IDEX 400", "RatRig V-Core 4 IDEX 400 COPY MODE", "RatRig V-Core 4 IDEX 400 MIRROR MODE", "RatRig V-Core 4 IDEX 500", "RatRig V-Core 4 IDEX 500 COPY MODE",
"RatRig V-Core 4 IDEX 500 MIRROR MODE"}},
{"re3D", {"re3D Gigabot 4", "re3D Gigabot 4 XLT", "re3D Terabot 4",
"re3D Gigabot X2", "re3D Gigabot X2 XLT", "re3D Terabot X2"}},
"re3D GigabotX 2", "re3D GigabotX 2 XLT", "re3D TerabotX 2"}},
{"RolohaunDesign", {"Rook MK1 LDO"}},
{"SecKit", {"SecKit SK-Tank", "Seckit Go3"}},
{"Snapmaker", {"Snapmaker J1", "Snapmaker A250", "Snapmaker A350", "Snapmaker A250 Dual", "Snapmaker A350 Dual",
+31 -9
View File
@@ -1,4 +1,7 @@
#include <nlohmann/json.hpp>
#include <exception>
#include "DevManager.h"
#include "DevUtil.h"
@@ -151,11 +154,15 @@ namespace Slic3r
{
keep_alive();
MachineObject* obj = this->get_selected_machine();
if (!obj) {
BOOST_LOG_TRIVIAL(warning) << "DeviceManager::check_pushing selected machine not found";
return;
}
std::chrono::system_clock::time_point start = std::chrono::system_clock::now();
auto internal = std::chrono::duration_cast<std::chrono::milliseconds>(start - obj->last_update_time);
if (obj && !obj->is_support_mqtt_alive)
if (!obj->is_support_mqtt_alive)
{
if (internal.count() > TIMEOUT_FOR_STRAT && internal.count() < 1000 * 60 * 60 * 300)
{
@@ -916,18 +923,33 @@ namespace Slic3r
const auto cloud_provider = Slic3r::GUI::wxGetApp().get_printer_cloud_provider();
if (Slic3r::GUI::wxGetApp().is_user_login(cloud_provider))
{
m_manager->check_pushing();
try
{
agent->refresh_connection(cloud_provider);
try {
m_manager->check_pushing();
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(error) << "DeviceManagerRefresher::on_timer check_pushing exception="
<< e.what();
} catch (...) {
BOOST_LOG_TRIVIAL(error) << "DeviceManagerRefresher::on_timer check_pushing unknown exception";
}
catch (...)
{
;
try {
agent->refresh_connection(cloud_provider);
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(error) << "DeviceManagerRefresher::on_timer refresh_connection exception="
<< e.what();
} catch (...) {
BOOST_LOG_TRIVIAL(error) << "DeviceManagerRefresher::on_timer refresh_connection unknown exception";
}
}
// certificate
agent->install_device_cert(obj->get_dev_id(), obj->is_lan_mode_printer());
try {
agent->install_device_cert(obj->get_dev_id(), obj->is_lan_mode_printer());
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(error) << "DeviceManagerRefresher::on_timer install_device_cert exception="
<< e.what();
} catch (...) {
BOOST_LOG_TRIVIAL(error) << "DeviceManagerRefresher::on_timer install_device_cert unknown exception";
}
}
}
+2 -2
View File
@@ -175,7 +175,7 @@ void DeviceErrorDialog::init_button_list()
init_button(PROBLEM_SOLVED_RESUME, _L("Problem Solved and Resume"));
init_button(TURN_OFF_FIRE_ALARM, _L("Got it, Turn off the Fire Alarm."));
init_button(RETRY_PROBLEM_SOLVED, _L("Retry (problem solved)"));
init_button(CANCLE, _L("Cancel"));
init_button(CANCEL, _L("Cancel"));
init_button(STOP_DRYING, _L("Stop Drying"));
init_button(PROCEED, _L("Proceed"));
init_button(DBL_CHECK_CANCEL, _L("Cancel"));
@@ -445,7 +445,7 @@ void DeviceErrorDialog::on_button_click(ActionButton btn_id)
m_obj->command_ams_control("resume");
break;
}
case DeviceErrorDialog::CANCLE: {
case DeviceErrorDialog::CANCEL: {
break;
}
case DeviceErrorDialog::STOP_DRYING: {
+1 -1
View File
@@ -43,7 +43,7 @@ public:
RETRY_PROBLEM_SOLVED = 34,
STOP_DRYING = 35,
CANCLE = 37,
CANCEL = 37,
REMOVE_CLOSE_BTN = 39, // special case, do not show close button
PROCEED = 41,
+15 -1
View File
@@ -338,7 +338,21 @@ void FilamentGroupPopup::OnRadioBtn(int idx)
}
}
void FilamentGroupPopup::OnTimer(wxTimerEvent &event) { Dismiss(); }
void FilamentGroupPopup::OnTimer(wxTimerEvent &event)
{
#if __APPLE__
// On macOS, when moving cursor from slice button to this popup window,
// the popup window entering event is triggered first, then the slice button
// leaving event got triggered. So the timer is stopped first, then started
// again, causing the popup being dismissed immediately.
// To fix this, we check if cursor is still inside the popup window before
// dismissing.
wxPoint pos = this->ScreenToClient(wxGetMousePosition());
if (this->GetClientRect().Contains(pos)) return;
#endif
Dismiss();
}
void FilamentGroupPopup::Dismiss() {
m_active = false;
+2 -2
View File
@@ -215,7 +215,7 @@ FilamentMapDialog::FilamentMapDialog(wxWindow *parent,
main_sizer->Add(bottom_panel, 0, wxEXPAND);
m_ok_btn->Bind(wxEVT_BUTTON, &FilamentMapDialog::on_ok, this);
m_cancel_btn->Bind(wxEVT_BUTTON, &FilamentMapDialog::on_cancle, this);
m_cancel_btn->Bind(wxEVT_BUTTON, &FilamentMapDialog::on_cancel, this);
SetEscapeId(wxID_CANCEL);
Bind(wxEVT_CHAR_HOOK, [this](wxKeyEvent& e) {
if (e.GetKeyCode() == WXK_ESCAPE) {
@@ -286,7 +286,7 @@ void FilamentMapDialog::on_ok(wxCommandEvent &event)
EndModal(wxID_OK);
}
void FilamentMapDialog::on_cancle(wxCommandEvent &event) { EndModal(wxID_CANCEL); }
void FilamentMapDialog::on_cancel(wxCommandEvent &event) { EndModal(wxID_CANCEL); }
void FilamentMapDialog::update_panel_status(PageType page)
{
+1 -1
View File
@@ -60,7 +60,7 @@ public:
void set_modal_btn_labels(const wxString& left_label, const wxString& right_label);
private:
void on_ok(wxCommandEvent &event);
void on_cancle(wxCommandEvent &event);
void on_cancel(wxCommandEvent &event);
void on_switch_mode(wxCommandEvent &event);
void on_checkbox(wxCommandEvent &event);
+257 -57
View File
@@ -43,6 +43,7 @@
#include <array>
#include <algorithm>
#include <cmath>
#include <chrono>
@@ -94,7 +95,7 @@ static std::string get_view_type_string(libvgcode::EViewType view_type)
return _u8L("Filament");
else if (view_type == libvgcode::EViewType::LayerTimeLinear)
return _u8L("Layer Time");
else if (view_type == libvgcode::EViewType::LayerTimeLogarithmic)
else if (view_type == libvgcode::EViewType::LayerTimeLogarithmic)
return _u8L("Layer Time (log)");
// ORCA: Add Pressure Advance visualization support
else if (view_type == libvgcode::EViewType::PressureAdvance)
@@ -124,6 +125,29 @@ static int find_close_layer_idx(const std::vector<double> &zs, double &z, double
return -1;
}
static std::string format_compact_weight(double value_in_grams, bool imperial_units)
{
char buffer[64];
if (imperial_units) {
::sprintf(buffer, "%.2f oz", value_in_grams / GizmoObjectManipulation::oz_to_g);
return buffer;
}
const double abs_value = value_in_grams < 0.0 ? -value_in_grams : value_in_grams;
const char* unit = "g";
double scaled_value = abs_value;
if (scaled_value >= 1000000.0) {
scaled_value /= 1000000.0;
unit = "t";
} else if (scaled_value >= 1000.0) {
scaled_value /= 1000.0;
unit = "kg";
}
::sprintf(buffer, "%s%.2f%s", value_in_grams < 0.0 ? "-" : "", scaled_value, unit);
return buffer;
}
#if ENABLE_ACTUAL_SPEED_DEBUG
int GCodeViewer::SequentialView::ActualSpeedImguiWidget::plot(const char* label, const std::array<float, 2>& frame_size)
{
@@ -1287,6 +1311,25 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const
//BBS: move the id to the end of reset
m_last_result_id = gcode_result.id;
m_gcode_result = &gcode_result;
m_move_type_counts.fill(0);
for (auto& move_type_times : m_move_type_times)
move_type_times.fill(0.0f);
m_move_type_distances.fill(0.0f);
for (const GCodeProcessorResult::MoveVertex& move : gcode_result.moves) {
if (move.internal_only)
continue;
const size_t move_type = static_cast<size_t>(move.type);
if (move_type < m_move_type_counts.size()) {
++m_move_type_counts[move_type];
for (size_t mode = 0; mode < move.time.size(); ++mode)
m_move_type_times[move_type][mode] += move.time[mode];
if (move.type == EMoveType::Retract || move.type == EMoveType::Unretract)
m_move_type_distances[move_type] += std::fabs(move.delta_extruder);
else
m_move_type_distances[move_type] += move.travel_dist;
}
}
m_only_gcode_in_preview = only_gcode;
m_sequential_view.gcode_window.load_gcode(gcode_result.filename, gcode_result.lines_ends);
@@ -1449,6 +1492,29 @@ void GCodeViewer::load_as_preview(libvgcode::GCodeInputData&& data)
{
m_loaded_as_preview = true;
m_move_type_counts.fill(0);
for (auto& move_type_times : m_move_type_times)
move_type_times.fill(0.0f);
m_move_type_distances.fill(0.0f);
const size_t normal_time_mode_idx = static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Normal);
for (size_t i = 0; i < data.vertices.size(); ++i) {
const libvgcode::PathVertex& vertex = data.vertices[i];
const size_t move_type = static_cast<size_t>(vertex.type);
if (move_type < m_move_type_counts.size()) {
++m_move_type_counts[move_type];
for (size_t mode = 0; mode < vertex.times.size(); ++mode)
m_move_type_times[move_type][mode] += vertex.times[mode];
if (vertex.type == libvgcode::EMoveType::Retract || vertex.type == libvgcode::EMoveType::Unretract) {
m_move_type_distances[move_type] += std::fabs(vertex.feedrate) * vertex.times[normal_time_mode_idx];
} else if (i > 0) {
const float dx = vertex.position[0] - data.vertices[i - 1].position[0];
const float dy = vertex.position[1] - data.vertices[i - 1].position[1];
const float dz = vertex.position[2] - data.vertices[i - 1].position[2];
m_move_type_distances[move_type] += std::sqrt(dx * dx + dy * dy + dz * dz);
}
}
}
m_viewer.set_extrusion_role_color(libvgcode::EGCodeExtrusionRole::Skirt, { 127, 255, 127 });
m_viewer.set_extrusion_role_color(libvgcode::EGCodeExtrusionRole::ExternalPerimeter, { 255, 255, 0 });
m_viewer.set_extrusion_role_color(libvgcode::EGCodeExtrusionRole::SupportMaterial, { 127, 255, 127 });
@@ -1496,6 +1562,10 @@ void GCodeViewer::reset()
m_extruders_count = 0;
m_filament_diameters = std::vector<float>();
m_filament_densities = std::vector<float>();
m_move_type_counts.fill(0);
for (auto& move_type_times : m_move_type_times)
move_type_times.fill(0.0f);
m_move_type_distances.fill(0.0f);
m_print_statistics.reset();
m_custom_gcode_per_print_z = std::vector<CustomGCode::Item>();
m_left_extruder_filament.clear();
@@ -2676,39 +2746,42 @@ void GCodeViewer::render_all_plates_stats(const std::vector<const GCodeProcessor
columns_offsets.push_back({ std::to_string(it->first + 1), offsets[_u8L("Filament")]});
char buf[64];
double unit_conver = imperial_units ? GizmoObjectManipulation::oz_to_g : 1.0;
float column_sum_m = 0.0f;
float column_sum_g = 0.0f;
if (displayed_columns & ColumnData::Model) {
const std::string weight_text = format_compact_weight(model_used_filaments_g_all_plates[i], imperial_units);
if ((displayed_columns & ~ColumnData::Model) > 0)
::sprintf(buf, imperial_units ? "%.2f in\n%.2f oz" : "%.2f m\n%.2f g", model_used_filaments_m_all_plates[i], model_used_filaments_g_all_plates[i] / unit_conver);
::sprintf(buf, imperial_units ? "%.2f in\n%s" : "%.2f m\n%s", model_used_filaments_m_all_plates[i], weight_text.c_str());
else
::sprintf(buf, imperial_units ? "%.2f in %.2f oz" : "%.2f m %.2f g", model_used_filaments_m_all_plates[i], model_used_filaments_g_all_plates[i] / unit_conver);
::sprintf(buf, imperial_units ? "%.2f in %s" : "%.2f m %s", model_used_filaments_m_all_plates[i], weight_text.c_str());
columns_offsets.push_back({ buf, offsets[_u8L("Model")] });
column_sum_m += model_used_filaments_m_all_plates[i];
column_sum_g += model_used_filaments_g_all_plates[i];
}
if (displayed_columns & ColumnData::Support) {
::sprintf(buf, imperial_units ? "%.2f in\n%.2f oz" : "%.2f m\n%.2f g", support_used_filaments_m_all_plates[i], support_used_filaments_g_all_plates[i] / unit_conver);
const std::string weight_text = format_compact_weight(support_used_filaments_g_all_plates[i], imperial_units);
::sprintf(buf, imperial_units ? "%.2f in\n%s" : "%.2f m\n%s", support_used_filaments_m_all_plates[i], weight_text.c_str());
columns_offsets.push_back({ buf, offsets[_u8L("Support")] });
column_sum_m += support_used_filaments_m_all_plates[i];
column_sum_g += support_used_filaments_g_all_plates[i];
}
if (displayed_columns & ColumnData::Flushed) {
::sprintf(buf, imperial_units ? "%.2f in\n%.2f oz" : "%.2f m\n%.2f g", flushed_filaments_m_all_plates[i], flushed_filaments_g_all_plates[i] / unit_conver);
const std::string weight_text = format_compact_weight(flushed_filaments_g_all_plates[i], imperial_units);
::sprintf(buf, imperial_units ? "%.2f in\n%s" : "%.2f m\n%s", flushed_filaments_m_all_plates[i], weight_text.c_str());
columns_offsets.push_back({ buf, offsets[_u8L("Flushed")] });
column_sum_m += flushed_filaments_m_all_plates[i];
column_sum_g += flushed_filaments_g_all_plates[i];
}
if (displayed_columns & ColumnData::WipeTower) {
::sprintf(buf, imperial_units ? "%.2f in\n%.2f oz" : "%.2f m\n%.2f g", wipe_tower_used_filaments_m_all_plates[i], wipe_tower_used_filaments_g_all_plates[i] / unit_conver);
const std::string weight_text = format_compact_weight(wipe_tower_used_filaments_g_all_plates[i], imperial_units);
::sprintf(buf, imperial_units ? "%.2f in\n%s" : "%.2f m\n%s", wipe_tower_used_filaments_m_all_plates[i], weight_text.c_str());
columns_offsets.push_back({ buf, offsets[_u8L("Tower")] });
column_sum_m += wipe_tower_used_filaments_m_all_plates[i];
column_sum_g += wipe_tower_used_filaments_g_all_plates[i];
}
if ((displayed_columns & ~ColumnData::Model) > 0) {
::sprintf(buf, imperial_units ? "%.2f in\n%.2f oz" : "%.2f m\n%.2f g", column_sum_m, column_sum_g / unit_conver);
const std::string weight_text = format_compact_weight(column_sum_g, imperial_units);
::sprintf(buf, imperial_units ? "%.2f in\n%s" : "%.2f m\n%s", column_sum_m, weight_text.c_str());
columns_offsets.push_back({ buf, offsets[_u8L("Total")] });
}
@@ -3072,7 +3145,9 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
const PrintEstimatedStatistics::Mode& time_mode = m_print_statistics.modes[static_cast<size_t>(m_viewer.get_time_mode())];
const libvgcode::EViewType curr_view_type = m_viewer.get_view_type();
const int curr_view_type_i = static_cast<int>(curr_view_type);
bool show_estimated_time = time_mode.time > 0.0f && (curr_view_type == libvgcode::EViewType::FeatureType ||
const size_t current_time_mode = static_cast<size_t>(m_viewer.get_time_mode());
const float total_estimated_time = time_mode.time > 0.0f ? time_mode.time : m_viewer.get_estimated_time();
bool show_estimated_time = total_estimated_time > 0.0f && (curr_view_type == libvgcode::EViewType::FeatureType ||
curr_view_type == libvgcode::EViewType::LayerTimeLinear || curr_view_type == libvgcode::EViewType::LayerTimeLogarithmic ||
(curr_view_type == libvgcode::EViewType::ColorPrint && !time_mode.custom_gcode_times.empty()));
@@ -3086,6 +3161,39 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
ImVec2 pos_rect = ImGui::GetCursorScreenPos();
float window_padding = 4.0f * m_scale;
auto format_compact_count = [](unsigned long long value) {
static constexpr const char* suffixes[] = { "", "K", "M", "B", "T", "P", "E" };
constexpr size_t suffix_count = sizeof(suffixes) / sizeof(suffixes[0]);
if (value < 1000)
return std::to_string(value);
size_t suffix_index = 0;
unsigned long long divisor = 1;
while (suffix_index + 1 < suffix_count && value / divisor >= 1000) {
divisor *= 1000;
++suffix_index;
}
const unsigned long long whole = value / divisor;
const unsigned long long tenths = (value % divisor) * 10 / divisor;
std::string ret = std::to_string(whole);
if (tenths != 0)
ret += "." + std::to_string(tenths);
ret += suffixes[suffix_index];
return ret;
};
auto format_percent = [](float percent) {
if (percent == 0.0f)
return std::string("0");
char buffer[64];
percent > 0.001f ? ::sprintf(buffer, "%.1f", percent * 100.0f) : ::sprintf(buffer, "<0.1");
return std::string(buffer);
};
// ORCA dont use background on top bar to give modern look
//draw_list->AddRectFilled(ImVec2(pos_rect.x,pos_rect.y - ImGui::GetStyle().WindowPadding.y),
//ImVec2(pos_rect.x + ImGui::GetWindowWidth() + ImGui::GetFrameHeight(),pos_rect.y + ImGui::GetFrameHeight() + window_padding * 2.5),
@@ -3122,10 +3230,10 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
case EItemType::Line: {
draw_list->AddLine({ pos.x + 1, pos.y + icon_size + 2 }, { pos.x + icon_size - 1, pos.y + 4 }, ImGuiWrapper::to_ImU32(color), 3.0f);
break;
}
case EItemType::None:
break;
}
}
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(20.0 * m_scale, 6.0 * m_scale));
@@ -3297,14 +3405,26 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
return _u8L("from") + " " + std::string(buf1) + " " + _u8L("to") + " " + std::string(buf2) + " " + _u8L("mm");
};
auto role_time_and_percent = [this, time_mode](libvgcode::EGCodeExtrusionRole role) {
auto role_time_and_percent = [this, total_estimated_time](libvgcode::EGCodeExtrusionRole role) {
const float time = m_viewer.get_extrusion_role_estimated_time(role);
return std::make_pair(time, time / time_mode.time);
return std::make_pair(time, total_estimated_time > 0.0f ? time / total_estimated_time : 0.0f);
};
auto travel_time_and_percent = [this, time_mode]() {
auto travel_time_and_percent = [this, total_estimated_time]() {
const float time = m_viewer.get_travels_estimated_time();
return std::make_pair(time, time / time_mode.time);
return std::make_pair(time, total_estimated_time > 0.0f ? time / total_estimated_time : 0.0f);
};
auto format_distance = [imperial_units](float distance_mm) {
char buffer[64];
if (imperial_units) {
::sprintf(buffer, "%.2fin", distance_mm / GizmoObjectManipulation::in_to_mm);
} else if (std::fabs(distance_mm) < 1000.0f) {
::sprintf(buffer, "%.0fmm", distance_mm);
} else {
::sprintf(buffer, "%.2fm", distance_mm / 1000.0f);
}
return std::string(buffer);
};
auto used_filament_per_role = [this, imperial_units](ExtrusionRole role) {
@@ -3409,6 +3529,8 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
std::vector<std::string> used_filaments_length;
std::vector<std::string> used_filaments_weight;
std::string travel_percent;
std::string travel_distance;
std::string travel_moves;
std::vector<double> model_used_filaments_m;
std::vector<double> model_used_filaments_g;
double total_model_used_filament_m = 0, total_model_used_filament_g = 0;
@@ -3528,8 +3650,7 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
auto [model_used_filament_m, model_used_filament_g] = used_filament_per_role(convert(role));
::sprintf(buffer, imperial_units ? "%.2fin" : "%.2fm", model_used_filament_m); // ORCA dont use spacing between value and unit
used_filaments_length.push_back(buffer);
::sprintf(buffer, imperial_units ? "%.2foz" : "%.2fg", model_used_filament_g); // ORCA dont use spacing between value and unit
used_filaments_weight.push_back(buffer);
used_filaments_weight.push_back(format_compact_weight(model_used_filament_g, imperial_units));
}
}
@@ -3542,10 +3663,19 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
else
percent > 0.001 ? ::sprintf(buffer, "%.1f", percent * 100) : ::sprintf(buffer, "<0.1");
travel_percent = buffer;
percents.push_back(travel_percent);
// Set travel distance and moves for the Travel row Usage columns
travel_distance = format_distance(m_print_statistics.total_travel_distance);
used_filaments_length.push_back(travel_distance);
travel_moves = format_compact_count(m_print_statistics.total_travel_moves);
used_filaments_weight.push_back(travel_moves);
}
// ORCA use % symbol for percentage and use "Usage" for "Used filaments"
offsets = calculate_offsets({ {_u8L("Line Type"), labels}, {_u8L("Time"), times}, {"%", percents}, {"", used_filaments_length}, {"", used_filaments_weight}, {_u8L("Display"), {""}}}, icon_size);
percents.pop_back();
append_headers({{_u8L("Line Type"), offsets[0]}, {_u8L("Time"), offsets[1]}, {"%", offsets[2]}, {_u8L("Usage"), offsets[3]}, {_u8L("Display"), offsets[5]}});
break;
}
@@ -3609,7 +3739,8 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
{
std::vector<std::string> total_filaments;
char buffer[64];
::sprintf(buffer, imperial_units ? "%.2f in\n%.2f oz" : "%.2f m\n%.2f g", ps.total_used_filament / /*1000*/koef, ps.total_weight / unit_conver);
const std::string total_weight_text = format_compact_weight(ps.total_weight, imperial_units);
::sprintf(buffer, imperial_units ? "%.2f in\n%s" : "%.2f m\n%s", ps.total_used_filament / /*1000*/koef, total_weight_text.c_str());
total_filaments.push_back(buffer);
@@ -3644,9 +3775,54 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
default: { break; }
}
auto append_option_item = [this, append_item](libvgcode::EOptionType type, std::vector<float> offsets) {
auto append_option_item_with_type = [this, offsets, append_item](libvgcode::EOptionType type, const ColorRGBA& color, const std::string& label, bool visible) {
append_item(EItemType::Rect, color, {{ label , offsets[0] }}, true, offsets.back()/*ORCA checkbox_pos*/, visible, [this, type, visible]() {
auto append_option_item = [this, append_item, current_time_mode, total_estimated_time, &format_compact_count, &format_percent, &format_distance](libvgcode::EOptionType type, std::vector<float> offsets) {
const bool full_layout = offsets.size() > 4;
auto option_stats = [this, current_time_mode, total_estimated_time, &format_compact_count, &format_percent, &format_distance, full_layout](libvgcode::EOptionType option_type) -> std::array<std::string, 4> {
libvgcode::EMoveType move_type;
bool has_move_type = true;
switch (option_type) {
case libvgcode::EOptionType::Wipes: { move_type = libvgcode::EMoveType::Wipe; break; }
case libvgcode::EOptionType::Retractions: { move_type = libvgcode::EMoveType::Retract; break; }
case libvgcode::EOptionType::Unretractions: { move_type = libvgcode::EMoveType::Unretract; break; }
case libvgcode::EOptionType::Seams: { move_type = libvgcode::EMoveType::Seam; break; }
case libvgcode::EOptionType::ToolChanges: { move_type = libvgcode::EMoveType::ToolChange; break; }
default: { has_move_type = false; break; }
}
if (!has_move_type)
return { "", "", "", "" };
const size_t move_type_idx = static_cast<size_t>(move_type);
float time = m_move_type_times[move_type_idx][current_time_mode];
if (option_type == libvgcode::EOptionType::ToolChanges) {
// Toolchange delays are injected via synchronize() and are not attributed to ToolChange move vertices.
time = m_print_statistics.total_filament_load_time + m_print_statistics.total_filament_unload_time + m_print_statistics.total_tool_change_time;
}
const std::string time_text = full_layout && time > 0.0f ? short_time(get_time_dhms(time)) : "";
const std::string percent_text = full_layout && total_estimated_time > 0.0f ? format_percent(time / total_estimated_time) : "";
const float seam_distance = m_print_statistics.total_seam_gap_distance + m_print_statistics.total_seam_scarf_distance;
const float distance = (option_type == libvgcode::EOptionType::Seams && seam_distance > 0.0f) ? seam_distance : m_move_type_distances[move_type_idx];
const std::string distance_text = full_layout && (option_type == libvgcode::EOptionType::Wipes || option_type == libvgcode::EOptionType::Retractions || option_type == libvgcode::EOptionType::Unretractions || option_type == libvgcode::EOptionType::Seams)
? format_distance(distance)
: "";
const std::string count_text = full_layout ? format_compact_count(m_move_type_counts[move_type_idx]) : "";
return { time_text, percent_text, distance_text, count_text };
};
auto append_option_item_with_type = [this, offsets, append_item, full_layout](libvgcode::EOptionType type, const ColorRGBA& color, const std::string& label, bool visible,
const std::string& time_text, const std::string& percent_text, const std::string& distance_text, const std::string& count_text) {
std::vector<std::pair<std::string, float>> columns_offsets;
columns_offsets.push_back({ label , offsets[0] });
if (full_layout && !time_text.empty())
columns_offsets.push_back({ time_text, offsets[1] });
if (full_layout && !percent_text.empty())
columns_offsets.push_back({ percent_text, offsets[2] });
if (full_layout && !distance_text.empty())
columns_offsets.push_back({ distance_text, offsets[3] });
if (full_layout && !count_text.empty())
columns_offsets.push_back({ count_text, distance_text.empty() ? offsets[3] : offsets[4] });
append_item(EItemType::Rect, color, columns_offsets, true, offsets.back()/*ORCA checkbox_pos*/, visible, [this, type, visible]() {
m_viewer.toggle_option_visibility(type);
update_moves_slider();
});
@@ -3654,18 +3830,33 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
const bool visible = m_viewer.is_option_visible(type);
if (type == libvgcode::EOptionType::Travels) {
//BBS: only display travel time in FeatureType view
append_option_item_with_type(type, libvgcode::convert(m_viewer.get_option_color(libvgcode::EOptionType::Travels)), _u8L("Travel"), visible);
append_option_item_with_type(type, libvgcode::convert(m_viewer.get_option_color(libvgcode::EOptionType::Travels)), _u8L("Travel"), visible, "", "", "", "");
}
else if (type == libvgcode::EOptionType::Seams) {
const auto option_values = option_stats(type);
append_option_item_with_type(type, libvgcode::convert(m_viewer.get_option_color(libvgcode::EOptionType::Seams)), _u8L("Seams"), visible,
option_values[0], option_values[1], option_values[2], option_values[3]);
}
else if (type == libvgcode::EOptionType::Retractions) {
const auto option_values = option_stats(type);
append_option_item_with_type(type, libvgcode::convert(m_viewer.get_option_color(libvgcode::EOptionType::Retractions)), _u8L("Retract"), visible,
option_values[0], option_values[1], option_values[2], option_values[3]);
}
else if (type == libvgcode::EOptionType::Unretractions) {
const auto option_values = option_stats(type);
append_option_item_with_type(type, libvgcode::convert(m_viewer.get_option_color(libvgcode::EOptionType::Unretractions)), _u8L("Unretract"), visible,
option_values[0], option_values[1], option_values[2], option_values[3]);
}
else if (type == libvgcode::EOptionType::ToolChanges) {
const auto option_values = option_stats(type);
append_option_item_with_type(type, libvgcode::convert(m_viewer.get_option_color(libvgcode::EOptionType::ToolChanges)), _u8L("Filament Changes"), visible,
option_values[0], option_values[1], option_values[2], option_values[3]);
}
else if (type == libvgcode::EOptionType::Wipes) {
const auto option_values = option_stats(type);
append_option_item_with_type(type, libvgcode::convert(m_viewer.get_option_color(libvgcode::EOptionType::Wipes)), _u8L("Wipe"), visible,
option_values[0], option_values[1], option_values[2], option_values[3]);
}
else if (type == libvgcode::EOptionType::Seams)
append_option_item_with_type(type, libvgcode::convert(m_viewer.get_option_color(libvgcode::EOptionType::Seams)), _u8L("Seams"), visible);
else if (type == libvgcode::EOptionType::Retractions)
append_option_item_with_type(type, libvgcode::convert(m_viewer.get_option_color(libvgcode::EOptionType::Retractions)), _u8L("Retract"), visible);
else if (type == libvgcode::EOptionType::Unretractions)
append_option_item_with_type(type, libvgcode::convert(m_viewer.get_option_color(libvgcode::EOptionType::Unretractions)), _u8L("Unretract"), visible);
else if (type == libvgcode::EOptionType::ToolChanges)
append_option_item_with_type(type, libvgcode::convert(m_viewer.get_option_color(libvgcode::EOptionType::ToolChanges)), _u8L("Filament Changes"), visible);
else if (type == libvgcode::EOptionType::Wipes)
append_option_item_with_type(type, libvgcode::convert(m_viewer.get_option_color(libvgcode::EOptionType::Wipes)), _u8L("Wipe"), visible);
};
const libvgcode::EViewType new_view_type = curr_view_type;
@@ -3705,6 +3896,8 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
columns_offsets.push_back({ _u8L("Travel"), offsets[0] });
columns_offsets.push_back({ travel_time, offsets[1] });
columns_offsets.push_back({ travel_percent, offsets[2] });
columns_offsets.push_back({ travel_distance, offsets[3] }); // Usage column
columns_offsets.push_back({ travel_moves, offsets[4] }); // Usage column
append_item(EItemType::Rect, libvgcode::convert(m_viewer.get_option_color(libvgcode::EOptionType::Travels)), columns_offsets, true, offsets.back()/*ORCA checkbox_pos*/, visible, [this, item, visible]() {
m_viewer.toggle_option_visibility(item);
update_moves_slider();
@@ -3796,7 +3989,8 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
size_t i = 0;
const std::vector<uint8_t>& used_extruders_ids = m_viewer.get_used_extruders_ids();
for (uint8_t extruder_id : used_extruders_ids) {
::sprintf(buf, imperial_units ? "%.2f in %.2f g" : "%.2f m %.2f g", model_used_filaments_m[i], model_used_filaments_g[i]);
const std::string weight_text = format_compact_weight(model_used_filaments_g[i], imperial_units);
::sprintf(buf, imperial_units ? "%.2f in %s" : "%.2f m %s", model_used_filaments_m[i], weight_text.c_str());
append_item(EItemType::Rect, libvgcode::convert(m_viewer.get_tool_colors()[extruder_id]), { { _u8L("Extruder") + " " + std::to_string(extruder_id + 1), offsets[0]}, {buf, offsets[1]} });
// append_item(EItemType::Rect, libvgcode::convert(m_viewer.get_tool_colors()[extruder_id]), _u8L("Extruder") + " " + std::to_string(extruder_id + 1),
// true, "", 0.0f, 0.0f, offsets, used_filaments_m[extruder_id], used_filaments_g[extruder_id]);
@@ -3809,7 +4003,8 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
char buf[64];
imgui.text(_u8L("Total") + ":");
ImGui::SameLine();
::sprintf(buf, imperial_units ? "%.2f in / %.2f oz" : "%.2f m / %.2f g", ps.total_used_filament / koef, ps.total_weight / unit_conver);
const std::string total_weight_text = format_compact_weight(ps.total_weight, imperial_units);
::sprintf(buf, imperial_units ? "%.2f in / %s" : "%.2f m / %s", ps.total_used_filament / koef, total_weight_text.c_str());
imgui.text(buf);
ImGui::Dummy({window_padding, window_padding});
@@ -3855,34 +4050,39 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
float column_sum_m = 0.0f;
float column_sum_g = 0.0f;
if (displayed_columns & ColumnData::Model) {
const std::string weight_text = format_compact_weight(model_used_filaments_g[i], imperial_units);
if ((displayed_columns & ~ColumnData::Model) > 0)
::sprintf(buf, imperial_units ? "%.2f in\n%.2f oz" : "%.2f m\n%.2f g", model_used_filaments_m[i], model_used_filaments_g[i] / unit_conver);
::sprintf(buf, imperial_units ? "%.2f in\n%s" : "%.2f m\n%s", model_used_filaments_m[i], weight_text.c_str());
else
::sprintf(buf, imperial_units ? "%.2f in %.2f oz" : "%.2f m %.2f g", model_used_filaments_m[i], model_used_filaments_g[i] / unit_conver);
::sprintf(buf, imperial_units ? "%.2f in %s" : "%.2f m %s", model_used_filaments_m[i], weight_text.c_str());
columns_offsets.push_back({ buf, color_print_offsets[_u8L("Model")] });
column_sum_m += model_used_filaments_m[i];
column_sum_g += model_used_filaments_g[i];
}
if (displayed_columns & ColumnData::Support) {
::sprintf(buf, imperial_units ? "%.2f in\n%.2f oz" : "%.2f m\n%.2f g", support_used_filaments_m[i], support_used_filaments_g[i] / unit_conver);
const std::string weight_text = format_compact_weight(support_used_filaments_g[i], imperial_units);
::sprintf(buf, imperial_units ? "%.2f in\n%s" : "%.2f m\n%s", support_used_filaments_m[i], weight_text.c_str());
columns_offsets.push_back({ buf, color_print_offsets[_u8L("Support")] });
column_sum_m += support_used_filaments_m[i];
column_sum_g += support_used_filaments_g[i];
}
if (displayed_columns & ColumnData::Flushed) {
::sprintf(buf, imperial_units ? "%.2f in\n%.2f oz" : "%.2f m\n%.2f g", flushed_filaments_m[i], flushed_filaments_g[i] / unit_conver);
const std::string weight_text = format_compact_weight(flushed_filaments_g[i], imperial_units);
::sprintf(buf, imperial_units ? "%.2f in\n%s" : "%.2f m\n%s", flushed_filaments_m[i], weight_text.c_str());
columns_offsets.push_back({ buf, color_print_offsets[_u8L("Flushed")]});
column_sum_m += flushed_filaments_m[i];
column_sum_g += flushed_filaments_g[i];
}
if (displayed_columns & ColumnData::WipeTower) {
::sprintf(buf, imperial_units ? "%.2f in\n%.2f oz" : "%.2f m\n%.2f g", wipe_tower_used_filaments_m[i], wipe_tower_used_filaments_g[i] / unit_conver);
const std::string weight_text = format_compact_weight(wipe_tower_used_filaments_g[i], imperial_units);
::sprintf(buf, imperial_units ? "%.2f in\n%s" : "%.2f m\n%s", wipe_tower_used_filaments_m[i], weight_text.c_str());
columns_offsets.push_back({ buf, color_print_offsets[_u8L("Tower")] });
column_sum_m += wipe_tower_used_filaments_m[i];
column_sum_g += wipe_tower_used_filaments_g[i];
}
if ((displayed_columns & ~ColumnData::Model) > 0) {
::sprintf(buf, imperial_units ? "%.2f in\n%.2f oz" : "%.2f m\n%.2f g", column_sum_m, column_sum_g / unit_conver);
const std::string weight_text = format_compact_weight(column_sum_g, imperial_units);
::sprintf(buf, imperial_units ? "%.2f in\n%s" : "%.2f m\n%s", column_sum_m, weight_text.c_str());
columns_offsets.push_back({ buf, color_print_offsets[_u8L("Total")] });
}
@@ -3908,27 +4108,32 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
std::vector<std::pair<std::string, float>> columns_offsets;
columns_offsets.push_back({ _u8L("Total"), color_print_offsets[_u8L("Filament")]});
if (displayed_columns & ColumnData::Model) {
const std::string weight_text = format_compact_weight(total_model_used_filament_g, imperial_units);
if ((displayed_columns & ~ColumnData::Model) > 0)
::sprintf(buf, imperial_units ? "%.2f in\n%.2f oz" : "%.2f m\n%.2f g", total_model_used_filament_m, total_model_used_filament_g / unit_conver);
::sprintf(buf, imperial_units ? "%.2f in\n%s" : "%.2f m\n%s", total_model_used_filament_m, weight_text.c_str());
else
::sprintf(buf, imperial_units ? "%.2f in %.2f oz" : "%.2f m %.2f g", total_model_used_filament_m, total_model_used_filament_g / unit_conver);
::sprintf(buf, imperial_units ? "%.2f in %s" : "%.2f m %s", total_model_used_filament_m, weight_text.c_str());
columns_offsets.push_back({ buf, color_print_offsets[_u8L("Model")] });
}
if (displayed_columns & ColumnData::Support) {
::sprintf(buf, imperial_units ? "%.2f in\n%.2f oz" : "%.2f m\n%.2f g", total_support_used_filament_m, total_support_used_filament_g / unit_conver);
const std::string weight_text = format_compact_weight(total_support_used_filament_g, imperial_units);
::sprintf(buf, imperial_units ? "%.2f in\n%s" : "%.2f m\n%s", total_support_used_filament_m, weight_text.c_str());
columns_offsets.push_back({ buf, color_print_offsets[_u8L("Support")] });
}
if (displayed_columns & ColumnData::Flushed) {
::sprintf(buf, imperial_units ? "%.2f in\n%.2f oz" : "%.2f m\n%.2f g", total_flushed_filament_m, total_flushed_filament_g / unit_conver);
const std::string weight_text = format_compact_weight(total_flushed_filament_g, imperial_units);
::sprintf(buf, imperial_units ? "%.2f in\n%s" : "%.2f m\n%s", total_flushed_filament_m, weight_text.c_str());
columns_offsets.push_back({ buf, color_print_offsets[_u8L("Flushed")] });
}
if (displayed_columns & ColumnData::WipeTower) {
::sprintf(buf, imperial_units ? "%.2f in\n%.2f oz" : "%.2f m\n%.2f g", total_wipe_tower_used_filament_m, total_wipe_tower_used_filament_g / unit_conver);
const std::string weight_text = format_compact_weight(total_wipe_tower_used_filament_g, imperial_units);
::sprintf(buf, imperial_units ? "%.2f in\n%s" : "%.2f m\n%s", total_wipe_tower_used_filament_m, weight_text.c_str());
columns_offsets.push_back({ buf, color_print_offsets[_u8L("Tower")] });
}
if ((displayed_columns & ~ColumnData::Model) > 0) {
::sprintf(buf, imperial_units ? "%.2f in\n%.2f oz" : "%.2f m\n%.2f g", total_model_used_filament_m + total_support_used_filament_m + total_flushed_filament_m + total_wipe_tower_used_filament_m,
(total_model_used_filament_g + total_support_used_filament_g + total_flushed_filament_g + total_wipe_tower_used_filament_g) / unit_conver);
const std::string weight_text = format_compact_weight(total_model_used_filament_g + total_support_used_filament_g + total_flushed_filament_g + total_wipe_tower_used_filament_g, imperial_units);
::sprintf(buf, imperial_units ? "%.2f in\n%s" : "%.2f m\n%s", total_model_used_filament_m + total_support_used_filament_m + total_flushed_filament_m + total_wipe_tower_used_filament_m,
weight_text.c_str());
columns_offsets.push_back({ buf, color_print_offsets[_u8L("Total")] });
}
append_item(EItemType::None, libvgcode::convert(tool_colors[0]), columns_offsets);
@@ -3939,16 +4144,14 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
ImGui::SameLine();
imgui.text(_u8L("Filament change times") + ":");
ImGui::SameLine();
::sprintf(buf, "%d", m_print_statistics.total_filament_changes);
imgui.text(buf);
imgui.text(format_compact_count(m_print_statistics.total_filament_changes));
//display tool change times
ImGui::Dummy({window_padding, window_padding});
ImGui::SameLine();
imgui.text(_u8L("Tool changes") + ":");
ImGui::SameLine();
::sprintf(buf, "%d", m_print_statistics.total_extruder_changes);
imgui.text(buf);
imgui.text(format_compact_count(m_print_statistics.total_extruder_changes));
//BBS display cost
ImGui::Dummy({ window_padding, window_padding });
@@ -4075,8 +4278,7 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
imgui.text(buffer);
ImGui::SameLine(offsets[3]);
::sprintf(buffer, "%.2f g", used_filament.second);
imgui.text(buffer);
imgui.text(format_compact_weight(used_filament.second, imperial_units));
}
};
@@ -4401,8 +4603,7 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
::sprintf(buf, imperial_units ? "%.2f in" : "%.2f m", ps.total_used_filament / koef);
imgui.text(buf);
ImGui::SameLine();
::sprintf(buf, imperial_units ? " %.2f oz" : " %.2f g", ps.total_weight / unit_conver);
imgui.text(buf);
imgui.text(" " + format_compact_weight(ps.total_weight, imperial_units));
ImGui::Dummy({ window_padding, window_padding });
ImGui::SameLine();
imgui.text(model_filament_str + ":");
@@ -4412,8 +4613,7 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
::sprintf(buf, imperial_units ? "%.2f in" : "%.2f m", ps.total_used_filament / koef - exlude_m);
imgui.text(buf);
ImGui::SameLine();
::sprintf(buf, imperial_units ? " %.2f oz" : " %.2f g", (ps.total_weight - exlude_g) / unit_conver);
imgui.text(buf);
imgui.text(" " + format_compact_weight(ps.total_weight - exlude_g, imperial_units));
//BBS: display cost of filaments
ImGui::Dummy({ window_padding, window_padding });
ImGui::SameLine();
+4
View File
@@ -14,6 +14,7 @@
// needed for tech VGCODE_ENABLE_COG_AND_TOOL_MARKERS
#include <libvgcode/include/Types.hpp>
#include <array>
#include <cstdint>
#include <float.h>
#include <set>
@@ -184,6 +185,9 @@ private:
unsigned int m_last_result_id{ 0 };
//BBS: save m_gcode_result as well
const GCodeProcessorResult* m_gcode_result;
std::array<unsigned int, static_cast<size_t>(EMoveType::Count)> m_move_type_counts{};
std::array<std::array<float, static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Count)>, static_cast<size_t>(EMoveType::Count)> m_move_type_times{};
std::array<float, static_cast<size_t>(EMoveType::Count)> m_move_type_distances{};
//BBS: add only gcode mode
bool m_only_gcode_in_preview {false};
+94 -33
View File
@@ -4374,12 +4374,12 @@ void GUI_App::get_login_info(const std::string& provider/* = ORCA_CLOUD_PROVIDER
if (m_agent) {
if (m_agent->is_user_login(provider)) {
std::string login_cmd = m_agent->build_login_cmd(provider);
wxString strJS = wxString::Format("window.postMessage(%s)", login_cmd);
wxString strJS = wxString::Format("window.postMessage(%s)", from_u8(login_cmd));
GUI::wxGetApp().run_script(strJS);
} else {
m_agent->user_logout(false, provider);
std::string logout_cmd = m_agent->build_logout_cmd(provider);
wxString strJS = wxString::Format("window.postMessage(%s)", logout_cmd);
wxString strJS = wxString::Format("window.postMessage(%s)", from_u8(logout_cmd));
GUI::wxGetApp().run_script(strJS);
}
mainframe->m_webview->SetLoginPanelVisibility(true);
@@ -4803,7 +4803,7 @@ void GUI_App::handle_http_error(unsigned int status, std::string body, const std
void GUI_App::on_http_error(wxCommandEvent &evt)
{
int status = evt.GetInt();
std::string provider = ORCA_CLOUD_PROVIDER;
std::string provider = "";
std::string body_str;
// Extract provider and body from event data
@@ -4845,18 +4845,24 @@ void GUI_App::on_http_error(wxCommandEvent &evt)
// request login
if (status == 401) {
if (m_agent) {
if (m_agent->is_user_login(provider)) {
BOOST_LOG_TRIVIAL(warning) << "logout: http error 401.";
this->request_user_logout(provider);
if (!provider.empty() && m_agent->is_user_login(provider)) {
if (std::chrono::steady_clock::now() - m_last_401_error_time > 30s) {
BOOST_LOG_TRIVIAL(warning) << "logout: http error 401.";
this->request_user_logout(provider);
if (!m_show_http_error_msgdlg) {
MessageDialog msg_dlg(nullptr, _L("Login information expired. Please login again."), "", wxAPPLY | wxOK);
m_show_http_error_msgdlg = true;
auto modal_result = msg_dlg.ShowModal();
if (modal_result == wxOK || modal_result == wxCLOSE) {
m_show_http_error_msgdlg = false;
return;
if (!m_show_http_error_msgdlg) {
MessageDialog msg_dlg(nullptr, _L("Login information expired. Please login again."), "", wxAPPLY | wxOK);
m_show_http_error_msgdlg = true;
auto modal_result = msg_dlg.ShowModal();
if (modal_result == wxOK || modal_result == wxCLOSE) {
m_show_http_error_msgdlg = false;
return;
}
}
m_last_401_error_time = std::chrono::steady_clock::now();
} else {
BOOST_LOG_TRIVIAL(warning) << "401 encountered within grace period, suppressing logout";
}
}
}
@@ -4874,11 +4880,11 @@ void GUI_App::on_http_error(wxCommandEvent &evt)
if (provider == ORCA_CLOUD_PROVIDER && status >= 400 && code != HttpErrorVersionLimited) {
wxString msg;
if (!error.empty()) {
msg = wxString::Format(_L("API error (HTTP %u): %s"), status, wxString::FromUTF8(error));
msg = wxString::Format(_L("Failed to connect to OrcaCloud.\nPlease check your network connectivity\n(HTTP %u): %s"), status, wxString::FromUTF8(error));
} else {
msg = wxString::Format(_L("API error (HTTP %u)"), status);
msg = wxString::Format(_L("Failed to connect to OrcaCloud.\nPlease check your network connectivity\n(HTTP %u)"), status);
}
if (app_config->get_bool("developer_mode")) {
// Use notification manager if ImGui is ready; fall back to wxMessageBox on Linux
// where ImGui may not be initialized until the user switches to the Prepare tab.
@@ -4893,7 +4899,7 @@ void GUI_App::on_http_error(wxCommandEvent &evt)
if (!m_is_error_shown) {
m_is_error_shown = true;
wxMessageBox(msg, _L("Orca Cloud API Error"), wxOK | wxICON_ERROR, wxGetApp().mainframe);
wxMessageBox(msg, _L("Cloud Error"), wxOK | wxICON_ERROR, wxGetApp().mainframe);
}
}
}
@@ -4931,6 +4937,10 @@ void GUI_App::on_user_login_handle(wxCommandEvent &evt)
std::string provider = evt.GetString().ToStdString();
if (provider.empty()) provider = ORCA_CLOUD_PROVIDER;
// Reset 401 grace period so transient token-propagation 401s
// during login warmup don't trigger immediate logout.
m_last_401_error_time = std::chrono::steady_clock::now();
m_agent->connect_server();
// get machine list
DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager();
@@ -5841,25 +5851,76 @@ void GUI_App::reload_settings()
return;
}
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " cloud user preset number is: " << user_presets.size();
// Check the user presets for any system vendors that need to be installed
for (auto data : user_presets) {
if (!check_preset_parent_available(data))
add_pending_vendor_preset(data);
}
load_pending_vendors();
preset_bundle->load_user_presets(*app_config, user_presets, ForwardCompatibilitySubstitutionRule::Enable);
preset_bundle->save_user_presets(*app_config, get_delete_cache_presets());
if (is_main_thread_active()) {
auto refresh_synced_ui = [this, user_presets = std::move(user_presets)]() mutable {
if (is_closing() || !preset_bundle || !app_config || !mainframe)
return;
// Snapshot each collection's edited config BEFORE any mutation.
// load_pending_vendors() via apply_vendor_config() can call select_preset(0)
// resetting all selections to defaults and overwriting m_edited_preset.
// The cloud load_user_presets() can also trigger select_preset() via
// remove_users_preset() and overwrite m_edited_preset.config via load_user_preset().
struct PresetSnapshot { std::string name; DynamicPrintConfig config; bool dirty; };
auto snapshot_collection = [](const PresetCollection& col) -> PresetSnapshot {
auto& sel = col.get_selected_preset();
auto& ed = col.get_edited_preset();
return {sel.name, ed.config, sel.is_dirty};
};
PresetSnapshot print_snap = snapshot_collection(preset_bundle->prints);
PresetSnapshot filament_snap = snapshot_collection(preset_bundle->filaments);
PresetSnapshot printer_snap = snapshot_collection(preset_bundle->printers);
// Check the user presets for any system vendors that need to be installed
for (auto data : user_presets) {
if (!check_preset_parent_available(data))
add_pending_vendor_preset(data);
}
load_pending_vendors();
preset_bundle->load_user_presets(*app_config, user_presets, ForwardCompatibilitySubstitutionRule::Enable);
preset_bundle->save_user_presets(*app_config, get_delete_cache_presets());
// Re-apply any edited config that was wiped during vendor loading or sync.
auto restore_snapshot = [](PresetCollection& col, const PresetSnapshot& snap, const char* label) {
auto& ed = col.get_edited_preset();
bool changed = !ed.config.equals(snap.config);
BOOST_LOG_TRIVIAL(info) << "reload_settings restore " << label
<< ": snap_name=" << snap.name << " snap_dirty=" << snap.dirty
<< " current_name=" << ed.name << " changed=" << changed;
if (!snap.dirty) return; // nothing to protect, let cloud updates stand
Preset* p = col.find_preset(snap.name, false, true);
if (p && p->name == snap.name) {
BOOST_LOG_TRIVIAL(info) << "reload_settings RESTORING " << label
<< ": name=" << snap.name;
// If the snapshot preset is not currently selected, re-select it first.
if (col.get_selected_preset().name != snap.name)
col.select_preset_by_name(snap.name, true);
ed = col.get_edited_preset();
ed.config = snap.config;
col.get_selected_preset().is_dirty = snap.dirty;
ed.is_dirty = snap.dirty;
} else {
BOOST_LOG_TRIVIAL(info) << "reload_settings restore " << label
<< ": preset not found name=" << snap.name;
}
};
restore_snapshot(preset_bundle->prints, print_snap, "print");
restore_snapshot(preset_bundle->filaments, filament_snap, "filament");
restore_snapshot(preset_bundle->printers, printer_snap, "printer");
// Orca: settings changed, refresh ui to reflect the new preset values
mainframe->update_side_preset_ui();
for (auto tab : tabs_list) {
tab->reload_config();
tab->update_changed_ui();
}
if (plater_)
plater_->sidebar().update_all_preset_comboboxes();
} else {
CallAfter([this] {
mainframe->update_side_preset_ui();
if (plater_)
plater_->sidebar().update_all_preset_comboboxes();
});
}
};
if (is_main_thread_active())
refresh_synced_ui();
else
CallAfter(refresh_synced_ui);
}
}
+2 -1
View File
@@ -326,6 +326,7 @@ private:
bool m_adding_script_handler { false };
bool m_side_popup_status{false};
bool m_show_http_error_msgdlg{false};
std::chrono::steady_clock::time_point m_last_401_error_time;
bool m_show_error_msgdlg{false};
wxString m_info_dialog_content;
HttpServer m_http_server;
@@ -493,7 +494,7 @@ public:
void request_open_project(std::string project_id);
void request_remove_project(std::string project_id);
void handle_http_error(unsigned int status, std::string body, const std::string& provider = ORCA_CLOUD_PROVIDER);
void handle_http_error(unsigned int status, std::string body, const std::string& provider = "");
void on_http_error(wxCommandEvent &evt);
void on_update_machine_list(wxCommandEvent& evt);
void on_user_login(wxCommandEvent &evt);
+36 -19
View File
@@ -121,6 +121,7 @@ std::map<std::string, std::vector<SimpleSettingData>> SettingsFactory::PART_CATE
{"bottom_shell_thickness", L("Bottom Minimum Shell Thickness"), 1},
{"bottom_surface_density", L("Bottom Surface Density"), 1},
{"sparse_infill_density", "", 1},
{"fill_multiline", "", 1},
{"sparse_infill_pattern", "", 1},
{"lateral_lattice_angle_1", "", 1},
{"lateral_lattice_angle_2", "", 1},
@@ -572,35 +573,51 @@ wxMenu* MenuFactory::append_submenu_add_generic(wxMenu* menu, ModelVolumeType ty
wxMenu* MenuFactory::append_submenu_add_handy_model(wxMenu* menu, ModelVolumeType type) {
auto sub_menu = new wxMenu;
for (auto &item : {L("Orca Cube"), L("Orca Tolerance Test"), L("3DBenchy"), L("Cali Cat"), L("Autodesk FDM Test"),
for (auto &item : {L("Orca Cube"), L("OrcaSliced Combo"), L("Orca Tolerance Test"), L("3DBenchy"), L("Cali Cat"), L("Autodesk FDM Test"),
L("Voron Cube"), L("Stanford Bunny"), L("Orca String Hell") }) {
append_menu_item(
sub_menu, wxID_ANY, _(item), "",
[type, item](wxCommandEvent&) {
std::vector<boost::filesystem::path> input_files;
bool is_stringhell = false;
std::string file_name = item;
if (file_name == L("Orca Cube"))
file_name = "OrcaCube_v2.3mf";
else if (file_name == L("Orca Tolerance Test"))
file_name = "OrcaToleranceTest.drc";
else if (file_name == L("3DBenchy"))
file_name = "3DBenchy.drc";
else if (file_name == L("Cali Cat"))
file_name = "calicat.drc";
else if (file_name == L("Autodesk FDM Test"))
file_name = "ksr_fdmtest_v4.drc";
else if (file_name == L("Voron Cube"))
file_name = "Voron_Design_Cube_v7.drc";
else if (file_name == L("Stanford Bunny"))
file_name = "Stanford_Bunny.drc";
else if (file_name == L("Orca String Hell")) {
file_name = "Orca_stringhell.drc";
std::vector<std::string> file_names;
bool arrange_after_import = false;
if (item == L("Orca Cube")){
file_names = { "OrcaCube_v2.drc", "OrcaPlug_v2.drc"};
arrange_after_import = true;
}
else if (item == L("OrcaSliced Combo"))
{
file_names = { "OrcaSliced.3mf", "OrcaCube_v2.drc", "OrcaPlug_v2.drc" };
arrange_after_import = true;
}
else if (item == L("Orca Tolerance Test"))
file_names = { "OrcaToleranceTest.drc" };
else if (item == L("3DBenchy"))
file_names = { "3DBenchy.drc" };
else if (item == L("Cali Cat"))
file_names = { "calicat.drc" };
else if (item == L("Autodesk FDM Test"))
file_names = { "ksr_fdmtest_v4.drc" };
else if (item == L("Voron Cube"))
file_names = { "Voron_Design_Cube_v7.drc" };
else if (item == L("Stanford Bunny"))
file_names = { "Stanford_Bunny.drc" };
else if (item == L("Orca String Hell")) {
file_names = { "Orca_stringhell.drc" };
is_stringhell = true;
} else
return;
input_files.push_back((boost::filesystem::path(Slic3r::resources_dir()) / "handy_models" / file_name));
input_files.reserve(file_names.size());
for (const auto& file_name : file_names)
input_files.push_back((boost::filesystem::path(Slic3r::resources_dir()) / "handy_models" / file_name));
plater()->load_files(input_files, LoadStrategy::LoadModel);
if (arrange_after_import) {
plater()->set_prepare_state(Job::PREPARE_STATE_MENU);
plater()->arrange();
}
// Suggest to change settings for stringhell
// This serves as mini tutorial for new users
-3
View File
@@ -2788,9 +2788,6 @@ void MainFrame::init_menubar_as_editor()
info_dlg.ShowModal();
return;
}
if (m_plater)
m_plater->get_notification_manager()->push_notification(
into_u8(_L("Syncing presets from cloud\u2026")));
wxGetApp().restart_sync_user_preset();
}, "", nullptr,
[this]() {
+15 -3
View File
@@ -279,9 +279,12 @@ ParamsPanel::ParamsPanel( wxWindow* parent, wxWindowID id, const wxPoint& pos, c
});
m_mode_icon->SetToolTip(_L("Cycle settings visibility"));
m_mode_view = new ModeSwitchButton(m_top_panel);
m_mode_view->SetSelection(mode_to_selection(wxGetApp().get_saved_mode()));
if (wxGetApp().get_mode() == comDevelop)
if (wxGetApp().get_mode() == comDevelop) {
m_mode_view->SetSelection(mode_to_selection(comExpert));
m_mode_view->Enable(false);
} else {
m_mode_view->SetSelection(mode_to_selection(wxGetApp().get_saved_mode()));
}
// BBS: new layout
//m_search_btn = new ScalableButton(m_top_panel, wxID_ANY, "search", wxEmptyString, wxDefaultSize, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, true);
@@ -648,18 +651,27 @@ void ParamsPanel::update_mode()
if (mode_view == nullptr)
return;
mode_view->SetSelection(mode_to_selection(Slic3r::GUI::wxGetApp().get_saved_mode()));
if (app_mode == comDevelop) {
mode_view->SetSelection(mode_to_selection(comExpert));
mode_view->Enable(false);
return;
}
mode_view->SetSelection(mode_to_selection(Slic3r::GUI::wxGetApp().get_saved_mode()));
if (!mode_view->IsEnabled())
mode_view->Enable();
};
sync_mode_view(m_mode_view);
sync_mode_view(m_current_tab ? dynamic_cast<Tab*>(m_current_tab)->m_mode_view : nullptr);
auto sync_mode_icon = [&](ScalableButton* mode_icon) {
if (mode_icon == nullptr)
return;
mode_icon->Show(app_mode != comDevelop);
};
sync_mode_icon(m_mode_icon);
sync_mode_icon(m_current_tab ? dynamic_cast<Tab*>(m_current_tab)->m_mode_icon : nullptr);
}
void ParamsPanel::msw_rescale()
+1
View File
@@ -338,6 +338,7 @@ public:
std::vector<int> get_extruders_without_support(bool conside_custom_gcode = false) const;
// get used filaments from gcode result, 1 based idx
std::vector<int> get_used_filaments();
const std::vector<FilamentInfo>& get_slice_filaments_info() const { return slice_filaments_info; }
int get_physical_extruder_by_filament_id(const DynamicConfig& g_config, int idx) const;
bool check_filament_printable(const DynamicPrintConfig & config, wxString& error_message);
bool check_tpu_printable_status(const DynamicPrintConfig & config, const std::vector<int> &tpu_filaments);
+2 -2
View File
@@ -824,7 +824,7 @@ bool PartSkipDialog::IsAllChecked()
return true;
}
bool PartSkipDialog::IsAllCancled()
bool PartSkipDialog::IsAllCanceled()
{
for (auto &[part_id, part_state] : m_parts_state) {
if (part_state == PartState::psChecked) return false;
@@ -851,7 +851,7 @@ void PartSkipDialog::OnAllCheckbox(wxCommandEvent &event)
void PartSkipDialog::UpdateApplyButtonStatus()
{
if (IsAllCancled()) {
if (IsAllCanceled()) {
m_apply_btn->SetStyle(ButtonStyle::Regular, ButtonType::Choice);
m_apply_btn->SetToolTip(_L("Nothing selected"));
m_enable_apply_btn = false;
+1 -1
View File
@@ -153,7 +153,7 @@ private:
void UpdateDialogUI();
void UpdateApplyButtonStatus();
bool IsAllChecked();
bool IsAllCancled();
bool IsAllCanceled();
void OnRetryButton(wxCommandEvent &event);
void OnAllCheckbox(wxCommandEvent &event);
+44 -6
View File
@@ -12,6 +12,7 @@
#include <wx/stattext.h>
#include <wx/textctrl.h>
#include <wx/button.h>
#include <wx/choicdlg.h>
#include <wx/statbox.h>
#include <wx/wupdlock.h>
@@ -31,6 +32,7 @@
#include "PrintHostDialogs.hpp"
#include "../Utils/ASCIIFolding.hpp"
#include "../Utils/PrintHost.hpp"
#include "../Utils/Flashforge.hpp"
#include "../Utils/UndoRedo.hpp"
#include "RemovableDriveManager.hpp"
#include "BitmapCache.hpp"
@@ -205,12 +207,12 @@ void PhysicalPrinterDialog::build_printhost_settings(ConfigOptionsGroup* m_optgr
{
auto sizer = create_sizer_with_btn(parent, &m_printhost_browse_btn, "printer_host_browser", _L("Browse") + " " + dots);
m_printhost_browse_btn->Bind(wxEVT_BUTTON, [=](wxCommandEvent& e) {
const auto host_type = m_config->opt_enum<PrintHostType>("host_type");
// Creality K-series printers announce themselves via DNS-SD under a
// per-device-unique service type _Creality-<MAC-hex>._udp, so the
// standard fixed-service-name Bonjour browser does not find them.
// Dispatch to the Creality-specific scanner instead.
const auto* host_type_opt = m_config->option<ConfigOptionEnum<PrintHostType>>("host_type");
const auto host_type = host_type_opt ? host_type_opt->value : htOctoPrint;
if (host_type == htCrealityPrint) {
CrealityDiscoveryDialog dialog(this);
if (dialog.ShowModal() == wxID_OK && !dialog.selected_ip().empty()) {
@@ -223,11 +225,39 @@ void PhysicalPrinterDialog::build_printhost_settings(ConfigOptionsGroup* m_optgr
return;
}
if (host_type == htFlashforge) {
wxBusyCursor wait;
std::vector<FlashforgeDiscoveredPrinter> printers;
wxString error_msg;
if (!Flashforge::discover_printers(printers, error_msg)) {
show_error(this, error_msg);
return;
}
wxArrayString choices;
for (const auto& printer : printers)
choices.Add(from_u8((boost::format("%1% (%2%) [%3%]") % printer.name % printer.ip_address % printer.serial_number).str()));
wxSingleChoiceDialog dialog(this, _L("Select a Flashforge printer"), _L("Discovered Printers"), choices);
if (dialog.ShowModal() == wxID_OK) {
const int idx = dialog.GetSelection();
if (idx >= 0 && idx < static_cast<int>(printers.size())) {
m_optgroup->set_value("print_host", from_u8(printers[idx].ip_address), true);
m_optgroup->set_value("flashforge_serial_number", from_u8(printers[idx].serial_number), true);
m_config->opt_string("print_host") = printers[idx].ip_address;
m_config->opt_string("flashforge_serial_number") = printers[idx].serial_number;
update_printhost_buttons();
}
}
return;
}
BonjourDialog dialog(this, Preset::printer_technology(*m_config));
if (dialog.show_and_lookup()) {
m_optgroup->set_value("print_host", dialog.get_selected(), true);
m_optgroup->get_field("print_host")->field_changed();
}
}
});
return sizer;
@@ -348,6 +378,10 @@ void PhysicalPrinterDialog::build_printhost_settings(ConfigOptionsGroup* m_optgr
option.opt.width = Field::def_width_wider();
m_optgroup->append_single_option_line(option);
option = m_optgroup->get_option("flashforge_serial_number");
option.opt.width = Field::def_width_wider();
m_optgroup->append_single_option_line(option);
option = m_optgroup->get_option("printhost_port");
option.opt.width = Field::def_width_wider();
Line port_line = m_optgroup->create_single_option_line(option);
@@ -704,13 +738,17 @@ void PhysicalPrinterDialog::update(bool printer_change)
}
if (opt->value == htFlashforge) {
m_optgroup->hide_field("printhost_apikey");
m_optgroup->hide_field("printhost_authorization_type");
}
m_optgroup->show_field("printhost_apikey");
m_optgroup->show_field("flashforge_serial_number");
m_optgroup->hide_field("printhost_authorization_type");
} else {
m_optgroup->hide_field("flashforge_serial_number");
}
}
else {
m_optgroup->set_value("host_type", int(PrintHostType::htOctoPrint), false);
m_optgroup->hide_field("host_type");
m_optgroup->hide_field("flashforge_serial_number");
m_optgroup->show_field("printhost_authorization_type");
@@ -828,7 +866,7 @@ void PhysicalPrinterDialog::on_dpi_changed(const wxRect& suggested_rect)
void PhysicalPrinterDialog::check_host_key_valid()
{
std::vector<std::string> keys = {"print_host", "print_host_webui", "printhost_apikey", "printhost_cafile", "printhost_user", "printhost_password", "printhost_port"};
std::vector<std::string> keys = {"print_host", "print_host_webui", "printhost_apikey", "flashforge_serial_number", "printhost_cafile", "printhost_user", "printhost_password", "printhost_port"};
for (auto &key : keys) {
auto it = m_config->option<ConfigOptionString>(key);
if (!it) m_config->set_key_value(key, new ConfigOptionString(""));
+96 -6
View File
@@ -1854,6 +1854,14 @@ Sidebar::Sidebar(Plater *parent)
e.Skip();
});
w->Bind(wxEVT_LEAVE_WINDOW, [this, panel_color](wxMouseEvent &e) {
// Orca: if the edit button is already hidden the handler has no
// state to change, so skip the expensive wxFindWindowAtPoint tree
// walk. Without this guard, when the parent window is on an
// inactive Hyprland/Wayland workspace, GTK keeps delivering
// synthetic leave events and the Hide()+Layout() below re-enters
// the same handler in a feedback loop that pegs a CPU core.
// (IsShownOnScreen() can't be used here — see Plater.cpp:9304.)
if (!p->btn_edit_printer->IsShown()) { e.Skip(); return; }
// Use event-relative coords instead of wxGetMousePosition() which
// returns (0,0) on Wayland for global screen coordinates.
wxWindow* evtObj = dynamic_cast<wxWindow*>(e.GetEventObject());
@@ -6852,12 +6860,12 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
double preset_nozzle_diameter = 0.4;
const ConfigOption *opt = printer_preset.config.option("nozzle_diameter");
if (opt) preset_nozzle_diameter = static_cast<const ConfigOptionFloats *>(opt)->values[0];
float machine_nozzle_diameter = obj->GetExtderSystem()->GetNozzleDiameter(0);
std::string machine_type = obj->printer_type;
if (obj->is_support_upgrade_kit && obj->installed_upgrade_kit) machine_type = "C12";
if (printer_preset.get_current_printer_type(preset_bundle) != machine_type || !is_approx((float) preset_nozzle_diameter, machine_nozzle_diameter)) {
bool nozzle_mismatch = !obj->GetExtderSystem()->NozzleDiameterMatchesOrUnknown(0, (float) preset_nozzle_diameter);
if (printer_preset.get_current_printer_type(preset_bundle) != machine_type || nozzle_mismatch) {
Preset *machine_preset = get_printer_preset(obj);
if (machine_preset != nullptr) {
std::string printer_model = machine_preset->config.option<ConfigOptionString>("printer_model")->value;
@@ -9568,7 +9576,7 @@ void Plater::priv::on_select_preset(wxCommandEvent &evt)
const auto& extruders = obj->GetExtderSystem()->GetExtruders();
for (const DevExtder &extruder : extruders) {
if (!is_approx(extruder.GetNozzleDiameter(), float(preset_nozzle_diameter))) {
if (!obj->GetExtderSystem()->NozzleDiameterMatchesOrUnknown(extruder.GetExtId(), float(preset_nozzle_diameter))) {
same_nozzle_diameter = false;
}
}
@@ -16083,6 +16091,18 @@ void Plater::send_gcode_legacy(int plate_idx, Export3mfProgressFn proFn, bool us
if (upload_job.empty())
return;
const auto host_type_opt = physical_printer_config->option<ConfigOptionEnum<PrintHostType>>("host_type");
const auto host_type = host_type_opt != nullptr ? host_type_opt->value : htElegooLink;
const auto* ff_serial_opt = physical_printer_config->option<ConfigOptionString>("flashforge_serial_number");
const auto* ff_code_opt = physical_printer_config->option<ConfigOptionString>("printhost_apikey");
const bool flashforge_local_api =
host_type == htFlashforge &&
ff_serial_opt != nullptr && !ff_serial_opt->value.empty() &&
ff_code_opt != nullptr && !ff_code_opt->value.empty();
if (flashforge_local_api)
use_3mf = true;
upload_job.upload_data.use_3mf = use_3mf;
// Obtain default output path
@@ -16130,8 +16150,6 @@ void Plater::send_gcode_legacy(int plate_idx, Export3mfProgressFn proFn, bool us
{
auto preset_bundle = wxGetApp().preset_bundle;
const auto opt = physical_printer_config->option<ConfigOptionEnum<PrintHostType>>("host_type");
const auto host_type = opt != nullptr ? opt->value : htElegooLink;
auto config = get_app_config();
std::unique_ptr<PrintHostSendDialog> pDlg;
@@ -16144,6 +16162,77 @@ void Plater::send_gcode_legacy(int plate_idx, Export3mfProgressFn proFn, bool us
storage_paths, storage_names,
config->get_bool("open_device_tab_post_upload"),
upload_job.printhost.get());
} else if (flashforge_local_api) {
auto* flashforge_host = dynamic_cast<Flashforge*>(upload_job.printhost.get());
if (flashforge_host == nullptr) {
show_error(this, _L("Flashforge host is not available."), false);
return;
}
std::vector<FlashforgeMaterialSlot> slots;
bool supports_material_station = false;
{
wxBusyCursor wait;
wxString msg;
if (!flashforge_host->fetch_material_slots(slots, &supports_material_station, msg)) {
show_error(this, msg.empty() ? _L("Unable to log in to the Flashforge printer.") : msg, false);
return;
}
}
std::vector<FilamentInfo> project_filaments;
PlateDataPtrs plate_data_list;
DynamicPrintConfig cfg = wxGetApp().preset_bundle->full_config();
const auto* filament_color = dynamic_cast<const ConfigOptionStrings*>(cfg.option("filament_colour"));
const auto* filament_id_opt = dynamic_cast<const ConfigOptionStrings*>(cfg.option("filament_ids"));
const int resolved_plate_idx = plate_idx == PLATE_CURRENT_IDX ? get_partplate_list().get_curr_plate_index() : plate_idx;
auto enrich_project_filaments = [&](std::vector<FilamentInfo>& filaments) {
for (auto& filament : filaments) {
if (filament.id < 0)
continue;
std::string display_filament_type;
try {
filament.type = cfg.get_filament_type(display_filament_type, filament.id);
} catch (...) {
}
if (filament.type.empty())
filament.type = display_filament_type;
if (filament.type.empty())
filament.type = "Unknown";
filament.filament_id = filament_id_opt ? filament_id_opt->get_at(static_cast<size_t>(filament.id)) : "";
filament.color = filament_color ? filament_color->get_at(static_cast<size_t>(filament.id)) : "#FFFFFF";
if (filament.color.empty())
filament.color = "#FFFFFF";
}
};
p->partplate_list.store_to_3mf_structure(plate_data_list, true, plate_idx);
PlateData* selected_plate_data = (resolved_plate_idx >= 0 && resolved_plate_idx < static_cast<int>(plate_data_list.size())) ? plate_data_list[resolved_plate_idx] : nullptr;
if (selected_plate_data == nullptr && !plate_data_list.empty())
selected_plate_data = plate_data_list.front();
if (selected_plate_data != nullptr)
project_filaments = selected_plate_data->slice_filaments_info;
if (project_filaments.empty()) {
if (PartPlate* plate = get_partplate_list().get_plate(resolved_plate_idx); plate != nullptr)
project_filaments = plate->get_slice_filaments_info();
}
if (!project_filaments.empty())
enrich_project_filaments(project_filaments);
release_PlateData_list(plate_data_list);
pDlg = std::make_unique<FlashforgePrintHostSendDialog>(default_output_file, upload_job.printhost->get_post_upload_actions(), groups,
storage_paths, storage_names,
config->get_bool("open_device_tab_post_upload"),
flashforge_host,
supports_material_station,
std::move(slots),
project_filaments);
} else {
pDlg = std::make_unique<PrintHostSendDialog>(default_output_file, upload_job.printhost->get_post_upload_actions(), groups,
storage_paths, storage_names, config->get_bool("open_device_tab_post_upload"));
@@ -16173,7 +16262,8 @@ void Plater::send_gcode_legacy(int plate_idx, Export3mfProgressFn proFn, bool us
if (use_3mf) {
// Process gcode
const int result = send_gcode(plate_idx, nullptr);
const int export_plate_idx = plate_idx == PLATE_CURRENT_IDX ? get_partplate_list().get_curr_plate_index() : plate_idx;
const int result = send_gcode(export_plate_idx, nullptr);
if (result < 0) {
wxString msg = _L("Abnormal print file data. Please slice again");
+902
View File
@@ -1,7 +1,9 @@
#include "PrintHostDialogs.hpp"
#include <algorithm>
#include <cctype>
#include <iomanip>
#include <limits>
#include <wx/frame.h>
#include <wx/progdlg.h>
@@ -11,6 +13,9 @@
#include <wx/checkbox.h>
#include <wx/button.h>
#include <wx/dataview.h>
#include <wx/dcbuffer.h>
#include <wx/graphics.h>
#include <wx/wrapsizer.h>
#include <wx/wupdlock.h>
#include <wx/debug.h>
#include <wx/msgdlg.h>
@@ -18,6 +23,8 @@
#include <boost/log/trivial.hpp>
#include <boost/filesystem.hpp>
#include <boost/nowide/convert.hpp>
#include <boost/algorithm/string.hpp>
#include <nlohmann/json.hpp>
#include "GUI.hpp"
#include "GUI_App.hpp"
@@ -35,10 +42,383 @@
#include <nlohmann/json.hpp>
namespace fs = boost::filesystem;
using json = nlohmann::json;
namespace Slic3r {
namespace GUI {
namespace {
wxColour contrasting_text_color(const wxColour& background)
{
return background.GetLuminance() < 0.60 ? *wxWHITE : wxColour("#303030");
}
long long color_distance_sq(const wxColour& lhs, const wxColour& rhs)
{
const long long dr = static_cast<long long>(lhs.Red()) - static_cast<long long>(rhs.Red());
const long long dg = static_cast<long long>(lhs.Green()) - static_cast<long long>(rhs.Green());
const long long db = static_cast<long long>(lhs.Blue()) - static_cast<long long>(rhs.Blue());
return dr * dr + dg * dg + db * db;
}
class FlashforgeSlotCard : public wxPanel
{
public:
FlashforgeSlotCard(wxWindow* parent)
: wxPanel(parent, wxID_ANY)
{
SetDoubleBuffered(true);
SetMinSize(wxSize(FromDIP(68), FromDIP(92)));
SetMaxSize(GetMinSize());
Bind(wxEVT_PAINT, &FlashforgeSlotCard::on_paint, this);
Bind(wxEVT_ENTER_WINDOW, [this](wxMouseEvent& e) {
m_hover = true;
SetCursor(wxCursor(m_enabled ? wxCURSOR_HAND : wxCURSOR_NO_ENTRY));
Refresh();
e.Skip();
});
Bind(wxEVT_LEAVE_WINDOW, [this](wxMouseEvent& e) {
m_hover = false;
SetCursor(wxCursor(wxCURSOR_ARROW));
Refresh();
e.Skip();
});
Bind(wxEVT_LEFT_DOWN, &FlashforgeSlotCard::on_left_down, this);
}
void set_slot(const Slic3r::FlashforgeMaterialSlot& slot, bool enabled)
{
m_slot_id = slot.slot_id;
m_color = parse_color(slot.material_color);
m_name = slot.material_name.empty() ? _L("Unknown") : from_u8(slot.material_name);
m_empty = !slot.has_filament;
m_enabled = enabled && !m_empty;
Refresh();
}
private:
static wxColour parse_color(const std::string& raw)
{
wxColour color(from_u8(raw));
if (color.IsOk())
return color;
std::string value = raw;
boost::trim(value);
if (!value.empty() && value.front() != '#')
value.insert(value.begin(), '#');
color = wxColour(from_u8(value));
return color.IsOk() ? color : wxColour("#D0D0D0");
}
void on_left_down(wxMouseEvent& e)
{
if (!m_enabled)
return;
wxCommandEvent evt(wxEVT_BUTTON, GetId());
evt.SetInt(m_slot_id);
evt.SetString(m_color.GetAsString(wxC2S_HTML_SYNTAX));
wxPostEvent(this, evt);
e.Skip();
}
void on_paint(wxPaintEvent&)
{
wxPaintDC dc(this);
std::unique_ptr<wxGraphicsContext> gc(wxGraphicsContext::Create(dc));
if (gc == nullptr)
return;
const wxSize size = GetSize();
const int circle_size = FromDIP(24);
const int outline_width = (m_hover && m_enabled) ? FromDIP(2) : FromDIP(1);
const wxRect body_rect(FromDIP(7), FromDIP(26), size.x - FromDIP(14), size.y - FromDIP(32));
const wxColour badge_color = m_enabled ? wxSystemSettings::GetColour(wxSYS_COLOUR_HIGHLIGHT)
: wxSystemSettings::GetColour(wxSYS_COLOUR_GRAYTEXT);
const wxColour body_border = m_enabled ? wxSystemSettings::GetColour(wxSYS_COLOUR_BTNSHADOW)
: wxSystemSettings::GetColour(wxSYS_COLOUR_GRAYTEXT);
gc->SetPen(*wxTRANSPARENT_PEN);
gc->SetBrush(wxBrush((m_hover && m_enabled) ? badge_color.ChangeLightness(130) : badge_color));
gc->DrawEllipse((size.x - circle_size) / 2.0, 0, circle_size, circle_size);
dc.SetFont(::Label::Body_13);
dc.SetTextForeground(*wxWHITE);
const wxString slot_txt = wxString::Format("%d", m_slot_id);
const wxSize slot_size = dc.GetTextExtent(slot_txt);
dc.DrawText(slot_txt, (size.x - slot_size.x) / 2, (circle_size - slot_size.y) / 2);
const wxColour bg_color = m_empty ? wxColour("#F6F6F6") : m_color;
gc->SetPen(wxPen(body_border, outline_width));
gc->SetBrush(wxBrush(bg_color));
gc->DrawRoundedRectangle(body_rect.x, body_rect.y, body_rect.width, body_rect.height, FromDIP(8));
dc.SetFont(::Label::Body_12);
dc.SetTextForeground(contrasting_text_color(bg_color));
wxString label = m_empty ? _L("Empty") : m_name;
if (dc.GetTextExtent(label).x > body_rect.width - FromDIP(8))
dc.SetFont(::Label::Body_10);
if (dc.GetTextExtent(label).x > body_rect.width - FromDIP(8)) {
while (!label.empty() && dc.GetTextExtent(label + "...").x > body_rect.width - FromDIP(8))
label.RemoveLast();
label += "...";
}
const wxSize label_size = dc.GetTextExtent(label);
dc.DrawText(label, body_rect.x + (body_rect.width - label_size.x) / 2, body_rect.y + (body_rect.height - label_size.y) / 2);
}
private:
int m_slot_id {0};
wxColour m_color {*wxWHITE};
wxString m_name;
bool m_empty {true};
bool m_enabled {false};
bool m_hover {false};
};
class FlashforgeSlotDialog : public DPIDialog
{
public:
FlashforgeSlotDialog(wxWindow* parent, const wxString& material_name)
: DPIDialog(parent, wxID_ANY, _L("Choose a slot for the selected color"), wxDefaultPosition, wxDefaultSize, wxCAPTION | wxCLOSE_BOX)
, m_material_name(material_name)
{
SetFont(wxGetApp().normal_font());
SetBackgroundColour(*wxWHITE);
auto* root = new wxBoxSizer(wxVERTICAL);
auto* title = new wxStaticText(this, wxID_ANY, _L("Material in the material station"));
title->SetFont(::Label::Head_13);
root->Add(title, 0, wxALL | wxALIGN_CENTER_HORIZONTAL, FromDIP(12));
m_grid = new wxGridSizer(1, 4, FromDIP(10), FromDIP(12));
auto* grid_row = new wxBoxSizer(wxHORIZONTAL);
grid_row->AddStretchSpacer();
grid_row->Add(m_grid, 0);
grid_row->AddStretchSpacer();
root->Add(grid_row, 0, wxLEFT | wxRIGHT | wxEXPAND, FromDIP(18));
auto* tip = new wxStaticText(this, wxID_ANY, _L("Only materials of the same type can be selected."));
tip->SetForegroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_GRAYTEXT));
root->Add(tip, 0, wxALL | wxALIGN_CENTER_HORIZONTAL, FromDIP(12));
SetSizer(root);
SetMinSize(wxSize(FromDIP(460), FromDIP(240)));
for (int i = 0; i < 4; ++i) {
auto* card = new FlashforgeSlotCard(this);
card->Bind(wxEVT_BUTTON, [this](wxCommandEvent& e) {
m_selected_slot_id = e.GetInt();
m_selected_color = wxColour(e.GetString());
EndModal(wxID_OK);
});
m_cards.push_back(card);
m_grid->Add(card, 0);
}
wxGetApp().UpdateDlgDarkUI(this);
Layout();
Fit();
CenterOnParent();
Refresh();
}
void update_slots(const std::vector<Slic3r::FlashforgeMaterialSlot>& slots, const std::function<bool(const Slic3r::FlashforgeMaterialSlot&)>& matcher)
{
for (size_t i = 0; i < m_cards.size(); ++i) {
Slic3r::FlashforgeMaterialSlot slot;
slot.slot_id = static_cast<int>(i) + 1;
if (const auto it = std::find_if(slots.begin(), slots.end(), [&](const Slic3r::FlashforgeMaterialSlot& item) { return item.slot_id == slot.slot_id; }); it != slots.end())
slot = *it;
const bool enabled = slot.has_filament && matcher(slot);
m_cards[i]->set_slot(slot, enabled);
}
Layout();
Fit();
}
bool has_selection() const { return m_selected_slot_id > 0; }
int selected_slot_id() const { return m_selected_slot_id; }
wxColour selected_color() const { return m_selected_color; }
protected:
void on_dpi_changed(const wxRect& suggested_rect) override
{
Fit();
Refresh();
if (suggested_rect.IsEmpty())
return;
SetSize(suggested_rect.GetSize());
}
private:
wxString m_material_name;
wxGridSizer* m_grid {nullptr};
std::vector<FlashforgeSlotCard*> m_cards;
int m_selected_slot_id {0};
wxColour m_selected_color;
};
class FlashforgeMaterialMapWidget : public wxPanel
{
public:
using SelectFn = std::function<void(FlashforgeMaterialMapWidget*)>;
FlashforgeMaterialMapWidget(wxWindow* parent, int tool_id, const wxColour& color, const wxString& material_name, SelectFn on_select)
: wxPanel(parent, wxID_ANY)
, m_tool_id(tool_id)
, m_color(color)
, m_name(material_name.Strip())
, m_select_fn(std::move(on_select))
{
SetDoubleBuffered(true);
const wxSize size(FromDIP(72), FromDIP(58));
SetSize(size);
SetMinSize(size);
SetMaxSize(size);
Bind(wxEVT_PAINT, &FlashforgeMaterialMapWidget::on_paint, this);
Bind(wxEVT_LEFT_DOWN, &FlashforgeMaterialMapWidget::on_left_down, this);
}
int tool_id() const { return m_tool_id; }
int selected_slot_id() const { return m_slot_id; }
bool is_slot_selected() const { return m_slot_id > 0; }
wxString material_name() const { return m_name; }
void set_enable_mapping(bool enable)
{
if (m_mapping_enabled == enable)
return;
m_mapping_enabled = enable;
if (!enable)
reset_slot();
Enable(enable);
Refresh();
}
void set_slot_selection(int slot_id, const wxColour& slot_color)
{
m_slot_id = slot_id;
m_slot_color = slot_color;
Refresh();
}
void reset_slot()
{
m_slot_id = 0;
m_slot_color = wxColour("#DDDDDD");
Refresh();
}
void update_popup_slots(const std::vector<Slic3r::FlashforgeMaterialSlot>& slots, const std::function<bool(const Slic3r::FlashforgeMaterialSlot&)>& matcher)
{
m_slots_snapshot = slots;
m_matcher = matcher;
}
wxSize DoGetBestSize() const override
{
return wxSize(FromDIP(72), FromDIP(58));
}
private:
void on_left_down(wxMouseEvent& e)
{
if (!m_mapping_enabled)
return;
FlashforgeSlotDialog dlg(this, m_name);
dlg.update_slots(m_slots_snapshot, m_matcher);
m_selected = true;
Refresh();
if (dlg.ShowModal() == wxID_OK && dlg.has_selection()) {
m_slot_id = dlg.selected_slot_id();
m_slot_color = dlg.selected_color();
if (m_select_fn)
m_select_fn(this);
}
m_selected = false;
Refresh();
e.Skip();
}
void on_paint(wxPaintEvent&)
{
wxPaintDC dc(this);
std::unique_ptr<wxGraphicsContext> gc(wxGraphicsContext::Create(dc));
if (gc == nullptr)
return;
const wxSize size = GetSize();
const int half_h = size.y / 2;
gc->SetPen(*wxTRANSPARENT_PEN);
gc->SetBrush(wxBrush(m_color));
gc->DrawRoundedRectangle(0, 0, size.x, half_h, FromDIP(3));
gc->DrawRectangle(0, half_h - FromDIP(3), size.x, FromDIP(3));
gc->SetBrush(wxBrush(m_mapping_enabled ? m_slot_color : wxColour("#DDDDDD")));
gc->DrawRoundedRectangle(0, half_h, size.x, half_h, FromDIP(3));
gc->DrawRectangle(0, half_h, size.x, FromDIP(3));
if (m_selected) {
gc->SetPen(wxPen(wxColour("#00AE42"), FromDIP(2)));
gc->SetBrush(*wxTRANSPARENT_BRUSH);
gc->DrawRoundedRectangle(0, 0, size.x - FromDIP(1), size.y - FromDIP(1), FromDIP(3));
} else if (m_color.GetLuminance() > 0.95 || m_slot_color.GetLuminance() > 0.95) {
gc->SetPen(wxPen(wxColour("#ACACAC"), FromDIP(1)));
gc->SetBrush(*wxTRANSPARENT_BRUSH);
gc->DrawRoundedRectangle(0, 0, size.x - FromDIP(1), size.y - FromDIP(1), FromDIP(3));
}
dc.SetFont(::Label::Body_13);
dc.SetTextForeground(contrasting_text_color(m_color));
wxString top_text = m_name;
if (dc.GetTextExtent(top_text).x > size.x - FromDIP(10)) {
dc.SetFont(::Label::Body_10);
}
wxSize top_size = dc.GetTextExtent(top_text);
dc.DrawText(top_text, (size.x - top_size.x) / 2, (half_h - top_size.y) / 2);
dc.SetFont(::Label::Body_13);
dc.SetTextForeground(contrasting_text_color(m_slot_color));
const wxString bottom_text = m_slot_id > 0 ? wxString::Format("%d", m_slot_id) : "-";
const wxSize bottom_size = dc.GetTextExtent(bottom_text);
dc.DrawText(bottom_text, (size.x - bottom_size.x - FromDIP(10)) / 2, half_h + (half_h - bottom_size.y) / 2);
wxPoint pts[3] = {
wxPoint(size.x - FromDIP(18), half_h + half_h / 2 - FromDIP(2)),
wxPoint(size.x - FromDIP(10), half_h + half_h / 2 - FromDIP(2)),
wxPoint(size.x - FromDIP(14), half_h + half_h / 2 + FromDIP(3))
};
dc.SetBrush(wxBrush(contrasting_text_color(m_slot_color)));
dc.SetPen(*wxTRANSPARENT_PEN);
dc.DrawPolygon(3, pts);
}
private:
int m_tool_id {-1};
wxColour m_color;
wxString m_name;
wxColour m_slot_color {wxColour("#DDDDDD")};
int m_slot_id {0};
bool m_selected {false};
bool m_mapping_enabled {true};
SelectFn m_select_fn;
std::vector<Slic3r::FlashforgeMaterialSlot> m_slots_snapshot;
std::function<bool(const Slic3r::FlashforgeMaterialSlot&)> m_matcher;
};
static FlashforgeMaterialMapWidget* as_ff_map_widget(wxWindow* window)
{
return dynamic_cast<FlashforgeMaterialMapWidget*>(window);
}
} // namespace
static const char *CONFIG_KEY_PATH = "printhost_path";
static const char *CONFIG_KEY_GROUP = "printhost_group";
static const char* CONFIG_KEY_STORAGE = "printhost_storage";
@@ -262,6 +642,528 @@ void PrintHostSendDialog::EndModal(int ret)
MsgDialog::EndModal(ret);
}
FlashforgePrintHostSendDialog::FlashforgePrintHostSendDialog(const fs::path& path,
PrintHostPostUploadActions post_actions,
const wxArrayString& groups,
const wxArrayString& storage_paths,
const wxArrayString& storage_names,
bool switch_to_device_tab,
const Slic3r::Flashforge* host,
bool supports_material_station,
std::vector<Slic3r::FlashforgeMaterialSlot> slots,
const std::vector<FilamentInfo>& project_filaments)
: PrintHostSendDialog(path, post_actions, groups, storage_paths, storage_names, switch_to_device_tab)
, m_host(host)
, m_slots(std::move(slots))
, m_project_filaments(project_filaments)
{
m_supports_material_station = supports_material_station;
m_slots_loaded = !m_slots.empty();
}
void FlashforgePrintHostSendDialog::init()
{
const AppConfig* app_config = wxGetApp().app_config;
const auto& path = m_path;
std::string leveling = app_config->get("recent", CONFIG_KEY_LEVELING);
if (!leveling.empty())
m_leveling_before_print = leveling == "1";
std::string timelapse = app_config->get("recent", CONFIG_KEY_TIMELAPSE);
if (!timelapse.empty())
m_time_lapse_video = timelapse == "1";
// Flashforge local printing should default to IFS enabled when supported.
// We don't revive an old stale "0" here.
m_use_material_station = m_supports_material_station;
if (m_supports_material_station && !app_config->has("recent", CONFIG_KEY_IFS))
const_cast<AppConfig*>(app_config)->set("recent", CONFIG_KEY_IFS, "1");
this->SetMinSize(wxSize(560, 420));
auto* label_dir_hint = new wxStaticText(this, wxID_ANY, _L("Use forward slashes ( / ) as a directory separator if needed."));
label_dir_hint->Wrap(CONTENT_WIDTH * wxGetApp().em_unit());
content_sizer->Add(txt_filename, 0, wxEXPAND);
content_sizer->Add(label_dir_hint);
content_sizer->AddSpacer(VERT_SPACING);
wxString recent_path = from_u8(app_config->get("recent", CONFIG_KEY_PATH));
if (recent_path.Length() > 0 && recent_path[recent_path.Length() - 1] != '/')
recent_path += '/';
const auto recent_path_len = recent_path.Length();
recent_path += path.filename().wstring();
wxString stem(path.stem().wstring());
const auto stem_len = stem.Length();
txt_filename->SetValue(recent_path);
{
auto checkbox_sizer = new wxBoxSizer(wxHORIZONTAL);
auto checkbox = new ::CheckBox(this, wxID_APPLY);
checkbox->SetValue(m_switch_to_device_tab);
checkbox->Bind(wxEVT_TOGGLEBUTTON, [this](wxCommandEvent& e) {
auto* source = dynamic_cast<::CheckBox*>(e.GetEventObject());
if (source != nullptr)
source->SetValue(e.IsChecked());
m_switch_to_device_tab = e.IsChecked();
e.Skip();
});
checkbox_sizer->Add(checkbox, 0, wxALL | wxALIGN_CENTER, FromDIP(2));
auto checkbox_text = new wxStaticText(this, wxID_ANY, _L("Switch to Device tab after upload."));
checkbox_text->SetFont(::Label::Body_13);
checkbox_text->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#323A3D")));
checkbox_sizer->Add(checkbox_text, 0, wxALL | wxALIGN_CENTER, FromDIP(2));
content_sizer->Add(checkbox_sizer);
content_sizer->AddSpacer(VERT_SPACING);
}
m_flashforge_options_sizer = new wxBoxSizer(wxVERTICAL);
auto add_option_checkbox = [this](wxBoxSizer* parent, const wxString& label, bool value, std::function<void(bool)> setter, ::CheckBox** out = nullptr) {
auto row = new wxBoxSizer(wxHORIZONTAL);
auto checkbox = new ::CheckBox(this);
checkbox->SetValue(value);
checkbox->Bind(wxEVT_TOGGLEBUTTON, [setter](wxCommandEvent& e) {
auto* source = dynamic_cast<::CheckBox*>(e.GetEventObject());
if (source != nullptr)
source->SetValue(e.IsChecked());
setter(e.IsChecked());
e.Skip();
});
row->Add(checkbox, 0, wxALL | wxALIGN_CENTER, FromDIP(2));
auto text = new wxStaticText(this, wxID_ANY, label);
text->SetFont(::Label::Body_13);
text->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#323A3D")));
row->Add(text, 0, wxALL | wxALIGN_CENTER, FromDIP(2));
parent->Add(row);
parent->AddSpacer(FromDIP(6));
if (out != nullptr)
*out = checkbox;
};
add_option_checkbox(m_flashforge_options_sizer, _L("Leveling before print"), m_leveling_before_print,
[this](bool checked) { m_leveling_before_print = checked; }, &m_checkbox_leveling);
add_option_checkbox(m_flashforge_options_sizer, _L("Time-lapse"), m_time_lapse_video,
[this](bool checked) { m_time_lapse_video = checked; }, &m_checkbox_timelapse);
add_option_checkbox(m_flashforge_options_sizer, _L("Enable IFS"), m_use_material_station,
[this](bool checked) {
m_use_material_station = checked;
if (checked) {
ensure_slots_loaded();
rebuild_mapping_rows();
}
sync_mapping_section_visibility();
}, &m_checkbox_ifs);
if (m_checkbox_ifs != nullptr && !m_supports_material_station)
m_checkbox_ifs->Enable(false);
m_status_text = new wxStaticText(this, wxID_ANY, wxEmptyString);
m_status_text->SetFont(::Label::Body_12);
m_flashforge_options_sizer->Add(m_status_text, 0, wxTOP | wxBOTTOM, FromDIP(4));
m_mapping_section_sizer = new wxBoxSizer(wxVERTICAL);
m_mapping_wrap_sizer = new wxWrapSizer(wxHORIZONTAL, wxWRAPSIZER_DEFAULT_FLAGS);
m_mapping_section_sizer->Add(m_mapping_wrap_sizer, 0, wxTOP | wxALIGN_LEFT, FromDIP(10));
m_flashforge_options_sizer->Add(m_mapping_section_sizer, 0, wxEXPAND);
content_sizer->Add(m_flashforge_options_sizer, 0, wxEXPAND);
if (m_supports_material_station)
m_status_text->SetLabel(wxString::Format(_L("Detected %d IFS slots on printer."), static_cast<int>(m_slots.size())));
else
m_status_text->SetLabel(_L("This printer does not report a material station."));
rebuild_mapping_rows();
sync_mapping_section_visibility();
if (size_t extension_start = recent_path.find_last_of('.'); extension_start != std::string::npos)
m_valid_suffix = recent_path.substr(extension_start);
auto validate_path = [this](const wxString& filename) -> bool {
if (!filename.Lower().EndsWith(m_valid_suffix.Lower())) {
MessageDialog msg_wingow(this, wxString::Format(_L("Upload filename doesn't end with \"%s\". Do you wish to continue?"), m_valid_suffix),
wxString(SLIC3R_APP_NAME), wxYES | wxNO);
if (msg_wingow.ShowModal() == wxID_NO)
return false;
}
return validate_before_close();
};
auto* btn_ok = add_button(wxID_OK, true, _L("Upload"));
btn_ok->Bind(wxEVT_BUTTON, [this, validate_path](wxCommandEvent&) {
if (validate_path(txt_filename->GetValue())) {
post_upload_action = PrintHostPostUploadAction::None;
EndDialog(wxID_OK);
}
});
if (m_post_actions.has(PrintHostPostUploadAction::StartPrint)) {
auto* btn_print = add_button(wxID_YES, false, _L("Upload and Print"));
btn_print->Bind(wxEVT_BUTTON, [this, validate_path](wxCommandEvent&) {
if (validate_path(txt_filename->GetValue())) {
post_upload_action = PrintHostPostUploadAction::StartPrint;
EndDialog(wxID_OK);
}
});
}
add_button(wxID_CANCEL, false, _L("Cancel"));
finalize();
txt_filename->SetFocus();
#ifdef __linux__
txt_filename->Bind(wxEVT_KILL_FOCUS, [this](wxEvent& e) {
e.Skip();
txt_filename->SetInsertionPoint(txt_filename->GetLastPosition());
}, txt_filename->GetId());
#endif /* __linux__ */
Bind(wxEVT_SHOW, [=](const wxShowEvent&) {
CallAfter([=]() {
txt_filename->SetInsertionPoint(0);
txt_filename->SetSelection(recent_path_len, recent_path_len + stem_len);
});
});
}
void FlashforgePrintHostSendDialog::EndModal(int ret)
{
if (ret == wxID_OK) {
AppConfig* app_config = wxGetApp().app_config;
app_config->set("recent", CONFIG_KEY_LEVELING, m_leveling_before_print ? "1" : "0");
app_config->set("recent", CONFIG_KEY_TIMELAPSE, m_time_lapse_video ? "1" : "0");
app_config->set("recent", CONFIG_KEY_IFS, m_use_material_station ? "1" : "0");
}
PrintHostSendDialog::EndModal(ret);
}
std::map<std::string, std::string> FlashforgePrintHostSendDialog::extendedInfo() const
{
json mappings = json::array();
int mapped_count = 0;
if (m_use_material_station) {
for (const auto& row : m_mapping_rows) {
auto* card = as_ff_map_widget(row.card);
if (card == nullptr || row.tool_id < 0)
continue;
const int slot_id = card->selected_slot_id();
if (slot_id <= 0)
continue;
const auto filament_it = std::find_if(m_project_filaments.begin(), m_project_filaments.end(), [&](const FilamentInfo& item) { return item.id == row.tool_id; });
const auto slot_it = std::find_if(m_slots.begin(), m_slots.end(), [&](const FlashforgeMaterialSlot& slot) { return slot.slot_id == slot_id; });
if (filament_it == m_project_filaments.end() || slot_it == m_slots.end())
continue;
mappings.push_back({
{"toolId", filament_it->id},
{"slotId", slot_it->slot_id},
{"materialName", slot_it->material_name},
{"toolMaterialColor", filament_it->color},
{"slotMaterialColor", slot_it->material_color}
});
++mapped_count;
}
}
return {
{"levelingBeforePrint", m_leveling_before_print ? "1" : "0"},
{"timeLapseVideo", m_time_lapse_video ? "1" : "0"},
{"useMatlStation", m_use_material_station ? "1" : "0"},
{"gcodeToolCnt", std::to_string(mapped_count)},
{"materialMappings", mappings.dump()}
};
}
void FlashforgePrintHostSendDialog::load_slots()
{
m_slots.clear();
m_slots_loaded = false;
m_supports_material_station = false;
if (m_host == nullptr) {
m_status_text->SetLabel(_L("Flashforge host is not available."));
return;
}
wxString msg;
bool supports_material_station = false;
if (!m_host->fetch_material_slots(m_slots, &supports_material_station, msg)) {
m_status_text->SetLabel(msg.empty() ? _L("Unable to read IFS slots from printer.") : msg);
return;
}
m_supports_material_station = supports_material_station;
m_slots_loaded = !m_slots.empty();
m_use_material_station = m_supports_material_station;
if (m_supports_material_station)
m_status_text->SetLabel(wxString::Format(_L("Detected %d IFS slots on printer."), static_cast<int>(m_slots.size())));
else
m_status_text->SetLabel(_L("This printer does not report a material station."));
}
bool FlashforgePrintHostSendDialog::ensure_slots_loaded(bool force_reload)
{
if (!force_reload && (m_slots_loaded || !m_supports_material_station))
return m_slots_loaded;
if (m_status_text != nullptr)
m_status_text->SetLabel(_L("Loading IFS slots from printer..."));
wxBusyCursor wait;
load_slots();
return m_slots_loaded;
}
void FlashforgePrintHostSendDialog::rebuild_mapping_rows()
{
if (m_mapping_wrap_sizer == nullptr)
return;
m_mapping_wrap_sizer->Clear(true);
m_mapping_rows.clear();
if (m_project_filaments.empty()) {
m_mapping_wrap_sizer->Add(new wxStaticText(this, wxID_ANY, _L("Slice the plate first to get project material information.")), 0, wxALL, FromDIP(2));
return;
}
for (const auto& filament : m_project_filaments) {
auto* card = new FlashforgeMaterialMapWidget(this, filament.id, to_wx_colour(filament.color), from_u8(filament.get_display_filament_type()),
[this](FlashforgeMaterialMapWidget* changed_card) {
if (changed_card == nullptr)
return;
for (auto& row : m_mapping_rows) {
if (row.card == changed_card) {
refresh_mapping_card(row);
break;
}
}
});
m_mapping_wrap_sizer->Add(card, 0, wxRIGHT | wxBOTTOM | wxFIXED_MINSIZE, FromDIP(10));
MappingRow row;
row.tool_id = filament.id;
row.card = card;
m_mapping_rows.push_back(row);
}
auto_assign_mappings();
}
void FlashforgePrintHostSendDialog::auto_assign_mappings()
{
for (size_t idx = 0; idx < m_project_filaments.size() && idx < m_mapping_rows.size(); ++idx) {
auto& filament = m_project_filaments[idx];
auto* card = as_ff_map_widget(m_mapping_rows[idx].card);
if (card == nullptr)
continue;
const wxColour filament_color = to_wx_colour(filament.color);
const Slic3r::FlashforgeMaterialSlot* best_slot = nullptr;
long long best_distance = std::numeric_limits<long long>::max();
for (const auto& slot : m_slots) {
if (!slot.has_filament || !slot_matches_filament(slot, filament))
continue;
const long long distance = color_distance_sq(filament_color, to_wx_colour(slot.material_color));
if (best_slot == nullptr || distance < best_distance) {
best_slot = &slot;
best_distance = distance;
}
}
if (best_slot != nullptr)
card->set_slot_selection(best_slot->slot_id, to_wx_colour(best_slot->material_color));
else
card->reset_slot();
refresh_mapping_card(m_mapping_rows[idx]);
}
}
void FlashforgePrintHostSendDialog::refresh_mapping_card(MappingRow& row)
{
auto* card = as_ff_map_widget(row.card);
if (card == nullptr)
return;
const auto* filament = find_filament_by_tool_id(row.tool_id);
card->set_enable_mapping(m_use_material_station);
card->update_popup_slots(m_slots, [this, filament](const FlashforgeMaterialSlot& slot) {
return filament != nullptr && slot_matches_filament(slot, *filament);
});
if (card->selected_slot_id() <= 0) {
card->reset_slot();
return;
}
const auto* slot = find_slot_by_id(std::to_string(card->selected_slot_id()));
if (slot == nullptr) {
card->reset_slot();
return;
}
card->set_slot_selection(slot->slot_id, to_wx_colour(slot->material_color));
}
void FlashforgePrintHostSendDialog::sync_mapping_section_visibility()
{
if (m_mapping_section_sizer == nullptr)
return;
m_mapping_section_sizer->ShowItems(m_use_material_station && m_supports_material_station);
if (wxSizer* sizer = GetSizer(); sizer != nullptr) {
sizer->Layout();
sizer->Fit(this);
SetMinSize(GetBestSize());
}
Layout();
Fit();
}
const Slic3r::FlashforgeMaterialSlot* FlashforgePrintHostSendDialog::find_slot_by_id(const std::string& slot_id_text) const
{
const auto slot_it = std::find_if(m_slots.begin(), m_slots.end(), [&](const FlashforgeMaterialSlot& slot) { return std::to_string(slot.slot_id) == slot_id_text; });
return slot_it == m_slots.end() ? nullptr : &(*slot_it);
}
const FilamentInfo* FlashforgePrintHostSendDialog::find_filament_by_tool_id(int tool_id) const
{
const auto filament_it = std::find_if(m_project_filaments.begin(), m_project_filaments.end(), [&](const FilamentInfo& filament) { return filament.id == tool_id; });
return filament_it == m_project_filaments.end() ? nullptr : &(*filament_it);
}
bool FlashforgePrintHostSendDialog::slot_matches_filament(const Slic3r::FlashforgeMaterialSlot& slot, const FilamentInfo& filament) const
{
if (!slot.has_filament)
return false;
const std::string project_material = normalize_material(!filament.type.empty() ? filament.type : filament.get_display_filament_type());
const std::string slot_material = normalize_material(slot.material_name);
return !project_material.empty() && !slot_material.empty() && project_material == slot_material;
}
bool FlashforgePrintHostSendDialog::validate_before_close()
{
if (!m_use_material_station && m_project_filaments.size() > 1) {
show_error(this, _L("This plate uses multiple materials. Enable IFS and assign each tool to a printer slot."));
return false;
}
if (!m_use_material_station)
return true;
for (const auto& row : m_mapping_rows) {
auto* card = as_ff_map_widget(row.card);
if (card == nullptr || !card->is_slot_selected()) {
show_error(this, _L("Each project material must be assigned to an IFS slot before printing."));
return false;
}
const auto* slot = find_slot_by_id(std::to_string(card->selected_slot_id()));
const auto* filament = find_filament_by_tool_id(row.tool_id);
if (slot == nullptr || filament == nullptr || !slot->has_filament) {
show_error(this, _L("Each project material must be assigned to a loaded IFS slot before printing."));
return false;
}
if (!slot_matches_filament(*slot, *filament)) {
show_error(this, _L("Each project material must match the material loaded in the selected IFS slot."));
return false;
}
}
return true;
}
std::string FlashforgePrintHostSendDialog::normalize_material(const std::string& material) const
{
std::string normalized = boost::to_upper_copy(material);
normalized.erase(std::remove_if(normalized.begin(), normalized.end(), [](unsigned char ch) { return !std::isalnum(ch); }), normalized.end());
if (normalized.empty())
return {};
if (normalized.find("SILK") != std::string::npos)
return "SILK";
if (normalized.find("PLA") != std::string::npos && normalized.find("CF") != std::string::npos)
return "PLACF";
if (normalized.find("PETG") != std::string::npos && normalized.find("CF") != std::string::npos)
return "PETGCF";
if (normalized == "PLA" || normalized == "PLA+" || normalized == "PLAPLUS")
return "PLA";
if (normalized.find("PLA") != std::string::npos)
return "PLA";
if (normalized == "ABS" || normalized.find("ABS") != std::string::npos)
return "ABS";
if (normalized == "ASA" || normalized.find("ASA") != std::string::npos)
return "ABS";
if (normalized.find("PETG") != std::string::npos)
return "PETG";
if (normalized.find("TPU") != std::string::npos || normalized.find("TPE") != std::string::npos || normalized.find("FLEX") != std::string::npos)
return "TPU";
return normalized;
}
wxColour FlashforgePrintHostSendDialog::to_wx_colour(const std::string& color) const
{
wxColour wx_color(from_u8(color));
if (wx_color.IsOk())
return wx_color;
std::string normalized = boost::trim_copy(color);
if (boost::istarts_with(normalized, "0x"))
normalized = normalized.substr(2);
if (!normalized.empty() && normalized.front() == '#')
normalized.erase(normalized.begin());
if (normalized.size() == 8) {
auto hex_to_byte = [](char hi, char lo) -> int {
auto hex_val = [](char c) -> int {
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
return -1;
};
const int h = hex_val(hi);
const int l = hex_val(lo);
return (h < 0 || l < 0) ? -1 : h * 16 + l;
};
const int r = hex_to_byte(normalized[0], normalized[1]);
const int g = hex_to_byte(normalized[2], normalized[3]);
const int b = hex_to_byte(normalized[4], normalized[5]);
const int a = hex_to_byte(normalized[6], normalized[7]);
if (r >= 0 && g >= 0 && b >= 0 && a >= 0)
return wxColour(r, g, b, a);
}
if (normalized.size() == 6) {
wx_color = wxColour("#" + from_u8(normalized));
if (wx_color.IsOk())
return wx_color;
}
return wxColour("#999999");
}
wxDEFINE_EVENT(EVT_PRINTHOST_PROGRESS, PrintHostQueueDialog::Event);
wxDEFINE_EVENT(EVT_PRINTHOST_ERROR, PrintHostQueueDialog::Event);
wxDEFINE_EVENT(EVT_PRINTHOST_CANCEL, PrintHostQueueDialog::Event);
+66 -2
View File
@@ -1,8 +1,8 @@
#ifndef slic3r_PrintHostSendDialog_hpp_
#define slic3r_PrintHostSendDialog_hpp_
#include <set>
#include <string>
#include <vector>
#include <boost/filesystem/path.hpp>
#include <wx/string.h>
@@ -12,12 +12,18 @@
#include "GUI_Utils.hpp"
#include "MsgDialog.hpp"
#include "../Utils/PrintHost.hpp"
#include "../Utils/Flashforge.hpp"
#include "libslic3r/PrintConfig.hpp"
#include "libslic3r/ProjectTask.hpp"
class wxButton;
class wxTextCtrl;
class wxChoice;
class wxComboBox;
class ComboBox;
class wxDataViewListCtrl;
class wxFlexGridSizer;
class wxStaticText;
class wxWrapSizer;
class CheckBox;
namespace Slic3r { namespace GUI { class BitmapComboBox; } }
@@ -211,6 +217,64 @@ private:
};
std::vector<SlotInfo> m_printer_slots;
std::vector<BitmapComboBox*> m_slot_combos; // one per gcode filament
class FlashforgePrintHostSendDialog : public PrintHostSendDialog
{
public:
FlashforgePrintHostSendDialog(const boost::filesystem::path& path,
PrintHostPostUploadActions post_actions,
const wxArrayString& groups,
const wxArrayString& storage_paths,
const wxArrayString& storage_names,
bool switch_to_device_tab,
const Slic3r::Flashforge* host,
bool supports_material_station,
std::vector<Slic3r::FlashforgeMaterialSlot> slots,
const std::vector<FilamentInfo>& project_filaments);
virtual void init() override;
virtual void EndModal(int ret) override;
virtual std::map<std::string, std::string> extendedInfo() const override;
private:
struct MappingRow {
int tool_id {-1};
wxWindow* card {nullptr};
};
void load_slots();
bool ensure_slots_loaded(bool force_reload = false);
void rebuild_mapping_rows();
void auto_assign_mappings();
void refresh_mapping_card(MappingRow& row);
void sync_mapping_section_visibility();
const Slic3r::FlashforgeMaterialSlot* find_slot_by_id(const std::string& slot_id_text) const;
const FilamentInfo* find_filament_by_tool_id(int tool_id) const;
bool slot_matches_filament(const Slic3r::FlashforgeMaterialSlot& slot, const FilamentInfo& filament) const;
bool validate_before_close();
std::string normalize_material(const std::string& material) const;
wxColour to_wx_colour(const std::string& color) const;
private:
const Slic3r::Flashforge* m_host {nullptr};
std::vector<FilamentInfo> m_project_filaments;
std::vector<Slic3r::FlashforgeMaterialSlot> m_slots;
std::vector<MappingRow> m_mapping_rows;
wxBoxSizer* m_flashforge_options_sizer {nullptr};
wxBoxSizer* m_mapping_section_sizer {nullptr};
wxWrapSizer* m_mapping_wrap_sizer {nullptr};
wxStaticText* m_status_text {nullptr};
::CheckBox* m_checkbox_leveling {nullptr};
::CheckBox* m_checkbox_timelapse {nullptr};
::CheckBox* m_checkbox_ifs {nullptr};
bool m_leveling_before_print {true};
bool m_time_lapse_video {false};
bool m_use_material_station {false};
bool m_supports_material_station {false};
bool m_slots_loaded {false};
const char* CONFIG_KEY_LEVELING = "flashforge_leveling_before_print";
const char* CONFIG_KEY_TIMELAPSE = "flashforge_timelapse_video";
const char* CONFIG_KEY_IFS = "flashforge_use_material_station";
};
wxDECLARE_EVENT(EVT_PRINTHOST_PROGRESS, PrintHostQueueDialog::Event);
+8
View File
@@ -242,6 +242,14 @@ void PrinterWebView::SendAPIKey()
)",
m_apikey);
m_browser->RemoveAllUserScripts();
#ifdef _WIN32
// RemoveAllUserScripts causes WebView2 to forget about our script message handler,
// so re-add it here.
m_browser->RemoveScriptMessageHandler("wx");
m_browser->AddScriptMessageHandler("wx");
#endif
#ifdef __linux__
// Re-inject the vue-resize/WebKitGTK workaround that RemoveAllUserScripts just cleared.
inject_vue_resize_workaround(m_browser);
-52
View File
@@ -1912,58 +1912,6 @@ bool SyncAmsInfoDialog::is_blocking_printing(MachineObject *obj_)
return false;
}
bool SyncAmsInfoDialog::is_same_nozzle_diameters(NozzleType &tag_nozzle_type, float &nozzle_diameter)
{
bool is_same_nozzle_diameters = true;
float preset_nozzle_diameters;
std::string preset_nozzle_type;
DeviceManager *dev = Slic3r::GUI::wxGetApp().getDeviceManager();
if (!dev) return true;
MachineObject *obj_ = dev->get_selected_machine();
if (obj_ == nullptr) return true;
try {
PresetBundle *preset_bundle = wxGetApp().preset_bundle;
auto opt_nozzle_diameters = preset_bundle->printers.get_edited_preset().config.option<ConfigOptionFloats>("nozzle_diameter");
const ConfigOptionEnumsGenericNullable *nozzle_type = preset_bundle->printers.get_edited_preset().config.option<ConfigOptionEnumsGenericNullable>("nozzle_type");
std::vector<std::string> preset_nozzle_types(nozzle_type->size());
for (size_t idx = 0; idx < nozzle_type->size(); ++idx) preset_nozzle_types[idx] = NozzleTypeEumnToStr[NozzleType(nozzle_type->values[idx])];
std::vector<std::string> machine_nozzle_types(obj_->GetExtderSystem()->GetTotalExtderCount());
for (size_t idx = 0; idx < obj_->GetExtderSystem()->GetTotalExtderCount(); ++idx) machine_nozzle_types[idx] = obj_->GetExtderSystem()->GetNozzleType(idx);
auto used_filaments = wxGetApp().plater()->get_partplate_list().get_curr_plate()->get_used_filaments(); // 1 based
auto filament_maps = wxGetApp().plater()->get_partplate_list().get_curr_plate()->get_real_filament_maps(preset_bundle->project_config); // 1 based
std::vector<int> used_extruders; // 0 based
for (auto f : used_filaments) {
int filament_extruder = filament_maps[f - 1] - 1;
if (std::find(used_extruders.begin(), used_extruders.end(), filament_extruder) == used_extruders.end()) used_extruders.emplace_back(filament_extruder);
}
std::sort(used_extruders.begin(), used_extruders.end());
// TODO [tao wang] : add idx mapping
tag_nozzle_type = obj_->GetExtderSystem()->GetNozzleType(0);
if (opt_nozzle_diameters != nullptr) {
for (auto i = 0; i < used_extruders.size(); i++) {
auto extruder = used_extruders[i];
preset_nozzle_diameters = float(opt_nozzle_diameters->get_at(extruder));
if (preset_nozzle_diameters != obj_->GetExtderSystem()->GetNozzleDiameter(0)) { is_same_nozzle_diameters = false; }
}
}
} catch (...) {}
nozzle_diameter = preset_nozzle_diameters;
return is_same_nozzle_diameters;
}
bool SyncAmsInfoDialog::is_same_nozzle_type(std::string &filament_type, NozzleType &tag_nozzle_type)
{
bool is_same_nozzle_type = true;
-1
View File
@@ -203,7 +203,6 @@ public:
void update_timelapse_enable_status();
bool is_same_printer_model();
bool is_blocking_printing(MachineObject *obj_);
bool is_same_nozzle_diameters(NozzleType &tag_nozzle_type, float &nozzle_diameter);
bool is_same_nozzle_type(std::string &filament_type, NozzleType &tag_nozzle_type);
bool is_timeout();
int update_print_required_data(Slic3r::DynamicPrintConfig config, Slic3r::Model model, Slic3r::PlateDataPtrs plate_data_list, std::string file_name, std::string file_path);
+6 -3
View File
@@ -424,9 +424,12 @@ void Tab::create_preset_tab()
});
m_top_sizer->Add(m_mode_icon, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(SidebarProps::WideSpacing()));
m_mode_view = new ModeSwitchButton(m_top_panel);
m_mode_view->SetSelection(mode_to_selection(wxGetApp().get_saved_mode()));
if (wxGetApp().get_mode() == comDevelop)
if (wxGetApp().get_mode() == comDevelop) {
m_mode_view->SetSelection(mode_to_selection(comExpert));
m_mode_view->Enable(false);
} else {
m_mode_view->SetSelection(mode_to_selection(wxGetApp().get_saved_mode()));
}
m_top_sizer->AddSpacer(FromDIP(SidebarProps::ElementSpacing()));
m_top_sizer->Add( m_mode_view, 0, wxALIGN_CENTER_VERTICAL);
}
@@ -2445,7 +2448,7 @@ void TabPrint::build()
optgroup->append_single_option_line("sparse_infill_density", "strength_settings_infill#sparse-infill-density");
optgroup->append_single_option_line("fill_multiline", "strength_settings_infill#fill-multiline");
optgroup->append_single_option_line("sparse_infill_pattern", "strength_settings_infill#sparse-infill-pattern");
optgroup->append_single_option_line("gyroid_optimized", "strength_settings_patterns#gyroid_optimized");
optgroup->append_single_option_line("gyroid_optimized", "strength_settings_patterns#gyroid-optimized");
optgroup->append_single_option_line("infill_direction", "strength_settings_infill#direction");
optgroup->append_single_option_line("sparse_infill_rotate_template", "strength_settings_infill_rotation_template_metalanguage");
optgroup->append_single_option_line("skin_infill_density", "strength_settings_patterns#locked-zag");
+1 -1
View File
@@ -146,7 +146,6 @@ protected:
//BBS: GUI refactor
wxPanel* m_top_panel;
ScalableButton* m_mode_icon; // ORCA m_static_title replacement
wxBoxSizer* m_main_sizer;
wxBoxSizer* m_top_sizer;
wxBoxSizer* m_top_left_sizer;
@@ -307,6 +306,7 @@ public:
int m_update_cnt = 0;
ModeSwitchButton *m_mode_view = nullptr;
ScalableButton* m_mode_icon = nullptr; // ORCA m_static_title replacement
SwitchButton *m_extruder_switch = nullptr;
MultiSwitchButton *m_variant_combo = nullptr;
+92 -1
View File
@@ -5,6 +5,7 @@
#include "libslic3r/AppConfig.hpp"
#include "slic3r/GUI/wxExtensions.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/Utils/NetworkAgent.hpp"
#include "libslic3r_version.h"
#include <wx/sizer.h>
@@ -371,7 +372,97 @@ void ZUserLogin::OnScriptMessage(wxWebViewEvent &evt)
{
m_AutotestToken = j["data"]["token"];
}
if (strCmd == "user_login") {
if (strCmd == "user_ticket_login") {
auto* agent = wxGetApp().getAgent();
if (!agent || !m_cloud_agent || !j.contains("data") || !j["data"].is_object() || !j["data"].contains("ticket")) {
wxMessageBox(_L("Login failed. Please try again."), _L("Login"), wxICON_WARNING);
return;
}
const auto provider = m_cloud_agent->get_id();
std::string ticket = j["data"]["ticket"].get<std::string>();
unsigned int token_http_code = 0;
std::string token_body;
int token_result = agent->get_my_token(ticket, &token_http_code, &token_body, provider);
if (token_result != 0) {
BOOST_LOG_TRIVIAL(warning) << "embedded_login: get_my_token failed, http_code=" << token_http_code;
wxMessageBox(_L("Login failed. Please try again."), _L("Login"), wxICON_WARNING);
return;
}
std::string access_token;
std::string refresh_token;
std::string expires_in_str;
std::string refresh_expires_in_str;
try {
json token_j = json::parse(token_body);
if (token_j.contains("accessToken"))
access_token = token_j["accessToken"].get<std::string>();
if (token_j.contains("refreshToken"))
refresh_token = token_j["refreshToken"].get<std::string>();
if (token_j.contains("expiresIn"))
expires_in_str = std::to_string(token_j["expiresIn"].get<double>());
if (token_j.contains("refreshExpiresIn"))
refresh_expires_in_str = std::to_string(token_j["refreshExpiresIn"].get<double>());
} catch (...) {
wxMessageBox(_L("Login failed. Please try again."), _L("Login"), wxICON_WARNING);
return;
}
if (access_token.empty()) {
wxMessageBox(_L("Login failed. Please try again."), _L("Login"), wxICON_WARNING);
return;
}
unsigned int profile_http_code = 0;
std::string profile_body;
int profile_result = agent->get_my_profile(access_token, &profile_http_code, &profile_body, provider);
if (profile_result != 0) {
BOOST_LOG_TRIVIAL(warning) << "embedded_login: get_my_profile failed, http_code=" << profile_http_code;
wxMessageBox(_L("Login failed. Please try again."), _L("Login"), wxICON_WARNING);
return;
}
std::string user_id;
std::string user_name;
std::string user_account;
std::string user_avatar;
try {
json user_j = json::parse(profile_body);
if (user_j.contains("uidStr"))
user_id = user_j["uidStr"].get<std::string>();
if (user_j.contains("name"))
user_name = user_j["name"].get<std::string>();
if (user_j.contains("avatar"))
user_avatar = user_j["avatar"].get<std::string>();
if (user_j.contains("account"))
user_account = user_j["account"].get<std::string>();
} catch (...) {
BOOST_LOG_TRIVIAL(warning) << "embedded_login: profile JSON parse failed";
}
json login_j;
login_j["command"] = "user_login";
login_j["data"]["autotest_token"] = m_AutotestToken;
login_j["data"]["refresh_token"] = refresh_token;
login_j["data"]["token"] = access_token;
login_j["data"]["expires_in"] = expires_in_str;
login_j["data"]["refresh_expires_in"] = refresh_expires_in_str;
login_j["data"]["user"]["uid"] = user_id;
login_j["data"]["user"]["name"] = user_name;
login_j["data"]["user"]["account"] = user_account;
login_j["data"]["user"]["avatar"] = user_avatar;
std::string message_json = login_j.dump();
// End modal dialog first to unblock event loop before processing callbacks
EndModal(wxID_OK);
// Handle message after modal dialog ends to avoid deadlock
// Use wxTheApp->CallAfter to ensure it runs after modal loop exits
wxTheApp->CallAfter([message_json, provider]() { wxGetApp().handle_script_message(message_json, provider); });
}
else if (strCmd == "user_login") {
j["data"]["autotest_token"] = m_AutotestToken;
std::string message_json = j.dump();
+2 -1
View File
@@ -42,7 +42,8 @@ static std::map<wxColour, wxColour> gDarkColors{
{"#D7E8DE", "#1F2B27"}, // rgb(215, 232, 222) Not Used anymore // Leftover from BBS
{"#2B3436", "#808080"}, // rgb(43, 52, 54) Not Used anymore // Leftover from BBS. Was used as main fill color of icons
{"#ABABAB", "#ABABAB"},
{"#D9D9D9", "#2D2D32"}, // rgb(217, 217, 217) Sidebar > Toggle button track color
{"#D9D9D9", "#27272A"}, // rgb(217, 217, 217) Sidebar > Toggle button track color
{"#FFFEFE", "#D9D9D9"}, // rgb(255, 254, 254) Sidebar > Toggle button thumb color
{"#EBF9F0", "#293F34"},
//{"#F0F0F0", "#4C4C54"},
// ORCA
+12 -14
View File
@@ -193,20 +193,18 @@ void StaticBox::doRender(wxDC& dc)
if ((border_width && border_color.count() > 0) || background_color.count() > 0) {
wxRect rc(0, 0, size.x, size.y);
if (border_width && border_color.count() > 0) {
if (dc.GetContentScaleFactor() == 1.0) {
int d = floor(border_width / 2.0);
int d2 = floor(border_width - 1);
rc.x += d;
rc.width -= d2;
rc.y += d;
rc.height -= d2;
} else {
int d = 1;
rc.x += d;
rc.width -= d;
rc.y += d;
rc.height -= d;
}
const double scale = dc.GetContentScaleFactor();
// Snap rect edges to physical pixel boundaries so the 1px pen doesn't straddle a pixel boundary
auto snap = [&](int logical) -> int {
return (int)(ceil(logical * scale) / scale);
};
int deflate = snap(border_width / 2.0); // at 175%: snap(0.5) = snap→1/1.75 ≈ 1
rc.x += deflate;
rc.y += deflate;
rc.width -= deflate * 2;
rc.height -= deflate * 2;
dc.SetPen(wxPen(border_color.colorForStates(states), border_width, border_style));
} else {
dc.SetPen(wxPen(background_color.colorForStates(states)));
+84 -51
View File
@@ -220,14 +220,40 @@ void SwitchButton::update()
ModeSwitchButton::ModeSwitchButton(wxWindow* parent, wxWindowID id)
{
background_color = StateColor(
std::make_pair(wxColour(0xF1, 0xF1, 0xF1), (int) StateColor::Disabled),
std::make_pair(wxColour(0xE3, 0xE3, 0xE3), (int) StateColor::Pressed),
std::make_pair(wxColour(0xD9, 0xD9, 0xD9), (int) StateColor::Normal));
std::make_pair(wxColour("#D9D9D9"), (int) StateColor::Disabled),
std::make_pair(wxColour("#D9D9D9"), (int) StateColor::Normal)
);
border_color = StateColor(
std::make_pair(wxColour(0xEA, 0xEA, 0xEA), (int) StateColor::Disabled),
std::make_pair(wxColour(0xBC, 0xBC, 0xBC), (int) StateColor::Hovered),
std::make_pair(wxColour(0xC8, 0xC8, 0xC8), (int) StateColor::Focused),
std::make_pair(wxColour(0xCE, 0xCE, 0xCE), (int) StateColor::Normal));
std::make_pair(wxColour("#D9D9D9"), (int) StateColor::Disabled),
std::make_pair(wxColour("#D9D9D9"), (int) StateColor::Hovered | ~StateColor::Focused),
std::make_pair(wxColour("#26A69A"), (int) StateColor::Focused),
std::make_pair(wxColour("#D9D9D9"), (int) StateColor::Normal)
);
track_background = StateColor(
std::make_pair(wxColour("#009688"), (int) StateColor::Disabled),
std::make_pair(wxColour("#009688"), (int) StateColor::Normal)
);
track_border = StateColor(
std::make_pair(wxColour("#D9D9D9"), (int) StateColor::Disabled),
std::make_pair(wxColour("#009688"), (int) StateColor::Hovered | ~StateColor::Focused),
std::make_pair(wxColour("#26A69A"), (int) StateColor::Focused),
std::make_pair(wxColour("#009688"), (int) StateColor::Normal)
);
dot_active = StateColor(
std::make_pair(wxColour("#FFFEFE"), (int) StateColor::Disabled),
std::make_pair(wxColour("#FFFEFE"), (int) StateColor::Normal)
);
dot_dimmed = StateColor(
std::make_pair(wxColour("#EEEEEE"), (int) StateColor::Disabled),
std::make_pair(wxColour("#EEEEEE"), (int) StateColor::Normal)
);
text_color = StateColor(
std::make_pair(wxColour("#6B6B6B"), (int) StateColor::Disabled),
std::make_pair(wxColour("#6B6B6B"), (int) StateColor::Normal)
);
state_handler.attach(std::vector<StateColor const*>{&dot_active, &dot_dimmed, &text_color});
state_handler.update_binds();
StaticBox::Create(parent, id, wxDefaultPosition, wxDefaultSize, 0);
SetBackgroundColour(StaticBox::GetParentBackgroundColor(parent));
@@ -263,7 +289,7 @@ void ModeSwitchButton::SelectAndNotify(int selection)
void ModeSwitchButton::Rescale()
{
const wxSize button_size = FromDIP(wxSize(48, 20));
const wxSize button_size = FromDIP(wxSize(48, 18));
SetMinSize(button_size);
SetMaxSize(button_size);
SetSize(button_size);
@@ -274,63 +300,70 @@ void ModeSwitchButton::Rescale()
bool ModeSwitchButton::Enable(bool enable /* = true */)
{
const bool changed = StaticBox::Enable(enable);
if (changed)
if (changed){
wxCommandEvent e(EVT_ENABLE_CHANGED);
e.SetEventObject(this);
GetEventHandler()->ProcessEvent(e);
m_enabled = enable; // IsEnabled() not works because variable changes after paint event
Refresh();
}
return changed;
}
void ModeSwitchButton::doRender(wxDC& dc)
{
dc.SetPen(*wxTRANSPARENT_PEN);
dc.SetBrush(wxBrush(GetBackgroundColour()));
dc.DrawRectangle(GetClientRect());
const wxRect bounds = GetClientRect().Deflate(1);
const wxRect bounds = GetClientRect();
if (bounds.width <= 0 || bounds.height <= 0)
return;
const int states = state_handler.states();
const bool hovered = (states & StateHandler::Hovered) != 0;
const bool focused = (states & StateHandler::Focused) != 0;
const bool disabled = !IsEnabled();
const wxColour track_fill = disabled ? wxColour(0xD0, 0xD0, 0xD4) :
m_pressed ? wxColour(0x5A, 0x5D, 0x64) : wxColour(0x66, 0x69, 0x70);
const wxColour track_border = disabled ? wxColour(0xDD, 0xDD, 0xE0) :
focused ? wxColour("#009688") :
hovered ? wxColour(0x7A, 0x7D, 0x84) : wxColour(0x75, 0x78, 0x7F);
const wxColour active_fill = disabled ? wxColour(0x9E, 0xBE, 0xB9) :
m_pressed ? wxColour(0x00877B) : wxColour("#009688");
const wxColour active_dot = disabled ? wxColour(0xEC, 0xF4, 0xF2) : wxColour(0xB7, 0xEB, 0xE3);
const wxColour inactive_dot = disabled ? wxColour(0xF2, 0xF2, 0xF4) : wxColour(0xB5, 0xB7, 0xBD);
const wxColour thumb_fill = disabled ? wxColour(0xFA, 0xFA, 0xFA) : *wxWHITE;
const wxColour thumb_border = disabled ? wxColour(0xE7, 0xE7, 0xEA) : wxColour(0xDD, 0xDF, 0xE3);
dc.SetPen(wxPen(track_border, 1));
dc.SetBrush(wxBrush(track_fill));
dc.DrawRoundedRectangle(bounds, bounds.height / 2.0);
const wxRect thumb = thumb_rect_for(m_selection);
const int fill_right = std::min(bounds.GetRight(), thumb.GetX() + thumb.GetWidth() / 2 + FromDIP(2));
wxRect active(bounds.x, bounds.y, fill_right - bounds.x + 1, bounds.height);
dc.SetPen(*wxTRANSPARENT_PEN);
dc.SetBrush(wxBrush(active_fill));
dc.DrawRoundedRectangle(active, bounds.height / 2.0);
dc.SetBrush(wxBrush(GetBackgroundColour()));
dc.DrawRectangle(bounds);
const int dot_radius = std::max(FromDIP(1), thumb.height / 7);
for (int idx = 0; idx < 3; ++idx) {
if (idx == m_selection)
continue;
int states = state_handler.states();
double v_center = bounds.height / 2.0;
const wxRect slot = thumb_rect_for(idx);
const wxPoint center(slot.GetX() + slot.GetWidth() / 2, slot.GetY() + slot.GetHeight() / 2);
dc.SetBrush(wxBrush(idx < m_selection ? active_dot : inactive_dot));
dc.DrawCircle(center, dot_radius);
// Background
dc.SetPen(wxPen(border_color.colorForStates(states), 1));
dc.SetBrush(wxBrush(background_color.colorForStates(states)));
dc.DrawRoundedRectangle(bounds, v_center);
if (m_enabled) {
double dot_dist = (bounds.width - bounds.height) * 0.50;
// Track
dc.SetPen(wxPen(track_border.colorForStates(states), 1));
dc.SetBrush(wxBrush(track_background.colorForStates(states)));
wxRect track_rc = bounds;
track_rc.width = int(v_center * 2.0 + dot_dist * m_selection);
dc.DrawRoundedRectangle(track_rc, v_center);
// Dots
dc.SetPen(*wxTRANSPARENT_PEN);
for (int idx = 0; idx < 3; ++idx) {
dc.SetBrush(wxBrush((idx <= m_selection ? dot_active : dot_dimmed).colorForStates(states)));
dc.DrawCircle(wxPoint(v_center + dot_dist * idx, v_center), track_rc.height * (double)(idx == m_selection ? 0.32 : 0.16));
}
}
else { // Developer mode
wxString str = "DEV";
int kerning = 3; // pixels between chars
dc.SetTextForeground(text_color.colorForStates(states));
dc.SetPen(wxPen(thumb_border, 1));
dc.SetBrush(wxBrush(thumb_fill));
dc.DrawRoundedRectangle(thumb, thumb.height / 2.0);
wxCoord totalWidth = 0;
for (char c : str)
totalWidth += dc.GetTextExtent(wxString(c)).x + kerning;
totalWidth -= kerning;
wxCoord x = bounds.x + (bounds.width - totalWidth) / 2;
wxCoord y = bounds.y + (bounds.height - dc.GetTextExtent(str).y) / 2 - 1;
for (char c : str) {
wxString ch(c);
dc.DrawText(ch, x, y);
x += dc.GetTextExtent(ch).x + kerning;
}
}
}
void ModeSwitchButton::mouseDown(wxMouseEvent& event)
+6
View File
@@ -78,7 +78,13 @@ private:
private:
int m_selection { 0 };
bool m_pressed { false };
bool m_enabled { true };
wxString m_tooltips[3];
StateColor dot_active;
StateColor dot_dimmed;
StateColor text_color;
StateColor track_background;
StateColor track_border;
};
class MultiSwitchButton : public StaticBox
+4 -1
View File
@@ -50,7 +50,10 @@ std::map<std::string, std::string> BBLCloudServiceAgent::get_extra_header()
{
std::map<std::string, std::string> extra_headers;
extra_headers.emplace("X-BBL-Client-Type", "slicer");
extra_headers.emplace("X-BBL-Client-Name", SLIC3R_APP_NAME);
// Unable to get camera live view when the printer is connected to cloud for H2D
extra_headers.emplace("X-BBL-Client-Name", "BambuStudio");
extra_headers.emplace("X-BBL-Client-Version", GUI::wxGetApp().get_bbl_client_version());
#if defined(__WINDOWS__)
#ifdef _M_X64
+1 -1
View File
@@ -20,7 +20,7 @@ public:
int extruder_id = 0;
int ams_id = 0;
int slot_id = 0;
float nozzle_diameter;
float nozzle_diameter = 0.0f;
ExtruderType extruder_type{ExtruderType::etDirectDrive};
NozzleVolumeType nozzle_volume_type;
Calib_Params params;
+490 -10
View File
@@ -1,8 +1,13 @@
#include "Flashforge.hpp"
#include <algorithm>
#include <array>
#include <ctime>
#include <chrono>
#include <thread>
#include <sstream>
#include <fstream>
#include <set>
#include <map>
#include <boost/filesystem/path.hpp>
#include <boost/format.hpp>
#include <boost/log/trivial.hpp>
@@ -19,6 +24,10 @@
#include <wx/textctrl.h>
#include <wx/checkbox.h>
#include <boost/beast/core/detail/base64.hpp>
#include <curl/curl.h>
#include <nlohmann/json.hpp>
#include "libslic3r/PrintConfig.hpp"
#include "slic3r/GUI/GUI.hpp"
#include "slic3r/GUI/I18N.hpp"
@@ -30,20 +39,298 @@
namespace fs = boost::filesystem;
namespace pt = boost::property_tree;
using json = nlohmann::json;
namespace Slic3r {
namespace {
constexpr unsigned short FLASHFORGE_DISCOVERY_PORT = 48899;
constexpr unsigned short FLASHFORGE_DISCOVERY_LISTEN_PORT = 18007;
const std::array<unsigned char, 20> FLASHFORGE_DISCOVERY_MESSAGE = {
0x77, 0x77, 0x77, 0x2e, 0x75, 0x73, 0x72, 0x22,
0x65, 0x36, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00
};
std::string trim_null_terminated_ascii(const char* data, size_t len)
{
std::string out(data, data + len);
const auto pos = out.find('\0');
if (pos != std::string::npos)
out.resize(pos);
boost::trim(out);
return out;
}
bool parse_discovery_response(const std::vector<unsigned char>& response, const std::string& ip_address, FlashforgeDiscoveredPrinter& printer)
{
if (response.size() < 0xC4)
return false;
printer.name = trim_null_terminated_ascii(reinterpret_cast<const char*>(response.data()), 32);
printer.serial_number = trim_null_terminated_ascii(reinterpret_cast<const char*>(response.data() + 0x92), 32);
printer.ip_address = ip_address;
return !(printer.name.empty() && printer.serial_number.empty());
}
std::vector<std::string> get_discovery_broadcast_addresses()
{
std::set<std::string> addresses = {"255.255.255.255", "192.168.0.255", "192.168.1.255"};
try {
boost::asio::io_context io_context;
boost::asio::ip::tcp::resolver resolver(io_context);
boost::system::error_code ec;
const auto host_name = boost::asio::ip::host_name(ec);
if (!ec) {
const auto results = resolver.resolve(boost::asio::ip::tcp::v4(), host_name, "", ec);
if (!ec) {
for (const auto& entry : results) {
const auto addr = entry.endpoint().address();
if (!addr.is_v4())
continue;
const auto bytes = addr.to_v4().to_bytes();
if (bytes[0] == 127)
continue;
addresses.insert((boost::format("%1%.%2%.%3%.255") % static_cast<int>(bytes[0]) % static_cast<int>(bytes[1]) % static_cast<int>(bytes[2])).str());
}
}
}
} catch (...) {
}
return {addresses.begin(), addresses.end()};
}
std::string safe_config_string(DynamicPrintConfig* config, const char* key)
{
if (config == nullptr)
return {};
if (const auto* opt = config->option<ConfigOptionString>(key); opt != nullptr)
return opt->value;
return {};
}
bool try_parse_json_int(const json& value, int& out)
{
try {
if (value.is_number_integer() || value.is_number_unsigned()) {
out = value.get<int>();
return true;
}
if (value.is_boolean()) {
out = value.get<bool>() ? 1 : 0;
return true;
}
if (value.is_string()) {
std::string text = value.get<std::string>();
boost::trim(text);
if (text.empty())
return false;
size_t pos = 0;
const long parsed = std::stol(text, &pos, 10);
if (pos == text.size()) {
out = static_cast<int>(parsed);
return true;
}
}
} catch (...) {
}
return false;
}
bool validate_local_api_response(const std::string& response_body, wxString& error_msg)
{
const auto parsed = json::parse(response_body, nullptr, false, true);
if (parsed.is_discarded() || !parsed.is_object()) {
error_msg = _(L("Flashforge returned an invalid JSON response."));
return false;
}
int result_code = 0;
bool has_code = false;
if (parsed.contains("code"))
has_code = try_parse_json_int(parsed["code"], result_code);
if (!has_code && parsed.contains("err"))
has_code = try_parse_json_int(parsed["err"], result_code);
if (has_code && result_code != 0) {
std::string message;
if (parsed.contains("message") && parsed["message"].is_string())
message = parsed["message"].get<std::string>();
else if (parsed.contains("msg") && parsed["msg"].is_string())
message = parsed["msg"].get<std::string>();
if (message.empty())
message = "Request failed";
error_msg = GUI::from_u8((boost::format("Flashforge local API error %1%: %2%") % result_code % message).str());
return false;
}
return true;
}
std::string sanitize_flashforge_filename(const std::string& filename, const std::string& fallback_extension = {})
{
std::string basename = fs::path(filename).filename().string();
if (basename.empty()) {
basename = "print";
if (!fallback_extension.empty())
basename += fallback_extension;
}
for (char& ch : basename) {
const bool is_ascii_alnum = (ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z');
if (!is_ascii_alnum && ch != '.' && ch != '_' && ch != '-') {
ch = '_';
}
}
return basename;
}
} // namespace
Flashforge::Flashforge(DynamicPrintConfig* config)
: m_host(config->opt_string("print_host"))
: m_host()
, m_serial_number()
, m_check_code()
, m_console_port("8899")
, m_gcFlavor(config->option<ConfigOptionEnum<GCodeFlavor>>("gcode_flavor")->value)
, m_gcFlavor(gcfMarlinLegacy)
, m_bufferSize(4096) // 4K buffer size
{}
{
m_host = safe_config_string(config, "print_host");
m_serial_number = safe_config_string(config, "flashforge_serial_number");
m_check_code = safe_config_string(config, "printhost_apikey");
if (config != nullptr) {
if (const auto* gcode_flavor = config->option<ConfigOptionEnum<GCodeFlavor>>("gcode_flavor"); gcode_flavor != nullptr)
m_gcFlavor = gcode_flavor->value;
}
}
const char* Flashforge::get_name() const { return "Flashforge"; }
bool Flashforge::discover_printers(std::vector<FlashforgeDiscoveredPrinter>& printers, wxString& msg, int timeout_ms, int idle_timeout_ms, int max_retries)
{
printers.clear();
try {
const auto broadcast_addresses = get_discovery_broadcast_addresses();
std::map<std::string, FlashforgeDiscoveredPrinter> by_ip;
for (int attempt = 0; attempt < std::max(1, max_retries); ++attempt) {
boost::asio::io_context io_context;
boost::asio::ip::udp::socket socket(io_context);
boost::system::error_code ec;
socket.open(boost::asio::ip::udp::v4(), ec);
if (ec) {
msg = wxString::FromUTF8(ec.message().c_str());
return false;
}
socket.set_option(boost::asio::socket_base::broadcast(true), ec);
if (ec) {
msg = wxString::FromUTF8(ec.message().c_str());
return false;
}
socket.set_option(boost::asio::socket_base::reuse_address(true), ec);
if (ec) {
msg = wxString::FromUTF8(ec.message().c_str());
return false;
}
socket.bind({boost::asio::ip::udp::v4(), FLASHFORGE_DISCOVERY_LISTEN_PORT}, ec);
if (ec) {
msg = wxString::FromUTF8(ec.message().c_str());
return false;
}
for (const auto& addr : broadcast_addresses) {
socket.send_to(boost::asio::buffer(FLASHFORGE_DISCOVERY_MESSAGE),
{boost::asio::ip::make_address_v4(addr, ec), FLASHFORGE_DISCOVERY_PORT}, 0, ec);
ec.clear();
}
socket.non_blocking(true, ec);
if (ec) {
msg = wxString::FromUTF8(ec.message().c_str());
return false;
}
const auto start = std::chrono::steady_clock::now();
auto last_reply = start;
while (true) {
const auto now = std::chrono::steady_clock::now();
if (std::chrono::duration_cast<std::chrono::milliseconds>(now - start).count() >= timeout_ms)
break;
if (!by_ip.empty() && std::chrono::duration_cast<std::chrono::milliseconds>(now - last_reply).count() >= idle_timeout_ms)
break;
std::vector<unsigned char> buffer(512);
boost::asio::ip::udp::endpoint remote_endpoint;
const auto received = socket.receive_from(boost::asio::buffer(buffer), remote_endpoint, 0, ec);
if (!ec) {
buffer.resize(received);
FlashforgeDiscoveredPrinter printer;
if (parse_discovery_response(buffer, remote_endpoint.address().to_string(), printer)) {
by_ip[printer.ip_address] = std::move(printer);
last_reply = std::chrono::steady_clock::now();
}
} else if (ec == boost::asio::error::would_block || ec == boost::asio::error::try_again) {
ec.clear();
std::this_thread::sleep_for(std::chrono::milliseconds(50));
} else {
msg = wxString::FromUTF8(ec.message().c_str());
return false;
}
}
if (!by_ip.empty())
break;
}
for (auto& [_, printer] : by_ip)
printers.emplace_back(std::move(printer));
std::sort(printers.begin(), printers.end(), [](const FlashforgeDiscoveredPrinter& lhs, const FlashforgeDiscoveredPrinter& rhs) {
if (lhs.name != rhs.name)
return lhs.name < rhs.name;
return lhs.ip_address < rhs.ip_address;
});
if (printers.empty()) {
msg = _(L("No Flashforge printers were discovered on the local network."));
return false;
}
return true;
} catch (const std::exception& ex) {
msg = wxString::FromUTF8(ex.what());
return false;
}
}
bool Flashforge::test(wxString& msg) const
{
if (!m_serial_number.empty() && !m_check_code.empty())
return test_local_api(msg);
BOOST_LOG_TRIVIAL(debug) << boost::format("[Flashforge Serial] testing connection");
// Utils::TCPConsole console(m_host, m_console_port);
Utils::TCPConsole client(m_host, m_console_port);
@@ -58,11 +345,19 @@ bool Flashforge::test(wxString& msg) const
return res;
}
wxString Flashforge::get_test_ok_msg() const { return _(L("Serial connection to Flashforge is working correctly.")); }
wxString Flashforge::get_test_ok_msg() const
{
if (!m_serial_number.empty() && !m_check_code.empty())
return _(L("Connected to Flashforge local API successfully."));
return _(L("Serial connection to Flashforge is working correctly."));
}
wxString Flashforge::get_test_failed_msg(wxString& msg) const
{
return GUI::from_u8((boost::format("%s: %s") % _utf8(L("Could not connect to Flashforge via serial")) % std::string(msg.ToUTF8())).str());
const std::string prefix = (!m_serial_number.empty() && !m_check_code.empty()) ?
_utf8(L("Could not connect to Flashforge local API")) :
_utf8(L("Could not connect to Flashforge via serial"));
return GUI::from_u8((boost::format("%s: %s") % prefix % std::string(msg.ToUTF8())).str());
}
@@ -98,21 +393,25 @@ bool Flashforge::connect(wxString& msg) const
bool Flashforge::start_print(wxString& msg, const std::string& filename) const
{
Utils::TCPConsole client(m_host, m_console_port);
Slic3r::Utils::SerialMessage startPrintCommand = {(boost::format("~M23 0:/user/%1%") % filename).str(), Slic3r::Utils::Command};
const std::string safe_filename = sanitize_flashforge_filename(filename);
Slic3r::Utils::SerialMessage startPrintCommand = {(boost::format("~M23 0:/user/%1%") % safe_filename).str(), Slic3r::Utils::Command};
client.enqueue_cmd(startPrintCommand);
bool res = client.run_queue();
if (!res) {
msg = wxString::FromUTF8(client.error_message().c_str());
BOOST_LOG_TRIVIAL(info) << boost::format("[Flashforge Serial] Failed to start print %1%") % filename;
BOOST_LOG_TRIVIAL(info) << boost::format("[Flashforge Serial] Failed to start print %1%") % safe_filename;
} else
BOOST_LOG_TRIVIAL(info) << boost::format("[Flashforge Serial] Started print %1%") % filename;
BOOST_LOG_TRIVIAL(info) << boost::format("[Flashforge Serial] Started print %1%") % safe_filename;
return res;
}
bool Flashforge::upload(PrintHostUpload upload_data, ProgressFn progress_fn, ErrorFn error_fn, InfoFn info_fn) const
{
if (!m_serial_number.empty() && !m_check_code.empty())
return upload_local_api(std::move(upload_data), std::move(progress_fn), std::move(error_fn));
bool res = true;
wxString errormsg;
@@ -121,6 +420,8 @@ bool Flashforge::upload(PrintHostUpload upload_data, ProgressFn progress_fn, Err
try {
res = connect(errormsg);
const std::string fallback_extension = upload_data.source_path.extension().string().empty() ? ".gcode" : upload_data.source_path.extension().string();
const std::string upload_filename = sanitize_flashforge_filename(upload_data.upload_path.string(), fallback_extension);
std::ifstream newfile;
newfile.open(upload_data.source_path.c_str(), std::ios::binary); // open a file to perform read operation using file object
@@ -142,7 +443,7 @@ bool Flashforge::upload(PrintHostUpload upload_data, ProgressFn progress_fn, Err
newfile.close(); // close the file object.
}
Slic3r::Utils::SerialMessage fileuploadCommand =
{(boost::format("~M28 %1% 0:/user/%2%") % gcodeFile.size() % upload_data.upload_path.generic_string()).str(),
{(boost::format("~M28 %1% 0:/user/%2%") % gcodeFile.size() % upload_filename).str(),
Slic3r::Utils::Command};
client.enqueue_cmd(fileuploadCommand);
@@ -178,7 +479,7 @@ bool Flashforge::upload(PrintHostUpload upload_data, ProgressFn progress_fn, Err
res = client.run_queue();
if (upload_data.post_action == PrintHostPostUploadAction::StartPrint)
res = start_print(errormsg, upload_data.upload_path.string());
res = start_print(errormsg, upload_filename);
}
} catch (const std::exception& e) {
@@ -190,6 +491,185 @@ bool Flashforge::upload(PrintHostUpload upload_data, ProgressFn progress_fn, Err
return res;
}
bool Flashforge::test_local_api(wxString& msg) const
{
std::string body;
return request_local_api_json("detail", json{{"serialNumber", m_serial_number}, {"checkCode", m_check_code}}.dump(), body, msg);
}
bool Flashforge::fetch_material_slots(std::vector<FlashforgeMaterialSlot>& slots, bool* supports_material_station, wxString& msg) const
{
slots.clear();
if (m_serial_number.empty() || m_check_code.empty()) {
msg = _(L("Flashforge local API requires both serial number and access code."));
return false;
}
std::string body;
if (!request_local_api_json("detail", json{{"serialNumber", m_serial_number}, {"checkCode", m_check_code}}.dump(), body, msg))
return false;
const auto parsed = json::parse(body, nullptr, false, true);
if (parsed.is_discarded()) {
msg = _(L("Flashforge returned an invalid JSON response."));
return false;
}
const auto& detail = parsed.contains("detail") ? parsed["detail"] : parsed;
const auto& station = detail.contains("matlStationInfo") ? detail["matlStationInfo"] :
detail.contains("MatlStationInfo") ? detail["MatlStationInfo"] : json();
const auto& slot_infos = station.contains("slotInfos") ? station["slotInfos"] :
station.contains("SlotInfos") ? station["SlotInfos"] : json::array();
bool reports_material_station = false;
int has_material_station_flag = 0;
if (detail.contains("hasMatlStation") && try_parse_json_int(detail["hasMatlStation"], has_material_station_flag))
reports_material_station = has_material_station_flag != 0;
else if (detail.contains("HasMatlStation") && try_parse_json_int(detail["HasMatlStation"], has_material_station_flag))
reports_material_station = has_material_station_flag != 0;
int slot_count = 0;
if (station.contains("slotCnt") && try_parse_json_int(station["slotCnt"], slot_count))
reports_material_station = reports_material_station || slot_count > 0;
else if (station.contains("SlotCnt") && try_parse_json_int(station["SlotCnt"], slot_count))
reports_material_station = reports_material_station || slot_count > 0;
if (slot_infos.is_array() && !slot_infos.empty())
reports_material_station = true;
if (supports_material_station != nullptr)
*supports_material_station = reports_material_station;
for (const auto& slot : slot_infos) {
FlashforgeMaterialSlot info;
info.slot_id = slot.value("slotId", static_cast<int>(slots.size()) + 1);
info.has_filament = slot.value("hasFilament", false);
info.material_name = slot.value("materialName", std::string());
info.material_color = slot.value("materialColor", std::string());
slots.emplace_back(std::move(info));
}
return true;
}
bool Flashforge::upload_local_api(PrintHostUpload upload_data, ProgressFn progress_fn, ErrorFn error_fn) const
{
bool res = true;
std::string material_map_b64;
std::string material_map_json = "[]";
auto leveling_before_print = upload_data.extended_info["levelingBeforePrint"] == "1";
auto time_lapse_video = upload_data.extended_info["timeLapseVideo"] == "1";
auto use_material_station = upload_data.extended_info["useMatlStation"] == "1";
if (auto it = upload_data.extended_info.find("materialMappings"); it != upload_data.extended_info.end())
material_map_json = it->second;
material_map_b64.resize(boost::beast::detail::base64::encoded_size(material_map_json.size()));
material_map_b64.resize(boost::beast::detail::base64::encode(material_map_b64.data(), material_map_json.data(), material_map_json.size()));
auto url = make_http_url("uploadGcode");
const std::string fallback_extension = upload_data.source_path.extension().string().empty() ? (upload_data.use_3mf ? ".3mf" : ".gcode") : upload_data.source_path.extension().string();
auto filename = sanitize_flashforge_filename(upload_data.upload_path.string(), fallback_extension);
std::string file_size;
try {
file_size = std::to_string(fs::file_size(upload_data.source_path));
} catch (...) {
file_size = "0";
}
auto http = Http::post(url);
http.header("serialNumber", m_serial_number)
.header("checkCode", m_check_code)
.header("fileSize", file_size)
.header("printNow", upload_data.post_action == PrintHostPostUploadAction::StartPrint ? "true" : "false")
.header("levelingBeforePrint", leveling_before_print ? "true" : "false")
.header("flowCalibration", "false")
.header("firstLayerInspection", "false")
.header("timeLapseVideo", time_lapse_video ? "true" : "false")
.header("useMatlStation", use_material_station ? "true" : "false")
.header("gcodeToolCnt", upload_data.extended_info["gcodeToolCnt"])
.header("materialMappings", material_map_b64)
.form_add_file("gcodeFile", upload_data.source_path.string(), filename)
.on_complete([&](std::string body, unsigned status) {
wxString msg;
if (!validate_local_api_response(body, msg)) {
BOOST_LOG_TRIVIAL(error) << boost::format("[Flashforge HTTP] upload rejected by printer: HTTP %1% body: `%2%`") % status % body;
error_fn(msg);
res = false;
} else {
BOOST_LOG_TRIVIAL(info) << boost::format("[Flashforge HTTP] upload complete: HTTP %1% body: %2%") % status % body;
}
})
.on_error([&](std::string body, std::string error, unsigned status) {
BOOST_LOG_TRIVIAL(error) << boost::format("[Flashforge HTTP] upload failed: %1%, HTTP %2%, body: `%3%`") % error % status % body;
error_fn(format_error(body, error, status));
res = false;
})
.on_progress([&](Http::Progress progress, bool& cancel) {
progress_fn(std::move(progress), cancel);
if (cancel)
res = false;
})
.perform_sync();
return res;
}
bool Flashforge::request_local_api_json(const std::string& path, const std::string& body, std::string& response_body, wxString& error_msg) const
{
bool ok = true;
auto http = Http::post(make_http_url(path));
http.header("Content-Type", "application/json")
.set_post_body(body)
.on_complete([&](std::string body_text, unsigned) {
response_body = std::move(body_text);
if (!validate_local_api_response(response_body, error_msg))
ok = false;
})
.on_error([&](std::string body_text, std::string error, unsigned status) {
response_body = std::move(body_text);
error_msg = format_error(response_body, error, status);
ok = false;
})
.perform_sync();
return ok;
}
std::string Flashforge::make_http_url(const std::string& path) const
{
return (boost::format("http://%1%:8898/%2%") % extract_host_name() % path).str();
}
std::string Flashforge::extract_host_name() const
{
std::string host = m_host;
if (host.find("://") == std::string::npos) {
const auto slash_pos = host.find('/');
if (slash_pos != std::string::npos)
host = host.substr(0, slash_pos);
return host;
}
std::string out = host;
CURLU* hurl = curl_url();
if (!hurl)
return host;
const auto rc = curl_url_set(hurl, CURLUPART_URL, host.c_str(), 0);
if (rc == CURLUE_OK) {
char* raw_host = nullptr;
if (curl_url_get(hurl, CURLUPART_HOST, &raw_host, 0) == CURLUE_OK && raw_host != nullptr) {
out = raw_host;
curl_free(raw_host);
}
}
curl_url_cleanup(hurl);
return out;
}
int Flashforge::get_err_code_from_body(const std::string& body) const
{
pt::ptree root;
+26 -1
View File
@@ -1,6 +1,7 @@
#ifndef slic3r_FlashForge_hpp_
#define slic3r_FlashForge_hpp_
#include <vector>
#include <string>
#include <wx/string.h>
#include "PrintHost.hpp"
@@ -12,6 +13,21 @@ namespace Slic3r {
class DynamicPrintConfig;
class Http;
struct FlashforgeMaterialSlot
{
int slot_id {0}; // API is 1-based.
bool has_filament {false};
std::string material_name;
std::string material_color;
};
struct FlashforgeDiscoveredPrinter
{
std::string name;
std::string serial_number;
std::string ip_address;
};
class Flashforge : public PrintHost
{
public:
@@ -24,13 +40,17 @@ public:
wxString get_test_ok_msg() const override;
wxString get_test_failed_msg(wxString &msg) const override;
bool upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn, InfoFn info_fn) const override;
bool has_auto_discovery() const override { return false; }
bool has_auto_discovery() const override { return true; }
bool can_test() const override { return true; }
PrintHostPostUploadActions get_post_upload_actions() const override { return PrintHostPostUploadAction::StartPrint; }
std::string get_host() const override { return m_host; }
bool fetch_material_slots(std::vector<FlashforgeMaterialSlot>& slots, bool* supports_material_station, wxString& msg) const;
static bool discover_printers(std::vector<FlashforgeDiscoveredPrinter>& printers, wxString& msg, int timeout_ms = 10000, int idle_timeout_ms = 1500, int max_retries = 3);
private:
std::string m_host;
std::string m_serial_number;
std::string m_check_code;
std::string m_console_port;
const int m_bufferSize;
GCodeFlavor m_gcFlavor;
@@ -43,6 +63,11 @@ private:
Slic3r::Utils::SerialMessage tempStatusCommand = {"~M105\r\n", Slic3r::Utils::Command};
Slic3r::Utils::SerialMessage printStatusCommand = {"~M27\r\n", Slic3r::Utils::Command};
Slic3r::Utils::SerialMessage saveFileCommand = {"~M29\r\n",Slic3r::Utils::Command};
bool upload_local_api(PrintHostUpload upload_data, ProgressFn progress_fn, ErrorFn error_fn) const;
bool test_local_api(wxString& msg) const;
bool request_local_api_json(const std::string& path, const std::string& body, std::string& response_body, wxString& error_msg) const;
std::string make_http_url(const std::string& path) const;
std::string extract_host_name() const;
int get_err_code_from_body(const std::string &body) const;
bool connect(wxString& msg) const;
bool start_print(wxString& msg, const std::string& filename) const;