diff --git a/README.md b/README.md index 01dcbc12a2..15b4e622d0 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ You can download Orca Slicer here: [github releases page](https://github.com/Sof - Auto calibrations for all printers - Sandwich(inner-outer-inner) mode - an improved version of the `External perimeters first` mode - Precise wall +- Polyholes conversion support [SuperSlicer Wiki: Polyholes](https://github.com/supermerill/SuperSlicer/wiki/Polyholes) - Klipper support - More granular controls - More features can be found in [change notes](https://github.com/SoftFever/OrcaSlicer/releases/) diff --git a/src/libslic3r/Point.hpp b/src/libslic3r/Point.hpp index 92a00df9e2..dacf49b080 100644 --- a/src/libslic3r/Point.hpp +++ b/src/libslic3r/Point.hpp @@ -209,6 +209,8 @@ public: double ccw_angle(const Point &p1, const Point &p2) const; Point projection_onto(const MultiPoint &poly) const; Point projection_onto(const Line &line) const; + + double distance_to(const Point &point) const { return (point - *this).cast().norm(); } }; inline bool operator<(const Point &l, const Point &r) diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 6fc81cb306..aee4117fd1 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -765,8 +765,8 @@ static std::vector s_Preset_print_options { "initial_layer_travel_speed", "exclude_object", "slow_down_layers", "infill_anchor", "infill_anchor_max","initial_layer_min_bead_width", "make_overhang_printable", "make_overhang_printable_angle", "make_overhang_printable_hole_size" ,"notes", "wipe_tower_cone_angle", "wipe_tower_extra_spacing", "wipe_tower_extruder", "wiping_volumes_extruders","wipe_tower_bridging", "single_extruder_multi_material_priming", - "wipe_tower_rotation_angle", "tree_support_branch_distance_organic", "tree_support_branch_diameter_organic", "tree_support_branch_angle_organic" - + "wipe_tower_rotation_angle", "tree_support_branch_distance_organic", "tree_support_branch_diameter_organic", "tree_support_branch_angle_organic", + "hole_to_polyhole", "hole_to_polyhole_threshold", "hole_to_polyhole_twisted" }; static std::vector s_Preset_filament_options { diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index 102f93ed0e..3f08414ddf 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -480,6 +480,8 @@ private: void detect_overhangs_for_lift(); void clear_overhangs_for_lift(); + void _transform_hole_to_polyholes(); + // Has any support (not counting the raft). void detect_surfaces_type(); void process_external_surfaces(); diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 5759fdfa2c..88472ad0e4 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -4153,6 +4153,34 @@ def = this->add("filament_loading_speed", coFloats); def->mode = comAdvanced; def->set_default_value(new ConfigOptionFloat(0)); + def = this->add("hole_to_polyhole", coBool); + def->label = L("Convert holes to polyholes"); + def->category = L("Quality"); + def->tooltip = L("Search for almost-circular holes that span more than one layer and convert the geometry to polyholes." + " Use the nozzle size and the (biggest) diameter to compute the polyhole." + "\nSee http://hydraraptor.blogspot.com/2011/02/polyholes.html"); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionBool(false)); + + def = this->add("hole_to_polyhole_threshold", coFloatOrPercent); + def->label = L("Polyhole detection margin"); + def->category = L("Quality"); + def->tooltip = L("Maximum defection of a point to the estimated radius of the circle." + "\nAs cylinders are often exported as triangles of varying size, points may not be on the circle circumference." + " This setting allows you some leway to broaden the detection." + "\nIn mm or in % of the radius."); + def->sidetext = L("mm or %"); + def->max_literal = 10; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloatOrPercent(0.01, false)); + + def = this->add("hole_to_polyhole_twisted", coBool); + def->label = L("Polyhole twist"); + def->category = L("Quality"); + def->tooltip = L("Rotate the polyhole every layer."); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionBool(true)); + def = this->add("thumbnails", coPoints); def->label = L("G-code thumbnails"); def->tooltip = L("Picture sizes to be stored into a .gcode and .sl1 / .sl1s files, in the following format: \"XxY, XxY, ...\""); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 7e1d5106cc..367e594e03 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -825,6 +825,9 @@ PRINT_CONFIG_CLASS_DEFINE( ((ConfigOptionBool, make_overhang_printable)) ((ConfigOptionBool, extra_perimeters_on_overhangs)) ((ConfigOptionBool, slowdown_for_curled_perimeters)) + ((ConfigOptionBool, hole_to_polyhole)) + ((ConfigOptionFloatOrPercent, hole_to_polyhole_threshold)) + ((ConfigOptionBool, hole_to_polyhole_twisted)) ) PRINT_CONFIG_CLASS_DEFINE( diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index c66c3406ed..7654dcb5ec 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -152,6 +152,145 @@ std::vector> PrintObject::all_regions( return out; } +Polygons create_polyholes(const Point center, const coord_t radius, const coord_t nozzle_diameter, bool multiple) +{ + // n = max(round(2 * d), 3); // for 0.4mm nozzle + size_t nb_edges = (int)std::max(3, (int)std::round(4.0 * unscaled(radius) * 0.4 / unscaled(nozzle_diameter))); + // cylinder(h = h, r = d / cos (180 / n), $fn = n); + //create x polyholes by rotation if multiple + int nb_polyhole = 1; + float rotation = 0; + if (multiple) { + nb_polyhole = 5; + rotation = 2 * float(PI) / (nb_edges * nb_polyhole); + } + Polygons list; + for (int i_poly = 0; i_poly < nb_polyhole; i_poly++) + list.emplace_back(); + for (int i_poly = 0; i_poly < nb_polyhole; i_poly++) { + Polygon& pts = (((i_poly % 2) == 0) ? list[i_poly / 2] : list[(nb_polyhole + 1) / 2 + i_poly / 2]); + const float new_radius = radius / float(std::cos(PI / nb_edges)); + for (size_t i_edge = 0; i_edge < nb_edges; ++i_edge) { + float angle = rotation * i_poly + (float(PI) * 2 * (float)i_edge) / nb_edges; + pts.points.emplace_back(center.x() + new_radius * cos(angle), center.y() + new_radius * sin(angle)); + } + pts.make_clockwise(); + } + //alternate + return list; +} + +// Detect and convert holes to polyholes, implementation is ported from SuperSlicer +void PrintObject::_transform_hole_to_polyholes() +{ + // get all circular holes for each layer + // the id is center-diameter-extruderid + //the tuple is Point center; float diameter_max; int extruder_id; coord_t max_variation; bool twist; + std::vector, Polygon*>>> layerid2center; + for (size_t i = 0; i < this->m_layers.size(); i++) layerid2center.emplace_back(); + tbb::parallel_for( + tbb::blocked_range(0, m_layers.size()), + [this, &layerid2center](const tbb::blocked_range& range) { + for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++layer_idx) { + m_print->throw_if_canceled(); + Layer* layer = m_layers[layer_idx]; + for (size_t region_idx = 0; region_idx < layer->m_regions.size(); ++region_idx) + { + if (layer->m_regions[region_idx]->region().config().hole_to_polyhole) { + for (Surface& surf : layer->m_regions[region_idx]->slices.surfaces) { + for (Polygon& hole : surf.expolygon.holes) { + //test if convex (as it's clockwise bc it's a hole, we have to do the opposite) + if (hole.convex_points(PI).empty() && hole.points.size() > 8) { + // Computing circle center + Point center = hole.centroid(); + double diameter_min = std::numeric_limits::max(), diameter_max = 0; + double diameter_sum = 0; + for (int i = 0; i < hole.points.size(); ++i) { + double dist = hole.points[i].distance_to(center); + diameter_min = std::min(diameter_min, dist); + diameter_max = std::max(diameter_max, dist); + diameter_sum += dist; + } + //also use center of lines to check it's not a rectangle + double diameter_line_min = std::numeric_limits::max(), diameter_line_max = 0; + Lines hole_lines = hole.lines(); + for (Line l : hole_lines) { + Point midline = (l.a + l.b) / 2; + double dist = center.distance_to(midline); + diameter_line_min = std::min(diameter_line_min, dist); + diameter_line_max = std::max(diameter_line_max, dist); + } + + + // SCALED_EPSILON was a bit too harsh. Now using a config, as some may want some harsh setting and some don't. + coord_t max_variation = std::max(SCALED_EPSILON, scale_(this->m_layers[layer_idx]->m_regions[region_idx]->region().config().hole_to_polyhole_threshold.get_abs_value(unscaled(diameter_sum / hole.points.size())))); + bool twist = this->m_layers[layer_idx]->m_regions[region_idx]->region().config().hole_to_polyhole_twisted.value; + if (diameter_max - diameter_min < max_variation * 2 && diameter_line_max - diameter_line_min < max_variation * 2) { + layerid2center[layer_idx].emplace_back( + std::tuple{center, diameter_max, layer->m_regions[region_idx]->region().config().wall_filament.value, max_variation, twist}, & hole); + } + } + } + } + } + } + // for layer->slices, it will be also replaced later. + } + }); + //sort holes per center-diameter + std::map, std::vector>> id2layerz2hole; + + //search & find hole that span at least X layers + const size_t min_nb_layers = 2; + for (size_t layer_idx = 0; layer_idx < this->m_layers.size(); ++layer_idx) { + for (size_t hole_idx = 0; hole_idx < layerid2center[layer_idx].size(); ++hole_idx) { + //get all other same polygons + std::tuple& id = layerid2center[layer_idx][hole_idx].first; + float max_z = layers()[layer_idx]->print_z; + std::vector> holes; + holes.emplace_back(layerid2center[layer_idx][hole_idx].second, layer_idx); + for (size_t search_layer_idx = layer_idx + 1; search_layer_idx < this->m_layers.size(); ++search_layer_idx) { + if (layers()[search_layer_idx]->print_z - layers()[search_layer_idx]->height - max_z > EPSILON) break; + //search an other polygon with same id + for (size_t search_hole_idx = 0; search_hole_idx < layerid2center[search_layer_idx].size(); ++search_hole_idx) { + std::tuple& search_id = layerid2center[search_layer_idx][search_hole_idx].first; + if (std::get<2>(id) == std::get<2>(search_id) + && std::get<0>(id).distance_to(std::get<0>(search_id)) < std::get<3>(id) + && std::abs(std::get<1>(id) - std::get<1>(search_id)) < std::get<3>(id) + ) { + max_z = layers()[search_layer_idx]->print_z; + holes.emplace_back(layerid2center[search_layer_idx][search_hole_idx].second, search_layer_idx); + layerid2center[search_layer_idx].erase(layerid2center[search_layer_idx].begin() + search_hole_idx); + search_hole_idx--; + break; + } + } + } + //check if strait hole or first layer hole (cause of first layer compensation) + if (holes.size() >= min_nb_layers || (holes.size() == 1 && holes[0].second == 0)) { + id2layerz2hole.emplace(std::move(id), std::move(holes)); + } + } + } + //create a polyhole per id and replace holes points by it. + for (auto entry : id2layerz2hole) { + Polygons polyholes = create_polyholes(std::get<0>(entry.first), std::get<1>(entry.first), scale_(print()->config().nozzle_diameter.get_at(std::get<2>(entry.first) - 1)), std::get<4>(entry.first)); + for (auto& poly_to_replace : entry.second) { + Polygon polyhole = polyholes[poly_to_replace.second % polyholes.size()]; + //search the clone in layers->slices + for (ExPolygon& explo_slice : m_layers[poly_to_replace.second]->lslices) { + for (Polygon& poly_slice : explo_slice.holes) { + if (poly_slice.points == poly_to_replace.first->points) { + poly_slice.points = polyhole.points; + } + } + } + // copy + poly_to_replace.first->points = polyhole.points; + } + } +} + // 1) Merges typed region slices into stInternal type. // 2) Increases an "extra perimeters" counter at region slices where needed. // 3) Generates perimeters, gap fills and fill regions (fill regions of type stInternal). @@ -816,6 +955,9 @@ bool PrintObject::invalidate_state_by_config_options( || opt_key == "max_bridge_length" || opt_key == "support_interface_top_layers" || opt_key == "support_critical_regions_only" + || opt_key == "hole_to_polyhole" + || opt_key == "hole_to_polyhole_threshold" + || opt_key == "hole_to_polyhole_twisted" ) { steps.emplace_back(posSlice); } else if (opt_key == "enable_support") { diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index 42443c7408..537be496c4 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -825,6 +825,9 @@ void PrintObject::slice() } #endif + // Detect and process holes that should be converted to polyholes + this->_transform_hole_to_polyholes(); + // BBS: the actual first layer slices stored in layers are re-sorted by volume group and will be used to generate brim groupingVolumesForBrim(this, m_layers, firstLayerReplacedBy); diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp index eeead56563..8b67342c33 100644 --- a/src/slic3r/GUI/ConfigManipulation.cpp +++ b/src/slic3r/GUI/ConfigManipulation.cpp @@ -721,6 +721,9 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, co toggle_line("exclude_object", gcflavor == gcfKlipper); toggle_line("min_width_top_surface",config->opt_bool("only_one_wall_top")); + + for (auto el : { "hole_to_polyhole_threshold", "hole_to_polyhole_twisted" }) + toggle_line(el, config->opt_bool("hole_to_polyhole")); } void ConfigManipulation::update_print_sla_config(DynamicPrintConfig* config, const bool is_global_config/* = false*/) diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 546f5124f0..b3d5156363 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -1870,6 +1870,9 @@ void TabPrint::build() optgroup->append_single_option_line("elefant_foot_compensation"); optgroup->append_single_option_line("elefant_foot_compensation_layers"); optgroup->append_single_option_line("precise_outer_wall"); + optgroup->append_single_option_line("hole_to_polyhole"); + optgroup->append_single_option_line("hole_to_polyhole_threshold"); + optgroup->append_single_option_line("hole_to_polyhole_twisted"); optgroup = page->new_optgroup(L("Ironing"), L"param_ironing"); optgroup->append_single_option_line("ironing_type");