Compare commits

..
Author SHA1 Message Date
Hanif Koh 245a94ab94 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 00:38:39 +08:00
Hanif Koh 129e3526b8 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.
2026-10-04 00:38:39 +08:00
Hanif Koh 64d78354e7 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().
2026-10-04 00:38:39 +08:00
HanifKoh 52ff374870 Refresh a CLI Project's Filament Settings From Their System Presets (#16038)
* Refresh a CLI Project's Filament Settings From Their System Presets

The CLI loads a project's printer and process settings as the GUI does,
taking every key the project does not list as changed from the current
system preset, but it kept the stored filament values. A project saved
before a profile update then sliced with old filament values on the
command line and with the current ones in the GUI.

Every project filament that no loaded filament replaces is now resolved
by its system preset name and fed to the filament merge the up-to-date
path already uses, which keeps the keys listed in
different_settings_to_system and maps per-variant values onto the
preset's variants. This covers a plain run, --uptodate without
--uptodate-filaments, and the slots --load-filaments leaves empty. The
merge tells refreshed entries from loaded ones per entry instead of by
the global loaded-filament count, and the entries are kept in slot
order. A project filament saved under a name the presets have since
split per nozzle is resolved through the name conversion the GUI uses,
which PresetBundle now exposes.

* Check the Project Refresh Test's Result Directly

