diff --git a/src/OrcaSlicer.cpp b/src/OrcaSlicer.cpp index b1cdc6e7f8..e8281406b4 100644 --- a/src/OrcaSlicer.cpp +++ b/src/OrcaSlicer.cpp @@ -685,162 +685,6 @@ static void load_default_gcodes_to_config(DynamicPrintConfig& config, Preset::Ty } } -static int load_assemble_plate_list(std::string config_file, std::vector &assemble_plate_info_list) -{ - int ret = 0; - boost::filesystem::path directory_path(config_file); - - BOOST_LOG_TRIVIAL(info) << boost::format("%1% enter, file %2%")%__FUNCTION__ % config_file; - if (!fs::exists(directory_path)) { - BOOST_LOG_TRIVIAL(error) << boost::format("directory %1% not exist.")%config_file; - return CLI_FILE_NOTFOUND; - } - - try { - json root_json; - boost::nowide::ifstream ifs(config_file); - ifs >> root_json; - ifs.close(); - - int plate_count = root_json[JSON_ASSEMPLE_PLATES].size(); - if ((plate_count <= 0) || (plate_count > MAX_PLATE_COUNT)) { - BOOST_LOG_TRIVIAL(error) << __FUNCTION__<< boost::format(": invalid plate count %1%")%plate_count; - return CLI_CONFIG_FILE_ERROR; - } - assemble_plate_info_list.resize(plate_count); - - for (int plate_index = 0; plate_index < plate_count; plate_index++) - { - assemble_plate_info_t &assemble_plate = assemble_plate_info_list[plate_index]; - const json& plate_json = root_json[JSON_ASSEMPLE_PLATES][plate_index]; - assemble_plate.plate_name = plate_json[JSON_ASSEMPLE_PLATE_NAME]; - assemble_plate.need_arrange = plate_json[JSON_ASSEMPLE_PLATE_NEED_ARRANGE]; - - if (plate_json.contains(JSON_ASSEMPLE_PLATE_PARAMS)) { - assemble_plate.plate_params = plate_json[JSON_ASSEMPLE_PLATE_PARAMS].get>(); - BOOST_LOG_TRIVIAL(debug) << boost::format("Plate %1%, has %2% plate params") % (plate_index + 1) % assemble_plate.plate_params.size(); - } - - int object_count = plate_json[JSON_ASSEMPLE_OBJECTS].size(); - if (object_count <= 0) { - BOOST_LOG_TRIVIAL(error) << __FUNCTION__<< boost::format(": invalid object count %1% in plate %2%")%object_count %(plate_index+1); - return CLI_CONFIG_FILE_ERROR; - } - - assemble_plate.assemble_obj_list.resize(object_count); - for (int object_index = 0; object_index < object_count; object_index++) - { - assemble_object_info_t& assemble_object = assemble_plate.assemble_obj_list[object_index]; - const json& object_json = plate_json[JSON_ASSEMPLE_OBJECTS][object_index]; - - assemble_object.path = object_json[JSON_ASSEMPLE_OBJECT_PATH]; - assemble_object.count = object_json[JSON_ASSEMPLE_OBJECT_COUNT]; - - if (assemble_object.count <= 0) { - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(": invalid object clone count %1% in plate %2% Object %3%") % assemble_object.count % (plate_index + 1) % assemble_object.path; - return CLI_CONFIG_FILE_ERROR; - } - - assemble_object.filaments = object_json.at(JSON_ASSEMPLE_OBJECT_FILAMENTS).get>(); - if ((assemble_object.filaments.size() > 0) && (assemble_object.filaments.size() != assemble_object.count) && (assemble_object.filaments.size() != 1)) - { - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(": object %1%'s filaments count %2% not equal to clone count %3%, also not equal to 1") % assemble_object.path % assemble_object.filaments.size() % assemble_object.count; - return CLI_CONFIG_FILE_ERROR; - } - - if (object_json.contains(JSON_ASSEMPLE_OBJECT_ASSEMBLE_INDEX)) { - assemble_object.assemble_index = object_json[JSON_ASSEMPLE_OBJECT_ASSEMBLE_INDEX].get>(); - if ((assemble_object.assemble_index.size() > 0) && (assemble_object.assemble_index.size() != assemble_object.count) && (assemble_object.assemble_index.size() != 1)) - { - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(": object %1%'s assemble_index count %2% not equal to clone count %3%, also not equal to 1") % assemble_object.path % assemble_object.assemble_index.size() % assemble_object.count; - return CLI_CONFIG_FILE_ERROR; - } - } - - if (object_json.contains(JSON_ASSEMPLE_OBJECT_POS_X)) { - assemble_object.pos_x = object_json[JSON_ASSEMPLE_OBJECT_POS_X].get>(); - if ((assemble_object.pos_x.size() > 0) && (assemble_object.pos_x.size() != assemble_object.count) && (assemble_object.pos_x.size() != 1)) - { - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(": object %1%'s pos_x count %2% not equal to clone count %3%, also not equal to 1") % assemble_object.path % assemble_object.pos_x.size() % assemble_object.count; - return CLI_CONFIG_FILE_ERROR; - } - } - if (object_json.contains(JSON_ASSEMPLE_OBJECT_POS_Y)) { - assemble_object.pos_y = object_json[JSON_ASSEMPLE_OBJECT_POS_Y].get>(); - if ((assemble_object.pos_y.size() > 0) && (assemble_object.pos_y.size() != assemble_object.count) && (assemble_object.pos_y.size() != 1)) - { - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(": object %1%'s pos_y count %2% not equal to clone count %3%, also not equal to 1") % assemble_object.path % assemble_object.pos_y.size() % assemble_object.count; - return CLI_CONFIG_FILE_ERROR; - } - } - if (object_json.contains(JSON_ASSEMPLE_OBJECT_POS_Z)) { - assemble_object.pos_z = object_json[JSON_ASSEMPLE_OBJECT_POS_Z].get>(); - if ((assemble_object.pos_z.size() > 0) && (assemble_object.pos_z.size() != assemble_object.count) && (assemble_object.pos_z.size() != 1)) - { - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(": object %1%'s pos_z count %2% not equal to clone count %3%, also not equal to 1") % assemble_object.path % assemble_object.pos_z.size() % assemble_object.count; - return CLI_CONFIG_FILE_ERROR; - } - } - if (object_json.contains(JSON_ASSEMPLE_OBJECT_PRINT_PARAMS)) { - assemble_object.print_params = object_json[JSON_ASSEMPLE_OBJECT_PRINT_PARAMS].get>(); - BOOST_LOG_TRIVIAL(debug) << boost::format("Plate %1%, object %2% has %3% print params") % (plate_index + 1) %assemble_object.path % assemble_object.print_params.size(); - } - if (object_json.contains(JSON_ASSEMPLE_OBJECT_HEIGHT_RANGES)) { - json height_range_json = object_json[JSON_ASSEMPLE_OBJECT_HEIGHT_RANGES]; - int range_count = height_range_json.size(); - - BOOST_LOG_TRIVIAL(debug) << boost::format("Plate %1%, object %2% has %3% height ranges") % (plate_index + 1) %assemble_object.path % range_count; - - assemble_object.height_ranges.resize(range_count); - for (int range_index = 0; range_index < range_count; range_index++) - { - height_range_info_t& height_range = assemble_object.height_ranges[range_index]; - height_range.min_z = height_range_json[range_index][JSON_ASSEMPLE_OBJECT_MIN_Z]; - height_range.max_z = height_range_json[range_index][JSON_ASSEMPLE_OBJECT_MAX_Z]; - height_range.range_params = height_range_json[range_index][JSON_ASSEMPLE_OBJECT_RANGE_PARAMS].get>(); - } - } - } - if (plate_json.contains(JSON_ASSEMPLE_ASSEMBLE_PARAMS)) { - json assemble_params_json = plate_json[JSON_ASSEMPLE_ASSEMBLE_PARAMS]; - int assemble_count = assemble_params_json.size(); - for (int i = 0; i < assemble_count; i++) - { - assembled_param_info_t assembled_param; - int assemble_index = assemble_params_json[i][JSON_ASSEMPLE_OBJECT_ASSEMBLE_INDEX]; - if (assemble_params_json[i].contains(JSON_ASSEMPLE_OBJECT_PRINT_PARAMS)) { - assembled_param.print_params = assemble_params_json[i][JSON_ASSEMPLE_OBJECT_PRINT_PARAMS].get>(); - BOOST_LOG_TRIVIAL(debug) << boost::format("Plate %1%, assemble object %2% has %3% print params") % (plate_index + 1) %i % assembled_param.print_params.size(); - } - if (assemble_params_json[i].contains(JSON_ASSEMPLE_OBJECT_HEIGHT_RANGES)) { - json height_range_json = assemble_params_json[i][JSON_ASSEMPLE_OBJECT_HEIGHT_RANGES]; - int range_count = height_range_json.size(); - - BOOST_LOG_TRIVIAL(debug) << boost::format("Plate %1%, assemble object %2% has %3% height ranges") % (plate_index + 1) %i % range_count; - - assembled_param.height_ranges.resize(range_count); - for (int range_index = 0; range_index < range_count; range_index++) - { - height_range_info_t& height_range = assembled_param.height_ranges[range_index]; - height_range.min_z = height_range_json[range_index][JSON_ASSEMPLE_OBJECT_MIN_Z]; - height_range.max_z = height_range_json[range_index][JSON_ASSEMPLE_OBJECT_MAX_Z]; - height_range.range_params = height_range_json[range_index][JSON_ASSEMPLE_OBJECT_RANGE_PARAMS].get>(); - } - } - assemble_plate.assembled_param_list.emplace(assemble_index, std::move(assembled_param)); - } - BOOST_LOG_TRIVIAL(debug) << boost::format("Plate %1%, has %2% plate params") % (plate_index + 1) % assemble_plate.plate_params.size(); - } - } - } - catch(std::exception &err) { - BOOST_LOG_TRIVIAL(error) << __FUNCTION__<< ": parse file "<("inherits_group"); + // One entry for the process, one per filament and one for the printer. A group of another + // length still has the process first and the printer last; one too short for that is ignored. + if (option_strings && option_strings->values.size() != current_filaments_name.size() + 2) { + boost::nowide::cerr << "Warning: inherits_group has " << option_strings->values.size() << " entries, expected " + << current_filaments_name.size() + 2 << " for " << current_filaments_name.size() << " filaments" << std::endl; + if (option_strings->values.size() < 2) + option_strings = nullptr; + } if (option_strings) { current_inherits_group = option_strings->values; size_t size = current_inherits_group.size(); @@ -1927,14 +1779,11 @@ int CLI::run(int argc, char **argv) BOOST_LOG_TRIVIAL(info) << boost::format("inherits of process valid, current_process_system_name is %1%") %current_process_system_name; } - current_filaments_system_name.resize(size - 2); - for (int index = 1; index < (size - 1); index++) { - if (current_inherits_group[index].empty()) { - current_filaments_system_name[index-1] = current_filaments_name[index-1]; - } - else { + // A filament without an entry of its own counts as a system preset. + current_filaments_system_name = current_filaments_name; + for (size_t index = 1; index < size - 1 && index <= current_filaments_name.size(); index++) { + if (!current_inherits_group[index].empty()) current_filaments_system_name[index-1] = current_inherits_group[index]; - } } } else { @@ -2067,7 +1916,12 @@ int CLI::run(int argc, char **argv) //parse the json and assemble object here Model model; - int ret = load_assemble_plate_list(load_assemble_list, assemble_plate_info_list); + AssembleListResult list_result = load_assemble_plate_list(load_assemble_list, assemble_plate_info_list, MAX_PLATE_COUNT); + int ret = CLI_SUCCESS; + if (list_result == AssembleListResult::FileNotFound) + ret = CLI_FILE_NOTFOUND; + else if (list_result == AssembleListResult::ConfigError) + ret = CLI_CONFIG_FILE_ERROR; if (ret) { record_exit_reson(outfile_dir, ret, 0, cli_errors[ret], sliced_info); flush_and_exit(ret); @@ -4881,6 +4735,11 @@ int CLI::run(int argc, char **argv) record_exit_reson(outfile_dir, CLI_INVALID_PARAMS, 0, cli_errors[CLI_INVALID_PARAMS], sliced_info); flush_and_exit(CLI_INVALID_PARAMS); } + if (m_models.empty()) { + boost::nowide::cerr << "Invalid params: --assemble needs at least one input model." << std::endl; + record_exit_reson(outfile_dir, CLI_INVALID_PARAMS, 0, cli_errors[CLI_INVALID_PARAMS], sliced_info); + flush_and_exit(CLI_INVALID_PARAMS); + } Model m; ModelObject* new_object = m.add_object(); new_object->name = _u8L("Assembly"); @@ -5821,7 +5680,7 @@ int CLI::run(int argc, char **argv) float w = dynamic_cast(m_print_config.option("prime_tower_width"))->value; float a = dynamic_cast(m_print_config.option("wipe_tower_rotation_angle"))->value; float v = dynamic_cast(m_print_config.option("prime_volume"))->value; - unsigned int filaments_cnt = plate_data_src[plate_to_slice-1]->slice_filaments_info.size(); + unsigned int filaments_cnt = (plate_data_src.size() >= static_cast(plate_to_slice)) ? plate_data_src[plate_to_slice-1]->slice_filaments_info.size() : 0; if ((filaments_cnt == 0) || need_skip) { // slice filaments info invalid diff --git a/src/OrcaSlicer.hpp b/src/OrcaSlicer.hpp index 9aceee4a20..6d18e16fce 100644 --- a/src/OrcaSlicer.hpp +++ b/src/OrcaSlicer.hpp @@ -12,6 +12,7 @@ #include "libslic3r/Config.hpp" #include "libslic3r/Model.hpp" +#include "libslic3r/Format/AssembleList.hpp" namespace Slic3r { @@ -26,64 +27,6 @@ namespace IO { }; } -#define JSON_ASSEMPLE_PLATES "plates" -#define JSON_ASSEMPLE_PLATE_PARAMS "plate_params" -#define JSON_ASSEMPLE_PLATE_NAME "plate_name" -#define JSON_ASSEMPLE_PLATE_NEED_ARRANGE "need_arrange" -#define JSON_ASSEMPLE_OBJECTS "objects" -#define JSON_ASSEMPLE_OBJECT_PATH "path" -#define JSON_ASSEMPLE_OBJECT_COUNT "count" -#define JSON_ASSEMPLE_OBJECT_FILAMENTS "filaments" -#define JSON_ASSEMPLE_OBJECT_POS_X "pos_x" -#define JSON_ASSEMPLE_OBJECT_POS_Y "pos_y" -#define JSON_ASSEMPLE_OBJECT_POS_Z "pos_z" -#define JSON_ASSEMPLE_OBJECT_ASSEMBLE_INDEX "assemble_index" -#define JSON_ASSEMPLE_OBJECT_PRINT_PARAMS "print_params" -#define JSON_ASSEMPLE_ASSEMBLE_PARAMS "assembled_params" - - -#define JSON_ASSEMPLE_OBJECT_MIN_Z "min_z" -#define JSON_ASSEMPLE_OBJECT_MAX_Z "max_z" -#define JSON_ASSEMPLE_OBJECT_HEIGHT_RANGES "height_ranges" -#define JSON_ASSEMPLE_OBJECT_RANGE_PARAMS "range_params" - -typedef struct _height_range_info { - float min_z; - float max_z; - - std::map range_params; -}height_range_info_t; - -typedef struct _assembled_param_info { - std::map print_params; - std::vector height_ranges; -}assembled_param_info_t; - -typedef struct _assemble_object_info { - std::string path; - int count; - - std::vector filaments; - std::vector assemble_index; - std::vector pos_x; - std::vector pos_y; - std::vector pos_z; - std::map print_params; - std::vector height_ranges; -}assemble_object_info_t; - -typedef struct _assemble_plate_info { - std::string plate_name; - bool need_arrange {false}; - int filaments_count {0}; - - std::map plate_params; - std::vector assemble_obj_list; - std::vector loaded_obj_list; - std::map assembled_param_list; -}assemble_plate_info_t; - - typedef struct _printer_plate_info { std::string printer_name; int printable_width{0}; diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index ef15304c8b..d569b1726e 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -202,6 +202,8 @@ set(lisbslic3r_sources Format/3mf.hpp Format/AMF.cpp Format/AMF.hpp + Format/AssembleList.cpp + Format/AssembleList.hpp Format/DRC.cpp Format/DRC.hpp Format/bbs_3mf.cpp diff --git a/src/libslic3r/Format/AssembleList.cpp b/src/libslic3r/Format/AssembleList.cpp new file mode 100644 index 0000000000..5000711ea6 --- /dev/null +++ b/src/libslic3r/Format/AssembleList.cpp @@ -0,0 +1,204 @@ +#include "AssembleList.hpp" + +#include + +#include +#include +#include +#include + +#include "nlohmann/json.hpp" + +#define JSON_ASSEMPLE_PLATES "plates" +#define JSON_ASSEMPLE_PLATE_PARAMS "plate_params" +#define JSON_ASSEMPLE_PLATE_NAME "plate_name" +#define JSON_ASSEMPLE_PLATE_NEED_ARRANGE "need_arrange" +#define JSON_ASSEMPLE_OBJECTS "objects" +#define JSON_ASSEMPLE_OBJECT_PATH "path" +#define JSON_ASSEMPLE_OBJECT_COUNT "count" +#define JSON_ASSEMPLE_OBJECT_FILAMENTS "filaments" +#define JSON_ASSEMPLE_OBJECT_POS_X "pos_x" +#define JSON_ASSEMPLE_OBJECT_POS_Y "pos_y" +#define JSON_ASSEMPLE_OBJECT_POS_Z "pos_z" +#define JSON_ASSEMPLE_OBJECT_ASSEMBLE_INDEX "assemble_index" +#define JSON_ASSEMPLE_OBJECT_PRINT_PARAMS "print_params" +#define JSON_ASSEMPLE_ASSEMBLE_PARAMS "assembled_params" + + +#define JSON_ASSEMPLE_OBJECT_MIN_Z "min_z" +#define JSON_ASSEMPLE_OBJECT_MAX_Z "max_z" +#define JSON_ASSEMPLE_OBJECT_HEIGHT_RANGES "height_ranges" +#define JSON_ASSEMPLE_OBJECT_RANGE_PARAMS "range_params" + +namespace Slic3r { + +using json = nlohmann::json; + +AssembleListResult load_assemble_plate_list(const std::string &config_file, std::vector &assemble_plate_info_list, int max_plate_count) +{ + AssembleListResult ret = AssembleListResult::Success; + boost::filesystem::path directory_path(config_file); + + BOOST_LOG_TRIVIAL(info) << boost::format("%1% enter, file %2%")%__FUNCTION__ % config_file; + if (!boost::filesystem::exists(directory_path)) { + BOOST_LOG_TRIVIAL(error) << boost::format("directory %1% not exist.")%config_file; + return AssembleListResult::FileNotFound; + } + + try { + json root_json; + boost::nowide::ifstream ifs(config_file); + ifs >> root_json; + ifs.close(); + + int plate_count = root_json[JSON_ASSEMPLE_PLATES].size(); + if ((plate_count <= 0) || (plate_count > max_plate_count)) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__<< boost::format(": invalid plate count %1%")%plate_count; + return AssembleListResult::ConfigError; + } + assemble_plate_info_list.resize(plate_count); + + for (int plate_index = 0; plate_index < plate_count; plate_index++) + { + assemble_plate_info_t &assemble_plate = assemble_plate_info_list[plate_index]; + const json& plate_json = root_json[JSON_ASSEMPLE_PLATES][plate_index]; + assemble_plate.plate_name = plate_json[JSON_ASSEMPLE_PLATE_NAME]; + assemble_plate.need_arrange = plate_json[JSON_ASSEMPLE_PLATE_NEED_ARRANGE]; + + if (plate_json.contains(JSON_ASSEMPLE_PLATE_PARAMS)) { + assemble_plate.plate_params = plate_json[JSON_ASSEMPLE_PLATE_PARAMS].get>(); + BOOST_LOG_TRIVIAL(debug) << boost::format("Plate %1%, has %2% plate params") % (plate_index + 1) % assemble_plate.plate_params.size(); + } + + int object_count = plate_json[JSON_ASSEMPLE_OBJECTS].size(); + if (object_count <= 0) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__<< boost::format(": invalid object count %1% in plate %2%")%object_count %(plate_index+1); + return AssembleListResult::ConfigError; + } + + assemble_plate.assemble_obj_list.resize(object_count); + for (int object_index = 0; object_index < object_count; object_index++) + { + assemble_object_info_t& assemble_object = assemble_plate.assemble_obj_list[object_index]; + const json& object_json = plate_json[JSON_ASSEMPLE_OBJECTS][object_index]; + + assemble_object.path = object_json[JSON_ASSEMPLE_OBJECT_PATH]; + assemble_object.count = object_json[JSON_ASSEMPLE_OBJECT_COUNT]; + + if (assemble_object.count <= 0) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(": invalid object clone count %1% in plate %2% Object %3%") % assemble_object.count % (plate_index + 1) % assemble_object.path; + return AssembleListResult::ConfigError; + } + + assemble_object.filaments = object_json.at(JSON_ASSEMPLE_OBJECT_FILAMENTS).get>(); + if (assemble_object.filaments.empty()) + { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(": object %1%'s filaments list is empty") % assemble_object.path; + return AssembleListResult::ConfigError; + } + if ((assemble_object.filaments.size() != assemble_object.count) && (assemble_object.filaments.size() != 1)) + { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(": object %1%'s filaments count %2% not equal to clone count %3%, also not equal to 1") % assemble_object.path % assemble_object.filaments.size() % assemble_object.count; + return AssembleListResult::ConfigError; + } + // 0 keeps the default filament, as it does for --load-filament-ids. + if (std::any_of(assemble_object.filaments.begin(), assemble_object.filaments.end(), [](int id) { return id < 0; })) + { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(": object %1% has a negative filament id") % assemble_object.path; + return AssembleListResult::ConfigError; + } + + if (object_json.contains(JSON_ASSEMPLE_OBJECT_ASSEMBLE_INDEX)) { + assemble_object.assemble_index = object_json[JSON_ASSEMPLE_OBJECT_ASSEMBLE_INDEX].get>(); + if ((assemble_object.assemble_index.size() > 0) && (assemble_object.assemble_index.size() != assemble_object.count) && (assemble_object.assemble_index.size() != 1)) + { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(": object %1%'s assemble_index count %2% not equal to clone count %3%, also not equal to 1") % assemble_object.path % assemble_object.assemble_index.size() % assemble_object.count; + return AssembleListResult::ConfigError; + } + } + + if (object_json.contains(JSON_ASSEMPLE_OBJECT_POS_X)) { + assemble_object.pos_x = object_json[JSON_ASSEMPLE_OBJECT_POS_X].get>(); + if ((assemble_object.pos_x.size() > 0) && (assemble_object.pos_x.size() != assemble_object.count) && (assemble_object.pos_x.size() != 1)) + { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(": object %1%'s pos_x count %2% not equal to clone count %3%, also not equal to 1") % assemble_object.path % assemble_object.pos_x.size() % assemble_object.count; + return AssembleListResult::ConfigError; + } + } + if (object_json.contains(JSON_ASSEMPLE_OBJECT_POS_Y)) { + assemble_object.pos_y = object_json[JSON_ASSEMPLE_OBJECT_POS_Y].get>(); + if ((assemble_object.pos_y.size() > 0) && (assemble_object.pos_y.size() != assemble_object.count) && (assemble_object.pos_y.size() != 1)) + { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(": object %1%'s pos_y count %2% not equal to clone count %3%, also not equal to 1") % assemble_object.path % assemble_object.pos_y.size() % assemble_object.count; + return AssembleListResult::ConfigError; + } + } + if (object_json.contains(JSON_ASSEMPLE_OBJECT_POS_Z)) { + assemble_object.pos_z = object_json[JSON_ASSEMPLE_OBJECT_POS_Z].get>(); + if ((assemble_object.pos_z.size() > 0) && (assemble_object.pos_z.size() != assemble_object.count) && (assemble_object.pos_z.size() != 1)) + { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(": object %1%'s pos_z count %2% not equal to clone count %3%, also not equal to 1") % assemble_object.path % assemble_object.pos_z.size() % assemble_object.count; + return AssembleListResult::ConfigError; + } + } + if (object_json.contains(JSON_ASSEMPLE_OBJECT_PRINT_PARAMS)) { + assemble_object.print_params = object_json[JSON_ASSEMPLE_OBJECT_PRINT_PARAMS].get>(); + BOOST_LOG_TRIVIAL(debug) << boost::format("Plate %1%, object %2% has %3% print params") % (plate_index + 1) %assemble_object.path % assemble_object.print_params.size(); + } + if (object_json.contains(JSON_ASSEMPLE_OBJECT_HEIGHT_RANGES)) { + json height_range_json = object_json[JSON_ASSEMPLE_OBJECT_HEIGHT_RANGES]; + int range_count = height_range_json.size(); + + BOOST_LOG_TRIVIAL(debug) << boost::format("Plate %1%, object %2% has %3% height ranges") % (plate_index + 1) %assemble_object.path % range_count; + + assemble_object.height_ranges.resize(range_count); + for (int range_index = 0; range_index < range_count; range_index++) + { + height_range_info_t& height_range = assemble_object.height_ranges[range_index]; + height_range.min_z = height_range_json[range_index][JSON_ASSEMPLE_OBJECT_MIN_Z]; + height_range.max_z = height_range_json[range_index][JSON_ASSEMPLE_OBJECT_MAX_Z]; + height_range.range_params = height_range_json[range_index][JSON_ASSEMPLE_OBJECT_RANGE_PARAMS].get>(); + } + } + } + if (plate_json.contains(JSON_ASSEMPLE_ASSEMBLE_PARAMS)) { + json assemble_params_json = plate_json[JSON_ASSEMPLE_ASSEMBLE_PARAMS]; + int assemble_count = assemble_params_json.size(); + for (int i = 0; i < assemble_count; i++) + { + assembled_param_info_t assembled_param; + int assemble_index = assemble_params_json[i][JSON_ASSEMPLE_OBJECT_ASSEMBLE_INDEX]; + if (assemble_params_json[i].contains(JSON_ASSEMPLE_OBJECT_PRINT_PARAMS)) { + assembled_param.print_params = assemble_params_json[i][JSON_ASSEMPLE_OBJECT_PRINT_PARAMS].get>(); + BOOST_LOG_TRIVIAL(debug) << boost::format("Plate %1%, assemble object %2% has %3% print params") % (plate_index + 1) %i % assembled_param.print_params.size(); + } + if (assemble_params_json[i].contains(JSON_ASSEMPLE_OBJECT_HEIGHT_RANGES)) { + json height_range_json = assemble_params_json[i][JSON_ASSEMPLE_OBJECT_HEIGHT_RANGES]; + int range_count = height_range_json.size(); + + BOOST_LOG_TRIVIAL(debug) << boost::format("Plate %1%, assemble object %2% has %3% height ranges") % (plate_index + 1) %i % range_count; + + assembled_param.height_ranges.resize(range_count); + for (int range_index = 0; range_index < range_count; range_index++) + { + height_range_info_t& height_range = assembled_param.height_ranges[range_index]; + height_range.min_z = height_range_json[range_index][JSON_ASSEMPLE_OBJECT_MIN_Z]; + height_range.max_z = height_range_json[range_index][JSON_ASSEMPLE_OBJECT_MAX_Z]; + height_range.range_params = height_range_json[range_index][JSON_ASSEMPLE_OBJECT_RANGE_PARAMS].get>(); + } + } + assemble_plate.assembled_param_list.emplace(assemble_index, std::move(assembled_param)); + } + BOOST_LOG_TRIVIAL(debug) << boost::format("Plate %1%, has %2% plate params") % (plate_index + 1) % assemble_plate.plate_params.size(); + } + } + } + catch(std::exception &err) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__<< ": parse file "< ${ORCA_CLI_TEST_PYTHON}) +set_tests_properties(cli_malformed_input PROPERTIES + LABELS "CLI;RequiresApp" + SKIP_RETURN_CODE 77 + TIMEOUT 900) diff --git a/tests/cli/test_cli_malformed_input.sh b/tests/cli/test_cli_malformed_input.sh new file mode 100644 index 0000000000..b6899a301f --- /dev/null +++ b/tests/cli/test_cli_malformed_input.sh @@ -0,0 +1,207 @@ +#!/usr/bin/env bash +# End-to-end checks that malformed CLI input fails cleanly, or loads, instead of crashing the +# orca-slicer binary. Each case lives inline in CLI::run(), so only the binary can reach it. +# +# - A project whose inherits_group does not have one entry per filament plus the process and +# printer entries still loads. +# - --slice N --arrange 1 on a project without plate metadata slices plate N. +# - An assemble list object with an empty filament list, or a negative filament id, is rejected +# as a config error. +# - --assemble with no input model is rejected as invalid parameters. +# +# usage: test_cli_malformed_input.sh +set -u + +BIN="${1:-}" +PY="${2:-python3}" +# 77 is the test's SKIP_RETURN_CODE. +[ -x "$BIN" ] || { echo "SKIP: orca-slicer binary not found: $BIN"; exit 77; } + +# From src/libslic3r/Utils.hpp. main() returns them, so the shell sees them modulo 256. +CLI_SUCCESS=0 +CLI_INVALID_PARAMS=-2 +CLI_CONFIG_FILE_ERROR=-5 + +WORK="$(mktemp -d "${TMPDIR:-/tmp}/orca-cli-malformed.XXXXXX")" +trap 'rm -rf "$WORK"' EXIT +mkdir -p "$WORK/datadir" + +# Standalone presets: without "inherits" the CLI loads them as-is, with no preset bundle. +cat > "$WORK/machine.json" <<'EOF' +{ + "type": "machine", + "from": "User", + "name": "CLI malformed input test printer", + "printable_area": ["0x0", "200x0", "200x200", "0x200"], + "printable_height": "100", + "layer_change_gcode": "G92 E0" +} +EOF +cat > "$WORK/process.json" <<'EOF' +{ + "type": "process", + "from": "User", + "name": "CLI malformed input test process" +} +EOF +cat > "$WORK/filament.json" <<'EOF' +{ + "type": "filament", + "from": "User", + "name": "CLI malformed input test filament" +} +EOF + +"$PY" - "$WORK/cube.stl" <<'EOF' +import sys + +v = [(x, y, z) for z in (0, 10) for y in (0, 10) for x in (0, 10)] +with open(sys.argv[1], "w") as f: + f.write("solid cube\n") + # Faces wound counter-clockwise seen from outside: -z, +z, -y, +y, -x, +x. + for a, b, c, d in ((0, 2, 3, 1), (4, 5, 7, 6), (0, 1, 5, 4), (2, 6, 7, 3), (0, 4, 6, 2), (1, 3, 7, 5)): + for tri in ((a, b, c), (a, c, d)): + f.write("facet normal 0 0 0\nouter loop\n") + for i in tri: + f.write("vertex %g %g %g\n" % v[i]) + f.write("endloop\nendfacet\n") + f.write("endsolid cube\n") +EOF + +fails=0 +fail() { echo "FAIL: $*"; fails=$((fails + 1)); } + +# run [option...]: run into $WORK/, keeping the log and the shell status there. +run() { + local out="$WORK/$1"; shift + mkdir -p "$out" + timeout 300 "$BIN" --datadir "$WORK/datadir" --outputdir "$out" "$@" > "$out/log" 2>&1 + echo $? > "$out/status" +} + +# run_presets [option...]: run with the standalone presets loaded. +run_presets() { + local tag="$1"; shift + run "$tag" --load-settings "$WORK/machine.json;$WORK/process.json" --load-filaments "$WORK/filament.json" "$@" +} + +# expect_status +expect_status() { + local got; got="$(cat "$WORK/$1/status")" + [ "$got" -eq $(( $2 & 255 )) ] || fail "$1: shell status $got, want $(( $2 & 255 )) (code $2)" +} + +# expect_result : a failing run must also carry an error_string. +expect_result() { + "$PY" - "$WORK/$1/result.json" "$2" <<'EOF' || fail "$1: result.json" +import json, sys + +try: + with open(sys.argv[1]) as f: + result = json.load(f) +except (OSError, ValueError) as e: + sys.exit("cannot read %s: %s" % (sys.argv[1], e)) +want_rc = int(sys.argv[2]) +if result.get("return_code") != want_rc: + sys.exit("return_code %r, want %d" % (result.get("return_code"), want_rc)) +if want_rc != 0 and not result.get("error_string"): + sys.exit("no error_string") +EOF +} + +# expect_log +expect_log() { + grep -qF -- "$2" "$WORK/$1/log" || fail "$1: log does not mention \"$2\"" +} + +# expect_gcode +expect_gcode() { + compgen -G "$WORK/$1/*.gcode" > /dev/null || fail "$1: no G-code was exported" +} + +# rewrite_3mf inherits | no-plates +rewrite_3mf() { + "$PY" - "$@" <<'EOF' +import json, re, sys, zipfile + +src, dst, mode = sys.argv[1:4] +with zipfile.ZipFile(src) as zin, zipfile.ZipFile(dst, "w", zipfile.ZIP_DEFLATED) as zout: + for info in zin.infolist(): + data = zin.read(info.filename) + if mode == "inherits" and info.filename == "Metadata/project_settings.config": + config = json.loads(data) + config["inherits_group"] = json.loads(sys.argv[4]) + data = json.dumps(config, indent=4).encode() + elif mode == "no-plates": + if re.match(r"Metadata/plate_\d+\.", info.filename): + continue + if info.filename == "Metadata/model_settings.config": + data = re.sub(rb"\s*.*?", b"", data, flags=re.S) + zout.writestr(info, data) +EOF +} + +# assemble_list +assemble_list() { + cat > "$1" < + +#include "libslic3r/Format/AssembleList.hpp" + +#include "test_utils.hpp" + +#include +#include + +#include +#include + +using namespace Slic3r; +using Catch::Matchers::WithinAbs; +using nlohmann::json; + +static constexpr int max_plates = 36; + +static AssembleListResult load_text(const std::string &text, std::vector &plates) +{ + ScopedTemporaryFile file(".json"); + { + boost::nowide::ofstream out(file.string()); + out << text; + } + return load_assemble_plate_list(file.string(), plates, max_plates); +} + +static AssembleListResult load_json(const json &root) +{ + std::vector plates; + return load_text(root.dump(), plates); +} + +// One plate with one object of three clones, which every optional field accepts. +static json valid_list() +{ + return json::parse(R"({ + "plates": [{ + "plate_name": "plate", + "need_arrange": false, + "objects": [{ + "path": "cube.stl", + "count": 3, + "filaments": [1], + "height_ranges": [{ "min_z": 0, "max_z": 5, "range_params": { "layer_height": "0.1" } }] + }], + "assembled_params": [{ + "assemble_index": 1, + "height_ranges": [{ "min_z": 0, "max_z": 5, "range_params": { "layer_height": "0.1" } }] + }] + }] + })"); +} + +TEST_CASE("A valid assemble list parses into its plates and objects", "[AssembleList]") +{ + const std::string text = R"({ + "plates": [ + { + "plate_name": "first", + "need_arrange": true, + "plate_params": { "curr_bed_type": "Textured PEI Plate" }, + "objects": [ + { + "path": "a.stl", + "count": 2, + "filaments": [1, 3], + "assemble_index": [1], + "pos_x": [10.5, 20.5], + "pos_y": [30], + "pos_z": [0, 1], + "print_params": { "sparse_infill_density": "30%" }, + "height_ranges": [{ "min_z": 1.5, "max_z": 4, "range_params": { "layer_height": "0.12" } }] + }, + { "path": "b.stl", "count": 1, "filaments": [0] } + ], + "assembled_params": [{ "assemble_index": 1, "print_params": { "wall_loops": "4" } }] + }, + { + "plate_name": "second", + "need_arrange": false, + "objects": [{ "path": "c.stl", "count": 1, "filaments": [2] }] + } + ] + })"; + std::vector plates; + REQUIRE(load_text(text, plates) == AssembleListResult::Success); + REQUIRE(plates.size() == 2); + + const assemble_plate_info_t &first = plates[0]; + CHECK(first.plate_name == "first"); + CHECK(first.need_arrange); + CHECK(first.plate_params.at("curr_bed_type") == "Textured PEI Plate"); + REQUIRE(first.assemble_obj_list.size() == 2); + + const assemble_object_info_t &a = first.assemble_obj_list[0]; + CHECK(a.path == "a.stl"); + CHECK(a.count == 2); + CHECK(a.filaments == std::vector{1, 3}); + CHECK(a.assemble_index == std::vector{1}); + REQUIRE(a.pos_x.size() == 2); + CHECK_THAT(a.pos_x[0], WithinAbs(10.5, 1e-6)); + CHECK_THAT(a.pos_x[1], WithinAbs(20.5, 1e-6)); + REQUIRE(a.pos_y.size() == 1); + CHECK_THAT(a.pos_y[0], WithinAbs(30., 1e-6)); + REQUIRE(a.pos_z.size() == 2); + CHECK_THAT(a.pos_z[1], WithinAbs(1., 1e-6)); + CHECK(a.print_params.at("sparse_infill_density") == "30%"); + REQUIRE(a.height_ranges.size() == 1); + CHECK_THAT(a.height_ranges[0].min_z, WithinAbs(1.5, 1e-6)); + CHECK_THAT(a.height_ranges[0].max_z, WithinAbs(4., 1e-6)); + CHECK(a.height_ranges[0].range_params.at("layer_height") == "0.12"); + + const assemble_object_info_t &b = first.assemble_obj_list[1]; + CHECK(b.path == "b.stl"); + CHECK(b.count == 1); + CHECK(b.filaments == std::vector{0}); + CHECK(b.pos_x.empty()); + CHECK(b.assemble_index.empty()); + + REQUIRE(first.assembled_param_list.count(1) == 1); + CHECK(first.assembled_param_list.at(1).print_params.at("wall_loops") == "4"); + + const assemble_plate_info_t &second = plates[1]; + CHECK(second.plate_name == "second"); + CHECK_FALSE(second.need_arrange); + REQUIRE(second.assemble_obj_list.size() == 1); + CHECK(second.assemble_obj_list[0].path == "c.stl"); + CHECK(second.assemble_obj_list[0].filaments == std::vector{2}); +} + +TEST_CASE("The unmodified fixture used by the rule tests is accepted", "[AssembleList]") +{ + CHECK(load_json(valid_list()) == AssembleListResult::Success); +} + +TEST_CASE("An object with an empty filament list is rejected", "[AssembleList]") +{ + json root = valid_list(); + root["plates"][0]["objects"][0]["filaments"] = json::array(); + CHECK(load_json(root) == AssembleListResult::ConfigError); +} + +TEST_CASE("An object with a negative filament id is rejected", "[AssembleList]") +{ + json root = valid_list(); + root["plates"][0]["objects"][0]["filaments"] = GENERATE(json::array({-1}), json::array({1, -2, 1})); + CAPTURE(root["plates"][0]["objects"][0]["filaments"].dump()); + CHECK(load_json(root) == AssembleListResult::ConfigError); +} + +TEST_CASE("Filament id 0 is accepted", "[AssembleList]") +{ + json root = valid_list(); + root["plates"][0]["objects"][0]["filaments"] = GENERATE(json::array({0}), json::array({0, 1, 0})); + CAPTURE(root["plates"][0]["objects"][0]["filaments"].dump()); + CHECK(load_json(root) == AssembleListResult::Success); +} + +TEST_CASE("Per-clone lists need one entry or one per clone", "[AssembleList]") +{ + // The fixture object has 3 clones. + const std::string key = GENERATE("filaments", "assemble_index", "pos_x", "pos_y", "pos_z"); + const size_t size = GENERATE(1, 2, 3, 4); + CAPTURE(key, size); + + json root = valid_list(); + root["plates"][0]["objects"][0][key] = json(std::vector(size, 1)); + const AssembleListResult expected = (size == 1 || size == 3) ? AssembleListResult::Success : AssembleListResult::ConfigError; + CHECK(load_json(root) == expected); +} + +TEST_CASE("An empty optional per-clone list is accepted", "[AssembleList]") +{ + const std::string key = GENERATE("assemble_index", "pos_x", "pos_y", "pos_z"); + CAPTURE(key); + + json root = valid_list(); + root["plates"][0]["objects"][0][key] = json::array(); + CHECK(load_json(root) == AssembleListResult::Success); +} + +// Fields read through a const reference (plate_name, need_arrange, objects, path, count) are +// looked up without a presence check, so only their wrong-type case is covered here. +TEST_CASE("A missing required field is rejected", "[AssembleList]") +{ + const std::string pointer = GENERATE("/plates", + "/plates/0/objects/0/filaments", + "/plates/0/objects/0/height_ranges/0/min_z", + "/plates/0/objects/0/height_ranges/0/max_z", + "/plates/0/objects/0/height_ranges/0/range_params", + "/plates/0/assembled_params/0/assemble_index", + "/plates/0/assembled_params/0/height_ranges/0/min_z", + "/plates/0/assembled_params/0/height_ranges/0/max_z", + "/plates/0/assembled_params/0/height_ranges/0/range_params"); + CAPTURE(pointer); + + json root = valid_list(); + const json::json_pointer ptr(pointer); + root[ptr.parent_pointer()].erase(ptr.back()); + CHECK(load_json(root) == AssembleListResult::ConfigError); +} + +TEST_CASE("A field of the wrong type is rejected", "[AssembleList]") +{ + const std::string pointer = GENERATE("/plates/0/plate_name", + "/plates/0/need_arrange", + "/plates/0/objects/0/path", + "/plates/0/objects/0/count", + "/plates/0/objects/0/filaments", + "/plates/0/objects/0/pos_x"); + CAPTURE(pointer); + + json root = valid_list(); + root[json::json_pointer(pointer)] = json::object(); + CHECK(load_json(root) == AssembleListResult::ConfigError); +} + +TEST_CASE("A plate or clone count out of range is rejected", "[AssembleList]") +{ + SECTION("no plates") + { + json root = valid_list(); + root["plates"] = json::array(); + CHECK(load_json(root) == AssembleListResult::ConfigError); + } + SECTION("more plates than the limit") + { + json root = valid_list(); + const json plate = root["plates"][0]; + for (int i = 1; i < max_plates; ++i) + root["plates"].push_back(plate); + CHECK(load_json(root) == AssembleListResult::Success); + root["plates"].push_back(plate); + CHECK(load_json(root) == AssembleListResult::ConfigError); + } + SECTION("a plate with no objects") + { + json root = valid_list(); + root["plates"][0]["objects"] = json::array(); + CHECK(load_json(root) == AssembleListResult::ConfigError); + } + SECTION("a clone count below 1") + { + json root = valid_list(); + root["plates"][0]["objects"][0]["count"] = GENERATE(0, -1); + CAPTURE(root["plates"][0]["objects"][0]["count"].dump()); + CHECK(load_json(root) == AssembleListResult::ConfigError); + } +} + +TEST_CASE("Malformed JSON is rejected", "[AssembleList]") +{ + const std::string text = GENERATE(std::string(), std::string("{\"plates\": ["), std::string("not json")); + CAPTURE(text); + std::vector plates; + CHECK(load_text(text, plates) == AssembleListResult::ConfigError); +} + +TEST_CASE("A missing file is reported as not found", "[AssembleList]") +{ + ScopedTemporaryFile file(".json"); + std::vector plates; + CHECK(load_assemble_plate_list(file.string(), plates, max_plates) == AssembleListResult::FileNotFound); +}