diff --git a/CLAUDE.md b/CLAUDE.md index 25aa516a56..9bd162c42c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -23,27 +23,7 @@ cmake --build build/arm64 --config RelWithDebInfo --target all -- ### Building on Linux **Always use this command to build the project when testing build issues on Linux.** ```bash -cmake --build build/arm64 --config RelWithDebInfo --target all -- - -``` -### Build test: - -**Always use this command to build the project when testing build issues on Windows.** -```bash -cmake --build . --config %build_type% --target ALL_BUILD -- -m -``` - -### Building on macOS -**Always use this command to build the project when testing build issues on macOS.** -```bash -cmake --build build/arm64 --config RelWithDebInfo --target all -- -``` - -### Building on Linux - **Always use this command to build the project when testing build issues on Linux.** -```bash -cmake --build build --config RelWithDebInfo --target all -- - +systemd-run --user --scope -p MemoryMax=48G cmake --build build --config RelWithDebInfo --target all -- -j18 -l 24 ``` diff --git a/src/libslic3r/BuildVolume.hpp b/src/libslic3r/BuildVolume.hpp index 0494efe9ba..2b0d8c2746 100644 --- a/src/libslic3r/BuildVolume.hpp +++ b/src/libslic3r/BuildVolume.hpp @@ -84,7 +84,7 @@ public: indexed_triangle_set bounding_mesh(bool scale=true) const; // Center of the print bed, unscaled. - Vec2d bed_center() const { return to_2d(m_bboxf.center()); } + Vec2d bed_center() const { return get_extents(m_bed_shape).center(); } // Convex hull of polygon(), scaled. const Polygon& convex_hull() const { return m_convex_hull; } // Smallest enclosing circle of polygon(), scaled. diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 02a233e988..41cfa7b72a 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -1745,8 +1745,16 @@ std::vector GCode::collect_layers_to_print(const PrintObjec // Check that there are extrusions on the very first layer. The case with empty // first layer may result in skirt/brim in the air and maybe other issues. + // Skip this check for belt printers. The shear transform tilts the + // model so the first horizontal layer plane intersects only a thin + // sliver of the model (width ≈ first_layer_height / shear_factor). + // This sliver is often narrower than the nozzle diameter, producing + // zero perimeters and an empty first layer — which is expected, not + // an error. In global shear mode the object may also start above + // Z=0 on the tilted belt surface. if (layers_to_print.size() == 1u) { - if (!has_extrusions) + bool skip_empty_check = object.print()->config().belt_printer.value; + if (!has_extrusions && !skip_empty_check) throw Slic3r::SlicingError(_(L("One object has an empty first layer and can't be printed. Please Cut the bottom or enable supports.")), object.id().id); } @@ -2515,9 +2523,27 @@ 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 in header for G-code processor detection. + // Belt printer: embed angle and transform configs in header for G-code processor detection. if (print.config().belt_printer.value) { file.write_format("; belt_printer_angle = %.1f\n", print.config().belt_printer_angle.value); + // Shear configs + const auto &full_cfg = print.full_print_config(); + file.write_format("; belt_shear_x = %s\n", full_cfg.opt_serialize("belt_shear_x").c_str()); + file.write_format("; belt_shear_x_angle = %.1f\n", print.config().belt_shear_x_angle.value); + file.write_format("; belt_shear_x_from = %s\n", full_cfg.opt_serialize("belt_shear_x_from").c_str()); + file.write_format("; belt_shear_y = %s\n", full_cfg.opt_serialize("belt_shear_y").c_str()); + file.write_format("; belt_shear_y_angle = %.1f\n", print.config().belt_shear_y_angle.value); + file.write_format("; belt_shear_y_from = %s\n", full_cfg.opt_serialize("belt_shear_y_from").c_str()); + file.write_format("; belt_shear_z = %s\n", full_cfg.opt_serialize("belt_shear_z").c_str()); + file.write_format("; belt_shear_z_angle = %.1f\n", print.config().belt_shear_z_angle.value); + file.write_format("; belt_shear_z_from = %s\n", full_cfg.opt_serialize("belt_shear_z_from").c_str()); + // Scale configs + file.write_format("; belt_scale_x = %s\n", full_cfg.opt_serialize("belt_scale_x").c_str()); + file.write_format("; belt_scale_x_angle = %.1f\n", print.config().belt_scale_x_angle.value); + file.write_format("; belt_scale_y = %s\n", full_cfg.opt_serialize("belt_scale_y").c_str()); + file.write_format("; belt_scale_y_angle = %.1f\n", print.config().belt_scale_y_angle.value); + file.write_format("; belt_scale_z = %s\n", full_cfg.opt_serialize("belt_scale_z").c_str()); + file.write_format("; belt_scale_z_angle = %.1f\n", print.config().belt_scale_z_angle.value); } if (is_bbl_printers) file.write_format(";%s\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Estimated_Printing_Time_Placeholder).c_str()); diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index 7ac3a1ffc4..8def7c08db 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -3051,6 +3051,84 @@ void GCodeProcessor::process_tags(const std::string_view comment, bool producers } catch (...) {} return; } + // Belt printer: parse shear configs from header comments. + { + auto parse_shear_mode = [](const std::string &s) -> BeltShearMode { + if (s == "pos_cot") return BeltShearMode::PosCot; + if (s == "neg_cot") return BeltShearMode::NegCot; + if (s == "pos_tan") return BeltShearMode::PosTan; + if (s == "neg_tan") return BeltShearMode::NegTan; + return BeltShearMode::None; + }; + auto parse_axis = [](const std::string &s) -> BeltAxis { + if (s == "y") return BeltAxis::Y; + if (s == "z") return BeltAxis::Z; + return BeltAxis::X; + }; + auto parse_scale_mode = [](const std::string &s) -> BeltScaleMode { + if (s == "inv_sin") return BeltScaleMode::InvSin; + if (s == "inv_cos") return BeltScaleMode::InvCos; + if (s == "sin") return BeltScaleMode::Sin; + if (s == "cos") return BeltScaleMode::Cos; + return BeltScaleMode::None; + }; + auto trim = [](const std::string &s) -> std::string { + size_t start = s.find_first_not_of(" \t\r\n"); + size_t end = s.find_last_not_of(" \t\r\n"); + return (start == std::string::npos) ? "" : s.substr(start, end - start + 1); + }; + // Shear X + if (boost::starts_with(comment, " belt_shear_x = ")) { + m_result.belt_shear_x = parse_shear_mode(trim(std::string(comment.substr(16)))); return; + } + if (boost::starts_with(comment, " belt_shear_x_angle = ")) { + try { m_result.belt_shear_x_angle = std::stof(std::string(comment.substr(22))); } catch (...) {} return; + } + if (boost::starts_with(comment, " belt_shear_x_from = ")) { + m_result.belt_shear_x_from = parse_axis(trim(std::string(comment.substr(21)))); return; + } + // Shear Y + if (boost::starts_with(comment, " belt_shear_y = ")) { + m_result.belt_shear_y = parse_shear_mode(trim(std::string(comment.substr(16)))); return; + } + if (boost::starts_with(comment, " belt_shear_y_angle = ")) { + try { m_result.belt_shear_y_angle = std::stof(std::string(comment.substr(22))); } catch (...) {} return; + } + if (boost::starts_with(comment, " belt_shear_y_from = ")) { + m_result.belt_shear_y_from = parse_axis(trim(std::string(comment.substr(21)))); return; + } + // Shear Z + if (boost::starts_with(comment, " belt_shear_z = ")) { + m_result.belt_shear_z = parse_shear_mode(trim(std::string(comment.substr(16)))); return; + } + if (boost::starts_with(comment, " belt_shear_z_angle = ")) { + try { m_result.belt_shear_z_angle = std::stof(std::string(comment.substr(22))); } catch (...) {} return; + } + if (boost::starts_with(comment, " belt_shear_z_from = ")) { + m_result.belt_shear_z_from = parse_axis(trim(std::string(comment.substr(21)))); return; + } + // Scale X + if (boost::starts_with(comment, " belt_scale_x = ")) { + m_result.belt_scale_x = parse_scale_mode(trim(std::string(comment.substr(16)))); return; + } + if (boost::starts_with(comment, " belt_scale_x_angle = ")) { + try { m_result.belt_scale_x_angle = std::stof(std::string(comment.substr(22))); } catch (...) {} return; + } + // Scale Y + if (boost::starts_with(comment, " belt_scale_y = ")) { + m_result.belt_scale_y = parse_scale_mode(trim(std::string(comment.substr(16)))); return; + } + if (boost::starts_with(comment, " belt_scale_y_angle = ")) { + try { m_result.belt_scale_y_angle = std::stof(std::string(comment.substr(22))); } catch (...) {} return; + } + // Scale Z + if (boost::starts_with(comment, " belt_scale_z = ")) { + m_result.belt_scale_z = parse_scale_mode(trim(std::string(comment.substr(16)))); return; + } + if (boost::starts_with(comment, " belt_scale_z_angle = ")) { + try { m_result.belt_scale_z_angle = std::stof(std::string(comment.substr(22))); } catch (...) {} return; + } + } // wipe start tag if (boost::starts_with(comment, reserved_tag(ETags::Wipe_Start))) { m_wiping = true; diff --git a/src/libslic3r/GCode/GCodeProcessor.hpp b/src/libslic3r/GCode/GCodeProcessor.hpp index e432a165a8..f953345f4e 100644 --- a/src/libslic3r/GCode/GCodeProcessor.hpp +++ b/src/libslic3r/GCode/GCodeProcessor.hpp @@ -229,6 +229,23 @@ class Print; float z_offset; // Belt printer: angle for coordinate transformation in preview. float belt_printer_angle{ 0.f }; + // Belt printer: per-axis shear config. + BeltShearMode belt_shear_x{ BeltShearMode::None }; + float belt_shear_x_angle{ 45.f }; + BeltAxis belt_shear_x_from{ BeltAxis::Y }; + BeltShearMode belt_shear_y{ BeltShearMode::None }; + float belt_shear_y_angle{ 45.f }; + BeltAxis belt_shear_y_from{ BeltAxis::Y }; + BeltShearMode belt_shear_z{ BeltShearMode::None }; + float belt_shear_z_angle{ 45.f }; + BeltAxis belt_shear_z_from{ BeltAxis::Y }; + // Belt printer: per-axis scale config. + BeltScaleMode belt_scale_x{ BeltScaleMode::None }; + float belt_scale_x_angle{ 45.f }; + BeltScaleMode belt_scale_y{ BeltScaleMode::None }; + float belt_scale_y_angle{ 45.f }; + BeltScaleMode belt_scale_z{ BeltScaleMode::None }; + float belt_scale_z_angle{ 45.f }; SettingsIds settings_ids; size_t filaments_count; bool backtrace_enabled; @@ -288,6 +305,22 @@ class Print; layer_filaments = other.layer_filaments; filament_change_count_map = other.filament_change_count_map; initial_layer_time = other.initial_layer_time; + belt_printer_angle = other.belt_printer_angle; + belt_shear_x = other.belt_shear_x; + belt_shear_x_angle = other.belt_shear_x_angle; + belt_shear_x_from = other.belt_shear_x_from; + belt_shear_y = other.belt_shear_y; + belt_shear_y_angle = other.belt_shear_y_angle; + belt_shear_y_from = other.belt_shear_y_from; + belt_shear_z = other.belt_shear_z; + belt_shear_z_angle = other.belt_shear_z_angle; + belt_shear_z_from = other.belt_shear_z_from; + belt_scale_x = other.belt_scale_x; + belt_scale_x_angle = other.belt_scale_x_angle; + belt_scale_y = other.belt_scale_y; + belt_scale_y_angle = other.belt_scale_y_angle; + belt_scale_z = other.belt_scale_z; + belt_scale_z_angle = other.belt_scale_z_angle; #if ENABLE_GCODE_VIEWER_STATISTICS time = other.time; #endif diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 9bc5e0a782..9437b6098e 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -1010,11 +1010,13 @@ static std::vector s_Preset_machine_limits_options { static std::vector s_Preset_printer_options { "printer_technology", - "printable_area", "extruder_printable_area", "bed_exclude_area","bed_custom_texture", "bed_custom_model", "build_plate_tilt_x", "build_plate_tilt_y", "belt_printer", "belt_printer_angle", "belt_printer_infinite_y", "belt_shear_x", "belt_shear_x_angle", "belt_shear_x_from", - "belt_shear_y", "belt_shear_y_angle", "belt_shear_y_from", - "belt_shear_z", "belt_shear_z_angle", "belt_shear_z_from", + "printable_area", "extruder_printable_area", "bed_exclude_area","bed_custom_texture", "bed_custom_model", "build_plate_tilt_x", "build_plate_tilt_y", "belt_printer", "belt_printer_angle", "belt_printer_infinite_y", "belt_shear_x", "belt_shear_x_angle", "belt_shear_x_from", "belt_shear_x_global", + "belt_shear_y", "belt_shear_y_angle", "belt_shear_y_from", "belt_shear_y_global", + "belt_shear_z", "belt_shear_z_angle", "belt_shear_z_from", "belt_shear_z_global", "belt_scale_x", "belt_scale_x_angle", "belt_scale_y", "belt_scale_y_angle", "belt_scale_z", "belt_scale_z_angle", - "belt_gcode_remap_x", "belt_gcode_remap_y", "belt_gcode_remap_z", "gcode_flavor", + "belt_gcode_remap_x", "belt_gcode_remap_y", "belt_gcode_remap_z", + "belt_support_floor_offset", "belt_support_floor_mode", "belt_support_z_offset_mode", + "gcode_flavor", "fan_kickstart", "fan_speedup_time", "fan_speedup_overhangs", "single_extruder_multi_material", "manual_filament_change", "file_start_gcode", "machine_start_gcode", "machine_end_gcode", "before_layer_change_gcode", "printing_by_object_gcode", "layer_change_gcode", "time_lapse_gcode", "wrapping_detection_gcode", "change_filament_gcode", "change_extrusion_role_gcode", "printer_model", "printer_variant", "printer_extruder_id", "printer_extruder_variant", "extruder_variant_list", "default_nozzle_volume_type", diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index f245cc32b6..0aef12a74b 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -99,6 +99,10 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n // Cache the plenty of parameters, which influence the G-code generator only, // or they are only notes not influencing the generated G-code. static std::unordered_set steps_gcode = { + // Belt printer G-code axis remap (only affects G-code output, not slicing). + "belt_gcode_remap_x", + "belt_gcode_remap_y", + "belt_gcode_remap_z", //BBS "additional_cooling_fan_speed", "reduce_crossing_wall", @@ -275,8 +279,34 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n // Spiral Vase forces different kind of slicing than the normal model: // In Spiral Vase mode, holes are closed and only the largest area contour is kept at each layer. // Therefore toggling the Spiral Vase on / off requires complete reslicing. - || opt_key == "spiral_mode") { + || opt_key == "spiral_mode" + // Belt printer transform options change the mesh geometry before slicing. + || opt_key == "belt_printer" + || opt_key == "belt_printer_angle" + || opt_key == "belt_shear_x" + || opt_key == "belt_shear_x_angle" + || opt_key == "belt_shear_x_from" + || opt_key == "belt_shear_x_global" + || opt_key == "belt_shear_y" + || opt_key == "belt_shear_y_angle" + || opt_key == "belt_shear_y_from" + || opt_key == "belt_shear_y_global" + || opt_key == "belt_shear_z" + || opt_key == "belt_shear_z_angle" + || opt_key == "belt_shear_z_from" + || opt_key == "belt_shear_z_global" + || opt_key == "belt_scale_x" + || opt_key == "belt_scale_x_angle" + || opt_key == "belt_scale_y" + || opt_key == "belt_scale_y_angle" + || opt_key == "belt_scale_z" + || opt_key == "belt_scale_z_angle") { osteps.emplace_back(posSlice); + } else if ( + opt_key == "belt_support_floor_offset" + || opt_key == "belt_support_floor_mode" + || opt_key == "belt_support_z_offset_mode") { + osteps.emplace_back(posSupportMaterial); } else if ( opt_key == "print_sequence" || opt_key == "filament_type" @@ -2124,15 +2154,21 @@ void Print::process(long long *time_cost_with_cache, bool use_cache) int object_count = m_objects.size(); std::set need_slicing_objects; std::set re_slicing_objects; + // Belt global Z shear: each object needs unique layer Z values based on + // its bed position, so sharing layers between "identical" objects is wrong. + bool belt_no_share = m_config.belt_printer.value && m_config.belt_shear_z_global.value + && m_config.belt_shear_z.value != BeltShearMode::None; if (!use_cache) { for (int index = 0; index < object_count; index++) { PrintObject *obj = m_objects[index]; - for (PrintObject *slicing_obj : need_slicing_objects) - { - if (is_print_object_the_same(obj, slicing_obj)) { - obj->set_shared_object(slicing_obj); - break; + if (!belt_no_share) { + for (PrintObject *slicing_obj : need_slicing_objects) + { + if (is_print_object_the_same(obj, slicing_obj)) { + obj->set_shared_object(slicing_obj); + break; + } } } if (!obj->get_shared_object()) @@ -2151,12 +2187,14 @@ void Print::process(long long *time_cost_with_cache, bool use_cache) PrintObject *obj = m_objects[index]; bool found_shared = false; if (need_slicing_objects.find(obj) == need_slicing_objects.end()) { - for (PrintObject *slicing_obj : need_slicing_objects) - { - if (is_print_object_the_same(obj, slicing_obj)) { - obj->set_shared_object(slicing_obj); - found_shared = true; - break; + if (!belt_no_share) { + for (PrintObject *slicing_obj : need_slicing_objects) + { + if (is_print_object_the_same(obj, slicing_obj)) { + obj->set_shared_object(slicing_obj); + found_shared = true; + break; + } } } if (!found_shared) { diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index b98811c8e3..d0c050f442 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -573,7 +573,15 @@ private: PrintObject* m_shared_object{ nullptr }; - + // Belt printer: global Z offset applied to this object's layers for shear positioning. + double m_belt_global_z_offset { 0.0 }; + // Belt printer: min_z of mesh after belt shear (before Z-shift), for z_offset calc. + double m_belt_min_z { 0.0 }; +public: + double belt_global_z_offset() const { return m_belt_global_z_offset; } +private: + + // SoftFever // // object id diff --git a/src/libslic3r/PrintApply.cpp b/src/libslic3r/PrintApply.cpp index d2a44463ea..7f3b41b0f1 100644 --- a/src/libslic3r/PrintApply.cpp +++ b/src/libslic3r/PrintApply.cpp @@ -135,22 +135,29 @@ struct PrintObjectTrafoAndInstances // Generate a list of trafos and XY offsets for instances of a ModelObject // Orca: Updated to include XYZ filament shrinkage compensation -static std::vector print_objects_from_model_object(const ModelObject &model_object, const Vec3d &shrinkage_compensation) +static std::vector print_objects_from_model_object(const ModelObject &model_object, const Vec3d &shrinkage_compensation, bool force_separate_instances = false) { std::set trafos; PrintObjectTrafoAndInstances trafo; //BBS: add useful logs for debug int index = 0; + int unique_counter = 0; for (ModelInstance *model_instance : model_object.instances) { if (model_instance->is_printable()) { // Orca: Updated with XYZ filament shrinkage compensation Geometry::Transformation model_instance_transformation = model_instance->get_transformation(); trafo.trafo = model_instance_transformation.get_matrix_with_applied_shrinkage_compensation(shrinkage_compensation); - + auto shift = Point::new_scale(trafo.trafo.data()[12], trafo.trafo.data()[13]); // Reset the XY axes of the transformation. trafo.trafo.data()[12] = 0; trafo.trafo.data()[13] = 0; + // Belt printer global mode: prevent instance grouping so each + // copy gets its own PrintObject with independent layer Z values. + // Add a tiny unique perturbation to the existing Z (don't replace + // it — the Z translation from ensure_on_bed must be preserved). + if (force_separate_instances) + trafo.trafo.data()[14] += 1e-10 * (++unique_counter); // Search or insert a trafo. auto it = trafos.emplace(trafo).first; const_cast(*it).instances.emplace_back(PrintInstance{ nullptr, model_instance, shift }); @@ -1506,11 +1513,16 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ PrintObjectPtrs print_objects_new; print_objects_new.reserve(std::max(m_objects.size(), m_model.objects.size())); bool new_objects = false; + bool belt_instances_shifted = false; // Walk over all new model objects and check, whether there are matching PrintObjects. for (ModelObject *model_object : m_model.objects) { ModelObjectStatus &model_object_status = const_cast(model_object_status_db.reuse(*model_object)); // Orca: Updated for XYZ filament shrink compensation - model_object_status.print_instances = print_objects_from_model_object(*model_object, this->shrinkage_compensation()); + // Belt global mode: force each instance into its own PrintObject + // so each gets independent layer Z values. + bool belt_force_separate = m_config.belt_printer.value && m_config.belt_shear_z_global.value + && m_config.belt_shear_z.value != BeltShearMode::None; + model_object_status.print_instances = print_objects_from_model_object(*model_object, this->shrinkage_compensation(), belt_force_separate); std::vector old; old.reserve(print_object_status_db.count(*model_object)); for (const PrintObjectStatus &print_object_status : print_object_status_db.get_range(*model_object)) @@ -1558,6 +1570,7 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ if (status != PrintBase::APPLY_STATUS_UNCHANGED) { size_t extruder_num = new_full_config.option("nozzle_diameter")->size(); update_apply_status(status == PrintBase::APPLY_STATUS_INVALIDATED); + belt_instances_shifted = true; } print_objects_new.emplace_back((*it_old)->print_object); const_cast(*it_old)->status = PrintObjectStatus::Reused; @@ -1593,6 +1606,17 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ update_apply_status(object->invalidate_step(posSlice)); } } + + // Belt printer global mode: when any object's instances shifted, + // recompute m_belt_global_z_offset for ALL objects (it depends on + // min_shift across all objects, so one move affects everyone). + if (belt_instances_shifted + && m_config.belt_printer.value + && m_config.belt_shear_z_global.value + && m_config.belt_shear_z.value != BeltShearMode::None) { + for (PrintObject *object : m_objects) + update_apply_status(object->invalidate_step(posSlice)); + } } //BBS: check the config again diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index a722a3bae7..7d06f96a85 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -327,6 +327,21 @@ static t_config_enum_values s_keys_map_BeltRemapAxis { }; CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(BeltRemapAxis) +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) + +static t_config_enum_values s_keys_map_BeltSupportZOffsetMode { + { "none", int(BeltSupportZOffsetMode::None) }, + { "unconditional", int(BeltSupportZOffsetMode::Unconditional) }, + { "raft_only", int(BeltSupportZOffsetMode::RaftOnly) }, +}; +CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(BeltSupportZOffsetMode) + static t_config_enum_values s_keys_map_SupportMaterialPattern { { "rectilinear", smpRectilinear }, { "rectilinear-grid", smpRectilinearGrid }, @@ -6047,17 +6062,29 @@ void PrintConfigDef::init_fff_params() def->set_default_value(new ConfigOptionEnum(default_axis)); }; + auto add_belt_shear_global = [this](const char *key, const char *label) { + auto def = this->add(key, coBool); + def->label = L(label); + def->category = L("Printable space"); + def->tooltip = L("Apply shear in global coordinates (position-aware) rather than object-local coordinates."); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionBool(false)); + }; + add_belt_shear_mode("belt_shear_x", "Function", BeltShearMode::None); add_belt_shear_angle("belt_shear_x_angle", "Angle"); add_belt_axis_enum("belt_shear_x_from", "From", "Source axis for X shear.", BeltAxis::Z); + add_belt_shear_global("belt_shear_x_global", "Global"); add_belt_shear_mode("belt_shear_y", "Function", BeltShearMode::PosCot); add_belt_shear_angle("belt_shear_y_angle", "Angle"); add_belt_axis_enum("belt_shear_y_from", "From", "Source axis for Y shear.", BeltAxis::Z); + add_belt_shear_global("belt_shear_y_global", "Global"); add_belt_shear_mode("belt_shear_z", "Function", BeltShearMode::None); add_belt_shear_angle("belt_shear_z_angle", "Angle"); add_belt_axis_enum("belt_shear_z_from", "From", "Source axis for Z shear.", BeltAxis::Y); + add_belt_shear_global("belt_shear_z_global", "Global"); // Per-axis scale controls for belt printer auto add_belt_scale_mode = [this](const char *key, const char *label, BeltScaleMode default_mode) { @@ -6109,6 +6136,43 @@ void PrintConfigDef::init_fff_params() add_belt_remap("belt_gcode_remap_y", "Y", "Which slicing axis maps to machine Y in G-code output.", BeltRemapAxis::PosY); add_belt_remap("belt_gcode_remap_z", "Z", "Which slicing axis maps to machine Z in G-code output.", BeltRemapAxis::PosZ); + // Belt support floor debug controls + def = this->add("belt_support_floor_offset", coFloat); + def->label = L("Floor Z offset"); + def->category = L("Printable space"); + def->tooltip = L("Shifts the computed belt floor up or down (mm). Negative values lower the floor, allowing more supports to survive. Use this to diagnose belt floor formula issues."); + def->sidetext = L("mm"); + def->min = -500; + def->max = 500; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(0)); + + { + auto def = this->add("belt_support_floor_mode", coEnum); + def->label = L("Floor mode"); + def->category = L("Printable space"); + def->tooltip = L("Controls belt floor awareness for supports. 'None' disables belt floor logic. " + "'Generator only' stops support generation at the belt floor plane."); + def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); + def->enum_values = {"none", "generator_only"}; + def->enum_labels = {L("None"), L("Generator only")}; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionEnum(BeltSupportFloorMode::GeneratorOnly)); + } + + { + auto def = this->add("belt_support_z_offset_mode", coEnum); + def->label = L("Z offset mode"); + def->category = L("Printable space"); + def->tooltip = L("How global Z offset is applied to support layers for belt printers with global shear. " + "'None' = don't offset. 'Unconditional' = offset all layers. 'Raft only' = only offset raft layers."); + def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); + def->enum_values = {"none", "unconditional", "raft_only"}; + def->enum_labels = {L("None"), L("Unconditional"), L("Raft only")}; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionEnum(BeltSupportZOffsetMode::Unconditional)); + } + def = this->add("tree_support_branch_angle", coFloat); def->label = L("Tree support branch angle"); def->category = L("Support"); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 4f149d9c8b..fc533bf6bb 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -189,6 +189,21 @@ enum class BeltRemapAxis RevX = 6, RevY = 7, RevZ = 8, // Reversed: max - pos }; +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 +{ + None, // Don't apply global_z_offset to support layers + Unconditional, // Apply to all support layers + RaftOnly, // Only apply to raft layers +}; + enum SupportMaterialPattern { smpDefault, smpRectilinear, smpRectilinearGrid, smpHoneycomb, @@ -536,6 +551,8 @@ CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltShearMode) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltScaleMode) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltAxis) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltRemapAxis) +CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltSupportFloorMode) +CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltSupportZOffsetMode) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SupportMaterialPattern) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SupportMaterialStyle) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SupportMaterialInterfacePattern) @@ -1456,12 +1473,15 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE( ((ConfigOptionEnum, belt_shear_x)) ((ConfigOptionFloat, belt_shear_x_angle)) ((ConfigOptionEnum, belt_shear_x_from)) + ((ConfigOptionBool, belt_shear_x_global)) ((ConfigOptionEnum, belt_shear_y)) ((ConfigOptionFloat, belt_shear_y_angle)) ((ConfigOptionEnum, belt_shear_y_from)) + ((ConfigOptionBool, belt_shear_y_global)) ((ConfigOptionEnum, belt_shear_z)) ((ConfigOptionFloat, belt_shear_z_angle)) ((ConfigOptionEnum, belt_shear_z_from)) + ((ConfigOptionBool, belt_shear_z_global)) ((ConfigOptionEnum, belt_scale_x)) ((ConfigOptionFloat, belt_scale_x_angle)) ((ConfigOptionEnum, belt_scale_y)) @@ -1471,6 +1491,9 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE( ((ConfigOptionEnum, belt_gcode_remap_x)) ((ConfigOptionEnum, belt_gcode_remap_y)) ((ConfigOptionEnum, belt_gcode_remap_z)) + ((ConfigOptionFloat, belt_support_floor_offset)) + ((ConfigOptionEnum, belt_support_floor_mode)) + ((ConfigOptionEnum, belt_support_z_offset_mode)) //BBS ((ConfigOptionInts, additional_cooling_fan_speed)) ((ConfigOptionBool, reduce_crossing_wall)) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index abd309fbff..f803cc0369 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -9,6 +9,7 @@ #include "MutablePolygon.hpp" #include "PrintConfig.hpp" #include "Support/SupportMaterial.hpp" +#include "Support/SupportCommon.hpp" #include "Support/SupportSpotsGenerator.hpp" #include "Support/TreeSupport.hpp" #include "Surface.hpp" @@ -1365,7 +1366,12 @@ bool PrintObject::invalidate_step(PrintObjectStep step) } else if (step == posSupportMaterial) { invalidated |= this->invalidate_steps({ posSimplifySupportPath }); invalidated |= m_print->invalidate_steps({ psSkirtBrim }); - m_slicing_params.valid = false; + // NOTE: do NOT set m_slicing_params.valid = false here. + // belt_floor_z_shift is patched to an exact value during posSlice + // (PrintObjectSlice.cpp, after slice_volumes). Invalidating slicing + // params here causes update_slicing_parameters() to overwrite that + // exact value with a bounding-box approximation, while posSlice does + // not re-run to correct it — breaking belt support clipping. } // Wipe tower depends on the ordering of extruders, which in turn depends on everything. @@ -3392,6 +3398,10 @@ void PrintObject::update_slicing_parameters() // Orca: updated function call for XYZ shrinkage compensation if (!m_slicing_params.valid) { coordf_t object_height = this->model_object()->max_z(); + // Belt floor parameters for support clipping (populated below if belt Z-shear is active). + double belt_floor_shear_factor_out = 0.0; + int belt_floor_from_axis_out = 1; + double belt_floor_z_shift_out = 0.0; // Belt shear/scale may change the effective Z height. const auto &pcfg = this->print()->config(); if (pcfg.belt_printer.value) { @@ -3435,6 +3445,12 @@ void PrintObject::update_slicing_parameters() max_rz = std::max(max_rz, new_z); } object_height = max_rz - min_rz; + belt_floor_shear_factor_out = shear_factor; + belt_floor_from_axis_out = from; + // Belt contact surface starts at bb.min.z() pre-shear; add the + // slicing Z-shift that keeps the mesh above Z=0. + // Exact value is patched after slice_volumes() in posSlice. + belt_floor_z_shift_out = bb.min.z() + ((min_rz < 0.) ? -min_rz : 0.); } else { object_height *= scale_z; } @@ -3442,6 +3458,10 @@ void PrintObject::update_slicing_parameters() } m_slicing_params = SlicingParameters::create_from_config(pcfg, m_config, object_height, this->object_extruders(), this->print()->shrinkage_compensation()); + // Populate belt floor parameters into slicing params for support clipping. + m_slicing_params.belt_floor_shear_factor = belt_floor_shear_factor_out; + m_slicing_params.belt_floor_from_axis = belt_floor_from_axis_out; + m_slicing_params.belt_floor_z_shift = belt_floor_z_shift_out; } } @@ -3479,6 +3499,11 @@ SlicingParameters PrintObject::slicing_parameters(const DynamicPrintConfig &full sort_remove_duplicates(object_extruders); //FIXME add painting extruders + // Belt floor parameters for support clipping (populated below if belt Z-shear is active). + double belt_floor_shear_factor_out = 0.0; + int belt_floor_from_axis_out = 1; + double belt_floor_z_shift_out = 0.0; + if (object_max_z <= 0.f) { BoundingBoxf3 bb = model_object.raw_bounding_box(); object_max_z = (float)bb.size().z(); @@ -3523,13 +3548,20 @@ SlicingParameters PrintObject::slicing_parameters(const DynamicPrintConfig &full max_rz = std::max(max_rz, new_z); } object_max_z = (float)(max_rz - min_rz); + belt_floor_shear_factor_out = shear_factor; + belt_floor_from_axis_out = from; + belt_floor_z_shift_out = bb.min.z() + ((min_rz < 0.) ? -min_rz : 0.); } else { object_max_z *= (float)scale_z; } } } } - return SlicingParameters::create_from_config(print_config, object_config, object_max_z, object_extruders, object_shrinkage_compensation); + SlicingParameters params = SlicingParameters::create_from_config(print_config, object_config, object_max_z, object_extruders, object_shrinkage_compensation); + params.belt_floor_shear_factor = belt_floor_shear_factor_out; + params.belt_floor_from_axis = belt_floor_from_axis_out; + params.belt_floor_z_shift = belt_floor_z_shift_out; + return params; } // returns 0-based indices of extruders used to print the object (without brim, support and other helper extrusions) @@ -4038,6 +4070,67 @@ 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)) { @@ -4049,6 +4142,25 @@ void PrintObject::_generate_support_material() PrintObjectSupportMaterial support_material(this, m_slicing_params); support_material.generate(*this); } + // Global Z offset for support layers: + // - Normal support: layers already inherit global_z_offset from object layers. + // - Non-organic tree support (slim/strong/hybrid): plan_layer_heights() reads + // from globally-offset object layers, so support layers already have it. + // - Organic tree support: generate_tree_support_3D() computes its own Z values + // independently and does NOT inherit the offset — apply it here. + // Belt floor polygon clipping for non-organic tree support is done inside + // draw_circles() before area_groups and toolpaths are built. + if (is_tree(m_config.support_type.value) && std::abs(m_belt_global_z_offset) > EPSILON) { + // Resolve effective support style (same logic as SupportParameters). + auto style = m_config.support_style.value; + if (style == smsDefault) + style = smsTreeOrganic; + if (style == smsTreeOrganic) { + for (SupportLayer *sl : m_support_layers) + sl->print_z += m_belt_global_z_offset; + } + } + } // BBS diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index 294c245eaa..404550da1a 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -1,4 +1,5 @@ #include +#include #include @@ -125,7 +126,8 @@ static std::vector slice_volumes_inner( ModelVolumePtrs model_volumes, const std::vector &layer_ranges, const std::vector &zs, - const std::function &throw_on_cancel_callback) + const std::function &throw_on_cancel_callback, + double *out_belt_min_z = nullptr) { model_volumes_sort_by_id(model_volumes); @@ -202,8 +204,32 @@ static std::vector slice_volumes_inner( } // Apply: scale * shear * trafo (shear first, then scale). - if (has_shear || has_scale) + if (has_shear || has_scale) { params_base.trafo = belt_scale * belt_shear * params_base.trafo; + + // After the shear/scale transform, the mesh may clip through the + // build plate (Z < 0). Detect this and shift the mesh up. + Transform3d combined = params_base.trafo; + double min_z = std::numeric_limits::max(); + for (const ModelVolume *mv : model_volumes) { + if (!mv->is_model_part()) continue; + for (const stl_vertex &v : mv->mesh().its.vertices) { + Vec3d pt = combined * v.cast(); + min_z = std::min(min_z, pt.z()); + } + } + double belt_z_shift_val = (min_z < 0. && min_z != std::numeric_limits::max()) ? -min_z : 0.; + BOOST_LOG_TRIVIAL(warning) << "Belt Z-shift: min_z=" << min_z + << " z_shift=" << belt_z_shift_val + << " trafo_z=" << object_trafo.matrix()(2, 3); + if (belt_z_shift_val > 0.) { + Transform3d z_shift = Transform3d::Identity(); + z_shift.matrix()(2, 3) = belt_z_shift_val; + params_base.trafo = z_shift * params_base.trafo; + } + if (out_belt_min_z) + *out_belt_min_z = (min_z != std::numeric_limits::max()) ? min_z : 0.; + } } //BBS: 0.0025mm is safe enough to simplify the data to speed slicing up for high-resolution model. //Also has on influence on arc fitting which has default resolution 0.0125mm. @@ -866,6 +892,20 @@ void PrintObject::slice() m_layers = new_layers(this, generate_object_layers(m_slicing_params, layer_height_profile, m_config.precise_z_height.value)); this->slice_volumes(); m_print->throw_if_canceled(); + + // After slicing, m_belt_min_z holds the exact post-shear minimum Z + // in trafo_centered space (which includes the ensure_on_bed Z offset). + // The belt surface is at Z=0 in trafo_centered space; after shear it + // becomes Z = sf*Y, and after the z-shift that keeps the mesh above + // Z=0 it becomes Z = sf*Y + z_shift_val. So belt_floor_z_shift is + // simply the z-shift applied, i.e. max(0, -m_belt_min_z). + // NOTE: do NOT add raw_bounding_box().min.z() here — m_belt_min_z + // already includes the ensure_on_bed offset, unlike the min_rz used + // in update_slicing_parameters() which needs that compensation. + if (std::abs(m_slicing_params.belt_floor_shear_factor) > EPSILON) { + m_slicing_params.belt_floor_z_shift = (m_belt_min_z < 0.) ? -m_belt_min_z : 0.; + } + int firstLayerReplacedBy = 0; #if 0 @@ -904,6 +944,83 @@ void PrintObject::slice() if (m_layers.empty()) throw Slic3r::SlicingError(L("No layers were detected. You might want to repair your STL file(s) or check their size or thickness and retry.\n")); + // Belt printer global mode: offset all layer Z values so objects at + // different bed positions print at different heights on the tilted belt. + // This is a post-slicing adjustment — the sliced geometry is identical + // regardless of global mode, only the output Z coordinates change. + { + const auto &pcfg = this->print()->config(); + BOOST_LOG_TRIVIAL(warning) << "Belt global check: belt_printer=" << pcfg.belt_printer.value + << " belt_shear_z=" << int(pcfg.belt_shear_z.value) + << " belt_shear_z_global=" << pcfg.belt_shear_z_global.value + << " object=" << this->model_object()->name; + if (pcfg.belt_printer.value) { + auto compute_shear_factor = [](BeltShearMode mode, double angle_deg) -> double { + double angle_rad = Geometry::deg2rad(angle_deg); + double sin_a = std::sin(angle_rad); + double cos_a = std::cos(angle_rad); + switch (mode) { + case BeltShearMode::PosCot: return (sin_a > EPSILON) ? cos_a / sin_a : 0.; + case BeltShearMode::NegCot: return (sin_a > EPSILON) ? -cos_a / sin_a : 0.; + case BeltShearMode::PosTan: return (cos_a > EPSILON) ? sin_a / cos_a : 0.; + case BeltShearMode::NegTan: return (cos_a > EPSILON) ? -sin_a / cos_a : 0.; + default: return 0.; + } + }; + + Point inst_shift = this->instances().empty() ? Point(0, 0) + : this->instances().front().shift - this->center_offset(); + BOOST_LOG_TRIVIAL(warning) << "Belt global: object " << this->model_object()->name + << " instances=" << this->instances().size() + << " shift=(" << unscale(inst_shift.x()) << ", " << unscale(inst_shift.y()) << ")"; + double global_z_offset = 0.; + + struct GAxis { BeltShearMode mode; double angle; int from; bool global; }; + GAxis gaxes[3] = { + { pcfg.belt_shear_x.value, pcfg.belt_shear_x_angle.value, int(pcfg.belt_shear_x_from.value), pcfg.belt_shear_x_global.value }, + { pcfg.belt_shear_y.value, pcfg.belt_shear_y_angle.value, int(pcfg.belt_shear_y_from.value), pcfg.belt_shear_y_global.value }, + { pcfg.belt_shear_z.value, pcfg.belt_shear_z_angle.value, int(pcfg.belt_shear_z_from.value), pcfg.belt_shear_z_global.value }, + }; + + // Only the Z-row shear contributes a Z offset from global mode. + // (X/Y row shears with global would offset X/Y, not Z — not useful here.) + // Offsets are RELATIVE: we subtract the minimum shift across all + // PrintObjects so the lowest-positioned object stays at Z=0. + const auto &za = gaxes[2]; // Z row + if (za.global && za.mode != BeltShearMode::None && za.from < 2) { + double factor = compute_shear_factor(za.mode, za.angle); + // The Z-shift brought the mesh's lowest sheared vertex to + // Z=0. That vertex's physical Y determines the belt contact + // point. With trafo_z preserved (ensure_on_bed offset), + // min_z = Y_at_contact * factor for bottom-face vertices, + // so: z_offset = center_Y * factor + min_z. + Point phys = inst_shift; // already has center_offset subtracted + double center_on_axis = (za.from == 0) ? unscale(phys.x()) : unscale(phys.y()); + global_z_offset += center_on_axis * factor + m_belt_min_z; + } + + BOOST_LOG_TRIVIAL(warning) << "Belt global: z_offset=" << global_z_offset + << " za.global=" << za.global << " za.mode=" << int(za.mode) << " za.from=" << za.from + << " (relative to min across " << this->print()->objects().size() << " objects)"; + m_belt_global_z_offset = global_z_offset; + if (std::abs(global_z_offset) > EPSILON) { + for (Layer *layer : m_layers) + layer->print_z += global_z_offset; + // Keep belt floor clipping in sync with the shifted print_z + // values — the support generator sees globally-offset object + // layer print_z, so belt_floor_z_shift must match. + m_slicing_params.belt_floor_z_shift += global_z_offset; + } + if (!m_layers.empty()) { + BOOST_LOG_TRIVIAL(warning) << "Belt global: first_layer_z=" << m_layers.front()->print_z + << " last_layer_z=" << m_layers.back()->print_z + << " num_layers=" << m_layers.size() + << " center_offset=(" << unscale(m_center_offset.x()) + << ", " << unscale(m_center_offset.y()) << ")"; + } + } + } + // BBS this->set_done(posSlice); } @@ -1208,7 +1325,8 @@ void PrintObject::slice_volumes() if (!slice_zs.empty()) { objSliceByVolume = slice_volumes_inner( print->config(), this->config(), this->trafo_centered(), - this->model_object()->volumes, m_shared_regions->layer_ranges, slice_zs, throw_on_cancel_callback); + this->model_object()->volumes, m_shared_regions->layer_ranges, slice_zs, throw_on_cancel_callback, + &m_belt_min_z); } //BBS: "model_part" volumes are grouded according to their connections diff --git a/src/libslic3r/Slicing.hpp b/src/libslic3r/Slicing.hpp index a7b21a140d..b06f2a7bc4 100644 --- a/src/libslic3r/Slicing.hpp +++ b/src/libslic3r/Slicing.hpp @@ -110,6 +110,13 @@ struct SlicingParameters coordf_t object_print_z_uncompensated_max { 0 }; // Scaling factor for compensating shrinkage in Z-axis. coordf_t object_shrinkage_compensation_z { 0 }; + + // Belt printer: floor plane parameters for support clipping. + // Belt contact surface in slicing coords: Z = bb_min_z + sf*Y + slicing_z_shift. + // cutoff = (print_z - belt_floor_z_shift - floor_offset) / shear_factor + double belt_floor_shear_factor { 0.0 }; // shear factor (e.g. cot(45deg)) + int belt_floor_from_axis { 1 }; // which axis the shear is from (0=X, 1=Y) + double belt_floor_z_shift { 0.0 }; // bb_min_z + max(0, -min_z_after_shear) }; static_assert(IsTriviallyCopyable::value, "SlicingParameters class is not POD (and it should be - see constructor)."); diff --git a/src/libslic3r/Support/SupportCommon.hpp b/src/libslic3r/Support/SupportCommon.hpp index 531daaa0a8..f15c7d417a 100644 --- a/src/libslic3r/Support/SupportCommon.hpp +++ b/src/libslic3r/Support/SupportCommon.hpp @@ -144,6 +144,12 @@ int idx_lower_or_equal(const std::vector &vec, int idx, FN_LOWER_EQUAL fn_lo return idx_lower_or_equal(vec.begin(), vec.end(), idx, fn_lower_equal); } +// Belt floor: compute the belt-side half-plane polygon at a given print_z. +// Used to clip support polygons against the belt surface. +Polygons belt_floor_surface_polygon( + const SlicingParameters &slicing_params, const PrintConfig &print_config, + const PrintObject &object, coordf_t print_z); + } // namespace Slic3r #endif /* slic3r_SupportCommon_hpp_ */ diff --git a/src/libslic3r/Support/SupportMaterial.cpp b/src/libslic3r/Support/SupportMaterial.cpp index c230924ac8..d70153a7dd 100644 --- a/src/libslic3r/Support/SupportMaterial.cpp +++ b/src/libslic3r/Support/SupportMaterial.cpp @@ -367,10 +367,21 @@ inline void layers_append(SupportGeneratorLayersPtr &dst, const SupportGenerator } // Support layer that is covered by some form of dense interface. -static constexpr const std::initializer_list support_types_interface { +static constexpr const std::initializer_list support_types_interface { SupporLayerType::RaftInterface, SupporLayerType::BottomContact, SupporLayerType::BottomInterface, SupporLayerType::TopContact, SupporLayerType::TopInterface }; +// Forward declarations for belt floor helpers (defined later in this file). +// belt_floor_surface_polygon is declared in SupportCommon.hpp (non-static, +// shared with TreeSupport.cpp). + +static Polygons belt_floor_valid_region_polygon( + const SlicingParameters &slicing_params, const PrintConfig &print_config, + const PrintObject &object, coordf_t print_z); +static void trim_support_layers_by_belt_floor( + const SlicingParameters &slicing_params, const PrintConfig &print_config, + const PrintObject &object, SupportGeneratorLayersPtr &support_layers); + void PrintObjectSupportMaterial::generate(PrintObject &object) { BOOST_LOG_TRIVIAL(info) << "Support generator - Start"; @@ -443,11 +454,12 @@ void PrintObjectSupportMaterial::generate(PrintObject &object) object, bottom_contacts, top_contacts, layer_storage); this->trim_support_layers_by_object(object, top_contacts, m_slicing_params.gap_support_object, m_slicing_params.gap_object_support, m_support_params.gap_xy); + trim_support_layers_by_belt_floor(m_slicing_params, *m_print_config, object, top_contacts); #ifdef SLIC3R_DEBUG for (const SupportGeneratorLayer *layer : top_contacts) Slic3r::SVG::export_expolygons( - debug_out_path("support-top-contacts-trimmed-by-object-%d-%lf.svg", iRun, layer->print_z), + debug_out_path("support-top-contacts-trimmed-by-object-%d-%lf.svg", iRun, layer->print_z), union_ex(layer->polygons)); #endif @@ -603,6 +615,139 @@ Polygons collect_region_slices_by_type(const Layer &layer, SurfaceType surface_t return out; } +// Belt printer: compute the belt-side half-plane polygon at a given print_z. +// This represents the region where the belt surface exists (the "phantom top surface"). +// Support that overlaps with this polygon should terminate with a bottom contact. +// Returns empty if belt floor is not active. +Polygons belt_floor_surface_polygon( + const SlicingParameters &slicing_params, + const PrintConfig &print_config, + const PrintObject &object, + coordf_t print_z) +{ + const double shear_factor = slicing_params.belt_floor_shear_factor; + if (std::abs(shear_factor) < EPSILON) + return {}; + + const int from_axis = slicing_params.belt_floor_from_axis; // 0=X, 1=Y + const double floor_offset = print_config.belt_support_floor_offset.value; + + // Belt floor line in slicing coordinates: Z = sf * Y + z_shift. + // z_shift accounts for the upward shift applied when post-shear geometry + // extends below the bed (overhangs). Solving for Y: + // cutoff = (print_z - z_shift - floor_offset) / sf + const double z_shift = slicing_params.belt_floor_z_shift; + const double cutoff = (print_z - z_shift - floor_offset) / shear_factor; + const coord_t cutoff_scaled = scale_(cutoff); + const coord_t large_bound = scale_(1e4); + + // Build the belt-side half-plane (inverted from the valid region). + // If shear_factor > 0: valid region is from_axis < cutoff, so belt surface is from_axis >= cutoff. + // If shear_factor < 0: valid region is from_axis > cutoff, so belt surface is from_axis <= cutoff. + Polygon belt_poly; + if (from_axis == 0) { + if (shear_factor > 0) { + // Belt surface: X >= cutoff + belt_poly.points = { + Point(cutoff_scaled, -large_bound), + Point(large_bound, -large_bound), + Point(large_bound, large_bound), + Point(cutoff_scaled, large_bound) + }; + } else { + // Belt surface: X <= cutoff + belt_poly.points = { + Point(-large_bound, -large_bound), + Point(cutoff_scaled, -large_bound), + Point(cutoff_scaled, large_bound), + Point(-large_bound, large_bound) + }; + } + } else { + if (shear_factor > 0) { + // Belt surface: Y >= cutoff + belt_poly.points = { + Point(-large_bound, cutoff_scaled), + Point( large_bound, cutoff_scaled), + Point( large_bound, large_bound), + Point(-large_bound, large_bound) + }; + } else { + // Belt surface: Y <= cutoff + belt_poly.points = { + Point(-large_bound, -large_bound), + Point( large_bound, -large_bound), + Point( large_bound, cutoff_scaled), + Point(-large_bound, cutoff_scaled) + }; + } + } + return { belt_poly }; +} + +// Belt printer: compute the valid-region half-plane polygon at a given print_z. +// This is the region where support is allowed to exist (above the belt). +// Used to clip the downward-propagating support projection. +static Polygons belt_floor_valid_region_polygon( + const SlicingParameters &slicing_params, + const PrintConfig &print_config, + const PrintObject &object, + coordf_t print_z) +{ + const double shear_factor = slicing_params.belt_floor_shear_factor; + if (std::abs(shear_factor) < EPSILON) + return {}; + + const int from_axis = slicing_params.belt_floor_from_axis; + const double floor_offset = print_config.belt_support_floor_offset.value; + + const double z_shift = slicing_params.belt_floor_z_shift; + const double cutoff = (print_z - z_shift - floor_offset) / shear_factor; + const coord_t cutoff_scaled = scale_(cutoff); + const coord_t large_bound = scale_(1e4); + + // Valid region: the complement of the belt surface polygon. + Polygon valid_poly; + if (from_axis == 0) { + if (shear_factor > 0) { + // Valid: X < cutoff + valid_poly.points = { + Point(-large_bound, -large_bound), + Point(cutoff_scaled, -large_bound), + Point(cutoff_scaled, large_bound), + Point(-large_bound, large_bound) + }; + } else { + // Valid: X > cutoff + valid_poly.points = { + Point(cutoff_scaled, -large_bound), + Point(large_bound, -large_bound), + Point(large_bound, large_bound), + Point(cutoff_scaled, large_bound) + }; + } + } else { + if (shear_factor > 0) { + // Valid: Y < cutoff + valid_poly.points = { + Point(-large_bound, -large_bound), + Point( large_bound, -large_bound), + Point( large_bound, cutoff_scaled), + Point(-large_bound, cutoff_scaled) + }; + } else { + // Valid: Y > cutoff + valid_poly.points = { + Point(-large_bound, cutoff_scaled), + Point( large_bound, cutoff_scaled), + Point( large_bound, large_bound), + Point(-large_bound, large_bound) + }; + } + } + return { valid_poly }; +} + // Collect outer contours of all slices of this layer. // This is useful for calculating the support base with holes filled. Polygons collect_slices_outer(const Layer &layer) @@ -2516,6 +2661,82 @@ static inline SupportGeneratorLayer* detect_bottom_contacts( return &layer_new; } +// Belt printer: detect bottom contacts where support meets the belt floor plane. +// Modeled on detect_bottom_contacts() but uses the belt plane polygon instead of stTop surfaces. +static inline SupportGeneratorLayer* detect_belt_floor_bottom_contacts( + const SlicingParameters &slicing_params, + const SupportParameters &support_params, + const PrintConfig &print_config, + const PrintObject &object, + const Layer &layer, + // Existing top contact layers, for snapping. + const SupportGeneratorLayersPtr &top_contacts, + size_t contact_idx, + SupportGeneratorLayerStorage &layer_storage, + std::vector &layer_support_areas, + const Polygons &supports_projected) +{ + // Compute the belt surface polygon at this layer's Z. + Polygons belt_surface = belt_floor_surface_polygon(slicing_params, print_config, object, layer.print_z); + if (belt_surface.empty()) + return nullptr; + + // Find where projected support overlaps the belt surface. + Polygons touching = intersection(belt_surface, supports_projected); + if (touching.empty()) + return nullptr; + + assert(layer.id() >= slicing_params.raft_layers()); + size_t layer_id = layer.id() - slicing_params.raft_layers(); + + // Allocate a new bottom contact layer resting on the belt plane. + SupportGeneratorLayer &layer_new = layer_storage.allocate_unguarded(SupporLayerType::BottomContact); + + // No object layer to sync with -- compute heights directly from flow parameters. + layer_new.height = support_params.support_material_bottom_interface_flow.height(); + layer_new.print_z = layer.print_z + layer_new.height + slicing_params.gap_object_support; + layer_new.bottom_z = layer.print_z; + layer_new.idx_object_layer_below = layer_id; + layer_new.bridging = ! slicing_params.soluble_interface && object.config().thick_bridges; + layer_new.polygons = expand(touching, float(support_params.support_material_flow.scaled_width()), SUPPORT_SURFACES_OFFSET_PARAMETERS); + + if (! slicing_params.soluble_interface) { + // Snap to nearby top contact layers to avoid very thin support layers. + for (size_t top_idx = size_t(std::max(0, int(contact_idx))); + top_idx < top_contacts.size() && top_contacts[top_idx]->print_z < layer_new.print_z + support_params.support_layer_height_min + EPSILON; + ++ top_idx) { + if (top_contacts[top_idx]->print_z > layer_new.print_z - support_params.support_layer_height_min - EPSILON) { + coordf_t diff = layer_new.print_z - top_contacts[top_idx]->print_z; + assert(std::abs(diff) <= support_params.support_layer_height_min + EPSILON); + if (diff > 0.F) { + if (layer_new.height - diff > support_params.support_layer_height_min) { + layer_new.print_z = top_contacts[top_idx]->print_z; + layer_new.height -= diff; + } else { + continue; + } + } else { + layer_new.print_z = top_contacts[top_idx]->print_z; + layer_new.height -= diff; + } + break; + } + } + } + + // Trim the already created base layers above this belt contact. + touching = expand(touching, float(SCALED_EPSILON)); + for (int layer_id_above = int(layer_id) + 1; layer_id_above < int(object.total_layer_count()); ++ layer_id_above) { + const Layer &layer_above = *object.layers()[layer_id_above]; + if (layer_above.print_z > layer_new.print_z - EPSILON) + break; + if (Polygons &above = layer_support_areas[layer_id_above]; ! above.empty()) + above = diff(above, touching); + } + + return &layer_new; +} + // Returns polygons to print + polygons to propagate downwards. // Called twice: First for normal supports, possibly trimmed by "on build plate only", second for support enforcers not trimmed by "on build plate only". static inline std::pair project_support_to_grid(const Layer &layer, const SupportGridParams &grid_params, const Polygons &overhangs, Polygons *layer_buildplate_covered @@ -2615,6 +2836,8 @@ SupportGeneratorLayersPtr PrintObjectSupportMaterial::bottom_contact_layers_and_ //const auto expansion_to_slice = m_support_material_flow.scaled_spacing() / 2 + 25; const SupportGridParams grid_params(*m_object_config, m_support_params.support_material_flow); const bool buildplate_only = ! buildplate_covered.empty(); + const bool has_belt_floor = std::abs(m_slicing_params.belt_floor_shear_factor) > EPSILON + && m_print_config->belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly; // Allocate empty surface areas, one per object layer. layer_support_areas.assign(object.total_layer_count(), Polygons()); @@ -2675,8 +2898,9 @@ SupportGeneratorLayersPtr PrintObjectSupportMaterial::bottom_contact_layers_and_ tbb::task_group task_group; const Polygons &overhangs_for_bottom_contacts = buildplate_only ? enforcers_projection_raw : overhangs_projection_raw; if (! overhangs_for_bottom_contacts.empty()) - // Find the bottom contact layers above the top surfaces of this layer. - task_group.run([this, &object, &layer, &top_contacts, contact_idx, &layer_storage, &layer_support_areas, &bottom_contacts, &overhangs_for_bottom_contacts + // Find the bottom contact layers above the top surfaces of this layer, + // and also detect belt floor contacts if belt mode is active. + task_group.run([this, &object, &layer, &top_contacts, contact_idx, &layer_storage, &layer_support_areas, &bottom_contacts, &overhangs_for_bottom_contacts, has_belt_floor #ifdef SLIC3R_DEBUG , iRun, &polygons_new #endif // SLIC3R_DEBUG @@ -2690,6 +2914,15 @@ SupportGeneratorLayersPtr PrintObjectSupportMaterial::bottom_contact_layers_and_ ); if (layer_new) bottom_contacts.push_back(layer_new); + // Belt floor phantom surface: detect where support meets the belt plane. + if (has_belt_floor) { + SupportGeneratorLayer *belt_layer = detect_belt_floor_bottom_contacts( + m_slicing_params, m_support_params, *m_print_config, object, + layer, top_contacts, contact_idx, layer_storage, + layer_support_areas, overhangs_for_bottom_contacts); + if (belt_layer) + bottom_contacts.push_back(belt_layer); + } }); Polygons &layer_support_area = layer_support_areas[layer_id]; @@ -2728,6 +2961,23 @@ SupportGeneratorLayersPtr PrintObjectSupportMaterial::bottom_contact_layers_and_ task_group.wait(); + // Belt floor: clip projections and support areas so support doesn't + // propagate below the belt plane. + if (has_belt_floor) { + Polygons valid_region = belt_floor_valid_region_polygon( + m_slicing_params, *m_print_config, object, layer.print_z); + if (! valid_region.empty()) { + if (! overhangs_projection.empty()) + overhangs_projection = intersection(overhangs_projection, valid_region); + if (! enforcers_projection.empty()) + enforcers_projection = intersection(enforcers_projection, valid_region); + if (! layer_support_area.empty()) + layer_support_area = intersection(layer_support_area, valid_region); + if (! layer_support_area_enforcers.empty()) + layer_support_area_enforcers = intersection(layer_support_area_enforcers, valid_region); + } + } + if (! layer_support_area_enforcers.empty()) { if (layer_support_area.empty()) layer_support_area = std::move(layer_support_area_enforcers); @@ -2738,6 +2988,7 @@ SupportGeneratorLayersPtr PrintObjectSupportMaterial::bottom_contact_layers_and_ std::reverse(bottom_contacts.begin(), bottom_contacts.end()); trim_support_layers_by_object(object, bottom_contacts, m_slicing_params.gap_support_object, m_slicing_params.gap_object_support, m_support_params.gap_xy); + trim_support_layers_by_belt_floor(m_slicing_params, *m_print_config, object, bottom_contacts); return bottom_contacts; } @@ -3108,6 +3359,37 @@ void PrintObjectSupportMaterial::generate_base_layers( #endif /* SLIC3R_DEBUG */ this->trim_support_layers_by_object(object, intermediate_layers, m_slicing_params.gap_support_object, m_slicing_params.gap_object_support, m_support_params.gap_xy); + trim_support_layers_by_belt_floor(m_slicing_params, *m_print_config, object, intermediate_layers); +} + +// Belt printer: trim support layer polygons by the belt floor plane. +// For each support layer, computes the belt floor half-plane at that layer's print_z +// and subtracts it from the support polygons. This follows the same diff() pattern +// as trim_support_layers_by_object() so that interface layers derived from trimmed +// intermediates automatically inherit the belt floor trimming. +static void trim_support_layers_by_belt_floor( + const SlicingParameters &slicing_params, + const PrintConfig &print_config, + const PrintObject &object, + SupportGeneratorLayersPtr &support_layers) +{ + if (std::abs(slicing_params.belt_floor_shear_factor) < EPSILON) + return; + if (print_config.belt_support_floor_mode.value != BeltSupportFloorMode::GeneratorOnly) + return; + + tbb::parallel_for(tbb::blocked_range(0, support_layers.size()), + [&](const tbb::blocked_range &range) { + for (size_t i = range.begin(); i < range.end(); ++ i) { + SupportGeneratorLayer *layer = support_layers[i]; + if (layer->polygons.empty()) + continue; + Polygons belt_surface = belt_floor_surface_polygon( + slicing_params, print_config, object, layer->print_z); + if (! belt_surface.empty()) + layer->polygons = diff(layer->polygons, belt_surface); + } + }); } void PrintObjectSupportMaterial::trim_support_layers_by_object( diff --git a/src/libslic3r/Support/TreeModelVolumes.cpp b/src/libslic3r/Support/TreeModelVolumes.cpp index 0267c0c327..e270b62aab 100644 --- a/src/libslic3r/Support/TreeModelVolumes.cpp +++ b/src/libslic3r/Support/TreeModelVolumes.cpp @@ -94,6 +94,46 @@ 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(); + const double sf = sp.belt_floor_shear_factor; + if (std::abs(sf) > EPSILON + && std::abs(print_object.belt_global_z_offset()) > EPSILON + && pcfg.belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) { + const int from_axis = sp.belt_floor_from_axis; + const double floor_off = pcfg.belt_support_floor_offset.value; + const double z_shift = sp.belt_floor_z_shift - print_object.belt_global_z_offset(); + 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(); + double cutoff = (print_z - z_shift - floor_off) / sf; + coord_t cutoff_sc = scale_(cutoff); + coord_t big = scale_(1e4); + Polygon belt_poly; + if (from_axis == 0) { + if (sf > 0) + belt_poly.points = {{cutoff_sc,-big},{big,-big},{big,big},{cutoff_sc,big}}; + else + belt_poly.points = {{-big,-big},{cutoff_sc,-big},{cutoff_sc,big},{-big,big}}; + } else { + if (sf > 0) + belt_poly.points = {{-big,cutoff_sc},{big,cutoff_sc},{big,big},{-big,big}}; + else + belt_poly.points = {{-big,-big},{big,-big},{big,cutoff_sc},{-big,cutoff_sc}}; + } + append(m_anti_overhang[layer_idx], Polygons{belt_poly}); + } + } + } TreeSupportMeshGroupSettings mesh_settings(print_object); const TreeSupportSettings config{ mesh_settings, print_object.slicing_parameters() }; m_current_min_xy_dist = config.xy_min_distance; @@ -102,6 +142,27 @@ TreeModelVolumes::TreeModelVolumes( m_increase_until_radius = config.increase_radius_until_radius; m_radius_0 = config.getRadius(0); m_raft_layers = config.raft_layers; + // Belt printer: add virtual belt raft layers below the object, matching + // the extra layers added in generate_support_areas() so both use the + // same layer indexing. + { + const auto &sp2 = print_object.slicing_parameters(); + const auto &pcfg2 = print_object.print()->config(); + double belt_sf = sp2.belt_floor_shear_factor; + if (std::abs(belt_sf) > EPSILON && std::abs(print_object.belt_global_z_offset()) > EPSILON + && pcfg2.belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) { + double bb_min_z = std::abs(print_object.model_object()->raw_bounding_box().min.z()); + double extra_depth = bb_min_z + 10.; + int num_extra = std::max(0, (int)std::ceil(extra_depth / sp2.layer_height)); + if (num_extra > 0) { + std::vector belt_layers; + belt_layers.reserve(num_extra); + for (int i = num_extra; i >= 1; --i) + belt_layers.push_back(sp2.first_object_layer_height - i * sp2.layer_height); + m_raft_layers.insert(m_raft_layers.begin(), belt_layers.begin(), belt_layers.end()); + } + } + } m_current_outline_idx = 0; m_layer_outlines.emplace_back(mesh_settings, std::vector{}); @@ -114,6 +175,47 @@ TreeModelVolumes::TreeModelVolumes( for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++ layer_idx) outlines[layer_idx] = polygons_simplify(to_polygons(print_object.get_layer(layer_idx - num_raft_layers)->lslices), mesh_settings.resolution, polygons_strictly_simple); }); + + // Belt floor: pre-compute belt surface polygon per-layer for clipping. + // Branches grow toward the belt and their slices are clipped at the belt + // surface in organic_draw_branches(). The organic pipeline works in LOCAL + // Z (no global_z_offset), so use local z_shift and local print_z. + const auto &slicing_params = print_object.slicing_parameters(); + const auto &pcfg = print_object.print()->config(); + const double sf = slicing_params.belt_floor_shear_factor; + if (std::abs(sf) > EPSILON + && pcfg.belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) { + const int from_axis = slicing_params.belt_floor_from_axis; + const double floor_off = pcfg.belt_support_floor_offset.value; + // Subtract global_z_offset to get the LOCAL z_shift — the organic + // pipeline's Z coordinates don't include the global offset. + const double z_shift = slicing_params.belt_floor_z_shift + - print_object.belt_global_z_offset(); + m_belt_floor.assign(num_layers, Polygons{}); + for (size_t layer_idx = 0; layer_idx < num_layers; ++layer_idx) { + // Use local print_z (subtract global offset from object layer). + double print_z = (layer_idx >= num_raft_layers) + ? print_object.get_layer(layer_idx - num_raft_layers)->print_z + - print_object.belt_global_z_offset() + : 0.; + double cutoff = (print_z - z_shift - floor_off) / sf; + coord_t cutoff_sc = scale_(cutoff); + coord_t big = scale_(1e4); + Polygon belt_poly; + if (from_axis == 0) { + if (sf > 0) + belt_poly.points = {{cutoff_sc,-big},{big,-big},{big,big},{cutoff_sc,big}}; + else + belt_poly.points = {{-big,-big},{cutoff_sc,-big},{cutoff_sc,big},{-big,big}}; + } else { + if (sf > 0) + belt_poly.points = {{-big,cutoff_sc},{big,cutoff_sc},{big,big},{-big,big}}; + else + belt_poly.points = {{-big,-big},{big,-big},{big,cutoff_sc},{-big,cutoff_sc}}; + } + m_belt_floor[layer_idx] = { belt_poly }; + } + } } #endif @@ -469,9 +571,9 @@ void TreeModelVolumes::calculateCollision(const coord_t radius, const LayerIndex }); // 2) Sum over top / bottom ranges. - const bool processing_last_mesh = outline_idx == layer_outline_indices.size(); + const bool processing_last_mesh = outline_idx == layer_outline_indices.back(); tbb::parallel_for(tbb::blocked_range(data.begin(), data.end()), - [&collision_areas_offsetted, &outlines, &machine_border = m_machine_border, &anti_overhang = m_anti_overhang, radius, + [&collision_areas_offsetted, &outlines, &machine_border = m_machine_border, &anti_overhang = m_anti_overhang, radius, xy_distance, z_distance_bottom_layers, z_distance_top_layers, min_resolution = m_min_resolution, &data, processing_last_mesh, &throw_on_cancel] (const tbb::blocked_range& range) { for (LayerIndex layer_idx = range.begin(); layer_idx != range.end(); ++ layer_idx) { @@ -517,9 +619,14 @@ void TreeModelVolumes::calculateCollision(const coord_t radius, const LayerIndex // not support an overhang<90 degree than to risk fusing to it. append(collisions, offset(union_ex(collision_areas_original), radius + required_range_x, ClipperLib::jtMiter, 1.2)); } - collisions = processing_last_mesh && layer_idx < int(anti_overhang.size()) ? - union_(collisions, offset(union_ex(anti_overhang[layer_idx]), radius, ClipperLib::jtMiter, 1.2)) : - union_(collisions); + if (processing_last_mesh) { + if (layer_idx < int(anti_overhang.size())) + append(collisions, offset(union_ex(anti_overhang[layer_idx]), radius, ClipperLib::jtMiter, 1.2)); + // NOTE: m_belt_floor is NOT added to collision here — branches + // should grow toward the belt and terminate at it, not avoid it. + // Belt floor clipping is done post-generation in organic_draw_branches(). + } + collisions = union_(collisions); auto &dst = data[layer_idx]; if (processing_last_mesh) { if (! dst.empty()) diff --git a/src/libslic3r/Support/TreeModelVolumes.hpp b/src/libslic3r/Support/TreeModelVolumes.hpp index eff4cadcaf..0ded395091 100644 --- a/src/libslic3r/Support/TreeModelVolumes.hpp +++ b/src/libslic3r/Support/TreeModelVolumes.hpp @@ -168,6 +168,9 @@ public: } Polygon m_bed_area; + // Belt floor polygons per layer — used for post-generation clipping + // in organic_draw_branches(). Public so the organic pipeline can access it. + std::vector m_belt_floor; private: // Caching polygons for a range of layers. diff --git a/src/libslic3r/Support/TreeSupport.cpp b/src/libslic3r/Support/TreeSupport.cpp index 864a0eb0e0..4966a5a139 100644 --- a/src/libslic3r/Support/TreeSupport.cpp +++ b/src/libslic3r/Support/TreeSupport.cpp @@ -637,6 +637,20 @@ TreeSupport::TreeSupport(PrintObject& object, const SlicingParameters &slicing_p } +double TreeSupport::belt_floor_print_z(const Point &pos_slicing) const +{ + double sf = m_slicing_params.belt_floor_shear_factor; + if (std::abs(sf) < EPSILON) + return -std::numeric_limits::max(); // no belt floor + int from = m_slicing_params.belt_floor_from_axis; + // Belt floor in slicing coords: Z = sf * Y + z_shift + floor_offset. + // Inverse of cutoff = (Z - z_shift - floor_offset) / sf. + double pos = unscale(from == 0 ? pos_slicing.x() : pos_slicing.y()); + double floor_offset = m_print_config->belt_support_floor_offset.value; + double z_shift = m_slicing_params.belt_floor_z_shift; + return sf * pos + floor_offset + z_shift; +} + #define SUPPORT_SURFACES_OFFSET_PARAMETERS ClipperLib::jtSquare, 0. void TreeSupport::detect_overhangs(bool check_support_necessity/* = false*/) { @@ -2141,6 +2155,41 @@ void TreeSupport::draw_circles() base_areas = diff_ex(base_areas, ClipperUtils::clip_clipper_polygons_with_subject_bbox(roofs, get_extents(base_areas))); base_areas = intersection_ex(base_areas, m_machine_border); + // Belt floor: clip tree support polygons by the belt surface plane. + // ts_layer->print_z is at LOCAL Z (global offset applied later in + // _generate_support_material), but belt_floor_z_shift includes + // global_z_offset — subtract it to get the cutoff in local coords. + if (std::abs(m_slicing_params.belt_floor_shear_factor) > EPSILON + && m_print_config->belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) { + const double sf = m_slicing_params.belt_floor_shear_factor; + const int from_axis = m_slicing_params.belt_floor_from_axis; + const double floor_off = m_print_config->belt_support_floor_offset.value; + const double z_shift_local = m_slicing_params.belt_floor_z_shift + - m_object->belt_global_z_offset(); + const double cutoff = (ts_layer->print_z - z_shift_local - floor_off) / sf; + const coord_t cutoff_sc = scale_(cutoff); + const coord_t big = scale_(1e4); + + Polygon belt_poly; + if (from_axis == 0) { + if (sf > 0) + belt_poly.points = { {cutoff_sc,-big}, {big,-big}, {big,big}, {cutoff_sc,big} }; + else + belt_poly.points = { {-big,-big}, {cutoff_sc,-big}, {cutoff_sc,big}, {-big,big} }; + } else { + if (sf > 0) + belt_poly.points = { {-big,cutoff_sc}, {big,cutoff_sc}, {big,big}, {-big,big} }; + else + belt_poly.points = { {-big,-big}, {big,-big}, {big,cutoff_sc}, {-big,cutoff_sc} }; + } + Polygons belt_surface = { belt_poly }; + base_areas = diff_ex(base_areas, belt_surface); + roof_areas = diff_ex(roof_areas, belt_surface); + roof_1st_layer = diff_ex(roof_1st_layer, belt_surface); + floor_areas = diff_ex(floor_areas, belt_surface); + roof_gap_areas = diff_ex(roof_gap_areas, belt_surface); + } + if (SQUARE_SUPPORT) { // simplify support contours ExPolygons base_areas_simplified; @@ -2445,6 +2494,9 @@ void TreeSupport::drop_nodes() const coordf_t radius_sample_resolution = m_ts_data->m_radius_sample_resolution; const bool support_on_buildplate_only = config.support_on_build_plate_only.value; const size_t top_interface_layers = config.support_interface_top_layers.value; + const auto belt_floor_mode = m_print_config->belt_support_floor_mode.value; + const bool has_belt_floor = std::abs(m_slicing_params.belt_floor_shear_factor) > EPSILON + && belt_floor_mode == BeltSupportFloorMode::GeneratorOnly; const size_t bottom_interface_layers = config.support_interface_bottom_layers.value < 0 ? top_interface_layers : config.support_interface_bottom_layers.value; SupportNode::diameter_angle_scale_factor = diameter_angle_scale_factor; float DO_NOT_MOVER_UNDER_MM = is_slim ? 0 : 5; // do not move contact points under 5mm @@ -2677,15 +2729,22 @@ void TreeSupport::drop_nodes() node_parent = p_node->parent ? p_node : neighbour; // Make sure the next pass doesn't drop down either of these (since that already happened). node_parent->merged_neighbours.push_front(node_parent == p_node ? neighbour : p_node); - const bool to_buildplate = !is_inside_ex(get_collision(0, obj_layer_nr_next), next_position); - SupportNode* next_node = m_ts_data->create_node(next_position, node_parent->distance_to_top + 1, obj_layer_nr_next, node_parent->support_roof_layers_below - 1, to_buildplate, node_parent, - print_z_next, height_next); - get_max_move_dist(next_node); - m_ts_data->m_mutex.lock(); - contact_nodes[layer_nr_next].push_back(next_node); + // Belt floor: don't drop merged node below belt surface. + // Treat as object-surface termination (not buildplate) so + // the node gets floor/interface areas instead of base pads. + if (has_belt_floor && print_z_next <= belt_floor_print_z(next_position)) { + node_parent->to_buildplate = false; + } else { + const bool to_buildplate = !is_inside_ex(get_collision(0, obj_layer_nr_next), next_position); + SupportNode* next_node = m_ts_data->create_node(next_position, node_parent->distance_to_top + 1, obj_layer_nr_next, node_parent->support_roof_layers_below - 1, to_buildplate, node_parent, + print_z_next, height_next); + get_max_move_dist(next_node); + m_ts_data->m_mutex.lock(); + contact_nodes[layer_nr_next].push_back(next_node); + m_ts_data->m_mutex.unlock(); + } neighbour->valid = false; p_node->valid = false; - m_ts_data->m_mutex.unlock(); } else if (neighbours.size() > 1) //Don't merge leaf nodes because we would then incur movement greater than the maximum move distance. { @@ -2729,6 +2788,12 @@ void TreeSupport::drop_nodes() ExPolygons overhangs_next = diff_clipped({ node.overhang }, get_collision(0, obj_layer_nr_next)); for(auto& overhang:overhangs_next) { Point next_pt = overhang.contour.centroid(); + // Belt floor: don't drop polygon node below belt surface. + // Treat as object-surface termination (not buildplate). + if (has_belt_floor && print_z_next <= belt_floor_print_z(next_pt)) { + p_node->to_buildplate = false; + continue; + } SupportNode *next_node = m_ts_data->create_node(next_pt, p_node->distance_to_top + 1, obj_layer_nr_next, p_node->support_roof_layers_below - 1, to_buildplate, p_node, print_z_next, height_next); next_node->max_move_dist = 0; @@ -2873,6 +2938,12 @@ void TreeSupport::drop_nodes() if (is_outside) { next_layer_vertex = candidate_vertex; } } } + // Belt floor: don't drop regular node below belt surface. + // Treat as object-surface termination (not buildplate). + if (has_belt_floor && print_z_next <= belt_floor_print_z(next_layer_vertex)) { + p_node->to_buildplate = false; + return; // from parallel_for_each lambda + } auto next_collision = get_collision(0, obj_layer_nr_next); const bool to_buildplate = !is_inside_ex(m_ts_data->m_layer_outlines[obj_layer_nr_next], next_layer_vertex); SupportNode * next_node = m_ts_data->create_node(next_layer_vertex, node.distance_to_top + 1, obj_layer_nr_next, node.support_roof_layers_below - 1, to_buildplate, p_node, @@ -3167,6 +3238,9 @@ void TreeSupport::generate_contact_points() bool on_buildplate_only = m_object_config->support_on_build_plate_only.value; const bool roof_enabled = config.support_interface_top_layers.value > 0; const bool force_tip_to_roof = roof_enabled && m_support_params.soluble_interface; + const auto belt_floor_mode = m_print_config->belt_support_floor_mode.value; + const bool has_belt_floor = std::abs(m_slicing_params.belt_floor_shear_factor) > EPSILON + && belt_floor_mode == BeltSupportFloorMode::GeneratorOnly; //First generate grid points to cover the entire area of the print. BoundingBox bounding_box = m_object->bounding_box(); @@ -3257,6 +3331,10 @@ void TreeSupport::generate_contact_points() auto insert_point = [&](Point pt, const ExPolygon& overhang, double radius, bool force_add = false, bool add_interface=true) { + // Belt floor: skip contact points whose bottom_z is at or below + // the belt floor at this XY position (overhang rests on the belt). + if (has_belt_floor && bottom_z <= belt_floor_print_z(pt)) + return (SupportNode*) nullptr; Point hash_pos = pt / ((radius_scaled + 1) / 1); SupportNode* contact_node = nullptr; if (force_add || !already_inserted.count(hash_pos)) { @@ -3292,8 +3370,10 @@ void TreeSupport::generate_contact_points() double radius = unscale_(overhang_bounds.radius()); Point candidate = overhang_bounds.center(); SupportNode *contact_node = insert_point(candidate, overhang, radius, true, true); - contact_node->type = ePolygon; - curr_nodes.emplace_back(contact_node); + if (contact_node) { + contact_node->type = ePolygon; + curr_nodes.emplace_back(contact_node); + } } }else{ // otherwise, all nodes should be circle nodes diff --git a/src/libslic3r/Support/TreeSupport.hpp b/src/libslic3r/Support/TreeSupport.hpp index e0446ad5f1..c3bd186df1 100644 --- a/src/libslic3r/Support/TreeSupport.hpp +++ b/src/libslic3r/Support/TreeSupport.hpp @@ -446,6 +446,10 @@ private: bool is_slim = false; bool with_infill = false; + // Belt printer: compute the belt floor print_z at a given XY position (in slicing coords). + // Returns -infinity if belt floor is not active. + double belt_floor_print_z(const Point &pos_slicing) const; + /*! diff --git a/src/libslic3r/Support/TreeSupport3D.cpp b/src/libslic3r/Support/TreeSupport3D.cpp index 2c24b933b2..b551a42ece 100644 --- a/src/libslic3r/Support/TreeSupport3D.cpp +++ b/src/libslic3r/Support/TreeSupport3D.cpp @@ -3366,6 +3366,36 @@ static void generate_support_areas(Print &print, TreeSupport* tree_support, cons // this struct is used to easy retrieve setting. No other function except those in TreeModelVolumes and generate_initial_areas() have knowledge of the existence of multiple meshes being processed. //FIXME this is a copy // Contains config settings to avoid loading them in every function. This was done to improve readability of the code. + // Belt printer: add virtual "belt raft" layers below the object so + // organic branches can extend below the model's first layer and + // terminate at the belt surface instead of creating a flat base at Z=0. + { + PrintObject &po = *print.get_object(processing.second.front()); + const auto &sp = po.slicing_parameters(); + const auto &pcfg = po.print()->config(); + const double sf = sp.belt_floor_shear_factor; + if (std::abs(sf) > EPSILON && std::abs(po.belt_global_z_offset()) > EPSILON + && pcfg.belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) { + // z_shift_local is the belt surface height at Y=0 in local coords. + // Extend below the belt so the base expansion and build-plate + // termination happen inside the belt region and get clipped. + // Use the distance from the pre-shear bbox min Z to the part's + // post-shear min Z, plus 10mm for base expansion headroom. + double bb_min_z = std::abs(po.model_object()->raw_bounding_box().min.z()); + double extra_depth = bb_min_z + 10.; + int num_extra = std::max(0, (int)std::ceil(extra_depth / sp.layer_height)); + if (num_extra > 0) { + // Insert belt raft layers at the front, from lowest Z to highest. + std::vector belt_layers; + belt_layers.reserve(num_extra); + for (int i = num_extra; i >= 1; --i) + belt_layers.push_back(sp.first_object_layer_height - i * sp.layer_height); + // Prepend to existing raft_layers (if any). + auto &rl = processing.first.raft_layers; + rl.insert(rl.begin(), belt_layers.begin(), belt_layers.end()); + } + } + } const TreeSupportSettings &config = processing.first; BOOST_LOG_TRIVIAL(info) << "Processing support tree mesh group " << counter + 1 << " of " << grouped_meshes.size() << " containing " << grouped_meshes[counter].second.size() << " meshes."; auto t_start = std::chrono::high_resolution_clock::now(); @@ -3561,6 +3591,43 @@ static void generate_support_areas(Print &print, TreeSupport* tree_support, cons if (layer) layer->polygons = intersection(layer->polygons, volumes.m_bed_area); }); + // Belt floor: clip ALL organic support layers (including intermediate/base + // fill) against the belt surface. The branch slices were already clipped + // in organic_draw_branches(), but intermediate layers generated between + // branches and the build plate need clipping too. + // Compute the belt floor polygon directly from each layer's print_z + // rather than mapping to a layer index (avoids index mismatch issues). + { + const auto &sp = print_object.slicing_parameters(); + const double sf = sp.belt_floor_shear_factor; + const double z_shift = sp.belt_floor_z_shift - print_object.belt_global_z_offset(); + const double floor_off = print_object.print()->config().belt_support_floor_offset.value; + const int from_axis = sp.belt_floor_from_axis; + if (std::abs(sf) > EPSILON + && print_object.print()->config().belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) { + tbb::parallel_for_each(layers_sorted.begin(), layers_sorted.end(), [&](SupportGeneratorLayer *layer) { + if (!layer || layer->polygons.empty()) + return; + double cutoff = (layer->print_z - z_shift - floor_off) / sf; + coord_t cutoff_sc = scale_(cutoff); + coord_t big = scale_(1e4); + Polygon belt_poly; + if (from_axis == 0) { + if (sf > 0) + belt_poly.points = {{cutoff_sc,-big},{big,-big},{big,big},{cutoff_sc,big}}; + else + belt_poly.points = {{-big,-big},{cutoff_sc,-big},{cutoff_sc,big},{-big,big}}; + } else { + if (sf > 0) + belt_poly.points = {{-big,cutoff_sc},{big,cutoff_sc},{big,big},{-big,big}}; + else + belt_poly.points = {{-big,-big},{big,-big},{big,cutoff_sc},{-big,cutoff_sc}}; + } + layer->polygons = diff(layer->polygons, Polygons{belt_poly}); + }); + } + } + print.set_status(69, _L("Generating support")); generate_support_toolpaths(print_object.support_layers(), print_object.config(), support_params, print_object.slicing_parameters(), raft_layers, bottom_contacts, top_contacts, intermediate_layers, interface_layers, base_interface_layers); @@ -3816,6 +3883,10 @@ void organic_draw_branches( for (LayerIndex i = 0; i < LayerIndex(slices.size()); ++i) { slices[i] = diff_clipped(slices[i], volumes.getCollision(0, layer_begin + i, true)); // FIXME parent_uses_min || draw_area.element->state.use_min_xy_dist); slices[i] = intersection(slices[i], volumes.m_bed_area); + // Belt floor: clip branch slices against the belt surface plane. + LayerIndex belt_idx = layer_begin + i; + if (belt_idx < LayerIndex(volumes.m_belt_floor.size()) && !volumes.m_belt_floor[belt_idx].empty()) + slices[i] = diff(slices[i], volumes.m_belt_floor[belt_idx]); } size_t num_empty = 0; if (slices.front().empty()) { @@ -3850,6 +3921,9 @@ void organic_draw_branches( //double support_area_min = 0.1 * support_area_min_radius; for (LayerIndex layer_idx = layer_begin - 1; layer_idx >= layer_bottommost; -- layer_idx) { rest_support = diff_clipped(rest_support.empty() ? slices.front() : rest_support, volumes.getCollision(0, layer_idx, false)); + // Belt floor: clip propagated support at belt surface. + if (layer_idx < LayerIndex(volumes.m_belt_floor.size()) && !volumes.m_belt_floor[layer_idx].empty()) + rest_support = diff(rest_support, volumes.m_belt_floor[layer_idx]); double rest_support_area = area(rest_support); if (rest_support_area < support_area_stop) // Don't propagate a fraction of the tree contact surface. diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 25b4ba4e9c..b9fe17ff2a 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -2210,17 +2210,11 @@ void GCodeViewer::render_toolpaths() { const Camera& camera = wxGetApp().plater()->get_camera(); Matrix4f view = camera.get_view_matrix().matrix().cast(); - // Belt "designed" view: apply inverse shear to view matrix so toolpaths appear - // upright (as originally designed) instead of sheared on the belt. - if (m_belt_show_designed && m_belt_view_enabled && m_belt_angle_deg > 0.f) { - double angle_rad = Geometry::deg2rad(static_cast(m_belt_angle_deg)); - double sin_a = std::sin(angle_rad); - if (sin_a > 1e-6) { - double cot_alpha = std::cos(angle_rad) / sin_a; - Transform3d inverse_shear = Transform3d::Identity(); - inverse_shear.matrix()(1, 2) = -cot_alpha; // Y -= Z * cot(α) - view = (camera.get_view_matrix() * inverse_shear).matrix().cast(); - } + // Belt "designed" view: apply the precomputed inverse of the full belt + // shear+scale transform so toolpaths appear upright (as originally designed) + // instead of transformed on the belt. + if (m_belt_show_designed && m_belt_view_enabled) { + view = (camera.get_view_matrix() * m_belt_inverse_transform).matrix().cast(); } const libvgcode::Mat4x4 converted_view_matrix = libvgcode::convert(view); const libvgcode::Mat4x4 converted_projetion_matrix = libvgcode::convert(static_cast(camera.get_projection_matrix().matrix().cast())); @@ -4418,12 +4412,18 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv if (m_nozzle_nums > 1 && (m_viewer.get_view_type() == libvgcode::EViewType::Summary || m_viewer.get_view_type() == libvgcode::EViewType::ColorPrint)) // ORCA show only on summary and filament tab render_legend_color_arr_recommen(window_padding); - // Belt printer: toggle for viewing raw slicing-frame G-code + // Belt printer: toggle for viewing designed (upright) vs. machine-frame G-code. + // Rendered with a separator and hint text so users can find it easily. if (m_belt_view_enabled) { + ImGui::Spacing(); + ImGui::Separator(); ImGui::Spacing(); ImGui::Dummy({ window_padding, 0 }); ImGui::SameLine(); - ImGui::Checkbox("Show designed view (upright)", &m_belt_show_designed); + ImGui::TextColored(ImVec4(0.f, 0.59f, 0.53f, 1.f), "%s", _u8L("Belt Printer").c_str()); + ImGui::Dummy({ window_padding, 0 }); + ImGui::SameLine(); + ImGui::Checkbox(_u8L("Show designed view (upright) [B]").c_str(), &m_belt_show_designed); } legend_height = ImGui::GetCurrentWindow()->Size.y; diff --git a/src/slic3r/GUI/GCodeViewer.hpp b/src/slic3r/GUI/GCodeViewer.hpp index 184c56e915..cd97613b2a 100644 --- a/src/slic3r/GUI/GCodeViewer.hpp +++ b/src/slic3r/GUI/GCodeViewer.hpp @@ -240,6 +240,7 @@ mutable bool m_no_render_path { false }; bool m_belt_view_enabled = false; float m_belt_angle_deg = 0.f; bool m_belt_show_designed = false; // Toggle: show designed (upright) view via inverse shear + Transform3d m_belt_inverse_transform{Transform3d::Identity()}; libvgcode::Viewer m_viewer; bool m_loaded_as_preview{ false }; @@ -341,7 +342,10 @@ public: void export_toolpaths_to_obj(const char* filename) const; void set_belt_printer(bool enabled, float angle_deg) { m_belt_view_enabled = enabled; m_belt_angle_deg = angle_deg; } + void set_belt_inverse_transform(const Transform3d& t) { m_belt_inverse_transform = t; } bool is_belt_view() const { return m_belt_view_enabled && m_belt_angle_deg > 0.f; } + void toggle_belt_show_designed() { if (m_belt_view_enabled) m_belt_show_designed = !m_belt_show_designed; } + bool is_belt_show_designed() const { return m_belt_show_designed; } size_t get_extruders_count() { return m_extruders_count; } void push_combo_style(); diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index a19bed05a8..3d964c7dab 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -3463,8 +3463,16 @@ void GLCanvas3D::on_char(wxKeyEvent& evt) post_event(SimpleEvent(EVT_GLCANVAS_ARRANGE)); break; } - //case 'B': - //case 'b': { zoom_to_bed(); break; } + case 'B': + case 'b': { + // Toggle belt printer "show designed" view when in G-code preview with belt mode active. + if (dynamic_cast(m_canvas->GetParent()) != nullptr && + m_gcode_viewer.is_belt_view()) { + m_gcode_viewer.toggle_belt_show_designed(); + m_dirty = true; + } + break; + } case 'C': case 'c': { wxGetApp().toggle_show_gcode_window(); m_dirty = true; request_extra_frame(); break; } //case 'G': diff --git a/src/slic3r/GUI/GUI_ObjectList.cpp b/src/slic3r/GUI/GUI_ObjectList.cpp index b20e26653a..ea7b40893f 100644 --- a/src/slic3r/GUI/GUI_ObjectList.cpp +++ b/src/slic3r/GUI/GUI_ObjectList.cpp @@ -2516,7 +2516,7 @@ void ObjectList::load_mesh_object(const TriangleMesh &mesh, const wxString &name Slic3r::save_object_mesh(*new_object); // BBS: find an empty cell to put the copied object - auto start_point = wxGetApp().plater()->build_volume().bounding_volume2d().center(); + auto start_point = wxGetApp().plater()->build_volume().bed_center(); auto empty_cell = wxGetApp().plater()->canvas3D()->get_nearest_empty_cell({start_point(0), start_point(1)}); new_object->instances[0]->set_offset(center ? to_3d(Vec2d(empty_cell(0), empty_cell(1)), -new_object->origin_translation.z()) : bb.center()); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 373ee2a3dc..ee235d4192 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -6907,7 +6907,7 @@ std::vector Plater::priv::load_model_objects(const ModelObjectPtrs& mode // BBS: find an empty cell to put the copied object for (auto& instance : new_instances) { auto offset = instance->get_offset(); - auto start_point = this->bed.build_volume().bounding_volume2d().center(); + auto start_point = this->bed.build_volume().bed_center(); bool plate_empty = partplate_list.get_curr_plate()->empty(); Vec3d displacement; if (plate_empty) @@ -10991,10 +10991,83 @@ void Plater::priv::set_bed_shape(const Pointfs &shape, bed.set_belt_printer(true, static_cast(belt_angle)); if (preview) preview->get_canvas3d()->get_gcode_viewer().set_belt_printer(true, static_cast(belt_angle)); + + // Compute the inverse of the full belt shear+scale transform for the G-code viewer. + auto compute_shear_factor = [](BeltShearMode mode, double angle_deg) -> double { + double angle_rad = Geometry::deg2rad(angle_deg); + double sin_a = std::sin(angle_rad); + double cos_a = std::cos(angle_rad); + switch (mode) { + case BeltShearMode::PosCot: return (sin_a > EPSILON) ? cos_a / sin_a : 0.; + case BeltShearMode::NegCot: return (sin_a > EPSILON) ? -cos_a / sin_a : 0.; + case BeltShearMode::PosTan: return (cos_a > EPSILON) ? sin_a / cos_a : 0.; + case BeltShearMode::NegTan: return (cos_a > EPSILON) ? -sin_a / cos_a : 0.; + default: return 0.; + } + }; + auto compute_scale_factor = [](BeltScaleMode mode, double angle_deg) -> double { + if (mode == BeltScaleMode::None) return 1.; + double angle_rad = Geometry::deg2rad(angle_deg); + double sin_a = std::sin(angle_rad); + double cos_a = std::cos(angle_rad); + switch (mode) { + case BeltScaleMode::InvSin: return (sin_a > EPSILON) ? 1. / sin_a : 1.; + case BeltScaleMode::InvCos: return (cos_a > EPSILON) ? 1. / cos_a : 1.; + case BeltScaleMode::Sin: return sin_a; + case BeltScaleMode::Cos: return cos_a; + default: return 1.; + } + }; + + // Read shear configs. + auto get_shear_mode = [this](const char *key) -> BeltShearMode { + auto opt = config->option>(key); + return opt ? opt->value : BeltShearMode::None; + }; + auto get_axis = [this](const char *key) -> BeltAxis { + auto opt = config->option>(key); + return opt ? opt->value : BeltAxis::X; + }; + auto get_scale_mode = [this](const char *key) -> BeltScaleMode { + auto opt = config->option>(key); + return opt ? opt->value : BeltScaleMode::None; + }; + + struct AxisShear { BeltShearMode mode; double angle; int from; }; + AxisShear axes[3] = { + { get_shear_mode("belt_shear_x"), config->opt_float("belt_shear_x_angle"), int(get_axis("belt_shear_x_from")) }, + { get_shear_mode("belt_shear_y"), config->opt_float("belt_shear_y_angle"), int(get_axis("belt_shear_y_from")) }, + { get_shear_mode("belt_shear_z"), config->opt_float("belt_shear_z_angle"), int(get_axis("belt_shear_z_from")) }, + }; + + Transform3d belt_shear = Transform3d::Identity(); + for (int row = 0; row < 3; ++row) { + if (axes[row].mode != BeltShearMode::None) { + double factor = compute_shear_factor(axes[row].mode, axes[row].angle); + if (std::abs(factor) > EPSILON) + belt_shear.matrix()(row, axes[row].from) += factor; + } + } + + double sx = compute_scale_factor(get_scale_mode("belt_scale_x"), config->opt_float("belt_scale_x_angle")); + double sy = compute_scale_factor(get_scale_mode("belt_scale_y"), config->opt_float("belt_scale_y_angle")); + double sz = compute_scale_factor(get_scale_mode("belt_scale_z"), config->opt_float("belt_scale_z_angle")); + Transform3d belt_scale = Transform3d::Identity(); + belt_scale.matrix()(0, 0) = sx; + belt_scale.matrix()(1, 1) = sy; + belt_scale.matrix()(2, 2) = sz; + + // Forward transform: scale * shear. Inverse for the viewer. + Transform3d forward = belt_scale * belt_shear; + Transform3d inverse = forward.inverse(); + if (preview) + preview->get_canvas3d()->get_gcode_viewer().set_belt_inverse_transform(inverse); } else { bed.set_belt_printer(false, 0.f); - if (preview) + if (preview) { preview->get_canvas3d()->get_gcode_viewer().set_belt_printer(false, 0.f); + preview->get_canvas3d()->get_gcode_viewer().set_belt_inverse_transform(Transform3d::Identity()); + } } } @@ -13286,6 +13359,15 @@ void Plater::load_gcode(const wxString& filename) current_print.set_gcode_file_ready(); + // Belt printer: detect belt_printer_angle from loaded G-code header and enable + // belt view mode on the GCodeViewer so the "Show designed view" toggle appears. + if (current_result->belt_printer_angle > 0.f) { + float angle = current_result->belt_printer_angle; + p->preview->get_canvas3d()->get_gcode_viewer().set_belt_printer(true, angle); + } else { + p->preview->get_canvas3d()->get_gcode_viewer().set_belt_printer(false, 0.f); + } + // show results p->preview->reload_print(m_only_gcode); //BBS: zoom to bed 0 for gcode preview diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 87e97c5ff7..e6de38df78 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -4364,58 +4364,6 @@ void TabPrinter::build_fff() optgroup->append_single_option_line(option, "printer_basic_information_printable_space#excluded-bed-area"); // optgroup->append_single_option_line("printable_area"); optgroup->append_single_option_line("printable_height", "printer_basic_information_printable_space#printable-height"); - optgroup->append_single_option_line("build_plate_tilt_x"); - optgroup->append_single_option_line("build_plate_tilt_y"); - optgroup->append_single_option_line("belt_printer"); - optgroup->append_single_option_line("belt_printer_angle"); - optgroup->append_single_option_line("belt_printer_infinite_y"); - // Per-axis shear: group mode + angle + source on one row per axis - { - Line line = { L("Shear X"), L("Shear applied to the X axis before slicing") }; - line.append_option(optgroup->get_option("belt_shear_x")); - line.append_option(optgroup->get_option("belt_shear_x_angle")); - line.append_option(optgroup->get_option("belt_shear_x_from")); - optgroup->append_line(line); - } - { - Line line = { L("Shear Y"), L("Shear applied to the Y axis before slicing") }; - line.append_option(optgroup->get_option("belt_shear_y")); - line.append_option(optgroup->get_option("belt_shear_y_angle")); - line.append_option(optgroup->get_option("belt_shear_y_from")); - optgroup->append_line(line); - } - { - Line line = { L("Shear Z"), L("Shear applied to the Z axis before slicing") }; - line.append_option(optgroup->get_option("belt_shear_z")); - line.append_option(optgroup->get_option("belt_shear_z_angle")); - line.append_option(optgroup->get_option("belt_shear_z_from")); - optgroup->append_line(line); - } - { - Line line = { L("Scale X"), L("Scale applied to the X axis before slicing") }; - line.append_option(optgroup->get_option("belt_scale_x")); - line.append_option(optgroup->get_option("belt_scale_x_angle")); - optgroup->append_line(line); - } - { - Line line = { L("Scale Y"), L("Scale applied to the Y axis before slicing") }; - line.append_option(optgroup->get_option("belt_scale_y")); - line.append_option(optgroup->get_option("belt_scale_y_angle")); - optgroup->append_line(line); - } - { - Line line = { L("Scale Z"), L("Scale applied to the Z axis before slicing") }; - line.append_option(optgroup->get_option("belt_scale_z")); - line.append_option(optgroup->get_option("belt_scale_z_angle")); - optgroup->append_line(line); - } - { - Line line = { L("G-code axis remap"), L("Remap slicing-frame axes to machine axes in G-code output") }; - line.append_option(optgroup->get_option("belt_gcode_remap_x")); - line.append_option(optgroup->get_option("belt_gcode_remap_y")); - line.append_option(optgroup->get_option("belt_gcode_remap_z")); - optgroup->append_line(line); - } optgroup->append_single_option_line("support_multi_bed_types","printer_basic_information_printable_space#support-multi-bed-types"); optgroup->append_single_option_line("best_object_pos", "printer_basic_information_printable_space#best-object-position"); // todo: for multi_extruder test @@ -4434,6 +4382,68 @@ void TabPrinter::build_fff() //option.opt.full_width = true; //optgroup->append_single_option_line(option); optgroup->append_single_option_line("disable_m73", "printer_basic_information_advanced#disable-set-remaining-print-time"); + optgroup->append_single_option_line("build_plate_tilt_x"); + optgroup->append_single_option_line("build_plate_tilt_y"); + optgroup->append_single_option_line("belt_printer"); + optgroup->append_single_option_line("belt_printer_angle"); + optgroup->append_single_option_line("belt_printer_infinite_y"); + // Per-axis shear: group mode + angle + source on one row per axis + { + Line line = { L("Mesh shear X"), L("Shear applied to the X axis before slicing") }; + line.append_option(optgroup->get_option("belt_shear_x")); + line.append_option(optgroup->get_option("belt_shear_x_angle")); + line.append_option(optgroup->get_option("belt_shear_x_from")); + line.append_option(optgroup->get_option("belt_shear_x_global")); + optgroup->append_line(line); + } + { + Line line = { L("Mesh shear Y"), L("Shear applied to the Y axis before slicing") }; + line.append_option(optgroup->get_option("belt_shear_y")); + line.append_option(optgroup->get_option("belt_shear_y_angle")); + line.append_option(optgroup->get_option("belt_shear_y_from")); + line.append_option(optgroup->get_option("belt_shear_y_global")); + optgroup->append_line(line); + } + { + Line line = { L("Mesh shear Z"), L("Shear applied to the Z axis before slicing") }; + line.append_option(optgroup->get_option("belt_shear_z")); + line.append_option(optgroup->get_option("belt_shear_z_angle")); + line.append_option(optgroup->get_option("belt_shear_z_from")); + line.append_option(optgroup->get_option("belt_shear_z_global")); + optgroup->append_line(line); + } + { + Line line = { L("Mesh scale X"), L("Scale applied to the X axis before slicing") }; + line.append_option(optgroup->get_option("belt_scale_x")); + line.append_option(optgroup->get_option("belt_scale_x_angle")); + optgroup->append_line(line); + } + { + Line line = { L("Mesh scale Y"), L("Scale applied to the Y axis before slicing") }; + line.append_option(optgroup->get_option("belt_scale_y")); + line.append_option(optgroup->get_option("belt_scale_y_angle")); + optgroup->append_line(line); + } + { + Line line = { L("Mesh scale Z"), L("Scale applied to the Z axis before slicing") }; + line.append_option(optgroup->get_option("belt_scale_z")); + line.append_option(optgroup->get_option("belt_scale_z_angle")); + optgroup->append_line(line); + } + { + Line line = { L("G-code axis remap"), L("Remap slicing-frame axes to machine axes in G-code output") }; + line.append_option(optgroup->get_option("belt_gcode_remap_x")); + line.append_option(optgroup->get_option("belt_gcode_remap_y")); + line.append_option(optgroup->get_option("belt_gcode_remap_z")); + optgroup->append_line(line); + } + { + Line line = { L("Support floor"), L("Belt floor awareness for support generation and clipping") }; + line.append_option(optgroup->get_option("belt_support_floor_mode")); + line.append_option(optgroup->get_option("belt_support_floor_offset")); + line.append_option(optgroup->get_option("belt_support_z_offset_mode")); + optgroup->append_line(line); + } option = optgroup->get_option("thumbnails"); option.opt.full_width = true; optgroup->append_single_option_line(option, "printer_basic_information_advanced#g-code-thumbnails"); @@ -5283,6 +5293,33 @@ void TabPrinter::toggle_options() "belt_scale_x", "belt_scale_y", "belt_scale_z", "belt_gcode_remap_x"}) toggle_line(el, is_belt); + + // Gray out angle/from sub-options when their parent shear/scale mode is None. + auto sx = m_config->option>("belt_shear_x")->value; + toggle_option("belt_shear_x_angle", is_belt && sx != BeltShearMode::None); + toggle_option("belt_shear_x_from", is_belt && sx != BeltShearMode::None); + toggle_option("belt_shear_x_global", is_belt && sx != BeltShearMode::None); + + auto sy = m_config->option>("belt_shear_y")->value; + toggle_option("belt_shear_y_angle", is_belt && sy != BeltShearMode::None); + toggle_option("belt_shear_y_from", is_belt && sy != BeltShearMode::None); + toggle_option("belt_shear_y_global", is_belt && sy != BeltShearMode::None); + + auto sz = m_config->option>("belt_shear_z")->value; + toggle_option("belt_shear_z_angle", is_belt && sz != BeltShearMode::None); + toggle_option("belt_shear_z_from", is_belt && sz != BeltShearMode::None); + toggle_option("belt_shear_z_global", is_belt && sz != BeltShearMode::None); + + auto scx = m_config->option>("belt_scale_x")->value; + toggle_option("belt_scale_x_angle", is_belt && scx != BeltScaleMode::None); + + auto scy = m_config->option>("belt_scale_y")->value; + toggle_option("belt_scale_y_angle", is_belt && scy != BeltScaleMode::None); + + auto scz = m_config->option>("belt_scale_z")->value; + toggle_option("belt_scale_z_angle", is_belt && scz != BeltScaleMode::None); + + toggle_line("belt_support_floor_mode", is_belt); }