Compare commits

...
Author SHA1 Message Date
Hanif Koh fc2ec7de0f Keep a Project's Changed Values on Extruder Variants It Doesn't List
A project's listed settings are carried onto its base preset by update_non_diff_values_to_base_config, which matched variants by exact name and id. A variant the base gained after the project was saved got the base's value, while the same value in a user preset now falls back to the preset's first variant of that extruder. So an old project opened with its printer preset already modified, and saving it wrote the base's values into the 3MF.

The function now maps variants with map_variant_indices, as update_diff_values_to_child_config does: a base variant the project does not list takes the project's first variant of the same extruder. The variant lists themselves stay the base's, so a fallback never writes one variant's name over another's.
2026-10-04 04:54:36 +08:00
HanifKoh d1a3ef68c5 Fix CLI Crashes on Malformed Project, Assemble List and No-Input Runs (#15978)
* Fix CLI Crashes on Malformed Project, Assemble List and No-Input Runs

Four CLI paths indexed vectors without checking their size and crashed
with SIGSEGV on malformed input:

- A project inherits_group whose length is not the filament count plus
  the process and printer entries was split by position. It is now
  ignored with a warning, as if the project had none.
- An assemble list object with an empty filaments list passed validation
  and was then read at index 0. It is now rejected as a config error, as
  is a negative filament id.
- --slice N --arrange 1 on a project without plate metadata read the
  missing plate data. It now falls back to the plate's own filaments,
  like the other plate data reads.
- --assemble with no input model built an object with no volumes. It is
  now rejected as invalid parameters.

A tests/cli script covers each case through the binary, since all four
live inline in CLI::run().

* 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.

* Keep the Process and Printer of an inherits_group of the Wrong Length

A project whose inherits_group did not have one entry per filament plus
the process and printer entries was loaded as if it had none. The CLI
then looked for system presets under the names of the user presets,
found none and refused to slice a project that slices on main.

The group is now read as before: the process first, the printer last
and the filaments in between, up to the filament count. A filament
without an entry counts as a system preset. A group with fewer than two
entries is still ignored. The warning stays.
2026-10-04 03:17:40 +08:00
HanifKoh a80c323614 Let the CLI Resolve Presets on Installs That Ship Preset Caches Only (#16047)
Release builds install each vendor as its preset cache alone. The
read-only preset load the CLI uses to resolve an inheriting user preset
passed allow_cache = false to keep caches from being written, which
also stopped them from being read, so every vendor fell back to JSONs
that are not installed and the CLI failed.

The flag now only gates writing: a read-only load reads caches and
writes none. The filament library is also read from its cache whenever
that is all that is installed, so a vendor updated over the air still
resolves against it.
2026-10-04 00:40:38 +08:00
14 changed files with 1034 additions and 249 deletions
+24 -165
View File
@@ -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_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.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<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) {
@@ -1906,6 +1750,14 @@ int CLI::run(int argc, char **argv)
BOOST_LOG_TRIVIAL(info) << boost::format("current_printer_name %1%, current_process_name %2%")%current_printer_name %current_process_name;
ConfigOptionStrings* option_strings = config.option<ConfigOptionStrings>("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<const ConfigOptionFloat *>(m_print_config.option("prime_tower_width"))->value;
float a = dynamic_cast<const ConfigOptionFloat *>(m_print_config.option("wipe_tower_rotation_angle"))->value;
float v = dynamic_cast<const ConfigOptionFloat *>(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<size_t>(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
+1 -58
View File
@@ -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<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};
+2
View File
@@ -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
+204
View File
@@ -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
+60
View File
@@ -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_ */
+12 -10
View File
@@ -626,13 +626,15 @@ const PresetBundle *PresetBundle::load_source_vendor(const boost::filesystem::pa
return it->second.get();
// The library loads with no base of its own, so the tree a vendor inherits from
// is the same one that resolves the library's own presets.
const std::string library_file = std::string(ORCA_FILAMENT_LIBRARY);
const PresetBundle *library = nullptr;
if (vendor_id != ORCA_FILAMENT_LIBRARY &&
(boost::filesystem::is_regular_file(root_dir / (library_file + ".json")) ||
(allow_cache && boost::filesystem::is_regular_file(root_dir / (library_file + ".opc"))))) {
library = load_source_vendor(root_dir, ORCA_FILAMENT_LIBRARY, compatibility_rule, error, allow_cache);
// is the same one that resolves the library's own presets. It is only a base, so
// it comes from its cache whenever that is all that is installed, even when the
// vendor itself is parsed (a vendor updated over the air).
const std::string library_file = std::string(ORCA_FILAMENT_LIBRARY);
const bool library_json = boost::filesystem::is_regular_file(root_dir / (library_file + ".json"));
const bool library_cache_only = !library_json && boost::filesystem::is_regular_file(root_dir / (library_file + ".opc"));
const PresetBundle *library = nullptr;
if (vendor_id != ORCA_FILAMENT_LIBRARY && (library_json || library_cache_only)) {
library = load_source_vendor(root_dir, ORCA_FILAMENT_LIBRARY, compatibility_rule, error, allow_cache || library_cache_only);
if (library == nullptr) {
error = "OrcaFilamentLibrary contains invalid presets";
return nullptr;
@@ -2576,7 +2578,7 @@ void PresetBundle::clear_printer_hold_aliases()
//BBS: add json related logic, load system presets from json
std::pair<PresetsConfigSubstitutions, std::string> PresetBundle::load_system_presets_from_json(
ForwardCompatibilitySubstitutionRule compatibility_rule, bool allow_cache)
ForwardCompatibilitySubstitutionRule compatibility_rule, bool write_caches)
{
//BBS: add config related logs
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" enter, compatibility_rule %1%")%compatibility_rule;
@@ -2596,14 +2598,14 @@ std::pair<PresetsConfigSubstitutions, std::string> PresetBundle::load_system_pre
// The vendors below are loaded whole and against each other — the filament
// library first, then every other vendor with it as the base — so each parse
// is complete enough to be worth caching.
m_generate_vendor_caches = allow_cache && (m_generate_vendor_caches || !validation_mode);
m_generate_vendor_caches = write_caches && (m_generate_vendor_caches || !validation_mode);
// Sorted, so any duplicate-preset warning comes out in the same order on every run.
std::vector<VendorSource> vendors;
for (const std::string& name : vendor_names_in(dir))
if (name == ORCA_FILAMENT_LIBRARY || !(validation_mode && !vendor_to_validate.empty() && name != vendor_to_validate))
vendors.push_back({ name, dir });
auto result = this->load_vendors(vendors, compatibility_rule, allow_cache);
auto result = this->load_vendors(vendors, compatibility_rule, true);
this->update_system_maps();
+3 -1
View File
@@ -851,7 +851,9 @@ private:
//std::pair<PresetsConfigSubstitutions, std::string> load_system_presets(ForwardCompatibilitySubstitutionRule compatibility_rule);
//BBS: add json related logic
std::pair<PresetsConfigSubstitutions, std::string> load_system_presets_from_json(ForwardCompatibilitySubstitutionRule compatibility_rule, bool allow_cache = true);
// Reads each vendor from its preset cache where one covers the profile, as every load does.
// write_caches = false keeps a read-only load from writing caches into the data directory.
std::pair<PresetsConfigSubstitutions, std::string> load_system_presets_from_json(ForwardCompatibilitySubstitutionRule compatibility_rule, bool write_caches = true);
// Update the multicolor information for filaments.
void update_filament_multi_color();
// Update renamed_from and alias maps of system profiles.
+15 -13
View File
@@ -11513,12 +11513,18 @@ void DynamicPrintConfig::update_non_diff_values_to_base_config(DynamicPrintConfi
int cur_variant_count = cur_extruder_variants.size();
int target_variant_count = target_extruder_variants.size();
// A base variant this config does not list (the base gained it after the config was saved, or the
// config lists none) takes this config's first variant of the same extruder, as a user preset's
// values do in update_diff_values_to_child_config. Left unmatched, the base's value would silently
// replace the user's.
variant_index.resize(target_variant_count, -1);
if (cur_variant_count == 0) {
// Defensive: target_variant_count may be 0 if the preset doesn't carry extruder_variant_name.
// In that case keep variant_index empty and let the downstream size checks produce a useful error.
if (!variant_index.empty())
variant_index[0] = 0;
// This config's one value belongs to the extruder of the base's first variant.
variant_index = map_variant_indices(target_extruder_variants, target_extruder_ids, {},
target_extruder_ids.empty() ? std::vector<int>() : std::vector<int>{target_extruder_ids[0]});
}
else if ((cur_extruder_ids.size() > 0) && cur_variant_count != cur_extruder_ids.size()){
//should not happen
@@ -11531,18 +11537,7 @@ void DynamicPrintConfig::update_non_diff_values_to_base_config(DynamicPrintConfi
%extruder_variant_name %target_variant_count %extruder_id_name %target_extruder_ids.size();
}
else {
for (int i = 0; i < target_variant_count; i++)
{
for (int j = 0; j < cur_variant_count; j++)
{
if ((target_extruder_variants[i] == cur_extruder_variants[j])
&&(target_extruder_ids.empty() || (target_extruder_ids[i] == cur_extruder_ids[j])))
{
variant_index[i] = j;
break;
}
}
}
variant_index = map_variant_indices(target_extruder_variants, target_extruder_ids, cur_extruder_variants, cur_extruder_ids);
}
for (auto& opt : keys) {
@@ -11567,6 +11562,13 @@ void DynamicPrintConfig::update_non_diff_values_to_base_config(DynamicPrintConfi
if (cur_variant_count > target_variant_count)
continue;
// The variant lists are the base's layout itself, which every other value is
// carried onto: a variant this config lacks keeps its own name and id.
if (opt == extruder_id_name || opt == extruder_variant_name) {
opt_src->set(opt_target);
continue;
}
int stride = 1;
if (key_set2.find(opt) != key_set2.end())
stride = 2;
+7
View File
@@ -23,3 +23,10 @@ set_tests_properties(cli_project_missing_keys PROPERTIES
LABELS "CLI;RequiresApp"
SKIP_RETURN_CODE 77
TIMEOUT 900)
add_test(NAME cli_malformed_input
COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/test_cli_malformed_input.sh $<TARGET_FILE:OrcaSlicer> ${ORCA_CLI_TEST_PYTHON})
set_tests_properties(cli_malformed_input PROPERTIES
LABELS "CLI;RequiresApp"
SKIP_RETURN_CODE 77
TIMEOUT 900)
+207
View File
@@ -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 <orca-slicer binary> <python3>
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 <tag> [option...]: run into $WORK/<tag>, 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 <tag> [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 <tag> <cli code>
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 <tag> <return_code>: 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 <tag> <text>
expect_log() {
grep -qF -- "$2" "$WORK/$1/log" || fail "$1: log does not mention \"$2\""
}
# expect_gcode <tag>
expect_gcode() {
compgen -G "$WORK/$1/*.gcode" > /dev/null || fail "$1: no G-code was exported"
}
# rewrite_3mf <in> <out> inherits <json list> | 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*<plate>.*?</plate>", b"", data, flags=re.S)
zout.writestr(info, data)
EOF
}
# assemble_list <file> <filaments json>
assemble_list() {
cat > "$1" <<EOF
{"plates": [{"plate_name": "p", "need_arrange": false,
"objects": [{"path": "$WORK/cube.stl", "count": 1, "filaments": $2,
"pos_x": [100], "pos_y": [100]}]}]}
EOF
}
echo "== a one-filament project exported by the CLI is the base for the project cases"
run_presets export --slice 0 --export-3mf project.3mf "$WORK/cube.stl"
expect_status export $CLI_SUCCESS
[ -f "$WORK/export/project.3mf" ] || { echo "FAIL: project export failed"; tail -n 40 "$WORK/export/log"; exit 1; }
echo "== an inherits_group of the wrong length still loads"
for group in '[]' '[""]' '["", "", "", "", ""]'; do
tag="inherits_$("$PY" -c 'import json, sys; print(len(json.loads(sys.argv[1])))' "$group")"
rewrite_3mf "$WORK/export/project.3mf" "$WORK/$tag.3mf" inherits "$group"
run "$tag" --info "$WORK/$tag.3mf"
expect_status "$tag" $CLI_SUCCESS
expect_log "$tag" "inherits_group"
done
echo "== --slice 1 --arrange 1 slices a project without plate metadata"
rewrite_3mf "$WORK/export/project.3mf" "$WORK/no_plates.3mf" no-plates
run_presets no_plates --slice 1 --arrange 1 "$WORK/no_plates.3mf"
expect_status no_plates $CLI_SUCCESS
expect_result no_plates $CLI_SUCCESS
expect_gcode no_plates
echo "== an assemble list with a valid filament id slices"
assemble_list "$WORK/assemble_valid.json" '[1]'
run_presets assemble_valid --slice 0 --load-assemble-list "$WORK/assemble_valid.json"
expect_status assemble_valid $CLI_SUCCESS
expect_gcode assemble_valid
echo "== an assemble list with an empty filament list or a negative filament id is rejected"
for filaments in '[]' '[-1]'; do
if [ "$filaments" = '[]' ]; then tag=assemble_empty; else tag=assemble_negative; fi
assemble_list "$WORK/$tag.json" "$filaments"
run_presets "$tag" --slice 0 --load-assemble-list "$WORK/$tag.json"
expect_status "$tag" $CLI_CONFIG_FILE_ERROR
expect_result "$tag" $CLI_CONFIG_FILE_ERROR
done
echo "== --assemble with no input model is rejected"
for action in "--slice 0" "--export-3mf out.3mf"; do
tag="assemble_no_input_${action%% *}"
tag="${tag//-/}"
# shellcheck disable=SC2086
run_presets "$tag" --assemble $action
expect_status "$tag" $CLI_INVALID_PARAMS
expect_result "$tag" $CLI_INVALID_PARAMS
expect_log "$tag" "--assemble"
done
if [ "$fails" -ne 0 ]; then
for log in "$WORK"/*/log; do
echo "--- $log"
tail -n 40 "$log"
done
exit 1
fi
echo "PASS"
+1
View File
@@ -9,6 +9,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
+266
View File
@@ -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);
}
+104
View File
@@ -536,6 +536,110 @@ SCENARIO("update_diff_values_to_child_config keeps a child's values on variants
}
}
SCENARIO("update_non_diff_values_to_base_config keeps a project's changed values on variants it does not list",
"[Config][Variant]") {
std::set<std::string> no_keys;
auto variants = [](std::initializer_list<std::string> names) { return new Slic3r::ConfigOptionStrings(names); };
GIVEN("A filament base with three variants") {
Slic3r::DynamicPrintConfig base;
base.set_key_value("filament_extruder_variant",
variants({"Direct Drive Standard", "Bowden Standard", "Direct Drive High Flow"}));
base.set_deserialize_strict("nozzle_temperature", "220,220,220");
WHEN("the project was saved when the base had only its first variant") {
Slic3r::DynamicPrintConfig project;
project.set_key_value("filament_extruder_variant", variants({"Direct Drive Standard"}));
project.set_deserialize_strict("nozzle_temperature", "199");
AND_WHEN("the project lists the value as changed") {
project.update_non_diff_values_to_base_config(base, project.keys(), {"nozzle_temperature"}, "", "filament_extruder_variant",
Slic3r::filament_options_with_variant, no_keys);
THEN("the project's value applies to every variant") {
REQUIRE(project.opt_serialize("nozzle_temperature") == "199,199,199");
}
}
AND_WHEN("the project does not list the value as changed") {
project.update_non_diff_values_to_base_config(base, project.keys(), {}, "", "filament_extruder_variant",
Slic3r::filament_options_with_variant, no_keys);
THEN("the base's values replace it") {
REQUIRE(project.opt_serialize("nozzle_temperature") == "220,220,220");
}
}
}
WHEN("the project lists every variant, in another order") {
Slic3r::DynamicPrintConfig project;
project.set_key_value("filament_extruder_variant",
variants({"Bowden Standard", "Direct Drive High Flow", "Direct Drive Standard"}));
project.set_deserialize_strict("nozzle_temperature", "190,205,199");
project.update_non_diff_values_to_base_config(base, project.keys(), {"nozzle_temperature"}, "", "filament_extruder_variant",
Slic3r::filament_options_with_variant, no_keys);
THEN("each variant keeps its own value") {
REQUIRE(project.opt_serialize("nozzle_temperature") == "199,190,205");
}
}
WHEN("the project lists no variants") {
Slic3r::DynamicPrintConfig project;
project.set_deserialize_strict("nozzle_temperature", "199");
project.update_non_diff_values_to_base_config(base, project.keys(), {"nozzle_temperature"}, "", "filament_extruder_variant",
Slic3r::filament_options_with_variant, no_keys);
THEN("the project's value applies to every variant") {
REQUIRE(project.opt_serialize("nozzle_temperature") == "199,199,199");
}
}
}
GIVEN("A two-extruder printer base with two variants per extruder") {
Slic3r::DynamicPrintConfig base;
base.set_key_value("printer_extruder_variant",
variants({"Direct Drive Standard", "Direct Drive High Flow", "Direct Drive Standard", "Direct Drive High Flow"}));
base.set_key_value("printer_extruder_id", new Slic3r::ConfigOptionInts({1, 1, 2, 2}));
base.set_deserialize_strict("retraction_length", "0.8,0.8,0.8,0.8");
WHEN("the project lists only the Standard variant of each extruder") {
Slic3r::DynamicPrintConfig project;
project.set_key_value("printer_extruder_variant", variants({"Direct Drive Standard", "Direct Drive Standard"}));
project.set_key_value("printer_extruder_id", new Slic3r::ConfigOptionInts({1, 2}));
project.set_deserialize_strict("retraction_length", "1.1,2.2");
project.update_non_diff_values_to_base_config(base, project.keys(), {"retraction_length"}, "printer_extruder_id", "printer_extruder_variant",
Slic3r::printer_options_with_variant_1,
Slic3r::printer_options_with_variant_2);
THEN("each extruder's High Flow variant takes that extruder's value") {
REQUIRE(project.opt_serialize("retraction_length") == "1.1,1.1,2.2,2.2");
}
}
WHEN("the project lists only the Standard variant of each extruder, and the variant lists as changed") {
Slic3r::DynamicPrintConfig project;
project.set_key_value("printer_extruder_variant", variants({"Direct Drive Standard", "Direct Drive Standard"}));
project.set_key_value("printer_extruder_id", new Slic3r::ConfigOptionInts({1, 2}));
project.set_deserialize_strict("machine_max_speed_x", "300,100,400,150");
base.set_deserialize_strict("machine_max_speed_x", "500,200,500,200,500,200,500,200");
project.update_non_diff_values_to_base_config(base, project.keys(),
{"machine_max_speed_x", "printer_extruder_id", "printer_extruder_variant"},
"printer_extruder_id", "printer_extruder_variant",
Slic3r::printer_options_with_variant_1,
Slic3r::printer_options_with_variant_2);
THEN("the variant lists are the base's") {
REQUIRE(project.opt_serialize("printer_extruder_variant") == base.opt_serialize("printer_extruder_variant"));
REQUIRE(project.opt_serialize("printer_extruder_id") == "1,1,2,2");
}
THEN("each extruder's High Flow variant takes that extruder's pair of limits") {
REQUIRE(project.opt_serialize("machine_max_speed_x") == "300,100,300,100,400,150,400,150");
}
}
WHEN("the project lists no variants") {
Slic3r::DynamicPrintConfig project;
project.set_deserialize_strict("retraction_length", "1.1");
project.update_non_diff_values_to_base_config(base, project.keys(), {"retraction_length"}, "printer_extruder_id", "printer_extruder_variant",
Slic3r::printer_options_with_variant_1,
Slic3r::printer_options_with_variant_2);
THEN("only the first extruder's variants take the project's value") {
REQUIRE(project.opt_serialize("retraction_length") == "1.1,1.1,0.8,0.8");
}
}
}
}
// SCENARIO("DynamicPrintConfig JSON serialization", "[Config]") {
// WHEN("DynamicPrintConfig is serialized and deserialized") {
// auto now = std::chrono::high_resolution_clock::now();
+128 -2
View File
@@ -5816,6 +5816,131 @@ TEST_CASE("A system preset no vendor lists is not resolved", "[Preset][Bundle]")
namespace {
// Writes each vendor's preset cache into dir, then deletes its profile JSONs: what a release build installs.
void reduce_vendors_to_caches(const fs::path &dir, const std::vector<std::string> &vendor_ids)
{
const std::string lib(PresetBundle::ORCA_FILAMENT_LIBRARY);
PresetBundle library;
if (fs::exists(dir / (lib + ".json"))) {
library.set_generate_vendor_caches(true);
library.load_vendor_configs_from_json(dir.string(), lib, PresetBundle::LoadSystem,
ForwardCompatibilitySubstitutionRule::EnableSilent);
}
for (const std::string &vendor_id : vendor_ids) {
if (vendor_id == lib)
continue;
PresetBundle writer;
writer.set_generate_vendor_caches(true);
writer.load_vendor_configs_from_json(dir.string(), vendor_id, PresetBundle::LoadSystem,
ForwardCompatibilitySubstitutionRule::EnableSilent, &library);
}
for (const std::string &vendor_id : vendor_ids) {
REQUIRE(fs::exists(dir / (vendor_id + ".opc")));
fs::remove(dir / (vendor_id + ".json"));
fs::remove_all(dir / vendor_id);
}
}
// The filament library with one abstract base filament, and an "Acme" vendor whose one filament inherits it.
void write_library_and_acme_filament(const fs::path &root)
{
const std::string lib(PresetBundle::ORCA_FILAMENT_LIBRARY);
fs::create_directories(root / lib / "filament");
std::ofstream((root / (lib + ".json")).string())
<< R"({"version":"1.0.0","name":")" << lib << R"(",)"
<< R"("filament_list":[{"name":"Generic PLA","sub_path":"filament/generic_pla.json"}]})";
std::ofstream((root / lib / "filament" / "generic_pla.json").string())
<< R"({"type":"filament","name":"Generic PLA","from":"system","instantiation":"false","filament_id":"GFL99","filament_cost":"27"})";
fs::create_directories(root / "Acme" / "filament");
std::ofstream((root / "Acme.json").string())
<< R"({"version":"1.0.0","name":"Acme","filament_list":[{"name":"Acme PLA","sub_path":"filament/pla.json"}]})";
std::ofstream((root / "Acme" / "filament" / "pla.json").string())
<< R"({"type":"filament","name":"Acme PLA","from":"system","instantiation":"true","inherits":"Generic PLA"})";
}
} // namespace
TEST_CASE("A read-only load resolves a user preset against vendors installed as their cache alone", "[Preset][Bundle][Regression]")
{
ScopedTemporaryDir temp_dir;
const fs::path data = temp_dir.path() / "data";
const fs::path system = data / PRESET_SYSTEM_DIR;
ScopedDataDir scoped_data(data);
ScopedResourcesDir scoped_resources(temp_dir.path() / "resources");
write_acme_printer_vendor(system, 33.);
reduce_vendors_to_caches(system, {"Acme"});
fs::create_directories(data / PRESET_USER_DIR / DEFAULT_USER_FOLDER_NAME / PRESET_PRINTER_NAME);
std::ofstream((data / PRESET_USER_DIR / DEFAULT_USER_FOLDER_NAME / PRESET_PRINTER_NAME / "My Acme.json").string())
<< R"({"type":"machine","name":"My Acme","from":"User","version":"2.3.0.0","inherits":"Acme Printer","printable_height":"123"})";
AppConfig app_config;
PresetBundle bundle;
std::string errors;
bundle.load_presets(app_config, ForwardCompatibilitySubstitutionRule::EnableSilent, PresetBundle::PresetPreferences(),
&errors, true);
CHECK(errors.empty());
const Preset *preset = bundle.printers.find_preset("My Acme");
REQUIRE(preset != nullptr);
CHECK_THAT(preset->config.opt_float("printable_height"), Catch::Matchers::WithinAbs(123., 1e-6));
CHECK_THAT(preset->config.opt_float("extruder_clearance_dist_to_rod"), Catch::Matchers::WithinAbs(33., 1e-6));
}
TEST_CASE("A read-only load writes no preset cache", "[Preset][Bundle][Regression]")
{
ScopedTemporaryDir temp_dir;
const fs::path system = temp_dir.path() / "data" / PRESET_SYSTEM_DIR;
ScopedDataDir scoped_data(temp_dir.path() / "data");
ScopedResourcesDir scoped_resources(temp_dir.path() / "resources");
write_acme_printer_vendor(system, 33.);
AppConfig app_config;
PresetBundle bundle;
std::string errors;
bundle.load_presets(app_config, ForwardCompatibilitySubstitutionRule::EnableSilent, PresetBundle::PresetPreferences(),
&errors, true);
CHECK(errors.empty());
CHECK(bundle.printers.find_preset("Acme Printer") != nullptr);
CHECK_FALSE(fs::exists(system / "Acme.opc"));
}
TEST_CASE("A vendor updated over the air resolves against the library installed as its cache alone", "[Preset][Bundle][Regression]")
{
ScopedTemporaryDir temp_dir;
const fs::path system = temp_dir.path() / "data" / PRESET_SYSTEM_DIR;
ScopedDataDir scoped_data(temp_dir.path() / "data");
ScopedResourcesDir scoped_resources(temp_dir.path() / "resources");
// System presets are found by name through the bundled profiles.
write_library_and_acme_filament(temp_dir.path() / "resources" / PRESET_PROFILES_DIR);
// The release install, then an update that brings Acme back as JSONs while the library stays a cache.
write_library_and_acme_filament(system);
reduce_vendors_to_caches(system, {PresetBundle::ORCA_FILAMENT_LIBRARY, "Acme"});
write_library_and_acme_filament(temp_dir.path() / "update");
fs::copy_file(temp_dir.path() / "update" / "Acme.json", system / "Acme.json");
fs::create_directories(system / "Acme" / "filament");
fs::copy_file(temp_dir.path() / "update" / "Acme" / "filament" / "pla.json", system / "Acme" / "filament" / "pla.json");
SECTION("by name") {
PresetBundle bundle;
DynamicPrintConfig config;
std::string error;
REQUIRE(bundle.resolve_system_preset(config, Preset::TYPE_FILAMENT, "Acme PLA",
ForwardCompatibilitySubstitutionRule::EnableSilent, error));
CHECK_THAT(config.opt<ConfigOptionFloats>("filament_cost")->values.front(), Catch::Matchers::WithinAbs(27., 1e-6));
}
SECTION("by its source file") {
PresetBundle bundle;
DynamicPrintConfig config;
config.option<ConfigOptionString>(BBL_JSON_KEY_INHERITS, true)->value = "Generic PLA";
std::string error;
REQUIRE(bundle.resolve_preset_config(config, Preset::TYPE_FILAMENT, (system / "Acme" / "filament" / "pla.json").string(),
ForwardCompatibilitySubstitutionRule::EnableSilent, error));
CHECK_THAT(config.opt<ConfigOptionFloats>("filament_cost")->values.front(), Catch::Matchers::WithinAbs(27., 1e-6));
}
}
namespace {
// A default preset config for type, built the way PresetBundle builds its default presets.
DynamicPrintConfig external_default_config(Preset::Type type)
{
@@ -5956,8 +6081,9 @@ TEST_CASE("A per-variant project value maps onto its base preset's variant layou
base_finder(&base, calls));
CHECK(config.option<ConfigOptionStrings>("print_extruder_variant")->values ==
std::vector<std::string>{"Direct Drive Standard", "Direct Drive High Flow"});
// The listed key keeps the project's Standard value and takes High Flow from the base.
check_double_vector(config.option<ConfigOptionFloats>("outer_wall_speed")->values, {100., 300.});
// The listed key keeps the project's Standard value, and High Flow, which the project does not
// list, takes it too, as a user preset's value does.
check_double_vector(config.option<ConfigOptionFloats>("outer_wall_speed")->values, {100., 100.});
check_double_vector(config.option<ConfigOptionFloats>("inner_wall_speed")->values, {250., 350.});
}