Merge branch 'main' into feat/ams-parsing

This commit is contained in:
Ian Chua
2026-10-15 14:42:59 +08:00
929 changed files with 94148 additions and 85515 deletions
+7 -2
View File
@@ -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
View File
@@ -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) {
+6
View File
@@ -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)
+29 -11
View File
@@ -202,6 +202,7 @@ Vec2d place_wipe_tower(DynamicPrintConfig &cfg, const Vec2d &center)
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
+292
View File
@@ -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;
}
+3
View File
@@ -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
+84 -9
View File
@@ -299,7 +299,15 @@ Points get_shrink_bedpts(const DynamicPrintConfig* print_cfg, const ArrangeParam
template<class PConf>
void fill_config(PConf& pcfg, const ArrangeParams &params) {
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;
+7
View File
@@ -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
+562
View File
@@ -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 &region, 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 &region_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
+178
View File
@@ -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 &region, 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_
+50
View File
@@ -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
+25
View File
@@ -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
+433
View File
@@ -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 &lt : 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 &lt : 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
+82
View File
@@ -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
+34
View File
@@ -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
+153
View File
@@ -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
+132
View File
@@ -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
View File
@@ -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,
+1 -1
View File
@@ -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.
-1
View File
@@ -84,7 +84,6 @@
#include <cmath>
#include <cctype>
#include <cstdio>
#include <functional>
#include <gp_XY.hxx>
#include <initializer_list>
#include <math.h>
+34 -4
View File
@@ -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)
+1 -1
View File
@@ -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
+292
View File
@@ -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, &params](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 &region, 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 &region : 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
+61
View File
@@ -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
-1
View File
@@ -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;
-1
View File
@@ -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>
+5
View File
@@ -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);
+54
View File
@@ -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)
+18
View File
@@ -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
View File
@@ -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 &params = 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.
+1
View File
@@ -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(
+14 -77
View File
@@ -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
+38 -5
View File
@@ -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();
+12 -2
View File
@@ -128,6 +128,9 @@ Polylines Fill::fill_surface(const Surface *surface, const FillParams &params)
{
// 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 &params)
_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 &params)
+12
View File
@@ -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 &params);
virtual ThickPolylines fill_surface_arachne(const Surface* surface, const FillParams& params);
+1
View File
@@ -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(
+26 -5
View File
@@ -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 = [&params, 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 ->
+1
View File
@@ -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)
+1
View File
@@ -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); };
+7 -18
View File
@@ -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 &params, 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) {
-3
View File
@@ -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
+823
View File
@@ -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 &center) 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 &params, 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
+143
View File
@@ -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 &center) 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 &params, coordf_t spacing,
const std::function<void(const FillParams &, const ExPolygon &)> &fill_shell);
} // namespace Slic3r
+43 -18
View File
@@ -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 = [&params, 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);
+1
View File
@@ -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;
+33 -15
View File
@@ -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 = [&params, 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);
+1
View File
@@ -31,6 +31,7 @@ public:
Polylines& polylines_out) override;
bool is_self_crossing() override { return false; }
bool aligned_to_origin() const override { return true; }
};
+4 -4
View File
@@ -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));
}
+7 -2
View File
@@ -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;*/
+2
View File
@@ -1,3 +1,5 @@
#pragma once
#include <string>
namespace Slic3r {
+15 -6
View File
@@ -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:
+249 -20
View File
@@ -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 &current_objects)
bool _BBS_3MF_Importer::_generate_current_object_list(std::vector<Component> &sub_objects, Id object_id, IdToCurrentObjectMap &current_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
View File
File diff suppressed because it is too large Load Diff
+152 -9
View File
@@ -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;
+33
View File
@@ -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
+38
View File
@@ -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_
+29
View File
@@ -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
+59
View File
@@ -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_
-2
View File
@@ -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);
+31 -2
View File
@@ -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);
+4 -1
View File
@@ -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;
};
}
+5 -2
View File
@@ -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)
+216 -22
View File
@@ -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);
+92 -1
View File
@@ -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_
+25
View File
@@ -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
+92
View File
@@ -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_
+1 -4
View File
@@ -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
+2 -2
View File
@@ -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
+1 -2
View File
@@ -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);
+68 -2
View File
@@ -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 &lt : 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 &lt = m_layer_tools[i];
const LayerTools &lt_next = m_layer_tools[i + 1];
if (lt.print_z < object_bottom_z + EPSILON && lt_next.print_z >= object_bottom_z + EPSILON) {
+10 -6
View File
@@ -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;
+4
View File
@@ -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;
+22
View File
@@ -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
+13
View File
@@ -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
-580
View File
@@ -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
-81
View File
@@ -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
View File
@@ -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 &center_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
View File
@@ -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 &center_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 &center_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
View File
@@ -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;
-2
View File
@@ -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
View File
@@ -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>
+220
View File
@@ -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
+101
View File
@@ -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
+712
View File
@@ -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
+609
View File
@@ -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
+379
View File
@@ -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
+94
View File
@@ -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
-349
View File
@@ -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
-39
View File
@@ -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_ */
+6 -2
View File
@@ -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;
+8 -9
View File
@@ -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
+1 -1
View File
@@ -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.;
+3 -4
View File
@@ -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));
}
};
+22 -89
View File
@@ -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++) {
-3
View File
@@ -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
+28
View File
@@ -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