mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-19 13:41:03 +00:00
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:
commit
8dfd480c52
1008 files changed
+31791
-25415
No files matched your search
@@ -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 */
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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++) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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: {
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]() {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -153,7 +153,7 @@ private:
|
||||
void UpdateDialogUI();
|
||||
void UpdateApplyButtonStatus();
|
||||
bool IsAllChecked();
|
||||
bool IsAllCancled();
|
||||
bool IsAllCanceled();
|
||||
|
||||
void OnRetryButton(wxCommandEvent &event);
|
||||
void OnAllCheckbox(wxCommandEvent &event);
|
||||
|
||||
@@ -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(""));
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)));
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user