mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-30 04:11:00 +00:00
Move the Assemble List Parser into libslic3r
Behaviour-preserving move of the --load-assemble-list JSON parser and its plate/object structs from the CLI into libslic3r/Format/AssembleList, so the format can be unit tested. The parser returns its own AssembleListResult and takes the plate limit as a parameter; CLI::run maps the result to the same exit codes as before. Every validation rule and log message is unchanged. Adds Catch2 coverage of the valid layout and each validation rule.
This commit is contained in:
+6
-168
@@ -642,173 +642,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_t> &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<std::map<std::string, std::string>>();
|
||||
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<std::vector<int>>();
|
||||
if (assemble_object.filaments.empty())
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(": object %1%'s filaments list is empty") % assemble_object.path;
|
||||
return CLI_CONFIG_FILE_ERROR;
|
||||
}
|
||||
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 CLI_CONFIG_FILE_ERROR;
|
||||
}
|
||||
// 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 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<std::vector<int>>();
|
||||
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<std::vector<float>>();
|
||||
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<std::vector<float>>();
|
||||
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<std::vector<float>>();
|
||||
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<std::map<std::string, std::string>>();
|
||||
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<std::map<std::string, std::string>>();
|
||||
}
|
||||
}
|
||||
}
|
||||
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<std::map<std::string, std::string>>();
|
||||
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<std::map<std::string, std::string>>();
|
||||
}
|
||||
}
|
||||
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 "<<config_file<<" got a generic exception, reason = " << err.what();
|
||||
ret = CLI_CONFIG_FILE_ERROR;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
void merge_or_add_object(assemble_plate_info_t& assemble_plate_info, Model &model, int assemble_index, std::map<int, ModelObject*> &merged_objects, ModelObject *ori_object)
|
||||
{
|
||||
if (assemble_index > 0) {
|
||||
@@ -2040,7 +1873,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);
|
||||
|
||||
+1
-58
@@ -5,6 +5,7 @@
|
||||
|
||||
#include "libslic3r/Config.hpp"
|
||||
#include "libslic3r/Model.hpp"
|
||||
#include "libslic3r/Format/AssembleList.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
@@ -19,64 +20,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<std::string, std::string> range_params;
|
||||
}height_range_info_t;
|
||||
|
||||
typedef struct _assembled_param_info {
|
||||
std::map<std::string, std::string> print_params;
|
||||
std::vector<height_range_info_t> height_ranges;
|
||||
}assembled_param_info_t;
|
||||
|
||||
typedef struct _assemble_object_info {
|
||||
std::string path;
|
||||
int count;
|
||||
|
||||
std::vector<int> filaments;
|
||||
std::vector<int> assemble_index;
|
||||
std::vector<float> pos_x;
|
||||
std::vector<float> pos_y;
|
||||
std::vector<float> pos_z;
|
||||
std::map<std::string, std::string> print_params;
|
||||
std::vector<height_range_info_t> 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<std::string, std::string> plate_params;
|
||||
std::vector<assemble_object_info_t> assemble_obj_list;
|
||||
std::vector<ModelObject *> loaded_obj_list;
|
||||
std::map<int, assembled_param_info_t> assembled_param_list;
|
||||
}assemble_plate_info_t;
|
||||
|
||||
|
||||
typedef struct _printer_plate_info {
|
||||
std::string printer_name;
|
||||
int printable_width{0};
|
||||
|
||||
@@ -205,6 +205,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
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
#include "AssembleList.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <boost/format.hpp>
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <boost/nowide/fstream.hpp>
|
||||
|
||||
#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_t> &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<std::map<std::string, std::string>>();
|
||||
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<std::vector<int>>();
|
||||
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<std::vector<int>>();
|
||||
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<std::vector<float>>();
|
||||
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<std::vector<float>>();
|
||||
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<std::vector<float>>();
|
||||
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<std::map<std::string, std::string>>();
|
||||
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<std::map<std::string, std::string>>();
|
||||
}
|
||||
}
|
||||
}
|
||||
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<std::map<std::string, std::string>>();
|
||||
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<std::map<std::string, std::string>>();
|
||||
}
|
||||
}
|
||||
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 "<<config_file<<" got a generic exception, reason = " << err.what();
|
||||
ret = AssembleListResult::ConfigError;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,60 @@
|
||||
#ifndef slic3r_Format_AssembleList_hpp_
|
||||
#define slic3r_Format_AssembleList_hpp_
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
class ModelObject;
|
||||
|
||||
typedef struct _height_range_info {
|
||||
float min_z;
|
||||
float max_z;
|
||||
|
||||
std::map<std::string, std::string> range_params;
|
||||
}height_range_info_t;
|
||||
|
||||
typedef struct _assembled_param_info {
|
||||
std::map<std::string, std::string> print_params;
|
||||
std::vector<height_range_info_t> height_ranges;
|
||||
}assembled_param_info_t;
|
||||
|
||||
typedef struct _assemble_object_info {
|
||||
std::string path;
|
||||
int count;
|
||||
|
||||
std::vector<int> filaments;
|
||||
std::vector<int> assemble_index;
|
||||
std::vector<float> pos_x;
|
||||
std::vector<float> pos_y;
|
||||
std::vector<float> pos_z;
|
||||
std::map<std::string, std::string> print_params;
|
||||
std::vector<height_range_info_t> 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<std::string, std::string> plate_params;
|
||||
std::vector<assemble_object_info_t> assemble_obj_list;
|
||||
std::vector<ModelObject *> loaded_obj_list;
|
||||
std::map<int, assembled_param_info_t> assembled_param_list;
|
||||
}assemble_plate_info_t;
|
||||
|
||||
enum class AssembleListResult {
|
||||
Success,
|
||||
FileNotFound,
|
||||
// Malformed JSON, a missing required field, or a value that fails validation.
|
||||
ConfigError
|
||||
};
|
||||
|
||||
// Read the JSON assemble list used by the CLI's --load-assemble-list into one entry per plate.
|
||||
AssembleListResult load_assemble_plate_list(const std::string &config_file, std::vector<assemble_plate_info_t> &assemble_plate_info_list, int max_plate_count);
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif /* slic3r_Format_AssembleList_hpp_ */
|
||||
@@ -7,6 +7,7 @@ add_executable(${_TEST_NAME}_tests
|
||||
test_appconfig.cpp
|
||||
test_arachne_walls.cpp
|
||||
test_arrange.cpp
|
||||
test_assemble_list.cpp
|
||||
test_bambu_networking.cpp
|
||||
test_buildvolume.cpp
|
||||
test_calib.cpp
|
||||
|
||||
@@ -0,0 +1,266 @@
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
#include "libslic3r/Format/AssembleList.hpp"
|
||||
|
||||
#include "test_utils.hpp"
|
||||
|
||||
#include <boost/nowide/fstream.hpp>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
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<assemble_plate_info_t> &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<assemble_plate_info_t> 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<assemble_plate_info_t> 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<int>{1, 3});
|
||||
CHECK(a.assemble_index == std::vector<int>{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<int>{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<int>{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<int>(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<assemble_plate_info_t> plates;
|
||||
CHECK(load_text(text, plates) == AssembleListResult::ConfigError);
|
||||
}
|
||||
|
||||
TEST_CASE("A missing file is reported as not found", "[AssembleList]")
|
||||
{
|
||||
ScopedTemporaryFile file(".json");
|
||||
std::vector<assemble_plate_info_t> plates;
|
||||
CHECK(load_assemble_plate_list(file.string(), plates, max_plates) == AssembleListResult::FileNotFound);
|
||||
}
|
||||
Reference in New Issue
Block a user