Shellcheck SC2181: test the checker's exit status in the if instead of
reading $? afterwards.
2026-10-04 00:36:10 +08:00
Kris AustinandRodrigo Faselli 6e0f04815b perf: speed up G-code export by up to 7% via post-processing fixes (#16031)
Co-authored-by: Rodrigo Faselli <162915171+RF47@users.noreply.github.com>
2026-10-03 13:29:55 -03:00
Kris AustinandRodrigo Faselli c67b54b39d perf: speed up G-code export by 4-17% via parallel overhang precompute (#16050)
Co-authored-by: Rodrigo Faselli <162915171+RF47@users.noreply.github.com>
2026-10-03 11:50:36 -03:00
48 changed files with 8720 additions and 488 deletions
+4
View File
@@ -74,8 +74,11 @@ src/slic3r/GUI/Gizmos/GizmoObjectManipulation.cpp
src/slic3r/GUI/Gizmos/GLGizmoCut.cpp
src/slic3r/GUI/Gizmos/GLGizmoCut.hpp
src/slic3r/GUI/Gizmos/GLGizmoSimplify.cpp
src/slic3r/GUI/Gizmos/GLGizmoFaceDetector.cpp
src/slic3r/GUI/Gizmos/GLGizmoSeam.cpp
src/slic3r/GUI/Gizmos/GLGizmoSeam.hpp
src/slic3r/GUI/Gizmos/GLGizmoText.cpp
src/slic3r/GUI/Gizmos/GLGizmoText.hpp
src/slic3r/GUI/Gizmos/GLGizmoEmboss.cpp
src/slic3r/GUI/Gizmos/GLGizmoSVG.cpp
src/slic3r/GUI/Gizmos/GLGizmoMeasure.cpp
@@ -255,6 +258,7 @@ src/slic3r/Utils/MKS.cpp
src/slic3r/Utils/Moonraker.cpp
src/slic3r/Utils/OctoPrint.cpp
src/slic3r/Utils/Repetier.cpp
src/slic3r/Utils/ProfileDescription.hpp
src/slic3r/GUI/SendMultiMachinePage.cpp
src/slic3r/GUI/MultiMachinePage.cpp
src/slic3r/GUI/MultiMachineManagerPage.cpp
+61 -169
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);
@@ -2823,6 +2677,37 @@ int CLI::run(int argc, char **argv)
fetch_compatible_values = true;
}
// Refresh every project filament no loaded filament replaces from its current system preset, as the GUI
// does when it loads the project; the filament merge below keeps the keys the project lists as changed.
// Entries stay in slot order, which the merge's variant bookkeeping relies on.
std::vector<bool> load_filaments_refresh(load_filaments_config.size(), false);
if (is_bbl_3mf && new_printer_name.empty()) {
const ConfigOptionStrings *project_filament_ids = m_print_config.option<ConfigOptionStrings>("filament_ids");
for (size_t index = 0; index < current_filaments_system_name.size(); index++) {
const int slot = static_cast<int>(index) + 1;
if (std::find(load_filaments_index.begin(), load_filaments_index.end(), slot) != load_filaments_index.end())
continue;
std::string system_name = current_filaments_system_name[index];
if (system_name.empty())
continue;
PresetBundle::convert_filament_preset_name(current_printer_name, system_name);
DynamicPrintConfig config;
std::string error;
if (!ensure_system_preset_resolver().resolve_system_preset(config, Preset::TYPE_FILAMENT, system_name, config_substitution_rule, error)) {
BOOST_LOG_TRIVIAL(warning) << boost::format("CLI: system filament preset '%1%' not resolved (%2%); filament %3% keeps its values") % system_name % error % slot;
continue;
}
const size_t at = std::upper_bound(load_filaments_index.begin(), load_filaments_index.end(), slot) - load_filaments_index.begin();
load_filaments_id.insert(load_filaments_id.begin() + at,
project_filament_ids != nullptr && index < project_filament_ids->size() ? project_filament_ids->values[index] : std::string());
load_filaments_name.insert(load_filaments_name.begin() + at, system_name);
load_filaments_config.insert(load_filaments_config.begin() + at, std::move(config));
load_filaments_index.insert(load_filaments_index.begin() + at, slot);
load_filaments_inherit.insert(load_filaments_inherit.begin() + at, system_name);
load_filaments_refresh.insert(load_filaments_refresh.begin() + at, true);
}
}
//fetch upward_compatible_machine
if (fetch_upward_values) {
if (!current_printer_system_name.empty()) {
@@ -3513,7 +3398,7 @@ int CLI::run(int argc, char **argv)
}
//set the filament settings into print config
if ((load_filament_count > 0) || (up_config_to_date))
if ((load_filament_count > 0) || (up_config_to_date) || !load_filaments_config.empty())
{
//std::vector<int> filament_variant_count(filament_count, 1);
std::vector<int> old_start_indice(filament_count, 0);
@@ -3572,6 +3457,8 @@ int CLI::run(int argc, char **argv)
for (int index = 0; index < load_filaments_config.size(); index++) {
DynamicPrintConfig& config = load_filaments_config[index];
int filament_index = load_filaments_index[index];
// A filament given with --load-filaments replaces the slot; a refreshed one keeps the project's changed keys.
const bool loaded = load_filament_count > 0 && !load_filaments_refresh[index];
std::vector<std::string> different_keys;
//ORCA: diff before load_default_gcodes_to_config, the way the process and machine
@@ -3581,12 +3468,12 @@ int CLI::run(int argc, char **argv)
// compared" to "compared as empty against the parent" and land in the column
// as an override the user never made.
std::string filament_different_settings;
if (load_filament_count > 0)
if (loaded)
filament_different_settings = cli_different_settings(config, load_filaments_inherit[index], Preset::TYPE_FILAMENT);
load_default_gcodes_to_config(config, Preset::TYPE_FILAMENT);
if (load_filament_count > 0) {
if (loaded) {
ConfigOptionStrings *opt_filament_settings = static_cast<ConfigOptionStrings *> (m_print_config.option("filament_settings_id", true));
std::string& filament_name = load_filaments_name[index];
ConfigOptionString* filament_name_setting = new ConfigOptionString(filament_name);
@@ -3658,7 +3545,7 @@ int CLI::run(int argc, char **argv)
flush_and_exit(CLI_CONFIG_FILE_ERROR);
}
if ((load_filament_count == 0) && !different_keys_set.empty())
if (!loaded && !different_keys_set.empty())
{
std::set<std::string>::iterator iter = different_keys_set.find(opt_key);
if ( iter != different_keys_set.end()) {
@@ -4848,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");
@@ -5788,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};
+1 -1
View File
@@ -358,7 +358,7 @@ namespace AABBTreeLines {
return dist;
}
std::vector<size_t> all_lines_in_radius(const Vec<LineType::Dim, Scalar>& point, Floating radius)
std::vector<size_t> all_lines_in_radius(const Vec<LineType::Dim, Scalar>& point, Floating radius) const
{
return AABBTreeLines::all_lines_in_radius(this->lines, this->tree, point.template cast<Floating>(), radius * radius);
}
+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
+116
View File
@@ -0,0 +1,116 @@
#ifndef VOXELIZECSGMESH_HPP
#define VOXELIZECSGMESH_HPP
#include <functional>
#include <stack>
#include "CSGMesh.hpp"
#include "libslic3r/OpenVDBUtils.hpp"
#include "libslic3r/Execution/ExecutionTBB.hpp"
namespace Slic3r { namespace csg {
using VoxelizeParams = MeshToGridParams;
// This method can be overriden when a specific CSGPart type supports caching
// of the voxel grid
template<class CSGPartT>
VoxelGridPtr get_voxelgrid(const CSGPartT &csgpart, VoxelizeParams params)
{
const indexed_triangle_set *its = csg::get_mesh(csgpart);
VoxelGridPtr ret;
params.trafo(params.trafo() * csg::get_transform(csgpart));
if (its)
ret = mesh_to_grid(*its, params);
return ret;
}
namespace detail {
inline void perform_csg(CSGType op, VoxelGridPtr &dst, VoxelGridPtr &src)
{
if (!dst || !src)
return;
switch (op) {
case CSGType::Union:
if (is_grid_empty(*dst) && !is_grid_empty(*src))
dst = clone(*src);
else
grid_union(*dst, *src);
break;
case CSGType::Difference:
grid_difference(*dst, *src);
break;
case CSGType::Intersection:
grid_intersection(*dst, *src);
break;
}
}
} // namespace detail
template<class It>
VoxelGridPtr voxelize_csgmesh(const Range<It> &csgrange,
const VoxelizeParams &params = {})
{
using namespace detail;
VoxelGridPtr ret;
std::vector<VoxelGridPtr> grids (csgrange.size());
execution::for_each(ex_tbb, size_t(0), csgrange.size(), [&](size_t csgidx) {
if (params.statusfn() && params.statusfn()(-1))
return;
auto it = csgrange.begin();
std::advance(it, csgidx);
auto &csgpart = *it;
grids[csgidx] = get_voxelgrid(csgpart, params);
}, execution::max_concurrency(ex_tbb));
size_t csgidx = 0;
struct Frame { CSGType op = CSGType::Union; VoxelGridPtr grid; };
std::stack opstack{std::vector<Frame>{}};
opstack.push({CSGType::Union, mesh_to_grid({}, params)});
for (auto &csgpart : csgrange) {
if (params.statusfn() && params.statusfn()(-1))
break;
auto &partgrid = grids[csgidx++];
auto op = get_operation(csgpart);
if (get_stack_operation(csgpart) == CSGStackOp::Push) {
opstack.push({op, mesh_to_grid({}, params)});
op = CSGType::Union;
}
Frame *top = &opstack.top();
perform_csg(get_operation(csgpart), top->grid, partgrid);
if (get_stack_operation(csgpart) == CSGStackOp::Pop) {
VoxelGridPtr popgrid = std::move(top->grid);
auto popop = opstack.top().op;
opstack.pop();
VoxelGridPtr &grid = opstack.top().grid;
perform_csg(popop, grid, popgrid);
}
}
ret = std::move(opstack.top().grid);
return ret;
}
}} // namespace Slic3r::csg
#endif // VOXELIZECSGMESH_HPP
-13
View File
@@ -871,19 +871,6 @@ ConfigSubstitutions ConfigBase::load_from_json(const std::string &file, ForwardC
return std::move(substitutions_ctxt.substitutions);
}
// Case-insensitive compare of a JSON key against a fixed ASCII one, without
// boost::iequals, whose std::locale() takes a lock the whole process shares in the
// MSVC runtime.
static bool ascii_iequals(const std::string &key, const char *literal)
{
auto lower = [](char c) { return (c >= 'A' && c <= 'Z') ? char(c - 'A' + 'a') : c; };
size_t i = 0;
for (; i < key.size() && literal[i] != '\0'; ++ i)
if (lower(key[i]) != lower(literal[i]))
return false;
return i == key.size() && literal[i] == '\0';
}
int ConfigBase::load_from_json(const std::string &file, ConfigSubstitutionContext& substitution_context, bool load_inherits_to_config, std::map<std::string, std::string>& key_values, std::string& reason)
{
json j;
@@ -175,6 +175,23 @@ private:
bool is_reverse{true};
};
// Calls `f` for every ExtrusionPath in `entity`, descending into collections, loops and multi-paths.
template<typename F> void for_each_extrusion_path(const ExtrusionEntity &entity, F &&f)
{
if (auto *collection = dynamic_cast<const ExtrusionEntityCollection *>(&entity)) {
for (const ExtrusionEntity *child : collection->entities)
for_each_extrusion_path(*child, f);
} else if (auto *loop = dynamic_cast<const ExtrusionLoop *>(&entity)) {
for (const ExtrusionPath &path : loop->paths)
f(path);
} else if (auto *multi_path = dynamic_cast<const ExtrusionMultiPath *>(&entity)) {
for (const ExtrusionPath &path : multi_path->paths)
f(path);
} else if (auto *path = dynamic_cast<const ExtrusionPath *>(&entity)) {
f(*path);
}
}
} // namespace Slic3r
#endif
+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_ */
+123 -74
View File
@@ -806,13 +806,13 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
GCodeReader parser;
parser.parse_buffer(gcode, [&changes](GCodeReader &parser, const GCodeReader::GCodeLine &line) {
const std::string_view cmd = line.cmd();
if (boost::iequals(cmd, "M204") || boost::iequals(cmd, "M201") ||
boost::iequals(cmd, "M202"))
if (ascii_iequals(cmd, "M204") || ascii_iequals(cmd, "M201") ||
ascii_iequals(cmd, "M202"))
changes.acceleration = true;
else if ((boost::iequals(cmd, "M205") || boost::iequals(cmd, "M207") || boost::iequals(cmd, "M566")) &&
else if ((ascii_iequals(cmd, "M205") || ascii_iequals(cmd, "M207") || ascii_iequals(cmd, "M566")) &&
custom_gcode_line_has_xy_parameter(line.raw()))
changes.jerk = true;
else if (boost::iequals(cmd, "SET_VELOCITY_LIMIT")) {
else if (ascii_iequals(cmd, "SET_VELOCITY_LIMIT")) {
changes.acceleration |= boost::icontains(line.raw(), "ACCEL=");
changes.jerk |= boost::icontains(line.raw(), "SQUARE_CORNER_VELOCITY=");
}
@@ -4404,6 +4404,75 @@ size_t GCode::get_nozzle_config_index(int filament_id) const
return get_extruder_id(filament_id);
}
namespace {
struct PrecomputedLayer
{
size_t index{size_t(-1)}; // size_t(-1) for the empty layer after the last
std::vector<PrecomputedOverhangLayer> overhang_layers;
};
} // namespace
template<typename BoolsOption> static bool any_enabled(const BoolsOption &option)
{
return std::any_of(option.values.begin(), option.values.end(), [](unsigned char enabled) { return enabled != 0; });
}
// Whether process_layer() prepares the overhang estimator for `layer`.
template<typename OverhangSpeed>
static bool prepares_overhang_estimator(const Layer &layer, bool overhang_fan, OverhangSpeed overhang_speed)
{
const LayerRegionPtrs &regions = layer.regions();
return std::any_of(regions.begin(), regions.end(), [overhang_fan, &overhang_speed](const LayerRegion *region) {
return region->has_extrusions() && (overhang_fan || overhang_speed(*region));
});
}
std::vector<PrecomputedOverhangLayer> precompute_overhang_layers(const std::vector<GCode::LayerToPrint> &layers, bool overhang_fan)
{
// Any filament may print the layer, so a region's overhang speed counts if it is enabled for any.
auto overhang_speed = [](const LayerRegion &region) { return any_enabled(region.region().config().enable_overhang_speed); };
std::vector<PrecomputedOverhangLayer> out;
for (const GCode::LayerToPrint &layer : layers)
if (layer.object_layer != nullptr && layer.object_layer->lower_layer != nullptr &&
prepares_overhang_estimator(*layer.object_layer, overhang_fan, overhang_speed)) {
const LayerRegionPtrs &regions = layer.object_layer->regions();
const bool curled_lines = std::any_of(regions.begin(), regions.end(), [](const LayerRegion *region) {
return any_enabled(region->region().config().slowdown_for_curled_perimeters);
});
out.push_back(precompute_overhang_layer(layer.original_object, *layer.object_layer, curled_lines));
}
return out;
}
// Hands out the index of each layer to process_layers(), then computes the layers' overhang data in parallel.
template<typename LayersAt>
static auto precomputed_layers_source(size_t &next_index, size_t layer_count, bool nop_layer, bool overhang_fan, LayersAt layers_at)
{
return tbb::make_filter<void, PrecomputedLayer>(slic3r_tbb_filtermode::serial_in_order,
[&next_index, layer_count, nop_layer](tbb::flow_control &fc) -> PrecomputedLayer {
if (next_index < layer_count)
return {next_index++};
// The pressure equalizer returns one layer back, so it gets an empty layer after the last.
if (next_index == layer_count + (nop_layer ? 1 : 0))
fc.stop();
else
++next_index;
return {};
}) &
tbb::make_filter<PrecomputedLayer, PrecomputedLayer>(slic3r_tbb_filtermode::parallel,
[layers_at, overhang_fan](PrecomputedLayer layer) -> PrecomputedLayer {
if (layer.index != size_t(-1))
layer.overhang_layers = precompute_overhang_layers(layers_at(layer.index), overhang_fan);
return layer;
});
}
// Whether the overhang fan can switch on for any filament.
static bool overhang_fan_enabled(const PrintConfig &config, bool cooling_markers)
{
return cooling_markers && any_enabled(config.enable_overhang_bridge_fan);
}
// Process all layers of all objects (non-sequential mode) with a parallel pipeline:
// Generate G-code, run the filters (vase mode, cooling buffer), run the G-code analyser
// and export G-code into file.
@@ -4416,29 +4485,23 @@ void GCode::process_layers(
{
// The pipeline is variable: The vase mode filter is optional.
size_t layer_to_print_idx = 0;
const auto generator = tbb::make_filter<void, LayerResult>(slic3r_tbb_filtermode::serial_in_order,
[this, &print, &tool_ordering, &print_object_instances_ordering, &layers_to_print, &layer_to_print_idx](tbb::flow_control& fc) -> LayerResult {
if (layer_to_print_idx >= layers_to_print.size()) {
if (layer_to_print_idx == layers_to_print.size() + (m_pressure_equalizer ? 1 : 0)) {
fc.stop();
return {};
} else {
// Pressure equalizer need insert empty input. Because it returns one layer back.
// Insert NOP (no operation) layer;
++layer_to_print_idx;
return LayerResult::make_nop_layer_result();
}
} else {
const std::pair<coordf_t, std::vector<LayerToPrint>>& layer = layers_to_print[layer_to_print_idx++];
const LayerTools& layer_tools = tool_ordering.tools_for_layer(layer.first);
print.set_status(80, Slic3r::format(_(L("Generating G-code: layer %1%")), std::to_string(layer_to_print_idx)));
if (m_wipe_tower && layer_tools.has_wipe_tower)
m_wipe_tower->next_layer();
//BBS
check_placeholder_parser_failed();
print.throw_if_canceled();
return this->process_layer(print, layer.second, layer_tools, &layer == &layers_to_print.back(), &print_object_instances_ordering, tool_ordering.get_most_used_extruder(), size_t(-1));
}
const auto source = precomputed_layers_source(layer_to_print_idx, layers_to_print.size(), m_pressure_equalizer != nullptr,
overhang_fan_enabled(print.config(), m_enable_cooling_markers),
[&layers_to_print](size_t index) -> const std::vector<LayerToPrint> & { return layers_to_print[index].second; });
const auto generator = tbb::make_filter<PrecomputedLayer, LayerResult>(slic3r_tbb_filtermode::serial_in_order,
[this, &print, &tool_ordering, &print_object_instances_ordering, &layers_to_print](PrecomputedLayer precomputed) -> LayerResult {
if (precomputed.index == size_t(-1))
return LayerResult::make_nop_layer_result();
const std::pair<coordf_t, std::vector<LayerToPrint>>& layer = layers_to_print[precomputed.index];
const LayerTools& layer_tools = tool_ordering.tools_for_layer(layer.first);
print.set_status(80, Slic3r::format(_(L("Generating G-code: layer %1%")), std::to_string(precomputed.index + 1)));
if (m_wipe_tower && layer_tools.has_wipe_tower)
m_wipe_tower->next_layer();
//BBS
check_placeholder_parser_failed();
print.throw_if_canceled();
m_extrusion_quality_estimator.set_precomputed_layers(std::move(precomputed.overhang_layers));
return this->process_layer(print, layer.second, layer_tools, &layer == &layers_to_print.back(), &print_object_instances_ordering, tool_ordering.get_most_used_extruder(), size_t(-1));
});
if (m_spiral_vase) {
float nozzle_diameter = EXTRUDER_CONFIG(nozzle_diameter);
@@ -4496,13 +4559,15 @@ void GCode::process_layers(
// The pipeline elements are joined using const references, thus no copying is performed.
if (m_spiral_vase && m_pressure_equalizer)
tbb::parallel_pipeline(12, generator & spiral_mode & pressure_equalizer & cooling & fan_mover & output);
tbb::parallel_pipeline(12, source & generator & spiral_mode & pressure_equalizer & cooling & fan_mover & output);
else if (m_spiral_vase)
tbb::parallel_pipeline(12, generator & spiral_mode & cooling & fan_mover & output);
tbb::parallel_pipeline(12, source & generator & spiral_mode & cooling & fan_mover & output);
else if (m_pressure_equalizer)
tbb::parallel_pipeline(12, generator & pressure_equalizer & cooling & fan_mover & pa_processor_filter & output);
tbb::parallel_pipeline(12, source & generator & pressure_equalizer & cooling & fan_mover & pa_processor_filter & output);
else
tbb::parallel_pipeline(12, generator & cooling & fan_mover & pa_processor_filter & output);
tbb::parallel_pipeline(12, source & generator & cooling & fan_mover & pa_processor_filter & output);
// The estimator's precomputed data points into this print's layers.
m_extrusion_quality_estimator.set_precomputed_layers({});
}
@@ -4520,26 +4585,20 @@ void GCode::process_layers(
{
// The pipeline is variable: The vase mode filter is optional.
size_t layer_to_print_idx = 0;
const auto generator = tbb::make_filter<void, LayerResult>(slic3r_tbb_filtermode::serial_in_order,
[this, &print, &tool_ordering, &layers_to_print, &layer_to_print_idx, single_object_idx, prime_extruder](tbb::flow_control& fc) -> LayerResult {
if (layer_to_print_idx >= layers_to_print.size()) {
if (layer_to_print_idx == layers_to_print.size() + (m_pressure_equalizer ? 1 : 0)) {
fc.stop();
return {};
} else {
// Pressure equalizer need insert empty input. Because it returns one layer back.
// Insert NOP (no operation) layer;
++layer_to_print_idx;
return LayerResult::make_nop_layer_result();
}
} else {
LayerToPrint &layer = layers_to_print[layer_to_print_idx ++];
print.set_status(80, Slic3r::format(_(L("Generating G-code: layer %1%")), std::to_string(layer_to_print_idx)));
//BBS
check_placeholder_parser_failed();
print.throw_if_canceled();
return this->process_layer(print, { std::move(layer) }, tool_ordering.tools_for_layer(layer.print_z()), &layer == &layers_to_print.back(), nullptr, tool_ordering.get_most_used_extruder(), single_object_idx, prime_extruder);
}
const auto source = precomputed_layers_source(layer_to_print_idx, layers_to_print.size(), m_pressure_equalizer != nullptr,
overhang_fan_enabled(print.config(), m_enable_cooling_markers),
[&layers_to_print](size_t index) { return std::vector<LayerToPrint>{layers_to_print[index]}; });
const auto generator = tbb::make_filter<PrecomputedLayer, LayerResult>(slic3r_tbb_filtermode::serial_in_order,
[this, &print, &tool_ordering, &layers_to_print, single_object_idx, prime_extruder](PrecomputedLayer precomputed) -> LayerResult {
if (precomputed.index == size_t(-1))
return LayerResult::make_nop_layer_result();
LayerToPrint &layer = layers_to_print[precomputed.index];
print.set_status(80, Slic3r::format(_(L("Generating G-code: layer %1%")), std::to_string(precomputed.index + 1)));
//BBS
check_placeholder_parser_failed();
print.throw_if_canceled();
m_extrusion_quality_estimator.set_precomputed_layers(std::move(precomputed.overhang_layers));
return this->process_layer(print, { std::move(layer) }, tool_ordering.tools_for_layer(layer.print_z()), &layer == &layers_to_print.back(), nullptr, tool_ordering.get_most_used_extruder(), single_object_idx, prime_extruder);
});
if (m_spiral_vase) {
float nozzle_diameter = EXTRUDER_CONFIG(nozzle_diameter);
@@ -4594,13 +4653,15 @@ void GCode::process_layers(
// The pipeline elements are joined using const references, thus no copying is performed.
if (m_spiral_vase && m_pressure_equalizer)
tbb::parallel_pipeline(12, generator & spiral_mode & pressure_equalizer & cooling & fan_mover & output);
tbb::parallel_pipeline(12, source & generator & spiral_mode & pressure_equalizer & cooling & fan_mover & output);
else if (m_spiral_vase)
tbb::parallel_pipeline(12, generator & spiral_mode & cooling & fan_mover & output);
tbb::parallel_pipeline(12, source & generator & spiral_mode & cooling & fan_mover & output);
else if (m_pressure_equalizer)
tbb::parallel_pipeline(12, generator & pressure_equalizer & cooling & fan_mover & pa_processor_filter & output);
tbb::parallel_pipeline(12, source & generator & pressure_equalizer & cooling & fan_mover & pa_processor_filter & output);
else
tbb::parallel_pipeline(12, generator & cooling & fan_mover & pa_processor_filter & output);
tbb::parallel_pipeline(12, source & generator & cooling & fan_mover & pa_processor_filter & output);
// The estimator's precomputed data points into this print's layers.
m_extrusion_quality_estimator.set_precomputed_layers({});
}
std::string GCode::placeholder_parser_process(const std::string &name, const std::string &templ, unsigned int current_filament_id, const DynamicConfig *config_override)
@@ -5960,25 +6021,13 @@ LayerResult GCode::process_layer(
return next_extruder;
};
for (const auto &layer_to_print : layers) {
if (layer_to_print.object_layer) {
const auto& regions = layer_to_print.object_layer->regions();
const bool has_extrusions = std::any_of(regions.begin(), regions.end(), [](const LayerRegion* r) {
return r->has_extrusions();
});
const bool enable_overhang_speed = std::any_of(regions.begin(), regions.end(), [this](const LayerRegion* r) {
return r->has_extrusions() && r->region().config().enable_overhang_speed.get_at(get_nozzle_config_index(m_writer.filament()->id()));
});
const bool enable_overhang_fan = m_enable_cooling_markers && has_extrusions &&
std::any_of(m_config.enable_overhang_bridge_fan.values.begin(),
m_config.enable_overhang_bridge_fan.values.end(),
[](unsigned char value) { return value != 0; });
if (enable_overhang_speed || enable_overhang_fan) {
m_extrusion_quality_estimator.prepare_for_new_layer(layer_to_print.original_object,
layer_to_print.object_layer);
}
}
}
const bool overhang_fan = overhang_fan_enabled(m_config, m_enable_cooling_markers);
auto overhang_speed = [this](const LayerRegion &region) {
return bool(region.region().config().enable_overhang_speed.get_at(get_nozzle_config_index(m_writer.filament()->id())));
};
for (const auto &layer_to_print : layers)
if (layer_to_print.object_layer && prepares_overhang_estimator(*layer_to_print.object_layer, overhang_fan, overhang_speed))
m_extrusion_quality_estimator.prepare_for_new_layer(layer_to_print.original_object, layer_to_print.object_layer);
// Group extrusions by an extruder, then by an object, an island and a region.
std::map<unsigned int, std::vector<ObjectByExtruder>> by_extruder;
+4
View File
@@ -864,6 +864,10 @@ private:
std::vector<const PrintInstance*> sort_object_instances_by_model_order(const Print& print, bool init_order = false);
// The overhang data ExtrusionQualityEstimator needs for the object layers in `layers`, computed ahead of the generator;
// `overhang_fan` says whether the overhang fan can switch on for any filament.
std::vector<PrecomputedOverhangLayer> precompute_overhang_layers(const std::vector<GCode::LayerToPrint> &layers, bool overhang_fan);
}
#endif
@@ -41,6 +41,9 @@ AdaptivePAProcessor::AdaptivePAProcessor(GCode &gcodegen)
m_pa_change_pattern(R"(; PA_CHANGE:T(\d+) MM3MM:([0-9]*\.[0-9]+) ACCEL:(\d+) BR:(\d+) RC:(\d+) OV:(\d+))"),
m_g1_f_pattern(R"(G1 F([0-9]+))")
{
const size_t indices = std::max(m_config.adaptive_pressure_advance.size(), m_config.enable_pressure_advance.size());
for (size_t i = 0; i < indices && !m_enabled; ++i)
m_enabled = m_config.adaptive_pressure_advance.get_at(i) && m_config.enable_pressure_advance.get_at(i);
}
// Method to get the interpolator for a specific filament config index.
@@ -66,6 +69,12 @@ AdaptivePAInterpolator* AdaptivePAProcessor::getInterpolator(unsigned int config
* @return A string containing the processed G-code with adaptive pressure advance applied.
*/
std::string AdaptivePAProcessor::process_layer(std::string &&gcode) {
// Without PA_CHANGE tags the loop below would only terminate the layer's last line.
if (!m_enabled && gcode.find("; PA_CHANGE") == std::string::npos) {
if (!gcode.empty() && gcode.back() != '\n')
gcode += '\n';
return std::move(gcode);
}
std::istringstream stream(gcode);
std::string line;
std::ostringstream output;
@@ -78,6 +78,7 @@ private:
double m_next_feedrate; ///< First feed rate (speed) for the upcomming island.
double m_current_feedrate; ///< Current, latest feedrate.
int m_last_config_index; ///< Filament config index of the last PA_CHANGE tag.
bool m_enabled{false}; ///< Whether any filament config index has both PA and adaptive PA on, the only ones that emit PA_CHANGE tags.
std::regex m_pa_change_pattern; ///< Regular expression to detect PA_CHANGE pattern.
std::regex m_g1_f_pattern; ///< Regular expression to detect G1 F pattern.
+5 -5
View File
@@ -1,4 +1,5 @@
#include "../GCode.hpp"
#include "../LocalesUtils.hpp"
#include "libslic3r/Extruder.hpp"
#include "libslic3r/Point.hpp"
#include "libslic3r/libslic3r.h"
@@ -408,13 +409,13 @@ std::vector<PerExtruderAdjustments> CoolingBuffer::parse_layer_gcode(const std::
if (*c == 0 || *c == ';')
break;
assert(is_decimal_separator_point()); // for atof
//BBS: Parse the axis.
size_t axis = (*c >= 'X' && *c <= 'Z') ? (*c - 'X') :
(*c == 'E') ? 3 : (*c == 'F') ? 4 :
(*c == 'I') ? 5 : (*c == 'J') ? 6 : size_t(-1);
if (axis != size_t(-1)) {
new_pos[axis] = float(atof(++c));
++ c;
new_pos[axis] = float(atof_decimal_point(std::string_view(c, sline.data() + sline.size() - c)));
if (axis == 4) {
// Convert mm/min to mm/sec.
new_pos[4] /= 60.f;
@@ -550,10 +551,9 @@ std::vector<PerExtruderAdjustments> CoolingBuffer::parse_layer_gcode(const std::
line.type = CoolingLine::TYPE_G4;
size_t pos_S = sline.find('S', 3);
size_t pos_P = sline.find('P', 3);
assert(is_decimal_separator_point()); // for atof
line.time = line.time_max = float(
(pos_S > 0) ? atof(sline.c_str() + pos_S + 1) :
(pos_P > 0) ? atof(sline.c_str() + pos_P + 1) * 0.001 : 0.);
(pos_S > 0) ? atof_decimal_point(sline.c_str() + pos_S + 1) :
(pos_P > 0) ? atof_decimal_point(sline.c_str() + pos_P + 1) * 0.001 : 0.);
} else if (boost::starts_with(sline, ";_FORCE_RESUME_FAN_SPEED")) {
line.type = CoolingLine::TYPE_FORCE_RESUME_FAN;
}
+128 -22
View File
@@ -24,6 +24,7 @@
#include <cstddef>
#include <functional>
#include <limits>
#include <memory>
#include <numeric>
#include <unordered_map>
#include <utility>
@@ -38,7 +39,14 @@ template<int Dim> struct ExtendedPoint
float curvature;
};
template<bool SCALED_INPUT, bool ADD_INTERSECTIONS, bool PREV_LAYER_BOUNDARY_OFFSET, bool SIGNED_DISTANCE, typename POINTS, typename L>
// A KNOWN_DISTANCES functor that knows no distances, so every input point is queried.
struct NoKnownDistances
{
template<typename P> const double *operator()(const P &) const { return nullptr; }
};
template<bool SCALED_INPUT, bool ADD_INTERSECTIONS, bool PREV_LAYER_BOUNDARY_OFFSET, bool SIGNED_DISTANCE, bool CURVATURE = true,
typename POINTS, typename L, typename KNOWN_DISTANCES = NoKnownDistances>
std::vector<ExtendedPoint<L::Dim>> estimate_points_properties(const POINTS& input_points,
const AABBTreeLines::LinesDistancer<L>& unscaled_prev_layer,
float flow_width,
@@ -49,7 +57,9 @@ std::vector<ExtendedPoint<L::Dim>> estimate_points_properties(const POINTS&
const std::function<float(float)>& distance_to_speed = {},
// Overlap (1 - distance / flow_width) at or below which the overhang
// fan switches on; negative when the fan does not depend on overlap.
float fan_overlap_threshold = -1.0f)
float fan_overlap_threshold = -1.0f,
// Returns an input point's signed distance if already known, else nullptr.
const KNOWN_DISTANCES& known_distance = KNOWN_DISTANCES{})
{
bool looped = input_points.front() == input_points.back();
std::function<size_t(size_t,size_t)> get_prev_index = [](size_t idx, size_t count) {
@@ -94,21 +104,26 @@ std::vector<ExtendedPoint<L::Dim>> estimate_points_properties(const POINTS&
float boundary_offset = PREV_LAYER_BOUNDARY_OFFSET ? 0.5 * flow_width : 0.0f;
auto maybe_unscale = [](const P& p) -> Vec { return SCALED_INPUT ? unscaled(p) : p.template cast<double>(); };
using Distance = typename AABBTreeLines::LinesDistancer<L>::Floating;
auto input_distance = [&unscaled_prev_layer, &known_distance](const P &input, const Vec &position) -> Distance {
if (const double *known = known_distance(input))
return Distance(*known);
auto [distance, nearest_line, x] = unscaled_prev_layer.template distance_from_lines_extra<SIGNED_DISTANCE>(
position.template cast<AABBScalar>());
return distance;
};
std::vector<ExtendedPoint<L::Dim>> points;
points.reserve(input_points.size() * (ADD_INTERSECTIONS ? 1.5 : 1));
{
ExtendedPoint<L::Dim> start_point{maybe_unscale(input_points.front())};
auto [distance, nearest_line, x] = unscaled_prev_layer.template distance_from_lines_extra<SIGNED_DISTANCE>(
start_point.position.template cast<AABBScalar>());
start_point.distance = distance + boundary_offset;
start_point.distance = input_distance(input_points.front(), start_point.position) + boundary_offset;
points.push_back(start_point);
}
for (size_t i = 1; i < input_points.size(); i++) {
ExtendedPoint<L::Dim> next_point{maybe_unscale(input_points[i])};
auto [distance, nearest_line,
x] = unscaled_prev_layer.template distance_from_lines_extra<SIGNED_DISTANCE>(next_point.position.template cast<AABBScalar>());
next_point.distance = distance + boundary_offset;
next_point.distance = input_distance(input_points[i], next_point.position) + boundary_offset;
// Intersection handling
if (ADD_INTERSECTIONS &&
@@ -349,6 +364,9 @@ std::vector<ExtendedPoint<L::Dim>> estimate_points_properties(const POINTS&
points = std::move(new_points);
}
if constexpr (!CURVATURE)
return points;
// Curvature calculation
float accumulated_distance = 0;
std::vector<float> distances_for_curvature(points.size());
@@ -417,6 +435,49 @@ std::vector<ExtendedPoint<L::Dim>> estimate_points_properties(const POINTS&
return points;
}
struct Point3Hash
{
size_t operator()(const Vec3crd &pt) const noexcept { return size_t(((89 * 31 + int64_t(pt.x())) * 31 + pt.y()) * 31 + pt.z()); }
};
// The trees of the layer below an object layer, and the signed distances from the layer's perimeter and bridge
// vertices to that layer's outline, computed for ExtrusionQualityEstimator ahead of the G-code generator.
struct PrecomputedOverhangLayer
{
const PrintObject *object{nullptr};
const Layer *layer{nullptr};
std::shared_ptr<const AABBTreeLines::LinesDistancer<Linef3>> lower_boundaries;
std::shared_ptr<const AABBTreeLines::LinesDistancer<CurledLine>> lower_curled_lines;
std::unordered_map<Point3, double, Point3Hash> distances;
};
// `layer` must have a layer below it; leave out `curled_lines` only when no region of `layer` slows down for curled
// perimeters.
inline PrecomputedOverhangLayer precompute_overhang_layer(const PrintObject *object, const Layer &layer, bool curled_lines = true)
{
PrecomputedOverhangLayer out{object, &layer,
std::make_shared<const AABBTreeLines::LinesDistancer<Linef3>>(to_unscaled_linesf3(layer.lower_layer->lslices)),
curled_lines ? std::make_shared<const AABBTreeLines::LinesDistancer<CurledLine>>(layer.lower_layer->curled_lines) :
nullptr,
{}};
const AABBTreeLines::LinesDistancer<Linef3> &lower = *out.lower_boundaries;
auto add_path = [&out, &lower](const ExtrusionPath &path) {
if (!is_bridge(path.role()) && !is_perimeter(path.role()))
return;
for (const Point3 &point : path.polyline.points)
if (auto [it, inserted] = out.distances.try_emplace(point, 0.); inserted) {
const Eigen::Matrix<double, 3, 1, Eigen::DontAlign> position = unscaled(point);
auto [distance, nearest_line, x] = lower.distance_from_lines_extra<true>(position.cast<double>());
it->second = distance;
}
};
for (const LayerRegion *region : layer.regions()) {
for_each_extrusion_path(region->perimeters, add_path);
for_each_extrusion_path(region->fills, add_path);
}
return out;
}
struct ProcessedPoint
{
Point3 p;
@@ -426,23 +487,53 @@ struct ProcessedPoint
class ExtrusionQualityEstimator
{
std::unordered_map<const PrintObject*, AABBTreeLines::LinesDistancer<Linef3>> prev_layer_boundaries;
std::unordered_map<const PrintObject*, AABBTreeLines::LinesDistancer<Linef3>> next_layer_boundaries;
std::unordered_map<const PrintObject *, AABBTreeLines::LinesDistancer<CurledLine>> prev_curled_extrusions;
std::unordered_map<const PrintObject *, AABBTreeLines::LinesDistancer<CurledLine>> next_curled_extrusions;
const PrintObject *current_object;
using Boundaries = AABBTreeLines::LinesDistancer<Linef3>;
using CurledLines = AABBTreeLines::LinesDistancer<CurledLine>;
std::unordered_map<const PrintObject*, std::shared_ptr<const Boundaries>> prev_layer_boundaries;
std::unordered_map<const PrintObject*, std::shared_ptr<const CurledLines>> prev_curled_extrusions;
// The layers the trees above are built from, and the layers prepared last.
std::unordered_map<const PrintObject*, const Layer*> prev_layer_sources;
std::unordered_map<const PrintObject*, const Layer*> last_prepared_layers;
std::vector<PrecomputedOverhangLayer> precomputed_layers;
const PrintObject *current_object;
const PrecomputedOverhangLayer *precomputed_for(const PrintObject *object) const
{
auto it = std::find_if(precomputed_layers.begin(), precomputed_layers.end(),
[object](const PrecomputedOverhangLayer &layer) { return layer.object == object; });
return it == precomputed_layers.end() ? nullptr : &*it;
}
template<typename T> static const T &or_empty(const std::shared_ptr<const T> &tree)
{
static const T empty;
return tree ? *tree : empty;
}
public:
void set_current_object(const PrintObject *object) { current_object = object; }
// Takes the data computed ahead for the layer about to be generated, replacing the previous layer's.
void set_precomputed_layers(std::vector<PrecomputedOverhangLayer> &&layers) { precomputed_layers = std::move(layers); }
// Measures the layer against the layer prepared before it.
void prepare_for_new_layer(const PrintObject * obj, const Layer *layer)
{
if (layer == nullptr) return;
const PrintObject *object = obj;
prev_layer_boundaries[object] = next_layer_boundaries[object];
next_layer_boundaries[object] = AABBTreeLines::LinesDistancer<Linef3>{to_unscaled_linesf3(layer->lslices)};
prev_curled_extrusions[object] = next_curled_extrusions[object];
next_curled_extrusions[object] = AABBTreeLines::LinesDistancer<CurledLine>{layer->curled_lines};
const Layer *prev = std::exchange(last_prepared_layers[object], layer);
prev_layer_sources[object] = prev;
const PrecomputedOverhangLayer *precomputed = precomputed_for(object);
if (prev == nullptr) {
prev_layer_boundaries[object] = nullptr;
prev_curled_extrusions[object] = nullptr;
} else if (precomputed != nullptr && precomputed->layer == layer && layer->lower_layer == prev) {
prev_layer_boundaries[object] = precomputed->lower_boundaries;
prev_curled_extrusions[object] = precomputed->lower_curled_lines;
} else {
prev_layer_boundaries[object] = std::make_shared<const Boundaries>(to_unscaled_linesf3(prev->lslices));
prev_curled_extrusions[object] = std::make_shared<const CurledLines>(prev->curled_lines);
}
}
std::vector<ProcessedPoint> estimate_extrusion_quality(const ExtrusionPath &path,
@@ -526,9 +617,24 @@ public:
return std::min(calculate_speed(distance), original_speed);
};
// Precomputed distances hold only if they were measured against the layer prev_layer_boundaries is built from.
const std::unordered_map<Point3, double, Point3Hash> *known = nullptr;
if (const PrecomputedOverhangLayer *precomputed = precomputed_for(current_object);
precomputed != nullptr && precomputed->layer->lower_layer == prev_layer_sources[current_object])
known = &precomputed->distances;
const Boundaries &prev_boundaries = or_empty(prev_layer_boundaries[current_object]);
const CurledLines &prev_curled = or_empty(prev_curled_extrusions[current_object]);
auto known_distance = [known](const Point3 &point) -> const double * {
if (known == nullptr)
return nullptr;
auto it = known->find(point);
return it == known->end() ? nullptr : &it->second;
};
std::vector<ExtendedPoint<3>> extended_points =
estimate_points_properties<true, true, true, true>(path.polyline.points, prev_layer_boundaries[current_object], path.width, -1,
smallest_distance_with_lower_speed, effective_speed, fan_overlap_threshold);
estimate_points_properties<true, true, true, true, false>(path.polyline.points, prev_boundaries, path.width, -1,
smallest_distance_with_lower_speed, effective_speed, fan_overlap_threshold,
known_distance);
const auto width_inv = 1.0f / path.width;
std::vector<ProcessedPoint> processed_points;
processed_points.reserve(extended_points.size());
@@ -542,7 +648,7 @@ public:
const double dist_limit = 10.0 * path.width;
{
Vec3d middle = 0.5 * (curr.position + next.position);
auto line_indices = prev_curled_extrusions[current_object].all_lines_in_radius(Point::new_scale(middle),
auto line_indices = prev_curled.all_lines_in_radius(Point::new_scale(middle),
scale_(dist_limit));
if (!line_indices.empty()) {
double len = (next.position - curr.position).norm();
@@ -563,7 +669,7 @@ public:
double projected_lengths_sum = 0;
for (size_t idx : line_indices) {
const CurledLine& line = prev_curled_extrusions[current_object].get_line(idx);
const CurledLine& line = prev_curled.get_line(idx);
Lines inside = intersection_ln({{line.a, line.b}}, {box_of_influence});
if (inside.empty())
continue;
@@ -576,7 +682,7 @@ public:
}
for (size_t idx : line_indices) {
const CurledLine &line = prev_curled_extrusions[current_object].get_line(idx);
const CurledLine &line = prev_curled.get_line(idx);
float distance_from_curled = unscaled(line_alg::distance_to(line, Point::new_scale(middle)));
float dist = path.width * (1.0 - (distance_from_curled / dist_limit)) *
(1.0 - (distance_from_curled / dist_limit)) *
+66 -42
View File
@@ -909,8 +909,8 @@ public:
{
// Orca: find start pos by seaching G28/G29/PRINT_START/START_PRINT commands
auto is_start_pos = [](const std::string& curr_cmd) {
return boost::iequals(curr_cmd, "G28") || boost::iequals(curr_cmd, "G29") || boost::iequals(curr_cmd, "PRINT_START") ||
boost::iequals(curr_cmd, "START_PRINT");
return ascii_iequals(curr_cmd, "G28") || ascii_iequals(curr_cmd, "G29") || ascii_iequals(curr_cmd, "PRINT_START") ||
ascii_iequals(curr_cmd, "START_PRINT");
};
assert(!m_lines.empty());
const float time_step = backtrace.time_step();
@@ -1265,6 +1265,9 @@ void GCodeProcessor::run_post_process()
// Process inline placeholders (print_time_total_sec, print_time_day, print_time_hour, print_time_minute, print_time_sec and used_filament_length)
auto process_inline_placeholders = [&](std::string& gcode_line) {
bool processed = false;
// Every inline placeholder contains '@', so a line without one has nothing to replace.
if (gcode_line.find('@') == std::string::npos)
return processed;
const std::string& print_time_total_placeholder = reserved_tag(ETags::Print_Time_Total_Sec_Placeholder);
const std::string& print_time_day_placeholder = reserved_tag(ETags::Print_Time_Day_Placeholder);
@@ -4007,13 +4010,13 @@ void GCodeProcessor::process_gcode_line(const GCodeReader::GCodeLine& line, bool
const std::string_view cmd = line.cmd();
if (m_flavor == gcfKlipper)
{
if (boost::iequals(cmd, "SET_VELOCITY_LIMIT"))
if (ascii_iequals(cmd, "SET_VELOCITY_LIMIT"))
{
process_SET_VELOCITY_LIMIT(line);
return;
}
// ORCA: Add Pressure Advance visualization support
if (boost::iequals(cmd, "SET_PRESSURE_ADVANCE"))
if (ascii_iequals(cmd, "SET_PRESSURE_ADVANCE"))
{
process_SET_PRESSURE_ADVANCE(line);
return;
@@ -7551,51 +7554,72 @@ void GCodeProcessor::calculate_time(GCodeProcessorResult& result, size_t keep_la
actual_speed_moves = std::move(machine.actual_speed_moves);
}
// insert actual speed moves into the move list
unsigned int inserted_actual_speed_moves_count = 0;
std::vector<GCodeProcessorResult::MoveVertex> new_moves;
std::map<unsigned int, unsigned int> id_map;
for (auto it = actual_speed_moves.begin(); it != actual_speed_moves.end(); ++it) {
const unsigned int base_id = it->move_id + inserted_actual_speed_moves_count;
if (it->position.has_value()) {
// insert actual speed move into the move list
// clone from existing move
GCodeProcessorResult::MoveVertex new_move = result.moves[base_id];
// override modified parameters
new_move.time = { 0.0f, 0.0f };
new_move.position = *it->position;
new_move.actual_feedrate = it->actual_feedrate;
new_move.delta_extruder = *it->delta_extruder;
new_move.feedrate = *it->feedrate;
new_move.width = *it->width;
new_move.height = *it->height;
new_move.mm3_per_mm = *it->mm3_per_mm;
new_move.fan_speed = *it->fan_speed;
new_move.temperature = *it->temperature;
new_move.internal_only = true;
new_moves.push_back(new_move);
// actual_speed_moves holds, per block in move order, the moves to insert before the block's move and then an
// entry without a position for that move; positioned entries after the last such entry are dropped.
std::vector<GCodeProcessorResult::MoveVertex>& moves = result.moves;
size_t inserted_actual_speed_moves_count = 0;
size_t kept = 0;
size_t group_start = 0;
for (size_t i = 0; i < actual_speed_moves.size(); ++i) {
if (actual_speed_moves[i].position.has_value())
continue;
const unsigned int move_id = actual_speed_moves[i].move_id;
// A VG1 block has no move of its own, so its id can fall behind the previous block's or point past the list.
if (move_id < moves.size() && (kept == 0 || move_id > actual_speed_moves[kept - 1].move_id)) {
inserted_actual_speed_moves_count += i - group_start;
moves[move_id].actual_feedrate = actual_speed_moves[i].actual_feedrate;
// A seam vertex right after a block's move shares its actual speed.
if (move_id + 1 < moves.size() && moves[move_id + 1].type == EMoveType::Seam)
moves[move_id + 1].actual_feedrate = actual_speed_moves[i].actual_feedrate;
for (size_t j = group_start; j <= i; ++j, ++kept)
if (kept != j)
actual_speed_moves[kept] = std::move(actual_speed_moves[j]);
}
else {
result.moves.insert(result.moves.begin() + base_id, new_moves.begin(), new_moves.end());
id_map[it->move_id] = base_id + new_moves.size();
// update move actual speed
result.moves[base_id + new_moves.size()].actual_feedrate = it->actual_feedrate;
inserted_actual_speed_moves_count += new_moves.size();
// synchronize seams actual speed
if (base_id + new_moves.size() + 1 < result.moves.size()) {
GCodeProcessorResult::MoveVertex& move = result.moves[base_id + new_moves.size() + 1];
if (move.type == EMoveType::Seam)
move.actual_feedrate = it->actual_feedrate;
}
new_moves.clear();
group_start = i + 1;
}
actual_speed_moves.erase(actual_speed_moves.begin() + kept, actual_speed_moves.end());
// Walks the blocks back to front, so each shifted move is moved once, into its final slot.
size_t read = moves.size(); // one past the last move not yet placed
moves.resize(moves.size() + inserted_actual_speed_moves_count);
size_t write = moves.size(); // one past the last free slot
m_actual_speed_id_map.clear();
size_t entry = actual_speed_moves.size();
while (entry > 0) {
const unsigned int block_id = actual_speed_moves[--entry].move_id;
assert(block_id < read);
while (read > block_id + 1)
moves[--write] = moves[--read];
const GCodeProcessorResult::MoveVertex block_move = moves[--read];
moves[--write] = block_move;
m_actual_speed_id_map.emplace_back(block_id, (unsigned int)write);
for (; entry > 0 && actual_speed_moves[entry - 1].position.has_value(); --entry) {
const TimeMachine::ActualSpeedMove& it = actual_speed_moves[entry - 1];
GCodeProcessorResult::MoveVertex new_move = block_move;
new_move.time = { 0.0f, 0.0f };
new_move.position = *it.position;
new_move.actual_feedrate = it.actual_feedrate;
new_move.delta_extruder = *it.delta_extruder;
new_move.feedrate = *it.feedrate;
new_move.width = *it.width;
new_move.height = *it.height;
new_move.mm3_per_mm = *it.mm3_per_mm;
new_move.fan_speed = *it.fan_speed;
new_move.temperature = *it.temperature;
new_move.internal_only = true;
moves[--write] = new_move;
}
}
assert(read == write);
// synchronize blocks' move_ids with after moves for actual speed insertion
std::reverse(m_actual_speed_id_map.begin(), m_actual_speed_id_map.end());
for (size_t i = 0; i < static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Count); ++i) {
for (GCodeProcessor::TimeBlock& block : m_time_processor.machines[i].blocks) {
auto it = id_map.find(block.move_id);
block.move_id = (it != id_map.end()) ? it->second : block.move_id + inserted_actual_speed_moves_count;
auto it = std::lower_bound(m_actual_speed_id_map.begin(), m_actual_speed_id_map.end(), block.move_id,
[](const std::pair<unsigned int, unsigned int>& entry, unsigned int id) { return entry.first < id; });
block.move_id = (it != m_actual_speed_id_map.end() && it->first == block.move_id) ?
it->second : block.move_id + (unsigned int)inserted_actual_speed_moves_count;
}
}
}
+47 -41
View File
@@ -337,54 +337,57 @@ class Print;
//BBS: add mutex for protection of gcode result
mutable std::mutex result_mutex;
GCodeProcessorResult& operator=(const GCodeProcessorResult &other)
GCodeProcessorResult& operator=(const GCodeProcessorResult &other) { assign(other); return *this; }
// Declared because the user-declared copy assignment suppresses the implicit move.
GCodeProcessorResult& operator=(GCodeProcessorResult &&other) { assign(std::move(other)); return *this; }
// Add a new member here, or neither assignment transfers it.
template<class Other> void assign(Other &&other)
{
filename = other.filename;
id = other.id;
moves = other.moves;
lines_ends = other.lines_ends;
printable_area = other.printable_area;
bed_exclude_area = other.bed_exclude_area;
wrapping_exclude_area = other.wrapping_exclude_area;
toolpath_outside = other.toolpath_outside;
label_object_enabled = other.label_object_enabled;
long_retraction_when_cut = other.long_retraction_when_cut;
timelapse_warning_code = other.timelapse_warning_code;
printable_height = other.printable_height;
settings_ids = other.settings_ids;
filaments_count = other.filaments_count;
extruder_colors = other.extruder_colors;
filament_diameters = other.filament_diameters;
filament_densities = other.filament_densities;
filament_costs = other.filament_costs;
print_statistics = other.print_statistics;
custom_gcode_per_print_z = other.custom_gcode_per_print_z;
spiral_vase_mode = other.spiral_vase_mode;
warnings = other.warnings;
bed_type = other.bed_type;
gcode_check_result = other.gcode_check_result;
limit_filament_maps = other.limit_filament_maps;
filament_printable_reuslt = other.filament_printable_reuslt;
filename = std::forward<Other>(other).filename;
id = std::forward<Other>(other).id;
moves = std::forward<Other>(other).moves;
lines_ends = std::forward<Other>(other).lines_ends;
printable_area = std::forward<Other>(other).printable_area;
bed_exclude_area = std::forward<Other>(other).bed_exclude_area;
wrapping_exclude_area = std::forward<Other>(other).wrapping_exclude_area;
toolpath_outside = std::forward<Other>(other).toolpath_outside;
label_object_enabled = std::forward<Other>(other).label_object_enabled;
long_retraction_when_cut = std::forward<Other>(other).long_retraction_when_cut;
timelapse_warning_code = std::forward<Other>(other).timelapse_warning_code;
printable_height = std::forward<Other>(other).printable_height;
settings_ids = std::forward<Other>(other).settings_ids;
filaments_count = std::forward<Other>(other).filaments_count;
extruder_colors = std::forward<Other>(other).extruder_colors;
filament_diameters = std::forward<Other>(other).filament_diameters;
filament_densities = std::forward<Other>(other).filament_densities;
filament_costs = std::forward<Other>(other).filament_costs;
print_statistics = std::forward<Other>(other).print_statistics;
custom_gcode_per_print_z = std::forward<Other>(other).custom_gcode_per_print_z;
spiral_vase_mode = std::forward<Other>(other).spiral_vase_mode;
warnings = std::forward<Other>(other).warnings;
bed_type = std::forward<Other>(other).bed_type;
gcode_check_result = std::forward<Other>(other).gcode_check_result;
limit_filament_maps = std::forward<Other>(other).limit_filament_maps;
filament_printable_reuslt = std::forward<Other>(other).filament_printable_reuslt;
// Orca: copy the shared grouping result so a copied result keeps it (shared_ptr =>
// memory-safe), rather than leaving a stale pointer on the target. No g-code effect either way.
nozzle_group_result = other.nozzle_group_result;
nozzle_group_result = std::forward<Other>(other).nozzle_group_result;
// Keep the per-extruder hotend types on a copied result (injector input).
extruder_types = other.extruder_types;
printer_extruder_variant = other.printer_extruder_variant;
printer_extruder_id = other.printer_extruder_id;
layer_filaments = other.layer_filaments;
filament_change_sequence = other.filament_change_sequence;
used_mixed_filaments = other.used_mixed_filaments;
nozzle_change_sequence = other.nozzle_change_sequence;
optimal_assignment = other.optimal_assignment;
filament_change_count_map = other.filament_change_count_map;
extruder_types = std::forward<Other>(other).extruder_types;
printer_extruder_variant = std::forward<Other>(other).printer_extruder_variant;
printer_extruder_id = std::forward<Other>(other).printer_extruder_id;
layer_filaments = std::forward<Other>(other).layer_filaments;
filament_change_sequence = std::forward<Other>(other).filament_change_sequence;
used_mixed_filaments = std::forward<Other>(other).used_mixed_filaments;
nozzle_change_sequence = std::forward<Other>(other).nozzle_change_sequence;
optimal_assignment = std::forward<Other>(other).optimal_assignment;
filament_change_count_map = std::forward<Other>(other).filament_change_count_map;
// Keep the SKIPPABLE per-type time on a copied result.
skippable_part_time = other.skippable_part_time;
initial_layer_time = other.initial_layer_time;
skippable_part_time = std::forward<Other>(other).skippable_part_time;
initial_layer_time = std::forward<Other>(other).initial_layer_time;
#if ENABLE_GCODE_VIEWER_STATISTICS
time = other.time;
time = std::forward<Other>(other).time;
#endif
return *this;
}
void lock() const { result_mutex.lock(); }
void unlock() const { result_mutex.unlock(); }
@@ -1216,6 +1219,9 @@ class Print;
EProducer m_producer;
TimeProcessor m_time_processor;
// calculate_time()'s map from each block's move id to its index after the actual speed moves are inserted,
// a member to reuse its capacity.
std::vector<std::pair<unsigned int, unsigned int>> m_actual_speed_id_map;
UsedFilaments m_used_filaments;
Print* m_print{ nullptr };
+15 -16
View File
@@ -465,32 +465,31 @@ std::string GCodeWriter::set_accel_and_jerk(unsigned int acceleration, double je
if (EXTRUDER_LIMIT(m_max_acceleration) > 0 && acceleration > EXTRUDER_LIMIT(m_max_acceleration))
acceleration = EXTRUDER_LIMIT(m_max_acceleration);
bool is_empty = true;
std::ostringstream gcode;
gcode << "SET_VELOCITY_LIMIT";
if (acceleration != 0 && acceleration != m_last_acceleration) {
gcode << " ACCEL=" << acceleration;
if (this->config.accel_to_decel_enable) {
gcode << " ACCEL_TO_DECEL=" << acceleration * this->config.accel_to_decel_factor / 100;
}
m_last_acceleration = acceleration;
is_empty = false;
}
// Clamp the jerk to the allowed maximum.
if (EXTRUDER_LIMIT(m_max_jerk_x) > 0 && jerk > EXTRUDER_LIMIT(m_max_jerk_x))
jerk = EXTRUDER_LIMIT(m_max_jerk_x);
if (EXTRUDER_LIMIT(m_max_jerk_y) > 0 && jerk > EXTRUDER_LIMIT(m_max_jerk_y))
jerk = EXTRUDER_LIMIT(m_max_jerk_y);
if (jerk > 0.01 && !is_approx(jerk, m_last_jerk)) {
const bool set_acceleration = acceleration != 0 && acceleration != m_last_acceleration;
const bool set_jerk = jerk > 0.01 && !is_approx(jerk, m_last_jerk);
if (!set_acceleration && !set_jerk)
return std::string();
std::ostringstream gcode;
gcode << "SET_VELOCITY_LIMIT";
if (set_acceleration) {
gcode << " ACCEL=" << acceleration;
if (this->config.accel_to_decel_enable) {
gcode << " ACCEL_TO_DECEL=" << acceleration * this->config.accel_to_decel_factor / 100;
}
m_last_acceleration = acceleration;
}
if (set_jerk) {
gcode << " SQUARE_CORNER_VELOCITY=" << jerk;
m_last_jerk = jerk;
is_empty = false;
}
if(is_empty)
return std::string();
if (GCodeWriter::full_gcode_comment)
gcode << " ; adjust VELOCITY_LIMIT(accel/jerk)";
gcode << "\n";
+13
View File
@@ -84,6 +84,19 @@ double string_to_double_decimal_point(const std::string_view str, size_t* pos /*
return out;
}
double atof_decimal_point(std::string_view str)
{
size_t i = 0;
while (i < str.size() && (str[i] == ' ' || (str[i] >= '\t' && str[i] <= '\r')))
++i;
if (i < str.size() && str[i] == '+') {
++i;
if (i < str.size() && str[i] == '-')
return 0.;
}
return string_to_double_decimal_point(str.substr(i));
}
std::string float_to_string_decimal_point(double value, int precision/* = -1*/)
{
// Our Windows build server fully supports C++17 std::to_chars. Let's use it.
+3
View File
@@ -48,6 +48,9 @@ bool is_decimal_separator_point();
std::string float_to_string_decimal_point(double value, int precision = -1);
//std::string float_to_string_decimal_point(float value, int precision = -1);
double string_to_double_decimal_point(const std::string_view str, size_t* pos = nullptr);
// Parses like atof in the C locale, skipping leading whitespace and a '+',
// without the C runtime's per-call locale lookup.
double atof_decimal_point(std::string_view str);
} // namespace Slic3r
+1 -1
View File
@@ -5310,7 +5310,7 @@ static void apply_receiver_mix_relocations(DynamicPrintConfig&
//convert the old filament preset to new one after split
static void convert_filament_preset_name(std::string& machine_name, std::string& filament_name)
void PresetBundle::convert_filament_preset_name(const std::string& machine_name, std::string& filament_name)
{
auto machine_iter = filament_preset_convert.find(machine_name);
if (machine_iter != filament_preset_convert.end())
+3
View File
@@ -247,6 +247,9 @@ public:
// Keys a project keeps when its presets are loaded: those listed in its escaped
// "different_settings_to_system" entry for the preset, plus the preset bookkeeping keys.
static std::set<std::string> project_different_keys(const std::string &different_settings);
// A project filament saved under a name the current presets split per nozzle (e.g. H2D 0.6) is loaded from
// the preset that now holds its values.
static void convert_filament_preset_name(const std::string& machine_name, std::string& filament_name);
PresetBundle();
PresetBundle(const PresetBundle &rhs);
+186
View File
@@ -0,0 +1,186 @@
#ifndef BICUBIC_HPP
#define BICUBIC_HPP
#include <algorithm>
#include <vector>
#include <cmath>
#include <Eigen/Dense>
namespace Slic3r {
namespace BicubicInternal {
// Linear kernel, to be able to test cubic methods with hat kernels.
template<typename T>
struct LinearKernel
{
typedef T FloatType;
static T a00() { return T(0.); }
static T a01() { return T(0.); }
static T a02() { return T(0.); }
static T a03() { return T(0.); }
static T a10() { return T(1.); }
static T a11() { return T(-1.); }
static T a12() { return T(0.); }
static T a13() { return T(0.); }
static T a20() { return T(0.); }
static T a21() { return T(1.); }
static T a22() { return T(0.); }
static T a23() { return T(0.); }
static T a30() { return T(0.); }
static T a31() { return T(0.); }
static T a32() { return T(0.); }
static T a33() { return T(0.); }
};
// Interpolation kernel aka Catmul-Rom aka Keyes kernel.
template<typename T>
struct CubicCatmulRomKernel
{
typedef T FloatType;
static T a00() { return 0; }
static T a01() { return (T)-0.5; }
static T a02() { return (T) 1.; }
static T a03() { return (T)-0.5; }
static T a10() { return (T) 1.; }
static T a11() { return 0; }
static T a12() { return (T)-5./2.; }
static T a13() { return (T) 3./2.; }
static T a20() { return 0; }
static T a21() { return (T) 0.5; }
static T a22() { return (T) 2.; }
static T a23() { return (T)-3./2.; }
static T a30() { return 0; }
static T a31() { return 0; }
static T a32() { return (T)-0.5; }
static T a33() { return (T) 0.5; }
};
// B-spline kernel
template<typename T>
struct CubicBSplineKernel
{
typedef T FloatType;
static T a00() { return (T) 1./6.; }
static T a01() { return (T) -3./6.; }
static T a02() { return (T) 3./6.; }
static T a03() { return (T) -1./6.; }
static T a10() { return (T) 4./6.; }
static T a11() { return 0; }
static T a12() { return (T) -6./6.; }
static T a13() { return (T) 3./6.; }
static T a20() { return (T) 1./6.; }
static T a21() { return (T) 3./6.; }
static T a22() { return (T) 3./6.; }
static T a23() { return (T)- 3./6.; }
static T a30() { return 0; }
static T a31() { return 0; }
static T a32() { return 0; }
static T a33() { return (T) 1./6.; }
};
template<class T>
inline T clamp(T a, T lower, T upper)
{
return (a < lower) ? lower :
(a > upper) ? upper : a;
}
}
template<typename KERNEL>
struct CubicKernel
{
typedef typename KERNEL KernelInternal;
typedef typename KERNEL::FloatType FloatType;
static FloatType kernel(FloatType x)
{
x = fabs(x);
if (x >= (FloatType)2.)
return 0.0f;
if (x <= (FloatType)1.) {
FloatType x2 = x * x;
FloatType x3 = x2 * x;
return KERNEL::a10() + KERNEL::a11() * x + KERNEL::a12() * x2 + KERNEL::a13() * x3;
}
assert(x > (FloatType)1. && x < (FloatType)2.);
x -= (FloatType)1.;
FloatType x2 = x * x;
FloatType x3 = x2 * x;
return KERNEL::a00() + KERNEL::a01() * x + KERNEL::a02() * x2 + KERNEL::a03() * x3;
}
static FloatType interpolate(FloatType f0, FloatType f1, FloatType f2, FloatType f3, FloatType x)
{
const FloatType x2 = x*x;
const FloatType x3 = x*x*x;
return f0*(KERNEL::a00() + KERNEL::a01() * x + KERNEL::a02() * x2 + KERNEL::a03() * x3) +
f1*(KERNEL::a10() + KERNEL::a11() * x + KERNEL::a12() * x2 + KERNEL::a13() * x3) +
f2*(KERNEL::a20() + KERNEL::a21() * x + KERNEL::a22() * x2 + KERNEL::a23() * x3) +
f3*(KERNEL::a30() + KERNEL::a31() * x + KERNEL::a32() * x2 + KERNEL::a33() * x3);
}
};
// Linear splines
typedef CubicKernel<BicubicInternal::LinearKernel<float>> LinearKernelf;
typedef CubicKernel<BicubicInternal::LinearKernel<double>> LinearKerneld;
// Catmul-Rom splines
typedef CubicKernel<BicubicInternal::CubicCatmulRomKernel<float>> CubicCatmulRomKernelf;
typedef CubicKernel<BicubicInternal::CubicCatmulRomKernel<double>> CubicCatmulRomKerneld;
typedef CubicKernel<BicubicInternal::CubicCatmulRomKernel<float>> CubicInterpolationKernelf;
typedef CubicKernel<BicubicInternal::CubicCatmulRomKernel<double>> CubicInterpolationKerneld;
// Cubic B-splines
typedef CubicKernel<BicubicInternal::CubicBSplineKernel<float>> CubicBSplineKernelf;
typedef CubicKernel<BicubicInternal::CubicBSplineKernel<double>> CubicBSplineKerneld;
template<typename KERNEL, typename Derived>
static float cubic_interpolate(const Eigen::ArrayBase<Derived> &F, const typename KERNEL::FloatType pt, const typename KERNEL::FloatType dx)
{
typedef typename KERNEL::FloatType T;
const int w = int(F.size());
const int ix = (int)floor(pt);
const T s = pt - (T)ix;
if (ix > 1 && ix + 2 < w) {
// Inside the fully interpolated region.
return KERNEL::interpolate(F[ix - 1], F[ix], F[ix + 1], F[ix + 2], s);
}
// Transition region. Extend with a constant function.
auto f = [&F, w](x) { return F[BicubicInternal::clamp(x, 0, w - 1)]; }
return KERNEL::interpolate(f(ix - 1), f(ix), f(ix + 1), f(ix + 2), s);
}
template<typename KERNEL, typename Derived>
static float bicubic_interpolate(const Eigen::MatrixBase<Derived> &F, const Eigen::Matrix<typename KERNEL::FloatType, 2, 1, Eigen::DontAlign> &pt, const typename KERNEL::FloatType dx)
{
typedef typename KERNEL::FloatType T;
const int w = F.cols();
const int h = F.rows();
const int ix = (int)floor(pt[0]);
const int iy = (int)floor(pt[1]);
const T s = pt[0] - (T)ix;
const T t = pt[1] - (T)iy;
if (ix > 1 && ix + 2 < w && iy > 1 && iy + 2 < h) {
// Inside the fully interpolated region.
return KERNEL::interpolate(
KERNEL::interpolate(F(ix-1,iy-1),F(ix ,iy-1),F(ix+1,iy-1),F(ix+2,iy-1),s),
KERNEL::interpolate(F(ix-1,iy ),F(ix ,iy ),F(ix+1,iy ),F(ix+2,iy ),s),
KERNEL::interpolate(F(ix-1,iy+1),F(ix ,iy+1),F(ix+1,iy+1),F(ix+2,iy+1),s),
KERNEL::interpolate(F(ix-1,iy+2),F(ix ,iy+2),F(ix+1,iy+2),F(ix+2,iy+2),s),t);
}
// Transition region. Extend with a constant function.
auto f = [&f, w, h](int x, int y) { return F(BicubicInternal::clamp(x,0,w-1),BicubicInternal::clamp(y,0,h-1)); }
return KERNEL::interpolate(
KERNEL::interpolate(f(ix-1,iy-1),f(ix ,iy-1),f(ix+1,iy-1),f(ix+2,iy-1),s),
KERNEL::interpolate(f(ix-1,iy ),f(ix ,iy ),f(ix+1,iy ),f(ix+2,iy ),s),
KERNEL::interpolate(f(ix-1,iy+1),f(ix ,iy+1),f(ix+1,iy+1),f(ix+2,iy+1),s),
KERNEL::interpolate(f(ix-1,iy+2),f(ix ,iy+2),f(ix+1,iy+2),f(ix+2,iy+2),s),t);
}
} // namespace Slic3r
#endif /* BICUBIC_HPP */
+10
View File
@@ -25,6 +25,8 @@
#include <initializer_list>
#include <string_view>
#include <regex>
#include <string_view>
#include <algorithm>
#include <boost/system/error_code.hpp>
#include <boost/algorithm/string.hpp>
@@ -302,6 +304,14 @@ extern bool is_absolute_path_within_root(const boost::filesystem::path &path, co
// Anything unknown is not safe.
extern bool is_safe_to_open_file_name(const std::string &file_name);
// Case-insensitive compare against a fixed ASCII keyword, without boost::iequals, whose
// std::locale() takes a lock the whole process shares in the MSVC runtime.
inline bool ascii_iequals(std::string_view a, std::string_view b)
{
auto lower = [](char c) { return (c >= 'A' && c <= 'Z') ? char(c - 'A' + 'a') : c; };
return a.size() == b.size() && std::equal(a.begin(), a.end(), b.begin(), [&lower](char x, char y) { return lower(x) == lower(y); });
}
// Orca: custom protocal support utils
inline bool is_orca_open(const std::string& url) { return boost::starts_with(url, "orcaslicer://open"); }
inline bool is_prusaslicer_open(const std::string& url) { return boost::starts_with(url, "prusaslicer://open"); }
+9
View File
@@ -178,12 +178,16 @@ set(SLIC3R_GUI_SOURCES
GUI/Gizmos/GLGizmoCut.hpp
GUI/Gizmos/GLGizmoEmboss.cpp
GUI/Gizmos/GLGizmoEmboss.hpp
#GUI/Gizmos/GLGizmoFaceDetector.cpp
#GUI/Gizmos/GLGizmoFaceDetector.hpp
GUI/Gizmos/GLGizmoFdmSupports.cpp
GUI/Gizmos/GLGizmoFdmSupports.hpp
GUI/Gizmos/GLGizmoFlatten.cpp
GUI/Gizmos/GLGizmoFlatten.hpp
GUI/Gizmos/GLGizmoFuzzySkin.cpp
GUI/Gizmos/GLGizmoFuzzySkin.hpp
#GUI/Gizmos/GLGizmoHollow.cpp
#GUI/Gizmos/GLGizmoHollow.hpp
GUI/Gizmos/GLGizmoMeasure.cpp
GUI/Gizmos/GLGizmoMeasure.hpp
GUI/Gizmos/GLGizmoMeshBoolean.cpp
@@ -204,6 +208,8 @@ set(SLIC3R_GUI_SOURCES
GUI/Gizmos/GLGizmoSeam.hpp
GUI/Gizmos/GLGizmoSimplify.cpp
GUI/Gizmos/GLGizmoSimplify.hpp
#GUI/Gizmos/GLGizmoSlaSupports.cpp
#GUI/Gizmos/GLGizmoSlaSupports.hpp
GUI/Gizmos/GLGizmosManager.cpp
GUI/Gizmos/GLGizmosManager.hpp
GUI/Gizmos/GLGizmoSVG.cpp
@@ -212,6 +218,8 @@ set(SLIC3R_GUI_SOURCES
GUI/Gizmos/GLGizmoTextureDisplacement.hpp
GUI/Gizmos/GLGizmoUtils.cpp
GUI/Gizmos/GLGizmoUtils.hpp
#GUI/Gizmos/GLGizmoText.cpp
#GUI/Gizmos/GLGizmoText.hpp
GUI/GLCanvas3D.cpp
GUI/GLCanvas3D.hpp
GUI/GLModel.cpp
@@ -800,6 +808,7 @@ set(SLIC3R_GUI_SOURCES
Utils/PrintHost.hpp
Utils/Process.cpp
Utils/Process.hpp
Utils/ProfileDescription.hpp
Utils/Profile.hpp
Utils/RaycastManager.cpp
Utils/RaycastManager.hpp
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,222 @@
#ifndef slic3r_GLGizmoAdvancedCut_hpp_
#define slic3r_GLGizmoAdvancedCut_hpp_
#include "GLGizmoBase.hpp"
#include "GLGizmoRotate.hpp"
#include "libslic3r/Model.hpp"
namespace Slic3r {
enum class CutConnectorType : int;
class ModelVolume;
struct CutConnectorAttributes;
namespace GUI {
enum class SLAGizmoEventType : unsigned char;
class GLGizmoAdvancedCut : public GLGizmoRotate3D
{
struct Rotate_data {
double angle;
Axis ax;
Rotate_data(double an, Axis a)
: angle(an), ax(a)
{
}
};
private:
static const double Offset;
static const double Margin;
static const ColorRGBA GrabberColor;
static const ColorRGBA GrabberHoverColor;
mutable double m_movement;
mutable double m_height; // height of cut plane to heatbed
mutable double m_height_delta; // height of cut plane to heatbed
double m_start_movement;
double m_start_height;
Vec3d m_rotation;
//Vec3d m_current_base_rotation;
std::vector<Rotate_data> m_rotate_cmds;
Vec3d m_buffered_rotation;
double m_buffered_movement;
double m_buffered_height;
Vec3d m_drag_pos;
bool m_keep_upper;
bool m_keep_lower;
bool m_cut_to_parts;
bool m_place_on_cut_upper{true};
bool m_place_on_cut_lower{false};
bool m_rotate_upper{false};
bool m_rotate_lower{false};
GLModel m_plane;
GLModel m_grabber_connection;
GLModel m_cut_line;
bool m_do_segment;
double m_segment_smoothing_alpha;
int m_segment_number;
std::array<Vec3d, 4> m_cut_plane_points;
mutable Grabber m_move_grabber;
unsigned int m_last_active_id;
bool m_connectors_editing{false};
bool m_show_shortcuts{false};
std::vector<std::pair<wxString, wxString>> m_shortcuts;
double m_label_width{150.0};
double m_control_width{ 200.0 };
double m_editing_window_width;
CutConnectorType m_connector_type;
size_t m_connector_style;
size_t m_connector_shape_id;
float m_connector_depth_ratio{3.f};
float m_connector_depth_ratio_tolerance{0.1f};
float m_connector_size{2.5f};
float m_connector_size_tolerance{0.f};
TriangleMesh m_connector_mesh;
bool m_has_invalid_connector{false};
// remember the connectors which is selected
mutable std::vector<bool> m_selected;
int m_selected_count{0};
Vec3d m_cut_plane_center{Vec3d::Zero()};
Vec3d m_cut_plane_normal{Vec3d::UnitZ()};
Vec3d m_cut_line_begin{Vec3d::Zero()};
Vec3d m_cut_line_end{Vec3d::Zero()};
Transform3d m_rotate_matrix{Transform3d::Identity()};
std::map<CutConnectorAttributes, GLModel> m_shapes;
struct InvalidConnectorsStatistics
{
unsigned int outside_cut_contour;
unsigned int outside_bb;
bool is_overlap;
void invalidate()
{
outside_cut_contour = 0;
outside_bb = 0;
is_overlap = false;
}
} m_info_stats;
//GLSelectionRectangle m_selection_rectangle;
public:
GLGizmoAdvancedCut(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id);
bool gizmo_event(SLAGizmoEventType action, const Vec2d &mouse_position, bool shift_down, bool alt_down, bool control_down);
bool on_key(wxKeyEvent &evt);
double get_movement() const { return m_movement; }
void set_movement(double movement) const;
void finish_rotation();
std::string get_tooltip() const override;
BoundingBoxf3 bounding_box() const;
//BoundingBoxf3 transformed_bounding_box(const Vec3d &plane_center, bool revert_move = false) const;
bool is_looking_forward() const;
bool unproject_on_cut_plane(const Vec2d &mouse_pos, Vec3d &pos, Vec3d &pos_world);
virtual bool apply_clipping_plane() { return m_connectors_editing; }
void data_changed(bool is_serializing) override;
protected:
bool on_init() override;
void on_load(cereal::BinaryInputArchive &ar) override;
void on_save(cereal::BinaryOutputArchive &ar) const override;
std::string on_get_name() const override;
void on_set_state() override;
bool on_is_activable() const override;
CommonGizmosDataID on_get_requirements() const override;
void on_start_dragging() override;
void on_stop_dragging() override;
void on_dragging(const UpdateData& data) override;
void on_render() override;
virtual void on_render_input_window(float x, float y, float bottom_limit);
void show_tooltip_information(float x, float y);
private:
void perform_cut(const Selection& selection);
bool can_perform_cut() const;
void apply_connectors_in_model(ModelObject *mo, bool &create_dowels_as_separate_object);
bool is_selection_changed(bool alt_down, bool shift_down);
void select_connector(int idx, bool select);
double calc_projection(const Linef3& mouse_ray) const;
Vec3d calc_plane_normal(const std::array<Vec3d, 4>& plane_points) const;
Vec3d calc_plane_center(const std::array<Vec3d, 4>& plane_points) const;
Vec3d get_plane_normal() const;
Vec3d get_plane_center() const;
void update_plane_points();
std::array<Vec3d, 4> get_plane_points() const;
std::array<Vec3d, 4> get_plane_points_world_coord() const;
void reset_cut_plane();
void reset_all();
// update the connectors position so that the connectors are on the cut plane
void put_connectors_on_cut_plane(const Vec3d &cp_normal, double cp_offset);
void update_clipper();
// on render
void render_cut_plane_and_grabbers();
void render_connectors();
void render_clipper_cut();
void render_cut_line();
void render_connector_model(GLModel &model, const ColorRGBA& color, Transform3d model_matrix, bool for_picking = false);
void clear_selection();
void init_connector_shapes();
void set_connectors_editing(bool connectors_editing);
void reset_connectors();
void update_connector_shape();
void apply_selected_connectors(std::function<void(size_t idx)> apply_fn);
void select_all_connectors();
void unselect_all_connectors();
void validate_connector_settings();
bool add_connector(CutConnectors &connectors, const Vec2d &mouse_position);
bool delete_selected_connectors();
bool is_outside_of_cut_contour(size_t idx, const CutConnectors &connectors, const Vec3d cur_pos);
bool is_conflict_for_connector(size_t idx, const CutConnectors &connectors, const Vec3d cur_pos);
void check_conflict_for_all_connectors();
// render input window
void render_cut_plane_input_window(float x, float y, float bottom_limit);
void init_connectors_input_window_data();
void render_connectors_input_window(float x, float y, float bottom_limit);
void render_input_window_warning() const;
bool render_reset_button(const std::string &label_id, const std::string &tooltip) const;
bool render_connect_type_radio_button(CutConnectorType type);
bool render_combo(const std::string &label, const std::vector<std::string> &lines, size_t &selection_idx);
bool render_slider_double_input(const std::string &label, float &value_in, float &tolerance_in);
bool cut_line_processing() const;
void discard_cut_line_processing();
bool process_cut_line(SLAGizmoEventType action, const Vec2d &mouse_position);
};
} // namespace GUI
} // namespace Slic3r
#endif // slic3r_GLGizmoAdvancedCut_hpp_
@@ -0,0 +1,133 @@
#include "GLGizmoFaceDetector.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/SLA/IndexedMesh.hpp"
#include "libslic3r/FaceDetector.hpp"
#include "slic3r/GUI/GLCanvas3D.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/ImGuiWrapper.hpp"
#include "slic3r/GUI/Plater.hpp"
#include <glad/gl.h>
#ifdef __WINDOWS__
#include <windows.h>
#include <stdio.h>
#endif
namespace Slic3r {
namespace GUI {
bool GLGizmoFaceDetector::on_init()
{
return true;
}
std::string GLGizmoFaceDetector::on_get_name() const
{
return (_L("Face recognition") + " [P]").ToUTF8().data();
}
void GLGizmoFaceDetector::on_render()
{
if (model.is_initialized()) {
model.set_color({0.f, 0.f, 1.f, 0.4f});
model.render();
}
}
void GLGizmoFaceDetector::on_render_input_window(float x, float y, float bottom_limit)
{
#if 0
if (!m_c->selection_info() || !m_c->selection_info()->model_object())
return;
const float approx_height = m_imgui->scaled(14.0f);
y = std::min(y, bottom_limit - approx_height);
//BBS: GUI refactor: move gizmo to the right
#if BBS_TOOLBAR_ON_TOP
m_imgui->set_next_window_pos(x, y, ImGuiCond_Always, 0.5f, 0.0f);
#else
m_imgui->set_next_window_pos(x, y, ImGuiCond_Always, 1.0f, 0.0f);
#endif
ImGuiWrapper::push_toolbar_style();
m_imgui->begin(on_get_name(), ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoCollapse);
ImGui::PushItemWidth(m_imgui->get_style_scaling() * 150);
ImGui::InputDouble("Sample interval", &m_sample_interval, 0.0f, 0.0f, "%.2f");
bool btn_clicked = m_imgui->button(_L("Perform Recognition"));
if (btn_clicked) {
perform_recognition(m_parent.get_selection());
}
m_imgui->end();
ImGuiWrapper::pop_toolbar_style();
#endif
}
void GLGizmoFaceDetector::on_set_state()
{
if (get_state() == On) {
model.reset();
display_exterior_face();
}
}
bool GLGizmoFaceDetector::on_is_activable() const
{
const Selection& selection = m_parent.get_selection();
return selection.is_single_full_instance() && !selection.is_wipe_tower();
}
void GLGizmoFaceDetector::perform_recognition(const Selection& selection)
{
ModelObject* mo = m_c->selection_info()->model_object();
//FaceDetector face_detector(mo, m_sample_interval);
//face_detector.detect_exterior_face();
}
void GLGizmoFaceDetector::display_exterior_face()
{
int cnt = 0;
model.reset();
GLModel::Geometry init_data;
init_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3N3, GLModel::Geometry::EIndexType::UINT };
const ModelObjectPtrs& objects = wxGetApp().model().objects;
for (ModelObject* mo : objects) {
const ModelInstance* mi = mo->instances[0];
Transform3d inst_transfo = mi->get_matrix();
for (ModelVolume* mv : mo->volumes) {
TriangleMesh mesh_temp = mv->mesh();
mesh_temp.transform(mv->get_matrix() * inst_transfo);
indexed_triangle_set& mv_its = mesh_temp.its;
for (int facet_idx = 0; facet_idx < mv_its.indices.size(); facet_idx++) {
const stl_triangle_vertex_indices& facet_vert_idxs = mv_its.indices[facet_idx];
if (mv_its.get_property(facet_idx).type != eExteriorAppearance)
continue;
for (int i = 0; i < 3; ++i) {
init_data.add_vertex((Vec3f) mv_its.vertices[facet_vert_idxs[i]].cast<float>(), Vec3f{0.0f, 0.0f, 1.0f});
}
init_data.add_uint_triangle(cnt, cnt + 1, cnt + 2);
cnt += 3;
}
}
}
model.init_from(std::move(init_data));
}
CommonGizmosDataID GLGizmoFaceDetector::on_get_requirements() const
{
return CommonGizmosDataID::SelectionInfo;
}
} // namespace GUI
} // namespace Slic3r
@@ -0,0 +1,39 @@
#ifndef slic3r_GLGizmoFaceDetector_hpp_
#define slic3r_GLGizmoFaceDetector_hpp_
#include "GLGizmoBase.hpp"
#include "slic3r/GUI/3DScene.hpp"
namespace Slic3r {
namespace GUI {
class GLGizmoFaceDetector : public GLGizmoBase
{
public:
GLGizmoFaceDetector(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id)
: GLGizmoBase(parent, icon_filename, sprite_id) {}
protected:
void on_render() override;
void on_render_for_picking() override {}
void on_render_input_window(float x, float y, float bottom_limit) override;
std::string on_get_name() const override;
void on_set_state() override;
bool on_is_activable() const override;
CommonGizmosDataID on_get_requirements() const override;
private:
bool on_init() override;
void perform_recognition(const Selection& selection);
void display_exterior_face();
GUI::GLModel model;
double m_sample_interval = {0.5};
};
} // namespace GUI
} // namespace Slic3r
#endif // slic3r_GLGizmoFaceDetector_hpp_
+872
View File
@@ -0,0 +1,872 @@
#include "GLGizmoHollow.hpp"
#include "slic3r/GUI/GLCanvas3D.hpp"
#include "slic3r/GUI/Camera.hpp"
#include "slic3r/GUI/Gizmos/GLGizmosCommon.hpp"
#include <glad/gl.h>
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/GUI_ObjectSettings.hpp"
#include "slic3r/GUI/GUI_ObjectList.hpp"
#include "slic3r/GUI/Plater.hpp"
#include "libslic3r/PresetBundle.hpp"
#include "libslic3r/Model.hpp"
namespace Slic3r {
namespace GUI {
GLGizmoHollow::GLGizmoHollow(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id)
: GLGizmoBase(parent, icon_filename, sprite_id)
{
}
bool GLGizmoHollow::on_init()
{
m_desc["enable"] = _(L("Hollow this object"));
m_desc["preview"] = _(L("Preview hollowed and drilled model"));
m_desc["offset"] = _(L("Offset")) + ": ";
m_desc["quality"] = _(L("Quality")) + ": ";
m_desc["closing_distance"] = _(L("Closing distance")) + ": ";
m_desc["hole_diameter"] = _(L("Hole diameter")) + ": ";
m_desc["hole_depth"] = _(L("Hole depth")) + ": ";
m_desc["remove_selected"] = _(L("Remove selected holes"));
m_desc["remove_all"] = _(L("Remove all holes"));
m_desc["clipping_of_view"] = _(L("Clipping of view"))+ ": ";
m_desc["reset_direction"] = _(L("Reset direction"));
m_desc["show_supports"] = _(L("Show supports"));
return true;
}
void GLGizmoHollow::set_sla_support_data(ModelObject*, const Selection&)
{
if (! m_c->selection_info())
return;
const ModelObject* mo = m_c->selection_info()->model_object();
if (m_state == On && mo) {
if (m_old_mo_id != mo->id()) {
reload_cache();
m_old_mo_id = mo->id();
}
if (m_c->hollowed_mesh() && m_c->hollowed_mesh()->get_hollowed_mesh())
m_holes_in_drilled_mesh = mo->sla_drain_holes;
}
}
void GLGizmoHollow::on_render()
{
if (!m_cylinder.is_initialized())
m_cylinder.init_from(its_make_cylinder(1.0, 1.0));
const Selection& selection = m_parent.get_selection();
const CommonGizmosDataObjects::SelectionInfo* sel_info = m_c->selection_info();
// If current m_c->m_model_object does not match selection, ask GLCanvas3D to turn us off
if (m_state == On
&& (sel_info->model_object() != selection.get_model()->objects[selection.get_object_idx()]
|| sel_info->get_active_instance() != selection.get_instance_idx())) {
m_parent.post_event(SimpleEvent(EVT_GLCANVAS_RESETGIZMOS));
return;
}
glsafe(::glEnable(GL_BLEND));
glsafe(::glEnable(GL_DEPTH_TEST));
if (selection.is_from_single_instance())
render_points(selection, false);
m_selection_rectangle.render(m_parent);
m_c->object_clipper()->render_cut();
m_c->supports_clipper()->render_cut();
glsafe(::glDisable(GL_BLEND));
}
void GLGizmoHollow::render_points(const Selection& selection, bool picking)
{
GLShaderProgram* shader = picking ? wxGetApp().get_shader("flat") : wxGetApp().get_shader("gouraud_light");
if (shader == nullptr)
return;
shader->start_using();
ScopeGuard guard([shader]() { shader->stop_using(); });
const GLVolume* vol = selection.get_volume(*selection.get_volume_idxs().begin());
const Transform3d instance_scaling_matrix_inverse = vol->get_instance_transformation().get_matrix(true, true, false, true).inverse();
const Transform3d instance_matrix = Geometry::assemble_transform(m_c->selection_info()->get_sla_shift() * Vec3d::UnitZ()) * vol->get_instance_transformation().get_matrix();
const Camera& camera = wxGetApp().plater()->get_camera();
const Transform3d& view_matrix = camera.get_view_matrix();
const Transform3d& projection_matrix = camera.get_projection_matrix();
shader->set_uniform("projection_matrix", projection_matrix);
ColorRGBA render_color;
const sla::DrainHoles& drain_holes = m_c->selection_info()->model_object()->sla_drain_holes;
const size_t cache_size = drain_holes.size();
for (size_t i = 0; i < cache_size; ++i) {
const sla::DrainHole& drain_hole = drain_holes[i];
const bool point_selected = m_selected[i];
if (is_mesh_point_clipped(drain_hole.pos.cast<double>()))
continue;
// First decide about the color of the point.
if (picking)
render_color = picking_color_component(i);
else {
if (size_t(m_hover_id) == i)
render_color = ColorRGBA::CYAN();
else if (m_c->hollowed_mesh() &&
i < m_c->hollowed_mesh()->get_drainholes().size() &&
m_c->hollowed_mesh()->get_drainholes()[i].failed) {
render_color = { 1.0f, 0.0f, 0.0f, 0.5f };
}
else // neither hover nor picking
render_color = point_selected ? ColorRGBA(1.0f, 0.3f, 0.3f, 0.5f) : ColorRGBA(1.0f, 1.0f, 1.0f, 0.5f);
}
m_cylinder.set_color(render_color);
// Inverse matrix of the instance scaling is applied so that the mark does not scale with the object.
const Transform3d hole_matrix = Geometry::assemble_transform(drain_hole.pos.cast<double>()) * instance_scaling_matrix_inverse;
if (vol->is_left_handed())
glFrontFace(GL_CW);
// Matrices set, we can render the point mark now.
Eigen::Quaterniond q;
q.setFromTwoVectors(Vec3d::UnitZ(), instance_scaling_matrix_inverse * (-drain_hole.normal).cast<double>());
const Eigen::AngleAxisd aa(q);
const Transform3d model_matrix = instance_matrix * hole_matrix * Transform3d(aa.toRotationMatrix()) *
Geometry::assemble_transform(-drain_hole.height * Vec3d::UnitZ(), Vec3d::Zero(), Vec3d(drain_hole.radius, drain_hole.radius, drain_hole.height + sla::HoleStickOutLength));
shader->set_uniform("view_model_matrix", view_matrix * model_matrix);
const Matrix3d view_normal_matrix = view_matrix.matrix().block(0, 0, 3, 3) * model_matrix.matrix().block(0, 0, 3, 3).inverse().transpose();
shader->set_uniform("view_normal_matrix", view_normal_matrix);
m_cylinder.render();
if (vol->is_left_handed())
glFrontFace(GL_CCW);
}
}
bool GLGizmoHollow::is_mesh_point_clipped(const Vec3d& point) const
{
if (m_c->object_clipper()->get_position() == 0.)
return false;
auto sel_info = m_c->selection_info();
int active_inst = m_c->selection_info()->get_active_instance();
const ModelInstance* mi = sel_info->model_object()->instances[active_inst];
const Transform3d& trafo = mi->get_transformation().get_matrix();
Vec3d transformed_point = trafo * point;
transformed_point(2) += sel_info->get_sla_shift();
return m_c->object_clipper()->get_clipping_plane()->is_point_clipped(transformed_point);
}
// Unprojects the mouse position on the mesh and saves hit point and normal of the facet into pos_and_normal
// Return false if no intersection was found, true otherwise.
bool GLGizmoHollow::unproject_on_mesh(const Vec2d& mouse_pos, std::pair<Vec3f, Vec3f>& pos_and_normal)
{
if (! m_c->raycaster()->raycaster())
return false;
const Camera& camera = wxGetApp().plater()->get_camera();
const Selection& selection = m_parent.get_selection();
const GLVolume* volume = selection.get_volume(*selection.get_volume_idxs().begin());
Geometry::Transformation trafo = volume->get_instance_transformation();
trafo.set_offset(trafo.get_offset() + Vec3d(0., 0., m_c->selection_info()->get_sla_shift()));
double clp_dist = m_c->object_clipper()->get_position();
const ClippingPlane* clp = m_c->object_clipper()->get_clipping_plane();
// The raycaster query
Vec3f hit;
Vec3f normal;
if (m_c->raycaster()->raycaster()->unproject_on_mesh(
mouse_pos,
trafo.get_matrix(),
camera,
hit,
normal,
clp_dist != 0. ? clp : nullptr))
{
if (m_c->hollowed_mesh() && m_c->hollowed_mesh()->get_hollowed_mesh()) {
// in this case the raycaster sees the hollowed and drilled mesh.
// if the point lies on the surface created by the hole, we want
// to ignore it.
for (const sla::DrainHole& hole : m_holes_in_drilled_mesh) {
sla::DrainHole outer(hole);
outer.radius *= 1.001f;
outer.height *= 1.001f;
if (outer.is_inside(hit))
return false;
}
}
// Return both the point and the facet normal.
pos_and_normal = std::make_pair(hit, normal);
return true;
}
else
return false;
}
// Following function is called from GLCanvas3D to inform the gizmo about a mouse/keyboard event.
// The gizmo has an opportunity to react - if it does, it should return true so that the Canvas3D is
// aware that the event was reacted to and stops trying to make different sense of it. If the gizmo
// concludes that the event was not intended for it, it should return false.
bool GLGizmoHollow::gizmo_event(SLAGizmoEventType action, const Vec2d& mouse_position, bool shift_down, bool alt_down, bool control_down)
{
ModelObject* mo = m_c->selection_info()->model_object();
int active_inst = m_c->selection_info()->get_active_instance();
// left down with shift - show the selection rectangle:
if (action == SLAGizmoEventType::LeftDown && (shift_down || alt_down || control_down)) {
if (m_hover_id == -1) {
if (shift_down || alt_down) {
m_selection_rectangle.start_dragging(mouse_position, shift_down ? GLSelectionRectangle::Select : GLSelectionRectangle::Deselect);
}
}
else {
if (m_selected[m_hover_id])
unselect_point(m_hover_id);
else {
if (!alt_down)
select_point(m_hover_id);
}
}
return true;
}
// left down without selection rectangle - place point on the mesh:
if (action == SLAGizmoEventType::LeftDown && !m_selection_rectangle.is_dragging() && !shift_down) {
// If any point is in hover state, this should initiate its move - return control back to GLCanvas:
if (m_hover_id != -1)
return false;
// If there is some selection, don't add new point and deselect everything instead.
if (m_selection_empty) {
std::pair<Vec3f, Vec3f> pos_and_normal;
if (unproject_on_mesh(mouse_position, pos_and_normal)) { // we got an intersection
Plater::TakeSnapshot snapshot(wxGetApp().plater(), "Add drainage hole");
mo->sla_drain_holes.emplace_back(pos_and_normal.first,
-pos_and_normal.second, m_new_hole_radius, m_new_hole_height);
m_selected.push_back(false);
assert(m_selected.size() == mo->sla_drain_holes.size());
m_parent.set_as_dirty();
m_wait_for_up_event = true;
}
else
return false;
}
else
select_point(NoPoints);
return true;
}
// left up with selection rectangle - select points inside the rectangle:
if ((action == SLAGizmoEventType::LeftUp || action == SLAGizmoEventType::ShiftUp || action == SLAGizmoEventType::AltUp) && m_selection_rectangle.is_dragging()) {
// Is this a selection or deselection rectangle?
GLSelectionRectangle::EState rectangle_status = m_selection_rectangle.get_state();
// First collect positions of all the points in world coordinates.
Geometry::Transformation trafo = mo->instances[active_inst]->get_transformation();
trafo.set_offset(trafo.get_offset() + Vec3d(0., 0., m_c->selection_info()->get_sla_shift()));
std::vector<Vec3d> points;
for (unsigned int i=0; i<mo->sla_drain_holes.size(); ++i)
points.push_back(trafo.get_matrix() * mo->sla_drain_holes[i].pos.cast<double>());
// Now ask the rectangle which of the points are inside.
std::vector<Vec3f> points_inside;
std::vector<unsigned int> points_idxs = m_selection_rectangle.stop_dragging(m_parent, points);
for (size_t idx : points_idxs)
points_inside.push_back(points[idx].cast<float>());
// Only select/deselect points that are actually visible
for (size_t idx : m_c->raycaster()->raycaster()->get_unobscured_idxs(
trafo, wxGetApp().plater()->get_camera(), points_inside,
m_c->object_clipper()->get_clipping_plane()))
{
if (rectangle_status == GLSelectionRectangle::Deselect)
unselect_point(points_idxs[idx]);
else
select_point(points_idxs[idx]);
}
return true;
}
// left up with no selection rectangle
if (action == SLAGizmoEventType::LeftUp) {
if (m_wait_for_up_event) {
m_wait_for_up_event = false;
}
return true;
}
// dragging the selection rectangle:
if (action == SLAGizmoEventType::Dragging) {
if (m_wait_for_up_event)
return true; // point has been placed and the button not released yet
// this prevents GLCanvas from starting scene rotation
if (m_selection_rectangle.is_dragging()) {
m_selection_rectangle.dragging(mouse_position);
return true;
}
return false;
}
if (action == SLAGizmoEventType::Delete) {
// delete key pressed
delete_selected_points();
return true;
}
if (action == SLAGizmoEventType::RightDown) {
if (m_hover_id != -1) {
select_point(NoPoints);
select_point(m_hover_id);
delete_selected_points();
return true;
}
return false;
}
if (action == SLAGizmoEventType::SelectAll) {
select_point(AllPoints);
return true;
}
if (action == SLAGizmoEventType::MouseWheelUp && control_down) {
double pos = m_c->object_clipper()->get_position();
pos = std::min(1., pos + 0.01);
m_c->object_clipper()->set_position(pos, true);
return true;
}
if (action == SLAGizmoEventType::MouseWheelDown && control_down) {
double pos = m_c->object_clipper()->get_position();
pos = std::max(0., pos - 0.01);
m_c->object_clipper()->set_position(pos, true);
return true;
}
if (action == SLAGizmoEventType::ResetClippingPlane) {
m_c->object_clipper()->set_position(-1., false);
return true;
}
return false;
}
void GLGizmoHollow::delete_selected_points()
{
Plater::TakeSnapshot snapshot(wxGetApp().plater(), "Delete drainage hole");
sla::DrainHoles& drain_holes = m_c->selection_info()->model_object()->sla_drain_holes;
for (unsigned int idx=0; idx<drain_holes.size(); ++idx) {
if (m_selected[idx]) {
m_selected.erase(m_selected.begin()+idx);
drain_holes.erase(drain_holes.begin() + (idx--));
}
}
select_point(NoPoints);
}
void GLGizmoHollow::on_update(const UpdateData& data)
{
sla::DrainHoles& drain_holes = m_c->selection_info()->model_object()->sla_drain_holes;
if (m_hover_id != -1) {
std::pair<Vec3f, Vec3f> pos_and_normal;
if (! unproject_on_mesh(data.mouse_pos.cast<double>(), pos_and_normal))
return;
drain_holes[m_hover_id].pos = pos_and_normal.first;
drain_holes[m_hover_id].normal = -pos_and_normal.second;
}
}
void GLGizmoHollow::hollow_mesh(bool postpone_error_messages)
{
wxGetApp().CallAfter([this, postpone_error_messages]() {
wxGetApp().plater()->reslice_SLA_hollowing(
*m_c->selection_info()->model_object(), postpone_error_messages);
});
}
std::vector<std::pair<const ConfigOption*, const ConfigOptionDef*>>
GLGizmoHollow::get_config_options(const std::vector<std::string>& keys) const
{
std::vector<std::pair<const ConfigOption*, const ConfigOptionDef*>> out;
const ModelObject* mo = m_c->selection_info()->model_object();
if (! mo)
return out;
const DynamicPrintConfig& object_cfg = mo->config.get();
const DynamicPrintConfig& print_cfg = wxGetApp().preset_bundle->sla_prints.get_edited_preset().config;
std::unique_ptr<DynamicPrintConfig> default_cfg = nullptr;
for (const std::string& key : keys) {
if (object_cfg.has(key))
out.emplace_back(object_cfg.option(key), &object_cfg.def()->options.at(key)); // at() needed for const map
else
if (print_cfg.has(key))
out.emplace_back(print_cfg.option(key), &print_cfg.def()->options.at(key));
else { // we must get it from defaults
if (default_cfg == nullptr)
default_cfg.reset(DynamicPrintConfig::new_from_defaults_keys(keys));
out.emplace_back(default_cfg->option(key), &default_cfg->def()->options.at(key));
}
}
return out;
}
void GLGizmoHollow::on_render_input_window(float x, float y, float bottom_limit)
{
ModelObject* mo = m_c->selection_info()->model_object();
if (! mo)
return;
bool first_run = true; // This is a hack to redraw the button when all points are removed,
// so it is not delayed until the background process finishes.
ConfigOptionMode current_mode = wxGetApp().get_mode();
std::vector<std::string> opts_keys = {"hollowing_min_thickness", "hollowing_quality", "hollowing_closing_distance"};
auto opts = get_config_options(opts_keys);
auto* offset_cfg = static_cast<const ConfigOptionFloat*>(opts[0].first);
float offset = offset_cfg->value;
double offset_min = opts[0].second->min;
double offset_max = opts[0].second->max;
auto* quality_cfg = static_cast<const ConfigOptionFloat*>(opts[1].first);
float quality = quality_cfg->value;
double quality_min = opts[1].second->min;
double quality_max = opts[1].second->max;
ConfigOptionMode quality_mode = opts[1].second->mode;
auto* closing_d_cfg = static_cast<const ConfigOptionFloat*>(opts[2].first);
float closing_d = closing_d_cfg->value;
double closing_d_min = opts[2].second->min;
double closing_d_max = opts[2].second->max;
ConfigOptionMode closing_d_mode = opts[2].second->mode;
m_desc["offset"] = _(opts[0].second->label) + ":";
m_desc["quality"] = _(opts[1].second->label) + ":";
m_desc["closing_distance"] = _(opts[2].second->label) + ":";
RENDER_AGAIN:
const float approx_height = m_imgui->scaled(20.0f);
y = std::min(y, bottom_limit - approx_height);
m_imgui->set_next_window_pos(x, y, ImGuiCond_Always);
m_imgui->begin(get_name(), ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoCollapse);
// First calculate width of all the texts that are could possibly be shown. We will decide set the dialog width based on that:
const float clipping_slider_left = std::max(m_imgui->calc_text_size(m_desc.at("clipping_of_view")).x,
m_imgui->calc_text_size(m_desc.at("reset_direction")).x) + m_imgui->scaled(0.5f);
const float settings_sliders_left =
std::max(std::max({m_imgui->calc_text_size(m_desc.at("offset")).x,
m_imgui->calc_text_size(m_desc.at("quality")).x,
m_imgui->calc_text_size(m_desc.at("closing_distance")).x,
m_imgui->calc_text_size(m_desc.at("hole_diameter")).x,
m_imgui->calc_text_size(m_desc.at("hole_depth")).x}) + m_imgui->scaled(0.5f), clipping_slider_left);
const float diameter_slider_left = settings_sliders_left; //m_imgui->calc_text_size(m_desc.at("hole_diameter")).x + m_imgui->scaled(1.f);
const float minimal_slider_width = m_imgui->scaled(4.f);
const float button_preview_width = m_imgui->calc_button_size(m_desc.at("preview")).x;
float window_width = minimal_slider_width + std::max({settings_sliders_left, clipping_slider_left, diameter_slider_left});
window_width = std::max(window_width, button_preview_width);
if (m_imgui->button(m_desc["preview"]))
hollow_mesh();
bool config_changed = false;
ImGui::Separator();
{
auto opts = get_config_options({"hollowing_enable"});
m_enable_hollowing = static_cast<const ConfigOptionBool*>(opts[0].first)->value;
if (m_imgui->checkbox(m_desc["enable"], m_enable_hollowing)) {
mo->config.set("hollowing_enable", m_enable_hollowing);
wxGetApp().obj_list()->update_and_show_object_settings_item();
config_changed = true;
}
}
m_imgui->disabled_begin(! m_enable_hollowing);
ImGui::AlignTextToFramePadding();
m_imgui->text(m_desc.at("offset"));
ImGui::SameLine(settings_sliders_left, m_imgui->get_item_spacing().x);
ImGui::PushItemWidth(window_width - settings_sliders_left);
m_imgui->slider_float("##offset", &offset, offset_min, offset_max, "%.1f mm", 1.0f, true, _L(opts[0].second->tooltip));
bool slider_clicked = m_imgui->get_last_slider_status().clicked; // someone clicked the slider
bool slider_edited =m_imgui->get_last_slider_status().edited; // someone is dragging the slider
bool slider_released =m_imgui->get_last_slider_status().deactivated_after_edit; // someone has just released the slider
if (current_mode >= quality_mode) {
ImGui::AlignTextToFramePadding();
m_imgui->text(m_desc.at("quality"));
ImGui::SameLine(settings_sliders_left, m_imgui->get_item_spacing().x);
m_imgui->slider_float("##quality", &quality, quality_min, quality_max, "%.1f", 1.0f, true, _L(opts[1].second->tooltip));
slider_clicked |= m_imgui->get_last_slider_status().clicked;
slider_edited |= m_imgui->get_last_slider_status().edited;
slider_released |= m_imgui->get_last_slider_status().deactivated_after_edit;
}
if (current_mode >= closing_d_mode) {
ImGui::AlignTextToFramePadding();
m_imgui->text(m_desc.at("closing_distance"));
ImGui::SameLine(settings_sliders_left, m_imgui->get_item_spacing().x);
m_imgui->slider_float("##closing_distance", &closing_d, closing_d_min, closing_d_max, "%.1f mm", 1.0f, true, _L(opts[2].second->tooltip));
slider_clicked |= m_imgui->get_last_slider_status().clicked;
slider_edited |= m_imgui->get_last_slider_status().edited;
slider_released |= m_imgui->get_last_slider_status().deactivated_after_edit;
}
if (slider_clicked) {
m_offset_stash = offset;
m_quality_stash = quality;
m_closing_d_stash = closing_d;
}
if (slider_edited || slider_released) {
if (slider_released) {
mo->config.set("hollowing_min_thickness", m_offset_stash);
mo->config.set("hollowing_quality", m_quality_stash);
mo->config.set("hollowing_closing_distance", m_closing_d_stash);
Plater::TakeSnapshot snapshot(wxGetApp().plater(), "Hollowing parameter change");
}
mo->config.set("hollowing_min_thickness", offset);
mo->config.set("hollowing_quality", quality);
mo->config.set("hollowing_closing_distance", closing_d);
if (slider_released) {
wxGetApp().obj_list()->update_and_show_object_settings_item();
config_changed = true;
}
}
m_imgui->disabled_end();
bool force_refresh = false;
bool remove_selected = false;
bool remove_all = false;
ImGui::Separator();
float diameter_upper_cap = 60.;
if (m_new_hole_radius * 2.f > diameter_upper_cap)
m_new_hole_radius = diameter_upper_cap / 2.f;
ImGui::AlignTextToFramePadding();
m_imgui->text(m_desc.at("hole_diameter"));
ImGui::SameLine(diameter_slider_left, m_imgui->get_item_spacing().x);
ImGui::PushItemWidth(window_width - diameter_slider_left);
float diam = 2.f * m_new_hole_radius;
m_imgui->slider_float("##hole_diameter", &diam, 1.f, 25.f, "%.1f mm", 1.f, false);
// Let's clamp the value (which could have been entered by keyboard) to a larger range
// than the slider. This allows entering off-scale values and still protects against
//complete non-sense.
diam = std::clamp(diam, 0.1f, diameter_upper_cap);
m_new_hole_radius = diam / 2.f;
bool clicked = m_imgui->get_last_slider_status().clicked;
bool edited = m_imgui->get_last_slider_status().edited;
bool deactivated = m_imgui->get_last_slider_status().deactivated_after_edit;
ImGui::AlignTextToFramePadding();
m_imgui->text(m_desc["hole_depth"]);
ImGui::SameLine(diameter_slider_left, m_imgui->get_item_spacing().x);
m_imgui->slider_float("##hole_depth", &m_new_hole_height, 0.f, 10.f, "%.1f mm", 1.f, false);
// Same as above:
m_new_hole_height = std::clamp(m_new_hole_height, 0.f, 100.f);
clicked |= m_imgui->get_last_slider_status().clicked;
edited |= m_imgui->get_last_slider_status().edited;
deactivated |= m_imgui->get_last_slider_status().deactivated_after_edit;;
// Following is a nasty way to:
// - save the initial value of the slider before one starts messing with it
// - keep updating the head radius during sliding so it is continuosly refreshed in 3D scene
// - take correct undo/redo snapshot after the user is done with moving the slider
if (! m_selection_empty) {
if (clicked) {
m_holes_stash = mo->sla_drain_holes;
}
if (edited) {
for (size_t idx=0; idx<m_selected.size(); ++idx)
if (m_selected[idx]) {
mo->sla_drain_holes[idx].radius = m_new_hole_radius;
mo->sla_drain_holes[idx].height = m_new_hole_height;
}
}
if (deactivated) {
// momentarily restore the old value to take snapshot
sla::DrainHoles new_holes = mo->sla_drain_holes;
mo->sla_drain_holes = m_holes_stash;
float backup_rad = m_new_hole_radius;
float backup_hei = m_new_hole_height;
for (size_t i=0; i<m_holes_stash.size(); ++i) {
if (m_selected[i]) {
m_new_hole_radius = m_holes_stash[i].radius;
m_new_hole_height = m_holes_stash[i].height;
break;
}
}
Plater::TakeSnapshot snapshot(wxGetApp().plater(), "Change drainage hole diameter");
m_new_hole_radius = backup_rad;
m_new_hole_height = backup_hei;
mo->sla_drain_holes = new_holes;
}
}
m_imgui->disabled_begin(m_selection_empty);
remove_selected = m_imgui->button(m_desc.at("remove_selected"));
m_imgui->disabled_end();
m_imgui->disabled_begin(mo->sla_drain_holes.empty());
remove_all = m_imgui->button(m_desc.at("remove_all"));
m_imgui->disabled_end();
// Following is rendered in both editing and non-editing mode:
// m_imgui->text("");
ImGui::Separator();
if (m_c->object_clipper()->get_position() == 0.f) {
ImGui::AlignTextToFramePadding();
m_imgui->text(m_desc.at("clipping_of_view"));
}
else {
if (m_imgui->button(m_desc.at("reset_direction"))) {
wxGetApp().CallAfter([this](){
m_c->object_clipper()->set_position(-1., false);
});
}
}
ImGui::SameLine(settings_sliders_left, m_imgui->get_item_spacing().x);
ImGui::PushItemWidth(window_width - settings_sliders_left);
float clp_dist = m_c->object_clipper()->get_position();
if (m_imgui->slider_float("##clp_dist", &clp_dist, 0.f, 1.f, "%.2f"))
m_c->object_clipper()->set_position(clp_dist, true);
// make sure supports are shown/hidden as appropriate
bool show_sups = m_c->instances_hider()->are_supports_shown();
if (m_imgui->checkbox(m_desc["show_supports"], show_sups)) {
m_c->instances_hider()->show_supports(show_sups);
force_refresh = true;
}
m_imgui->end();
if (remove_selected || remove_all) {
force_refresh = false;
m_parent.set_as_dirty();
if (remove_all) {
select_point(AllPoints);
delete_selected_points();
}
if (remove_selected)
delete_selected_points();
if (first_run) {
first_run = false;
goto RENDER_AGAIN;
}
}
if (force_refresh)
m_parent.set_as_dirty();
if (config_changed)
m_parent.post_event(SimpleEvent(EVT_GLCANVAS_FORCE_UPDATE));
}
bool GLGizmoHollow::on_is_activable() const
{
const Selection& selection = m_parent.get_selection();
if (wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology() != ptSLA
|| !selection.is_from_single_instance())
return false;
// Check that none of the selected volumes is outside. Only SLA auxiliaries (supports) are allowed outside.
const Selection::IndicesList& list = selection.get_volume_idxs();
for (const auto& idx : list)
if (selection.get_volume(idx)->is_outside && selection.get_volume(idx)->composite_id.volume_id >= 0)
return false;
return true;
}
bool GLGizmoHollow::on_is_selectable() const
{
return (wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology() == ptSLA);
}
std::string GLGizmoHollow::on_get_name() const
{
return _u8L("Hollow and drill");
}
CommonGizmosDataID GLGizmoHollow::on_get_requirements() const
{
return CommonGizmosDataID(
int(CommonGizmosDataID::SelectionInfo)
| int(CommonGizmosDataID::InstancesHider)
| int(CommonGizmosDataID::Raycaster)
| int(CommonGizmosDataID::HollowedMesh)
| int(CommonGizmosDataID::ObjectClipper)
| int(CommonGizmosDataID::SupportsClipper));
}
void GLGizmoHollow::on_set_state()
{
if (m_state == m_old_state)
return;
if (m_state == Off && m_old_state != Off) // the gizmo was just turned Off
m_parent.post_event(SimpleEvent(EVT_GLCANVAS_FORCE_UPDATE));
m_old_state = m_state;
}
void GLGizmoHollow::on_start_dragging()
{
if (m_hover_id != -1) {
select_point(NoPoints);
select_point(m_hover_id);
m_hole_before_drag = m_c->selection_info()->model_object()->sla_drain_holes[m_hover_id].pos;
}
else
m_hole_before_drag = Vec3f::Zero();
}
void GLGizmoHollow::on_stop_dragging()
{
sla::DrainHoles& drain_holes = m_c->selection_info()->model_object()->sla_drain_holes;
if (m_hover_id != -1) {
Vec3f backup = drain_holes[m_hover_id].pos;
if (m_hole_before_drag != Vec3f::Zero() // some point was touched
&& backup != m_hole_before_drag) // and it was moved, not just selected
{
drain_holes[m_hover_id].pos = m_hole_before_drag;
Plater::TakeSnapshot snapshot(wxGetApp().plater(), "Move drainage hole");
drain_holes[m_hover_id].pos = backup;
}
}
m_hole_before_drag = Vec3f::Zero();
}
void GLGizmoHollow::on_load(cereal::BinaryInputArchive& ar)
{
ar(m_new_hole_radius,
m_new_hole_height,
m_selected,
m_selection_empty
);
}
void GLGizmoHollow::on_save(cereal::BinaryOutputArchive& ar) const
{
ar(m_new_hole_radius,
m_new_hole_height,
m_selected,
m_selection_empty
);
}
void GLGizmoHollow::select_point(int i)
{
const sla::DrainHoles& drain_holes = m_c->selection_info()->model_object()->sla_drain_holes;
if (i == AllPoints || i == NoPoints) {
m_selected.assign(m_selected.size(), i == AllPoints);
m_selection_empty = (i == NoPoints);
if (i == AllPoints) {
m_new_hole_radius = drain_holes[0].radius;
m_new_hole_height = drain_holes[0].height;
}
}
else {
while (size_t(i) >= m_selected.size())
m_selected.push_back(false);
m_selected[i] = true;
m_selection_empty = false;
m_new_hole_radius = drain_holes[i].radius;
m_new_hole_height = drain_holes[i].height;
}
}
void GLGizmoHollow::unselect_point(int i)
{
m_selected[i] = false;
m_selection_empty = true;
for (const bool sel : m_selected) {
if (sel) {
m_selection_empty = false;
break;
}
}
}
void GLGizmoHollow::reload_cache()
{
m_selected.clear();
m_selected.assign(m_c->selection_info()->model_object()->sla_drain_holes.size(), false);
}
void GLGizmoHollow::on_set_hover_id()
{
if (int(m_c->selection_info()->model_object()->sla_drain_holes.size()) <= m_hover_id)
m_hover_id = -1;
}
} // namespace GUI
} // namespace Slic3r
+111
View File
@@ -0,0 +1,111 @@
#ifndef slic3r_GLGizmoHollow_hpp_
#define slic3r_GLGizmoHollow_hpp_
#include "GLGizmoBase.hpp"
#include "slic3r/GUI/GLSelectionRectangle.hpp"
#include <libslic3r/SLA/Hollowing.hpp>
#include <libslic3r/ObjectID.hpp>
#include <wx/dialog.h>
#include <cereal/types/vector.hpp>
namespace Slic3r {
class ConfigOption;
class ConfigOptionDef;
namespace GUI {
enum class SLAGizmoEventType : unsigned char;
class GLGizmoHollow : public GLGizmoBase
{
private:
bool unproject_on_mesh(const Vec2d& mouse_pos, std::pair<Vec3f, Vec3f>& pos_and_normal);
public:
GLGizmoHollow(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id);
virtual ~GLGizmoHollow() = default;
void set_sla_support_data(ModelObject* model_object, const Selection& selection);
bool gizmo_event(SLAGizmoEventType action, const Vec2d& mouse_position, bool shift_down, bool alt_down, bool control_down);
void delete_selected_points();
bool is_selection_rectangle_dragging() const {
return m_selection_rectangle.is_dragging();
}
private:
bool on_init() override;
void on_update(const UpdateData& data) override;
void on_render() override;
void render_points(const Selection& selection, bool picking = false);
void hollow_mesh(bool postpone_error_messages = false);
bool unsaved_changes() const;
ObjectID m_old_mo_id = -1;
GLModel m_cylinder;
float m_new_hole_radius = 2.f; // Size of a new hole.
float m_new_hole_height = 6.f;
mutable std::vector<bool> m_selected; // which holes are currently selected
bool m_enable_hollowing = true;
// Stashes to keep data for undo redo. Is taken after the editing
// is done, the data are updated continuously.
float m_offset_stash = 3.0f;
float m_quality_stash = 0.5f;
float m_closing_d_stash = 2.f;
Vec3f m_hole_before_drag = Vec3f::Zero();
sla::DrainHoles m_holes_in_drilled_mesh;
sla::DrainHoles m_holes_stash;
// This map holds all translated description texts, so they can be easily referenced during layout calculations
// etc. When language changes, GUI is recreated and this class constructed again, so the change takes effect.
std::map<std::string, wxString> m_desc;
GLSelectionRectangle m_selection_rectangle;
bool m_wait_for_up_event = false;
bool m_selection_empty = true;
EState m_old_state = Off; // to be able to see that the gizmo has just been closed (see on_set_state)
std::vector<std::pair<const ConfigOption*, const ConfigOptionDef*>> get_config_options(const std::vector<std::string>& keys) const;
bool is_mesh_point_clipped(const Vec3d& point) const;
// Methods that do the model_object and editing cache synchronization,
// editing mode selection, etc:
enum {
AllPoints = -2,
NoPoints,
};
void select_point(int i);
void unselect_point(int i);
void reload_cache();
protected:
void on_set_state() override;
void on_set_hover_id() override;
void on_start_dragging() override;
void on_stop_dragging() override;
void on_render_input_window(float x, float y, float bottom_limit) override;
virtual CommonGizmosDataID on_get_requirements() const override;
std::string on_get_name() const override;
bool on_is_activable() const override;
bool on_is_selectable() const override;
void on_load(cereal::BinaryInputArchive& ar) override;
void on_save(cereal::BinaryOutputArchive& ar) const override;
};
} // namespace GUI
} // namespace Slic3r
#endif // slic3r_GLGizmoHollow_hpp_
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,159 @@
#ifndef slic3r_GLGizmoSlaSupports_hpp_
#define slic3r_GLGizmoSlaSupports_hpp_
#include "GLGizmoBase.hpp"
#include "slic3r/GUI/GLSelectionRectangle.hpp"
#include "libslic3r/SLA/SupportPoint.hpp"
#include "libslic3r/ObjectID.hpp"
#include <wx/dialog.h>
#include <cereal/types/vector.hpp>
namespace Slic3r {
class ConfigOption;
namespace GUI {
enum class SLAGizmoEventType : unsigned char;
class GLGizmoSlaSupports : public GLGizmoBase
{
private:
bool unproject_on_mesh(const Vec2d& mouse_pos, std::pair<Vec3f, Vec3f>& pos_and_normal);
const float RenderPointScale = 1.f;
class CacheEntry {
public:
CacheEntry() :
support_point(sla::SupportPoint()), selected(false), normal(Vec3f::Zero()) {}
CacheEntry(const sla::SupportPoint& point, bool sel = false, const Vec3f& norm = Vec3f::Zero()) :
support_point(point), selected(sel), normal(norm) {}
bool operator==(const CacheEntry& rhs) const {
return (support_point == rhs.support_point);
}
bool operator!=(const CacheEntry& rhs) const {
return ! ((*this) == rhs);
}
sla::SupportPoint support_point;
bool selected; // whether the point is selected
Vec3f normal;
template<class Archive>
void serialize(Archive & ar)
{
ar(support_point, selected, normal);
}
};
public:
GLGizmoSlaSupports(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id);
virtual ~GLGizmoSlaSupports() = default;
void set_sla_support_data(ModelObject* model_object, const Selection& selection);
bool gizmo_event(SLAGizmoEventType action, const Vec2d& mouse_position, bool shift_down, bool alt_down, bool control_down);
void delete_selected_points(bool force = false);
//ClippingPlane get_sla_clipping_plane() const;
bool is_in_editing_mode() const { return m_editing_mode; }
bool is_selection_rectangle_dragging() const { return m_selection_rectangle.is_dragging(); }
bool has_backend_supports() const;
void reslice_SLA_supports(bool postpone_error_messages = false) const;
bool wants_enter_leave_snapshots() const override { return true; }
std::string get_gizmo_entering_text() const override { return "Entering SLA support points"; }
std::string get_gizmo_leaving_text() const override { return "Leaving SLA support points"; }
private:
bool on_init() override;
void on_update(const UpdateData& data) override;
void on_render() override;
void render_points(const Selection& selection, bool picking = false);
bool unsaved_changes() const;
bool m_lock_unique_islands = false;
bool m_editing_mode = false; // Is editing mode active?
float m_new_point_head_diameter; // Size of a new point.
CacheEntry m_point_before_drag; // undo/redo - so we know what state was edited
float m_old_point_head_diameter = 0.; // the same
float m_minimal_point_distance_stash = 0.f; // and again
float m_density_stash = 0.f; // and again
mutable std::vector<CacheEntry> m_editing_cache; // a support point and whether it is currently selected
std::vector<sla::SupportPoint> m_normal_cache; // to restore after discarding changes or undo/redo
ObjectID m_old_mo_id;
GLModel m_cone;
GLModel m_cylinder;
GLModel m_sphere;
// This map holds all translated description texts, so they can be easily referenced during layout calculations
// etc. When language changes, GUI is recreated and this class constructed again, so the change takes effect.
std::map<std::string, wxString> m_desc;
GLSelectionRectangle m_selection_rectangle;
bool m_wait_for_up_event = false;
bool m_selection_empty = true;
EState m_old_state = Off; // to be able to see that the gizmo has just been closed (see on_set_state)
std::vector<const ConfigOption*> get_config_options(const std::vector<std::string>& keys) const;
bool is_mesh_point_clipped(const Vec3d& point) const;
bool is_point_in_hole(const Vec3f& pt) const;
//void find_intersecting_facets(const igl::AABB<Eigen::MatrixXf, 3>* aabb, const Vec3f& normal, double offset, std::vector<unsigned int>& out) const;
// Methods that do the model_object and editing cache synchronization,
// editing mode selection, etc:
enum {
AllPoints = -2,
NoPoints,
};
void select_point(int i);
void unselect_point(int i);
void editing_mode_apply_changes();
void editing_mode_discard_changes();
void reload_cache();
void get_data_from_backend();
void auto_generate();
void switch_to_editing_mode();
void disable_editing_mode();
void ask_about_changes_call_after(std::function<void()> on_yes, std::function<void()> on_no);
protected:
void on_set_state() override;
void on_set_hover_id() override
{
if (! m_editing_mode || (int)m_editing_cache.size() <= m_hover_id)
m_hover_id = -1;
}
void on_start_dragging() override;
void on_stop_dragging() override;
void on_render_input_window(float x, float y, float bottom_limit) override;
std::string on_get_name() const override;
bool on_is_activable() const override;
bool on_is_selectable() const override;
virtual CommonGizmosDataID on_get_requirements() const override;
void on_load(cereal::BinaryInputArchive& ar) override;
void on_save(cereal::BinaryOutputArchive& ar) const override;
};
class SlaGizmoHelpDialog : public wxDialog
{
public:
SlaGizmoHelpDialog();
};
} // namespace GUI
} // namespace Slic3r
#endif // slic3r_GLGizmoSlaSupports_hpp_
File diff suppressed because it is too large Load Diff
+126
View File
@@ -0,0 +1,126 @@
#ifndef slic3r_GLGizmoText_hpp_
#define slic3r_GLGizmoText_hpp_
#include "GLGizmoBase.hpp"
#include "slic3r/GUI/3DScene.hpp"
#include "../GLTexture.hpp"
#include "../Camera.hpp"
#include "libslic3r/Model.hpp"
namespace Slic3r {
enum class ModelVolumeType : int;
class ModelVolume;
namespace GUI {
enum class SLAGizmoEventType : unsigned char;
class GLGizmoText : public GLGizmoBase
{
private:
std::vector<std::string> m_avail_font_names;
char m_text[1024] = { 0 };
std::string m_font_name;
float m_font_size = 16.f;
int m_curr_font_idx = 0;
bool m_bold = true;
bool m_italic = false;
float m_thickness = 2.f;
float m_embeded_depth = 0.f;
float m_rotate_angle = 0;
float m_text_gap = 0.f;
bool m_is_surface_text = false;
bool m_keep_horizontal = false;
mutable RaycastResult m_rr;
float m_combo_height = 0.0f;
float m_combo_width = 0.0f;
float m_scale;
Vec2d m_mouse_position = Vec2d::Zero();
Vec2d m_origin_mouse_position = Vec2d::Zero();
bool m_shift_down = false;
class TextureInfo {
public:
GLTexture* texture { nullptr };
int h;
int w;
int hl;
std::string font_name;
};
std::vector<TextureInfo> m_textures;
std::vector<std::string> m_font_names;
bool m_is_modify = false;
bool m_need_update_text = false;
int m_object_idx = -1;
int m_volume_idx = -1;
int m_preview_text_volume_id = -1;
Vec3d m_mouse_position_world = Vec3d::Zero();
Vec3d m_mouse_normal_world = Vec3d::Zero();
Vec3d m_cut_plane_dir = Vec3d::UnitZ();
std::vector<Vec3d> m_position_points;
std::vector<Vec3d> m_normal_points;
// This map holds all translated description texts, so they can be easily referenced during layout calculations
// etc. When language changes, GUI is recreated and this class constructed again, so the change takes effect.
std::map<std::string, wxString> m_desc;
public:
GLGizmoText(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id);
~GLGizmoText();
void update_font_texture();
bool gizmo_event(SLAGizmoEventType action, const Vec2d &mouse_position, bool shift_down, bool alt_down, bool control_down);
bool on_mouse(const wxMouseEvent &mouse_event) override;
bool is_mesh_point_clipped(const Vec3d &point, const Transform3d &trafo) const;
BoundingBoxf3 bounding_box() const;
protected:
virtual bool on_init() override;
virtual std::string on_get_name() const override;
virtual bool on_is_activable() const override;
virtual void on_render() override;
virtual void on_dragging(const UpdateData &data) override;
void push_combo_style(const float scale);
void pop_combo_style();
void push_button_style(bool pressed);
void pop_button_style();
virtual void on_set_state() override;
virtual CommonGizmosDataID on_get_requirements() const override;
virtual void on_render_input_window(float x, float y, float bottom_limit);
virtual void on_register_raycasters_for_picking() override;
virtual void on_unregister_raycasters_for_picking() override;
void show_tooltip_information(float x, float y);
private:
ModelVolume *get_selected_single_volume(int& out_object_idx, int& out_volume_idx) const;
void reset_text_info();
bool update_text_positions(const std::vector<std::string>& texts);
TriangleMesh get_text_mesh(const char* text_str, const Vec3d &position, const Vec3d &normal, const Vec3d &text_up_dir);
bool update_raycast_cache(const Vec2d &mouse_position, const Camera &camera, const std::vector<Transform3d> &trafo_matrices);
void generate_text_volume(bool is_temp = true);
void delete_temp_preview_text_volume();
TextInfo get_text_info();
void load_from_text_info(const TextInfo &text_info);
};
} // namespace GUI
} // namespace Slic3r
#endif // slic3r_GLGizmoText_hpp_
+41
View File
@@ -0,0 +1,41 @@
#ifndef slic3r_GLGizmos_hpp_
#define slic3r_GLGizmos_hpp_
// this describes events being passed from GLCanvas3D to SlaSupport gizmo
namespace Slic3r {
namespace GUI {
enum class SLAGizmoEventType : unsigned char {
LeftDown = 1,
LeftUp,
RightDown,
Dragging,
Delete,
SelectAll,
ShiftUp,
AltUp,
ApplyChanges,
DiscardChanges,
AutomaticGeneration,
ManualEditing,
MouseWheelUp,
MouseWheelDown,
ResetClippingPlane
};
} // namespace GUI
} // namespace Slic3r
// BBS
#include "slic3r/GUI/Gizmos/GLGizmoMoveScale.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoRotate.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoFlatten.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoSlaSupports.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoFuzzySkin.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp"
// BBS
#include "slic3r/GUI/Gizmos/GLGizmoAdvancedCut.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoHollow.hpp"
#endif //slic3r_GLGizmos_hpp_
+60
View File
@@ -0,0 +1,60 @@
#include <I18N.hpp>
#include <wx/string.h>
#ifndef _L
#define _L(s) Slic3r::I18N::translate(s)
#endif
namespace ProfileDescrption {
const std::string PROFILE_DESCRIPTION_0 = _L("It has a small layer height. This results in almost negligible layer lines and high print quality. It is suitable for most printing cases.");
const std::string PROFILE_DESCRIPTION_1 = _L("Compared with the default profile of a 0.2 mm nozzle, it has lower speeds and acceleration, and the sparse infill pattern is Gyroid. This results in much higher print quality but a much longer print time.");
const std::string PROFILE_DESCRIPTION_2 = _L("Compared with the default profile of a 0.2 mm nozzle, it has a slightly bigger layer height. This results in almost negligible layer lines and slightly shorter print time.");
const std::string PROFILE_DESCRIPTION_3 = _L("Compared with the default profile of a 0.2 mm nozzle, it has a bigger layer height. This results in slightly visible layer lines but shorter print time.");
const std::string PROFILE_DESCRIPTION_4 = _L("Compared with the default profile of a 0.2 mm nozzle, it has a smaller layer height. This results in almost invisible layer lines and higher print quality but longer print time.");
const std::string PROFILE_DESCRIPTION_5 = _L("Compared with the default profile of a 0.2 mm nozzle, it has a smaller layer lines, lower speeds and acceleration, and the sparse infill pattern is Gyroid. This results in almost invisible layer lines and much higher print quality but much longer print time.");
const std::string PROFILE_DESCRIPTION_6 = _L("Compared with the default profile of a 0.2 mm nozzle, it has a smaller layer height. This results in minimal layer lines and higher print quality but longer print time.");
const std::string PROFILE_DESCRIPTION_7 = _L("Compared with the default profile of a 0.2 mm nozzle, it has a smaller layer lines, lower speeds and acceleration, and the sparse infill pattern is Gyroid. This results in minimal layer lines and much higher print quality but much longer print time.");
const std::string PROFILE_DESCRIPTION_8 = _L("It has a normal layer height. This results in average layer lines and print quality. It is suitable for most printing cases.");
const std::string PROFILE_DESCRIPTION_9 = _L("Compared with the default profile of a 0.4 mm nozzle, it has more wall loops and a higher sparse infill density. This results in higher print strength but more filament consumption and longer print time.");
const std::string PROFILE_DESCRIPTION_10 = _L("Compared with the default profile of a 0.4 mm nozzle, it has a bigger layer height. This results in more apparent layer lines and lower print quality, but slightly shorter print time.");
const std::string PROFILE_DESCRIPTION_11 = _L("Compared with the default profile of a 0.4 mm nozzle, it has a bigger layer height. This results in more apparent layer lines and lower print quality, but shorter print time.");
const std::string PROFILE_DESCRIPTION_12 = _L("Compared with the default profile of a 0.4 mm nozzle, it has a smaller layer height. This results in less apparent layer lines and higher print quality but longer print time.");
const std::string PROFILE_DESCRIPTION_13 = _L("Compared with the default profile of a 0.4 mm nozzle, it has a smaller layer height, lower speeds and acceleration, and the sparse infill pattern is Gyroid. This results in less apparent layer lines and much higher print quality but much longer print time.");
const std::string PROFILE_DESCRIPTION_14 = _L("Compared with the default profile of a 0.4 mm nozzle, it has a smaller layer height. This results in almost negligible layer lines and higher print quality but longer print time.");
const std::string PROFILE_DESCRIPTION_15 = _L("Compared with the default profile of a 0.4 mm nozzle, it has a smaller layer height, lower speeds and acceleration, and the sparse infill pattern is Gyroid. This results in almost negligible layer lines and much higher print quality but much longer print time.");
const std::string PROFILE_DESCRIPTION_16 = _L("Compared with the default profile of a 0.4 mm nozzle, it has a smaller layer height. This results in almost negligible layer lines and longer print time.");
const std::string PROFILE_DESCRIPTION_17 = _L("It has a big layer height. This results in apparent layer lines and ordinary print quality and print time.");
const std::string PROFILE_DESCRIPTION_18 = _L("Compared with the default profile of a 0.6 mm nozzle, it has more wall loops and a higher sparse infill density. This results in higher print strength but more filament consumption and longer print time.");
const std::string PROFILE_DESCRIPTION_19 = _L("Compared with the default profile of a 0.6 mm nozzle, it has a bigger layer height. This results in more apparent layer lines and lower print quality, but shorter print time in some cases.");
const std::string PROFILE_DESCRIPTION_20 = _L("Compared with the default profile of a 0.6 mm nozzle, it has a bigger layer height. This results in much more apparent layer lines and much lower print quality, but shorter print time in some cases.");
const std::string PROFILE_DESCRIPTION_21 = _L("Compared with the default profile of a 0.6 mm nozzle, it has a smaller layer height. This results in less apparent layer lines and slight higher print quality but longer print time.");
const std::string PROFILE_DESCRIPTION_22 = _L("Compared with the default profile of a 0.6 mm nozzle, it has a smaller layer height. This results in less apparent layer lines and higher print quality but longer print time.");
const std::string PROFILE_DESCRIPTION_23 = _L("It has a very big layer height. This results in very apparent layer lines, low print quality and shorter print time.");
const std::string PROFILE_DESCRIPTION_24 = _L("Compared with the default profile of a 0.8 mm nozzle, it has a bigger layer height. This results in very apparent layer lines and much lower print quality, but shorter print time in some cases.");
const std::string PROFILE_DESCRIPTION_25 = _L("Compared with the default profile of a 0.8 mm nozzle, it has a much bigger layer height. This results in extremely apparent layer lines and much lower print quality, but much shorter print time in some cases.");
const std::string PROFILE_DESCRIPTION_26 = _L("Compared with the default profile of a 0.8 mm nozzle, it has a slightly smaller layer height. This results in slightly less but still apparent layer lines and slightly higher print quality but longer print time in some cases.");
const std::string PROFILE_DESCRIPTION_27 = _L("Compared with the default profile of a 0.8 mm nozzle, it has a smaller layer height. This results in less but still apparent layer lines and slightly higher print quality but longer print time in some cases.");
const std::string PROFILE_DESCRIPTION_28 = _L("This is neither a commonly used filament, nor one of Bambu filaments, and it varies a lot from brand to brand. So, it's highly recommended to ask its vendor for suitable profile before printing and adjust some parameters according to its performances.");
const std::string PROFILE_DESCRIPTION_29 = _L("When printing this filament, there's a risk of warping and low layer adhesion strength. To get better results, please refer to this wiki: Printing Tips for High Temp / Engineering materials.");
const std::string PROFILE_DESCRIPTION_30 = _L("When printing this filament, there's a risk of nozzle clogging, oozing, warping and low layer adhesion strength. To get better results, please refer to this wiki: Printing Tips for High Temp / Engineering materials.");
const std::string PROFILE_DESCRIPTION_31 = _L("To get better transparent or translucent results with the corresponding filament, please refer to this wiki: Printing tips for transparent PETG.");
const std::string PROFILE_DESCRIPTION_32 = _L("To make the prints get higher gloss, please dry the filament before use, and set the outer wall speed to be 40 to 60 mm/s when slicing.");
const std::string PROFILE_DESCRIPTION_33 = _L("This filament is only used to print models with a low density usually, and some special parameters are required. To get better printing quality, please refer to this wiki: Instructions for printing RC model with foaming PLA (PLA Aero).");
const std::string PROFILE_DESCRIPTION_34 = _L("This filament is only used to print models with a low density usually, and some special parameters are required. To get better printing quality, please refer to this wiki: ASA Aero Printing Guide.");
const std::string PROFILE_DESCRIPTION_35 = _L("This filament is too soft and not compatible with the AMS. Printing it is of many requirements, and to get better printing quality, please refer to this wiki: TPU printing guide.");
const std::string PROFILE_DESCRIPTION_36 = _L("This filament has high enough hardness (about 67D) and is compatible with the AMS. Printing it is of many requirements, and to get better printing quality, please refer to this wiki: TPU printing guide.");
const std::string PROFILE_DESCRIPTION_37 = _L("If you are to print a kind of soft TPU, please don't slice with this profile, and it is only for TPU that has high enough hardness (not less than 55D) and is compatible with the AMS. To get better printing quality, please refer to this wiki: TPU printing guide.");
const std::string PROFILE_DESCRIPTION_38 = _L("This is a water-soluble support filament, and usually it is only for the support structure and not for the model body. Printing this filament is of many requirements, and to get better printing quality, please refer to this wiki: PVA Printing Guide.");
const std::string PROFILE_DESCRIPTION_39 = _L("This is a non-water-soluble support filament, and usually it is only for the support structure and not for the model body. To get better printing quality, please refer to this wiki: Printing Tips for Support Filament and Support Function.");
const std::string PROFILE_DESCRIPTION_40 = _L("The generic presets are conservatively tuned for compatibility with a wider range of filaments. For higher printing quality and speeds, please use Bambu filaments with Bambu presets.");
const std::string PROFILE_DESCRIPTION_41 = _L("High quality profile for 0.2mm nozzle, prioritizing print quality.");
const std::string PROFILE_DESCRIPTION_42 = _L("High quality profile for 0.16mm layer height, prioritizing print quality and strength.");
const std::string PROFILE_DESCRIPTION_43 = _L("Standard profile for 0.16mm layer height, prioritizing speed.");
const std::string PROFILE_DESCRIPTION_44 = _L("High quality profile for 0.2mm layer height, prioritizing strength and print quality.");
const std::string PROFILE_DESCRIPTION_45 = _L("Standard profile for 0.4mm nozzle, prioritizing speed.");
const std::string PROFILE_DESCRIPTION_46 = _L("High quality profile for 0.6mm nozzle, prioritizing print quality and strength.");
const std::string PROFILE_DESCRIPTION_47 = _L("Strength profile for 0.6mm nozzle, prioritizing strength.");
const std::string PROFILE_DESCRIPTION_48 = _L("Standard profile for 0.6mm nozzle, prioritizing speed.");
const std::string PROFILE_DESCRIPTION_49 = _L("High quality profile for 0.8mm nozzle, prioritizing print quality.");
const std::string PROFILE_DESCRIPTION_50 = _L("Strength profile for 0.8mm nozzle, prioritizing strength.");
const std::string PROFILE_DESCRIPTION_51 = _L("Standard profile for 0.8mm nozzle, prioritizing speed.");
}
+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"
+29 -10
View File
@@ -1,11 +1,13 @@
#!/usr/bin/env bash
# End-to-end check that the CLI loads a project's printer and process settings as the GUI does.
# End-to-end check that the CLI loads a project's printer, process and filament settings as the GUI does.
#
# The GUI takes every key a project does not list as changed from the project's current system preset:
# keys saved before an option existed, and keys holding an older system value. Keys the project lists
# in different_settings_to_system keep the project's value. A project is exported from the shipped
# Bambu Lab P1S presets; one printer key and one process key are removed, one printer key and one
# process key are changed without being listed, one key is changed and listed, and it is sliced again.
# Bambu Lab P1S presets with two filaments; one printer key and one process key are removed, one printer
# key, one process key and two filament keys (one per filament, one per extruder variant) are changed
# without being listed, one process key and the first filament's density are changed and listed, and it
# is sliced again: as is, with --uptodate, and with --load-filaments replacing only the second filament.
#
# usage: test_cli_project_missing_keys.sh <orca-slicer binary> <python3> <resources/profiles/BBL>
set -u
@@ -45,7 +47,7 @@ slice() {
slice base "$WORK/cube.stl" \
--load-settings "$PROFILES/machine/Bambu Lab P1S 0.4 nozzle.json;$PROFILES/process/0.20mm Standard @BBL X1C.json" \
--load-filaments "$PROFILES/filament/Bambu PLA Basic @BBL P1S 0.4 nozzle.json"
--load-filaments "$PROFILES/filament/Bambu PLA Basic @BBL P1S 0.4 nozzle.json;$PROFILES/filament/Bambu PLA Basic @BBL P1S 0.4 nozzle.json"
# The removed keys, with their option defaults from PrintConfig.cpp; stale keys changed without being
# listed as different, which must come back with the system value; and a listed key the project keeps.
@@ -67,30 +69,47 @@ with zipfile.ZipFile(src) as zin, zipfile.ZipFile(dst, "w", zipfile.ZIP_DEFLATED
for key in ("top_shell_layers", "extruder_clearance_height_to_rod"):
expected[key] = config[key]
config[key] = str(int(float(config[key])) + 1)
for key in ("filament_cost", "filament_max_volumetric_speed"):
expected[key] = config[key]
config[key] = [str(float(v) + 1) for v in config[key]]
expected["wall_loops"] = str(int(config["wall_loops"]) + 1)
config["wall_loops"] = expected["wall_loops"]
expected["filament_density"] = [str(float(config["filament_density"][0]) + 1)] + config["filament_density"][1:]
config["filament_density"] = [expected["filament_density"][0]] + [str(float(v) + 1) for v in config["filament_density"][1:]]
# One entry for the process, one per filament, one for the printer.
different = config["different_settings_to_system"]
different[0] = ";".join([k for k in different[0].split(";") if k] + ["wall_loops"])
different[1] = ";".join([k for k in different[1].split(";") if k] + ["filament_density"])
data = json.dumps(config, indent=4)
zout.writestr(item, data)
with open(dst + ".expected.json", "w") as f:
json.dump(expected, f)
EOF
slice project "$WORK/old.3mf"
"$PY" - "$WORK/project/out.3mf" "$WORK/old.3mf.expected.json" <<'EOF'
check() {
if ! "$PY" - "$WORK/$1/out.3mf" "$WORK/old.3mf.expected.json" "$1" <<'EOF'
import json, sys, zipfile
with zipfile.ZipFile(sys.argv[1]) as z:
config = json.loads(z.read("Metadata/project_settings.config"))
with open(sys.argv[2]) as f:
expected = json.load(f)
errors = ["%s is %r, want %r" % (key, config.get(key), want) for key, want in expected.items() if config.get(key) != want]
errors = ["%s: %s is %r, want %r" % (sys.argv[3], key, config.get(key), want) for key, want in expected.items() if config.get(key) != want]
for e in errors:
print("FAIL: " + e)
sys.exit(1 if errors else 0)
EOF
status=$?
[ "$status" -eq 0 ] || { tail -n 40 "$WORK/project/log"; exit 1; }
then
tail -n 40 "$WORK/$1/log"
exit 1
fi
}
slice project "$WORK/old.3mf"
check project
slice uptodate "$WORK/old.3mf" --uptodate
check uptodate
# The replaced second filament takes the system values the refresh would have given it.
slice partial "$WORK/old.3mf" --load-filaments ";$PROFILES/filament/Bambu PLA Basic @BBL P1S 0.4 nozzle.json"
check partial
echo "PASS"
@@ -7,6 +7,7 @@
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include <catch2/benchmark/catch_benchmark.hpp>
#include "libslic3r/AABBTreeLines.hpp"
#include "libslic3r/GCode.hpp"
#include "libslic3r/GCode/ExtrusionProcessor.hpp"
#include "libslic3r/GCodeReader.hpp"
#include "libslic3r/TriangleMesh.hpp"
@@ -15,6 +16,8 @@
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <cstring>
#include <functional>
#include "libslic3r/Line.hpp"
#include "libslic3r/Point.hpp"
@@ -538,6 +541,187 @@ TEST_CASE("A wall is left whole where neither its speed nor its cooling changes"
REQUIRE(points.size() == 3);
}
namespace {
// The caged overhang box, sliced, and a layer on its slope.
struct SlicedCage
{
Print print;
Model model;
const PrintObject *object{nullptr};
const Layer *layer{nullptr};
explicit SlicedCage(const DynamicPrintConfig &config = caged_overhang_config("classic"))
{
init_print(std::vector<TriangleMesh>{caged_overhang_mesh()}, print, model, config, nullptr, false);
print.process();
object = print.objects().front();
layer = object->get_layer(int(std::lround((caged_slope_z_min + caged_slope_z_max) / 2. / caged_layer_height)));
}
};
using Walls = std::vector<std::vector<ProcessedPoint>>;
// Estimates every wall of `layer` against whatever layer `estimator` was last prepared with before it.
Walls estimate_walls(ExtrusionQualityEstimator &estimator, const PrintObject *object, const Layer &layer)
{
const ConfigOptionPercents overlaps({90, 75, 50, 25, 13, 0});
const ConfigOptionFloatsOrPercents speeds({FloatOrPercent{100, true}, FloatOrPercent{50, true}, FloatOrPercent{30, true},
FloatOrPercent{20, true}, FloatOrPercent{10, true}, FloatOrPercent{5, true}});
Walls walls;
estimator.set_current_object(object);
for (const LayerRegion *region : layer.regions())
for_each_extrusion_path(region->perimeters, [&](const ExtrusionPath &path) {
if (is_perimeter(path.role()))
walls.push_back(estimator.estimate_extrusion_quality(path, overlaps, speeds, caged_outer_wall_speed, caged_outer_wall_speed,
true, 0.5f));
});
return walls;
}
uint32_t float_bits(float value)
{
uint32_t bits;
std::memcpy(&bits, &value, sizeof(bits));
return bits;
}
bool same_point(const ProcessedPoint &a, const ProcessedPoint &b)
{
return a.p == b.p && float_bits(a.speed) == float_bits(b.speed) && float_bits(a.overlap) == float_bits(b.overlap);
}
// Requires the walls to match point for point, bit for bit.
void check_identical(const Walls &actual, const Walls &expected)
{
REQUIRE(actual.size() == expected.size());
for (size_t wall = 0; wall < actual.size(); ++wall) {
INFO("wall " << wall);
REQUIRE(actual[wall].size() == expected[wall].size());
for (size_t i = 0; i < actual[wall].size(); ++i) {
const ProcessedPoint &a = actual[wall][i];
const ProcessedPoint &e = expected[wall][i];
INFO("point " << i << ": speed " << a.speed << " vs " << e.speed << ", overlap " << a.overlap << " vs " << e.overlap);
CHECK(a.p == e.p);
CHECK(float_bits(a.speed) == float_bits(e.speed));
CHECK(float_bits(a.overlap) == float_bits(e.overlap));
}
}
}
bool any_difference(const Walls &a, const Walls &b)
{
return !std::equal(a.begin(), a.end(), b.begin(), b.end(), [](const std::vector<ProcessedPoint> &wa, const std::vector<ProcessedPoint> &wb) {
return std::equal(wa.begin(), wa.end(), wb.begin(), wb.end(), same_point);
});
}
bool any_slowed(const Walls &walls)
{
return std::any_of(walls.begin(), walls.end(), [](const std::vector<ProcessedPoint> &wall) {
return std::any_of(wall.begin(), wall.end(), [](const ProcessedPoint &point) { return point.speed < caged_outer_wall_speed; });
});
}
} // namespace
TEST_CASE("Overhang data computed ahead of the generator gives the same wall speeds", "[ExtrusionProcessor]")
{
const SlicedCage cage;
REQUIRE(cage.layer->lower_layer != nullptr);
ExtrusionQualityEstimator queried;
queried.prepare_for_new_layer(cage.object, cage.layer->lower_layer);
queried.prepare_for_new_layer(cage.object, cage.layer);
const Walls expected = estimate_walls(queried, cage.object, *cage.layer);
REQUIRE(any_slowed(expected));
ExtrusionQualityEstimator precomputed;
precomputed.prepare_for_new_layer(cage.object, cage.layer->lower_layer);
precomputed.set_precomputed_layers({precompute_overhang_layer(cage.object, *cage.layer)});
precomputed.prepare_for_new_layer(cage.object, cage.layer);
check_identical(estimate_walls(precomputed, cage.object, *cage.layer), expected);
}
TEST_CASE("Overhang distances measured against another layer than the previous one are not used", "[ExtrusionProcessor]")
{
const SlicedCage cage;
const Layer *two_below = cage.layer->lower_layer->lower_layer;
REQUIRE(two_below != nullptr);
ExtrusionQualityEstimator queried;
queried.prepare_for_new_layer(cage.object, two_below);
queried.prepare_for_new_layer(cage.object, cage.layer);
const Walls expected = estimate_walls(queried, cage.object, *cage.layer);
ExtrusionQualityEstimator one_below;
one_below.prepare_for_new_layer(cage.object, cage.layer->lower_layer);
one_below.prepare_for_new_layer(cage.object, cage.layer);
REQUIRE(any_difference(estimate_walls(one_below, cage.object, *cage.layer), expected));
ExtrusionQualityEstimator precomputed;
precomputed.prepare_for_new_layer(cage.object, two_below);
precomputed.set_precomputed_layers({precompute_overhang_layer(cage.object, *cage.layer)});
precomputed.prepare_for_new_layer(cage.object, cage.layer);
check_identical(estimate_walls(precomputed, cage.object, *cage.layer), expected);
}
TEST_CASE("Overhang data computed for another layer is not used", "[ExtrusionProcessor]")
{
const SlicedCage cage;
const Layer *one_below = cage.layer->lower_layer;
REQUIRE(one_below != nullptr);
REQUIRE(one_below->lower_layer != nullptr);
ExtrusionQualityEstimator queried;
queried.prepare_for_new_layer(cage.object, one_below);
queried.prepare_for_new_layer(cage.object, cage.layer);
const Walls expected = estimate_walls(queried, cage.object, *cage.layer);
ExtrusionQualityEstimator two_below;
two_below.prepare_for_new_layer(cage.object, one_below->lower_layer);
two_below.prepare_for_new_layer(cage.object, cage.layer);
REQUIRE(any_difference(estimate_walls(two_below, cage.object, *cage.layer), expected));
ExtrusionQualityEstimator precomputed;
precomputed.set_precomputed_layers({precompute_overhang_layer(cage.object, *one_below)});
precomputed.prepare_for_new_layer(cage.object, one_below);
precomputed.prepare_for_new_layer(cage.object, cage.layer);
check_identical(estimate_walls(precomputed, cage.object, *cage.layer), expected);
}
TEST_CASE("Precomputed overhang data has the curled-line tree exactly when a region slows down for curled perimeters", "[ExtrusionProcessor]")
{
const bool slowdown = GENERATE(false, true);
DynamicPrintConfig config = caged_overhang_config("classic");
config.set_deserialize_strict("slowdown_for_curled_perimeters", slowdown ? "1" : "0");
const SlicedCage cage(config);
GCode::LayerToPrint layer;
layer.object_layer = cage.layer;
layer.original_object = cage.object;
const std::vector<PrecomputedOverhangLayer> precomputed = precompute_overhang_layers({layer}, false);
REQUIRE(precomputed.size() == 1);
CHECK((precomputed.front().lower_curled_lines != nullptr) == slowdown);
}
TEST_CASE("Caged external overhangs are slowed when printed by object or through the pressure equalizer", "[ExtrusionProcessor]")
{
const auto [key, value] = GENERATE(table<const char *, const char *>({
{"print_sequence", "by object"},
{"max_volumetric_extrusion_rate_slope", "10"},
}));
INFO(key << " = " << value);
DynamicPrintConfig config = caged_overhang_config("classic");
config.set_deserialize_strict(key, value);
Print print;
Model model;
init_print(std::vector<TriangleMesh>{caged_overhang_mesh()}, print, model, config, nullptr, false);
const std::vector<double> feed_rates = caged_slope_feed_rates(gcode(print));
info_feed_rates("caged slope", feed_rates);
REQUIRE_FALSE(feed_rates.empty());
REQUIRE(*std::max_element(feed_rates.begin(), feed_rates.end()) < caged_slow_speed * MM_PER_MIN);
}
TEST_CASE("Benchmark caged overhang interior sampling", "[ExtrusionProcessor][!benchmark]"){
const char* wall_generator = GENERATE("classic", "arachne");
+92
View File
@@ -4,7 +4,12 @@
#include <catch2/generators/catch_generators.hpp>
#include "libslic3r/libslic3r.h"
#include "libslic3r/GCode/GCodeProcessor.hpp"
#include "libslic3r/Utils.hpp"
#include "test_utils.hpp"
#include <fstream>
#include <sstream>
#include <string>
#include <vector>
@@ -79,3 +84,90 @@ TEST_CASE("Reserved keyword detection reports every offending line", "[GCodeProc
CHECK(tags.empty());
}
}
namespace {
// Closed outer-wall squares, each after a fast travel and before an inner-wall move, so the processor
// records seams and inserts actual speed moves. virtual_moves adds a VG1 move after each square.
void process_squares(int squares, GCodeProcessorResult &result, bool virtual_moves = false)
{
std::ostringstream gcode;
gcode << "M83\nG90\n";
for (int i = 0; i < squares; ++i) {
gcode << "G1 X10 Y10 Z" << 0.2 * (i + 1) << " F12000\n"
<< "; FEATURE: Outer wall\n"
<< "G1 X50 Y10 E2 F3000\nG1 X50 Y50 E2\nG1 X10 Y50 E2\nG1 X10 Y10 E2\n"
<< "; FEATURE: Inner wall\n"
<< "G1 X12 Y12 E0.1\nG1 X30 Y12 E1\n";
if (virtual_moves)
gcode << "VG1 X20 Y30 F12000\n";
}
FullPrintConfig config;
config.gcode_flavor.value = gcfMarlinFirmware;
// s_IsBBLPrinter selects the "; FEATURE: " role tags this G-code uses.
const bool was_bbl_printer = GCodeProcessor::s_IsBBLPrinter;
const ScopeGuard restore_bbl_printer([was_bbl_printer] { GCodeProcessor::s_IsBBLPrinter = was_bbl_printer; });
GCodeProcessor::s_IsBBLPrinter = true;
ScopedTemporaryFile temp(".gcode");
std::ofstream(temp.string()) << gcode.str();
GCodeProcessor processor;
processor.apply_config(config);
processor.process_file(temp.string());
result = std::move(processor.extract_result());
}
bool is_block_move(const GCodeProcessorResult::MoveVertex &move)
{
return !move.internal_only && (move.type == EMoveType::Extrude || move.type == EMoveType::Travel);
}
} // namespace
TEST_CASE("Actual speed moves are inserted on their block's segment just before its move", "[GCodeProcessor]")
{
// 60 squares take several planner passes, which remap the blocks kept between passes.
const int squares = GENERATE(10, 60);
const bool virtual_moves = GENERATE(false, true);
GCodeProcessorResult result;
process_squares(squares, result, virtual_moves);
const auto &moves = result.moves;
constexpr size_t normal = size_t(PrintEstimatedStatistics::ETimeMode::Normal);
size_t inserted = 0;
for (size_t i = 1; i < moves.size(); ++i) {
if (!moves[i].internal_only)
continue;
++inserted;
// Inserted moves have zero time, but a VG1 block's time is written to whatever move its move_id names.
if (!virtual_moves)
CHECK(moves[i].time[normal] == 0.f);
size_t block = i + 1;
while (block < moves.size() && moves[block].internal_only)
++block;
size_t previous = i - 1;
while (previous > 0 && moves[previous].internal_only)
--previous;
REQUIRE(block < moves.size());
CHECK(moves[block].gcode_id == moves[i].gcode_id);
const Vec3f segment = moves[block].position - moves[previous].position;
const Vec3f offset = moves[i].position - moves[previous].position;
CHECK(segment.cross(offset).norm() / segment.norm() < 1e-3f);
}
REQUIRE(inserted > 0);
}
TEST_CASE("A seam takes the actual speed of the move it follows", "[GCodeProcessor]")
{
GCodeProcessorResult result;
// 10 squares fit in one planner pass, so the seam's move and the block after it are timed together.
process_squares(10, result);
const auto &moves = result.moves;
size_t seams = 0;
for (size_t i = 1; i < moves.size(); ++i)
if (moves[i].type == EMoveType::Seam && is_block_move(moves[i - 1])) {
++seams;
CHECK_THAT(moves[i].actual_feedrate, Catch::Matchers::WithinAbs(moves[i - 1].actual_feedrate, 1e-4));
}
REQUIRE(seams > 0);
}
+120 -36
View File
@@ -22,6 +22,7 @@
#include "libslic3r/Print.hpp"
#include <limits>
#include <optional>
#include <regex>
#include <set>
#include <sstream>
#include <string>
@@ -842,6 +843,47 @@ TEST_CASE("Each filament sets the pressure advance of its extruder variant", "[M
}
}
// A two-extruder printer, Standard nozzle on extruder 1 and High Flow on extruder 2, whose per-variant arrays
// hold filament 1 Standard, filament 1 High Flow, filament 2 Standard and filament 2 High Flow.
static DynamicPrintConfig two_extruder_pressure_advance_config(const std::string &filament_map, const std::string &adaptive_pressure_advance,
int wall_filament, int infill_filament)
{
DynamicPrintConfig config = multifilament_config(2, {
{ "gcode_flavor", "klipper" },
{ "single_extruder_multi_material", 0 },
{ "nozzle_diameter", "0.4,0.4" },
{ "extruder_printable_height", "0,0" },
{ "printer_extruder_id", "1,2" },
{ "printer_extruder_variant", "Direct Drive Standard;Direct Drive High Flow" },
{ "extruder_variant_list", "Direct Drive Standard;Direct Drive High Flow" },
{ "filament_map", filament_map },
{ "filament_extruder_variant", "Direct Drive Standard;Direct Drive High Flow;Direct Drive Standard;Direct Drive High Flow" },
{ "filament_self_index", "1,1,2,2" },
{ "enable_pressure_advance", "1,1,1,1" },
{ "pressure_advance", "0.021,0.037,0.043,0.049" },
{ "adaptive_pressure_advance", adaptive_pressure_advance },
{ "sparse_infill_filament_id", infill_filament },
{ "internal_solid_filament_id", infill_filament },
{ "top_surface_filament_id", infill_filament },
{ "bottom_surface_filament_id", infill_filament },
{ "outer_wall_filament_id", wall_filament },
{ "inner_wall_filament_id", wall_filament },
{ "enable_prime_tower", 0 },
{ "skirt_loops", 0 },
{ "brim_type", "no_brim" },
// custom G-code indexes the per-filament arrays by filament
{ "machine_start_gcode", "; start pressure advance {pressure_advance[initial_extruder]}" },
});
config.option<ConfigOptionStrings>("adaptive_pressure_advance_model")->values = {
constant_pressure_advance_model("0.012"), constant_pressure_advance_model("0.034"),
constant_pressure_advance_model("0.056"), constant_pressure_advance_model("0.078") };
config.option<ConfigOptionEnumsGeneric>("extruder_type", true)->values = { etDirectDrive, etDirectDrive };
config.option<ConfigOptionEnumsGeneric>("nozzle_volume_type", true)->values = { nvtStandard, nvtHighFlow };
// print each filament on the extruder filament_map gives it
config.option<ConfigOptionEnum<FilamentMapMode>>("filament_map_mode", true)->value = fmmManual;
return config;
}
// On a printer with two extruders, a filament takes the pressure advance of the variant of the extruder
// it is mapped to, whichever filament and extruder that is.
TEST_CASE("Each filament sets the pressure advance of its extruder variant on a two-extruder printer", "[MultiFilament]")
@@ -857,42 +899,8 @@ TEST_CASE("Each filament sets the pressure advance of its extruder variant on a
// the other filament goes on the other extruder
const std::string filament_map = filament == 1 ? std::to_string(extruder) + "," + std::to_string(3 - extruder) :
std::to_string(3 - extruder) + "," + std::to_string(extruder);
DynamicPrintConfig config = multifilament_config(2, {
{ "gcode_flavor", "klipper" },
{ "single_extruder_multi_material", 0 },
{ "nozzle_diameter", "0.4,0.4" },
{ "extruder_printable_height", "0,0" },
// extruder 1 has a Standard nozzle, extruder 2 a High Flow one
{ "printer_extruder_id", "1,2" },
{ "printer_extruder_variant", "Direct Drive Standard;Direct Drive High Flow" },
{ "extruder_variant_list", "Direct Drive Standard;Direct Drive High Flow" },
{ "filament_map", filament_map },
// both filaments define Standard and High Flow
{ "filament_extruder_variant", "Direct Drive Standard;Direct Drive High Flow;Direct Drive Standard;Direct Drive High Flow" },
{ "filament_self_index", "1,1,2,2" },
{ "enable_pressure_advance", "1,1,1,1" },
{ "pressure_advance", "0.021,0.037,0.043,0.049" },
{ "adaptive_pressure_advance", adaptive ? "1,1,1,1" : "0,0,0,0" },
{ "sparse_infill_filament_id", filament },
{ "internal_solid_filament_id", filament },
{ "top_surface_filament_id", filament },
{ "bottom_surface_filament_id", filament },
{ "outer_wall_filament_id", filament },
{ "inner_wall_filament_id", filament },
{ "enable_prime_tower", 0 },
{ "skirt_loops", 0 },
{ "brim_type", "no_brim" },
// custom G-code indexes the per-filament arrays by filament
{ "machine_start_gcode", "; start pressure advance {pressure_advance[initial_extruder]}" },
});
config.option<ConfigOptionStrings>("adaptive_pressure_advance_model")->values = {
constant_pressure_advance_model("0.012"), constant_pressure_advance_model("0.034"),
constant_pressure_advance_model("0.056"), constant_pressure_advance_model("0.078") };
config.option<ConfigOptionEnumsGeneric>("extruder_type", true)->values = { etDirectDrive, etDirectDrive };
config.option<ConfigOptionEnumsGeneric>("nozzle_volume_type", true)->values = { nvtStandard, nvtHighFlow };
// keep the mapping above rather than grouping the filaments automatically
config.option<ConfigOptionEnum<FilamentMapMode>>("filament_map_mode", true)->value = fmmManual;
const std::string gcode = slice({ cube(20) }, config);
const std::string gcode = slice({ cube(20) },
two_extruder_pressure_advance_config(filament_map, adaptive ? "1,1,1,1" : "0,0,0,0", filament, filament));
std::set<std::string> expected{ pressure_advance };
if (adaptive)
@@ -902,6 +910,82 @@ TEST_CASE("Each filament sets the pressure advance of its extruder variant on a
}
}
// Filament 1 prints the walls on extruder 1 (variant index 0), filament 2 the infill on extruder 2 (variant index 3).
TEST_CASE("Adaptive pressure advance on one extruder leaves the other extruder's pressure advance alone", "[MultiFilament]")
{
auto [adaptive, expected] = GENERATE(table<std::string, std::set<std::string>>({
{ "1,0,0,0", { "0.021", "0.049", "0.012" } },
{ "0,0,0,1", { "0.021", "0.049", "0.078" } },
}));
DYNAMIC_SECTION("adaptive " << adaptive) {
const std::string gcode = slice({ cube(20) }, two_extruder_pressure_advance_config("1,2", adaptive, 1, 2));
CHECK(pressure_advance_values(gcode) == expected);
}
}
// The pressure advance values a Klipper G-code sets while `tool` is active, in order, without repeating the value already set.
static std::vector<std::string> pressure_advance_sequence(const std::string &gcode, int tool)
{
const std::string token = "SET_PRESSURE_ADVANCE ADVANCE=";
std::vector<std::string> values;
int current_tool = 0;
std::istringstream stream(gcode);
for (std::string line; std::getline(stream, line);) {
if (line.size() > 1 && line[0] == 'T' && std::isdigit((unsigned char) line[1]))
current_tool = std::stoi(line.substr(1));
else if (current_tool == tool && line.rfind(token, 0) == 0) {
std::string value = line.substr(token.size(), line.find(';') - token.size());
if (values.empty() || values.back() != value)
values.push_back(std::move(value));
}
}
return values;
}
TEST_CASE("Adaptive pressure advance predicts the same values after layers only a non-adaptive extruder prints", "[MultiFilament]")
{
auto slice_with = [](const std::string &adaptive) {
DynamicPrintConfig config = two_extruder_pressure_advance_config("1,2", adaptive, 1, 1);
config.set_deserialize_strict({
{ "print_sequence", "by object" },
// extruder 2 moves at one speed on its first layer, so no G1 F follows its first PA_CHANGE tag
{ "filament_max_volumetric_speed", "100" },
{ "slow_down_for_layer_cooling", "0" },
{ "travel_speed", "120,40" },
{ "retraction_speed", "30,40" },
{ "deretraction_speed", "30,40" },
{ "initial_layer_speed", "30,40" },
{ "initial_layer_infill_speed", "60,40" },
});
auto &models = config.option<ConfigOptionStrings>("adaptive_pressure_advance_model")->values;
// with adaptive pressure advance on, filament 1 gets PA_CHANGE tags on every layer but keeps its pressure advance
models[0] = constant_pressure_advance_model("0.021");
// a prediction that rises with flow, so it depends on the print speed
models[3] = "0.01,1,1000\n0.09,40,1000\n0.01,1,100000\n0.09,40,100000";
return slice_with_object_overrides({ cube(20), cube(20) }, config,
{ {}, { { "outer_wall_filament_id", 2 }, { "inner_wall_filament_id", 2 }, { "sparse_infill_filament_id", 2 },
{ "internal_solid_filament_id", 2 }, { "top_surface_filament_id", 2 }, { "bottom_surface_filament_id", 2 } } });
};
const std::vector<std::string> expected = pressure_advance_sequence(slice_with("1,0,0,1"), 1);
REQUIRE(expected.size() > 2);
CHECK(pressure_advance_sequence(slice_with("0,0,0,1"), 1) == expected);
}
TEST_CASE("Adaptive pressure advance on an unused extruder variant leaves the G-code unchanged", "[MultiFilament]")
{
const std::string adaptive = GENERATE("0,1,0,0", "0,0,1,0");
DYNAMIC_SECTION("adaptive " << adaptive) {
// the time and object ids differ between any two slices, and the config block lists the setting itself
auto masked = [](const std::string &gcode) {
return std::regex_replace(gcode.substr(0, gcode.find("; CONFIG_BLOCK_START")), std::regex("; generated by .*| id:\\d+"), "");
};
const std::string reference = masked(slice({ cube(20) }, two_extruder_pressure_advance_config("1,2", "0,0,0,0", 1, 2)));
const std::string gcode = masked(slice({ cube(20) }, two_extruder_pressure_advance_config("1,2", adaptive, 1, 2)));
REQUIRE(reference.find("SET_PRESSURE_ADVANCE") != std::string::npos);
CHECK(gcode == reference);
}
}
// The speeds, in percent, a G-code turns a fan on at: the part cooling fan for `M106 S`, the auxiliary
// fan for `M106 P2 S`.
static std::set<int> fan_speeds(const std::string &gcode, const std::string &command)
+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);
}
+19
View File
@@ -69,3 +69,22 @@ TEST_CASE("a setter nested in another sets C again when the locale changed betwe
CHECK_FALSE(is_decimal_separator_point());
}
}
TEST_CASE("atof_decimal_point parses what atof parses in the C locale", "[LocalesUtils]")
{
const auto [text, value] = GENERATE(table<const char*, double>({
{ "5", 5. },
{ " 12.5", 12.5 },
{ "\t+3", 3. },
{ "\r\n7", 7. },
{ "-1.25", -1.25 },
{ "1e2", 100. },
{ ".5", 0.5 },
{ "12.5;comment", 12.5 },
{ "+-5", 0. },
{ "", 0. },
{ "abc", 0. },
}));
INFO(text);
CHECK_THAT(atof_decimal_point(text), Catch::Matchers::WithinAbs(value, 1e-12));
}
+12
View File
@@ -509,3 +509,15 @@ TEST_CASE("is_safe_to_open_file_name rejects programs and anything it does not k
INFO(unsafe);
CHECK_FALSE(is_safe_to_open_file_name(unsafe));
}
TEST_CASE("ascii_iequals compares ASCII letters regardless of case", "[Utils]") {
CHECK(ascii_iequals("set_velocity_limit", "SET_VELOCITY_LIMIT"));
CHECK(ascii_iequals("G28", "g28"));
CHECK(ascii_iequals("", ""));
CHECK_FALSE(ascii_iequals("G28", "G29"));
CHECK_FALSE(ascii_iequals("G2", "G28"));
CHECK_FALSE(ascii_iequals("G28", "G2"));
// Non-letters 0x20 apart are not equal.
CHECK_FALSE(ascii_iequals("[", "{"));
CHECK_FALSE(ascii_iequals("@", "`"));
}