From 4f0894aa6c0cb9cea87ecf5958c307baf13c68be Mon Sep 17 00:00:00 2001 From: YuSanka Date: Tue, 20 Feb 2024 13:11:47 +0100 Subject: [PATCH] SPE-2028 : Implement the command line interface for querying presets --- src/PrusaSlicer.cpp | 100 ++++++- src/PrusaSlicer.hpp | 3 + src/libslic3r/PrintConfig.cpp | 39 ++- src/libslic3r/PrintConfig.hpp | 10 + src/slic3r/CMakeLists.txt | 2 + src/slic3r/Utils/ProfilesSharingUtils.cpp | 335 ++++++++++++++++++++++ src/slic3r/Utils/ProfilesSharingUtils.hpp | 16 ++ 7 files changed, 502 insertions(+), 3 deletions(-) create mode 100644 src/slic3r/Utils/ProfilesSharingUtils.cpp create mode 100644 src/slic3r/Utils/ProfilesSharingUtils.hpp diff --git a/src/PrusaSlicer.cpp b/src/PrusaSlicer.cpp index 685bc42bb1..d8ca4f1d4d 100644 --- a/src/PrusaSlicer.cpp +++ b/src/PrusaSlicer.cpp @@ -32,6 +32,9 @@ #include #include #include +#include +#include +#include #include #include "unix/fhs.hpp" // Generated by CMake from ../platform/unix/fhs.hpp.in @@ -61,6 +64,8 @@ #include "PrusaSlicer.hpp" +#include "slic3r/Utils/ProfilesSharingUtils.hpp" + #ifdef SLIC3R_GUI #include "slic3r/GUI/GUI_Init.hpp" #endif /* SLIC3R_GUI */ @@ -693,6 +698,8 @@ int CLI::run(int argc, char **argv) } } + if (processed_profiles_sharing()) + return 1; if (start_gui) { #ifdef SLIC3R_GUI @@ -815,6 +822,8 @@ bool CLI::setup(int argc, char **argv) m_actions.emplace_back(opt_key); else if (cli_transform_config_def.has(opt_key)) m_transforms.emplace_back(opt_key); + else if (cli_profiles_sharing_config_def.has(opt_key)) + m_profiles_sharing.emplace_back(opt_key); } { @@ -833,7 +842,10 @@ bool CLI::setup(int argc, char **argv) std::string validity = m_config.validate(); // Initialize with defaults. - for (const t_optiondef_map *options : { &cli_actions_config_def.options, &cli_transform_config_def.options, &cli_misc_config_def.options }) + 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) m_config.option(optdef.first, true); @@ -863,6 +875,7 @@ void CLI::print_help(bool include_print_options, PrinterTechnology printer_techn << std::endl << "Actions:" << std::endl; cli_actions_config_def.print_cli_help(boost::nowide::cout, false); + cli_profiles_sharing_config_def.print_cli_help(boost::nowide::cout, false); boost::nowide::cout << std::endl @@ -874,6 +887,11 @@ void CLI::print_help(bool include_print_options, PrinterTechnology printer_techn << "Other options:" << std::endl; cli_misc_config_def.print_cli_help(boost::nowide::cout, false); + //boost::nowide::cout + // << std::endl + // << "Profiles sharing options:" << std::endl; + // cli_profiles_sharing_config_def.print_cli_help(boost::nowide::cout, false); + boost::nowide::cout << std::endl << "Print options are processed in the following order:" << std::endl @@ -939,6 +957,86 @@ std::string CLI::output_filepath(const Model &model, IO::ExportFormat format) co return proposed_path.string(); } +std::set query_options = { + "printer-profile" +}; + +bool CLI::processed_profiles_sharing() +{ + if (m_profiles_sharing.empty()) + return false; + + std::string ret; + for (auto const& opt_key : m_profiles_sharing) { + if (query_options.find(opt_key) != query_options.end()) + continue; + if (opt_key == "query-printer-models") { + ret = Slic3r::get_json_printer_models(Preset::printer_technology(m_config)); + } + else if (opt_key == "query-printer-profiles") { + if (!m_config.has("printer_model") || !m_config.has("printer_variant")) { + boost::nowide::cerr << "error in '" << opt_key << "' : this action requires set 'printer-model' and 'printer-variant' options" << std::endl; + break; + } + ret = Slic3r::get_json_printer_profiles(m_config.opt_string("printer_model"), m_config.opt_string("printer_variant")); + + if (ret.empty()) + ret = "\nPrinter_model '" + m_config.opt_string("printer_model") + + "' with printer_variant '" + m_config.opt_string("printer_variant") + + "' wasn't found among intalled printers.\nOr the request can be wrong.\n"; + } + else if (opt_key == "query-print-filament-profiles") { + if (!m_config.has("printer-profile")) { + boost::nowide::cerr << "error: this action requires set printer-preset option" << opt_key << std::endl; + break; + } + ret = Slic3r::get_json_print_filament_profiles(m_config.opt_string("printer-profile")); + + if (ret.empty()) + ret = "\nPrinter profile '" + m_config.opt_string("printer-profile") + + "' wasn't found among intalled printers.\nOr the request can be wrong.\n"; + } + else { + boost::nowide::cerr << "error: option not implemented yet: " << opt_key << std::endl; + break; + } + } + + // use --output when available + + std::string cmdline_param = m_config.opt_string("output"); + if (cmdline_param.empty()) { + printf("\n"); + if (ret.empty()) + printf("Wrong request"); + else + printf(ret.c_str()); + } + else { + // 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"; + if (boost::filesystem::is_directory(cmdline_path)) + proposed_path = (cmdline_path / proposed_path.filename()); + else if (cmdline_path.extension().empty()) + proposed_path = cmdline_path.replace_extension("json"); + else + proposed_path = cmdline_path; + const std::string file = proposed_path.string(); + + boost::nowide::ofstream c; + c.open(file, std::ios::out | std::ios::trunc); + c << ret << std::endl; + c.close(); + + printf("\nOutput for your request in written into \""); + printf(file.c_str()); + printf("\"\n"); + } + + return true; +} + // __has_feature() is used later for Clang, this is for compatibility with other compilers (such as GCC and MSVC) #ifndef __has_feature # define __has_feature(x) 0 diff --git a/src/PrusaSlicer.hpp b/src/PrusaSlicer.hpp index 0554580e53..8910f56980 100644 --- a/src/PrusaSlicer.hpp +++ b/src/PrusaSlicer.hpp @@ -28,6 +28,7 @@ private: std::vector m_input_files; std::vector m_actions; std::vector m_transforms; + std::vector m_profiles_sharing; std::vector m_models; bool setup(int argc, char **argv); @@ -39,6 +40,8 @@ private: bool export_models(IO::ExportFormat format); bool has_print_action() const { return m_config.opt_bool("export_gcode") || m_config.opt_bool("export_sla"); } + + bool processed_profiles_sharing(); std::string output_filepath(const Model &model, IO::ExportFormat format) const; }; diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 4404aea7be..59f1b08527 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -2191,7 +2191,7 @@ void PrintConfigDef::init_fff_params() def->label = L("Printer type"); def->tooltip = L("Type of the printer."); def->set_default_value(new ConfigOptionString()); - def->cli = ConfigOptionDef::nocli; +// def->cli = ConfigOptionDef::nocli; def = this->add("printer_notes", coString); def->label = L("Printer notes"); @@ -2212,7 +2212,7 @@ void PrintConfigDef::init_fff_params() def->label = L("Printer variant"); def->tooltip = L("Name of the printer variant. For example, the printer variants may be differentiated by a nozzle diameter."); def->set_default_value(new ConfigOptionString()); - def->cli = ConfigOptionDef::nocli; +// def->cli = ConfigOptionDef::nocli; def = this->add("print_settings_id", coString); def->set_default_value(new ConfigOptionString("")); @@ -5121,15 +5121,50 @@ CLIMiscConfigDef::CLIMiscConfigDef() #endif /* _MSC_VER */ } +CLIProfilesSharingConfigDef::CLIProfilesSharingConfigDef() +{ + ConfigOptionDef* def; + + def = this->add("query-printer-models", coBool); + def->label = L("Get list of printer models"); + def->tooltip = L("Get available printer models into JSON.\n" + "Note:\nTo print printer models for required technology use 'printer-technology' option. By default printer_technology is FFF.\n" + "To print out JSON into file use 'output' option.\n" + "To specify configuration folder use 'datadir' option. Otherwise all known vendors will be loaded and processed."); + + + def = this->add("query-printer-profiles", coBool); + def->label = L("Get list of printer profiles in respect to selected printer model and printer variant"); + def->tooltip = L("Get list of printer profiles in respect to selected 'printer-model' and 'printer-variant' into JSON.\n" + "Note:\n" + "To print out JSON into file use 'output' option.\n" + "To specify configuration folder use 'datadir' option. Otherwise all known vendors will be loaded and processed."); + + + def = this->add("query-print-filament-profiles", coBool); + def->label = L("Get list of prints and filaments profiles in respect to selected printer profile"); + def->tooltip = L("Get list of prints and filaments profiles in respect to selected 'printer-profile' into JSON.\n" + "Note:\n" + "To print out JSON into file use 'output' option.\n" + "To specify configuration folder use 'datadir' option. Otherwise all known vendors will be loaded and processed."); + + def = this->add("printer-profile", coString); + def->label = L("Printer preset name"); + def->tooltip = L("Name of the printer preset used for slicing."); + def->set_default_value(new ConfigOptionString()); +} + const CLIActionsConfigDef cli_actions_config_def; const CLITransformConfigDef cli_transform_config_def; const CLIMiscConfigDef cli_misc_config_def; +const CLIProfilesSharingConfigDef cli_profiles_sharing_config_def; DynamicPrintAndCLIConfig::PrintAndCLIConfigDef DynamicPrintAndCLIConfig::s_def; void DynamicPrintAndCLIConfig::handle_legacy(t_config_option_key &opt_key, std::string &value) const { if (cli_actions_config_def .options.find(opt_key) == cli_actions_config_def .options.end() && + cli_profiles_sharing_config_def.options.find(opt_key) == cli_profiles_sharing_config_def.options.end() && cli_transform_config_def.options.find(opt_key) == cli_transform_config_def.options.end() && cli_misc_config_def .options.find(opt_key) == cli_misc_config_def .options.end()) { PrintConfigDef::handle_legacy(opt_key, value); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index f1dc6b96a4..c14230d4f4 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -1211,6 +1211,12 @@ public: CLIMiscConfigDef(); }; +class CLIProfilesSharingConfigDef : public ConfigDef +{ +public: + CLIProfilesSharingConfigDef(); +}; + typedef std::string t_custom_gcode_key; // This map containes list of specific placeholders for each custom G-code, if any exist const std::map& custom_gcode_specific_placeholders(); @@ -1282,6 +1288,9 @@ extern const CLITransformConfigDef cli_transform_config_def; // This class defines all command line options that are not actions or transforms. extern const CLIMiscConfigDef cli_misc_config_def; +// This class defines the command line options representing profiles sharing commands. +extern const CLIProfilesSharingConfigDef cli_profiles_sharing_config_def; + class DynamicPrintAndCLIConfig : public DynamicPrintConfig { public: @@ -1306,6 +1315,7 @@ private: this->options.insert(cli_actions_config_def.options.begin(), cli_actions_config_def.options.end()); this->options.insert(cli_transform_config_def.options.begin(), cli_transform_config_def.options.end()); this->options.insert(cli_misc_config_def.options.begin(), cli_misc_config_def.options.end()); + this->options.insert(cli_profiles_sharing_config_def.options.begin(), cli_profiles_sharing_config_def.options.end()); for (const auto &kvp : this->options) this->by_serialization_key_ordinal[kvp.second.serialization_key_ordinal] = &kvp.second; } diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index fea794c943..d22dc873ce 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -335,6 +335,8 @@ set(SLIC3R_GUI_SOURCES Utils/WxFontUtils.hpp Utils/WifiScanner.hpp Utils/WifiScanner.cpp + Utils/ProfilesSharingUtils.hpp + Utils/ProfilesSharingUtils.cpp ) find_package(NanoSVG REQUIRED) diff --git a/src/slic3r/Utils/ProfilesSharingUtils.cpp b/src/slic3r/Utils/ProfilesSharingUtils.cpp new file mode 100644 index 0000000000..adfbab9bdb --- /dev/null +++ b/src/slic3r/Utils/ProfilesSharingUtils.cpp @@ -0,0 +1,335 @@ +///|/ Copyright (c) Prusa Research 2021 - 2023 Oleksandra Iushchenko @YuSanka +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ +#include "ProfilesSharingUtils.hpp" +#include "libslic3r/utils.hpp" + +#include "slic3r/GUI/ConfigWizard_private.hpp" +#include "slic3r/GUI/format.hpp" + +#include + +namespace Slic3r { + +namespace pt = boost::property_tree; + +using namespace GUI; + +static pt::ptree get_printer_models_for_vendor(const VendorProfile* vendor_profile, PrinterTechnology printer_technology) +{ + pt::ptree vendor_node; + + for (const auto& printer_model : vendor_profile->models) { + if (printer_technology != ptAny && printer_model.technology != printer_technology) + continue; + + pt::ptree data_node; + data_node.put("vendor_name", vendor_profile->name); + data_node.put("id", printer_model.id); + data_node.put("name", printer_model.name); + + if (printer_model.technology == ptFFF) { + pt::ptree variants_node; + for (const auto& variant : printer_model.variants) { + pt::ptree variant_node; + variant_node.put("", variant.name); + variants_node.push_back(std::make_pair("", variant_node)); + } + data_node.add_child("variants", variants_node); + } + + data_node.put("technology", printer_model.technology == ptFFF ? "FFF" : "SLA"); + data_node.put("family", printer_model.family); + + pt::ptree def_materials_node; + for (const std::string& material : printer_model.default_materials) { + pt::ptree material_node; + material_node.put("", material); + def_materials_node.push_back(std::make_pair("", material_node)); + } + data_node.add_child("default_materials", def_materials_node); + + vendor_node.push_back(std::make_pair("", data_node)); + } + + return vendor_node; +} + +static bool load_preset_bandle_from_datadir(PresetBundle& preset_bundle) +{ + AppConfig app_config = AppConfig(AppConfig::EAppMode::Editor); + if (!app_config.exists()) { + printf("Configuration wasn't found. Check your 'datadir' value.\n"); + return false; + } + + printf("Loading: AppConfig"); + + if (std::string error = app_config.load(); !error.empty()) { + throw Slic3r::RuntimeError(Slic3r::format("Error parsing PrusaSlicer config file, it is probably corrupted. " + "Try to manually delete the file to recover from the error. Your user profiles will not be affected." + "\n\n%1%\n\n%2%", app_config.config_path(), error)); + return false; + } + + // just checking for existence of Slic3r::data_dir is not enough : it may be an empty directory + // supplied as argument to --datadir; in that case we should still run the wizard + preset_bundle.setup_directories(); + + std::string delayed_error_load_presets; + // Suppress the '- default -' presets. + preset_bundle.set_default_suppressed(app_config.get_bool("no_defaults")); + try { + printf(", presets"); + auto preset_substitutions = preset_bundle.load_presets(app_config, ForwardCompatibilitySubstitutionRule::EnableSystemSilent); + if (!preset_substitutions.empty()) { + printf("Some substitutions are found during loading presets.\n"); + return false; + } + + // Post-process vendor map to delete non-installed models/varians + + VendorMap& vendors = preset_bundle.vendors; + for (auto& [vendor_id, vendor_profile] : vendors) { + std::vector models; + + for (auto& printer_model : vendor_profile.models) { + std::vector variants; + + for (const auto& variant : printer_model.variants) { + // check if printer model with variant is intalled + if (app_config.get_variant(vendor_id, printer_model.id, variant.name)) + variants.push_back(variant); + } + + if (!variants.empty()) { + if (printer_model.variants.size() != variants.size()) + printer_model.variants = variants; + models.push_back(printer_model); + } + } + + if (!models.empty()) { + if (vendor_profile.models.size() != models.size()) + vendor_profile.models = models; + } + } + + return true; + } + catch (const std::exception& ex) { + delayed_error_load_presets = ex.what(); + printf(delayed_error_load_presets.c_str()); + return false; + } +} + +std::string get_json_printer_models(PrinterTechnology printer_technology) +{ + // Build a property tree with all the information. + pt::ptree root; + + if (data_dir().empty()) { + printf("Loading of all known vendors ."); + BundleMap bundles = BundleMap::load(); + + for (const auto& [bundle_id, bundle] : bundles) { + pt::ptree vendor_node = get_printer_models_for_vendor(bundle.vendor_profile, printer_technology); + if (!vendor_node.empty()) + root.add_child(bundle_id, vendor_node); + } + } + else { + PresetBundle preset_bundle; + if (!load_preset_bandle_from_datadir(preset_bundle)) + return ""; + printf(", vendors"); + + const VendorMap& vendors_map = preset_bundle.vendors; + for (const auto& [vendor_id, vendor] : vendors_map) { + pt::ptree vendor_node = get_printer_models_for_vendor(&vendor, printer_technology); + if (!vendor_node.empty()) + root.add_child(vendor_id, vendor_node); + } + } + + // Serialize the tree into JSON and return it. + std::stringstream ss; + pt::write_json(ss, root); + return ss.str(); +} + + + +struct PrinterAttr +{ + std::string model_name; + std::string variant; +}; + +static std::string get_printer_profiles(const VendorProfile* vendor_profile, + const PresetBundle* preset_bundle, + const PrinterAttr& printer_attr) +{ + for (const auto& printer_model : vendor_profile->models) { + if (printer_model.name != printer_attr.model_name) + continue; + + for (const auto& variant : printer_model.variants) + if (variant.name == printer_attr.variant) + { + pt::ptree data_node; + data_node.put("printer_model", printer_model.name); + data_node.put("printer_variant", printer_attr.variant); + + pt::ptree printer_profiles_node; + for (const Preset& printer_preset : preset_bundle->printers) { + if (printer_preset.vendor->id == vendor_profile->id && + printer_preset.is_visible && // ??? + printer_preset.config.opt_string("printer_model") == printer_model.id && + printer_preset.config.opt_string("printer_variant") == printer_attr.variant) { + pt::ptree profile_node; + profile_node.put("", printer_preset.name); + printer_profiles_node.push_back(std::make_pair("", profile_node)); + } + } + data_node.add_child("printer_profiles", printer_profiles_node); + + // Serialize the tree into JSON and return it. + std::stringstream ss; + pt::write_json(ss, data_node); + return ss.str(); + } + } + + return ""; +} + +std::string get_json_printer_profiles(const std::string& printer_model_name, const std::string& printer_variant) +{ + PrinterAttr printer_attr({printer_model_name, printer_variant}); + + if (data_dir().empty()) { + printf("Loading of all known vendors ."); + BundleMap bundles = BundleMap::load(); + + for (const auto& [bundle_id, bundle] : bundles) { + std::string out = get_printer_profiles(bundle.vendor_profile, bundle.preset_bundle.get(), printer_attr); + if (!out.empty()) + return out; + } + } + else { + PresetBundle preset_bundle; + if (!load_preset_bandle_from_datadir(preset_bundle)) + return ""; + printf(", vendors"); + + const VendorMap& vendors = preset_bundle.vendors; + for (const auto& [vendor_id, vendor] : vendors) { + std::string out = get_printer_profiles(&vendor, &preset_bundle, printer_attr); + if (!out.empty()) + return out; + } + } + + return ""; +} + + +static std::string get_installed_print_and_filament_profiles(const PresetBundle* preset_bundle, const Preset* printer_preset) +{ + PrinterTechnology printer_technology = printer_preset->printer_technology(); + + printf("\n\nSearching for compatible print profiles ."); + + pt::ptree print_profiles; + + const PresetWithVendorProfile printer_preset_with_vendor_profile = preset_bundle->printers.get_preset_with_vendor_profile(*printer_preset); + + const PresetCollection& print_presets = printer_technology == ptFFF ? preset_bundle->prints : preset_bundle->sla_prints; + const PresetCollection& material_presets = printer_technology == ptFFF ? preset_bundle->filaments : preset_bundle->sla_materials; + const std::string material_node_name = printer_technology == ptFFF ? "filament_profiles" : "sla_material_profiles"; + + int output_counter{ 0 }; + for (auto print_preset : print_presets) { + + ++output_counter; + if (output_counter == 10) { + printf("."); + output_counter = 0; + } + + const PresetWithVendorProfile print_preset_with_vendor_profile = print_presets.get_preset_with_vendor_profile(print_preset); + + if (is_compatible_with_printer(print_preset_with_vendor_profile, printer_preset_with_vendor_profile)) + { + pt::ptree print_profile_node; + print_profile_node.put("name", print_preset.name); + + pt::ptree materials_profile_node; + + for (auto material_preset : material_presets) { + + // ?! check visible and no-template presets only + if (!material_preset.is_visible || material_preset.vendor->templates_profile) + continue; + + const PresetWithVendorProfile material_preset_with_vendor_profile = material_presets.get_preset_with_vendor_profile(material_preset); + + if (is_compatible_with_printer(material_preset_with_vendor_profile, printer_preset_with_vendor_profile) && + is_compatible_with_print(material_preset_with_vendor_profile, print_preset_with_vendor_profile, printer_preset_with_vendor_profile)) { + pt::ptree material_node; + material_node.put("", material_preset.name); + materials_profile_node.push_back(std::make_pair("", material_node)); + } + } + + print_profile_node.add_child(material_node_name, materials_profile_node); + print_profiles.push_back(std::make_pair("", print_profile_node)); + } + } + + printf("\n"); + + if (print_profiles.empty()) + return ""; + + pt::ptree tree; + tree.put("printer_profile", printer_preset->name); + tree.add_child("print_profiles", print_profiles); + + // Serialize the tree into JSON and return it. + std::stringstream ss; + pt::write_json(ss, tree); + return ss.str(); +} + +std::string get_json_print_filament_profiles(const std::string& printer_profile) +{ + if (data_dir().empty()) { + printf("Loading of all known vendors ."); + BundleMap bundles = BundleMap::load(); + + printf(GUI::format("\n\nSearching for \"%1%\" .", printer_profile).c_str()); + + for (const auto& [bundle_id, bundle] : bundles) { + printf("."); + if (const Preset* preset = bundle.preset_bundle->printers.find_preset(printer_profile, false, false)) + return get_installed_print_and_filament_profiles(bundle.preset_bundle.get(), preset); + } + } + else { + PresetBundle preset_bundle; + if (!load_preset_bandle_from_datadir(preset_bundle)) + return ""; + + if (const Preset* preset = preset_bundle.printers.find_preset(printer_profile, false, false)) + return get_installed_print_and_filament_profiles(&preset_bundle, preset); + } + + return ""; +} + +} // namespace Slic3r diff --git a/src/slic3r/Utils/ProfilesSharingUtils.hpp b/src/slic3r/Utils/ProfilesSharingUtils.hpp new file mode 100644 index 0000000000..ab870f8984 --- /dev/null +++ b/src/slic3r/Utils/ProfilesSharingUtils.hpp @@ -0,0 +1,16 @@ +///|/ Copyright (c) Prusa Research 2021 Oleksandra Iushchenko @YuSanka +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ +#ifndef slic3r_ProfilesSharingUtils_hpp_ +#define slic3r_ProfilesSharingUtils_hpp_ + +namespace Slic3r { + +std::string get_json_printer_models(PrinterTechnology printer_technology); +std::string get_json_printer_profiles(const std::string& printer_model, const std::string& printer_variant); +std::string get_json_print_filament_profiles(const std::string& printer_profile); + +} // namespace Slic3r + +#endif // slic3r_ProfilesSharingUtils_hpp_