mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-17 04:41:21 +00:00
Merge branch 'main' into feat/ams-parsing
This commit is contained in:
+7
-2
@@ -91,6 +91,12 @@ if (SLIC3R_GUI)
|
||||
# list(REMOVE_ITEM wxWidgets_LIBRARIES oleacc)
|
||||
|
||||
find_package(wxInspector REQUIRED)
|
||||
# wxInspector 1.0.0 installs its headers but accidentally declares the
|
||||
# INSTALL_INTERFACE include directory PRIVATE, so its imported target does
|
||||
# not expose them to consumers. Restore the package prefix include path until
|
||||
# the upstream export is fixed.
|
||||
get_filename_component(WXINSPECTOR_PREFIX "${wxInspector_DIR}/../../.." ABSOLUTE)
|
||||
target_include_directories(wxInspector::wxInspector INTERFACE "${WXINSPECTOR_PREFIX}/include")
|
||||
|
||||
# wxInspector's exported interface names the release wxWidgets import
|
||||
# libraries, which a Debug build cannot link. wx is linked above instead.
|
||||
@@ -183,10 +189,9 @@ else ()
|
||||
target_link_libraries(OrcaSlicer ${CMAKE_DL_LIBS} -lstdc++ Threads::Threads pangoft2-1.0)
|
||||
endif ()
|
||||
|
||||
# Add the Slic3r GUI library, libcurl, OpenGL and GLU libraries.
|
||||
if (SLIC3R_GUI)
|
||||
# target_link_libraries(OrcaSlicer ws2_32 uxtheme setupapi libslic3r_gui ${wxWidgets_LIBRARIES})
|
||||
target_link_libraries(OrcaSlicer libslic3r_gui)
|
||||
target_link_libraries(OrcaSlicer libslic3r_gui wxInspector::wxInspector)
|
||||
if (MSVC)
|
||||
# Generate debug symbols even in release mode.
|
||||
target_link_options(OrcaSlicer PUBLIC "$<$<CONFIG:RELEASE>:/DEBUG>")
|
||||
|
||||
+82
-21
@@ -99,6 +99,7 @@
|
||||
#include "libslic3r/Preset.hpp"
|
||||
#include "libslic3r/Geometry.hpp"
|
||||
#include "libslic3r/GCode.hpp"
|
||||
#include "libslic3r/MeshRepair.hpp"
|
||||
#include "libslic3r/Model.hpp"
|
||||
#include "libslic3r/ModelArrange.hpp"
|
||||
#include "libslic3r/Platform.hpp"
|
||||
@@ -206,7 +207,7 @@ std::map<int, std::string> cli_errors = {
|
||||
{CLI_OBJECT_ORIENT_FAILED, "An error occurred when auto-orienting object(s)."},
|
||||
{CLI_MODIFIED_PARAMS_TO_PRINTER, "Found modified parameter in printer preset in the 3mf file, which should not be changed."},
|
||||
{CLI_FILE_VERSION_NOT_SUPPORTED, "Unsupported 3MF version. Please make sure the 3MF file was created with the official version of Bambu Studio, not a beta version."},
|
||||
{CLI_NO_SUITABLE_OBJECTS, "One of the plate is empty or has no object fully inside it. Please check that the 3mf contains no empty plate in Orca Slicer before uploading."},
|
||||
{CLI_NO_SUITABLE_OBJECTS, "The plate has nothing to slice: it is empty, or no object on it is both printable and fully inside the print volume."},
|
||||
{CLI_VALIDATE_ERROR, "There are some incorrect slicing parameters in the 3mf. Please verify the slicing of all plates in Orca Slicer before uploading."},
|
||||
{CLI_OBJECTS_PARTLY_INSIDE, "Some objects are located over the boundary of the heated bed."},
|
||||
{CLI_EXPORT_CACHE_DIRECTORY_CREATE_FAILED, "Failed creating directory when exporting cache data."},
|
||||
@@ -1463,7 +1464,7 @@ int CLI::run(int argc, char **argv)
|
||||
//int arrange_option;
|
||||
int plate_to_slice = 0, filament_count = 0, duplicate_count = 0, real_duplicate_count = 0, current_extruder_count = 1, new_extruder_count = 1, current_printer_variant_count = 1, current_print_variant_count = 1, new_printer_variant_count = 1;
|
||||
bool first_file = true, is_bbl_3mf = false, need_arrange = true, has_thumbnails = false, up_config_to_date = false, normative_check = true, duplicate_single_object = false, use_first_fila_as_default = false, minimum_save = false, enable_timelapse = false;
|
||||
bool allow_rotations = true, skip_modified_gcodes = false, avoid_extrusion_cali_region = false, skip_useless_pick = false, allow_newer_file = false, current_is_multi_extruder = false, new_is_multi_extruder = false, allow_mix_temp = false, enable_wrapping_detect = false;
|
||||
bool allow_rotations = false, skip_modified_gcodes = false, avoid_extrusion_cali_region = false, skip_useless_pick = false, allow_newer_file = false, current_is_multi_extruder = false, new_is_multi_extruder = false, allow_mix_temp = false, enable_wrapping_detect = false;
|
||||
Semver file_version;
|
||||
std::map<size_t, bool> orients_requirement;
|
||||
std::vector<Preset*> project_presets;
|
||||
@@ -1820,6 +1821,8 @@ int CLI::run(int argc, char **argv)
|
||||
old_printable_width = static_cast<int>(old_printable_bbox.size().x());
|
||||
old_printable_depth = static_cast<int>(old_printable_bbox.size().y());
|
||||
}
|
||||
// A 3mf can carry an empty project_settings.config - the models in
|
||||
// resources/handy_models do - and opt_float() dereferences without checking.
|
||||
if (config.option<ConfigOptionFloat>("printable_height"))
|
||||
old_printable_height = (int)(config.opt_float("printable_height"));
|
||||
|
||||
@@ -2505,7 +2508,8 @@ int CLI::run(int argc, char **argv)
|
||||
orig_printable_width = static_cast<int>(orig_printable_bbox.size().x());
|
||||
orig_printable_depth = static_cast<int>(orig_printable_bbox.size().y());
|
||||
}
|
||||
orig_printable_height = (int)(config.opt_float("printable_height"));
|
||||
if (config.option<ConfigOptionFloat>("printable_height"))
|
||||
orig_printable_height = (int)(config.opt_float("printable_height"));
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__<< boost::format(":%1%, check printable size: old_printable_width=%2%, orig_printable_width=%3%, old_printable_depth=%4%, orig_printable_depth=%5%, old_printable_height=%6%, orig_printable_height=%7%")
|
||||
%__LINE__ %old_printable_width %orig_printable_width %old_printable_depth %orig_printable_depth %old_printable_height %orig_printable_height;
|
||||
if ((orig_printable_width > 0) && (orig_printable_depth > 0) && (orig_printable_height > 0))
|
||||
@@ -3432,9 +3436,14 @@ int CLI::run(int argc, char **argv)
|
||||
max_self_index = std::max(max_self_index, v);
|
||||
min_self_index = std::min(min_self_index, v);
|
||||
}
|
||||
if (max_self_index > filament_count || min_self_index < 1) {
|
||||
BOOST_LOG_TRIVIAL(warning) << boost::format("filament_self_index range [%1%, %2%] is invalid for filament_count %3%, regenerating")
|
||||
% min_self_index % max_self_index % filament_count;
|
||||
// And a project saved with FEWER filaments than are now loaded (a
|
||||
// one-filament project sliced with two --load-filaments) leaves the tables half filled:
|
||||
// the variant matching below then reads past filament_extruder_variant and
|
||||
// set_with_restore_2 throws an uncaught size error. Regenerate in that case too.
|
||||
if (max_self_index > filament_count || min_self_index < 1 || max_self_index < filament_count
|
||||
|| (int) filament_self_index_opt->values.size() < filament_count) {
|
||||
BOOST_LOG_TRIVIAL(warning) << boost::format("filament_self_index range [%1%, %2%] (size %4%) is invalid for filament_count %3%, regenerating")
|
||||
% min_self_index % max_self_index % filament_count % filament_self_index_opt->values.size();
|
||||
need_regenerate_self_index = true;
|
||||
}
|
||||
}
|
||||
@@ -3525,6 +3534,10 @@ int CLI::run(int argc, char **argv)
|
||||
std::vector<std::string>& filament_variants = curr_variant_opt->values;
|
||||
filament_variants.resize(filament_count, get_extruder_variant_string(etDirectDrive, nvtStandard));
|
||||
}
|
||||
// See the filament_self_index note above: one variant per filament for
|
||||
// the filaments the project did not know about.
|
||||
if ((int) curr_variant_opt->values.size() < filament_count)
|
||||
curr_variant_opt->values.resize(filament_count, get_extruder_variant_string(etDirectDrive, nvtStandard));
|
||||
const ConfigOptionStrings *new_variant_opt = dynamic_cast<const ConfigOptionStrings*>(config.option("filament_extruder_variant", true));
|
||||
|
||||
std::vector<int> new_variant_indice;
|
||||
@@ -3533,7 +3546,7 @@ int CLI::run(int argc, char **argv)
|
||||
|
||||
for (int i = 0; i < new_variant_count; i++)
|
||||
{
|
||||
for (int j = old_start_indice[filament_index - 1]; j < old_start_indice[filament_index - 1] + old_variant_count; j++)
|
||||
for (int j = old_start_indice[filament_index - 1]; j < old_start_indice[filament_index - 1] + old_variant_count && j < (int) curr_variant_opt->values.size(); j++)
|
||||
{
|
||||
if (curr_variant_opt->values[j] == new_variant_opt->values[i]) {
|
||||
new_variant_indice[i] = j;
|
||||
@@ -3585,7 +3598,18 @@ int CLI::run(int argc, char **argv)
|
||||
ConfigOptionVectorBase* opt_vec_dst = static_cast<ConfigOptionVectorBase*>(opt);
|
||||
const ConfigOptionVectorBase* opt_vec_src = static_cast<const ConfigOptionVectorBase*>(source_opt);
|
||||
//set with index
|
||||
opt_vec_dst->set_with_restore_2(opt_vec_src, new_variant_indice, old_start_indice[filament_index - 1], old_variant_count);
|
||||
try {
|
||||
// A project with fewer filaments than are loaded: grow the
|
||||
// destination to the filament's slot first (set_with_restore_2 only restores).
|
||||
if (opt_vec_src->size() > 0 && opt_vec_dst->size() < size_t(old_start_indice[filament_index - 1] + old_variant_count))
|
||||
opt_vec_dst->resize(size_t(old_start_indice[filament_index - 1] + old_variant_count), opt_vec_src);
|
||||
opt_vec_dst->set_with_restore_2(opt_vec_src, new_variant_indice, old_start_indice[filament_index - 1], old_variant_count);
|
||||
} catch (const std::exception &ex) { // Was an uncaught abort
|
||||
BOOST_LOG_TRIVIAL(error) << boost::format("filament %1%: option %2% could not be applied: %3%") % filament_index % opt_key % ex.what();
|
||||
boost::nowide::cerr << "filament " << filament_index << ": option " << opt_key << " could not be applied: " << ex.what() << std::endl;
|
||||
record_exit_reson(outfile_dir, CLI_CONFIG_FILE_ERROR, 0, cli_errors[CLI_CONFIG_FILE_ERROR], sliced_info);
|
||||
flush_and_exit(CLI_CONFIG_FILE_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
continue;
|
||||
@@ -3634,7 +3658,16 @@ int CLI::run(int argc, char **argv)
|
||||
if (filament_options_with_variant.find(opt_key) != filament_options_with_variant.end()) {
|
||||
std::vector<int> temp_variant_indice;
|
||||
temp_variant_indice.resize(new_variant_count, -1);
|
||||
opt_vec_dst->set_with_restore_2(opt_vec_src, temp_variant_indice, old_start_indice[filament_index - 1], old_variant_count, true);
|
||||
try {
|
||||
if (opt_vec_src->size() > 0 && opt_vec_dst->size() < size_t(old_start_indice[filament_index - 1] + old_variant_count)) // See above
|
||||
opt_vec_dst->resize(size_t(old_start_indice[filament_index - 1] + old_variant_count), opt_vec_src);
|
||||
opt_vec_dst->set_with_restore_2(opt_vec_src, temp_variant_indice, old_start_indice[filament_index - 1], old_variant_count, true);
|
||||
} catch (const std::exception &ex) { // Was an uncaught abort
|
||||
BOOST_LOG_TRIVIAL(error) << boost::format("filament %1%: option %2% could not be applied: %3%") % filament_index % opt_key % ex.what();
|
||||
boost::nowide::cerr << "filament " << filament_index << ": option " << opt_key << " could not be applied: " << ex.what() << std::endl;
|
||||
record_exit_reson(outfile_dir, CLI_CONFIG_FILE_ERROR, 0, cli_errors[CLI_CONFIG_FILE_ERROR], sliced_info);
|
||||
flush_and_exit(CLI_CONFIG_FILE_ERROR);
|
||||
}
|
||||
|
||||
if (opt_key == "filament_extruder_variant")
|
||||
new_variant_counts[filament_index - 1] = opt_vec_src->size();
|
||||
@@ -4094,6 +4127,8 @@ int CLI::run(int argc, char **argv)
|
||||
flush_and_exit(CLI_MIXED_FILAMENT_INVALID);
|
||||
}
|
||||
}
|
||||
if (filament_count > 0)
|
||||
resize_mixed_filament_metadata(m_print_config, size_t(filament_count), size_t(filament_count));
|
||||
|
||||
m_print_config.option<ConfigOptionEnum<PrinterTechnology>>("printer_technology", true)->value = printer_technology;
|
||||
|
||||
@@ -4148,6 +4183,10 @@ int CLI::run(int argc, char **argv)
|
||||
BOOST_LOG_TRIVIAL(info) << boost::format("%1%, set disable_wipe_tower_after_mapping back to false due to wrapping detect")%__LINE__;
|
||||
}
|
||||
|
||||
// Belt printers never get the classic wipe tower (see Print::has_wipe_tower()), so reserve no space for it.
|
||||
const ConfigOptionBool* belt_printer_opt = m_print_config.option<ConfigOptionBool>("belt_printer");
|
||||
const bool is_belt_printer = belt_printer_opt && belt_printer_opt->value;
|
||||
|
||||
auto timelapse_type_opt = m_print_config.option("timelapse_type");
|
||||
bool is_smooth_timelapse = false;
|
||||
if (enable_timelapse && timelapse_type_opt && (timelapse_type_opt->getInt() == TimelapseType::tlSmooth))
|
||||
@@ -4385,11 +4424,11 @@ int CLI::run(int argc, char **argv)
|
||||
}
|
||||
};
|
||||
|
||||
auto check_plate_wipe_tower = [get_print_sequence, is_smooth_timelapse](Slic3r::GUI::PartPlate* plate, int plate_index, DynamicPrintConfig& print_config, plate_obj_size_info_t &plate_obj_size_info) {
|
||||
auto check_plate_wipe_tower = [get_print_sequence, is_smooth_timelapse, is_belt_printer](Slic3r::GUI::PartPlate* plate, int plate_index, DynamicPrintConfig& print_config, plate_obj_size_info_t &plate_obj_size_info) {
|
||||
plate_obj_size_info.obj_bbox= plate->get_objects_bounding_box();
|
||||
BOOST_LOG_TRIVIAL(info) << boost::format("plate %1%, object bbox: min {%2%, %3%, %4%} - max {%5%, %6%, %7%}")
|
||||
%(plate_index+1) %plate_obj_size_info.obj_bbox.min.x() % plate_obj_size_info.obj_bbox.min.y() % plate_obj_size_info.obj_bbox.min.z() %plate_obj_size_info.obj_bbox.max.x() % plate_obj_size_info.obj_bbox.max.y() % plate_obj_size_info.obj_bbox.max.z();
|
||||
if (!print_config.has("wipe_tower_x")) {
|
||||
if (is_belt_printer || !print_config.has("wipe_tower_x")) {
|
||||
plate_obj_size_info.has_wipe_tower = false;
|
||||
BOOST_LOG_TRIVIAL(info) << boost::format("can not found wipe_tower_x in config, set to no wipe tower");
|
||||
return;
|
||||
@@ -4615,7 +4654,8 @@ int CLI::run(int argc, char **argv)
|
||||
BoundingBoxf temp_printable_bbox(temp_printable_area);
|
||||
printer_plate.printable_width = static_cast<int>(temp_printable_bbox.size().x());
|
||||
printer_plate.printable_depth = static_cast<int>(temp_printable_bbox.size().y());
|
||||
printer_plate.printable_height = (int)(config.opt_float("printable_height"));
|
||||
if (config.option<ConfigOptionFloat>("printable_height"))
|
||||
printer_plate.printable_height = (int)(config.opt_float("printable_height"));
|
||||
}
|
||||
if (temp_exclude_area.size() >= 4) {
|
||||
printer_plate.exclude_width = (int)(temp_exclude_area[2].x() - temp_exclude_area[0].x());
|
||||
@@ -5091,9 +5131,19 @@ int CLI::run(int argc, char **argv)
|
||||
}
|
||||
}
|
||||
} else if (opt_key == "repair") {
|
||||
// Models are repaired by default.
|
||||
//for (auto &model : m_models)
|
||||
// model.repair();
|
||||
if (m_config.opt_bool(opt_key))
|
||||
for (Model &model : m_models)
|
||||
for (ModelObject *object : model.objects) {
|
||||
for (ModelVolume *volume : object->volumes) {
|
||||
MeshRepairStats stats;
|
||||
std::string error;
|
||||
if (!volume->repair(true, &stats, &error))
|
||||
BOOST_LOG_TRIVIAL(warning) << "repair of " << object->name << " / " << volume->name << " failed: " << error;
|
||||
else if (stats.modified)
|
||||
BOOST_LOG_TRIVIAL(info) << "repaired " << object->name << " / " << volume->name;
|
||||
}
|
||||
object->invalidate_bounding_box();
|
||||
}
|
||||
} else {
|
||||
boost::nowide::cerr << "error: option not implemented yet: " << opt_key << std::endl;
|
||||
record_exit_reson(outfile_dir, CLI_UNSUPPORTED_OPERATION, 0, cli_errors[CLI_UNSUPPORTED_OPERATION], sliced_info);
|
||||
@@ -5103,6 +5153,7 @@ int CLI::run(int argc, char **argv)
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << "finished model pre-process commands\n";
|
||||
bool oriented_or_arranged = false;
|
||||
bool arranged_without_rotations = false;
|
||||
//BBS: add orient and arrange logic here
|
||||
for (auto& model : m_models)
|
||||
{
|
||||
@@ -5259,7 +5310,7 @@ int CLI::run(int argc, char **argv)
|
||||
}
|
||||
}
|
||||
|
||||
if ((!arrange_cfg.is_seq_print && (assemble_plate.filaments_count > 1))||(enable_wrapping_detect && !current_wrapping_exclude_area.empty()))
|
||||
if (!is_belt_printer && ((!arrange_cfg.is_seq_print && (assemble_plate.filaments_count > 1)) || (enable_wrapping_detect && !current_wrapping_exclude_area.empty())))
|
||||
{
|
||||
//prepare the wipe tower
|
||||
int plate_count = partplate_list.get_plate_count();
|
||||
@@ -5370,6 +5421,7 @@ int CLI::run(int argc, char **argv)
|
||||
BOOST_LOG_TRIVIAL(info) << boost::format("start plate %1%'s arranging...") % (i + 1);
|
||||
arrangement::arrange(selected, unselected, beds, arrange_cfg);
|
||||
//arrangement::arrange(unprintable, {}, beds, arrange_cfg);
|
||||
arranged_without_rotations |= !arrange_cfg.allow_rotations;
|
||||
BOOST_LOG_TRIVIAL(info) << boost::format("finished plate %1%'s arranging") % (i + 1);
|
||||
|
||||
//step-4: postprocess the bed index and result
|
||||
@@ -5411,7 +5463,7 @@ int CLI::run(int argc, char **argv)
|
||||
bool is_seq_print = false;
|
||||
get_print_sequence(cur_plate, m_print_config, is_seq_print);
|
||||
|
||||
if (!is_seq_print && (assemble_plate.filaments_count > 1) && !has_wipe_tower_position)
|
||||
if (!is_belt_printer && !is_seq_print && (assemble_plate.filaments_count > 1) && !has_wipe_tower_position)
|
||||
{
|
||||
//prepare the wipe tower
|
||||
auto printer_structure_opt = m_print_config.option<ConfigOptionEnum<PrinterStructure>>("printer_structure");
|
||||
@@ -5579,7 +5631,7 @@ int CLI::run(int argc, char **argv)
|
||||
};
|
||||
const int max_filament_count = plate_count > 0 ? *std::max_element(plate_filament_counts.begin(), plate_filament_counts.end()) : 0;
|
||||
|
||||
if (plate_needs_wipe_tower(max_filament_count))
|
||||
if (!is_belt_printer && plate_needs_wipe_tower(max_filament_count))
|
||||
{
|
||||
//prepare the wipe tower
|
||||
auto printer_structure_opt = m_print_config.option<ConfigOptionEnum<PrinterStructure>>("printer_structure");
|
||||
@@ -5686,7 +5738,7 @@ int CLI::run(int argc, char **argv)
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": found single object mode");
|
||||
}
|
||||
|
||||
if (m_print_config.has("wipe_tower_x") && (is_smooth_timelapse || !arrange_cfg.is_seq_print || (selected.size() <= 1))) {
|
||||
if (!is_belt_printer && m_print_config.has("wipe_tower_x") && (is_smooth_timelapse || !arrange_cfg.is_seq_print || (selected.size() <= 1))) {
|
||||
float x;
|
||||
float y;
|
||||
if (duplicate_count > 0) {
|
||||
@@ -5842,6 +5894,7 @@ int CLI::run(int argc, char **argv)
|
||||
BOOST_LOG_TRIVIAL(info) << boost::format("start %1% th arranging...")%arrange_count;
|
||||
arrangement::arrange(selected, unselected, beds, arrange_cfg);
|
||||
arrangement::arrange(unprintable, {}, beds, arrange_cfg);
|
||||
arranged_without_rotations |= !arrange_cfg.allow_rotations;
|
||||
BOOST_LOG_TRIVIAL(info) << boost::format("finished %1% th arranging...")%arrange_count;
|
||||
|
||||
//Step-4:postprocess by partplate list&&apply the result
|
||||
@@ -6311,7 +6364,7 @@ int CLI::run(int argc, char **argv)
|
||||
// The stored (or default) tower position may not fit the tower these plates
|
||||
// need, and no CLI placement site runs on a plain slice - mirror the GUI's
|
||||
// reload clamp and fit every plate's tower into the printable area first.
|
||||
if (m_print_config.option<ConfigOptionBool>("enable_prime_tower", true)->value) {
|
||||
if (!is_belt_printer && m_print_config.option<ConfigOptionBool>("enable_prime_tower", true)->value) {
|
||||
for (int index = 0; index < partplate_list.get_plate_count(); index++) {
|
||||
if ((plate_to_slice != 0) && (plate_to_slice != (index + 1)))
|
||||
continue;
|
||||
@@ -6386,7 +6439,15 @@ int CLI::run(int argc, char **argv)
|
||||
|
||||
if (count == 0) {
|
||||
BOOST_LOG_TRIVIAL(error) << "plate "<< index+1<< ": Nothing to be sliced, Either the print is empty or no object is fully inside the print volume before apply." << std::endl;
|
||||
record_exit_reson(outfile_dir, CLI_NO_SUITABLE_OBJECTS, index+1, cli_errors[CLI_NO_SUITABLE_OBJECTS], sliced_info);
|
||||
std::string no_objects_reason = cli_errors[CLI_NO_SUITABLE_OBJECTS];
|
||||
if (arranged_without_rotations) {
|
||||
//ORCA: result.json is only written on Linux, so also print the reason to stderr.
|
||||
no_objects_reason = "No object is fully inside the print volume. Arrange ran without rotations."
|
||||
" If an object only fits the bed when rotated, pass --allow-rotations."
|
||||
" Otherwise the plate is empty or an object is larger than the printable area or height.";
|
||||
boost::nowide::cerr << "plate " << index+1 << ": " << no_objects_reason << std::endl;
|
||||
}
|
||||
record_exit_reson(outfile_dir, CLI_NO_SUITABLE_OBJECTS, index+1, no_objects_reason, sliced_info);
|
||||
flush_and_exit(CLI_NO_SUITABLE_OBJECTS);
|
||||
}
|
||||
else if ((plate_to_slice != 0) || pre_check) {
|
||||
|
||||
@@ -28,6 +28,12 @@ if (ORCA_TOOLS)
|
||||
target_link_libraries(generate_system_cache libslic3r boost_headeronly)
|
||||
target_compile_definitions(generate_system_cache PRIVATE ${_DEV_DEFS})
|
||||
|
||||
# texture_unwrap_dump: reports the LSCM unwrap of a saved project's texture displacement layers,
|
||||
# chart by chart, so a defect can be reproduced from the project file instead of from a screenshot.
|
||||
add_executable(texture_unwrap_dump texture_unwrap_dump.cpp)
|
||||
target_link_libraries(texture_unwrap_dump libslic3r boost_headeronly nanosvg)
|
||||
target_compile_definitions(texture_unwrap_dump PRIVATE ${_DEV_DEFS})
|
||||
|
||||
# profile_include_dump: prints what included templates contribute to a vendor's presets,
|
||||
# to diff against the same tool built in BambuStudio. Built only on request.
|
||||
add_executable(profile_include_dump EXCLUDE_FROM_ALL profile_include_dump.cpp)
|
||||
|
||||
@@ -202,6 +202,7 @@ Vec2d place_wipe_tower(DynamicPrintConfig &cfg, const Vec2d ¢er)
|
||||
std::string slice_two_color_cube_and_export(DynamicPrintConfig cfg, bool is_bbl, bool by_object)
|
||||
{
|
||||
const Vec2d center = printable_area_center(cfg);
|
||||
const bool belt = cfg.opt_bool("belt_printer");
|
||||
std::vector<Vec2d> cube_mins;
|
||||
if (by_object) {
|
||||
// By-object printing fires the hook only without a wipe tower, and rules out clumping detection and
|
||||
@@ -212,6 +213,12 @@ std::string slice_two_color_cube_and_export(DynamicPrintConfig cfg, bool is_bbl,
|
||||
cfg.set_key_value("timelapse_type", new ConfigOptionEnum<TimelapseType>(tlTraditional));
|
||||
cfg.set_key_value("skirt_loops", new ConfigOptionInt(0));
|
||||
cube_mins = {center + Vec2d(-20., -5.), center + Vec2d(10., -5.)};
|
||||
} else if (belt) {
|
||||
// A belt object's slicing Z starts at the belt below its leading end, well below its first
|
||||
// printed layer, so a height range in slicing Z does not map onto the part. Two cubes one
|
||||
// behind the other along the belt, the second on filament 2, give the one filament change
|
||||
// instead (the purge prism is an object the GUI adds, so there is no tower to place).
|
||||
cube_mins = {center - Vec2d(5., 15.), center + Vec2d(-5., 5.)};
|
||||
} else {
|
||||
// Clumping detection changes the tower footprint, so turn it on before placing the tower.
|
||||
if (!cfg.opt_string("wrapping_detection_gcode").empty())
|
||||
@@ -229,14 +236,20 @@ std::string slice_two_color_cube_and_export(DynamicPrintConfig cfg, bool is_bbl,
|
||||
obj->name = "cube"; // populates [input_filename_base] the way a loaded model does
|
||||
obj->add_volume(m);
|
||||
obj->add_instance();
|
||||
// Filament 2 is used only above z=4, so the upper layers carry a single filament change.
|
||||
DynamicPrintConfig range_config;
|
||||
range_config.set_key_value("extruder", new ConfigOptionInt(2));
|
||||
// Every range must carry a layer_height; use the process's own so a fine nozzle (e.g. 0.15 mm
|
||||
// printing ~0.1 mm layers) isn't forced to a height its extrusion width can't support - that
|
||||
// trips Flow::with_spacing.
|
||||
range_config.set_key_value("layer_height", new ConfigOptionFloat(cfg.opt_float("layer_height")));
|
||||
obj->layer_config_ranges[{4.0, 10.0}].assign_config(std::move(range_config));
|
||||
if (belt && !by_object) {
|
||||
// The second cube along the belt is on filament 2 (see cube_mins above).
|
||||
if (&cube_min == &cube_mins.back())
|
||||
obj->config.set_key_value("extruder", new ConfigOptionInt(2));
|
||||
} else {
|
||||
// Filament 2 is used only above z=4, so the upper layers carry a single filament change.
|
||||
DynamicPrintConfig range_config;
|
||||
range_config.set_key_value("extruder", new ConfigOptionInt(2));
|
||||
// Every range must carry a layer_height; use the process's own so a fine nozzle (e.g. 0.15 mm
|
||||
// printing ~0.1 mm layers) isn't forced to a height its extrusion width can't support - that
|
||||
// trips Flow::with_spacing.
|
||||
range_config.set_key_value("layer_height", new ConfigOptionFloat(cfg.opt_float("layer_height")));
|
||||
obj->layer_config_ranges[{4.0, 10.0}].assign_config(std::move(range_config));
|
||||
}
|
||||
obj->ensure_on_bed();
|
||||
print.auto_assign_extruders(obj);
|
||||
}
|
||||
@@ -521,11 +534,16 @@ int slice_all_printers(const std::string &vendor, const std::string &outdir)
|
||||
const std::string filament_name = bundle.filaments.get_selected_preset_name();
|
||||
const std::string what = "Printer \"" + printer + "\"";
|
||||
const std::string file_base = sanitize_filename(vendor_name) + "__" + sanitize_filename(printer);
|
||||
// A belt printer has no wipe tower (it purges into a prism object on the belt), so its
|
||||
// filament change is the plain tool change set_extruder() emits rather than the tower's block.
|
||||
const bool belt = bundle.printers.get_selected_preset().config.opt_bool("belt_printer");
|
||||
const std::string marker = belt ? "\nT1\n" : "CP TOOLCHANGE START";
|
||||
if (const std::string out = slice_selection(bundle, what, false, outdir, file_base); out.empty())
|
||||
++failures;
|
||||
else if (out.find("CP TOOLCHANGE START") == std::string::npos) {
|
||||
// The filament change never rode the tower, so change_filament_gcode was not exercised.
|
||||
BOOST_LOG_TRIVIAL(error) << what << " sliced but the filament change never fired (no CP TOOLCHANGE START)";
|
||||
else if (out.find(marker) == std::string::npos) {
|
||||
// The filament change never fired, so change_filament_gcode was not exercised.
|
||||
BOOST_LOG_TRIVIAL(error) << what << " sliced but the filament change never fired (no "
|
||||
<< (belt ? "T1" : "CP TOOLCHANGE START") << ")";
|
||||
++failures;
|
||||
}
|
||||
cover(bundle.prints.get_selected_preset());
|
||||
|
||||
@@ -303,10 +303,11 @@ else
|
||||
fi
|
||||
|
||||
has_host_runtime_library() {
|
||||
local lib_name path
|
||||
local lib_name="\$1"
|
||||
local path
|
||||
|
||||
if command -v ldconfig >/dev/null 2>&1; then
|
||||
if ldconfig -p 2>/dev/null | grep -Fq " \$lib_name"; then
|
||||
if ldconfig -p 2>/dev/null | grep -Fq "\$lib_name ("; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
@@ -343,7 +344,7 @@ TARGET_BIN="\$1"
|
||||
|
||||
if target_missing_runtime_library "\$TARGET_BIN" "libOpenGL.so.0" || ! has_host_runtime_library "libOpenGL.so.0"; then
|
||||
echo "Error: missing host OpenGL runtime library libOpenGL.so.0." >&2
|
||||
echo "On Ubuntu/Pop!_OS/Debian, install: libopengl0 and libglu1-mesa" >&2
|
||||
echo "On Ubuntu/Pop!_OS/Debian, install: libopengl0" >&2
|
||||
echo "On Arch/CachyOS, install: libglvnd" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,292 @@
|
||||
// Diagnostic for the LSCM unwrap of a texture displacement layer.
|
||||
//
|
||||
// It exists because the defect it hunts only shows up on a real painted patch: the paint mask is built
|
||||
// by TriangleSelector splitting base triangles, so the patch topology cannot be written down by hand,
|
||||
// and reasoning about it from a screenshot of the 3D view had already produced three wrong diagnoses.
|
||||
// This loads a saved project, rebuilds exactly the patch the bake would act on, runs the same unwrap,
|
||||
// and reports what came out - per chart, so a bad one can be pointed at rather than guessed at.
|
||||
//
|
||||
// texture_unwrap_dump <project.3mf>
|
||||
|
||||
// nanosvg is header-only and libslic3r's 3mf import references it without carrying the implementation,
|
||||
// so every executable that links libslic3r has to supply it. Must precede any include that pulls the
|
||||
// header in, or its include guard suppresses the implementation. Same pattern as the other dev tools.
|
||||
#define NANOSVG_IMPLEMENTATION
|
||||
#include "nanosvg/nanosvg.h"
|
||||
#define NANOSVGRAST_IMPLEMENTATION
|
||||
#include "nanosvg/nanosvgrast.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include <functional>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "libslic3r/Model.hpp"
|
||||
#include "libslic3r/TextureDisplacement.hpp"
|
||||
#include "libslic3r/Format/bbs_3mf.hpp"
|
||||
#include "libslic3r/Utils.hpp"
|
||||
|
||||
#include <boost/filesystem.hpp>
|
||||
|
||||
using namespace Slic3r;
|
||||
|
||||
namespace {
|
||||
|
||||
uint64_t edge_key(int a, int b)
|
||||
{
|
||||
if (a > b)
|
||||
std::swap(a, b);
|
||||
return (uint64_t(uint32_t(a)) << 32) | uint32_t(b);
|
||||
}
|
||||
|
||||
// Boundary loops and the Euler characteristic of a face set, which together say whether a chart is the
|
||||
// topological disk LSCM needs (one loop, V - E + F == 1).
|
||||
void chart_topology(const indexed_triangle_set &mesh, const std::vector<int> &faces, int &loops, int &euler)
|
||||
{
|
||||
std::unordered_map<uint64_t, int> edge_use;
|
||||
std::unordered_map<int, int> local;
|
||||
for (const int f : faces) {
|
||||
const stl_triangle_vertex_indices &t = mesh.indices[size_t(f)];
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
++edge_use[edge_key(t[i], t[(i + 1) % 3])];
|
||||
local.emplace(t[i], int(local.size()));
|
||||
}
|
||||
}
|
||||
euler = int(local.size()) - int(edge_use.size()) + int(faces.size());
|
||||
|
||||
std::unordered_map<int, int> parent;
|
||||
const std::function<int(int)> find = [&](int x) {
|
||||
while (parent[x] != x)
|
||||
x = parent[x] = parent[parent[x]];
|
||||
return x;
|
||||
};
|
||||
for (const auto &[key, uses] : edge_use)
|
||||
if (uses == 1)
|
||||
for (const int v : { int(key >> 32), int(uint32_t(key)) })
|
||||
parent.emplace(v, v);
|
||||
for (const auto &[key, uses] : edge_use)
|
||||
if (uses == 1) {
|
||||
const int a = find(int(key >> 32)), b = find(int(uint32_t(key)));
|
||||
if (a != b)
|
||||
parent[b] = a;
|
||||
}
|
||||
std::unordered_map<int, int> roots;
|
||||
for (const auto &[v, p] : parent)
|
||||
roots[find(v)] = 1;
|
||||
loops = int(roots.size());
|
||||
}
|
||||
|
||||
float signed_area_2d(const Vec2f &a, const Vec2f &b, const Vec2f &c)
|
||||
{
|
||||
return 0.5f * ((b.x() - a.x()) * (c.y() - a.y()) - (c.x() - a.x()) * (b.y() - a.y()));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
if (argc < 2) {
|
||||
std::printf("usage: texture_unwrap_dump <project.3mf>\n");
|
||||
return 2;
|
||||
}
|
||||
|
||||
Model model;
|
||||
DynamicPrintConfig config;
|
||||
ConfigSubstitutionContext ctx(ForwardCompatibilitySubstitutionRule::Enable);
|
||||
PlateDataPtrs plate_data;
|
||||
std::vector<Preset *> project_presets;
|
||||
bool is_bbl_3mf = false, is_orca_3mf = false;
|
||||
Semver file_version;
|
||||
// The importer writes a backup copy under the data dir and silently loses objects without one.
|
||||
const boost::filesystem::path tmp = boost::filesystem::temp_directory_path() / "texture_unwrap_dump";
|
||||
boost::filesystem::create_directories(tmp);
|
||||
set_data_dir(tmp.string());
|
||||
|
||||
// LoadModel so the meshes come through; AddDefaultInstances because an object with no instance is
|
||||
// dropped by the plate mapping, which is what "skip this object" in the log means.
|
||||
if (!load_bbs_3mf(argv[1], &config, &ctx, &model, &plate_data, &project_presets, &is_bbl_3mf, &is_orca_3mf,
|
||||
&file_version, nullptr,
|
||||
LoadStrategy::LoadModel | LoadStrategy::LoadConfig | LoadStrategy::AddDefaultInstances |
|
||||
LoadStrategy::Silence)) {
|
||||
std::printf("failed to load %s\n", argv[1]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::printf("loaded: %zu object(s)\n", model.objects.size());
|
||||
|
||||
for (const ModelObject *object : model.objects)
|
||||
for (const ModelVolume *volume : object->volumes) {
|
||||
if (volume->texture_displacement_layers.empty()) {
|
||||
std::printf("volume \"%s\": no texture displacement layers; paint masks per slot:",
|
||||
volume->name.c_str());
|
||||
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
|
||||
std::printf(" %zu", volume->texture_displacement_facet(i).get_data().triangles_to_split.size());
|
||||
std::printf("\n");
|
||||
continue;
|
||||
}
|
||||
std::printf("volume \"%s\": %zu base triangles, %zu layer(s)\n", volume->name.c_str(),
|
||||
volume->mesh().its.indices.size(), volume->texture_displacement_layers.size());
|
||||
|
||||
for (const TextureDisplacementLayer &layer : volume->texture_displacement_layers) {
|
||||
std::printf("\n layer %d \"%s\" mapping=%d seam_angle=%.1f connect=%d islands_stored=%zu\n",
|
||||
layer.slot, layer.name.c_str(), int(layer.projection_method),
|
||||
layer.lscm_seam_angle_deg, int(layer.auto_connect_islands), layer.islands.size());
|
||||
if (layer.projection_method != TextureProjectionMethod::LSCM)
|
||||
continue;
|
||||
|
||||
const indexed_triangle_set patch =
|
||||
extract_painted_patch(volume->mesh().its, volume->texture_displacement_facet(layer.slot).get_data());
|
||||
std::printf(" patch: %zu vertices, %zu triangles\n", patch.vertices.size(), patch.indices.size());
|
||||
if (patch.indices.empty())
|
||||
continue;
|
||||
|
||||
const auto t0 = std::chrono::steady_clock::now();
|
||||
const PatchUnwrap unwrap = compute_patch_unwrap(patch, layer.lscm_seam_angle_deg, 0.f,
|
||||
layer.lscm_seam_edges);
|
||||
const auto t1 = std::chrono::steady_clock::now();
|
||||
std::printf(" TIMING compute_patch_unwrap: %.0f ms\n",
|
||||
std::chrono::duration<double, std::milli>(t1 - t0).count());
|
||||
std::printf(" unwrap: %d charts, %zu unwrapped triangles\n", unwrap.chart_count,
|
||||
unwrap.indices.size());
|
||||
|
||||
// Group the patch's faces by chart so each can be examined on its own.
|
||||
std::vector<std::vector<int>> chart_faces(size_t(std::max(unwrap.chart_count, 0)));
|
||||
for (size_t i = 0; i < unwrap.indices.size(); ++i) {
|
||||
const int chart = unwrap.vertex_chart[size_t(unwrap.indices[i][0])];
|
||||
if (chart >= 0 && size_t(chart) < chart_faces.size())
|
||||
chart_faces[size_t(chart)].push_back(unwrap.source_face[i]);
|
||||
}
|
||||
|
||||
int bad_charts = 0;
|
||||
for (size_t c = 0; c < chart_faces.size(); ++c) {
|
||||
int loops = 0, euler = 0;
|
||||
chart_topology(patch, chart_faces[c], loops, euler);
|
||||
|
||||
// Flipped triangles: the unwrap folded over itself, which is what a planar fallback
|
||||
// does to a chart that is not flat. Measured on the unwrap's own triangles.
|
||||
int pos = 0, neg = 0;
|
||||
for (size_t i = 0; i < unwrap.indices.size(); ++i) {
|
||||
const stl_triangle_vertex_indices &t = unwrap.indices[i];
|
||||
if (unwrap.vertex_chart[size_t(t[0])] != int(c))
|
||||
continue;
|
||||
const float a = signed_area_2d(unwrap.uvs[size_t(t[0])], unwrap.uvs[size_t(t[1])],
|
||||
unwrap.uvs[size_t(t[2])]);
|
||||
if (a > 0.f) ++pos; else if (a < 0.f) ++neg;
|
||||
}
|
||||
const int flipped = std::min(pos, neg);
|
||||
const bool disk = loops == 1 && euler == 1;
|
||||
if (!disk || flipped > 0) {
|
||||
++bad_charts;
|
||||
std::printf(" chart %2zu: %4zu faces loops=%d euler=%d%s flipped=%d/%d%s\n", c,
|
||||
chart_faces[c].size(), loops, euler, disk ? "" : " NOT A DISK", flipped,
|
||||
pos + neg, flipped ? " FOLDED" : "");
|
||||
}
|
||||
}
|
||||
std::printf(" charts with a defect: %d / %d\n", bad_charts, unwrap.chart_count);
|
||||
|
||||
// What the eye actually sees. Every patch edge shared by two charts should carry the same
|
||||
// UV on both sides once the islands are laid out as a connected net; where it does not,
|
||||
// the texture jumps across that seam. Measured through compute_lscm_uvs(), i.e. the exact
|
||||
// coordinates the bake and the checker overlay sample.
|
||||
{
|
||||
const auto n0 = std::chrono::steady_clock::now();
|
||||
const std::vector<TextureIsland> net = compute_connected_net(unwrap);
|
||||
const auto n1 = std::chrono::steady_clock::now();
|
||||
std::printf(" TIMING compute_connected_net: %.0f ms (%zu islands)\n",
|
||||
std::chrono::duration<double, std::milli>(n1 - n0).count(), net.size());
|
||||
}
|
||||
const auto t2 = std::chrono::steady_clock::now();
|
||||
const std::vector<Vec2f> uv = compute_lscm_uvs(patch, layer);
|
||||
const auto t3 = std::chrono::steady_clock::now();
|
||||
std::printf(" TIMING compute_lscm_uvs: %.0f ms (called on every preview, overlay and bake)\n",
|
||||
std::chrono::duration<double, std::milli>(t3 - t2).count());
|
||||
if (uv.size() != patch.vertices.size()) {
|
||||
std::printf(" compute_lscm_uvs returned %zu uvs for %zu vertices\n", uv.size(),
|
||||
patch.vertices.size());
|
||||
continue;
|
||||
}
|
||||
// Per-corner UVs carry each chart's own placement, so an edge shared by two charts shows
|
||||
// the jump directly: the same mesh vertex lands at two different UVs. That is exactly what
|
||||
// the eye reads as the texture breaking.
|
||||
const auto t4 = std::chrono::steady_clock::now();
|
||||
const std::vector<Vec2f> corner = compute_lscm_corner_uvs(patch, layer);
|
||||
const auto t5 = std::chrono::steady_clock::now();
|
||||
std::printf(" TIMING compute_lscm_corner_uvs: %.0f ms\n",
|
||||
std::chrono::duration<double, std::milli>(t5 - t4).count());
|
||||
// Keyed by edge, holding the UV each incident face gives to the edge's *lower-numbered*
|
||||
// endpoint. Comparing that same vertex on both sides is the point: indexing by corner
|
||||
// position instead compares opposite ends of the edge, because the two faces wind it in
|
||||
// opposite directions.
|
||||
std::unordered_map<uint64_t, std::vector<Vec2f>> edge_seen;
|
||||
if (corner.size() == patch.indices.size() * 3)
|
||||
for (size_t f = 0; f < patch.indices.size(); ++f) {
|
||||
const stl_triangle_vertex_indices &t = patch.indices[f];
|
||||
for (int k = 0; k < 3; ++k) {
|
||||
const int a = t[k], b = t[(k + 1) % 3];
|
||||
const int probe = std::min(a, b);
|
||||
const int local = (a == probe) ? k : (k + 1) % 3;
|
||||
edge_seen[edge_key(a, b)].push_back(corner[f * 3 + size_t(local)]);
|
||||
}
|
||||
}
|
||||
// Which chart each patch face belongs to, so a broken edge can be attributed to a pair.
|
||||
std::vector<int> chart_of_face(patch.indices.size(), -1);
|
||||
for (size_t i = 0; i < unwrap.indices.size(); ++i)
|
||||
chart_of_face[size_t(unwrap.source_face[i])] = unwrap.vertex_chart[size_t(unwrap.indices[i][0])];
|
||||
|
||||
std::unordered_map<uint64_t, std::vector<int>> edge_faces;
|
||||
for (size_t f = 0; f < patch.indices.size(); ++f) {
|
||||
const stl_triangle_vertex_indices &t = patch.indices[f];
|
||||
for (int k = 0; k < 3; ++k)
|
||||
edge_faces[edge_key(t[k], t[(k + 1) % 3])].push_back(int(f));
|
||||
}
|
||||
|
||||
int adjacent = 0, broken = 0, broken_same_chart = 0;
|
||||
float worst = 0.f;
|
||||
std::map<std::pair<int, int>, std::pair<int, float>> by_pair;
|
||||
for (const auto &[key, seen] : edge_seen) {
|
||||
if (seen.size() != 2)
|
||||
continue;
|
||||
++adjacent;
|
||||
const float d = (seen[0] - seen[1]).norm();
|
||||
if (d <= 1e-4f)
|
||||
continue;
|
||||
++broken;
|
||||
worst = std::max(worst, d);
|
||||
const auto &faces_here = edge_faces[key];
|
||||
int c1 = -1, c2 = -1;
|
||||
if (faces_here.size() == 2) {
|
||||
c1 = chart_of_face[size_t(faces_here[0])];
|
||||
c2 = chart_of_face[size_t(faces_here[1])];
|
||||
}
|
||||
if (c1 == c2)
|
||||
++broken_same_chart;
|
||||
auto &slot = by_pair[{ std::min(c1, c2), std::max(c1, c2) }];
|
||||
++slot.first;
|
||||
slot.second = std::max(slot.second, d);
|
||||
}
|
||||
std::printf(" broken edges inside a single chart: %d\n", broken_same_chart);
|
||||
std::printf(" broken by chart pair:");
|
||||
for (const auto &[pk, v] : by_pair)
|
||||
std::printf(" (%d,%d)x%d/%.1f", pk.first, pk.second, v.first, v.second);
|
||||
std::printf("\n");
|
||||
// Total length of the seams left broken, in mm: how much visibly torn edge the layout has,
|
||||
// which is what the eye adds up. A count alone hides whether the breaks are hairlines or
|
||||
// whole sides of an island.
|
||||
float seam_mm = 0.f;
|
||||
for (const auto &[key, seen] : edge_seen) {
|
||||
if (seen.size() != 2 || (seen[0] - seen[1]).norm() <= 1e-4f)
|
||||
continue;
|
||||
seam_mm += (patch.vertices[size_t(key >> 32)] - patch.vertices[size_t(uint32_t(key))]).norm();
|
||||
}
|
||||
std::printf(" interior edges: %d, discontinuous: %d, total torn seam: %.2f mm (worst jump %.3f)\n",
|
||||
adjacent, broken, seam_mm, worst);
|
||||
std::printf(" stored islands %zu vs charts %d -> %s\n", layer.islands.size(),
|
||||
unwrap.chart_count,
|
||||
layer.islands.size() == size_t(unwrap.chart_count) ? "stored placements used"
|
||||
: "net rebuilt");
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -411,6 +411,9 @@ void AppConfig::set_defaults()
|
||||
if (get("show_overhang").empty())
|
||||
set_bool("show_overhang", false);
|
||||
|
||||
if (get("show_center_of_mass").empty())
|
||||
set_bool("show_center_of_mass", false);
|
||||
|
||||
#ifdef _WIN32
|
||||
|
||||
//#ifdef SUPPORT_3D_CONNEXION
|
||||
|
||||
@@ -299,7 +299,15 @@ Points get_shrink_bedpts(const DynamicPrintConfig* print_cfg, const ArrangeParam
|
||||
template<class PConf>
|
||||
void fill_config(PConf& pcfg, const ArrangeParams ¶ms) {
|
||||
|
||||
if (params.is_seq_print) {
|
||||
if (params.is_belt) {
|
||||
// Pack from the end of the belt that prints first, and keep the pile on the
|
||||
// bed when it is larger than the room around that end.
|
||||
pcfg.starting_point = !params.belt_reversed ? PConf::Alignment::BOTTOM_LEFT :
|
||||
params.belt_axis == 1 ? PConf::Alignment::TOP_LEFT :
|
||||
PConf::Alignment::BOTTOM_RIGHT;
|
||||
pcfg.clamp_to_bin = true;
|
||||
}
|
||||
else if (params.is_seq_print) {
|
||||
// Start placing the items from the center of the print bed
|
||||
pcfg.starting_point = PConf::Alignment::BOTTOM_LEFT;
|
||||
}
|
||||
@@ -444,6 +452,50 @@ protected:
|
||||
return bindist;
|
||||
}
|
||||
|
||||
// Belt printers pack from the end of the belt that prints first, and a corner
|
||||
// packer's checks (pile inside the bin, pack origin) apply to them as well.
|
||||
bool corner_packing() const { return params.is_belt || m_pconf.starting_point == PConfig::Alignment::BOTTOM_LEFT; }
|
||||
|
||||
static double at(const Box::PointType &pt, int i) { return double(i == 0 ? getX(pt) : getY(pt)); }
|
||||
|
||||
// Position along the belt in print order: increasing from the end that prints first.
|
||||
double belt_pos(const Box::PointType &pt) const { return params.belt_reversed ? -at(pt, params.belt_axis) : at(pt, params.belt_axis); }
|
||||
double belt_start(const Box &bb) const { return belt_pos(params.belt_reversed ? bb.maxCorner() : bb.minCorner()); }
|
||||
double belt_end(const Box &bb) const { return belt_pos(params.belt_reversed ? bb.minCorner() : bb.maxCorner()); }
|
||||
|
||||
// The corner of the bin the belt pile grows from.
|
||||
Box::PointType belt_origin() const
|
||||
{
|
||||
const Box bb = sl::boundingBox(m_bin);
|
||||
auto o = bb.minCorner();
|
||||
if (params.belt_reversed) {
|
||||
if (params.belt_axis == 0) setX(o, getX(bb.maxCorner()));
|
||||
else setY(o, getY(bb.maxCorner()));
|
||||
}
|
||||
return o;
|
||||
}
|
||||
|
||||
// An item's far edge in print order is what it costs (so a row fills across the
|
||||
// belt before the pile advances), with a slight pull toward the near lateral
|
||||
// edge and the same penalty as the bottom-left heuristic for sitting outside
|
||||
// the corner.
|
||||
double dist_along_belt(const Box &ibb)
|
||||
{
|
||||
const Box bin = sl::boundingBox(m_bin);
|
||||
const int l = 1 - params.belt_axis;
|
||||
const double lat = at(ibb.minCorner(), l) - at(bin.minCorner(), l);
|
||||
double d = belt_end(ibb) - belt_start(bin);
|
||||
d += lat < 0 ? 10 * -lat : 0.1 * lat;
|
||||
if (double behind = belt_start(ibb) - belt_start(bin); behind < 0)
|
||||
d += 10 * -behind;
|
||||
return norm(d);
|
||||
}
|
||||
|
||||
double corner_bindist(const Box &ibb, const Slic3r::Point &origin_pack)
|
||||
{
|
||||
return params.is_belt ? dist_along_belt(ibb) : dist_for_BOTTOM_LEFT(ibb, origin_pack);
|
||||
}
|
||||
|
||||
double dist_to_bin(const Box& ibb, const Slic3r::Point& origin_pack, typename Packer::PlacementConfig::Alignment starting_point_alignment)
|
||||
{
|
||||
double bindist = 0;
|
||||
@@ -533,8 +585,8 @@ protected:
|
||||
|
||||
// The smalles distance from the arranged pile center:
|
||||
double dist = norm(*(std::min_element(dists.begin(), dists.end())));
|
||||
if (m_pconf.starting_point == PConfig::Alignment::BOTTOM_LEFT) {
|
||||
double bindist = dist_for_BOTTOM_LEFT(ibb, origin_pack);
|
||||
if (corner_packing()) {
|
||||
double bindist = corner_bindist(ibb, origin_pack);
|
||||
score = 0.2 * dist + 0.8 * bindist;
|
||||
}
|
||||
else {
|
||||
@@ -591,8 +643,8 @@ protected:
|
||||
break;
|
||||
}
|
||||
case LAST_BIG_ITEM: {
|
||||
if (m_pconf.starting_point == PConfig::Alignment::BOTTOM_LEFT) {
|
||||
score = dist_for_BOTTOM_LEFT(ibb, origin_pack);
|
||||
if (corner_packing()) {
|
||||
score = corner_bindist(ibb, origin_pack);
|
||||
}
|
||||
else {
|
||||
if (m_pilebb.defined)
|
||||
@@ -607,8 +659,8 @@ protected:
|
||||
// already processed bigger items.
|
||||
// No need to play around with the anchor points, the center will be
|
||||
// just fine for small items
|
||||
if (m_pconf.starting_point == PConfig::Alignment::BOTTOM_LEFT)
|
||||
score = dist_for_BOTTOM_LEFT(ibb, origin_pack);
|
||||
if (corner_packing())
|
||||
score = corner_bindist(ibb, origin_pack);
|
||||
else {
|
||||
// Align mainly around existing items
|
||||
score = 0.8 * norm(pl::distance(ibb.center(), bigbb.center()))+ 0.2*norm(pl::distance(ibb.center(), origin_pack));
|
||||
@@ -709,6 +761,28 @@ protected:
|
||||
score += 1 * (new_extruder_cnt-last_extruder_cnt);
|
||||
}
|
||||
|
||||
// On a belt the parts print in belt order, so every colour change between
|
||||
// parts is a filament change. Items arrive sorted by extruder and the pile
|
||||
// grows from the leading end; keep each colour's run contiguous by charging
|
||||
// an item for every packed item of another colour it does not fully follow,
|
||||
// counting the tilted layers that reach belt_tilt_slope * height past that
|
||||
// item's far edge.
|
||||
if (params.is_belt && !params.is_seq_print) {
|
||||
const std::set<int> item_colours(item.extrude_ids.begin(), item.extrude_ids.end());
|
||||
const double item_start = belt_start(ibb);
|
||||
for (Item &p : m_items) {
|
||||
if (p.is_virt_object)
|
||||
continue;
|
||||
const std::set<int> p_colours(p.extrude_ids.begin(), p.extrude_ids.end());
|
||||
const bool same_colour = std::includes(item_colours.begin(), item_colours.end(), p_colours.begin(), p_colours.end())
|
||||
|| std::includes(p_colours.begin(), p_colours.end(), item_colours.begin(), item_colours.end());
|
||||
if (same_colour)
|
||||
continue;
|
||||
if (item_start < belt_end(p.boundingBox()) + scaled(p.height * params.belt_tilt_slope))
|
||||
score += 10.;
|
||||
}
|
||||
}
|
||||
|
||||
return std::make_tuple(score, fullbb);
|
||||
}
|
||||
|
||||
@@ -785,7 +859,8 @@ public:
|
||||
|
||||
auto binbb = sl::boundingBox(m_bin);
|
||||
|
||||
auto starting_point = cfg.starting_point == PConfig::Alignment::BOTTOM_LEFT ? binbb.minCorner() : binbb.center();
|
||||
auto starting_point = this->params.is_belt ? belt_origin() :
|
||||
cfg.starting_point == PConfig::Alignment::BOTTOM_LEFT ? binbb.minCorner() : binbb.center();
|
||||
// if we have wipe tower, items should be arranged around wipe tower
|
||||
for (Item itm : items) {
|
||||
if (itm.is_wipe_tower) {
|
||||
@@ -936,7 +1011,7 @@ std::function<double(const Item &, const ItemGroup&)> AutoArranger<ExPolygon>::g
|
||||
auto mp = m_merged_pile;
|
||||
mp.emplace_back(itm.transformedShape());
|
||||
auto chull = sl::convexHull(mp);
|
||||
if (m_pconf.starting_point == PConfig::Alignment::BOTTOM_LEFT)
|
||||
if (corner_packing())
|
||||
{
|
||||
if (!sl::isInside(chull, m_bin))
|
||||
score += LARGE_COST_TO_REJECT;
|
||||
|
||||
@@ -146,6 +146,13 @@ struct ArrangeParams {
|
||||
float nozzle_height = 0;
|
||||
float printable_height = 256.0;
|
||||
Vec2d align_center{ 0.5,0.5 };
|
||||
// Belt printer: items print in the order they lie along the belt axis, from
|
||||
// its low end unless belt_reversed, and a part's top prints
|
||||
// belt_tilt_slope * height further along it than its base.
|
||||
bool is_belt = false;
|
||||
int belt_axis = 1; // 0 = X, 1 = Y
|
||||
bool belt_reversed = false;
|
||||
float belt_tilt_slope = 1.f; // cot(belt tilt angle), 0 when the belt is not tilted
|
||||
|
||||
ArrangePolygons excluded_regions; // regions cant't be used
|
||||
ArrangePolygons nonprefered_regions; // regions can be used but not prefered
|
||||
|
||||
@@ -0,0 +1,562 @@
|
||||
#include <limits>
|
||||
#include "BeltBrim.hpp"
|
||||
|
||||
#include "ClipperUtils.hpp"
|
||||
#include "Flow.hpp"
|
||||
#include "Layer.hpp"
|
||||
#include "Polygon.hpp"
|
||||
#include "Print.hpp"
|
||||
#include "ShortestPath.hpp"
|
||||
#include "Support/BeltFloorContext.hpp"
|
||||
#include "BoundingBox.hpp"
|
||||
#include "ExPolygon.hpp"
|
||||
#include "ExtrusionEntity.hpp"
|
||||
#include "ExtrusionEntityCollection.hpp"
|
||||
#include "Point.hpp"
|
||||
#include "Polyline.hpp"
|
||||
#include "PrintConfig.hpp"
|
||||
#include "libslic3r.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// ---------------------------------------------------------------- scaling
|
||||
|
||||
static inline Point scale_u_point(const Point &p, int from_axis, double factor)
|
||||
{
|
||||
// llround, not a cast: casting truncates toward zero, so a round trip would
|
||||
// walk every vertex toward the origin by up to one unit per pass.
|
||||
return from_axis == 0 ?
|
||||
Point(coord_t(std::llround(double(p.x()) * factor)), p.y()) :
|
||||
Point(p.x(), coord_t(std::llround(double(p.y()) * factor)));
|
||||
}
|
||||
|
||||
static inline void scale_u_polygon(Polygon &poly, int from_axis, double factor)
|
||||
{
|
||||
for (Point &p : poly.points)
|
||||
p = scale_u_point(p, from_axis, factor);
|
||||
}
|
||||
|
||||
ExPolygons belt_scale_u(const ExPolygons &src, const BeltBrimFrame &frame, double factor)
|
||||
{
|
||||
ExPolygons out = src;
|
||||
for (ExPolygon &ex : out) {
|
||||
scale_u_polygon(ex.contour, frame.from_axis, factor);
|
||||
for (Polygon &hole : ex.holes)
|
||||
scale_u_polygon(hole, frame.from_axis, factor);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
Polylines belt_scale_u(const Polylines &src, const BeltBrimFrame &frame, double factor)
|
||||
{
|
||||
Polylines out = src;
|
||||
for (Polyline &pl : out)
|
||||
for (Point &p : pl.points)
|
||||
p = scale_u_point(p, frame.from_axis, factor);
|
||||
return out;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- sweep
|
||||
|
||||
ExPolygons sweep_ex(const ExPolygons &src, const Point &t)
|
||||
{
|
||||
if (src.empty())
|
||||
return {};
|
||||
if (t == Point(0, 0))
|
||||
return src;
|
||||
|
||||
// One parallelogram per boundary edge. Together with P and P + t these
|
||||
// cover the Minkowski sum exactly: for any q = p + s*t with p in P and
|
||||
// s in [0, 1], let s* be the smallest lambda >= 0 with q - lambda*t in P.
|
||||
// Either s* == 0 (so q is in P) or q - s* * t lies on some boundary edge e,
|
||||
// putting q in that edge's parallelogram. Hole edges must be included, or
|
||||
// holes narrower than t along t would wrongly survive the sweep.
|
||||
Polygons quads;
|
||||
for (const ExPolygon &ex : src)
|
||||
for (size_t c = 0; c < ex.num_contours(); ++ c)
|
||||
for (const Line &e : ex.contour_or_hole(c).lines()) {
|
||||
if (e.a == e.b)
|
||||
continue;
|
||||
Polygon q;
|
||||
q.points = { e.a, e.b, e.b + t, e.a + t };
|
||||
// The non-zero fill rule counts a clockwise ring as -1, which
|
||||
// would punch a hole instead of adding material. Edges parallel
|
||||
// to t give a zero-area quad; Clipper discards those harmlessly.
|
||||
if (q.is_clockwise())
|
||||
q.reverse();
|
||||
quads.emplace_back(std::move(q));
|
||||
}
|
||||
|
||||
ExPolygons shifted = src;
|
||||
for (ExPolygon &ex : shifted)
|
||||
ex.translate(t);
|
||||
|
||||
// union_ex(ExPolygons, Polygons) uses pftNonZero, which is the fill rule the
|
||||
// argument above relies on.
|
||||
return union_ex(union_ex(src, shifted), quads);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- brim region
|
||||
|
||||
ExPolygons belt_brim_region(const ExPolygons &footprint_flat,
|
||||
bool has_outer,
|
||||
bool has_inner,
|
||||
coord_t brim_width,
|
||||
coord_t object_gap,
|
||||
coord_t leading,
|
||||
coord_t lateral,
|
||||
const BeltBrimFrame &frame)
|
||||
{
|
||||
if (footprint_flat.empty() || (! has_outer && ! has_inner))
|
||||
return {};
|
||||
|
||||
ExPolygons out;
|
||||
|
||||
if (has_outer) {
|
||||
// Offset the outer ring from the contours only, so a hole cannot punch
|
||||
// through it. Same reasoning as the plate brim in Brim.cpp.
|
||||
Polygons contours;
|
||||
contours.reserve(footprint_flat.size());
|
||||
for (const ExPolygon &ex : footprint_flat)
|
||||
contours.emplace_back(ex.contour);
|
||||
|
||||
// Inner and outer boundary offset from the same polygon, to avoid
|
||||
// round-off mismatch between them.
|
||||
ExPolygons inner = offset_ex(contours, float(object_gap), jtRound, SCALED_RESOLUTION);
|
||||
|
||||
// Close the interior before offsetting outwards. A belt contact patch is often a
|
||||
// narrow, broken-up strip, and the offset rings of two islands less than
|
||||
// 2 x brim_width apart merge and fill the space between them - space that lies
|
||||
// UNDER the part, which is not what "outer brim" means. Closing also swallows
|
||||
// holes in the patch for the same reason. Concavity-filling only, so an apron or
|
||||
// any other outward protrusion is untouched.
|
||||
ExPolygons envelope = brim_width > 0 ? closing_ex(inner, float(brim_width)) : inner;
|
||||
|
||||
ExPolygons base = envelope;
|
||||
if (leading > 0) {
|
||||
// Sweep downhill from the gapped keep-out, so the apron is contiguous with
|
||||
// the ring instead of starting inside the gap.
|
||||
const Point t = frame.from_axis == 0 ?
|
||||
Point(frame.downhill_sign() * leading, 0) :
|
||||
Point(0, frame.downhill_sign() * leading);
|
||||
base = union_ex(base, sweep_ex(envelope, t));
|
||||
}
|
||||
if (lateral > 0) {
|
||||
// Across the belt, both ways. Swept from `base` so the apron is widened
|
||||
// too, and in the flattened frame the cross-belt axis is unscaled, so this
|
||||
// distance is already a true on-belt distance.
|
||||
const Point t = frame.from_axis == 0 ? Point(0, lateral) : Point(lateral, 0);
|
||||
ExPolygons widened = union_ex(sweep_ex(base, t), sweep_ex(base, Point(-t.x(), -t.y())));
|
||||
base = union_ex(base, to_polygons(widened));
|
||||
}
|
||||
ExPolygons outer = offset_ex(base, float(brim_width), jtRound, SCALED_RESOLUTION);
|
||||
expolygons_append(out, diff_ex(outer, envelope));
|
||||
}
|
||||
|
||||
if (has_inner) {
|
||||
// Holes reversed so a negative offset grows inward, mirroring Brim.cpp.
|
||||
// No apron here: an apron growing into a hole interior is never useful.
|
||||
Polygons holes;
|
||||
for (const ExPolygon &ex : footprint_flat)
|
||||
polygons_append(holes, ex.holes);
|
||||
polygons_reverse(holes);
|
||||
if (! holes.empty()) {
|
||||
ExPolygons hole_inner = offset_ex(holes, - float(brim_width + object_gap));
|
||||
ExPolygons hole_outer = offset_ex(holes, - float(object_gap));
|
||||
expolygons_append(out, intersection_ex(diff_ex(hole_outer, hole_inner), holes));
|
||||
}
|
||||
}
|
||||
|
||||
return union_ex(out);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- line lattice
|
||||
|
||||
std::vector<coord_t> belt_brim_line_positions(coord_t u_lo,
|
||||
coord_t u_hi,
|
||||
coord_t pitch_u,
|
||||
coord_t u_anchor)
|
||||
{
|
||||
std::vector<coord_t> out;
|
||||
if (pitch_u <= 0 || u_hi <= u_lo)
|
||||
return out;
|
||||
|
||||
// Walk the lattice from just below u_lo. Integer arithmetic throughout, so
|
||||
// the half-open interval needs no epsilon: a point landing exactly on u_hi
|
||||
// belongs to the next band.
|
||||
int64_t k = int64_t(std::floor(double(u_lo - u_anchor) / double(pitch_u))) - 1;
|
||||
while (u_anchor + coord_t(k) * pitch_u < u_lo)
|
||||
++ k;
|
||||
for (;; ++ k) {
|
||||
const coord_t u = u_anchor + coord_t(k) * pitch_u;
|
||||
if (u >= u_hi)
|
||||
break;
|
||||
out.emplace_back(u);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- pipeline
|
||||
|
||||
// A band of the belt surface as an explicit box, clamped to `bounds` along the
|
||||
// shear axis. Deliberately not BeltFloorContext::surface_polygon(): those
|
||||
// half-planes span +-1000 mm, which is wasteful to clip against and dangerous to
|
||||
// feed through the flattening scale.
|
||||
static Polygon band_box(const BoundingBox &bounds, int from_axis, coordf_t u_lo, coordf_t u_hi)
|
||||
{
|
||||
coord_t lo = scale_(u_lo);
|
||||
coord_t hi = scale_(u_hi);
|
||||
const coord_t bmin = from_axis == 0 ? bounds.min.x() : bounds.min.y();
|
||||
const coord_t bmax = from_axis == 0 ? bounds.max.x() : bounds.max.y();
|
||||
lo = std::max(lo, bmin);
|
||||
hi = std::min(hi, bmax);
|
||||
Polygon poly;
|
||||
if (hi <= lo)
|
||||
return poly;
|
||||
if (from_axis == 0)
|
||||
poly.points = { Point(lo, bounds.min.y()), Point(hi, bounds.min.y()),
|
||||
Point(hi, bounds.max.y()), Point(lo, bounds.max.y()) };
|
||||
else
|
||||
poly.points = { Point(bounds.min.x(), lo), Point(bounds.max.x(), lo),
|
||||
Point(bounds.max.x(), hi), Point(bounds.min.x(), hi) };
|
||||
return poly;
|
||||
}
|
||||
|
||||
ExPolygons belt_brim_clip_leading_edge(const ExPolygons ®ion, const BeltBrimFrame &frame, coordf_t u_cut)
|
||||
{
|
||||
if (region.empty())
|
||||
return region;
|
||||
BoundingBox keep_bb = get_extents(region);
|
||||
keep_bb.offset(scale_(1.));
|
||||
const bool low_side = frame.downhill_sign() < 0; // downhill is -u
|
||||
const Polygon keep = band_box(keep_bb, frame.from_axis,
|
||||
low_side ? unscale<double>(frame.from_axis == 0 ? keep_bb.min.x() : keep_bb.min.y()) : u_cut,
|
||||
low_side ? u_cut : unscale<double>(frame.from_axis == 0 ? keep_bb.max.x() : keep_bb.max.y()));
|
||||
return keep.empty() ? ExPolygons{} : intersection_ex(region, Polygons{ keep });
|
||||
}
|
||||
|
||||
// Everything the per-band line generator needs, gathered once per object.
|
||||
struct BeltBrimContext
|
||||
{
|
||||
BeltFloorContext ctx;
|
||||
BeltBrimFrame frame;
|
||||
ExPolygons region; // brim region, object-local slicing XY
|
||||
BoundingBox region_bbox;
|
||||
Flow brim_flow;
|
||||
coord_t pitch_u = 0;
|
||||
coord_t u_anchor = 0;
|
||||
double in_plane_pitch = 0.; // mm
|
||||
};
|
||||
|
||||
// Emit the cross-belt brim lines that belong to the band [print_z - height, print_z].
|
||||
static void belt_brim_band_paths(const BeltBrimContext &bc,
|
||||
coordf_t print_z,
|
||||
coordf_t height,
|
||||
const Polygons &obstacles,
|
||||
ExtrusionEntityCollection &out,
|
||||
ExPolygons &areas_out)
|
||||
{
|
||||
coordf_t u_lo = bc.ctx.cutoff_u(print_z - height);
|
||||
coordf_t u_hi = bc.ctx.cutoff_u(print_z);
|
||||
if (u_lo > u_hi)
|
||||
std::swap(u_lo, u_hi);
|
||||
|
||||
// How wide this band is measured ON the belt, versus one nominal bead.
|
||||
const double band_in_plane = (u_hi - u_lo) * bc.frame.u_stretch();
|
||||
|
||||
// Fraction of the layer height at which a line sits above the belt. Toward the
|
||||
// downhill edge, so the sheet is reasonably thick while the nozzle stays clear of
|
||||
// the belt itself.
|
||||
static constexpr double BAND_CLEARANCE_FRACTION = 0.75;
|
||||
|
||||
std::vector<coord_t> us;
|
||||
double uniform_clearance = 0.; // 0 => derive per line from its own position
|
||||
double line_pitch = bc.in_plane_pitch;
|
||||
// One line also serves a band up to half a bead wider than the nominal pitch (a 0.3 mm
|
||||
// first layer at 45 degrees): its flow is matched to the band, so the bead is that much
|
||||
// wider. Two lattice lines in such a band would land almost on top of each other.
|
||||
if (band_in_plane <= 1.5 * bc.in_plane_pitch + EPSILON) {
|
||||
// Steep belt, which is the normal case: the band is narrower than one bead, so
|
||||
// exactly one line fits. Place it at a FIXED fraction of the band rather than
|
||||
// on a nominal-spacing lattice. On a lattice each line lands at an arbitrary
|
||||
// point in its band, the clearance sweeps [0, height] from band to band, and the
|
||||
// bead width therefore varies by 2x - visible as ragged, uneven brim lines.
|
||||
// Anchoring to the band makes the clearance identical everywhere, so every bead
|
||||
// is the same width.
|
||||
//
|
||||
// The spacing is then whatever the bands give (height / sin(tilt) on the belt)
|
||||
// rather than the nominal bead spacing, so the flow below is matched to THAT
|
||||
// pitch. Matched flow at the real pitch is what keeps the sheet uniform and
|
||||
// gap-free; using nominal flow at band spacing would over-feed it.
|
||||
us.push_back(scale_(bc.ctx.cutoff_u(print_z - BAND_CLEARANCE_FRACTION * height)));
|
||||
uniform_clearance = BAND_CLEARANCE_FRACTION * height;
|
||||
line_pitch = band_in_plane;
|
||||
} else {
|
||||
// Shallow belt: the band is wider than a bead, so it takes several lines and they
|
||||
// have to sit on the nominal lattice. Their clearances then differ, and so do
|
||||
// their widths - unavoidable here, but shallow belts are the rare case.
|
||||
us = belt_brim_line_positions(scale_(u_lo), scale_(u_hi), bc.pitch_u, bc.u_anchor);
|
||||
}
|
||||
if (us.empty())
|
||||
return;
|
||||
|
||||
const Polygons region_polys = to_polygons(bc.region);
|
||||
|
||||
// One lattice line at a time: the clearance - and therefore the extrusion
|
||||
// volume - is a property of the line's u, so the pieces of different lines
|
||||
// must not be pooled before the flow is resolved.
|
||||
// Overshoot the region so the clip, not the line's ends, decides the extent.
|
||||
const coord_t margin = coord_t(SCALED_EPSILON) + 1;
|
||||
coord_t u_prev = std::numeric_limits<coord_t>::min();
|
||||
for (coord_t u : us) {
|
||||
// Nozzle-to-belt clearance for this line. Constant along the line, because the
|
||||
// belt height depends only on the shear-axis coordinate. Band-anchored lines
|
||||
// share one clearance by construction; lattice lines (shallow belts, or a first
|
||||
// layer thick enough that the band is wider than a bead) each get their own.
|
||||
//
|
||||
// A lattice line can fall where the belt is only a hair below the band's print_z.
|
||||
// The bead there would be laid scraping the belt while its flow is sized for a
|
||||
// taller cell, so it is moved uphill to the same fraction of the band the
|
||||
// single-line case uses. (The clearance is along slice Z; the real gap under the
|
||||
// nozzle is clearance x cos(tilt), 0.53 h at 45 degrees for the 0.75 fraction.)
|
||||
double clearance = uniform_clearance;
|
||||
if (clearance <= 0.) {
|
||||
const Point probe = bc.frame.from_axis == 0 ? Point(u, 0) : Point(0, u);
|
||||
clearance = print_z - bc.ctx.floor_print_z(probe);
|
||||
if (clearance < BAND_CLEARANCE_FRACTION * height) {
|
||||
clearance = BAND_CLEARANCE_FRACTION * height;
|
||||
u = scale_(bc.ctx.cutoff_u(print_z - clearance));
|
||||
}
|
||||
clearance = std::min(clearance, height);
|
||||
}
|
||||
// A line moved uphill can land on, or almost on, its neighbour; two beads closer
|
||||
// than half a pitch would be laid into the same cell.
|
||||
if (u_prev != std::numeric_limits<coord_t>::min() && std::abs(u - u_prev) < bc.pitch_u / 2)
|
||||
continue;
|
||||
u_prev = u;
|
||||
|
||||
Polyline line;
|
||||
if (bc.frame.from_axis == 0)
|
||||
line.points = { Point(u, coord_t(bc.region_bbox.min.y() - margin)),
|
||||
Point(u, coord_t(bc.region_bbox.max.y() + margin)) };
|
||||
else
|
||||
line.points = { Point(coord_t(bc.region_bbox.min.x() - margin), u),
|
||||
Point(coord_t(bc.region_bbox.max.x() + margin), u) };
|
||||
|
||||
Polylines pieces = intersection_pl(Polylines{ line }, region_polys);
|
||||
if (! obstacles.empty())
|
||||
pieces = diff_pl(pieces, obstacles);
|
||||
if (pieces.empty())
|
||||
continue;
|
||||
|
||||
// with_cross_section, not with_height: it reaches the prescribed volume while
|
||||
// KEEPING the extrusion spacing, so the bead is sized to fill exactly one
|
||||
// pitch x clearance cell of the sheet.
|
||||
const Flow f = bc.brim_flow.with_cross_section(float(line_pitch * clearance));
|
||||
|
||||
// Footprint of these beads, for the first-layer convex hull and bbox.
|
||||
for (const Polygon &p : offset(pieces, 0.5f * float(f.scaled_width())))
|
||||
areas_out.emplace_back(ExPolygon(p));
|
||||
|
||||
extrusion_entities_append_paths(out.entities, chain_polylines(std::move(pieces)),
|
||||
erBrim, f.mm3_per_mm(), f.width(), float(clearance));
|
||||
}
|
||||
}
|
||||
|
||||
// Union of everything extruded at `print_z` that the brim must keep clear of, expressed
|
||||
// in `self`'s local slicing frame. Includes `self` itself: its slice at this Z can
|
||||
// overhang outside the belt footprint and land in the brim ring, which the flattened
|
||||
// brim_object_gap - a belt-plane separation - does not cover.
|
||||
//
|
||||
// SEQUENCING: this reads every object's layers and this object's own support layers.
|
||||
// Another object's support step shifts that object's layer Z into the object frame for
|
||||
// the duration of the run (PrintObject::_generate_support_material()), so the brims must
|
||||
// not overlap with the parallel support step: Print::process() generates them one object
|
||||
// after the other once that step is over (PrintObject::generate_belt_brim()), and an
|
||||
// object that arrives on or leaves the plate invalidates the other brim owners' support
|
||||
// step (PrintApply.cpp) so their brims are clipped against what is there now. Only this
|
||||
// object's supports are dodged; another object's support at the same Z is not.
|
||||
// `region_bbox` bounds the brim; anything outside it cannot clip a brim line, so whole
|
||||
// objects are skipped without materialising their polygons. On a typical plate the
|
||||
// objects do not overlap and every foreign object drops out here, which matters because
|
||||
// this runs once per band - hundreds of times per object.
|
||||
static Polygons belt_brim_obstacles(const Print &print, const PrintObject &self,
|
||||
const BoundingBox ®ion_bbox, coordf_t print_z, coordf_t tol)
|
||||
{
|
||||
const Point shift_self = self.instances().empty() ? Point(0, 0)
|
||||
: self.instances().front().shift_without_plate_offset();
|
||||
Polygons out;
|
||||
for (const PrintObject *o : print.objects()) {
|
||||
const bool is_self = (o == &self);
|
||||
for (const PrintInstance &inst : o->instances()) {
|
||||
const Point delta = inst.shift_without_plate_offset() - shift_self;
|
||||
if (const Layer *l = o->get_layer_at_printz(print_z, tol)) {
|
||||
BoundingBox lb = get_extents(l->lslices);
|
||||
lb.translate(delta.x(), delta.y());
|
||||
if (lb.overlap(region_bbox)) {
|
||||
Polygons ps = to_polygons(l->lslices);
|
||||
for (Polygon &p : ps)
|
||||
p.translate(delta);
|
||||
polygons_append(out, std::move(ps));
|
||||
}
|
||||
}
|
||||
if (! is_self)
|
||||
continue;
|
||||
if (const SupportLayer *sl = o->get_support_layer_at_printz(print_z, tol)) {
|
||||
Polygons ps = sl->support_fills.polygons_covered_by_spacing();
|
||||
for (Polygon &p : ps)
|
||||
p.translate(delta);
|
||||
polygons_append(out, std::move(ps));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (out.size() < 2)
|
||||
return out; // union_() of 0 or 1 polygons is pure overhead
|
||||
return union_(out);
|
||||
}
|
||||
|
||||
void make_belt_brim(PrintObject &object)
|
||||
{
|
||||
object.clear_belt_brim();
|
||||
if (! object.has_belt_brim())
|
||||
return;
|
||||
|
||||
const Print &print = *object.print();
|
||||
BeltBrimContext bc;
|
||||
if (! bc.ctx.init(object.slicing_parameters(), print.config()))
|
||||
return;
|
||||
bc.frame = BeltBrimFrame{ bc.ctx.shear_factor(), bc.ctx.from_axis() };
|
||||
|
||||
const size_t nlayers = object.layers().size();
|
||||
if (nlayers == 0)
|
||||
return;
|
||||
|
||||
// 1. Belt footprint: the union of each layer's slice clipped to that layer's
|
||||
// own contact band. This is the object's bottom face, which on a belt is
|
||||
// spread over every layer instead of sitting in layer 0.
|
||||
ExPolygons footprint_acc;
|
||||
// The first layer that touches the belt: where the leading-edge brim is cut.
|
||||
const Layer *first_contact = nullptr;
|
||||
for (size_t i = 0; i < nlayers; ++ i) {
|
||||
const Layer &layer = *object.layers()[i];
|
||||
if (layer.lslices.empty())
|
||||
continue;
|
||||
// print_z - height, not the previous layer's print_z: variable layer
|
||||
// heights make the latter wrong.
|
||||
coordf_t u_lo = bc.ctx.cutoff_u(layer.print_z - layer.height);
|
||||
coordf_t u_hi = bc.ctx.cutoff_u(layer.print_z);
|
||||
if (u_lo > u_hi)
|
||||
std::swap(u_lo, u_hi);
|
||||
BoundingBox bb = get_extents(layer.lslices);
|
||||
bb.offset(scale_(1.));
|
||||
const Polygon band = band_box(bb, bc.frame.from_axis, u_lo, u_hi);
|
||||
if (band.empty())
|
||||
continue;
|
||||
ExPolygons contact = intersection_ex(layer.lslices, Polygons{ band });
|
||||
if (contact.empty())
|
||||
continue;
|
||||
if (first_contact == nullptr)
|
||||
first_contact = &layer;
|
||||
expolygons_append(footprint_acc, std::move(contact));
|
||||
}
|
||||
const ExPolygons footprint = union_ex(footprint_acc);
|
||||
if (footprint.empty())
|
||||
return;
|
||||
|
||||
// 2. Brim region, offset in the flattened (true on-belt) metric.
|
||||
const PrintObjectConfig &cfg = object.config();
|
||||
bc.brim_flow = print.brim_flow();
|
||||
const double flow_w = bc.brim_flow.scaled_spacing() * SCALING_FACTOR;
|
||||
// Quantize to an even number of lines, as the plate brim does.
|
||||
const coord_t width = scale_(std::floor(cfg.brim_width.value / flow_w / 2) * flow_w * 2);
|
||||
const coord_t leading = scale_(cfg.leading_brim_length.value);
|
||||
const coord_t lateral = scale_(cfg.extra_brim_width.value);
|
||||
const coord_t gap = scale_(cfg.brim_object_gap.value);
|
||||
|
||||
// Belt printers collapse Auto / Mouse ear / Painted to outer-only: the auto width
|
||||
// heuristic and flat ear discs have no meaning on a tilted plane. Leading-edge-only
|
||||
// is an outer brim too; it is narrowed down to the first contact below.
|
||||
const BrimType bt = cfg.brim_type.value;
|
||||
const bool has_outer = bt == btOuterOnly || bt == btOuterAndInner
|
||||
|| bt == btAutoBrim || bt == btEar || bt == btPainted
|
||||
|| bt == btLeadingEdgeOnly;
|
||||
const bool has_inner = bt == btInnerOnly || bt == btOuterAndInner;
|
||||
|
||||
bc.region = belt_unflatten(
|
||||
belt_brim_region(belt_flatten(footprint, bc.frame), has_outer, has_inner,
|
||||
width, gap, leading, lateral, bc.frame),
|
||||
bc.frame);
|
||||
|
||||
if (bt == btLeadingEdgeOnly && first_contact != nullptr && ! bc.region.empty())
|
||||
// The cut is the uphill edge of the first contact's band: everything past it
|
||||
// belongs to later contacts. The first contact is the first layer that touches
|
||||
// the belt (step 1), neither layers().front(), an empty lead-in layer, nor the
|
||||
// first layer with geometry, which is an overhang's tip when the part overhangs
|
||||
// its leading end: both lie ahead of the part.
|
||||
bc.region = belt_brim_clip_leading_edge(bc.region, bc.frame, bc.ctx.cutoff_u(first_contact->print_z));
|
||||
|
||||
if (bc.region.empty())
|
||||
return;
|
||||
bc.region_bbox = get_extents(bc.region);
|
||||
|
||||
// 3. Line lattice. Fixed pitch in the flattened metric, anchored at the
|
||||
// footprint's leading-most edge so lines stay collinear across
|
||||
// disconnected islands and across the apron prologue.
|
||||
bc.pitch_u = std::max<coord_t>(1, coord_t(bc.brim_flow.scaled_spacing() * bc.frame.cos_tilt()));
|
||||
bc.in_plane_pitch = unscale<double>(bc.pitch_u) * bc.frame.u_stretch();
|
||||
{
|
||||
const BoundingBox fbb = get_extents(footprint);
|
||||
const bool low_side = bc.frame.shear > 0.;
|
||||
bc.u_anchor = bc.frame.from_axis == 0 ? (low_side ? fbb.min.x() : fbb.max.x())
|
||||
: (low_side ? fbb.min.y() : fbb.max.y());
|
||||
}
|
||||
|
||||
// 4. Bands coincident with an object layer.
|
||||
std::vector<ExtrusionEntityCollection> by_layer(nlayers);
|
||||
std::vector<ExPolygons> areas_by_layer(nlayers);
|
||||
for (size_t i = 0; i < nlayers; ++ i) {
|
||||
const Layer &layer = *object.layers()[i];
|
||||
const Polygons obstacles = belt_brim_obstacles(print, object, bc.region_bbox, layer.print_z, 0.5 * layer.height);
|
||||
belt_brim_band_paths(bc, layer.print_z, layer.height, obstacles, by_layer[i], areas_by_layer[i]);
|
||||
}
|
||||
|
||||
// 5. Apron prologue: the part of the region downhill of the object's first
|
||||
// layer, which has no object layer to ride on.
|
||||
std::vector<BeltBrimBand> prologue;
|
||||
{
|
||||
const Layer &first = *object.layers().front();
|
||||
const coordf_t h = first.height;
|
||||
const bool low_side = bc.frame.shear > 0.;
|
||||
const coord_t u_lead_s = bc.frame.from_axis == 0
|
||||
? (low_side ? bc.region_bbox.min.x() : bc.region_bbox.max.x())
|
||||
: (low_side ? bc.region_bbox.min.y() : bc.region_bbox.max.y());
|
||||
const coordf_t u_lead = unscale<double>(u_lead_s);
|
||||
// print_z at which the belt surface crosses the region's leading edge.
|
||||
const coordf_t z_lead = bc.ctx.shear_factor() * u_lead
|
||||
+ bc.ctx.floor_offset() + bc.ctx.z_shift();
|
||||
if (h > EPSILON)
|
||||
for (coordf_t z = first.print_z - h; z > z_lead - h; z -= h) {
|
||||
const Polygons obstacles = belt_brim_obstacles(print, object, bc.region_bbox, z, 0.5 * h);
|
||||
BeltBrimBand band;
|
||||
band.print_z = z;
|
||||
band.height = h;
|
||||
belt_brim_band_paths(bc, z, h, obstacles, band.fills, band.areas);
|
||||
if (! band.fills.empty())
|
||||
prologue.emplace_back(std::move(band));
|
||||
}
|
||||
// Lowest Z first, so collect_layers_to_print sees them in print order.
|
||||
std::reverse(prologue.begin(), prologue.end());
|
||||
}
|
||||
|
||||
object.set_belt_brim(std::move(by_layer), std::move(areas_by_layer), std::move(prologue));
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,178 @@
|
||||
#ifndef slic3r_BeltBrim_hpp_
|
||||
#define slic3r_BeltBrim_hpp_
|
||||
|
||||
#include "ExPolygon.hpp"
|
||||
#include "ExtrusionEntityCollection.hpp"
|
||||
#include "Point.hpp"
|
||||
#include "Polyline.hpp"
|
||||
#include "libslic3r.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
|
||||
// Belt-printer brim geometry.
|
||||
//
|
||||
// A belt printer slices in a ROTATED frame, so the belt surface is not the
|
||||
// Z=0 bed plane but a tilted plane in slicing space:
|
||||
//
|
||||
// z_slicing(u) = shear * u + floor_offset + z_shift, u = X or Y
|
||||
//
|
||||
// where `shear == tan(tilt)` (SlicingParameters::belt_floor_shear_factor) and
|
||||
// the axis is selected by SlicingParameters::belt_floor_from_axis. See
|
||||
// Support/BeltFloorContext.hpp for the canonical accessors.
|
||||
//
|
||||
// Consequences that drive everything in this file:
|
||||
//
|
||||
// * A horizontal slicing layer touches the belt only along a narrow strip at
|
||||
// its leading edge, `layer_height / shear` wide (~0.2 mm at 45 degrees).
|
||||
// The object's belt footprint - its bottom face - is therefore spread over
|
||||
// every layer, not contained in layer 0.
|
||||
// * Distances measured in slicing XY are NOT on-belt distances: moving `du`
|
||||
// along the shear axis travels `du / cos(tilt)` across the belt. So brim
|
||||
// offsets have to be taken in a "flattened" space where the shear axis is
|
||||
// stretched by `1 / cos(tilt)`, then mapped back.
|
||||
// * Brim ahead of the part (downhill) lies at slicing Z BELOW the object's
|
||||
// first layer, because the object's layer 0 is precisely its leading
|
||||
// contact with the belt.
|
||||
//
|
||||
// Everything here is pure geometry on ExPolygons/Polylines so it can be unit
|
||||
// tested without a Print. Keep user-visible strings out of this file: it is
|
||||
// not listed in localization/i18n/list.txt.
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// Tilt window within which the BELT plane, not the bed plane, is the adhesion
|
||||
// surface. Below ~1 degree a belt is a flat bed as far as adhesion goes, and the
|
||||
// contact band would be layer_height/sin(tilt) - tens of millimetres - so the
|
||||
// ordinary plate brim is both correct and cheaper. Above ~85 degrees the whole
|
||||
// brim compresses into a sliver and is not worth generating.
|
||||
inline constexpr double BELT_BRIM_MIN_TILT_DEG = 1.;
|
||||
inline constexpr double BELT_BRIM_MAX_TILT_DEG = 85.;
|
||||
|
||||
// Description of the tilted belt plane, reduced to what the brim geometry needs.
|
||||
struct BeltBrimFrame
|
||||
{
|
||||
// tan(tilt). Sign selects which way is downhill.
|
||||
double shear = 0.;
|
||||
// 0 = X, 1 = Y. Matches BeltFloorContext::from_axis().
|
||||
int from_axis = 1;
|
||||
|
||||
// 1 / cos(tilt). Stretch factor that turns a projected distance along
|
||||
// `from_axis` into the true distance travelled across the belt.
|
||||
double u_stretch() const { return std::sqrt(1. + shear * shear); }
|
||||
// cos(tilt). The inverse mapping.
|
||||
double cos_tilt() const { return 1. / this->u_stretch(); }
|
||||
// Downhill is where the belt surface is lower, i.e. printed earlier, i.e.
|
||||
// the leading edge of the part. For shear > 0 that is -u.
|
||||
int downhill_sign() const { return shear > 0. ? -1 : +1; }
|
||||
};
|
||||
|
||||
// Scale only the `from_axis` component by `factor`, rounding to nearest.
|
||||
//
|
||||
// Deliberately not MultiPoint::scale(fx, fy) / ExPolygon::scale(fx, fy): those
|
||||
// truncate toward zero, which is asymmetric about the origin and loses up to a
|
||||
// full coordinate unit per vertex on every round trip.
|
||||
ExPolygons belt_scale_u(const ExPolygons &src, const BeltBrimFrame &frame, double factor);
|
||||
Polylines belt_scale_u(const Polylines &src, const BeltBrimFrame &frame, double factor);
|
||||
|
||||
// Into / out of the space where Euclidean offsets equal true on-belt distances.
|
||||
inline ExPolygons belt_flatten(const ExPolygons &src, const BeltBrimFrame &frame)
|
||||
{ return belt_scale_u(src, frame, frame.u_stretch()); }
|
||||
inline ExPolygons belt_unflatten(const ExPolygons &src, const BeltBrimFrame &frame)
|
||||
{ return belt_scale_u(src, frame, frame.cos_tilt()); }
|
||||
|
||||
// Minkowski sum of `src` with the segment [0, t]: the region swept by sliding
|
||||
// `src` along t. Used to grow the brim downhill for "extra brim width".
|
||||
//
|
||||
// Implemented as union_(P, P + t, {parallelogram per boundary edge}) over ALL
|
||||
// contours including holes, with every parallelogram forced counter-clockwise
|
||||
// so the non-zero fill rule closes holes narrower than t along the sweep
|
||||
// direction. A hole survives exactly when it is wider than |t| measured along
|
||||
// t - not when it is wider in its narrowest Euclidean direction.
|
||||
ExPolygons sweep_ex(const ExPolygons &src, const Point &t);
|
||||
|
||||
// "Leading edge only": keep the part of a brim region (unflattened, slicing XY)
|
||||
// at or downhill of the object's first contact with the belt, so the part is
|
||||
// supported as it lands and nothing is printed alongside it afterwards. `u_cut`
|
||||
// is the uphill edge of the first layer's contact band along `frame.from_axis`,
|
||||
// in mm (BeltFloorContext::cutoff_u of the first layer); downhill is the side
|
||||
// `frame.downhill_sign()` points to.
|
||||
ExPolygons belt_brim_clip_leading_edge(const ExPolygons ®ion, const BeltBrimFrame &frame, coordf_t u_cut);
|
||||
|
||||
// Brim region for one already-flattened belt footprint. All lengths are scaled
|
||||
// and measured in the flattened (true on-belt) metric.
|
||||
//
|
||||
// `has_outer` / `has_inner` are the resolved BrimType: belt printers collapse
|
||||
// Auto / Mouse ear / Painted to outer-only, so the caller does that mapping and
|
||||
// this function never needs PrintConfig.
|
||||
//
|
||||
// Two directional extras are applied to the footprint before the outer offset, so
|
||||
// each one buys reach in one direction only:
|
||||
//
|
||||
// `leading` (leading_brim_length) sweeps the footprint DOWNHILL along the belt,
|
||||
// so every leading-facing edge gains an apron ahead of it.
|
||||
// `lateral` (extra_brim_width) sweeps it BOTH WAYS across the belt, widening
|
||||
// the brim sideways without pushing it further ahead or behind.
|
||||
//
|
||||
// Neither is applied to the inner (hole) ring.
|
||||
ExPolygons belt_brim_region(const ExPolygons &footprint_flat,
|
||||
bool has_outer,
|
||||
bool has_inner,
|
||||
coord_t brim_width,
|
||||
coord_t object_gap,
|
||||
coord_t leading,
|
||||
coord_t lateral,
|
||||
const BeltBrimFrame &frame);
|
||||
|
||||
// Brim line positions for one layer band.
|
||||
//
|
||||
// Lines sit on a fixed lattice `u_anchor + k * pitch_u` so the on-belt spacing
|
||||
// between neighbouring brim lines is constant regardless of how the lattice
|
||||
// falls across layer bands. Snapping to band centres instead would quantise
|
||||
// the spacing to whole bands and under-deposit by ~35% at 45 degrees.
|
||||
//
|
||||
// The band is half-open, [u_lo, u_hi), so every lattice point belongs to
|
||||
// exactly one band: none duplicated at a boundary, none dropped. A band
|
||||
// narrower than the pitch simply yields nothing; a band much wider (shallow
|
||||
// tilt) yields several lines.
|
||||
std::vector<coord_t> belt_brim_line_positions(coord_t u_lo,
|
||||
coord_t u_hi,
|
||||
coord_t pitch_u,
|
||||
coord_t u_anchor);
|
||||
|
||||
// ---------------------------------------------------------------- pipeline
|
||||
|
||||
// One brim-only layer printed BEFORE the object's first layer, carrying the
|
||||
// apron that has to be stuck to the belt ahead of the part.
|
||||
//
|
||||
// Deliberately not a Layer subclass. A synthetic Layer would inherit id()
|
||||
// semantics that leak into initial-layer temperature selection, the spiral vase
|
||||
// probe, gradual interpolation, avoid-crossing-perimeters and cooling, all of
|
||||
// which key off Layer::id() == 0 or off a layer's regions. A plain record
|
||||
// carries only what the emitter needs.
|
||||
//
|
||||
// `height` is the LAYER height, used for the Z move and ordering metadata only.
|
||||
// Each extrusion path inside `fills` carries its own height, equal to that
|
||||
// line's nozzle-to-belt clearance, which varies across the band.
|
||||
struct BeltBrimBand
|
||||
{
|
||||
coordf_t print_z = 0.;
|
||||
coordf_t height = 0.;
|
||||
// erBrim paths in the object's local slicing frame, untranslated.
|
||||
ExtrusionEntityCollection fills;
|
||||
// Footprint of those paths, for the first-layer convex hull / bbox.
|
||||
ExPolygons areas;
|
||||
};
|
||||
|
||||
class PrintObject;
|
||||
|
||||
// Generate the belt brim for one object: fills its per-object-layer bands and
|
||||
// its apron prologue. No-op unless PrintObject::has_belt_brim().
|
||||
//
|
||||
// Runs inside posSupportMaterial rather than the brim step, because the prologue
|
||||
// print_z values must exist before ToolOrdering is built at psWipeTower.
|
||||
void make_belt_brim(PrintObject &object);
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // slic3r_BeltBrim_hpp_
|
||||
@@ -0,0 +1,50 @@
|
||||
#include "BeltGCode.hpp"
|
||||
#include "GCodeWriter.hpp"
|
||||
#include "GCode/BeltKinematics.hpp"
|
||||
#include "BeltTransform.hpp"
|
||||
#include "Print.hpp"
|
||||
#include "Point.hpp"
|
||||
#include "PrintConfig.hpp"
|
||||
#include "libslic3r.h"
|
||||
#include <cstdlib>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
void BeltGCode::init_belt_writer(Print &print)
|
||||
{
|
||||
// Axis remap and build volume max are set by base GCode after init_belt_writer
|
||||
// returns; set_kinematics() replays them, so install order does not matter.
|
||||
install_belt_kinematics(m_writer, print.config());
|
||||
m_writer.set_force_normal_lift(true);
|
||||
}
|
||||
|
||||
void BeltGCode::write_belt_header(GCodeOutputStream &file, const Print &print)
|
||||
{
|
||||
const auto &full_cfg = print.full_print_config();
|
||||
// Slicing rotation: the belt tilt (axis + angle) and the single source of truth
|
||||
// for the physical tilt the G-code viewer uses to enable belt view.
|
||||
file.write_format("; belt_slice_rotation = %s\n", full_cfg.opt_serialize("belt_slice_rotation").c_str());
|
||||
file.write_format("; belt_slice_rotation_angle = %.1f\n", print.config().belt_slice_rotation_angle.value);
|
||||
// Machine-frame transform: shear (cot) + scale (1/|sin|) derived from the belt
|
||||
// tilt angle (or belt_frame_tilt_angle when decoupled).
|
||||
file.write_format("; belt_frame_tilt_decouple = %d\n", print.config().belt_frame_tilt_decouple.value ? 1 : 0);
|
||||
file.write_format("; belt_frame_tilt_angle = %.1f\n", print.config().belt_frame_tilt_angle.value);
|
||||
}
|
||||
|
||||
void BeltGCode::on_set_origin(const PrintObject * /*obj*/, const Point & /*inst_shift*/)
|
||||
{
|
||||
// Matches the per-instance Z-offset added in PrintObjectSlice.cpp: transform
|
||||
// the origin through the belt pipeline so that back_transform(T * origin) =
|
||||
// origin (correct machine position). The back_transform applied during
|
||||
// G-code emission is the inverse of the forward transform.
|
||||
|
||||
// Adjust origin: transform through belt forward pipeline so that
|
||||
// the back-transform correctly recovers model-space positions.
|
||||
Transform3d T = BeltTransformPipeline::build_forward_transform(m_config);
|
||||
Vec2d cur_origin = this->origin();
|
||||
Vec3d origin3d(cur_origin.x(), cur_origin.y(), 0.);
|
||||
Vec3d adjusted = T.linear() * origin3d;
|
||||
this->set_origin(Vec2d(adjusted.x(), adjusted.y()));
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
#include "GCode.hpp"
|
||||
#include "Point.hpp"
|
||||
#include "Print.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// Belt-printer-specific GCode export.
|
||||
//
|
||||
// Inherits from GCode and overrides virtual hooks to:
|
||||
// - Install a BeltKinematics on the GCodeWriter
|
||||
// - Write belt configuration to the G-code header
|
||||
// - Adjust the origin for global pre-slice transforms when switching instances
|
||||
// (Arc fitting is disabled for belt printers by BeltKinematics::supports_arc_moves(),
|
||||
// which the base GCode::should_disable_arc_fitting() consults -- no override needed.)
|
||||
class BeltGCode : public GCode
|
||||
{
|
||||
protected:
|
||||
void init_belt_writer(Print &print) override;
|
||||
void write_belt_header(GCodeOutputStream &file, const Print &print) override;
|
||||
void on_set_origin(const PrintObject *obj, const Point &inst_shift) override;
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,433 @@
|
||||
// ORCA-Belt: backend of the belt purge tower (the belt replacement for the
|
||||
// classic wipe/prime tower).
|
||||
//
|
||||
// Kept in its own translation unit so the belt-purge logic stays out of the way
|
||||
// of unrelated upstream changes to Print.cpp / PrintObjectSlice.cpp and carries
|
||||
// no regression risk for normal printers: none of these methods do anything
|
||||
// unless the print is a belt printer with the belt purge tower enabled.
|
||||
//
|
||||
// Print::has_belt_purge_tower() - is the belt purge tower active?
|
||||
// Print::_align_belt_purge_layers() - snap the prism's layer grid onto the
|
||||
// printed objects' grid
|
||||
// Print::_plan_belt_purge() - route filament-change purging into the
|
||||
// prism (flush-into-objects), no wipe tower
|
||||
// PrintObject::belt_shift_layer_grid() - shift a sliced layer grid
|
||||
// PrintObject::belt_truncate_layers_above() - cancel the prism past the last swap
|
||||
//
|
||||
// (Declarations live in Print.hpp alongside the rest of the Print interface.)
|
||||
|
||||
#include "Print.hpp"
|
||||
#include "PrintConfig.hpp"
|
||||
#include "Exception.hpp"
|
||||
#include "GCode/ToolOrdering.hpp"
|
||||
#include "Layer.hpp"
|
||||
#include "ExtrusionEntity.hpp"
|
||||
#include "ExtrusionEntityCollection.hpp"
|
||||
#include "I18N.hpp"
|
||||
#include "format.hpp"
|
||||
#include "LocalesUtils.hpp"
|
||||
#include "libslic3r.h"
|
||||
#include "BeltBrim.hpp"
|
||||
#include "PrintBase.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <cassert>
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// Belt purge prism: purging after filament changes is routed into a sliced
|
||||
// prism object via the flush-into-objects machinery instead of a wipe tower.
|
||||
bool Print::has_belt_purge_tower() const
|
||||
{
|
||||
// Its own purge-tower "type", gated by the belt-only enable_belt_purge_tower
|
||||
// option (not the classic enable_prime_tower).
|
||||
if (!(m_config.belt_printer.value
|
||||
&& m_config.enable_belt_purge_tower.value
|
||||
&& !m_config.spiral_mode.value
|
||||
&& m_config.filament_diameter.values.size() > 1))
|
||||
return false;
|
||||
|
||||
return std::any_of(m_objects.begin(), m_objects.end(), [](const PrintObject *object) {
|
||||
return object->config().belt_purge_tower_object.value;
|
||||
});
|
||||
}
|
||||
|
||||
// Belt mode: align ALL objects on the plate (the printed objects AND the purge
|
||||
// prism) onto one common layer grid, so the prism can absorb every toolchange.
|
||||
//
|
||||
// After belt slicing each object's layer print_z carries a per-object global z
|
||||
// offset (mesh-vertex-scan belt_z_shift + instance-Y-dependent terms), so
|
||||
// objects at different belt-Y positions get layer grids with DIFFERENT residues
|
||||
// (mod layer height). Purge marking looks absorbers up with
|
||||
// get_layer_at_printz(lt.print_z, EPSILON), so a toolchange on object B only
|
||||
// absorbs into the prism if the prism has a layer at B's print_z. Snapping only
|
||||
// the prism to one object therefore worked for a single (assembled) multi-color
|
||||
// object but failed with multiple separate objects — the prism could follow only
|
||||
// one grid, and toolchanges on the others went unabsorbed ("multiple layer
|
||||
// grids" warning).
|
||||
//
|
||||
// Fix: pick one reference grid (the tallest object) and shift every object onto
|
||||
// it. Each shift is at most half a layer height — a sub-100µm move along the
|
||||
// belt, the very same mechanism the per-object global_z_offset already uses, and
|
||||
// it keeps each object internally consistent (belt_shift_layer_grid moves the
|
||||
// object's layers, its support layers, and its belt floor together). Equal layer
|
||||
// height across objects is enforced by Print::validate(), so once residues match
|
||||
// every object steps on the same lattice {ref_offset + k*h} and every toolchange
|
||||
// layer coincides with a prism layer.
|
||||
void Print::_align_belt_purge_layers()
|
||||
{
|
||||
PrintObject *prism = nullptr;
|
||||
for (PrintObject *po : m_objects)
|
||||
if (po->config().belt_purge_tower_object.value && !po->layers().empty()) {
|
||||
prism = po;
|
||||
break;
|
||||
}
|
||||
if (prism == nullptr || prism->layers().empty())
|
||||
return;
|
||||
|
||||
const double h = prism->config().layer_height.value;
|
||||
if (h <= EPSILON)
|
||||
return;
|
||||
|
||||
// Grid residue of an object's layer grid: identical for all of an object's
|
||||
// layers above the first since they step by h.
|
||||
auto grid_offset = [h](const PrintObject *po) -> double {
|
||||
if (po->layers().empty())
|
||||
return 0.;
|
||||
const double z = po->layers().front()->print_z;
|
||||
return z - std::floor(z / h) * h; // in [0, h)
|
||||
};
|
||||
|
||||
// Reference grid: the tallest non-prism object (proxy for the object with
|
||||
// the most toolchange layers — minimizes how far the rest must move).
|
||||
const PrintObject *ref = nullptr;
|
||||
double ref_top = -std::numeric_limits<double>::max();
|
||||
for (const PrintObject *po : m_objects) {
|
||||
if (po->config().belt_purge_tower_object.value || po->layers().empty())
|
||||
continue;
|
||||
const double top = po->layers().back()->print_z;
|
||||
if (top > ref_top) {
|
||||
ref_top = top;
|
||||
ref = po;
|
||||
}
|
||||
}
|
||||
if (ref == nullptr)
|
||||
return;
|
||||
|
||||
const double ref_offset = grid_offset(ref);
|
||||
|
||||
// Snap every object (printed objects AND the prism) onto the reference grid.
|
||||
for (PrintObject *po : m_objects) {
|
||||
if (po->layers().empty())
|
||||
continue;
|
||||
double delta = ref_offset - grid_offset(po);
|
||||
if (delta > 0.5 * h)
|
||||
delta -= h;
|
||||
else if (delta <= -0.5 * h)
|
||||
delta += h;
|
||||
po->belt_shift_layer_grid(delta); // no-op for the reference object (delta ~ 0)
|
||||
}
|
||||
}
|
||||
|
||||
// Belt mode replacement for _make_wipe_tower(): plan filament-change purging
|
||||
// into the belt purge prism (and any other flush_into_* object) using the
|
||||
// flush-into-objects machinery, without generating classic wipe tower G-code.
|
||||
// The toolchange itself is emitted by GCode::set_extruder() via the
|
||||
// change_filament_gcode macro; the overrides marked here make the new
|
||||
// filament's first extrusions land in the purge prism.
|
||||
void Print::_plan_belt_purge()
|
||||
{
|
||||
m_wipe_tower_data.clear();
|
||||
|
||||
// psWipeTower may be invalidated without posSlice (for example after a
|
||||
// filament-map or tool-ordering change). Restore a prism shortened by the
|
||||
// previous plan so a newly higher toolchange can use its original layers.
|
||||
for (PrintObject *po : m_objects)
|
||||
if (po->config().belt_purge_tower_object.value)
|
||||
po->belt_undo_purge_plan();
|
||||
|
||||
// Must run before ToolOrdering is built: LayerTools merge per-object layer
|
||||
// print_z values, and the prism only absorbs purge where its (snapped)
|
||||
// layers coincide with the toolchange layers.
|
||||
this->_align_belt_purge_layers();
|
||||
|
||||
const unsigned int number_of_extruders = (unsigned int) m_config.filament_colour.values.size();
|
||||
|
||||
// No initial priming extrusions: there is no tower to prime on.
|
||||
m_wipe_tower_data.tool_ordering = ToolOrdering(*this, (unsigned int) -1, false);
|
||||
m_wipe_tower_data.tool_ordering.sort_and_build_data(*this, (unsigned int) -1, false);
|
||||
|
||||
if (m_wipe_tower_data.tool_ordering.empty() || m_wipe_tower_data.tool_ordering.last_extruder() == unsigned(-1))
|
||||
throw Slic3r::SlicingError("The print is empty. The model is not printable with current print settings.");
|
||||
|
||||
// Is there any filament change at all? Not ToolOrdering::has_wipe_tower(): that reads the
|
||||
// FIRST layer's flag, and on a belt the first layer may be a brim apron band, which carries
|
||||
// neither object nor support and so never gets the flag even when the print changes filament.
|
||||
{
|
||||
bool any_change = false;
|
||||
unsigned int cur = m_wipe_tower_data.tool_ordering.first_extruder();
|
||||
for (const auto < : m_wipe_tower_data.tool_ordering.layer_tools())
|
||||
for (const unsigned int e : lt.extruders)
|
||||
if (e != cur) { any_change = true; cur = e; }
|
||||
if (! any_change)
|
||||
return;
|
||||
}
|
||||
|
||||
this->throw_if_canceled();
|
||||
|
||||
// Flush volumes per filament pair, mirroring the generic wipe tower path:
|
||||
// full flush matrix for single extruder multi material with purging enabled,
|
||||
// plain prime volume otherwise.
|
||||
std::vector<float> flush_matrix(cast<float>(
|
||||
get_flush_volumes_matrix(m_config.flush_volumes_matrix.values, 0, m_config.nozzle_diameter.values.size())));
|
||||
std::vector<std::vector<float>> wipe_volumes;
|
||||
for (unsigned int i = 0; i < number_of_extruders; ++i)
|
||||
wipe_volumes.push_back(std::vector<float>(flush_matrix.begin() + i * number_of_extruders,
|
||||
flush_matrix.begin() + (i + 1) * number_of_extruders));
|
||||
const bool use_flush_matrix = m_config.purge_in_prime_tower && m_config.single_extruder_multi_material;
|
||||
const float flush_multiplier = (float) m_config.flush_multiplier.get_at(0);
|
||||
|
||||
// Cancel the purge prism early: pre-scan the tool ordering for the highest
|
||||
// print_z that actually has a toolchange, then drop the prism's layers above
|
||||
// it so the tower stops at the last color swap (saves filament/time). This
|
||||
// MUST happen before the marking loop below: ensure_perimeters_infills_order
|
||||
// force-overrides the prism's extrusions on every layer (it is a dedicated
|
||||
// flush object), so truncating afterwards would leave dangling overrides
|
||||
// pointing into deleted layers.
|
||||
{
|
||||
// The tool ordering covers the WHOLE print, and the prism is a printed
|
||||
// object in it. Left unbounded, the scan below sees the prism's own
|
||||
// toolchanges on layers above every model object -- the prism runs past
|
||||
// them by design (ramp/height compensation at the tilted ends) -- so
|
||||
// last_tc_z lands at the prism's own top and the truncation cancels
|
||||
// nothing. The tower ends up justifying its own existence.
|
||||
//
|
||||
// Nothing above the tallest printed object can require a color change,
|
||||
// so bound the scan there. On MCTEST5 that is 197 toolchanges spanning
|
||||
// z=154.00..193.20 with the tallest object topping out at 153.80, i.e.
|
||||
// 39.4 mm of tower that no swap ever needed.
|
||||
// Support layers count too: on a belt they can extend above the object's
|
||||
// own top, and a toolchange there is a real one.
|
||||
double obj_top_z = -1.;
|
||||
for (const PrintObject *po : m_objects) {
|
||||
if (po->config().belt_purge_tower_object.value)
|
||||
continue;
|
||||
if (!po->layers().empty())
|
||||
obj_top_z = std::max(obj_top_z, po->layers().back()->print_z);
|
||||
if (!po->support_layers().empty())
|
||||
obj_top_z = std::max(obj_top_z, po->support_layers().back()->print_z);
|
||||
}
|
||||
|
||||
double last_tc_z = -1.;
|
||||
unsigned int cur_ext = m_wipe_tower_data.tool_ordering.first_extruder();
|
||||
for (const auto < : m_wipe_tower_data.tool_ordering.layer_tools()) {
|
||||
// layer_tools() is ordered by print_z ascending.
|
||||
if (obj_top_z >= 0. && lt.print_z > obj_top_z + EPSILON)
|
||||
break;
|
||||
for (const unsigned int e : lt.extruders)
|
||||
if (e != cur_ext) { last_tc_z = lt.print_z; cur_ext = e; }
|
||||
}
|
||||
// Deliberately NOT cancelling the prism outright when no object toolchange
|
||||
// exists: belt_truncate_layers_above(0.) empties m_layers, and an object
|
||||
// with zero layers is not something the rest of the pipeline expects. The
|
||||
// GUI already declines to create a prism unless more than one filament is
|
||||
// in use, so this case is a stale prism, not a hot path -- leave it whole
|
||||
// rather than risk a zero-layer object.
|
||||
if (last_tc_z >= 0.)
|
||||
for (PrintObject *po : m_objects)
|
||||
if (po->config().belt_purge_tower_object.value && !po->layers().empty()) {
|
||||
po->belt_truncate_layers_above(last_tc_z);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// The prism only absorbs purge at toolchange layers whose print_z coincides
|
||||
// with one of its own layers.
|
||||
PrintObject *prism_po = nullptr;
|
||||
for (PrintObject *po : m_objects)
|
||||
if (po->config().belt_purge_tower_object.value && !po->layers().empty()) { prism_po = po; break; }
|
||||
|
||||
float total_leftover = 0.f;
|
||||
float worst_layer_leftover = 0.f;
|
||||
double worst_layer_z = 0.;
|
||||
|
||||
unsigned int current_extruder_id = m_wipe_tower_data.tool_ordering.first_extruder();
|
||||
for (auto &layer_tools : m_wipe_tower_data.tool_ordering.layer_tools()) {
|
||||
float layer_leftover = 0.f;
|
||||
for (const unsigned int extruder_id : layer_tools.extruders) {
|
||||
if (extruder_id == current_extruder_id)
|
||||
continue;
|
||||
float volume_to_wipe = use_flush_matrix ?
|
||||
wipe_volumes[current_extruder_id][extruder_id] * flush_multiplier :
|
||||
(float) m_config.prime_volume;
|
||||
float leftover = layer_tools.wiping_extrusions().mark_wiping_extrusions(*this, current_extruder_id, extruder_id,
|
||||
volume_to_wipe);
|
||||
layer_leftover += leftover;
|
||||
current_extruder_id = extruder_id;
|
||||
}
|
||||
|
||||
// Plastic saving: drop the prism's fills that no toolchange on this layer
|
||||
// claimed. At this point the prism's OVERRIDDEN fills are exactly the
|
||||
// purge; the rest would print as solid infill in the prism's own filament
|
||||
// for nothing -- which is the whole prism on a layer with no toolchange
|
||||
// (141 of 692 layers on MCTEST5 before the truncation fix). Perimeters are
|
||||
// left alone so the bar keeps a continuous wall along the belt.
|
||||
//
|
||||
// Non-destructive: the entities are stashed with their positions and put
|
||||
// back by belt_restore_dropped_fills() at the top of the next plan. An
|
||||
// earlier version deleted them outright, which broke replanning when a
|
||||
// later tool ordering needed what this one had not claimed -- that is why
|
||||
// it was removed rather than kept.
|
||||
if (prism_po != nullptr) {
|
||||
const auto &we = layer_tools.wiping_extrusions();
|
||||
prism_po->belt_drop_unclaimed_fills(
|
||||
prism_po->get_layer_at_printz(layer_tools.print_z, EPSILON),
|
||||
[&we, prism_po](const ExtrusionEntity *e) { return we.is_entity_overridden(e, prism_po, 0); });
|
||||
}
|
||||
|
||||
layer_tools.wiping_extrusions().ensure_perimeters_infills_order(*this);
|
||||
if (layer_leftover > 0.f) {
|
||||
total_leftover += layer_leftover;
|
||||
if (layer_leftover > worst_layer_leftover) {
|
||||
worst_layer_leftover = layer_leftover;
|
||||
worst_layer_z = layer_tools.print_z;
|
||||
}
|
||||
}
|
||||
this->throw_if_canceled();
|
||||
}
|
||||
|
||||
if (total_leftover > 1.f) {
|
||||
this->active_step_add_warning(
|
||||
PrintStateBase::WarningLevel::CRITICAL,
|
||||
Slic3r::format(_u8L("The belt purge tower cannot absorb the full purge volume: %1% mm³ in total could not "
|
||||
"be purged (worst layer: %2% mm³ at height %3%). The print may show color bleeding. "
|
||||
"Increase the belt purge tower width, or reduce flushing volumes."),
|
||||
int(std::ceil(total_leftover)), int(std::ceil(worst_layer_leftover)),
|
||||
Slic3r::float_to_string_decimal_point(worst_layer_z, 2)));
|
||||
}
|
||||
}
|
||||
|
||||
// Belt mode: shift the sliced layer grid by delta. Mirrors the global_z_offset
|
||||
// application in slice() — layer print_z and belt_floor_z_shift move together
|
||||
// so belt floor clipping stays consistent with the shifted grid. Used by
|
||||
// Print::_align_belt_purge_layers() to snap the purge prism onto the printed
|
||||
// objects' layer grid; |delta| <= half a layer height, i.e. a sub-layer shift
|
||||
// of the prism along the belt.
|
||||
void PrintObject::belt_shift_layer_grid(double delta)
|
||||
{
|
||||
if (std::abs(delta) < EPSILON)
|
||||
return;
|
||||
for (Layer *layer : m_layers)
|
||||
layer->print_z += delta;
|
||||
for (SupportLayer *layer : m_support_layers)
|
||||
layer->print_z += delta;
|
||||
// The brim's apron bands below the first layer carry their own print_z.
|
||||
for (BeltBrimBand &band : m_belt_brim_prologue)
|
||||
band.print_z += delta;
|
||||
m_slicing_params.belt_floor_z_shift += delta;
|
||||
// The grid stays shifted across a support-only or brim-only change (posSlice does
|
||||
// not rerun), so everything slice() derived from it has to follow: the cached floor
|
||||
// that update_slicing_parameters() restores, and the global offset the organic
|
||||
// support layers and the adaptive infill octree are placed with. Left alone, the
|
||||
// next alignment finds a delta of 0 and the floor and the supports sit up to half
|
||||
// a layer off the grid, unlike a fresh slice.
|
||||
if (m_belt_floor_z_shift_cache_valid)
|
||||
m_belt_floor_z_shift_cached += delta;
|
||||
m_belt_global_z_offset += delta;
|
||||
}
|
||||
|
||||
// Belt mode: drop layers strictly above z (used to cancel the purge prism early
|
||||
// once there are no more toolchanges above z, so the tower stops at the last
|
||||
// color swap instead of wasting filament up the rest of the belt). Each layer's
|
||||
// cross-section is already sliced, so removing upper layers does not affect the
|
||||
// last toolchange's coverage. Deletes the Layer objects and clears the new top
|
||||
// layer's upper-layer link. Returns the number of layers removed.
|
||||
size_t PrintObject::belt_truncate_layers_above(coordf_t z)
|
||||
{
|
||||
// A repeated plan always starts from the restored full layer set.
|
||||
assert(m_belt_truncated_layers.empty());
|
||||
size_t keep = m_layers.size();
|
||||
while (keep > 0 && m_layers[keep - 1]->print_z > z + EPSILON)
|
||||
--keep;
|
||||
if (keep >= m_layers.size())
|
||||
return 0;
|
||||
const size_t removed = m_layers.size() - keep;
|
||||
m_belt_truncated_layers.assign(m_layers.begin() + keep, m_layers.end());
|
||||
m_layers.resize(keep);
|
||||
if (!m_layers.empty())
|
||||
m_layers.back()->upper_layer = nullptr;
|
||||
return removed;
|
||||
}
|
||||
|
||||
// Plastic saving on the purge prism: keep only the fills a toolchange claimed.
|
||||
//
|
||||
// Called per layer from _plan_belt_purge(), after the real-purge marking and
|
||||
// BEFORE ensure_perimeters_infills_order() -- that pass force-overrides every
|
||||
// remaining fill on the prism (it is a dedicated flush object), so afterwards
|
||||
// everything looks claimed and nothing could be distinguished.
|
||||
size_t PrintObject::belt_drop_unclaimed_fills(Layer *layer, const std::function<bool(const ExtrusionEntity*)> &claimed)
|
||||
{
|
||||
if (layer == nullptr)
|
||||
return 0;
|
||||
size_t dropped = 0;
|
||||
for (size_t ri = 0; ri < layer->regions().size(); ++ri) {
|
||||
LayerRegion *lr = layer->get_region(ri);
|
||||
auto &ents = lr->fills.entities;
|
||||
ExtrusionEntitiesPtr keep;
|
||||
keep.reserve(ents.size());
|
||||
for (size_t i = 0; i < ents.size(); ++i) {
|
||||
if (claimed(ents[i])) {
|
||||
keep.emplace_back(ents[i]);
|
||||
} else {
|
||||
// Stash with its original index so the restore is exact.
|
||||
m_belt_dropped_fills.push_back(BeltDroppedFill{ layer, ri, i, ents[i] });
|
||||
++dropped;
|
||||
}
|
||||
}
|
||||
ents = std::move(keep);
|
||||
}
|
||||
return dropped;
|
||||
}
|
||||
|
||||
void PrintObject::belt_restore_dropped_fills()
|
||||
{
|
||||
if (m_belt_dropped_fills.empty())
|
||||
return;
|
||||
// Ascending index per (layer, region): inserting in that order lands every
|
||||
// entity back at its original position, because each insertion shifts only
|
||||
// the entries after it, which are themselves still to be inserted.
|
||||
std::stable_sort(m_belt_dropped_fills.begin(), m_belt_dropped_fills.end(),
|
||||
[](const BeltDroppedFill &a, const BeltDroppedFill &b) {
|
||||
if (a.layer != b.layer) return a.layer < b.layer;
|
||||
if (a.region_idx != b.region_idx) return a.region_idx < b.region_idx;
|
||||
return a.index < b.index;
|
||||
});
|
||||
for (const BeltDroppedFill &d : m_belt_dropped_fills) {
|
||||
auto &ents = d.layer->get_region(d.region_idx)->fills.entities;
|
||||
ents.insert(ents.begin() + std::min(d.index, ents.size()), d.entity);
|
||||
}
|
||||
m_belt_dropped_fills.clear();
|
||||
}
|
||||
|
||||
void PrintObject::belt_restore_truncated_layers()
|
||||
{
|
||||
if (m_belt_truncated_layers.empty())
|
||||
return;
|
||||
|
||||
m_layers.insert(m_layers.end(), m_belt_truncated_layers.begin(), m_belt_truncated_layers.end());
|
||||
m_belt_truncated_layers.clear();
|
||||
for (size_t i = 0; i < m_layers.size(); ++i) {
|
||||
m_layers[i]->lower_layer = i == 0 ? nullptr : m_layers[i - 1];
|
||||
m_layers[i]->upper_layer = i + 1 < m_layers.size() ? m_layers[i + 1] : nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,82 @@
|
||||
#include "BeltSliceStrategy.hpp"
|
||||
#include "Model.hpp"
|
||||
#include "BeltTransform.hpp"
|
||||
#include "Point.hpp"
|
||||
#include "PrintConfig.hpp"
|
||||
|
||||
#include <limits>
|
||||
#include <algorithm>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
void BeltSliceStrategy::apply_preslice_transforms(Transform3d &trafo,
|
||||
const PrintConfig &config,
|
||||
const ModelVolumePtrs &model_volumes,
|
||||
double *out_belt_min_z)
|
||||
{
|
||||
// 1. Belt rotation — the sole mesh-side belt transform (matching
|
||||
// BeltTransformPipeline::build_forward_transform). Only active in
|
||||
// belt-printer mode.
|
||||
bool has_rotation = false;
|
||||
if (config.belt_printer.value) {
|
||||
const Matrix3d rot = BeltTransformPipeline::build_rotation_matrix(config, &has_rotation);
|
||||
if (has_rotation) {
|
||||
Transform3d belt_xform = Transform3d::Identity();
|
||||
belt_xform.linear() = rot;
|
||||
trafo = belt_xform * trafo;
|
||||
}
|
||||
}
|
||||
|
||||
if (!has_rotation)
|
||||
return;
|
||||
|
||||
// 2. Z-shift — detect if the mesh clips below the build plate after the
|
||||
// transforms and lift it. Each mesh vertex must be brought into object space
|
||||
// via mv->get_matrix() before applying the full trafo (which is in object
|
||||
// space). Missing this on assemblies (where per-volume get_matrix() positions
|
||||
// each volume within the object) would compute min_z against mesh-local vertex
|
||||
// coordinates rather than object-space coordinates, so volumes translated along
|
||||
// the slicer's Z axis would be silently excluded from the bound check.
|
||||
|
||||
//
|
||||
// The lift is measured to the lowest point of the SUPPORT region, not of the
|
||||
// mesh: the belt floor (z = shear * u in this rotated frame, u the from-axis
|
||||
// coordinate) runs below every vertex, and under the leading end of an
|
||||
// overhang it lies below the lowest vertex by up to the overhang's length
|
||||
// times the shear. Supports have to reach that floor, and every support
|
||||
// generator works in layers at z >= 0, so z = 0 has to be the lowest floor
|
||||
// point under the footprint. The layers between it and the first vertex
|
||||
// come out empty, which belt slicing already tolerates (the bottom corner
|
||||
// of a tilted part is a point). Vertices on the belt have z == floor, so
|
||||
// for a part resting on the belt this is simply the floor at its leading
|
||||
// extreme, less the frame margin (see BeltTransformPipeline::frame_margin).
|
||||
BeltTransformPipeline::BeltFloorParams floor;
|
||||
const bool has_floor = BeltTransformPipeline::floor_shear(config, floor);
|
||||
double min_z = std::numeric_limits<double>::max();
|
||||
for (const ModelVolume *mv : model_volumes) {
|
||||
if (!mv->is_model_part()) continue;
|
||||
Transform3d vol_trafo = trafo * mv->get_matrix();
|
||||
const auto &its = mv->mesh().its;
|
||||
for (const stl_vertex &v : its.vertices) {
|
||||
Vec3d vm = v.cast<double>();
|
||||
Vec3d pt = vol_trafo * vm;
|
||||
min_z = std::min(min_z, pt.z());
|
||||
if (has_floor)
|
||||
min_z = std::min(min_z, floor.shear_factor * (floor.from_axis == 0 ? pt.x() : pt.y()));
|
||||
}
|
||||
}
|
||||
if (has_floor && min_z != std::numeric_limits<double>::max())
|
||||
min_z -= BeltTransformPipeline::frame_margin(floor);
|
||||
const double z_shift_val = (min_z < 0. && min_z != std::numeric_limits<double>::max()) ? -min_z : 0.;
|
||||
if (z_shift_val > 0.) {
|
||||
Transform3d z_shift = Transform3d::Identity();
|
||||
z_shift.matrix()(2, 3) = z_shift_val;
|
||||
trafo = z_shift * trafo;
|
||||
}
|
||||
// out_belt_min_z is only meaningful in belt mode.
|
||||
if (out_belt_min_z && config.belt_printer.value) {
|
||||
*out_belt_min_z = (min_z != std::numeric_limits<double>::max()) ? min_z : 0.;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
|
||||
#include "libslic3r.h"
|
||||
#include "Point.hpp"
|
||||
#include "BeltTransform.hpp"
|
||||
#include "PrintConfig.hpp"
|
||||
#include "Model.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// Belt printer pre-slice transform strategy.
|
||||
//
|
||||
// Composes, in order, the mesh transforms applied before slicing on a belt printer:
|
||||
// 1. Belt rotation (the sole mesh-side belt transform; shear & scale are a
|
||||
// g-code-side stage, see MachineFrameTransform)
|
||||
// 2. Per-object Z-shift that lifts the mesh so its slicing frame starts at the
|
||||
// belt below its footprint
|
||||
//
|
||||
// Isolates this belt-specific logic from the generic slicing pipeline in
|
||||
// PrintObjectSlice.cpp.
|
||||
class BeltSliceStrategy
|
||||
{
|
||||
public:
|
||||
// Apply the belt rotation + Z-shift to `trafo` in place. No-op when no belt
|
||||
// rotation is configured.
|
||||
//
|
||||
// out_belt_min_z (if non-null) receives the minimum mesh Z after the transforms.
|
||||
static void apply_preslice_transforms(Transform3d &trafo,
|
||||
const PrintConfig &config,
|
||||
const ModelVolumePtrs &model_volumes,
|
||||
double *out_belt_min_z = nullptr);
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,153 @@
|
||||
#include "BeltTransform.hpp"
|
||||
#include "Model.hpp"
|
||||
#include "BoundingBox.hpp"
|
||||
#include "Config.hpp"
|
||||
#include "Geometry.hpp"
|
||||
#include "Point.hpp"
|
||||
#include "PrintConfig.hpp"
|
||||
#include "libslic3r.h"
|
||||
|
||||
#include <limits>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// ---- Matrix builders ------------------------------------------------------
|
||||
|
||||
Matrix3d BeltTransformPipeline::build_rotation_matrix(const PrintConfig &config, bool *has_rot_out)
|
||||
{
|
||||
BeltRotationAxis axis = config.belt_slice_rotation.value;
|
||||
double angle_deg = config.belt_slice_rotation_angle.value;
|
||||
bool active = axis != BeltRotationAxis::None && std::abs(angle_deg) > EPSILON;
|
||||
if (has_rot_out) *has_rot_out = active;
|
||||
if (!active)
|
||||
return Matrix3d::Identity();
|
||||
double angle_rad = Geometry::deg2rad(angle_deg);
|
||||
Vec3d unit_axis;
|
||||
switch (axis) {
|
||||
case BeltRotationAxis::X: unit_axis = Vec3d::UnitX(); break;
|
||||
case BeltRotationAxis::Y: unit_axis = Vec3d::UnitY(); break;
|
||||
case BeltRotationAxis::Z: unit_axis = Vec3d::UnitZ(); break;
|
||||
default: return Matrix3d::Identity();
|
||||
}
|
||||
return Eigen::AngleAxisd(angle_rad, unit_axis).toRotationMatrix();
|
||||
}
|
||||
|
||||
Transform3d BeltTransformPipeline::build_forward_transform(const PrintConfig &config)
|
||||
{
|
||||
// Mesh-side belt transform: the rotation. (Shear & scale are a g-code-side
|
||||
// stage, not part of the mesh transform.)
|
||||
Transform3d combined = Transform3d::Identity();
|
||||
combined.linear() = build_rotation_matrix(config);
|
||||
return combined;
|
||||
}
|
||||
|
||||
// ---- Belt floor parameters ------------------------------------------------
|
||||
|
||||
namespace {
|
||||
|
||||
// Belt floor in the rotated slicer frame: the image of z_machine = 0 under R.
|
||||
// R(+α, X): point (·, y, 0) → (·, cos α · y, sin α · y) ⇒ z = tan(α) · y_s
|
||||
// R(+α, Y): point (x, ·, 0) → (cos α · x, ·, -sin α · x) ⇒ z = -tan(α) · x_s
|
||||
// R(+α, Z): point (·, ·, 0) → (·, ·, 0); no tilt → no floor
|
||||
void belt_floor_shear(BeltRotationAxis rot_axis, double angle_rad, BeltTransformPipeline::BeltFloorParams &out)
|
||||
{
|
||||
double sin_a = std::sin(angle_rad), cos_a = std::cos(angle_rad);
|
||||
switch (rot_axis) {
|
||||
case BeltRotationAxis::X:
|
||||
out.shear_factor = (std::abs(cos_a) > EPSILON) ? sin_a / cos_a : 0.;
|
||||
out.from_axis = 1; // Y
|
||||
break;
|
||||
case BeltRotationAxis::Y:
|
||||
out.shear_factor = (std::abs(cos_a) > EPSILON) ? -sin_a / cos_a : 0.;
|
||||
out.from_axis = 0; // X
|
||||
break;
|
||||
case BeltRotationAxis::Z:
|
||||
default:
|
||||
out.shear_factor = 0.0;
|
||||
out.from_axis = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Z of the belt floor directly under a point of the rotated (unshifted) frame.
|
||||
inline double belt_floor_z(const BeltTransformPipeline::BeltFloorParams &fp, const Vec3d &pt)
|
||||
{
|
||||
return fp.shear_factor * (fp.from_axis == 0 ? pt.x() : pt.y());
|
||||
}
|
||||
|
||||
|
||||
BeltTransformPipeline::BeltHeightResult compute_belt_height_and_floor_impl(
|
||||
const PrintConfig &config, const BoundingBoxf3 &bb, double original_height)
|
||||
{
|
||||
BeltTransformPipeline::BeltHeightResult result;
|
||||
result.object_height = original_height;
|
||||
|
||||
// The mesh rotation (the sole mesh-side belt transform).
|
||||
const BeltRotationAxis rot_axis = config.belt_slice_rotation.value;
|
||||
const double rot_angle = config.belt_slice_rotation_angle.value;
|
||||
|
||||
bool has_rotation = rot_axis != BeltRotationAxis::None && std::abs(rot_angle) > EPSILON;
|
||||
if (!has_rotation)
|
||||
return result;
|
||||
|
||||
// Rotation path: sweep the 8 bbox corners through R to get the rotated height,
|
||||
// then derive the belt floor (the image of machine-Z = 0 under R).
|
||||
double angle_rad = Geometry::deg2rad(rot_angle);
|
||||
Vec3d unit_axis;
|
||||
switch (rot_axis) {
|
||||
case BeltRotationAxis::X: unit_axis = Vec3d::UnitX(); break;
|
||||
case BeltRotationAxis::Y: unit_axis = Vec3d::UnitY(); break;
|
||||
case BeltRotationAxis::Z: unit_axis = Vec3d::UnitZ(); break;
|
||||
default: unit_axis = Vec3d::UnitX(); break;
|
||||
}
|
||||
Matrix3d R = Eigen::AngleAxisd(angle_rad, unit_axis).toRotationMatrix();
|
||||
belt_floor_shear(rot_axis, angle_rad, result.floor_params);
|
||||
// The slicing frame starts at the lowest point of the support region: the
|
||||
// lowest belt-floor point under the footprint, not the lowest vertex. The
|
||||
// belt under the leading end of an overhang lies below every vertex of the
|
||||
// part, and supports have to be able to reach it (see
|
||||
// BeltSliceStrategy::apply_preslice_transforms for the exact vertex-scan
|
||||
// counterpart of this bbox estimate).
|
||||
double min_rz = std::numeric_limits<double>::max();
|
||||
double max_rz = std::numeric_limits<double>::lowest();
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
Vec3d c((i & 1) ? bb.max.x() : bb.min.x(),
|
||||
(i & 2) ? bb.max.y() : bb.min.y(),
|
||||
(i & 4) ? bb.max.z() : bb.min.z());
|
||||
Vec3d rc = R * c;
|
||||
double z = rc.z();
|
||||
min_rz = std::min(min_rz, z);
|
||||
max_rz = std::max(max_rz, z);
|
||||
min_rz = std::min(min_rz, belt_floor_z(result.floor_params, rc));
|
||||
}
|
||||
min_rz -= BeltTransformPipeline::frame_margin(result.floor_params);
|
||||
result.object_height = max_rz - min_rz;
|
||||
|
||||
result.floor_params.z_shift = bb.min.z() + ((min_rz < 0.) ? -min_rz : 0.);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
BeltTransformPipeline::BeltHeightResult BeltTransformPipeline::compute_belt_height_and_floor(
|
||||
const PrintConfig &config, const BoundingBoxf3 &bbox, double original_height)
|
||||
{
|
||||
return compute_belt_height_and_floor_impl(config, bbox, original_height);
|
||||
}
|
||||
|
||||
bool BeltTransformPipeline::floor_shear(const PrintConfig &config, BeltFloorParams &out)
|
||||
{
|
||||
out = BeltFloorParams{};
|
||||
const BeltRotationAxis rot_axis = config.belt_slice_rotation.value;
|
||||
const double rot_angle = config.belt_slice_rotation_angle.value;
|
||||
if (rot_axis == BeltRotationAxis::None || std::abs(rot_angle) <= EPSILON)
|
||||
return false;
|
||||
belt_floor_shear(rot_axis, Geometry::deg2rad(rot_angle), out);
|
||||
return std::abs(out.shear_factor) > EPSILON;
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,132 @@
|
||||
#pragma once
|
||||
|
||||
#include "libslic3r.h"
|
||||
#include "Point.hpp"
|
||||
#include "BoundingBox.hpp"
|
||||
#include "PrintConfig.hpp"
|
||||
#include "Geometry.hpp"
|
||||
#include "Config.hpp"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
class ModelObject;
|
||||
|
||||
// Shared belt-printer transform math.
|
||||
//
|
||||
// The pre-slice pipeline applied in PrintObjectSlice.cpp is:
|
||||
// trafo_out = z_shift * rotation * trafo_in
|
||||
//
|
||||
// Rotation is the sole mesh-side belt transform; shear & scale are applied
|
||||
// to the g-code instead (see MachineFrameTransform). This class provides the
|
||||
// building blocks so every call site uses the same implementation. z_shift is
|
||||
// object-dependent (computed from mesh vertex bounds) and is NOT included in
|
||||
// build_forward_transform(). The machine-frame shear/scale is derived directly
|
||||
// from the tilt angle in MachineFrameTransform and no longer lives here.
|
||||
//
|
||||
// Design note: this mesh-rotation approach replaced an earlier pre-shear
|
||||
// method (now removed). While that initial pre-shear method was instrumental
|
||||
// in getting belt printer slicing off the ground in the first place, its place is
|
||||
// in the past. A big thank you goes to the Unlayered3D team, who recommended
|
||||
// switching to a pre-slice rotation stage instead. Doing so keeps the slicing
|
||||
// operation isometric — no distortion of the sliced geometry — while the
|
||||
// non-orthogonal machine-axis compensation is confined to a g-code-side shear/scale
|
||||
// derived from the same tilt angle.
|
||||
//
|
||||
// This fixed a number of issues, including several issues noticed by hotcubcar
|
||||
// regarding adaptive infills not working, gyroid becoming anisotropic, and more
|
||||
// that were all mostly resolved as a result of the switch.
|
||||
//
|
||||
// This also means that the pre-slice rotation transform methodology can be used
|
||||
// more cleanly on non-belt printers.
|
||||
// - HarrierPigeon (Joseph Robertson)
|
||||
|
||||
class BeltTransformPipeline
|
||||
{
|
||||
public:
|
||||
// ---- Identity checks --------------------------------------------------
|
||||
|
||||
// Whether the G-code axis remap applies at all. The remap fields are only
|
||||
// offered in the belt printer group, so a value left in a profile must not
|
||||
// change a non-belt print: with belt mode off every belt-only key is a no-op.
|
||||
// This is the one place to widen if a non-belt use ever needs them.
|
||||
static bool axis_remap_enabled(const PrintConfig &config) { return config.belt_printer.value; }
|
||||
|
||||
static bool has_rotation(const PrintConfig &config)
|
||||
{
|
||||
return config.belt_slice_rotation.value != BeltRotationAxis::None &&
|
||||
std::abs(config.belt_slice_rotation_angle.value) > EPSILON;
|
||||
}
|
||||
|
||||
// Physical belt tilt derived from the slicing rotation — the single source of
|
||||
// truth for bed rendering, support gravity tilt and the bed-exclusion
|
||||
// projection. Returns the tilt magnitude in degrees split onto the X and Y
|
||||
// build-plate tilt axes according to the rotation axis:
|
||||
// rotation about X → tilt_x = angle (gantry tilts in the YZ plane)
|
||||
// rotation about Y → tilt_y = angle (gantry tilts in the XZ plane)
|
||||
// rotation about Z / None → no tilt (in-plane spin doesn't tilt the belt)
|
||||
// The magnitude uses abs(angle) so a negative rotation still reports a positive
|
||||
// physical tilt.
|
||||
struct PhysicalTilt { double tilt_x_deg = 0.; double tilt_y_deg = 0.; };
|
||||
|
||||
static PhysicalTilt physical_tilt(BeltRotationAxis axis, double angle_deg)
|
||||
{
|
||||
PhysicalTilt t;
|
||||
double mag = std::abs(angle_deg);
|
||||
switch (axis) {
|
||||
case BeltRotationAxis::X: t.tilt_x_deg = mag; break;
|
||||
case BeltRotationAxis::Y: t.tilt_y_deg = mag; break;
|
||||
default: break; // Z / None: no physical tilt
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
// ---- Matrix builders --------------------------------------------------
|
||||
|
||||
// Build the 3x3 rotation matrix from belt_slice_rotation* config.
|
||||
// Returns Identity if rotation axis is None or angle is ~0.
|
||||
// Also sets has_rot_out if non-null.
|
||||
static Matrix3d build_rotation_matrix(const PrintConfig &config, bool *has_rot_out = nullptr);
|
||||
|
||||
// Forward transform (the rotation) — the mesh-side belt transform that
|
||||
// BeltSliceStrategy applies and BeltBackTransform inverts.
|
||||
// Does NOT include the per-object Z-shift.
|
||||
static Transform3d build_forward_transform(const PrintConfig &config);
|
||||
|
||||
// ---- Belt floor parameters --------------------------------------------
|
||||
|
||||
struct BeltFloorParams {
|
||||
double shear_factor = 0.0;
|
||||
int from_axis = 1;
|
||||
double z_shift = 0.0;
|
||||
};
|
||||
|
||||
// Shear factor and from-axis of the belt floor in the rotated slicer frame
|
||||
// (z_floor = shear_factor * u, u = the from-axis coordinate), for the
|
||||
// rotation the config selects. z_shift is left at 0. Returns false (and
|
||||
// zero shear) when the config has no tilt.
|
||||
static bool floor_shear(const PrintConfig &config, BeltFloorParams &out);
|
||||
|
||||
// How far below the lowest belt-floor point under the footprint the slicing
|
||||
// frame starts, in slicing Z. A support column meeting the belt is wider at
|
||||
// its base than at its tip, so under a leading overhang the base reaches ahead
|
||||
// of the part along the belt, and the layers that trim it to the belt plane
|
||||
// lie below that lowest point: 10 mm along the belt.
|
||||
static double frame_margin(const BeltFloorParams &fp) { return 10. * std::abs(fp.shear_factor); }
|
||||
|
||||
// Result of computing belt height + floor params.
|
||||
struct BeltHeightResult {
|
||||
double object_height; // Effective object height after shear/scale
|
||||
BeltFloorParams floor_params;
|
||||
};
|
||||
|
||||
// Compute effective object height and belt floor parameters from config
|
||||
// and the object's bounding box. original_height is the input height
|
||||
// (bb.size().z() or model_object.max_z()).
|
||||
static BeltHeightResult compute_belt_height_and_floor(
|
||||
const PrintConfig &config, const BoundingBoxf3 &bbox,
|
||||
double original_height);
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
+14
-1
@@ -474,7 +474,9 @@ static ExPolygons outer_inner_brim_area(const Print& print,
|
||||
const bool use_brim_ears = object->config().brim_type == btPainted;
|
||||
const bool use_inner_brim_ears = (use_auto_brim_ears || use_brim_ears) && !object->config().brim_ears_outer_only.value;
|
||||
const bool has_inner_brim = brim_type == btInnerOnly || brim_type == btOuterAndInner || use_inner_brim_ears;
|
||||
const bool has_outer_brim = brim_type == btOuterOnly || brim_type == btOuterAndInner || brim_type == btAutoBrim || use_auto_brim_ears || use_brim_ears;
|
||||
// btLeadingEdgeOnly is a belt-printer mode; on a flat bed there is no leading
|
||||
// edge, so it degrades to an ordinary outer brim rather than silently to none.
|
||||
const bool has_outer_brim = brim_type == btOuterOnly || brim_type == btOuterAndInner || brim_type == btAutoBrim || brim_type == btLeadingEdgeOnly || use_auto_brim_ears || use_brim_ears;
|
||||
coord_t ear_detection_length = scale_(object->config().brim_ears_detection_length.value);
|
||||
coordf_t brim_ears_max_angle = object->config().brim_ears_max_angle.value;
|
||||
//ORCA: Select brim base slices from EFC-compensated outline when enabled.
|
||||
@@ -889,6 +891,17 @@ void make_brim(const Print& print, PrintTryCancel try_cancel, Polygons& islands_
|
||||
std::vector<unsigned int>& printExtruders,
|
||||
std::map<ObjectInstanceID, ExPolygons>* objectBrimAreasByInstanceOut)
|
||||
{
|
||||
// Belt printers never use the flat plate brim.
|
||||
//
|
||||
// With a tilted belt the brim has to be laid onto the belt plane over many layers,
|
||||
// which BeltBrim.cpp does during posSupportMaterial. With an untilted belt this
|
||||
// could in principle fall through and produce an ordinary brim, but it would never
|
||||
// reach the G-code: the plate brim is emitted out of skirt_brim_groups(), which
|
||||
// _make_skirt() builds, and that returns early for every belt printer. Running the
|
||||
// generator anyway would just burn time on geometry nobody prints.
|
||||
if (print.config().belt_printer.value)
|
||||
return;
|
||||
|
||||
std::map<ObjectInstanceID, ExPolygons> brimAreaMap;
|
||||
Flow flow = print.brim_flow();
|
||||
ExPolygons islands_area_ex = outer_inner_brim_area(print,
|
||||
|
||||
@@ -84,7 +84,7 @@ public:
|
||||
indexed_triangle_set bounding_mesh(bool scale=true) const;
|
||||
|
||||
// Center of the print bed, unscaled.
|
||||
Vec2d bed_center() const { return to_2d(m_bboxf.center()); }
|
||||
Vec2d bed_center() const { return get_extents(m_bed_shape).center(); }
|
||||
// Convex hull of polygon(), scaled.
|
||||
const Polygon& convex_hull() const { return m_convex_hull; }
|
||||
// Smallest enclosing circle of polygon(), scaled.
|
||||
|
||||
@@ -84,7 +84,6 @@
|
||||
#include <cmath>
|
||||
#include <cctype>
|
||||
#include <cstdio>
|
||||
#include <functional>
|
||||
#include <gp_XY.hxx>
|
||||
#include <initializer_list>
|
||||
#include <math.h>
|
||||
|
||||
@@ -85,6 +85,15 @@ set(lisbslic3r_sources
|
||||
BoundingBox.hpp
|
||||
BridgeDetector.cpp
|
||||
BridgeDetector.hpp
|
||||
BeltBrim.cpp
|
||||
BeltBrim.hpp
|
||||
BeltGCode.cpp
|
||||
BeltGCode.hpp
|
||||
BeltPurge.cpp
|
||||
BeltSliceStrategy.cpp
|
||||
BeltSliceStrategy.hpp
|
||||
BeltTransform.cpp
|
||||
BeltTransform.hpp
|
||||
Brim.cpp
|
||||
BrimEarsPoint.hpp
|
||||
Brim.hpp
|
||||
@@ -103,6 +112,8 @@ set(lisbslic3r_sources
|
||||
CommonDefs.hpp
|
||||
Config.cpp
|
||||
Config.hpp
|
||||
ConnectedBodies.cpp
|
||||
ConnectedBodies.hpp
|
||||
ContourZ.cpp
|
||||
CustomGCode.cpp
|
||||
CustomGCode.hpp
|
||||
@@ -171,6 +182,8 @@ set(lisbslic3r_sources
|
||||
Fill/FillPlanePath.hpp
|
||||
Fill/FillRectilinear.cpp
|
||||
Fill/FillRectilinear.hpp
|
||||
Fill/FillTpmsAdaptive.cpp
|
||||
Fill/FillTpmsAdaptive.hpp
|
||||
Fill/FillTpmsD.cpp
|
||||
Fill/FillTpmsD.hpp
|
||||
Fill/FillTpmsFK.cpp
|
||||
@@ -232,6 +245,14 @@ set(lisbslic3r_sources
|
||||
GCode/AdaptivePAProcessor.hpp
|
||||
GCode/AvoidCrossingPerimeters.cpp
|
||||
GCode/AvoidCrossingPerimeters.hpp
|
||||
GCode/BeltBackTransform.cpp
|
||||
GCode/BeltBackTransform.hpp
|
||||
GCode/MachineFrameTransform.cpp
|
||||
GCode/MachineFrameTransform.hpp
|
||||
GCode/BeltKinematics.cpp
|
||||
GCode/BeltKinematics.hpp
|
||||
GCode/MachineKinematics.cpp
|
||||
GCode/MachineKinematics.hpp
|
||||
GCode/ConflictChecker.cpp
|
||||
GCode/ConflictChecker.hpp
|
||||
GCode/CoolingBuffer.cpp
|
||||
@@ -261,8 +282,6 @@ set(lisbslic3r_sources
|
||||
GCode/PreciseSeam.cpp
|
||||
GCode/PreciseSeam.hpp
|
||||
GCode/PreciseSeamInternal.hpp
|
||||
#GCodeSender.cpp
|
||||
#GCodeSender.hpp
|
||||
GCode/SmallAreaInfillFlowCompensator.cpp
|
||||
GCode/SmallAreaInfillFlowCompensator.hpp
|
||||
GCode/SpiralVase.cpp
|
||||
@@ -306,6 +325,12 @@ set(lisbslic3r_sources
|
||||
Geometry/VoronoiUtils.cpp
|
||||
Geometry/VoronoiUtils.hpp
|
||||
Geometry/VoronoiVisualUtils.hpp
|
||||
IMEXArrange.cpp
|
||||
IMEXArrange.hpp
|
||||
IMEXHelpers.cpp
|
||||
IMEXHelpers.hpp
|
||||
IMEXZones.cpp
|
||||
IMEXZones.hpp
|
||||
InstanceLock.cpp
|
||||
InstanceLock.hpp
|
||||
Int128.hpp
|
||||
@@ -380,6 +405,7 @@ set(lisbslic3r_sources
|
||||
Polyline.hpp
|
||||
PresetBundle.cpp
|
||||
PresetBundle.hpp
|
||||
PrinterBedAssets.hpp
|
||||
PresetCacheFormat.cpp
|
||||
PresetCacheFormat.hpp
|
||||
Preset.cpp
|
||||
@@ -447,7 +473,6 @@ set(lisbslic3r_sources
|
||||
SLA/SupportTreeBuildsteps.hpp
|
||||
SLA/SupportTree.cpp
|
||||
SLA/SupportTree.hpp
|
||||
#SLA/SupportTreeIGL.cpp
|
||||
SLA/SupportTreeMesher.cpp
|
||||
SLA/SupportTreeMesher.hpp
|
||||
SlicesToTriangleMesh.cpp
|
||||
@@ -456,6 +481,8 @@ set(lisbslic3r_sources
|
||||
SlicingAdaptive.hpp
|
||||
Slicing.cpp
|
||||
Slicing.hpp
|
||||
Support/BeltFloorContext.cpp
|
||||
Support/BeltFloorContext.hpp
|
||||
Support/SupportCommon.cpp
|
||||
Support/SupportCommon.hpp
|
||||
Support/SupportLayer.hpp
|
||||
@@ -598,6 +625,7 @@ add_library(libslic3r_cgal STATIC
|
||||
CutSurface.hpp CutSurface.cpp
|
||||
IntersectionPoints.hpp IntersectionPoints.cpp
|
||||
MeshBoolean.hpp MeshBoolean.cpp
|
||||
MeshRepair.hpp MeshRepair.cpp
|
||||
TryCatchSignal.hpp TryCatchSignal.cpp
|
||||
Triangulation.hpp Triangulation.cpp
|
||||
)
|
||||
@@ -625,7 +653,9 @@ if (IS_CLANG_CL)
|
||||
target_compile_options(libslic3r_cgal PRIVATE -Wno-overriding-option)
|
||||
endif ()
|
||||
|
||||
target_link_libraries(libslic3r_cgal PRIVATE ${_cgal_tgt} admesh libigl mcut boost_libs)
|
||||
# Lets CGAL algorithms given CGAL::Parallel_if_available_tag run on TBB.
|
||||
target_compile_definitions(libslic3r_cgal PRIVATE CGAL_LINKED_WITH_TBB)
|
||||
target_link_libraries(libslic3r_cgal PRIVATE ${_cgal_tgt} admesh libigl mcut boost_libs TBB::tbb)
|
||||
|
||||
if (MSVC AND "${CMAKE_SIZEOF_VOID_P}" STREQUAL "4") # 32 bit MSVC workaround
|
||||
target_compile_definitions(libslic3r_cgal PRIVATE CGAL_DO_NOT_USE_MPZF)
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace Slic3r { namespace csg {
|
||||
// A CSGPartT should be an object that can provide at least a mesh + trafo and an
|
||||
// associated csg operation. A collection of CSGPartT objects can then
|
||||
// be interpreted as one model and used in various contexts. It can be assembled
|
||||
// with CGAL or OpenVDB, rendered with OpenCSG or provided to a ray-tracer to
|
||||
// with CGAL or OpenVDB or provided to a ray-tracer to
|
||||
// deal with various parts of it according to the supported CSG types...
|
||||
//
|
||||
// A few simple templated interface functions are provided here and a default
|
||||
|
||||
@@ -0,0 +1,292 @@
|
||||
#include "ConnectedBodies.hpp"
|
||||
|
||||
#include "AABBTreeIndirect.hpp"
|
||||
#include "BoundingBox.hpp"
|
||||
#include "ClipperUtils.hpp"
|
||||
#include "ExPolygon.hpp"
|
||||
#include "Geometry/ConvexHull.hpp"
|
||||
#include "Point.hpp"
|
||||
#include "Polygon.hpp"
|
||||
#include "TriangleMesh.hpp"
|
||||
#include "TriangleMeshSlicer.hpp"
|
||||
#include "libslic3r.h"
|
||||
|
||||
#include <tbb/blocked_range.h>
|
||||
#include <tbb/parallel_for.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
#include <limits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
std::vector<std::vector<size_t>> connected_bodies(const std::vector<const ExPolygons *> &layers, size_t &count,
|
||||
const std::function<void()> &throw_if_canceled)
|
||||
{
|
||||
// Union-find over the islands of all layers, numbered layer after layer.
|
||||
std::vector<size_t> first(layers.size() + 1, 0);
|
||||
for (size_t l = 0; l < layers.size(); ++l)
|
||||
first[l + 1] = first[l] + layers[l]->size();
|
||||
std::vector<size_t> parent(first.back());
|
||||
for (size_t i = 0; i < parent.size(); ++i)
|
||||
parent[i] = i;
|
||||
const auto find = [&parent](size_t i) {
|
||||
while (parent[i] != i)
|
||||
i = parent[i] = parent[parent[i]];
|
||||
return i;
|
||||
};
|
||||
|
||||
std::vector<std::vector<BoundingBox>> boxes(layers.size());
|
||||
for (size_t l = 0; l < layers.size(); ++l)
|
||||
for (const ExPolygon &island : *layers[l])
|
||||
boxes[l].emplace_back(get_extents(island));
|
||||
for (size_t l = 0; l + 1 < layers.size(); ++l) {
|
||||
if (throw_if_canceled)
|
||||
throw_if_canceled();
|
||||
// Index the smaller of the two layers, so that a fragmented layer is not scanned island by island.
|
||||
size_t a_layer = l;
|
||||
size_t b_layer = l + 1;
|
||||
if (layers[a_layer]->size() < layers[b_layer]->size())
|
||||
std::swap(a_layer, b_layer);
|
||||
if (layers[b_layer]->empty())
|
||||
continue;
|
||||
using IslandTree = AABBTreeIndirect::Tree<2, coord_t>;
|
||||
std::vector<AABBTreeIndirect::BoundingBoxWrapper> wrappers;
|
||||
wrappers.reserve(boxes[b_layer].size());
|
||||
for (size_t b = 0; b < boxes[b_layer].size(); ++b)
|
||||
wrappers.emplace_back(b, boxes[b_layer][b]);
|
||||
IslandTree tree;
|
||||
tree.build_modify_input(wrappers);
|
||||
for (size_t a = 0; a < boxes[a_layer].size(); ++a) {
|
||||
const IslandTree::BoundingBox query(boxes[a_layer][a].min, boxes[a_layer][a].max);
|
||||
AABBTreeIndirect::traverse(
|
||||
tree, [&query](const IslandTree::Node &node) { return node.bbox.intersects(query); },
|
||||
[&](const IslandTree::Node &node) {
|
||||
// The tree's boxes are widened by an epsilon, and islands already joined need no clipping.
|
||||
const size_t b = node.idx;
|
||||
if (boxes[a_layer][a].overlap(boxes[b_layer][b]) && find(first[a_layer] + a) != find(first[b_layer] + b) &&
|
||||
!intersection_ex((*layers[a_layer])[a], (*layers[b_layer])[b]).empty())
|
||||
parent[find(first[a_layer] + a)] = find(first[b_layer] + b);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<size_t> body(parent.size(), std::numeric_limits<size_t>::max());
|
||||
std::vector<std::vector<size_t>> out(layers.size());
|
||||
count = 0;
|
||||
for (size_t l = 0; l < layers.size(); ++l)
|
||||
for (size_t i = 0; i < layers[l]->size(); ++i) {
|
||||
size_t &b = body[find(first[l] + i)];
|
||||
if (b == std::numeric_limits<size_t>::max())
|
||||
b = count++;
|
||||
out[l].emplace_back(b);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
IslandLocator::IslandLocator(const ExPolygons &islands, coord_t margin) : m_islands(&islands), m_alone(islands.size(), true)
|
||||
{
|
||||
m_boxes.reserve(islands.size());
|
||||
for (const ExPolygon &island : islands)
|
||||
m_boxes.emplace_back(get_extents(island).inflated(margin));
|
||||
// Sweep the boxes along x, so that only those reaching each other are compared.
|
||||
std::vector<size_t> order(m_boxes.size());
|
||||
for (size_t i = 0; i < order.size(); ++i)
|
||||
order[i] = i;
|
||||
std::sort(order.begin(), order.end(), [this](size_t l, size_t r) { return m_boxes[l].min.x() < m_boxes[r].min.x(); });
|
||||
for (size_t a = 0; a < order.size(); ++a)
|
||||
for (size_t b = a + 1; b < order.size() && m_boxes[order[b]].min.x() <= m_boxes[order[a]].max.x(); ++b)
|
||||
if (m_boxes[order[a]].overlap(m_boxes[order[b]]))
|
||||
m_alone[order[a]] = m_alone[order[b]] = false;
|
||||
}
|
||||
|
||||
bool IslandLocator::holds(size_t island, const Point &point, bool strict) const
|
||||
{
|
||||
return m_boxes[island].contains(point) && ((m_alone[island] && !strict) || (*m_islands)[island].contains(point));
|
||||
}
|
||||
|
||||
std::pair<int, double> IslandLocator::find(const Point &point, bool strict) const
|
||||
{
|
||||
int nearest = -1;
|
||||
double distance = std::numeric_limits<double>::max();
|
||||
for (size_t i = 0; i < m_boxes.size(); ++i)
|
||||
if (m_boxes[i].contains(point)) {
|
||||
if ((m_alone[i] && !strict) || (*m_islands)[i].contains(point))
|
||||
return { int(i), 0. };
|
||||
if (const double d = ((*m_islands)[i].point_projection(point) - point).cast<double>().squaredNorm(); d < distance) {
|
||||
distance = d;
|
||||
nearest = int(i);
|
||||
}
|
||||
}
|
||||
return { nearest, distance };
|
||||
}
|
||||
|
||||
// The area of polygons and their first and second moments of area, which holes, running clockwise, subtract.
|
||||
struct AreaMoments
|
||||
{
|
||||
double area{ 0. };
|
||||
Vec2d first{ Vec2d::Zero() };
|
||||
// Of x^2, y^2 and xy.
|
||||
Vec3d second{ Vec3d::Zero() };
|
||||
|
||||
void add(const Polygon &polygon)
|
||||
{
|
||||
if (polygon.points.size() < 3)
|
||||
return;
|
||||
Vec2d p1 = unscaled(polygon.points.back());
|
||||
for (const Point &point : polygon.points) {
|
||||
const Vec2d p2 = unscaled(point);
|
||||
const double a = cross2(p1, p2);
|
||||
area += a / 2.;
|
||||
first += a / 6. * (p1 + p2);
|
||||
second += a / 12. *
|
||||
Vec3d(p1.x() * p1.x() + p1.x() * p2.x() + p2.x() * p2.x(), p1.y() * p1.y() + p1.y() * p2.y() + p2.y() * p2.y(),
|
||||
p1.x() * p1.y() + p2.x() * p2.y() + 0.5 * (p1.x() * p2.y() + p2.x() * p1.y()));
|
||||
p1 = p2;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Mass, volume and the first and second moments of mass about the origin.
|
||||
struct Moments
|
||||
{
|
||||
double mass{ 0. };
|
||||
double volume{ 0. };
|
||||
Vec3d first{ Vec3d::Zero() };
|
||||
Matrix3d second{ Matrix3d::Zero() };
|
||||
|
||||
void add(const Moments &other)
|
||||
{
|
||||
mass += other.mass;
|
||||
volume += other.volume;
|
||||
first += other.first;
|
||||
second += other.second;
|
||||
}
|
||||
};
|
||||
|
||||
BoundingBoxf3 SolidBody::bounding_box(const Transform3d &trafo) const
|
||||
{
|
||||
BoundingBoxf3 box;
|
||||
for (const Point &point : hull.points)
|
||||
for (const double z : { z_min, z_max })
|
||||
box.merge(trafo * Vec3d(unscaled(point.x()), unscaled(point.y()), z));
|
||||
return box;
|
||||
}
|
||||
|
||||
std::vector<SolidBody> solid_bodies(const std::vector<MeshInPlace> &solids, const std::vector<double> &densities,
|
||||
const std::vector<MeshInPlace> &negatives, size_t slabs)
|
||||
{
|
||||
assert(densities.size() == solids.size());
|
||||
double z_min = std::numeric_limits<double>::max();
|
||||
double z_max = std::numeric_limits<double>::lowest();
|
||||
for (const auto &[mesh, trafo] : solids)
|
||||
for (const stl_vertex &v : mesh->vertices) {
|
||||
const double z = (trafo * v.cast<double>()).z();
|
||||
z_min = std::min(z_min, z);
|
||||
z_max = std::max(z_max, z);
|
||||
}
|
||||
if (z_min >= z_max || slabs == 0)
|
||||
return {};
|
||||
|
||||
// Each slab sliced at its middle.
|
||||
const double thickness = (z_max - z_min) / double(slabs);
|
||||
std::vector<float> zs(slabs);
|
||||
for (size_t k = 0; k < slabs; ++k)
|
||||
zs[k] = float(z_min + (double(k) + 0.5) * thickness);
|
||||
|
||||
MeshSlicingParamsEx params;
|
||||
const auto slice = [&zs, ¶ms](const MeshInPlace &mesh) {
|
||||
params.trafo = mesh.second;
|
||||
return slice_mesh_ex(*mesh.first, zs, params);
|
||||
};
|
||||
std::vector<std::vector<ExPolygons>> slices;
|
||||
for (const MeshInPlace &solid : solids)
|
||||
slices.emplace_back(slice(solid));
|
||||
std::vector<ExPolygons> cut(slabs);
|
||||
for (const MeshInPlace &negative : negatives) {
|
||||
std::vector<ExPolygons> slices_negative = slice(negative);
|
||||
for (size_t k = 0; k < slabs; ++k)
|
||||
append(cut[k], std::move(slices_negative[k]));
|
||||
}
|
||||
|
||||
// The islands of each slab, and the moments of what each solid prints of them with its density.
|
||||
const bool uniform = std::all_of(densities.begin(), densities.end(), [&densities](double d) { return d == densities.front(); });
|
||||
std::vector<ExPolygons> islands(slabs);
|
||||
std::vector<std::vector<Moments>> moments(slabs);
|
||||
tbb::parallel_for(tbb::blocked_range<size_t>(0, slabs), [&](const tbb::blocked_range<size_t> &range) {
|
||||
for (size_t k = range.begin(); k < range.end(); ++k) {
|
||||
ExPolygons all;
|
||||
for (const std::vector<ExPolygons> &solid : slices)
|
||||
append(all, solid[k]);
|
||||
islands[k] = diff_ex(union_ex(all), cut[k]);
|
||||
moments[k].assign(islands[k].size(), {});
|
||||
const double z = zs[k];
|
||||
const auto add = [&](const ExPolygon ®ion, double density, size_t island) {
|
||||
AreaMoments area;
|
||||
area.add(region.contour);
|
||||
for (const Polygon &hole : region.holes)
|
||||
area.add(hole);
|
||||
if (area.area <= 0.)
|
||||
return;
|
||||
// A prism of the slab's thickness.
|
||||
Matrix3d second;
|
||||
second << area.second.x(), area.second.z(), area.first.x() * z, area.second.z(), area.second.y(), area.first.y() * z,
|
||||
area.first.x() * z, area.first.y() * z, area.area * (z * z + thickness * thickness / 12.);
|
||||
moments[k][island].add({ density * area.area * thickness, area.area * thickness,
|
||||
density * thickness * Vec3d(area.first.x(), area.first.y(), area.area * z), density * thickness * second });
|
||||
};
|
||||
if (uniform) {
|
||||
for (size_t j = 0; j < islands[k].size(); ++j)
|
||||
add(islands[k][j], densities.front(), j);
|
||||
continue;
|
||||
}
|
||||
// A later solid prints where it overlaps an earlier one, and each region it prints lies in one island.
|
||||
const IslandLocator locator(islands[k], 10);
|
||||
ExPolygons later = cut[k];
|
||||
for (size_t i = solids.size(); i-- > 0;)
|
||||
if (!slices[i][k].empty()) {
|
||||
for (const ExPolygon ®ion : diff_ex(slices[i][k], later))
|
||||
if (const int island = locator.find(region.contour.points.front()).first; island >= 0)
|
||||
add(region, densities[i], size_t(island));
|
||||
later = union_ex(later, slices[i][k]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
std::vector<const ExPolygons *> layers;
|
||||
layers.reserve(slabs);
|
||||
for (const ExPolygons &layer : islands)
|
||||
layers.emplace_back(&layer);
|
||||
size_t count = 0;
|
||||
const std::vector<std::vector<size_t>> bodies = connected_bodies(layers, count);
|
||||
std::vector<Moments> sums(count);
|
||||
std::vector<Points> outlines(count);
|
||||
std::vector<SolidBody> out(count);
|
||||
for (SolidBody &body : out) {
|
||||
body.z_min = std::numeric_limits<double>::max();
|
||||
body.z_max = std::numeric_limits<double>::lowest();
|
||||
}
|
||||
for (size_t k = 0; k < slabs; ++k)
|
||||
for (size_t j = 0; j < islands[k].size(); ++j) {
|
||||
const size_t body = bodies[k][j];
|
||||
sums[body].add(moments[k][j]);
|
||||
append(outlines[body], islands[k][j].contour.points);
|
||||
out[body].z_min = std::min(out[body].z_min, zs[k] - 0.5 * thickness);
|
||||
out[body].z_max = std::max(out[body].z_max, zs[k] + 0.5 * thickness);
|
||||
}
|
||||
for (size_t body = 0; body < count; ++body)
|
||||
if (const Moments &sum = sums[body]; sum.mass > 0.) {
|
||||
const Vec3d center = sum.first / sum.mass;
|
||||
MassProperties &solid = out[body];
|
||||
solid = { sum.mass, sum.volume, center, sum.second / sum.mass - center * center.transpose() };
|
||||
out[body].hull = Geometry::convex_hull(std::move(outlines[body]));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,61 @@
|
||||
#pragma once
|
||||
|
||||
#include "BoundingBox.hpp"
|
||||
#include "ExPolygon.hpp"
|
||||
#include "Point.hpp"
|
||||
#include "Polygon.hpp"
|
||||
#include "TriangleMesh.hpp"
|
||||
#include "libslic3r.h"
|
||||
|
||||
#include <admesh/stl.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// The connected body of each island of each layer: islands of adjacent layers whose slices overlap are one body.
|
||||
// Bodies are numbered from 0 in the order of their first island.
|
||||
std::vector<std::vector<size_t>> connected_bodies(const std::vector<const ExPolygons *> &layers, size_t &count,
|
||||
const std::function<void()> &throw_if_canceled = nullptr);
|
||||
|
||||
// Finds the island of a layer that a point lies in, testing the polygons only where the boxes of several islands hold it.
|
||||
class IslandLocator
|
||||
{
|
||||
public:
|
||||
// The islands must outlive the locator. Their boxes are widened by the margin, for points reaching past an outline.
|
||||
IslandLocator(const ExPolygons &islands, coord_t margin);
|
||||
// Whether the island holds the point, or its box does where no other box reaches unless strict.
|
||||
bool holds(size_t island, const Point &point, bool strict = false) const;
|
||||
// The island holding the point as above, else the nearest one whose box holds it, with the squared distance to it; -1
|
||||
// for none.
|
||||
std::pair<int, double> find(const Point &point, bool strict = false) const;
|
||||
const std::vector<BoundingBox> &boxes() const { return m_boxes; }
|
||||
|
||||
private:
|
||||
const ExPolygons *m_islands;
|
||||
std::vector<BoundingBox> m_boxes;
|
||||
std::vector<bool> m_alone;
|
||||
};
|
||||
|
||||
using MeshInPlace = std::pair<const indexed_triangle_set *, Transform3d>;
|
||||
|
||||
// A connected body of solids, with its outline seen from above as a convex hull and the height it spans.
|
||||
struct SolidBody : MassProperties
|
||||
{
|
||||
Polygon hull;
|
||||
double z_min{ 0. };
|
||||
double z_max{ 0. };
|
||||
|
||||
// Its box once transformed, tight for a transformation that rotates about z only.
|
||||
BoundingBoxf3 bounding_box(const Transform3d &trafo) const;
|
||||
};
|
||||
|
||||
// Each connected body of the union of the solids less the negatives, sliced into slabs, each solid weighing its density.
|
||||
// Where solids overlap, the later one counts, as slicing prints it.
|
||||
std::vector<SolidBody> solid_bodies(const std::vector<MeshInPlace> &solids, const std::vector<double> &densities,
|
||||
const std::vector<MeshInPlace> &negatives, size_t slabs);
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -2589,7 +2589,6 @@ void priv::create_face_types(FaceTypeMap &map,
|
||||
}
|
||||
|
||||
#include <CGAL/Polygon_mesh_processing/clip.h>
|
||||
#include <CGAL/Polygon_mesh_processing/corefinement.h>
|
||||
bool priv::clip_cut(SurfacePatch &cut, CutMesh clipper)
|
||||
{
|
||||
CutMesh& tm = cut.mesh;
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
#include "Emboss.hpp"
|
||||
#include <optional>
|
||||
#include <stdio.h>
|
||||
#include <numeric>
|
||||
#include <cstdlib>
|
||||
#include <boost/nowide/convert.hpp>
|
||||
#include <boost/log/trivial.hpp>
|
||||
|
||||
@@ -403,6 +403,11 @@ inline void translate(ExPolygons &expolys, const Point &p) {
|
||||
expoly.translate(p);
|
||||
}
|
||||
|
||||
inline void translate(Polygons &polys, const Point &p) {
|
||||
for (Polygon &poly : polys)
|
||||
poly.translate(p);
|
||||
}
|
||||
|
||||
inline void polygons_append(Polygons &dst, const ExPolygon &src)
|
||||
{
|
||||
dst.reserve(dst.size() + src.holes.size() + 1);
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include <utility>
|
||||
|
||||
#include "ColorDecomposeRecipe.hpp"
|
||||
#include "Config.hpp"
|
||||
#include "FilamentMixerModel.hpp"
|
||||
#include "LocalesUtils.hpp"
|
||||
|
||||
@@ -427,6 +428,59 @@ std::vector<double> parse_mixed_ratios(const std::string &str, size_t n_componen
|
||||
return ratios;
|
||||
}
|
||||
|
||||
std::string format_mixed_components(const std::vector<unsigned int> &components)
|
||||
{
|
||||
std::string out;
|
||||
for (size_t i = 0; i < components.size(); ++i) {
|
||||
if (i > 0)
|
||||
out += ",";
|
||||
out += std::to_string(components[i]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string format_mixed_ratios(const std::vector<int> &weights)
|
||||
{
|
||||
int sum = std::accumulate(weights.begin(), weights.end(), 0);
|
||||
if (sum <= 0)
|
||||
sum = 100;
|
||||
CNumericLocalesSetter c_locale_setter;
|
||||
std::string out;
|
||||
for (size_t i = 0; i < weights.size(); ++i) {
|
||||
if (i > 0)
|
||||
out += ",";
|
||||
char buf[32];
|
||||
std::snprintf(buf, sizeof(buf), "%.4f", float(weights[i]) / float(sum));
|
||||
out += buf;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
int find_fixed_mixed_filament(const ConfigBase &project_config,
|
||||
const std::vector<unsigned int> &components,
|
||||
const std::vector<int> &weights)
|
||||
{
|
||||
const auto *is_mixed = project_config.option<ConfigOptionBools>("filament_is_mixed");
|
||||
const auto *comps = project_config.option<ConfigOptionStrings>("filament_mixed_components");
|
||||
const auto *ratios = project_config.option<ConfigOptionStrings>("filament_mixed_sublayer_ratios");
|
||||
if (is_mixed == nullptr || comps == nullptr || ratios == nullptr)
|
||||
return -1;
|
||||
// Created lazily with the first mixed slot, so an older project may not have it at all.
|
||||
const auto *gradient = project_config.option<ConfigOptionBools>("filament_mixed_gradient");
|
||||
|
||||
const std::string comp_str = format_mixed_components(components);
|
||||
const std::string ratio_str = format_mixed_ratios(weights);
|
||||
for (size_t i = 0; i < is_mixed->values.size(); ++i) {
|
||||
if (!is_mixed->values[i] || i >= comps->values.size() || i >= ratios->values.size())
|
||||
continue;
|
||||
if (gradient != nullptr && i < gradient->values.size() && gradient->values[i])
|
||||
continue;
|
||||
if (comps->values[i] == comp_str && ratios->values[i] == ratio_str)
|
||||
return int(i);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool has_any_mixed_filament(const std::vector<unsigned char> &is_mixed)
|
||||
{
|
||||
for (unsigned char v : is_mixed)
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
class ConfigBase;
|
||||
|
||||
// Photoshop-style gradient curve control point in [0,1] x [0,1].
|
||||
// (x, y) is the anchor position; (m_in, m_out) are optional cubic Hermite tangent
|
||||
// overrides. NaN means "use the PCHIP-computed default", which is the case for plain
|
||||
@@ -94,6 +96,22 @@ std::vector<unsigned int> parse_mixed_components(const std::string &str);
|
||||
// Normalizes so the sum equals 1.0.
|
||||
std::vector<double> parse_mixed_ratios(const std::string &str, size_t n_components);
|
||||
|
||||
// The text a mixed slot stores in filament_mixed_components, e.g. {1, 3} → "1,3".
|
||||
std::string format_mixed_components(const std::vector<unsigned int> &components);
|
||||
|
||||
// The text a mixed slot stores in filament_mixed_sublayer_ratios: the weights normalised to sum to 1,
|
||||
// four decimals, e.g. {1, 2} → "0.3333,0.6667". A non-positive sum is read as 100.
|
||||
std::string format_mixed_ratios(const std::vector<int> &weights);
|
||||
|
||||
// The 0-based index of the mixed slot in `project_config` that blends `components` (1-based physical
|
||||
// filaments) in `weights` at a fixed ratio, or -1 when there is none. Matched on the stored text, as
|
||||
// format_mixed_components() and format_mixed_ratios() write it. A gradient slot never matches: its
|
||||
// ratio drifts from layer to layer, so it is not the blend asked for even where its stored ratios
|
||||
// are the same.
|
||||
int find_fixed_mixed_filament(const ConfigBase &project_config,
|
||||
const std::vector<unsigned int> &components,
|
||||
const std::vector<int> &weights);
|
||||
|
||||
// Returns true if any element in is_mixed is true.
|
||||
// ConfigOptionBools stores values as std::vector<unsigned char>.
|
||||
bool has_any_mixed_filament(const std::vector<unsigned char> &is_mixed);
|
||||
|
||||
+60
-27
@@ -29,6 +29,7 @@
|
||||
#include "ExtrusionEntity.hpp"
|
||||
#include "Fill.hpp"
|
||||
#include "libslic3r/Fill/FillBase.hpp"
|
||||
#include "FillAdaptive.hpp"
|
||||
#include "FillRectilinear.hpp"
|
||||
#include "FillLightning.hpp"
|
||||
#include "FillConcentricInternal.hpp"
|
||||
@@ -310,6 +311,11 @@ struct SurfaceFillParams
|
||||
// For Gyroid: when true, use the parameterized "optimized" wave.
|
||||
bool gyroid_optimized = false;
|
||||
|
||||
// For TPMS: grade the density from the surface to the interior of the object.
|
||||
TpmsAdaptiveMode tpms_adaptive = TpmsAdaptiveMode::Disabled;
|
||||
float tpms_interior_density = 0.f;
|
||||
TpmsAdaptiveGradient tpms_adaptive_gradient = TpmsAdaptiveGradient::Linear;
|
||||
|
||||
// Orca: corner smoothing factor in the range [0, 1].
|
||||
double smooth_factor { 0. };
|
||||
|
||||
@@ -352,6 +358,9 @@ struct SurfaceFillParams
|
||||
RETURN_COMPARE_NON_EQUAL(skin_infill_depth);
|
||||
RETURN_COMPARE_NON_EQUAL(infill_overhang_angle);
|
||||
RETURN_COMPARE_NON_EQUAL(gyroid_optimized);
|
||||
RETURN_COMPARE_NON_EQUAL(tpms_adaptive);
|
||||
RETURN_COMPARE_NON_EQUAL(tpms_interior_density);
|
||||
RETURN_COMPARE_NON_EQUAL(tpms_adaptive_gradient);
|
||||
RETURN_COMPARE_NON_EQUAL(smooth_factor);
|
||||
RETURN_COMPARE_NON_EQUAL(center_of_surface_pattern);
|
||||
RETURN_COMPARE_NON_EQUAL(separated_infills);
|
||||
@@ -385,6 +394,9 @@ struct SurfaceFillParams
|
||||
this->center_of_surface_pattern == rhs.center_of_surface_pattern &&
|
||||
this->separated_infills == rhs.separated_infills &&
|
||||
this->gyroid_optimized == rhs.gyroid_optimized &&
|
||||
this->tpms_adaptive == rhs.tpms_adaptive &&
|
||||
this->tpms_interior_density == rhs.tpms_interior_density &&
|
||||
this->tpms_adaptive_gradient == rhs.tpms_adaptive_gradient &&
|
||||
this->smooth_factor == rhs.smooth_factor &&
|
||||
this->fill_order == rhs.fill_order;
|
||||
}
|
||||
@@ -926,7 +938,6 @@ std::vector<SurfaceFill> group_fills(const Layer &layer, LockRegionParam &lock_p
|
||||
params.lateral_lattice_angle_2 = region_config.lateral_lattice_angle_2;
|
||||
params.infill_overhang_angle = region_config.infill_overhang_angle;
|
||||
params.center_of_surface_pattern = region_config.center_of_surface_pattern;
|
||||
params.separated_infills = region_config.separated_infills;
|
||||
if (params.pattern == ipLockedZag) {
|
||||
params.infill_lock_depth = scale_(region_config.infill_lock_depth);
|
||||
params.skin_infill_depth = scale_(region_config.skin_infill_depth);
|
||||
@@ -994,11 +1005,22 @@ std::vector<SurfaceFill> group_fills(const Layer &layer, LockRegionParam &lock_p
|
||||
// Orca: apply fill multiline only for sparse infill
|
||||
params.multiline = params.extrusion_role == erInternalInfill ? int(region_config.fill_multiline) : 1;
|
||||
|
||||
// Orca: Pass through separated_infills only where it can move the pattern.
|
||||
params.separated_infills = region_config.separated_infills && is_separable_infill_pattern(params.pattern) &&
|
||||
params.extrusion_role != erTopSolidInfill && params.extrusion_role != erBottomSurface;
|
||||
|
||||
// Orca: only the TPMS sparse infill is graded; reset otherwise, as params is reused.
|
||||
params.tpms_adaptive = is_tpms_adaptive_pattern(params.pattern) && params.extrusion_role == erInternalInfill ?
|
||||
region_config.tpms_adaptive.value : TpmsAdaptiveMode::Disabled;
|
||||
const bool tpms_adaptive = params.tpms_adaptive != TpmsAdaptiveMode::Disabled;
|
||||
params.tpms_interior_density = tpms_adaptive ? std::max(1.f, float(region_config.tpms_interior_density)) : 0.f;
|
||||
params.tpms_adaptive_gradient = tpms_adaptive ? region_config.tpms_adaptive_gradient.value : TpmsAdaptiveGradient::Linear;
|
||||
|
||||
// Pass through gyroid_optimized only when the effective pattern is Gyroid,
|
||||
// so non-Gyroid fills do not differ in SurfaceFillParams by an irrelevant flag
|
||||
// (which would unnecessarily split fill batching).
|
||||
// (which would unnecessarily split fill batching). Adaptive density replaces it.
|
||||
// Stored on SurfaceFillParams; copied to FillParams during conversion.
|
||||
params.gyroid_optimized = (params.pattern == ipGyroid) && region_config.gyroid_optimized;
|
||||
params.gyroid_optimized = (params.pattern == ipGyroid) && region_config.gyroid_optimized && !tpms_adaptive;
|
||||
|
||||
if (params.extrusion_role == erInternalInfill) {
|
||||
params.angle = calculate_infill_rotation_angle(layer.object(), layer.id(), region_config.infill_direction.value,
|
||||
@@ -1073,7 +1095,7 @@ std::vector<SurfaceFill> group_fills(const Layer &layer, LockRegionParam &lock_p
|
||||
//get locked region param
|
||||
if (params.pattern == ipLockedZag){
|
||||
const PrintObject *object = layerm.layer()->object();
|
||||
auto nozzle_diameter = float(object->print()->config().nozzle_diameter.get_at(layerm.region().extruder(extrusion_role) - 1));
|
||||
auto nozzle_diameter = float(nozzle_diameter_for_filament(object->print()->config(), layerm.region().extruder(extrusion_role), object->print()->is_BBL_printer()));
|
||||
Flow skin_flow = params.bridge ? params.flow : Flow::new_from_config_width(extrusion_role, region_config.skin_infill_line_width, nozzle_diameter, float((surface.thickness == -1) ? layer.height : surface.thickness));
|
||||
//add skin flow
|
||||
append_flow_param(lock_param.skin_flow_params, skin_flow, surface.expolygon);
|
||||
@@ -1271,29 +1293,28 @@ std::vector<SurfaceFill> group_fills(const Layer &layer, LockRegionParam &lock_p
|
||||
|
||||
// Orca: Anchors and printed infill must share the same body origin. Keep the choice
|
||||
// here so per-model surface centering and separated sparse infill cannot drift apart.
|
||||
static BoundingBox infill_bounding_box(const Layer &layer, const SurfaceFill &fill, const ExPolygon &expoly, BoundingBox bbox)
|
||||
// Returns the connected body the fill region is laid out on, or -1 to keep the object's origin.
|
||||
static int infill_body(const Layer &layer, const SurfaceFill &fill, const ExPolygon &expoly)
|
||||
{
|
||||
const auto ¶ms = fill.params;
|
||||
const auto &config = layer.regions()[fill.region_id]->region().config();
|
||||
const bool external = params.extrusion_role == erTopSolidInfill || params.extrusion_role == erBottomSurface;
|
||||
const bool per_model = external && params.center_of_surface_pattern == CenterOfSurfacePattern::Each_Model &&
|
||||
const bool per_model = (params.extrusion_role == erTopSolidInfill || params.extrusion_role == erBottomSurface) &&
|
||||
params.center_of_surface_pattern == CenterOfSurfacePattern::Each_Model &&
|
||||
(params.pattern == ipArchimedeanChords || params.pattern == ipOctagramSpiral);
|
||||
const bool separate = !external && params.separated_infills &&
|
||||
(is_separable_infill_pattern(params.pattern) || !config.solid_infill_rotate_template.value.empty() ||
|
||||
!config.sparse_infill_rotate_template.value.empty());
|
||||
if (per_model || separate) {
|
||||
double best_overlap = 0.;
|
||||
for (size_t i = 0; i < layer.lslices.size() && i < layer.lslices_separated_component_bboxes.size(); ++i) {
|
||||
int body = -1;
|
||||
if (per_model || params.separated_infills || is_octree_infill_pattern(params.pattern)) {
|
||||
const BoundingBox box = get_extents(expoly);
|
||||
double best_overlap = 0.;
|
||||
for (size_t i = 0; i < layer.lslices.size() && i < layer.lslices_separated_component_ids.size(); ++i) {
|
||||
if (! layer.lslices_bboxes[i].overlap(box))
|
||||
continue;
|
||||
const double overlap = area(intersection_ex(layer.lslices[i], expoly));
|
||||
if (overlap > best_overlap) {
|
||||
best_overlap = overlap;
|
||||
const Point center = layer.lslices_separated_component_bboxes[i].center();
|
||||
bbox = layer.object()->bounding_box();
|
||||
bbox.translate(center.x(), center.y());
|
||||
body = int(layer.lslices_separated_component_ids[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return bbox;
|
||||
return body;
|
||||
}
|
||||
|
||||
#ifdef SLIC3R_DEBUG_SLICE_PROCESSING
|
||||
@@ -1318,7 +1339,7 @@ void export_group_fills_to_svg(const char *path, const std::vector<SurfaceFill>
|
||||
#endif
|
||||
|
||||
// friend to Layer
|
||||
void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive::Octree* support_fill_octree, FillLightning::Generator* lightning_generator)
|
||||
void Layer::make_fills(const FillAdaptive::RegionOctrees* fill_octrees, FillLightning::Generator* lightning_generator)
|
||||
{
|
||||
for (LayerRegion *layerm : m_regions)
|
||||
layerm->fills.clear();
|
||||
@@ -1351,7 +1372,8 @@ void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive:
|
||||
f->z = this->print_z;
|
||||
f->angle = surface_fill.params.angle;
|
||||
f->fixed_angle = surface_fill.params.fixed_angle;
|
||||
f->adapt_fill_octree = (surface_fill.params.pattern == ipSupportCubic) ? support_fill_octree : adaptive_fill_octree;
|
||||
const FillAdaptive::Octrees *octrees = fill_octrees ? fill_octrees->region(surface_fill.region_id) : nullptr;
|
||||
f->tpms_radial_field = this->object()->tpms_radial_field(surface_fill.params.tpms_adaptive);
|
||||
f->print_config = &this->object()->print()->config();
|
||||
f->print_object_config = &this->object()->config();
|
||||
if (surface_fill.params.pattern == ipConcentricInternal) {
|
||||
@@ -1401,6 +1423,9 @@ void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive:
|
||||
params.lateral_lattice_angle_2 = surface_fill.params.lateral_lattice_angle_2;
|
||||
params.infill_overhang_angle = surface_fill.params.infill_overhang_angle;
|
||||
params.gyroid_optimized = surface_fill.params.gyroid_optimized;
|
||||
params.tpms_adaptive = surface_fill.params.tpms_adaptive;
|
||||
params.tpms_interior_density = float(0.01 * surface_fill.params.tpms_interior_density);
|
||||
params.tpms_adaptive_gradient = surface_fill.params.tpms_adaptive_gradient;
|
||||
params.smooth_factor = surface_fill.params.smooth_factor;
|
||||
|
||||
// BBS
|
||||
@@ -1443,8 +1468,10 @@ void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive:
|
||||
params.can_reverse = false;
|
||||
for (ExPolygon& expoly : surface_fill.expolygons) {
|
||||
|
||||
// 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));
|
||||
// Orca: Reuse the body box and octree used for bridge anchoring, resetting them for each surface.
|
||||
const int body = infill_body(*this, surface_fill, expoly);
|
||||
f->set_bounding_box(body >= 0 ? this->object()->separated_body_bboxes()[body] : bbox);
|
||||
f->adapt_fill_octree = octrees ? octrees->get(body) : nullptr;
|
||||
|
||||
f->no_overlap_expolygons = intersection_ex(surface_fill.no_overlap_expolygons, ExPolygons() = {expoly}, ApplySafetyOffset::Yes);
|
||||
if (params.symmetric_infill_y_axis) {
|
||||
@@ -1512,7 +1539,7 @@ void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive:
|
||||
* - For lightning/adaptive patterns, the respective generators are wired so their
|
||||
* polylines match the final infill layout.
|
||||
*/
|
||||
Polylines Layer::generate_sparse_infill_polylines_for_anchoring(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive::Octree* support_fill_octree, FillLightning::Generator* lightning_generator) const
|
||||
Polylines Layer::generate_sparse_infill_polylines_for_anchoring(const FillAdaptive::RegionOctrees* fill_octrees, FillLightning::Generator* lightning_generator) const
|
||||
{
|
||||
LockRegionParam skin_inner_param;
|
||||
std::vector<SurfaceFill> surface_fills = group_fills(*this, skin_inner_param);
|
||||
@@ -1570,7 +1597,8 @@ Polylines Layer::generate_sparse_infill_polylines_for_anchoring(FillAdaptive::Oc
|
||||
f->z = this->print_z;
|
||||
f->angle = surface_fill.params.angle;
|
||||
f->fixed_angle = surface_fill.params.fixed_angle;
|
||||
f->adapt_fill_octree = (surface_fill.params.pattern == ipSupportCubic) ? support_fill_octree : adaptive_fill_octree;
|
||||
const FillAdaptive::Octrees *octrees = fill_octrees ? fill_octrees->region(surface_fill.region_id) : nullptr;
|
||||
f->tpms_radial_field = this->object()->tpms_radial_field(surface_fill.params.tpms_adaptive);
|
||||
f->print_config = &this->object()->print()->config();
|
||||
f->print_object_config = &this->object()->config();
|
||||
|
||||
@@ -1610,6 +1638,9 @@ Polylines Layer::generate_sparse_infill_polylines_for_anchoring(FillAdaptive::Oc
|
||||
params.infill_overhang_angle = surface_fill.params.infill_overhang_angle;
|
||||
params.multiline = surface_fill.params.multiline;
|
||||
params.gyroid_optimized = surface_fill.params.gyroid_optimized;
|
||||
params.tpms_adaptive = surface_fill.params.tpms_adaptive;
|
||||
params.tpms_interior_density = float(0.01 * surface_fill.params.tpms_interior_density);
|
||||
params.tpms_adaptive_gradient = surface_fill.params.tpms_adaptive_gradient;
|
||||
params.smooth_factor = surface_fill.params.smooth_factor;
|
||||
// Orca: Match make_fills() when choosing the origin of plane-path patterns.
|
||||
// Without the sparse extrusion role, the filler uses each surface's bounds
|
||||
@@ -1617,8 +1648,10 @@ Polylines Layer::generate_sparse_infill_polylines_for_anchoring(FillAdaptive::Oc
|
||||
params.extrusion_role = surface_fill.params.extrusion_role;
|
||||
|
||||
for (ExPolygon &expoly : surface_fill.expolygons) {
|
||||
// Orca: Match the per-body origin of make_fills() before generating physical anchors.
|
||||
f->set_bounding_box(infill_bounding_box(*this, surface_fill, expoly, bbox));
|
||||
// Orca: Match the per-body box and octree of make_fills() before generating physical anchors.
|
||||
const int body = infill_body(*this, surface_fill, expoly);
|
||||
f->set_bounding_box(body >= 0 ? this->object()->separated_body_bboxes()[body] : bbox);
|
||||
f->adapt_fill_octree = octrees ? octrees->get(body) : nullptr;
|
||||
// Spacing is modified by the filler to indicate adjustments. Reset it for each expolygon.
|
||||
f->spacing = surface_fill.params.spacing;
|
||||
surface_fill.surface.expolygon = std::move(expoly);
|
||||
@@ -1788,7 +1821,7 @@ void Layer::make_ironing()
|
||||
|
||||
// Create the ironing extrusions for regions <i, j)
|
||||
ExPolygons ironing_areas;
|
||||
double nozzle_dmr = this->object()->print()->config().nozzle_diameter.get_at(ironing_params.extruder - 1);
|
||||
double nozzle_dmr = nozzle_diameter_for_filament(this->object()->print()->config(), ironing_params.extruder, this->object()->print()->is_BBL_printer());
|
||||
if (ironing_params.just_infill) {
|
||||
//TODO just_infill is currently not used.
|
||||
// Just infill.
|
||||
|
||||
@@ -25,6 +25,7 @@ public:
|
||||
// pattern is placed on top of previous layers
|
||||
bool use_bridge_flow() const override { return false; }
|
||||
bool is_self_crossing() override { return false; }
|
||||
bool aligned_to_origin() const override { return true; }
|
||||
|
||||
protected:
|
||||
void _fill_surface_single(
|
||||
|
||||
@@ -301,88 +301,25 @@ void OctreeDeleter::operator()(Octree *p) {
|
||||
delete p;
|
||||
}
|
||||
|
||||
std::pair<double, double> adaptive_fill_line_spacing(const PrintObject &print_object)
|
||||
std::vector<double> adaptive_fill_line_spacing(const PrintObject &print_object)
|
||||
{
|
||||
// Output, spacing for icAdaptiveCubic and icSupportCubic
|
||||
double adaptive_line_spacing = 0.;
|
||||
double support_line_spacing = 0.;
|
||||
|
||||
enum class Tristate {
|
||||
Yes,
|
||||
No,
|
||||
Maybe
|
||||
};
|
||||
struct RegionFillData {
|
||||
Tristate has_adaptive_infill;
|
||||
Tristate has_support_infill;
|
||||
double density;
|
||||
double extrusion_width;
|
||||
};
|
||||
std::vector<RegionFillData> region_fill_data;
|
||||
region_fill_data.reserve(print_object.num_printing_regions());
|
||||
bool build_octree = false;
|
||||
std::vector<double> line_spacing(print_object.num_printing_regions(), 0.);
|
||||
const std::vector<double> &nozzle_diameters = print_object.print()->config().nozzle_diameter.values;
|
||||
double max_nozzle_diameter = *std::max_element(nozzle_diameters.begin(), nozzle_diameters.end());
|
||||
double default_infill_extrusion_width = Flow::auto_extrusion_width(FlowRole::frInfill, float(max_nozzle_diameter));
|
||||
for (size_t region_id = 0; region_id < print_object.num_printing_regions(); ++ region_id) {
|
||||
const PrintRegionConfig &config = print_object.printing_region(region_id).config();
|
||||
bool nonempty = config.sparse_infill_density > 0;
|
||||
bool has_adaptive_infill = nonempty && config.sparse_infill_pattern == ipAdaptiveCubic;
|
||||
bool has_support_infill = nonempty && config.sparse_infill_pattern == ipSupportCubic;
|
||||
double sparse_infill_line_width = config.sparse_infill_line_width.get_abs_value(max_nozzle_diameter);
|
||||
region_fill_data.push_back(RegionFillData({
|
||||
has_adaptive_infill ? Tristate::Maybe : Tristate::No,
|
||||
has_support_infill ? Tristate::Maybe : Tristate::No,
|
||||
config.sparse_infill_density,
|
||||
sparse_infill_line_width != 0. ? sparse_infill_line_width : default_infill_extrusion_width
|
||||
}));
|
||||
build_octree |= has_adaptive_infill || has_support_infill;
|
||||
for (size_t region_id = 0; region_id < line_spacing.size(); ++ region_id) {
|
||||
const PrintRegionConfig &config = print_object.printing_region(region_id).config();
|
||||
if (config.sparse_infill_density <= 0 || ! is_octree_infill_pattern(config.sparse_infill_pattern) ||
|
||||
std::none_of(print_object.layers().begin(), print_object.layers().end(), [region_id](const Layer *layer) {
|
||||
return region_id < layer->regions().size() && ! layer->regions()[region_id]->fill_surfaces.empty();
|
||||
}))
|
||||
continue;
|
||||
double extrusion_width = config.sparse_infill_line_width.get_abs_value(max_nozzle_diameter);
|
||||
if (extrusion_width == 0.)
|
||||
extrusion_width = default_infill_extrusion_width;
|
||||
line_spacing[region_id] = extrusion_width / ((config.sparse_infill_density / 100.0f) * 0.333333333f) * config.fill_multiline.value;
|
||||
}
|
||||
|
||||
if (build_octree) {
|
||||
// Compute the average of above parameters over all layers
|
||||
for (const Layer *layer : print_object.layers())
|
||||
for (size_t region_id = 0; region_id < layer->regions().size(); ++ region_id) {
|
||||
RegionFillData &rd = region_fill_data[region_id];
|
||||
if (rd.has_adaptive_infill == Tristate::Maybe && ! layer->regions()[region_id]->fill_surfaces.empty())
|
||||
rd.has_adaptive_infill = Tristate::Yes;
|
||||
if (rd.has_support_infill == Tristate::Maybe && ! layer->regions()[region_id]->fill_surfaces.empty())
|
||||
rd.has_support_infill = Tristate::Yes;
|
||||
}
|
||||
|
||||
double adaptive_fill_density = 0.;
|
||||
double adaptive_infill_extrusion_width = 0.;
|
||||
int adaptive_cnt = 0;
|
||||
double support_fill_density = 0.;
|
||||
double support_infill_extrusion_width = 0.;
|
||||
int support_cnt = 0;
|
||||
|
||||
for (const RegionFillData &rd : region_fill_data) {
|
||||
if (rd.has_adaptive_infill == Tristate::Yes) {
|
||||
adaptive_fill_density += rd.density;
|
||||
adaptive_infill_extrusion_width += rd.extrusion_width;
|
||||
++ adaptive_cnt;
|
||||
} else if (rd.has_support_infill == Tristate::Yes) {
|
||||
support_fill_density += rd.density;
|
||||
support_infill_extrusion_width += rd.extrusion_width;
|
||||
++ support_cnt;
|
||||
}
|
||||
}
|
||||
|
||||
auto to_line_spacing = [](int cnt, double density, double extrusion_width) {
|
||||
if (cnt) {
|
||||
density /= double(cnt);
|
||||
extrusion_width /= double(cnt);
|
||||
return extrusion_width / ((density / 100.0f) * 0.333333333f);
|
||||
} else
|
||||
return 0.;
|
||||
};
|
||||
const int n_multiline = print_object.printing_region(0).config().fill_multiline.value;
|
||||
adaptive_line_spacing = to_line_spacing(adaptive_cnt, adaptive_fill_density, adaptive_infill_extrusion_width) * n_multiline;
|
||||
support_line_spacing = to_line_spacing(support_cnt, support_fill_density, support_infill_extrusion_width) * n_multiline;
|
||||
}
|
||||
|
||||
return std::make_pair(adaptive_line_spacing, support_line_spacing);
|
||||
return line_spacing;
|
||||
}
|
||||
|
||||
// Context used by generate_infill_lines() when recursively traversing an octree in a DDA fashion
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include "libslic3r/BoundingBox.hpp"
|
||||
#include "libslic3r/ExPolygon.hpp"
|
||||
#include "FillBase.hpp"
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
#include <Eigen/Geometry>
|
||||
@@ -37,11 +38,43 @@ struct Octree;
|
||||
struct OctreeDeleter { void operator()(Octree *p); };
|
||||
using OctreePtr = std::unique_ptr<Octree, OctreeDeleter>;
|
||||
|
||||
// Calculate line spacing for
|
||||
// 1) adaptive cubic infill
|
||||
// 2) adaptive internal support cubic infill
|
||||
// Returns zero for a particular infill type if no such infill is to be generated.
|
||||
std::pair<double, double> adaptive_fill_line_spacing(const PrintObject &print_object);
|
||||
// Orca: One octree per body (see Layer::lslices_separated_component_ids), and one of the whole object
|
||||
// for objects of a single body or with a body that has none of its own.
|
||||
struct Octrees
|
||||
{
|
||||
OctreePtr object;
|
||||
std::vector<OctreePtr> bodies;
|
||||
|
||||
// A body without an octree, or body -1, uses the object's, or any body's when the object has none.
|
||||
Octree *get(int body) const
|
||||
{
|
||||
if (body >= 0 && size_t(body) < bodies.size() && bodies[body])
|
||||
return bodies[body].get();
|
||||
if (object)
|
||||
return object.get();
|
||||
for (const OctreePtr &octree : bodies)
|
||||
if (octree)
|
||||
return octree.get();
|
||||
return nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
// Orca: The octrees of each line spacing the regions of an object fill with.
|
||||
struct RegionOctrees
|
||||
{
|
||||
std::vector<Octrees> sets;
|
||||
// Index into sets for each region, -1 for a region without adaptive or support cubic infill.
|
||||
std::vector<int> region_set;
|
||||
|
||||
const Octrees *region(size_t region_id) const
|
||||
{
|
||||
return region_id < region_set.size() && region_set[region_id] >= 0 ? &sets[region_set[region_id]] : nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
// Line spacing of the adaptive or support cubic infill of each region of the object,
|
||||
// zero for a region that generates no such infill.
|
||||
std::vector<double> adaptive_fill_line_spacing(const PrintObject &print_object);
|
||||
|
||||
// Rotation of the octree to stand on one of its corners.
|
||||
Eigen::Quaterniond transform_to_world();
|
||||
|
||||
@@ -128,6 +128,9 @@ Polylines Fill::fill_surface(const Surface *surface, const FillParams ¶ms)
|
||||
{
|
||||
// Perform offset.
|
||||
Slic3r::ExPolygons expp = offset_ex(surface->expolygon, float(scale_(this->overlap - 0.5 * this->spacing)));
|
||||
// Orca: Separated infills move the box center onto each body; origin-aligned patterns follow it.
|
||||
const Point shift = this->aligned_to_origin() && ! empty(this->bounding_box) ? this->bounding_box.center() : Point::Zero();
|
||||
translate(expp, -shift);
|
||||
// Create the infills for each of the regions.
|
||||
Polylines polylines_out;
|
||||
for (size_t i = 0; i < expp.size(); ++ i)
|
||||
@@ -137,6 +140,8 @@ Polylines Fill::fill_surface(const Surface *surface, const FillParams ¶ms)
|
||||
_infill_direction(surface),
|
||||
std::move(expp[i]),
|
||||
polylines_out);
|
||||
for (Polyline &pl : polylines_out)
|
||||
pl.translate(shift);
|
||||
return polylines_out;
|
||||
}
|
||||
|
||||
@@ -1591,13 +1596,18 @@ BoundaryInfillGraph create_boundary_infill_graph(const Polylines &infill_ordered
|
||||
// The extended bounding box of the whole object that covers any rotation of every layer.
|
||||
BoundingBox Fill::extended_object_bounding_box() const
|
||||
{
|
||||
BoundingBox out = bounding_box;
|
||||
// Orca: Extend about the box center, which separated infills move off the origin.
|
||||
const Point c = this->bounding_box.center();
|
||||
BoundingBox out = this->bounding_box;
|
||||
out.translate(-c.x(), -c.y());
|
||||
out.merge(Point(out.min.y(), out.min.x()));
|
||||
out.merge(Point(out.max.y(), out.max.x()));
|
||||
|
||||
// The bounding box is scaled by sqrt(2.) to ensure that the bounding box
|
||||
// covers any possible rotations.
|
||||
return out.scaled(sqrt(2.));
|
||||
out = out.scaled(sqrt(2.));
|
||||
out.translate(c.x(), c.y());
|
||||
return out;
|
||||
}
|
||||
|
||||
void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Polygon*> &boundary_src, const BoundingBox &bbox, Polylines &polylines_out, const double spacing, const FillParams ¶ms)
|
||||
|
||||
@@ -33,6 +33,7 @@ namespace Slic3r { class ExtrusionEntityCollection; }
|
||||
namespace Slic3r {
|
||||
|
||||
class Surface;
|
||||
class TpmsRadialField;
|
||||
enum InfillPattern : int;
|
||||
|
||||
namespace FillAdaptive {
|
||||
@@ -91,6 +92,11 @@ struct FillParams
|
||||
// For Gyroid: when true, use the parameterized "optimized" variant.
|
||||
bool gyroid_optimized { false };
|
||||
|
||||
// For TPMS: grade the density from the surface to the interior of the object. Density fraction.
|
||||
TpmsAdaptiveMode tpms_adaptive { TpmsAdaptiveMode::Disabled };
|
||||
float tpms_interior_density { 0.f };
|
||||
TpmsAdaptiveGradient tpms_adaptive_gradient { TpmsAdaptiveGradient::Linear };
|
||||
|
||||
// Orca: corner smoothing factor in the range [0, 1].
|
||||
double smooth_factor { 0. };
|
||||
|
||||
@@ -155,6 +161,9 @@ public:
|
||||
// Octree builds on mesh for usage in the adaptive cubic infill
|
||||
FillAdaptive::Octree* adapt_fill_octree = nullptr;
|
||||
|
||||
// Radial coordinate inside the object for the adaptive TPMS infill
|
||||
const TpmsRadialField* tpms_radial_field = nullptr;
|
||||
|
||||
// PrintConfig and PrintObjectConfig are used by infills that use Arachne (Concentric and FillEnsuring).
|
||||
// Orca: also used by gap fill function.
|
||||
const PrintConfig *print_config = nullptr;
|
||||
@@ -188,6 +197,9 @@ public:
|
||||
// Return true if infill has a consistent pattern between layers.
|
||||
virtual bool has_consistent_pattern() const { return false; }
|
||||
|
||||
// Orca: Is the pattern laid out from the origin instead of the bounding box center?
|
||||
virtual bool aligned_to_origin() const { return false; }
|
||||
|
||||
// Perform the fill.
|
||||
virtual Polylines fill_surface(const Surface *surface, const FillParams ¶ms);
|
||||
virtual ThickPolylines fill_surface_arachne(const Surface* surface, const FillParams& params);
|
||||
|
||||
@@ -19,6 +19,7 @@ public:
|
||||
Fill *clone() const override { return new FillCrossHatch(*this); };
|
||||
~FillCrossHatch() override {}
|
||||
bool is_self_crossing() override { return false; }
|
||||
bool aligned_to_origin() const override { return true; }
|
||||
|
||||
protected:
|
||||
void _fill_surface_single(
|
||||
|
||||
@@ -17,6 +17,17 @@
|
||||
#include "libslic3r/Polyline.hpp"
|
||||
#include "FillGyroid.hpp"
|
||||
#include "libslic3r/Polygon.hpp"
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
#include "FillTpmsAdaptive.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
static float gyroid(float x, float y, float z)
|
||||
{
|
||||
return std::sin(x) * std::cos(y) + std::sin(y) * std::cos(z) + std::sin(z) * std::cos(x);
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Marching-squares scalar field for the optimized gyroid branch.
|
||||
@@ -62,10 +73,7 @@ struct GyroidField
|
||||
|
||||
float get_scalar(coordf_t x, coordf_t y, coordf_t z_arg) const
|
||||
{
|
||||
const float a = fx * float(x);
|
||||
const float b = fy * float(y);
|
||||
const float c = fz * float(z_arg);
|
||||
return std::sin(a) * std::cos(b) + std::sin(b) * std::cos(c) + std::sin(c) * std::cos(a);
|
||||
return gyroid(fx * float(x), fy * float(y), fz * float(z_arg));
|
||||
}
|
||||
|
||||
float get_scalar(Coord p) const
|
||||
@@ -307,6 +315,14 @@ void FillGyroid::_fill_surface_single(
|
||||
ExPolygon expolygon,
|
||||
Polylines &polylines_out)
|
||||
{
|
||||
if (params.tpms_adaptive == TpmsAdaptiveMode::SteppedShells && this->tpms_radial_field != nullptr) {
|
||||
fill_tpms_shells(*this->tpms_radial_field, expolygon, this->z - 0.5 * params.layer_height, params, this->spacing,
|
||||
[&](const FillParams &shell_params, const ExPolygon &shell) {
|
||||
this->_fill_surface_single(shell_params, thickness_layers, direction, shell, polylines_out);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
auto infill_angle = float(this->angle + (CorrectionAngle * 2*M_PI) / 360.);
|
||||
if(std::abs(infill_angle) >= EPSILON)
|
||||
expolygon.rotate(-infill_angle);
|
||||
@@ -326,7 +342,12 @@ void FillGyroid::_fill_surface_single(
|
||||
|
||||
// generate pattern
|
||||
Polylines polylines;
|
||||
if (params.gyroid_optimized) {
|
||||
if (params.tpms_adaptive != TpmsAdaptiveMode::Disabled && this->tpms_radial_field != nullptr) {
|
||||
// Radians per mm of the regular pattern at a density.
|
||||
auto frequency = [¶ms, this](double density) { return density * DensityAdjust / (params.multiline * this->spacing); };
|
||||
polylines = make_adaptive_tpms({gyroid, frequency(params.density), frequency(params.tpms_interior_density), params.tpms_adaptive_gradient},
|
||||
*this->tpms_radial_field, bb, this->z, params.layer_height, this->spacing, infill_angle);
|
||||
} else if (params.gyroid_optimized) {
|
||||
// Marching-squares path on the gyroid implicit field. Base period matches
|
||||
// the standard parametric path's wavelength: 2*pi * spacing / density_adj.
|
||||
// omega >= 1 always, so fz >= baseline -> shorter vertical wavelength ->
|
||||
|
||||
@@ -20,6 +20,7 @@ public:
|
||||
// require bridge flow since most of this pattern hangs in air
|
||||
bool use_bridge_flow() const override { return false; }
|
||||
bool is_self_crossing() override { return false; }
|
||||
bool aligned_to_origin() const override { return true; }
|
||||
|
||||
// Correction applied to regular infill angle to maximize printing
|
||||
// speed in default configuration (degrees)
|
||||
|
||||
@@ -20,6 +20,7 @@ class FillHoneycomb : public Fill
|
||||
public:
|
||||
~FillHoneycomb() override {}
|
||||
bool is_self_crossing() override { return false; }
|
||||
bool aligned_to_origin() const override { return true; }
|
||||
|
||||
protected:
|
||||
Fill* clone() const override { return new FillHoneycomb(*this); };
|
||||
|
||||
@@ -2750,23 +2750,6 @@ static void polylines_from_paths(const std::vector<MonotonicRegionLink> &path, c
|
||||
}
|
||||
}
|
||||
|
||||
// The extended bounding box of the whole object that covers any rotation of every layer.
|
||||
BoundingBox FillRectilinear::extended_object_bounding_box() const {
|
||||
// Build the extension around the box center. The transpose merge and the sqrt(2.) scaling
|
||||
// (which covers any possible rotation) are both defined about the origin, so a box that is not
|
||||
// origin-centered — e.g. a separated-infill box re-centered on a single assembly part — would be
|
||||
// distorted. Shift to the origin first and back afterwards; for the default origin-centered box
|
||||
// the two translations cancel and this is identical to the original behavior.
|
||||
const Point c = this->bounding_box.center();
|
||||
BoundingBox out = this->bounding_box;
|
||||
out.translate(-c.x(), -c.y());
|
||||
out.merge(Point(out.min.y(), out.min.x()));
|
||||
out.merge(Point(out.max.y(), out.max.x()));
|
||||
out = out.scaled(sqrt(2.));
|
||||
out.translate(c.x(), c.y());
|
||||
return out;
|
||||
}
|
||||
|
||||
bool FillRectilinear::fill_surface_by_lines(const Surface *surface, const FillParams ¶ms, float angleBase, float pattern_shift, Polylines &polylines_out)
|
||||
{
|
||||
// At the end, only the new polylines will be rotated back.
|
||||
@@ -2801,7 +2784,13 @@ bool FillRectilinear::fill_surface_by_lines(const Surface *surface, const FillPa
|
||||
// For infill that needs to be consistent between layers (like Zig Zag),
|
||||
// we use bounding box of whole object to match vertical lines between layers.
|
||||
BoundingBox bounding_box_src = poly_with_offset.bounding_box_src();
|
||||
BoundingBox bounding_box = this->has_consistent_pattern() ? this->extended_object_bounding_box() : bounding_box_src;
|
||||
BoundingBox bounding_box = bounding_box_src;
|
||||
if (this->has_consistent_pattern()) {
|
||||
// Orca: The polygons are rotated about the origin, so follow the box center to where it was rotated.
|
||||
const Point c = this->bounding_box.center();
|
||||
bounding_box = this->extended_object_bounding_box();
|
||||
bounding_box.translate(c.rotated(- rotate_vector.first) - c);
|
||||
}
|
||||
|
||||
// define flow spacing according to requested density
|
||||
if (params.full_infill() && !params.dont_adjust) {
|
||||
|
||||
@@ -42,9 +42,6 @@ protected:
|
||||
};
|
||||
bool fill_surface_by_multilines(const Surface *surface, FillParams params, const std::initializer_list<SweepParams> &sweep_params, Polylines &polylines_out);
|
||||
bool fill_surface_trapezoidal(const Surface *surface, FillParams params, Polylines &polylines_out, int Pattern_type);
|
||||
|
||||
// The extended bounding box of the whole object that covers any rotation of every layer.
|
||||
BoundingBox extended_object_bounding_box() const;
|
||||
};
|
||||
|
||||
class FillAlignedRectilinear : public FillRectilinear
|
||||
|
||||
@@ -0,0 +1,823 @@
|
||||
#include "FillTpmsAdaptive.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <deque>
|
||||
#include <functional>
|
||||
#include <limits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <tbb/blocked_range.h>
|
||||
#include <tbb/parallel_for.h>
|
||||
|
||||
#include "../BoundingBox.hpp"
|
||||
#include "../ClipperUtils.hpp"
|
||||
#include "../ExPolygon.hpp"
|
||||
#include "FillBase.hpp"
|
||||
#include "../Execution/ExecutionTBB.hpp"
|
||||
#include "../MarchingSquares.hpp"
|
||||
#include "../Point.hpp"
|
||||
#include "../Polygon.hpp"
|
||||
#include "../Polyline.hpp"
|
||||
#include "../PrintConfig.hpp"
|
||||
#include "../libslic3r.h"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
namespace {
|
||||
|
||||
// At most 4 MB of body indices; finer cells would not change the grading.
|
||||
constexpr double MaxNodes = double(1 << 20);
|
||||
constexpr double MinCellSize = 0.5;
|
||||
|
||||
// Two deepest points are in separate lobes when the depth between them drops below this ratio of the shallower one.
|
||||
constexpr double NeckRatio = 0.8;
|
||||
// Lobes shallower than this ratio of the deepest one of their body are graded as part of it.
|
||||
constexpr double MinLobeRatio = 0.3;
|
||||
// A lobe reaches twice as far as the side towards its neighbour, so that the side is half way to the surface.
|
||||
constexpr double LobeReach = 2.;
|
||||
// Width of the morph between the patterns of two lobes, in their distance to the center over its depth.
|
||||
constexpr double LobeMorph = 0.1;
|
||||
// Densities of the levels of Stepped shells, and of Smooth blend, are at most these ratios apart. Fewer levels blend
|
||||
// with fewer lines running along the blends.
|
||||
constexpr double ShellRatio = 1.5;
|
||||
constexpr double BlendRatio = 2.5;
|
||||
// Distance warp: samples of the mean depth along a ray.
|
||||
constexpr int Samples = 32;
|
||||
|
||||
constexpr float InfF = std::numeric_limits<float>::infinity();
|
||||
constexpr double InfD = std::numeric_limits<double>::infinity();
|
||||
|
||||
// Squared distance transform of a line (Felzenszwalb & Huttenlocher); infinite samples are no sites.
|
||||
void distance_transform_line(const float *f, float *d, int n, int *v, double *s)
|
||||
{
|
||||
int k = -1;
|
||||
for (int q = 0; q < n; ++q) {
|
||||
if (f[q] == InfF)
|
||||
continue;
|
||||
double x = -InfD;
|
||||
while (k >= 0) {
|
||||
x = (f[q] + double(q) * q - f[v[k]] - double(v[k]) * v[k]) / (2. * (q - v[k]));
|
||||
if (x > s[k])
|
||||
break;
|
||||
--k;
|
||||
}
|
||||
if (k < 0)
|
||||
x = -InfD;
|
||||
v[++k] = q;
|
||||
s[k] = x;
|
||||
s[k + 1] = InfD;
|
||||
}
|
||||
if (k < 0) {
|
||||
std::fill(d, d + n, InfF);
|
||||
return;
|
||||
}
|
||||
for (int q = 0, j = 0; q < n; ++q) {
|
||||
while (s[j + 1] < q)
|
||||
++j;
|
||||
d[q] = float(sqr(double(q - v[j])) + f[v[j]]);
|
||||
}
|
||||
}
|
||||
|
||||
void distance_transform_axis(std::vector<float> &grid, const Vec3i32 &size, int axis, const std::function<void()> &throw_if_canceled)
|
||||
{
|
||||
const int n = size[axis];
|
||||
const int a1 = (axis + 1) % 3;
|
||||
const int a2 = (axis + 2) % 3;
|
||||
const size_t stride = axis == 0 ? 1 : axis == 1 ? size_t(size.x()) : size_t(size.x()) * size.y();
|
||||
tbb::parallel_for(tbb::blocked_range<size_t>(0, size_t(size[a1]) * size[a2]), [&](const tbb::blocked_range<size_t> &range) {
|
||||
std::vector<float> f(n), d(n);
|
||||
std::vector<int> v(n);
|
||||
std::vector<double> s(n + 1);
|
||||
for (size_t line = range.begin(); line < range.end(); ++line) {
|
||||
Vec3i32 idx;
|
||||
idx[axis] = 0;
|
||||
idx[a1] = int(line % size[a1]);
|
||||
idx[a2] = int(line / size[a1]);
|
||||
const size_t first = (size_t(idx.z()) * size.y() + idx.y()) * size.x() + idx.x();
|
||||
for (int i = 0; i < n; ++i)
|
||||
f[i] = grid[first + i * stride];
|
||||
distance_transform_line(f.data(), d.data(), n, v.data(), s.data());
|
||||
for (int i = 0; i < n; ++i)
|
||||
grid[first + i * stride] = d[i];
|
||||
}
|
||||
throw_if_canceled();
|
||||
});
|
||||
}
|
||||
|
||||
// Marks the nodes inside the expolygons with infinity, by even-odd scanlines.
|
||||
void rasterize(const ExPolygons &expolygons, const Vec2d &origin, double cell, int nx, int ny, float *nodes)
|
||||
{
|
||||
std::vector<std::vector<double>> crossings(ny);
|
||||
auto add_crossings = [&](const Polygon &polygon) {
|
||||
const Points &pts = polygon.points;
|
||||
for (size_t i = 0; i < pts.size(); ++i) {
|
||||
const Vec2d a = unscaled(pts[i]);
|
||||
const Vec2d b = unscaled(pts[i + 1 == pts.size() ? 0 : i + 1]);
|
||||
if (a.y() == b.y())
|
||||
continue;
|
||||
const auto [lo, hi] = std::minmax(a.y(), b.y());
|
||||
const int j0 = std::max(0, int(std::ceil((lo - origin.y()) / cell)));
|
||||
const int j1 = std::min(ny, int(std::ceil((hi - origin.y()) / cell)));
|
||||
for (int j = j0; j < j1; ++j) {
|
||||
const double y = origin.y() + j * cell;
|
||||
crossings[j].push_back(a.x() + (b.x() - a.x()) * (y - a.y()) / (b.y() - a.y()));
|
||||
}
|
||||
}
|
||||
};
|
||||
for (const ExPolygon &expolygon : expolygons) {
|
||||
add_crossings(expolygon.contour);
|
||||
for (const Polygon &hole : expolygon.holes)
|
||||
add_crossings(hole);
|
||||
}
|
||||
for (int j = 0; j < ny; ++j) {
|
||||
std::vector<double> &xs = crossings[j];
|
||||
std::sort(xs.begin(), xs.end());
|
||||
for (size_t k = 0; k + 1 < xs.size(); k += 2) {
|
||||
const int i0 = std::max(0, int(std::ceil((xs[k] - origin.x()) / cell)));
|
||||
const int i1 = std::min(nx, int(std::ceil((xs[k + 1] - origin.x()) / cell)));
|
||||
std::fill(nodes + size_t(j) * nx + std::min(i0, i1), nodes + size_t(j) * nx + i1, InfF);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Scale of the pattern relative to the surface at a depth from 0 at the surface to 1 at the deepest point.
|
||||
double target_scale(double ratio, TpmsAdaptiveGradient gradient, double depth)
|
||||
{
|
||||
switch (gradient) {
|
||||
case TpmsAdaptiveGradient::Quadratic: return 1. + (ratio - 1.) * depth * depth;
|
||||
case TpmsAdaptiveGradient::Exponential: return std::pow(ratio, depth);
|
||||
default: return 1. + (ratio - 1.) * depth;
|
||||
}
|
||||
}
|
||||
|
||||
// Levels of Stepped shells and Smooth blend, geometric from the surface at 0 to the interior at count, and the
|
||||
// continuous level of the target of a depth.
|
||||
struct DensityLevels
|
||||
{
|
||||
DensityLevels(double ratio, double step, TpmsAdaptiveGradient gradient)
|
||||
: ratio(ratio), gradient(gradient), count(int(std::ceil(std::abs(std::log(ratio)) / std::log(step) - EPSILON)))
|
||||
{}
|
||||
|
||||
double scale(int level) const { return count == 0 ? 1. : std::pow(ratio, double(level) / count); }
|
||||
double level(double depth) const
|
||||
{
|
||||
return count == 0 ? 0. : count * std::log(target_scale(ratio, gradient, depth)) / std::log(ratio);
|
||||
}
|
||||
|
||||
double ratio;
|
||||
TpmsAdaptiveGradient gradient;
|
||||
int count;
|
||||
};
|
||||
|
||||
// Scale of the pattern around the center at a radial coordinate t. The mean cell scale over the ball of radius t,
|
||||
// t^-3 * integral of 3 t'^2 * target(t'), or over the disc in 2D, follows the gradient; beyond the surface the target
|
||||
// is the surface scale.
|
||||
class RadialScale
|
||||
{
|
||||
public:
|
||||
RadialScale(const AdaptiveTpms &tpms, int dimensions) : m_dimensions(dimensions)
|
||||
{
|
||||
const double ratio = std::max(tpms.interior_frequency / tpms.surface_frequency, 1e-3);
|
||||
auto target = [&tpms, ratio](double depth) { return target_scale(ratio, tpms.gradient, depth); };
|
||||
m_scale[0] = target(1.);
|
||||
double volume = 0.;
|
||||
for (size_t i = 1; i < m_scale.size(); ++i) {
|
||||
const double t0 = double(i - 1) / double(m_scale.size() - 1);
|
||||
const double t1 = double(i) / double(m_scale.size() - 1);
|
||||
volume += (std::pow(t1, m_dimensions) - std::pow(t0, m_dimensions)) * target(1. - 0.5 * (t0 + t1));
|
||||
m_scale[i] = volume / std::pow(t1, m_dimensions);
|
||||
}
|
||||
}
|
||||
|
||||
double operator()(double t) const
|
||||
{
|
||||
if (t >= 1.) {
|
||||
const double volume = std::pow(t, m_dimensions);
|
||||
return (m_scale.back() + volume - 1.) / volume;
|
||||
}
|
||||
const double x = t * double(m_scale.size() - 1);
|
||||
const size_t i = std::min(size_t(x), m_scale.size() - 2);
|
||||
return m_scale[i] + (m_scale[i + 1] - m_scale[i]) * (x - double(i));
|
||||
}
|
||||
|
||||
private:
|
||||
int m_dimensions;
|
||||
std::array<double, 257> m_scale;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
TpmsRadialField::TpmsRadialField(const std::vector<Slice> &slices, const BoundingBox &bbox, TpmsAdaptiveMode mode,
|
||||
const std::function<void()> &throw_if_canceled)
|
||||
: m_mode(mode)
|
||||
, m_axis(mode == TpmsAdaptiveMode::NormalX ? 0 : mode == TpmsAdaptiveMode::NormalY ? 1 : mode == TpmsAdaptiveMode::NormalZ ? 2 : -1)
|
||||
{
|
||||
assert(!slices.empty() && mode != TpmsAdaptiveMode::Disabled);
|
||||
const Vec3d min(unscaled(bbox.min.x()), unscaled(bbox.min.y()), slices.front().bottom_z);
|
||||
const Vec3d extent = Vec3d(unscaled(bbox.max.x()), unscaled(bbox.max.y()), slices.back().top_z) - min;
|
||||
|
||||
// Padded by a node on each side, so that the border of the grid is outside.
|
||||
m_cell = std::max(MinCellSize, std::cbrt(extent.prod() / MaxNodes));
|
||||
auto nodes = [this](double length) { return int(std::ceil(length / m_cell)) + 3; };
|
||||
while (double(nodes(extent.x())) * nodes(extent.y()) * nodes(extent.z()) > MaxNodes)
|
||||
m_cell *= 1.1;
|
||||
m_size = Vec3i32(nodes(extent.x()), nodes(extent.y()), nodes(extent.z()));
|
||||
m_origin = min - Vec3d::Constant(m_cell);
|
||||
|
||||
const size_t sy = size_t(m_size.x());
|
||||
const size_t sz = sy * m_size.y();
|
||||
std::vector<float> depth(sz * m_size.z(), 0.f);
|
||||
tbb::parallel_for(tbb::blocked_range<int>(0, m_size.z()), [&](const tbb::blocked_range<int> &range) {
|
||||
for (int k = range.begin(); k < range.end(); ++k) {
|
||||
const double z = m_origin.z() + k * m_cell;
|
||||
auto it = std::lower_bound(slices.begin(), slices.end(), z, [](const Slice &s, double z) { return s.top_z < z; });
|
||||
if (it != slices.end() && z > it->bottom_z)
|
||||
rasterize(*it->expolygons, m_origin.head<2>(), m_cell, m_size.x(), m_size.y(), depth.data() + k * sz);
|
||||
}
|
||||
throw_if_canceled();
|
||||
});
|
||||
for (int axis = 0; axis < 3; ++axis)
|
||||
if (axis != m_axis)
|
||||
distance_transform_axis(depth, m_size, axis, throw_if_canceled);
|
||||
|
||||
auto position = [this, sy, sz](size_t i) {
|
||||
return Vec3d(m_origin + m_cell * Vec3d(double(i % sy), double(i / sy % m_size.y()), double(i / sz)));
|
||||
};
|
||||
const std::array<std::ptrdiff_t, 6> steps{1, -1, std::ptrdiff_t(sy), -std::ptrdiff_t(sy), std::ptrdiff_t(sz), -std::ptrdiff_t(sz)};
|
||||
|
||||
auto node_of = [this, sy, sz](const Vec3d &pt) -> std::ptrdiff_t {
|
||||
const Vec3d f = (pt - m_origin) / m_cell;
|
||||
const long x = std::lround(f.x()), y = std::lround(f.y()), z = std::lround(f.z());
|
||||
if (x < 0 || y < 0 || z < 0 || x >= m_size.x() || y >= m_size.y() || z >= m_size.z())
|
||||
return -1;
|
||||
return std::ptrdiff_t(size_t(z) * sz + size_t(y) * sy + size_t(x));
|
||||
};
|
||||
// In the 2D modes, the steps within a section.
|
||||
auto in_section = [this](size_t step) { return int(step / 2) != m_axis; };
|
||||
std::vector<std::ptrdiff_t> neighbours;
|
||||
for (int dz = -1; dz <= 1; ++dz)
|
||||
for (int dy = -1; dy <= 1; ++dy)
|
||||
for (int dx = -1; dx <= 1; ++dx)
|
||||
if ((dx != 0 || dy != 0 || dz != 0) && (m_axis < 0 || Vec3i32(dx, dy, dz)[m_axis] == 0))
|
||||
neighbours.push_back(std::ptrdiff_t(dz) * std::ptrdiff_t(sz) + std::ptrdiff_t(dy) * std::ptrdiff_t(sy) + dx);
|
||||
|
||||
// Bodies are the connected inside nodes, none of which is on the border. The deepest nodes of a body are the
|
||||
// centers of its lobes, unless the depth between them stays above NeckRatio; where the depth ties, the center
|
||||
// is the node nearest to the middle of the tied nodes.
|
||||
const bool lobes = m_mode != TpmsAdaptiveMode::SteppedShells && m_mode != TpmsAdaptiveMode::SmoothBlend;
|
||||
m_body.assign(depth.size(), -1);
|
||||
std::vector<size_t> body_nodes;
|
||||
for (size_t seed = 0; seed < depth.size(); ++seed) {
|
||||
if (depth[seed] == 0.f || m_body[seed] >= 0)
|
||||
continue;
|
||||
const int id = int(m_bodies.size());
|
||||
body_nodes.assign(1, seed);
|
||||
m_body[seed] = id;
|
||||
float max_depth = 0.f;
|
||||
for (size_t k = 0; k < body_nodes.size(); ++k) {
|
||||
max_depth = std::max(max_depth, depth[body_nodes[k]]);
|
||||
for (size_t s = 0; s < steps.size(); ++s)
|
||||
if (const size_t j = body_nodes[k] + steps[s]; in_section(s) && depth[j] > 0.f && m_body[j] < 0) {
|
||||
m_body[j] = id;
|
||||
body_nodes.push_back(j);
|
||||
}
|
||||
}
|
||||
m_bodies.push_back({m_lobes.size(), 0, (std::sqrt(double(max_depth)) - 0.5) * m_cell});
|
||||
if (!lobes)
|
||||
continue;
|
||||
|
||||
std::vector<size_t> peaks;
|
||||
for (size_t i : body_nodes)
|
||||
if (depth[i] >= sqr(MinLobeRatio) * max_depth &&
|
||||
std::all_of(neighbours.begin(), neighbours.end(), [&](std::ptrdiff_t n) { return depth[i + n] <= depth[i]; }))
|
||||
peaks.push_back(i);
|
||||
std::sort(peaks.begin(), peaks.end(), [&depth](size_t a, size_t b) { return depth[a] > depth[b] || (depth[a] == depth[b] && a < b); });
|
||||
auto necked = [&](size_t a, size_t b) {
|
||||
const Vec3d pa = position(a), pb = position(b);
|
||||
const double limit = sqr(NeckRatio) * std::min(depth[a], depth[b]);
|
||||
const int samples = int(std::ceil((pb - pa).norm() / (0.5 * m_cell)));
|
||||
for (int s = 1; s < samples; ++s)
|
||||
if (depth[node_of(pa + (pb - pa) * (double(s) / samples))] < limit)
|
||||
return true;
|
||||
return false;
|
||||
};
|
||||
// A peak joins the first lobe it sees without a neck, if it is as deep. The lobes are made one at a time,
|
||||
// from the first remaining peak, testing the others in parallel.
|
||||
std::vector<std::vector<size_t>> ties;
|
||||
while (!peaks.empty()) {
|
||||
const size_t front = peaks.front();
|
||||
std::vector<char> joins(peaks.size(), 0);
|
||||
tbb::parallel_for(tbb::blocked_range<size_t>(1, peaks.size()), [&](const tbb::blocked_range<size_t> &range) {
|
||||
for (size_t k = range.begin(); k < range.end(); ++k)
|
||||
if (!necked(front, peaks[k]))
|
||||
joins[k] = std::sqrt(depth[peaks[k]]) >= std::sqrt(depth[front]) - 1.f ? 1 : 2;
|
||||
});
|
||||
std::vector<size_t> &tied = ties.emplace_back(1, front);
|
||||
std::vector<size_t> remaining;
|
||||
for (size_t k = 1; k < peaks.size(); ++k)
|
||||
if (joins[k] == 0)
|
||||
remaining.push_back(peaks[k]);
|
||||
else if (joins[k] == 1)
|
||||
tied.push_back(peaks[k]);
|
||||
peaks = std::move(remaining);
|
||||
}
|
||||
m_bodies.back().lobes = ties.size();
|
||||
for (const std::vector<size_t> &tied : ties) {
|
||||
Vec3d middle(0., 0., 0.);
|
||||
for (size_t i : tied)
|
||||
middle += position(i);
|
||||
middle /= double(tied.size());
|
||||
Vec3d center = position(tied.front());
|
||||
for (size_t i : tied)
|
||||
if ((position(i) - middle).squaredNorm() < (center - middle).squaredNorm())
|
||||
center = position(i);
|
||||
m_lobes.push_back({center, (std::sqrt(double(depth[tied.front()])) - 0.5) * m_cell, {}});
|
||||
}
|
||||
}
|
||||
throw_if_canceled();
|
||||
|
||||
if (m_mode == TpmsAdaptiveMode::SteppedShells || m_mode == TpmsAdaptiveMode::SmoothBlend || m_mode == TpmsAdaptiveMode::DistanceWarp) {
|
||||
m_depth.assign(depth.size(), 0.f);
|
||||
for (size_t i = 0; i < depth.size(); ++i)
|
||||
if (m_body[i] >= 0)
|
||||
m_depth[i] = float(std::min(1., std::max(0., std::sqrt(double(depth[i])) - 0.5) * m_cell / m_bodies[m_body[i]].depth));
|
||||
}
|
||||
|
||||
// The reach of a lobe is the first exit along each direction from its center, smoothed over the directions.
|
||||
// It is shortened towards a neighbouring lobe, from where the point is nearer to the other lobe relative to their depths.
|
||||
std::vector<int> lobe_body(m_lobes.size());
|
||||
for (size_t id = 0; id < m_bodies.size(); ++id)
|
||||
std::fill_n(lobe_body.begin() + m_bodies[id].first_lobe, m_bodies[id].lobes, int(id));
|
||||
const int rows = this->directions() / Azimuth;
|
||||
auto direction = [this](int i, int j) {
|
||||
const double azimuth = j * 2. * PI / Azimuth;
|
||||
Vec3d dir = Vec3d::Zero();
|
||||
if (m_axis < 0) {
|
||||
const double polar = (i + 0.5) * PI / Polar;
|
||||
dir = Vec3d(std::sin(polar) * std::cos(azimuth), std::sin(polar) * std::sin(azimuth), std::cos(polar));
|
||||
} else {
|
||||
dir[(m_axis + 1) % 3] = std::cos(azimuth);
|
||||
dir[(m_axis + 2) % 3] = std::sin(azimuth);
|
||||
}
|
||||
return dir;
|
||||
};
|
||||
// Two passes of a box filter over the neighbouring directions, for each of the values of a direction.
|
||||
auto smooth = [rows](std::vector<double> &values, size_t count) {
|
||||
for (int pass = 0; pass < 2; ++pass) {
|
||||
std::vector<double> smoothed(values.size(), 0.);
|
||||
for (int i = 0; i < rows; ++i)
|
||||
for (int j = 0; j < Azimuth; ++j)
|
||||
for (size_t k = 0; k < count; ++k) {
|
||||
double &sum = smoothed[size_t(i * Azimuth + j) * count + k];
|
||||
for (int di = -1; di <= 1; ++di)
|
||||
for (int dj = -1; dj <= 1; ++dj)
|
||||
sum += values[size_t(std::clamp(i + di, 0, rows - 1) * Azimuth + (j + dj + Azimuth) % Azimuth) * count + k];
|
||||
sum /= 9.;
|
||||
}
|
||||
values = std::move(smoothed);
|
||||
}
|
||||
};
|
||||
tbb::parallel_for(tbb::blocked_range<size_t>(0, m_lobes.size()), [&](const tbb::blocked_range<size_t> &range) {
|
||||
for (size_t l = range.begin(); l < range.end(); ++l) {
|
||||
const int id = lobe_body[l];
|
||||
const Body &body = m_bodies[id];
|
||||
Lobe &lobe = m_lobes[l];
|
||||
// The other lobes of the body, by the distance from the center beyond which they may be nearer.
|
||||
std::vector<std::pair<double, size_t>> others;
|
||||
for (size_t k = body.first_lobe; k < body.first_lobe + body.lobes; ++k)
|
||||
if (k != l)
|
||||
others.emplace_back((m_lobes[k].center - lobe.center).norm() / (1. + m_lobes[k].depth / lobe.depth), k);
|
||||
std::sort(others.begin(), others.end());
|
||||
auto nearer_lobe = [&](const Vec3d &pt, double r) {
|
||||
for (auto it = others.begin(); it != others.end() && it->first < r; ++it)
|
||||
if ((pt - m_lobes[it->second].center).norm() / m_lobes[it->second].depth < r / lobe.depth)
|
||||
return true;
|
||||
return false;
|
||||
};
|
||||
const double step = 0.5 * m_cell;
|
||||
std::vector<double> log_reach(this->directions());
|
||||
for (int i = 0; i < rows; ++i)
|
||||
for (int j = 0; j < Azimuth; ++j) {
|
||||
const Vec3d dir = direction(i, j);
|
||||
double r = 0.;
|
||||
double limit = InfD;
|
||||
for (;;) {
|
||||
const Vec3d pt = lobe.center + (r + step) * dir;
|
||||
const std::ptrdiff_t n = node_of(pt);
|
||||
if (n < 0 || depth[n] == 0.f || m_body[n] != id || r + step >= limit)
|
||||
break;
|
||||
if (limit == InfD && nearer_lobe(pt, r + step))
|
||||
limit = LobeReach * (r + step);
|
||||
r += step;
|
||||
}
|
||||
log_reach[i * Azimuth + j] = std::log(std::min(r + 0.5 * step, limit));
|
||||
}
|
||||
smooth(log_reach, 1);
|
||||
lobe.reach.resize(log_reach.size());
|
||||
std::transform(log_reach.begin(), log_reach.end(), lobe.reach.begin(), [](double v) { return float(std::exp(v)); });
|
||||
|
||||
if (m_mode == TpmsAdaptiveMode::DistanceWarp) {
|
||||
std::vector<double> mean(log_reach.size() * (Samples + 1));
|
||||
for (size_t d = 0; d < log_reach.size(); ++d) {
|
||||
const Vec3d dir = direction(int(d) / Azimuth, int(d) % Azimuth);
|
||||
double integral = 0.;
|
||||
double previous = this->depth(lobe.center);
|
||||
mean[d * (Samples + 1)] = previous;
|
||||
for (int k = 1; k <= Samples; ++k) {
|
||||
// Exact for a depth linear between the samples, a + b * tau, weighted by tau^2.
|
||||
const double t0 = double(k - 1) / Samples;
|
||||
const double t1 = double(k) / Samples;
|
||||
const double d1 = this->depth(lobe.center + t1 * lobe.reach[d] * dir);
|
||||
const double b = (d1 - previous) * Samples;
|
||||
const double a = previous - b * t0;
|
||||
integral += a * (std::pow(t1, 3) - std::pow(t0, 3)) / 3. + b * (std::pow(t1, 4) - std::pow(t0, 4)) / 4.;
|
||||
previous = d1;
|
||||
mean[d * (Samples + 1) + k] = 3. * integral / std::pow(t1, 3);
|
||||
}
|
||||
}
|
||||
// Smoothed like the reach: sharper profiles shear the pattern across the layer, adding lines.
|
||||
smooth(mean, Samples + 1);
|
||||
lobe.mean_depth.assign(mean.begin(), mean.end());
|
||||
}
|
||||
}
|
||||
throw_if_canceled();
|
||||
});
|
||||
|
||||
// Every other node belongs to its nearest body, within its section in the 2D modes.
|
||||
if (m_axis >= 0) {
|
||||
std::vector<bool> has_body(m_size[m_axis], false);
|
||||
for (size_t i = 0; i < m_body.size(); ++i)
|
||||
if (m_body[i] >= 0)
|
||||
has_body[m_axis == 0 ? i % sy : m_axis == 1 ? i / sy % m_size.y() : i / sz] = true;
|
||||
m_section.assign(m_size[m_axis], -1);
|
||||
for (int k = 0; k < m_size[m_axis]; ++k)
|
||||
for (int d = 0; d < m_size[m_axis] && m_section[k] < 0; ++d)
|
||||
if (k - d >= 0 && has_body[k - d])
|
||||
m_section[k] = k - d;
|
||||
else if (k + d < m_size[m_axis] && has_body[k + d])
|
||||
m_section[k] = k + d;
|
||||
}
|
||||
// Stepped shells and Smooth blend only look up the depth.
|
||||
if (!lobes) {
|
||||
m_body = {};
|
||||
return;
|
||||
}
|
||||
if (m_bodies.size() == 1) {
|
||||
std::fill(m_body.begin(), m_body.end(), 0);
|
||||
return;
|
||||
}
|
||||
std::deque<size_t> queue;
|
||||
for (size_t i = 0; i < m_body.size(); ++i)
|
||||
if (m_body[i] >= 0)
|
||||
queue.push_back(i);
|
||||
while (!queue.empty()) {
|
||||
const size_t i = queue.front();
|
||||
queue.pop_front();
|
||||
const size_t x = i % sy, y = i / sy % m_size.y(), z = i / sz;
|
||||
const std::array<bool, 6> valid{x + 1 < sy, x > 0, y + 1 < size_t(m_size.y()), y > 0, z + 1 < size_t(m_size.z()), z > 0};
|
||||
for (size_t k = 0; k < steps.size(); ++k)
|
||||
if (valid[k] && in_section(k) && m_body[i + steps[k]] < 0) {
|
||||
m_body[i + steps[k]] = m_body[i];
|
||||
queue.push_back(i + steps[k]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
double TpmsRadialField::depth(const Vec3d &pt) const
|
||||
{
|
||||
assert(!m_depth.empty());
|
||||
const Vec3d f = (pt - m_origin) / m_cell;
|
||||
std::array<int, 3> n0;
|
||||
std::array<double, 3> w;
|
||||
for (int a = 0; a < 3; ++a) {
|
||||
n0[a] = std::clamp(int(std::floor(f[a])), 0, m_size[a] - 2);
|
||||
w[a] = std::clamp(f[a] - n0[a], 0., 1.);
|
||||
}
|
||||
const size_t sy = size_t(m_size.x());
|
||||
const size_t sz = sy * size_t(m_size.y());
|
||||
double value = 0.;
|
||||
for (int c = 0; c < 8; ++c) {
|
||||
const size_t n = size_t(n0[2] + (c >> 2)) * sz + size_t(n0[1] + (c >> 1 & 1)) * sy + size_t(n0[0] + (c & 1));
|
||||
value += (c & 1 ? w[0] : 1. - w[0]) * (c >> 1 & 1 ? w[1] : 1. - w[1]) * (c >> 2 ? w[2] : 1. - w[2]) * m_depth[n];
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
Vec3d TpmsRadialField::offset(const Vec3d &pt, const Vec3d ¢er) const
|
||||
{
|
||||
Vec3d d = pt - center;
|
||||
if (m_axis >= 0)
|
||||
d[m_axis] = 0.;
|
||||
return d;
|
||||
}
|
||||
|
||||
double TpmsRadialField::radial(const Lobe &lobe, const Vec3d &pt) const
|
||||
{
|
||||
const Vec3d d = this->offset(pt, lobe.center);
|
||||
const double r = d.norm();
|
||||
// Distance warp: the radial coordinate of the linear profile with the same mean depth, which is 1 - 3/4 of it.
|
||||
auto warp = [](double mean_depth) { return std::max(0., 4. / 3. * (1. - mean_depth)); };
|
||||
if (r < EPSILON)
|
||||
return lobe.mean_depth.empty() ? 0. : warp(lobe.mean_depth.front());
|
||||
int i = 0;
|
||||
double fi = 0.;
|
||||
double azimuth;
|
||||
if (m_axis < 0) {
|
||||
const double polar = std::clamp(std::acos(std::clamp(d.z() / r, -1., 1.)) / PI * Polar - 0.5, 0., double(Polar - 1));
|
||||
i = std::min(int(polar), Polar - 2);
|
||||
fi = polar - i;
|
||||
azimuth = std::atan2(d.y(), d.x());
|
||||
} else
|
||||
azimuth = std::atan2(d[(m_axis + 2) % 3], d[(m_axis + 1) % 3]);
|
||||
azimuth *= Azimuth / (2. * PI);
|
||||
if (azimuth < 0.)
|
||||
azimuth += Azimuth;
|
||||
const int j0 = int(azimuth) % Azimuth;
|
||||
const int j1 = (j0 + 1) % Azimuth;
|
||||
const double fj = azimuth - std::floor(azimuth);
|
||||
auto at = [&lobe](int i, int j) { return double(lobe.reach[i * Azimuth + j]); };
|
||||
double reach = at(i, j0) * (1. - fj) + at(i, j1) * fj;
|
||||
if (fi > 0.)
|
||||
reach = reach * (1. - fi) + (at(i + 1, j0) * (1. - fj) + at(i + 1, j1) * fj) * fi;
|
||||
const double tau = r / reach;
|
||||
if (lobe.mean_depth.empty())
|
||||
return tau;
|
||||
// Beyond the surface, the depth is zero.
|
||||
auto mean_at = [&lobe, tau](int i, int j) {
|
||||
const float *mean = lobe.mean_depth.data() + size_t(i * Azimuth + j) * (Samples + 1);
|
||||
if (tau >= 1.)
|
||||
return double(mean[Samples]) / (tau * tau * tau);
|
||||
const double x = tau * Samples;
|
||||
const int k = std::min(int(x), Samples - 1);
|
||||
return mean[k] + (mean[k + 1] - mean[k]) * (x - k);
|
||||
};
|
||||
double mean = mean_at(i, j0) * (1. - fj) + mean_at(i, j1) * fj;
|
||||
if (fi > 0.)
|
||||
mean = mean * (1. - fi) + (mean_at(i + 1, j0) * (1. - fj) + mean_at(i + 1, j1) * fj) * fi;
|
||||
return warp(mean);
|
||||
}
|
||||
|
||||
size_t TpmsRadialField::radial(const Vec3d &pt, Radials &out) const
|
||||
{
|
||||
assert(!this->empty());
|
||||
Vec3i32 idx;
|
||||
for (int axis = 0; axis < 3; ++axis)
|
||||
idx[axis] = std::clamp<int>(int(std::lround((pt[axis] - m_origin[axis]) / m_cell)), 0, m_size[axis] - 1);
|
||||
auto node = [this](const Vec3i32 &idx) { return (size_t(idx.z()) * m_size.y() + idx.y()) * m_size.x() + idx.x(); };
|
||||
if (m_axis < 0)
|
||||
return this->body_radial(node(idx), pt, 1.f, out.data());
|
||||
|
||||
// The sections around pt, or the nearest ones with a body.
|
||||
const double f = std::clamp((pt[m_axis] - m_origin[m_axis]) / m_cell, 0., double(m_size[m_axis] - 1));
|
||||
const int k = std::min(int(f), m_size[m_axis] - 2);
|
||||
const float w = float(f - k);
|
||||
size_t count = 0;
|
||||
if (w < 1.f) {
|
||||
idx[m_axis] = m_section[k];
|
||||
count += this->body_radial(node(idx), pt, 1.f - w, out.data());
|
||||
}
|
||||
if (w > 0.f) {
|
||||
idx[m_axis] = m_section[k + 1];
|
||||
count += this->body_radial(node(idx), pt, w, out.data() + count);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
size_t TpmsRadialField::body_radial(size_t node, const Vec3d &pt, float weight, Radial *out) const
|
||||
{
|
||||
const Body &body = m_bodies[m_body[node]];
|
||||
if (body.lobes == 1) {
|
||||
const Lobe &lobe = m_lobes[body.first_lobe];
|
||||
out[0] = {lobe.center, radial(lobe, pt), weight};
|
||||
return 1;
|
||||
}
|
||||
|
||||
// The lobes nearest relative to their depth; they morph into each other near the sides where they are as near.
|
||||
std::array<std::pair<double, size_t>, MaxMorph> nearest;
|
||||
size_t count = 0;
|
||||
for (size_t l = body.first_lobe; l < body.first_lobe + body.lobes; ++l) {
|
||||
const double d = this->offset(pt, m_lobes[l].center).norm() / m_lobes[l].depth;
|
||||
if (count < MaxMorph)
|
||||
nearest[count++] = {d, l};
|
||||
else if (d < nearest.back().first)
|
||||
nearest.back() = {d, l};
|
||||
else
|
||||
continue;
|
||||
for (size_t k = count - 1; k > 0 && nearest[k].first < nearest[k - 1].first; --k)
|
||||
std::swap(nearest[k], nearest[k - 1]);
|
||||
}
|
||||
std::array<double, MaxMorph> blend;
|
||||
double total = 0.;
|
||||
size_t morphs = 0;
|
||||
for (; morphs < count; ++morphs) {
|
||||
const double u = 0.5 - (nearest[morphs].first - nearest[0].first) / LobeMorph;
|
||||
if (u <= 0.)
|
||||
break;
|
||||
blend[morphs] = u * u * (3. - 2. * u);
|
||||
total += blend[morphs];
|
||||
}
|
||||
for (size_t k = 0; k < morphs; ++k) {
|
||||
const Lobe &lobe = m_lobes[nearest[k].second];
|
||||
out[k] = {lobe.center, radial(lobe, pt), float(weight * blend[k] / total)};
|
||||
}
|
||||
return morphs;
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
namespace marchsq {
|
||||
using namespace Slic3r;
|
||||
|
||||
struct AdaptiveTpmsField
|
||||
{
|
||||
static constexpr float gsizef = 0.40f; // grid cell size in mm (roughly line segment length).
|
||||
static constexpr float rsizef = 0.004f; // raster pixel size in mm (roughly point accuracy).
|
||||
const coord_t rsize = scaled(rsizef);
|
||||
const long gsize = std::lround(gsizef / rsizef);
|
||||
|
||||
const AdaptiveTpms &tpms;
|
||||
const TpmsRadialField &radial_field;
|
||||
RadialScale scale;
|
||||
DensityLevels levels;
|
||||
Point size;
|
||||
Point offs;
|
||||
double z;
|
||||
double cos_angle;
|
||||
double sin_angle;
|
||||
|
||||
AdaptiveTpmsField(const AdaptiveTpms &tpms, const TpmsRadialField &radial_field, const BoundingBox &bbox, coordf_t z, float angle)
|
||||
: tpms(tpms), radial_field(radial_field), scale(tpms, radial_field.axis() < 0 ? 3 : 2)
|
||||
, levels(std::max(tpms.interior_frequency / tpms.surface_frequency, 1e-3), BlendRatio, tpms.gradient)
|
||||
, size(bbox.size()), offs(bbox.min), z(z)
|
||||
, cos_angle(std::cos(angle)), sin_angle(std::sin(angle))
|
||||
{}
|
||||
|
||||
// The pattern is scaled around the center of the lobe, morphing into the pattern of a neighbouring lobe near the
|
||||
// side between them. In the 2D modes only within the section, with the interior frequency along the axis.
|
||||
// The radial field is in the object frame, the fill is rotated by -angle.
|
||||
float get_scalar(const Coord &p) const
|
||||
{
|
||||
const Point pt = to_Point(p);
|
||||
const double x = unscaled(pt.x());
|
||||
const double y = unscaled(pt.y());
|
||||
const Vec3d obj(cos_angle * x - sin_angle * y, sin_angle * x + cos_angle * y, z);
|
||||
if (radial_field.mode() == TpmsAdaptiveMode::SmoothBlend) {
|
||||
// The regular patterns of the two levels around the target of the depth, blended by a smoothstep.
|
||||
auto lattice = [this, x, y](int level) {
|
||||
const double frequency = tpms.surface_frequency * levels.scale(level);
|
||||
return tpms.equation(float(frequency * x), float(frequency * y), float(frequency * z));
|
||||
};
|
||||
if (levels.count == 0)
|
||||
return lattice(0);
|
||||
const double c = std::clamp(levels.level(radial_field.depth(obj)), 0., double(levels.count));
|
||||
const int k = std::min(int(c), levels.count - 1);
|
||||
const double u = c - k;
|
||||
const double w = u * u * (3. - 2. * u);
|
||||
return float((w < 1. ? (1. - w) * lattice(k) : 0.) + (w > 0. ? w * lattice(k + 1) : 0.));
|
||||
}
|
||||
const int axis = radial_field.axis();
|
||||
TpmsRadialField::Radials radials;
|
||||
const size_t count = radial_field.radial(obj, radials);
|
||||
float value = 0.f;
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
const auto &[center, t, weight] = radials[i];
|
||||
Vec3d q = tpms.surface_frequency * scale(t) * (obj - center);
|
||||
if (axis >= 0)
|
||||
q[axis] = tpms.interior_frequency * obj[axis];
|
||||
value += weight * tpms.equation(float(cos_angle * q.x() + sin_angle * q.y()), float(cos_angle * q.y() - sin_angle * q.x()), float(q.z()));
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
inline coord_t to_coord(long x) const { return x * rsize; }
|
||||
inline long to_coordr(coord_t x) const { return x / rsize; }
|
||||
inline Point to_Point(const Coord &p) const { return Point(to_coord(p.c) + offs.x(), to_coord(p.r) + offs.y()); }
|
||||
};
|
||||
|
||||
template<> struct _RasterTraits<AdaptiveTpmsField>
|
||||
{
|
||||
using ValueType = float;
|
||||
static float get(const AdaptiveTpmsField &sf, size_t row, size_t col) { return sf.get_scalar(Coord(long(row), long(col))); }
|
||||
static size_t rows(const AdaptiveTpmsField &sf) { return sf.to_coordr(sf.size.y()); }
|
||||
static size_t cols(const AdaptiveTpmsField &sf) { return sf.to_coordr(sf.size.x()); }
|
||||
};
|
||||
|
||||
// Continuous density level of the depth over a layer, in the object frame.
|
||||
struct TpmsLevelField
|
||||
{
|
||||
static constexpr float gsizef = 0.5f;
|
||||
static constexpr float rsizef = 0.05f;
|
||||
const coord_t rsize = scaled(rsizef);
|
||||
const long gsize = std::lround(gsizef / rsizef);
|
||||
|
||||
const TpmsRadialField &field;
|
||||
const DensityLevels &levels;
|
||||
Point size;
|
||||
Point offs;
|
||||
double z;
|
||||
|
||||
TpmsLevelField(const TpmsRadialField &field, const DensityLevels &levels, const BoundingBox &bbox, coordf_t z)
|
||||
: field(field), levels(levels), size(bbox.size()), offs(bbox.min), z(z)
|
||||
{}
|
||||
|
||||
float get_scalar(const Coord &p) const
|
||||
{
|
||||
const Point pt = to_Point(p);
|
||||
return float(levels.level(field.depth(Vec3d(unscaled(pt.x()), unscaled(pt.y()), z))));
|
||||
}
|
||||
|
||||
inline coord_t to_coord(long x) const { return x * rsize; }
|
||||
inline long to_coordr(coord_t x) const { return x / rsize; }
|
||||
inline Point to_Point(const Coord &p) const { return Point(to_coord(p.c) + offs.x(), to_coord(p.r) + offs.y()); }
|
||||
};
|
||||
|
||||
template<> struct _RasterTraits<TpmsLevelField>
|
||||
{
|
||||
using ValueType = float;
|
||||
static float get(const TpmsLevelField &sf, size_t row, size_t col) { return sf.get_scalar(Coord(long(row), long(col))); }
|
||||
static size_t rows(const TpmsLevelField &sf) { return sf.to_coordr(sf.size.y()); }
|
||||
static size_t cols(const TpmsLevelField &sf) { return sf.to_coordr(sf.size.x()); }
|
||||
};
|
||||
|
||||
} // namespace marchsq
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
Polylines make_adaptive_tpms(const AdaptiveTpms &tpms, const TpmsRadialField &field, BoundingBox bbox,
|
||||
coordf_t z, coordf_t layer_height, coordf_t spacing, float angle)
|
||||
{
|
||||
// A cell of margin for the rings closed along the raster border, and a fixed sampling grid for every region.
|
||||
const coord_t cell = scaled(marchsq::AdaptiveTpmsField::gsizef);
|
||||
bbox.offset(cell);
|
||||
bbox.merge(align_to_grid(bbox.min, Point(cell, cell)));
|
||||
const marchsq::AdaptiveTpmsField raster(tpms, field, bbox, z - 0.5 * layer_height, angle);
|
||||
const std::vector<marchsq::Ring> rings = marchsq::execute_with_policy(ex_tbb, raster, 0.f, {raster.gsize, raster.gsize});
|
||||
|
||||
// Loops narrower than two lines print as blobs.
|
||||
const double min_loop_length = scaled(2. * PI * spacing);
|
||||
Polylines polylines;
|
||||
polylines.reserve(rings.size());
|
||||
for (const marchsq::Ring &ring : rings) {
|
||||
Polyline polyline;
|
||||
polyline.points.reserve(ring.size() + 1);
|
||||
for (const marchsq::Coord &crd : ring)
|
||||
polyline.points.emplace_back(raster.to_Point(crd));
|
||||
polyline.points.push_back(polyline.points.front());
|
||||
polyline.simplify(SCALED_SPARSE_INFILL_RESOLUTION);
|
||||
if (polyline.length() >= min_loop_length)
|
||||
polylines.push_back(std::move(polyline));
|
||||
}
|
||||
return polylines;
|
||||
}
|
||||
|
||||
std::vector<TpmsShell> make_tpms_shells(const TpmsRadialField &field, const ExPolygon &expolygon, coordf_t z,
|
||||
float surface_density, float interior_density, TpmsAdaptiveGradient gradient)
|
||||
{
|
||||
const DensityLevels levels(interior_density / surface_density, ShellRatio, gradient);
|
||||
if (levels.count == 0)
|
||||
return {{surface_density, {expolygon}}};
|
||||
// A fixed sampling grid, so that every region of a layer gets the same shells.
|
||||
const coord_t cell = scaled(marchsq::TpmsLevelField::gsizef);
|
||||
BoundingBox bbox = get_extents(expolygon);
|
||||
bbox.offset(cell);
|
||||
bbox.merge(align_to_grid(bbox.min, Point(cell, cell)));
|
||||
const marchsq::TpmsLevelField raster(field, levels, bbox, z);
|
||||
|
||||
// Each level takes the part deeper than the middle between it and the previous one.
|
||||
std::vector<TpmsShell> shells;
|
||||
ExPolygons remaining{expolygon};
|
||||
for (int level = 0; level < levels.count && !remaining.empty(); ++level) {
|
||||
Polygons deeper;
|
||||
for (const marchsq::Ring &ring : marchsq::execute_with_policy(ex_tbb, raster, float(level + 0.5), {raster.gsize, raster.gsize})) {
|
||||
Polygon &polygon = deeper.emplace_back();
|
||||
polygon.points.reserve(ring.size());
|
||||
for (const marchsq::Coord &crd : ring)
|
||||
polygon.points.emplace_back(raster.to_Point(crd));
|
||||
}
|
||||
ExPolygons inner = intersection_ex(union_ex(deeper), remaining);
|
||||
shells.push_back({float(surface_density * levels.scale(level)), diff_ex(remaining, inner)});
|
||||
remaining = std::move(inner);
|
||||
}
|
||||
if (!remaining.empty())
|
||||
shells.push_back({interior_density, std::move(remaining)});
|
||||
return shells;
|
||||
}
|
||||
|
||||
void fill_tpms_shells(const TpmsRadialField &field, const ExPolygon &expolygon, coordf_t z, const FillParams ¶ms, coordf_t spacing,
|
||||
const std::function<void(const FillParams &, const ExPolygon &)> &fill_shell)
|
||||
{
|
||||
FillParams shell_params = params;
|
||||
shell_params.tpms_adaptive = TpmsAdaptiveMode::Disabled;
|
||||
for (const TpmsShell &shell : make_tpms_shells(field, expolygon, z, params.density, params.tpms_interior_density, params.tpms_adaptive_gradient)) {
|
||||
shell_params.density = shell.density;
|
||||
for (const ExPolygon &part : offset_ex(shell.expolygons, -float(scale_(0.5 * spacing))))
|
||||
fill_shell(shell_params, part);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,143 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "../libslic3r.h"
|
||||
#include "FillBase.hpp"
|
||||
#include "../BoundingBox.hpp"
|
||||
#include "../ExPolygon.hpp"
|
||||
#include "../Point.hpp"
|
||||
#include "../Polyline.hpp"
|
||||
#include "../PrintConfig.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// Radial coordinate inside the lobes of the bodies of an object, sampled from its slices: 0 at the center of a
|
||||
// lobe, 1 at its surface. Lobes are parts of a body separated by a neck, like two spheres united. In the 2D modes,
|
||||
// every section normal to the axis has its own bodies and lobes, and distances are measured within the section. The
|
||||
// modes following the distance to the surface use the depth of every point instead; Distance warp also the lobes.
|
||||
class TpmsRadialField
|
||||
{
|
||||
public:
|
||||
struct Slice
|
||||
{
|
||||
coordf_t bottom_z;
|
||||
coordf_t top_z;
|
||||
const ExPolygons *expolygons;
|
||||
};
|
||||
|
||||
struct Radial
|
||||
{
|
||||
Vec3d center;
|
||||
double t;
|
||||
float weight;
|
||||
};
|
||||
|
||||
// Slices sorted by z, in the XY coordinates of the fill and the print Z.
|
||||
TpmsRadialField(const std::vector<Slice> &slices, const BoundingBox &bbox, TpmsAdaptiveMode mode,
|
||||
const std::function<void()> &throw_if_canceled);
|
||||
|
||||
// Lobes blended near the sides between them, from a body or from each of the two sections around a point.
|
||||
static constexpr size_t MaxMorph = 4;
|
||||
using Radials = std::array<Radial, 2 * MaxMorph>;
|
||||
|
||||
// Without a body, as when the object is thinner than the grid cells.
|
||||
bool empty() const { return m_bodies.empty(); }
|
||||
|
||||
// Radial coordinates of pt in unscaled coordinates towards the lobe it belongs to, and towards the neighbouring
|
||||
// lobes near the sides between them, with weights summing to 1. In the 2D modes, those of the two sections around
|
||||
// pt. Returns their count.
|
||||
size_t radial(const Vec3d &pt, Radials &out) const;
|
||||
|
||||
// Axis normal to the sections in the 2D modes, -1 in the modes graded in 3D.
|
||||
int axis() const { return m_axis; }
|
||||
TpmsAdaptiveMode mode() const { return m_mode; }
|
||||
|
||||
// In the modes following the distance to the surface: depth relative to the deepest point of the body, from 0 at
|
||||
// the surface to 1.
|
||||
double depth(const Vec3d &pt) const;
|
||||
|
||||
private:
|
||||
struct Lobe
|
||||
{
|
||||
Vec3d center;
|
||||
double depth;
|
||||
// Distance from the center to the surface on a latitude-longitude grid of directions.
|
||||
std::vector<float> reach;
|
||||
// Distance warp: mean depth over the ball along each direction, sampled up to the reach.
|
||||
std::vector<float> mean_depth;
|
||||
};
|
||||
|
||||
struct Body
|
||||
{
|
||||
size_t first_lobe;
|
||||
size_t lobes;
|
||||
// Distance from the deepest point to the surface.
|
||||
double depth;
|
||||
};
|
||||
|
||||
double radial(const Lobe &lobe, const Vec3d &pt) const;
|
||||
size_t body_radial(size_t node, const Vec3d &pt, float weight, Radial *out) const;
|
||||
// Offset of pt from a center, within the section in the 2D modes.
|
||||
Vec3d offset(const Vec3d &pt, const Vec3d ¢er) const;
|
||||
int directions() const { return m_axis < 0 ? Polar * Azimuth : Azimuth; }
|
||||
|
||||
// Directions of the reach of a lobe, on a latitude-longitude grid, or a circle in the 2D modes.
|
||||
static constexpr int Polar = 24;
|
||||
static constexpr int Azimuth = 48;
|
||||
|
||||
TpmsAdaptiveMode m_mode;
|
||||
int m_axis;
|
||||
Vec3d m_origin;
|
||||
double m_cell;
|
||||
Vec3i32 m_size;
|
||||
// Nearest body of every grid node.
|
||||
std::vector<int> m_body;
|
||||
std::vector<Body> m_bodies;
|
||||
std::vector<Lobe> m_lobes;
|
||||
// In the 2D modes, the nearest section with a body to every section.
|
||||
std::vector<int> m_section;
|
||||
// In the modes following the distance to the surface, the depth of every grid node.
|
||||
std::vector<float> m_depth;
|
||||
};
|
||||
|
||||
using TpmsRadialFieldPtr = std::unique_ptr<TpmsRadialField>;
|
||||
// A field for every adaptive mode in use, indexed by the mode.
|
||||
using TpmsRadialFields = std::array<TpmsRadialFieldPtr, size_t(TpmsAdaptiveMode::Count)>;
|
||||
|
||||
struct AdaptiveTpms
|
||||
{
|
||||
// Implicit TPMS equation with a period of 2 PI.
|
||||
float (*equation)(float x, float y, float z);
|
||||
// Pattern frequencies at the surface and at the center, in radians per mm.
|
||||
double surface_frequency;
|
||||
double interior_frequency;
|
||||
TpmsAdaptiveGradient gradient;
|
||||
};
|
||||
|
||||
// Infill lines in the fill frame, the object frame rotated by -angle; z is the print_z of the layer.
|
||||
Polylines make_adaptive_tpms(const AdaptiveTpms &tpms, const TpmsRadialField &field, BoundingBox bbox,
|
||||
coordf_t z, coordf_t layer_height, coordf_t spacing, float angle);
|
||||
|
||||
struct TpmsShell
|
||||
{
|
||||
float density;
|
||||
ExPolygons expolygons;
|
||||
};
|
||||
|
||||
// Stepped shells: the parts of an expolygon in the object frame at each density, from the surface inwards; z is the
|
||||
// middle of the layer.
|
||||
std::vector<TpmsShell> make_tpms_shells(const TpmsRadialField &field, const ExPolygon &expolygon, coordf_t z,
|
||||
float surface_density, float interior_density, TpmsAdaptiveGradient gradient);
|
||||
|
||||
// Stepped shells: fills every shell with fill_shell at its density, each shrunk by half a line like a filled region,
|
||||
// so the lines connected along the boundaries of two shells don't overlap.
|
||||
void fill_tpms_shells(const TpmsRadialField &field, const ExPolygon &expolygon, coordf_t z, const FillParams ¶ms, coordf_t spacing,
|
||||
const std::function<void(const FillParams &, const ExPolygon &)> &fill_shell);
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -14,8 +14,10 @@
|
||||
#include "libslic3r/Fill/FillBase.hpp"
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include "libslic3r/Polygon.hpp"
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
#include "libslic3r/libslic3r.h"
|
||||
#include "FillTpmsD.hpp"
|
||||
#include "FillTpmsAdaptive.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
@@ -23,6 +25,11 @@ static double scaled_floor(double x,double scale){
|
||||
return std::floor(x/scale)*scale;
|
||||
}
|
||||
|
||||
static float schwarz_d(float x, float y, float z)
|
||||
{
|
||||
return std::sin(x) * std::sin(y) * std::sin(z) - std::cos(x) * std::cos(y) * std::cos(z);
|
||||
}
|
||||
|
||||
static Polylines make_waves(double gridZ, double density_adjusted, double line_spacing, double width, double height)
|
||||
{
|
||||
const double scaleFactor = scale_(line_spacing) / density_adjusted;
|
||||
@@ -110,31 +117,49 @@ void FillTpmsD::_fill_surface_single(
|
||||
ExPolygon expolygon,
|
||||
Polylines &polylines_out)
|
||||
{
|
||||
if (params.tpms_adaptive == TpmsAdaptiveMode::SteppedShells && this->tpms_radial_field != nullptr) {
|
||||
fill_tpms_shells(*this->tpms_radial_field, expolygon, this->z - 0.5 * params.layer_height, params, this->spacing,
|
||||
[&](const FillParams &shell_params, const ExPolygon &shell) {
|
||||
this->_fill_surface_single(shell_params, thickness_layers, direction, shell, polylines_out);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
auto infill_angle = float(this->angle + (CorrectionAngle * 2*M_PI) / 360.);
|
||||
if(std::abs(infill_angle) >= EPSILON)
|
||||
expolygon.rotate(-infill_angle);
|
||||
|
||||
BoundingBox bb = expolygon.contour.bounding_box();
|
||||
// Density adjusted to have a good %of weight.
|
||||
double density_adjusted = std::max(0., params.density * DensityAdjust / params.multiline);
|
||||
// Distance between the gyroid waves in scaled coordinates.
|
||||
coord_t distance = coord_t(scale_(this->spacing) / density_adjusted);
|
||||
Polylines polylines;
|
||||
if (params.tpms_adaptive != TpmsAdaptiveMode::Disabled && this->tpms_radial_field != nullptr) {
|
||||
// Radians per mm of the regular pattern at a density.
|
||||
auto frequency = [¶ms, this](double density) { return density * DensityAdjust / (params.multiline * this->spacing); };
|
||||
BoundingBox bbox = expolygon.contour.bounding_box();
|
||||
bbox.offset(scale_((params.multiline + 1) * this->spacing));
|
||||
polylines = make_adaptive_tpms({schwarz_d, frequency(params.density), frequency(params.tpms_interior_density), params.tpms_adaptive_gradient},
|
||||
*this->tpms_radial_field, bbox, this->z, params.layer_height, this->spacing, infill_angle);
|
||||
} else {
|
||||
BoundingBox bb = expolygon.contour.bounding_box();
|
||||
// Density adjusted to have a good %of weight.
|
||||
double density_adjusted = std::max(0., params.density * DensityAdjust / params.multiline);
|
||||
// Distance between the gyroid waves in scaled coordinates.
|
||||
coord_t distance = coord_t(scale_(this->spacing) / density_adjusted);
|
||||
|
||||
// align bounding box to a multiple of our grid module
|
||||
bb.merge(align_to_grid(bb.min, Point(2*M_PI*distance, 2*M_PI*distance)));
|
||||
// align bounding box to a multiple of our grid module
|
||||
bb.merge(align_to_grid(bb.min, Point(2*M_PI*distance, 2*M_PI*distance)));
|
||||
|
||||
// generate pattern
|
||||
Polylines polylines = make_waves(
|
||||
scale_(this->z),
|
||||
density_adjusted,
|
||||
this->spacing,
|
||||
ceil(bb.size()(0) / distance) + 1.,
|
||||
ceil(bb.size()(1) / distance) + 1.);
|
||||
// generate pattern
|
||||
polylines = make_waves(
|
||||
scale_(this->z),
|
||||
density_adjusted,
|
||||
this->spacing,
|
||||
ceil(bb.size()(0) / distance) + 1.,
|
||||
ceil(bb.size()(1) / distance) + 1.);
|
||||
|
||||
// shift the polyline to the grid origin
|
||||
for (Polyline &pl : polylines)
|
||||
pl.translate(bb.min);
|
||||
}
|
||||
|
||||
// shift the polyline to the grid origin
|
||||
for (Polyline &pl : polylines)
|
||||
pl.translate(bb.min);
|
||||
|
||||
// Apply multiline offset if needed
|
||||
multiline_fill(polylines, params, spacing);
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ public:
|
||||
Polylines& polylines_out) override;
|
||||
|
||||
bool is_self_crossing() override { return false; }
|
||||
bool aligned_to_origin() const override { return true; }
|
||||
|
||||
// Density adjustment to have a good %of weight.
|
||||
static constexpr double DensityAdjust = 2.1;
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "libslic3r/Fill/FillBase.hpp"
|
||||
#include "libslic3r/ExPolygon.hpp"
|
||||
#include "FillTpmsFK.hpp"
|
||||
#include "FillTpmsAdaptive.hpp"
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
@@ -17,6 +18,18 @@
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
#include "libslic3r/Polygon.hpp"
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// Fischer - Koch S equation:
|
||||
// cos(2x)sin(y)cos(z) + cos(2y)sin(z)cos(x) + cos(2z)sin(x)cos(y) = 0
|
||||
static float fischer_koch(float x, float y, float z)
|
||||
{
|
||||
return cosf(2 * x) * sinf(y) * cosf(z) + cosf(2 * y) * sinf(z) * cosf(x) + cosf(2 * z) * sinf(x) * cosf(y);
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
namespace marchsq {
|
||||
using namespace Slic3r;
|
||||
@@ -42,16 +55,7 @@ struct ScalarField
|
||||
{}
|
||||
|
||||
// Get the scalar field value at x,y,z in coordf_t coordinates.
|
||||
float get_scalar(coordf_t x, coordf_t y, coordf_t z) const
|
||||
{
|
||||
const float fx = freq * x;
|
||||
const float fy = freq * y;
|
||||
const float fz = freq * z;
|
||||
|
||||
// Fischer - Koch S equation:
|
||||
// cos(2x)sin(y)cos(z) + cos(2y)sin(z)cos(x) + cos(2z)sin(x)cos(y) = 0
|
||||
return cosf(2 * fx) * sinf(fy) * cosf(fz) + cosf(2 * fy) * sinf(fz) * cosf(fx) + cosf(2 * fz) * sinf(fx) * cosf(fy);
|
||||
}
|
||||
float get_scalar(coordf_t x, coordf_t y, coordf_t z) const { return fischer_koch(freq * x, freq * y, freq * z); }
|
||||
|
||||
// Get the scalar field value at a Coord for the current z value.
|
||||
float get_scalar(Coord p) const
|
||||
@@ -128,20 +132,34 @@ void FillTpmsFK::_fill_surface_single(const FillParams& params,
|
||||
ExPolygon expolygon,
|
||||
Polylines& polylines_out)
|
||||
{
|
||||
if (params.tpms_adaptive == TpmsAdaptiveMode::SteppedShells && this->tpms_radial_field != nullptr) {
|
||||
fill_tpms_shells(*this->tpms_radial_field, expolygon, this->z - 0.5 * params.layer_height, params, this->spacing,
|
||||
[&](const FillParams &shell_params, const ExPolygon &shell) {
|
||||
this->_fill_surface_single(shell_params, thickness_layers, direction, shell, polylines_out);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
auto infill_angle = float(this->angle + (CorrectionAngle * 2 * M_PI) / 360.);
|
||||
if (std::abs(infill_angle) >= EPSILON)
|
||||
expolygon.rotate(-infill_angle);
|
||||
|
||||
float density_factor = std::min(0.9f, params.density);
|
||||
// Density (field period) adjusted to have a good %of weight.
|
||||
const float vari_T = 4.18f * spacing * params.multiline / density_factor;
|
||||
auto period = [¶ms, this](float density) { return 4.18f * spacing * params.multiline / std::min(0.9f, density); };
|
||||
|
||||
BoundingBox bbox = expolygon.contour.bounding_box();
|
||||
// Enlarge the bounding box by the multi-line width to avoid artifacts at the edges.
|
||||
bbox.offset(scale_((params.multiline + 1) * spacing));
|
||||
marchsq::ScalarField sf = marchsq::ScalarField(bbox, this->z, vari_T);
|
||||
// Get simplified lines using coarse tolerance of 0.1mm (this is infill).
|
||||
Polylines polylines = marchsq::get_polylines(sf, SCALED_SPARSE_INFILL_RESOLUTION);
|
||||
Polylines polylines;
|
||||
if (params.tpms_adaptive != TpmsAdaptiveMode::Disabled && this->tpms_radial_field != nullptr) {
|
||||
polylines = make_adaptive_tpms({fischer_koch, 2. * PI / period(params.density), 2. * PI / period(params.tpms_interior_density),
|
||||
params.tpms_adaptive_gradient},
|
||||
*this->tpms_radial_field, bbox, this->z, params.layer_height, spacing, infill_angle);
|
||||
} else {
|
||||
marchsq::ScalarField sf = marchsq::ScalarField(bbox, this->z, period(params.density));
|
||||
// Get simplified lines using coarse tolerance of 0.1mm (this is infill).
|
||||
polylines = marchsq::get_polylines(sf, SCALED_SPARSE_INFILL_RESOLUTION);
|
||||
}
|
||||
|
||||
// Apply multiline offset if needed
|
||||
multiline_fill(polylines, params, spacing);
|
||||
|
||||
@@ -31,6 +31,7 @@ public:
|
||||
Polylines& polylines_out) override;
|
||||
|
||||
bool is_self_crossing() override { return false; }
|
||||
bool aligned_to_origin() const override { return true; }
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -241,14 +241,14 @@ Flow support_material_flow(const PrintObject *object, float layer_height)
|
||||
// The width parameter accepted by new_from_config_width is of type ConfigOptionFloatOrPercent, the Flow class takes care of the percent to value substitution.
|
||||
(object->config().support_line_width.value > 0) ? object->config().support_line_width : object->config().line_width,
|
||||
// if object->config().support_filament == 0 (which means to not trigger tool change, but use the current extruder instead), get_at will return the 0th component.
|
||||
float(object->print()->config().nozzle_diameter.get_at(object->config().support_filament-1)),
|
||||
float(nozzle_diameter_for_filament(object->print()->config(), object->config().support_filament, object->print()->is_BBL_printer())),
|
||||
(layer_height > 0.f) ? layer_height : float(object->config().layer_height.value));
|
||||
}
|
||||
//BBS
|
||||
Flow support_transition_flow(const PrintObject* object)
|
||||
{
|
||||
//BBS: support transition of tree support is bridge flow
|
||||
float dmr = float(object->print()->config().nozzle_diameter.get_at(object->config().support_filament - 1));
|
||||
float dmr = float(nozzle_diameter_for_filament(object->print()->config(), object->config().support_filament, object->print()->is_BBL_printer()));
|
||||
return Flow::bridging_flow(dmr, dmr);
|
||||
}
|
||||
|
||||
@@ -260,7 +260,7 @@ Flow support_material_1st_layer_flow(const PrintObject *object, float layer_heig
|
||||
frSupportMaterial,
|
||||
// The width parameter accepted by new_from_config_width is of type ConfigOptionFloatOrPercent, the Flow class takes care of the percent to value substitution.
|
||||
(width.value > 0) ? width : object->config().line_width,
|
||||
float(print_config.nozzle_diameter.get_at(object->config().support_filament-1)),
|
||||
float(nozzle_diameter_for_filament(print_config, object->config().support_filament, object->print()->is_BBL_printer())),
|
||||
(layer_height > 0.f) ? layer_height : float(print_config.initial_layer_print_height.value));
|
||||
}
|
||||
|
||||
@@ -271,7 +271,7 @@ Flow support_material_interface_flow(const PrintObject *object, float layer_heig
|
||||
// The width parameter accepted by new_from_config_width is of type ConfigOptionFloatOrPercent, the Flow class takes care of the percent to value substitution.
|
||||
(object->config().support_line_width > 0) ? object->config().support_line_width : object->config().line_width,
|
||||
// if object->config().support_interface_filament == 0 (which means to not trigger tool change, but use the current extruder instead), get_at will return the 0th component.
|
||||
float(object->print()->config().nozzle_diameter.get_at(object->config().support_interface_filament-1)),
|
||||
float(nozzle_diameter_for_filament(object->print()->config(), object->config().support_interface_filament, object->print()->is_BBL_printer())),
|
||||
(layer_height > 0.f) ? layer_height : float(object->config().layer_height.value));
|
||||
}
|
||||
|
||||
|
||||
@@ -314,8 +314,13 @@ void AMFParserContext::startElement(const char *name, const char **atts)
|
||||
case 2:
|
||||
if (strcmp(name, "metadata") == 0) {
|
||||
if (m_path[1] == NODE_TYPE_MATERIAL || m_path[1] == NODE_TYPE_OBJECT) {
|
||||
m_value[0] = get_attribute(atts, "type");
|
||||
node_type_new = NODE_TYPE_METADATA;
|
||||
const char *type = get_attribute(atts, "type");
|
||||
if (type == nullptr)
|
||||
this->stop();
|
||||
else {
|
||||
m_value[0] = type;
|
||||
node_type_new = NODE_TYPE_METADATA;
|
||||
}
|
||||
}
|
||||
}/* else if (strcmp(name, "layer_config_ranges") == 0 && m_path[1] == NODE_TYPE_OBJECT)
|
||||
node_type_new = NODE_TYPE_LAYER_CONFIG;*/
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
@@ -55,7 +55,6 @@
|
||||
#include "TDataStd_Name.hxx"
|
||||
#include "BRepBuilderAPI_Transform.hxx"
|
||||
#include "TopExp_Explorer.hxx"
|
||||
#include "TopExp_Explorer.hxx"
|
||||
#include "BRep_Tool.hxx"
|
||||
#include "BRepTools.hxx"
|
||||
#include <IMeshTools_Parameters.hxx>
|
||||
@@ -237,19 +236,29 @@ static void getNamedSolids(const TopLoc_Location& location,
|
||||
int i = 0;
|
||||
switch (shape_type) {
|
||||
case TopAbs_COMPOUND:
|
||||
if (!isSplitCompound) {
|
||||
namedSolids.emplace_back(TopoDS::Compound(transform.Shape()), fullName);
|
||||
break;
|
||||
}
|
||||
case TopAbs_COMPSOLID:
|
||||
if (!isSplitCompound) {
|
||||
namedSolids.emplace_back(TopoDS::CompSolid(transform.Shape()), fullName);
|
||||
namedSolids.emplace_back(transform.Shape(), fullName);
|
||||
} else {
|
||||
for (explorer.Init(transform.Shape(), TopAbs_SOLID); explorer.More(); explorer.Next()) {
|
||||
i++;
|
||||
const TopoDS_Shape& currentShape = explorer.Current();
|
||||
namedSolids.emplace_back(TopoDS::Solid(currentShape), fullName + "-SOLID-" + std::to_string(i));
|
||||
}
|
||||
// Surface models have no solids: each shell outside a solid is a part, and loose faces stay together.
|
||||
i = 0;
|
||||
for (explorer.Init(transform.Shape(), TopAbs_SHELL, TopAbs_SOLID); explorer.More(); explorer.Next())
|
||||
namedSolids.emplace_back(explorer.Current(), fullName + "-SHELL-" + std::to_string(++i));
|
||||
TopoDS_Compound loose_faces;
|
||||
TopoDS_Builder builder;
|
||||
builder.MakeCompound(loose_faces);
|
||||
bool has_loose_faces = false;
|
||||
for (explorer.Init(transform.Shape(), TopAbs_FACE, TopAbs_SHELL); explorer.More(); explorer.Next()) {
|
||||
builder.Add(loose_faces, explorer.Current());
|
||||
has_loose_faces = true;
|
||||
}
|
||||
if (has_loose_faces)
|
||||
namedSolids.emplace_back(loose_faces, fullName);
|
||||
}
|
||||
break;
|
||||
case TopAbs_SOLID:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "../libslic3r.h"
|
||||
#include "../Exception.hpp"
|
||||
#include "../IMEXHelpers.hpp"
|
||||
#include "../Model.hpp"
|
||||
#include "../Preset.hpp"
|
||||
#include "../Utils.hpp"
|
||||
@@ -21,6 +22,7 @@
|
||||
#include "libslic3r/BrimEarsPoint.hpp"
|
||||
#include "libslic3r/Config.hpp"
|
||||
#include "libslic3r/CustomGCode.hpp"
|
||||
#include "libslic3r/TextureDisplacement.hpp"
|
||||
#include "libslic3r_version.h"
|
||||
|
||||
#include "bbs_3mf.hpp"
|
||||
@@ -353,6 +355,13 @@ static constexpr const char* CUSTOM_SUPPORTS_ATTR = "paint_supports";
|
||||
static constexpr const char* CUSTOM_FUZZY_SKIN_ATTR = "paint_fuzzy_skin";
|
||||
static constexpr const char* CUSTOM_SEAM_ATTR = "paint_seam";
|
||||
static constexpr const char* MMU_SEGMENTATION_ATTR = "paint_color";
|
||||
// Texture displacement. One paint mask per layer slot, mirroring paint_color; the layer stack itself is
|
||||
// a JSON file in the archive, named by the volume metadata key below (see add_texture_displacement()).
|
||||
static constexpr const char* TEXTURE_DISPLACEMENT_ATTRS[Slic3r::TEXTURE_DISPLACEMENT_MAX_LAYERS] = {
|
||||
"paint_texture_0", "paint_texture_1", "paint_texture_2", "paint_texture_3",
|
||||
"paint_texture_4", "paint_texture_5", "paint_texture_6", "paint_texture_7" };
|
||||
static constexpr const char* TEXTURE_DISPLACEMENT_KEY = "texture_displacement";
|
||||
static constexpr const char* TEXTURE_DISPLACEMENT_DIR = "Metadata/texture_displacement/";
|
||||
// BBS
|
||||
static constexpr const char* FACE_PROPERTY_ATTR = "face_property";
|
||||
|
||||
@@ -368,6 +377,12 @@ static constexpr const char* FIRST_LAYER_PRINT_SEQUENCE_ATTR = "first_layer_prin
|
||||
static constexpr const char* OTHER_LAYERS_PRINT_SEQUENCE_ATTR = "other_layers_print_sequence";
|
||||
static constexpr const char* OTHER_LAYERS_PRINT_SEQUENCE_NUMS_ATTR = "other_layers_print_sequence_nums";
|
||||
static constexpr const char* SPIRAL_VASE_MODE = "spiral_mode";
|
||||
static constexpr const char* IMEX_PARALLEL_MODE_ATTR = "imex_parallel_mode";
|
||||
// The same attribute before the feature was renamed IMEX. Plate metadata is matched by exact
|
||||
// string and written with set_key_value, so it never passes through handle_legacy: without this
|
||||
// a project saved in that window loads every plate back on the Primary mode, silently.
|
||||
static constexpr const char* IXEX_PARALLEL_MODE_ATTR_LEGACY = "ixex_parallel_mode";
|
||||
static constexpr const char* IMEX_HEAD_FILAMENT_MAP_ATTR = "imex_head_filament_map";
|
||||
static constexpr const char* FILAMENT_MAP_MODE_ATTR = "filament_map_mode";
|
||||
static constexpr const char* FILAMENT_MAP_ATTR = "filament_maps";
|
||||
static constexpr const char* FILAMENT_VOL_MAP_ATTR = "filament_volume_maps";
|
||||
@@ -840,6 +855,9 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
std::vector<std::string> custom_seam;
|
||||
std::vector<std::string> mmu_segmentation;
|
||||
std::vector<std::string> fuzzy_skin;
|
||||
// One per texture displacement layer slot, indexed like `triangles` but stored only as far as the
|
||||
// slot's last painted triangle: empty when the slot has no paint, which is nearly every file.
|
||||
std::vector<std::string> texture_displacement[TEXTURE_DISPLACEMENT_MAX_LAYERS];
|
||||
// BBS
|
||||
std::vector<std::string> face_properties;
|
||||
|
||||
@@ -860,6 +878,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
custom_seam.clear();
|
||||
mmu_segmentation.clear();
|
||||
fuzzy_skin.clear();
|
||||
for (std::vector<std::string> &slot : texture_displacement)
|
||||
slot.clear();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1041,10 +1061,10 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
void _stop_object_xml_parser(const std::string& msg = std::string())
|
||||
{
|
||||
assert(! obj_parse_error);
|
||||
assert(obj_parse_error_message.empty());
|
||||
assert(object_xml_parser != nullptr);
|
||||
obj_parse_error = true;
|
||||
obj_parse_error_message = msg;
|
||||
if (! msg.empty() || obj_parse_error_message.empty()) // a handler may have set the message already
|
||||
obj_parse_error_message = msg;
|
||||
XML_StopParser(object_xml_parser, false);
|
||||
}
|
||||
|
||||
@@ -1193,6 +1213,11 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
/*IdToSlaSupportPointsMap m_sla_support_points;
|
||||
IdToSlaDrainHolesMap m_sla_drain_holes;*/
|
||||
PathToEmbossShapeFileMap m_path_to_emboss_shape_files;
|
||||
// Texture displacement: every file under Metadata/texture_displacement/, by archive path. Volumes
|
||||
// are built only after the whole archive has been walked, so by the time a volume names its JSON
|
||||
// every file it could refer to is already in here, whatever order the archive happened to be in.
|
||||
std::map<std::string, std::string> m_texture_displacement_files;
|
||||
void _apply_texture_displacement(ModelVolume &volume, const std::string &json_path);
|
||||
std::string m_curr_metadata_name;
|
||||
std::string m_curr_characters;
|
||||
std::string m_name;
|
||||
@@ -1395,7 +1420,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
|
||||
bool _handle_start_relationship(const char** attributes, unsigned int num_attributes);
|
||||
|
||||
void _generate_current_object_list(std::vector<Component> &sub_objects, Id object_id, IdToCurrentObjectMap& current_objects);
|
||||
bool _generate_current_object_list(std::vector<Component> &sub_objects, Id object_id, IdToCurrentObjectMap& current_objects);
|
||||
bool _generate_volumes_new(ModelObject& object, const std::vector<Component> &sub_objects, const ObjectMetadata::VolumeMetadataList& volumes, ConfigSubstitutionContext& config_substitutions);
|
||||
//bool _generate_volumes(ModelObject& object, const Geometry& geometry, const ObjectMetadata::VolumeMetadataList& volumes, ConfigSubstitutionContext& config_substitutions);
|
||||
|
||||
@@ -1722,8 +1747,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
//plate->pattern_file = it->second->pattern_file;
|
||||
plate->no_light_thumbnail_file = it->second->no_light_thumbnail_file;
|
||||
plate->top_file = it->second->top_file;
|
||||
plate->pick_file = it->second->pick_file.empty();
|
||||
plate->pattern_bbox_file = it->second->pattern_bbox_file.empty();
|
||||
plate->pick_file = it->second->pick_file;
|
||||
plate->pattern_bbox_file = it->second->pattern_bbox_file;
|
||||
plate->config = it->second->config;
|
||||
|
||||
if (!plate->thumbnail_file.empty())
|
||||
@@ -2066,6 +2091,13 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
else if (_is_svg_shape_file(name)) {
|
||||
_extract_embossed_svg_shape_file(name, archive, stat);
|
||||
}
|
||||
else if (boost::algorithm::istarts_with(name, TEXTURE_DISPLACEMENT_DIR)) {
|
||||
std::string contents(stat.m_uncomp_size, '\0');
|
||||
if (mz_zip_reader_extract_to_mem(&archive, stat.m_file_index, contents.data(), stat.m_uncomp_size, 0))
|
||||
m_texture_displacement_files.emplace(name, std::move(contents));
|
||||
else
|
||||
add_error("Error while reading texture displacement data");
|
||||
}
|
||||
else if (!dont_load_config && boost::algorithm::iequals(name, SLICE_INFO_CONFIG_FILE)) {
|
||||
m_parsing_slice_info = true;
|
||||
//extract slice info from archive
|
||||
@@ -2117,7 +2149,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
return false;
|
||||
}
|
||||
std::vector<Component> object_id_list;
|
||||
_generate_current_object_list(object_id_list, object.first, m_current_objects);
|
||||
if (!_generate_current_object_list(object_id_list, object.first, m_current_objects))
|
||||
return false;
|
||||
|
||||
ObjectMetadata::VolumeMetadataList volumes;
|
||||
ObjectMetadata::VolumeMetadataList* volumes_ptr = nullptr;
|
||||
@@ -2216,7 +2249,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
}*/
|
||||
|
||||
std::vector<Component> object_id_list;
|
||||
_generate_current_object_list(object_id_list, object.first, m_current_objects);
|
||||
if (!_generate_current_object_list(object_id_list, object.first, m_current_objects))
|
||||
return false;
|
||||
|
||||
ObjectMetadata::VolumeMetadataList volumes;
|
||||
ObjectMetadata::VolumeMetadataList* volumes_ptr = nullptr;
|
||||
@@ -3301,6 +3335,45 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
}
|
||||
}*/
|
||||
|
||||
// Restores one volume's texture displacement stack from the JSON it named. Called while the volume is
|
||||
// being built, which is after the whole archive has been walked - so every file it can refer to is
|
||||
// already extracted, and no deferral is needed.
|
||||
//
|
||||
// A missing or malformed JSON leaves the volume with no layers but keeps the paint mask it already
|
||||
// loaded, which is the same state as a project saved by a build without the feature: recoverable by
|
||||
// picking the texture again rather than a hard failure.
|
||||
void _BBS_3MF_Importer::_apply_texture_displacement(ModelVolume &volume, const std::string &json_path)
|
||||
{
|
||||
const auto json = m_texture_displacement_files.find(json_path);
|
||||
if (json == m_texture_displacement_files.end()) {
|
||||
add_error("Missing texture displacement data: " + json_path);
|
||||
return;
|
||||
}
|
||||
std::vector<TextureDisplacementLayer> layers;
|
||||
TextureDisplacementOptions options;
|
||||
if (!texture_displacement_layers_from_json(json->second, layers, options)) {
|
||||
add_error("Malformed texture displacement data: " + json_path);
|
||||
return;
|
||||
}
|
||||
// The archive is the only source of a layer's pixels. `path` comes from the project file, so loading
|
||||
// the project must never read the file it names: that would let whoever made the project pick any
|
||||
// local (or, on Windows, network) file to read, and the next save would embed it in the archive.
|
||||
// `path` stays a label of where the texture came from; a layer the archive carries no image for
|
||||
// loads without one, its settings intact, until the user picks a texture again.
|
||||
for (TextureDisplacementLayer &layer : layers) {
|
||||
if (layer.path_in_3mf.empty())
|
||||
continue;
|
||||
const auto image = m_texture_displacement_files.find(layer.path_in_3mf);
|
||||
if (image == m_texture_displacement_files.end()) {
|
||||
add_error("Missing texture displacement image: " + layer.path_in_3mf);
|
||||
continue;
|
||||
}
|
||||
layer.image_data = std::make_shared<std::vector<unsigned char>>(image->second.begin(), image->second.end());
|
||||
}
|
||||
volume.texture_displacement_layers = std::move(layers);
|
||||
volume.texture_displacement_options = options;
|
||||
}
|
||||
|
||||
void _BBS_3MF_Importer::_extract_embossed_svg_shape_file(const std::string &filename, mz_zip_archive &archive, const mz_zip_archive_file_stat &stat){
|
||||
assert(m_path_to_emboss_shape_files.find(filename) == m_path_to_emboss_shape_files.end());
|
||||
auto file = std::make_unique<std::string>(stat.m_uncomp_size, '\0');
|
||||
@@ -3901,11 +3974,18 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
{
|
||||
// appends the vertex coordinates
|
||||
// missing values are set equal to ZERO
|
||||
if (m_curr_object)
|
||||
m_curr_object->geometry.vertices.emplace_back(
|
||||
m_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, X_ATTR),
|
||||
m_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, Y_ATTR),
|
||||
m_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, Z_ATTR));
|
||||
if (m_curr_object) {
|
||||
const Vec3f v(m_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, X_ATTR),
|
||||
m_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, Y_ATTR),
|
||||
m_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, Z_ATTR));
|
||||
// A non-finite coordinate ("nan", "inf") used to be accepted and crashed
|
||||
// qhull in ModelVolume's convex hull while the file was still loading. Refuse the file.
|
||||
if (! v.allFinite()) {
|
||||
_stop_xml_parser("Invalid vertex coordinate: not a finite number");
|
||||
return true; // the parser is stopped; returning false would overwrite the message
|
||||
}
|
||||
m_curr_object->geometry.vertices.emplace_back(v);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -3950,6 +4030,12 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
m_curr_object->geometry.custom_seam.push_back(bbs_get_attribute_value_string(attributes, num_attributes, CUSTOM_SEAM_ATTR));
|
||||
m_curr_object->geometry.mmu_segmentation.push_back(bbs_get_attribute_value_string(attributes, num_attributes, MMU_SEGMENTATION_ATTR));
|
||||
m_curr_object->geometry.fuzzy_skin.push_back(bbs_get_attribute_value_string(attributes, num_attributes, CUSTOM_FUZZY_SKIN_ATTR));
|
||||
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot)
|
||||
if (const char *paint = bbs_get_attribute_value_charptr(attributes, num_attributes, TEXTURE_DISPLACEMENT_ATTRS[slot])) {
|
||||
std::vector<std::string> &mask = m_curr_object->geometry.texture_displacement[slot];
|
||||
mask.resize(m_curr_object->geometry.triangles.size());
|
||||
mask.back() = paint;
|
||||
}
|
||||
// BBS
|
||||
m_curr_object->geometry.face_properties.push_back(bbs_get_attribute_value_string(attributes, num_attributes, FACE_PROPERTY_ATTR));
|
||||
}
|
||||
@@ -4567,6 +4653,12 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
std::istringstream(value) >> std::boolalpha >> spiral_mode;
|
||||
m_curr_plater->config.set_key_value("spiral_mode", new ConfigOptionBool(spiral_mode));
|
||||
}
|
||||
else if (key == IMEX_PARALLEL_MODE_ATTR || key == IXEX_PARALLEL_MODE_ATTR_LEGACY) {
|
||||
m_curr_plater->config.set_key_value("imex_parallel_mode", new ConfigOptionString(value));
|
||||
}
|
||||
else if (key == IMEX_HEAD_FILAMENT_MAP_ATTR) {
|
||||
m_curr_plater->config.set_key_value("imex_head_filament_map", new ConfigOptionString(value));
|
||||
}
|
||||
else if (key == FILAMENT_MAP_MODE_ATTR)
|
||||
{
|
||||
FilamentMapMode map_mode = FilamentMapMode::fmmAutoForFlush;
|
||||
@@ -5064,11 +5156,18 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
return true;
|
||||
}
|
||||
|
||||
void _BBS_3MF_Importer::_generate_current_object_list(std::vector<Component> &sub_objects, Id object_id, IdToCurrentObjectMap ¤t_objects)
|
||||
bool _BBS_3MF_Importer::_generate_current_object_list(std::vector<Component> &sub_objects, Id object_id, IdToCurrentObjectMap ¤t_objects)
|
||||
{
|
||||
// A cycle in the component graph would expand forever, and an acyclic graph can still expand
|
||||
// exponentially, so bound the number of component references queued. Checking before they are
|
||||
// queued bounds the work list itself, whatever the fan-out. A valid file over the budget is
|
||||
// rejected too, but the budget is way above the component references of any real object.
|
||||
static constexpr size_t max_components = 100000;
|
||||
|
||||
std::list<std::pair<Component, Transform3d>> id_list;
|
||||
id_list.push_back(std::make_pair(Component(object_id, Transform3d::Identity()), Transform3d::Identity()));
|
||||
|
||||
size_t num_components = 0;
|
||||
while (!id_list.empty())
|
||||
{
|
||||
auto current_item = id_list.front();
|
||||
@@ -5078,6 +5177,12 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
if (current_object != current_objects.end()) {
|
||||
//found one
|
||||
if (!current_object->second.components.empty()) {
|
||||
num_components += current_object->second.components.size();
|
||||
if (num_components > max_components) {
|
||||
add_error("invalid 3mf: cyclic or too many component references");
|
||||
sub_objects.clear();
|
||||
return false;
|
||||
}
|
||||
for (const Component &comp : current_object->second.components) {
|
||||
id_list.push_back(std::pair(comp, current_item.second * comp.transform));
|
||||
}
|
||||
@@ -5089,6 +5194,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool _BBS_3MF_Importer::_generate_volumes_new(ModelObject& object, const std::vector<Component> &sub_objects, const ObjectMetadata::VolumeMetadataList& volumes, ConfigSubstitutionContext& config_substitutions)
|
||||
@@ -5195,6 +5301,11 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
}
|
||||
}
|
||||
|
||||
for (const Vec3f &v : sub_object->geometry.vertices)
|
||||
if (! v.allFinite()) { // Qhull cannot take a NaN vertex
|
||||
add_error("invalid (non-finite) vertex in object " + std::to_string(sub_object->id));
|
||||
return false;
|
||||
}
|
||||
its.vertices.assign(sub_object->geometry.vertices.begin(), sub_object->geometry.vertices.end());
|
||||
|
||||
// BBS
|
||||
@@ -5276,6 +5387,18 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
volume->mmu_segmentation_facets.touch();
|
||||
volume->fuzzy_skin_facets.shrink_to_fit();
|
||||
volume->fuzzy_skin_facets.touch();
|
||||
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot) {
|
||||
const std::vector<std::string> &mask = sub_object->geometry.texture_displacement[slot];
|
||||
if (mask.empty())
|
||||
continue; // no triangle painted on this slot
|
||||
FacetsAnnotation &facets = volume->texture_displacement_facet(slot);
|
||||
facets.reserve(triangles_count);
|
||||
for (size_t i = 0; i < triangles_count && i < mask.size(); ++i)
|
||||
if (!mask[i].empty())
|
||||
facets.set_triangle_from_string(i, mask[i]);
|
||||
facets.shrink_to_fit();
|
||||
facets.touch();
|
||||
}
|
||||
}
|
||||
|
||||
volume->set_type(volume_data->part_type);
|
||||
@@ -5291,6 +5414,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
for (const Metadata& metadata : volume_data->metadata) {
|
||||
if (metadata.key == NAME_KEY)
|
||||
volume->name = metadata.value;
|
||||
else if (metadata.key == TEXTURE_DISPLACEMENT_KEY)
|
||||
_apply_texture_displacement(*volume, metadata.value);
|
||||
//else if ((metadata.key == MODIFIER_KEY) && (metadata.value == "1"))
|
||||
// volume->set_type(ModelVolumeType::PARAMETER_MODIFIER);
|
||||
//for old format
|
||||
@@ -5455,12 +5580,28 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
volume->seam_facets.shrink_to_fit();
|
||||
volume->mmu_segmentation_facets.shrink_to_fit();
|
||||
|
||||
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot) {
|
||||
const std::vector<std::string> &mask = geometry.texture_displacement[slot];
|
||||
if (mask.empty())
|
||||
continue; // written by a build without the feature
|
||||
FacetsAnnotation &facets = volume->texture_displacement_facet(slot);
|
||||
facets.reserve(triangles_count);
|
||||
for (size_t i = 0; i < triangles_count; ++i) {
|
||||
const size_t index = volume_data.first_triangle_id + i;
|
||||
if (index < mask.size() && !mask[index].empty())
|
||||
facets.set_triangle_from_string(i, mask[index]);
|
||||
}
|
||||
facets.shrink_to_fit();
|
||||
facets.touch();
|
||||
}
|
||||
volume->set_type(volume_data.part_type);
|
||||
|
||||
// apply the remaining volume's metadata
|
||||
for (const Metadata& metadata : volume_data.metadata) {
|
||||
if (metadata.key == NAME_KEY)
|
||||
volume->name = metadata.value;
|
||||
else if (metadata.key == TEXTURE_DISPLACEMENT_KEY)
|
||||
_apply_texture_displacement(*volume, metadata.value);
|
||||
//else if ((metadata.key == MODIFIER_KEY) && (metadata.value == "1"))
|
||||
// volume->set_type(ModelVolumeType::PARAMETER_MODIFIER);
|
||||
//for old format
|
||||
@@ -5708,11 +5849,18 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
{
|
||||
// appends the vertex coordinates
|
||||
// missing values are set equal to ZERO
|
||||
if (current_object)
|
||||
current_object->geometry.vertices.emplace_back(
|
||||
object_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, X_ATTR),
|
||||
object_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, Y_ATTR),
|
||||
object_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, Z_ATTR));
|
||||
if (current_object) {
|
||||
const Vec3f v(object_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, X_ATTR),
|
||||
object_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, Y_ATTR),
|
||||
object_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, Z_ATTR));
|
||||
// See _BBS_3MF_Importer::_handle_start_vertex: a non-finite coordinate
|
||||
// crashed qhull while the file loaded. The dispatcher stops this parser on `false`.
|
||||
if (! v.allFinite()) {
|
||||
obj_parse_error_message = "Invalid vertex coordinate: not a finite number";
|
||||
return false;
|
||||
}
|
||||
current_object->geometry.vertices.emplace_back(v);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -5757,6 +5905,12 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
current_object->geometry.custom_seam.push_back(bbs_get_attribute_value_string(attributes, num_attributes, CUSTOM_SEAM_ATTR));
|
||||
current_object->geometry.mmu_segmentation.push_back(bbs_get_attribute_value_string(attributes, num_attributes, MMU_SEGMENTATION_ATTR));
|
||||
current_object->geometry.fuzzy_skin.push_back(bbs_get_attribute_value_string(attributes, num_attributes, CUSTOM_FUZZY_SKIN_ATTR));
|
||||
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot)
|
||||
if (const char *paint = bbs_get_attribute_value_charptr(attributes, num_attributes, TEXTURE_DISPLACEMENT_ATTRS[slot])) {
|
||||
std::vector<std::string> &mask = current_object->geometry.texture_displacement[slot];
|
||||
mask.resize(current_object->geometry.triangles.size());
|
||||
mask.back() = paint;
|
||||
}
|
||||
// BBS
|
||||
current_object->geometry.face_properties.push_back(bbs_get_attribute_value_string(attributes, num_attributes, FACE_PROPERTY_ATTR));
|
||||
}
|
||||
@@ -6760,6 +6914,9 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
stream << " <Default Extension=\"rels\" ContentType=\"application/vnd.openxmlformats-package.relationships+xml\"/>\n";
|
||||
stream << " <Default Extension=\"model\" ContentType=\"application/vnd.ms-package.3dmanufacturing-3dmodel+xml\"/>\n";
|
||||
stream << " <Default Extension=\"png\" ContentType=\"image/png\"/>\n";
|
||||
stream << " <Default Extension=\"jpg\" ContentType=\"image/jpeg\"/>\n";
|
||||
stream << " <Default Extension=\"jpeg\" ContentType=\"image/jpeg\"/>\n";
|
||||
stream << " <Default Extension=\"json\" ContentType=\"application/json\"/>\n";
|
||||
stream << " <Default Extension=\"gcode\" ContentType=\"text/x.gcode\"/>\n";
|
||||
stream << "</Types>";
|
||||
|
||||
@@ -7184,10 +7341,14 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
if (iter != m_shared_meshes.end())
|
||||
{
|
||||
const ModelVolume* shared_volume = iter->second.second;
|
||||
bool same_texture_paint = true;
|
||||
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS) && same_texture_paint; ++slot)
|
||||
same_texture_paint = shared_volume->texture_displacement_facet(slot).equals(volume->texture_displacement_facet(slot));
|
||||
if ((shared_volume->supported_facets.equals(volume->supported_facets))
|
||||
&& (shared_volume->seam_facets.equals(volume->seam_facets))
|
||||
&& (shared_volume->mmu_segmentation_facets.equals(volume->mmu_segmentation_facets))
|
||||
&& (shared_volume->fuzzy_skin_facets.equals(volume->fuzzy_skin_facets)))
|
||||
&& (shared_volume->fuzzy_skin_facets.equals(volume->fuzzy_skin_facets))
|
||||
&& same_texture_paint)
|
||||
{
|
||||
auto data = iter->second.first;
|
||||
const_cast<_BBS_3MF_Exporter *>(this)->m_volume_paths.insert({volume, {data->sub_path, data->volumes_objectID.find(iter->second.second)->second}});
|
||||
@@ -7603,6 +7764,20 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
output_buffer += "\"";
|
||||
}
|
||||
|
||||
// One attribute per texture displacement layer slot. Older readers ignore attributes they do
|
||||
// not know, so a project written here still opens in a build without the feature - it just
|
||||
// loses the paint, which is also all it could have done with it.
|
||||
for (int slot = 0; slot < TEXTURE_DISPLACEMENT_MAX_LAYERS; ++slot) {
|
||||
const std::string texture_paint = volume->texture_displacement_facet(slot).get_triangle_as_string(i);
|
||||
if (texture_paint.empty())
|
||||
continue;
|
||||
output_buffer += " ";
|
||||
output_buffer += TEXTURE_DISPLACEMENT_ATTRS[slot];
|
||||
output_buffer += "=\"";
|
||||
output_buffer += texture_paint;
|
||||
output_buffer += "\"";
|
||||
}
|
||||
|
||||
// BBS
|
||||
if (i < its.properties.size()) {
|
||||
std::string prop_str = its.properties[i].to_string();
|
||||
@@ -8052,7 +8227,42 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
return file_path;
|
||||
}
|
||||
|
||||
bool _BBS_3MF_Exporter::_add_model_config_file_to_archive(mz_zip_archive& archive, const Model& model, PlateDataPtrs& plate_data_list, const ObjectToObjectDataMap &objects_data, const DynamicPrintConfig& config, int export_plate_idx, bool save_gcode, bool use_loaded_id)
|
||||
// Writes a volume's texture displacement stack into the archive and points the volume's metadata at it.
|
||||
// The layer settings go in as JSON and each layer's texture as the image file it was loaded from, both
|
||||
// referenced by path - the same split EmbossShape makes for its SVG. The image deliberately stays out of
|
||||
// the XML: it is binary and routinely megabytes, and base64 in an attribute would bloat the one file
|
||||
// every reader has to parse just to list the objects.
|
||||
static void add_texture_displacement(std::stringstream &stream, const ModelVolume &volume, mz_zip_archive &archive,
|
||||
const std::string &id)
|
||||
{
|
||||
if (volume.texture_displacement_layers.empty())
|
||||
return;
|
||||
|
||||
// A copy, because path_in_3mf is only meaningful inside the archive being written and the volume
|
||||
// being exported is const (and may be saved again, elsewhere, with different paths).
|
||||
std::vector<TextureDisplacementLayer> layers = volume.texture_displacement_layers;
|
||||
for (TextureDisplacementLayer &layer : layers) {
|
||||
layer.path_in_3mf.clear();
|
||||
if (!layer.image_data || layer.image_data->empty())
|
||||
continue;
|
||||
// Always .png: the importer converts whatever the user picked (jpg, bmp, ...) to an 8-bit
|
||||
// greyscale PNG, so image_data is PNG whatever layer.path happens to be named.
|
||||
const std::string path = std::string(TEXTURE_DISPLACEMENT_DIR) + id + "_" + std::to_string(layer.slot) + ".png";
|
||||
// No deflate: a PNG is already compressed, so a second pass only costs time.
|
||||
if (mz_zip_writer_add_mem(&archive, path.c_str(), layer.image_data->data(), layer.image_data->size(),
|
||||
MZ_NO_COMPRESSION))
|
||||
layer.path_in_3mf = path;
|
||||
}
|
||||
|
||||
const std::string json = texture_displacement_layers_to_json(layers, volume.texture_displacement_options);
|
||||
const std::string json_path = std::string(TEXTURE_DISPLACEMENT_DIR) + id + ".json";
|
||||
if (!mz_zip_writer_add_mem(&archive, json_path.c_str(), json.data(), json.size(), MZ_DEFAULT_COMPRESSION))
|
||||
return;
|
||||
stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << TEXTURE_DISPLACEMENT_KEY << "\" "
|
||||
<< VALUE_ATTR << "=\"" << xml_escape(json_path) << "\"/>\n";
|
||||
}
|
||||
|
||||
bool _BBS_3MF_Exporter::_add_model_config_file_to_archive(mz_zip_archive& archive, const Model& model, PlateDataPtrs& plate_data_list, const ObjectToObjectDataMap &objects_data, const DynamicPrintConfig& config, int export_plate_idx, bool save_gcode, bool use_loaded_id)
|
||||
{
|
||||
std::stringstream stream;
|
||||
// Store mesh transformation in full precision, as the volumes are stored transformed and they need to be transformed back
|
||||
@@ -8163,6 +8373,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
if (const std::optional<EmbossShape> &es = volume->emboss_shape; es.has_value()) {
|
||||
to_xml(stream, *es, *volume, archive, m_fullpath_sources);
|
||||
}
|
||||
|
||||
add_texture_displacement(stream, *volume, archive, std::to_string(volume->id().id));
|
||||
|
||||
if (const std::optional<TextConfiguration> &tc = volume->text_configuration;
|
||||
tc.has_value())
|
||||
@@ -8242,6 +8454,23 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
if (spiral_mode_opt)
|
||||
stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << SPIRAL_VASE_MODE << "\" " << VALUE_ATTR << "=\"" << spiral_mode_opt->getBool() << "\"/>\n";
|
||||
|
||||
{
|
||||
// Mode names are user supplied free text, so the value has to be escaped for an
|
||||
// attribute. xml_escape_double_quotes_attribute_value() is used rather than
|
||||
// xml_escape() because it also emits tab/CR/LF as numeric character references:
|
||||
// XML normalizes literal whitespace in attribute values on read, which would
|
||||
// silently rename the mode. The reader takes the value straight from expat,
|
||||
// which resolves both entities and character references, so this round-trips.
|
||||
auto* imex_mode_opt = plate_data->config.option<ConfigOptionString>("imex_parallel_mode");
|
||||
if (imex_mode_opt && !imex_mode_opt->value.empty() && imex_mode_opt->value != kImexPrimaryMode)
|
||||
stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << IMEX_PARALLEL_MODE_ATTR << "\" " << VALUE_ATTR << "=\"" << xml_escape_double_quotes_attribute_value(imex_mode_opt->value) << "\"/>\n";
|
||||
}
|
||||
{
|
||||
auto* imex_hfm_opt = plate_data->config.option<ConfigOptionString>("imex_head_filament_map");
|
||||
if (imex_hfm_opt && !imex_hfm_opt->value.empty())
|
||||
stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << IMEX_HEAD_FILAMENT_MAP_ATTR << "\" " << VALUE_ATTR << "=\"" << xml_escape_double_quotes_attribute_value(imex_hfm_opt->value) << "\"/>\n";
|
||||
}
|
||||
|
||||
//filament map related
|
||||
ConfigOption* filament_map_mode_opt = plate_data->config.option("filament_map_mode");
|
||||
t_config_enum_names filament_map_mode_names = ConfigOptionEnum<FilamentMapMode>::get_enum_names();
|
||||
|
||||
+1241
-192
File diff suppressed because it is too large
Load Diff
+152
-9
@@ -7,6 +7,7 @@
|
||||
#include "Print.hpp"
|
||||
#include "libslic3r.h"
|
||||
#include "GCodeWriter.hpp"
|
||||
#include "GCode/BeltKinematics.hpp"
|
||||
#include "Layer.hpp"
|
||||
#include "Point.hpp"
|
||||
#include "PlaceholderParser.hpp"
|
||||
@@ -40,13 +41,16 @@
|
||||
#include <memory>
|
||||
#include <map>
|
||||
#include <unordered_map>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <cfloat>
|
||||
#include <vector>
|
||||
#include <utility>
|
||||
#include <cmath>
|
||||
#include "BoundingBox.hpp"
|
||||
#include "Polyline.hpp"
|
||||
#include "BeltBrim.hpp"
|
||||
|
||||
namespace Slic3r { class ExtrusionEntityCollection; }
|
||||
|
||||
@@ -249,14 +253,24 @@ public:
|
||||
m_toolchange_count(0),
|
||||
m_nominal_z(0.)
|
||||
{}
|
||||
~GCode() = default;
|
||||
virtual ~GCode() = default;
|
||||
|
||||
public:
|
||||
// throws std::runtime_exception on error,
|
||||
// throws CanceledException through print->throw_if_canceled().
|
||||
void do_export(Print* print, const char* path, GCodeProcessorResult* result = nullptr, ThumbnailsGeneratorCallback thumbnail_cb = nullptr);
|
||||
void export_layer_filaments(GCodeProcessorResult* result);
|
||||
//BBS: set offset for gcode writer
|
||||
void set_gcode_offset(double x, double y) { m_writer.set_xy_offset(x, y); m_processor.set_xy_offset(x, y);}
|
||||
// IMEX firmware-managed slice: writer offset is augmented by the IMEX shift so the
|
||||
// emitted gcode is centered at bed origin (firmware fans copies/mirrors out from there).
|
||||
// Processor offset stays at plate_origin only so the gcode-preview visualizer renders
|
||||
// the centered toolpath at bed center, not at the prepare-view zone placement. When
|
||||
// imex_x/imex_y are zero this is byte-identical to set_gcode_offset(x, y).
|
||||
void set_gcode_offset_with_imex_shift(double x, double y, double imex_x, double imex_y) {
|
||||
m_writer.set_xy_offset(x + imex_x, y + imex_y);
|
||||
m_processor.set_xy_offset(x, y);
|
||||
}
|
||||
|
||||
// Exported for the helper classes (OozePrevention, Wipe) and for the Perl binding for unit tests.
|
||||
const Vec2d& origin() const { return m_origin; }
|
||||
@@ -297,7 +311,11 @@ public:
|
||||
std::string unretract(float extra_retract = 0.f) { return m_writer.unlift() + m_writer.unretract(extra_retract); }
|
||||
std::string set_extruder(unsigned int extruder_id, double print_z, bool by_object=false, int toolchange_temp_override = -1, bool defer_temp_wait = false);
|
||||
// Sets the pressure advance of the filament's extruder variant, if enabled for it.
|
||||
std::string set_filament_pressure_advance(unsigned int filament_id);
|
||||
// tool addresses one IMEX carriage explicitly. -1 means no carriage: it omits the tool
|
||||
// qualifier on Klipper, Marlin and BBL, and keeps RepRapFirmware's historical `D0`, since a
|
||||
// bare M572 there applies to whatever tool is selected and errors when none is. That is
|
||||
// what every non-IMEX caller wants, and what imex_pem_tool_for() returns off IMEX.
|
||||
std::string set_filament_pressure_advance(unsigned int filament_id, int tool = -1);
|
||||
bool is_BBL_Printer();
|
||||
WipeTowerType wipe_tower_type();
|
||||
|
||||
@@ -342,6 +360,13 @@ public:
|
||||
const Layer* object_layer;
|
||||
const SupportLayer* support_layer;
|
||||
const PrintObject* original_object; //BBS: used for shared object logic
|
||||
// Belt printers only: an apron band that prints BELOW the object's first
|
||||
// layer, so it has no object or support layer of its own. Deliberately
|
||||
// not a Layer, so it cannot leak Layer::id() semantics into initial-layer
|
||||
// temperature, spiral vase, cooling or interpolation logic. When this is
|
||||
// the only thing set, layer() is null and process_layer() takes its
|
||||
// dedicated brim-only branch.
|
||||
const BeltBrimBand* belt_brim_band { nullptr };
|
||||
const Layer* layer() const
|
||||
{
|
||||
if (object_layer != nullptr)
|
||||
@@ -371,11 +396,17 @@ public:
|
||||
count++;
|
||||
}
|
||||
|
||||
// A brim-only apron band contributes no object/support layer, and
|
||||
// averaging zero terms would yield NaN. Never folded into the
|
||||
// average, so the non-belt result is bit-identical.
|
||||
if (count == 0 && belt_brim_band != nullptr)
|
||||
return belt_brim_band->print_z;
|
||||
|
||||
return sum_z / count;
|
||||
}
|
||||
};
|
||||
|
||||
private:
|
||||
protected:
|
||||
class GCodeOutputStream {
|
||||
public:
|
||||
GCodeOutputStream(FILE *f, GCodeProcessor &processor) : f(f), m_processor(processor) {}
|
||||
@@ -403,9 +434,21 @@ private:
|
||||
FILE *f = nullptr;
|
||||
GCodeProcessor &m_processor;
|
||||
};
|
||||
|
||||
// Virtual hooks for belt printer subclass (BeltGCode).
|
||||
// No-ops in base GCode; overridden in BeltGCode.
|
||||
virtual void init_belt_writer(Print &print) {}
|
||||
virtual void write_belt_header(GCodeOutputStream &file, const Print &print) {}
|
||||
virtual void on_set_origin(const PrintObject *obj, const Point &inst_shift) {}
|
||||
// Arc fitting is suppressed whenever the writer's machine mapping cannot
|
||||
// represent a G2/G3 arc. Belt printers get this through BeltKinematics
|
||||
// rather than through an override of their own.
|
||||
virtual bool should_disable_arc_fitting() const
|
||||
{ return ! m_writer.kinematics().supports_arc_moves(); }
|
||||
|
||||
void _do_export(Print &print, GCodeOutputStream &file, ThumbnailsGeneratorCallback thumbnail_cb);
|
||||
|
||||
static std::vector<LayerToPrint> collect_layers_to_print(const PrintObject &object);
|
||||
static std::vector<LayerToPrint> collect_layers_to_print(const PrintObject &object, bool skip_empty_first_layer = false);
|
||||
static std::vector<std::pair<coordf_t, std::vector<LayerToPrint>>> collect_layers_to_print(const Print &print);
|
||||
|
||||
std::string generate_skirt(const Print &print,
|
||||
@@ -425,7 +468,29 @@ private:
|
||||
std::string generate_object_brim(const Print &print,
|
||||
const PrintObject &object,
|
||||
size_t instance_id,
|
||||
bool first_layer);
|
||||
bool first_layer,
|
||||
const Layer *object_layer);
|
||||
|
||||
// Belt printers: emit one brim-only apron layer. These print below the
|
||||
// object's first layer, so there is no object or support layer for the normal
|
||||
// process_layer() machinery to work from. Kept to the minimum a layer needs -
|
||||
// tool, Z move, extrusions - so that nothing here can perturb the
|
||||
// Layer::id()-based logic the ordinary path relies on.
|
||||
LayerResult process_belt_brim_layer(
|
||||
const Print &print,
|
||||
const std::vector<LayerToPrint> &layers,
|
||||
const LayerTools &layer_tools,
|
||||
const bool last_layer,
|
||||
const size_t single_object_instance_idx);
|
||||
|
||||
// Emit the apron bands carried by these layers whose brim filament is extruder_id
|
||||
// (0-based). Called from both the brim-only branch and the ordinary path, since a
|
||||
// band's print_z can coincide with another object's layer on a multi-object belt.
|
||||
std::string emit_belt_brim_bands(
|
||||
const Print &print,
|
||||
const std::vector<LayerToPrint> &layers,
|
||||
const size_t single_object_instance_idx,
|
||||
const unsigned int extruder_id);
|
||||
|
||||
LayerResult process_layer(
|
||||
const Print &print,
|
||||
@@ -629,9 +694,21 @@ private:
|
||||
};
|
||||
|
||||
// Cache the per-filament island tour to avoid recomputing while the layer's island layout is
|
||||
// unchanged. Key: filament_id. Value: {nodes the tour was computed from, resulting visits}.
|
||||
std::map<unsigned int, std::pair<std::vector<IslandOrderNode>, std::vector<InstanceVisit>>>
|
||||
m_ordering_cache;
|
||||
// unchanged. Key: filament_id. Value: the nodes the tour was computed from, the per-instance
|
||||
// island layout (count and whether the trailing catch-all island has anything to print), and
|
||||
// the resulting visits.
|
||||
// The layout is part of the key. Nodes only cover the chainable islands, so two
|
||||
// layers with the same centroids but a different number of islands (thin walls, negative
|
||||
// volumes come and go) matched the cache and the visit's catch-all index -- islands.size() - 1
|
||||
// of the OLD layer -- ran past the new layer's islands (found by fuzzing: segfault in
|
||||
// extrude_perimeters on multi-part objects).
|
||||
struct IslandOrderCacheEntry
|
||||
{
|
||||
std::vector<IslandOrderNode> nodes;
|
||||
std::vector<std::pair<size_t, bool>> layout;
|
||||
std::vector<InstanceVisit> visits;
|
||||
};
|
||||
std::map<unsigned int, IslandOrderCacheEntry> m_ordering_cache;
|
||||
|
||||
ExtrusionQualityEstimator m_extrusion_quality_estimator;
|
||||
|
||||
@@ -768,11 +845,18 @@ private:
|
||||
|
||||
std::unique_ptr<CoolingBuffer> m_cooling_buffer;
|
||||
std::unique_ptr<SpiralVase> m_spiral_vase;
|
||||
|
||||
std::unique_ptr<PressureEqualizer> m_pressure_equalizer;
|
||||
|
||||
std::unique_ptr<AdaptivePAProcessor> m_pa_processor;
|
||||
|
||||
// IMEX: active parallel mode name ("primary", "copy", "iq-copy", etc.).
|
||||
// Set at the start of export. Empty string means non-IMEX or not yet set.
|
||||
// PA and temperature tool-qualification is only applied when this is not "primary".
|
||||
std::string m_imex_parallel_mode;
|
||||
// IMEX: parsed per-plate head→filament overrides (physical T-index → 1-based filament slot).
|
||||
// Cached from imex_head_filament_map at print start. Empty map means "fall back to pem".
|
||||
std::map<int,int> m_imex_head_filament_map;
|
||||
|
||||
std::unique_ptr<WipeTowerIntegration> m_wipe_tower;
|
||||
|
||||
std::unique_ptr<SmallAreaInfillFlowCompensator> m_small_area_infill_flow_compensator;
|
||||
@@ -823,6 +907,25 @@ private:
|
||||
mutable ConfigIndexCache m_filament_index_cache;
|
||||
mutable ConfigIndexCache m_nozzle_index_cache;
|
||||
|
||||
// Belt brim apron layers only. They have no Layer, so the print_z that
|
||||
// _extrude() needs for the first-layer-plane probe is published here instead.
|
||||
// Scoped by BeltBrimZGuard in process_belt_brim_layer(), never left set.
|
||||
std::optional<coordf_t> m_belt_brim_z;
|
||||
// Belt brim only. Brim and coincident apron bands are emitted before m_layer
|
||||
// is switched to their object, so belt_height_above_floor() would otherwise
|
||||
// read the previously visited object's belt description -- making a brim's
|
||||
// classification depend on plate visiting order. Those paths publish the
|
||||
// owner here for the duration of the emission. Never left set.
|
||||
const PrintObject *m_belt_floor_object{nullptr};
|
||||
struct BeltFloorObjectGuard {
|
||||
const PrintObject *&slot;
|
||||
BeltFloorObjectGuard(const PrintObject *&s, const PrintObject *o) : slot(s) { slot = o; }
|
||||
~BeltFloorObjectGuard() { slot = nullptr; }
|
||||
};
|
||||
|
||||
// The last extrusion segment was inside the belt's first-layer fan band (see _extrude()).
|
||||
bool m_belt_in_band{false};
|
||||
|
||||
std::set<unsigned int> m_initial_layer_extruders;
|
||||
std::vector<std::vector<unsigned int>> m_sorted_layer_filaments;
|
||||
// BBS
|
||||
@@ -840,6 +943,46 @@ private:
|
||||
// On the first printing layer. This flag triggers first layer speeds.
|
||||
//BBS
|
||||
bool on_first_layer() const { return m_layer != nullptr && m_layer->id() == 0 && abs(m_layer->bottom_z()) < EPSILON; }
|
||||
// Per-point first-layer test. On a belt printer the result depends on the
|
||||
// supplied slicing-frame point (its height above the belt); otherwise we
|
||||
// delegate to the legacy per-layer test. This is the entry point used by
|
||||
// per-path call sites in _extrude.
|
||||
bool on_first_layer(const Vec3d &point_slicing_mm) const {
|
||||
double h;
|
||||
if (this->belt_height_above_floor(point_slicing_mm, h))
|
||||
return h <= m_config.initial_layer_print_height.value + EPSILON;
|
||||
return on_first_layer();
|
||||
}
|
||||
// "Effective layer index" used to drive layer-count thresholds like
|
||||
// slow_down_layers. On a belt printer this is the height above the belt in
|
||||
// first_layer_band_mm() units; otherwise it is the legacy slicing layer index.
|
||||
int effective_layer_index_for_point(const Vec3d &point_slicing_mm) const {
|
||||
double h;
|
||||
if (this->belt_height_above_floor(point_slicing_mm, h)) {
|
||||
const double lh = this->first_layer_band_mm();
|
||||
return h <= 0. ? 0 : int(std::floor(h / lh));
|
||||
}
|
||||
return on_first_layer() ? 0 : layer_id();
|
||||
}
|
||||
|
||||
// Band thickness for the *effective layer index*: one first layer height, so
|
||||
// "the first N layers" means the same height above the belt as on a flat bed.
|
||||
double first_layer_band_mm() const {
|
||||
const double band = m_config.initial_layer_print_height.value;
|
||||
return band > 0. ? band : 0.2;
|
||||
}
|
||||
|
||||
// Height of a slicing-frame point above the belt surface, or false when this
|
||||
// is not a belt print.
|
||||
//
|
||||
// The belt surface is known exactly in the slicing frame from the slicing
|
||||
// parameters (belt_floor_shear_factor / _from_axis / _z_shift) -- the same
|
||||
// description the support generator uses, independent of every remap and
|
||||
// back-transform.
|
||||
bool belt_height_above_floor(const Vec3d &point_slicing_mm, double &height_mm) const;
|
||||
// 1 / 0 / -1: the object layer is entirely past the first-layer band above the
|
||||
// belt / reaches into it / the belt surface is not known for it.
|
||||
int belt_layer_past_first_layer_band(const Layer *object_layer) const;
|
||||
int layer_id() const {
|
||||
if (m_layer == nullptr)
|
||||
return -1;
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
#include "BeltBackTransform.hpp"
|
||||
#include "../BeltTransform.hpp"
|
||||
#include "../Point.hpp"
|
||||
#include "../PrintConfig.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
bool BeltBackTransform::init_from_config(const PrintConfig &config)
|
||||
{
|
||||
m_active = false;
|
||||
m_inverse = Transform3d::Identity();
|
||||
|
||||
if (!config.belt_printer.value)
|
||||
return false;
|
||||
|
||||
// Build the forward pipeline (the rotation) and store its inverse.
|
||||
Transform3d forward = BeltTransformPipeline::build_forward_transform(config);
|
||||
if (forward.isApprox(Transform3d::Identity()))
|
||||
return false;
|
||||
|
||||
m_inverse = forward.inverse();
|
||||
m_active = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
Vec3d BeltBackTransform::apply(const Vec3d &pos) const
|
||||
{
|
||||
if (!m_active)
|
||||
return pos;
|
||||
return m_inverse * pos;
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,38 @@
|
||||
#ifndef slic3r_BeltBackTransform_hpp_
|
||||
#define slic3r_BeltBackTransform_hpp_
|
||||
|
||||
#include "../libslic3r.h"
|
||||
#include "../Point.hpp"
|
||||
#include "../PrintConfig.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// Reverses the pre-slice rotation that PrintObjectSlice.cpp applies to belt
|
||||
// printer geometry, converting G-code coordinates from the sliced (rotated)
|
||||
// frame back to the machine's real coordinate space.
|
||||
//
|
||||
// Initialized once from PrintConfig, then applied per-point in
|
||||
// BeltKinematics::to_machine() before axis remapping.
|
||||
//
|
||||
// Active on belt printers with a non-identity pre-slice rotation.
|
||||
class BeltBackTransform
|
||||
{
|
||||
public:
|
||||
BeltBackTransform() = default;
|
||||
|
||||
// Initialize from belt printer config. Rebuilds the same pre-slice rotation
|
||||
// as PrintObjectSlice.cpp and precomputes the affine inverse. Returns true if a non-identity back-transform was computed.
|
||||
bool init_from_config(const PrintConfig &config);
|
||||
|
||||
// Apply the inverse transform to a point. Returns pos unchanged if
|
||||
// no back-transform is active.
|
||||
Vec3d apply(const Vec3d &pos) const;
|
||||
|
||||
private:
|
||||
bool m_active = false;
|
||||
Transform3d m_inverse = Transform3d::Identity();
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // slic3r_BeltBackTransform_hpp_
|
||||
@@ -0,0 +1,29 @@
|
||||
#include "BeltKinematics.hpp"
|
||||
#include "../BeltTransform.hpp"
|
||||
#include "../PrintConfig.hpp"
|
||||
#include "../GCodeWriter.hpp"
|
||||
#include "../Point.hpp"
|
||||
#include <memory>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
BeltKinematics::BeltKinematics(const PrintConfig &config, bool world_coordinates)
|
||||
: m_world_coordinates(world_coordinates)
|
||||
{
|
||||
m_back_transform.init_from_config(config);
|
||||
m_machine_frame.init_from_config(config);
|
||||
}
|
||||
|
||||
Vec3d BeltKinematics::to_machine(const Vec3d &p) const
|
||||
{
|
||||
const Vec3d after_back = m_world_coordinates ? p : m_back_transform.apply(p);
|
||||
const Vec3d after_remap = this->apply_axis_remap(after_back);
|
||||
return m_machine_frame.apply(after_remap);
|
||||
}
|
||||
|
||||
void install_belt_kinematics(GCodeWriter &writer, const PrintConfig &config, bool world_coordinates)
|
||||
{
|
||||
writer.set_kinematics(std::make_unique<BeltKinematics>(config, world_coordinates));
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,59 @@
|
||||
#ifndef slic3r_BeltKinematics_hpp_
|
||||
#define slic3r_BeltKinematics_hpp_
|
||||
|
||||
#include "MachineKinematics.hpp"
|
||||
#include "BeltBackTransform.hpp"
|
||||
#include "MachineFrameTransform.hpp"
|
||||
#include "../Point.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
class PrintConfig;
|
||||
class GCodeWriter;
|
||||
|
||||
// Belt-printer machine frame.
|
||||
//
|
||||
// Forward order, as applied per emitted point:
|
||||
// machine = MachineFrameTransform( axis_remap( BeltBackTransform( logical ) ) )
|
||||
//
|
||||
// i.e. the slicer->world back-transform runs FIRST and the machine-frame
|
||||
// shear/scale LAST, so the latter acts as a global linear transform on the
|
||||
// already-placed coordinates.
|
||||
//
|
||||
// world_coordinates mode (the PA line / PA pattern calibration generators)
|
||||
// treats the incoming point as already relative to the belt surface -- X across,
|
||||
// Y along the belt, Z above it -- and therefore skips the back-transform while
|
||||
// keeping the remap and the machine frame. It is a different coordinate map, not
|
||||
// a writer mode, which is why it is fixed at construction.
|
||||
class BeltKinematics : public CartesianKinematics
|
||||
{
|
||||
public:
|
||||
explicit BeltKinematics(const PrintConfig &config, bool world_coordinates = false);
|
||||
|
||||
Vec3d to_machine(const Vec3d &p) const override;
|
||||
|
||||
// A belt writer has always emitted full XYZ on every move, whether or not any
|
||||
// individual stage reports itself active. Making this conditional would change
|
||||
// emitted G-code for an identity-transform belt configuration.
|
||||
bool must_emit_all_axes() const override { return true; }
|
||||
bool suppress_lift_at_unknown_position() const override { return true; }
|
||||
// The machine frame shears and scales, so a circle is an ellipse in machine
|
||||
// coordinates and G2/G3 cannot describe it.
|
||||
bool supports_arc_moves() const override { return false; }
|
||||
|
||||
private:
|
||||
BeltBackTransform m_back_transform;
|
||||
MachineFrameTransform m_machine_frame;
|
||||
bool m_world_coordinates { false };
|
||||
};
|
||||
|
||||
// Install a belt machine frame on any GCodeWriter. Any axis remap and build
|
||||
// volume already configured on the writer are carried over, so this may be
|
||||
// called before or after those setters. Re-calling it with a different
|
||||
// world_coordinates value swaps the map (used around the PA line generator).
|
||||
void install_belt_kinematics(GCodeWriter &writer, const PrintConfig &config,
|
||||
bool world_coordinates = false);
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // slic3r_BeltKinematics_hpp_
|
||||
@@ -148,8 +148,6 @@ struct ConflictComputeResult
|
||||
|
||||
using ConflictComputeOpt = std::optional<ConflictComputeResult>;
|
||||
|
||||
using ConflictObjName = std::optional<std::pair<std::string, std::string>>;
|
||||
|
||||
struct ConflictChecker
|
||||
{
|
||||
static ConflictResultOpt find_inter_of_lines_in_diff_objs(PrintObjectPtrs objs, std::optional<const FakeWipeTower *> wtdptr);
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include <iostream>
|
||||
#include <float.h>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <system_error>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
@@ -46,10 +47,12 @@ CoolingBuffer::CoolingBuffer(GCode &gcodegen) : m_config(gcodegen.config()), m_g
|
||||
m_num_extruders = std::max(ex.id() + 1, m_num_extruders);
|
||||
m_extruder_ids.emplace_back(ex.id());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void CoolingBuffer::reset(const Vec3d &position)
|
||||
{
|
||||
m_belt_band_active = false;
|
||||
// BBS: add I and J axis to store center of arc
|
||||
m_current_pos.assign(7, 0.f);
|
||||
m_current_pos[0] = float(position.x());
|
||||
@@ -89,6 +92,9 @@ struct CoolingLine
|
||||
// ORCA: Add support for ironing fan speed control
|
||||
TYPE_IRONING_FAN_START = 1 << 19,
|
||||
TYPE_IRONING_FAN_END = 1 << 20,
|
||||
// Belt printers: extrusions within the first-layer band above the belt.
|
||||
TYPE_BELT_BAND_START = 1 << 21,
|
||||
TYPE_BELT_BAND_END = 1 << 22,
|
||||
};
|
||||
|
||||
CoolingLine(unsigned int type, size_t line_start, size_t line_end) :
|
||||
@@ -549,6 +555,10 @@ std::vector<PerExtruderAdjustments> CoolingBuffer::parse_layer_gcode(const std::
|
||||
line.type = CoolingLine::TYPE_IRONING_FAN_START;
|
||||
} else if (boost::starts_with(sline, ";_IRONING_FAN_END")) { // ORCA: Add support for ironing fan speed control
|
||||
line.type = CoolingLine::TYPE_IRONING_FAN_END;
|
||||
} else if (boost::starts_with(sline, ";_BELT_BAND_START")) {
|
||||
line.type = CoolingLine::TYPE_BELT_BAND_START;
|
||||
} else if (boost::starts_with(sline, ";_BELT_BAND_END")) {
|
||||
line.type = CoolingLine::TYPE_BELT_BAND_END;
|
||||
} else if (boost::starts_with(sline, "G4 ")) {
|
||||
// Parse the wait time.
|
||||
line.type = CoolingLine::TYPE_G4;
|
||||
@@ -891,7 +901,9 @@ std::string CoolingBuffer::apply_layer_cooldown(
|
||||
{CoolingLine::TYPE_SUPPORT_INTERFACE_FAN_START, false},
|
||||
{CoolingLine::TYPE_IRONING_FAN_START, false}, // ORCA: Add support for ironing fan speed control
|
||||
{CoolingLine::TYPE_FORCE_RESUME_FAN, false}};
|
||||
bool need_set_fan = false;
|
||||
// Belt printers: a band still open from the previous layer has to take the fan back from
|
||||
// the layer-level speed issued just above.
|
||||
bool need_set_fan = m_belt_band_active;
|
||||
|
||||
for (const CoolingLine *line : lines) {
|
||||
const char *line_start = gcode.c_str() + line->line_start;
|
||||
@@ -905,6 +917,8 @@ std::string CoolingBuffer::apply_layer_cooldown(
|
||||
if (new_extruder != m_current_extruder) {
|
||||
m_current_extruder = new_extruder;
|
||||
change_extruder_set_fan(true);
|
||||
if (m_belt_band_active)
|
||||
need_set_fan = true;
|
||||
}
|
||||
}
|
||||
new_gcode.append(line_start, line_end - line_start);
|
||||
@@ -957,6 +971,13 @@ std::string CoolingBuffer::apply_layer_cooldown(
|
||||
if (m_additional_fan_speed != -1 && m_config.auxiliary_fan.value)
|
||||
new_gcode += GCodeWriter::set_additional_fan(m_additional_fan_speed);
|
||||
}
|
||||
else if (line->type & CoolingLine::TYPE_BELT_BAND_START) {
|
||||
m_belt_band_active = true;
|
||||
need_set_fan = true;
|
||||
} else if (line->type & CoolingLine::TYPE_BELT_BAND_END) {
|
||||
m_belt_band_active = false;
|
||||
need_set_fan = true;
|
||||
}
|
||||
else if (line->type & CoolingLine::TYPE_EXTRUDE_END) {
|
||||
// Just remove this comment.
|
||||
} else if (line->type & (CoolingLine::TYPE_ADJUSTABLE | CoolingLine::TYPE_EXTERNAL_PERIMETER | CoolingLine::TYPE_WIPE | CoolingLine::TYPE_HAS_F)) {
|
||||
@@ -1049,7 +1070,15 @@ std::string CoolingBuffer::apply_layer_cooldown(
|
||||
m_current_fan_speed = speed;
|
||||
}
|
||||
};
|
||||
if (fan_speed_change_requests[CoolingLine::TYPE_OVERHANG_FAN_START]){
|
||||
if (m_belt_band_active) {
|
||||
// Belt printers: a tilted layer runs from the belt to the top of the part, so
|
||||
// "the first layers" are a band along the belt rather than the first slicing
|
||||
// layers. Extrusions GCode::_extrude() marks as inside that band print with the
|
||||
// fan off, whatever overhang, bridge or resume request is pending, as the first
|
||||
// layers of a flat bed do. Leaving the band falls through to the branches below.
|
||||
set_fan(0);
|
||||
fan_speed_change_requests[CoolingLine::TYPE_FORCE_RESUME_FAN] = false;
|
||||
} else if (fan_speed_change_requests[CoolingLine::TYPE_OVERHANG_FAN_START]){
|
||||
set_fan(overhang_fan_speed);
|
||||
} else if (fan_speed_change_requests[CoolingLine::TYPE_INTERNAL_BRIDGE_FAN_START]){ // ORCA: Add support for separate internal bridge fan speed control
|
||||
set_fan(internal_bridge_fan_speed);
|
||||
|
||||
@@ -21,7 +21,7 @@ struct PerExtruderAdjustments;
|
||||
//
|
||||
// The simple it sounds, the actual implementation is significantly more complex.
|
||||
// Namely, for a multi-extruder print, each material may require a different cooling logic.
|
||||
// For example, some materials may not like to print too slowly, while with some materials
|
||||
// For example, some materials may not like to print too slowly, while with some materials
|
||||
// we may slow down significantly.
|
||||
//
|
||||
class CoolingBuffer {
|
||||
@@ -63,6 +63,9 @@ private:
|
||||
unsigned int m_current_nozzle;
|
||||
//BBS: current fan speed
|
||||
int m_current_fan_speed;
|
||||
// Belt printers: the extrusion being processed lies in the first-layer band above the
|
||||
// belt (between a ";_BELT_BAND_START" and a ";_BELT_BAND_END"). Kept across layers.
|
||||
bool m_belt_band_active = false;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -27,8 +27,6 @@
|
||||
#include "../PrintConfig.hpp"
|
||||
#include "../Utils.hpp"
|
||||
#include "Print.hpp"
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
#include "libslic3r/libslic3r.h"
|
||||
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <string>
|
||||
@@ -447,6 +445,11 @@ void FanMover::_process_gcode_line(GCodeReader& reader, const GCodeReader::GCode
|
||||
std::string extrusion_string = line.raw().substr(6, line.raw().size() - 6);
|
||||
current_role = ExtrusionEntity::string_to_role(extrusion_string);
|
||||
}
|
||||
if (line.raw().rfind(";_FAN_MOVER_SKIP_", 0) == 0) {
|
||||
// Internal markers: read here, then dropped from the output.
|
||||
m_is_custom_gcode = line.raw().rfind(";_FAN_MOVER_SKIP_START", 0) == 0;
|
||||
time = -1;
|
||||
}
|
||||
if (line.raw().size() > 16) {
|
||||
if (line.raw().rfind("; custom gcode", 0) != std::string::npos) {
|
||||
if (line.raw().rfind("; custom gcode end", 0) != std::string::npos)
|
||||
|
||||
@@ -89,7 +89,6 @@ static const float DEFAULT_TRAVEL_ACCELERATION = 1250.0f;
|
||||
static const size_t MIN_EXTRUDERS_COUNT = 5;
|
||||
static const float DEFAULT_FILAMENT_DIAMETER = 1.75f;
|
||||
static const int DEFAULT_FILAMENT_HRC = 0;
|
||||
static const float DEFAULT_FILAMENT_DENSITY = 1.245f;
|
||||
static const float DEFAULT_FILAMENT_COST = 29.99f;
|
||||
static const int DEFAULT_FILAMENT_VITRIFICATION_TEMPERATURE = 0;
|
||||
static const Slic3r::Vec3f DEFAULT_EXTRUDER_OFFSET = Slic3r::Vec3f::Zero();
|
||||
@@ -2603,8 +2602,13 @@ void GCodeProcessorResult::reset() {
|
||||
//BBS: add mutex for protection of gcode result
|
||||
lock();
|
||||
|
||||
moves.clear();
|
||||
lines_ends.clear();
|
||||
// moves and lines_ends grow with the print: free them, as no reset result reuses the capacity.
|
||||
clear_and_shrink(moves);
|
||||
plate_mass = {};
|
||||
object_masses.clear();
|
||||
body_masses.clear();
|
||||
support_masses.clear();
|
||||
clear_and_shrink(lines_ends);
|
||||
printable_area = Pointfs();
|
||||
//BBS: add bed exclude area
|
||||
bed_exclude_area = Pointfs();
|
||||
@@ -2616,6 +2620,9 @@ void GCodeProcessorResult::reset() {
|
||||
long_retraction_when_cut = false;
|
||||
timelapse_warning_code = 0;
|
||||
printable_height = 0.0f;
|
||||
machine_frame_transform_active = false;
|
||||
belt_tilt_angle = 0.f;
|
||||
belt_z_origin = 0.f;
|
||||
settings_ids.reset();
|
||||
filaments_count = 0;
|
||||
backtrace_enabled = false;
|
||||
@@ -2852,6 +2859,32 @@ bool GCodeProcessor::check_multi_extruder_gcode_valid(const int
|
||||
return ps;
|
||||
};
|
||||
|
||||
// Belt-printer post-gcode shear/scale/post_remap is applied as the final
|
||||
// step of BeltKinematics::to_machine, so MoveVertex.position is
|
||||
// in the printer's machine frame. Undo it here so the XY area check
|
||||
// operates in the build-volume frame that printable_area is defined in
|
||||
// (the height checks below are skipped on belt printers). For non-belt printers
|
||||
// (is_active() == false) apply_inverse is identity and behaviour is
|
||||
// unchanged from before.
|
||||
const bool machine_frame_active = m_machine_frame_transform.is_active();
|
||||
auto compare_pos = [&](const GCodeProcessorResult::MoveVertex &move) -> Vec3d {
|
||||
Vec3d pos = move.position.cast<double>();
|
||||
if (!machine_frame_active)
|
||||
return pos;
|
||||
Vec3d extruder_off = Vec3d::Zero();
|
||||
if (size_t(move.extruder_id) < m_extruder_offsets.size())
|
||||
extruder_off = m_extruder_offsets[move.extruder_id].cast<double>();
|
||||
// Strip plate + extruder offsets to recover the raw machine-frame
|
||||
// coordinate that was emitted into the G-code (see store_move_vertex).
|
||||
Vec3d machine(pos.x() - m_x_offset - extruder_off.x(),
|
||||
pos.y() - m_y_offset - extruder_off.y(),
|
||||
pos.z() - extruder_off.z() + m_z_offset);
|
||||
Vec3d build = m_machine_frame_transform.apply_inverse(machine);
|
||||
// Re-apply plate offset so the result matches plate_printable_poly,
|
||||
// which is translated by plate_offset below.
|
||||
return Vec3d(build.x() + m_x_offset, build.y() + m_y_offset, build.z());
|
||||
};
|
||||
|
||||
struct GCodePosInfo
|
||||
{
|
||||
Points pos;
|
||||
@@ -2863,26 +2896,20 @@ bool GCodeProcessor::check_multi_extruder_gcode_valid(const int
|
||||
for (const GCodeProcessorResult::MoveVertex &move : m_result.moves) {
|
||||
// sometimes, the start line extrude was outside the edge of plate a little, this is allowed, so do not include into the gcode_path_pos
|
||||
if (move.type == EMoveType::Extrude /* && move.extrusion_role != ExtrusionRole::erFlush || move.type == EMoveType::Travel*/) {
|
||||
const Vec3d cp = compare_pos(move);
|
||||
// For belt printers we read Z from the inverse-transformed position
|
||||
// (post-origin-snap, pre-machine-frame). Otherwise keep the
|
||||
// original print_z source (the slicer's layer-Z comment) so
|
||||
// non-belt behaviour is bit-for-bit unchanged.
|
||||
const float z_for_height = machine_frame_active ? float(cp.z()) : move.print_z;
|
||||
if (move.extrusion_role == ExtrusionRole::erCustom) {
|
||||
/*if (move.is_arc_move_with_interpolation_points()) {
|
||||
for (int i = 0; i < move.interpolation_points.size(); i++) {
|
||||
gcode_path_pos[move.object_label_id][int(move.extruder_id)].pos_custom.emplace_back(to_2d(move.interpolation_points[i].cast<double>()));
|
||||
}
|
||||
} else {*/
|
||||
gcode_path_pos[move.object_label_id][int(move.extruder_id)].pos_custom.emplace_back(to_2d(move.position.cast<double>()));
|
||||
//}
|
||||
gcode_path_pos[move.object_label_id][int(move.extruder_id)].pos_custom.emplace_back(to_2d(cp));
|
||||
gcode_path_pos[move.object_label_id][int(move.extruder_id)].max_print_z_custom =
|
||||
std::max(gcode_path_pos[move.object_label_id][int(move.extruder_id)].max_print_z_custom, move.print_z);
|
||||
std::max(gcode_path_pos[move.object_label_id][int(move.extruder_id)].max_print_z_custom, z_for_height);
|
||||
} else {
|
||||
/*if (move.is_arc_move_with_interpolation_points()) {
|
||||
for (int i = 0; i < move.interpolation_points.size(); i++) {
|
||||
gcode_path_pos[move.object_label_id][int(move.extruder_id)].pos.emplace_back(to_2d(move.interpolation_points[i].cast<double>()));
|
||||
}
|
||||
} else {*/
|
||||
gcode_path_pos[move.object_label_id][int(move.extruder_id)].pos.emplace_back(to_2d(move.position.cast<double>()));
|
||||
//}
|
||||
gcode_path_pos[move.object_label_id][int(move.extruder_id)].pos.emplace_back(to_2d(cp));
|
||||
gcode_path_pos[move.object_label_id][int(move.extruder_id)].max_print_z = std::max(gcode_path_pos[move.object_label_id][int(move.extruder_id)].max_print_z,
|
||||
move.print_z);
|
||||
z_for_height);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2917,7 +2944,12 @@ bool GCodeProcessor::check_multi_extruder_gcode_valid(const int
|
||||
valid = false;
|
||||
}
|
||||
}
|
||||
if ( iter->second.max_print_z > plate_printable_height ) { //over height
|
||||
// Belt printers: the Z recorded here grows with belt travel (machine Z with the
|
||||
// frame transform, the slicing-frame Z without it), while printable_height is the
|
||||
// clearance above the belt; the two are not comparable, so the over-height check
|
||||
// is skipped, as the preview's ToolHeightOutside warning already is.
|
||||
// Print::validate() checks the object's height against the clearance.
|
||||
if ( !m_belt_printer && iter->second.max_print_z > plate_printable_height ) { //over height
|
||||
m_result.gcode_check_result.error_code |= (1 << 3);
|
||||
std::pair<int, int> filament_to_object_id;
|
||||
filament_to_object_id.first = iter->first;
|
||||
@@ -2958,7 +2990,7 @@ bool GCodeProcessor::check_multi_extruder_gcode_valid(const int
|
||||
}
|
||||
|
||||
// check printable height
|
||||
if ((extruder_id < printable_heights.size()) && (iter->second.max_print_z > printable_heights[extruder_id])) {
|
||||
if (!m_belt_printer && (extruder_id < printable_heights.size()) && (iter->second.max_print_z > printable_heights[extruder_id])) {
|
||||
m_result.gcode_check_result.error_code |= (1 << 1);
|
||||
std::pair<int, int> filament_to_object_id;
|
||||
filament_to_object_id.first = iter->first;
|
||||
@@ -3124,6 +3156,13 @@ void GCodeProcessor::apply_config(const PrintConfig& config)
|
||||
|
||||
m_result.printable_height = config.printable_height;
|
||||
|
||||
// Belt printer: cache the post-gcode machine-frame transform so the
|
||||
// multi-extruder validator can undo it and compare against build-volume
|
||||
// bounds rather than machine-frame positions.
|
||||
m_machine_frame_transform.init_from_config(config);
|
||||
m_result.machine_frame_transform_active = m_machine_frame_transform.is_active();
|
||||
m_belt_printer = config.belt_printer.value;
|
||||
|
||||
auto filament_maps = config.option<ConfigOptionInts>("filament_map");
|
||||
if (filament_maps != nullptr) {
|
||||
m_filament_maps = filament_maps->values;
|
||||
@@ -3154,6 +3193,32 @@ void GCodeProcessor::apply_config(const DynamicPrintConfig& config)
|
||||
{
|
||||
m_parser.apply_config(config);
|
||||
|
||||
// Belt printer: remember the file's belt keys for export_config_for_render(). The
|
||||
// config block lists belt_printer for every printer, so a non-belt file loaded while
|
||||
// a belt printer is selected switches the preview's belt view off, and a belt file
|
||||
// loaded on another printer brings its own tilt, remaps and bed along.
|
||||
m_belt_render_config.clear();
|
||||
{
|
||||
const auto *belt = config.option<ConfigOptionBool>("belt_printer");
|
||||
if (belt != nullptr) {
|
||||
static const char *belt_keys[] = {
|
||||
"belt_printer", "belt_slice_rotation", "belt_slice_rotation_angle",
|
||||
"gcode_remap_x", "gcode_remap_y", "gcode_remap_z",
|
||||
"belt_frame_tilt_decouple", "belt_frame_tilt_angle",
|
||||
};
|
||||
for (const char *key : belt_keys)
|
||||
if (const ConfigOption *opt = config.option(key); opt != nullptr)
|
||||
m_belt_render_config.set_key_value(key, opt->clone());
|
||||
// The Rev remaps mirror inside the build volume, so the designed view needs
|
||||
// the bed the file was sliced for. Only a belt file may override it.
|
||||
static const char *bed_keys[] = { "printable_area", "printable_height" };
|
||||
if (belt->value)
|
||||
for (const char *key : bed_keys)
|
||||
if (const ConfigOption *opt = config.option(key); opt != nullptr)
|
||||
m_belt_render_config.set_key_value(key, opt->clone());
|
||||
}
|
||||
}
|
||||
|
||||
//BBS
|
||||
const ConfigOptionFloatsNullable* nozzle_volume = config.option<ConfigOptionFloatsNullable>("nozzle_volume");
|
||||
if (nozzle_volume != nullptr) {
|
||||
@@ -3534,6 +3599,10 @@ void GCodeProcessor::apply_config(const DynamicPrintConfig& config)
|
||||
if (has_scarf_joint_seam != nullptr)
|
||||
m_detect_layer_based_on_tag = m_detect_layer_based_on_tag || has_scarf_joint_seam->value;
|
||||
|
||||
const ConfigOptionBool* manual_filament_change = config.option<ConfigOptionBool>("manual_filament_change");
|
||||
if (manual_filament_change != nullptr)
|
||||
m_manual_filament_change = manual_filament_change->value;
|
||||
|
||||
const ConfigOptionEnumGeneric *bed_type = config.option<ConfigOptionEnumGeneric>("curr_bed_type");
|
||||
if (bed_type != nullptr)
|
||||
m_result.bed_type = (BedType)bed_type->value;
|
||||
@@ -3637,9 +3706,11 @@ void GCodeProcessor::reset()
|
||||
m_zero_layer_height = 0.0f;
|
||||
m_first_layer_height = 0.0f;
|
||||
m_processing_start_custom_gcode = false;
|
||||
m_in_config_block = false;
|
||||
m_g1_line_id = 0;
|
||||
m_layer_id = 0;
|
||||
m_cp_color.reset();
|
||||
m_mass_locator = nullptr;
|
||||
|
||||
m_producer = EProducer::Unknown;
|
||||
|
||||
@@ -3677,6 +3748,7 @@ DynamicConfig GCodeProcessor::export_config_for_render() const
|
||||
config.set_key_value("filament_is_support", new ConfigOptionBools(m_parser.get_config().filament_is_support.values));
|
||||
config.set_key_value("filament_type", new ConfigOptionStrings(m_parser.get_config().filament_type.values));
|
||||
config.set_key_value("filament_map", new ConfigOptionInts(m_parser.get_config().filament_map.values));
|
||||
config.apply(m_belt_render_config);
|
||||
return config;
|
||||
}
|
||||
|
||||
@@ -3778,6 +3850,7 @@ void GCodeProcessor::process_buffer(const std::string &buffer)
|
||||
void GCodeProcessor::finalize(bool post_process)
|
||||
{
|
||||
m_result.z_offset = m_z_offset;
|
||||
finalize_object_masses();
|
||||
|
||||
// update width/height of wipe moves
|
||||
for (GCodeProcessorResult::MoveVertex& move : m_result.moves) {
|
||||
@@ -4242,6 +4315,34 @@ void GCodeProcessor::process_tags(const std::string_view comment, bool producers
|
||||
return;
|
||||
}
|
||||
|
||||
// ;Z: -- the layer Z tag non-BBL printers write. Only read on a belt printer,
|
||||
// where the preview labels its layers with it (GCodeViewer::load_as_gcode);
|
||||
// elsewhere print_z stays unset, as it always was, so nothing downstream of
|
||||
// it changes for other printers.
|
||||
if (m_belt_printer && boost::starts_with(comment, "Z:")) {
|
||||
m_print_z = get_z_height(comment);
|
||||
return;
|
||||
}
|
||||
|
||||
if (boost::starts_with(comment, " CONFIG_BLOCK_START")) {
|
||||
m_in_config_block = true;
|
||||
return;
|
||||
}
|
||||
if (boost::starts_with(comment, " CONFIG_BLOCK_END")) {
|
||||
m_in_config_block = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Belt printer: derive the physical tilt magnitude from the slicing-rotation
|
||||
// angle header comment (used to enable the preview's belt view). Only the belt
|
||||
// header carries it outside the config block; the config block lists the key
|
||||
// for every printer, belt or not.
|
||||
if (!m_in_config_block && boost::starts_with(comment, " belt_slice_rotation_angle = ")) {
|
||||
try {
|
||||
m_result.belt_tilt_angle = std::abs(std::stof(std::string(comment.substr(29))));
|
||||
} catch (...) {}
|
||||
return;
|
||||
}
|
||||
// wipe start tag
|
||||
if (boost::starts_with(comment, reserved_tag(ETags::Wipe_Start))) {
|
||||
m_wiping = true;
|
||||
@@ -5378,6 +5479,9 @@ void GCodeProcessor::process_G1(const std::array<std::optional<double>, 4>& axes
|
||||
m_seams_detector.set_first_vertex(m_result.moves.back().position - m_extruder_offsets[filament_id] - plate_offset);
|
||||
}
|
||||
|
||||
if (type == EMoveType::Extrude)
|
||||
add_object_mass(filament_id, area_filament_cross_section * delta_pos[E]);
|
||||
|
||||
// store move
|
||||
store_move_vertex(type);
|
||||
}
|
||||
@@ -6138,6 +6242,13 @@ void GCodeProcessor::process_G92(const GCodeReader::GCodeLine& line)
|
||||
if (line.has_z()) {
|
||||
m_origin[Z] = m_end_position[Z] - line.z() * lengths_scale_factor;
|
||||
any_found = true;
|
||||
// Belt only: the start G-code's purge-blob advance + G92 Z0 resets leave a constant
|
||||
// machine-Z origin offset here; the designed-view back-transform subtracts it so
|
||||
// toolpaths map to the model's belt coordinate (gcode Z). Gated on belt_tilt_angle
|
||||
// (set from the belt header, parsed before the body) so non-belt G-code processing
|
||||
// is byte-identical — no unconditional work on the shared path.
|
||||
if (m_result.belt_tilt_angle != 0.f)
|
||||
m_result.belt_z_origin = m_origin[Z];
|
||||
}
|
||||
|
||||
if (line.has_e()) {
|
||||
@@ -7116,6 +7227,22 @@ void GCodeProcessor::store_move_vertex(EMoveType type, EMovePathType path_type,
|
||||
m_result.print_statistics.total_travel_distance += m_travel_dist;
|
||||
}
|
||||
|
||||
// During the start G-code "prepare" stage the toolhead Z is not yet a real
|
||||
// print height on a normal printer, so it is pinned to the first-layer height
|
||||
// to keep the preview tidy. Belt printers are the exception: there the Z is
|
||||
// written explicitly by the belt kinematics and the designed-view back-transform
|
||||
// couples machine Z into the rendered model Y (the belt tilt mixes the height
|
||||
// and belt-feed axes). Overriding Z therefore back-transforms the last
|
||||
// prepare-stage move (the unretract before the first extrusion) to model
|
||||
// Y ~= 0, and the libvgcode path builder then draws a phantom extrusion
|
||||
// segment from Y ~= 0 to the first real toolpath. Keep the real Z for belt
|
||||
// printers so prepare-stage moves map correctly. Gated on belt_tilt_angle (set
|
||||
// from the G-code header before the body is processed) so non-belt processing
|
||||
// is byte-identical.
|
||||
const float store_z = (m_processing_start_custom_gcode && m_result.belt_tilt_angle == 0.f)
|
||||
? m_first_layer_height
|
||||
: m_end_position[Z] - m_z_offset;
|
||||
|
||||
m_result.moves.push_back({
|
||||
m_last_line_id,
|
||||
type,
|
||||
@@ -7123,7 +7250,7 @@ void GCodeProcessor::store_move_vertex(EMoveType type, EMovePathType path_type,
|
||||
static_cast<unsigned char>(filament_id),
|
||||
m_cp_color.current,
|
||||
//BBS: add plate's offset to the rendering vertices
|
||||
Vec3f(m_end_position[X] + m_x_offset, m_end_position[Y] + m_y_offset, m_processing_start_custom_gcode ? m_first_layer_height : m_end_position[Z]- m_z_offset) + m_extruder_offsets[filament_id],
|
||||
Vec3f(m_end_position[X] + m_x_offset, m_end_position[Y] + m_y_offset, store_z) + m_extruder_offsets[filament_id],
|
||||
static_cast<float>(m_end_position[E] - m_start_position[E]),
|
||||
m_feedrate,
|
||||
0.0f, // actual feedrate
|
||||
@@ -7163,6 +7290,73 @@ void GCodeProcessor::store_move_vertex(EMoveType type, EMovePathType path_type,
|
||||
}
|
||||
}
|
||||
|
||||
void GCodeProcessorResult::ObjectMass::add(const Sum &sum, const BoundingBoxf3 &extent, size_t layer)
|
||||
{
|
||||
box.merge(extent);
|
||||
if (printed_up_to_layer.size() <= layer)
|
||||
printed_up_to_layer.resize(layer + 1);
|
||||
printed_up_to_layer[layer].add(sum);
|
||||
}
|
||||
|
||||
void GCodeProcessor::add_object_mass(int filament_id, float volume)
|
||||
{
|
||||
// Skirt, prime tower and custom G-code belong to no object.
|
||||
const ExtrusionRole role = m_extrusion_role;
|
||||
if (volume <= 0.f || role == erNone || role == erSkirt || role == erWipeTower || role == erCustom || role == erMixed)
|
||||
return;
|
||||
|
||||
const bool has_density = size_t(filament_id) < m_result.filament_densities.size() && m_result.filament_densities[filament_id] > 0.f;
|
||||
const double mass = double(volume) * (has_density ? m_result.filament_densities[filament_id] : DEFAULT_FILAMENT_DENSITY);
|
||||
// In the frame of the stored moves, the bead's center half its height below the nozzle, from the move's start to its end.
|
||||
const Vec3d half_height = 0.5 * double(m_height) * Vec3d::UnitZ();
|
||||
const Vec3d offset = Vec3d(m_x_offset, m_y_offset, -m_z_offset) - half_height + m_extruder_offsets[filament_id].cast<double>();
|
||||
const Vec3d start = Vec3d(m_start_position[X], m_start_position[Y], m_start_position[Z]) + offset;
|
||||
const Vec3d end = Vec3d(m_end_position[X], m_end_position[Y], m_end_position[Z]) + offset;
|
||||
// The second moments of a uniform segment.
|
||||
const GCodeProcessorResult::ObjectMass::Sum sum{ mass, double(volume), 0.5 * mass * (start + end),
|
||||
mass / 3. * (start.cwiseProduct(start) + start.cwiseProduct(end) + end.cwiseProduct(end)) };
|
||||
// Of the bead's center line and its height, as its width is only estimated. Merged, as a wall along an axis is flat.
|
||||
BoundingBoxf3 extent;
|
||||
extent.merge(start.cwiseMin(end) - half_height);
|
||||
extent.merge(start.cwiseMax(end) + half_height);
|
||||
const bool part = role != erBrim && !is_support(role);
|
||||
const size_t layer = std::max<unsigned int>(1, m_layer_id) - 1;
|
||||
|
||||
m_result.plate_mass.add(sum, extent, layer);
|
||||
// The brim belongs to the plate alone.
|
||||
if (role == erBrim || !m_mass_locator)
|
||||
return;
|
||||
const auto add = [&sum, &extent, layer](std::vector<GCodeProcessorResult::ObjectMass> &masses, int index) {
|
||||
if (index < 0)
|
||||
return;
|
||||
if (masses.size() <= size_t(index))
|
||||
masses.resize(index + 1);
|
||||
masses[index].add(sum, extent, layer);
|
||||
};
|
||||
// At the nozzle's height, which the layers print at.
|
||||
const MassLocation location = m_mass_locator(0.5 * (start + end) + half_height, !part);
|
||||
if (part) {
|
||||
add(m_result.object_masses, location.object);
|
||||
add(m_result.body_masses, location.body);
|
||||
} else
|
||||
add(m_result.support_masses, location.object);
|
||||
}
|
||||
|
||||
void GCodeProcessor::finalize_object_masses()
|
||||
{
|
||||
const auto accumulate = [](GCodeProcessorResult::ObjectMass &object) {
|
||||
for (size_t i = 1; i < object.printed_up_to_layer.size(); ++i)
|
||||
object.printed_up_to_layer[i].add(object.printed_up_to_layer[i - 1]);
|
||||
};
|
||||
accumulate(m_result.plate_mass);
|
||||
for (GCodeProcessorResult::ObjectMass &object : m_result.object_masses)
|
||||
accumulate(object);
|
||||
for (GCodeProcessorResult::ObjectMass &body : m_result.body_masses)
|
||||
accumulate(body);
|
||||
for (GCodeProcessorResult::ObjectMass &support : m_result.support_masses)
|
||||
accumulate(support);
|
||||
}
|
||||
|
||||
void GCodeProcessor::set_extrusion_role(ExtrusionRole role)
|
||||
{
|
||||
m_used_filaments.process_role_cache(this);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#include "libslic3r/CommonDefs.hpp"
|
||||
#include "libslic3r/libslic3r.h"
|
||||
#include "libslic3r/BoundingBox.hpp"
|
||||
#include "libslic3r/Polygon.hpp"
|
||||
#include "libslic3r/Config.hpp"
|
||||
#include "libslic3r/ArcFitter.hpp"
|
||||
@@ -12,6 +13,7 @@
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
#include "libslic3r/CustomGCode.hpp"
|
||||
#include "libslic3r/MultiNozzleUtils.hpp"
|
||||
#include "libslic3r/GCode/MachineFrameTransform.hpp"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cassert>
|
||||
@@ -34,6 +36,9 @@ namespace Slic3r {
|
||||
|
||||
class Print;
|
||||
|
||||
// For a filament whose density is not set, in g/cm³.
|
||||
inline constexpr float DEFAULT_FILAMENT_DENSITY = 1.245f;
|
||||
|
||||
// slice warnings enum strings
|
||||
#define NOZZLE_HRC_CHECKER "the_actual_nozzle_hrc_smaller_than_the_required_nozzle_hrc"
|
||||
#define BED_TEMP_TOO_HIGH_THAN_FILAMENT "bed_temperature_too_high_than_filament"
|
||||
@@ -269,9 +274,44 @@ class Print;
|
||||
std::vector<std::string> params; // extra msg info
|
||||
};
|
||||
|
||||
// Material extruded for the plate, one object instance or one connected body of it, for their centers of mass.
|
||||
struct ObjectMass
|
||||
{
|
||||
struct Sum
|
||||
{
|
||||
double mass{ 0. };
|
||||
double volume{ 0. };
|
||||
Vec3d moment{ Vec3d::Zero() };
|
||||
// Of the mass about the origin along each axis, the sums of m x^2, m y^2 and m z^2.
|
||||
Vec3d second{ Vec3d::Zero() };
|
||||
|
||||
void add(const Sum &other)
|
||||
{
|
||||
mass += other.mass;
|
||||
volume += other.volume;
|
||||
moment += other.moment;
|
||||
second += other.second;
|
||||
}
|
||||
};
|
||||
// Everything printed up to each layer id, the plate's with brim, raft and supports, and the box it fills.
|
||||
std::vector<Sum> printed_up_to_layer;
|
||||
BoundingBoxf3 box;
|
||||
// Of an object, whether it is an assembly.
|
||||
bool assembly{ false };
|
||||
|
||||
Sum total() const { return printed_up_to_layer.empty() ? Sum{} : printed_up_to_layer.back(); }
|
||||
void add(const Sum &sum, const BoundingBoxf3 &extent, size_t layer);
|
||||
};
|
||||
|
||||
std::string filename;
|
||||
unsigned int id;
|
||||
std::vector<MoveVertex> moves;
|
||||
ObjectMass plate_mass;
|
||||
// One per object instance, and one per connected body of the instances of several, when the sliced objects were at hand.
|
||||
std::vector<ObjectMass> object_masses;
|
||||
std::vector<ObjectMass> body_masses;
|
||||
// One per object instance, of its supports and raft.
|
||||
std::vector<ObjectMass> support_masses;
|
||||
// Positions of ends of lines of the final G-code this->filename after TimeProcessor::post_process() finalizes the G-code.
|
||||
std::vector<size_t> lines_ends;
|
||||
Pointfs printable_area;
|
||||
@@ -290,6 +330,19 @@ class Print;
|
||||
bool support_traditional_timelapse{true};
|
||||
float printable_height;
|
||||
float z_offset;
|
||||
// Belt printer: physical tilt magnitude (deg) parsed from the slicing-rotation
|
||||
// header comment; used to enable the preview's belt view.
|
||||
float belt_tilt_angle{ 0.f };
|
||||
// Belt printer: machine-Z origin offset (mm) left in m_origin[Z] by the start
|
||||
// G-code (purge-blob belt advance + G92 Z0 resets). Move positions are stored
|
||||
// as gcode_Z + this offset, so the designed-view back-transform must subtract it
|
||||
// to recover the model's belt coordinate.
|
||||
float belt_z_origin{ 0.f };
|
||||
// Belt printer: post-gcode shear/scale/post_remap is configured and
|
||||
// non-identity. When set, the layer Z values in `moves` are in the
|
||||
// machine frame and should not be compared against `printable_height`
|
||||
// (which lives in the build-volume frame).
|
||||
bool machine_frame_transform_active{ false };
|
||||
SettingsIds settings_ids;
|
||||
size_t filaments_count;
|
||||
bool backtrace_enabled;
|
||||
@@ -346,6 +399,10 @@ class Print;
|
||||
filename = std::forward<Other>(other).filename;
|
||||
id = std::forward<Other>(other).id;
|
||||
moves = std::forward<Other>(other).moves;
|
||||
plate_mass = std::forward<Other>(other).plate_mass;
|
||||
object_masses = std::forward<Other>(other).object_masses;
|
||||
body_masses = std::forward<Other>(other).body_masses;
|
||||
support_masses = std::forward<Other>(other).support_masses;
|
||||
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;
|
||||
@@ -385,6 +442,9 @@ class Print;
|
||||
// Keep the SKIPPABLE per-type time on a copied result.
|
||||
skippable_part_time = std::forward<Other>(other).skippable_part_time;
|
||||
initial_layer_time = std::forward<Other>(other).initial_layer_time;
|
||||
belt_tilt_angle = std::forward<Other>(other).belt_tilt_angle;
|
||||
belt_z_origin = std::forward<Other>(other).belt_z_origin;
|
||||
machine_frame_transform_active = std::forward<Other>(other).machine_frame_transform_active;
|
||||
#if ENABLE_GCODE_VIEWER_STATISTICS
|
||||
time = std::forward<Other>(other).time;
|
||||
#endif
|
||||
@@ -1082,9 +1142,22 @@ class Print;
|
||||
};
|
||||
#endif // ENABLE_GCODE_VIEWER_DATA_CHECKING
|
||||
|
||||
// The object instance and the connected body of an instance of several that a point lies in, -1 for none.
|
||||
struct MassLocation
|
||||
{
|
||||
int object{ -1 };
|
||||
int body{ -1 };
|
||||
};
|
||||
// For a support, the object instance only.
|
||||
using MassLocator = std::function<MassLocation(const Vec3d &point, bool support)>;
|
||||
|
||||
private:
|
||||
CommandProcessor m_command_processor;
|
||||
GCodeReader m_parser;
|
||||
// Belt printer: the belt keys of the loaded file's config block (plus the bed they
|
||||
// are relative to), handed to the preview through export_config_for_render() so the
|
||||
// belt view and its back-transform follow the file, not the selected printer.
|
||||
DynamicConfig m_belt_render_config;
|
||||
EUnits m_units;
|
||||
EPositioningType m_global_positioning_type;
|
||||
EPositioningType m_e_local_positioning_type;
|
||||
@@ -1105,6 +1178,7 @@ class Print;
|
||||
bool m_skippable{false};
|
||||
SkipType m_skippable_type{SkipType::stNone};
|
||||
int m_object_label_id{-1};
|
||||
MassLocator m_mass_locator;
|
||||
float m_print_z{0.0f};
|
||||
std::vector<float> m_remaining_volume;
|
||||
ExtruderTemps m_filament_nozzle_temp;
|
||||
@@ -1154,12 +1228,19 @@ class Print;
|
||||
// m_result.nozzle_group_result, which is handed over only after the stream for the
|
||||
// pre-heat injector's second pass and gates the richer change-time model.
|
||||
std::shared_ptr<MultiNozzleUtils::NozzleGroupResultBase> m_nozzle_group_result;
|
||||
bool m_manual_filament_change;
|
||||
bool m_manual_filament_change{ false };
|
||||
|
||||
//BBS: x, y offset for gcode generated
|
||||
double m_x_offset{ 0 };
|
||||
double m_y_offset{ 0 };
|
||||
|
||||
// Belt-printer post-gcode shear/scale/post_remap. Used by
|
||||
// check_multi_extruder_gcode_valid to undo the machine-frame
|
||||
// transform on move positions so bounds checks operate in the
|
||||
// pre-machine-frame (build-volume) frame.
|
||||
MachineFrameTransform m_machine_frame_transform;
|
||||
bool m_belt_printer{ false };
|
||||
|
||||
unsigned int m_line_id;
|
||||
unsigned int m_last_line_id;
|
||||
float m_feedrate; // mm/s
|
||||
@@ -1189,6 +1270,7 @@ class Print;
|
||||
float m_first_layer_height; // mm
|
||||
float m_zero_layer_height; // mm
|
||||
bool m_processing_start_custom_gcode;
|
||||
bool m_in_config_block;
|
||||
unsigned int m_g1_line_id;
|
||||
unsigned int m_layer_id;
|
||||
CpColor m_cp_color;
|
||||
@@ -1251,6 +1333,13 @@ class Print;
|
||||
const std::vector<std::set<int>>& unprintable_filament_types );
|
||||
void apply_config(const PrintConfig& config);
|
||||
void set_print(Print* print) { m_print = print; }
|
||||
// Locates extrusions in the objects and bodies it numbers, those objects listed beforehand.
|
||||
void set_mass_locator(MassLocator locator, std::vector<GCodeProcessorResult::ObjectMass> objects)
|
||||
{
|
||||
m_mass_locator = std::move(locator);
|
||||
m_result.support_masses.assign(objects.size(), {});
|
||||
m_result.object_masses = std::move(objects);
|
||||
}
|
||||
// Hand the nozzle grouping context to the estimator BEFORE the streaming replay, so the
|
||||
// per-slot machine-limit resolution can follow the active nozzle. Null is fine (slot 0).
|
||||
void initialize_from_context(const std::shared_ptr<MultiNozzleUtils::NozzleGroupResultBase>& nozzle_group_result) {
|
||||
@@ -1505,6 +1594,8 @@ class Print;
|
||||
|
||||
//BBS: different path_type is only used for arc move
|
||||
void store_move_vertex(EMoveType type, EMovePathType path_type = EMovePathType::Noop_move, bool internal_only = false);
|
||||
void add_object_mass(int filament_id, float volume);
|
||||
void finalize_object_masses();
|
||||
|
||||
void set_extrusion_role(ExtrusionRole role);
|
||||
// Resolve the SKIPPABLE_TYPE payload to a SkipType.
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
#include "MachineFrameTransform.hpp"
|
||||
#include "../Geometry.hpp"
|
||||
#include "../Point.hpp"
|
||||
#include "../PrintConfig.hpp"
|
||||
#include "../libslic3r.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
bool MachineFrameTransform::init_from_config(const PrintConfig &config)
|
||||
{
|
||||
m_active = false;
|
||||
m_transform = Transform3d::Identity();
|
||||
m_transform_inverse = Transform3d::Identity();
|
||||
|
||||
if (!config.belt_printer.value)
|
||||
return false;
|
||||
|
||||
// The machine-frame transform is derived from the single belt tilt (axis +
|
||||
// angle) that also drives the pre-slice mesh rotation. Expert decouple lets
|
||||
// the machine-frame angle differ from the slicing rotation; otherwise both
|
||||
// use belt_slice_rotation_angle.
|
||||
const BeltRotationAxis axis = config.belt_slice_rotation.value;
|
||||
if (axis == BeltRotationAxis::None || axis == BeltRotationAxis::Z)
|
||||
return false; // Z is an in-plane spin: no machine-frame tilt.
|
||||
|
||||
const double angle_deg = config.belt_frame_tilt_decouple.value
|
||||
? config.belt_frame_tilt_angle.value
|
||||
: config.belt_slice_rotation_angle.value;
|
||||
if (std::abs(angle_deg) <= EPSILON)
|
||||
return false;
|
||||
|
||||
const double angle_rad = Geometry::deg2rad(angle_deg);
|
||||
const double sin_a = std::sin(angle_rad);
|
||||
if (std::abs(sin_a) <= EPSILON)
|
||||
return false;
|
||||
const double cot_a = std::cos(angle_rad) / sin_a;
|
||||
const double inv_sin = 1.0 / std::abs(sin_a);
|
||||
|
||||
// This stage runs after the conventional belt axis swap. For an X-axis
|
||||
// slicing rotation, remapped Y is model height and remapped Z is travel
|
||||
// along the belt. Convert those Cartesian coordinates to machine axes with
|
||||
// the established belt-printer convention:
|
||||
// machine gantry = model height / sin(a)
|
||||
// machine belt = model belt + model height * cot(a)
|
||||
// The Y-rotation case is the same mapping on X/Z, with the rotation sign.
|
||||
// At 45 degrees tan/cot and sin/cos are equal, which previously hid the
|
||||
// incorrect complementary-angle formulas used by this unified transform.
|
||||
Matrix3d shear = Matrix3d::Identity();
|
||||
Matrix3d scale = Matrix3d::Identity();
|
||||
if (axis == BeltRotationAxis::X) {
|
||||
shear(2, 1) = cot_a; // Z from Y
|
||||
scale(1, 1) = inv_sin; // Y
|
||||
} else { // BeltRotationAxis::Y
|
||||
shear(2, 0) = -cot_a; // Z from X
|
||||
scale(0, 0) = inv_sin; // X
|
||||
}
|
||||
|
||||
// Apply shear first, then scale (the historical default ShearThenScale order:
|
||||
// result = scale * shear * p). For the canonical 45°/X belt this maps
|
||||
// (x,y,z) -> (x, y/sin, y + z), matching the previous per-axis config.
|
||||
Transform3d combined = Transform3d::Identity();
|
||||
combined.linear() = scale * shear;
|
||||
|
||||
if (combined.isApprox(Transform3d::Identity()))
|
||||
return false;
|
||||
|
||||
m_transform = combined;
|
||||
m_transform_inverse = combined.inverse();
|
||||
m_active = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
Vec3d MachineFrameTransform::apply(const Vec3d &pos) const
|
||||
{
|
||||
if (!m_active)
|
||||
return pos;
|
||||
return m_transform * pos;
|
||||
}
|
||||
|
||||
Vec3d MachineFrameTransform::apply_inverse(const Vec3d &pos) const
|
||||
{
|
||||
if (!m_active)
|
||||
return pos;
|
||||
return m_transform_inverse * pos;
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,54 @@
|
||||
#ifndef slic3r_MachineFrameTransform_hpp_
|
||||
#define slic3r_MachineFrameTransform_hpp_
|
||||
|
||||
#include "../libslic3r.h"
|
||||
#include "../Point.hpp"
|
||||
#include "../PrintConfig.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// Post-stage machine-frame transform for belt printers.
|
||||
//
|
||||
// Applied in BeltKinematics::to_machine AFTER the back-transform and
|
||||
// the gcode_remap_* axis remap. Maps Cartesian (axis-permuted) G-code
|
||||
// coordinates into the printer's physical machine frame.
|
||||
//
|
||||
// Derived entirely from the single belt tilt (belt_slice_rotation axis +
|
||||
// belt_slice_rotation_angle): a shear coupling the height axis to the belt-feed
|
||||
// axis (factor cot a) plus a 1/sin a scale on the gantry-height axis. The expert
|
||||
// belt_frame_tilt_decouple flag lets the machine-frame angle differ from the
|
||||
// pre-slice rotation angle via belt_frame_tilt_angle.
|
||||
class MachineFrameTransform
|
||||
{
|
||||
public:
|
||||
MachineFrameTransform() = default;
|
||||
|
||||
// Initialize from belt printer config. Returns true if a non-identity
|
||||
// transform was computed. Inactive when belt_printer is disabled or
|
||||
// both shear and scale are identity.
|
||||
bool init_from_config(const PrintConfig &config);
|
||||
|
||||
// Apply the transform to a point. Returns pos unchanged if not active.
|
||||
Vec3d apply(const Vec3d &pos) const;
|
||||
|
||||
// Apply the inverse transform. Returns pos unchanged if not active.
|
||||
// Used by validators that need to compare emitted machine-frame
|
||||
// coordinates against build-volume bounds.
|
||||
Vec3d apply_inverse(const Vec3d &pos) const;
|
||||
|
||||
bool is_active() const { return m_active; }
|
||||
|
||||
// The composed shear*scale transform (identity when inactive). Exposed so the
|
||||
// G-code viewer can build the machine->model back-transform for the upright
|
||||
// ("designed") belt preview.
|
||||
const Transform3d& transform() const { return m_transform; }
|
||||
|
||||
private:
|
||||
bool m_active = false;
|
||||
Transform3d m_transform = Transform3d::Identity();
|
||||
Transform3d m_transform_inverse = Transform3d::Identity();
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // slic3r_MachineFrameTransform_hpp_
|
||||
@@ -0,0 +1,25 @@
|
||||
#include "MachineKinematics.hpp"
|
||||
#include "../Point.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// Moved verbatim from GCodeWriter::apply_axis_remap().
|
||||
Vec3d CartesianKinematics::apply_axis_remap(const Vec3d &pos) const
|
||||
{
|
||||
if (!has_axis_remap())
|
||||
return pos;
|
||||
auto remap = [this, &pos](int r) -> double {
|
||||
int axis = r % 3;
|
||||
if (r < 3) return pos[axis];
|
||||
if (r < 6) return -pos[axis];
|
||||
return m_build_vol_max[axis] - pos[axis];
|
||||
};
|
||||
return { remap(m_remap_x), remap(m_remap_y), remap(m_remap_z) };
|
||||
}
|
||||
|
||||
Vec3d CartesianKinematics::to_machine(const Vec3d &p) const
|
||||
{
|
||||
return this->apply_axis_remap(p);
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,92 @@
|
||||
#ifndef slic3r_MachineKinematics_hpp_
|
||||
#define slic3r_MachineKinematics_hpp_
|
||||
|
||||
#include "../Point.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// The frame contract for emitted movement.
|
||||
//
|
||||
// GCodeWriter produces points in the *logical placed* frame: plate offsets have
|
||||
// already been subtracted, but no machine-specific mapping has been applied.
|
||||
// A MachineKinematics turns that into the coordinates actually written to
|
||||
// G-code, and answers the two structural questions the writer needs in order to
|
||||
// decide which axis words it may omit.
|
||||
//
|
||||
// This is a seam for writer-generated movement only. Start/end/custom G-code,
|
||||
// classic wipe-tower output and GCodeWriter::extrude_arc_to_xy() do NOT pass
|
||||
// through it: they write machine coordinates directly.
|
||||
class MachineKinematics
|
||||
{
|
||||
public:
|
||||
virtual ~MachineKinematics() = default;
|
||||
|
||||
// Logical placed point -> emitted machine point.
|
||||
virtual Vec3d to_machine(const Vec3d &p) const = 0;
|
||||
|
||||
// True when a move must emit X, Y and Z because omitting a word would be
|
||||
// wrong under this mapping. Deliberately not called "couples_axes": a pure
|
||||
// axis permutation forces full emission without physically coupling axes.
|
||||
virtual bool must_emit_all_axes() const = 0;
|
||||
|
||||
// True when a lift must be suppressed while the current position is unknown,
|
||||
// because _travel_to_z() re-emits the logical X/Y through this mapping and an
|
||||
// uninitialised position would map to a bogus machine point -- for a reverse
|
||||
// mapping, the far corner of the bed.
|
||||
virtual bool suppress_lift_at_unknown_position() const = 0;
|
||||
|
||||
// True when a G2/G3 arc in the logical XY plane is still the same arc in the
|
||||
// machine frame. Arc moves emit only X, Y, I and J, so this asks a narrower
|
||||
// question than must_emit_all_axes(): whether logical X and Y reach the
|
||||
// machine unchanged. A mapping that only negates or reverses Z keeps its
|
||||
// arcs; one that permutes X or Y moves the arc out of the plane that I/J
|
||||
// describes, and a shear turns the circle into an ellipse G2/G3 cannot
|
||||
// express at all.
|
||||
virtual bool supports_arc_moves() const = 0;
|
||||
|
||||
// Configuration. GCodeWriter forwards its setters here so that the state
|
||||
// lives with the strategy and a strategy installed before the setters run
|
||||
// still receives it.
|
||||
virtual void set_axis_remap(int rx, int ry, int rz) = 0;
|
||||
virtual void set_build_volume_max(const Vec3d &max) = 0;
|
||||
};
|
||||
|
||||
// Axis remap only -- the historical GCodeWriter behaviour, moved verbatim.
|
||||
//
|
||||
// The remap encodes, per output axis, which source axis feeds it and how:
|
||||
// r < 3 : source axis r, unchanged
|
||||
// r < 6 : source axis r-3, negated
|
||||
// else : source axis r-6, reversed within the build volume
|
||||
class CartesianKinematics : public MachineKinematics
|
||||
{
|
||||
public:
|
||||
Vec3d to_machine(const Vec3d &p) const override;
|
||||
|
||||
bool must_emit_all_axes() const override { return this->has_axis_remap(); }
|
||||
bool suppress_lift_at_unknown_position() const override { return this->has_axis_remap(); }
|
||||
|
||||
// X and Y must reach the machine untouched. Because the remap is a
|
||||
// permutation, pinning those two also pins Z to Z, so a mapping that only
|
||||
// negates or reverses Z still supports arcs -- every word a G2/G3 emits is
|
||||
// unchanged by it.
|
||||
bool supports_arc_moves() const override { return m_remap_x == 0 && m_remap_y == 1; }
|
||||
|
||||
void set_axis_remap(int rx, int ry, int rz) override
|
||||
{ m_remap_x = rx; m_remap_y = ry; m_remap_z = rz; }
|
||||
void set_build_volume_max(const Vec3d &max) override { m_build_vol_max = max; }
|
||||
|
||||
bool has_axis_remap() const
|
||||
{ return m_remap_x != 0 || m_remap_y != 1 || m_remap_z != 2; }
|
||||
|
||||
protected:
|
||||
Vec3d apply_axis_remap(const Vec3d &pos) const;
|
||||
|
||||
int m_remap_x { 0 };
|
||||
int m_remap_y { 1 };
|
||||
int m_remap_z { 2 };
|
||||
Vec3d m_build_vol_max { Vec3d::Zero() };
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // slic3r_MachineKinematics_hpp_
|
||||
@@ -22,9 +22,6 @@
|
||||
namespace Slic3r {
|
||||
namespace PreciseSeam {
|
||||
|
||||
// Import EnforcedBlockedSeamPoint from SeamPlacerImpl namespace for convenience
|
||||
using SeamPlacerImpl::EnforcedBlockedSeamPoint;
|
||||
|
||||
// Geometry and its exterior bounds are prepared together, then treated as read-only.
|
||||
struct ModifierRegion {
|
||||
ExPolygon polygon;
|
||||
@@ -137,7 +134,7 @@ SegmentExtraction extract_perimeter_segments(const PreparedPerimeter &prepared,
|
||||
|
||||
// Result of weak modifier segment processing
|
||||
struct WeakModifierSegment {
|
||||
EnforcedBlockedSeamPoint type; // Enforced/Blocked/Neutral
|
||||
SeamPlacerImpl::EnforcedBlockedSeamPoint type; // Enforced/Blocked/Neutral
|
||||
Point left_point; // Coordinates of left (first) point of segment
|
||||
PerimeterPosition left_position; // Position on the source perimeter before insertion/refinement.
|
||||
Point right_point; // Coordinates of right (last) point of segment
|
||||
|
||||
@@ -706,7 +706,7 @@ void compute_global_occlusion(GlobalModelInfo &result, const PrintObject *po,
|
||||
SeamPosition seam_position = spAligned) {
|
||||
BOOST_LOG_TRIVIAL(debug)
|
||||
<< "SeamPlacer: gather occlusion meshes: start";
|
||||
auto obj_transform = po->trafo_centered();
|
||||
auto obj_transform = po->trafo_sliced();
|
||||
indexed_triangle_set triangle_set;
|
||||
indexed_triangle_set negative_volumes_set;
|
||||
//add all parts
|
||||
@@ -796,7 +796,7 @@ void gather_enforcers_blockers(GlobalModelInfo &result, const PrintObject *po) {
|
||||
BOOST_LOG_TRIVIAL(debug)
|
||||
<< "SeamPlacer: build AABB trees for raycasting enforcers/blockers: start";
|
||||
|
||||
auto obj_transform = po->trafo_centered();
|
||||
auto obj_transform = po->trafo_sliced();
|
||||
|
||||
for (const ModelVolume *mv : po->model_object()->volumes) {
|
||||
// Collect painting only from model parts (what the gizmo edits) and negative volumes (the only way
|
||||
|
||||
@@ -40,8 +40,7 @@ std::string get_error_string(const ThumbnailErrors& errors);
|
||||
|
||||
|
||||
typedef std::vector<std::pair<GCodeThumbnailsFormat, Vec2d>> GCodeThumbnailDefinitionsList;
|
||||
using namespace std::literals;
|
||||
std::pair<GCodeThumbnailDefinitionsList, ThumbnailErrors> make_and_check_thumbnail_list(const std::string& thumbnails_string, const std::string_view def_ext = "PNG"sv);
|
||||
std::pair<GCodeThumbnailDefinitionsList, ThumbnailErrors> make_and_check_thumbnail_list(const std::string& thumbnails_string, const std::string_view def_ext = "PNG");
|
||||
std::pair<GCodeThumbnailDefinitionsList, ThumbnailErrors> make_and_check_thumbnail_list(const ConfigBase &config);
|
||||
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include "Utils.hpp"
|
||||
#include "format.hpp"
|
||||
#include "I18N.hpp"
|
||||
#include "../BeltBrim.hpp"
|
||||
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <vector>
|
||||
@@ -419,6 +420,10 @@ bool ToolOrdering::insert_wipe_tower_extruder()
|
||||
{
|
||||
if (!m_print_config_ptr || !m_print_config_ptr->enable_prime_tower)
|
||||
return false;
|
||||
// Belt mode has no classic wipe tower; the dedicated wipe tower filament
|
||||
// must not inject extra toolchanges into the purge prism planning.
|
||||
if (m_print_config_ptr->belt_printer)
|
||||
return false;
|
||||
if (m_print_config_ptr->wipe_tower_filament == 0)
|
||||
return false;
|
||||
|
||||
@@ -516,6 +521,11 @@ ToolOrdering::ToolOrdering(const PrintObject &object, unsigned int first_extrude
|
||||
zs.emplace_back(layer->print_z);
|
||||
for (auto layer : object.support_layers())
|
||||
zs.emplace_back(layer->print_z);
|
||||
// Belt brim apron bands sit below the object's first layer and have no
|
||||
// layer of their own, but tools_for_layer() asserts an exact Z match, so
|
||||
// their print_z must be part of the ordering.
|
||||
for (const BeltBrimBand &band : object.belt_brim_prologue())
|
||||
zs.emplace_back(band.print_z);
|
||||
this->initialize_layers(zs);
|
||||
}
|
||||
|
||||
@@ -560,6 +570,10 @@ ToolOrdering::ToolOrdering(const Print &print, unsigned int first_extruder, bool
|
||||
zs.emplace_back(layer->print_z);
|
||||
for (auto layer : object->support_layers())
|
||||
zs.emplace_back(layer->print_z);
|
||||
// See the single-object ctor: belt brim apron bands need their own
|
||||
// ordering entries or tools_for_layer() will assert.
|
||||
for (const BeltBrimBand &band : object->belt_brim_prologue())
|
||||
zs.emplace_back(band.print_z);
|
||||
|
||||
max_layer_height = std::max(max_layer_height, object->config().layer_height.value);
|
||||
}
|
||||
@@ -994,6 +1008,42 @@ void ToolOrdering::collect_extruders(const PrintObject &object, const std::vecto
|
||||
}
|
||||
}
|
||||
|
||||
// Belt brim apron bands own their layers outright: they print below the
|
||||
// object's first layer, so no object or support layer claims an extruder there
|
||||
// and process_layer() would bail out at "Nothing to extrude". Claim the
|
||||
// object's outer wall filament, in the same raw 1-based domain the loops above
|
||||
// push. Deliberately not layer_tools.has_object, which drives skirt marking
|
||||
// and wiping overrides.
|
||||
if (! object.belt_brim_prologue().empty()) {
|
||||
// 1-based, same domain the object/support pushes above use; reindexed to 0-based
|
||||
// with the rest of the list later.
|
||||
const unsigned int brim_filament = object.belt_brim_filament();
|
||||
for (const BeltBrimBand &band : object.belt_brim_prologue()) {
|
||||
if (band.fills.empty())
|
||||
continue;
|
||||
LayerTools &layer_tools = this->tools_for_layer(band.print_z);
|
||||
layer_tools.extruders.push_back(brim_filament);
|
||||
}
|
||||
}
|
||||
|
||||
// Coincident brim bands (belt_brim_by_layer) print ON an object layer rather than
|
||||
// below it, but that layer can produce no InstanceVisit in process_layer - a
|
||||
// zero-extrusion lead-in slice with no coinciding support - and the band would then
|
||||
// be silently dropped. Register the brim filament on every layer that carries a
|
||||
// coincident band, in the same 1-based domain as the prologue push above, so a brim
|
||||
// pass always exists there.
|
||||
if (object.has_belt_brim()) {
|
||||
const unsigned int brim_filament = object.belt_brim_filament();
|
||||
const auto &by_layer = object.belt_brim_by_layer();
|
||||
const size_t n = std::min(by_layer.size(), object.layers().size());
|
||||
for (size_t i = 0; i < n; ++ i) {
|
||||
if (by_layer[i].empty())
|
||||
continue;
|
||||
LayerTools &layer_tools = this->tools_for_layer(object.layers()[i]->print_z);
|
||||
layer_tools.extruders.push_back(brim_filament);
|
||||
}
|
||||
}
|
||||
|
||||
for (auto& layer : m_layer_tools) {
|
||||
// Sort and remove duplicates
|
||||
sort_remove_duplicates(layer.extruders);
|
||||
@@ -1036,12 +1086,28 @@ void ToolOrdering::fill_wipe_tower_partitions(const PrintConfig &config, coordf_
|
||||
}
|
||||
|
||||
//FIXME this is a hack to get the ball rolling.
|
||||
// The `print_z < object_bottom_z` clause reads "below the object" as "raft
|
||||
// gap". On a belt printer that is wrong: the brim apron legitimately prints
|
||||
// below the object's first layer, and treating those layers as raft would put a
|
||||
// wipe tower at negative Z. A belt printer never prints the classic
|
||||
// prime tower (Print::has_wipe_tower()), so simply drop the clause there.
|
||||
//
|
||||
// Gate on config.belt_printer, NOT on has_belt_brim: every layer below the
|
||||
// object bottom on a belt printer is legitimately a sub-object stream - brim
|
||||
// apron, belt support printed below Z0, or the object's own lead-in - and none of
|
||||
// them is ever raft, because Print::validate() rejects raft_layers>0 on a belt
|
||||
// printer outright. Narrowing this to has_belt_brim would reclassify
|
||||
// belt-support-below-floor layers as raft on brim-less belt prints and reintroduce
|
||||
// the negative-Z wipe tower, so the broad belt_printer gate is correct.
|
||||
const bool belt_no_raft_gap = config.belt_printer.value;
|
||||
for (LayerTools < : m_layer_tools)
|
||||
lt.has_wipe_tower |= ((lt.has_object || lt.has_support) && (config.timelapse_type == TimelapseType::tlSmooth || lt.wipe_tower_partitions > 0))
|
||||
|| lt.print_z < object_bottom_z + EPSILON;
|
||||
|| (! belt_no_raft_gap && lt.print_z < object_bottom_z + EPSILON);
|
||||
|
||||
// Test for a raft, insert additional wipe tower layer to fill in the raft separation gap.
|
||||
for (size_t i = 0; i + 1 < m_layer_tools.size(); ++ i) {
|
||||
// Skipped on belt printers for the same reason as the clause above: layers
|
||||
// below the object are brim apron, not raft.
|
||||
for (size_t i = 0; ! belt_no_raft_gap && i + 1 < m_layer_tools.size(); ++ i) {
|
||||
const LayerTools < = m_layer_tools[i];
|
||||
const LayerTools <_next = m_layer_tools[i + 1];
|
||||
if (lt.print_z < object_bottom_z + EPSILON && lt_next.print_z >= object_bottom_z + EPSILON) {
|
||||
|
||||
@@ -84,7 +84,17 @@ public:
|
||||
|
||||
void set_layer_tools_ptr(const LayerTools* lt) { m_layer_tools = lt; }
|
||||
|
||||
// Returns true if entity is not printed with its usual extruder for a given
|
||||
// copy -- i.e. it was claimed as a wiping/purge extrusion. Public because the
|
||||
// belt purge prism uses it to tell which of its fills actually carry purge
|
||||
// from the ones that are unclaimed waste (Print::_plan_belt_purge()).
|
||||
bool is_entity_overridden(const ExtrusionEntity* entity, const PrintObject *object, size_t copy_id) const {
|
||||
auto it = entity_map.find(std::make_tuple(entity, object));
|
||||
return it != entity_map.end() && copy_id < it->second.size() && it->second[copy_id] != -1;
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
int first_nonsoluble_extruder_on_layer(const PrintConfig& print_config) const;
|
||||
int last_nonsoluble_extruder_on_layer(const PrintConfig& print_config) const;
|
||||
|
||||
@@ -94,12 +104,6 @@ private:
|
||||
void set_support_extruder_override(const PrintObject* object, size_t copy_id, int extruder, size_t num_of_copies);
|
||||
void set_support_interface_extruder_override(const PrintObject* object, size_t copy_id, int extruder, size_t num_of_copies);
|
||||
|
||||
// Returns true in case that entity is not printed with its usual extruder for a given copy:
|
||||
bool is_entity_overridden(const ExtrusionEntity* entity, const PrintObject *object, size_t copy_id) const {
|
||||
auto it = entity_map.find(std::make_tuple(entity, object));
|
||||
return it == entity_map.end() ? false : it->second[copy_id] != -1;
|
||||
}
|
||||
|
||||
std::map<std::tuple<const ExtrusionEntity*, const PrintObject *>, ExtruderPerCopy> entity_map; // to keep track of who prints what
|
||||
// BBS
|
||||
std::map<const PrintObject*, int> support_map;
|
||||
|
||||
@@ -1472,6 +1472,10 @@ public:
|
||||
void set_multi_nozzle_group_result(const MultiNozzleUtils::LayeredNozzleGroupResult *multi_nozzle_group_result) { m_multi_nozzle_group_result = multi_nozzle_group_result; }
|
||||
void set_physical_extruder_map(const std::vector<int> &physical_extruder_map) { m_physical_extruder_map = physical_extruder_map; }
|
||||
|
||||
// physical_extruder_map defaults to the single element {0} and is only widened to one entry
|
||||
// per extruder on IMEX printers, so indexing it by tool is out of range on any other
|
||||
// multi-extruder machine. Fall back to the tool's own index, matching the bounds-checked
|
||||
// form GCodeProcessor uses for the same map.
|
||||
private:
|
||||
std::string set_normal_acceleration() {
|
||||
std::vector<unsigned int> accelerations = m_is_first_layer ? m_first_layer_normal_accelerations : m_normal_accelerations;
|
||||
|
||||
@@ -28,6 +28,28 @@ static const ConfigOption *option_of(const ConfigBase &config, const char *key)
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool prime_tower_is_printed(const ConfigBase &config, int used_filaments, int num_objects, bool has_mixed_filament)
|
||||
{
|
||||
const auto *ept = config.option<ConfigOptionBool>("enable_prime_tower");
|
||||
if (ept == nullptr || !ept->value)
|
||||
return false;
|
||||
// normalize_fdm_2 only reconsiders the option when the plate uses a filament; below that it
|
||||
// leaves the user's choice alone, and there is no tower to reason about either way.
|
||||
if (used_filaments <= 0)
|
||||
return true;
|
||||
|
||||
const ConfigOption *timelapse = option_of(config, "timelapse_type");
|
||||
const bool smooth_timelapse = timelapse != nullptr && timelapse->getInt() == int(TimelapseType::tlSmooth);
|
||||
const auto *wrapping = config.option<ConfigOptionBool>("enable_wrapping_detection");
|
||||
const bool enable_wrapping = wrapping != nullptr && wrapping->value;
|
||||
if (smooth_timelapse || enable_wrapping)
|
||||
return true;
|
||||
|
||||
const ConfigOption *sequence = option_of(config, "print_sequence");
|
||||
const bool by_object = sequence != nullptr && sequence->getInt() == int(PrintSequence::ByObject);
|
||||
return !((used_filaments == 1 && !has_mixed_filament) || (by_object && num_objects > 1));
|
||||
}
|
||||
|
||||
WipeTowerType resolve_wipe_tower_type(const ConfigBase &config)
|
||||
{
|
||||
// printer_model is what the CLI keys its Bambu Lab detection on; the GUI's vendor flag
|
||||
|
||||
@@ -44,4 +44,17 @@ WipeTowerFootprint estimate_wipe_tower_footprint(const ConfigBase
|
||||
double layer_height,
|
||||
double max_object_height);
|
||||
|
||||
// Whether a prime tower is PRINTED for a plate. The footprint estimate above answers how big a
|
||||
// tower is and never reads enable_prime_tower or print_sequence, so it reports a size for a plate
|
||||
// that prints no tower at all; the authority is DynamicPrintConfig::normalize_fdm_2(), which clears
|
||||
// enable_prime_tower before the plate is sliced. This mirrors that rule so pre-slice consumers can
|
||||
// ask it without mutating a config, and a test pins the two together.
|
||||
//
|
||||
// used_filaments: the plate's filament SLOTS as authored - a mixed slot counts once, matching
|
||||
// Print::extruders(), which is what normalize_fdm_2 is handed.
|
||||
// num_objects: objects printed on the plate; only ByObject sequencing looks at it.
|
||||
// has_mixed_filament: filament_is_mixed is a PROJECT option, so it is passed rather than read -
|
||||
// `config` only has to carry the print preset's own keys.
|
||||
bool prime_tower_is_printed(const ConfigBase &config, int used_filaments, int num_objects, bool has_mixed_filament);
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
@@ -1,580 +0,0 @@
|
||||
#include "GCodeSender.hpp"
|
||||
#include <iostream>
|
||||
#include <istream>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <boost/algorithm/string/predicate.hpp>
|
||||
#include <boost/algorithm/string/trim.hpp>
|
||||
#include <boost/date_time/posix_time/posix_time.hpp>
|
||||
#include <boost/lexical_cast.hpp>
|
||||
|
||||
#if defined(__APPLE__) || defined(__OpenBSD__)
|
||||
#include <termios.h>
|
||||
#endif
|
||||
#ifdef __APPLE__
|
||||
#include <sys/ioctl.h>
|
||||
#include <IOKit/serial/ioss.h>
|
||||
#endif
|
||||
#ifdef __linux__
|
||||
#include <sys/ioctl.h>
|
||||
#include <fcntl.h>
|
||||
#include "/usr/include/asm-generic/ioctls.h"
|
||||
|
||||
/* The following definitions are kindly borrowed from:
|
||||
/usr/include/asm-generic/termbits.h
|
||||
Unfortunately we cannot just include that one because
|
||||
it would redefine the "struct termios" already defined
|
||||
the <termios.h> already included by Boost.ASIO. */
|
||||
#define K_NCCS 19
|
||||
struct termios2 {
|
||||
tcflag_t c_iflag;
|
||||
tcflag_t c_oflag;
|
||||
tcflag_t c_cflag;
|
||||
tcflag_t c_lflag;
|
||||
cc_t c_line;
|
||||
cc_t c_cc[K_NCCS];
|
||||
speed_t c_ispeed;
|
||||
speed_t c_ospeed;
|
||||
};
|
||||
#define BOTHER CBAUDEX
|
||||
|
||||
#endif
|
||||
|
||||
//#define DEBUG_SERIAL
|
||||
#ifdef DEBUG_SERIAL
|
||||
#include <cstdlib>
|
||||
#include <fstream>
|
||||
std::fstream fs;
|
||||
#endif
|
||||
|
||||
#define KEEP_SENT 20
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
GCodeSender::GCodeSender()
|
||||
: io(), serial(io), can_send(false), sent(0), open(false), error(false),
|
||||
connected(false), queue_paused(false)
|
||||
{
|
||||
#ifdef DEBUG_SERIAL
|
||||
std::srand(std::time(nullptr));
|
||||
#endif
|
||||
}
|
||||
|
||||
GCodeSender::~GCodeSender()
|
||||
{
|
||||
this->disconnect();
|
||||
}
|
||||
|
||||
bool
|
||||
GCodeSender::connect(std::string devname, unsigned int baud_rate)
|
||||
{
|
||||
this->disconnect();
|
||||
|
||||
this->set_error_status(false);
|
||||
try {
|
||||
this->serial.open(devname);
|
||||
|
||||
this->serial.set_option(boost::asio::serial_port_base::parity(boost::asio::serial_port_base::parity::odd));
|
||||
this->serial.set_option(boost::asio::serial_port_base::character_size(boost::asio::serial_port_base::character_size(8)));
|
||||
this->serial.set_option(boost::asio::serial_port_base::flow_control(boost::asio::serial_port_base::flow_control::none));
|
||||
this->serial.set_option(boost::asio::serial_port_base::stop_bits(boost::asio::serial_port_base::stop_bits::one));
|
||||
this->set_baud_rate(baud_rate);
|
||||
|
||||
this->serial.close();
|
||||
this->serial.open(devname);
|
||||
this->serial.set_option(boost::asio::serial_port_base::parity(boost::asio::serial_port_base::parity::none));
|
||||
|
||||
// set baud rate again because set_option overwrote it
|
||||
this->set_baud_rate(baud_rate);
|
||||
this->open = true;
|
||||
this->reset();
|
||||
} catch (boost::system::system_error &) {
|
||||
this->set_error_status(true);
|
||||
return false;
|
||||
}
|
||||
|
||||
// a reset firmware expect line numbers to start again from 1
|
||||
this->sent = 0;
|
||||
this->last_sent.clear();
|
||||
|
||||
/* Initialize debugger */
|
||||
#ifdef DEBUG_SERIAL
|
||||
fs.open("serial.txt", std::fstream::out | std::fstream::trunc);
|
||||
#endif
|
||||
|
||||
// this gives some work to the io_service before it is started
|
||||
// (post() runs the supplied function in its thread)
|
||||
this->io.post(boost::bind(&GCodeSender::do_read, this));
|
||||
|
||||
// start reading in the background thread
|
||||
boost::thread t(boost::bind(&boost::asio::io_service::run, &this->io));
|
||||
this->background_thread.swap(t);
|
||||
|
||||
// always send a M105 to check for connection because firmware might be silent on connect
|
||||
//FIXME Vojtech: This is being sent too early, leading to line number synchronization issues,
|
||||
// from which the GCodeSender never recovers.
|
||||
// this->send("M105", true);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
GCodeSender::set_baud_rate(unsigned int baud_rate)
|
||||
{
|
||||
try {
|
||||
// This does not support speeds > 115200
|
||||
this->serial.set_option(boost::asio::serial_port_base::baud_rate(baud_rate));
|
||||
} catch (boost::system::system_error &) {
|
||||
boost::asio::serial_port::native_handle_type handle = this->serial.native_handle();
|
||||
|
||||
#if __APPLE__
|
||||
termios ios;
|
||||
::tcgetattr(handle, &ios);
|
||||
::cfsetspeed(&ios, baud_rate);
|
||||
speed_t newSpeed = baud_rate;
|
||||
ioctl(handle, IOSSIOSPEED, &newSpeed);
|
||||
::tcsetattr(handle, TCSANOW, &ios);
|
||||
#elif __linux__
|
||||
termios2 ios;
|
||||
if (ioctl(handle, TCGETS2, &ios))
|
||||
printf("Error in TCGETS2: %s\n", strerror(errno));
|
||||
ios.c_ispeed = ios.c_ospeed = baud_rate;
|
||||
ios.c_cflag &= ~CBAUD;
|
||||
ios.c_cflag |= BOTHER | CLOCAL | CREAD;
|
||||
ios.c_cc[VMIN] = 1; // Minimum of characters to read, prevents eof errors when 0 bytes are read
|
||||
ios.c_cc[VTIME] = 1;
|
||||
if (ioctl(handle, TCSETS2, &ios))
|
||||
printf("Error in TCSETS2: %s\n", strerror(errno));
|
||||
|
||||
#elif __OpenBSD__
|
||||
struct termios ios;
|
||||
::tcgetattr(handle, &ios);
|
||||
::cfsetspeed(&ios, baud_rate);
|
||||
if (::tcsetattr(handle, TCSAFLUSH, &ios) != 0)
|
||||
printf("Failed to set baud rate: %s\n", strerror(errno));
|
||||
#else
|
||||
//throw Slic3r::InvalidArgument("OS does not currently support custom bauds");
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
GCodeSender::disconnect()
|
||||
{
|
||||
if (!this->open) return;
|
||||
this->open = false;
|
||||
this->connected = false;
|
||||
this->io.post(boost::bind(&GCodeSender::do_close, this));
|
||||
this->background_thread.join();
|
||||
this->io.reset();
|
||||
/*
|
||||
if (this->error_status()) {
|
||||
throw(boost::system::system_error(boost::system::error_code(),
|
||||
"Error while closing the device"));
|
||||
}
|
||||
*/
|
||||
|
||||
#ifdef DEBUG_SERIAL
|
||||
fs << "DISCONNECTED" << std::endl << std::flush;
|
||||
fs.close();
|
||||
#endif
|
||||
}
|
||||
|
||||
bool
|
||||
GCodeSender::is_connected() const
|
||||
{
|
||||
return this->connected;
|
||||
}
|
||||
|
||||
bool
|
||||
GCodeSender::wait_connected(unsigned int timeout) const
|
||||
{
|
||||
using namespace boost::posix_time;
|
||||
ptime t0 = second_clock::local_time() + seconds(timeout);
|
||||
while (!this->connected) {
|
||||
if (second_clock::local_time() > t0) return false;
|
||||
boost::this_thread::sleep(boost::posix_time::milliseconds(100));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
size_t
|
||||
GCodeSender::queue_size() const
|
||||
{
|
||||
boost::lock_guard<boost::mutex> l(this->queue_mutex);
|
||||
return this->queue.size();
|
||||
}
|
||||
|
||||
void
|
||||
GCodeSender::pause_queue()
|
||||
{
|
||||
boost::lock_guard<boost::mutex> l(this->queue_mutex);
|
||||
this->queue_paused = true;
|
||||
}
|
||||
|
||||
void
|
||||
GCodeSender::resume_queue()
|
||||
{
|
||||
{
|
||||
boost::lock_guard<boost::mutex> l(this->queue_mutex);
|
||||
this->queue_paused = false;
|
||||
}
|
||||
this->send();
|
||||
}
|
||||
|
||||
void
|
||||
GCodeSender::purge_queue(bool priority)
|
||||
{
|
||||
boost::lock_guard<boost::mutex> l(this->queue_mutex);
|
||||
if (priority) {
|
||||
// clear priority queue
|
||||
std::list<std::string> empty;
|
||||
std::swap(this->priqueue, empty);
|
||||
} else {
|
||||
// clear queue
|
||||
std::queue<std::string> empty;
|
||||
std::swap(this->queue, empty);
|
||||
this->queue_paused = false;
|
||||
}
|
||||
}
|
||||
|
||||
// purge log and return its contents
|
||||
std::vector<std::string>
|
||||
GCodeSender::purge_log()
|
||||
{
|
||||
boost::lock_guard<boost::mutex> l(this->log_mutex);
|
||||
std::vector<std::string> retval;
|
||||
retval.reserve(this->log.size());
|
||||
while (!this->log.empty()) {
|
||||
retval.push_back(this->log.front());
|
||||
this->log.pop();
|
||||
}
|
||||
return retval;
|
||||
}
|
||||
|
||||
std::string
|
||||
GCodeSender::getT() const
|
||||
{
|
||||
boost::lock_guard<boost::mutex> l(this->log_mutex);
|
||||
return this->T;
|
||||
}
|
||||
|
||||
std::string
|
||||
GCodeSender::getB() const
|
||||
{
|
||||
boost::lock_guard<boost::mutex> l(this->log_mutex);
|
||||
return this->B;
|
||||
}
|
||||
|
||||
void
|
||||
GCodeSender::do_close()
|
||||
{
|
||||
this->set_error_status(false);
|
||||
boost::system::error_code ec;
|
||||
this->serial.cancel(ec);
|
||||
if (ec) this->set_error_status(true);
|
||||
this->serial.close(ec);
|
||||
if (ec) this->set_error_status(true);
|
||||
}
|
||||
|
||||
void
|
||||
GCodeSender::set_error_status(bool e)
|
||||
{
|
||||
boost::lock_guard<boost::mutex> l(this->error_mutex);
|
||||
this->error = e;
|
||||
}
|
||||
|
||||
bool
|
||||
GCodeSender::error_status() const
|
||||
{
|
||||
boost::lock_guard<boost::mutex> l(this->error_mutex);
|
||||
return this->error;
|
||||
}
|
||||
|
||||
void
|
||||
GCodeSender::do_read()
|
||||
{
|
||||
// read one line
|
||||
boost::asio::async_read_until(
|
||||
this->serial,
|
||||
this->read_buffer,
|
||||
'\n',
|
||||
boost::bind(
|
||||
&GCodeSender::on_read,
|
||||
this,
|
||||
boost::asio::placeholders::error,
|
||||
boost::asio::placeholders::bytes_transferred
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void
|
||||
GCodeSender::on_read(const boost::system::error_code& error,
|
||||
size_t bytes_transferred)
|
||||
{
|
||||
this->set_error_status(false);
|
||||
if (error) {
|
||||
#ifdef __APPLE__
|
||||
if (error.value() == 45) {
|
||||
// OS X bug: http://osdir.com/ml/lib.boost.asio.user/2008-08/msg00004.html
|
||||
this->do_read();
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
// printf("ERROR: [%d] %s\n", error.value(), error.message().c_str());
|
||||
// error can be true even because the serial port was closed.
|
||||
// In this case it is not a real error, so ignore.
|
||||
if (this->open) {
|
||||
this->do_close();
|
||||
this->set_error_status(true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
std::istream is(&this->read_buffer);
|
||||
std::string line;
|
||||
std::getline(is, line);
|
||||
if (!line.empty()) {
|
||||
#ifdef DEBUG_SERIAL
|
||||
fs << "<< " << line << std::endl << std::flush;
|
||||
#endif
|
||||
|
||||
// note that line might contain \r at its end
|
||||
// parse incoming line
|
||||
if (!this->connected
|
||||
&& (boost::starts_with(line, "start")
|
||||
|| boost::starts_with(line, "Grbl ")
|
||||
|| boost::starts_with(line, "ok")
|
||||
|| boost::contains(line, "T:"))) {
|
||||
this->connected = true;
|
||||
{
|
||||
boost::lock_guard<boost::mutex> l(this->queue_mutex);
|
||||
this->can_send = true;
|
||||
}
|
||||
this->send();
|
||||
} else if (boost::starts_with(line, "ok")) {
|
||||
{
|
||||
boost::lock_guard<boost::mutex> l(this->queue_mutex);
|
||||
this->can_send = true;
|
||||
}
|
||||
this->send();
|
||||
} else if (boost::istarts_with(line, "resend") // Marlin uses "Resend: "
|
||||
|| boost::istarts_with(line, "rs")) {
|
||||
// extract the first number from line
|
||||
boost::algorithm::trim_left_if(line, !boost::algorithm::is_digit());
|
||||
size_t toresend = boost::lexical_cast<size_t>(line.substr(0, line.find_first_not_of("0123456789")));
|
||||
|
||||
#ifdef DEBUG_SERIAL
|
||||
fs << "!! line num out of sync: toresend = " << toresend << ", sent = " << sent << ", last_sent.size = " << last_sent.size() << std::endl;
|
||||
#endif
|
||||
|
||||
if (toresend > this->sent - this->last_sent.size() && toresend <= this->sent) {
|
||||
{
|
||||
boost::lock_guard<boost::mutex> l(this->queue_mutex);
|
||||
|
||||
const auto lines_to_resend = this->sent - toresend + 1;
|
||||
#ifdef DEBUG_SERIAL
|
||||
fs << "!! resending " << lines_to_resend << " lines" << std::endl;
|
||||
#endif
|
||||
// move the unsent lines to priqueue
|
||||
this->priqueue.insert(
|
||||
this->priqueue.begin(), // insert at the beginning
|
||||
this->last_sent.begin() + this->last_sent.size() - lines_to_resend,
|
||||
this->last_sent.end()
|
||||
);
|
||||
|
||||
// we can empty last_sent because it's not useful anymore
|
||||
this->last_sent.clear();
|
||||
|
||||
// start resending with the requested line number
|
||||
this->sent = toresend - 1;
|
||||
this->can_send = true;
|
||||
}
|
||||
this->send();
|
||||
} else {
|
||||
printf("Cannot resend %zu (oldest we have is %zu)\n", toresend, this->sent - this->last_sent.size());
|
||||
}
|
||||
} else if (boost::starts_with(line, "wait")) {
|
||||
// ignore
|
||||
} else {
|
||||
// push any other line into the log
|
||||
boost::lock_guard<boost::mutex> l(this->log_mutex);
|
||||
this->log.push(line);
|
||||
}
|
||||
|
||||
// parse temperature info
|
||||
{
|
||||
size_t pos = line.find("T:");
|
||||
if (pos != std::string::npos && line.size() > pos + 2) {
|
||||
// we got temperature info
|
||||
boost::lock_guard<boost::mutex> l(this->log_mutex);
|
||||
this->T = line.substr(pos+2, line.find_first_not_of("0123456789.", pos+2) - (pos+2));
|
||||
|
||||
pos = line.find("B:");
|
||||
if (pos != std::string::npos && line.size() > pos + 2) {
|
||||
// we got bed temperature info
|
||||
this->B = line.substr(pos+2, line.find_first_not_of("0123456789.", pos+2) - (pos+2));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this->do_read();
|
||||
}
|
||||
|
||||
void
|
||||
GCodeSender::send(const std::vector<std::string> &lines, bool priority)
|
||||
{
|
||||
// append lines to queue
|
||||
{
|
||||
boost::lock_guard<boost::mutex> l(this->queue_mutex);
|
||||
for (std::vector<std::string>::const_iterator line = lines.begin(); line != lines.end(); ++line) {
|
||||
if (priority) {
|
||||
this->priqueue.push_back(*line);
|
||||
} else {
|
||||
this->queue.push(*line);
|
||||
}
|
||||
}
|
||||
}
|
||||
this->send();
|
||||
}
|
||||
|
||||
void
|
||||
GCodeSender::send(const std::string &line, bool priority)
|
||||
{
|
||||
// append line to queue
|
||||
{
|
||||
boost::lock_guard<boost::mutex> l(this->queue_mutex);
|
||||
if (priority) {
|
||||
this->priqueue.push_back(line);
|
||||
} else {
|
||||
this->queue.push(line);
|
||||
}
|
||||
}
|
||||
this->send();
|
||||
}
|
||||
|
||||
void
|
||||
GCodeSender::send()
|
||||
{
|
||||
this->io.post(boost::bind(&GCodeSender::do_send, this));
|
||||
}
|
||||
|
||||
void
|
||||
GCodeSender::do_send()
|
||||
{
|
||||
boost::lock_guard<boost::mutex> l(this->queue_mutex);
|
||||
|
||||
// printer is not connected or we're still waiting for the previous ack
|
||||
if (!this->can_send) return;
|
||||
|
||||
std::string line;
|
||||
while (!this->priqueue.empty() || (!this->queue.empty() && !this->queue_paused)) {
|
||||
if (!this->priqueue.empty()) {
|
||||
line = this->priqueue.front();
|
||||
this->priqueue.pop_front();
|
||||
} else {
|
||||
line = this->queue.front();
|
||||
this->queue.pop();
|
||||
}
|
||||
|
||||
// strip comments
|
||||
size_t comment_pos = line.find_first_of(';');
|
||||
if (comment_pos != std::string::npos)
|
||||
line.erase(comment_pos, std::string::npos);
|
||||
boost::algorithm::trim(line);
|
||||
|
||||
// if line is not empty, send it
|
||||
if (!line.empty()) break;
|
||||
// if line is empty, process next item in queue
|
||||
}
|
||||
if (line.empty()) return;
|
||||
|
||||
// compute full line
|
||||
++ this->sent;
|
||||
#ifndef DEBUG_SERIAL
|
||||
const auto line_num = this->sent;
|
||||
#else
|
||||
// In DEBUG_SERIAL mode, test line re-synchronization by sending bad line number 1/4 of the time
|
||||
const auto line_num = std::rand() < RAND_MAX/4 ? 0 : this->sent;
|
||||
#endif
|
||||
std::string full_line = "N" + boost::lexical_cast<std::string>(line_num) + " " + line;
|
||||
|
||||
// calculate checksum
|
||||
int cs = 0;
|
||||
for (std::string::const_iterator it = full_line.begin(); it != full_line.end(); ++it)
|
||||
cs = cs ^ *it;
|
||||
|
||||
// write line to device
|
||||
full_line += "*";
|
||||
full_line += boost::lexical_cast<std::string>(cs);
|
||||
full_line += "\n";
|
||||
|
||||
#ifdef DEBUG_SERIAL
|
||||
fs << ">> " << full_line << std::flush;
|
||||
#endif
|
||||
|
||||
this->last_sent.push_back(line);
|
||||
this->can_send = false;
|
||||
|
||||
while (this->last_sent.size() > KEEP_SENT) {
|
||||
this->last_sent.pop_front();
|
||||
}
|
||||
|
||||
// we can't supply boost::asio::buffer(full_line) to async_write() because full_line is on the
|
||||
// stack and the buffer would lose its underlying storage causing memory corruption
|
||||
std::ostream os(&this->write_buffer);
|
||||
os << full_line;
|
||||
boost::asio::async_write(this->serial, this->write_buffer, boost::bind(&GCodeSender::on_write, this, boost::asio::placeholders::error,
|
||||
boost::asio::placeholders::bytes_transferred));
|
||||
}
|
||||
|
||||
void
|
||||
GCodeSender::on_write(const boost::system::error_code& error,
|
||||
size_t bytes_transferred)
|
||||
{
|
||||
this->set_error_status(false);
|
||||
if (error) {
|
||||
if (this->open) {
|
||||
this->do_close();
|
||||
this->set_error_status(true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this->do_send();
|
||||
}
|
||||
|
||||
void
|
||||
GCodeSender::set_DTR(bool on)
|
||||
{
|
||||
#if defined(_WIN32) && !defined(__SYMBIAN32__)
|
||||
boost::asio::serial_port_service::native_handle_type handle = this->serial.native_handle();
|
||||
if (on)
|
||||
EscapeCommFunction(handle, SETDTR);
|
||||
else
|
||||
EscapeCommFunction(handle, CLRDTR);
|
||||
#else
|
||||
int fd = this->serial.native_handle();
|
||||
int status;
|
||||
ioctl(fd, TIOCMGET, &status);
|
||||
if (on)
|
||||
status |= TIOCM_DTR;
|
||||
else
|
||||
status &= ~TIOCM_DTR;
|
||||
ioctl(fd, TIOCMSET, &status);
|
||||
#endif
|
||||
}
|
||||
|
||||
void
|
||||
GCodeSender::reset()
|
||||
{
|
||||
set_DTR(false);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
||||
set_DTR(true);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
||||
set_DTR(false);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(500));
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -1,81 +0,0 @@
|
||||
#ifndef slic3r_GCodeSender_hpp_
|
||||
#define slic3r_GCodeSender_hpp_
|
||||
|
||||
#include "libslic3r.h"
|
||||
#include <queue>
|
||||
#include <boost/core/noncopyable.hpp>
|
||||
#include <cstddef>
|
||||
#include <boost/asio/io_service.hpp>
|
||||
#include <boost/asio/serial_port.hpp>
|
||||
#include <boost/asio/streambuf.hpp>
|
||||
#include <list>
|
||||
#include <deque>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <boost/asio.hpp>
|
||||
#include <boost/noncopyable.hpp>
|
||||
#include <boost/bind/bind.hpp>
|
||||
#include <boost/thread.hpp>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
namespace asio = boost::asio;
|
||||
|
||||
class GCodeSender : private boost::noncopyable {
|
||||
public:
|
||||
GCodeSender();
|
||||
~GCodeSender();
|
||||
bool connect(std::string devname, unsigned int baud_rate);
|
||||
void send(const std::vector<std::string> &lines, bool priority = false);
|
||||
void send(const std::string &s, bool priority = false);
|
||||
void disconnect();
|
||||
bool error_status() const;
|
||||
bool is_connected() const;
|
||||
bool wait_connected(unsigned int timeout = 3) const;
|
||||
size_t queue_size() const;
|
||||
void pause_queue();
|
||||
void resume_queue();
|
||||
void purge_queue(bool priority = false);
|
||||
std::vector<std::string> purge_log();
|
||||
std::string getT() const;
|
||||
std::string getB() const;
|
||||
void set_DTR(bool on);
|
||||
void reset();
|
||||
|
||||
private:
|
||||
asio::io_service io;
|
||||
asio::serial_port serial;
|
||||
boost::thread background_thread;
|
||||
boost::asio::streambuf read_buffer, write_buffer;
|
||||
bool open; // whether the serial socket is connected
|
||||
bool connected; // whether the printer is online
|
||||
bool error;
|
||||
mutable boost::mutex error_mutex;
|
||||
|
||||
// this mutex guards queue, priqueue, can_send, queue_paused, sent, last_sent
|
||||
mutable boost::mutex queue_mutex;
|
||||
std::queue<std::string> queue;
|
||||
std::list<std::string> priqueue;
|
||||
bool can_send;
|
||||
bool queue_paused;
|
||||
size_t sent;
|
||||
std::deque<std::string> last_sent;
|
||||
|
||||
// this mutex guards log, T, B
|
||||
mutable boost::mutex log_mutex;
|
||||
std::queue<std::string> log;
|
||||
std::string T, B;
|
||||
|
||||
void set_baud_rate(unsigned int baud_rate);
|
||||
void set_error_status(bool e);
|
||||
void do_send();
|
||||
void on_write(const boost::system::error_code& error, size_t bytes_transferred);
|
||||
void do_close();
|
||||
void do_read();
|
||||
void on_read(const boost::system::error_code& error, size_t bytes_transferred);
|
||||
void send();
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif /* slic3r_GCodeSender_hpp_ */
|
||||
+241
-35
@@ -1,10 +1,12 @@
|
||||
#include "GCodeWriter.hpp"
|
||||
#include "Config.hpp"
|
||||
#include "Extruder.hpp"
|
||||
#include "Geometry.hpp"
|
||||
#include "I18N.hpp"
|
||||
#include "Point.hpp"
|
||||
#include "Polygon.hpp"
|
||||
#include "PrintConfig.hpp"
|
||||
#include "IMEXHelpers.hpp"
|
||||
#include "ClipperUtils.hpp"
|
||||
#include "Geometry/ArcWelder.hpp"
|
||||
#include "Line.hpp"
|
||||
@@ -31,6 +33,7 @@
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <math.h>
|
||||
#include <memory>
|
||||
|
||||
#ifdef __APPLE__
|
||||
#include <boost/spirit/include/karma.hpp>
|
||||
@@ -43,6 +46,57 @@ namespace Slic3r {
|
||||
|
||||
bool GCodeWriter::full_gcode_comment = true;
|
||||
|
||||
// A lift emitted through _travel_to_z() re-emits the stored logical X/Y under a
|
||||
// mapping that must emit every axis. While the position is unknown that X/Y is
|
||||
// the uninitialised origin, which maps to a real but wrong machine point, so the
|
||||
// lift has to be skipped rather than commanded.
|
||||
bool GCodeWriter::must_skip_lift_now() const
|
||||
{
|
||||
return m_kinematics->suppress_lift_at_unknown_position() && ! this->is_current_position_clear();
|
||||
}
|
||||
|
||||
bool GCodeWriter::point_on_first_layer(const Vec3d &point_logical) const
|
||||
{
|
||||
if (m_first_layer_point_test)
|
||||
return m_first_layer_point_test(point_logical);
|
||||
return m_is_first_layer;
|
||||
}
|
||||
|
||||
void GCodeWriter::set_axis_remap(int rx, int ry, int rz)
|
||||
{
|
||||
m_remap_x = rx;
|
||||
m_remap_y = ry;
|
||||
m_remap_z = rz;
|
||||
m_kinematics->set_axis_remap(rx, ry, rz);
|
||||
}
|
||||
|
||||
void GCodeWriter::set_build_volume_max(const Vec3d &max)
|
||||
{
|
||||
m_build_vol_max = max;
|
||||
m_kinematics->set_build_volume_max(max);
|
||||
}
|
||||
|
||||
void GCodeWriter::set_kinematics(std::unique_ptr<MachineKinematics> kinematics)
|
||||
{
|
||||
assert(kinematics);
|
||||
m_kinematics = std::move(kinematics);
|
||||
// Replay whatever was configured on the previous strategy so callers may
|
||||
// install the kinematics before or after set_axis_remap/set_build_volume_max.
|
||||
m_kinematics->set_axis_remap(m_remap_x, m_remap_y, m_remap_z);
|
||||
m_kinematics->set_build_volume_max(m_build_vol_max);
|
||||
}
|
||||
|
||||
// Kept as the writer-facing name for "this move must emit every axis word".
|
||||
bool GCodeWriter::has_axis_remap() const
|
||||
{
|
||||
return m_kinematics->must_emit_all_axes();
|
||||
}
|
||||
|
||||
Vec3d GCodeWriter::apply_axis_remap(const Vec3d &pos) const
|
||||
{
|
||||
return m_kinematics->to_machine(pos);
|
||||
}
|
||||
|
||||
bool GCodeWriter::supports_separate_travel_acceleration(GCodeFlavor flavor)
|
||||
{
|
||||
return (flavor == gcfRepetier || flavor == gcfMarlinFirmware || flavor == gcfRepRapFirmware);
|
||||
@@ -305,8 +359,16 @@ std::string GCodeWriter::set_temperature(unsigned int temperature, GCodeFlavor f
|
||||
std::string GCodeWriter::set_temperature(unsigned int temperature, bool wait, int tool) const
|
||||
{
|
||||
// set tool to -1 to make sure we won't emit T parameter for single extruder or SEMM
|
||||
if (!this->multiple_extruders || m_single_extruder_multi_material)
|
||||
if (!this->multiple_extruders || m_single_extruder_multi_material) {
|
||||
tool = -1;
|
||||
} else {
|
||||
// Every caller of this overload addresses filaments by LOGICAL id, but M104/M109
|
||||
// name a physical heater -- so translate at the one point they all pass through.
|
||||
// The static overload below is already physical-in (GCode.cpp:5952,
|
||||
// GCode/GCodeProcessor.cpp:1410) and must not be remapped, which is why the
|
||||
// translation lives here and not there.
|
||||
tool = imex_physical_heater_for(this->config.is_imex.value, this->config.physical_extruder_map, tool);
|
||||
}
|
||||
return set_temperature(temperature, this->config.gcode_flavor, wait, tool);
|
||||
}
|
||||
|
||||
@@ -529,26 +591,43 @@ std::string GCodeWriter::set_junction_deviation(double junction_deviation){
|
||||
return gcode.str();
|
||||
}
|
||||
|
||||
std::string GCodeWriter::set_pressure_advance(double pa) const
|
||||
std::string GCodeWriter::set_pressure_advance(double pa, int tool) const
|
||||
{
|
||||
std::ostringstream gcode;
|
||||
if (pa < 0)
|
||||
return gcode.str();
|
||||
if(m_is_bbl_printers){
|
||||
//SoftFever: set L1000 to use linear model
|
||||
gcode << "M900 K" <<std::setprecision(4)<< pa << " L1000 M10 ; Override pressure advance value\n";
|
||||
}
|
||||
else{
|
||||
if (FLAVOR_IS(gcfKlipper))
|
||||
gcode << "SET_PRESSURE_ADVANCE ADVANCE=" << std::setprecision(4) << pa << "; Override pressure advance value\n";
|
||||
else if(FLAVOR_IS(gcfRepRapFirmware))
|
||||
gcode << ("M572 D0 S") << std::setprecision(4) << pa << "; Override pressure advance value\n";
|
||||
else if (FLAVOR_IS(gcfRepetier))
|
||||
// Repetier M233: X is quadratic (K), Y is linear (L).
|
||||
// Applying the value to both parameters simultaneously.
|
||||
gcode << "M233 X" << std::setprecision(4) << pa << " Y" << std::setprecision(4) << pa << " ; Override pressure advance value\n";
|
||||
else
|
||||
gcode << "M900 K" <<std::setprecision(4)<< pa << "; Override pressure advance value\n";
|
||||
if (m_is_bbl_printers) {
|
||||
// SoftFever: set L1000 to use linear model
|
||||
gcode << "M900 K" << std::setprecision(4) << pa << " L1000 M10 ; Override pressure advance value\n";
|
||||
} else if (FLAVOR_IS(gcfKlipper)) {
|
||||
// Klipper routes PA by extruder name, not active tool.
|
||||
// Convention: first extruder is "extruder", subsequent are "extruder1", "extruder2", etc.
|
||||
gcode << "SET_PRESSURE_ADVANCE ADVANCE=" << std::setprecision(4) << pa;
|
||||
if (tool > 0)
|
||||
gcode << " EXTRUDER=extruder" << tool;
|
||||
else if (tool == 0)
|
||||
gcode << " EXTRUDER=extruder";
|
||||
gcode << "; Override pressure advance value\n";
|
||||
} else if (FLAVOR_IS(gcfRepRapFirmware)) {
|
||||
// RRF: M572 D<n> targets a specific extruder; a bare M572 applies to whatever tool is
|
||||
// currently selected and errors when there isn't one. Callers with no tool index keep
|
||||
// the historical D0 rather than the bare form: PA would otherwise depend on
|
||||
// tool-selection state for every RRF user, none of whom are asking for IMEX.
|
||||
gcode << "M572 D" << (tool >= 0 ? tool : 0)
|
||||
<< " S" << std::setprecision(4) << pa << "; Override pressure advance value\n";
|
||||
} else if (FLAVOR_IS(gcfRepetier)) {
|
||||
// Repetier M233: X is quadratic (K), Y is linear (L).
|
||||
// Applying the value to both parameters simultaneously.
|
||||
gcode << "M233 X" << std::setprecision(4) << pa << " Y" << std::setprecision(4) << pa << " ; Override pressure advance value\n";
|
||||
} else if (FLAVOR_IS(gcfMarlinFirmware)) {
|
||||
// Marlin 2.x supports T parameter for per-extruder LA
|
||||
gcode << "M900 K" << std::setprecision(4) << pa;
|
||||
if (tool >= 0)
|
||||
gcode << " T" << tool;
|
||||
gcode << "; Override pressure advance value\n";
|
||||
} else {
|
||||
// Marlin Legacy and everything else: single-extruder M900, no tool parameter
|
||||
gcode << "M900 K" << std::setprecision(4) << pa << "; Override pressure advance value\n";
|
||||
}
|
||||
return gcode.str();
|
||||
}
|
||||
@@ -796,8 +875,14 @@ std::string GCodeWriter::travel_to_xy(const Vec2d &point, const std::string &com
|
||||
Vec2d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset };
|
||||
|
||||
GCodeG1Formatter w;
|
||||
w.emit_xy(point_on_plate);
|
||||
auto speed = m_is_first_layer
|
||||
if (has_axis_remap()) {
|
||||
// Axis remap may couple XY with Z; emit full XYZ in machine coordinates.
|
||||
Vec3d machine = apply_axis_remap(Vec3d(point_on_plate.x(), point_on_plate.y(), m_pos.z()));
|
||||
w.emit_xyz(machine);
|
||||
} else {
|
||||
w.emit_xy(point_on_plate);
|
||||
}
|
||||
auto speed = this->point_on_first_layer(Vec3d(point_on_plate.x(), point_on_plate.y(), m_pos.z()))
|
||||
? this->config.get_abs_value_at("initial_layer_travel_speed", m_cached_extruder_idx) : this->config.travel_speed.get_at(m_cached_extruder_idx);
|
||||
w.emit_f(speed * 60.0);
|
||||
//BBS
|
||||
@@ -810,6 +895,8 @@ it will not perform subsequent lifts, even if Z was raised manually
|
||||
(i.e. with travel_to_z()) and thus _lifted was reduced. */
|
||||
std::string GCodeWriter::lazy_lift(LiftType lift_type, bool spiral_vase)
|
||||
{
|
||||
if (m_force_normal_lift)
|
||||
lift_type = LiftType::NormalLift;
|
||||
// check whether the above/below conditions are met
|
||||
double target_lift = 0;
|
||||
{
|
||||
@@ -824,6 +911,10 @@ std::string GCodeWriter::lazy_lift(LiftType lift_type, bool spiral_vase)
|
||||
// BBS
|
||||
if (m_lifted == 0 && m_to_lift == 0 && target_lift > 0) {
|
||||
if (spiral_vase) {
|
||||
if (this->must_skip_lift_now())
|
||||
// Record no lift, so a later unlift() does not descend from a
|
||||
// height that was never commanded.
|
||||
return "";
|
||||
m_lifted = target_lift;
|
||||
return this->_travel_to_z(m_pos(2) + target_lift, "lift Z");
|
||||
}
|
||||
@@ -836,8 +927,9 @@ std::string GCodeWriter::lazy_lift(LiftType lift_type, bool spiral_vase)
|
||||
}
|
||||
|
||||
// BBS: immediately execute an undelayed lift move with a spiral lift pattern
|
||||
// designed specifically for subsequent gcode injection (e.g. timelapse)
|
||||
// designed specifically for subsequent gcode injection (e.g. timelapse)
|
||||
std::string GCodeWriter::eager_lift(const LiftType type) {
|
||||
const LiftType effective_type = m_force_normal_lift ? LiftType::NormalLift : type;
|
||||
std::string lift_move;
|
||||
double target_lift = 0;
|
||||
{
|
||||
@@ -851,7 +943,7 @@ std::string GCodeWriter::eager_lift(const LiftType type) {
|
||||
}
|
||||
|
||||
// BBS: spiral lift only safe with known position
|
||||
if (type == LiftType::SpiralLift && this->is_current_position_clear()) {
|
||||
if (effective_type == LiftType::SpiralLift && this->is_current_position_clear()) {
|
||||
double radius = target_lift / (2 * PI * atan(filament()->travel_slope()));
|
||||
// static spiral alignment when no move in x,y plane.
|
||||
// spiral centra is a radius distance to the right (y=0)
|
||||
@@ -868,7 +960,12 @@ std::string GCodeWriter::eager_lift(const LiftType type) {
|
||||
}
|
||||
//BBS: if position is unknown use normal lift
|
||||
else if (target_lift > 0) {
|
||||
lift_move = _travel_to_z(m_pos(2) + target_lift, "normal lift Z");
|
||||
if (this->must_skip_lift_now())
|
||||
// Skipped, not deferred: leave m_lifted at zero below so unlift()
|
||||
// does not descend from a height that was never commanded.
|
||||
target_lift = 0.;
|
||||
else
|
||||
lift_move = _travel_to_z(m_pos(2) + target_lift, "normal lift Z");
|
||||
}
|
||||
m_lifted = target_lift;
|
||||
m_to_lift = 0;
|
||||
@@ -889,7 +986,12 @@ std::string GCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &co
|
||||
// BBS
|
||||
Vec3d dest_point = point;
|
||||
auto travel_speed =
|
||||
m_is_first_layer ? this->config.get_abs_value_at("initial_layer_travel_speed", m_cached_extruder_idx) : this->config.travel_speed.get_at(m_cached_extruder_idx);
|
||||
this->point_on_first_layer(Vec3d(point.x() - m_x_offset, point.y() - m_y_offset, point.z())) ? this->config.get_abs_value_at("initial_layer_travel_speed", m_cached_extruder_idx) : this->config.travel_speed.get_at(m_cached_extruder_idx);
|
||||
// See uses_pointwise_travel_speed(): the historical path deliberately emits the
|
||||
// raw configured speed in the final branch below, ignoring travel_speed.
|
||||
const double final_travel_speed = this->uses_pointwise_travel_speed()
|
||||
? travel_speed
|
||||
: this->config.travel_speed.get_at(m_cached_extruder_idx);
|
||||
//BBS: a z_hop need to be handle when travel
|
||||
if (std::abs(m_to_lift) > EPSILON) {
|
||||
assert(std::abs(m_lifted) < EPSILON);
|
||||
@@ -938,13 +1040,23 @@ std::string GCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &co
|
||||
Vec2d temp = delta_no_z.normalized() * delta(2) / tan(this->filament()->travel_slope());
|
||||
Vec3d slope_top_point = Vec3d(temp(0), temp(1), delta(2)) + source;
|
||||
GCodeG1Formatter w0;
|
||||
w0.emit_xyz(slope_top_point);
|
||||
// A slope lift is a straight (linear) diagonal move, so remapping its
|
||||
// endpoint is exact. Route the destination through apply_axis_remap()
|
||||
// when a remap is active (no-op at identity).
|
||||
w0.emit_xyz(has_axis_remap() ? apply_axis_remap(slope_top_point) : slope_top_point);
|
||||
w0.emit_f(travel_speed * 60.0);
|
||||
//BBS
|
||||
w0.emit_comment(GCodeWriter::full_gcode_comment, comment);
|
||||
slop_move = w0.string();
|
||||
}
|
||||
else if (m_to_lift_type == LiftType::NormalLift) {
|
||||
else if (m_to_lift_type == LiftType::NormalLift && ! this->must_skip_lift_now()) {
|
||||
// Only lift in place when the current position is known, for a mapping
|
||||
// that makes _travel_to_z re-emit logical X/Y: at print start (and after
|
||||
// custom gcode) m_pos.xy is still the uninitialised origin, which would
|
||||
// map to a bogus machine point. The xy_z_move below then travels straight
|
||||
// to the destination with full XYZ and establishes the correct position.
|
||||
// Mappings that do not need this (the historical Cartesian behaviour)
|
||||
// report false and keep lifting unconditionally.
|
||||
slop_move = _travel_to_z(target.z(), "normal lift Z");
|
||||
}
|
||||
}
|
||||
@@ -952,7 +1064,14 @@ std::string GCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &co
|
||||
std::string xy_z_move;
|
||||
{
|
||||
GCodeG1Formatter w0;
|
||||
if (this->is_current_position_clear()) {
|
||||
if (has_axis_remap()) {
|
||||
// Remap may couple XY with Z; emit full XYZ in machine coordinates.
|
||||
w0.emit_xyz(apply_axis_remap(target));
|
||||
w0.emit_f(travel_speed * 60.0);
|
||||
w0.emit_comment(GCodeWriter::full_gcode_comment, comment);
|
||||
xy_z_move = w0.string();
|
||||
}
|
||||
else if (this->is_current_position_clear()) {
|
||||
w0.emit_xyz(target);
|
||||
w0.emit_f(travel_speed * 60.0);
|
||||
w0.emit_comment(GCodeWriter::full_gcode_comment, comment);
|
||||
@@ -990,17 +1109,23 @@ std::string GCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &co
|
||||
Vec3d point_on_plate = { dest_point(0) - m_x_offset, dest_point(1) - m_y_offset, dest_point(2) };
|
||||
std::string out_string;
|
||||
GCodeG1Formatter w;
|
||||
if (!this->is_current_position_clear())
|
||||
if (has_axis_remap()) {
|
||||
// Remap may couple XY with Z; emit full XYZ in machine coordinates.
|
||||
w.emit_xyz(apply_axis_remap(point_on_plate));
|
||||
w.emit_f(final_travel_speed * 60.0);
|
||||
w.emit_comment(GCodeWriter::full_gcode_comment, comment);
|
||||
out_string = w.string();
|
||||
} else if (!this->is_current_position_clear())
|
||||
{
|
||||
//force to move xy first then z after filament change
|
||||
w.emit_xy(Vec2d(point_on_plate.x(), point_on_plate.y()));
|
||||
w.emit_f(this->config.travel_speed.get_at(m_cached_extruder_idx) * 60.0);
|
||||
w.emit_f(final_travel_speed * 60.0);
|
||||
w.emit_comment(GCodeWriter::full_gcode_comment, comment);
|
||||
out_string = w.string() + _travel_to_z(point_on_plate.z(), comment);
|
||||
} else {
|
||||
GCodeG1Formatter w;
|
||||
w.emit_xyz(point_on_plate);
|
||||
w.emit_f(this->config.travel_speed.get_at(m_cached_extruder_idx) * 60.0);
|
||||
w.emit_f(final_travel_speed * 60.0);
|
||||
w.emit_comment(GCodeWriter::full_gcode_comment, comment);
|
||||
out_string = w.string();
|
||||
}
|
||||
@@ -1035,12 +1160,19 @@ std::string GCodeWriter::_travel_to_z(double z, const std::string &comment)
|
||||
|
||||
double speed = this->config.travel_speed_z.get_at(m_cached_extruder_idx);
|
||||
if (speed == 0.) {
|
||||
speed = m_is_first_layer ? this->config.get_abs_value_at("initial_layer_travel_speed", m_cached_extruder_idx)
|
||||
: this->config.travel_speed.get_at(m_cached_extruder_idx);
|
||||
speed = this->point_on_first_layer(Vec3d(m_pos.x() - m_x_offset, m_pos.y() - m_y_offset, z))
|
||||
? this->config.get_abs_value_at("initial_layer_travel_speed", m_cached_extruder_idx)
|
||||
: this->config.travel_speed.get_at(m_cached_extruder_idx);
|
||||
}
|
||||
|
||||
GCodeG1Formatter w;
|
||||
w.emit_z(z);
|
||||
if (has_axis_remap()) {
|
||||
// Remap may couple Z with other axes; emit full XYZ.
|
||||
Vec3d machine = apply_axis_remap(Vec3d(m_pos.x() - m_x_offset, m_pos.y() - m_y_offset, z));
|
||||
w.emit_xyz(machine);
|
||||
} else {
|
||||
w.emit_z(z);
|
||||
}
|
||||
w.emit_f(speed * 60.0);
|
||||
//BBS
|
||||
w.emit_comment(GCodeWriter::full_gcode_comment, comment);
|
||||
@@ -1049,6 +1181,14 @@ std::string GCodeWriter::_travel_to_z(double z, const std::string &comment)
|
||||
|
||||
std::string GCodeWriter::_spiral_travel_to_z(double z, const Vec2d &ij_offset, const std::string &comment)
|
||||
{
|
||||
// A circular XY arc / spiral lift cannot be correctly axis-remapped by
|
||||
// transforming only its endpoint: the arc plane (G17/XY) and the I-J center
|
||||
// would change under the remap. When an axis remap is active, fall back to a
|
||||
// plain linear lift instead of emitting a possibly-wrong spiral/arc. This
|
||||
// single guard covers every spiral call site (lazy/eager lift and travel_to_xyz).
|
||||
if (has_axis_remap())
|
||||
return _travel_to_z(z, comment);
|
||||
|
||||
std::string output;
|
||||
double speed = this->config.travel_speed_z.get_at(m_cached_extruder_idx);
|
||||
|
||||
@@ -1155,7 +1295,12 @@ void GCodeWriter::extrude_to_xy(std::string &out, const Vec2d &point, double dE,
|
||||
Vec2d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset };
|
||||
|
||||
GCodeG1Formatter w;
|
||||
w.emit_xy(point_on_plate);
|
||||
if (has_axis_remap()) {
|
||||
Vec3d machine = apply_axis_remap(Vec3d(point_on_plate.x(), point_on_plate.y(), m_pos.z()));
|
||||
w.emit_xyz(machine);
|
||||
} else {
|
||||
w.emit_xy(point_on_plate);
|
||||
}
|
||||
if (!force_no_extrusion)
|
||||
w.emit_e(filament()->E());
|
||||
//BBS
|
||||
@@ -1163,6 +1308,42 @@ void GCodeWriter::extrude_to_xy(std::string &out, const Vec2d &point, double dE,
|
||||
w.append_to(out);
|
||||
}
|
||||
|
||||
// Approximate an arc with linear extrusions, for machine mappings that cannot
|
||||
// express a G2/G3 (see extrude_arc_to_xy). center_offset is I/J: the centre
|
||||
// relative to the CURRENT position, which is why this must run before m_pos is
|
||||
// updated.
|
||||
void GCodeWriter::extrude_arc_as_polyline(std::string &out, const Vec2d &point, const Vec2d ¢er_offset,
|
||||
double dE, const bool is_ccw,
|
||||
const std::string &comment, bool force_no_extrusion)
|
||||
{
|
||||
const Vec2d start = Vec2d(m_pos.x(), m_pos.y());
|
||||
const Vec2d centre = start + center_offset;
|
||||
const double r = (start - centre).norm();
|
||||
if (r < EPSILON) {
|
||||
// Degenerate: no arc to speak of, so a single move is exact.
|
||||
this->extrude_to_xy(out, point, dE, comment, force_no_extrusion);
|
||||
return;
|
||||
}
|
||||
|
||||
double a0 = std::atan2(start.y() - centre.y(), start.x() - centre.x());
|
||||
double a1 = std::atan2(point.y() - centre.y(), point.x() - centre.x());
|
||||
double sweep = a1 - a0;
|
||||
if (is_ccw) { while (sweep <= 0.) sweep += 2. * PI; }
|
||||
else { while (sweep >= 0.) sweep -= 2. * PI; }
|
||||
|
||||
// Segment count from a chord-deviation bound: r*(1-cos(dtheta/2)) <= tol.
|
||||
const double tol = 0.005; // mm
|
||||
const double dmax = (tol >= r) ? PI : 2. * std::acos(1. - tol / r);
|
||||
const int n = std::max(2, int(std::ceil(std::abs(sweep) / std::max(dmax, EPSILON))));
|
||||
|
||||
for (int i = 1; i <= n; ++ i) {
|
||||
const double a = a0 + sweep * (double(i) / double(n));
|
||||
const Vec2d p = (i == n) ? point
|
||||
: Vec2d(centre.x() + r * std::cos(a), centre.y() + r * std::sin(a));
|
||||
this->extrude_to_xy(out, p, dE / double(n), i == n ? comment : std::string(), force_no_extrusion);
|
||||
}
|
||||
}
|
||||
|
||||
//BBS: generate G2 or G3 extrude which moves by arc
|
||||
//point is end point which means X and Y axis
|
||||
//center_offset is I and J axis
|
||||
@@ -1175,6 +1356,23 @@ std::string GCodeWriter::extrude_arc_to_xy(const Vec2d& point, const Vec2d& cent
|
||||
|
||||
void GCodeWriter::extrude_arc_to_xy(std::string &out, const Vec2d& point, const Vec2d& center_offset, double dE, const bool is_ccw, const std::string& comment, bool force_no_extrusion)
|
||||
{
|
||||
// Arcs emit only X/Y/I/J, so a mapping that moves logical X or Y cannot be
|
||||
// expressed as a G2/G3. GCode::should_disable_arc_fitting() normally stops
|
||||
// arcs being generated at all for such a mapping, but this is public API, so
|
||||
// define the behaviour rather than asserting.
|
||||
//
|
||||
// This check MUST precede every state mutation below: falling through to
|
||||
// extrude_to_xy() after filament()->extrude(dE) would advance E twice.
|
||||
//
|
||||
// A single chord is not a safe substitute either -- a semicircle would become
|
||||
// its diameter and a full circle a stationary blob -- so approximate the arc
|
||||
// with linear segments bounded by a chord tolerance, splitting dE between
|
||||
// them in proportion to arc length.
|
||||
if (! m_kinematics->supports_arc_moves()) {
|
||||
this->extrude_arc_as_polyline(out, point, center_offset, dE, is_ccw, comment, force_no_extrusion);
|
||||
return;
|
||||
}
|
||||
|
||||
m_pos(0) = point(0);
|
||||
m_pos(1) = point(1);
|
||||
if (!force_no_extrusion)
|
||||
@@ -1215,10 +1413,18 @@ void GCodeWriter::extrude_to_xyz(std::string &out, const Vec3d &point, double dE
|
||||
Vec3d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset, point(2) };
|
||||
|
||||
GCodeG1Formatter w;
|
||||
if (z_changed)
|
||||
if (has_axis_remap()) {
|
||||
// z_changed was computed from the ORIGINAL slicing Z, but an axis remap can
|
||||
// make machine-Z depend on slicing X/Y. An X/Y-only move (slicing-Z
|
||||
// unchanged) would then drop the required machine-Z word, so always emit
|
||||
// full XYZ whenever a remap is active.
|
||||
point_on_plate = apply_axis_remap(point_on_plate);
|
||||
w.emit_xyz(point_on_plate);
|
||||
else
|
||||
} else if (z_changed) {
|
||||
w.emit_xyz(point_on_plate);
|
||||
} else {
|
||||
w.emit_xy(Vec2d(point_on_plate.x(), point_on_plate.y()));
|
||||
}
|
||||
if (!force_no_extrusion)
|
||||
w.emit_e(filament()->E());
|
||||
//BBS
|
||||
|
||||
+102
-29
@@ -10,31 +10,38 @@
|
||||
#include <string>
|
||||
#include <charconv>
|
||||
#include <vector>
|
||||
#include <functional>
|
||||
#include <utility>
|
||||
#include "Extruder.hpp"
|
||||
#include "Point.hpp"
|
||||
#include "Polygon.hpp"
|
||||
#include "PrintConfig.hpp"
|
||||
#include "Config.hpp"
|
||||
#include "GCode/MachineKinematics.hpp"
|
||||
#include <memory>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
|
||||
class GCodeWriter {
|
||||
public:
|
||||
GCodeConfig config;
|
||||
bool multiple_extruders;
|
||||
|
||||
GCodeWriter() :
|
||||
multiple_extruders(false), m_curr_filament_extruder(MAXIMUM_EXTRUDER_NUMBER, nullptr),
|
||||
m_curr_extruder_id (-1),
|
||||
m_cached_extruder_idx(0),
|
||||
m_single_extruder_multi_material(false),
|
||||
m_last_acceleration(0), m_max_acceleration(0),m_last_travel_acceleration(0), m_max_travel_acceleration(0),
|
||||
m_last_jerk(0), m_max_jerk_x(0), m_max_jerk_y(0),
|
||||
m_last_bed_temperature(0), m_last_bed_temperature_reached(true),
|
||||
multiple_extruders(false),
|
||||
m_lifted(0),
|
||||
m_to_lift(0),
|
||||
m_to_lift_type(LiftType::NormalLift),
|
||||
m_current_speed(3600), m_is_first_layer(true)
|
||||
m_is_first_layer(true), m_current_speed(3600),
|
||||
m_kinematics(std::make_unique<CartesianKinematics>()),
|
||||
m_cached_extruder_idx(0),
|
||||
m_curr_filament_extruder(MAXIMUM_EXTRUDER_NUMBER, nullptr),
|
||||
m_curr_extruder_id (-1),
|
||||
m_single_extruder_multi_material(false),
|
||||
m_last_acceleration(0), m_max_acceleration(0),m_last_travel_acceleration(0), m_max_travel_acceleration(0),
|
||||
m_last_jerk(0), m_max_jerk_x(0), m_max_jerk_y(0),
|
||||
m_last_bed_temperature(0), m_last_bed_temperature_reached(true)
|
||||
{}
|
||||
Extruder* filament(size_t extruder_id) { assert(extruder_id < m_curr_filament_extruder.size()); return m_curr_filament_extruder[extruder_id]; }
|
||||
const Extruder* filament(size_t extruder_id) const { assert(extruder_id < m_curr_filament_extruder.size()); return m_curr_filament_extruder[extruder_id]; }
|
||||
@@ -65,7 +72,7 @@ public:
|
||||
// Orca: set acceleration and jerk in one command for Klipper
|
||||
std::string set_accel_and_jerk(unsigned int acceleration, double jerk);
|
||||
std::string set_junction_deviation(double junction_deviation);
|
||||
std::string set_pressure_advance(double pa) const;
|
||||
std::string set_pressure_advance(double pa, int tool = -1) const;
|
||||
std::string set_input_shaping(char axis, float damp, float freq, std::string type) const;
|
||||
std::string reset_e(bool force = false);
|
||||
std::string update_progress(unsigned int num, unsigned int tot, bool allow_100 = false) const;
|
||||
@@ -91,6 +98,10 @@ public:
|
||||
std::string extrude_to_xy(const Vec2d &point, double dE, const std::string &comment = std::string(), bool force_no_extrusion = false);
|
||||
//BBS: generate G2 or G3 extrude which moves by arc
|
||||
std::string extrude_arc_to_xy(const Vec2d &point, const Vec2d ¢er_offset, double dE, const bool is_ccw, const std::string &comment = std::string(), bool force_no_extrusion = false);
|
||||
// Linear approximation of an arc, used when the machine mapping cannot
|
||||
// express a G2/G3. Must be called before m_pos is updated: center_offset is
|
||||
// relative to the current position.
|
||||
void extrude_arc_as_polyline(std::string &out, const Vec2d &point, const Vec2d ¢er_offset, double dE, const bool is_ccw, const std::string &comment = std::string(), bool force_no_extrusion = false);
|
||||
std::string extrude_to_xyz(const Vec3d &point, double dE, const std::string &comment = std::string(), bool force_no_extrusion = false);
|
||||
// Each appends its line to `out`.
|
||||
void set_speed(std::string &out, double F, const std::string &comment = std::string(), const std::string &cooling_marker = std::string());
|
||||
@@ -147,16 +158,94 @@ public:
|
||||
void invalidate_acceleration() { m_last_acceleration = 0; m_last_travel_acceleration = 0; }
|
||||
void invalidate_jerk() { m_last_jerk = 0; }
|
||||
|
||||
// Axis remap: permute/negate/reverse axes in G-code output.
|
||||
// Works standalone (without belt mode) for printers with non-standard axis conventions.
|
||||
void set_axis_remap(int rx, int ry, int rz);
|
||||
void set_build_volume_max(const Vec3d &max);
|
||||
bool has_axis_remap() const;
|
||||
|
||||
// Install the machine frame mapping. Any axis remap / build volume already
|
||||
// configured is carried over, so install order does not matter.
|
||||
void set_kinematics(std::unique_ptr<MachineKinematics> kinematics);
|
||||
const MachineKinematics& kinematics() const { return *m_kinematics; }
|
||||
|
||||
// Per-point first-layer test. When set, travel speed selection asks it per
|
||||
// destination point (in the writer's logical placed frame) instead of using
|
||||
// the layer-coarse m_is_first_layer flag. GCode installs it on belt printers
|
||||
// with the same test its extrusions use (GCode::on_first_layer(point)), so a
|
||||
// travel is judged against the belt surface exactly as the path it leads to.
|
||||
using FirstLayerPointTest = std::function<bool(const Vec3d &point_logical)>;
|
||||
void set_first_layer_point_test(FirstLayerPointTest test) { m_first_layer_point_test = std::move(test); }
|
||||
|
||||
// Force every lift to a plain vertical lift. Spiral and slope lifts compute
|
||||
// their slope in the logical frame and do not account for a machine mapping
|
||||
// that couples axes.
|
||||
void set_force_normal_lift(bool force) { m_force_normal_lift = force; }
|
||||
|
||||
// Returns whether this flavor supports separate print and travel acceleration.
|
||||
static bool supports_separate_travel_acceleration(GCodeFlavor flavor);
|
||||
private:
|
||||
protected:
|
||||
// Position/lift/offset state.
|
||||
Vec3d m_pos = Vec3d::Zero();
|
||||
double m_x_offset{ 0 };
|
||||
double m_y_offset{ 0 };
|
||||
double m_lifted;
|
||||
double m_to_lift;
|
||||
LiftType m_to_lift_type;
|
||||
bool m_is_first_layer = true;
|
||||
bool m_is_current_pos_clear = false;
|
||||
double m_current_speed;
|
||||
|
||||
std::string _travel_to_z(double z, const std::string &comment);
|
||||
|
||||
// Whether a destination gets first-layer treatment. With a point test
|
||||
// installed it decides; otherwise the layer-coarse m_is_first_layer flag does.
|
||||
bool point_on_first_layer(const Vec3d &point_logical) const;
|
||||
|
||||
// True when a lift must be skipped because this mapping would emit the
|
||||
// stored logical X/Y and that position is not yet known.
|
||||
bool must_skip_lift_now() const;
|
||||
|
||||
// True when travel speed is selected per destination point rather than per
|
||||
// layer. Set for writers that install a first-layer point test. The
|
||||
// historical path emits the raw configured travel speed in the final branch
|
||||
// of travel_to_xyz(), ignoring the first-layer selection computed at the top
|
||||
// of that function; a point-test-driven writer uses the first-layer-aware
|
||||
// value throughout. Both are preserved exactly -- unifying them would change
|
||||
// emitted feedrates and belongs in its own commit.
|
||||
bool uses_pointwise_travel_speed() const { return bool(m_first_layer_point_test); }
|
||||
|
||||
FirstLayerPointTest m_first_layer_point_test;
|
||||
bool m_force_normal_lift = false;
|
||||
|
||||
// The machine frame mapping. Owns the axis-remap state that used to live
|
||||
// here as m_remap_* / m_build_vol_max; the setters above forward to it.
|
||||
// Never null: a CartesianKinematics at the identity remap reproduces the
|
||||
// historical behaviour exactly.
|
||||
std::unique_ptr<MachineKinematics> m_kinematics;
|
||||
|
||||
// Last configured remap / build volume, replayed onto a newly installed
|
||||
// kinematics so set_kinematics() and the setters are order-independent.
|
||||
int m_remap_x = 0; // RemapAxis: 0=+X, 1=+Y, 2=+Z, 3=-X, etc.
|
||||
int m_remap_y = 1;
|
||||
int m_remap_z = 2;
|
||||
Vec3d m_build_vol_max = Vec3d::Zero();
|
||||
|
||||
// Apply the machine frame mapping to a point. Returns pos unchanged when the
|
||||
// mapping is the identity.
|
||||
Vec3d apply_axis_remap(const Vec3d &pos) const;
|
||||
|
||||
// Motion uses the global/base process variant until a filament becomes active.
|
||||
// Indexes the per-extruder speed options (travel_speed, travel_speed_z,
|
||||
// initial_layer_travel_speed).
|
||||
size_t m_cached_extruder_idx;
|
||||
|
||||
private:
|
||||
// Extruders are sorted by their ID, so that binary search is possible.
|
||||
std::vector<Extruder> m_filament_extruders;
|
||||
bool m_single_extruder_multi_material;
|
||||
std::vector<Extruder*> m_curr_filament_extruder;
|
||||
int m_curr_extruder_id;
|
||||
// Motion uses the global/base process variant until a filament becomes active.
|
||||
size_t m_cached_extruder_idx;
|
||||
unsigned int m_last_acceleration;
|
||||
unsigned int m_last_travel_acceleration;
|
||||
std::vector<unsigned int> m_max_travel_acceleration;
|
||||
@@ -178,19 +267,6 @@ public:
|
||||
//BBS
|
||||
int m_last_bed_temperature;
|
||||
bool m_last_bed_temperature_reached;
|
||||
double m_lifted;
|
||||
|
||||
// BBS
|
||||
double m_to_lift;
|
||||
LiftType m_to_lift_type;
|
||||
Vec3d m_pos = Vec3d::Zero();
|
||||
//BBS: this flag is used to indicate whether the m_pos is real.
|
||||
//A example that of the first move, the m_pos is zero, but the real position of extruder doesn't
|
||||
//Pos must be clear after the first xyz travel move
|
||||
bool m_is_current_pos_clear = false;
|
||||
//BBS: x, y offset for gcode generated
|
||||
double m_x_offset{ 0 };
|
||||
double m_y_offset{ 0 };
|
||||
|
||||
// Orca: slicing resolution in mm
|
||||
double m_resolution = 0.01;
|
||||
@@ -202,21 +278,18 @@ public:
|
||||
// non-rectangular beds such as delta/circular printers.
|
||||
Polygon m_bed_printable_area;
|
||||
std::vector<Polygon> m_extruder_printable_areas;
|
||||
|
||||
|
||||
std::string m_gcode_label_objects_start;
|
||||
std::string m_gcode_label_objects_end;
|
||||
|
||||
//SoftFever
|
||||
bool m_is_bbl_printers = false;
|
||||
double m_current_speed;
|
||||
bool m_is_first_layer = true;
|
||||
|
||||
enum class Acceleration {
|
||||
Travel,
|
||||
Print
|
||||
};
|
||||
|
||||
std::string _travel_to_z(double z, const std::string &comment);
|
||||
std::string _spiral_travel_to_z(double z, const Vec2d &ij_offset, const std::string &comment);
|
||||
// Orca: printable area of the active extruder (per-extruder when configured, otherwise the bed). Null when unknown.
|
||||
const Polygon *active_printable_area() const;
|
||||
|
||||
+31
-41
@@ -428,55 +428,44 @@ Transform3d Transformation::get_offset_matrix() const
|
||||
return translation_transform(get_offset());
|
||||
}
|
||||
|
||||
static Transform3d extract_rotation_matrix(const Transform3d& trafo)
|
||||
{
|
||||
Matrix3d rotation;
|
||||
Matrix3d scale;
|
||||
trafo.computeRotationScaling(&rotation, &scale);
|
||||
return Transform3d(rotation);
|
||||
}
|
||||
|
||||
static Transform3d extract_scale(const Transform3d& trafo)
|
||||
{
|
||||
Matrix3d rotation;
|
||||
Matrix3d scale;
|
||||
trafo.computeRotationScaling(&rotation, &scale);
|
||||
return Transform3d(scale);
|
||||
}
|
||||
|
||||
// computeRotationScaling() may put a mirror on any axis; use the local axis needing the least rotation.
|
||||
static std::pair<Transform3d, Transform3d> extract_rotation_scale(const Transform3d& trafo)
|
||||
{
|
||||
Matrix3d rotation;
|
||||
Matrix3d scale;
|
||||
trafo.computeRotationScaling(&rotation, &scale);
|
||||
if (trafo.linear().determinant() >= 0.0) {
|
||||
trafo.computeRotationScaling(&rotation, &scale);
|
||||
return { Transform3d(rotation), Transform3d(scale) };
|
||||
}
|
||||
for (int axis = 0; axis < 3; ++axis) {
|
||||
Transform3d flipped = trafo;
|
||||
flipped.linear().col(axis) *= -1.0;
|
||||
Matrix3d r;
|
||||
Matrix3d s;
|
||||
flipped.computeRotationScaling(&r, &s);
|
||||
// EPSILON keeps the lowest axis on ties, e.g. a mirror rotated by 90 degrees.
|
||||
if (axis == 0 || r.trace() > rotation.trace() + EPSILON) {
|
||||
rotation = r;
|
||||
scale = s;
|
||||
scale.col(axis) *= -1.0;
|
||||
}
|
||||
}
|
||||
return { Transform3d(rotation), Transform3d(scale) };
|
||||
}
|
||||
|
||||
static Transform3d extract_rotation_matrix(const Transform3d& trafo)
|
||||
{
|
||||
return extract_rotation_scale(trafo).first;
|
||||
}
|
||||
|
||||
static Transform3d extract_scale(const Transform3d& trafo)
|
||||
{
|
||||
return extract_rotation_scale(trafo).second;
|
||||
}
|
||||
|
||||
static bool contains_skew(const Transform3d& trafo)
|
||||
{
|
||||
Matrix3d rotation;
|
||||
Matrix3d scale;
|
||||
trafo.computeRotationScaling(&rotation, &scale);
|
||||
|
||||
if (scale.isDiagonal())
|
||||
return false;
|
||||
|
||||
if (scale.determinant() >= 0.0)
|
||||
return true;
|
||||
|
||||
// the matrix contains mirror
|
||||
const Matrix3d ratio = scale.cwiseQuotient(trafo.matrix().block<3,3>(0,0));
|
||||
|
||||
auto check_skew = [&ratio](int i, int j, bool& skew) {
|
||||
if (!std::isnan(ratio(i, j)) && !std::isnan(ratio(j, i)))
|
||||
skew |= std::abs(ratio(i, j) * ratio(j, i) - 1.0) > EPSILON;
|
||||
};
|
||||
|
||||
bool has_skew = false;
|
||||
check_skew(0, 1, has_skew);
|
||||
check_skew(0, 2, has_skew);
|
||||
check_skew(1, 2, has_skew);
|
||||
return has_skew;
|
||||
return !extract_scale(trafo).linear().isDiagonal();
|
||||
}
|
||||
|
||||
Vec3d Transformation::get_rotation() const
|
||||
@@ -550,7 +539,8 @@ void Transformation::set_scaling_factor(Axis axis, double scaling_factor)
|
||||
assert(scaling_factor > 0.0);
|
||||
|
||||
auto [rotation, scale] = extract_rotation_scale(m_matrix);
|
||||
scale(axis, axis) = scaling_factor;
|
||||
// Keep a mirror that sits on this axis.
|
||||
scale(axis, axis) = std::copysign(scaling_factor, scale(axis, axis));
|
||||
|
||||
const Vec3d offset = get_offset();
|
||||
m_matrix = rotation * scale;
|
||||
|
||||
@@ -94,8 +94,6 @@ struct Circle {
|
||||
|
||||
using Circlef = Circle<Vec2f>;
|
||||
using Circled = Circle<Vec2d>;
|
||||
using CircleSqf = CircleSq<Vec2f>;
|
||||
using CircleSqd = CircleSq<Vec2d>;
|
||||
|
||||
/// Find the center of the circle corresponding to the vector of Points as an arc.
|
||||
Point circle_center_taubin_newton(const Points::const_iterator& input_start, const Points::const_iterator& input_end, size_t cycles = 20);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include "MedialAxis.hpp"
|
||||
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <boost/polygon/polygon.hpp>
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
#include "libslic3r/IMEXArrange.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <libnest2d/common.hpp>
|
||||
|
||||
#include "libslic3r/Arrange.hpp"
|
||||
#include "libslic3r/BoundingBox.hpp"
|
||||
#include "libslic3r/ClipperUtils.hpp"
|
||||
#include "libslic3r/Config.hpp"
|
||||
#include "libslic3r/IMEXHelpers.hpp"
|
||||
#include "libslic3r/IMEXZones.hpp"
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include "libslic3r/Polygon.hpp"
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
#include "libslic3r/libslic3r.h"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
namespace {
|
||||
|
||||
// The arranger's names for the keep-outs. finish() leaves the fences out when it arranges a
|
||||
// plate inside its zone, where the zone itself is the bed shape.
|
||||
constexpr const char* kFenceName = "IMEXOutsidePrimaryZone";
|
||||
constexpr const char* kStripName = "IMEXCollisionZone";
|
||||
|
||||
// Each plate computes its zones from where it sits, so the same zone differs in the last bits.
|
||||
bool same_zone(const BoundingBoxf& a, const BoundingBoxf& b)
|
||||
{
|
||||
return is_approx(a.min, b.min) && is_approx(a.max, b.max);
|
||||
}
|
||||
|
||||
BoundingBox inset(BoundingBox box, const Point& by)
|
||||
{
|
||||
box.min += by;
|
||||
box.max -= by;
|
||||
return box;
|
||||
}
|
||||
|
||||
// The parts of `bed` outside `zone`: full-depth boxes left and right of it, and the pieces in
|
||||
// front of and behind it. Empty pieces are left out.
|
||||
std::vector<BoundingBox> boxes_around(const BoundingBox& bed, const BoundingBox& zone)
|
||||
{
|
||||
std::vector<BoundingBox> out;
|
||||
auto add = [&out](coord_t x0, coord_t y0, coord_t x1, coord_t y1) {
|
||||
if (x1 > x0 && y1 > y0)
|
||||
out.emplace_back(Point(x0, y0), Point(x1, y1));
|
||||
};
|
||||
const coord_t x0 = std::max(zone.min.x(), bed.min.x());
|
||||
const coord_t x1 = std::min(zone.max.x(), bed.max.x());
|
||||
add(bed.min.x(), bed.min.y(), zone.min.x(), bed.max.y());
|
||||
add(zone.max.x(), bed.min.y(), bed.max.x(), bed.max.y());
|
||||
add(x0, bed.min.y(), x1, zone.min.y());
|
||||
add(x0, zone.max.y(), x1, bed.max.y());
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::optional<ImexArrangeZones> imex_arrange_zones(const ImexZoneLayout& layout)
|
||||
{
|
||||
if (!layout.primary_zone_box)
|
||||
return std::nullopt;
|
||||
ImexArrangeZones zones{*layout.primary_zone_box, {}};
|
||||
for (const BoundingBoxf3& strip : layout.collision_zones)
|
||||
zones.collision_zones.emplace_back(Vec2d(strip.min.head<2>()), Vec2d(strip.max.head<2>()));
|
||||
return zones;
|
||||
}
|
||||
|
||||
void ImexArrangeInput::read_config(const DynamicPrintConfig& full_config)
|
||||
{
|
||||
// A plate the arrange adds has no mode of its own, so it takes the process preset's.
|
||||
added_plate.reset();
|
||||
if (adds_plates) {
|
||||
const auto* mode = full_config.option<ConfigOptionString>("imex_parallel_mode");
|
||||
added_plate = imex_arrange_zones(compute_imex_zone_layout(full_config, kImexPrimaryMode, mode ? mode->value : std::string(),
|
||||
get_extents(full_config.opt<ConfigOptionPoints>("printable_area")->values)));
|
||||
}
|
||||
|
||||
// An estimate: auto brim picks its own width, which brim_width stands in for.
|
||||
const auto* brim_type = full_config.option<ConfigOptionEnum<BrimType>>("brim_type");
|
||||
brim = !brim_type || brim_type->value == btNoBrim || brim_type->value == btInnerOnly ?
|
||||
0. :
|
||||
full_config.opt_float("brim_width") + full_config.opt_float("brim_object_gap");
|
||||
}
|
||||
|
||||
ImexArranger::ImexArranger(const ImexArrangeInput& input, const arrangement::ArrangeParams& params, const Points& bed)
|
||||
: m_bed(bed)
|
||||
, m_bed_bb(bed)
|
||||
, m_margin(scaled(std::max(0.f, params.bed_shrink_x)), scaled(std::max(0.f, params.bed_shrink_y)))
|
||||
, m_adds_plates(input.adds_plates)
|
||||
{
|
||||
for (size_t i = 0; i < input.beds.size(); ++i)
|
||||
if (input.beds[i])
|
||||
m_beds.push_back({int(i), *input.beds[i]});
|
||||
|
||||
// The bed shape can be a zone only when every plate being arranged has it, and so does every
|
||||
// plate the arrange may add.
|
||||
const auto& first = input.beds.empty() ? std::nullopt : input.beds.front();
|
||||
if (first &&
|
||||
std::all_of(input.beds.begin(), input.beds.end(),
|
||||
[&](const std::optional<ImexArrangeZones>& b) { return b && same_zone(b->primary_zone, first->primary_zone); }) &&
|
||||
(!input.adds_plates || (input.added_plate && same_zone(input.added_plate->primary_zone, first->primary_zone))))
|
||||
m_shared_room = room_in(first->primary_zone);
|
||||
|
||||
// The beds past the plates being arranged become new plates.
|
||||
if (input.adds_plates && input.added_plate)
|
||||
for (int bed_idx = int(input.beds.size()); bed_idx < MAX_NUM_PLATES; ++bed_idx)
|
||||
m_beds.push_back({bed_idx, *input.added_plate});
|
||||
|
||||
// A zone narrower than its margins leaves no room, and its fences then fill the whole bed.
|
||||
const Point strip_margin = m_margin + Point::Constant(scaled(input.brim));
|
||||
for (const Bed& b : m_beds) {
|
||||
if (!m_shared_room)
|
||||
for (const BoundingBox& box : boxes_around(m_bed_bb, inset(scaled(b.zones.primary_zone), m_margin)))
|
||||
m_keep_outs.push_back({b.bed_idx, box, true});
|
||||
for (const BoundingBoxf& strip : b.zones.collision_zones)
|
||||
m_keep_outs.push_back({b.bed_idx, inset(scaled(strip), -strip_margin), false});
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<BoundingBox> ImexArranger::room_in(const BoundingBoxf& zone) const
|
||||
{
|
||||
BoundingBox room = inset(scaled(zone), m_margin);
|
||||
room.min = room.min.cwiseMax(m_bed_bb.min);
|
||||
room.max = room.max.cwiseMin(m_bed_bb.max);
|
||||
if (room.min.x() >= room.max.x() || room.min.y() >= room.max.y())
|
||||
return std::nullopt;
|
||||
return room;
|
||||
}
|
||||
|
||||
Points ImexArranger::bed_shape() const { return m_shared_room ? m_shared_room->polygon().points : m_bed; }
|
||||
|
||||
void ImexArranger::add_keep_outs(arrangement::ArrangePolygons& fixed) const
|
||||
{
|
||||
for (const KeepOut& keep_out : m_keep_outs) {
|
||||
arrangement::ArrangePolygon ap;
|
||||
ap.poly.contour = keep_out.box.polygon();
|
||||
ap.is_virt_object = true;
|
||||
ap.bed_idx = keep_out.bed_idx;
|
||||
ap.height = 1;
|
||||
ap.name = keep_out.fence ? kFenceName : kStripName;
|
||||
fixed.push_back(std::move(ap));
|
||||
}
|
||||
}
|
||||
|
||||
bool ImexArranger::crosses_keep_out(const arrangement::ArrangePolygon& part, int bed_idx) const
|
||||
{
|
||||
const Polygon hull = part.transformed_poly().contour;
|
||||
return std::any_of(m_keep_outs.begin(), m_keep_outs.end(), [&](const KeepOut& keep_out) {
|
||||
return keep_out.bed_idx == bed_idx && !intersection(hull, keep_out.box.polygon()).empty();
|
||||
});
|
||||
}
|
||||
|
||||
void ImexArranger::finish(arrangement::ArrangePolygons& items,
|
||||
const arrangement::ArrangePolygons& before,
|
||||
const arrangement::ArrangePolygons& fixed,
|
||||
const arrangement::ArrangeParams& params) const
|
||||
{
|
||||
for (arrangement::ArrangePolygon& part : items)
|
||||
if (crosses_keep_out(part, part.bed_idx))
|
||||
part.bed_idx = arrangement::UNARRANGED;
|
||||
|
||||
// Only fenced plates need centering, and arranging one plate never fences.
|
||||
if (m_shared_room || !m_adds_plates)
|
||||
return;
|
||||
|
||||
arrangement::ArrangeParams one_plate = params;
|
||||
one_plate.progressind = {};
|
||||
for (const Bed& b : m_beds) {
|
||||
const std::optional<BoundingBox> room = room_in(b.zones.primary_zone);
|
||||
std::vector<size_t> on_bed;
|
||||
for (size_t i = 0; i < items.size(); ++i)
|
||||
if (items[i].bed_idx == b.bed_idx)
|
||||
on_bed.push_back(i);
|
||||
if (on_bed.empty() || !room)
|
||||
continue;
|
||||
|
||||
arrangement::ArrangePolygons parts, bed_fixed;
|
||||
// Bed 0, where the nester takes a part as placeable rather than as one that does not fit.
|
||||
for (size_t i : on_bed) {
|
||||
parts.push_back(before[i]);
|
||||
parts.back().bed_idx = 0;
|
||||
}
|
||||
for (const arrangement::ArrangePolygon& ap : fixed)
|
||||
if (ap.bed_idx == b.bed_idx && ap.name != kFenceName) {
|
||||
bed_fixed.push_back(ap);
|
||||
bed_fixed.back().bed_idx = 0;
|
||||
}
|
||||
arrangement::arrange(parts, bed_fixed, room->polygon().points, one_plate);
|
||||
// A canceled pass leaves parts it never reached on bed 0 with their old ids.
|
||||
if (one_plate.stopcondition && one_plate.stopcondition())
|
||||
return;
|
||||
|
||||
// Keep the first layout when this one does not fit.
|
||||
if (!std::all_of(parts.begin(), parts.end(), [&](const arrangement::ArrangePolygon& part) {
|
||||
return part.bed_idx == 0 && !crosses_keep_out(part, b.bed_idx);
|
||||
}))
|
||||
continue;
|
||||
|
||||
// By-object printing follows the packing order, which this pass numbered 0..n-1: carry
|
||||
// it over onto the ids the first pass gave these parts.
|
||||
std::vector<int> ids;
|
||||
for (size_t i : on_bed)
|
||||
ids.push_back(items[i].itemid);
|
||||
std::sort(ids.begin(), ids.end());
|
||||
for (size_t k = 0; k < on_bed.size(); ++k) {
|
||||
items[on_bed[k]].translation = parts[k].translation;
|
||||
items[on_bed[k]].rotation = parts[k].rotation;
|
||||
items[on_bed[k]].itemid = ids[parts[k].itemid];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,101 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
#include "libslic3r/Arrange.hpp"
|
||||
#include "libslic3r/BoundingBox.hpp"
|
||||
#include "libslic3r/Point.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
class DynamicPrintConfig;
|
||||
struct ImexZoneLayout;
|
||||
|
||||
// A plate's IDEX/IQEX zones as Arrange sees them, in plate-local millimeters.
|
||||
struct ImexArrangeZones
|
||||
{
|
||||
BoundingBoxf primary_zone;
|
||||
std::vector<BoundingBoxf> collision_zones;
|
||||
};
|
||||
|
||||
// The zones of `layout`, none when it has no primary zone, i.e. outside a parallel mode.
|
||||
std::optional<ImexArrangeZones> imex_arrange_zones(const ImexZoneLayout& layout);
|
||||
|
||||
// What an arrange needs to know about the plates it packs into.
|
||||
struct ImexArrangeInput
|
||||
{
|
||||
// Per arranger bed, in order: the zones of the plate it packs into, none for a plate whose
|
||||
// parts may go anywhere on the bed.
|
||||
std::vector<std::optional<ImexArrangeZones>> beds;
|
||||
// Whether the beds past `beds` become new plates, as when arranging every plate, and the
|
||||
// zones such a plate gets.
|
||||
bool adds_plates = false;
|
||||
std::optional<ImexArrangeZones> added_plate;
|
||||
// Room a strip keeps for the brim: brim width plus its gap to the part, in mm.
|
||||
double brim = 0.;
|
||||
|
||||
// Sets `added_plate` and `brim` from the full config. Call after setting `adds_plates`.
|
||||
void read_config(const DynamicPrintConfig& full_config);
|
||||
};
|
||||
|
||||
// Keeps an arrange inside each IDEX/IQEX plate's primary zone and out of that zone's carriage
|
||||
// collision strips.
|
||||
//
|
||||
// The arranger packs every bed into one bed shape. When every bed has the same primary zone,
|
||||
// that shape becomes the zone, which keeps the parts centered in it. Otherwise each plate in a
|
||||
// parallel mode fences off the rest of its bed with fixed items on its own bed, leaving the
|
||||
// other plates the whole bed, and finish() arranges each fenced plate again inside its zone,
|
||||
// since a fenced pile sits against the zone edge nearest the bed's center.
|
||||
//
|
||||
// A zone edge inside the bed gets the margin the bed's own edges get, since a copy printed from
|
||||
// a part at the zone edge sits at the far edge of the bed. A strip also gets the brim, which
|
||||
// would take the nozzle into it.
|
||||
class ImexArranger
|
||||
{
|
||||
public:
|
||||
// `bed` is the arranger's bed shape, shrunk by params.bed_shrink_x/y.
|
||||
ImexArranger(const ImexArrangeInput& input, const arrangement::ArrangeParams& params, const Points& bed);
|
||||
|
||||
// Whether any bed has a zone. Nothing below has an effect otherwise.
|
||||
bool active() const { return !m_beds.empty(); }
|
||||
|
||||
// The bed shape to arrange in.
|
||||
Points bed_shape() const;
|
||||
|
||||
// Adds the fixed items that keep parts out of each bed's keep-outs.
|
||||
void add_keep_outs(arrangement::ArrangePolygons& fixed) const;
|
||||
|
||||
// Call after arrangement::arrange(items, fixed, bed_shape(), params), with `before` holding
|
||||
// `items` as they were before it. Leaves unarranged any part across a keep-out: one that does
|
||||
// not fit an empty bed is retried there without the bed's fixed items, so a part too big for
|
||||
// its zone comes back across them. Then centers each fenced plate's parts in its zone.
|
||||
void finish(arrangement::ArrangePolygons& items, const arrangement::ArrangePolygons& before,
|
||||
const arrangement::ArrangePolygons& fixed, const arrangement::ArrangeParams& params) const;
|
||||
|
||||
private:
|
||||
struct Bed
|
||||
{
|
||||
int bed_idx;
|
||||
ImexArrangeZones zones;
|
||||
};
|
||||
struct KeepOut
|
||||
{
|
||||
int bed_idx;
|
||||
BoundingBox box;
|
||||
bool fence; // outside the primary zone, rather than a strip
|
||||
};
|
||||
|
||||
std::optional<BoundingBox> room_in(const BoundingBoxf& zone) const;
|
||||
bool crosses_keep_out(const arrangement::ArrangePolygon& part, int bed_idx) const;
|
||||
|
||||
Points m_bed;
|
||||
BoundingBox m_bed_bb;
|
||||
Point m_margin;
|
||||
bool m_adds_plates = false;
|
||||
std::vector<Bed> m_beds;
|
||||
std::optional<BoundingBox> m_shared_room;
|
||||
std::vector<KeepOut> m_keep_outs;
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,712 @@
|
||||
#include "libslic3r/IMEXHelpers.hpp"
|
||||
|
||||
#include <Eigen/Core>
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <map>
|
||||
#include <numeric>
|
||||
#include <cassert>
|
||||
#include <cctype>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <unordered_set>
|
||||
|
||||
#include <boost/format.hpp>
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <vector>
|
||||
#include <utility>
|
||||
|
||||
#include "Config.hpp"
|
||||
#include "libslic3r.h"
|
||||
#include "Point.hpp"
|
||||
#include "BoundingBox.hpp"
|
||||
#include "Polygon.hpp"
|
||||
#include "libslic3r/ClipperUtils.hpp"
|
||||
#include "libslic3r/I18N.hpp"
|
||||
#include "libslic3r/PresetBundle.hpp"
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
|
||||
// Mark string for localization and translate.
|
||||
#define L(s) Slic3r::I18N::translate(s)
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
namespace {
|
||||
// Strip every whitespace byte from a token in place.
|
||||
//
|
||||
// Not `::isspace` directly: it takes an int that must be representable as unsigned char
|
||||
// (or equal EOF), and `char` is signed on all three targets, so any byte >= 0x80 is sign-
|
||||
// extended to a negative int and the call is undefined. The strings parsed below come from
|
||||
// printer profiles and 3MF metadata, neither of which is guaranteed ASCII, so the unsigned
|
||||
// char cast is the same one the rest of the codebase already applies to std::toupper.
|
||||
void strip_whitespace(std::string& token)
|
||||
{
|
||||
token.erase(std::remove_if(token.begin(), token.end(),
|
||||
[](unsigned char c) { return std::isspace(c) != 0; }),
|
||||
token.end());
|
||||
}
|
||||
} // namespace
|
||||
|
||||
ConfigOptionInts effective_physical_extruder_map(const ConfigOptionInts* explicit_pem, int nozzle_count)
|
||||
{
|
||||
// One entry per logical extruder, so a profile has authored a map only when its length
|
||||
// matches. The single-element PrintConfig default does not, and is replaced rather than
|
||||
// treated as a one-extruder machine.
|
||||
if (explicit_pem && nozzle_count > 0 && (int) explicit_pem->values.size() == nozzle_count)
|
||||
return *explicit_pem;
|
||||
|
||||
ConfigOptionInts derived;
|
||||
derived.values.resize(std::max(nozzle_count, 1));
|
||||
std::iota(derived.values.begin(), derived.values.end(), 0);
|
||||
return derived;
|
||||
}
|
||||
|
||||
ConfigOptionInts effective_physical_extruder_map(const PresetBundle& pb)
|
||||
{
|
||||
const ConfigOptionInts* explicit_pem = pb.project_config.option<ConfigOptionInts>("physical_extruder_map");
|
||||
if (!explicit_pem || explicit_pem->values.size() < 2)
|
||||
explicit_pem = pb.printers.get_edited_preset().config.option<ConfigOptionInts>("physical_extruder_map");
|
||||
const auto* nozzles = pb.printers.get_edited_preset().config.option<ConfigOptionFloats>("nozzle_diameter");
|
||||
return effective_physical_extruder_map(explicit_pem, nozzles ? (int) nozzles->values.size() : 0);
|
||||
}
|
||||
|
||||
int imex_pem_tool_for(int filament_id, const std::string& parallel_mode, const ConfigOptionInts& pem)
|
||||
{
|
||||
const bool imex_parallel = !parallel_mode.empty() && parallel_mode != kImexPrimaryMode;
|
||||
if (!imex_parallel || pem.values.empty())
|
||||
return -1;
|
||||
// Bounds-checked, not get_at() -- see the note in IMEXHelpers.hpp for why the two index
|
||||
// spaces diverge. Emitting no tool qualifier is correct for a miss; pinning PA onto the
|
||||
// primary's carriage is not.
|
||||
if (filament_id < 0 || filament_id >= (int) pem.values.size())
|
||||
return -1;
|
||||
return pem.values[filament_id];
|
||||
}
|
||||
|
||||
int imex_physical_heater_for(bool is_imex, const ConfigOptionInts& pem, int logical_id)
|
||||
{
|
||||
// Bounds-check rather than get_at(): get_at() CLAMPS to values.front(), which would
|
||||
// silently retarget an out-of-range id at whatever heater sits in slot 0.
|
||||
if (!is_imex || logical_id < 0 || logical_id >= (int) pem.values.size())
|
||||
return logical_id;
|
||||
return pem.values[logical_id];
|
||||
}
|
||||
|
||||
bool imex_suppresses_bare_toolchange(const std::string& parallel_mode, unsigned int toolchange_count)
|
||||
{
|
||||
return toolchange_count <= 1
|
||||
&& !parallel_mode.empty()
|
||||
&& parallel_mode != kImexPrimaryMode;
|
||||
}
|
||||
|
||||
std::string imex_multicolor_block_reason(const std::string& parallel_mode,
|
||||
const std::string& active_tools_str,
|
||||
int tools_per_gantry,
|
||||
const std::vector<int>& used_filaments_0b,
|
||||
const ConfigOptionInts& pem)
|
||||
{
|
||||
// Not an IMEX parallel print → nothing to validate here.
|
||||
if (parallel_mode.empty() || parallel_mode == kImexPrimaryMode) return {};
|
||||
// Single-color prints have no toolchanges to constrain.
|
||||
if (used_filaments_0b.size() <= 1) return {};
|
||||
|
||||
// MMU lane sharing detection: two or more used filaments routed to the same
|
||||
// physical head means a mid-print MMU swap on that head, which IMEX parallel
|
||||
// modes can't slave to the secondary gantry.
|
||||
if (!pem.values.empty()) {
|
||||
std::unordered_set<int> seen_physicals;
|
||||
for (int filament : used_filaments_0b) {
|
||||
if (filament < 0 || filament >= (int)pem.values.size()) continue;
|
||||
const int phys = pem.values[filament]; // bounds-checked above; get_at would clamp
|
||||
if (!seen_physicals.insert(phys).second) {
|
||||
return L("Multi-color prints in IDEX/IQEX parallel modes require each filament "
|
||||
"to have its own dedicated physical extruder. Two or more of the active "
|
||||
"filaments are routed to the same physical head via the printer's "
|
||||
"physical extruder map (an MMU/AFC manifold), which the slaved gantry "
|
||||
"cannot follow.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Determine which physical head is the primary in this mode.
|
||||
const int primary_physical = imex_primary_tool_for_mode(active_tools_str);
|
||||
if (primary_physical < 0) {
|
||||
return L("The active IDEX/IQEX mode does not define a primary tool, so multi-color "
|
||||
"printing cannot be scheduled. Open the printer settings IDEX/IQEX Modes "
|
||||
"editor and assign a Primary role to one tool.");
|
||||
}
|
||||
|
||||
// Walk the active tools once: collect the set of distinct gantries spanned by
|
||||
// the mode and check for an explicit Span marker on the primary's gantry.
|
||||
// Span declares "this tool is the multicolor partner of primary on the same
|
||||
// gantry" — without it, two tools on primary's gantry mean two independent
|
||||
// copies, not a within-gantry toolchange topology, which can't carry
|
||||
// multicolor in a parallel mode.
|
||||
const int tpg = std::max(1, tools_per_gantry);
|
||||
const int primary_gantry = primary_physical / tpg;
|
||||
bool span_on_primary_gantry = false;
|
||||
std::set<int> active_gantries;
|
||||
for (const auto& [phys, role] : parse_imex_active_tools(active_tools_str)) {
|
||||
active_gantries.insert(phys / tpg);
|
||||
if (phys / tpg == primary_gantry && role == ImexRole::Span)
|
||||
span_on_primary_gantry = true;
|
||||
}
|
||||
|
||||
// Single-gantry mode: the active tools all sit on one gantry, so no second
|
||||
// gantry is being copied/mirrored to. The "Copy"/"Mirror" label is decorative
|
||||
// — there's no actual parallel printing happening, just a regular multi-tool
|
||||
// single-gantry print. Multi-color in this configuration is a misconfiguration
|
||||
// (use Primary mode for that), and the user's mode_gcode would still emit
|
||||
// parallel-print firmware setup that doesn't apply here.
|
||||
if (active_gantries.size() < 2) {
|
||||
return L("Multi-color in this mode isn't a parallel-print scenario — all of the "
|
||||
"active tools sit on a single gantry, so there's no second gantry being "
|
||||
"copied or mirrored to. Switch the plate to Primary mode for multi-color "
|
||||
"printing on a single gantry, or define an IDEX/IQEX mode that includes "
|
||||
"tools on a second gantry.");
|
||||
}
|
||||
|
||||
// Dual-gantry mode but no Span tool on the primary's gantry — the slicer would
|
||||
// emit toolchanges between filaments but the mode doesn't declare a within-gantry
|
||||
// multicolor partner, so the swap can't carry. The user might intend independent
|
||||
// copies (each gantry tool prints its own object) which is incompatible with
|
||||
// mid-print multicolor in a parallel mode.
|
||||
if (!span_on_primary_gantry) {
|
||||
return L("Multi-color prints in IDEX/IQEX parallel modes require a Span tool on the "
|
||||
"primary's gantry — without one, the mode doesn't declare a within-gantry "
|
||||
"multicolor partner. Either reduce the print to a single filament, switch to "
|
||||
"Primary mode, or open the printer settings IDEX/IQEX Modes editor and mark a "
|
||||
"tool on the primary's gantry as Span.");
|
||||
}
|
||||
// Only the primary and its Span partners print the plate's colors; every other active head
|
||||
// replays them. A color routed anywhere else -- a copying head, one the mode leaves inactive,
|
||||
// or past the end of the map -- has no head of its own to print from.
|
||||
std::set<int> color_heads{ primary_physical };
|
||||
for (const auto& [phys, role] : parse_imex_active_tools(active_tools_str))
|
||||
if (phys / tpg == primary_gantry && role == ImexRole::Span)
|
||||
color_heads.insert(phys);
|
||||
for (int filament : used_filaments_0b) {
|
||||
const int phys = pem.values.empty() ? filament
|
||||
: (filament >= 0 && filament < (int) pem.values.size()) ? pem.values[filament] : -1;
|
||||
if (color_heads.count(phys))
|
||||
continue;
|
||||
std::string tools;
|
||||
for (int head : color_heads)
|
||||
tools += (tools.empty() ? "T" : "/T") + std::to_string(head);
|
||||
// Filaments are numbered from 1 as the sidebar shows them, heads by their T index; the map
|
||||
// decides which head a filament is on, so with MMU lanes the two numbers differ.
|
||||
if (phys < 0)
|
||||
return (boost::format(L("In this Span mode only %1% print colors, but filament %2% isn't routed to any "
|
||||
"tool. Use filaments routed to %1%, or switch this plate to Primary mode.")) %
|
||||
tools % (filament + 1)).str();
|
||||
return (boost::format(L("In this Span mode only %1% print colors, but filament %2% is routed to T%3%. Use "
|
||||
"filaments routed to %1%, or switch this plate to Primary mode.")) %
|
||||
tools % (filament + 1) % phys).str();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
int first_filament_for_physical_head(const ConfigOptionInts& pem, int physical)
|
||||
{
|
||||
const auto& v = pem.values;
|
||||
if (v.empty())
|
||||
return (physical == 0) ? 0 : -1; // degenerate: treat as identity on head 0
|
||||
for (size_t i = 0; i < v.size(); ++i) {
|
||||
if (v[i] == physical)
|
||||
return static_cast<int>(i);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool has_mmu(const ConfigOptionInts& pem)
|
||||
{
|
||||
if (pem.values.size() < 2)
|
||||
return false;
|
||||
std::unordered_set<int> seen;
|
||||
for (int pv : pem.values) {
|
||||
if (!seen.insert(pv).second)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool has_non_primary_mmu(const ConfigOptionInts& pem, int primary_physical)
|
||||
{
|
||||
if (pem.values.size() < 2)
|
||||
return false;
|
||||
std::unordered_set<int> seen;
|
||||
for (int pv : pem.values) {
|
||||
if (pv == primary_physical)
|
||||
continue;
|
||||
if (!seen.insert(pv).second)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// The def-side match is on type() rather than a dynamic_cast: ConfigOptionPercent and
|
||||
// ConfigOptionFloatOrPercent both derive from ConfigOptionFloat, so a cast would accept a
|
||||
// registration whose value cfg.option<ConfigOptionFloat>() had just refused, and silently return
|
||||
// the default while a real value sat in the config. For the int, float and bool accessors here
|
||||
// both halves therefore agree on the type. imex_cfg_enum() is the exception and goes the other
|
||||
// way -- dynamic_cast on both halves -- because every ConfigOptionEnum<T> reports coEnum and a
|
||||
// type() check cannot tell one enum type from another; see the comment on it in IMEXHelpers.hpp.
|
||||
//
|
||||
// Reaching the final return means the key is not registered at all -- a typo in the literal, which
|
||||
// the compiler cannot catch. For the clearances that would be worse than the literal it replaced:
|
||||
// zero suppresses every collision strip. assert() is compiled out under NDEBUG, so it catches this
|
||||
// in a debug build only; the log line is what remains in Release, and the return is still a
|
||||
// degraded value. This is a programming-error path, not a runtime-input one.
|
||||
void imex_cfg_report_unregistered(const char* fn, const std::string& key)
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(error) << fn << ": no registered default for " << key;
|
||||
}
|
||||
|
||||
int imex_cfg_int(const ConfigBase& cfg, const std::string& key)
|
||||
{
|
||||
if (const auto* opt = cfg.option<ConfigOptionInt>(key))
|
||||
return opt->value;
|
||||
if (const ConfigOptionDef* def = print_config_def.get(key))
|
||||
if (const ConfigOption* dv = def->default_value.get())
|
||||
if (dv->type() == ConfigOptionInt::static_type())
|
||||
return static_cast<const ConfigOptionInt*>(dv)->value;
|
||||
assert(false && "imex_cfg_int: key not registered in print_config_def");
|
||||
imex_cfg_report_unregistered("imex_cfg_int", key);
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool imex_cfg_bool(const ConfigBase& cfg, const std::string& key)
|
||||
{
|
||||
if (const auto* opt = cfg.option<ConfigOptionBool>(key))
|
||||
return opt->value;
|
||||
if (const ConfigOptionDef* def = print_config_def.get(key))
|
||||
if (const ConfigOption* dv = def->default_value.get())
|
||||
if (dv->type() == ConfigOptionBool::static_type())
|
||||
return static_cast<const ConfigOptionBool*>(dv)->value;
|
||||
assert(false && "imex_cfg_bool: key not registered in print_config_def");
|
||||
imex_cfg_report_unregistered("imex_cfg_bool", key);
|
||||
return false;
|
||||
}
|
||||
|
||||
double imex_cfg_float(const ConfigBase& cfg, const std::string& key)
|
||||
{
|
||||
if (const auto* opt = cfg.option<ConfigOptionFloat>(key))
|
||||
return opt->value;
|
||||
if (const ConfigOptionDef* def = print_config_def.get(key))
|
||||
if (const ConfigOption* dv = def->default_value.get())
|
||||
if (dv->type() == ConfigOptionFloat::static_type())
|
||||
return static_cast<const ConfigOptionFloat*>(dv)->value;
|
||||
assert(false && "imex_cfg_float: key not registered in print_config_def");
|
||||
imex_cfg_report_unregistered("imex_cfg_float", key);
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
char imex_role_letter(ImexRole role)
|
||||
{
|
||||
for (const ImexRoleDesc& d : kImexRoleTable)
|
||||
if (d.role == role)
|
||||
return d.letter;
|
||||
// Only reachable if a role was added to the enum but not to kImexRoleTable. Serializing
|
||||
// it as Copy keeps the string parseable rather than emitting a token no parser accepts.
|
||||
return 'C';
|
||||
}
|
||||
|
||||
ImexRole imex_role_from_suffix(const std::string& suffix)
|
||||
{
|
||||
if (suffix.size() == 1)
|
||||
for (const ImexRoleDesc& d : kImexRoleTable)
|
||||
if (d.letter == suffix[0])
|
||||
return d.role;
|
||||
return ImexRole::Copy;
|
||||
}
|
||||
|
||||
std::vector<std::pair<int, ImexRole>> parse_imex_active_tools(const std::string& active_tools_for_mode)
|
||||
{
|
||||
std::vector<std::pair<int, ImexRole>> out;
|
||||
if (active_tools_for_mode.empty()) return out;
|
||||
std::istringstream ss(active_tools_for_mode);
|
||||
std::string tok;
|
||||
while (std::getline(ss, tok, ',')) {
|
||||
strip_whitespace(tok);
|
||||
if (tok.empty()) continue;
|
||||
const auto colon = tok.find(':');
|
||||
const std::string idx_str = (colon == std::string::npos) ? tok : tok.substr(0, colon);
|
||||
int phys = -1;
|
||||
try { phys = std::stoi(idx_str); } catch (...) { continue; }
|
||||
if (phys < 0 || phys >= (int)MAXIMUM_EXTRUDER_NUMBER) continue;
|
||||
// A bare token (no ':') is Copy here by design -- see the header note; the
|
||||
// "bare == Primary" backwards-compat rule lives in imex_primary_tool_for_mode().
|
||||
const ImexRole role = (colon == std::string::npos)
|
||||
? ImexRole::Copy
|
||||
: imex_role_from_suffix(tok.substr(colon + 1));
|
||||
out.emplace_back(phys, role);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
ImexModeKind imex_mode_kind(const std::string& active_tools_for_mode)
|
||||
{
|
||||
const int primary = imex_primary_tool_for_mode(active_tools_for_mode);
|
||||
std::set<ImexRole> roles;
|
||||
for (const auto& [phys, role] : parse_imex_active_tools(active_tools_for_mode))
|
||||
if (phys != primary && role != ImexRole::Primary)
|
||||
roles.insert(role);
|
||||
if (roles.empty())
|
||||
return ImexModeKind::Primary;
|
||||
if (roles.count(ImexRole::Span))
|
||||
return ImexModeKind::Custom;
|
||||
return roles.count(ImexRole::Mirror) ? ImexModeKind::Mirror : ImexModeKind::Copy;
|
||||
}
|
||||
|
||||
ImexGantryGrouping group_imex_active_tools_by_gantry(const std::string& active_tools_for_mode,
|
||||
int tools_per_gantry)
|
||||
{
|
||||
ImexGantryGrouping out;
|
||||
const int tpg = std::max(1, tools_per_gantry);
|
||||
|
||||
const int primary = imex_primary_tool_for_mode(active_tools_for_mode);
|
||||
if (primary < 0)
|
||||
return out;
|
||||
out.primary_phys = primary;
|
||||
out.primary_gantry = primary / tpg;
|
||||
|
||||
const int primary_col = primary % tpg;
|
||||
|
||||
// Bucket every active tool by its gantry index. Skip the primary entry itself
|
||||
// since it carries no role-driven semantics for grouping (it's just the source).
|
||||
std::map<int, std::vector<std::pair<int, ImexRole>>> by_gantry;
|
||||
bool span_seen = false;
|
||||
for (const auto& [phys, role] : parse_imex_active_tools(active_tools_for_mode)) {
|
||||
if (phys < 0) continue;
|
||||
const int g = phys / tpg;
|
||||
by_gantry[g].emplace_back(phys, role);
|
||||
if (g == out.primary_gantry && role == ImexRole::Span)
|
||||
span_seen = true;
|
||||
}
|
||||
out.span_on_primary = span_seen;
|
||||
|
||||
for (auto& [g, tools] : by_gantry) {
|
||||
ImexGantryGroup grp;
|
||||
grp.gantry_index = g;
|
||||
grp.tools = tools;
|
||||
|
||||
// Pick representative: column-paired to primary if active, else lowest phys.
|
||||
const int paired_phys = g * tpg + primary_col;
|
||||
auto pick = std::find_if(tools.begin(), tools.end(),
|
||||
[&](const auto& pr) { return pr.first == paired_phys; });
|
||||
if (pick == tools.end())
|
||||
pick = std::min_element(tools.begin(), tools.end(),
|
||||
[](const auto& a, const auto& b) { return a.first < b.first; });
|
||||
grp.representative_phys = pick->first;
|
||||
grp.representative_role = pick->second;
|
||||
|
||||
// Aggregate iff Span is on primary's gantry, this is a non-primary gantry,
|
||||
// it has ≥2 tools, and all those tools share the same role (so the merged
|
||||
// visualization is unambiguous). Mixed-role gantries fall back to per-tool.
|
||||
const bool same_role = std::all_of(tools.begin(), tools.end(),
|
||||
[&](const auto& pr) { return pr.second == grp.representative_role; });
|
||||
grp.aggregate = span_seen
|
||||
&& g != out.primary_gantry
|
||||
&& tools.size() >= 2
|
||||
&& same_role;
|
||||
|
||||
out.groups.push_back(std::move(grp));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
int imex_primary_tool_for_mode(const std::string& active_tools_for_mode)
|
||||
{
|
||||
if (active_tools_for_mode.empty())
|
||||
return -1;
|
||||
std::istringstream ss(active_tools_for_mode);
|
||||
std::string token;
|
||||
int first_bare = -1;
|
||||
while (std::getline(ss, token, ',')) {
|
||||
strip_whitespace(token);
|
||||
if (token.empty())
|
||||
continue;
|
||||
const auto colon = token.find(':');
|
||||
const std::string idx_str = (colon == std::string::npos) ? token : token.substr(0, colon);
|
||||
int idx = -1;
|
||||
try {
|
||||
idx = std::stoi(idx_str);
|
||||
} catch (...) {
|
||||
continue;
|
||||
}
|
||||
if (idx < 0)
|
||||
continue;
|
||||
if (colon == std::string::npos) {
|
||||
// Backwards-compat: a bare index is Primary.
|
||||
if (first_bare < 0)
|
||||
first_bare = idx;
|
||||
continue;
|
||||
}
|
||||
// Same suffix reading as parse_imex_active_tools(), so the two can never disagree
|
||||
// about which letter means Primary.
|
||||
if (imex_role_from_suffix(token.substr(colon + 1)) == ImexRole::Primary)
|
||||
return idx;
|
||||
}
|
||||
return first_bare;
|
||||
}
|
||||
|
||||
std::map<int,int> parse_imex_head_filament_map(const std::string& s)
|
||||
{
|
||||
std::map<int,int> result;
|
||||
if (s.empty()) return result;
|
||||
std::istringstream ss(s);
|
||||
std::string token;
|
||||
while (std::getline(ss, token, ',')) {
|
||||
strip_whitespace(token);
|
||||
if (token.empty()) continue;
|
||||
auto colon = token.find(':');
|
||||
if (colon == std::string::npos) continue;
|
||||
try {
|
||||
int phys = std::stoi(token.substr(0, colon));
|
||||
int slot = std::stoi(token.substr(colon + 1));
|
||||
// Same absolute sanity cap parse_imex_active_tools applies to its own physical
|
||||
// index: neither can exceed the slicer-wide extruder ceiling. This only rejects
|
||||
// nonsense; the bound that matters (slot must index a filament that exists) is
|
||||
// in resolve_filament_for_head, which is the one that knows the count.
|
||||
if (phys >= 0 && phys < (int) MAXIMUM_EXTRUDER_NUMBER
|
||||
&& slot >= 1 && slot <= (int) MAXIMUM_EXTRUDER_NUMBER)
|
||||
result[phys] = slot;
|
||||
} catch (...) {}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
int imex_primary_logical_from_objects(const std::vector<int>& used_slots_1b,
|
||||
const ConfigOptionInts& pem,
|
||||
int primary_physical)
|
||||
{
|
||||
if (pem.values.empty()) return -1;
|
||||
const int pem_size = (int)pem.values.size();
|
||||
for (int slot_1b : used_slots_1b) {
|
||||
const int slot_0b = slot_1b - 1;
|
||||
if (slot_0b >= 0 && slot_0b < pem_size && pem.values[slot_0b] == primary_physical)
|
||||
return slot_0b;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
ImexRouting imex_resolve_routing(const ConfigBase& cfg,
|
||||
const std::string& parallel_mode,
|
||||
const std::vector<int>& used_slots_1b,
|
||||
const ConfigOptionInts& pem)
|
||||
{
|
||||
ImexRouting out;
|
||||
// An empty mode is a plate that never carried one; kImexPrimaryMode is the reserved
|
||||
// "no parallel printing" sentinel. Neither resolves a row, so neither is looked up.
|
||||
if (parallel_mode.empty() || parallel_mode == kImexPrimaryMode)
|
||||
return out;
|
||||
out.parallel = true;
|
||||
|
||||
// One lookup of the mode table, one parse of its roster, one search for the Primary
|
||||
// marker. Every field below is derived from these, so no caller can re-derive one of
|
||||
// them differently.
|
||||
out.active_tools = find_imex_mode(cfg, parallel_mode).active_tools;
|
||||
out.tools = parse_imex_active_tools(out.active_tools);
|
||||
out.primary_phys = imex_primary_tool_for_mode(out.active_tools);
|
||||
|
||||
if (out.primary_phys >= 0)
|
||||
out.primary_logical = imex_primary_logical_from_objects(used_slots_1b, pem, out.primary_phys);
|
||||
|
||||
// Bounds-checked exactly as imex_primary_logical_from_objects checks: get_at() would
|
||||
// clamp an out-of-range slot to values.front(), which is how a head that nothing routes
|
||||
// to can end up listed as a head something routes to.
|
||||
out.routed_heads.reserve(used_slots_1b.size());
|
||||
for (int slot_1b : used_slots_1b) {
|
||||
const int slot_0b = slot_1b - 1;
|
||||
if (slot_0b >= 0 && slot_0b < (int) pem.values.size())
|
||||
out.routed_heads.push_back(pem.values[slot_0b]);
|
||||
}
|
||||
std::sort(out.routed_heads.begin(), out.routed_heads.end());
|
||||
out.routed_heads.erase(std::unique(out.routed_heads.begin(), out.routed_heads.end()),
|
||||
out.routed_heads.end());
|
||||
|
||||
// The pem emptiness test is not folded into primary_logical: an unpopulated map means
|
||||
// the printer has declared no routing at all, which is not the same claim as "this
|
||||
// plate's filaments go somewhere else", and only the latter is an error.
|
||||
out.primary_unrouted = out.primary_phys >= 0 && !pem.values.empty() && out.primary_logical < 0;
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<int> imex_secondary_logical_slots(const std::vector<int>& active_physicals,
|
||||
int primary_physical,
|
||||
const std::map<int,int>& plate_head_filament_map,
|
||||
const ConfigOptionInts& pem)
|
||||
{
|
||||
std::vector<int> out;
|
||||
std::unordered_set<int> seen;
|
||||
for (int phys : active_physicals) {
|
||||
if (phys == primary_physical) continue;
|
||||
const int logical = resolve_filament_for_head(plate_head_filament_map, pem, phys);
|
||||
if (logical < 0) continue;
|
||||
if (seen.insert(logical).second)
|
||||
out.push_back(logical);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
int resolve_filament_for_head(const std::map<int,int>& plate_map,
|
||||
const ConfigOptionInts& pem,
|
||||
int physical)
|
||||
{
|
||||
auto it = plate_map.find(physical);
|
||||
if (it != plate_map.end()) {
|
||||
const int zero_based = it->second - 1;
|
||||
// pem is indexed by filament slot here, so an override outside it names a filament
|
||||
// this printer cannot route. Bounding here rather
|
||||
// than at the parse site is deliberate: the parser is handed a raw string with no
|
||||
// notion of how many filaments the project has, while every consumer of this
|
||||
// function's result indexes a per-filament array. The picker only ever offers
|
||||
// indices into pem (IMEXFilamentPickerPopover::build_row), so nothing the UI can
|
||||
// author is rejected -- only a hand-edited or corrupt 3MF. Such an override falls
|
||||
// through to the printer's own routing, i.e. it behaves as if it were absent,
|
||||
// rather than resolving to a wrong filament that get_at() would silently clamp.
|
||||
if (zero_based >= 0 && zero_based < (int) pem.values.size())
|
||||
return zero_based;
|
||||
}
|
||||
return first_filament_for_physical_head(pem, physical);
|
||||
}
|
||||
|
||||
ImexMirrorAxis imex_mirror_axis_for(int primary_phys, int target_phys, int tools_per_gantry)
|
||||
{
|
||||
const int tpg = std::max(1, tools_per_gantry);
|
||||
return (target_phys / tpg) == (primary_phys / tpg) ? ImexMirrorAxis::X : ImexMirrorAxis::Y;
|
||||
}
|
||||
|
||||
Transform3d imex_head_transform(int /*primary*/, int /*target*/, ImexRole role,
|
||||
const Vec2d& gantry_offset,
|
||||
const Vec2d& primary_zone_center,
|
||||
ImexMirrorAxis mirror_axis)
|
||||
{
|
||||
switch (role) {
|
||||
case ImexRole::Primary:
|
||||
case ImexRole::Span:
|
||||
// Span is the within-gantry multicolor partner of Primary: it prints the same
|
||||
// objects in the same zone via mid-print toolchanges, so it has no zone of its own
|
||||
// to be translated or reflected into. Identity is the only transform that keeps it
|
||||
// on top of the primary. calc_imex_ghosts skips Span outright (no ghost is baked
|
||||
// for it, since the primary's ghost already covers that zone), so today this case
|
||||
// is unreachable — it is here so that a future caller which does reach it gets the
|
||||
// right answer rather than the fall-through, and so -Wswitch stays quiet.
|
||||
return Transform3d::Identity();
|
||||
case ImexRole::Copy:
|
||||
return Eigen::Translation3d(gantry_offset.x(), gantry_offset.y(), 0.0)
|
||||
* Transform3d::Identity();
|
||||
case ImexRole::Mirror: {
|
||||
const double len2 = gantry_offset.squaredNorm();
|
||||
if (len2 < 1e-12)
|
||||
return Transform3d::Identity();
|
||||
// True reflection about the zone-boundary plane between the two zones: perpendicular
|
||||
// to `mirror_axis`, passing through the midpoint of the zone centers along it. The
|
||||
// ghost lands at the mirrored position within the target zone — matching where the
|
||||
// mirror tool actually prints — and drag reflects across that plane, so the mirrored
|
||||
// axis inverts while the other translates 1:1.
|
||||
// X: linear = diag(-1, 1, 1), translation = (2*center.x + off.x, off.y, 0)
|
||||
// Y: linear = diag( 1,-1, 1), translation = (off.x, 2*center.y + off.y, 0)
|
||||
// Both have det = -1: a real mirror image, not a 180° rotation (which would be
|
||||
// diag(-1,-1,1), det = +1, and would print the primary's part merely turned around).
|
||||
Transform3d out = Transform3d::Identity();
|
||||
if (mirror_axis == ImexMirrorAxis::Y) {
|
||||
out.linear() = Eigen::DiagonalMatrix<double, 3>(1.0, -1.0, 1.0);
|
||||
out.translation() = Vec3d(gantry_offset.x(),
|
||||
2.0 * primary_zone_center.y() + gantry_offset.y(),
|
||||
0.0);
|
||||
} else {
|
||||
out.linear() = Eigen::DiagonalMatrix<double, 3>(-1.0, 1.0, 1.0);
|
||||
out.translation() = Vec3d(2.0 * primary_zone_center.x() + gantry_offset.x(),
|
||||
gantry_offset.y(),
|
||||
0.0);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
}
|
||||
return Transform3d::Identity();
|
||||
}
|
||||
|
||||
Vec2d compute_imex_slice_offset(bool firmware_managed,
|
||||
const std::string& parallel_mode,
|
||||
const std::optional<BoundingBoxf>& primary_zone_box)
|
||||
{
|
||||
if (!firmware_managed) return Vec2d::Zero();
|
||||
if (parallel_mode.empty()) return Vec2d::Zero();
|
||||
if (parallel_mode == kImexPrimaryMode) return Vec2d::Zero();
|
||||
if (!primary_zone_box.has_value()) return Vec2d::Zero();
|
||||
return primary_zone_box->center();
|
||||
}
|
||||
|
||||
namespace {
|
||||
// Builds the ImexMode for row `i`, padding whatever sibling array does not reach it.
|
||||
// `names` is known non-null and `i` known in range; `tools` / `gcodes` may be either.
|
||||
ImexMode imex_mode_at(const ConfigOptionStrings* names,
|
||||
const ConfigOptionStrings* tools,
|
||||
const ConfigOptionStrings* gcodes,
|
||||
size_t i)
|
||||
{
|
||||
ImexMode m;
|
||||
m.index = (int) i;
|
||||
m.name = names->values[i];
|
||||
const bool has_tools = tools != nullptr && i < tools->values.size();
|
||||
const bool has_gcode = gcodes != nullptr && i < gcodes->values.size();
|
||||
if (has_tools) m.active_tools = tools->values[i];
|
||||
if (has_gcode) m.gcode = gcodes->values[i];
|
||||
m.ragged = !has_tools || !has_gcode;
|
||||
return m;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
ImexMode find_imex_mode(const ConfigBase& cfg, const std::string& name)
|
||||
{
|
||||
const auto* names = cfg.option<ConfigOptionStrings>("imex_mode_names");
|
||||
if (names == nullptr)
|
||||
return {};
|
||||
// The siblings are fetched once, not per row: a missing option is the same answer for
|
||||
// every row, and it is padding rather than a reason to fail the lookup.
|
||||
const auto* tools = cfg.option<ConfigOptionStrings>("imex_mode_active_tools");
|
||||
const auto* gcodes = cfg.option<ConfigOptionStrings>("imex_mode_gcodes");
|
||||
for (size_t i = 0; i < names->values.size(); ++i)
|
||||
if (names->values[i] == name)
|
||||
return imex_mode_at(names, tools, gcodes, i);
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<ImexMode> imex_mode_table(const ConfigBase& cfg)
|
||||
{
|
||||
std::vector<ImexMode> table;
|
||||
const auto* names = cfg.option<ConfigOptionStrings>("imex_mode_names");
|
||||
if (names == nullptr)
|
||||
return table;
|
||||
const auto* tools = cfg.option<ConfigOptionStrings>("imex_mode_active_tools");
|
||||
const auto* gcodes = cfg.option<ConfigOptionStrings>("imex_mode_gcodes");
|
||||
table.reserve(names->values.size());
|
||||
for (size_t i = 0; i < names->values.size(); ++i)
|
||||
table.push_back(imex_mode_at(names, tools, gcodes, i));
|
||||
return table;
|
||||
}
|
||||
|
||||
std::vector<std::string> imex_plate_mode_choices(const ConfigBase& cfg)
|
||||
{
|
||||
std::vector<std::string> choices{ kImexPrimaryMode };
|
||||
for (const ImexMode& m : imex_mode_table(cfg))
|
||||
if (!m.name.empty() && std::find(choices.begin(), choices.end(), m.name) == choices.end())
|
||||
choices.push_back(m.name);
|
||||
return choices;
|
||||
}
|
||||
|
||||
bool imex_hull_violates_zones(const std::vector<BoundingBoxf3>& zones, const Polygon& hull)
|
||||
{
|
||||
if (hull.points.empty())
|
||||
return false;
|
||||
for (const auto& box : zones) {
|
||||
if (!intersection({box.polygon(true)}, {hull}).empty())
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,609 @@
|
||||
#pragma once
|
||||
|
||||
#include <cassert>
|
||||
#include <cstddef>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <Eigen/Geometry>
|
||||
#include "Config.hpp"
|
||||
#include "libslic3r/BoundingBox.hpp"
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include "libslic3r/Polygon.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
class PresetBundle;
|
||||
|
||||
// Reserved sentinel name for the always-present, non-deletable Primary mode in
|
||||
// `imex_mode_names`. Comparison with this constant indicates "no parallel printing
|
||||
// in effect" — IMEX zone visualization, ghost rendering, secondary-tool PA / temp
|
||||
// emission, and the 3MF metadata serialization all short-circuit when the active
|
||||
// plate mode equals this. Treat any value NOT equal to this as a parallel mode.
|
||||
inline constexpr const char* kImexPrimaryMode = "primary";
|
||||
|
||||
// =============================================================================
|
||||
// The printer's IMEX mode table
|
||||
// =============================================================================
|
||||
// `imex_mode_names`, `imex_mode_active_tools` and `imex_mode_gcodes` are three
|
||||
// independent ConfigOptionStrings coupled only by POSITION: entry i of each describes
|
||||
// mode i. Nothing in the config system enforces equal length, and all three are on-disk
|
||||
// format, so a hand-edited preset, a profile written before a key existed, or a 3MF from
|
||||
// another build can arrive with a sibling array shorter than `imex_mode_names`.
|
||||
//
|
||||
// ImexMode is the in-memory view of one row. It is NOT a config type — nothing
|
||||
// serializes it, and the three keys keep their names, their types and their on-disk
|
||||
// representation exactly as before.
|
||||
//
|
||||
// Resolution rule. Applied by find_imex_mode() / imex_mode_table() and NOWHERE else;
|
||||
// every consumer in the tree goes through one of the two:
|
||||
// * `imex_mode_names` is the roster. A mode exists iff a row of that array carries its
|
||||
// name, and the FIRST such row wins. (The modes editor makes a name unique when it is
|
||||
// committed, so duplicates reach here from a hand-edited profile; first-match is what a
|
||||
// std::find over the names array already did, and what the editor's own row order
|
||||
// means.)
|
||||
// * A sibling array too short to reach that row yields an EMPTY string for that field
|
||||
// and sets `ragged`. It never yields an out-of-range read, and it never degrades to
|
||||
// "no such mode": a missing script is a mode with no script, and the row's index
|
||||
// still has to be reported to {imex_mode_index}. Callers that need tools already
|
||||
// treat an empty tools string as "no tools" (imex_primary_tool_for_mode() returns -1,
|
||||
// parse_imex_active_tools() returns {}), which is the behavior they had before.
|
||||
// * `ragged` separates "the profile omitted this row" from "the author wrote an empty
|
||||
// entry" — a distinction every open-coded call site used to lose.
|
||||
//
|
||||
// Reporting: a non-Primary mode that resolves to an empty tool roster produces G-code
|
||||
// that is wrong rather than merely unconfigured (no per-head temperature transition, and
|
||||
// a suppressed initial T<n>), so GCode::_do_export() warns and falls back to Primary,
|
||||
// exactly as it already does for a name that resolves to no row at all.
|
||||
struct ImexMode
|
||||
{
|
||||
// Position in `imex_mode_names`. -1 means no row carries the requested name — the
|
||||
// not-found case, explicit rather than implied by an out-of-range index.
|
||||
int index = -1;
|
||||
// The row's entry in `imex_mode_names`. Empty when !found().
|
||||
std::string name;
|
||||
// `imex_mode_active_tools[index]`, or "" when that array is shorter than index + 1.
|
||||
std::string active_tools;
|
||||
// `imex_mode_gcodes[index]`, or "" when that array is shorter than index + 1.
|
||||
std::string gcode;
|
||||
// At least one sibling array was too short to cover `index`. Always false when
|
||||
// !found().
|
||||
bool ragged = false;
|
||||
|
||||
bool found() const { return this->index >= 0; }
|
||||
explicit operator bool() const { return this->found(); }
|
||||
};
|
||||
|
||||
// Resolves `name` against the printer's mode table; see the rule above. `cfg` is any
|
||||
// config carrying the printer keys (a DynamicPrintConfig, a PrintConfig, ...). A config
|
||||
// with no `imex_mode_names` yields a not-found ImexMode rather than throwing, so callers
|
||||
// need no null check of their own.
|
||||
ImexMode find_imex_mode(const ConfigBase& cfg, const std::string& name);
|
||||
|
||||
// The whole roster: one ImexMode per entry of `imex_mode_names`, in config order, padded
|
||||
// by the same rule. Empty when `imex_mode_names` is absent. Use this where the caller
|
||||
// wants every mode (the modes editor, the plate's mode menu) rather than one by name.
|
||||
std::vector<ImexMode> imex_mode_table(const ConfigBase& cfg);
|
||||
|
||||
// The modes a plate can be set to, in the order its mode button cycles them: kImexPrimaryMode,
|
||||
// then each name in the table once. A repeated name is listed at its first row only, the one
|
||||
// find_imex_mode() resolves it to, and empty names and rows named kImexPrimaryMode are left out.
|
||||
std::vector<std::string> imex_plate_mode_choices(const ConfigBase& cfg);
|
||||
|
||||
// =============================================================================
|
||||
// PHYSICAL vs LOGICAL extruder indices — read this before adding a new IMEX call site
|
||||
// =============================================================================
|
||||
// IMEX has two distinct index spaces and they are NOT interchangeable on MMU/AFC
|
||||
// printers (where multiple logical filament slots share one physical extruder):
|
||||
//
|
||||
// * PHYSICAL extruder index — identifies a hardware carriage / hotend.
|
||||
// Source: `imex_mode_active_tools` ("0:P,1:C,2:M") and `printer_extruder_id`.
|
||||
// Use for: user-facing labels (shown as "T0", "T1"...), per-firmware tool
|
||||
// qualifiers (Klipper EXTRUDER=extruderN, RRF M572 D<N>, Marlin T<N>), and
|
||||
// anything that addresses hardware.
|
||||
//
|
||||
// * LOGICAL filament slot index — identifies one filament in the project.
|
||||
// Source: indexing into per-filament arrays — `filament_presets`,
|
||||
// `nozzle_temperature_initial_layer`, bed_temp arrays per plate type, etc.
|
||||
// Use for: looking up filament data by slot.
|
||||
//
|
||||
// On a single-extruder or pure-IDEX printer the two indices coincide. On any
|
||||
// printer with `physical_extruder_map` size > 1 they diverge. Confusing them
|
||||
// produces silent wrong-filament behavior — the slicer reads the wrong filament
|
||||
// preset / temperature for a given carriage with no error or log line.
|
||||
//
|
||||
// Translate physical → logical via:
|
||||
// resolve_filament_for_head(plate_head_filament_map, pem, physical_idx)
|
||||
// (or the simpler `first_filament_for_physical_head` if no per-plate override).
|
||||
//
|
||||
// The reverse translation (logical → physical) is `imex_pem_tool_for`. Do NOT reach for
|
||||
// `pem.get_at(logical_idx)`: get_at() CLAMPS an out-of-range index to values.front(), silently
|
||||
// answering "physical head pem[0]" for a logical id that has no mapping at all. The map is one
|
||||
// entry per nozzle while logical ids index filament slots, and nothing caps the slot count at
|
||||
// the nozzle count, so out-of-range is reachable. Bounds-check and treat the miss as "no
|
||||
// mapping" (-1).
|
||||
//
|
||||
// The same nozzle-vs-slot divergence bites in the other direction: anything derived from pem
|
||||
// -- `resolve_filament_for_head`, `first_filament_for_physical_head` -- answers in NOZZLE index
|
||||
// space, so bound it against the FILAMENT SLOT COUNT before using it as a filament id. Not
|
||||
// against the option you are about to read: a variant-expanded option such as
|
||||
// nozzle_temperature is as long as its columns, and is_extruder_used is padded to
|
||||
// MAXIMUM_EXTRUDER_NUMBER, so both admit slots the project does not have.
|
||||
//
|
||||
// A miss is -1, and -1 is a correct tool QUALIFIER but not a skip-the-primary sentinel: no
|
||||
// physical head equals it, so a test written as `head == primary` stops skipping anything. Do
|
||||
// not substitute the mode's declared primary there. Which head prints a filament is answered by
|
||||
// the filament and pem, never by a mode role: a primary-mode print may use any or all tools, one
|
||||
// at a time, and pem is what says which. When pem cannot answer, the head is genuinely unknown -
|
||||
// an omitted qualifier then applies to the active tool, which is the head printing that filament,
|
||||
// so the honest options are to leave it unqualified or to emit nothing, not to name a carriage.
|
||||
// Known gap, unreported: the two skip sites in GCode.cpp therefore skip nothing for a filament
|
||||
// slot past the end of pem, so a plate with more slots than nozzles double-writes the primary's
|
||||
// pressure advance and hands it another filament's transition temperature.
|
||||
//
|
||||
// -1 as a tool qualifier reaches GCodeWriter::set_pressure_advance, which omits the qualifier
|
||||
// on Klipper, Marlin and BBL but substitutes the historical `D0` on RepRapFirmware
|
||||
// (GCodeWriter.cpp, set_pressure_advance) -- deliberate, not a miss.
|
||||
//
|
||||
// Past bugs in this class:
|
||||
// - GCode PA emission used the inline `pem.get_at(filament_id)` form at two
|
||||
// sites (consolidated into `imex_pem_tool_for` in c2492ccc47).
|
||||
// - Pre-slice warnings indexed `filament_presets` directly by physical idx,
|
||||
// causing wrong-filament names on AFC/MMU layouts (fixed in fbc58d2a1d).
|
||||
// - Ghost color resolution had its own inline pem lookup with a stale default
|
||||
// (centralized in `effective_physical_extruder_map` in 6a3de6a28f).
|
||||
//
|
||||
// New call sites: if you index a per-filament array, you need a logical index.
|
||||
// If you label a hardware carriage, use the physical index. When in doubt,
|
||||
// route through one of the helpers below.
|
||||
// =============================================================================
|
||||
|
||||
// physical_extruder_map has one entry per LOGICAL extruder -- the index space of
|
||||
// nozzle_diameter -- with each value a physical extruder index. It is NOT derivable from
|
||||
// printer_extruder_id, which is indexed by variant slot (one entry per extruder+variant pair):
|
||||
// an X1 Carbon has one nozzle and printer_extruder_id {1,1}, an H2D two nozzles and {1,1,2,2,2}.
|
||||
//
|
||||
// A profile has authored a map only when its length matches nozzle_count; otherwise the identity
|
||||
// is returned, which is the same default upstream applies (see Plater.cpp's extruder_map).
|
||||
ConfigOptionInts effective_physical_extruder_map(const ConfigOptionInts* explicit_pem, int nozzle_count);
|
||||
|
||||
// GUI overload: resolves the effective pem from a live PresetBundle using the project_config
|
||||
// → printer preset fallback. Use this instead of open-coding the lookup at ghost-color,
|
||||
// tooltip, click-gate and cache-key call sites so they all agree with what the slicer sees.
|
||||
ConfigOptionInts effective_physical_extruder_map(const PresetBundle& pb);
|
||||
|
||||
// Returns the physical extruder index to emit as a per-tool qualifier (PA / temperature)
|
||||
// for the given filament slot, or -1 when the current IMEX state does not warrant a
|
||||
// per-tool qualification: non-IMEX / primary mode, or the pem is empty / unpopulated.
|
||||
// Used at tool-change and second-layer transition call sites where IMEX parallel modes
|
||||
// route emission through the physical extruder, while ordinary tool changes emit bare
|
||||
// firmware commands like any non-IMEX printer.
|
||||
int imex_pem_tool_for(int filament_id, const std::string& parallel_mode, const ConfigOptionInts& pem);
|
||||
|
||||
// Translate a logical filament id into the physical heater that an M104/M109 `T` must
|
||||
// name. Applies in every IMEX mode including Primary -- a heater command names hardware,
|
||||
// so it does not depend on a parallel mode being active the way imex_pem_tool_for does.
|
||||
//
|
||||
// Gated on is_imex because physical_extruder_map carries two readings in this tree: the
|
||||
// BBL paths index it by extruder id (GCode.cpp:3295, WipeTower.cpp:1353), the IMEX paths
|
||||
// by filament id. Those coincide only when the filament and nozzle counts match, so an
|
||||
// ungated mapping would impose the IMEX reading on profiles that mean the other one --
|
||||
// fdm_bbl_3dp_002_common ships a non-identity [1,0] and is spared today only because
|
||||
// single_extruder_multi_material suppresses the T qualifier entirely.
|
||||
//
|
||||
// Out-of-range ids pass through unchanged, the same rule GCodeProcessor's preheat rewrite
|
||||
// uses (GCode/GCodeProcessor.cpp:1407), so a printer whose map is the registered
|
||||
// single-entry default is unaffected.
|
||||
int imex_physical_heater_for(bool is_imex, const ConfigOptionInts& pem, int logical_id);
|
||||
|
||||
// True when GCode::set_extruder should suppress its bare T<n> at the print-start
|
||||
// initial-tool selection because the active IMEX parallel mode's setup macro
|
||||
// (imex_mode_gcode) and machine_start_gcode already activate the primary tool
|
||||
// (Klipper SET_PRINT_MODE / RRF M567 — both reference an active tool by definition),
|
||||
// so any slicer-emitted T<n> at print-start is a duplicate.
|
||||
//
|
||||
// Mid-print toolchanges in parallel mode are intentionally NOT suppressed:
|
||||
// - Single-color prints have no mid-print toolchanges anyway
|
||||
// - Multi-color prints are blocked at slice-time when the active IMEX configuration
|
||||
// can't physically support multi-color (see imex_multicolor_block_reason). The only
|
||||
// not-blocked case is IQEX with 2+ tools active on the primary gantry, where mid-
|
||||
// print T<n> is legitimately needed and the firmware handles the slaved gantry.
|
||||
//
|
||||
// Returns false for: non-IMEX printers (empty parallel_mode), Primary mode, and any
|
||||
// toolchange after the first (toolchange_count > 1). Custom change_filament_gcode that
|
||||
// contains its own T<n> is handled separately by GCode.cpp's custom_gcode_changes_tool().
|
||||
bool imex_suppresses_bare_toolchange(const std::string& parallel_mode, unsigned int toolchange_count);
|
||||
|
||||
// Returns a user-facing block reason when multi-color printing is incompatible with
|
||||
// the active IMEX parallel mode, or an empty string when the configuration is OK.
|
||||
//
|
||||
// Multi-color in a parallel mode requires the firmware to swap tools mid-print on the
|
||||
// primary gantry while the slaved gantry follows automatically. That works only when
|
||||
// every used filament has its own dedicated physical head (no MMU lane sharing), the
|
||||
// active mode definition explicitly marks a Span tool on the primary's gantry (Span
|
||||
// declares "this tool is the within-gantry multicolor partner of Primary"; it
|
||||
// disambiguates the multicolor topology from independent same-gantry copies), AND every
|
||||
// used filament routes to the primary or one of its Span tools, the only heads that print
|
||||
// colors of their own.
|
||||
//
|
||||
// Catches:
|
||||
// - IDEX (1 tool per gantry): primary's gantry can never carry a Span partner → blocked
|
||||
// - IQEX 2-tool-active (e.g. T0 primary + T2 copy on different gantries): no Span on
|
||||
// primary's gantry → blocked
|
||||
// - MMU/AFC sharing among used filaments: multiple used filaments routed to same
|
||||
// physical head → blocked (the slaved gantry can't follow MMU lane swaps)
|
||||
// - IQEX 4-tool independent copies (T0:P,T1:C,T2:M,T3:M): no Span declared, T1 is an
|
||||
// independent copy → blocked
|
||||
// - IQEX paired-gantry multicolor (T0:P,T1:S,T2:M,T3:M): Span on T1 declares the
|
||||
// multicolor partner, no MMU sharing → ALLOWED for colors on T0 and T1; a color
|
||||
// routed to T2, T3 or past the end of the map → blocked
|
||||
//
|
||||
// Returns empty string for: non-IMEX (empty parallel_mode), Primary mode, single-color
|
||||
// prints, or any configuration where multi-color is physically supportable.
|
||||
//
|
||||
// Inputs:
|
||||
// parallel_mode — m_imex_parallel_mode at slice time
|
||||
// active_tools_str — the imex_mode_active_tools entry for the active mode, e.g.
|
||||
// "0:P,1:C,2:M,3:M"
|
||||
// tools_per_gantry — imex_tools_per_gantry from printer config (>= 1)
|
||||
// used_filaments_0b — 0-based logical filament indices used by the print
|
||||
// pem — physical_extruder_map (logical idx → physical head)
|
||||
std::string imex_multicolor_block_reason(const std::string& parallel_mode,
|
||||
const std::string& active_tools_str,
|
||||
int tools_per_gantry,
|
||||
const std::vector<int>& used_filaments_0b,
|
||||
const ConfigOptionInts& pem);
|
||||
|
||||
// Returns the lowest 0-based logical filament index L such that pem[L] == physical.
|
||||
// Returns -1 if no filament routes to `physical`.
|
||||
// Degenerate case: an empty pem returns 0 when `physical == 0` (identity-on-head-0
|
||||
// fallback for printers that never configured a pem) and -1 otherwise.
|
||||
// With identity pem, the caller sees logical == physical behavior because
|
||||
// position L holds value L.
|
||||
int first_filament_for_physical_head(const ConfigOptionInts& pem, int physical);
|
||||
|
||||
// Returns true if the printer has at least one physical head fed by >= 2 logical
|
||||
// filaments (i.e., an MMU/AFC is present on some head). Drives the plater's
|
||||
// click-handler branch: true -> open popover; false -> retain cycle-through.
|
||||
bool has_mmu(const ConfigOptionInts& pem);
|
||||
|
||||
// Returns true if any physical head OTHER than `primary_physical` is fed by
|
||||
// >= 2 logical filaments. Primary is the head whose filament is already
|
||||
// controlled by the left sidebar's object->filament assignment, so an MMU
|
||||
// there needs no popover row; only non-primary MMU heads do.
|
||||
// Returns false when pem is empty or size < 2.
|
||||
bool has_non_primary_mmu(const ConfigOptionInts& pem, int primary_physical);
|
||||
|
||||
// Parses one mode's entry from `imex_mode_active_tools` and returns the
|
||||
// 0-based physical T-index carrying the `:P` (Primary) role marker.
|
||||
// Accepts two forms:
|
||||
// "0:P,1:C,2:C" (explicit role suffix)
|
||||
// "0" (backwards-compat: a bare index == Primary)
|
||||
// Returns -1 on empty/malformed input or if no primary is found.
|
||||
// The caller is responsible for indexing into imex_mode_active_tools by mode.
|
||||
int imex_primary_tool_for_mode(const std::string& active_tools_for_mode);
|
||||
|
||||
// Parses the "phys:slot,phys:slot" serialization of imex_head_filament_map.
|
||||
// Keys are physical T-indices (0-based); values are 1-based filament slots.
|
||||
// Returns empty map on empty/malformed input. This string arrives straight from 3MF
|
||||
// metadata, so tokens outside [0, MAXIMUM_EXTRUDER_NUMBER) are dropped here as an absolute
|
||||
// sanity cap; the bound against the *project's* filament count lives in
|
||||
// resolve_filament_for_head, which is where that count is actually known.
|
||||
std::map<int,int> parse_imex_head_filament_map(const std::string& s);
|
||||
|
||||
// Resolves which 0-based logical filament to use for a physical head.
|
||||
// 1. If the plate map overrides `physical` with a slot that indexes an existing filament,
|
||||
// returns (slot - 1) — 1-based → 0-based.
|
||||
// 2. Else falls back to first_filament_for_physical_head(pem, physical).
|
||||
// Returns -1 if neither source yields a valid filament.
|
||||
//
|
||||
// `pem` has one entry per logical filament slot, so its size is the slot count and the
|
||||
// result is always a valid index into it (or -1). An override naming a slot at or past
|
||||
// that size cannot be honored — every consumer indexes a per-filament array, and the
|
||||
// ConfigOption get_at() several of them use clamps rather than failing, which would turn a
|
||||
// corrupt 3MF's "1:9999" into a silently wrong filament. Such an override is ignored and
|
||||
// the printer's own routing applies, exactly as if the override were absent.
|
||||
int resolve_filament_for_head(const std::map<int,int>& plate_map,
|
||||
const ConfigOptionInts& pem,
|
||||
int physical);
|
||||
|
||||
// Returns the 0-based logical filament slot the IMEX *primary* tool prints with,
|
||||
// based on what the plate's objects are actually assigned to. Walks `used_slots_1b`
|
||||
// (1-based filament indices, e.g. from PartPlate::get_extruders) and returns the
|
||||
// first one whose pem entry maps to `primary_physical`. Returns -1 if no object on
|
||||
// the plate routes to the primary's physical extruder, or if pem is empty.
|
||||
//
|
||||
// On non-MMU/non-AFC printers (one logical per physical) this is unambiguous; on
|
||||
// AFC layouts where multiple logicals route to one physical, the first match wins
|
||||
// — sufficient for warning labels and is_extruder_used marking. Multi-color
|
||||
// primaries on the AFC manifold may want all matches; that's a separate iteration.
|
||||
int imex_primary_logical_from_objects(const std::vector<int>& used_slots_1b,
|
||||
const ConfigOptionInts& pem,
|
||||
int primary_physical);
|
||||
|
||||
// Returns the 0-based logical filament slots that IMEX *secondary* carriages
|
||||
// will load during the print. Iterates `active_physicals` (the set returned by
|
||||
// imex_mode_active_tools parsing — physical extruder indices), skips entries
|
||||
// equal to `primary_physical` (the primary is owned by tool_ordering /
|
||||
// per-object filament assignment, not enumerated here), and resolves each
|
||||
// remaining physical via `resolve_filament_for_head` (per-plate override
|
||||
// first, then first_filament_for_physical_head as fallback).
|
||||
//
|
||||
// Returned slots are deduplicated and -1 entries (no routing found) are
|
||||
// dropped. Use this for is_extruder_used marking and pre-slice warnings'
|
||||
// secondary lookup; the caller still owns whatever it does with the slots
|
||||
// (mark a bool array, compare temps, etc.).
|
||||
std::vector<int> imex_secondary_logical_slots(const std::vector<int>& active_physicals,
|
||||
int primary_physical,
|
||||
const std::map<int,int>& plate_head_filament_map,
|
||||
const ConfigOptionInts& pem);
|
||||
|
||||
enum class ImexRole { Primary, Copy, Mirror, Span };
|
||||
|
||||
// The one table a new role has to be added to.
|
||||
//
|
||||
// `letter` is the suffix the role carries in the `imex_mode_active_tools` on-disk format
|
||||
// ("0:P,1:C,2:M,3:S"). That string is written into printer profiles and into 3MF projects,
|
||||
// so the letters are FORMAT: never renumber, rename or reorder them, only append. The table
|
||||
// order is also the order the modes editor cycles its tiles through and lists its color
|
||||
// legend in, which is why Primary comes first.
|
||||
//
|
||||
// Everything that used to open-code the letters or a parallel small-int encoding of the
|
||||
// enum now goes through this table or through ImexRole itself, so adding a fifth role means
|
||||
// editing the enum, this table, and the places that must genuinely make a new decision
|
||||
// about it (the transform switch, the zone/marker classification, the editor's per-role
|
||||
// color + label) — not a scattered set of int mappings that fail silently when missed.
|
||||
struct ImexRoleDesc {
|
||||
ImexRole role;
|
||||
char letter;
|
||||
};
|
||||
inline constexpr ImexRoleDesc kImexRoleTable[] = {
|
||||
{ ImexRole::Primary, 'P' },
|
||||
{ ImexRole::Copy, 'C' },
|
||||
{ ImexRole::Mirror, 'M' },
|
||||
{ ImexRole::Span, 'S' },
|
||||
};
|
||||
|
||||
// Reads an IMEX geometry key, falling back to the value registered for that option in
|
||||
// print_config_def when the key is absent from `cfg`, rather than to a literal repeated at the
|
||||
// call site. Only a partial or hand-built config reaches the fallback -- anything instantiated
|
||||
// from the ConfigDef carries every key -- but that is exactly the case a literal gets wrong.
|
||||
int imex_cfg_int(const ConfigBase& cfg, const std::string& key);
|
||||
double imex_cfg_float(const ConfigBase& cfg, const std::string& key);
|
||||
|
||||
// Enum sibling. ConfigOptionEnum<T> derives from ConfigOptionSingle<T>, not from ConfigOptionInt,
|
||||
// so it cannot go through imex_cfg_int(). Header-inline because it is a template.
|
||||
bool imex_cfg_bool(const ConfigBase& cfg, const std::string& key);
|
||||
|
||||
// Logs an unregistered-key lookup. Out-of-line to keep boost.log out of this header.
|
||||
void imex_cfg_report_unregistered(const char* fn, const std::string& key);
|
||||
|
||||
template<class T>
|
||||
T imex_cfg_enum(const ConfigBase& cfg, const std::string& key)
|
||||
{
|
||||
// Matched by type, not by the coEnum tag. Every ConfigOptionEnum<T> reports coEnum, so the
|
||||
// type() comparison that option<TYPE>() (Config.hpp:2656) and the int/float accessors rely on
|
||||
// cannot tell one enum type from another: it would accept a ConfigOptionEnum<OtherEnum> and
|
||||
// static_cast it to this T. ConfigOptionEnum<A> and ConfigOptionEnum<B> are unrelated
|
||||
// siblings (both derive from ConfigOptionSingle<T>), so a dynamic_cast rejects the mismatch.
|
||||
//
|
||||
// A coEnum value has a second representation, ConfigOptionEnumGeneric, which derives from
|
||||
// ConfigOptionInt rather than ConfigOptionSingle<T> - the enum analogue of the
|
||||
// ConfigOptionPercent : ConfigOptionFloat inheritance that rules dynamic_cast out for
|
||||
// imex_cfg_float. It is what a config not seeded from the static PrinterConfig holds, so it
|
||||
// is read here too, keyed on the value map it carries: only T's own map yields a T.
|
||||
const ConfigOptionDef* def = print_config_def.get(key);
|
||||
if (const ConfigOption* opt = cfg.option(key)) {
|
||||
if (const auto* e = dynamic_cast<const ConfigOptionEnum<T>*>(opt))
|
||||
return e->value;
|
||||
// The map the generic option was built from identifies the enum it holds: T's own map
|
||||
// means T's own enumerators, so the stored int is a T. A key whose definition names a
|
||||
// different enum carries a different map and falls through to the default below,
|
||||
// exactly as the typed cast above would reject it.
|
||||
if (const auto* g = dynamic_cast<const ConfigOptionEnumGeneric*>(opt))
|
||||
if (g->keys_map == &ConfigOptionEnum<T>::get_enum_values())
|
||||
return static_cast<T>(g->value);
|
||||
}
|
||||
if (def)
|
||||
if (const ConfigOption* dv = def->default_value.get())
|
||||
if (const auto* e = dynamic_cast<const ConfigOptionEnum<T>*>(dv))
|
||||
return e->value;
|
||||
// Same policy as imex_cfg_int/_float: an unregistered key is a typo the compiler cannot
|
||||
// catch, so say so rather than return a silent zero that reads as a legitimate enumerator.
|
||||
// The log call is out-of-line so this header, which much of libslic3r and the GUI includes,
|
||||
// does not pull in boost.log for a path that only a typo reaches.
|
||||
assert(false && "imex_cfg_enum: key not registered in print_config_def");
|
||||
imex_cfg_report_unregistered("imex_cfg_enum", key);
|
||||
return static_cast<T>(0);
|
||||
}
|
||||
|
||||
// The on-disk suffix letter for `role`. Inverse of imex_role_from_suffix().
|
||||
char imex_role_letter(ImexRole role);
|
||||
|
||||
// Reads a whole token suffix — everything after the ':' — back into a role. Anything that
|
||||
// is not exactly one of the table's letters is Copy: that covers "C" itself, an empty
|
||||
// suffix ("3:"), a multi-character suffix, and any letter a future build might write that
|
||||
// this one does not know. Copy is the historical fallback for an unrecognised suffix and
|
||||
// must stay so, or a project saved by a newer build changes meaning when reopened here.
|
||||
ImexRole imex_role_from_suffix(const std::string& suffix);
|
||||
|
||||
// Parses `imex_mode_active_tools[mode]` into a list of (physical_head, role) pairs.
|
||||
// Accepted token forms (comma-separated, whitespace-tolerant):
|
||||
// "phys" — bare index, role defaults to Copy
|
||||
// "phys:P" — Primary
|
||||
// "phys:C" — Copy
|
||||
// "phys:M" — Mirror
|
||||
// "phys:S" — Span (multicolor partner of Primary on the same gantry; only
|
||||
// meaningful on tools sharing primary's gantry, and only on a
|
||||
// multi-gantry printer. Drives the multicolor-allow path and
|
||||
// paired-gantry ghost/zone aggregation.)
|
||||
// "phys:???" — unknown role suffix, treated as Copy
|
||||
// Malformed tokens (unparseable int, negative phys) are skipped.
|
||||
// NOTE: the bare-token → Copy default differs from `imex_primary_tool_for_mode`,
|
||||
// which separately scans for a Primary; callers needing the primary index should
|
||||
// use that helper. This parser is for call sites that already have the primary
|
||||
// in hand and need the full head/role list (e.g. ghost factory/updater).
|
||||
std::vector<std::pair<int, ImexRole>> parse_imex_active_tools(const std::string& active_tools_for_mode);
|
||||
|
||||
// What a mode does, as the plate's mode icon shows it. The heads beside the primary that
|
||||
// imex_primary_tool_for_mode() finds decide: none is Primary, any Span head is Custom (the
|
||||
// multicolor modes), any Mirror head is Mirror, and all heads copying is Copy. One Mirror head is
|
||||
// enough because an IQEX mirror mode copies within the primary's gantry.
|
||||
enum class ImexModeKind { Primary, Copy, Mirror, Custom };
|
||||
inline constexpr size_t kImexModeKindCount = 4;
|
||||
static_assert(size_t(ImexModeKind::Custom) + 1 == kImexModeKindCount);
|
||||
ImexModeKind imex_mode_kind(const std::string& active_tools_for_mode);
|
||||
|
||||
// =============================================================================
|
||||
// The one derivation of "what does this plate's mode do, and does its filament reach
|
||||
// the primary" — shared by the hard block and the pre-slice warning
|
||||
// =============================================================================
|
||||
// Print::validate() refuses a plate whose filaments do not route to the mode's declared
|
||||
// primary; Plater's collect_imex_warnings() names that same primary's filament in its
|
||||
// bed-temperature and filament-type warnings. Both need the identical chain: resolve the
|
||||
// mode row -> parse its tool roster -> find the declared primary -> ask whether any used
|
||||
// filament slot routes there. Computed twice, the two drift, and the warning ends up
|
||||
// describing a plate the block describes differently. Computed here, they cannot.
|
||||
//
|
||||
// Everything in ImexRouting is a pure function of the four inputs. Deliberately absent:
|
||||
// the `is_imex` gate (Print::validate's, and only its, outer condition) and any bound on
|
||||
// the tool roster against the project's filament count (the plater's, and only its, way of
|
||||
// dropping tools it has no filament preset to name). Those are genuinely per-caller and
|
||||
// each stays with the caller that owns it.
|
||||
struct ImexRouting
|
||||
{
|
||||
// The mode is a parallel mode: non-empty and not kImexPrimaryMode. False leaves every
|
||||
// other field at its default — no mode row is even looked up.
|
||||
bool parallel = false;
|
||||
// `imex_mode_active_tools` for this mode, or "" when the mode resolves to no row / a
|
||||
// ragged table. See find_imex_mode().
|
||||
std::string active_tools;
|
||||
// The parsed roster, physical head + role, in the order the string lists them.
|
||||
std::vector<std::pair<int, ImexRole>> tools;
|
||||
// Declared primary PHYSICAL head, from imex_primary_tool_for_mode(). -1 when the mode
|
||||
// resolves to no row, an empty roster, or a roster with no Primary marker.
|
||||
int primary_phys = -1;
|
||||
// The 0-based LOGICAL filament slot the primary prints with: the first entry of
|
||||
// `used_slots_1b` whose pem entry maps to `primary_phys`. -1 when nothing on the plate
|
||||
// routes there (which is exactly what `primary_unrouted` reports as a hard error, and
|
||||
// what the plater's warning path falls back out of so it still has a filament to name).
|
||||
int primary_logical = -1;
|
||||
// Sorted, deduplicated PHYSICAL heads that `used_slots_1b` actually reach. Slots outside
|
||||
// the pem are dropped rather than clamped: ConfigOption::get_at() clamps to values front,
|
||||
// which would let an error message name the very head it just said nothing routes to.
|
||||
std::vector<int> routed_heads;
|
||||
// The mode declares a primary, the printer has a routing map, and no used filament
|
||||
// reaches that primary. The precise condition Print::validate refuses the plate on.
|
||||
bool primary_unrouted = false;
|
||||
|
||||
bool routes_to_primary() const { return this->primary_logical >= 0; }
|
||||
};
|
||||
|
||||
// `cfg` is any config carrying the printer's mode-table keys — the Print's applied config
|
||||
// in the slicer, the edited printer preset in the GUI. `used_slots_1b` is 1-based filament
|
||||
// indices (Print::extruders() + 1, or PartPlate::get_extruders()). `pem` is the effective
|
||||
// physical_extruder_map: already normalised on the Print's config by Print::apply(), and
|
||||
// obtained from effective_physical_extruder_map(PresetBundle) in the GUI.
|
||||
ImexRouting imex_resolve_routing(const ConfigBase& cfg,
|
||||
const std::string& parallel_mode,
|
||||
const std::vector<int>& used_slots_1b,
|
||||
const ConfigOptionInts& pem);
|
||||
|
||||
// Per-gantry grouping derived from the active_tools string. The single source of
|
||||
// truth for paired-gantry visualization aggregation: when the primary's gantry
|
||||
// has at least one Span tool, every non-primary gantry's representative tool
|
||||
// stands in for the whole gantry (one ghost, one zone strip). Gantries without
|
||||
// the aggregation trigger keep per-tool semantics.
|
||||
//
|
||||
// `representative_phys` is the column-paired tool to primary on that gantry —
|
||||
// i.e. `gantry_index * tools_per_gantry + (primary_phys % tools_per_gantry)` if
|
||||
// active, else the lowest active phys on that gantry. Aggregation falls back
|
||||
// when tools on the same non-primary gantry carry mixed roles (e.g. one Copy
|
||||
// + one Mirror), since the user explicitly authored two distinct topologies.
|
||||
struct ImexGantryGroup {
|
||||
int gantry_index = -1;
|
||||
int representative_phys = -1;
|
||||
ImexRole representative_role = ImexRole::Copy;
|
||||
bool aggregate = false; // collapse this gantry to one ghost/zone
|
||||
std::vector<std::pair<int, ImexRole>> tools; // every active tool on this gantry
|
||||
};
|
||||
|
||||
struct ImexGantryGrouping {
|
||||
int primary_phys = -1;
|
||||
int primary_gantry = -1;
|
||||
bool span_on_primary = false; // any Span tool on primary's gantry
|
||||
std::vector<ImexGantryGroup> groups; // every gantry with ≥1 active tool, sorted by index
|
||||
};
|
||||
|
||||
// Groups parsed active tools by gantry (`phys / tools_per_gantry`). Aggregation
|
||||
// triggers iff `span_on_primary` is true AND a non-primary gantry's tools all
|
||||
// carry the same role; mixed-role non-primary gantries fall back to per-tool.
|
||||
// Returns an empty grouping when active_tools_str is empty / has no primary.
|
||||
ImexGantryGrouping group_imex_active_tools_by_gantry(const std::string& active_tools_for_mode,
|
||||
int tools_per_gantry);
|
||||
|
||||
// World-space transform composed as `head_xf * primary_instance_world` to place a ghost
|
||||
// copy of the primary into `target`'s frame under `role`.
|
||||
// Which boundary a Mirror reflects across. Tools on the primary's own gantry sit beside
|
||||
// it along X, so they mirror across the vertical boundary between their zones. Tools on a
|
||||
// different gantry sit in front of / behind it along Y, so they mirror across the
|
||||
// horizontal boundary between the gantry row strips — the part comes off that gantry as a
|
||||
// Y-reflection of the tool directly behind it, not an X-reflection. Single-gantry printers
|
||||
// only ever use X.
|
||||
enum class ImexMirrorAxis { X, Y };
|
||||
|
||||
// The single source of truth for that choice. Gantry row is `phys / tools_per_gantry`, the
|
||||
// same derivation PartPlate and GCodeViewer use to build their zone grids. Keep every caller
|
||||
// on this helper: if the three of them ever disagree about the axis, the plate ghosts and the
|
||||
// preview markers place the same tool in different spots for the same mode.
|
||||
// `tools_per_gantry` is clamped to >= 1, so a zero/negative config divides safely. Note the
|
||||
// clamp lands on "one tool per gantry", meaning every secondary is then cross-gantry and
|
||||
// mirrors on Y — not "everything on one gantry". That is the honest reading of tpg = 1.
|
||||
ImexMirrorAxis imex_mirror_axis_for(int primary_phys, int target_phys, int tools_per_gantry);
|
||||
|
||||
// `gantry_offset` = center_for(target) - center_for(primary) (XY, in mm).
|
||||
// `primary_zone_center` = XY center of the primary head's zone in world coords. Only
|
||||
// consulted for Mirror; Copy/Primary ignore it.
|
||||
// `mirror_axis` = axis the Mirror reflection negates; get it from imex_mirror_axis_for().
|
||||
// Deliberately NOT defaulted: a default would silently hand a forgetful caller the X
|
||||
// reflection, which is wrong for every cross-gantry tool and would fail silently — the
|
||||
// ghosts would just quietly go back to mirroring on the wrong axis. Ignored by Copy/Primary.
|
||||
// Copy: pure translation by gantry_offset. Ghost tracks primary 1:1 during drag.
|
||||
// Mirror: true reflection (det = -1, so chirality flips — a real mirror image) about the
|
||||
// zone-boundary plane between primary and target, perpendicular to `mirror_axis`
|
||||
// and passing through the midpoint of the two zone centers along that axis. The
|
||||
// ghost origin lands at the mirrored position within the target zone, and primary
|
||||
// drag reflects across that plane, so the mirrored axis moves opposite the primary
|
||||
// while the other axis tracks 1:1.
|
||||
// Zero-length gantry_offset degenerates to identity.
|
||||
// Primary: identity.
|
||||
// Span: identity, same as Primary. A Span tool shares the primary's gantry AND its zone,
|
||||
// printing the same objects through mid-print toolchanges rather than a duplicate
|
||||
// placed elsewhere, so there is nothing to translate or reflect. calc_imex_ghosts
|
||||
// bakes no ghost for a Span head, so no caller reaches this today.
|
||||
Transform3d imex_head_transform(int primary, int target, ImexRole role,
|
||||
const Vec2d& gantry_offset,
|
||||
const Vec2d& primary_zone_center,
|
||||
ImexMirrorAxis mirror_axis);
|
||||
|
||||
// Slice-time XY shift for printers that delegate copy/mirror placement to firmware.
|
||||
// When `imex_firmware_managed_zones` is on AND the active mode is non-primary, returns
|
||||
// the primary zone's plate-local center so gcode emission can subtract it (yielding a
|
||||
// centered slice the firmware can fan out). Returns Vec2d::Zero() in every other case:
|
||||
// flag off, primary/empty mode, or no zone box (IMEX disabled, primary-only, etc.).
|
||||
Vec2d compute_imex_slice_offset(bool firmware_managed,
|
||||
const std::string& parallel_mode,
|
||||
const std::optional<BoundingBoxf>& primary_zone_box);
|
||||
|
||||
// True if `hull` (scaled Clipper coords) overlaps any of `zones` (unscaled mm) —
|
||||
// the one definition of "overlap" shared by the object and prime-tower placement
|
||||
// checks. Area-based: Clipper yields no result for shapes sharing only an edge, so
|
||||
// a hull flush against a zone boundary is NOT reported, it has to cross. That
|
||||
// matches the long-standing object behavior. An empty hull never violates.
|
||||
bool imex_hull_violates_zones(const std::vector<BoundingBoxf3>& zones, const Polygon& hull);
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,379 @@
|
||||
#include "libslic3r/IMEXZones.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "PrintConfig.hpp"
|
||||
#include "BoundingBox.hpp"
|
||||
#include "Config.hpp"
|
||||
#include "libslic3r/IMEXHelpers.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
ImexZoneLayout compute_imex_zone_layout(const DynamicPrintConfig& printer_cfg,
|
||||
const std::string& plate_mode,
|
||||
const std::string& process_mode,
|
||||
const BoundingBoxf& bed_extents)
|
||||
{
|
||||
ImexZoneLayout out;
|
||||
|
||||
auto* is_imex_opt = printer_cfg.option<ConfigOptionBool>("is_imex");
|
||||
if (!is_imex_opt || !is_imex_opt->value)
|
||||
return out;
|
||||
|
||||
// Per-plate mode takes priority over the process preset.
|
||||
std::string active_mode = plate_mode;
|
||||
if (active_mode == kImexPrimaryMode && !process_mode.empty())
|
||||
active_mode = process_mode;
|
||||
if (active_mode == kImexPrimaryMode || active_mode.empty())
|
||||
return out;
|
||||
|
||||
// Grid dimensions and tool layout from printer config
|
||||
|
||||
int n_cols = std::max(1, imex_cfg_int(printer_cfg, "imex_tools_per_gantry"));
|
||||
int n_rows = std::max(1, imex_cfg_int(printer_cfg, "imex_gantry_count"));
|
||||
|
||||
// Which corner is T0? flip_x: col 0 is right(max-X); flip_y: row 0 is rear(max-Y)
|
||||
const ImexToolLayout layout = imex_cfg_enum<ImexToolLayout>(printer_cfg, "imex_tool_layout");
|
||||
bool flip_x = (layout == ImexToolLayout::FrontRight || layout == ImexToolLayout::RearRight);
|
||||
bool flip_y = (layout == ImexToolLayout::RearLeft || layout == ImexToolLayout::RearRight);
|
||||
|
||||
// Convert tool index to physical (col=X-index, row=Y-index), col/row 0 = min-X/min-Y
|
||||
auto tool_to_phys = [&](int idx) -> std::pair<int,int> {
|
||||
int raw_col = idx % n_cols, raw_row = idx / n_cols;
|
||||
return { flip_x ? (n_cols - 1 - raw_col) : raw_col,
|
||||
flip_y ? (n_rows - 1 - raw_row) : raw_row };
|
||||
};
|
||||
|
||||
int pri_col = 0, pri_row = 0;
|
||||
|
||||
if (n_cols == 1 && n_rows == 1)
|
||||
return out; // nothing to dim with a single zone
|
||||
|
||||
// Look up secondary tool indices (active in mode, but NOT the primary tool).
|
||||
// An unresolved mode, and a mode whose row the tools array does not reach, both come
|
||||
// back as an empty string; the zone_roles guard below then returns an empty layout.
|
||||
const std::string active_tools_str = find_imex_mode(printer_cfg, active_mode).active_tools;
|
||||
|
||||
// Which heads own a zone cell, and in what role. Roles carry through from
|
||||
// parse_imex_active_tools() rather than being flattened into small ints, so the
|
||||
// classification below is a comparison the compiler checks and a new role cannot slip
|
||||
// through as an unhandled number.
|
||||
//
|
||||
// imex_primary_tool_for_mode handles the Primary slot (it owns the bare-legacy-token →
|
||||
// Primary rule); parse_imex_active_tools fills in the Copy/Mirror secondaries. Mode
|
||||
// strings use physical T-indices directly; filament routing is separate
|
||||
// (imex_head_filament_map).
|
||||
//
|
||||
// Only Primary / Copy / Mirror land in this map. A Span head shares the primary's zone
|
||||
// instead of owning one, so it deliberately gets no entry (and hence no zone center --
|
||||
// see ImexZoneLayout::head_zone_centers), and an extra Primary entry beyond the first is
|
||||
// ignored. Any further role must decide here whether it owns a cell.
|
||||
std::map<int, ImexRole> zone_roles;
|
||||
{
|
||||
const int primary = imex_primary_tool_for_mode(active_tools_str);
|
||||
if (primary >= 0 && primary < n_rows * n_cols)
|
||||
zone_roles[primary] = ImexRole::Primary;
|
||||
for (const auto& [phys_idx, role] : parse_imex_active_tools(active_tools_str)) {
|
||||
if (phys_idx < 0 || phys_idx >= n_rows * n_cols) continue;
|
||||
if (phys_idx == primary) continue;
|
||||
// Exhaustive on purpose, with no default: a role added to the enum has to answer
|
||||
// "does this own a zone cell?" here, and -Wswitch asks the question at compile
|
||||
// time instead of letting the head silently vanish from the layout.
|
||||
switch (role) {
|
||||
case ImexRole::Copy:
|
||||
case ImexRole::Mirror:
|
||||
zone_roles[phys_idx] = role;
|
||||
break;
|
||||
case ImexRole::Primary: // an extra Primary beyond the first is ignored
|
||||
case ImexRole::Span: // shares the primary's zone rather than owning one
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the mode name was found but has no tools (e.g. stale process-preset mode on a new
|
||||
// printer that has no modes defined yet), there is nothing to compute.
|
||||
if (zone_roles.empty())
|
||||
return out;
|
||||
|
||||
// Identify the Primary tool from the mode definition
|
||||
for (const auto& [idx, role] : zone_roles) {
|
||||
if (role == ImexRole::Primary) { auto [c, r] = tool_to_phys(idx); pri_col = c; pri_row = r;
|
||||
out.primary_head = idx; break; }
|
||||
}
|
||||
|
||||
// No Primary landed inside the grid. Either the mode declares none at all, or its `:P`
|
||||
// index is off-grid: the modes editor deliberately preserves tool assignments across
|
||||
// imex_gantry_count changes, so a mode authored on a 4-tool IQEX with Primary on T3 keeps
|
||||
// that assignment when the printer is reconfigured as a 2-tool IDEX, and the
|
||||
// `primary < n_rows * n_cols` bound above then drops it from zone_roles.
|
||||
//
|
||||
// Every zone, strip, ghost center and slice offset below is derived from pri_col/pri_row,
|
||||
// which would still be sitting at their (0,0) initializers — a position no active tool
|
||||
// occupies. That yields a primary zone and a secondary zone that both claim the whole bed:
|
||||
// imex_primary_zone() reports the plate clear while the blocking boxes report it full, and
|
||||
// under imex_firmware_managed_zones compute_imex_slice_offset() shifts the slice by the bed
|
||||
// center. Degrade to "this plate has no IMEX zones" instead — the same answer the earlier
|
||||
// early returns give, and the one every consumer already handles.
|
||||
if (out.primary_head < 0)
|
||||
return ImexZoneLayout{};
|
||||
|
||||
// Per-gantry grouping drives Span-based aggregation. When the mode declares a
|
||||
// Span partner on primary's gantry, non-primary gantries with multiple same-role
|
||||
// tools collapse to one cell each (placed at primary's column so has_col_sep
|
||||
// stays false and make_boxes expands the cell into a full-X row-strip).
|
||||
// Mixed-role / single-tool / no-Span configurations keep per-tool cells.
|
||||
const ImexGantryGrouping grouping =
|
||||
group_imex_active_tools_by_gantry(active_tools_str, n_cols);
|
||||
std::map<int, const ImexGantryGroup*> group_by_gantry;
|
||||
for (const auto& grp : grouping.groups)
|
||||
group_by_gantry[grp.gantry_index] = &grp;
|
||||
|
||||
// Separate copy and mirror secondary cells using physical coordinates
|
||||
std::set<std::pair<int,int>> copy_cells, mirror_cells;
|
||||
for (const auto& [idx, role] : zone_roles) {
|
||||
if (role == ImexRole::Primary) continue; // primary handled separately
|
||||
|
||||
const int phys_gantry = idx / n_cols;
|
||||
auto git = group_by_gantry.find(phys_gantry);
|
||||
const ImexGantryGroup* grp = (git == group_by_gantry.end()) ? nullptr : git->second;
|
||||
|
||||
if (grp && grp->aggregate) {
|
||||
// Only the representative contributes a cell; non-reps are folded into
|
||||
// the same row-strip and skipped entirely.
|
||||
if (idx != grp->representative_phys) continue;
|
||||
auto [rep_c, r] = tool_to_phys(idx);
|
||||
(void)rep_c; // intentionally discarded — aggregated cell pins to pri_col
|
||||
if (role == ImexRole::Copy) copy_cells.insert({pri_col, r});
|
||||
else if (role == ImexRole::Mirror) mirror_cells.insert({pri_col, r});
|
||||
} else {
|
||||
auto [c, r] = tool_to_phys(idx);
|
||||
if (role == ImexRole::Copy) copy_cells.insert({c, r});
|
||||
else if (role == ImexRole::Mirror) mirror_cells.insert({c, r});
|
||||
}
|
||||
}
|
||||
|
||||
// All active secondary cells combined (for separation axis computation)
|
||||
std::set<std::pair<int,int>> all_secondary;
|
||||
for (auto& p : copy_cells) all_secondary.insert(p);
|
||||
for (auto& p : mirror_cells) all_secondary.insert(p);
|
||||
|
||||
double x_min = bed_extents.min(0), x_max = bed_extents.max(0);
|
||||
double y_min = bed_extents.min(1), y_max = bed_extents.max(1);
|
||||
|
||||
// Zone sizing is based on ACTIVE tool count per axis, not total grid dimensions.
|
||||
// Inactive tools (absent from zone_roles) donate their bed share to active neighbors.
|
||||
// Sorted active col/row lists map physical index k → zone index k.
|
||||
std::vector<int> active_cols_v, active_rows_v;
|
||||
{
|
||||
std::set<int> ac_set, ar_set;
|
||||
for (const auto& [idx, role] : zone_roles) {
|
||||
auto [c, r] = tool_to_phys(idx);
|
||||
ac_set.insert(c); ar_set.insert(r);
|
||||
}
|
||||
active_cols_v.assign(ac_set.begin(), ac_set.end()); // sorted ascending
|
||||
active_rows_v.assign(ar_set.begin(), ar_set.end());
|
||||
}
|
||||
int n_active_cols = std::max(1, (int)active_cols_v.size());
|
||||
int n_active_rows = std::max(1, (int)active_rows_v.size());
|
||||
|
||||
std::map<int,int> col_to_zone, row_to_zone;
|
||||
for (int k = 0; k < n_active_cols; ++k) col_to_zone[active_cols_v[k]] = k;
|
||||
for (int k = 0; k < n_active_rows; ++k) row_to_zone[active_rows_v[k]] = k;
|
||||
|
||||
double zone_w = (x_max - x_min) / n_active_cols;
|
||||
double zone_h = (y_max - y_min) / n_active_rows;
|
||||
|
||||
// Zone index of the primary tool
|
||||
int pri_col_k = col_to_zone.count(pri_col) ? col_to_zone[pri_col] : 0;
|
||||
int pri_row_k = row_to_zone.count(pri_row) ? row_to_zone[pri_row] : 0;
|
||||
|
||||
// Zone center per physical head — ghost placement consumes this so that
|
||||
// ghosts land in their own secondary zone instead of stacking on primary.
|
||||
// Every active tool (including primary) gets an entry; ghost offset math is
|
||||
// simply center[head] - center[primary].
|
||||
for (const auto& [idx, role] : zone_roles) {
|
||||
auto [c, r] = tool_to_phys(idx);
|
||||
int ck = col_to_zone.count(c) ? col_to_zone.at(c) : 0;
|
||||
int rk = row_to_zone.count(r) ? row_to_zone.at(r) : 0;
|
||||
out.head_zone_centers[idx] = Vec2d(
|
||||
x_min + (ck + 0.5) * zone_w,
|
||||
y_min + (rk + 0.5) * zone_h);
|
||||
}
|
||||
|
||||
// Separation axes: row-sep = secondaries on a different gantry, col-sep = different column
|
||||
bool has_row_sep = false, has_col_sep = false;
|
||||
for (const auto& [sc, sr] : all_secondary) {
|
||||
if (sr != pri_row) has_row_sep = true;
|
||||
if (sc != pri_col) has_col_sep = true;
|
||||
}
|
||||
|
||||
// Build expanded zone rects for a set of cells.
|
||||
// When secondaries share only a row difference (same column as primary) → expand to full X width.
|
||||
// When secondaries share only a column difference → expand to full Y height.
|
||||
// When both axes differ → per-quadrant.
|
||||
auto make_boxes = [&](const std::set<std::pair<int,int>>& cells) -> std::vector<BoundingBoxf> {
|
||||
std::vector<BoundingBoxf> boxes;
|
||||
if (cells.empty()) return boxes;
|
||||
auto ck = [&](int c) { return col_to_zone.count(c) ? col_to_zone.at(c) : 0; };
|
||||
auto rk = [&](int r) { return row_to_zone.count(r) ? row_to_zone.at(r) : 0; };
|
||||
if (has_row_sep && !has_col_sep) {
|
||||
std::set<int> rows; for (auto& [c,r] : cells) rows.insert(r);
|
||||
for (int sr : rows) {
|
||||
int k = rk(sr);
|
||||
boxes.emplace_back(Vec2d(x_min, y_min + k*zone_h), Vec2d(x_max, y_min + (k+1)*zone_h));
|
||||
}
|
||||
} else if (has_col_sep && !has_row_sep) {
|
||||
std::set<int> cols; for (auto& [c,r] : cells) cols.insert(c);
|
||||
for (int sc : cols) {
|
||||
int k = ck(sc);
|
||||
boxes.emplace_back(Vec2d(x_min + k*zone_w, y_min), Vec2d(x_min + (k+1)*zone_w, y_max));
|
||||
}
|
||||
} else {
|
||||
for (auto& [sc,sr] : cells) {
|
||||
int ck_ = ck(sc), rk_ = rk(sr);
|
||||
boxes.emplace_back(Vec2d(x_min + ck_*zone_w, y_min + rk_*zone_h),
|
||||
Vec2d(x_min + (ck_+1)*zone_w, y_min + (rk_+1)*zone_h));
|
||||
}
|
||||
}
|
||||
return boxes;
|
||||
};
|
||||
|
||||
out.copy_zones = make_boxes(copy_cells);
|
||||
out.mirror_zones = make_boxes(mirror_cells);
|
||||
|
||||
// --- Secondary zone blocking ---
|
||||
// The expanded bounding boxes for all secondary (copy+mirror) zones. check_outside()
|
||||
// uses these to prevent objects being placed outside the primary zone.
|
||||
auto push_secondary_box = [&](const std::vector<BoundingBoxf>& boxes) {
|
||||
for (const auto& b : boxes)
|
||||
out.secondary_zone_boxes.emplace_back(Vec3d(b.min.x(), b.min.y(), -1.0),
|
||||
Vec3d(b.max.x(), b.max.y(), 1e4));
|
||||
};
|
||||
push_secondary_box(out.copy_zones);
|
||||
push_secondary_box(out.mirror_zones);
|
||||
|
||||
// --- Carriage collision danger strips ---
|
||||
// Only add strips at boundaries of the PRIMARY zone — objects are only placed in the
|
||||
// primary zone, so secondary-to-secondary boundaries have no relevance.
|
||||
//
|
||||
// Strip width is the literal nozzle clearance value on the primary side only:
|
||||
// right X boundary: [bnd_x - nozzle_clearance_x, bnd_x]
|
||||
// left X boundary: [bnd_x, bnd_x + nozzle_clearance_x]
|
||||
// top Y boundary: [bnd_y - nozzle_clearance_y, bnd_y]
|
||||
// bottom Y boundary:[bnd_y, bnd_y + nozzle_clearance_y]
|
||||
//
|
||||
// Strip length matches the primary zone extent (same expansion logic as make_boxes):
|
||||
// !has_row_sep → full bed height; has_row_sep → primary row only
|
||||
// !has_col_sep → full bed width; has_col_sep → primary column only
|
||||
|
||||
// Both strip blocks below are gated on `carriage_w > 0.0` / `carriage_h > 0.0`, so a zero
|
||||
// clearance emits no collision strips at all while the preview still draws toolhead boxes.
|
||||
// That is why these read through imex_cfg_float() rather than carrying a literal: the value
|
||||
// for an absent key comes from the option's own registration, not from a number repeated here.
|
||||
double carriage_w = imex_cfg_float(printer_cfg, "imex_nozzle_clearance_x");
|
||||
double carriage_h = imex_cfg_float(printer_cfg, "imex_nozzle_clearance_y");
|
||||
double margin = imex_cfg_float(printer_cfg, "imex_carriage_margin");
|
||||
|
||||
// Primary zone extent (the clear printable area):
|
||||
// row-sep only → full bed width × primary row's Y band
|
||||
// col-sep only → primary col's X band × full bed height
|
||||
// both → primary quadrant
|
||||
// Uses zone indices so inactive tools don't shrink the zone.
|
||||
double pz_x0 = has_col_sep ? x_min + pri_col_k * zone_w : x_min;
|
||||
double pz_x1 = has_col_sep ? x_min + (pri_col_k + 1) * zone_w : x_max;
|
||||
double pz_y0 = has_row_sep ? y_min + pri_row_k * zone_h : y_min;
|
||||
double pz_y1 = has_row_sep ? y_min + (pri_row_k + 1) * zone_h : y_max;
|
||||
out.primary_zone_box = BoundingBoxf(Vec2d(pz_x0, pz_y0), Vec2d(pz_x1, pz_y1));
|
||||
|
||||
auto add_strip = [&](double sx0, double sx1, double sy0, double sy1) {
|
||||
out.collision_zones.emplace_back(Vec3d(sx0, sy0, -1.0), Vec3d(sx1, sy1, 1e4));
|
||||
};
|
||||
auto add_margin_band = [&](double sx0, double sx1, double sy0, double sy1) {
|
||||
out.margin_bands.emplace_back(Vec2d(sx0, sy0), Vec2d(sx1, sy1));
|
||||
};
|
||||
|
||||
// Determine which directions have mirror secondaries adjacent to the primary.
|
||||
// Only mirror tools can cause carriage collisions — they move toward each other.
|
||||
// Copy tools always move in the same direction, so no collision strip is needed.
|
||||
//
|
||||
// Driven by `mirror_cells`, not by zone_roles: on an AGGREGATED gantry the cell is
|
||||
// pinned to the primary's column and make_boxes then expands it into a full-width row
|
||||
// strip, so the painted zone — not any one head's own cell — is what the primary's
|
||||
// carriage can meet. Reading each head's own cell instead lost the strip whenever the
|
||||
// representative's column differed from the primary's: at tpg=3, gantry_count=2, mode
|
||||
// "0:P,1:S,4:M,5:M" the rear gantry paints a full-width mirror strip against the
|
||||
// primary's rear boundary, yet neither T4 (col 1) nor T5 (col 2) is in the primary's
|
||||
// column, so no strip and no margin band were emitted at all.
|
||||
//
|
||||
// Both axes require zone-adjacent AND same row/column on the OTHER axis: a mirror
|
||||
// diagonally offset from primary (different row AND different column) can't collide
|
||||
// with primary's carriage on either axis since the gantries don't overlap there.
|
||||
// Without these checks, paired-gantry mc-mirror (`0:P,1:S,2:M,3:M`) would draw a
|
||||
// spurious right-edge strip from T3 even though T3 lives on the other gantry's row.
|
||||
//
|
||||
// The adjacency tests (±1) are in ZONE indices so an inactive column or row between
|
||||
// two active ones cannot hide the boundary they really share (see "inactive tools
|
||||
// donate their bed share"), while the co-linearity test on the other axis stays in
|
||||
// physical indices. The two forms agree for any cell that owns a zone — col_to_zone
|
||||
// and row_to_zone are order-preserving bijections over exactly the active indices —
|
||||
// so this is one rule written two ways, not two rules.
|
||||
bool has_right_sec = false, has_left_sec = false;
|
||||
bool has_top_sec = false, has_bottom_sec = false;
|
||||
for (const auto& [sc, sr] : mirror_cells) {
|
||||
int zc = col_to_zone.count(sc) ? col_to_zone.at(sc) : -1;
|
||||
int zr = row_to_zone.count(sr) ? row_to_zone.at(sr) : -1;
|
||||
if (zc < 0 || zr < 0) continue; // no zone of its own, so no boundary with primary
|
||||
// X-boundary strips: mirror zone-adjacent in X, same physical row as primary.
|
||||
if (zr == pri_row_k && zc == pri_col_k + 1) has_right_sec = true;
|
||||
if (zr == pri_row_k && zc == pri_col_k - 1) has_left_sec = true;
|
||||
// Y-boundary strips: mirror zone-adjacent in Y, same physical column as primary.
|
||||
if (zr == pri_row_k + 1 && sc == pri_col) has_top_sec = true;
|
||||
if (zr == pri_row_k - 1 && sc == pri_col) has_bottom_sec = true;
|
||||
}
|
||||
|
||||
// X-axis boundaries (vertical strips, width = nozzle_clearance_x on primary side)
|
||||
if (carriage_w > 0.0) {
|
||||
if (has_right_sec) {
|
||||
double bnd_x = x_min + (pri_col_k + 1) * zone_w;
|
||||
double strip_inner = bnd_x - carriage_w;
|
||||
add_strip(strip_inner, bnd_x, pz_y0, pz_y1);
|
||||
if (margin > 0.0)
|
||||
add_margin_band(strip_inner - margin, strip_inner, pz_y0, pz_y1);
|
||||
}
|
||||
if (has_left_sec) {
|
||||
double bnd_x = x_min + pri_col_k * zone_w;
|
||||
double strip_inner = bnd_x + carriage_w;
|
||||
add_strip(bnd_x, strip_inner, pz_y0, pz_y1);
|
||||
if (margin > 0.0)
|
||||
add_margin_band(strip_inner, strip_inner + margin, pz_y0, pz_y1);
|
||||
}
|
||||
}
|
||||
|
||||
// Y-axis boundaries (horizontal strips, width = nozzle_clearance_y on primary side)
|
||||
if (carriage_h > 0.0) {
|
||||
if (has_top_sec) {
|
||||
double bnd_y = y_min + (pri_row_k + 1) * zone_h;
|
||||
double strip_inner = bnd_y - carriage_h;
|
||||
add_strip(pz_x0, pz_x1, strip_inner, bnd_y);
|
||||
if (margin > 0.0)
|
||||
add_margin_band(pz_x0, pz_x1, strip_inner - margin, strip_inner);
|
||||
}
|
||||
if (has_bottom_sec) {
|
||||
double bnd_y = y_min + pri_row_k * zone_h;
|
||||
double strip_inner = bnd_y + carriage_h;
|
||||
add_strip(pz_x0, pz_x1, bnd_y, strip_inner);
|
||||
if (margin > 0.0)
|
||||
add_margin_band(pz_x0, pz_x1, strip_inner, strip_inner + margin);
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,94 @@
|
||||
#pragma once
|
||||
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "libslic3r/BoundingBox.hpp"
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// Where every IMEX bed zone, carriage collision strip and advisory margin band sits, in
|
||||
// plate-local millimetres. Purely geometric: no rendering, no clipping against the bed
|
||||
// outline, no GUI types. PartPlate turns the rectangles into GLModels (clipping each one
|
||||
// to the bed polygon as it goes) and copies the boxes straight into the members that
|
||||
// check_outside() consults, so this struct is the whole answer to "which boxes".
|
||||
//
|
||||
// Every list is empty and `primary_zone_box` unset when no parallel mode is in effect —
|
||||
// IMEX off, Primary mode, a 1x1 tool grid, a mode whose tool roster is empty, or a mode
|
||||
// whose Primary tool is not on the current grid (no `:P` marker, or a `:P` index left
|
||||
// over from a wider grid). The whole layout hangs off the primary's cell, so without one
|
||||
// there is nothing to divide and consumers must see the same "no zones" answer.
|
||||
struct ImexZoneLayout
|
||||
{
|
||||
// Physical T-index of the active mode's Primary tool; -1 when the layout is empty.
|
||||
int primary_head = -1;
|
||||
|
||||
// Physical head -> XY center of that head's zone. One entry per tool the mode makes
|
||||
// active, Primary included. A Span tool has no entry: it shares the primary's zone
|
||||
// rather than owning one. Ghost placement offsets by center[head] - center[primary].
|
||||
//
|
||||
// EXCEPTION, read before using the X component. These centers are built from the head's
|
||||
// OWN grid cell, but an AGGREGATED representative's zone rectangle is pinned to the
|
||||
// primary's column and then expanded to a full-width row strip. So for an aggregated head
|
||||
// whose own column differs from the primary's, center[head].x names a column its painted
|
||||
// zone does not have, and `center[head] - center[primary]` carries a bogus X offset.
|
||||
// Reachable at imex_tools_per_gantry >= 3 when the primary-column tool on the aggregated
|
||||
// gantry is inactive (e.g. tpg=3, gantry_count=2, mode "0:P,1:S,4:M,5:M").
|
||||
// Both current consumers pin X to the primary's center for aggregated heads --
|
||||
// PartPlate::calc_imex_ghosts() via bed_x_center, GCodeViewer via sec_center_of() -- and a
|
||||
// new consumer must do the same until this is either pinned here or the aggregate flag is
|
||||
// exposed on the layout.
|
||||
std::map<int, Vec2d> head_zone_centers;
|
||||
|
||||
// The clear printable area — the primary tool's zone. Objects go here and nowhere else.
|
||||
std::optional<BoundingBoxf> primary_zone_box;
|
||||
|
||||
// Copy / Mirror zone rectangles, already expanded along whichever axis carries no
|
||||
// separation: secondaries differing from primary only by row give full-bed-width
|
||||
// strips, only by column give full-bed-height strips, and differing on both axes give
|
||||
// per-tool quadrants.
|
||||
std::vector<BoundingBoxf> copy_zones;
|
||||
std::vector<BoundingBoxf> mirror_zones;
|
||||
|
||||
// `copy_zones` followed by `mirror_zones`, as full-height boxes for the placement
|
||||
// check. An object overlapping one of these is outside the primary zone.
|
||||
std::vector<BoundingBoxf3> secondary_zone_boxes;
|
||||
|
||||
// Carriage danger strips lying just inside the primary zone's boundary, one per
|
||||
// boundary that faces an adjacent Mirror ZONE -- an aggregated gantry contributes the
|
||||
// one pinned zone it paints, not one per head -- in right / left / top / bottom order.
|
||||
// Width is the literal nozzle clearance for that axis. Copy tools never contribute:
|
||||
// they travel in the same direction as the primary and cannot close on it.
|
||||
std::vector<BoundingBoxf3> collision_zones;
|
||||
|
||||
// Advisory (non-blocking) bands immediately inside each collision strip, present only
|
||||
// when imex_carriage_margin > 0. Same boundary order as `collision_zones`.
|
||||
std::vector<BoundingBoxf> margin_bands;
|
||||
};
|
||||
|
||||
// Computes the IMEX zone layout for one plate.
|
||||
//
|
||||
// `printer_cfg` — the edited printer preset's config. Read for is_imex, the tool grid
|
||||
// (imex_gantry_count / imex_tools_per_gantry / imex_tool_layout), the
|
||||
// mode roster (imex_mode_names / imex_mode_active_tools) and the strip
|
||||
// widths (imex_nozzle_clearance_x / _y, imex_carriage_margin). A key absent
|
||||
// from the config falls back to the value registered for that option in
|
||||
// print_config_def, via the imex_cfg_* accessors.
|
||||
// `plate_mode` — the plate's own IMEX mode. Wins over the process preset unless it is
|
||||
// `kImexPrimaryMode`.
|
||||
// `process_mode` — the process preset's `imex_parallel_mode`, used only as the fallback
|
||||
// when `plate_mode` is `kImexPrimaryMode`. Pass "" when unavailable.
|
||||
// `bed_extents` — XY extents of the plate shape, in plate-local mm. Zones subdivide it.
|
||||
//
|
||||
// Zone sizing keys off the count of ACTIVE tools per axis rather than the grid dimensions,
|
||||
// so a tool the mode leaves out donates its share of the bed to its active neighbours.
|
||||
ImexZoneLayout compute_imex_zone_layout(const DynamicPrintConfig& printer_cfg,
|
||||
const std::string& plate_mode,
|
||||
const std::string& process_mode,
|
||||
const BoundingBoxf& bed_extents);
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -1,349 +0,0 @@
|
||||
#include "JumpPointSearch.hpp"
|
||||
#include "BoundingBox.hpp"
|
||||
#include "ExPolygon.hpp"
|
||||
#include "Point.hpp"
|
||||
#include "libslic3r/AStar.hpp"
|
||||
#include "libslic3r/KDTreeIndirect.hpp"
|
||||
#include "libslic3r/Polygon.hpp"
|
||||
#include "libslic3r/Polyline.hpp"
|
||||
#include "libslic3r/libslic3r.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <iterator>
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include <oneapi/tbb/scalable_allocator.h>
|
||||
|
||||
//#define DEBUG_FILES
|
||||
#ifdef DEBUG_FILES
|
||||
#include "libslic3r/SVG.hpp"
|
||||
#endif
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// execute fn for each pixel on the line. If fn returns false, terminate the iteration
|
||||
template<typename PointFn> void dda(coord_t x0, coord_t y0, coord_t x1, coord_t y1, const PointFn &fn)
|
||||
{
|
||||
coord_t dx = abs(x1 - x0);
|
||||
coord_t dy = abs(y1 - y0);
|
||||
coord_t x = x0;
|
||||
coord_t y = y0;
|
||||
coord_t n = 1 + dx + dy;
|
||||
coord_t x_inc = (x1 > x0) ? 1 : -1;
|
||||
coord_t y_inc = (y1 > y0) ? 1 : -1;
|
||||
coord_t error = dx - dy;
|
||||
dx *= 2;
|
||||
dy *= 2;
|
||||
|
||||
for (; n > 0; --n) {
|
||||
if (!fn(x, y)) return;
|
||||
|
||||
if (error > 0) {
|
||||
x += x_inc;
|
||||
error -= dy;
|
||||
} else {
|
||||
y += y_inc;
|
||||
error += dx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// will draw the line twice, second time with and offset of 1 in the direction of normal
|
||||
// may call the fn on the same coordiantes multiple times!
|
||||
template<typename PointFn> void double_dda_with_offset(coord_t x0, coord_t y0, coord_t x1, coord_t y1, const PointFn &fn)
|
||||
{
|
||||
Vec2d normal = Point{y1 - y0, x1 - x0}.cast<double>().normalized();
|
||||
normal.x() = ceil(normal.x());
|
||||
normal.y() = ceil(normal.y());
|
||||
Point start_offset = Point(x0, y0) + (normal).cast<coord_t>();
|
||||
Point end_offset = Point(x1, y1) + (normal).cast<coord_t>();
|
||||
|
||||
dda(x0, y0, x1, y1, fn);
|
||||
dda(start_offset.x(), start_offset.y(), end_offset.x(), end_offset.y(), fn);
|
||||
}
|
||||
|
||||
template<typename CellPositionType, typename CellQueryFn> class JPSTracer
|
||||
{
|
||||
public:
|
||||
// Use incoming_dir [0,0] for starting points, so that all directions are checked from that point
|
||||
struct Node
|
||||
{
|
||||
CellPositionType position;
|
||||
CellPositionType incoming_dir;
|
||||
};
|
||||
|
||||
JPSTracer(CellPositionType target, CellQueryFn is_passable) : target(target), is_passable(is_passable) {}
|
||||
|
||||
private:
|
||||
CellPositionType target;
|
||||
CellQueryFn is_passable; // should return boolean whether the cell is passable or not
|
||||
|
||||
CellPositionType find_jump_point(CellPositionType start, CellPositionType forward_dir) const
|
||||
{
|
||||
CellPositionType next = start + forward_dir;
|
||||
while (next != target && is_passable(next) && !(is_jump_point(next, forward_dir))) { next = next + forward_dir; }
|
||||
|
||||
if (is_passable(next)) {
|
||||
return next;
|
||||
} else {
|
||||
return start;
|
||||
}
|
||||
}
|
||||
|
||||
bool is_jump_point(CellPositionType pos, CellPositionType forward_dir) const
|
||||
{
|
||||
if (abs(forward_dir.x()) + abs(forward_dir.y()) == 2) {
|
||||
// diagonal
|
||||
CellPositionType horizontal_check_dir = CellPositionType{forward_dir.x(), 0};
|
||||
CellPositionType vertical_check_dir = CellPositionType{0, forward_dir.y()};
|
||||
|
||||
if (!is_passable(pos - horizontal_check_dir) && is_passable(pos + forward_dir - 2 * horizontal_check_dir)) { return true; }
|
||||
|
||||
if (!is_passable(pos - vertical_check_dir) && is_passable(pos + forward_dir - 2 * vertical_check_dir)) { return true; }
|
||||
|
||||
if (find_jump_point(pos, horizontal_check_dir) != pos) { return true; }
|
||||
|
||||
if (find_jump_point(pos, vertical_check_dir) != pos) { return true; }
|
||||
|
||||
return false;
|
||||
} else { // horizontal or vertical
|
||||
CellPositionType side_dir = CellPositionType(forward_dir.y(), forward_dir.x());
|
||||
|
||||
if (!is_passable(pos + side_dir) && is_passable(pos + forward_dir + side_dir)) { return true; }
|
||||
|
||||
if (!is_passable(pos - side_dir) && is_passable(pos + forward_dir - side_dir)) { return true; }
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
template<class Fn> void foreach_reachable(const Node &from, Fn &&fn) const
|
||||
{
|
||||
const CellPositionType & pos = from.position;
|
||||
const CellPositionType & forward_dir = from.incoming_dir;
|
||||
std::vector<CellPositionType> dirs_to_check{};
|
||||
|
||||
if (abs(forward_dir.x()) + abs(forward_dir.y()) == 0) { // special case for starting point
|
||||
dirs_to_check = all_directions;
|
||||
} else if (abs(forward_dir.x()) + abs(forward_dir.y()) == 2) {
|
||||
// diagonal
|
||||
CellPositionType horizontal_check_dir = CellPositionType{forward_dir.x(), 0};
|
||||
CellPositionType vertical_check_dir = CellPositionType{0, forward_dir.y()};
|
||||
|
||||
if (!is_passable(pos - horizontal_check_dir) && is_passable(pos + forward_dir - 2 * horizontal_check_dir)) {
|
||||
dirs_to_check.push_back(forward_dir - 2 * horizontal_check_dir);
|
||||
}
|
||||
|
||||
if (!is_passable(pos - vertical_check_dir) && is_passable(pos + forward_dir - 2 * vertical_check_dir)) {
|
||||
dirs_to_check.push_back(forward_dir - 2 * vertical_check_dir);
|
||||
}
|
||||
|
||||
dirs_to_check.push_back(horizontal_check_dir);
|
||||
dirs_to_check.push_back(vertical_check_dir);
|
||||
dirs_to_check.push_back(forward_dir);
|
||||
|
||||
} else { // horizontal or vertical
|
||||
CellPositionType side_dir = CellPositionType(forward_dir.y(), forward_dir.x());
|
||||
|
||||
if (!is_passable(pos + side_dir) && is_passable(pos + forward_dir + side_dir)) { dirs_to_check.push_back(forward_dir + side_dir); }
|
||||
|
||||
if (!is_passable(pos - side_dir) && is_passable(pos + forward_dir - side_dir)) { dirs_to_check.push_back(forward_dir - side_dir); }
|
||||
dirs_to_check.push_back(forward_dir);
|
||||
}
|
||||
|
||||
for (const CellPositionType &dir : dirs_to_check) {
|
||||
CellPositionType jp = find_jump_point(pos, dir);
|
||||
if (jp != pos) fn(Node{jp, dir});
|
||||
}
|
||||
}
|
||||
|
||||
float distance(Node a, Node b) const { return (a.position - b.position).template cast<double>().norm(); }
|
||||
|
||||
float goal_heuristic(Node n) const { return n.position == target ? -1.f : (target - n.position).template cast<double>().norm(); }
|
||||
|
||||
size_t unique_id(Node n) const { return (static_cast<size_t>(uint16_t(n.position.x())) << 16) + static_cast<size_t>(uint16_t(n.position.y())); }
|
||||
|
||||
const std::vector<CellPositionType> all_directions{{1, 0}, {1, 1}, {0, 1}, {-1, 1}, {-1, 0}, {-1, -1}, {0, -1}, {1, -1}};
|
||||
};
|
||||
|
||||
void JPSPathFinder::clear()
|
||||
{
|
||||
inpassable.clear();
|
||||
max_search_box.max = Pixel(std::numeric_limits<coord_t>::min(), std::numeric_limits<coord_t>::min());
|
||||
max_search_box.min = Pixel(std::numeric_limits<coord_t>::max(), std::numeric_limits<coord_t>::max());
|
||||
add_obstacles(bed_shape);
|
||||
}
|
||||
|
||||
void JPSPathFinder::add_obstacles(const Lines &obstacles)
|
||||
{
|
||||
auto store_obstacle = [&](coord_t x, coord_t y) {
|
||||
max_search_box.max.x() = std::max(max_search_box.max.x(), x);
|
||||
max_search_box.max.y() = std::max(max_search_box.max.y(), y);
|
||||
max_search_box.min.x() = std::min(max_search_box.min.x(), x);
|
||||
max_search_box.min.y() = std::min(max_search_box.min.y(), y);
|
||||
inpassable.insert(Pixel{x, y});
|
||||
return true;
|
||||
};
|
||||
|
||||
for (const Line &l : obstacles) {
|
||||
Pixel start = pixelize(l.a);
|
||||
Pixel end = pixelize(l.b);
|
||||
double_dda_with_offset(start.x(), start.y(), end.x(), end.y(), store_obstacle);
|
||||
}
|
||||
}
|
||||
|
||||
Polyline JPSPathFinder::find_path(const Point &p0, const Point &p1)
|
||||
{
|
||||
Pixel start = pixelize(p0);
|
||||
Pixel end = pixelize(p1);
|
||||
if (inpassable.empty() || (start - end).cast<float>().norm() < 3.0) { return Polyline{p0, p1}; }
|
||||
|
||||
if (inpassable.find(start) != inpassable.end()) {
|
||||
dda(start.x(), start.y(), end.x(), end.y(), [&](coord_t x, coord_t y) {
|
||||
if (inpassable.find(Pixel(x, y)) == inpassable.end() || start == end) { // new start not found yet, and xy passable
|
||||
start = Pixel(x, y);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
if (inpassable.find(end) != inpassable.end()) {
|
||||
dda(end.x(), end.y(), start.x(), start.y(), [&](coord_t x, coord_t y) {
|
||||
if (inpassable.find(Pixel(x, y)) == inpassable.end() || start == end) { // new start not found yet, and xy passable
|
||||
end = Pixel(x, y);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
BoundingBox search_box = max_search_box;
|
||||
search_box.max -= Pixel(1, 1);
|
||||
search_box.min += Pixel(1, 1);
|
||||
|
||||
BoundingBox bounding_square(Points{start, end});
|
||||
bounding_square.max += Pixel(5, 5);
|
||||
bounding_square.min -= Pixel(5, 5);
|
||||
coord_t bounding_square_size = 2 * std::max(bounding_square.size().x(), bounding_square.size().y());
|
||||
bounding_square.max.x() += (bounding_square_size - bounding_square.size().x()) / 2;
|
||||
bounding_square.min.x() -= (bounding_square_size - bounding_square.size().x()) / 2;
|
||||
bounding_square.max.y() += (bounding_square_size - bounding_square.size().y()) / 2;
|
||||
bounding_square.min.y() -= (bounding_square_size - bounding_square.size().y()) / 2;
|
||||
|
||||
// Intersection - limit the search box to a square area around the start and end, to fasten the path searching
|
||||
search_box.max = search_box.max.cwiseMin(bounding_square.max);
|
||||
search_box.min = search_box.min.cwiseMax(bounding_square.min);
|
||||
|
||||
auto cell_query = [&](Pixel pixel) { return search_box.contains(pixel) && (pixel == start || pixel == end || inpassable.find(pixel) == inpassable.end()); };
|
||||
|
||||
JPSTracer<Pixel, decltype(cell_query)> tracer(end, cell_query);
|
||||
using QNode = astar::QNode<JPSTracer<Pixel, decltype(cell_query)>>;
|
||||
|
||||
std::unordered_map<size_t, QNode> astar_cache{};
|
||||
std::vector<Pixel, PointsAllocator<Pixel>> out_path;
|
||||
std::vector<decltype(tracer)::Node> out_nodes;
|
||||
|
||||
if (!astar::search_route(tracer, {start, {0, 0}}, std::back_inserter(out_nodes), astar_cache)) {
|
||||
// path not found - just reconstruct the best path from astar cache.
|
||||
// Note that astar_cache is NOT empty - at least the starting point should always be there
|
||||
auto coordiante_func = [&astar_cache](size_t idx, size_t dim) { return float(astar_cache[idx].node.position[dim]); };
|
||||
std::vector<size_t> keys;
|
||||
keys.reserve(astar_cache.size());
|
||||
for (const auto &pair : astar_cache) { keys.push_back(pair.first); }
|
||||
KDTreeIndirect<2, float, decltype(coordiante_func)> kd_tree(coordiante_func, keys);
|
||||
size_t closest_qnode = find_closest_point(kd_tree, end.cast<float>());
|
||||
|
||||
out_path.push_back(end);
|
||||
while (closest_qnode != astar::Unassigned) {
|
||||
out_path.push_back(astar_cache[closest_qnode].node.position);
|
||||
closest_qnode = astar_cache[closest_qnode].parent;
|
||||
}
|
||||
} else {
|
||||
for (const auto &node : out_nodes) { out_path.push_back(node.position); }
|
||||
out_path.push_back(start);
|
||||
}
|
||||
|
||||
#ifdef DEBUG_FILES
|
||||
auto scaled_points = [](const Points &ps) {
|
||||
Points r;
|
||||
for (const Point &p : ps) { r.push_back(Point::new_scale(p.x(), p.y())); }
|
||||
return r;
|
||||
};
|
||||
auto scaled_point = [](const Point &p) { return Point::new_scale(p.x(), p.y()); };
|
||||
::Slic3r::SVG svg(debug_out_path(("path_jps" + std::to_string(print_z) + "_" + std::to_string(rand() % 1000)).c_str()).c_str(),
|
||||
BoundingBox(scaled_point(search_box.min), scaled_point(search_box.max)));
|
||||
for (const auto &p : inpassable) { svg.draw(scaled_point(p), "black", scale_(0.4)); }
|
||||
for (const auto &qn : astar_cache) { svg.draw(scaled_point(qn.second.node.position), "blue", scale_(0.3)); }
|
||||
svg.draw(Polyline(scaled_points(out_path)), "yellow", scale_(0.25));
|
||||
svg.draw(scaled_point(end), "purple", scale_(0.4));
|
||||
svg.draw(scaled_point(start), "green", scale_(0.4));
|
||||
#endif
|
||||
|
||||
std::vector<Pixel, PointsAllocator<Pixel>> tmp_path;
|
||||
tmp_path.reserve(out_path.size());
|
||||
// Some path found, reverse and remove points that do not change direction
|
||||
std::reverse(out_path.begin(), out_path.end());
|
||||
{
|
||||
tmp_path.push_back(out_path.front()); // first point
|
||||
for (size_t i = 1; i < out_path.size() - 1; i++) {
|
||||
if ((out_path[i] - out_path[i - 1]).cast<float>().normalized() != (out_path[i + 1] - out_path[i]).cast<float>().normalized()) { tmp_path.push_back(out_path[i]); }
|
||||
}
|
||||
tmp_path.push_back(out_path.back()); // last_point
|
||||
out_path = tmp_path;
|
||||
}
|
||||
|
||||
#ifdef DEBUG_FILES
|
||||
svg.draw(Polyline(scaled_points(out_path)), "orange", scale_(0.20));
|
||||
#endif
|
||||
|
||||
tmp_path.clear();
|
||||
// remove redundant jump points - there are points that change direction but are not needed - this inefficiency arises from the
|
||||
// usage of grid search The removal alg tries to find the longest Px Px+k path without obstacles. If Px Px+k+1 is blocked, it will
|
||||
// insert the Px+k point to result and continue search from Px+k
|
||||
{
|
||||
tmp_path.push_back(out_path.front()); // first point
|
||||
size_t index_of_last_stored_point = 0;
|
||||
for (size_t i = 1; i < out_path.size(); i++) {
|
||||
if (i - index_of_last_stored_point < 2) continue;
|
||||
bool passable = true;
|
||||
auto store_obstacle = [&](coord_t x, coord_t y) {
|
||||
if (Pixel(x, y) != start && Pixel(x, y) != end && inpassable.find(Pixel(x, y)) != inpassable.end()) {
|
||||
passable = false;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
dda(tmp_path.back().x(), tmp_path.back().y(), out_path[i].x(), out_path[i].y(), store_obstacle);
|
||||
if (!passable) {
|
||||
tmp_path.push_back(out_path[i - 1]);
|
||||
index_of_last_stored_point = i - 1;
|
||||
}
|
||||
}
|
||||
tmp_path.push_back(out_path.back()); // last_point
|
||||
out_path = tmp_path;
|
||||
}
|
||||
|
||||
#ifdef DEBUG_FILES
|
||||
svg.draw(Polyline(scaled_points(out_path)), "red", scale_(0.15));
|
||||
svg.Close();
|
||||
#endif
|
||||
|
||||
// before returing the path, transform it from pixels back to points.
|
||||
// Also replace the first and last pixel by input points so that result path patches input params exactly.
|
||||
for (Pixel &p : out_path) { p = unpixelize(p); }
|
||||
out_path.front() = p0;
|
||||
out_path.back() = p1;
|
||||
|
||||
return Polyline(out_path);
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -1,39 +0,0 @@
|
||||
#pragma once
|
||||
#include "Line.hpp"
|
||||
#ifndef SRC_LIBSLIC3R_JUMPPOINTSEARCH_HPP_
|
||||
#define SRC_LIBSLIC3R_JUMPPOINTSEARCH_HPP_
|
||||
|
||||
#include "BoundingBox.hpp"
|
||||
#include "Polygon.hpp"
|
||||
#include "libslic3r/Layer.hpp"
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include "libslic3r/Polyline.hpp"
|
||||
#include "libslic3r/libslic3r.h"
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
class JPSPathFinder
|
||||
{
|
||||
using Pixel = Point;
|
||||
std::unordered_set<Pixel, PointHash> inpassable;
|
||||
coordf_t print_z;
|
||||
BoundingBox max_search_box;
|
||||
Lines bed_shape;
|
||||
|
||||
const coord_t resolution = scaled(1.5);
|
||||
Pixel pixelize(const Point &p) { return p / resolution; }
|
||||
Point unpixelize(const Pixel &p) { return p * resolution; }
|
||||
|
||||
public:
|
||||
JPSPathFinder() = default;
|
||||
void init_bed_shape(const Points &bed_shape) { this->bed_shape = (to_lines(Polygon{bed_shape})); };
|
||||
void clear();
|
||||
void add_obstacles(const Lines &obstacles);
|
||||
Polyline find_path(const Point &start, const Point &end);
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif /* SRC_LIBSLIC3R_JUMPPOINTSEARCH_HPP_ */
|
||||
@@ -437,10 +437,14 @@ coordf_t Layer::get_sparse_infill_max_void_area()
|
||||
double max_void_area = 0.;
|
||||
for (auto layerm : m_regions) {
|
||||
Flow flow = layerm->flow(frInfill);
|
||||
float density = layerm->region().config().sparse_infill_density;
|
||||
InfillPattern pattern = layerm->region().config().sparse_infill_pattern;
|
||||
const PrintRegionConfig &config = layerm->region().config();
|
||||
float density = config.sparse_infill_density;
|
||||
InfillPattern pattern = config.sparse_infill_pattern;
|
||||
if (density == 0.)
|
||||
return -1;
|
||||
// Orca: the adaptive TPMS infill is as sparse as its interior density.
|
||||
if (density < 100.f && config.tpms_adaptive != TpmsAdaptiveMode::Disabled && is_tpms_adaptive_pattern(pattern))
|
||||
density = std::min(density, std::max(1.f, float(config.tpms_interior_density)));
|
||||
|
||||
//BBS: rough estimation and need to be optimized
|
||||
double spacing = flow.scaled_spacing() * (100 - density) / density;
|
||||
|
||||
@@ -33,7 +33,7 @@ class PrintObject;
|
||||
class Print;
|
||||
|
||||
namespace FillAdaptive {
|
||||
struct Octree;
|
||||
struct RegionOctrees;
|
||||
};
|
||||
|
||||
namespace FillLightning {
|
||||
@@ -170,10 +170,10 @@ public:
|
||||
ExPolygons lslices;
|
||||
ExPolygons lslices_extrudable; // BBS: the extrudable part of lslices used for tree support
|
||||
std::vector<BoundingBox> lslices_bboxes;
|
||||
// Orca: for separated infills / per-model centering. Aligned with lslices: for each island, the
|
||||
// full bounding box of the 3D connected body (across all layers) it belongs to. Populated by
|
||||
// PrintObject::infill() only when the feature is used; empty otherwise.
|
||||
std::vector<BoundingBox> lslices_separated_component_bboxes;
|
||||
// Orca: for separated infills / per-model centering / octree infills. Aligned with lslices: for each
|
||||
// island, the 3D connected body (across all layers) it belongs to, indexing
|
||||
// PrintObject::separated_body_bboxes(). Populated by PrintObject::prepare_infill() only when needed.
|
||||
std::vector<size_t> lslices_separated_component_ids;
|
||||
|
||||
// BBS
|
||||
ExPolygons loverhangs;
|
||||
@@ -207,10 +207,9 @@ public:
|
||||
static bool is_perimeter_compatible(const Print& print, const PrintRegion& a, const PrintRegion& b);
|
||||
void make_perimeters();
|
||||
// Phony version of make_fills() without parameters for Perl integration only.
|
||||
void make_fills() { this->make_fills(nullptr, nullptr); }
|
||||
void make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive::Octree* support_fill_octree, FillLightning::Generator* lightning_generator = nullptr);
|
||||
Polylines generate_sparse_infill_polylines_for_anchoring(FillAdaptive::Octree *adaptive_fill_octree,
|
||||
FillAdaptive::Octree *support_fill_octree,
|
||||
void make_fills() { this->make_fills(nullptr); }
|
||||
void make_fills(const FillAdaptive::RegionOctrees* fill_octrees, FillLightning::Generator* lightning_generator = nullptr);
|
||||
Polylines generate_sparse_infill_polylines_for_anchoring(const FillAdaptive::RegionOctrees *fill_octrees,
|
||||
FillLightning::Generator* lightning_generator) const;
|
||||
void make_ironing();
|
||||
// Returns the filament id (1-based) the region is ironed with, or -1 when the
|
||||
|
||||
@@ -62,7 +62,7 @@ Flow LayerRegion::bridging_flow(FlowRole role, bool thick_bridge) const
|
||||
const PrintObject &print_object = *this->layer()->object();
|
||||
Flow bridge_flow;
|
||||
// Here this->extruder(role) - 1 may underflow to MAX_INT, but then the get_at() will fall back to zero'th element, so everything is all right.
|
||||
auto nozzle_diameter = float(print_object.print()->config().nozzle_diameter.get_at(region.extruder(role) - 1));
|
||||
auto nozzle_diameter = float(nozzle_diameter_for_filament(print_object.print()->config(), region.extruder(role), print_object.print()->is_BBL_printer()));
|
||||
const ConfigOptionFloatOrPercent& bridge_width_opt = region_config.bridge_line_width;
|
||||
const double bridge_width = bridge_width_opt.get_abs_value(nozzle_diameter);
|
||||
const bool has_bridge_width = bridge_width > 0.;
|
||||
|
||||
@@ -154,13 +154,12 @@ template<class ExecutionPolicy, class Enable = void> struct _Loop
|
||||
};
|
||||
|
||||
// Add Specialization for using ExecutionTBB for parallel loops.
|
||||
using namespace Slic3r;
|
||||
template<> struct _Loop<ExecutionTBB>
|
||||
template<> struct _Loop<Slic3r::ExecutionTBB>
|
||||
{
|
||||
template<class It, class Fn> static void for_each_idx(It from, It to, Fn&& fn)
|
||||
{
|
||||
execution::for_each(
|
||||
ex_tbb, size_t(0), size_t(to - from), [&from, &fn](size_t i) { fn(from[i], i); }, execution::max_concurrency(ex_tbb));
|
||||
Slic3r::execution::for_each(
|
||||
Slic3r::ex_tbb, size_t(0), size_t(to - from), [&from, &fn](size_t i) { fn(from[i], i); }, Slic3r::execution::max_concurrency(Slic3r::ex_tbb));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -29,8 +29,6 @@
|
||||
#include <CGAL/Polygon_mesh_processing/triangulate_hole.h>
|
||||
#include <CGAL/Polygon_mesh_processing/stitch_borders.h>
|
||||
#include <CGAL/Polygon_mesh_processing/polygon_soup_to_polygon_mesh.h>
|
||||
#include <CGAL/Polygon_mesh_processing/repair_degeneracies.h>
|
||||
#include <CGAL/Polygon_mesh_processing/manifoldness.h>
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <algorithm>
|
||||
@@ -50,7 +48,6 @@
|
||||
#include <CGAL/Polygon_mesh_processing/orient_polygon_soup.h>
|
||||
#include <CGAL/Polygon_mesh_processing/repair.h>
|
||||
#include <CGAL/Polygon_mesh_processing/remesh.h>
|
||||
#include <CGAL/Polygon_mesh_processing/repair_polygon_soup.h>
|
||||
#include <CGAL/Polygon_mesh_processing/orientation.h>
|
||||
// BBS: for segment
|
||||
#include <CGAL/mesh_segmentation.h>
|
||||
@@ -658,91 +655,6 @@ bool empty(const CGALMesh &mesh)
|
||||
return mesh.m.is_empty();
|
||||
}
|
||||
|
||||
bool repair(TriangleMesh& mesh, RepairedMeshErrors* repaired_errors, std::string* error)
|
||||
{
|
||||
namespace PMP = CGAL::Polygon_mesh_processing;
|
||||
|
||||
if (mesh.empty())
|
||||
return true;
|
||||
|
||||
try {
|
||||
// 1) Convert to polygon soup
|
||||
std::vector<_EpicMesh::Point> points;
|
||||
std::vector<std::vector<std::size_t>> polygons;
|
||||
|
||||
points.reserve(mesh.its.vertices.size());
|
||||
polygons.reserve(mesh.its.indices.size());
|
||||
|
||||
for (const auto& v : mesh.its.vertices)
|
||||
points.emplace_back(v.x(), v.y(), v.z());
|
||||
|
||||
for (const auto& f : mesh.its.indices)
|
||||
polygons.push_back({size_t(f[0]), size_t(f[1]), size_t(f[2])});
|
||||
|
||||
// 2) Aggressive soup cleanup
|
||||
PMP::repair_polygon_soup(points, polygons);
|
||||
|
||||
// 3) Convert soup → mesh
|
||||
_EpicMesh cgal_mesh;
|
||||
PMP::polygon_soup_to_polygon_mesh(points, polygons, cgal_mesh);
|
||||
|
||||
// 4) Remove degenerate geometry
|
||||
PMP::remove_degenerate_faces(cgal_mesh);
|
||||
PMP::remove_isolated_vertices(cgal_mesh);
|
||||
|
||||
// 5) Fix remaining non-manifold vertices
|
||||
PMP::duplicate_non_manifold_vertices(cgal_mesh);
|
||||
|
||||
// 6) Boolean union (keeps only outer shell)
|
||||
_EpicMesh tmp;
|
||||
if (PMP::corefine_and_compute_union(cgal_mesh, cgal_mesh, tmp)) {
|
||||
cgal_mesh = std::move(tmp);
|
||||
}
|
||||
// If it fails, continue anyway with previous mesh
|
||||
|
||||
// 7) Fill holes
|
||||
if (!CGAL::is_closed(cgal_mesh)) {
|
||||
using halfedge_descriptor = boost::graph_traits<_EpicMesh>::halfedge_descriptor;
|
||||
|
||||
std::vector<halfedge_descriptor> borders;
|
||||
CGAL::extract_boundary_cycles(cgal_mesh, std::back_inserter(borders));
|
||||
|
||||
for (halfedge_descriptor h : borders) {
|
||||
PMP::triangulate_and_refine_hole(cgal_mesh, h);
|
||||
}
|
||||
}
|
||||
|
||||
// 8) Final validity check
|
||||
if (!CGAL::is_closed(cgal_mesh)) {
|
||||
if (error)
|
||||
*error = "Repair failed: mesh still open after hole filling.";
|
||||
return false;
|
||||
}
|
||||
|
||||
// 9) Ensure outward orientation
|
||||
if (!PMP::does_bound_a_volume(cgal_mesh))
|
||||
PMP::orient_to_bound_a_volume(cgal_mesh);
|
||||
|
||||
// 10) Convert back
|
||||
indexed_triangle_set its = cgal_to_indexed_triangle_set(cgal_mesh);
|
||||
|
||||
RepairedMeshErrors errs{};
|
||||
errs.facets_removed = 0;
|
||||
errs.edges_fixed = 0;
|
||||
|
||||
mesh = TriangleMesh(std::move(its), errs);
|
||||
|
||||
if (repaired_errors)
|
||||
*repaired_errors = errs;
|
||||
|
||||
return true;
|
||||
} catch (const std::exception& e) {
|
||||
if (error)
|
||||
*error = e.what();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
CGALMeshPtr clone(const CGALMesh &m)
|
||||
{
|
||||
return CGALMeshPtr{new CGALMesh{m}};
|
||||
@@ -1045,7 +957,28 @@ void do_boolean(McutMesh& srcMesh, const McutMesh& cutMesh, const std::string& b
|
||||
// But we can force it to work by spliting the src mesh into disconnected components,
|
||||
// and do booleans seperately, then merge all the results.
|
||||
indexed_triangle_set all_its;
|
||||
if (boolean_opts == "UNION" || boolean_opts == "A_NOT_B") {
|
||||
if (boolean_opts == "A_NOT_B") {
|
||||
// Each cut can leave the source with several disconnected components, which mcut rejects
|
||||
// in the next dispatch, so re-split after every cut part (e.g. each letter of a text).
|
||||
std::vector<indexed_triangle_set> parts = std::move(src_parts);
|
||||
for (size_t j = 0; j < cut_parts.size(); j++) {
|
||||
auto cut_part = triangle_mesh_to_mcut(cut_parts[j]);
|
||||
std::vector<indexed_triangle_set> next_parts;
|
||||
for (indexed_triangle_set &part : parts) {
|
||||
auto src_part = triangle_mesh_to_mcut(part);
|
||||
if (do_boolean_single(*src_part, *cut_part, boolean_opts)) {
|
||||
TriangleMesh tri_part = mcut_to_triangle_mesh(*src_part);
|
||||
std::vector<indexed_triangle_set> pieces = its_split(tri_part.its);
|
||||
std::move(pieces.begin(), pieces.end(), std::back_inserter(next_parts));
|
||||
} else
|
||||
next_parts.emplace_back(std::move(part));
|
||||
}
|
||||
parts = std::move(next_parts);
|
||||
}
|
||||
for (const indexed_triangle_set &part : parts)
|
||||
its_merge(all_its, part);
|
||||
}
|
||||
else if (boolean_opts == "UNION") {
|
||||
for (size_t i = 0; i < src_parts.size(); i++) {
|
||||
auto src_part = triangle_mesh_to_mcut(src_parts[i]);
|
||||
for (size_t j = 0; j < cut_parts.size(); j++) {
|
||||
|
||||
@@ -77,9 +77,6 @@ TriangleMesh merge(std::vector<TriangleMesh> meshes);
|
||||
bool does_bound_a_volume(const CGALMesh &mesh);
|
||||
bool empty(const CGALMesh &mesh);
|
||||
|
||||
// Repair a mesh using CGAL. Returns true on success. Optionally returns a summary of repairs and an error string.
|
||||
bool repair(TriangleMesh &mesh, RepairedMeshErrors *repaired_errors = nullptr, std::string *error = nullptr);
|
||||
|
||||
// Real UV unwrap of an open mesh patch via CGAL's LSCM (Least Squares Conformal Maps) surface
|
||||
// parameterization. Returns one UV coordinate per input vertex (same indexing as `mesh.vertices`),
|
||||
// or nullopt if `mesh` isn't a single topological disk -- LSCM needs exactly one connected
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
class TriangleMesh;
|
||||
|
||||
struct MeshRepairStats
|
||||
{
|
||||
std::size_t welded_vertices = 0;
|
||||
std::size_t removed_triangles = 0;
|
||||
std::size_t reversed_shells = 0;
|
||||
std::size_t filled_holes = 0;
|
||||
std::size_t removed_shells = 0;
|
||||
std::size_t self_intersections = 0;
|
||||
// False when the input already was closed, consistently oriented and free of self-intersections.
|
||||
bool modified = false;
|
||||
};
|
||||
|
||||
// Returns false, leaving the mesh untouched, if no solid can be built. progress gets the percentage done and may
|
||||
// throw to cancel. See docs/HLSD/mesh-repair.md.
|
||||
bool repair_mesh(TriangleMesh &mesh, MeshRepairStats *stats = nullptr, std::string *error = nullptr,
|
||||
const std::function<void(int)> &progress = {});
|
||||
|
||||
} // namespace Slic3r
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user