diff --git a/resources/images/camera_switch.svg b/resources/images/camera_switch.svg new file mode 100644 index 0000000000..be8cf27a09 --- /dev/null +++ b/resources/images/camera_switch.svg @@ -0,0 +1,76 @@ + + + + + + + + + + + + + diff --git a/resources/images/camera_switch_dark.svg b/resources/images/camera_switch_dark.svg new file mode 100644 index 0000000000..5c40b35d14 --- /dev/null +++ b/resources/images/camera_switch_dark.svg @@ -0,0 +1,76 @@ + + + + + + + + + + + + + diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 99c849a873..bd872464c6 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -2478,9 +2478,11 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato CalibPressureAdvanceLine pa_test(this); - auto fast_speed = CalibPressureAdvance::find_optimal_PA_speed(print.full_print_config(), pa_test.line_width(), 0.2); - auto slow_speed = std::max(20.0, fast_speed / 10.0); - + auto fast_speed = CalibPressureAdvance::find_optimal_PA_speed(print.full_print_config(), pa_test.line_width(), pa_test.height_layer()); + auto slow_speed = std::max(10.0, fast_speed / 10.0); + if (fast_speed < slow_speed + 5) + fast_speed = slow_speed + 5; + pa_test.set_speed(fast_speed, slow_speed); pa_test.draw_numbers() = print.calib_params().print_numbers; gcode += pa_test.generate_test(params.start, params.step, std::llround(std::ceil((params.end - params.start) / params.step)) + 1); diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index e638a96570..84ef8d7522 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -786,7 +786,7 @@ static std::vector s_Preset_print_options { "independent_support_layer_height", "support_angle", "support_interface_top_layers", "support_interface_bottom_layers", "support_interface_pattern", "support_interface_spacing", "support_interface_loop_pattern", - "support_top_z_distance", "support_on_build_plate_only","support_critical_regions_only", "bridge_no_support", "thick_bridges", "thick_internal_bridges", "max_bridge_length", "print_sequence", "support_remove_small_overhang", + "support_top_z_distance", "support_on_build_plate_only","support_critical_regions_only", "bridge_no_support", "thick_bridges", "thick_internal_bridges","dont_filter_internal_bridges", "max_bridge_length", "print_sequence", "support_remove_small_overhang", "filename_format", "wall_filament", "support_bottom_z_distance", "sparse_infill_filament", "solid_infill_filament", "support_filament", "support_interface_filament","support_interface_not_for_body", "ooze_prevention", "standby_temperature_delta", "interface_shells", "line_width", "initial_layer_line_width", diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 824af48092..d963dc811b 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -248,6 +248,13 @@ static t_config_enum_values s_keys_map_SeamPosition { }; CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(SeamPosition) +static t_config_enum_values s_keys_map_InternalBridgeFilter { + { "disabled", ibfDisabled }, + { "limited", ibfLimited }, + { "nofilter", ibfNofilter }, +}; +CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(InternalBridgeFilter) + static const t_config_enum_values s_keys_map_SLADisplayOrientation = { { "landscape", sladoLandscape}, { "portrait", sladoPortrait} @@ -1220,6 +1227,32 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionBool(true)); + def = this->add("dont_filter_internal_bridges", coEnum); + def->label = L("Don't filter out small internal bridges (experimental)"); + def->category = L("Quality"); + def->tooltip = L("This option can help reducing pillowing on top surfaces in heavily slanted or curved models.\n\n" + "By default, small internal bridges are filtered out and the internal solid infill is printed directly" + " over the sparse infill. This works well in most cases, speeding up printing without too much compromise" + " on top surface quality. \n\nHowever, in heavily slanted or curved models especially where too low sparse" + " infill density is used, this may result in curling of the unsupported solid infill, causing pillowing.\n\n" + "Enabling this option will print internal bridge layer over slightly unsupported internal" + " solid infill. The options below control the amount of filtering, i.e. the amount of internal bridges " + "created.\n\n" + "Disabled - Disables this option. This is the default behaviour and works well in most cases.\n\n" + "Limited filtering - Creates internal bridges on heavily slanted surfaces, while avoiding creating " + "uncessesary interal bridges. This works well for most difficult models.\n\n" + "No filtering - Creates internal bridges on every potential internal overhang. This option is useful " + "for heavily slanted top surface models. However, in most cases it creates too many unecessary bridges."); + def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); + def->enum_values.push_back("disabled"); + def->enum_values.push_back("limited"); + def->enum_values.push_back("nofilter"); + def->enum_labels.push_back(L("Disabled")); + def->enum_labels.push_back(L("Limited filtering")); + def->enum_labels.push_back(L("No filtering")); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionEnum(ibfDisabled)); + def = this->add("max_bridge_length", coFloat); def->label = L("Max bridge length"); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 85e062c832..f921342496 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -152,6 +152,10 @@ enum SeamPosition { spNearest, spAligned, spRear, spRandom }; +enum InternalBridgeFilter { + ibfDisabled, ibfLimited, ibfNofilter +}; + enum LiftType { NormalLift, SpiralLift, @@ -744,6 +748,7 @@ PRINT_CONFIG_CLASS_DEFINE( // Orca internal thick bridge ((ConfigOptionBool, thick_bridges)) ((ConfigOptionBool, thick_internal_bridges)) + ((ConfigOptionEnum, dont_filter_internal_bridges)) // Overhang angle threshold. ((ConfigOptionInt, support_threshold_angle)) ((ConfigOptionFloat, support_object_xy_distance)) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 9af5df80b6..da0aff7963 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -1969,7 +1969,6 @@ template void debug_draw(std::string name, const T& a, const T& b, c void PrintObject::bridge_over_infill() { BOOST_LOG_TRIVIAL(info) << "Bridge over infill - Start" << log_memory_info(); - struct CandidateSurface { CandidateSurface(const Surface *original_surface, @@ -1991,12 +1990,20 @@ void PrintObject::bridge_over_infill() }; std::map> surfaces_by_layer; + // Orca: + // Detect use of lightning infill. Moved earlier in the function to pass to the gather and filter surfaces threads. + bool has_lightning_infill = false; + for (size_t i = 0; i < this->num_printing_regions(); i++) { + if (this->printing_region(i).config().sparse_infill_pattern == ipLightning) { + has_lightning_infill = true; + break; + } + } // SECTION to gather and filter surfaces for expanding, and then cluster them by layer { tbb::concurrent_vector candidate_surfaces; - tbb::parallel_for(tbb::blocked_range(0, this->layers().size()), [po = static_cast(this), - &candidate_surfaces](tbb::blocked_range r) { + tbb::parallel_for(tbb::blocked_range(0, this->layers().size()), [po = static_cast(this), &candidate_surfaces, has_lightning_infill](tbb::blocked_range r) { PRINT_OBJECT_TIME_LIMIT_MILLIS(PRINT_OBJECT_TIME_LIMIT_DEFAULT); for (size_t lidx = r.begin(); lidx < r.end(); lidx++) { const Layer *layer = po->get_layer(lidx); @@ -2019,44 +2026,60 @@ void PrintObject::bridge_over_infill() } } unsupported_area = closing(unsupported_area, float(SCALED_EPSILON)); + + // Orca: + // Lightning infill benefits from always having a bridge layer so don't filter out small unsupported areas. Also, don't filter small internal unsupported areas if the user has requested so. + double expansion_multiplier = 3; + if(has_lightning_infill || po->config().dont_filter_internal_bridges.value !=ibfDisabled){ + expansion_multiplier = 1; + } // By expanding the lower layer solids, we avoid making bridges from the tiny internal overhangs that are (very likely) supported by previous layer solids // NOTE that we cannot filter out polygons worth bridging by their area, because sometimes there is a very small internal island that will grow into large hole lower_layer_solids = shrink(lower_layer_solids, 1 * spacing); // first remove thin regions that will not support anything - lower_layer_solids = expand(lower_layer_solids, (1 + 3) * spacing); // then expand back (opening), and further for parts supported by internal solids + lower_layer_solids = expand(lower_layer_solids, (1 + expansion_multiplier) * spacing); // then expand back (opening), and further for parts supported by internal solids // By shrinking the unsupported area, we avoid making bridges from narrow ensuring region along perimeters. - unsupported_area = shrink(unsupported_area, 3 * spacing); + unsupported_area = shrink(unsupported_area, expansion_multiplier * spacing); unsupported_area = diff(unsupported_area, lower_layer_solids); - + for (const LayerRegion *region : layer->regions()) { SurfacesPtr region_internal_solids = region->fill_surfaces.filter_by_type(stInternalSolid); for (const Surface *s : region_internal_solids) { Polygons unsupported = intersection(to_polygons(s->expolygon), unsupported_area); - // The following flag marks those surfaces, which overlap with unuspported area, but at least part of them is supported. - // These regions can be filtered by area, because they for sure are touching solids on lower layers, and it does not make sense to bridge their tiny overhangs - bool partially_supported = area(unsupported) < area(to_polygons(s->expolygon)) - EPSILON; - if (!unsupported.empty() && (!partially_supported || area(unsupported) > 3 * 3 * spacing * spacing)) { - Polygons worth_bridging = intersection(to_polygons(s->expolygon), expand(unsupported, 4 * spacing)); - // after we extracted the part worth briding, we go over the leftovers and merge the tiny ones back, to not brake the surface too much - for (const Polygon& p : diff(to_polygons(s->expolygon), expand(worth_bridging, spacing))) { - double area = p.area(); - if (area < spacing * scale_(12.0) && area > spacing * spacing) { - worth_bridging.push_back(p); + + // Orca: If the user has selected to always support internal overhanging regions, no matter how small + // skip the filtering + if (po->config().dont_filter_internal_bridges.value == ibfNofilter){ + // expand the unsupported area by 4x spacing to trigger internal bridging + unsupported = expand(unsupported, 4 * spacing); + candidate_surfaces.push_back(CandidateSurface(s, lidx, unsupported, region, 0)); + }else{ + // The following flag marks those surfaces, which overlap with unuspported area, but at least part of them is supported. + // These regions can be filtered by area, because they for sure are touching solids on lower layers, and it does not make sense to bridge their tiny overhangs + bool partially_supported = area(unsupported) < area(to_polygons(s->expolygon)) - EPSILON; + if (!unsupported.empty() && (!partially_supported || area(unsupported) > 3 * 3 * spacing * spacing)) { + Polygons worth_bridging = intersection(to_polygons(s->expolygon), expand(unsupported, 4 * spacing)); + // after we extracted the part worth briding, we go over the leftovers and merge the tiny ones back, to not brake the surface too much + for (const Polygon& p : diff(to_polygons(s->expolygon), expand(worth_bridging, spacing))) { + double area = p.area(); + if (area < spacing * scale_(12.0) && area > spacing * spacing) { + worth_bridging.push_back(p); + } } + worth_bridging = intersection(closing(worth_bridging, float(SCALED_EPSILON)), s->expolygon); + candidate_surfaces.push_back(CandidateSurface(s, lidx, worth_bridging, region, 0)); + +#ifdef DEBUG_BRIDGE_OVER_INFILL + debug_draw(std::to_string(lidx) + "_candidate_surface_" + std::to_string(area(s->expolygon)), + to_lines(region->layer()->lslices), to_lines(s->expolygon), to_lines(worth_bridging), + to_lines(unsupported_area)); +#endif +#ifdef DEBUG_BRIDGE_OVER_INFILL + debug_draw(std::to_string(lidx) + "_candidate_processing_" + std::to_string(area(unsupported)), + to_lines(unsupported), to_lines(intersection(to_polygons(s->expolygon), expand(unsupported, 5 * spacing))), + to_lines(diff(to_polygons(s->expolygon), expand(worth_bridging, spacing))), + to_lines(unsupported_area)); +#endif } - worth_bridging = intersection(closing(worth_bridging, float(SCALED_EPSILON)), s->expolygon); - candidate_surfaces.push_back(CandidateSurface(s, lidx, worth_bridging, region, 0)); - -#ifdef DEBUG_BRIDGE_OVER_INFILL - debug_draw(std::to_string(lidx) + "_candidate_surface_" + std::to_string(area(s->expolygon)), - to_lines(region->layer()->lslices), to_lines(s->expolygon), to_lines(worth_bridging), - to_lines(unsupported_area)); -#endif -#ifdef DEBUG_BRIDGE_OVER_INFILL - debug_draw(std::to_string(lidx) + "_candidate_processing_" + std::to_string(area(unsupported)), - to_lines(unsupported), to_lines(intersection(to_polygons(s->expolygon), expand(unsupported, 5 * spacing))), - to_lines(diff(to_polygons(s->expolygon), expand(worth_bridging, spacing))), - to_lines(unsupported_area)); -#endif } } } @@ -2071,13 +2094,6 @@ void PrintObject::bridge_over_infill() // LIGHTNING INFILL SECTION - If lightning infill is used somewhere, we check the areas that are going to be bridges, and those that rely on the // lightning infill under them get expanded. This somewhat helps to ensure that most of the extrusions are anchored to the lightning infill at the ends. // It requires modifying this instance of print object in a specific way, so that we do not invalidate the pointers in our surfaces_by_layer structure. - bool has_lightning_infill = false; - for (size_t i = 0; i < this->num_printing_regions(); i++) { - if (this->printing_region(i).config().sparse_infill_pattern == ipLightning) { - has_lightning_infill = true; - break; - } - } if (has_lightning_infill) { // Prepare backup data for the Layer Region infills. Before modfiyng the layer region, we backup its fill surfaces by moving! them into this map. // then a copy is created, modifiyed and passed to lightning infill generator. After generator is created, we restore the original state of the fills diff --git a/src/libslic3r/calib.cpp b/src/libslic3r/calib.cpp index 162d7b9b5a..224479c097 100644 --- a/src/libslic3r/calib.cpp +++ b/src/libslic3r/calib.cpp @@ -424,6 +424,13 @@ std::string CalibPressureAdvance::draw_box(GCodeWriter &writer, double min_x, do return gcode.str(); } +CalibPressureAdvanceLine::CalibPressureAdvanceLine(GCode* gcodegen) + : CalibPressureAdvance(gcodegen->config()), mp_gcodegen(gcodegen), m_nozzle_diameter(gcodegen->config().nozzle_diameter.get_at(0)) +{ + m_line_width = m_nozzle_diameter < 0.51 ? m_nozzle_diameter * 1.5 : m_nozzle_diameter * 1.05; + m_height_layer = gcodegen->config().initial_layer_print_height; + m_number_line_width = m_thin_line_width = m_nozzle_diameter; +}; std::string CalibPressureAdvanceLine::generate_test(double start_pa /*= 0*/, double step_pa /*= 0.002*/, int count /*= 10*/) { @@ -496,14 +503,15 @@ std::string CalibPressureAdvanceLine::print_pa_lines(double start_x, double star // gcode << move_to(Vec2d(start_x + m_length_short + m_length_long, y_pos + (num - 1) * m_space_y + 7), writer); // gcode << writer.extrude_to_xy(Vec2d(start_x + m_length_short + m_length_long, y_pos + (num - 1) * m_space_y + 2), thin_e_per_mm * 7); - DrawBoxOptArgs default_box_opt_args(2, m_height_layer, 0.6, fast); + const auto box_start_x = start_x + m_length_short + m_length_long + m_length_short; + DrawBoxOptArgs default_box_opt_args(2, m_height_layer, m_line_width, fast); default_box_opt_args.is_filled = true; - gcode << draw_box(writer, start_x + m_length_short + m_length_long + m_length_short, start_y - m_space_y, number_spacing() * 8, - (num + 1) * m_space_y, default_box_opt_args); + gcode << draw_box(writer, box_start_x, start_y - m_space_y, + number_spacing() * 8, (num + 1) * m_space_y, default_box_opt_args); gcode << writer.travel_to_z(m_height_layer*2); for (int i = 0; i < num; i += 2) { - gcode << draw_number(start_x + m_length_short + m_length_long + m_length_short + 3, y_pos + i * m_space_y + m_space_y / 2, - start_pa + i * step_pa, m_draw_digit_mode, m_number_line_width, number_e_per_mm, 3600, writer); + gcode << draw_number(box_start_x + 3 + m_line_width, y_pos + i * m_space_y + m_space_y / 2, start_pa + i * step_pa, m_draw_digit_mode, + m_number_line_width, number_e_per_mm, 3600, writer); } } return gcode.str(); diff --git a/src/libslic3r/calib.hpp b/src/libslic3r/calib.hpp index fdabd47cc1..ee74d98a63 100644 --- a/src/libslic3r/calib.hpp +++ b/src/libslic3r/calib.hpp @@ -187,7 +187,7 @@ protected: class CalibPressureAdvanceLine : public CalibPressureAdvance { public: - CalibPressureAdvanceLine(GCode *gcodegen) : CalibPressureAdvance(gcodegen->config()), mp_gcodegen(gcodegen),m_nozzle_diameter(gcodegen->config().nozzle_diameter.get_at(0)){}; + CalibPressureAdvanceLine(GCode* gcodegen); ~CalibPressureAdvanceLine(){}; std::string generate_test(double start_pa = 0, double step_pa = 0.002, int count = 50); @@ -199,6 +199,7 @@ public: } const double &line_width() { return m_line_width; }; + const double &height_layer() { return m_height_layer; }; bool is_delta() const; bool &draw_numbers() { return m_draw_numbers; } @@ -212,10 +213,10 @@ private: double m_nozzle_diameter; double m_slow_speed, m_fast_speed; - const double m_height_layer{0.2}; - const double m_line_width{0.6}; - const double m_thin_line_width{0.44}; - const double m_number_line_width{0.48}; + double m_height_layer{0.2}; + double m_line_width{0.6}; + double m_thin_line_width{0.44}; + double m_number_line_width{0.48}; const double m_space_y{3.5}; double m_length_short{20.0}, m_length_long{40.0}; diff --git a/src/slic3r/GUI/CameraPopup.cpp b/src/slic3r/GUI/CameraPopup.cpp index e7b5cf68dd..08e4801372 100644 --- a/src/slic3r/GUI/CameraPopup.cpp +++ b/src/slic3r/GUI/CameraPopup.cpp @@ -23,6 +23,7 @@ wxEND_EVENT_TABLE() wxDEFINE_EVENT(EVT_VCAMERA_SWITCH, wxMouseEvent); wxDEFINE_EVENT(EVT_SDCARD_ABSENT_HINT, wxCommandEvent); +wxDEFINE_EVENT(EVT_CAM_SOURCE_CHANGE, wxCommandEvent); #define CAMERAPOPUP_CLICK_INTERVAL 20 @@ -78,6 +79,34 @@ CameraPopup::CameraPopup(wxWindow *parent) top_sizer->Add(0, 0, wxALL, 0); } + // custom IP camera + m_custom_camera_input_confirm = new Button(m_panel, _L("Enable")); + m_custom_camera_input_confirm->SetBackgroundColor(wxColour(38, 166, 154)); + m_custom_camera_input_confirm->SetBorderColor(wxColour(38, 166, 154)); + m_custom_camera_input_confirm->SetTextColor(wxColour(0xFFFFFE)); + m_custom_camera_input_confirm->SetFont(Label::Body_14); + m_custom_camera_input_confirm->SetMinSize(wxSize(FromDIP(90), FromDIP(30))); + m_custom_camera_input_confirm->SetPosition(wxDefaultPosition); + m_custom_camera_input_confirm->SetCornerRadius(FromDIP(12)); + m_custom_camera_input = new TextInput(m_panel, wxEmptyString, wxEmptyString, wxEmptyString, wxDefaultPosition, wxDefaultSize); + m_custom_camera_input->GetTextCtrl()->SetHint(_L("Hostname or IP")); + m_custom_camera_input->GetTextCtrl()->SetFont(Label::Body_14); + m_custom_camera_hint = new wxStaticText(m_panel, wxID_ANY, _L("Custom camera source")); + m_custom_camera_hint->Wrap(-1); + m_custom_camera_hint->SetFont(Label::Head_14); + m_custom_camera_hint->SetForegroundColour(TEXT_COL); + + m_custom_camera_input_confirm->Bind(wxEVT_BUTTON, &CameraPopup::on_camera_source_changed, this); + + if (!wxGetApp().app_config->get("camera", "custom_source").empty()) { + m_custom_camera_input->GetTextCtrl()->SetValue(wxGetApp().app_config->get("camera", "custom_source")); + set_custom_cam_button_state(wxGetApp().app_config->get("camera", "enable_custom_source") == "true"); + } + + top_sizer->Add(m_custom_camera_hint, 0, wxALIGN_CENTER_VERTICAL | wxALIGN_LEFT | wxALL, FromDIP(5)); + top_sizer->Add(0, 0, wxALL, 0); + top_sizer->Add(m_custom_camera_input, 2, wxALIGN_CENTER_VERTICAL | wxALIGN_LEFT | wxEXPAND | wxALL, FromDIP(5)); + top_sizer->Add(m_custom_camera_input_confirm, 1, wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT | wxALL, FromDIP(5)); main_sizer->Add(top_sizer, 0, wxALL, FromDIP(10)); auto url = wxString::Format(L"https://wiki.bambulab.com/%s/software/bambu-studio/virtual-camera", L"en"); @@ -132,6 +161,37 @@ void CameraPopup::sdcard_absent_hint() GetEventHandler()->ProcessEvent(evt); } +void CameraPopup::on_camera_source_changed(wxCommandEvent &event) +{ + if (m_obj && !m_custom_camera_input->GetTextCtrl()->IsEmpty()) { + handle_camera_source_change(); + } +} + +void CameraPopup::handle_camera_source_change() +{ + m_custom_camera_enabled = !m_custom_camera_enabled; + + set_custom_cam_button_state(m_custom_camera_enabled); + + wxGetApp().app_config->set("camera", "custom_source", m_custom_camera_input->GetTextCtrl()->GetValue().ToStdString()); + wxGetApp().app_config->set("camera", "enable_custom_source", m_custom_camera_enabled); + + wxCommandEvent evt(EVT_CAM_SOURCE_CHANGE); + evt.SetEventObject(this); + GetEventHandler()->ProcessEvent(evt); +} + +void CameraPopup::set_custom_cam_button_state(bool state) +{ + m_custom_camera_enabled = state; + auto stateColour = state ? wxColour(170, 0, 0) : wxColour(38, 166, 154); + auto stateText = state ? "Disable" : "Enable"; + m_custom_camera_input_confirm->SetBackgroundColor(stateColour); + m_custom_camera_input_confirm->SetBorderColor(stateColour); + m_custom_camera_input_confirm->SetLabel(_L(stateText)); +} + void CameraPopup::on_switch_recording(wxCommandEvent& event) { if (!m_obj) return; diff --git a/src/slic3r/GUI/CameraPopup.hpp b/src/slic3r/GUI/CameraPopup.hpp index a9b53a621e..a256f317b0 100644 --- a/src/slic3r/GUI/CameraPopup.hpp +++ b/src/slic3r/GUI/CameraPopup.hpp @@ -14,12 +14,14 @@ #include "Widgets/SwitchButton.hpp" #include "Widgets/RadioBox.hpp" #include "Widgets/PopupWindow.hpp" +#include "Widgets/TextInput.hpp" namespace Slic3r { namespace GUI { wxDECLARE_EVENT(EVT_VCAMERA_SWITCH, wxMouseEvent); wxDECLARE_EVENT(EVT_SDCARD_ABSENT_HINT, wxCommandEvent); +wxDECLARE_EVENT(EVT_CAM_SOURCE_CHANGE, wxCommandEvent); class CameraPopup : public PopupWindow { @@ -50,6 +52,9 @@ protected: void on_switch_recording(wxCommandEvent& event); void on_set_resolution(); void sdcard_absent_hint(); + void on_camera_source_changed(wxCommandEvent& event); + void handle_camera_source_change(); + void set_custom_cam_button_state(bool state); wxWindow * create_item_radiobox(wxString title, wxWindow *parent, wxString tooltip, int padding_left); void select_curr_radiobox(int btn_idx); @@ -66,6 +71,10 @@ private: SwitchButton* m_switch_recording; wxStaticText* m_text_vcamera; SwitchButton* m_switch_vcamera; + wxStaticText* m_custom_camera_hint; + TextInput* m_custom_camera_input; + Button* m_custom_camera_input_confirm; + bool m_custom_camera_enabled{ false }; wxStaticText* m_text_resolution; wxWindow* m_resolution_options[RESOLUTION_OPTIONS_NUM]; wxScrolledWindow *m_panel; diff --git a/src/slic3r/GUI/StatusPanel.cpp b/src/slic3r/GUI/StatusPanel.cpp index e4fed1aac6..988f5e26cf 100644 --- a/src/slic3r/GUI/StatusPanel.cpp +++ b/src/slic3r/GUI/StatusPanel.cpp @@ -5,6 +5,7 @@ #include "Widgets/Button.hpp" #include "Widgets/StepCtrl.hpp" #include "Widgets/SideTools.hpp" +#include "Widgets/WebView.hpp" #include "BitmapCache.hpp" #include "GUI_App.hpp" @@ -889,6 +890,11 @@ StatusBasePanel::StatusBasePanel(wxWindow *parent, wxWindowID id, const wxPoint StatusBasePanel::~StatusBasePanel() { delete m_media_play_ctrl; + + if (m_custom_camera_view) { + delete m_custom_camera_view; + m_custom_camera_view = nullptr; + } } void StatusBasePanel::init_bitmaps() @@ -922,6 +928,7 @@ void StatusBasePanel::init_bitmaps() m_bitmap_timelapse_off = ScalableBitmap(this, wxGetApp().dark_mode() ? "monitor_timelapse_off_dark" : "monitor_timelapse_off", 20); m_bitmap_vcamera_on = ScalableBitmap(this, wxGetApp().dark_mode() ? "monitor_vcamera_on_dark" : "monitor_vcamera_on", 20); m_bitmap_vcamera_off = ScalableBitmap(this, wxGetApp().dark_mode() ? "monitor_vcamera_off_dark" : "monitor_vcamera_off", 20); + m_bitmap_switch_camera = ScalableBitmap(this, wxGetApp().dark_mode() ? "camera_switch_dark" : "camera_switch", 20); } @@ -989,12 +996,27 @@ wxBoxSizer *StatusBasePanel::create_monitoring_page() m_setting_button->SetMinSize(wxSize(FromDIP(38), FromDIP(24))); m_setting_button->SetBackgroundColour(STATUS_TITLE_BG); + m_camera_switch_button = new wxStaticBitmap(m_panel_monitoring_title, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize(FromDIP(38), FromDIP(24)), 0); + m_camera_switch_button->SetMinSize(wxSize(FromDIP(38), FromDIP(24))); + m_camera_switch_button->SetBackgroundColour(STATUS_TITLE_BG); + m_camera_switch_button->SetBitmap(m_bitmap_switch_camera.bmp()); + m_camera_switch_button->Bind(wxEVT_LEFT_DOWN, &StatusBasePanel::on_camera_switch_toggled, this); + m_camera_switch_button->Bind(wxEVT_RIGHT_DOWN, [this](auto& e) { + const std::string js_request_pip = R"( + document.querySelector('video').requestPictureInPicture(); + )"; + m_custom_camera_view->RunScript(js_request_pip); + }); + m_camera_switch_button->Hide(); + m_bitmap_sdcard_img->SetToolTip(_L("SD Card")); m_bitmap_timelapse_img->SetToolTip(_L("Timelapse")); m_bitmap_recording_img->SetToolTip(_L("Video")); m_bitmap_vcamera_img->SetToolTip(_L("Go Live")); m_setting_button->SetToolTip(_L("Camera Setting")); + m_camera_switch_button->SetToolTip(_L("Switch Camera View")); + bSizer_monitoring_title->Add(m_camera_switch_button, 0, wxALIGN_CENTER_VERTICAL | wxALL, FromDIP(5)); bSizer_monitoring_title->Add(m_bitmap_sdcard_img, 0, wxALIGN_CENTER_VERTICAL | wxALL, FromDIP(5)); bSizer_monitoring_title->Add(m_bitmap_timelapse_img, 0, wxALIGN_CENTER_VERTICAL | wxALL, FromDIP(5)); bSizer_monitoring_title->Add(m_bitmap_recording_img, 0, wxALIGN_CENTER_VERTICAL | wxALL, FromDIP(5)); @@ -1014,17 +1036,44 @@ wxBoxSizer *StatusBasePanel::create_monitoring_page() m_media_ctrl = new wxMediaCtrl2(this); m_media_ctrl->SetMinSize(wxSize(PAGE_MIN_WIDTH, FromDIP(288))); + m_custom_camera_view = WebView::CreateWebView(this, wxEmptyString); + m_custom_camera_view->EnableContextMenu(false); + Bind(wxEVT_WEBVIEW_NAVIGATING, &StatusBasePanel::on_webview_navigating, this, m_custom_camera_view->GetId()); + m_media_play_ctrl = new MediaPlayCtrl(this, m_media_ctrl, wxDefaultPosition, wxSize(-1, FromDIP(40))); + m_custom_camera_view->Hide(); + m_custom_camera_view->Bind(wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, [this](wxWebViewEvent& evt) { + if (evt.GetString() == "leavepictureinpicture") { + // When leaving PiP, video gets paused in some cases and toggling play + // programmatically does not work. + m_custom_camera_view->Reload(); + } + else if (evt.GetString() == "enterpictureinpicture") { + toggle_builtin_camera(); + } + }); sizer->Add(m_media_ctrl, 1, wxEXPAND | wxALL, 0); + sizer->Add(m_custom_camera_view, 1, wxEXPAND | wxALL, 0); sizer->Add(m_media_play_ctrl, 0, wxEXPAND | wxALL, 0); // media_ctrl_panel->SetSizer(bSizer_monitoring); // media_ctrl_panel->Layout(); // // sizer->Add(media_ctrl_panel, 1, wxEXPAND | wxALL, 1); + + if (wxGetApp().app_config->get("camera", "enable_custom_source") == "true") { + handle_camera_source_change(); + } + return sizer; } +void StatusBasePanel::on_webview_navigating(wxWebViewEvent& evt) { + wxGetApp().CallAfter([this] { + remove_controls(); + }); +} + wxBoxSizer *StatusBasePanel::create_machine_control_page(wxWindow *parent) { wxBoxSizer *bSizer_right = new wxBoxSizer(wxVERTICAL); @@ -3863,6 +3912,7 @@ void StatusPanel::on_camera_enter(wxMouseEvent& event) } sdcard_hint_dlg->on_show(); }); + m_camera_popup->Bind(EVT_CAM_SOURCE_CHANGE, &StatusPanel::on_camera_source_change, this); wxWindow* ctrl = (wxWindow*)event.GetEventObject(); wxPoint pos = ctrl->ClientToScreen(wxPoint(0, 0)); wxSize sz = ctrl->GetSize(); @@ -3874,6 +3924,71 @@ void StatusPanel::on_camera_enter(wxMouseEvent& event) } } +void StatusBasePanel::on_camera_source_change(wxCommandEvent& event) +{ + handle_camera_source_change(); +} + +void StatusBasePanel::handle_camera_source_change() +{ + const auto new_cam_url = wxGetApp().app_config->get("camera", "custom_source"); + const auto enabled = wxGetApp().app_config->get("camera", "enable_custom_source") == "true"; + + if (enabled && !new_cam_url.empty()) { + m_custom_camera_view->LoadURL(new_cam_url); + toggle_custom_camera(); + m_camera_switch_button->Show(); + } else { + toggle_builtin_camera(); + m_camera_switch_button->Hide(); + } +} + +void StatusBasePanel::toggle_builtin_camera() +{ + m_custom_camera_view->Hide(); + m_media_ctrl->Show(); + m_media_play_ctrl->Show(); +} + +void StatusBasePanel::toggle_custom_camera() +{ + const auto enabled = wxGetApp().app_config->get("camera", "enable_custom_source") == "true"; + + if (enabled) { + m_custom_camera_view->Show(); + m_media_ctrl->Hide(); + m_media_play_ctrl->Hide(); + } +} + +void StatusBasePanel::on_camera_switch_toggled(wxMouseEvent& event) +{ + const auto enabled = wxGetApp().app_config->get("camera", "enable_custom_source") == "true"; + if (enabled && m_media_ctrl->IsShown()) { + toggle_custom_camera(); + } else { + toggle_builtin_camera(); + } +} + +void StatusBasePanel::remove_controls() +{ + const std::string js_cleanup_video_element = R"( + document.body.style.overflow='hidden'; + const video = document.querySelector('video'); + video.setAttribute('style', 'width: 100% !important;'); + video.removeAttribute('controls'); + video.addEventListener('leavepictureinpicture', () => { + window.wx.postMessage('leavepictureinpicture'); + }); + video.addEventListener('enterpictureinpicture', () => { + window.wx.postMessage('enterpictureinpicture'); + }); + )"; + m_custom_camera_view->RunScript(js_cleanup_video_element); +} + void StatusPanel::on_camera_leave(wxMouseEvent& event) { if (obj && m_camera_popup) { diff --git a/src/slic3r/GUI/StatusPanel.hpp b/src/slic3r/GUI/StatusPanel.hpp index fba9dc2f0f..399566373b 100644 --- a/src/slic3r/GUI/StatusPanel.hpp +++ b/src/slic3r/GUI/StatusPanel.hpp @@ -287,6 +287,7 @@ protected: ScalableBitmap m_bitmap_timelapse_off; ScalableBitmap m_bitmap_vcamera_on; ScalableBitmap m_bitmap_vcamera_off; + ScalableBitmap m_bitmap_switch_camera; /* title panel */ wxPanel * media_ctrl_panel; @@ -307,6 +308,7 @@ protected: wxStaticBitmap *m_bitmap_sdcard_img; wxStaticBitmap *m_bitmap_static_use_time; wxStaticBitmap *m_bitmap_static_use_weight; + wxStaticBitmap* m_camera_switch_button; wxMediaCtrl2 * m_media_ctrl; @@ -326,6 +328,7 @@ protected: ScalableButton *m_button_pause_resume; ScalableButton *m_button_abort; Button * m_button_clean; + wxWebView * m_custom_camera_view{nullptr}; wxStaticText * m_text_tasklist_caption; @@ -410,6 +413,13 @@ protected: virtual void on_axis_ctrl_z_down_10(wxCommandEvent &event) { event.Skip(); } virtual void on_axis_ctrl_e_up_10(wxCommandEvent &event) { event.Skip(); } virtual void on_axis_ctrl_e_down_10(wxCommandEvent &event) { event.Skip(); } + void on_camera_source_change(wxCommandEvent& event); + void handle_camera_source_change(); + void remove_controls(); + void on_webview_navigating(wxWebViewEvent& evt); + void on_camera_switch_toggled(wxMouseEvent& event); + void toggle_custom_camera(); + void toggle_builtin_camera(); public: StatusBasePanel(wxWindow * parent, diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 32608833b5..e879000899 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -1995,6 +1995,7 @@ void TabPrint::build() optgroup->append_single_option_line("bridge_density"); optgroup->append_single_option_line("thick_bridges"); optgroup->append_single_option_line("thick_internal_bridges"); + optgroup->append_single_option_line("dont_filter_internal_bridges"); optgroup = page->new_optgroup(L("Overhangs"), L"param_advanced"); optgroup->append_single_option_line("detect_overhang_wall");