diff --git a/src/PrusaSlicer.cpp b/src/PrusaSlicer.cpp index 87a5553386..cd701a7b96 100644 --- a/src/PrusaSlicer.cpp +++ b/src/PrusaSlicer.cpp @@ -78,6 +78,214 @@ using namespace Slic3r; + +// struct which is filled from comand line input +struct Cli +{ + DynamicPrintConfig input_config; + DynamicPrintConfig overrides_config; + DynamicPrintConfig transform_config; + DynamicPrintConfig misc_config; + DynamicPrintConfig actions_config; + + std::vector input_files; + + bool read(int argc, const char* const argv[]); + + bool empty() { + return input_files.empty() + && input_config.empty() + && overrides_config.empty() + && transform_config.empty() + && actions_config.empty(); + } + +}; + + +enum class CliType +{ + Input, + Overrides, + Transformations, + Misc, + Actions +}; + + +bool Cli::read(int argc, const char* const argv[]) +{ + // cache the CLI option => opt_key mapping + std::map > opts; + + std::initializer_list> list = { + { cli_input_config_def, CliType::Input}, + { print_config_def, CliType::Overrides}, + { cli_transform_config_def, CliType::Transformations}, + { cli_misc_config_def, CliType::Misc}, + { cli_actions_config_def, CliType::Actions} + }; + + for (const auto& [config_def, type] : list) { + for (const auto& oit : config_def.options) + for (const std::string& t : oit.second.cli_args(oit.first)) + opts[t] = { oit.first , type }; + } + + bool parse_options = true; + for (int i = 1; i < argc; ++i) { + std::string token = argv[i]; + // Store non-option arguments in the provided vector. + if (!parse_options || !boost::starts_with(token, "-")) { + input_files.push_back(token); + continue; + } +#ifdef __APPLE__ + if (boost::starts_with(token, "-psn_")) + // OSX launcher may add a "process serial number", for example "-psn_0_989382" to the command line. + // While it is supposed to be dropped since OSX 10.9, we will rather ignore it. + continue; +#endif /* __APPLE__ */ + // Stop parsing tokens as options when -- is supplied. + if (token == "--") { + parse_options = false; + continue; + } + // Remove leading dashes (one or two). + token.erase(token.begin(), token.begin() + (boost::starts_with(token, "--") ? 2 : 1)); + // Read value when supplied in the --key=value form. + std::string value; + { + size_t equals_pos = token.find("="); + if (equals_pos != std::string::npos) { + value = token.substr(equals_pos + 1); + token.erase(equals_pos); + } + } + // Look for the cli -> option mapping. + auto it = opts.find(token); + bool no = false; + if (it == opts.end()) { + // Remove the "no-" prefix used to negate boolean options. + std::string yes_token; + if (boost::starts_with(token, "no-")) { + yes_token = token.substr(3); + it = opts.find(yes_token); + no = true; + } + if (it == opts.end()) { + boost::nowide::cerr << "Unknown option --" << token.c_str() << std::endl; + return false; + } + if (no) + token = yes_token; + } + + //const t_config_option_key& opt_key = it->second.first; + //const ConfigOptionDef& optdef = *this->option_def(opt_key); + const auto& [opt_key, type] = it->second; + const ConfigDef* config_def; + DynamicPrintConfig* config; + if (type == CliType::Input) { + config_def = &cli_input_config_def; + config = &input_config; + } + else if (type == CliType::Transformations) { + config_def = &cli_transform_config_def; + config = &transform_config; + } + else if(type == CliType::Misc) { + config_def = &cli_misc_config_def; + config = &misc_config; + } + else if(type == CliType::Actions) { + config_def = &cli_actions_config_def; + config = &actions_config; + } + else { + config_def = &print_config_def; + config = &overrides_config; + } + + const ConfigOptionDef* optdef = config_def->get(opt_key); + assert(optdef); + + // If the option type expects a value and it was not already provided, + // look for it in the next token. + if (value.empty() && optdef->type != coBool && optdef->type != coBools) { + if (i == argc - 1) { + boost::nowide::cerr << "No value supplied for --" << token.c_str() << std::endl; + return false; + } + value = argv[++i]; + } + + if (no) { + assert(optdef->type == coBool || optdef->type == coBools); + if (!value.empty()) { + boost::nowide::cerr << "Boolean options negated by the --no- prefix cannot have a value." << std::endl; + return false; + } + } + + // Store the option value. + const bool existing = config->has(opt_key); + ConfigOption* opt_base = existing ? config->option(opt_key) : optdef->create_default_option(); + if (!existing) + config->set_key_value(opt_key, opt_base); + ConfigOptionVectorBase* opt_vector = opt_base->is_vector() ? static_cast(opt_base) : nullptr; + if (opt_vector) { + if (!existing) + // remove the default values + opt_vector->clear(); + // Vector values will be chained. Repeated use of a parameter will append the parameter or parameters + // to the end of the value. + if (opt_base->type() == coBools && value.empty()) + static_cast(opt_base)->values.push_back(!no); + else + // Deserialize any other vector value (ConfigOptionInts, Floats, Percents, Points) the same way + // they get deserialized from an .ini file. For ConfigOptionStrings, that means that the C-style unescape + // will be applied for values enclosed in quotes, while values non-enclosed in quotes are left to be + // unescaped by the calling shell. + opt_vector->deserialize(value, true); + } + else if (opt_base->type() == coBool) { + if (value.empty()) + static_cast(opt_base)->value = !no; + else + opt_base->deserialize(value); + } + else if (opt_base->type() == coString) { + // Do not unescape single string values, the unescaping is left to the calling shell. + static_cast(opt_base)->value = value; + } + else { + // Just bail out if the configuration value is not understood. + ConfigSubstitutionContext context(ForwardCompatibilitySubstitutionRule::Disable); + // Any scalar value of a type different from Bool and String. + if (!config->set_deserialize_nothrow(opt_key, value, context, false)) { + boost::nowide::cerr << "Invalid value supplied for --" << token.c_str() << std::endl; + return false; + } + } + } + + // normalize override options + if (!overrides_config.empty()) + overrides_config.normalize_fdm(); + + if (!misc_config.has("config_compatibility")) { + // "config_compatibility" can be used during the loading configuration + // So, if this option wasn't set, then initialise it from default value + const ConfigOptionDef* optdef = cli_misc_config_def.get("config_compatibility"); + ConfigOption* opt_with_def_value = optdef->create_default_option(); + if (opt_with_def_value) + misc_config.set_key_value("config_compatibility", opt_with_def_value); + } + + return true; +} + static void print_help(bool include_print_options = false, PrinterTechnology printer_technology = ptAny) { boost::nowide::cout @@ -89,12 +297,19 @@ static void print_help(bool include_print_options = false, PrinterTechnology pri #endif /* SLIC3R_GUI */ << std::endl << "https://github.com/prusa3d/PrusaSlicer" << std::endl << std::endl - << "Usage: prusa-slicer [ ACTIONS ] [ TRANSFORM ] [ OPTIONS ] [ file.stl ... ]" << std::endl + << "Usage: prusa-slicer [ INPUT ] [ OPTIONS ] [ ACTIONS ] [ TRANSFORM ] [ file.stl ... ]" << std::endl; + + boost::nowide::cout + << std::endl + << "Input:" << std::endl; + cli_input_config_def.print_cli_help(boost::nowide::cout, false); + + boost::nowide::cout << std::endl << "Actions:" << std::endl; - cli_actions_config_def.print_cli_help(boost::nowide::cout, false); - // << std::endl - // << "Profiles sharing options:" << std::endl; + cli_actions_config_def.print_cli_help(boost::nowide::cout, false)//; + << std::endl + << "Profiles sharing options:" << std::endl; cli_profiles_sharing_config_def.print_cli_help(boost::nowide::cout, false); boost::nowide::cout @@ -113,7 +328,7 @@ static void print_help(bool include_print_options = false, PrinterTechnology pri << "\t1) Config keys from the command line, for example --fill-pattern=stars" << std::endl << "\t (highest priority, overwrites everything below)" << std::endl << "\t2) Config files loaded with --load" << std::endl - << "\t3) Config values loaded from amf or 3mf files" << std::endl; + << "\t3) Config values loaded from 3mf files" << std::endl; if (include_print_options) { boost::nowide::cout << std::endl; @@ -149,37 +364,44 @@ static bool can_apply_printer_technology(PrinterTechnology& printer_technology, return !invalid_other_pt; } -static bool process_profiles_sharing(const CliInParams& in) +static bool has_profile_sharing_action(const Cli& cli) { - if (in.profiles_sharing.empty()) + return cli.actions_config.has("query-printer-models") || cli.actions_config.has("query-print-filament-profiles"); +} + +static bool has_full_config_from_profiles(const Cli& cli) +{ + const DynamicPrintConfig& input = cli.input_config; + return !has_profile_sharing_action(cli) && + (input.has("print-profile") && !input.opt_string("print-profile").empty() || + input.has("material-profile") && !input.option("material-profile")->values.empty() || + input.has("printer-profile") && !input.opt_string("printer-profile").empty()); +} + +static bool process_profiles_sharing(const Cli& cli) +{ + if (!has_profile_sharing_action(cli)) return false; std::string ret; - for (auto const& opt_key : in.profiles_sharing) { - if (opt_key == "query-printer-models") { - ret = Slic3r::get_json_printer_models(get_printer_technology(in.config)); - break; - } - else - if (opt_key == "query-print-filament-profiles") { - const std::string printer_profile = in.config.opt_string("printer-profile"); - if (printer_profile.empty()) { - boost::nowide::cerr << opt_key << " error: This action requires set 'printer-profile' option" << std::endl; - return true; - } + if (cli.actions_config.has("query-printer-models")) { + ret = Slic3r::get_json_printer_models(get_printer_technology(cli.overrides_config)); + } + else if (cli.actions_config.has("query-print-filament-profiles")) { + if (cli.input_config.has("printer-profile") && !cli.input_config.opt_string("printer-profile").empty()) { + const std::string printer_profile = cli.input_config.opt_string("printer-profile"); ret = Slic3r::get_json_print_filament_profiles(printer_profile); if (ret.empty()) { - boost::nowide::cerr << opt_key << " error: Printer profile '" << printer_profile << + boost::nowide::cerr << "query-print-filament-profiles error: Printer profile '" << printer_profile << "' wasn't found among installed printers." << std::endl << "Or the request can be wrong." << std::endl; return true; } - break; } else { - boost::nowide::cerr << "error: option not implemented yet: " << opt_key << std::endl; - break; + boost::nowide::cerr << "query-print-filament-profiles error: This action requires set 'printer-profile' option" << std::endl; + return true; } } @@ -190,10 +412,8 @@ static bool process_profiles_sharing(const CliInParams& in) // use --output when available - std::string cmdline_param = in.config.opt_string("output"); - if (cmdline_param.empty()) - printf("%s", ret.c_str()); - else { + if (cli.misc_config.has("output")) { + std::string cmdline_param = cli.misc_config.opt_string("output"); // if we were supplied a directory, use it and append our automatically generated filename boost::filesystem::path cmdline_path(cmdline_param); boost::filesystem::path proposed_path = boost::filesystem::path(Slic3r::resources_dir()) / "out.json"; @@ -212,6 +432,8 @@ static bool process_profiles_sharing(const CliInParams& in) boost::nowide::cout << "Output for your request is written into " << file << std::endl; } + else + printf("%s", ret.c_str()); return true; } @@ -225,197 +447,171 @@ static void print_config_substitutions(const ConfigSubstitutions& config_substit boost::nowide::cout << "\tkey = \"" << subst.opt_def->opt_key << "\"\t loaded = \"" << subst.old_value << "\tsubstituted = \"" << subst.new_value->serialize() << "\"\n"; } -static bool apply_model_and_print_config_from_file(const std::string& file, Model& model, CliOutParams& cli) +static bool process_transform(Cli& cli, const DynamicPrintConfig& print_config, std::vector& models) { - // When loading an AMF or 3MF, config is imported as well, including the printer technology. - DynamicPrintConfig config; - ConfigSubstitutionContext config_substitutions_ctxt(cli.config_substitution_rule); - //FIXME should we check the version here? // | Model::LoadAttribute::CheckVersion ? - model = FileReader::load_model_with_config(file, &config, &config_substitutions, prusaslicer_generator_version, FileReader::LoadAttribute::AddDefaultInstances); - if (!can_apply_printer_technology(cli.printer_technology, get_printer_technology(config))) - return false; + DynamicPrintConfig& transform = cli.transform_config; + DynamicPrintConfig& actions = cli.actions_config; - print_config_substitutions(config_substitutions_ctxt.substitutions, file); + const Vec2crd gap{ s_multiple_beds.get_bed_gap() }; + arr2::ArrangeBed bed = arr2::to_arrange_bed(get_bed_shape(print_config), gap); + arr2::ArrangeSettings arrange_cfg; + if (transform.has("merge") || transform.has("duplicate")) + arrange_cfg.set_distance_from_objects(min_object_distance(print_config)); - // config is applied to cli.print_config before the current m_config values. - config += std::move(cli.print_config); - cli.print_config = std::move(config); - - return true; -} - -static bool has_print_action(const DynamicPrintAndCLIConfig& config) -{ - return config.opt_bool("export_gcode") || config.opt_bool("export_sla"); -} - -static bool process_transform( CliInParams& in, - CliOutParams& out, - const arr2::ArrangeBed& bed, - const arr2::ArrangeSettingsView& arrange_cfg) -{ - for (auto const& opt_key : in.transforms) { - if (opt_key == "merge") { - Model m; - for (auto& model : out.models) - for (ModelObject* o : model.objects) - m.add_object(*o); - // Rearrange instances unless --dont-arrange is supplied - if (!in.config.opt_bool("dont_arrange")) { - m.add_default_instances(); - if (has_print_action(in.config)) - arrange_objects(m, bed, arrange_cfg); - else - arrange_objects(m, arr2::InfiniteBed{}, arrange_cfg); - } - out.models.clear(); - out.models.emplace_back(std::move(m)); + if (transform.has("merge")) { + Model m; + for (auto& model : models) + for (ModelObject* o : model.objects) + m.add_object(*o); + // Rearrange instances unless --dont-arrange is supplied + if (!transform.has("dont_arrange") && !transform.opt_bool("dont_arrange")) { + m.add_default_instances(); + if (actions.has("slice")) + arrange_objects(m, bed, arrange_cfg); + else + arrange_objects(m, arr2::InfiniteBed{}, arrange_cfg);//?????? } - else if (opt_key == "duplicate") { - for (auto& model : out.models) { - const bool all_objects_have_instances = std::none_of( - model.objects.begin(), model.objects.end(), - [](ModelObject* o) { return o->instances.empty(); } - ); + models.clear(); + models.emplace_back(std::move(m)); + } - int dups = in.config.opt_int("duplicate"); - if (!all_objects_have_instances) model.add_default_instances(); + if (transform.has("duplicate")) { + for (auto& model : models) { + const bool all_objects_have_instances = std::none_of( + model.objects.begin(), model.objects.end(), + [](ModelObject* o) { return o->instances.empty(); } + ); - try { - if (dups > 1) { - // if all input objects have defined position(s) apply duplication to the whole model - duplicate(model, size_t(dups), bed, arrange_cfg); - } - else { - arrange_objects(model, bed, arrange_cfg); - } + int dups = transform.opt_int("duplicate"); + if (!all_objects_have_instances) model.add_default_instances(); + + try { + if (dups > 1) { + // if all input objects have defined position(s) apply duplication to the whole model + duplicate(model, size_t(dups), bed, arrange_cfg); } - catch (std::exception& ex) { - boost::nowide::cerr << "error: " << ex.what() << std::endl; - return false; + else { + arrange_objects(model, bed, arrange_cfg); } } - } - else if (opt_key == "duplicate_grid") { - std::vector& ints = in.config.option("duplicate_grid")->values; - const int x = ints.size() > 0 ? ints.at(0) : 1; - const int y = ints.size() > 1 ? ints.at(1) : 1; - FullPrintConfig fff_print_config; - const double distance = fff_print_config.duplicate_distance.value; - for (auto& model : out.models) - model.duplicate_objects_grid(x, y, (distance > 0) ? distance : 6); // TODO: this is not the right place for setting a default - } - else if (opt_key == "center") { - out.user_center_specified = true; - for (auto& model : out.models) { - model.add_default_instances(); - // this affects instances: - model.center_instances_around_point(in.config.option("center")->value); - // this affects volumes: - //FIXME Vojtech: Who knows why the complete model should be aligned with Z as a single rigid body? - //model.align_to_ground(); - BoundingBoxf3 bbox; - for (ModelObject* model_object : model.objects) - // We are interested into the Z span only, therefore it is sufficient to measure the bounding box of the 1st instance only. - bbox.merge(model_object->instance_bounding_box(0, false)); - for (ModelObject* model_object : model.objects) - for (ModelInstance* model_instance : model_object->instances) - model_instance->set_offset(Z, model_instance->get_offset(Z) - bbox.min.z()); - } - } - else if (opt_key == "align_xy") { - const Vec2d& p = in.config.option("align_xy")->value; - for (auto& model : out.models) { - BoundingBoxf3 bb = model.bounding_box_exact(); - // this affects volumes: - model.translate(-(bb.min.x() - p.x()), -(bb.min.y() - p.y()), -bb.min.z()); - } - } - else if (opt_key == "dont_arrange") { - // do nothing - this option alters other transform options - } - else if (opt_key == "ensure_on_bed") { - // do nothing, the value is used later - } - else if (opt_key == "rotate") { - for (auto& model : out.models) - for (auto& o : model.objects) - // this affects volumes: - o->rotate(Geometry::deg2rad(in.config.opt_float(opt_key)), Z); - } - else if (opt_key == "rotate_x") { - for (auto& model : out.models) - for (auto& o : model.objects) - // this affects volumes: - o->rotate(Geometry::deg2rad(in.config.opt_float(opt_key)), X); - } - else if (opt_key == "rotate_y") { - for (auto& model : out.models) - for (auto& o : model.objects) - // this affects volumes: - o->rotate(Geometry::deg2rad(in.config.opt_float(opt_key)), Y); - } - else if (opt_key == "scale") { - for (auto& model : out.models) - for (auto& o : model.objects) - // this affects volumes: - o->scale(in.config.get_abs_value(opt_key, 1)); - } - else if (opt_key == "scale_to_fit") { - const Vec3d& opt = in.config.opt(opt_key)->value; - if (opt.x() <= 0 || opt.y() <= 0 || opt.z() <= 0) { - boost::nowide::cerr << "--scale-to-fit requires a positive volume" << std::endl; + catch (std::exception& ex) { + boost::nowide::cerr << "error: " << ex.what() << std::endl; return false; } - for (auto& model : out.models) - for (auto& o : model.objects) - // this affects volumes: - o->scale_to_fit(opt); } - else if (opt_key == "cut") { - std::vector new_models; - for (auto& model : out.models) { - model.translate(0, 0, -model.bounding_box_exact().min.z()); // align to z = 0 - size_t num_objects = model.objects.size(); - for (size_t i = 0; i < num_objects; ++i) { - Cut cut(model.objects.front(), 0, Geometry::translation_transform(in.config.opt_float("cut") * Vec3d::UnitZ()), - ModelObjectCutAttribute::KeepLower | ModelObjectCutAttribute::KeepUpper | ModelObjectCutAttribute::PlaceOnCutUpper); - auto cut_objects = cut.perform_with_plane(); - for (ModelObject* obj : cut_objects) - model.add_object(*obj); - model.delete_object(size_t(0)); - } - } + } + if (transform.has("duplicate_grid")) { + std::vector& ints = transform.option("duplicate_grid")->values; + const int x = ints.size() > 0 ? ints.at(0) : 1; + const int y = ints.size() > 1 ? ints.at(1) : 1; + const double distance = print_config.opt_float("duplicate_distance"); + for (auto& model : models) + model.duplicate_objects_grid(x, y, (distance > 0) ? distance : 6); // TODO: this is not the right place for setting a default + } + + if (transform.has("center")) { + for (auto& model : models) { + model.add_default_instances(); + // this affects instances: + model.center_instances_around_point(transform.option("center")->value); + // this affects volumes: + //FIXME Vojtech: Who knows why the complete model should be aligned with Z as a single rigid body? + //model.align_to_ground(); + BoundingBoxf3 bbox; + for (ModelObject* model_object : model.objects) + // We are interested into the Z span only, therefore it is sufficient to measure the bounding box of the 1st instance only. + bbox.merge(model_object->instance_bounding_box(0, false)); + for (ModelObject* model_object : model.objects) + for (ModelInstance* model_instance : model_object->instances) + model_instance->set_offset(Z, model_instance->get_offset(Z) - bbox.min.z()); + } + } - // TODO: copy less stuff around using pointers - out.models = new_models; + if (transform.has("align_xy")) { + const Vec2d& p = transform.option("align_xy")->value; + for (auto& model : models) { + BoundingBoxf3 bb = model.bounding_box_exact(); + // this affects volumes: + model.translate(-(bb.min.x() - p.x()), -(bb.min.y() - p.y()), -bb.min.z()); + } + } - if (in.actions.empty()) - in.actions.push_back("export_stl"); - } - else if (opt_key == "split") { - for (Model& model : out.models) { - 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); - model.delete_object(size_t(0)); - } - } - } - else if (opt_key == "delete-after-load") { - out.delete_after_load = true; - } - else { - boost::nowide::cerr << "error: option not implemented yet: " << opt_key << std::endl; + if (transform.has("rotate")) { + for (auto& model : models) + for (auto& o : model.objects) + // this affects volumes: + o->rotate(Geometry::deg2rad(transform.opt_float("rotate")), Z); + } + if (transform.has("rotate_x")) { + for (auto& model : models) + for (auto& o : model.objects) + // this affects volumes: + o->rotate(Geometry::deg2rad(transform.opt_float("rotate_x")), X); + } + if (transform.has("rotate_y")) { + for (auto& model : models) + for (auto& o : model.objects) + // this affects volumes: + o->rotate(Geometry::deg2rad(transform.opt_float("rotate_y")), Y); + } + + if (transform.has("scale")) { + for (auto& model : models) + for (auto& o : model.objects) + // this affects volumes: + o->scale(transform.get_abs_value("scale", 1)); + } + if (transform.has("scale_to_fit")) { + const Vec3d& opt = transform.opt("scale_to_fit")->value; + if (opt.x() <= 0 || opt.y() <= 0 || opt.z() <= 0) { + boost::nowide::cerr << "--scale-to-fit requires a positive volume" << std::endl; return false; } + for (auto& model : models) + for (auto& o : model.objects) + // this affects volumes: + o->scale_to_fit(opt); + } + + if (transform.has("cut")) { + std::vector new_models; + for (auto& model : models) { + model.translate(0, 0, -model.bounding_box_exact().min.z()); // align to z = 0 + size_t num_objects = model.objects.size(); + for (size_t i = 0; i < num_objects; ++i) { + Cut cut(model.objects.front(), 0, Geometry::translation_transform(transform.opt_float("cut") * Vec3d::UnitZ()), + ModelObjectCutAttribute::KeepLower | ModelObjectCutAttribute::KeepUpper | ModelObjectCutAttribute::PlaceOnCutUpper); + auto cut_objects = cut.perform_with_plane(); + for (ModelObject* obj : cut_objects) + model.add_object(*obj); + model.delete_object(size_t(0)); + } + } + + // TODO: copy less stuff around using pointers + models = new_models; + + if (actions.empty()) { + // cutting transformations are setting an "export" action. + actions.set_key_value("export_stl", new ConfigOptionBool(true)); + } + } + + if (transform.has("split")) { + for (Model& model : models) { + 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); + model.delete_object(size_t(0)); + } + } } // All transforms have been dealt with. Now ensure that the objects are on bed. // (Unless the user said otherwise.) - if (in.config.opt_bool("ensure_on_bed")) - for (auto& model : out.models) + if (!transform.has("ensure_on_bed") || transform.opt_bool("ensure_on_bed")) + for (auto& model : models) for (auto& o : model.objects) o->ensure_on_bed(); @@ -423,7 +619,18 @@ static bool process_transform( CliInParams& in, } -static std::string output_filepath(const Model& model, IO::ExportFormat format, const std::string cmdline_param) + +namespace IO { + enum ExportFormat : int { + OBJ, + STL, + // SVG, + TMF, + Gcode + }; +} + +static std::string output_filepath(const Model& model, IO::ExportFormat format, const std::string& cmdline_param) { std::string ext; switch (format) { @@ -445,7 +652,7 @@ static std::string output_filepath(const Model& model, IO::ExportFormat format, return proposed_path.string(); } -static bool export_models(std::vector& models, IO::ExportFormat format, const std::string cmdline_param) +static bool export_models(std::vector& models, IO::ExportFormat format, const std::string& cmdline_param) { for (Model& model : models) { const std::string path = output_filepath(model, format, cmdline_param); @@ -466,236 +673,241 @@ static bool export_models(std::vector& models, IO::ExportFormat format, c return true; } -static bool process_actions( CliInParams& in, - CliOutParams& out, - const arr2::ArrangeBed& bed, - const arr2::ArrangeSettingsView& arrange_cfg) +static std::function get_thumbnail_generator_cli(Print& fff_print) { + if (!fff_print.model().objects.empty() && boost::iends_with(fff_print.model().objects.front()->input_file, ".3mf")) { + std::string filename = fff_print.model().objects.front()->input_file; + return [filename](const ThumbnailsParams&) { + ThumbnailsList list_out; - for (auto const& opt_key : in.actions) { - if (opt_key == "help") { - print_help(); - } - else if (opt_key == "help_fff") { - print_help(true, ptFFF); - } - else if (opt_key == "help_sla") { - print_help(true, ptSLA); - } - else if (opt_key == "save") { - //FIXME check for mixing the FFF / SLA parameters. - // or better save fff_print_config vs. sla_print_config - out.print_config.save(in.config.opt_string("save")); - } - else if (opt_key == "info") { - // --info works on unrepaired model - for (Model& model : out.models) { - model.add_default_instances(); - model.print_info(); - } - } - else if (opt_key == "export_stl") { - for (auto& model : out.models) - model.add_default_instances(); - if (!export_models(out.models, IO::STL, in.config.opt_string("output"))) - return 1; - } - else if (opt_key == "export_obj") { - for (auto& model : out.models) - model.add_default_instances(); - if (!export_models(out.models, IO::OBJ, in.config.opt_string("output"))) - return 1; - } - else if (opt_key == "export_3mf") { - if (!export_models(out.models, IO::TMF, in.config.opt_string("output"))) - return 1; - } - else if (opt_key == "export_gcode" || opt_key == "export_sla" || opt_key == "slice") { - if (opt_key == "export_gcode" && out.printer_technology == ptSLA) { - boost::nowide::cerr << "error: cannot export G-code for an FFF configuration" << std::endl; - return 1; - } - else if (opt_key == "export_sla" && out.printer_technology == ptFFF) { - boost::nowide::cerr << "error: cannot export SLA slices for a SLA configuration" << std::endl; - return 1; - } - // Make a copy of the model if the current action is not the last action, as the model may be - // modified by the centering and such. - Model model_copy; - bool make_copy = &opt_key != &in.actions.back(); - for (Model& model_in : out.models) { - if (make_copy) - model_copy = model_in; - Model& model = make_copy ? model_copy : model_in; - // If all objects have defined instances, their relative positions will be - // honored when printing (they will be only centered, unless --dont-arrange - // is supplied); if any object has no instances, it will get a default one - // and all instances will be rearranged (unless --dont-arrange is supplied). - std::string outfile = in.config.opt_string("output"); - Print fff_print; - SLAPrint sla_print; - sla_print.set_status_callback( - [](const PrintBase::SlicingStatus& s) - { - if (s.percent >= 0) { // FIXME: is this sufficient? - printf("%3d%s %s\n", s.percent, "% =>", s.text.c_str()); - std::fflush(stdout); - } - }); + mz_zip_archive archive; + mz_zip_zero_struct(&archive); - PrintBase* print = (out.printer_technology == ptFFF) ? static_cast(&fff_print) : static_cast(&sla_print); - if (!in.config.opt_bool("dont_arrange")) { - if (out.user_center_specified) { - Vec2d c = in.config.option("center")->value; - arrange_objects(model, arr2::InfiniteBed{ scaled(c) }, arrange_cfg); - } - else - arrange_objects(model, bed, arrange_cfg); + if (!open_zip_reader(&archive, filename)) + return list_out; + mz_uint num_entries = mz_zip_reader_get_num_files(&archive); + mz_zip_archive_file_stat stat; + + int index = mz_zip_reader_locate_file(&archive, "Metadata/thumbnail.png", nullptr, 0); + if (index < 0 || !mz_zip_reader_file_stat(&archive, index, &stat)) + return list_out; + std::string buffer; + buffer.resize(int(stat.m_uncomp_size)); + mz_bool res = mz_zip_reader_extract_file_to_mem(&archive, stat.m_filename, buffer.data(), (size_t)stat.m_uncomp_size, 0); + if (res == 0) + return list_out; + close_zip_reader(&archive); + + std::vector data; + unsigned width = 0; + unsigned height = 0; + png::decode_png(buffer, data, width, height); + + { + // Flip the image vertically so it matches the convention in Thumbnails generator. + const int row_size = width * 4; // Each pixel is 4 bytes (RGBA) + std::vector temp_row(row_size); + for (int i = 0; i < height / 2; ++i) { + unsigned char* top_row = &data[i * row_size]; + unsigned char* bottom_row = &data[(height - i - 1) * row_size]; + std::copy(bottom_row, bottom_row + row_size, temp_row.begin()); + std::copy(top_row, top_row + row_size, bottom_row); + std::copy(temp_row.begin(), temp_row.end(), top_row); } - if (out.printer_technology == ptFFF) { - for (auto* mo : model.objects) - fff_print.auto_assign_extruders(mo); + } + + ThumbnailData th; + th.set(width, height); + th.pixels = data; + list_out.push_back(th); + return list_out; + }; + } + + return [](const ThumbnailsParams&) ->ThumbnailsList { return {}; }; +} + +static bool process_actions(Cli& cli, const DynamicPrintConfig& print_config, std::vector& models) +{ + DynamicPrintConfig& actions = cli.actions_config; + DynamicPrintConfig& transform = cli.transform_config; + + // doesn't need any aditional input + + if (actions.has("help")) { + print_help(); + } + if (actions.has("help_fff")) { + print_help(true, ptFFF); + } + if (actions.has("help_sla")) { + print_help(true, ptSLA); + } + + if (actions.has("info")) { + if (models.empty()) { + boost::nowide::cerr << "error: cannot show info for empty models." << std::endl; + return 1; + } + // --info works on unrepaired model + for (Model& model : models) { + model.add_default_instances(); + model.print_info(); + } + } + + if (actions.has("save")) { + //FIXME check for mixing the FFF / SLA parameters. + // or better save fff_print_config vs. sla_print_config + print_config.save(actions.opt_string("save")); + } + + if (models.empty() && (actions.has("export_stl") || actions.has("export_obj") || actions.has("export_3mf"))) { + boost::nowide::cerr << "error: cannot export empty models." << std::endl; + return 1; + } + + const std::string output = cli.misc_config.has("output") ? cli.misc_config.opt_string("output") : ""; + + if (actions.has("export_stl")) { + for (auto& model : models) + model.add_default_instances(); + if (!export_models(models, IO::STL, output)) + return 1; + } + if (actions.has("export_obj")) { + for (auto& model : models) + model.add_default_instances(); + if (!export_models(models, IO::OBJ, output)) + return 1; + } + if (actions.has("export_3mf")) { + if (!export_models(models, IO::TMF, output)) + return 1; + } + + if (actions.has("slice")) { + PrinterTechnology printer_technology = Preset::printer_technology(print_config); + + const Vec2crd gap{ s_multiple_beds.get_bed_gap() }; + arr2::ArrangeBed bed = arr2::to_arrange_bed(get_bed_shape(print_config), gap); + arr2::ArrangeSettings arrange_cfg; + arrange_cfg.set_distance_from_objects(min_object_distance(print_config)); + + for (Model& model : models) { + // If all objects have defined instances, their relative positions will be + // honored when printing (they will be only centered, unless --dont-arrange + // is supplied); if any object has no instances, it will get a default one + // and all instances will be rearranged (unless --dont-arrange is supplied). + if (!transform.has("dont_arrange") || !transform.opt_bool("dont_arrange")) { + if (transform.has("center")) { + Vec2d c = transform.option("center")->value; + arrange_objects(model, arr2::InfiniteBed{ scaled(c) }, arrange_cfg); } - print->apply(model, out.print_config); - std::string err = print->validate(); - if (!err.empty()) { - boost::nowide::cerr << err << std::endl; - return 1; - } - if (print->empty()) - boost::nowide::cout << "Nothing to print for " << outfile << " . Either the print is empty or no object is fully inside the print volume." << std::endl; else - try { - std::string outfile_final; - print->process(); - if (out.printer_technology == ptFFF) { - - - - - - std::function thumbnail_generator_cli; - if (!fff_print.model().objects.empty() && boost::iends_with(fff_print.model().objects.front()->input_file, ".3mf")) { - std::string filename = fff_print.model().objects.front()->input_file; - thumbnail_generator_cli = [filename](const ThumbnailsParams&) { - ThumbnailsList list_out; - - mz_zip_archive archive; - mz_zip_zero_struct(&archive); - - if (!open_zip_reader(&archive, filename)) - return list_out; - mz_uint num_entries = mz_zip_reader_get_num_files(&archive); - mz_zip_archive_file_stat stat; - - int index = mz_zip_reader_locate_file(&archive, "Metadata/thumbnail.png", nullptr, 0); - if (index < 0 || !mz_zip_reader_file_stat(&archive, index, &stat)) - return list_out; - std::string buffer; - buffer.resize(int(stat.m_uncomp_size)); - mz_bool res = mz_zip_reader_extract_file_to_mem(&archive, stat.m_filename, buffer.data(), (size_t)stat.m_uncomp_size, 0); - if (res == 0) - return list_out; - close_zip_reader(&archive); - - std::vector data; - unsigned width = 0; - unsigned height = 0; - png::decode_png(buffer, data, width, height); - - { - // Flip the image vertically so it matches the convention in Thumbnails generator. - const int row_size = width * 4; // Each pixel is 4 bytes (RGBA) - std::vector temp_row(row_size); - for (int i = 0; i < height / 2; ++i) { - unsigned char* top_row = &data[i * row_size]; - unsigned char* bottom_row = &data[(height - i - 1) * row_size]; - std::copy(bottom_row, bottom_row + row_size, temp_row.begin()); - std::copy(top_row, top_row + row_size, bottom_row); - std::copy(temp_row.begin(), temp_row.end(), top_row); - } - } - - ThumbnailData th; - th.set(width, height); - th.pixels = data; - list_out.push_back(th); - return list_out; - }; - } - - - - - - // The outfile is processed by a PlaceholderParser. - outfile = fff_print.export_gcode(outfile, nullptr, thumbnail_generator_cli); - outfile_final = fff_print.print_statistics().finalize_output_path(outfile); - } - else { - outfile = sla_print.output_filepath(outfile); - // We need to finalize the filename beforehand because the export function sets the filename inside the zip metadata - outfile_final = sla_print.print_statistics().finalize_output_path(outfile); - sla_print.export_print(outfile_final); - } - if (outfile != outfile_final) { - if (Slic3r::rename_file(outfile, outfile_final)) { - boost::nowide::cerr << "Renaming file " << outfile << " to " << outfile_final << " failed" << std::endl; - return false; - } - outfile = outfile_final; - } - // Run the post-processing scripts if defined. - run_post_process_scripts(outfile, fff_print.full_print_config()); - boost::nowide::cout << "Slicing result exported to " << outfile << std::endl; - } - catch (const std::exception& ex) { - boost::nowide::cerr << ex.what() << std::endl; - return false; - } -/* - print.center = ! in.config.has("center") - && ! in.config.has("align_xy") - && ! in.config.opt_bool("dont_arrange"); - print.set_model(model); - - // start chronometer - typedef std::chrono::high_resolution_clock clock_; - typedef std::chrono::duration > second_; - std::chrono::time_point t0{ clock_::now() }; - - const std::string outfile = this->output_filepath(model, IO::Gcode); - try { - print.export_gcode(outfile); - } catch (std::runtime_error &e) { - boost::nowide::cerr << e.what() << std::endl; - return 1; - } - boost::nowide::cout << "G-code exported to " << outfile << std::endl; - - // output some statistics - double duration { std::chrono::duration_cast(clock_::now() - t0).count() }; - boost::nowide::cout << std::fixed << std::setprecision(0) - << "Done. Process took " << (duration/60) << " minutes and " - << std::setprecision(3) - << std::fmod(duration, 60.0) << " seconds." << std::endl - << std::setprecision(2) - << "Filament required: " << print.total_used_filament() << "mm" - << " (" << print.total_extruded_volume()/1000 << "cm3)" << std::endl; -*/ + arrange_objects(model, bed, arrange_cfg); } - } - else { - boost::nowide::cerr << "error: option not supported yet: " << opt_key << std::endl; - return false; + + Print fff_print; + SLAPrint sla_print; + sla_print.set_status_callback( [](const PrintBase::SlicingStatus& s) { + if (s.percent >= 0) { // FIXME: is this sufficient? + printf("%3d%s %s\n", s.percent, "% =>", s.text.c_str()); + std::fflush(stdout); + } + }); + + PrintBase* print = (printer_technology == ptFFF) ? static_cast(&fff_print) : static_cast(&sla_print); + if (printer_technology == ptFFF) { + for (auto* mo : model.objects) + fff_print.auto_assign_extruders(mo); + } + print->apply(model, print_config); + std::string err = print->validate(); + if (!err.empty()) { + boost::nowide::cerr << err << std::endl; + return 1; + } + + std::string outfile = output; + + if (print->empty()) + boost::nowide::cout << "Nothing to print for " << outfile << " . Either the print is empty or no object is fully inside the print volume." << std::endl; + else + try { + std::string outfile_final; + print->process(); + if (printer_technology == ptFFF) { + // The outfile is processed by a PlaceholderParser. + outfile = fff_print.export_gcode(outfile, nullptr, get_thumbnail_generator_cli(fff_print)); + outfile_final = fff_print.print_statistics().finalize_output_path(outfile); + } + else { + outfile = sla_print.output_filepath(outfile); + // We need to finalize the filename beforehand because the export function sets the filename inside the zip metadata + outfile_final = sla_print.print_statistics().finalize_output_path(outfile); + sla_print.export_print(outfile_final); + } + if (outfile != outfile_final) { + if (Slic3r::rename_file(outfile, outfile_final)) { + boost::nowide::cerr << "Renaming file " << outfile << " to " << outfile_final << " failed" << std::endl; + return false; + } + outfile = outfile_final; + } + // Run the post-processing scripts if defined. + run_post_process_scripts(outfile, fff_print.full_print_config()); + boost::nowide::cout << "Slicing result exported to " << outfile << std::endl; + } + catch (const std::exception& ex) { + boost::nowide::cerr << ex.what() << std::endl; + return false; + } + } } return true; } +#ifdef SLIC3R_GUI +static int start_gui_with_params(GUI::GUI_InitParams& params) +{ + #if !defined(_WIN32) && !defined(__APPLE__) + // likely some linux / unix system + const char* display = boost::nowide::getenv("DISPLAY"); + // const char *wayland_display = boost::nowide::getenv("WAYLAND_DISPLAY"); + //if (! ((display && *display) || (wayland_display && *wayland_display))) { + if (!(display && *display)) { + // DISPLAY not set. + boost::nowide::cerr << "DISPLAY not set, GUI mode not available." << std::endl << std::endl; + this->print_help(false); + // Indicate an error. + return 1; + } + #endif // some linux / unix system + return Slic3r::GUI::GUI_Run(params); +} -int CLI::run(int argc, char **argv) +static int start_as_gcode_viewer(const Cli& cli, GUI::GUI_InitParams& gui_params) +{ + if (cli.input_files.size() > 1) { + boost::nowide::cerr << "You can open only one .gcode file at a time in GCodeViewer" << std::endl; + return 1; + } + + if (cli.input_files.size() == 1) { + if (!is_gcode_file(cli.input_files[0]) || !boost::filesystem::exists(cli.input_files[0])) { + boost::nowide::cerr << "Input file isn't a .gcode file or doesn't exist. GCodeViewer can't be start." << std::endl; + return 1; + } + } + + return start_gui_with_params(gui_params); +} +#endif // SLIC3R_GUI + +static bool setup_general() { // Mark the main thread for the debugger and for runtime checks. set_current_thread_name("slic3r_main"); @@ -713,359 +925,28 @@ int CLI::run(int argc, char **argv) ::setenv("WEBKIT_DISABLE_DMABUF_RENDERER", "1", /* replace */ false); #endif - // Switch boost::filesystem to utf8. + // Switch boost::filesystem to utf8. try { boost::nowide::nowide_filesystem(); - } catch (const std::runtime_error& ex) { + } + catch (const std::runtime_error& ex) { std::string caption = std::string(SLIC3R_APP_NAME) + " Error"; std::string text = std::string("An error occured while setting up locale.\n") + ( #if !defined(_WIN32) && !defined(__APPLE__) - // likely some linux system - "You may need to reconfigure the missing locales, likely by running the \"locale-gen\" and \"dpkg-reconfigure locales\" commands.\n" + // likely some linux system + "You may need to reconfigure the missing locales, likely by running the \"locale-gen\" and \"dpkg-reconfigure locales\" commands.\n" #endif - SLIC3R_APP_NAME " will now terminate.\n\n") + ex.what(); + SLIC3R_APP_NAME " will now terminate.\n\n") + ex.what(); #if defined(_WIN32) && defined(SLIC3R_GUI) MessageBoxA(NULL, text.c_str(), caption.c_str(), MB_OK | MB_ICONERROR); #endif boost::nowide::cerr << text.c_str() << std::endl; - return 1; + return false; } - CliInParams cli_in; - if (! this->setup(argc, argv, cli_in)) - return 1; - - if (process_profiles_sharing(cli_in)) - return 1; - - CliOutParams cli_out; - - /* ysFIXME it looks like m_extra_config is a redundant parametr - * because of m_extra_config is empty from the very beginning and is applied with ignorring of the non-existing options - * - m_extra_config.apply(m_config, true); - m_extra_config.normalize_fdm(); - */ - - cli_out.printer_technology = get_printer_technology(cli_in.config); - - bool start_gui = cli_in.actions.empty() && - // cutting transformations are setting an "export" action. - std::find(cli_in.transforms.begin(), cli_in.transforms.end(), "cut") == cli_in.transforms.end(); - bool start_downloader = false; - std::string download_url; - bool start_as_gcodeviewer = -#ifdef _WIN32 - false; -#else - // On Unix systems, the prusa-slicer binary may be symlinked to give the application a different meaning. - boost::algorithm::iends_with(boost::filesystem::path(argv[0]).filename().string(), "gcodeviewer"); -#endif // _WIN32 - - const std::vector &load_configs = cli_in.config.option("load", true)->values; - cli_out.config_substitution_rule = cli_in.config.option>("config_compatibility", true)->value; - - // load config files supplied via --load - for (auto const &file : load_configs) { - if (! boost::filesystem::exists(file)) { - if (cli_in.config.opt_bool("ignore_nonexistent_config")) { - continue; - } else { - boost::nowide::cerr << "No such file: " << file << std::endl; - return 1; - } - } - DynamicPrintConfig config; - ConfigSubstitutions config_substitutions; - try { - config_substitutions = config.load(file, cli_out.config_substitution_rule); - } catch (std::exception &ex) { - boost::nowide::cerr << "Error while reading config file \"" << file << "\": " << ex.what() << std::endl; - return 1; - } - - if (!can_apply_printer_technology(cli_out.printer_technology, get_printer_technology(config))) - return 1; - - print_config_substitutions(config_substitutions, file); - - config.normalize_fdm(); - cli_out.print_config.apply(config); - } - - bool has_config_from_profiles = cli_in.profiles_sharing.empty() && - (!cli_in.config.opt_string("print-profile").empty() || - !cli_in.config.option("material-profile")->values.empty() || - !cli_in.config.opt_string("printer-profile").empty() ); - - // load config from profiles set - if (has_config_from_profiles) { - Slic3r::DynamicPrintConfig config = {}; - std::string errors = Slic3r::load_full_print_config(cli_in.config.opt_string("print-profile"), - cli_in.config.option("material-profile")->values, - cli_in.config.opt_string("printer-profile"), - config, cli_out.printer_technology); - if (!errors.empty()) { - boost::nowide::cerr << errors << std::endl; - return 1; - } - - if (!can_apply_printer_technology(cli_out.printer_technology, get_printer_technology(config))) - return 1; - - config.normalize_fdm(); - cli_out.print_config.apply(config); - } - -#ifdef SLIC3R_GUI - if (cli_in.config.has("webdev")) { - Utils::ServiceConfig::instance().set_webdev_enabled(cli_in.config.opt_bool("webdev")); - } - std::vector::iterator it; - bool opengl_aa = false; - it = std::find(cli_in.actions.begin(), cli_in.actions.end(), "opengl-aa"); - if (it != cli_in.actions.end()) { - start_gui = true; - opengl_aa = true; - cli_in.actions.erase(it); - } -#if SLIC3R_OPENGL_ES - // are we starting as gcodeviewer ? - for (auto it = cli_in.actions.begin(); it != cli_in.actions.end(); ++it) { - if (*it == "gcodeviewer") { - start_gui = true; - start_as_gcodeviewer = true; - cli_in.actions.erase(it); - break; - } - } -#else - std::pair opengl_version = { 0, 0 }; - bool opengl_debug = false; - bool opengl_compatibility_profile = false; - - // search for special keys into command line parameters - it = std::find(cli_in.actions.begin(), cli_in.actions.end(), "gcodeviewer"); - if (it != cli_in.actions.end()) { - start_gui = true; - start_as_gcodeviewer = true; - cli_in.actions.erase(it); - } - - it = std::find(cli_in.actions.begin(), cli_in.actions.end(), "opengl-version"); - if (it != cli_in.actions.end()) { - const Semver opengl_minimum = Semver(3,2,0); - const std::string opengl_version_str = cli_in.config.opt_string("opengl-version"); - boost::optional semver = Semver::parse(opengl_version_str); - if (semver.has_value() && (*semver) >= opengl_minimum ) { - opengl_version.first = semver->maj(); - opengl_version.second = semver->min(); - if (std::find(Slic3r::GUI::OpenGLVersions::core.begin(), Slic3r::GUI::OpenGLVersions::core.end(), std::make_pair(opengl_version.first, opengl_version.second)) == Slic3r::GUI::OpenGLVersions::core.end()) { - opengl_version = { 0, 0 }; - boost::nowide::cerr << "Required OpenGL version " << opengl_version_str << " not recognized.\n Option 'opengl-version' ignored." << std::endl; - } - } else - boost::nowide::cerr << "Required OpenGL version " << opengl_version_str << " is invalid. Must be greater than or equal to " << - opengl_minimum.to_string() << "\n Option 'opengl-version' ignored." << std::endl; - start_gui = true; - cli_in.actions.erase(it); - } - - it = std::find(cli_in.actions.begin(), cli_in.actions.end(), "opengl-compatibility"); - if (it != cli_in.actions.end()) { - start_gui = true; - opengl_compatibility_profile = true; - // reset version as compatibility profile always take the highest version - // supported by the graphic card - opengl_version = std::make_pair(0, 0); - cli_in.actions.erase(it); - } - - it = std::find(cli_in.actions.begin(), cli_in.actions.end(), "opengl-debug"); - if (it != cli_in.actions.end()) { - start_gui = true; - opengl_debug = true; - cli_in.actions.erase(it); - } -#endif // SLIC3R_OPENGL_ES -#else // SLIC3R_GUI - // If there is no GUI, we shall ignore the parameters. Remove them from the list. - for (const std::string& s : { "opengl-version", "opengl-compatibility", "opengl-debug", "opengl-aa", "gcodeviewer" }) { - auto it = std::find(cli_in.actions.cbegin(), cli_in.actions.cend(), s); - if (it != cli_in.actions.end()) { - boost::nowide::cerr << "Parameter '" << s << "' is ignored, this PrusaSlicer build is CLI only." << std::endl; - cli_in.actions.erase(it); - } - } -#endif // SLIC3R_GUI - - - // Read input file(s) if any. - for (const std::string& file : cli_in.input_files) - if (is_gcode_file(file) && boost::filesystem::exists(file)) { - start_as_gcodeviewer = true; - break; - } - - if (!start_as_gcodeviewer) { - for (const std::string& file : cli_in.input_files) { - if (boost::starts_with(file, "prusaslicer://")) { - start_downloader = true; - download_url = file; - continue; - } - if (!boost::filesystem::exists(file)) { - boost::nowide::cerr << "No such file: " << file << std::endl; - exit(1); - } - Model model; - try { - if (has_config_from_profiles) - model = FileReader::load_model(file); - else if (!apply_model_and_print_config_from_file(file, model, cli_out)) - return 1; - - // If model for slicing is loaded from 3mf file, then its geometry has to be used and arrange couldn't be apply for this model. - if ((boost::algorithm::iends_with(file, ".3mf") || boost::algorithm::iends_with(file, ".zip")) && !cli_in.config.opt_bool("dont_arrange")) { - //So, check a state of "dont_arrange" parameter and set it to true, if its value is false. - cli_in.config.set_key_value("dont_arrange", new ConfigOptionBool(true)); - } - } - catch (std::exception& e) { - boost::nowide::cerr << file << ": " << e.what() << std::endl; - return 1; - } - if (model.objects.empty()) { - boost::nowide::cerr << "Error: file is empty: " << file << std::endl; - continue; - } - cli_out.models.push_back(model); - } - } - - if (!start_gui) { - const auto* post_process = cli_out.print_config.opt("post_process"); - if (post_process != nullptr && !post_process->values.empty()) { - boost::nowide::cout << "\nA post-processing script has been detected in the config data:\n\n"; - for (const auto& s : post_process->values) { - boost::nowide::cout << "> " << s << "\n"; - } - boost::nowide::cout << "\nContinue(Y/N) ? "; - char in; - boost::nowide::cin >> in; - if (in != 'Y' && in != 'y') - return 0; - } - } - - /* ysFIXME - // Apply command line options to a more specific DynamicPrintConfig which provides normalize() - // (command line options override --load files) - cli.print_config.apply(cli_in.extra_config, true); - // Normalizing after importing the 3MFs / AMFs - cli.print_config.normalize_fdm(); - */ - - if (cli_out.printer_technology == ptUnknown) - cli_out.printer_technology = std::find(cli_in.actions.begin(), cli_in.actions.end(), "export_sla") == cli_in.actions.end() ? ptFFF : ptSLA; - cli_out.print_config.option>("printer_technology", true)->value = cli_out.printer_technology; - - // Initialize full print configs for both the FFF and SLA technologies. - FullPrintConfig fff_print_config; - SLAFullPrintConfig sla_print_config; - - // Synchronize the default parameters and the ones received on the command line. - if (cli_out.printer_technology == ptFFF) { - fff_print_config.apply(cli_out.print_config, true); - cli_out.print_config.apply(fff_print_config, true); - } else { - assert(cli_out.printer_technology == ptSLA); - sla_print_config.output_filename_format.value = "[input_filename_base].sl1"; - - // The default bed shape should reflect the default display parameters - // and not the fff defaults. - double w = sla_print_config.display_width.getFloat(); - double h = sla_print_config.display_height.getFloat(); - sla_print_config.bed_shape.values = { Vec2d(0, 0), Vec2d(w, 0), Vec2d(w, h), Vec2d(0, h) }; - - sla_print_config.apply(cli_out.print_config, true); - cli_out.print_config.apply(sla_print_config, true); - } - { - std::string validity = cli_out.print_config.validate(); - if (! validity.empty()) { - boost::nowide::cerr << "Error: The composite configation is not valid: " << validity << std::endl; - return 1; - } - } - - const Vec2crd gap{s_multiple_beds.get_bed_gap()}; - arr2::ArrangeBed bed = arr2::to_arrange_bed(get_bed_shape(cli_out.print_config), gap); - arr2::ArrangeSettings arrange_cfg; - arrange_cfg.set_distance_from_objects(min_object_distance(cli_out.print_config)); - - // Loop through transform options. - if (!process_transform(cli_in, cli_out, bed, arrange_cfg)) - return 1; - - // loop through action options - if (!process_actions(cli_in, cli_out, bed, arrange_cfg)) - return 1; - - if (start_gui) { -#ifdef SLIC3R_GUI - #if !defined(_WIN32) && !defined(__APPLE__) - // likely some linux / unix system - const char *display = boost::nowide::getenv("DISPLAY"); - // const char *wayland_display = boost::nowide::getenv("WAYLAND_DISPLAY"); - //if (! ((display && *display) || (wayland_display && *wayland_display))) { - if (! (display && *display)) { - // DISPLAY not set. - boost::nowide::cerr << "DISPLAY not set, GUI mode not available." << std::endl << std::endl; - this->print_help(false); - // Indicate an error. - return 1; - } - #endif // some linux / unix system - Slic3r::GUI::GUI_InitParams params; - params.argc = argc; - params.argv = argv; - params.load_configs = load_configs; -// params.extra_config = std::move(m_extra_config); - params.input_files = std::move(cli_in.input_files); - if (has_config_from_profiles && params.input_files.empty()) { - params.selected_presets = Slic3r::GUI::CLISelectedProfiles{ cli_in.config.opt_string("print-profile"), - cli_in.config.opt_string("printer-profile") , - cli_in.config.option("material-profile")->values }; - } - params.start_as_gcodeviewer = start_as_gcodeviewer; - params.start_downloader = start_downloader; - params.download_url = download_url; - params.delete_after_load = cli_out.delete_after_load; - params.opengl_aa = opengl_aa; -#if !SLIC3R_OPENGL_ES - params.opengl_version = opengl_version; - params.opengl_debug = opengl_debug; - params.opengl_compatibiity_profile = opengl_compatibility_profile; -#endif // !SLIC3R_OPENGL_ES - return Slic3r::GUI::GUI_Run(params); -#else // SLIC3R_GUI - // No GUI support. Just print out a help. - this->print_help(false); - // If started without a parameter, consider it to be OK, otherwise report an error code (no action etc). - return (argc == 0) ? 0 : 1; -#endif // SLIC3R_GUI - } - - return 0; -} - -bool CLI::setup(int argc, char **argv, CliInParams& in) -{ - { - Slic3r::set_logging_level(1); - const char *loglevel = boost::nowide::getenv("SLIC3R_LOGLEVEL"); + Slic3r::set_logging_level(1); + const char* loglevel = boost::nowide::getenv("SLIC3R_LOGLEVEL"); if (loglevel != nullptr) { if (loglevel[0] >= '0' && loglevel[0] <= '9' && loglevel[1] == 0) set_logging_level(loglevel[0] - '0'); @@ -1125,61 +1006,399 @@ bool CLI::setup(int argc, char **argv, CliInParams& in) set_sys_shapes_dir((path_resources / "shapes").string()); set_custom_gcodes_dir((path_resources / "custom_gcodes").string()); - // Parse all command line options into a DynamicConfig. - // If any option is unsupported, print usage and abort immediately. - t_config_option_keys opt_order; - if (! in.config.read_cli(argc, argv, &in.input_files, &opt_order)) { - // Separate error message reported by the CLI parser from the help. - boost::nowide::cerr << std::endl; - print_help(); - return false; - } - // Parse actions and transform options. - for (auto const &opt_key : opt_order) { - if (cli_actions_config_def.has(opt_key)) - in.actions.emplace_back(opt_key); - else if (cli_transform_config_def.has(opt_key)) - in.transforms.emplace_back(opt_key); - else if (cli_profiles_sharing_config_def.has(opt_key)) - in.profiles_sharing.emplace_back(opt_key); - } + return true; +} +static bool load_cli(Cli& cli, int argc, char** argv) +{ + if (cli.read(argc, argv)) + return true; + + // Separate error message reported by the CLI parser from the help. + boost::nowide::cerr << std::endl; + print_help(); + return false; +} + +static void setup_from_cli(Cli& cli) +{ + if (cli.misc_config.has("loglevel")) { - const ConfigOptionInt *opt_loglevel = in.config.opt("loglevel"); - if (opt_loglevel != 0) - set_logging_level(opt_loglevel->value); + int loglevel = cli.misc_config.opt_int("loglevel"); + if (loglevel != 0) + set_logging_level(loglevel); } - { - const ConfigOptionInt *opt_threads = in.config.opt("threads"); - if (opt_threads != nullptr) - thread_count = opt_threads->value; + if (cli.misc_config.has("threads")) + thread_count = cli.misc_config.opt_int("threads"); + + set_data_dir(cli.misc_config.has("datadir") ? cli.misc_config.opt_string("datadir") : get_default_datadir()); + +#ifdef SLIC3R_GUI + if (cli.misc_config.has("webdev")) { + Utils::ServiceConfig::instance().set_webdev_enabled(cli.misc_config.opt_bool("webdev")); + } +#endif +} + +#ifdef SLIC3R_GUI +// set data for init GUI parameters +// and return state of start_gui +static bool init_gui_params(GUI::GUI_InitParams& gui_params, int argc, char** argv, Cli& cli) +{ + bool start_gui = false; + + gui_params.argc = argc; + gui_params.argv = argv; + gui_params.input_files = cli.input_files; + + if (cli.misc_config.has("opengl-aa")) { + start_gui = true; + gui_params.opengl_aa = true; + } +#if SLIC3R_OPENGL_ES + // are we starting as gcodeviewer ? + if (cli.misc_config.has("gcodeviewer")) { + cli.start_gui = true; + cli.start_as_gcodeviewer = true; + } +#else + + // search for special keys into command line parameters + if (cli.misc_config.has("gcodeviewer")) { + start_gui = true; + gui_params.start_as_gcodeviewer = true; + } + else { +#ifndef _WIN32 + // On Unix systems, the prusa-slicer binary may be symlinked to give the application a different meaning. + gui_params.start_as_gcodeviewer = boost::algorithm::iends_with(boost::filesystem::path(argv[0]).filename().string(), "gcodeviewer"); +#endif // _WIN32 } - //FIXME Validating at this stage most likely does not make sense, as the config is not fully initialized yet. - std::string validity = in.config.validate(); + if (cli.misc_config.has("opengl-version")) { + const Semver opengl_minimum = Semver(3, 2, 0); + const std::string opengl_version_str = cli.misc_config.opt_string("opengl-version"); + boost::optional semver = Semver::parse(opengl_version_str); + if (semver.has_value() && (*semver) >= opengl_minimum) { + std::pair& version = gui_params.opengl_version; + version.first = semver->maj(); + version.second = semver->min(); + if (std::find(Slic3r::GUI::OpenGLVersions::core.begin(), Slic3r::GUI::OpenGLVersions::core.end(), std::make_pair(version.first, version.second)) == Slic3r::GUI::OpenGLVersions::core.end()) { + version = { 0, 0 }; + boost::nowide::cerr << "Required OpenGL version " << opengl_version_str << " not recognized.\n Option 'opengl-version' ignored." << std::endl; + } + } + else + boost::nowide::cerr << "Required OpenGL version " << opengl_version_str << " is invalid. Must be greater than or equal to " << + opengl_minimum.to_string() << "\n Option 'opengl-version' ignored." << std::endl; + start_gui = true; + } - // Initialize with defaults. - // Some of those values will used later - for (const t_optiondef_map* options : { &cli_actions_config_def.options - , &cli_transform_config_def.options - , &cli_profiles_sharing_config_def.options - , &cli_misc_config_def.options }) - for (const t_optiondef_map::value_type &optdef : *options) - in.config.option(optdef.first, true); + if (cli.misc_config.has("opengl-compatibility")) { + start_gui = true; + gui_params.opengl_compatibility_profile = true; + // reset version as compatibility profile always take the highest version + // supported by the graphic card + gui_params.opengl_version = std::make_pair(0, 0); + } - const std::string provided_datadir = in.config.opt_string("datadir"); - set_data_dir(provided_datadir.empty() ? get_default_datadir() : provided_datadir); + if (cli.misc_config.has("opengl-debug")) { + start_gui = true; + gui_params.opengl_debug = true; + } +#endif // SLIC3R_OPENGL_ES - //FIXME Validating at this stage most likely does not make sense, as the config is not fully initialized yet. + if (cli.misc_config.has("delete-after-load")) { + gui_params.delete_after_load = true; + } + + if (!gui_params.start_as_gcodeviewer && !cli.input_config.has("load")) { + // Read input file(s) if any and check if can start GcodeViewer + if (cli.input_files.size() == 1 && is_gcode_file(cli.input_files[0]) && boost::filesystem::exists(cli.input_files[0])) + gui_params.start_as_gcodeviewer = true; + } + + if (has_full_config_from_profiles(cli)) { + gui_params.selected_presets = Slic3r::GUI::CLISelectedProfiles{ cli.input_config.opt_string("print-profile"), + cli.input_config.opt_string("printer-profile") , + cli.input_config.option("material-profile")->values }; + } + + if (!cli.overrides_config.empty()) + gui_params.extra_config = cli.overrides_config; + + if (cli.input_config.has("load")) { + gui_params.load_configs = cli.input_config.option("load")->values; + start_gui = true; + } + + for (const std::string& file : cli.input_files) { + if (boost::starts_with(file, "prusaslicer://")) { + gui_params.start_downloader = true; + gui_params.download_url = file; + break; + } + } + + return start_gui; +} +#else // SLIC3R_GUI + // If there is no GUI, we shall ignore the parameters. Remove them from the list. +#endif // SLIC3R_GUI + +static bool load_print_config(DynamicPrintConfig &print_config, PrinterTechnology& printer_technology, const Cli& cli) +{ + // first of all load configuration from "--load" if any + + if (cli.input_config.has("load")) { + + const std::vector& load_configs = cli.input_config.option("load")->values; + ForwardCompatibilitySubstitutionRule config_substitution_rule = cli.misc_config.option>("config_compatibility")->value; + + // load config files supplied via --load + for (auto const& file : load_configs) { + if (!boost::filesystem::exists(file)) { + if (cli.misc_config.has("ignore_nonexistent_config") && cli.misc_config.opt_bool("ignore_nonexistent_config")) { + continue; + } + else { + boost::nowide::cerr << "No such file: " << file << std::endl; + return false; + } + } + DynamicPrintConfig config; + ConfigSubstitutions config_substitutions; + try { + config_substitutions = config.load(file, config_substitution_rule); + } + catch (std::exception& ex) { + boost::nowide::cerr << "Error while reading config file \"" << file << "\": " << ex.what() << std::endl; + return false; + } + + if (!can_apply_printer_technology(printer_technology, get_printer_technology(config))) + return false; + + print_config_substitutions(config_substitutions, file); + + config.normalize_fdm(); + print_config.apply(config); + } + } + + // than apply other options from full print config if any is provided by prifiles set + + if (has_full_config_from_profiles(cli)) { + DynamicPrintConfig config; + // load config from profiles set + std::string errors = Slic3r::load_full_print_config(cli.input_config.opt_string("print-profile"), + cli.input_config.option("material-profile")->values, + cli.input_config.opt_string("printer-profile"), + config, printer_technology); + if (!errors.empty()) { + boost::nowide::cerr << "Error while loading config from profiles: " << errors << std::endl; + return false; + } + + if (!can_apply_printer_technology(printer_technology, get_printer_technology(config))) + return false; + + config.normalize_fdm(); + + // config is applied with print_config loaded before + config += std::move(print_config); + print_config = std::move(config); + } + + return true; +} + +static bool process_input_files(std::vector& models, DynamicPrintConfig& print_config, PrinterTechnology& printer_technology, Cli& cli) +{ + for (const std::string& file : cli.input_files) { + if (boost::starts_with(file, "prusaslicer://")) { + continue; + } + if (!boost::filesystem::exists(file)) { + boost::nowide::cerr << "No such file: " << file << std::endl; + return false; + } + + Model model; + try { + if (has_full_config_from_profiles(cli)) { + // we have full banch of options from profiles set + // so, just load a geometry + model = FileReader::load_model(file); + } + else { + // load model and configuration from the file + DynamicPrintConfig config; + ConfigSubstitutionContext config_substitutions_ctxt(cli.misc_config.option>("config_compatibility")->value); + + boost::optional prusaslicer_generator_version; + //FIXME should we check the version here? // | Model::LoadAttribute::CheckVersion ? + model = FileReader::load_model_with_config(file, &config, &config_substitutions, prusaslicer_generator_version, FileReader::LoadAttribute::AddDefaultInstances); + + if (!can_apply_printer_technology(printer_technology, get_printer_technology(config))) + return false; + + print_config_substitutions(config_substitutions_ctxt.substitutions, file); + + // config is applied with print_config loaded before + config += std::move(print_config); + print_config = std::move(config); + } + + // If model for slicing is loaded from 3mf file, then its geometry has to be used and arrange couldn't be apply for this model. + if ((boost::algorithm::iends_with(file, ".3mf") || boost::algorithm::iends_with(file, ".zip")) && + (!cli.transform_config.has("dont_arrange") || !cli.transform_config.opt_bool("dont_arrange"))) { + //So, check a state of "dont_arrange" parameter and set it to true, if its value is false. + cli.transform_config.set_key_value("dont_arrange", new ConfigOptionBool(true)); + } + } + catch (std::exception& e) { + boost::nowide::cerr << file << ": " << e.what() << std::endl; + return false; + } + if (model.objects.empty()) { + boost::nowide::cerr << "Error: file is empty: " << file << std::endl; + continue; + } + models.push_back(model); + } + + return true; +} + +static bool is_needed_post_processing(const DynamicPrintConfig& print_config, const Cli& cli) +{ + if (print_config.has("post_process")) { + const std::vector& post_process = print_config.opt("post_process")->values; + if (!post_process.empty()) { + boost::nowide::cout << "\nA post-processing script has been detected in the config data:\n\n"; + for (const std::string& s : post_process) { + boost::nowide::cout << "> " << s << "\n"; + } + boost::nowide::cout << "\nContinue(Y/N) ? "; + char in; + boost::nowide::cin >> in; + if (in != 'Y' && in != 'y') + return true; + } + } + + return false; +} + +static bool finalize_print_config(DynamicPrintConfig& print_config, PrinterTechnology& printer_technology, const Cli& cli) +{ + // Apply command line options to a more specific DynamicPrintConfig which provides normalize() + // (command line options override --load files or congiguration which is loaded prom profiles) + print_config.apply(cli.overrides_config, true); + // Normalizing after importing the 3MFs / AMFs + print_config.normalize_fdm(); + + if (printer_technology == ptUnknown) + printer_technology = cli.actions_config.has("export_sla") ? ptSLA : ptFFF; + print_config.option>("printer_technology", true)->value = printer_technology; + + // Initialize full print configs for both the FFF and SLA technologies. + FullPrintConfig fff_print_config; + SLAFullPrintConfig sla_print_config; + + // Synchronize the default parameters and the ones received on the command line. + if (printer_technology == ptFFF) { + fff_print_config.apply(print_config, true); + print_config.apply(fff_print_config, true); + } + else { + assert(printer_technology == ptSLA); + sla_print_config.output_filename_format.value = "[input_filename_base].sl1"; + + // The default bed shape should reflect the default display parameters + // and not the fff defaults. + double w = sla_print_config.display_width.getFloat(); + double h = sla_print_config.display_height.getFloat(); + sla_print_config.bed_shape.values = { Vec2d(0, 0), Vec2d(w, 0), Vec2d(w, h), Vec2d(0, h) }; + + sla_print_config.apply(print_config, true); + print_config.apply(sla_print_config, true); + } + + // validate print configuration + std::string validity = print_config.validate(); if (!validity.empty()) { - boost::nowide::cerr << "error: " << validity << std::endl; + boost::nowide::cerr << "Error: The composite configation is not valid: " << validity << std::endl; return false; } return true; } +int Slic3r::CLI::run(int argc, char** argv) +{ + if (!setup_general()) + return 1; + + Cli cli; + if (!load_cli(cli, argc, argv)) + return 1; + + setup_from_cli(cli); + + if (process_profiles_sharing(cli)) + return 1; + + bool start_gui = cli.empty() || (has_full_config_from_profiles(cli) && cli.actions_config.empty()); + PrinterTechnology printer_technology = cli.overrides_config.has("printer_technology") ? Preset::printer_technology(cli.overrides_config) : ptUnknown; + DynamicPrintConfig print_config = {}; + std::vector models; + +#ifdef SLIC3R_GUI + GUI::GUI_InitParams gui_params; + start_gui |= init_gui_params(gui_params, argc, argv, cli); + + if (gui_params.start_as_gcodeviewer) + return start_as_gcode_viewer(cli, gui_params); +#endif + + if (!load_print_config(print_config, printer_technology, cli)) + return 1; + + if (!process_input_files(models, print_config, printer_technology, cli)) + return 1; + + if (!start_gui && is_needed_post_processing(print_config, cli)) + return 0; + + if (!finalize_print_config(print_config, printer_technology, cli)) + return 1; + + if (!process_transform(cli, print_config, models)) + return 1; + + if (!process_actions(cli, print_config, models)) + return 1; + + // this need to be check AFTER process transformations, where this option can be added + start_gui |= cli.actions_config.has("export_stl"); + + if (start_gui) { +#ifdef SLIC3R_GUI + return start_gui_with_params(gui_params); +#else + // No GUI support. Just print out a help. + print_help(false); + // If started without a parameter, consider it to be OK, otherwise report an error code (no action etc). + return (argc == 0) ? 0 : 1; +#endif + } + + return 0; +} + // __has_feature() is used later for Clang, this is for compatibility with other compilers (such as GCC and MSVC) #ifndef __has_feature @@ -1226,7 +1445,7 @@ extern "C" { for (size_t i = 0; i < argc; ++ i) argv_ptrs[i] = argv_narrow[i].data(); // Call the UTF8 main. - return CLI().run(argc, argv_ptrs.data()); + return Slic3r::CLI::run(argc, argv_ptrs.data()); } } #else /* _MSC_VER */ diff --git a/src/PrusaSlicer.hpp b/src/PrusaSlicer.hpp index 2f9d9e5ada..302666a4d1 100644 --- a/src/PrusaSlicer.hpp +++ b/src/PrusaSlicer.hpp @@ -1,49 +1,9 @@ #ifndef SLIC3R_HPP #define SLIC3R_HPP -#include "libslic3r/Config.hpp" -#include "libslic3r/Model.hpp" - -namespace Slic3r { - -namespace IO { - enum ExportFormat : int { - OBJ, - STL, - // SVG, - TMF, - Gcode - }; -} - -struct CliInParams +namespace Slic3r::CLI { - DynamicPrintAndCLIConfig config; - std::vector input_files; - std::vector actions; - std::vector transforms; - std::vector profiles_sharing; -}; - -struct CliOutParams -{ - DynamicPrintConfig print_config; - PrinterTechnology printer_technology; - ForwardCompatibilitySubstitutionRule config_substitution_rule; - std::vector models; - bool user_center_specified { false }; - bool delete_after_load { false }; -}; - -class CLI { -public: int run(int argc, char **argv); - -private: - - bool setup(int argc, char **argv, CliInParams& in); -}; - } #endif