diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 66698d4ada..4d3ad00dd0 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -717,6 +717,7 @@ void GCode::_do_export(Print &print, FILE *file) // Prepare the helper object for replacing placeholders in custom G-code and output filename. m_placeholder_parser = print.placeholder_parser(); m_placeholder_parser.update_timestamp(); + print.update_object_placeholders(m_placeholder_parser.config_writable()); // Get optimal tool ordering to minimize tool switches of a multi-exruder print. // For a print by objects, find the 1st printing object. @@ -1622,6 +1623,8 @@ void GCode::process_layer( unsigned int copy_id = 0; for (const Point © : copies) { + if (this->config().gcode_label_objects) + gcode += std::string("; printing object ") + print_object->model_object()->name + " id:" + std::to_string(layer_id) + " copy " + std::to_string(copy_id) + "\n"; // When starting a new object, use the external motion planner for the first travel move. std::pair this_object_copy(print_object, copy); if (m_last_obj_copy != this_object_copy) @@ -1646,7 +1649,9 @@ void GCode::process_layer( gcode += this->extrude_infill(print,by_region_specific); } } - ++copy_id; + if (this->config().gcode_label_objects) + gcode += std::string("; stop printing object ") + print_object->model_object()->name + " id:" + std::to_string(layer_id) + " copy " + std::to_string(copy_id) + "\n"; + ++ copy_id; } } } diff --git a/src/libslic3r/GCode/CoolingBuffer.cpp b/src/libslic3r/GCode/CoolingBuffer.cpp index 40ccc7b09f..552fbf88c8 100644 --- a/src/libslic3r/GCode/CoolingBuffer.cpp +++ b/src/libslic3r/GCode/CoolingBuffer.cpp @@ -83,7 +83,7 @@ struct CoolingLine struct PerExtruderAdjustments { // Calculate the total elapsed time per this extruder, adjusted for the slowdown. - float elapsed_time_total() { + float elapsed_time_total() const { float time_total = 0.f; for (const CoolingLine &line : lines) time_total += line.time; @@ -91,7 +91,7 @@ struct PerExtruderAdjustments } // Calculate the total elapsed time when slowing down // to the minimum extrusion feed rate defined for the current material. - float maximum_time_after_slowdown(bool slowdown_external_perimeters) { + float maximum_time_after_slowdown(bool slowdown_external_perimeters) const { float time_total = 0.f; for (const CoolingLine &line : lines) if (line.adjustable(slowdown_external_perimeters)) { @@ -104,7 +104,7 @@ struct PerExtruderAdjustments return time_total; } // Calculate the adjustable part of the total time. - float adjustable_time(bool slowdown_external_perimeters) { + float adjustable_time(bool slowdown_external_perimeters) const { float time_total = 0.f; for (const CoolingLine &line : lines) if (line.adjustable(slowdown_external_perimeters)) @@ -112,7 +112,7 @@ struct PerExtruderAdjustments return time_total; } // Calculate the non-adjustable part of the total time. - float non_adjustable_time(bool slowdown_external_perimeters) { + float non_adjustable_time(bool slowdown_external_perimeters) const { float time_total = 0.f; for (const CoolingLine &line : lines) if (! line.adjustable(slowdown_external_perimeters)) @@ -169,7 +169,7 @@ struct PerExtruderAdjustments // Calculate the maximum time stretch when slowing down to min_feedrate. // Slowdown to min_feedrate shall be allowed for this extruder's material. // Used by non-proportional slow down. - float time_stretch_when_slowing_down_to_feedrate(float min_feedrate) { + float time_stretch_when_slowing_down_to_feedrate(float min_feedrate) const { float time_stretch = 0.f; assert(this->min_print_speed < min_feedrate + EPSILON); for (size_t i = 0; i < n_lines_adjustable; ++ i) { @@ -221,6 +221,63 @@ struct PerExtruderAdjustments size_t idx_line_end = 0; }; +// Calculate a new feedrate when slowing down by time_stretch for segments faster than min_feedrate. +// Used by non-proportional slow down. +float new_feedrate_to_reach_time_stretch( + std::vector::const_iterator it_begin, std::vector::const_iterator it_end, + float min_feedrate, float time_stretch, size_t max_iter = 20) +{ + float new_feedrate = min_feedrate; + for (size_t iter = 0; iter < max_iter; ++ iter) { + float nomin = 0; + float denom = time_stretch; + for (auto it = it_begin; it != it_end; ++ it) { + assert((*it)->min_print_speed < min_feedrate + EPSILON); + for (size_t i = 0; i < (*it)->n_lines_adjustable; ++i) { + const CoolingLine &line = (*it)->lines[i]; + if (line.feedrate > min_feedrate) { + nomin += line.time * line.feedrate; + denom += line.time; + } + } + } + assert(denom > 0); + if (denom < 0) + return min_feedrate; + new_feedrate = nomin / denom; + assert(new_feedrate > min_feedrate - EPSILON); + if (new_feedrate < min_feedrate + EPSILON) + goto finished; + for (auto it = it_begin; it != it_end; ++ it) + for (size_t i = 0; i < (*it)->n_lines_adjustable; ++i) { + const CoolingLine &line = (*it)->lines[i]; + if (line.feedrate > min_feedrate && line.feedrate < new_feedrate) + // Some of the line segments taken into account in the calculation of nomin / denom are now slower than new_feedrate, + // which makes the new_feedrate lower than it should be. + // Re-run the calculation with a new min_feedrate limit, so that the segments with current feedrate lower than new_feedrate + // are not taken into account. + goto not_finished_yet; + } + goto finished; +not_finished_yet: + min_feedrate = new_feedrate; + } + // Failed to find the new feedrate for the time_stretch. + +finished: + // Test whether the time_stretch was achieved. +#ifndef NDEBUG + { + float time_stretch_final = 0.f; + for (auto it = it_begin; it != it_end; ++ it) + time_stretch_final += (*it)->time_stretch_when_slowing_down_to_feedrate(new_feedrate); + assert(std::abs(time_stretch - time_stretch_final) < EPSILON); + } +#endif /* NDEBUG */ + + return new_feedrate; +} + std::string CoolingBuffer::process_layer(const std::string &gcode, size_t layer_id) { std::vector per_extruder_adjustments = this->parse_layer_gcode(gcode, m_current_pos); @@ -241,12 +298,12 @@ std::vector CoolingBuffer::parse_layer_gcode(const std:: std::vector per_extruder_adjustments(extruders.size()); std::vector map_extruder_to_per_extruder_adjustment(num_extruders, 0); for (size_t i = 0; i < extruders.size(); ++ i) { - PerExtruderAdjustments &adj = per_extruder_adjustments[i]; - unsigned int extruder_id = extruders[i].id(); - adj.extruder_id = extruder_id; - adj.cooling_slow_down_enabled = config.cooling.get_at(extruder_id); - adj.slowdown_below_layer_time = config.slowdown_below_layer_time.get_at(extruder_id); - adj.min_print_speed = config.min_print_speed.get_at(extruder_id); + PerExtruderAdjustments &adj = per_extruder_adjustments[i]; + unsigned int extruder_id = extruders[i].id(); + adj.extruder_id = extruder_id; + adj.cooling_slow_down_enabled = config.cooling.get_at(extruder_id); + adj.slowdown_below_layer_time = config.slowdown_below_layer_time.get_at(extruder_id); + adj.min_print_speed = config.min_print_speed.get_at(extruder_id); map_extruder_to_per_extruder_adjustment[extruder_id] = i; } @@ -452,14 +509,14 @@ static inline void extruder_range_slow_down_non_proportional( std::vector by_min_print_speed(it_begin, it_end); // Find the next highest adjustable feedrate among the extruders. float feedrate = 0; - for (PerExtruderAdjustments *adj : by_min_print_speed) { - adj->idx_line_begin = 0; - adj->idx_line_end = 0; - assert(adj->idx_line_begin < adj->n_lines_adjustable); - if (adj->lines[adj->idx_line_begin].feedrate > feedrate) - feedrate = adj->lines[adj->idx_line_begin].feedrate; - } - assert(feedrate > 0.f); + for (PerExtruderAdjustments *adj : by_min_print_speed) { + adj->idx_line_begin = 0; + adj->idx_line_end = 0; + assert(adj->idx_line_begin < adj->n_lines_adjustable); + if (adj->lines[adj->idx_line_begin].feedrate > feedrate) + feedrate = adj->lines[adj->idx_line_begin].feedrate; + } + assert(feedrate > 0.f); // Sort by min_print_speed, maximum speed first. std::sort(by_min_print_speed.begin(), by_min_print_speed.end(), [](const PerExtruderAdjustments *p1, const PerExtruderAdjustments *p2){ return p1->min_print_speed > p2->min_print_speed; }); @@ -496,7 +553,7 @@ static inline void extruder_range_slow_down_non_proportional( for (auto it = adj; it != by_min_print_speed.end(); ++ it) time_stretch_max += (*it)->time_stretch_when_slowing_down_to_feedrate(feedrate_limit); if (time_stretch_max >= time_stretch) { - feedrate_limit = feedrate - (feedrate - feedrate_limit) * time_stretch / time_stretch_max; + feedrate_limit = new_feedrate_to_reach_time_stretch(adj, by_min_print_speed.end(), feedrate_limit, time_stretch, 20); done = true; } else time_stretch -= time_stretch_max; diff --git a/src/libslic3r/GCode/CoolingBuffer.hpp b/src/libslic3r/GCode/CoolingBuffer.hpp index bf4b082e25..511089ad0c 100644 --- a/src/libslic3r/GCode/CoolingBuffer.hpp +++ b/src/libslic3r/GCode/CoolingBuffer.hpp @@ -9,7 +9,7 @@ namespace Slic3r { class GCode; class Layer; -class PerExtruderAdjustments; +struct PerExtruderAdjustments; // A standalone G-code filter, to control cooling of the print. // The G-code is processed per layer. Once a layer is collected, fan start / stop commands are edited diff --git a/src/libslic3r/GCode/SpiralVase.hpp b/src/libslic3r/GCode/SpiralVase.hpp index 60aa668d89..7872b1d3c5 100644 --- a/src/libslic3r/GCode/SpiralVase.hpp +++ b/src/libslic3r/GCode/SpiralVase.hpp @@ -13,7 +13,7 @@ class SpiralVase { SpiralVase(const PrintConfig &config) : enable(false), _config(&config) { - this->_reader.z() = this->_config->z_offset; + this->_reader.z() = (float)this->_config->z_offset; this->_reader.apply_config(*this->_config); }; std::string process_layer(const std::string &gcode); diff --git a/src/libslic3r/GCodeReader.hpp b/src/libslic3r/GCodeReader.hpp index 13f9e7dd74..f64605a9c5 100644 --- a/src/libslic3r/GCodeReader.hpp +++ b/src/libslic3r/GCodeReader.hpp @@ -43,7 +43,7 @@ public: } bool cmd_is(const char *cmd_test) const { const char *cmd = GCodeReader::skip_whitespaces(m_raw.c_str()); - int len = strlen(cmd_test); + size_t len = strlen(cmd_test); return strncmp(cmd, cmd_test, len) == 0 && GCodeReader::is_end_of_word(cmd[len]); } bool extruding(const GCodeReader &reader) const { return this->cmd_is("G1") && this->dist_E(reader) > 0; } diff --git a/src/libslic3r/Geometry.cpp b/src/libslic3r/Geometry.cpp index 58324893d0..a9d3be5396 100644 --- a/src/libslic3r/Geometry.cpp +++ b/src/libslic3r/Geometry.cpp @@ -1182,8 +1182,6 @@ Vec3d extract_euler_angles(const Eigen::Matrix& { #if ENABLE_NEW_EULER_ANGLES // reference: http://www.gregslabaugh.net/publications/euler.pdf - auto is_approx = [](double value, double test_value) -> bool { return std::abs(value - test_value) < EPSILON; }; - Vec3d angles1 = Vec3d::Zero(); Vec3d angles2 = Vec3d::Zero(); if (is_approx(std::abs(rotation_matrix(2, 0)), 1.0)) diff --git a/src/libslic3r/Model.cpp b/src/libslic3r/Model.cpp index a88bdd9910..08eb8df818 100644 --- a/src/libslic3r/Model.cpp +++ b/src/libslic3r/Model.cpp @@ -547,13 +547,33 @@ void Model::reset_auto_extruder_id() s_auto_extruder_id = 1; } -std::string Model::propose_export_file_name() const +// Propose a filename including path derived from the ModelObject's input path. +// If object's name is filled in, use the object name, otherwise use the input name. +std::string Model::propose_export_file_name_and_path() const { + std::string input_file; for (const ModelObject *model_object : this->objects) for (ModelInstance *model_instance : model_object->instances) - if (model_instance->is_printable()) - return model_object->name.empty() ? model_object->input_file : model_object->name; - return std::string(); + if (model_instance->is_printable()) { + input_file = model_object->input_file; + if (! model_object->name.empty()) { + if (input_file.empty()) + // model_object->input_file was empty, just use model_object->name + input_file = model_object->name; + else { + // Replace file name in input_file with model_object->name, but keep the path and file extension. + input_file = (boost::filesystem::path(model_object->name).parent_path().empty()) ? + (boost::filesystem::path(input_file).parent_path() / model_object->name).make_preferred().string() : + model_object->name; + } + } + if (! input_file.empty()) + goto end; + // Other instances will produce the same name, skip them. + break; + } +end: + return input_file; } ModelObject::~ModelObject() diff --git a/src/libslic3r/Model.hpp b/src/libslic3r/Model.hpp index ba109246a5..b998fbb7db 100644 --- a/src/libslic3r/Model.hpp +++ b/src/libslic3r/Model.hpp @@ -385,6 +385,8 @@ public: const Transform3d& get_matrix(bool dont_translate = false, bool dont_rotate = false, bool dont_scale = false, bool dont_mirror = false) const { return m_transformation.get_matrix(dont_translate, dont_rotate, dont_scale, dont_mirror); } + using ModelBase::set_new_unique_id; + protected: friend class Print; friend class SLAPrint; @@ -607,8 +609,8 @@ public: static std::string get_auto_extruder_id_as_string(unsigned int max_extruders); static void reset_auto_extruder_id(); - // Propose an output file name based on the first printable object's name. - std::string propose_export_file_name() const; + // Propose an output file name & path based on the first printable object's name and source input file's path. + std::string propose_export_file_name_and_path() const; private: MODELBASE_DERIVED_PRIVATE_COPY_MOVE(Model) diff --git a/src/libslic3r/PlaceholderParser.hpp b/src/libslic3r/PlaceholderParser.hpp index b5ed56fa1e..22c790e6b5 100644 --- a/src/libslic3r/PlaceholderParser.hpp +++ b/src/libslic3r/PlaceholderParser.hpp @@ -32,7 +32,8 @@ public: void set(const std::string &key, double value) { this->set(key, new ConfigOptionFloat(value)); } void set(const std::string &key, const std::vector &values) { this->set(key, new ConfigOptionStrings(values)); } void set(const std::string &key, ConfigOption *opt) { m_config.set_key_value(key, opt); } - const DynamicConfig& config() const { return m_config; } + DynamicConfig& config_writable() { return m_config; } + const DynamicConfig& config() const { return m_config; } const ConfigOption* option(const std::string &key) const { return m_config.option(key); } // Fill in the template using a macro processing language. diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 3efb185997..bc692ca90d 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -127,6 +127,7 @@ bool Print::invalidate_state_by_config_options(const std::vectorconfig_apply(src_normalized, true); } - - this->update_object_placeholders(); } bool Print::apply_config(DynamicPrintConfig config) @@ -1095,9 +1094,6 @@ Print::ApplyStatus Print::apply(const Model &model, const DynamicPrintConfig &co } } - //FIXME there may be a race condition with the G-code export running at the background thread. - this->update_object_placeholders(); - #ifdef _DEBUG check_model_ids_equal(m_model, model); #endif /* _DEBUG */ @@ -1854,6 +1850,9 @@ int Print::get_extruder(const ExtrusionEntityCollection& fill, const PrintRegion std::max(region.config().perimeter_extruder.value - 1, 0); } +// Generate a recommended G-code output file name based on the format template, default extension, and template parameters +// (timestamps, object placeholders derived from the model, current placeholder prameters and print statistics. +// Use the final print statistics if available, or just keep the print statistics placeholders if not available yet (before G-code is finalized). std::string Print::output_filename() const { // Set the placeholders for the data know first after the G-code export is finished. diff --git a/src/libslic3r/PrintBase.cpp b/src/libslic3r/PrintBase.cpp index 1d078da305..3fe9a2b4d3 100644 --- a/src/libslic3r/PrintBase.cpp +++ b/src/libslic3r/PrintBase.cpp @@ -15,7 +15,7 @@ namespace Slic3r size_t PrintStateBase::g_last_timestamp = 0; // Update "scale", "input_filename", "input_filename_base" placeholders from the current m_objects. -void PrintBase::update_object_placeholders() +void PrintBase::update_object_placeholders(DynamicConfig &config) const { // get the first input file name std::string input_file; @@ -33,27 +33,29 @@ void PrintBase::update_object_placeholders() "% y:" + boost::lexical_cast(printable->get_scaling_factor(Y) * 100) + "% z:" + boost::lexical_cast(printable->get_scaling_factor(Z) * 100) + "%"); if (input_file.empty()) - input_file = model_object->input_file; + input_file = model_object->name.empty() ? model_object->input_file : model_object->name; } } - PlaceholderParser &pp = m_placeholder_parser; - pp.set("scale", v_scale); + config.set_key_value("year", new ConfigOptionStrings(v_scale)); if (! input_file.empty()) { // get basename with and without suffix const std::string input_basename = boost::filesystem::path(input_file).filename().string(); - pp.set("input_filename", input_basename); + config.set_key_value("input_filename", new ConfigOptionString(input_basename)); const std::string input_basename_base = input_basename.substr(0, input_basename.find_last_of(".")); - pp.set("input_filename_base", input_basename_base); + config.set_key_value("input_filename_base", new ConfigOptionString(input_basename_base)); } } +// Generate an output file name based on the format template, default extension, and template parameters +// (timestamps, object placeholders derived from the model, current placeholder prameters, print statistics - config_override) std::string PrintBase::output_filename(const std::string &format, const std::string &default_ext, const DynamicConfig *config_override) const { DynamicConfig cfg; if (config_override != nullptr) cfg = *config_override; PlaceholderParser::update_timestamp(cfg); + this->update_object_placeholders(cfg); try { boost::filesystem::path filename = this->placeholder_parser().process(format, 0, &cfg); if (filename.extension().empty()) @@ -67,20 +69,9 @@ std::string PrintBase::output_filename(const std::string &format, const std::str std::string PrintBase::output_filepath(const std::string &path) const { // if we were supplied no path, generate an automatic one based on our first object's input file - if (path.empty()) { + if (path.empty()) // get the first input file name - std::string input_file; - for (const ModelObject *model_object : m_model.objects) { - for (ModelInstance *model_instance : model_object->instances) - if (model_instance->is_printable()) { - input_file = model_object->input_file; - break; - } - if (! input_file.empty()) - break; - } - return (boost::filesystem::path(input_file).parent_path() / this->output_filename()).make_preferred().string(); - } + return (boost::filesystem::path(m_model.propose_export_file_name_and_path()).parent_path() / this->output_filename()).make_preferred().string(); // if we were supplied a directory, use it and append our automatically generated filename boost::filesystem::path p(path); diff --git a/src/libslic3r/PrintBase.hpp b/src/libslic3r/PrintBase.hpp index 1a61921d60..84d04d26fe 100644 --- a/src/libslic3r/PrintBase.hpp +++ b/src/libslic3r/PrintBase.hpp @@ -309,7 +309,7 @@ protected: // To be called by this->output_filename() with the format string pulled from the configuration layer. std::string output_filename(const std::string &format, const std::string &default_ext, const DynamicConfig *config_override = nullptr) const; // Update "scale", "input_filename", "input_filename_base" placeholders from the current printable ModelObjects. - void update_object_placeholders(); + void update_object_placeholders(DynamicConfig &config) const; Model m_model; diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index a51bd32f12..e716710215 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -930,6 +930,15 @@ void PrintConfigDef::init_fff_params() def->mode = comExpert; def->default_value = new ConfigOptionEnum(gcfRepRap); + def = this->add("gcode_label_objects", coBool); + def->label = "Label objects"; + def->tooltip = "Enable this to add comments into the G-Code labeling print moves with what object they belong to," + " which is useful for the Octoprint CancelObject plugin. This settings is NOT compatible with " + "Single Extruder Multi Material setup and Wipe into Object / Wipe into Infill."; + def->cli = "gcode-label-objects!"; + def->mode = comAdvanced; + def->default_value = new ConfigOptionBool(0); + def = this->add("high_current_on_filament_swap", coBool); def->label = L("High extruder current on filament swap"); def->tooltip = L("It may be beneficial to increase the extruder motor current during the filament exchange" diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 1b218a192a..6b9e68eeff 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -619,6 +619,7 @@ public: ConfigOptionStrings filament_ramming_parameters; ConfigOptionBool gcode_comments; ConfigOptionEnum gcode_flavor; + ConfigOptionBool gcode_label_objects; ConfigOptionString layer_gcode; ConfigOptionFloat max_print_speed; ConfigOptionFloat max_volumetric_speed; @@ -690,6 +691,7 @@ protected: OPT_PTR(filament_ramming_parameters); OPT_PTR(gcode_comments); OPT_PTR(gcode_flavor); + OPT_PTR(gcode_label_objects); OPT_PTR(layer_gcode); OPT_PTR(max_print_speed); OPT_PTR(max_volumetric_speed); @@ -730,7 +732,7 @@ protected: class PrintConfig : public MachineEnvelopeConfig, public GCodeConfig { STATIC_PRINT_CONFIG_CACHE_DERIVED(PrintConfig) - PrintConfig() : GCodeConfig(0) { initialize_cache(); *this = s_cache_PrintConfig.defaults(); } + PrintConfig() : MachineEnvelopeConfig(0), GCodeConfig(0) { initialize_cache(); *this = s_cache_PrintConfig.defaults(); } public: double min_object_distance() const; static double min_object_distance(const ConfigBase *config); @@ -808,7 +810,7 @@ public: ConfigOptionFloat exp_time_first; protected: - PrintConfig(int) : GCodeConfig(1) {} + PrintConfig(int) : MachineEnvelopeConfig(1), GCodeConfig(1) {} void initialize(StaticCacheBase &cache, const char *base_ptr) { this->MachineEnvelopeConfig::initialize(cache, base_ptr); diff --git a/src/libslic3r/SLA/SLAAutoSupports.cpp b/src/libslic3r/SLA/SLAAutoSupports.cpp index ee87c6b663..98313be3fd 100644 --- a/src/libslic3r/SLA/SLAAutoSupports.cpp +++ b/src/libslic3r/SLA/SLAAutoSupports.cpp @@ -15,7 +15,7 @@ namespace Slic3r { SLAAutoSupports::SLAAutoSupports(const TriangleMesh& mesh, const sla::EigenMesh3D& emesh, const std::vector& slices, const std::vector& heights, const Config& config, std::function throw_on_cancel) -: m_config(config), m_V(emesh.V), m_F(emesh.F), m_throw_on_cancel(throw_on_cancel) +: m_config(config), m_V(emesh.V()), m_F(emesh.F()), m_throw_on_cancel(throw_on_cancel) { // FIXME: It might be safer to get rid of the rand() calls altogether, because it is probably // not always thread-safe and can be slow if it is. @@ -332,4 +332,4 @@ void SLAAutoSupports::project_upward_onto_mesh(std::vector& points) const } -} // namespace Slic3r \ No newline at end of file +} // namespace Slic3r diff --git a/src/libslic3r/SLA/SLARotfinder.cpp b/src/libslic3r/SLA/SLARotfinder.cpp index e66e26706d..1a91041b78 100644 --- a/src/libslic3r/SLA/SLARotfinder.cpp +++ b/src/libslic3r/SLA/SLARotfinder.cpp @@ -28,7 +28,7 @@ std::array find_best_rotation(const ModelObject& modelobj, // We will use only one instance of this converted mesh to examine different // rotations - EigenMesh3D emesh = to_eigenmesh(modelobj); + EigenMesh3D emesh(modelobj.raw_mesh()); // For current iteration number unsigned status = 0; @@ -68,12 +68,12 @@ std::array find_best_rotation(const ModelObject& modelobj, // area. The current function is only an example of how to optimize. // Later we can add more criteria like the number of overhangs, etc... - for(int i = 0; i < m.F.rows(); i++) { - auto idx = m.F.row(i); + for(int i = 0; i < m.F().rows(); i++) { + auto idx = m.F().row(i); - Vec3d p1 = m.V.row(idx(0)); - Vec3d p2 = m.V.row(idx(1)); - Vec3d p3 = m.V.row(idx(2)); + Vec3d p1 = m.V().row(idx(0)); + Vec3d p2 = m.V().row(idx(1)); + Vec3d p3 = m.V().row(idx(2)); Eigen::Vector3d U = p2 - p1; Eigen::Vector3d V = p3 - p1; diff --git a/src/libslic3r/SLA/SLASupportTree.cpp b/src/libslic3r/SLA/SLASupportTree.cpp index 763166d72f..54e3a0189b 100644 --- a/src/libslic3r/SLA/SLASupportTree.cpp +++ b/src/libslic3r/SLA/SLASupportTree.cpp @@ -13,6 +13,7 @@ #include #include +#include /** * Terminology: @@ -510,7 +511,6 @@ struct CompactBridge { // A wrapper struct around the base pool (pad) struct Pad { -// Contour3D mesh; TriangleMesh tmesh; PoolConfig cfg; double zlevel = 0; @@ -543,23 +543,6 @@ struct Pad { bool empty() const { return tmesh.facets_count() == 0; } }; -EigenMesh3D to_eigenmesh(const Contour3D& cntr) { - EigenMesh3D emesh; - - auto& V = emesh.V; - auto& F = emesh.F; - - V.resize(Eigen::Index(cntr.points.size()), 3); - F.resize(Eigen::Index(cntr.indices.size()), 3); - - for (int i = 0; i < V.rows(); ++i) { - V.row(i) = cntr.points[size_t(i)]; - F.row(i) = cntr.indices[size_t(i)]; - } - - return emesh; -} - // The minimum distance for two support points to remain valid. static const double /*constexpr*/ D_SP = 0.1; @@ -567,46 +550,6 @@ enum { // For indexing Eigen vectors as v(X), v(Y), v(Z) instead of numbers X, Y, Z }; -EigenMesh3D to_eigenmesh(const TriangleMesh& tmesh) { - - const stl_file& stl = tmesh.stl; - - EigenMesh3D outmesh; - - auto&& bb = tmesh.bounding_box(); - outmesh.ground_level += bb.min(Z); - - auto& V = outmesh.V; - auto& F = outmesh.F; - - V.resize(3*stl.stats.number_of_facets, 3); - F.resize(stl.stats.number_of_facets, 3); - for (unsigned int i = 0; i < stl.stats.number_of_facets; ++i) { - const stl_facet* facet = stl.facet_start+i; - V(3*i+0, 0) = double(facet->vertex[0](0)); - V(3*i+0, 1) = double(facet->vertex[0](1)); - V(3*i+0, 2) = double(facet->vertex[0](2)); - - V(3*i+1, 0) = double(facet->vertex[1](0)); - V(3*i+1, 1) = double(facet->vertex[1](1)); - V(3*i+1, 2) = double(facet->vertex[1](2)); - - V(3*i+2, 0) = double(facet->vertex[2](0)); - V(3*i+2, 1) = double(facet->vertex[2](1)); - V(3*i+2, 2) = double(facet->vertex[2](2)); - - F(i, 0) = int(3*i+0); - F(i, 1) = int(3*i+1); - F(i, 2) = int(3*i+2); - } - - return outmesh; -} - -EigenMesh3D to_eigenmesh(const ModelObject& modelobj) { - return to_eigenmesh(modelobj.raw_mesh()); -} - PointSet to_point_set(const std::vector &v) { PointSet ret(v.size(), 3); @@ -618,9 +561,170 @@ Vec3d model_coord(const ModelInstance& object, const Vec3f& mesh_coord) { return object.transform_vector(mesh_coord.cast()); } -double ray_mesh_intersect(const Vec3d& s, - const Vec3d& dir, - const EigenMesh3D& m); +inline double ray_mesh_intersect(const Vec3d& s, + const Vec3d& dir, + const EigenMesh3D& m) +{ + return m.query_ray_hit(s, dir).distance(); +} + +// This function will test if a future pinhead would not collide with the model +// geometry. It does not take a 'Head' object because those are created after +// this test. +// Parameters: +// s: The touching point on the model surface. +// dir: This is the direction of the head from the pin to the back +// r_pin, r_back: the radiuses of the pin and the back sphere +// width: This is the full width from the pin center to the back center +// m: The object mesh +// +// Optional: +// samples: how many rays will be shot +// safety distance: This will be added to the radiuses to have a safety distance +// from the mesh. +double pinhead_mesh_intersect(const Vec3d& s, + const Vec3d& dir, + double r_pin, + double r_back, + double width, + const EigenMesh3D& m, + unsigned samples = 8, + double safety_distance = 0.001) +{ + // method based on: + // https://math.stackexchange.com/questions/73237/parametric-equation-of-a-circle-in-3d-space + + + // We will shoot multiple rays from the head pinpoint in the direction of + // the pinhead robe (side) surface. The result will be the smallest hit + // distance. + + // Move away slightly from the touching point to avoid raycasting on the + // inner surface of the mesh. + Vec3d v = dir; // Our direction (axis) + Vec3d c = s + width * dir; + const double& sd = safety_distance; + + // Two vectors that will be perpendicular to each other and to the axis. + // Values for a(X) and a(Y) are now arbitrary, a(Z) is just a placeholder. + Vec3d a(0, 1, 0), b; + + // The portions of the circle (the head-back circle) for which we will shoot + // rays. + std::vector phis(samples); + for(size_t i = 0; i < phis.size(); ++i) phis[i] = i*2*PI/phis.size(); + + a(Z) = -(v(X)*a(X) + v(Y)*a(Y)) / v(Z); + + b = a.cross(v); + + // Now a and b vectors are perpendicular to v and to each other. Together + // they define the plane where we have to iterate with the given angles + // in the 'phis' vector + + tbb::parallel_for(size_t(0), phis.size(), + [&phis, &m, sd, r_pin, r_back, s, a, b, c](size_t i) + { + double& phi = phis[i]; + double sinphi = std::sin(phi); + double cosphi = std::cos(phi); + + // Let's have a safety coefficient for the radiuses. + double rpscos = (sd + r_pin) * cosphi; + double rpssin = (sd + r_pin) * sinphi; + double rpbcos = (sd + r_back) * cosphi; + double rpbsin = (sd + r_back) * sinphi; + + // Point on the circle on the pin sphere + Vec3d ps(s(X) + rpscos * a(X) + rpssin * b(X), + s(Y) + rpscos * a(Y) + rpssin * b(Y), + s(Z) + rpscos * a(Z) + rpssin * b(Z)); + + // Point ps is not on mesh but can be inside or outside as well. This + // would cause many problems with ray-casting. So we query the closest + // point on the mesh to this. +// auto psq = m.signed_distance(ps); + + // This is the point on the circle on the back sphere + Vec3d p(c(X) + rpbcos * a(X) + rpbsin * b(X), + c(Y) + rpbcos * a(Y) + rpbsin * b(Y), + c(Z) + rpbcos * a(Z) + rpbsin * b(Z)); + +// Vec3d n = (p - psq.point_on_mesh()).normalized(); +// phi = m.query_ray_hit(psq.point_on_mesh() + sd*n, n); + + Vec3d n = (p - ps).normalized(); + auto hr = m.query_ray_hit(ps + sd*n, n); + + if(hr.is_inside()) { // the hit is inside the model + if(hr.distance() > 2*r_pin) phi = 0; + else { + // re-cast the ray from the outside of the object + auto hr2 = m.query_ray_hit(ps + (hr.distance() + 2*sd)*n, n); + phi = hr2.distance(); + } + } else phi = hr.distance(); + }); + + auto mit = std::min_element(phis.begin(), phis.end()); + + return *mit; +} + +// Checking bridge (pillar and stick as well) intersection with the model. If +// the function is used for headless sticks, the ins_check parameter have to be +// true as the beginning of the stick might be inside the model geometry. +double bridge_mesh_intersect(const Vec3d& s, + const Vec3d& dir, + double r, + const EigenMesh3D& m, + bool ins_check = false, + unsigned samples = 4, + double safety_distance = 0.001) +{ + // helper vector calculations + Vec3d a(0, 1, 0), b; + const double& sd = safety_distance; + + a(Z) = -(dir(X)*a(X) + dir(Y)*a(Y)) / dir(Z); + b = a.cross(dir); + + // circle portions + std::vector phis(samples); + for(size_t i = 0; i < phis.size(); ++i) phis[i] = i*2*PI/phis.size(); + + tbb::parallel_for(size_t(0), phis.size(), + [&phis, &m, a, b, sd, dir, r, s, ins_check](size_t i) + { + double& phi = phis[i]; + double sinphi = std::sin(phi); + double cosphi = std::cos(phi); + + // Let's have a safety coefficient for the radiuses. + double rcos = (sd + r) * cosphi; + double rsin = (sd + r) * sinphi; + + // Point on the circle on the pin sphere + Vec3d p (s(X) + rcos * a(X) + rsin * b(X), + s(Y) + rcos * a(Y) + rsin * b(Y), + s(Z) + rcos * a(Z) + rsin * b(Z)); + + auto hr = m.query_ray_hit(p + sd*dir, dir); + + if(ins_check && hr.is_inside()) { + if(hr.distance() > 2*r) phi = 0; + else { + // re-cast the ray from the outside of the object + auto hr2 = m.query_ray_hit(p + (hr.distance() + 2*sd)*dir, dir); + phi = hr2.distance(); + } + } else phi = hr.distance(); + }); + + auto mit = std::min_element(phis.begin(), phis.end()); + + return *mit; +} PointSet normals(const PointSet& points, const EigenMesh3D& mesh, double eps = 0.05, // min distance from edges @@ -640,6 +744,19 @@ ClusteredPoints cluster( std::function pred, unsigned max_points = 0); +// This class will hold the support tree meshes with some additional bookkeeping +// as well. Various parts of the support geometry are stored separately and are +// merged when the caller queries the merged mesh. The merged result is cached +// for fast subsequent delivery of the merged mesh which can be quite complex. +// An object of this class will be used as the result type during the support +// generation algorithm. Parts will be added with the appropriate methods such +// as add_head or add_pillar which forwards the constructor arguments and fills +// the IDs of these substructures. The IDs are basically indices into the arrays +// of the appropriate type (heads, pillars, etc...). One can later query e.g. a +// pillar for a specific head... +// +// The support pad is considered an auxiliary geometry and is not part of the +// merged mesh. It can be retrieved using a dedicated method (pad()) class SLASupportTree::Impl { std::vector m_heads; std::vector m_pillars; @@ -1001,16 +1118,22 @@ bool SLASupportTree::generate(const PointSet &points, // Indices of those who don't touch the ground IndexSet noground_heads; + // Groups of the 'ground_head' indices that belong into one cluster. These + // are candidates to be connected to one pillar. ClusteredPoints ground_connectors; + // A help function to translate ground head index to the actual coordinates. auto gnd_head_pt = [&ground_heads, &head_positions] (size_t idx) { return Vec3d(head_positions.row(ground_heads[idx])); }; + // This algorithm uses the Impl class as its output stream. It will be + // filled gradually with support elements (heads, pillars, bridges, ...) using Result = SLASupportTree::Impl; - Result& result = *m_impl; + // Let's define the individual steps of the processing. We can experiment + // later with the ordering and the dependencies between them. enum Steps { BEGIN, FILTER, @@ -1026,14 +1149,15 @@ bool SLASupportTree::generate(const PointSet &points, //... }; - // Debug: - // for(int pn = 0; pn < points.rows(); ++pn) { - // std::cout << "p " << pn << " " << points.row(pn) << std::endl; - // } - - + // t-hrow i-f c-ance-l-ed: It will be called many times so a shorthand will + // come in handy. auto& tifcl = ctl.cancelfn; + // Filtering step: here we will discard inappropriate support points and + // decide the future of the appropriate ones. We will check if a pinhead + // is applicable and adjust its angle at each support point. + // We will also merge the support points that are just too close and can be + // considered as one. auto filterfn = [tifcl] ( const SupportConfig& cfg, const PointSet& points, @@ -1044,10 +1168,6 @@ bool SLASupportTree::generate(const PointSet &points, PointSet& headless_pos, PointSet& headless_norm) { - /* ******************************************************** */ - /* Filtering step */ - /* ******************************************************** */ - // Get the points that are too close to each other and keep only the // first one auto aliases = @@ -1108,6 +1228,8 @@ bool SLASupportTree::generate(const PointSet &points, std::sin(azimuth) * std::sin(polar), std::cos(polar)); + nn.normalize(); + // save the head (pinpoint) position Vec3d hp = filt_pts.row(i); @@ -1118,11 +1240,15 @@ bool SLASupportTree::generate(const PointSet &points, // We should shoot a ray in the direction of the pinhead and // see if there is enough space for it - double t = ray_mesh_intersect(hp + 0.1*nn, nn, mesh); - - if(t > 2*w || std::isinf(t)) { - // 2*w because of lower and upper pinhead + double t = pinhead_mesh_intersect( + hp, // touching point + nn, + cfg.head_front_radius_mm, // approx the radius + cfg.head_back_radius_mm, + w, + mesh); + if(t > w || std::isinf(t)) { head_pos.row(pcount) = hp; // save the verified and corrected normal @@ -1144,7 +1270,8 @@ bool SLASupportTree::generate(const PointSet &points, headless_norm.conservativeResize(hlcount, Eigen::NoChange); }; - // Function to write the pinheads into the result + // Pinhead creation: based on the filtering results, the Head objects will + // be constructed (together with their triangle meshes). auto pinheadfn = [tifcl] ( const SupportConfig& cfg, PointSet& head_pos, @@ -1170,8 +1297,13 @@ bool SLASupportTree::generate(const PointSet &points, } }; - // &filtered_points, &head_positions, &result, &mesh, - // &gndidx, &gndheight, &nogndidx, cfg + // Further classification of the support points with pinheads. If the + // ground is directly reachable through a vertical line parallel to the Z + // axis we consider a support point as pillar candidate. If touches the + // model geometry, it will be marked as non-ground facing and further steps + // will process it. Also, the pillars will be grouped into clusters that can + // be interconnected with bridges. Elements of these groups may or may not + // be interconnected. Here we only run the clustering algorithm. auto classifyfn = [tifcl] ( const SupportConfig& cfg, const EigenMesh3D& mesh, @@ -1192,23 +1324,81 @@ bool SLASupportTree::generate(const PointSet &points, gndidx.reserve(size_t(head_pos.rows())); nogndidx.reserve(size_t(head_pos.rows())); + // First we search decide which heads reach the ground and can be full + // pillars and which shall be connected to the model surface (or search + // a suitable path around the surface that leads to the ground -- TODO) for(unsigned i = 0; i < head_pos.rows(); i++) { tifcl(); - auto& head = result.heads()[i]; + auto& head = result.head(i); Vec3d dir(0, 0, -1); - Vec3d startpoint = head.junction_point(); + bool accept = false; + int ri = 1; + double t = std::numeric_limits::infinity(); + double hw = head.width_mm; - double t = ray_mesh_intersect(startpoint, dir, mesh); + // We will try to assign a pillar to all the pinheads. If a pillar + // would pierce the model surface, we will try to adjust slightly + // the head with so that the pillar can be deployed. + while(!accept && head.width_mm > 0) { + Vec3d startpoint = head.junction_point(); + + // Collision detection + t = bridge_mesh_intersect(startpoint, dir, head.r_back_mm, mesh); + + // Precise distance measurement + double tprec = ray_mesh_intersect(startpoint, dir, mesh); + + if(std::isinf(tprec) && !std::isinf(t)) { + // This is a damned case where the pillar melds into the + // model but its center ray can reach the ground. We can + // not route this to the ground nor to the model surface. + head.width_mm = hw + (ri % 2? -1 : 1) * ri * head.r_back_mm; + } else { + accept = true; t = tprec; + + auto id = head.id; + // We need to regenerate the head geometry + head = Head(head.r_back_mm, + head.r_pin_mm, + head.width_mm, + head.penetration_mm, + head.dir, + head.tr); + head.id = id; + } + + ri++; + } + + // Save the distance from a surface in the Z axis downwards. It may + // be infinity but that is telling us that it touches the ground. gndheight.emplace_back(t); - if(std::isinf(t)) gndidx.emplace_back(i); - else nogndidx.emplace_back(i); + if(accept) { + if(std::isinf(t)) gndidx.emplace_back(i); + else nogndidx.emplace_back(i); + } else { + // This is a serious issue. There was no way to deploy a pillar + // for the given pinhead. The whole thing has to be discarded + // leaving the model potentially unprintable. + // + // TODO: In the future this has to be solved by searching for + // a path in 3D space from this support point to a suitable + // pillar position or an existing pillar. + // As a workaround we could mark this head as "sidehead only" + // let it go trough the nearby pillar search in the next step. + BOOST_LOG_TRIVIAL(warning) << "A support point at " + << head.tr.transpose() + << " had to be discarded as there is" + << " nowhere to route it."; + head.invalidate(); + } } + // Transform the ground facing point indices top actual coordinates. PointSet gnd(gndidx.size(), 3); - for(size_t i = 0; i < gndidx.size(); i++) gnd.row(long(i)) = head_pos.row(gndidx[i]); @@ -1228,7 +1418,8 @@ bool SLASupportTree::generate(const PointSet &points, }, 3); // max 3 heads to connect to one centroid }; - // Helper function for interconnecting two pillars with zig-zag bridges + // Helper function for interconnecting two pillars with zig-zag bridges. + // This is not an individual step. auto interconnect = [&cfg]( const Pillar& pillar, const Pillar& nextpillar, @@ -1246,7 +1437,7 @@ bool SLASupportTree::generate(const PointSet &points, double zstep = pillar_dist * std::tan(-cfg.tilt); ej(Z) = sj(Z) + zstep; - double chkd = ray_mesh_intersect(sj, dirv(sj, ej), emesh); + double chkd = bridge_mesh_intersect(sj, dirv(sj, ej), pillar.r, emesh); double bridge_distance = pillar_dist / std::cos(-cfg.tilt); // If the pillars are so close that they touch each other, @@ -1254,7 +1445,7 @@ bool SLASupportTree::generate(const PointSet &points, if(pillar_dist > 2*cfg.head_back_radius_mm && bridge_distance < cfg.max_bridge_length_mm) while(sj(Z) > pillar.endpoint(Z) + cfg.base_radius_mm && - ej(Z) > nextpillar.endpoint(Z) + + cfg.base_radius_mm) + ej(Z) > nextpillar.endpoint(Z) + cfg.base_radius_mm) { if(chkd >= bridge_distance) { result.add_bridge(sj, ej, pillar.r); @@ -1272,9 +1463,11 @@ bool SLASupportTree::generate(const PointSet &points, Vec3d bej(sj(X), sj(Y), ej(Z)); // need to check collision for the cross stick - double backchkd = ray_mesh_intersect(bsj, - dirv(bsj, bej), - emesh); + double backchkd = bridge_mesh_intersect(bsj, + dirv(bsj, bej), + pillar.r, + emesh); + if(backchkd >= bridge_distance) { result.add_bridge(bsj, bej, pillar.r); @@ -1283,10 +1476,15 @@ bool SLASupportTree::generate(const PointSet &points, } sj.swap(ej); ej(Z) = sj(Z) + zstep; - chkd = ray_mesh_intersect(sj, dirv(sj, ej), emesh); + chkd = bridge_mesh_intersect(sj, dirv(sj, ej), pillar.r, emesh); } }; + // Step: Routing the ground connected pinheads, and interconnecting them + // with additional (angled) bridges. Not all of these pinheads will be + // a full pillar (ground connected). Some will connect to a nearby pillar + // using a bridge. The max number of such side-heads for a central pillar + // is limited to avoid bad weight distribution. auto routing_ground_fn = [gnd_head_pt, interconnect, tifcl]( const SupportConfig& cfg, const ClusteredPoints& gnd_clusters, @@ -1361,12 +1559,12 @@ bool SLASupportTree::generate(const PointSet &points, // is distributed more effectively on the pillar. auto search_nearest = - [&cfg, &result, &emesh, maxbridgelen, gndlvl] + [&tifcl, &cfg, &result, &emesh, maxbridgelen, gndlvl, pradius] (SpatIndex& spindex, const Vec3d& jsh) { long nearest_id = -1; const double max_len = maxbridgelen / 2; - while(nearest_id < 0 && !spindex.empty()) { + while(nearest_id < 0 && !spindex.empty()) { tifcl(); // loop until a suitable head is not found // if there is a pillar closer than the cluster center // (this may happen as the clustering is not perfect) @@ -1391,10 +1589,13 @@ bool SLASupportTree::generate(const PointSet &points, } double d = distance(jp, jn); - if(jn(Z) <= (gndlvl + 2*cfg.head_width_mm) || d > max_len) + + if(jn(Z) <= gndlvl + 2*cfg.head_width_mm || d > max_len) break; - double chkd = ray_mesh_intersect(jp, dirv(jp, jn), emesh); + double chkd = bridge_mesh_intersect(jp, dirv(jp, jn), + pradius, + emesh); if(chkd >= d) nearest_id = ne.second; spindex.remove(ne); @@ -1480,7 +1681,7 @@ bool SLASupportTree::generate(const PointSet &points, if(!ring.empty()) { // inner ring is now in 'newring' and outer ring is in 'ring' SpatIndex innerring; - for(unsigned i : newring) { + for(unsigned i : newring) { tifcl(); const Pillar& pill = result.head_pillar(gndidx[i]); assert(pill.id >= 0); innerring.insert(pill.endpoint, unsigned(pill.id)); @@ -1489,7 +1690,7 @@ bool SLASupportTree::generate(const PointSet &points, // For all pillars in the outer ring find the closest in the // inner ring and connect them. This will create the spider web // fashioned connections between pillars - for(unsigned i : ring) { + for(unsigned i : ring) { tifcl(); const Pillar& outerpill = result.head_pillar(gndidx[i]); auto res = innerring.nearest(outerpill.endpoint, 1); if(res.empty()) continue; @@ -1515,6 +1716,7 @@ bool SLASupportTree::generate(const PointSet &points, next != ring.end(); ++it, ++next) { + tifcl(); const Pillar& pillar = result.head_pillar(gndidx[*it]); const Pillar& nextpillar = result.head_pillar(gndidx[*next]); interconnect(pillar, nextpillar, emesh, result); @@ -1529,6 +1731,11 @@ bool SLASupportTree::generate(const PointSet &points, } }; + // Step: routing the pinheads that are would connect to the model surface + // along the Z axis downwards. For now these will actually be connected with + // the model surface with a flipped pinhead. In the future here we could use + // some smart algorithms to search for a safe path to the ground or to a + // nearby pillar that can hold the supported weight. auto routing_nongnd_fn = [tifcl]( const SupportConfig& cfg, const std::vector& gndheight, @@ -1590,6 +1797,9 @@ bool SLASupportTree::generate(const PointSet &points, } }; + // Step: process the support points where there is not enough space for a + // full pinhead. In this case we will use a rounded sphere as a touching + // point and use a thinner bridge (let's call it a stick). auto process_headless = [tifcl]( const SupportConfig& cfg, const PointSet& headless_pts, @@ -1606,32 +1816,48 @@ bool SLASupportTree::generate(const PointSet &points, // We will sink the pins into the model surface for a distance of 1/3 of // the pin radius for(int i = 0; i < headless_pts.rows(); i++) { tifcl(); - Vec3d sp = headless_pts.row(i); - - Vec3d n = headless_norm.row(i); - sp = sp - n * HWIDTH_MM; + Vec3d sph = headless_pts.row(i); // Exact support position + Vec3d n = headless_norm.row(i); // mesh outward normal + Vec3d sp = sph - n * HWIDTH_MM; // stick head start point Vec3d dir = {0, 0, -1}; - Vec3d sj = sp + R * n; + Vec3d sj = sp + R * n; // stick start point + + // This is only for checking + double idist = bridge_mesh_intersect(sph, dir, R, emesh, true); double dist = ray_mesh_intersect(sj, dir, emesh); - if(std::isinf(dist) || std::isnan(dist) || dist < 2*R) continue; + if(std::isinf(idist) || std::isnan(idist) || idist < 2*R || + std::isinf(dist) || std::isnan(dist) || dist < 2*R) { + BOOST_LOG_TRIVIAL(warning) << "Can not find route for headless" + << " support stick at: " + << sj.transpose(); + continue; + } Vec3d ej = sj + (dist + HWIDTH_MM)* dir; result.add_compact_bridge(sp, ej, n, R); } }; - using std::ref; - using std::cref; + // Now that the individual blocks are defined, lets connect the wires. We + // will create an array of functions which represents a program. Place the + // step methods in the array and bind the right arguments to the methods + // This way the data dependencies will be easily traceable between + // individual steps. + // There will be empty steps as well like the begin step or the done or + // abort steps. These are slots for future initialization or cleanup. + + using std::cref; // Bind inputs with cref (read-only) + using std::ref; // Bind outputs with ref (writable) using std::bind; // Here we can easily track what goes in and what comes out of each step: // (see the cref-s as inputs and ref-s as outputs) std::array, NUM_STEPS> program = { [] () { - // Begin - // clear up the shared data + // Begin... + // Potentially clear up the shared data (not needed for now) }, // Filtering unnecessary support points @@ -1674,6 +1900,7 @@ bool SLASupportTree::generate(const PointSet &points, Steps pc = BEGIN, pc_prev = BEGIN; + // Let's define a simple automaton that will run our program. auto progress = [&ctl, &pc, &pc_prev] () { static const std::array stepstr { "Starting", @@ -1795,7 +2022,7 @@ SLASupportTree::SLASupportTree(const PointSet &points, const Controller &ctl): m_impl(new Impl(ctl)) { - m_impl->ground_level = emesh.ground_level - cfg.object_elevation_mm; + m_impl->ground_level = emesh.ground_level() - cfg.object_elevation_mm; generate(points, emesh, cfg, ctl); } diff --git a/src/libslic3r/SLA/SLASupportTree.hpp b/src/libslic3r/SLA/SLASupportTree.hpp index 5a86d4623c..8de8d2b337 100644 --- a/src/libslic3r/SLA/SLASupportTree.hpp +++ b/src/libslic3r/SLA/SLASupportTree.hpp @@ -78,7 +78,7 @@ struct SupportConfig { double object_elevation_mm = 10; // The max Z angle for a normal at which it will get completely ignored. - double normal_cutoff_angle = 110.0 * M_PI / 180.0; + double normal_cutoff_angle = 150.0 * M_PI / 180.0; }; @@ -104,18 +104,90 @@ struct Controller { /// An index-triangle structure for libIGL functions. Also serves as an /// alternative (raw) input format for the SLASupportTree -struct EigenMesh3D { - Eigen::MatrixXd V; - Eigen::MatrixXi F; - double ground_level = 0; +class EigenMesh3D { + class AABBImpl; + + Eigen::MatrixXd m_V; + Eigen::MatrixXi m_F; + double m_ground_level = 0; + + std::unique_ptr m_aabb; +public: + + EigenMesh3D(const TriangleMesh&); + EigenMesh3D(const EigenMesh3D& other); + EigenMesh3D& operator=(const EigenMesh3D&); + + ~EigenMesh3D(); + + inline double ground_level() const { return m_ground_level; } + + inline const Eigen::MatrixXd& V() const { return m_V; } + inline const Eigen::MatrixXi& F() const { return m_F; } + + // Result of a raycast + class hit_result { + double m_t = std::numeric_limits::infinity(); + int m_face_id = -1; + const EigenMesh3D& m_mesh; + Vec3d m_dir; + inline hit_result(const EigenMesh3D& em): m_mesh(em) {} + friend class EigenMesh3D; + public: + + inline double distance() const { return m_t; } + + inline int face() const { return m_face_id; } + + inline Vec3d normal() const { + if(m_face_id < 0) return {}; + auto trindex = m_mesh.m_F.row(m_face_id); + const Vec3d& p1 = m_mesh.V().row(trindex(0)); + const Vec3d& p2 = m_mesh.V().row(trindex(1)); + const Vec3d& p3 = m_mesh.V().row(trindex(2)); + Eigen::Vector3d U = p2 - p1; + Eigen::Vector3d V = p3 - p1; + return U.cross(V).normalized(); + } + + inline bool is_inside() { + return m_face_id >= 0 && normal().dot(m_dir) > 0; + } + }; + + // Casting a ray on the mesh, returns the distance where the hit occures. + hit_result query_ray_hit(const Vec3d &s, const Vec3d &dir) const; + + class si_result { + double m_value; + int m_fidx; + Vec3d m_p; + si_result(double val, int i, const Vec3d& c): + m_value(val), m_fidx(i), m_p(c) {} + friend class EigenMesh3D; + public: + + si_result() = delete; + + double value() const { return m_value; } + operator double() const { return m_value; } + const Vec3d& point_on_mesh() const { return m_p; } + int F_idx() const { return m_fidx; } + }; + + // The signed distance from a point to the mesh. Outputs the distance, + // the index of the triangle and the closest point in mesh coordinate space. + si_result signed_distance(const Vec3d& p) const; + + bool inside(const Vec3d& p) const; }; using PointSet = Eigen::MatrixXd; -EigenMesh3D to_eigenmesh(const TriangleMesh& m); +//EigenMesh3D to_eigenmesh(const TriangleMesh& m); // needed for find best rotation -EigenMesh3D to_eigenmesh(const ModelObject& model); +//EigenMesh3D to_eigenmesh(const ModelObject& model); // Simple conversion of 'vector of points' to an Eigen matrix PointSet to_point_set(const std::vector&); diff --git a/src/libslic3r/SLA/SLASupportTreeIGL.cpp b/src/libslic3r/SLA/SLASupportTreeIGL.cpp index 0cc9f14e0f..d3af1eac89 100644 --- a/src/libslic3r/SLA/SLASupportTreeIGL.cpp +++ b/src/libslic3r/SLA/SLASupportTreeIGL.cpp @@ -3,6 +3,9 @@ #include "SLA/SLABoilerPlate.hpp" #include "SLA/SLASpatIndex.hpp" +// Workaround: IGL signed_distance.h will define PI in the igl namespace. +#undef PI + // HEAVY headers... takes eternity to compile // for concave hull merging decisions @@ -12,6 +15,7 @@ #include #include #include +#include #include "SLASpatIndex.hpp" #include "ClipperUtils.hpp" @@ -19,6 +23,13 @@ namespace Slic3r { namespace sla { +// Bring back PI from the igl namespace +using igl::PI; + +/* ************************************************************************** + * SpatIndex implementation + * ************************************************************************** */ + class SpatIndex::Impl { public: using BoostIndex = boost::geometry::index::rtree< SpatElement, @@ -78,6 +89,101 @@ size_t SpatIndex::size() const return m_impl->m_store.size(); } +/* **************************************************************************** + * EigenMesh3D implementation + * ****************************************************************************/ + +class EigenMesh3D::AABBImpl: public igl::AABB { +public: + igl::WindingNumberAABB windtree; +}; + +EigenMesh3D::EigenMesh3D(const TriangleMesh& tmesh): m_aabb(new AABBImpl()) { + static const double dEPS = 1e-6; + + const stl_file& stl = tmesh.stl; + + auto&& bb = tmesh.bounding_box(); + m_ground_level += bb.min(Z); + + Eigen::MatrixXd V; + Eigen::MatrixXi F; + + V.resize(3*stl.stats.number_of_facets, 3); + F.resize(stl.stats.number_of_facets, 3); + for (unsigned int i = 0; i < stl.stats.number_of_facets; ++i) { + const stl_facet* facet = stl.facet_start+i; + V(3*i+0, 0) = double(facet->vertex[0](0)); + V(3*i+0, 1) = double(facet->vertex[0](1)); + V(3*i+0, 2) = double(facet->vertex[0](2)); + + V(3*i+1, 0) = double(facet->vertex[1](0)); + V(3*i+1, 1) = double(facet->vertex[1](1)); + V(3*i+1, 2) = double(facet->vertex[1](2)); + + V(3*i+2, 0) = double(facet->vertex[2](0)); + V(3*i+2, 1) = double(facet->vertex[2](1)); + V(3*i+2, 2) = double(facet->vertex[2](2)); + + F(i, 0) = int(3*i+0); + F(i, 1) = int(3*i+1); + F(i, 2) = int(3*i+2); + } + + // We will convert this to a proper 3d mesh with no duplicate points. + Eigen::VectorXi SVI, SVJ; + igl::remove_duplicate_vertices(V, F, dEPS, m_V, SVI, SVJ, m_F); + + // Build the AABB accelaration tree + m_aabb->init(m_V, m_F); + m_aabb->windtree.set_mesh(m_V, m_F); +} + +EigenMesh3D::~EigenMesh3D() {} + +EigenMesh3D::EigenMesh3D(const EigenMesh3D &other): + m_V(other.m_V), m_F(other.m_F), m_ground_level(other.m_ground_level), + m_aabb( new AABBImpl(*other.m_aabb) ) {} + +EigenMesh3D &EigenMesh3D::operator=(const EigenMesh3D &other) +{ + m_V = other.m_V; + m_F = other.m_F; + m_ground_level = other.m_ground_level; + m_aabb.reset(new AABBImpl(*other.m_aabb)); return *this; +} + +EigenMesh3D::hit_result +EigenMesh3D::query_ray_hit(const Vec3d &s, const Vec3d &dir) const +{ + igl::Hit hit; + hit.t = std::numeric_limits::infinity(); + m_aabb->intersect_ray(m_V, m_F, s, dir, hit); + + hit_result ret(*this); + ret.m_t = double(hit.t); + ret.m_dir = dir; + if(!std::isinf(hit.t) && !std::isnan(hit.t)) ret.m_face_id = hit.id; + + return ret; +} + +EigenMesh3D::si_result EigenMesh3D::signed_distance(const Vec3d &p) const { + double sign = 0; double sqdst = 0; int i = 0; Vec3d c; + igl::signed_distance_winding_number(*m_aabb, m_V, m_F, m_aabb->windtree, + p, sign, sqdst, i, c); + + return si_result(sign * std::sqrt(sqdst), i, c); +} + +bool EigenMesh3D::inside(const Vec3d &p) const { + return m_aabb->windtree.inside(p); +} + +/* **************************************************************************** + * Misc functions + * ****************************************************************************/ + bool point_on_edge(const Vec3d& p, const Vec3d& e1, const Vec3d& e2, double eps = 0.05) { @@ -93,35 +199,27 @@ template double distance(const Vec& pp1, const Vec& pp2) { return std::sqrt(p.transpose() * p); } -PointSet normals(const PointSet& points, const EigenMesh3D& emesh, +PointSet normals(const PointSet& points, const EigenMesh3D& mesh, double eps, std::function throw_on_cancel) { - if(points.rows() == 0 || emesh.V.rows() == 0 || emesh.F.rows() == 0) + if(points.rows() == 0 || mesh.V().rows() == 0 || mesh.F().rows() == 0) return {}; Eigen::VectorXd dists; Eigen::VectorXi I; PointSet C; - // We need to remove duplicate vertices and have a true index triangle - // structure - EigenMesh3D mesh; - Eigen::VectorXi SVI, SVJ; - static const double dEPS = 1e-6; - igl::remove_duplicate_vertices(emesh.V, emesh.F, dEPS, - mesh.V, SVI, SVJ, mesh.F); - - igl::point_mesh_squared_distance( points, mesh.V, mesh.F, dists, I, C); + igl::point_mesh_squared_distance( points, mesh.V(), mesh.F(), dists, I, C); PointSet ret(I.rows(), 3); for(int i = 0; i < I.rows(); i++) { throw_on_cancel(); auto idx = I(i); - auto trindex = mesh.F.row(idx); + auto trindex = mesh.F().row(idx); - const Vec3d& p1 = mesh.V.row(trindex(0)); - const Vec3d& p2 = mesh.V.row(trindex(1)); - const Vec3d& p3 = mesh.V.row(trindex(2)); + const Vec3d& p1 = mesh.V().row(trindex(0)); + const Vec3d& p2 = mesh.V().row(trindex(1)); + const Vec3d& p3 = mesh.V().row(trindex(2)); // We should check if the point lies on an edge of the hosting triangle. // If it does than all the other triangles using the same two points @@ -159,18 +257,18 @@ PointSet normals(const PointSet& points, const EigenMesh3D& emesh, // vector for the neigboring triangles including the detected one. std::vector neigh; if(ic >= 0) { // The point is right on a vertex of the triangle - for(int n = 0; n < mesh.F.rows(); ++n) { + for(int n = 0; n < mesh.F().rows(); ++n) { throw_on_cancel(); - Vec3i ni = mesh.F.row(n); + Vec3i ni = mesh.F().row(n); if((ni(X) == ic || ni(Y) == ic || ni(Z) == ic)) neigh.emplace_back(ni); } } else if(ia >= 0 && ib >= 0) { // the point is on and edge // now get all the neigboring triangles - for(int n = 0; n < mesh.F.rows(); ++n) { + for(int n = 0; n < mesh.F().rows(); ++n) { throw_on_cancel(); - Vec3i ni = mesh.F.row(n); + Vec3i ni = mesh.F().row(n); if((ni(X) == ia || ni(Y) == ia || ni(Z) == ia) && (ni(X) == ib || ni(Y) == ib || ni(Z) == ib)) neigh.emplace_back(ni); @@ -180,9 +278,9 @@ PointSet normals(const PointSet& points, const EigenMesh3D& emesh, // Calculate the normals for the neighboring triangles std::vector neighnorms; neighnorms.reserve(neigh.size()); for(const Vec3i& tri : neigh) { - const Vec3d& pt1 = mesh.V.row(tri(0)); - const Vec3d& pt2 = mesh.V.row(tri(1)); - const Vec3d& pt3 = mesh.V.row(tri(2)); + const Vec3d& pt1 = mesh.V().row(tri(0)); + const Vec3d& pt2 = mesh.V().row(tri(1)); + const Vec3d& pt3 = mesh.V().row(tri(2)); Eigen::Vector3d U = pt2 - pt1; Eigen::Vector3d V = pt3 - pt1; neighnorms.emplace_back(U.cross(V).normalized()); @@ -222,16 +320,6 @@ PointSet normals(const PointSet& points, const EigenMesh3D& emesh, return ret; } -double ray_mesh_intersect(const Vec3d& s, - const Vec3d& dir, - const EigenMesh3D& m) -{ - igl::Hit hit; - hit.t = std::numeric_limits::infinity(); - igl::ray_mesh_intersect(s, dir, m.V, m.F, hit); - return double(hit.t); -} - // Clustering a set of points by the given criteria ClusteredPoints cluster( const sla::PointSet& points, @@ -309,53 +397,5 @@ ClusteredPoints cluster( return result; } -using Segments = std::vector>; - -Segments model_boundary(const EigenMesh3D& emesh, double offs) -{ - Segments ret; - Polygons pp; - pp.reserve(size_t(emesh.F.rows())); - - for (int i = 0; i < emesh.F.rows(); i++) { - auto trindex = emesh.F.row(i); - auto& p1 = emesh.V.row(trindex(0)); - auto& p2 = emesh.V.row(trindex(1)); - auto& p3 = emesh.V.row(trindex(2)); - - Polygon p; - p.points.resize(3); - p.points[0] = Point::new_scale(p1(X), p1(Y)); - p.points[1] = Point::new_scale(p2(X), p2(Y)); - p.points[2] = Point::new_scale(p3(X), p3(Y)); - p.make_counter_clockwise(); - pp.emplace_back(p); - } - - ExPolygons merged = union_ex(Slic3r::offset(pp, float(scale_(offs))), true); - - for(auto& expoly : merged) { - auto lines = expoly.lines(); - for(Line& l : lines) { - Vec2d a(l.a(X) * SCALING_FACTOR, l.a(Y) * SCALING_FACTOR); - Vec2d b(l.b(X) * SCALING_FACTOR, l.b(Y) * SCALING_FACTOR); - ret.emplace_back(std::make_pair(a, b)); - } - } - - return ret; -} - -//struct SegmentIndex { - -//}; - -//using SegmentIndexEl = std::pair; - -//SegmentIndexEl - - - - } } diff --git a/src/libslic3r/SLAPrint.cpp b/src/libslic3r/SLAPrint.cpp index bd0a573521..3cc6895589 100644 --- a/src/libslic3r/SLAPrint.cpp +++ b/src/libslic3r/SLAPrint.cpp @@ -29,6 +29,8 @@ public: SupportTreePtr support_tree_ptr; // the supports SlicedSupports support_slices; // sliced supports std::vector level_ids; + + inline SupportData(const TriangleMesh& trmesh): emesh(trmesh) {} }; namespace { @@ -385,8 +387,6 @@ SLAPrint::ApplyStatus SLAPrint::apply(const Model &model, const DynamicPrintConf update_apply_status(false); } - this->update_object_placeholders(); - #ifdef _DEBUG check_model_ids_equal(m_model, model); #endif /* _DEBUG */ @@ -503,8 +503,8 @@ void SLAPrint::process() // support points. Then we sprinkle the rest of the mesh. auto support_points = [this, ilh](SLAPrintObject& po) { const ModelObject& mo = *po.m_model_object; - po.m_supportdata.reset(new SLAPrintObject::SupportData()); - po.m_supportdata->emesh = sla::to_eigenmesh(po.transformed_mesh()); + po.m_supportdata.reset( + new SLAPrintObject::SupportData(po.transformed_mesh()) ); // If supports are disabled, we can skip the model scan. if(!po.m_config.supports_enable.getBool()) return; diff --git a/src/libslic3r/Technologies.hpp b/src/libslic3r/Technologies.hpp index b6b69ae27f..8d788ed51e 100644 --- a/src/libslic3r/Technologies.hpp +++ b/src/libslic3r/Technologies.hpp @@ -12,6 +12,7 @@ // Renders a small sphere in the center of the bounding box of the current selection when no gizmo is active #define ENABLE_RENDER_SELECTION_CENTER 0 + //==================== // 1.42.0.alpha1 techs //==================== @@ -27,16 +28,14 @@ // Use wxDataViewRender instead of wxDataViewCustomRenderer #define ENABLE_NONCUSTOM_DATA_VIEW_RENDERING (0 && ENABLE_1_42_0_ALPHA1) + //==================== // 1.42.0.alpha2 techs //==================== #define ENABLE_1_42_0_ALPHA2 1 -// Improves navigation between sidebar fields -#define ENABLE_IMPROVED_SIDEBAR_OBJECTS_MANIPULATION (1 && ENABLE_1_42_0_ALPHA2) // Adds print bed models to 3D scene #define ENABLE_PRINT_BED_MODELS (1 && ENABLE_1_42_0_ALPHA2) -#endif // _technologies_h_ //==================== @@ -65,3 +64,5 @@ // Toolbar items hidden/shown in dependence of the user mode #define ENABLE_MODE_AWARE_TOOLBAR_ITEMS (1 && ENABLE_1_42_0_ALPHA5) + +#endif // _technologies_h_ diff --git a/src/libslic3r/Utils.hpp b/src/libslic3r/Utils.hpp index ed12d05594..046745e6f5 100644 --- a/src/libslic3r/Utils.hpp +++ b/src/libslic3r/Utils.hpp @@ -8,6 +8,8 @@ #include "libslic3r.h" +namespace boost { namespace filesystem { class directory_entry; }} + namespace Slic3r { extern void set_logging_level(unsigned int level); @@ -61,6 +63,12 @@ extern int rename_file(const std::string &from, const std::string &to); // Copy a file, adjust the access attributes, so that the target is writable. extern int copy_file(const std::string &from, const std::string &to); +// Ignore system and hidden files, which may be created by the DropBox synchronisation process. +// https://github.com/prusa3d/Slic3r/issues/1298 +extern bool is_plain_file(const boost::filesystem::directory_entry &path); +extern bool is_ini_file(const boost::filesystem::directory_entry &path); +extern bool is_idx_file(const boost::filesystem::directory_entry &path); + // File path / name / extension splitting utilities, working with UTF-8, // to be published to Perl. namespace PerlUtils { diff --git a/src/libslic3r/libslic3r.h b/src/libslic3r/libslic3r.h index 19c6d3065c..c7d9f78544 100644 --- a/src/libslic3r/libslic3r.h +++ b/src/libslic3r/libslic3r.h @@ -16,6 +16,7 @@ #include #include #include +#include #include "Technologies.hpp" @@ -164,6 +165,12 @@ static inline T lerp(const T& a, const T& b, Number t) return (Number(1) - t) * a + t * b; } +template +static inline bool is_approx(Number value, Number test_value) +{ + return std::fabs(double(value) - double(test_value)) < double(EPSILON); +}; + } // namespace Slic3r #endif diff --git a/src/libslic3r/utils.cpp b/src/libslic3r/utils.cpp index f48abfd89e..6727bb799a 100644 --- a/src/libslic3r/utils.cpp +++ b/src/libslic3r/utils.cpp @@ -30,7 +30,13 @@ #include -#include +#if defined(__linux) || defined(__GNUC__ ) +#include +#endif /* __linux */ + +#ifdef _MSC_VER + #define strcasecmp _stricmp +#endif namespace Slic3r { @@ -248,6 +254,30 @@ int copy_file(const std::string &from, const std::string &to) return 0; } +// Ignore system and hidden files, which may be created by the DropBox synchronisation process. +// https://github.com/prusa3d/Slic3r/issues/1298 +bool is_plain_file(const boost::filesystem::directory_entry &dir_entry) +{ + if (! boost::filesystem::is_regular_file(dir_entry.status())) + return false; +#ifdef _MSC_VER + DWORD attributes = GetFileAttributesW(boost::nowide::widen(dir_entry.path().string()).c_str()); + return (attributes & (FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_SYSTEM)) == 0; +#else + return true; +#endif +} + +bool is_ini_file(const boost::filesystem::directory_entry &dir_entry) +{ + return is_plain_file(dir_entry) && strcasecmp(dir_entry.path().extension().string().c_str(), ".ini") == 0; +} + +bool is_idx_file(const boost::filesystem::directory_entry &dir_entry) +{ + return is_plain_file(dir_entry) && strcasecmp(dir_entry.path().extension().string().c_str(), ".idx") == 0; +} + } // namespace Slic3r #ifdef WIN32 diff --git a/src/slic3r.cpp b/src/slic3r.cpp index 0b7dada70f..62b56f7ffa 100644 --- a/src/slic3r.cpp +++ b/src/slic3r.cpp @@ -252,10 +252,6 @@ int main(int argc, char **argv) model.arrange_objects(fff_print.config().min_object_distance()); model.center_instances_around_point(cli_config.print_center); } - if (outfile.empty()) { - outfile = model.propose_export_file_name(); - outfile += (printer_technology == ptFFF) ? ".gcode" : ".zip"; - } if (printer_technology == ptFFF) { for (auto* mo : model.objects) fff_print.auto_assign_extruders(mo); @@ -265,6 +261,7 @@ int main(int argc, char **argv) std::string err = print->validate(); if (err.empty()) { if (printer_technology == ptFFF) { + // The outfile is processed by a PlaceholderParser. fff_print.export_gcode(outfile, nullptr); } else { assert(printer_technology == ptSLA); diff --git a/src/slic3r/Config/Snapshot.cpp b/src/slic3r/Config/Snapshot.cpp index 35bfde0b6b..b208554b50 100644 --- a/src/slic3r/Config/Snapshot.cpp +++ b/src/slic3r/Config/Snapshot.cpp @@ -249,10 +249,10 @@ bool Snapshot::equal_to_active(const AppConfig &app_config) const boost::filesystem::path path2 = snapshot_dir / subdir; std::vector files1, files2; for (auto &dir_entry : boost::filesystem::directory_iterator(path1)) - if (boost::filesystem::is_regular_file(dir_entry.status()) && boost::algorithm::iends_with(dir_entry.path().filename().string(), ".ini")) + if (Slic3r::is_ini_file(dir_entry)) files1.emplace_back(dir_entry.path().filename().string()); for (auto &dir_entry : boost::filesystem::directory_iterator(path2)) - if (boost::filesystem::is_regular_file(dir_entry.status()) && boost::algorithm::iends_with(dir_entry.path().filename().string(), ".ini")) + if (Slic3r::is_ini_file(dir_entry)) files2.emplace_back(dir_entry.path().filename().string()); std::sort(files1.begin(), files1.end()); std::sort(files2.begin(), files2.end()); @@ -343,7 +343,7 @@ static void copy_config_dir_single_level(const boost::filesystem::path &path_src throw std::runtime_error(std::string("Slic3r was unable to create a directory at ") + path_dst.string()); for (auto &dir_entry : boost::filesystem::directory_iterator(path_src)) - if (boost::filesystem::is_regular_file(dir_entry.status()) && boost::algorithm::iends_with(dir_entry.path().filename().string(), ".ini")) + if (Slic3r::is_ini_file(dir_entry)) boost::filesystem::copy_file(dir_entry.path(), path_dst / dir_entry.path().filename(), boost::filesystem::copy_option::overwrite_if_exists); } diff --git a/src/slic3r/Config/Version.cpp b/src/slic3r/Config/Version.cpp index 48ace7b609..70b12f23b6 100644 --- a/src/slic3r/Config/Version.cpp +++ b/src/slic3r/Config/Version.cpp @@ -297,7 +297,7 @@ std::vector Index::load_db() std::vector index_db; std::string errors_cummulative; for (auto &dir_entry : boost::filesystem::directory_iterator(cache_dir)) - if (boost::filesystem::is_regular_file(dir_entry.status()) && boost::algorithm::iends_with(dir_entry.path().filename().string(), ".idx")) { + if (Slic3r::is_idx_file(dir_entry)) { Index idx; try { idx.load(dir_entry.path()); diff --git a/src/slic3r/GUI/3DScene.cpp b/src/slic3r/GUI/3DScene.cpp index fd1645c9e2..6b1df6ab36 100644 --- a/src/slic3r/GUI/3DScene.cpp +++ b/src/slic3r/GUI/3DScene.cpp @@ -252,6 +252,7 @@ GLVolume::GLVolume(float r, float g, float b, float a) , is_modifier(false) , is_wipe_tower(false) , is_extrusion_path(false) + , force_transparent(false) , tverts_range(0, size_t(-1)) , qverts_range(0, size_t(-1)) { @@ -293,6 +294,9 @@ void GLVolume::set_render_color() set_render_color(OUTSIDE_COLOR, 4); else set_render_color(color, 4); + + if (force_transparent) + render_color[3] = color[3]; } void GLVolume::set_color_from_model_volume(const ModelVolume *model_volume) diff --git a/src/slic3r/GUI/3DScene.hpp b/src/slic3r/GUI/3DScene.hpp index 7430ff4aa0..2732b5a137 100644 --- a/src/slic3r/GUI/3DScene.hpp +++ b/src/slic3r/GUI/3DScene.hpp @@ -295,6 +295,8 @@ public: bool is_wipe_tower; // Wheter or not this volume has been generated from an extrusion path bool is_extrusion_path; + // Wheter or not to always render this volume using its own alpha + bool force_transparent; // Interleaved triangles & normals with indexed triangles & quads. GLIndexedVertexArray indexed_vertex_array; diff --git a/src/slic3r/GUI/ConfigWizard.cpp b/src/slic3r/GUI/ConfigWizard.cpp index 6cf45166db..3471395c29 100644 --- a/src/slic3r/GUI/ConfigWizard.cpp +++ b/src/slic3r/GUI/ConfigWizard.cpp @@ -635,35 +635,32 @@ void ConfigWizard::priv::load_vendors() const auto rsrc_vendor_dir = fs::path(resources_dir()) / "profiles"; // Load vendors from the "vendors" directory in datadir - for (fs::directory_iterator it(vendor_dir); it != fs::directory_iterator(); ++it) { - if (it->path().extension() == ".ini") { + for (auto &dir_entry : boost::filesystem::directory_iterator(vendor_dir)) + if (Slic3r::is_ini_file(dir_entry)) { try { - auto vp = VendorProfile::from_ini(it->path()); + auto vp = VendorProfile::from_ini(dir_entry.path()); vendors[vp.id] = std::move(vp); } catch (const std::exception& e) { - BOOST_LOG_TRIVIAL(error) << boost::format("Error loading vendor bundle %1%: %2%") % it->path() % e.what(); + BOOST_LOG_TRIVIAL(error) << boost::format("Error loading vendor bundle %1%: %2%") % dir_entry.path() % e.what(); } - } - } // Additionally load up vendors from the application resources directory, but only those not seen in the datadir - for (fs::directory_iterator it(rsrc_vendor_dir); it != fs::directory_iterator(); ++it) { - if (it->path().extension() == ".ini") { - const auto id = it->path().stem().string(); + for (auto &dir_entry : boost::filesystem::directory_iterator(rsrc_vendor_dir)) + if (Slic3r::is_ini_file(dir_entry)) { + const auto id = dir_entry.path().stem().string(); if (vendors.find(id) == vendors.end()) { try { - auto vp = VendorProfile::from_ini(it->path()); - vendors_rsrc[vp.id] = it->path().filename().string(); + auto vp = VendorProfile::from_ini(dir_entry.path()); + vendors_rsrc[vp.id] = dir_entry.path().filename().string(); vendors[vp.id] = std::move(vp); } catch (const std::exception& e) { - BOOST_LOG_TRIVIAL(error) << boost::format("Error loading vendor bundle %1%: %2%") % it->path() % e.what(); + BOOST_LOG_TRIVIAL(error) << boost::format("Error loading vendor bundle %1%: %2%") % dir_entry.path() % e.what(); } } } - } // Load up the set of vendors / models / variants the user has had enabled up till now const AppConfig *app_config = GUI::get_app_config(); @@ -672,14 +669,15 @@ void ConfigWizard::priv::load_vendors() } else { // In case of legacy datadir, try to guess the preference based on the printer preset files that are present const auto printer_dir = fs::path(Slic3r::data_dir()) / "printer"; - for (fs::directory_iterator it(printer_dir); it != fs::directory_iterator(); ++it) { - auto needle = legacy_preset_map.find(it->path().filename().string()); - if (needle == legacy_preset_map.end()) { continue; } + for (auto &dir_entry : boost::filesystem::directory_iterator(printer_dir)) + if (Slic3r::is_ini_file(dir_entry)) { + auto needle = legacy_preset_map.find(dir_entry.path().filename().string()); + if (needle == legacy_preset_map.end()) { continue; } - const auto &model = needle->second.first; - const auto &variant = needle->second.second; - appconfig_vendors.set_variant("PrusaResearch", model, variant, true); - } + const auto &model = needle->second.first; + const auto &variant = needle->second.second; + appconfig_vendors.set_variant("PrusaResearch", model, variant, true); + } } } diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 72dfe52bdd..12ff00dce3 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -1617,6 +1617,9 @@ void GLCanvas3D::Selection::clear() _update_type(); m_bounding_box_dirty = true; + + // resets the cache in the sidebar + wxGetApp().obj_manipul()->reset_cache(); } // Update the selection based on the map from old indices to new indices after m_volumes changed. @@ -1817,7 +1820,7 @@ void GLCanvas3D::Selection::rotate(const Vec3d& rotation, bool local) if (rot_axis_max != 2 && first_volume_idx != -1) { // Generic rotation, but no rotation around the Z axis. // Always do a local rotation (do not consider the selection to be a rigid body). - assert(rotation.z() == 0); + assert(is_approx(rotation.z(), 0.0)); const GLVolume &first_volume = *(*m_volumes)[first_volume_idx]; const Vec3d &rotation = first_volume.get_instance_rotation(); double z_diff = rotation_diff_z(m_cache.volumes_data[first_volume_idx].get_instance_rotation(), m_cache.volumes_data[i].get_instance_rotation()); @@ -1842,7 +1845,7 @@ void GLCanvas3D::Selection::rotate(const Vec3d& rotation, bool local) else if (is_single_volume() || is_single_modifier()) { if (local) - volume.set_volume_rotation(rotation); + volume.set_volume_rotation(volume.get_volume_rotation() + rotation); else { Transform3d m = Geometry::assemble_transform(Vec3d::Zero(), rotation); @@ -2259,7 +2262,7 @@ void GLCanvas3D::Selection::render_sidebar_hints(const std::string& sidebar_fiel } else if (is_single_volume() || is_single_modifier()) { - Transform3d orient_matrix = (*m_volumes)[*m_list.begin()]->get_instance_transformation().get_matrix(true, false, true, true); + Transform3d orient_matrix = (*m_volumes)[*m_list.begin()]->get_instance_transformation().get_matrix(true, false, true, true) * (*m_volumes)[*m_list.begin()]->get_volume_transformation().get_matrix(true, false, true, true); ::glTranslated(center(0), center(1), center(2)); ::glMultMatrixd(orient_matrix.data()); } @@ -3992,13 +3995,12 @@ wxDEFINE_EVENT(EVT_GLCANVAS_VIEWPORT_CHANGED, SimpleEvent); wxDEFINE_EVENT(EVT_GLCANVAS_RIGHT_CLICK, Vec2dEvent); wxDEFINE_EVENT(EVT_GLCANVAS_REMOVE_OBJECT, SimpleEvent); wxDEFINE_EVENT(EVT_GLCANVAS_ARRANGE, SimpleEvent); +wxDEFINE_EVENT(EVT_GLCANVAS_SELECT_ALL, SimpleEvent); wxDEFINE_EVENT(EVT_GLCANVAS_QUESTION_MARK, SimpleEvent); wxDEFINE_EVENT(EVT_GLCANVAS_INCREASE_INSTANCES, Event); wxDEFINE_EVENT(EVT_GLCANVAS_INSTANCE_MOVED, SimpleEvent); -#if ENABLE_IMPROVED_SIDEBAR_OBJECTS_MANIPULATION wxDEFINE_EVENT(EVT_GLCANVAS_INSTANCE_ROTATED, SimpleEvent); wxDEFINE_EVENT(EVT_GLCANVAS_INSTANCE_SCALED, SimpleEvent); -#endif // ENABLE_IMPROVED_SIDEBAR_OBJECTS_MANIPULATION wxDEFINE_EVENT(EVT_GLCANVAS_WIPETOWER_MOVED, Vec3dEvent); wxDEFINE_EVENT(EVT_GLCANVAS_ENABLE_ACTION_BUTTONS, Event); wxDEFINE_EVENT(EVT_GLCANVAS_UPDATE_GEOMETRY, Vec3dsEvent<2>); @@ -4286,6 +4288,13 @@ bool GLCanvas3D::is_reload_delayed() const void GLCanvas3D::enable_layers_editing(bool enable) { m_layers_editing.set_enabled(enable); + const Selection::IndicesList& idxs = m_selection.get_volume_idxs(); + for (unsigned int idx : idxs) + { + GLVolume* v = m_volumes.volumes[idx]; + if (v->is_modifier) + v->force_transparent = enable; + } } void GLCanvas3D::enable_warning_texture(bool enable) @@ -4420,6 +4429,7 @@ void GLCanvas3D::update_toolbar_items_visibility() ConfigOptionMode mode = wxGetApp().get_mode(); m_toolbar.set_item_visible("more", mode != comSimple); m_toolbar.set_item_visible("fewer", mode != comSimple); + m_toolbar.set_item_visible("splitvolumes", mode != comSimple); m_dirty = true; } #endif // ENABLE_MODE_AWARE_TOOLBAR_ITEMS @@ -5046,7 +5056,6 @@ void GLCanvas3D::bind_event_handlers() m_canvas->Bind(wxEVT_MIDDLE_DCLICK, &GLCanvas3D::on_mouse, this); m_canvas->Bind(wxEVT_RIGHT_DCLICK, &GLCanvas3D::on_mouse, this); m_canvas->Bind(wxEVT_PAINT, &GLCanvas3D::on_paint, this); - m_canvas->Bind(wxEVT_KEY_DOWN, &GLCanvas3D::on_key_down, this); } } @@ -5060,7 +5069,7 @@ void GLCanvas3D::unbind_event_handlers() m_canvas->Unbind(wxEVT_MOUSEWHEEL, &GLCanvas3D::on_mouse_wheel, this); m_canvas->Unbind(wxEVT_TIMER, &GLCanvas3D::on_timer, this); m_canvas->Unbind(wxEVT_LEFT_DOWN, &GLCanvas3D::on_mouse, this); - m_canvas->Unbind(wxEVT_LEFT_UP, &GLCanvas3D::on_mouse, this); + m_canvas->Unbind(wxEVT_LEFT_UP, &GLCanvas3D::on_mouse, this); m_canvas->Unbind(wxEVT_MIDDLE_DOWN, &GLCanvas3D::on_mouse, this); m_canvas->Unbind(wxEVT_MIDDLE_UP, &GLCanvas3D::on_mouse, this); m_canvas->Unbind(wxEVT_RIGHT_DOWN, &GLCanvas3D::on_mouse, this); @@ -5072,7 +5081,6 @@ void GLCanvas3D::unbind_event_handlers() m_canvas->Unbind(wxEVT_MIDDLE_DCLICK, &GLCanvas3D::on_mouse, this); m_canvas->Unbind(wxEVT_RIGHT_DCLICK, &GLCanvas3D::on_mouse, this); m_canvas->Unbind(wxEVT_PAINT, &GLCanvas3D::on_paint, this); - m_canvas->Unbind(wxEVT_KEY_DOWN, &GLCanvas3D::on_key_down, this); } } @@ -5091,71 +5099,67 @@ void GLCanvas3D::on_idle(wxIdleEvent& evt) void GLCanvas3D::on_char(wxKeyEvent& evt) { - if (evt.HasModifiers()) + // see include/wx/defs.h enum wxKeyCode + int keyCode = evt.GetKeyCode(); + int ctrlMask = wxMOD_CONTROL; +//#ifdef __APPLE__ +// ctrlMask |= wxMOD_RAW_CONTROL; +//#endif /* __APPLE__ */ + if ((evt.GetModifiers() & ctrlMask) != 0) { + switch (keyCode) { +#ifndef __APPLE__ + // Even though Control+A is captured by the accelerator on OSX/wxWidgets in Slic3r, it works in text edit lines. + case WXK_CONTROL_A: post_event(SimpleEvent(EVT_GLCANVAS_SELECT_ALL)); break; +#endif /* __APPLE__ */ +#ifdef __APPLE__ + case WXK_BACK: // the low cost Apple solutions are not equipped with a Delete key, use Backspace instead. +#endif /* __APPLE__ */ + case WXK_DELETE: post_event(SimpleEvent(EVT_GLTOOLBAR_DELETE_ALL)); break; + default: evt.Skip(); + } + } else if (evt.HasModifiers()) { evt.Skip(); - else - { - int keyCode = evt.GetKeyCode(); - switch (keyCode - 48) + } else { + switch (keyCode) { - // numerical input - case 0: { select_view("iso"); break; } - case 1: { select_view("top"); break; } - case 2: { select_view("bottom"); break; } - case 3: { select_view("front"); break; } - case 4: { select_view("rear"); break; } - case 5: { select_view("left"); break; } - case 6: { select_view("right"); break; } + // key ESC + case WXK_ESCAPE: { m_gizmos.reset_all_states(); m_dirty = true; break; } +#ifdef __APPLE__ + case WXK_BACK: // the low cost Apple solutions are not equipped with a Delete key, use Backspace instead. +#endif /* __APPLE__ */ + case WXK_DELETE: post_event(SimpleEvent(EVT_GLTOOLBAR_DELETE)); break; + case '0': { select_view("iso"); break; } + case '1': { select_view("top"); break; } + case '2': { select_view("bottom"); break; } + case '3': { select_view("front"); break; } + case '4': { select_view("rear"); break; } + case '5': { select_view("left"); break; } + case '6': { select_view("right"); break; } + case '+': { post_event(Event(EVT_GLCANVAS_INCREASE_INSTANCES, +1)); break; } + case '-': { post_event(Event(EVT_GLCANVAS_INCREASE_INSTANCES, -1)); break; } + case '?': { post_event(SimpleEvent(EVT_GLCANVAS_QUESTION_MARK)); break; } + case 'A': + case 'a': { post_event(SimpleEvent(EVT_GLCANVAS_ARRANGE)); break; } + case 'B': + case 'b': { zoom_to_bed(); break; } + case 'I': + case 'i': { set_camera_zoom(1.0f); break; } + case 'O': + case 'o': { set_camera_zoom(-1.0f); break; } + case 'Z': + case 'z': { m_selection.is_empty() ? zoom_to_volumes() : zoom_to_selection(); break; } default: + { + if (m_gizmos.handle_shortcut(keyCode, m_selection)) { - // text input - switch (keyCode) - { - // key ESC - case 27: { m_gizmos.reset_all_states(); m_dirty = true; break; } - // key + - case 43: { post_event(Event(EVT_GLCANVAS_INCREASE_INSTANCES, +1)); break; } - // key - - case 45: { post_event(Event(EVT_GLCANVAS_INCREASE_INSTANCES, -1)); break; } - // key ? - case 63: { post_event(SimpleEvent(EVT_GLCANVAS_QUESTION_MARK)); break; } - // key A/a - case 65: - case 97: { post_event(SimpleEvent(EVT_GLCANVAS_ARRANGE)); break; } - // key B/b - case 66: - case 98: { zoom_to_bed(); break; } - // key I/i - case 73: - case 105: { set_camera_zoom(1.0f); break; } - // key O/o - case 79: - case 111: { set_camera_zoom(-1.0f); break; } - // key Z/z - case 90: - case 122: - { - if (m_selection.is_empty()) - zoom_to_volumes(); - else - zoom_to_selection(); - - break; - } - default: - { - if (m_gizmos.handle_shortcut(keyCode, m_selection)) - { - _update_gizmos_data(); - m_dirty = true; - } - else - evt.Skip(); - - break; - } - } + _update_gizmos_data(); + m_dirty = true; } + else + evt.Skip(); + + break; + } } } } @@ -5354,9 +5358,7 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) bool already_selected = m_selection.contains_volume(m_hover_volume_id); bool shift_down = evt.ShiftDown(); -#if ENABLE_IMPROVED_SIDEBAR_OBJECTS_MANIPULATION Selection::IndicesList curr_idxs = m_selection.get_volume_idxs(); -#endif // ENABLE_IMPROVED_SIDEBAR_OBJECTS_MANIPULATION if (already_selected && shift_down) m_selection.remove(m_hover_volume_id); @@ -5373,21 +5375,14 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) #endif // ENABLE_MOVE_MIN_THRESHOLD } -#if ENABLE_IMPROVED_SIDEBAR_OBJECTS_MANIPULATION if (curr_idxs != m_selection.get_volume_idxs()) { -#endif // ENABLE_IMPROVED_SIDEBAR_OBJECTS_MANIPULATION m_gizmos.update_on_off_state(m_selection); _update_gizmos_data(); -#if !ENABLE_IMPROVED_SIDEBAR_OBJECTS_MANIPULATION - wxGetApp().obj_manipul()->update_settings_value(m_selection); -#endif // !ENABLE_IMPROVED_SIDEBAR_OBJECTS_MANIPULATION post_event(SimpleEvent(EVT_GLCANVAS_OBJECT_SELECT)); m_dirty = true; -#if ENABLE_IMPROVED_SIDEBAR_OBJECTS_MANIPULATION } -#endif // ENABLE_IMPROVED_SIDEBAR_OBJECTS_MANIPULATION } } @@ -5679,24 +5674,6 @@ void GLCanvas3D::on_paint(wxPaintEvent& evt) this->render(); } -void GLCanvas3D::on_key_down(wxKeyEvent& evt) -{ - if (evt.HasModifiers()) - evt.Skip(); - else - { - int key = evt.GetKeyCode(); -#ifdef __WXOSX__ - if (key == WXK_BACK) -#else - if (key == WXK_DELETE) -#endif // __WXOSX__ - post_event(SimpleEvent(EVT_GLCANVAS_REMOVE_OBJECT)); - else - evt.Skip(); - } -} - Size GLCanvas3D::get_canvas_size() const { int w = 0; @@ -5782,7 +5759,6 @@ void GLCanvas3D::do_move() ModelObject* model_object = m_model->objects[object_idx]; if (model_object != nullptr) { -#if ENABLE_IMPROVED_SIDEBAR_OBJECTS_MANIPULATION if (selection_mode == Selection::Instance) model_object->instances[instance_idx]->set_offset(v->get_instance_offset()); else if (selection_mode == Selection::Volume) @@ -5790,20 +5766,6 @@ void GLCanvas3D::do_move() object_moved = true; model_object->invalidate_bounding_box(); -#else - if (selection_mode == Selection::Instance) - { - model_object->instances[instance_idx]->set_offset(v->get_instance_offset()); - object_moved = true; - } - else if (selection_mode == Selection::Volume) - { - model_object->volumes[volume_idx]->set_offset(v->get_volume_offset()); - object_moved = true; - } - if (object_moved) - model_object->invalidate_bounding_box(); -#endif // ENABLE_IMPROVED_SIDEBAR_OBJECTS_MANIPULATION } } else if (object_idx == 1000) @@ -5874,12 +5836,8 @@ void GLCanvas3D::do_rotate() m->translate_instance(i.second, shift); } -#if ENABLE_IMPROVED_SIDEBAR_OBJECTS_MANIPULATION if (!done.empty()) post_event(SimpleEvent(EVT_GLCANVAS_INSTANCE_ROTATED)); -#else - post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS)); -#endif // ENABLE_IMPROVED_SIDEBAR_OBJECTS_MANIPULATION } void GLCanvas3D::do_scale() @@ -5930,12 +5888,8 @@ void GLCanvas3D::do_scale() m->translate_instance(i.second, shift); } -#if ENABLE_IMPROVED_SIDEBAR_OBJECTS_MANIPULATION if (!done.empty()) post_event(SimpleEvent(EVT_GLCANVAS_INSTANCE_ROTATED)); -#else - post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS)); -#endif // ENABLE_IMPROVED_SIDEBAR_OBJECTS_MANIPULATION } void GLCanvas3D::do_flatten() @@ -6097,7 +6051,7 @@ bool GLCanvas3D::_init_toolbar() GLToolbarItem::Data item; item.name = "add"; - item.tooltip = GUI::L_str("Add... [Ctrl+I]"); + item.tooltip = GUI::L_str("Add...") + " [" + GUI::shortkey_ctrl_prefix() + "I]"; item.sprite_id = 0; item.is_toggable = false; item.action_event = EVT_GLTOOLBAR_ADD; @@ -6105,7 +6059,7 @@ bool GLCanvas3D::_init_toolbar() return false; item.name = "delete"; - item.tooltip = GUI::L_str("Delete [Del]"); + item.tooltip = GUI::L_str("Delete") + " [Del]"; item.sprite_id = 1; item.is_toggable = false; item.action_event = EVT_GLTOOLBAR_DELETE; @@ -6113,7 +6067,7 @@ bool GLCanvas3D::_init_toolbar() return false; item.name = "deleteall"; - item.tooltip = GUI::L_str("Delete all [Ctrl+Del]"); + item.tooltip = GUI::L_str("Delete all") + " [" + GUI::shortkey_ctrl_prefix() + "Del]"; item.sprite_id = 2; item.is_toggable = false; item.action_event = EVT_GLTOOLBAR_DELETE_ALL; diff --git a/src/slic3r/GUI/GLCanvas3D.hpp b/src/slic3r/GUI/GLCanvas3D.hpp index cbc5013eac..351a3d9497 100644 --- a/src/slic3r/GUI/GLCanvas3D.hpp +++ b/src/slic3r/GUI/GLCanvas3D.hpp @@ -121,14 +121,13 @@ wxDECLARE_EVENT(EVT_GLCANVAS_VIEWPORT_CHANGED, SimpleEvent); wxDECLARE_EVENT(EVT_GLCANVAS_RIGHT_CLICK, Vec2dEvent); wxDECLARE_EVENT(EVT_GLCANVAS_REMOVE_OBJECT, SimpleEvent); wxDECLARE_EVENT(EVT_GLCANVAS_ARRANGE, SimpleEvent); +wxDECLARE_EVENT(EVT_GLCANVAS_SELECT_ALL, SimpleEvent); wxDECLARE_EVENT(EVT_GLCANVAS_QUESTION_MARK, SimpleEvent); wxDECLARE_EVENT(EVT_GLCANVAS_INCREASE_INSTANCES, Event); // data: +1 => increase, -1 => decrease wxDECLARE_EVENT(EVT_GLCANVAS_INSTANCE_MOVED, SimpleEvent); wxDECLARE_EVENT(EVT_GLCANVAS_WIPETOWER_MOVED, Vec3dEvent); -#if ENABLE_IMPROVED_SIDEBAR_OBJECTS_MANIPULATION wxDECLARE_EVENT(EVT_GLCANVAS_INSTANCE_ROTATED, SimpleEvent); wxDECLARE_EVENT(EVT_GLCANVAS_INSTANCE_SCALED, SimpleEvent); -#endif // ENABLE_IMPROVED_SIDEBAR_OBJECTS_MANIPULATION wxDECLARE_EVENT(EVT_GLCANVAS_ENABLE_ACTION_BUTTONS, Event); wxDECLARE_EVENT(EVT_GLCANVAS_UPDATE_GEOMETRY, Vec3dsEvent<2>); wxDECLARE_EVENT(EVT_GLCANVAS_MOUSE_DRAGGING_FINISHED, SimpleEvent); @@ -1044,7 +1043,6 @@ public: void on_timer(wxTimerEvent& evt); void on_mouse(wxMouseEvent& evt); void on_paint(wxPaintEvent& evt); - void on_key_down(wxKeyEvent& evt); Size get_canvas_size() const; Point get_local_mouse_position() const; diff --git a/src/slic3r/GUI/GUI.cpp b/src/slic3r/GUI/GUI.cpp index 148285e86e..8e80d64c30 100644 --- a/src/slic3r/GUI/GUI.cpp +++ b/src/slic3r/GUI/GUI.cpp @@ -75,6 +75,30 @@ void break_to_debugger() #endif /* _WIN32 */ } +const std::string& shortkey_ctrl_prefix() +{ + static const std::string str = +#ifdef __APPLE__ + "⌘" +#else + "Ctrl+" +#endif + ; + return str; +} + +const std::string& shortkey_alt_prefix() +{ + static const std::string str = +#ifdef __APPLE__ + "⌥" +#else + "Alt+" +#endif + ; + return str; +} + bool config_wizard_startup(bool app_config_exists) { if (!app_config_exists || wxGetApp().preset_bundle->printers.size() <= 1) { diff --git a/src/slic3r/GUI/GUI.hpp b/src/slic3r/GUI/GUI.hpp index e33be8f58c..f066c82a80 100644 --- a/src/slic3r/GUI/GUI.hpp +++ b/src/slic3r/GUI/GUI.hpp @@ -27,7 +27,11 @@ void enable_screensaver(); bool debugged(); void break_to_debugger(); -AppConfig* get_app_config(); +// Platform specific Ctrl+/Alt+ (Windows, Linux) vs. ⌘/⌥ (OSX) prefixes +extern const std::string& shortkey_ctrl_prefix(); +extern const std::string& shortkey_alt_prefix(); + +extern AppConfig* get_app_config(); extern void add_menus(wxMenuBar *menu, int event_preferences_changed, int event_language_change); diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 33e6b839a7..95c1089a79 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -562,7 +562,13 @@ void GUI_App::add_config_menu(wxMenuBar *menu) local_menu->Append(config_id_base + ConfigMenuTakeSnapshot, _(L("Take Configuration &Snapshot")), _(L("Capture a configuration snapshot"))); // local_menu->Append(config_id_base + ConfigMenuUpdate, _(L("Check for updates")), _(L("Check for configuration updates"))); local_menu->AppendSeparator(); - local_menu->Append(config_id_base + ConfigMenuPreferences, _(L("&Preferences")) + dots + "\tCtrl+P", _(L("Application preferences"))); + local_menu->Append(config_id_base + ConfigMenuPreferences, _(L("&Preferences")) + dots + +#ifdef __APPLE__ + "\tCtrl+,", +#else + "\tCtrl+P", +#endif + _(L("Application preferences"))); local_menu->AppendSeparator(); auto mode_menu = new wxMenu(); mode_menu->AppendRadioItem(config_id_base + ConfigMenuModeSimple, _(L("Simple")), _(L("Simple View Mode"))); diff --git a/src/slic3r/GUI/GUI_ObjectList.cpp b/src/slic3r/GUI/GUI_ObjectList.cpp index 57c01a4471..9a789633fb 100644 --- a/src/slic3r/GUI/GUI_ObjectList.cpp +++ b/src/slic3r/GUI/GUI_ObjectList.cpp @@ -66,6 +66,12 @@ ObjectList::ObjectList(wxWindow* parent) : // describe control behavior Bind(wxEVT_DATAVIEW_SELECTION_CHANGED, [this](wxEvent& event) { +#ifndef __APPLE__ + // On Windows and Linux, forces a kill focus emulation on the object manipulator fields because this event handler is called + // before the kill focus event handler on the object manipulator when changing selection in the list, invalidating the object + // manipulator cache with the following call to selection_changed() + wxGetApp().obj_manipul()->emulate_kill_focus(); +#endif // __APPLE__ selection_changed(); #ifndef __WXMSW__ set_tooltip_for_item(get_mouse_position_in_control()); diff --git a/src/slic3r/GUI/GUI_ObjectManipulation.cpp b/src/slic3r/GUI/GUI_ObjectManipulation.cpp index 9a1b159a7c..50ef54dd5c 100644 --- a/src/slic3r/GUI/GUI_ObjectManipulation.cpp +++ b/src/slic3r/GUI/GUI_ObjectManipulation.cpp @@ -17,103 +17,24 @@ namespace GUI ObjectManipulation::ObjectManipulation(wxWindow* parent) : OG_Settings(parent, true) +#ifndef __APPLE__ + , m_focused_option("") +#endif // __APPLE__ { m_og->set_name(_(L("Object Manipulation"))); m_og->label_width = 9 * wxGetApp().em_unit();//125; m_og->set_grid_vgap(5); - m_og->m_on_change = [this](const std::string& opt_key, const boost::any& value) { - std::vector axes{ "_x", "_y", "_z" }; - - std::string param; - std::copy(opt_key.begin(), opt_key.end() - 2, std::back_inserter(param)); - - size_t i = 0; - Vec3d new_value; - for (auto axis : axes) - new_value(i++) = boost::any_cast(m_og->get_value(param+axis)); - - if (param == "position") - change_position_value(new_value); - else if (param == "rotation") - change_rotation_value(new_value); - else if (param == "scale") -#if ENABLE_IMPROVED_SIDEBAR_OBJECTS_MANIPULATION - change_scale_value(new_value); -#else - change_scale_value(new_value); -#endif // ENABLE_IMPROVED_SIDEBAR_OBJECTS_MANIPULATION - else if (param == "size") -#if ENABLE_IMPROVED_SIDEBAR_OBJECTS_MANIPULATION - change_size_value(new_value); -#else - change_size_value(new_value); -#endif // ENABLE_IMPROVED_SIDEBAR_OBJECTS_MANIPULATION - - wxGetApp().plater()->canvas3D()->handle_sidebar_focus_event(opt_key, false); - }; - - m_og->m_fill_empty_value = [this](const std::string& opt_key) - { -#if !ENABLE_IMPROVED_SIDEBAR_OBJECTS_MANIPULATION - this->update_if_dirty(); -#endif // !ENABLE_IMPROVED_SIDEBAR_OBJECTS_MANIPULATION - - std::string param; - std::copy(opt_key.begin(), opt_key.end() - 2, std::back_inserter(param)); - - double value = 0.0; - - if (param == "position") { - int axis = opt_key.back() == 'x' ? 0 : - opt_key.back() == 'y' ? 1 : 2; - -#if ENABLE_IMPROVED_SIDEBAR_OBJECTS_MANIPULATION - value = m_cache.position(axis); -#else - value = m_cache_position(axis); -#endif // ENABLE_IMPROVED_SIDEBAR_OBJECTS_MANIPULATION - } - else if (param == "rotation") { - int axis = opt_key.back() == 'x' ? 0 : - opt_key.back() == 'y' ? 1 : 2; - -#if ENABLE_IMPROVED_SIDEBAR_OBJECTS_MANIPULATION - value = m_cache.rotation(axis); -#else - value = m_cache_rotation(axis); -#endif // ENABLE_IMPROVED_SIDEBAR_OBJECTS_MANIPULATION - } - else if (param == "scale") { - int axis = opt_key.back() == 'x' ? 0 : - opt_key.back() == 'y' ? 1 : 2; - -#if ENABLE_IMPROVED_SIDEBAR_OBJECTS_MANIPULATION - value = m_cache.scale(axis); -#else - value = m_cache_scale(axis); -#endif // ENABLE_IMPROVED_SIDEBAR_OBJECTS_MANIPULATION - } - else if (param == "size") { - int axis = opt_key.back() == 'x' ? 0 : - opt_key.back() == 'y' ? 1 : 2; - -#if ENABLE_IMPROVED_SIDEBAR_OBJECTS_MANIPULATION - value = m_cache.size(axis); -#else - value = m_cache_size(axis); -#endif // ENABLE_IMPROVED_SIDEBAR_OBJECTS_MANIPULATION - } - - m_og->set_value(opt_key, double_to_string(value)); - wxGetApp().plater()->canvas3D()->handle_sidebar_focus_event(opt_key, false); - }; + m_og->m_on_change = std::bind(&ObjectManipulation::on_change, this, std::placeholders::_1, std::placeholders::_2); + m_og->m_fill_empty_value = std::bind(&ObjectManipulation::on_fill_empty_value, this, std::placeholders::_1); m_og->m_set_focus = [this](const std::string& opt_key) { -#if !ENABLE_IMPROVED_SIDEBAR_OBJECTS_MANIPULATION - this->update_if_dirty(); -#endif // !ENABLE_IMPROVED_SIDEBAR_OBJECTS_MANIPULATION +#ifndef __APPLE__ + m_focused_option = opt_key; +#endif // __APPLE__ + + // needed to show the visual hints in 3D scene wxGetApp().plater()->canvas3D()->handle_sidebar_focus_event(opt_key, true); }; @@ -233,9 +154,6 @@ void ObjectManipulation::UpdateAndShow(const bool show) { if (show) { update_settings_value(wxGetApp().plater()->canvas3D()->get_selection()); -#if !ENABLE_IMPROVED_SIDEBAR_OBJECTS_MANIPULATION - update_if_dirty(); -#endif // !ENABLE_IMPROVED_SIDEBAR_OBJECTS_MANIPULATION } OG_Settings::UpdateAndShow(show); @@ -254,7 +172,6 @@ void ObjectManipulation::update_settings_value(const GLCanvas3D::Selection& sele m_new_rotation = volume->get_instance_rotation(); m_new_scale = volume->get_instance_scaling_factor(); int obj_idx = volume->object_idx(); -#if ENABLE_IMPROVED_SIDEBAR_OBJECTS_MANIPULATION int instance_idx = volume->instance_idx(); if ((0 <= obj_idx) && (obj_idx < (int)wxGetApp().model_objects()->size())) { @@ -270,21 +187,12 @@ void ObjectManipulation::update_settings_value(const GLCanvas3D::Selection& sele else // this should never happen m_new_size = Vec3d::Zero(); -#else - if ((0 <= obj_idx) && (obj_idx < (int)wxGetApp().model_objects()->size())) - m_new_size = volume->get_instance_transformation().get_matrix(true, true) * (*wxGetApp().model_objects())[obj_idx]->raw_mesh_bounding_box().size(); - else - // this should never happen - m_new_size = Vec3d::Zero(); -#endif // ENABLE_IMPROVED_SIDEBAR_OBJECTS_MANIPULATION m_new_enabled = true; } else if (selection.is_single_full_object()) { -#if ENABLE_IMPROVED_SIDEBAR_OBJECTS_MANIPULATION m_cache.instance.reset(); -#endif // ENABLE_IMPROVED_SIDEBAR_OBJECTS_MANIPULATION const BoundingBoxf3& box = selection.get_bounding_box(); m_new_position = box.center(); @@ -297,9 +205,7 @@ void ObjectManipulation::update_settings_value(const GLCanvas3D::Selection& sele } else if (selection.is_single_modifier() || selection.is_single_volume()) { -#if ENABLE_IMPROVED_SIDEBAR_OBJECTS_MANIPULATION m_cache.instance.reset(); -#endif // ENABLE_IMPROVED_SIDEBAR_OBJECTS_MANIPULATION // the selection contains a single volume const GLVolume* volume = selection.get_volume(*selection.get_volume_idxs().begin()); @@ -329,7 +235,6 @@ void ObjectManipulation::update_if_dirty() if (!m_dirty) return; -#if ENABLE_IMPROVED_SIDEBAR_OBJECTS_MANIPULATION if (m_cache.move_label_string != _(m_new_move_label_string)+ ":") { m_cache.move_label_string = _(m_new_move_label_string)+ ":"; @@ -382,16 +287,22 @@ void ObjectManipulation::update_if_dirty() m_cache.size = m_new_size; + Vec3d deg_rotation; + for (size_t i = 0; i < 3; ++i) + { + deg_rotation(i) = Geometry::rad2deg(m_new_rotation(i)); + } + if (m_cache.rotation(0) != m_new_rotation(0)) - m_og->set_value("rotation_x", double_to_string(Geometry::rad2deg(m_new_rotation(0)), 2)); + m_og->set_value("rotation_x", double_to_string(deg_rotation(0), 2)); if (m_cache.rotation(1) != m_new_rotation(1)) - m_og->set_value("rotation_y", double_to_string(Geometry::rad2deg(m_new_rotation(1)), 2)); + m_og->set_value("rotation_y", double_to_string(deg_rotation(1), 2)); if (m_cache.rotation(2) != m_new_rotation(2)) - m_og->set_value("rotation_z", double_to_string(Geometry::rad2deg(m_new_rotation(2)), 2)); + m_og->set_value("rotation_z", double_to_string(deg_rotation(2), 2)); - m_cache.rotation = m_new_rotation; + m_cache.rotation = deg_rotation; if (wxGetApp().plater()->canvas3D()->get_selection().requires_uniform_scale()) { m_lock_bnt->SetLock(true); @@ -406,41 +317,27 @@ void ObjectManipulation::update_if_dirty() m_og->enable(); else m_og->disable(); -#else - m_move_Label->SetLabel(_(m_new_move_label_string)); - m_rotate_Label->SetLabel(_(m_new_rotate_label_string)); - m_scale_Label->SetLabel(_(m_new_scale_label_string)); - - m_og->set_value("position_x", double_to_string(m_new_position(0), 2)); - m_og->set_value("position_y", double_to_string(m_new_position(1), 2)); - m_og->set_value("position_z", double_to_string(m_new_position(2), 2)); - m_cache_position = m_new_position; - - auto scale = m_new_scale * 100.0; - m_og->set_value("scale_x", double_to_string(scale(0), 2)); - m_og->set_value("scale_y", double_to_string(scale(1), 2)); - m_og->set_value("scale_z", double_to_string(scale(2), 2)); - m_cache_scale = scale; - - m_og->set_value("size_x", double_to_string(m_new_size(0), 2)); - m_og->set_value("size_y", double_to_string(m_new_size(1), 2)); - m_og->set_value("size_z", double_to_string(m_new_size(2), 2)); - m_cache_size = m_new_size; - - m_og->set_value("rotation_x", double_to_string(round_nearest(Geometry::rad2deg(m_new_rotation(0)), 0), 2)); - m_og->set_value("rotation_y", double_to_string(round_nearest(Geometry::rad2deg(m_new_rotation(1)), 0), 2)); - m_og->set_value("rotation_z", double_to_string(round_nearest(Geometry::rad2deg(m_new_rotation(2)), 0), 2)); - m_cache_rotation = m_new_rotation; - - if (m_new_enabled) - m_og->enable(); - else - m_og->disable(); -#endif // ENABLE_IMPROVED_SIDEBAR_OBJECTS_MANIPULATION m_dirty = false; } +#ifndef __APPLE__ +void ObjectManipulation::emulate_kill_focus() +{ + if (m_focused_option.empty()) + return; + + // we need to use a copy because the value of m_focused_option is modified inside on_change() and on_fill_empty_value() + std::string option = m_focused_option; + + // see TextCtrl::propagate_value() + if (static_cast(m_og->get_fieldc(option, 0)->getWindow())->GetValue().empty()) + on_fill_empty_value(option); + else + on_change(option, 0); +} +#endif // __APPLE__ + void ObjectManipulation::reset_settings_value() { m_new_position = Vec3d::Zero(); @@ -448,9 +345,7 @@ void ObjectManipulation::reset_settings_value() m_new_scale = Vec3d::Ones(); m_new_size = Vec3d::Zero(); m_new_enabled = false; -#if ENABLE_IMPROVED_SIDEBAR_OBJECTS_MANIPULATION m_cache.instance.reset(); -#endif // ENABLE_IMPROVED_SIDEBAR_OBJECTS_MANIPULATION m_dirty = true; } @@ -459,18 +354,10 @@ void ObjectManipulation::change_position_value(const Vec3d& position) auto canvas = wxGetApp().plater()->canvas3D(); GLCanvas3D::Selection& selection = canvas->get_selection(); selection.start_dragging(); -#if ENABLE_IMPROVED_SIDEBAR_OBJECTS_MANIPULATION selection.translate(position - m_cache.position, selection.requires_local_axes()); -#else - selection.translate(position - m_cache_position, selection.requires_local_axes()); -#endif // ENABLE_IMPROVED_SIDEBAR_OBJECTS_MANIPULATION canvas->do_move(); -#if ENABLE_IMPROVED_SIDEBAR_OBJECTS_MANIPULATION m_cache.position = position; -#else - m_cache_position = position; -#endif // ENABLE_IMPROVED_SIDEBAR_OBJECTS_MANIPULATION } void ObjectManipulation::change_rotation_value(const Vec3d& rotation) @@ -478,23 +365,19 @@ void ObjectManipulation::change_rotation_value(const Vec3d& rotation) GLCanvas3D* canvas = wxGetApp().plater()->canvas3D(); const GLCanvas3D::Selection& selection = canvas->get_selection(); + Vec3d delta_rotation = rotation - m_cache.rotation; + Vec3d rad_rotation; for (size_t i = 0; i < 3; ++i) { - rad_rotation(i) = Geometry::deg2rad(rotation(i)); + rad_rotation(i) = Geometry::deg2rad(delta_rotation(i)); } canvas->get_selection().start_dragging(); -#if ENABLE_IMPROVED_SIDEBAR_OBJECTS_MANIPULATION canvas->get_selection().rotate(rad_rotation, selection.is_single_full_instance() || selection.requires_local_axes()); -#else - canvas->get_selection().rotate(rad_rotation, selection.is_single_full_instance()); -#endif // ENABLE_IMPROVED_SIDEBAR_OBJECTS_MANIPULATION canvas->do_rotate(); -#if ENABLE_IMPROVED_SIDEBAR_OBJECTS_MANIPULATION m_cache.rotation = rotation; -#endif // ENABLE_IMPROVED_SIDEBAR_OBJECTS_MANIPULATION } void ObjectManipulation::change_scale_value(const Vec3d& scale) @@ -503,11 +386,7 @@ void ObjectManipulation::change_scale_value(const Vec3d& scale) const GLCanvas3D::Selection& selection = wxGetApp().plater()->canvas3D()->get_selection(); if (m_uniform_scale || selection.requires_uniform_scale()) { -#if ENABLE_IMPROVED_SIDEBAR_OBJECTS_MANIPULATION Vec3d abs_scale_diff = (scale - m_cache.scale).cwiseAbs(); -#else - Vec3d abs_scale_diff = (scale - m_cache_scale).cwiseAbs(); -#endif // ENABLE_IMPROVED_SIDEBAR_OBJECTS_MANIPULATION double max_diff = abs_scale_diff(X); Axis max_diff_axis = X; if (max_diff < abs_scale_diff(Y)) @@ -530,12 +409,10 @@ void ObjectManipulation::change_scale_value(const Vec3d& scale) canvas->get_selection().scale(scaling_factor, false); canvas->do_scale(); -#if ENABLE_IMPROVED_SIDEBAR_OBJECTS_MANIPULATION if (!m_cache.scale.isApprox(scale)) m_cache.instance.instance_idx = -1; m_cache.scale = scale; -#endif // ENABLE_IMPROVED_SIDEBAR_OBJECTS_MANIPULATION } void ObjectManipulation::change_size_value(const Vec3d& size) @@ -543,7 +420,6 @@ void ObjectManipulation::change_size_value(const Vec3d& size) const GLCanvas3D::Selection& selection = wxGetApp().plater()->canvas3D()->get_selection(); Vec3d ref_size = m_cache.size; -#if ENABLE_IMPROVED_SIDEBAR_OBJECTS_MANIPULATION if (selection.is_single_volume() || selection.is_single_modifier()) { const GLVolume* volume = selection.get_volume(*selection.get_volume_idxs().begin()); @@ -581,15 +457,81 @@ void ObjectManipulation::change_size_value(const Vec3d& size) canvas->do_scale(); m_cache.size = size; -#else - if (selection.is_single_full_instance()) - { - const GLVolume* volume = selection.get_volume(*selection.get_volume_idxs().begin()); - ref_size = volume->bounding_box.size(); +} + +void ObjectManipulation::on_change(const t_config_option_key& opt_key, const boost::any& value) +{ + // needed to hide the visual hints in 3D scene + wxGetApp().plater()->canvas3D()->handle_sidebar_focus_event(opt_key, false); +#ifndef __APPLE__ + m_focused_option = ""; +#endif // __APPLE__ + + if (!m_cache.is_valid()) + return; + + std::vector axes{ "_x", "_y", "_z" }; + + std::string param; + std::copy(opt_key.begin(), opt_key.end() - 2, std::back_inserter(param)); + + size_t i = 0; + Vec3d new_value; + for (auto axis : axes) + new_value(i++) = boost::any_cast(m_og->get_value(param + axis)); + + if (param == "position") + change_position_value(new_value); + else if (param == "rotation") + change_rotation_value(new_value); + else if (param == "scale") + change_scale_value(new_value); + else if (param == "size") + change_size_value(new_value); +} + +void ObjectManipulation::on_fill_empty_value(const std::string& opt_key) +{ + // needed to hide the visual hints in 3D scene + wxGetApp().plater()->canvas3D()->handle_sidebar_focus_event(opt_key, false); +#ifndef __APPLE__ + m_focused_option = ""; +#endif // __APPLE__ + + if (!m_cache.is_valid()) + return; + + std::string param; + std::copy(opt_key.begin(), opt_key.end() - 2, std::back_inserter(param)); + + double value = 0.0; + + if (param == "position") { + int axis = opt_key.back() == 'x' ? 0 : + opt_key.back() == 'y' ? 1 : 2; + + value = m_cache.position(axis); + } + else if (param == "rotation") { + int axis = opt_key.back() == 'x' ? 0 : + opt_key.back() == 'y' ? 1 : 2; + + value = m_cache.rotation(axis); + } + else if (param == "scale") { + int axis = opt_key.back() == 'x' ? 0 : + opt_key.back() == 'y' ? 1 : 2; + + value = m_cache.scale(axis); + } + else if (param == "size") { + int axis = opt_key.back() == 'x' ? 0 : + opt_key.back() == 'y' ? 1 : 2; + + value = m_cache.size(axis); } - change_scale_value(100.0 * Vec3d(size(0) / ref_size(0), size(1) / ref_size(1), size(2) / ref_size(2))); -#endif // ENABLE_IMPROVED_SIDEBAR_OBJECTS_MANIPULATION + m_og->set_value(opt_key, double_to_string(value)); } } //namespace GUI diff --git a/src/slic3r/GUI/GUI_ObjectManipulation.hpp b/src/slic3r/GUI/GUI_ObjectManipulation.hpp index 1684a1360f..16160c84d1 100644 --- a/src/slic3r/GUI/GUI_ObjectManipulation.hpp +++ b/src/slic3r/GUI/GUI_ObjectManipulation.hpp @@ -15,7 +15,6 @@ namespace GUI { class ObjectManipulation : public OG_Settings { -#if ENABLE_IMPROVED_SIDEBAR_OBJECTS_MANIPULATION struct Cache { Vec3d position; @@ -43,20 +42,22 @@ class ObjectManipulation : public OG_Settings Instance instance; - Cache() : position(Vec3d(DBL_MAX, DBL_MAX, DBL_MAX)) , rotation(Vec3d(DBL_MAX, DBL_MAX, DBL_MAX)) - , scale(Vec3d(DBL_MAX, DBL_MAX, DBL_MAX)) , size(Vec3d(DBL_MAX, DBL_MAX, DBL_MAX)) - , move_label_string("") , rotate_label_string("") , scale_label_string("") + Cache() { reset(); } + void reset() { + position = Vec3d(DBL_MAX, DBL_MAX, DBL_MAX); + rotation = Vec3d(DBL_MAX, DBL_MAX, DBL_MAX); + scale = Vec3d(DBL_MAX, DBL_MAX, DBL_MAX); + size = Vec3d(DBL_MAX, DBL_MAX, DBL_MAX); + move_label_string = ""; + rotate_label_string = ""; + scale_label_string = ""; + instance.reset(); } + bool is_valid() const { return position != Vec3d(DBL_MAX, DBL_MAX, DBL_MAX); } }; Cache m_cache; -#else - Vec3d m_cache_position{ 0., 0., 0. }; - Vec3d m_cache_rotation{ 0., 0., 0. }; - Vec3d m_cache_scale{ 100., 100., 100. }; - Vec3d m_cache_size{ 0., 0., 0. }; -#endif // ENABLE_IMPROVED_SIDEBAR_OBJECTS_MANIPULATION wxStaticText* m_move_Label = nullptr; wxStaticText* m_scale_Label = nullptr; @@ -76,6 +77,11 @@ class ObjectManipulation : public OG_Settings bool m_uniform_scale {true}; PrusaLockButton* m_lock_bnt{ nullptr }; +#ifndef __APPLE__ + // Currently focused option name (empty if none) + std::string m_focused_option; +#endif // __APPLE__ + public: ObjectManipulation(wxWindow* parent); ~ObjectManipulation() {} @@ -92,6 +98,14 @@ public: void set_uniform_scaling(const bool uniform_scale) { m_uniform_scale = uniform_scale;} bool get_uniform_scaling() const { return m_uniform_scale; } + void reset_cache() { m_cache.reset(); } +#ifndef __APPLE__ + // On Windows and Linux, emulates a kill focus event on the currently focused option (if any) + // Used only in ObjectList wxEVT_DATAVIEW_SELECTION_CHANGED handler which is called before the regular kill focus event + // bound to this class when changing selection in the objects list + void emulate_kill_focus(); +#endif // __APPLE__ + private: void reset_settings_value(); @@ -105,6 +119,9 @@ private: void change_rotation_value(const Vec3d& rotation); void change_scale_value(const Vec3d& scale); void change_size_value(const Vec3d& size); + + void on_change(const t_config_option_key& opt_key, const boost::any& value); + void on_fill_empty_value(const std::string& opt_key); }; }} diff --git a/src/slic3r/GUI/KBShortcutsDialog.cpp b/src/slic3r/GUI/KBShortcutsDialog.cpp index 5b0daf3665..c783539191 100644 --- a/src/slic3r/GUI/KBShortcutsDialog.cpp +++ b/src/slic3r/GUI/KBShortcutsDialog.cpp @@ -88,13 +88,8 @@ KBShortcutsDialog::KBShortcutsDialog() void KBShortcutsDialog::fill_shortcuts() { -#ifdef __WXOSX__ - const std::string ctrl = "⌘"; - const std::string alt = "⌥"; -#else - const std::string ctrl = "Ctrl+"; - const std::string alt = "Alt+"; -#endif // __WXOSX__ + const std::string &ctrl = GUI::shortkey_ctrl_prefix(); + const std::string &alt = GUI::shortkey_alt_prefix(); m_full_shortcuts.reserve(4); diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 3b47abf3d9..706f7436e5 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -329,7 +329,19 @@ void MainFrame::init_menubar() if (m_plater != nullptr) { editMenu = new wxMenu(); - wxMenuItem* item_select_all = append_menu_item(editMenu, wxID_ANY, _(L("&Select all")) + "\tCtrl+A", _(L("Selects all objects")), + // \xA0 is a non-breaking space. It is entered here to spoil the automatic accelerators, + // as the simple numeric accelerators spoil all numeric data entry. + wxMenuItem* item_select_all = append_menu_item(editMenu, wxID_ANY, _(L("&Select all")) + +#ifdef _MSC_VER + "\t\xA0" + "Ctrl+\xA0" + "A" +#else +#ifdef __APPLE__ + "\tCtrl+A" +#else + " - Ctrl+A" +#endif +#endif + , _(L("Selects all objects")), [this](wxCommandEvent&) { m_plater->select_all(); }, ""); editMenu->AppendSeparator(); wxMenuItem* item_delete_sel = append_menu_item(editMenu, wxID_ANY, _(L("&Delete selected")) + "\tDel", _(L("Deletes the current selection")), @@ -391,19 +403,23 @@ void MainFrame::init_menubar() // View menu wxMenu* viewMenu = nullptr; + wxString sep = +#ifdef _MSC_VER + "\t"; +#else + " - "; +#endif if (m_plater) { viewMenu = new wxMenu(); - // \xA0 is a non-breaing space. It is entered here to spoil the automatic accelerators, - // as the simple numeric accelerators spoil all numeric data entry. // The camera control accelerators are captured by GLCanvas3D::on_char(). - wxMenuItem* item_iso = append_menu_item(viewMenu, wxID_ANY, _(L("&Iso")) + "\t\xA0" + "0", _(L("Iso View")), [this](wxCommandEvent&) { select_view("iso"); }); + wxMenuItem* item_iso = append_menu_item(viewMenu, wxID_ANY, _(L("Iso")) + sep + "&0", _(L("Iso View")), [this](wxCommandEvent&) { select_view("iso"); }); viewMenu->AppendSeparator(); - wxMenuItem* item_top = append_menu_item(viewMenu, wxID_ANY, _(L("&Top")) + "\t\xA0" + "1", _(L("Top View")), [this](wxCommandEvent&) { select_view("top"); }); - wxMenuItem* item_bottom = append_menu_item(viewMenu, wxID_ANY, _(L("&Bottom")) + "\t\xA0" + "2", _(L("Bottom View")), [this](wxCommandEvent&) { select_view("bottom"); }); - wxMenuItem* item_front = append_menu_item(viewMenu, wxID_ANY, _(L("&Front")) + "\t\xA0" + "3", _(L("Front View")), [this](wxCommandEvent&) { select_view("front"); }); - wxMenuItem* item_rear = append_menu_item(viewMenu, wxID_ANY, _(L("R&ear")) + "\t\xA0" + "4", _(L("Rear View")), [this](wxCommandEvent&) { select_view("rear"); }); - wxMenuItem* item_left = append_menu_item(viewMenu, wxID_ANY, _(L("&Left")) + "\t\xA0" + "5", _(L("Left View")), [this](wxCommandEvent&) { select_view("left"); }); - wxMenuItem* item_right = append_menu_item(viewMenu, wxID_ANY, _(L("&Right")) + "\t\xA0" + "6", _(L("Right View")), [this](wxCommandEvent&) { select_view("right"); }); + wxMenuItem* item_top = append_menu_item(viewMenu, wxID_ANY, _(L("Top")) + sep + "&1", _(L("Top View")), [this](wxCommandEvent&) { select_view("top"); }); + wxMenuItem* item_bottom = append_menu_item(viewMenu, wxID_ANY, _(L("Bottom")) + sep + "&2", _(L("Bottom View")), [this](wxCommandEvent&) { select_view("bottom"); }); + wxMenuItem* item_front = append_menu_item(viewMenu, wxID_ANY, _(L("Front")) + sep + "&3", _(L("Front View")), [this](wxCommandEvent&) { select_view("front"); }); + wxMenuItem* item_rear = append_menu_item(viewMenu, wxID_ANY, _(L("Rear")) + sep + "&4", _(L("Rear View")), [this](wxCommandEvent&) { select_view("rear"); }); + wxMenuItem* item_left = append_menu_item(viewMenu, wxID_ANY, _(L("Left")) + sep + "&5", _(L("Left View")), [this](wxCommandEvent&) { select_view("left"); }); + wxMenuItem* item_right = append_menu_item(viewMenu, wxID_ANY, _(L("Right")) + sep + "&6", _(L("Right View")), [this](wxCommandEvent&) { select_view("right"); }); Bind(wxEVT_UPDATE_UI, [this](wxUpdateUIEvent& evt) { evt.Enable(can_change_view()); }, item_iso->GetId()); Bind(wxEVT_UPDATE_UI, [this](wxUpdateUIEvent& evt) { evt.Enable(can_change_view()); }, item_top->GetId()); @@ -439,7 +455,7 @@ void MainFrame::init_menubar() append_menu_item(helpMenu, wxID_ANY, _(L("&About Slic3r")), _(L("Show about dialog")), [this](wxCommandEvent&) { Slic3r::GUI::about(); }); helpMenu->AppendSeparator(); - append_menu_item(helpMenu, wxID_ANY, _(L("&Keyboard Shortcuts")) + "\t\xA0?", _(L("Show the list of the keyboard shortcuts")), + append_menu_item(helpMenu, wxID_ANY, _(L("Keyboard Shortcuts")) + sep + "&?", _(L("Show the list of the keyboard shortcuts")), [this](wxCommandEvent&) { wxGetApp().keyboard_shortcuts(); }); } diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 4a2da83e80..729f4997d5 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -896,7 +896,7 @@ std::vector& Sidebar::combos_filament() class PlaterDropTarget : public wxFileDropTarget { public: - PlaterDropTarget(Plater *plater) : plater(plater) {} + PlaterDropTarget(Plater *plater) : plater(plater) { this->SetDefaultAction(wxDragCopy); } virtual bool OnDropFiles(wxCoord x, wxCoord y, const wxArrayString &filenames); @@ -1028,7 +1028,8 @@ struct Plater::priv unsigned int update_background_process(bool force_validation = false); // Restart background processing thread based on a bitmask of UpdateBackgroundProcessReturnState. bool restart_background_process(unsigned int state); - void update_restart_background_process(bool force_scene_update, bool force_preview_update); + // returns bit mask of UpdateBackgroundProcessReturnState + unsigned int update_restart_background_process(bool force_scene_update, bool force_preview_update); void export_gcode(fs::path output_path, PrintHostJob upload_job); void reload_from_disk(); void fix_through_netfabb(const int obj_idx, const int vol_idx = -1); @@ -1170,15 +1171,14 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) view3D_canvas->Bind(EVT_GLCANVAS_RIGHT_CLICK, &priv::on_right_click, this); view3D_canvas->Bind(EVT_GLCANVAS_REMOVE_OBJECT, [q](SimpleEvent&) { q->remove_selected(); }); view3D_canvas->Bind(EVT_GLCANVAS_ARRANGE, [this](SimpleEvent&) { arrange(); }); + view3D_canvas->Bind(EVT_GLCANVAS_SELECT_ALL, [this](SimpleEvent&) { this->q->select_all(); }); view3D_canvas->Bind(EVT_GLCANVAS_QUESTION_MARK, [this](SimpleEvent&) { wxGetApp().keyboard_shortcuts(); }); view3D_canvas->Bind(EVT_GLCANVAS_INCREASE_INSTANCES, [this](Event &evt) { if (evt.data == 1) this->q->increase_instances(); else if (this->can_decrease_instances()) this->q->decrease_instances(); }); view3D_canvas->Bind(EVT_GLCANVAS_INSTANCE_MOVED, [this](SimpleEvent&) { update(); }); view3D_canvas->Bind(EVT_GLCANVAS_WIPETOWER_MOVED, &priv::on_wipetower_moved, this); -#if ENABLE_IMPROVED_SIDEBAR_OBJECTS_MANIPULATION view3D_canvas->Bind(EVT_GLCANVAS_INSTANCE_ROTATED, [this](SimpleEvent&) { update(); }); view3D_canvas->Bind(EVT_GLCANVAS_INSTANCE_SCALED, [this](SimpleEvent&) { update(); }); -#endif // ENABLE_IMPROVED_SIDEBAR_OBJECTS_MANIPULATION view3D_canvas->Bind(EVT_GLCANVAS_ENABLE_ACTION_BUTTONS, [this](Event &evt) { this->sidebar->enable_buttons(evt.data); }); view3D_canvas->Bind(EVT_GLCANVAS_UPDATE_GEOMETRY, &priv::on_update_geometry, this); view3D_canvas->Bind(EVT_GLCANVAS_MOUSE_DRAGGING_FINISHED, &priv::on_3dcanvas_mouse_dragging_finished, this); @@ -1577,7 +1577,7 @@ std::unique_ptr Plater::priv::get_export_file(GUI::FileType // Update printbility state of each of the ModelInstances. this->update_print_volume_state(); // Find the file name of the first printable object. - fs::path output_file = this->model.propose_export_file_name(); + fs::path output_file = this->model.propose_export_file_name_and_path(); switch (file_type) { case FT_STL: output_file.replace_extension("stl"); break; @@ -1737,6 +1737,8 @@ void Plater::priv::arrange() // Guard the arrange process arranging.store(true); + wxBusyCursor wait; + // Disable the arrange button (to prevent reentrancies, we will call wxYied) view3D->enable_toolbar_item("arrange", can_arrange()); @@ -2047,7 +2049,7 @@ void Plater::priv::export_gcode(fs::path output_path, PrintHostJob upload_job) this->restart_background_process(priv::UPDATE_BACKGROUND_PROCESS_FORCE_EXPORT); } -void Plater::priv::update_restart_background_process(bool force_update_scene, bool force_update_preview) +unsigned int Plater::priv::update_restart_background_process(bool force_update_scene, bool force_update_preview) { // bitmask of UpdateBackgroundProcessReturnState unsigned int state = this->update_background_process(false); @@ -2057,6 +2059,7 @@ void Plater::priv::update_restart_background_process(bool force_update_scene, bo if (force_update_preview) this->preview->reload_print(); this->restart_background_process(state); + return state; } void Plater::priv::update_fff_scene() @@ -2110,31 +2113,10 @@ void Plater::priv::fix_through_netfabb(const int obj_idx, const int vol_idx/* = { if (obj_idx < 0) return; - - const auto model_object = model.objects[obj_idx]; - Model model_fixed;// = new Model(); - fix_model_by_win10_sdk_gui(*model_object, this->fff_print, model_fixed); - - auto new_obj_idxs = load_model_objects(model_fixed.objects); - if (new_obj_idxs.empty()) - return; - - for(auto new_obj_idx : new_obj_idxs) { - auto o = model.objects[new_obj_idx]; - o->clear_instances(); - for (auto instance: model_object->instances) - o->add_instance(*instance); - o->invalidate_bounding_box(); - - if (o->volumes.size() == model_object->volumes.size()) { - for (int i = 0; i < o->volumes.size(); i++) { - o->volumes[i]->config.apply(model_object->volumes[i]->config); - } - } - // FIXME restore volumes and their configs, layer_height_ranges, layer_height_profile - } - - remove(obj_idx); + fix_model_by_win10_sdk_gui(*model.objects[obj_idx], vol_idx); + this->object_list_changed(); + this->update(); + this->schedule_background_process(); } void Plater::priv::set_current_panel(wxPanel* panel) @@ -2555,7 +2537,7 @@ void Plater::priv::init_view_toolbar() GLToolbarItem::Data item; item.name = "3D"; - item.tooltip = GUI::L_str("3D editor view [Ctrl+5]"); + item.tooltip = GUI::L_str("3D editor view") + " [" + GUI::shortkey_ctrl_prefix() + "5]"; item.sprite_id = 0; item.action_event = EVT_GLVIEWTOOLBAR_3D; item.is_toggable = false; @@ -2563,7 +2545,7 @@ void Plater::priv::init_view_toolbar() return; item.name = "Preview"; - item.tooltip = GUI::L_str("Preview [Ctrl+6]"); + item.tooltip = GUI::L_str("Preview") + " [" + GUI::shortkey_ctrl_prefix() + "6]"; item.sprite_id = 1; item.action_event = EVT_GLVIEWTOOLBAR_PREVIEW; item.is_toggable = false; @@ -2848,51 +2830,43 @@ void Plater::cut(size_t obj_idx, size_t instance_idx, coordf_t z, bool keep_uppe p->load_model_objects(new_objects); } -void Plater::export_gcode(fs::path output_path) +void Plater::export_gcode() { if (p->model.objects.empty()) return; - // select output file - if (output_path.empty()) { - // XXX: take output path from CLI opts? Ancient Slic3r versions used to do that... - - // If possible, remove accents from accented latin characters. - // This function is useful for generating file names to be processed by legacy firmwares. - fs::path default_output_file; - try { - default_output_file = this->p->background_process.current_print()->output_filepath(output_path.string()); - } catch (const std::exception &ex) { - show_error(this, ex.what()); - return; - } - default_output_file = fs::path(Slic3r::fold_utf8_to_ascii(default_output_file.string())); - auto start_dir = wxGetApp().app_config->get_last_output_dir(default_output_file.parent_path().string()); - - wxFileDialog dlg(this, (printer_technology() == ptFFF) ? _(L("Save G-code file as:")) : _(L("Save Zip file as:")), - start_dir, - from_path(default_output_file.filename()), - GUI::file_wildcards((printer_technology() == ptFFF) ? FT_GCODE : FT_PNGZIP, default_output_file.extension().string()), - wxFD_SAVE | wxFD_OVERWRITE_PROMPT - ); - - if (dlg.ShowModal() == wxID_OK) { - fs::path path = into_path(dlg.GetPath()); - wxGetApp().app_config->update_last_output_dir(path.parent_path().string()); - output_path = std::move(path); - } - } else { - try { - output_path = this->p->background_process.current_print()->output_filepath(output_path.string()); - } catch (const std::exception &ex) { - show_error(this, ex.what()); - return; - } + // If possible, remove accents from accented latin characters. + // This function is useful for generating file names to be processed by legacy firmwares. + fs::path default_output_file; + try { + // Update the background processing, so that the placeholder parser will get the correct values for the ouput file template. + // Also if there is something wrong with the current configuration, a pop-up dialog will be shown and the export will not be performed. + unsigned int state = this->p->update_restart_background_process(false, false); + if (state & priv::UPDATE_BACKGROUND_PROCESS_INVALID) + return; + default_output_file = this->p->background_process.current_print()->output_filepath(""); + } catch (const std::exception &ex) { + show_error(this, ex.what()); + return; } + default_output_file = fs::path(Slic3r::fold_utf8_to_ascii(default_output_file.string())); + auto start_dir = wxGetApp().app_config->get_last_output_dir(default_output_file.parent_path().string()); - if (! output_path.empty()) { + wxFileDialog dlg(this, (printer_technology() == ptFFF) ? _(L("Save G-code file as:")) : _(L("Save Zip file as:")), + start_dir, + from_path(default_output_file.filename()), + GUI::file_wildcards((printer_technology() == ptFFF) ? FT_GCODE : FT_PNGZIP, default_output_file.extension().string()), + wxFD_SAVE | wxFD_OVERWRITE_PROMPT + ); + + fs::path output_path; + if (dlg.ShowModal() == wxID_OK) { + fs::path path = into_path(dlg.GetPath()); + wxGetApp().app_config->update_last_output_dir(path.parent_path().string()); + output_path = std::move(path); + } + if (! output_path.empty()) p->export_gcode(std::move(output_path), PrintHostJob()); - } } void Plater::export_stl(bool selection_only) @@ -2993,7 +2967,12 @@ void Plater::send_gcode() // Obtain default output path fs::path default_output_file; try { - default_output_file = this->p->background_process.current_print()->output_filepath(""); + // Update the background processing, so that the placeholder parser will get the correct values for the ouput file template. + // Also if there is something wrong with the current configuration, a pop-up dialog will be shown and the export will not be performed. + unsigned int state = this->p->update_restart_background_process(false, false); + if (state & priv::UPDATE_BACKGROUND_PROCESS_INVALID) + return; + default_output_file = this->p->background_process.current_print()->output_filepath(""); } catch (const std::exception &ex) { show_error(this, ex.what()); return; diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index 09b7348d5d..e3601b65c3 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -140,8 +140,7 @@ public: void cut(size_t obj_idx, size_t instance_idx, coordf_t z, bool keep_upper = true, bool keep_lower = true, bool rotate_lower = false); - // Note: empty path means "use the default" - void export_gcode(boost::filesystem::path output_path = boost::filesystem::path()); + void export_gcode(); void export_stl(bool selection_only = false); void export_amf(); void export_3mf(const boost::filesystem::path& output_path = boost::filesystem::path()); diff --git a/src/slic3r/GUI/Preset.cpp b/src/slic3r/GUI/Preset.cpp index ef1caf0358..871cd0b736 100644 --- a/src/slic3r/GUI/Preset.cpp +++ b/src/slic3r/GUI/Preset.cpp @@ -336,7 +336,7 @@ const std::vector& Preset::print_options() "support_material_synchronize_layers", "support_material_angle", "support_material_interface_layers", "support_material_interface_spacing", "support_material_interface_contact_loops", "support_material_contact_distance", "support_material_buildplate_only", "dont_support_bridges", "notes", "complete_objects", "extruder_clearance_radius", - "extruder_clearance_height", "gcode_comments", "output_filename_format", "post_process", "perimeter_extruder", + "extruder_clearance_height", "gcode_comments", "gcode_label_objects", "output_filename_format", "post_process", "perimeter_extruder", "infill_extruder", "solid_infill_extruder", "support_material_extruder", "support_material_interface_extruder", "ooze_prevention", "standby_temperature_delta", "interface_shells", "extrusion_width", "first_layer_extrusion_width", "perimeter_extrusion_width", "external_perimeter_extrusion_width", "infill_extrusion_width", "solid_infill_extrusion_width", @@ -517,16 +517,6 @@ void PresetCollection::add_default_preset(const std::vector &keys, ++ m_num_default_presets; } -bool is_file_plain(const std::string &path) -{ -#ifdef _MSC_VER - DWORD attributes = GetFileAttributesW(boost::nowide::widen(path).c_str()); - return (attributes & (FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_SYSTEM)) == 0; -#else - return true; -#endif -} - // Load all presets found in dir_path. // Throws an exception on error. void PresetCollection::load_presets(const std::string &dir_path, const std::string &subdir) @@ -538,10 +528,7 @@ void PresetCollection::load_presets(const std::string &dir_path, const std::stri // (see the "Preset already present, not loading" message). std::deque presets_loaded; for (auto &dir_entry : boost::filesystem::directory_iterator(dir)) - if (boost::filesystem::is_regular_file(dir_entry.status()) && boost::algorithm::iends_with(dir_entry.path().filename().string(), ".ini") && - // Ignore system and hidden files, which may be created by the DropBox synchronisation process. - // https://github.com/prusa3d/Slic3r/issues/1298 - is_file_plain(dir_entry.path().string())) { + if (Slic3r::is_ini_file(dir_entry)) { std::string name = dir_entry.path().filename().string(); // Remove the .ini suffix. name.erase(name.size() - 4); @@ -1163,12 +1150,24 @@ std::string PresetCollection::name() const case Preset::TYPE_PRINT: return L("print"); case Preset::TYPE_FILAMENT: return L("filament"); case Preset::TYPE_SLA_PRINT: return L("SLA print"); - case Preset::TYPE_SLA_MATERIAL: return L("SLA material"); + case Preset::TYPE_SLA_MATERIAL: return L("SLA material"); case Preset::TYPE_PRINTER: return L("printer"); default: return "invalid"; } } +std::string PresetCollection::section_name() const +{ + switch (this->type()) { + case Preset::TYPE_PRINT: return "print"; + case Preset::TYPE_FILAMENT: return "filament"; + case Preset::TYPE_SLA_PRINT: return "sla_print"; + case Preset::TYPE_SLA_MATERIAL: return "sla_material"; + case Preset::TYPE_PRINTER: return "printer"; + default: return "invalid"; + } +} + std::vector PresetCollection::system_preset_names() const { size_t num = 0; diff --git a/src/slic3r/GUI/Preset.hpp b/src/slic3r/GUI/Preset.hpp index eae406bef2..73a921cf74 100644 --- a/src/slic3r/GUI/Preset.hpp +++ b/src/slic3r/GUI/Preset.hpp @@ -230,7 +230,10 @@ public: void reset(bool delete_files); Preset::Type type() const { return m_type; } + // Name, to be used on the screen and in error messages. Not localized. std::string name() const; + // Name, to be used as a section name in config bundle, and as a folder name for presets. + std::string section_name() const; const std::deque& operator()() const { return m_presets; } // Add default preset at the start of the collection, increment the m_default_preset counter. diff --git a/src/slic3r/GUI/PresetBundle.cpp b/src/slic3r/GUI/PresetBundle.cpp index 4c6c52763c..5f33fd00ab 100644 --- a/src/slic3r/GUI/PresetBundle.cpp +++ b/src/slic3r/GUI/PresetBundle.cpp @@ -243,7 +243,7 @@ std::string PresetBundle::load_system_presets() std::string errors_cummulative; bool first = true; for (auto &dir_entry : boost::filesystem::directory_iterator(dir)) - if (boost::filesystem::is_regular_file(dir_entry.status()) && boost::algorithm::iends_with(dir_entry.path().filename().string(), ".ini")) { + if (Slic3r::is_ini_file(dir_entry)) { std::string name = dir_entry.path().filename().string(); // Remove the .ini suffix. name.erase(name.size() - 4); @@ -1208,7 +1208,7 @@ size_t PresetBundle::load_configbundle(const std::string &path, unsigned int fla #else // Store the print/filament/printer presets at the same location as the upstream Slic3r. #endif - / presets->name() / file_name).make_preferred(); + / presets->section_name() / file_name).make_preferred(); // Load the preset into the list of presets, save it to disk. Preset &loaded = presets->load_preset(file_path.string(), preset_name, std::move(config), false); if (flags & LOAD_CFGBNDLE_SAVE) @@ -1365,7 +1365,7 @@ void PresetBundle::export_configbundle(const std::string &path, bool export_syst if (preset.is_default || preset.is_external || (preset.is_system && ! export_system_settings)) // Only export the common presets, not external files or the default preset. continue; - c << std::endl << "[" << presets->name() << ":" << preset.name << "]" << std::endl; + c << std::endl << "[" << presets->section_name() << ":" << preset.name << "]" << std::endl; for (const std::string &opt_key : preset.config.keys()) c << opt_key << " = " << preset.config.serialize(opt_key) << std::endl; } diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index e42cdbc86e..5994ba96b3 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -1097,6 +1097,7 @@ void TabPrint::build() optgroup = page->new_optgroup(_(L("Output file"))); optgroup->append_single_option_line("gcode_comments"); + optgroup->append_single_option_line("gcode_label_objects"); option = optgroup->get_option("output_filename_format"); option.opt.full_width = true; optgroup->append_single_option_line(option); diff --git a/src/slic3r/Utils/FixModelByWin10.cpp b/src/slic3r/Utils/FixModelByWin10.cpp index 4b487588a0..1daeaff269 100644 --- a/src/slic3r/Utils/FixModelByWin10.cpp +++ b/src/slic3r/Utils/FixModelByWin10.cpp @@ -216,7 +216,7 @@ void fix_model_by_win10_sdk(const std::string &path_src, const std::string &path HRESULT hr = (*s_RoInitialize)(RO_INIT_MULTITHREADED); { - on_progress(L("Exporting the source model"), 20); + on_progress(L("Exporting source model"), 20); Microsoft::WRL::ComPtr fileStream; hr = winrt_open_file_stream(boost::nowide::widen(path_src), ABI::Windows::Storage::FileAccessMode::FileAccessMode_Read, fileStream.GetAddressOf(), throw_on_cancel); @@ -239,7 +239,7 @@ void fix_model_by_win10_sdk(const std::string &path_src, const std::string &path unsigned num_meshes = 0; hr = meshes->get_Size(&num_meshes); - on_progress(L("Repairing the model by the Netfabb service"), 40); + on_progress(L("Repairing model by the Netfabb service"), 40); Microsoft::WRL::ComPtr repairAsync; hr = model->RepairAsync(repairAsync.GetAddressOf()); @@ -248,7 +248,7 @@ void fix_model_by_win10_sdk(const std::string &path_src, const std::string &path throw std::runtime_error(L("Mesh repair failed.")); repairAsync->GetResults(); - on_progress(L("Loading the repaired model"), 60); + on_progress(L("Loading repaired model"), 60); // Verify the number of meshes returned after the repair action. meshes.Reset(); @@ -316,7 +316,7 @@ public: const char* what() const throw() { return "Model repair has been canceled"; } }; -void fix_model_by_win10_sdk_gui(const ModelObject &model_object, const Print &print, Model &result) +void fix_model_by_win10_sdk_gui(ModelObject &model_object, int volume_idx) { std::mutex mutex; std::condition_variable condition; @@ -329,6 +329,12 @@ void fix_model_by_win10_sdk_gui(const ModelObject &model_object, const Print &pr std::atomic canceled = false; std::atomic finished = false; + std::vector volumes; + if (volume_idx == -1) + volumes = model_object.volumes; + else + volumes.emplace_back(model_object.volumes[volume_idx]); + // Open a progress dialog. wxProgressDialog progress_dialog( _(L("Model fixing")), @@ -336,43 +342,64 @@ void fix_model_by_win10_sdk_gui(const ModelObject &model_object, const Print &pr 100, nullptr, wxPD_AUTO_HIDE | wxPD_APP_MODAL | wxPD_CAN_ABORT); // Executing the calculation in a background thread, so that the COM context could be created with its own threading model. // (It seems like wxWidgets initialize the COM contex as single threaded and we need a multi-threaded context). - bool success = false; - auto on_progress = [&mutex, &condition, &progress](const char *msg, unsigned prcnt) { + bool success = false; + size_t ivolume = 0; + auto on_progress = [&mutex, &condition, &ivolume, &volumes, &progress](const char *msg, unsigned prcnt) { std::lock_guard lk(mutex); progress.message = msg; - progress.percent = prcnt; + progress.percent = (int)floor((float(prcnt) + float(ivolume) * 100.f) / float(volumes.size())); progress.updated = true; condition.notify_all(); }; - auto worker_thread = boost::thread([&model_object, &print, &result, on_progress, &success, &canceled, &finished]() { + auto worker_thread = boost::thread([&model_object, &volumes, &ivolume, on_progress, &success, &canceled, &finished]() { try { - on_progress(L("Exporting the source model"), 0); - boost::filesystem::path path_src = boost::filesystem::temp_directory_path() / boost::filesystem::unique_path(); - path_src += ".3mf"; - Model model; - DynamicPrintConfig config; - model.add_object(model_object); - if (! Slic3r::store_3mf(path_src.string().c_str(), &model, &config)) { + std::vector meshes_repaired; + meshes_repaired.reserve(volumes.size()); + for (; ivolume < volumes.size(); ++ ivolume) { + on_progress(L("Exporting source model"), 0); + boost::filesystem::path path_src = boost::filesystem::temp_directory_path() / boost::filesystem::unique_path(); + path_src += ".3mf"; + Model model; + ModelObject *model_object = model.add_object(); + model_object->add_volume(*volumes[ivolume]); + model_object->add_instance(); + if (! Slic3r::store_3mf(path_src.string().c_str(), &model, nullptr)) { + boost::filesystem::remove(path_src); + throw std::runtime_error(L("Export of a temporary 3mf file failed")); + } + model.clear_objects(); + model.clear_materials(); + boost::filesystem::path path_dst = boost::filesystem::temp_directory_path() / boost::filesystem::unique_path(); + path_dst += ".3mf"; + fix_model_by_win10_sdk(path_src.string().c_str(), path_dst.string(), on_progress, + [&canceled]() { if (canceled) throw RepairCanceledException(); }); boost::filesystem::remove(path_src); - throw std::runtime_error(L("Export of a temporary 3mf file failed")); + // PresetBundle bundle; + on_progress(L("Loading repaired model"), 80); + DynamicPrintConfig config; + bool loaded = Slic3r::load_3mf(path_dst.string().c_str(), &config, &model); + boost::filesystem::remove(path_dst); + if (! loaded) + throw std::runtime_error(L("Import of the repaired 3mf file failed")); + if (model.objects.size() == 0) + throw std::runtime_error(L("Repaired 3MF file does not contain any object")); + if (model.objects.size() > 1) + throw std::runtime_error(L("Repaired 3MF file contains more than one object")); + if (model.objects.front()->volumes.size() == 0) + throw std::runtime_error(L("Repaired 3MF file does not contain any volume")); + if (model.objects.front()->volumes.size() > 1) + throw std::runtime_error(L("Repaired 3MF file contains more than one volume")); + meshes_repaired.emplace_back(std::move(model.objects.front()->volumes.front()->mesh)); } - model.clear_objects(); - model.clear_materials(); - boost::filesystem::path path_dst = boost::filesystem::temp_directory_path() / boost::filesystem::unique_path(); - path_dst += ".3mf"; - fix_model_by_win10_sdk(path_src.string().c_str(), path_dst.string(), on_progress, - [&canceled]() { if (canceled) throw RepairCanceledException(); }); - boost::filesystem::remove(path_src); - // PresetBundle bundle; - on_progress(L("Loading the repaired model"), 80); - bool loaded = Slic3r::load_3mf(path_dst.string().c_str(), &config, &result); - result.objects[0]->name = boost::filesystem::path(model_object.name).filename().stem().string() + "_fixed"; - boost::filesystem::remove(path_dst); - if (! loaded) - throw std::runtime_error(L("Import of the repaired 3mf file failed")); + for (size_t i = 0; i < volumes.size(); ++ i) { + volumes[i]->mesh = std::move(meshes_repaired[i]); + volumes[i]->set_new_unique_id(); + } + model_object.invalidate_bounding_box(); + -- ivolume; + on_progress(L("Model repair finished"), 100); success = true; finished = true; - on_progress(L("Model repair finished"), 100); } catch (RepairCanceledException & /* ex */) { canceled = true; finished = true; diff --git a/src/slic3r/Utils/FixModelByWin10.hpp b/src/slic3r/Utils/FixModelByWin10.hpp index c148a6970d..8e47664678 100644 --- a/src/slic3r/Utils/FixModelByWin10.hpp +++ b/src/slic3r/Utils/FixModelByWin10.hpp @@ -12,12 +12,12 @@ class Print; #ifdef HAS_WIN10SDK extern bool is_windows10(); -extern void fix_model_by_win10_sdk_gui(const ModelObject &model_object, const Print &print, Model &result); +extern void fix_model_by_win10_sdk_gui(ModelObject &model_object, int volume_idx); #else /* HAS_WIN10SDK */ inline bool is_windows10() { return false; } -inline void fix_model_by_win10_sdk_gui(const ModelObject &, const Print &, Model &) {} +inline void fix_model_by_win10_sdk_gui(ModelObject &, int) {} #endif /* HAS_WIN10SDK */ diff --git a/src/slic3r/Utils/PresetUpdater.cpp b/src/slic3r/Utils/PresetUpdater.cpp index bfa3af3e69..a22d463fb7 100644 --- a/src/slic3r/Utils/PresetUpdater.cpp +++ b/src/slic3r/Utils/PresetUpdater.cpp @@ -180,12 +180,11 @@ bool PresetUpdater::priv::get_file(const std::string &url, const fs::path &targe // Remove leftover paritally downloaded files, if any. void PresetUpdater::priv::prune_tmps() const { - for (fs::directory_iterator it(cache_path); it != fs::directory_iterator(); ++it) { - if (it->path().extension() == TMP_EXTENSION) { - BOOST_LOG_TRIVIAL(debug) << "Cache prune: " << it->path().string(); - fs::remove(it->path()); + for (auto &dir_entry : boost::filesystem::directory_iterator(cache_path)) + if (is_plain_file(dir_entry) && dir_entry.path().extension() == TMP_EXTENSION) { + BOOST_LOG_TRIVIAL(debug) << "Cache prune: " << dir_entry.path().string(); + fs::remove(dir_entry.path()); } - } } // Get Slic3rPE version available online, save in AppConfig. @@ -299,9 +298,9 @@ void PresetUpdater::priv::check_install_indices() const { BOOST_LOG_TRIVIAL(info) << "Checking if indices need to be installed from resources..."; - for (fs::directory_iterator it(rsrc_path); it != fs::directory_iterator(); ++it) { - const auto &path = it->path(); - if (path.extension() == ".idx") { + for (auto &dir_entry : boost::filesystem::directory_iterator(rsrc_path)) + if (is_idx_file(dir_entry)) { + const auto &path = dir_entry.path(); const auto path_in_cache = cache_path / path.filename(); if (! fs::exists(path_in_cache)) { @@ -318,7 +317,6 @@ void PresetUpdater::priv::check_install_indices() const } } } - } } // Generates a list of bundle updates that are to be performed