mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-14 11:21:18 +00:00
Merge upstream main: belt printing, texture displacement color mixing, 3MF component cycle checks, undo blocked during background jobs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+7
-1
@@ -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.
|
||||
@@ -186,7 +192,7 @@ 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>")
|
||||
|
||||
+52
-15
@@ -1820,6 +1820,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 +2507,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 +3435,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 +3533,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 +3545,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 +3597,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 +3657,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();
|
||||
@@ -4150,6 +4182,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))
|
||||
@@ -4387,11 +4423,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;
|
||||
@@ -4617,7 +4653,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());
|
||||
@@ -5261,7 +5298,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();
|
||||
@@ -5413,7 +5450,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");
|
||||
@@ -5581,7 +5618,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");
|
||||
@@ -5688,7 +5725,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) {
|
||||
@@ -6313,7 +6350,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;
|
||||
|
||||
@@ -28,6 +28,12 @@ if (ORCA_TOOLS)
|
||||
target_link_libraries(generate_system_cache libslic3r boost_headeronly)
|
||||
target_compile_definitions(generate_system_cache PRIVATE ${_DEV_DEFS})
|
||||
|
||||
# texture_unwrap_dump: reports the LSCM unwrap of a saved project's texture displacement layers,
|
||||
# chart by chart, so a defect can be reproduced from the project file instead of from a screenshot.
|
||||
add_executable(texture_unwrap_dump texture_unwrap_dump.cpp)
|
||||
target_link_libraries(texture_unwrap_dump libslic3r boost_headeronly nanosvg)
|
||||
target_compile_definitions(texture_unwrap_dump PRIVATE ${_DEV_DEFS})
|
||||
|
||||
# profile_include_dump: prints what included templates contribute to a vendor's presets,
|
||||
# to diff against the same tool built in BambuStudio. Built only on request.
|
||||
add_executable(profile_include_dump EXCLUDE_FROM_ALL profile_include_dump.cpp)
|
||||
|
||||
@@ -202,6 +202,7 @@ Vec2d place_wipe_tower(DynamicPrintConfig &cfg, const Vec2d ¢er)
|
||||
std::string slice_two_color_cube_and_export(DynamicPrintConfig cfg, bool is_bbl, bool by_object)
|
||||
{
|
||||
const Vec2d center = printable_area_center(cfg);
|
||||
const bool belt = cfg.opt_bool("belt_printer");
|
||||
std::vector<Vec2d> cube_mins;
|
||||
if (by_object) {
|
||||
// By-object printing fires the hook only without a wipe tower, and rules out clumping detection and
|
||||
@@ -212,6 +213,12 @@ std::string slice_two_color_cube_and_export(DynamicPrintConfig cfg, bool is_bbl,
|
||||
cfg.set_key_value("timelapse_type", new ConfigOptionEnum<TimelapseType>(tlTraditional));
|
||||
cfg.set_key_value("skirt_loops", new ConfigOptionInt(0));
|
||||
cube_mins = {center + Vec2d(-20., -5.), center + Vec2d(10., -5.)};
|
||||
} else if (belt) {
|
||||
// A belt object's slicing Z starts at the belt below its leading end, well below its first
|
||||
// printed layer, so a height range in slicing Z does not map onto the part. Two cubes one
|
||||
// behind the other along the belt, the second on filament 2, give the one filament change
|
||||
// instead (the purge prism is an object the GUI adds, so there is no tower to place).
|
||||
cube_mins = {center - Vec2d(5., 15.), center + Vec2d(-5., 5.)};
|
||||
} else {
|
||||
// Clumping detection changes the tower footprint, so turn it on before placing the tower.
|
||||
if (!cfg.opt_string("wrapping_detection_gcode").empty())
|
||||
@@ -229,14 +236,20 @@ std::string slice_two_color_cube_and_export(DynamicPrintConfig cfg, bool is_bbl,
|
||||
obj->name = "cube"; // populates [input_filename_base] the way a loaded model does
|
||||
obj->add_volume(m);
|
||||
obj->add_instance();
|
||||
// Filament 2 is used only above z=4, so the upper layers carry a single filament change.
|
||||
DynamicPrintConfig range_config;
|
||||
range_config.set_key_value("extruder", new ConfigOptionInt(2));
|
||||
// Every range must carry a layer_height; use the process's own so a fine nozzle (e.g. 0.15 mm
|
||||
// printing ~0.1 mm layers) isn't forced to a height its extrusion width can't support - that
|
||||
// trips Flow::with_spacing.
|
||||
range_config.set_key_value("layer_height", new ConfigOptionFloat(cfg.opt_float("layer_height")));
|
||||
obj->layer_config_ranges[{4.0, 10.0}].assign_config(std::move(range_config));
|
||||
if (belt && !by_object) {
|
||||
// The second cube along the belt is on filament 2 (see cube_mins above).
|
||||
if (&cube_min == &cube_mins.back())
|
||||
obj->config.set_key_value("extruder", new ConfigOptionInt(2));
|
||||
} else {
|
||||
// Filament 2 is used only above z=4, so the upper layers carry a single filament change.
|
||||
DynamicPrintConfig range_config;
|
||||
range_config.set_key_value("extruder", new ConfigOptionInt(2));
|
||||
// Every range must carry a layer_height; use the process's own so a fine nozzle (e.g. 0.15 mm
|
||||
// printing ~0.1 mm layers) isn't forced to a height its extrusion width can't support - that
|
||||
// trips Flow::with_spacing.
|
||||
range_config.set_key_value("layer_height", new ConfigOptionFloat(cfg.opt_float("layer_height")));
|
||||
obj->layer_config_ranges[{4.0, 10.0}].assign_config(std::move(range_config));
|
||||
}
|
||||
obj->ensure_on_bed();
|
||||
print.auto_assign_extruders(obj);
|
||||
}
|
||||
@@ -521,11 +534,16 @@ int slice_all_printers(const std::string &vendor, const std::string &outdir)
|
||||
const std::string filament_name = bundle.filaments.get_selected_preset_name();
|
||||
const std::string what = "Printer \"" + printer + "\"";
|
||||
const std::string file_base = sanitize_filename(vendor_name) + "__" + sanitize_filename(printer);
|
||||
// A belt printer has no wipe tower (it purges into a prism object on the belt), so its
|
||||
// filament change is the plain tool change set_extruder() emits rather than the tower's block.
|
||||
const bool belt = bundle.printers.get_selected_preset().config.opt_bool("belt_printer");
|
||||
const std::string marker = belt ? "\nT1\n" : "CP TOOLCHANGE START";
|
||||
if (const std::string out = slice_selection(bundle, what, false, outdir, file_base); out.empty())
|
||||
++failures;
|
||||
else if (out.find("CP TOOLCHANGE START") == std::string::npos) {
|
||||
// The filament change never rode the tower, so change_filament_gcode was not exercised.
|
||||
BOOST_LOG_TRIVIAL(error) << what << " sliced but the filament change never fired (no CP TOOLCHANGE START)";
|
||||
else if (out.find(marker) == std::string::npos) {
|
||||
// The filament change never fired, so change_filament_gcode was not exercised.
|
||||
BOOST_LOG_TRIVIAL(error) << what << " sliced but the filament change never fired (no "
|
||||
<< (belt ? "T1" : "CP TOOLCHANGE START") << ")";
|
||||
++failures;
|
||||
}
|
||||
cover(bundle.prints.get_selected_preset());
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -299,7 +299,15 @@ Points get_shrink_bedpts(const DynamicPrintConfig* print_cfg, const ArrangeParam
|
||||
template<class PConf>
|
||||
void fill_config(PConf& pcfg, const ArrangeParams ¶ms) {
|
||||
|
||||
if (params.is_seq_print) {
|
||||
if (params.is_belt) {
|
||||
// Pack from the end of the belt that prints first, and keep the pile on the
|
||||
// bed when it is larger than the room around that end.
|
||||
pcfg.starting_point = !params.belt_reversed ? PConf::Alignment::BOTTOM_LEFT :
|
||||
params.belt_axis == 1 ? PConf::Alignment::TOP_LEFT :
|
||||
PConf::Alignment::BOTTOM_RIGHT;
|
||||
pcfg.clamp_to_bin = true;
|
||||
}
|
||||
else if (params.is_seq_print) {
|
||||
// Start placing the items from the center of the print bed
|
||||
pcfg.starting_point = PConf::Alignment::BOTTOM_LEFT;
|
||||
}
|
||||
@@ -444,6 +452,50 @@ protected:
|
||||
return bindist;
|
||||
}
|
||||
|
||||
// Belt printers pack from the end of the belt that prints first, and a corner
|
||||
// packer's checks (pile inside the bin, pack origin) apply to them as well.
|
||||
bool corner_packing() const { return params.is_belt || m_pconf.starting_point == PConfig::Alignment::BOTTOM_LEFT; }
|
||||
|
||||
static double at(const Box::PointType &pt, int i) { return double(i == 0 ? getX(pt) : getY(pt)); }
|
||||
|
||||
// Position along the belt in print order: increasing from the end that prints first.
|
||||
double belt_pos(const Box::PointType &pt) const { return params.belt_reversed ? -at(pt, params.belt_axis) : at(pt, params.belt_axis); }
|
||||
double belt_start(const Box &bb) const { return belt_pos(params.belt_reversed ? bb.maxCorner() : bb.minCorner()); }
|
||||
double belt_end(const Box &bb) const { return belt_pos(params.belt_reversed ? bb.minCorner() : bb.maxCorner()); }
|
||||
|
||||
// The corner of the bin the belt pile grows from.
|
||||
Box::PointType belt_origin() const
|
||||
{
|
||||
const Box bb = sl::boundingBox(m_bin);
|
||||
auto o = bb.minCorner();
|
||||
if (params.belt_reversed) {
|
||||
if (params.belt_axis == 0) setX(o, getX(bb.maxCorner()));
|
||||
else setY(o, getY(bb.maxCorner()));
|
||||
}
|
||||
return o;
|
||||
}
|
||||
|
||||
// An item's far edge in print order is what it costs (so a row fills across the
|
||||
// belt before the pile advances), with a slight pull toward the near lateral
|
||||
// edge and the same penalty as the bottom-left heuristic for sitting outside
|
||||
// the corner.
|
||||
double dist_along_belt(const Box &ibb)
|
||||
{
|
||||
const Box bin = sl::boundingBox(m_bin);
|
||||
const int l = 1 - params.belt_axis;
|
||||
const double lat = at(ibb.minCorner(), l) - at(bin.minCorner(), l);
|
||||
double d = belt_end(ibb) - belt_start(bin);
|
||||
d += lat < 0 ? 10 * -lat : 0.1 * lat;
|
||||
if (double behind = belt_start(ibb) - belt_start(bin); behind < 0)
|
||||
d += 10 * -behind;
|
||||
return norm(d);
|
||||
}
|
||||
|
||||
double corner_bindist(const Box &ibb, const Slic3r::Point &origin_pack)
|
||||
{
|
||||
return params.is_belt ? dist_along_belt(ibb) : dist_for_BOTTOM_LEFT(ibb, origin_pack);
|
||||
}
|
||||
|
||||
double dist_to_bin(const Box& ibb, const Slic3r::Point& origin_pack, typename Packer::PlacementConfig::Alignment starting_point_alignment)
|
||||
{
|
||||
double bindist = 0;
|
||||
@@ -533,8 +585,8 @@ protected:
|
||||
|
||||
// The smalles distance from the arranged pile center:
|
||||
double dist = norm(*(std::min_element(dists.begin(), dists.end())));
|
||||
if (m_pconf.starting_point == PConfig::Alignment::BOTTOM_LEFT) {
|
||||
double bindist = dist_for_BOTTOM_LEFT(ibb, origin_pack);
|
||||
if (corner_packing()) {
|
||||
double bindist = corner_bindist(ibb, origin_pack);
|
||||
score = 0.2 * dist + 0.8 * bindist;
|
||||
}
|
||||
else {
|
||||
@@ -591,8 +643,8 @@ protected:
|
||||
break;
|
||||
}
|
||||
case LAST_BIG_ITEM: {
|
||||
if (m_pconf.starting_point == PConfig::Alignment::BOTTOM_LEFT) {
|
||||
score = dist_for_BOTTOM_LEFT(ibb, origin_pack);
|
||||
if (corner_packing()) {
|
||||
score = corner_bindist(ibb, origin_pack);
|
||||
}
|
||||
else {
|
||||
if (m_pilebb.defined)
|
||||
@@ -607,8 +659,8 @@ protected:
|
||||
// already processed bigger items.
|
||||
// No need to play around with the anchor points, the center will be
|
||||
// just fine for small items
|
||||
if (m_pconf.starting_point == PConfig::Alignment::BOTTOM_LEFT)
|
||||
score = dist_for_BOTTOM_LEFT(ibb, origin_pack);
|
||||
if (corner_packing())
|
||||
score = corner_bindist(ibb, origin_pack);
|
||||
else {
|
||||
// Align mainly around existing items
|
||||
score = 0.8 * norm(pl::distance(ibb.center(), bigbb.center()))+ 0.2*norm(pl::distance(ibb.center(), origin_pack));
|
||||
@@ -709,6 +761,28 @@ protected:
|
||||
score += 1 * (new_extruder_cnt-last_extruder_cnt);
|
||||
}
|
||||
|
||||
// On a belt the parts print in belt order, so every colour change between
|
||||
// parts is a filament change. Items arrive sorted by extruder and the pile
|
||||
// grows from the leading end; keep each colour's run contiguous by charging
|
||||
// an item for every packed item of another colour it does not fully follow,
|
||||
// counting the tilted layers that reach belt_tilt_slope * height past that
|
||||
// item's far edge.
|
||||
if (params.is_belt && !params.is_seq_print) {
|
||||
const std::set<int> item_colours(item.extrude_ids.begin(), item.extrude_ids.end());
|
||||
const double item_start = belt_start(ibb);
|
||||
for (Item &p : m_items) {
|
||||
if (p.is_virt_object)
|
||||
continue;
|
||||
const std::set<int> p_colours(p.extrude_ids.begin(), p.extrude_ids.end());
|
||||
const bool same_colour = std::includes(item_colours.begin(), item_colours.end(), p_colours.begin(), p_colours.end())
|
||||
|| std::includes(p_colours.begin(), p_colours.end(), item_colours.begin(), item_colours.end());
|
||||
if (same_colour)
|
||||
continue;
|
||||
if (item_start < belt_end(p.boundingBox()) + scaled(p.height * params.belt_tilt_slope))
|
||||
score += 10.;
|
||||
}
|
||||
}
|
||||
|
||||
return std::make_tuple(score, fullbb);
|
||||
}
|
||||
|
||||
@@ -785,7 +859,8 @@ public:
|
||||
|
||||
auto binbb = sl::boundingBox(m_bin);
|
||||
|
||||
auto starting_point = cfg.starting_point == PConfig::Alignment::BOTTOM_LEFT ? binbb.minCorner() : binbb.center();
|
||||
auto starting_point = this->params.is_belt ? belt_origin() :
|
||||
cfg.starting_point == PConfig::Alignment::BOTTOM_LEFT ? binbb.minCorner() : binbb.center();
|
||||
// if we have wipe tower, items should be arranged around wipe tower
|
||||
for (Item itm : items) {
|
||||
if (itm.is_wipe_tower) {
|
||||
@@ -936,7 +1011,7 @@ std::function<double(const Item &, const ItemGroup&)> AutoArranger<ExPolygon>::g
|
||||
auto mp = m_merged_pile;
|
||||
mp.emplace_back(itm.transformedShape());
|
||||
auto chull = sl::convexHull(mp);
|
||||
if (m_pconf.starting_point == PConfig::Alignment::BOTTOM_LEFT)
|
||||
if (corner_packing())
|
||||
{
|
||||
if (!sl::isInside(chull, m_bin))
|
||||
score += LARGE_COST_TO_REJECT;
|
||||
|
||||
@@ -146,6 +146,13 @@ struct ArrangeParams {
|
||||
float nozzle_height = 0;
|
||||
float printable_height = 256.0;
|
||||
Vec2d align_center{ 0.5,0.5 };
|
||||
// Belt printer: items print in the order they lie along the belt axis, from
|
||||
// its low end unless belt_reversed, and a part's top prints
|
||||
// belt_tilt_slope * height further along it than its base.
|
||||
bool is_belt = false;
|
||||
int belt_axis = 1; // 0 = X, 1 = Y
|
||||
bool belt_reversed = false;
|
||||
float belt_tilt_slope = 1.f; // cot(belt tilt angle), 0 when the belt is not tilted
|
||||
|
||||
ArrangePolygons excluded_regions; // regions cant't be used
|
||||
ArrangePolygons nonprefered_regions; // regions can be used but not prefered
|
||||
|
||||
@@ -0,0 +1,562 @@
|
||||
#include <limits>
|
||||
#include "BeltBrim.hpp"
|
||||
|
||||
#include "ClipperUtils.hpp"
|
||||
#include "Flow.hpp"
|
||||
#include "Layer.hpp"
|
||||
#include "Polygon.hpp"
|
||||
#include "Print.hpp"
|
||||
#include "ShortestPath.hpp"
|
||||
#include "Support/BeltFloorContext.hpp"
|
||||
#include "BoundingBox.hpp"
|
||||
#include "ExPolygon.hpp"
|
||||
#include "ExtrusionEntity.hpp"
|
||||
#include "ExtrusionEntityCollection.hpp"
|
||||
#include "Point.hpp"
|
||||
#include "Polyline.hpp"
|
||||
#include "PrintConfig.hpp"
|
||||
#include "libslic3r.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// ---------------------------------------------------------------- scaling
|
||||
|
||||
static inline Point scale_u_point(const Point &p, int from_axis, double factor)
|
||||
{
|
||||
// llround, not a cast: casting truncates toward zero, so a round trip would
|
||||
// walk every vertex toward the origin by up to one unit per pass.
|
||||
return from_axis == 0 ?
|
||||
Point(coord_t(std::llround(double(p.x()) * factor)), p.y()) :
|
||||
Point(p.x(), coord_t(std::llround(double(p.y()) * factor)));
|
||||
}
|
||||
|
||||
static inline void scale_u_polygon(Polygon &poly, int from_axis, double factor)
|
||||
{
|
||||
for (Point &p : poly.points)
|
||||
p = scale_u_point(p, from_axis, factor);
|
||||
}
|
||||
|
||||
ExPolygons belt_scale_u(const ExPolygons &src, const BeltBrimFrame &frame, double factor)
|
||||
{
|
||||
ExPolygons out = src;
|
||||
for (ExPolygon &ex : out) {
|
||||
scale_u_polygon(ex.contour, frame.from_axis, factor);
|
||||
for (Polygon &hole : ex.holes)
|
||||
scale_u_polygon(hole, frame.from_axis, factor);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
Polylines belt_scale_u(const Polylines &src, const BeltBrimFrame &frame, double factor)
|
||||
{
|
||||
Polylines out = src;
|
||||
for (Polyline &pl : out)
|
||||
for (Point &p : pl.points)
|
||||
p = scale_u_point(p, frame.from_axis, factor);
|
||||
return out;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- sweep
|
||||
|
||||
ExPolygons sweep_ex(const ExPolygons &src, const Point &t)
|
||||
{
|
||||
if (src.empty())
|
||||
return {};
|
||||
if (t == Point(0, 0))
|
||||
return src;
|
||||
|
||||
// One parallelogram per boundary edge. Together with P and P + t these
|
||||
// cover the Minkowski sum exactly: for any q = p + s*t with p in P and
|
||||
// s in [0, 1], let s* be the smallest lambda >= 0 with q - lambda*t in P.
|
||||
// Either s* == 0 (so q is in P) or q - s* * t lies on some boundary edge e,
|
||||
// putting q in that edge's parallelogram. Hole edges must be included, or
|
||||
// holes narrower than t along t would wrongly survive the sweep.
|
||||
Polygons quads;
|
||||
for (const ExPolygon &ex : src)
|
||||
for (size_t c = 0; c < ex.num_contours(); ++ c)
|
||||
for (const Line &e : ex.contour_or_hole(c).lines()) {
|
||||
if (e.a == e.b)
|
||||
continue;
|
||||
Polygon q;
|
||||
q.points = { e.a, e.b, e.b + t, e.a + t };
|
||||
// The non-zero fill rule counts a clockwise ring as -1, which
|
||||
// would punch a hole instead of adding material. Edges parallel
|
||||
// to t give a zero-area quad; Clipper discards those harmlessly.
|
||||
if (q.is_clockwise())
|
||||
q.reverse();
|
||||
quads.emplace_back(std::move(q));
|
||||
}
|
||||
|
||||
ExPolygons shifted = src;
|
||||
for (ExPolygon &ex : shifted)
|
||||
ex.translate(t);
|
||||
|
||||
// union_ex(ExPolygons, Polygons) uses pftNonZero, which is the fill rule the
|
||||
// argument above relies on.
|
||||
return union_ex(union_ex(src, shifted), quads);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- brim region
|
||||
|
||||
ExPolygons belt_brim_region(const ExPolygons &footprint_flat,
|
||||
bool has_outer,
|
||||
bool has_inner,
|
||||
coord_t brim_width,
|
||||
coord_t object_gap,
|
||||
coord_t leading,
|
||||
coord_t lateral,
|
||||
const BeltBrimFrame &frame)
|
||||
{
|
||||
if (footprint_flat.empty() || (! has_outer && ! has_inner))
|
||||
return {};
|
||||
|
||||
ExPolygons out;
|
||||
|
||||
if (has_outer) {
|
||||
// Offset the outer ring from the contours only, so a hole cannot punch
|
||||
// through it. Same reasoning as the plate brim in Brim.cpp.
|
||||
Polygons contours;
|
||||
contours.reserve(footprint_flat.size());
|
||||
for (const ExPolygon &ex : footprint_flat)
|
||||
contours.emplace_back(ex.contour);
|
||||
|
||||
// Inner and outer boundary offset from the same polygon, to avoid
|
||||
// round-off mismatch between them.
|
||||
ExPolygons inner = offset_ex(contours, float(object_gap), jtRound, SCALED_RESOLUTION);
|
||||
|
||||
// Close the interior before offsetting outwards. A belt contact patch is often a
|
||||
// narrow, broken-up strip, and the offset rings of two islands less than
|
||||
// 2 x brim_width apart merge and fill the space between them - space that lies
|
||||
// UNDER the part, which is not what "outer brim" means. Closing also swallows
|
||||
// holes in the patch for the same reason. Concavity-filling only, so an apron or
|
||||
// any other outward protrusion is untouched.
|
||||
ExPolygons envelope = brim_width > 0 ? closing_ex(inner, float(brim_width)) : inner;
|
||||
|
||||
ExPolygons base = envelope;
|
||||
if (leading > 0) {
|
||||
// Sweep downhill from the gapped keep-out, so the apron is contiguous with
|
||||
// the ring instead of starting inside the gap.
|
||||
const Point t = frame.from_axis == 0 ?
|
||||
Point(frame.downhill_sign() * leading, 0) :
|
||||
Point(0, frame.downhill_sign() * leading);
|
||||
base = union_ex(base, sweep_ex(envelope, t));
|
||||
}
|
||||
if (lateral > 0) {
|
||||
// Across the belt, both ways. Swept from `base` so the apron is widened
|
||||
// too, and in the flattened frame the cross-belt axis is unscaled, so this
|
||||
// distance is already a true on-belt distance.
|
||||
const Point t = frame.from_axis == 0 ? Point(0, lateral) : Point(lateral, 0);
|
||||
ExPolygons widened = union_ex(sweep_ex(base, t), sweep_ex(base, Point(-t.x(), -t.y())));
|
||||
base = union_ex(base, to_polygons(widened));
|
||||
}
|
||||
ExPolygons outer = offset_ex(base, float(brim_width), jtRound, SCALED_RESOLUTION);
|
||||
expolygons_append(out, diff_ex(outer, envelope));
|
||||
}
|
||||
|
||||
if (has_inner) {
|
||||
// Holes reversed so a negative offset grows inward, mirroring Brim.cpp.
|
||||
// No apron here: an apron growing into a hole interior is never useful.
|
||||
Polygons holes;
|
||||
for (const ExPolygon &ex : footprint_flat)
|
||||
polygons_append(holes, ex.holes);
|
||||
polygons_reverse(holes);
|
||||
if (! holes.empty()) {
|
||||
ExPolygons hole_inner = offset_ex(holes, - float(brim_width + object_gap));
|
||||
ExPolygons hole_outer = offset_ex(holes, - float(object_gap));
|
||||
expolygons_append(out, intersection_ex(diff_ex(hole_outer, hole_inner), holes));
|
||||
}
|
||||
}
|
||||
|
||||
return union_ex(out);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- line lattice
|
||||
|
||||
std::vector<coord_t> belt_brim_line_positions(coord_t u_lo,
|
||||
coord_t u_hi,
|
||||
coord_t pitch_u,
|
||||
coord_t u_anchor)
|
||||
{
|
||||
std::vector<coord_t> out;
|
||||
if (pitch_u <= 0 || u_hi <= u_lo)
|
||||
return out;
|
||||
|
||||
// Walk the lattice from just below u_lo. Integer arithmetic throughout, so
|
||||
// the half-open interval needs no epsilon: a point landing exactly on u_hi
|
||||
// belongs to the next band.
|
||||
int64_t k = int64_t(std::floor(double(u_lo - u_anchor) / double(pitch_u))) - 1;
|
||||
while (u_anchor + coord_t(k) * pitch_u < u_lo)
|
||||
++ k;
|
||||
for (;; ++ k) {
|
||||
const coord_t u = u_anchor + coord_t(k) * pitch_u;
|
||||
if (u >= u_hi)
|
||||
break;
|
||||
out.emplace_back(u);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- pipeline
|
||||
|
||||
// A band of the belt surface as an explicit box, clamped to `bounds` along the
|
||||
// shear axis. Deliberately not BeltFloorContext::surface_polygon(): those
|
||||
// half-planes span +-1000 mm, which is wasteful to clip against and dangerous to
|
||||
// feed through the flattening scale.
|
||||
static Polygon band_box(const BoundingBox &bounds, int from_axis, coordf_t u_lo, coordf_t u_hi)
|
||||
{
|
||||
coord_t lo = scale_(u_lo);
|
||||
coord_t hi = scale_(u_hi);
|
||||
const coord_t bmin = from_axis == 0 ? bounds.min.x() : bounds.min.y();
|
||||
const coord_t bmax = from_axis == 0 ? bounds.max.x() : bounds.max.y();
|
||||
lo = std::max(lo, bmin);
|
||||
hi = std::min(hi, bmax);
|
||||
Polygon poly;
|
||||
if (hi <= lo)
|
||||
return poly;
|
||||
if (from_axis == 0)
|
||||
poly.points = { Point(lo, bounds.min.y()), Point(hi, bounds.min.y()),
|
||||
Point(hi, bounds.max.y()), Point(lo, bounds.max.y()) };
|
||||
else
|
||||
poly.points = { Point(bounds.min.x(), lo), Point(bounds.max.x(), lo),
|
||||
Point(bounds.max.x(), hi), Point(bounds.min.x(), hi) };
|
||||
return poly;
|
||||
}
|
||||
|
||||
ExPolygons belt_brim_clip_leading_edge(const ExPolygons ®ion, const BeltBrimFrame &frame, coordf_t u_cut)
|
||||
{
|
||||
if (region.empty())
|
||||
return region;
|
||||
BoundingBox keep_bb = get_extents(region);
|
||||
keep_bb.offset(scale_(1.));
|
||||
const bool low_side = frame.downhill_sign() < 0; // downhill is -u
|
||||
const Polygon keep = band_box(keep_bb, frame.from_axis,
|
||||
low_side ? unscale<double>(frame.from_axis == 0 ? keep_bb.min.x() : keep_bb.min.y()) : u_cut,
|
||||
low_side ? u_cut : unscale<double>(frame.from_axis == 0 ? keep_bb.max.x() : keep_bb.max.y()));
|
||||
return keep.empty() ? ExPolygons{} : intersection_ex(region, Polygons{ keep });
|
||||
}
|
||||
|
||||
// Everything the per-band line generator needs, gathered once per object.
|
||||
struct BeltBrimContext
|
||||
{
|
||||
BeltFloorContext ctx;
|
||||
BeltBrimFrame frame;
|
||||
ExPolygons region; // brim region, object-local slicing XY
|
||||
BoundingBox region_bbox;
|
||||
Flow brim_flow;
|
||||
coord_t pitch_u = 0;
|
||||
coord_t u_anchor = 0;
|
||||
double in_plane_pitch = 0.; // mm
|
||||
};
|
||||
|
||||
// Emit the cross-belt brim lines that belong to the band [print_z - height, print_z].
|
||||
static void belt_brim_band_paths(const BeltBrimContext &bc,
|
||||
coordf_t print_z,
|
||||
coordf_t height,
|
||||
const Polygons &obstacles,
|
||||
ExtrusionEntityCollection &out,
|
||||
ExPolygons &areas_out)
|
||||
{
|
||||
coordf_t u_lo = bc.ctx.cutoff_u(print_z - height);
|
||||
coordf_t u_hi = bc.ctx.cutoff_u(print_z);
|
||||
if (u_lo > u_hi)
|
||||
std::swap(u_lo, u_hi);
|
||||
|
||||
// How wide this band is measured ON the belt, versus one nominal bead.
|
||||
const double band_in_plane = (u_hi - u_lo) * bc.frame.u_stretch();
|
||||
|
||||
// Fraction of the layer height at which a line sits above the belt. Toward the
|
||||
// downhill edge, so the sheet is reasonably thick while the nozzle stays clear of
|
||||
// the belt itself.
|
||||
static constexpr double BAND_CLEARANCE_FRACTION = 0.75;
|
||||
|
||||
std::vector<coord_t> us;
|
||||
double uniform_clearance = 0.; // 0 => derive per line from its own position
|
||||
double line_pitch = bc.in_plane_pitch;
|
||||
// One line also serves a band up to half a bead wider than the nominal pitch (a 0.3 mm
|
||||
// first layer at 45 degrees): its flow is matched to the band, so the bead is that much
|
||||
// wider. Two lattice lines in such a band would land almost on top of each other.
|
||||
if (band_in_plane <= 1.5 * bc.in_plane_pitch + EPSILON) {
|
||||
// Steep belt, which is the normal case: the band is narrower than one bead, so
|
||||
// exactly one line fits. Place it at a FIXED fraction of the band rather than
|
||||
// on a nominal-spacing lattice. On a lattice each line lands at an arbitrary
|
||||
// point in its band, the clearance sweeps [0, height] from band to band, and the
|
||||
// bead width therefore varies by 2x - visible as ragged, uneven brim lines.
|
||||
// Anchoring to the band makes the clearance identical everywhere, so every bead
|
||||
// is the same width.
|
||||
//
|
||||
// The spacing is then whatever the bands give (height / sin(tilt) on the belt)
|
||||
// rather than the nominal bead spacing, so the flow below is matched to THAT
|
||||
// pitch. Matched flow at the real pitch is what keeps the sheet uniform and
|
||||
// gap-free; using nominal flow at band spacing would over-feed it.
|
||||
us.push_back(scale_(bc.ctx.cutoff_u(print_z - BAND_CLEARANCE_FRACTION * height)));
|
||||
uniform_clearance = BAND_CLEARANCE_FRACTION * height;
|
||||
line_pitch = band_in_plane;
|
||||
} else {
|
||||
// Shallow belt: the band is wider than a bead, so it takes several lines and they
|
||||
// have to sit on the nominal lattice. Their clearances then differ, and so do
|
||||
// their widths - unavoidable here, but shallow belts are the rare case.
|
||||
us = belt_brim_line_positions(scale_(u_lo), scale_(u_hi), bc.pitch_u, bc.u_anchor);
|
||||
}
|
||||
if (us.empty())
|
||||
return;
|
||||
|
||||
const Polygons region_polys = to_polygons(bc.region);
|
||||
|
||||
// One lattice line at a time: the clearance - and therefore the extrusion
|
||||
// volume - is a property of the line's u, so the pieces of different lines
|
||||
// must not be pooled before the flow is resolved.
|
||||
// Overshoot the region so the clip, not the line's ends, decides the extent.
|
||||
const coord_t margin = coord_t(SCALED_EPSILON) + 1;
|
||||
coord_t u_prev = std::numeric_limits<coord_t>::min();
|
||||
for (coord_t u : us) {
|
||||
// Nozzle-to-belt clearance for this line. Constant along the line, because the
|
||||
// belt height depends only on the shear-axis coordinate. Band-anchored lines
|
||||
// share one clearance by construction; lattice lines (shallow belts, or a first
|
||||
// layer thick enough that the band is wider than a bead) each get their own.
|
||||
//
|
||||
// A lattice line can fall where the belt is only a hair below the band's print_z.
|
||||
// The bead there would be laid scraping the belt while its flow is sized for a
|
||||
// taller cell, so it is moved uphill to the same fraction of the band the
|
||||
// single-line case uses. (The clearance is along slice Z; the real gap under the
|
||||
// nozzle is clearance x cos(tilt), 0.53 h at 45 degrees for the 0.75 fraction.)
|
||||
double clearance = uniform_clearance;
|
||||
if (clearance <= 0.) {
|
||||
const Point probe = bc.frame.from_axis == 0 ? Point(u, 0) : Point(0, u);
|
||||
clearance = print_z - bc.ctx.floor_print_z(probe);
|
||||
if (clearance < BAND_CLEARANCE_FRACTION * height) {
|
||||
clearance = BAND_CLEARANCE_FRACTION * height;
|
||||
u = scale_(bc.ctx.cutoff_u(print_z - clearance));
|
||||
}
|
||||
clearance = std::min(clearance, height);
|
||||
}
|
||||
// A line moved uphill can land on, or almost on, its neighbour; two beads closer
|
||||
// than half a pitch would be laid into the same cell.
|
||||
if (u_prev != std::numeric_limits<coord_t>::min() && std::abs(u - u_prev) < bc.pitch_u / 2)
|
||||
continue;
|
||||
u_prev = u;
|
||||
|
||||
Polyline line;
|
||||
if (bc.frame.from_axis == 0)
|
||||
line.points = { Point(u, coord_t(bc.region_bbox.min.y() - margin)),
|
||||
Point(u, coord_t(bc.region_bbox.max.y() + margin)) };
|
||||
else
|
||||
line.points = { Point(coord_t(bc.region_bbox.min.x() - margin), u),
|
||||
Point(coord_t(bc.region_bbox.max.x() + margin), u) };
|
||||
|
||||
Polylines pieces = intersection_pl(Polylines{ line }, region_polys);
|
||||
if (! obstacles.empty())
|
||||
pieces = diff_pl(pieces, obstacles);
|
||||
if (pieces.empty())
|
||||
continue;
|
||||
|
||||
// with_cross_section, not with_height: it reaches the prescribed volume while
|
||||
// KEEPING the extrusion spacing, so the bead is sized to fill exactly one
|
||||
// pitch x clearance cell of the sheet.
|
||||
const Flow f = bc.brim_flow.with_cross_section(float(line_pitch * clearance));
|
||||
|
||||
// Footprint of these beads, for the first-layer convex hull and bbox.
|
||||
for (const Polygon &p : offset(pieces, 0.5f * float(f.scaled_width())))
|
||||
areas_out.emplace_back(ExPolygon(p));
|
||||
|
||||
extrusion_entities_append_paths(out.entities, chain_polylines(std::move(pieces)),
|
||||
erBrim, f.mm3_per_mm(), f.width(), float(clearance));
|
||||
}
|
||||
}
|
||||
|
||||
// Union of everything extruded at `print_z` that the brim must keep clear of, expressed
|
||||
// in `self`'s local slicing frame. Includes `self` itself: its slice at this Z can
|
||||
// overhang outside the belt footprint and land in the brim ring, which the flattened
|
||||
// brim_object_gap - a belt-plane separation - does not cover.
|
||||
//
|
||||
// SEQUENCING: this reads every object's layers and this object's own support layers.
|
||||
// Another object's support step shifts that object's layer Z into the object frame for
|
||||
// the duration of the run (PrintObject::_generate_support_material()), so the brims must
|
||||
// not overlap with the parallel support step: Print::process() generates them one object
|
||||
// after the other once that step is over (PrintObject::generate_belt_brim()), and an
|
||||
// object that arrives on or leaves the plate invalidates the other brim owners' support
|
||||
// step (PrintApply.cpp) so their brims are clipped against what is there now. Only this
|
||||
// object's supports are dodged; another object's support at the same Z is not.
|
||||
// `region_bbox` bounds the brim; anything outside it cannot clip a brim line, so whole
|
||||
// objects are skipped without materialising their polygons. On a typical plate the
|
||||
// objects do not overlap and every foreign object drops out here, which matters because
|
||||
// this runs once per band - hundreds of times per object.
|
||||
static Polygons belt_brim_obstacles(const Print &print, const PrintObject &self,
|
||||
const BoundingBox ®ion_bbox, coordf_t print_z, coordf_t tol)
|
||||
{
|
||||
const Point shift_self = self.instances().empty() ? Point(0, 0)
|
||||
: self.instances().front().shift_without_plate_offset();
|
||||
Polygons out;
|
||||
for (const PrintObject *o : print.objects()) {
|
||||
const bool is_self = (o == &self);
|
||||
for (const PrintInstance &inst : o->instances()) {
|
||||
const Point delta = inst.shift_without_plate_offset() - shift_self;
|
||||
if (const Layer *l = o->get_layer_at_printz(print_z, tol)) {
|
||||
BoundingBox lb = get_extents(l->lslices);
|
||||
lb.translate(delta.x(), delta.y());
|
||||
if (lb.overlap(region_bbox)) {
|
||||
Polygons ps = to_polygons(l->lslices);
|
||||
for (Polygon &p : ps)
|
||||
p.translate(delta);
|
||||
polygons_append(out, std::move(ps));
|
||||
}
|
||||
}
|
||||
if (! is_self)
|
||||
continue;
|
||||
if (const SupportLayer *sl = o->get_support_layer_at_printz(print_z, tol)) {
|
||||
Polygons ps = sl->support_fills.polygons_covered_by_spacing();
|
||||
for (Polygon &p : ps)
|
||||
p.translate(delta);
|
||||
polygons_append(out, std::move(ps));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (out.size() < 2)
|
||||
return out; // union_() of 0 or 1 polygons is pure overhead
|
||||
return union_(out);
|
||||
}
|
||||
|
||||
void make_belt_brim(PrintObject &object)
|
||||
{
|
||||
object.clear_belt_brim();
|
||||
if (! object.has_belt_brim())
|
||||
return;
|
||||
|
||||
const Print &print = *object.print();
|
||||
BeltBrimContext bc;
|
||||
if (! bc.ctx.init(object.slicing_parameters(), print.config()))
|
||||
return;
|
||||
bc.frame = BeltBrimFrame{ bc.ctx.shear_factor(), bc.ctx.from_axis() };
|
||||
|
||||
const size_t nlayers = object.layers().size();
|
||||
if (nlayers == 0)
|
||||
return;
|
||||
|
||||
// 1. Belt footprint: the union of each layer's slice clipped to that layer's
|
||||
// own contact band. This is the object's bottom face, which on a belt is
|
||||
// spread over every layer instead of sitting in layer 0.
|
||||
ExPolygons footprint_acc;
|
||||
// The first layer that touches the belt: where the leading-edge brim is cut.
|
||||
const Layer *first_contact = nullptr;
|
||||
for (size_t i = 0; i < nlayers; ++ i) {
|
||||
const Layer &layer = *object.layers()[i];
|
||||
if (layer.lslices.empty())
|
||||
continue;
|
||||
// print_z - height, not the previous layer's print_z: variable layer
|
||||
// heights make the latter wrong.
|
||||
coordf_t u_lo = bc.ctx.cutoff_u(layer.print_z - layer.height);
|
||||
coordf_t u_hi = bc.ctx.cutoff_u(layer.print_z);
|
||||
if (u_lo > u_hi)
|
||||
std::swap(u_lo, u_hi);
|
||||
BoundingBox bb = get_extents(layer.lslices);
|
||||
bb.offset(scale_(1.));
|
||||
const Polygon band = band_box(bb, bc.frame.from_axis, u_lo, u_hi);
|
||||
if (band.empty())
|
||||
continue;
|
||||
ExPolygons contact = intersection_ex(layer.lslices, Polygons{ band });
|
||||
if (contact.empty())
|
||||
continue;
|
||||
if (first_contact == nullptr)
|
||||
first_contact = &layer;
|
||||
expolygons_append(footprint_acc, std::move(contact));
|
||||
}
|
||||
const ExPolygons footprint = union_ex(footprint_acc);
|
||||
if (footprint.empty())
|
||||
return;
|
||||
|
||||
// 2. Brim region, offset in the flattened (true on-belt) metric.
|
||||
const PrintObjectConfig &cfg = object.config();
|
||||
bc.brim_flow = print.brim_flow();
|
||||
const double flow_w = bc.brim_flow.scaled_spacing() * SCALING_FACTOR;
|
||||
// Quantize to an even number of lines, as the plate brim does.
|
||||
const coord_t width = scale_(std::floor(cfg.brim_width.value / flow_w / 2) * flow_w * 2);
|
||||
const coord_t leading = scale_(cfg.leading_brim_length.value);
|
||||
const coord_t lateral = scale_(cfg.extra_brim_width.value);
|
||||
const coord_t gap = scale_(cfg.brim_object_gap.value);
|
||||
|
||||
// Belt printers collapse Auto / Mouse ear / Painted to outer-only: the auto width
|
||||
// heuristic and flat ear discs have no meaning on a tilted plane. Leading-edge-only
|
||||
// is an outer brim too; it is narrowed down to the first contact below.
|
||||
const BrimType bt = cfg.brim_type.value;
|
||||
const bool has_outer = bt == btOuterOnly || bt == btOuterAndInner
|
||||
|| bt == btAutoBrim || bt == btEar || bt == btPainted
|
||||
|| bt == btLeadingEdgeOnly;
|
||||
const bool has_inner = bt == btInnerOnly || bt == btOuterAndInner;
|
||||
|
||||
bc.region = belt_unflatten(
|
||||
belt_brim_region(belt_flatten(footprint, bc.frame), has_outer, has_inner,
|
||||
width, gap, leading, lateral, bc.frame),
|
||||
bc.frame);
|
||||
|
||||
if (bt == btLeadingEdgeOnly && first_contact != nullptr && ! bc.region.empty())
|
||||
// The cut is the uphill edge of the first contact's band: everything past it
|
||||
// belongs to later contacts. The first contact is the first layer that touches
|
||||
// the belt (step 1), neither layers().front(), an empty lead-in layer, nor the
|
||||
// first layer with geometry, which is an overhang's tip when the part overhangs
|
||||
// its leading end: both lie ahead of the part.
|
||||
bc.region = belt_brim_clip_leading_edge(bc.region, bc.frame, bc.ctx.cutoff_u(first_contact->print_z));
|
||||
|
||||
if (bc.region.empty())
|
||||
return;
|
||||
bc.region_bbox = get_extents(bc.region);
|
||||
|
||||
// 3. Line lattice. Fixed pitch in the flattened metric, anchored at the
|
||||
// footprint's leading-most edge so lines stay collinear across
|
||||
// disconnected islands and across the apron prologue.
|
||||
bc.pitch_u = std::max<coord_t>(1, coord_t(bc.brim_flow.scaled_spacing() * bc.frame.cos_tilt()));
|
||||
bc.in_plane_pitch = unscale<double>(bc.pitch_u) * bc.frame.u_stretch();
|
||||
{
|
||||
const BoundingBox fbb = get_extents(footprint);
|
||||
const bool low_side = bc.frame.shear > 0.;
|
||||
bc.u_anchor = bc.frame.from_axis == 0 ? (low_side ? fbb.min.x() : fbb.max.x())
|
||||
: (low_side ? fbb.min.y() : fbb.max.y());
|
||||
}
|
||||
|
||||
// 4. Bands coincident with an object layer.
|
||||
std::vector<ExtrusionEntityCollection> by_layer(nlayers);
|
||||
std::vector<ExPolygons> areas_by_layer(nlayers);
|
||||
for (size_t i = 0; i < nlayers; ++ i) {
|
||||
const Layer &layer = *object.layers()[i];
|
||||
const Polygons obstacles = belt_brim_obstacles(print, object, bc.region_bbox, layer.print_z, 0.5 * layer.height);
|
||||
belt_brim_band_paths(bc, layer.print_z, layer.height, obstacles, by_layer[i], areas_by_layer[i]);
|
||||
}
|
||||
|
||||
// 5. Apron prologue: the part of the region downhill of the object's first
|
||||
// layer, which has no object layer to ride on.
|
||||
std::vector<BeltBrimBand> prologue;
|
||||
{
|
||||
const Layer &first = *object.layers().front();
|
||||
const coordf_t h = first.height;
|
||||
const bool low_side = bc.frame.shear > 0.;
|
||||
const coord_t u_lead_s = bc.frame.from_axis == 0
|
||||
? (low_side ? bc.region_bbox.min.x() : bc.region_bbox.max.x())
|
||||
: (low_side ? bc.region_bbox.min.y() : bc.region_bbox.max.y());
|
||||
const coordf_t u_lead = unscale<double>(u_lead_s);
|
||||
// print_z at which the belt surface crosses the region's leading edge.
|
||||
const coordf_t z_lead = bc.ctx.shear_factor() * u_lead
|
||||
+ bc.ctx.floor_offset() + bc.ctx.z_shift();
|
||||
if (h > EPSILON)
|
||||
for (coordf_t z = first.print_z - h; z > z_lead - h; z -= h) {
|
||||
const Polygons obstacles = belt_brim_obstacles(print, object, bc.region_bbox, z, 0.5 * h);
|
||||
BeltBrimBand band;
|
||||
band.print_z = z;
|
||||
band.height = h;
|
||||
belt_brim_band_paths(bc, z, h, obstacles, band.fills, band.areas);
|
||||
if (! band.fills.empty())
|
||||
prologue.emplace_back(std::move(band));
|
||||
}
|
||||
// Lowest Z first, so collect_layers_to_print sees them in print order.
|
||||
std::reverse(prologue.begin(), prologue.end());
|
||||
}
|
||||
|
||||
object.set_belt_brim(std::move(by_layer), std::move(areas_by_layer), std::move(prologue));
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,178 @@
|
||||
#ifndef slic3r_BeltBrim_hpp_
|
||||
#define slic3r_BeltBrim_hpp_
|
||||
|
||||
#include "ExPolygon.hpp"
|
||||
#include "ExtrusionEntityCollection.hpp"
|
||||
#include "Point.hpp"
|
||||
#include "Polyline.hpp"
|
||||
#include "libslic3r.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
|
||||
// Belt-printer brim geometry.
|
||||
//
|
||||
// A belt printer slices in a ROTATED frame, so the belt surface is not the
|
||||
// Z=0 bed plane but a tilted plane in slicing space:
|
||||
//
|
||||
// z_slicing(u) = shear * u + floor_offset + z_shift, u = X or Y
|
||||
//
|
||||
// where `shear == tan(tilt)` (SlicingParameters::belt_floor_shear_factor) and
|
||||
// the axis is selected by SlicingParameters::belt_floor_from_axis. See
|
||||
// Support/BeltFloorContext.hpp for the canonical accessors.
|
||||
//
|
||||
// Consequences that drive everything in this file:
|
||||
//
|
||||
// * A horizontal slicing layer touches the belt only along a narrow strip at
|
||||
// its leading edge, `layer_height / shear` wide (~0.2 mm at 45 degrees).
|
||||
// The object's belt footprint - its bottom face - is therefore spread over
|
||||
// every layer, not contained in layer 0.
|
||||
// * Distances measured in slicing XY are NOT on-belt distances: moving `du`
|
||||
// along the shear axis travels `du / cos(tilt)` across the belt. So brim
|
||||
// offsets have to be taken in a "flattened" space where the shear axis is
|
||||
// stretched by `1 / cos(tilt)`, then mapped back.
|
||||
// * Brim ahead of the part (downhill) lies at slicing Z BELOW the object's
|
||||
// first layer, because the object's layer 0 is precisely its leading
|
||||
// contact with the belt.
|
||||
//
|
||||
// Everything here is pure geometry on ExPolygons/Polylines so it can be unit
|
||||
// tested without a Print. Keep user-visible strings out of this file: it is
|
||||
// not listed in localization/i18n/list.txt.
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// Tilt window within which the BELT plane, not the bed plane, is the adhesion
|
||||
// surface. Below ~1 degree a belt is a flat bed as far as adhesion goes, and the
|
||||
// contact band would be layer_height/sin(tilt) - tens of millimetres - so the
|
||||
// ordinary plate brim is both correct and cheaper. Above ~85 degrees the whole
|
||||
// brim compresses into a sliver and is not worth generating.
|
||||
inline constexpr double BELT_BRIM_MIN_TILT_DEG = 1.;
|
||||
inline constexpr double BELT_BRIM_MAX_TILT_DEG = 85.;
|
||||
|
||||
// Description of the tilted belt plane, reduced to what the brim geometry needs.
|
||||
struct BeltBrimFrame
|
||||
{
|
||||
// tan(tilt). Sign selects which way is downhill.
|
||||
double shear = 0.;
|
||||
// 0 = X, 1 = Y. Matches BeltFloorContext::from_axis().
|
||||
int from_axis = 1;
|
||||
|
||||
// 1 / cos(tilt). Stretch factor that turns a projected distance along
|
||||
// `from_axis` into the true distance travelled across the belt.
|
||||
double u_stretch() const { return std::sqrt(1. + shear * shear); }
|
||||
// cos(tilt). The inverse mapping.
|
||||
double cos_tilt() const { return 1. / this->u_stretch(); }
|
||||
// Downhill is where the belt surface is lower, i.e. printed earlier, i.e.
|
||||
// the leading edge of the part. For shear > 0 that is -u.
|
||||
int downhill_sign() const { return shear > 0. ? -1 : +1; }
|
||||
};
|
||||
|
||||
// Scale only the `from_axis` component by `factor`, rounding to nearest.
|
||||
//
|
||||
// Deliberately not MultiPoint::scale(fx, fy) / ExPolygon::scale(fx, fy): those
|
||||
// truncate toward zero, which is asymmetric about the origin and loses up to a
|
||||
// full coordinate unit per vertex on every round trip.
|
||||
ExPolygons belt_scale_u(const ExPolygons &src, const BeltBrimFrame &frame, double factor);
|
||||
Polylines belt_scale_u(const Polylines &src, const BeltBrimFrame &frame, double factor);
|
||||
|
||||
// Into / out of the space where Euclidean offsets equal true on-belt distances.
|
||||
inline ExPolygons belt_flatten(const ExPolygons &src, const BeltBrimFrame &frame)
|
||||
{ return belt_scale_u(src, frame, frame.u_stretch()); }
|
||||
inline ExPolygons belt_unflatten(const ExPolygons &src, const BeltBrimFrame &frame)
|
||||
{ return belt_scale_u(src, frame, frame.cos_tilt()); }
|
||||
|
||||
// Minkowski sum of `src` with the segment [0, t]: the region swept by sliding
|
||||
// `src` along t. Used to grow the brim downhill for "extra brim width".
|
||||
//
|
||||
// Implemented as union_(P, P + t, {parallelogram per boundary edge}) over ALL
|
||||
// contours including holes, with every parallelogram forced counter-clockwise
|
||||
// so the non-zero fill rule closes holes narrower than t along the sweep
|
||||
// direction. A hole survives exactly when it is wider than |t| measured along
|
||||
// t - not when it is wider in its narrowest Euclidean direction.
|
||||
ExPolygons sweep_ex(const ExPolygons &src, const Point &t);
|
||||
|
||||
// "Leading edge only": keep the part of a brim region (unflattened, slicing XY)
|
||||
// at or downhill of the object's first contact with the belt, so the part is
|
||||
// supported as it lands and nothing is printed alongside it afterwards. `u_cut`
|
||||
// is the uphill edge of the first layer's contact band along `frame.from_axis`,
|
||||
// in mm (BeltFloorContext::cutoff_u of the first layer); downhill is the side
|
||||
// `frame.downhill_sign()` points to.
|
||||
ExPolygons belt_brim_clip_leading_edge(const ExPolygons ®ion, const BeltBrimFrame &frame, coordf_t u_cut);
|
||||
|
||||
// Brim region for one already-flattened belt footprint. All lengths are scaled
|
||||
// and measured in the flattened (true on-belt) metric.
|
||||
//
|
||||
// `has_outer` / `has_inner` are the resolved BrimType: belt printers collapse
|
||||
// Auto / Mouse ear / Painted to outer-only, so the caller does that mapping and
|
||||
// this function never needs PrintConfig.
|
||||
//
|
||||
// Two directional extras are applied to the footprint before the outer offset, so
|
||||
// each one buys reach in one direction only:
|
||||
//
|
||||
// `leading` (leading_brim_length) sweeps the footprint DOWNHILL along the belt,
|
||||
// so every leading-facing edge gains an apron ahead of it.
|
||||
// `lateral` (extra_brim_width) sweeps it BOTH WAYS across the belt, widening
|
||||
// the brim sideways without pushing it further ahead or behind.
|
||||
//
|
||||
// Neither is applied to the inner (hole) ring.
|
||||
ExPolygons belt_brim_region(const ExPolygons &footprint_flat,
|
||||
bool has_outer,
|
||||
bool has_inner,
|
||||
coord_t brim_width,
|
||||
coord_t object_gap,
|
||||
coord_t leading,
|
||||
coord_t lateral,
|
||||
const BeltBrimFrame &frame);
|
||||
|
||||
// Brim line positions for one layer band.
|
||||
//
|
||||
// Lines sit on a fixed lattice `u_anchor + k * pitch_u` so the on-belt spacing
|
||||
// between neighbouring brim lines is constant regardless of how the lattice
|
||||
// falls across layer bands. Snapping to band centres instead would quantise
|
||||
// the spacing to whole bands and under-deposit by ~35% at 45 degrees.
|
||||
//
|
||||
// The band is half-open, [u_lo, u_hi), so every lattice point belongs to
|
||||
// exactly one band: none duplicated at a boundary, none dropped. A band
|
||||
// narrower than the pitch simply yields nothing; a band much wider (shallow
|
||||
// tilt) yields several lines.
|
||||
std::vector<coord_t> belt_brim_line_positions(coord_t u_lo,
|
||||
coord_t u_hi,
|
||||
coord_t pitch_u,
|
||||
coord_t u_anchor);
|
||||
|
||||
// ---------------------------------------------------------------- pipeline
|
||||
|
||||
// One brim-only layer printed BEFORE the object's first layer, carrying the
|
||||
// apron that has to be stuck to the belt ahead of the part.
|
||||
//
|
||||
// Deliberately not a Layer subclass. A synthetic Layer would inherit id()
|
||||
// semantics that leak into initial-layer temperature selection, the spiral vase
|
||||
// probe, gradual interpolation, avoid-crossing-perimeters and cooling, all of
|
||||
// which key off Layer::id() == 0 or off a layer's regions. A plain record
|
||||
// carries only what the emitter needs.
|
||||
//
|
||||
// `height` is the LAYER height, used for the Z move and ordering metadata only.
|
||||
// Each extrusion path inside `fills` carries its own height, equal to that
|
||||
// line's nozzle-to-belt clearance, which varies across the band.
|
||||
struct BeltBrimBand
|
||||
{
|
||||
coordf_t print_z = 0.;
|
||||
coordf_t height = 0.;
|
||||
// erBrim paths in the object's local slicing frame, untranslated.
|
||||
ExtrusionEntityCollection fills;
|
||||
// Footprint of those paths, for the first-layer convex hull / bbox.
|
||||
ExPolygons areas;
|
||||
};
|
||||
|
||||
class PrintObject;
|
||||
|
||||
// Generate the belt brim for one object: fills its per-object-layer bands and
|
||||
// its apron prologue. No-op unless PrintObject::has_belt_brim().
|
||||
//
|
||||
// Runs inside posSupportMaterial rather than the brim step, because the prologue
|
||||
// print_z values must exist before ToolOrdering is built at psWipeTower.
|
||||
void make_belt_brim(PrintObject &object);
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // slic3r_BeltBrim_hpp_
|
||||
@@ -0,0 +1,50 @@
|
||||
#include "BeltGCode.hpp"
|
||||
#include "GCodeWriter.hpp"
|
||||
#include "GCode/BeltKinematics.hpp"
|
||||
#include "BeltTransform.hpp"
|
||||
#include "Print.hpp"
|
||||
#include "Point.hpp"
|
||||
#include "PrintConfig.hpp"
|
||||
#include "libslic3r.h"
|
||||
#include <cstdlib>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
void BeltGCode::init_belt_writer(Print &print)
|
||||
{
|
||||
// Axis remap and build volume max are set by base GCode after init_belt_writer
|
||||
// returns; set_kinematics() replays them, so install order does not matter.
|
||||
install_belt_kinematics(m_writer, print.config());
|
||||
m_writer.set_force_normal_lift(true);
|
||||
}
|
||||
|
||||
void BeltGCode::write_belt_header(GCodeOutputStream &file, const Print &print)
|
||||
{
|
||||
const auto &full_cfg = print.full_print_config();
|
||||
// Slicing rotation: the belt tilt (axis + angle) and the single source of truth
|
||||
// for the physical tilt the G-code viewer uses to enable belt view.
|
||||
file.write_format("; belt_slice_rotation = %s\n", full_cfg.opt_serialize("belt_slice_rotation").c_str());
|
||||
file.write_format("; belt_slice_rotation_angle = %.1f\n", print.config().belt_slice_rotation_angle.value);
|
||||
// Machine-frame transform: shear (cot) + scale (1/|sin|) derived from the belt
|
||||
// tilt angle (or belt_frame_tilt_angle when decoupled).
|
||||
file.write_format("; belt_frame_tilt_decouple = %d\n", print.config().belt_frame_tilt_decouple.value ? 1 : 0);
|
||||
file.write_format("; belt_frame_tilt_angle = %.1f\n", print.config().belt_frame_tilt_angle.value);
|
||||
}
|
||||
|
||||
void BeltGCode::on_set_origin(const PrintObject * /*obj*/, const Point & /*inst_shift*/)
|
||||
{
|
||||
// Matches the per-instance Z-offset added in PrintObjectSlice.cpp: transform
|
||||
// the origin through the belt pipeline so that back_transform(T * origin) =
|
||||
// origin (correct machine position). The back_transform applied during
|
||||
// G-code emission is the inverse of the forward transform.
|
||||
|
||||
// Adjust origin: transform through belt forward pipeline so that
|
||||
// the back-transform correctly recovers model-space positions.
|
||||
Transform3d T = BeltTransformPipeline::build_forward_transform(m_config);
|
||||
Vec2d cur_origin = this->origin();
|
||||
Vec3d origin3d(cur_origin.x(), cur_origin.y(), 0.);
|
||||
Vec3d adjusted = T.linear() * origin3d;
|
||||
this->set_origin(Vec2d(adjusted.x(), adjusted.y()));
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
#include "GCode.hpp"
|
||||
#include "Point.hpp"
|
||||
#include "Print.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// Belt-printer-specific GCode export.
|
||||
//
|
||||
// Inherits from GCode and overrides virtual hooks to:
|
||||
// - Install a BeltKinematics on the GCodeWriter
|
||||
// - Write belt configuration to the G-code header
|
||||
// - Adjust the origin for global pre-slice transforms when switching instances
|
||||
// (Arc fitting is disabled for belt printers by BeltKinematics::supports_arc_moves(),
|
||||
// which the base GCode::should_disable_arc_fitting() consults -- no override needed.)
|
||||
class BeltGCode : public GCode
|
||||
{
|
||||
protected:
|
||||
void init_belt_writer(Print &print) override;
|
||||
void write_belt_header(GCodeOutputStream &file, const Print &print) override;
|
||||
void on_set_origin(const PrintObject *obj, const Point &inst_shift) override;
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,433 @@
|
||||
// ORCA-Belt: backend of the belt purge tower (the belt replacement for the
|
||||
// classic wipe/prime tower).
|
||||
//
|
||||
// Kept in its own translation unit so the belt-purge logic stays out of the way
|
||||
// of unrelated upstream changes to Print.cpp / PrintObjectSlice.cpp and carries
|
||||
// no regression risk for normal printers: none of these methods do anything
|
||||
// unless the print is a belt printer with the belt purge tower enabled.
|
||||
//
|
||||
// Print::has_belt_purge_tower() - is the belt purge tower active?
|
||||
// Print::_align_belt_purge_layers() - snap the prism's layer grid onto the
|
||||
// printed objects' grid
|
||||
// Print::_plan_belt_purge() - route filament-change purging into the
|
||||
// prism (flush-into-objects), no wipe tower
|
||||
// PrintObject::belt_shift_layer_grid() - shift a sliced layer grid
|
||||
// PrintObject::belt_truncate_layers_above() - cancel the prism past the last swap
|
||||
//
|
||||
// (Declarations live in Print.hpp alongside the rest of the Print interface.)
|
||||
|
||||
#include "Print.hpp"
|
||||
#include "PrintConfig.hpp"
|
||||
#include "Exception.hpp"
|
||||
#include "GCode/ToolOrdering.hpp"
|
||||
#include "Layer.hpp"
|
||||
#include "ExtrusionEntity.hpp"
|
||||
#include "ExtrusionEntityCollection.hpp"
|
||||
#include "I18N.hpp"
|
||||
#include "format.hpp"
|
||||
#include "LocalesUtils.hpp"
|
||||
#include "libslic3r.h"
|
||||
#include "BeltBrim.hpp"
|
||||
#include "PrintBase.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <cassert>
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// Belt purge prism: purging after filament changes is routed into a sliced
|
||||
// prism object via the flush-into-objects machinery instead of a wipe tower.
|
||||
bool Print::has_belt_purge_tower() const
|
||||
{
|
||||
// Its own purge-tower "type", gated by the belt-only enable_belt_purge_tower
|
||||
// option (not the classic enable_prime_tower).
|
||||
if (!(m_config.belt_printer.value
|
||||
&& m_config.enable_belt_purge_tower.value
|
||||
&& !m_config.spiral_mode.value
|
||||
&& m_config.filament_diameter.values.size() > 1))
|
||||
return false;
|
||||
|
||||
return std::any_of(m_objects.begin(), m_objects.end(), [](const PrintObject *object) {
|
||||
return object->config().belt_purge_tower_object.value;
|
||||
});
|
||||
}
|
||||
|
||||
// Belt mode: align ALL objects on the plate (the printed objects AND the purge
|
||||
// prism) onto one common layer grid, so the prism can absorb every toolchange.
|
||||
//
|
||||
// After belt slicing each object's layer print_z carries a per-object global z
|
||||
// offset (mesh-vertex-scan belt_z_shift + instance-Y-dependent terms), so
|
||||
// objects at different belt-Y positions get layer grids with DIFFERENT residues
|
||||
// (mod layer height). Purge marking looks absorbers up with
|
||||
// get_layer_at_printz(lt.print_z, EPSILON), so a toolchange on object B only
|
||||
// absorbs into the prism if the prism has a layer at B's print_z. Snapping only
|
||||
// the prism to one object therefore worked for a single (assembled) multi-color
|
||||
// object but failed with multiple separate objects — the prism could follow only
|
||||
// one grid, and toolchanges on the others went unabsorbed ("multiple layer
|
||||
// grids" warning).
|
||||
//
|
||||
// Fix: pick one reference grid (the tallest object) and shift every object onto
|
||||
// it. Each shift is at most half a layer height — a sub-100µm move along the
|
||||
// belt, the very same mechanism the per-object global_z_offset already uses, and
|
||||
// it keeps each object internally consistent (belt_shift_layer_grid moves the
|
||||
// object's layers, its support layers, and its belt floor together). Equal layer
|
||||
// height across objects is enforced by Print::validate(), so once residues match
|
||||
// every object steps on the same lattice {ref_offset + k*h} and every toolchange
|
||||
// layer coincides with a prism layer.
|
||||
void Print::_align_belt_purge_layers()
|
||||
{
|
||||
PrintObject *prism = nullptr;
|
||||
for (PrintObject *po : m_objects)
|
||||
if (po->config().belt_purge_tower_object.value && !po->layers().empty()) {
|
||||
prism = po;
|
||||
break;
|
||||
}
|
||||
if (prism == nullptr || prism->layers().empty())
|
||||
return;
|
||||
|
||||
const double h = prism->config().layer_height.value;
|
||||
if (h <= EPSILON)
|
||||
return;
|
||||
|
||||
// Grid residue of an object's layer grid: identical for all of an object's
|
||||
// layers above the first since they step by h.
|
||||
auto grid_offset = [h](const PrintObject *po) -> double {
|
||||
if (po->layers().empty())
|
||||
return 0.;
|
||||
const double z = po->layers().front()->print_z;
|
||||
return z - std::floor(z / h) * h; // in [0, h)
|
||||
};
|
||||
|
||||
// Reference grid: the tallest non-prism object (proxy for the object with
|
||||
// the most toolchange layers — minimizes how far the rest must move).
|
||||
const PrintObject *ref = nullptr;
|
||||
double ref_top = -std::numeric_limits<double>::max();
|
||||
for (const PrintObject *po : m_objects) {
|
||||
if (po->config().belt_purge_tower_object.value || po->layers().empty())
|
||||
continue;
|
||||
const double top = po->layers().back()->print_z;
|
||||
if (top > ref_top) {
|
||||
ref_top = top;
|
||||
ref = po;
|
||||
}
|
||||
}
|
||||
if (ref == nullptr)
|
||||
return;
|
||||
|
||||
const double ref_offset = grid_offset(ref);
|
||||
|
||||
// Snap every object (printed objects AND the prism) onto the reference grid.
|
||||
for (PrintObject *po : m_objects) {
|
||||
if (po->layers().empty())
|
||||
continue;
|
||||
double delta = ref_offset - grid_offset(po);
|
||||
if (delta > 0.5 * h)
|
||||
delta -= h;
|
||||
else if (delta <= -0.5 * h)
|
||||
delta += h;
|
||||
po->belt_shift_layer_grid(delta); // no-op for the reference object (delta ~ 0)
|
||||
}
|
||||
}
|
||||
|
||||
// Belt mode replacement for _make_wipe_tower(): plan filament-change purging
|
||||
// into the belt purge prism (and any other flush_into_* object) using the
|
||||
// flush-into-objects machinery, without generating classic wipe tower G-code.
|
||||
// The toolchange itself is emitted by GCode::set_extruder() via the
|
||||
// change_filament_gcode macro; the overrides marked here make the new
|
||||
// filament's first extrusions land in the purge prism.
|
||||
void Print::_plan_belt_purge()
|
||||
{
|
||||
m_wipe_tower_data.clear();
|
||||
|
||||
// psWipeTower may be invalidated without posSlice (for example after a
|
||||
// filament-map or tool-ordering change). Restore a prism shortened by the
|
||||
// previous plan so a newly higher toolchange can use its original layers.
|
||||
for (PrintObject *po : m_objects)
|
||||
if (po->config().belt_purge_tower_object.value)
|
||||
po->belt_undo_purge_plan();
|
||||
|
||||
// Must run before ToolOrdering is built: LayerTools merge per-object layer
|
||||
// print_z values, and the prism only absorbs purge where its (snapped)
|
||||
// layers coincide with the toolchange layers.
|
||||
this->_align_belt_purge_layers();
|
||||
|
||||
const unsigned int number_of_extruders = (unsigned int) m_config.filament_colour.values.size();
|
||||
|
||||
// No initial priming extrusions: there is no tower to prime on.
|
||||
m_wipe_tower_data.tool_ordering = ToolOrdering(*this, (unsigned int) -1, false);
|
||||
m_wipe_tower_data.tool_ordering.sort_and_build_data(*this, (unsigned int) -1, false);
|
||||
|
||||
if (m_wipe_tower_data.tool_ordering.empty() || m_wipe_tower_data.tool_ordering.last_extruder() == unsigned(-1))
|
||||
throw Slic3r::SlicingError("The print is empty. The model is not printable with current print settings.");
|
||||
|
||||
// Is there any filament change at all? Not ToolOrdering::has_wipe_tower(): that reads the
|
||||
// FIRST layer's flag, and on a belt the first layer may be a brim apron band, which carries
|
||||
// neither object nor support and so never gets the flag even when the print changes filament.
|
||||
{
|
||||
bool any_change = false;
|
||||
unsigned int cur = m_wipe_tower_data.tool_ordering.first_extruder();
|
||||
for (const auto < : m_wipe_tower_data.tool_ordering.layer_tools())
|
||||
for (const unsigned int e : lt.extruders)
|
||||
if (e != cur) { any_change = true; cur = e; }
|
||||
if (! any_change)
|
||||
return;
|
||||
}
|
||||
|
||||
this->throw_if_canceled();
|
||||
|
||||
// Flush volumes per filament pair, mirroring the generic wipe tower path:
|
||||
// full flush matrix for single extruder multi material with purging enabled,
|
||||
// plain prime volume otherwise.
|
||||
std::vector<float> flush_matrix(cast<float>(
|
||||
get_flush_volumes_matrix(m_config.flush_volumes_matrix.values, 0, m_config.nozzle_diameter.values.size())));
|
||||
std::vector<std::vector<float>> wipe_volumes;
|
||||
for (unsigned int i = 0; i < number_of_extruders; ++i)
|
||||
wipe_volumes.push_back(std::vector<float>(flush_matrix.begin() + i * number_of_extruders,
|
||||
flush_matrix.begin() + (i + 1) * number_of_extruders));
|
||||
const bool use_flush_matrix = m_config.purge_in_prime_tower && m_config.single_extruder_multi_material;
|
||||
const float flush_multiplier = (float) m_config.flush_multiplier.get_at(0);
|
||||
|
||||
// Cancel the purge prism early: pre-scan the tool ordering for the highest
|
||||
// print_z that actually has a toolchange, then drop the prism's layers above
|
||||
// it so the tower stops at the last color swap (saves filament/time). This
|
||||
// MUST happen before the marking loop below: ensure_perimeters_infills_order
|
||||
// force-overrides the prism's extrusions on every layer (it is a dedicated
|
||||
// flush object), so truncating afterwards would leave dangling overrides
|
||||
// pointing into deleted layers.
|
||||
{
|
||||
// The tool ordering covers the WHOLE print, and the prism is a printed
|
||||
// object in it. Left unbounded, the scan below sees the prism's own
|
||||
// toolchanges on layers above every model object -- the prism runs past
|
||||
// them by design (ramp/height compensation at the tilted ends) -- so
|
||||
// last_tc_z lands at the prism's own top and the truncation cancels
|
||||
// nothing. The tower ends up justifying its own existence.
|
||||
//
|
||||
// Nothing above the tallest printed object can require a color change,
|
||||
// so bound the scan there. On MCTEST5 that is 197 toolchanges spanning
|
||||
// z=154.00..193.20 with the tallest object topping out at 153.80, i.e.
|
||||
// 39.4 mm of tower that no swap ever needed.
|
||||
// Support layers count too: on a belt they can extend above the object's
|
||||
// own top, and a toolchange there is a real one.
|
||||
double obj_top_z = -1.;
|
||||
for (const PrintObject *po : m_objects) {
|
||||
if (po->config().belt_purge_tower_object.value)
|
||||
continue;
|
||||
if (!po->layers().empty())
|
||||
obj_top_z = std::max(obj_top_z, po->layers().back()->print_z);
|
||||
if (!po->support_layers().empty())
|
||||
obj_top_z = std::max(obj_top_z, po->support_layers().back()->print_z);
|
||||
}
|
||||
|
||||
double last_tc_z = -1.;
|
||||
unsigned int cur_ext = m_wipe_tower_data.tool_ordering.first_extruder();
|
||||
for (const auto < : m_wipe_tower_data.tool_ordering.layer_tools()) {
|
||||
// layer_tools() is ordered by print_z ascending.
|
||||
if (obj_top_z >= 0. && lt.print_z > obj_top_z + EPSILON)
|
||||
break;
|
||||
for (const unsigned int e : lt.extruders)
|
||||
if (e != cur_ext) { last_tc_z = lt.print_z; cur_ext = e; }
|
||||
}
|
||||
// Deliberately NOT cancelling the prism outright when no object toolchange
|
||||
// exists: belt_truncate_layers_above(0.) empties m_layers, and an object
|
||||
// with zero layers is not something the rest of the pipeline expects. The
|
||||
// GUI already declines to create a prism unless more than one filament is
|
||||
// in use, so this case is a stale prism, not a hot path -- leave it whole
|
||||
// rather than risk a zero-layer object.
|
||||
if (last_tc_z >= 0.)
|
||||
for (PrintObject *po : m_objects)
|
||||
if (po->config().belt_purge_tower_object.value && !po->layers().empty()) {
|
||||
po->belt_truncate_layers_above(last_tc_z);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// The prism only absorbs purge at toolchange layers whose print_z coincides
|
||||
// with one of its own layers.
|
||||
PrintObject *prism_po = nullptr;
|
||||
for (PrintObject *po : m_objects)
|
||||
if (po->config().belt_purge_tower_object.value && !po->layers().empty()) { prism_po = po; break; }
|
||||
|
||||
float total_leftover = 0.f;
|
||||
float worst_layer_leftover = 0.f;
|
||||
double worst_layer_z = 0.;
|
||||
|
||||
unsigned int current_extruder_id = m_wipe_tower_data.tool_ordering.first_extruder();
|
||||
for (auto &layer_tools : m_wipe_tower_data.tool_ordering.layer_tools()) {
|
||||
float layer_leftover = 0.f;
|
||||
for (const unsigned int extruder_id : layer_tools.extruders) {
|
||||
if (extruder_id == current_extruder_id)
|
||||
continue;
|
||||
float volume_to_wipe = use_flush_matrix ?
|
||||
wipe_volumes[current_extruder_id][extruder_id] * flush_multiplier :
|
||||
(float) m_config.prime_volume;
|
||||
float leftover = layer_tools.wiping_extrusions().mark_wiping_extrusions(*this, current_extruder_id, extruder_id,
|
||||
volume_to_wipe);
|
||||
layer_leftover += leftover;
|
||||
current_extruder_id = extruder_id;
|
||||
}
|
||||
|
||||
// Plastic saving: drop the prism's fills that no toolchange on this layer
|
||||
// claimed. At this point the prism's OVERRIDDEN fills are exactly the
|
||||
// purge; the rest would print as solid infill in the prism's own filament
|
||||
// for nothing -- which is the whole prism on a layer with no toolchange
|
||||
// (141 of 692 layers on MCTEST5 before the truncation fix). Perimeters are
|
||||
// left alone so the bar keeps a continuous wall along the belt.
|
||||
//
|
||||
// Non-destructive: the entities are stashed with their positions and put
|
||||
// back by belt_restore_dropped_fills() at the top of the next plan. An
|
||||
// earlier version deleted them outright, which broke replanning when a
|
||||
// later tool ordering needed what this one had not claimed -- that is why
|
||||
// it was removed rather than kept.
|
||||
if (prism_po != nullptr) {
|
||||
const auto &we = layer_tools.wiping_extrusions();
|
||||
prism_po->belt_drop_unclaimed_fills(
|
||||
prism_po->get_layer_at_printz(layer_tools.print_z, EPSILON),
|
||||
[&we, prism_po](const ExtrusionEntity *e) { return we.is_entity_overridden(e, prism_po, 0); });
|
||||
}
|
||||
|
||||
layer_tools.wiping_extrusions().ensure_perimeters_infills_order(*this);
|
||||
if (layer_leftover > 0.f) {
|
||||
total_leftover += layer_leftover;
|
||||
if (layer_leftover > worst_layer_leftover) {
|
||||
worst_layer_leftover = layer_leftover;
|
||||
worst_layer_z = layer_tools.print_z;
|
||||
}
|
||||
}
|
||||
this->throw_if_canceled();
|
||||
}
|
||||
|
||||
if (total_leftover > 1.f) {
|
||||
this->active_step_add_warning(
|
||||
PrintStateBase::WarningLevel::CRITICAL,
|
||||
Slic3r::format(_u8L("The belt purge tower cannot absorb the full purge volume: %1% mm³ in total could not "
|
||||
"be purged (worst layer: %2% mm³ at height %3%). The print may show color bleeding. "
|
||||
"Increase the belt purge tower width, or reduce flushing volumes."),
|
||||
int(std::ceil(total_leftover)), int(std::ceil(worst_layer_leftover)),
|
||||
Slic3r::float_to_string_decimal_point(worst_layer_z, 2)));
|
||||
}
|
||||
}
|
||||
|
||||
// Belt mode: shift the sliced layer grid by delta. Mirrors the global_z_offset
|
||||
// application in slice() — layer print_z and belt_floor_z_shift move together
|
||||
// so belt floor clipping stays consistent with the shifted grid. Used by
|
||||
// Print::_align_belt_purge_layers() to snap the purge prism onto the printed
|
||||
// objects' layer grid; |delta| <= half a layer height, i.e. a sub-layer shift
|
||||
// of the prism along the belt.
|
||||
void PrintObject::belt_shift_layer_grid(double delta)
|
||||
{
|
||||
if (std::abs(delta) < EPSILON)
|
||||
return;
|
||||
for (Layer *layer : m_layers)
|
||||
layer->print_z += delta;
|
||||
for (SupportLayer *layer : m_support_layers)
|
||||
layer->print_z += delta;
|
||||
// The brim's apron bands below the first layer carry their own print_z.
|
||||
for (BeltBrimBand &band : m_belt_brim_prologue)
|
||||
band.print_z += delta;
|
||||
m_slicing_params.belt_floor_z_shift += delta;
|
||||
// The grid stays shifted across a support-only or brim-only change (posSlice does
|
||||
// not rerun), so everything slice() derived from it has to follow: the cached floor
|
||||
// that update_slicing_parameters() restores, and the global offset the organic
|
||||
// support layers and the adaptive infill octree are placed with. Left alone, the
|
||||
// next alignment finds a delta of 0 and the floor and the supports sit up to half
|
||||
// a layer off the grid, unlike a fresh slice.
|
||||
if (m_belt_floor_z_shift_cache_valid)
|
||||
m_belt_floor_z_shift_cached += delta;
|
||||
m_belt_global_z_offset += delta;
|
||||
}
|
||||
|
||||
// Belt mode: drop layers strictly above z (used to cancel the purge prism early
|
||||
// once there are no more toolchanges above z, so the tower stops at the last
|
||||
// color swap instead of wasting filament up the rest of the belt). Each layer's
|
||||
// cross-section is already sliced, so removing upper layers does not affect the
|
||||
// last toolchange's coverage. Deletes the Layer objects and clears the new top
|
||||
// layer's upper-layer link. Returns the number of layers removed.
|
||||
size_t PrintObject::belt_truncate_layers_above(coordf_t z)
|
||||
{
|
||||
// A repeated plan always starts from the restored full layer set.
|
||||
assert(m_belt_truncated_layers.empty());
|
||||
size_t keep = m_layers.size();
|
||||
while (keep > 0 && m_layers[keep - 1]->print_z > z + EPSILON)
|
||||
--keep;
|
||||
if (keep >= m_layers.size())
|
||||
return 0;
|
||||
const size_t removed = m_layers.size() - keep;
|
||||
m_belt_truncated_layers.assign(m_layers.begin() + keep, m_layers.end());
|
||||
m_layers.resize(keep);
|
||||
if (!m_layers.empty())
|
||||
m_layers.back()->upper_layer = nullptr;
|
||||
return removed;
|
||||
}
|
||||
|
||||
// Plastic saving on the purge prism: keep only the fills a toolchange claimed.
|
||||
//
|
||||
// Called per layer from _plan_belt_purge(), after the real-purge marking and
|
||||
// BEFORE ensure_perimeters_infills_order() -- that pass force-overrides every
|
||||
// remaining fill on the prism (it is a dedicated flush object), so afterwards
|
||||
// everything looks claimed and nothing could be distinguished.
|
||||
size_t PrintObject::belt_drop_unclaimed_fills(Layer *layer, const std::function<bool(const ExtrusionEntity*)> &claimed)
|
||||
{
|
||||
if (layer == nullptr)
|
||||
return 0;
|
||||
size_t dropped = 0;
|
||||
for (size_t ri = 0; ri < layer->regions().size(); ++ri) {
|
||||
LayerRegion *lr = layer->get_region(ri);
|
||||
auto &ents = lr->fills.entities;
|
||||
ExtrusionEntitiesPtr keep;
|
||||
keep.reserve(ents.size());
|
||||
for (size_t i = 0; i < ents.size(); ++i) {
|
||||
if (claimed(ents[i])) {
|
||||
keep.emplace_back(ents[i]);
|
||||
} else {
|
||||
// Stash with its original index so the restore is exact.
|
||||
m_belt_dropped_fills.push_back(BeltDroppedFill{ layer, ri, i, ents[i] });
|
||||
++dropped;
|
||||
}
|
||||
}
|
||||
ents = std::move(keep);
|
||||
}
|
||||
return dropped;
|
||||
}
|
||||
|
||||
void PrintObject::belt_restore_dropped_fills()
|
||||
{
|
||||
if (m_belt_dropped_fills.empty())
|
||||
return;
|
||||
// Ascending index per (layer, region): inserting in that order lands every
|
||||
// entity back at its original position, because each insertion shifts only
|
||||
// the entries after it, which are themselves still to be inserted.
|
||||
std::stable_sort(m_belt_dropped_fills.begin(), m_belt_dropped_fills.end(),
|
||||
[](const BeltDroppedFill &a, const BeltDroppedFill &b) {
|
||||
if (a.layer != b.layer) return a.layer < b.layer;
|
||||
if (a.region_idx != b.region_idx) return a.region_idx < b.region_idx;
|
||||
return a.index < b.index;
|
||||
});
|
||||
for (const BeltDroppedFill &d : m_belt_dropped_fills) {
|
||||
auto &ents = d.layer->get_region(d.region_idx)->fills.entities;
|
||||
ents.insert(ents.begin() + std::min(d.index, ents.size()), d.entity);
|
||||
}
|
||||
m_belt_dropped_fills.clear();
|
||||
}
|
||||
|
||||
void PrintObject::belt_restore_truncated_layers()
|
||||
{
|
||||
if (m_belt_truncated_layers.empty())
|
||||
return;
|
||||
|
||||
m_layers.insert(m_layers.end(), m_belt_truncated_layers.begin(), m_belt_truncated_layers.end());
|
||||
m_belt_truncated_layers.clear();
|
||||
for (size_t i = 0; i < m_layers.size(); ++i) {
|
||||
m_layers[i]->lower_layer = i == 0 ? nullptr : m_layers[i - 1];
|
||||
m_layers[i]->upper_layer = i + 1 < m_layers.size() ? m_layers[i + 1] : nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,82 @@
|
||||
#include "BeltSliceStrategy.hpp"
|
||||
#include "Model.hpp"
|
||||
#include "BeltTransform.hpp"
|
||||
#include "Point.hpp"
|
||||
#include "PrintConfig.hpp"
|
||||
|
||||
#include <limits>
|
||||
#include <algorithm>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
void BeltSliceStrategy::apply_preslice_transforms(Transform3d &trafo,
|
||||
const PrintConfig &config,
|
||||
const ModelVolumePtrs &model_volumes,
|
||||
double *out_belt_min_z)
|
||||
{
|
||||
// 1. Belt rotation — the sole mesh-side belt transform (matching
|
||||
// BeltTransformPipeline::build_forward_transform). Only active in
|
||||
// belt-printer mode.
|
||||
bool has_rotation = false;
|
||||
if (config.belt_printer.value) {
|
||||
const Matrix3d rot = BeltTransformPipeline::build_rotation_matrix(config, &has_rotation);
|
||||
if (has_rotation) {
|
||||
Transform3d belt_xform = Transform3d::Identity();
|
||||
belt_xform.linear() = rot;
|
||||
trafo = belt_xform * trafo;
|
||||
}
|
||||
}
|
||||
|
||||
if (!has_rotation)
|
||||
return;
|
||||
|
||||
// 2. Z-shift — detect if the mesh clips below the build plate after the
|
||||
// transforms and lift it. Each mesh vertex must be brought into object space
|
||||
// via mv->get_matrix() before applying the full trafo (which is in object
|
||||
// space). Missing this on assemblies (where per-volume get_matrix() positions
|
||||
// each volume within the object) would compute min_z against mesh-local vertex
|
||||
// coordinates rather than object-space coordinates, so volumes translated along
|
||||
// the slicer's Z axis would be silently excluded from the bound check.
|
||||
|
||||
//
|
||||
// The lift is measured to the lowest point of the SUPPORT region, not of the
|
||||
// mesh: the belt floor (z = shear * u in this rotated frame, u the from-axis
|
||||
// coordinate) runs below every vertex, and under the leading end of an
|
||||
// overhang it lies below the lowest vertex by up to the overhang's length
|
||||
// times the shear. Supports have to reach that floor, and every support
|
||||
// generator works in layers at z >= 0, so z = 0 has to be the lowest floor
|
||||
// point under the footprint. The layers between it and the first vertex
|
||||
// come out empty, which belt slicing already tolerates (the bottom corner
|
||||
// of a tilted part is a point). Vertices on the belt have z == floor, so
|
||||
// for a part resting on the belt this is simply the floor at its leading
|
||||
// extreme, less the frame margin (see BeltTransformPipeline::frame_margin).
|
||||
BeltTransformPipeline::BeltFloorParams floor;
|
||||
const bool has_floor = BeltTransformPipeline::floor_shear(config, floor);
|
||||
double min_z = std::numeric_limits<double>::max();
|
||||
for (const ModelVolume *mv : model_volumes) {
|
||||
if (!mv->is_model_part()) continue;
|
||||
Transform3d vol_trafo = trafo * mv->get_matrix();
|
||||
const auto &its = mv->mesh().its;
|
||||
for (const stl_vertex &v : its.vertices) {
|
||||
Vec3d vm = v.cast<double>();
|
||||
Vec3d pt = vol_trafo * vm;
|
||||
min_z = std::min(min_z, pt.z());
|
||||
if (has_floor)
|
||||
min_z = std::min(min_z, floor.shear_factor * (floor.from_axis == 0 ? pt.x() : pt.y()));
|
||||
}
|
||||
}
|
||||
if (has_floor && min_z != std::numeric_limits<double>::max())
|
||||
min_z -= BeltTransformPipeline::frame_margin(floor);
|
||||
const double z_shift_val = (min_z < 0. && min_z != std::numeric_limits<double>::max()) ? -min_z : 0.;
|
||||
if (z_shift_val > 0.) {
|
||||
Transform3d z_shift = Transform3d::Identity();
|
||||
z_shift.matrix()(2, 3) = z_shift_val;
|
||||
trafo = z_shift * trafo;
|
||||
}
|
||||
// out_belt_min_z is only meaningful in belt mode.
|
||||
if (out_belt_min_z && config.belt_printer.value) {
|
||||
*out_belt_min_z = (min_z != std::numeric_limits<double>::max()) ? min_z : 0.;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
|
||||
#include "libslic3r.h"
|
||||
#include "Point.hpp"
|
||||
#include "BeltTransform.hpp"
|
||||
#include "PrintConfig.hpp"
|
||||
#include "Model.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// Belt printer pre-slice transform strategy.
|
||||
//
|
||||
// Composes, in order, the mesh transforms applied before slicing on a belt printer:
|
||||
// 1. Belt rotation (the sole mesh-side belt transform; shear & scale are a
|
||||
// g-code-side stage, see MachineFrameTransform)
|
||||
// 2. Per-object Z-shift that lifts the mesh so its slicing frame starts at the
|
||||
// belt below its footprint
|
||||
//
|
||||
// Isolates this belt-specific logic from the generic slicing pipeline in
|
||||
// PrintObjectSlice.cpp.
|
||||
class BeltSliceStrategy
|
||||
{
|
||||
public:
|
||||
// Apply the belt rotation + Z-shift to `trafo` in place. No-op when no belt
|
||||
// rotation is configured.
|
||||
//
|
||||
// out_belt_min_z (if non-null) receives the minimum mesh Z after the transforms.
|
||||
static void apply_preslice_transforms(Transform3d &trafo,
|
||||
const PrintConfig &config,
|
||||
const ModelVolumePtrs &model_volumes,
|
||||
double *out_belt_min_z = nullptr);
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,153 @@
|
||||
#include "BeltTransform.hpp"
|
||||
#include "Model.hpp"
|
||||
#include "BoundingBox.hpp"
|
||||
#include "Config.hpp"
|
||||
#include "Geometry.hpp"
|
||||
#include "Point.hpp"
|
||||
#include "PrintConfig.hpp"
|
||||
#include "libslic3r.h"
|
||||
|
||||
#include <limits>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// ---- Matrix builders ------------------------------------------------------
|
||||
|
||||
Matrix3d BeltTransformPipeline::build_rotation_matrix(const PrintConfig &config, bool *has_rot_out)
|
||||
{
|
||||
BeltRotationAxis axis = config.belt_slice_rotation.value;
|
||||
double angle_deg = config.belt_slice_rotation_angle.value;
|
||||
bool active = axis != BeltRotationAxis::None && std::abs(angle_deg) > EPSILON;
|
||||
if (has_rot_out) *has_rot_out = active;
|
||||
if (!active)
|
||||
return Matrix3d::Identity();
|
||||
double angle_rad = Geometry::deg2rad(angle_deg);
|
||||
Vec3d unit_axis;
|
||||
switch (axis) {
|
||||
case BeltRotationAxis::X: unit_axis = Vec3d::UnitX(); break;
|
||||
case BeltRotationAxis::Y: unit_axis = Vec3d::UnitY(); break;
|
||||
case BeltRotationAxis::Z: unit_axis = Vec3d::UnitZ(); break;
|
||||
default: return Matrix3d::Identity();
|
||||
}
|
||||
return Eigen::AngleAxisd(angle_rad, unit_axis).toRotationMatrix();
|
||||
}
|
||||
|
||||
Transform3d BeltTransformPipeline::build_forward_transform(const PrintConfig &config)
|
||||
{
|
||||
// Mesh-side belt transform: the rotation. (Shear & scale are a g-code-side
|
||||
// stage, not part of the mesh transform.)
|
||||
Transform3d combined = Transform3d::Identity();
|
||||
combined.linear() = build_rotation_matrix(config);
|
||||
return combined;
|
||||
}
|
||||
|
||||
// ---- Belt floor parameters ------------------------------------------------
|
||||
|
||||
namespace {
|
||||
|
||||
// Belt floor in the rotated slicer frame: the image of z_machine = 0 under R.
|
||||
// R(+α, X): point (·, y, 0) → (·, cos α · y, sin α · y) ⇒ z = tan(α) · y_s
|
||||
// R(+α, Y): point (x, ·, 0) → (cos α · x, ·, -sin α · x) ⇒ z = -tan(α) · x_s
|
||||
// R(+α, Z): point (·, ·, 0) → (·, ·, 0); no tilt → no floor
|
||||
void belt_floor_shear(BeltRotationAxis rot_axis, double angle_rad, BeltTransformPipeline::BeltFloorParams &out)
|
||||
{
|
||||
double sin_a = std::sin(angle_rad), cos_a = std::cos(angle_rad);
|
||||
switch (rot_axis) {
|
||||
case BeltRotationAxis::X:
|
||||
out.shear_factor = (std::abs(cos_a) > EPSILON) ? sin_a / cos_a : 0.;
|
||||
out.from_axis = 1; // Y
|
||||
break;
|
||||
case BeltRotationAxis::Y:
|
||||
out.shear_factor = (std::abs(cos_a) > EPSILON) ? -sin_a / cos_a : 0.;
|
||||
out.from_axis = 0; // X
|
||||
break;
|
||||
case BeltRotationAxis::Z:
|
||||
default:
|
||||
out.shear_factor = 0.0;
|
||||
out.from_axis = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Z of the belt floor directly under a point of the rotated (unshifted) frame.
|
||||
inline double belt_floor_z(const BeltTransformPipeline::BeltFloorParams &fp, const Vec3d &pt)
|
||||
{
|
||||
return fp.shear_factor * (fp.from_axis == 0 ? pt.x() : pt.y());
|
||||
}
|
||||
|
||||
|
||||
BeltTransformPipeline::BeltHeightResult compute_belt_height_and_floor_impl(
|
||||
const PrintConfig &config, const BoundingBoxf3 &bb, double original_height)
|
||||
{
|
||||
BeltTransformPipeline::BeltHeightResult result;
|
||||
result.object_height = original_height;
|
||||
|
||||
// The mesh rotation (the sole mesh-side belt transform).
|
||||
const BeltRotationAxis rot_axis = config.belt_slice_rotation.value;
|
||||
const double rot_angle = config.belt_slice_rotation_angle.value;
|
||||
|
||||
bool has_rotation = rot_axis != BeltRotationAxis::None && std::abs(rot_angle) > EPSILON;
|
||||
if (!has_rotation)
|
||||
return result;
|
||||
|
||||
// Rotation path: sweep the 8 bbox corners through R to get the rotated height,
|
||||
// then derive the belt floor (the image of machine-Z = 0 under R).
|
||||
double angle_rad = Geometry::deg2rad(rot_angle);
|
||||
Vec3d unit_axis;
|
||||
switch (rot_axis) {
|
||||
case BeltRotationAxis::X: unit_axis = Vec3d::UnitX(); break;
|
||||
case BeltRotationAxis::Y: unit_axis = Vec3d::UnitY(); break;
|
||||
case BeltRotationAxis::Z: unit_axis = Vec3d::UnitZ(); break;
|
||||
default: unit_axis = Vec3d::UnitX(); break;
|
||||
}
|
||||
Matrix3d R = Eigen::AngleAxisd(angle_rad, unit_axis).toRotationMatrix();
|
||||
belt_floor_shear(rot_axis, angle_rad, result.floor_params);
|
||||
// The slicing frame starts at the lowest point of the support region: the
|
||||
// lowest belt-floor point under the footprint, not the lowest vertex. The
|
||||
// belt under the leading end of an overhang lies below every vertex of the
|
||||
// part, and supports have to be able to reach it (see
|
||||
// BeltSliceStrategy::apply_preslice_transforms for the exact vertex-scan
|
||||
// counterpart of this bbox estimate).
|
||||
double min_rz = std::numeric_limits<double>::max();
|
||||
double max_rz = std::numeric_limits<double>::lowest();
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
Vec3d c((i & 1) ? bb.max.x() : bb.min.x(),
|
||||
(i & 2) ? bb.max.y() : bb.min.y(),
|
||||
(i & 4) ? bb.max.z() : bb.min.z());
|
||||
Vec3d rc = R * c;
|
||||
double z = rc.z();
|
||||
min_rz = std::min(min_rz, z);
|
||||
max_rz = std::max(max_rz, z);
|
||||
min_rz = std::min(min_rz, belt_floor_z(result.floor_params, rc));
|
||||
}
|
||||
min_rz -= BeltTransformPipeline::frame_margin(result.floor_params);
|
||||
result.object_height = max_rz - min_rz;
|
||||
|
||||
result.floor_params.z_shift = bb.min.z() + ((min_rz < 0.) ? -min_rz : 0.);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
BeltTransformPipeline::BeltHeightResult BeltTransformPipeline::compute_belt_height_and_floor(
|
||||
const PrintConfig &config, const BoundingBoxf3 &bbox, double original_height)
|
||||
{
|
||||
return compute_belt_height_and_floor_impl(config, bbox, original_height);
|
||||
}
|
||||
|
||||
bool BeltTransformPipeline::floor_shear(const PrintConfig &config, BeltFloorParams &out)
|
||||
{
|
||||
out = BeltFloorParams{};
|
||||
const BeltRotationAxis rot_axis = config.belt_slice_rotation.value;
|
||||
const double rot_angle = config.belt_slice_rotation_angle.value;
|
||||
if (rot_axis == BeltRotationAxis::None || std::abs(rot_angle) <= EPSILON)
|
||||
return false;
|
||||
belt_floor_shear(rot_axis, Geometry::deg2rad(rot_angle), out);
|
||||
return std::abs(out.shear_factor) > EPSILON;
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,132 @@
|
||||
#pragma once
|
||||
|
||||
#include "libslic3r.h"
|
||||
#include "Point.hpp"
|
||||
#include "BoundingBox.hpp"
|
||||
#include "PrintConfig.hpp"
|
||||
#include "Geometry.hpp"
|
||||
#include "Config.hpp"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
class ModelObject;
|
||||
|
||||
// Shared belt-printer transform math.
|
||||
//
|
||||
// The pre-slice pipeline applied in PrintObjectSlice.cpp is:
|
||||
// trafo_out = z_shift * rotation * trafo_in
|
||||
//
|
||||
// Rotation is the sole mesh-side belt transform; shear & scale are applied
|
||||
// to the g-code instead (see MachineFrameTransform). This class provides the
|
||||
// building blocks so every call site uses the same implementation. z_shift is
|
||||
// object-dependent (computed from mesh vertex bounds) and is NOT included in
|
||||
// build_forward_transform(). The machine-frame shear/scale is derived directly
|
||||
// from the tilt angle in MachineFrameTransform and no longer lives here.
|
||||
//
|
||||
// Design note: this mesh-rotation approach replaced an earlier pre-shear
|
||||
// method (now removed). While that initial pre-shear method was instrumental
|
||||
// in getting belt printer slicing off the ground in the first place, its place is
|
||||
// in the past. A big thank you goes to the Unlayered3D team, who recommended
|
||||
// switching to a pre-slice rotation stage instead. Doing so keeps the slicing
|
||||
// operation isometric — no distortion of the sliced geometry — while the
|
||||
// non-orthogonal machine-axis compensation is confined to a g-code-side shear/scale
|
||||
// derived from the same tilt angle.
|
||||
//
|
||||
// This fixed a number of issues, including several issues noticed by hotcubcar
|
||||
// regarding adaptive infills not working, gyroid becoming anisotropic, and more
|
||||
// that were all mostly resolved as a result of the switch.
|
||||
//
|
||||
// This also means that the pre-slice rotation transform methodology can be used
|
||||
// more cleanly on non-belt printers.
|
||||
// - HarrierPigeon (Joseph Robertson)
|
||||
|
||||
class BeltTransformPipeline
|
||||
{
|
||||
public:
|
||||
// ---- Identity checks --------------------------------------------------
|
||||
|
||||
// Whether the G-code axis remap applies at all. The remap fields are only
|
||||
// offered in the belt printer group, so a value left in a profile must not
|
||||
// change a non-belt print: with belt mode off every belt-only key is a no-op.
|
||||
// This is the one place to widen if a non-belt use ever needs them.
|
||||
static bool axis_remap_enabled(const PrintConfig &config) { return config.belt_printer.value; }
|
||||
|
||||
static bool has_rotation(const PrintConfig &config)
|
||||
{
|
||||
return config.belt_slice_rotation.value != BeltRotationAxis::None &&
|
||||
std::abs(config.belt_slice_rotation_angle.value) > EPSILON;
|
||||
}
|
||||
|
||||
// Physical belt tilt derived from the slicing rotation — the single source of
|
||||
// truth for bed rendering, support gravity tilt and the bed-exclusion
|
||||
// projection. Returns the tilt magnitude in degrees split onto the X and Y
|
||||
// build-plate tilt axes according to the rotation axis:
|
||||
// rotation about X → tilt_x = angle (gantry tilts in the YZ plane)
|
||||
// rotation about Y → tilt_y = angle (gantry tilts in the XZ plane)
|
||||
// rotation about Z / None → no tilt (in-plane spin doesn't tilt the belt)
|
||||
// The magnitude uses abs(angle) so a negative rotation still reports a positive
|
||||
// physical tilt.
|
||||
struct PhysicalTilt { double tilt_x_deg = 0.; double tilt_y_deg = 0.; };
|
||||
|
||||
static PhysicalTilt physical_tilt(BeltRotationAxis axis, double angle_deg)
|
||||
{
|
||||
PhysicalTilt t;
|
||||
double mag = std::abs(angle_deg);
|
||||
switch (axis) {
|
||||
case BeltRotationAxis::X: t.tilt_x_deg = mag; break;
|
||||
case BeltRotationAxis::Y: t.tilt_y_deg = mag; break;
|
||||
default: break; // Z / None: no physical tilt
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
// ---- Matrix builders --------------------------------------------------
|
||||
|
||||
// Build the 3x3 rotation matrix from belt_slice_rotation* config.
|
||||
// Returns Identity if rotation axis is None or angle is ~0.
|
||||
// Also sets has_rot_out if non-null.
|
||||
static Matrix3d build_rotation_matrix(const PrintConfig &config, bool *has_rot_out = nullptr);
|
||||
|
||||
// Forward transform (the rotation) — the mesh-side belt transform that
|
||||
// BeltSliceStrategy applies and BeltBackTransform inverts.
|
||||
// Does NOT include the per-object Z-shift.
|
||||
static Transform3d build_forward_transform(const PrintConfig &config);
|
||||
|
||||
// ---- Belt floor parameters --------------------------------------------
|
||||
|
||||
struct BeltFloorParams {
|
||||
double shear_factor = 0.0;
|
||||
int from_axis = 1;
|
||||
double z_shift = 0.0;
|
||||
};
|
||||
|
||||
// Shear factor and from-axis of the belt floor in the rotated slicer frame
|
||||
// (z_floor = shear_factor * u, u = the from-axis coordinate), for the
|
||||
// rotation the config selects. z_shift is left at 0. Returns false (and
|
||||
// zero shear) when the config has no tilt.
|
||||
static bool floor_shear(const PrintConfig &config, BeltFloorParams &out);
|
||||
|
||||
// How far below the lowest belt-floor point under the footprint the slicing
|
||||
// frame starts, in slicing Z. A support column meeting the belt is wider at
|
||||
// its base than at its tip, so under a leading overhang the base reaches ahead
|
||||
// of the part along the belt, and the layers that trim it to the belt plane
|
||||
// lie below that lowest point: 10 mm along the belt.
|
||||
static double frame_margin(const BeltFloorParams &fp) { return 10. * std::abs(fp.shear_factor); }
|
||||
|
||||
// Result of computing belt height + floor params.
|
||||
struct BeltHeightResult {
|
||||
double object_height; // Effective object height after shear/scale
|
||||
BeltFloorParams floor_params;
|
||||
};
|
||||
|
||||
// Compute effective object height and belt floor parameters from config
|
||||
// and the object's bounding box. original_height is the input height
|
||||
// (bb.size().z() or model_object.max_z()).
|
||||
static BeltHeightResult compute_belt_height_and_floor(
|
||||
const PrintConfig &config, const BoundingBoxf3 &bbox,
|
||||
double original_height);
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
+14
-1
@@ -474,7 +474,9 @@ static ExPolygons outer_inner_brim_area(const Print& print,
|
||||
const bool use_brim_ears = object->config().brim_type == btPainted;
|
||||
const bool use_inner_brim_ears = (use_auto_brim_ears || use_brim_ears) && !object->config().brim_ears_outer_only.value;
|
||||
const bool has_inner_brim = brim_type == btInnerOnly || brim_type == btOuterAndInner || use_inner_brim_ears;
|
||||
const bool has_outer_brim = brim_type == btOuterOnly || brim_type == btOuterAndInner || brim_type == btAutoBrim || use_auto_brim_ears || use_brim_ears;
|
||||
// btLeadingEdgeOnly is a belt-printer mode; on a flat bed there is no leading
|
||||
// edge, so it degrades to an ordinary outer brim rather than silently to none.
|
||||
const bool has_outer_brim = brim_type == btOuterOnly || brim_type == btOuterAndInner || brim_type == btAutoBrim || brim_type == btLeadingEdgeOnly || use_auto_brim_ears || use_brim_ears;
|
||||
coord_t ear_detection_length = scale_(object->config().brim_ears_detection_length.value);
|
||||
coordf_t brim_ears_max_angle = object->config().brim_ears_max_angle.value;
|
||||
//ORCA: Select brim base slices from EFC-compensated outline when enabled.
|
||||
@@ -889,6 +891,17 @@ void make_brim(const Print& print, PrintTryCancel try_cancel, Polygons& islands_
|
||||
std::vector<unsigned int>& printExtruders,
|
||||
std::map<ObjectInstanceID, ExPolygons>* objectBrimAreasByInstanceOut)
|
||||
{
|
||||
// Belt printers never use the flat plate brim.
|
||||
//
|
||||
// With a tilted belt the brim has to be laid onto the belt plane over many layers,
|
||||
// which BeltBrim.cpp does during posSupportMaterial. With an untilted belt this
|
||||
// could in principle fall through and produce an ordinary brim, but it would never
|
||||
// reach the G-code: the plate brim is emitted out of skirt_brim_groups(), which
|
||||
// _make_skirt() builds, and that returns early for every belt printer. Running the
|
||||
// generator anyway would just burn time on geometry nobody prints.
|
||||
if (print.config().belt_printer.value)
|
||||
return;
|
||||
|
||||
std::map<ObjectInstanceID, ExPolygons> brimAreaMap;
|
||||
Flow flow = print.brim_flow();
|
||||
ExPolygons islands_area_ex = outer_inner_brim_area(print,
|
||||
|
||||
@@ -84,7 +84,7 @@ public:
|
||||
indexed_triangle_set bounding_mesh(bool scale=true) const;
|
||||
|
||||
// Center of the print bed, unscaled.
|
||||
Vec2d bed_center() const { return to_2d(m_bboxf.center()); }
|
||||
Vec2d bed_center() const { return get_extents(m_bed_shape).center(); }
|
||||
// Convex hull of polygon(), scaled.
|
||||
const Polygon& convex_hull() const { return m_convex_hull; }
|
||||
// Smallest enclosing circle of polygon(), scaled.
|
||||
|
||||
@@ -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
|
||||
@@ -232,6 +241,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
|
||||
@@ -457,6 +474,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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;*/
|
||||
|
||||
@@ -1048,10 +1048,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);
|
||||
}
|
||||
|
||||
@@ -1402,7 +1402,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);
|
||||
|
||||
@@ -2124,7 +2124,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;
|
||||
@@ -2223,7 +2224,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;
|
||||
@@ -3908,11 +3910,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;
|
||||
}
|
||||
|
||||
@@ -5077,11 +5086,18 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
return true;
|
||||
}
|
||||
|
||||
void _BBS_3MF_Importer::_generate_current_object_list(std::vector<Component> &sub_objects, Id object_id, IdToCurrentObjectMap ¤t_objects)
|
||||
bool _BBS_3MF_Importer::_generate_current_object_list(std::vector<Component> &sub_objects, Id object_id, IdToCurrentObjectMap ¤t_objects)
|
||||
{
|
||||
// A cycle in the component graph would expand forever, and an acyclic graph can still expand
|
||||
// exponentially, so bound the number of component references queued. Checking before they are
|
||||
// queued bounds the work list itself, whatever the fan-out. A valid file over the budget is
|
||||
// rejected too, but the budget is way above the component references of any real object.
|
||||
static constexpr size_t max_components = 100000;
|
||||
|
||||
std::list<std::pair<Component, Transform3d>> id_list;
|
||||
id_list.push_back(std::make_pair(Component(object_id, Transform3d::Identity()), Transform3d::Identity()));
|
||||
|
||||
size_t num_components = 0;
|
||||
while (!id_list.empty())
|
||||
{
|
||||
auto current_item = id_list.front();
|
||||
@@ -5091,6 +5107,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));
|
||||
}
|
||||
@@ -5102,6 +5124,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)
|
||||
@@ -5208,6 +5231,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
|
||||
@@ -5721,11 +5749,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;
|
||||
}
|
||||
|
||||
|
||||
+633
-39
File diff suppressed because it is too large
Load Diff
+130
-8
@@ -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,8 +253,9 @@ 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);
|
||||
@@ -355,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)
|
||||
@@ -384,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) {}
|
||||
@@ -416,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,
|
||||
@@ -438,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,
|
||||
@@ -642,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;
|
||||
|
||||
@@ -781,7 +845,6 @@ 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;
|
||||
@@ -844,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
|
||||
@@ -861,6 +943,46 @@ private:
|
||||
// On the first printing layer. This flag triggers first layer speeds.
|
||||
//BBS
|
||||
bool on_first_layer() const { return m_layer != nullptr && m_layer->id() == 0 && abs(m_layer->bottom_z()) < EPSILON; }
|
||||
// Per-point first-layer test. On a belt printer the result depends on the
|
||||
// supplied slicing-frame point (its height above the belt); otherwise we
|
||||
// delegate to the legacy per-layer test. This is the entry point used by
|
||||
// per-path call sites in _extrude.
|
||||
bool on_first_layer(const Vec3d &point_slicing_mm) const {
|
||||
double h;
|
||||
if (this->belt_height_above_floor(point_slicing_mm, h))
|
||||
return h <= m_config.initial_layer_print_height.value + EPSILON;
|
||||
return on_first_layer();
|
||||
}
|
||||
// "Effective layer index" used to drive layer-count thresholds like
|
||||
// slow_down_layers. On a belt printer this is the height above the belt in
|
||||
// first_layer_band_mm() units; otherwise it is the legacy slicing layer index.
|
||||
int effective_layer_index_for_point(const Vec3d &point_slicing_mm) const {
|
||||
double h;
|
||||
if (this->belt_height_above_floor(point_slicing_mm, h)) {
|
||||
const double lh = this->first_layer_band_mm();
|
||||
return h <= 0. ? 0 : int(std::floor(h / lh));
|
||||
}
|
||||
return on_first_layer() ? 0 : layer_id();
|
||||
}
|
||||
|
||||
// Band thickness for the *effective layer index*: one first layer height, so
|
||||
// "the first N layers" means the same height above the belt as on a flat bed.
|
||||
double first_layer_band_mm() const {
|
||||
const double band = m_config.initial_layer_print_height.value;
|
||||
return band > 0. ? band : 0.2;
|
||||
}
|
||||
|
||||
// Height of a slicing-frame point above the belt surface, or false when this
|
||||
// is not a belt print.
|
||||
//
|
||||
// The belt surface is known exactly in the slicing frame from the slicing
|
||||
// parameters (belt_floor_shear_factor / _from_axis / _z_shift) -- the same
|
||||
// description the support generator uses, independent of every remap and
|
||||
// back-transform.
|
||||
bool belt_height_above_floor(const Vec3d &point_slicing_mm, double &height_mm) const;
|
||||
// 1 / 0 / -1: the object layer is entirely past the first-layer band above the
|
||||
// belt / reaches into it / the belt surface is not known for it.
|
||||
int belt_layer_past_first_layer_band(const Layer *object_layer) const;
|
||||
int layer_id() const {
|
||||
if (m_layer == nullptr)
|
||||
return -1;
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
#include "BeltBackTransform.hpp"
|
||||
#include "../BeltTransform.hpp"
|
||||
#include "../Point.hpp"
|
||||
#include "../PrintConfig.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
bool BeltBackTransform::init_from_config(const PrintConfig &config)
|
||||
{
|
||||
m_active = false;
|
||||
m_inverse = Transform3d::Identity();
|
||||
|
||||
if (!config.belt_printer.value)
|
||||
return false;
|
||||
|
||||
// Build the forward pipeline (the rotation) and store its inverse.
|
||||
Transform3d forward = BeltTransformPipeline::build_forward_transform(config);
|
||||
if (forward.isApprox(Transform3d::Identity()))
|
||||
return false;
|
||||
|
||||
m_inverse = forward.inverse();
|
||||
m_active = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
Vec3d BeltBackTransform::apply(const Vec3d &pos) const
|
||||
{
|
||||
if (!m_active)
|
||||
return pos;
|
||||
return m_inverse * pos;
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,38 @@
|
||||
#ifndef slic3r_BeltBackTransform_hpp_
|
||||
#define slic3r_BeltBackTransform_hpp_
|
||||
|
||||
#include "../libslic3r.h"
|
||||
#include "../Point.hpp"
|
||||
#include "../PrintConfig.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// Reverses the pre-slice rotation that PrintObjectSlice.cpp applies to belt
|
||||
// printer geometry, converting G-code coordinates from the sliced (rotated)
|
||||
// frame back to the machine's real coordinate space.
|
||||
//
|
||||
// Initialized once from PrintConfig, then applied per-point in
|
||||
// BeltKinematics::to_machine() before axis remapping.
|
||||
//
|
||||
// Active on belt printers with a non-identity pre-slice rotation.
|
||||
class BeltBackTransform
|
||||
{
|
||||
public:
|
||||
BeltBackTransform() = default;
|
||||
|
||||
// Initialize from belt printer config. Rebuilds the same pre-slice rotation
|
||||
// as PrintObjectSlice.cpp and precomputes the affine inverse. Returns true if a non-identity back-transform was computed.
|
||||
bool init_from_config(const PrintConfig &config);
|
||||
|
||||
// Apply the inverse transform to a point. Returns pos unchanged if
|
||||
// no back-transform is active.
|
||||
Vec3d apply(const Vec3d &pos) const;
|
||||
|
||||
private:
|
||||
bool m_active = false;
|
||||
Transform3d m_inverse = Transform3d::Identity();
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // slic3r_BeltBackTransform_hpp_
|
||||
@@ -0,0 +1,29 @@
|
||||
#include "BeltKinematics.hpp"
|
||||
#include "../BeltTransform.hpp"
|
||||
#include "../PrintConfig.hpp"
|
||||
#include "../GCodeWriter.hpp"
|
||||
#include "../Point.hpp"
|
||||
#include <memory>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
BeltKinematics::BeltKinematics(const PrintConfig &config, bool world_coordinates)
|
||||
: m_world_coordinates(world_coordinates)
|
||||
{
|
||||
m_back_transform.init_from_config(config);
|
||||
m_machine_frame.init_from_config(config);
|
||||
}
|
||||
|
||||
Vec3d BeltKinematics::to_machine(const Vec3d &p) const
|
||||
{
|
||||
const Vec3d after_back = m_world_coordinates ? p : m_back_transform.apply(p);
|
||||
const Vec3d after_remap = this->apply_axis_remap(after_back);
|
||||
return m_machine_frame.apply(after_remap);
|
||||
}
|
||||
|
||||
void install_belt_kinematics(GCodeWriter &writer, const PrintConfig &config, bool world_coordinates)
|
||||
{
|
||||
writer.set_kinematics(std::make_unique<BeltKinematics>(config, world_coordinates));
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,59 @@
|
||||
#ifndef slic3r_BeltKinematics_hpp_
|
||||
#define slic3r_BeltKinematics_hpp_
|
||||
|
||||
#include "MachineKinematics.hpp"
|
||||
#include "BeltBackTransform.hpp"
|
||||
#include "MachineFrameTransform.hpp"
|
||||
#include "../Point.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
class PrintConfig;
|
||||
class GCodeWriter;
|
||||
|
||||
// Belt-printer machine frame.
|
||||
//
|
||||
// Forward order, as applied per emitted point:
|
||||
// machine = MachineFrameTransform( axis_remap( BeltBackTransform( logical ) ) )
|
||||
//
|
||||
// i.e. the slicer->world back-transform runs FIRST and the machine-frame
|
||||
// shear/scale LAST, so the latter acts as a global linear transform on the
|
||||
// already-placed coordinates.
|
||||
//
|
||||
// world_coordinates mode (the PA line / PA pattern calibration generators)
|
||||
// treats the incoming point as already relative to the belt surface -- X across,
|
||||
// Y along the belt, Z above it -- and therefore skips the back-transform while
|
||||
// keeping the remap and the machine frame. It is a different coordinate map, not
|
||||
// a writer mode, which is why it is fixed at construction.
|
||||
class BeltKinematics : public CartesianKinematics
|
||||
{
|
||||
public:
|
||||
explicit BeltKinematics(const PrintConfig &config, bool world_coordinates = false);
|
||||
|
||||
Vec3d to_machine(const Vec3d &p) const override;
|
||||
|
||||
// A belt writer has always emitted full XYZ on every move, whether or not any
|
||||
// individual stage reports itself active. Making this conditional would change
|
||||
// emitted G-code for an identity-transform belt configuration.
|
||||
bool must_emit_all_axes() const override { return true; }
|
||||
bool suppress_lift_at_unknown_position() const override { return true; }
|
||||
// The machine frame shears and scales, so a circle is an ellipse in machine
|
||||
// coordinates and G2/G3 cannot describe it.
|
||||
bool supports_arc_moves() const override { return false; }
|
||||
|
||||
private:
|
||||
BeltBackTransform m_back_transform;
|
||||
MachineFrameTransform m_machine_frame;
|
||||
bool m_world_coordinates { false };
|
||||
};
|
||||
|
||||
// Install a belt machine frame on any GCodeWriter. Any axis remap and build
|
||||
// volume already configured on the writer are carried over, so this may be
|
||||
// called before or after those setters. Re-calling it with a different
|
||||
// world_coordinates value swaps the map (used around the PA line generator).
|
||||
void install_belt_kinematics(GCodeWriter &writer, const PrintConfig &config,
|
||||
bool world_coordinates = false);
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // slic3r_BeltKinematics_hpp_
|
||||
@@ -18,6 +18,7 @@
|
||||
#include <iostream>
|
||||
#include <float.h>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <system_error>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
@@ -46,10 +47,12 @@ CoolingBuffer::CoolingBuffer(GCode &gcodegen) : m_config(gcodegen.config()), m_g
|
||||
m_num_extruders = std::max(ex.id() + 1, m_num_extruders);
|
||||
m_extruder_ids.emplace_back(ex.id());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void CoolingBuffer::reset(const Vec3d &position)
|
||||
{
|
||||
m_belt_band_active = false;
|
||||
// BBS: add I and J axis to store center of arc
|
||||
m_current_pos.assign(7, 0.f);
|
||||
m_current_pos[0] = float(position.x());
|
||||
@@ -89,6 +92,9 @@ struct CoolingLine
|
||||
// ORCA: Add support for ironing fan speed control
|
||||
TYPE_IRONING_FAN_START = 1 << 19,
|
||||
TYPE_IRONING_FAN_END = 1 << 20,
|
||||
// Belt printers: extrusions within the first-layer band above the belt.
|
||||
TYPE_BELT_BAND_START = 1 << 21,
|
||||
TYPE_BELT_BAND_END = 1 << 22,
|
||||
};
|
||||
|
||||
CoolingLine(unsigned int type, size_t line_start, size_t line_end) :
|
||||
@@ -549,6 +555,10 @@ std::vector<PerExtruderAdjustments> CoolingBuffer::parse_layer_gcode(const std::
|
||||
line.type = CoolingLine::TYPE_IRONING_FAN_START;
|
||||
} else if (boost::starts_with(sline, ";_IRONING_FAN_END")) { // ORCA: Add support for ironing fan speed control
|
||||
line.type = CoolingLine::TYPE_IRONING_FAN_END;
|
||||
} else if (boost::starts_with(sline, ";_BELT_BAND_START")) {
|
||||
line.type = CoolingLine::TYPE_BELT_BAND_START;
|
||||
} else if (boost::starts_with(sline, ";_BELT_BAND_END")) {
|
||||
line.type = CoolingLine::TYPE_BELT_BAND_END;
|
||||
} else if (boost::starts_with(sline, "G4 ")) {
|
||||
// Parse the wait time.
|
||||
line.type = CoolingLine::TYPE_G4;
|
||||
@@ -891,7 +901,9 @@ std::string CoolingBuffer::apply_layer_cooldown(
|
||||
{CoolingLine::TYPE_SUPPORT_INTERFACE_FAN_START, false},
|
||||
{CoolingLine::TYPE_IRONING_FAN_START, false}, // ORCA: Add support for ironing fan speed control
|
||||
{CoolingLine::TYPE_FORCE_RESUME_FAN, false}};
|
||||
bool need_set_fan = false;
|
||||
// Belt printers: a band still open from the previous layer has to take the fan back from
|
||||
// the layer-level speed issued just above.
|
||||
bool need_set_fan = m_belt_band_active;
|
||||
|
||||
for (const CoolingLine *line : lines) {
|
||||
const char *line_start = gcode.c_str() + line->line_start;
|
||||
@@ -905,6 +917,8 @@ std::string CoolingBuffer::apply_layer_cooldown(
|
||||
if (new_extruder != m_current_extruder) {
|
||||
m_current_extruder = new_extruder;
|
||||
change_extruder_set_fan(true);
|
||||
if (m_belt_band_active)
|
||||
need_set_fan = true;
|
||||
}
|
||||
}
|
||||
new_gcode.append(line_start, line_end - line_start);
|
||||
@@ -957,6 +971,13 @@ std::string CoolingBuffer::apply_layer_cooldown(
|
||||
if (m_additional_fan_speed != -1 && m_config.auxiliary_fan.value)
|
||||
new_gcode += GCodeWriter::set_additional_fan(m_additional_fan_speed);
|
||||
}
|
||||
else if (line->type & CoolingLine::TYPE_BELT_BAND_START) {
|
||||
m_belt_band_active = true;
|
||||
need_set_fan = true;
|
||||
} else if (line->type & CoolingLine::TYPE_BELT_BAND_END) {
|
||||
m_belt_band_active = false;
|
||||
need_set_fan = true;
|
||||
}
|
||||
else if (line->type & CoolingLine::TYPE_EXTRUDE_END) {
|
||||
// Just remove this comment.
|
||||
} else if (line->type & (CoolingLine::TYPE_ADJUSTABLE | CoolingLine::TYPE_EXTERNAL_PERIMETER | CoolingLine::TYPE_WIPE | CoolingLine::TYPE_HAS_F)) {
|
||||
@@ -1049,7 +1070,15 @@ std::string CoolingBuffer::apply_layer_cooldown(
|
||||
m_current_fan_speed = speed;
|
||||
}
|
||||
};
|
||||
if (fan_speed_change_requests[CoolingLine::TYPE_OVERHANG_FAN_START]){
|
||||
if (m_belt_band_active) {
|
||||
// Belt printers: a tilted layer runs from the belt to the top of the part, so
|
||||
// "the first layers" are a band along the belt rather than the first slicing
|
||||
// layers. Extrusions GCode::_extrude() marks as inside that band print with the
|
||||
// fan off, whatever overhang, bridge or resume request is pending, as the first
|
||||
// layers of a flat bed do. Leaving the band falls through to the branches below.
|
||||
set_fan(0);
|
||||
fan_speed_change_requests[CoolingLine::TYPE_FORCE_RESUME_FAN] = false;
|
||||
} else if (fan_speed_change_requests[CoolingLine::TYPE_OVERHANG_FAN_START]){
|
||||
set_fan(overhang_fan_speed);
|
||||
} else if (fan_speed_change_requests[CoolingLine::TYPE_INTERNAL_BRIDGE_FAN_START]){ // ORCA: Add support for separate internal bridge fan speed control
|
||||
set_fan(internal_bridge_fan_speed);
|
||||
|
||||
@@ -21,7 +21,7 @@ struct PerExtruderAdjustments;
|
||||
//
|
||||
// The simple it sounds, the actual implementation is significantly more complex.
|
||||
// Namely, for a multi-extruder print, each material may require a different cooling logic.
|
||||
// For example, some materials may not like to print too slowly, while with some materials
|
||||
// For example, some materials may not like to print too slowly, while with some materials
|
||||
// we may slow down significantly.
|
||||
//
|
||||
class CoolingBuffer {
|
||||
@@ -63,6 +63,9 @@ private:
|
||||
unsigned int m_current_nozzle;
|
||||
//BBS: current fan speed
|
||||
int m_current_fan_speed;
|
||||
// Belt printers: the extrusion being processed lies in the first-layer band above the
|
||||
// belt (between a ";_BELT_BAND_START" and a ";_BELT_BAND_END"). Kept across layers.
|
||||
bool m_belt_band_active = false;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -2616,6 +2616,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 +2855,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 +2892,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 +2940,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 +2986,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 +3152,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 +3189,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) {
|
||||
@@ -3637,6 +3698,7 @@ 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();
|
||||
@@ -3677,6 +3739,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;
|
||||
}
|
||||
|
||||
@@ -4242,6 +4305,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;
|
||||
@@ -6138,6 +6229,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 +7214,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 +7237,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
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
#include "libslic3r/CustomGCode.hpp"
|
||||
#include "libslic3r/MultiNozzleUtils.hpp"
|
||||
#include "libslic3r/GCode/MachineFrameTransform.hpp"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cassert>
|
||||
@@ -290,6 +291,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;
|
||||
@@ -385,6 +399,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
|
||||
@@ -1085,6 +1102,10 @@ class Print;
|
||||
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;
|
||||
@@ -1160,6 +1181,13 @@ class Print;
|
||||
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 +1217,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;
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
#include "MachineFrameTransform.hpp"
|
||||
#include "../Geometry.hpp"
|
||||
#include "../Point.hpp"
|
||||
#include "../PrintConfig.hpp"
|
||||
#include "../libslic3r.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
bool MachineFrameTransform::init_from_config(const PrintConfig &config)
|
||||
{
|
||||
m_active = false;
|
||||
m_transform = Transform3d::Identity();
|
||||
m_transform_inverse = Transform3d::Identity();
|
||||
|
||||
if (!config.belt_printer.value)
|
||||
return false;
|
||||
|
||||
// The machine-frame transform is derived from the single belt tilt (axis +
|
||||
// angle) that also drives the pre-slice mesh rotation. Expert decouple lets
|
||||
// the machine-frame angle differ from the slicing rotation; otherwise both
|
||||
// use belt_slice_rotation_angle.
|
||||
const BeltRotationAxis axis = config.belt_slice_rotation.value;
|
||||
if (axis == BeltRotationAxis::None || axis == BeltRotationAxis::Z)
|
||||
return false; // Z is an in-plane spin: no machine-frame tilt.
|
||||
|
||||
const double angle_deg = config.belt_frame_tilt_decouple.value
|
||||
? config.belt_frame_tilt_angle.value
|
||||
: config.belt_slice_rotation_angle.value;
|
||||
if (std::abs(angle_deg) <= EPSILON)
|
||||
return false;
|
||||
|
||||
const double angle_rad = Geometry::deg2rad(angle_deg);
|
||||
const double sin_a = std::sin(angle_rad);
|
||||
if (std::abs(sin_a) <= EPSILON)
|
||||
return false;
|
||||
const double cot_a = std::cos(angle_rad) / sin_a;
|
||||
const double inv_sin = 1.0 / std::abs(sin_a);
|
||||
|
||||
// This stage runs after the conventional belt axis swap. For an X-axis
|
||||
// slicing rotation, remapped Y is model height and remapped Z is travel
|
||||
// along the belt. Convert those Cartesian coordinates to machine axes with
|
||||
// the established belt-printer convention:
|
||||
// machine gantry = model height / sin(a)
|
||||
// machine belt = model belt + model height * cot(a)
|
||||
// The Y-rotation case is the same mapping on X/Z, with the rotation sign.
|
||||
// At 45 degrees tan/cot and sin/cos are equal, which previously hid the
|
||||
// incorrect complementary-angle formulas used by this unified transform.
|
||||
Matrix3d shear = Matrix3d::Identity();
|
||||
Matrix3d scale = Matrix3d::Identity();
|
||||
if (axis == BeltRotationAxis::X) {
|
||||
shear(2, 1) = cot_a; // Z from Y
|
||||
scale(1, 1) = inv_sin; // Y
|
||||
} else { // BeltRotationAxis::Y
|
||||
shear(2, 0) = -cot_a; // Z from X
|
||||
scale(0, 0) = inv_sin; // X
|
||||
}
|
||||
|
||||
// Apply shear first, then scale (the historical default ShearThenScale order:
|
||||
// result = scale * shear * p). For the canonical 45°/X belt this maps
|
||||
// (x,y,z) -> (x, y/sin, y + z), matching the previous per-axis config.
|
||||
Transform3d combined = Transform3d::Identity();
|
||||
combined.linear() = scale * shear;
|
||||
|
||||
if (combined.isApprox(Transform3d::Identity()))
|
||||
return false;
|
||||
|
||||
m_transform = combined;
|
||||
m_transform_inverse = combined.inverse();
|
||||
m_active = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
Vec3d MachineFrameTransform::apply(const Vec3d &pos) const
|
||||
{
|
||||
if (!m_active)
|
||||
return pos;
|
||||
return m_transform * pos;
|
||||
}
|
||||
|
||||
Vec3d MachineFrameTransform::apply_inverse(const Vec3d &pos) const
|
||||
{
|
||||
if (!m_active)
|
||||
return pos;
|
||||
return m_transform_inverse * pos;
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,54 @@
|
||||
#ifndef slic3r_MachineFrameTransform_hpp_
|
||||
#define slic3r_MachineFrameTransform_hpp_
|
||||
|
||||
#include "../libslic3r.h"
|
||||
#include "../Point.hpp"
|
||||
#include "../PrintConfig.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// Post-stage machine-frame transform for belt printers.
|
||||
//
|
||||
// Applied in BeltKinematics::to_machine AFTER the back-transform and
|
||||
// the gcode_remap_* axis remap. Maps Cartesian (axis-permuted) G-code
|
||||
// coordinates into the printer's physical machine frame.
|
||||
//
|
||||
// Derived entirely from the single belt tilt (belt_slice_rotation axis +
|
||||
// belt_slice_rotation_angle): a shear coupling the height axis to the belt-feed
|
||||
// axis (factor cot a) plus a 1/sin a scale on the gantry-height axis. The expert
|
||||
// belt_frame_tilt_decouple flag lets the machine-frame angle differ from the
|
||||
// pre-slice rotation angle via belt_frame_tilt_angle.
|
||||
class MachineFrameTransform
|
||||
{
|
||||
public:
|
||||
MachineFrameTransform() = default;
|
||||
|
||||
// Initialize from belt printer config. Returns true if a non-identity
|
||||
// transform was computed. Inactive when belt_printer is disabled or
|
||||
// both shear and scale are identity.
|
||||
bool init_from_config(const PrintConfig &config);
|
||||
|
||||
// Apply the transform to a point. Returns pos unchanged if not active.
|
||||
Vec3d apply(const Vec3d &pos) const;
|
||||
|
||||
// Apply the inverse transform. Returns pos unchanged if not active.
|
||||
// Used by validators that need to compare emitted machine-frame
|
||||
// coordinates against build-volume bounds.
|
||||
Vec3d apply_inverse(const Vec3d &pos) const;
|
||||
|
||||
bool is_active() const { return m_active; }
|
||||
|
||||
// The composed shear*scale transform (identity when inactive). Exposed so the
|
||||
// G-code viewer can build the machine->model back-transform for the upright
|
||||
// ("designed") belt preview.
|
||||
const Transform3d& transform() const { return m_transform; }
|
||||
|
||||
private:
|
||||
bool m_active = false;
|
||||
Transform3d m_transform = Transform3d::Identity();
|
||||
Transform3d m_transform_inverse = Transform3d::Identity();
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // slic3r_MachineFrameTransform_hpp_
|
||||
@@ -0,0 +1,25 @@
|
||||
#include "MachineKinematics.hpp"
|
||||
#include "../Point.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// Moved verbatim from GCodeWriter::apply_axis_remap().
|
||||
Vec3d CartesianKinematics::apply_axis_remap(const Vec3d &pos) const
|
||||
{
|
||||
if (!has_axis_remap())
|
||||
return pos;
|
||||
auto remap = [this, &pos](int r) -> double {
|
||||
int axis = r % 3;
|
||||
if (r < 3) return pos[axis];
|
||||
if (r < 6) return -pos[axis];
|
||||
return m_build_vol_max[axis] - pos[axis];
|
||||
};
|
||||
return { remap(m_remap_x), remap(m_remap_y), remap(m_remap_z) };
|
||||
}
|
||||
|
||||
Vec3d CartesianKinematics::to_machine(const Vec3d &p) const
|
||||
{
|
||||
return this->apply_axis_remap(p);
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,92 @@
|
||||
#ifndef slic3r_MachineKinematics_hpp_
|
||||
#define slic3r_MachineKinematics_hpp_
|
||||
|
||||
#include "../Point.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// The frame contract for emitted movement.
|
||||
//
|
||||
// GCodeWriter produces points in the *logical placed* frame: plate offsets have
|
||||
// already been subtracted, but no machine-specific mapping has been applied.
|
||||
// A MachineKinematics turns that into the coordinates actually written to
|
||||
// G-code, and answers the two structural questions the writer needs in order to
|
||||
// decide which axis words it may omit.
|
||||
//
|
||||
// This is a seam for writer-generated movement only. Start/end/custom G-code,
|
||||
// classic wipe-tower output and GCodeWriter::extrude_arc_to_xy() do NOT pass
|
||||
// through it: they write machine coordinates directly.
|
||||
class MachineKinematics
|
||||
{
|
||||
public:
|
||||
virtual ~MachineKinematics() = default;
|
||||
|
||||
// Logical placed point -> emitted machine point.
|
||||
virtual Vec3d to_machine(const Vec3d &p) const = 0;
|
||||
|
||||
// True when a move must emit X, Y and Z because omitting a word would be
|
||||
// wrong under this mapping. Deliberately not called "couples_axes": a pure
|
||||
// axis permutation forces full emission without physically coupling axes.
|
||||
virtual bool must_emit_all_axes() const = 0;
|
||||
|
||||
// True when a lift must be suppressed while the current position is unknown,
|
||||
// because _travel_to_z() re-emits the logical X/Y through this mapping and an
|
||||
// uninitialised position would map to a bogus machine point -- for a reverse
|
||||
// mapping, the far corner of the bed.
|
||||
virtual bool suppress_lift_at_unknown_position() const = 0;
|
||||
|
||||
// True when a G2/G3 arc in the logical XY plane is still the same arc in the
|
||||
// machine frame. Arc moves emit only X, Y, I and J, so this asks a narrower
|
||||
// question than must_emit_all_axes(): whether logical X and Y reach the
|
||||
// machine unchanged. A mapping that only negates or reverses Z keeps its
|
||||
// arcs; one that permutes X or Y moves the arc out of the plane that I/J
|
||||
// describes, and a shear turns the circle into an ellipse G2/G3 cannot
|
||||
// express at all.
|
||||
virtual bool supports_arc_moves() const = 0;
|
||||
|
||||
// Configuration. GCodeWriter forwards its setters here so that the state
|
||||
// lives with the strategy and a strategy installed before the setters run
|
||||
// still receives it.
|
||||
virtual void set_axis_remap(int rx, int ry, int rz) = 0;
|
||||
virtual void set_build_volume_max(const Vec3d &max) = 0;
|
||||
};
|
||||
|
||||
// Axis remap only -- the historical GCodeWriter behaviour, moved verbatim.
|
||||
//
|
||||
// The remap encodes, per output axis, which source axis feeds it and how:
|
||||
// r < 3 : source axis r, unchanged
|
||||
// r < 6 : source axis r-3, negated
|
||||
// else : source axis r-6, reversed within the build volume
|
||||
class CartesianKinematics : public MachineKinematics
|
||||
{
|
||||
public:
|
||||
Vec3d to_machine(const Vec3d &p) const override;
|
||||
|
||||
bool must_emit_all_axes() const override { return this->has_axis_remap(); }
|
||||
bool suppress_lift_at_unknown_position() const override { return this->has_axis_remap(); }
|
||||
|
||||
// X and Y must reach the machine untouched. Because the remap is a
|
||||
// permutation, pinning those two also pins Z to Z, so a mapping that only
|
||||
// negates or reverses Z still supports arcs -- every word a G2/G3 emits is
|
||||
// unchanged by it.
|
||||
bool supports_arc_moves() const override { return m_remap_x == 0 && m_remap_y == 1; }
|
||||
|
||||
void set_axis_remap(int rx, int ry, int rz) override
|
||||
{ m_remap_x = rx; m_remap_y = ry; m_remap_z = rz; }
|
||||
void set_build_volume_max(const Vec3d &max) override { m_build_vol_max = max; }
|
||||
|
||||
bool has_axis_remap() const
|
||||
{ return m_remap_x != 0 || m_remap_y != 1 || m_remap_z != 2; }
|
||||
|
||||
protected:
|
||||
Vec3d apply_axis_remap(const Vec3d &pos) const;
|
||||
|
||||
int m_remap_x { 0 };
|
||||
int m_remap_y { 1 };
|
||||
int m_remap_z { 2 };
|
||||
Vec3d m_build_vol_max { Vec3d::Zero() };
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // slic3r_MachineKinematics_hpp_
|
||||
@@ -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
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include "Utils.hpp"
|
||||
#include "format.hpp"
|
||||
#include "I18N.hpp"
|
||||
#include "../BeltBrim.hpp"
|
||||
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <vector>
|
||||
@@ -419,6 +420,10 @@ bool ToolOrdering::insert_wipe_tower_extruder()
|
||||
{
|
||||
if (!m_print_config_ptr || !m_print_config_ptr->enable_prime_tower)
|
||||
return false;
|
||||
// Belt mode has no classic wipe tower; the dedicated wipe tower filament
|
||||
// must not inject extra toolchanges into the purge prism planning.
|
||||
if (m_print_config_ptr->belt_printer)
|
||||
return false;
|
||||
if (m_print_config_ptr->wipe_tower_filament == 0)
|
||||
return false;
|
||||
|
||||
@@ -516,6 +521,11 @@ ToolOrdering::ToolOrdering(const PrintObject &object, unsigned int first_extrude
|
||||
zs.emplace_back(layer->print_z);
|
||||
for (auto layer : object.support_layers())
|
||||
zs.emplace_back(layer->print_z);
|
||||
// Belt brim apron bands sit below the object's first layer and have no
|
||||
// layer of their own, but tools_for_layer() asserts an exact Z match, so
|
||||
// their print_z must be part of the ordering.
|
||||
for (const BeltBrimBand &band : object.belt_brim_prologue())
|
||||
zs.emplace_back(band.print_z);
|
||||
this->initialize_layers(zs);
|
||||
}
|
||||
|
||||
@@ -560,6 +570,10 @@ ToolOrdering::ToolOrdering(const Print &print, unsigned int first_extruder, bool
|
||||
zs.emplace_back(layer->print_z);
|
||||
for (auto layer : object->support_layers())
|
||||
zs.emplace_back(layer->print_z);
|
||||
// See the single-object ctor: belt brim apron bands need their own
|
||||
// ordering entries or tools_for_layer() will assert.
|
||||
for (const BeltBrimBand &band : object->belt_brim_prologue())
|
||||
zs.emplace_back(band.print_z);
|
||||
|
||||
max_layer_height = std::max(max_layer_height, object->config().layer_height.value);
|
||||
}
|
||||
@@ -994,6 +1008,42 @@ void ToolOrdering::collect_extruders(const PrintObject &object, const std::vecto
|
||||
}
|
||||
}
|
||||
|
||||
// Belt brim apron bands own their layers outright: they print below the
|
||||
// object's first layer, so no object or support layer claims an extruder there
|
||||
// and process_layer() would bail out at "Nothing to extrude". Claim the
|
||||
// object's outer wall filament, in the same raw 1-based domain the loops above
|
||||
// push. Deliberately not layer_tools.has_object, which drives skirt marking
|
||||
// and wiping overrides.
|
||||
if (! object.belt_brim_prologue().empty()) {
|
||||
// 1-based, same domain the object/support pushes above use; reindexed to 0-based
|
||||
// with the rest of the list later.
|
||||
const unsigned int brim_filament = object.belt_brim_filament();
|
||||
for (const BeltBrimBand &band : object.belt_brim_prologue()) {
|
||||
if (band.fills.empty())
|
||||
continue;
|
||||
LayerTools &layer_tools = this->tools_for_layer(band.print_z);
|
||||
layer_tools.extruders.push_back(brim_filament);
|
||||
}
|
||||
}
|
||||
|
||||
// Coincident brim bands (belt_brim_by_layer) print ON an object layer rather than
|
||||
// below it, but that layer can produce no InstanceVisit in process_layer - a
|
||||
// zero-extrusion lead-in slice with no coinciding support - and the band would then
|
||||
// be silently dropped. Register the brim filament on every layer that carries a
|
||||
// coincident band, in the same 1-based domain as the prologue push above, so a brim
|
||||
// pass always exists there.
|
||||
if (object.has_belt_brim()) {
|
||||
const unsigned int brim_filament = object.belt_brim_filament();
|
||||
const auto &by_layer = object.belt_brim_by_layer();
|
||||
const size_t n = std::min(by_layer.size(), object.layers().size());
|
||||
for (size_t i = 0; i < n; ++ i) {
|
||||
if (by_layer[i].empty())
|
||||
continue;
|
||||
LayerTools &layer_tools = this->tools_for_layer(object.layers()[i]->print_z);
|
||||
layer_tools.extruders.push_back(brim_filament);
|
||||
}
|
||||
}
|
||||
|
||||
for (auto& layer : m_layer_tools) {
|
||||
// Sort and remove duplicates
|
||||
sort_remove_duplicates(layer.extruders);
|
||||
@@ -1036,12 +1086,28 @@ void ToolOrdering::fill_wipe_tower_partitions(const PrintConfig &config, coordf_
|
||||
}
|
||||
|
||||
//FIXME this is a hack to get the ball rolling.
|
||||
// The `print_z < object_bottom_z` clause reads "below the object" as "raft
|
||||
// gap". On a belt printer that is wrong: the brim apron legitimately prints
|
||||
// below the object's first layer, and treating those layers as raft would put a
|
||||
// wipe tower at negative Z. A belt printer never prints the classic
|
||||
// prime tower (Print::has_wipe_tower()), so simply drop the clause there.
|
||||
//
|
||||
// Gate on config.belt_printer, NOT on has_belt_brim: every layer below the
|
||||
// object bottom on a belt printer is legitimately a sub-object stream - brim
|
||||
// apron, belt support printed below Z0, or the object's own lead-in - and none of
|
||||
// them is ever raft, because Print::validate() rejects raft_layers>0 on a belt
|
||||
// printer outright. Narrowing this to has_belt_brim would reclassify
|
||||
// belt-support-below-floor layers as raft on brim-less belt prints and reintroduce
|
||||
// the negative-Z wipe tower, so the broad belt_printer gate is correct.
|
||||
const bool belt_no_raft_gap = config.belt_printer.value;
|
||||
for (LayerTools < : m_layer_tools)
|
||||
lt.has_wipe_tower |= ((lt.has_object || lt.has_support) && (config.timelapse_type == TimelapseType::tlSmooth || lt.wipe_tower_partitions > 0))
|
||||
|| lt.print_z < object_bottom_z + EPSILON;
|
||||
|| (! belt_no_raft_gap && lt.print_z < object_bottom_z + EPSILON);
|
||||
|
||||
// Test for a raft, insert additional wipe tower layer to fill in the raft separation gap.
|
||||
for (size_t i = 0; i + 1 < m_layer_tools.size(); ++ i) {
|
||||
// Skipped on belt printers for the same reason as the clause above: layers
|
||||
// below the object are brim apron, not raft.
|
||||
for (size_t i = 0; ! belt_no_raft_gap && i + 1 < m_layer_tools.size(); ++ i) {
|
||||
const LayerTools < = m_layer_tools[i];
|
||||
const LayerTools <_next = m_layer_tools[i + 1];
|
||||
if (lt.print_z < object_bottom_z + EPSILON && lt_next.print_z >= object_bottom_z + EPSILON) {
|
||||
|
||||
@@ -84,7 +84,17 @@ public:
|
||||
|
||||
void set_layer_tools_ptr(const LayerTools* lt) { m_layer_tools = lt; }
|
||||
|
||||
// Returns true if entity is not printed with its usual extruder for a given
|
||||
// copy -- i.e. it was claimed as a wiping/purge extrusion. Public because the
|
||||
// belt purge prism uses it to tell which of its fills actually carry purge
|
||||
// from the ones that are unclaimed waste (Print::_plan_belt_purge()).
|
||||
bool is_entity_overridden(const ExtrusionEntity* entity, const PrintObject *object, size_t copy_id) const {
|
||||
auto it = entity_map.find(std::make_tuple(entity, object));
|
||||
return it != entity_map.end() && copy_id < it->second.size() && it->second[copy_id] != -1;
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
int first_nonsoluble_extruder_on_layer(const PrintConfig& print_config) const;
|
||||
int last_nonsoluble_extruder_on_layer(const PrintConfig& print_config) const;
|
||||
|
||||
@@ -94,12 +104,6 @@ private:
|
||||
void set_support_extruder_override(const PrintObject* object, size_t copy_id, int extruder, size_t num_of_copies);
|
||||
void set_support_interface_extruder_override(const PrintObject* object, size_t copy_id, int extruder, size_t num_of_copies);
|
||||
|
||||
// Returns true in case that entity is not printed with its usual extruder for a given copy:
|
||||
bool is_entity_overridden(const ExtrusionEntity* entity, const PrintObject *object, size_t copy_id) const {
|
||||
auto it = entity_map.find(std::make_tuple(entity, object));
|
||||
return it == entity_map.end() ? false : it->second[copy_id] != -1;
|
||||
}
|
||||
|
||||
std::map<std::tuple<const ExtrusionEntity*, const PrintObject *>, ExtruderPerCopy> entity_map; // to keep track of who prints what
|
||||
// BBS
|
||||
std::map<const PrintObject*, int> support_map;
|
||||
|
||||
+198
-18
@@ -1,6 +1,7 @@
|
||||
#include "GCodeWriter.hpp"
|
||||
#include "Config.hpp"
|
||||
#include "Extruder.hpp"
|
||||
#include "Geometry.hpp"
|
||||
#include "I18N.hpp"
|
||||
#include "Point.hpp"
|
||||
#include "Polygon.hpp"
|
||||
@@ -32,6 +33,7 @@
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <math.h>
|
||||
#include <memory>
|
||||
|
||||
#ifdef __APPLE__
|
||||
#include <boost/spirit/include/karma.hpp>
|
||||
@@ -44,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);
|
||||
@@ -822,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
|
||||
@@ -836,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;
|
||||
{
|
||||
@@ -850,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");
|
||||
}
|
||||
@@ -862,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;
|
||||
{
|
||||
@@ -877,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)
|
||||
@@ -894,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;
|
||||
@@ -915,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);
|
||||
@@ -964,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");
|
||||
}
|
||||
}
|
||||
@@ -978,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);
|
||||
@@ -1016,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();
|
||||
}
|
||||
@@ -1061,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);
|
||||
@@ -1075,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);
|
||||
|
||||
@@ -1181,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
|
||||
@@ -1189,6 +1308,42 @@ void GCodeWriter::extrude_to_xy(std::string &out, const Vec2d &point, double dE,
|
||||
w.append_to(out);
|
||||
}
|
||||
|
||||
// Approximate an arc with linear extrusions, for machine mappings that cannot
|
||||
// express a G2/G3 (see extrude_arc_to_xy). center_offset is I/J: the centre
|
||||
// relative to the CURRENT position, which is why this must run before m_pos is
|
||||
// updated.
|
||||
void GCodeWriter::extrude_arc_as_polyline(std::string &out, const Vec2d &point, const Vec2d ¢er_offset,
|
||||
double dE, const bool is_ccw,
|
||||
const std::string &comment, bool force_no_extrusion)
|
||||
{
|
||||
const Vec2d start = Vec2d(m_pos.x(), m_pos.y());
|
||||
const Vec2d centre = start + center_offset;
|
||||
const double r = (start - centre).norm();
|
||||
if (r < EPSILON) {
|
||||
// Degenerate: no arc to speak of, so a single move is exact.
|
||||
this->extrude_to_xy(out, point, dE, comment, force_no_extrusion);
|
||||
return;
|
||||
}
|
||||
|
||||
double a0 = std::atan2(start.y() - centre.y(), start.x() - centre.x());
|
||||
double a1 = std::atan2(point.y() - centre.y(), point.x() - centre.x());
|
||||
double sweep = a1 - a0;
|
||||
if (is_ccw) { while (sweep <= 0.) sweep += 2. * PI; }
|
||||
else { while (sweep >= 0.) sweep -= 2. * PI; }
|
||||
|
||||
// Segment count from a chord-deviation bound: r*(1-cos(dtheta/2)) <= tol.
|
||||
const double tol = 0.005; // mm
|
||||
const double dmax = (tol >= r) ? PI : 2. * std::acos(1. - tol / r);
|
||||
const int n = std::max(2, int(std::ceil(std::abs(sweep) / std::max(dmax, EPSILON))));
|
||||
|
||||
for (int i = 1; i <= n; ++ i) {
|
||||
const double a = a0 + sweep * (double(i) / double(n));
|
||||
const Vec2d p = (i == n) ? point
|
||||
: Vec2d(centre.x() + r * std::cos(a), centre.y() + r * std::sin(a));
|
||||
this->extrude_to_xy(out, p, dE / double(n), i == n ? comment : std::string(), force_no_extrusion);
|
||||
}
|
||||
}
|
||||
|
||||
//BBS: generate G2 or G3 extrude which moves by arc
|
||||
//point is end point which means X and Y axis
|
||||
//center_offset is I and J axis
|
||||
@@ -1201,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)
|
||||
@@ -1241,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
|
||||
|
||||
+101
-28
@@ -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]; }
|
||||
@@ -91,6 +98,10 @@ public:
|
||||
std::string extrude_to_xy(const Vec2d &point, double dE, const std::string &comment = std::string(), bool force_no_extrusion = false);
|
||||
//BBS: generate G2 or G3 extrude which moves by arc
|
||||
std::string extrude_arc_to_xy(const Vec2d &point, const Vec2d ¢er_offset, double dE, const bool is_ccw, const std::string &comment = std::string(), bool force_no_extrusion = false);
|
||||
// Linear approximation of an arc, used when the machine mapping cannot
|
||||
// express a G2/G3. Must be called before m_pos is updated: center_offset is
|
||||
// relative to the current position.
|
||||
void extrude_arc_as_polyline(std::string &out, const Vec2d &point, const Vec2d ¢er_offset, double dE, const bool is_ccw, const std::string &comment = std::string(), bool force_no_extrusion = false);
|
||||
std::string extrude_to_xyz(const Vec3d &point, double dE, const std::string &comment = std::string(), bool force_no_extrusion = false);
|
||||
// Each appends its line to `out`.
|
||||
void set_speed(std::string &out, double F, const std::string &comment = std::string(), const std::string &cooling_marker = std::string());
|
||||
@@ -147,16 +158,94 @@ public:
|
||||
void invalidate_acceleration() { m_last_acceleration = 0; m_last_travel_acceleration = 0; }
|
||||
void invalidate_jerk() { m_last_jerk = 0; }
|
||||
|
||||
// Axis remap: permute/negate/reverse axes in G-code output.
|
||||
// Works standalone (without belt mode) for printers with non-standard axis conventions.
|
||||
void set_axis_remap(int rx, int ry, int rz);
|
||||
void set_build_volume_max(const Vec3d &max);
|
||||
bool has_axis_remap() const;
|
||||
|
||||
// Install the machine frame mapping. Any axis remap / build volume already
|
||||
// configured is carried over, so install order does not matter.
|
||||
void set_kinematics(std::unique_ptr<MachineKinematics> kinematics);
|
||||
const MachineKinematics& kinematics() const { return *m_kinematics; }
|
||||
|
||||
// Per-point first-layer test. When set, travel speed selection asks it per
|
||||
// destination point (in the writer's logical placed frame) instead of using
|
||||
// the layer-coarse m_is_first_layer flag. GCode installs it on belt printers
|
||||
// with the same test its extrusions use (GCode::on_first_layer(point)), so a
|
||||
// travel is judged against the belt surface exactly as the path it leads to.
|
||||
using FirstLayerPointTest = std::function<bool(const Vec3d &point_logical)>;
|
||||
void set_first_layer_point_test(FirstLayerPointTest test) { m_first_layer_point_test = std::move(test); }
|
||||
|
||||
// Force every lift to a plain vertical lift. Spiral and slope lifts compute
|
||||
// their slope in the logical frame and do not account for a machine mapping
|
||||
// that couples axes.
|
||||
void set_force_normal_lift(bool force) { m_force_normal_lift = force; }
|
||||
|
||||
// Returns whether this flavor supports separate print and travel acceleration.
|
||||
static bool supports_separate_travel_acceleration(GCodeFlavor flavor);
|
||||
private:
|
||||
protected:
|
||||
// Position/lift/offset state.
|
||||
Vec3d m_pos = Vec3d::Zero();
|
||||
double m_x_offset{ 0 };
|
||||
double m_y_offset{ 0 };
|
||||
double m_lifted;
|
||||
double m_to_lift;
|
||||
LiftType m_to_lift_type;
|
||||
bool m_is_first_layer = true;
|
||||
bool m_is_current_pos_clear = false;
|
||||
double m_current_speed;
|
||||
|
||||
std::string _travel_to_z(double z, const std::string &comment);
|
||||
|
||||
// Whether a destination gets first-layer treatment. With a point test
|
||||
// installed it decides; otherwise the layer-coarse m_is_first_layer flag does.
|
||||
bool point_on_first_layer(const Vec3d &point_logical) const;
|
||||
|
||||
// True when a lift must be skipped because this mapping would emit the
|
||||
// stored logical X/Y and that position is not yet known.
|
||||
bool must_skip_lift_now() const;
|
||||
|
||||
// True when travel speed is selected per destination point rather than per
|
||||
// layer. Set for writers that install a first-layer point test. The
|
||||
// historical path emits the raw configured travel speed in the final branch
|
||||
// of travel_to_xyz(), ignoring the first-layer selection computed at the top
|
||||
// of that function; a point-test-driven writer uses the first-layer-aware
|
||||
// value throughout. Both are preserved exactly -- unifying them would change
|
||||
// emitted feedrates and belongs in its own commit.
|
||||
bool uses_pointwise_travel_speed() const { return bool(m_first_layer_point_test); }
|
||||
|
||||
FirstLayerPointTest m_first_layer_point_test;
|
||||
bool m_force_normal_lift = false;
|
||||
|
||||
// The machine frame mapping. Owns the axis-remap state that used to live
|
||||
// here as m_remap_* / m_build_vol_max; the setters above forward to it.
|
||||
// Never null: a CartesianKinematics at the identity remap reproduces the
|
||||
// historical behaviour exactly.
|
||||
std::unique_ptr<MachineKinematics> m_kinematics;
|
||||
|
||||
// Last configured remap / build volume, replayed onto a newly installed
|
||||
// kinematics so set_kinematics() and the setters are order-independent.
|
||||
int m_remap_x = 0; // RemapAxis: 0=+X, 1=+Y, 2=+Z, 3=-X, etc.
|
||||
int m_remap_y = 1;
|
||||
int m_remap_z = 2;
|
||||
Vec3d m_build_vol_max = Vec3d::Zero();
|
||||
|
||||
// Apply the machine frame mapping to a point. Returns pos unchanged when the
|
||||
// mapping is the identity.
|
||||
Vec3d apply_axis_remap(const Vec3d &pos) const;
|
||||
|
||||
// Motion uses the global/base process variant until a filament becomes active.
|
||||
// Indexes the per-extruder speed options (travel_speed, travel_speed_z,
|
||||
// initial_layer_travel_speed).
|
||||
size_t m_cached_extruder_idx;
|
||||
|
||||
private:
|
||||
// Extruders are sorted by their ID, so that binary search is possible.
|
||||
std::vector<Extruder> m_filament_extruders;
|
||||
bool m_single_extruder_multi_material;
|
||||
std::vector<Extruder*> m_curr_filament_extruder;
|
||||
int m_curr_extruder_id;
|
||||
// Motion uses the global/base process variant until a filament becomes active.
|
||||
size_t m_cached_extruder_idx;
|
||||
unsigned int m_last_acceleration;
|
||||
unsigned int m_last_travel_acceleration;
|
||||
std::vector<unsigned int> m_max_travel_acceleration;
|
||||
@@ -178,19 +267,6 @@ public:
|
||||
//BBS
|
||||
int m_last_bed_temperature;
|
||||
bool m_last_bed_temperature_reached;
|
||||
double m_lifted;
|
||||
|
||||
// BBS
|
||||
double m_to_lift;
|
||||
LiftType m_to_lift_type;
|
||||
Vec3d m_pos = Vec3d::Zero();
|
||||
//BBS: this flag is used to indicate whether the m_pos is real.
|
||||
//A example that of the first move, the m_pos is zero, but the real position of extruder doesn't
|
||||
//Pos must be clear after the first xyz travel move
|
||||
bool m_is_current_pos_clear = false;
|
||||
//BBS: x, y offset for gcode generated
|
||||
double m_x_offset{ 0 };
|
||||
double m_y_offset{ 0 };
|
||||
|
||||
// Orca: slicing resolution in mm
|
||||
double m_resolution = 0.01;
|
||||
@@ -202,21 +278,18 @@ public:
|
||||
// non-rectangular beds such as delta/circular printers.
|
||||
Polygon m_bed_printable_area;
|
||||
std::vector<Polygon> m_extruder_printable_areas;
|
||||
|
||||
|
||||
std::string m_gcode_label_objects_start;
|
||||
std::string m_gcode_label_objects_end;
|
||||
|
||||
//SoftFever
|
||||
bool m_is_bbl_printers = false;
|
||||
double m_current_speed;
|
||||
bool m_is_first_layer = true;
|
||||
|
||||
enum class Acceleration {
|
||||
Travel,
|
||||
Print
|
||||
};
|
||||
|
||||
std::string _travel_to_z(double z, const std::string &comment);
|
||||
std::string _spiral_travel_to_z(double z, const Vec2d &ij_offset, const std::string &comment);
|
||||
// Orca: printable area of the active extruder (per-extruder when configured, otherwise the bed). Null when unknown.
|
||||
const Polygon *active_printable_area() const;
|
||||
|
||||
@@ -1260,7 +1260,7 @@ static inline std::vector<std::vector<ExPolygons>> segmentation_top_and_bottom_l
|
||||
// project downards pointing painted triangles over bottom surfaces.
|
||||
std::vector<std::vector<Polygons>> top_raw(num_facets_states), bottom_raw(num_facets_states);
|
||||
std::vector<float> zs = zs_from_layers(layers);
|
||||
Transform3d object_trafo = print_object.trafo_centered();
|
||||
Transform3d object_trafo = print_object.trafo_sliced();
|
||||
|
||||
#ifdef MM_SEGMENTATION_DEBUG_TOP_BOTTOM
|
||||
static int iRun = 0;
|
||||
@@ -1289,10 +1289,16 @@ static inline std::vector<std::vector<ExPolygons>> segmentation_top_and_bottom_l
|
||||
slicing_params.trafo = volume_trafo;
|
||||
Polygons bottom_slice = slice_mesh(painted, zs[0], slicing_params);
|
||||
|
||||
top.erase(top.begin());
|
||||
bottom.erase(bottom.begin());
|
||||
|
||||
bottom[0] = union_(bottom[0], bottom_slice);
|
||||
// Only the requested projections exist: with
|
||||
// top_shell_layers = 0 `top` is empty and erasing its begin() was
|
||||
// undefined (found by fuzzing: a sunk, painted object crashed here).
|
||||
if (! top.empty())
|
||||
top.erase(top.begin());
|
||||
if (! bottom.empty()) {
|
||||
bottom.erase(bottom.begin());
|
||||
if (! bottom.empty())
|
||||
bottom[0] = union_(bottom[0], bottom_slice);
|
||||
}
|
||||
} else
|
||||
slice_mesh_slabs(painted, zs, volume_trafo, max_top_layers > 0 ? &top : nullptr, max_bottom_layers > 0 ? &bottom : nullptr, nullptr, throw_on_cancel_callback);
|
||||
auto merge = [](std::vector<Polygons> &&src, std::vector<Polygons> &dst) {
|
||||
@@ -2088,17 +2094,19 @@ std::vector<std::vector<ExPolygons>> segmentation_by_painting(const PrintObject
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(debug) << "Print object segmentation - Projection of painted triangles - Begin";
|
||||
// The layers were sliced in this frame (belt rotation, remap and Z lift included), and it already centers the object.
|
||||
const Transform3d object_trafo = print_object.trafo_sliced();
|
||||
for (const ModelVolume *mv : print_object.model_object()->volumes) {
|
||||
const ModelVolumeFacetsInfo facets_info = extract_facets_info(*mv);
|
||||
tbb::parallel_for(tbb::blocked_range<size_t>(1, num_facets_states), [&mv, &print_object, &facets_info, &layers, &edge_grids, &painted_lines, &painted_lines_mutex, &input_expolygons, &throw_on_cancel_callback](const tbb::blocked_range<size_t> &range) {
|
||||
tbb::parallel_for(tbb::blocked_range<size_t>(1, num_facets_states), [&mv, &object_trafo, &facets_info, &layers, &edge_grids, &painted_lines, &painted_lines_mutex, &input_expolygons, &throw_on_cancel_callback](const tbb::blocked_range<size_t> &range) {
|
||||
for (size_t extruder_idx = range.begin(); extruder_idx < range.end(); ++extruder_idx) {
|
||||
throw_on_cancel_callback();
|
||||
const indexed_triangle_set custom_facets = facets_info.facets_annotation.get_facets(*mv, EnforcerBlockerType(extruder_idx));
|
||||
if (!mv->is_model_part() || custom_facets.indices.empty())
|
||||
continue;
|
||||
|
||||
const Transform3f tr = print_object.trafo().cast<float>() * mv->get_matrix().cast<float>();
|
||||
tbb::parallel_for(tbb::blocked_range<size_t>(0, custom_facets.indices.size()), [&tr, &custom_facets, &print_object, &layers, &edge_grids, &input_expolygons, &painted_lines, &painted_lines_mutex, &extruder_idx](const tbb::blocked_range<size_t> &range) {
|
||||
const Transform3f tr = (object_trafo * mv->get_matrix()).cast<float>();
|
||||
tbb::parallel_for(tbb::blocked_range<size_t>(0, custom_facets.indices.size()), [&tr, &custom_facets, &layers, &edge_grids, &input_expolygons, &painted_lines, &painted_lines_mutex, &extruder_idx](const tbb::blocked_range<size_t> &range) {
|
||||
for (size_t facet_idx = range.begin(); facet_idx < range.end(); ++facet_idx) {
|
||||
float min_z = std::numeric_limits<float>::max();
|
||||
float max_z = std::numeric_limits<float>::lowest();
|
||||
@@ -2151,7 +2159,6 @@ std::vector<std::vector<ExPolygons>> segmentation_by_painting(const PrintObject
|
||||
|
||||
Line line_to_test(Point(scale_(line_start_f.x()), scale_(line_start_f.y())),
|
||||
Point(scale_(line_end_f.x()), scale_(line_end_f.y())));
|
||||
line_to_test.translate(-print_object.center_offset());
|
||||
|
||||
// BoundingBoxes for EdgeGrids are computed from printable regions. It is possible that the painted line (line_to_test) could
|
||||
// be outside EdgeGrid's BoundingBox, for example, when the negative volume is used on the painted area (GH #7618).
|
||||
|
||||
@@ -1839,8 +1839,10 @@ void PerimeterGenerator::process_classic()
|
||||
bool is_outer_wall_first = this->config->wall_sequence == WallSequence::OuterInner;
|
||||
if (is_outer_wall_first ||
|
||||
//BBS: always print outer wall first when there indeed has brim.
|
||||
// btLeadingEdgeOnly is an outer brim too (a belt brim at the part's first contact).
|
||||
(this->layer_id == 0 &&
|
||||
this->object_config->brim_type == BrimType::btOuterOnly &&
|
||||
(this->object_config->brim_type == BrimType::btOuterOnly ||
|
||||
this->object_config->brim_type == BrimType::btLeadingEdgeOnly) &&
|
||||
this->object_config->brim_width.value > 0))
|
||||
entities.reverse();
|
||||
// Orca: sandwich mode. Apply after 1st layer.
|
||||
|
||||
@@ -1238,7 +1238,7 @@ static std::vector<std::string> s_Preset_print_options{
|
||||
"top_surface_speed", "support_speed", "support_object_xy_distance", "support_object_first_layer_gap", "support_interface_speed",
|
||||
"bridge_speed", "internal_bridge_speed", "gap_infill_speed", "travel_speed", "travel_speed_z", "initial_layer_speed",
|
||||
"outer_wall_acceleration", "initial_layer_acceleration", "top_surface_acceleration", "default_acceleration", "skirt_type", "skirt_loops", "skirt_speed","min_skirt_length", "skirt_distance", "skirt_start_angle", "skirt_height","single_loop_draft_shield", "draft_shield",
|
||||
"brim_width", "brim_object_gap", "brim_flow_ratio", "brim_use_efc_outline", "combine_brims", "brim_type", "brim_ears_max_angle", "brim_ears_detection_length", "brim_ears_outer_only", "enable_support", "support_type", "support_threshold_angle", "support_threshold_overlap","enforce_support_layers",
|
||||
"brim_width", "leading_brim_length", "extra_brim_width", "brim_object_gap", "brim_flow_ratio", "brim_use_efc_outline", "combine_brims", "brim_type", "brim_ears_max_angle", "brim_ears_detection_length", "brim_ears_outer_only", "enable_support", "support_type", "support_threshold_angle", "support_threshold_overlap","enforce_support_layers",
|
||||
"raft_layers", "raft_first_layer_density", "raft_first_layer_expansion", "raft_contact_distance", "raft_expansion",
|
||||
"support_base_pattern", "support_base_pattern_spacing", "support_expansion", "support_style",
|
||||
// BBS
|
||||
@@ -1308,6 +1308,8 @@ static std::vector<std::string> s_Preset_print_options{
|
||||
"prime_volume",
|
||||
"prime_tower_infill_gap",
|
||||
"prime_tower_flat_ironing",
|
||||
"belt_purge_tower_width",
|
||||
"belt_purge_tower_object",
|
||||
"enable_tower_interface_features",
|
||||
"enable_tower_interface_cooldown_during_tower",
|
||||
"wipe_tower_no_sparse_layers",
|
||||
@@ -1561,8 +1563,14 @@ static std::vector<std::string> s_Preset_machine_limits_options {
|
||||
|
||||
static std::vector<std::string> s_Preset_printer_options {
|
||||
"printer_technology",
|
||||
"printable_area", "extruder_printable_area", "support_parallel_printheads", "parallel_printheads_count", "parallel_printheads_bed_exclude_areas", "bed_exclude_area","bed_custom_texture", "bed_custom_model", "gcode_flavor",
|
||||
"gcode_skip_config_block", "fan_kickstart", "part_cooling_fan_min_pwm", "fan_speedup_time", "fan_speedup_overhangs",
|
||||
"printable_area", "extruder_printable_area", "support_parallel_printheads", "parallel_printheads_count", "parallel_printheads_bed_exclude_areas", "bed_exclude_area","bed_custom_texture", "bed_custom_model", "build_plate_tilt_x", "build_plate_tilt_y", "belt_printer", "belt_printer_infinite_y",
|
||||
"belt_slice_rotation", "belt_slice_rotation_angle",
|
||||
"gcode_remap_x", "gcode_remap_y", "gcode_remap_z",
|
||||
"belt_frame_tilt_decouple", "belt_frame_tilt_angle",
|
||||
"belt_support_floor_offset",
|
||||
"enable_belt_purge_tower",
|
||||
"gcode_flavor", "gcode_skip_config_block",
|
||||
"fan_kickstart", "part_cooling_fan_min_pwm", "fan_speedup_time", "fan_speedup_overhangs",
|
||||
"single_extruder_multi_material", "manual_filament_change", "file_start_gcode", "machine_start_gcode", "machine_end_gcode", "before_layer_change_gcode", "printing_by_object_gcode", "layer_change_gcode", "time_lapse_gcode", "wrapping_detection_gcode", "change_filament_gcode", "change_extrusion_role_gcode",
|
||||
"printer_model", "printer_variant", "printer_extruder_id", "printer_extruder_variant", "extruder_variant_list", "default_nozzle_volume_type",
|
||||
"printable_height", "extruder_printable_height", "extruder_clearance_radius", "extruder_clearance_height_to_lid", "extruder_clearance_height_to_rod", "extruder_clearance_dist_to_rod",
|
||||
|
||||
+314
-50
@@ -71,6 +71,8 @@
|
||||
#include "Thread.hpp"
|
||||
#include "Time.hpp"
|
||||
#include "GCode.hpp"
|
||||
#include "BeltGCode.hpp"
|
||||
#include "BeltTransform.hpp"
|
||||
#include "GCode/WipeTower.hpp"
|
||||
#include "GCode/WipeTower2.hpp"
|
||||
#include "GCode/WipeTowerEstimate.hpp"
|
||||
@@ -79,6 +81,7 @@
|
||||
#include "MaterialType.hpp"
|
||||
#include "Model.hpp"
|
||||
#include "format.hpp"
|
||||
#include "LocalesUtils.hpp"
|
||||
#include <float.h>
|
||||
|
||||
#include <algorithm>
|
||||
@@ -105,6 +108,7 @@
|
||||
#include "Format/STEP.hpp"
|
||||
#include "PlaceholderParser.hpp"
|
||||
#include "SurfaceCollection.hpp"
|
||||
#include "BeltBrim.hpp"
|
||||
|
||||
namespace fs = boost::filesystem;
|
||||
|
||||
@@ -168,6 +172,14 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n
|
||||
// Cache the plenty of parameters, which influence the G-code generator only,
|
||||
// or they are only notes not influencing the generated G-code.
|
||||
static std::unordered_set<std::string> steps_gcode = {
|
||||
// Belt printer G-code axis remap (only affects G-code output, not slicing).
|
||||
"gcode_remap_x",
|
||||
"gcode_remap_y",
|
||||
"gcode_remap_z",
|
||||
// Machine-frame transform (derived from belt tilt; only affects G-code output).
|
||||
"belt_frame_tilt_decouple", "belt_frame_tilt_angle",
|
||||
// Only inflates the GUI bed volume, like printable_area.
|
||||
"belt_printer_infinite_y",
|
||||
//BBS
|
||||
"additional_cooling_fan_speed",
|
||||
"reduce_crossing_wall",
|
||||
@@ -369,8 +381,18 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n
|
||||
// Spiral Vase forces different kind of slicing than the normal model:
|
||||
// In Spiral Vase mode, holes are closed and only the largest area contour is kept at each layer.
|
||||
// Therefore toggling the Spiral Vase on / off requires complete reslicing.
|
||||
|| opt_key == "spiral_mode") {
|
||||
|| opt_key == "spiral_mode"
|
||||
// Build plate tilt changes slicing plane orientation.
|
||||
|| opt_key == "build_plate_tilt_x"
|
||||
|| opt_key == "build_plate_tilt_y"
|
||||
// Belt printer transform options change the mesh geometry before slicing.
|
||||
|| opt_key == "belt_printer"
|
||||
|| opt_key == "belt_slice_rotation"
|
||||
|| opt_key == "belt_slice_rotation_angle") {
|
||||
osteps.emplace_back(posSlice);
|
||||
} else if (
|
||||
opt_key == "belt_support_floor_offset") {
|
||||
osteps.emplace_back(posSupportMaterial);
|
||||
} else if (
|
||||
opt_key == "print_sequence"
|
||||
|| opt_key == "filament_type"
|
||||
@@ -405,6 +427,7 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n
|
||||
|| opt_key == "hot_plate_temp"
|
||||
|| opt_key == "textured_plate_temp"
|
||||
|| opt_key == "enable_prime_tower"
|
||||
|| opt_key == "enable_belt_purge_tower"
|
||||
|| opt_key == "enable_wrapping_detection"
|
||||
|| opt_key == "prime_tower_enable_framework"
|
||||
|| opt_key == "prime_tower_width"
|
||||
@@ -439,6 +462,7 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n
|
||||
|| opt_key == "prime_volume"
|
||||
|| opt_key == "flush_into_infill"
|
||||
|| opt_key == "flush_into_support"
|
||||
|| opt_key == "belt_purge_tower_width"
|
||||
|| opt_key == "initial_layer_infill_speed"
|
||||
|| opt_key == "travel_speed"
|
||||
|| opt_key == "travel_speed_z"
|
||||
@@ -676,6 +700,9 @@ std::vector<ObjectID> Print::print_object_ids() const
|
||||
|
||||
bool Print::has_infinite_skirt() const
|
||||
{
|
||||
// Belt printer: no skirt support.
|
||||
if (m_config.belt_printer.value)
|
||||
return false;
|
||||
// Orca: unclear why (m_config.ooze_prevention && this->extruders().size() > 1) logic is here, removed.
|
||||
// return (m_config.draft_shield == dsEnabled && m_config.skirt_loops > 0) || (m_config.ooze_prevention && this->extruders().size() > 1);
|
||||
|
||||
@@ -684,6 +711,9 @@ bool Print::has_infinite_skirt() const
|
||||
|
||||
bool Print::has_skirt() const
|
||||
{
|
||||
// Belt printer: no skirt support.
|
||||
if (m_config.belt_printer.value)
|
||||
return false;
|
||||
return (m_config.skirt_height > 0);
|
||||
}
|
||||
|
||||
@@ -692,6 +722,24 @@ bool Print::has_brim() const
|
||||
return std::any_of(m_objects.begin(), m_objects.end(), [](PrintObject *object) { return object->has_brim(); });
|
||||
}
|
||||
|
||||
bool Print::has_tilted_belt() const
|
||||
{
|
||||
if (! m_config.belt_printer.value)
|
||||
return false;
|
||||
// A Z rotation leaves the belt floor flat (BeltTransform forces shear = 0) and no
|
||||
// rotation at all means the machine is geometrically a flat bed.
|
||||
const BeltRotationAxis axis = m_config.belt_slice_rotation.value;
|
||||
if (axis != BeltRotationAxis::X && axis != BeltRotationAxis::Y)
|
||||
return false;
|
||||
const double tilt = std::abs(m_config.belt_slice_rotation_angle.value);
|
||||
return tilt >= BELT_BRIM_MIN_TILT_DEG && tilt <= BELT_BRIM_MAX_TILT_DEG;
|
||||
}
|
||||
|
||||
bool Print::has_belt_brim() const
|
||||
{
|
||||
return std::any_of(m_objects.begin(), m_objects.end(), [](PrintObject *object) { return object->has_belt_brim(); });
|
||||
}
|
||||
|
||||
//BBS
|
||||
std::vector<size_t> Print::layers_sorted_for_object(float start, float end, std::vector<LayerPtrs> &layers_of_objects, std::vector<BoundingBox> &boundingBox_for_objects, VecOfPoints &objects_instances_shift)
|
||||
{
|
||||
@@ -1937,6 +1985,75 @@ StringObjectException Print::validate(std::vector<StringObjectException> *warnin
|
||||
}
|
||||
}
|
||||
|
||||
// Belt printer validation: incompatible features.
|
||||
if (m_config.belt_printer.value) {
|
||||
for (const PrintObject *object : m_objects) {
|
||||
if (object->config().raft_layers > 0)
|
||||
return { L("Raft is not compatible with belt printer mode.") };
|
||||
}
|
||||
if (m_config.draft_shield != dsDisabled)
|
||||
return { L("Draft shield is not compatible with belt printer mode.") };
|
||||
|
||||
// Belt brim spans many layers and owns the layers below the object, which
|
||||
// spiral vase cannot share. The prime tower setting is no obstacle: belt
|
||||
// printers never print the classic tower, and the belt purge prism is an
|
||||
// ordinary object that never takes a brim.
|
||||
if (this->has_belt_brim()) {
|
||||
if (m_config.spiral_mode.value)
|
||||
return { L("Brim is not compatible with spiral vase mode on a belt printer. "
|
||||
"Disable one of them.") };
|
||||
}
|
||||
|
||||
for (const PrintObject *object : m_objects) {
|
||||
const PrintObjectConfig &ocfg = object->config();
|
||||
// Mirror PrintObject::has_belt_brim(): an inner-only brim needs a positive
|
||||
// brim_width (leading/extra widen only the outer ring), so keep this
|
||||
// predicate in step or the belt-brim warnings below would fire for a brim
|
||||
// that has_belt_brim() rejects.
|
||||
const bool wants_brim = ocfg.brim_type != btNoBrim
|
||||
&& (ocfg.brim_type == btInnerOnly
|
||||
? ocfg.brim_width.value > 0.
|
||||
: (ocfg.brim_width.value > 0. || ocfg.leading_brim_length.value > 0.
|
||||
|| ocfg.extra_brim_width.value > 0.));
|
||||
if (! wants_brim)
|
||||
continue;
|
||||
|
||||
if (! this->has_tilted_belt()) {
|
||||
if (std::abs(m_config.belt_slice_rotation_angle.value) > BELT_BRIM_MAX_TILT_DEG)
|
||||
warn(L("The belt is too steep for a brim, so no brim will be generated."),
|
||||
"brim_width", object->model_object());
|
||||
else
|
||||
warn(L("A brim is only generated when the belt is tilted. Set a belt tilt angle, "
|
||||
"or remove the brim setting."),
|
||||
"brim_type", object->model_object());
|
||||
}
|
||||
|
||||
if (ocfg.brim_type == btAutoBrim || ocfg.brim_type == btEar || ocfg.brim_type == btPainted)
|
||||
warn(L("Belt printers support outer and inner brim only. Auto, Mouse ear and Painted "
|
||||
"brim are printed as Outer brim only, using Brim width."),
|
||||
"brim_type", object->model_object());
|
||||
|
||||
if (ocfg.leading_brim_length.value > 0. && ocfg.brim_object_gap.value > 0.)
|
||||
warn(L("Brim-object gap separates the leading brim from the object's leading edge, "
|
||||
"which is the edge it is meant to anchor. Set the gap to 0 when using leading "
|
||||
"brim length."),
|
||||
"brim_object_gap", object->model_object());
|
||||
}
|
||||
if (this->has_belt_brim() && m_objects.size() > 1)
|
||||
warn(L("Leading brim length extends ahead of each object along the belt, and Arrange does "
|
||||
"not reserve that space. Leave room between objects."),
|
||||
"leading_brim_length");
|
||||
} else {
|
||||
// "Leading edge only" describes where a part meets a moving belt, so it has no
|
||||
// meaning on a fixed bed. Brim.cpp prints it as an ordinary outer brim rather
|
||||
// than silently producing nothing; say so.
|
||||
for (const PrintObject *object : m_objects)
|
||||
if (object->config().brim_type == btLeadingEdgeOnly)
|
||||
warn(L("\"Leading edge only\" brim applies to belt printers. On this printer it is "
|
||||
"printed as an ordinary outer brim."),
|
||||
"brim_type", object->model_object());
|
||||
}
|
||||
|
||||
// Orca: a gradient mixed filament only renders its gradient with "Mixed color sublayer" on;
|
||||
// without it ToolOrdering::resolve_mixed_filaments prints one whole component per layer and
|
||||
// the gradient is dropped silently. extruders() already covers painting, height ranges,
|
||||
@@ -2002,6 +2119,40 @@ StringObjectException Print::validate(std::vector<StringObjectException> *warnin
|
||||
}
|
||||
}
|
||||
|
||||
if (m_config.belt_printer.value && m_config.enable_belt_purge_tower.value
|
||||
&& m_config.print_sequence == PrintSequence::ByObject
|
||||
&& extruders.size() > 1) {
|
||||
StringObjectException warningtemp;
|
||||
warningtemp.string = L("The belt purge tower is not generated in \"By object\" print sequence; "
|
||||
"filament changes will not be purged.");
|
||||
warningtemp.opt_key = "enable_belt_purge_tower";
|
||||
warningtemp.is_warning = true;
|
||||
add_warning(warningtemp);
|
||||
}
|
||||
|
||||
// The purge tower is a model object the GUI creates and sizes; libslic3r only purges
|
||||
// into one that exists. A project sliced without it (the CLI on a project saved before
|
||||
// the tower was generated) changes filament with nowhere to purge.
|
||||
if (m_config.belt_printer.value && m_config.enable_belt_purge_tower.value
|
||||
&& m_config.print_sequence != PrintSequence::ByObject
|
||||
&& ! m_config.spiral_mode.value && this->object_extruders().size() > 1 && ! this->has_belt_purge_tower()) {
|
||||
StringObjectException warningtemp;
|
||||
warningtemp.string = L("The belt purge tower is enabled but the project has no purge tower object; "
|
||||
"filament changes will not be purged. Open the project in the application "
|
||||
"to generate the tower.");
|
||||
warningtemp.opt_key = "enable_belt_purge_tower";
|
||||
warningtemp.is_warning = true;
|
||||
add_warning(warningtemp);
|
||||
}
|
||||
|
||||
if (m_config.belt_printer.value && m_config.enable_belt_purge_tower.value) {
|
||||
const size_t prism_count = std::count_if(m_objects.begin(), m_objects.end(), [](const PrintObject *object) {
|
||||
return object->config().belt_purge_tower_object.value;
|
||||
});
|
||||
if (prism_count > 1)
|
||||
return {L("The project contains multiple managed belt purge towers. Reload the plate or toggle the belt purge tower off and on to regenerate it.")};
|
||||
}
|
||||
|
||||
if (m_config.enable_prime_tower) {
|
||||
for (const PrintObject* object : m_objects) {
|
||||
if (object->config().precise_z_height.value) {
|
||||
@@ -2055,35 +2206,57 @@ StringObjectException Print::validate(std::vector<StringObjectException> *warnin
|
||||
return profile;
|
||||
};
|
||||
|
||||
// Checks that the print does not exceed the max print height
|
||||
// Checks that the print does not exceed the max print height.
|
||||
// For belt printers the slicing-frame Z spans the sheared X-length and
|
||||
// is not comparable to printable_height (which is gantry clearance in the
|
||||
// build-volume frame). Compare against the model's pre-shear Z instead,
|
||||
// mirroring the bbox computed in PrintObject::update_slicing_parameters.
|
||||
// The machine-frame transform only changes how that height is written to
|
||||
// G-code, not how much room there is under the gantry.
|
||||
const bool belt_printer = this->config().belt_printer.value;
|
||||
const double shrinkage_compensation_z = this->shrinkage_compensation().z();
|
||||
for (size_t print_object_idx = 0; print_object_idx < m_objects.size(); ++ print_object_idx) {
|
||||
const PrintObject &print_object = *m_objects[print_object_idx];
|
||||
//FIXME It is quite expensive to generate object layers just to get the print height!
|
||||
if (auto layers = generate_object_layers(print_object.slicing_parameters(), layer_height_profile(print_object_idx), print_object.config().precise_z_height.value);
|
||||
!layers.empty()) {
|
||||
|
||||
Vec3d test =this->shrinkage_compensation();
|
||||
const double shrinkage_compensation_z = this->shrinkage_compensation().z();
|
||||
|
||||
if (shrinkage_compensation_z != 1. && layers.back() > (this->config().printable_height / shrinkage_compensation_z + EPSILON)) {
|
||||
// The object exceeds the maximum build volume height because of shrinkage compensation.
|
||||
return StringObjectException{
|
||||
Slic3r::format(_u8L("While the object %1% itself fits the build volume, it exceeds the maximum build volume height because of material shrinkage compensation."), print_object.model_object()->name),
|
||||
print_object.model_object(),
|
||||
""
|
||||
};
|
||||
} else if (layers.back() > this->config().printable_height + EPSILON) {
|
||||
// Test whether the last slicing plane is below or above the print volume.
|
||||
return StringObjectException{
|
||||
0.5 * (layers[layers.size() - 2] + layers.back()) > this->config().printable_height + EPSILON ?
|
||||
Slic3r::format(_u8L("The object %1% exceeds the maximum build volume height."), print_object.model_object()->name) :
|
||||
Slic3r::format(_u8L("While the object %1% itself fits the build volume, its last layer exceeds the maximum build volume height."), print_object.model_object()->name) +
|
||||
" " + _u8L("You might want to reduce the size of your model or change current print settings and retry."),
|
||||
print_object.model_object(),
|
||||
""
|
||||
};
|
||||
double effective_max_z = 0;
|
||||
bool last_layer_below_max = false;
|
||||
bool have_height = false;
|
||||
|
||||
if (belt_printer) {
|
||||
const double raw_z = print_object.model_object()->max_z();
|
||||
effective_max_z = raw_z;
|
||||
have_height = raw_z > 0;
|
||||
} else {
|
||||
//FIXME It is quite expensive to generate object layers just to get the print height!
|
||||
auto layers = generate_object_layers(print_object.slicing_parameters(), layer_height_profile(print_object_idx), print_object.config().precise_z_height.value);
|
||||
if (!layers.empty()) {
|
||||
effective_max_z = layers.back();
|
||||
last_layer_below_max = layers.size() >= 2 &&
|
||||
0.5 * (layers[layers.size() - 2] + layers.back()) <= this->config().printable_height + EPSILON;
|
||||
have_height = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!have_height)
|
||||
continue;
|
||||
|
||||
if (shrinkage_compensation_z != 1. && effective_max_z > (this->config().printable_height / shrinkage_compensation_z + EPSILON)) {
|
||||
// The object exceeds the maximum build volume height because of shrinkage compensation.
|
||||
return StringObjectException{
|
||||
Slic3r::format(_u8L("While the object %1% itself fits the build volume, it exceeds the maximum build volume height because of material shrinkage compensation."), print_object.model_object()->name),
|
||||
print_object.model_object(),
|
||||
""
|
||||
};
|
||||
} else if (effective_max_z > this->config().printable_height + EPSILON) {
|
||||
return StringObjectException{
|
||||
last_layer_below_max ?
|
||||
Slic3r::format(_u8L("While the object %1% itself fits the build volume, its last layer exceeds the maximum build volume height."), print_object.model_object()->name) +
|
||||
" " + _u8L("You might want to reduce the size of your model or change current print settings and retry.") :
|
||||
Slic3r::format(_u8L("The object %1% exceeds the maximum build volume height."), print_object.model_object()->name),
|
||||
print_object.model_object(),
|
||||
""
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Some of the objects has variable layer height applied by painting or by a table.
|
||||
@@ -2103,12 +2276,12 @@ StringObjectException Print::validate(std::vector<StringObjectException> *warnin
|
||||
return {_u8L("Variable layer height is not supported with Organic supports.") };
|
||||
}
|
||||
|
||||
if (this->has_wipe_tower() && ! m_objects.empty()) {
|
||||
if ((this->has_wipe_tower() || this->has_belt_purge_tower()) && ! m_objects.empty()) {
|
||||
// Orca: wipe_tower_filament (issue #10971) is inserted into the tool order after
|
||||
// resolve_mixed_filaments has expanded every mixed (virtual) slot, so a mixed slot here
|
||||
// would reach the G-code as a tool change to a slot no nozzle carries. The GUI hides
|
||||
// mixed slots from the option; this guards loaded projects and the CLI.
|
||||
if (m_config.wipe_tower_filament > 0) {
|
||||
if (this->has_wipe_tower() && m_config.wipe_tower_filament > 0) {
|
||||
const auto &is_mixed = m_config.filament_is_mixed.values;
|
||||
const size_t wipe_idx = size_t(m_config.wipe_tower_filament - 1);
|
||||
if (wipe_idx < is_mixed.size() && is_mixed[wipe_idx])
|
||||
@@ -2130,12 +2303,17 @@ StringObjectException Print::validate(std::vector<StringObjectException> *warnin
|
||||
}
|
||||
}
|
||||
|
||||
if (! m_config.use_relative_e_distances)
|
||||
return { L("The Wipe Tower is currently only supported with the relative extruder addressing (use_relative_e_distances=1).") };
|
||||
// The following two constraints come from the classic wipe tower G-code
|
||||
// generator; purging into the belt purge prism uses normal object
|
||||
// extrusions and does not need them.
|
||||
if (this->has_wipe_tower()) {
|
||||
if (! m_config.use_relative_e_distances)
|
||||
return { L("The Wipe Tower is currently only supported with the relative extruder addressing (use_relative_e_distances=1).") };
|
||||
|
||||
if (m_config.ooze_prevention && m_config.single_extruder_multi_material)
|
||||
return {L("Ooze prevention is only supported with the wipe tower when 'single_extruder_multi_material' is off.")};
|
||||
}
|
||||
|
||||
if (m_config.ooze_prevention && m_config.single_extruder_multi_material)
|
||||
return {L("Ooze prevention is only supported with the wipe tower when 'single_extruder_multi_material' is off.")};
|
||||
|
||||
#if 0
|
||||
if (m_config.gcode_flavor != gcfRepRapSprinter && m_config.gcode_flavor != gcfRepRapFirmware &&
|
||||
m_config.gcode_flavor != gcfRepetier && m_config.gcode_flavor != gcfMarlinLegacy && m_config.gcode_flavor != gcfMarlinFirmware)
|
||||
@@ -2862,8 +3040,28 @@ BoundingBox PrintObject::get_first_layer_bbox(float& a, float& layer_height, std
|
||||
a += area(slice);
|
||||
}
|
||||
}
|
||||
if (has_brim())
|
||||
// Guard on `defined`: make_brim() can return before assigning this (it does on
|
||||
// belt printers, where has_brim() is still true but the plate brim is skipped),
|
||||
// and overwriting a valid bbox with an undefined one corrupted the first-layer
|
||||
// centre and the GUI's first-layer area readout.
|
||||
if (has_brim() && firstLayerObjectBrimBoundingBox.defined)
|
||||
bbox = firstLayerObjectBrimBoundingBox;
|
||||
// Belt brim: the apron reaches ahead of the object along the belt.
|
||||
if (has_belt_brim()) {
|
||||
const Point shift = instances().empty() ? Point(0, 0) : instances()[0].shift_without_plate_offset();
|
||||
for (const ExPolygons &areas : m_belt_brim_areas_by_layer)
|
||||
for (const ExPolygon &ex : areas) {
|
||||
BoundingBox bb = get_extents(ex.contour);
|
||||
bb.translate(shift.x(), shift.y());
|
||||
bbox.merge(bb);
|
||||
}
|
||||
for (const BeltBrimBand &band : m_belt_brim_prologue)
|
||||
for (const ExPolygon &ex : band.areas) {
|
||||
BoundingBox bb = get_extents(ex.contour);
|
||||
bb.translate(shift.x(), shift.y());
|
||||
bbox.merge(bb);
|
||||
}
|
||||
}
|
||||
return bbox;
|
||||
}
|
||||
|
||||
@@ -2914,6 +3112,19 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
|
||||
if (m_objects.empty())
|
||||
return;
|
||||
|
||||
// Belt purge prism: _plan_belt_purge() (psWipeTower) truncates the prism's
|
||||
// layers and drops its unclaimed fills, stashing both so a replan can undo
|
||||
// them. The object steps below regenerate per-layer content over m_layers
|
||||
// ONLY, so if any of them is about to rerun the stashes must go back first;
|
||||
// otherwise truncated layers keep stale perimeters/fills and dropped fills
|
||||
// are re-inserted next to freshly generated ones. Every object-step
|
||||
// invalidation also invalidates psWipeTower, so "psWipeTower not done" is
|
||||
// exactly "some object step may rerun" -- and when it IS done nothing below
|
||||
// regenerates, and the plan's edits have to stay.
|
||||
if (!this->is_step_done(psWipeTower))
|
||||
for (PrintObject *obj : m_objects)
|
||||
obj->belt_undo_purge_plan();
|
||||
|
||||
{
|
||||
LifecycleEventContext ctx;
|
||||
ctx.id = std::to_string(m_model.id().id);
|
||||
@@ -2975,15 +3186,20 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
|
||||
int object_count = m_objects.size();
|
||||
std::set<PrintObject*> need_slicing_objects;
|
||||
std::set<PrintObject*> re_slicing_objects;
|
||||
// Belt global modes couple each object's bed position into its layer Z values,
|
||||
// so sharing layers between "identical" objects is wrong.
|
||||
bool belt_no_share = m_config.belt_printer.value;
|
||||
if (!use_cache) {
|
||||
for (int index = 0; index < object_count; index++)
|
||||
{
|
||||
PrintObject *obj = m_objects[index];
|
||||
for (PrintObject *slicing_obj : need_slicing_objects)
|
||||
{
|
||||
if (is_print_object_the_same(obj, slicing_obj)) {
|
||||
obj->set_shared_object(slicing_obj);
|
||||
break;
|
||||
if (!belt_no_share) {
|
||||
for (PrintObject *slicing_obj : need_slicing_objects)
|
||||
{
|
||||
if (is_print_object_the_same(obj, slicing_obj)) {
|
||||
obj->set_shared_object(slicing_obj);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!obj->get_shared_object())
|
||||
@@ -3002,12 +3218,14 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
|
||||
PrintObject *obj = m_objects[index];
|
||||
bool found_shared = false;
|
||||
if (need_slicing_objects.find(obj) == need_slicing_objects.end()) {
|
||||
for (PrintObject *slicing_obj : need_slicing_objects)
|
||||
{
|
||||
if (is_print_object_the_same(obj, slicing_obj)) {
|
||||
obj->set_shared_object(slicing_obj);
|
||||
found_shared = true;
|
||||
break;
|
||||
if (!belt_no_share) {
|
||||
for (PrintObject *slicing_obj : need_slicing_objects)
|
||||
{
|
||||
if (is_print_object_the_same(obj, slicing_obj)) {
|
||||
obj->set_shared_object(slicing_obj);
|
||||
found_shared = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!found_shared) {
|
||||
@@ -3130,7 +3348,8 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
|
||||
for (int i = range.begin(); i < range.end(); i++) {
|
||||
PrintObject* obj = m_objects[i];
|
||||
if (need_slicing_objects.count(obj) != 0) {
|
||||
obj->generate_support_material();
|
||||
// The belt brim follows sequentially below.
|
||||
obj->generate_support_material(false);
|
||||
}
|
||||
else {
|
||||
if (obj->set_started(posSupportMaterial))
|
||||
@@ -3139,6 +3358,10 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
|
||||
}
|
||||
}
|
||||
);
|
||||
// The belt brim keeps clear of every object's layers and support layers,
|
||||
// so it runs once no support step is rebuilding them any more.
|
||||
for (PrintObject *obj : m_objects)
|
||||
obj->generate_belt_brim();
|
||||
|
||||
if (m_pipeline_plugin_active)
|
||||
for (size_t i = 0; i < m_objects.size(); ++i)
|
||||
@@ -3215,7 +3438,10 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
|
||||
|
||||
m_wipe_tower_data.clear();
|
||||
m_tool_ordering.clear();
|
||||
if (this->has_wipe_tower()) {
|
||||
if (this->has_belt_purge_tower() && this->config().print_sequence != PrintSequence::ByObject) {
|
||||
this->_plan_belt_purge();
|
||||
}
|
||||
else if (this->has_wipe_tower()) {
|
||||
this->_make_wipe_tower();
|
||||
}
|
||||
else if (this->config().print_sequence != PrintSequence::ByObject) {
|
||||
@@ -3469,6 +3695,26 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
|
||||
}
|
||||
|
||||
|
||||
// Belt brim: bound the first-layer convex hull by the lowest apron band, so
|
||||
// bed levelling and the initial purge line account for brim that reaches
|
||||
// ahead of every object.
|
||||
if (this->has_belt_brim()) {
|
||||
for (PrintObject *object : m_objects) {
|
||||
if (! object->has_belt_brim() || object->belt_brim_prologue().empty())
|
||||
continue;
|
||||
const BeltBrimBand &lowest = object->belt_brim_prologue().front();
|
||||
for (const PrintInstance &instance : object->instances())
|
||||
for (const ExPolygon &ex : lowest.areas) {
|
||||
Polygon poly = ex.contour;
|
||||
poly.translate(instance.shift);
|
||||
append(m_first_layer_convex_hull.points, std::move(poly.points));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Unchanged for belt printers: _make_skirt() already returns early for them, and
|
||||
// the belt brim does not populate m_brimMapByInstance, which is what the
|
||||
// skirt/brim grouping reads.
|
||||
if (has_skirt() || has_infinite_skirt() || has_brim()) {
|
||||
// Generate skirt/brim groups after brim so per-object and draft-shield footprints
|
||||
// include brims when grouping and offsetting skirt loops.
|
||||
@@ -3572,7 +3818,12 @@ std::string Print::export_gcode(const std::string& path_template, GCodeProcessor
|
||||
this->set_status(80, message);
|
||||
|
||||
// The following line may die for multiple reasons.
|
||||
GCode gcode;
|
||||
// Factory: use BeltGCode for belt printers, plain GCode otherwise.
|
||||
std::unique_ptr<GCode> gcode;
|
||||
if (m_config.belt_printer.value)
|
||||
gcode = std::make_unique<BeltGCode>();
|
||||
else
|
||||
gcode = std::make_unique<GCode>();
|
||||
//BBS: compute plate offset for gcode-generator
|
||||
const Vec3d origin = this->get_plate_origin();
|
||||
// IMEX firmware-managed zones: writer offset gets the IMEX shift so emitted gcode is
|
||||
@@ -3584,9 +3835,9 @@ std::string Print::export_gcode(const std::string& path_template, GCodeProcessor
|
||||
// still emits in the frame the current config asks for.
|
||||
this->update_imex_slice_offset();
|
||||
const Vec2d imex_off = this->get_imex_slice_offset();
|
||||
gcode.set_gcode_offset_with_imex_shift(origin(0), origin(1), imex_off.x(), imex_off.y());
|
||||
gcode.do_export(this, path.c_str(), result, thumbnail_cb);
|
||||
gcode.export_layer_filaments(result);
|
||||
gcode->set_gcode_offset_with_imex_shift(origin(0), origin(1), imex_off.x(), imex_off.y());
|
||||
gcode->do_export(this, path.c_str(), result, thumbnail_cb);
|
||||
gcode->export_layer_filaments(result);
|
||||
//BBS
|
||||
if (result != nullptr) {
|
||||
result->conflict_result = m_conflict_result;
|
||||
@@ -3601,6 +3852,10 @@ std::string Print::export_gcode(const std::string& path_template, GCodeProcessor
|
||||
|
||||
void Print::_make_skirt()
|
||||
{
|
||||
// Belt printer: skirt is not compatible.
|
||||
if (m_config.belt_printer.value)
|
||||
return;
|
||||
|
||||
const bool generate_skirt = this->has_skirt() || this->has_infinite_skirt();
|
||||
|
||||
// First off we need to decide how tall the skirt must be.
|
||||
@@ -4695,6 +4950,13 @@ int Print::get_config_index(int filament_id, int layer_id, const std::vector<std
|
||||
// Wipe tower support.
|
||||
bool Print::has_wipe_tower() const
|
||||
{
|
||||
// Belt printers never get the classic wipe tower: its G-code is generated
|
||||
// directly in machine XY coordinates and bypasses the belt rotation
|
||||
// transform. Purging is routed into the belt purge prism instead
|
||||
// (see has_belt_purge_tower() / _plan_belt_purge()).
|
||||
if (m_config.belt_printer.value)
|
||||
return false;
|
||||
|
||||
if (m_config.enable_prime_tower.value == true) {
|
||||
if (m_config.enable_wrapping_detection.value && m_config.wrapping_exclude_area.values.size() > 2)
|
||||
return true;
|
||||
@@ -4707,6 +4969,7 @@ bool Print::has_wipe_tower() const
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
const WipeTowerData &Print::wipe_tower_data(size_t filaments_cnt) const
|
||||
{
|
||||
// Until the tower is generated, size it with the estimate the GUI/CLI placement uses, so
|
||||
@@ -4736,6 +4999,7 @@ bool Print::enable_timelapse_print() const
|
||||
return m_config.timelapse_type.value == TimelapseType::tlSmooth;
|
||||
}
|
||||
|
||||
|
||||
void Print::_make_wipe_tower()
|
||||
{
|
||||
m_wipe_tower_data.clear();
|
||||
|
||||
+118
-2
@@ -27,6 +27,8 @@
|
||||
#include "GCode/ThumbnailData.hpp"
|
||||
#include "GCode/GCodeProcessor.hpp"
|
||||
#include "MultiMaterialSegmentation.hpp"
|
||||
#include "BeltBrim.hpp"
|
||||
#include "BeltTransform.hpp"
|
||||
#include "ObjectID.hpp"
|
||||
#include "TriangleSelector.hpp"
|
||||
#include "libslic3r.h"
|
||||
@@ -374,6 +376,9 @@ public:
|
||||
// Trafo with the center_offset() applied after the transformation, to center the object in XY before slicing.
|
||||
Transform3d trafo_centered() const
|
||||
{ Transform3d t = this->trafo(); t.pretranslate(Vec3d(- unscale<double>(m_center_offset.x()), - unscale<double>(m_center_offset.y()), 0)); return t; }
|
||||
// trafo_centered() with the belt pre-slice transforms applied: the frame the layers were sliced in (Layer::slice_z).
|
||||
// Equal to trafo_centered() unless a belt rotation or pre-slice remap is active.
|
||||
Transform3d trafo_sliced() const;
|
||||
const PrintInstances& instances() const { return m_instances; }
|
||||
// Orca: Bounding box of each connected body, indexed by Layer::lslices_separated_component_ids.
|
||||
const std::vector<BoundingBox>& separated_body_bboxes() const { return m_separated_body_bboxes; }
|
||||
@@ -415,6 +420,26 @@ public:
|
||||
&& ! this->has_raft();
|
||||
}
|
||||
|
||||
// Belt brim. A tilted belt needs its brim laid onto the belt plane over many
|
||||
// layers instead of as one flat first-layer ring, so it is generated by
|
||||
// BeltBrim.cpp and stored per layer here. Deliberately separate from
|
||||
// has_brim(): that predicate feeds PrintRegion extruder collection, support
|
||||
// trimming and the spiral vase probe, and widening it would perturb belt
|
||||
// support output.
|
||||
bool has_belt_brim() const;
|
||||
// Brim filament for this object's belt brim, returned in the 1-based domain of
|
||||
// PrintRegion::outer_wall_filament_id and the raw values pushed into
|
||||
// LayerTools::extruders in ToolOrdering::collect_extruders (ToolOrdering reindexes
|
||||
// the whole list to 0-based afterwards). Lowest positive outer_wall_filament_id
|
||||
// over the printing regions, 1 if none is explicitly set.
|
||||
unsigned int belt_brim_filament() const;
|
||||
const std::vector<ExtrusionEntityCollection>& belt_brim_by_layer() const { return m_belt_brim_by_layer; }
|
||||
const std::vector<BeltBrimBand>& belt_brim_prologue() const { return m_belt_brim_prologue; }
|
||||
void clear_belt_brim();
|
||||
void set_belt_brim(std::vector<ExtrusionEntityCollection> &&by_layer,
|
||||
std::vector<ExPolygons> &&areas,
|
||||
std::vector<BeltBrimBand> &&prologue);
|
||||
|
||||
// BBS
|
||||
const ExtrusionEntityCollection& object_skirt() const {
|
||||
return m_skirt;
|
||||
@@ -550,7 +575,13 @@ private:
|
||||
void ironing();
|
||||
bool need_z_contouring() const;
|
||||
void contour_z();
|
||||
void generate_support_material();
|
||||
// with_belt_brim = false leaves the belt brim to generate_belt_brim(), for a
|
||||
// caller that runs the support step of several objects in parallel.
|
||||
void generate_support_material(bool with_belt_brim = true);
|
||||
// The belt brim keeps clear of every object's layers and support layers, so
|
||||
// it has to run after all support steps finished. A no-op unless a support
|
||||
// step left it pending.
|
||||
void generate_belt_brim();
|
||||
void estimate_curled_extrusions();
|
||||
void simplify_extrusion_path();
|
||||
|
||||
@@ -566,6 +597,39 @@ private:
|
||||
std::vector<std::set<int>> detect_extruder_geometric_unprintables() const;
|
||||
|
||||
void slice_volumes();
|
||||
// Belt mode: shift the sliced layer grid (layer print_z and the belt floor
|
||||
// clipping shift) by delta, used by Print::_align_belt_purge_layers() to
|
||||
// snap the purge prism's layers onto the printed objects' layer grid.
|
||||
void belt_shift_layer_grid(double delta);
|
||||
// Belt mode: remove layers above z (cancel the purge prism past the last
|
||||
// toolchange). Returns how many layers were dropped.
|
||||
size_t belt_truncate_layers_above(coordf_t z);
|
||||
// Restore layers removed by belt_truncate_layers_above() before replanning.
|
||||
// Wipe-tower-only invalidations do not necessarily reslice the object, so
|
||||
// truncation must be reversible when later toolchanges move upward.
|
||||
void belt_restore_truncated_layers();
|
||||
// Belt purge prism, plastic saving: drop the fills on one layer that no
|
||||
// toolchange claimed. `claimed` reports whether an entity was overridden as
|
||||
// purge; everything else on that layer would otherwise print as solid infill
|
||||
// in the prism's own filament for nothing. Perimeters are never touched, so
|
||||
// the bar keeps a continuous wall along the belt.
|
||||
//
|
||||
// Entities are STASHED, not deleted, with their original positions -- the
|
||||
// same reversibility contract belt_truncate_layers_above() has, and the
|
||||
// reason the original version of this had to be removed: psWipeTower can
|
||||
// rerun without regenerating infill, and a later tool ordering may claim what
|
||||
// this one did not. Returns the number of entities dropped.
|
||||
size_t belt_drop_unclaimed_fills(Layer *layer, const std::function<bool(const ExtrusionEntity*)> &claimed);
|
||||
// Put every stashed fill back at its original index. Must run before a replan.
|
||||
void belt_restore_dropped_fills();
|
||||
// Undo every edit _plan_belt_purge() made to this object's layers, leaving
|
||||
// m_layers exactly as the object steps produced it. Fills first: they point
|
||||
// into layers that are still live, and truncated layers were stashed whole
|
||||
// with their own fills untouched, so the two stashes never share an entity.
|
||||
// Print::process() calls this before any object step may rerun (those steps
|
||||
// regenerate per-layer content over m_layers only, so a stale stash would
|
||||
// otherwise be restored on top of fresh content); the plan calls it too.
|
||||
void belt_undo_purge_plan() { belt_restore_dropped_fills(); belt_restore_truncated_layers(); }
|
||||
//BBS
|
||||
ExPolygons _shrink_contour_holes(double contour_delta, double hole_delta, const ExPolygons& polys) const;
|
||||
// BBS
|
||||
@@ -608,7 +672,24 @@ private:
|
||||
|
||||
SlicingParameters m_slicing_params;
|
||||
LayerPtrs m_layers;
|
||||
LayerPtrs m_belt_truncated_layers;
|
||||
// Fills removed by belt_drop_unclaimed_fills(), owned by this vector until
|
||||
// restored or until clear_layers() deletes them. An entity is in exactly one
|
||||
// of the live collection or this stash, never both.
|
||||
struct BeltDroppedFill {
|
||||
Layer *layer { nullptr };
|
||||
size_t region_idx { 0 };
|
||||
size_t index { 0 }; // position in the original fills.entities
|
||||
ExtrusionEntity *entity { nullptr };
|
||||
};
|
||||
std::vector<BeltDroppedFill> m_belt_dropped_fills;
|
||||
SupportLayerPtrs m_support_layers;
|
||||
// Belt brim, generated in posSupportMaterial by BeltBrim.cpp. Object-local
|
||||
// slicing frame, one entry per object layer plus a prologue of brim-only
|
||||
// bands that print below the object's first layer.
|
||||
std::vector<ExtrusionEntityCollection> m_belt_brim_by_layer;
|
||||
std::vector<ExPolygons> m_belt_brim_areas_by_layer;
|
||||
std::vector<BeltBrimBand> m_belt_brim_prologue;
|
||||
// BBS
|
||||
std::shared_ptr<TreeSupportData> m_tree_support_preview_cache;
|
||||
|
||||
@@ -628,7 +709,25 @@ private:
|
||||
|
||||
PrintObject* m_shared_object{ nullptr };
|
||||
|
||||
|
||||
// Belt printer: global Z offset applied to this object's layers for shear positioning.
|
||||
double m_belt_global_z_offset { 0.0 };
|
||||
// generate_support_material(false) finished and generate_belt_brim() has not run yet.
|
||||
bool m_belt_brim_pending { false };
|
||||
// Belt printer: min_z of mesh after belt shear (before Z-shift), for z_offset calc.
|
||||
double m_belt_min_z { 0.0 };
|
||||
// Belt printer: exact belt_floor_z_shift computed during posSlice from a
|
||||
// vertex-level scan of the post-transform mesh. Cached separately from
|
||||
// m_slicing_params so that rebuilding m_slicing_params on a non-belt-affecting
|
||||
// invalidation (e.g. support config change) doesn't replace the exact value
|
||||
// with the bbox approximation seeded by create_from_config(). Cleared when
|
||||
// posSlice is invalidated.
|
||||
double m_belt_floor_z_shift_cached { 0.0 };
|
||||
bool m_belt_floor_z_shift_cache_valid { false };
|
||||
public:
|
||||
double belt_global_z_offset() const { return m_belt_global_z_offset; }
|
||||
private:
|
||||
|
||||
|
||||
// SoftFever
|
||||
//
|
||||
// object id
|
||||
@@ -991,6 +1090,15 @@ public:
|
||||
bool has_infinite_skirt() const;
|
||||
bool has_skirt() const;
|
||||
bool has_brim() const;
|
||||
// True when the belt is tilted enough that the BELT plane, not the bed plane, is
|
||||
// the adhesion surface. The flat plate brim is geometrically meaningless then
|
||||
// and must not run, whatever the per-object brim settings say - in particular
|
||||
// brim_type "Auto", which has_brim() reports as enabled even at width 0.
|
||||
bool has_tilted_belt() const;
|
||||
// True when at least one object actually gets a tilted-belt brim generated.
|
||||
// Implies has_tilted_belt(), but additionally requires the object's own brim
|
||||
// settings to ask for one.
|
||||
bool has_belt_brim() const;
|
||||
//BBS
|
||||
bool has_auto_brim() const {
|
||||
return std::any_of(m_objects.begin(), m_objects.end(), [](PrintObject* object) { return object->config().brim_type == btAutoBrim; });
|
||||
@@ -1067,6 +1175,8 @@ public:
|
||||
|
||||
// Wipe tower support.
|
||||
bool has_wipe_tower() const;
|
||||
// Belt purge prism (belt-mode replacement for the wipe tower).
|
||||
bool has_belt_purge_tower() const;
|
||||
const WipeTowerData& wipe_tower_data(size_t filaments_cnt = 0) const;
|
||||
const ToolOrdering& tool_ordering() const { return m_tool_ordering; }
|
||||
|
||||
@@ -1337,6 +1447,12 @@ private:
|
||||
|
||||
void _make_skirt();
|
||||
void _make_wipe_tower();
|
||||
// Belt mode: route filament-change purging into the belt purge prism
|
||||
// (flush-into-objects) without generating a classic wipe tower.
|
||||
void _plan_belt_purge();
|
||||
// Belt mode: shift the purge prism's layer grid to match the printed
|
||||
// objects' grid so toolchange layers find prism layers to purge into.
|
||||
void _align_belt_purge_layers();
|
||||
void finalize_first_layer_convex_hull();
|
||||
void update_filament_self_index_cache();
|
||||
// Deduplicates, per filament, the (extruder type x volume type) variants the grouping
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "Point.hpp"
|
||||
#include "Polygon.hpp"
|
||||
#include "Print.hpp"
|
||||
#include "BeltTransform.hpp"
|
||||
#include "FilamentMixer.hpp"
|
||||
#include "Slicing.hpp"
|
||||
#include "libslic3r.h"
|
||||
@@ -187,22 +188,29 @@ struct PrintObjectTrafoAndInstances
|
||||
|
||||
// Generate a list of trafos and XY offsets for instances of a ModelObject
|
||||
// Orca: Updated to include XYZ filament shrinkage compensation
|
||||
static std::vector<PrintObjectTrafoAndInstances> print_objects_from_model_object(const ModelObject &model_object, const Vec3d &shrinkage_compensation)
|
||||
static std::vector<PrintObjectTrafoAndInstances> print_objects_from_model_object(const ModelObject &model_object, const Vec3d &shrinkage_compensation, bool force_separate_instances = false)
|
||||
{
|
||||
std::set<PrintObjectTrafoAndInstances> trafos;
|
||||
PrintObjectTrafoAndInstances trafo;
|
||||
//BBS: add useful logs for debug
|
||||
int index = 0;
|
||||
int unique_counter = 0;
|
||||
for (ModelInstance *model_instance : model_object.instances) {
|
||||
if (model_instance->is_printable()) {
|
||||
// Orca: Updated with XYZ filament shrinkage compensation
|
||||
Geometry::Transformation model_instance_transformation = model_instance->get_transformation();
|
||||
trafo.trafo = model_instance_transformation.get_matrix_with_applied_shrinkage_compensation(shrinkage_compensation);
|
||||
|
||||
|
||||
auto shift = Point::new_scale(trafo.trafo.data()[12], trafo.trafo.data()[13]);
|
||||
// Reset the XY axes of the transformation.
|
||||
trafo.trafo.data()[12] = 0;
|
||||
trafo.trafo.data()[13] = 0;
|
||||
// Belt printer global mode: prevent instance grouping so each
|
||||
// copy gets its own PrintObject with independent layer Z values.
|
||||
// Add a tiny unique perturbation to the existing Z (don't replace
|
||||
// it — the Z translation from ensure_on_bed must be preserved).
|
||||
if (force_separate_instances)
|
||||
trafo.trafo.data()[14] += 1e-10 * (++unique_counter);
|
||||
// Search or insert a trafo.
|
||||
auto it = trafos.emplace(trafo).first;
|
||||
const_cast<PrintObjectTrafoAndInstances&>(*it).instances.emplace_back(PrintInstance{ nullptr, model_instance, shift });
|
||||
@@ -1287,6 +1295,17 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", i=%1%, key=%2%")%i %changed_keys[i];
|
||||
}
|
||||
}
|
||||
// On belt printers the support tilt follows the slicing rotation. The GUI keeps the two in
|
||||
// sync, but a CLI or 3MF edit of the rotation alone would otherwise leave supports on a stale tilt.
|
||||
if (const auto *belt_opt = new_full_config.option<ConfigOptionBool>("belt_printer"); belt_opt && belt_opt->value) {
|
||||
const auto *axis_opt = new_full_config.option<ConfigOptionEnum<BeltRotationAxis>>("belt_slice_rotation");
|
||||
const auto *angle_opt = new_full_config.option<ConfigOptionFloat>("belt_slice_rotation_angle");
|
||||
if (axis_opt && angle_opt) {
|
||||
const auto tilt = BeltTransformPipeline::physical_tilt(axis_opt->value, angle_opt->value);
|
||||
new_full_config.set_key_value("build_plate_tilt_x", new ConfigOptionFloat(tilt.tilt_x_deg));
|
||||
new_full_config.set_key_value("build_plate_tilt_y", new ConfigOptionFloat(tilt.tilt_y_deg));
|
||||
}
|
||||
}
|
||||
const ConfigOption* enable_support_option = new_full_config.option("enable_support");
|
||||
if (enable_support_option && enable_support_option->getBool())
|
||||
m_support_used = true;
|
||||
@@ -1848,11 +1867,15 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
|
||||
PrintObjectPtrs print_objects_new;
|
||||
print_objects_new.reserve(std::max(m_objects.size(), m_model.objects.size()));
|
||||
bool new_objects = false;
|
||||
bool belt_instances_shifted = false;
|
||||
// Walk over all new model objects and check, whether there are matching PrintObjects.
|
||||
for (ModelObject *model_object : m_model.objects) {
|
||||
ModelObjectStatus &model_object_status = const_cast<ModelObjectStatus&>(model_object_status_db.reuse(*model_object));
|
||||
// Orca: Updated for XYZ filament shrink compensation
|
||||
model_object_status.print_instances = print_objects_from_model_object(*model_object, this->shrinkage_compensation());
|
||||
// Belt printers: force each instance into its own PrintObject so each
|
||||
// gets independent layer Z values (its bed position is folded into them).
|
||||
bool belt_force_separate = m_config.belt_printer.value;
|
||||
model_object_status.print_instances = print_objects_from_model_object(*model_object, this->shrinkage_compensation(), belt_force_separate);
|
||||
std::vector<const PrintObjectStatus*> old;
|
||||
old.reserve(print_object_status_db.count(*model_object));
|
||||
for (const PrintObjectStatus &print_object_status : print_object_status_db.get_range(*model_object))
|
||||
@@ -1900,6 +1923,7 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
|
||||
if (status != PrintBase::APPLY_STATUS_UNCHANGED) {
|
||||
size_t extruder_num = new_full_config.option<ConfigOptionFloats>("nozzle_diameter")->size();
|
||||
update_apply_status(status == PrintBase::APPLY_STATUS_INVALIDATED);
|
||||
belt_instances_shifted = true;
|
||||
}
|
||||
print_objects_new.emplace_back((*it_old)->print_object);
|
||||
const_cast<PrintObjectStatus*>(*it_old)->status = PrintObjectStatus::Reused;
|
||||
@@ -1922,6 +1946,13 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
|
||||
}
|
||||
if (new_objects || deleted_objects)
|
||||
update_apply_status(this->invalidate_steps({ psSkirtBrim, psWipeTower, psGCodeExport }));
|
||||
// A belt brim is clipped against the other objects on the plate (BeltBrim.cpp,
|
||||
// belt_brim_obstacles), and it is rebuilt with its object's support step: an
|
||||
// object that arrived or left changes every other brim owner's brim.
|
||||
if ((new_objects || deleted_objects) && m_config.belt_printer.value)
|
||||
for (PrintObject *object : m_objects)
|
||||
if (object->has_belt_brim())
|
||||
update_apply_status(object->invalidate_step(posSupportMaterial));
|
||||
if (new_objects)
|
||||
update_apply_status(false);
|
||||
print_regions_reshuffled = true;
|
||||
@@ -1935,6 +1966,14 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
|
||||
update_apply_status(object->invalidate_step(posSlice));
|
||||
}
|
||||
}
|
||||
|
||||
// Belt printer: when any object's instances shifted, re-slice every object.
|
||||
// The global Z offset follows each object's position along the belt, and the
|
||||
// belt brims are clipped against the other objects.
|
||||
if (belt_instances_shifted && m_config.belt_printer.value) {
|
||||
for (PrintObject *object : m_objects)
|
||||
update_apply_status(object->invalidate_step(posSlice));
|
||||
}
|
||||
}
|
||||
|
||||
//BBS: check the config again
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
#include "Point.hpp"
|
||||
#include "Polygon.hpp"
|
||||
#include "PrintConfigConstants.hpp"
|
||||
#include "BeltTransform.hpp"
|
||||
#include "ClipperUtils.hpp"
|
||||
#include "Config.hpp"
|
||||
#include "Geometry.hpp"
|
||||
#include "FilamentMixer.hpp"
|
||||
#include "MaterialType.hpp"
|
||||
#include "I18N.hpp"
|
||||
@@ -385,6 +387,27 @@ static t_config_enum_values s_keys_map_SlicingMode {
|
||||
};
|
||||
CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(SlicingMode)
|
||||
|
||||
static t_config_enum_values s_keys_map_BeltRotationAxis {
|
||||
{ "none", int(BeltRotationAxis::None) },
|
||||
{ "x", int(BeltRotationAxis::X) },
|
||||
{ "y", int(BeltRotationAxis::Y) },
|
||||
{ "z", int(BeltRotationAxis::Z) },
|
||||
};
|
||||
CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(BeltRotationAxis)
|
||||
|
||||
static t_config_enum_values s_keys_map_RemapAxis {
|
||||
{ "pos_x", int(RemapAxis::PosX) },
|
||||
{ "pos_y", int(RemapAxis::PosY) },
|
||||
{ "pos_z", int(RemapAxis::PosZ) },
|
||||
{ "neg_x", int(RemapAxis::NegX) },
|
||||
{ "neg_y", int(RemapAxis::NegY) },
|
||||
{ "neg_z", int(RemapAxis::NegZ) },
|
||||
{ "rev_x", int(RemapAxis::RevX) },
|
||||
{ "rev_y", int(RemapAxis::RevY) },
|
||||
{ "rev_z", int(RemapAxis::RevZ) },
|
||||
};
|
||||
CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(RemapAxis)
|
||||
|
||||
static t_config_enum_values s_keys_map_SupportMaterialPattern {
|
||||
{ "rectilinear", smpRectilinear },
|
||||
{ "rectilinear-grid", smpRectilinearGrid },
|
||||
@@ -501,6 +524,7 @@ static const t_config_enum_values s_keys_map_BrimType = {
|
||||
{"auto_brim", btAutoBrim}, // BBS
|
||||
{"brim_ears", btEar}, // Orca
|
||||
{"painted", btPainted}, // BBS
|
||||
{"leading_edge_only", btLeadingEdgeOnly}, // belt printers
|
||||
};
|
||||
CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(BrimType)
|
||||
|
||||
@@ -1964,6 +1988,45 @@ void PrintConfigDef::init_fff_params()
|
||||
def->mode = comSimple;
|
||||
def->set_default_value(new ConfigOptionFloat(0.));
|
||||
|
||||
def = this->add("leading_brim_length", coFloat);
|
||||
def->label = L("Leading brim length");
|
||||
def->category = L("Support");
|
||||
def->tooltip = L("Belt printers only. Extends the brim AHEAD of the object along the belt, on "
|
||||
"every downhill-facing edge of its contact area - both the object's first "
|
||||
"contact with the belt and any island that lands later. This apron is laid "
|
||||
"onto the belt before the object reaches it, so the leading edge has "
|
||||
"something already stuck down to hold on to.\n\n"
|
||||
"Measured on the belt surface, and added on top of Brim width: the brim "
|
||||
"reaches Brim-object gap + Leading brim length + Brim width ahead of the "
|
||||
"object. Set Brim-object gap to 0, or the apron will not touch the object it "
|
||||
"is meant to anchor.\n\n"
|
||||
"On a tilted belt each layer lays one strip of the brim, so the thickness of "
|
||||
"the resulting brim sheet is set by flow rather than by layer height. Use "
|
||||
"Brim flow ratio to tune it.\n\n"
|
||||
"Set to 0 to disable.");
|
||||
def->sidetext = L("mm"); // millimeters, CIS languages need translation
|
||||
def->min = 0;
|
||||
def->max = 100;
|
||||
def->mode = comAdvanced;
|
||||
def->set_default_value(new ConfigOptionFloat(0.));
|
||||
|
||||
def = this->add("extra_brim_width", coFloat);
|
||||
def->label = L("Extra brim width");
|
||||
def->category = L("Support");
|
||||
def->tooltip = L("Belt printers only. Widens the brim SIDEWAYS, across the belt, without "
|
||||
"extending it further ahead of or behind the object. Use it when a part needs "
|
||||
"more grip along its length than Brim width alone gives.\n\n"
|
||||
"Measured on the belt surface, and added on top of Brim width: the brim "
|
||||
"reaches Brim-object gap + Brim width + Extra brim width to either side of "
|
||||
"the object. To extend the brim ahead of the object instead, use Leading brim "
|
||||
"length.\n\n"
|
||||
"Set to 0 to disable.");
|
||||
def->sidetext = L("mm"); // millimeters, CIS languages need translation
|
||||
def->min = 0;
|
||||
def->max = 100;
|
||||
def->mode = comAdvanced;
|
||||
def->set_default_value(new ConfigOptionFloat(0.));
|
||||
|
||||
def = this->add("brim_type", coEnum);
|
||||
def->label = L("Brim type");
|
||||
def->category = L("Support");
|
||||
@@ -1977,6 +2040,7 @@ void PrintConfigDef::init_fff_params()
|
||||
def->enum_values.emplace_back("inner_only");
|
||||
def->enum_values.emplace_back("outer_and_inner");
|
||||
def->enum_values.emplace_back("no_brim");
|
||||
def->enum_values.emplace_back("leading_edge_only");
|
||||
def->enum_labels.emplace_back(L("Auto"));
|
||||
def->enum_labels.emplace_back(L("Mouse ear"));
|
||||
def->enum_labels.emplace_back(L("Painted"));
|
||||
@@ -1984,6 +2048,7 @@ void PrintConfigDef::init_fff_params()
|
||||
def->enum_labels.emplace_back(L("Inner brim only"));
|
||||
def->enum_labels.emplace_back(L("Outer and inner brim"));
|
||||
def->enum_labels.emplace_back(L("No-brim"));
|
||||
def->enum_labels.emplace_back(L("Leading edge only"));
|
||||
def->mode = comSimple;
|
||||
def->set_default_value(new ConfigOptionEnum<BrimType>(btAutoBrim));
|
||||
|
||||
@@ -7423,6 +7488,166 @@ void PrintConfigDef::init_fff_params()
|
||||
def->mode = comSimple;
|
||||
def->set_default_value(new ConfigOptionFloatOrPercent(50., true));
|
||||
|
||||
def = this->add("build_plate_tilt_x", coFloat);
|
||||
def->label = L("Build plate tilt X");
|
||||
def->category = L("Support");
|
||||
def->tooltip = L("Tilt angle of the build plate along the X axis. "
|
||||
"A positive value tilts the plate so the +X side is higher, shifting gravity toward -X and increasing overhangs on the +X side. "
|
||||
"A negative value tilts the -X side higher. Set to 0 for no X-axis tilt. "
|
||||
"In belt printer mode, this is automatically synced to the belt angle.");
|
||||
def->sidetext = u8"\u00B0";
|
||||
def->min = -89;
|
||||
def->max = 89;
|
||||
def->mode = comExpert;
|
||||
def->set_default_value(new ConfigOptionFloat(0.));
|
||||
|
||||
def = this->add("build_plate_tilt_y", coFloat);
|
||||
def->label = L("Build plate tilt Y");
|
||||
def->category = L("Support");
|
||||
def->tooltip = L("Tilt angle of the build plate along the Y axis. "
|
||||
"A positive value tilts the plate so the +Y side is higher, shifting gravity toward -Y and increasing overhangs on the +Y side. "
|
||||
"A negative value tilts the -Y side higher. Set to 0 for no Y-axis tilt.");
|
||||
def->sidetext = u8"\u00B0";
|
||||
def->min = -89;
|
||||
def->max = 89;
|
||||
def->mode = comExpert;
|
||||
def->set_default_value(new ConfigOptionFloat(0.));
|
||||
|
||||
def = this->add("belt_printer", coBool);
|
||||
def->label = L("Enable belt printing");
|
||||
def->category = L("Printable space");
|
||||
def->tooltip = L("Enable belt printer mode. Belt printers use a conveyor belt as the build surface, "
|
||||
"tilted at an angle (typically 45 degrees). The slicer will rotate the slicing plane "
|
||||
"and transform G-code coordinates for the tilted build surface.");
|
||||
def->mode = comAdvanced;
|
||||
def->set_default_value(new ConfigOptionBool(false));
|
||||
|
||||
def = this->add("belt_printer_infinite_y", coBool);
|
||||
def->label = L("Infinite Y axis");
|
||||
def->category = L("Printable space");
|
||||
def->tooltip = L("Enable infinite Y axis for belt printers. "
|
||||
"When enabled, the Y axis build volume limit is effectively removed, "
|
||||
"allowing objects of any length to be printed along the belt direction.");
|
||||
def->mode = comAdvanced;
|
||||
def->set_default_value(new ConfigOptionBool(true));
|
||||
|
||||
// Mesh rotation applied before slicing — the sole mesh-side belt transform AND
|
||||
// the single source of truth for the physical belt tilt (bed rendering, support
|
||||
// gravity tilt and bed-exclusion projection all derive their angle from this).
|
||||
def = this->add("belt_slice_rotation", coEnum);
|
||||
def->label = L("Belt tilt axis");
|
||||
def->category = L("Printable space");
|
||||
def->tooltip = L("Axis the mesh is rotated about before slicing. This is the belt "
|
||||
"printer's tilt: an isometric (no distortion) rotation that also "
|
||||
"drives bed rendering and support gravity tilt, and that the g-code "
|
||||
"back-transform inverts before the machine-frame shear/scale and remap. "
|
||||
"X is the typical gantry tilt (belt travels along Y).");
|
||||
def->enum_keys_map = &ConfigOptionEnum<BeltRotationAxis>::get_enum_values();
|
||||
def->enum_values = {"none", "x", "y", "z"};
|
||||
def->enum_labels = {L("None"), L("X"), L("Y"), L("Z")};
|
||||
def->mode = comDevelop;
|
||||
def->set_default_value(new ConfigOptionEnum<BeltRotationAxis>(BeltRotationAxis::X));
|
||||
|
||||
def = this->add("belt_slice_rotation_angle", coFloat);
|
||||
def->label = L("Belt tilt angle");
|
||||
def->category = L("Printable space");
|
||||
def->tooltip = L("Tilt angle of the belt surface, in degrees. Most belt printers use "
|
||||
"45°. Positive values rotate counter-clockwise looking down the "
|
||||
"positive tilt axis; the magnitude is also the physical belt tilt "
|
||||
"used for bed rendering and support gravity.");
|
||||
def->sidetext = L("°");
|
||||
def->min = -180.;
|
||||
def->max = 180.;
|
||||
def->mode = comAdvanced;
|
||||
def->set_default_value(new ConfigOptionFloat(45.));
|
||||
|
||||
def = this->add("belt_frame_tilt_decouple", coBool);
|
||||
def->label = L("Decouple machine-frame tilt");
|
||||
def->category = L("Printable space");
|
||||
def->tooltip = L("Expert override: set the machine-frame (g-code shear/scale) tilt angle "
|
||||
"independently of the pre-slice rotation angle. When disabled, the "
|
||||
"machine-frame transform is derived from the belt tilt angle, so a single "
|
||||
"angle drives both stages. Enable only to compensate for a machine whose "
|
||||
"physical gantry tilt differs from the slicing rotation.");
|
||||
def->mode = comExpert;
|
||||
def->set_default_value(new ConfigOptionBool(false));
|
||||
|
||||
def = this->add("belt_frame_tilt_angle", coFloat);
|
||||
def->label = L("Machine-frame tilt angle");
|
||||
def->category = L("Printable space");
|
||||
def->tooltip = L("Tilt angle (degrees) used to derive the machine-frame shear (cot) and "
|
||||
"scale (1/|sin|) applied to G-code. Only used when 'Decouple machine-frame "
|
||||
"tilt' is enabled; otherwise the belt tilt angle is used.");
|
||||
def->sidetext = L("°");
|
||||
def->min = -89.9;
|
||||
def->max = 89.9;
|
||||
def->mode = comExpert;
|
||||
def->set_default_value(new ConfigOptionFloat(45.));
|
||||
|
||||
// G-code axis remap with sign. Each field is its own row in the settings tab. The
|
||||
// labels and tooltips are literals in L() so they are extracted for translation.
|
||||
auto add_belt_remap = [this](const char *key, const std::string &label, const std::string &tooltip,
|
||||
RemapAxis default_axis, ConfigOptionMode mode) {
|
||||
auto def = this->add(key, coEnum);
|
||||
def->label = label;
|
||||
def->category = L("Printable space");
|
||||
def->tooltip = tooltip;
|
||||
def->enum_keys_map = &ConfigOptionEnum<RemapAxis>::get_enum_values();
|
||||
def->enum_values = {"pos_x", "pos_y", "pos_z", "neg_x", "neg_y", "neg_z", "rev_x", "rev_y", "rev_z"};
|
||||
def->enum_labels = {L("+X"), L("+Y"), L("+Z"), L("-X"), L("-Y"), L("-Z"), L("Rev X"), L("Rev Y"), L("Rev Z")};
|
||||
def->mode = mode; // Visibility may also be gated by toggle_line in Tab.cpp
|
||||
def->set_default_value(new ConfigOptionEnum<RemapAxis>(default_axis));
|
||||
};
|
||||
add_belt_remap("gcode_remap_x", L("G-code remap X"),
|
||||
L("Which slicing axis maps to machine X in G-code output. Applied AFTER slicing, during G-code generation."),
|
||||
RemapAxis::PosX, comDevelop);
|
||||
add_belt_remap("gcode_remap_y", L("G-code remap Y"),
|
||||
L("Which slicing axis maps to machine Y in G-code output. Applied AFTER slicing, during G-code generation."),
|
||||
RemapAxis::PosY, comDevelop);
|
||||
add_belt_remap("gcode_remap_z", L("G-code remap Z"),
|
||||
L("Which slicing axis maps to machine Z in G-code output. Applied AFTER slicing, during G-code generation."),
|
||||
RemapAxis::PosZ, comDevelop);
|
||||
|
||||
// The machine-frame G-code transform (shear + scale) is no longer configured
|
||||
// by per-axis keys: it is derived from the belt tilt (belt_slice_rotation axis
|
||||
// + angle, or belt_frame_tilt_angle when decoupled) in MachineFrameTransform.
|
||||
|
||||
// Belt support floor debug controls
|
||||
def = this->add("belt_support_floor_offset", coFloat);
|
||||
def->label = L("Support Floor Z offset");
|
||||
def->category = L("Printable space");
|
||||
def->tooltip = L("Shifts the computed belt floor up or down (mm). Negative values lower the floor, allowing more supports to survive. Use this to diagnose belt floor formula issues.");
|
||||
def->sidetext = L("mm");
|
||||
def->min = -500;
|
||||
def->max = 500;
|
||||
def->mode = comAdvanced;
|
||||
def->set_default_value(new ConfigOptionFloat(0));
|
||||
|
||||
def = this->add("enable_belt_purge_tower", coBool);
|
||||
def->label = L("Enable belt purge tower");
|
||||
def->category = L("Multimaterial");
|
||||
def->tooltip = L("Belt-printer replacement for the wipe/prime tower. When enabled on a belt "
|
||||
"printer, a purge prism is automatically generated next to the printed parts "
|
||||
"and filament-change purging is routed into it (the classic wipe tower cannot "
|
||||
"be used on belt printers because its G-code bypasses the belt transform). "
|
||||
"Only available on belt printers.");
|
||||
def->mode = comAdvanced;
|
||||
def->set_default_value(new ConfigOptionBool(false));
|
||||
|
||||
def = this->add("belt_purge_tower_width", coFloat);
|
||||
def->label = L("Belt purge tower width");
|
||||
def->category = L("Printable space");
|
||||
def->tooltip = L("Width (machine X, across the belt) of the purge prism that is automatically "
|
||||
"generated on belt printers when the belt purge tower is enabled and multiple "
|
||||
"filaments are used. Filament-change purging is routed into this prism's "
|
||||
"extrusions instead of a classic wipe tower. Its height is computed "
|
||||
"automatically from the worst-case purge volume per layer: a wider prism "
|
||||
"results in a shorter one.");
|
||||
def->sidetext = L("mm");
|
||||
def->min = 1.;
|
||||
def->mode = comAdvanced;
|
||||
def->set_default_value(new ConfigOptionFloat(35.));
|
||||
|
||||
def = this->add("tree_support_branch_angle", coFloat);
|
||||
def->label = L("Tree support branch angle");
|
||||
def->category = L("Support");
|
||||
@@ -8076,6 +8301,16 @@ void PrintConfigDef::init_fff_params()
|
||||
"It will not take effect unless the prime tower is enabled.");
|
||||
def->set_default_value(new ConfigOptionBool(false));
|
||||
|
||||
// Internal marker (not shown in any settings tab): identifies the auto-generated
|
||||
// belt purge prism so it can be updated/removed by the auto-manager and aligned
|
||||
// to the object layer grid by the backend. Persisted to 3mf like any per-object key.
|
||||
def = this->add("belt_purge_tower_object", coBool);
|
||||
def->category = L("Flush options");
|
||||
def->label = L("Belt purge tower object");
|
||||
def->tooltip = L("Marks the auto-generated belt purge prism. Managed automatically; do not set manually.");
|
||||
def->mode = comDevelop;
|
||||
def->set_default_value(new ConfigOptionBool(false));
|
||||
|
||||
def = this->add("wipe_tower_bridging", coFloat);
|
||||
def->label = L("Maximal bridging distance");
|
||||
def->tooltip = L("Maximal distance between supports on sparse infill sections.");
|
||||
@@ -9578,6 +9813,13 @@ void PrintConfigDef::handle_legacy(t_config_option_key &opt_key, std::string &va
|
||||
"smooth_coefficient", "overhang_totally_speed", "silent_mode",
|
||||
"overhang_speed_classic",
|
||||
"anisotropic_surfaces", // superseded by top_surface_fill_order / bottom_surface_fill_order
|
||||
// Belt printer keys retired before the first release: the global-mode and
|
||||
// back-transform switches are presumed on, and the pre-slice axis remap, the
|
||||
// support Z offset mode, the support floor mode (always on) and the first-layer
|
||||
// plane evaluator were removed.
|
||||
"belt_slice_rotation_global", "preslice_remap_x", "preslice_remap_y", "preslice_remap_z", "preslice_remap_global",
|
||||
"belt_support_z_offset_mode", "first_layer_plane", "first_layer_plane_offset",
|
||||
"belt_preslice_global", "gcode_back_transform", "belt_support_floor_mode", "first_layer_plane_thickness",
|
||||
};
|
||||
|
||||
if (ignore.find(opt_key) != ignore.end()) {
|
||||
@@ -13308,10 +13550,22 @@ Polygons get_bed_excluded_area(const PrintConfig& cfg)
|
||||
{
|
||||
const Pointfs exclude_area_points = cfg.bed_exclude_area.values;
|
||||
|
||||
// Belt printer: project exclusion zone points from the belt surface to machine-frame XY.
|
||||
// On the belt surface Z=0, so the in-plane axis foreshortens by cos(tilt). The tilt
|
||||
// axis decides which bed axis foreshortens: tilt about X (belt along Y) scales Y,
|
||||
// tilt about Y (belt along X) scales X. Derived from belt_slice_rotation.
|
||||
const bool is_belt = cfg.belt_printer.value;
|
||||
const auto tilt = BeltTransformPipeline::physical_tilt(
|
||||
cfg.belt_slice_rotation.value, cfg.belt_slice_rotation_angle.value);
|
||||
const double cos_x = is_belt ? std::cos(Geometry::deg2rad(tilt.tilt_x_deg)) : 1.0; // foreshortens Y
|
||||
const double cos_y = is_belt ? std::cos(Geometry::deg2rad(tilt.tilt_y_deg)) : 1.0; // foreshortens X
|
||||
|
||||
Polygon exclude_poly;
|
||||
for (int i = 0; i < exclude_area_points.size(); i++) {
|
||||
auto pt = exclude_area_points[i];
|
||||
exclude_poly.points.emplace_back(scale_(pt.x()), scale_(pt.y()));
|
||||
double x = is_belt ? pt.x() * cos_y : pt.x();
|
||||
double y = is_belt ? pt.y() * cos_x : pt.y();
|
||||
exclude_poly.points.emplace_back(scale_(x), scale_(y));
|
||||
}
|
||||
|
||||
exclude_poly.make_counter_clockwise();
|
||||
|
||||
@@ -304,6 +304,25 @@ enum class SlicingMode
|
||||
CloseHoles,
|
||||
};
|
||||
|
||||
// Axis around which the mesh is rotated before slicing, when
|
||||
// `belt_slice_rotation` is set. None disables the rotation stage. This is the
|
||||
// single "belt tilt" axis: it drives both the pre-slice mesh rotation and the
|
||||
// post-slice machine-frame transform (shear + scale derived from the tilt angle).
|
||||
enum class BeltRotationAxis
|
||||
{
|
||||
None = 0,
|
||||
X = 1,
|
||||
Y = 2,
|
||||
Z = 3,
|
||||
};
|
||||
|
||||
enum class RemapAxis
|
||||
{
|
||||
PosX = 0, PosY = 1, PosZ = 2,
|
||||
NegX = 3, NegY = 4, NegZ = 5,
|
||||
RevX = 6, RevY = 7, RevZ = 8, // Reversed: max - pos
|
||||
};
|
||||
|
||||
enum SupportMaterialPattern {
|
||||
smpDefault,
|
||||
smpRectilinear, smpRectilinearGrid, smpHoneycomb,
|
||||
@@ -411,6 +430,10 @@ enum BrimType {
|
||||
btInnerOnly,
|
||||
btOuterAndInner,
|
||||
btNoBrim,
|
||||
// Belt printers: brim only where the part first touches the belt, nothing after
|
||||
// that. Appended last so no existing value shifts. On a non-belt printer this
|
||||
// has no meaning and behaves as btOuterOnly.
|
||||
btLeadingEdgeOnly,
|
||||
};
|
||||
|
||||
enum TimelapseType : int {
|
||||
@@ -744,6 +767,8 @@ CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(NoiseType)
|
||||
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(InfillPattern)
|
||||
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(IroningType)
|
||||
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SlicingMode)
|
||||
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltRotationAxis)
|
||||
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(RemapAxis)
|
||||
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SupportMaterialPattern)
|
||||
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SupportMaterialStyle)
|
||||
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SupportMaterialInterfacePattern)
|
||||
@@ -1215,6 +1240,8 @@ PRINT_CONFIG_CLASS_DEFINE(
|
||||
((ConfigOptionBool, brim_use_efc_outline))
|
||||
((ConfigOptionEnum<BrimType>, brim_type))
|
||||
((ConfigOptionFloat, brim_width))
|
||||
((ConfigOptionFloat, leading_brim_length))
|
||||
((ConfigOptionFloat, extra_brim_width))
|
||||
((ConfigOptionFloat, brim_ears_detection_length))
|
||||
((ConfigOptionFloat, brim_ears_max_angle))
|
||||
((ConfigOptionBool, brim_ears_outer_only))
|
||||
@@ -1298,6 +1325,9 @@ PRINT_CONFIG_CLASS_DEFINE(
|
||||
// BBS
|
||||
((ConfigOptionBool, flush_into_infill))
|
||||
((ConfigOptionBool, flush_into_support))
|
||||
// Marker for the auto-generated belt purge prism; identifies the object to
|
||||
// the auto-manager (GUI) and the layer-grid alignment step (backend).
|
||||
((ConfigOptionBool, belt_purge_tower_object))
|
||||
// BBS
|
||||
((ConfigOptionFloat, tree_support_branch_distance))
|
||||
((ConfigOptionFloat, tree_support_tip_diameter))
|
||||
@@ -1875,6 +1905,33 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE(
|
||||
PrintConfig,
|
||||
(MachineEnvelopeConfig, GCodeConfig),
|
||||
|
||||
// Build plate tilt for off-axis gravity support generation (printer-level setting).
|
||||
((ConfigOptionFloat, build_plate_tilt_x))
|
||||
((ConfigOptionFloat, build_plate_tilt_y))
|
||||
// Belt printer settings (printer-level).
|
||||
((ConfigOptionBool, belt_printer))
|
||||
((ConfigOptionBool, belt_printer_infinite_y))
|
||||
// Mesh rotation applied before slicing — the single source of truth for the
|
||||
// physical belt tilt. Its angle + axis drive bed rendering, support gravity
|
||||
// tilt, the bed-exclusion projection, AND the post-slice machine-frame
|
||||
// transform (shear + scale, derived from the tilt angle; see
|
||||
// MachineFrameTransform). Isometric (no distortion) on the mesh side; the
|
||||
// g-code back-transform inverts the rotation before the machine-frame stage.
|
||||
((ConfigOptionEnum<BeltRotationAxis>, belt_slice_rotation))
|
||||
((ConfigOptionFloat, belt_slice_rotation_angle))
|
||||
// Expert override: decouple the machine-frame tilt angle from the pre-slice
|
||||
// rotation angle. When disabled, the machine frame uses belt_slice_rotation_angle.
|
||||
((ConfigOptionBool, belt_frame_tilt_decouple))
|
||||
((ConfigOptionFloat, belt_frame_tilt_angle))
|
||||
((ConfigOptionEnum<RemapAxis>, gcode_remap_x))
|
||||
((ConfigOptionEnum<RemapAxis>, gcode_remap_y))
|
||||
((ConfigOptionEnum<RemapAxis>, gcode_remap_z))
|
||||
((ConfigOptionFloat, belt_support_floor_offset))
|
||||
// Width (machine X, across the belt) of the auto-generated belt purge prism.
|
||||
((ConfigOptionFloat, belt_purge_tower_width))
|
||||
// Belt-printer-only "type" of purge tower: enables the auto-generated belt
|
||||
// purge prism (the belt replacement for the classic wipe/prime tower).
|
||||
((ConfigOptionBool, enable_belt_purge_tower))
|
||||
//BBS
|
||||
((ConfigOptionInts, additional_cooling_fan_speed))
|
||||
((ConfigOptionInts, close_additional_fan_first_x_layers))
|
||||
|
||||
+236
-13
@@ -8,6 +8,8 @@
|
||||
#include "Polygon.hpp"
|
||||
#include "Polyline.hpp"
|
||||
#include "Print.hpp"
|
||||
#include "BeltTransform.hpp"
|
||||
|
||||
#include "BoundingBox.hpp"
|
||||
#include "ClipperUtils.hpp"
|
||||
#include "Geometry.hpp"
|
||||
@@ -18,6 +20,7 @@
|
||||
#include "PrintConfig.hpp"
|
||||
#include "SLA/IndexedMesh.hpp"
|
||||
#include "Support/SupportMaterial.hpp"
|
||||
#include "Support/SupportCommon.hpp"
|
||||
#include "Support/SupportSpotsGenerator.hpp"
|
||||
#include "Support/TreeSupport.hpp"
|
||||
#include "Surface.hpp"
|
||||
@@ -81,6 +84,7 @@
|
||||
#include "Fill/Lightning/Generator.hpp"
|
||||
#include "SurfaceCollection.hpp"
|
||||
#include "TriangleMesh.hpp"
|
||||
#include "BeltBrim.hpp"
|
||||
|
||||
namespace Slic3r { enum class EnforcerBlockerType : int8_t; }
|
||||
|
||||
@@ -983,7 +987,7 @@ void PrintObject::detect_overhangs_for_lift()
|
||||
}
|
||||
}
|
||||
|
||||
void PrintObject::generate_support_material()
|
||||
void PrintObject::generate_support_material(bool with_belt_brim)
|
||||
{
|
||||
if (this->set_started(posSupportMaterial)) {
|
||||
this->clear_support_layers();
|
||||
@@ -1026,10 +1030,30 @@ void PrintObject::generate_support_material()
|
||||
this->_generate_support_material();
|
||||
m_print->throw_if_canceled();
|
||||
}
|
||||
// Belt brim rides here rather than in the brim step because its apron
|
||||
// prologue introduces print_z values below the object's first layer, and
|
||||
// those must exist before ToolOrdering is built at psWipeTower - one step
|
||||
// ahead of psSkirtBrim. The brim options already invalidate
|
||||
// posSupportMaterial, so this needs no extra invalidation edges.
|
||||
m_belt_brim_pending = true;
|
||||
if (with_belt_brim)
|
||||
this->generate_belt_brim();
|
||||
this->set_done(posSupportMaterial);
|
||||
}
|
||||
}
|
||||
|
||||
void PrintObject::generate_belt_brim()
|
||||
{
|
||||
if (! m_belt_brim_pending)
|
||||
return;
|
||||
// belt_brim_obstacles() looks up the layers and support layers of every object
|
||||
// on the plate by print_z. Another object's support step rebuilds those (and
|
||||
// temporarily shifts its layer Z values), so this must not overlap with it.
|
||||
make_belt_brim(*this);
|
||||
m_print->throw_if_canceled();
|
||||
m_belt_brim_pending = false;
|
||||
}
|
||||
|
||||
void PrintObject::estimate_curled_extrusions()
|
||||
{
|
||||
if (this->set_started(posEstimateCurledExtrusions)) {
|
||||
@@ -1193,7 +1217,10 @@ FillAdaptive::RegionOctrees PrintObject::prepare_adaptive_infill_data(
|
||||
indexed_triangle_set mesh = this->model_object()->raw_indexed_triangle_set();
|
||||
// Rotate mesh and build octree on it with axis-aligned (standart base) cubes.
|
||||
auto to_octree = transform_to_octree().toRotationMatrix();
|
||||
its_transform(mesh, to_octree * this->trafo_centered(), true);
|
||||
// Overhangs below are placed at Layer::bottom_z(), which includes the belt global Z offset.
|
||||
Transform3d object_trafo = this->trafo_sliced();
|
||||
object_trafo.translation().z() += m_belt_global_z_offset;
|
||||
its_transform(mesh, to_octree * object_trafo, true);
|
||||
|
||||
// Triangulate internal bridging surfaces.
|
||||
std::vector<std::vector<Vec3d>> overhangs(std::max(surfaces_w_layer.size(), size_t(1)));
|
||||
@@ -1276,6 +1303,16 @@ void PrintObject::clear_layers()
|
||||
for (Layer *l : m_layers)
|
||||
delete l;
|
||||
m_layers.clear();
|
||||
for (Layer *l : m_belt_truncated_layers)
|
||||
delete l;
|
||||
m_belt_truncated_layers.clear();
|
||||
// Fills dropped for plastic saving are owned by the stash while they sit
|
||||
// outside their layer's collection, so they are freed here too. Order
|
||||
// matters only in that these point at layers deleted just above, and we
|
||||
// never dereference the layer -- just the entity.
|
||||
for (const BeltDroppedFill &d : m_belt_dropped_fills)
|
||||
delete d.entity;
|
||||
m_belt_dropped_fills.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1309,6 +1346,75 @@ void PrintObject::clear_support_layers()
|
||||
l->cantilevers.clear();
|
||||
}
|
||||
}
|
||||
// Belt brim is owned by the same step, so it must die with it or an
|
||||
// invalidate-without-rerun would leave stale bands (and stale prologue Zs)
|
||||
// behind. Unconditional: unlike support layers it is never shared.
|
||||
this->clear_belt_brim();
|
||||
}
|
||||
|
||||
// Belt brim ------------------------------------------------------------------
|
||||
//
|
||||
// The tilt test is answered from the print CONFIG, not from SlicingParameters:
|
||||
// invalidating posSupportMaterial clears m_slicing_params.valid, and this is
|
||||
// queried from Print::process() dispatch, Brim.cpp and the G-code emitter, where
|
||||
// a stale zero shear factor would silently drop the brim. BeltBrim.cpp itself
|
||||
// reads the real belt floor through BeltFloorContext, where the parameters are
|
||||
// guaranteed current.
|
||||
bool PrintObject::has_belt_brim() const
|
||||
{
|
||||
if (! m_print->has_tilted_belt())
|
||||
return false;
|
||||
// The purge prism is sacrificial and sits at the plate's edge; its generator sets no_brim, and
|
||||
// this keeps it brimless whatever its config says, so a brim on the parts never blocks purging.
|
||||
if (m_config.belt_purge_tower_object.value)
|
||||
return false;
|
||||
if (m_config.brim_type == btNoBrim)
|
||||
return false;
|
||||
// An inner-only brim has no leading/extra geometry: leading_brim_length and
|
||||
// extra_brim_width both widen the OUTER ring, which btInnerOnly never emits, so it
|
||||
// produces nothing unless brim_width itself is positive. Every other brim type is
|
||||
// satisfied by any one of the three widths. Requiring the width here (instead of
|
||||
// "any width") stops has_belt_brim() - and therefore Print::validate() - from
|
||||
// rejecting the prime tower / spiral vase for a brim that would never be drawn.
|
||||
if (m_config.brim_type == btInnerOnly) {
|
||||
if (m_config.brim_width.value <= 0.)
|
||||
return false;
|
||||
} else if (m_config.brim_width.value <= 0. && m_config.leading_brim_length.value <= 0.
|
||||
&& m_config.extra_brim_width.value <= 0.) {
|
||||
return false;
|
||||
}
|
||||
return ! this->has_raft();
|
||||
}
|
||||
|
||||
unsigned int PrintObject::belt_brim_filament() const
|
||||
{
|
||||
// 1-based, matching PrintRegion::outer_wall_filament_id and the raw values pushed
|
||||
// into LayerTools::extruders in ToolOrdering::collect_extruders (the whole list is
|
||||
// reindexed to 0-based later). Lowest positive outer-wall filament over the
|
||||
// printing regions; 1 when none is explicitly set.
|
||||
unsigned int brim_filament = 0;
|
||||
for (size_t i = 0; i < this->num_printing_regions(); ++ i) {
|
||||
const unsigned int f = this->printing_region(i).config().outer_wall_filament_id.value;
|
||||
if (f > 0 && (brim_filament == 0 || f < brim_filament))
|
||||
brim_filament = f;
|
||||
}
|
||||
return brim_filament == 0 ? 1u : brim_filament;
|
||||
}
|
||||
|
||||
void PrintObject::clear_belt_brim()
|
||||
{
|
||||
m_belt_brim_by_layer.clear();
|
||||
m_belt_brim_areas_by_layer.clear();
|
||||
m_belt_brim_prologue.clear();
|
||||
}
|
||||
|
||||
void PrintObject::set_belt_brim(std::vector<ExtrusionEntityCollection> &&by_layer,
|
||||
std::vector<ExPolygons> &&areas,
|
||||
std::vector<BeltBrimBand> &&prologue)
|
||||
{
|
||||
m_belt_brim_by_layer = std::move(by_layer);
|
||||
m_belt_brim_areas_by_layer = std::move(areas);
|
||||
m_belt_brim_prologue = std::move(prologue);
|
||||
}
|
||||
|
||||
std::shared_ptr<TreeSupportData> PrintObject::alloc_tree_support_preview_cache()
|
||||
@@ -1351,6 +1457,8 @@ bool PrintObject::invalidate_state_by_config_options(
|
||||
bool invalidated = false;
|
||||
for (const t_config_option_key &opt_key : opt_keys) {
|
||||
if ( opt_key == "brim_width"
|
||||
|| opt_key == "leading_brim_length"
|
||||
|| opt_key == "extra_brim_width"
|
||||
|| opt_key == "brim_object_gap"
|
||||
|| opt_key == "brim_use_efc_outline"
|
||||
|| opt_key == "brim_type"
|
||||
@@ -1375,7 +1483,10 @@ bool PrintObject::invalidate_state_by_config_options(
|
||||
const auto* new_brim_type = new_config.option<ConfigOptionEnum<BrimType>>(opt_key);
|
||||
//BBS: When switch to manual brim, the object must have brim, then re-generate perimeter
|
||||
//to make the wall order of first layer to be outer-first
|
||||
if (old_brim_type->value == btOuterOnly || new_brim_type->value == btOuterOnly)
|
||||
// btLeadingEdgeOnly is printed as an outer brim (Brim.cpp, BeltBrim.cpp), so it
|
||||
// takes part in the same first-layer wall order rule.
|
||||
if (old_brim_type->value == btOuterOnly || new_brim_type->value == btOuterOnly ||
|
||||
old_brim_type->value == btLeadingEdgeOnly || new_brim_type->value == btLeadingEdgeOnly)
|
||||
steps.emplace_back(posPerimeters);
|
||||
}
|
||||
} else if (
|
||||
@@ -1735,7 +1846,8 @@ bool PrintObject::invalidate_state_by_config_options(
|
||||
} else if (
|
||||
opt_key == "flush_into_infill"
|
||||
|| opt_key == "flush_into_objects"
|
||||
|| opt_key == "flush_into_support") {
|
||||
|| opt_key == "flush_into_support"
|
||||
|| opt_key == "belt_purge_tower_object") {
|
||||
invalidated |= m_print->invalidate_step(psWipeTower);
|
||||
invalidated |= m_print->invalidate_step(psGCodeExport);
|
||||
} else {
|
||||
@@ -1765,12 +1877,25 @@ bool PrintObject::invalidate_step(PrintObjectStep step)
|
||||
invalidated |= this->invalidate_steps({ posIroning, posContouring, posSimplifyInfill });
|
||||
invalidated |= m_print->invalidate_steps({ psSkirtBrim });
|
||||
} else if (step == posSlice) {
|
||||
invalidated |= this->invalidate_steps({ posPerimeters, posPrepareInfill, posInfill, posIroning, posContouring, posSupportMaterial, posSimplifyPath, posSimplifyInfill });
|
||||
// posSimplifySupportPath is listed with posSupportMaterial: invalidate_steps() does not
|
||||
// propagate, so without it a re-slice regenerated the supports but kept the step done,
|
||||
// and the new support paths were exported unsimplified, unlike a fresh slice.
|
||||
// posDetectOverhangsForLift reads the layers' overhang regions, which a re-slice
|
||||
// starts over empty: without it here the step stayed done and the lift logic in
|
||||
// GCode::needs_retraction() had no overhangs to test against until something else
|
||||
// invalidated it.
|
||||
invalidated |= this->invalidate_steps({ posPerimeters, posPrepareInfill, posInfill, posIroning, posContouring, posSupportMaterial, posSimplifyPath, posSimplifyInfill, posSimplifySupportPath, posDetectOverhangsForLift });
|
||||
invalidated |= m_print->invalidate_steps({ psSkirtBrim });
|
||||
m_slicing_params.valid = false;
|
||||
// The exact belt_floor_z_shift is recomputed when slice() runs again.
|
||||
m_belt_floor_z_shift_cache_valid = false;
|
||||
} else if (step == posSupportMaterial) {
|
||||
invalidated |= this->invalidate_steps({ posSimplifySupportPath });
|
||||
invalidated |= m_print->invalidate_steps({ psSkirtBrim });
|
||||
// SlicingParameters depend on support config (enable_support /
|
||||
// raft_layers / enforce_support_layers feed min/max layer height in
|
||||
// Slicing.cpp), so invalidate them here. The vertex-scan
|
||||
// belt_floor_z_shift is preserved via m_belt_floor_z_shift_cached.
|
||||
m_slicing_params.valid = false;
|
||||
}
|
||||
|
||||
@@ -1791,6 +1916,7 @@ bool PrintObject::invalidate_all_steps()
|
||||
bool result = inherited_invalidated || print_invalidated;
|
||||
// Then reset some of the depending values.
|
||||
m_slicing_params.valid = false;
|
||||
m_belt_floor_z_shift_cache_valid = false;
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -4127,8 +4253,36 @@ void PrintObject::update_slicing_parameters()
|
||||
{
|
||||
// Orca: updated function call for XYZ shrinkage compensation
|
||||
if (!m_slicing_params.valid) {
|
||||
m_slicing_params = SlicingParameters::create_from_config(this->print()->config(), m_config, this->model_object()->max_z(),
|
||||
coordf_t object_height = this->model_object()->max_z();
|
||||
BeltTransformPipeline::BeltFloorParams belt_floor;
|
||||
const auto &pcfg = this->print()->config();
|
||||
if (pcfg.belt_printer.value) {
|
||||
// The box of the mesh in the frame it is sliced in: XY centred and Z as
|
||||
// placed on the bed (trafo_centered()). raw_bounding_box() has the
|
||||
// instance's Z offset removed, and the belt floor is not invariant to a
|
||||
// Z shift (a point's z and the floor under it move in opposite
|
||||
// directions under the rotation), so an offset box under-estimates the
|
||||
// height by twice the shift and the layers stop part way up the object.
|
||||
BoundingBoxf3 bb;
|
||||
const Transform3d trafo = this->trafo_centered();
|
||||
for (const ModelVolume *v : this->model_object()->volumes)
|
||||
if (v->is_model_part())
|
||||
bb.merge(v->mesh().transformed_bounding_box(trafo * v->get_matrix()));
|
||||
auto hr = BeltTransformPipeline::compute_belt_height_and_floor(pcfg, bb, object_height);
|
||||
object_height = hr.object_height;
|
||||
belt_floor = hr.floor_params;
|
||||
}
|
||||
m_slicing_params = SlicingParameters::create_from_config(pcfg, m_config, object_height,
|
||||
this->object_extruders(), this->print()->shrinkage_compensation());
|
||||
// Populate belt floor parameters into slicing params for support clipping.
|
||||
m_slicing_params.belt_floor_shear_factor = belt_floor.shear_factor;
|
||||
m_slicing_params.belt_floor_from_axis = belt_floor.from_axis;
|
||||
m_slicing_params.belt_floor_z_shift = belt_floor.z_shift;
|
||||
// Prefer the vertex-scan z_shift over the bbox approximation when
|
||||
// slice() has already produced one (e.g. this rebuild was triggered
|
||||
// by a support-config change, which doesn't move the belt floor).
|
||||
if (m_belt_floor_z_shift_cache_valid)
|
||||
m_slicing_params.belt_floor_z_shift = m_belt_floor_z_shift_cached;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4169,9 +4323,28 @@ SlicingParameters PrintObject::slicing_parameters(const DynamicPrintConfig &full
|
||||
sort_remove_duplicates(object_extruders);
|
||||
//FIXME add painting extruders
|
||||
|
||||
if (object_max_z <= 0.f)
|
||||
object_max_z = (float)model_object.raw_bounding_box().size().z();
|
||||
return SlicingParameters::create_from_config(print_config, object_config, object_max_z, object_extruders, object_shrinkage_compensation);
|
||||
BeltTransformPipeline::BeltFloorParams belt_floor;
|
||||
if (object_max_z <= 0.f) {
|
||||
BoundingBoxf3 bb = model_object.raw_bounding_box();
|
||||
object_max_z = (float)bb.size().z();
|
||||
if (print_config.belt_printer.value) {
|
||||
// Z as placed on the bed, XY around the instance origin: the belt floor
|
||||
// depends on where the box sits in Z (see update_slicing_parameters()).
|
||||
if (! model_object.instances.empty()) {
|
||||
bb = model_object.instance_bounding_box(0, false);
|
||||
const Vec3d off = model_object.instances.front()->get_offset();
|
||||
bb.translate(-off.x(), -off.y(), 0.);
|
||||
}
|
||||
auto hr = BeltTransformPipeline::compute_belt_height_and_floor(print_config, bb, object_max_z);
|
||||
object_max_z = (float)hr.object_height;
|
||||
belt_floor = hr.floor_params;
|
||||
}
|
||||
}
|
||||
SlicingParameters params = SlicingParameters::create_from_config(print_config, object_config, object_max_z, object_extruders, object_shrinkage_compensation);
|
||||
params.belt_floor_shear_factor = belt_floor.shear_factor;
|
||||
params.belt_floor_from_axis = belt_floor.from_axis;
|
||||
params.belt_floor_z_shift = belt_floor.z_shift;
|
||||
return params;
|
||||
}
|
||||
|
||||
// returns 0-based indices of extruders used to print the object (without brim, support and other helper extrusions)
|
||||
@@ -4688,9 +4861,58 @@ void PrintObject::_generate_support_material()
|
||||
tree_support.generate();
|
||||
}
|
||||
else {
|
||||
PrintObjectSupportMaterial support_material(this, m_slicing_params);
|
||||
support_material.generate(*this);
|
||||
// The normal generator anchors its layer grid at the slicing frame origin
|
||||
// (SlicingParameters: first layer at first_print_layer_height, raft at
|
||||
// z = 0), so it has to see the object layers in that frame. On a belt the
|
||||
// object layers carry the global Z offset (PrintObject::slice()), which is
|
||||
// negative for the leading half of the belt: a top contact below z = 0
|
||||
// then turns the intermediate-layer count negative and the generator
|
||||
// allocates layers until memory runs out. Lift the offset off the object
|
||||
// layers and the belt floor for the duration of the run and put it back
|
||||
// on everything, including the new support layers, afterwards (organic
|
||||
// tree support is shifted the same way below).
|
||||
const double global_z = m_belt_global_z_offset;
|
||||
const bool unshift = std::abs(global_z) > EPSILON;
|
||||
auto shift_object_frame = [this, global_z](double sign) {
|
||||
for (Layer *layer : m_layers)
|
||||
layer->print_z += sign * global_z;
|
||||
m_slicing_params.belt_floor_z_shift += sign * global_z;
|
||||
};
|
||||
if (unshift)
|
||||
shift_object_frame(-1.);
|
||||
try {
|
||||
PrintObjectSupportMaterial support_material(this, m_slicing_params);
|
||||
support_material.generate(*this);
|
||||
} catch (...) {
|
||||
if (unshift)
|
||||
shift_object_frame(1.);
|
||||
throw;
|
||||
}
|
||||
if (unshift) {
|
||||
shift_object_frame(1.);
|
||||
for (SupportLayer *sl : m_support_layers)
|
||||
sl->print_z += global_z;
|
||||
}
|
||||
}
|
||||
// Global Z offset for support layers:
|
||||
// - Normal support: generated in the object frame above and shifted afterwards.
|
||||
// - Non-organic tree support (slim/strong/hybrid): plan_layer_heights() reads
|
||||
// from globally-offset object layers, so support layers already have it.
|
||||
// - Organic tree support: generate_tree_support_3D() computes its own Z values
|
||||
// independently and does NOT inherit the offset — apply it here.
|
||||
// Belt floor polygon clipping for non-organic tree support is done inside
|
||||
// draw_circles() before area_groups and toolpaths are built.
|
||||
if (is_tree(m_config.support_type.value) && std::abs(m_belt_global_z_offset) > EPSILON) {
|
||||
// Resolve effective support style (same logic as SupportParameters).
|
||||
auto style = m_config.support_style.value;
|
||||
if (style == smsDefault)
|
||||
style = smsTreeOrganic;
|
||||
if (style == smsTreeOrganic) {
|
||||
for (SupportLayer *sl : m_support_layers)
|
||||
sl->print_z += m_belt_global_z_offset;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// BBS
|
||||
@@ -5040,6 +5262,7 @@ static void project_triangles_to_slabs(ConstLayerPtrsAdaptor layers, const index
|
||||
void PrintObject::project_and_append_custom_facets(
|
||||
bool seam, EnforcerBlockerType type, std::vector<Polygons>& out, std::vector<std::pair<Vec3f, Vec3f>>* vertical_points) const
|
||||
{
|
||||
const Transform3d object_trafo = this->trafo_sliced();
|
||||
for (const ModelVolume* mv : this->model_object()->volumes)
|
||||
if (mv->is_model_part()) {
|
||||
const indexed_triangle_set custom_facets = seam
|
||||
@@ -5048,12 +5271,12 @@ void PrintObject::project_and_append_custom_facets(
|
||||
if (! custom_facets.indices.empty()) {
|
||||
if (seam)
|
||||
project_triangles_to_slabs(this->layers(), custom_facets,
|
||||
(this->trafo_centered() * mv->get_matrix()).cast<float>(),
|
||||
(object_trafo * mv->get_matrix()).cast<float>(),
|
||||
seam, out);
|
||||
else {
|
||||
std::vector<Polygons> projected;
|
||||
// Support blockers or enforcers. Project downward facing painted areas upwards to their respective slicing plane.
|
||||
slice_mesh_slabs(custom_facets, zs_from_layers(this->layers()), this->trafo_centered() * mv->get_matrix(), nullptr, &projected, vertical_points, [](){});
|
||||
slice_mesh_slabs(custom_facets, zs_from_layers(this->layers()), object_trafo * mv->get_matrix(), nullptr, &projected, vertical_points, [](){});
|
||||
// Merge these projections with the output, layer by layer.
|
||||
assert(! projected.empty());
|
||||
assert(out.empty() || out.size() == projected.size());
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include <algorithm>
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <limits>
|
||||
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
@@ -27,6 +28,9 @@
|
||||
#include "Point.hpp"
|
||||
#include "Polygon.hpp"
|
||||
#include "Print.hpp"
|
||||
#include "BeltTransform.hpp"
|
||||
#include "BeltSliceStrategy.hpp"
|
||||
#include "Geometry.hpp"
|
||||
//BBS
|
||||
#include "PrintConfig.hpp"
|
||||
#include "PrintBase.hpp"
|
||||
@@ -180,7 +184,8 @@ static std::vector<VolumeSlices> slice_volumes_inner(
|
||||
ModelVolumePtrs model_volumes,
|
||||
const std::vector<PrintObjectRegions::LayerRangeRegions> &layer_ranges,
|
||||
const std::vector<float> &zs,
|
||||
const std::function<void()> &throw_on_cancel_callback)
|
||||
const std::function<void()> &throw_on_cancel_callback,
|
||||
double *out_belt_min_z = nullptr)
|
||||
{
|
||||
model_volumes_sort_by_id(model_volumes);
|
||||
|
||||
@@ -195,6 +200,10 @@ static std::vector<VolumeSlices> slice_volumes_inner(
|
||||
params_base.closing_radius = print_object_config.slice_closing_radius.value;
|
||||
params_base.extra_offset = 0;
|
||||
params_base.trafo = object_trafo;
|
||||
// Pre-slice mesh transforms: axis remap (standalone — works without belt
|
||||
// mode), belt rotation, and the per-object Z-shift. Owned by BeltSliceStrategy
|
||||
// so this belt/remap-specific logic stays out of the generic slicing pipeline.
|
||||
BeltSliceStrategy::apply_preslice_transforms(params_base.trafo, print_config, model_volumes, out_belt_min_z);
|
||||
//BBS: 0.0025mm is safe enough to simplify the data to speed slicing up for high-resolution model.
|
||||
//Also has on influence on arc fitting which has default resolution 0.0125mm.
|
||||
params_base.resolution = print_config.resolution <= 0.001 ? 0.0f : 0.0025;
|
||||
@@ -328,14 +337,38 @@ static std::vector<std::vector<ExPolygons>> slices_to_regions(
|
||||
}
|
||||
} else {
|
||||
zs_complex.reserve(zs.size());
|
||||
// region.bbox is computed in pre-belt-transform slicer space (see PrintApply.cpp::trafo_for_bbox).
|
||||
// When belt transforms are active, layer Z values are in post-rotation/shear/scale/remap space,
|
||||
// so the Z components of region.bbox aren't comparable to z. Skipping the Z filter here
|
||||
// pushes those layers into the parallel_for path below, which handles multi-volume
|
||||
// clipping per layer without relying on the bbox Z range.
|
||||
const bool bbox_z_in_layer_frame = !(print_config.belt_printer.value &&
|
||||
BeltTransformPipeline::has_rotation(print_config));
|
||||
// Belt-transform addendum: with bbox-Z untrusted, the simple path's
|
||||
// "first model_part wins" logic drops subsequent volumes' slices unless
|
||||
// they XY-overlap with the first. Assemblies whose volumes are stacked
|
||||
// or side-by-side in pre-transform Z (different bbox.z ranges) thus lose
|
||||
// the volumes that originally sat outside the first volume's Z range —
|
||||
// showing up as truncation at the top or bottom of the assembly. Force
|
||||
// every layer in a multi-volume range through the parallel_for path,
|
||||
// which correctly merges all volumes per layer.
|
||||
int num_model_parts = 0;
|
||||
for (const PrintObjectRegions::VolumeRegion &vr : layer_range.volume_regions)
|
||||
if (vr.model_volume->is_model_part())
|
||||
++num_model_parts;
|
||||
const bool force_complex_for_belt = !bbox_z_in_layer_frame && num_model_parts > 1;
|
||||
for (; z_idx < zs.size() && zs[z_idx] < layer_range.layer_height_range.second; ++ z_idx) {
|
||||
float z = zs[z_idx];
|
||||
if (force_complex_for_belt) {
|
||||
zs_complex.push_back({ z_idx, z });
|
||||
continue;
|
||||
}
|
||||
int idx_first_printable_region = -1;
|
||||
bool complex = false;
|
||||
std::vector<int> printable_region_ids;
|
||||
for (int idx_region = 0; idx_region < int(layer_range.volume_regions.size()); ++ idx_region) {
|
||||
const PrintObjectRegions::VolumeRegion ®ion = layer_range.volume_regions[idx_region];
|
||||
if (region.bbox->min().z() <= z && region.bbox->max().z() >= z) {
|
||||
if (!bbox_z_in_layer_frame || (region.bbox->min().z() <= z && region.bbox->max().z() >= z)) {
|
||||
if (region.model_volume->is_model_part())
|
||||
printable_region_ids.push_back(idx_region);
|
||||
|
||||
@@ -346,7 +379,9 @@ static std::vector<std::vector<ExPolygons>> slices_to_regions(
|
||||
// Test for overlap with some other region.
|
||||
for (int idx_region2 = idx_first_printable_region; idx_region2 < idx_region; ++ idx_region2) {
|
||||
const PrintObjectRegions::VolumeRegion ®ion2 = layer_range.volume_regions[idx_region2];
|
||||
if (region2.bbox->min().z() <= z && region2.bbox->max().z() >= z && overlap_in_xy(*region.bbox, *region2.bbox)) {
|
||||
const bool region2_in_z = !bbox_z_in_layer_frame
|
||||
|| (region2.bbox->min().z() <= z && region2.bbox->max().z() >= z);
|
||||
if (region2_in_z && overlap_in_xy(*region.bbox, *region2.bbox)) {
|
||||
complex = true;
|
||||
break;
|
||||
}
|
||||
@@ -859,10 +894,32 @@ void PrintObject::slice()
|
||||
this->update_layer_height_profile(*this->model_object(), m_slicing_params, layer_height_profile);
|
||||
m_print->throw_if_canceled();
|
||||
m_typed_slices = false;
|
||||
// The belt state below is only written while belt mode is on (and the min-Z
|
||||
// lift only when there is a rotation or remap). Start every slice from zero,
|
||||
// or a project switched from a belt printer to a normal one, or whose tilt
|
||||
// axis was set to None, keeps the previous offsets: the adaptive infill octree
|
||||
// and the organic support layers (PrintObject.cpp) would still be shifted by
|
||||
// them.
|
||||
m_belt_min_z = 0.;
|
||||
m_belt_global_z_offset = 0.;
|
||||
this->clear_layers();
|
||||
m_layers = new_layers(this, generate_object_layers(m_slicing_params, layer_height_profile, m_config.precise_z_height.value));
|
||||
this->slice_volumes();
|
||||
m_print->throw_if_canceled();
|
||||
|
||||
// Belt floor Z-shift: where is the belt surface in final slicer space?
|
||||
//
|
||||
// The belt surface is the model's Z=0 plane. After the belt rotation and the
|
||||
// Z-shift it is the plane Z_belt = shear_factor * from_axis + z_shift_val in
|
||||
// slicer space, with z_shift_val = max(0, -m_belt_min_z), the lift that starts
|
||||
// the slicing frame at the belt below the footprint.
|
||||
if (std::abs(m_slicing_params.belt_floor_shear_factor) > EPSILON) {
|
||||
double z_shift_val = (m_belt_min_z < 0.) ? -m_belt_min_z : 0.;
|
||||
// The belt surface is at Z=0 in centered slicer space and bb.min.z() is
|
||||
// already folded into m_belt_min_z.
|
||||
m_slicing_params.belt_floor_z_shift = z_shift_val;
|
||||
}
|
||||
|
||||
int firstLayerReplacedBy = 0;
|
||||
|
||||
#if 0
|
||||
@@ -901,6 +958,105 @@ void PrintObject::slice()
|
||||
if (m_layers.empty())
|
||||
throw Slic3r::SlicingError(L("No layers were detected. You might want to repair your STL file(s) or check their size or thickness and retry.\n"));
|
||||
|
||||
// Belt printer: offset all layer Z values so objects at different positions
|
||||
// along the belt print at different heights on the tilted belt. This is a
|
||||
// post-slicing adjustment: the sliced geometry is the same, only the output Z
|
||||
// coordinates change.
|
||||
{
|
||||
const auto &pcfg = this->print()->config();
|
||||
BOOST_LOG_TRIVIAL(trace) << "Belt global check: belt_printer=" << pcfg.belt_printer.value
|
||||
<< " belt_slice_rotation=" << int(pcfg.belt_slice_rotation.value)
|
||||
<< " object=" << this->model_object()->name;
|
||||
if (pcfg.belt_printer.value) {
|
||||
|
||||
Point inst_shift = this->instances().empty() ? Point(0, 0)
|
||||
: this->instances().front().shift - this->center_offset();
|
||||
BOOST_LOG_TRIVIAL(trace) << "Belt global: object " << this->model_object()->name
|
||||
<< " instances=" << this->instances().size()
|
||||
<< " shift=(" << unscale<double>(inst_shift.x()) << ", " << unscale<double>(inst_shift.y()) << ")";
|
||||
|
||||
// Per-object Z-shift compensation, applied regardless of global mode.
|
||||
//
|
||||
// BeltSliceStrategy::apply_preslice_transforms lifts the mesh by max(0, -m_belt_min_z)
|
||||
// so the slicer can slice with slicer_z >= 0. BeltBackTransform inverts
|
||||
// build_forward_transform() which DOES NOT include this per-object
|
||||
// Z-shift (it's not known until vertex scan time). Result: G-code
|
||||
// coords emerge offset by the un-undone Z-shift — the inverse rotation
|
||||
// couples slicer_z back into both machine_y and machine_z. Compensating
|
||||
// layer.print_z by belt_z_shift here makes the back-transform produce
|
||||
// correct machine-frame coordinates whether or not a global mode is active.
|
||||
// The compensation must mirror the Z-shift actually applied, which
|
||||
// is max(0, -m_belt_min_z): when the transformed mesh starts ABOVE
|
||||
// slicer Z=0 (m_belt_min_z > 0 — possible for counter-rotated or
|
||||
// asymmetric geometry whose centered-frame minimum lands positive)
|
||||
// no lift was applied, and an unclamped m_belt_min_z here would
|
||||
// leak straight into the layer Z values, floating the whole object
|
||||
// off the belt by exactly that amount.
|
||||
double belt_z_shift = std::min(m_belt_min_z, 0.); // the belt surface is Z=0 in centered slicer space
|
||||
double global_z_offset = belt_z_shift;
|
||||
|
||||
// Centering correction: trafo_centered pretranslates by
|
||||
// -m_center_offset.{x,y}. Under the belt forward transform, the
|
||||
// Y component of that pretranslate couples into slicer-Z (shear:
|
||||
// tan*c.y, rotation: sin*c.y). BeltBackTransform inverts the
|
||||
// rotation/shear but doesn't undo centering, so this Z component
|
||||
// leaks into machine output as a position offset whenever
|
||||
// m_center_offset != 0. When a user moves a volume within an
|
||||
// assembly such that the combined bbox center shifts, this shows
|
||||
// up as a small Z translation in the print. Compensate by adding
|
||||
// the Z component of the centering through the forward transform.
|
||||
{
|
||||
Transform3d T_fwd = BeltTransformPipeline::build_forward_transform(pcfg);
|
||||
Vec3d c_off(unscale<double>(m_center_offset.x()),
|
||||
unscale<double>(m_center_offset.y()),
|
||||
0.);
|
||||
double centering_z_corr = (T_fwd.linear() * c_off).z();
|
||||
global_z_offset += centering_z_corr;
|
||||
}
|
||||
|
||||
{
|
||||
// Global pre-slice mode: compute full correction c = (T.linear() - I) * d
|
||||
// where T is the belt forward transform and d is the bed position, so
|
||||
// objects at different bed positions print at different machine Z values
|
||||
// along the inclined belt.
|
||||
Transform3d T = BeltTransformPipeline::build_forward_transform(pcfg);
|
||||
Vec3d d(unscale<double>(inst_shift.x()), unscale<double>(inst_shift.y()), 0.);
|
||||
Vec3d c = T.linear() * d - d;
|
||||
global_z_offset += c.z();
|
||||
|
||||
BOOST_LOG_TRIVIAL(trace) << "Belt preslice_global: correction=("
|
||||
<< c.x() << ", " << c.y() << ", " << c.z() << ")"
|
||||
<< " belt_z_shift=" << belt_z_shift << " (m_belt_min_z=" << m_belt_min_z << ")";
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(trace) << "Belt global: z_offset=" << global_z_offset
|
||||
<< " (" << this->print()->objects().size() << " objects on the plate)";
|
||||
m_belt_global_z_offset = global_z_offset;
|
||||
if (std::abs(global_z_offset) > EPSILON) {
|
||||
for (Layer *layer : m_layers)
|
||||
layer->print_z += global_z_offset;
|
||||
// Keep belt floor clipping in sync with the shifted print_z
|
||||
// values — the support generator sees globally-offset object
|
||||
// layer print_z, so belt_floor_z_shift must match.
|
||||
m_slicing_params.belt_floor_z_shift += global_z_offset;
|
||||
}
|
||||
if (!m_layers.empty()) {
|
||||
BOOST_LOG_TRIVIAL(trace) << "Belt global: first_layer_z=" << m_layers.front()->print_z
|
||||
<< " last_layer_z=" << m_layers.back()->print_z
|
||||
<< " num_layers=" << m_layers.size()
|
||||
<< " center_offset=(" << unscale<double>(m_center_offset.x())
|
||||
<< ", " << unscale<double>(m_center_offset.y()) << ")";
|
||||
}
|
||||
|
||||
// Cache the final patched belt_floor_z_shift so a later support-only
|
||||
// invalidation can rebuild m_slicing_params without losing this exact
|
||||
// (vertex-scan-derived) value. update_slicing_parameters() will
|
||||
// restore it after create_from_config() seeds the bbox approximation.
|
||||
m_belt_floor_z_shift_cached = m_slicing_params.belt_floor_z_shift;
|
||||
m_belt_floor_z_shift_cache_valid = true;
|
||||
}
|
||||
}
|
||||
|
||||
// BBS
|
||||
this->set_done(posSlice);
|
||||
}
|
||||
@@ -1172,6 +1328,7 @@ void apply_fuzzy_skin_segmentation(PrintObject &print_object, ThrowOnCancel thro
|
||||
}); // end of parallel_for
|
||||
}
|
||||
|
||||
|
||||
// 1) Decides Z positions of the layers,
|
||||
// 2) Initializes layers and their regions
|
||||
// 3) Slices the object meshes
|
||||
@@ -1205,7 +1362,8 @@ void PrintObject::slice_volumes()
|
||||
if (!slice_zs.empty()) {
|
||||
objSliceByVolume = slice_volumes_inner(
|
||||
print->config(), this->config(), this->trafo_centered(),
|
||||
this->model_object()->volumes, m_shared_regions->layer_ranges, slice_zs, throw_on_cancel_callback);
|
||||
this->model_object()->volumes, m_shared_regions->layer_ranges, slice_zs, throw_on_cancel_callback,
|
||||
&m_belt_min_z);
|
||||
}
|
||||
|
||||
//BBS: "model_part" volumes are grouded according to their connections
|
||||
@@ -1577,6 +1735,13 @@ ExPolygons PrintObject::_shrink_contour_holes(double contour_delta, double hole_
|
||||
return union_ex(new_ex_polys);
|
||||
}
|
||||
|
||||
Transform3d PrintObject::trafo_sliced() const
|
||||
{
|
||||
Transform3d trafo = this->trafo_centered();
|
||||
BeltSliceStrategy::apply_preslice_transforms(trafo, this->print()->config(), this->model_object()->volumes);
|
||||
return trafo;
|
||||
}
|
||||
|
||||
std::vector<Polygons> PrintObject::slice_support_volumes(const ModelVolumeType model_volume_type) const
|
||||
{
|
||||
auto it_volume = this->model_object()->volumes.begin();
|
||||
@@ -1591,7 +1756,7 @@ std::vector<Polygons> PrintObject::slice_support_volumes(const ModelVolumeType m
|
||||
const Print *print = this->print();
|
||||
auto throw_on_cancel_callback = std::function<void()>([print](){ print->throw_if_canceled(); });
|
||||
MeshSlicingParamsEx params;
|
||||
params.trafo = this->trafo_centered();
|
||||
params.trafo = this->trafo_sliced();
|
||||
for (; it_volume != it_volume_end; ++ it_volume)
|
||||
if ((*it_volume)->type() == model_volume_type) {
|
||||
std::vector<ExPolygons> slices2 = slice_volume(*(*it_volume), zs, params, throw_on_cancel_callback);
|
||||
@@ -1634,10 +1799,11 @@ std::vector<ExPolygons> PrintObject::slice_single_volume_regions(const ModelVolu
|
||||
{
|
||||
if (volume == nullptr)
|
||||
return {};
|
||||
// Match the existing slicing heights and centered transform without flattening holes.
|
||||
// Match the existing slicing heights and the frame the layers were sliced in (belt
|
||||
// pre-slice transforms included) without flattening holes.
|
||||
const std::vector<float> zs = zs_from_layers(this->layers());
|
||||
MeshSlicingParamsEx params;
|
||||
params.trafo = this->trafo_centered();
|
||||
params.trafo = this->trafo_sliced();
|
||||
const Print *print = this->print();
|
||||
return slice_volume(*volume, zs, params, [print]() { print->throw_if_canceled(); });
|
||||
}
|
||||
|
||||
@@ -113,6 +113,13 @@ struct SlicingParameters
|
||||
coordf_t object_print_z_uncompensated_max { 0 };
|
||||
// Scaling factor for compensating shrinkage in Z-axis.
|
||||
coordf_t object_shrinkage_compensation_z { 0 };
|
||||
|
||||
// Belt printer: floor plane parameters for support clipping.
|
||||
// Belt contact surface in slicing coords: Z = bb_min_z + sf*Y + slicing_z_shift.
|
||||
// cutoff = (print_z - belt_floor_z_shift - floor_offset) / shear_factor
|
||||
double belt_floor_shear_factor { 0.0 }; // shear factor (e.g. cot(45deg))
|
||||
int belt_floor_from_axis { 1 }; // which axis the shear is from (0=X, 1=Y)
|
||||
double belt_floor_z_shift { 0.0 }; // bb_min_z + max(0, -min_z_after_shear)
|
||||
};
|
||||
static_assert(IsTriviallyCopyable<SlicingParameters>::value, "SlicingParameters class is not POD (and it should be - see constructor).");
|
||||
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
#include "BeltFloorContext.hpp"
|
||||
#include "../Point.hpp"
|
||||
#include "../Polygon.hpp"
|
||||
#include "../PrintConfig.hpp"
|
||||
#include "../Slicing.hpp"
|
||||
#include "../libslic3r.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
bool BeltFloorContext::init(const SlicingParameters &sp, const PrintConfig &pcfg)
|
||||
{
|
||||
m_active = false;
|
||||
m_shear_factor = sp.belt_floor_shear_factor;
|
||||
m_from_axis = sp.belt_floor_from_axis;
|
||||
m_z_shift = sp.belt_floor_z_shift;
|
||||
m_floor_offset = pcfg.belt_support_floor_offset.value;
|
||||
|
||||
if (std::abs(m_shear_factor) < EPSILON)
|
||||
return false;
|
||||
|
||||
m_active = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BeltFloorContext::init_local(const SlicingParameters &sp, const PrintConfig &pcfg,
|
||||
double global_z_offset)
|
||||
{
|
||||
if (!init(sp, pcfg))
|
||||
return false;
|
||||
// Local Z: subtract the global Z offset so polygon computation
|
||||
// works in the object's local coordinate space.
|
||||
m_z_shift -= global_z_offset;
|
||||
return true;
|
||||
}
|
||||
|
||||
Polygons BeltFloorContext::surface_polygon(coordf_t print_z) const
|
||||
{
|
||||
return half_plane(print_z, /*belt_surface=*/true);
|
||||
}
|
||||
|
||||
Polygons BeltFloorContext::valid_region_polygon(coordf_t print_z) const
|
||||
{
|
||||
return half_plane(print_z, /*belt_surface=*/false);
|
||||
}
|
||||
|
||||
double BeltFloorContext::floor_print_z(const Point &pos_slicing) const
|
||||
{
|
||||
if (!m_active)
|
||||
return -std::numeric_limits<double>::infinity();
|
||||
double pos = unscale<double>(m_from_axis == 0 ? pos_slicing.x() : pos_slicing.y());
|
||||
return m_shear_factor * pos + m_floor_offset + m_z_shift;
|
||||
}
|
||||
|
||||
std::vector<Polygons> BeltFloorContext::compute_per_layer_floors(
|
||||
size_t num_layers,
|
||||
const std::function<double(size_t)> &layer_print_z) const
|
||||
{
|
||||
std::vector<Polygons> result(num_layers);
|
||||
if (!m_active)
|
||||
return result;
|
||||
for (size_t i = 0; i < num_layers; ++i)
|
||||
result[i] = surface_polygon(layer_print_z(i));
|
||||
return result;
|
||||
}
|
||||
|
||||
Polygons BeltFloorContext::half_plane(coordf_t print_z, bool belt_surface) const
|
||||
{
|
||||
if (!m_active)
|
||||
return {};
|
||||
|
||||
const double cutoff = this->cutoff_u(print_z);
|
||||
const coord_t cutoff_scaled = scale_(cutoff);
|
||||
const coord_t large_bound = scale_(1e3);
|
||||
|
||||
// The belt surface is on one side of the cutoff line; the valid region
|
||||
// is on the other side. Which side depends on shear_factor sign.
|
||||
//
|
||||
// belt_surface=true → the belt side (where support should NOT exist)
|
||||
// belt_surface=false → the valid side (where support IS allowed)
|
||||
//
|
||||
// For shear_factor > 0: belt surface is from_axis >= cutoff
|
||||
// For shear_factor < 0: belt surface is from_axis <= cutoff
|
||||
bool high_side = (m_shear_factor > 0) == belt_surface;
|
||||
|
||||
Polygon poly;
|
||||
if (m_from_axis == 0) {
|
||||
if (high_side) {
|
||||
// X >= cutoff
|
||||
poly.points = {
|
||||
Point(cutoff_scaled, -large_bound),
|
||||
Point(large_bound, -large_bound),
|
||||
Point(large_bound, large_bound),
|
||||
Point(cutoff_scaled, large_bound)
|
||||
};
|
||||
} else {
|
||||
// X < cutoff
|
||||
poly.points = {
|
||||
Point(-large_bound, -large_bound),
|
||||
Point(cutoff_scaled, -large_bound),
|
||||
Point(cutoff_scaled, large_bound),
|
||||
Point(-large_bound, large_bound)
|
||||
};
|
||||
}
|
||||
} else {
|
||||
if (high_side) {
|
||||
// Y >= cutoff
|
||||
poly.points = {
|
||||
Point(-large_bound, cutoff_scaled),
|
||||
Point( large_bound, cutoff_scaled),
|
||||
Point( large_bound, large_bound),
|
||||
Point(-large_bound, large_bound)
|
||||
};
|
||||
} else {
|
||||
// Y < cutoff
|
||||
poly.points = {
|
||||
Point(-large_bound, -large_bound),
|
||||
Point( large_bound, -large_bound),
|
||||
Point( large_bound, cutoff_scaled),
|
||||
Point(-large_bound, cutoff_scaled)
|
||||
};
|
||||
}
|
||||
}
|
||||
return { poly };
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,83 @@
|
||||
#pragma once
|
||||
|
||||
#include "../libslic3r.h"
|
||||
#include "../Point.hpp"
|
||||
#include "../Polygon.hpp"
|
||||
#include "../Slicing.hpp"
|
||||
#include "../PrintConfig.hpp"
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
class PrintObject;
|
||||
|
||||
// Belt floor context: encapsulates the parameters and polygon computation
|
||||
// for belt printer floor clipping in support generation.
|
||||
//
|
||||
// All belt floor code across SupportMaterial, TreeSupport, TreeSupport3D,
|
||||
// and TreeModelVolumes uses the same 4 parameters and the same 4-case
|
||||
// polygon construction. This class consolidates that logic.
|
||||
//
|
||||
// Construct once per PrintObject, then call surface_polygon() or
|
||||
// valid_region_polygon() per layer with the layer's print_z.
|
||||
class BeltFloorContext
|
||||
{
|
||||
public:
|
||||
BeltFloorContext() = default;
|
||||
|
||||
// Initialize from slicing parameters and print config.
|
||||
// Uses global Z coordinates (for SupportMaterial, non-organic TreeSupport).
|
||||
bool init(const SlicingParameters &sp, const PrintConfig &pcfg);
|
||||
|
||||
// Initialize with a Z offset subtracted from z_shift.
|
||||
// Uses local Z coordinates (for TreeSupport3D, TreeModelVolumes organic pipeline).
|
||||
bool init_local(const SlicingParameters &sp, const PrintConfig &pcfg,
|
||||
double global_z_offset);
|
||||
|
||||
bool is_active() const { return m_active; }
|
||||
|
||||
// Compute the belt-side half-plane polygon at a given print_z.
|
||||
// This is the region where the belt surface exists.
|
||||
Polygons surface_polygon(coordf_t print_z) const;
|
||||
|
||||
// Compute the valid-region half-plane polygon at a given print_z.
|
||||
// This is the complement: the region where support is allowed.
|
||||
Polygons valid_region_polygon(coordf_t print_z) const;
|
||||
|
||||
// Compute the belt floor Z position at a given XY position (in slicing coords).
|
||||
// Returns -infinity if not active.
|
||||
double floor_print_z(const Point &pos_slicing) const;
|
||||
|
||||
// The from_axis coordinate (unscaled, slicing frame) where the belt surface
|
||||
// crosses a horizontal plane at print_z. Inverse of floor_print_z() along
|
||||
// the shear axis. Only meaningful when is_active().
|
||||
coordf_t cutoff_u(coordf_t print_z) const
|
||||
{ return (print_z - m_z_shift - m_floor_offset) / m_shear_factor; }
|
||||
|
||||
// Pre-compute belt floor polygons for a range of layers.
|
||||
// layer_print_z(i) returns the print_z for layer index i.
|
||||
std::vector<Polygons> compute_per_layer_floors(
|
||||
size_t num_layers,
|
||||
const std::function<double(size_t)> &layer_print_z) const;
|
||||
|
||||
// Accessors
|
||||
double shear_factor() const { return m_shear_factor; }
|
||||
int from_axis() const { return m_from_axis; }
|
||||
double z_shift() const { return m_z_shift; }
|
||||
double floor_offset() const { return m_floor_offset; }
|
||||
|
||||
private:
|
||||
bool m_active = false;
|
||||
double m_shear_factor = 0.0;
|
||||
int m_from_axis = 1; // 0=X, 1=Y
|
||||
double m_z_shift = 0.0;
|
||||
double m_floor_offset = 0.0;
|
||||
|
||||
// Internal: compute the raw half-plane polygon.
|
||||
// If belt_surface=true, returns the belt side; otherwise the valid (complement) side.
|
||||
Polygons half_plane(coordf_t print_z, bool belt_surface) const;
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -290,7 +290,9 @@ SupportGeneratorLayersPtr generate_raft_base(
|
||||
// The object does not have a raft.
|
||||
// Calculate the area covered by the brim.
|
||||
const BrimType brim_type = object.config().brim_type;
|
||||
const bool brim_outer = brim_type == btOuterOnly || brim_type == btOuterAndInner;
|
||||
// btLeadingEdgeOnly only means anything on a belt printer, where this code path
|
||||
// does not run; elsewhere it degrades to an outer brim (see Brim.cpp).
|
||||
const bool brim_outer = brim_type == btOuterOnly || brim_type == btOuterAndInner || brim_type == btLeadingEdgeOnly;
|
||||
const bool brim_inner = brim_type == btInnerOnly || brim_type == btOuterAndInner;
|
||||
// BBS: the pattern of raft and brim are the same, thus the brim can be serpated by support raft.
|
||||
const auto brim_object_gap = scaled<float>(object.config().brim_object_gap.value);
|
||||
@@ -318,7 +320,13 @@ SupportGeneratorLayersPtr generate_raft_base(
|
||||
|
||||
// How much to inflate the support columns to be stable. This also applies to the 1st layer, if no raft layers are to be printed.
|
||||
const float inflate_factor_fine = float(scale_((slicing_params.raft_layers() > 1) ? 0.5 : EPSILON));
|
||||
const float inflate_factor_1st_layer = std::max(0.f, float(scale_(object.config().raft_first_layer_expansion)) - inflate_factor_fine);
|
||||
// On a belt the first support layer is the leading tip of the support, a sliver
|
||||
// where the belt crosses the layer, not a flange on a flat bed: inflating it
|
||||
// puts lines in the air ahead of the belt crossing (and into the belt behind
|
||||
// it). The belt brim takes the adhesion role instead.
|
||||
const bool belt_floor_active = std::abs(slicing_params.belt_floor_shear_factor) > EPSILON;
|
||||
const float inflate_factor_1st_layer = belt_floor_active ? 0.f :
|
||||
std::max(0.f, float(scale_(object.config().raft_first_layer_expansion)) - inflate_factor_fine);
|
||||
SupportGeneratorLayer *contacts = top_contacts .empty() ? nullptr : top_contacts .front();
|
||||
SupportGeneratorLayer *interfaces = interface_layers .empty() ? nullptr : interface_layers .front();
|
||||
SupportGeneratorLayer *base_interfaces = base_interface_layers.empty() ? nullptr : base_interface_layers.front();
|
||||
@@ -1795,7 +1803,14 @@ void generate_support_toolpaths(
|
||||
bool sheath = support_params.with_sheath;
|
||||
bool no_sort = false;
|
||||
bool done = false;
|
||||
if (base_layer.layer->bottom_z < EPSILON) {
|
||||
// Belt printers have no flat bed first layer — the belt is the tilted
|
||||
// build surface — so the dense raft_first_layer_density flange must not
|
||||
// fire anywhere, including the layer at z=0 (the belt-surface line).
|
||||
// (belt_floor_shear_factor is non-zero only when belt_printer is on.)
|
||||
// For every other printer type, support z is never negative, so this
|
||||
// matches the original "first layer at z=0" behaviour unchanged.
|
||||
const bool is_belt_printer = std::abs(slicing_params.belt_floor_shear_factor) > EPSILON;
|
||||
if (! is_belt_printer && base_layer.layer->bottom_z < EPSILON) {
|
||||
// Base flange (the 1st layer).
|
||||
filler = filler_first_layer;
|
||||
filler->angle = Geometry::deg2rad(float(config.support_angle.value + 90.));
|
||||
@@ -2053,4 +2068,10 @@ sub clip_with_shape {
|
||||
}
|
||||
*/
|
||||
|
||||
Vec2d build_plate_tilt_slope(const PrintConfig &print_config)
|
||||
{
|
||||
auto slope = [](double tilt_deg) { return std::tan(Geometry::deg2rad(std::clamp(tilt_deg, -89., 89.))); };
|
||||
return { slope(print_config.build_plate_tilt_y.value), slope(print_config.build_plate_tilt_x.value) };
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
#include "libslic3r/ExtrusionEntity.hpp"
|
||||
#include "SupportLayer.hpp"
|
||||
#include "SupportParameters.hpp"
|
||||
#include "../Point.hpp"
|
||||
#include "../libslic3r.h"
|
||||
#include <utility>
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
@@ -152,6 +154,16 @@ int idx_lower_or_equal(const std::vector<T*> &vec, int idx, FN_LOWER_EQUAL fn_lo
|
||||
return idx_lower_or_equal(vec.begin(), vec.end(), idx, fn_lower_equal);
|
||||
}
|
||||
|
||||
// Belt floor: compute the belt-side half-plane polygon at a given print_z.
|
||||
// Used to clip support polygons against the belt surface.
|
||||
Polygons belt_floor_surface_polygon(
|
||||
const SlicingParameters &slicing_params, const PrintConfig &print_config,
|
||||
const PrintObject &object, coordf_t print_z);
|
||||
|
||||
// Build plate tilt: XY drift of gravity per unit of layer height, zero on a level plate.
|
||||
// The tilt is capped below 90 degrees to keep the drift finite.
|
||||
Vec2d build_plate_tilt_slope(const PrintConfig &print_config);
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif /* slic3r_SupportCommon_hpp_ */
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "libslic3r/Support/SupportLayer.hpp"
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
#include "SupportCommon.hpp"
|
||||
#include "BeltFloorContext.hpp"
|
||||
#include "Geometry.hpp"
|
||||
#include "Point.hpp"
|
||||
#include "MutablePolygon.hpp"
|
||||
@@ -397,10 +398,21 @@ inline void layers_append(SupportGeneratorLayersPtr &dst, const SupportGenerator
|
||||
}
|
||||
|
||||
// Support layer that is covered by some form of dense interface.
|
||||
static constexpr const std::initializer_list<SupporLayerType> support_types_interface {
|
||||
static constexpr const std::initializer_list<SupporLayerType> support_types_interface {
|
||||
SupporLayerType::RaftInterface, SupporLayerType::BottomContact, SupporLayerType::BottomInterface, SupporLayerType::TopContact, SupporLayerType::TopInterface
|
||||
};
|
||||
|
||||
// Forward declarations for belt floor helpers (defined later in this file).
|
||||
// belt_floor_surface_polygon is declared in SupportCommon.hpp (non-static,
|
||||
// shared with TreeSupport.cpp).
|
||||
|
||||
static Polygons belt_floor_valid_region_polygon(
|
||||
const SlicingParameters &slicing_params, const PrintConfig &print_config,
|
||||
const PrintObject &object, coordf_t print_z);
|
||||
static void trim_support_layers_by_belt_floor(
|
||||
const SlicingParameters &slicing_params, const PrintConfig &print_config,
|
||||
const PrintObject &object, SupportGeneratorLayersPtr &support_layers);
|
||||
|
||||
void PrintObjectSupportMaterial::generate(PrintObject &object)
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(info) << "Support generator - Start";
|
||||
@@ -473,11 +485,12 @@ void PrintObjectSupportMaterial::generate(PrintObject &object)
|
||||
object, bottom_contacts, top_contacts, layer_storage);
|
||||
|
||||
this->trim_support_layers_by_object(object, top_contacts, m_slicing_params.gap_support_object, m_slicing_params.gap_object_support, m_support_params.gap_xy);
|
||||
trim_support_layers_by_belt_floor(m_slicing_params, *m_print_config, object, top_contacts);
|
||||
|
||||
#ifdef SLIC3R_DEBUG
|
||||
for (const SupportGeneratorLayer *layer : top_contacts)
|
||||
Slic3r::SVG::export_expolygons(
|
||||
debug_out_path("support-top-contacts-trimmed-by-object-%d-%lf.svg", iRun, layer->print_z),
|
||||
debug_out_path("support-top-contacts-trimmed-by-object-%d-%lf.svg", iRun, layer->print_z),
|
||||
union_ex(layer->polygons));
|
||||
#endif
|
||||
|
||||
@@ -633,6 +646,37 @@ Polygons collect_region_slices_by_type(const Layer &layer, SurfaceType surface_t
|
||||
return out;
|
||||
}
|
||||
|
||||
// Belt printer: compute the belt-side half-plane polygon at a given print_z.
|
||||
// This represents the region where the belt surface exists (the "phantom top surface").
|
||||
// Support that overlaps with this polygon should terminate with a bottom contact.
|
||||
// Returns empty if belt floor is not active.
|
||||
Polygons belt_floor_surface_polygon(
|
||||
const SlicingParameters &slicing_params,
|
||||
const PrintConfig &print_config,
|
||||
const PrintObject &object,
|
||||
coordf_t print_z)
|
||||
{
|
||||
BeltFloorContext ctx;
|
||||
if (!ctx.init(slicing_params, print_config))
|
||||
return {};
|
||||
return ctx.surface_polygon(print_z);
|
||||
}
|
||||
|
||||
// Belt printer: compute the valid-region half-plane polygon at a given print_z.
|
||||
// This is the region where support is allowed to exist (above the belt).
|
||||
// Used to clip the downward-propagating support projection.
|
||||
static Polygons belt_floor_valid_region_polygon(
|
||||
const SlicingParameters &slicing_params,
|
||||
const PrintConfig &print_config,
|
||||
const PrintObject &object,
|
||||
coordf_t print_z)
|
||||
{
|
||||
BeltFloorContext ctx;
|
||||
if (!ctx.init(slicing_params, print_config))
|
||||
return {};
|
||||
return ctx.valid_region_polygon(print_z);
|
||||
}
|
||||
|
||||
// Collect outer contours of all slices of this layer.
|
||||
// This is useful for calculating the support base with holes filled.
|
||||
Polygons collect_slices_outer(const Layer &layer)
|
||||
@@ -1424,6 +1468,9 @@ static inline ExPolygons detect_overhangs(
|
||||
const double threshold_rad = Geometry::deg2rad(thresh_angle);
|
||||
const bool bridge_no_support = object_config.bridge_no_support.value;
|
||||
const coordf_t xy_expansion = scale_(object_config.support_expansion.value);
|
||||
// Build plate tilt: compute per-layer XY shift for tilted gravity direction
|
||||
const Vec2d tilt_slope = build_plate_tilt_slope(print_config);
|
||||
const bool has_tilt = tilt_slope.cwiseAbs().maxCoeff() > EPSILON;
|
||||
float lower_layer_offset = 0;
|
||||
|
||||
if (layer_id == 0)
|
||||
@@ -1457,6 +1504,16 @@ static inline ExPolygons detect_overhangs(
|
||||
}
|
||||
}
|
||||
|
||||
// Apply build plate tilt: shift lower layer polygons to simulate tilted gravity.
|
||||
// This is loop-invariant across regions, so compute it once here.
|
||||
const Polygons *effective_lower = &lower_layer_polygons;
|
||||
Polygons tilted_lower;
|
||||
if (has_tilt) {
|
||||
tilted_lower = lower_layer_polygons;
|
||||
translate(tilted_lower, Point::new_scale(tilt_slope * lower_layer.height));
|
||||
effective_lower = &tilted_lower;
|
||||
}
|
||||
|
||||
for (LayerRegion *layerm : layer.regions()) {
|
||||
// Extrusion width accounts for the roundings of the extrudates.
|
||||
// It is the maximum widh of the extrudate.
|
||||
@@ -1475,7 +1532,7 @@ static inline ExPolygons detect_overhangs(
|
||||
Polygons layerm_polygons = to_polygons(layerm->slices.surfaces);
|
||||
if (lower_layer_offset == 0.f) {
|
||||
// Support everything.
|
||||
diff_polygons = diff(layerm_polygons, lower_layer_polygons);
|
||||
diff_polygons = diff(layerm_polygons, *effective_lower);
|
||||
if (buildplate_only) {
|
||||
// Don't support overhangs above the top surfaces.
|
||||
// This step is done before the contact surface is calculated by growing the overhang region.
|
||||
@@ -1484,9 +1541,9 @@ static inline ExPolygons detect_overhangs(
|
||||
} else if (auto_normal_support) {
|
||||
// Get the regions needing a suport, collapse very tiny spots.
|
||||
//FIXME cache the lower layer offset if this layer has multiple regions.
|
||||
diff_polygons =
|
||||
diff_polygons =
|
||||
diff(layerm_polygons,
|
||||
expand(lower_layer_polygons, lower_layer_offset, SUPPORT_SURFACES_OFFSET_PARAMETERS));
|
||||
expand(*effective_lower, lower_layer_offset, SUPPORT_SURFACES_OFFSET_PARAMETERS));
|
||||
if (buildplate_only && ! annotations.buildplate_covered[layer_id].empty()) {
|
||||
// Don't support overhangs above the top surfaces.
|
||||
// This step is done before the contact surface is calculated by growing the overhang region.
|
||||
@@ -1494,9 +1551,9 @@ static inline ExPolygons detect_overhangs(
|
||||
}
|
||||
if (! diff_polygons.empty()) {
|
||||
// Offset the support regions back to a full overhang, restrict them to the full overhang.
|
||||
// This is done to increase size of the supporting columns below, as they are calculated by
|
||||
// This is done to increase size of the supporting columns below, as they are calculated by
|
||||
// propagating these contact surfaces downwards.
|
||||
diff_polygons = diff(intersection(expand(diff_polygons, lower_layer_offset, SUPPORT_SURFACES_OFFSET_PARAMETERS), layerm_polygons), lower_layer_polygons);
|
||||
diff_polygons = diff(intersection(expand(diff_polygons, lower_layer_offset, SUPPORT_SURFACES_OFFSET_PARAMETERS), layerm_polygons), *effective_lower);
|
||||
}
|
||||
//FIXME add user defined filtering here based on minimal area or minimum radius or whatever.
|
||||
|
||||
@@ -2536,6 +2593,82 @@ static inline SupportGeneratorLayer* detect_bottom_contacts(
|
||||
return &layer_new;
|
||||
}
|
||||
|
||||
// Belt printer: detect bottom contacts where support meets the belt floor plane.
|
||||
// Modeled on detect_bottom_contacts() but uses the belt plane polygon instead of stTop surfaces.
|
||||
static inline SupportGeneratorLayer* detect_belt_floor_bottom_contacts(
|
||||
const SlicingParameters &slicing_params,
|
||||
const SupportParameters &support_params,
|
||||
const PrintConfig &print_config,
|
||||
const PrintObject &object,
|
||||
const Layer &layer,
|
||||
// Existing top contact layers, for snapping.
|
||||
const SupportGeneratorLayersPtr &top_contacts,
|
||||
size_t contact_idx,
|
||||
SupportGeneratorLayerStorage &layer_storage,
|
||||
std::vector<Polygons> &layer_support_areas,
|
||||
const Polygons &supports_projected)
|
||||
{
|
||||
// Compute the belt surface polygon at this layer's Z.
|
||||
Polygons belt_surface = belt_floor_surface_polygon(slicing_params, print_config, object, layer.print_z);
|
||||
if (belt_surface.empty())
|
||||
return nullptr;
|
||||
|
||||
// Find where projected support overlaps the belt surface.
|
||||
Polygons touching = intersection(belt_surface, supports_projected);
|
||||
if (touching.empty())
|
||||
return nullptr;
|
||||
|
||||
assert(layer.id() >= slicing_params.raft_layers());
|
||||
size_t layer_id = layer.id() - slicing_params.raft_layers();
|
||||
|
||||
// Allocate a new bottom contact layer resting on the belt plane.
|
||||
SupportGeneratorLayer &layer_new = layer_storage.allocate_unguarded(SupporLayerType::BottomContact);
|
||||
|
||||
// No object layer to sync with -- compute heights directly from flow parameters.
|
||||
layer_new.height = support_params.support_material_bottom_interface_flow.height();
|
||||
layer_new.print_z = layer.print_z + layer_new.height + slicing_params.gap_object_support;
|
||||
layer_new.bottom_z = layer.print_z;
|
||||
layer_new.idx_object_layer_below = layer_id;
|
||||
layer_new.bridging = ! slicing_params.zero_gap_interface_bottom && object.config().thick_bridges;
|
||||
layer_new.polygons = expand(touching, float(support_params.support_material_flow.scaled_width()), SUPPORT_SURFACES_OFFSET_PARAMETERS);
|
||||
|
||||
if (! slicing_params.zero_gap_interface_bottom) {
|
||||
// Snap to nearby top contact layers to avoid very thin support layers.
|
||||
for (size_t top_idx = size_t(std::max<int>(0, int(contact_idx)));
|
||||
top_idx < top_contacts.size() && top_contacts[top_idx]->print_z < layer_new.print_z + support_params.support_layer_height_min + EPSILON;
|
||||
++ top_idx) {
|
||||
if (top_contacts[top_idx]->print_z > layer_new.print_z - support_params.support_layer_height_min - EPSILON) {
|
||||
coordf_t diff = layer_new.print_z - top_contacts[top_idx]->print_z;
|
||||
assert(std::abs(diff) <= support_params.support_layer_height_min + EPSILON);
|
||||
if (diff > 0.F) {
|
||||
if (layer_new.height - diff > support_params.support_layer_height_min) {
|
||||
layer_new.print_z = top_contacts[top_idx]->print_z;
|
||||
layer_new.height -= diff;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
layer_new.print_z = top_contacts[top_idx]->print_z;
|
||||
layer_new.height -= diff;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Trim the already created base layers above this belt contact.
|
||||
touching = expand(touching, float(SCALED_EPSILON));
|
||||
for (int layer_id_above = int(layer_id) + 1; layer_id_above < int(object.total_layer_count()); ++ layer_id_above) {
|
||||
const Layer &layer_above = *object.layers()[layer_id_above];
|
||||
if (layer_above.print_z > layer_new.print_z - EPSILON)
|
||||
break;
|
||||
if (Polygons &above = layer_support_areas[layer_id_above]; ! above.empty())
|
||||
above = diff(above, touching);
|
||||
}
|
||||
|
||||
return &layer_new;
|
||||
}
|
||||
|
||||
// Returns polygons to print + polygons to propagate downwards.
|
||||
// Called twice: First for normal supports, possibly trimmed by "on build plate only", second for support enforcers not trimmed by "on build plate only".
|
||||
static inline std::pair<Polygons, Polygons> project_support_to_grid(const Layer &layer, const SupportGridParams &grid_params, const Polygons &overhangs, Polygons *layer_buildplate_covered
|
||||
@@ -2635,6 +2768,7 @@ SupportGeneratorLayersPtr PrintObjectSupportMaterial::bottom_contact_layers_and_
|
||||
//const auto expansion_to_slice = m_support_material_flow.scaled_spacing() / 2 + 25;
|
||||
const SupportGridParams grid_params(*m_object_config, m_support_params.support_material_flow);
|
||||
const bool buildplate_only = ! buildplate_covered.empty();
|
||||
const bool has_belt_floor = std::abs(m_slicing_params.belt_floor_shear_factor) > EPSILON;
|
||||
|
||||
// Allocate empty surface areas, one per object layer.
|
||||
layer_support_areas.assign(object.total_layer_count(), Polygons());
|
||||
@@ -2695,8 +2829,9 @@ SupportGeneratorLayersPtr PrintObjectSupportMaterial::bottom_contact_layers_and_
|
||||
tbb::task_group task_group;
|
||||
const Polygons &overhangs_for_bottom_contacts = buildplate_only ? enforcers_projection_raw : overhangs_projection_raw;
|
||||
if (! overhangs_for_bottom_contacts.empty())
|
||||
// Find the bottom contact layers above the top surfaces of this layer.
|
||||
task_group.run([this, &object, &layer, &top_contacts, contact_idx, &layer_storage, &layer_support_areas, &bottom_contacts, &overhangs_for_bottom_contacts
|
||||
// Find the bottom contact layers above the top surfaces of this layer,
|
||||
// and also detect belt floor contacts if belt mode is active.
|
||||
task_group.run([this, &object, &layer, &top_contacts, contact_idx, &layer_storage, &layer_support_areas, &bottom_contacts, &overhangs_for_bottom_contacts, has_belt_floor
|
||||
#ifdef SLIC3R_DEBUG
|
||||
, iRun, &polygons_new
|
||||
#endif // SLIC3R_DEBUG
|
||||
@@ -2710,6 +2845,15 @@ SupportGeneratorLayersPtr PrintObjectSupportMaterial::bottom_contact_layers_and_
|
||||
);
|
||||
if (layer_new)
|
||||
bottom_contacts.push_back(layer_new);
|
||||
// Belt floor phantom surface: detect where support meets the belt plane.
|
||||
if (has_belt_floor) {
|
||||
SupportGeneratorLayer *belt_layer = detect_belt_floor_bottom_contacts(
|
||||
m_slicing_params, m_support_params, *m_print_config, object,
|
||||
layer, top_contacts, contact_idx, layer_storage,
|
||||
layer_support_areas, overhangs_for_bottom_contacts);
|
||||
if (belt_layer)
|
||||
bottom_contacts.push_back(belt_layer);
|
||||
}
|
||||
});
|
||||
|
||||
Polygons &layer_support_area = layer_support_areas[layer_id];
|
||||
@@ -2748,6 +2892,23 @@ SupportGeneratorLayersPtr PrintObjectSupportMaterial::bottom_contact_layers_and_
|
||||
|
||||
task_group.wait();
|
||||
|
||||
// Belt floor: clip projections and support areas so support doesn't
|
||||
// propagate below the belt plane.
|
||||
if (has_belt_floor) {
|
||||
Polygons valid_region = belt_floor_valid_region_polygon(
|
||||
m_slicing_params, *m_print_config, object, layer.print_z);
|
||||
if (! valid_region.empty()) {
|
||||
if (! overhangs_projection.empty())
|
||||
overhangs_projection = intersection(overhangs_projection, valid_region);
|
||||
if (! enforcers_projection.empty())
|
||||
enforcers_projection = intersection(enforcers_projection, valid_region);
|
||||
if (! layer_support_area.empty())
|
||||
layer_support_area = intersection(layer_support_area, valid_region);
|
||||
if (! layer_support_area_enforcers.empty())
|
||||
layer_support_area_enforcers = intersection(layer_support_area_enforcers, valid_region);
|
||||
}
|
||||
}
|
||||
|
||||
if (! layer_support_area_enforcers.empty()) {
|
||||
if (layer_support_area.empty())
|
||||
layer_support_area = std::move(layer_support_area_enforcers);
|
||||
@@ -2758,6 +2919,7 @@ SupportGeneratorLayersPtr PrintObjectSupportMaterial::bottom_contact_layers_and_
|
||||
|
||||
std::reverse(bottom_contacts.begin(), bottom_contacts.end());
|
||||
trim_support_layers_by_object(object, bottom_contacts, m_slicing_params.gap_support_object, m_slicing_params.gap_object_support, m_support_params.gap_xy);
|
||||
trim_support_layers_by_belt_floor(m_slicing_params, *m_print_config, object, bottom_contacts);
|
||||
return bottom_contacts;
|
||||
}
|
||||
|
||||
@@ -3131,6 +3293,31 @@ void PrintObjectSupportMaterial::generate_base_layers(
|
||||
#endif /* SLIC3R_DEBUG */
|
||||
|
||||
this->trim_support_layers_by_object(object, intermediate_layers, m_slicing_params.gap_support_object, m_slicing_params.gap_object_support, m_support_params.gap_xy);
|
||||
trim_support_layers_by_belt_floor(m_slicing_params, *m_print_config, object, intermediate_layers);
|
||||
}
|
||||
|
||||
// Belt printer: trim support layer polygons by the belt floor plane.
|
||||
// For each support layer, computes the belt floor half-plane at that layer's print_z
|
||||
// and subtracts it from the support polygons. This follows the same diff() pattern
|
||||
// as trim_support_layers_by_object() so that interface layers derived from trimmed
|
||||
// intermediates automatically inherit the belt floor trimming.
|
||||
static void trim_support_layers_by_belt_floor(
|
||||
const SlicingParameters &slicing_params,
|
||||
const PrintConfig &print_config,
|
||||
const PrintObject &object,
|
||||
SupportGeneratorLayersPtr &support_layers)
|
||||
{
|
||||
BeltFloorContext ctx;
|
||||
if (!ctx.init(slicing_params, print_config))
|
||||
return;
|
||||
|
||||
tbb::parallel_for(tbb::blocked_range<size_t>(0, support_layers.size()),
|
||||
[&](const tbb::blocked_range<size_t> &range) {
|
||||
for (size_t i = range.begin(); i < range.end(); ++i)
|
||||
if (support_layers[i])
|
||||
support_layers[i]->polygons = diff(support_layers[i]->polygons,
|
||||
ctx.surface_polygon(support_layers[i]->print_z));
|
||||
});
|
||||
}
|
||||
|
||||
void PrintObjectSupportMaterial::trim_support_layers_by_object(
|
||||
|
||||
@@ -752,7 +752,9 @@ std::tuple<ObjectPart, float> build_object_part_from_slice(const size_t &slice_i
|
||||
// thus has lower adhesion. For now this effect will be neglected.
|
||||
ExPolygon slice_poly = layer->lslices[slice_idx];
|
||||
ExPolygons brim;
|
||||
if (params.brim_type == BrimType::btOuterAndInner || params.brim_type == BrimType::btOuterOnly) {
|
||||
// btLeadingEdgeOnly degrades to an outer brim off belt printers (see Brim.cpp).
|
||||
if (params.brim_type == BrimType::btOuterAndInner || params.brim_type == BrimType::btOuterOnly
|
||||
|| params.brim_type == BrimType::btLeadingEdgeOnly) {
|
||||
Polygon brim_hole = slice_poly.contour;
|
||||
brim_hole.reverse();
|
||||
Polygons c = expand(slice_poly.contour, scale_(params.brim_width)); // For very small polygons, the expand may result in empty vector, even thought the input is correct.
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "libslic3r/Polygon.hpp"
|
||||
#include "libslic3r/ExPolygon.hpp"
|
||||
#include "TreeSupportCommon.hpp"
|
||||
#include "BeltFloorContext.hpp"
|
||||
|
||||
#include "../BuildVolume.hpp"
|
||||
#include "../ClipperUtils.hpp"
|
||||
@@ -19,6 +20,7 @@
|
||||
#include "../Utils.hpp"
|
||||
#include "../format.hpp"
|
||||
#include "libslic3r/libslic3r.h"
|
||||
#include "../PrintConfig.hpp"
|
||||
|
||||
#include <cstddef>
|
||||
#include <algorithm>
|
||||
@@ -115,6 +117,21 @@ TreeModelVolumes::TreeModelVolumes(
|
||||
m_increase_until_radius = config.increase_radius_until_radius;
|
||||
m_radius_0 = config.getRadius(0);
|
||||
m_raft_layers = config.raft_layers;
|
||||
// Support blockers are consumed in the same index space as m_layer_outlines
|
||||
// (object layer i lives at index num_raft_layers + i), but
|
||||
// slice_support_blockers() returns them in object-layer space. Shift them.
|
||||
//
|
||||
// The belt surface is deliberately NOT a blocker. A blocker is a collision,
|
||||
// and a branch descending onto a collision slides off it: on a belt that
|
||||
// walks the branch down the tilted surface, ahead of the part, until it
|
||||
// reaches the bottom layer floating in mid-air. The belt is where branches
|
||||
// END: organic_draw_branches() clips their slices with m_belt_floor and the
|
||||
// first clipped slice is the contact.
|
||||
{
|
||||
const size_t num_raft = m_raft_layers.size();
|
||||
if (num_raft > 0 && ! m_anti_overhang.empty())
|
||||
m_anti_overhang.insert(m_anti_overhang.begin(), num_raft, Polygons{});
|
||||
}
|
||||
m_current_outline_idx = 0;
|
||||
|
||||
m_layer_outlines.emplace_back(mesh_settings, std::vector<Polygons>{});
|
||||
@@ -127,6 +144,32 @@ TreeModelVolumes::TreeModelVolumes(
|
||||
for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++ layer_idx)
|
||||
outlines[layer_idx] = polygons_simplify(to_polygons(print_object.get_layer(layer_idx - num_raft_layers)->lslices), mesh_settings.resolution);
|
||||
});
|
||||
|
||||
// Belt floor: pre-compute belt surface polygon per-layer for clipping.
|
||||
// Branches grow toward the belt and their slices are clipped at the belt
|
||||
// surface in organic_draw_branches(). The organic pipeline works in LOCAL
|
||||
// Z (no global_z_offset), so use local z_shift and local print_z.
|
||||
{
|
||||
const auto &slicing_params = print_object.slicing_parameters();
|
||||
const auto &pcfg2 = print_object.print()->config();
|
||||
BeltFloorContext ctx;
|
||||
ctx.init_local(slicing_params, pcfg2, print_object.belt_global_z_offset());
|
||||
if (ctx.is_active()) {
|
||||
m_belt_floor = ctx.compute_per_layer_floors(num_layers, [&](size_t layer_idx) -> double {
|
||||
// Object layers: local print_z (subtract global offset).
|
||||
if (layer_idx >= num_raft_layers)
|
||||
return print_object.get_layer(layer_idx - num_raft_layers)->print_z
|
||||
- print_object.belt_global_z_offset();
|
||||
// Belt raft layers (below the object): each carries its own
|
||||
// local print_z in m_raft_layers. The belt floor is a tilted
|
||||
// plane, so the half-plane to clip grows as print_z drops —
|
||||
// using 0 here clipped every below-object layer against the
|
||||
// Z=0 belt surface, under-clipping the raft region and leaving
|
||||
// a dense support mass below the floor.
|
||||
return (layer_idx < m_raft_layers.size()) ? m_raft_layers[layer_idx] : 0.;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -484,7 +527,7 @@ void TreeModelVolumes::calculateCollision(const coord_t radius, const LayerIndex
|
||||
// 2) Sum over top / bottom ranges.
|
||||
const bool processing_last_mesh = outline_idx == layer_outline_indices.back();
|
||||
tbb::parallel_for(tbb::blocked_range<LayerIndex>(data.begin(), data.end()),
|
||||
[&collision_areas_offsetted, &outlines, &machine_border = m_machine_border, &anti_overhang = m_anti_overhang, radius,
|
||||
[&collision_areas_offsetted, &outlines, &machine_border = m_machine_border, &anti_overhang = m_anti_overhang, radius,
|
||||
xy_distance, z_distance_bottom_layers, z_distance_top_layers, min_resolution = m_min_resolution, &data, processing_last_mesh, &throw_on_cancel]
|
||||
(const tbb::blocked_range<LayerIndex>& range) {
|
||||
for (LayerIndex layer_idx = range.begin(); layer_idx != range.end(); ++ layer_idx) {
|
||||
@@ -530,9 +573,14 @@ void TreeModelVolumes::calculateCollision(const coord_t radius, const LayerIndex
|
||||
// not support an overhang<90 degree than to risk fusing to it.
|
||||
append(collisions, offset(union_ex(collision_areas_original), radius + required_range_x, jtMiter, 1.2));
|
||||
}
|
||||
collisions = processing_last_mesh && layer_idx < int(anti_overhang.size()) ?
|
||||
union_(collisions, offset(union_ex(anti_overhang[layer_idx]), radius, jtMiter, 1.2)) :
|
||||
union_(collisions);
|
||||
if (processing_last_mesh) {
|
||||
if (layer_idx < int(anti_overhang.size()))
|
||||
append(collisions, offset(union_ex(anti_overhang[layer_idx]), radius, jtMiter, 1.2));
|
||||
// NOTE: m_belt_floor is NOT added to collision here — branches
|
||||
// should grow toward the belt and terminate at it, not avoid it.
|
||||
// Belt floor clipping is done post-generation in organic_draw_branches().
|
||||
}
|
||||
collisions = union_(collisions);
|
||||
auto &dst = data[layer_idx];
|
||||
if (processing_last_mesh) {
|
||||
if (! dst.empty())
|
||||
|
||||
@@ -176,6 +176,9 @@ public:
|
||||
}
|
||||
|
||||
Polygon m_bed_area;
|
||||
// Belt floor polygons per layer — used for post-generation clipping
|
||||
// in organic_draw_branches(). Public so the organic pipeline can access it.
|
||||
std::vector<Polygons> m_belt_floor;
|
||||
|
||||
private:
|
||||
// Caching polygons for a range of layers.
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
#include "TreeSupportCommon.hpp"
|
||||
#include "TreeSupport.hpp"
|
||||
#include "TreeSupport3D.hpp"
|
||||
#include "BeltFloorContext.hpp"
|
||||
#include "libslic3r/libslic3r.h"
|
||||
#include <libnest2d/backends/libslic3r/geometries.hpp>
|
||||
#include <libnest2d/placers/nfpplacer.hpp>
|
||||
@@ -707,6 +708,33 @@ TreeSupport::TreeSupport(PrintObject& object, const SlicingParameters &slicing_p
|
||||
}
|
||||
|
||||
|
||||
double TreeSupport::belt_floor_print_z(const Point &pos_slicing) const
|
||||
{
|
||||
BeltFloorContext ctx;
|
||||
if (!ctx.init(m_slicing_params, *m_print_config))
|
||||
return -std::numeric_limits<double>::infinity();
|
||||
return ctx.floor_print_z(pos_slicing);
|
||||
}
|
||||
|
||||
bool TreeSupport::belt_node_landed(const Point &pos_slicing, double radius, double print_z) const
|
||||
{
|
||||
BeltFloorContext ctx;
|
||||
if (!ctx.init(m_slicing_params, *m_print_config))
|
||||
return false;
|
||||
return print_z <= ctx.floor_print_z(pos_slicing) - std::abs(ctx.shear_factor()) * std::max(0., radius);
|
||||
}
|
||||
|
||||
bool TreeSupport::belt_polygon_landed(const ExPolygon &poly, double print_z) const
|
||||
{
|
||||
BeltFloorContext ctx;
|
||||
if (!ctx.init(m_slicing_params, *m_print_config))
|
||||
return false;
|
||||
double min_floor = std::numeric_limits<double>::max();
|
||||
for (const Point &pt : poly.contour.points)
|
||||
min_floor = std::min(min_floor, ctx.floor_print_z(pt));
|
||||
return print_z <= min_floor;
|
||||
}
|
||||
|
||||
#define SUPPORT_SURFACES_OFFSET_PARAMETERS jtSquare, 0.
|
||||
void TreeSupport::detect_overhangs(bool check_support_necessity/* = false*/)
|
||||
{
|
||||
@@ -741,6 +769,21 @@ void TreeSupport::detect_overhangs(bool check_support_necessity/* = false*/)
|
||||
double thresh_angle = config.support_threshold_angle.value > EPSILON ? config.support_threshold_angle.value + 1 : 30;
|
||||
thresh_angle = std::min(thresh_angle, 89.); // should be smaller than 90
|
||||
const double threshold_rad = Geometry::deg2rad(thresh_angle);
|
||||
// Build plate tilt: compute per-layer XY shift for tilted gravity direction
|
||||
const PrintConfig& print_cfg = m_object->print()->config();
|
||||
const Vec2d tilt_slope = build_plate_tilt_slope(print_cfg);
|
||||
const bool has_tilt = tilt_slope.cwiseAbs().maxCoeff() > EPSILON;
|
||||
|
||||
// Belt printers: the object is pre-rotated by the belt angle before slicing, so a wall
|
||||
// that is vertical in the world advances by one layer height per layer in the sliced
|
||||
// frame. The build-plate tilt shift above compensates for that, but its direction has to
|
||||
// follow the belt shear -- the sign and axis are already known exactly from the slicing
|
||||
// parameters, so take them from there rather than from tan(build_plate_tilt), which
|
||||
// carries a magnitude but no direction. Non-belt tilted beds keep the existing behaviour.
|
||||
BeltFloorContext ovh_belt_ctx;
|
||||
const bool belt_ovh_active = ovh_belt_ctx.init(m_slicing_params, print_cfg);
|
||||
const double belt_shear = ovh_belt_ctx.shear_factor();
|
||||
const int belt_axis = ovh_belt_ctx.from_axis();
|
||||
// FIXME this is a fudge constant!
|
||||
double support_tree_tip_diameter = 0.8;
|
||||
auto enforcer_overhang_offset = scaled<double>(support_tree_tip_diameter);
|
||||
@@ -883,8 +926,64 @@ void TreeSupport::detect_overhangs(bool check_support_necessity/* = false*/)
|
||||
ExPolygons& curr_polys = layer->lslices_extrudable;
|
||||
ExPolygons& lower_polys = lower_layer->lslices_extrudable;
|
||||
|
||||
// Apply build plate tilt: shift lower layer polygons to simulate tilted gravity
|
||||
//
|
||||
// On a belt the object's very first slice can come out empty (the bottom
|
||||
// vertex is a sub-extrudable sliver), leaving the layer above it with an
|
||||
// empty predecessor even though it rests on the belt. That case needs no
|
||||
// special handling here: the belt surface is unioned into effective_lower
|
||||
// below and sampled at the bottom of the layer, so a contacting island is
|
||||
// covered and a genuinely floating one still reports its overhang. Doing it
|
||||
// that way keeps the decision per-island -- an earlier whole-layer skip,
|
||||
// conditioned on the nearest point of the *union* of the cross-section,
|
||||
// let one contacting island silence a separate floating one.
|
||||
ExPolygons shifted_lower;
|
||||
if (belt_ovh_active || has_tilt) {
|
||||
shifted_lower = lower_polys; // copy
|
||||
const double lh = lower_layer->height;
|
||||
Point tilt_shift(0, 0);
|
||||
if (belt_ovh_active) {
|
||||
// Advance the lower layer along the belt by exactly the amount a
|
||||
// world-vertical wall moves per layer, so such a wall stops reading
|
||||
// as an overhang. Sign comes from the shear, not from a tilt angle.
|
||||
const coord_t d = coord_t(-scale_(lh * belt_shear));
|
||||
if (belt_axis == 0) tilt_shift.x() = d; else tilt_shift.y() = d;
|
||||
} else {
|
||||
tilt_shift = Point::new_scale(tilt_slope * lh);
|
||||
}
|
||||
translate(shifted_lower, tilt_shift);
|
||||
}
|
||||
ExPolygons effective_lower = (belt_ovh_active || has_tilt) ? shifted_lower : lower_polys;
|
||||
|
||||
// Belt printers: material resting on the belt is held up by the belt, not by
|
||||
// the layer below it, so the belt surface counts as support from underneath.
|
||||
// Without this the object's belt-contact face reads as a fresh overhang on
|
||||
// every layer -- the leading strip that produced the spurious support nub.
|
||||
if (belt_ovh_active) {
|
||||
// surface_polygon() is a +/-1000mm half-plane. Unioning that raw with
|
||||
// 20mm-scale geometry and then offsetting it puts a huge dynamic range
|
||||
// through Clipper, which left intermittent artefacts every few layers.
|
||||
// Clip it to the layer's own bounding box first.
|
||||
// Evaluate the belt surface at the BOTTOM of the layer, not its top:
|
||||
// a layer meets the belt across its whole thickness, and print_z is the
|
||||
// top. On the object's first layer -- which is thicker, and whose lower
|
||||
// layer is empty -- using print_z left the leading 0.37mm uncovered and
|
||||
// produced the one remaining spurious overhang.
|
||||
Polygons belt_surface = ovh_belt_ctx.surface_polygon(layer->print_z - layer->height);
|
||||
if (! belt_surface.empty()) {
|
||||
BoundingBox clip_bb = get_extents(curr_polys);
|
||||
clip_bb.merge(get_extents(lower_polys));
|
||||
clip_bb.offset(scale_(10.));
|
||||
belt_surface = intersection(belt_surface, Polygons{ clip_bb.polygon() });
|
||||
if (! belt_surface.empty()) {
|
||||
append(effective_lower, union_ex(belt_surface));
|
||||
effective_lower = union_ex(effective_lower);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// normal overhang
|
||||
ExPolygons lower_layer_offseted = offset_ex(lower_polys, support_offset_scaled, SUPPORT_SURFACES_OFFSET_PARAMETERS);
|
||||
ExPolygons lower_layer_offseted = offset_ex(effective_lower, support_offset_scaled, SUPPORT_SURFACES_OFFSET_PARAMETERS);
|
||||
overhangs_all_layers[layer_nr] = diff_ex(curr_polys, lower_layer_offseted);
|
||||
|
||||
double duration{ std::chrono::duration_cast<second_>(clock_::now() - t0).count() };
|
||||
@@ -900,8 +999,13 @@ void TreeSupport::detect_overhangs(bool check_support_necessity/* = false*/)
|
||||
for (const ExPolygon& expoly : curr_polys) {
|
||||
bool is_sharp_tail = false;
|
||||
// 1. nothing below
|
||||
// this is a sharp tail region if it's floating and non-ignorable
|
||||
if (!overlaps(offset_ex(expoly, 0.1 * extrusion_width_scaled), lower_polys)) {
|
||||
// this is a sharp tail region if it's floating and non-ignorable.
|
||||
// On a belt, "below" has to include the belt itself and the
|
||||
// shear-advanced lower layer, or every belt-contact island reads as
|
||||
// a sharp tail -- which is what the empty-predecessor skip above was
|
||||
// really masking. effective_lower is exactly that notion of below.
|
||||
const ExPolygons &tail_lower = belt_ovh_active ? effective_lower : lower_polys;
|
||||
if (!overlaps(offset_ex(expoly, 0.1 * extrusion_width_scaled), tail_lower)) {
|
||||
is_sharp_tail = !offset_ex(expoly, -0.1 * extrusion_width_scaled).empty();
|
||||
}
|
||||
|
||||
@@ -1524,6 +1628,15 @@ void TreeSupport::generate_toolpaths()
|
||||
// ORCA: base angle used for explicit interlaced interface orientation.
|
||||
const float base_support_angle = Geometry::deg2rad(object_config.support_angle.value);
|
||||
|
||||
// Belt floor: the lowest support layer rests on the moving, tilted belt, not
|
||||
// on a flat bed — so it must NOT get the bed first-layer treatment (a brim on
|
||||
// interface areas, a first-layer-flow sheath at raft_first_layer_density on
|
||||
// base areas). That treatment draws a loop along the Z=0 belt-floor line that
|
||||
// reads as a stray brim/skirt. Gate those layer_id==0 special cases off when
|
||||
// the belt floor is active; false on non-belt printers so behavior is unchanged.
|
||||
BeltFloorContext belt_ctx;
|
||||
const bool belt_floor_active = belt_ctx.init(m_slicing_params, *m_print_config);
|
||||
|
||||
// generate tree support tool paths
|
||||
tbb::parallel_for(
|
||||
tbb::blocked_range<size_t>(m_raft_layers, m_object->support_layer_count()),
|
||||
@@ -1559,7 +1672,7 @@ void TreeSupport::generate_toolpaths()
|
||||
filler_interface->angle = m_support_params.support_interface_angle(area_group.interface_id);
|
||||
if (area_group.type != SupportLayer::BaseType) {
|
||||
// interface
|
||||
if (layer_id == 0) {
|
||||
if (layer_id == 0 && !belt_floor_active) {
|
||||
Flow flow = m_raft_layers == 0 ? m_object->print()->brim_flow() : support_flow;
|
||||
ExtrusionRole brim_role = (area_group.type == SupportLayer::RoofType && !area_group.interface_as_base) ?
|
||||
erSupportMaterialInterface : erSupportMaterial;
|
||||
@@ -1632,7 +1745,7 @@ void TreeSupport::generate_toolpaths()
|
||||
}
|
||||
else {
|
||||
// base_areas
|
||||
bool support_base_on_bed = (layer_id == 0 && m_raft_layers == 0);
|
||||
bool support_base_on_bed = (layer_id == 0 && m_raft_layers == 0 && !belt_floor_active);
|
||||
Flow flow = support_base_on_bed ? m_support_params.first_layer_flow : support_flow;
|
||||
bool need_infill = with_infill;
|
||||
if(m_object_config->support_base_pattern==smpDefault)
|
||||
@@ -1653,7 +1766,7 @@ void TreeSupport::generate_toolpaths()
|
||||
std::unique_ptr<ExtrusionEntityCollection> base_eec = std::make_unique<ExtrusionEntityCollection>();
|
||||
base_eec->no_sort = true;
|
||||
ExtrusionEntitiesPtr &base_dst = base_eec->entities;
|
||||
if (layer_id == 0) {
|
||||
if (layer_id == 0 && !belt_floor_active) {
|
||||
float density = float(m_object_config->raft_first_layer_density.value * 0.01);
|
||||
fill_expolygons_with_sheath_generate_paths(base_dst, loops, filler_support.get(), density, erSupportMaterial, flow,
|
||||
m_support_params, true, false);
|
||||
@@ -2059,6 +2172,15 @@ void TreeSupport::draw_circles()
|
||||
coordf_t support_extrusion_width = m_support_params.support_extrusion_width;
|
||||
const float tree_brim_width = config.tree_support_brim_width.value;
|
||||
|
||||
// Belt floor: the first object layer is not on a flat bed — it rests on the
|
||||
// tilted, moving belt. So the first-object-layer adhesion features (the tree
|
||||
// support brim, the hybrid first-layer base expansion) must be suppressed:
|
||||
// their expanded contact rings project to a stray brim/skirt loop sitting in
|
||||
// the Z=0 belt plane around the support footprint. false on non-belt printers,
|
||||
// so behavior there is unchanged.
|
||||
BeltFloorContext belt_ctx;
|
||||
const bool belt_floor_active = belt_ctx.init(m_slicing_params, *m_print_config);
|
||||
|
||||
if (m_object->support_layer_count() <= m_raft_layers)
|
||||
return;
|
||||
BOOST_LOG_TRIVIAL(info) << "draw_circles for object: " << m_object->model_object()->name;
|
||||
@@ -2169,7 +2291,7 @@ void TreeSupport::draw_circles()
|
||||
circle.points[i] = circle.points[i] * scale + node.position;
|
||||
}
|
||||
}
|
||||
if (obj_layer_nr == 0 && m_raft_layers == 0) {
|
||||
if (obj_layer_nr == 0 && m_raft_layers == 0 && !belt_floor_active) {
|
||||
double brim_width = !config.tree_support_auto_brim ? tree_brim_width : std::max(MIN_BRANCH_RADIUS_FIRST_LAYER, std::min(node.radius + node.dist_mm_to_top / (scale * branch_radius) * 0.5, MAX_BRANCH_RADIUS_FIRST_LAYER) - node.radius);
|
||||
auto tmp=offset(circle, scale_(brim_width));
|
||||
if(!tmp.empty())
|
||||
@@ -2239,6 +2361,29 @@ void TreeSupport::draw_circles()
|
||||
base_areas = diff_ex(base_areas, ClipperUtils::clip_clipper_polygons_with_subject_bbox(roofs, get_extents(base_areas)));
|
||||
base_areas = intersection_ex(base_areas, m_machine_border);
|
||||
|
||||
// Belt floor: clip tree support polygons by the belt surface plane.
|
||||
// Non-organic tree support layers inherit their print_z from the
|
||||
// (already globally-offset) object layers — see plan_layer_heights()
|
||||
// and add_tree_support_layer(); only ORGANIC layers get the global
|
||||
// Z offset applied later in _generate_support_material(). So here
|
||||
// ts_layer->print_z is in the GLOBAL frame and we must use init()
|
||||
// (global), not init_local(): mixing a local-frame clip plane with
|
||||
// a global print_z displaces the cutoff line by belt_global_z_offset
|
||||
// along the shear axis, leaving an un-clipped wedge of support below
|
||||
// the belt floor. In per-object (non-global) mode belt_global_z_offset
|
||||
// is 0 so init() and init_local() coincide — this is a no-op there.
|
||||
{
|
||||
BeltFloorContext ctx;
|
||||
if (ctx.init(m_slicing_params, *m_print_config)) {
|
||||
Polygons belt_surface = ctx.surface_polygon(ts_layer->print_z);
|
||||
base_areas = diff_ex(base_areas, belt_surface);
|
||||
roof_areas = diff_ex(roof_areas, belt_surface);
|
||||
roof_1st_layer = diff_ex(roof_1st_layer, belt_surface);
|
||||
floor_areas = diff_ex(floor_areas, belt_surface);
|
||||
roof_gap_areas = diff_ex(roof_gap_areas, belt_surface);
|
||||
}
|
||||
}
|
||||
|
||||
if (SQUARE_SUPPORT) {
|
||||
// simplify support contours
|
||||
ExPolygons base_areas_simplified;
|
||||
@@ -2371,7 +2516,7 @@ void TreeSupport::draw_circles()
|
||||
// part. area_poly is collected from ePolygon nodes above, which are the normal
|
||||
// support nodes in Hybrid mode. Apply the expansion before area_groups and
|
||||
// lslices are built so toolpaths and brim avoidance use the same footprint.
|
||||
if (layer_nr == 0 && m_raft_layers == 0 && m_support_params.support_style == smsTreeHybrid &&
|
||||
if (layer_nr == 0 && m_raft_layers == 0 && !belt_floor_active && m_support_params.support_style == smsTreeHybrid &&
|
||||
m_object_config->raft_first_layer_expansion.value > 0.f) {
|
||||
ExPolygons expanded_base_areas;
|
||||
const float inflate_factor_1st_layer = float(scale_(m_object_config->raft_first_layer_expansion.value));
|
||||
@@ -2706,6 +2851,7 @@ void TreeSupport::drop_nodes()
|
||||
const size_t tip_layers = base_radius / layer_height; //The number of layers to be shrinking the circle to create a tip. This produces a 45 degree angle.
|
||||
const coordf_t radius_sample_resolution = m_ts_data->m_radius_sample_resolution;
|
||||
const bool support_on_buildplate_only = config.support_on_build_plate_only.value;
|
||||
const bool has_belt_floor = std::abs(m_slicing_params.belt_floor_shear_factor) > EPSILON;
|
||||
const size_t bottom_interface_layers = number_of_support_interface_bottom_layers(config);
|
||||
SupportNode::diameter_angle_scale_factor = diameter_angle_scale_factor;
|
||||
float DO_NOT_MOVER_UNDER_MM = is_slim ? 0 : 5; // do not move contact points under 5mm
|
||||
@@ -2941,16 +3087,27 @@ void TreeSupport::drop_nodes()
|
||||
node_parent = p_node->parent ? p_node : neighbour;
|
||||
// Make sure the next pass doesn't drop down either of these (since that already happened).
|
||||
node_parent->merged_neighbours.push_front(node_parent == p_node ? neighbour : p_node);
|
||||
const bool to_buildplate = !is_inside_ex(get_collision(0, obj_layer_nr_next), next_position);
|
||||
SupportNode* next_node = m_ts_data->create_node(next_position, node_parent->distance_to_top + 1, obj_layer_nr_next,
|
||||
node_parent->support_roof_layers_below - (node_parent->distance_to_top >= 0 ? 1 : 0),
|
||||
to_buildplate, node_parent, print_z_next, height_next);
|
||||
get_max_move_dist(next_node);
|
||||
m_ts_data->m_mutex.lock();
|
||||
contact_nodes[layer_nr_next].push_back(next_node);
|
||||
neighbour->valid = false;
|
||||
p_node->valid = false;
|
||||
m_ts_data->m_mutex.unlock();
|
||||
// Belt floor: a merged node ends once its whole circle is in the belt
|
||||
// (its slices are clipped to the belt plane in draw_circles(), so it
|
||||
// tapers to a tip on the belt). Treat as object-surface termination
|
||||
// (not buildplate) so the node gets floor/interface areas instead of
|
||||
// base pads.
|
||||
if (has_belt_floor && belt_node_landed(next_position, std::max(node.radius, neighbour->radius), print_z_next)) {
|
||||
std::scoped_lock lock(m_ts_data->m_mutex);
|
||||
node_parent->to_buildplate = false;
|
||||
neighbour->valid = false;
|
||||
p_node->valid = false;
|
||||
} else {
|
||||
const bool to_buildplate = !is_inside_ex(get_collision(0, obj_layer_nr_next), next_position);
|
||||
SupportNode* next_node = m_ts_data->create_node(next_position, node_parent->distance_to_top + 1, obj_layer_nr_next,
|
||||
node_parent->support_roof_layers_below - (node_parent->distance_to_top >= 0 ? 1 : 0),
|
||||
to_buildplate, node_parent, print_z_next, height_next);
|
||||
get_max_move_dist(next_node);
|
||||
std::scoped_lock lock(m_ts_data->m_mutex);
|
||||
contact_nodes[layer_nr_next].push_back(next_node);
|
||||
neighbour->valid = false;
|
||||
p_node->valid = false;
|
||||
}
|
||||
}
|
||||
else if (neighbours.size() > 1) //Don't merge leaf nodes because we would then incur movement greater than the maximum move distance.
|
||||
{
|
||||
@@ -3019,6 +3176,12 @@ void TreeSupport::drop_nodes()
|
||||
ExPolygons overhangs_next = diff_clipped({ node.overhang }, get_collision(0, obj_layer_nr_next));
|
||||
for(auto& overhang:overhangs_next) {
|
||||
Point next_pt = overhang.contour.centroid();
|
||||
// Belt floor: a polygon node ends once all of it is in the belt.
|
||||
// Treat as object-surface termination (not buildplate).
|
||||
if (has_belt_floor && belt_polygon_landed(overhang, print_z_next)) {
|
||||
p_node->to_buildplate = false;
|
||||
continue;
|
||||
}
|
||||
PendingNode pending;
|
||||
pending.position = next_pt;
|
||||
pending.distance_to_top = p_node->distance_to_top + 1;
|
||||
@@ -3166,6 +3329,14 @@ void TreeSupport::drop_nodes()
|
||||
if (is_outside) { next_layer_vertex = candidate_vertex; }
|
||||
}
|
||||
}
|
||||
// Belt floor: a node ends once its whole circle is in the belt; until
|
||||
// then it keeps dropping and draw_circles() clips each layer's circle
|
||||
// to the belt plane, so the branch tapers to a tip on the belt.
|
||||
// Treat as object-surface termination (not buildplate).
|
||||
if (has_belt_floor && belt_node_landed(next_layer_vertex, node.radius, print_z_next)) {
|
||||
p_node->to_buildplate = false;
|
||||
return; // from parallel_for_each lambda
|
||||
}
|
||||
auto next_collision = get_collision(0, obj_layer_nr_next);
|
||||
const bool to_buildplate = !is_inside_ex(m_ts_data->m_layer_outlines[obj_layer_nr_next], next_layer_vertex);
|
||||
// don't increase radius if next node will collide partially with the object (STUDIO-7883)
|
||||
@@ -3491,6 +3662,8 @@ void TreeSupport::generate_contact_points()
|
||||
const coordf_t max_bridge_length = scale_(config.max_bridge_length.value);
|
||||
coord_t radius_scaled = scale_(base_radius);
|
||||
bool on_buildplate_only = m_object_config->support_on_build_plate_only.value;
|
||||
const bool has_belt_floor = std::abs(m_slicing_params.belt_floor_shear_factor) > EPSILON;
|
||||
|
||||
//First generate grid points to cover the entire area of the print.
|
||||
BoundingBox bounding_box = m_object->bounding_box();
|
||||
const Point bounding_box_size = bounding_box.max - bounding_box.min;
|
||||
@@ -3578,6 +3751,10 @@ void TreeSupport::generate_contact_points()
|
||||
|
||||
|
||||
auto insert_point = [&](Point pt, const ExPolygon& overhang, double radius, bool force_add = false, bool add_interface=true) {
|
||||
// Belt floor: skip contact points whose bottom_z is at or below
|
||||
// the belt floor at this XY position (overhang rests on the belt).
|
||||
if (has_belt_floor && bottom_z <= belt_floor_print_z(pt))
|
||||
return (SupportNode*) nullptr;
|
||||
Point hash_pos = pt / ((radius_scaled + 1) / 1);
|
||||
SupportNode* contact_node = nullptr;
|
||||
if (force_add || !already_inserted.count(hash_pos)) {
|
||||
@@ -3613,8 +3790,10 @@ void TreeSupport::generate_contact_points()
|
||||
double radius = unscale_(overhang_bounds.radius());
|
||||
Point candidate = overhang_bounds.center();
|
||||
SupportNode *contact_node = insert_point(candidate, overhang, radius, true, true);
|
||||
contact_node->type = ePolygon;
|
||||
curr_nodes.emplace_back(contact_node);
|
||||
if (contact_node) {
|
||||
contact_node->type = ePolygon;
|
||||
curr_nodes.emplace_back(contact_node);
|
||||
}
|
||||
}
|
||||
}else{
|
||||
// otherwise, all nodes should be circle nodes
|
||||
@@ -3748,6 +3927,14 @@ TreeSupportData::TreeSupportData(const PrintObject &object, coordf_t xy_distance
|
||||
poly.simplify(scale_(m_radius_sample_resolution), &outline);
|
||||
}
|
||||
|
||||
// The belt surface is deliberately NOT part of the outlines. The outlines
|
||||
// feed the collision and avoidance maps, and a node descending onto an
|
||||
// obstacle is pushed out of it: with the belt as an obstacle the nodes slid
|
||||
// down the tilted surface, ahead of the part, instead of landing on it. The
|
||||
// belt is where a branch ENDS: drop_nodes() stops a node once its whole
|
||||
// circle is in the belt (belt_node_landed()) and draw_circles() clips every
|
||||
// layer's circles to the belt plane, so the branch tapers to a tip on it.
|
||||
|
||||
if (layer_nr == 0)
|
||||
m_layer_outlines_below.push_back(outline);
|
||||
else
|
||||
|
||||
@@ -460,6 +460,17 @@ private:
|
||||
bool is_slim = false;
|
||||
bool with_infill = false;
|
||||
|
||||
// Belt printer: compute the belt floor print_z at a given XY position (in slicing coords).
|
||||
// Returns -infinity if belt floor is not active.
|
||||
double belt_floor_print_z(const Point &pos_slicing) const;
|
||||
// Whether a node's whole circle (radius in mm) sits at or below the belt at
|
||||
// print_z. The belt is a tilted plane, so the circle's leading edge crosses it
|
||||
// |shear| * radius lower than its centre; stopping a node when its centre crosses
|
||||
// would leave that edge floating a radius above the belt.
|
||||
bool belt_node_landed(const Point &pos_slicing, double radius, double print_z) const;
|
||||
// The same for a polygon: every point of it is at or below the belt.
|
||||
bool belt_polygon_landed(const ExPolygon &poly, double print_z) const;
|
||||
|
||||
|
||||
|
||||
/*!
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
#include "Polygon.hpp"
|
||||
#include "Polyline.hpp"
|
||||
#include "MutablePolygon.hpp"
|
||||
#include "BeltFloorContext.hpp"
|
||||
#include "libslic3r/Support/TreeSupportCommon.hpp"
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
#include "libslic3r/Support/TreeModelVolumes.hpp"
|
||||
@@ -244,6 +245,9 @@ static std::vector<std::pair<TreeSupportSettings, std::vector<size_t>>> group_me
|
||||
const bool support_threshold_auto = support_threshold == 0;
|
||||
// +1 makes the threshold inclusive
|
||||
double tan_threshold = support_threshold_auto ? 0. : tan(M_PI * double(support_threshold + 1) / 180.);
|
||||
// Build plate tilt: compute per-layer XY shift for tilted gravity direction
|
||||
const Vec2d tilt_slope = build_plate_tilt_slope(print_config);
|
||||
const bool has_tilt = tilt_slope.cwiseAbs().maxCoeff() > EPSILON;
|
||||
//FIXME this is a fudge constant!
|
||||
auto enforcer_overhang_offset = scaled<double>(config.tree_support_tip_diameter.value);
|
||||
const coordf_t radius_sample_resolution = g_config_tree_support_collision_resolution;
|
||||
@@ -265,7 +269,7 @@ static std::vector<std::pair<TreeSupportSettings, std::vector<size_t>>> group_me
|
||||
size_t num_overhang_layers = support_auto ? num_object_layers : std::min(num_object_layers, std::max(size_t(support_enforce_layers), enforcers_layers.size()));
|
||||
tbb::parallel_for(tbb::blocked_range<LayerIndex>(1, num_overhang_layers),
|
||||
[&print_object, &config, &print_config, &enforcers_layers, &blockers_layers,
|
||||
support_auto, support_enforce_layers, support_threshold_auto, tan_threshold, enforcer_overhang_offset, num_raft_layers, radius_sample_resolution, &throw_on_cancel, &out]
|
||||
support_auto, support_enforce_layers, support_threshold_auto, tan_threshold, enforcer_overhang_offset, num_raft_layers, radius_sample_resolution, has_tilt, tilt_slope, &throw_on_cancel, &out]
|
||||
(const tbb::blocked_range<LayerIndex> &range) {
|
||||
for (LayerIndex layer_id = range.begin(); layer_id < range.end(); ++ layer_id) {
|
||||
const Layer ¤t_layer = *print_object.get_layer(layer_id);
|
||||
@@ -289,7 +293,15 @@ static std::vector<std::pair<TreeSupportSettings, std::vector<size_t>>> group_me
|
||||
lower_layer_offset = external_perimeter_width - float(scale_(config.support_threshold_overlap.get_abs_value(unscale_(external_perimeter_width))));
|
||||
} else
|
||||
lower_layer_offset = scaled<float>(lower_layer.height / tan_threshold);
|
||||
Polygons lower_layer_offseted = offset(lower_layer.lslices_extrudable, lower_layer_offset);
|
||||
// Apply build plate tilt: shift lower layer polygons to simulate tilted gravity
|
||||
Polygons lower_layer_offseted;
|
||||
if (has_tilt) {
|
||||
Polygons lower_src = to_polygons(lower_layer.lslices_extrudable);
|
||||
translate(lower_src, Point::new_scale(tilt_slope * lower_layer.height));
|
||||
lower_layer_offseted = offset(lower_src, lower_layer_offset);
|
||||
} else {
|
||||
lower_layer_offseted = offset(lower_layer.lslices_extrudable, lower_layer_offset);
|
||||
}
|
||||
overhangs = diff(current_layer.lslices_extrudable, lower_layer_offseted);
|
||||
if (lower_layer_offset == 0) {
|
||||
raw_overhangs = overhangs;
|
||||
@@ -3633,6 +3645,26 @@ static void generate_support_areas(Print &print, TreeSupport* tree_support, cons
|
||||
if (layer) layer->polygons = intersection(layer->polygons, volumes.m_bed_area);
|
||||
});
|
||||
|
||||
// Belt floor: clip ALL organic support layers (including intermediate/base
|
||||
// fill) against the belt surface. The branch slices were already clipped
|
||||
// in organic_draw_branches(), but intermediate layers generated between
|
||||
// branches and the build plate need clipping too.
|
||||
// Compute the belt floor polygon directly from each layer's print_z
|
||||
// rather than mapping to a layer index (avoids index mismatch issues).
|
||||
{
|
||||
const auto &sp = print_object.slicing_parameters();
|
||||
const auto &pcfg = print_object.print()->config();
|
||||
BeltFloorContext ctx;
|
||||
ctx.init_local(sp, pcfg, print_object.belt_global_z_offset());
|
||||
if (ctx.is_active()) {
|
||||
tbb::parallel_for_each(layers_sorted.begin(), layers_sorted.end(), [&](SupportGeneratorLayer *layer) {
|
||||
if (!layer || layer->polygons.empty())
|
||||
return;
|
||||
layer->polygons = diff(layer->polygons, ctx.surface_polygon(layer->print_z));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
print.set_status(69, _L("Generating support"));
|
||||
generate_support_toolpaths(print_object.support_layers(), print_object.config(), support_params, print_object.slicing_parameters(),
|
||||
raft_layers, bottom_contacts, top_contacts, intermediate_layers, interface_layers, base_interface_layers);
|
||||
@@ -3923,6 +3955,10 @@ void organic_draw_branches(
|
||||
// ORCA: safety offset when trimming collision/bed to improve robustness.
|
||||
slices[i] = diff_clipped(slices[i], volumes.getCollision(0, layer_begin + i, true), ApplySafetyOffset::Yes); // FIXME parent_uses_min || draw_area.element->state.use_min_xy_dist);
|
||||
slices[i] = intersection(slices[i], volumes.m_bed_area, ApplySafetyOffset::Yes);
|
||||
// Belt floor: clip branch slices against the belt surface plane.
|
||||
LayerIndex belt_idx = layer_begin + i;
|
||||
if (belt_idx < LayerIndex(volumes.m_belt_floor.size()) && !volumes.m_belt_floor[belt_idx].empty())
|
||||
slices[i] = diff(slices[i], volumes.m_belt_floor[belt_idx]);
|
||||
remove_small(slices[i], tiny_area);
|
||||
}
|
||||
|
||||
@@ -3967,7 +4003,10 @@ void organic_draw_branches(
|
||||
if (!contacts.empty())
|
||||
bottom_contacts.emplace_back(std::move(contacts));
|
||||
}
|
||||
} else if (layer_begin > 0) {
|
||||
} else if (layer_begin > 0 && (volumes.m_belt_floor.empty() || num_empty == 0)) {
|
||||
// Belt-floor clipping makes initial slices empty often; without this
|
||||
// gate, "verylost" branches propagate rest_support down to layer 0 and
|
||||
// OOM on tall belt prints.
|
||||
// Drop down areas that do rest non - gracefully on the model to ensure the branch actually rests on something.
|
||||
struct BottomExtraSlice {
|
||||
Polygons polygons;
|
||||
@@ -3976,12 +4015,20 @@ void organic_draw_branches(
|
||||
std::vector<BottomExtraSlice> bottom_extra_slices;
|
||||
Polygons rest_support;
|
||||
coord_t bottom_radius = support_element_radius(config, *branch.path.front());
|
||||
// Belt printer (GeneratorOnly belt floor): the tilted belt surface is the
|
||||
// build surface, so a branch should terminate ON the belt with a thin tip,
|
||||
// not stamp its full footprint straight down to Z=0 and weld neighbouring
|
||||
// branches into a solid floor slab. m_belt_floor is only populated in that
|
||||
// mode, so it doubles as the gate (no effect on other printer types).
|
||||
const bool belt_mode = !volumes.m_belt_floor.empty();
|
||||
// Don't propagate further than 1.5 * bottom radius.
|
||||
//LayerIndex layers_propagate_max = 2 * bottom_radius / config.layer_height;
|
||||
LayerIndex layers_propagate_max = 5 * bottom_radius / config.layer_height;
|
||||
LayerIndex layer_bottommost = branch.path.front()->state.verylost ?
|
||||
LayerIndex layer_bottommost = (branch.path.front()->state.verylost && !belt_mode) ?
|
||||
// If the tree bottom is hanging in the air, bring it down to some surface.
|
||||
0 :
|
||||
// In belt mode never force-drop to Z=0 (the belt clip below handles
|
||||
// termination); otherwise the "verylost" branch welds into the slab.
|
||||
//FIXME the "verylost" branches should stop when crossing another support.
|
||||
std::max(0, layer_begin - layers_propagate_max);
|
||||
double support_area_min_radius = M_PI * sqr(double(config.branch_radius));
|
||||
@@ -3992,12 +4039,29 @@ void organic_draw_branches(
|
||||
LayerIndex collision_layer = (layer_idx == layer_begin - 1) ? layer_begin : layer_idx;
|
||||
Polygons collision = volumes.getCollision(0, collision_layer, false);
|
||||
rest_support = diff_clipped(rest_support.empty() ? slice_front_contact : rest_support, collision, ApplySafetyOffset::Yes);
|
||||
// Belt floor: clip propagated support at belt surface.
|
||||
bool belt_cut = false;
|
||||
if (layer_idx < LayerIndex(volumes.m_belt_floor.size()) && !volumes.m_belt_floor[layer_idx].empty()) {
|
||||
double area_before = area(rest_support);
|
||||
rest_support = diff(rest_support, volumes.m_belt_floor[layer_idx]);
|
||||
// The belt counts as "reached" only when it actually removes part
|
||||
// of this branch's footprint. The belt half-plane is non-empty at
|
||||
// every near-belt layer, so testing non-emptiness alone would
|
||||
// terminate a laterally-distant branch ~1 layer above true contact,
|
||||
// leaving a gap. Require a real area reduction instead.
|
||||
belt_cut = belt_mode && area(rest_support) < area_before - tiny_area;
|
||||
}
|
||||
remove_small(rest_support, tiny_area);
|
||||
double rest_support_area = area(rest_support);
|
||||
if (rest_support_area < support_area_stop)
|
||||
// Don't propagate a fraction of the tree contact surface.
|
||||
break;
|
||||
bottom_extra_slices.push_back({ rest_support, rest_support_area });
|
||||
// Belt mode: once the belt surface actually starts cutting this branch
|
||||
// it has reached the belt — keep this last (belt-clipped) slice as the
|
||||
// contact and stop, rather than stamping the footprint further down.
|
||||
if (belt_cut)
|
||||
break;
|
||||
}
|
||||
// Now remove those bottom slices that are not supported at all.
|
||||
#if 0
|
||||
@@ -4015,7 +4079,10 @@ void organic_draw_branches(
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (config.settings.support_floor_layers > 0) {
|
||||
// Belt mode: no solid support-floor pad under these branches — it is what
|
||||
// welds neighbouring belt-terminating branches into the dense Z=0 slab.
|
||||
// They simply taper out onto the tilted belt as distributed thin contacts.
|
||||
if (!belt_mode && config.settings.support_floor_layers > 0) {
|
||||
Polygons contacts;
|
||||
if (!bottom_extra_slices.empty()) {
|
||||
const int contact_idx = int(bottom_extra_slices.size()) - 1; // Use the lowest contact slice as the footprint.
|
||||
@@ -4054,7 +4121,10 @@ void organic_draw_branches(
|
||||
}
|
||||
|
||||
// ORCA: retain bottom contacts even when no placeable areas intersect.
|
||||
if (branch.has_root && config.support_rests_on_model && branch.path.front()->state.layer_idx > 0 &&
|
||||
// Skipped in belt mode (m_belt_floor populated) so we don't re-introduce a
|
||||
// solid floor pad for branches that terminate on the tilted belt surface.
|
||||
if (volumes.m_belt_floor.empty() &&
|
||||
branch.has_root && config.support_rests_on_model && branch.path.front()->state.layer_idx > 0 &&
|
||||
config.settings.support_floor_layers > 0 && config.z_distance_bottom_layers > 0 &&
|
||||
bottom_contacts.empty() && !slice_front_contact.empty())
|
||||
bottom_contacts.emplace_back(slice_front_contact);
|
||||
|
||||
@@ -667,7 +667,22 @@ inline SupportGeneratorLayer& layer_initialize(
|
||||
const size_t layer_idx)
|
||||
{
|
||||
layer_new.print_z = layer_z(slicing_params, config, layer_idx);
|
||||
layer_new.bottom_z = layer_idx > 0 ? layer_z(slicing_params, config, layer_idx - 1) : 0;
|
||||
// Layer 0 has no layer below it, so its bottom is the build plate at z = 0 --
|
||||
// true for a flat bed, false for a belt, whose virtual support layers extend
|
||||
// below zero. Taking 0 there made the bottom-most belt layer's height come out
|
||||
// as its own (negative) print_z, which reached Flow::with_height() and threw
|
||||
// FlowErrorNegativeFlow, so tree support could not slice any belt model whose
|
||||
// branches reached down that far.
|
||||
//
|
||||
// Only the negative case is corrected. An earlier version used
|
||||
// min(0, print_z - layer_height), which also fires whenever the initial layer
|
||||
// is THINNER than the regular layer height -- e.g. 0.2 over 0.3, both
|
||||
// independently configurable -- and silently changed flat-bed support layer
|
||||
// heights. Keying on the sign leaves every non-negative print_z on exactly
|
||||
// the previous value of 0.
|
||||
layer_new.bottom_z = layer_idx > 0 ? layer_z(slicing_params, config, layer_idx - 1) : 0.;
|
||||
if (layer_idx == 0 && layer_new.print_z < 0.)
|
||||
layer_new.bottom_z = layer_new.print_z - slicing_params.layer_height;
|
||||
layer_new.height = layer_new.print_z - layer_new.bottom_z;
|
||||
return layer_new;
|
||||
}
|
||||
|
||||
@@ -551,9 +551,13 @@ DecimateResult decimate(const TriSoup &geometry, size_t target_triangles, bool h
|
||||
|
||||
// Rebuild from the surviving faces, with per-face normals.
|
||||
TriSoup &out = result.geometry;
|
||||
if (!face_color.empty())
|
||||
result.face_color.reserve(active_faces);
|
||||
for (size_t f = 0; f < face_count; ++f) {
|
||||
if (faces[f * 3] < 0)
|
||||
continue;
|
||||
if (!face_color.empty())
|
||||
result.face_color.push_back(f < face_color.size() ? face_color[f] : -1);
|
||||
const Vec3f a = pos[size_t(faces[f * 3])].cast<float>();
|
||||
const Vec3f b = pos[size_t(faces[f * 3 + 1])].cast<float>();
|
||||
const Vec3f c = pos[size_t(faces[f * 3 + 2])].cast<float>();
|
||||
|
||||
@@ -46,6 +46,13 @@ using DecimateProgressFn = std::function<bool(double fraction)>;
|
||||
struct DecimateResult
|
||||
{
|
||||
TriSoup geometry;
|
||||
// One entry per output face, carried from the `face_color` handed in: a colour difference is a
|
||||
// crease, so no collapse ever merges two faces of different colour and every survivor keeps exactly
|
||||
// the colour it came with. Empty when no `face_color` was given.
|
||||
//
|
||||
// This is what lets the caller colour the simplified mesh by *provenance* rather than by sampling it
|
||||
// again: the input colours were masked by the paint on the fine mesh, where that mask is exact.
|
||||
std::vector<int> face_color;
|
||||
// The locked faces alone met the target, so it was unreachable without touching preserved
|
||||
// geometry.
|
||||
bool locked_over_budget = false;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "libslic3r/Point.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
@@ -42,7 +43,7 @@ TriSoup to_soup(const indexed_triangle_set &its, const std::vector<uint8_t> &fac
|
||||
return out;
|
||||
}
|
||||
|
||||
indexed_triangle_set to_indexed_triangle_set(const TriSoup &soup)
|
||||
indexed_triangle_set to_indexed_triangle_set(const TriSoup &soup, std::vector<int> *face_color)
|
||||
{
|
||||
indexed_triangle_set out;
|
||||
const size_t n = soup.pos.size();
|
||||
@@ -54,12 +55,22 @@ indexed_triangle_set to_indexed_triangle_set(const TriSoup &soup)
|
||||
if (map.inserted())
|
||||
out.vertices.push_back(soup.pos[i]);
|
||||
}
|
||||
const bool track_color = face_color != nullptr && !face_color->empty();
|
||||
std::vector<int> kept_color;
|
||||
if (track_color)
|
||||
kept_color.reserve(face_color->size());
|
||||
for (size_t t = 0; t + 2 < n; t += 3) {
|
||||
// Welded-together corners carry no area.
|
||||
if (id[t] == id[t + 1] || id[t + 1] == id[t + 2] || id[t] == id[t + 2])
|
||||
continue;
|
||||
out.indices.emplace_back(id[t], id[t + 1], id[t + 2]);
|
||||
if (track_color) {
|
||||
const size_t src = t / 3;
|
||||
kept_color.push_back(src < face_color->size() ? (*face_color)[src] : -1);
|
||||
}
|
||||
}
|
||||
if (track_color)
|
||||
*face_color = std::move(kept_color);
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,10 @@ namespace TextureBake {
|
||||
TriSoup to_soup(const indexed_triangle_set &its, const std::vector<uint8_t> &face_excluded = {});
|
||||
|
||||
// Welds at the geometry grid.
|
||||
indexed_triangle_set to_indexed_triangle_set(const TriSoup &soup);
|
||||
// `face_color`, when given, is read as one entry per soup triangle and rewritten to match the output.
|
||||
// Welding can leave a triangle with no area, and those are dropped here, so the two would otherwise
|
||||
// fall out of step.
|
||||
indexed_triangle_set to_indexed_triangle_set(const TriSoup &soup, std::vector<int> *face_color = nullptr);
|
||||
|
||||
} // namespace TextureBake
|
||||
} // namespace Slic3r
|
||||
|
||||
@@ -290,6 +290,34 @@ PipelineResult run_pipeline(const TriSoup &input, const HeightSampleFn &sample,
|
||||
return result;
|
||||
}
|
||||
|
||||
// Colour per face, taken here and carried from here on. This is the only point where the paint mask
|
||||
// is exact: `exclude_weight` says which faces the paint left out, and the mesh is still the refined
|
||||
// one the displacement produced. Everything downstream (the collapse, the T-junction repair) carries
|
||||
// these along rather than sampling again, and the caller uses them as they are.
|
||||
//
|
||||
// It also gives the collapse its crease criterion: an edge between two colours is never collapsed
|
||||
// across, which is what keeps a survivor's colour well defined.
|
||||
if (color_sample) {
|
||||
const size_t nf = displaced.triangle_count();
|
||||
result.face_color.assign(nf, -1);
|
||||
const bool have_w = !displaced.exclude_weight.empty();
|
||||
tbb::parallel_for(tbb::blocked_range<size_t>(0, nf), [&](const tbb::blocked_range<size_t> &r) {
|
||||
for (size_t t = r.begin(); t < r.end(); ++t) {
|
||||
// Unpainted faces take no colour at all, which is what stops the texture appearing on
|
||||
// surfaces the paint never covered.
|
||||
if (have_w && (displaced.exclude_weight[t * 3] + displaced.exclude_weight[t * 3 + 1] +
|
||||
displaced.exclude_weight[t * 3 + 2]) / 3.f > 0.99f)
|
||||
continue; // stays FACE_UNPAINTED
|
||||
const Vec3f &a = displaced.pos[t * 3], &b = displaced.pos[t * 3 + 1], &c = displaced.pos[t * 3 + 2];
|
||||
const int sampled = color_sample((a + b + c) / 3.f, displaced.nrm[t * 3]);
|
||||
// Painted either way. The sampler expects a point on the base surface and these are on
|
||||
// the displaced one, so off the patch by more than its tolerance it simply says "no
|
||||
// colour" - which must not be confused with "not painted".
|
||||
result.face_color[t] = (sampled >= 0) ? sampled : FACE_NO_COLOUR;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 4. Decimate - export only. A bake needs the face-parent map, which a collapse destroys.
|
||||
std::vector<int> parent = std::move(sub.face_parent_id);
|
||||
const size_t displaced_before_decimate = displaced.triangle_count();
|
||||
@@ -323,24 +351,8 @@ PipelineResult run_pipeline(const TriSoup &input, const HeightSampleFn &sample,
|
||||
// unless the budget was lowered until decimation had to run. Only collapses costing less than
|
||||
// harvest_tol are taken, so this does not reach the relief.
|
||||
const bool harvest_only = !over_budget && settings.harvest_flat && displaced.triangle_count() > 0;
|
||||
std::vector<int> &face_color = result.face_color;
|
||||
if (over_budget || harvest_only) {
|
||||
// Colour per face on the fine mesh, so colour boundaries become creases the collapse
|
||||
// respects. Excluded (unpainted) faces take no colour.
|
||||
std::vector<int> face_color;
|
||||
if (color_sample) {
|
||||
const size_t nf = displaced.triangle_count();
|
||||
face_color.assign(nf, -1);
|
||||
const bool have_w = !displaced.exclude_weight.empty();
|
||||
tbb::parallel_for(tbb::blocked_range<size_t>(0, nf), [&](const tbb::blocked_range<size_t> &r) {
|
||||
for (size_t t = r.begin(); t < r.end(); ++t) {
|
||||
if (have_w && (displaced.exclude_weight[t * 3] + displaced.exclude_weight[t * 3 + 1] +
|
||||
displaced.exclude_weight[t * 3 + 2]) / 3.f > 0.99f)
|
||||
continue;
|
||||
const Vec3f &a = displaced.pos[t * 3], &b = displaced.pos[t * 3 + 1], &c = displaced.pos[t * 3 + 2];
|
||||
face_color[t] = color_sample((a + b + c) / 3.f, displaced.nrm[t * 3]);
|
||||
}
|
||||
});
|
||||
}
|
||||
// Harvesting alone is asked for by handing it the count it already has: nothing is then
|
||||
// over the target, so the loop only ever pops collapses under the tolerance.
|
||||
const size_t before = displaced.triangle_count();
|
||||
@@ -350,6 +362,7 @@ PipelineResult run_pipeline(const TriSoup &input, const HeightSampleFn &sample,
|
||||
result.locked_over_budget = dec.locked_over_budget;
|
||||
result.budget_limited = result.simplified = dec.target_cost_detail;
|
||||
displaced = std::move(dec.geometry);
|
||||
face_color = std::move(dec.face_color);
|
||||
lap("decimate", displaced, over_budget ? "over budget, simplified" : "flat faces harvested");
|
||||
BOOST_LOG_TRIVIAL(info) << "TextureBake decimate: " << before << " -> " << displaced.triangle_count()
|
||||
<< (over_budget ? " (budget " : " (flat harvest, budget ") << target << ")";
|
||||
@@ -377,7 +390,7 @@ PipelineResult run_pipeline(const TriSoup &input, const HeightSampleFn &sample,
|
||||
|
||||
// 6. Close the T-junctions decimation left behind. Only meaningful when it ran.
|
||||
if (mode == PipelineMode::Export && parent.empty()) {
|
||||
displaced = resolve_t_junctions(displaced);
|
||||
displaced = resolve_t_junctions(displaced, {}, &result.face_color);
|
||||
lap("repair", displaced);
|
||||
}
|
||||
|
||||
|
||||
@@ -108,9 +108,27 @@ using PipelineProgressFn = std::function<bool(const char *stage, double fraction
|
||||
// colour-boundary creases. Only consulted when the mesh is over budget.
|
||||
using ColorSampleFn = std::function<int(const Vec3f ¢roid, const Vec3f &normal)>;
|
||||
|
||||
// Sentinels for PipelineResult::face_color.
|
||||
static constexpr int FACE_UNPAINTED = -1; // the paint did not cover this face's origin
|
||||
static constexpr int FACE_NO_COLOUR = -2; // painted, but the sampler returned nothing at this point
|
||||
|
||||
struct PipelineResult
|
||||
{
|
||||
TriSoup geometry;
|
||||
// One entry per output face, carried through decimation, the T-junction repair and the weld.
|
||||
// FACE_UNPAINTED means the paint never covered the geometry this face came from; anything else means
|
||||
// it did, and is the palette index `color_sample` returned there (FACE_NO_COLOUR when it returned
|
||||
// none). The distinction matters: the sampler answers for points on the *base* surface, and these
|
||||
// are sampled on the displaced one, so a painted face can easily come back without a colour. Only
|
||||
// the painted/unpainted split is reliable here, and that is what a caller should use it for.
|
||||
//
|
||||
// Empty unless the caller gave a `color_sample`.
|
||||
//
|
||||
// A caller that needs per-face colour must use this rather than sampling the result again. The
|
||||
// result is displaced geometry: a point on it is no longer where its base surface was, so matching
|
||||
// it back by proximity colours whatever base surface happens to be nearest - which on a part thinner
|
||||
// than the relief depth is the *opposite* face, picking up the texture meant for the painted one.
|
||||
std::vector<int> face_color;
|
||||
// Output face -> input face. Empty in Export mode, where decimation invalidates it.
|
||||
std::vector<int> face_parent_id;
|
||||
bool safety_cap_hit = false;
|
||||
|
||||
@@ -72,7 +72,8 @@ size_t count_area_slivers(const TriSoup &geometry)
|
||||
return n;
|
||||
}
|
||||
|
||||
TriSoup resolve_t_junctions(const TriSoup &geometry, const RepairOptions &opts)
|
||||
TriSoup resolve_t_junctions(const TriSoup &geometry, const RepairOptions &opts,
|
||||
std::vector<int> *face_color)
|
||||
{
|
||||
const size_t n_tri = geometry.triangle_count();
|
||||
const double on_tol2 = opts.on_seg_tol * opts.on_seg_tol;
|
||||
@@ -98,7 +99,12 @@ TriSoup resolve_t_junctions(const TriSoup &geometry, const RepairOptions &opts)
|
||||
// grid. A needle reads as watertight yet is deleted downstream, and dropping it leaves exactly
|
||||
// the on-edge-vertex topology the pass below closes.
|
||||
std::vector<std::array<int, 3>> faces;
|
||||
// Parallel to `faces` throughout, so a split or a dropped degenerate keeps the two in step.
|
||||
const bool track_color = face_color != nullptr && !face_color->empty();
|
||||
std::vector<int> colors;
|
||||
faces.reserve(n_tri);
|
||||
if (track_color)
|
||||
colors.reserve(n_tri);
|
||||
for (size_t t = 0; t < n_tri; ++t) {
|
||||
const int a = vid[t * 3], b = vid[t * 3 + 1], c = vid[t * 3 + 2];
|
||||
if (a == b || b == c || a == c)
|
||||
@@ -108,6 +114,8 @@ TriSoup resolve_t_junctions(const TriSoup &geometry, const RepairOptions &opts)
|
||||
if (u.cross(w).squaredNorm() < DEGENERATE_AREA_SQ)
|
||||
continue;
|
||||
faces.push_back({ a, b, c });
|
||||
if (track_color)
|
||||
colors.push_back(t < face_color->size() ? (*face_color)[t] : -1);
|
||||
}
|
||||
|
||||
for (int iter = 0; iter < opts.max_iters; ++iter) {
|
||||
@@ -166,11 +174,16 @@ TriSoup resolve_t_junctions(const TriSoup &geometry, const RepairOptions &opts)
|
||||
break;
|
||||
|
||||
std::vector<std::array<int, 3>> next;
|
||||
std::vector<int> next_colors;
|
||||
next.reserve(faces.size() + splits.size() * 2);
|
||||
if (track_color)
|
||||
next_colors.reserve(next.capacity());
|
||||
for (size_t fi = 0; fi < faces.size(); ++fi) {
|
||||
const auto it = splits.find(fi);
|
||||
if (it == splits.end()) {
|
||||
next.push_back(faces[fi]);
|
||||
if (track_color)
|
||||
next_colors.push_back(colors[fi]);
|
||||
continue;
|
||||
}
|
||||
const auto &f = faces[fi];
|
||||
@@ -195,11 +208,18 @@ TriSoup resolve_t_junctions(const TriSoup &geometry, const RepairOptions &opts)
|
||||
seq.insert(seq.end(), sp.mids.rbegin(), sp.mids.rend());
|
||||
seq.push_back(sp.a);
|
||||
}
|
||||
for (size_t s = 0; s + 1 < seq.size(); ++s)
|
||||
for (size_t s = 0; s + 1 < seq.size(); ++s) {
|
||||
next.push_back({ seq[s], seq[s + 1], apex });
|
||||
if (track_color)
|
||||
next_colors.push_back(colors[fi]); // every piece of a split face keeps its colour
|
||||
}
|
||||
}
|
||||
faces.swap(next);
|
||||
if (track_color)
|
||||
colors.swap(next_colors);
|
||||
}
|
||||
if (track_color)
|
||||
*face_color = std::move(colors);
|
||||
|
||||
TriSoup out;
|
||||
out.pos.reserve(faces.size() * 3);
|
||||
|
||||
@@ -43,7 +43,12 @@ struct RepairOptions
|
||||
int max_iters = 16;
|
||||
};
|
||||
|
||||
TriSoup resolve_t_junctions(const TriSoup &geometry, const RepairOptions &opts = {});
|
||||
// `face_color`, when given, is read as one entry per input face and rewritten to match the output: a
|
||||
// face split to close a T-junction hands its colour to every piece, and a degenerate face dropped on
|
||||
// the way takes its entry with it. Without this the caller would have no way to keep a per-face colour
|
||||
// across this pass, which changes the triangle count.
|
||||
TriSoup resolve_t_junctions(const TriSoup &geometry, const RepairOptions &opts = {},
|
||||
std::vector<int> *face_color = nullptr);
|
||||
|
||||
} // namespace TextureBake
|
||||
} // namespace Slic3r
|
||||
|
||||
@@ -1248,52 +1248,81 @@ bool triangles_overlap(const Tri2 &a, const Tri2 &b, float eps)
|
||||
struct NetGrid
|
||||
{
|
||||
static constexpr int BIG_SPAN = 16;
|
||||
float cell;
|
||||
float eps;
|
||||
std::unordered_map<uint64_t, std::vector<Tri2>> cells;
|
||||
std::vector<Tri2> big;
|
||||
// Each stored triangle keeps its own bounding box. Overlap testing is dominated by rejects - a cell
|
||||
// holds every triangle whose box touches it, and a candidate meets only a couple of them for real -
|
||||
// so paying six floats per entry to answer most of those rejects with four comparisons, instead of a
|
||||
// full triangle intersection, is what makes the net affordable. Measured on a 42k-triangle patch the
|
||||
// grid ran ~19 million candidate pairs per net, nearly all of them misses, and rejecting them this
|
||||
// way took the net from ~175 ms to ~53 ms.
|
||||
//
|
||||
// The box rides inside the entry rather than in a parallel array: splitting them to scan boxes back
|
||||
// to back was tried and came out slower, because each bucket then grows two vectors instead of one.
|
||||
struct Entry
|
||||
{
|
||||
Tri2 tri;
|
||||
Vec2f lo, hi;
|
||||
};
|
||||
float cell;
|
||||
float eps;
|
||||
std::unordered_map<uint64_t, std::vector<Entry>> cells;
|
||||
std::vector<Entry> big;
|
||||
|
||||
static uint64_t key(int x, int y) { return (uint64_t(uint32_t(x)) << 32) | uint32_t(y); }
|
||||
bool range(const Tri2 &t, int &x0, int &y0, int &x1, int &y1) const
|
||||
static Entry entry(const Tri2 &t)
|
||||
{
|
||||
return Entry{ t, t[0].cwiseMin(t[1]).cwiseMin(t[2]), t[0].cwiseMax(t[1]).cwiseMax(t[2]) };
|
||||
}
|
||||
bool range(const Vec2f &lo, const Vec2f &hi, int &x0, int &y0, int &x1, int &y1) const
|
||||
{
|
||||
const Vec2f lo = t[0].cwiseMin(t[1]).cwiseMin(t[2]), hi = t[0].cwiseMax(t[1]).cwiseMax(t[2]);
|
||||
x0 = int(std::floor(lo.x() / cell));
|
||||
y0 = int(std::floor(lo.y() / cell));
|
||||
x1 = int(std::floor(hi.x() / cell));
|
||||
y1 = int(std::floor(hi.y() / cell));
|
||||
return x1 - x0 <= BIG_SPAN && y1 - y0 <= BIG_SPAN;
|
||||
}
|
||||
// Boxes grown by eps on both sides, to match the tolerance triangles_overlap() itself works to: a
|
||||
// reject here must never discard a pair that test would have called touching.
|
||||
bool boxes_apart(const Entry &a, const Entry &b) const
|
||||
{
|
||||
return a.hi.x() + eps < b.lo.x() || b.hi.x() + eps < a.lo.x() || a.hi.y() + eps < b.lo.y() ||
|
||||
b.hi.y() + eps < a.lo.y();
|
||||
}
|
||||
bool hits(const Entry &q, const std::vector<Entry> &bucket) const
|
||||
{
|
||||
for (const Entry &b : bucket)
|
||||
if (!boxes_apart(q, b) && triangles_overlap(q.tri, b.tri, eps))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
bool overlaps(const Tri2 &t) const
|
||||
{
|
||||
for (const Tri2 &b : big)
|
||||
if (triangles_overlap(t, b, eps))
|
||||
return true;
|
||||
const Entry q = entry(t);
|
||||
if (hits(q, big))
|
||||
return true;
|
||||
int x0, y0, x1, y1;
|
||||
if (!range(t, x0, y0, x1, y1)) {
|
||||
for (const auto &[k, tris] : cells)
|
||||
for (const Tri2 &b : tris)
|
||||
if (triangles_overlap(t, b, eps))
|
||||
return true;
|
||||
if (!range(q.lo, q.hi, x0, y0, x1, y1)) {
|
||||
for (const auto &[k, bucket] : cells)
|
||||
if (hits(q, bucket))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
for (int x = x0; x <= x1; ++x)
|
||||
for (int y = y0; y <= y1; ++y)
|
||||
if (const auto it = cells.find(key(x, y)); it != cells.end())
|
||||
for (const Tri2 &b : it->second)
|
||||
if (triangles_overlap(t, b, eps))
|
||||
return true;
|
||||
if (const auto it = cells.find(key(x, y)); it != cells.end() && hits(q, it->second))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
void insert(const Tri2 &t)
|
||||
{
|
||||
int x0, y0, x1, y1;
|
||||
if (!range(t, x0, y0, x1, y1)) {
|
||||
big.push_back(t);
|
||||
const Entry e = entry(t);
|
||||
int x0, y0, x1, y1;
|
||||
if (!range(e.lo, e.hi, x0, y0, x1, y1)) {
|
||||
big.push_back(e);
|
||||
return;
|
||||
}
|
||||
for (int x = x0; x <= x1; ++x)
|
||||
for (int y = y0; y <= y1; ++y)
|
||||
cells[key(x, y)].push_back(t);
|
||||
cells[key(x, y)].push_back(e);
|
||||
}
|
||||
};
|
||||
} // namespace
|
||||
@@ -1305,11 +1334,20 @@ std::vector<TextureIsland> compute_connected_net(const PatchUnwrap &unwrap)
|
||||
if (n <= 1)
|
||||
return islands;
|
||||
|
||||
// Chart adjacency, with one representative shared edge per adjacent pair.
|
||||
// Chart adjacency, with one representative shared edge per adjacent pair: the fold line the pair is
|
||||
// unfolded about.
|
||||
//
|
||||
// Which edge that is matters, because two charts can touch along more than one run. A chart cut open
|
||||
// to flatten it - a ring opened by segment_into_charts(), say - touches its other half along *both*
|
||||
// sides of the cut. Folding is rigid, so only the run the fold line belongs to comes out matching;
|
||||
// every other run is left mismatched, and a mismatched run is exactly where the texture visibly
|
||||
// jumps. Taking whichever edge the map happened to yield first therefore left the long side broken
|
||||
// about as often as the short one. The fold line is picked from the longest run instead, so what is
|
||||
// left discontinuous is the shortest boundary the pair has.
|
||||
const auto edges = build_shared_edges(unwrap);
|
||||
struct PairEdge { ChartEdge a, b; };
|
||||
std::map<std::pair<int, int>, PairEdge> pair_edge;
|
||||
std::vector<std::vector<int>> adj(static_cast<size_t>(n));
|
||||
struct SharedEdge { PairEdge fold; int base_lo = -1, base_hi = -1; float length = 0.f; };
|
||||
std::map<std::pair<int, int>, std::vector<SharedEdge>> pair_shared;
|
||||
for (const auto &[base_edge, list] : edges) {
|
||||
for (size_t i = 0; i < list.size(); ++i)
|
||||
for (size_t j = i + 1; j < list.size(); ++j) {
|
||||
@@ -1317,14 +1355,50 @@ std::vector<TextureIsland> compute_connected_net(const PatchUnwrap &unwrap)
|
||||
if (c1 == c2 || c1 < 0 || c2 < 0 || c1 >= n || c2 >= n)
|
||||
continue;
|
||||
const std::pair<int, int> pk{ std::min(c1, c2), std::max(c1, c2) };
|
||||
if (pair_edge.count(pk))
|
||||
continue; // keep the first shared edge as the fold line for this pair
|
||||
pair_edge[pk] = (c1 < c2) ? PairEdge{ list[i], list[j] } : PairEdge{ list[j], list[i] };
|
||||
adj[size_t(pk.first)].push_back(pk.second);
|
||||
adj[size_t(pk.second)].push_back(pk.first);
|
||||
SharedEdge se;
|
||||
se.fold = (c1 < c2) ? PairEdge{ list[i], list[j] } : PairEdge{ list[j], list[i] };
|
||||
se.base_lo = base_edge.first;
|
||||
se.base_hi = base_edge.second;
|
||||
// The unwrap is scaled to true surface area, so a uv distance is a length in mm.
|
||||
se.length = (unwrap.uvs[size_t(se.fold.a.uv_lo)] - unwrap.uvs[size_t(se.fold.a.uv_hi)]).norm();
|
||||
pair_shared[pk].push_back(se);
|
||||
}
|
||||
}
|
||||
|
||||
std::map<std::pair<int, int>, PairEdge> pair_edge;
|
||||
std::map<std::pair<int, int>, float> pair_weight; // length of the run each pair folds across
|
||||
std::vector<std::vector<int>> adj(static_cast<size_t>(n));
|
||||
for (const auto &[pk, shared] : pair_shared) {
|
||||
// Group the pair's shared edges into runs - edges joined end to end through a base vertex - and
|
||||
// total each run's length.
|
||||
std::unordered_map<int, int> local;
|
||||
for (const SharedEdge &se : shared)
|
||||
for (const int v : { se.base_lo, se.base_hi })
|
||||
local.emplace(v, int(local.size()));
|
||||
UnionFind runs(local.size());
|
||||
for (const SharedEdge &se : shared)
|
||||
runs.unite(local[se.base_lo], local[se.base_hi]);
|
||||
|
||||
std::unordered_map<int, float> run_length;
|
||||
std::unordered_map<int, size_t> run_first;
|
||||
for (size_t i = 0; i < shared.size(); ++i) {
|
||||
const int root = runs.find(local[shared[i].base_lo]);
|
||||
run_length[root] += shared[i].length;
|
||||
run_first.emplace(root, i);
|
||||
}
|
||||
int best_root = -1;
|
||||
float best_len = -1.f;
|
||||
for (const auto &[root, len] : run_length)
|
||||
if (len > best_len) { best_len = len; best_root = root; }
|
||||
if (best_root < 0)
|
||||
continue;
|
||||
|
||||
pair_edge[pk] = shared[run_first[best_root]].fold;
|
||||
pair_weight[pk] = best_len;
|
||||
adj[size_t(pk.first)].push_back(pk.second);
|
||||
adj[size_t(pk.second)].push_back(pk.first);
|
||||
}
|
||||
|
||||
// Per chart: its vertices, its triangles and its flattened area.
|
||||
std::vector<std::vector<int>> chart_verts(static_cast<size_t>(n)), chart_tris(static_cast<size_t>(n));
|
||||
std::vector<float> chart_area(static_cast<size_t>(n), 0.f);
|
||||
@@ -1378,15 +1452,29 @@ std::vector<TextureIsland> compute_connected_net(const PatchUnwrap &unwrap)
|
||||
for (const int t : chart_tris[size_t(root)])
|
||||
grid.insert(placed(m, t));
|
||||
}
|
||||
std::queue<int> q;
|
||||
q.push(root);
|
||||
// Grown strongest-adjacency-first (Prim, not breadth-first): a chart is folded onto whichever
|
||||
// neighbour it shares the longest boundary with, among everything reachable so far. Order matters
|
||||
// because only the fold a chart is actually reached by comes out matching - every other boundary
|
||||
// it has is left to chance. Taking neighbours in breadth-first order, biggest-area first, let a
|
||||
// far-off branch claim a chart across a short boundary before its true neighbour was reached, and
|
||||
// the long boundary they shared then stayed broken. That is the visible seam next to a hole: a
|
||||
// ring is cut into two halves that share a long boundary, and whichever half was reached first
|
||||
// took the other one along some unrelated edge.
|
||||
using Candidate = std::pair<float, std::pair<int, int>>; // weight, (from, to)
|
||||
std::priority_queue<Candidate> q;
|
||||
const auto push_neighbours = [&](int p) {
|
||||
for (const int c : adj[size_t(p)])
|
||||
if (net_of[size_t(c)] < 0 && !chart_tris[size_t(c)].empty()) {
|
||||
const auto w = pair_weight.find({ std::min(p, c), std::max(p, c) });
|
||||
q.push({ w == pair_weight.end() ? 0.f : w->second, { p, c } });
|
||||
}
|
||||
};
|
||||
push_neighbours(root);
|
||||
while (!q.empty()) {
|
||||
const int p = q.front();
|
||||
const auto [weight, link] = q.top();
|
||||
q.pop();
|
||||
std::vector<int> neighbours = adj[size_t(p)];
|
||||
std::stable_sort(neighbours.begin(), neighbours.end(),
|
||||
[&chart_area](int a, int b) { return chart_area[size_t(a)] > chart_area[size_t(b)]; });
|
||||
for (const int c : neighbours) {
|
||||
const int p = link.first, c = link.second;
|
||||
{
|
||||
if (net_of[size_t(c)] >= 0 || chart_tris[size_t(c)].empty())
|
||||
continue;
|
||||
const auto it = pair_edge.find({ std::min(p, c), std::max(p, c) });
|
||||
@@ -1417,7 +1505,7 @@ std::vector<TextureIsland> compute_connected_net(const PatchUnwrap &unwrap)
|
||||
for (const Tri2 &t : tris)
|
||||
grid.insert(t);
|
||||
net_of[size_t(c)] = net;
|
||||
q.push(c);
|
||||
push_neighbours(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2284,12 +2372,11 @@ indexed_triangle_set build_texture_displacement_v2(const indexed_triangle_set
|
||||
//
|
||||
// The *palette* index, not the printed filament. The decimation treats any edge whose two faces
|
||||
// differ as a crease (TextureBakeDecimate.cpp), so it must only ever see where the **perceived**
|
||||
// colour changes - which is exactly what ColorResolveFn's own contract says the interleaving may
|
||||
// never be fed into. Handing it the resolved filament made every Z band boundary a crease: on an
|
||||
// upright wall that is one crease per band, so the collapse ran along those lines and left a stack
|
||||
// of horizontal slivers, each printing in a single filament. Those were the horizontal colour
|
||||
// lines in the baked result, and they also spent the triangle budget drawing a pattern the eye is
|
||||
// meant to blend away. Faces the paint excludes are skipped by the pipeline itself.
|
||||
// colour changes. A mix is one perceived colour however its components are laid down, which is why
|
||||
// it has to be the palette index here: back when this was handed a per-triangle interleave instead,
|
||||
// every band boundary read as a crease, the collapse ran along those lines and left a stack of
|
||||
// horizontal slivers, and the triangle budget went on drawing a pattern the eye is meant to blend
|
||||
// away. Faces the paint excludes are skipped by the pipeline itself.
|
||||
const TextureBake::ColorSampleFn color_sample =
|
||||
color_sampler ? TextureBake::ColorSampleFn([&color_sampler](const Vec3f &p, const Vec3f &n) {
|
||||
return color_sampler(p, n);
|
||||
@@ -2315,7 +2402,7 @@ indexed_triangle_set build_texture_displacement_v2(const indexed_triangle_set
|
||||
stats->triangles_budget = result.triangles_budget;
|
||||
stats->budget_limited = result.budget_limited;
|
||||
}
|
||||
indexed_triangle_set out = TextureBake::to_indexed_triangle_set(result.geometry);
|
||||
indexed_triangle_set out = TextureBake::to_indexed_triangle_set(result.geometry, &result.face_color);
|
||||
if (out.indices.empty())
|
||||
return mesh;
|
||||
if (flip_normals)
|
||||
@@ -2336,6 +2423,8 @@ indexed_triangle_set build_texture_displacement_v2(const indexed_triangle_set
|
||||
max_depth = std::max(max_depth, std::abs(layer.depth_mm));
|
||||
const float relief_tol = max_depth + paint_tol;
|
||||
std::vector<int> palette(out.indices.size(), -1);
|
||||
const std::vector<int> &face_mask = result.face_color;
|
||||
const bool have_face_mask = face_mask.size() == out.indices.size();
|
||||
tbb::parallel_for(tbb::blocked_range<size_t>(0, out.indices.size()), [&](const tbb::blocked_range<size_t> &r) {
|
||||
for (size_t i = r.begin(); i < r.end(); ++i) {
|
||||
const stl_triangle_vertex_indices &t = out.indices[i];
|
||||
@@ -2351,8 +2440,19 @@ indexed_triangle_set build_texture_displacement_v2(const indexed_triangle_set
|
||||
// reason; this path was the inconsistent one.
|
||||
Vec3f foot = centroid, base_n = Vec3f::UnitZ();
|
||||
const float d2 = painted_closest(centroid, &foot, &base_n);
|
||||
if (!all_painted && d2 >= relief_tol * relief_tol)
|
||||
// Which faces may be coloured comes from the pipeline, which recorded it on the
|
||||
// refined mesh where the paint mask is exact, and carried it through the collapse,
|
||||
// the T-junction repair and the weld. Proximity cannot answer this: a displaced face
|
||||
// is no longer where its base was, so on a part thinner than the relief depth the
|
||||
// nearest painted surface to the *opposite* face is the painted one, and the texture
|
||||
// appeared there too. Only the position to sample at still comes from the base
|
||||
// surface, for the projection reason above.
|
||||
if (have_face_mask) {
|
||||
if (face_mask[i] == TextureBake::FACE_UNPAINTED)
|
||||
continue;
|
||||
} else if (!all_painted && d2 >= relief_tol * relief_tol) {
|
||||
continue;
|
||||
}
|
||||
palette[i] = sampler(foot, base_n);
|
||||
}
|
||||
});
|
||||
@@ -2374,7 +2474,7 @@ indexed_triangle_set build_texture_displacement_v2(const indexed_triangle_set
|
||||
Vec3f normal = (b - a).cross(c - a);
|
||||
const float nl = normal.norm();
|
||||
normal = (nl > 0.f) ? Vec3f(normal / nl) : Vec3f::UnitZ();
|
||||
const int filament = color->resolve ? color->resolve(palette[i], centroid, normal) : palette[i];
|
||||
const int filament = palette[i];
|
||||
if (filament >= 0)
|
||||
out_color[i] = uint8_t(std::min(filament + 1, 255));
|
||||
}
|
||||
@@ -2831,8 +2931,7 @@ static indexed_triangle_set build_texture_displacement_in_place(
|
||||
Vec3f normal = (b - a).cross(c - a);
|
||||
const float nl = normal.norm();
|
||||
normal = (nl > 0.f) ? Vec3f(normal / nl) : Vec3f::UnitZ();
|
||||
const int filament = color->resolve ? color->resolve(triangle_palette[i], centroid, normal)
|
||||
: triangle_palette[i];
|
||||
const int filament = triangle_palette[i];
|
||||
if (filament >= 0)
|
||||
out_color[i] = uint8_t(std::min(filament + 1, 255));
|
||||
}
|
||||
@@ -5053,4 +5152,5 @@ indexed_triangle_set cut_mesh_at_steps(const indexed_triangle_set &mesh, const s
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
@@ -321,25 +321,6 @@ struct TextureDisplacementLayer
|
||||
}
|
||||
};
|
||||
|
||||
// How a *mixed* palette entry - one that names two filaments rather than one - is turned into real
|
||||
// per-facet paint. An MMU extrudes one filament at a time, so an intermediate colour exists only by
|
||||
// interleaving two of them finely enough that the eye does the blending.
|
||||
enum class ColorMixMode : int
|
||||
{
|
||||
// Horizontal bands: which of the two filaments a point takes depends on its height, so
|
||||
// consecutive print layers alternate. This is how filament-blend prints actually work, and on a
|
||||
// vertical-ish surface it reads as a genuinely smooth colour. On a near-horizontal surface a whole
|
||||
// layer is one band, so the blend disappears - that is what XYDither is for.
|
||||
ZBands = 0,
|
||||
// An ordered (Bayer) checkerboard across the surface, at any orientation. Independent of layer
|
||||
// height, but its cell is around the size of one facet, so a fine mix can read as texture rather
|
||||
// than as a clean blend.
|
||||
XYDither = 1,
|
||||
// Per triangle, by its orientation: bands where the surface is upright enough for consecutive
|
||||
// layers to alternate, the checkerboard where it faces up or down and a layer would be one band.
|
||||
// The default - a flat-topped part with a mix on top gets no blend at all from bands alone.
|
||||
Auto = 2,
|
||||
};
|
||||
|
||||
// Settings that apply to the whole layer stack rather than to one layer, held per ModelVolume next
|
||||
// to texture_displacement_layers and consumed by build_texture_displacement().
|
||||
@@ -406,7 +387,6 @@ struct TextureDisplacementOptions
|
||||
// image (TextureDetail::flat_colors): a texture of flat colours prints in single filaments, a
|
||||
// photograph or gradient in mixes. Off forces single filaments everywhere.
|
||||
bool color_mix_enabled = true;
|
||||
ColorMixMode color_mix_mode = ColorMixMode::Auto;
|
||||
// Majority-filter passes over the assigned colours. See TextureColorRequest::despeckle_passes -
|
||||
// this is the control for it, and 2 is enough to clear the salt-and-pepper an image with detail
|
||||
// finer than the mesh leaves behind, without eating features that are genuinely a facet wide.
|
||||
@@ -414,11 +394,9 @@ struct TextureDisplacementOptions
|
||||
|
||||
template<class Archive> void serialize(Archive &ar)
|
||||
{
|
||||
int mix_mode = int(color_mix_mode);
|
||||
ar(displace_border, smooth_enabled, smooth_strength, smooth_iterations, smooth_skip_border,
|
||||
pipeline_v2, v2_refine_mm, v2_regularize, v2_max_triangles_k,
|
||||
v2_relocate, color_mix_enabled, mix_mode, color_despeckle);
|
||||
color_mix_mode = ColorMixMode(mix_mode);
|
||||
v2_relocate, color_mix_enabled, color_despeckle);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -511,17 +489,9 @@ DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer
|
||||
// is. See GLGizmoTextureDisplacement::make_palette_quantizer().
|
||||
using ColorQuantizeFn = std::function<int(const Vec3f &)>;
|
||||
|
||||
// Resolves a palette index plus a surface position to the filament index that position should print
|
||||
// in. A pure entry ignores the position; a mixed one interleaves its two filaments per ColorMixMode.
|
||||
//
|
||||
// Deliberately separate from ColorQuantizeFn, and deliberately *not* used by the subdivision's colour
|
||||
// criterion: that criterion asks where the **perceived** colour changes, and must not see the
|
||||
// interleaving. Refining on every band or dither-cell boundary would spend the whole triangle budget
|
||||
// drawing a pattern the eye is supposed to blend away.
|
||||
using ColorResolveFn = std::function<int(int palette_index, const Vec3f &pos, const Vec3f &normal)>;
|
||||
|
||||
// One printable colour: either a loaded filament on its own, or a blend of two of them realised by
|
||||
// interleaving (see ColorMixMode). Plain data, so it can be captured into a background job.
|
||||
// interleaving, which the slicer does per print layer. Plain data, so it can be captured into a job.
|
||||
struct PrintableColor
|
||||
{
|
||||
Vec3f rgb = Vec3f::Zero(); // what it looks like; for a mix, the perceptual average of the two
|
||||
@@ -538,9 +508,6 @@ struct TextureColorSettings
|
||||
{
|
||||
std::vector<PrintableColor> palette;
|
||||
std::vector<PrintableColor> palette_pure; // the filaments alone, for flat-colour images
|
||||
ColorMixMode mix_mode = ColorMixMode::ZBands;
|
||||
float layer_height = 0.2f; // sizes the Z bands
|
||||
float dither_cell_mm = 0.4f; // sizes the XY dither cells
|
||||
int despeckle_passes = 2;
|
||||
|
||||
bool empty() const { return palette.empty(); }
|
||||
@@ -780,9 +747,6 @@ struct TextureColorRequest
|
||||
// made of flat colours (TextureDetail::flat_colors) is matched with this one, so a tile or a logo
|
||||
// prints in single filaments while a photograph on another layer may still use mixes.
|
||||
ColorQuantizeFn quantize_pure;
|
||||
// Palette index + position -> filament. Optional: without it a palette index is taken to be a
|
||||
// filament index directly, which is the no-mixing case.
|
||||
ColorResolveFn resolve;
|
||||
// Majority-filter passes over the *perceived* colour, before any interleaving is resolved.
|
||||
//
|
||||
// Sampling a detailed image once per triangle leaves salt-and-pepper wherever the image's own
|
||||
|
||||
@@ -298,7 +298,7 @@ int TriangleSelector::select_unsplit_triangle(const Vec3f &hit, int facet_idx) c
|
||||
return this->select_unsplit_triangle(hit, facet_idx, neighbors);
|
||||
}
|
||||
|
||||
void TriangleSelector::select_patch(int facet_start, std::unique_ptr<Cursor> &&cursor, EnforcerBlockerType new_state, const Transform3d& trafo_no_translate, bool triangle_splitting, float highlight_by_angle_deg, const bool select_partially)
|
||||
void TriangleSelector::select_patch(int facet_start, std::unique_ptr<Cursor> &&cursor, EnforcerBlockerType new_state, const Transform3d& trafo_no_translate, bool triangle_splitting, float highlight_by_angle_deg, const Vec3f &up_direction, const bool select_partially)
|
||||
{
|
||||
assert(facet_start < m_orig_size_indices);
|
||||
|
||||
@@ -359,8 +359,8 @@ void TriangleSelector::select_patch(int facet_start, std::unique_ptr<Cursor> &&c
|
||||
int facet = facets_to_check[facet_idx];
|
||||
const Vec3f& facet_normal = m_face_normals[m_triangles[facet].source_triangle];
|
||||
Matrix3f normal_matrix = static_cast<Matrix3f>(trafo_no_translate.matrix().block(0, 0, 3, 3).inverse().transpose().cast<float>());
|
||||
float world_normal_z = (normal_matrix* facet_normal).normalized().z();
|
||||
if (!visited[facet] && (highlight_by_angle_deg == 0.f || world_normal_z < highlight_angle_limit)) {
|
||||
float world_normal_dot = (normal_matrix * facet_normal).normalized().dot(up_direction);
|
||||
if (!visited[facet] && (highlight_by_angle_deg == 0.f || world_normal_dot < highlight_angle_limit)) {
|
||||
if (select_triangle(facet, new_state, triangle_splitting, select_partially)) {
|
||||
// add neighboring facets to list to be processed later
|
||||
for (int neighbor_idx : m_neighbors[facet])
|
||||
@@ -385,7 +385,7 @@ bool TriangleSelector::is_facet_clipped(int facet_idx, const ClippingPlane &clp)
|
||||
|
||||
void TriangleSelector::seed_fill_select_triangles(const Vec3f &hit, int facet_start, const Transform3d& trafo_no_translate,
|
||||
const ClippingPlane &clp, float seed_fill_angle, float highlight_by_angle_deg,
|
||||
bool force_reselection)
|
||||
const Vec3f &up_direction, bool force_reselection)
|
||||
{
|
||||
assert(facet_start < m_orig_size_indices);
|
||||
|
||||
@@ -409,8 +409,8 @@ void TriangleSelector::seed_fill_select_triangles(const Vec3f &hit, int facet_st
|
||||
|
||||
const Vec3f &facet_normal = m_face_normals[m_triangles[current_facet].source_triangle];
|
||||
Matrix3f normal_matrix = static_cast<Matrix3f>(trafo_no_translate.matrix().block(0, 0, 3, 3).inverse().transpose().cast<float>());
|
||||
float world_normal_z = (normal_matrix * facet_normal).normalized().z();
|
||||
if (!visited[current_facet] && (highlight_by_angle_deg == 0.f || world_normal_z < highlight_angle_limit)) {
|
||||
float world_normal_dot = (normal_matrix * facet_normal).normalized().dot(up_direction);
|
||||
if (!visited[current_facet] && (highlight_by_angle_deg == 0.f || world_normal_dot < highlight_angle_limit)) {
|
||||
if (m_triangles[current_facet].is_split()) {
|
||||
for (int split_triangle_idx = 0; split_triangle_idx <= m_triangles[current_facet].number_of_split_sides(); ++split_triangle_idx) {
|
||||
assert(split_triangle_idx < int(m_triangles[current_facet].children.size()));
|
||||
@@ -2605,7 +2605,7 @@ TriangleSelector::TriangleSplittingData TriangleSelector::remap_painting(
|
||||
if (TriangleCursor::check_normal(norm_b, -norm_a) && check_overlap(pv0, pv1, pv2, ta, tb, tc)) {
|
||||
// Paint this face
|
||||
target_selector.select_patch(face_idx, TriangleCursor::build_cursor(source_selector, tri), tri.get_state(),
|
||||
Transform3d::Identity(), true, 0.f, true);
|
||||
Transform3d::Identity(), true, 0.f, Vec3f::UnitZ(), true);
|
||||
}
|
||||
return true; // continue traversal
|
||||
});
|
||||
|
||||
@@ -350,6 +350,7 @@ public:
|
||||
const Transform3d &trafo_no_translate, // matrix to get from mesh to world without translation
|
||||
bool triangle_splitting, // If triangles will be split base on the cursor or not
|
||||
float highlight_by_angle_deg = 0.f, // The maximal angle of overhang. If it is set to a non-zero value, it is possible to paint only the triangles of overhang defined by this angle in degrees.
|
||||
const Vec3f &up_direction = Vec3f::UnitZ(), // Up direction for overhang detection (accounts for build plate tilt)
|
||||
bool select_partially = false); // Select a triangle if it's partially in the cursor but too small to be subdivided
|
||||
|
||||
void seed_fill_select_triangles(const Vec3f &hit, // point where to start
|
||||
@@ -358,6 +359,7 @@ public:
|
||||
const ClippingPlane &clp, // Clipping plane to limit painting to not clipped facets only
|
||||
float seed_fill_angle, // the maximal angle between two facets to be painted by the same color
|
||||
float highlight_by_angle_deg = 0.f, // The maximal angle of overhang. If it is set to a non-zero value, it is possible to paint only the triangles of overhang defined by this angle in degrees.
|
||||
const Vec3f &up_direction = Vec3f::UnitZ(), // Up direction for overhang detection (accounts for build plate tilt)
|
||||
bool force_reselection = false); // force reselection of the triangle mesh even in cases that mouse is pointing on the selected triangle
|
||||
|
||||
void bucket_fill_select_triangles(const Vec3f &hit, // point where to start
|
||||
|
||||
+56
-31
@@ -1,4 +1,5 @@
|
||||
#include "calib.hpp"
|
||||
#include "GCode/BeltKinematics.hpp"
|
||||
#include "BoundingBox.hpp"
|
||||
#include "Config.hpp"
|
||||
#include "Flow.hpp"
|
||||
@@ -18,6 +19,8 @@
|
||||
#include <iomanip>
|
||||
#include <vector>
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
#include "clonable_ptr.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
@@ -648,9 +651,9 @@ CustomGCode::Info CalibPressureAdvancePattern::generate_custom_gcodes(const Dyna
|
||||
|
||||
refresh_setup(config, is_bbl_machine, object, origin);
|
||||
|
||||
gcode << move_to(Vec2d(m_starting_point.x(), m_starting_point.y()), m_writer, "Move to start XY position");
|
||||
gcode << m_writer.travel_to_z(height_first_layer() + height_z_offset(), "Move to start Z position");
|
||||
gcode << m_writer.set_pressure_advance(m_params.start);
|
||||
gcode << move_to(Vec2d(m_starting_point.x(), m_starting_point.y()), *m_writer, "Move to start XY position");
|
||||
gcode << m_writer->travel_to_z(height_first_layer() + height_z_offset(), "Move to start Z position");
|
||||
gcode << m_writer->set_pressure_advance(m_params.start);
|
||||
|
||||
const DrawBoxOptArgs default_box_opt_args(wall_count(), height_first_layer(), line_width_first_layer(),
|
||||
speed_adjust(speed_first_layer()));
|
||||
@@ -658,15 +661,15 @@ CustomGCode::Info CalibPressureAdvancePattern::generate_custom_gcodes(const Dyna
|
||||
// create anchoring frame
|
||||
//pattern uses outer wall speed/width
|
||||
gcode << ";" << GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Role) << "Outer wall\n";
|
||||
gcode << draw_box(m_writer, m_starting_point.x(), m_starting_point.y(), print_size_x(), frame_size_y(), default_box_opt_args);
|
||||
gcode << draw_box(*m_writer, m_starting_point.x(), m_starting_point.y(), print_size_x(), frame_size_y(), default_box_opt_args);
|
||||
|
||||
// create tab for numbers
|
||||
DrawBoxOptArgs draw_box_opt_args = default_box_opt_args;
|
||||
draw_box_opt_args.is_filled = true;
|
||||
draw_box_opt_args.num_perimeters = wall_count();
|
||||
//draw box as bottom surface, so numbers are clearly visible on top
|
||||
gcode << ";" << GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Role) << "Bottom surface\n";
|
||||
gcode << draw_box(m_writer, m_starting_point.x(), m_starting_point.y() + frame_size_y() + line_spacing_first_layer(),
|
||||
gcode << ";" << GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Role) << "Bottom surface\n";
|
||||
gcode << draw_box(*m_writer, m_starting_point.x(), m_starting_point.y() + frame_size_y() + line_spacing_first_layer(),
|
||||
print_size_x(),
|
||||
max_numbering_height() + line_spacing_first_layer() + m_glyph_padding_vertical * 2, draw_box_opt_args);
|
||||
|
||||
@@ -694,15 +697,15 @@ CustomGCode::Info CalibPressureAdvancePattern::generate_custom_gcodes(const Dyna
|
||||
gcode = std::stringstream(); // reset for next layer contents
|
||||
gcode << "; start pressure advance pattern for layer\n";
|
||||
|
||||
gcode << m_writer.travel_to_z(layer_height, "Move to layer height");
|
||||
gcode << m_writer.reset_e();
|
||||
gcode << m_writer->travel_to_z(layer_height, "Move to layer height");
|
||||
gcode << m_writer->reset_e();
|
||||
}
|
||||
|
||||
// line numbering
|
||||
if (i == 1) {
|
||||
m_number_len = max_numbering_length();
|
||||
|
||||
gcode << m_writer.set_pressure_advance(m_params.start);
|
||||
gcode << m_writer->set_pressure_advance(m_params.start);
|
||||
|
||||
double number_e_per_mm = e_per_mm(line_width(), height_layer(),
|
||||
m_config.option<ConfigOptionFloats>("nozzle_diameter")->get_at(0),
|
||||
@@ -714,20 +717,20 @@ CustomGCode::Info CalibPressureAdvancePattern::generate_custom_gcodes(const Dyna
|
||||
for (int j = 0; j < num_patterns; j += 2) {
|
||||
gcode << draw_number(glyph_start_x(j), m_starting_point.y() + frame_size_y() + m_glyph_padding_vertical + line_width(),
|
||||
m_params.start + (j * m_params.step), m_draw_digit_mode, line_width(), number_e_per_mm,
|
||||
speed_first_layer(), m_writer);
|
||||
speed_first_layer(), *m_writer);
|
||||
}
|
||||
|
||||
// flow value
|
||||
int line_num = num_patterns + 2;
|
||||
gcode << draw_number(glyph_start_x(line_num), m_starting_point.y() + frame_size_y() + m_glyph_padding_vertical + line_width(),
|
||||
flow_val(), m_draw_digit_mode, line_width(), number_e_per_mm,
|
||||
speed_first_layer(), m_writer);
|
||||
speed_first_layer(), *m_writer);
|
||||
|
||||
// acceleration
|
||||
line_num = num_patterns + 4;
|
||||
gcode << draw_number(glyph_start_x(line_num), m_starting_point.y() + frame_size_y() + m_glyph_padding_vertical + line_width(),
|
||||
accel, m_draw_digit_mode, line_width(), number_e_per_mm,
|
||||
speed_first_layer(), m_writer);
|
||||
speed_first_layer(), *m_writer);
|
||||
}
|
||||
|
||||
|
||||
@@ -746,20 +749,20 @@ CustomGCode::Info CalibPressureAdvancePattern::generate_custom_gcodes(const Dyna
|
||||
/* Draw a line at slightly slower accel and speed in order to trick gcode writer to force update acceleration and speed.
|
||||
* We do this since several tests may be generated by their own gcode writers which are
|
||||
* not aware about their neighbours updating acceleration/speed */
|
||||
gcode << m_writer.set_print_acceleration(std::max<int>(1, accel - 1));
|
||||
gcode << move_to(Vec2d(m_starting_point.x(), m_starting_point.y()), m_writer, "Move to starting point", zhop_height, layer_height);
|
||||
gcode << draw_line(m_writer, Vec2d(m_starting_point.x(), m_starting_point.y() + frame_size_y()), line_width(), height_layer(), speed_adjust(std::max<int>(1, speed_perimeter() - 1)), "Accel/flow trick line");
|
||||
gcode << m_writer.set_print_acceleration(accel);
|
||||
gcode << m_writer->set_print_acceleration(std::max<int>(1, accel - 1));
|
||||
gcode << move_to(Vec2d(m_starting_point.x(), m_starting_point.y()), *m_writer, "Move to starting point", zhop_height, layer_height);
|
||||
gcode << draw_line(*m_writer, Vec2d(m_starting_point.x(), m_starting_point.y() + frame_size_y()), line_width(), height_layer(), speed_adjust(std::max<int>(1, speed_perimeter() - 1)), "Accel/flow trick line");
|
||||
gcode << m_writer->set_print_acceleration(accel);
|
||||
}
|
||||
|
||||
double initial_x = to_x;
|
||||
double initial_y = to_y;
|
||||
|
||||
gcode << move_to(Vec2d(to_x, to_y), m_writer, "Move to pattern start",zhop_height,layer_height);
|
||||
gcode << move_to(Vec2d(to_x, to_y), *m_writer, "Move to pattern start",zhop_height,layer_height);
|
||||
|
||||
for (int j = 0; j < num_patterns; ++j) {
|
||||
// increment pressure advance
|
||||
gcode << m_writer.set_pressure_advance(m_params.start + (j * m_params.step));
|
||||
gcode << m_writer->set_pressure_advance(m_params.start + (j * m_params.step));
|
||||
gcode << ";" << GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Role) << "Outer wall\n";
|
||||
for (int k = 0; k < wall_count(); ++k) {
|
||||
to_x += std::cos(to_radians(m_corner_angle) / 2) * side_length;
|
||||
@@ -769,27 +772,27 @@ CustomGCode::Info CalibPressureAdvancePattern::generate_custom_gcodes(const Dyna
|
||||
auto draw_line_arg_line_width = line_width(); // don't use line_width_first_layer so results are consistent across all layers
|
||||
auto draw_line_arg_speed = i == 0 ? speed_adjust(speed_first_layer()) : speed_adjust(speed_perimeter());
|
||||
auto draw_line_arg_comment = "Print pattern wall";
|
||||
gcode << draw_line(m_writer, Vec2d(to_x, to_y), draw_line_arg_line_width, draw_line_arg_height, draw_line_arg_speed, draw_line_arg_comment);
|
||||
gcode << draw_line(*m_writer, Vec2d(to_x, to_y), draw_line_arg_line_width, draw_line_arg_height, draw_line_arg_speed, draw_line_arg_comment);
|
||||
|
||||
to_x -= std::cos(to_radians(m_corner_angle) / 2) * side_length;
|
||||
to_y += std::sin(to_radians(m_corner_angle) / 2) * side_length;
|
||||
|
||||
gcode << draw_line(m_writer, Vec2d(to_x, to_y), draw_line_arg_line_width, draw_line_arg_height, draw_line_arg_speed, draw_line_arg_comment);
|
||||
gcode << draw_line(*m_writer, Vec2d(to_x, to_y), draw_line_arg_line_width, draw_line_arg_height, draw_line_arg_speed, draw_line_arg_comment);
|
||||
|
||||
to_y = initial_y;
|
||||
if (k != wall_count() - 1) {
|
||||
// perimeters not done yet. move to next perimeter
|
||||
to_x += line_spacing_angle();
|
||||
gcode << move_to(Vec2d(to_x, to_y), m_writer, "Move to start next pattern wall", zhop_height, layer_height); // Call move to command with XY as well as z hop and layer height to invoke and undo z lift
|
||||
gcode << move_to(Vec2d(to_x, to_y), *m_writer, "Move to start next pattern wall", zhop_height, layer_height); // Call move to command with XY as well as z hop and layer height to invoke and undo z lift
|
||||
} else if (j != num_patterns - 1) {
|
||||
// patterns not done yet. move to next pattern
|
||||
to_x += m_pattern_spacing + line_width();
|
||||
gcode << move_to(Vec2d(to_x, to_y), m_writer, "Move to next pattern", zhop_height, layer_height); // Call move to command with XY as well as z hop and layer height to invoke and undo z lift
|
||||
gcode << move_to(Vec2d(to_x, to_y), *m_writer, "Move to next pattern", zhop_height, layer_height); // Call move to command with XY as well as z hop and layer height to invoke and undo z lift
|
||||
} else if (i != m_num_layers - 1) {
|
||||
// layers not done yet. move back to start
|
||||
to_x = initial_x;
|
||||
gcode << move_to(Vec2d(to_x, to_y), m_writer, "Move back to start position", zhop_height, layer_height); // Call move to command with XY as well as z hop and layer height to invoke and undo z lift
|
||||
gcode << m_writer.reset_e(); // reset extruder before printing placeholder cube to avoid over extrusion
|
||||
gcode << move_to(Vec2d(to_x, to_y), *m_writer, "Move back to start position", zhop_height, layer_height); // Call move to command with XY as well as z hop and layer height to invoke and undo z lift
|
||||
gcode << m_writer->reset_e(); // reset extruder before printing placeholder cube to avoid over extrusion
|
||||
} else {
|
||||
// everything done
|
||||
}
|
||||
@@ -797,8 +800,8 @@ CustomGCode::Info CalibPressureAdvancePattern::generate_custom_gcodes(const Dyna
|
||||
}
|
||||
}
|
||||
|
||||
gcode << m_writer.reset_e();
|
||||
gcode << m_writer.set_pressure_advance(m_params.start);
|
||||
gcode << m_writer->reset_e();
|
||||
gcode << m_writer->set_pressure_advance(m_params.start);
|
||||
gcode << "; end pressure advance pattern for layer\n";
|
||||
|
||||
CustomGCode::Item item;
|
||||
@@ -875,13 +878,35 @@ void CalibPressureAdvancePattern::_refresh_writer(bool is_bbl_machine, const Mod
|
||||
PrintConfig print_config;
|
||||
print_config.apply(m_config, true);
|
||||
|
||||
m_writer.apply_print_config(print_config);
|
||||
m_writer.set_xy_offset(origin(0), origin(1));
|
||||
m_writer.set_is_bbl_machine(is_bbl_machine);
|
||||
// ORCA-Belt: the pattern is drawn in logical bed coordinates directly on
|
||||
// the build surface — on a belt printer that means the belt plane, which
|
||||
// needs the machine kinematics (axis remap + frame shear/scale) with the
|
||||
// coordinates interpreted as world points (see set_world_coordinates).
|
||||
if (print_config.belt_printer.value) {
|
||||
auto belt_writer = std::make_shared<GCodeWriter>();
|
||||
install_belt_kinematics(*belt_writer, print_config, /*world_coordinates=*/true);
|
||||
const int rx = int(print_config.gcode_remap_x.value);
|
||||
const int ry = int(print_config.gcode_remap_y.value);
|
||||
const int rz = int(print_config.gcode_remap_z.value);
|
||||
if (rx != 0 || ry != 1 || rz != 2) {
|
||||
belt_writer->set_axis_remap(rx, ry, rz);
|
||||
BoundingBoxf bbox_bed(print_config.printable_area.values);
|
||||
belt_writer->set_build_volume_max(Vec3d(bbox_bed.max.x(), bbox_bed.max.y(),
|
||||
print_config.printable_height.value));
|
||||
}
|
||||
m_writer = std::move(belt_writer);
|
||||
} else if (m_writer && dynamic_cast<const BeltKinematics *>(&m_writer->kinematics()) != nullptr) {
|
||||
// Previously configured for a belt printer; drop back to a plain writer.
|
||||
m_writer = std::make_shared<GCodeWriter>();
|
||||
}
|
||||
|
||||
m_writer->apply_print_config(print_config);
|
||||
m_writer->set_xy_offset(origin(0), origin(1));
|
||||
m_writer->set_is_bbl_machine(is_bbl_machine);
|
||||
|
||||
const unsigned int extruder_id = object.volumes.front()->extruder_id();
|
||||
m_writer.set_extruders({extruder_id});
|
||||
m_writer.set_extruder(extruder_id);
|
||||
m_writer->set_extruders({extruder_id});
|
||||
m_writer->set_extruder(extruder_id);
|
||||
}
|
||||
|
||||
double CalibPressureAdvancePattern::object_size_x() const
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <utility>
|
||||
#include <memory>
|
||||
#define calib_pressure_advance_dd
|
||||
|
||||
#include "GCodeWriter.hpp"
|
||||
@@ -369,7 +370,10 @@ private:
|
||||
|
||||
const Calib_Params &m_params;
|
||||
|
||||
GCodeWriter m_writer;
|
||||
// Belt printers get belt kinematics installed on it (_refresh_writer);
|
||||
// shared_ptr keeps the class copyable — the writer is rebuilt by
|
||||
// refresh_setup() before every use anyway.
|
||||
std::shared_ptr<GCodeWriter> m_writer{std::make_shared<GCodeWriter>()};
|
||||
Vec3d m_starting_point;
|
||||
bool m_is_start_point_fixed = false;
|
||||
|
||||
|
||||
@@ -440,6 +440,8 @@ set(SLIC3R_GUI_SOURCES
|
||||
GUI/ParamsPanel.hpp
|
||||
GUI/PartPlate.cpp
|
||||
GUI/PartPlate.hpp
|
||||
GUI/BeltPurgeTower.cpp
|
||||
GUI/BeltPurgeTower.hpp
|
||||
GUI/FilamentGroupPopup.hpp
|
||||
GUI/FilamentGroupPopup.cpp
|
||||
GUI/PhysicalPrinterDialog.cpp
|
||||
|
||||
@@ -36,9 +36,12 @@
|
||||
#include <tuple>
|
||||
#include "libslic3r/Preset.hpp"
|
||||
#include "libslic3r/Config.hpp"
|
||||
#include <Eigen/Geometry>
|
||||
|
||||
#if BOOST_VERSION >= 107800
|
||||
#include <boost/timer/timer.hpp>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#else
|
||||
#include <boost/timer.hpp>
|
||||
#endif
|
||||
@@ -410,6 +413,8 @@ void Bed3D::render_internal(GLCanvas3D& canvas, const Transform3d& view_matrix,
|
||||
case Type::Custom: { render_custom(canvas, view_matrix, projection_matrix, bottom); break; }
|
||||
}
|
||||
|
||||
render_gravity_arrow(view_matrix, projection_matrix);
|
||||
|
||||
glsafe(::glDisable(GL_DEPTH_TEST));
|
||||
}
|
||||
|
||||
@@ -714,7 +719,7 @@ void Bed3D::render_model(const Transform3d& view_matrix, const Transform3d& proj
|
||||
if (shader != nullptr) {
|
||||
shader->start_using();
|
||||
shader->set_uniform("emission_factor", 0.0f);
|
||||
const Transform3d model_matrix = Geometry::assemble_transform(m_model_offset);
|
||||
Transform3d model_matrix = Geometry::assemble_transform(m_model_offset);
|
||||
shader->set_uniform("volume_world_matrix", model_matrix);
|
||||
shader->set_uniform("view_model_matrix", view_matrix * model_matrix);
|
||||
shader->set_uniform("projection_matrix", projection_matrix);
|
||||
@@ -753,6 +758,59 @@ void Bed3D::render_custom(GLCanvas3D& canvas, const Transform3d& view_matrix, co
|
||||
render_texture(bottom, canvas);*/
|
||||
}
|
||||
|
||||
void Bed3D::render_gravity_arrow(const Transform3d& view_matrix, const Transform3d& projection_matrix)
|
||||
{
|
||||
// build_plate_tilt_{x,y} are kept in sync with the belt tilt (see TabPrinter), so
|
||||
// reading them here covers both belt and non-belt tilted printers.
|
||||
const Vec3d up_dir = build_plate_tilt_up_direction();
|
||||
if (up_dir == Vec3d::UnitZ()) {
|
||||
m_gravity_arrow.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
// A plain line along the tilted "up" direction -- the way the layers lean, i.e.
|
||||
// the gantry -- drawn like the bed axes (no tip: the other direction is not
|
||||
// possible) and shorter than them, so it reads as a hint inside the YZ corner.
|
||||
const float length = 0.6f * m_axes.get_total_length();
|
||||
if (!m_gravity_arrow.is_initialized() || m_gravity_arrow_length != length) {
|
||||
m_gravity_arrow.reset();
|
||||
m_gravity_arrow.init_from(smooth_cylinder(16, /*Radius*/ length / 75.f, length));
|
||||
m_gravity_arrow_length = length;
|
||||
}
|
||||
|
||||
// The cylinder model points along +Z. Compute the rotation that aligns it with
|
||||
// up_dir: rotation axis = cross(+Z, up_dir), angle = acos(dot(+Z, up_dir)).
|
||||
Vec3d from = Vec3d::UnitZ();
|
||||
Vec3d to = up_dir;
|
||||
double dot = from.dot(to);
|
||||
Transform3d rot = Transform3d::Identity();
|
||||
if (dot < -0.9999) {
|
||||
// Nearly opposite -- rotate 180 degrees around X
|
||||
rot = Eigen::AngleAxisd(M_PI, Vec3d::UnitX()) * rot;
|
||||
} else if (dot < 0.9999) {
|
||||
Vec3d axis = from.cross(to).normalized();
|
||||
double angle = std::acos(std::clamp(dot, -1.0, 1.0));
|
||||
rot = Eigen::AngleAxisd(angle, axis) * rot;
|
||||
}
|
||||
|
||||
GLShaderProgram* shader = wxGetApp().get_shader("flat");
|
||||
if (shader == nullptr)
|
||||
return;
|
||||
|
||||
glsafe(::glEnable(GL_DEPTH_TEST));
|
||||
shader->start_using();
|
||||
|
||||
const Camera& camera = wxGetApp().plater()->get_camera();
|
||||
Transform3d model_matrix = Eigen::Translation3d(m_axes.get_origin()) * rot;
|
||||
shader->set_uniform("view_model_matrix", camera.get_view_matrix() * model_matrix);
|
||||
shader->set_uniform("projection_matrix", camera.get_projection_matrix());
|
||||
|
||||
m_gravity_arrow.set_color({ 1.0f, 0.85f, 0.0f, 1.0f }); // yellow
|
||||
m_gravity_arrow.render();
|
||||
|
||||
shader->stop_using();
|
||||
}
|
||||
|
||||
void Bed3D::render_default(bool bottom, const Transform3d& view_matrix, const Transform3d& projection_matrix)
|
||||
{
|
||||
// m_texture.reset();
|
||||
|
||||
@@ -115,6 +115,8 @@ private:
|
||||
//GLTexture m_temp_texture;
|
||||
GLModel m_model;
|
||||
Vec3d m_model_offset{ Vec3d::Zero() };
|
||||
GLModel m_gravity_arrow;
|
||||
float m_gravity_arrow_length{ 0.f };
|
||||
Axes m_axes;
|
||||
|
||||
float m_scale_factor{ 1.0f };
|
||||
@@ -144,6 +146,7 @@ public:
|
||||
|
||||
// Build volume geometry for various collision detection tasks.
|
||||
const BuildVolume& build_volume() const { return m_build_volume; }
|
||||
BuildVolume& build_volume() { return m_build_volume; }
|
||||
|
||||
// Was the model provided, or was it generated procedurally?
|
||||
Type get_type() const { return m_type; }
|
||||
@@ -183,7 +186,8 @@ private:
|
||||
void render_model(const Transform3d& view_matrix, const Transform3d& projection_matrix);
|
||||
void render_custom(GLCanvas3D& canvas, const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom);
|
||||
void render_default(bool bottom, const Transform3d& view_matrix, const Transform3d& projection_matrix);
|
||||
|
||||
void render_gravity_arrow(const Transform3d& view_matrix, const Transform3d& projection_matrix);
|
||||
|
||||
// BBS: remove the bed picking logic
|
||||
// void register_raycasters_for_picking(const GLModel::Geometry& geometry, const Transform3d& trafo);
|
||||
};
|
||||
|
||||
@@ -1238,6 +1238,10 @@ void GLVolumeCollection::render(GLVolumeCollection::ERenderType type,
|
||||
// default sampler unit 0, which can conflict with other sampler types.
|
||||
shader->set_uniform("depth_tex", OUTLINE_DEPTH_TEX_UNIT);
|
||||
|
||||
// Compute up direction accounting for build plate tilt. This is frame-invariant
|
||||
// (config cannot change mid-render), so compute it once before the volume loop.
|
||||
const Vec3f up_direction = GUI::build_plate_tilt_up_direction().cast<float>();
|
||||
|
||||
for (GLVolumeWithIdAndZ& volume : to_render) {
|
||||
#if ENABLE_MODIFIERS_ALWAYS_TRANSPARENT
|
||||
if (type == ERenderType::Transparent) {
|
||||
@@ -1316,6 +1320,7 @@ void GLVolumeCollection::render(GLVolumeCollection::ERenderType type,
|
||||
shader->set_uniform("slope.actived", m_slope.isGlobalActive && !volume.first->is_modifier && !volume.first->is_wipe_tower);
|
||||
shader->set_uniform("slope.volume_world_normal_matrix", static_cast<Matrix3f>(volume.first->world_matrix().matrix().block(0, 0, 3, 3).inverse().transpose().cast<float>()));
|
||||
shader->set_uniform("slope.normal_z", support_normal_z);
|
||||
shader->set_uniform("slope.up_direction", up_direction);
|
||||
|
||||
#if ENABLE_ENVIRONMENT_MAP
|
||||
unsigned int environment_texture_id = GUI::wxGetApp().plater()->get_environment_texture_id();
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
#include "slic3r/GUI/Widgets/Label.hpp"
|
||||
#include <utility>
|
||||
#include "slic3r/GUI/wxExtensions.hpp"
|
||||
#include "slic3r/GUI/Widgets/HyperLink.hpp"
|
||||
#include <cstdlib>
|
||||
#include <cstdio>
|
||||
#include "slic3r/GUI/DeviceCore/DevDefs.h"
|
||||
@@ -48,6 +47,7 @@
|
||||
#include <wx/panel.h>
|
||||
#include <wx/textctrl.h>
|
||||
#include <wx/string.h>
|
||||
#include <wx/utils.h>
|
||||
#include <wx/valtext.h>
|
||||
#include <wx/sizer.h>
|
||||
#include <wx/peninfobase.h>
|
||||
@@ -103,6 +103,14 @@ void AMSMaterialsSetting::create()
|
||||
|
||||
m_sizer_button->Add(0, 0, 1, wxEXPAND, 0);
|
||||
|
||||
// Orca: link to the Orca Slicer pressure-advance wiki (region-agnostic).
|
||||
m_wiki_ctrl = new Button(this, "", "toolbar_wiki", 0, 15);
|
||||
auto wiki_url = "https://www.orcaslicer.com/wiki/pressure_advance_calib";
|
||||
m_wiki_ctrl->SetToolTip(_L("Wiki Guide") + "\n" + wiki_url);
|
||||
m_wiki_ctrl->SetStyle(ButtonStyle::Confirm, ButtonType::Circle);
|
||||
m_wiki_ctrl->SetCanFocus(false);
|
||||
m_wiki_ctrl->Bind(wxEVT_LEFT_DOWN, ([wiki_url](auto& e) {wxLaunchDefaultBrowser(wiki_url);}));
|
||||
|
||||
m_button_confirm = new Button(this, _L("Confirm"));
|
||||
m_button_confirm->SetStyle(ButtonStyle::Confirm, ButtonType::Choice);
|
||||
m_button_confirm->Bind(wxEVT_BUTTON, &AMSMaterialsSetting::on_select_ok, this);
|
||||
@@ -115,8 +123,9 @@ void AMSMaterialsSetting::create()
|
||||
m_button_close->SetStyle(ButtonStyle::Regular, ButtonType::Choice);
|
||||
m_button_close->Bind(wxEVT_BUTTON, &AMSMaterialsSetting::on_select_close, this);
|
||||
|
||||
m_sizer_button->Add(m_button_confirm, 0, wxALIGN_CENTER | wxRIGHT, FromDIP(20));
|
||||
m_sizer_button->Add(m_button_reset, 0, wxALIGN_CENTER | wxRIGHT, FromDIP(20));
|
||||
m_sizer_button->Add(m_wiki_ctrl, 0, wxALIGN_CENTER | wxRIGHT, FromDIP(20));
|
||||
m_sizer_button->Add(m_button_confirm, 0, wxALIGN_CENTER | wxRIGHT, FromDIP(10));
|
||||
m_sizer_button->Add(m_button_reset, 0, wxALIGN_CENTER | wxRIGHT, FromDIP(10));
|
||||
m_sizer_button->Add(m_button_close, 0, wxALIGN_CENTER, 0);
|
||||
|
||||
m_sizer_main->Add(m_panel_normal, 0, wxALL, FromDIP(2));
|
||||
@@ -340,11 +349,7 @@ void AMSMaterialsSetting::create_panel_kn(wxWindow* parent)
|
||||
m_ratio_text->SetForegroundColour(wxColour(50, 58, 61));
|
||||
m_ratio_text->SetFont(Label::Head_14);
|
||||
|
||||
// Orca: link to the Orca Slicer pressure-advance wiki (region-agnostic).
|
||||
wxString link_url = "https://www.orcaslicer.com/wiki/pressure_advance_calib";
|
||||
m_wiki_ctrl = new HyperLink(parent, _L("Wiki Guide"), link_url);
|
||||
cali_title_sizer->Add(m_ratio_text, 0, wxALIGN_CENTER_VERTICAL);
|
||||
cali_title_sizer->Add(m_wiki_ctrl, 0, wxALIGN_CENTER_VERTICAL);
|
||||
|
||||
wxBoxSizer *m_sizer_cali_resutl = new wxBoxSizer(wxHORIZONTAL);
|
||||
// pa profile
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
#include "Widgets/CheckBox.hpp"
|
||||
#include "Widgets/ComboBox.hpp"
|
||||
#include "Widgets/TextInput.hpp"
|
||||
#include "Widgets/HyperLink.hpp"
|
||||
#include "slic3r/Utils/CalibUtils.hpp"
|
||||
#include <wx/anybutton.h>
|
||||
#include <wx/colour.h>
|
||||
@@ -194,7 +193,7 @@ protected:
|
||||
|
||||
wxPanel * m_panel_kn;
|
||||
wxStaticText* m_ratio_text;
|
||||
HyperLink * m_wiki_ctrl;
|
||||
Button * m_wiki_ctrl;
|
||||
wxStaticText* m_k_param;
|
||||
TextInput* m_input_k_val;
|
||||
wxStaticText* m_n_param;
|
||||
|
||||
@@ -0,0 +1,465 @@
|
||||
#include "BeltPurgeTower.hpp"
|
||||
|
||||
#include "GUI_App.hpp"
|
||||
#include "GUI_ObjectList.hpp"
|
||||
#include "PartPlate.hpp"
|
||||
#include "I18N.hpp"
|
||||
|
||||
#include "libslic3r/Model.hpp"
|
||||
#include "libslic3r/Preset.hpp"
|
||||
#include "libslic3r/PresetBundle.hpp"
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
#include "libslic3r/FilamentMixer.hpp"
|
||||
#include "libslic3r/TriangleMesh.hpp"
|
||||
#include "libslic3r/Geometry.hpp"
|
||||
#include "libslic3r/BoundingBox.hpp"
|
||||
#include "libslic3r/Config.hpp"
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include "libslic3r/libslic3r.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <set>
|
||||
#include <vector>
|
||||
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <cstddef>
|
||||
#include <math.h>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace Slic3r {
|
||||
namespace GUI {
|
||||
|
||||
// The classic wipe tower is disabled in belt mode (its G-code bypasses the belt
|
||||
// transform), so filament-change purging is routed into this prism via
|
||||
// flush_into_objects (see Print::_plan_belt_purge()). The prism is a real model
|
||||
// object so it is sliced through the normal pipeline and picks up the belt
|
||||
// rotation. Width across the belt is user-set (belt_purge_tower_width); height
|
||||
// is sized so each tilted slicing plane's cross-section through the prism can
|
||||
// absorb the worst-case purge volume of one layer; length follows the printed
|
||||
// objects along the belt (plus ramp/height compensation at both tilted ends).
|
||||
bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, ObjectList *obj_list, std::vector<BeltPurgeSignature> &sigs)
|
||||
{
|
||||
auto is_prism = [](const ModelObject *mo) {
|
||||
const ConfigOption *opt = mo->config.option("belt_purge_tower_object");
|
||||
return opt != nullptr && opt->getBool();
|
||||
};
|
||||
|
||||
// Every plate gets its own prism. A prism belongs to the plate it lies on; one
|
||||
// that lies on no plate is stale.
|
||||
const int plate_count = partplate_list.get_plate_count();
|
||||
sigs.resize(size_t(plate_count));
|
||||
std::vector<std::vector<int>> prisms_by_plate(static_cast<size_t>(plate_count));
|
||||
std::vector<int> stale_prisms;
|
||||
for (int i = 0; i < (int) model.objects.size(); ++i)
|
||||
if (is_prism(model.objects[i])) {
|
||||
const int plate_idx = model.objects[i]->instances.empty() ? -1 : partplate_list.find_instance(i, 0);
|
||||
if (plate_idx >= 0 && plate_idx < plate_count)
|
||||
prisms_by_plate[size_t(plate_idx)].push_back(i);
|
||||
else
|
||||
stale_prisms.push_back(i);
|
||||
}
|
||||
|
||||
// Deletes prism objects, keeping the sidebar and part plates in sync
|
||||
// (same primitives as Plater::priv::remove(), minus scene update — the
|
||||
// caller refreshes the scene). Highest index first, so the others stay valid.
|
||||
auto remove_prisms = [&](std::vector<int> idxs) {
|
||||
std::sort(idxs.begin(), idxs.end());
|
||||
for (auto it = idxs.rbegin(); it != idxs.rend(); ++it) {
|
||||
model.delete_object(size_t(*it));
|
||||
partplate_list.notify_instance_removed(*it, -1);
|
||||
obj_list->delete_object_from_list(size_t(*it));
|
||||
}
|
||||
};
|
||||
auto all_prisms = [&]() {
|
||||
std::vector<int> all = stale_prisms;
|
||||
for (const auto &v : prisms_by_plate)
|
||||
all.insert(all.end(), v.begin(), v.end());
|
||||
return all;
|
||||
};
|
||||
|
||||
// Cheap early-out for non-belt printers: only belt printers ever get a belt
|
||||
// purge tower, and this runs on every background-process tick — so avoid the
|
||||
// full_config() merge below unless this is actually a belt printer. (Also
|
||||
// tidies up a stale prism if the user switched away from a belt printer.)
|
||||
{
|
||||
const auto *belt_pre = wxGetApp().preset_bundle->printers.get_edited_preset().config.option<ConfigOptionBool>("belt_printer");
|
||||
if (belt_pre == nullptr || !belt_pre->value) {
|
||||
std::fill(sigs.begin(), sigs.end(), BeltPurgeSignature{});
|
||||
const std::vector<int> all = all_prisms();
|
||||
if (all.empty())
|
||||
return false;
|
||||
remove_prisms(all);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Read every sizing input from the MERGED full config — the exact same
|
||||
// config the backend slices with (BackgroundSlicingProcess::apply uses
|
||||
// preset_bundle->full_config()). Reading from the individual presets caused
|
||||
// GUI/backend mismatches: e.g. purge_in_prime_tower or a populated
|
||||
// flush_volumes_matrix present in the merged config but false/empty in the
|
||||
// preset the GUI happened to read, so the tower was sized for the small
|
||||
// prime_volume instead of the real color-change flush and could not absorb
|
||||
// it. The three names below alias the one merged config so the rest of the
|
||||
// function is unchanged.
|
||||
const DynamicPrintConfig full_cfg = wxGetApp().preset_bundle->full_config();
|
||||
const DynamicPrintConfig &printer_config = full_cfg;
|
||||
const DynamicPrintConfig &print_config = full_cfg;
|
||||
const DynamicPrintConfig &project_config = full_cfg;
|
||||
|
||||
const auto *belt_opt = printer_config.option<ConfigOptionBool>("belt_printer");
|
||||
const bool belt = belt_opt != nullptr && belt_opt->value;
|
||||
// Belt purge tower is its own type, gated by the belt-only printer option
|
||||
// enable_belt_purge_tower (not the classic process enable_prime_tower).
|
||||
const bool prime_tower_enabled = printer_config.has("enable_belt_purge_tower") && printer_config.opt_bool("enable_belt_purge_tower");
|
||||
const auto *seq_opt = print_config.option<ConfigOptionEnum<PrintSequence>>("print_sequence");
|
||||
const bool by_object = seq_opt != nullptr && seq_opt->value == PrintSequence::ByObject;
|
||||
|
||||
// Mixed filament slots are expanded to their physical components below.
|
||||
std::vector<unsigned char> is_mixed;
|
||||
std::vector<std::string> comp_strs;
|
||||
if (const auto *o = full_cfg.option<ConfigOptionBools>("filament_is_mixed"))
|
||||
is_mixed = o->values;
|
||||
if (const auto *o = full_cfg.option<ConfigOptionStrings>("filament_mixed_components"))
|
||||
comp_strs = o->values;
|
||||
|
||||
const double gap = 1.0;
|
||||
const double layer_h = print_config.has("layer_height") ? print_config.opt_float("layer_height") : 0.2;
|
||||
|
||||
// Belt geometry. The rotation axis is the gantry tilt axis; the belt
|
||||
// travels along the *other* horizontal axis (X-rotation -> belt along Y,
|
||||
// the CR-30 default). The purge prism is a long bar laid along the belt
|
||||
// travel direction, beside the parts. For no/Z rotation we fall back to
|
||||
// vertical slicing geometry (theta = 90 deg).
|
||||
const auto *axis_opt = printer_config.option<ConfigOptionEnum<BeltRotationAxis>>("belt_slice_rotation");
|
||||
const auto *angle_opt = printer_config.option<ConfigOptionFloat>("belt_slice_rotation_angle");
|
||||
const BeltRotationAxis rot = axis_opt != nullptr ? axis_opt->value : BeltRotationAxis::X;
|
||||
const bool belt_is_y = (rot != BeltRotationAxis::Y); // X / None / Z -> belt along Y
|
||||
double theta = M_PI / 2.;
|
||||
if ((rot == BeltRotationAxis::X || rot == BeltRotationAxis::Y) && angle_opt != nullptr && std::abs(angle_opt->value) > EPSILON)
|
||||
theta = std::clamp(Geometry::deg2rad(std::abs(angle_opt->value)), Geometry::deg2rad(5.), M_PI / 2.);
|
||||
const double sin_t = std::sin(theta);
|
||||
const double cot_t = std::cos(theta) / sin_t;
|
||||
|
||||
// NOTE: both purge_in_prime_tower and single_extruder_multi_material are
|
||||
// PRINTER options (Preset.cpp s_Preset_printer_options) — read them from the
|
||||
// printer preset. Reading purge_in_prime_tower from the print preset returns
|
||||
// has()==false, collapsing use_matrix to false and sizing the tower for the
|
||||
// small prime_volume instead of the real color-change flush. This must match
|
||||
// the backend Print::_plan_belt_purge() which reads both from the merged config.
|
||||
const bool use_matrix = (printer_config.has("purge_in_prime_tower") && printer_config.opt_bool("purge_in_prime_tower"))
|
||||
&& (printer_config.has("single_extruder_multi_material") && printer_config.opt_bool("single_extruder_multi_material"));
|
||||
const double prime_volume = print_config.has("prime_volume") ? print_config.opt_float("prime_volume") : 45.;
|
||||
const double printable_height = printer_config.has("printable_height") ? printer_config.opt_float("printable_height") : 250.;
|
||||
|
||||
// The bed (printable_area) is plate-local but model instances live in the
|
||||
// plate's world frame, so the plate origin is added to every bed coordinate.
|
||||
const double inset = 1.;
|
||||
BoundingBoxf bed_ext;
|
||||
if (const auto *bed_opt = printer_config.option<ConfigOptionPoints>("printable_area");
|
||||
bed_opt != nullptr && !bed_opt->values.empty())
|
||||
bed_ext = get_extents(bed_opt->values);
|
||||
|
||||
// What each plate needs, decided before anything is deleted or created.
|
||||
struct Plan
|
||||
{
|
||||
bool wanted = false;
|
||||
BeltPurgeSignature sig;
|
||||
int n_islands = 1;
|
||||
double w_sub = 0., length = 0., height = 0.;
|
||||
Vec3d center = Vec3d::Zero();
|
||||
};
|
||||
std::vector<Plan> plans(static_cast<size_t>(plate_count));
|
||||
|
||||
for (int plate_idx = 0; plate_idx < plate_count; ++plate_idx) {
|
||||
Plan &plan = plans[size_t(plate_idx)];
|
||||
PartPlate *plate = partplate_list.get_plate(plate_idx);
|
||||
|
||||
// Filaments used and bounding extent of the non-prism objects on this
|
||||
// plate (1-based filament ids; volume extruder 0 = object default).
|
||||
std::set<int> filaments;
|
||||
double x_min = std::numeric_limits<double>::max();
|
||||
double x_max = -std::numeric_limits<double>::max();
|
||||
double y_min = std::numeric_limits<double>::max();
|
||||
double y_max = -std::numeric_limits<double>::max();
|
||||
double z_max = 0.;
|
||||
bool have_objects = false;
|
||||
if (belt && plate != nullptr) {
|
||||
for (int obj_idx = 0; obj_idx < (int) model.objects.size(); ++obj_idx) {
|
||||
const ModelObject *mo = model.objects[obj_idx];
|
||||
if (is_prism(mo))
|
||||
continue;
|
||||
int obj_extruder = 1;
|
||||
if (const ConfigOption *opt = mo->config.option("extruder"); opt != nullptr && opt->getInt() > 0)
|
||||
obj_extruder = opt->getInt();
|
||||
bool any_instance_on_plate = false;
|
||||
for (int inst_idx = 0; inst_idx < (int) mo->instances.size(); ++inst_idx) {
|
||||
if (!plate->contain_instance_totally(obj_idx, inst_idx))
|
||||
continue;
|
||||
any_instance_on_plate = true;
|
||||
const BoundingBoxf3 bb = mo->instance_bounding_box(inst_idx);
|
||||
x_min = std::min(x_min, bb.min.x());
|
||||
x_max = std::max(x_max, bb.max.x());
|
||||
y_min = std::min(y_min, bb.min.y());
|
||||
y_max = std::max(y_max, bb.max.y());
|
||||
z_max = std::max(z_max, bb.max.z());
|
||||
}
|
||||
if (!any_instance_on_plate)
|
||||
continue;
|
||||
have_objects = true;
|
||||
for (const ModelVolume *mv : mo->volumes)
|
||||
for (int e : mv->get_extruders())
|
||||
filaments.insert(e > 0 ? e : obj_extruder);
|
||||
}
|
||||
}
|
||||
|
||||
// A mixed filament slot is VIRTUAL: it never reaches a nozzle. At slice time
|
||||
// ToolOrdering::resolve_mixed_filaments() replaces it with its physical
|
||||
// components, so the toolchanges the prism has to absorb are between those
|
||||
// components, not to the mixed slot itself. Counting the slot as a filament
|
||||
// of its own therefore over-provisions the prism by one island per mixed slot
|
||||
// -- the "extra purge tower" -- and, when every component is already used by
|
||||
// another object, by an island that can never be reached at all.
|
||||
//
|
||||
// Expand here with the same helper the backend uses (Print.cpp's sequential
|
||||
// path), so the GUI sizes the prism against the same filament set the slicer
|
||||
// will actually produce. No-op when no filament is mixed.
|
||||
if (has_any_mixed_filament(is_mixed)) {
|
||||
std::vector<unsigned int> zero_based;
|
||||
zero_based.reserve(filaments.size());
|
||||
for (int f : filaments)
|
||||
if (f > 0)
|
||||
zero_based.push_back((unsigned int) (f - 1));
|
||||
zero_based = expand_mixed_filaments(zero_based, is_mixed, comp_strs);
|
||||
filaments.clear();
|
||||
for (unsigned int f : zero_based)
|
||||
filaments.insert((int) f + 1);
|
||||
}
|
||||
|
||||
plan.wanted = belt && prime_tower_enabled && !by_object && have_objects && filaments.size() > 1;
|
||||
if (!plan.wanted)
|
||||
continue;
|
||||
|
||||
// --- Sizing -----------------------------------------------------------
|
||||
const int n_islands = std::max(1, (int) filaments.size() - 1);
|
||||
// Every disconnected island needs at least 1 mm of printable width. Honor
|
||||
// the configured total width whenever possible, but never let the island
|
||||
// layout silently grow past the footprint used for placement.
|
||||
const double min_width = n_islands + (n_islands - 1) * gap;
|
||||
const double width = std::max(min_width,
|
||||
print_config.has("belt_purge_tower_width") ? print_config.opt_float("belt_purge_tower_width") : 35.);
|
||||
const double printable_width = width - (n_islands - 1) * gap;
|
||||
|
||||
// Parts' extent along the belt-travel axis and the lateral (across-belt) axis.
|
||||
const double belt_min = belt_is_y ? y_min : x_min;
|
||||
const double belt_max = belt_is_y ? y_max : x_max;
|
||||
const double lat_min = belt_is_y ? x_min : y_min;
|
||||
const double lat_max = belt_is_y ? x_max : y_max;
|
||||
|
||||
// Worst-case purge volume of one layer: up to (filament count - 1)
|
||||
// toolchanges, each needing the worst flush matrix entry (mirrors the
|
||||
// volume selection in Print::_plan_belt_purge()).
|
||||
double max_flush = prime_volume;
|
||||
if (use_matrix) {
|
||||
const size_t extruder_nums = wxGetApp().preset_bundle->get_printer_extruder_count();
|
||||
const std::vector<double> matrix = get_flush_volumes_matrix(
|
||||
project_config.option<ConfigOptionFloats>("flush_volumes_matrix")->values, 0, extruder_nums);
|
||||
const auto * multi_opt = project_config.option<ConfigOptionFloats>("flush_multiplier");
|
||||
const double multiplier = multi_opt != nullptr && !multi_opt->values.empty() ? multi_opt->get_at(0) : 1.;
|
||||
const int n_total = (int) (std::sqrt(double(matrix.size())) + 0.5);
|
||||
double m = 0.;
|
||||
for (int i : filaments)
|
||||
for (int j : filaments)
|
||||
if (i != j && i <= n_total && j <= n_total)
|
||||
m = std::max(m, matrix[size_t(i - 1) * n_total + size_t(j - 1)]);
|
||||
if (m > 0.)
|
||||
max_flush = m * multiplier;
|
||||
}
|
||||
const double v_layer = double(filaments.size() - 1) * max_flush;
|
||||
|
||||
// Height from the per-layer purge demand. A tilted slicing plane cuts a
|
||||
// printable_width x (height/sin) rectangle out of the bars, so one layer
|
||||
// slab absorbs printable_width * (height/sin) * layer_height of purge. Solve for the height that
|
||||
// holds the worst-case per-layer purge, with eta (infill/perimeter packing)
|
||||
// and a safety margin for the tilt ramps / grid-alignment slop, plus a
|
||||
// minimum so the tower is a real printable body rather than a sliver.
|
||||
const double eta = 0.85;
|
||||
const double safety = 1.6;
|
||||
double height = safety * v_layer * sin_t / (printable_width * layer_h * eta);
|
||||
height = std::clamp(height, 8.0, std::max(8.0, printable_height));
|
||||
|
||||
// --- Idempotence (input-keyed) ----------------------------------------
|
||||
auto q = [](double v) { return std::lround(v * 10.0); }; // 0.1 mm quantization
|
||||
BeltPurgeSignature &new_sig = plan.sig;
|
||||
new_sig.valid = true;
|
||||
new_sig.filament_count = (int) filaments.size();
|
||||
new_sig.key[0] = q(width);
|
||||
new_sig.key[1] = q(layer_h);
|
||||
new_sig.key[2] = q(height);
|
||||
new_sig.key[3] = q(belt_min);
|
||||
new_sig.key[4] = q(belt_max);
|
||||
new_sig.key[5] = q(lat_min);
|
||||
new_sig.key[6] = q(z_max);
|
||||
new_sig.key[7] = static_cast<long>(rot);
|
||||
new_sig.key[8] = std::lround(theta * 10000.0);
|
||||
new_sig.key[9] = q(lat_max);
|
||||
const Vec3d plate_origin = plate->get_origin();
|
||||
new_sig.key[10] = q(plate_origin.x());
|
||||
new_sig.key[11] = q(plate_origin.y());
|
||||
if (bed_ext.defined) {
|
||||
new_sig.key[12] = q(bed_ext.max.x());
|
||||
new_sig.key[13] = q(bed_ext.max.y());
|
||||
}
|
||||
|
||||
// --- Position ----------------------------------------------------------
|
||||
// Belt-travel axis. With the mesh rotated by theta before slicing, a machine
|
||||
// point (y,z) maps to slicing-Z = y*sin(theta) + z*cos(theta). The parts
|
||||
// occupy slicing-Z in [y_min*sin, y_max*sin + z_max*cos], and the bar's
|
||||
// FULL-cross-section region (the part not in a triangular end ramp) spans
|
||||
// slicing-Z [belt_start*sin + H*cos, belt_end*sin]. Covering the parts'
|
||||
// whole band needs belt_start <= y_min - H*cot (bar's own leading ramp) and
|
||||
// belt_end >= y_max + z_max*cot (parts' top features print further up the
|
||||
// belt). The trailing z_max*cot term dominates the bar's own ramp.
|
||||
//
|
||||
// Along the belt the bar stops at the end of the plate: a longer bar cannot be
|
||||
// printed, and the cross-sections it loses there are reported by the purge
|
||||
// planner when the parts' last layers then purge more than the bar holds.
|
||||
const double margin = 5.;
|
||||
const double ramp_compensation = height / sin_t;
|
||||
const double belt_origin = plate_origin[belt_is_y ? 1 : 0];
|
||||
double belt_end = belt_max + margin + ramp_compensation + z_max * cot_t; // + parts' top-feature belt reach
|
||||
if (bed_ext.defined)
|
||||
belt_end = std::min(belt_end, belt_origin + (belt_is_y ? bed_ext.max.y() : bed_ext.max.x()) - inset);
|
||||
const double belt_start = std::max(belt_origin, std::min(belt_min - ramp_compensation, belt_end - 10.)); // leading ramp, toward belt origin
|
||||
const double length = std::max(belt_end - belt_start, 10.);
|
||||
belt_end = belt_start + length;
|
||||
const double belt_center = 0.5 * (belt_start + belt_end);
|
||||
|
||||
// Across-belt: flush against the bed's maximum edge, inset by half the bar
|
||||
// width so the bar's far edge sits on the boundary and the whole bar stays
|
||||
// on the bed. lat_min/lat_max come from instance_bounding_box (world frame).
|
||||
const double lat_origin = plate_origin[belt_is_y ? 0 : 1];
|
||||
double lat_center = lat_max + 5. + 0.5 * width; // fallback: just past the parts
|
||||
if (bed_ext.defined) {
|
||||
const double bed_lat_max = belt_is_y ? bed_ext.max.x() : bed_ext.max.y();
|
||||
lat_center = lat_origin + bed_lat_max - inset - 0.5 * width;
|
||||
}
|
||||
|
||||
plan.n_islands = n_islands;
|
||||
plan.w_sub = (width - (n_islands - 1) * gap) / n_islands;
|
||||
plan.length = length;
|
||||
plan.height = height;
|
||||
plan.center = Vec3d(belt_is_y ? lat_center : belt_center,
|
||||
belt_is_y ? belt_center : lat_center,
|
||||
0.5 * height);
|
||||
}
|
||||
|
||||
// --- Decide ---------------------------------------------------------------
|
||||
// A plate whose prism exists and matches its recorded inputs is left alone, so
|
||||
// subsequent ticks are no-ops until the parts/config actually change.
|
||||
std::vector<int> to_delete = stale_prisms;
|
||||
std::vector<int> to_create;
|
||||
for (int plate_idx = 0; plate_idx < plate_count; ++plate_idx) {
|
||||
const Plan &plan = plans[size_t(plate_idx)];
|
||||
const std::vector<int> &existing = prisms_by_plate[size_t(plate_idx)];
|
||||
BeltPurgeSignature &sig = sigs[size_t(plate_idx)];
|
||||
if (!plan.wanted) {
|
||||
sig = BeltPurgeSignature{};
|
||||
to_delete.insert(to_delete.end(), existing.begin(), existing.end());
|
||||
} else if (existing.size() == 1 && model.objects[size_t(existing.front())]->instances.size() == 1 && plan.sig == sig) {
|
||||
continue;
|
||||
} else {
|
||||
to_delete.insert(to_delete.end(), existing.begin(), existing.end());
|
||||
to_create.push_back(plate_idx);
|
||||
}
|
||||
}
|
||||
if (to_delete.empty() && to_create.empty())
|
||||
return false;
|
||||
|
||||
remove_prisms(to_delete);
|
||||
|
||||
// --- (Re)create -----------------------------------------------------------
|
||||
// Build the prism as N DISCONNECTED sub-bars side by side across the belt,
|
||||
// N = (filaments - 1) = the worst-case number of toolchanges on one layer.
|
||||
// Why: mark_wiping_extrusions overrides whole extrusion-entity COLLECTIONS,
|
||||
// and each disconnected island slices into its own infill collection. With a
|
||||
// single solid box there is one collection per layer, so the FIRST toolchange
|
||||
// on a layer grabs the entire collection (consuming all of it for one swap)
|
||||
// and any further swaps on that layer find nothing left and go unabsorbed
|
||||
// (the classic wipe tower hides this by spilling leftover into the real
|
||||
// tower; the belt prism has no fallback). One island per simultaneous swap
|
||||
// lets each swap claim its own island. Total lateral footprint stays `width`
|
||||
// (each island width/N wide, separated by a small gap), so per-layer capacity
|
||||
// per island ~= max_flush, matching the height sizing.
|
||||
// Minimal gap between sub-bars: they must stay just-separated so the slicer
|
||||
// keeps them as distinct islands (hence distinct infill collections, one per
|
||||
// simultaneous swap). Zero gap would union them into one collection and
|
||||
// reintroduce the multi-swap-per-layer absorption bug; a hair over ~2 line
|
||||
// widths also keeps gap-fill from bridging them. 1 mm is about as close as
|
||||
// they can butt up while staying individually purgeable.
|
||||
for (int plate_idx : to_create) {
|
||||
const Plan &plan = plans[size_t(plate_idx)];
|
||||
|
||||
TriangleMesh prism_mesh;
|
||||
for (int i = 0; i < plan.n_islands; ++i) {
|
||||
const double lat_off = i * (plan.w_sub + gap);
|
||||
// Box dims: lateral = w_sub, along-belt = length, vertical = height.
|
||||
TriangleMesh box = belt_is_y ? make_cube(plan.w_sub, plan.length, plan.height) // X = lateral, Y = belt
|
||||
: make_cube(plan.length, plan.w_sub, plan.height); // X = belt, Y = lateral
|
||||
box.translate(belt_is_y ? Vec3f((float) lat_off, 0.f, 0.f) : Vec3f(0.f, (float) lat_off, 0.f));
|
||||
prism_mesh.merge(box);
|
||||
}
|
||||
|
||||
ModelObject *new_object = model.add_object();
|
||||
new_object->name = _u8L("Belt Purge Tower");
|
||||
new_object->add_instance();
|
||||
ModelVolume *new_volume = new_object->add_volume(std::move(prism_mesh));
|
||||
new_volume->name = new_object->name;
|
||||
|
||||
auto &cfg = new_object->config;
|
||||
cfg.set_key_value("belt_purge_tower_object", new ConfigOptionBool(true));
|
||||
cfg.set_key_value("flush_into_objects", new ConfigOptionBool(true));
|
||||
cfg.set_key_value("extruder", new ConfigOptionInt(1));
|
||||
// Sacrificial solid prism: one wall, no shells, dense rectilinear infill —
|
||||
// every extrusion is overriddable, so the absorbed volume matches the
|
||||
// cross-section x layer-height estimate used for the height above.
|
||||
cfg.set_key_value("wall_loops", new ConfigOptionInt(1));
|
||||
cfg.set_key_value("top_shell_layers", new ConfigOptionInt(0));
|
||||
cfg.set_key_value("bottom_shell_layers", new ConfigOptionInt(0));
|
||||
cfg.set_key_value("sparse_infill_density", new ConfigOptionPercent(100));
|
||||
cfg.set_key_value("sparse_infill_pattern", new ConfigOptionEnum<InfillPattern>(ipRectilinear));
|
||||
cfg.set_key_value("enable_support", new ConfigOptionBool(false));
|
||||
cfg.set_key_value("brim_type", new ConfigOptionEnum<BrimType>(btNoBrim));
|
||||
cfg.set_key_value("seam_slope_type", new ConfigOptionEnum<SeamScarfType>(SeamScarfType::None));
|
||||
cfg.set_key_value("precise_z_height", new ConfigOptionBool(false));
|
||||
|
||||
// Position by the belt-calibration pattern: drop to the bed, then translate
|
||||
// the instance by the delta between the object's ACTUAL bbox center and the
|
||||
// target. Setting the instance offset directly is unreliable here — the
|
||||
// freshly added cube's local frame is not centered, so set_offset() lands
|
||||
// the min corner (not the center) on the target, leaving the bar centered
|
||||
// on the bed edge with half of it hanging off.
|
||||
new_object->invalidate_bounding_box();
|
||||
new_object->ensure_on_bed();
|
||||
const BoundingBoxf3 cur = new_object->bounding_box_exact();
|
||||
new_object->translate_instances(Vec3d(plan.center.x() - cur.center().x(),
|
||||
plan.center.y() - cur.center().y(),
|
||||
0.0));
|
||||
new_object->instances.front()->set_assemble_transformation(new_object->instances.front()->get_transformation());
|
||||
|
||||
const size_t obj_idx = model.objects.size() - 1;
|
||||
// Registers the object in the sidebar and notifies the part plates;
|
||||
// selection is left untouched (auto-managed object).
|
||||
obj_list->add_object_to_list(obj_idx, /*call_selection_changed=*/false);
|
||||
|
||||
sigs[size_t(plate_idx)] = plan.sig;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace GUI
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
// ORCA-Belt: auto-managed purge prism for belt printers.
|
||||
//
|
||||
// Kept in its own translation unit (not buried in Plater.cpp) so it stays out
|
||||
// of the way of unrelated upstream changes and carries no regression risk for
|
||||
// normal printers: nothing here runs unless the printer is a belt printer with
|
||||
// the belt purge tower enabled. See Slic3r::GUI::ensure_belt_purge_tower().
|
||||
|
||||
namespace Slic3r {
|
||||
class Model;
|
||||
class PartPlateList;
|
||||
namespace GUI {
|
||||
class ObjectList;
|
||||
|
||||
// Inputs of the last belt purge prism generation. Idempotence is keyed on these
|
||||
// (not the prism's resulting bbox): the prism gets nudged by plate assignment
|
||||
// after creation, so comparing its bbox would falsely detect a change and
|
||||
// regenerate it on every background-process tick — which would re-invalidate a
|
||||
// freshly sliced result and make the G-code preview unreachable.
|
||||
struct BeltPurgeSignature
|
||||
{
|
||||
bool valid = false;
|
||||
int filament_count = 0;
|
||||
long key[14] = {0}; // rounded geometry, plate and bed inputs (0.1 mm units)
|
||||
bool operator==(const BeltPurgeSignature &o) const
|
||||
{
|
||||
if (valid != o.valid || filament_count != o.filament_count)
|
||||
return false;
|
||||
for (int i = 0; i < 14; ++i)
|
||||
if (key[i] != o.key[i])
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
// Keep the auto-generated belt purge prisms, one per plate, in sync with the
|
||||
// current config and plate contents. Creates / updates / removes the marked prism
|
||||
// ModelObjects; `sigs` holds the last inputs per plate index.
|
||||
// Returns true when the model was mutated (caller should refresh the scene).
|
||||
// Runs on every background-process update, so it is idempotent: it only mutates
|
||||
// the model when the desired prism differs from the cached signature in `sig`.
|
||||
bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, ObjectList *obj_list, std::vector<BeltPurgeSignature> &sigs);
|
||||
|
||||
} // namespace GUI
|
||||
} // namespace Slic3r
|
||||
@@ -21,7 +21,6 @@
|
||||
#include "slic3r/GUI/PresetComboBoxes.hpp"
|
||||
#include "slic3r/GUI/wxExtensions.hpp"
|
||||
#include <wx/anybutton.h>
|
||||
#include "slic3r/GUI/Widgets/HyperLink.hpp"
|
||||
#include <wx/utils.h>
|
||||
#include <wx/arrstr.h>
|
||||
#include "slic3r/GUI/Widgets/PopupWindow.hpp"
|
||||
@@ -494,7 +493,10 @@ void CaliPageCaption::init_bitmaps() {
|
||||
void CaliPageCaption::create_wiki(wxWindow* parent)
|
||||
{
|
||||
// ORCA standardized HyperLink
|
||||
m_wiki_text = new HyperLink(parent, _L("Wiki Guide"));
|
||||
m_wiki_text = new Button(parent, "", "toolbar_wiki", 0, 15);
|
||||
m_wiki_text->SetToolTip(_L("Wiki Guide"));
|
||||
m_wiki_text->SetStyle(ButtonStyle::Confirm, ButtonType::Circle);
|
||||
m_wiki_text->SetCanFocus(false);
|
||||
m_wiki_text->Bind(wxEVT_LEFT_UP, [this](wxMouseEvent& e) {
|
||||
if (!m_wiki_url.empty())
|
||||
wxLaunchDefaultBrowser(m_wiki_url);
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
#include "Widgets/TextInput.hpp"
|
||||
#include "Widgets/AMSControl.hpp"
|
||||
#include "Widgets/ProgressBar.hpp"
|
||||
#include "Widgets/HyperLink.hpp"
|
||||
#include "wxExtensions.hpp"
|
||||
#include "PresetComboBoxes.hpp"
|
||||
|
||||
@@ -155,7 +154,7 @@ private:
|
||||
void init_bitmaps();
|
||||
void create_wiki(wxWindow* parent);
|
||||
|
||||
HyperLink* m_wiki_text; // ORCA
|
||||
Button* m_wiki_text; // ORCA
|
||||
wxString m_wiki_url;
|
||||
ScalableBitmap m_prev_bmp_normal;
|
||||
ScalableBitmap m_prev_bmp_hover;
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include "slic3r/GUI/GUI.hpp"
|
||||
#include "libslic3r/libslic3r.h"
|
||||
#include "slic3r/GUI/Field.hpp"
|
||||
#include "libslic3r/BeltBrim.hpp"
|
||||
#include <cstdlib>
|
||||
#include <set>
|
||||
#include <map>
|
||||
@@ -351,6 +352,31 @@ void ConfigManipulation::update_print_fff_config(DynamicPrintConfig* config, con
|
||||
|
||||
bool is_object_config = (!is_global_config && !is_plate_config);
|
||||
|
||||
// Belt printer: a raft and a draft shield are refused by Print::validate(), and
|
||||
// the fields that would clear them are greyed out in belt mode, so a preset that
|
||||
// carries either could not be sliced at all. Reset them instead of only disabling
|
||||
// the fields.
|
||||
if (GUI::wxGetApp().preset_bundle != nullptr) {
|
||||
const auto *belt_opt = GUI::wxGetApp().preset_bundle->printers.get_edited_preset().config.option<ConfigOptionBool>("belt_printer");
|
||||
const auto *raft_opt = config->option<ConfigOptionInt>("raft_layers");
|
||||
const auto *shield_opt = config->option<ConfigOptionEnum<DraftShield>>("draft_shield");
|
||||
const bool has_raft = raft_opt != nullptr && raft_opt->value > 0;
|
||||
const bool has_shield = shield_opt != nullptr && shield_opt->value != dsDisabled;
|
||||
if (belt_opt != nullptr && belt_opt->value && (has_raft || has_shield)) {
|
||||
const wxString msg_text = _(L("Raft and draft shield are not available on belt printers.\nThey have been disabled."));
|
||||
MessageDialog dialog(m_msg_dlg_parent, msg_text, "", wxICON_WARNING | wxOK);
|
||||
DynamicPrintConfig new_conf = *config;
|
||||
is_msg_dlg_already_exist = true;
|
||||
dialog.ShowModal();
|
||||
if (has_raft)
|
||||
new_conf.set_key_value("raft_layers", new ConfigOptionInt(0));
|
||||
if (has_shield)
|
||||
new_conf.set_key_value("draft_shield", new ConfigOptionEnum<DraftShield>(dsDisabled));
|
||||
apply(config, &new_conf);
|
||||
is_msg_dlg_already_exist = false;
|
||||
}
|
||||
}
|
||||
|
||||
// layer_height shouldn't be equal to zero
|
||||
auto layer_height = config->opt_float("layer_height");
|
||||
if (layer_height < EPSILON)
|
||||
@@ -754,9 +780,31 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in
|
||||
const bool gcf_is_marlin_firmware = gcflavor == GCodeFlavor::gcfMarlinFirmware;
|
||||
const bool gcf_is_klipper = gcflavor == GCodeFlavor::gcfKlipper;
|
||||
|
||||
// Belt printer: detect early since it affects multiple toggle decisions below.
|
||||
// `is_belt_tilted` is the stricter test that mirrors PrintObject::has_belt_brim():
|
||||
// only a tilted belt gets the belt-plane brim, while a belt printer with no
|
||||
// rotation is geometrically a flat bed and keeps the ordinary plate brim.
|
||||
bool is_belt_printer = false;
|
||||
bool is_belt_tilted = false;
|
||||
{
|
||||
const auto &printer_cfg = preset_bundle->printers.get_edited_preset().config;
|
||||
const auto *belt_opt = printer_cfg.option<ConfigOptionBool>("belt_printer");
|
||||
if (belt_opt)
|
||||
is_belt_printer = belt_opt->value;
|
||||
const auto *axis = printer_cfg.option<ConfigOptionEnum<BeltRotationAxis>>("belt_slice_rotation");
|
||||
const auto *angle = printer_cfg.option<ConfigOptionFloat>("belt_slice_rotation_angle");
|
||||
if (is_belt_printer && axis != nullptr && angle != nullptr) {
|
||||
// Same window as Print::has_tilted_belt(); shared constants so the GUI and
|
||||
// the backend cannot drift apart.
|
||||
const double tilt = std::abs(angle->value);
|
||||
is_belt_tilted = (axis->value == BeltRotationAxis::X || axis->value == BeltRotationAxis::Y)
|
||||
&& tilt >= BELT_BRIM_MIN_TILT_DEG && tilt <= BELT_BRIM_MAX_TILT_DEG;
|
||||
}
|
||||
}
|
||||
|
||||
bool have_volumetric_extrusion_rate_slope = config->option<ConfigOptionFloat>("max_volumetric_extrusion_rate_slope")->value > 0;
|
||||
float have_volumetric_extrusion_rate_slope_segment_length = config->option<ConfigOptionFloat>("max_volumetric_extrusion_rate_slope_segment_length")->value;
|
||||
toggle_field("enable_arc_fitting", !have_volumetric_extrusion_rate_slope);
|
||||
toggle_field("enable_arc_fitting", !have_volumetric_extrusion_rate_slope && !is_belt_printer);
|
||||
toggle_line("max_volumetric_extrusion_rate_slope_segment_length", have_volumetric_extrusion_rate_slope);
|
||||
toggle_line("extrusion_rate_smoothing_external_perimeter_only", have_volumetric_extrusion_rate_slope);
|
||||
if(have_volumetric_extrusion_rate_slope) config->set_key_value("enable_arc_fitting", new ConfigOptionBool(false));
|
||||
@@ -923,27 +971,47 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in
|
||||
}
|
||||
}
|
||||
|
||||
bool have_skirt = config->opt_int("skirt_loops") > 0;
|
||||
// Belt printer: disable skirt, brim, raft, and draft shield controls.
|
||||
bool have_skirt = config->opt_int("skirt_loops") > 0 && !is_belt_printer;
|
||||
toggle_field("skirt_height", have_skirt && config->opt_enum<DraftShield>("draft_shield") != dsEnabled);
|
||||
toggle_line("single_loop_draft_shield", have_skirt); // ORCA: Display one wall if skirt enabled
|
||||
for (auto el : {"skirt_type", "min_skirt_length", "skirt_distance", "skirt_start_angle", "skirt_speed", "draft_shield"})
|
||||
toggle_field(el, have_skirt);
|
||||
if (is_belt_printer) {
|
||||
toggle_field("skirt_loops", false);
|
||||
toggle_field("skirt_height", false);
|
||||
}
|
||||
|
||||
bool have_brim = (config->opt_enum<BrimType>("brim_type") != btNoBrim);
|
||||
// Belt printers now get a brim too, laid onto the tilted belt by BeltBrim.cpp,
|
||||
// so brim type / width / object gap all apply. A belt printer with no tilt is
|
||||
// geometrically a flat bed and uses the ordinary plate brim, hence the separate
|
||||
// is_belt_tilted test.
|
||||
bool have_brim = config->opt_enum<BrimType>("brim_type") != btNoBrim;
|
||||
toggle_field("brim_object_gap", have_brim);
|
||||
toggle_field("brim_use_efc_outline", have_brim);
|
||||
toggle_field("combine_brims", have_brim);
|
||||
bool have_brim_width = (config->opt_enum<BrimType>("brim_type") != btNoBrim) && config->opt_enum<BrimType>("brim_type") != btAutoBrim &&
|
||||
// Both are first-layer-only concepts that the belt path cannot honour.
|
||||
toggle_field("brim_use_efc_outline", have_brim && !is_belt_tilted);
|
||||
toggle_field("combine_brims", have_brim && !is_belt_tilted);
|
||||
bool have_brim_width = have_brim && config->opt_enum<BrimType>("brim_type") != btAutoBrim &&
|
||||
config->opt_enum<BrimType>("brim_type") != btPainted;
|
||||
toggle_field("brim_width", have_brim_width);
|
||||
// On a tilted belt Auto / Mouse ear / Painted all collapse to outer-only at the
|
||||
// configured width, so the width field has to stay live for them too.
|
||||
toggle_field("brim_width", have_brim_width || (have_brim && is_belt_tilted));
|
||||
toggle_field("brim_flow_ratio", have_brim);
|
||||
// Paired toggle_line + toggle_field: cb_toggle_line is null in the per-object
|
||||
// override panel, so the row cannot be hidden there and greying out is the
|
||||
// fallback. Both extras are belt-only: one extends the brim ahead along the belt,
|
||||
// the other widens it across the belt.
|
||||
for (auto el : { "leading_brim_length", "extra_brim_width" }) {
|
||||
toggle_line(el, is_belt_tilted);
|
||||
toggle_field(el, is_belt_tilted && have_brim);
|
||||
}
|
||||
// Wall filament selectors use the same logic as in Print::extruders().
|
||||
toggle_field("outer_wall_filament_id", have_perimeters || have_brim);
|
||||
toggle_field("inner_wall_filament_id", have_perimeters || have_brim);
|
||||
|
||||
const BrimType brim_type = config->opt_enum<BrimType>("brim_type");
|
||||
const bool have_auto_brim_ear = brim_type == btEar;
|
||||
const bool have_painted_brim_ear = brim_type == btPainted;
|
||||
const bool have_auto_brim_ear = brim_type == btEar && !is_belt_tilted;
|
||||
const bool have_painted_brim_ear = brim_type == btPainted && !is_belt_tilted;
|
||||
set_option_label("brim_width", have_auto_brim_ear ? _L("Brim ear radius") : _L("Brim width"));
|
||||
const auto brim_width = config->opt_float("brim_width");
|
||||
// Automatic brim ear settings require a non-zero brim width.
|
||||
@@ -958,7 +1026,9 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in
|
||||
// Hide Elephant foot compensation layers if elefant_foot_compensation is not enabled
|
||||
toggle_line("elefant_foot_compensation_layers", config->opt_float("elefant_foot_compensation") > 0 || config->option<ConfigOptionPercent>("elefant_foot_layers_density")->get_abs_value(1.0f) < 1.0f);
|
||||
|
||||
bool have_raft = config->opt_int("raft_layers") > 0;
|
||||
bool have_raft = config->opt_int("raft_layers") > 0 && !is_belt_printer;
|
||||
if (is_belt_printer)
|
||||
toggle_field("raft_layers", false);
|
||||
bool have_support_material = config->opt_bool("enable_support") || have_raft;
|
||||
|
||||
SupportType support_type = config->opt_enum<SupportType>("support_type");
|
||||
@@ -1066,33 +1136,41 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in
|
||||
toggle_line("preheat_steps", have_ooze_prevention && (preheat_steps > 0));
|
||||
|
||||
bool have_prime_tower = config->opt_bool("enable_prime_tower");
|
||||
// ORCA-Belt: belt printers replace the classic wipe tower with the
|
||||
// auto-generated belt purge prism, enabled by the belt-only printer option
|
||||
// enable_belt_purge_tower. Its width (a process option) is only relevant
|
||||
// then. (is_belt_printer is computed at the top of this function.)
|
||||
const bool have_belt_purge_tower = is_belt_printer
|
||||
&& preset_bundle->printers.get_edited_preset().config.has("enable_belt_purge_tower")
|
||||
&& preset_bundle->printers.get_edited_preset().config.opt_bool("enable_belt_purge_tower");
|
||||
toggle_line("belt_purge_tower_width", have_belt_purge_tower);
|
||||
for (auto el : {"prime_tower_width", "prime_tower_brim_width", "prime_tower_skip_points", "wipe_tower_wall_type", "prime_tower_infill_gap","prime_tower_enable_framework", "enable_tower_interface_features"})
|
||||
toggle_line(el, have_prime_tower);
|
||||
toggle_line(el, have_prime_tower && !is_belt_printer);
|
||||
|
||||
toggle_line("enable_tower_interface_cooldown_during_tower",
|
||||
have_prime_tower && config->opt_bool("enable_tower_interface_features"));
|
||||
have_prime_tower && !is_belt_printer && config->opt_bool("enable_tower_interface_features"));
|
||||
|
||||
bool purge_in_primetower = preset_bundle->printers.get_edited_preset().config.opt_bool("purge_in_prime_tower");
|
||||
|
||||
for (auto el : {"wipe_tower_rotation_angle", "wipe_tower_cone_angle",
|
||||
"wipe_tower_extra_spacing", "wipe_tower_max_purge_speed",
|
||||
"wipe_tower_bridging", "wipe_tower_extra_flow"})
|
||||
toggle_line(el, have_prime_tower && supports_wipe_tower_2);
|
||||
toggle_line(el, have_prime_tower && supports_wipe_tower_2 && !is_belt_printer);
|
||||
|
||||
// Orca: both tower generators skip sparse layers, so this is not a wipe tower 2 exclusive.
|
||||
toggle_line("wipe_tower_no_sparse_layers", have_prime_tower);
|
||||
toggle_line("wipe_tower_no_sparse_layers", have_prime_tower && !is_belt_printer);
|
||||
// Dropping the sparse layers outright leaves nothing to combine, so the two are exclusive.
|
||||
toggle_line("wipe_tower_sparse_layers_combination", have_prime_tower && !config->opt_bool("wipe_tower_no_sparse_layers"));
|
||||
toggle_line("wipe_tower_sparse_layers_combination", have_prime_tower && !is_belt_printer && !config->opt_bool("wipe_tower_no_sparse_layers"));
|
||||
|
||||
WipeTowerWallType wipe_tower_wall_type = config->opt_enum<WipeTowerWallType>("wipe_tower_wall_type");
|
||||
bool have_rib_wall = (wipe_tower_wall_type == WipeTowerWallType::wtwRib)&&have_prime_tower;
|
||||
toggle_line("wipe_tower_cone_angle", have_prime_tower && supports_wipe_tower_2 && wipe_tower_wall_type == WipeTowerWallType::wtwCone);
|
||||
bool have_rib_wall = (wipe_tower_wall_type == WipeTowerWallType::wtwRib)&&have_prime_tower&&!is_belt_printer;
|
||||
toggle_line("wipe_tower_cone_angle", have_prime_tower && supports_wipe_tower_2 && !is_belt_printer && wipe_tower_wall_type == WipeTowerWallType::wtwCone);
|
||||
toggle_line("wipe_tower_extra_rib_length", have_rib_wall);
|
||||
toggle_line("wipe_tower_rib_width", have_rib_wall);
|
||||
toggle_line("wipe_tower_fillet_wall", have_rib_wall);
|
||||
toggle_field("prime_tower_width", have_prime_tower && !have_rib_wall);
|
||||
toggle_field("prime_tower_width", have_prime_tower && !have_rib_wall && !is_belt_printer);
|
||||
|
||||
toggle_line("single_extruder_multi_material_priming", !bSEMM && have_prime_tower && supports_wipe_tower_2);
|
||||
toggle_line("single_extruder_multi_material_priming", !bSEMM && have_prime_tower && supports_wipe_tower_2 && !is_belt_printer);
|
||||
|
||||
bool use_cyclic_ordering = config->opt_enum<ToolChangeOrderingType>("toolchange_ordering") == ToolChangeOrderingType::Cyclic;
|
||||
toggle_line("toolchange_cyclic_order", use_cyclic_ordering);
|
||||
@@ -1188,8 +1266,10 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in
|
||||
toggle_line("small_area_infill_flow_compensation_model", have_small_area_infill_flow_compensation);
|
||||
|
||||
|
||||
toggle_field("seam_slope_type", !has_spiral_vase);
|
||||
bool has_seam_slope = !has_spiral_vase && config->opt_enum<SeamScarfType>("seam_slope_type") != SeamScarfType::None;
|
||||
// Belt printers: the scarf would start one layer back along the belt, inside the
|
||||
// previous layer (GCode::extrude_loop skips it there too).
|
||||
toggle_field("seam_slope_type", !has_spiral_vase && !is_belt_printer);
|
||||
bool has_seam_slope = !has_spiral_vase && !is_belt_printer && config->opt_enum<SeamScarfType>("seam_slope_type") != SeamScarfType::None;
|
||||
toggle_line("seam_slope_conditional", has_seam_slope);
|
||||
toggle_line("seam_slope_start_height", has_seam_slope);
|
||||
toggle_line("seam_slope_entire_loop", has_seam_slope);
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
#include "wx/dcgraph.h"
|
||||
#include "I18N.hpp"
|
||||
#include "PartPlate.hpp"
|
||||
#include "Widgets/HyperLink.hpp"
|
||||
#include <wx/colour.h>
|
||||
#include "libslic3r/Config.hpp"
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
@@ -27,6 +26,7 @@
|
||||
#include "slic3r/GUI/Widgets/StaticBox.hpp"
|
||||
#include <utility>
|
||||
#include "slic3r/GUI/Widgets/SwitchButton.hpp"
|
||||
#include "slic3r/GUI/Widgets/Button.hpp"
|
||||
#include <wx/tglbtn.h>
|
||||
|
||||
namespace Slic3r { namespace GUI {
|
||||
@@ -203,18 +203,24 @@ FilamentGroupPopup::FilamentGroupPopup(wxWindow *parent) : PopupWindow(parent, w
|
||||
{
|
||||
wxBoxSizer *button_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
// ORCA Unified hyperlinks
|
||||
video_link = new HyperLink(this, _L("Video tutorial"));
|
||||
video_link->Bind(wxEVT_LEFT_DOWN, [](wxMouseEvent& e)
|
||||
{
|
||||
play_dual_extruder_slice_video();
|
||||
wxGetApp().app_config->set("play_slicing_video", "false");
|
||||
});
|
||||
button_sizer->Add(video_link, 0, wxLEFT, horizontal_margin + FromDIP(3));
|
||||
button_sizer->AddStretchSpacer();
|
||||
|
||||
wiki_link = new HyperLink(this, _L("Wiki Guide"));
|
||||
wiki_link->Bind(wxEVT_LEFT_DOWN, [](wxMouseEvent&) { open_filament_group_wiki(); });
|
||||
button_sizer->Add(wiki_link, 0, wxLEFT, horizontal_margin);
|
||||
auto wiki_btn = new Button(this, "", "toolbar_wiki", 0, 15);
|
||||
wiki_btn->SetToolTip(_L("Wiki Guide") + "\n" + "https://e.bambulab.com/t?c=mOkvsXkJ9pldGYp9");
|
||||
wiki_btn->SetStyle(ButtonStyle::Confirm, ::ButtonType::Circle);
|
||||
wiki_btn->SetCanFocus(false);
|
||||
wiki_btn->Bind(wxEVT_LEFT_DOWN, ([](auto& e) {open_filament_group_wiki();}));
|
||||
|
||||
auto video_btn = new Button(this, "", "toolbar_video_guide", 0, 15);
|
||||
video_btn->SetToolTip(_L("Video Guide"));
|
||||
video_btn->SetStyle(ButtonStyle::Confirm, ::ButtonType::Circle);
|
||||
video_btn->SetCanFocus(false);
|
||||
video_btn->Bind(wxEVT_LEFT_DOWN, ([](auto& e) {
|
||||
play_dual_extruder_slice_video();
|
||||
wxGetApp().app_config->set("play_slicing_video", "false");
|
||||
}));
|
||||
|
||||
button_sizer->Add(wiki_btn , 0, wxLEFT, horizontal_margin + FromDIP(3));
|
||||
button_sizer->Add(video_btn, 0, wxLEFT, FromDIP(10));
|
||||
|
||||
top_sizer->Add(button_sizer, 0, wxEXPAND | wxLEFT | wxRIGHT, horizontal_margin);
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ public:
|
||||
FilamentGroupPopup(wxWindow *parent);
|
||||
void tryPopup(Plater* plater,PartPlate* plate, bool slice_all);
|
||||
void tryClose();
|
||||
void Dismiss() override;
|
||||
|
||||
FilamentMapMode GetSelectedMode() const { return m_mode; }
|
||||
private:
|
||||
@@ -40,7 +41,6 @@ private:
|
||||
void OnLeaveWindow(wxMouseEvent &);
|
||||
void OnEnterWindow(wxMouseEvent &);
|
||||
void OnTimer(wxTimerEvent &event);
|
||||
void Dismiss();
|
||||
|
||||
void CreateBmps();
|
||||
|
||||
@@ -80,10 +80,6 @@ private:
|
||||
wxBitmap unchecked_hover_bmp;
|
||||
wxBitmap global_tag_bmp;
|
||||
|
||||
|
||||
wxStaticText *wiki_link;
|
||||
wxStaticText *video_link;
|
||||
|
||||
StaticBox *m_smart_filament_panel{nullptr};
|
||||
wxSizerItem *m_smart_filament_spacer{nullptr};
|
||||
SwitchButton *m_smart_filament_switch{nullptr};
|
||||
|
||||
+224
-37
@@ -14,6 +14,7 @@
|
||||
//BBS: add convex hull logic for toolpath check
|
||||
|
||||
#include "GUI_App.hpp"
|
||||
#include "Shortcuts.hpp"
|
||||
#include "Plater.hpp"
|
||||
#include "Camera.hpp"
|
||||
#include "I18N.hpp"
|
||||
@@ -22,6 +23,8 @@
|
||||
#include "GLCanvas3D.hpp"
|
||||
#include "FilamentGroupPopup.hpp"
|
||||
#include "GLToolbar.hpp"
|
||||
#include "libslic3r/BeltTransform.hpp"
|
||||
#include "libslic3r/GCode/MachineFrameTransform.hpp"
|
||||
#include "MsgDialog.hpp"
|
||||
#include <boost/container_hash/hash.hpp>
|
||||
#include "slic3r/GUI/MeshUtils.hpp"
|
||||
@@ -36,6 +39,7 @@
|
||||
#include "slic3r/GUI/ImGuiWrapper.hpp"
|
||||
#include "slic3r/GUI/GLModel.hpp"
|
||||
#include "slic3r/GUI/GLShader.hpp"
|
||||
#include "slic3r/GUI/Gizmos/GLGizmoUtils.hpp"
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include <math.h>
|
||||
#include "libvgcode/include/Viewer.hpp"
|
||||
@@ -88,9 +92,11 @@
|
||||
|
||||
#include <array>
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
#include <sstream>
|
||||
#include <cmath>
|
||||
#include <chrono>
|
||||
#include <Eigen/Geometry>
|
||||
#include "libslic3r/AppConfig.hpp"
|
||||
#include "libslic3r/BoundingBox.hpp"
|
||||
#include "libslic3r/CutUtils.hpp"
|
||||
@@ -1323,6 +1329,52 @@ std::vector<int> GCodeViewer::get_plater_extruder()
|
||||
return m_plater_extruder;
|
||||
}
|
||||
|
||||
// Belt printers: compute the full machine->model back-transform from the print
|
||||
// config, so the "designed" (upright) G-code preview maps each toolpath vertex
|
||||
// back to Cartesian space. The G-code forward pipeline is (BeltKinematics::
|
||||
// to_machine_coords): gcode = MachineFrame( AxisRemap( X ) ), with X the model
|
||||
// already un-rotated to Cartesian by the back-transform. So the inverse is:
|
||||
// model = AxisRemap^-1 . MachineFrame^-1
|
||||
// (origin-snap is a per-instance translation that only shifts position, not
|
||||
// orientation, so it is intentionally omitted.)
|
||||
static Transform3d compute_belt_back_transform(const PrintConfig& cfg)
|
||||
{
|
||||
if (!cfg.belt_printer.value)
|
||||
return Transform3d::Identity();
|
||||
|
||||
MachineFrameTransform mft;
|
||||
mft.init_from_config(cfg);
|
||||
const Transform3d mf_inv = mft.is_active() ? Transform3d(mft.transform().inverse())
|
||||
: Transform3d::Identity();
|
||||
|
||||
// Forward G-code axis remap as an affine matrix (out_i = sign * in[r_i % 3], plus a
|
||||
// build-volume offset for Rev axes). This is the matrix form of the per-point
|
||||
// GCodeWriter::apply_axis_remap (row convention: each OUTPUT axis selects an input
|
||||
// axis + sign) and MUST stay in sync with it. The build-volume max matches what the
|
||||
// writer is fed in GCode.cpp (printable_area max + printable_height). (Follow-up:
|
||||
// precompute this matrix once in GCodeWriter and share it with apply_axis_remap to
|
||||
// remove the parallel encoding.)
|
||||
Transform3d ar = Transform3d::Identity();
|
||||
const int rr[3] = { int(cfg.gcode_remap_x.value), int(cfg.gcode_remap_y.value), int(cfg.gcode_remap_z.value) };
|
||||
if (rr[0] != 0 || rr[1] != 1 || rr[2] != 2) {
|
||||
BoundingBoxf bbox_bed(cfg.printable_area.values);
|
||||
const Vec3d vmax(bbox_bed.max.x(), bbox_bed.max.y(), cfg.printable_height.value);
|
||||
Matrix3d M = Matrix3d::Zero();
|
||||
Vec3d t = Vec3d::Zero();
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
const int axis = rr[i] % 3;
|
||||
if (rr[i] < 3) M(i, axis) = 1.0;
|
||||
else if (rr[i] < 6) M(i, axis) = -1.0;
|
||||
else { M(i, axis) = -1.0; t[i] = vmax[axis]; }
|
||||
}
|
||||
ar.linear() = M;
|
||||
ar.translation() = t;
|
||||
}
|
||||
const Transform3d ar_inv = ar.inverse();
|
||||
|
||||
return ar_inv * mf_inv;
|
||||
}
|
||||
|
||||
//BBS: always load shell at preview
|
||||
void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const Print& print, const std::vector<std::string>& str_tool_colors,
|
||||
const std::vector<std::string>& str_color_print_colors, const BuildVolume& build_volume,
|
||||
@@ -1330,6 +1382,15 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const
|
||||
{
|
||||
m_loaded_as_preview = false;
|
||||
|
||||
// Belt printers: drive the designed/raw view UI (legend checkbox, hotkey B, canvas-toolbar
|
||||
// menu item) from the G-code itself. Only BeltGCode writes the belt header, so its tilt
|
||||
// (gcode_result.belt_tilt_angle, abs of the slicing rotation) says whether this is belt
|
||||
// G-code; the selected printer does not, for a file opened from disk. The back-transform
|
||||
// below still reads print.config(), which Plater::load_gcode() fills from the file's own
|
||||
// config block (GCodeProcessor::export_config_for_render).
|
||||
m_belt_view_enabled = gcode_result.belt_tilt_angle > 0.f;
|
||||
m_belt_angle_deg = gcode_result.belt_tilt_angle;
|
||||
|
||||
const bool current_top_layer_only = m_viewer.is_top_layer_only_view_range();
|
||||
const bool required_top_layer_only = get_app_config()->get_bool("seq_top_layer_only");
|
||||
if (current_top_layer_only != required_top_layer_only)
|
||||
@@ -1339,8 +1400,12 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const
|
||||
m_viewer.set_dim_previous_layers(get_app_config()->get_bool("preview_dim_previous_layers"));
|
||||
m_viewer.set_dim_previous_layers_brightness(0.01f * std::stoi(get_app_config()->get("preview_dim_previous_layers_brightness")));
|
||||
|
||||
// avoid processing if called with the same gcode_result
|
||||
if (m_last_result_id == gcode_result.id && wxGetApp().is_editor()) {
|
||||
// avoid processing if called with the same gcode_result.
|
||||
// On a belt printer the toolpath geometry fed to libvgcode also depends on the
|
||||
// designed/raw view state (the back-transform is applied in convert), so the
|
||||
// same result is converted again only when that view has been toggled.
|
||||
const bool same_belt_view = !m_belt_view_enabled || m_last_belt_show_designed == m_belt_show_designed;
|
||||
if (m_last_result_id == gcode_result.id && wxGetApp().is_editor() && same_belt_view) {
|
||||
//BBS: add logs
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": the same id %1%, return directly, result %2% ") % m_last_result_id % (&gcode_result);
|
||||
|
||||
@@ -1381,8 +1446,112 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const
|
||||
return;
|
||||
}
|
||||
|
||||
// convert data from PrusaSlicer format to libvgcode format
|
||||
libvgcode::GCodeInputData data = libvgcode::convert(gcode_result, str_tool_colors, str_color_print_colors, m_viewer);
|
||||
// convert data from PrusaSlicer format to libvgcode format.
|
||||
// Belt printers: when the designed (upright) view is active, back-transform
|
||||
// the toolpath geometry into model/Cartesian space using the general belt
|
||||
// inverse (handles the mesh rotation, shear and axis remap). When off, the
|
||||
// raw machine-frame G-code is shown (useful for checking the transform itself).
|
||||
const bool is_belt = m_belt_view_enabled && print.config().belt_printer.value;
|
||||
Transform3d belt_inv = (is_belt && m_belt_show_designed)
|
||||
? compute_belt_back_transform(print.config()) : Transform3d::Identity();
|
||||
// Belt: move positions are stored as gcode_Z + belt_z_origin (the start G-code's
|
||||
// purge-blob advance baked into the machine-Z origin by its G92 Z0 resets). Subtract
|
||||
// that constant before the linear back-transform so every toolpath maps to the model's
|
||||
// belt coordinate. Without it the back-transform mixes the offset with the gantry-Y
|
||||
// term, leaving a per-move designed-Y error that min-corner anchoring cannot remove
|
||||
// when a bridge/keel move happens to cancel it at the bbox minimum.
|
||||
if (is_belt && m_belt_show_designed && gcode_result.belt_z_origin != 0.0f)
|
||||
belt_inv = belt_inv * Transform3d(Eigen::Translation3d(Vec3d(0.0, 0.0, -double(gcode_result.belt_z_origin))));
|
||||
const bool apply_belt = is_belt && m_belt_show_designed
|
||||
&& !belt_inv.matrix().isApprox(Transform3d::Identity().matrix());
|
||||
if (apply_belt) {
|
||||
// The linear belt back-transform recovers the print's shape and orientation but not
|
||||
// the per-object placement/lift translation: the object's position on the belt, the
|
||||
// BeltSliceStrategy min-Z lift, and the centering pre-translate are applied OUTSIDE
|
||||
// build_forward_transform() (see PrintObjectSlice.cpp), so its linear inverse leaves
|
||||
// them un-undone. Uncorrected, the upright toolpaths float a fixed offset from the
|
||||
// model shell. Recover the translation generally — independent of the offset's exact
|
||||
// source or the axis remap — by anchoring the back-transformed object body onto the
|
||||
// upright model bounding box (the same space the shells render in).
|
||||
BoundingBoxf3 model_bb;
|
||||
for (const PrintObject* po : print.objects())
|
||||
for (const ModelInstance* mi : po->model_object()->instances)
|
||||
model_bb.merge(po->model_object()->instance_bounding_box(*mi));
|
||||
// Build the anchor bbox from surface toolpaths only. After the belt_z_origin
|
||||
// correction the surface back-transforms onto the model, but a few elevated
|
||||
// features (bridges/overhangs over the chevron gap) are mis-mapped by the linear
|
||||
// inverse to well outside the model body; if one becomes the bbox minimum it
|
||||
// drags the min-corner anchor by ~20mm. Drop moves that land clearly outside the
|
||||
// (correct) model bbox — a geometric filter, not a role guess.
|
||||
// Tolerance around the model bbox for outlier rejection, and the minimum fraction
|
||||
// of MOVES (by count) the clip must retain before we trust it over the full bbox.
|
||||
// Count, not bbox extent: a single far-flung outlier move (e.g. a belt-entry purge
|
||||
// that maps to y~0) inflates the full bbox so much that a size-fraction test would
|
||||
// wrongly reject the clip and re-include the outlier, dragging the min-corner anchor
|
||||
// by ~the offset. By count the clip keeps ~100% of moves here and ~0% only when the
|
||||
// object is genuinely offset from the belt entry (the real fall-back-to-full case).
|
||||
constexpr double ANCHOR_CLIP_MARGIN_MM = 10.0;
|
||||
constexpr double ANCHOR_CLIP_MIN_KEEP = 0.5;
|
||||
BoundingBoxf3 tp_bb_clip, tp_bb_full;
|
||||
size_t n_filtered = 0, n_clip = 0;
|
||||
const double y_lo = model_bb.defined ? model_bb.min.y() - ANCHOR_CLIP_MARGIN_MM : -1e30;
|
||||
const double y_hi = model_bb.defined ? model_bb.max.y() + ANCHOR_CLIP_MARGIN_MM : 1e30;
|
||||
// The anchor aligns the toolpath body onto the object-only model bbox, so it must be
|
||||
// built from OBJECT-body toolpaths only. Support, skirt, brim and wipe-tower extrusions
|
||||
// are not part of the model mesh and extend past it (support especially reaches well
|
||||
// beyond the object on a belt), so including them drags tp_bb.min and shifts the whole
|
||||
// back-transformed g-code off the mesh — but only when those features are present. That
|
||||
// is the "g-code shifts vs mesh as soon as supports are enabled" bug. Filter them out.
|
||||
auto is_object_body = [](ExtrusionRole r) {
|
||||
return r != erSupportMaterial && r != erSupportMaterialInterface
|
||||
&& r != erSupportTransition
|
||||
&& r != erSkirt && r != erBrim && r != erWipeTower;
|
||||
};
|
||||
auto accumulate = [&](bool body_only) {
|
||||
tp_bb_clip = BoundingBoxf3();
|
||||
tp_bb_full = BoundingBoxf3();
|
||||
n_filtered = 0;
|
||||
n_clip = 0;
|
||||
for (const GCodeProcessorResult::MoveVertex& mv : gcode_result.moves)
|
||||
if (mv.type == EMoveType::Extrude && mv.layer_id >= 1 // skip layer-0 prime/skirt
|
||||
&& (!body_only || is_object_body(mv.extrusion_role))) {
|
||||
++n_filtered;
|
||||
const Vec3d p = belt_inv * mv.position.cast<double>();
|
||||
tp_bb_full.merge(p);
|
||||
if (p.y() >= y_lo && p.y() <= y_hi) {
|
||||
tp_bb_clip.merge(p);
|
||||
++n_clip;
|
||||
}
|
||||
}
|
||||
};
|
||||
accumulate(/*body_only=*/true);
|
||||
// Fallback: a plate with no object-body extrusions above layer 0 (e.g. a
|
||||
// support-only object) would leave tp_bb undefined and silently skip the
|
||||
// anchor. Re-accumulate over ALL extrude moves so the min-corner anchor is
|
||||
// still computed rather than letting the preview float by the placement offset.
|
||||
if (n_filtered == 0)
|
||||
accumulate(/*body_only=*/false);
|
||||
// Use the clipped bbox when it still holds the bulk of the moves (outliers removed).
|
||||
// If the object sits far from the belt entry the toolpaths are grossly offset and the
|
||||
// clip drops most of them — fall back to the full bbox so the min-corner anchor still
|
||||
// recovers that gross translation rather than breaking.
|
||||
const BoundingBoxf3& tp_bb =
|
||||
(tp_bb_clip.defined && n_filtered > 0 &&
|
||||
double(n_clip) >= ANCHOR_CLIP_MIN_KEEP * double(n_filtered)) ? tp_bb_clip : tp_bb_full;
|
||||
if (model_bb.defined && tp_bb.defined) {
|
||||
// Anchor the back-transformed toolpath body onto the upright model bbox by
|
||||
// its MIN corner. (Center anchoring was tried and regressed when the toolpath
|
||||
// and model bounding boxes differ in extent.) With the belt_z_origin
|
||||
// correction above and the outlier-robust bbox below, the surface overlaps the
|
||||
// shell to well under a millimetre when the object is at the belt entry; an
|
||||
// object placed elsewhere in global-rotation mode still carries the placement
|
||||
// translation, which this min-corner step recovers.
|
||||
const Vec3d d = model_bb.min - tp_bb.min;
|
||||
belt_inv = Transform3d(Eigen::Translation3d(d)) * belt_inv;
|
||||
}
|
||||
}
|
||||
libvgcode::GCodeInputData data = libvgcode::convert(gcode_result, str_tool_colors, str_color_print_colors, m_viewer,
|
||||
apply_belt ? &belt_inv : nullptr);
|
||||
|
||||
//#define ENABLE_DATA_EXPORT 1
|
||||
//#if ENABLE_DATA_EXPORT
|
||||
@@ -1480,6 +1649,26 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const
|
||||
m_viewer.reset_default_extrusion_roles_colors();
|
||||
m_viewer.load(std::move(data));
|
||||
|
||||
// Belt printers: libvgcode labels a layer with the height of its toolpaths, which
|
||||
// on a tilted layer is wherever its last extrusion happened to end, and the layer
|
||||
// slider looks its labels and the colour-change ticks up in that list assuming it
|
||||
// increases. Give it the layers' print Z instead (the slicer's layer Z, which
|
||||
// increases along the belt), numbered the way libvgcode::convert() numbers the
|
||||
// layers: consecutively over the moves that exist.
|
||||
m_belt_layer_zs.clear();
|
||||
if (is_belt) {
|
||||
unsigned int src_layer_id = std::numeric_limits<unsigned int>::max();
|
||||
for (size_t i = 1; i < gcode_result.moves.size(); ++ i) {
|
||||
const GCodeProcessorResult::MoveVertex &mv = gcode_result.moves[i];
|
||||
if (mv.layer_id != src_layer_id) {
|
||||
src_layer_id = mv.layer_id;
|
||||
m_belt_layer_zs.emplace_back(double(mv.print_z));
|
||||
}
|
||||
}
|
||||
if (m_belt_layer_zs.size() != m_viewer.get_layers_count())
|
||||
m_belt_layer_zs.clear();
|
||||
}
|
||||
|
||||
// #if !VGCODE_ENABLE_COG_AND_TOOL_MARKERS
|
||||
// const size_t vertices_count = m_viewer.get_vertices_count();
|
||||
// m_cog.reset();
|
||||
@@ -1513,13 +1702,24 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const
|
||||
m_paths_bounding_box = BoundingBoxf3(libvgcode::convert(bbox[0]).cast<double>(), libvgcode::convert(bbox[1]).cast<double>());
|
||||
m_max_bounding_box = m_paths_bounding_box;
|
||||
|
||||
if (wxGetApp().is_editor())
|
||||
m_contained_in_bed = wxGetApp().plater()->build_volume().all_paths_inside(gcode_result, m_paths_bounding_box);
|
||||
if (wxGetApp().is_editor()) {
|
||||
if (is_belt) {
|
||||
// The moves are machine-frame coordinates (Z is belt travel), so the per-move
|
||||
// test inside all_paths_inside() can never pass on a belt. Judge the
|
||||
// back-transformed box instead, with room for the designed view's min-corner
|
||||
// anchor, which is only accurate to a fraction of a millimetre.
|
||||
BoundingBoxf3 bed = wxGetApp().plater()->build_volume().bounding_volume();
|
||||
bed.offset(1.);
|
||||
m_contained_in_bed = !m_paths_bounding_box.defined || (bed.contains(m_paths_bounding_box.min) && bed.contains(m_paths_bounding_box.max));
|
||||
} else
|
||||
m_contained_in_bed = wxGetApp().plater()->build_volume().all_paths_inside(gcode_result, m_paths_bounding_box);
|
||||
}
|
||||
|
||||
m_extruders_count = gcode_result.filaments_count;
|
||||
|
||||
//BBS: move the id to the end of reset
|
||||
m_last_result_id = gcode_result.id;
|
||||
m_last_belt_show_designed = m_belt_show_designed;
|
||||
m_gcode_result = &gcode_result;
|
||||
m_move_type_counts.fill(0);
|
||||
for (auto& move_type_times : m_move_type_times)
|
||||
@@ -1550,8 +1750,10 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const
|
||||
m_custom_gcode_per_print_z = gcode_result.custom_gcode_per_print_z;
|
||||
|
||||
m_max_print_height = gcode_result.printable_height;
|
||||
m_machine_frame_transform_active = gcode_result.machine_frame_transform_active;
|
||||
m_z_offset = gcode_result.z_offset;
|
||||
|
||||
|
||||
// load_toolpaths(gcode_result, build_volume, exclude_bounding_box);
|
||||
|
||||
// ORCA: Apply the default view type now that we know how many tools the G-code actually uses.
|
||||
@@ -1737,6 +1939,7 @@ void GCodeViewer::reset()
|
||||
//BBS: should also reset the result id
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": current result id %1% ")%m_last_result_id;
|
||||
m_last_result_id = -1;
|
||||
m_belt_layer_zs.clear();
|
||||
//BBS: add only gcode mode
|
||||
m_only_gcode_in_preview = false;
|
||||
|
||||
@@ -1745,6 +1948,7 @@ void GCodeViewer::reset()
|
||||
m_paths_bounding_box = BoundingBoxf3();
|
||||
m_max_bounding_box = BoundingBoxf3();
|
||||
m_max_print_height = 0.0f;
|
||||
m_machine_frame_transform_active = false;
|
||||
m_z_offset = 0.0f;
|
||||
m_extruders_count = 0;
|
||||
m_filament_diameters = std::vector<float>();
|
||||
@@ -3015,7 +3219,14 @@ void GCodeViewer::load_shells(const Print& print, bool initialized, bool force_p
|
||||
void GCodeViewer::render_toolpaths()
|
||||
{
|
||||
const Camera& camera = wxGetApp().plater()->get_camera();
|
||||
const libvgcode::Mat4x4 converted_view_matrix = libvgcode::convert(static_cast<Matrix4f>(camera.get_view_matrix().matrix().cast<float>()));
|
||||
Matrix4f view = camera.get_view_matrix().matrix().cast<float>();
|
||||
// Belt "designed" (upright) view is produced by back-transforming the toolpath
|
||||
// GEOMETRY into model space at load time (see load_as_gcode ->
|
||||
// compute_belt_back_transform -> libvgcode::convert). The camera is left untouched
|
||||
// here; transforming the view as well would double-apply the inverse. Baking the
|
||||
// transform into the geometry (not the camera) also keeps the bed and toolpaths in
|
||||
// the same frame so they stay aligned.
|
||||
const libvgcode::Mat4x4 converted_view_matrix = libvgcode::convert(view);
|
||||
const libvgcode::Mat4x4 converted_projetion_matrix = libvgcode::convert(static_cast<Matrix4f>(camera.get_projection_matrix().matrix().cast<float>()));
|
||||
#if VGCODE_ENABLE_COG_AND_TOOL_MARKERS
|
||||
m_viewer.set_cog_marker_scale_factor(m_cog_marker_fixed_screen_size ? 10.0f * m_cog_marker_size * camera.get_inv_zoom() : m_cog_marker_size);
|
||||
@@ -3590,27 +3801,6 @@ void GCodeViewer::render_legend_color_arr_recommen(float window_padding)
|
||||
}
|
||||
};
|
||||
|
||||
auto link_filament_group_wiki = [&](const std::string& label) {
|
||||
ImVec2 wiki_part_size = ImGui::CalcTextSize(label.c_str());
|
||||
ImColor HyperColor = ImColor(0, 150, 136, 255); // ORCA match color
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, HyperColor.Value);
|
||||
imgui.text(label.c_str());
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
// ORCA use underline to match hyperlink style
|
||||
ImVec2 lineEnd = ImGui::GetItemRectMax();
|
||||
lineEnd.y -= 2.0f;
|
||||
ImVec2 lineStart = lineEnd;
|
||||
lineStart.x = ImGui::GetItemRectMin().x;
|
||||
ImGui::GetWindowDrawList()->AddLine(lineStart, lineEnd, HyperColor);
|
||||
// click behavior
|
||||
if (ImGui::IsMouseHoveringRect(ImGui::GetItemRectMin(), ImGui::GetItemRectMax(), true)) {
|
||||
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
|
||||
open_filament_group_wiki();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
auto draw_dash_line = [&](ImDrawList* draw_list, int dash_length = 5, int gap_length = 3) {
|
||||
ImVec2 p1 = ImGui::GetCursorScreenPos();
|
||||
ImVec2 p2 = ImVec2(p1.x + ImGui::GetContentRegionAvail().x, p1.y);
|
||||
@@ -3684,7 +3874,7 @@ void GCodeViewer::render_legend_color_arr_recommen(float window_padding)
|
||||
else
|
||||
tips_count = 5;
|
||||
|
||||
float AMS_container_height = ams_item_height + line_height * tips_count + line_height;
|
||||
float AMS_container_height = ams_item_height + line_height * tips_count + line_height + window_padding * 2.f;
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0, 0, 0, 0)); // this shold be 0 since its child of gcodeviewer
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1, 1, 1, 1));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(window_padding * 3, 0));
|
||||
@@ -3699,10 +3889,6 @@ void GCodeViewer::render_legend_color_arr_recommen(float window_padding)
|
||||
ImGui::Dummy({window_padding, window_padding});
|
||||
ImGui::PushStyleColor(ImGuiCol_Separator, ImVec4(1.0f,1.0f,1.0f,0.6f));
|
||||
imgui.bold_text(_u8L("Filament Grouping"));
|
||||
ImGui::SameLine();
|
||||
std::string tip_str = _u8L("Why this grouping");
|
||||
ImGui::SetCursorPosX(ImGui::GetWindowContentRegionWidth() - window_padding - ImGui::CalcTextSize(tip_str.c_str()).x);
|
||||
link_filament_group_wiki(tip_str);
|
||||
ImGui::Separator();
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::Dummy({window_padding, window_padding});
|
||||
@@ -3807,6 +3993,9 @@ void GCodeViewer::render_legend_color_arr_recommen(float window_padding)
|
||||
}
|
||||
|
||||
ImGui::Dummy({window_padding, window_padding});
|
||||
GLGizmoUtils::render_wiki_guide_button(*wxGetApp().plater()->get_current_canvas3D(), m_scale,"https://e.bambulab.com/t?c=mOkvsXkJ9pldGYp9");
|
||||
ImGui::SameLine();
|
||||
|
||||
if (!is_optimal_group) {
|
||||
link_text_set_to_optional(_u8L("Set to Optimal"));
|
||||
ImGui::SameLine();
|
||||
@@ -3815,10 +4004,7 @@ void GCodeViewer::render_legend_color_arr_recommen(float window_padding)
|
||||
}
|
||||
link_text(_u8L("Regroup filament"));
|
||||
|
||||
ImGui::SameLine();
|
||||
std::string wiki_str = _u8L("Wiki Guide"); // ORCA
|
||||
ImGui::SetCursorPosX(ImGui::GetWindowContentRegionWidth() - window_padding - ImGui::CalcTextSize(wiki_str.c_str()).x);
|
||||
link_filament_group_wiki(wiki_str);
|
||||
ImGui::Dummy({window_padding, window_padding});
|
||||
|
||||
ImGui::EndChild();
|
||||
}
|
||||
@@ -5417,6 +5603,7 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
|
||||
if (m_nozzle_nums > 1 && (m_viewer.get_view_type() == libvgcode::EViewType::Summary || m_viewer.get_view_type() == libvgcode::EViewType::ColorPrint)) // ORCA show only on summary and filament tab
|
||||
render_legend_color_arr_recommen(window_padding);
|
||||
|
||||
|
||||
legend_height = ImGui::GetCurrentWindow()->Size.y;
|
||||
imgui.end();
|
||||
ImGui::PopStyleColor(7);
|
||||
|
||||
@@ -204,6 +204,10 @@ private:
|
||||
std::vector<int> m_plater_extruder;
|
||||
bool m_gl_data_initialized{ false };
|
||||
unsigned int m_last_result_id{ 0 };
|
||||
// Belt printers: the view the loaded result was converted for (see load_as_gcode).
|
||||
bool m_last_belt_show_designed{ true };
|
||||
// Belt printers: the print Z of each viewer layer, in the viewer's layer numbering.
|
||||
std::vector<double> m_belt_layer_zs;
|
||||
//BBS: save m_gcode_result as well
|
||||
const GCodeProcessorResult* m_gcode_result;
|
||||
std::array<unsigned int, static_cast<size_t>(EMoveType::Count)> m_move_type_counts{};
|
||||
@@ -224,6 +228,7 @@ private:
|
||||
//BBS: add shell bounding box
|
||||
BoundingBoxf3 m_shell_bounding_box;
|
||||
float m_max_print_height{ 0.0f };
|
||||
bool m_machine_frame_transform_active{ false };
|
||||
float m_z_offset{ 0.0f };
|
||||
|
||||
ConfigOptionMode m_user_mode;
|
||||
@@ -354,6 +359,11 @@ private:
|
||||
mutable bool m_no_render_path { false };
|
||||
bool m_is_dark = false;
|
||||
|
||||
bool m_belt_view_enabled = false;
|
||||
bool m_belt_show_designed = true; // Designed (upright, back-transformed) view by default; off shows
|
||||
// the raw machine-frame G-code (canvas view menu, hotkey B).
|
||||
float m_belt_angle_deg = 0.f;
|
||||
|
||||
libvgcode::Viewer m_viewer;
|
||||
// ORCA: section view, as the viewer has it. What it cuts away casts no shadow.
|
||||
std::array<float, 4> m_clipping_plane{ 0.0f, 0.0f, 0.0f, 1.0f };
|
||||
@@ -410,10 +420,15 @@ public:
|
||||
std::vector<int> get_plater_extruder();
|
||||
|
||||
const float get_max_print_height() const { return m_max_print_height; }
|
||||
bool is_machine_frame_transform_active() const { return m_machine_frame_transform_active; }
|
||||
const BoundingBoxf3& get_paths_bounding_box() const { return m_paths_bounding_box; }
|
||||
const BoundingBoxf3& get_max_bounding_box() const { return m_max_bounding_box; }
|
||||
const BoundingBoxf3& get_shell_bounding_box() const { return m_shell_bounding_box; }
|
||||
std::vector<double> get_layers_zs() const {
|
||||
// Belt printers: the layer Z the slider labels and the colour-change ticks
|
||||
// use is the layer's print Z (see load_as_gcode), not a toolpath height.
|
||||
if (! m_belt_layer_zs.empty())
|
||||
return m_belt_layer_zs;
|
||||
const std::vector<float> zs = m_viewer.get_layers_zs();
|
||||
std::vector<double> ret;
|
||||
std::transform(zs.begin(), zs.end(), std::back_inserter(ret), [](float z) { return static_cast<double>(z); });
|
||||
@@ -489,6 +504,11 @@ public:
|
||||
|
||||
void export_toolpaths_to_obj(const char* filename) const;
|
||||
|
||||
void set_belt_printer(bool enabled, float angle_deg) { m_belt_view_enabled = enabled; m_belt_angle_deg = angle_deg; }
|
||||
bool is_belt_view() const { return m_belt_view_enabled && m_belt_angle_deg > 0.f; }
|
||||
void toggle_belt_show_designed() { if (m_belt_view_enabled) m_belt_show_designed = !m_belt_show_designed; }
|
||||
bool is_belt_show_designed() const { return m_belt_show_designed; }
|
||||
|
||||
size_t get_extruders_count() { return m_extruders_count; }
|
||||
void push_combo_style();
|
||||
void pop_combo_style();
|
||||
|
||||
@@ -3185,7 +3185,13 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re
|
||||
need_wipe_tower |= dynamic_cast<const ConfigOptionBool*>(dconfig.option("enable_wrapping_detection"))->value;
|
||||
}
|
||||
|
||||
if (wt && (need_wipe_tower || filaments_count > 1) && !wxGetApp().plater()->only_gcode_mode() && !wxGetApp().plater()->is_gcode_3mf()) {
|
||||
// Belt printers replace the classic wipe tower with the auto-generated
|
||||
// belt purge prism (a real model object), so never draw the tower widget.
|
||||
bool is_belt_printer = false;
|
||||
if (const auto *belt_opt = wxGetApp().preset_bundle->printers.get_edited_preset().config.option<ConfigOptionBool>("belt_printer"))
|
||||
is_belt_printer = belt_opt->value;
|
||||
|
||||
if (wt && !is_belt_printer && (need_wipe_tower || filaments_count > 1) && !wxGetApp().plater()->only_gcode_mode() && !wxGetApp().plater()->is_gcode_3mf()) {
|
||||
// The tower size estimate reads printer- and filament-scope keys, which the print preset
|
||||
// does not carry; built once here rather than per plate.
|
||||
const DynamicPrintConfig full_config = wxGetApp().preset_bundle->full_config();
|
||||
@@ -3253,6 +3259,28 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re
|
||||
}
|
||||
|
||||
update_volumes_colors_by_extruder();
|
||||
|
||||
// ORCA-Belt: render the auto-generated belt purge prism like a wipe tower —
|
||||
// semi-transparent, in its (filament) color. It is a real sliced object, so
|
||||
// the G-code preview already shows the actual per-layer purge colors; here in
|
||||
// the editor we just make the block translucent so it reads as a purge tower
|
||||
// rather than a solid part. update_colors_by_extruder() preserves alpha when
|
||||
// not updating alpha, so lowering it once sticks across recolors.
|
||||
if (m_model != nullptr) {
|
||||
for (GLVolume *volume : m_volumes.volumes) {
|
||||
if (volume == nullptr || volume->volume_idx() < 0)
|
||||
continue;
|
||||
const int obj_idx = volume->object_idx();
|
||||
if (obj_idx < 0 || obj_idx >= (int) m_model->objects.size())
|
||||
continue;
|
||||
const ConfigOption *opt = m_model->objects[obj_idx]->config.option("belt_purge_tower_object");
|
||||
if (opt != nullptr && opt->getBool()) {
|
||||
volume->color.a(0.66f);
|
||||
volume->force_transparent = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update selection indices based on the old/new GLVolumeCollection.
|
||||
if (m_selection.get_mode() == Selection::Instance)
|
||||
m_selection.instances_changed(instance_ids_selected);
|
||||
@@ -3791,6 +3819,16 @@ bool GLCanvas3D::handle_shortcut(const KeyChord& chord)
|
||||
get_gcode_viewer().get_layers_slider()->switch_one_layer_mode();
|
||||
m_dirty = true;
|
||||
break;
|
||||
case Shortcut::ToggleBeltRawGcode:
|
||||
// Same state as the canvas view menu item. The designed-view back-transform is
|
||||
// baked into the toolpaths at load time, so the preview is re-converted.
|
||||
if (m_gcode_viewer.is_belt_view()) {
|
||||
m_gcode_viewer.toggle_belt_show_designed();
|
||||
if (Plater* plater = wxGetApp().plater())
|
||||
plater->refresh_belt_view();
|
||||
m_dirty = true;
|
||||
}
|
||||
break;
|
||||
case Shortcut::GoToLayer:
|
||||
if (!m_gizmos.is_enabled()) {
|
||||
get_gcode_viewer().get_layers_slider()->show_go_to_layer(true);
|
||||
@@ -10470,6 +10508,7 @@ void GLCanvas3D::_render_canvas_toolbar()
|
||||
ImGui::TextColored(enable ? ImVec4(1,1,1,1) : ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled), "%s", into_u8(condition ? ImGui::VisibleIcon : ImGui::HiddenIcon).c_str());
|
||||
};
|
||||
|
||||
|
||||
create_menu_item( _utf8(L("3D Navigator")),
|
||||
m_canvas_type != ECanvasType::CanvasAssembleView, // not work on assembly
|
||||
wxGetApp().show_3d_navigator(),
|
||||
@@ -10557,6 +10596,22 @@ void GLCanvas3D::_render_canvas_toolbar()
|
||||
[p]{p->show_view3D_labels(!p->are_view3D_labels_shown());}
|
||||
);
|
||||
|
||||
// Belt printers, G-code preview only: show the raw machine-frame G-code instead of
|
||||
// the designed (upright) view. This menu is the only place the toggle lives (plus
|
||||
// its shortcut); the reload is deferred (CallAfter) so the preview is not rebuilt
|
||||
// mid-render.
|
||||
if (m_canvas_type == ECanvasType::CanvasPreview && m_gcode_viewer.is_belt_view()) {
|
||||
ImGui::Separator();
|
||||
create_menu_item( _utf8(L("Show raw G-code (belt only)")),
|
||||
true,
|
||||
!m_gcode_viewer.is_belt_show_designed(), // eye lit = raw machine-frame G-code (designed view off)
|
||||
[this, p]{
|
||||
m_gcode_viewer.toggle_belt_show_designed();
|
||||
p->CallAfter([p]{ p->refresh_belt_view(); });
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
ImGui::PopItemFlag();
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
@@ -11547,7 +11602,11 @@ void GLCanvas3D::_set_warning_notification_if_needed(EWarning warning)
|
||||
if (current_printer_technology() != ptSLA) {
|
||||
unsigned int max_z_layer = m_gcode_viewer.get_layers_z_range().back();
|
||||
if (warning == EWarning::ToolHeightOutside) // check if max z_layer height exceed max print height
|
||||
show = m_gcode_viewer.has_data() && (m_gcode_viewer.get_layers_zs()[max_z_layer] - m_gcode_viewer.get_max_print_height() >= 1e-6);
|
||||
// Belt printer with active post-gcode machine-frame transform: layer Z values
|
||||
// live in the machine frame, not the build-volume frame, so the comparison
|
||||
// against printable_height is meaningless. Suppress the warning entirely.
|
||||
show = m_gcode_viewer.has_data() && !m_gcode_viewer.is_machine_frame_transform_active()
|
||||
&& (m_gcode_viewer.get_layers_zs()[max_z_layer] - m_gcode_viewer.get_max_print_height() >= 1e-6);
|
||||
else if (warning == EWarning::ToolpathOutside) { // check if max x,y coords exceed bed area
|
||||
show = m_gcode_viewer.has_data() && !m_gcode_viewer.is_contained_in_bed() &&
|
||||
(m_gcode_viewer.get_max_print_height() -m_gcode_viewer.get_layers_zs()[max_z_layer] >= 1e-6);
|
||||
|
||||
@@ -121,6 +121,7 @@
|
||||
// This is the only place where we want to allow that, so define an override macro.
|
||||
#define SLIC3R_ALLOW_LIBSLIC3R_I18N_IN_SLIC3R
|
||||
#include "libslic3r/I18N.hpp"
|
||||
#include "libslic3r/Point.hpp"
|
||||
#undef SLIC3R_ALLOW_LIBSLIC3R_I18N_IN_SLIC3R
|
||||
#include "slic3r/GUI/I18N.hpp"
|
||||
|
||||
@@ -171,6 +172,7 @@
|
||||
#include <openssl/evp.h>
|
||||
|
||||
#include "libslic3r/Utils.hpp"
|
||||
#include "libslic3r/Geometry.hpp"
|
||||
#include "libslic3r/Model.hpp"
|
||||
#include "libslic3r/PresetBundle.hpp"
|
||||
#include "libslic3r/InstanceLock.hpp"
|
||||
@@ -10113,5 +10115,17 @@ bool is_support_filament(int extruder_id, bool strict_check)
|
||||
return support_option->get_at(0);
|
||||
};
|
||||
|
||||
Vec3d build_plate_tilt_up_direction()
|
||||
{
|
||||
const DynamicPrintConfig &cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config;
|
||||
const auto *opt_x = cfg.option<ConfigOptionFloat>("build_plate_tilt_x");
|
||||
const auto *opt_y = cfg.option<ConfigOptionFloat>("build_plate_tilt_y");
|
||||
const double tilt_x = opt_x != nullptr ? opt_x->value : 0.;
|
||||
const double tilt_y = opt_y != nullptr ? opt_y->value : 0.;
|
||||
if (tilt_x == 0. && tilt_y == 0.)
|
||||
return Vec3d::UnitZ();
|
||||
return Vec3d(std::tan(Geometry::deg2rad(tilt_y)), std::tan(Geometry::deg2rad(tilt_x)), 1.).normalized();
|
||||
}
|
||||
|
||||
} // GUI
|
||||
} //Slic3r
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
#include "slic3r/GUI/Jobs/UpgradeNetworkJob.hpp"
|
||||
#include "slic3r/GUI/HttpServer.hpp"
|
||||
#include "../Utils/PrintHost.hpp"
|
||||
#include "libslic3r/Point.hpp"
|
||||
|
||||
#include <vector>
|
||||
#include <utility>
|
||||
@@ -878,6 +879,8 @@ bool is_support_filament(int extruder_id, bool strict_check = true);
|
||||
bool is_soluble_filament(int extruder_id);
|
||||
// check if the filament for model is in the list
|
||||
bool has_filaments(const std::vector<std::string>& model_filaments);
|
||||
// Up direction of the edited printer's tilted build plate (+Z when untilted).
|
||||
Vec3d build_plate_tilt_up_direction();
|
||||
} // namespace GUI
|
||||
} // Slic3r
|
||||
|
||||
|
||||
@@ -110,14 +110,14 @@ std::map<std::string, std::vector<SimpleSettingData>> SettingsFactory::OBJECT_C
|
||||
{"make_overhang_printable_angle","", 8},{"make_overhang_printable_hole_size","",9}, {"wall_sequence","",10},
|
||||
{"precise_z_height", "",10}
|
||||
}},
|
||||
{ L("Support"), {{"brim_type", "",1},{"brim_width", "",2},{"brim_object_gap", "",3},{"brim_flow_ratio", "",4},{"brim_use_efc_outline", "",5},
|
||||
{"enable_support", "",6},{"support_type", "",7},{"support_threshold_angle", "",8}, {"support_threshold_overlap", "",9}, {"support_on_build_plate_only", "",10},
|
||||
{"support_filament", "",11},{"support_interface_filament", "",12},{"support_expansion", "",13},{"support_style", "",14},
|
||||
{"tree_support_brim_width", "",15}, {"tree_support_branch_angle", "",16},{"tree_support_branch_angle_organic","",17}, {"tree_support_wall_count", "",18},{"tree_support_branch_diameter_angle", "",19},//tree support
|
||||
{"support_bottom_z_distance", "",20},{"support_top_z_distance", "",21},{"support_base_pattern", "",22},{"support_base_pattern_spacing", "",23},
|
||||
{"support_interface_top_layers", "",24},{"support_interface_bottom_layers", "",25},{"support_interface_spacing", "",26},{"support_bottom_interface_spacing", "",27},
|
||||
{"support_object_xy_distance", "",28}, {"bridge_no_support", "",29},{"max_bridge_length", "",30},{"support_critical_regions_only", "",31},{"support_remove_small_overhang","",32},
|
||||
{"support_object_first_layer_gap","",33}
|
||||
{ L("Support"), {{"brim_type", "",1},{"brim_width", "",2},{"leading_brim_length", "",3},{"extra_brim_width", "",4},{"brim_object_gap", "",5},{"brim_flow_ratio", "",6},{"brim_use_efc_outline", "",7},
|
||||
{"enable_support", "",8},{"support_type", "",9},{"support_threshold_angle", "",10}, {"support_threshold_overlap", "",11}, {"support_on_build_plate_only", "",12},
|
||||
{"support_filament", "",13},{"support_interface_filament", "",14},{"support_expansion", "",15},{"support_style", "",16},
|
||||
{"tree_support_brim_width", "",17}, {"tree_support_branch_angle", "",18},{"tree_support_branch_angle_organic","",19}, {"tree_support_wall_count", "",20},{"tree_support_branch_diameter_angle", "",21},//tree support
|
||||
{"support_bottom_z_distance", "",22},{"support_top_z_distance", "",23},{"support_base_pattern", "",24},{"support_base_pattern_spacing", "",25},
|
||||
{"support_interface_top_layers", "",26},{"support_interface_bottom_layers", "",27},{"support_interface_spacing", "",28},{"support_bottom_interface_spacing", "",29},
|
||||
{"support_object_xy_distance", "",30}, {"bridge_no_support", "",31},{"max_bridge_length", "",32},{"support_critical_regions_only", "",33},{"support_remove_small_overhang","",34},
|
||||
{"support_object_first_layer_gap","",35}
|
||||
}},
|
||||
{ L("Speed"), {{"support_speed", "",12}, {"support_interface_speed", "",13}
|
||||
}}
|
||||
@@ -1850,7 +1850,7 @@ void MenuFactory::create_filament_action_menu(bool init, int active_filament_men
|
||||
while (menu->GetMenuItemCount() > 0)
|
||||
menu->Destroy(menu->FindItemByPosition(0));
|
||||
|
||||
//if (init) { //
|
||||
//if (init) { //
|
||||
append_menu_item(
|
||||
menu, wxID_ANY, _L("Edit"), "", [](wxCommandEvent&) {
|
||||
plater()->sidebar().edit_filament(); }, "", nullptr,
|
||||
|
||||
@@ -2701,7 +2701,7 @@ void ObjectList::load_mesh_object(const std::vector<std::pair<const TriangleMesh
|
||||
Slic3r::save_object_mesh(*new_object);
|
||||
|
||||
// BBS: find an empty cell to put the copied object
|
||||
auto start_point = wxGetApp().plater()->build_volume().bounding_volume2d().center();
|
||||
auto start_point = wxGetApp().plater()->build_volume().bed_center();
|
||||
auto empty_cell = wxGetApp().plater()->canvas3D()->get_nearest_empty_cell({start_point(0), start_point(1)});
|
||||
|
||||
new_object->instances[0]->set_offset(center ? to_3d(Vec2d(empty_cell(0), empty_cell(1)), -new_object->origin_translation.z()) : bb.center());
|
||||
|
||||
@@ -369,6 +369,25 @@ void Preview::reload_print(bool only_gcode)
|
||||
m_only_gcode = only_gcode;
|
||||
}
|
||||
|
||||
void Preview::refresh_belt_view()
|
||||
{
|
||||
// Re-run the G-code preview conversion so the belt "designed view" toggle takes effect
|
||||
// (the back-transform is baked into the toolpath geometry in GCodeViewer::load_as_gcode,
|
||||
// whose same-result cache also keys on the view state, so the re-convert runs).
|
||||
// Reset m_loaded_print to bypass the "already loaded" guard the way reload_print does, but
|
||||
// keep the current layer range and only-gcode mode so the view doesn't jump on toggle.
|
||||
// The layer Z values differ between the designed and the raw view (the raw view's are
|
||||
// machine-frame heights), so keep_z_range alone cannot find the old span: carry the
|
||||
// slider over by layer index instead.
|
||||
IMSlider *layers_slider = m_canvas->get_gcode_viewer().get_layers_slider();
|
||||
const int lower = layers_slider->GetLowerValue();
|
||||
const int higher = layers_slider->GetHigherValue();
|
||||
m_loaded_print = nullptr;
|
||||
load_print(true /*keep_z_range*/, m_only_gcode);
|
||||
if (higher <= layers_slider->GetMaxValue())
|
||||
layers_slider->SetSelectionSpan(lower, higher);
|
||||
}
|
||||
|
||||
//BBS: always load shell at preview
|
||||
void Preview::load_shells(const Print& print, bool force_previewing)
|
||||
{
|
||||
|
||||
@@ -148,6 +148,8 @@ public:
|
||||
//BBS: add only gcode mode
|
||||
void load_print(bool keep_z_range = false, bool only_gcode = false);
|
||||
void reload_print(bool only_gcode = false);
|
||||
// Belt printers: re-convert the G-code preview so the "designed view" toggle takes effect.
|
||||
void refresh_belt_view();
|
||||
//BBS: always load shell at preview
|
||||
void load_shells(const Print& print, bool force_previewing = false);
|
||||
void reset_shells();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user