Merge branch 'master' of https://github.com/prusa3d/PrusaSlicer into et_gcode_viewer

This commit is contained in:
enricoturri1966 2020-04-29 12:53:55 +02:00
commit 05e4476808
76 changed files with 16873 additions and 901 deletions

View file

@ -59,6 +59,29 @@ if (SLIC3R_GUI)
include(${wxWidgets_USE_FILE})
string(REGEX MATCH "wxpng" WX_PNG_BUILTIN ${wxWidgets_LIBRARIES})
if (PNG_FOUND AND NOT WX_PNG_BUILTIN)
list(FILTER wxWidgets_LIBRARIES EXCLUDE REGEX png)
list(APPEND wxWidgets_LIBRARIES ${PNG_LIBRARIES})
endif ()
string(REGEX MATCH "wxexpat" WX_EXPAT_BUILTIN ${wxWidgets_LIBRARIES})
if (EXPAT_FOUND AND NOT WX_EXPAT_BUILTIN)
list(FILTER wxWidgets_LIBRARIES EXCLUDE REGEX expat)
list(APPEND wxWidgets_LIBRARIES ${EXPAT_LIBRARIES})
endif ()
# This is an issue in the new wxWidgets cmake build, doesn't deal with librt
find_library(LIBRT rt)
if(LIBRT)
list(APPEND wxWidgets_LIBRARIES ${LIBRT})
endif()
# This fixes a OpenGL linking issue on OSX. wxWidgets cmake build includes
# wrong libs for opengl in the link line and it does not link to it by himself.
# libslic3r_gui will link to opengl anyway, so lets override wx
list(FILTER wxWidgets_LIBRARIES EXCLUDE REGEX OpenGL)
# list(REMOVE_ITEM wxWidgets_LIBRARIES oleacc)
message(STATUS "wx libs: ${wxWidgets_LIBRARIES}")
@ -178,13 +201,13 @@ if (WIN32)
elseif (XCODE)
# Because of Debug/Release/etc. configurations (similar to MSVC) the slic3r binary is located in an extra level
add_custom_command(TARGET PrusaSlicer POST_BUILD
COMMAND ln -sf "${SLIC3R_RESOURCES_DIR}" "${CMAKE_CURRENT_BINARY_DIR}/resources"
COMMAND ln -sfn "${SLIC3R_RESOURCES_DIR}" "${CMAKE_CURRENT_BINARY_DIR}/resources"
COMMENT "Symlinking the resources directory into the build tree"
VERBATIM
)
else ()
add_custom_command(TARGET PrusaSlicer POST_BUILD
COMMAND ln -sf "${SLIC3R_RESOURCES_DIR}" "${CMAKE_CURRENT_BINARY_DIR}/../resources"
COMMAND ln -sfn "${SLIC3R_RESOURCES_DIR}" "${CMAKE_CURRENT_BINARY_DIR}/../resources"
COMMENT "Symlinking the resources directory into the build tree"
VERBATIM
)

View file

