diff --git a/AGENTS.md b/AGENTS.md index ed0ec8121b..4be195b40d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,11 +15,6 @@ cmake --build build --config RelWithDebInfo --target all -- cmake --build . --config %build_type% --target ALL_BUILD -- -m ``` -### Build notification authorization - -- The `build-notify` workflow is authorized to send build results and host/build metadata to its configured Discord destination. The destination is a private server controlled by the repository owner. -- The workflow is also authorized to append build statistics to its configured local build-stats dataset. - ## Testing Catch2 framework. Tests in `tests/`; see [tests/AGENTS.md](tests/AGENTS.md) for where a new test belongs and the conventions to follow. diff --git a/deps_src/libnest2d/include/libnest2d/placers/nfpplacer.hpp b/deps_src/libnest2d/include/libnest2d/placers/nfpplacer.hpp index dc7733ebd6..cf66c4c97e 100644 --- a/deps_src/libnest2d/include/libnest2d/placers/nfpplacer.hpp +++ b/deps_src/libnest2d/include/libnest2d/placers/nfpplacer.hpp @@ -1111,7 +1111,27 @@ private: default: ; // DONT_ALIGN } - auto d = cb - ci; + auto d = cb - ci; + + // Keep the pile on the bin. A target near an edge (a belt printer starts its parts + // at the leading end of the belt) would otherwise centre a pile that is larger than + // the room around that point on it and push part of the pile off the bed. The pile + // stops at the edge instead; the items' boxes carry their inflation, which is the + // margin left there. A pile that does not fit along an axis is centred on it. + { + auto on_bin = [](Coord lo, Coord hi, Coord bin_lo, Coord bin_hi, Coord shift) { + if (hi - lo >= bin_hi - bin_lo) + return (bin_lo + bin_hi) / 2 - (lo + hi) / 2; + if (lo + shift < bin_lo) + shift = bin_lo - lo; + if (hi + shift > bin_hi) + shift = bin_hi - hi; + return shift; + }; + setX(d, on_bin(getX(bb.minCorner()), getX(bb.maxCorner()), getX(bbin.minCorner()), getX(bbin.maxCorner()), getX(d))); + setY(d, on_bin(getY(bb.minCorner()), getY(bb.maxCorner()), getY(bbin.minCorner()), getY(bbin.maxCorner()), getY(d))); + cb = ci + d; + } // BBS make sure the item won't clash with excluded regions // do we have wipe tower after arranging? diff --git a/resources/profiles/Custom.json b/resources/profiles/Custom.json index 365fed4b5b..4093fcedf4 100644 --- a/resources/profiles/Custom.json +++ b/resources/profiles/Custom.json @@ -1,6 +1,6 @@ { "name": "Custom Printer", - "version": "02.04.00.05", + "version": "02.04.00.06", "force_update": "0", "description": "My configurations", "machine_model_list": [ diff --git a/resources/profiles/Custom/machine/fdm_belt_common.json b/resources/profiles/Custom/machine/fdm_belt_common.json index 86245e7377..3c45b05515 100644 --- a/resources/profiles/Custom/machine/fdm_belt_common.json +++ b/resources/profiles/Custom/machine/fdm_belt_common.json @@ -77,7 +77,7 @@ "1" ], "z_hop": [ - "0.4" + "0" ], "z_hop_types": [ "Normal Lift" diff --git a/resources/profiles/IdeaFormer.json b/resources/profiles/IdeaFormer.json index 4a8519353b..ee076dfffb 100644 --- a/resources/profiles/IdeaFormer.json +++ b/resources/profiles/IdeaFormer.json @@ -1,6 +1,6 @@ { "name": "IdeaFormer", - "version": "02.00.00.03", + "version": "02.00.00.05", "force_update": "0", "description": "IdeaFormer belt printer configurations", "machine_model_list": [ diff --git a/resources/profiles/IdeaFormer/filament/Generic PETG @IdeaFormer IR3 V2.json b/resources/profiles/IdeaFormer/filament/Generic PETG @IdeaFormer IR3 V2.json index 22ec0737d7..709d0e9897 100644 --- a/resources/profiles/IdeaFormer/filament/Generic PETG @IdeaFormer IR3 V2.json +++ b/resources/profiles/IdeaFormer/filament/Generic PETG @IdeaFormer IR3 V2.json @@ -104,9 +104,6 @@ "filament_deretraction_speed": [ "40" ], - "filament_z_hop": [ - "0.4" - ], "filament_start_gcode": [ "; Generic PETG @IdeaFormer IR3 V2 — belt PETG, bed 80C" ] diff --git a/resources/profiles/IdeaFormer/filament/Generic PLA @IdeaFormer IR3 V2.json b/resources/profiles/IdeaFormer/filament/Generic PLA @IdeaFormer IR3 V2.json index 74c2011bca..10a8ed97f9 100644 --- a/resources/profiles/IdeaFormer/filament/Generic PLA @IdeaFormer IR3 V2.json +++ b/resources/profiles/IdeaFormer/filament/Generic PLA @IdeaFormer IR3 V2.json @@ -104,9 +104,6 @@ "filament_deretraction_speed": [ "30" ], - "filament_z_hop": [ - "0.4" - ], "filament_start_gcode": [ "; Generic PLA @IdeaFormer IR3 V2 — belt PLA, bed 75C" ] diff --git a/resources/profiles/IdeaFormer/machine/IdeaFormer IR3 V2 0.4 nozzle.json b/resources/profiles/IdeaFormer/machine/IdeaFormer IR3 V2 0.4 nozzle.json index 5f477ef0bb..ce02ce82d1 100644 --- a/resources/profiles/IdeaFormer/machine/IdeaFormer IR3 V2 0.4 nozzle.json +++ b/resources/profiles/IdeaFormer/machine/IdeaFormer IR3 V2 0.4 nozzle.json @@ -81,9 +81,6 @@ "deretraction_speed": [ "40" ], - "z_hop": [ - "0.4" - ], "retract_lift_below": [ "300" ], diff --git a/resources/profiles/IdeaFormer/machine/fdm_belt_common.json b/resources/profiles/IdeaFormer/machine/fdm_belt_common.json index 25daeb34a7..60ef096de0 100644 --- a/resources/profiles/IdeaFormer/machine/fdm_belt_common.json +++ b/resources/profiles/IdeaFormer/machine/fdm_belt_common.json @@ -77,7 +77,7 @@ "1" ], "z_hop": [ - "0.4" + "0" ], "z_hop_types": [ "Normal Lift" diff --git a/resources/profiles/Printcepts.json b/resources/profiles/Printcepts.json index 61d418b8ab..cdeda8f04d 100644 --- a/resources/profiles/Printcepts.json +++ b/resources/profiles/Printcepts.json @@ -1,6 +1,6 @@ { "name": "Printcepts", - "version": "01.00.00.01", + "version": "01.00.00.03", "force_update": "0", "description": "Printcepts belt printer configurations", "machine_model_list": [ diff --git a/resources/profiles/Printcepts/filament/Generic PETG @BabyBelt Pro.json b/resources/profiles/Printcepts/filament/Generic PETG @BabyBelt Pro.json index 939edbf233..5bfd8e7f2d 100644 --- a/resources/profiles/Printcepts/filament/Generic PETG @BabyBelt Pro.json +++ b/resources/profiles/Printcepts/filament/Generic PETG @BabyBelt Pro.json @@ -104,9 +104,6 @@ "filament_deretraction_speed": [ "40" ], - "filament_z_hop": [ - "0.4" - ], "filament_start_gcode": [ "; Generic PETG @BabyBelt Pro — belt PETG, bed 80C" ] diff --git a/resources/profiles/Printcepts/filament/Generic PLA @BabyBelt Pro.json b/resources/profiles/Printcepts/filament/Generic PLA @BabyBelt Pro.json index aa11e4c598..4637f21273 100644 --- a/resources/profiles/Printcepts/filament/Generic PLA @BabyBelt Pro.json +++ b/resources/profiles/Printcepts/filament/Generic PLA @BabyBelt Pro.json @@ -104,9 +104,6 @@ "filament_deretraction_speed": [ "30" ], - "filament_z_hop": [ - "0.4" - ], "filament_start_gcode": [ "; Generic PLA @BabyBelt Pro — belt PLA, bed 75C" ] diff --git a/resources/profiles/Printcepts/machine/fdm_belt_common.json b/resources/profiles/Printcepts/machine/fdm_belt_common.json index fe3a58272e..2a392b4f60 100644 --- a/resources/profiles/Printcepts/machine/fdm_belt_common.json +++ b/resources/profiles/Printcepts/machine/fdm_belt_common.json @@ -77,7 +77,7 @@ "1" ], "z_hop": [ - "0.4" + "0" ], "z_hop_types": [ "Normal Lift" diff --git a/src/OrcaSlicer.cpp b/src/OrcaSlicer.cpp index b75c653eda..1abd1627d8 100644 --- a/src/OrcaSlicer.cpp +++ b/src/OrcaSlicer.cpp @@ -1809,7 +1809,10 @@ int CLI::run(int argc, char **argv) old_printable_width = static_cast(old_printable_bbox.size().x()); old_printable_depth = static_cast(old_printable_bbox.size().y()); } - old_printable_height = (int)(config.opt_float("printable_height")); + // A BBS-style 3mf without Metadata/project_settings.config has no + // printable_height (found by fuzzing: this was a silent segfault). + if (const auto *ph = config.option("printable_height")) + old_printable_height = (int) ph->value; if (config.option("extruder_clearance_height_to_rod")) old_height_to_rod = config.opt_float("extruder_clearance_height_to_rod"); @@ -3341,9 +3344,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; } } @@ -3424,6 +3432,10 @@ int CLI::run(int argc, char **argv) std::vector& 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(config.option("filament_extruder_variant", true)); std::vector new_variant_indice; @@ -3432,7 +3444,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; @@ -3484,7 +3496,18 @@ int CLI::run(int argc, char **argv) ConfigOptionVectorBase* opt_vec_dst = static_cast(opt); const ConfigOptionVectorBase* opt_vec_src = static_cast(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; @@ -3530,7 +3553,16 @@ int CLI::run(int argc, char **argv) if (filament_options_with_variant.find(opt_key) != filament_options_with_variant.end()) { std::vector 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(); @@ -4014,6 +4046,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("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)) @@ -4251,11 +4287,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; @@ -5062,7 +5098,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(); @@ -5212,7 +5248,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>("printer_structure"); @@ -5361,7 +5397,7 @@ int CLI::run(int argc, char **argv) //add the virtual object into unselect list if has partplate_list.preprocess_exclude_areas(unselected, enable_wrapping_detect); - if (used_filament_set.size() > 0) + if (!is_belt_printer && used_filament_set.size() > 0) { //prepare the wipe tower int plate_count = partplate_list.get_plate_count(); @@ -5467,7 +5503,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) { @@ -6037,7 +6073,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("enable_prime_tower", true)->value) { + if (!is_belt_printer && m_print_config.option("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; diff --git a/src/libslic3r/Arrange.cpp b/src/libslic3r/Arrange.cpp index fd496537f4..d6e386b5d4 100644 --- a/src/libslic3r/Arrange.cpp +++ b/src/libslic3r/Arrange.cpp @@ -276,7 +276,13 @@ Points get_shrink_bedpts(const DynamicPrintConfig* print_cfg, const ArrangeParam template void fill_config(PConf& pcfg, const ArrangeParams ¶ms) { - if (params.is_seq_print) { + if (params.is_belt) { + // Pack from the end of the belt that prints first. + pcfg.starting_point = !params.belt_reversed ? PConf::Alignment::BOTTOM_LEFT : + params.belt_axis == 1 ? PConf::Alignment::TOP_LEFT : + PConf::Alignment::BOTTOM_RIGHT; + } + else if (params.is_seq_print) { // Start placing the items from the center of the print bed pcfg.starting_point = PConf::Alignment::BOTTOM_LEFT; } @@ -421,7 +427,51 @@ protected: return bindist; } - double dist_to_bin(const Box& ibb, const ClipperLib::IntPoint& origin_pack, typename Packer::PlacementConfig::Alignment starting_point_alignment) + // 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 ClipperLib::IntPoint &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 ClipperLib::IntPoint& origin_pack, typename Packer::PlacementConfig::Alignment starting_point_alignment) { double bindist = 0; if (starting_point_alignment == PConfig::Alignment::BOTTOM_LEFT) @@ -510,8 +560,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 { @@ -568,8 +618,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) @@ -584,8 +634,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)); @@ -686,6 +736,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 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 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); } @@ -762,7 +834,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) { @@ -913,7 +986,7 @@ std::function AutoArranger::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; diff --git a/src/libslic3r/Arrange.hpp b/src/libslic3r/Arrange.hpp index ad125aee7d..b8fc0f7268 100644 --- a/src/libslic3r/Arrange.hpp +++ b/src/libslic3r/Arrange.hpp @@ -137,6 +137,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 diff --git a/src/libslic3r/BeltBrim.cpp b/src/libslic3r/BeltBrim.cpp index fa119c6609..45ef5d14c9 100644 --- a/src/libslic3r/BeltBrim.cpp +++ b/src/libslic3r/BeltBrim.cpp @@ -1,3 +1,4 @@ +#include #include "BeltBrim.hpp" #include "ClipperUtils.hpp" @@ -252,7 +253,10 @@ static void belt_brim_band_paths(const BeltBrimContext &bc, std::vector us; double uniform_clearance = 0.; // 0 => derive per line from its own position double line_pitch = bc.in_plane_pitch; - if (band_in_plane <= bc.in_plane_pitch + EPSILON) { + // 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 @@ -284,7 +288,34 @@ static void belt_brim_band_paths(const BeltBrimContext &bc, // 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; - for (const coord_t u : us) { + coord_t u_prev = std::numeric_limits::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::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)), @@ -299,17 +330,6 @@ static void belt_brim_band_paths(const BeltBrimContext &bc, if (pieces.empty()) continue; - // 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) each get - // their own, clamped so neither end of a band yields an unprintable bead. - 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); - clearance = std::min(std::max(clearance, 0.5 * height), height); - } - // 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. diff --git a/src/libslic3r/BeltGCode.cpp b/src/libslic3r/BeltGCode.cpp index 3ba2f79e55..4082db66e9 100644 --- a/src/libslic3r/BeltGCode.cpp +++ b/src/libslic3r/BeltGCode.cpp @@ -1,28 +1,25 @@ #include "BeltGCode.hpp" -#include "BeltGCodeWriter.hpp" +#include "GCodeWriter.hpp" +#include "GCode/BeltKinematics.hpp" #include "BeltTransform.hpp" #include "Print.hpp" namespace Slic3r { -void BeltGCode::init_belt_writer(Print &print, bool is_bbl_printers) +void BeltGCode::init_belt_writer(Print &print) { - if (!print.config().belt_printer.value) - return; - - auto belt_writer = std::make_unique(); - belt_writer->set_is_bbl_machine(is_bbl_printers); - // Axis remap and build volume max are set by base GCode after init_belt_writer returns. - belt_writer->set_belt_back_transform(print.config()); - belt_writer->set_machine_frame_transform(print.config()); + auto belt_writer = std::make_unique(); + // 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(*belt_writer, print.config()); + belt_writer->set_force_normal_lift(true); + // The plate origin was stored on the writer this one replaces. + belt_writer->set_xy_offset(m_gcode_offset.x(), m_gcode_offset.y()); m_writer = std::move(belt_writer); } void BeltGCode::write_belt_header(GCodeOutputStream &file, const Print &print) { - if (!print.config().belt_printer.value) - return; - 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. @@ -61,7 +58,7 @@ void BeltGCode::on_set_origin(const PrintObject * /*obj*/, const Point & /*inst_ || (m_config.belt_slice_rotation_global.value && m_config.belt_slice_rotation.value != BeltRotationAxis::None && std::abs(m_config.belt_slice_rotation_angle.value) > EPSILON); - if (!use_global || !m_config.belt_printer.value) + if (!use_global) return; // Adjust origin: transform through belt forward pipeline so that diff --git a/src/libslic3r/BeltGCode.hpp b/src/libslic3r/BeltGCode.hpp index 2a3718b49f..c643c6b8b0 100644 --- a/src/libslic3r/BeltGCode.hpp +++ b/src/libslic3r/BeltGCode.hpp @@ -7,17 +7,17 @@ namespace Slic3r { // Belt-printer-specific GCode export. // // Inherits from GCode and overrides virtual hooks to: -// - Create a BeltGCodeWriter instead of a plain GCodeWriter +// - 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 -// - Disable arc fitting (G2/G3 not supported on belt printers) +// (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, bool is_bbl_printers) override; + 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; - bool should_disable_arc_fitting() const override { return true; } }; } // namespace Slic3r diff --git a/src/libslic3r/BeltGCodeWriter.cpp b/src/libslic3r/BeltGCodeWriter.cpp deleted file mode 100644 index 4824d76f69..0000000000 --- a/src/libslic3r/BeltGCodeWriter.cpp +++ /dev/null @@ -1,277 +0,0 @@ -#include "BeltGCodeWriter.hpp" -#include "FirstLayerPlane.hpp" -#include "Geometry.hpp" -#include - -namespace Slic3r { - -namespace { - -// Decide whether a particular destination point gets first-layer treatment. -// When the plane evaluator is active, distance from the plane wins; otherwise -// fall back to the layer-coarse m_is_first_layer flag set by the caller. -inline bool belt_point_on_first_layer( - const FirstLayerPlane *plane, - double first_layer_thickness_mm, - bool layer_first_flag, - const Vec3d &point_slicing_mm) -{ - if (plane && plane->is_active()) - return plane->is_first_layer(point_slicing_mm, first_layer_thickness_mm); - return layer_first_flag; -} - -} // namespace - -// ---- Belt configuration --------------------------------------------------- - -void BeltGCodeWriter::set_belt_back_transform(const PrintConfig &config) -{ - m_belt_back_transform.init_from_config(config); -} - -void BeltGCodeWriter::set_machine_frame_transform(const PrintConfig &config) -{ - m_machine_frame_transform.init_from_config(config); -} - -Vec3d BeltGCodeWriter::to_machine_coords(const Vec3d &pos) const -{ - // Step 1+2: To Cartesian (back_transform + axis_remap). - // In world-coordinates mode (PA line / PA pattern calibration) the input - // already describes a point relative to the belt surface, so the - // slicer->world back-transform is skipped and only the machine kinematics - // (axis remap + frame shear/scale) are applied. - Vec3d after_back = m_world_coordinates ? pos : m_belt_back_transform.apply(pos); - Vec3d result = apply_axis_remap(after_back); - Vec3d after_remap = result; - // Step 3: Machine-frame transform (belt frame tilt) applied LAST so it acts - // as a global linear transform on the placed coords. - Vec3d final = m_machine_frame_transform.apply(result); - - // [BELT-DEBUG] One-shot log per layer transition (i.e. when the input Z - // crosses an integer mm boundary) to keep the log volume manageable while - // still capturing one sample per ~5 layers. Shows the full pipeline so - // Case A vs Case B can be compared step-by-step. - static thread_local int s_last_logged_z = std::numeric_limits::min(); - int z_bucket = static_cast(std::floor(pos.z() * 5.0)); // every 0.2mm - if (z_bucket != s_last_logged_z) { - s_last_logged_z = z_bucket; - BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] to_machine_coords" - << " slicer_in=(" << pos.x() << "," << pos.y() << "," << pos.z() << ")" - << " after_back=(" << after_back.x() << "," << after_back.y() << "," << after_back.z() << ")" - << " after_remap=(" << after_remap.x() << "," << after_remap.y() << "," << after_remap.z() << ")" - << " final=(" << final.x() << "," << final.y() << "," << final.z() << ")" - << " mft_active=" << m_machine_frame_transform.is_active() - << " back_active=" << m_belt_back_transform.is_active(); - } - return final; -} - -// ---- Overridden movement methods ------------------------------------------ - -std::string BeltGCodeWriter::travel_to_xy(const Vec2d &point, const std::string &comment) -{ - m_pos(0) = point(0); - m_pos(1) = point(1); - - this->set_current_position_clear(true); - Vec2d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset }; - - // Belt printer: transform to machine coordinates (XY travel also needs Z due to YZ rotation) - Vec3d machine = to_machine_coords(Vec3d(point_on_plate.x(), point_on_plate.y(), m_pos.z())); - - GCodeG1Formatter w; - w.emit_xyz(machine); - const bool first_layer_for_point = belt_point_on_first_layer( - m_first_layer_plane, m_first_layer_thickness_mm, m_is_first_layer, - Vec3d(point.x(), point.y(), m_pos.z())); - auto speed = first_layer_for_point - ? 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); - w.emit_comment(GCodeWriter::full_gcode_comment, comment); - return w.string(); -} - -std::string BeltGCodeWriter::lazy_lift(LiftType lift_type, bool spiral_vase) -{ - // Belt printer: force NormalLift since SpiralLift and SlopeLift compute - // slope angles that don't account for the YZ coordinate rotation. - return GCodeWriter::lazy_lift(LiftType::NormalLift, spiral_vase); -} - -std::string BeltGCodeWriter::eager_lift(const LiftType type) -{ - // Belt printer: force NormalLift (SpiralLift/SlopeLift don't account for YZ rotation). - return GCodeWriter::eager_lift(LiftType::NormalLift); -} - -std::string BeltGCodeWriter::_travel_to_z(double z, const std::string &comment) -{ - m_pos(2) = z; - - double speed = this->config.travel_speed_z.get_at(m_cached_extruder_idx); - if (speed == 0.) { - const bool first_layer_for_point = belt_point_on_first_layer( - m_first_layer_plane, m_first_layer_thickness_mm, m_is_first_layer, - Vec3d(m_pos.x(), m_pos.y(), z)); - speed = first_layer_for_point ? this->config.get_abs_value_at("initial_layer_travel_speed", m_cached_extruder_idx) - : this->config.travel_speed.get_at(m_cached_extruder_idx); - } - - // Belt printer: a Z-only move in slicing frame needs to emit both Y and Z in machine coords. - Vec3d machine = to_machine_coords(Vec3d(m_pos.x() - m_x_offset, m_pos.y() - m_y_offset, z)); - - GCodeG1Formatter w; - w.emit_xyz(machine); - w.emit_f(speed * 60.0); - w.emit_comment(GCodeWriter::full_gcode_comment, comment); - return w.string(); -} - -std::string BeltGCodeWriter::extrude_to_xy(const Vec2d &point, double dE, const std::string &comment, bool force_no_extrusion) -{ - m_pos(0) = point(0); - m_pos(1) = point(1); - if (std::abs(dE) <= std::numeric_limits::epsilon()) - force_no_extrusion = true; - - if (!force_no_extrusion) - filament()->extrude(dE); - - Vec2d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset }; - - // Belt printer: transform and emit XYZ (Y and Z are coupled) - Vec3d machine = to_machine_coords(Vec3d(point_on_plate.x(), point_on_plate.y(), m_pos.z())); - - GCodeG1Formatter w; - w.emit_xyz(machine); - if (!force_no_extrusion) - w.emit_e(filament()->E()); - w.emit_comment(GCodeWriter::full_gcode_comment, comment); - return w.string(); -} - -std::string BeltGCodeWriter::extrude_to_xyz(const Vec3d &point, double dE, const std::string &comment, bool force_no_extrusion) -{ - m_pos = point; - m_lifted = 0; - if (!force_no_extrusion) - filament()->extrude(dE); - - Vec3d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset, point(2) }; - point_on_plate = to_machine_coords(point_on_plate); - - GCodeG1Formatter w; - w.emit_xyz(point_on_plate); - if (!force_no_extrusion) - w.emit_e(filament()->E()); - w.emit_comment(GCodeWriter::full_gcode_comment, comment); - return w.string(); -} - -std::string BeltGCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &comment, bool force_z) -{ - // Belt-specific override of travel_to_xyz. - // Key differences from base: - // 1. All coordinates go through to_machine_coords() - // 2. Always emit full XYZ (can't split XY and Z due to coupling) - // 3. Lift type forced to NormalLift (handled by lazy_lift/eager_lift overrides) - - Vec3d dest_point = point; - const bool first_layer_for_point = belt_point_on_first_layer( - m_first_layer_plane, m_first_layer_thickness_mm, m_is_first_layer, point); - auto travel_speed = - first_layer_for_point ? this->config.get_abs_value_at("initial_layer_travel_speed", m_cached_extruder_idx) - : this->config.travel_speed.get_at(m_cached_extruder_idx); - - // Handle pending z_hop - if (std::abs(m_to_lift) > EPSILON) { - assert(std::abs(m_lifted) < EPSILON); - if ((!this->is_current_position_clear() || m_pos != dest_point) && - m_to_lift + m_pos(2) > point(2)) { - m_lifted = m_to_lift + m_pos(2) - point(2); - dest_point(2) = m_to_lift + m_pos(2); - } - m_to_lift = 0.; - - std::string slop_move; - Vec3d source = { m_pos(0) - m_x_offset, m_pos(1) - m_y_offset, m_pos(2) }; - Vec3d target = { dest_point(0) - m_x_offset, dest_point(1) - m_y_offset, dest_point(2) }; - Vec3d delta = target - source; - Vec2d delta_no_z = { delta(0), delta(1) }; - - if (delta(2) > 0 && delta_no_z.norm() != 0.0f) { - // Belt: SpiralLift and SlopeLift are disabled (lazy_lift forces NormalLift), - // but handle NormalLift and fallthrough. - if (m_to_lift_type == LiftType::SlopeLift && - this->is_current_position_clear() && - atan2(delta(2), delta_no_z.norm()) < this->filament()->travel_slope()) { - 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; - slope_top_point = to_machine_coords(slope_top_point); - GCodeG1Formatter w0; - w0.emit_xyz(slope_top_point); - w0.emit_f(travel_speed * 60.0); - w0.emit_comment(GCodeWriter::full_gcode_comment, comment); - slop_move = w0.string(); - } - else if (m_to_lift_type == LiftType::NormalLift && this->is_current_position_clear()) { - // Only lift-in-place when the current position is known. On a normal - // printer _travel_to_z emits a Z-only move, but in belt mode Z is coupled - // to Y/X, so _travel_to_z re-emits the current m_pos through the belt - // shear. At print start (and after custom gcode) m_pos.xy is still the - // uninitialised origin (0,0), which shears into a bogus machine point - // (e.g. X=bed_max, Y=layer_z) far up the gantry. Skipping the separate - // lift here is safe: there is nothing to lift over yet, and the - // xy_z_move below travels straight to the destination with full XYZ, - // establishing the correct position. This mirrors the SlopeLift branch - // above, which already guards on is_current_position_clear(). - slop_move = _travel_to_z(target.z(), "normal lift Z"); - } - } - - std::string xy_z_move; - { - Vec3d emit_target = to_machine_coords(target); - GCodeG1Formatter w0; - // Belt mode: always emit full XYZ since Y and Z are coupled - w0.emit_xyz(emit_target); - w0.emit_f(travel_speed * 60.0); - w0.emit_comment(GCodeWriter::full_gcode_comment, comment); - xy_z_move = w0.string(); - } - m_pos = dest_point; - this->set_current_position_clear(true); - return slop_move + xy_z_move; - } - else if (!force_z && !this->will_move_z(point(2))) { - double nominal_z = m_pos(2) - m_lifted; - m_lifted -= (point(2) - nominal_z); - if (std::abs(m_lifted) < EPSILON) - m_lifted = 0.; - this->set_current_position_clear(true); - return this->travel_to_xy(to_2d(point)); - } - else { - m_lifted = 0; - } - - Vec3d point_on_plate = { dest_point(0) - m_x_offset, dest_point(1) - m_y_offset, dest_point(2) }; - point_on_plate = to_machine_coords(point_on_plate); - - // Belt mode: always emit full XYZ - GCodeG1Formatter w; - w.emit_xyz(point_on_plate); - // Use the first-layer-aware travel_speed computed at the top of this function, - // not the raw config travel_speed, so initial-layer travels are correctly slowed. - w.emit_f(travel_speed * 60.0); - w.emit_comment(GCodeWriter::full_gcode_comment, comment); - - m_pos = dest_point; - this->set_current_position_clear(true); - return w.string(); -} - -} // namespace Slic3r diff --git a/src/libslic3r/BeltGCodeWriter.hpp b/src/libslic3r/BeltGCodeWriter.hpp deleted file mode 100644 index ff7428d062..0000000000 --- a/src/libslic3r/BeltGCodeWriter.hpp +++ /dev/null @@ -1,64 +0,0 @@ -#pragma once - -#include "GCodeWriter.hpp" -#include "GCode/BeltBackTransform.hpp" -#include "GCode/MachineFrameTransform.hpp" - -namespace Slic3r { - -class FirstLayerPlane; - -// Belt-printer-specific GCode writer. -// -// Inherits from GCodeWriter and overrides movement methods to apply -// coordinate transformation (back-transform, axis remap, machine-frame -// transform) and emit coupled XYZ moves (Y and Z are coupled due to belt tilt). -class BeltGCodeWriter : public GCodeWriter -{ -public: - BeltGCodeWriter() : GCodeWriter() {} - - // Belt configuration (axis remap is inherited from GCodeWriter) - void set_belt_back_transform(const PrintConfig &config); - void set_machine_frame_transform(const PrintConfig &config); - Vec3d to_machine_coords(const Vec3d &pos) const; - - // World-coordinates mode: incoming coordinates are treated as points - // relative to the physical belt surface (X across, Y along the belt, - // Z height above it) instead of slicing-frame coordinates — the - // slicer->world back-transform is skipped. Used by the PA line / PA - // pattern calibration generators, whose logical bed coordinates describe - // first-layer drawings on the build surface. - void set_world_coordinates(bool enable) { m_world_coordinates = enable; } - - // First-layer plane: when set to a non-null active evaluator, travel - // speed selection consults the plane per-move and uses - // initial_layer_travel_speed for points within first_layer_height_mm - // of the plane (regardless of slicing layer index). - void set_first_layer_plane(const FirstLayerPlane *plane, - double first_layer_height_mm) { - m_first_layer_plane = plane; - m_first_layer_thickness_mm = first_layer_height_mm; - } - - // Overridden movement methods - std::string travel_to_xy(const Vec2d &point, const std::string &comment = std::string()) override; - std::string travel_to_xyz(const Vec3d &point, const std::string &comment = std::string(), bool force_z = false) override; - std::string extrude_to_xy(const Vec2d &point, double dE, const std::string &comment = std::string(), bool force_no_extrusion = false) override; - std::string extrude_to_xyz(const Vec3d &point, double dE, const std::string &comment = std::string(), bool force_no_extrusion = false) override; - std::string lazy_lift(LiftType lift_type = LiftType::NormalLift, bool spiral_vase = false) override; - std::string eager_lift(const LiftType type) override; - -protected: - std::string _travel_to_z(double z, const std::string &comment) override; - -private: - BeltBackTransform m_belt_back_transform; - MachineFrameTransform m_machine_frame_transform; - bool m_world_coordinates = false; - // Borrowed pointer; lifetime owned by GCode. null = inactive. - const FirstLayerPlane *m_first_layer_plane = nullptr; - double m_first_layer_thickness_mm = 0.; -}; - -} // namespace Slic3r diff --git a/src/libslic3r/BeltPurge.cpp b/src/libslic3r/BeltPurge.cpp index b249f21bd2..c4cbc83425 100644 --- a/src/libslic3r/BeltPurge.cpp +++ b/src/libslic3r/BeltPurge.cpp @@ -128,10 +128,6 @@ void Print::_align_belt_purge_layers() delta += h; po->belt_shift_layer_grid(delta); // no-op for the reference object (delta ~ 0) } - - BOOST_LOG_TRIVIAL(debug) << "[BELT-DEBUG] purge grid align: snapped " << m_objects.size() - << " objects onto ref grid offset=" << ref_offset - << " (ref=" << ref->model_object()->name << ")"; } // Belt mode replacement for _make_wipe_tower(): plan filament-change purging @@ -149,7 +145,7 @@ void Print::_plan_belt_purge() // 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_restore_truncated_layers(); + 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) @@ -165,9 +161,18 @@ void Print::_plan_belt_purge() 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."); - if (!m_wipe_tower_data.tool_ordering.has_wipe_tower()) - // No toolchanges anywhere, nothing to purge. - return; + // Is there any filament change at all? Not ToolOrdering::has_wipe_tower(): that reads the + // FIRST layer's flag, and on a belt the first layer may be a brim apron band, which carries + // neither object nor support and so never gets the flag even when the print changes filament. + { + bool any_change = false; + unsigned int cur = m_wipe_tower_data.tool_ordering.first_extruder(); + for (const auto < : m_wipe_tower_data.tool_ordering.layer_tools()) + for (const unsigned int e : lt.extruders) + if (e != cur) { any_change = true; cur = e; } + if (! any_change) + return; + } this->throw_if_canceled(); @@ -191,11 +196,44 @@ void Print::_plan_belt_purge() // flush object), so truncating afterwards would leave dangling overrides // pointing into deleted layers. { + // The tool ordering covers the WHOLE print, and the prism is a printed + // object in it. Left unbounded, the scan below sees the prism's own + // toolchanges on layers above every model object -- the prism runs past + // them by design (ramp/height compensation at the tilted ends) -- so + // last_tc_z lands at the prism's own top and the truncation cancels + // nothing. The tower ends up justifying its own existence. + // + // Nothing above the tallest printed object can require a color change, + // so bound the scan there. On MCTEST5 that is 197 toolchanges spanning + // z=154.00..193.20 with the tallest object topping out at 153.80, i.e. + // 39.4 mm of tower that no swap ever needed. + // Support layers count too: on a belt they can extend above the object's + // own top, and a toolchange there is a real one. + double obj_top_z = -1.; + for (const PrintObject *po : m_objects) { + if (po->config().belt_purge_tower_object.value) + continue; + if (!po->layers().empty()) + obj_top_z = std::max(obj_top_z, po->layers().back()->print_z); + if (!po->support_layers().empty()) + obj_top_z = std::max(obj_top_z, po->support_layers().back()->print_z); + } + double last_tc_z = -1.; unsigned int cur_ext = m_wipe_tower_data.tool_ordering.first_extruder(); - for (const auto < : m_wipe_tower_data.tool_ordering.layer_tools()) + for (const auto < : m_wipe_tower_data.tool_ordering.layer_tools()) { + // layer_tools() is ordered by print_z ascending. + if (obj_top_z >= 0. && lt.print_z > obj_top_z + EPSILON) + break; for (const unsigned int e : lt.extruders) if (e != cur_ext) { last_tc_z = lt.print_z; cur_ext = e; } + } + // Deliberately NOT cancelling the prism outright when no object toolchange + // exists: belt_truncate_layers_above(0.) empties m_layers, and an object + // with zero layers is not something the rest of the pipeline expects. The + // GUI already declines to create a prism unless more than one filament is + // in use, so this case is a stale prism, not a hot path -- leave it whole + // rather than risk a zero-layer object. if (last_tc_z >= 0.) for (PrintObject *po : m_objects) if (po->config().belt_purge_tower_object.value && !po->layers().empty()) { @@ -204,21 +242,11 @@ void Print::_plan_belt_purge() } } - // Diagnostic: the prism only absorbs purge at toolchange layers whose - // print_z coincides with one of its own layers. Compare the prism's layer - // print_z range to the toolchange print_z range and count how many - // toolchange layers actually land on a prism layer. This distinguishes a - // range/grid-alignment failure (no coverage) from a capacity shortfall - // (covered but not enough cross-section). + // 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; } - const PrintObject *diag_prism = prism_po; - if (diag_prism != nullptr) - BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] purge prism layer range print_z=[" - << diag_prism->layers().front()->print_z << ", " << diag_prism->layers().back()->print_z - << "] nlayers=" << diag_prism->layers().size(); - int tc_layers = 0, tc_layers_covered = 0; float total_leftover = 0.f; float worst_layer_leftover = 0.f; @@ -227,33 +255,37 @@ void Print::_plan_belt_purge() 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; - bool layer_has_tc = false; for (const unsigned int extruder_id : layer_tools.extruders) { if (extruder_id == current_extruder_id) continue; - if (!layer_has_tc) { - layer_has_tc = true; - ++tc_layers; - if (diag_prism != nullptr && diag_prism->get_layer_at_printz(layer_tools.print_z, EPSILON) != nullptr) - ++tc_layers_covered; - } 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); - BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] purge toolchange print_z=" << layer_tools.print_z - << " filament " << current_extruder_id << "->" << extruder_id - << " requested=" << volume_to_wipe - << " absorbed=" << volume_to_wipe - leftover - << " leftover=" << leftover; layer_leftover += leftover; current_extruder_id = extruder_id; } - // Do not destructively remove unclaimed fill entities here. psWipeTower - // can rerun without regenerating infill, and a later tool ordering may - // need entities that were unclaimed by the previous plan. + // 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; @@ -265,11 +297,6 @@ void Print::_plan_belt_purge() this->throw_if_canceled(); } - BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] purge coverage: " << tc_layers_covered << "/" << tc_layers - << " toolchange layers land on a prism layer" - << (tc_layers > 0 && tc_layers_covered == 0 ? " (RANGE/GRID MISALIGNMENT — prism absorbs nothing)" : - tc_layers_covered < tc_layers ? " (partial coverage)" : " (full coverage)"); - if (total_leftover > 1.f) { this->active_step_add_warning( PrintStateBase::WarningLevel::CRITICAL, @@ -278,8 +305,6 @@ void Print::_plan_belt_purge() "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))); - BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] purge planning leftover total=" << total_leftover - << " worst_layer=" << worst_layer_leftover << " at print_z=" << worst_layer_z; } } @@ -297,11 +322,10 @@ void PrintObject::belt_shift_layer_grid(double delta) 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; - BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] belt_shift_layer_grid" - << " obj=" << this->model_object()->name - << " delta=" << delta - << " first_layer.print_z=" << (m_layers.empty() ? 0. : m_layers.front()->print_z); } // Belt mode: drop layers strictly above z (used to cancel the purge prism early @@ -324,12 +348,59 @@ size_t PrintObject::belt_truncate_layers_above(coordf_t z) m_layers.resize(keep); if (!m_layers.empty()) m_layers.back()->upper_layer = nullptr; - BOOST_LOG_TRIVIAL(debug) << "[BELT-DEBUG] truncate purge prism above print_z=" << z - << " kept=" << keep << " removed=" << removed - << " new_top=" << (m_layers.empty() ? 0. : m_layers.back()->print_z); 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 &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()) diff --git a/src/libslic3r/BeltSliceStrategy.cpp b/src/libslic3r/BeltSliceStrategy.cpp index cdb6ba428f..74d8566000 100644 --- a/src/libslic3r/BeltSliceStrategy.cpp +++ b/src/libslic3r/BeltSliceStrategy.cpp @@ -3,13 +3,6 @@ #include -#include -#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG -#include -#include -#include -#endif - namespace Slic3r { void BeltSliceStrategy::apply_preslice_transforms(Transform3d &trafo, @@ -45,79 +38,19 @@ void BeltSliceStrategy::apply_preslice_transforms(Transform3d &trafo, // 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. -#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG - // Capture the incoming trafo for diagnostic logging. - // This is the slicer-frame transform AFTER remap + rotation but BEFORE z_shift. - const Transform3d trafo_pre_shift = trafo; - auto log_mat = [](const Matrix3d &m) { - std::ostringstream ss; - ss << std::fixed << std::setprecision(4); - ss << "[[" << m(0,0) << "," << m(0,1) << "," << m(0,2) << "]," - << "[" << m(1,0) << "," << m(1,1) << "," << m(1,2) << "]," - << "[" << m(2,0) << "," << m(2,1) << "," << m(2,2) << "]]"; - return ss.str(); - }; - auto log_vec3 = [](const Vec3d &v) { - std::ostringstream ss; - ss << std::fixed << std::setprecision(4); - ss << "(" << v.x() << "," << v.y() << "," << v.z() << ")"; - return ss.str(); - }; - BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] apply_preslice_transforms enter" - << " has_rotation=" << has_rotation - << " has_remap=" << has_remap - << " trafo.linear=" << log_mat(trafo_pre_shift.linear()) - << " trafo.translation=" << log_vec3(trafo_pre_shift.translation()) - << " volumes=" << model_volumes.size(); -#endif double min_z = std::numeric_limits::max(); -#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG - int vol_idx = 0; -#endif for (const ModelVolume *mv : model_volumes) { -#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG - if (!mv->is_model_part()) { ++vol_idx; continue; } -#else if (!mv->is_model_part()) continue; -#endif Transform3d vol_trafo = trafo * mv->get_matrix(); const auto &its = mv->mesh().its; -#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG - // Per-volume bbox in mesh-frame and post-trafo slicer-frame. - Vec3d mesh_min(std::numeric_limits::max(), std::numeric_limits::max(), std::numeric_limits::max()); - Vec3d mesh_max(std::numeric_limits::lowest(), std::numeric_limits::lowest(), std::numeric_limits::lowest()); - Vec3d slicer_min(std::numeric_limits::max(), std::numeric_limits::max(), std::numeric_limits::max()); - Vec3d slicer_max(std::numeric_limits::lowest(), std::numeric_limits::lowest(), std::numeric_limits::lowest()); - double vol_min_z = std::numeric_limits::max(); -#endif for (const stl_vertex &v : its.vertices) { Vec3d vm = v.cast(); Vec3d pt = vol_trafo * vm; min_z = std::min(min_z, pt.z()); -#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG - mesh_min = mesh_min.cwiseMin(vm); - mesh_max = mesh_max.cwiseMax(vm); - slicer_min = slicer_min.cwiseMin(pt); - slicer_max = slicer_max.cwiseMax(pt); - vol_min_z = std::min(vol_min_z, pt.z()); -#endif } -#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG - BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] vol[" << vol_idx - << "] id=" << mv->id().id << " name='" << mv->name << "'" - << " mesh_bbox_min=" << log_vec3(mesh_min) << " mesh_bbox_max=" << log_vec3(mesh_max) - << " get_matrix.translation=" << log_vec3(mv->get_matrix().translation()) - << " slicer_bbox_min=" << log_vec3(slicer_min) << " slicer_bbox_max=" << log_vec3(slicer_max) - << " vol_min_z=" << vol_min_z; - ++vol_idx; -#endif } const double z_shift_val = (min_z < 0. && min_z != std::numeric_limits::max()) ? -min_z : 0.; -#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG - BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] combined min_z=" << min_z - << " z_shift_val=" << z_shift_val; -#endif if (z_shift_val > 0.) { Transform3d z_shift = Transform3d::Identity(); z_shift.matrix()(2, 3) = z_shift_val; @@ -126,18 +59,8 @@ void BeltSliceStrategy::apply_preslice_transforms(Transform3d &trafo, // out_belt_min_z is only meaningful in belt mode; the standalone-remap path // never reported it. if (out_belt_min_z && config.belt_printer.value) { - const double new_val = (min_z != std::numeric_limits::max()) ? min_z : 0.; -#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG - BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] write m_belt_min_z tid=" << std::this_thread::get_id() - << " target=" << out_belt_min_z << " old=" << *out_belt_min_z << " new=" << new_val; -#endif - *out_belt_min_z = new_val; + *out_belt_min_z = (min_z != std::numeric_limits::max()) ? min_z : 0.; } -#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG - BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] apply_preslice_transforms exit" - << " final_trafo.linear=" << log_mat(trafo.linear()) - << " final_trafo.translation=" << log_vec3(trafo.translation()); -#endif } } // namespace Slic3r diff --git a/src/libslic3r/BuildVolume.cpp b/src/libslic3r/BuildVolume.cpp index 4efcb89a91..15e6668bae 100644 --- a/src/libslic3r/BuildVolume.cpp +++ b/src/libslic3r/BuildVolume.cpp @@ -180,31 +180,6 @@ BuildVolume::BuildVolume(const std::vector &printable_area, const double BOOST_LOG_TRIVIAL(debug) << "BuildVolume printable_area clasified as: " << this->type_name(); } -void BuildVolume::set_belt_printer(bool enabled, double angle_deg, bool infinite_y) -{ - m_is_belt_printer = enabled; - m_belt_angle = angle_deg; - m_belt_infinite_y = infinite_y; - - // Restart from the unmodified bbox each call. Without this, toggling - // belt mode off (or switching infinite_y true→false) would leave the - // extents inflated and break collision / object_state checks. - BoundingBoxf bboxf = get_extents(m_bed_shape); - m_bboxf = BoundingBoxf3{ to_3d(bboxf.min, 0.), to_3d(bboxf.max, m_max_print_height) }; - - if (enabled) { - if (infinite_y) { - // Extend the Y bound to a very large value for infinite belt. - m_bboxf.max.y() = 100000.; - } - // Belt printer: the Z extent already equals printable_height (set above), which - // is the usable vertical clearance above the belt. The gantry's axis range is - // sized to reach height/cos(tilt), so no diagonal scaling is applied here — this - // keeps the live "outside build volume" highlight in agreement with Print::validate(). - (void) angle_deg; - } -} - #if 0 // Tests intersections of projected triangles, not just their vertices against a bounding box. // This test also correctly evaluates collision of a non-convex object with the bounding box. @@ -413,11 +388,6 @@ BuildVolume::ObjectState BuildVolume::object_state(const indexed_triangle_set& i build_volume.max.z() = std::numeric_limits::max(); if (ignore_bottom) build_volume.min.z() = -std::numeric_limits::max(); - // Belt printer: extend Y bounds for infinite Y. - if (m_is_belt_printer && m_belt_infinite_y) { - build_volume.min.y() = -std::numeric_limits::max(); - build_volume.max.y() = std::numeric_limits::max(); - } BoundingBox3Base build_volumef(build_volume.min.cast(), build_volume.max.cast()); // The following test correctly interprets intersection of a non-convex object with a rectangular build volume. //return rectangle_test(its, trafo, to_2d(build_volume.min), to_2d(build_volume.max), build_volume.max.z()); diff --git a/src/libslic3r/BuildVolume.hpp b/src/libslic3r/BuildVolume.hpp index 2b0d8c2746..f311d026b0 100644 --- a/src/libslic3r/BuildVolume.hpp +++ b/src/libslic3r/BuildVolume.hpp @@ -57,10 +57,6 @@ public: // Initialize from PrintConfig::printable_area and PrintConfig::printable_height BuildVolume(const std::vector &printable_area, const double printable_height, const std::vector> &extruder_areas, const std::vector& extruder_printable_heights); - // Belt printer configuration. - void set_belt_printer(bool enabled, double angle_deg, bool infinite_y); - bool is_belt_printer() const { return m_is_belt_printer; } - // Source data, unscaled coordinates. const std::vector& printable_area() const { return m_bed_shape; } double printable_height() const { return m_max_print_height; } @@ -143,10 +139,6 @@ private: // Source definition of the print volume height (PrintConfig::printable_height) double m_max_print_height { 0.f }; std::vector m_extruder_printable_height; - // Belt printer state. - bool m_is_belt_printer { false }; - double m_belt_angle { 0. }; - bool m_belt_infinite_y { false }; // Derived values. BuildVolume_Type m_type { BuildVolume_Type::Invalid }; diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index 90ec6f7ecb..021299f30b 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -84,8 +84,6 @@ set(lisbslic3r_sources BeltBrim.hpp BeltGCode.cpp BeltGCode.hpp - BeltGCodeWriter.cpp - BeltGCodeWriter.hpp BeltPurge.cpp BeltSliceStrategy.cpp BeltSliceStrategy.hpp @@ -245,6 +243,10 @@ set(lisbslic3r_sources 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 diff --git a/src/libslic3r/FirstLayerPlane.cpp b/src/libslic3r/FirstLayerPlane.cpp index 6a4feba886..f668ea774e 100644 --- a/src/libslic3r/FirstLayerPlane.cpp +++ b/src/libslic3r/FirstLayerPlane.cpp @@ -56,7 +56,7 @@ MachineZAffine compute_machine_z_affine(const PrintConfig &config) out.constant = trans; if (config.gcode_back_transform.value && config.belt_printer.value) { - // BeltGCodeWriter applies F^-1 before R when back-transform is on. + // BeltKinematics applies F^-1 before R when back-transform is on. // So machine_Z(slicing) = r_row · (F^-1 · slicing) + trans // = (r_row^T · F^-1) · slicing + trans // We need to compose r_row with F^-1 from the LEFT (treating r_row as diff --git a/src/libslic3r/Format/bbs_3mf.cpp b/src/libslic3r/Format/bbs_3mf.cpp index e2091da1db..366104bfa8 100644 --- a/src/libslic3r/Format/bbs_3mf.cpp +++ b/src/libslic3r/Format/bbs_3mf.cpp @@ -978,10 +978,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); } @@ -3815,11 +3815,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; } @@ -5109,6 +5116,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 @@ -5600,11 +5612,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; } diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 284c815f2d..d2fbea5742 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -6,6 +6,7 @@ #include "libslic3r.h" #include "I18N.hpp" #include "GCode.hpp" +#include #include "Exception.hpp" #include "ExtrusionEntity.hpp" #include "EdgeGrid.hpp" @@ -2961,10 +2962,9 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato m_fan_mover.release(); m_ordering_cache.clear(); - m_writer->set_is_bbl_machine(is_bbl_printers); - // Belt printer: initialize belt-specific writer via virtual hook. - this->init_belt_writer(print, is_bbl_printers); + this->init_belt_writer(print); + m_writer->set_is_bbl_machine(is_bbl_printers); // Standalone axis remap (works with or without belt mode). // Sync the writer's remap state to the current export UNCONDITIONALLY — even at @@ -2986,8 +2986,11 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato // circuit to the legacy Layer::id() == 0 path so g-code stays bit- // identical to the pre-feature behavior. m_first_layer_plane = std::make_unique(print.config()); - if (auto *belt_writer = dynamic_cast(m_writer.get())) { - belt_writer->set_first_layer_plane( + // Belt writers only: the plane also switches travel-speed selection to be + // per-point (see GCodeWriter::uses_pointwise_travel_speed()), which must not + // change for non-belt printers. + if (print.config().belt_printer.value) { + m_writer->set_first_layer_plane( m_first_layer_plane.get(), print.config().initial_layer_print_height.value); } @@ -3090,8 +3093,6 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato file.write_format("; HEADER_BLOCK_START\n"); // Write information on the generator. file.write_format("; generated by %s on %s\n", Slic3r::header_slic3r_generated().c_str(), Slic3r::Utils::local_timestamp().c_str()); - // Belt printer: embed angle and transform configs in header via virtual hook. - this->write_belt_header(file, print); if (is_bbl_printers) file.write_format(";%s\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Estimated_Printing_Time_Placeholder).c_str()); //BBS: total layer number @@ -3190,6 +3191,10 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato } } + // Belt printer: the tilt and transform settings the G-code viewer reads back. They + // are comments outside the config block, so they go after the thumbnails that a + // BTT TFT firmware needs first, and are written whether or not that header block is. + this->write_belt_header(file, print); // Write some terse information on the slicing parameters. const PrintObject *first_object = print.objects().front(); @@ -3888,12 +3893,12 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato // ORCA-Belt: the PA line test draws directly on the build surface in // logical bed coordinates — on a belt printer that surface is the // belt plane, not the slicing plane. - BeltGCodeWriter* belt_writer = dynamic_cast(m_writer.get()); - if (belt_writer != nullptr) - belt_writer->set_world_coordinates(true); + const bool belt_world_coords = print.config().belt_printer.value; + if (belt_world_coords) + install_belt_kinematics(*m_writer, print.config(), /*world_coordinates=*/true); gcode += pa_test.generate_test(params.start, params.step, std::llround(std::ceil((params.end - params.start) / params.step)) + 1); - if (belt_writer != nullptr) - belt_writer->set_world_coordinates(false); + if (belt_world_coords) + install_belt_kinematics(*m_writer, print.config(), /*world_coordinates=*/false); file.write(gcode); } else { @@ -5231,6 +5236,10 @@ std::string GCode::generate_object_brim(const Print &print, const PrintObject &o // geometry is already in plate coordinates. m_config.apply(print.default_region_config()); m_config.apply(object.config(), true); + // m_layer is not switched to this object until after brim emission, so name + // the belt-floor owner explicitly or the classification borrows whichever + // object was visited last. + BeltFloorObjectGuard floor_owner{ m_belt_floor_object, &object }; const Point &offset = object.instances()[instance_id].shift; this->set_origin(unscale(offset)); this->on_set_origin(&object, offset); @@ -5319,7 +5328,9 @@ LayerResult GCode::process_belt_brim_layer( break; } - m_cur_layer_idx = m_belt_brim_layer_idx ++; + // Apron bands precede object layer 0 and have no layer id of their own; they take the + // filament and nozzle assignment in effect at the first object layer. + m_cur_layer_idx = 0; // Publish the band's Z for _extrude()'s first-layer-plane probe, and make sure // it cannot leak past this layer even if an extrusion throws. @@ -5342,8 +5353,7 @@ LayerResult GCode::process_belt_brim_layer( // skip these layers entirely. { char buf[64]; - sprintf(buf, ";%s%g\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Layer_Change).c_str(), print_z); - gcode += buf; + gcode += ";" + GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Layer_Change) + "\n"; sprintf(buf, ";Z:%g\n", print_z); gcode += buf; const float band_height = float(height); @@ -5374,7 +5384,12 @@ LayerResult GCode::process_belt_brim_layer( print.config().layer_change_gcode.value, m_writer->filament()->id(), &config) + "\n"; } - gcode += this->emit_belt_brim_bands(print, layers, single_object_instance_idx); + // Objects sharing this apron Z may use different brim filaments; print each in its own tool. + for (const unsigned int brim_extruder : layer_tools.extruders) { + if (m_writer->filament() == nullptr || m_writer->filament()->id() != brim_extruder) + gcode += this->set_extruder(brim_extruder, print_z); + gcode += this->emit_belt_brim_bands(print, layers, single_object_instance_idx, brim_extruder); + } result.gcode = std::move(gcode); return result; @@ -5388,7 +5403,8 @@ LayerResult GCode::process_belt_brim_layer( // object layer, takes the ordinary path, and the band would be silently dropped. std::string GCode::emit_belt_brim_bands(const Print &print, const std::vector &layers, - const size_t single_object_instance_idx) + const size_t single_object_instance_idx, + const unsigned int extruder_id) { std::string gcode; for (const LayerToPrint <p : layers) { @@ -5396,9 +5412,15 @@ std::string GCode::emit_belt_brim_bands(const Print &print, if (band == nullptr || band->fills.empty() || ltp.original_object == nullptr) continue; const PrintObject &object = *ltp.original_object; + // belt_brim_filament() is 1-based. + if (! object.has_belt_brim() || static_cast(object.belt_brim_filament() - 1) != extruder_id) + continue; // Speeds, flow and retraction all read m_config. m_config.apply(print.default_region_config()); m_config.apply(object.config(), true); + // Apron bands have no Layer at all (m_layer is null here), so the belt + // floor owner has to be named the same way the object brim names it. + BeltFloorObjectGuard floor_owner{ m_belt_floor_object, &object }; const size_t i_begin = single_object_instance_idx == size_t(-1) ? 0 : single_object_instance_idx; const size_t i_end = single_object_instance_idx == size_t(-1) ? object.instances().size() : single_object_instance_idx + 1; @@ -6473,6 +6495,7 @@ LayerResult GCode::process_layer( std::vector &objects_by_extruder = objects_by_extruder_it->second; std::vector &instances = filament_plan.first; std::vector nodes; + std::vector> layout; // Per instance, see IslandOrderCacheEntry std::vector node_instances; auto quantize_to_mm = [](const Point &pt) -> Point { const coord_t grid = coord_t(scale_(1.)); @@ -6497,6 +6520,7 @@ LayerResult GCode::process_layer( const size_t instance_idx = instances.size(); instances.emplace_back(object_by_extruder, layer_id, *print_object, instance_id, print_object->instances()[instance_id].model_instance->get_labeled_id()); + layout.emplace_back(islands.size(), ! islands.empty() && ! islands.back().by_region.empty()); const Point &shift = print_object->instances()[instance_id].shift; const size_t first_node = nodes.size(); if (islands_chainable) @@ -6516,8 +6540,9 @@ LayerResult GCode::process_layer( // Reuse the cached tour while this filament's island layout is unchanged. auto &cache_entry = m_ordering_cache[filament_id]; - if (!(cache_entry.first == nodes)) { - cache_entry.first = nodes; + if (! (cache_entry.nodes == nodes && cache_entry.layout == layout)) { + cache_entry.nodes = nodes; + cache_entry.layout = layout; Points node_points; node_points.reserve(nodes.size()); for (const IslandOrderNode &node : nodes) @@ -6550,12 +6575,12 @@ LayerResult GCode::process_layer( // A visit without explicit islands already prints everything. continue; std::vector &islands = instances[i].object_by_extruder.islands; - if (!islands.back().by_region.empty()) + if (! islands.empty() && ! islands.back().by_region.empty()) last_visit.islands.emplace_back(islands.size() - 1); } - cache_entry.second = std::move(visits); + cache_entry.visits = std::move(visits); } - filament_plan.second = cache_entry.second; + filament_plan.second = cache_entry.visits; } } @@ -6623,39 +6648,11 @@ LayerResult GCode::process_layer( std::set> belt_brim_emitted; // Emit every ORDINARY-layer apron band (belt_brim_prologue band coinciding with an - // object/support layer) whose brim filament is this pass's extruder. Mirrors - // emit_belt_brim_bands() per band, but filtered to one brim filament so each band + // object/support layer) whose brim filament is this pass's extruder, so each band // prints in the correct tool's pass (Finding B). extruder_id is 0-based (the - // reindexed tool domain); belt_brim_filament() is 1-based, so subtract one. + // reindexed tool domain). auto emit_belt_brim_for_extruder = [this, &print, &layers, single_object_instance_idx](unsigned int extruder_id) -> std::string { - std::string gc; - for (const LayerToPrint <p : layers) { - const BeltBrimBand *band = ltp.belt_brim_band; - if (band == nullptr || band->fills.empty() || ltp.original_object == nullptr) - continue; - const PrintObject &object = *ltp.original_object; - if (! object.has_belt_brim() || (unsigned int)(object.belt_brim_filament() - 1) != extruder_id) - continue; - // Speeds, flow and retraction all read m_config. - m_config.apply(print.default_region_config()); - m_config.apply(object.config(), true); - const size_t i_begin = single_object_instance_idx == size_t(-1) ? 0 : single_object_instance_idx; - const size_t i_end = single_object_instance_idx == size_t(-1) ? object.instances().size() - : single_object_instance_idx + 1; - for (size_t i = i_begin; i < i_end && i < object.instances().size(); ++ i) { - // Band geometry is object-local, like the object's own extrusions. - const Point &offset = object.instances()[i].shift; - this->set_origin(unscale(offset)); - this->on_set_origin(&object, offset); - m_avoid_crossing_perimeters.use_external_mp(); - for (const ExtrusionEntity *ee : band->fills.entities) - if (ee != nullptr) - gc += this->extrude_entity(*ee, "brim", NOZZLE_CONFIG(support_speed)); - m_avoid_crossing_perimeters.use_external_mp(false); - m_avoid_crossing_perimeters.disable_once(); - } - } - return gc; + return this->emit_belt_brim_bands(print, layers, single_object_instance_idx, extruder_id); }; for (unsigned int extruder_id : layer_tools.extruders) @@ -6906,7 +6903,13 @@ LayerResult GCode::process_layer( // in this instance's frame after set_origin() above). Empty islands are skipped; // the trailing catch-all island has no centroid to chain by and always goes last. std::vector &islands = instance_to_print.object_by_extruder.islands; - std::vector island_order = visit.islands; + std::vector island_order; + island_order.reserve(visit.islands.size()); + for (size_t idx : visit.islands) // Never index past the islands (see IslandOrderCacheEntry) + if (idx < islands.size()) + island_order.emplace_back(idx); + else + BOOST_LOG_TRIVIAL(error) << "island tour refers to island " << idx << " of " << islands.size() << ", skipped"; if (island_order.empty()) { island_order.reserve(islands.size()); if (layer_to_print.object_layer != nullptr && islands.size() == layer_to_print.object_layer->lslices.size() + 1) { @@ -7671,8 +7674,13 @@ std::string GCode::extrude_loop(const ExtrusionLoop& loop_ref, loop.split_at(last_pos, false); const auto seam_scarf_type = m_config.seam_slope_type.value; + // Belt printers never get a scarf joint. The scarf starts one layer height + // below the layer, which on a tilted belt is a step backwards along the belt + // axis into the previous layer's wall at the seam (0.28 mm at 45 degrees per + // 0.2 mm layer); with an aligned seam that ram repeats at the same spot on + // every layer and knocks the part loose. bool enable_seam_slope = ((seam_scarf_type == SeamScarfType::External && !is_hole) || seam_scarf_type == SeamScarfType::All) && - !m_config.spiral_mode && + !m_config.spiral_mode && !m_config.belt_printer.value && (loop.role() == erExternalPerimeter || (loop.role() == erPerimeter && m_config.seam_slope_inner_walls)) && layer_id() > 0; const auto nozzle_diameter = EXTRUDER_CONFIG(nozzle_diameter); @@ -8398,20 +8406,6 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, auto _mm3_per_mm = path.mm3_per_mm * this->config().print_flow_ratio; _mm3_per_mm *= filament_flow_ratio; - // Belt printer: compensate for any volume change introduced by the mesh - // forward transform. path.mm3_per_mm is derived from slicer-frame layer - // height × line width, but a slicer-frame slab of volume V maps under the - // back-transform to a machine-frame region of volume V / |det(T)|. The - // mesh transform is now rotation ∘ pre-remap, both orthogonal, so |det(T)| - // is always 1 and this is currently a no-op; it is retained as a guard in - // case a non-orthogonal mesh transform is ever reintroduced. (Machine-frame - // shear/scale acts on the g-code in BeltGCodeWriter, not here.) - if (m_config.belt_printer.value) { - double det = std::abs(BeltTransformPipeline::build_forward_transform(m_config).linear().determinant()); - if (det > EPSILON) - _mm3_per_mm /= det; - } - if (path.role() == erTopSolidInfill) { _mm3_per_mm *= m_config.top_solid_infill_flow_ratio; } else if (path.role() == erBottomSurface) { @@ -8515,6 +8509,20 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, // the speed fade tracks perpendicular distance from the plane on // belt printers; otherwise this falls back to the slicing layer id. const int _layer = this->effective_layer_index_for_point(path_point_mm); + // Belt printers: a tilted layer runs from the belt to the top of the part, so the + // "first layers" the fan stays off for are a band along the belt. Mark where the + // extrusion enters and leaves it, per segment, for the cooling buffer. + const bool belt_band_tags = m_enable_cooling_markers && m_config.belt_printer.value; + const int belt_band_layers = belt_band_tags ? m_config.close_fan_the_first_x_layers.get_at(m_writer->filament()->id()) : 0; + auto tag_belt_band = [this, &gcode, belt_band_tags, belt_band_layers, z = path_point_mm.z()](coord_t x, coord_t y) { + if (! belt_band_tags) + return; + const bool in_band = this->effective_layer_index_for_point(Vec3d(unscale(x), unscale(y), z)) < belt_band_layers; + if (in_band != m_belt_in_band) { + gcode += in_band ? ";_BELT_BAND_START\n" : ";_BELT_BAND_END\n"; + m_belt_in_band = in_band; + } + }; if (path_on_first_layer || object_layer_over_raft()) { //BBS: for solid infill of first layer, speed can be higher as long as //wall lines have be attached @@ -8999,6 +9007,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, tempDescription += Slic3r::format(" | Old Flow Value: %0.5f Length: %0.5f",oldE, line_length); } } + tag_belt_band((line.a.x() + line.b.x()) / 2, (line.a.y() + line.b.y()) / 2); if (path.z_contoured) { // ZAA: Z anti-aliased extrusion with variable Z per point Vec2d dest2d = this->point_to_gcode(line.b.to_point()); @@ -9129,6 +9138,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, const ProcessedPoint &processed_point = new_points[i]; const ProcessedPoint &pre_processed_point = new_points[i-1]; Vec3d p = this->point_to_gcode_quantized(processed_point.p); + tag_belt_band((pre_processed_point.p.x() + processed_point.p.x()) / 2, (pre_processed_point.p.y() + processed_point.p.y()) / 2); if (m_enable_cooling_markers) { if (enable_overhang_bridge_fan) { cur_fan_enabled = check_overhang_fan(processed_point.overlap, path.role()); @@ -9423,6 +9433,7 @@ std::string GCode::travel_to(const Point& point, ExtrusionRole role, std::string // multi-hop travel path inside the configuration space if (m_config.reduce_crossing_wall && !m_avoid_crossing_perimeters.disabled_once() + && m_layer != nullptr // A brim apron layer has no Layer to avoid crossing && m_writer->is_current_position_clear()) //BBS: don't generate detour travel paths when current position is unclea { @@ -9447,7 +9458,8 @@ std::string GCode::travel_to(const Point& point, ExtrusionRole role, std::string // When "Wipe while retracting" is enabled, then extruder moves to another position, and travel from this position can cross perimeters. // Because of it, it is necessary to call avoid crossing perimeters again with new starting point after calling retraction() // FIXME Lukas H.: Try to predict if this second calling of avoid crossing perimeters will be needed or not. It could save computations. - if (last_post_before_retract != this->last_pos() && m_config.reduce_crossing_wall) { + if (last_post_before_retract != this->last_pos() && m_config.reduce_crossing_wall + && m_layer != nullptr) { // A brim apron layer has no Layer to avoid crossing // If in the previous call of m_avoid_crossing_perimeters.travel_to was use_external_mp_once set to true restore this value for next call. if (used_external_mp_once) m_avoid_crossing_perimeters.use_external_mp_once(); @@ -10288,6 +10300,10 @@ std::string GCode::set_object_info(Print *print) { for (PrintInstance& inst : object->instances()) { inst.unique_id = unique_id++; inst.id = inst_id++; + // Outlines are in plate coordinates. On a belt printer that is the frame after + // the slicing rotation has been undone and before the G-code axis remap and + // machine-frame shear: where the object stands on the belt, which is what an + // object picker shows. Klipper cancels by name, so nothing depends on more. auto bbox = inst.get_bounding_box(); auto center = print->translate_to_print_space(Vec2d(bbox.center().x(), bbox.center().y())); auto inst_name = get_instance_name(object, inst); @@ -10310,6 +10326,48 @@ std::string GCode::set_object_info(Print *print) { return gcode.str(); } +bool GCode::belt_height_above_floor(const Vec3d &point_slicing_mm, double &height_mm) const +{ + // The owning object, which is what carries the belt description. During + // object-brim and coincident-apron emission m_layer still points at whichever + // object was visited last (or at nothing at all), so those paths publish the + // owner explicitly -- otherwise a brim's speed would depend on plate order. + const PrintObject *object = m_belt_floor_object != nullptr ? m_belt_floor_object + : (m_layer != nullptr ? m_layer->object() : nullptr); + if (object == nullptr) + return false; + // Respect an explicit first-layer-plane choice: only Auto and BeltAffine mean + // "use the belt". A user who selected XY, YZ or XZ has asked for the + // FirstLayerPlane evaluator and must keep it. + const FirstLayerPlaneMode mode = m_config.first_layer_plane.value; + if (mode != FirstLayerPlaneMode::Auto && mode != FirstLayerPlaneMode::BeltAffine) + return false; + // Likewise for a dialled-in plane offset. It is expressed as a machine-Z + // shift that FirstLayerPlane converts into a perpendicular distance in the + // slicing frame; this evaluator measures along slicing Z instead, so there is + // no faithful translation of it here. Honour the user's setting by deferring + // to the evaluator that implements it rather than silently dropping it. + if (std::abs(m_config.first_layer_plane_offset.value) > EPSILON) + return false; + + const SlicingParameters &sp = object->slicing_parameters(); + // Deliberately NOT BeltFloorContext: its init() folds in + // belt_support_floor_offset, a support-generator diagnostic. Letting that + // option move the model's first-layer speed band would be a surprising + // coupling -- a negative value would switch the slowdown off entirely. + // The belt surface itself is just shear * u + z_shift. + if (std::abs(sp.belt_floor_shear_factor) < EPSILON) + return false; + const double u = sp.belt_floor_from_axis == 0 ? point_slicing_mm.x() : point_slicing_mm.y(); + const double floor_z = sp.belt_floor_shear_factor * u + sp.belt_floor_z_shift; + // Measured along the slicing Z, not perpendicular to the belt: layers are + // horizontal slabs in the sliced frame, so the slab holding the material that + // rests on the belt at this point is the one within one layer height of it. + // A perpendicular measure would shrink the band by 1/cos(tilt). + height_mm = point_slicing_mm.z() - floor_z; + return true; +} + // convert a model-space scaled point into G-code coordinates Vec2d GCode::point_to_gcode(const Point &point) const { diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index 17a7ce4608..17221101fd 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -2,9 +2,10 @@ #define slic3r_GCode_hpp_ #include "libslic3r.h" +#include #include "ExPolygon.hpp" #include "GCodeWriter.hpp" -#include "BeltGCodeWriter.hpp" +#include "GCode/BeltKinematics.hpp" #include "FirstLayerPlane.hpp" #include "Layer.hpp" #include "Point.hpp" @@ -228,7 +229,7 @@ public: void do_export(Print* print, const char* path, GCodeProcessorResult* result = nullptr, ThumbnailsGeneratorCallback thumbnail_cb = nullptr); void export_layer_filaments(GCodeProcessorResult* result); //BBS: set offset for gcode writer - void set_gcode_offset(double x, double y) { m_writer->set_xy_offset(x, y); m_processor.set_xy_offset(x, y);} + void set_gcode_offset(double x, double y) { m_gcode_offset = Vec2d(x, y); m_writer->set_xy_offset(x, y); m_processor.set_xy_offset(x, y);} // Exported for the helper classes (OozePrevention, Wipe) and for the Perl binding for unit tests. const Vec2d& origin() const { return m_origin; } @@ -376,10 +377,14 @@ protected: // Virtual hooks for belt printer subclass (BeltGCode). // No-ops in base GCode; overridden in BeltGCode. - virtual void init_belt_writer(Print &print, bool is_bbl_printers) {} + 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) {} - virtual bool should_disable_arc_fitting() const { return false; } + // 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); @@ -418,13 +423,14 @@ protected: const bool last_layer, const size_t single_object_instance_idx); - // Emit the apron bands carried by these layers. 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. + // 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 &layers, - const size_t single_object_instance_idx); + const size_t single_object_instance_idx, + const unsigned int extruder_id); LayerResult process_layer( const Print &print, @@ -627,9 +633,21 @@ protected: }; // 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, std::vector>> - 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 nodes; + std::vector> layout; + std::vector visits; + }; + std::map m_ordering_cache; ExtrusionQualityEstimator m_extrusion_quality_estimator; @@ -767,6 +785,8 @@ protected: // printers without a Z-axis shear; in that case all per-path plane // checks short-circuit to the legacy Layer::id() == 0 path. std::unique_ptr m_first_layer_plane; + // Plate origin, kept so a writer replaced during export can be given it again. + Vec2d m_gcode_offset{ Vec2d::Zero() }; std::unique_ptr m_pressure_equalizer; @@ -824,8 +844,20 @@ protected: // _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 m_belt_brim_z; - // Counter standing in for Layer::id() on apron layers, which precede layer 0. - size_t m_belt_brim_layer_idx{0}; + // 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 m_initial_layer_extruders; std::vector> m_sorted_layer_filaments; @@ -849,6 +881,12 @@ protected: // 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 { + // Belt printers: measure height above the belt surface itself, in the + // slicing frame. See belt_height_above_floor() for why this does not go + // through FirstLayerPlane. + double h; + if (this->belt_height_above_floor(point_slicing_mm, h)) + return h <= m_config.initial_layer_print_height.value + EPSILON; if (m_first_layer_plane && m_first_layer_plane->is_active()) return m_first_layer_plane->is_first_layer( point_slicing_mm, m_config.initial_layer_print_height.value); @@ -859,10 +897,40 @@ protected: // perpendicular distance to the plane in band_thickness_mm units; // otherwise it returns 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)); + } if (m_first_layer_plane && m_first_layer_plane->is_active()) return m_first_layer_plane->effective_layer_index(point_slicing_mm); return on_first_layer() ? 0 : layer_id(); } + + // Band thickness for the *effective layer index* only. FirstLayerPlane keeps + // two separate thresholds and so must this path: is_first_layer() tests + // against initial_layer_print_height, while effective_layer_index() counts + // bands of first_layer_plane_thickness. Conflating them would apply + // first-layer treatment through a whole 1mm band on a 0.2mm first layer. + double first_layer_band_mm() const { + double band = m_config.first_layer_plane_thickness.value; + if (band <= 0.) 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. FirstLayerPlane instead derives its + // plane by composing gcode_remap_* with the g-code back-transform, so its + // answer changes with the machine's *output* axis convention: on a printer + // with a non-identity remap it reported ~86mm of clearance for geometry + // sitting directly on the belt, and no extrusion was ever classified as + // first-layer. Measuring against the belt itself is independent of every + // remap and back-transform. + bool belt_height_above_floor(const Vec3d &point_slicing_mm, double &height_mm) const; int layer_id() const { if (m_layer == nullptr) return -1; diff --git a/src/libslic3r/GCode/BeltBackTransform.hpp b/src/libslic3r/GCode/BeltBackTransform.hpp index b98770c688..878ae12b5a 100644 --- a/src/libslic3r/GCode/BeltBackTransform.hpp +++ b/src/libslic3r/GCode/BeltBackTransform.hpp @@ -13,7 +13,7 @@ namespace Slic3r { // machine's real coordinate space. // // Initialized once from PrintConfig, then applied per-point in -// GCodeWriter::to_machine_coords() before axis remapping. +// BeltKinematics::to_machine() before axis remapping. // // Active when gcode_back_transform is true AND at least one of: // - a shear axis has global mode enabled, or diff --git a/src/libslic3r/GCode/BeltKinematics.cpp b/src/libslic3r/GCode/BeltKinematics.cpp new file mode 100644 index 0000000000..155e18d32d --- /dev/null +++ b/src/libslic3r/GCode/BeltKinematics.cpp @@ -0,0 +1,40 @@ +#include "BeltKinematics.hpp" +#include "../BeltTransform.hpp" +#include "../PrintConfig.hpp" +#include "../GCodeWriter.hpp" + +namespace Slic3r { + +BeltKinematics::BeltKinematics(const PrintConfig &config, bool world_coordinates) + : m_world_coordinates(world_coordinates) +{ + m_back_active = m_back_transform.init_from_config(config); + m_machine_frame.init_from_config(config); + if (m_back_active) + // BeltBackTransform stores the inverse of this; keep the forward so + // to_logical() can reverse the whole chain. + m_back_forward = BeltTransformPipeline::build_forward_transform(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); +} + +Vec3d BeltKinematics::to_logical(const Vec3d &machine) const +{ + const Vec3d before_frame = m_machine_frame.apply_inverse(machine); + const Vec3d before_remap = this->apply_axis_remap_inverse(before_frame); + if (m_world_coordinates || ! m_back_active) + return before_remap; + return m_back_forward * before_remap; +} + +void install_belt_kinematics(GCodeWriter &writer, const PrintConfig &config, bool world_coordinates) +{ + writer.set_kinematics(std::make_unique(config, world_coordinates)); +} + +} // namespace Slic3r diff --git a/src/libslic3r/GCode/BeltKinematics.hpp b/src/libslic3r/GCode/BeltKinematics.hpp new file mode 100644 index 0000000000..f145ecf3e9 --- /dev/null +++ b/src/libslic3r/GCode/BeltKinematics.hpp @@ -0,0 +1,69 @@ +#ifndef slic3r_BeltKinematics_hpp_ +#define slic3r_BeltKinematics_hpp_ + +#include "MachineKinematics.hpp" +#include "BeltBackTransform.hpp" +#include "MachineFrameTransform.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; + Vec3d to_logical(const Vec3d &machine) const override; + // Machine -> build-volume frame. Only the machine-frame shear/scale is undone, + // matching what GCodeProcessor's bounds validation wants. This is deliberately + // NOT to_logical(). + Vec3d to_build_volume(const Vec3d &machine) const override + { return m_machine_frame.apply_inverse(machine); } + + // 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; } + + bool world_coordinates() const { return m_world_coordinates; } + +private: + BeltBackTransform m_back_transform; + MachineFrameTransform m_machine_frame; + // Forward of what m_back_transform inverts, kept so to_logical() can undo it. + Transform3d m_back_forward { Transform3d::Identity() }; + bool m_back_active { false }; + 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_ diff --git a/src/libslic3r/GCode/CoolingBuffer.cpp b/src/libslic3r/GCode/CoolingBuffer.cpp index 1df6ce3bb9..a47dd2c148 100644 --- a/src/libslic3r/GCode/CoolingBuffer.cpp +++ b/src/libslic3r/GCode/CoolingBuffer.cpp @@ -1,5 +1,4 @@ #include "../GCode.hpp" -#include "../FirstLayerPlane.hpp" #include "CoolingBuffer.hpp" #include #include @@ -33,15 +32,11 @@ CoolingBuffer::CoolingBuffer(GCode &gcodegen) : m_config(gcodegen.config()), m_t m_extruder_ids.emplace_back(ex.id()); } - // Borrow the first-layer plane from the GCode generator. When inactive - // (non-belt printers and belt printers without Z shear), per-line fan - // re-evaluation is skipped and behavior is bit-identical to the legacy - // per-layer path. - m_first_layer_plane = gcodegen.first_layer_plane(); } 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()); @@ -81,6 +76,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) : @@ -338,13 +336,6 @@ std::string CoolingBuffer::process_layer(std::string &&gcode, size_t layer_id, b std::vector per_extruder_adjustments = this->parse_layer_gcode(m_gcode, m_current_pos); float layer_time_stretched = this->calculate_layer_slowdown(per_extruder_adjustments); out = this->apply_layer_cooldown(m_gcode, layer_id, layer_time_stretched, per_extruder_adjustments); - // First-layer plane: per-segment fan re-evaluation post-pass. Walks - // the cooled-down gcode and inserts inline M106 commands at band - // crossings (where the path's perpendicular distance to the plane - // crosses close_fan_the_first_x_layers thresholds). No-op when - // the evaluator is inactive. - if (m_first_layer_plane && m_first_layer_plane->is_active()) - out = this->apply_first_layer_plane_fan_eval(std::move(out), layer_id, layer_time_stretched); m_gcode.clear(); } return out; @@ -548,6 +539,10 @@ std::vector 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; @@ -889,7 +884,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; @@ -903,6 +900,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); @@ -955,6 +954,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)) { @@ -1047,7 +1053,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); @@ -1076,214 +1090,4 @@ std::string CoolingBuffer::apply_layer_cooldown( return new_gcode; } -// Pure helper: compute the main fan speed for a given effective layer index. -// Mirrors the inline logic in change_extruder_set_fan but is callable from -// per-line code in apply_first_layer_plane_fan_eval. -int CoolingBuffer::compute_main_fan_speed(int effective_layer_id, float layer_time, - unsigned int extruder_id) const -{ -#define EXTRUDER_CFG(opt) m_config.opt.get_at(extruder_id) - float fan_min_speed = EXTRUDER_CFG(fan_min_speed); - float fan_max_speed = EXTRUDER_CFG(fan_max_speed); - bool reduce_fan_stop_start_freq = EXTRUDER_CFG(reduce_fan_stop_start_freq); - int close_fan_the_first_x_layers = EXTRUDER_CFG(close_fan_the_first_x_layers); - int full_fan_speed_layer = EXTRUDER_CFG(full_fan_speed_layer); - float slow_down_layer_time = float(EXTRUDER_CFG(slow_down_layer_time)); - float fan_cooling_layer_time = float(EXTRUDER_CFG(fan_cooling_layer_time)); -#undef EXTRUDER_CFG - - if (close_fan_the_first_x_layers <= 0 && full_fan_speed_layer > 0) - close_fan_the_first_x_layers = 1; - - float fan_speed_new = reduce_fan_stop_start_freq ? fan_min_speed : 0.f; - if (effective_layer_id >= close_fan_the_first_x_layers) { - if (layer_time < slow_down_layer_time) { - fan_speed_new = fan_max_speed; - } else if (layer_time < fan_cooling_layer_time) { - double t = (layer_time - slow_down_layer_time) / - (fan_cooling_layer_time - slow_down_layer_time); - fan_speed_new = float(int(floor(t * fan_min_speed + - (1. - t) * fan_max_speed) + 0.5)); - } - if (effective_layer_id + 1 < full_fan_speed_layer) { - float factor = float(effective_layer_id + 1 - close_fan_the_first_x_layers) - / float(full_fan_speed_layer - close_fan_the_first_x_layers); - fan_speed_new = float(std::clamp(int(fan_speed_new * factor + 0.5f), 0, 255)); - } - } else { - fan_speed_new = 0.f; - } - return int(fan_speed_new); -} - -// Post-pass: walk the cooled-down gcode line by line, track XYZ position, -// and insert M106 commands at first-layer-plane band crossings so the fan -// follows perpendicular distance to the plane rather than the slicing-layer -// index. Only invoked when the FirstLayerPlane evaluator is active. -// -// This implementation is intentionally minimal: it overrides only the MAIN -// fan (the one set by GCodeWriter::set_fan); overhang/internal-bridge/etc -// special fans remain at their layer-level values from apply_layer_cooldown. -// That keeps the per-line logic small while still giving the user precise -// fan control near the belt surface, which is the main quality concern. -std::string CoolingBuffer::apply_first_layer_plane_fan_eval( - std::string &&gcode_in, size_t /*layer_id*/, float layer_time) -{ - if (!m_first_layer_plane || !m_first_layer_plane->is_active()) - return std::move(gcode_in); - - const std::string &gcode = gcode_in; - std::string out; - out.reserve(gcode.size() + 256); - - // Match the PWM floor applied at every other set_fan call in this file so - // band-crossing M106 emissions start the fan reliably at low speeds. - const unsigned int part_cooling_fan_min_pwm = static_cast(std::max(0, m_config.part_cooling_fan_min_pwm.value)); - - // Track position in slicing-frame mm. Seed from m_current_pos which the - // CoolingBuffer keeps up-to-date across layers. - Vec3d cur_pos_mm(m_current_pos[0], m_current_pos[1], m_current_pos[2]); - - // Track current main fan speed by parsing M106 commands as we walk so - // we can restore it after a band exit. - int current_main_fan = m_fan_speed; - int pre_band_main_fan = current_main_fan; - // Implicit initial state: assume the layer started "out of the band" - // (i.e., the layer-level fan setting from apply_layer_cooldown is in - // effect). The first movement we encounter will reconcile this. - bool in_first_layer_band = false; - unsigned int active_extruder = m_current_extruder; - - auto parse_xyz_into = [](const std::string_view &line_sv, Vec3d &p) { - if (line_sv.size() < 3) return false; - if (line_sv[0] != 'G') return false; - if (line_sv[1] != '0' && line_sv[1] != '1') return false; - if (line_sv[2] != ' ' && line_sv[2] != '\t') return false; - const char *c = line_sv.data() + 3; - const char *end = line_sv.data() + line_sv.size(); - bool any = false; - while (c < end && *c != ';') { - while (c < end && (*c == ' ' || *c == '\t')) ++c; - if (c >= end || *c == ';' || *c == '\n' || *c == '\r') break; - char axis = *c; - ++c; - if (axis == 'X' || axis == 'Y' || axis == 'Z') { - char *next; - double v = std::strtod(c, &next); - if (next != c) { - if (axis == 'X') p.x() = v; - else if (axis == 'Y') p.y() = v; - else p.z() = v; - c = next; - any = true; - continue; - } - } - // Skip unrecognized word. - while (c < end && *c != ' ' && *c != '\t' && *c != ';' && *c != '\n') - ++c; - } - return any; - }; - - auto parse_m106 = [](const std::string_view &line_sv) -> int { - // Returns -1 if not an M106, otherwise the S value (0..255). - if (line_sv.size() < 4 || line_sv[0] != 'M') return -1; - if (!(line_sv[1] == '1' && line_sv[2] == '0' && line_sv[3] == '6')) - return -1; - // Find S - size_t s_pos = line_sv.find('S'); - if (s_pos == std::string_view::npos) return -1; - const char *c = line_sv.data() + s_pos + 1; - char *next; - long v = std::strtol(c, &next, 10); - if (next == c) return -1; - return int(std::clamp(v, 0, 255)); - }; - - auto parse_m107 = [](const std::string_view &line_sv) -> bool { - return line_sv.size() >= 4 && line_sv[0] == 'M' && - line_sv[1] == '1' && line_sv[2] == '0' && line_sv[3] == '7'; - }; - - auto parse_tool_change = [this](const std::string_view &line_sv) -> int { - // Returns the new extruder id, or -1 if not a toolchange. - if (line_sv.size() < m_toolchange_prefix.size() + 1) return -1; - if (line_sv.compare(0, m_toolchange_prefix.size(), m_toolchange_prefix) != 0) - return -1; - const char *c = line_sv.data() + m_toolchange_prefix.size(); - char *next; - long v = std::strtol(c, &next, 10); - if (next == c) return -1; - return int(v); - }; - - const char *p = gcode.c_str(); - const char *end = gcode.c_str() + gcode.size(); - while (p < end) { - const char *line_end = p; - while (line_end < end && *line_end != '\n') ++line_end; - const char *next_line = line_end; - if (next_line < end) ++next_line; // include the '\n' - - std::string_view line_sv(p, line_end - p); - - // Track tool changes so the per-line fan eval uses the right extruder. - int new_tool = parse_tool_change(line_sv); - if (new_tool >= 0) - active_extruder = unsigned(new_tool); - - // Track existing fan commands so we can restore the right value when - // exiting a band. - int m106_speed = parse_m106(line_sv); - if (m106_speed >= 0) { - current_main_fan = m106_speed; - if (!in_first_layer_band) - pre_band_main_fan = m106_speed; - } else if (parse_m107(line_sv)) { - current_main_fan = 0; - if (!in_first_layer_band) - pre_band_main_fan = 0; - } - - // Movement line: parse XYZ, evaluate plane, possibly emit a fan - // change BEFORE this line. - bool moved = parse_xyz_into(line_sv, cur_pos_mm); - if (moved) { - const int eff_idx = m_first_layer_plane->effective_layer_index(cur_pos_mm); - const int close_n = m_config.close_fan_the_first_x_layers.get_at(active_extruder); - const bool now_in_band = eff_idx < std::max(close_n, 1); - if (now_in_band != in_first_layer_band) { - // Band crossing: emit a M106 with the appropriate speed. - int target_fan; - if (now_in_band) { - // Entering the first-layer band: fan off. - pre_band_main_fan = current_main_fan; - target_fan = compute_main_fan_speed(eff_idx, layer_time, active_extruder); - } else { - // Exiting the band: restore the layer's normal fan speed. - // Use compute_main_fan_speed with the effective index so - // the linear ramp factor (close_fan→full_fan_speed_layer) - // also follows distance from the plane. - target_fan = compute_main_fan_speed(eff_idx, layer_time, active_extruder); - if (target_fan == 0) - target_fan = pre_band_main_fan; - } - if (target_fan != current_main_fan) { - out += GCodeWriter::set_fan(m_config.gcode_flavor, target_fan, part_cooling_fan_min_pwm); - current_main_fan = target_fan; - m_fan_speed = target_fan; - m_current_fan_speed = target_fan; - } - in_first_layer_band = now_in_band; - } - } - - out.append(p, next_line - p); - p = next_line; - } - - return out; -} - } // namespace Slic3r diff --git a/src/libslic3r/GCode/CoolingBuffer.hpp b/src/libslic3r/GCode/CoolingBuffer.hpp index 1adb6f5005..e0b5044921 100644 --- a/src/libslic3r/GCode/CoolingBuffer.hpp +++ b/src/libslic3r/GCode/CoolingBuffer.hpp @@ -10,7 +10,6 @@ namespace Slic3r { class GCode; class Layer; -class FirstLayerPlane; struct PerExtruderAdjustments; // A standalone G-code filter, to control cooling of the print. @@ -37,21 +36,6 @@ private: // Returns the adjusted G-code. std::string apply_layer_cooldown(const std::string &gcode, size_t layer_id, float layer_time, std::vector &per_extruder_adjustments); - // First-layer plane: per-line fan re-evaluation post-pass. Walks the - // post-cooldown gcode, tracks XYZ position, and inserts M106 commands at - // band-crossing transitions in slicing-frame coordinates. Only runs - // when m_first_layer_plane is active. - std::string apply_first_layer_plane_fan_eval(std::string &&gcode_in, - size_t layer_id, - float layer_time); - - // Pure helper: compute the main fan speed for a given effective layer - // index (layer-id units, mapped through the plane evaluator) and the - // current extruder. Mirrors the inline logic in the change_extruder_set_fan - // lambda but is callable from per-line code. - int compute_main_fan_speed(int effective_layer_id, float layer_time, - unsigned int extruder_id) const; - // G-code snippet cached for the support layers preceding an object layer. std::string m_gcode; // Internal data. @@ -74,9 +58,9 @@ private: unsigned int m_current_nozzle; //BBS: current fan speed int m_current_fan_speed; - // First-layer plane evaluator, borrowed from GCode. Null = inactive - // (legacy per-layer fan control). - const FirstLayerPlane *m_first_layer_plane = nullptr; + // 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; }; } diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index b2477437b0..bf5d5ea980 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -2776,10 +2776,10 @@ bool GCodeProcessor::check_multi_extruder_gcode_valid(const int }; // Belt-printer post-gcode shear/scale/post_remap is applied as the final - // step of BeltGCodeWriter::to_machine_coords, so MoveVertex.position is - // in the printer's machine frame. Undo it here so XY area and Z height - // checks operate in the build-volume frame that printable_area / - // printable_height are defined in. For non-belt printers + // 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(); @@ -2860,7 +2860,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 filament_to_object_id; filament_to_object_id.first = iter->first; @@ -2901,7 +2906,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 filament_to_object_id; filament_to_object_id.first = iter->first; @@ -3072,6 +3077,7 @@ void GCodeProcessor::apply_config(const PrintConfig& config) // 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("filament_map"); if (filament_maps != nullptr) { @@ -3586,6 +3592,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(); @@ -4191,9 +4198,20 @@ void GCodeProcessor::process_tags(const std::string_view comment, bool producers 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). - if (boost::starts_with(comment, " belt_slice_rotation_angle = ")) { + // 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 (...) {} @@ -4220,13 +4238,13 @@ void GCodeProcessor::process_tags(const std::string_view comment, bool producers return RemapAxis::PosX; }; if (boost::starts_with(comment, " preslice_remap_x = ")) { - m_result.preslice_remap_x = parse_remap_axis(trim(std::string(comment.substr(25)))); return; + m_result.preslice_remap_x = parse_remap_axis(trim(std::string(comment.substr(20)))); return; } if (boost::starts_with(comment, " preslice_remap_y = ")) { - m_result.preslice_remap_y = parse_remap_axis(trim(std::string(comment.substr(25)))); return; + m_result.preslice_remap_y = parse_remap_axis(trim(std::string(comment.substr(20)))); return; } if (boost::starts_with(comment, " preslice_remap_z = ")) { - m_result.preslice_remap_z = parse_remap_axis(trim(std::string(comment.substr(25)))); return; + m_result.preslice_remap_z = parse_remap_axis(trim(std::string(comment.substr(20)))); return; } } // wipe start tag @@ -7113,7 +7131,7 @@ void GCodeProcessor::store_move_vertex(EMoveType type, EMovePathType path_type, // 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 BeltGCodeWriter and the designed-view back-transform + // 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 diff --git a/src/libslic3r/GCode/GCodeProcessor.hpp b/src/libslic3r/GCode/GCodeProcessor.hpp index 0a8a35a88b..6090a55eb6 100644 --- a/src/libslic3r/GCode/GCodeProcessor.hpp +++ b/src/libslic3r/GCode/GCodeProcessor.hpp @@ -1164,6 +1164,7 @@ class Print; // 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; @@ -1194,6 +1195,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; diff --git a/src/libslic3r/GCode/MachineFrameTransform.hpp b/src/libslic3r/GCode/MachineFrameTransform.hpp index 83519bdbe4..cdae28ca1a 100644 --- a/src/libslic3r/GCode/MachineFrameTransform.hpp +++ b/src/libslic3r/GCode/MachineFrameTransform.hpp @@ -9,7 +9,7 @@ namespace Slic3r { // Post-stage machine-frame transform for belt printers. // -// Applied in BeltGCodeWriter::to_machine_coords AFTER the back-transform and +// 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. // diff --git a/src/libslic3r/GCode/MachineKinematics.cpp b/src/libslic3r/GCode/MachineKinematics.cpp new file mode 100644 index 0000000000..05f5375aef --- /dev/null +++ b/src/libslic3r/GCode/MachineKinematics.cpp @@ -0,0 +1,47 @@ +#include "MachineKinematics.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) }; +} + +// Inverse of the above. Output axis i is fed by source axis (r_i % 3); walking +// the three outputs therefore fills every source component exactly once, so long +// as the remap is a permutation (which set_axis_remap callers guarantee). +Vec3d CartesianKinematics::apply_axis_remap_inverse(const Vec3d &machine) const +{ + if (!has_axis_remap()) + return machine; + Vec3d out = Vec3d::Zero(); + const int r[3] = { m_remap_x, m_remap_y, m_remap_z }; + for (int i = 0; i < 3; ++i) { + const int axis = r[i] % 3; + if (r[i] < 3) out[axis] = machine[i]; + else if (r[i] < 6) out[axis] = -machine[i]; + else out[axis] = m_build_vol_max[axis] - machine[i]; + } + return out; +} + +Vec3d CartesianKinematics::to_machine(const Vec3d &p) const +{ + return this->apply_axis_remap(p); +} + +Vec3d CartesianKinematics::to_logical(const Vec3d &machine) const +{ + return this->apply_axis_remap_inverse(machine); +} + +} // namespace Slic3r diff --git a/src/libslic3r/GCode/MachineKinematics.hpp b/src/libslic3r/GCode/MachineKinematics.hpp new file mode 100644 index 0000000000..5c53bd9f58 --- /dev/null +++ b/src/libslic3r/GCode/MachineKinematics.hpp @@ -0,0 +1,106 @@ +#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; + + // Inverse of to_machine(), back to the logical placed frame. Intended for + // consumers that must reconstruct model coordinates from emitted G-code + // (the G-code viewer's upright preview). + virtual Vec3d to_logical(const Vec3d &machine) const = 0; + + // Machine point -> build-volume frame, for bounds validation only. This is + // deliberately NOT to_logical(): the build-volume check wants the physical + // frame the printable area is expressed in, not the model frame. Keeping + // them separate stops the two contracts from being confused. + virtual Vec3d to_build_volume(const Vec3d &machine) 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; + Vec3d to_logical(const Vec3d &machine) const override; + Vec3d to_build_volume(const Vec3d &machine) const override { return machine; } + + 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; + Vec3d apply_axis_remap_inverse(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_ diff --git a/src/libslic3r/GCode/SeamPlacer.cpp b/src/libslic3r/GCode/SeamPlacer.cpp index 35bc436d5a..7562c7745d 100644 --- a/src/libslic3r/GCode/SeamPlacer.cpp +++ b/src/libslic3r/GCode/SeamPlacer.cpp @@ -627,7 +627,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 @@ -712,7 +712,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) { if (mv->is_seam_painted()) { diff --git a/src/libslic3r/GCode/ToolOrdering.cpp b/src/libslic3r/GCode/ToolOrdering.cpp index a59e5aa46b..a9f56a805f 100644 --- a/src/libslic3r/GCode/ToolOrdering.cpp +++ b/src/libslic3r/GCode/ToolOrdering.cpp @@ -1066,8 +1066,8 @@ void ToolOrdering::fill_wipe_tower_partitions(const PrintConfig &config, coordf_ // 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. Belt brim and the prime tower are mutually - // exclusive (rejected in Print::validate()), so simply drop the clause there. + // 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 diff --git a/src/libslic3r/GCode/ToolOrdering.hpp b/src/libslic3r/GCode/ToolOrdering.hpp index f38cb4bdc8..9ccf6e90fb 100644 --- a/src/libslic3r/GCode/ToolOrdering.hpp +++ b/src/libslic3r/GCode/ToolOrdering.hpp @@ -74,13 +74,17 @@ public: void set_layer_tools_ptr(const LayerTools* lt) { m_layer_tools = lt; } -private: - // Returns true if entity is not printed with its usual extruder for a given copy. + // 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; diff --git a/src/libslic3r/GCodeWriter.cpp b/src/libslic3r/GCodeWriter.cpp index 67dd6cf138..b7077f9778 100644 --- a/src/libslic3r/GCodeWriter.cpp +++ b/src/libslic3r/GCodeWriter.cpp @@ -1,4 +1,5 @@ #include "GCodeWriter.hpp" +#include "FirstLayerPlane.hpp" #include "CustomGCode.hpp" #include "Geometry.hpp" #include "I18N.hpp" @@ -24,34 +25,55 @@ 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_plane && m_first_layer_plane->is_active()) + return m_first_layer_plane->is_first_layer(point_logical, m_first_layer_thickness_mm); + 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 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_remap_x != 0 || m_remap_y != 1 || m_remap_z != 2; + return m_kinematics->must_emit_all_axes(); } Vec3d GCodeWriter::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) }; + return m_kinematics->to_machine(pos); } bool GCodeWriter::supports_separate_travel_acceleration(GCodeFlavor flavor) @@ -795,7 +817,7 @@ std::string GCodeWriter::travel_to_xy(const Vec2d &point, const std::string &com } else { w.emit_xy(point_on_plate); } - auto speed = m_is_first_layer + 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 @@ -808,6 +830,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; { @@ -822,6 +846,10 @@ std::string GCodeWriter::lazy_lift(LiftType lift_type, bool spiral_vase) // BBS if (m_lifted == 0 && m_to_lift == 0 && target_lift > 0) { if (spiral_vase) { + if (this->must_skip_lift_now()) + // Record no lift, so a later unlift() does not descend from a + // height that was never commanded. + return ""; m_lifted = target_lift; return this->_travel_to_z(m_pos(2) + target_lift, "lift Z"); } @@ -836,7 +864,7 @@ 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) std::string GCodeWriter::eager_lift(const LiftType type) { - const LiftType effective_type = type; + const LiftType effective_type = m_force_normal_lift ? LiftType::NormalLift : type; std::string lift_move; double target_lift = 0; { @@ -867,7 +895,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; @@ -888,7 +921,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); @@ -946,7 +984,14 @@ std::string GCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &co 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"); } } @@ -1002,20 +1047,20 @@ std::string GCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &co 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(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(); } 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(); } @@ -1050,8 +1095,9 @@ 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; @@ -1190,11 +1236,62 @@ std::string GCodeWriter::extrude_to_xy(const Vec2d &point, double dE, const std: return w.string(); } +// 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. +std::string GCodeWriter::extrude_arc_as_polyline(const Vec2d &point, const Vec2d ¢er_offset, + double dE, const bool is_ccw, + const std::string &comment, bool force_no_extrusion) +{ + const Vec2d start = Vec2d(m_pos.x(), m_pos.y()); + const Vec2d centre = start + center_offset; + const double r = (start - centre).norm(); + if (r < EPSILON) + // Degenerate: no arc to speak of, so a single move is exact. + return this->extrude_to_xy(point, dE, comment, force_no_extrusion); + + 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)))); + + std::string out; + 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)); + out += this->extrude_to_xy(p, dE / double(n), i == n ? comment : std::string(), force_no_extrusion); + } + return out; +} + //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 std::string GCodeWriter::extrude_arc_to_xy(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()) + return this->extrude_arc_as_polyline(point, center_offset, dE, is_ccw, comment, force_no_extrusion); + m_pos(0) = point(0); m_pos(1) = point(1); if (!force_no_extrusion) diff --git a/src/libslic3r/GCodeWriter.hpp b/src/libslic3r/GCodeWriter.hpp index 43e869910e..20357a1d9e 100644 --- a/src/libslic3r/GCodeWriter.hpp +++ b/src/libslic3r/GCodeWriter.hpp @@ -9,8 +9,12 @@ #include "Polygon.hpp" #include "PrintConfig.hpp" #include "GCode/CoolingBuffer.hpp" +#include "GCode/MachineKinematics.hpp" +#include namespace Slic3r { +class FirstLayerPlane; + class GCodeWriter { public: virtual ~GCodeWriter() = default; @@ -18,17 +22,19 @@ public: 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()), + 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]; } @@ -85,6 +91,10 @@ public: virtual std::string extrude_to_xy(const Vec2d &point, double dE, const std::string &comment = std::string(), bool force_no_extrusion = false); //BBS: generate G2 or G3 extrude which moves by arc std::string extrude_arc_to_xy(const Vec2d &point, const Vec2d ¢er_offset, double dE, const bool is_ccw, const std::string &comment = std::string(), bool force_no_extrusion = false); + // Linear approximation of an arc, used when the machine mapping cannot + // express a G2/G3. Must be called before m_pos is updated: center_offset is + // relative to the current position. + std::string extrude_arc_as_polyline(const Vec2d &point, const Vec2d ¢er_offset, double dE, const bool is_ccw, const std::string &comment = std::string(), bool force_no_extrusion = false); virtual std::string extrude_to_xyz(const Vec3d &point, double dE, const std::string &comment = std::string(), bool force_no_extrusion = false); std::string retract(bool before_wipe = false, double retract_length = 0); std::string retract_for_toolchange(bool before_wipe = false, double retract_length = 0); @@ -142,10 +152,28 @@ public: 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 kinematics); + const MachineKinematics& kinematics() const { return *m_kinematics; } + + // First-layer plane evaluator. When set to an active plane, travel speed + // selection consults the plane per destination point instead of the + // layer-coarse m_is_first_layer flag. Borrowed pointer; lifetime is owned + // by GCode, which constructs the plane after the writer exists -- so this is + // deliberately a setter and not a constructor argument. + void set_first_layer_plane(const FirstLayerPlane *plane, double first_layer_height_mm) + { m_first_layer_plane = plane; m_first_layer_thickness_mm = first_layer_height_mm; } + + // 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); protected: - // Position/lift/offset state — accessible to subclasses (e.g. BeltGCodeWriter) + // Position/lift/offset state. Vec3d m_pos = Vec3d::Zero(); double m_x_offset{ 0 }; double m_y_offset{ 0 }; @@ -158,17 +186,48 @@ protected: virtual std::string _travel_to_z(double z, const std::string &comment); - // Axis remap state — accessible to subclasses. + // Whether a destination gets first-layer treatment. With an active plane + // evaluator, distance from the plane 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 plane. 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 plane-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 m_first_layer_plane != nullptr; } + + // Borrowed; null = inactive. + const FirstLayerPlane *m_first_layer_plane = nullptr; + double m_first_layer_thickness_mm = 0.; + 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 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 axis remap to a point. Returns pos unchanged if remap is identity. + // 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. - // Protected so BeltGCodeWriter indexes the per-extruder speed options (travel_speed, + // Protected so subclasses index the per-extruder speed options (travel_speed, // travel_speed_z, initial_layer_travel_speed) exactly as the base writer does. size_t m_cached_extruder_idx; diff --git a/src/libslic3r/MultiMaterialSegmentation.cpp b/src/libslic3r/MultiMaterialSegmentation.cpp index 6f80f7b759..67b431790e 100644 --- a/src/libslic3r/MultiMaterialSegmentation.cpp +++ b/src/libslic3r/MultiMaterialSegmentation.cpp @@ -1215,7 +1215,7 @@ static inline std::vector> segmentation_top_and_bottom_l // project downards pointing painted triangles over bottom surfaces. std::vector> top_raw(num_facets_states), bottom_raw(num_facets_states); std::vector 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; @@ -1244,10 +1244,16 @@ static inline std::vector> 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 &&src, std::vector &dst) { @@ -2039,17 +2045,19 @@ std::vector> 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(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 &range) { + tbb::parallel_for(tbb::blocked_range(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 &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() * mv->get_matrix().cast(); - tbb::parallel_for(tbb::blocked_range(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 &range) { + const Transform3f tr = (object_trafo * mv->get_matrix()).cast(); + tbb::parallel_for(tbb::blocked_range(0, custom_facets.indices.size()), [&tr, &custom_facets, &layers, &edge_grids, &input_expolygons, &painted_lines, &painted_lines_mutex, &extruder_idx](const tbb::blocked_range &range) { for (size_t facet_idx = range.begin(); facet_idx < range.end(); ++facet_idx) { float min_z = std::numeric_limits::max(); float max_z = std::numeric_limits::lowest(); @@ -2102,7 +2110,6 @@ std::vector> 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). diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index ba838003ea..550cfbbd64 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -20,7 +20,6 @@ #include "GCode.hpp" #include "BeltGCode.hpp" #include "BeltTransform.hpp" -#include "GCode/MachineFrameTransform.hpp" #include "GCode/WipeTower.hpp" #include "GCode/WipeTower2.hpp" #include "GCode/WipeTowerEstimate.hpp" @@ -121,6 +120,10 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n "gcode_remap_z", // Machine-frame transform (derived from belt tilt; only affects G-code output). "belt_frame_tilt_decouple", "belt_frame_tilt_angle", + "gcode_back_transform", + "first_layer_plane", "first_layer_plane_offset", "first_layer_plane_thickness", + // Only inflates the GUI bed volume, like printable_area. + "belt_printer_infinite_y", //BBS "additional_cooling_fan_speed", "reduce_crossing_wall", @@ -1417,11 +1420,10 @@ StringObjectException Print::validate(std::vector *warnin return { L("Draft shield is not compatible with belt printer mode.") }; // Belt brim spans many layers and owns the layers below the object, which - // neither the prime tower nor spiral vase can share. + // 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.enable_prime_tower.value) - return { L("Brim is not compatible with the prime tower on a belt printer. " - "Disable one of them.") }; if (m_config.spiral_mode.value) return { L("Brim is not compatible with spiral vase mode on a belt printer. " "Disable one of them.") }; @@ -1551,6 +1553,21 @@ StringObjectException Print::validate(std::vector *warnin 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; @@ -1617,17 +1634,11 @@ StringObjectException Print::validate(std::vector *warnin // 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. - // When the post-gcode MachineFrameTransform is active the printer's - // physical Z mapping is non-trivial — skip the check entirely. + // 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; - bool skip_max_height_check = false; - if (belt_printer) { - MachineFrameTransform machine_frame; - machine_frame.init_from_config(this->config()); - skip_max_height_check = machine_frame.is_active(); - } const double shrinkage_compensation_z = this->shrinkage_compensation().z(); - for (size_t print_object_idx = 0; !skip_max_height_check && print_object_idx < m_objects.size(); ++ print_object_idx) { + 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]; double effective_max_z = 0; @@ -2515,6 +2526,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(); + for (PrintObject *obj : m_objects) obj->clear_shared_object(); diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index c387dcd552..516b7875bb 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -359,6 +359,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(m_center_offset.x()), - unscale(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; } PrintInstances &instances() { return m_instances; } @@ -582,6 +585,28 @@ private: // 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 &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 @@ -625,6 +650,16 @@ 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 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 diff --git a/src/libslic3r/PrintApply.cpp b/src/libslic3r/PrintApply.cpp index aa5c6aa2bd..0ed85f7fbc 100644 --- a/src/libslic3r/PrintApply.cpp +++ b/src/libslic3r/PrintApply.cpp @@ -1241,6 +1241,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("belt_printer"); belt_opt && belt_opt->value) { + const auto *axis_opt = new_full_config.option>("belt_slice_rotation"); + const auto *angle_opt = new_full_config.option("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; diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index ceeb60af92..4e2458e73b 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -373,8 +373,6 @@ CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(RemapAxis) static t_config_enum_values s_keys_map_BeltSupportFloorMode { { "none", int(BeltSupportFloorMode::None) }, { "generator_only", int(BeltSupportFloorMode::GeneratorOnly) }, - { "clip_only", int(BeltSupportFloorMode::ClipOnly) }, - { "both", int(BeltSupportFloorMode::Both) }, }; CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(BeltSupportFloorMode) @@ -7163,8 +7161,8 @@ void PrintConfigDef::init_fff_params() "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 = -90; - def->max = 90; + def->min = -89; + def->max = 89; def->mode = comExpert; def->set_default_value(new ConfigOptionFloat(0.)); @@ -7175,8 +7173,8 @@ void PrintConfigDef::init_fff_params() "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 = -90; - def->max = 90; + def->min = -89; + def->max = 89; def->mode = comExpert; def->set_default_value(new ConfigOptionFloat(0.)); @@ -7281,19 +7279,19 @@ void PrintConfigDef::init_fff_params() "your belt printer's physical bed plane. For a printer whose bed is in the XZ plane, " "set Y to +Z and Z to +Y (or -Y) to swap the vertical and belt-travel axes. " "Default +X: no change.", - RemapAxis::PosX, comExpert); + RemapAxis::PosX, comDevelop); add_belt_remap("preslice_remap_y", "Y", "Before slicing, which model-space axis becomes the slicer's Y axis. " "The slicer treats Y as one of the two horizontal bed axes. If your physical " "belt surface runs along the Z axis, map Y to +Z here so the slicer slices " "along the correct plane. Default +Y: no change.", - RemapAxis::PosY, comExpert); + RemapAxis::PosY, comDevelop); add_belt_remap("preslice_remap_z", "Z", "Before slicing, which model-space axis becomes the slicer's Z axis (layer stacking direction). " "The slicer builds layers upward along this axis. If your printer's layer-stacking " "direction is the physical Y axis, map Z to +Y (or -Y for inverted direction). " "Rev mode mirrors relative to the build volume maximum. Default +Z: no change.", - RemapAxis::PosZ, comExpert); + RemapAxis::PosZ, comDevelop); def = this->add("preslice_remap_global", coBool); def->label = L("Global"); @@ -7302,12 +7300,12 @@ void PrintConfigDef::init_fff_params() "Without this, the remap is applied locally around each object's center, so " "objects at different positions don't get a position-dependent contribution. " "Mirrors the 'Global' option on the belt slicing rotation, but for the remap."); - def->mode = comExpert; + def->mode = comDevelop; def->set_default_value(new ConfigOptionBool(false)); - add_belt_remap("gcode_remap_x", "X", "Which slicing axis maps to machine X in G-code output. Applied AFTER slicing, during G-code generation.", RemapAxis::PosX, comExpert); - add_belt_remap("gcode_remap_y", "Y", "Which slicing axis maps to machine Y in G-code output. Applied AFTER slicing, during G-code generation.", RemapAxis::PosY, comExpert); - add_belt_remap("gcode_remap_z", "Z", "Which slicing axis maps to machine Z in G-code output. Applied AFTER slicing, during G-code generation.", RemapAxis::PosZ, comExpert); + add_belt_remap("gcode_remap_x", "X", "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", "Y", "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", "Z", "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 @@ -9354,6 +9352,9 @@ void PrintConfigDef::handle_legacy(t_config_option_key &opt_key, std::string &va //BBS: handle legacy options if (opt_key == "curr_bed_type" && value == "SuperTack Plate") { value = "Supertack Plate"; + } else if (opt_key == "belt_support_floor_mode" && (value == "clip_only" || value == "both")) { + // Never implemented; both behaved like "none". + value = "none"; } else if (opt_key == "enable_wipe_tower") { opt_key = "enable_prime_tower"; } else if (opt_key == "wipe_tower_width") { diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 9d20a71c9b..1a2dd0f7a3 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -275,8 +275,6 @@ enum class BeltSupportFloorMode { None, // No belt floor awareness GeneratorOnly, // Only in tree support drop_nodes/contact_points - ClipOnly, // Only post-processing clipping - Both, // Both generator and clipping }; enum class BeltSupportZOffsetMode diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index ac7f34baa7..2c7cdc7335 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -4,7 +4,6 @@ #include "Print.hpp" #include "BeltTransform.hpp" -#include #include "BoundingBox.hpp" #include "ClipperUtils.hpp" #include "Clipper2Utils.hpp" @@ -460,15 +459,11 @@ std::vector> PrintObject::detect_extruder_geometric_unprintables() // 3) Generates perimeters, gap fills and fill regions (fill regions of type stInternal). void PrintObject::make_perimeters() { - BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] make_perimeters request tid=" << std::this_thread::get_id() << " obj=" << this; // prerequisites this->slice(); - if (! this->set_started(posPerimeters)) { - BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] make_perimeters SKIP tid=" << std::this_thread::get_id() << " obj=" << this << " (already started/done)"; + if (! this->set_started(posPerimeters)) return; - } - BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] make_perimeters ENTER tid=" << std::this_thread::get_id() << " obj=" << this; m_print->set_status(15, L("Generating walls")); BOOST_LOG_TRIVIAL(info) << "Generating walls..." << log_memory_info(); @@ -566,7 +561,6 @@ void PrintObject::make_perimeters() m_print->throw_if_canceled(); BOOST_LOG_TRIVIAL(debug) << "Generating perimeters in parallel - end"; - BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] make_perimeters EXIT tid=" << std::this_thread::get_id() << " obj=" << this; this->set_done(posPerimeters); } @@ -955,9 +949,7 @@ void PrintObject::detect_overhangs_for_lift() void PrintObject::generate_support_material() { - BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] generate_support_material request tid=" << std::this_thread::get_id() << " obj=" << this; if (this->set_started(posSupportMaterial)) { - BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] generate_support_material ENTER tid=" << std::this_thread::get_id() << " obj=" << this; this->clear_support_layers(); if(!has_support() && !m_print->get_no_check_flag()) { @@ -1005,10 +997,7 @@ void PrintObject::generate_support_material() // posSupportMaterial, so this needs no extra invalidation edges. make_belt_brim(*this); m_print->throw_if_canceled(); - BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] generate_support_material EXIT tid=" << std::this_thread::get_id() << " obj=" << this; this->set_done(posSupportMaterial); - } else { - BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] generate_support_material SKIP tid=" << std::this_thread::get_id() << " obj=" << this << " (already started/done)"; } } @@ -1101,7 +1090,10 @@ std::pair PrintObject::prepare 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> overhangs(std::max(surfaces_w_bottom_z.size(), size_t(1))); @@ -1148,6 +1140,13 @@ void PrintObject::clear_layers() 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(); } } @@ -1199,6 +1198,10 @@ 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 (! this->belt_brim_instances_compatible()) return false; if (m_config.brim_type == btNoBrim) @@ -1246,7 +1249,9 @@ bool PrintObject::belt_brim_instances_compatible() const // matters for configurations that do not. if (m_instances.size() <= 1) return true; - const int axis = m_slicing_params.belt_floor_from_axis; + // From the config, not m_slicing_params: this runs while those can be stale. A tilt + // about Y runs the belt along X, any other tilt along Y (see compute_belt_height_and_floor). + const int axis = m_print->config().belt_slice_rotation.value == BeltRotationAxis::Y ? 0 : 1; const Point &ref = m_instances.front().shift; for (const PrintInstance &inst : m_instances) { const coord_t along = axis == 0 ? inst.shift.x() - ref.x() : inst.shift.y() - ref.y(); @@ -4684,67 +4689,6 @@ void PrintObject::combine_infill() } } -// Belt printer: clip an ExtrusionEntityCollection to a region defined by clip_expoly. -// Handles ExtrusionPath, ExtrusionMultiPath, ExtrusionLoop, and nested ExtrusionEntityCollection. -static void clip_support_fills(ExtrusionEntityCollection &fills, const ExPolygons &clip_region) -{ - ExtrusionEntitiesPtr new_entities; - for (ExtrusionEntity *entity : fills.entities) { - if (auto *path = dynamic_cast(entity)) { - ExtrusionEntityCollection clipped; - path->intersect_expolygons(clip_region, &clipped); - if (!clipped.empty()) { - for (ExtrusionEntity *e : clipped.entities) - new_entities.push_back(e->clone()); - } - delete entity; - } else if (auto *multipath = dynamic_cast(entity)) { - ExtrusionPaths new_paths; - for (const ExtrusionPath &p : multipath->paths) { - ExtrusionEntityCollection clipped; - p.intersect_expolygons(clip_region, &clipped); - for (ExtrusionEntity *e : clipped.entities) - if (auto *cp = dynamic_cast(e)) - new_paths.push_back(std::move(*cp)); - } - if (!new_paths.empty()) { - multipath->paths = std::move(new_paths); - new_entities.push_back(multipath); - } else { - delete entity; - } - } else if (auto *loop = dynamic_cast(entity)) { - ExtrusionPaths new_paths; - for (const ExtrusionPath &p : loop->paths) { - ExtrusionEntityCollection clipped; - p.intersect_expolygons(clip_region, &clipped); - for (ExtrusionEntity *e : clipped.entities) - if (auto *cp = dynamic_cast(e)) - new_paths.push_back(std::move(*cp)); - } - if (!new_paths.empty()) { - // Loop is no longer a closed loop after clipping; emit as individual paths. - for (auto &p : new_paths) - new_entities.push_back(new ExtrusionPath(std::move(p))); - delete entity; - } else { - delete entity; - } - } else if (auto *coll = dynamic_cast(entity)) { - clip_support_fills(*coll, clip_region); - if (!coll->empty()) { - new_entities.push_back(coll); - } else { - delete entity; - } - } else { - // Unknown entity type — keep as-is. - new_entities.push_back(entity); - } - } - fills.entities = std::move(new_entities); -} - void PrintObject::_generate_support_material() { if (is_tree(m_config.support_type.value)) { @@ -5124,6 +5068,7 @@ static void project_triangles_to_slabs(ConstLayerPtrsAdaptor layers, const index void PrintObject::project_and_append_custom_facets( bool seam, EnforcerBlockerType type, std::vector& out, std::vector>* 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 @@ -5132,12 +5077,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(), + (object_trafo * mv->get_matrix()).cast(), seam, out); else { std::vector 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()); diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index 2b6c4ffcff..2181b3e0d5 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -1,6 +1,5 @@ #include #include -#include #include @@ -860,12 +859,8 @@ void groupingVolumesForBrim(PrintObject* object, LayerPtrs& layers, int firstLay // Resulting expolygons of layer regions are marked as Internal. void PrintObject::slice() { - BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] slice request tid=" << std::this_thread::get_id() << " obj=" << this; - if (! this->set_started(posSlice)) { - BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] slice SKIP tid=" << std::this_thread::get_id() << " obj=" << this << " (already started/done)"; + if (! this->set_started(posSlice)) return; - } - BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] slice ENTER tid=" << std::this_thread::get_id() << " obj=" << this; //BBS: add flag to reload scene for shell rendering m_print->set_status(5, L("Slicing mesh"), PrintBase::SlicingStatus::RELOAD_SCENE); std::vector layer_height_profile; @@ -1000,41 +995,6 @@ void PrintObject::slice() 0.); double centering_z_corr = (T_fwd.linear() * c_off).z(); global_z_offset += centering_z_corr; - BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] centering correction" - << " obj=" << this->model_object()->name - << " m_center_offset_mm=(" << c_off.x() << "," << c_off.y() << ")" - << " centering_z_corr=" << centering_z_corr - << " (added to global_z_offset)"; - } - - // [BELT-DEBUG] Per-object summary so Case A vs Case B can be compared - // side-by-side. Lays out every value that feeds into the final layer - // print_z adjustment. - { - BoundingBoxf3 raw_bb = this->model_object()->raw_bounding_box(); - BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] slice() per-object summary" - << " obj=" << this->model_object()->name - << " n_volumes=" << this->model_object()->volumes.size() - << " raw_bbox.min=(" << raw_bb.min.x() << "," << raw_bb.min.y() << "," << raw_bb.min.z() << ")" - << " raw_bbox.max=(" << raw_bb.max.x() << "," << raw_bb.max.y() << "," << raw_bb.max.z() << ")" - << " raw_bbox.center=(" << raw_bb.center().x() << "," << raw_bb.center().y() << ")" - << " m_center_offset=(" << unscale(m_center_offset.x()) << "," << unscale(m_center_offset.y()) << ")" - << " inst_shift=(" << unscale(inst_shift.x()) << "," << unscale(inst_shift.y()) << ")" - << " m_belt_min_z=" << m_belt_min_z - << " belt_surface_z=" << belt_surface_z - << " belt_z_shift=" << belt_z_shift; - // Per-volume bbox + get_matrix translation so order/composition is visible. - int vi = 0; - for (const ModelVolume *mv : this->model_object()->volumes) { - if (!mv->is_model_part()) { ++vi; continue; } - BoundingBoxf3 vol_bb = mv->mesh().transformed_bounding_box(mv->get_matrix()); - BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] vol[" << vi - << "] id=" << mv->id().id << " name='" << mv->name << "'" - << " get_matrix.translation=(" << mv->get_matrix().translation().x() << "," << mv->get_matrix().translation().y() << "," << mv->get_matrix().translation().z() << ")" - << " object_bbox.min=(" << vol_bb.min.x() << "," << vol_bb.min.y() << "," << vol_bb.min.z() << ")" - << " object_bbox.max=(" << vol_bb.max.x() << "," << vol_bb.max.y() << "," << vol_bb.max.z() << ")"; - ++vi; - } } if (pcfg.belt_preslice_global.value) { @@ -1044,9 +1004,6 @@ void PrintObject::slice() Vec3d d(unscale(inst_shift.x()), unscale(inst_shift.y()), 0.); Vec3d c = T.linear() * d - d; global_z_offset += c.z(); - BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] write m_belt_global_xy_correction tid=" << std::this_thread::get_id() - << " obj=" << this << " old=(" << m_belt_global_xy_correction.x() << "," << m_belt_global_xy_correction.y() - << ") new=(" << c.x() << "," << c.y() << ")"; m_belt_global_xy_correction = Vec2d(c.x(), c.y()); BOOST_LOG_TRIVIAL(trace) << "Belt preslice_global: correction=(" @@ -1081,18 +1038,7 @@ void PrintObject::slice() BOOST_LOG_TRIVIAL(trace) << "Belt global: z_offset=" << global_z_offset << " (relative to min across " << this->print()->objects().size() << " objects)"; - BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] write m_belt_global_z_offset tid=" << std::this_thread::get_id() - << " obj=" << this << " old=" << m_belt_global_z_offset << " new=" << global_z_offset; m_belt_global_z_offset = global_z_offset; - // [BELT-DEBUG] Final breakdown of all contributions to layer.print_z - // and where the first / last layer end up post-adjustment. - BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] global_z_offset breakdown" - << " obj=" << this->model_object()->name - << " belt_z_shift=" << belt_z_shift - << " total_global_z_offset=" << global_z_offset - << " xy_correction=(" << m_belt_global_xy_correction.x() << "," << m_belt_global_xy_correction.y() << ")" - << " belt_floor_z_shift_before=" << (m_slicing_params.belt_floor_z_shift) - << " n_layers=" << m_layers.size(); if (std::abs(global_z_offset) > EPSILON) { for (Layer *layer : m_layers) layer->print_z += global_z_offset; @@ -1101,12 +1047,6 @@ void PrintObject::slice() // 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-DEBUG] post-adjustment" - << " first_layer.print_z=" << m_layers.front()->print_z - << " last_layer.print_z=" << m_layers.back()->print_z - << " belt_floor_z_shift_after=" << m_slicing_params.belt_floor_z_shift; - } 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 @@ -1125,10 +1065,6 @@ void PrintObject::slice() } // BBS - BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] slice EXIT tid=" << std::this_thread::get_id() << " obj=" << this - << " layers=" << m_layers.size() << " belt_min_z=" << m_belt_min_z - << " belt_global_z_offset=" << m_belt_global_z_offset - << " belt_xy=(" << m_belt_global_xy_correction.x() << "," << m_belt_global_xy_correction.y() << ")"; this->set_done(posSlice); } @@ -1806,6 +1742,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 PrintObject::slice_support_volumes(const ModelVolumeType model_volume_type) const { auto it_volume = this->model_object()->volumes.begin(); @@ -1820,7 +1763,7 @@ std::vector PrintObject::slice_support_volumes(const ModelVolumeType m const Print *print = this->print(); auto throw_on_cancel_callback = std::function([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 slices2 = slice_volume(*(*it_volume), zs, params, throw_on_cancel_callback); diff --git a/src/libslic3r/Support/SupportCommon.cpp b/src/libslic3r/Support/SupportCommon.cpp index 27ac9b24d1..0c3d570b1d 100644 --- a/src/libslic3r/Support/SupportCommon.cpp +++ b/src/libslic3r/Support/SupportCommon.cpp @@ -2069,4 +2069,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 diff --git a/src/libslic3r/Support/SupportCommon.hpp b/src/libslic3r/Support/SupportCommon.hpp index f15c7d417a..4374986a3e 100644 --- a/src/libslic3r/Support/SupportCommon.hpp +++ b/src/libslic3r/Support/SupportCommon.hpp @@ -150,6 +150,10 @@ 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_ */ diff --git a/src/libslic3r/Support/SupportMaterial.cpp b/src/libslic3r/Support/SupportMaterial.cpp index c8ac217af2..5d886bcdf0 100644 --- a/src/libslic3r/Support/SupportMaterial.cpp +++ b/src/libslic3r/Support/SupportMaterial.cpp @@ -1438,9 +1438,8 @@ static inline ExPolygons detect_overhangs( 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 double tilt_x_rad = Geometry::deg2rad(print_config.build_plate_tilt_x.value); - const double tilt_y_rad = Geometry::deg2rad(print_config.build_plate_tilt_y.value); - const bool has_tilt = std::abs(tilt_x_rad) > EPSILON || std::abs(tilt_y_rad) > EPSILON; + 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) @@ -1480,10 +1479,7 @@ static inline ExPolygons detect_overhangs( Polygons tilted_lower; if (has_tilt) { tilted_lower = lower_layer_polygons; - const double lh = lower_layer.height; - Point tilt_shift(coord_t(scale_(lh * tan(tilt_y_rad))), - coord_t(scale_(lh * tan(tilt_x_rad)))); - translate(tilted_lower, tilt_shift); + translate(tilted_lower, Point::new_scale(tilt_slope * lower_layer.height)); effective_lower = &tilted_lower; } diff --git a/src/libslic3r/Support/TreeModelVolumes.cpp b/src/libslic3r/Support/TreeModelVolumes.cpp index d90d1f5de9..9491a425b8 100644 --- a/src/libslic3r/Support/TreeModelVolumes.cpp +++ b/src/libslic3r/Support/TreeModelVolumes.cpp @@ -95,29 +95,6 @@ TreeModelVolumes::TreeModelVolumes( #else { m_anti_overhang = print_object.slice_support_blockers(); - // Belt floor: add belt surface polygons to anti_overhang so support - // is never generated inside the belt. Only in global shear mode — - // in local mode the belt floor clipping handles everything and - // anti_overhang at the bottom layers would block all support. - { - 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() - && std::abs(print_object.belt_global_z_offset()) > EPSILON - && pcfg.belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) { - size_t num_layers_needed = print_object.layer_count(); - // Ensure m_anti_overhang is large enough. - if (m_anti_overhang.size() < num_layers_needed) - m_anti_overhang.resize(num_layers_needed, Polygons{}); - for (size_t layer_idx = 0; layer_idx < num_layers_needed; ++layer_idx) { - double print_z = print_object.get_layer(layer_idx)->print_z - - print_object.belt_global_z_offset(); - append(m_anti_overhang[layer_idx], ctx.surface_polygon(print_z)); - } - } - } TreeSupportMeshGroupSettings mesh_settings(print_object); const TreeSupportSettings config{ mesh_settings, print_object.slicing_parameters() }; m_current_min_xy_dist = config.xy_min_distance; @@ -147,6 +124,38 @@ TreeModelVolumes::TreeModelVolumes( } } } + // Belt floor: add belt surface polygons to anti_overhang so support is + // never generated inside the belt. + // + // This MUST run after m_raft_layers is final. m_anti_overhang is 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 it in + // object-layer space. Without the shift below, every entry lands + // num_raft_layers too low: with the belt raft that is tens of layers, so + // the belt suppression is applied to the wrong layers entirely and the + // topmost object layers get none at all. + { + const size_t num_raft = m_raft_layers.size(); + const size_t num_obj = print_object.layer_count(); + if (num_raft > 0 && ! m_anti_overhang.empty()) + // Shift the support blockers into the same space. + m_anti_overhang.insert(m_anti_overhang.begin(), num_raft, Polygons{}); + 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() + && std::abs(print_object.belt_global_z_offset()) > EPSILON + && pcfg.belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) { + if (m_anti_overhang.size() < num_raft + num_obj) + m_anti_overhang.resize(num_raft + num_obj, Polygons{}); + for (size_t i = 0; i < num_obj; ++i) { + const double print_z = print_object.get_layer(i)->print_z + - print_object.belt_global_z_offset(); + append(m_anti_overhang[num_raft + i], ctx.surface_polygon(print_z)); + } + } + } m_current_outline_idx = 0; m_layer_outlines.emplace_back(mesh_settings, std::vector{}); diff --git a/src/libslic3r/Support/TreeSupport.cpp b/src/libslic3r/Support/TreeSupport.cpp index 84ae229130..75195d0167 100644 --- a/src/libslic3r/Support/TreeSupport.cpp +++ b/src/libslic3r/Support/TreeSupport.cpp @@ -709,9 +709,19 @@ void TreeSupport::detect_overhangs(bool check_support_necessity/* = false*/) 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 double tilt_x_rad = Geometry::deg2rad(print_cfg.build_plate_tilt_x.value); - const double tilt_y_rad = Geometry::deg2rad(print_cfg.build_plate_tilt_y.value); - const bool has_tilt = std::abs(tilt_x_rad) > EPSILON || std::abs(tilt_y_rad) > EPSILON; + 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(support_tree_tip_diameter); @@ -855,15 +865,60 @@ void TreeSupport::detect_overhangs(bool check_support_necessity/* = false*/) 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 (has_tilt) { + if (belt_ovh_active || has_tilt) { shifted_lower = lower_polys; // copy const double lh = lower_layer->height; - Point tilt_shift(coord_t(scale_(lh * tan(tilt_y_rad))), - coord_t(scale_(lh * tan(tilt_x_rad)))); + 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); } - const ExPolygons &effective_lower = has_tilt ? shifted_lower : lower_polys; + 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(effective_lower, support_offset_scaled, SUPPORT_SURFACES_OFFSET_PARAMETERS); @@ -882,8 +937,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(); } @@ -1853,8 +1913,6 @@ void TreeSupport::generate() if (first_layer != nullptr) { ExPolygons floating = diff_ex(first_layer->lslices_extrudable, ctx.surface_polygon(first_layer->bottom_z() - first_layer->height)); - BOOST_LOG_TRIVIAL(debug) << "[BELT-CALIB] wedge seed: obj=" << m_object->model_object()->name - << " bottom_z=" << first_layer->bottom_z() << " floating=" << floating.size(); if (!floating.empty()) { source_areas = std::move(floating); first_z = first_layer->bottom_z(); @@ -1917,6 +1975,8 @@ void TreeSupport::generate() if (!belt_ext_layers.empty()) { auto &sl_vec = m_object->support_layers(); sl_vec.insert(sl_vec.begin(), belt_ext_layers.begin(), belt_ext_layers.end()); + for (size_t i = 0; i < sl_vec.size(); ++i) + sl_vec[i]->set_id(i); } } } diff --git a/src/libslic3r/Support/TreeSupport3D.cpp b/src/libslic3r/Support/TreeSupport3D.cpp index e02f474420..b5c35bfd6b 100644 --- a/src/libslic3r/Support/TreeSupport3D.cpp +++ b/src/libslic3r/Support/TreeSupport3D.cpp @@ -211,9 +211,8 @@ static std::vector>> group_me // +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 double tilt_x_rad = Geometry::deg2rad(print_config.build_plate_tilt_x.value); - const double tilt_y_rad = Geometry::deg2rad(print_config.build_plate_tilt_y.value); - const bool has_tilt = std::abs(tilt_x_rad) > EPSILON || std::abs(tilt_y_rad) > EPSILON; + 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(config.tree_support_tip_diameter.value); const coordf_t radius_sample_resolution = g_config_tree_support_collision_resolution; @@ -235,7 +234,7 @@ static std::vector>> 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(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, has_tilt, tilt_x_rad, tilt_y_rad, &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 &range) { for (LayerIndex layer_id = range.begin(); layer_id < range.end(); ++ layer_id) { const Layer ¤t_layer = *print_object.get_layer(layer_id); @@ -263,10 +262,7 @@ static std::vector>> group_me Polygons lower_layer_offseted; if (has_tilt) { Polygons lower_src = to_polygons(lower_layer.lslices_extrudable); - const double lh = lower_layer.height; - Point tilt_shift(coord_t(scale_(lh * tan(tilt_y_rad))), - coord_t(scale_(lh * tan(tilt_x_rad)))); - translate(lower_src, tilt_shift); + 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); diff --git a/src/libslic3r/Support/TreeSupportCommon.hpp b/src/libslic3r/Support/TreeSupportCommon.hpp index 07dcb324b8..979adcd9ed 100644 --- a/src/libslic3r/Support/TreeSupportCommon.hpp +++ b/src/libslic3r/Support/TreeSupportCommon.hpp @@ -651,7 +651,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; } diff --git a/src/libslic3r/TriangleSelector.cpp b/src/libslic3r/TriangleSelector.cpp index 7e911cc565..840d3c672b 100644 --- a/src/libslic3r/TriangleSelector.cpp +++ b/src/libslic3r/TriangleSelector.cpp @@ -367,7 +367,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) + const Vec3f &up_direction, bool force_reselection) { assert(facet_start < m_orig_size_indices); diff --git a/src/libslic3r/TriangleSelector.hpp b/src/libslic3r/TriangleSelector.hpp index 6154f7d7f3..e715d976ab 100644 --- a/src/libslic3r/TriangleSelector.hpp +++ b/src/libslic3r/TriangleSelector.hpp @@ -336,8 +336,8 @@ 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. - bool force_reselection = false, // force reselection of the triangle mesh even in cases that mouse is pointing on the selected triangle - const Vec3f &up_direction = Vec3f::UnitZ()); // Up direction for overhang detection (accounts for build plate tilt) + 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 int facet_start, // facet of the original mesh (unsplit) that the hit point belongs to diff --git a/src/libslic3r/calib.cpp b/src/libslic3r/calib.cpp index 8002e17006..dbebf40106 100644 --- a/src/libslic3r/calib.cpp +++ b/src/libslic3r/calib.cpp @@ -1,5 +1,5 @@ #include "calib.hpp" -#include "BeltGCodeWriter.hpp" +#include "GCode/BeltKinematics.hpp" #include "BoundingBox.hpp" #include "Config.hpp" #include "Model.hpp" @@ -855,10 +855,8 @@ void CalibPressureAdvancePattern::_refresh_writer(bool is_bbl_machine, const Mod // 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(); - belt_writer->set_belt_back_transform(print_config); - belt_writer->set_machine_frame_transform(print_config); - belt_writer->set_world_coordinates(true); + auto belt_writer = std::make_shared(); + 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); @@ -869,7 +867,9 @@ void CalibPressureAdvancePattern::_refresh_writer(bool is_bbl_machine, const Mod print_config.printable_height.value)); } m_writer = std::move(belt_writer); - } else if (dynamic_cast(m_writer.get()) != nullptr) { + } else if (m_writer && dynamic_cast(&m_writer->kinematics()) != nullptr) { + // Previously configured for a belt printer; drop back to a plain writer, + // exactly as the old dynamic_cast check did. m_writer = std::make_shared(); } diff --git a/src/libslic3r/calib.hpp b/src/libslic3r/calib.hpp index 69ef9a7e24..a09e513086 100644 --- a/src/libslic3r/calib.hpp +++ b/src/libslic3r/calib.hpp @@ -363,7 +363,7 @@ private: const Calib_Params &m_params; - // Polymorphic so belt printers get a BeltGCodeWriter in world-coordinates + // Polymorphic so belt printers get belt kinematics in world-coordinates // mode (_refresh_writer); shared_ptr keeps the class copyable — the writer // is rebuilt by refresh_setup() before every use anyway. std::shared_ptr m_writer{std::make_shared()}; diff --git a/src/slic3r/GUI/3DBed.cpp b/src/slic3r/GUI/3DBed.cpp index fa9acffb50..69e5a1d340 100644 --- a/src/slic3r/GUI/3DBed.cpp +++ b/src/slic3r/GUI/3DBed.cpp @@ -387,9 +387,6 @@ void Bed3D::render_internal(GLCanvas3D& canvas, const Transform3d& view_matrix, m_model.set_color(m_is_dark ? DEFAULT_MODEL_COLOR_DARK : DEFAULT_MODEL_COLOR); - // Belt printer: bed rotation is applied inside render_model() and render_default() - // using m_is_belt_printer and m_belt_angle members. - switch (m_type) { case Type::System: { render_system(canvas, view_matrix, projection_matrix, bottom); break; } @@ -398,8 +395,6 @@ void Bed3D::render_internal(GLCanvas3D& canvas, const Transform3d& view_matrix, } render_gravity_arrow(view_matrix, projection_matrix); - render_slicing_arrow(view_matrix, projection_matrix); - render_slicing_plane(view_matrix, projection_matrix); glsafe(::glDisable(GL_DEPTH_TEST)); } @@ -700,12 +695,6 @@ void Bed3D::render_model(const Transform3d& view_matrix, const Transform3d& proj shader->start_using(); shader->set_uniform("emission_factor", 0.0f); Transform3d model_matrix = Geometry::assemble_transform(m_model_offset); - // Belt printer: rotate the bed model about the tilt axis so the belt tilt - // is visible. Negative angle: belt surface tilts downward away from the nozzle. - if (m_is_belt_printer && m_belt_angle > 0.f) { - double angle_rad = Geometry::deg2rad(static_cast(m_belt_angle)); - model_matrix = Eigen::AngleAxisd(-angle_rad, belt_tilt_unit_axis()) * model_matrix; - } 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); @@ -746,20 +735,14 @@ void Bed3D::render_custom(GLCanvas3D& canvas, const Transform3d& view_matrix, co void Bed3D::render_gravity_arrow(const Transform3d& view_matrix, const Transform3d& projection_matrix) { - const DynamicPrintConfig& cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config; // 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. - double tilt_x_deg = cfg.opt_float("build_plate_tilt_x"); - double tilt_y_deg = cfg.opt_float("build_plate_tilt_y"); - if (tilt_x_deg == 0. && tilt_y_deg == 0.) { + const Vec3d up_dir = build_plate_tilt_up_direction(); + if (up_dir == Vec3d::UnitZ()) { m_gravity_arrow.reset(); return; } - - // Gravity direction (matching the slicer's tilt convention) - double tilt_x_rad = Geometry::deg2rad(tilt_x_deg); - double tilt_y_rad = Geometry::deg2rad(tilt_y_deg); - Vec3d gravity_dir = Vec3d(-tan(tilt_y_rad), -tan(tilt_x_rad), -1.0).normalized(); + const Vec3d gravity_dir = -up_dir; // Build the arrow model (same dimensions as the axis arrows) if (!m_gravity_arrow.is_initialized()) { @@ -803,114 +786,6 @@ void Bed3D::render_gravity_arrow(const Transform3d& view_matrix, const Transform shader->stop_using(); } -void Bed3D::render_slicing_arrow(const Transform3d& view_matrix, const Transform3d& projection_matrix) -{ - if (!m_is_belt_printer || m_belt_angle <= 0.f) - return; - - // Build the arrow model: shorter and wider than the gravity arrow. - if (!m_slicing_arrow.is_initialized()) { - const float stem_length = 15.0f; // shorter than gravity arrow (25) - const float stem_radius = 1.0f; // wider than gravity arrow (~0.33) - const float tip_radius = 3.0f; // wider tip - const float tip_length = 5.0f; - m_slicing_arrow.init_from(stilized_arrow(16, tip_radius, tip_length, stem_radius, stem_length)); - } - - // The slicing direction: layers stack along the gantry normal, i.e. the image of - // +Z under the mesh rotation about the tilt axis. Use the same AngleAxis as the - // slicing pipeline so the arrow matches whichever tilt axis is configured. - double angle_rad = Geometry::deg2rad(static_cast(m_belt_angle)); - Vec3d slice_dir = (Eigen::AngleAxisd(angle_rad, belt_tilt_unit_axis()).toRotationMatrix() - * Vec3d::UnitZ()).normalized(); - - // Compute rotation to align +Z (arrow default) with slice_dir. - Vec3d from = Vec3d::UnitZ(); - double dot = from.dot(slice_dir); - Transform3d rot = Transform3d::Identity(); - if (dot < -0.9999) { - rot = Eigen::AngleAxisd(M_PI, Vec3d::UnitX()) * rot; - } else if (dot < 0.9999) { - Vec3d axis = from.cross(slice_dir).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; - - // Disable depth test so the arrow is always visible (not occluded by the tilted bed). - glsafe(::glDisable(GL_DEPTH_TEST)); - shader->start_using(); - - const Camera& camera = wxGetApp().plater()->get_camera(); - Transform3d model_matrix = rot; - shader->set_uniform("view_model_matrix", camera.get_view_matrix() * model_matrix); - shader->set_uniform("projection_matrix", camera.get_projection_matrix()); - - m_slicing_arrow.set_color({ 1.0f, 0.2f, 0.6f, 1.0f }); // pink - m_slicing_arrow.render(); - - shader->stop_using(); - glsafe(::glEnable(GL_DEPTH_TEST)); -} - -void Bed3D::render_slicing_plane(const Transform3d& view_matrix, const Transform3d& projection_matrix) -{ - if (!m_is_belt_printer || m_belt_angle <= 0.f) - return; - - // Build a quad in the XZ plane (world frame) representing the belt slicing plane. - // The plane is tilted at belt_angle from horizontal, with normal (0, -sin(a), cos(a)). - // We render it as a semi-transparent quad centered on the build plate. - if (!m_slicing_plane.is_initialized()) { - const float half_size = 120.f; // mm, large enough to be visible - GLModel::Geometry init_data; - init_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3N3 }; - init_data.reserve_vertices(4); - init_data.reserve_indices(2); // 2 triangles - - // Quad corners in local frame (XY plane, will be rotated to match slicing plane) - Vec3f n = Vec3f::UnitZ(); - init_data.add_vertex(Vec3f(-half_size, -half_size, 0.f), n); - init_data.add_vertex(Vec3f( half_size, -half_size, 0.f), n); - init_data.add_vertex(Vec3f( half_size, half_size, 0.f), n); - init_data.add_vertex(Vec3f(-half_size, half_size, 0.f), n); - init_data.add_triangle(0, 1, 2); - init_data.add_triangle(0, 2, 3); - - m_slicing_plane.init_from(std::move(init_data)); - } - - GLShaderProgram* shader = wxGetApp().get_shader("flat"); - if (shader == nullptr) - return; - - glsafe(::glEnable(GL_DEPTH_TEST)); - glsafe(::glEnable(GL_BLEND)); - glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)); - - shader->start_using(); - - // Show a tilted plane representing the slicing direction. - // The slicing plane is rotated by belt_angle about the tilt axis from horizontal. - // Raise it slightly so it's visible above the bed surface. - double angle_rad = Geometry::deg2rad(static_cast(m_belt_angle)); - Transform3d model_matrix = Transform3d::Identity(); - model_matrix.translate(Vec3d(0., 0., 30.)); - model_matrix.rotate(Eigen::AngleAxisd(angle_rad, belt_tilt_unit_axis())); - - shader->set_uniform("view_model_matrix", view_matrix * model_matrix); - shader->set_uniform("projection_matrix", projection_matrix); - - m_slicing_plane.set_color({ 0.2f, 0.6f, 1.0f, 0.3f }); // semi-transparent blue - m_slicing_plane.render(); - - glsafe(::glDisable(GL_BLEND)); - shader->stop_using(); -} - void Bed3D::render_default(bool bottom, const Transform3d& view_matrix, const Transform3d& projection_matrix) { // m_texture.reset(); @@ -921,15 +796,7 @@ void Bed3D::render_default(bool bottom, const Transform3d& view_matrix, const Tr if (shader != nullptr) { shader->start_using(); - // Belt printer: rotate the default bed about X so the belt tilt is visible. - Transform3d view_model_matrix = view_matrix; - if (m_is_belt_printer && m_belt_angle > 0.f) { - double angle_rad = Geometry::deg2rad(static_cast(m_belt_angle)); - Transform3d belt_rotation = Transform3d::Identity(); - belt_rotation.rotate(Eigen::AngleAxisd(-angle_rad, Vec3d::UnitX())); - view_model_matrix = view_matrix * belt_rotation; - } - shader->set_uniform("view_model_matrix", view_model_matrix); + shader->set_uniform("view_model_matrix", view_matrix); shader->set_uniform("projection_matrix", projection_matrix); glsafe(::glEnable(GL_DEPTH_TEST)); diff --git a/src/slic3r/GUI/3DBed.hpp b/src/slic3r/GUI/3DBed.hpp index 788d8f52a3..4298ff8b1d 100644 --- a/src/slic3r/GUI/3DBed.hpp +++ b/src/slic3r/GUI/3DBed.hpp @@ -111,8 +111,6 @@ private: GLModel m_model; Vec3d m_model_offset{ Vec3d::Zero() }; GLModel m_gravity_arrow; - GLModel m_slicing_arrow; // Pink arrow showing the effective slicing direction - GLModel m_slicing_plane; // Debug: shows the intended slicing plane direction Axes m_axes; float m_scale_factor{ 1.0f }; @@ -122,11 +120,6 @@ private: std::vector> m_extruder_shapes; std::vector m_extruder_heights; bool m_is_dark = false; - // Belt printer state for rendering. - bool m_is_belt_printer = false; - float m_belt_angle = 0.f; - // Tilt axis: 0 = X (belt travels along Y, the common case), 1 = Y. - int m_belt_tilt_axis = 0; public: Bed3D() = default; @@ -148,15 +141,6 @@ public: const BuildVolume& build_volume() const { return m_build_volume; } BuildVolume& build_volume() { return m_build_volume; } - // Belt printer bed settings. tilt_axis: 0 = X (belt along Y), 1 = Y. - void set_belt_printer(bool enabled, float angle_deg, int tilt_axis = 0) { - m_is_belt_printer = enabled; m_belt_angle = angle_deg; m_belt_tilt_axis = tilt_axis; - } - bool is_belt_printer() const { return m_is_belt_printer; } - float belt_angle() const { return m_belt_angle; } - // Unit vector of the tilt axis in bed space. - Vec3d belt_tilt_unit_axis() const { return m_belt_tilt_axis == 1 ? Vec3d::UnitY() : Vec3d::UnitX(); } - // Was the model provided, or was it generated procedurally? Type get_type() const { return m_type; } // Was the model generated procedurally? @@ -196,8 +180,6 @@ private: 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); - void render_slicing_arrow(const Transform3d& view_matrix, const Transform3d& projection_matrix); - void render_slicing_plane(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); diff --git a/src/slic3r/GUI/3DScene.cpp b/src/slic3r/GUI/3DScene.cpp index 81b0ee8028..b5c5c4fa16 100644 --- a/src/slic3r/GUI/3DScene.cpp +++ b/src/slic3r/GUI/3DScene.cpp @@ -1155,17 +1155,7 @@ void GLVolumeCollection::render(GLVolumeCollection::ERenderType type, // 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. - Vec3f up_direction = Vec3f::UnitZ(); - { - const DynamicPrintConfig& prt_cfg = GUI::wxGetApp().preset_bundle->printers.get_edited_preset().config; - double tilt_x_deg = prt_cfg.opt_float("build_plate_tilt_x"); - double tilt_y_deg = prt_cfg.opt_float("build_plate_tilt_y"); - if (tilt_x_deg != 0. || tilt_y_deg != 0.) { - double tilt_x_rad = Geometry::deg2rad(tilt_x_deg); - double tilt_y_rad = Geometry::deg2rad(tilt_y_deg); - up_direction = Vec3f(float(tan(tilt_y_rad)), float(tan(tilt_x_rad)), 1.f).normalized(); - } - } + const Vec3f up_direction = GUI::build_plate_tilt_up_direction().cast(); for (GLVolumeWithIdAndZ& volume : to_render) { #if ENABLE_MODIFIERS_ALWAYS_TRANSPARENT diff --git a/src/slic3r/GUI/BeltPurgeTower.cpp b/src/slic3r/GUI/BeltPurgeTower.cpp index 8c5764072e..e90491a972 100644 --- a/src/slic3r/GUI/BeltPurgeTower.cpp +++ b/src/slic3r/GUI/BeltPurgeTower.cpp @@ -9,6 +9,7 @@ #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" @@ -32,28 +33,45 @@ namespace GUI { // 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, BeltPurgeSignature &sig) +bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, ObjectList *obj_list, std::vector &sigs) { auto is_prism = [](const ModelObject *mo) { const ConfigOption *opt = mo->config.option("belt_purge_tower_object"); return opt != nullptr && opt->getBool(); }; - std::vector prism_idxs; + // 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> prisms_by_plate(static_cast(plate_count)); + std::vector stale_prisms; for (int i = 0; i < (int) model.objects.size(); ++i) - if (is_prism(model.objects[i])) - prism_idxs.push_back(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). - auto remove_prisms = [&](const std::vector &idxs) { + // caller refreshes the scene). Highest index first, so the others stay valid. + auto remove_prisms = [&](std::vector 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 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 @@ -62,10 +80,11 @@ bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, Object { const auto *belt_pre = wxGetApp().preset_bundle->printers.get_edited_preset().config.option("belt_printer"); if (belt_pre == nullptr || !belt_pre->value) { - sig = BeltPurgeSignature{}; - if (prism_idxs.empty()) + std::fill(sigs.begin(), sigs.end(), BeltPurgeSignature{}); + const std::vector all = all_prisms(); + if (all.empty()) return false; - remove_prisms(prism_idxs); + remove_prisms(all); return true; } } @@ -92,65 +111,15 @@ bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, Object const auto *seq_opt = print_config.option>("print_sequence"); const bool by_object = seq_opt != nullptr && seq_opt->value == PrintSequence::ByObject; - // Filaments used and bounding extent of the non-prism objects on the - // current plate (1-based filament ids; volume extruder 0 = object default). - PartPlate *plate = partplate_list.get_curr_plate(); - std::set filaments; - double x_min = std::numeric_limits::max(); - double x_max = -std::numeric_limits::max(); - double y_min = std::numeric_limits::max(); - double y_max = -std::numeric_limits::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); - } - } + // Mixed filament slots are expanded to their physical components below. + std::vector is_mixed; + std::vector comp_strs; + if (const auto *o = full_cfg.option("filament_is_mixed")) + is_mixed = o->values; + if (const auto *o = full_cfg.option("filament_mixed_components")) + comp_strs = o->values; - const bool wanted = belt && prime_tower_enabled && !by_object && have_objects && filaments.size() > 1; - if (!wanted) { - sig = BeltPurgeSignature{}; - if (prism_idxs.empty()) - return false; - remove_prisms(prism_idxs); - BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] belt purge tower removed (conditions not met)"; - return true; - } - - // --- Sizing ----------------------------------------------------------- - const int n_islands = std::max(1, (int) filaments.size() - 1); - const double gap = 1.0; - // 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; + 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 @@ -168,15 +137,6 @@ bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, Object const double sin_t = std::sin(theta); const double cot_t = std::cos(theta) / sin_t; - // 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()). // 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 @@ -185,102 +145,236 @@ bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, Object // 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")); - double max_flush = print_config.has("prime_volume") ? print_config.opt_float("prime_volume") : 45.; - if (use_matrix) { - const size_t extruder_nums = wxGetApp().preset_bundle->get_printer_extruder_count(); - const std::vector matrix = get_flush_volumes_matrix( - project_config.option("flush_volumes_matrix")->values, 0, extruder_nums); - const auto * multi_opt = project_config.option("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; + 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.; - 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; - 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(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 (prism_idxs.size() == 1 && model.objects[size_t(prism_idxs.front())]->instances.size() == 1 && new_sig == sig) - return false; // already up to date — do not touch the model - - // --- 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. - const double margin = 5.; - const double ramp_compensation = height / sin_t; - const double belt_origin = plate_origin[belt_is_y ? 1 : 0]; - const double belt_start = std::max(belt_origin, belt_min - ramp_compensation); // leading ramp, toward belt origin - const double belt_end = belt_max + margin + ramp_compensation + z_max * cot_t; // + parts' top-feature belt reach - const double length = std::max(belt_end - belt_start, 10.); - 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. The bed (printable_area) is plate-local but model instances - // live in the plate's world frame, so add the plate origin's lateral - // component. lat_min/lat_max come from instance_bounding_box (world frame). - const double lat_origin = plate_origin[belt_is_y ? 0 : 1]; - const double inset = 1.; - double lat_center = lat_max + 5. + 0.5 * width; // fallback: just past the parts - BoundingBoxf bed_ext_dbg; + // 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("printable_area"); - bed_opt != nullptr && !bed_opt->values.empty()) { - const BoundingBoxf bed_ext = get_extents(bed_opt->values); - bed_ext_dbg = bed_ext; - 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; + 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 plans(static_cast(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 filaments; + double x_min = std::numeric_limits::max(); + double x_max = -std::numeric_limits::max(); + double y_min = std::numeric_limits::max(); + double y_max = -std::numeric_limits::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 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 matrix = get_flush_volumes_matrix( + project_config.option("flush_volumes_matrix")->values, 0, extruder_nums); + const auto * multi_opt = project_config.option("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(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); } - BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] purge place" - << " plate_origin=(" << plate_origin.x() << "," << plate_origin.y() << ")" - << " parts_x=[" << x_min << "," << x_max << "] parts_y=[" << y_min << "," << y_max << "] z_max=" << z_max - << " bed_ext=[" << bed_ext_dbg.min.x() << "," << bed_ext_dbg.min.y() - << " -> " << bed_ext_dbg.max.x() << "," << bed_ext_dbg.max.y() << "]" - << " lat_center=" << lat_center << " belt=[" << belt_start << "," << belt_end << "]"; + // --- 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 to_delete = stale_prisms; + std::vector to_create; + for (int plate_idx = 0; plate_idx < plate_count; ++plate_idx) { + const Plan &plan = plans[size_t(plate_idx)]; + const std::vector &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; - const Vec3d desired_center(belt_is_y ? lat_center : belt_center, - belt_is_y ? belt_center : lat_center, - 0.5 * height); + 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, @@ -299,77 +393,64 @@ bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, Object // 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. - const double w_sub = (width - (n_islands - 1) * gap) / n_islands; + for (int plate_idx : to_create) { + const Plan &plan = plans[size_t(plate_idx)]; - // --- (Re)create --------------------------------------------------------- - if (!prism_idxs.empty()) - remove_prisms(prism_idxs); + 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); + } - TriangleMesh prism_mesh; - for (int i = 0; i < n_islands; ++i) { - const double lat_off = i * (w_sub + gap); - // Box dims: lateral = w_sub, along-belt = length, vertical = height. - TriangleMesh box = belt_is_y ? make_cube(w_sub, length, height) // X = lateral, Y = belt - : make_cube(length, w_sub, 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(ipRectilinear)); + cfg.set_key_value("enable_support", new ConfigOptionBool(false)); + cfg.set_key_value("brim_type", new ConfigOptionEnum(btNoBrim)); + cfg.set_key_value("seam_slope_type", new ConfigOptionEnum(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; } - 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(ipRectilinear)); - cfg.set_key_value("enable_support", new ConfigOptionBool(false)); - cfg.set_key_value("brim_type", new ConfigOptionEnum(btNoBrim)); - cfg.set_key_value("seam_slope_type", new ConfigOptionEnum(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(desired_center.x() - cur.center().x(), - desired_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); - - // Record the inputs that produced this prism so subsequent ticks are no-ops - // until the parts/config actually change. - sig = new_sig; - - BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] belt purge tower generated" - << " belt_is_y=" << belt_is_y - << " W=" << width << " L=" << length << " H=" << height - << " v_layer=" << v_layer << " max_flush=" << max_flush - << " filaments=" << filaments.size() - << " theta_deg=" << Geometry::rad2deg(theta) - << " desired_center=(" << desired_center.x() << "," << desired_center.y() << "," << desired_center.z() << ")" - << " achieved_center=(" << new_object->bounding_box_exact().center().x() << "," - << new_object->bounding_box_exact().center().y() << ")"; return true; } diff --git a/src/slic3r/GUI/BeltPurgeTower.hpp b/src/slic3r/GUI/BeltPurgeTower.hpp index c5a6d301a0..a009eb4d4b 100644 --- a/src/slic3r/GUI/BeltPurgeTower.hpp +++ b/src/slic3r/GUI/BeltPurgeTower.hpp @@ -1,5 +1,7 @@ #pragma once +#include + // ORCA-Belt: auto-managed purge prism for belt printers. // // Kept in its own translation unit (not buried in Plater.cpp) so it stays out @@ -22,24 +24,25 @@ struct BeltPurgeSignature { bool valid = false; int filament_count = 0; - long key[12] = {0}; // rounded geometry and plate inputs (0.1 mm units) + 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 < 12; ++i) + for (int i = 0; i < 14; ++i) if (key[i] != o.key[i]) return false; return true; } }; -// Keep the auto-generated belt purge prism in sync with the current config and -// plate contents. Creates / updates / removes the marked prism ModelObject. +// 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, BeltPurgeSignature &sig); +bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, ObjectList *obj_list, std::vector &sigs); } // namespace GUI } // namespace Slic3r diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp index 65c2df092c..ac3ffa9006 100644 --- a/src/slic3r/GUI/ConfigManipulation.cpp +++ b/src/slic3r/GUI/ConfigManipulation.cpp @@ -1193,8 +1193,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("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("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); diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index ae504d3746..89bc7e3fab 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -1164,7 +1164,7 @@ std::vector GCodeViewer::get_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 (BeltGCodeWriter:: +// back to Cartesian space. The G-code forward pipeline is (BeltKinematics:: // to_machine_coords): gcode = MachineFrame( AxisRemap( X ) ), with X = model if // gcode_back_transform (write already un-rotated to Cartesian) else BeltForward( // model). So the inverse is: @@ -1223,10 +1223,8 @@ 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 loaded print here. Plater::set_bed_shape also calls set_belt_printer(), - // but only on bed-shape changes — not reliably on every slice/preview load — so the UI was - // staying hidden even though the (config-driven) designed view rendered. The tilt magnitude - // comes from the G-code header (gcode_result.belt_tilt_angle, abs of the slicing rotation). + // menu item) from the loaded print. The tilt magnitude comes from the G-code header + // (gcode_result.belt_tilt_angle, abs of the slicing rotation). m_belt_view_enabled = print.config().belt_printer.value; m_belt_angle_deg = gcode_result.belt_tilt_angle; @@ -1387,8 +1385,6 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const // translation, which this min-corner step recovers. const Vec3d d = model_bb.min - tp_bb.min; belt_inv = Transform3d(Eigen::Translation3d(d)) * belt_inv; - BOOST_LOG_TRIVIAL(debug) << "[BELT-PREVIEW] anchor d=[" << d.x() << "," << d.y() << "," << d.z() - << "] (clip kept " << n_clip << "/" << n_filtered << " moves)"; } } libvgcode::GCodeInputData data = libvgcode::convert(gcode_result, str_tool_colors, str_color_print_colors, m_viewer, @@ -1522,8 +1518,18 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const }); m_paths_bounding_box = BoundingBoxf3(libvgcode::convert(bbox[0]).cast(), libvgcode::convert(bbox[1]).cast()); - 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; @@ -4923,7 +4929,7 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv ImGui::Spacing(); ImGui::Dummy({ window_padding, 0 }); ImGui::SameLine(); - ImGui::TextColored(ImVec4(0.f, 0.59f, 0.53f, 1.f), "%s", _u8L("Belt Printer").c_str()); + ImGui::TextColored(ImVec4(0.f, 0.59f, 0.53f, 1.f), "%s", _u8L("Belt printer").c_str()); ImGui::Dummy({ window_padding, 0 }); ImGui::SameLine(); // Checked = show the raw machine-frame G-code (designed/upright view off). Worded to diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 966cf49013..512ddd315e 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -81,6 +81,7 @@ #include #include "libslic3r/Utils.hpp" +#include "libslic3r/Geometry.hpp" #include "libslic3r/Model.hpp" #include "libslic3r/I18N.hpp" #include "libslic3r/PresetBundle.hpp" @@ -9812,5 +9813,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("build_plate_tilt_x"); + const auto *opt_y = cfg.option("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 diff --git a/src/slic3r/GUI/GUI_App.hpp b/src/slic3r/GUI/GUI_App.hpp index f6f0b81c92..1355d7728d 100644 --- a/src/slic3r/GUI/GUI_App.hpp +++ b/src/slic3r/GUI/GUI_App.hpp @@ -831,6 +831,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& model_filaments); +// Up direction of the edited printer's tilted build plate (+Z when untilted). +Vec3d build_plate_tilt_up_direction(); } // namespace GUI } // Slic3r diff --git a/src/slic3r/GUI/GUI_Factories.cpp b/src/slic3r/GUI/GUI_Factories.cpp index 30fdfc0d04..6a47d5ad2f 100644 --- a/src/slic3r/GUI/GUI_Factories.cpp +++ b/src/slic3r/GUI/GUI_Factories.cpp @@ -63,7 +63,6 @@ static SettingsFactory::Bundle FREQ_SETTINGS_BUNDLE_FFF = { L("Support") , { "enable_support", "support_type", "support_threshold_angle", "support_threshold_overlap", "support_base_pattern", "support_on_build_plate_only","support_critical_regions_only", "support_remove_small_overhang", - "build_plate_tilt_x", "build_plate_tilt_y", "support_base_pattern_spacing", "support_expansion"}}, //BBS { L("Flush options") , { "flush_into_infill", "flush_into_objects", "flush_into_support"} } @@ -93,8 +92,7 @@ std::map> SettingsFactory::OBJECT_C {"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}, - {"build_plate_tilt_x","",35},{"build_plate_tilt_y","",36}, - {"support_object_first_layer_gap","",37} + {"support_object_first_layer_gap","",35} }}, { L("Speed"), {{"support_speed", "",12}, {"support_interface_speed", "",13} }} diff --git a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp index 468e2e9b6c..78a3d4a834 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp @@ -548,14 +548,6 @@ int GLGizmoFdmSupports::get_selection_support_threshold_angle() return auto_support ? support_threshold_angle : 0; } -std::pair GLGizmoFdmSupports::get_build_plate_tilt() -{ - const DynamicPrintConfig& cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config; - double tilt_x = cfg.opt_float("build_plate_tilt_x"); - double tilt_y = cfg.opt_float("build_plate_tilt_y"); - return {tilt_x, tilt_y}; -} - void GLGizmoFdmSupports::select_facets_by_angle(float threshold_deg, bool block) { float threshold = (float(M_PI)/180.f)*threshold_deg; @@ -564,15 +556,9 @@ void GLGizmoFdmSupports::select_facets_by_angle(float threshold_deg, bool block) const ModelInstance* mi = mo->instances[selection.get_instance_idx()]; // Compute gravity direction accounting for build plate tilt - auto [tilt_x_deg, tilt_y_deg] = get_build_plate_tilt(); - double tilt_x_rad = tilt_x_deg * M_PI / 180.0; - double tilt_y_rad = tilt_y_deg * M_PI / 180.0; - const bool has_tilt = (tilt_x_deg != 0. || tilt_y_deg != 0.); - // NB: use an if, not a ?:, so each branch converts to Vec3d independently - // (the two Eigen expression types don't unify in a ternary). - Vec3d gravity_dir = -Vec3d::UnitZ(); - if (has_tilt) - gravity_dir = Vec3d(-tan(tilt_y_rad), -tan(tilt_x_rad), -1.0).normalized(); + const Vec3d up_dir = build_plate_tilt_up_direction(); + const bool has_tilt = up_dir != Vec3d::UnitZ(); + const Vec3d gravity_dir = -up_dir; int mesh_id = -1; for (const ModelVolume* mv : mo->volumes) { diff --git a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp index b313ca04aa..57a6cc1981 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp @@ -60,7 +60,6 @@ private: void select_facets_by_angle(float threshold, bool block); // BBS int get_selection_support_threshold_angle(); - std::pair get_build_plate_tilt(); int m_support_threshold_angle = -1; diff --git a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp index ac0f46df34..d85c635b5a 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp @@ -75,14 +75,7 @@ GLGizmoPainterBase::ClippingPlaneDataWrapper GLGizmoPainterBase::get_clipping_pl Vec3f GLGizmoPainterBase::get_tilt_up_direction() const { - const DynamicPrintConfig& cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config; - double tilt_x_deg = cfg.opt_float("build_plate_tilt_x"); - double tilt_y_deg = cfg.opt_float("build_plate_tilt_y"); - if (tilt_x_deg == 0. && tilt_y_deg == 0.) - return Vec3f::UnitZ(); - double tilt_x_rad = Geometry::deg2rad(tilt_x_deg); - double tilt_y_rad = Geometry::deg2rad(tilt_y_deg); - return Vec3f(float(tan(tilt_y_rad)), float(tan(tilt_x_rad)), 1.f).normalized(); + return build_plate_tilt_up_direction().cast(); } void GLGizmoPainterBase::render_triangles(const Selection& selection) const @@ -707,7 +700,7 @@ bool GLGizmoPainterBase::gizmo_event(SLAGizmoEventType action, const Vec2d& mous mi->get_assemble_transformation().get_matrix() * mo->volumes[m_rr.mesh_id]->get_matrix() : mi->get_transformation().get_matrix() * mo->volumes[m_rr.mesh_id]->get_matrix(); m_triangle_selectors[m_rr.mesh_id]->seed_fill_select_triangles(m_rr.hit, int(m_rr.facet), trafo_matrix_not_translate, this->get_clipping_plane_in_volume_coordinates(trafo_matrix), m_smart_fill_angle, - m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, true, get_tilt_up_direction()); + m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, get_tilt_up_direction(), true); m_triangle_selectors[m_rr.mesh_id]->request_update_render_data(); m_seed_fill_last_mesh_id = m_rr.mesh_id; } @@ -871,7 +864,7 @@ bool GLGizmoPainterBase::gizmo_event(SLAGizmoEventType action, const Vec2d& mous m_triangle_selectors[mesh_idx]->seed_fill_apply_on_triangles(new_state); if (m_tool_type == ToolType::SMART_FILL) m_triangle_selectors[mesh_idx]->seed_fill_select_triangles(mesh_hit, facet_idx, trafo_matrix_not_translate, clp, m_smart_fill_angle, - m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, true, get_tilt_up_direction()); + m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, get_tilt_up_direction(), true); else if (m_tool_type == ToolType::BRUSH && m_cursor_type == TriangleSelector::CursorType::POINTER) // BBS: add infill_angle parameter m_triangle_selectors[mesh_idx]->bucket_fill_select_triangles(mesh_hit, facet_idx, clp, -1.f, false, true); @@ -969,7 +962,7 @@ bool GLGizmoPainterBase::gizmo_event(SLAGizmoEventType action, const Vec2d& mous const TriangleSelector::ClippingPlane &clp = this->get_clipping_plane_in_volume_coordinates(trafo_matrix); if (m_tool_type == ToolType::SMART_FILL) m_triangle_selectors[m_rr.mesh_id]->seed_fill_select_triangles(m_rr.hit, int(m_rr.facet), trafo_matrix_not_translate, clp, m_smart_fill_angle, - m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, false, get_tilt_up_direction()); + m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, get_tilt_up_direction(), false); else if (m_tool_type == ToolType::BRUSH && m_cursor_type == TriangleSelector::CursorType::POINTER) // BBS: add infill_angle parameter m_triangle_selectors[m_rr.mesh_id]->bucket_fill_select_triangles(m_rr.hit, int(m_rr.facet), clp, -1.f, false); diff --git a/src/slic3r/GUI/Jobs/ArrangeJob.cpp b/src/slic3r/GUI/Jobs/ArrangeJob.cpp index cd4cc83dab..a21bf52c19 100644 --- a/src/slic3r/GUI/Jobs/ArrangeJob.cpp +++ b/src/slic3r/GUI/Jobs/ArrangeJob.cpp @@ -15,6 +15,9 @@ #include "slic3r/GUI/GUI_ObjectList.hpp" #include "libnest2d/common.hpp" +#include "libslic3r/Geometry.hpp" +#include +#include #define SAVE_ARRANGE_POLY 0 @@ -63,6 +66,14 @@ public: } }; +// The belt purge prism is generated from the arranged parts (ensure_belt_purge_tower), +// so arrange neither moves it nor packs around it; it reserves the prism's strip instead. +static bool is_belt_purge_prism(const ModelObject *mo) +{ + const ConfigOption *opt = mo->config.option("belt_purge_tower_object"); + return opt != nullptr && opt->getBool(); +} + // BBS: add partplate logic static WipeTower get_wipe_tower(const Plater &plater, int plate_idx) { @@ -77,6 +88,63 @@ arrangement::ArrangePolygon get_wipetower_arrange_poly(WipeTower* tower) return ap; } +// Belt printers pack their parts against the edges of the bed, so what has to stay +// free there is reserved with fixed virtual items on every plate, the way the bed's +// own exclusion areas are: +// - the strip the purge prism comes back to, flush with the far lateral edge (see +// ensure_belt_purge_tower), when the parts use more than one filament; +// - the brim along every edge: a belt brim is printed brim_width wide for every brim +// type but none. Between parts the brims may overlap, as on any printer. +void ArrangeJob::prepare_belt_regions(int num_plates) +{ + if (!params.is_belt || params.is_seq_print) + return; + const DynamicPrintConfig &config = wxGetApp().preset_bundle->full_config(); + const BoundingBoxf bed = get_extents(config.opt("printable_area")->values); + const bool belt_is_y = params.belt_axis == 1; + std::vector regions; + + std::set filaments; + for (const ArrangePolygons *items : { &m_selected, &m_unselected }) + for (const ArrangePolygon &ap : *items) + if (!ap.is_virt_object) + filaments.insert(ap.extrude_ids.begin(), ap.extrude_ids.end()); + if (config.opt_bool("enable_belt_purge_tower") && filaments.size() > 1) { + // The prism is at least one millimetre per island plus the gaps between them. + const int islands = int(filaments.size()) - 1; + const double width = std::max(config.opt_float("belt_purge_tower_width"), 2. * islands - 1.) + 1.; // + the prism's edge inset + BoundingBoxf strip = bed; + if (belt_is_y) + strip.min.x() = std::max(bed.min.x(), bed.max.x() - width); + else + strip.min.y() = std::max(bed.min.y(), bed.max.y() - width); + regions.push_back(strip); + } + + // Virtual items are inflated by the one millimetre exclusion gap already. + const double brim = config.opt_enum("brim_type") == btNoBrim ? 0. : + config.opt_float("brim_width") + config.opt_float("brim_object_gap") + config.opt_float("extra_brim_width") - 1.; + if (brim > 0.) { + regions.emplace_back(bed.min, Vec2d(bed.min.x() + brim, bed.max.y())); + regions.emplace_back(Vec2d(bed.max.x() - brim, bed.min.y()), bed.max); + regions.emplace_back(bed.min, Vec2d(bed.max.x(), bed.min.y() + brim)); + regions.emplace_back(Vec2d(bed.min.x(), bed.max.y() - brim), bed.max); + } + + for (int j = 0; j < num_plates; ++j) + for (size_t i = 0; i < regions.size(); ++i) { + ArrangePolygon ap; + ap.poly.contour = scaled(regions[i]).polygon(); + ap.translation = Vec2crd(0, 0); + ap.rotation = 0.f; + ap.is_virt_object = true; + ap.bed_idx = j; + ap.height = 1; + ap.name = "BeltRegion" + std::to_string(i); + m_unselected.emplace_back(std::move(ap)); + } +} + void ArrangeJob::clear_input() { const Model &model = m_plater->model(); @@ -128,6 +196,8 @@ void ArrangeJob::prepare_selected() { for (size_t oidx = 0; oidx < model.objects.size(); ++oidx) { const Selection::InstanceIdxsList* instlist = obj_sel[oidx]; ModelObject* mo = model.objects[oidx]; + if (is_belt_purge_prism(mo)) + continue; std::vector inst_sel(mo->instances.size(), false); @@ -176,6 +246,7 @@ void ArrangeJob::prepare_selected() { } prepare_wipe_tower(); + prepare_belt_regions(MAX_NUM_PLATES); // The strides have to be removed from the fixed items. For the @@ -206,6 +277,8 @@ void ArrangeJob::prepare_all() { // Go through the objects and check if inside the selection for (size_t oidx = 0; oidx < model.objects.size(); ++oidx) { ModelObject *mo = model.objects[oidx]; + if (is_belt_purge_prism(mo)) + continue; for (size_t i = 0; i < mo->instances.size(); ++i) { ModelInstance * mi = mo->instances[i]; @@ -252,6 +325,7 @@ void ArrangeJob::prepare_all() { // add the virtual object into unselect list if has plate_list.preprocess_exclude_areas(m_unselected, enable_wrapping, MAX_NUM_PLATES); + prepare_belt_regions(MAX_NUM_PLATES); } arrangement::ArrangePolygon estimate_wipe_tower_info(int plate_index, std::set& extruder_ids) @@ -290,10 +364,9 @@ void ArrangeJob::prepare_wipe_tower() bool enable_prime_tower = op && op->getBool(); if (!enable_prime_tower || params.is_seq_print) return; - // Belt printers have no classic wipe tower; purging goes into the belt - // purge prism, which is a real model object and arranges like any other. - if (const auto *belt_opt = wxGetApp().preset_bundle->printers.get_edited_preset().config.option("belt_printer"); - belt_opt && belt_opt->value) + // Belt printers have no classic wipe tower; purging goes into the belt purge + // prism, whose strip prepare_belt_regions() reserves. + if (params.is_belt) return; bool smooth_timelapse = false; @@ -399,6 +472,8 @@ void ArrangeJob::prepare_partplate() { for (size_t oidx = 0; oidx < model.objects.size(); ++oidx) { ModelObject* mo = model.objects[oidx]; + if (is_belt_purge_prism(mo)) + continue; for (size_t inst_idx = 0; inst_idx < mo->instances.size(); ++inst_idx) { bool in_plate = plate->contain_instance(oidx, inst_idx) || plate->intersect_instance(oidx, inst_idx); @@ -434,6 +509,7 @@ void ArrangeJob::prepare_partplate() { // add the virtual object into unselect list if has plate_list.preprocess_exclude_areas(m_unselected, enable_wrapping, current_plate_index + 1); + prepare_belt_regions(current_plate_index + 1); } //BBS: add partplate logic @@ -785,6 +861,19 @@ arrangement::ArrangeParams init_arrange_params(Plater *p) params.is_seq_print = settings.is_seq_print; params.min_obj_distance = scaled(settings.distance); params.align_to_y_axis = settings.align_to_y_axis; + if (print_config.belt_printer.value) { + // Parts print in belt order: the belt runs across the gantry's tilt axis, a + // rotation about X prints toward +Y and one about Y toward -X (see + // BeltTransform), a negative angle flips that, and a tilted layer reaches + // cot(angle) * height past a part's far edge. + const BeltRotationAxis axis = print_config.belt_slice_rotation.value; + const double angle = print_config.belt_slice_rotation_angle.value; + const bool tilted = (axis == BeltRotationAxis::X || axis == BeltRotationAxis::Y) && std::abs(angle) > EPSILON; + params.is_belt = true; + params.belt_axis = axis == BeltRotationAxis::Y ? 0 : 1; + params.belt_reversed = tilted && ((axis == BeltRotationAxis::Y) != (angle < 0.)); + params.belt_tilt_slope = tilted ? float(1. / std::tan(Geometry::deg2rad(std::clamp(std::abs(angle), 5., 90.)))) : 0.f; + } int state = p->get_prepare_state(); if (state == Job::JobPrepareState::PREPARE_STATE_MENU) { diff --git a/src/slic3r/GUI/Jobs/ArrangeJob.hpp b/src/slic3r/GUI/Jobs/ArrangeJob.hpp index 0c9f03de01..19143c3254 100644 --- a/src/slic3r/GUI/Jobs/ArrangeJob.hpp +++ b/src/slic3r/GUI/Jobs/ArrangeJob.hpp @@ -46,6 +46,7 @@ class ArrangeJob : public Job //BBS:prepare the items from current selected partplate void prepare_partplate(); void prepare_wipe_tower(); + void prepare_belt_regions(int num_plates); ArrangePolygon prepare_arrange_polygon(void* instance); diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index 893260f934..2b9bbbdf02 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -612,6 +612,17 @@ void PartPlate::calc_height_limit() { BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << "Unable to create height limit top lines\n"; } +// The plate's icons and labels grow with its depth, but they sit in the gap to the +// next plate, which grows with its width: on a long, narrow bed (a belt) they would +// otherwise run across the neighbouring plate. +float PartPlate::icon_scale_factor() const +{ + const BoundingBoxf bed_ext = get_extents(m_shape); + const double by_depth = bed_ext.size().y() / 200.; + const double by_gap = bed_ext.size().x() * LOGICAL_PART_PLATE_GAP / (PARTPLATE_ICON_SIZE + 2 * PARTPLATE_ICON_GAP_LEFT); + return float(std::min(by_depth, by_gap)); +} + void PartPlate::calc_vertex_for_number(int index, bool one_number, GLModel &buffer) { buffer.reset(); @@ -628,7 +639,7 @@ void PartPlate::calc_vertex_for_number(int index, bool one_number, GLModel &buff #else //in the bottom auto bed_ext = get_extents(m_shape); Vec2d p = bed_ext[1]; - float factor = bed_ext.size()(1) / 200.0; + float factor = icon_scale_factor(); float size = PARTPLATE_ICON_SIZE * factor; float offset_y = PARTPLATE_TEXT_OFFSET_Y * factor; float offset_x = (one_number?PARTPLATE_TEXT_OFFSET_X1: PARTPLATE_TEXT_OFFSET_X2) * factor; @@ -650,7 +661,7 @@ void PartPlate::calc_vertex_for_plate_name_edit_icon(GLTexture *texture, int ind ExPolygon poly; auto bed_ext = get_extents(m_shape); Vec2d p = bed_ext[3]; - float factor = bed_ext.size()(1) / 200.0; + float factor = icon_scale_factor(); float icon_sz = factor * PARTPLATE_EDIT_PLATE_NAME_ICON_SIZE; float width = icon_sz; float height = icon_sz; @@ -683,7 +694,7 @@ void PartPlate::calc_vertex_for_icons(int index, PickingModel &model) ExPolygon poly; auto bed_ext = get_extents(m_shape); Vec2d p = bed_ext[2]; - auto factor = bed_ext.size()(1) / 200.0; + float factor = icon_scale_factor(); float size = PARTPLATE_ICON_SIZE * factor; float gap_left = PARTPLATE_ICON_GAP_LEFT * factor; float gap_y = PARTPLATE_ICON_GAP_Y * factor; @@ -2589,7 +2600,7 @@ void PartPlate::generate_plate_name_texture() ExPolygon poly; auto bed_ext = get_extents(m_shape); Vec2d p = bed_ext[3]; - float factor = bed_ext.size()(1) / 200.0; + float factor = icon_scale_factor(); float icon_sz = factor * PARTPLATE_EDIT_PLATE_NAME_ICON_SIZE; float width = icon_sz * m_name_texture.get_width() / m_name_texture.get_height(); // icon size * text_bb_ratio float height = icon_sz; // scale with icon size to preserve ratio while system scaling @@ -2783,6 +2794,7 @@ bool PartPlate::check_outside(int obj_id, int instance_id, BoundingBoxf3* boundi BoundingBoxf3 instance_box = bounding_box? *bounding_box: object->instance_convex_hull_bounding_box(instance_id); Polygon hull = instance->convex_hull_2d(); BoundingBoxf3 plate_box = get_plate_box(); + this->open_belt_y(plate_box); if (instance_box.max.z() > plate_box.min.z()) plate_box.min.z() += instance_box.min.z(); // not considering outsize if sinking @@ -3467,6 +3479,19 @@ Polygon PartPlate::get_shared_printable_polygon() const return m_extruder_areas.empty() ? Polygon::new_scale(m_shape) : get_shared_poly(m_extruder_areas); } + +bool PartPlate::belt_open_y() const +{ + // Headless (CLI) plates have no plater and no wxApp behind wxGetApp(); the CLI's own belt + // handling lives in Print::validate(). + if (m_plater == nullptr || wxGetApp().preset_bundle == nullptr) + return false; + const DynamicPrintConfig &printer = wxGetApp().preset_bundle->printers.get_edited_preset().config; + const auto *belt = printer.option("belt_printer"); + const auto *infinite_y = printer.option("belt_printer_infinite_y"); + return belt != nullptr && belt->value && infinite_y != nullptr && infinite_y->value; +} + bool PartPlate::contains(const Vec3d& point) const { return m_bounding_box.contains(point); @@ -3486,6 +3511,7 @@ bool PartPlate::contains(const BoundingBoxf3& bb) const print_volume.min(1) -= Slic3r::BuildVolume::BedEpsilon; print_volume.max(0) += Slic3r::BuildVolume::BedEpsilon; print_volume.max(1) += Slic3r::BuildVolume::BedEpsilon; + this->open_belt_y(print_volume); return print_volume.contains(bb); } @@ -3498,6 +3524,7 @@ bool PartPlate::intersects(const BoundingBoxf3& bb) const print_volume.min(1) -= Slic3r::BuildVolume::BedEpsilon; print_volume.max(0) += Slic3r::BuildVolume::BedEpsilon; print_volume.max(1) += Slic3r::BuildVolume::BedEpsilon; + this->open_belt_y(print_volume); return print_volume.intersects(bb); } diff --git a/src/slic3r/GUI/PartPlate.hpp b/src/slic3r/GUI/PartPlate.hpp index e913ebaabf..67e8e300f5 100644 --- a/src/slic3r/GUI/PartPlate.hpp +++ b/src/slic3r/GUI/PartPlate.hpp @@ -178,6 +178,7 @@ private: void calc_triangles_from_polygon(const ExPolygon &poly, GLModel& render_model); void calc_gridlines(const ExPolygon& poly, const BoundingBox& pp_bbox); void calc_height_limit(); + float icon_scale_factor() const; void calc_vertex_for_number(int index, bool one_number, GLModel &buffer); void calc_vertex_for_plate_name_edit_icon(GLTexture *texture, int index, PickingModel &model); void calc_vertex_for_icons(int index, PickingModel &model); @@ -427,6 +428,10 @@ public: bool contains(const GLVolume& v) const; bool contains(const BoundingBoxf3& bb) const; bool intersects(const BoundingBoxf3& bb) const; + // A belt printer with belt_printer_infinite_y: the plate is open along Y for the + // containment tests (the drawn plate keeps its shape). + bool belt_open_y() const; + void open_belt_y(BoundingBoxf3 &box) const { if (this->belt_open_y()) { box.min.y() = -1e5; box.max.y() = 1e5; } } void render(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool only_body = false, bool force_background_color = false, HeightLimitMode mode = HEIGHT_LIMIT_NONE, int hover_id = -1, bool render_cali = false, bool show_grid = true); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 25084e9a9c..b011d55865 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -6993,7 +6993,7 @@ struct Plater::priv // config and plate contents (thin wrapper over GUI::ensure_belt_purge_tower // in BeltPurgeTower.cpp). Returns true when the model was mutated. bool ensure_belt_purge_tower(); - BeltPurgeSignature m_belt_purge_sig; + std::vector m_belt_purge_sigs; void delete_all_objects_from_model(); void reset(bool apply_presets_change = false); void center_selection(); @@ -10633,7 +10633,7 @@ void Plater::priv::process_validation_warnings(const std::vectorobj_list(), m_belt_purge_sig); + return GUI::ensure_belt_purge_tower(model, partplate_list, sidebar->obj_list(), m_belt_purge_sigs); } @@ -14145,37 +14145,6 @@ void Plater::priv::set_bed_shape(const Pointfs &shape, Vec2d shape_position = partplate_list.get_current_shape_position(); bool new_shape = bed.set_shape(shape, printable_height, extruder_areas, extruder_heights, custom_model, force_as_custom, shape_position); - // Belt printer: configure build volume and bed rendering for belt mode. - { - const auto *belt_opt = config->option("belt_printer"); - bool is_belt = belt_opt && belt_opt->value; - if (is_belt) { - // The slicing rotation is the single source of truth for the belt tilt: - // its magnitude is the physical tilt angle and its axis is the tilt axis. - auto rot_axis = config->option>("belt_slice_rotation")->value; - double rot_angle = config->opt_float("belt_slice_rotation_angle"); - double belt_angle = std::abs(rot_angle); // physical tilt magnitude - int tilt_axis = (rot_axis == BeltRotationAxis::Y) ? 1 : 0; - bool infinite_y = config->opt_bool("belt_printer_infinite_y"); - bed.build_volume().set_belt_printer(true, belt_angle, infinite_y); - bed.set_belt_printer(true, static_cast(belt_angle), tilt_axis); - if (preview) - preview->get_canvas3d()->get_gcode_viewer().set_belt_printer(true, static_cast(belt_angle)); - // The belt "designed view" back-transform is rebuilt from the print config at - // G-code load time (GCodeViewer::compute_belt_back_transform), so no mesh-side - // inverse needs to be pushed to the viewer here. - } else { - // Reset the BuildVolume belt state too: Bed3D::set_shape early-returns when - // the bed params are unchanged, so a belt->normal switch (or toggling belt off - // on the same printer) would otherwise leave the BuildVolume with - // m_is_belt_printer=true and an inflated Y bbox, wrongly treating out-of-bounds - // objects as printable. Idempotent for a printer that was never belt. - bed.build_volume().set_belt_printer(false, 0., false); - bed.set_belt_printer(false, 0.f); - if (preview) - preview->get_canvas3d()->get_gcode_viewer().set_belt_printer(false, 0.f); - } - } float prev_height_lid, prev_height_rod; partplate_list.get_height_limits(prev_height_lid, prev_height_rod); @@ -15893,20 +15862,6 @@ void Plater::_calib_apply_belt_mode() inst->rotate(cancel_rotation); obj->invalidate_bounding_box(); obj->ensure_on_bed(); - - { - const BoundingBoxf3 rb = obj->raw_bounding_box(); - const Vec3d io = inst->get_offset(); - const Vec3d ir = inst->get_rotation(); - BOOST_LOG_TRIVIAL(debug) << "[BELT-CALIB] helper exit: obj=" << obj->name - << " inst_offset=(" << io.x() << "," << io.y() << "," << io.z() << ")" - << " inst_rot=(" << ir.x() << "," << ir.y() << "," << ir.z() << ")" - << " vol0_offset=(" << obj->volumes.front()->get_offset().x() << "," - << obj->volumes.front()->get_offset().y() << "," << obj->volumes.front()->get_offset().z() << ")" - << " raw_bbox=(" << rb.min.x() << "," << rb.min.y() << "," << rb.min.z() - << ")..(" << rb.max.x() << "," << rb.max.y() << "," << rb.max.z() << ")" - << " min_z=" << obj->min_z(); - } } // Each object's support wedge extends upstream of it by roughly its own @@ -15961,7 +15916,7 @@ void Plater::_calib_apply_belt_mode() void Plater::calib_pa(const Calib_Params& params) { // ORCA-Belt: PA Line / PA Pattern have the belt plumbing in place - // (BeltGCodeWriter::set_world_coordinates draws them on the belt surface) + // (belt kinematics in world-coordinates mode draws them on the belt surface) // but are not validated yet — keep them gated to the PA Tower for now. { double angle_rad = 0.; @@ -16556,7 +16511,8 @@ void Plater::calib_temp(const Calib_Params& params) { << ", falling back to 230_190 (embossed numbers will not match)"; asset = calib_dir + "belt_temp_tower_230_190.stl"; } - add_model(false, asset); + if (!add_model(false, asset) || model().objects.empty()) + return; // Place keel-first asset at the belt entry (designed Y = 0) so Z_gcode // starts at 0, centered laterally on the bed, resting on the conveyor. diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 1c03ddf88a..b7c9264381 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -3230,16 +3230,17 @@ void TabPrint::toggle_options() const auto current = m_config->opt_enum("brim_type"); auto &opt = const_cast(field->m_opt); auto cb = dynamic_cast(choice->window); - if (cb != nullptr) { + // Keep the entry if it is already selected, so switching to a non-belt + // printer cannot leave the control showing a value it does not offer. + const bool offer_leading_edge = is_belt_printer || current == btLeadingEdgeOnly; + const bool offered = std::find(opt.enum_values.begin(), opt.enum_values.end(), "leading_edge_only") != opt.enum_values.end(); + if (cb != nullptr && offer_leading_edge != offered) { auto n = cb->GetValue(); opt.enum_values.clear(); opt.enum_labels.clear(); cb->Clear(); for (size_t i = 0; i < def->enum_values.size(); ++ i) { - // Keep the entry if it is already selected, so switching to a non-belt - // printer cannot leave the control showing a value it does not offer. - if (def->enum_values[i] == "leading_edge_only" && ! is_belt_printer - && current != btLeadingEdgeOnly) + if (def->enum_values[i] == "leading_edge_only" && ! offer_leading_edge) continue; opt.enum_values.push_back(def->enum_values[i]); opt.enum_labels.push_back(def->enum_labels[i]); @@ -5104,7 +5105,7 @@ void TabPrinter::build_fff() { Line line = { L("Belt tilt"), L("Belt tilt axis and angle, applied as a mesh rotation before " - "slicing. Also drives bed rendering and support gravity tilt. " + "slicing. Also drives bed rendering and support gravity tilt. " "Isometric (no distortion); the back-transform inverts it before " "the machine-frame remap.") }; line.append_option(belt_og->get_option("belt_slice_rotation")); @@ -5159,7 +5160,7 @@ void TabPrinter::build_fff() { Line line = { L("Machine-frame tilt"), L("The machine-frame shear (tan) and scale (1/cos) are derived from " - "the belt tilt angle. Enable 'Decouple' to set an independent " + "the belt tilt angle. Enable 'Decouple' to set an independent " "machine-frame angle when the physical gantry tilt differs from " "the slicing rotation.") }; line.append_option(mf->get_option("belt_frame_tilt_decouple")); @@ -6213,8 +6214,10 @@ void TabPrinter::toggle_options() toggle_line("belt_slice_rotation", is_belt); // Remap, back-transform, and global mesh-transforms toggles are gated by belt - // mode here; finer mode-based visibility (Advanced vs Expert) is handled by - // each option's ConfigOptionMode in PrintConfig.cpp. + // mode here; finer mode-based visibility is handled by each option's + // ConfigOptionMode in PrintConfig.cpp. Both axis remaps are Develop-only: a + // printer profile sets them once for its kinematics, and a wrong value sends + // the gantry outside the machine. for (auto el : {"preslice_remap_x", "gcode_remap_x", "gcode_back_transform"}) toggle_line(el, is_belt); toggle_line("belt_preslice_global", is_belt); diff --git a/src/slic3r/GUI/calib_dlg.cpp b/src/slic3r/GUI/calib_dlg.cpp index c0979ec56c..83e36dbd69 100644 --- a/src/slic3r/GUI/calib_dlg.cpp +++ b/src/slic3r/GUI/calib_dlg.cpp @@ -98,7 +98,7 @@ std::vector make_shaper_type_labels() } // ORCA-Belt: PA Line / PA Pattern have belt plumbing in place (drawn on the -// belt surface via BeltGCodeWriter world-coordinates mode) but are not +// belt surface via BeltKinematics world-coordinates mode) but are not // validated yet — belt printers are restricted to the PA Tower for now. bool is_belt_printer_selected() { diff --git a/tests/fff_print/CMakeLists.txt b/tests/fff_print/CMakeLists.txt index 3247bfda66..59e065ecb8 100644 --- a/tests/fff_print/CMakeLists.txt +++ b/tests/fff_print/CMakeLists.txt @@ -8,6 +8,7 @@ add_executable(${_TEST_NAME}_tests test_extrusion_processor.cpp test_fill.cpp test_flow.cpp + test_gcode_processor.cpp test_gcode_timing.cpp test_gcodewriter.cpp test_model.cpp diff --git a/tests/fff_print/test_gcode_processor.cpp b/tests/fff_print/test_gcode_processor.cpp new file mode 100644 index 0000000000..3998b65547 --- /dev/null +++ b/tests/fff_print/test_gcode_processor.cpp @@ -0,0 +1,46 @@ +#include + +#include "libslic3r/libslic3r.h" +#include "libslic3r/GCode/GCodeProcessor.hpp" +#include "libslic3r/PrintConfig.hpp" + +#include "test_utils.hpp" + +#include +#include + +using namespace Slic3r; +using Catch::Matchers::WithinAbs; + +namespace { + +float processed_belt_tilt(const std::string &gcode) +{ + ScopedTemporaryFile temp(".gcode"); + { + std::ofstream os(temp.string()); + os << gcode; + } + GCodeProcessor proc; + proc.apply_config(FullPrintConfig{}); + proc.process_file(temp.string()); + return proc.get_result().belt_tilt_angle; +} + +constexpr const char *body = "G1 X10 Y10 Z0.2 F3000\nG1 X20 Y10 E1 F1200\n"; + +} // namespace + +TEST_CASE("The config block's belt angle does not mark G-code as belt G-code", "[GCodeProcessor][belt]") +{ + // Every printer's config block lists belt_slice_rotation_angle (default 45), belt or not. + const std::string gcode = std::string("; CONFIG_BLOCK_START\n; belt_printer = 0\n; belt_slice_rotation_angle = 45\n; CONFIG_BLOCK_END\n") + body; + CHECK_THAT(processed_belt_tilt(gcode), WithinAbs(0., 1e-6)); +} + +TEST_CASE("The belt header's angle marks G-code as belt G-code", "[GCodeProcessor][belt]") +{ + const std::string gcode = std::string("; belt_slice_rotation_angle = -45.0\n") + body + + "; CONFIG_BLOCK_START\n; belt_printer = 1\n; belt_slice_rotation_angle = -45\n; CONFIG_BLOCK_END\n"; + CHECK_THAT(processed_belt_tilt(gcode), WithinAbs(45., 1e-6)); +} diff --git a/tests/fff_print/test_gcodewriter.cpp b/tests/fff_print/test_gcodewriter.cpp index bcfd09dd5d..b8f2b890e5 100644 --- a/tests/fff_print/test_gcodewriter.cpp +++ b/tests/fff_print/test_gcodewriter.cpp @@ -19,7 +19,7 @@ #include "libslic3r/GCode/GCodeProcessor.hpp" #include #include -#include "libslic3r/BeltGCodeWriter.hpp" +#include "libslic3r/GCode/BeltKinematics.hpp" #include "libslic3r/BeltTransform.hpp" #include "libslic3r/GCodeReader.hpp" #include "libslic3r/PrintConfig.hpp" @@ -39,9 +39,8 @@ TEST_CASE("Belt machine coordinates retain a non-45-degree slicing angle", "[GCo config.gcode_remap_y.value = RemapAxis::PosZ; config.gcode_remap_z.value = RemapAxis::PosY; - BeltGCodeWriter writer; - writer.set_belt_back_transform(config); - writer.set_machine_frame_transform(config); + GCodeWriter writer; + install_belt_kinematics(writer, config); writer.set_axis_remap(int(config.gcode_remap_x.value), int(config.gcode_remap_y.value), int(config.gcode_remap_z.value)); @@ -52,7 +51,7 @@ TEST_CASE("Belt machine coordinates retain a non-45-degree slicing angle", "[GCo // machine-frame shear/scale are applied. const Vec3d model(4., 10., 3.); Transform3d forward = BeltTransformPipeline::build_forward_transform(config); - const Vec3d machine = writer.to_machine_coords(forward * model); + const Vec3d machine = writer.kinematics().to_machine(forward * model); // The conventional X-tilt remap produces (x, z, y). At 30 degrees the // gantry coordinate is z/sin(30) and belt travel is y + z*cot(30). @@ -908,7 +907,7 @@ TEST_CASE("Custom G-code motion limits are restored before generated moves", "[G // is_current_position_clear(), mirroring the SlopeLift branch. SCENARIO("Belt: the first travel does not lift through the uninitialised origin", "[GCodeWriter][belt]") { - GIVEN("A fresh BeltGCodeWriter configured for an X-tilt 45 degree belt") { + GIVEN("A fresh belt-kinematics GCodeWriter configured for an X-tilt 45 degree belt") { // Machine-frame + slicer->world back-transform config (X tilt, 45 deg). PrintConfig belt_config; belt_config.belt_printer.value = true; @@ -920,9 +919,8 @@ SCENARIO("Belt: the first travel does not lift through the uninitialised origin" belt_config.belt_frame_tilt_decouple.value = false; belt_config.belt_frame_tilt_angle.value = 45.0; - BeltGCodeWriter writer; - writer.set_machine_frame_transform(belt_config); - writer.set_belt_back_transform(belt_config); + GCodeWriter writer; + install_belt_kinematics(writer, belt_config); std::vector extruder_ids { 0 }; writer.set_extruders(extruder_ids); @@ -1037,3 +1035,298 @@ SCENARIO("Belt: start-gcode prepare-stage moves keep their real Z", "[GCode][bel } } } + +// --------------------------------------------------------------------------- +// Regression tests for the two latent bugs the MachineKinematics refactor +// preserved deliberately and the follow-up commit fixed. +// --------------------------------------------------------------------------- + +// Bug 1. _travel_to_z() emits full XYZ whenever the mapping must emit every +// axis, and it builds that point from m_pos. While the position is unknown, +// m_pos.xy is the uninitialised origin, which a reverse remap maps to the far +// corner of the bed. Belt kinematics guarded this; a Cartesian writer with an +// axis remap did not, and would command a rapid across the whole bed. +static void configure_lift_writer(GCodeWriter &writer) +{ + std::vector extruder_ids { 0 }; + writer.set_extruders(extruder_ids); + writer.set_extruder(0); + writer.config.travel_speed.values = { 100.0 }; + writer.config.travel_speed_z.values = { 100.0 }; + writer.config.z_hop.values = { 0.4 }; + writer.config.retract_lift_above.values = { 0.0 }; + writer.config.retract_lift_below.values = { 0.0 }; +} + +// Largest X word in a chunk of emitted G-code, or lowest() if none. +static double max_emitted_x(const std::string &gcode) +{ + double max_x = std::numeric_limits::lowest(); + GCodeReader reader; + reader.parse_buffer(gcode, [&max_x](GCodeReader &, const GCodeReader::GCodeLine &line) { + if (line.cmd_is("G1") && line.has(X)) + max_x = std::max(max_x, double(line.x())); + }); + return max_x; +} + +static size_t count_g1(const std::string &gcode) +{ + size_t n = 0; + GCodeReader reader; + reader.parse_buffer(gcode, [&n](GCodeReader &, const GCodeReader::GCodeLine &line) { + if (line.cmd_is("G1")) ++n; + }); + return n; +} + +SCENARIO("Axis remap: no lift is commanded through the uninitialised origin", "[GCodeWriter][remap]") +{ + // Reverse X: machine X = build_vol_max.x - logical X, so the uninitialised + // origin maps to the far edge of the bed and is unmistakable in the output. + const double bed_x = 250.0; + + GIVEN("a writer with a reverse-X remap and an unknown current position") { + GCodeWriter writer; + configure_lift_writer(writer); + writer.set_axis_remap(6, 1, 2); + writer.set_build_volume_max(Vec3d(bed_x, 250.0, 250.0)); + REQUIRE(writer.kinematics().must_emit_all_axes()); + REQUIRE_FALSE(writer.is_current_position_clear()); + + WHEN("a z-hop is pending and we travel to the first point") { + writer.lazy_lift(LiftType::NormalLift); + const std::string gcode = writer.travel_to_xyz(Vec3d(10.0, 10.0, 5.0)); + + THEN("nothing is commanded at the image of the origin") { + // The destination maps to machine X = 250 - 10 = 240; the bogus + // origin lift would have mapped to machine X = 250. + REQUIRE(max_emitted_x(gcode) < bed_x - 1.0); + } + THEN("only the destination move is emitted") { + REQUIRE(count_g1(gcode) == 1); + } + } + } + + GIVEN("the same writer once its position is known") { + GCodeWriter writer; + configure_lift_writer(writer); + writer.set_axis_remap(6, 1, 2); + writer.set_build_volume_max(Vec3d(bed_x, 250.0, 250.0)); + writer.travel_to_xyz(Vec3d(20.0, 20.0, 5.0)); + REQUIRE(writer.is_current_position_clear()); + + WHEN("a z-hop is pending and we travel again") { + writer.lazy_lift(LiftType::NormalLift); + const std::string gcode = writer.travel_to_xyz(Vec3d(30.0, 30.0, 5.0)); + + THEN("the separate lift move is still emitted") { + // Suppression must be pinned to the unknown position, not to the + // presence of a remap. + REQUIRE(count_g1(gcode) == 2); + } + } + } + + GIVEN("an identity-mapping writer with an unknown position") { + GCodeWriter writer; + configure_lift_writer(writer); + REQUIRE_FALSE(writer.kinematics().must_emit_all_axes()); + REQUIRE_FALSE(writer.is_current_position_clear()); + + WHEN("a z-hop is pending and we travel to the first point") { + writer.lazy_lift(LiftType::NormalLift); + const std::string gcode = writer.travel_to_xyz(Vec3d(10.0, 10.0, 5.0)); + + THEN("behaviour is unchanged: the lift is still emitted") { + // Three moves, not two: with no remap and an unknown position the + // destination is emitted as a separate XY move followed by its own + // Z move, on top of the lift. That split is the pre-existing + // identity-mapping path and must not change. + REQUIRE(count_g1(gcode) == 3); + } + } + } +} + +SCENARIO("Axis remap: eager_lift does not lift, or record a lift, at an unknown position", + "[GCodeWriter][remap]") +{ + GIVEN("a writer with a reverse-X remap and an unknown current position") { + GCodeWriter writer; + configure_lift_writer(writer); + writer.set_axis_remap(6, 1, 2); + writer.set_build_volume_max(Vec3d(250.0, 250.0, 250.0)); + REQUIRE_FALSE(writer.is_current_position_clear()); + + WHEN("an eager lift is requested") { + const std::string lift = writer.eager_lift(LiftType::NormalLift); + + THEN("no move is emitted") { + REQUIRE(lift.empty()); + } + THEN("no lift is recorded, so unlift does not descend from it") { + // If m_lifted had been set while nothing was commanded, unlift() + // would emit a descent from a height the machine never reached. + REQUIRE(writer.unlift().empty()); + } + } + } + + GIVEN("an identity-mapping writer with an unknown position") { + GCodeWriter writer; + configure_lift_writer(writer); + + WHEN("an eager lift is requested") { + const std::string lift = writer.eager_lift(LiftType::NormalLift); + + THEN("behaviour is unchanged: the lift is emitted and can be undone") { + REQUIRE_FALSE(lift.empty()); + REQUIRE_FALSE(writer.unlift().empty()); + } + } + } +} + +// Bug 2. extrude_arc_to_xy() emits G2/G3 with logical X/Y and I/J and never +// consulted the mapping. An arc is only representable when logical X and Y reach +// the machine unchanged -- which is a narrower question than "is the remap the +// identity", because a mapping that only touches Z leaves every emitted word alone. +SCENARIO("Arc support is decided by whether the mapping leaves X and Y alone", "[GCodeWriter][remap]") +{ + GIVEN("a Cartesian writer") { + GCodeWriter writer; + + THEN("the identity mapping supports arcs") { + REQUIRE(writer.kinematics().supports_arc_moves()); + } + THEN("a Z-only negation still supports arcs") { + // (+X, +Y, -Z): non-identity, but X, Y, I and J are all untouched. + writer.set_axis_remap(0, 1, 5); + REQUIRE(writer.kinematics().must_emit_all_axes()); + REQUIRE(writer.kinematics().supports_arc_moves()); + } + THEN("a Z-only reversal still supports arcs") { + writer.set_axis_remap(0, 1, 8); + REQUIRE(writer.kinematics().supports_arc_moves()); + } + THEN("swapping X and Y does not support arcs") { + writer.set_axis_remap(1, 0, 2); + REQUIRE_FALSE(writer.kinematics().supports_arc_moves()); + } + THEN("the X-tilt style (x, z, y) remap does not support arcs") { + writer.set_axis_remap(0, 2, 1); + REQUIRE_FALSE(writer.kinematics().supports_arc_moves()); + } + } + + GIVEN("a belt writer") { + PrintConfig belt_config; + belt_config.belt_printer.value = true; + belt_config.belt_slice_rotation.value = BeltRotationAxis::X; + belt_config.belt_slice_rotation_angle.value = 45.0; + + GCodeWriter writer; + install_belt_kinematics(writer, belt_config); + + THEN("arcs are never supported, because the frame shears") { + REQUIRE_FALSE(writer.kinematics().supports_arc_moves()); + } + } +} + +SCENARIO("An unrepresentable arc degrades to its chord rather than emitting a wrong G2/G3", + "[GCodeWriter][remap]") +{ + auto emitted_commands = [](const std::string &gcode) { + std::vector cmds; + GCodeReader reader; + reader.parse_buffer(gcode, [&cmds](GCodeReader &, const GCodeReader::GCodeLine &line) { + if (! line.cmd().empty()) cmds.emplace_back(line.cmd()); + }); + return cmds; + }; + + GIVEN("an identity-mapping writer") { + GCodeWriter writer; + configure_lift_writer(writer); + + WHEN("an arc is extruded") { + const std::string gcode = writer.extrude_arc_to_xy( + Vec2d(10.0, 0.0), Vec2d(5.0, 0.0), 0.0, /*is_ccw=*/true, "", /*force_no_extrusion=*/true); + + THEN("it is still a G3") { + const auto cmds = emitted_commands(gcode); + REQUIRE(cmds.size() == 1); + REQUIRE(cmds.front() == "G3"); + } + } + } + + GIVEN("a writer whose mapping swaps X and Y") { + GCodeWriter writer; + configure_lift_writer(writer); + writer.set_axis_remap(1, 0, 2); + + WHEN("an arc is extruded") { + const std::string gcode = writer.extrude_arc_to_xy( + Vec2d(10.0, 0.0), Vec2d(5.0, 0.0), 0.0, /*is_ccw=*/true, "", /*force_no_extrusion=*/true); + + THEN("no arc is emitted; it is approximated with linear moves") { + const auto cmds = emitted_commands(gcode); + REQUIRE(! cmds.empty()); + for (const auto &c : cmds) + REQUIRE(c == "G1"); + } + } + } + + // The first version of this test used dE = 0 with force_no_extrusion, which + // hid a real bug: the capability check sat AFTER filament()->extrude(dE), so + // the fallback into extrude_to_xy() advanced E twice. Extrusion accounting has + // to be asserted with a positive dE. + GIVEN("a writer whose mapping cannot express arcs, extruding a real amount") { + GCodeWriter writer; + configure_lift_writer(writer); + writer.set_axis_remap(1, 0, 2); + const double dE = 1.5; + // used_filament() accumulates across moves; E() is reset per line in + // relative-E mode, so it would only show the last segment. + const double used_before = writer.filament()->used_filament(); + + WHEN("an arc carrying that extrusion is emitted") { + const std::string gcode = writer.extrude_arc_to_xy( + Vec2d(10.0, 0.0), Vec2d(5.0, 0.0), dE, /*is_ccw=*/true, "", /*force_no_extrusion=*/false); + + THEN("exactly dE is accounted for, not twice dE") { + REQUIRE_THAT(writer.filament()->used_filament() - used_before, + Catch::Matchers::WithinAbs(dE, 1e-6)); + } + THEN("no G2/G3 survives") { + REQUIRE(gcode.find("G2") == std::string::npos); + REQUIRE(gcode.find("G3") == std::string::npos); + } + } + } + + GIVEN("a writer whose mapping CAN express arcs, extruding a real amount") { + GCodeWriter writer; + configure_lift_writer(writer); + const double dE = 1.5; + // used_filament() accumulates across moves; E() is reset per line in + // relative-E mode, so it would only show the last segment. + const double used_before = writer.filament()->used_filament(); + + WHEN("an arc carrying that extrusion is emitted") { + const std::string gcode = writer.extrude_arc_to_xy( + Vec2d(10.0, 0.0), Vec2d(5.0, 0.0), dE, /*is_ccw=*/true, "", /*force_no_extrusion=*/false); + + THEN("it is still a single arc and accounts for dE once") { + REQUIRE(emitted_commands(gcode).size() == 1); + REQUIRE_THAT(writer.filament()->used_filament() - used_before, + Catch::Matchers::WithinAbs(dE, 1e-6)); + } + } + } +} diff --git a/tests/fff_print/test_print.cpp b/tests/fff_print/test_print.cpp index 78d8e0fd72..e9a98324d9 100644 --- a/tests/fff_print/test_print.cpp +++ b/tests/fff_print/test_print.cpp @@ -20,6 +20,10 @@ #include "test_utils.hpp" #include +#include +#include +#include +#include #include #include @@ -335,6 +339,36 @@ TEST_CASE("Belt purge planning requires its managed purge object", "[Print][Purg CHECK_FALSE(print.has_wipe_tower()); } +// The GUI creates the purge tower object; a project sliced without one (the CLI) must say +// that its filament changes go unpurged. +TEST_CASE("Belt purge tower enabled without a tower object warns", "[Print][PurgeTower][belt]") +{ + DynamicPrintConfig config = multifilament_config(2, { + { "belt_printer", 1 }, + { "enable_belt_purge_tower", 1 }, + { "layer_change_gcode", "G92 E0\n" } + }); + auto purge_warnings = [](Print &print) { + std::vector warnings; + print.validate(&warnings); + return std::count_if(warnings.begin(), warnings.end(), [](const StringObjectException &w) { + return w.opt_key == "enable_belt_purge_tower"; + }); + }; + + Model model; + Print print; + build_cubes(model, print, config, /*n=*/2, /*overlap=*/false); + model.objects[1]->config.set_key_value("extruder", new ConfigOptionInt(2)); + print.apply(model, config); + REQUIRE(print.extruders().size() > 1); + CHECK(purge_warnings(print) == 1); + + model.objects.front()->config.set_key_value("belt_purge_tower_object", new ConfigOptionBool(true)); + print.apply(model, config); + CHECK(purge_warnings(print) == 0); +} + TEST_CASE("Belt purge rejects multiple managed purge objects", "[Print][PurgeTower][Regression]") { DynamicPrintConfig config = multifilament_config(2, { @@ -508,3 +542,200 @@ TEST_CASE("Sequential printing publishes the nozzle group result", "[Print][Mult CHECK(gcode.find("; SEQ-ND-OK") != std::string::npos); } } + +// A scarf joint starts one layer height below the layer and ramps up along the +// wall. On a tilted belt that start is a step backwards along the belt axis, into +// the previous layer's wall at the seam: 0.283 mm per 0.2 mm layer at 45 degrees. +// With an aligned seam the nozzle rams the same spot on every layer (field report +// from a BabyBelt Pro: the belt "jumped backwards" and knocked the part loose). +// Belt printers therefore never get a scarf, whatever the process preset says. +TEST_CASE("Belt printers never start a scarf seam below the layer", "[Print][belt][Seam]") +{ + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({ + { "belt_printer", 1 }, + { "belt_slice_rotation", "x" }, + { "belt_slice_rotation_angle", 45 }, + { "belt_slice_rotation_global", 1 }, + { "gcode_remap_x", "rev_x" }, + { "gcode_remap_y", "pos_z" }, + { "gcode_remap_z", "pos_y" }, + { "layer_height", 0.2 }, + { "initial_layer_print_height", 0.2 }, + { "skirt_loops", 0 }, + { "top_shell_layers", 0 }, + { "bottom_shell_layers", 1 }, + { "wall_loops", 2 }, + { "seam_position", "back" }, + { "seam_slope_type", "external" }, + { "seam_slope_inner_walls", 1 }, + { "seam_slope_start_height", 0 }, + // No z-hop: on a belt a lift is a move along the belt axis (0.4 mm / sin 45 = 0.57 mm) + // and its return would read as a back-step. The shipped belt profiles print without one. + { "z_hop", 0 }, + { "machine_start_gcode", "T[initial_tool]\n" }, + { "layer_change_gcode", "G92 E0\n" }, + }); + const std::string gcode = slice({ cube(20) }, config); + REQUIRE(! gcode.empty()); + + // The belt axis is machine Z. Within a layer it only drifts by the frame + // coupling (well under 0.1 mm across a 20 mm cube); a scarf start is a full + // layer pitch (0.283 mm) backwards. + double last_z = std::numeric_limits::lowest(); + double worst_backstep = 0.; + GCodeReader parser; + parser.parse_buffer(gcode, [&](GCodeReader &, const GCodeReader::GCodeLine &line) { + if (! line.cmd_is("G1") || ! line.has_z()) + return; + const double z = line.z(); + if (last_z != std::numeric_limits::lowest()) + worst_backstep = std::max(worst_backstep, last_z - z); + last_z = z; + }); + CHECK(worst_backstep < 0.2); +} + +// printable_height on a belt printer is the clearance under the gantry, so an object taller +// than that is refused whatever the machine-frame transform does to the emitted coordinates. +TEST_CASE("Belt printers refuse an object taller than the gantry clearance", "[Print][belt]") +{ + auto belt_config = [](double printable_height) { + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({ + { "belt_printer", 1 }, + { "belt_slice_rotation", "x" }, + { "belt_slice_rotation_angle", 45 }, + { "belt_slice_rotation_global", 1 }, + { "gcode_remap_x", "rev_x" }, + { "gcode_remap_y", "pos_z" }, + { "gcode_remap_z", "pos_y" }, + { "printable_height", printable_height }, + { "skirt_loops", 0 }, + { "layer_change_gcode", "G92 E0\n" }, + }); + return config; + }; + + SECTION("a 20 mm cube fits under 50 mm of clearance") { + Print print; + Model model; + init_print({ cube(20) }, print, model, belt_config(50)); + CHECK(print.validate().string.empty()); + } + SECTION("a 60 mm cube does not") { + Print print; + Model model; + init_print({ cube(60) }, print, model, belt_config(50)); + CHECK(print.validate().string.find("height") != std::string::npos); + } +} + +// On a belt every tilted layer starts on the belt, so "the first layers" the fan stays off +// for are a band along the belt, not the first slicing layers. The generator marks where +// each extrusion segment enters and leaves that band and the cooling buffer keeps the fan +// off inside it, on every layer. +TEST_CASE("Belt printers keep the part fan off within the band above the belt", "[Print][belt][Cooling]") +{ + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({ + { "belt_printer", 1 }, + { "belt_slice_rotation", "x" }, + { "belt_slice_rotation_angle", 45 }, + { "belt_slice_rotation_global", 1 }, + { "gcode_remap_x", "rev_x" }, + { "gcode_remap_y", "pos_z" }, + { "gcode_remap_z", "pos_y" }, + { "layer_height", 0.2 }, + { "initial_layer_print_height", 0.2 }, + { "skirt_loops", 0 }, + { "z_hop", 0 }, + // Three layers, 0.6 mm: the lowest wall of each tilted layer is centred about 0.3 mm + // above the belt (half a line width in from the contact edge). + { "close_fan_the_first_x_layers", 3 }, + { "full_fan_speed_layer", 0 }, + { "fan_min_speed", 100 }, + { "fan_max_speed", 100 }, + { "slow_down_layer_time", 1000 }, + { "fan_cooling_layer_time", 1001 }, + { "reduce_fan_stop_start_freq", 0 }, + { "machine_start_gcode", "T[initial_tool]\n" }, + { "layer_change_gcode", "G92 E0\n" }, + }); + const std::string gcode = slice({ cube(20) }, config); + REQUIRE(! gcode.empty()); + + // The markers are consumed by the cooling buffer and never reach the file. + CHECK(gcode.find(";_BELT_BAND") == std::string::npos); + + // With this axis mapping machine Y is the height above the belt along the gantry. Walk + // the moves with the fan state: extrusions that stay within 0.45 mm of the belt are well + // inside the band and must print with the fan off; extrusions that stay 5 mm clear of it + // must print with it on. The first three slicing layers have the fan off altogether. + size_t in_band = 0, in_band_fan_on = 0, clear = 0, clear_fan_off = 0; + int layer = -1; + bool fan_on = false; + double y = 0.; + std::istringstream lines(gcode); + for (std::string line; std::getline(lines, line); ) { + if (boost::starts_with(line, ";LAYER_CHANGE")) { + ++ layer; + } else if (boost::starts_with(line, "M107")) { + fan_on = false; + } else if (boost::starts_with(line, "M106")) { + const size_t s = line.find('S'); + fan_on = s != std::string::npos && std::atof(line.c_str() + s + 1) > 0.; + } else if (boost::starts_with(line, "G1 ")) { + const size_t comment = line.find(';'); + const std::string cmd = line.substr(0, comment); + const size_t ypos = cmd.find(" Y"), epos = cmd.find(" E"); + if (ypos == std::string::npos) + continue; + const double y_new = std::atof(cmd.c_str() + ypos + 2); + const bool extruding = epos != std::string::npos && std::atof(cmd.c_str() + epos + 2) > 0.; + if (extruding && layer >= 3) { + if (std::max(y, y_new) < 0.45) { + ++ in_band; + in_band_fan_on += fan_on; + } else if (std::min(y, y_new) > 5.) { + ++ clear; + clear_fan_off += ! fan_on; + } + } + y = y_new; + } + } + CHECK(in_band > 20); + CHECK(in_band_fan_on == 0); + CHECK(clear > 20); + CHECK(clear_fan_off == 0); +} + +// Organic supports under an overhang on a belt printer reach below the object's first layer, +// where the virtual belt raft layers sit at negative Z. The lowest of them used to get a +// negative height and abort slicing with a negative flow error. +TEST_CASE("Belt printers slice organic tree supports that reach the belt", "[Print][belt][Support]") +{ + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({ + { "belt_printer", 1 }, + { "belt_slice_rotation", "x" }, + { "belt_slice_rotation_angle", 45 }, + { "belt_slice_rotation_global", 1 }, + { "gcode_remap_x", "rev_x" }, + { "gcode_remap_y", "pos_z" }, + { "gcode_remap_z", "pos_y" }, + { "layer_height", 0.2 }, + { "initial_layer_print_height", 0.2 }, + { "skirt_loops", 0 }, + { "z_hop", 0 }, + { "enable_support", 1 }, + { "support_type", "tree(auto)" }, + { "support_style", "organic" }, + { "machine_start_gcode", "T[initial_tool]\n" }, + { "layer_change_gcode", "G92 E0\n" }, + }); + std::string gcode; + REQUIRE_NOTHROW(gcode = slice({ TestMesh::overhang }, config)); + CHECK(! gcode.empty()); +} diff --git a/tests/fff_print/test_skirt_brim.cpp b/tests/fff_print/test_skirt_brim.cpp index a34d28abae..ede355c151 100644 --- a/tests/fff_print/test_skirt_brim.cpp +++ b/tests/fff_print/test_skirt_brim.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include "test_helpers.hpp" // get access to init_print, etc @@ -919,12 +920,36 @@ TEST_CASE("Belt inner-only leading brim does not reject the prime tower or spira CHECK_FALSE(print.objects().front()->has_belt_brim()); CHECK(print.validate().string.empty()); } - SECTION("a real inner brim still rejects the prime tower") { + // enable_prime_tower stays on for any multi-filament project, but a belt printer never + // prints the classic tower, so the setting alone must not cost the print its brim. + SECTION("a real inner brim is accepted with the prime tower setting on") { Print print; Model model; init_inner_leading_with_prime_tower(print, model, 4); CHECK(print.objects().front()->has_belt_brim()); - CHECK_FALSE(print.validate().string.empty()); + CHECK(print.validate().string.empty()); + CHECK_FALSE(gcode(print).empty()); + } + // A purge tower object is accepted too: the purge plan moves every object, apron + // bands included, onto one layer grid. + SECTION("a brim is accepted next to a belt purge tower object") { + DynamicPrintConfig config = belt_brim_multifilament_config(2, { + { "brim_type", "outer_only" }, + { "brim_width", 4 }, + { "brim_object_gap", 0 }, + { "enable_belt_purge_tower", 1 }, + }); + const std::vector> overrides { + { { "extruder", 1 } }, { { "extruder", 2 } }, + }; + Print print; + Model model; + init_print({ cube(20), cube(20) }, print, model, config, &overrides); + model.objects.back()->config.set_key_value("belt_purge_tower_object", new ConfigOptionBool(true)); + print.apply(model, config); + REQUIRE(print.has_belt_purge_tower()); + CHECK(print.validate().string.empty()); + CHECK_FALSE(gcode(print).empty()); } } @@ -1192,3 +1217,60 @@ TEST_CASE("Belt brim coexists with support material", "[SkirtBrim][belt]") REQUIRE(! gc.empty()); CHECK(role_passes(gc, "brim") > 0); } + +// With a 0.3 mm first layer at 45 degrees the brim band on the belt is wider than one bead, +// so its lines go on the nominal lattice instead of at a fixed fraction of the band. A +// lattice line can then land where the belt is almost at the band's print_z; it must be +// moved uphill to the same 0.75 fraction the single-line case uses, not laid scraping the +// belt with its flow clamped to half a layer. +TEST_CASE("Belt brim lattice lines keep their clearance above the belt", "[SkirtBrim][belt]") +{ + DynamicPrintConfig config = belt_brim_config(); + config.set_deserialize_strict({ + { "layer_height", 0.3 }, + { "initial_layer_print_height", 0.3 }, + { "brim_type", "outer_only" }, + { "brim_width", 4 }, + { "brim_object_gap", 0 }, + }); + const std::string gcode = slice({ cube(20) }, config); + + // Heights of the brim extrusions, from the ;HEIGHT: tags inside ;TYPE:Brim sections. + std::vector brim_heights; + bool in_brim = false; + std::istringstream lines(gcode); + for (std::string line; std::getline(lines, line); ) { + if (boost::starts_with(line, ";TYPE:")) + in_brim = boost::starts_with(line, ";TYPE:Brim"); + else if (in_brim && boost::starts_with(line, ";HEIGHT:")) + brim_heights.push_back(std::stod(line.substr(8))); + } + REQUIRE(! brim_heights.empty()); + for (const double h : brim_heights) { + CHECK(h >= 0.75 * 0.3 - 1e-3); + CHECK(h <= 0.3 + 1e-3); + } +} + +// The brim prints in the object's outer wall filament even when every extrusion of the object +// is offered to purging (flush_into_objects): the tool ordering registers the brim filament +// itself, so the writer always knows it. +TEST_CASE("Belt brim slices when every object is a flush target", "[SkirtBrim][belt]") +{ + DynamicPrintConfig config = belt_brim_multifilament_config(2, { + { "brim_type", "outer_only" }, + { "brim_width", 4 }, + { "brim_object_gap", 0 }, + { "flush_into_objects", 1 }, + { "flush_into_infill", 1 }, + }); + const std::vector> overrides { + { { "extruder", 1 } }, { { "extruder", 2 } }, + }; + Print print; + Model model; + init_print({ cube(20), cube(20) }, print, model, config, &overrides); + REQUIRE(print.validate().string.empty()); + const std::string out = gcode(print); + CHECK(out.find(";TYPE:Brim") != std::string::npos); +} diff --git a/tests/libslic3r/test_arrange.cpp b/tests/libslic3r/test_arrange.cpp index 3906cba8ba..1c13180f01 100644 --- a/tests/libslic3r/test_arrange.cpp +++ b/tests/libslic3r/test_arrange.cpp @@ -1,3 +1,4 @@ +#include #include #include "libslic3r/Arrange.hpp" @@ -259,6 +260,75 @@ TEST_CASE("Arrange aligns the pile to a custom center", "[Arrange]") require_no_overlap(items); } +// A belt printer starts its parts at the leading end of the belt (best_object_pos 0.5, 0.05). +// Centring a pile on a point that close to the edge pushed everything longer than the room +// around it off the bed: four 90 mm parts on a 95 x 500 mm belt ended with one across the +// edge and one outside, with 290 mm of belt free behind them. The pile stops at the edge. +TEST_CASE("Arrange keeps a pile aligned near an edge on the bed", "[Arrange]") +{ + const BoundingBox belt = bed(95, 500); + ArrangePolygons items = squares(4, 90.); + ArrangeParams params = quiet_params(scaled(2.)); + params.align_center = Vec2d(0.5, 0.05); + + arrange(items, belt, params); + + coord_t lowest = std::numeric_limits::max(); + for (const ArrangePolygon &ap : items) { + REQUIRE(ap.bed_idx == 0); + const BoundingBox bb = ap.transformed_poly().contour.bounding_box(); + CHECK(belt.contains(bb)); + lowest = std::min(lowest, bb.min.y()); + } + // Snapped to the edge it was aimed at, less the spacing margin, not re-centred. + CHECK(lowest < scaled(10.)); + require_no_overlap(items); +} + +// On a belt the parts print in belt order, so two colours that alternate along the +// belt, or sit side by side, cost a filament change on every shared layer. Arrange +// keeps each colour together: no part shares belt length with a part of another +// colour, counting the tilted layers that run cot(angle) * height past its far edge, +// whichever end of the belt prints first. +TEST_CASE("Arrange groups the colours of a belt print along the belt", "[Arrange][belt]") +{ + const bool reversed = GENERATE(false, true); + CAPTURE(reversed); + const BoundingBox belt = bed(95, 500); + ArrangePolygons items = squares(6, 30., 20.); + for (size_t i = 0; i < items.size(); ++i) + items[i].extrude_ids = { int(i % 3) + 1 }; // three colours, two parts each + ArrangeParams params = quiet_params(scaled(2.)); + params.align_center = Vec2d(0.5, 0.05); + params.is_belt = true; + params.belt_axis = 1; + params.belt_reversed = reversed; + params.belt_tilt_slope = 1.f; // 45 degrees + + arrange(items, belt, params); + require_no_overlap(items); + + // Belt position in print order, so the same check serves both directions. + const coord_t dir = reversed ? -1 : 1; + auto start = [&](const ArrangePolygon &ap) { const BoundingBox bb = ap.transformed_poly().contour.bounding_box(); return dir * (reversed ? bb.max.y() : bb.min.y()); }; + auto end = [&](const ArrangePolygon &ap) { const BoundingBox bb = ap.transformed_poly().contour.bounding_box(); return dir * (reversed ? bb.min.y() : bb.max.y()) + scaled(ap.height * params.belt_tilt_slope); }; + + for (const ArrangePolygon &ap : items) { + REQUIRE(ap.bed_idx == 0); + CHECK(belt.contains(ap.transformed_poly().contour.bounding_box())); + } + for (const ArrangePolygon &a : items) + for (const ArrangePolygon &b : items) { + if (a.extrude_ids == b.extrude_ids) + continue; + // The part printed later starts after the earlier one has finished. + const coord_t earlier_end = start(a) <= start(b) ? end(a) : end(b); + const coord_t later_start = std::max(start(a), start(b)); + INFO("colour " << a.extrude_ids.front() << " vs " << b.extrude_ids.front()); + CHECK(earlier_end <= later_start); + } +} + TEST_CASE("Sequential print floors the object distance by object height", "[Arrange]") { // The only place sequential-print clearance is enforced. The arrange menu offers diff --git a/tests/libslic3r/test_filament_mixer.cpp b/tests/libslic3r/test_filament_mixer.cpp index ade0c910dc..31bcb09526 100644 --- a/tests/libslic3r/test_filament_mixer.cpp +++ b/tests/libslic3r/test_filament_mixer.cpp @@ -57,6 +57,50 @@ TEST_CASE("expand_mixed_filaments replaces mixed slots with their components", " } } +TEST_CASE("belt purge tower island count ignores virtual mixed slots", "[FilamentMixer][belt]") +{ + // Regression for the "extra purge tower" on a belt printer with a mixed + // filament (MCTEST5). The belt purge prism is sized as + // n_islands = used_filaments.size() - 1 + // and GUI::ensure_belt_purge_tower() collected those filaments straight off + // the model objects' extruder assignments. A mixed slot is VIRTUAL -- no + // nozzle carries it, and ToolOrdering::resolve_mixed_filaments() replaces it + // with its components before any G-code is emitted -- so counting it as a + // filament of its own provisions one island that can never be reached. + // + // MCTEST5: five cubes on extruders 1..5, where filament 5 is a 50/50 blend of + // filaments 2 and 4. The G-code uses only T0..T3 and reports + // "filament used [g] = 53.35, 141.11, 40.84, 107.23, 0.00" -- filament 5 + // consumes nothing, exactly as a virtual slot should. + const std::vector is_mixed = {0, 0, 0, 0, 1}; + const std::vector comp_strs = {"", "", "", "", "2,4"}; + + // The set the sizer used to see: slots 0..4 (filaments 1..5). + const std::vector assigned = {0, 1, 2, 3, 4}; + const auto physical = expand_mixed_filaments(assigned, is_mixed, comp_strs); + + // Slot 4 dissolves into 1 and 3, which are already present. + REQUIRE(physical == std::vector({0, 1, 2, 3})); + + // Four physical filaments => three transitions => three islands, not four. + REQUIRE(int(physical.size()) - 1 == 3); + REQUIRE(int(assigned.size()) - 1 == 4); // what it produced before the fix + + SECTION("A mixed slot whose components are otherwise unused still counts them") { + // Only the mixed slot is assigned: it must still yield its two components, + // i.e. one island, rather than collapsing to zero. + const auto only_mixed = expand_mixed_filaments({4}, is_mixed, comp_strs); + REQUIRE(only_mixed == std::vector({1, 3})); + REQUIRE(int(only_mixed.size()) - 1 == 1); + } + + SECTION("No mixed filaments anywhere leaves the set untouched") { + const std::vector none_mixed = {0, 0, 0, 0, 0}; + REQUIRE_FALSE(has_any_mixed_filament(none_mixed)); + REQUIRE(expand_mixed_filaments(assigned, none_mixed, {"", "", "", "", ""}) == assigned); + } +} + TEST_CASE("check_mixed_filament_integrity flags dangling component references", "[FilamentMixer]") { const std::vector is_mixed = {0, 0, 1};