diff --git a/src/PrusaSlicer.cpp b/src/PrusaSlicer.cpp index 5eb3893447..ba41e05fa0 100644 --- a/src/PrusaSlicer.cpp +++ b/src/PrusaSlicer.cpp @@ -47,6 +47,8 @@ #include "libslic3r/Geometry.hpp" #include "libslic3r/GCode/PostProcessor.hpp" #include "libslic3r/Model.hpp" +#include "libslic3r/ModelProcessing.hpp" +#include "libslic3r/FileReader.hpp" #include "libslic3r/CutUtils.hpp" #include #include "libslic3r/Platform.hpp" @@ -291,13 +293,14 @@ int CLI::run(int argc, char **argv) Model model; try { if (has_config_from_profiles) - model = Model::read_from_file(file, nullptr, nullptr, Model::LoadAttribute::AddDefaultInstances); + model = FileReader::load_model(file); else { // When loading an AMF or 3MF, config is imported as well, including the printer technology. DynamicPrintConfig config; ConfigSubstitutionContext config_substitutions(config_substitution_rule); + boost::optional prusaslicer_generator_version; //FIXME should we check the version here? // | Model::LoadAttribute::CheckVersion ? - model = Model::read_from_file(file, &config, &config_substitutions, Model::LoadAttribute::AddDefaultInstances); + model = FileReader::load_model_with_config(file, &config, &config_substitutions, prusaslicer_generator_version, FileReader::LoadAttribute::AddDefaultInstances); PrinterTechnology other_printer_technology = get_printer_technology(config); if (printer_technology == ptUnknown) { printer_technology = other_printer_technology; @@ -563,7 +566,7 @@ int CLI::run(int argc, char **argv) size_t num_objects = model.objects.size(); for (size_t i = 0; i < num_objects; ++ i) { ModelObjectPtrs new_objects; - model.objects.front()->split(&new_objects); + ModelProcessing::split(model.objects.front(), &new_objects); model.delete_object(size_t(0)); } } diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index d80b17a0bc..57d42e6409 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -262,6 +262,10 @@ set(SLIC3R_SOURCES CutUtils.hpp Model.cpp Model.hpp + ModelProcessing.cpp + ModelProcessing.hpp + FileReader.cpp + FileReader.hpp MultiMaterialSegmentation.cpp MultiMaterialSegmentation.hpp MeshNormals.hpp diff --git a/src/libslic3r/FileReader.cpp b/src/libslic3r/FileReader.cpp new file mode 100644 index 0000000000..cf05144e56 --- /dev/null +++ b/src/libslic3r/FileReader.cpp @@ -0,0 +1,252 @@ +///|/ Copyright (c) Prusa Research 2016 - 2023 Tomáš Mészáros @tamasmeszaros, Oleksandra Iushchenko @YuSanka, David Kocík @kocikdav, Enrico Turri @enricoturri1966, Lukáš Matěna @lukasmatena, Vojtěch Bubník @bubnikv, Lukáš Hejl @hejllukas, Filip Sykala @Jony01, Vojtěch Král @vojtechkral +///|/ Copyright (c) 2021 Boleslaw Ciesielski +///|/ Copyright (c) 2019 John Drake @foxox +///|/ Copyright (c) 2019 Sijmen Schoon +///|/ Copyright (c) Slic3r 2014 - 2016 Alessandro Ranellucci @alranel +///|/ Copyright (c) 2015 Maksim Derbasov @ntfshard +///|/ +///|/ ported from lib/Slic3r/Model.pm: +///|/ Copyright (c) Prusa Research 2016 - 2022 Vojtěch Bubník @bubnikv, Enrico Turri @enricoturri1966 +///|/ Copyright (c) Slic3r 2012 - 2016 Alessandro Ranellucci @alranel +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ +#include "FileReader.hpp" +#include "Model.hpp" +#include "ModelProcessing.hpp" +#include "TriangleMesh.hpp" + +#include "Format/AMF.hpp" +#include "Format/OBJ.hpp" +#include "Format/STL.hpp" +#include "Format/3mf.hpp" +#include "Format/STEP.hpp" +#include "Format/SVG.hpp" +#include "Format/PrintRequest.hpp" + +#include + +#include "I18N.hpp" + +namespace Slic3r::FileReader +{ +// Loading model from a file, it may be a simple geometry file as STL or OBJ, however it may be a project file as well. +static Model read_model_from_file(const std::string& input_file, LoadAttributes options) +{ + Model model; + + DynamicPrintConfig temp_config; + ConfigSubstitutionContext temp_config_substitutions_context(ForwardCompatibilitySubstitutionRule::EnableSilent); + + bool result = false; + if (boost::algorithm::iends_with(input_file, ".stl")) + result = load_stl(input_file.c_str(), &model); + else if (boost::algorithm::iends_with(input_file, ".obj")) + result = load_obj(input_file.c_str(), &model); + else if (boost::algorithm::iends_with(input_file, ".step") || boost::algorithm::iends_with(input_file, ".stp")) + result = load_step(input_file.c_str(), &model); + else if (boost::algorithm::iends_with(input_file, ".amf") || boost::algorithm::iends_with(input_file, ".amf.xml")) +//? result = load_amf(input_file.c_str(), &temp_config, &temp_config_substitutions_context, &model, options & LoadAttribute::CheckVersion); +//? LoadAttribute::CheckVersion is needed here, when we loading just a geometry + result = load_amf(input_file.c_str(), &temp_config, &temp_config_substitutions_context, &model, false); + else if (boost::algorithm::iends_with(input_file, ".3mf") || boost::algorithm::iends_with(input_file, ".zip")) { + //FIXME options & LoadAttribute::CheckVersion ? + boost::optional prusaslicer_generator_version; + result = load_3mf(input_file.c_str(), temp_config, temp_config_substitutions_context, &model, false, prusaslicer_generator_version); + } else if (boost::algorithm::iends_with(input_file, ".svg")) + result = load_svg(input_file, model); + else if (boost::ends_with(input_file, ".printRequest")) + result = load_printRequest(input_file.c_str(), &model); + else + throw Slic3r::RuntimeError(L("Unknown file format. Input file must have .stl, .obj, .step/.stp, .svg, .amf(.xml) or extension .3mf(.zip).")); + + if (!result) + throw Slic3r::RuntimeError(L("Loading of a model file failed.")); + + if (model.objects.empty()) + throw Slic3r::RuntimeError(L("The supplied file couldn't be read because it's empty")); + + if (!boost::ends_with(input_file, ".printRequest")) + for (ModelObject* o : model.objects) + o->input_file = input_file; + + if (options & LoadAttribute::AddDefaultInstances) + model.add_default_instances(); + + return model; +} + +// Loading model from a file, it may be a simple geometry file as STL or OBJ, however it may be a project file as well. +static Model read_all_from_file(const std::string& input_file, + DynamicPrintConfig* config, + ConfigSubstitutionContext* config_substitutions, + boost::optional &prusaslicer_generator_version, + LoadAttributes options) +{ + const bool is_project_file = boost::algorithm::iends_with(input_file, ".3mf") || boost::algorithm::iends_with(input_file, ".zip"); + assert(is_project_file); + assert(config != nullptr); + assert(config_substitutions != nullptr); + + Model model; + + bool result = false; + if (is_project_file) + result = load_3mf(input_file.c_str(), *config, *config_substitutions, &model, options & LoadAttribute::CheckVersion, prusaslicer_generator_version); + else + throw Slic3r::RuntimeError(L("Unknown file format. Input file must have .3mf extension.")); + + if (!result) + throw Slic3r::RuntimeError(L("Loading of a model file failed.")); + + if (model.objects.empty()) + throw Slic3r::RuntimeError(L("The supplied file couldn't be read because it's empty")); + + for (ModelObject* o : model.objects) + o->input_file = input_file; + + if (options & LoadAttribute::AddDefaultInstances) + model.add_default_instances(); + + for (CustomGCode::Info& info : model.get_custom_gcode_per_print_z_vector()) { + CustomGCode::update_custom_gcode_per_print_z_from_config(info, config); + CustomGCode::check_mode_for_custom_gcode_per_print_z(info); + } + sort_remove_duplicates(config_substitutions->substitutions); + return model; +} + +TriangleMesh load_mesh(const std::string& input_file) +{ + Model model; + try { + model = read_model_from_file(input_file, LoadAttribute::AddDefaultInstances); + } + catch (std::exception&) { + throw Slic3r::RuntimeError(L("Error! Invalid model")); + } + + return model.mesh(); +} + +static bool looks_like_multipart_object(const Model& model) +{ + if (model.objects.size() <= 1) + return false; + + BoundingBoxf3 tbb; + + for (const ModelObject* obj : model.objects) { + if (obj->volumes.size() > 1 || obj->config.keys().size() > 1) + return false; + + BoundingBoxf3 bb_this = obj->volumes[0]->mesh().bounding_box(); + + // FIXME: There is sadly the case when instances are empty (AMF files). The normalization of instances in that + // case is performed only after this function is called. For now (shortly before the 2.7.2 release, let's + // just do this non-invasive check. Reordering all the functions could break it much more. + BoundingBoxf3 tbb_this = (!obj->instances.empty() ? obj->instances[0]->transform_bounding_box(bb_this) : bb_this); + + if (!tbb.defined) + tbb = tbb_this; + else if (tbb.intersects(tbb_this) || tbb.shares_boundary(tbb_this)) + // The volumes has intersects bounding boxes or share some boundary + return true; + } + return false; +} + +static bool looks_like_imperial_units(const Model& model) +{ + if (model.objects.empty()) + return false; + + for (ModelObject* obj : model.objects) + if (ModelProcessing::get_object_mesh_stats(obj).volume < ModelProcessing::volume_threshold_inches) { + if (!obj->is_cut()) + return true; + bool all_cut_parts_look_like_imperial_units = true; + for (ModelObject* obj_other : model.objects) { + if (obj_other == obj) + continue; + if (obj_other->cut_id.is_equal(obj->cut_id) && ModelProcessing::get_object_mesh_stats(obj_other).volume >= ModelProcessing::volume_threshold_inches) { + all_cut_parts_look_like_imperial_units = false; + break; + } + } + if (all_cut_parts_look_like_imperial_units) + return true; + } + + return false; +} + +static bool looks_like_saved_in_meters(const Model& model) +{ + if (model.objects.size() == 0) + return false; + + for (ModelObject* obj : model.objects) + if (ModelProcessing::get_object_mesh_stats(obj).volume < ModelProcessing::volume_threshold_meters) + return true; + + return false; +} + +static constexpr const double zero_volume = 0.0000000001; + +static int removed_objects_with_zero_volume(Model& model) +{ + if (model.objects.size() == 0) + return 0; + + int removed = 0; + for (int i = int(model.objects.size()) - 1; i >= 0; i--) + if (ModelProcessing::get_object_mesh_stats(model.objects[i]).volume < zero_volume) { + model.delete_object(size_t(i)); + removed++; + } + return removed; +} + +Model load_model(const std::string& input_file, + LoadAttributes options/* = LoadAttribute::AddDefaultInstances*/, + LoadStats* stats/*= nullptr*/) +{ + Model model = read_model_from_file(input_file, options); + + for (auto obj : model.objects) + if (obj->name.empty()) + obj->name = boost::filesystem::path(obj->input_file).filename().string(); + + if (stats) { + // 3mf contains information about units, so there is no need to detect possible convertions for these files + bool from_3mf = boost::algorithm::iends_with(input_file, ".3mf") || boost::algorithm::iends_with(input_file, ".zip"); + + stats->deleted_objects_cnt = removed_objects_with_zero_volume(model); + stats->looks_like_multipart_object = looks_like_multipart_object(model); + stats->looks_like_saved_in_meters = !from_3mf && looks_like_saved_in_meters(model); + stats->looks_like_imperial_units = !from_3mf && looks_like_imperial_units(model); + } + + return model; +} + +Model load_model_with_config(const std::string& input_file, + DynamicPrintConfig* config, + ConfigSubstitutionContext* config_substitutions, + boost::optional& prusaslicer_generator_version, + LoadAttributes options, + LoadStats* stats) +{ + Model model = read_all_from_file(input_file, config, config_substitutions, prusaslicer_generator_version, options); + + if (stats && !model.mesh().empty()) { + stats->deleted_objects_cnt = removed_objects_with_zero_volume(model); + stats->looks_like_multipart_object = looks_like_multipart_object(model); + } + + return model; +} + +} diff --git a/src/libslic3r/FileReader.hpp b/src/libslic3r/FileReader.hpp new file mode 100644 index 0000000000..ab3cb63132 --- /dev/null +++ b/src/libslic3r/FileReader.hpp @@ -0,0 +1,61 @@ +///|/ Copyright (c) Prusa Research 2016 - 2023 Tomáš Mészáros @tamasmeszaros, Oleksandra Iushchenko @YuSanka, Enrico Turri @enricoturri1966, Lukáš Matěna @lukasmatena, Vojtěch Bubník @bubnikv, Filip Sykala @Jony01, Lukáš Hejl @hejllukas, David Kocík @kocikdav, Vojtěch Král @vojtechkral +///|/ Copyright (c) 2019 John Drake @foxox +///|/ Copyright (c) 2019 Sijmen Schoon +///|/ Copyright (c) 2017 Eyal Soha @eyal0 +///|/ Copyright (c) Slic3r 2014 - 2015 Alessandro Ranellucci @alranel +///|/ +///|/ ported from lib/Slic3r/Model.pm: +///|/ Copyright (c) Prusa Research 2016 - 2022 Vojtěch Bubník @bubnikv, Enrico Turri @enricoturri1966 +///|/ Copyright (c) Slic3r 2012 - 2016 Alessandro Ranellucci @alranel +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ +#pragma once + +#include "PrintConfig.hpp" +#include "enum_bitmask.hpp" + +namespace Slic3r { + +class Model; +class TriangleMesh; + +namespace FileReader +{ + enum class LoadAttribute : int { + AddDefaultInstances, + CheckVersion + }; + using LoadAttributes = enum_bitmask; + + struct LoadStats { + int deleted_objects_cnt { 0 }; + bool looks_like_saved_in_meters { false }; + bool looks_like_imperial_units { false }; + bool looks_like_multipart_object { false }; + }; + + // Load model from input file and return the its mesh. + // Throw RuntimeError if some problem was detected during model loading + TriangleMesh load_mesh(const std::string& input_file); + + // Load model from input file and fill statistics if it's required. + // In respect to the params will be applied needed convertions over the model. + // Exceptions don't catched inside + Model load_model(const std::string& input_file, + LoadAttributes options = LoadAttribute::AddDefaultInstances, + LoadStats* statistics = nullptr); + + // Load model, config and config substitutions from input file and fill statistics if it's required. + // Exceptions don't catched inside + Model load_model_with_config(const std::string& input_file, + DynamicPrintConfig* config, + ConfigSubstitutionContext* config_substitutions, + boost::optional &prusaslicer_generator_version, + LoadAttributes options, + LoadStats* statistics = nullptr); +} + +ENABLE_ENUM_BITMASK_OPERATORS(FileReader::LoadAttribute) + +} // namespace Slic3r::ModelProcessing diff --git a/src/libslic3r/Model.cpp b/src/libslic3r/Model.cpp index d843ebaa92..e435940b2f 100644 --- a/src/libslic3r/Model.cpp +++ b/src/libslic3r/Model.cpp @@ -15,21 +15,13 @@ #include "libslic3r.h" #include "BuildVolume.hpp" #include "Exception.hpp" -#include "Model.hpp" + #include "Geometry/ConvexHull.hpp" #include "MTUtils.hpp" #include "TriangleMeshSlicer.hpp" #include "TriangleSelector.hpp" #include "MultipleBeds.hpp" -#include "Format/AMF.hpp" -#include "Format/OBJ.hpp" -#include "Format/STL.hpp" -#include "Format/3mf.hpp" -#include "Format/STEP.hpp" -#include "Format/SVG.hpp" -#include "Format/PrintRequest.hpp" - #include #include @@ -148,109 +140,6 @@ const CustomGCode::Info& Model::custom_gcode_per_print_z() const { return custom_gcode_per_print_z_vector[s_multiple_beds.get_active_bed()]; } - - -// Loading model from a file, it may be a simple geometry file as STL or OBJ, however it may be a project file as well. -Model Model::read_from_file(const std::string& input_file, DynamicPrintConfig* config, ConfigSubstitutionContext* config_substitutions, LoadAttributes options) -{ - Model model; - - DynamicPrintConfig temp_config; - ConfigSubstitutionContext temp_config_substitutions_context(ForwardCompatibilitySubstitutionRule::EnableSilent); - if (config == nullptr) - config = &temp_config; - if (config_substitutions == nullptr) - config_substitutions = &temp_config_substitutions_context; - - bool result = false; - if (boost::algorithm::iends_with(input_file, ".stl")) - result = load_stl(input_file.c_str(), &model); - else if (boost::algorithm::iends_with(input_file, ".obj")) - result = load_obj(input_file.c_str(), &model); - else if (boost::algorithm::iends_with(input_file, ".step") || boost::algorithm::iends_with(input_file, ".stp")) - result = load_step(input_file.c_str(), &model); - else if (boost::algorithm::iends_with(input_file, ".amf") || boost::algorithm::iends_with(input_file, ".amf.xml")) - result = load_amf(input_file.c_str(), config, config_substitutions, &model, options & LoadAttribute::CheckVersion); - else if (boost::algorithm::iends_with(input_file, ".3mf") || boost::algorithm::iends_with(input_file, ".zip")) { - //FIXME options & LoadAttribute::CheckVersion ? - boost::optional prusaslicer_generator_version; - result = load_3mf(input_file.c_str(), *config, *config_substitutions, &model, false, prusaslicer_generator_version); - } else if (boost::algorithm::iends_with(input_file, ".svg")) - result = load_svg(input_file, model); - else if (boost::ends_with(input_file, ".printRequest")) - result = load_printRequest(input_file.c_str(), &model); - else - throw Slic3r::RuntimeError("Unknown file format. Input file must have .stl, .obj, .step/.stp, .svg, .amf(.xml) or extension .3mf(.zip)."); - - if (! result) - throw Slic3r::RuntimeError("Loading of a model file failed."); - - if (model.objects.empty()) - throw Slic3r::RuntimeError("The supplied file couldn't be read because it's empty"); - - if (!boost::ends_with(input_file, ".printRequest")) - for (ModelObject *o : model.objects) - o->input_file = input_file; - - if (options & LoadAttribute::AddDefaultInstances) - model.add_default_instances(); - - for (CustomGCode::Info& info : model.custom_gcode_per_print_z_vector) { - CustomGCode::update_custom_gcode_per_print_z_from_config(info, config); - CustomGCode::check_mode_for_custom_gcode_per_print_z(info); - } - - sort_remove_duplicates(config_substitutions->substitutions); - return model; -} - -// Loading model from a file (3MF or AMF), not from a simple geometry file (STL or OBJ). -Model Model::read_from_archive( - const std::string& input_file, - DynamicPrintConfig* config, - ConfigSubstitutionContext* config_substitutions, - boost::optional &prusaslicer_generator_version, - LoadAttributes options -) { - assert(config != nullptr); - assert(config_substitutions != nullptr); - - Model model; - - bool result = false; - if (boost::algorithm::iends_with(input_file, ".3mf") || boost::algorithm::iends_with(input_file, ".zip")) { - result = load_3mf(input_file.c_str(), *config, *config_substitutions, &model, options & LoadAttribute::CheckVersion, prusaslicer_generator_version); - } else if (boost::algorithm::iends_with(input_file, ".zip.amf")) - result = load_amf(input_file.c_str(), config, config_substitutions, &model, options & LoadAttribute::CheckVersion); - else - throw Slic3r::RuntimeError("Unknown file format. Input file must have .3mf or .zip.amf extension."); - - if (!result) - throw Slic3r::RuntimeError("Loading of a model file failed."); - - for (ModelObject *o : model.objects) { -// if (boost::algorithm::iends_with(input_file, ".zip.amf")) -// { -// // we remove the .zip part of the extension to avoid it be added to filenames when exporting -// o->input_file = boost::ireplace_last_copy(input_file, ".zip.", "."); -// } -// else - o->input_file = input_file; - } - - if (options & LoadAttribute::AddDefaultInstances) - model.add_default_instances(); - - for (CustomGCode::Info& info : model.custom_gcode_per_print_z_vector) { - CustomGCode::update_custom_gcode_per_print_z_from_config(info, config); - CustomGCode::check_mode_for_custom_gcode_per_print_z(info); - } - - handle_legacy_sla(*config); - - return model; -} - ModelObject* Model::add_object() { this->objects.emplace_back(new ModelObject(this)); @@ -472,169 +361,6 @@ void Model::duplicate_objects_grid(size_t x, size_t y, coordf_t dist) } } -bool Model::looks_like_multipart_object() const -{ - if (this->objects.size() <= 1) - return false; - - BoundingBoxf3 tbb; - - for (const ModelObject *obj : this->objects) { - if (obj->volumes.size() > 1 || obj->config.keys().size() > 1) - return false; - - BoundingBoxf3 bb_this = obj->volumes[0]->mesh().bounding_box(); - - // FIXME: There is sadly the case when instances are empty (AMF files). The normalization of instances in that - // case is performed only after this function is called. For now (shortly before the 2.7.2 release, let's - // just do this non-invasive check. Reordering all the functions could break it much more. - BoundingBoxf3 tbb_this = (! obj->instances.empty() ? obj->instances[0]->transform_bounding_box(bb_this) : bb_this); - - if (!tbb.defined) - tbb = tbb_this; - else if (tbb.intersects(tbb_this) || tbb.shares_boundary(tbb_this)) - // The volumes has intersects bounding boxes or share some boundary - return true; - } - return false; -} - -// Generate next extruder ID string, in the range of (1, max_extruders). -static inline int auto_extruder_id(unsigned int max_extruders, unsigned int &cntr) -{ - int out = ++ cntr; - if (cntr == max_extruders) - cntr = 0; - return out; -} - -void Model::convert_multipart_object(unsigned int max_extruders) -{ - assert(this->objects.size() >= 2); - if (this->objects.size() < 2) - return; - - ModelObject* object = new ModelObject(this); - object->input_file = this->objects.front()->input_file; - object->name = boost::filesystem::path(this->objects.front()->input_file).stem().string(); - //FIXME copy the config etc? - - unsigned int extruder_counter = 0; - for (const ModelObject* o : this->objects) - for (const ModelVolume* v : o->volumes) { - // If there are more than one object, put all volumes together - // Each object may contain any number of volumes and instances - // The volumes transformations are relative to the object containing them... - Geometry::Transformation trafo_volume = v->get_transformation(); - // Revert the centering operation. - trafo_volume.set_offset(trafo_volume.get_offset() - o->origin_translation); - int counter = 1; - auto copy_volume = [o, max_extruders, &counter, &extruder_counter](ModelVolume *new_v) { - assert(new_v != nullptr); - new_v->name = (counter > 1) ? o->name + "_" + std::to_string(counter++) : o->name; - new_v->config.set("extruder", auto_extruder_id(max_extruders, extruder_counter)); - return new_v; - }; - if (o->instances.empty()) { - copy_volume(object->add_volume(*v))->set_transformation(trafo_volume); - } else { - for (const ModelInstance* i : o->instances) - // ...so, transform everything to a common reference system (world) - copy_volume(object->add_volume(*v))->set_transformation(i->get_transformation() * trafo_volume); - } - } - - // commented-out to fix #2868 -// object->add_instance(); -// object->instances[0]->set_offset(object->raw_mesh_bounding_box().center()); - - this->clear_objects(); - this->objects.push_back(object); -} - -static constexpr const double volume_threshold_inches = 9.0; // 9 = 3*3*3; - -bool Model::looks_like_imperial_units() const -{ - if (this->objects.empty()) - return false; - - for (ModelObject* obj : this->objects) - if (obj->get_object_stl_stats().volume < volume_threshold_inches) { - if (!obj->is_cut()) - return true; - bool all_cut_parts_look_like_imperial_units = true; - for (ModelObject* obj_other : this->objects) { - if (obj_other == obj) - continue; - if (obj_other->cut_id.is_equal(obj->cut_id) && obj_other->get_object_stl_stats().volume >= volume_threshold_inches) { - all_cut_parts_look_like_imperial_units = false; - break; - } - } - if (all_cut_parts_look_like_imperial_units) - return true; - } - - return false; -} - -void Model::convert_from_imperial_units(bool only_small_volumes) -{ - static constexpr const float in_to_mm = 25.4f; - for (ModelObject* obj : this->objects) - if (! only_small_volumes || obj->get_object_stl_stats().volume < volume_threshold_inches) { - obj->scale_mesh_after_creation(in_to_mm); - for (ModelVolume* v : obj->volumes) { - assert(! v->source.is_converted_from_meters); - v->source.is_converted_from_inches = true; - } - } -} - -static constexpr const double volume_threshold_meters = 0.001; // 0.001 = 0.1*0.1*0.1 - -bool Model::looks_like_saved_in_meters() const -{ - if (this->objects.size() == 0) - return false; - - for (ModelObject* obj : this->objects) - if (obj->get_object_stl_stats().volume < volume_threshold_meters) - return true; - - return false; -} - -void Model::convert_from_meters(bool only_small_volumes) -{ - static constexpr const double m_to_mm = 1000; - for (ModelObject* obj : this->objects) - if (! only_small_volumes || obj->get_object_stl_stats().volume < volume_threshold_meters) { - obj->scale_mesh_after_creation(m_to_mm); - for (ModelVolume* v : obj->volumes) { - assert(! v->source.is_converted_from_inches); - v->source.is_converted_from_meters = true; - } - } -} - -static constexpr const double zero_volume = 0.0000000001; - -int Model::removed_objects_with_zero_volume() -{ - if (objects.size() == 0) - return 0; - - int removed = 0; - for (int i = int(objects.size()) - 1; i >= 0; i--) - if (objects[i]->get_object_stl_stats().volume < zero_volume) { - delete_object(size_t(i)); - removed++; - } - return removed; -} - void Model::adjust_min_z() { if (objects.empty()) @@ -833,6 +559,13 @@ ModelVolume* ModelObject::add_volume(const ModelVolume &other, TriangleMesh &&me return v; } +ModelVolume* ModelObject::insert_volume(size_t idx, const ModelVolume& other, TriangleMesh&& mesh) +{ + ModelVolume* v = new ModelVolume(this, other, std::move(mesh)); + this->volumes.insert(this->volumes.begin() + idx, v); + return v; +} + void ModelObject::delete_volume(size_t idx) { ModelVolumePtrs::iterator i = this->volumes.begin() + idx; @@ -1264,72 +997,6 @@ void ModelObject::scale_mesh_after_creation(const float scale) this->invalidate_bounding_box(); } -void ModelObject::convert_units(ModelObjectPtrs& new_objects, ConversionType conv_type, std::vector volume_idxs) -{ - BOOST_LOG_TRIVIAL(trace) << "ModelObject::convert_units - start"; - - ModelObject* new_object = new_clone(*this); - - float koef = conv_type == ConversionType::CONV_FROM_INCH ? 25.4f : conv_type == ConversionType::CONV_TO_INCH ? 0.0393700787f : - conv_type == ConversionType::CONV_FROM_METER ? 1000.f : conv_type == ConversionType::CONV_TO_METER ? 0.001f : 1.f; - - new_object->set_model(nullptr); - new_object->sla_support_points.clear(); - new_object->sla_drain_holes.clear(); - new_object->sla_points_status = sla::PointsStatus::NoPoints; - new_object->clear_volumes(); - new_object->input_file.clear(); - - int vol_idx = 0; - for (ModelVolume* volume : volumes) { - if (!volume->mesh().empty()) { - TriangleMesh mesh(volume->mesh()); - - ModelVolume* vol = new_object->add_volume(mesh); - vol->name = volume->name; - vol->set_type(volume->type()); - // Don't copy the config's ID. - vol->config.assign_config(volume->config); - assert(vol->config.id().valid()); - assert(vol->config.id() != volume->config.id()); - vol->set_material(volume->material_id(), *volume->material()); - vol->source.input_file = volume->source.input_file; - vol->source.object_idx = (int)new_objects.size(); - vol->source.volume_idx = vol_idx; - vol->source.is_converted_from_inches = volume->source.is_converted_from_inches; - vol->source.is_converted_from_meters = volume->source.is_converted_from_meters; - vol->source.is_from_builtin_objects = volume->source.is_from_builtin_objects; - - vol->supported_facets.assign(volume->supported_facets); - vol->seam_facets.assign(volume->seam_facets); - vol->mm_segmentation_facets.assign(volume->mm_segmentation_facets); - vol->fuzzy_skin_facets.assign(volume->fuzzy_skin_facets); - - // Perform conversion only if the target "imperial" state is different from the current one. - // This check supports conversion of "mixed" set of volumes, each with different "imperial" state. - if (//vol->source.is_converted_from_inches != from_imperial && - (volume_idxs.empty() || - std::find(volume_idxs.begin(), volume_idxs.end(), vol_idx) != volume_idxs.end())) { - vol->scale_geometry_after_creation(koef); - vol->set_offset(Vec3d(koef, koef, koef).cwiseProduct(volume->get_offset())); - if (conv_type == ConversionType::CONV_FROM_INCH || conv_type == ConversionType::CONV_TO_INCH) - vol->source.is_converted_from_inches = conv_type == ConversionType::CONV_FROM_INCH; - if (conv_type == ConversionType::CONV_FROM_METER || conv_type == ConversionType::CONV_TO_METER) - vol->source.is_converted_from_meters = conv_type == ConversionType::CONV_FROM_METER; - assert(! vol->source.is_converted_from_inches || ! vol->source.is_converted_from_meters); - } - else - vol->set_offset(volume->get_offset()); - } - vol_idx ++; - } - new_object->invalidate_bounding_box(); - - new_objects.push_back(new_object); - - BOOST_LOG_TRIVIAL(trace) << "ModelObject::convert_units - end"; -} - size_t ModelObject::materials_count() const { std::set material_ids; @@ -1420,111 +1087,6 @@ void ModelVolume::reset_extra_facets() } -/// -/// Compare TriangleMeshes by Bounding boxes (mainly for sort) -/// From Front(Z) Upper(Y) TopLeft(X) corner. -/// 1. Seraparate group not overlaped i Z axis -/// 2. Seraparate group not overlaped i Y axis -/// 3. Start earlier in X (More on left side) -/// -/// Compare from -/// Compare to -/// True when triangle mesh 1 is closer, upper or lefter than triangle mesh 2 other wise false -static bool is_front_up_left(const TriangleMesh &trinagle_mesh1, const TriangleMesh &triangle_mesh2) -{ - // stats form t1 - const Vec3f &min1 = trinagle_mesh1.stats().min; - const Vec3f &max1 = trinagle_mesh1.stats().max; - // stats from t2 - const Vec3f &min2 = triangle_mesh2.stats().min; - const Vec3f &max2 = triangle_mesh2.stats().max; - // priority Z, Y, X - for (int axe = 2; axe > 0; --axe) { - if (max1[axe] < min2[axe]) - return true; - if (min1[axe] > max2[axe]) - return false; - } - return min1.x() < min2.x(); -} - -void ModelObject::split(ModelObjectPtrs* new_objects) -{ - for (ModelVolume* volume : this->volumes) { - if (volume->type() != ModelVolumeType::MODEL_PART) - continue; - - // splited volume should not be text object - if (volume->text_configuration.has_value()) - volume->text_configuration.reset(); - - std::vector meshes = volume->mesh().split(); - std::sort(meshes.begin(), meshes.end(), is_front_up_left); - - size_t counter = 1; - for (TriangleMesh &mesh : meshes) { - // FIXME: crashes if not satisfied - if (mesh.facets_count() < 3 || mesh.has_zero_volume()) - continue; - - // XXX: this seems to be the only real usage of m_model, maybe refactor this so that it's not needed? - ModelObject* new_object = m_model->add_object(); - if (meshes.size() == 1) { - new_object->name = volume->name; - // Don't copy the config's ID. - new_object->config.assign_config(this->config.size() > 0 ? this->config : volume->config); - } - else { - new_object->name = this->name + (meshes.size() > 1 ? "_" + std::to_string(counter++) : ""); - // Don't copy the config's ID. - new_object->config.assign_config(this->config); - } - assert(new_object->config.id().valid()); - assert(new_object->config.id() != this->config.id()); - new_object->instances.reserve(this->instances.size()); - for (const ModelInstance* model_instance : this->instances) - new_object->add_instance(*model_instance); - ModelVolume* new_vol = new_object->add_volume(*volume, std::move(mesh)); - - // Invalidate extruder value in volume's config, - // otherwise there will no way to change extruder for object after splitting, - // because volume's extruder value overrides object's extruder value. - if (new_vol->config.has("extruder")) - new_vol->config.set_key_value("extruder", new ConfigOptionInt(0)); - - for (ModelInstance* model_instance : new_object->instances) { - const Vec3d shift = model_instance->get_transformation().get_matrix_no_offset() * new_vol->get_offset(); - model_instance->set_offset(model_instance->get_offset() + shift); - } - - new_vol->set_offset(Vec3d::Zero()); - // reset the source to disable reload from disk - new_vol->source = ModelVolume::Source(); - new_objects->emplace_back(new_object); - } - } -} - - -void ModelObject::merge() -{ - if (this->volumes.size() == 1) { - // We can't merge meshes if there's just one volume - return; - } - - TriangleMesh mesh; - - for (ModelVolume* volume : volumes) - if (!volume->mesh().empty()) - mesh.merge(volume->mesh()); - - this->clear_volumes(); - ModelVolume* vol = this->add_volume(mesh); - - if (!vol) - return; -} // Support for non-uniform scaling of instances. If an instance is rotated by angles, which are not multiples of ninety degrees, // then the scaling in world coordinate system is not representable by the Geometry::Transformation structure. @@ -1733,42 +1295,6 @@ std::string ModelObject::get_export_filename() const return ret; } -TriangleMeshStats ModelObject::get_object_stl_stats() const -{ - TriangleMeshStats full_stats; - full_stats.volume = 0.f; - - // fill full_stats from all objet's meshes - for (ModelVolume* volume : this->volumes) - { - const TriangleMeshStats& stats = volume->mesh().stats(); - - // initialize full_stats (for repaired errors) - full_stats.open_edges += stats.open_edges; - full_stats.repaired_errors.merge(stats.repaired_errors); - - // another used satistics value - if (volume->is_model_part()) { - Transform3d trans = instances.empty() ? volume->get_matrix() : (volume->get_matrix() * instances[0]->get_matrix()); - full_stats.volume += stats.volume * std::fabs(trans.matrix().block(0, 0, 3, 3).determinant()); - full_stats.number_of_parts += stats.number_of_parts; - } - } - - return full_stats; -} - -int ModelObject::get_repaired_errors_count(const int vol_idx /*= -1*/) const -{ - if (vol_idx >= 0) - return this->volumes[vol_idx]->get_repaired_errors_count(); - - const RepairedMeshErrors& stats = get_object_stl_stats().repaired_errors; - - return stats.degenerate_facets + stats.edges_fixed + stats.facets_removed + - stats.facets_reversed + stats.backwards_edges; -} - bool ModelObject::has_solid_mesh() const { for (const ModelVolume* volume : volumes) @@ -1849,14 +1375,6 @@ void ModelVolume::calculate_convex_hull() assert(m_convex_hull.get()); } -int ModelVolume::get_repaired_errors_count() const -{ - const RepairedMeshErrors &stats = this->mesh().stats().repaired_errors; - - return stats.degenerate_facets + stats.edges_fixed + stats.facets_removed + - stats.facets_reversed + stats.backwards_edges; -} - const TriangleMesh& ModelVolume::get_convex_hull() const { return *m_convex_hull.get(); @@ -1897,68 +1415,6 @@ std::string ModelVolume::type_to_string(const ModelVolumeType t) } } -// Split this volume, append the result to the object owning this volume. -// Return the number of volumes created from this one. -// This is useful to assign different materials to different volumes of an object. -size_t ModelVolume::split(unsigned int max_extruders) -{ - std::vector meshes = this->mesh().split(); - if (meshes.size() <= 1) - return 1; - - std::sort(meshes.begin(), meshes.end(), is_front_up_left); - - // splited volume should not be text object - if (text_configuration.has_value()) - text_configuration.reset(); - - size_t idx = 0; - size_t ivolume = std::find(this->object->volumes.begin(), this->object->volumes.end(), this) - this->object->volumes.begin(); - const std::string& name = this->name; - - unsigned int extruder_counter = 0; - const Vec3d offset = this->get_offset(); - - for (TriangleMesh &mesh : meshes) { - if (mesh.empty() || mesh.has_zero_volume()) - // Repair may have removed unconnected triangles, thus emptying the mesh. - continue; - - if (idx == 0) { - this->set_mesh(std::move(mesh)); - this->calculate_convex_hull(); - // Assign a new unique ID, so that a new GLVolume will be generated. - this->set_new_unique_id(); - // reset the source to disable reload from disk - this->source = ModelVolume::Source(); - } - else - this->object->volumes.insert(this->object->volumes.begin() + (++ivolume), new ModelVolume(object, *this, std::move(mesh))); - - this->object->volumes[ivolume]->set_offset(Vec3d::Zero()); - this->object->volumes[ivolume]->center_geometry_after_creation(); - this->object->volumes[ivolume]->translate(offset); - this->object->volumes[ivolume]->name = name + "_" + std::to_string(idx + 1); - this->object->volumes[ivolume]->config.set("extruder", auto_extruder_id(max_extruders, extruder_counter)); - this->object->volumes[ivolume]->m_is_splittable = 0; - ++ idx; - } - - // discard volumes for which the convex hull was not generated or is degenerate - size_t i = 0; - while (i < this->object->volumes.size()) { - const std::shared_ptr &hull = this->object->volumes[i]->get_convex_hull_shared_ptr(); - if (hull == nullptr || hull->its.vertices.empty() || hull->its.indices.empty()) { - this->object->delete_volume(i); - --idx; - --i; - } - ++i; - } - - return idx; -} - void ModelVolume::translate(const Vec3d& displacement) { set_offset(get_offset() + displacement); @@ -2052,22 +1508,6 @@ void ModelVolume::transform_this_mesh(const Matrix3d &matrix, bool fix_left_hand this->set_new_unique_id(); } -void ModelVolume::convert_from_imperial_units() -{ - assert(! this->source.is_converted_from_meters); - this->scale_geometry_after_creation(25.4f); - this->set_offset(Vec3d(0, 0, 0)); - this->source.is_converted_from_inches = true; -} - -void ModelVolume::convert_from_meters() -{ - assert(! this->source.is_converted_from_inches); - this->scale_geometry_after_creation(1000.f); - this->set_offset(Vec3d(0, 0, 0)); - this->source.is_converted_from_meters = true; -} - std::vector ModelVolume::get_extruders_from_multi_material_painting() const { if (!this->is_mm_painted()) return {}; diff --git a/src/libslic3r/Model.hpp b/src/libslic3r/Model.hpp index 62e4312f85..bd0127791e 100644 --- a/src/libslic3r/Model.hpp +++ b/src/libslic3r/Model.hpp @@ -14,17 +14,14 @@ #define slic3r_Model_hpp_ #include "libslic3r.h" -#include "enum_bitmask.hpp" #include "Geometry.hpp" #include "ObjectID.hpp" #include "Point.hpp" -#include "PrintConfig.hpp" #include "Slicing.hpp" #include "SLA/SupportPoint.hpp" #include "SLA/Hollowing.hpp" #include "TriangleMesh.hpp" #include "CustomGCode.hpp" -#include "enum_bitmask.hpp" #include "TextConfiguration.hpp" #include "EmbossShape.hpp" #include "TriangleSelector.hpp" @@ -47,7 +44,6 @@ namespace cereal { } namespace Slic3r { -enum class ConversionType; class BuildVolume; class Model; @@ -424,6 +420,7 @@ public: ModelVolume* add_volume(TriangleMesh &&mesh, ModelVolumeType type = ModelVolumeType::MODEL_PART); ModelVolume* add_volume(const ModelVolume &volume, ModelVolumeType type = ModelVolumeType::INVALID); ModelVolume* add_volume(const ModelVolume &volume, TriangleMesh &&mesh); + ModelVolume* insert_volume(size_t idx, const ModelVolume &volume, TriangleMesh &&mesh); void delete_volume(size_t idx); void clear_volumes(); void sort_volumes(bool full_sort); @@ -509,7 +506,6 @@ public: // This method could only be called before the meshes of this ModelVolumes are not shared! void scale_mesh_after_creation(const float scale); - void convert_units(ModelObjectPtrs&new_objects, ConversionType conv_type, std::vector volume_idxs); size_t materials_count() const; size_t facets_count() const; @@ -519,9 +515,6 @@ public: // delete volumes which are marked as connector for this object void delete_connectors(); void clone_for_cut(ModelObject **obj); - - void split(ModelObjectPtrs*new_objects); - void merge(); // Support for non-uniform scaling of instances. If an instance is rotated by angles, which are not multiples of ninety degrees, // then the scaling in world coordinate system is not representable by the Geometry::Transformation structure. // This situation is solved by baking in the instance transformation into the mesh vertices. @@ -536,11 +529,6 @@ public: std::string get_export_filename() const; - // Get full stl statistics for all object's meshes - TriangleMeshStats get_object_stl_stats() const; - // Get count of errors in the mesh( or all object's meshes, if volume index isn't defined) - int get_repaired_errors_count(const int vol_idx = -1) const; - // Detect if object has at least one solid mash bool has_solid_mesh() const; // Detect if object has at least one negative volume mash @@ -697,13 +685,6 @@ private: void update_min_max_z(); }; -enum class ConversionType : int { - CONV_TO_INCH, - CONV_FROM_INCH, - CONV_TO_METER, - CONV_FROM_METER, -}; - class FacetsAnnotation final : public ObjectWithTimestamp { public: // Assign the content if the timestamp differs, don't assign an ObjectID. @@ -879,11 +860,8 @@ public: int extruder_id() const; bool is_splittable() const; + void discard_splittable() { m_is_splittable = 0; } - // Split this volume, append the result to the object owning this volume. - // Return the number of volumes created from this one. - // This is useful to assign different materials to different volumes of an object. - size_t split(unsigned int max_extruders); void translate(double x, double y, double z) { translate(Vec3d(x, y, z)); } void translate(const Vec3d& displacement); void scale(const Vec3d& scaling_factors); @@ -904,8 +882,6 @@ public: void calculate_convex_hull(); const TriangleMesh& get_convex_hull() const; const std::shared_ptr& get_convex_hull_shared_ptr() const { return m_convex_hull; } - // Get count of errors in the mesh - int get_repaired_errors_count() const; // Helpers for loading / storing into AMF / 3MF files. static ModelVolumeType type_from_string(const std::string &s); @@ -940,8 +916,6 @@ public: void set_mirror(const Vec3d& mirror) { m_transformation.set_mirror(mirror); } void set_mirror(Axis axis, double mirror) { m_transformation.set_mirror(axis, mirror); } - void convert_from_imperial_units(); - void convert_from_meters(); const Transform3d& get_matrix() const { return m_transformation.get_matrix(); } Transform3d get_matrix_no_offset() const { return m_transformation.get_matrix_no_offset(); } @@ -1318,24 +1292,6 @@ public: OBJECTBASE_DERIVED_COPY_MOVE_CLONE(Model) - enum class LoadAttribute : int { - AddDefaultInstances, - CheckVersion - }; - using LoadAttributes = enum_bitmask; - - static Model read_from_file( - const std::string& input_file, - DynamicPrintConfig* config = nullptr, ConfigSubstitutionContext* config_substitutions = nullptr, - LoadAttributes options = LoadAttribute::AddDefaultInstances); - static Model read_from_archive( - const std::string& input_file, - DynamicPrintConfig* config, - ConfigSubstitutionContext* config_substitutions, - boost::optional &prusaslicer_generator_version, - LoadAttributes options = LoadAttribute::AddDefaultInstances - ); - // Add a new ModelObject to this Model, generate a new ID for this ModelObject. ModelObject* add_object(); ModelObject* add_object(const char *name, const char *path, const TriangleMesh &mesh); @@ -1373,14 +1329,6 @@ public: // Croaks if the duplicated objects do not fit the print bed. void duplicate_objects_grid(size_t x, size_t y, coordf_t dist); - bool looks_like_multipart_object() const; - void convert_multipart_object(unsigned int max_extruders); - bool looks_like_imperial_units() const; - void convert_from_imperial_units(bool only_small_volumes); - bool looks_like_saved_in_meters() const; - void convert_from_meters(bool only_small_volumes); - int removed_objects_with_zero_volume(); - // Ensures that the min z of the model is not negative void adjust_min_z(); @@ -1412,8 +1360,6 @@ private: } }; -ENABLE_ENUM_BITMASK_OPERATORS(Model::LoadAttribute) - #undef OBJECTBASE_DERIVED_COPY_MOVE_CLONE #undef OBJECTBASE_DERIVED_PRIVATE_COPY_MOVE diff --git a/src/libslic3r/ModelProcessing.cpp b/src/libslic3r/ModelProcessing.cpp new file mode 100644 index 0000000000..7fb2fc62a6 --- /dev/null +++ b/src/libslic3r/ModelProcessing.cpp @@ -0,0 +1,401 @@ +///|/ Copyright (c) Prusa Research 2016 - 2023 Tomáš Mészáros @tamasmeszaros, Oleksandra Iushchenko @YuSanka, David Kocík @kocikdav, Enrico Turri @enricoturri1966, Lukáš Matěna @lukasmatena, Vojtěch Bubník @bubnikv, Lukáš Hejl @hejllukas, Filip Sykala @Jony01, Vojtěch Král @vojtechkral +///|/ Copyright (c) 2021 Boleslaw Ciesielski +///|/ Copyright (c) 2019 John Drake @foxox +///|/ Copyright (c) 2019 Sijmen Schoon +///|/ Copyright (c) Slic3r 2014 - 2016 Alessandro Ranellucci @alranel +///|/ Copyright (c) 2015 Maksim Derbasov @ntfshard +///|/ +///|/ ported from lib/Slic3r/Model.pm: +///|/ Copyright (c) Prusa Research 2016 - 2022 Vojtěch Bubník @bubnikv, Enrico Turri @enricoturri1966 +///|/ Copyright (c) Slic3r 2012 - 2016 Alessandro Ranellucci @alranel +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ +#include "Model.hpp" +#include "ModelProcessing.hpp" + +#include +#include + +namespace Slic3r::ModelProcessing { + +// Generate next extruder ID string, in the range of (1, max_extruders). +static inline int auto_extruder_id(unsigned int max_extruders, unsigned int& cntr) +{ + int out = ++cntr; + if (cntr == max_extruders) + cntr = 0; + return out; +} + +void convert_to_multipart_object(Model& model, unsigned int max_extruders) +{ + assert(model.objects.size() >= 2); + if (model.objects.size() < 2) + return; + + Model tmp_model = Model(); + tmp_model.add_object(); + + ModelObject* object = tmp_model.objects[0]; + object->input_file = model.objects.front()->input_file; + object->name = boost::filesystem::path(model.objects.front()->input_file).stem().string(); + //FIXME copy the config etc? + + unsigned int extruder_counter = 0; + for (const ModelObject* o : model.objects) + for (const ModelVolume* v : o->volumes) { + // If there are more than one object, put all volumes together + // Each object may contain any number of volumes and instances + // The volumes transformations are relative to the object containing them... + Geometry::Transformation trafo_volume = v->get_transformation(); + // Revert the centering operation. + trafo_volume.set_offset(trafo_volume.get_offset() - o->origin_translation); + int counter = 1; + auto copy_volume = [o, max_extruders, &counter, &extruder_counter](ModelVolume* new_v) { + assert(new_v != nullptr); + new_v->name = (counter > 1) ? o->name + "_" + std::to_string(counter++) : o->name; + new_v->config.set("extruder", auto_extruder_id(max_extruders, extruder_counter)); + return new_v; + }; + if (o->instances.empty()) { + copy_volume(object->add_volume(*v))->set_transformation(trafo_volume); + } + else { + for (const ModelInstance* i : o->instances) + // ...so, transform everything to a common reference system (world) + copy_volume(object->add_volume(*v))->set_transformation(i->get_transformation() * trafo_volume); + } + } + + // commented-out to fix #2868 +// object->add_instance(); +// object->instances[0]->set_offset(object->raw_mesh_bounding_box().center()); + + model.clear_objects(); + model.add_object(*object); +} + +void convert_from_imperial_units(Model& model, bool only_small_volumes) +{ + static constexpr const float in_to_mm = 25.4f; + for (ModelObject* obj : model.objects) + if (!only_small_volumes || get_object_mesh_stats(obj).volume < volume_threshold_inches) { + obj->scale_mesh_after_creation(in_to_mm); + for (ModelVolume* v : obj->volumes) { + assert(!v->source.is_converted_from_meters); + v->source.is_converted_from_inches = true; + } + } +} + +void convert_from_imperial_units(ModelVolume* volume) +{ + assert(!volume->source.is_converted_from_meters); + volume->scale_geometry_after_creation(25.4f); + volume->set_offset(Vec3d(0, 0, 0)); + volume->source.is_converted_from_inches = true; +} + +void convert_from_meters(Model& model, bool only_small_volumes) +{ + static constexpr const double m_to_mm = 1000; + for (ModelObject* obj : model.objects) + if (!only_small_volumes || get_object_mesh_stats(obj).volume < volume_threshold_meters) { + obj->scale_mesh_after_creation(m_to_mm); + for (ModelVolume* v : obj->volumes) { + assert(!v->source.is_converted_from_inches); + v->source.is_converted_from_meters = true; + } + } +} + +void convert_from_meters(ModelVolume* volume) +{ + assert(!volume->source.is_converted_from_inches); + volume->scale_geometry_after_creation(1000.f); + volume->set_offset(Vec3d(0, 0, 0)); + volume->source.is_converted_from_meters = true; +} + +void convert_units(Model& model_to, ModelObject* object_from, ConversionType conv_type, std::vector volume_idxs) +{ + BOOST_LOG_TRIVIAL(trace) << "ModelObject::convert_units - start"; + + float koef = conv_type == ConversionType::CONV_FROM_INCH ? 25.4f : conv_type == ConversionType::CONV_TO_INCH ? 0.0393700787f : + conv_type == ConversionType::CONV_FROM_METER ? 1000.f : conv_type == ConversionType::CONV_TO_METER ? 0.001f : 1.f; + + ModelObject* new_object = model_to.add_object(*object_from); + new_object->sla_support_points.clear(); + new_object->sla_drain_holes.clear(); + new_object->sla_points_status = sla::PointsStatus::NoPoints; + new_object->clear_volumes(); + new_object->input_file.clear(); + + int vol_idx = 0; + for (ModelVolume* volume : object_from->volumes) { + if (!volume->mesh().empty()) { + TriangleMesh mesh(volume->mesh()); + + ModelVolume* vol = new_object->add_volume(mesh); + vol->name = volume->name; + vol->set_type(volume->type()); + // Don't copy the config's ID. + vol->config.assign_config(volume->config); + assert(vol->config.id().valid()); + assert(vol->config.id() != volume->config.id()); + vol->set_material(volume->material_id(), *volume->material()); + vol->source.input_file = volume->source.input_file; + vol->source.object_idx = (int)model_to.objects.size()-1; + vol->source.volume_idx = vol_idx; + vol->source.is_converted_from_inches = volume->source.is_converted_from_inches; + vol->source.is_converted_from_meters = volume->source.is_converted_from_meters; + vol->source.is_from_builtin_objects = volume->source.is_from_builtin_objects; + + vol->supported_facets.assign(volume->supported_facets); + vol->seam_facets.assign(volume->seam_facets); + vol->mm_segmentation_facets.assign(volume->mm_segmentation_facets); + + // Perform conversion only if the target "imperial" state is different from the current one. + // This check supports conversion of "mixed" set of volumes, each with different "imperial" state. + if (//vol->source.is_converted_from_inches != from_imperial && + (volume_idxs.empty() || + std::find(volume_idxs.begin(), volume_idxs.end(), vol_idx) != volume_idxs.end())) { + vol->scale_geometry_after_creation(koef); + vol->set_offset(Vec3d(koef, koef, koef).cwiseProduct(volume->get_offset())); + if (conv_type == ConversionType::CONV_FROM_INCH || conv_type == ConversionType::CONV_TO_INCH) + vol->source.is_converted_from_inches = conv_type == ConversionType::CONV_FROM_INCH; + if (conv_type == ConversionType::CONV_FROM_METER || conv_type == ConversionType::CONV_TO_METER) + vol->source.is_converted_from_meters = conv_type == ConversionType::CONV_FROM_METER; + assert(!vol->source.is_converted_from_inches || !vol->source.is_converted_from_meters); + } + else + vol->set_offset(volume->get_offset()); + } + vol_idx++; + } + new_object->invalidate_bounding_box(); + + BOOST_LOG_TRIVIAL(trace) << "ModelObject::convert_units - end"; +} + +TriangleMeshStats get_object_mesh_stats(const ModelObject* object) +{ + TriangleMeshStats full_stats; + full_stats.volume = 0.f; + + // fill full_stats from all objet's meshes + for (ModelVolume* volume : object->volumes) + { + const TriangleMeshStats& stats = volume->mesh().stats(); + + // initialize full_stats (for repaired errors) + full_stats.open_edges += stats.open_edges; + full_stats.repaired_errors.merge(stats.repaired_errors); + + // another used satistics value + if (volume->is_model_part()) { + Transform3d trans = object->instances.empty() ? volume->get_matrix() : (volume->get_matrix() * object->instances[0]->get_matrix()); + full_stats.volume += stats.volume * std::fabs(trans.matrix().block(0, 0, 3, 3).determinant()); + full_stats.number_of_parts += stats.number_of_parts; + } + } + + return full_stats; +} + +int get_repaired_errors_count(const ModelVolume* volume) +{ + const RepairedMeshErrors& errors = volume->mesh().stats().repaired_errors; + return errors.degenerate_facets + + errors.edges_fixed + + errors.facets_removed + + errors.facets_reversed + + errors.backwards_edges; +} + +int get_repaired_errors_count(const ModelObject* object, const int vol_idx /*= -1*/) +{ + if (vol_idx >= 0) + return get_repaired_errors_count(object->volumes[vol_idx]); + + const RepairedMeshErrors& errors = get_object_mesh_stats(object).repaired_errors; + return errors.degenerate_facets + + errors.edges_fixed + + errors.facets_removed + + errors.facets_reversed + + errors.backwards_edges; +} + + + +/// +/// Compare TriangleMeshes by Bounding boxes (mainly for sort) +/// From Front(Z) Upper(Y) TopLeft(X) corner. +/// 1. Seraparate group not overlaped i Z axis +/// 2. Seraparate group not overlaped i Y axis +/// 3. Start earlier in X (More on left side) +/// +/// Compare from +/// Compare to +/// True when triangle mesh 1 is closer, upper or lefter than triangle mesh 2 other wise false +static bool is_front_up_left(const TriangleMesh &trinagle_mesh1, const TriangleMesh &triangle_mesh2) +{ + // stats form t1 + const Vec3f &min1 = trinagle_mesh1.stats().min; + const Vec3f &max1 = trinagle_mesh1.stats().max; + // stats from t2 + const Vec3f &min2 = triangle_mesh2.stats().min; + const Vec3f &max2 = triangle_mesh2.stats().max; + // priority Z, Y, X + for (int axe = 2; axe > 0; --axe) { + if (max1[axe] < min2[axe]) + return true; + if (min1[axe] > max2[axe]) + return false; + } + return min1.x() < min2.x(); +} + +// Split this volume, append the result to the object owning this volume. +// Return the number of volumes created from this one. +// This is useful to assign different materials to different volumes of an object. +size_t split(ModelVolume* volume, unsigned int max_extruders) +{ + std::vector meshes = volume->mesh().split(); + if (meshes.size() <= 1) + return 1; + + std::sort(meshes.begin(), meshes.end(), is_front_up_left); + + // splited volume should not be text object + if (volume->text_configuration.has_value()) + volume->text_configuration.reset(); + + ModelObject* object = volume->get_object(); + + size_t idx = 0; + size_t ivolume = std::find(object->volumes.begin(), object->volumes.end(), volume) - object->volumes.begin(); + const std::string& name = volume->name; + + unsigned int extruder_counter = 0; + const Vec3d offset = volume->get_offset(); + + for (TriangleMesh &mesh : meshes) { + if (mesh.empty() || mesh.has_zero_volume()) + // Repair may have removed unconnected triangles, thus emptying the mesh. + continue; + + if (idx == 0) { + volume->set_mesh(std::move(mesh)); + volume->calculate_convex_hull(); + // Assign a new unique ID, so that a new GLVolume will be generated. + volume->set_new_unique_id(); + // reset the source to disable reload from disk + volume->source = ModelVolume::Source(); + } + else + object->insert_volume((++ivolume), *volume, std::move(mesh));//object->volumes.insert(object->volumes.begin() + (++ivolume), new ModelVolume(object, *volume, std::move(mesh))); + + object->volumes[ivolume]->set_offset(Vec3d::Zero()); + object->volumes[ivolume]->center_geometry_after_creation(); + object->volumes[ivolume]->translate(offset); + object->volumes[ivolume]->name = name + "_" + std::to_string(idx + 1); + object->volumes[ivolume]->config.set("extruder", auto_extruder_id(max_extruders, extruder_counter)); + object->volumes[ivolume]->discard_splittable(); + ++ idx; + } + + // discard volumes for which the convex hull was not generated or is degenerate + size_t i = 0; + while (i < object->volumes.size()) { + const std::shared_ptr &hull = object->volumes[i]->get_convex_hull_shared_ptr(); + if (hull == nullptr || hull->its.vertices.empty() || hull->its.indices.empty()) { + object->delete_volume(i); + --idx; + --i; + } + ++i; + } + + return idx; +} + +void split(ModelObject* object, ModelObjectPtrs* new_objects) +{ + for (ModelVolume* volume : object->volumes) { + if (volume->type() != ModelVolumeType::MODEL_PART) + continue; + + // splited volume should not be text object + if (volume->text_configuration.has_value()) + volume->text_configuration.reset(); + + std::vector meshes = volume->mesh().split(); + std::sort(meshes.begin(), meshes.end(), is_front_up_left); + + size_t counter = 1; + for (TriangleMesh &mesh : meshes) { + // FIXME: crashes if not satisfied + if (mesh.facets_count() < 3 || mesh.has_zero_volume()) + continue; + + // XXX: this seems to be the only real usage of m_model, maybe refactor this so that it's not needed? + ModelObject* new_object = object->get_model()->add_object(); + if (meshes.size() == 1) { + new_object->name = volume->name; + // Don't copy the config's ID. + new_object->config.assign_config(object->config.size() > 0 ? object->config : volume->config); + } + else { + new_object->name = object->name + (meshes.size() > 1 ? "_" + std::to_string(counter++) : ""); + // Don't copy the config's ID. + new_object->config.assign_config(object->config); + } + assert(new_object->config.id().valid()); + assert(new_object->config.id() != object->config.id()); + new_object->instances.reserve(object->instances.size()); + for (const ModelInstance* model_instance : object->instances) + new_object->add_instance(*model_instance); + ModelVolume* new_vol = new_object->add_volume(*volume, std::move(mesh)); + + // Invalidate extruder value in volume's config, + // otherwise there will no way to change extruder for object after splitting, + // because volume's extruder value overrides object's extruder value. + if (new_vol->config.has("extruder")) + new_vol->config.set_key_value("extruder", new ConfigOptionInt(0)); + + for (ModelInstance* model_instance : new_object->instances) { + const Vec3d shift = model_instance->get_transformation().get_matrix_no_offset() * new_vol->get_offset(); + model_instance->set_offset(model_instance->get_offset() + shift); + } + + new_vol->set_offset(Vec3d::Zero()); + // reset the source to disable reload from disk + new_vol->source = ModelVolume::Source(); + new_objects->emplace_back(new_object); + } + } +} + +void merge(ModelObject* object) +{ + if (object->volumes.size() == 1) { + // We can't merge meshes if there's just one volume + return; + } + + TriangleMesh mesh; + + for (ModelVolume* volume : object->volumes) + if (!volume->mesh().empty()) + mesh.merge(volume->mesh()); + + object->clear_volumes(); + ModelVolume* vol = object->add_volume(mesh); + + if (!vol) + return; +} + +} diff --git a/src/libslic3r/ModelProcessing.hpp b/src/libslic3r/ModelProcessing.hpp new file mode 100644 index 0000000000..816bf83a5b --- /dev/null +++ b/src/libslic3r/ModelProcessing.hpp @@ -0,0 +1,62 @@ +///|/ Copyright (c) Prusa Research 2016 - 2023 Tomáš Mészáros @tamasmeszaros, Oleksandra Iushchenko @YuSanka, Enrico Turri @enricoturri1966, Lukáš Matěna @lukasmatena, Vojtěch Bubník @bubnikv, Filip Sykala @Jony01, Lukáš Hejl @hejllukas, David Kocík @kocikdav, Vojtěch Král @vojtechkral +///|/ Copyright (c) 2019 John Drake @foxox +///|/ Copyright (c) 2019 Sijmen Schoon +///|/ Copyright (c) 2017 Eyal Soha @eyal0 +///|/ Copyright (c) Slic3r 2014 - 2015 Alessandro Ranellucci @alranel +///|/ +///|/ ported from lib/Slic3r/Model.pm: +///|/ Copyright (c) Prusa Research 2016 - 2022 Vojtěch Bubník @bubnikv, Enrico Turri @enricoturri1966 +///|/ Copyright (c) Slic3r 2012 - 2016 Alessandro Ranellucci @alranel +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ +#pragma once + +#include "TriangleMesh.hpp" +#include + +namespace Slic3r { + +class Model; +class ModelObject; +class ModelVolume; + +enum class ConversionType : int { + CONV_TO_INCH, + CONV_FROM_INCH, + CONV_TO_METER, + CONV_FROM_METER, +}; + +namespace ModelProcessing +{ + static constexpr const double volume_threshold_inches = 9.0; // 9 = 3*3*3; + static constexpr const double volume_threshold_meters = 0.001; // 0.001 = 0.1*0.1*0.1 + + void convert_to_multipart_object(Model& model, unsigned int max_extruders); + + void convert_from_imperial_units(Model& model, bool only_small_volumes); + void convert_from_imperial_units(ModelVolume* volume); + + void convert_from_meters(Model& model, bool only_small_volumes); + void convert_from_meters(ModelVolume* volume); + + void convert_units(Model& model_to, ModelObject* object_from, ConversionType conv_type, std::vector volume_idxs); + + // Get full stl statistics for all object's meshes + TriangleMeshStats get_object_mesh_stats(const ModelObject* object); + // Get count of errors in the mesh + int get_repaired_errors_count(const ModelVolume* volume); + // Get count of errors in the mesh( or all object's meshes, if volume index isn't defined) + int get_repaired_errors_count(const ModelObject* object, const int vol_idx = -1); + + // Split this volume, append the result to the object owning this volume. + // Return the number of volumes created from this one. + // This is useful to assign different materials to different volumes of an object. + size_t split(ModelVolume* volume, unsigned int max_extruders); + + void split(ModelObject* object, std::vector* new_objects); + void merge(ModelObject* object); +} + +} // namespace Slic3r::ModelProcessing diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 090ea06741..a54ac49bae 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -334,7 +334,7 @@ void Preset::normalize(DynamicPrintConfig &config) first_layer_height->percent = false; } - handle_legacy_sla(config); + // handle_legacy_sla(config); // it looks like the best place for call it, is handle_legacy_composite } std::string Preset::remove_invalid_keys(DynamicPrintConfig &config, const DynamicPrintConfig &default_config) diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 907fdfb02a..c85303c949 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -5064,6 +5064,8 @@ void PrintConfigDef::handle_legacy_composite(DynamicPrintConfig &config) } config.set_key_value("wiping_volumes_use_custom_matrix", new ConfigOptionBool(custom)); } + + handle_legacy_sla(config); } const PrintConfigDef print_config_def; diff --git a/src/slic3r/GUI/BedShapeDialog.cpp b/src/slic3r/GUI/BedShapeDialog.cpp index 8a6f3dddb7..9f8acb10ff 100644 --- a/src/slic3r/GUI/BedShapeDialog.cpp +++ b/src/slic3r/GUI/BedShapeDialog.cpp @@ -19,8 +19,9 @@ #include #include "libslic3r/BoundingBox.hpp" -#include "libslic3r/Model.hpp" #include "libslic3r/Polygon.hpp" +#include "libslic3r/FileReader.hpp" +#include "libslic3r/TriangleMesh.hpp" #include #include @@ -535,17 +536,15 @@ void BedShapePanel::load_stl() } wxBusyCursor wait; - - Model model; - try { - model = Model::read_from_file(file_name); - } - catch (std::exception &) { - show_error(this, _L("Error! Invalid model")); + TriangleMesh mesh; + try { + mesh = FileReader::load_mesh(file_name); + } + catch (std::exception& e) { + show_error(this, e.what()); return; } - auto mesh = model.mesh(); auto expolygons = mesh.horizontal_projection(); if (expolygons.size() == 0) { diff --git a/src/slic3r/GUI/GLModel.cpp b/src/slic3r/GUI/GLModel.cpp index 3b714714dc..5b50723363 100644 --- a/src/slic3r/GUI/GLModel.cpp +++ b/src/slic3r/GUI/GLModel.cpp @@ -14,7 +14,7 @@ #endif // ENABLE_GLMODEL_STATISTICS #include "libslic3r/TriangleMesh.hpp" -#include "libslic3r/Model.hpp" +#include "libslic3r/FileReader.hpp" #include "libslic3r/Polygon.hpp" #include "libslic3r/BuildVolume.hpp" #include "libslic3r/Geometry/ConvexHull.hpp" @@ -653,15 +653,14 @@ bool GLModel::init_from_file(const std::string& filename) if (!boost::algorithm::iends_with(filename, ".stl")) return false; - Model model; + TriangleMesh mesh; try { - model = Model::read_from_file(filename); + mesh = FileReader::load_mesh(filename); } catch (std::exception&) { return false; } - - init_from(model.mesh()); + init_from(mesh); m_filename = filename; diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 93efb19381..27315f8078 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -800,9 +800,7 @@ void GUI_App::post_init() if (plater()->load_files(fns) && this->init_params->input_files.size() == 1) { // Update application titlebar when opening a project file const std::string& filename = this->init_params->input_files.front(); - if (boost::algorithm::iends_with(filename, ".amf") || - boost::algorithm::iends_with(filename, ".amf.xml") || - boost::algorithm::iends_with(filename, ".3mf")) + if (boost::algorithm::iends_with(filename, ".3mf")) this->plater()->set_project_filename(from_u8(filename)); } if (this->init_params->delete_after_load) { diff --git a/src/slic3r/GUI/GUI_Factories.cpp b/src/slic3r/GUI/GUI_Factories.cpp index 34cfead654..d1cc9f5c4a 100644 --- a/src/slic3r/GUI/GUI_Factories.cpp +++ b/src/slic3r/GUI/GUI_Factories.cpp @@ -5,6 +5,7 @@ #include "libslic3r/libslic3r.h" #include "libslic3r/PresetBundle.hpp" #include "libslic3r/Model.hpp" +#include "libslic3r/ModelProcessing.hpp" #include "GUI_Factories.hpp" #include "GUI_ObjectList.hpp" diff --git a/src/slic3r/GUI/GUI_ObjectList.cpp b/src/slic3r/GUI/GUI_ObjectList.cpp index f83b2403e7..e5160deab7 100644 --- a/src/slic3r/GUI/GUI_ObjectList.cpp +++ b/src/slic3r/GUI/GUI_ObjectList.cpp @@ -8,6 +8,8 @@ #include "libslic3r/PresetBundle.hpp" #include "libslic3r/TextConfiguration.hpp" #include "libslic3r/BuildVolume.hpp" // IWYU pragma: keep +#include "libslic3r/ModelProcessing.hpp" +#include "libslic3r/FileReader.hpp" #include "GUI_ObjectList.hpp" #include "GUI_Factories.hpp" #include "GUI_ObjectManipulation.hpp" @@ -434,7 +436,7 @@ void ObjectList::get_selection_indexes(std::vector& obj_idxs, std::vector= 0 ? (*m_objects)[obj_idx]->get_repaired_errors_count(vol_idx) : 0; + return obj_idx >= 0 ? ModelProcessing::get_repaired_errors_count(object(obj_idx), vol_idx) : 0; } static std::string get_warning_icon_name(const TriangleMeshStats& stats) @@ -455,7 +457,7 @@ MeshErrorsInfo ObjectList::get_mesh_errors_info(const int obj_idx, const int vol } const TriangleMeshStats& stats = vol_idx == -1 ? - (*m_objects)[obj_idx]->get_object_stl_stats() : + ModelProcessing::get_object_mesh_stats((*m_objects)[obj_idx]) : (*m_objects)[obj_idx]->volumes[vol_idx]->mesh().stats(); if (!stats.repaired() && stats.manifold()) { @@ -1607,10 +1609,10 @@ void ObjectList::load_from_files(const wxArrayString& input_files, ModelObject& Model model; try { - model = Model::read_from_file(input_file); + model = FileReader::load_model(input_file); } catch (std::exception& e) { - auto msg = _L("Error!") + " " + input_file + " : " + e.what() + "."; + auto msg = _L("Error!") + " " + input_file + " : " + _(e.what()) + "."; show_error(parent, msg); exit(1); } @@ -1886,7 +1888,7 @@ bool ObjectList::del_subobject_item(wxDataViewItem& item) // If last volume item with warning was deleted, unmark object item if (type & itVolume) { - const std::string& icon_name = get_warning_icon_name(object(obj_idx)->get_object_stl_stats()); + const std::string& icon_name = get_warning_icon_name(ModelProcessing::get_object_mesh_stats(object(obj_idx))); m_objects_model->UpdateWarningIcon(parent, icon_name); } @@ -2148,7 +2150,7 @@ void ObjectList::split() wxGetApp().plater()->clear_before_change_mesh(obj_idx, _u8L("Custom supports, seams, fuzzy skin and multi-material painting were " "removed after splitting the object.")); - volume->split(nozzle_dmrs_cnt); + ModelProcessing::split(volume, nozzle_dmrs_cnt); (*m_objects)[obj_idx]->input_file.clear(); @@ -2343,7 +2345,7 @@ void ObjectList::merge(bool to_multipart_object) Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Merge all parts to the one single object")); ModelObject* model_object = (*m_objects)[obj_idx]; - model_object->merge(); + ModelProcessing::merge(model_object); m_objects_model->DeleteVolumeChildren(item); @@ -3173,7 +3175,7 @@ bool ObjectList::delete_from_model_and_list(const std::vector& it m_objects_model->SetExtruder(extruder, parent); } // If last volume item with warning was deleted, unmark object item - m_objects_model->UpdateWarningIcon(parent, get_warning_icon_name(obj->get_object_stl_stats())); + m_objects_model->UpdateWarningIcon(parent, get_warning_icon_name(ModelProcessing::get_object_mesh_stats(obj))); } wxGetApp().plater()->canvas3D()->ensure_on_bed(item->obj_idx, printer_technology() != ptSLA); } @@ -4623,8 +4625,8 @@ void ObjectList::fix_through_winsdk() if (vol_idxs.empty()) { #if !FIX_THROUGH_WINSDK_ALWAYS for (int i = int(obj_idxs.size())-1; i >= 0; --i) - if (object(obj_idxs[i])->get_repaired_errors_count() == 0) - obj_idxs.erase(obj_idxs.begin()+i); + if (ModelProcessing::get_repaired_errors_count(object(obj_idxs[i])) == 0) + obj_idxs.erase(obj_idxs.begin()+i); #endif // FIX_THROUGH_WINSDK_ALWAYS for (int obj_idx : obj_idxs) model_names.push_back(object(obj_idx)->name); @@ -4633,7 +4635,7 @@ void ObjectList::fix_through_winsdk() ModelObject* obj = object(obj_idxs.front()); #if !FIX_THROUGH_WINSDK_ALWAYS for (int i = int(vol_idxs.size()) - 1; i >= 0; --i) - if (obj->get_repaired_errors_count(vol_idxs[i]) == 0) + if (ModelProcessing::get_repaired_errors_count(obj, vol_idxs[i]) == 0) vol_idxs.erase(vol_idxs.begin() + i); #endif // FIX_THROUGH_WINSDK_ALWAYS for (int vol_idx : vol_idxs) @@ -4689,7 +4691,7 @@ void ObjectList::fix_through_winsdk() int vol_idx{ -1 }; for (int obj_idx : obj_idxs) { #if !FIX_THROUGH_WINSDK_ALWAYS - if (object(obj_idx)->get_repaired_errors_count(vol_idx) == 0) + if (ModelProcessing::get_repaired_errors_count(object(obj_idx), vol_idx) == 0) continue; #endif // FIX_THROUGH_WINSDK_ALWAYS if (!fix_and_update_progress(obj_idx, vol_idx, model_idx, progress_dlg, succes_models, failed_models)) @@ -4734,7 +4736,7 @@ void ObjectList::update_item_error_icon(const int obj_idx, const int vol_idx) co { auto obj = object(obj_idx); if (wxDataViewItem obj_item = m_objects_model->GetItemById(obj_idx)) { - const std::string& icon_name = get_warning_icon_name(obj->get_object_stl_stats()); + const std::string& icon_name = get_warning_icon_name(ModelProcessing::get_object_mesh_stats(obj)); m_objects_model->UpdateWarningIcon(obj_item, icon_name); } diff --git a/src/slic3r/GUI/GalleryDialog.cpp b/src/slic3r/GUI/GalleryDialog.cpp index 438fc42962..e344ffc9ba 100644 --- a/src/slic3r/GUI/GalleryDialog.cpp +++ b/src/slic3r/GUI/GalleryDialog.cpp @@ -33,6 +33,7 @@ #include "libslic3r/AppConfig.hpp" #include "libslic3r/BuildVolume.hpp" #include "libslic3r/Model.hpp" +#include "libslic3r/FileReader.hpp" #include "libslic3r/GCode/ThumbnailData.hpp" #include "libslic3r/Format/OBJ.hpp" #include "libslic3r/MultipleBeds.hpp" @@ -64,7 +65,7 @@ bool GalleryDropTarget::OnDropFiles(wxCoord x, wxCoord y, const wxArrayString& f // hides the system icon this->MSWUpdateDragImageOnLeave(); #endif // WIN32 - return gallery_dlg ? gallery_dlg->load_files(filenames) : false; + return gallery_dlg ? gallery_dlg->add_files_to_custom_dir(filenames) : false; } @@ -281,7 +282,7 @@ static void generate_thumbnail_from_model(const std::string& filename) Model model; try { - model = Model::read_from_file(filename); + model = FileReader::load_model(filename); } catch (std::exception&) { BOOST_LOG_TRIVIAL(error) << "Error loading model from " << filename << " in generate_thumbnail_from_model()"; @@ -343,10 +344,15 @@ void GalleryDialog::load_label_icon_list() std::vector sorted_names; for (auto& dir_entry : fs::directory_iterator(dir)) { - TriangleMesh mesh; - if ((is_gallery_file(dir_entry, ".stl") && mesh.ReadSTLFile(dir_entry.path().string().c_str())) || - (is_gallery_file(dir_entry, ".obj") && load_obj(dir_entry.path().string().c_str(), &mesh) ) ) + if (is_gallery_file(dir_entry, ".stl") || is_gallery_file(dir_entry, ".obj")) { + try { + Model model = FileReader::load_model(dir_entry.path().string()); + } + catch (std::exception&) { + continue; + } sorted_names.push_back(dir_entry.path().filename().string()); + } } // sort the filename case insensitive @@ -441,7 +447,7 @@ void GalleryDialog::add_custom_shapes(wxEvent& event) if (input_files.IsEmpty()) return; - load_files(input_files); + add_files_to_custom_dir(input_files); } void GalleryDialog::del_custom_shapes() @@ -558,7 +564,7 @@ void GalleryDialog::update() load_label_icon_list(); } -bool GalleryDialog::load_files(const wxArrayString& input_files) +bool GalleryDialog::add_files_to_custom_dir(const wxArrayString& input_files) { auto dest_dir = get_dir(false); @@ -577,16 +583,14 @@ bool GalleryDialog::load_files(const wxArrayString& input_files) // Iterate through the input files for (size_t i = 0; i < input_files.size(); ++i) { std::string input_file = into_u8(input_files.Item(i)); - - TriangleMesh mesh; - if (is_gallery_file(input_file, ".stl") && !mesh.ReadSTLFile(input_file.c_str())) { - show_warning(format_wxstr(_L("Loading of the \"%1%\""), input_file), "STL"); - continue; - } - - if (is_gallery_file(input_file, ".obj") && !load_obj(input_file.c_str(), &mesh)) { - show_warning(format_wxstr(_L("Loading of the \"%1%\""), input_file), "OBJ"); - continue; + if (is_gallery_file(input_file, ".stl") || is_gallery_file(input_file, ".obj")) { + try { + Model model = FileReader::load_model(input_file); + } + catch (std::exception&) { + show_warning(format_wxstr(_L("Loading of the \"%1%\""), input_file), is_gallery_file(input_file, ".obj") ? "OBJ" : "STL"); + continue; + } } try { diff --git a/src/slic3r/GUI/GalleryDialog.hpp b/src/slic3r/GUI/GalleryDialog.hpp index 085335c8b2..94799708b7 100644 --- a/src/slic3r/GUI/GalleryDialog.hpp +++ b/src/slic3r/GUI/GalleryDialog.hpp @@ -58,7 +58,7 @@ public: int show(bool show_from_menu = false); void get_input_files(wxArrayString& input_files); - bool load_files(const wxArrayString& input_files); + bool add_files_to_custom_dir(const wxArrayString& input_files); protected: void on_dpi_changed(const wxRect& suggested_rect) override; diff --git a/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp b/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp index 96e3c46dc1..e3a667e01a 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp @@ -22,6 +22,7 @@ #include "slic3r/Utils/FixModelByWin10.hpp" #include "libslic3r/AppConfig.hpp" #include "libslic3r/TriangleMeshSlicer.hpp" +#include "libslic3r/ModelProcessing.hpp" #ifndef IMGUI_DEFINE_MATH_OPERATORS #define IMGUI_DEFINE_MATH_OPERATORS @@ -1898,7 +1899,7 @@ GLGizmoCut3D::PartSelection::PartSelection(const ModelObject* mo, const Transfor // split to parts for (int id = int(volumes.size())-1; id >= 0; id--) if (volumes[id]->is_splittable() && volumes[id]->is_model_part()) // we have to split just solid volumes - volumes[id]->split(1); + ModelProcessing::split(volumes[id], 1); m_parts.clear(); for (const ModelVolume* volume : volumes) { @@ -3293,8 +3294,8 @@ static void check_objects_after_cut(const ModelObjectPtrs& objects) if (connectors_count != connectors_names.size()) err_objects_names.push_back(object->name); - // check manifol/repairs - auto stats = object->get_object_stl_stats(); + // check manifold/repairs + auto stats = ModelProcessing::get_object_mesh_stats(object); if (!stats.manifold() || stats.repaired()) err_objects_idxs.push_back(obj_idx); obj_idx++; diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index debfe625fd..766a121f56 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -76,6 +76,8 @@ #include "libslic3r/Utils.hpp" #include "libslic3r/PresetBundle.hpp" #include "libslic3r/miniz_extension.hpp" +#include "libslic3r/ModelProcessing.hpp" +#include "libslic3r/FileReader.hpp" #include "libslic3r/MultipleBeds.hpp" // For stl export @@ -160,6 +162,7 @@ using Slic3r::GUI::format_wxstr; static const std::pair THUMBNAIL_SIZE_3MF = { 256, 256 }; namespace Slic3r { +using namespace ModelProcessing; namespace GUI { // BackgroundSlicingProcess updates UI with slicing progress: Status bar / progress bar has to be updated, possibly scene has to be refreshed, @@ -609,11 +612,9 @@ private: bool show_warning_dialog { false }; }; -const std::regex Plater::priv::pattern_bundle(".*[.](amf|amf[.]xml|zip[.]amf|3mf|prusa)", std::regex::icase); +const std::regex Plater::priv::pattern_bundle(".*[.](amf|amf[.]xml|3mf)", std::regex::icase); const std::regex Plater::priv::pattern_3mf(".*3mf", std::regex::icase); -const std::regex Plater::priv::pattern_zip_amf(".*[.]zip[.]amf", std::regex::icase); const std::regex Plater::priv::pattern_any_amf(".*[.](amf|amf[.]xml|zip[.]amf)", std::regex::icase); -const std::regex Plater::priv::pattern_prusa(".*prusa", std::regex::icase); const std::regex Plater::priv::pattern_zip(".*zip", std::regex::icase); const std::regex Plater::priv::pattern_printRequest(".*printRequest", std::regex::icase); @@ -1204,7 +1205,7 @@ void Plater::notify_about_installed_presets() std::vector Plater::priv::load_files(const std::vector& input_files, bool load_model, bool load_config, bool imperial_units/* = false*/) { - if (input_files.empty()) { return std::vector(); } + if (input_files.empty()) { return std::vector(); } auto *nozzle_dmrs = config->opt("nozzle_diameter"); @@ -1235,11 +1236,10 @@ std::vector Plater::priv::load_files(const std::vector& input_ wxProgressDialog progress_dlg_stack(loading, "", 100, find_toplevel_parent(q), wxPD_APP_MODAL | wxPD_AUTO_HIDE); wxProgressDialog* progress_dlg = &progress_dlg_stack; #endif - wxBusyCursor busy; - auto *new_model = (!load_model || one_by_one) ? nullptr : new Slic3r::Model(); + auto *extra_model = (!load_model || one_by_one) ? nullptr : new Slic3r::Model(); std::vector obj_idxs; boost::optional prusaslicer_generator_version; @@ -1268,9 +1268,7 @@ std::vector Plater::priv::load_files(const std::vector& input_ } const bool type_3mf = std::regex_match(path.string(), pattern_3mf) || std::regex_match(path.string(), pattern_zip); - const bool type_zip_amf = !type_3mf && std::regex_match(path.string(), pattern_zip_amf); const bool type_any_amf = !type_3mf && std::regex_match(path.string(), pattern_any_amf); - const bool type_prusa = std::regex_match(path.string(), pattern_prusa); const bool type_printRequest = std::regex_match(path.string(), pattern_printRequest); if (type_printRequest && printer_technology != ptSLA) { @@ -1281,109 +1279,38 @@ std::vector Plater::priv::load_files(const std::vector& input_ } Slic3r::Model model; - bool is_project_file = type_prusa; + bool is_project_file = false; + +#ifdef __linux__ + // On Linux Constructor of the ProgressDialog calls DisableOtherWindows() function which causes a disabling of all children of the find_toplevel_parent(q) + // And a destructor of the ProgressDialog calls ReenableOtherWindows() function which revert previously disabled children. + // But if printer technology will be changes during project loading, + // then related SLA Print and Materials Settings or FFF Print and Filaments Settings will be unparent from the wxNoteBook + // and that is why they will never be enabled after destruction of the ProgressDialog. + // So, distroy progress_gialog if we are loading project file + if (input_files_size == 1 && progress_dlg) { + progress_dlg->Destroy(); + progress_dlg = nullptr; + } +#endif + + DynamicPrintConfig config; + PrinterTechnology loaded_printer_technology{ ptFFF }; + + DynamicPrintConfig config_loaded; + ConfigSubstitutionContext config_substitutions{ ForwardCompatibilitySubstitutionRule::Enable }; + + if (!type_3mf) + load_config = false; // just 3mf file contains config + + FileReader::LoadStats load_stats; try { - if (type_3mf || type_zip_amf) { -#ifdef __linux__ - // On Linux Constructor of the ProgressDialog calls DisableOtherWindows() function which causes a disabling of all children of the find_toplevel_parent(q) - // And a destructor of the ProgressDialog calls ReenableOtherWindows() function which revert previously disabled children. - // But if printer technology will be changes during project loading, - // then related SLA Print and Materials Settings or FFF Print and Filaments Settings will be unparent from the wxNoteBook - // and that is why they will never be enabled after destruction of the ProgressDialog. - // So, distroy progress_gialog if we are loading project file - if (input_files_size == 1 && progress_dlg) { - progress_dlg->Destroy(); - progress_dlg = nullptr; - } -#endif - DynamicPrintConfig config; - PrinterTechnology loaded_printer_technology {ptFFF}; - { - DynamicPrintConfig config_loaded; - ConfigSubstitutionContext config_substitutions{ ForwardCompatibilitySubstitutionRule::Enable }; - model = Slic3r::Model::read_from_archive( - path.string(), - &config_loaded, - &config_substitutions, - prusaslicer_generator_version, - only_if(load_config, Model::LoadAttribute::CheckVersion) - ); - if (load_config && !config_loaded.empty()) { - // Based on the printer technology field found in the loaded config, select the base for the config, - loaded_printer_technology = Preset::printer_technology(config_loaded); - - // We can't to load SLA project if there is at least one multi-part object on the bed - if (loaded_printer_technology == ptSLA) { - const ModelObjectPtrs& objects = q->model().objects; - for (auto object : objects) - if (object->volumes.size() > 1) { - Slic3r::GUI::show_info(nullptr, - _L("You cannot load SLA project with a multi-part object on the bed") + "\n\n" + - _L("Please check your object list before preset changing."), - _L("Attention!")); - return obj_idxs; - } - } - - config.apply(loaded_printer_technology == ptFFF ? - static_cast(FullPrintConfig::defaults()) : - static_cast(SLAFullPrintConfig::defaults())); - // Set all the nullable values in defaults to nils. - config.null_nullables(); - // and place the loaded config over the base. - config += std::move(config_loaded); - } - if (! config_substitutions.empty()) - show_substitutions_info(config_substitutions.substitutions, filename.string()); - - if (load_config) { - this->model.get_custom_gcode_per_print_z_vector() = model.get_custom_gcode_per_print_z_vector(); - this->model.get_wipe_tower_vector() = model.get_wipe_tower_vector(); - } - } - - if (load_config) { - if (!config.empty()) { - const auto* post_process = config.opt("post_process"); - if (post_process != nullptr && !post_process->values.empty()) { - // TRN The placeholder is either "3MF" or "AMF" - wxString msg = GUI::format_wxstr(_L("The selected %1% file contains a post-processing script.\n" - "Please review the script carefully before exporting G-code."), type_3mf ? "3MF" : "AMF" ); - std::string text; - for (const std::string& s : post_process->values) - text += s; - - InfoDialog msg_dlg(nullptr, msg, from_u8(text), true, wxOK | wxICON_WARNING); - msg_dlg.set_caption(wxString(SLIC3R_APP_NAME " - ") + _L("Attention!")); - msg_dlg.ShowModal(); - } - - Preset::normalize(config); - PresetBundle* preset_bundle = wxGetApp().preset_bundle; - preset_bundle->load_config_model(filename.string(), std::move(config)); - q->notify_about_installed_presets(); - - //if (loaded_printer_technology == ptFFF) - // CustomGCode::update_custom_gcode_per_print_z_from_config(model.custom_gcode_per_print_z(), &preset_bundle->project_config); - - // For exporting from the amf/3mf we shouldn't check printer_presets for the containing information about "Print Host upload" - wxGetApp().load_current_presets(false); - // Update filament colors for the MM-printer profile in the full config - // to avoid black (default) colors for Extruders in the ObjectList, - // when for extruder colors are used filament colors - q->update_filament_colors_in_full_config(); - is_project_file = true; - } - if (!in_temp) - wxGetApp().app_config->update_config_dir(path.parent_path().string()); - } + if (load_config) { + model = FileReader::load_model_with_config(path.string(), &config_loaded, &config_substitutions, prusaslicer_generator_version, FileReader::LoadAttribute::CheckVersion, &load_stats); } - else { - model = Slic3r::Model::read_from_file(path.string(), nullptr, nullptr, only_if(load_config, Model::LoadAttribute::CheckVersion)); - for (auto obj : model.objects) - if (obj->name.empty()) - obj->name = fs::path(obj->input_file).filename().string(); + else if (load_model) { + model = FileReader::load_model(path.string(), FileReader::LoadAttributes{}, &load_stats); } } catch (const ConfigurationError &e) { std::string message = GUI::format(_L("Failed loading file \"%1%\" due to an invalid configuration."), filename.string()) + "\n\n" + e.what(); @@ -1394,33 +1321,80 @@ std::vector Plater::priv::load_files(const std::vector& input_ continue; } - if (load_model) { - // The model should now be initialized + if (load_config) { - auto convert_from_imperial_units = [](Model& model, bool only_small_volumes) { - model.convert_from_imperial_units(only_small_volumes); -// wxGetApp().app_config->set("use_inches", "1"); -// wxGetApp().sidebar().update_ui_from_settings(); - }; + if (!config_loaded.empty()) { + // Based on the printer technology field found in the loaded config, select the base for the config, + loaded_printer_technology = Preset::printer_technology(config_loaded); + + config.apply(loaded_printer_technology == ptFFF ? + static_cast(FullPrintConfig::defaults()) : + static_cast(SLAFullPrintConfig::defaults())); + // Set all the nullable values in defaults to nils. + config.null_nullables(); + // and place the loaded config over the base. + config += std::move(config_loaded); + } + if (!config_substitutions.empty()) + show_substitutions_info(config_substitutions.substitutions, filename.string()); + + if (!config.empty()) { + const auto* post_process = config.opt("post_process"); + if (post_process != nullptr && !post_process->values.empty()) { + wxString msg = _L("The selected 3MF file contains a post-processing script.\n" + "Please review the script carefully before exporting G-code."); + std::string text; + for (const std::string& s : post_process->values) + text += s; + + InfoDialog msg_dlg(nullptr, msg, from_u8(text), true, wxOK | wxICON_WARNING); + msg_dlg.set_caption(wxString(SLIC3R_APP_NAME " - ") + _L("Attention!")); + msg_dlg.ShowModal(); + } + + Preset::normalize(config); //??? + PresetBundle* preset_bundle = wxGetApp().preset_bundle; + preset_bundle->load_config_model(filename.string(), std::move(config)); + q->notify_about_installed_presets(); + + //if (loaded_printer_technology == ptFFF && load_model) + // CustomGCode::update_custom_gcode_per_print_z_from_config(model.custom_gcode_per_print_z(), &preset_bundle->project_config); + + // For exporting from the 3mf we shouldn't check printer_presets for the containing information about "Print Host upload" + wxGetApp().load_current_presets(false); + // Update filament colors for the MM-printer profile in the full config + // to avoid black (default) colors for Extruders in the ObjectList, + // when for extruder colors are used filament colors + q->update_filament_colors_in_full_config(); + is_project_file = true; + } + + this->model.get_custom_gcode_per_print_z_vector() = model.get_custom_gcode_per_print_z_vector(); + this->model.get_wipe_tower_vector() = model.get_wipe_tower_vector(); + + if (!in_temp) + wxGetApp().app_config->update_config_dir(path.parent_path().string()); + } + + if (load_model) { if (!is_project_file) { - if (int deleted_objects = model.removed_objects_with_zero_volume(); deleted_objects > 0) { + + if (load_stats.deleted_objects_cnt > 0) { MessageDialog(q, format_wxstr(_L_PLURAL( "Object size from file %s appears to be zero.\n" "This object has been removed from the model", "Objects size from file %s appears to be zero.\n" - "These objects have been removed from the model", deleted_objects), from_path(filename)) + "\n", + "These objects have been removed from the model", load_stats.deleted_objects_cnt), from_path(filename)) + "\n", _L("The size of the object is zero"), wxICON_INFORMATION | wxOK).ShowModal(); } - if (imperial_units) + + if (imperial_units) { // Convert even if the object is big. - convert_from_imperial_units(model, false); - else if (!type_3mf && model.looks_like_saved_in_meters()) { - auto convert_model_if = [](Model& model, bool condition) { - if (condition) - //FIXME up-scale only the small parts? - model.convert_from_meters(true); - }; + // It's a case, when we load model from STL, saved in imperial units + ModelProcessing::convert_from_imperial_units(model, false); + } + else if (load_stats.looks_like_saved_in_meters) { if (answer_convert_from_meters == wxOK_DEFAULT) { RichMessageDialog dlg(q, format_wxstr(_L_PLURAL( "The dimensions of the object from file %s seem to be defined in meters.\n" @@ -1432,17 +1406,12 @@ std::vector Plater::priv::load_files(const std::vector& input_ int answer = dlg.ShowModal(); if (dlg.IsCheckBoxChecked()) answer_convert_from_meters = answer; - else - convert_model_if(model, answer == wxID_YES); } - convert_model_if(model, answer_convert_from_meters == wxID_YES); + if (answer_convert_from_meters == wxID_YES) + //FIXME up-scale only the small parts? + ModelProcessing::convert_from_meters(model, true); } - else if (!type_3mf && model.looks_like_imperial_units()) { - auto convert_model_if = [convert_from_imperial_units](Model& model, bool condition) { - if (condition) - //FIXME up-scale only the small parts? - convert_from_imperial_units(model, true); - }; + else if (load_stats.looks_like_imperial_units) { if (answer_convert_from_imperial_units == wxOK_DEFAULT) { RichMessageDialog dlg(q, format_wxstr(_L_PLURAL( "The dimensions of the object from file %s seem to be defined in inches.\n" @@ -1454,13 +1423,13 @@ std::vector Plater::priv::load_files(const std::vector& input_ int answer = dlg.ShowModal(); if (dlg.IsCheckBoxChecked()) answer_convert_from_imperial_units = answer; - else - convert_model_if(model, answer == wxID_YES); } - convert_model_if(model, answer_convert_from_imperial_units == wxID_YES); + if (answer_convert_from_imperial_units == wxID_YES) + //FIXME up-scale only the small parts? + ModelProcessing::convert_from_imperial_units(model, true); } - if (!type_printRequest && model.looks_like_multipart_object()) { + if (load_stats.looks_like_multipart_object) { if (answer_consider_as_multi_part_objects == wxOK_DEFAULT) { RichMessageDialog dlg(q, _L( "This file contains several objects positioned at multiple heights.\n" @@ -1471,14 +1440,13 @@ std::vector Plater::priv::load_files(const std::vector& input_ int answer = dlg.ShowModal(); if (dlg.IsCheckBoxChecked()) answer_consider_as_multi_part_objects = answer; - if (answer == wxID_YES) - model.convert_multipart_object(nozzle_dmrs->size()); } - else if (answer_consider_as_multi_part_objects == wxID_YES) - model.convert_multipart_object(nozzle_dmrs->size()); + if (/*!type_printRequest && */answer_consider_as_multi_part_objects == wxID_YES) //! type_printRequest is no need here, SLA allow multipart object now + ModelProcessing::convert_to_multipart_object(model, nozzle_dmrs->size()); } } - if ((wxGetApp().get_mode() == comSimple) && (type_3mf || type_any_amf) && model_has_advanced_features(model)) { + + if ((wxGetApp().get_mode() == comSimple) && type_3mf && model_has_advanced_features(model)) { MessageDialog msg_dlg(q, _L("This file cannot be loaded in a simple mode. Do you want to switch to an advanced mode?")+"\n", _L("Detected advanced data"), wxICON_WARNING | wxOK | wxCANCEL); if (msg_dlg.ShowModal() == wxID_OK) { @@ -1486,11 +1454,11 @@ std::vector Plater::priv::load_files(const std::vector& input_ view3D->set_as_dirty(); } else - return obj_idxs; + continue;// return obj_idxs; } for (ModelObject* model_object : model.objects) { - if (!type_3mf && !type_zip_amf) { + if (!type_3mf) { model_object->center_around_origin(false); if (type_any_amf && model_object->instances.empty()) { ModelInstance* instance = model_object->add_instance(); @@ -1505,12 +1473,12 @@ std::vector Plater::priv::load_files(const std::vector& input_ } } } - if (type_printRequest) { + + if (type_printRequest ) { assert(model.materials.size()); for (const auto& material : model.materials) { - std::string preset_name = wxGetApp().preset_bundle->get_preset_name_by_alias_invisible(Preset::Type::TYPE_SLA_MATERIAL, - Preset::remove_suffix_modified(material.first)); + std::string preset_name = wxGetApp().preset_bundle->get_preset_name_by_alias_invisible(Preset::Type::TYPE_SLA_MATERIAL, material.first); Preset* prst = wxGetApp().preset_bundle->sla_materials.find_preset(preset_name, false); if (!prst) { //did not find compatible profile // try find alias of material comaptible with another print profile - if exists, use the print profile @@ -1522,8 +1490,7 @@ std::vector Plater::priv::load_files(const std::vector& input_ if (it->name != edited_print_name) { BOOST_LOG_TRIVIAL(error) << it->name; wxGetApp().get_tab(Preset::Type::TYPE_SLA_PRINT)->select_preset(it->name, false); - preset_name = wxGetApp().preset_bundle->get_preset_name_by_alias_invisible(Preset::Type::TYPE_SLA_MATERIAL, - Preset::remove_suffix_modified(material.first)); + preset_name = wxGetApp().preset_bundle->get_preset_name_by_alias_invisible(Preset::Type::TYPE_SLA_MATERIAL, material.first); prst = wxGetApp().preset_bundle->sla_materials.find_preset(preset_name, false); if (prst) { found = true; @@ -1553,14 +1520,14 @@ std::vector Plater::priv::load_files(const std::vector& input_ } if (one_by_one) { - if ((type_3mf && !is_project_file) || (type_any_amf && !type_zip_amf)) + if ((type_3mf && !is_project_file) || type_any_amf) model.center_instances_around_point(this->bed.build_volume().bed_center()); auto loaded_idxs = load_model_objects(model.objects, is_project_file); obj_idxs.insert(obj_idxs.end(), loaded_idxs.begin(), loaded_idxs.end()); } else { // This must be an .stl or .obj file, which may contain a maximum of one volume. for (const ModelObject* model_object : model.objects) { - new_model->add_object(*model_object); + extra_model->add_object(*model_object); } } @@ -1569,18 +1536,17 @@ std::vector Plater::priv::load_files(const std::vector& input_ } } - if (new_model != nullptr && new_model->objects.size() > 1) { - //wxMessageDialog msg_dlg(q, _L( + if (extra_model != nullptr && extra_model->objects.size() > 1) { MessageDialog msg_dlg(q, _L( "Multiple objects were loaded for a multi-material printer.\n" "Instead of considering them as multiple objects, should I consider\n" "these files to represent a single object having multiple parts?") + "\n", _L("Multi-part object detected"), wxICON_WARNING | wxYES | wxNO); if (msg_dlg.ShowModal() == wxID_YES) { - new_model->convert_multipart_object(nozzle_dmrs->values.size()); + ModelProcessing::convert_to_multipart_object(*extra_model, nozzle_dmrs->values.size()); } - auto loaded_idxs = load_model_objects(new_model->objects); + auto loaded_idxs = load_model_objects(extra_model->objects); obj_idxs.insert(obj_idxs.end(), loaded_idxs.begin(), loaded_idxs.end()); } @@ -2066,7 +2032,7 @@ void Plater::priv::split_object() wxBusyCursor wait; ModelObjectPtrs new_objects; - current_model_object->split(&new_objects); + ModelProcessing::split(current_model_object, &new_objects); if (new_objects.size() == 1) // #ysFIXME use notification Slic3r::GUI::warning_catcher(q, _L("The selected object couldn't be split because it contains only one solid part.")); @@ -2651,7 +2617,7 @@ bool Plater::priv::replace_volume_with_stl(int object_idx, int volume_idx, const Model new_model; try { - new_model = Model::read_from_file(path, nullptr, nullptr, Model::LoadAttribute::AddDefaultInstances); + new_model = FileReader::load_model(path); for (ModelObject* model_object : new_model.objects) { model_object->center_around_origin(); model_object->ensure_on_bed(); @@ -2689,9 +2655,9 @@ bool Plater::priv::replace_volume_with_stl(int object_idx, int volume_idx, const new_volume->translate(new_volume->get_transformation().get_matrix_no_offset() * (new_volume->source.mesh_offset - old_volume->source.mesh_offset)); assert(!old_volume->source.is_converted_from_inches || !old_volume->source.is_converted_from_meters); if (old_volume->source.is_converted_from_inches) - new_volume->convert_from_imperial_units(); + ModelProcessing::convert_from_imperial_units(new_volume); else if (old_volume->source.is_converted_from_meters) - new_volume->convert_from_meters(); + ModelProcessing::convert_from_meters(new_volume); if (old_volume->mesh().its == new_volume->mesh().its) { // This function is called both from reload_from_disk and replace_with_stl. @@ -2901,7 +2867,7 @@ void Plater::priv::reload_from_disk() Model new_model; try { - new_model = Model::read_from_file(path, nullptr, nullptr, Model::LoadAttribute::AddDefaultInstances); + new_model = FileReader::load_model(path); for (ModelObject* model_object : new_model.objects) { model_object->center_around_origin(); model_object->ensure_on_bed(); @@ -2984,9 +2950,9 @@ void Plater::priv::reload_from_disk() new_volume->source.volume_idx = old_volume->source.volume_idx; assert(!old_volume->source.is_converted_from_inches || !old_volume->source.is_converted_from_meters); if (old_volume->source.is_converted_from_inches) - new_volume->convert_from_imperial_units(); + ModelProcessing::convert_from_imperial_units(new_volume); else if (old_volume->source.is_converted_from_meters) - new_volume->convert_from_meters(); + ModelProcessing::convert_from_meters(new_volume); std::swap(old_model_object->volumes[vol_idx], old_model_object->volumes.back()); old_model_object->delete_volume(old_model_object->volumes.size() - 1); if (!sinking) @@ -3910,14 +3876,14 @@ bool Plater::priv::can_fix_through_winsdk() const // Fixing only if the model is not manifold. if (vol_idxs.empty()) { for (auto obj_idx : obj_idxs) - if (model.objects[obj_idx]->get_repaired_errors_count() > 0) + if (get_repaired_errors_count(model.objects[obj_idx]) > 0) return true; return false; } int obj_idx = obj_idxs.front(); for (auto vol_idx : vol_idxs) - if (model.objects[obj_idx]->get_repaired_errors_count(vol_idx) > 0) + if (get_repaired_errors_count(model.objects[obj_idx], vol_idx) > 0) return true; return false; #endif // FIX_THROUGH_WINSDK_ALWAYS @@ -5063,86 +5029,6 @@ bool Plater::preview_zip_archive(const boost::filesystem::path& archive_path) { return false; } -#if 0 - // 1 project, 0 models - behave like drag n drop - if (project_paths.size() == 1 && non_project_paths.empty()) - { - wxArrayString aux; - aux.Add(from_u8(project_paths.front().string())); - load_files(aux); - //load_files(project_paths, true, true); - boost::system::error_code ec; - fs::remove(project_paths.front(), ec); - if (ec) - BOOST_LOG_TRIVIAL(error) << ec.message(); - return true; - } - // 1 model (or more and other instances are not allowed), 0 projects - open geometry - if (project_paths.empty() && (non_project_paths.size() == 1 || wxGetApp().app_config->get_bool("single_instance"))) - { - load_files(non_project_paths, true, false); - boost::system::error_code ec; - fs::remove(non_project_paths.front(), ec); - if (ec) - BOOST_LOG_TRIVIAL(error) << ec.message(); - return true; - } - - bool delete_after = true; - - LoadProjectsDialog dlg(project_paths); - if (dlg.ShowModal() == wxID_OK) { - LoadProjectsDialog::LoadProjectOption option = static_cast(dlg.get_action()); - switch (option) - { - case LoadProjectsDialog::LoadProjectOption::AllGeometry: { - load_files(project_paths, true, false); - load_files(non_project_paths, true, false); - break; - } - case LoadProjectsDialog::LoadProjectOption::AllNewWindow: { - delete_after = false; - for (const fs::path& path : project_paths) { - wxString f = from_path(path); - start_new_slicer(&f, false); - } - for (const fs::path& path : non_project_paths) { - wxString f = from_path(path); - start_new_slicer(&f, false); - } - break; - } - case LoadProjectsDialog::LoadProjectOption::OneProject: { - int pos = dlg.get_selected(); - assert(pos >= 0 && pos < project_paths.size()); - if (wxGetApp().can_load_project()) - load_project(from_path(project_paths[pos])); - project_paths.erase(project_paths.begin() + pos); - load_files(project_paths, true, false); - load_files(non_project_paths, true, false); - break; - } - case LoadProjectsDialog::LoadProjectOption::OneConfig: { - int pos = dlg.get_selected(); - assert(pos >= 0 && pos < project_paths.size()); - std::vector aux; - aux.push_back(project_paths[pos]); - load_files(aux, false, true); - project_paths.erase(project_paths.begin() + pos); - load_files(project_paths, true, false); - load_files(non_project_paths, true, false); - break; - } - case LoadProjectsDialog::LoadProjectOption::Unknown: - default: - assert(false); - break; - } - } - - if (!delete_after) - return true; -#else // 1 project file and some models - behave like drag n drop of 3mf and then load models if (project_paths.size() == 1) { @@ -5169,7 +5055,6 @@ bool Plater::preview_zip_archive(const boost::filesystem::path& archive_path) // load all projects and all models as geometry load_files(project_paths, true, false); load_files(non_project_paths, true, false); -#endif // 0 for (const fs::path& path : project_paths) { @@ -5323,7 +5208,7 @@ bool Plater::load_files(const wxArrayString& filenames, bool delete_after_load/* for (std::vector::const_reverse_iterator it = paths.rbegin(); it != paths.rend(); ++it) { std::string filename = (*it).filename().string(); - bool handle_as_project = (boost::algorithm::iends_with(filename, ".3mf") || boost::algorithm::iends_with(filename, ".amf")); + bool handle_as_project = boost::algorithm::iends_with(filename, ".3mf"); if (boost::algorithm::iends_with(filename, ".zip") && is_project_3mf(it->string())) { BOOST_LOG_TRIVIAL(warning) << "File with .zip extension is 3mf project, opening as it would have .3mf extension: " << *it; handle_as_project = true; @@ -5331,8 +5216,7 @@ bool Plater::load_files(const wxArrayString& filenames, bool delete_after_load/* if (handle_as_project && load_just_one_file) { ProjectDropDialog::LoadType load_type = ProjectDropDialog::LoadType::Unknown; { - if ((boost::algorithm::iends_with(filename, ".3mf") && !is_project_3mf(it->string())) || - (boost::algorithm::iends_with(filename, ".amf") && !boost::algorithm::iends_with(filename, ".zip.amf"))) + if (boost::algorithm::iends_with(filename, ".3mf") && !is_project_3mf(it->string())) load_type = ProjectDropDialog::LoadType::LoadGeometry; else { if (wxGetApp().app_config->get_bool("show_drop_project_dialog")) { @@ -5695,19 +5579,23 @@ void Plater::convert_unit(ConversionType conv_type) conv_type == ConversionType::CONV_FROM_METER ? _L("Convert from meters") : _L("Revert conversion from meters")); wxBusyCursor wait; - ModelObjectPtrs objects; - for (int obj_idx : obj_idxs) { - ModelObject *object = p->model.objects[obj_idx]; - object->convert_units(objects, conv_type, volume_idxs); - remove(obj_idx); + { + Model tmp_model; + for (int obj_idx : obj_idxs) { + //ModelObject *object = p->model.objects[obj_idx]; + //object->convert_units(objects, conv_type, volume_idxs); + ModelProcessing::convert_units(tmp_model, p->model.objects[obj_idx], conv_type, volume_idxs); + remove(obj_idx); + } + p->load_model_objects(tmp_model.objects); } - p->load_model_objects(objects); + Selection& selection = p->view3D->get_canvas3d()->get_selection(); size_t last_obj_idx = p->model.objects.size() - 1; if (volume_idxs.empty()) { - for (size_t i = 0; i < objects.size(); ++i) + for (size_t i = 0; i < obj_idxs.size(); ++i) selection.add_object((unsigned int)(last_obj_idx - i), i == 0); } else { diff --git a/src/slic3r/GUI/Sidebar.cpp b/src/slic3r/GUI/Sidebar.cpp index 7fcc0efaf2..9b799265a8 100644 --- a/src/slic3r/GUI/Sidebar.cpp +++ b/src/slic3r/GUI/Sidebar.cpp @@ -45,6 +45,7 @@ #include "libslic3r/Print.hpp" #include "libslic3r/SLAPrint.hpp" #include "libslic3r/PresetBundle.hpp" +#include "libslic3r/ModelProcessing.hpp" #include "GUI.hpp" #include "GUI_App.hpp" @@ -930,7 +931,7 @@ void Sidebar::show_info_sizer() Vec3d size = vol ? vol->mesh().transformed_bounding_box(t).size() : model_object->instance_bounding_box(inst_idx).size(); m_object_info->info_size->SetLabel(wxString::Format("%.2f x %.2f x %.2f", size(0)*koef, size(1)*koef, size(2)*koef)); - const TriangleMeshStats& stats = vol ? vol->mesh().stats() : model_object->get_object_stl_stats(); + const TriangleMeshStats& stats = vol ? vol->mesh().stats() : ModelProcessing::get_object_mesh_stats(model_object); double volume_val = stats.volume; if (vol) diff --git a/tests/sla_print/sla_archive_readwrite_tests.cpp b/tests/sla_print/sla_archive_readwrite_tests.cpp index fb1af3d7f7..3c1dc835fd 100644 --- a/tests/sla_print/sla_archive_readwrite_tests.cpp +++ b/tests/sla_print/sla_archive_readwrite_tests.cpp @@ -6,6 +6,7 @@ #include "libslic3r/Format/SLAArchiveFormatRegistry.hpp" #include "libslic3r/Format/SLAArchiveWriter.hpp" #include "libslic3r/Format/SLAArchiveReader.hpp" +#include "libslic3r/FileReader.hpp" #include @@ -20,7 +21,7 @@ TEST_CASE("Archive export test", "[sla_archives]") { SLAPrint print; SLAFullPrintConfig fullcfg; - auto m = Model::read_from_file(TEST_DATA_DIR PATH_SEPARATOR + std::string(pname) + ".obj", nullptr); + auto m = FileReader::load_model(TEST_DATA_DIR PATH_SEPARATOR + std::string(pname) + ".obj"); fullcfg.printer_technology.setInt(ptSLA); // FIXME this should be ensured fullcfg.set("sla_archive_format", entry.id); diff --git a/tests/slic3rutils/slic3r_arrangejob_tests.cpp b/tests/slic3rutils/slic3r_arrangejob_tests.cpp index 1cd35de5b4..ee492ff2cb 100644 --- a/tests/slic3rutils/slic3r_arrangejob_tests.cpp +++ b/tests/slic3rutils/slic3r_arrangejob_tests.cpp @@ -9,6 +9,7 @@ #include "slic3r/GUI/Jobs/ArrangeJob2.hpp" #include "libslic3r/Model.hpp" +#include "libslic3r/FileReader.hpp" #include "libslic3r/SLAPrint.hpp" #include "libslic3r/Format/3mf.hpp" @@ -84,7 +85,7 @@ TEST_CASE("Basic arrange with cube", "[arrangejob]") { DynamicPrintConfig cfg; cfg.load_from_ini(basepath + "default_fff.ini", ForwardCompatibilitySubstitutionRule::Enable); - Model m = Model::read_from_file(basepath + "20mm_cube.obj", &cfg); + Model m = FileReader::load_model(basepath + "20mm_cube.obj"); UIThreadWorker w; arr2::ArrangeSettings settings;