@ -51,13 +51,14 @@
#include "slic3r/GUI/GUI.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/3DScene.hpp"
#include "slic3r/GUI/InstanceCheck.hpp"
#include "slic3r/GUI/AppConfig.hpp"
#endif /* SLIC3R_GUI */
using namespace Slic3r;
int CLI::run(int argc, char **argv)
{
#ifdef __WXGTK__
// On Linux, wxGTK has no support for Wayland, and the app crashes on
// startup if gtk3 is used. This env var has to be set explicitly to
@ -528,6 +529,16 @@ int CLI::run(int argc, char **argv)
#ifdef SLIC3R_GUI
// #ifdef USE_WX
GUI::GUI_App *gui = new GUI::GUI_App();
bool gui_single_instance_setting = gui->app_config->get("single_instance") == "1";
if (Slic3r::instance_check(argc, argv, gui_single_instance_setting)) {
//TODO: do we have delete gui and other stuff?
return -1;
}
//gui->app_config = app_config;
//app_config = nullptr;
// gui->autosave = m_config.opt_string("autosave");
GUI::GUI_App::SetInstance(gui);
gui->CallAfter([gui, this, &load_configs] {

View file

@ -7,6 +7,8 @@
#include <shellapi.h>
#include <wchar.h>
#ifdef SLIC3R_GUI
extern "C"
{
@ -216,7 +218,6 @@ int APIENTRY wWinMain(HINSTANCE /* hInstance */, HINSTANCE /* hPrevInstance */,
int wmain(int argc, wchar_t **argv)
{
#endif
std::vector<wchar_t*> argv_extended;
argv_extended.emplace_back(argv[0]);

View file

@ -274,7 +274,8 @@ endif ()
encoding_check(libslic3r)
target_compile_definitions(libslic3r PUBLIC -DUSE_TBB -DTBB_USE_CAPTURED_EXCEPTION=0)
target_include_directories(libslic3r PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${LIBNEST2D_INCLUDES} PUBLIC ${CMAKE_CURRENT_BINARY_DIR})
target_include_directories(libslic3r PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} PUBLIC ${CMAKE_CURRENT_BINARY_DIR})
target_include_directories(libslic3r PUBLIC ${EXPAT_INCLUDE_DIRS})
target_link_libraries(libslic3r
libnest2d
admesh

View file

@ -316,6 +316,7 @@ std::string ExtrusionEntity::role_to_string(ExtrusionRole role)
case erOverhangPerimeter : return L("Overhang perimeter");
case erInternalInfill : return L("Internal infill");
case erSolidInfill : return L("Solid infill");
case erIroning : return L("Ironing");
case erTopSolidInfill : return L("Top solid infill");
case erBridgeInfill : return L("Bridge infill");
case erGapFill : return L("Gap fill");

View file

@ -22,6 +22,7 @@ enum ExtrusionRole : uint8_t {
erInternalInfill,
erSolidInfill,
erTopSolidInfill,
erIroning,
erBridgeInfill,
erGapFill,
erSkirt,
@ -54,14 +55,16 @@ inline bool is_infill(ExtrusionRole role)
return role == erBridgeInfill
|| role == erInternalInfill
|| role == erSolidInfill
|| role == erTopSolidInfill;
|| role == erTopSolidInfill
|| role == erIroning;
}
inline bool is_solid_infill(ExtrusionRole role)
{
return role == erBridgeInfill
|| role == erSolidInfill
|| role == erTopSolidInfill;
|| role == erTopSolidInfill
|| role == erIroning;
}
inline bool is_bridge(ExtrusionRole role) {

View file

@ -10,6 +10,7 @@
#include "../Surface.hpp"
#include "FillBase.hpp"
#include "FillRectilinear2.hpp"
namespace Slic3r {
@ -372,7 +373,11 @@ void Layer::make_fills()
// Spacing is modified by the filler to indicate adjustments. Reset it for each expolygon.
f->spacing = surface_fill.params.spacing;
surface_fill.surface.expolygon = std::move(expoly);
Polylines polylines = f->fill_surface(&surface_fill.surface, params);
Polylines polylines;
try {
polylines = f->fill_surface(&surface_fill.surface, params);
} catch (InfillFailedException &) {
}
if (! polylines.empty()) {
// calculate actual flow from spacing (which might have been adjusted by the infill
// pattern generator)
@ -388,8 +393,8 @@ void Layer::make_fills()
flow_width = new_flow.width;
}
// Save into layer.
auto *eec = new ExtrusionEntityCollection();
m_regions[surface_fill.region_id]->fills.entities.push_back(eec);
ExtrusionEntityCollection* eec = nullptr;
m_regions[surface_fill.region_id]->fills.entities.push_back(eec = new ExtrusionEntityCollection());
// Only concentric fills are not sorted.
eec->no_sort = f->no_sort();
extrusion_entities_append_paths(
@ -418,4 +423,170 @@ void Layer::make_fills()
#endif
}
// Create ironing extrusions over top surfaces.
void Layer::make_ironing()
{
// LayerRegion::slices contains surfaces marked with SurfaceType.
// Here we want to collect top surfaces extruded with the same extruder.
// A surface will be ironed with the same extruder to not contaminate the print with another material leaking from the nozzle.
// First classify regions based on the extruder used.
struct IroningParams {
int extruder = -1;
bool just_infill = false;
// Spacing of the ironing lines, also to calculate the extrusion flow from.
double line_spacing;
// Height of the extrusion, to calculate the extrusion flow from.
double height;
double speed;
double angle;
bool operator<(const IroningParams &rhs) const {
if (this->extruder < rhs.extruder)
return true;
if (this->extruder > rhs.extruder)
return false;
if (int(this->just_infill) < int(rhs.just_infill))
return true;
if (int(this->just_infill) > int(rhs.just_infill))
return false;
if (this->line_spacing < rhs.line_spacing)
return true;
if (this->line_spacing > rhs.line_spacing)
return false;
if (this->height < rhs.height)
return true;
if (this->height > rhs.height)
return false;
if (this->speed < rhs.speed)
return true;
if (this->speed > rhs.speed)
return false;
if (this->angle < rhs.angle)
return true;
if (this->angle > rhs.angle)
return false;
return false;
}
bool operator==(const IroningParams &rhs) const {
return this->extruder == rhs.extruder && this->just_infill == rhs.just_infill &&
this->line_spacing == rhs.line_spacing && this->height == rhs.height && this->speed == rhs.speed &&
this->angle == rhs.angle;
}
LayerRegion *layerm = nullptr;
// IdeaMaker: ironing
// ironing flowrate (5% percent)
// ironing speed (10 mm/sec)
// Kisslicer:
// iron off, Sweep, Group
// ironing speed: 15 mm/sec
// Cura:
// Pattern (zig-zag / concentric)
// line spacing (0.1mm)
// flow: from normal layer height. 10%
// speed: 20 mm/sec
};
std::vector<IroningParams> by_extruder;
bool extruder_dont_care = this->object()->config().wipe_into_objects;
double default_layer_height = this->object()->config().layer_height;
for (LayerRegion *layerm : m_regions)
if (! layerm->slices.empty()) {
IroningParams ironing_params;
const PrintRegionConfig &config = layerm->region()->config();
if (config.ironing &&
(config.ironing_type == IroningType::AllSolid ||
(config.top_solid_layers > 0 &&
(config.ironing_type == IroningType::TopSurfaces ||
(config.ironing_type == IroningType::TopmostOnly && layerm->layer()->upper_layer == nullptr))))) {
if (config.perimeter_extruder == config.solid_infill_extruder || config.perimeters == 0) {
// Iron the whole face.
ironing_params.extruder = config.solid_infill_extruder;
} else {
// Iron just the infill.
ironing_params.extruder = config.solid_infill_extruder;
}
}
if (ironing_params.extruder != -1) {
ironing_params.just_infill = false;
ironing_params.line_spacing = config.ironing_spacing;
ironing_params.height = default_layer_height * 0.01 * config.ironing_flowrate;
ironing_params.speed = config.ironing_speed;
ironing_params.angle = config.fill_angle * M_PI / 180.;
ironing_params.layerm = layerm;
by_extruder.emplace_back(ironing_params);
}
}
std::sort(by_extruder.begin(), by_extruder.end());
FillRectilinear2 fill;
FillParams fill_params;
fill.set_bounding_box(this->object()->bounding_box());
fill.layer_id = this->id();
fill.z = this->print_z;
fill.overlap = 0;
fill_params.density = 1.;
// fill_params.dont_connect = true;
fill_params.dont_connect = false;
fill_params.monotonous = true;
for (size_t i = 0; i < by_extruder.size(); ++ i) {
// Find span of regions equivalent to the ironing operation.
IroningParams &ironing_params = by_extruder[i];
size_t j = i;
for (++ j; j < by_extruder.size() && ironing_params == by_extruder[j]; ++ j) ;
// Create the ironing extrusions for regions <i, j)
ExPolygons ironing_areas;
double nozzle_dmr = this->object()->print()->config().nozzle_diameter.values[ironing_params.extruder - 1];
if (ironing_params.just_infill) {
// Just infill.
} else {
// Infill and perimeter.
// Merge top surfaces with the same ironing parameters.
Polygons polys;
for (size_t k = i; k < j; ++ k)
for (const Surface &surface : by_extruder[k].layerm->slices.surfaces)
if (surface.surface_type == stTop)
polygons_append(polys, surface.expolygon);
// Trim the top surfaces with half the nozzle diameter.
ironing_areas = intersection_ex(polys, offset(this->lslices, - float(scale_(0.5 * nozzle_dmr))));
}
// Create the filler object.
fill.spacing = ironing_params.line_spacing;
fill.angle = float(ironing_params.angle + 0.25 * M_PI);
fill.link_max_length = (coord_t)scale_(3. * fill.spacing);
double height = ironing_params.height * fill.spacing / nozzle_dmr;
Flow flow = Flow::new_from_spacing(float(nozzle_dmr), 0., float(height), false);
double flow_mm3_per_mm = flow.mm3_per_mm();
Surface surface_fill(stTop, ExPolygon());
for (ExPolygon &expoly : ironing_areas) {
surface_fill.expolygon = std::move(expoly);
Polylines polylines;
try {
polylines = fill.fill_surface(&surface_fill, fill_params);
} catch (InfillFailedException &) {
}
if (! polylines.empty()) {
// Save into layer.
ExtrusionEntityCollection *eec = nullptr;
ironing_params.layerm->fills.entities.push_back(eec = new ExtrusionEntityCollection());
// Don't sort the ironing infill lines as they are monotonously ordered.
eec->no_sort = true;
extrusion_entities_append_paths(
eec->entities, std::move(polylines),
erIroning,
flow_mm3_per_mm, float(flow.width), float(height));
}
}
}
}
} // namespace Slic3r

View file

@ -27,7 +27,7 @@ Fill* Fill::new_from_type(const InfillPattern type)
case ip3DHoneycomb: return new Fill3DHoneycomb();
case ipGyroid: return new FillGyroid();
case ipRectilinear: return new FillRectilinear2();
// case ipRectilinear: return new FillRectilinear();
case ipMonotonous: return new FillMonotonous();
case ipLine: return new FillLine();
case ipGrid: return new FillGrid2();
case ipTriangles: return new FillTriangles();

View file

@ -5,6 +5,7 @@
#include <memory.h>
#include <float.h>
#include <stdint.h>
#include <stdexcept>
#include <type_traits>
@ -18,29 +19,31 @@ namespace Slic3r {
class ExPolygon;
class Surface;
class InfillFailedException : public std::runtime_error {
public:
InfillFailedException() : std::runtime_error("Infill failed") {}
};
struct FillParams
{
FillParams() {
memset(this, 0, sizeof(FillParams));
// Adjustment does not work.
dont_adjust = true;
}
bool full_infill() const { return density > 0.9999f; }
// Fill density, fraction in <0, 1>
float density;
float density { 0.f };
// Don't connect the fill lines around the inner perimeter.
bool dont_connect;
bool dont_connect { false };
// Don't adjust spacing to fill the space evenly.
bool dont_adjust;
bool dont_adjust { true };
// Monotonous infill - strictly left to right for better surface quality of top infills.
bool monotonous { false };
// For Honeycomb.
// we were requested to complete each loop;
// in this case we don't try to make more continuous paths
bool complete;
bool complete { false };
};
static_assert(IsTriviallyCopyable<FillParams>::value, "FillParams class is not POD (and it should be - see constructor).");

File diff suppressed because it is too large Load diff

View file

@ -13,18 +13,27 @@ class FillRectilinear2 : public Fill
{
public:
virtual Fill* clone() const { return new FillRectilinear2(*this); };
virtual ~FillRectilinear2() {}
virtual ~FillRectilinear2() = default;
virtual Polylines fill_surface(const Surface *surface, const FillParams &params);
protected:
bool fill_surface_by_lines(const Surface *surface, const FillParams &params, float angleBase, float pattern_shift, Polylines &polylines_out);
};
class FillMonotonous : public FillRectilinear2
{
public:
virtual Fill* clone() const { return new FillMonotonous(*this); };
virtual ~FillMonotonous() = default;
virtual Polylines fill_surface(const Surface *surface, const FillParams &params);
virtual bool no_sort() const { return true; }
};
class FillGrid2 : public FillRectilinear2
{
public:
virtual Fill* clone() const { return new FillGrid2(*this); };
virtual ~FillGrid2() {}
virtual ~FillGrid2() = default;
virtual Polylines fill_surface(const Surface *surface, const FillParams &params);
protected:
@ -36,7 +45,7 @@ class FillTriangles : public FillRectilinear2
{
public:
virtual Fill* clone() const { return new FillTriangles(*this); };
virtual ~FillTriangles() {}
virtual ~FillTriangles() = default;
virtual Polylines fill_surface(const Surface *surface, const FillParams &params);
protected:
@ -48,7 +57,7 @@ class FillStars : public FillRectilinear2
{
public:
virtual Fill* clone() const { return new FillStars(*this); };
virtual ~FillStars() {}
virtual ~FillStars() = default;
virtual Polylines fill_surface(const Surface *surface, const FillParams &params);
protected:
@ -60,7 +69,7 @@ class FillCubic : public FillRectilinear2
{
public:
virtual Fill* clone() const { return new FillCubic(*this); };
virtual ~FillCubic() {}
virtual ~FillCubic() = default;
virtual Polylines fill_surface(const Surface *surface, const FillParams &params);
protected:

View file

@ -2297,12 +2297,14 @@ void GCode::process_layer(
const auto& by_region_specific = is_anything_overridden ? island.by_region_per_copy(by_region_per_copy_cache, static_cast<unsigned int>(instance_to_print.instance_id), extruder_id, print_wipe_extrusions != 0) : island.by_region;
//FIXME the following code prints regions in the order they are defined, the path is not optimized in any way.
if (print.config().infill_first) {
gcode += this->extrude_infill(print, by_region_specific);
gcode += this->extrude_infill(print, by_region_specific, false);
gcode += this->extrude_perimeters(print, by_region_specific, lower_layer_edge_grids[instance_to_print.layer_id]);
} else {
gcode += this->extrude_perimeters(print, by_region_specific, lower_layer_edge_grids[instance_to_print.layer_id]);
gcode += this->extrude_infill(print,by_region_specific);
gcode += this->extrude_infill(print,by_region_specific, false);
}
// ironing
gcode += this->extrude_infill(print,by_region_specific, true);
}
if (this->config().gcode_label_objects)
gcode += std::string("; stop printing object ") + instance_to_print.print_object.model_object()->name + " id:" + std::to_string(instance_to_print.layer_id) + " copy " + std::to_string(instance_to_print.instance_id) + "\n";
@ -2924,22 +2926,30 @@ std::string GCode::extrude_perimeters(const Print &print, const std::vector<Obje
}
// Chain the paths hierarchically by a greedy algorithm to minimize a travel distance.
std::string GCode::extrude_infill(const Print &print, const std::vector<ObjectByExtruder::Island::Region> &by_region)
std::string GCode::extrude_infill(const Print &print, const std::vector<ObjectByExtruder::Island::Region> &by_region, bool ironing)
{
std::string gcode;
std::string gcode;
ExtrusionEntitiesPtr extrusions;
const char* extrusion_name = ironing ? "ironing" : "infill";
for (const ObjectByExtruder::Island::Region &region : by_region)
if (! region.infills.empty()) {
m_config.apply(print.regions()[&region - &by_region.front()]->config());
ExtrusionEntitiesPtr extrusions { region.infills };
chain_and_reorder_extrusion_entities(extrusions, &m_last_pos);
for (const ExtrusionEntity *fill : extrusions) {
auto *eec = dynamic_cast<const ExtrusionEntityCollection*>(fill);
if (eec) {
for (ExtrusionEntity *ee : eec->chained_path_from(m_last_pos).entities)
gcode += this->extrude_entity(*ee, "infill");
} else
gcode += this->extrude_entity(*fill, "infill");
}
extrusions.clear();
extrusions.reserve(region.infills.size());
for (ExtrusionEntity *ee : region.infills)
if ((ee->role() == erIroning) == ironing)
extrusions.emplace_back(ee);
if (! extrusions.empty()) {
m_config.apply(print.regions()[&region - &by_region.front()]->config());
chain_and_reorder_extrusion_entities(extrusions, &m_last_pos);
for (const ExtrusionEntity *fill : extrusions) {
auto *eec = dynamic_cast<const ExtrusionEntityCollection*>(fill);
if (eec) {
for (ExtrusionEntity *ee : eec->chained_path_from(m_last_pos).entities)
gcode += this->extrude_entity(*ee, extrusion_name);
} else
gcode += this->extrude_entity(*fill, extrusion_name);
}
}
}
return gcode;
}
@ -3078,6 +3088,8 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description,
speed = m_config.get_abs_value("solid_infill_speed");
} else if (path.role() == erTopSolidInfill) {
speed = m_config.get_abs_value("top_solid_infill_speed");
} else if (path.role() == erIroning) {
speed = m_config.get_abs_value("ironing_speed");
} else if (path.role() == erGapFill) {
speed = m_config.get_abs_value("gap_fill_speed");
} else {
@ -3494,10 +3506,13 @@ void GCode::ObjectByExtruder::Island::Region::append(const Type type, const Extr
// First we append the entities, there are eec->entities.size() of them:
size_t old_size = perimeters_or_infills->size();
size_t new_size = old_size + eec->entities.size();
size_t new_size = old_size + (eec->can_reverse() ? eec->entities.size() : 1);
perimeters_or_infills->reserve(new_size);
for (auto* ee : eec->entities)
perimeters_or_infills->emplace_back(ee);
if (eec->can_reverse()) {
for (auto* ee : eec->entities)
perimeters_or_infills->emplace_back(ee);
} else
perimeters_or_infills->emplace_back(const_cast<ExtrusionEntityCollection*>(eec));
if (copies_extruder != nullptr) {
// Don't reallocate overrides if not needed.

View file

@ -302,7 +302,7 @@ private:
const size_t single_object_instance_idx);
std::string extrude_perimeters(const Print &print, const std::vector<ObjectByExtruder::Island::Region> &by_region, std::unique_ptr<EdgeGrid::Grid> &lower_layer_edge_grid);
std::string extrude_infill(const Print &print, const std::vector<ObjectByExtruder::Island::Region> &by_region);
std::string extrude_infill(const Print &print, const std::vector<ObjectByExtruder::Island::Region> &by_region, bool ironing);
std::string extrude_support(const ExtrusionEntityCollection &support_fills);
std::string travel_to(const Point &point, ExtrusionRole role, std::string comment);

View file

@ -117,6 +117,7 @@ const Color GCodePreviewData::Extrusion::Default_Extrusion_Role_Colors[erCount]
Color(1.0f, 1.0f, 0.0f, 1.0f), // erInternalInfill
Color(1.0f, 0.0f, 1.0f, 1.0f), // erSolidInfill
Color(0.0f, 1.0f, 1.0f, 1.0f), // erTopSolidInfill
Color(0.0f, 1.0f, 1.0f, 1.0f), // erIroning
Color(0.5f, 0.5f, 0.5f, 1.0f), // erBridgeInfill
Color(1.0f, 1.0f, 1.0f, 1.0f), // erGapFill
Color(0.5f, 0.0f, 0.0f, 1.0f), // erSkirt

View file

@ -36,11 +36,6 @@ public:
// collection of surfaces for infill generation
SurfaceCollection fill_surfaces;
// Collection of perimeter surfaces. This is a cached result of diff(slices, fill_surfaces).
// While not necessary, the memory consumption is meager and it speeds up calculation.
// The perimeter_surfaces keep the IDs of the slices (top/bottom/)
SurfaceCollection perimeter_surfaces;
// collection of expolygons representing the bridged areas (thus not
// needing support material)
Polygons bridged;
@ -140,6 +135,7 @@ public:
}
void make_perimeters();
void make_fills();
void make_ironing();
void export_region_slices_to_svg(const char *path) const;
void export_region_fill_surfaces_to_svg(const char *path) const;

View file

@ -1586,6 +1586,8 @@ void Print::process()
this->set_status(70, L("Infilling layers"));
for (PrintObject *obj : m_objects)
obj->infill();
for (PrintObject *obj : m_objects)
obj->ironing();
for (PrintObject *obj : m_objects)
obj->generate_support_material();
if (this->set_started(psWipeTower)) {

View file

@ -44,7 +44,7 @@ enum PrintStep {
enum PrintObjectStep {
posSlice, posPerimeters, posPrepareInfill,
posInfill, posSupportMaterial, posCount,
posInfill, posIroning, posSupportMaterial, posCount,
};
// A PrintRegion object represents a group of volumes to print
@ -226,6 +226,7 @@ private:
void make_perimeters();
void prepare_infill();
void infill();
void ironing();
void generate_support_material();
void _slice(const std::vector<coordf_t> &layer_height_profile);

View file

@ -39,6 +39,11 @@ void PrintConfigDef::init_common_params()
{
ConfigOptionDef* def;
def = this->add("single_instance", coBool);
def->label = L("Single Instance");
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionBool(false));
def = this->add("printer_technology", coEnum);
def->label = L("Printer technology");
def->tooltip = L("Printer technology");
@ -418,18 +423,20 @@ void PrintConfigDef::init_fff_params()
def->cli = "top-fill-pattern|external-fill-pattern|solid-fill-pattern";
def->enum_keys_map = &ConfigOptionEnum<InfillPattern>::get_enum_values();
def->enum_values.push_back("rectilinear");
def->enum_values.push_back("monotonous");
def->enum_values.push_back("concentric");
def->enum_values.push_back("hilbertcurve");
def->enum_values.push_back("archimedeanchords");
def->enum_values.push_back("octagramspiral");
def->enum_labels.push_back(L("Rectilinear"));
def->enum_labels.push_back(L("Monotonous"));
def->enum_labels.push_back(L("Concentric"));
def->enum_labels.push_back(L("Hilbert Curve"));
def->enum_labels.push_back(L("Archimedean Chords"));
def->enum_labels.push_back(L("Octagram Spiral"));
// solid_fill_pattern is an obsolete equivalent to top_fill_pattern/bottom_fill_pattern.
def->aliases = { "solid_fill_pattern", "external_fill_pattern" };
def->set_default_value(new ConfigOptionEnum<InfillPattern>(ipRectilinear));
def->set_default_value(new ConfigOptionEnum<InfillPattern>(ipMonotonous));
def = this->add("bottom_fill_pattern", coEnum);
def->label = L("Bottom fill pattern");
@ -1076,6 +1083,53 @@ void PrintConfigDef::init_fff_params()
def->mode = comExpert;
def->set_default_value(new ConfigOptionBool(false));
def = this->add("ironing", coBool);
def->label = L("Enable ironing");
def->tooltip = L("Enable ironing of the top layers with the hot print head for smooth surface");
def->category = L("Ironing");
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionBool(false));
def = this->add("ironing_type", coEnum);
def->label = L("Ironingy Type");
def->tooltip = L("Ironingy Type");
def->enum_keys_map = &ConfigOptionEnum<IroningType>::get_enum_values();
def->enum_values.push_back("top");
def->enum_values.push_back("topmost");
def->enum_values.push_back("solid");
def->enum_labels.push_back("All top surfaces");
def->enum_labels.push_back("Topmost surface only");
def->enum_labels.push_back("All solid surfaces");
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionEnum<IroningType>(IroningType::TopSurfaces));
def = this->add("ironing_flowrate", coPercent);
def->label = L("Flow rate");
def->category = L("Ironing");
def->tooltip = L("Percent of a flow rate relative to object's normal layer height.");
def->sidetext = L("%");
def->ratio_over = "layer_height";
def->min = 0;
def->mode = comExpert;
def->set_default_value(new ConfigOptionPercent(15));
def = this->add("ironing_spacing", coFloat);
def->label = L("Spacing between ironing passes");
def->tooltip = L("Distance between ironing lins");
def->sidetext = L("mm");
def->min = 0;
def->mode = comExpert;
def->set_default_value(new ConfigOptionFloat(0.1));
def = this->add("ironing_speed", coFloat);
def->label = L("Ironing speed");
def->category = L("Speed");
def->tooltip = L("Ironing speed");
def->sidetext = L("mm/s");
def->min = 0;
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionFloat(15));
def = this->add("layer_gcode", coString);
def->label = L("After layer change G-code");
def->tooltip = L("This custom code is inserted at every layer change, right after the Z move "

View file

@ -34,10 +34,17 @@ enum PrintHostType {
};
enum InfillPattern {
ipRectilinear, ipGrid, ipTriangles, ipStars, ipCubic, ipLine, ipConcentric, ipHoneycomb, ip3DHoneycomb,
ipRectilinear, ipMonotonous, ipGrid, ipTriangles, ipStars, ipCubic, ipLine, ipConcentric, ipHoneycomb, ip3DHoneycomb,
ipGyroid, ipHilbertCurve, ipArchimedeanChords, ipOctagramSpiral, ipCount,
};
enum class IroningType {
TopSurfaces,
TopmostOnly,
AllSolid,
Count,
};
enum SupportMaterialPattern {
smpRectilinear, smpRectilinearGrid, smpHoneycomb,
};
@ -106,6 +113,7 @@ template<> inline const t_config_enum_values& ConfigOptionEnum<InfillPattern>::g
static t_config_enum_values keys_map;
if (keys_map.empty()) {
keys_map["rectilinear"] = ipRectilinear;
keys_map["monotonous"] = ipMonotonous;
keys_map["grid"] = ipGrid;
keys_map["triangles"] = ipTriangles;
keys_map["stars"] = ipStars;
@ -122,6 +130,16 @@ template<> inline const t_config_enum_values& ConfigOptionEnum<InfillPattern>::g
return keys_map;
}
template<> inline const t_config_enum_values& ConfigOptionEnum<IroningType>::get_enum_values() {
static t_config_enum_values keys_map;
if (keys_map.empty()) {
keys_map["top"] = int(IroningType::TopSurfaces);
keys_map["topmost"] = int(IroningType::TopmostOnly);
keys_map["solid"] = int(IroningType::AllSolid);
}
return keys_map;
}
template<> inline const t_config_enum_values& ConfigOptionEnum<SupportMaterialPattern>::get_enum_values() {
static t_config_enum_values keys_map;
if (keys_map.empty()) {
@ -488,6 +506,12 @@ public:
ConfigOptionInt infill_every_layers;
ConfigOptionFloatOrPercent infill_overlap;
ConfigOptionFloat infill_speed;
// Ironing options
ConfigOptionBool ironing;
ConfigOptionEnum<IroningType> ironing_type;
ConfigOptionPercent ironing_flowrate;
ConfigOptionFloat ironing_spacing;
ConfigOptionFloat ironing_speed;
// Detect bridging perimeters
ConfigOptionBool overhangs;
ConfigOptionInt perimeter_extruder;
@ -533,6 +557,11 @@ protected:
OPT_PTR(infill_every_layers);
OPT_PTR(infill_overlap);
OPT_PTR(infill_speed);
OPT_PTR(ironing);
OPT_PTR(ironing_type);
OPT_PTR(ironing_flowrate);
OPT_PTR(ironing_spacing);
OPT_PTR(ironing_speed);
OPT_PTR(overhangs);
OPT_PTR(perimeter_extruder);
OPT_PTR(perimeter_extrusion_width);

View file

@ -387,6 +387,25 @@ void PrintObject::infill()
}
}
void PrintObject::ironing()
{
if (this->set_started(posIroning)) {
BOOST_LOG_TRIVIAL(debug) << "Ironing in parallel - start";
tbb::parallel_for(
tbb::blocked_range<size_t>(1, m_layers.size()),
[this](const tbb::blocked_range<size_t>& range) {
for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++ layer_idx) {
m_print->throw_if_canceled();
m_layers[layer_idx]->make_ironing();
}
}
);
m_print->throw_if_canceled();
BOOST_LOG_TRIVIAL(debug) << "Ironing in parallel - end";
this->set_done(posIroning);
}
}
void PrintObject::generate_support_material()
{
if (this->set_started(posSupportMaterial)) {
@ -2610,6 +2629,7 @@ void PrintObject::combine_infill()
// Because fill areas for rectilinear and honeycomb are grown
// later to overlap perimeters, we need to counteract that too.
((region->config().fill_pattern == ipRectilinear ||
region->config().fill_pattern == ipMonotonous ||
region->config().fill_pattern == ipGrid ||
region->config().fill_pattern == ipLine ||
region->config().fill_pattern == ipHoneycomb) ? 1.5f : 0.5f) *

View file

@ -38,11 +38,12 @@ std::vector<std::pair<size_t, bool>> chain_segments_closest_point(std::vector<En
// Ignore the starting point as the starting point is considered to be occupied, no end point coud connect to it.
size_t next_idx = find_closest_point(kdtree, this_point.pos,
[this_idx, &end_points, &could_reverse_func](size_t idx) {
return (idx ^ this_idx) > 1 && end_points[idx].chain_id == 0 && ((idx ^ 1) == 0 || could_reverse_func(idx >> 1));
return (idx ^ this_idx) > 1 && end_points[idx].chain_id == 0 && ((idx & 1) == 0 || could_reverse_func(idx >> 1));
});
assert(next_idx < end_points.size());
EndPointType &end_point = end_points[next_idx];
end_point.chain_id = 1;
assert((next_idx & 1) == 0 || could_reverse_func(next_idx >> 1));
out.emplace_back(next_idx / 2, (next_idx & 1) != 0);
this_idx = next_idx ^ 1;
}
@ -165,7 +166,9 @@ std::vector<std::pair<size_t, bool>> chain_segments_greedy_constrained_reversals
EndPoint *first_point = nullptr;
size_t first_point_idx = std::numeric_limits<size_t>::max();
if (start_near != nullptr) {
size_t idx = find_closest_point(kdtree, start_near->template cast<double>());
size_t idx = find_closest_point(kdtree, start_near->template cast<double>(),
// Don't start with a reverse segment, if flipping of the segment is not allowed.
[&could_reverse_func](size_t idx) { return (idx & 1) == 0 || could_reverse_func(idx >> 1); });
assert(idx < end_points.size());
first_point = &end_points[idx];
first_point->distance_out = 0.;

View file

@ -2328,10 +2328,15 @@ static inline void fill_expolygons_generate_paths(
fill_params.dont_adjust = true;
for (const ExPolygon &expoly : expolygons) {
Surface surface(stInternal, expoly);
Polylines polylines;
try {
polylines = filler->fill_surface(&surface, fill_params);
} catch (InfillFailedException &) {
}
extrusion_entities_append_paths(
dst,
filler->fill_surface(&surface, fill_params),
role,
std::move(polylines),
role,
flow.mm3_per_mm(), flow.width, flow.height);
}
}
@ -2350,9 +2355,14 @@ static inline void fill_expolygons_generate_paths(
fill_params.dont_adjust = true;
for (ExPolygon &expoly : expolygons) {
Surface surface(stInternal, std::move(expoly));
Polylines polylines;
try {
polylines = filler->fill_surface(&surface, fill_params);
} catch (InfillFailedException &) {
}
extrusion_entities_append_paths(
dst,
filler->fill_surface(&surface, fill_params),
std::move(polylines),
role,
flow.mm3_per_mm(), flow.width, flow.height);
}

View file

@ -26,6 +26,7 @@
// Saves around 32% RAM after slicing step, 6.7% after G-code export (tested on PrusaSlicer 2.2.0 final).
typedef int32_t coord_t;
#else
//FIXME At least FillRectilinear2 requires coord_t to be 32bit.
typedef int64_t coord_t;
#endif

View file

@ -163,6 +163,8 @@ set(SLIC3R_GUI_SOURCES
GUI/DoubleSlider.hpp
GUI/ObjectDataViewModel.cpp
GUI/ObjectDataViewModel.hpp
GUI/InstanceCheck.cpp
GUI/InstanceCheck.hpp
Utils/Http.cpp
Utils/Http.hpp
Utils/FixModelByWin10.cpp
@ -197,6 +199,8 @@ if (APPLE)
GUI/RemovableDriveManagerMM.mm
GUI/RemovableDriveManagerMM.h
GUI/Mouse3DHandlerMac.mm
GUI/InstanceCheckMac.mm
GUI/InstanceCheckMac.h
)
FIND_LIBRARY(DISKARBITRATION_LIBRARY DiskArbitration)
@ -208,6 +212,10 @@ encoding_check(libslic3r_gui)
target_link_libraries(libslic3r_gui libslic3r avrdude cereal imgui GLEW::GLEW OpenGL::GL OpenGL::GLU hidapi libcurl ${wxWidgets_LIBRARIES})
if (CMAKE_SYSTEM_NAME STREQUAL "Linux")
target_link_libraries(libslic3r_gui ${DBUS_LIBRARIES})
endif()
if(APPLE)
target_link_libraries(libslic3r_gui ${DISKARBITRATION_LIBRARY})
endif()

View file

@ -69,6 +69,9 @@ void AppConfig::set_defaults()
set("use_retina_opengl", "1");
#endif
if (get("single_instance").empty())
set("single_instance", "0");
if (get("remember_output_path").empty())
set("remember_output_path", "1");

View file

@ -298,6 +298,10 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig* config)
toggle_field("support_material_extruder", have_support_material || have_skirt);
toggle_field("support_material_speed", have_support_material || have_brim || have_skirt);
bool has_ironing = config->opt_bool("ironing");
for (auto el : { "ironing_type", "ironing_flowrate", "ironing_spacing", "ironing_speed" })
toggle_field(el, has_ironing);
bool have_sequential_printing = config->opt_bool("complete_objects");
for (auto el : { "extruder_clearance_radius", "extruder_clearance_height" })
toggle_field(el, have_sequential_printing);

View file

@ -943,7 +943,7 @@ void Choice::set_value(const boost::any& value, bool change_event)
}
case coEnum: {
int val = boost::any_cast<int>(value);
if (m_opt_id == "top_fill_pattern" || m_opt_id == "bottom_fill_pattern")
if (m_opt_id == "top_fill_pattern" || m_opt_id == "bottom_fill_pattern" || m_opt_id == "fill_pattern")
{
if (!m_opt.enum_values.empty()) {
std::string key;
@ -1013,7 +1013,7 @@ boost::any& Choice::get_value()
if (m_opt.type == coEnum)
{
int ret_enum = field->GetSelection();
if (m_opt_id == "top_fill_pattern" || m_opt_id == "bottom_fill_pattern")
if (m_opt_id == "top_fill_pattern" || m_opt_id == "bottom_fill_pattern" || m_opt_id == "fill_pattern")
{
if (!m_opt.enum_values.empty()) {
std::string key = m_opt.enum_values[ret_enum];
@ -1025,8 +1025,8 @@ boost::any& Choice::get_value()
else
m_value = static_cast<InfillPattern>(0);
}
if (m_opt_id.compare("fill_pattern") == 0)
m_value = static_cast<InfillPattern>(ret_enum);
else if (m_opt_id.compare("ironing_type") == 0)
m_value = static_cast<IroningType>(ret_enum);
else if (m_opt_id.compare("gcode_flavor") == 0)
m_value = static_cast<GCodeFlavor>(ret_enum);
else if (m_opt_id.compare("support_material_pattern") == 0)

View file

@ -188,6 +188,8 @@ void change_opt_value(DynamicPrintConfig& config, const t_config_option_key& opt
opt_key == "bottom_fill_pattern" ||
opt_key == "fill_pattern")
config.set_key_value(opt_key, new ConfigOptionEnum<InfillPattern>(boost::any_cast<InfillPattern>(value)));
else if (opt_key.compare("ironing_type") == 0)
config.set_key_value(opt_key, new ConfigOptionEnum<IroningType>(boost::any_cast<IroningType>(value)));
else if (opt_key.compare("gcode_flavor") == 0)
config.set_key_value(opt_key, new ConfigOptionEnum<GCodeFlavor>(boost::any_cast<GCodeFlavor>(value)));
else if (opt_key.compare("support_material_pattern") == 0)

View file

@ -50,6 +50,7 @@
#include "UpdateDialogs.hpp"
#include "Mouse3DController.hpp"
#include "RemovableDriveManager.hpp"
#include "InstanceCheck.hpp"
#ifdef __WXMSW__
#include <dbt.h>
@ -209,6 +210,17 @@ static void register_win32_device_notification_event()
}
return false;
});
wxWindow::MSWRegisterMessageHandler(WM_COPYDATA, [](wxWindow* win, WXUINT /* nMsg */, WXWPARAM wParam, WXLPARAM lParam) {
COPYDATASTRUCT* copy_data_structure = { 0 };
copy_data_structure = (COPYDATASTRUCT*)lParam;
if (copy_data_structure->dwData == 1) {
LPCWSTR arguments = (LPCWSTR)copy_data_structure->lpData;
Slic3r::GUI::wxGetApp().other_instance_message_handler()->handle_message(boost::nowide::narrow(arguments));
}
return true;
});
}
#endif // WIN32
@ -253,7 +265,11 @@ GUI_App::GUI_App()
, m_imgui(new ImGuiWrapper())
, m_wizard(nullptr)
, m_removable_drive_manager(std::make_unique<RemovableDriveManager>())
{}
, m_other_instance_message_handler(std::make_unique<OtherInstanceMessageHandler>())
{
//app config initializes early becasuse it is used in instance checking in PrusaSlicer.cpp
this->init_app_config();
}
GUI_App::~GUI_App()
{
@ -284,6 +300,30 @@ bool GUI_App::init_opengl()
}
#endif // ENABLE_NON_STATIC_CANVAS_MANAGER
void GUI_App::init_app_config()
{
// Profiles for the alpha are stored into the PrusaSlicer-alpha directory to not mix with the current release.
SetAppName(SLIC3R_APP_KEY);
//SetAppName(SLIC3R_APP_KEY "-beta");
SetAppDisplayName(SLIC3R_APP_NAME);
// Set the Slic3r data directory at the Slic3r XS module.
// Unix: ~/ .Slic3r
// Windows : "C:\Users\username\AppData\Roaming\Slic3r" or "C:\Documents and Settings\username\Application Data\Slic3r"
// Mac : "~/Library/Application Support/Slic3r"
if (data_dir().empty())
set_data_dir(wxStandardPaths::Get().GetUserDataDir().ToUTF8().data());
if (!app_config)
app_config = new AppConfig();
// load settings
app_conf_exists = app_config->exists();
if (app_conf_exists) {
app_config->load();
}
}
bool GUI_App::OnInit()
{
try {
@ -301,34 +341,14 @@ bool GUI_App::on_init_inner()
wxCHECK_MSG(wxDirExists(resources_dir), false,
wxString::Format("Resources path does not exist or is not a directory: %s", resources_dir));
// Profiles for the alpha are stored into the PrusaSlicer-alpha directory to not mix with the current release.
SetAppName(SLIC3R_APP_KEY);
// SetAppName(SLIC3R_APP_KEY "-beta");
SetAppDisplayName(SLIC3R_APP_NAME);
// Enable this to get the default Win32 COMCTRL32 behavior of static boxes.
// Enable this to get the default Win32 COMCTRL32 behavior of static boxes.
// wxSystemOptions::SetOption("msw.staticbox.optimized-paint", 0);
// Enable this to disable Windows Vista themes for all wxNotebooks. The themes seem to lead to terrible
// performance when working on high resolution multi-display setups.
// Enable this to disable Windows Vista themes for all wxNotebooks. The themes seem to lead to terrible
// performance when working on high resolution multi-display setups.
// wxSystemOptions::SetOption("msw.notebook.themed-background", 0);
// Slic3r::debugf "wxWidgets version %s, Wx version %s\n", wxVERSION_STRING, wxVERSION;
// Set the Slic3r data directory at the Slic3r XS module.
// Unix: ~/ .Slic3r
// Windows : "C:\Users\username\AppData\Roaming\Slic3r" or "C:\Documents and Settings\username\Application Data\Slic3r"
// Mac : "~/Library/Application Support/Slic3r"
if (data_dir().empty())
set_data_dir(wxStandardPaths::Get().GetUserDataDir().ToUTF8().data());
app_config = new AppConfig();
// load settings
app_conf_exists = app_config->exists();
if (app_conf_exists) {
app_config->load();
}
std::string msg = Http::tls_global_init();
wxRichMessageDialog
dlg(nullptr,
@ -407,6 +427,8 @@ bool GUI_App::on_init_inner()
if (! plater_)
return;
//m_other_instance_message_handler->report();
if (app_config->dirty() && app_config->get("autosave") == "1")
app_config->save();

View file

@ -35,6 +35,7 @@ class PrintHostJobQueue;
namespace GUI{
class RemovableDriveManager;
class OtherInstanceMessageHandler;
enum FileType
{
FT_STL,
@ -108,7 +109,7 @@ class GUI_App : public wxApp
std::unique_ptr<ImGuiWrapper> m_imgui;
std::unique_ptr<PrintHostJobQueue> m_printhost_job_queue;
ConfigWizard* m_wizard; // Managed by wxWindow tree
std::unique_ptr <OtherInstanceMessageHandler> m_other_instance_message_handler;
public:
bool OnInit() override;
bool initialized() const { return m_initialized; }
@ -196,6 +197,7 @@ public:
std::vector<Tab *> tabs_list;
RemovableDriveManager* removable_drive_manager() { return m_removable_drive_manager.get(); }
OtherInstanceMessageHandler* other_instance_message_handler() { return m_other_instance_message_handler.get(); }
ImGuiWrapper* imgui() { return m_imgui.get(); }
@ -211,6 +213,7 @@ public:
private:
bool on_init_inner();
void init_app_config();
void window_pos_save(wxTopLevelWindow* window, const std::string &name);
void window_pos_restore(wxTopLevelWindow* window, const std::string &name, bool default_maximized = false);
void window_pos_sanitize(wxTopLevelWindow* window);

View file

@ -0,0 +1,495 @@
#include "GUI_App.hpp"
#include "InstanceCheck.hpp"
#include "boost/nowide/convert.hpp"
#include <boost/log/trivial.hpp>
#include <iostream>
#include <fcntl.h>
#include <errno.h>
#if __linux__
#include <dbus/dbus.h> /* Pull in all of D-Bus headers. */
#endif //__linux__
namespace Slic3r {
namespace instance_check_internal
{
struct CommandLineAnalysis
{
bool should_send;
std::string cl_string;
};
static CommandLineAnalysis process_command_line(int argc, char** argv) //d:\3dmodels\Klapka\Klapka.3mf
{
CommandLineAnalysis ret { false };
if (argc < 2)
return ret;
ret.cl_string = argv[0];
for (size_t i = 1; i < argc; i++) {
std::string token = argv[i];
if (token == "--single-instance") {
ret.should_send = true;
} else {
ret.cl_string += " ";
ret.cl_string += token;
}
}
BOOST_LOG_TRIVIAL(debug) << "single instance: "<< ret.should_send << ". other params: " << ret.cl_string;
return ret;
}
} //namespace instance_check_internal
#if _WIN32
namespace instance_check_internal
{
static HWND l_prusa_slicer_hwnd;
static BOOL CALLBACK EnumWindowsProc(_In_ HWND hwnd, _In_ LPARAM lParam)
{
//checks for other instances of prusaslicer, if found brings it to front and return false to stop enumeration and quit this instance
//search is done by classname(wxWindowNR is wxwidgets thing, so probably not unique) and name in window upper panel
//other option would be do a mutex and check for its existence
TCHAR wndText[1000];
TCHAR className[1000];
GetClassName(hwnd, className, 1000);
GetWindowText(hwnd, wndText, 1000);
std::wstring classNameString(className);
std::wstring wndTextString(wndText);
if (wndTextString.find(L"PrusaSlicer") != std::wstring::npos && classNameString == L"wxWindowNR") {
l_prusa_slicer_hwnd = hwnd;
ShowWindow(hwnd, SW_SHOWMAXIMIZED);
SetForegroundWindow(hwnd);
return false;
}
return true;
}
static void send_message(const HWND hwnd)
{
LPWSTR command_line_args = GetCommandLine();
//Create a COPYDATASTRUCT to send the information
//cbData represents the size of the information we want to send.
//lpData represents the information we want to send.
//dwData is an ID defined by us(this is a type of ID different than WM_COPYDATA).
COPYDATASTRUCT data_to_send = { 0 };
data_to_send.dwData = 1;
data_to_send.cbData = sizeof(TCHAR) * (wcslen(command_line_args) + 1);
data_to_send.lpData = command_line_args;
SendMessage(hwnd, WM_COPYDATA, 0, (LPARAM)&data_to_send);
}
} //namespace instance_check_internal
bool instance_check(int argc, char** argv, bool app_config_single_instance)
{
instance_check_internal::CommandLineAnalysis cla = instance_check_internal::process_command_line(argc, argv);
if (cla.should_send || app_config_single_instance) {
// Call EnumWidnows with own callback. cons: Based on text in the name of the window and class name which is generic.
if (!EnumWindows(instance_check_internal::EnumWindowsProc, 0)) {
BOOST_LOG_TRIVIAL(info) << "instance check: Another instance found. This instance will terminate.";
instance_check_internal::send_message(instance_check_internal::l_prusa_slicer_hwnd);
return true;
}
}
BOOST_LOG_TRIVIAL(info) << "instance check: Another instance not found or single-instance not set.";
return false;
}
#elif defined(__APPLE__)
namespace instance_check_internal
{
static int get_lock()
{
struct flock fl;
int fdlock;
fl.l_type = F_WRLCK;
fl.l_whence = SEEK_SET;
fl.l_start = 0;
fl.l_len = 1;
if ((fdlock = open("/tmp/prusaslicer.lock", O_WRONLY | O_CREAT, 0666)) == -1)
return 0;
if (fcntl(fdlock, F_SETLK, &fl) == -1)
return 0;
return 1;
}
} //namespace instance_check_internal
bool instance_check(int argc, char** argv, bool app_config_single_instance)
{
instance_check_internal::CommandLineAnalysis cla = instance_check_internal::process_command_line(argc, argv);
if (!instance_check_internal::get_lock() && (cla.should_send || app_config_single_instance)) {
BOOST_LOG_TRIVIAL(info) << "instance check: Another instance found. This instance will terminate.";
send_message_mac(cla.cl_string);
return true;
}
BOOST_LOG_TRIVIAL(info) << "instance check: Another instance not found or single-instance not set.";
return false;
}
#elif defined(__linux__)
namespace instance_check_internal
{
static int get_lock()
{
struct flock fl;
int fdlock;
fl.l_type = F_WRLCK;
fl.l_whence = SEEK_SET;
fl.l_start = 0;
fl.l_len = 1;
if ((fdlock = open("/tmp/prusaslicer.lock", O_WRONLY | O_CREAT, 0666)) == -1)
return 0;
if (fcntl(fdlock, F_SETLK, &fl) == -1)
return 0;
return 1;
}
static void send_message(std::string message_text)
{
DBusMessage* msg;
DBusMessageIter args;
DBusConnection* conn;
DBusError err;
dbus_uint32_t serial = 0;
const char* sigval = message_text.c_str();
std::string interface_name = "com.prusa3d.prusaslicer.InstanceCheck";
std::string method_name = "AnotherInstace";
std::string object_name = "/com/prusa3d/prusaslicer/InstanceCheck";
// initialise the error value
dbus_error_init(&err);
// connect to bus, and check for errors (use SESSION bus everywhere!)
conn = dbus_bus_get(DBUS_BUS_SESSION, &err);
if (dbus_error_is_set(&err)) {
BOOST_LOG_TRIVIAL(error) << "DBus Connection Error. Message to another instance wont be send.";
BOOST_LOG_TRIVIAL(error) << "DBus Connection Error: "<< err.message;
dbus_error_free(&err);
return;
}
if (NULL == conn) {
BOOST_LOG_TRIVIAL(error) << "DBus Connection is NULL. Message to another instance wont be send.";
return;
}
//some sources do request interface ownership before constructing msg but i think its wrong.
//create new method call message
msg = dbus_message_new_method_call(interface_name.c_str(), object_name.c_str(), interface_name.c_str(), method_name.c_str());
if (NULL == msg) {
BOOST_LOG_TRIVIAL(error) << "DBus Message is NULL. Message to another instance wont be send.";
dbus_connection_unref(conn);
return;
}
//the AnotherInstace method is not sending reply.
dbus_message_set_no_reply(msg, TRUE);
//append arguments to message
if (!dbus_message_append_args(msg, DBUS_TYPE_STRING, &sigval, DBUS_TYPE_INVALID)) {
BOOST_LOG_TRIVIAL(error) << "Ran out of memory while constructing args for DBus message. Message to another instance wont be send.";
dbus_message_unref(msg);
dbus_connection_unref(conn);
return;
}
// send the message and flush the connection
if (!dbus_connection_send(conn, msg, &serial)) {
BOOST_LOG_TRIVIAL(error) << "Ran out of memory while sending DBus message.";
dbus_message_unref(msg);
dbus_connection_unref(conn);
return;
}
dbus_connection_flush(conn);
BOOST_LOG_TRIVIAL(trace) << "DBus message sent.";
// free the message and close the connection
dbus_message_unref(msg);
dbus_connection_unref(conn);
}
} //namespace instance_check_internal
bool instance_check(int argc, char** argv, bool app_config_single_instance)
{
instance_check_internal::CommandLineAnalysis cla = instance_check_internal::process_command_line(argc, argv);
if (!instance_check_internal::get_lock() && (cla.should_send || app_config_single_instance)) {
BOOST_LOG_TRIVIAL(info) << "instance check: Another instance found. This instance will terminate.";
instance_check_internal::send_message(cla.cl_string);
return true;
}
BOOST_LOG_TRIVIAL(info) << "instance check: Another instance not found or single-instance not set.";
return false;
}
#endif //_WIN32/__APPLE__/__linux__
namespace GUI {
wxDEFINE_EVENT(EVT_LOAD_MODEL_OTHER_INSTANCE, LoadFromOtherInstanceEvent);
wxDEFINE_EVENT(EVT_INSTANCE_GO_TO_FRONT, InstanceGoToFrontEvent);
void OtherInstanceMessageHandler::init(wxEvtHandler* callback_evt_handler)
{
assert(!m_initialized);
assert(m_callback_evt_handler == nullptr);
if (m_initialized)
return;
m_initialized = true;
m_callback_evt_handler = callback_evt_handler;
#if _WIN32
//create_listener_window();
#endif //_WIN32
#if defined(__APPLE__)
this->register_for_messages();
#endif //__APPLE__
#ifdef BACKGROUND_MESSAGE_LISTENER
m_thread = boost::thread((boost::bind(&OtherInstanceMessageHandler::listen, this)));
#endif //BACKGROUND_MESSAGE_LISTENER
}
void OtherInstanceMessageHandler::shutdown()
{
BOOST_LOG_TRIVIAL(debug) << "message handler shutdown().";
assert(m_initialized);
if (m_initialized) {
#if __APPLE__
//delete macos implementation
this->unregister_for_messages();
#endif //__APPLE__
#ifdef BACKGROUND_MESSAGE_LISTENER
if (m_thread.joinable()) {
// Stop the worker thread, if running.
{
// Notify the worker thread to cancel wait on detection polling.
std::lock_guard<std::mutex> lck(m_thread_stop_mutex);
m_stop = true;
}
m_thread_stop_condition.notify_all();
// Wait for the worker thread to stop.
m_thread.join();
m_stop = false;
}
#endif //BACKGROUND_MESSAGE_LISTENER
m_initialized = false;
}
}
namespace MessageHandlerInternal
{
// returns ::path to possible model or empty ::path if input string is not existing path
static boost::filesystem::path get_path(const std::string possible_path)
{
BOOST_LOG_TRIVIAL(debug) << "message part: " << possible_path;
if (possible_path.empty() || possible_path.size() < 3) {
BOOST_LOG_TRIVIAL(debug) << "empty";
return boost::filesystem::path();
}
if (boost::filesystem::exists(possible_path)) {
BOOST_LOG_TRIVIAL(debug) << "is path";
return boost::filesystem::path(possible_path);
} else if (possible_path[0] == '\"') {
if(boost::filesystem::exists(possible_path.substr(1, possible_path.size() - 2))) {
BOOST_LOG_TRIVIAL(debug) << "is path in quotes";
return boost::filesystem::path(possible_path.substr(1, possible_path.size() - 2));
}
}
BOOST_LOG_TRIVIAL(debug) << "is NOT path";
return boost::filesystem::path();
}
} //namespace MessageHandlerInternal
void OtherInstanceMessageHandler::handle_message(const std::string message) {
std::vector<boost::filesystem::path> paths;
auto next_space = message.find(' ');
size_t last_space = 0;
int counter = 0;
BOOST_LOG_TRIVIAL(info) << "message from other instance: " << message;
while (next_space != std::string::npos)
{
if (counter != 0) {
const std::string possible_path = message.substr(last_space, next_space - last_space);
boost::filesystem::path p = MessageHandlerInternal::get_path(possible_path);
if(!p.string().empty())
paths.emplace_back(p);
}
last_space = next_space;
next_space = message.find(' ', last_space + 1);
counter++;
}
if (counter != 0 ) {
boost::filesystem::path p = MessageHandlerInternal::get_path(message.substr(last_space + 1));
if (!p.string().empty())
paths.emplace_back(p);
}
if (!paths.empty()) {
//wxEvtHandler* evt_handler = wxGetApp().plater(); //assert here?
//if (evt_handler) {
wxPostEvent(m_callback_evt_handler, LoadFromOtherInstanceEvent(GUI::EVT_LOAD_MODEL_OTHER_INSTANCE, std::vector<boost::filesystem::path>(std::move(paths))));
//}
}
}
#ifdef BACKGROUND_MESSAGE_LISTENER
namespace MessageHandlerDBusInternal
{
//reply to introspect makes our DBus object visible for other programs like D-Feet
static void respond_to_introspect(DBusConnection *connection, DBusMessage *request)
{
DBusMessage *reply;
const char *introspection_data =
" <!DOCTYPE node PUBLIC \"-//freedesktop//DTD D-BUS Object Introspection 1.0//EN\" "
"\"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd\">"
" <!-- dbus-sharp 0.8.1 -->"
" <node>"
" <interface name=\"org.freedesktop.DBus.Introspectable\">"
" <method name=\"Introspect\">"
" <arg name=\"data\" direction=\"out\" type=\"s\" />"
" </method>"
" </interface>"
" <interface name=\"com.prusa3d.prusaslicer.InstanceCheck\">"
" <method name=\"AnotherInstace\">"
" <arg name=\"data\" direction=\"in\" type=\"s\" />"
" </method>"
" </interface>"
" </node>";
reply = dbus_message_new_method_return(request);
dbus_message_append_args(reply, DBUS_TYPE_STRING, &introspection_data, DBUS_TYPE_INVALID);
dbus_connection_send(connection, reply, NULL);
dbus_message_unref(reply);
}
//method AnotherInstance receives message from another PrusaSlicer instance
static void handle_method_another_instance(DBusConnection *connection, DBusMessage *request)
{
DBusError err;
char* text= "";
wxEvtHandler* evt_handler;
dbus_error_init(&err);
dbus_message_get_args(request, &err, DBUS_TYPE_STRING, &text, DBUS_TYPE_INVALID);
if (dbus_error_is_set(&err)) {
BOOST_LOG_TRIVIAL(trace) << "Dbus method AnotherInstance received with wrong arguments.";
dbus_error_free(&err);
return;
}
wxGetApp().other_instance_message_handler()->handle_message(text);
evt_handler = wxGetApp().plater();
if (evt_handler) {
wxPostEvent(evt_handler, InstanceGoToFrontEvent(EVT_INSTANCE_GO_TO_FRONT));
}
}
//every dbus message received comes here
static DBusHandlerResult handle_dbus_object_message(DBusConnection *connection, DBusMessage *message, void *user_data)
{
const char* interface_name = dbus_message_get_interface(message);
const char* member_name = dbus_message_get_member(message);
BOOST_LOG_TRIVIAL(trace) << "DBus message received: interface: " << interface_name << ", member: " << member_name;
if (0 == strcmp("org.freedesktop.DBus.Introspectable", interface_name) && 0 == strcmp("Introspect", member_name)) {
respond_to_introspect(connection, message);
return DBUS_HANDLER_RESULT_HANDLED;
} else if (0 == strcmp("com.prusa3d.prusaslicer.InstanceCheck", interface_name) && 0 == strcmp("AnotherInstace", member_name)) {
handle_method_another_instance(connection, message);
return DBUS_HANDLER_RESULT_HANDLED;
}
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
}
} //namespace MessageHandlerDBusInternal
void OtherInstanceMessageHandler::listen()
{
DBusConnection* conn;
DBusError err;
int name_req_val;
DBusObjectPathVTable vtable;
std::string interface_name = "com.prusa3d.prusaslicer.InstanceCheck";
std::string object_name = "/com/prusa3d/prusaslicer/InstanceCheck";
dbus_error_init(&err);
// connect to the bus and check for errors (use SESSION bus everywhere!)
conn = dbus_bus_get(DBUS_BUS_SESSION, &err);
if (dbus_error_is_set(&err)) {
BOOST_LOG_TRIVIAL(error) << "DBus Connection Error: "<< err.message;
BOOST_LOG_TRIVIAL(error) << "Dbus Messages listening terminating.";
dbus_error_free(&err);
return;
}
if (NULL == conn) {
BOOST_LOG_TRIVIAL(error) << "DBus Connection is NULL. Dbus Messages listening terminating.";
return;
}
// request our name on the bus and check for errors
name_req_val = dbus_bus_request_name(conn, interface_name.c_str(), DBUS_NAME_FLAG_REPLACE_EXISTING , &err);
if (dbus_error_is_set(&err)) {
BOOST_LOG_TRIVIAL(error) << "DBus Request name Error: "<< err.message;
BOOST_LOG_TRIVIAL(error) << "Dbus Messages listening terminating.";
dbus_error_free(&err);
dbus_connection_unref(conn);
return;
}
if (DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER != name_req_val) {
BOOST_LOG_TRIVIAL(error) << "Not primary owner of DBus name - probably another PrusaSlicer instance is running.";
BOOST_LOG_TRIVIAL(error) << "Dbus Messages listening terminating.";
dbus_connection_unref(conn);
return;
}
// Set callbacks. Unregister function should not be nessary.
vtable.message_function = MessageHandlerDBusInternal::handle_dbus_object_message;
vtable.unregister_function = NULL;
// register new object - this is our access to DBus
dbus_connection_try_register_object_path(conn, object_name.c_str(), &vtable, NULL, &err);
if ( dbus_error_is_set(&err) ) {
BOOST_LOG_TRIVIAL(error) << "DBus Register object Error: "<< err.message;
BOOST_LOG_TRIVIAL(error) << "Dbus Messages listening terminating.";
dbus_connection_unref(conn);
dbus_error_free(&err);
return;
}
BOOST_LOG_TRIVIAL(trace) << "Dbus object registered. Starting listening for messages.";
for (;;) {
// Wait for 1 second
// Cancellable.
{
std::unique_lock<std::mutex> lck(m_thread_stop_mutex);
m_thread_stop_condition.wait_for(lck, std::chrono::seconds(1), [this] { return m_stop; });
}
if (m_stop)
// Stop the worker thread.
break;
//dispatch should do all the work with incoming messages
//second parameter is blocking time that funciton waits for new messages
//that is handled here with our own event loop above
dbus_connection_read_write_dispatch(conn, 0);
}
dbus_connection_unref(conn);
}
#endif //BACKGROUND_MESSAGE_LISTENER
} // namespace GUI
} // namespace Slic3r

View file

@ -0,0 +1,91 @@
#ifndef slic3r_InstanceCheck_hpp_
#define slic3r_InstanceCheck_hpp_
#include "Event.hpp"
#if _WIN32
#include <windows.h>
#endif //_WIN32
#include <string>
#include <boost/filesystem.hpp>
#include <boost/thread.hpp>
#include <tbb/mutex.h>
#include <condition_variable>
namespace Slic3r {
// checks for other running instances and sends them argv,
// if there is --single-instance argument or AppConfig is set to single_instance=1
// returns true if this instance should terminate
bool instance_check(int argc, char** argv, bool app_config_single_instance);
#if __APPLE__
// apple implementation of inner functions of instance_check
// in InstanceCheckMac.mm
void send_message_mac(const std::string msg);
#endif //__APPLE__
namespace GUI {
#if __linux__
#define BACKGROUND_MESSAGE_LISTENER
#endif // __linux__
using LoadFromOtherInstanceEvent = Event<std::vector<boost::filesystem::path>>;
wxDECLARE_EVENT(EVT_LOAD_MODEL_OTHER_INSTANCE, LoadFromOtherInstanceEvent);
using InstanceGoToFrontEvent = SimpleEvent;
wxDECLARE_EVENT(EVT_INSTANCE_GO_TO_FRONT, InstanceGoToFrontEvent);
class OtherInstanceMessageHandler
{
public:
OtherInstanceMessageHandler() = default;
OtherInstanceMessageHandler(OtherInstanceMessageHandler const&) = delete;
void operator=(OtherInstanceMessageHandler const&) = delete;
~OtherInstanceMessageHandler() { assert(!m_initialized); }
// inits listening, on each platform different. On linux starts background thread
void init(wxEvtHandler* callback_evt_handler);
// stops listening, on linux stops the background thread
void shutdown();
//finds paths to models in message(= command line arguments, first should be prusaSlicer executable)
//and sends them to plater via LoadFromOtherInstanceEvent
//security of messages: from message all existing paths are proccesed to load model
// win32 - anybody who has hwnd can send message.
// mac - anybody who posts notification with name:@"OtherPrusaSlicerTerminating"
// linux - instrospectable on dbus
void handle_message(const std::string message);
private:
bool m_initialized { false };
wxEvtHandler* m_callback_evt_handler { nullptr };
#ifdef BACKGROUND_MESSAGE_LISTENER
//worker thread to listen incoming dbus communication
boost::thread m_thread;
std::condition_variable m_thread_stop_condition;
mutable std::mutex m_thread_stop_mutex;
bool m_stop{ false };
bool m_start{ true };
// background thread method
void listen();
#endif //BACKGROUND_MESSAGE_LISTENER
#if __APPLE__
//implemented at InstanceCheckMac.mm
void register_for_messages();
void unregister_for_messages();
// Opaque pointer to RemovableDriveManagerMM
void* m_impl_osx;
#endif //__APPLE__
};
} // namespace GUI
} // namespace Slic3r
#endif // slic3r_InstanceCheck_hpp_

View file

@ -0,0 +1,8 @@
#import <Cocoa/Cocoa.h>
@interface OtherInstanceMessageHandlerMac : NSObject
-(instancetype) init;
-(void) add_observer;
-(void) message_update:(NSNotification *)note;
@end

View file

@ -0,0 +1,68 @@
#import "InstanceCheck.hpp"
#import "InstanceCheckMac.h"
#import "GUI_App.hpp"
@implementation OtherInstanceMessageHandlerMac
-(instancetype) init
{
self = [super init];
return self;
}
-(void)add_observer
{
NSLog(@"adding observer");
[[NSDistributedNotificationCenter defaultCenter] addObserver:self selector:@selector(message_update:) name:@"OtherPrusaSlicerInstanceMessage" object:nil suspensionBehavior:NSNotificationSuspensionBehaviorDeliverImmediately];
}
-(void)message_update:(NSNotification *)msg
{
//NSLog(@"recieved msg %@", msg);
//demiaturize all windows
for(NSWindow* win in [NSApp windows])
{
if([win isMiniaturized])
{
[win deminiaturize:self];
}
}
//bring window to front
[[NSApplication sharedApplication] activateIgnoringOtherApps : YES];
//pass message
Slic3r::GUI::wxGetApp().other_instance_message_handler()->handle_message(std::string([msg.userInfo[@"data"] UTF8String]));
}
@end
namespace Slic3r {
void send_message_mac(const std::string msg)
{
NSString *nsmsg = [NSString stringWithCString:msg.c_str() encoding:[NSString defaultCStringEncoding]];
//NSLog(@"sending msg %@", nsmsg);
[[NSDistributedNotificationCenter defaultCenter] postNotificationName:@"OtherPrusaSlicerInstanceMessage" object:nil userInfo:[NSDictionary dictionaryWithObject:nsmsg forKey:@"data"] deliverImmediately:YES];
}
namespace GUI {
void OtherInstanceMessageHandler::register_for_messages()
{
m_impl_osx = [[OtherInstanceMessageHandlerMac alloc] init];
if(m_impl_osx) {
[m_impl_osx add_observer];
}
}
void OtherInstanceMessageHandler::unregister_for_messages()
{
//NSLog(@"unreegistering other instance messages");
if (m_impl_osx) {
[m_impl_osx release];
m_impl_osx = nullptr;
} else {
NSLog(@"unreegister not required");
}
}
}//namespace GUI
}//namespace Slicer

View file

@ -26,6 +26,7 @@
#include "GUI_ObjectList.hpp"
#include "Mouse3DController.hpp"
#include "RemovableDriveManager.hpp"
#include "InstanceCheck.hpp"
#include "I18N.hpp"
#include <fstream>
@ -234,7 +235,8 @@ void MainFrame::shutdown()
// Stop the background thread of the removable drive manager, so that no new updates will be sent to the Plater.
wxGetApp().removable_drive_manager()->shutdown();
//stop listening for messages from other instances
wxGetApp().other_instance_message_handler()->shutdown();
// Save the slic3r.ini.Usually the ini file is saved from "on idle" callback,
// but in rare cases it may not have been called yet.
wxGetApp().app_config->save();

View file

@ -680,6 +680,9 @@ boost::any ConfigOptionsGroup::get_config_value(const DynamicPrintConfig& config
opt_key == "fill_pattern" ) {
ret = static_cast<int>(config.option<ConfigOptionEnum<InfillPattern>>(opt_key)->value);
}
else if (opt_key.compare("ironing_type") == 0 ) {
ret = static_cast<int>(config.option<ConfigOptionEnum<IroningType>>(opt_key)->value);
}
else if (opt_key.compare("gcode_flavor") == 0 ) {
ret = static_cast<int>(config.option<ConfigOptionEnum<GCodeFlavor>>(opt_key)->value);
}

View file

@ -74,6 +74,8 @@
#include "../Utils/FixModelByWin10.hpp"
#include "../Utils/UndoRedo.hpp"
#include "RemovableDriveManager.hpp"
#include "InstanceCheck.hpp"
#if ENABLE_NON_STATIC_CANVAS_MANAGER
#ifdef __APPLE__
#include "Gizmos/GLGizmosManager.hpp"
@ -1963,6 +1965,28 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame)
// Initialize the Undo / Redo stack with a first snapshot.
this->take_snapshot(_L("New Project"));
this->q->Bind(EVT_LOAD_MODEL_OTHER_INSTANCE, [this](LoadFromOtherInstanceEvent &evt) {
BOOST_LOG_TRIVIAL(debug) << "received load from other instance event ";
this->load_files(evt.data, true, true);
});
this->q->Bind(EVT_INSTANCE_GO_TO_FRONT, [this](InstanceGoToFrontEvent &) {
BOOST_LOG_TRIVIAL(debug) << "prusaslicer window going forward";
//this code maximize app window on Fedora
wxGetApp().mainframe->Iconize(false);
if (wxGetApp().mainframe->IsMaximized())
wxGetApp().mainframe->Maximize(true);
else
wxGetApp().mainframe->Maximize(false);
//this code (without code above) maximize window on Ubuntu
wxGetApp().mainframe->Restore();
wxGetApp().GetTopWindow()->SetFocus(); // focus on my window
wxGetApp().GetTopWindow()->Raise(); // bring window to front
wxGetApp().GetTopWindow()->Show(true); // show the window
});
wxGetApp().other_instance_message_handler()->init(this->q);
}
Plater::priv::~priv()

View file

@ -100,6 +100,13 @@ void PreferencesDialog::build()
option = Option (def,"show_incompatible_presets");
m_optgroup_general->append_single_option_line(option);
def.label = L("Single Instance");
def.type = coBool;
def.tooltip = L("If this is enabled, when staring PrusaSlicer and another instance is running, that instance will be reactivated instead.");
def.set_default_value(new ConfigOptionBool{ app_config->has("single_instance") ? app_config->get("single_instance") == "1" : false });
option = Option(def, "single_instance");
m_optgroup_general->append_single_option_line(option);
#if __APPLE__
def.label = L("Use Retina resolution for the 3D scene");
def.type = coBool;
@ -177,6 +184,8 @@ void PreferencesDialog::accept()
app_config->set(it->first, it->second);
}
app_config->save();
EndModal(wxID_OK);
// Nothify the UI to update itself from the ini file.

View file

@ -405,8 +405,9 @@ const std::vector<std::string>& Preset::print_options()
"extra_perimeters", "ensure_vertical_shell_thickness", "avoid_crossing_perimeters", "thin_walls", "overhangs",
"seam_position", "external_perimeters_first", "fill_density", "fill_pattern", "top_fill_pattern", "bottom_fill_pattern",
"infill_every_layers", "infill_only_where_needed", "solid_infill_every_layers", "fill_angle", "bridge_angle",
"solid_infill_below_area", "only_retract_when_crossing_perimeters", "infill_first", "max_print_speed",
"max_volumetric_speed",
"solid_infill_below_area", "only_retract_when_crossing_perimeters", "infill_first",
"ironing", "ironing_type", "ironing_flowrate", "ironing_speed", "ironing_spacing",
"max_print_speed", "max_volumetric_speed",
#ifdef HAS_PRESSURE_EQUALIZER
"max_volumetric_extrusion_rate_slope_positive", "max_volumetric_extrusion_rate_slope_negative",
#endif /* HAS_PRESSURE_EQUALIZER */

View file

@ -1162,6 +1162,12 @@ void TabPrint::build()
optgroup->append_single_option_line("top_fill_pattern");
optgroup->append_single_option_line("bottom_fill_pattern");
optgroup = page->new_optgroup(_(L("Ironing")));
optgroup->append_single_option_line("ironing");
optgroup->append_single_option_line("ironing_type");
optgroup->append_single_option_line("ironing_flowrate");
optgroup->append_single_option_line("ironing_spacing");
optgroup = page->new_optgroup(_(L("Reducing printing time")));
optgroup->append_single_option_line("infill_every_layers");
optgroup->append_single_option_line("infill_only_where_needed");
@ -1222,6 +1228,7 @@ void TabPrint::build()
optgroup->append_single_option_line("support_material_interface_speed");
optgroup->append_single_option_line("bridge_speed");
optgroup->append_single_option_line("gap_fill_speed");
optgroup->append_single_option_line("ironing_speed");
optgroup = page->new_optgroup(_(L("Speed for non-print moves")));
optgroup->append_single_option_line("travel_speed");