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");