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:
Clifford Garwood
2026-10-09 07:44:12 -04:00
co-authored by Claude Opus 5.5
227 changed files with 14506 additions and 1079 deletions
+7 -1
View File
@@ -91,6 +91,12 @@ if (SLIC3R_GUI)
# list(REMOVE_ITEM wxWidgets_LIBRARIES oleacc)
find_package(wxInspector REQUIRED)
# wxInspector 1.0.0 installs its headers but accidentally declares the
# INSTALL_INTERFACE include directory PRIVATE, so its imported target does
# not expose them to consumers. Restore the package prefix include path until
# the upstream export is fixed.
get_filename_component(WXINSPECTOR_PREFIX "${wxInspector_DIR}/../../.." ABSOLUTE)
target_include_directories(wxInspector::wxInspector INTERFACE "${WXINSPECTOR_PREFIX}/include")
# wxInspector's exported interface names the release wxWidgets import
# libraries, which a Debug build cannot link. wx is linked above instead.
@@ -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
View File
@@ -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;
+6
View File
@@ -28,6 +28,12 @@ if (ORCA_TOOLS)
target_link_libraries(generate_system_cache libslic3r boost_headeronly)
target_compile_definitions(generate_system_cache PRIVATE ${_DEV_DEFS})
# texture_unwrap_dump: reports the LSCM unwrap of a saved project's texture displacement layers,
# chart by chart, so a defect can be reproduced from the project file instead of from a screenshot.
add_executable(texture_unwrap_dump texture_unwrap_dump.cpp)
target_link_libraries(texture_unwrap_dump libslic3r boost_headeronly nanosvg)
target_compile_definitions(texture_unwrap_dump PRIVATE ${_DEV_DEFS})
# profile_include_dump: prints what included templates contribute to a vendor's presets,
# to diff against the same tool built in BambuStudio. Built only on request.
add_executable(profile_include_dump EXCLUDE_FROM_ALL profile_include_dump.cpp)
+29 -11
View File
@@ -202,6 +202,7 @@ Vec2d place_wipe_tower(DynamicPrintConfig &cfg, const Vec2d &center)
std::string slice_two_color_cube_and_export(DynamicPrintConfig cfg, bool is_bbl, bool by_object)
{
const Vec2d center = printable_area_center(cfg);
const bool belt = cfg.opt_bool("belt_printer");
std::vector<Vec2d> cube_mins;
if (by_object) {
// By-object printing fires the hook only without a wipe tower, and rules out clumping detection and
@@ -212,6 +213,12 @@ std::string slice_two_color_cube_and_export(DynamicPrintConfig cfg, bool is_bbl,
cfg.set_key_value("timelapse_type", new ConfigOptionEnum<TimelapseType>(tlTraditional));
cfg.set_key_value("skirt_loops", new ConfigOptionInt(0));
cube_mins = {center + Vec2d(-20., -5.), center + Vec2d(10., -5.)};
} else if (belt) {
// A belt object's slicing Z starts at the belt below its leading end, well below its first
// printed layer, so a height range in slicing Z does not map onto the part. Two cubes one
// behind the other along the belt, the second on filament 2, give the one filament change
// instead (the purge prism is an object the GUI adds, so there is no tower to place).
cube_mins = {center - Vec2d(5., 15.), center + Vec2d(-5., 5.)};
} else {
// Clumping detection changes the tower footprint, so turn it on before placing the tower.
if (!cfg.opt_string("wrapping_detection_gcode").empty())
@@ -229,14 +236,20 @@ std::string slice_two_color_cube_and_export(DynamicPrintConfig cfg, bool is_bbl,
obj->name = "cube"; // populates [input_filename_base] the way a loaded model does
obj->add_volume(m);
obj->add_instance();
// Filament 2 is used only above z=4, so the upper layers carry a single filament change.
DynamicPrintConfig range_config;
range_config.set_key_value("extruder", new ConfigOptionInt(2));
// Every range must carry a layer_height; use the process's own so a fine nozzle (e.g. 0.15 mm
// printing ~0.1 mm layers) isn't forced to a height its extrusion width can't support - that
// trips Flow::with_spacing.
range_config.set_key_value("layer_height", new ConfigOptionFloat(cfg.opt_float("layer_height")));
obj->layer_config_ranges[{4.0, 10.0}].assign_config(std::move(range_config));
if (belt && !by_object) {
// The second cube along the belt is on filament 2 (see cube_mins above).
if (&cube_min == &cube_mins.back())
obj->config.set_key_value("extruder", new ConfigOptionInt(2));
} else {
// Filament 2 is used only above z=4, so the upper layers carry a single filament change.
DynamicPrintConfig range_config;
range_config.set_key_value("extruder", new ConfigOptionInt(2));
// Every range must carry a layer_height; use the process's own so a fine nozzle (e.g. 0.15 mm
// printing ~0.1 mm layers) isn't forced to a height its extrusion width can't support - that
// trips Flow::with_spacing.
range_config.set_key_value("layer_height", new ConfigOptionFloat(cfg.opt_float("layer_height")));
obj->layer_config_ranges[{4.0, 10.0}].assign_config(std::move(range_config));
}
obj->ensure_on_bed();
print.auto_assign_extruders(obj);
}
@@ -521,11 +534,16 @@ int slice_all_printers(const std::string &vendor, const std::string &outdir)
const std::string filament_name = bundle.filaments.get_selected_preset_name();
const std::string what = "Printer \"" + printer + "\"";
const std::string file_base = sanitize_filename(vendor_name) + "__" + sanitize_filename(printer);
// A belt printer has no wipe tower (it purges into a prism object on the belt), so its
// filament change is the plain tool change set_extruder() emits rather than the tower's block.
const bool belt = bundle.printers.get_selected_preset().config.opt_bool("belt_printer");
const std::string marker = belt ? "\nT1\n" : "CP TOOLCHANGE START";
if (const std::string out = slice_selection(bundle, what, false, outdir, file_base); out.empty())
++failures;
else if (out.find("CP TOOLCHANGE START") == std::string::npos) {
// The filament change never rode the tower, so change_filament_gcode was not exercised.
BOOST_LOG_TRIVIAL(error) << what << " sliced but the filament change never fired (no CP TOOLCHANGE START)";
else if (out.find(marker) == std::string::npos) {
// The filament change never fired, so change_filament_gcode was not exercised.
BOOST_LOG_TRIVIAL(error) << what << " sliced but the filament change never fired (no "
<< (belt ? "T1" : "CP TOOLCHANGE START") << ")";
++failures;
}
cover(bundle.prints.get_selected_preset());
+292
View File
@@ -0,0 +1,292 @@
// Diagnostic for the LSCM unwrap of a texture displacement layer.
//
// It exists because the defect it hunts only shows up on a real painted patch: the paint mask is built
// by TriangleSelector splitting base triangles, so the patch topology cannot be written down by hand,
// and reasoning about it from a screenshot of the 3D view had already produced three wrong diagnoses.
// This loads a saved project, rebuilds exactly the patch the bake would act on, runs the same unwrap,
// and reports what came out - per chart, so a bad one can be pointed at rather than guessed at.
//
// texture_unwrap_dump <project.3mf>
// nanosvg is header-only and libslic3r's 3mf import references it without carrying the implementation,
// so every executable that links libslic3r has to supply it. Must precede any include that pulls the
// header in, or its include guard suppresses the implementation. Same pattern as the other dev tools.
#define NANOSVG_IMPLEMENTATION
#include "nanosvg/nanosvg.h"
#define NANOSVGRAST_IMPLEMENTATION
#include "nanosvg/nanosvgrast.h"
#include <chrono>
#include <cstdio>
#include <string>
#include <functional>
#include <unordered_map>
#include <vector>
#include "libslic3r/Model.hpp"
#include "libslic3r/TextureDisplacement.hpp"
#include "libslic3r/Format/bbs_3mf.hpp"
#include "libslic3r/Utils.hpp"
#include <boost/filesystem.hpp>
using namespace Slic3r;
namespace {
uint64_t edge_key(int a, int b)
{
if (a > b)
std::swap(a, b);
return (uint64_t(uint32_t(a)) << 32) | uint32_t(b);
}
// Boundary loops and the Euler characteristic of a face set, which together say whether a chart is the
// topological disk LSCM needs (one loop, V - E + F == 1).
void chart_topology(const indexed_triangle_set &mesh, const std::vector<int> &faces, int &loops, int &euler)
{
std::unordered_map<uint64_t, int> edge_use;
std::unordered_map<int, int> local;
for (const int f : faces) {
const stl_triangle_vertex_indices &t = mesh.indices[size_t(f)];
for (int i = 0; i < 3; ++i) {
++edge_use[edge_key(t[i], t[(i + 1) % 3])];
local.emplace(t[i], int(local.size()));
}
}
euler = int(local.size()) - int(edge_use.size()) + int(faces.size());
std::unordered_map<int, int> parent;
const std::function<int(int)> find = [&](int x) {
while (parent[x] != x)
x = parent[x] = parent[parent[x]];
return x;
};
for (const auto &[key, uses] : edge_use)
if (uses == 1)
for (const int v : { int(key >> 32), int(uint32_t(key)) })
parent.emplace(v, v);
for (const auto &[key, uses] : edge_use)
if (uses == 1) {
const int a = find(int(key >> 32)), b = find(int(uint32_t(key)));
if (a != b)
parent[b] = a;
}
std::unordered_map<int, int> roots;
for (const auto &[v, p] : parent)
roots[find(v)] = 1;
loops = int(roots.size());
}
float signed_area_2d(const Vec2f &a, const Vec2f &b, const Vec2f &c)
{
return 0.5f * ((b.x() - a.x()) * (c.y() - a.y()) - (c.x() - a.x()) * (b.y() - a.y()));
}
} // namespace
int main(int argc, char **argv)
{
if (argc < 2) {
std::printf("usage: texture_unwrap_dump <project.3mf>\n");
return 2;
}
Model model;
DynamicPrintConfig config;
ConfigSubstitutionContext ctx(ForwardCompatibilitySubstitutionRule::Enable);
PlateDataPtrs plate_data;
std::vector<Preset *> project_presets;
bool is_bbl_3mf = false, is_orca_3mf = false;
Semver file_version;
// The importer writes a backup copy under the data dir and silently loses objects without one.
const boost::filesystem::path tmp = boost::filesystem::temp_directory_path() / "texture_unwrap_dump";
boost::filesystem::create_directories(tmp);
set_data_dir(tmp.string());
// LoadModel so the meshes come through; AddDefaultInstances because an object with no instance is
// dropped by the plate mapping, which is what "skip this object" in the log means.
if (!load_bbs_3mf(argv[1], &config, &ctx, &model, &plate_data, &project_presets, &is_bbl_3mf, &is_orca_3mf,
&file_version, nullptr,
LoadStrategy::LoadModel | LoadStrategy::LoadConfig | LoadStrategy::AddDefaultInstances |
LoadStrategy::Silence)) {
std::printf("failed to load %s\n", argv[1]);
return 1;
}
std::printf("loaded: %zu object(s)\n", model.objects.size());
for (const ModelObject *object : model.objects)
for (const ModelVolume *volume : object->volumes) {
if (volume->texture_displacement_layers.empty()) {
std::printf("volume \"%s\": no texture displacement layers; paint masks per slot:",
volume->name.c_str());
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
std::printf(" %zu", volume->texture_displacement_facet(i).get_data().triangles_to_split.size());
std::printf("\n");
continue;
}
std::printf("volume \"%s\": %zu base triangles, %zu layer(s)\n", volume->name.c_str(),
volume->mesh().its.indices.size(), volume->texture_displacement_layers.size());
for (const TextureDisplacementLayer &layer : volume->texture_displacement_layers) {
std::printf("\n layer %d \"%s\" mapping=%d seam_angle=%.1f connect=%d islands_stored=%zu\n",
layer.slot, layer.name.c_str(), int(layer.projection_method),
layer.lscm_seam_angle_deg, int(layer.auto_connect_islands), layer.islands.size());
if (layer.projection_method != TextureProjectionMethod::LSCM)
continue;
const indexed_triangle_set patch =
extract_painted_patch(volume->mesh().its, volume->texture_displacement_facet(layer.slot).get_data());
std::printf(" patch: %zu vertices, %zu triangles\n", patch.vertices.size(), patch.indices.size());
if (patch.indices.empty())
continue;
const auto t0 = std::chrono::steady_clock::now();
const PatchUnwrap unwrap = compute_patch_unwrap(patch, layer.lscm_seam_angle_deg, 0.f,
layer.lscm_seam_edges);
const auto t1 = std::chrono::steady_clock::now();
std::printf(" TIMING compute_patch_unwrap: %.0f ms\n",
std::chrono::duration<double, std::milli>(t1 - t0).count());
std::printf(" unwrap: %d charts, %zu unwrapped triangles\n", unwrap.chart_count,
unwrap.indices.size());
// Group the patch's faces by chart so each can be examined on its own.
std::vector<std::vector<int>> chart_faces(size_t(std::max(unwrap.chart_count, 0)));
for (size_t i = 0; i < unwrap.indices.size(); ++i) {
const int chart = unwrap.vertex_chart[size_t(unwrap.indices[i][0])];
if (chart >= 0 && size_t(chart) < chart_faces.size())
chart_faces[size_t(chart)].push_back(unwrap.source_face[i]);
}
int bad_charts = 0;
for (size_t c = 0; c < chart_faces.size(); ++c) {
int loops = 0, euler = 0;
chart_topology(patch, chart_faces[c], loops, euler);
// Flipped triangles: the unwrap folded over itself, which is what a planar fallback
// does to a chart that is not flat. Measured on the unwrap's own triangles.
int pos = 0, neg = 0;
for (size_t i = 0; i < unwrap.indices.size(); ++i) {
const stl_triangle_vertex_indices &t = unwrap.indices[i];
if (unwrap.vertex_chart[size_t(t[0])] != int(c))
continue;
const float a = signed_area_2d(unwrap.uvs[size_t(t[0])], unwrap.uvs[size_t(t[1])],
unwrap.uvs[size_t(t[2])]);
if (a > 0.f) ++pos; else if (a < 0.f) ++neg;
}
const int flipped = std::min(pos, neg);
const bool disk = loops == 1 && euler == 1;
if (!disk || flipped > 0) {
++bad_charts;
std::printf(" chart %2zu: %4zu faces loops=%d euler=%d%s flipped=%d/%d%s\n", c,
chart_faces[c].size(), loops, euler, disk ? "" : " NOT A DISK", flipped,
pos + neg, flipped ? " FOLDED" : "");
}
}
std::printf(" charts with a defect: %d / %d\n", bad_charts, unwrap.chart_count);
// What the eye actually sees. Every patch edge shared by two charts should carry the same
// UV on both sides once the islands are laid out as a connected net; where it does not,
// the texture jumps across that seam. Measured through compute_lscm_uvs(), i.e. the exact
// coordinates the bake and the checker overlay sample.
{
const auto n0 = std::chrono::steady_clock::now();
const std::vector<TextureIsland> net = compute_connected_net(unwrap);
const auto n1 = std::chrono::steady_clock::now();
std::printf(" TIMING compute_connected_net: %.0f ms (%zu islands)\n",
std::chrono::duration<double, std::milli>(n1 - n0).count(), net.size());
}
const auto t2 = std::chrono::steady_clock::now();
const std::vector<Vec2f> uv = compute_lscm_uvs(patch, layer);
const auto t3 = std::chrono::steady_clock::now();
std::printf(" TIMING compute_lscm_uvs: %.0f ms (called on every preview, overlay and bake)\n",
std::chrono::duration<double, std::milli>(t3 - t2).count());
if (uv.size() != patch.vertices.size()) {
std::printf(" compute_lscm_uvs returned %zu uvs for %zu vertices\n", uv.size(),
patch.vertices.size());
continue;
}
// Per-corner UVs carry each chart's own placement, so an edge shared by two charts shows
// the jump directly: the same mesh vertex lands at two different UVs. That is exactly what
// the eye reads as the texture breaking.
const auto t4 = std::chrono::steady_clock::now();
const std::vector<Vec2f> corner = compute_lscm_corner_uvs(patch, layer);
const auto t5 = std::chrono::steady_clock::now();
std::printf(" TIMING compute_lscm_corner_uvs: %.0f ms\n",
std::chrono::duration<double, std::milli>(t5 - t4).count());
// Keyed by edge, holding the UV each incident face gives to the edge's *lower-numbered*
// endpoint. Comparing that same vertex on both sides is the point: indexing by corner
// position instead compares opposite ends of the edge, because the two faces wind it in
// opposite directions.
std::unordered_map<uint64_t, std::vector<Vec2f>> edge_seen;
if (corner.size() == patch.indices.size() * 3)
for (size_t f = 0; f < patch.indices.size(); ++f) {
const stl_triangle_vertex_indices &t = patch.indices[f];
for (int k = 0; k < 3; ++k) {
const int a = t[k], b = t[(k + 1) % 3];
const int probe = std::min(a, b);
const int local = (a == probe) ? k : (k + 1) % 3;
edge_seen[edge_key(a, b)].push_back(corner[f * 3 + size_t(local)]);
}
}
// Which chart each patch face belongs to, so a broken edge can be attributed to a pair.
std::vector<int> chart_of_face(patch.indices.size(), -1);
for (size_t i = 0; i < unwrap.indices.size(); ++i)
chart_of_face[size_t(unwrap.source_face[i])] = unwrap.vertex_chart[size_t(unwrap.indices[i][0])];
std::unordered_map<uint64_t, std::vector<int>> edge_faces;
for (size_t f = 0; f < patch.indices.size(); ++f) {
const stl_triangle_vertex_indices &t = patch.indices[f];
for (int k = 0; k < 3; ++k)
edge_faces[edge_key(t[k], t[(k + 1) % 3])].push_back(int(f));
}
int adjacent = 0, broken = 0, broken_same_chart = 0;
float worst = 0.f;
std::map<std::pair<int, int>, std::pair<int, float>> by_pair;
for (const auto &[key, seen] : edge_seen) {
if (seen.size() != 2)
continue;
++adjacent;
const float d = (seen[0] - seen[1]).norm();
if (d <= 1e-4f)
continue;
++broken;
worst = std::max(worst, d);
const auto &faces_here = edge_faces[key];
int c1 = -1, c2 = -1;
if (faces_here.size() == 2) {
c1 = chart_of_face[size_t(faces_here[0])];
c2 = chart_of_face[size_t(faces_here[1])];
}
if (c1 == c2)
++broken_same_chart;
auto &slot = by_pair[{ std::min(c1, c2), std::max(c1, c2) }];
++slot.first;
slot.second = std::max(slot.second, d);
}
std::printf(" broken edges inside a single chart: %d\n", broken_same_chart);
std::printf(" broken by chart pair:");
for (const auto &[pk, v] : by_pair)
std::printf(" (%d,%d)x%d/%.1f", pk.first, pk.second, v.first, v.second);
std::printf("\n");
// Total length of the seams left broken, in mm: how much visibly torn edge the layout has,
// which is what the eye adds up. A count alone hides whether the breaks are hairlines or
// whole sides of an island.
float seam_mm = 0.f;
for (const auto &[key, seen] : edge_seen) {
if (seen.size() != 2 || (seen[0] - seen[1]).norm() <= 1e-4f)
continue;
seam_mm += (patch.vertices[size_t(key >> 32)] - patch.vertices[size_t(uint32_t(key))]).norm();
}
std::printf(" interior edges: %d, discontinuous: %d, total torn seam: %.2f mm (worst jump %.3f)\n",
adjacent, broken, seam_mm, worst);
std::printf(" stored islands %zu vs charts %d -> %s\n", layer.islands.size(),
unwrap.chart_count,
layer.islands.size() == size_t(unwrap.chart_count) ? "stored placements used"
: "net rebuilt");
}
}
return 0;
}
+84 -9
View File
@@ -299,7 +299,15 @@ Points get_shrink_bedpts(const DynamicPrintConfig* print_cfg, const ArrangeParam
template<class PConf>
void fill_config(PConf& pcfg, const ArrangeParams &params) {
if (params.is_seq_print) {
if (params.is_belt) {
// Pack from the end of the belt that prints first, and keep the pile on the
// bed when it is larger than the room around that end.
pcfg.starting_point = !params.belt_reversed ? PConf::Alignment::BOTTOM_LEFT :
params.belt_axis == 1 ? PConf::Alignment::TOP_LEFT :
PConf::Alignment::BOTTOM_RIGHT;
pcfg.clamp_to_bin = true;
}
else if (params.is_seq_print) {
// Start placing the items from the center of the print bed
pcfg.starting_point = PConf::Alignment::BOTTOM_LEFT;
}
@@ -444,6 +452,50 @@ protected:
return bindist;
}
// Belt printers pack from the end of the belt that prints first, and a corner
// packer's checks (pile inside the bin, pack origin) apply to them as well.
bool corner_packing() const { return params.is_belt || m_pconf.starting_point == PConfig::Alignment::BOTTOM_LEFT; }
static double at(const Box::PointType &pt, int i) { return double(i == 0 ? getX(pt) : getY(pt)); }
// Position along the belt in print order: increasing from the end that prints first.
double belt_pos(const Box::PointType &pt) const { return params.belt_reversed ? -at(pt, params.belt_axis) : at(pt, params.belt_axis); }
double belt_start(const Box &bb) const { return belt_pos(params.belt_reversed ? bb.maxCorner() : bb.minCorner()); }
double belt_end(const Box &bb) const { return belt_pos(params.belt_reversed ? bb.minCorner() : bb.maxCorner()); }
// The corner of the bin the belt pile grows from.
Box::PointType belt_origin() const
{
const Box bb = sl::boundingBox(m_bin);
auto o = bb.minCorner();
if (params.belt_reversed) {
if (params.belt_axis == 0) setX(o, getX(bb.maxCorner()));
else setY(o, getY(bb.maxCorner()));
}
return o;
}
// An item's far edge in print order is what it costs (so a row fills across the
// belt before the pile advances), with a slight pull toward the near lateral
// edge and the same penalty as the bottom-left heuristic for sitting outside
// the corner.
double dist_along_belt(const Box &ibb)
{
const Box bin = sl::boundingBox(m_bin);
const int l = 1 - params.belt_axis;
const double lat = at(ibb.minCorner(), l) - at(bin.minCorner(), l);
double d = belt_end(ibb) - belt_start(bin);
d += lat < 0 ? 10 * -lat : 0.1 * lat;
if (double behind = belt_start(ibb) - belt_start(bin); behind < 0)
d += 10 * -behind;
return norm(d);
}
double corner_bindist(const Box &ibb, const Slic3r::Point &origin_pack)
{
return params.is_belt ? dist_along_belt(ibb) : dist_for_BOTTOM_LEFT(ibb, origin_pack);
}
double dist_to_bin(const Box& ibb, const Slic3r::Point& origin_pack, typename Packer::PlacementConfig::Alignment starting_point_alignment)
{
double bindist = 0;
@@ -533,8 +585,8 @@ protected:
// The smalles distance from the arranged pile center:
double dist = norm(*(std::min_element(dists.begin(), dists.end())));
if (m_pconf.starting_point == PConfig::Alignment::BOTTOM_LEFT) {
double bindist = dist_for_BOTTOM_LEFT(ibb, origin_pack);
if (corner_packing()) {
double bindist = corner_bindist(ibb, origin_pack);
score = 0.2 * dist + 0.8 * bindist;
}
else {
@@ -591,8 +643,8 @@ protected:
break;
}
case LAST_BIG_ITEM: {
if (m_pconf.starting_point == PConfig::Alignment::BOTTOM_LEFT) {
score = dist_for_BOTTOM_LEFT(ibb, origin_pack);
if (corner_packing()) {
score = corner_bindist(ibb, origin_pack);
}
else {
if (m_pilebb.defined)
@@ -607,8 +659,8 @@ protected:
// already processed bigger items.
// No need to play around with the anchor points, the center will be
// just fine for small items
if (m_pconf.starting_point == PConfig::Alignment::BOTTOM_LEFT)
score = dist_for_BOTTOM_LEFT(ibb, origin_pack);
if (corner_packing())
score = corner_bindist(ibb, origin_pack);
else {
// Align mainly around existing items
score = 0.8 * norm(pl::distance(ibb.center(), bigbb.center()))+ 0.2*norm(pl::distance(ibb.center(), origin_pack));
@@ -709,6 +761,28 @@ protected:
score += 1 * (new_extruder_cnt-last_extruder_cnt);
}
// On a belt the parts print in belt order, so every colour change between
// parts is a filament change. Items arrive sorted by extruder and the pile
// grows from the leading end; keep each colour's run contiguous by charging
// an item for every packed item of another colour it does not fully follow,
// counting the tilted layers that reach belt_tilt_slope * height past that
// item's far edge.
if (params.is_belt && !params.is_seq_print) {
const std::set<int> item_colours(item.extrude_ids.begin(), item.extrude_ids.end());
const double item_start = belt_start(ibb);
for (Item &p : m_items) {
if (p.is_virt_object)
continue;
const std::set<int> p_colours(p.extrude_ids.begin(), p.extrude_ids.end());
const bool same_colour = std::includes(item_colours.begin(), item_colours.end(), p_colours.begin(), p_colours.end())
|| std::includes(p_colours.begin(), p_colours.end(), item_colours.begin(), item_colours.end());
if (same_colour)
continue;
if (item_start < belt_end(p.boundingBox()) + scaled(p.height * params.belt_tilt_slope))
score += 10.;
}
}
return std::make_tuple(score, fullbb);
}
@@ -785,7 +859,8 @@ public:
auto binbb = sl::boundingBox(m_bin);
auto starting_point = cfg.starting_point == PConfig::Alignment::BOTTOM_LEFT ? binbb.minCorner() : binbb.center();
auto starting_point = this->params.is_belt ? belt_origin() :
cfg.starting_point == PConfig::Alignment::BOTTOM_LEFT ? binbb.minCorner() : binbb.center();
// if we have wipe tower, items should be arranged around wipe tower
for (Item itm : items) {
if (itm.is_wipe_tower) {
@@ -936,7 +1011,7 @@ std::function<double(const Item &, const ItemGroup&)> AutoArranger<ExPolygon>::g
auto mp = m_merged_pile;
mp.emplace_back(itm.transformedShape());
auto chull = sl::convexHull(mp);
if (m_pconf.starting_point == PConfig::Alignment::BOTTOM_LEFT)
if (corner_packing())
{
if (!sl::isInside(chull, m_bin))
score += LARGE_COST_TO_REJECT;
+7
View File
@@ -146,6 +146,13 @@ struct ArrangeParams {
float nozzle_height = 0;
float printable_height = 256.0;
Vec2d align_center{ 0.5,0.5 };
// Belt printer: items print in the order they lie along the belt axis, from
// its low end unless belt_reversed, and a part's top prints
// belt_tilt_slope * height further along it than its base.
bool is_belt = false;
int belt_axis = 1; // 0 = X, 1 = Y
bool belt_reversed = false;
float belt_tilt_slope = 1.f; // cot(belt tilt angle), 0 when the belt is not tilted
ArrangePolygons excluded_regions; // regions cant't be used
ArrangePolygons nonprefered_regions; // regions can be used but not prefered
+562
View File
@@ -0,0 +1,562 @@
#include <limits>
#include "BeltBrim.hpp"
#include "ClipperUtils.hpp"
#include "Flow.hpp"
#include "Layer.hpp"
#include "Polygon.hpp"
#include "Print.hpp"
#include "ShortestPath.hpp"
#include "Support/BeltFloorContext.hpp"
#include "BoundingBox.hpp"
#include "ExPolygon.hpp"
#include "ExtrusionEntity.hpp"
#include "ExtrusionEntityCollection.hpp"
#include "Point.hpp"
#include "Polyline.hpp"
#include "PrintConfig.hpp"
#include "libslic3r.h"
#include <algorithm>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <cstdlib>
#include <utility>
#include <vector>
namespace Slic3r {
// ---------------------------------------------------------------- scaling
static inline Point scale_u_point(const Point &p, int from_axis, double factor)
{
// llround, not a cast: casting truncates toward zero, so a round trip would
// walk every vertex toward the origin by up to one unit per pass.
return from_axis == 0 ?
Point(coord_t(std::llround(double(p.x()) * factor)), p.y()) :
Point(p.x(), coord_t(std::llround(double(p.y()) * factor)));
}
static inline void scale_u_polygon(Polygon &poly, int from_axis, double factor)
{
for (Point &p : poly.points)
p = scale_u_point(p, from_axis, factor);
}
ExPolygons belt_scale_u(const ExPolygons &src, const BeltBrimFrame &frame, double factor)
{
ExPolygons out = src;
for (ExPolygon &ex : out) {
scale_u_polygon(ex.contour, frame.from_axis, factor);
for (Polygon &hole : ex.holes)
scale_u_polygon(hole, frame.from_axis, factor);
}
return out;
}
Polylines belt_scale_u(const Polylines &src, const BeltBrimFrame &frame, double factor)
{
Polylines out = src;
for (Polyline &pl : out)
for (Point &p : pl.points)
p = scale_u_point(p, frame.from_axis, factor);
return out;
}
// ---------------------------------------------------------------- sweep
ExPolygons sweep_ex(const ExPolygons &src, const Point &t)
{
if (src.empty())
return {};
if (t == Point(0, 0))
return src;
// One parallelogram per boundary edge. Together with P and P + t these
// cover the Minkowski sum exactly: for any q = p + s*t with p in P and
// s in [0, 1], let s* be the smallest lambda >= 0 with q - lambda*t in P.
// Either s* == 0 (so q is in P) or q - s* * t lies on some boundary edge e,
// putting q in that edge's parallelogram. Hole edges must be included, or
// holes narrower than t along t would wrongly survive the sweep.
Polygons quads;
for (const ExPolygon &ex : src)
for (size_t c = 0; c < ex.num_contours(); ++ c)
for (const Line &e : ex.contour_or_hole(c).lines()) {
if (e.a == e.b)
continue;
Polygon q;
q.points = { e.a, e.b, e.b + t, e.a + t };
// The non-zero fill rule counts a clockwise ring as -1, which
// would punch a hole instead of adding material. Edges parallel
// to t give a zero-area quad; Clipper discards those harmlessly.
if (q.is_clockwise())
q.reverse();
quads.emplace_back(std::move(q));
}
ExPolygons shifted = src;
for (ExPolygon &ex : shifted)
ex.translate(t);
// union_ex(ExPolygons, Polygons) uses pftNonZero, which is the fill rule the
// argument above relies on.
return union_ex(union_ex(src, shifted), quads);
}
// ---------------------------------------------------------------- brim region
ExPolygons belt_brim_region(const ExPolygons &footprint_flat,
bool has_outer,
bool has_inner,
coord_t brim_width,
coord_t object_gap,
coord_t leading,
coord_t lateral,
const BeltBrimFrame &frame)
{
if (footprint_flat.empty() || (! has_outer && ! has_inner))
return {};
ExPolygons out;
if (has_outer) {
// Offset the outer ring from the contours only, so a hole cannot punch
// through it. Same reasoning as the plate brim in Brim.cpp.
Polygons contours;
contours.reserve(footprint_flat.size());
for (const ExPolygon &ex : footprint_flat)
contours.emplace_back(ex.contour);
// Inner and outer boundary offset from the same polygon, to avoid
// round-off mismatch between them.
ExPolygons inner = offset_ex(contours, float(object_gap), jtRound, SCALED_RESOLUTION);
// Close the interior before offsetting outwards. A belt contact patch is often a
// narrow, broken-up strip, and the offset rings of two islands less than
// 2 x brim_width apart merge and fill the space between them - space that lies
// UNDER the part, which is not what "outer brim" means. Closing also swallows
// holes in the patch for the same reason. Concavity-filling only, so an apron or
// any other outward protrusion is untouched.
ExPolygons envelope = brim_width > 0 ? closing_ex(inner, float(brim_width)) : inner;
ExPolygons base = envelope;
if (leading > 0) {
// Sweep downhill from the gapped keep-out, so the apron is contiguous with
// the ring instead of starting inside the gap.
const Point t = frame.from_axis == 0 ?
Point(frame.downhill_sign() * leading, 0) :
Point(0, frame.downhill_sign() * leading);
base = union_ex(base, sweep_ex(envelope, t));
}
if (lateral > 0) {
// Across the belt, both ways. Swept from `base` so the apron is widened
// too, and in the flattened frame the cross-belt axis is unscaled, so this
// distance is already a true on-belt distance.
const Point t = frame.from_axis == 0 ? Point(0, lateral) : Point(lateral, 0);
ExPolygons widened = union_ex(sweep_ex(base, t), sweep_ex(base, Point(-t.x(), -t.y())));
base = union_ex(base, to_polygons(widened));
}
ExPolygons outer = offset_ex(base, float(brim_width), jtRound, SCALED_RESOLUTION);
expolygons_append(out, diff_ex(outer, envelope));
}
if (has_inner) {
// Holes reversed so a negative offset grows inward, mirroring Brim.cpp.
// No apron here: an apron growing into a hole interior is never useful.
Polygons holes;
for (const ExPolygon &ex : footprint_flat)
polygons_append(holes, ex.holes);
polygons_reverse(holes);
if (! holes.empty()) {
ExPolygons hole_inner = offset_ex(holes, - float(brim_width + object_gap));
ExPolygons hole_outer = offset_ex(holes, - float(object_gap));
expolygons_append(out, intersection_ex(diff_ex(hole_outer, hole_inner), holes));
}
}
return union_ex(out);
}
// ---------------------------------------------------------------- line lattice
std::vector<coord_t> belt_brim_line_positions(coord_t u_lo,
coord_t u_hi,
coord_t pitch_u,
coord_t u_anchor)
{
std::vector<coord_t> out;
if (pitch_u <= 0 || u_hi <= u_lo)
return out;
// Walk the lattice from just below u_lo. Integer arithmetic throughout, so
// the half-open interval needs no epsilon: a point landing exactly on u_hi
// belongs to the next band.
int64_t k = int64_t(std::floor(double(u_lo - u_anchor) / double(pitch_u))) - 1;
while (u_anchor + coord_t(k) * pitch_u < u_lo)
++ k;
for (;; ++ k) {
const coord_t u = u_anchor + coord_t(k) * pitch_u;
if (u >= u_hi)
break;
out.emplace_back(u);
}
return out;
}
// ---------------------------------------------------------------- pipeline
// A band of the belt surface as an explicit box, clamped to `bounds` along the
// shear axis. Deliberately not BeltFloorContext::surface_polygon(): those
// half-planes span +-1000 mm, which is wasteful to clip against and dangerous to
// feed through the flattening scale.
static Polygon band_box(const BoundingBox &bounds, int from_axis, coordf_t u_lo, coordf_t u_hi)
{
coord_t lo = scale_(u_lo);
coord_t hi = scale_(u_hi);
const coord_t bmin = from_axis == 0 ? bounds.min.x() : bounds.min.y();
const coord_t bmax = from_axis == 0 ? bounds.max.x() : bounds.max.y();
lo = std::max(lo, bmin);
hi = std::min(hi, bmax);
Polygon poly;
if (hi <= lo)
return poly;
if (from_axis == 0)
poly.points = { Point(lo, bounds.min.y()), Point(hi, bounds.min.y()),
Point(hi, bounds.max.y()), Point(lo, bounds.max.y()) };
else
poly.points = { Point(bounds.min.x(), lo), Point(bounds.max.x(), lo),
Point(bounds.max.x(), hi), Point(bounds.min.x(), hi) };
return poly;
}
ExPolygons belt_brim_clip_leading_edge(const ExPolygons &region, const BeltBrimFrame &frame, coordf_t u_cut)
{
if (region.empty())
return region;
BoundingBox keep_bb = get_extents(region);
keep_bb.offset(scale_(1.));
const bool low_side = frame.downhill_sign() < 0; // downhill is -u
const Polygon keep = band_box(keep_bb, frame.from_axis,
low_side ? unscale<double>(frame.from_axis == 0 ? keep_bb.min.x() : keep_bb.min.y()) : u_cut,
low_side ? u_cut : unscale<double>(frame.from_axis == 0 ? keep_bb.max.x() : keep_bb.max.y()));
return keep.empty() ? ExPolygons{} : intersection_ex(region, Polygons{ keep });
}
// Everything the per-band line generator needs, gathered once per object.
struct BeltBrimContext
{
BeltFloorContext ctx;
BeltBrimFrame frame;
ExPolygons region; // brim region, object-local slicing XY
BoundingBox region_bbox;
Flow brim_flow;
coord_t pitch_u = 0;
coord_t u_anchor = 0;
double in_plane_pitch = 0.; // mm
};
// Emit the cross-belt brim lines that belong to the band [print_z - height, print_z].
static void belt_brim_band_paths(const BeltBrimContext &bc,
coordf_t print_z,
coordf_t height,
const Polygons &obstacles,
ExtrusionEntityCollection &out,
ExPolygons &areas_out)
{
coordf_t u_lo = bc.ctx.cutoff_u(print_z - height);
coordf_t u_hi = bc.ctx.cutoff_u(print_z);
if (u_lo > u_hi)
std::swap(u_lo, u_hi);
// How wide this band is measured ON the belt, versus one nominal bead.
const double band_in_plane = (u_hi - u_lo) * bc.frame.u_stretch();
// Fraction of the layer height at which a line sits above the belt. Toward the
// downhill edge, so the sheet is reasonably thick while the nozzle stays clear of
// the belt itself.
static constexpr double BAND_CLEARANCE_FRACTION = 0.75;
std::vector<coord_t> us;
double uniform_clearance = 0.; // 0 => derive per line from its own position
double line_pitch = bc.in_plane_pitch;
// One line also serves a band up to half a bead wider than the nominal pitch (a 0.3 mm
// first layer at 45 degrees): its flow is matched to the band, so the bead is that much
// wider. Two lattice lines in such a band would land almost on top of each other.
if (band_in_plane <= 1.5 * bc.in_plane_pitch + EPSILON) {
// Steep belt, which is the normal case: the band is narrower than one bead, so
// exactly one line fits. Place it at a FIXED fraction of the band rather than
// on a nominal-spacing lattice. On a lattice each line lands at an arbitrary
// point in its band, the clearance sweeps [0, height] from band to band, and the
// bead width therefore varies by 2x - visible as ragged, uneven brim lines.
// Anchoring to the band makes the clearance identical everywhere, so every bead
// is the same width.
//
// The spacing is then whatever the bands give (height / sin(tilt) on the belt)
// rather than the nominal bead spacing, so the flow below is matched to THAT
// pitch. Matched flow at the real pitch is what keeps the sheet uniform and
// gap-free; using nominal flow at band spacing would over-feed it.
us.push_back(scale_(bc.ctx.cutoff_u(print_z - BAND_CLEARANCE_FRACTION * height)));
uniform_clearance = BAND_CLEARANCE_FRACTION * height;
line_pitch = band_in_plane;
} else {
// Shallow belt: the band is wider than a bead, so it takes several lines and they
// have to sit on the nominal lattice. Their clearances then differ, and so do
// their widths - unavoidable here, but shallow belts are the rare case.
us = belt_brim_line_positions(scale_(u_lo), scale_(u_hi), bc.pitch_u, bc.u_anchor);
}
if (us.empty())
return;
const Polygons region_polys = to_polygons(bc.region);
// One lattice line at a time: the clearance - and therefore the extrusion
// volume - is a property of the line's u, so the pieces of different lines
// must not be pooled before the flow is resolved.
// Overshoot the region so the clip, not the line's ends, decides the extent.
const coord_t margin = coord_t(SCALED_EPSILON) + 1;
coord_t u_prev = std::numeric_limits<coord_t>::min();
for (coord_t u : us) {
// Nozzle-to-belt clearance for this line. Constant along the line, because the
// belt height depends only on the shear-axis coordinate. Band-anchored lines
// share one clearance by construction; lattice lines (shallow belts, or a first
// layer thick enough that the band is wider than a bead) each get their own.
//
// A lattice line can fall where the belt is only a hair below the band's print_z.
// The bead there would be laid scraping the belt while its flow is sized for a
// taller cell, so it is moved uphill to the same fraction of the band the
// single-line case uses. (The clearance is along slice Z; the real gap under the
// nozzle is clearance x cos(tilt), 0.53 h at 45 degrees for the 0.75 fraction.)
double clearance = uniform_clearance;
if (clearance <= 0.) {
const Point probe = bc.frame.from_axis == 0 ? Point(u, 0) : Point(0, u);
clearance = print_z - bc.ctx.floor_print_z(probe);
if (clearance < BAND_CLEARANCE_FRACTION * height) {
clearance = BAND_CLEARANCE_FRACTION * height;
u = scale_(bc.ctx.cutoff_u(print_z - clearance));
}
clearance = std::min(clearance, height);
}
// A line moved uphill can land on, or almost on, its neighbour; two beads closer
// than half a pitch would be laid into the same cell.
if (u_prev != std::numeric_limits<coord_t>::min() && std::abs(u - u_prev) < bc.pitch_u / 2)
continue;
u_prev = u;
Polyline line;
if (bc.frame.from_axis == 0)
line.points = { Point(u, coord_t(bc.region_bbox.min.y() - margin)),
Point(u, coord_t(bc.region_bbox.max.y() + margin)) };
else
line.points = { Point(coord_t(bc.region_bbox.min.x() - margin), u),
Point(coord_t(bc.region_bbox.max.x() + margin), u) };
Polylines pieces = intersection_pl(Polylines{ line }, region_polys);
if (! obstacles.empty())
pieces = diff_pl(pieces, obstacles);
if (pieces.empty())
continue;
// with_cross_section, not with_height: it reaches the prescribed volume while
// KEEPING the extrusion spacing, so the bead is sized to fill exactly one
// pitch x clearance cell of the sheet.
const Flow f = bc.brim_flow.with_cross_section(float(line_pitch * clearance));
// Footprint of these beads, for the first-layer convex hull and bbox.
for (const Polygon &p : offset(pieces, 0.5f * float(f.scaled_width())))
areas_out.emplace_back(ExPolygon(p));
extrusion_entities_append_paths(out.entities, chain_polylines(std::move(pieces)),
erBrim, f.mm3_per_mm(), f.width(), float(clearance));
}
}
// Union of everything extruded at `print_z` that the brim must keep clear of, expressed
// in `self`'s local slicing frame. Includes `self` itself: its slice at this Z can
// overhang outside the belt footprint and land in the brim ring, which the flattened
// brim_object_gap - a belt-plane separation - does not cover.
//
// SEQUENCING: this reads every object's layers and this object's own support layers.
// Another object's support step shifts that object's layer Z into the object frame for
// the duration of the run (PrintObject::_generate_support_material()), so the brims must
// not overlap with the parallel support step: Print::process() generates them one object
// after the other once that step is over (PrintObject::generate_belt_brim()), and an
// object that arrives on or leaves the plate invalidates the other brim owners' support
// step (PrintApply.cpp) so their brims are clipped against what is there now. Only this
// object's supports are dodged; another object's support at the same Z is not.
// `region_bbox` bounds the brim; anything outside it cannot clip a brim line, so whole
// objects are skipped without materialising their polygons. On a typical plate the
// objects do not overlap and every foreign object drops out here, which matters because
// this runs once per band - hundreds of times per object.
static Polygons belt_brim_obstacles(const Print &print, const PrintObject &self,
const BoundingBox &region_bbox, coordf_t print_z, coordf_t tol)
{
const Point shift_self = self.instances().empty() ? Point(0, 0)
: self.instances().front().shift_without_plate_offset();
Polygons out;
for (const PrintObject *o : print.objects()) {
const bool is_self = (o == &self);
for (const PrintInstance &inst : o->instances()) {
const Point delta = inst.shift_without_plate_offset() - shift_self;
if (const Layer *l = o->get_layer_at_printz(print_z, tol)) {
BoundingBox lb = get_extents(l->lslices);
lb.translate(delta.x(), delta.y());
if (lb.overlap(region_bbox)) {
Polygons ps = to_polygons(l->lslices);
for (Polygon &p : ps)
p.translate(delta);
polygons_append(out, std::move(ps));
}
}
if (! is_self)
continue;
if (const SupportLayer *sl = o->get_support_layer_at_printz(print_z, tol)) {
Polygons ps = sl->support_fills.polygons_covered_by_spacing();
for (Polygon &p : ps)
p.translate(delta);
polygons_append(out, std::move(ps));
}
}
}
if (out.size() < 2)
return out; // union_() of 0 or 1 polygons is pure overhead
return union_(out);
}
void make_belt_brim(PrintObject &object)
{
object.clear_belt_brim();
if (! object.has_belt_brim())
return;
const Print &print = *object.print();
BeltBrimContext bc;
if (! bc.ctx.init(object.slicing_parameters(), print.config()))
return;
bc.frame = BeltBrimFrame{ bc.ctx.shear_factor(), bc.ctx.from_axis() };
const size_t nlayers = object.layers().size();
if (nlayers == 0)
return;
// 1. Belt footprint: the union of each layer's slice clipped to that layer's
// own contact band. This is the object's bottom face, which on a belt is
// spread over every layer instead of sitting in layer 0.
ExPolygons footprint_acc;
// The first layer that touches the belt: where the leading-edge brim is cut.
const Layer *first_contact = nullptr;
for (size_t i = 0; i < nlayers; ++ i) {
const Layer &layer = *object.layers()[i];
if (layer.lslices.empty())
continue;
// print_z - height, not the previous layer's print_z: variable layer
// heights make the latter wrong.
coordf_t u_lo = bc.ctx.cutoff_u(layer.print_z - layer.height);
coordf_t u_hi = bc.ctx.cutoff_u(layer.print_z);
if (u_lo > u_hi)
std::swap(u_lo, u_hi);
BoundingBox bb = get_extents(layer.lslices);
bb.offset(scale_(1.));
const Polygon band = band_box(bb, bc.frame.from_axis, u_lo, u_hi);
if (band.empty())
continue;
ExPolygons contact = intersection_ex(layer.lslices, Polygons{ band });
if (contact.empty())
continue;
if (first_contact == nullptr)
first_contact = &layer;
expolygons_append(footprint_acc, std::move(contact));
}
const ExPolygons footprint = union_ex(footprint_acc);
if (footprint.empty())
return;
// 2. Brim region, offset in the flattened (true on-belt) metric.
const PrintObjectConfig &cfg = object.config();
bc.brim_flow = print.brim_flow();
const double flow_w = bc.brim_flow.scaled_spacing() * SCALING_FACTOR;
// Quantize to an even number of lines, as the plate brim does.
const coord_t width = scale_(std::floor(cfg.brim_width.value / flow_w / 2) * flow_w * 2);
const coord_t leading = scale_(cfg.leading_brim_length.value);
const coord_t lateral = scale_(cfg.extra_brim_width.value);
const coord_t gap = scale_(cfg.brim_object_gap.value);
// Belt printers collapse Auto / Mouse ear / Painted to outer-only: the auto width
// heuristic and flat ear discs have no meaning on a tilted plane. Leading-edge-only
// is an outer brim too; it is narrowed down to the first contact below.
const BrimType bt = cfg.brim_type.value;
const bool has_outer = bt == btOuterOnly || bt == btOuterAndInner
|| bt == btAutoBrim || bt == btEar || bt == btPainted
|| bt == btLeadingEdgeOnly;
const bool has_inner = bt == btInnerOnly || bt == btOuterAndInner;
bc.region = belt_unflatten(
belt_brim_region(belt_flatten(footprint, bc.frame), has_outer, has_inner,
width, gap, leading, lateral, bc.frame),
bc.frame);
if (bt == btLeadingEdgeOnly && first_contact != nullptr && ! bc.region.empty())
// The cut is the uphill edge of the first contact's band: everything past it
// belongs to later contacts. The first contact is the first layer that touches
// the belt (step 1), neither layers().front(), an empty lead-in layer, nor the
// first layer with geometry, which is an overhang's tip when the part overhangs
// its leading end: both lie ahead of the part.
bc.region = belt_brim_clip_leading_edge(bc.region, bc.frame, bc.ctx.cutoff_u(first_contact->print_z));
if (bc.region.empty())
return;
bc.region_bbox = get_extents(bc.region);
// 3. Line lattice. Fixed pitch in the flattened metric, anchored at the
// footprint's leading-most edge so lines stay collinear across
// disconnected islands and across the apron prologue.
bc.pitch_u = std::max<coord_t>(1, coord_t(bc.brim_flow.scaled_spacing() * bc.frame.cos_tilt()));
bc.in_plane_pitch = unscale<double>(bc.pitch_u) * bc.frame.u_stretch();
{
const BoundingBox fbb = get_extents(footprint);
const bool low_side = bc.frame.shear > 0.;
bc.u_anchor = bc.frame.from_axis == 0 ? (low_side ? fbb.min.x() : fbb.max.x())
: (low_side ? fbb.min.y() : fbb.max.y());
}
// 4. Bands coincident with an object layer.
std::vector<ExtrusionEntityCollection> by_layer(nlayers);
std::vector<ExPolygons> areas_by_layer(nlayers);
for (size_t i = 0; i < nlayers; ++ i) {
const Layer &layer = *object.layers()[i];
const Polygons obstacles = belt_brim_obstacles(print, object, bc.region_bbox, layer.print_z, 0.5 * layer.height);
belt_brim_band_paths(bc, layer.print_z, layer.height, obstacles, by_layer[i], areas_by_layer[i]);
}
// 5. Apron prologue: the part of the region downhill of the object's first
// layer, which has no object layer to ride on.
std::vector<BeltBrimBand> prologue;
{
const Layer &first = *object.layers().front();
const coordf_t h = first.height;
const bool low_side = bc.frame.shear > 0.;
const coord_t u_lead_s = bc.frame.from_axis == 0
? (low_side ? bc.region_bbox.min.x() : bc.region_bbox.max.x())
: (low_side ? bc.region_bbox.min.y() : bc.region_bbox.max.y());
const coordf_t u_lead = unscale<double>(u_lead_s);
// print_z at which the belt surface crosses the region's leading edge.
const coordf_t z_lead = bc.ctx.shear_factor() * u_lead
+ bc.ctx.floor_offset() + bc.ctx.z_shift();
if (h > EPSILON)
for (coordf_t z = first.print_z - h; z > z_lead - h; z -= h) {
const Polygons obstacles = belt_brim_obstacles(print, object, bc.region_bbox, z, 0.5 * h);
BeltBrimBand band;
band.print_z = z;
band.height = h;
belt_brim_band_paths(bc, z, h, obstacles, band.fills, band.areas);
if (! band.fills.empty())
prologue.emplace_back(std::move(band));
}
// Lowest Z first, so collect_layers_to_print sees them in print order.
std::reverse(prologue.begin(), prologue.end());
}
object.set_belt_brim(std::move(by_layer), std::move(areas_by_layer), std::move(prologue));
}
} // namespace Slic3r
+178
View File
@@ -0,0 +1,178 @@
#ifndef slic3r_BeltBrim_hpp_
#define slic3r_BeltBrim_hpp_
#include "ExPolygon.hpp"
#include "ExtrusionEntityCollection.hpp"
#include "Point.hpp"
#include "Polyline.hpp"
#include "libslic3r.h"
#include <cmath>
#include <vector>
// Belt-printer brim geometry.
//
// A belt printer slices in a ROTATED frame, so the belt surface is not the
// Z=0 bed plane but a tilted plane in slicing space:
//
// z_slicing(u) = shear * u + floor_offset + z_shift, u = X or Y
//
// where `shear == tan(tilt)` (SlicingParameters::belt_floor_shear_factor) and
// the axis is selected by SlicingParameters::belt_floor_from_axis. See
// Support/BeltFloorContext.hpp for the canonical accessors.
//
// Consequences that drive everything in this file:
//
// * A horizontal slicing layer touches the belt only along a narrow strip at
// its leading edge, `layer_height / shear` wide (~0.2 mm at 45 degrees).
// The object's belt footprint - its bottom face - is therefore spread over
// every layer, not contained in layer 0.
// * Distances measured in slicing XY are NOT on-belt distances: moving `du`
// along the shear axis travels `du / cos(tilt)` across the belt. So brim
// offsets have to be taken in a "flattened" space where the shear axis is
// stretched by `1 / cos(tilt)`, then mapped back.
// * Brim ahead of the part (downhill) lies at slicing Z BELOW the object's
// first layer, because the object's layer 0 is precisely its leading
// contact with the belt.
//
// Everything here is pure geometry on ExPolygons/Polylines so it can be unit
// tested without a Print. Keep user-visible strings out of this file: it is
// not listed in localization/i18n/list.txt.
namespace Slic3r {
// Tilt window within which the BELT plane, not the bed plane, is the adhesion
// surface. Below ~1 degree a belt is a flat bed as far as adhesion goes, and the
// contact band would be layer_height/sin(tilt) - tens of millimetres - so the
// ordinary plate brim is both correct and cheaper. Above ~85 degrees the whole
// brim compresses into a sliver and is not worth generating.
inline constexpr double BELT_BRIM_MIN_TILT_DEG = 1.;
inline constexpr double BELT_BRIM_MAX_TILT_DEG = 85.;
// Description of the tilted belt plane, reduced to what the brim geometry needs.
struct BeltBrimFrame
{
// tan(tilt). Sign selects which way is downhill.
double shear = 0.;
// 0 = X, 1 = Y. Matches BeltFloorContext::from_axis().
int from_axis = 1;
// 1 / cos(tilt). Stretch factor that turns a projected distance along
// `from_axis` into the true distance travelled across the belt.
double u_stretch() const { return std::sqrt(1. + shear * shear); }
// cos(tilt). The inverse mapping.
double cos_tilt() const { return 1. / this->u_stretch(); }
// Downhill is where the belt surface is lower, i.e. printed earlier, i.e.
// the leading edge of the part. For shear > 0 that is -u.
int downhill_sign() const { return shear > 0. ? -1 : +1; }
};
// Scale only the `from_axis` component by `factor`, rounding to nearest.
//
// Deliberately not MultiPoint::scale(fx, fy) / ExPolygon::scale(fx, fy): those
// truncate toward zero, which is asymmetric about the origin and loses up to a
// full coordinate unit per vertex on every round trip.
ExPolygons belt_scale_u(const ExPolygons &src, const BeltBrimFrame &frame, double factor);
Polylines belt_scale_u(const Polylines &src, const BeltBrimFrame &frame, double factor);
// Into / out of the space where Euclidean offsets equal true on-belt distances.
inline ExPolygons belt_flatten(const ExPolygons &src, const BeltBrimFrame &frame)
{ return belt_scale_u(src, frame, frame.u_stretch()); }
inline ExPolygons belt_unflatten(const ExPolygons &src, const BeltBrimFrame &frame)
{ return belt_scale_u(src, frame, frame.cos_tilt()); }
// Minkowski sum of `src` with the segment [0, t]: the region swept by sliding
// `src` along t. Used to grow the brim downhill for "extra brim width".
//
// Implemented as union_(P, P + t, {parallelogram per boundary edge}) over ALL
// contours including holes, with every parallelogram forced counter-clockwise
// so the non-zero fill rule closes holes narrower than t along the sweep
// direction. A hole survives exactly when it is wider than |t| measured along
// t - not when it is wider in its narrowest Euclidean direction.
ExPolygons sweep_ex(const ExPolygons &src, const Point &t);
// "Leading edge only": keep the part of a brim region (unflattened, slicing XY)
// at or downhill of the object's first contact with the belt, so the part is
// supported as it lands and nothing is printed alongside it afterwards. `u_cut`
// is the uphill edge of the first layer's contact band along `frame.from_axis`,
// in mm (BeltFloorContext::cutoff_u of the first layer); downhill is the side
// `frame.downhill_sign()` points to.
ExPolygons belt_brim_clip_leading_edge(const ExPolygons &region, const BeltBrimFrame &frame, coordf_t u_cut);
// Brim region for one already-flattened belt footprint. All lengths are scaled
// and measured in the flattened (true on-belt) metric.
//
// `has_outer` / `has_inner` are the resolved BrimType: belt printers collapse
// Auto / Mouse ear / Painted to outer-only, so the caller does that mapping and
// this function never needs PrintConfig.
//
// Two directional extras are applied to the footprint before the outer offset, so
// each one buys reach in one direction only:
//
// `leading` (leading_brim_length) sweeps the footprint DOWNHILL along the belt,
// so every leading-facing edge gains an apron ahead of it.
// `lateral` (extra_brim_width) sweeps it BOTH WAYS across the belt, widening
// the brim sideways without pushing it further ahead or behind.
//
// Neither is applied to the inner (hole) ring.
ExPolygons belt_brim_region(const ExPolygons &footprint_flat,
bool has_outer,
bool has_inner,
coord_t brim_width,
coord_t object_gap,
coord_t leading,
coord_t lateral,
const BeltBrimFrame &frame);
// Brim line positions for one layer band.
//
// Lines sit on a fixed lattice `u_anchor + k * pitch_u` so the on-belt spacing
// between neighbouring brim lines is constant regardless of how the lattice
// falls across layer bands. Snapping to band centres instead would quantise
// the spacing to whole bands and under-deposit by ~35% at 45 degrees.
//
// The band is half-open, [u_lo, u_hi), so every lattice point belongs to
// exactly one band: none duplicated at a boundary, none dropped. A band
// narrower than the pitch simply yields nothing; a band much wider (shallow
// tilt) yields several lines.
std::vector<coord_t> belt_brim_line_positions(coord_t u_lo,
coord_t u_hi,
coord_t pitch_u,
coord_t u_anchor);
// ---------------------------------------------------------------- pipeline
// One brim-only layer printed BEFORE the object's first layer, carrying the
// apron that has to be stuck to the belt ahead of the part.
//
// Deliberately not a Layer subclass. A synthetic Layer would inherit id()
// semantics that leak into initial-layer temperature selection, the spiral vase
// probe, gradual interpolation, avoid-crossing-perimeters and cooling, all of
// which key off Layer::id() == 0 or off a layer's regions. A plain record
// carries only what the emitter needs.
//
// `height` is the LAYER height, used for the Z move and ordering metadata only.
// Each extrusion path inside `fills` carries its own height, equal to that
// line's nozzle-to-belt clearance, which varies across the band.
struct BeltBrimBand
{
coordf_t print_z = 0.;
coordf_t height = 0.;
// erBrim paths in the object's local slicing frame, untranslated.
ExtrusionEntityCollection fills;
// Footprint of those paths, for the first-layer convex hull / bbox.
ExPolygons areas;
};
class PrintObject;
// Generate the belt brim for one object: fills its per-object-layer bands and
// its apron prologue. No-op unless PrintObject::has_belt_brim().
//
// Runs inside posSupportMaterial rather than the brim step, because the prologue
// print_z values must exist before ToolOrdering is built at psWipeTower.
void make_belt_brim(PrintObject &object);
} // namespace Slic3r
#endif // slic3r_BeltBrim_hpp_
+50
View File
@@ -0,0 +1,50 @@
#include "BeltGCode.hpp"
#include "GCodeWriter.hpp"
#include "GCode/BeltKinematics.hpp"
#include "BeltTransform.hpp"
#include "Print.hpp"
#include "Point.hpp"
#include "PrintConfig.hpp"
#include "libslic3r.h"
#include <cstdlib>
namespace Slic3r {
void BeltGCode::init_belt_writer(Print &print)
{
// Axis remap and build volume max are set by base GCode after init_belt_writer
// returns; set_kinematics() replays them, so install order does not matter.
install_belt_kinematics(m_writer, print.config());
m_writer.set_force_normal_lift(true);
}
void BeltGCode::write_belt_header(GCodeOutputStream &file, const Print &print)
{
const auto &full_cfg = print.full_print_config();
// Slicing rotation: the belt tilt (axis + angle) and the single source of truth
// for the physical tilt the G-code viewer uses to enable belt view.
file.write_format("; belt_slice_rotation = %s\n", full_cfg.opt_serialize("belt_slice_rotation").c_str());
file.write_format("; belt_slice_rotation_angle = %.1f\n", print.config().belt_slice_rotation_angle.value);
// Machine-frame transform: shear (cot) + scale (1/|sin|) derived from the belt
// tilt angle (or belt_frame_tilt_angle when decoupled).
file.write_format("; belt_frame_tilt_decouple = %d\n", print.config().belt_frame_tilt_decouple.value ? 1 : 0);
file.write_format("; belt_frame_tilt_angle = %.1f\n", print.config().belt_frame_tilt_angle.value);
}
void BeltGCode::on_set_origin(const PrintObject * /*obj*/, const Point & /*inst_shift*/)
{
// Matches the per-instance Z-offset added in PrintObjectSlice.cpp: transform
// the origin through the belt pipeline so that back_transform(T * origin) =
// origin (correct machine position). The back_transform applied during
// G-code emission is the inverse of the forward transform.
// Adjust origin: transform through belt forward pipeline so that
// the back-transform correctly recovers model-space positions.
Transform3d T = BeltTransformPipeline::build_forward_transform(m_config);
Vec2d cur_origin = this->origin();
Vec3d origin3d(cur_origin.x(), cur_origin.y(), 0.);
Vec3d adjusted = T.linear() * origin3d;
this->set_origin(Vec2d(adjusted.x(), adjusted.y()));
}
} // namespace Slic3r
+25
View File
@@ -0,0 +1,25 @@
#pragma once
#include "GCode.hpp"
#include "Point.hpp"
#include "Print.hpp"
namespace Slic3r {
// Belt-printer-specific GCode export.
//
// Inherits from GCode and overrides virtual hooks to:
// - Install a BeltKinematics on the GCodeWriter
// - Write belt configuration to the G-code header
// - Adjust the origin for global pre-slice transforms when switching instances
// (Arc fitting is disabled for belt printers by BeltKinematics::supports_arc_moves(),
// which the base GCode::should_disable_arc_fitting() consults -- no override needed.)
class BeltGCode : public GCode
{
protected:
void init_belt_writer(Print &print) override;
void write_belt_header(GCodeOutputStream &file, const Print &print) override;
void on_set_origin(const PrintObject *obj, const Point &inst_shift) override;
};
} // namespace Slic3r
+433
View File
@@ -0,0 +1,433 @@
// ORCA-Belt: backend of the belt purge tower (the belt replacement for the
// classic wipe/prime tower).
//
// Kept in its own translation unit so the belt-purge logic stays out of the way
// of unrelated upstream changes to Print.cpp / PrintObjectSlice.cpp and carries
// no regression risk for normal printers: none of these methods do anything
// unless the print is a belt printer with the belt purge tower enabled.
//
// Print::has_belt_purge_tower() - is the belt purge tower active?
// Print::_align_belt_purge_layers() - snap the prism's layer grid onto the
// printed objects' grid
// Print::_plan_belt_purge() - route filament-change purging into the
// prism (flush-into-objects), no wipe tower
// PrintObject::belt_shift_layer_grid() - shift a sliced layer grid
// PrintObject::belt_truncate_layers_above() - cancel the prism past the last swap
//
// (Declarations live in Print.hpp alongside the rest of the Print interface.)
#include "Print.hpp"
#include "PrintConfig.hpp"
#include "Exception.hpp"
#include "GCode/ToolOrdering.hpp"
#include "Layer.hpp"
#include "ExtrusionEntity.hpp"
#include "ExtrusionEntityCollection.hpp"
#include "I18N.hpp"
#include "format.hpp"
#include "LocalesUtils.hpp"
#include "libslic3r.h"
#include "BeltBrim.hpp"
#include "PrintBase.hpp"
#include <algorithm>
#include <cmath>
#include <limits>
#include <boost/log/trivial.hpp>
#include <cassert>
#include <cstddef>
#include <functional>
#include <utility>
#include <vector>
namespace Slic3r {
// Belt purge prism: purging after filament changes is routed into a sliced
// prism object via the flush-into-objects machinery instead of a wipe tower.
bool Print::has_belt_purge_tower() const
{
// Its own purge-tower "type", gated by the belt-only enable_belt_purge_tower
// option (not the classic enable_prime_tower).
if (!(m_config.belt_printer.value
&& m_config.enable_belt_purge_tower.value
&& !m_config.spiral_mode.value
&& m_config.filament_diameter.values.size() > 1))
return false;
return std::any_of(m_objects.begin(), m_objects.end(), [](const PrintObject *object) {
return object->config().belt_purge_tower_object.value;
});
}
// Belt mode: align ALL objects on the plate (the printed objects AND the purge
// prism) onto one common layer grid, so the prism can absorb every toolchange.
//
// After belt slicing each object's layer print_z carries a per-object global z
// offset (mesh-vertex-scan belt_z_shift + instance-Y-dependent terms), so
// objects at different belt-Y positions get layer grids with DIFFERENT residues
// (mod layer height). Purge marking looks absorbers up with
// get_layer_at_printz(lt.print_z, EPSILON), so a toolchange on object B only
// absorbs into the prism if the prism has a layer at B's print_z. Snapping only
// the prism to one object therefore worked for a single (assembled) multi-color
// object but failed with multiple separate objects — the prism could follow only
// one grid, and toolchanges on the others went unabsorbed ("multiple layer
// grids" warning).
//
// Fix: pick one reference grid (the tallest object) and shift every object onto
// it. Each shift is at most half a layer height — a sub-100µm move along the
// belt, the very same mechanism the per-object global_z_offset already uses, and
// it keeps each object internally consistent (belt_shift_layer_grid moves the
// object's layers, its support layers, and its belt floor together). Equal layer
// height across objects is enforced by Print::validate(), so once residues match
// every object steps on the same lattice {ref_offset + k*h} and every toolchange
// layer coincides with a prism layer.
void Print::_align_belt_purge_layers()
{
PrintObject *prism = nullptr;
for (PrintObject *po : m_objects)
if (po->config().belt_purge_tower_object.value && !po->layers().empty()) {
prism = po;
break;
}
if (prism == nullptr || prism->layers().empty())
return;
const double h = prism->config().layer_height.value;
if (h <= EPSILON)
return;
// Grid residue of an object's layer grid: identical for all of an object's
// layers above the first since they step by h.
auto grid_offset = [h](const PrintObject *po) -> double {
if (po->layers().empty())
return 0.;
const double z = po->layers().front()->print_z;
return z - std::floor(z / h) * h; // in [0, h)
};
// Reference grid: the tallest non-prism object (proxy for the object with
// the most toolchange layers — minimizes how far the rest must move).
const PrintObject *ref = nullptr;
double ref_top = -std::numeric_limits<double>::max();
for (const PrintObject *po : m_objects) {
if (po->config().belt_purge_tower_object.value || po->layers().empty())
continue;
const double top = po->layers().back()->print_z;
if (top > ref_top) {
ref_top = top;
ref = po;
}
}
if (ref == nullptr)
return;
const double ref_offset = grid_offset(ref);
// Snap every object (printed objects AND the prism) onto the reference grid.
for (PrintObject *po : m_objects) {
if (po->layers().empty())
continue;
double delta = ref_offset - grid_offset(po);
if (delta > 0.5 * h)
delta -= h;
else if (delta <= -0.5 * h)
delta += h;
po->belt_shift_layer_grid(delta); // no-op for the reference object (delta ~ 0)
}
}
// Belt mode replacement for _make_wipe_tower(): plan filament-change purging
// into the belt purge prism (and any other flush_into_* object) using the
// flush-into-objects machinery, without generating classic wipe tower G-code.
// The toolchange itself is emitted by GCode::set_extruder() via the
// change_filament_gcode macro; the overrides marked here make the new
// filament's first extrusions land in the purge prism.
void Print::_plan_belt_purge()
{
m_wipe_tower_data.clear();
// psWipeTower may be invalidated without posSlice (for example after a
// filament-map or tool-ordering change). Restore a prism shortened by the
// previous plan so a newly higher toolchange can use its original layers.
for (PrintObject *po : m_objects)
if (po->config().belt_purge_tower_object.value)
po->belt_undo_purge_plan();
// Must run before ToolOrdering is built: LayerTools merge per-object layer
// print_z values, and the prism only absorbs purge where its (snapped)
// layers coincide with the toolchange layers.
this->_align_belt_purge_layers();
const unsigned int number_of_extruders = (unsigned int) m_config.filament_colour.values.size();
// No initial priming extrusions: there is no tower to prime on.
m_wipe_tower_data.tool_ordering = ToolOrdering(*this, (unsigned int) -1, false);
m_wipe_tower_data.tool_ordering.sort_and_build_data(*this, (unsigned int) -1, false);
if (m_wipe_tower_data.tool_ordering.empty() || m_wipe_tower_data.tool_ordering.last_extruder() == unsigned(-1))
throw Slic3r::SlicingError("The print is empty. The model is not printable with current print settings.");
// Is there any filament change at all? Not ToolOrdering::has_wipe_tower(): that reads the
// FIRST layer's flag, and on a belt the first layer may be a brim apron band, which carries
// neither object nor support and so never gets the flag even when the print changes filament.
{
bool any_change = false;
unsigned int cur = m_wipe_tower_data.tool_ordering.first_extruder();
for (const auto &lt : m_wipe_tower_data.tool_ordering.layer_tools())
for (const unsigned int e : lt.extruders)
if (e != cur) { any_change = true; cur = e; }
if (! any_change)
return;
}
this->throw_if_canceled();
// Flush volumes per filament pair, mirroring the generic wipe tower path:
// full flush matrix for single extruder multi material with purging enabled,
// plain prime volume otherwise.
std::vector<float> flush_matrix(cast<float>(
get_flush_volumes_matrix(m_config.flush_volumes_matrix.values, 0, m_config.nozzle_diameter.values.size())));
std::vector<std::vector<float>> wipe_volumes;
for (unsigned int i = 0; i < number_of_extruders; ++i)
wipe_volumes.push_back(std::vector<float>(flush_matrix.begin() + i * number_of_extruders,
flush_matrix.begin() + (i + 1) * number_of_extruders));
const bool use_flush_matrix = m_config.purge_in_prime_tower && m_config.single_extruder_multi_material;
const float flush_multiplier = (float) m_config.flush_multiplier.get_at(0);
// Cancel the purge prism early: pre-scan the tool ordering for the highest
// print_z that actually has a toolchange, then drop the prism's layers above
// it so the tower stops at the last color swap (saves filament/time). This
// MUST happen before the marking loop below: ensure_perimeters_infills_order
// force-overrides the prism's extrusions on every layer (it is a dedicated
// flush object), so truncating afterwards would leave dangling overrides
// pointing into deleted layers.
{
// The tool ordering covers the WHOLE print, and the prism is a printed
// object in it. Left unbounded, the scan below sees the prism's own
// toolchanges on layers above every model object -- the prism runs past
// them by design (ramp/height compensation at the tilted ends) -- so
// last_tc_z lands at the prism's own top and the truncation cancels
// nothing. The tower ends up justifying its own existence.
//
// Nothing above the tallest printed object can require a color change,
// so bound the scan there. On MCTEST5 that is 197 toolchanges spanning
// z=154.00..193.20 with the tallest object topping out at 153.80, i.e.
// 39.4 mm of tower that no swap ever needed.
// Support layers count too: on a belt they can extend above the object's
// own top, and a toolchange there is a real one.
double obj_top_z = -1.;
for (const PrintObject *po : m_objects) {
if (po->config().belt_purge_tower_object.value)
continue;
if (!po->layers().empty())
obj_top_z = std::max(obj_top_z, po->layers().back()->print_z);
if (!po->support_layers().empty())
obj_top_z = std::max(obj_top_z, po->support_layers().back()->print_z);
}
double last_tc_z = -1.;
unsigned int cur_ext = m_wipe_tower_data.tool_ordering.first_extruder();
for (const auto &lt : m_wipe_tower_data.tool_ordering.layer_tools()) {
// layer_tools() is ordered by print_z ascending.
if (obj_top_z >= 0. && lt.print_z > obj_top_z + EPSILON)
break;
for (const unsigned int e : lt.extruders)
if (e != cur_ext) { last_tc_z = lt.print_z; cur_ext = e; }
}
// Deliberately NOT cancelling the prism outright when no object toolchange
// exists: belt_truncate_layers_above(0.) empties m_layers, and an object
// with zero layers is not something the rest of the pipeline expects. The
// GUI already declines to create a prism unless more than one filament is
// in use, so this case is a stale prism, not a hot path -- leave it whole
// rather than risk a zero-layer object.
if (last_tc_z >= 0.)
for (PrintObject *po : m_objects)
if (po->config().belt_purge_tower_object.value && !po->layers().empty()) {
po->belt_truncate_layers_above(last_tc_z);
break;
}
}
// The prism only absorbs purge at toolchange layers whose print_z coincides
// with one of its own layers.
PrintObject *prism_po = nullptr;
for (PrintObject *po : m_objects)
if (po->config().belt_purge_tower_object.value && !po->layers().empty()) { prism_po = po; break; }
float total_leftover = 0.f;
float worst_layer_leftover = 0.f;
double worst_layer_z = 0.;
unsigned int current_extruder_id = m_wipe_tower_data.tool_ordering.first_extruder();
for (auto &layer_tools : m_wipe_tower_data.tool_ordering.layer_tools()) {
float layer_leftover = 0.f;
for (const unsigned int extruder_id : layer_tools.extruders) {
if (extruder_id == current_extruder_id)
continue;
float volume_to_wipe = use_flush_matrix ?
wipe_volumes[current_extruder_id][extruder_id] * flush_multiplier :
(float) m_config.prime_volume;
float leftover = layer_tools.wiping_extrusions().mark_wiping_extrusions(*this, current_extruder_id, extruder_id,
volume_to_wipe);
layer_leftover += leftover;
current_extruder_id = extruder_id;
}
// Plastic saving: drop the prism's fills that no toolchange on this layer
// claimed. At this point the prism's OVERRIDDEN fills are exactly the
// purge; the rest would print as solid infill in the prism's own filament
// for nothing -- which is the whole prism on a layer with no toolchange
// (141 of 692 layers on MCTEST5 before the truncation fix). Perimeters are
// left alone so the bar keeps a continuous wall along the belt.
//
// Non-destructive: the entities are stashed with their positions and put
// back by belt_restore_dropped_fills() at the top of the next plan. An
// earlier version deleted them outright, which broke replanning when a
// later tool ordering needed what this one had not claimed -- that is why
// it was removed rather than kept.
if (prism_po != nullptr) {
const auto &we = layer_tools.wiping_extrusions();
prism_po->belt_drop_unclaimed_fills(
prism_po->get_layer_at_printz(layer_tools.print_z, EPSILON),
[&we, prism_po](const ExtrusionEntity *e) { return we.is_entity_overridden(e, prism_po, 0); });
}
layer_tools.wiping_extrusions().ensure_perimeters_infills_order(*this);
if (layer_leftover > 0.f) {
total_leftover += layer_leftover;
if (layer_leftover > worst_layer_leftover) {
worst_layer_leftover = layer_leftover;
worst_layer_z = layer_tools.print_z;
}
}
this->throw_if_canceled();
}
if (total_leftover > 1.f) {
this->active_step_add_warning(
PrintStateBase::WarningLevel::CRITICAL,
Slic3r::format(_u8L("The belt purge tower cannot absorb the full purge volume: %1% mm³ in total could not "
"be purged (worst layer: %2% mm³ at height %3%). The print may show color bleeding. "
"Increase the belt purge tower width, or reduce flushing volumes."),
int(std::ceil(total_leftover)), int(std::ceil(worst_layer_leftover)),
Slic3r::float_to_string_decimal_point(worst_layer_z, 2)));
}
}
// Belt mode: shift the sliced layer grid by delta. Mirrors the global_z_offset
// application in slice() — layer print_z and belt_floor_z_shift move together
// so belt floor clipping stays consistent with the shifted grid. Used by
// Print::_align_belt_purge_layers() to snap the purge prism onto the printed
// objects' layer grid; |delta| <= half a layer height, i.e. a sub-layer shift
// of the prism along the belt.
void PrintObject::belt_shift_layer_grid(double delta)
{
if (std::abs(delta) < EPSILON)
return;
for (Layer *layer : m_layers)
layer->print_z += delta;
for (SupportLayer *layer : m_support_layers)
layer->print_z += delta;
// The brim's apron bands below the first layer carry their own print_z.
for (BeltBrimBand &band : m_belt_brim_prologue)
band.print_z += delta;
m_slicing_params.belt_floor_z_shift += delta;
// The grid stays shifted across a support-only or brim-only change (posSlice does
// not rerun), so everything slice() derived from it has to follow: the cached floor
// that update_slicing_parameters() restores, and the global offset the organic
// support layers and the adaptive infill octree are placed with. Left alone, the
// next alignment finds a delta of 0 and the floor and the supports sit up to half
// a layer off the grid, unlike a fresh slice.
if (m_belt_floor_z_shift_cache_valid)
m_belt_floor_z_shift_cached += delta;
m_belt_global_z_offset += delta;
}
// Belt mode: drop layers strictly above z (used to cancel the purge prism early
// once there are no more toolchanges above z, so the tower stops at the last
// color swap instead of wasting filament up the rest of the belt). Each layer's
// cross-section is already sliced, so removing upper layers does not affect the
// last toolchange's coverage. Deletes the Layer objects and clears the new top
// layer's upper-layer link. Returns the number of layers removed.
size_t PrintObject::belt_truncate_layers_above(coordf_t z)
{
// A repeated plan always starts from the restored full layer set.
assert(m_belt_truncated_layers.empty());
size_t keep = m_layers.size();
while (keep > 0 && m_layers[keep - 1]->print_z > z + EPSILON)
--keep;
if (keep >= m_layers.size())
return 0;
const size_t removed = m_layers.size() - keep;
m_belt_truncated_layers.assign(m_layers.begin() + keep, m_layers.end());
m_layers.resize(keep);
if (!m_layers.empty())
m_layers.back()->upper_layer = nullptr;
return removed;
}
// Plastic saving on the purge prism: keep only the fills a toolchange claimed.
//
// Called per layer from _plan_belt_purge(), after the real-purge marking and
// BEFORE ensure_perimeters_infills_order() -- that pass force-overrides every
// remaining fill on the prism (it is a dedicated flush object), so afterwards
// everything looks claimed and nothing could be distinguished.
size_t PrintObject::belt_drop_unclaimed_fills(Layer *layer, const std::function<bool(const ExtrusionEntity*)> &claimed)
{
if (layer == nullptr)
return 0;
size_t dropped = 0;
for (size_t ri = 0; ri < layer->regions().size(); ++ri) {
LayerRegion *lr = layer->get_region(ri);
auto &ents = lr->fills.entities;
ExtrusionEntitiesPtr keep;
keep.reserve(ents.size());
for (size_t i = 0; i < ents.size(); ++i) {
if (claimed(ents[i])) {
keep.emplace_back(ents[i]);
} else {
// Stash with its original index so the restore is exact.
m_belt_dropped_fills.push_back(BeltDroppedFill{ layer, ri, i, ents[i] });
++dropped;
}
}
ents = std::move(keep);
}
return dropped;
}
void PrintObject::belt_restore_dropped_fills()
{
if (m_belt_dropped_fills.empty())
return;
// Ascending index per (layer, region): inserting in that order lands every
// entity back at its original position, because each insertion shifts only
// the entries after it, which are themselves still to be inserted.
std::stable_sort(m_belt_dropped_fills.begin(), m_belt_dropped_fills.end(),
[](const BeltDroppedFill &a, const BeltDroppedFill &b) {
if (a.layer != b.layer) return a.layer < b.layer;
if (a.region_idx != b.region_idx) return a.region_idx < b.region_idx;
return a.index < b.index;
});
for (const BeltDroppedFill &d : m_belt_dropped_fills) {
auto &ents = d.layer->get_region(d.region_idx)->fills.entities;
ents.insert(ents.begin() + std::min(d.index, ents.size()), d.entity);
}
m_belt_dropped_fills.clear();
}
void PrintObject::belt_restore_truncated_layers()
{
if (m_belt_truncated_layers.empty())
return;
m_layers.insert(m_layers.end(), m_belt_truncated_layers.begin(), m_belt_truncated_layers.end());
m_belt_truncated_layers.clear();
for (size_t i = 0; i < m_layers.size(); ++i) {
m_layers[i]->lower_layer = i == 0 ? nullptr : m_layers[i - 1];
m_layers[i]->upper_layer = i + 1 < m_layers.size() ? m_layers[i + 1] : nullptr;
}
}
} // namespace Slic3r
+82
View File
@@ -0,0 +1,82 @@
#include "BeltSliceStrategy.hpp"
#include "Model.hpp"
#include "BeltTransform.hpp"
#include "Point.hpp"
#include "PrintConfig.hpp"
#include <limits>
#include <algorithm>
namespace Slic3r {
void BeltSliceStrategy::apply_preslice_transforms(Transform3d &trafo,
const PrintConfig &config,
const ModelVolumePtrs &model_volumes,
double *out_belt_min_z)
{
// 1. Belt rotation — the sole mesh-side belt transform (matching
// BeltTransformPipeline::build_forward_transform). Only active in
// belt-printer mode.
bool has_rotation = false;
if (config.belt_printer.value) {
const Matrix3d rot = BeltTransformPipeline::build_rotation_matrix(config, &has_rotation);
if (has_rotation) {
Transform3d belt_xform = Transform3d::Identity();
belt_xform.linear() = rot;
trafo = belt_xform * trafo;
}
}
if (!has_rotation)
return;
// 2. Z-shift — detect if the mesh clips below the build plate after the
// transforms and lift it. Each mesh vertex must be brought into object space
// via mv->get_matrix() before applying the full trafo (which is in object
// space). Missing this on assemblies (where per-volume get_matrix() positions
// each volume within the object) would compute min_z against mesh-local vertex
// coordinates rather than object-space coordinates, so volumes translated along
// the slicer's Z axis would be silently excluded from the bound check.
//
// The lift is measured to the lowest point of the SUPPORT region, not of the
// mesh: the belt floor (z = shear * u in this rotated frame, u the from-axis
// coordinate) runs below every vertex, and under the leading end of an
// overhang it lies below the lowest vertex by up to the overhang's length
// times the shear. Supports have to reach that floor, and every support
// generator works in layers at z >= 0, so z = 0 has to be the lowest floor
// point under the footprint. The layers between it and the first vertex
// come out empty, which belt slicing already tolerates (the bottom corner
// of a tilted part is a point). Vertices on the belt have z == floor, so
// for a part resting on the belt this is simply the floor at its leading
// extreme, less the frame margin (see BeltTransformPipeline::frame_margin).
BeltTransformPipeline::BeltFloorParams floor;
const bool has_floor = BeltTransformPipeline::floor_shear(config, floor);
double min_z = std::numeric_limits<double>::max();
for (const ModelVolume *mv : model_volumes) {
if (!mv->is_model_part()) continue;
Transform3d vol_trafo = trafo * mv->get_matrix();
const auto &its = mv->mesh().its;
for (const stl_vertex &v : its.vertices) {
Vec3d vm = v.cast<double>();
Vec3d pt = vol_trafo * vm;
min_z = std::min(min_z, pt.z());
if (has_floor)
min_z = std::min(min_z, floor.shear_factor * (floor.from_axis == 0 ? pt.x() : pt.y()));
}
}
if (has_floor && min_z != std::numeric_limits<double>::max())
min_z -= BeltTransformPipeline::frame_margin(floor);
const double z_shift_val = (min_z < 0. && min_z != std::numeric_limits<double>::max()) ? -min_z : 0.;
if (z_shift_val > 0.) {
Transform3d z_shift = Transform3d::Identity();
z_shift.matrix()(2, 3) = z_shift_val;
trafo = z_shift * trafo;
}
// out_belt_min_z is only meaningful in belt mode.
if (out_belt_min_z && config.belt_printer.value) {
*out_belt_min_z = (min_z != std::numeric_limits<double>::max()) ? min_z : 0.;
}
}
} // namespace Slic3r
+34
View File
@@ -0,0 +1,34 @@
#pragma once
#include "libslic3r.h"
#include "Point.hpp"
#include "BeltTransform.hpp"
#include "PrintConfig.hpp"
#include "Model.hpp"
namespace Slic3r {
// Belt printer pre-slice transform strategy.
//
// Composes, in order, the mesh transforms applied before slicing on a belt printer:
// 1. Belt rotation (the sole mesh-side belt transform; shear & scale are a
// g-code-side stage, see MachineFrameTransform)
// 2. Per-object Z-shift that lifts the mesh so its slicing frame starts at the
// belt below its footprint
//
// Isolates this belt-specific logic from the generic slicing pipeline in
// PrintObjectSlice.cpp.
class BeltSliceStrategy
{
public:
// Apply the belt rotation + Z-shift to `trafo` in place. No-op when no belt
// rotation is configured.
//
// out_belt_min_z (if non-null) receives the minimum mesh Z after the transforms.
static void apply_preslice_transforms(Transform3d &trafo,
const PrintConfig &config,
const ModelVolumePtrs &model_volumes,
double *out_belt_min_z = nullptr);
};
} // namespace Slic3r
+153
View File
@@ -0,0 +1,153 @@
#include "BeltTransform.hpp"
#include "Model.hpp"
#include "BoundingBox.hpp"
#include "Config.hpp"
#include "Geometry.hpp"
#include "Point.hpp"
#include "PrintConfig.hpp"
#include "libslic3r.h"
#include <limits>
#include <algorithm>
#include <cmath>
#include <cstdlib>
namespace Slic3r {
// ---- Matrix builders ------------------------------------------------------
Matrix3d BeltTransformPipeline::build_rotation_matrix(const PrintConfig &config, bool *has_rot_out)
{
BeltRotationAxis axis = config.belt_slice_rotation.value;
double angle_deg = config.belt_slice_rotation_angle.value;
bool active = axis != BeltRotationAxis::None && std::abs(angle_deg) > EPSILON;
if (has_rot_out) *has_rot_out = active;
if (!active)
return Matrix3d::Identity();
double angle_rad = Geometry::deg2rad(angle_deg);
Vec3d unit_axis;
switch (axis) {
case BeltRotationAxis::X: unit_axis = Vec3d::UnitX(); break;
case BeltRotationAxis::Y: unit_axis = Vec3d::UnitY(); break;
case BeltRotationAxis::Z: unit_axis = Vec3d::UnitZ(); break;
default: return Matrix3d::Identity();
}
return Eigen::AngleAxisd(angle_rad, unit_axis).toRotationMatrix();
}
Transform3d BeltTransformPipeline::build_forward_transform(const PrintConfig &config)
{
// Mesh-side belt transform: the rotation. (Shear & scale are a g-code-side
// stage, not part of the mesh transform.)
Transform3d combined = Transform3d::Identity();
combined.linear() = build_rotation_matrix(config);
return combined;
}
// ---- Belt floor parameters ------------------------------------------------
namespace {
// Belt floor in the rotated slicer frame: the image of z_machine = 0 under R.
// R(+α, X): point (·, y, 0) → (·, cos α · y, sin α · y) ⇒ z = tan(α) · y_s
// R(+α, Y): point (x, ·, 0) → (cos α · x, ·, -sin α · x) ⇒ z = -tan(α) · x_s
// R(+α, Z): point (·, ·, 0) → (·, ·, 0); no tilt → no floor
void belt_floor_shear(BeltRotationAxis rot_axis, double angle_rad, BeltTransformPipeline::BeltFloorParams &out)
{
double sin_a = std::sin(angle_rad), cos_a = std::cos(angle_rad);
switch (rot_axis) {
case BeltRotationAxis::X:
out.shear_factor = (std::abs(cos_a) > EPSILON) ? sin_a / cos_a : 0.;
out.from_axis = 1; // Y
break;
case BeltRotationAxis::Y:
out.shear_factor = (std::abs(cos_a) > EPSILON) ? -sin_a / cos_a : 0.;
out.from_axis = 0; // X
break;
case BeltRotationAxis::Z:
default:
out.shear_factor = 0.0;
out.from_axis = 1;
break;
}
}
// Z of the belt floor directly under a point of the rotated (unshifted) frame.
inline double belt_floor_z(const BeltTransformPipeline::BeltFloorParams &fp, const Vec3d &pt)
{
return fp.shear_factor * (fp.from_axis == 0 ? pt.x() : pt.y());
}
BeltTransformPipeline::BeltHeightResult compute_belt_height_and_floor_impl(
const PrintConfig &config, const BoundingBoxf3 &bb, double original_height)
{
BeltTransformPipeline::BeltHeightResult result;
result.object_height = original_height;
// The mesh rotation (the sole mesh-side belt transform).
const BeltRotationAxis rot_axis = config.belt_slice_rotation.value;
const double rot_angle = config.belt_slice_rotation_angle.value;
bool has_rotation = rot_axis != BeltRotationAxis::None && std::abs(rot_angle) > EPSILON;
if (!has_rotation)
return result;
// Rotation path: sweep the 8 bbox corners through R to get the rotated height,
// then derive the belt floor (the image of machine-Z = 0 under R).
double angle_rad = Geometry::deg2rad(rot_angle);
Vec3d unit_axis;
switch (rot_axis) {
case BeltRotationAxis::X: unit_axis = Vec3d::UnitX(); break;
case BeltRotationAxis::Y: unit_axis = Vec3d::UnitY(); break;
case BeltRotationAxis::Z: unit_axis = Vec3d::UnitZ(); break;
default: unit_axis = Vec3d::UnitX(); break;
}
Matrix3d R = Eigen::AngleAxisd(angle_rad, unit_axis).toRotationMatrix();
belt_floor_shear(rot_axis, angle_rad, result.floor_params);
// The slicing frame starts at the lowest point of the support region: the
// lowest belt-floor point under the footprint, not the lowest vertex. The
// belt under the leading end of an overhang lies below every vertex of the
// part, and supports have to be able to reach it (see
// BeltSliceStrategy::apply_preslice_transforms for the exact vertex-scan
// counterpart of this bbox estimate).
double min_rz = std::numeric_limits<double>::max();
double max_rz = std::numeric_limits<double>::lowest();
for (int i = 0; i < 8; ++i) {
Vec3d c((i & 1) ? bb.max.x() : bb.min.x(),
(i & 2) ? bb.max.y() : bb.min.y(),
(i & 4) ? bb.max.z() : bb.min.z());
Vec3d rc = R * c;
double z = rc.z();
min_rz = std::min(min_rz, z);
max_rz = std::max(max_rz, z);
min_rz = std::min(min_rz, belt_floor_z(result.floor_params, rc));
}
min_rz -= BeltTransformPipeline::frame_margin(result.floor_params);
result.object_height = max_rz - min_rz;
result.floor_params.z_shift = bb.min.z() + ((min_rz < 0.) ? -min_rz : 0.);
return result;
}
} // anonymous namespace
BeltTransformPipeline::BeltHeightResult BeltTransformPipeline::compute_belt_height_and_floor(
const PrintConfig &config, const BoundingBoxf3 &bbox, double original_height)
{
return compute_belt_height_and_floor_impl(config, bbox, original_height);
}
bool BeltTransformPipeline::floor_shear(const PrintConfig &config, BeltFloorParams &out)
{
out = BeltFloorParams{};
const BeltRotationAxis rot_axis = config.belt_slice_rotation.value;
const double rot_angle = config.belt_slice_rotation_angle.value;
if (rot_axis == BeltRotationAxis::None || std::abs(rot_angle) <= EPSILON)
return false;
belt_floor_shear(rot_axis, Geometry::deg2rad(rot_angle), out);
return std::abs(out.shear_factor) > EPSILON;
}
} // namespace Slic3r
+132
View File
@@ -0,0 +1,132 @@
#pragma once
#include "libslic3r.h"
#include "Point.hpp"
#include "BoundingBox.hpp"
#include "PrintConfig.hpp"
#include "Geometry.hpp"
#include "Config.hpp"
#include <cmath>
namespace Slic3r {
class ModelObject;
// Shared belt-printer transform math.
//
// The pre-slice pipeline applied in PrintObjectSlice.cpp is:
// trafo_out = z_shift * rotation * trafo_in
//
// Rotation is the sole mesh-side belt transform; shear & scale are applied
// to the g-code instead (see MachineFrameTransform). This class provides the
// building blocks so every call site uses the same implementation. z_shift is
// object-dependent (computed from mesh vertex bounds) and is NOT included in
// build_forward_transform(). The machine-frame shear/scale is derived directly
// from the tilt angle in MachineFrameTransform and no longer lives here.
//
// Design note: this mesh-rotation approach replaced an earlier pre-shear
// method (now removed). While that initial pre-shear method was instrumental
// in getting belt printer slicing off the ground in the first place, its place is
// in the past. A big thank you goes to the Unlayered3D team, who recommended
// switching to a pre-slice rotation stage instead. Doing so keeps the slicing
// operation isometric — no distortion of the sliced geometry — while the
// non-orthogonal machine-axis compensation is confined to a g-code-side shear/scale
// derived from the same tilt angle.
//
// This fixed a number of issues, including several issues noticed by hotcubcar
// regarding adaptive infills not working, gyroid becoming anisotropic, and more
// that were all mostly resolved as a result of the switch.
//
// This also means that the pre-slice rotation transform methodology can be used
// more cleanly on non-belt printers.
// - HarrierPigeon (Joseph Robertson)
class BeltTransformPipeline
{
public:
// ---- Identity checks --------------------------------------------------
// Whether the G-code axis remap applies at all. The remap fields are only
// offered in the belt printer group, so a value left in a profile must not
// change a non-belt print: with belt mode off every belt-only key is a no-op.
// This is the one place to widen if a non-belt use ever needs them.
static bool axis_remap_enabled(const PrintConfig &config) { return config.belt_printer.value; }
static bool has_rotation(const PrintConfig &config)
{
return config.belt_slice_rotation.value != BeltRotationAxis::None &&
std::abs(config.belt_slice_rotation_angle.value) > EPSILON;
}
// Physical belt tilt derived from the slicing rotation — the single source of
// truth for bed rendering, support gravity tilt and the bed-exclusion
// projection. Returns the tilt magnitude in degrees split onto the X and Y
// build-plate tilt axes according to the rotation axis:
// rotation about X → tilt_x = angle (gantry tilts in the YZ plane)
// rotation about Y → tilt_y = angle (gantry tilts in the XZ plane)
// rotation about Z / None → no tilt (in-plane spin doesn't tilt the belt)
// The magnitude uses abs(angle) so a negative rotation still reports a positive
// physical tilt.
struct PhysicalTilt { double tilt_x_deg = 0.; double tilt_y_deg = 0.; };
static PhysicalTilt physical_tilt(BeltRotationAxis axis, double angle_deg)
{
PhysicalTilt t;
double mag = std::abs(angle_deg);
switch (axis) {
case BeltRotationAxis::X: t.tilt_x_deg = mag; break;
case BeltRotationAxis::Y: t.tilt_y_deg = mag; break;
default: break; // Z / None: no physical tilt
}
return t;
}
// ---- Matrix builders --------------------------------------------------
// Build the 3x3 rotation matrix from belt_slice_rotation* config.
// Returns Identity if rotation axis is None or angle is ~0.
// Also sets has_rot_out if non-null.
static Matrix3d build_rotation_matrix(const PrintConfig &config, bool *has_rot_out = nullptr);
// Forward transform (the rotation) — the mesh-side belt transform that
// BeltSliceStrategy applies and BeltBackTransform inverts.
// Does NOT include the per-object Z-shift.
static Transform3d build_forward_transform(const PrintConfig &config);
// ---- Belt floor parameters --------------------------------------------
struct BeltFloorParams {
double shear_factor = 0.0;
int from_axis = 1;
double z_shift = 0.0;
};
// Shear factor and from-axis of the belt floor in the rotated slicer frame
// (z_floor = shear_factor * u, u = the from-axis coordinate), for the
// rotation the config selects. z_shift is left at 0. Returns false (and
// zero shear) when the config has no tilt.
static bool floor_shear(const PrintConfig &config, BeltFloorParams &out);
// How far below the lowest belt-floor point under the footprint the slicing
// frame starts, in slicing Z. A support column meeting the belt is wider at
// its base than at its tip, so under a leading overhang the base reaches ahead
// of the part along the belt, and the layers that trim it to the belt plane
// lie below that lowest point: 10 mm along the belt.
static double frame_margin(const BeltFloorParams &fp) { return 10. * std::abs(fp.shear_factor); }
// Result of computing belt height + floor params.
struct BeltHeightResult {
double object_height; // Effective object height after shear/scale
BeltFloorParams floor_params;
};
// Compute effective object height and belt floor parameters from config
// and the object's bounding box. original_height is the input height
// (bb.size().z() or model_object.max_z()).
static BeltHeightResult compute_belt_height_and_floor(
const PrintConfig &config, const BoundingBoxf3 &bbox,
double original_height);
};
} // namespace Slic3r
+14 -1
View File
@@ -474,7 +474,9 @@ static ExPolygons outer_inner_brim_area(const Print& print,
const bool use_brim_ears = object->config().brim_type == btPainted;
const bool use_inner_brim_ears = (use_auto_brim_ears || use_brim_ears) && !object->config().brim_ears_outer_only.value;
const bool has_inner_brim = brim_type == btInnerOnly || brim_type == btOuterAndInner || use_inner_brim_ears;
const bool has_outer_brim = brim_type == btOuterOnly || brim_type == btOuterAndInner || brim_type == btAutoBrim || use_auto_brim_ears || use_brim_ears;
// btLeadingEdgeOnly is a belt-printer mode; on a flat bed there is no leading
// edge, so it degrades to an ordinary outer brim rather than silently to none.
const bool has_outer_brim = brim_type == btOuterOnly || brim_type == btOuterAndInner || brim_type == btAutoBrim || brim_type == btLeadingEdgeOnly || use_auto_brim_ears || use_brim_ears;
coord_t ear_detection_length = scale_(object->config().brim_ears_detection_length.value);
coordf_t brim_ears_max_angle = object->config().brim_ears_max_angle.value;
//ORCA: Select brim base slices from EFC-compensated outline when enabled.
@@ -889,6 +891,17 @@ void make_brim(const Print& print, PrintTryCancel try_cancel, Polygons& islands_
std::vector<unsigned int>& printExtruders,
std::map<ObjectInstanceID, ExPolygons>* objectBrimAreasByInstanceOut)
{
// Belt printers never use the flat plate brim.
//
// With a tilted belt the brim has to be laid onto the belt plane over many layers,
// which BeltBrim.cpp does during posSupportMaterial. With an untilted belt this
// could in principle fall through and produce an ordinary brim, but it would never
// reach the G-code: the plate brim is emitted out of skirt_brim_groups(), which
// _make_skirt() builds, and that returns early for every belt printer. Running the
// generator anyway would just burn time on geometry nobody prints.
if (print.config().belt_printer.value)
return;
std::map<ObjectInstanceID, ExPolygons> brimAreaMap;
Flow flow = print.brim_flow();
ExPolygons islands_area_ex = outer_inner_brim_area(print,
+1 -1
View File
@@ -84,7 +84,7 @@ public:
indexed_triangle_set bounding_mesh(bool scale=true) const;
// Center of the print bed, unscaled.
Vec2d bed_center() const { return to_2d(m_bboxf.center()); }
Vec2d bed_center() const { return get_extents(m_bed_shape).center(); }
// Convex hull of polygon(), scaled.
const Polygon& convex_hull() const { return m_convex_hull; }
// Smallest enclosing circle of polygon(), scaled.
+19
View File
@@ -85,6 +85,15 @@ set(lisbslic3r_sources
BoundingBox.hpp
BridgeDetector.cpp
BridgeDetector.hpp
BeltBrim.cpp
BeltBrim.hpp
BeltGCode.cpp
BeltGCode.hpp
BeltPurge.cpp
BeltSliceStrategy.cpp
BeltSliceStrategy.hpp
BeltTransform.cpp
BeltTransform.hpp
Brim.cpp
BrimEarsPoint.hpp
Brim.hpp
@@ -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
+5
View File
@@ -403,6 +403,11 @@ inline void translate(ExPolygons &expolys, const Point &p) {
expoly.translate(p);
}
inline void translate(Polygons &polys, const Point &p) {
for (Polygon &poly : polys)
poly.translate(p);
}
inline void polygons_append(Polygons &dst, const ExPolygon &src)
{
dst.reserve(dst.size() + src.holes.size() + 1);
+7 -2
View File
@@ -314,8 +314,13 @@ void AMFParserContext::startElement(const char *name, const char **atts)
case 2:
if (strcmp(name, "metadata") == 0) {
if (m_path[1] == NODE_TYPE_MATERIAL || m_path[1] == NODE_TYPE_OBJECT) {
m_value[0] = get_attribute(atts, "type");
node_type_new = NODE_TYPE_METADATA;
const char *type = get_attribute(atts, "type");
if (type == nullptr)
this->stop();
else {
m_value[0] = type;
node_type_new = NODE_TYPE_METADATA;
}
}
}/* else if (strcmp(name, "layer_config_ranges") == 0 && m_path[1] == NODE_TYPE_OBJECT)
node_type_new = NODE_TYPE_LAYER_CONFIG;*/
+51 -16
View File
@@ -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 &current_objects)
bool _BBS_3MF_Importer::_generate_current_object_list(std::vector<Component> &sub_objects, Id object_id, IdToCurrentObjectMap &current_objects)
{
// A cycle in the component graph would expand forever, and an acyclic graph can still expand
// exponentially, so bound the number of component references queued. Checking before they are
// queued bounds the work list itself, whatever the fan-out. A valid file over the budget is
// rejected too, but the budget is way above the component references of any real object.
static constexpr size_t max_components = 100000;
std::list<std::pair<Component, Transform3d>> id_list;
id_list.push_back(std::make_pair(Component(object_id, Transform3d::Identity()), Transform3d::Identity()));
size_t num_components = 0;
while (!id_list.empty())
{
auto current_item = id_list.front();
@@ -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
View File
File diff suppressed because it is too large Load Diff
+130 -8
View File
@@ -7,6 +7,7 @@
#include "Print.hpp"
#include "libslic3r.h"
#include "GCodeWriter.hpp"
#include "GCode/BeltKinematics.hpp"
#include "Layer.hpp"
#include "Point.hpp"
#include "PlaceholderParser.hpp"
@@ -40,13 +41,16 @@
#include <memory>
#include <map>
#include <unordered_map>
#include <optional>
#include <set>
#include <string>
#include <cfloat>
#include <vector>
#include <utility>
#include <cmath>
#include "BoundingBox.hpp"
#include "Polyline.hpp"
#include "BeltBrim.hpp"
namespace Slic3r { class ExtrusionEntityCollection; }
@@ -249,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;
+33
View File
@@ -0,0 +1,33 @@
#include "BeltBackTransform.hpp"
#include "../BeltTransform.hpp"
#include "../Point.hpp"
#include "../PrintConfig.hpp"
namespace Slic3r {
bool BeltBackTransform::init_from_config(const PrintConfig &config)
{
m_active = false;
m_inverse = Transform3d::Identity();
if (!config.belt_printer.value)
return false;
// Build the forward pipeline (the rotation) and store its inverse.
Transform3d forward = BeltTransformPipeline::build_forward_transform(config);
if (forward.isApprox(Transform3d::Identity()))
return false;
m_inverse = forward.inverse();
m_active = true;
return true;
}
Vec3d BeltBackTransform::apply(const Vec3d &pos) const
{
if (!m_active)
return pos;
return m_inverse * pos;
}
} // namespace Slic3r
+38
View File
@@ -0,0 +1,38 @@
#ifndef slic3r_BeltBackTransform_hpp_
#define slic3r_BeltBackTransform_hpp_
#include "../libslic3r.h"
#include "../Point.hpp"
#include "../PrintConfig.hpp"
namespace Slic3r {
// Reverses the pre-slice rotation that PrintObjectSlice.cpp applies to belt
// printer geometry, converting G-code coordinates from the sliced (rotated)
// frame back to the machine's real coordinate space.
//
// Initialized once from PrintConfig, then applied per-point in
// BeltKinematics::to_machine() before axis remapping.
//
// Active on belt printers with a non-identity pre-slice rotation.
class BeltBackTransform
{
public:
BeltBackTransform() = default;
// Initialize from belt printer config. Rebuilds the same pre-slice rotation
// as PrintObjectSlice.cpp and precomputes the affine inverse. Returns true if a non-identity back-transform was computed.
bool init_from_config(const PrintConfig &config);
// Apply the inverse transform to a point. Returns pos unchanged if
// no back-transform is active.
Vec3d apply(const Vec3d &pos) const;
private:
bool m_active = false;
Transform3d m_inverse = Transform3d::Identity();
};
} // namespace Slic3r
#endif // slic3r_BeltBackTransform_hpp_
+29
View File
@@ -0,0 +1,29 @@
#include "BeltKinematics.hpp"
#include "../BeltTransform.hpp"
#include "../PrintConfig.hpp"
#include "../GCodeWriter.hpp"
#include "../Point.hpp"
#include <memory>
namespace Slic3r {
BeltKinematics::BeltKinematics(const PrintConfig &config, bool world_coordinates)
: m_world_coordinates(world_coordinates)
{
m_back_transform.init_from_config(config);
m_machine_frame.init_from_config(config);
}
Vec3d BeltKinematics::to_machine(const Vec3d &p) const
{
const Vec3d after_back = m_world_coordinates ? p : m_back_transform.apply(p);
const Vec3d after_remap = this->apply_axis_remap(after_back);
return m_machine_frame.apply(after_remap);
}
void install_belt_kinematics(GCodeWriter &writer, const PrintConfig &config, bool world_coordinates)
{
writer.set_kinematics(std::make_unique<BeltKinematics>(config, world_coordinates));
}
} // namespace Slic3r
+59
View File
@@ -0,0 +1,59 @@
#ifndef slic3r_BeltKinematics_hpp_
#define slic3r_BeltKinematics_hpp_
#include "MachineKinematics.hpp"
#include "BeltBackTransform.hpp"
#include "MachineFrameTransform.hpp"
#include "../Point.hpp"
namespace Slic3r {
class PrintConfig;
class GCodeWriter;
// Belt-printer machine frame.
//
// Forward order, as applied per emitted point:
// machine = MachineFrameTransform( axis_remap( BeltBackTransform( logical ) ) )
//
// i.e. the slicer->world back-transform runs FIRST and the machine-frame
// shear/scale LAST, so the latter acts as a global linear transform on the
// already-placed coordinates.
//
// world_coordinates mode (the PA line / PA pattern calibration generators)
// treats the incoming point as already relative to the belt surface -- X across,
// Y along the belt, Z above it -- and therefore skips the back-transform while
// keeping the remap and the machine frame. It is a different coordinate map, not
// a writer mode, which is why it is fixed at construction.
class BeltKinematics : public CartesianKinematics
{
public:
explicit BeltKinematics(const PrintConfig &config, bool world_coordinates = false);
Vec3d to_machine(const Vec3d &p) const override;
// A belt writer has always emitted full XYZ on every move, whether or not any
// individual stage reports itself active. Making this conditional would change
// emitted G-code for an identity-transform belt configuration.
bool must_emit_all_axes() const override { return true; }
bool suppress_lift_at_unknown_position() const override { return true; }
// The machine frame shears and scales, so a circle is an ellipse in machine
// coordinates and G2/G3 cannot describe it.
bool supports_arc_moves() const override { return false; }
private:
BeltBackTransform m_back_transform;
MachineFrameTransform m_machine_frame;
bool m_world_coordinates { false };
};
// Install a belt machine frame on any GCodeWriter. Any axis remap and build
// volume already configured on the writer are carried over, so this may be
// called before or after those setters. Re-calling it with a different
// world_coordinates value swaps the map (used around the PA line generator).
void install_belt_kinematics(GCodeWriter &writer, const PrintConfig &config,
bool world_coordinates = false);
} // namespace Slic3r
#endif // slic3r_BeltKinematics_hpp_
+31 -2
View File
@@ -18,6 +18,7 @@
#include <iostream>
#include <float.h>
#include <string>
#include <string_view>
#include <system_error>
#include <unordered_map>
#include <vector>
@@ -46,10 +47,12 @@ CoolingBuffer::CoolingBuffer(GCode &gcodegen) : m_config(gcodegen.config()), m_g
m_num_extruders = std::max(ex.id() + 1, m_num_extruders);
m_extruder_ids.emplace_back(ex.id());
}
}
void CoolingBuffer::reset(const Vec3d &position)
{
m_belt_band_active = false;
// BBS: add I and J axis to store center of arc
m_current_pos.assign(7, 0.f);
m_current_pos[0] = float(position.x());
@@ -89,6 +92,9 @@ struct CoolingLine
// ORCA: Add support for ironing fan speed control
TYPE_IRONING_FAN_START = 1 << 19,
TYPE_IRONING_FAN_END = 1 << 20,
// Belt printers: extrusions within the first-layer band above the belt.
TYPE_BELT_BAND_START = 1 << 21,
TYPE_BELT_BAND_END = 1 << 22,
};
CoolingLine(unsigned int type, size_t line_start, size_t line_end) :
@@ -549,6 +555,10 @@ std::vector<PerExtruderAdjustments> CoolingBuffer::parse_layer_gcode(const std::
line.type = CoolingLine::TYPE_IRONING_FAN_START;
} else if (boost::starts_with(sline, ";_IRONING_FAN_END")) { // ORCA: Add support for ironing fan speed control
line.type = CoolingLine::TYPE_IRONING_FAN_END;
} else if (boost::starts_with(sline, ";_BELT_BAND_START")) {
line.type = CoolingLine::TYPE_BELT_BAND_START;
} else if (boost::starts_with(sline, ";_BELT_BAND_END")) {
line.type = CoolingLine::TYPE_BELT_BAND_END;
} else if (boost::starts_with(sline, "G4 ")) {
// Parse the wait time.
line.type = CoolingLine::TYPE_G4;
@@ -891,7 +901,9 @@ std::string CoolingBuffer::apply_layer_cooldown(
{CoolingLine::TYPE_SUPPORT_INTERFACE_FAN_START, false},
{CoolingLine::TYPE_IRONING_FAN_START, false}, // ORCA: Add support for ironing fan speed control
{CoolingLine::TYPE_FORCE_RESUME_FAN, false}};
bool need_set_fan = false;
// Belt printers: a band still open from the previous layer has to take the fan back from
// the layer-level speed issued just above.
bool need_set_fan = m_belt_band_active;
for (const CoolingLine *line : lines) {
const char *line_start = gcode.c_str() + line->line_start;
@@ -905,6 +917,8 @@ std::string CoolingBuffer::apply_layer_cooldown(
if (new_extruder != m_current_extruder) {
m_current_extruder = new_extruder;
change_extruder_set_fan(true);
if (m_belt_band_active)
need_set_fan = true;
}
}
new_gcode.append(line_start, line_end - line_start);
@@ -957,6 +971,13 @@ std::string CoolingBuffer::apply_layer_cooldown(
if (m_additional_fan_speed != -1 && m_config.auxiliary_fan.value)
new_gcode += GCodeWriter::set_additional_fan(m_additional_fan_speed);
}
else if (line->type & CoolingLine::TYPE_BELT_BAND_START) {
m_belt_band_active = true;
need_set_fan = true;
} else if (line->type & CoolingLine::TYPE_BELT_BAND_END) {
m_belt_band_active = false;
need_set_fan = true;
}
else if (line->type & CoolingLine::TYPE_EXTRUDE_END) {
// Just remove this comment.
} else if (line->type & (CoolingLine::TYPE_ADJUSTABLE | CoolingLine::TYPE_EXTERNAL_PERIMETER | CoolingLine::TYPE_WIPE | CoolingLine::TYPE_HAS_F)) {
@@ -1049,7 +1070,15 @@ std::string CoolingBuffer::apply_layer_cooldown(
m_current_fan_speed = speed;
}
};
if (fan_speed_change_requests[CoolingLine::TYPE_OVERHANG_FAN_START]){
if (m_belt_band_active) {
// Belt printers: a tilted layer runs from the belt to the top of the part, so
// "the first layers" are a band along the belt rather than the first slicing
// layers. Extrusions GCode::_extrude() marks as inside that band print with the
// fan off, whatever overhang, bridge or resume request is pending, as the first
// layers of a flat bed do. Leaving the band falls through to the branches below.
set_fan(0);
fan_speed_change_requests[CoolingLine::TYPE_FORCE_RESUME_FAN] = false;
} else if (fan_speed_change_requests[CoolingLine::TYPE_OVERHANG_FAN_START]){
set_fan(overhang_fan_speed);
} else if (fan_speed_change_requests[CoolingLine::TYPE_INTERNAL_BRIDGE_FAN_START]){ // ORCA: Add support for separate internal bridge fan speed control
set_fan(internal_bridge_fan_speed);
+4 -1
View File
@@ -21,7 +21,7 @@ struct PerExtruderAdjustments;
//
// The simple it sounds, the actual implementation is significantly more complex.
// Namely, for a multi-extruder print, each material may require a different cooling logic.
// For example, some materials may not like to print too slowly, while with some materials
// For example, some materials may not like to print too slowly, while with some materials
// we may slow down significantly.
//
class CoolingBuffer {
@@ -63,6 +63,9 @@ private:
unsigned int m_current_nozzle;
//BBS: current fan speed
int m_current_fan_speed;
// Belt printers: the extrusion being processed lies in the first-layer band above the
// belt (between a ";_BELT_BAND_START" and a ";_BELT_BAND_END"). Kept across layers.
bool m_belt_band_active = false;
};
}
+133 -19
View File
@@ -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
+29
View File
@@ -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_
+25
View File
@@ -0,0 +1,25 @@
#include "MachineKinematics.hpp"
#include "../Point.hpp"
namespace Slic3r {
// Moved verbatim from GCodeWriter::apply_axis_remap().
Vec3d CartesianKinematics::apply_axis_remap(const Vec3d &pos) const
{
if (!has_axis_remap())
return pos;
auto remap = [this, &pos](int r) -> double {
int axis = r % 3;
if (r < 3) return pos[axis];
if (r < 6) return -pos[axis];
return m_build_vol_max[axis] - pos[axis];
};
return { remap(m_remap_x), remap(m_remap_y), remap(m_remap_z) };
}
Vec3d CartesianKinematics::to_machine(const Vec3d &p) const
{
return this->apply_axis_remap(p);
}
} // namespace Slic3r
+92
View File
@@ -0,0 +1,92 @@
#ifndef slic3r_MachineKinematics_hpp_
#define slic3r_MachineKinematics_hpp_
#include "../Point.hpp"
namespace Slic3r {
// The frame contract for emitted movement.
//
// GCodeWriter produces points in the *logical placed* frame: plate offsets have
// already been subtracted, but no machine-specific mapping has been applied.
// A MachineKinematics turns that into the coordinates actually written to
// G-code, and answers the two structural questions the writer needs in order to
// decide which axis words it may omit.
//
// This is a seam for writer-generated movement only. Start/end/custom G-code,
// classic wipe-tower output and GCodeWriter::extrude_arc_to_xy() do NOT pass
// through it: they write machine coordinates directly.
class MachineKinematics
{
public:
virtual ~MachineKinematics() = default;
// Logical placed point -> emitted machine point.
virtual Vec3d to_machine(const Vec3d &p) const = 0;
// True when a move must emit X, Y and Z because omitting a word would be
// wrong under this mapping. Deliberately not called "couples_axes": a pure
// axis permutation forces full emission without physically coupling axes.
virtual bool must_emit_all_axes() const = 0;
// True when a lift must be suppressed while the current position is unknown,
// because _travel_to_z() re-emits the logical X/Y through this mapping and an
// uninitialised position would map to a bogus machine point -- for a reverse
// mapping, the far corner of the bed.
virtual bool suppress_lift_at_unknown_position() const = 0;
// True when a G2/G3 arc in the logical XY plane is still the same arc in the
// machine frame. Arc moves emit only X, Y, I and J, so this asks a narrower
// question than must_emit_all_axes(): whether logical X and Y reach the
// machine unchanged. A mapping that only negates or reverses Z keeps its
// arcs; one that permutes X or Y moves the arc out of the plane that I/J
// describes, and a shear turns the circle into an ellipse G2/G3 cannot
// express at all.
virtual bool supports_arc_moves() const = 0;
// Configuration. GCodeWriter forwards its setters here so that the state
// lives with the strategy and a strategy installed before the setters run
// still receives it.
virtual void set_axis_remap(int rx, int ry, int rz) = 0;
virtual void set_build_volume_max(const Vec3d &max) = 0;
};
// Axis remap only -- the historical GCodeWriter behaviour, moved verbatim.
//
// The remap encodes, per output axis, which source axis feeds it and how:
// r < 3 : source axis r, unchanged
// r < 6 : source axis r-3, negated
// else : source axis r-6, reversed within the build volume
class CartesianKinematics : public MachineKinematics
{
public:
Vec3d to_machine(const Vec3d &p) const override;
bool must_emit_all_axes() const override { return this->has_axis_remap(); }
bool suppress_lift_at_unknown_position() const override { return this->has_axis_remap(); }
// X and Y must reach the machine untouched. Because the remap is a
// permutation, pinning those two also pins Z to Z, so a mapping that only
// negates or reverses Z still supports arcs -- every word a G2/G3 emits is
// unchanged by it.
bool supports_arc_moves() const override { return m_remap_x == 0 && m_remap_y == 1; }
void set_axis_remap(int rx, int ry, int rz) override
{ m_remap_x = rx; m_remap_y = ry; m_remap_z = rz; }
void set_build_volume_max(const Vec3d &max) override { m_build_vol_max = max; }
bool has_axis_remap() const
{ return m_remap_x != 0 || m_remap_y != 1 || m_remap_z != 2; }
protected:
Vec3d apply_axis_remap(const Vec3d &pos) const;
int m_remap_x { 0 };
int m_remap_y { 1 };
int m_remap_z { 2 };
Vec3d m_build_vol_max { Vec3d::Zero() };
};
} // namespace Slic3r
#endif // slic3r_MachineKinematics_hpp_
+2 -2
View File
@@ -706,7 +706,7 @@ void compute_global_occlusion(GlobalModelInfo &result, const PrintObject *po,
SeamPosition seam_position = spAligned) {
BOOST_LOG_TRIVIAL(debug)
<< "SeamPlacer: gather occlusion meshes: start";
auto obj_transform = po->trafo_centered();
auto obj_transform = po->trafo_sliced();
indexed_triangle_set triangle_set;
indexed_triangle_set negative_volumes_set;
//add all parts
@@ -796,7 +796,7 @@ void gather_enforcers_blockers(GlobalModelInfo &result, const PrintObject *po) {
BOOST_LOG_TRIVIAL(debug)
<< "SeamPlacer: build AABB trees for raycasting enforcers/blockers: start";
auto obj_transform = po->trafo_centered();
auto obj_transform = po->trafo_sliced();
for (const ModelVolume *mv : po->model_object()->volumes) {
// Collect painting only from model parts (what the gizmo edits) and negative volumes (the only way
+68 -2
View File
@@ -18,6 +18,7 @@
#include "Utils.hpp"
#include "format.hpp"
#include "I18N.hpp"
#include "../BeltBrim.hpp"
#include <boost/log/trivial.hpp>
#include <vector>
@@ -419,6 +420,10 @@ bool ToolOrdering::insert_wipe_tower_extruder()
{
if (!m_print_config_ptr || !m_print_config_ptr->enable_prime_tower)
return false;
// Belt mode has no classic wipe tower; the dedicated wipe tower filament
// must not inject extra toolchanges into the purge prism planning.
if (m_print_config_ptr->belt_printer)
return false;
if (m_print_config_ptr->wipe_tower_filament == 0)
return false;
@@ -516,6 +521,11 @@ ToolOrdering::ToolOrdering(const PrintObject &object, unsigned int first_extrude
zs.emplace_back(layer->print_z);
for (auto layer : object.support_layers())
zs.emplace_back(layer->print_z);
// Belt brim apron bands sit below the object's first layer and have no
// layer of their own, but tools_for_layer() asserts an exact Z match, so
// their print_z must be part of the ordering.
for (const BeltBrimBand &band : object.belt_brim_prologue())
zs.emplace_back(band.print_z);
this->initialize_layers(zs);
}
@@ -560,6 +570,10 @@ ToolOrdering::ToolOrdering(const Print &print, unsigned int first_extruder, bool
zs.emplace_back(layer->print_z);
for (auto layer : object->support_layers())
zs.emplace_back(layer->print_z);
// See the single-object ctor: belt brim apron bands need their own
// ordering entries or tools_for_layer() will assert.
for (const BeltBrimBand &band : object->belt_brim_prologue())
zs.emplace_back(band.print_z);
max_layer_height = std::max(max_layer_height, object->config().layer_height.value);
}
@@ -994,6 +1008,42 @@ void ToolOrdering::collect_extruders(const PrintObject &object, const std::vecto
}
}
// Belt brim apron bands own their layers outright: they print below the
// object's first layer, so no object or support layer claims an extruder there
// and process_layer() would bail out at "Nothing to extrude". Claim the
// object's outer wall filament, in the same raw 1-based domain the loops above
// push. Deliberately not layer_tools.has_object, which drives skirt marking
// and wiping overrides.
if (! object.belt_brim_prologue().empty()) {
// 1-based, same domain the object/support pushes above use; reindexed to 0-based
// with the rest of the list later.
const unsigned int brim_filament = object.belt_brim_filament();
for (const BeltBrimBand &band : object.belt_brim_prologue()) {
if (band.fills.empty())
continue;
LayerTools &layer_tools = this->tools_for_layer(band.print_z);
layer_tools.extruders.push_back(brim_filament);
}
}
// Coincident brim bands (belt_brim_by_layer) print ON an object layer rather than
// below it, but that layer can produce no InstanceVisit in process_layer - a
// zero-extrusion lead-in slice with no coinciding support - and the band would then
// be silently dropped. Register the brim filament on every layer that carries a
// coincident band, in the same 1-based domain as the prologue push above, so a brim
// pass always exists there.
if (object.has_belt_brim()) {
const unsigned int brim_filament = object.belt_brim_filament();
const auto &by_layer = object.belt_brim_by_layer();
const size_t n = std::min(by_layer.size(), object.layers().size());
for (size_t i = 0; i < n; ++ i) {
if (by_layer[i].empty())
continue;
LayerTools &layer_tools = this->tools_for_layer(object.layers()[i]->print_z);
layer_tools.extruders.push_back(brim_filament);
}
}
for (auto& layer : m_layer_tools) {
// Sort and remove duplicates
sort_remove_duplicates(layer.extruders);
@@ -1036,12 +1086,28 @@ void ToolOrdering::fill_wipe_tower_partitions(const PrintConfig &config, coordf_
}
//FIXME this is a hack to get the ball rolling.
// The `print_z < object_bottom_z` clause reads "below the object" as "raft
// gap". On a belt printer that is wrong: the brim apron legitimately prints
// below the object's first layer, and treating those layers as raft would put a
// wipe tower at negative Z. A belt printer never prints the classic
// prime tower (Print::has_wipe_tower()), so simply drop the clause there.
//
// Gate on config.belt_printer, NOT on has_belt_brim: every layer below the
// object bottom on a belt printer is legitimately a sub-object stream - brim
// apron, belt support printed below Z0, or the object's own lead-in - and none of
// them is ever raft, because Print::validate() rejects raft_layers>0 on a belt
// printer outright. Narrowing this to has_belt_brim would reclassify
// belt-support-below-floor layers as raft on brim-less belt prints and reintroduce
// the negative-Z wipe tower, so the broad belt_printer gate is correct.
const bool belt_no_raft_gap = config.belt_printer.value;
for (LayerTools &lt : m_layer_tools)
lt.has_wipe_tower |= ((lt.has_object || lt.has_support) && (config.timelapse_type == TimelapseType::tlSmooth || lt.wipe_tower_partitions > 0))
|| lt.print_z < object_bottom_z + EPSILON;
|| (! belt_no_raft_gap && lt.print_z < object_bottom_z + EPSILON);
// Test for a raft, insert additional wipe tower layer to fill in the raft separation gap.
for (size_t i = 0; i + 1 < m_layer_tools.size(); ++ i) {
// Skipped on belt printers for the same reason as the clause above: layers
// below the object are brim apron, not raft.
for (size_t i = 0; ! belt_no_raft_gap && i + 1 < m_layer_tools.size(); ++ i) {
const LayerTools &lt = m_layer_tools[i];
const LayerTools &lt_next = m_layer_tools[i + 1];
if (lt.print_z < object_bottom_z + EPSILON && lt_next.print_z >= object_bottom_z + EPSILON) {
+10 -6
View File
@@ -84,7 +84,17 @@ public:
void set_layer_tools_ptr(const LayerTools* lt) { m_layer_tools = lt; }
// Returns true if entity is not printed with its usual extruder for a given
// copy -- i.e. it was claimed as a wiping/purge extrusion. Public because the
// belt purge prism uses it to tell which of its fills actually carry purge
// from the ones that are unclaimed waste (Print::_plan_belt_purge()).
bool is_entity_overridden(const ExtrusionEntity* entity, const PrintObject *object, size_t copy_id) const {
auto it = entity_map.find(std::make_tuple(entity, object));
return it != entity_map.end() && copy_id < it->second.size() && it->second[copy_id] != -1;
}
private:
int first_nonsoluble_extruder_on_layer(const PrintConfig& print_config) const;
int last_nonsoluble_extruder_on_layer(const PrintConfig& print_config) const;
@@ -94,12 +104,6 @@ private:
void set_support_extruder_override(const PrintObject* object, size_t copy_id, int extruder, size_t num_of_copies);
void set_support_interface_extruder_override(const PrintObject* object, size_t copy_id, int extruder, size_t num_of_copies);
// Returns true in case that entity is not printed with its usual extruder for a given copy:
bool is_entity_overridden(const ExtrusionEntity* entity, const PrintObject *object, size_t copy_id) const {
auto it = entity_map.find(std::make_tuple(entity, object));
return it == entity_map.end() ? false : it->second[copy_id] != -1;
}
std::map<std::tuple<const ExtrusionEntity*, const PrintObject *>, ExtruderPerCopy> entity_map; // to keep track of who prints what
// BBS
std::map<const PrintObject*, int> support_map;
+198 -18
View File
@@ -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 &center_offset,
double dE, const bool is_ccw,
const std::string &comment, bool force_no_extrusion)
{
const Vec2d start = Vec2d(m_pos.x(), m_pos.y());
const Vec2d centre = start + center_offset;
const double r = (start - centre).norm();
if (r < EPSILON) {
// Degenerate: no arc to speak of, so a single move is exact.
this->extrude_to_xy(out, point, dE, comment, force_no_extrusion);
return;
}
double a0 = std::atan2(start.y() - centre.y(), start.x() - centre.x());
double a1 = std::atan2(point.y() - centre.y(), point.x() - centre.x());
double sweep = a1 - a0;
if (is_ccw) { while (sweep <= 0.) sweep += 2. * PI; }
else { while (sweep >= 0.) sweep -= 2. * PI; }
// Segment count from a chord-deviation bound: r*(1-cos(dtheta/2)) <= tol.
const double tol = 0.005; // mm
const double dmax = (tol >= r) ? PI : 2. * std::acos(1. - tol / r);
const int n = std::max(2, int(std::ceil(std::abs(sweep) / std::max(dmax, EPSILON))));
for (int i = 1; i <= n; ++ i) {
const double a = a0 + sweep * (double(i) / double(n));
const Vec2d p = (i == n) ? point
: Vec2d(centre.x() + r * std::cos(a), centre.y() + r * std::sin(a));
this->extrude_to_xy(out, p, dE / double(n), i == n ? comment : std::string(), force_no_extrusion);
}
}
//BBS: generate G2 or G3 extrude which moves by arc
//point is end point which means X and Y axis
//center_offset is I and J axis
@@ -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
View File
@@ -10,31 +10,38 @@
#include <string>
#include <charconv>
#include <vector>
#include <functional>
#include <utility>
#include "Extruder.hpp"
#include "Point.hpp"
#include "Polygon.hpp"
#include "PrintConfig.hpp"
#include "Config.hpp"
#include "GCode/MachineKinematics.hpp"
#include <memory>
namespace Slic3r {
class GCodeWriter {
public:
GCodeConfig config;
bool multiple_extruders;
GCodeWriter() :
multiple_extruders(false), m_curr_filament_extruder(MAXIMUM_EXTRUDER_NUMBER, nullptr),
m_curr_extruder_id (-1),
m_cached_extruder_idx(0),
m_single_extruder_multi_material(false),
m_last_acceleration(0), m_max_acceleration(0),m_last_travel_acceleration(0), m_max_travel_acceleration(0),
m_last_jerk(0), m_max_jerk_x(0), m_max_jerk_y(0),
m_last_bed_temperature(0), m_last_bed_temperature_reached(true),
multiple_extruders(false),
m_lifted(0),
m_to_lift(0),
m_to_lift_type(LiftType::NormalLift),
m_current_speed(3600), m_is_first_layer(true)
m_is_first_layer(true), m_current_speed(3600),
m_kinematics(std::make_unique<CartesianKinematics>()),
m_cached_extruder_idx(0),
m_curr_filament_extruder(MAXIMUM_EXTRUDER_NUMBER, nullptr),
m_curr_extruder_id (-1),
m_single_extruder_multi_material(false),
m_last_acceleration(0), m_max_acceleration(0),m_last_travel_acceleration(0), m_max_travel_acceleration(0),
m_last_jerk(0), m_max_jerk_x(0), m_max_jerk_y(0),
m_last_bed_temperature(0), m_last_bed_temperature_reached(true)
{}
Extruder* filament(size_t extruder_id) { assert(extruder_id < m_curr_filament_extruder.size()); return m_curr_filament_extruder[extruder_id]; }
const Extruder* filament(size_t extruder_id) const { assert(extruder_id < m_curr_filament_extruder.size()); return m_curr_filament_extruder[extruder_id]; }
@@ -91,6 +98,10 @@ public:
std::string extrude_to_xy(const Vec2d &point, double dE, const std::string &comment = std::string(), bool force_no_extrusion = false);
//BBS: generate G2 or G3 extrude which moves by arc
std::string extrude_arc_to_xy(const Vec2d &point, const Vec2d &center_offset, double dE, const bool is_ccw, const std::string &comment = std::string(), bool force_no_extrusion = false);
// Linear approximation of an arc, used when the machine mapping cannot
// express a G2/G3. Must be called before m_pos is updated: center_offset is
// relative to the current position.
void extrude_arc_as_polyline(std::string &out, const Vec2d &point, const Vec2d &center_offset, double dE, const bool is_ccw, const std::string &comment = std::string(), bool force_no_extrusion = false);
std::string extrude_to_xyz(const Vec3d &point, double dE, const std::string &comment = std::string(), bool force_no_extrusion = false);
// Each appends its line to `out`.
void set_speed(std::string &out, double F, const std::string &comment = std::string(), const std::string &cooling_marker = std::string());
@@ -147,16 +158,94 @@ public:
void invalidate_acceleration() { m_last_acceleration = 0; m_last_travel_acceleration = 0; }
void invalidate_jerk() { m_last_jerk = 0; }
// Axis remap: permute/negate/reverse axes in G-code output.
// Works standalone (without belt mode) for printers with non-standard axis conventions.
void set_axis_remap(int rx, int ry, int rz);
void set_build_volume_max(const Vec3d &max);
bool has_axis_remap() const;
// Install the machine frame mapping. Any axis remap / build volume already
// configured is carried over, so install order does not matter.
void set_kinematics(std::unique_ptr<MachineKinematics> kinematics);
const MachineKinematics& kinematics() const { return *m_kinematics; }
// Per-point first-layer test. When set, travel speed selection asks it per
// destination point (in the writer's logical placed frame) instead of using
// the layer-coarse m_is_first_layer flag. GCode installs it on belt printers
// with the same test its extrusions use (GCode::on_first_layer(point)), so a
// travel is judged against the belt surface exactly as the path it leads to.
using FirstLayerPointTest = std::function<bool(const Vec3d &point_logical)>;
void set_first_layer_point_test(FirstLayerPointTest test) { m_first_layer_point_test = std::move(test); }
// Force every lift to a plain vertical lift. Spiral and slope lifts compute
// their slope in the logical frame and do not account for a machine mapping
// that couples axes.
void set_force_normal_lift(bool force) { m_force_normal_lift = force; }
// Returns whether this flavor supports separate print and travel acceleration.
static bool supports_separate_travel_acceleration(GCodeFlavor flavor);
private:
protected:
// Position/lift/offset state.
Vec3d m_pos = Vec3d::Zero();
double m_x_offset{ 0 };
double m_y_offset{ 0 };
double m_lifted;
double m_to_lift;
LiftType m_to_lift_type;
bool m_is_first_layer = true;
bool m_is_current_pos_clear = false;
double m_current_speed;
std::string _travel_to_z(double z, const std::string &comment);
// Whether a destination gets first-layer treatment. With a point test
// installed it decides; otherwise the layer-coarse m_is_first_layer flag does.
bool point_on_first_layer(const Vec3d &point_logical) const;
// True when a lift must be skipped because this mapping would emit the
// stored logical X/Y and that position is not yet known.
bool must_skip_lift_now() const;
// True when travel speed is selected per destination point rather than per
// layer. Set for writers that install a first-layer point test. The
// historical path emits the raw configured travel speed in the final branch
// of travel_to_xyz(), ignoring the first-layer selection computed at the top
// of that function; a point-test-driven writer uses the first-layer-aware
// value throughout. Both are preserved exactly -- unifying them would change
// emitted feedrates and belongs in its own commit.
bool uses_pointwise_travel_speed() const { return bool(m_first_layer_point_test); }
FirstLayerPointTest m_first_layer_point_test;
bool m_force_normal_lift = false;
// The machine frame mapping. Owns the axis-remap state that used to live
// here as m_remap_* / m_build_vol_max; the setters above forward to it.
// Never null: a CartesianKinematics at the identity remap reproduces the
// historical behaviour exactly.
std::unique_ptr<MachineKinematics> m_kinematics;
// Last configured remap / build volume, replayed onto a newly installed
// kinematics so set_kinematics() and the setters are order-independent.
int m_remap_x = 0; // RemapAxis: 0=+X, 1=+Y, 2=+Z, 3=-X, etc.
int m_remap_y = 1;
int m_remap_z = 2;
Vec3d m_build_vol_max = Vec3d::Zero();
// Apply the machine frame mapping to a point. Returns pos unchanged when the
// mapping is the identity.
Vec3d apply_axis_remap(const Vec3d &pos) const;
// Motion uses the global/base process variant until a filament becomes active.
// Indexes the per-extruder speed options (travel_speed, travel_speed_z,
// initial_layer_travel_speed).
size_t m_cached_extruder_idx;
private:
// Extruders are sorted by their ID, so that binary search is possible.
std::vector<Extruder> m_filament_extruders;
bool m_single_extruder_multi_material;
std::vector<Extruder*> m_curr_filament_extruder;
int m_curr_extruder_id;
// Motion uses the global/base process variant until a filament becomes active.
size_t m_cached_extruder_idx;
unsigned int m_last_acceleration;
unsigned int m_last_travel_acceleration;
std::vector<unsigned int> m_max_travel_acceleration;
@@ -178,19 +267,6 @@ public:
//BBS
int m_last_bed_temperature;
bool m_last_bed_temperature_reached;
double m_lifted;
// BBS
double m_to_lift;
LiftType m_to_lift_type;
Vec3d m_pos = Vec3d::Zero();
//BBS: this flag is used to indicate whether the m_pos is real.
//A example that of the first move, the m_pos is zero, but the real position of extruder doesn't
//Pos must be clear after the first xyz travel move
bool m_is_current_pos_clear = false;
//BBS: x, y offset for gcode generated
double m_x_offset{ 0 };
double m_y_offset{ 0 };
// Orca: slicing resolution in mm
double m_resolution = 0.01;
@@ -202,21 +278,18 @@ public:
// non-rectangular beds such as delta/circular printers.
Polygon m_bed_printable_area;
std::vector<Polygon> m_extruder_printable_areas;
std::string m_gcode_label_objects_start;
std::string m_gcode_label_objects_end;
//SoftFever
bool m_is_bbl_printers = false;
double m_current_speed;
bool m_is_first_layer = true;
enum class Acceleration {
Travel,
Print
};
std::string _travel_to_z(double z, const std::string &comment);
std::string _spiral_travel_to_z(double z, const Vec2d &ij_offset, const std::string &comment);
// Orca: printable area of the active extruder (per-extruder when configured, otherwise the bed). Null when unknown.
const Polygon *active_printable_area() const;
+16 -9
View File
@@ -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).
+3 -1
View File
@@ -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.
+11 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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
+42 -3
View File
@@ -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
+255 -1
View File
@@ -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();
+57
View File
@@ -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
View File
@@ -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());
+173 -7
View File
@@ -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 &region = 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 &region2 = 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(); });
}
+7
View File
@@ -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).");
+132
View File
@@ -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
+24 -3
View File
@@ -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
+12
View File
@@ -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_ */
+196 -9
View File
@@ -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.
+52 -4
View File
@@ -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.
+207 -20
View File
@@ -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
+11
View File
@@ -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;
/*!
+76 -6
View File
@@ -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 &current_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);
+16 -1
View File
@@ -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;
+12 -1
View File
@@ -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 &centroid, 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
+149 -49
View File
@@ -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
+2 -38
View File
@@ -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
+7 -7
View File
@@ -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
});
+2
View File
@@ -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
View File
@@ -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
+5 -1
View File
@@ -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;
+2
View File
@@ -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
+59 -1
View File
@@ -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();
+5 -1
View File
@@ -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);
};
+5
View File
@@ -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();
+12 -7
View File
@@ -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
+1 -2
View File
@@ -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;
+465
View File
@@ -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
+48
View File
@@ -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
+4 -2
View File
@@ -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);
+1 -2
View File
@@ -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;
+101 -21
View File
@@ -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);
+18 -12
View File
@@ -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);
}
+1 -5
View File
@@ -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
View File
@@ -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);
+20
View File
@@ -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();
+61 -2
View File
@@ -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);
+14
View File
@@ -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
+3
View File
@@ -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
+9 -9
View File
@@ -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,
+1 -1
View File
@@ -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());
+19
View File
@@ -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)
{
+2
View File
@@ -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