Compare commits

..
Author SHA1 Message Date
Hanif Koh 0d80446301 Add Missing Includes to Code Merged Since the Include Cleanup 2026-10-04 04:40:47 +08:00
HanifKoh d1a3ef68c5 Fix CLI Crashes on Malformed Project, Assemble List and No-Input Runs (#15978)
* Fix CLI Crashes on Malformed Project, Assemble List and No-Input Runs

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

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

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

* Move the Assemble List Parser into libslic3r

Behaviour-preserving move of the --load-assemble-list JSON parser and
its plate/object structs from the CLI into libslic3r/Format/AssembleList,
so the format can be unit tested. The parser returns its own
AssembleListResult and takes the plate limit as a parameter; CLI::run
maps the result to the same exit codes as before. Every validation rule
and log message is unchanged.

Adds Catch2 coverage of the valid layout and each validation rule.

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

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

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

The flag now only gates writing: a read-only load reads caches and
writes none. The filament library is also read from its cache whenever
that is all that is installed, so a vendor updated over the air still
resolves against it.
2026-10-04 00:40:38 +08:00
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
68 changed files with 2591 additions and 1977 deletions
-17
View File
@@ -62,23 +62,6 @@ Clipper2 behaves differently by default, the wrapper adjusts it.
clip do not leave slivers.
- Open polylines are clipped with the non-zero rule and keep their direction.
### Tiled booleans
The sweep slows down with the number of edges crossing a scan line, so a layer
cut into thousands of pieces makes every whole-layer boolean expensive.
`diff_ex_by_piece()` and `intersection_ex_by_piece()` take a subject of
non-overlapping `ExPolygons`, group them into tiles with
`ClipperUtils::tile_expolygons()`, and run each tile in parallel against only
the clip polygons near it, cut to the tile's box. Below 128 pieces there is a
single tile, and they are the plain `diff_ex()` / `intersection_ex()`.
The result covers the same area as the plain call. Without the safety offset
the rings are the same. With it, each tile unites only the clip polygons near
it, so a clip edge that the whole-layer union splits where it crosses a distant
clip polygon stays whole, and a crossing with the subject can round 1 unit
differently. The tiles' results are concatenated in tile order, so the order of
the output `ExPolygons` differs from the plain call.
### Offsets
- Before offsetting, input vertices closer than
+62 -169
View File
@@ -37,6 +37,7 @@
#include "libslic3r/TriangleSelector.hpp"
#include <algorithm>
#include "libslic3r/Format/bbs_3mf.hpp"
#include "libslic3r/Format/AssembleList.hpp"
#include <cstdlib>
#include <stdlib.h>
#include <stdexcept>
@@ -685,162 +686,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 +1751,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 +1780,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 +1917,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 +2678,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 +3399,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 +3458,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 +3469,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 +3546,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 +4736,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 +5681,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
-68
View File
@@ -9,8 +9,6 @@
#include <numeric>
#include <unordered_map>
#include <tbb/parallel_for.h>
#include "ClipperUtils.hpp"
#include "BoundingBox.hpp"
#include "ExPolygon.hpp"
@@ -802,72 +800,6 @@ Slic3r::ExPolygons intersection_ex(const Slic3r::Surfaces &subject, const Slic3r
{ return _clipper_ex(ctIntersection, ClipperUtils::SurfacesProvider(subject), ClipperUtils::SurfacesProvider(clip), do_safety_offset); }
Slic3r::ExPolygons intersection_ex(const Slic3r::SurfacesPtr &subject, const Slic3r::ExPolygons &clip, ApplySafetyOffset do_safety_offset)
{ return _clipper_ex(ctIntersection, ClipperUtils::SurfacesPtrProvider(subject), ClipperUtils::ExPolygonsProvider(clip), do_safety_offset); }
namespace ClipperUtils {
std::vector<ExPolygonsTile> tile_expolygons(const ExPolygons &expolygons, size_t per_tile)
{
BoundingBox extent;
std::vector<BoundingBox> bboxes;
bboxes.reserve(expolygons.size());
for (const ExPolygon &expoly : expolygons) {
bboxes.emplace_back(get_extents(expoly));
extent.merge(bboxes.back());
}
if (! extent.defined)
return {};
const int tiles = std::clamp(int(std::sqrt(double(expolygons.size()) / double(std::max<size_t>(per_tile, 1)))), 1, 32);
const Point size = extent.size();
const coord_t tile_w = std::max<coord_t>(1, size.x() / tiles + 1), tile_h = std::max<coord_t>(1, size.y() / tiles + 1);
std::vector<ExPolygonsTile> out(size_t(tiles * tiles));
for (size_t i = 0; i < expolygons.size(); ++ i) {
const Point c = bboxes[i].center();
ExPolygonsTile &tile = out[size_t(std::clamp(int((c.y() - extent.min.y()) / tile_h), 0, tiles - 1) * tiles +
std::clamp(int((c.x() - extent.min.x()) / tile_w), 0, tiles - 1))];
tile.members.emplace_back(i);
tile.bbox.merge(bboxes[i]);
}
out.erase(std::remove_if(out.begin(), out.end(), [](const ExPolygonsTile &tile) { return tile.members.empty(); }), out.end());
return out;
}
}
static Slic3r::ExPolygons clipper_ex_by_piece(ClipType clipType, const Slic3r::ExPolygons &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset)
{
// A few dozen subject ExPolygons to a tile, each tile one Clipper call with the clip cut to the tile's box.
const std::vector<ClipperUtils::ExPolygonsTile> tiles = ClipperUtils::tile_expolygons(subject, 32);
// One tile is the plain call: cutting the clip would only cost time.
if (tiles.size() <= 1)
return _clipper_ex(clipType, ClipperUtils::ExPolygonsProvider(subject), ClipperUtils::PolygonsProvider(clip), do_safety_offset);
std::vector<BoundingBox> clip_bboxes;
clip_bboxes.reserve(clip.size());
for (const Polygon &polygon : clip)
clip_bboxes.emplace_back(get_extents(polygon));
std::vector<Slic3r::ExPolygons> out_tiles(tiles.size());
tbb::parallel_for(size_t(0), tiles.size(), [&](size_t tile_idx) {
const ClipperUtils::ExPolygonsTile &tile = tiles[tile_idx];
Slic3r::ExPolygons local_subject;
local_subject.reserve(tile.members.size());
for (size_t i : tile.members)
local_subject.emplace_back(subject[i]);
// Grown so that the cut edges of the clip stay clear of the subject, also after the safety offset.
const BoundingBox bbox = tile.bbox.inflated(SCALED_EPSILON);
Polygons local_clip;
for (size_t i = 0; i < clip.size(); ++i)
if (clip_bboxes[i].overlap(bbox))
if (Polygon clipped = ClipperUtils::clip_clipper_polygon_with_subject_bbox(clip[i], bbox); ! clipped.empty())
local_clip.emplace_back(std::move(clipped));
out_tiles[tile_idx] = _clipper_ex(clipType, ClipperUtils::ExPolygonsProvider(local_subject), ClipperUtils::PolygonsProvider(local_clip), do_safety_offset);
});
Slic3r::ExPolygons out;
for (Slic3r::ExPolygons &out_tile : out_tiles)
append(out, std::move(out_tile));
return out;
}
Slic3r::ExPolygons diff_ex_by_piece(const Slic3r::ExPolygons &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset)
{ return clipper_ex_by_piece(ctDifference, subject, clip, do_safety_offset); }
Slic3r::ExPolygons intersection_ex_by_piece(const Slic3r::ExPolygons &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset)
{ return clipper_ex_by_piece(ctIntersection, subject, clip, do_safety_offset); }
// May be used to "heal" unusual models (3DLabPrints etc.) by providing fill_type (pftEvenOdd, pftNonZero, pftPositive, pftNegative).
Slic3r::ExPolygons union_ex(const Slic3r::Polygons &subject, PolyFillType fill_type)
{ return _clipper_ex(ctUnion, ClipperUtils::PolygonsProvider(subject), ClipperUtils::EmptyPathsProvider(), ApplySafetyOffset::No, fill_type); }
-15
View File
@@ -5,7 +5,6 @@
#include "Polyline.hpp"
#include "Line.hpp"
#include "libslic3r.h"
#include "BoundingBox.hpp"
#include "ExPolygon.hpp"
#include "Polygon.hpp"
#include "Surface.hpp"
@@ -338,15 +337,6 @@ namespace ClipperUtils {
[[nodiscard]] Polygons clip_clipper_polygons_with_subject_bbox(const ExPolygon &src, const BoundingBox &bbox, const bool get_entire_polygons = false);
[[nodiscard]] Polygons clip_clipper_polygons_with_subject_bbox(const ExPolygons &src, const BoundingBox &bbox, const bool get_entire_polygons = false);
// Splits ExPolygons into tiles by the centres of their boxes, about `per_tile` of them to a tile, to run Clipper on a
// layer of many pieces tile by tile. Returns the non-empty tiles, each with the indices of its ExPolygons and their box.
struct ExPolygonsTile
{
BoundingBox bbox;
std::vector<size_t> members;
};
[[nodiscard]] std::vector<ExPolygonsTile> tile_expolygons(const ExPolygons &expolygons, size_t per_tile);
}
// offset Polygons
@@ -542,11 +532,6 @@ Slic3r::ExPolygons intersection_ex(const Slic3r::Surfaces &subject, const Slic3r
Slic3r::ExPolygons intersection_ex(const Slic3r::Surfaces &subject, const Slic3r::ExPolygons &clip, ApplySafetyOffset do_safety_offset = ApplySafetyOffset::No);
Slic3r::ExPolygons intersection_ex(const Slic3r::Surfaces &subject, const Slic3r::Surfaces &clip, ApplySafetyOffset do_safety_offset = ApplySafetyOffset::No);
Slic3r::ExPolygons intersection_ex(const Slic3r::SurfacesPtr &subject, const Slic3r::ExPolygons &clip, ApplySafetyOffset do_safety_offset = ApplySafetyOffset::No);
// diff_ex() / intersection_ex() of the subject split into tiles, each against only the part of the clip near it, the tiles in
// parallel. The same area as the operation on the whole subject when its ExPolygons do not overlap, and much faster for a
// subject of thousands of pieces spread over a layer: Clipper slows down with the number of edges crossing a scan line.
Slic3r::ExPolygons diff_ex_by_piece(const Slic3r::ExPolygons &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset = ApplySafetyOffset::No);
Slic3r::ExPolygons intersection_ex_by_piece(const Slic3r::ExPolygons &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset = ApplySafetyOffset::No);
Slic3r::Polylines intersection_pl(const Slic3r::Polylines &subject, const Slic3r::Polygon &clip);
Slic3r::Polylines intersection_pl(const Slic3r::Polyline &subject, const Slic3r::ExPolygon &clip);
Slic3r::Polylines intersection_pl(const Slic3r::Polylines &subject, const Slic3r::ExPolygon &clip);
-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
+12 -31
View File
@@ -22,8 +22,6 @@
#include "../PrintConfig.hpp"
#include "../Surface.hpp"
#include <tbb/parallel_for.h>
#include "AABBTreeLines.hpp"
#include "libslic3r/Config.hpp"
#include "libslic3r/ExPolygon.hpp"
@@ -654,28 +652,24 @@ void split_solid_surface(size_t layer_id, const SurfaceFill &fill, ExPolygons &n
if (!line_based_pattern) {
const coord_t scaled_spacing = scaled<coord_t>(fill.params.spacing);
// Each expolygon is split on its own, so they run in parallel and are collected in their original order.
std::vector<std::pair<ExPolygons, ExPolygons>> split_parts(fill.expolygons.size()); // normal, narrow
tbb::parallel_for(size_t(0), fill.expolygons.size(), [&](size_t idx) {
const ExPolygon &expolygon = fill.expolygons[idx];
for (const ExPolygon &expolygon : fill.expolygons) {
Polygons filled_area = to_polygons(expolygon);
// "Core" area: open (erode+dilate) to drop thin features, then clamp back to the original polygon.
Polygons inner_area = intersection(filled_area, opening(filled_area, scaled_spacing, scaled_spacing));
if (inner_area.empty()) {
split_parts[idx].second.emplace_back(expolygon);
return;
narrow_infill.emplace_back(expolygon);
continue;
}
ExPolygons inner_ex = union_ex(inner_area);
ExPolygons expolys{expolygon};
split_parts[idx].second = diff_ex(expolys, inner_ex); // narrow infill area
split_parts[idx].first = intersection_ex(expolys, inner_ex); // normal infill area
});
for (auto &[normal_ex, narrow_ex] : split_parts) {
append(normal_infill, std::move(normal_ex));
append(narrow_infill, std::move(narrow_ex));
ExPolygons narrow_ex = diff_ex(expolys, inner_ex);
ExPolygons normal_ex = intersection_ex(expolys, inner_ex);
append(normal_infill, normal_ex); // normal infill area
append(narrow_infill, narrow_ex); // narrow infill area
}
return;
@@ -697,10 +691,7 @@ void split_solid_surface(size_t layer_id, const SurfaceFill &fill, ExPolygons &n
}
const double aligning_angle = -base_angle + PI;
// Each expolygon is reconstructed on its own, so they run in parallel and are collected in their original order.
std::vector<Polygons> split_reconstructed(fill.expolygons.size());
tbb::parallel_for(size_t(0), fill.expolygons.size(), [&](size_t expolygon_idx) {
const ExPolygon &expolygon = fill.expolygons[expolygon_idx];
for (const ExPolygon &expolygon : fill.expolygons) {
Polygons filled_area = to_polygons(expolygon);
polygons_rotate(filled_area, aligning_angle);
BoundingBox bb = get_extents(filled_area);
@@ -831,10 +822,8 @@ void split_solid_surface(size_t layer_id, const SurfaceFill &fill, ExPolygons &n
}
}
split_reconstructed[expolygon_idx] = std::move(reconstructed_area);
});
for (Polygons &reconstructed_area : split_reconstructed)
polygons_append(normal_fill_areas, std::move(reconstructed_area));
polygons_append(normal_fill_areas, reconstructed_area);
}
polygons_rotate(normal_fill_areas, -aligning_angle);
@@ -1442,15 +1431,7 @@ void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive:
// Orca: Reuse the body origin used for bridge anchoring, resetting it for each surface.
f->set_bounding_box(infill_bounding_box(*this, surface_fill, expoly, bbox));
// Only the part of the layer-wide no-overlap area under this expolygon matters, so clip it to the
// expolygon's box first (padded past the safety offset, which grows the clip side). The result is
// identical; the cost is not: a layer split into many small fills, e.g. by colour painting,
// otherwise intersects every one of them with the whole layer.
BoundingBox no_overlap_bbox = get_extents(expoly);
no_overlap_bbox.offset(SCALED_EPSILON);
f->no_overlap_expolygons = intersection_ex(
ClipperUtils::clip_clipper_polygons_with_subject_bbox(surface_fill.no_overlap_expolygons, no_overlap_bbox),
ExPolygons() = {expoly}, ApplySafetyOffset::Yes);
f->no_overlap_expolygons = intersection_ex(surface_fill.no_overlap_expolygons, ExPolygons() = {expoly}, ApplySafetyOffset::Yes);
if (params.symmetric_infill_y_axis) {
params.symmetric_y_axis = f->extended_object_bounding_box().center().x();
expoly.symmetric_y(params.symmetric_y_axis);
+211
View File
@@ -0,0 +1,211 @@
#include "AssembleList.hpp"
#include <algorithm>
#include <boost/filesystem.hpp>
#include <boost/filesystem/path.hpp>
#include <boost/filesystem/operations.hpp>
#include <boost/format.hpp>
#include <boost/log/trivial.hpp>
#include <boost/nowide/fstream.hpp>
#include <string>
#include <vector>
#include <map>
#include <utility>
#include <exception>
#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
@@ -18,6 +18,7 @@
#include <cmath>
#include <cctype>
#include <string>
#include <utility>
namespace Slic3r {
@@ -41,6 +42,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 +70,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;
}
+130 -22
View File
@@ -15,6 +15,7 @@
#include "../ClipperUtils.hpp"
#include "../Flow.hpp"
#include "../Config.hpp"
#include "libslic3r/ExtrusionEntityCollection.hpp"
#include "libslic3r/Line.hpp"
#include "libslic3r/ExPolygon.hpp"
@@ -22,8 +23,10 @@
#include <algorithm>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <functional>
#include <limits>
#include <memory>
#include <numeric>
#include <unordered_map>
#include <utility>
@@ -38,7 +41,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 +59,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 +106,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 +366,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 +437,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 +489,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 +619,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 +650,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 +671,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 +684,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 };
+5 -77
View File
@@ -139,87 +139,15 @@ bool tsp_remove_crossings(std::vector<size_t>& path, const Points& centers)
return {std::numeric_limits<size_t>::max(), std::numeric_limits<size_t>::max()};
};
// For many islands, the same scan with the edges binned in a uniform grid over their boxes, so each edge is only tested against the edges sharing a
// cell with it - two edges whose boxes overlap always do. It returns the same crossing as the all-pairs scan
// (smallest i, then smallest j), so the result is unchanged. The all-pairs scan is quadratic in the edge count and
// runs again after every reversal; rebuilding the grid costs more than it saves below the threshold.
constexpr size_t grid_min_size = 500;
BoundingBox extent;
for (size_t idx : path)
extent.merge(centers[idx]);
const int grid_n = std::clamp(int(std::sqrt(double(pn))), 1, 256);
const coord_t cell_w = std::max<coord_t>(1, (extent.max.x() - extent.min.x()) / grid_n + 1);
const coord_t cell_h = std::max<coord_t>(1, (extent.max.y() - extent.min.y()) / grid_n + 1);
const auto for_cells = [&](const Point& a, const Point& b, auto&& fn) {
const int x0 = int((std::min(a.x(), b.x()) - extent.min.x()) / cell_w), x1 = int((std::max(a.x(), b.x()) - extent.min.x()) / cell_w);
const int y0 = int((std::min(a.y(), b.y()) - extent.min.y()) / cell_h), y1 = int((std::max(a.y(), b.y()) - extent.min.y()) / cell_h);
for (int y = y0; y <= y1; ++y)
for (int x = x0; x <= x1; ++x)
fn(y * grid_n + x);
};
std::vector<std::vector<size_t>> edge_cells(size_t(grid_n) * grid_n);
auto find_crossing_grid = [&]() -> std::pair<size_t, size_t> {
for (std::vector<size_t>& cell : edge_cells)
cell.clear();
for (size_t j = 0; j < n_edges; ++j)
for_cells(centers[path[j]], centers[path[(j + 1) % pn]], [&](int cell) { edge_cells[cell].emplace_back(j); });
for (size_t i = 0; i < n_edges; ++i) {
const Point& ai = centers[path[i]];
const Point& bi = centers[path[(i + 1) % pn]];
size_t first_j = std::numeric_limits<size_t>::max();
for_cells(ai, bi, [&](int cell) {
for (size_t j : edge_cells[cell]) {
if (j < i + 2 || j >= first_j) continue;
// Skip the (0, pn-1) pair: edges (0,1) and (pn-1,0) share node 0.
if (i == 0 && j == pn - 1) continue;
const Point& aj = centers[path[j]];
const Point& bj = centers[path[(j + 1) % pn]];
if (!bboxes_overlap(ai, bi, aj, bj)) continue;
if (Geometry::segments_intersect(ai, bi, aj, bj))
first_j = j;
}
});
if (first_j != std::numeric_limits<size_t>::max())
return {i, first_j};
}
return {std::numeric_limits<size_t>::max(), std::numeric_limits<size_t>::max()};
};
// Process crossings one at a time: find first, reverse it, restart scan.
// Cap iterations to prevent infinite loops on collinear/overlapping segments.
const int max_iters = static_cast<int>(pn * pn);
int max_iters = static_cast<int>(pn * pn);
bool improved = false;
// Reversing between two segments that only touch or overlap along a line need not remove the intersection, so on
// islands laid out on a regular grid (a tiled texture, an array of parts) the loop can cycle through the same
// orderings until the pn * pn cap. Once an ordering repeats the rest of the loop is periodic, so only the steps
// to the ordering the capped loop would have stopped on are taken.
std::unordered_map<uint64_t, int> seen_paths; // path hash -> reversals done when it was reached
const auto path_hash = [&path]() {
uint64_t h = 1469598103934665603ull; // FNV-1a
for (size_t idx : path)
h = (h ^ uint64_t(idx)) * 1099511628211ull;
return h;
};
const auto reverse_first_crossing = [&]() {
auto [ci, cj] = pn >= grid_min_size ? find_crossing_grid() : find_crossing();
if (ci == std::numeric_limits<size_t>::max())
return false;
std::reverse(path.begin() + ci + 1, path.begin() + cj + 1);
return true;
};
seen_paths.emplace(path_hash(), 0);
for (int iter = 1; iter <= max_iters && reverse_first_crossing(); ++iter) {
while (max_iters-- > 0) {
auto [ci, cj] = find_crossing();
if (ci == std::numeric_limits<size_t>::max()) break;
improved = true;
if (auto [it, inserted] = seen_paths.emplace(path_hash(), iter); !inserted) {
for (int steps = (max_iters - iter) % (iter - it->second); steps > 0; --steps)
reverse_first_crossing();
break;
}
std::reverse(path.begin() + ci + 1, path.begin() + cj + 1);
}
return improved;
}
+13 -18
View File
@@ -28,7 +28,6 @@
#include <memory>
#include <random>
#include <algorithm>
#include <limits>
#include <queue>
#include <string>
#include <unordered_map>
@@ -1202,21 +1201,21 @@ std::optional<std::pair<size_t, size_t>> SeamPlacer::find_next_seam_in_layer(
const size_t layer_idx, const float max_distance,
const SeamPlacerImpl::SeamComparator &comparator) const {
using namespace SeamPlacerImpl;
// Find the best nearby point and the nearest one. A layer of a fine relief has tens of thousands of candidates within
// the radius, so they are looked at as the search finds them rather than collected into a vector first.
constexpr size_t none = std::numeric_limits<size_t>::max();
size_t best_nearby_point_index = none;
size_t nearest_point_index = none;
visit_nearby_points(*layers[layer_idx].points_tree, projected_position, max_distance,
[&layers, &comparator, &projected_position, layer_idx, &best_nearby_point_index, &nearest_point_index]
(size_t nearby_point_index) {
if (best_nearby_point_index == none) {
// The first point found starts both, as the first of the collected ones did.
best_nearby_point_index = nearest_point_index = nearby_point_index;
}
std::vector<size_t> nearby_points_indices = find_nearby_points(*layers[layer_idx].points_tree, projected_position,
max_distance);
if (nearby_points_indices.empty()) {
return {};
}
size_t best_nearby_point_index = nearby_points_indices[0];
size_t nearest_point_index = nearby_points_indices[0];
// Now find best nearby point, nearest point, and corresponding indices
for (const size_t &nearby_point_index : nearby_points_indices) {
const SeamCandidate &point = layers[layer_idx].points[nearby_point_index];
if (point.perimeter.finalized) {
return; // skip over finalized perimeters, try to find some that is not finalized
continue; // skip over finalized perimeters, try to find some that is not finalized
}
if (comparator.is_first_better(point, layers[layer_idx].points[best_nearby_point_index],
projected_position.head<2>())
@@ -1228,10 +1227,6 @@ std::optional<std::pair<size_t, size_t>> SeamPlacer::find_next_seam_in_layer(
|| layers[layer_idx].points[nearest_point_index].perimeter.finalized) {
nearest_point_index = nearby_point_index;
}
});
if (best_nearby_point_index == none) {
return {};
}
const SeamCandidate &best_nearby_point = layers[layer_idx].points[best_nearby_point_index];
+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";
-30
View File
@@ -318,36 +318,6 @@ std::vector<size_t> find_nearby_points(const KDTreeIndirectType &kdtree, const P
return visitor.result;
}
// Visits the points within max_distance of center, in the order find_nearby_points() would collect them, and hands
// each of them to `visitor_fn` instead of returning them all: a search over a dense set spends more on collecting the
// points into a vector than on the search itself, and its caller usually keeps only a few of them.
template<typename KDTreeIndirectType, typename PointType, typename VisitorFn>
void visit_nearby_points(const KDTreeIndirectType &kdtree, const PointType &center,
const typename KDTreeIndirectType::CoordType &max_distance, VisitorFn visitor_fn)
{
using CoordType = typename KDTreeIndirectType::CoordType;
struct Visitor {
const KDTreeIndirectType &kdtree;
const PointType center;
const CoordType max_distance_squared;
VisitorFn visitor_fn;
unsigned int operator()(size_t idx, size_t dimension) {
auto dist = CoordType(0);
for (size_t i = 0; i < KDTreeIndirectType::NumDimensions; ++i) {
CoordType d = center[i] - kdtree.coordinate(idx, i);
dist += d * d;
}
if (dist < max_distance_squared)
visitor_fn(idx);
return kdtree.descent_mask(center[dimension], max_distance_squared, idx, dimension);
}
} visitor { kdtree, center, max_distance * max_distance, visitor_fn };
kdtree.visit(visitor);
}
template<typename KDTreeIndirectType, typename PointType>
std::vector<size_t> find_nearby_points(const KDTreeIndirectType &kdtree, const PointType &center,
const typename KDTreeIndirectType::CoordType& max_distance)
+1 -2
View File
@@ -96,11 +96,10 @@ void LayerRegion::slices_to_fill_surfaces_clipped()
by_surface[size_t(surface.surface_type)].emplace_back(&surface);
// Trim surfaces by the fill_boundaries.
this->fill_surfaces.surfaces.clear();
const Polygons fill_boundaries = to_polygons(this->fill_expolygons);
for (size_t surface_type = 0; surface_type < size_t(stCount); ++ surface_type) {
const SurfacesPtr &this_surfaces = by_surface[surface_type];
if (! this_surfaces.empty())
this->fill_surfaces.append(intersection_ex_by_piece(to_expolygons(this_surfaces), fill_boundaries), SurfaceType(surface_type));
this->fill_surfaces.append(intersection_ex(this_surfaces, this->fill_expolygons), SurfaceType(surface_type));
}
}
+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
+72 -272
View File
@@ -24,7 +24,6 @@
#include "format.hpp"
#include "libslic3r.h"
#include <numeric>
#include <cmath>
#include <cstddef>
#include <list>
@@ -1354,15 +1353,10 @@ static inline std::vector<std::vector<ExPolygons>> segmentation_top_and_bottom_l
}
#endif // MM_SEGMENTATION_DEBUG_TOP_BOTTOM
// When the upper surface of an object is occluded, it should no longer be considered the upper surface.
// Every (colour, layer) pair is trimmed on its own, so they all run at once: the painted faces of a finely
// textured part project hundreds of thousands of triangles onto one layer, which used to be trimmed serially.
// When the upper surface of an object is occluded, it should no longer be considered the upper surface
{
const size_t occluded_pairs = num_facets_states * layers.size();
tbb::parallel_for(tbb::blocked_range<size_t>(0, occluded_pairs), [&](const tbb::blocked_range<size_t> &range) {
for (size_t pair_idx = range.begin(); pair_idx < range.end(); ++pair_idx) {
const size_t extruder_idx = pair_idx / layers.size();
const size_t layer_idx = pair_idx % layers.size();
for (size_t extruder_idx = 0; extruder_idx < num_facets_states; ++extruder_idx) {
for (size_t layer_idx = 0; layer_idx < layers.size(); ++layer_idx) {
if (!top_raw[extruder_idx].empty() && !top_raw[extruder_idx][layer_idx].empty() && layer_idx + 1 < layers.size()) {
top_raw[extruder_idx][layer_idx] = diff(top_raw[extruder_idx][layer_idx], input_expolygons[layer_idx + 1]);
}
@@ -1370,7 +1364,7 @@ static inline std::vector<std::vector<ExPolygons>> segmentation_top_and_bottom_l
bottom_raw[extruder_idx][layer_idx] = diff(bottom_raw[extruder_idx][layer_idx], input_expolygons[layer_idx - 1]);
}
}
});
}
}
std::vector<std::vector<ExPolygons>> triangles_by_color_bottom(num_facets_states);
@@ -1428,58 +1422,11 @@ static inline std::vector<std::vector<ExPolygons>> segmentation_top_and_bottom_l
return out;
};
// Projects a painted top or bottom face `ex` of layer `layer_idx` onto the shell layers below or above it (in
// `shell_layers`, nearest first), one more perimeter in on each, stopping at the first layer where nothing is left.
// Only the slices within the deepest offset of `ex` (three times that with the miter joins) decide the result, so the
// work is done per tile of `ex`'s ExPolygons on the slices cut to the tile's box grown by that much: the same result, but
// each Clipper call stays the size of a tile rather than of a layer cut through a fine relief, and the tiles run in
// parallel.
const auto project_to_shells = [&input_expolygons](const ExPolygons &ex, size_t layer_idx, const std::vector<size_t> &shell_layers,
const LayerColorStat &stat, ShellProjections &dst) {
std::vector<float> offsets(shell_layers.size());
float offset = 0.f;
for (size_t i = 0; i < shell_layers.size(); ++i) {
//BBS: offset width should be 2*spacing to avoid too narrow area which has overlap of wall line
offset -= (stat.extrusion_spacing + stat.extrusion_width);
offsets[i] = offset;
}
if (offsets.empty())
return;
const coord_t reach = coord_t(std::ceil(DefaultMiterLimit * std::abs(offsets.back()))) + 10 * SCALED_EPSILON;
const std::vector<ClipperUtils::ExPolygonsTile> tiles = ClipperUtils::tile_expolygons(ex, 16);
// [shell layer][tile]
std::vector<std::vector<ExPolygons>> shells(shell_layers.size(), std::vector<ExPolygons>(tiles.size()));
tbb::parallel_for(size_t(0), tiles.size(), [&](size_t tile_idx) {
const ClipperUtils::ExPolygonsTile &tile = tiles[tile_idx];
const BoundingBox bbox = tile.bbox.inflated(reach);
ExPolygons tile_ex;
tile_ex.reserve(tile.members.size());
for (size_t i : tile.members)
tile_ex.emplace_back(ex[i]);
Polygons layer_slices_trimmed = ClipperUtils::clip_clipper_polygons_with_subject_bbox(input_expolygons[layer_idx], bbox);
for (size_t i = 0; i < shell_layers.size() && ! layer_slices_trimmed.empty(); ++i) {
const ExPolygons trimmed = intersection_ex(layer_slices_trimmed, ClipperUtils::clip_clipper_polygons_with_subject_bbox(input_expolygons[shell_layers[i]], bbox));
shells[i][tile_idx] = opening_ex(intersection_ex(tile_ex, offset_ex(trimmed, offsets[i])), stat.small_region_threshold);
layer_slices_trimmed = to_polygons(trimmed);
}
});
for (size_t i = 0; i < shell_layers.size(); ++i) {
ExPolygons shell;
for (ExPolygons &tile_shell : shells[i])
append(shell, std::move(tile_shell));
if (shell.empty())
break;
dst.emplace_back(shell_layers[i], std::move(shell));
}
};
tbb::parallel_for(tbb::blocked_range<size_t>(0, num_layers), [&num_layers, &num_facets_states, &layer_color_stat, &top_raw, &triangles_by_color_top,
&throw_on_cancel_callback, &bottom_raw, &triangles_by_color_bottom, &project_to_shells,
&throw_on_cancel_callback, &input_expolygons, &bottom_raw, &triangles_by_color_bottom,
&shell_triangles_by_color_top, &shell_triangles_by_color_bottom](const tbb::blocked_range<size_t> &range) {
for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++ layer_idx) {
// Each colour writes only its own vectors, so the colours run in parallel: a painted top or bottom face
// projects onto a single layer, which otherwise did all of its colours on one thread.
tbb::parallel_for(size_t(0), size_t(num_facets_states), [&](size_t color_idx) {
for (size_t color_idx = 0; color_idx < num_facets_states; ++color_idx) {
throw_on_cancel_callback();
LayerColorStat stat = layer_color_stat(layer_idx, color_idx);
if (std::vector<Polygons> &top = top_raw[color_idx]; ! top.empty() && ! top[layer_idx].empty())
@@ -1488,10 +1435,18 @@ static inline std::vector<std::vector<ExPolygons>> segmentation_top_and_bottom_l
top_ex = opening_ex(top_ex, stat.small_region_threshold);
if (! top_ex.empty()) {
append(triangles_by_color_top[color_idx][layer_idx], top_ex);
std::vector<size_t> shell_layers;
for (int last_idx = int(layer_idx) - 1; last_idx > std::max(int(layer_idx - stat.top_shell_layers), int(0)); --last_idx)
shell_layers.emplace_back(size_t(last_idx));
project_to_shells(top_ex, layer_idx, shell_layers, stat, shell_triangles_by_color_top[color_idx][layer_idx]);
float offset = 0.f;
ExPolygons layer_slices_trimmed = input_expolygons[layer_idx];
for (int last_idx = int(layer_idx) - 1; last_idx > std::max(int(layer_idx - stat.top_shell_layers), int(0)); --last_idx) {
//BBS: offset width should be 2*spacing to avoid too narrow area which has overlap of wall line
//offset -= stat.extrusion_width ;
offset -= (stat.extrusion_spacing + stat.extrusion_width);
layer_slices_trimmed = intersection_ex(layer_slices_trimmed, input_expolygons[last_idx]);
ExPolygons last = opening_ex(intersection_ex(top_ex, offset_ex(layer_slices_trimmed, offset)), stat.small_region_threshold);
if (last.empty())
break;
shell_triangles_by_color_top[color_idx][layer_idx].emplace_back(size_t(last_idx), std::move(last));
}
}
}
if (std::vector<Polygons> &bottom = bottom_raw[color_idx]; ! bottom.empty() && ! bottom[layer_idx].empty())
@@ -1500,13 +1455,21 @@ static inline std::vector<std::vector<ExPolygons>> segmentation_top_and_bottom_l
bottom_ex = opening_ex(bottom_ex, stat.small_region_threshold);
if (! bottom_ex.empty()) {
append(triangles_by_color_bottom[color_idx][layer_idx], bottom_ex);
std::vector<size_t> shell_layers;
for (size_t last_idx = layer_idx + 1; last_idx < std::min(layer_idx + stat.bottom_shell_layers, num_layers); ++last_idx)
shell_layers.emplace_back(last_idx);
project_to_shells(bottom_ex, layer_idx, shell_layers, stat, shell_triangles_by_color_bottom[color_idx][layer_idx]);
float offset = 0.f;
ExPolygons layer_slices_trimmed = input_expolygons[layer_idx];
for (size_t last_idx = layer_idx + 1; last_idx < std::min(layer_idx + stat.bottom_shell_layers, num_layers); ++last_idx) {
//BBS: offset width should be 2*spacing to avoid too narrow area which has overlap of wall line
//offset -= stat.extrusion_width;
offset -= (stat.extrusion_spacing + stat.extrusion_width);
layer_slices_trimmed = intersection_ex(layer_slices_trimmed, input_expolygons[last_idx]);
ExPolygons last = opening_ex(intersection_ex(bottom_ex, offset_ex(layer_slices_trimmed, offset)), stat.small_region_threshold);
if (last.empty())
break;
shell_triangles_by_color_bottom[color_idx][layer_idx].emplace_back(last_idx, std::move(last));
}
}
}
});
}
}
});
@@ -1527,23 +1490,20 @@ static inline std::vector<std::vector<ExPolygons>> segmentation_top_and_bottom_l
&shell_top_by_layer, &shell_bottom_by_layer](const tbb::blocked_range<size_t> &range) {
for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++ layer_idx) {
throw_on_cancel_callback();
// The per-colour unions below are independent of each other, so they run in parallel (a painted top or
// bottom face puts all of its colours on one layer); whatever combines the colours stays in colour order.
const auto merge_colour_union = [&](size_t color_idx) {
ExPolygons painted_exploys;
for (size_t color_idx = 0; color_idx < triangles_by_color_merged.size(); ++color_idx) {
auto &self = triangles_by_color_merged[color_idx][layer_idx];
append(self, std::move(triangles_by_color_bottom[color_idx][layer_idx]));
append(self, std::move(triangles_by_color_top[color_idx][layer_idx]));
self = union_ex(self);
};
tbb::parallel_for(size_t(0), triangles_by_color_merged.size(), merge_colour_union);
ExPolygons painted_exploys;
for (size_t color_idx = 0; color_idx < triangles_by_color_merged.size(); ++color_idx)
append(painted_exploys, triangles_by_color_merged[color_idx][layer_idx]);
append(painted_exploys, self);
}
painted_exploys = union_ex(painted_exploys);
//BBS: merge the top and bottom shell layers
tbb::parallel_for(size_t(0), triangles_by_color_merged.size(), [&](size_t color_idx) {
for (size_t color_idx = 0; color_idx < triangles_by_color_merged.size(); ++color_idx) {
auto &self = triangles_by_color_merged[color_idx][layer_idx];
auto top_area = diff_ex(union_ex(shell_top_by_layer[color_idx][layer_idx]), painted_exploys);
@@ -1552,7 +1512,7 @@ static inline std::vector<std::vector<ExPolygons>> segmentation_top_and_bottom_l
append(self, top_area);
append(self, bottom_area);
self = union_ex(self);
});
}
// Trim one region by the other if some of the regions overlap.
ExPolygons painted_regions;
for (size_t color_idx = 1; color_idx < triangles_by_color_merged.size(); ++color_idx) {
@@ -1919,69 +1879,7 @@ static void remove_multiple_edges_in_vertices(MMU_Graph &graph, const std::vecto
}
}
// Finds the islands (layer ExPolygons) a region piece overlaps. A top or bottom region is projected from the neighbouring
// layers and may reach past the island it belongs to, or over several islands.
class IslandLocator
{
public:
explicit IslandLocator(const ExPolygons &islands) : m_islands(islands)
{
m_bboxes.reserve(islands.size());
for (const ExPolygon &island : islands) {
m_bboxes.emplace_back(get_extents(island));
m_extent.merge(m_bboxes.back());
}
if (!m_extent.defined)
return;
const Point size = m_extent.size();
m_cell_w = std::max<coord_t>(1, size.x() / GRID + 1);
m_cell_h = std::max<coord_t>(1, size.y() / GRID + 1);
m_grid.assign(GRID * GRID, {});
for (size_t i = 0; i < m_bboxes.size(); ++i)
for_cells(m_bboxes[i], [&](int cell) { m_grid[cell].emplace_back(i); });
}
void find(const ExPolygon &piece, std::vector<size_t> &out) const
{
out.clear();
const BoundingBox bbox = get_extents(piece);
if (!m_extent.defined || !m_extent.overlap(bbox))
return;
for_cells(bbox, [&](int cell) {
for (size_t i : m_grid[cell])
if (m_bboxes[i].overlap(bbox))
out.emplace_back(i);
});
sort_remove_duplicates(out);
if (out.size() > 1)
out.erase(std::remove_if(out.begin(), out.end(), [&](size_t i) {
const BoundingBox common(m_bboxes[i].min.cwiseMax(bbox.min), m_bboxes[i].max.cwiseMin(bbox.max));
return intersection(ClipperUtils::clip_clipper_polygons_with_subject_bbox(piece, common.inflated(SCALED_EPSILON)),
ClipperUtils::clip_clipper_polygons_with_subject_bbox(m_islands[i], common.inflated(SCALED_EPSILON))).empty();
}), out.end());
}
private:
static constexpr int GRID = 64;
template<typename Fn> void for_cells(const BoundingBox &bb, Fn &&fn) const
{
const int x0 = std::clamp(int((bb.min.x() - m_extent.min.x()) / m_cell_w), 0, GRID - 1), x1 = std::clamp(int((bb.max.x() - m_extent.min.x()) / m_cell_w), 0, GRID - 1);
const int y0 = std::clamp(int((bb.min.y() - m_extent.min.y()) / m_cell_h), 0, GRID - 1), y1 = std::clamp(int((bb.max.y() - m_extent.min.y()) / m_cell_h), 0, GRID - 1);
for (int y = y0; y <= y1; ++y)
for (int x = x0; x <= x1; ++x)
fn(y * GRID + x);
}
const ExPolygons &m_islands;
std::vector<BoundingBox> m_bboxes;
BoundingBox m_extent;
coord_t m_cell_w = 1, m_cell_h = 1;
std::vector<std::vector<size_t>> m_grid;
};
static std::vector<std::vector<ExPolygons>> merge_segmented_layers(const std::vector<ExPolygons> &input_expolygons,
const std::vector<std::vector<ExPolygons>> &segmented_regions,
static std::vector<std::vector<ExPolygons>> merge_segmented_layers(const std::vector<std::vector<ExPolygons>> &segmented_regions,
std::vector<std::vector<ExPolygons>> &&top_and_bottom_layers,
const size_t num_facets_states,
const std::function<void()> &throw_on_cancel_callback)
@@ -1992,91 +1890,33 @@ static std::vector<std::vector<ExPolygons>> merge_segmented_layers(const std::ve
assert(!top_and_bottom_layers.size() || num_facets_states == top_and_bottom_layers.size());
BOOST_LOG_TRIVIAL(debug) << "Print object segmentation - Merging segmented layers in parallel - Begin";
// Every region of a layer is merged together with the regions of the islands it overlaps, and the islands are further
// apart than the dimple removal below reaches, so this gives the same result as merging the layer at once. On a layer
// cut through a fine relief every region shares thousands of hole contours with every other, and Clipper, splitting
// and re-linking one huge polygon over and over, took anything up to half an hour for a layer; per island each operation
// stays the size of the island, and the islands run in parallel.
tbb::parallel_for(tbb::blocked_range<size_t>(0, num_layers), [&](const tbb::blocked_range<size_t> &range) {
tbb::parallel_for(tbb::blocked_range<size_t>(0, num_layers), [&segmented_regions, &top_and_bottom_layers, &segmented_regions_merged, &num_facets_states, &throw_on_cancel_callback](const tbb::blocked_range<size_t> &range) {
for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++layer_idx) {
assert(segmented_regions[layer_idx].size() == num_facets_states);
throw_on_cancel_callback();
// Group the islands joined by a region overlapping several of them; the last group takes the regions lying
// outside every island.
const ExPolygons &islands = input_expolygons[layer_idx];
const IslandLocator locator(islands);
std::vector<size_t> parent(islands.size() + 1);
std::iota(parent.begin(), parent.end(), 0);
const auto root = [&parent](size_t i) {
while (parent[i] != i)
i = parent[i] = parent[parent[i]];
return i;
};
// Islands of every piece: side regions of colours 1.., then top/bottom regions of colours 0..
std::vector<const ExPolygon *> pieces;
for (size_t extruder_id = 1; extruder_id < num_facets_states; ++extruder_id)
for (const ExPolygon &piece : segmented_regions[layer_idx][extruder_id])
pieces.emplace_back(&piece);
if (!top_and_bottom_layers.empty())
for (size_t color_idx = 0; color_idx < num_facets_states; ++color_idx)
for (const ExPolygon &piece : top_and_bottom_layers[color_idx][layer_idx])
pieces.emplace_back(&piece);
std::vector<std::vector<size_t>> overlapped(pieces.size());
tbb::parallel_for(size_t(0), pieces.size(), [&](size_t i) { locator.find(*pieces[i], overlapped[i]); });
std::vector<size_t> piece_island(pieces.size());
for (size_t i = 0; i < pieces.size(); ++i) {
piece_island[i] = overlapped[i].empty() ? islands.size() : overlapped[i].front();
for (size_t island : overlapped[i])
parent[root(island)] = root(piece_island[i]);
}
std::vector<size_t> bucket_of(parent.size(), size_t(-1));
size_t num_buckets = 0;
for (size_t i = 0; i < parent.size(); ++i)
if (size_t &b = bucket_of[root(i)]; b == size_t(-1))
b = num_buckets++;
// [bucket][colour]
std::vector<std::vector<ExPolygons>> sides(num_buckets, std::vector<ExPolygons>(num_facets_states));
std::vector<std::vector<ExPolygons>> tops(num_buckets, std::vector<ExPolygons>(num_facets_states));
size_t piece_idx = 0;
for (size_t extruder_id = 1; extruder_id < num_facets_states; ++extruder_id)
for (const ExPolygon &piece : segmented_regions[layer_idx][extruder_id])
sides[bucket_of[root(piece_island[piece_idx++])]][extruder_id].emplace_back(piece);
if (!top_and_bottom_layers.empty())
for (size_t color_idx = 0; color_idx < num_facets_states; ++color_idx)
for (const ExPolygon &piece : top_and_bottom_layers[color_idx][layer_idx])
tops[bucket_of[root(piece_island[piece_idx++])]][color_idx].emplace_back(piece);
// Side regions minus the top/bottom regions of every colour.
std::vector<std::vector<ExPolygons>> merged(num_buckets, std::vector<ExPolygons>(num_facets_states));
tbb::parallel_for(size_t(0), num_buckets, [&](size_t bucket) {
Polygons tops_all;
for (const ExPolygons &t : tops[bucket])
polygons_append(tops_all, t);
for (size_t extruder_id = 1; extruder_id < num_facets_states; ++extruder_id)
if (!sides[bucket][extruder_id].empty())
merged[bucket][extruder_id] = tops_all.empty() ? std::move(sides[bucket][extruder_id]) :
diff_ex_by_piece(sides[bucket][extruder_id], tops_all);
});
// Then this colour's top/bottom regions, with the dimples removed (#7235) when the layer has side regions left.
// Zero is skipped because it is the default color of the volume
for (size_t extruder_id = 1; extruder_id < num_facets_states; ++extruder_id) {
if (top_and_bottom_layers.empty() || top_and_bottom_layers[extruder_id][layer_idx].empty()) {
for (size_t bucket = 0; bucket < num_buckets; ++bucket)
append(segmented_regions_merged[layer_idx][extruder_id - 1], std::move(merged[bucket][extruder_id]));
continue;
throw_on_cancel_callback();
if (!segmented_regions[layer_idx][extruder_id].empty()) {
ExPolygons segmented_regions_trimmed = segmented_regions[layer_idx][extruder_id];
if (!top_and_bottom_layers.empty()) {
for (const std::vector<ExPolygons> &top_and_bottom_by_extruder : top_and_bottom_layers) {
if (!top_and_bottom_by_extruder[layer_idx].empty() && !segmented_regions_trimmed.empty()) {
segmented_regions_trimmed = diff_ex(segmented_regions_trimmed, top_and_bottom_by_extruder[layer_idx]);
}
}
}
segmented_regions_merged[layer_idx][extruder_id - 1] = std::move(segmented_regions_trimmed);
}
if (!top_and_bottom_layers.empty() && !top_and_bottom_layers[extruder_id][layer_idx].empty()) {
bool was_top_and_bottom_empty = segmented_regions_merged[layer_idx][extruder_id - 1].empty();
append(segmented_regions_merged[layer_idx][extruder_id - 1], top_and_bottom_layers[extruder_id][layer_idx]);
// Remove dimples (#7235) appearing after merging side segmentation of the model with tops and bottoms painted layers.
if (!was_top_and_bottom_empty)
segmented_regions_merged[layer_idx][extruder_id - 1] = offset2_ex(union_ex(segmented_regions_merged[layer_idx][extruder_id - 1]), float(SCALED_EPSILON), -float(SCALED_EPSILON));
}
bool was_top_and_bottom_empty = true;
for (size_t bucket = 0; bucket < num_buckets && was_top_and_bottom_empty; ++bucket)
was_top_and_bottom_empty = merged[bucket][extruder_id].empty();
tbb::parallel_for(size_t(0), num_buckets, [&](size_t bucket) {
ExPolygons &region = merged[bucket][extruder_id];
append(region, tops[bucket][extruder_id]);
if (!was_top_and_bottom_empty && !region.empty())
region = offset2_ex(union_ex(region), float(SCALED_EPSILON), -float(SCALED_EPSILON));
});
for (size_t bucket = 0; bucket < num_buckets; ++bucket)
append(segmented_regions_merged[layer_idx][extruder_id - 1], std::move(merged[bucket][extruder_id]));
}
}
}); // end of parallel_for
@@ -2363,56 +2203,16 @@ std::vector<std::vector<ExPolygons>> segmentation_by_painting(const PrintObject
assert(!color_poly.empty());
assert(!color_poly.front().empty());
// Each island (an ExPolygon with its holes) is segmented on its own. Any point of an island is closer to
// that island's contours than to any other island's - the way out crosses its own boundary first - so its
// Voronoi cells, and with them its colour regions, depend on nothing else. A layer cut through a fine relief
// has thousands of islands, and one Voronoi diagram over all of them degenerated into overlapping regions
// that every boolean afterwards had to untangle. Per island the diagrams stay small and the islands run in
// parallel; an island in a single colour needs no diagram at all.
const ExPolygons &islands = input_expolygons[layer_idx];
std::vector<std::pair<size_t, size_t>> island_contours(islands.size()); // [first, last) into color_poly
{
// The same order EdgeGrid::Grid::create() lists the contours in, and so colorize_contours().
size_t idx = 0;
for (size_t island_idx = 0; island_idx < islands.size(); ++island_idx) {
const size_t first = idx;
if (!islands[island_idx].contour.empty())
++idx;
for (const Polygon &hole : islands[island_idx].holes)
if (!hole.empty())
++idx;
island_contours[island_idx] = {first, idx};
}
assert(idx == color_poly.size());
if (has_layer_only_one_color(color_poly)) {
// If the whole layer is painted using the same color, it is not needed to construct a Voronoi diagram for the segmentation of this layer.
segmented_regions[layer_idx][size_t(color_poly.front().front().color)] = input_expolygons[layer_idx];
} else {
MMU_Graph graph = build_graph(layer_idx, color_poly);
remove_multiple_edges_in_vertices(graph, color_poly);
graph.remove_nodes_with_one_arc();
segmented_regions[layer_idx] = extract_colored_segments(graph, num_facets_states);
//segmented_regions[layer_idx] = extract_colored_segments(color_poly, num_extruders, layer_idx);
}
std::vector<std::vector<ExPolygons>> island_regions(islands.size());
tbb::parallel_for(size_t(0), islands.size(), [&](size_t island_idx) {
const auto [first, last] = island_contours[island_idx];
if (first == last)
return;
const std::vector<ColoredLines> island_poly(color_poly.begin() + first, color_poly.begin() + last);
std::vector<ExPolygons> &regions = island_regions[island_idx];
if (has_layer_only_one_color(island_poly)) {
regions.assign(num_facets_states, ExPolygons());
regions[size_t(island_poly.front().front().color)].emplace_back(islands[island_idx]);
} else {
MMU_Graph graph = build_graph(layer_idx, island_poly);
remove_multiple_edges_in_vertices(graph, island_poly);
graph.remove_nodes_with_one_arc();
regions = extract_colored_segments(graph, num_facets_states);
// The faces of one colour tile it without overlapping; merged here, where an island is small,
// every later boolean gets a few regions instead of thousands of faces sharing their edges. An
// island with many holes keeps its faces: merged, each colour would be one region with thousands
// of holes, and subtracting from that is far slower than from the faces one at a time.
if (island_poly.size() <= 64)
for (ExPolygons &faces : regions)
if (faces.size() > 1)
faces = union_ex(faces);
}
});
for (std::vector<ExPolygons> &regions : island_regions)
for (size_t color_idx = 0; color_idx < regions.size(); ++color_idx)
append(segmented_regions[layer_idx][color_idx], std::move(regions[color_idx]));
#ifdef MM_SEGMENTATION_DEBUG_REGIONS
export_regions_to_svg(debug_out_path("3-mm-regions-sides-%d-%d.svg", layer_idx, iRun), segmented_regions[layer_idx], input_expolygons[layer_idx]);
@@ -2435,7 +2235,7 @@ std::vector<std::vector<ExPolygons>> segmentation_by_painting(const PrintObject
throw_on_cancel_callback();
}
std::vector<std::vector<ExPolygons>> segmented_regions_merged = merge_segmented_layers(input_expolygons, segmented_regions, std::move(top_and_bottom_layers), num_facets_states, throw_on_cancel_callback);
std::vector<std::vector<ExPolygons>> segmented_regions_merged = merge_segmented_layers(segmented_regions, std::move(top_and_bottom_layers), num_facets_states, throw_on_cancel_callback);
throw_on_cancel_callback();
#ifdef MM_SEGMENTATION_DEBUG_REGIONS
+2 -6
View File
@@ -23,15 +23,11 @@ public:
MultiPoint() {}
MultiPoint(const MultiPoint &other) : points(other.points) {}
MultiPoint(MultiPoint &&other) noexcept : points(std::move(other.points)) {}
MultiPoint(MultiPoint &&other) : points(std::move(other.points)) {}
MultiPoint(std::initializer_list<Point> list) : points(list) {}
explicit MultiPoint(const Points &_points) : points(_points) {}
// Without it, the derived classes' move constructors passing std::move(points) here copied them, which
// also means a moved-from Polygon or Polyline is now really empty where it used to silently keep its
// points: a use-after-move anywhere in the tree that happened to work before now sees nothing.
explicit MultiPoint(Points &&_points) noexcept : points(std::move(_points)) {}
MultiPoint& operator=(const MultiPoint &other) { points = other.points; return *this; }
MultiPoint& operator=(MultiPoint &&other) noexcept { points = std::move(other.points); return *this; }
MultiPoint& operator=(MultiPoint &&other) { points = std::move(other.points); return *this; }
virtual ~MultiPoint() = default;
void scale(double factor);
void scale(double factor_x, double factor_y);
+363 -388
View File
@@ -33,8 +33,6 @@
#include <tuple>
#include <unordered_set>
#include <thread>
#include <tbb/blocked_range.h>
#include <tbb/parallel_for.h>
#include <vector>
#include "libslic3r.h"
#include <utility>
@@ -2493,444 +2491,421 @@ void PerimeterGenerator::process_arachne()
const bool only_one_wall_first_layer = this->config->only_one_wall_first_layer && has_bottom_shell_layers(*this->config);
// we need to process each island separately because we might have different
// extra perimeters for each one
// Each island is independent up to its outputs, so they are generated in parallel - a layer split into thousands
// of islands (e.g. by colour painting) otherwise ran on one thread - and the outputs are then committed in the
// original island order, which is what the extra overhang perimeters (applied to the last island's loops and to
// all fill surfaces so far) depend on.
struct ArachneSurfaceResult
{
ExtrusionEntityCollection loops;
bool has_loops = false;
ExPolygons infill;
ExPolygons no_overlap;
};
std::vector<ArachneSurfaceResult> results(all_surfaces.size());
tbb::parallel_for(tbb::blocked_range<size_t>(0, all_surfaces.size()), [&](const tbb::blocked_range<size_t> &range) {
for (size_t surface_idx = range.begin(); surface_idx < range.end(); ++surface_idx) {
const Surface &surface = all_surfaces[surface_idx];
ArachneSurfaceResult &result = results[surface_idx];
coord_t bead_width_0 = ext_perimeter_spacing;
// detect how many perimeters must be generated for this island
int loop_number = this->config->wall_loops + surface.extra_perimeters - 1; // 0-indexed loops
int sparse_infill_density = this->config->sparse_infill_density.value;
if (this->config->alternate_extra_wall && this->layer_id % 2 == 1 && !m_spiral_vase && sparse_infill_density > 0) // add alternating extra wall
loop_number++;
for (const Surface& surface : all_surfaces) {
coord_t bead_width_0 = ext_perimeter_spacing;
// detect how many perimeters must be generated for this island
int loop_number = this->config->wall_loops + surface.extra_perimeters - 1; // 0-indexed loops
int sparse_infill_density = this->config->sparse_infill_density.value;
if (this->config->alternate_extra_wall && this->layer_id % 2 == 1 && !m_spiral_vase && sparse_infill_density > 0) // add alternating extra wall
loop_number++;
// Set the bottommost layer to be one wall
const bool is_bottom_layer = (this->layer_id == object_config->raft_layers) ? true : false;
if (is_bottom_layer && only_one_wall_first_layer)
loop_number = 0;
// Set the bottommost layer to be one wall
const bool is_bottom_layer = (this->layer_id == object_config->raft_layers) ? true : false;
if (is_bottom_layer && only_one_wall_first_layer)
loop_number = 0;
// Orca: set the topmost layer to be one wall according to the config
const bool is_topmost_layer = (this->upper_slices == nullptr) ? true : false;
if (is_topmost_layer && loop_number > 0 && only_one_wall_top)
loop_number = 0;
// Orca: set the topmost layer to be one wall according to the config
const bool is_topmost_layer = (this->upper_slices == nullptr) ? true : false;
if (is_topmost_layer && loop_number > 0 && only_one_wall_top)
loop_number = 0;
auto apply_precise_outer_wall = config->precise_outer_wall && config->wall_sequence == WallSequence::InnerOuter;
// Orca: properly adjust offset for the outer wall if precise_outer_wall is enabled.
ExPolygons last = offset_ex(surface.expolygon.simplify_p(surface_simplify_resolution),
apply_precise_outer_wall? -float(ext_perimeter_width - ext_perimeter_spacing )
: -float(ext_perimeter_width / 2. - ext_perimeter_spacing / 2.));
auto apply_precise_outer_wall = config->precise_outer_wall && config->wall_sequence == WallSequence::InnerOuter;
// Orca: properly adjust offset for the outer wall if precise_outer_wall is enabled.
ExPolygons last = offset_ex(surface.expolygon.simplify_p(surface_simplify_resolution),
apply_precise_outer_wall? -float(ext_perimeter_width - ext_perimeter_spacing )
: -float(ext_perimeter_width / 2. - ext_perimeter_spacing / 2.));
Arachne::WallToolPathsParams input_params = Arachne::make_paths_params(this->layer_id, *object_config, *print_config);
// Set params is_top_or_bottom_layer for adjusting short-wall removal sensitivity.
input_params.is_top_or_bottom_layer = (is_bottom_layer || is_topmost_layer) ? true : false;
Arachne::WallToolPathsParams input_params = Arachne::make_paths_params(this->layer_id, *object_config, *print_config);
// Set params is_top_or_bottom_layer for adjusting short-wall removal sensitivity.
input_params.is_top_or_bottom_layer = (is_bottom_layer || is_topmost_layer) ? true : false;
coord_t wall_0_inset = 0;
if (apply_precise_outer_wall)
wall_0_inset = -coord_t(ext_perimeter_width / 2 - ext_perimeter_spacing / 2);
coord_t wall_0_inset = 0;
if (apply_precise_outer_wall)
wall_0_inset = -coord_t(ext_perimeter_width / 2 - ext_perimeter_spacing / 2);
//PS: One wall top surface for Arachne
ExPolygons top_expolygons;
// Calculate how many inner loops remain when TopSurfaces is selected.
const int inner_loop_number = (only_one_wall_top && upper_slices != nullptr) ? loop_number - 1 : -1;
//PS: One wall top surface for Arachne
ExPolygons top_expolygons;
// Calculate how many inner loops remain when TopSurfaces is selected.
const int inner_loop_number = (only_one_wall_top && upper_slices != nullptr) ? loop_number - 1 : -1;
// Set one perimeter when TopSurfaces is selected.
if (only_one_wall_top && loop_number > 0)
loop_number = 0;
// Set one perimeter when TopSurfaces is selected.
if (only_one_wall_top && loop_number > 0)
loop_number = 0;
Arachne::WallToolPathsParams input_params_tmp = input_params;
Arachne::WallToolPathsParams input_params_tmp = input_params;
Polygons last_p = to_polygons(last);
Arachne::WallToolPaths wallToolPaths(last_p, bead_width_0, perimeter_spacing, coord_t(loop_number + 1),
wall_0_inset, layer_height, input_params_tmp);
std::vector<Arachne::VariableWidthLines> perimeters = wallToolPaths.getToolPaths();
ExPolygons infill_contour = union_ex(wallToolPaths.getInnerContour());
Polygons last_p = to_polygons(last);
Arachne::WallToolPaths wallToolPaths(last_p, bead_width_0, perimeter_spacing, coord_t(loop_number + 1),
wall_0_inset, layer_height, input_params_tmp);
std::vector<Arachne::VariableWidthLines> perimeters = wallToolPaths.getToolPaths();
ExPolygons infill_contour = union_ex(wallToolPaths.getInnerContour());
// Check if there are some remaining perimeters to generate (the number of perimeters
// is greater than one together with enabled the single perimeter on top surface feature).
if (inner_loop_number >= 0) {
assert(upper_slices != nullptr);
// Check if there are some remaining perimeters to generate (the number of perimeters
// is greater than one together with enabled the single perimeter on top surface feature).
if (inner_loop_number >= 0) {
assert(upper_slices != nullptr);
// Infill contour bounding box.
BoundingBox infill_contour_bbox = get_extents(infill_contour);
infill_contour_bbox.offset(SCALED_EPSILON);
// Infill contour bounding box.
BoundingBox infill_contour_bbox = get_extents(infill_contour);
infill_contour_bbox.offset(SCALED_EPSILON);
coord_t perimeter_width = this->perimeter_flow.scaled_width();
coord_t perimeter_width = this->perimeter_flow.scaled_width();
// Get top ExPolygons from current infill contour.
Polygons upper_slices_clipped;
if (object_config->interface_shells) {
auto upper_slicer_same_region = to_expolygons(this->upper_slices_same_region->surfaces);
upper_slices_clipped = ClipperUtils::clip_clipper_polygons_with_subject_bbox(upper_slicer_same_region, infill_contour_bbox);
} else
upper_slices_clipped = ClipperUtils::clip_clipper_polygons_with_subject_bbox(*upper_slices, infill_contour_bbox);
// Get top ExPolygons from current infill contour.
Polygons upper_slices_clipped;
if (object_config->interface_shells) {
auto upper_slicer_same_region = to_expolygons(this->upper_slices_same_region->surfaces);
upper_slices_clipped = ClipperUtils::clip_clipper_polygons_with_subject_bbox(upper_slicer_same_region, infill_contour_bbox);
} else
upper_slices_clipped = ClipperUtils::clip_clipper_polygons_with_subject_bbox(*upper_slices, infill_contour_bbox);
top_expolygons = diff_ex(infill_contour, upper_slices_clipped);
top_expolygons = diff_ex(infill_contour, upper_slices_clipped);
if (!top_expolygons.empty()) {
if (lower_slices != nullptr) {
const float bridge_offset = float(std::max<coord_t>(ext_perimeter_spacing, perimeter_width));
const Polygons lower_slices_clipped = ClipperUtils::clip_clipper_polygons_with_subject_bbox(*lower_slices, infill_contour_bbox);
const ExPolygons current_slices_bridges = offset_ex(diff_ex(top_expolygons, lower_slices_clipped), bridge_offset);
if (!top_expolygons.empty()) {
if (lower_slices != nullptr) {
const float bridge_offset = float(std::max<coord_t>(ext_perimeter_spacing, perimeter_width));
const Polygons lower_slices_clipped = ClipperUtils::clip_clipper_polygons_with_subject_bbox(*lower_slices, infill_contour_bbox);
const ExPolygons current_slices_bridges = offset_ex(diff_ex(top_expolygons, lower_slices_clipped), bridge_offset);
// Remove bridges from top surface polygons.
top_expolygons = diff_ex(top_expolygons, current_slices_bridges);
}
// Filter out areas that are too thin and expand top surface polygons a bit to hide the wall line.
// ORCA: skip if the top surface area is smaller than "min_width_top_surface"
const float top_surface_min_width = std::max<float>(float(ext_perimeter_spacing) / 4.f + scaled<float>(0.00001), float(scale_(config->min_width_top_surface.get_abs_value(unscale_(perimeter_width)))) / 4.f);
// Shrink the polygon to remove the small areas, then expand it back out plus a maragin to hide the wall line a little.
// ORCA: Expand the polygon with half the perimeter width in addition to the contracted amount,
// not the full perimeter width as PS does, to enable thin lettering to print on the top surface without nozzle collisions
// due to thin lines being generated
top_expolygons = offset2_ex(top_expolygons, -top_surface_min_width, top_surface_min_width + float(perimeter_width * 0.85));
// Get final top ExPolygons (bridges were excluded above, so they stay walled).
top_expolygons = intersection_ex(top_expolygons, infill_contour);
// ORCA: onion the real region (inside the outer wall) so the remaining walls follow the actual
// geometry, then cut away the parts over the top surface. Re-onioning the non-top complement
// instead - the fallback when there is no top fill - walls the top/non-top interface and rings
// top-surface islands with inner walls that don't exist when the feature is disabled.
const bool clip_walls_over_top = top_fill_replaces_inner_walls(*this->config);
const Polygons inner_region = to_polygons(offset_ex(clip_walls_over_top ? infill_contour
: diff_ex(infill_contour, top_expolygons),
wall_0_inset));
Arachne::WallToolPaths inner_wall_tool_paths(inner_region, perimeter_spacing, perimeter_spacing, coord_t(inner_loop_number + 1), 0, layer_height, input_params_tmp);
std::vector<Arachne::VariableWidthLines> inner_perimeters = inner_wall_tool_paths.getToolPaths();
if (clip_walls_over_top) {
Polygons kept_over_top;
clip_inner_walls_over_top(inner_perimeters, top_expolygons, perimeter_width, kept_over_top);
// Route the top fill around the walls kept despite grazing the top.
if (! kept_over_top.empty())
top_expolygons = diff_ex(top_expolygons, kept_over_top);
}
// Recalculate indexes of inner perimeters before merging them: they come after the single outer wall.
if (!perimeters.empty())
for (Arachne::VariableWidthLines &inner_perimeter : inner_perimeters)
for (Arachne::ExtrusionLine &el : inner_perimeter)
++el.inset_idx;
perimeters.insert(perimeters.end(), inner_perimeters.begin(), inner_perimeters.end());
infill_contour = union_ex(top_expolygons, inner_wall_tool_paths.getInnerContour());
} else {
// There is no top surface ExPolygon, so we call Arachne again with parameters
// like when the single perimeter feature is disabled.
Arachne::WallToolPaths no_single_perimeter_tool_paths(last_p, bead_width_0, perimeter_spacing, coord_t(inner_loop_number + 2), wall_0_inset, layer_height, input_params_tmp);
perimeters = no_single_perimeter_tool_paths.getToolPaths();
infill_contour = union_ex(no_single_perimeter_tool_paths.getInnerContour());
// Remove bridges from top surface polygons.
top_expolygons = diff_ex(top_expolygons, current_slices_bridges);
}
// Filter out areas that are too thin and expand top surface polygons a bit to hide the wall line.
// ORCA: skip if the top surface area is smaller than "min_width_top_surface"
const float top_surface_min_width = std::max<float>(float(ext_perimeter_spacing) / 4.f + scaled<float>(0.00001), float(scale_(config->min_width_top_surface.get_abs_value(unscale_(perimeter_width)))) / 4.f);
// Shrink the polygon to remove the small areas, then expand it back out plus a maragin to hide the wall line a little.
// ORCA: Expand the polygon with half the perimeter width in addition to the contracted amount,
// not the full perimeter width as PS does, to enable thin lettering to print on the top surface without nozzle collisions
// due to thin lines being generated
top_expolygons = offset2_ex(top_expolygons, -top_surface_min_width, top_surface_min_width + float(perimeter_width * 0.85));
// Get final top ExPolygons (bridges were excluded above, so they stay walled).
top_expolygons = intersection_ex(top_expolygons, infill_contour);
// ORCA: onion the real region (inside the outer wall) so the remaining walls follow the actual
// geometry, then cut away the parts over the top surface. Re-onioning the non-top complement
// instead - the fallback when there is no top fill - walls the top/non-top interface and rings
// top-surface islands with inner walls that don't exist when the feature is disabled.
const bool clip_walls_over_top = top_fill_replaces_inner_walls(*this->config);
const Polygons inner_region = to_polygons(offset_ex(clip_walls_over_top ? infill_contour
: diff_ex(infill_contour, top_expolygons),
wall_0_inset));
Arachne::WallToolPaths inner_wall_tool_paths(inner_region, perimeter_spacing, perimeter_spacing, coord_t(inner_loop_number + 1), 0, layer_height, input_params_tmp);
std::vector<Arachne::VariableWidthLines> inner_perimeters = inner_wall_tool_paths.getToolPaths();
if (clip_walls_over_top) {
Polygons kept_over_top;
clip_inner_walls_over_top(inner_perimeters, top_expolygons, perimeter_width, kept_over_top);
// Route the top fill around the walls kept despite grazing the top.
if (! kept_over_top.empty())
top_expolygons = diff_ex(top_expolygons, kept_over_top);
}
// Recalculate indexes of inner perimeters before merging them: they come after the single outer wall.
if (!perimeters.empty())
for (Arachne::VariableWidthLines &inner_perimeter : inner_perimeters)
for (Arachne::ExtrusionLine &el : inner_perimeter)
++el.inset_idx;
perimeters.insert(perimeters.end(), inner_perimeters.begin(), inner_perimeters.end());
infill_contour = union_ex(top_expolygons, inner_wall_tool_paths.getInnerContour());
} else {
// There is no top surface ExPolygon, so we call Arachne again with parameters
// like when the single perimeter feature is disabled.
Arachne::WallToolPaths no_single_perimeter_tool_paths(last_p, bead_width_0, perimeter_spacing, coord_t(inner_loop_number + 2), wall_0_inset, layer_height, input_params_tmp);
perimeters = no_single_perimeter_tool_paths.getToolPaths();
infill_contour = union_ex(no_single_perimeter_tool_paths.getInnerContour());
}
//PS
}
//PS
loop_number = int(perimeters.size()) - 1;
loop_number = int(perimeters.size()) - 1;
#ifdef ARACHNE_DEBUG
{
static int iRun = 0;
export_perimeters_to_svg(debug_out_path("arachne-perimeters-%d-%d.svg", layer_id, iRun++), to_polygons(last), perimeters, union_ex(wallToolPaths.getInnerContour()));
}
#endif
#ifdef ARACHNE_DEBUG
{
static int iRun = 0;
export_perimeters_to_svg(debug_out_path("arachne-perimeters-%d-%d.svg", layer_id, iRun++), to_polygons(last), perimeters, union_ex(wallToolPaths.getInnerContour()));
}
#endif
// All closed ExtrusionLine should have the same the first and the last point.
// But in rare cases, Arachne produce ExtrusionLine marked as closed but without
// equal the first and the last point.
assert([&perimeters = std::as_const(perimeters)]() -> bool {
for (const Arachne::VariableWidthLines& perimeter : perimeters)
for (const Arachne::ExtrusionLine& el : perimeter)
if (el.is_closed && el.junctions.front().p != el.junctions.back().p)
return false;
return true;
}());
// All closed ExtrusionLine should have the same the first and the last point.
// But in rare cases, Arachne produce ExtrusionLine marked as closed but without
// equal the first and the last point.
assert([&perimeters = std::as_const(perimeters)]() -> bool {
for (const Arachne::VariableWidthLines& perimeter : perimeters)
for (const Arachne::ExtrusionLine& el : perimeter)
if (el.is_closed && el.junctions.front().p != el.junctions.back().p)
return false;
return true;
}());
int start_perimeter = int(perimeters.size()) - 1;
int end_perimeter = -1;
int direction = -1;
int start_perimeter = int(perimeters.size()) - 1;
int end_perimeter = -1;
int direction = -1;
bool is_outer_wall_first =
this->config->wall_sequence == WallSequence::OuterInner ||
this->config->wall_sequence == WallSequence::InnerOuterInner;
bool is_outer_wall_first =
this->config->wall_sequence == WallSequence::OuterInner ||
this->config->wall_sequence == WallSequence::InnerOuterInner;
if (layer_id == 0){ // disable inner outer inner algorithm after the first layer
is_outer_wall_first =
this->config->wall_sequence == WallSequence::OuterInner;
}
if (is_outer_wall_first) {
start_perimeter = 0;
end_perimeter = int(perimeters.size());
direction = 1;
if (layer_id == 0){ // disable inner outer inner algorithm after the first layer
is_outer_wall_first =
this->config->wall_sequence == WallSequence::OuterInner;
}
if (is_outer_wall_first) {
start_perimeter = 0;
end_perimeter = int(perimeters.size());
direction = 1;
}
std::vector<Arachne::ExtrusionLine*> all_extrusions;
for (int perimeter_idx = start_perimeter; perimeter_idx != end_perimeter; perimeter_idx += direction) {
if (perimeters[perimeter_idx].empty())
continue;
for (Arachne::ExtrusionLine& wall : perimeters[perimeter_idx])
all_extrusions.emplace_back(&wall);
}
// Find topological order with constraints from extrusions_constrains.
std::vector<size_t> blocked(all_extrusions.size(), 0); // Value indicating how many extrusions it is blocking (preceding extrusions) an extrusion.
std::vector<std::vector<size_t>> blocking(all_extrusions.size()); // Each extrusion contains a vector of extrusions that are blocked by this extrusion.
std::unordered_map<const Arachne::ExtrusionLine*, size_t> map_extrusion_to_idx;
for (size_t idx = 0; idx < all_extrusions.size(); idx++)
map_extrusion_to_idx.emplace(all_extrusions[idx], idx);
auto extrusions_constrains = Arachne::WallToolPaths::getRegionOrder(all_extrusions, is_outer_wall_first);
for (auto [before, after] : extrusions_constrains) {
auto after_it = map_extrusion_to_idx.find(after);
++blocked[after_it->second];
blocking[map_extrusion_to_idx.find(before)->second].emplace_back(after_it->second);
}
std::vector<bool> processed(all_extrusions.size(), false); // Indicate that the extrusion was already processed.
Point current_position = all_extrusions.empty() ? Point::Zero() : all_extrusions.front()->junctions.front().p; // Some starting position.
std::vector<PerimeterGeneratorArachneExtrusion> ordered_extrusions; // To store our result in. At the end we'll std::swap.
ordered_extrusions.reserve(all_extrusions.size());
while (ordered_extrusions.size() < all_extrusions.size()) {
size_t best_candidate = 0;
double best_distance_sqr = std::numeric_limits<double>::max();
bool is_best_closed = false;
std::vector<size_t> available_candidates;
for (size_t candidate = 0; candidate < all_extrusions.size(); ++candidate) {
if (processed[candidate] || blocked[candidate])
continue; // Not a valid candidate.
available_candidates.push_back(candidate);
}
std::vector<Arachne::ExtrusionLine*> all_extrusions;
for (int perimeter_idx = start_perimeter; perimeter_idx != end_perimeter; perimeter_idx += direction) {
if (perimeters[perimeter_idx].empty())
std::sort(available_candidates.begin(), available_candidates.end(), [&all_extrusions](const size_t a_idx, const size_t b_idx) -> bool {
return all_extrusions[a_idx]->is_closed < all_extrusions[b_idx]->is_closed;
});
for (const size_t candidate_path_idx : available_candidates) {
auto& path = all_extrusions[candidate_path_idx];
if (path->junctions.empty()) { // No vertices in the path. Can't find the start position then or really plan it in. Put that at the end.
if (best_distance_sqr == std::numeric_limits<double>::max()) {
best_candidate = candidate_path_idx;
is_best_closed = path->is_closed;
}
continue;
for (Arachne::ExtrusionLine& wall : perimeters[perimeter_idx])
all_extrusions.emplace_back(&wall);
}
// Find topological order with constraints from extrusions_constrains.
std::vector<size_t> blocked(all_extrusions.size(), 0); // Value indicating how many extrusions it is blocking (preceding extrusions) an extrusion.
std::vector<std::vector<size_t>> blocking(all_extrusions.size()); // Each extrusion contains a vector of extrusions that are blocked by this extrusion.
std::unordered_map<const Arachne::ExtrusionLine*, size_t> map_extrusion_to_idx;
for (size_t idx = 0; idx < all_extrusions.size(); idx++)
map_extrusion_to_idx.emplace(all_extrusions[idx], idx);
auto extrusions_constrains = Arachne::WallToolPaths::getRegionOrder(all_extrusions, is_outer_wall_first);
for (auto [before, after] : extrusions_constrains) {
auto after_it = map_extrusion_to_idx.find(after);
++blocked[after_it->second];
blocking[map_extrusion_to_idx.find(before)->second].emplace_back(after_it->second);
}
std::vector<bool> processed(all_extrusions.size(), false); // Indicate that the extrusion was already processed.
Point current_position = all_extrusions.empty() ? Point::Zero() : all_extrusions.front()->junctions.front().p; // Some starting position.
std::vector<PerimeterGeneratorArachneExtrusion> ordered_extrusions; // To store our result in. At the end we'll std::swap.
ordered_extrusions.reserve(all_extrusions.size());
while (ordered_extrusions.size() < all_extrusions.size()) {
size_t best_candidate = 0;
double best_distance_sqr = std::numeric_limits<double>::max();
bool is_best_closed = false;
std::vector<size_t> available_candidates;
for (size_t candidate = 0; candidate < all_extrusions.size(); ++candidate) {
if (processed[candidate] || blocked[candidate])
continue; // Not a valid candidate.
available_candidates.push_back(candidate);
}
std::sort(available_candidates.begin(), available_candidates.end(), [&all_extrusions](const size_t a_idx, const size_t b_idx) -> bool {
return all_extrusions[a_idx]->is_closed < all_extrusions[b_idx]->is_closed;
});
for (const size_t candidate_path_idx : available_candidates) {
auto& path = all_extrusions[candidate_path_idx];
if (path->junctions.empty()) { // No vertices in the path. Can't find the start position then or really plan it in. Put that at the end.
if (best_distance_sqr == std::numeric_limits<double>::max()) {
best_candidate = candidate_path_idx;
is_best_closed = path->is_closed;
}
continue;
}
const Point candidate_position = path->junctions.front().p;
double distance_sqr = (current_position - candidate_position).cast<double>().norm();
if (distance_sqr < best_distance_sqr) { // Closer than the best candidate so far.
if (path->is_closed || (!path->is_closed && best_distance_sqr != std::numeric_limits<double>::max()) || (!path->is_closed && !is_best_closed)) {
best_candidate = candidate_path_idx;
best_distance_sqr = distance_sqr;
is_best_closed = path->is_closed;
}
const Point candidate_position = path->junctions.front().p;
double distance_sqr = (current_position - candidate_position).cast<double>().norm();
if (distance_sqr < best_distance_sqr) { // Closer than the best candidate so far.
if (path->is_closed || (!path->is_closed && best_distance_sqr != std::numeric_limits<double>::max()) || (!path->is_closed && !is_best_closed)) {
best_candidate = candidate_path_idx;
best_distance_sqr = distance_sqr;
is_best_closed = path->is_closed;
}
}
auto& best_path = all_extrusions[best_candidate];
ordered_extrusions.push_back({ best_path, best_path->is_contour() });
processed[best_candidate] = true;
for (size_t unlocked_idx : blocking[best_candidate])
blocked[unlocked_idx]--;
if (!best_path->junctions.empty()) { //If all paths were empty, the best path is still empty. We don't upate the current position then.
if (best_path->is_closed)
current_position = best_path->junctions[0].p; //We end where we started.
else
current_position = best_path->junctions.back().p; //Pick the other end from where we started.
}
}
// printf("New Layer: Layer ID %d\n",layer_id); //debug - new layer
if (this->config->wall_sequence == WallSequence::InnerOuterInner && layer_id > 0) { // only enable inner outer inner algorithm after first layer
if (ordered_extrusions.size() > 2) { // 3 walls minimum needed to do inner outer inner ordering
int position = 0; // index to run the re-ordering for multiple external perimeters in a single island.
int arr_i, arr_j = 0; // indexes to run through the walls in the for loops
int outer, first_internal, second_internal, max_internal, current_perimeter; // allocate index values
// To address any remaining scenarios where the outer perimeter contour is not first on the list as arachne sometimes reorders the perimeters when clustering
// for OI mode that is used the basis for IOI
bringContoursToFront(ordered_extrusions);
std::vector<PerimeterGeneratorArachneExtrusion> reordered_extrusions;
// Debug statement to print spacing values:
//printf("External threshold - Ext perimeter: %d Ext spacing: %d Int perimeter: %d Int spacing: %d\n", this->ext_perimeter_flow.scaled_width(),this->ext_perimeter_flow.scaled_spacing(),this->perimeter_flow.scaled_width(), this->perimeter_flow.scaled_spacing());
auto& best_path = all_extrusions[best_candidate];
ordered_extrusions.push_back({ best_path, best_path->is_contour() });
processed[best_candidate] = true;
for (size_t unlocked_idx : blocking[best_candidate])
blocked[unlocked_idx]--;
// Get searching thresholds. For an external perimeter we take the external perimeter spacing/2 plus the internal perimeter spacing/2 and expand by the factor
// rounding errors. When precise wall is enabled, the external perimeter full spacing is used.
coord_t threshold_external = (apply_precise_outer_wall)
// Precise outer wall: use the full external spacing
? ( this->ext_perimeter_flow.scaled_spacing()
+ this->perimeter_flow.scaled_spacing()/2.0 )
// Normal: half ext spacing plus half int spacing
: ( this->ext_perimeter_flow.scaled_spacing()/2.0
+ this->perimeter_flow.scaled_spacing()/2.0 );
if (!best_path->junctions.empty()) { //If all paths were empty, the best path is still empty. We don't upate the current position then.
if (best_path->is_closed)
current_position = best_path->junctions[0].p; //We end where we started.
else
current_position = best_path->junctions.back().p; //Pick the other end from where we started.
}
}
// printf("New Layer: Layer ID %d\n",layer_id); //debug - new layer
if (this->config->wall_sequence == WallSequence::InnerOuterInner && layer_id > 0) { // only enable inner outer inner algorithm after first layer
if (ordered_extrusions.size() > 2) { // 3 walls minimum needed to do inner outer inner ordering
int position = 0; // index to run the re-ordering for multiple external perimeters in a single island.
int arr_i, arr_j = 0; // indexes to run through the walls in the for loops
int outer, first_internal, second_internal, max_internal, current_perimeter; // allocate index values
// For the intenal perimeter threshold, the distance is the internal perimeter spacing expanded by the factor to cover rounding errors.
coord_t threshold_internal = this->perimeter_flow.scaled_spacing();
// To address any remaining scenarios where the outer perimeter contour is not first on the list as arachne sometimes reorders the perimeters when clustering
// for OI mode that is used the basis for IOI
bringContoursToFront(ordered_extrusions);
std::vector<PerimeterGeneratorArachneExtrusion> reordered_extrusions;
// Re-order extrusions based on distance
// Alorithm will aggresively optimise for the appearance of the outermost perimeter
ordered_extrusions = reorderPerimetersByProximity(ordered_extrusions,threshold_external,threshold_internal );
reordered_extrusions = ordered_extrusions; // copy them into the reordered extrusions vector to allow for IOI operations to be performed below without altering the base ordered extrusions list.
// Debug statement to print spacing values:
//printf("External threshold - Ext perimeter: %d Ext spacing: %d Int perimeter: %d Int spacing: %d\n", this->ext_perimeter_flow.scaled_width(),this->ext_perimeter_flow.scaled_spacing(),this->perimeter_flow.scaled_width(), this->perimeter_flow.scaled_spacing());
// Get searching thresholds. For an external perimeter we take the external perimeter spacing/2 plus the internal perimeter spacing/2 and expand by the factor
// rounding errors. When precise wall is enabled, the external perimeter full spacing is used.
coord_t threshold_external = (apply_precise_outer_wall)
// Precise outer wall ⇒ use “full external spacing”
? ( this->ext_perimeter_flow.scaled_spacing()
+ this->perimeter_flow.scaled_spacing()/2.0 )
// Normal ⇒ half ext spacing + half int spacing
: ( this->ext_perimeter_flow.scaled_spacing()/2.0
+ this->perimeter_flow.scaled_spacing()/2.0 );
// Now start the sandwich mode wall re-ordering using the reordered_extrusions as the basis
// scan to find the external perimeter, first internal, second internal and last perimeter in the island.
// We then advance the position index to move to the second island and continue until there are no more
// perimeters left.
while (position < reordered_extrusions.size()) {
outer = first_internal = second_internal = current_perimeter = -1; // initialise all index values to -1
max_internal = reordered_extrusions.size()-1; // initialise the maximum internal perimeter to the last perimeter on the extrusion list
// run through the walls to get the index values that need re-ordering until the first one for each
// is found. Start at "position" index to enable the for loop to iterate for multiple external
// perimeters in a single island
// printf("Reorder Loop. Position %d, extrusion list size: %d, Outer index %d, inner index %d, second inner index %d\n", position, reordered_extrusions.size(),outer,first_internal,second_internal);
for (arr_i = position; arr_i < reordered_extrusions.size(); ++arr_i) {
// printf("Perimeter: extrusion inset index %d, ordered extrusions array position %d\n",reordered_extrusions[arr_i].extrusion->inset_idx, arr_i);
switch (reordered_extrusions[arr_i].extrusion->inset_idx) {
case 0: // external perimeter
if (outer == -1)
outer = arr_i;
break;
case 1: // first internal wall
if (first_internal==-1 && arr_i>outer && outer!=-1){
first_internal = arr_i;
}
break;
case 2: // second internal wall
if (second_internal == -1 && arr_i > first_internal && outer!=-1){
second_internal = arr_i;
}
break;
}
if(outer >-1 && first_internal>-1 && reordered_extrusions[arr_i].extrusion->inset_idx == 0){ // found a new external perimeter after we've found at least a first internal perimeter to re-order.
// This means we entered a new island.
arr_i=arr_i-1; //step back one perimeter
max_internal = arr_i; // new maximum internal perimeter is now this as we have found a new external perimeter, hence a new island.
break; // exit the for loop
}
}
// printf("Layer ID %d, Outer index %d, inner index %d, second inner index %d, maximum internal perimeter %d \n",layer_id,outer,first_internal,second_internal, max_internal);
if (outer > -1 && first_internal > -1 && second_internal > -1) { // found all three perimeters to re-order? If not the perimeters will be processed outside in.
std::vector<PerimeterGeneratorArachneExtrusion> inner_outer_extrusions; // temporary array to hold extrusions for reordering
inner_outer_extrusions.resize(max_internal - position + 1); // reserve array containing the number of perimeters before a new island. Variables are array indexes hence need to add +1 to convert to position allocations
// printf("Allocated array size %d, max_internal index %d, start position index %d \n",max_internal-position+1,max_internal,position);
for (arr_j = max_internal; arr_j >=position; --arr_j){ // go inside out towards the external perimeter (perimeters in reverse order) and store all internal perimeters until the first one identified with inset index 2
if(arr_j >= second_internal){
//printf("Inside out loop: Mapped perimeter index %d to array position %d\n", arr_j, max_internal-arr_j);
inner_outer_extrusions[max_internal-arr_j] = reordered_extrusions[arr_j];
current_perimeter++;
// For the intenal perimeter threshold, the distance is the internal perimeter spacing expanded by the factor to cover rounding errors.
coord_t threshold_internal = this->perimeter_flow.scaled_spacing();
// Re-order extrusions based on distance
// Alorithm will aggresively optimise for the appearance of the outermost perimeter
ordered_extrusions = reorderPerimetersByProximity(ordered_extrusions,threshold_external,threshold_internal );
reordered_extrusions = ordered_extrusions; // copy them into the reordered extrusions vector to allow for IOI operations to be performed below without altering the base ordered extrusions list.
// Now start the sandwich mode wall re-ordering using the reordered_extrusions as the basis
// scan to find the external perimeter, first internal, second internal and last perimeter in the island.
// We then advance the position index to move to the second island and continue until there are no more
// perimeters left.
while (position < reordered_extrusions.size()) {
outer = first_internal = second_internal = current_perimeter = -1; // initialise all index values to -1
max_internal = reordered_extrusions.size()-1; // initialise the maximum internal perimeter to the last perimeter on the extrusion list
// run through the walls to get the index values that need re-ordering until the first one for each
// is found. Start at "position" index to enable the for loop to iterate for multiple external
// perimeters in a single island
// printf("Reorder Loop. Position %d, extrusion list size: %d, Outer index %d, inner index %d, second inner index %d\n", position, reordered_extrusions.size(),outer,first_internal,second_internal);
for (arr_i = position; arr_i < reordered_extrusions.size(); ++arr_i) {
// printf("Perimeter: extrusion inset index %d, ordered extrusions array position %d\n",reordered_extrusions[arr_i].extrusion->inset_idx, arr_i);
switch (reordered_extrusions[arr_i].extrusion->inset_idx) {
case 0: // external perimeter
if (outer == -1)
outer = arr_i;
break;
case 1: // first internal wall
if (first_internal==-1 && arr_i>outer && outer!=-1){
first_internal = arr_i;
}
}
for (arr_j = position; arr_j < second_internal; ++arr_j){ // go outside in and map the remaining perimeters (external and first internal wall(s)) using the outside in wall order
// printf("Outside in loop: Mapped perimeter index %d to array position %d\n", arr_j, current_perimeter+1);
inner_outer_extrusions[++current_perimeter] = reordered_extrusions[arr_j];
}
for(arr_j = position; arr_j <= max_internal; ++arr_j) // replace perimeter array with the new re-ordered array
ordered_extrusions[arr_j] = inner_outer_extrusions[arr_j-position];
break;
case 2: // second internal wall
if (second_internal == -1 && arr_i > first_internal && outer!=-1){
second_internal = arr_i;
}
break;
}
if(outer >-1 && first_internal>-1 && reordered_extrusions[arr_i].extrusion->inset_idx == 0){ // found a new external perimeter after we've found at least a first internal perimeter to re-order.
// This means we entered a new island.
arr_i=arr_i-1; //step back one perimeter
max_internal = arr_i; // new maximum internal perimeter is now this as we have found a new external perimeter, hence a new island.
break; // exit the for loop
}
// go to the next perimeter from the current position to continue scanning for external walls in the same island
position = arr_i + 1;
}
// printf("Layer ID %d, Outer index %d, inner index %d, second inner index %d, maximum internal perimeter %d \n",layer_id,outer,first_internal,second_internal, max_internal);
if (outer > -1 && first_internal > -1 && second_internal > -1) { // found all three perimeters to re-order? If not the perimeters will be processed outside in.
std::vector<PerimeterGeneratorArachneExtrusion> inner_outer_extrusions; // temporary array to hold extrusions for reordering
inner_outer_extrusions.resize(max_internal - position + 1); // reserve array containing the number of perimeters before a new island. Variables are array indexes hence need to add +1 to convert to position allocations
// printf("Allocated array size %d, max_internal index %d, start position index %d \n",max_internal-position+1,max_internal,position);
for (arr_j = max_internal; arr_j >=position; --arr_j){ // go inside out towards the external perimeter (perimeters in reverse order) and store all internal perimeters until the first one identified with inset index 2
if(arr_j >= second_internal){
//printf("Inside out loop: Mapped perimeter index %d to array position %d\n", arr_j, max_internal-arr_j);
inner_outer_extrusions[max_internal-arr_j] = reordered_extrusions[arr_j];
current_perimeter++;
}
}
for (arr_j = position; arr_j < second_internal; ++arr_j){ // go outside in and map the remaining perimeters (external and first internal wall(s)) using the outside in wall order
// printf("Outside in loop: Mapped perimeter index %d to array position %d\n", arr_j, current_perimeter+1);
inner_outer_extrusions[++current_perimeter] = reordered_extrusions[arr_j];
}
for(arr_j = position; arr_j <= max_internal; ++arr_j) // replace perimeter array with the new re-ordered array
ordered_extrusions[arr_j] = inner_outer_extrusions[arr_j-position];
}
// go to the next perimeter from the current position to continue scanning for external walls in the same island
position = arr_i + 1;
}
}
}
bool steep_overhang_contour = false;
bool steep_overhang_hole = false;
if (!config->overhang_reverse) {
// Skip steep overhang detection no reverse is specified
steep_overhang_contour = true;
steep_overhang_hole = true;
}
if (ExtrusionEntityCollection extrusion_coll = traverse_extrusions(*this, ordered_extrusions, steep_overhang_contour, steep_overhang_hole); !extrusion_coll.empty()) {
if (config->overhang_reverse) {
reorient_perimeters(extrusion_coll, steep_overhang_contour, steep_overhang_hole,
this->config->overhang_reverse_internal_only);
}
defer_unsupported_loops(*this, extrusion_coll);
result.loops = std::move(extrusion_coll);
result.has_loops = true;
bool steep_overhang_contour = false;
bool steep_overhang_hole = false;
if (!config->overhang_reverse) {
// Skip steep overhang detection no reverse is specified
steep_overhang_contour = true;
steep_overhang_hole = true;
}
if (ExtrusionEntityCollection extrusion_coll = traverse_extrusions(*this, ordered_extrusions, steep_overhang_contour, steep_overhang_hole); !extrusion_coll.empty()) {
if (config->overhang_reverse) {
reorient_perimeters(extrusion_coll, steep_overhang_contour, steep_overhang_hole,
this->config->overhang_reverse_internal_only);
}
defer_unsupported_loops(*this, extrusion_coll);
this->loops->append(extrusion_coll);
}
const coord_t spacing = (perimeters.size() == 1) ? ext_perimeter_spacing2 : perimeter_spacing;
const coord_t spacing = (perimeters.size() == 1) ? ext_perimeter_spacing2 : perimeter_spacing;
if (offset_ex(infill_contour, -float(spacing / 2.)).empty())
infill_contour.clear(); // Infill region is too small, so let's filter it out.
if (offset_ex(infill_contour, -float(spacing / 2.)).empty())
infill_contour.clear(); // Infill region is too small, so let's filter it out.
// create one more offset to be used as boundary for fill
// we offset by half the perimeter spacing (to get to the actual infill boundary)
// and then we offset back and forth by half the infill spacing to only consider the
// non-collapsing regions
coord_t inset =
(loop_number < 0) ? 0 :
(loop_number == 0) ?
// one loop
ext_perimeter_spacing :
// two or more loops?
perimeter_spacing;
coord_t top_inset = inset;
// create one more offset to be used as boundary for fill
// we offset by half the perimeter spacing (to get to the actual infill boundary)
// and then we offset back and forth by half the infill spacing to only consider the
// non-collapsing regions
coord_t inset =
(loop_number < 0) ? 0 :
(loop_number == 0) ?
// one loop
ext_perimeter_spacing :
// two or more loops?
perimeter_spacing;
coord_t top_inset = inset;
top_inset = coord_t(scale_(this->config->top_bottom_infill_wall_overlap.get_abs_value(unscale<double>(inset))));
if(is_topmost_layer || is_bottom_layer)
inset = coord_t(scale_(this->config->top_bottom_infill_wall_overlap.get_abs_value(unscale<double>(inset))));
else
inset = coord_t(scale_(this->config->infill_wall_overlap.get_abs_value(unscale<double>(inset))));
top_inset = coord_t(scale_(this->config->top_bottom_infill_wall_overlap.get_abs_value(unscale<double>(inset))));
if(is_topmost_layer || is_bottom_layer)
inset = coord_t(scale_(this->config->top_bottom_infill_wall_overlap.get_abs_value(unscale<double>(inset))));
else
inset = coord_t(scale_(this->config->infill_wall_overlap.get_abs_value(unscale<double>(inset))));
// simplify infill contours according to resolution
Polygons pp;
for (ExPolygon& ex : infill_contour)
ex.simplify_p(m_scaled_resolution, &pp);
ExPolygons not_filled_exp = union_ex(pp);
// collapse too narrow infill areas
const auto min_perimeter_infill_spacing = coord_t(solid_infill_spacing * (1. - INSET_OVERLAP_TOLERANCE));
// simplify infill contours according to resolution
Polygons pp;
for (ExPolygon& ex : infill_contour)
ex.simplify_p(m_scaled_resolution, &pp);
ExPolygons not_filled_exp = union_ex(pp);
// collapse too narrow infill areas
const auto min_perimeter_infill_spacing = coord_t(solid_infill_spacing * (1. - INSET_OVERLAP_TOLERANCE));
ExPolygons infill_exp = offset2_ex(
ExPolygons infill_exp = offset2_ex(
not_filled_exp,
float(-min_perimeter_infill_spacing / 2.),
float(inset + min_perimeter_infill_spacing / 2.));
// append infill areas to fill_surfaces
if (!top_expolygons.empty()) {
infill_exp = union_ex(infill_exp, offset_ex(top_expolygons, double(top_inset)));
}
this->fill_surfaces->append(infill_exp, stInternal);
apply_extra_perimeters(infill_exp);
// BBS: get the no-overlap infill expolygons
{
ExPolygons polyWithoutOverlap;
polyWithoutOverlap = offset2_ex(
not_filled_exp,
float(-min_perimeter_infill_spacing / 2.),
float(inset + min_perimeter_infill_spacing / 2.));
// append infill areas to fill_surfaces
if (!top_expolygons.empty()) {
infill_exp = union_ex(infill_exp, offset_ex(top_expolygons, double(top_inset)));
}
result.infill = std::move(infill_exp);
// BBS: get the no-overlap infill expolygons
{
ExPolygons polyWithoutOverlap;
polyWithoutOverlap = offset2_ex(
not_filled_exp,
float(-min_perimeter_infill_spacing / 2.),
float(+min_perimeter_infill_spacing / 2.));
if (!top_expolygons.empty())
polyWithoutOverlap = union_ex(polyWithoutOverlap, top_expolygons);
result.no_overlap = std::move(polyWithoutOverlap);
}
}
});
for (ArachneSurfaceResult &result : results) {
if (result.has_loops)
// Moved, not copied: append(const ExtrusionEntity &) clones the whole wall tree of the island.
this->loops->append(std::move(result.loops));
this->fill_surfaces->append(result.infill, stInternal);
apply_extra_perimeters(result.infill);
append(*this->fill_no_overlap, std::move(result.no_overlap));
float(+min_perimeter_infill_spacing / 2.));
if (!top_expolygons.empty())
polyWithoutOverlap = union_ex(polyWithoutOverlap, top_expolygons);
this->fill_no_overlap->insert(this->fill_no_overlap->end(), polyWithoutOverlap.begin(), polyWithoutOverlap.end());
}
}
}
+2 -2
View File
@@ -34,7 +34,7 @@ public:
explicit Polygon(const Points &points) : MultiPoint(points) {}
Polygon(std::initializer_list<Point> points) : MultiPoint(points) {}
Polygon(const Polygon &other) : MultiPoint(other.points) {}
Polygon(Polygon &&other) noexcept : MultiPoint(std::move(other.points)) {}
Polygon(Polygon &&other) : MultiPoint(std::move(other.points)) {}
static Polygon new_scale(const std::vector<Vec2d> &points) {
Polygon pgn;
pgn.points.reserve(points.size());
@@ -43,7 +43,7 @@ public:
return pgn;
}
Polygon& operator=(const Polygon &other) { points = other.points; return *this; }
Polygon& operator=(Polygon &&other) noexcept { points = std::move(other.points); return *this; }
Polygon& operator=(Polygon &&other) { points = std::move(other.points); return *this; }
Point& operator[](Points::size_type idx) { return this->points[idx]; }
const Point& operator[](Points::size_type idx) const { return this->points[idx]; }
+2 -2
View File
@@ -26,7 +26,7 @@ class Polyline : public MultiPoint {
public:
Polyline() {};
Polyline(const Polyline& other) : MultiPoint(other.points), fitting_result(other.fitting_result) {}
Polyline(Polyline &&other) noexcept : MultiPoint(std::move(other.points)), fitting_result(std::move(other.fitting_result)) {}
Polyline(Polyline &&other) : MultiPoint(std::move(other.points)), fitting_result(std::move(other.fitting_result)) {}
Polyline(std::initializer_list<Point> list) : MultiPoint(list) {
fitting_result.clear();
}
@@ -47,7 +47,7 @@ public:
fitting_result = other.fitting_result;
return *this;
}
Polyline& operator=(Polyline&& other) noexcept {
Polyline& operator=(Polyline&& other) {
points = std::move(other.points);
fitting_result = std::move(other.fitting_result);
return *this;
+13 -11
View File
@@ -626,13 +626,15 @@ const PresetBundle *PresetBundle::load_source_vendor(const boost::filesystem::pa
return it->second.get();
// The library loads with no base of its own, so the tree a vendor inherits from
// is the same one that resolves the library's own presets.
const std::string library_file = std::string(ORCA_FILAMENT_LIBRARY);
const PresetBundle *library = nullptr;
if (vendor_id != ORCA_FILAMENT_LIBRARY &&
(boost::filesystem::is_regular_file(root_dir / (library_file + ".json")) ||
(allow_cache && boost::filesystem::is_regular_file(root_dir / (library_file + ".opc"))))) {
library = load_source_vendor(root_dir, ORCA_FILAMENT_LIBRARY, compatibility_rule, error, allow_cache);
// is the same one that resolves the library's own presets. It is only a base, so
// it comes from its cache whenever that is all that is installed, even when the
// vendor itself is parsed (a vendor updated over the air).
const std::string library_file = std::string(ORCA_FILAMENT_LIBRARY);
const bool library_json = boost::filesystem::is_regular_file(root_dir / (library_file + ".json"));
const bool library_cache_only = !library_json && boost::filesystem::is_regular_file(root_dir / (library_file + ".opc"));
const PresetBundle *library = nullptr;
if (vendor_id != ORCA_FILAMENT_LIBRARY && (library_json || library_cache_only)) {
library = load_source_vendor(root_dir, ORCA_FILAMENT_LIBRARY, compatibility_rule, error, allow_cache || library_cache_only);
if (library == nullptr) {
error = "OrcaFilamentLibrary contains invalid presets";
return nullptr;
@@ -2576,7 +2578,7 @@ void PresetBundle::clear_printer_hold_aliases()
//BBS: add json related logic, load system presets from json
std::pair<PresetsConfigSubstitutions, std::string> PresetBundle::load_system_presets_from_json(
ForwardCompatibilitySubstitutionRule compatibility_rule, bool allow_cache)
ForwardCompatibilitySubstitutionRule compatibility_rule, bool write_caches)
{
//BBS: add config related logs
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" enter, compatibility_rule %1%")%compatibility_rule;
@@ -2596,14 +2598,14 @@ std::pair<PresetsConfigSubstitutions, std::string> PresetBundle::load_system_pre
// The vendors below are loaded whole and against each other — the filament
// library first, then every other vendor with it as the base — so each parse
// is complete enough to be worth caching.
m_generate_vendor_caches = allow_cache && (m_generate_vendor_caches || !validation_mode);
m_generate_vendor_caches = write_caches && (m_generate_vendor_caches || !validation_mode);
// Sorted, so any duplicate-preset warning comes out in the same order on every run.
std::vector<VendorSource> vendors;
for (const std::string& name : vendor_names_in(dir))
if (name == ORCA_FILAMENT_LIBRARY || !(validation_mode && !vendor_to_validate.empty() && name != vendor_to_validate))
vendors.push_back({ name, dir });
auto result = this->load_vendors(vendors, compatibility_rule, allow_cache);
auto result = this->load_vendors(vendors, compatibility_rule, true);
this->update_system_maps();
@@ -5310,7 +5312,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())
+6 -1
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);
@@ -848,7 +851,9 @@ private:
//std::pair<PresetsConfigSubstitutions, std::string> load_system_presets(ForwardCompatibilitySubstitutionRule compatibility_rule);
//BBS: add json related logic
std::pair<PresetsConfigSubstitutions, std::string> load_system_presets_from_json(ForwardCompatibilitySubstitutionRule compatibility_rule, bool allow_cache = true);
// Reads each vendor from its preset cache where one covers the profile, as every load does.
// write_caches = false keeps a read-only load from writing caches into the data directory.
std::pair<PresetsConfigSubstitutions, std::string> load_system_presets_from_json(ForwardCompatibilitySubstitutionRule compatibility_rule, bool write_caches = true);
// Update the multicolor information for filaments.
void update_filament_multi_color();
// Update renamed_from and alias maps of system profiles.
+186 -356
View File
@@ -47,7 +47,6 @@
#include <cstdlib>
#include <cstdint>
#include <float.h>
#include <array>
#include <functional>
#include <ios>
#include <iomanip>
@@ -73,7 +72,6 @@
#include <boost/log/trivial.hpp>
#include <tbb/parallel_for.h>
#include <tbb/parallel_invoke.h>
#include <tbb/spin_mutex.h>
#include <tbb/concurrent_unordered_set.h>
@@ -1697,9 +1695,7 @@ void PrintObject::detect_surfaces_type()
bool interface_shells = ! spiral_mode && m_config.interface_shells.value;
size_t num_layers = spiral_mode ? std::min(size_t(this->printing_region(0).config().bottom_shell_layers), m_layers.size()) : m_layers.size();
// The regions of a layer do not see each other here, and a layer cut through a fine relief takes far longer than the
// others, so the regions run next to each other instead of one after another, each still over all layers.
tbb::parallel_for(size_t(0), this->num_printing_regions(), [&](size_t region_id) {
for (size_t region_id = 0; region_id < this->num_printing_regions(); ++ region_id) {
BOOST_LOG_TRIVIAL(debug) << "Detecting solid surfaces for region " << region_id << " in parallel - start";
#ifdef SLIC3R_DEBUG_SLICE_PROCESSING
for (Layer *layer : m_layers)
@@ -1757,7 +1753,7 @@ void PrintObject::detect_surfaces_type()
if (upper_layer) {
ExPolygons upper_slices = interface_shells ?
diff_ex(layerm_slices_surfaces, upper_layer->m_regions[region_id]->slices.surfaces, ApplySafetyOffset::Yes) :
diff_ex_by_piece(layerm_slices_surfaces, to_polygons(upper_layer->lslices), ApplySafetyOffset::Yes);
diff_ex(layerm_slices_surfaces, upper_layer->lslices, ApplySafetyOffset::Yes);
surfaces_append(top, opening_ex(upper_slices, offset), stTop);
} else {
// if no upper layer, all surfaces of this one are solid
@@ -1783,7 +1779,7 @@ void PrintObject::detect_surfaces_type()
surfaces_append(
bottom,
opening_ex(
diff_ex_by_piece(layerm_slices_surfaces, to_polygons(lower_layer->lslices), ApplySafetyOffset::Yes),
diff_ex(layerm_slices_surfaces, lower_layer->lslices, ApplySafetyOffset::Yes),
offset),
surface_type_bottom_other);
// if user requested internal shells, we need to identify surfaces
@@ -1814,44 +1810,34 @@ void PrintObject::detect_surfaces_type()
// and top surfaces; let's do an intersection to discover them and consider them
// as bottom surfaces (to allow for bridge detection)
if (! top.empty() && ! bottom.empty()) {
const auto cracks = intersection_ex_by_piece(to_expolygons(top), to_polygons(bottom));
const auto cracks = intersection_ex(top, bottom);
if (!cracks.empty()) {
if (lower_layer) { // Only detect small cracks for non-first layer, because first layer should always be bottom
const float small_crack_threshold = -layerm->flow(frExternalPerimeter).scaled_width() * 1.5;
// Only the bottom surfaces near a crack can take part: one that contains it must contain its box,
// and one whose box misses the grown crack is left unchanged by removing it. A layer cut through
// a fine relief has thousands of both, which made this loop quadratic.
for (const auto& crack : cracks) {
if (offset_ex(crack, small_crack_threshold).empty()) {
// For small cracks, if it's part of a large bottom surface, then it should be added to bottom as well
const BoundingBox crack_bbox = get_extents(crack);
if (std::any_of(bottom.begin(), bottom.end(), [&crack, &crack_bbox, small_crack_threshold](const Surface& s) {
if (std::any_of(bottom.begin(), bottom.end(), [&crack, small_crack_threshold](const Surface& s) {
const auto& se = s.expolygon;
return get_extents(se).inflated(SCALED_EPSILON).contains(crack_bbox)
&& diff_ex(crack, se, ApplySafetyOffset::Yes).empty()
return diff_ex(crack, se, ApplySafetyOffset::Yes).empty()
&& se.area() > crack.area() * 2
&& !offset_ex(diff_ex(se, crack), small_crack_threshold).empty();
})) continue;
// Crack too small, leave it as part of the top surface, remove it from bottom surfaces
const ExPolygons grown_crack = offset_ex(crack, -small_crack_threshold);
const BoundingBox grown_bbox = get_extents(grown_crack);
Surfaces bot_tmp;
for (auto& b : bottom) {
if (get_extents(b.expolygon).overlap(grown_bbox))
surfaces_append(bot_tmp, diff_ex(b.expolygon, grown_crack), b.surface_type);
else
bot_tmp.emplace_back(std::move(b));
surfaces_append(bot_tmp, diff_ex(b.expolygon, offset_ex(crack, -small_crack_threshold)), b.surface_type);
}
bottom = std::move(bot_tmp);
}
}
}
ExPolygons top_expolygons = to_expolygons(std::move(top));
Polygons top_polygons = to_polygons(std::move(top));
top.clear();
surfaces_append(top, diff_ex_by_piece(top_expolygons, to_polygons(bottom)), stTop);
surfaces_append(top, diff_ex(top_polygons, bottom), stTop);
}
}
@@ -1942,7 +1928,7 @@ void PrintObject::detect_surfaces_type()
{
Polygons topbottom = to_polygons(top);
polygons_append(topbottom, to_polygons(bottom));
surfaces_append(surfaces_out, diff_ex_by_piece(surfaces_prev_expolys, topbottom), stInternal);
surfaces_append(surfaces_out, diff_ex(surfaces_prev_expolys, topbottom), stInternal);
}
surfaces_append(surfaces_out, std::move(top));
@@ -2119,31 +2105,29 @@ void PrintObject::detect_surfaces_type()
}
}
);
// ==============================================================================================================
// === ORCA: Interim workaround - for now the new stInternalAfterExternalBridge surfaace is re-classified ==============
// === back to a bottom bridge. As a starting point, this improves bridging reliability as it extrudes ==========
// === two external bridge layers. However, TODO: Implement a new surface type throughout the codebase ==========
// ==============================================================================================================
for (size_t region_id = 0; region_id < this->num_printing_regions(); ++region_id) {
tbb::parallel_for( tbb::blocked_range<size_t>(0, m_layers.size()), [this, region_id](const tbb::blocked_range<size_t> &range) {
for (size_t idx_layer = range.begin(); idx_layer < range.end(); ++idx_layer) {
Surfaces &surfs = m_layers[idx_layer]->m_regions[region_id]->slices.surfaces;
for (Surface &s : surfs) {
if (s.surface_type == stInternalAfterExternalBridge) {
s.surface_type = stBottomBridge;
}
}
}
}
);
}
}
// ==============================================================================================================
// === ORCA: End of second external bridge layer changes =======================================================
// ==============================================================================================================
}); // for each this->print->region_count
// ==============================================================================================================
// === ORCA: Interim workaround - for now the new stInternalAfterExternalBridge surfaace is re-classified ==============
// === back to a bottom bridge. As a starting point, this improves bridging reliability as it extrudes ==========
// === two external bridge layers. However, TODO: Implement a new surface type throughout the codebase ==========
// ==============================================================================================================
// Once all the regions have their second bridge layer, and before their slices are trimmed into fill surfaces below.
if ((this->config().enable_extra_bridge_layer.value == eblApplyToAll) || (this->config().enable_extra_bridge_layer.value == eblExternalBridgeOnly)) {
tbb::parallel_for(tbb::blocked_range<size_t>(0, m_layers.size()), [this](const tbb::blocked_range<size_t> &range) {
for (size_t idx_layer = range.begin(); idx_layer < range.end(); ++idx_layer)
for (LayerRegion *layerm : m_layers[idx_layer]->regions())
for (Surface &s : layerm->slices.surfaces)
if (s.surface_type == stInternalAfterExternalBridge)
s.surface_type = stBottomBridge;
});
m_print->throw_if_canceled();
}
tbb::parallel_for(size_t(0), this->num_printing_regions(), [&](size_t region_id) {
BOOST_LOG_TRIVIAL(debug) << "Detecting solid surfaces for region " << region_id << " - clipping in parallel - start";
// Fill in layerm->fill_surfaces by trimming the layerm->slices by the cummulative layerm->fill_surfaces.
tbb::parallel_for(
@@ -2160,7 +2144,7 @@ void PrintObject::detect_surfaces_type()
});
m_print->throw_if_canceled();
BOOST_LOG_TRIVIAL(debug) << "Detecting solid surfaces for region " << region_id << " - clipping in parallel - end";
});
} // for each this->print->region_count
// Mark the object to have the region slices classified (typed, which also means they are split based on whether they are supported, bridging, top layers etc.)
m_typed_slices = true;
@@ -2227,10 +2211,8 @@ void PrintObject::process_external_surfaces()
BOOST_LOG_TRIVIAL(debug) << "Collecting surfaces covered with extrusions in parallel - end";
}
BOOST_LOG_TRIVIAL(debug) << "Processing external surfaces in parallel - start";
// The regions of a layer do not see each other here, and a layer cut through a fine relief takes far longer than the
// others, so the regions run next to each other instead of one after another, each still over all layers.
tbb::parallel_for(size_t(0), this->num_printing_regions(), [this, &surfaces_covered](size_t region_id) {
for (size_t region_id = 0; region_id < this->num_printing_regions(); ++region_id) {
BOOST_LOG_TRIVIAL(debug) << "Processing external surfaces for region " << region_id << " in parallel - start";
tbb::parallel_for(
tbb::blocked_range<size_t>(0, m_layers.size()),
[this, &surfaces_covered, region_id](const tbb::blocked_range<size_t>& range) {
@@ -2245,9 +2227,9 @@ void PrintObject::process_external_surfaces()
}
}
);
});
m_print->throw_if_canceled();
BOOST_LOG_TRIVIAL(debug) << "Processing external surfaces in parallel - end";
m_print->throw_if_canceled();
BOOST_LOG_TRIVIAL(debug) << "Processing external surfaces for region " << region_id << " in parallel - end";
}
}
void PrintObject::discover_vertical_shells()
@@ -2286,10 +2268,10 @@ void PrintObject::discover_vertical_shells()
// The "ensure vertical wall thickness" feature is not applicable to any of the regions. Quit.
return;
BOOST_LOG_TRIVIAL(debug) << "Discovering vertical shells in parallel - start : cache top / bottom";
// One layer per task: on a layer cut through a fine relief the unions below take far longer than elsewhere, and a
// few such layers next to each other must not end up in one task.
//FIXME Improve the heuristics for a grain size.
size_t grain_size = std::max(num_layers / 16, size_t(1));
tbb::parallel_for(
tbb::blocked_range<size_t>(0, num_layers, 1),
tbb::blocked_range<size_t>(0, num_layers, grain_size),
[this, &cache_top_botom_regions](const tbb::blocked_range<size_t>& range) {
const std::initializer_list<SurfaceType> surfaces_bottom { stBottom, stBottomBridge };
const size_t num_regions = this->num_printing_regions();
@@ -2297,198 +2279,67 @@ void PrintObject::discover_vertical_shells()
m_print->throw_if_canceled();
const Layer &layer = *m_layers[idx_layer];
DiscoverVerticalShellsCacheEntry &cache = cache_top_botom_regions[idx_layer];
const auto top_bottom_expansion = [&layer](size_t region_id) {
return float(layer.m_regions[region_id]->flow(frSolidInfill).scaled_spacing()) * top_bottom_expansion_coeff;
};
// Simulate single set of perimeters over all merged regions.
float perimeter_offset = 0.f;
float perimeter_min_spacing = FLT_MAX;
#ifdef SLIC3R_DEBUG_SLICE_PROCESSING
static size_t debug_idx = 0;
++ debug_idx;
#endif /* SLIC3R_DEBUG_SLICE_PROCESSING */
// The top surfaces, the bottom surfaces and the holes are independent of each other.
tbb::parallel_invoke(
[&]() {
for (size_t region_id = 0; region_id < num_regions; ++ region_id)
append(cache.top_surfaces, offset(layer.m_regions[region_id]->slices.filter_by_type(stTop), top_bottom_expansion(region_id)));
// append(cache.top_surfaces, offset(layerm.fill_surfaces.filter_by_type(stTop), top_bottom_expansion));
// Save some computing time by reducing the number of polygons.
cache.top_surfaces = union_(cache.top_surfaces);
},
[&]() {
for (size_t region_id = 0; region_id < num_regions; ++ region_id)
append(cache.bottom_surfaces, offset(layer.m_regions[region_id]->slices.filter_by_types(surfaces_bottom), top_bottom_expansion(region_id)));
// append(cache.bottom_surfaces, offset(layerm.fill_surfaces.filter_by_types(surfaces_bottom), top_bottom_expansion));
cache.bottom_surfaces = union_(cache.bottom_surfaces);
},
[&]() {
// Simulate single set of perimeters over all merged regions.
float perimeter_offset = 0.f;
float perimeter_min_spacing = FLT_MAX;
for (size_t region_id = 0; region_id < num_regions; ++ region_id) {
const LayerRegion &layerm = *layer.m_regions[region_id];
// Calculate the maximum perimeter offset as if the slice was extruded with a single extruder only.
// First find the maxium number of perimeters per region slice.
unsigned int perimeters = 0;
for (const Surface &s : layerm.slices.surfaces)
perimeters = std::max<unsigned int>(perimeters, s.extra_perimeters);
perimeters += layerm.region().config().wall_loops.value;
// Then calculate the infill offset.
if (perimeters > 0) {
Flow extflow = layerm.flow(frExternalPerimeter);
Flow flow = layerm.flow(frPerimeter);
perimeter_offset = std::max(perimeter_offset,
0.5f * float(extflow.scaled_width() + extflow.scaled_spacing()) + (float(perimeters) - 1.f) * flow.scaled_spacing());
perimeter_min_spacing = std::min(perimeter_min_spacing, float(std::min(extflow.scaled_spacing(), flow.scaled_spacing())));
}
polygons_append(cache.holes, to_polygons(layerm.fill_expolygons));
}
// For a multi-material print, simulate perimeter / infill split as if only a single extruder has been used for the whole print.
if (perimeter_offset > 0.) {
// The layer.lslices are forced to merge by expanding them first.
polygons_append(cache.holes, offset2(layer.lslices, 0.3f * perimeter_min_spacing, - perimeter_offset - 0.3f * perimeter_min_spacing));
for (size_t region_id = 0; region_id < num_regions; ++ region_id) {
LayerRegion &layerm = *layer.m_regions[region_id];
float top_bottom_expansion = float(layerm.flow(frSolidInfill).scaled_spacing()) * top_bottom_expansion_coeff;
// Top surfaces.
append(cache.top_surfaces, offset(layerm.slices.filter_by_type(stTop), top_bottom_expansion));
// append(cache.top_surfaces, offset(layerm.fill_surfaces.filter_by_type(stTop), top_bottom_expansion));
// Bottom surfaces.
append(cache.bottom_surfaces, offset(layerm.slices.filter_by_types(surfaces_bottom), top_bottom_expansion));
// append(cache.bottom_surfaces, offset(layerm.fill_surfaces.filter_by_types(surfaces_bottom), top_bottom_expansion));
// Calculate the maximum perimeter offset as if the slice was extruded with a single extruder only.
// First find the maxium number of perimeters per region slice.
unsigned int perimeters = 0;
for (Surface &s : layerm.slices.surfaces)
perimeters = std::max<unsigned int>(perimeters, s.extra_perimeters);
perimeters += layerm.region().config().wall_loops.value;
// Then calculate the infill offset.
if (perimeters > 0) {
Flow extflow = layerm.flow(frExternalPerimeter);
Flow flow = layerm.flow(frPerimeter);
perimeter_offset = std::max(perimeter_offset,
0.5f * float(extflow.scaled_width() + extflow.scaled_spacing()) + (float(perimeters) - 1.f) * flow.scaled_spacing());
perimeter_min_spacing = std::min(perimeter_min_spacing, float(std::min(extflow.scaled_spacing(), flow.scaled_spacing())));
}
polygons_append(cache.holes, to_polygons(layerm.fill_expolygons));
}
// Save some computing time by reducing the number of polygons.
cache.top_surfaces = union_(cache.top_surfaces);
cache.bottom_surfaces = union_(cache.bottom_surfaces);
// For a multi-material print, simulate perimeter / infill split as if only a single extruder has been used for the whole print.
if (perimeter_offset > 0.) {
// The layer.lslices are forced to merge by expanding them first.
polygons_append(cache.holes, offset2(layer.lslices, 0.3f * perimeter_min_spacing, - perimeter_offset - 0.3f * perimeter_min_spacing));
#ifdef SLIC3R_DEBUG_SLICE_PROCESSING
{
Slic3r::SVG svg(debug_out_path("discover_vertical_shells-extra-holes-%d.svg", debug_idx), get_extents(layer.lslices));
svg.draw(layer.lslices, "blue");
svg.draw(union_ex(cache.holes), "red");
svg.draw_outline(union_ex(cache.holes), "black", "blue", scale_(0.05));
svg.Close();
}
{
Slic3r::SVG svg(debug_out_path("discover_vertical_shells-extra-holes-%d.svg", debug_idx), get_extents(layer.lslices));
svg.draw(layer.lslices, "blue");
svg.draw(union_ex(cache.holes), "red");
svg.draw_outline(union_ex(cache.holes), "black", "blue", scale_(0.05));
svg.Close();
}
#endif /* SLIC3R_DEBUG_SLICE_PROCESSING */
}
cache.holes = union_(cache.holes);
});
}
cache.holes = union_(cache.holes);
}
});
m_print->throw_if_canceled();
BOOST_LOG_TRIVIAL(debug) << "Discovering vertical shells in parallel - end : cache top / bottom";
}
// With one top/bottom cache for all regions, the shell and hole accumulation in the loop below depends on nothing
// region-specific but the shell settings and the external perimeter spacing, so a region sharing them with an earlier
// one reuses its result instead of repeating it: that accumulation is a union over several layers of top/bottom
// surfaces, and a multi-material print has a region per filament.
using AccumulationKey = std::array<double, 5>;
struct ShellAccumulation
{
AccumulationKey key;
Polygons shell;
Polygons holes;
};
const auto accumulation_key = [](const PrintRegionConfig &region_config, const LayerRegion *layerm) {
return AccumulationKey{ double(region_config.top_shell_layers.value), region_config.top_shell_thickness.value,
double(region_config.bottom_shell_layers.value), region_config.bottom_shell_thickness.value,
double(layerm->flow(frExternalPerimeter).scaled_spacing()) };
};
const auto accumulate_shell = [this, &cache_top_botom_regions](size_t idx_layer, const PrintRegionConfig &region_config,
const LayerRegion *layerm, Polygons &shell, Polygons &holes) {
const Layer *layer = m_layers[idx_layer];
polygons_append(holes, cache_top_botom_regions[idx_layer].holes);
auto combine_holes = [&holes](const Polygons &holes2) {
if (holes.empty() || holes2.empty())
holes.clear();
else
holes = intersection(holes, holes2);
};
auto combine_shells = [&shell](const Polygons &shells2) {
if (shell.empty())
shell = std::move(shells2);
else if (! shells2.empty()) {
polygons_append(shell, shells2);
// Running the union_ using the Clipper library piece by piece is cheaper
// than running the union_ all at once.
shell = union_(shell);
}
};
static constexpr const bool one_more_layer_below_top_bottom_surfaces = false;
if (int n_top_layers = region_config.top_shell_layers.value; n_top_layers > 0) {
// Gather top regions projected to this layer.
coordf_t print_z = layer->print_z;
int i = int(idx_layer) + 1;
int itop = int(idx_layer) + n_top_layers;
bool at_least_one_top_projected = false;
for (; i < int(cache_top_botom_regions.size()) &&
(i < itop || m_layers[i]->print_z - print_z < region_config.top_shell_thickness - EPSILON);
++ i) {
at_least_one_top_projected = true;
const DiscoverVerticalShellsCacheEntry &cache = cache_top_botom_regions[i];
combine_holes(cache.holes);
combine_shells(cache.top_surfaces);
}
if (!at_least_one_top_projected && i < int(cache_top_botom_regions.size())) {
// Lets consider this a special case - with only 1 top solid and minimal shell thickness settings, the
// boundaries of solid layers are not anchored over/under perimeters, so lets fix it by adding at least one
// perimeter width of area
Polygons anchor_area = intersection(expand(cache_top_botom_regions[idx_layer].top_surfaces,
layerm->flow(frExternalPerimeter).scaled_spacing()),
to_polygons(m_layers[i]->lslices));
combine_shells(anchor_area);
}
if (one_more_layer_below_top_bottom_surfaces)
if (i < int(cache_top_botom_regions.size()) &&
(i <= itop || m_layers[i]->bottom_z() - print_z < region_config.top_shell_thickness - EPSILON))
combine_holes(cache_top_botom_regions[i].holes);
}
if (int n_bottom_layers = region_config.bottom_shell_layers.value; n_bottom_layers > 0) {
// Gather bottom regions projected to this layer.
coordf_t bottom_z = layer->bottom_z();
int i = int(idx_layer) - 1;
int ibottom = int(idx_layer) - n_bottom_layers;
bool at_least_one_bottom_projected = false;
for (; i >= 0 &&
(i > ibottom || bottom_z - m_layers[i]->bottom_z() < region_config.bottom_shell_thickness - EPSILON);
-- i) {
at_least_one_bottom_projected = true;
const DiscoverVerticalShellsCacheEntry &cache = cache_top_botom_regions[i];
combine_holes(cache.holes);
combine_shells(cache.bottom_surfaces);
}
if (!at_least_one_bottom_projected && i >= 0) {
Polygons anchor_area = intersection(expand(cache_top_botom_regions[idx_layer].bottom_surfaces,
layerm->flow(frExternalPerimeter).scaled_spacing()),
to_polygons(m_layers[i]->lslices));
combine_shells(anchor_area);
}
if (one_more_layer_below_top_bottom_surfaces)
if (i >= 0 &&
(i > ibottom || bottom_z - m_layers[i]->print_z < region_config.bottom_shell_thickness - EPSILON))
combine_holes(cache_top_botom_regions[i].holes);
}
};
std::vector<std::vector<ShellAccumulation>> shell_accumulations(top_bottom_surfaces_all_regions ? num_layers : 0);
if (! shell_accumulations.empty()) {
// Every (layer, key) pair is accumulated once, before the regions, so that nothing in the loop below is shared
// between them and they can run next to each other.
std::vector<std::array<size_t, 3>> todo; // layer, its slot, a region holding the key
for (size_t idx_layer = 0; idx_layer < num_layers; ++ idx_layer) {
std::vector<ShellAccumulation> &accumulations = shell_accumulations[idx_layer];
for (size_t region_id = 0; region_id < this->num_printing_regions(); ++ region_id) {
if (this->printing_region(region_id).config().ensure_vertical_shell_thickness.value != evstAll)
continue;
const LayerRegion *layerm = m_layers[idx_layer]->m_regions[region_id];
const AccumulationKey key = accumulation_key(layerm->region().config(), layerm);
if (std::none_of(accumulations.begin(), accumulations.end(), [&key](const ShellAccumulation &a) { return a.key == key; })) {
todo.push_back({ idx_layer, accumulations.size(), region_id });
accumulations.push_back({ key, {}, {} });
}
}
}
tbb::parallel_for(size_t(0), todo.size(), [this, &todo, &shell_accumulations, &accumulate_shell](size_t i) {
m_print->throw_if_canceled();
const LayerRegion *layerm = m_layers[todo[i][0]]->m_regions[todo[i][2]];
ShellAccumulation &out = shell_accumulations[todo[i][0]][todo[i][1]];
accumulate_shell(todo[i][0], layerm->region().config(), layerm, out.shell, out.holes);
});
m_print->throw_if_canceled();
}
const auto process_region = [&](size_t region_id) {
for (size_t region_id = 0; region_id < this->num_printing_regions(); ++ region_id) {
const PrintRegion &region = this->printing_region(region_id);
if (region.config().ensure_vertical_shell_thickness.value != evstAll )
// This region will be handled by discover_horizontal_shells().
return;
continue;
//FIXME Improve the heuristics for a grain size.
size_t grain_size = std::max(num_layers / 16, size_t(1));
@@ -2528,7 +2379,7 @@ void PrintObject::discover_vertical_shells()
grain_size = 1;
tbb::parallel_for(
tbb::blocked_range<size_t>(0, num_layers, grain_size),
[this, region_id, &shell_accumulations, &accumulation_key, &accumulate_shell]
[this, region_id, &cache_top_botom_regions]
(const tbb::blocked_range<size_t>& range) {
// printf("discover_vertical_shells from %d to %d\n", range.begin(), range.end());
for (size_t idx_layer = range.begin(); idx_layer < range.end(); ++ idx_layer) {
@@ -2578,19 +2429,80 @@ void PrintObject::discover_vertical_shells()
}
}
#endif /* SLIC3R_DEBUG_SLICE_PROCESSING */
const AccumulationKey key = accumulation_key(region_config, layerm);
const ShellAccumulation *reused = shell_accumulations.empty() ? nullptr :
[&]() -> const ShellAccumulation * {
for (const ShellAccumulation &a : shell_accumulations[idx_layer])
if (a.key == key)
return &a;
return nullptr;
}();
if (reused != nullptr) {
shell = reused->shell;
holes = reused->holes;
} else
accumulate_shell(idx_layer, region_config, layerm, shell, holes);
polygons_append(holes, cache_top_botom_regions[idx_layer].holes);
auto combine_holes = [&holes](const Polygons &holes2) {
if (holes.empty() || holes2.empty())
holes.clear();
else
holes = intersection(holes, holes2);
};
auto combine_shells = [&shell](const Polygons &shells2) {
if (shell.empty())
shell = std::move(shells2);
else if (! shells2.empty()) {
polygons_append(shell, shells2);
// Running the union_ using the Clipper library piece by piece is cheaper
// than running the union_ all at once.
shell = union_(shell);
}
};
static constexpr const bool one_more_layer_below_top_bottom_surfaces = false;
if (int n_top_layers = region_config.top_shell_layers.value; n_top_layers > 0) {
// Gather top regions projected to this layer.
coordf_t print_z = layer->print_z;
int i = int(idx_layer) + 1;
int itop = int(idx_layer) + n_top_layers;
bool at_least_one_top_projected = false;
for (; i < int(cache_top_botom_regions.size()) &&
(i < itop || m_layers[i]->print_z - print_z < region_config.top_shell_thickness - EPSILON);
++ i) {
at_least_one_top_projected = true;
const DiscoverVerticalShellsCacheEntry &cache = cache_top_botom_regions[i];
combine_holes(cache.holes);
combine_shells(cache.top_surfaces);
}
if (!at_least_one_top_projected && i < int(cache_top_botom_regions.size())) {
// Lets consider this a special case - with only 1 top solid and minimal shell thickness settings, the
// boundaries of solid layers are not anchored over/under perimeters, so lets fix it by adding at least one
// perimeter width of area
Polygons anchor_area = intersection(expand(cache_top_botom_regions[idx_layer].top_surfaces,
layerm->flow(frExternalPerimeter).scaled_spacing()),
to_polygons(m_layers[i]->lslices));
combine_shells(anchor_area);
}
if (one_more_layer_below_top_bottom_surfaces)
if (i < int(cache_top_botom_regions.size()) &&
(i <= itop || m_layers[i]->bottom_z() - print_z < region_config.top_shell_thickness - EPSILON))
combine_holes(cache_top_botom_regions[i].holes);
}
if (int n_bottom_layers = region_config.bottom_shell_layers.value; n_bottom_layers > 0) {
// Gather bottom regions projected to this layer.
coordf_t bottom_z = layer->bottom_z();
int i = int(idx_layer) - 1;
int ibottom = int(idx_layer) - n_bottom_layers;
bool at_least_one_bottom_projected = false;
for (; i >= 0 &&
(i > ibottom || bottom_z - m_layers[i]->bottom_z() < region_config.bottom_shell_thickness - EPSILON);
-- i) {
at_least_one_bottom_projected = true;
const DiscoverVerticalShellsCacheEntry &cache = cache_top_botom_regions[i];
combine_holes(cache.holes);
combine_shells(cache.bottom_surfaces);
}
if (!at_least_one_bottom_projected && i >= 0) {
Polygons anchor_area = intersection(expand(cache_top_botom_regions[idx_layer].bottom_surfaces,
layerm->flow(frExternalPerimeter).scaled_spacing()),
to_polygons(m_layers[i]->lslices));
combine_shells(anchor_area);
}
if (one_more_layer_below_top_bottom_surfaces)
if (i >= 0 &&
(i > ibottom || bottom_z - m_layers[i]->print_z < region_config.bottom_shell_thickness - EPSILON))
combine_holes(cache_top_botom_regions[i].holes);
}
#ifdef SLIC3R_DEBUG_SLICE_PROCESSING
{
Slic3r::SVG svg(debug_out_path("discover_vertical_shells-perimeters-before-union-%d.svg", debug_idx), get_extents(shell));
@@ -2684,8 +2596,11 @@ void PrintObject::discover_vertical_shells()
Polygons object_volume;
Polygons internal_volume;
{
if (idx_layer > 0 && idx_layer + 1 < m_layers.size())
object_volume = to_polygons(intersection_ex_by_piece(m_layers[idx_layer - 1]->lslices, to_polygons(m_layers[idx_layer + 1]->lslices)));
Polygons shrinked_bottom_slice = idx_layer > 0 ? to_polygons(m_layers[idx_layer - 1]->lslices) : Polygons{};
Polygons shrinked_upper_slice = (idx_layer + 1) < m_layers.size() ?
to_polygons(m_layers[idx_layer + 1]->lslices) :
Polygons{};
object_volume = intersection(shrinked_bottom_slice, shrinked_upper_slice);
internal_volume = closing(polygonsInternal, SCALED_EPSILON);
}
@@ -2696,34 +2611,15 @@ void PrintObject::discover_vertical_shells()
// the in-model condition is there due to small sloping surfaces, e.g. top of the hull of the benchy
// 2. the area does not fully cover an internal polygon
// This is there mainly for a very thin parts, where the solid layers would be missing if the part area is quite small
// Both tests below compare a small piece against the whole layer. Done literally, that is
// quadratic in the number of pieces, which is what a layer split up by colour painting has,
// so each is restricted to the part of the layer near the piece with an identical result:
// object_volume is clipped to the piece's box, and only the internal polygons whose box meets
// the expanded piece take part in the count, since the others pass through the difference
// unchanged and add the same number to both sides of it.
std::vector<BoundingBox> internal_bboxes;
internal_bboxes.reserve(internal_volume.size());
for (const Polygon &poly : internal_volume)
internal_bboxes.emplace_back(get_extents(poly));
regularized_shell.erase(std::remove_if(regularized_shell.begin(), regularized_shell.end(),
[&internal_volume, &internal_bboxes, &min_perimeter_infill_spacing,
[&internal_volume, &min_perimeter_infill_spacing,
&object_volume](const ExPolygon &p) {
const bool small = p.area() < min_perimeter_infill_spacing * scaled(1.5) ||
(p.area() < min_perimeter_infill_spacing * scaled(8.0) &&
diff(to_polygons(p),
ClipperUtils::clip_clipper_polygons_with_subject_bbox(
object_volume, get_extents(p).inflated(SCALED_EPSILON)))
.empty());
if (!small)
return false;
const Polygons expanded = expand(to_polygons(p), min_perimeter_infill_spacing);
const BoundingBox bbox = get_extents(expanded);
Polygons nearby;
for (size_t i = 0; i < internal_volume.size(); ++i)
if (internal_bboxes[i].overlap(bbox))
nearby.emplace_back(internal_volume[i]);
return diff(nearby, expanded).size() >= nearby.size();
return (p.area() < min_perimeter_infill_spacing * scaled(1.5) ||
(p.area() < min_perimeter_infill_spacing * scaled(8.0) &&
diff(to_polygons(p), object_volume).empty())) &&
diff(internal_volume,
expand(to_polygons(p), min_perimeter_infill_spacing))
.size() >= internal_volume.size();
}),
regularized_shell.end());
}
@@ -2745,9 +2641,8 @@ void PrintObject::discover_vertical_shells()
#endif /* SLIC3R_DEBUG_SLICE_PROCESSING */
// Trim the internal & internalvoid by the shell.
const Polygons regularized_shell_polygons = to_polygons(regularized_shell);
Slic3r::ExPolygons new_internal = diff_ex_by_piece(to_expolygons(layerm->fill_surfaces.filter_by_type(stInternal)), regularized_shell_polygons);
Slic3r::ExPolygons new_internal_void = diff_ex_by_piece(to_expolygons(layerm->fill_surfaces.filter_by_type(stInternalVoid)), regularized_shell_polygons);
Slic3r::ExPolygons new_internal = diff_ex(layerm->fill_surfaces.filter_by_type(stInternal), regularized_shell);
Slic3r::ExPolygons new_internal_void = diff_ex(layerm->fill_surfaces.filter_by_type(stInternalVoid), regularized_shell);
#ifdef SLIC3R_DEBUG_SLICE_PROCESSING
{
@@ -2774,15 +2669,7 @@ void PrintObject::discover_vertical_shells()
layerm->export_region_fill_surfaces_to_svg_debug("3_discover_vertical_shells-final");
}
#endif /* SLIC3R_DEBUG_SLICE_PROCESSING */
}; // for each region
if (top_bottom_surfaces_all_regions)
// Nothing is shared between the regions, and a layer cut through a fine relief takes far longer than the others,
// so they run next to each other instead of one after another.
tbb::parallel_for(size_t(0), this->num_printing_regions(), process_region);
else
// Here every region fills the one top/bottom cache with its own surfaces first.
for (size_t region_id = 0; region_id < this->num_printing_regions(); ++ region_id)
process_region(region_id);
} // for each region
} // void PrintObject::discover_vertical_shells()
// #define DEBUG_BRIDGE_OVER_INFILL
@@ -3303,16 +3190,6 @@ void PrintObject::bridge_over_infill()
vertical_lines[i].b = Point{x, y_max};
}
// The vertical lines only span the bridged area's x range, so anchors entirely outside it can never be
// hit. Leaving them out gives the same intersections without building a tree over the whole layer's
// boundary for every bridge.
const coord_t scan_x_min = bb_x.min.x();
const coord_t scan_x_max = bb_x.min.x() + coord_t(n_vlines) * scan_spacing;
anchors.erase(std::remove_if(anchors.begin(), anchors.end(),
[scan_x_min, scan_x_max](const Line &l) {
return std::max(l.a.x(), l.b.x()) < scan_x_min || std::min(l.a.x(), l.b.x()) > scan_x_max;
}),
anchors.end());
auto anchors_and_walls_tree = AABBTreeLines::LinesDistancer<Line>{std::move(anchors)};
auto bridged_area_tree = AABBTreeLines::LinesDistancer<Line>{to_lines(bridged_area)};
@@ -3557,62 +3434,28 @@ void PrintObject::bridge_over_infill()
std::vector<CandidateSurface> expanded_surfaces;
expanded_surfaces.reserve(surfaces_by_layer[lidx].size());
// The expanded fill boundary depends only on the bridging flow, and total_fill_area is not
// modified below, so build it once per spacing rather than once per candidate. A layer split
// into many candidates (e.g. by colour painting) otherwise repeats a layer-wide offset for each.
std::map<coord_t, Polylines> boundary_by_spacing;
// expansion_area is a clean, non-overlapping set, so uniting it with a bridge or cutting a bridge
// out of it only changes the polygons near that bridge. The rest are passed through untouched
// instead of being fed to Clipper with the whole layer again for every candidate.
// Not `near`/`far`: the Windows headers still define those as macros, and they expand to
// nothing, which turns the declaration below into an empty one.
const auto split_near = [](const Polygons &polys, const BoundingBox &bbox, Polygons &rest) {
Polygons nearby;
for (const Polygon &p : polys)
(get_extents(p).overlap(bbox) ? nearby : rest).emplace_back(p);
return nearby;
};
for (const CandidateSurface &candidate : surfaces_by_layer[lidx]) {
const auto &region_config = candidate.region->region().config();
const bool turning_pattern = region_config.sparse_infill_pattern == ipHilbertCurve ||
region_config.sparse_infill_pattern == ipOctagramSpiral;
const Flow &flow = candidate.region->bridging_flow(frSolidInfill, true);
Polygons area_to_be_bridge = expand(candidate.new_polys, flow.scaled_spacing());
// deep_infill_area and internal_unsupported_area cover the whole layer; only their part under
// this candidate can change the results, so they are clipped to its box first.
if (!area_to_be_bridge.empty())
area_to_be_bridge = intersection(area_to_be_bridge,
ClipperUtils::clip_clipper_polygons_with_subject_bbox(
deep_infill_area, get_extents(area_to_be_bridge).inflated(SCALED_EPSILON)));
area_to_be_bridge = intersection(area_to_be_bridge, deep_infill_area);
area_to_be_bridge.erase(std::remove_if(area_to_be_bridge.begin(), area_to_be_bridge.end(),
[&internal_unsupported_area](const Polygon &p) {
return intersection({p}, ClipperUtils::clip_clipper_polygons_with_subject_bbox(
internal_unsupported_area,
get_extents(p).inflated(SCALED_EPSILON)))
.empty();
[internal_unsupported_area](const Polygon &p) {
return intersection({p}, internal_unsupported_area).empty();
}),
area_to_be_bridge.end());
Polygons limiting_area = union_(area_to_be_bridge, expansion_area);
if (area_to_be_bridge.empty())
continue;
Polygons limiting_area;
const Polygons near_expansion = split_near(expansion_area, get_extents(area_to_be_bridge).inflated(SCALED_EPSILON),
limiting_area);
const size_t num_far = limiting_area.size();
append(limiting_area, union_(area_to_be_bridge, near_expansion));
auto boundary_it = boundary_by_spacing.find(flow.scaled_spacing());
if (boundary_it == boundary_by_spacing.end())
boundary_it = boundary_by_spacing
.emplace(flow.scaled_spacing(), to_polylines(expand(total_fill_area, 1.3 * flow.scaled_spacing())))
.first;
Polylines boundary_plines = boundary_it->second;
Polylines boundary_plines = to_polylines(expand(total_fill_area, 1.3 * flow.scaled_spacing()));
{
// The sub-unit offset (spacing is in mm) still re-unites touching polygons by the bridge, which the anchors depend on.
Polylines limiting_plines = to_polylines(Polygons(limiting_area.begin(), limiting_area.begin() + num_far));
append(limiting_plines, to_polylines(expand(Polygons(limiting_area.begin() + num_far, limiting_area.end()), 0.3 * flow.spacing())));
Polylines limiting_plines = to_polylines(expand(limiting_area, 0.3*flow.spacing()));
boundary_plines.insert(boundary_plines.end(), limiting_plines.begin(), limiting_plines.end());
}
@@ -3686,12 +3529,9 @@ void PrintObject::bridge_over_infill()
// Check collision with other expanded surfaces
{
bool reconstruct = false;
Polygons tmp_expanded_area = expand(bridging_area, 3.0 * flow.scaled_spacing());
const BoundingBox tmp_expanded_bbox = get_extents(tmp_expanded_area);
Polygons tmp_expanded_area = expand(bridging_area, 3.0 * flow.scaled_spacing());
for (const CandidateSurface &s : expanded_surfaces) {
// Surfaces whose boxes miss each other cannot intersect, which is most pairs on a busy layer.
if (get_extents(s.new_polys).overlap(tmp_expanded_bbox) &&
!intersection(s.new_polys, tmp_expanded_area).empty()) {
if (!intersection(s.new_polys, tmp_expanded_area).empty()) {
bridging_angle = s.bridge_angle;
reconstruct = true;
break;
@@ -3715,20 +3555,10 @@ void PrintObject::bridge_over_infill()
bridging_area = union_(bridging_area, construct_anchored_polygon(bridging_area, to_lines(boundary_plines), flow,
bridging_angle, scan_spacing, true));
}
// Each of these meets one bridge with the whole layer, so the layer side is first cut down to the
// bridge's box (and expansion_area split as above); the result is the same.
if (!bridging_area.empty()) {
const BoundingBox bridging_bbox = get_extents(bridging_area).inflated(SCALED_EPSILON);
bridging_area = intersection(bridging_area, ClipperUtils::clip_clipper_polygons_with_subject_bbox(limiting_area, bridging_bbox));
bridging_area = intersection(bridging_area, ClipperUtils::clip_clipper_polygons_with_subject_bbox(total_fill_area, bridging_bbox));
bridging_area = diff(bridging_area, ClipperUtils::clip_clipper_polygons_with_subject_bbox(total_top_area, bridging_bbox));
}
if (!bridging_area.empty()) {
Polygons kept;
const Polygons cut = split_near(expansion_area, get_extents(bridging_area).inflated(SCALED_EPSILON), kept);
append(kept, diff(cut, bridging_area));
expansion_area = std::move(kept);
}
bridging_area = intersection(bridging_area, limiting_area);
bridging_area = intersection(bridging_area, total_fill_area);
bridging_area = diff(bridging_area, total_top_area);
expansion_area = diff(expansion_area, bridging_area);
#ifdef DEBUG_BRIDGE_OVER_INFILL
debug_draw(std::to_string(lidx) + "_" + std::to_string(cluster_idx) + "_" + std::to_string(job_idx) + "_" + "_expanded_bridging" + std::to_string(r),
+1 -1
View File
@@ -955,9 +955,9 @@ public:
::fread(&y, sizeof(coord_t), 1, file);
poly.points.emplace_back(Point(x * scale, y * scale));
}
printf("Polygon %d, area: %lf\n", i, area(poly.points));
if (which == -1 || which == i)
m_support_polygons_deserialized.emplace_back(std::move(poly));
printf("Polygon %d, area: %lf\n", i, area(poly.points));
}
::fread(&n_polygons, 4, 1, file);
m_trimming_polygons_deserialized.reserve(n_polygons);
+1 -31
View File
@@ -891,41 +891,11 @@ void TreeSupport::detect_overhangs(bool check_support_necessity/* = false*/)
if (is_auto(stype) && config_detect_sharp_tails)
{
// BBS detect sharp tail
// Each island is tested only against the lower islands whose box meets its own; overlaps() tries
// every pair, which is quadratic in the island counts of the two layers.
std::vector<BoundingBox> lower_bboxes;
lower_bboxes.reserve(lower_polys.size());
for (const ExPolygon &lower : lower_polys)
lower_bboxes.emplace_back(get_extents(lower));
for (const ExPolygon& expoly : curr_polys) {
bool is_sharp_tail = false;
// 1. nothing below
// this is a sharp tail region if it's floating and non-ignorable
const ExPolygons expanded = offset_ex(expoly, 0.1 * extrusion_width_scaled);
const BoundingBox bbox = get_extents(expanded);
ExPolygons lower_nearby;
for (size_t i = 0; i < lower_polys.size(); ++i)
if (lower_bboxes[i].overlap(bbox))
lower_nearby.emplace_back(lower_polys[i]);
// As overlaps(expanded, lower_nearby), with each lower island cut to the island's box first:
// below a fine relief the lower layer is a few islands with thousands of holes, and the whole
// of that boundary would otherwise be intersected once per island above.
const auto overlaps_nearby = [&]() {
for (const ExPolygon &a : expanded) {
if (a.empty())
continue;
const BoundingBox a_bbox = get_extents(a);
for (const ExPolygon &b : lower_nearby) {
if (b.empty() || !get_extents(b).overlap(a_bbox))
continue;
const Polygons b_near = ClipperUtils::clip_clipper_polygons_with_subject_bbox(b, a_bbox.inflated(SCALED_EPSILON));
if (!intersection_pl(to_polylines(b_near), a).empty() || b.contains(a.contour.points.front()))
return true;
}
}
return false;
};
if (!overlaps_nearby()) {
if (!overlaps(offset_ex(expoly, 0.1 * extrusion_width_scaled), lower_polys)) {
is_sharp_tail = !offset_ex(expoly, -0.1 * extrusion_width_scaled).empty();
}
+2 -2
View File
@@ -69,7 +69,7 @@ public:
thickness(other.thickness), thickness_layers(other.thickness_layers),
bridge_angle(other.bridge_angle), extra_perimeters(other.extra_perimeters)
{};
Surface(Surface &&rhs) noexcept
Surface(Surface &&rhs)
: surface_type(rhs.surface_type), expolygon(std::move(rhs.expolygon)),
thickness(rhs.thickness), thickness_layers(rhs.thickness_layers),
bridge_angle(rhs.bridge_angle), extra_perimeters(rhs.extra_perimeters)
@@ -95,7 +95,7 @@ public:
return *this;
}
Surface& operator=(Surface &&rhs) noexcept
Surface& operator=(Surface &&rhs)
{
surface_type = rhs.surface_type;
expolygon = std::move(rhs.expolygon);
+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"); }
+4 -4
View File
@@ -168,10 +168,10 @@ inline void append(std::vector<T, Alloc> &dest, std::vector<T, Alloc> &&src)
{
if (dest.empty())
dest = std::move(src);
else
// insert() grows the capacity geometrically; reserving exactly the new size reallocated on every call, which
// made appending piece by piece quadratic.
dest.insert(dest.end(), std::make_move_iterator(src.begin()), std::make_move_iterator(src.end()));
else {
dest.reserve(dest.size() + src.size());
std::move(std::begin(src), std::end(src), std::back_inserter(dest));
}
src.clear();
src.shrink_to_fit();
}
+1
View File
@@ -7,6 +7,7 @@
#include "Types.hpp"
#include <array>
#include <cstddef>
#include <cstdint>
#include <string>
+1
View File
@@ -4,6 +4,7 @@
///|/
#include "../include/Viewer.hpp"
#include "ViewerImpl.hpp"
#include <array>
#include <string>
#include <utility>
#include "../include/Types.hpp"
+5
View File
@@ -3,10 +3,15 @@
#include "3DScene.hpp"
#include <chrono>
#include <array>
#include <glad/gl.h>
#include <algorithm>
#include <cstring>
#include <ratio>
#include <vector>
#include <utility>
namespace Slic3r {
namespace GUI {
+2
View File
@@ -28,6 +28,8 @@
#include "libslic3r/Layer.hpp"
#include "Widgets/ProgressDialog.hpp"
#include "MsgDialog.hpp"
#include <boost/container_hash/hash.hpp>
#include "slic3r/GUI/MeshUtils.hpp"
#include <string>
#include "libvgcode/include/Types.hpp"
#include <vector>
+1
View File
@@ -28,6 +28,7 @@
#include <cstdint>
#include <float.h>
#include <set>
#include "slic3r/GUI/MeshUtils.hpp"
#include <string>
#include <unordered_set>
#include <utility>
@@ -29,6 +29,7 @@
#include "slic3r/GUI/Gizmos/GLGizmoSVG.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoMeshBoolean.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoAssembly.hpp"
#include <initializer_list>
#include <wx/timer.h>
#include <vector>
#include <cstddef>
+1
View File
@@ -8,6 +8,7 @@
#include "libslic3r/Utils.hpp"
#include "slic3r/GUI/GUI_Utils.hpp"
#include "libslic3r/Preset.hpp"
#include <wx/wx.h>
#include <wx/gdicmn.h>
#include <wx/toplevel.h>
+1
View File
@@ -9,6 +9,7 @@
#include "libslic3r_version.h"
#include <boost/filesystem/path.hpp>
#include <utility>
#include <wx/panel.h>
#include <wx/gdicmn.h>
#include <memory>
+1
View File
@@ -13,6 +13,7 @@
#include <boost/algorithm/string/case_conv.hpp>
#include <boost/log/trivial.hpp>
#include <utility>
#include <vector>
#include <string>
#include "slic3r/GUI/GUI.hpp"
+18
View File
@@ -21,12 +21,30 @@
#include "libslic3r/AppConfig.hpp"
#include "libslic3r/libslic3r.h"
#include "libslic3r_version.h"
#include <array>
#include <glad/gl.h>
#include <vector>
#include <string>
#include "slic3r/GUI/FrameProfiler.hpp"
#include "slic3r/GUI/GUI_Utils.hpp"
#include "libslic3r/Point.hpp"
#include "libslic3r/BoundingBox.hpp"
#include <ratio>
#include "libslic3r/Preset.hpp"
#include <ios>
#include <wx/clipbrd.h>
#include <wx/gdicmn.h>
#include <wx/colour.h>
#include <wx/event.h>
#include <wx/dcclient.h>
#include <wx/dataobj.h>
#include <wx/glcanvas.h>
#include <wx/string.h>
#include <wx/textctrl.h>
#include <wx/timer.h>
#include <wx/toplevel.h>
#include <wx/utils.h>
#include <algorithm>
+1
View File
@@ -14,6 +14,7 @@
#include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/json_parser.hpp>
#include <utility>
#include <wx/event.h>
#include <wx/panel.h>
#include <wx/gdicmn.h>
+1
View File
@@ -22,6 +22,7 @@
#include "slic3r/GUI/WebPanel.hpp"
#include <optional>
#include "slic3r/plugin/PythonPluginInterface.hpp"
#include "slic3r/GUI/LazyPage.hpp"
#include <stdexcept>
#include <string>
#include <wx/app.h>
+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"
@@ -22,6 +25,10 @@
#include "libslic3r/PrintConfig.hpp"
#include "libslic3r/Print.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/Config.hpp"
#include "libslic3r/Layer.hpp"
#include "libslic3r/ExtrusionEntityCollection.hpp"
#include "libslic3r/ExtrusionEntity.hpp"
#include <string>
#include <string_view>
#include <vector>
@@ -538,6 +545,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");
+98
View File
@@ -2,10 +2,21 @@
#include <catch2/catch_test_macros.hpp>
#include <catch2/generators/catch_generators.hpp>
#include <catch2/matchers/catch_matchers.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include "libslic3r/libslic3r.h"
#include "libslic3r/GCode/GCodeProcessor.hpp"
#include "libslic3r/Utils.hpp"
#include "test_utils.hpp"
#include <cstddef>
#include <fstream>
#include "libslic3r/PrintConfig.hpp"
#include "libslic3r/Point.hpp"
#include <sstream>
#include <string>
#include <utility>
#include <vector>
using namespace Slic3r;
@@ -79,3 +90,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 -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
@@ -30,7 +31,6 @@ add_executable(${_TEST_NAME}_tests
test_filament_mixer.cpp
test_fill_plane_path.cpp
test_geometry.cpp
test_kdtree.cpp
test_multimaterial_segmentation.cpp
test_placeholder_parser.cpp
test_polygon.cpp
+272
View File
@@ -0,0 +1,272 @@
#include <catch2/catch_all.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers.hpp>
#include <catch2/generators/catch_generators.hpp>
#include <catch2/catch_message.hpp>
#include "libslic3r/Format/AssembleList.hpp"
#include "test_utils.hpp"
#include <boost/nowide/fstream.hpp>
#include <cstddef>
#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);
}
+1 -88
View File
@@ -7,8 +7,8 @@
#include "libslic3r/libslic3r.h"
#include <numeric>
#include <iostream>
#include <utility>
#include <boost/filesystem.hpp>
#include <utility>
#include <vector>
#include <catch2/catch_test_macros.hpp>
@@ -300,90 +300,3 @@ TEST_CASE("Top level expolygons of an even-odd union", "[ClipperUtils]") {
REQUIRE(area_sum == Catch::Approx(area(top_level) + area(nested)));
REQUIRE(top_level_expolygons(reference).size() == 1);
}
// Rings flattened to x,y,x,y... and sorted, with each ring rotated to start at its lowest point: two
// encodings of the same geometry compare equal however the pieces came back or wherever a ring started.
static std::vector<std::vector<coord_t>> canonical_rings(const ExPolygons &expolygons)
{
std::vector<std::vector<coord_t>> rings;
const auto add = [&rings](const Polygon &poly) {
if (poly.points.empty())
return;
Points pts = poly.points;
std::rotate(pts.begin(),
std::min_element(pts.begin(), pts.end(), [](const Point &a, const Point &b) {
return std::make_pair(a.x(), a.y()) < std::make_pair(b.x(), b.y());
}),
pts.end());
std::vector<coord_t> flat;
flat.reserve(pts.size() * 2);
for (const Point &p : pts) {
flat.emplace_back(p.x());
flat.emplace_back(p.y());
}
rings.emplace_back(std::move(flat));
};
for (const ExPolygon &expoly : expolygons) {
add(expoly.contour);
for (const Polygon &hole : expoly.holes)
add(hole);
}
std::sort(rings.begin(), rings.end());
return rings;
}
// The same rings, every coordinate within `tolerance`.
static bool same_rings(const ExPolygons &a, const ExPolygons &b, coord_t tolerance)
{
const std::vector<std::vector<coord_t>> ra = canonical_rings(a), rb = canonical_rings(b);
return std::equal(ra.begin(), ra.end(), rb.begin(), rb.end(), [tolerance](const std::vector<coord_t> &x, const std::vector<coord_t> &y) {
return std::equal(x.begin(), x.end(), y.begin(), y.end(), [tolerance](coord_t u, coord_t v) { return std::abs(u - v) <= tolerance; });
});
}
TEST_CASE("Tiled diff and intersection return the same polygons as the plain calls", "[ClipperUtils]") {
// A grid of disjoint framed squares, enough of them to be split into several tiles.
const int n = 40;
const coord_t cell = scaled<coord_t>(2.), side = scaled<coord_t>(1.5), frame = scaled<coord_t>(0.3);
ExPolygons subject;
for (int y = 0; y < n; ++ y)
for (int x = 0; x < n; ++ x) {
const Point o(x * cell, y * cell);
ExPolygon square(Polygon({ o, o + Point(side, 0), o + Point(side, side), o + Point(0, side) }));
Polygon hole({ o + Point(frame, frame), o + Point(frame, side - frame), o + Point(side - frame, side - frame), o + Point(side - frame, frame) });
square.holes.emplace_back(std::move(hole));
subject.emplace_back(std::move(square));
}
// Clip polygons crossing many squares, one of them large with holes of its own.
Polygons clip;
const coord_t span = n * cell;
for (int i = 0; i < 8; ++ i) {
const coord_t y0 = coord_t(i) * span / 8, y1 = y0 + scaled<coord_t>(0.9);
clip.emplace_back(Polygon({ Point(- cell, y0), Point(span, y0 + cell * 3), Point(span, y1 + cell * 3), Point(- cell, y1) }));
}
ExPolygon big(Polygon({ Point(span / 4, span / 4), Point(3 * span / 4, span / 4), Point(3 * span / 4, 3 * span / 4), Point(span / 4, 3 * span / 4) }));
for (int i = 0; i < 4; ++ i) {
const Point o(span / 4 + scaled<coord_t>(3.1) + i * scaled<coord_t>(9.7), span / 4 + scaled<coord_t>(5.3));
big.holes.emplace_back(Polygon({ o, o + Point(0, scaled<coord_t>(20.)), o + Point(scaled<coord_t>(5.), scaled<coord_t>(20.)), o + Point(scaled<coord_t>(5.), 0) }));
}
polygons_append(clip, to_polygons(big));
const ApplySafetyOffset safety = GENERATE(ApplySafetyOffset::No, ApplySafetyOffset::Yes);
// The point of the fixture: below 128 pieces the helpers fall back to a single tile and the tiled
// path under test is never taken.
REQUIRE(ClipperUtils::tile_expolygons(subject, 32).size() > 1);
// With the safety offset a tile unites fewer clip polygons, so Clipper2 can round a crossing 1 unit differently.
const coord_t tolerance = safety == ApplySafetyOffset::Yes ? 1 : 0;
const ExPolygons diff_plain = diff_ex(subject, clip, safety);
const ExPolygons diff_tiled = diff_ex_by_piece(subject, clip, safety);
REQUIRE(area(diff_plain) > 0.);
CHECK(same_rings(diff_tiled, diff_plain, tolerance));
const ExPolygons intersection_plain = intersection_ex(subject, clip, safety);
const ExPolygons intersection_tiled = intersection_ex_by_piece(subject, clip, safety);
REQUIRE(area(intersection_plain) > 0.);
CHECK(same_rings(intersection_tiled, intersection_plain, tolerance));
}
+1
View File
@@ -19,6 +19,7 @@
#include <boost/filesystem.hpp>
#include <boost/nowide/fstream.hpp>
#include "libslic3r/Config.hpp"
#include <initializer_list>
#include <memory>
#include <map>
#include <iterator>
-67
View File
@@ -1,67 +0,0 @@
#include <catch2/catch_all.hpp>
#include <numeric>
#include <random>
#include <vector>
#include "libslic3r/KDTreeIndirect.hpp"
#include "libslic3r/Point.hpp"
using namespace Slic3r;
TEST_CASE("Visiting the nearby points gives what collecting them gives", "[KDTree]") {
std::mt19937 rng(19937);
std::uniform_real_distribution<float> coord(-50.f, 50.f);
// Points in a box, so that a radius search returns anything from none of them to all of them.
std::vector<Vec3f> points(2000);
for (Vec3f &p : points)
p = Vec3f(coord(rng), coord(rng), coord(rng));
auto coordinate = [&points](size_t idx, size_t dimension) { return points[idx](int(dimension)); };
KDTreeIndirect<3, float, decltype(coordinate)> tree(coordinate);
std::vector<size_t> indices(points.size());
std::iota(indices.begin(), indices.end(), 0);
tree.build(indices);
const float radius = GENERATE(0.5f, 5.f, 25.f, 200.f);
for (int i = 0; i < 20; ++ i) {
const Vec3f center(coord(rng), coord(rng), coord(rng));
const std::vector<size_t> collected = find_nearby_points(tree, center, radius);
std::vector<size_t> visited;
visit_nearby_points(tree, center, radius, [&visited](size_t idx) { visited.emplace_back(idx); });
// Same points, and in the same order: a caller that keeps the first of several equally good ones
// must get the same answer either way.
REQUIRE(visited == collected);
}
}
TEST_CASE("A radius search returns every point within the radius and no other", "[KDTree]") {
std::mt19937 rng(2024);
std::uniform_real_distribution<float> coord(-20.f, 20.f);
std::vector<Vec3f> points(500);
for (Vec3f &p : points)
p = Vec3f(coord(rng), coord(rng), coord(rng));
auto coordinate = [&points](size_t idx, size_t dimension) { return points[idx](int(dimension)); };
KDTreeIndirect<3, float, decltype(coordinate)> tree(coordinate);
std::vector<size_t> indices(points.size());
std::iota(indices.begin(), indices.end(), 0);
tree.build(indices);
const Vec3f center(1.f, -2.f, 3.f);
const float radius = 7.f;
std::vector<size_t> expected;
for (size_t i = 0; i < points.size(); ++ i)
if ((points[i] - center).squaredNorm() < radius * radius)
expected.emplace_back(i);
std::vector<size_t> visited;
visit_nearby_points(tree, center, radius, [&visited](size_t idx) { visited.emplace_back(idx); });
std::sort(visited.begin(), visited.end());
REQUIRE(! expected.empty());
REQUIRE(visited == expected);
}
+23
View File
@@ -4,6 +4,10 @@
#include <locale.h>
#include <catch2/catch_test_macros.hpp>
#include <catch2/generators/catch_generators.hpp>
#include <catch2/catch_message.hpp>
#include <catch2/matchers/catch_matchers.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include "libslic3r/LocalesUtils.hpp"
using namespace Slic3r;
@@ -69,3 +73,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));
}
@@ -5816,6 +5816,131 @@ TEST_CASE("A system preset no vendor lists is not resolved", "[Preset][Bundle]")
namespace {
// Writes each vendor's preset cache into dir, then deletes its profile JSONs: what a release build installs.
void reduce_vendors_to_caches(const fs::path &dir, const std::vector<std::string> &vendor_ids)
{
const std::string lib(PresetBundle::ORCA_FILAMENT_LIBRARY);
PresetBundle library;
if (fs::exists(dir / (lib + ".json"))) {
library.set_generate_vendor_caches(true);
library.load_vendor_configs_from_json(dir.string(), lib, PresetBundle::LoadSystem,
ForwardCompatibilitySubstitutionRule::EnableSilent);
}
for (const std::string &vendor_id : vendor_ids) {
if (vendor_id == lib)
continue;
PresetBundle writer;
writer.set_generate_vendor_caches(true);
writer.load_vendor_configs_from_json(dir.string(), vendor_id, PresetBundle::LoadSystem,
ForwardCompatibilitySubstitutionRule::EnableSilent, &library);
}
for (const std::string &vendor_id : vendor_ids) {
REQUIRE(fs::exists(dir / (vendor_id + ".opc")));
fs::remove(dir / (vendor_id + ".json"));
fs::remove_all(dir / vendor_id);
}
}
// The filament library with one abstract base filament, and an "Acme" vendor whose one filament inherits it.
void write_library_and_acme_filament(const fs::path &root)
{
const std::string lib(PresetBundle::ORCA_FILAMENT_LIBRARY);
fs::create_directories(root / lib / "filament");
std::ofstream((root / (lib + ".json")).string())
<< R"({"version":"1.0.0","name":")" << lib << R"(",)"
<< R"("filament_list":[{"name":"Generic PLA","sub_path":"filament/generic_pla.json"}]})";
std::ofstream((root / lib / "filament" / "generic_pla.json").string())
<< R"({"type":"filament","name":"Generic PLA","from":"system","instantiation":"false","filament_id":"GFL99","filament_cost":"27"})";
fs::create_directories(root / "Acme" / "filament");
std::ofstream((root / "Acme.json").string())
<< R"({"version":"1.0.0","name":"Acme","filament_list":[{"name":"Acme PLA","sub_path":"filament/pla.json"}]})";
std::ofstream((root / "Acme" / "filament" / "pla.json").string())
<< R"({"type":"filament","name":"Acme PLA","from":"system","instantiation":"true","inherits":"Generic PLA"})";
}
} // namespace
TEST_CASE("A read-only load resolves a user preset against vendors installed as their cache alone", "[Preset][Bundle][Regression]")
{
ScopedTemporaryDir temp_dir;
const fs::path data = temp_dir.path() / "data";
const fs::path system = data / PRESET_SYSTEM_DIR;
ScopedDataDir scoped_data(data);
ScopedResourcesDir scoped_resources(temp_dir.path() / "resources");
write_acme_printer_vendor(system, 33.);
reduce_vendors_to_caches(system, {"Acme"});
fs::create_directories(data / PRESET_USER_DIR / DEFAULT_USER_FOLDER_NAME / PRESET_PRINTER_NAME);
std::ofstream((data / PRESET_USER_DIR / DEFAULT_USER_FOLDER_NAME / PRESET_PRINTER_NAME / "My Acme.json").string())
<< R"({"type":"machine","name":"My Acme","from":"User","version":"2.3.0.0","inherits":"Acme Printer","printable_height":"123"})";
AppConfig app_config;
PresetBundle bundle;
std::string errors;
bundle.load_presets(app_config, ForwardCompatibilitySubstitutionRule::EnableSilent, PresetBundle::PresetPreferences(),
&errors, true);
CHECK(errors.empty());
const Preset *preset = bundle.printers.find_preset("My Acme");
REQUIRE(preset != nullptr);
CHECK_THAT(preset->config.opt_float("printable_height"), Catch::Matchers::WithinAbs(123., 1e-6));
CHECK_THAT(preset->config.opt_float("extruder_clearance_dist_to_rod"), Catch::Matchers::WithinAbs(33., 1e-6));
}
TEST_CASE("A read-only load writes no preset cache", "[Preset][Bundle][Regression]")
{
ScopedTemporaryDir temp_dir;
const fs::path system = temp_dir.path() / "data" / PRESET_SYSTEM_DIR;
ScopedDataDir scoped_data(temp_dir.path() / "data");
ScopedResourcesDir scoped_resources(temp_dir.path() / "resources");
write_acme_printer_vendor(system, 33.);
AppConfig app_config;
PresetBundle bundle;
std::string errors;
bundle.load_presets(app_config, ForwardCompatibilitySubstitutionRule::EnableSilent, PresetBundle::PresetPreferences(),
&errors, true);
CHECK(errors.empty());
CHECK(bundle.printers.find_preset("Acme Printer") != nullptr);
CHECK_FALSE(fs::exists(system / "Acme.opc"));
}
TEST_CASE("A vendor updated over the air resolves against the library installed as its cache alone", "[Preset][Bundle][Regression]")
{
ScopedTemporaryDir temp_dir;
const fs::path system = temp_dir.path() / "data" / PRESET_SYSTEM_DIR;
ScopedDataDir scoped_data(temp_dir.path() / "data");
ScopedResourcesDir scoped_resources(temp_dir.path() / "resources");
// System presets are found by name through the bundled profiles.
write_library_and_acme_filament(temp_dir.path() / "resources" / PRESET_PROFILES_DIR);
// The release install, then an update that brings Acme back as JSONs while the library stays a cache.
write_library_and_acme_filament(system);
reduce_vendors_to_caches(system, {PresetBundle::ORCA_FILAMENT_LIBRARY, "Acme"});
write_library_and_acme_filament(temp_dir.path() / "update");
fs::copy_file(temp_dir.path() / "update" / "Acme.json", system / "Acme.json");
fs::create_directories(system / "Acme" / "filament");
fs::copy_file(temp_dir.path() / "update" / "Acme" / "filament" / "pla.json", system / "Acme" / "filament" / "pla.json");
SECTION("by name") {
PresetBundle bundle;
DynamicPrintConfig config;
std::string error;
REQUIRE(bundle.resolve_system_preset(config, Preset::TYPE_FILAMENT, "Acme PLA",
ForwardCompatibilitySubstitutionRule::EnableSilent, error));
CHECK_THAT(config.opt<ConfigOptionFloats>("filament_cost")->values.front(), Catch::Matchers::WithinAbs(27., 1e-6));
}
SECTION("by its source file") {
PresetBundle bundle;
DynamicPrintConfig config;
config.option<ConfigOptionString>(BBL_JSON_KEY_INHERITS, true)->value = "Generic PLA";
std::string error;
REQUIRE(bundle.resolve_preset_config(config, Preset::TYPE_FILAMENT, (system / "Acme" / "filament" / "pla.json").string(),
ForwardCompatibilitySubstitutionRule::EnableSilent, error));
CHECK_THAT(config.opt<ConfigOptionFloats>("filament_cost")->values.front(), Catch::Matchers::WithinAbs(27., 1e-6));
}
}
namespace {
// A default preset config for type, built the way PresetBundle builds its default presets.
DynamicPrintConfig external_default_config(Preset::Type type)
{
@@ -18,6 +18,7 @@
#include "libslic3r/Config.hpp"
#include "libslic3r/GCode/ToolOrderUtils.hpp"
#include "libslic3r/PrintBase.hpp"
#include <cstddef>
#include <map>
#include <memory>
#include <set>
@@ -1,7 +1,13 @@
#include <algorithm>
#include <catch2/catch_all.hpp>
#include "libslic3r/Point.hpp"
#include "libslic3r/Polygon.hpp"
#include <tbb/global_control.h>
#include <vector>
#include <utility>
#include <catch2/catch_test_macros.hpp>
#include "libslic3r/TriangleMesh.hpp"
#include "libslic3r/TriangleMeshSlicer.hpp"
+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("@", "`"));
}