diff --git a/lib/Slic3r/GUI/PresetEditor.pm b/lib/Slic3r/GUI/PresetEditor.pm index 2563940429..085ac9cbd7 100644 --- a/lib/Slic3r/GUI/PresetEditor.pm +++ b/lib/Slic3r/GUI/PresetEditor.pm @@ -439,7 +439,8 @@ sub options { adaptive_slicing adaptive_slicing_quality match_horizontal_surfaces perimeters spiral_vase top_solid_layers min_shell_thickness min_top_bottom_shell_thickness bottom_solid_layers - extra_perimeters avoid_crossing_perimeters thin_walls overhangs + extra_perimeters avoid_crossing_perimeters + thin_walls thin_walls_min_width thin_walls_overlap overhangs seam_position external_perimeters_first fill_density fill_pattern top_infill_pattern bottom_infill_pattern fill_gaps infill_every_layers infill_only_where_needed @@ -540,7 +541,13 @@ sub build { my $optgroup = $page->new_optgroup('Quality (slower slicing)'); $optgroup->append_single_option_line('extra_perimeters'); $optgroup->append_single_option_line('avoid_crossing_perimeters'); - $optgroup->append_single_option_line('thin_walls'); + my $line = Slic3r::GUI::OptionsGroup::Line->new( + label => 'Detect thin walls', + ); + $line->append_option($optgroup->get_option('thin_walls')); + $line->append_option($optgroup->get_option('thin_walls_min_width')); + $line->append_option($optgroup->get_option('thin_walls_overlap')); + $optgroup->append_line($line); $optgroup->append_single_option_line('overhangs'); } { @@ -886,8 +893,9 @@ sub _update { my $have_perimeters = ($config->perimeters > 0) || ($config->min_shell_thickness > 0); if (any { /$opt_key/ } qw(all_keys perimeters)) { $self->get_field($_)->toggle($have_perimeters) - for qw(extra_perimeters thin_walls overhangs seam_position external_perimeters_first - external_perimeter_extrusion_width + for qw(extra_perimeters thin_walls thin_walls_min_width thin_walls_overlap + overhangs seam_position + external_perimeters_first external_perimeter_extrusion_width perimeter_speed small_perimeter_speed external_perimeter_speed); } @@ -970,6 +978,10 @@ sub _update { $self->get_field($_)->toggle($have_support_material && $have_support_interface) for qw(support_material_interface_spacing support_material_interface_extruder support_material_interface_speed); + + # thin walls settigns only when thins walls is activated + $self->get_field($_)->toggle($config->thin_walls) + for qw(thin_walls_min_width thin_walls_overlap); $self->get_field('perimeter_extrusion_width')->toggle($have_perimeters || $have_skirt || $have_brim); $self->get_field('support_material_extruder')->toggle($have_support_material || $have_skirt); diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 3adc0a10c5..2d8f47f685 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -202,6 +202,7 @@ add_library(libslic3r STATIC ${LIBDIR}/libslic3r/LayerHeightSpline.cpp ${LIBDIR}/libslic3r/Line.cpp ${LIBDIR}/libslic3r/Log.cpp + ${LIBDIR}/libslic3r/MedialAxis.cpp ${LIBDIR}/libslic3r/Model.cpp ${LIBDIR}/libslic3r/MotionPlanner.cpp ${LIBDIR}/libslic3r/MultiPoint.cpp diff --git a/src/GUI/Dialogs/PresetEditor.hpp b/src/GUI/Dialogs/PresetEditor.hpp index a6046c10bb..9a7e52983b 100644 --- a/src/GUI/Dialogs/PresetEditor.hpp +++ b/src/GUI/Dialogs/PresetEditor.hpp @@ -143,7 +143,8 @@ class PrintEditor : public PresetEditor { "adaptive_slicing"s, "adaptive_slicing_quality"s, "match_horizontal_surfaces"s, "perimeters"s, "spiral_vase"s, "top_solid_layers"s, "bottom_solid_layers"s, - "extra_perimeters"s, "avoid_crossing_perimeters"s, "thin_walls"s, "overhangs"s, + "extra_perimeters"s, "avoid_crossing_perimeters"s, + "thin_walls"s, "thin_walls_min_width"s, "thin_walls_overlap"s, "overhangs"s, "seam_position"s, "external_perimeters_first"s, "fill_density"s, "fill_pattern"s, "top_infill_pattern"s, "bottom_infill_pattern"s, "fill_gaps"s, "infill_every_layers"s, "infill_only_where_needed"s, diff --git a/t/adaptive_width.t b/t/adaptive_width.t index 7a0baa7527..14ffd3dd4c 100644 --- a/t/adaptive_width.t +++ b/t/adaptive_width.t @@ -32,6 +32,7 @@ sub scale_points (@) { map [scale $_->[X], scale $_->[Y]], @_ } expolygon => $expolygon, )); my $config = Slic3r::Config::Full->new; + $config->set('thin_walls_overlap',0); my $loops = Slic3r::ExtrusionPath::Collection->new; my $gap_fill = Slic3r::ExtrusionPath::Collection->new; my $fill_surfaces = Slic3r::Surface::Collection->new; diff --git a/t/thin.t b/t/thin.t index 2d256d2864..3bacbdfe06 100644 --- a/t/thin.t +++ b/t/thin.t @@ -1,4 +1,4 @@ -use Test::More tests => 23; +use Test::More tests => 29; use strict; use warnings; @@ -102,10 +102,44 @@ if (0) { is scalar(@$res), 1, 'medial axis of a semicircumference is a single line'; # check whether turns are all CCW or all CW - my @lines = @{$res->[0]->lines}; + my @all_lines = @{$res->[0]->lines}; + # remove lines that are near the end. + my @lines = grep($_->a->y >= 1578184 || $_->b->y >= 1578184, @all_lines); my @angles = map { $lines[$_-1]->ccw($lines[$_]->b) } 1..$#lines; ok !!(none { $_ < 0 } @angles) || (none { $_ > 0 } @angles), - 'all medial axis segments of a semicircumference have the same orientation'; + 'all medial axis segments of a semicircumference have the same orientation (but the 2 end points)'; +} + +{ + my $expolygon = Slic3r::ExPolygon->new(Slic3r::Polygon->new_scale( + [4.3, 4], [4.3, 0], [4,0], [4,4], [0,4], [0,4.5], [4,4.5], [4,10], [4.3,10], [4.3, 4.5], + [6, 4.5], [6,10], [6.2,10], [6.2,4.5], [10,4.5], [10,4], [6.2,4], [6.2,0], [6, 0], [6, 4], + )); + $expolygon->contour->make_counter_clockwise(); + my $res = $expolygon->medial_axis(scale 0.55, scale 0.25); + is scalar(@$res), 2, 'medial axis of a (bit too narrow) french cross is two lines'; + ok unscale($res->[0]->length) >= (9.9) - epsilon, 'medial axis has reasonable length'; + ok unscale($res->[1]->length) >= (9.9) - epsilon, 'medial axis has reasonable length'; + + my @lines1 = @{$res->[0]->lines}; + my @angles1 = map { $lines1[$_-1]->ccw($lines1[$_]->b) } 1..$#lines1; + my @lines2 = @{$res->[1]->lines}; + my @angles2 = map { $lines2[$_-1]->ccw($lines2[$_]->b) } 1..$#lines2; + my @angles = (@angles1, @angles2); + ok !!(none { $_ != 0 } @angles), + 'medial axis of a (bit too narrow) french cross is two lines has only strait lines'; +} + +{ + my $expolygon = Slic3r::ExPolygon->new(Slic3r::Polygon->new_scale( + [0.86526705,1.4509841], [0.57696039,1.8637021], [0.4502297,2.5569978], [0.45626199,3.2965596], [1.1218851,3.3049455], [0.96681072,2.8243202], [0.86328971,2.2056997], [0.85367905,1.7790778], + )); + $expolygon->contour->make_counter_clockwise(); + my $res = $expolygon->medial_axis(scale 1, scale 0.25); + is scalar(@$res), 1, 'medial axis of a (bit too narrow) french cross is two lines'; + ok unscale($res->[0]->length) >= (1.4) - epsilon, 'medial axis has reasonable length'; + # TODO: check if min width is < 0.3 and max width is > 0.6 (min($res->[0]->width.front, $res->[0]->width.back) # problem: can't have access to width + } { diff --git a/xs/MANIFEST b/xs/MANIFEST index 4d20158719..3295cbd2c2 100644 --- a/xs/MANIFEST +++ b/xs/MANIFEST @@ -121,6 +121,8 @@ src/libslic3r/libslic3r.h src/libslic3r/Line.cpp src/libslic3r/Line.hpp src/libslic3r/Log.hpp +src/libslic3r/MedialAxis.cpp +src/libslic3r/MedialAxis.hpp src/libslic3r/Model.cpp src/libslic3r/Model.hpp src/libslic3r/MotionPlanner.cpp diff --git a/xs/src/libslic3r/ExPolygon.cpp b/xs/src/libslic3r/ExPolygon.cpp index 0e7a50e498..d1cc295611 100644 --- a/xs/src/libslic3r/ExPolygon.cpp +++ b/xs/src/libslic3r/ExPolygon.cpp @@ -1,5 +1,6 @@ #include "BoundingBox.hpp" #include "ExPolygon.hpp" +#include "MedialAxis.hpp" #include "Geometry.hpp" #include "Polygon.hpp" #include "Line.hpp" @@ -184,251 +185,42 @@ ExPolygon::simplify(double tolerance, ExPolygons* expolygons) const expolygons->insert(expolygons->end(), ep.begin(), ep.end()); } +/// remove point that are at SCALED_EPSILON * 2 distance. +//simplier than simplify void -ExPolygon::medial_axis(const ExPolygon &bounds, double max_width, double min_width, ThickPolylines* polylines) const -{ - // init helper object - Slic3r::Geometry::MedialAxis ma(max_width, min_width, this); - ma.lines = this->lines(); - - // compute the Voronoi diagram and extract medial axis polylines - ThickPolylines pp; - ma.build(&pp); - - /* // Commented out debug code - SVG svg("medial_axis.svg"); - svg.draw(*this); - svg.draw(pp); - svg.Close(); - */ - - // Find the maximum width returned; we're going to use this for validating and - // filtering the output segments. - double max_w = 0; - for (ThickPolylines::const_iterator it = pp.begin(); it != pp.end(); ++it) - max_w = fmaxf(max_w, *std::max_element(it->width.begin(), it->width.end())); - - - // Aligned fusion: Fusion the bits at the end of lines by "increasing thickness" - // For that, we have to find other lines, - // and with a next point no more distant than the max width. - // Then, we can merge the bit from the first point to the second by following the mean. - - bool changes = true; - while (changes) { - changes = false; - for (size_t i = 0; i < pp.size(); ++i) { - ThickPolyline& polyline = pp[i]; - - ThickPolyline* best_candidate = nullptr; - float best_dot = -1; - int best_idx = 0; - - // find another polyline starting here - for (size_t j = i + 1; j < pp.size(); ++j) { - ThickPolyline& other = pp[j]; - if (polyline.last_point().coincides_with(other.last_point())) { - polyline.reverse(); - other.reverse(); - } - else if (polyline.first_point().coincides_with(other.last_point())) { - other.reverse(); - } - else if (polyline.first_point().coincides_with(other.first_point())) { - } - else if (polyline.last_point().coincides_with(other.first_point())) { - polyline.reverse(); - } else { - continue; - } - - //only consider the other if the next point is near us - if (polyline.points.size() < 2 && other.points.size() < 2) continue; - if (!polyline.endpoints.second || !other.endpoints.second) continue; - if (polyline.points.back().distance_to(other.points.back()) > max_width) continue; - if (polyline.points.size() != other.points.size()) continue; - - - Pointf v_poly(polyline.lines().front().vector().x, polyline.lines().front().vector().y); - v_poly.scale(1 / std::sqrt(v_poly.x*v_poly.x + v_poly.y*v_poly.y)); - Pointf v_other(other.lines().front().vector().x, other.lines().front().vector().y); - v_other.scale(1 / std::sqrt(v_other.x*v_other.x + v_other.y*v_other.y)); - float other_dot = v_poly.x*v_other.x + v_poly.y*v_other.y; - if (other_dot > best_dot) { - best_candidate = &other; - best_idx = j; - best_dot = other_dot; - } - } - if (best_candidate != nullptr) { - - //assert polyline.size == best_candidate->size (see selection loop, an 'if' takes care of that) - - //iterate the points - // as voronoi should create symetric thing, we can iterate synchonously - unsigned int idx_point = 1; - while (idx_point < polyline.points.size() && polyline.points[idx_point].distance_to(best_candidate->points[idx_point]) < max_width) { - //fusion - polyline.points[idx_point].x += best_candidate->points[idx_point].x; - polyline.points[idx_point].x /= 2; - polyline.points[idx_point].y += best_candidate->points[idx_point].y; - polyline.points[idx_point].y /= 2; - polyline.width[idx_point] += best_candidate->width[idx_point]; - ++idx_point; - } - //select if an end occur - polyline.endpoints.second &= best_candidate->endpoints.second; - - //remove points that are the same or too close each other, ie simplify - for (unsigned int idx_point = 1; idx_point < polyline.points.size(); ++idx_point) { - //distance of 1 is on the sclaed coordinates, so it correspond to SCALE_FACTOR, so it's very small - if (polyline.points[idx_point - 1].distance_to(polyline.points[idx_point]) < 1) { - if (idx_point < polyline.points.size() -1) { - polyline.points.erase(polyline.points.begin() + idx_point); - } else { - polyline.points.erase(polyline.points.begin() + idx_point -1); - } - --idx_point; - } - } - //remove points that are outside of the geometry - for (unsigned int idx_point = 0; idx_point < polyline.points.size(); ++idx_point) { - //distance of 1 is on the sclaed coordinates, so it correspond to SCALE_FACTOR, so it's very small - if (!bounds.contains_b(polyline.points[idx_point])) { - polyline.points.erase(polyline.points.begin() + idx_point); - --idx_point; - } - } - if (polyline.points.size() < 2) { - //remove self - pp.erase(pp.begin() + i); - --i; - --best_idx; - } - - pp.erase(pp.begin() + best_idx); - changes = true; - } +ExPolygon::remove_point_too_near(const coord_t tolerance) { + size_t id = 1; + while (id < this->contour.points.size() - 1) { + double newdist = min(this->contour.points[id].distance_to(this->contour.points[id - 1]) + , this->contour.points[id].distance_to(this->contour.points[id + 1])); + if (newdist < (double)tolerance) { + this->contour.points.erase(this->contour.points.begin() + id); + newdist = this->contour.points[id].distance_to(this->contour.points[id - 1]); } - } - - // Loop through all returned polylines in order to extend their endpoints to the - // expolygon boundaries - bool removed = false; - for (size_t i = 0; i < pp.size(); ++i) { - ThickPolyline& polyline = pp[i]; - - // extend initial and final segments of each polyline if they're actual endpoints - // Assign new endpoints to temporary variables because in case of a single-line - // polyline. After the start point is extended it will be caught by the intersection() - // call, so keep the inner point until the second intersection() is performed. - Point new_front = polyline.points.front(); - Point new_back = polyline.points.back(); - if (polyline.endpoints.first && !bounds.has_boundary_point(new_front)) { - Line line(polyline.points.front(), polyline.points[1]); - - // prevent the line from touching on the other side, otherwise intersection() might return that solution - if (polyline.points.size() == 2) line.b = line.midpoint(); - - line.extend_start(max_width); - (void)bounds.contour.intersection(line, &new_front); - } - if (polyline.endpoints.second && !bounds.has_boundary_point(new_back)) { - Line line( - *(polyline.points.end() - 2), - polyline.points.back() - ); - - // prevent the line from touching on the other side, otherwise intersection() might return that solution - if (polyline.points.size() == 2) line.a = line.midpoint(); - line.extend_end(max_width); - - (void)bounds.contour.intersection(line, &new_back); + //go to next one + //if you removed a point, it check if the next one isn't too near from the previous one. + // if not, it byepass it. + if (newdist > (double)tolerance) { + ++id; } - polyline.points.front() = new_front; - polyline.points.back() = new_back; } - - // If we removed any short polylines, we now try to connect consecutive polylines - // in order to allow loop detection. Note that this algorithm is greedier than - // MedialAxis::process_edge_neighbors(), as it will connect random pairs of - // polylines even when more than two start from the same point. This has no - // drawbacks since we optimize later using nearest-neighbor which would do the - // same, but should we use a more sophisticated optimization algorithm. - // We should not connect polylines when more than two meet. - // Optimisation of the old algorithm : Select the most "straight line" choice - // when we merge with an other line at a point with more than two meet. - - for (size_t i = 0; i < pp.size(); ++i) { - ThickPolyline& polyline = pp[i]; - if (polyline.endpoints.first && polyline.endpoints.second) continue; // optimization - - ThickPolyline* best_candidate = nullptr; - float best_dot = -1; - int best_idx = 0; - - // find another polyline starting here - for (size_t j = i+1; j < pp.size(); ++j) { - ThickPolyline& other = pp[j]; - if (polyline.last_point().coincides_with(other.last_point())) { - other.reverse(); - } else if (polyline.first_point().coincides_with(other.last_point())) { - polyline.reverse(); - other.reverse(); - } else if (polyline.first_point().coincides_with(other.first_point())) { - polyline.reverse(); - } else if (!polyline.last_point().coincides_with(other.first_point())) { - continue; - } - - Pointf v_poly(polyline.lines().back().vector().x, polyline.lines().back().vector().y); - v_poly.scale(1 / std::sqrt(v_poly.x*v_poly.x + v_poly.y*v_poly.y)); - Pointf v_other(other.lines().front().vector().x, other.lines().front().vector().y); - v_other.scale(1 / std::sqrt(v_other.x*v_other.x + v_other.y*v_other.y)); - float other_dot = v_poly.x*v_other.x + v_poly.y*v_other.y; - if (other_dot > best_dot) { - best_candidate = &other; - best_idx = j; - best_dot = other_dot; - } - } - if (best_candidate != nullptr) { - - polyline.points.insert(polyline.points.end(), best_candidate->points.begin() + 1, best_candidate->points.end()); - polyline.width.insert(polyline.width.end(), best_candidate->width.begin(), best_candidate->width.end()); - polyline.endpoints.second = best_candidate->endpoints.second; - assert(polyline.width.size() == polyline.points.size()*2 - 2); - - pp.erase(pp.begin() + best_idx); - } - } - - for (size_t i = 0; i < pp.size(); ++i) { - ThickPolyline& polyline = pp[i]; - - // remove too short polylines - // (we can't do this check before endpoints extension and clipping because we don't - // know how long will the endpoints be extended since it depends on polygon thickness - // which is variable - extension will be <= max_width/2 on each side) - if ((polyline.endpoints.first || polyline.endpoints.second) - && polyline.length() < max_w * 2) { - pp.erase(pp.begin() + i); - --i; - removed = true; - continue; - } - + if (this->contour.points.front().distance_to(this->contour.points.back()) < tolerance) { + this->contour.points.erase(this->contour.points.end() -1); } +} - - polylines->insert(polylines->end(), pp.begin(), pp.end()); +void +ExPolygon::medial_axis(const ExPolygon &bounds, double max_width, double min_width, ThickPolylines* polylines, double height, bool stop_at_min_width) const { + Slic3r::MedialAxis ma(*this, bounds, max_width, min_width, height); + ma.stop_at_min_width = stop_at_min_width; + ma.build(polylines); } void ExPolygon::medial_axis(double max_width, double min_width, Polylines* polylines) const { ThickPolylines tp; - this->medial_axis(*this, max_width, min_width, &tp); + this->medial_axis(*this, max_width, min_width, &tp, max_width/2.0); polylines->insert(polylines->end(), tp.begin(), tp.end()); } diff --git a/xs/src/libslic3r/ExPolygon.hpp b/xs/src/libslic3r/ExPolygon.hpp index fcdabc2723..cd3ab9ea7e 100644 --- a/xs/src/libslic3r/ExPolygon.hpp +++ b/xs/src/libslic3r/ExPolygon.hpp @@ -42,7 +42,8 @@ class ExPolygon Polygons simplify_p(double tolerance) const; ExPolygons simplify(double tolerance) const; void simplify(double tolerance, ExPolygons* expolygons) const; - void medial_axis(const ExPolygon &bounds, double max_width, double min_width, ThickPolylines* polylines) const; + void remove_point_too_near(const coord_t tolerance); + void medial_axis(const ExPolygon &bounds, double max_width, double min_width, ThickPolylines* polylines, double height, bool stop_at_min_width = true) const; void medial_axis(double max_width, double min_width, Polylines* polylines) const; void get_trapezoids(Polygons* polygons) const; void get_trapezoids(Polygons* polygons, double angle) const; diff --git a/xs/src/libslic3r/Geometry.cpp b/xs/src/libslic3r/Geometry.cpp index b972e7e176..c71bc4bb6a 100644 --- a/xs/src/libslic3r/Geometry.cpp +++ b/xs/src/libslic3r/Geometry.cpp @@ -648,261 +648,5 @@ arrange(size_t total_parts, const Pointf &part_size, coordf_t dist, const Boundi return true; } -void -MedialAxis::build(ThickPolylines* polylines) -{ - construct_voronoi(this->lines.begin(), this->lines.end(), &this->vd); - - /* - // DEBUG: dump all Voronoi edges - { - SVG svg("voronoi.svg"); - svg.draw(*this->expolygon); - for (VD::const_edge_iterator edge = this->vd.edges().begin(); edge != this->vd.edges().end(); ++edge) { - if (edge->is_infinite()) continue; - - ThickPolyline polyline; - polyline.points.push_back(Point( edge->vertex0()->x(), edge->vertex0()->y() )); - polyline.points.push_back(Point( edge->vertex1()->x(), edge->vertex1()->y() )); - polyline.width.push_back(this->max_width); - polyline.width.push_back(this->max_width); - polylines->push_back(polyline); - - svg.draw(polyline); - } - svg.Close(); - return; - } - */ - - // collect valid edges (i.e. prune those not belonging to MAT) - // note: this keeps twins, so it inserts twice the number of the valid edges - this->valid_edges.clear(); - { - std::set seen_edges; - for (VD::const_edge_iterator edge = this->vd.edges().begin(); edge != this->vd.edges().end(); ++edge) { - // if we only process segments representing closed loops, none if the - // infinite edges (if any) would be part of our MAT anyway - if (edge->is_secondary() || edge->is_infinite()) continue; - - // don't re-validate twins - if (seen_edges.find(&*edge) != seen_edges.end()) continue; // TODO: is this needed? - seen_edges.insert(&*edge); - seen_edges.insert(edge->twin()); - - if (!this->validate_edge(&*edge)) continue; - this->valid_edges.insert(&*edge); - this->valid_edges.insert(edge->twin()); - } - } - this->edges = this->valid_edges; - // iterate through the valid edges to build polylines - while (!this->edges.empty()) { - const VD::edge_type* edge = *this->edges.begin(); - - // start a polyline - ThickPolyline polyline; - polyline.points.push_back(Point( edge->vertex0()->x(), edge->vertex0()->y() )); - polyline.points.push_back(Point( edge->vertex1()->x(), edge->vertex1()->y() )); - polyline.width.push_back(this->thickness[edge].first); - polyline.width.push_back(this->thickness[edge].second); - - // remove this edge and its twin from the available edges - (void)this->edges.erase(edge); - (void)this->edges.erase(edge->twin()); - - // get next points - this->process_edge_neighbors(edge, &polyline); - - // get previous points - { - ThickPolyline rpolyline; - this->process_edge_neighbors(edge->twin(), &rpolyline); - polyline.points.insert(polyline.points.begin(), rpolyline.points.rbegin(), rpolyline.points.rend()); - polyline.width.insert(polyline.width.begin(), rpolyline.width.rbegin(), rpolyline.width.rend()); - polyline.endpoints.first = rpolyline.endpoints.second; - } - - assert(polyline.width.size() == polyline.points.size()*2 - 2); - - // prevent loop endpoints from being extended - if (polyline.first_point().coincides_with(polyline.last_point())) { - polyline.endpoints.first = false; - polyline.endpoints.second = false; - } - - // append polyline to result - polylines->push_back(polyline); - } -} - -void -MedialAxis::build(Polylines* polylines) -{ - ThickPolylines tp; - this->build(&tp); - polylines->insert(polylines->end(), tp.begin(), tp.end()); -} - -void -MedialAxis::process_edge_neighbors(const VD::edge_type* edge, ThickPolyline* polyline) -{ - while (true) { - // Since rot_next() works on the edge starting point but we want - // to find neighbors on the ending point, we just swap edge with - // its twin. - const VD::edge_type* twin = edge->twin(); - - // count neighbors for this edge - std::vector neighbors; - for (const VD::edge_type* neighbor = twin->rot_next(); neighbor != twin; - neighbor = neighbor->rot_next()) { - if (this->valid_edges.count(neighbor) > 0) neighbors.push_back(neighbor); - } - - // if we have a single neighbor then we can continue recursively - if (neighbors.size() == 1) { - const VD::edge_type* neighbor = neighbors.front(); - - // break if this is a closed loop - if (this->edges.count(neighbor) == 0) return; - - Point new_point(neighbor->vertex1()->x(), neighbor->vertex1()->y()); - polyline->points.push_back(new_point); - polyline->width.push_back(this->thickness[neighbor].first); - polyline->width.push_back(this->thickness[neighbor].second); - (void)this->edges.erase(neighbor); - (void)this->edges.erase(neighbor->twin()); - edge = neighbor; - } else if (neighbors.size() == 0) { - polyline->endpoints.second = true; - return; - } else { - // T-shaped or star-shaped joint - return; - } - } -} - -bool -MedialAxis::validate_edge(const VD::edge_type* edge) -{ - // prevent overflows and detect almost-infinite edges - if (std::abs(edge->vertex0()->x()) > (double)MAX_COORD - || std::abs(edge->vertex0()->y()) > (double)MAX_COORD - || std::abs(edge->vertex1()->x()) > (double)MAX_COORD - || std::abs(edge->vertex1()->y()) > (double)MAX_COORD) - return false; - - // construct the line representing this edge of the Voronoi diagram - const Line line( - Point( edge->vertex0()->x(), edge->vertex0()->y() ), - Point( edge->vertex1()->x(), edge->vertex1()->y() ) - ); - - // discard edge if it lies outside the supplied shape - // this could maybe be optimized (checking inclusion of the endpoints - // might give false positives as they might belong to the contour itself) - if (this->expolygon != NULL) { - if (line.a.coincides_with(line.b)) { - // in this case, contains(line) returns a false positive - if (!this->expolygon->contains(line.a)) return false; - } else { - if (!this->expolygon->contains(line)) return false; - } - } - - // retrieve the original line segments which generated the edge we're checking - const VD::cell_type* cell_l = edge->cell(); - const VD::cell_type* cell_r = edge->twin()->cell(); - const Line &segment_l = this->retrieve_segment(cell_l); - const Line &segment_r = this->retrieve_segment(cell_r); - - /* - SVG svg("edge.svg"); - svg.draw(*this->expolygon); - svg.draw(line); - svg.draw(segment_l, "red"); - svg.draw(segment_r, "blue"); - svg.Close(); - */ - - /* Calculate thickness of the cross-section at both the endpoints of this edge. - Our Voronoi edge is part of a CCW sequence going around its Voronoi cell - located on the left side. (segment_l). - This edge's twin goes around segment_r. Thus, segment_r is - oriented in the same direction as our main edge, and segment_l is oriented - in the same direction as our twin edge. - We used to only consider the (half-)distances to segment_r, and that works - whenever segment_l and segment_r are almost specular and facing. However, - at curves they are staggered and they only face for a very little length - (our very short edge represents such visibility). - Both w0 and w1 can be calculated either towards cell_l or cell_r with equal - results by Voronoi definition. - When cell_l or cell_r don't refer to the segment but only to an endpoint, we - calculate the distance to that endpoint instead. */ - - coordf_t w0 = cell_r->contains_segment() - ? line.a.distance_to(segment_r)*2 - : line.a.distance_to(this->retrieve_endpoint(cell_r))*2; - - coordf_t w1 = cell_l->contains_segment() - ? line.b.distance_to(segment_l)*2 - : line.b.distance_to(this->retrieve_endpoint(cell_l))*2; - - if (cell_l->contains_segment() && cell_r->contains_segment()) { - // calculate the relative angle between the two boundary segments - double angle = fabs(segment_r.orientation() - segment_l.orientation()); - if (angle > PI) angle = 2*PI - angle; - assert(angle >= 0 && angle <= PI); - - // fabs(angle) ranges from 0 (collinear, same direction) to PI (collinear, opposite direction) - // we're interested only in segments close to the second case (facing segments) - // so we allow some tolerance. - // this filter ensures that we're dealing with a narrow/oriented area (longer than thick) - // we don't run it on edges not generated by two segments (thus generated by one segment - // and the endpoint of another segment), since their orientation would not be meaningful - if (PI - angle > PI/8) { - // angle is not narrow enough - - // only apply this filter to segments that are not too short otherwise their - // angle could possibly be not meaningful - if (w0 < SCALED_EPSILON || w1 < SCALED_EPSILON || line.length() >= this->min_width) - return false; - } - } else { - if (w0 < SCALED_EPSILON || w1 < SCALED_EPSILON) - return false; - } - - if (w0 < this->min_width && w1 < this->min_width) - return false; - - if (w0 > this->max_width && w1 > this->max_width) - return false; - - this->thickness[edge] = std::make_pair(w0, w1); - this->thickness[edge->twin()] = std::make_pair(w1, w0); - - return true; -} - -const Line& -MedialAxis::retrieve_segment(const VD::cell_type* cell) const -{ - return this->lines[cell->source_index()]; -} - -const Point& -MedialAxis::retrieve_endpoint(const VD::cell_type* cell) const -{ - const Line& line = this->retrieve_segment(cell); - if (cell->source_category() == SOURCE_CATEGORY_SEGMENT_START_POINT) { - return line.a; - } else { - return line.b; - } -} - } } diff --git a/xs/src/libslic3r/Geometry.hpp b/xs/src/libslic3r/Geometry.hpp index 9d4579389c..8dce56a2fb 100644 --- a/xs/src/libslic3r/Geometry.hpp +++ b/xs/src/libslic3r/Geometry.hpp @@ -7,9 +7,6 @@ #include "Polygon.hpp" #include "Polyline.hpp" -#include "boost/polygon/voronoi.hpp" -using boost::polygon::voronoi_builder; -using boost::polygon::voronoi_diagram; namespace Slic3r { namespace Geometry { @@ -45,28 +42,6 @@ bool arrange( // output Pointfs &positions); -class MedialAxis { - public: - Lines lines; - const ExPolygon* expolygon; - double max_width; - double min_width; - MedialAxis(double _max_width, double _min_width, const ExPolygon* _expolygon = NULL) - : expolygon(_expolygon), max_width(_max_width), min_width(_min_width) {}; - void build(ThickPolylines* polylines); - void build(Polylines* polylines); - - private: - typedef voronoi_diagram VD; - VD vd; - std::set edges, valid_edges; - std::map > thickness; - void process_edge_neighbors(const VD::edge_type* edge, ThickPolyline* polyline); - bool validate_edge(const VD::edge_type* edge); - const Line& retrieve_segment(const VD::cell_type* cell) const; - const Point& retrieve_endpoint(const VD::cell_type* cell) const; -}; - } } #endif diff --git a/xs/src/libslic3r/Layer.cpp b/xs/src/libslic3r/Layer.cpp index 3e627b1644..35cf9b566a 100644 --- a/xs/src/libslic3r/Layer.cpp +++ b/xs/src/libslic3r/Layer.cpp @@ -189,6 +189,8 @@ Layer::make_perimeters() && config.overhangs == other_config.overhangs && config.serialize("perimeter_extrusion_width").compare(other_config.serialize("perimeter_extrusion_width")) == 0 && config.thin_walls == other_config.thin_walls + && config.thin_walls_min_width == other_config.thin_walls_min_width + && config.thin_walls_overlap == other_config.thin_walls_overlap && config.external_perimeters_first == other_config.external_perimeters_first) { layerms.push_back(other_layerm); done.insert(it - this->regions.begin()); diff --git a/xs/src/libslic3r/MedialAxis.cpp b/xs/src/libslic3r/MedialAxis.cpp new file mode 100644 index 0000000000..59fa73da57 --- /dev/null +++ b/xs/src/libslic3r/MedialAxis.cpp @@ -0,0 +1,1633 @@ +#include "MedialAxis.hpp" +#include "BoundingBox.hpp" +#include "ExPolygon.hpp" +#include "Geometry.hpp" +#include "Polygon.hpp" +#include "Line.hpp" +#include "ClipperUtils.hpp" +#include "SVG.hpp" +#include "polypartition.h" +#include "poly2tri/poly2tri.h" +#include +#include +#include + +namespace Slic3r { + +int MedialAxis::id = 0; + +void +MedialAxis::build(Polylines* polylines) +{ + ThickPolylines tp; + this->build(&tp); + polylines->insert(polylines->end(), tp.begin(), tp.end()); +} + +void +MedialAxis::polyline_from_voronoi(const Lines& voronoi_edges, ThickPolylines* polylines) +{ + this->lines = voronoi_edges; + construct_voronoi(lines.begin(), lines.end(), &this->vd); + + /* + // DEBUG: dump all Voronoi edges + { + for (VD::const_edge_iterator edge = this->vd.edges().begin(); edge != this->vd.edges().end(); ++edge) { + if (edge->is_infinite()) continue; + + ThickPolyline polyline; + polyline.points.push_back(Point( edge->vertex0()->x(), edge->vertex0()->y() )); + polyline.points.push_back(Point( edge->vertex1()->x(), edge->vertex1()->y() )); + polylines->push_back(polyline); + } + return; + } + */ + + typedef const VD::edge_type edge_t; + + // collect valid edges (i.e. prune those not belonging to MAT) + // note: this keeps twins, so it inserts twice the number of the valid edges + this->valid_edges.clear(); + { + std::set seen_edges; + for (VD::const_edge_iterator edge = this->vd.edges().begin(); edge != this->vd.edges().end(); ++edge) { + // if we only process segments representing closed loops, none if the + // infinite edges (if any) would be part of our MAT anyway + if (edge->is_secondary() || edge->is_infinite()) continue; + + // don't re-validate twins + if (seen_edges.find(&*edge) != seen_edges.end()) continue; // TODO: is this needed? + seen_edges.insert(&*edge); + seen_edges.insert(edge->twin()); + + if (!this->validate_edge(&*edge)) continue; + this->valid_edges.insert(&*edge); + this->valid_edges.insert(edge->twin()); + } + } + this->edges = this->valid_edges; + + // iterate through the valid edges to build polylines + while (!this->edges.empty()) { + const edge_t* edge = *this->edges.begin(); + + // start a polyline + ThickPolyline polyline; + polyline.points.push_back(Point( edge->vertex0()->x(), edge->vertex0()->y() )); + polyline.points.push_back(Point( edge->vertex1()->x(), edge->vertex1()->y() )); + polyline.width.push_back(this->thickness[edge].first); + polyline.width.push_back(this->thickness[edge].second); + + // remove this edge and its twin from the available edges + (void)this->edges.erase(edge); + (void)this->edges.erase(edge->twin()); + + // get next points + this->process_edge_neighbors(edge, &polyline); + + // get previous points + { + ThickPolyline rpolyline; + this->process_edge_neighbors(edge->twin(), &rpolyline); + polyline.points.insert(polyline.points.begin(), rpolyline.points.rbegin(), rpolyline.points.rend()); + polyline.width.insert(polyline.width.begin(), rpolyline.width.rbegin(), rpolyline.width.rend()); + polyline.endpoints.first = rpolyline.endpoints.second; + } + + assert(polyline.width.size() == polyline.points.size()); + + // prevent loop endpoints from being extended + if (polyline.first_point().coincides_with(polyline.last_point())) { + polyline.endpoints.first = false; + polyline.endpoints.second = false; + } + + // append polyline to result + polylines->push_back(polyline); + } + + #ifdef SLIC3R_DEBUG + { + static int iRun = 0; + dump_voronoi_to_svg(this->lines, this->vd, polylines, debug_out_path("MedialAxis-%d.svg", iRun ++).c_str()); + printf("Thick lines: "); + for (ThickPolylines::const_iterator it = polylines->begin(); it != polylines->end(); ++ it) { + ThickLines lines = it->thicklines(); + for (ThickLines::const_iterator it2 = lines.begin(); it2 != lines.end(); ++ it2) { + printf("%f,%f ", it2->a_width, it2->b_width); + } + } + printf("\n"); + } + #endif /* SLIC3R_DEBUG */ +} + +void +MedialAxis::process_edge_neighbors(const VD::edge_type* edge, ThickPolyline* polyline) +{ + while (true) { + // Since rot_next() works on the edge starting point but we want + // to find neighbors on the ending point, we just swap edge with + // its twin. + const VD::edge_type* twin = edge->twin(); + + // count neighbors for this edge + std::vector neighbors; + for (const VD::edge_type* neighbor = twin->rot_next(); neighbor != twin; + neighbor = neighbor->rot_next()) { + if (this->valid_edges.count(neighbor) > 0) neighbors.push_back(neighbor); + } + + // if we have a single neighbor then we can continue recursively + if (neighbors.size() == 1) { + const VD::edge_type* neighbor = neighbors.front(); + + // break if this is a closed loop + if (this->edges.count(neighbor) == 0) return; + + Point new_point(neighbor->vertex1()->x(), neighbor->vertex1()->y()); + polyline->points.push_back(new_point); + polyline->width.push_back(this->thickness[neighbor].second); + + (void)this->edges.erase(neighbor); + (void)this->edges.erase(neighbor->twin()); + edge = neighbor; + } else if (neighbors.size() == 0) { + polyline->endpoints.second = true; + return; + } else { + // T-shaped or star-shaped joint + return; + } + } +} + +bool +MedialAxis::validate_edge(const VD::edge_type* edge) +{ + // prevent overflows and detect almost-infinite edges + if (std::abs(edge->vertex0()->x()) > double(MAX_COORD) || + std::abs(edge->vertex0()->y()) > double(MAX_COORD) || + std::abs(edge->vertex1()->x()) > double(MAX_COORD) || + std::abs(edge->vertex1()->y()) > double(MAX_COORD)) + return false; + + // construct the line representing this edge of the Voronoi diagram + const Line line( + Point( edge->vertex0()->x(), edge->vertex0()->y() ), + Point( edge->vertex1()->x(), edge->vertex1()->y() ) + ); + + // discard edge if it lies outside the supplied shape + // this could maybe be optimized (checking inclusion of the endpoints + // might give false positives as they might belong to the contour itself) + if (line.a.coincides_with(line.b)) { + // in this case, contains(line) returns a false positive + if (!this->expolygon.contains(line.a)) return false; + } else { + if (!this->expolygon.contains(line)) return false; + } + + // retrieve the original line segments which generated the edge we're checking + const VD::cell_type* cell_l = edge->cell(); + const VD::cell_type* cell_r = edge->twin()->cell(); + const Line &segment_l = this->retrieve_segment(cell_l); + const Line &segment_r = this->retrieve_segment(cell_r); + + + //SVG svg("edge.svg"); + //svg.draw(this->expolygon.expolygon); + //svg.draw(line); + //svg.draw(segment_l, "red"); + //svg.draw(segment_r, "blue"); + //svg.Close(); + // + + /* Calculate thickness of the cross-section at both the endpoints of this edge. + Our Voronoi edge is part of a CCW sequence going around its Voronoi cell + located on the left side. (segment_l). + This edge's twin goes around segment_r. Thus, segment_r is + oriented in the same direction as our main edge, and segment_l is oriented + in the same direction as our twin edge. + We used to only consider the (half-)distances to segment_r, and that works + whenever segment_l and segment_r are almost specular and facing. However, + at curves they are staggered and they only face for a very little length + (our very short edge represents such visibility). + Both w0 and w1 can be calculated either towards cell_l or cell_r with equal + results by Voronoi definition. + When cell_l or cell_r don't refer to the segment but only to an endpoint, we + calculate the distance to that endpoint instead. */ + + coordf_t w0 = cell_r->contains_segment() + ? line.a.distance_to(segment_r)*2 + : line.a.distance_to(this->retrieve_endpoint(cell_r))*2; + + coordf_t w1 = cell_l->contains_segment() + ? line.b.distance_to(segment_l)*2 + : line.b.distance_to(this->retrieve_endpoint(cell_l))*2; + + //don't remove the line that goes to the intersection of the contour + // we use them to create nicer thin wall lines + //if (cell_l->contains_segment() && cell_r->contains_segment()) { + // // calculate the relative angle between the two boundary segments + // double angle = fabs(segment_r.orientation() - segment_l.orientation()); + // if (angle > PI) angle = 2*PI - angle; + // assert(angle >= 0 && angle <= PI); + // + // // fabs(angle) ranges from 0 (collinear, same direction) to PI (collinear, opposite direction) + // // we're interested only in segments close to the second case (facing segments) + // // so we allow some tolerance. + // // this filter ensures that we're dealing with a narrow/oriented area (longer than thick) + // // we don't run it on edges not generated by two segments (thus generated by one segment + // // and the endpoint of another segment), since their orientation would not be meaningful + // if (PI - angle > PI/8) { + // // angle is not narrow enough + // + // // only apply this filter to segments that are not too short otherwise their + // // angle could possibly be not meaningful + // if (w0 < SCALED_EPSILON || w1 < SCALED_EPSILON || line.length() >= this->min_width) + // return false; + // } + //} else { + // if (w0 < SCALED_EPSILON || w1 < SCALED_EPSILON) + // return false; + //} + + // don't do that before we try to fusion them + //if (w0 < this->min_width && w1 < this->min_width) + // return false; + // + + //shouldn't occur if perimeter_generator is well made + if (w0 > this->max_width && w1 > this->max_width) + return false; + + this->thickness[edge] = std::make_pair(w0, w1); + this->thickness[edge->twin()] = std::make_pair(w1, w0); + + return true; +} + +const Line& +MedialAxis::retrieve_segment(const VD::cell_type* cell) const +{ + return lines[cell->source_index()]; +} + +const Point& +MedialAxis::retrieve_endpoint(const VD::cell_type* cell) const +{ + const Line& line = this->retrieve_segment(cell); + if (cell->source_category() == boost::polygon::SOURCE_CATEGORY_SEGMENT_START_POINT) { + return line.a; + } else { + return line.b; + } +} + + +/// remove point that are at SCALED_EPSILON * 2 distance. +void +remove_point_too_near(ThickPolyline* to_reduce) +{ + const coord_t smallest = SCALED_EPSILON * 2; + size_t id = 1; + while (id < to_reduce->points.size() - 1) { + coord_t newdist = (coord_t)std::min(to_reduce->points[id].distance_to(to_reduce->points[id - 1]) + , to_reduce->points[id].distance_to(to_reduce->points[id + 1])); + if (newdist < smallest) { + to_reduce->points.erase(to_reduce->points.begin() + id); + to_reduce->width.erase(to_reduce->width.begin() + id); + newdist = (coord_t)to_reduce->points[id].distance_to(to_reduce->points[id - 1]); + //if you removed a point, it check if the next one isn't too near from the previous one. + // if not, it bypass it. + if (newdist > smallest) { + ++id; + } + } + //go to next one + else ++id; + } +} + +/// add points from pattern to to_modify at the same % of the length +/// so not add if an other point is present at the correct position +void +add_point_same_percent(ThickPolyline* pattern, ThickPolyline* to_modify) +{ + const double to_modify_length = to_modify->length(); + const double percent_epsilon = SCALED_EPSILON / to_modify_length; + const double pattern_length = pattern->length(); + + double percent_length = 0; + for (size_t idx_point = 1; idx_point < pattern->points.size() - 1; ++idx_point) { + percent_length += pattern->points[idx_point-1].distance_to(pattern->points[idx_point]) / pattern_length; + //find position + size_t idx_other = 1; + double percent_length_other_before = 0; + double percent_length_other = 0; + while (idx_other < to_modify->points.size()) { + percent_length_other_before = percent_length_other; + percent_length_other += to_modify->points[idx_other-1].distance_to(to_modify->points[idx_other]) + / to_modify_length; + if (percent_length_other > percent_length - percent_epsilon) { + //if higher (we have gone over it) + break; + } + ++idx_other; + } + if (percent_length_other > percent_length + percent_epsilon) { + //insert a new point before the position + double percent_dist = (percent_length - percent_length_other_before) / (percent_length_other - percent_length_other_before); + coordf_t new_width = to_modify->width[idx_other - 1] * (1 - percent_dist); + new_width += to_modify->width[idx_other] * (percent_dist); + to_modify->width.insert(to_modify->width.begin() + idx_other, new_width); + to_modify->points.insert(to_modify->points.begin() + idx_other, + to_modify->points[idx_other - 1].interpolate(percent_dist, to_modify->points[idx_other])); + } + } +} + +/// find the nearest angle in the contour (or 2 nearest if it's difficult to choose) +/// return 1 for an angle of 90° and 0 for an angle of 0° or 180° +/// find the nearest angle in the contour (or 2 nearest if it's difficult to choose) +/// return 1 for an angle of 90° and 0 for an angle of 0° or 180° +double +get_coeff_from_angle_countour(Point &point, const ExPolygon &contour, coord_t min_dist_between_point) { + double nearest_dist = point.distance_to(contour.contour.points.front()); + Point point_nearest = contour.contour.points.front(); + size_t id_nearest = 0; + double near_dist = nearest_dist; + Point point_near = point_nearest; + size_t id_near = 0; + for (size_t id_point = 1; id_point < contour.contour.points.size(); ++id_point) { + if (nearest_dist > point.distance_to(contour.contour.points[id_point])) { + //update point_near + id_near = id_nearest; + point_near = point_nearest; + near_dist = nearest_dist; + //update nearest + nearest_dist = point.distance_to(contour.contour.points[id_point]); + point_nearest = contour.contour.points[id_point]; + id_nearest = id_point; + } + } + double angle = 0; + size_t id_before = id_nearest == 0 ? contour.contour.points.size() - 1 : id_nearest - 1; + Point point_before = id_nearest == 0 ? contour.contour.points.back() : contour.contour.points[id_nearest - 1]; + //Search one point far enough to be relevant + while (point_nearest.distance_to(point_before) < min_dist_between_point) { + point_before = id_before == 0 ? contour.contour.points.back() : contour.contour.points[id_before - 1]; + id_before = id_before == 0 ? contour.contour.points.size() - 1 : id_before - 1; + //don't loop + if (id_before == id_nearest) { + id_before = id_nearest == 0 ? contour.contour.points.size() - 1 : id_nearest - 1; + point_before = id_nearest == 0 ? contour.contour.points.back() : contour.contour.points[id_nearest - 1]; + break; + } + } + size_t id_after = id_nearest == contour.contour.points.size() - 1 ? 0 : id_nearest + 1; + Point point_after = id_nearest == contour.contour.points.size() - 1 ? contour.contour.points.front() : contour.contour.points[id_nearest + 1]; + //Search one point far enough to be relevant + while (point_nearest.distance_to(point_after) < min_dist_between_point) { + point_after = id_after == contour.contour.points.size() - 1 ? contour.contour.points.front() : contour.contour.points[id_after + 1]; + id_after = id_after == contour.contour.points.size() - 1 ? 0 : id_after + 1; + //don't loop + if (id_after == id_nearest) { + id_after = id_nearest == contour.contour.points.size() - 1 ? 0 : id_nearest + 1; + point_after = id_nearest == contour.contour.points.size() - 1 ? contour.contour.points.front() : contour.contour.points[id_nearest + 1]; + break; + } + } + //compute angle + angle = point_nearest.ccw_angle(point_before, point_after); + if (angle >= PI) angle = 2 * PI - angle; // smaller angle + //compute the diff from 90° + angle = abs(angle - PI / 2); + if (point_near.coincides_with(point_nearest) && std::max(nearest_dist, near_dist) + SCALED_EPSILON < point_nearest.distance_to(point_near)) { + //not only nearest + Point point_before = id_near == 0 ? contour.contour.points.back() : contour.contour.points[id_near - 1]; + Point point_after = id_near == contour.contour.points.size() - 1 ? contour.contour.points.front() : contour.contour.points[id_near + 1]; + double angle2 = std::min(point_nearest.ccw_angle(point_before, point_after), point_nearest.ccw_angle(point_after, point_before)); + angle2 = abs(angle - PI / 2); + angle = (angle + angle2) / 2; + } + + return 1 - (angle / (PI / 2)); +} + +double +dot(Line l1, Line l2) +{ + Vectorf v_1 = normalize(Vectorf(l1.b.x - l1.a.x, l1.b.y - l1.a.y)); + Vectorf v_2 = normalize(Vectorf(l2.b.x - l2.a.x, l2.b.y - l2.a.y)); + return v_1.x*v_2.x + v_1.y*v_2.y; +} + +void +MedialAxis::fusion_curve(ThickPolylines &pp) +{ + //fusion Y with only 1 '0' value => the "0" branch "pull" the cross-point + bool changes = false; + for (size_t i = 0; i < pp.size(); ++i) { + ThickPolyline& polyline = pp[i]; + // only consider 2-point polyline with endpoint + if (polyline.points.size() != 2) continue; + if (polyline.endpoints.first) polyline.reverse(); + else if (!polyline.endpoints.second) continue; + if (polyline.width.back() > EPSILON) continue; + + //check my length is small + coord_t length = (coord_t)polyline.length(); + if (length > max_width) continue; + + size_t closest_point_idx = this->expolygon.contour.closest_point_index(polyline.points.back()); + + //check the 0-wodth point is on the contour. + if (closest_point_idx == (size_t)-1) continue; + + size_t prev_idx = closest_point_idx == 0 ? this->expolygon.contour.points.size() - 1 : closest_point_idx - 1; + size_t next_idx = closest_point_idx == this->expolygon.contour.points.size() - 1 ? 0 : closest_point_idx + 1; + double mindot = 1; + mindot = std::min(mindot, abs(dot(Line(polyline.points[polyline.points.size() - 1], polyline.points[polyline.points.size() - 2]), + (Line(this->expolygon.contour.points[closest_point_idx], this->expolygon.contour.points[prev_idx]))))); + mindot = std::min(mindot, abs(dot(Line(polyline.points[polyline.points.size() - 1], polyline.points[polyline.points.size() - 2]), + (Line(this->expolygon.contour.points[closest_point_idx], this->expolygon.contour.points[next_idx]))))); + + //compute angle + double coeff_contour_angle = this->expolygon.contour.points[closest_point_idx].ccw_angle(this->expolygon.contour.points[prev_idx], this->expolygon.contour.points[next_idx]); + if (coeff_contour_angle >= PI) coeff_contour_angle = 2 * PI - coeff_contour_angle; // smaller angle + //compute the diff from 90° + coeff_contour_angle = abs(coeff_contour_angle - PI / 2); + + + // look if other end is a cross point with almost 90° angle + double sum_dot = 0; + double min_dot = 0; + // look if other end is a cross point with multiple other branch + std::vector crosspoint; + for (size_t j = 0; j < pp.size(); ++j) { + if (j == i) continue; + ThickPolyline& other = pp[j]; + if (polyline.first_point().coincides_with(other.last_point())) { + other.reverse(); + crosspoint.push_back(j); + double dot_temp = dot(Line(polyline.points[0], polyline.points[1]), (Line(other.points[0], other.points[1]))); + min_dot = std::min(min_dot, abs(dot_temp)); + sum_dot += dot_temp; + } else if (polyline.first_point().coincides_with(other.first_point())) { + crosspoint.push_back(j); + double dot_temp = dot(Line(polyline.points[0], polyline.points[1]), (Line(other.points[0], other.points[1]))); + min_dot = std::min(min_dot, abs(dot_temp)); + sum_dot += dot_temp; + } + } + //only consider very shallow angle for contour + if (mindot > 0.15 && + (1 - (coeff_contour_angle / (PI / 2))) > 0.2) continue; + + //check if it's a line that we can pull + if (crosspoint.size() != 2) continue; + if (sum_dot > 0.2) continue; + if (min_dot > 0.5) continue; + + //don't pull, it distords the line if there are too many points. + //// pull it a bit, depends on my size, the dot?, and the coeff at my 0-end (~14% for a square, almost 0 for a gentle curve) + //coord_t length_pull = polyline.length(); + //length_pull *= 0.144 * get_coeff_from_angle_countour(polyline.points.back(), this->expolygon, min(min_width, polyline.length() / 2)); + + ////compute dir + //Vectorf pull_direction(polyline.points[1].x - polyline.points[0].x, polyline.points[1].y - polyline.points[0].y); + //pull_direction = normalize(pull_direction); + //pull_direction.x *= length_pull; + //pull_direction.y *= length_pull; + + ////pull the points + //Point &p1 = pp[crosspoint[0]].points[0]; + //p1.x = p1.x + (coord_t)pull_direction.x; + //p1.y = p1.y + (coord_t)pull_direction.y; + + //Point &p2 = pp[crosspoint[1]].points[0]; + //p2.x = p2.x + (coord_t)pull_direction.x; + //p2.y = p2.y + (coord_t)pull_direction.y; + + //delete the now unused polyline + pp.erase(pp.begin() + i); + --i; + changes = true; + } + if (changes) { + concatThickPolylines(pp); + ///reorder, in case of change + std::sort(pp.begin(), pp.end(), [](const ThickPolyline & a, const ThickPolyline & b) { return a.length() < b.length(); }); + } +} + +void +MedialAxis::fusion_corners(ThickPolylines &pp) +{ + + //fusion Y with only 1 '0' value => the "0" branch "pull" the cross-point + bool changes = false; + for (size_t i = 0; i < pp.size(); ++i) { + ThickPolyline& polyline = pp[i]; + // only consider polyline with 0-end + if (polyline.points.size() != 2) continue; + if (polyline.endpoints.first) polyline.reverse(); + else if (!polyline.endpoints.second) continue; + if (polyline.width.back() > 0) continue; + + //check my length is small + coord_t length = (coord_t)polyline.length(); + if (length > max_width) continue; + + // look if other end is a cross point with multiple other branch + std::vector crosspoint; + for (size_t j = 0; j < pp.size(); ++j) { + if (j == i) continue; + ThickPolyline& other = pp[j]; + if (polyline.first_point().coincides_with(other.last_point())) { + other.reverse(); + crosspoint.push_back(j); + } else if (polyline.first_point().coincides_with(other.first_point())) { + crosspoint.push_back(j); + } + } + //check if it's a line that we can pull + if (crosspoint.size() != 2) continue; + + // check if i am at the external side of a curve + double angle1 = polyline.points[0].ccw_angle(polyline.points[1], pp[crosspoint[0]].points[1]); + if (angle1 >= PI) angle1 = 2 * PI - angle1; // smaller angle + double angle2 = polyline.points[0].ccw_angle(polyline.points[1], pp[crosspoint[1]].points[1]); + if (angle2 >= PI) angle2 = 2 * PI - angle2; // smaller angle + if (angle1 + angle2 < PI) continue; + + //check if is smaller or the other ones are not endpoits + if (pp[crosspoint[0]].endpoints.second && length > pp[crosspoint[0]].length()) continue; + if (pp[crosspoint[1]].endpoints.second && length > pp[crosspoint[1]].length()) continue; + + //FIXME: also pull (a bit less) points that are near to this one. + // if true, pull it a bit, depends on my size, the dot?, and the coeff at my 0-end (~14% for a square, almost 0 for a gentle curve) + coord_t length_pull = (coord_t)polyline.length(); + length_pull *= (coord_t)( 0.144 * get_coeff_from_angle_countour( + polyline.points.back(), + this->expolygon, + std::min(min_width, (coord_t)(polyline.length() / 2)))); + + //compute dir + Vectorf pull_direction(polyline.points[1].x - polyline.points[0].x, polyline.points[1].y - polyline.points[0].y); + pull_direction = normalize(pull_direction); + pull_direction.x *= length_pull; + pull_direction.y *= length_pull; + + //pull the points + Point &p1 = pp[crosspoint[0]].points[0]; + p1.x = p1.x + pull_direction.x; + p1.y = p1.y + pull_direction.y; + + Point &p2 = pp[crosspoint[1]].points[0]; + p2.x = p2.x + pull_direction.x; + p2.y = p2.y + pull_direction.y; + + //delete the now unused polyline + pp.erase(pp.begin() + i); + --i; + changes = true; + } + if (changes) { + concatThickPolylines(pp); + ///reorder, in case of change + std::sort(pp.begin(), pp.end(), [](const ThickPolyline & a, const ThickPolyline & b) { return a.length() < b.length(); }); + } +} + +void +MedialAxis::extends_line_both_side(ThickPolylines& pp) { + const ExPolygons anchors = offset2_ex(to_polygons(diff_ex(this->bounds, this->expolygon)), -SCALED_RESOLUTION, SCALED_RESOLUTION); + for (size_t i = 0; i < pp.size(); ++i) { + ThickPolyline& polyline = pp[i]; + this->extends_line(polyline, anchors, this->min_width); + polyline.reverse(); + this->extends_line(polyline, anchors, this->min_width); + } +} + +void +MedialAxis::extends_line(ThickPolyline& polyline, const ExPolygons& anchors, const coord_t join_width) +{ + // extend initial and final segments of each polyline if they're actual endpoints + // We assign new endpoints to temporary variables because in case of a single-line + // polyline, after we extend the start point it will be caught by the intersection() + // call, so we keep the inner point until we perform the second intersection() as well + if (polyline.endpoints.second && !bounds.has_boundary_point(polyline.points.back())) { + size_t first_idx = polyline.points.size() - 2; + Line line(*(polyline.points.begin() + first_idx), polyline.points.back()); + while (line.length() < SCALED_RESOLUTION && first_idx>0) { + first_idx--; + line.a = *(polyline.points.begin() + first_idx); + } + // prevent the line from touching on the other side, otherwise intersection() might return that solution + if (polyline.points.size() == 2 && this->expolygon.contains(line.midpoint())) line.a = line.midpoint(); + + line.extend_end(max_width); + Point new_back; + if (this->expolygon.contour.has_boundary_point(polyline.points.back())) { + new_back = polyline.points.back(); + } else { + //TODO: verify also for holes. + bool finded = this->expolygon.contour.first_intersection(line, &new_back); + //verify also for holes. + Point new_back_temp; + for (Polygon hole : this->expolygon.holes) { + if (hole.first_intersection(line, &new_back_temp)) { + if (!finded || line.a.distance_to(new_back_temp) < line.a.distance_to(new_back)) { + finded = true; + new_back = new_back_temp; + } + } + } + // safety check if no intersection + if (!finded) new_back = line.b; + + polyline.points.push_back(new_back); + polyline.width.push_back(polyline.width.back()); + } + Point new_bound; + bool finded = bounds.contour.first_intersection(line, &new_bound); + //verify also for holes. + Point new_bound_temp; + for (Polygon hole : bounds.holes) { + if (hole.first_intersection(line, &new_bound_temp)) { + if (!finded || line.a.distance_to(new_bound_temp) < line.a.distance_to(new_bound)) { + finded = true; + new_bound = new_bound_temp; + } + } + } + // safety check if no intersection + if (!finded) { + if (line.b.coincides_with_epsilon(polyline.points.back())) return; + //check if we don't over-shoot inside us + bool is_in_anchor = false; + for (const ExPolygon& a : anchors) { + if (a.contains(line.b)) { + is_in_anchor = true; + break; + } + } + if (!is_in_anchor) return; + new_bound = line.b; + } + // find anchor + Point best_anchor; + double shortest_dist = max_width; + for (const ExPolygon& a : anchors) { + Point p_maybe_inside = a.contour.centroid(); + double test_dist = new_bound.distance_to(p_maybe_inside) + new_back.distance_to(p_maybe_inside); + //if (test_dist < max_width / 2 && (test_dist < shortest_dist || shortest_dist < 0)) { + double angle_test = new_back.ccw_angle(p_maybe_inside, line.a); + if (angle_test > PI) angle_test = 2 * PI - angle_test; + if (test_dist < max_width && test_dist PI / 2) { + shortest_dist = test_dist; + best_anchor = p_maybe_inside; + } + } + if (best_anchor.x != 0 && best_anchor.y != 0) { + Point p_obj = best_anchor + new_bound; + p_obj.x /= 2; + p_obj.y /= 2; + Line l2 = Line(new_back, p_obj); + l2.extend_end(max_width); + (void)bounds.contour.first_intersection(l2, &new_bound); + } + if (new_bound.coincides_with_epsilon(new_back)) { + return; + } + polyline.points.push_back(new_bound); + //polyline.width.push_back(join_width); + //it thickens the line a bit too early, imo + polyline.width.push_back(polyline.width.back()); + } +} + +void +MedialAxis::main_fusion(ThickPolylines& pp) +{ + + bool changes = true; + std::map coeff_angle_cache; + while (changes) { + concatThickPolylines(pp); + //reoder pp by length (ascending) It's really important to do that to avoid building the line from the width insteand of the length + std::sort(pp.begin(), pp.end(), [](const ThickPolyline & a, const ThickPolyline & b) { + bool ahas0 = a.width.front() == 0 || a.width.back() == 0; + bool bhas0 = b.width.front() == 0 || b.width.back() == 0; + if (ahas0 && !bhas0) return true; + if (!ahas0 && bhas0) return false; + return a.length() < b.length(); + }); + changes = false; + for (size_t i = 0; i < pp.size(); ++i) { + ThickPolyline& polyline = pp[i]; + + //simple check to see if i can be fusionned + if (!polyline.endpoints.first && !polyline.endpoints.second) continue; + + + ThickPolyline* best_candidate = nullptr; + float best_dot = -1; + size_t best_idx = 0; + double dot_poly_branch = 0; + double dot_candidate_branch = 0; + + // find another polyline starting here + for (size_t j = i + 1; j < pp.size(); ++j) { + ThickPolyline& other = pp[j]; + if (polyline.last_point().coincides_with(other.last_point())) { + polyline.reverse(); + other.reverse(); + } else if (polyline.first_point().coincides_with(other.last_point())) { + other.reverse(); + } else if (polyline.first_point().coincides_with(other.first_point())) { + } else if (polyline.last_point().coincides_with(other.first_point())) { + polyline.reverse(); + } else { + continue; + } + + //// mergeable tests + if (polyline.points.size() < 2 && other.points.size() < 2) continue; + if (!polyline.endpoints.second || !other.endpoints.second) continue; + // test if the new width will not be too big if a fusion occur + //note that this isn't the real calcul. It's just to avoid merging lines too far apart. + if ( + ((polyline.points.back().distance_to(other.points.back()) + + (polyline.width.back() + other.width.back()) / 4) + > max_width*1.05)) + continue; + // test if the lines are not too different in length. + if (abs(polyline.length() - other.length()) > max_width) continue; + + + //test if we don't merge with something too different and without any relevance. + double coeffSizePolyI = 1; + if (polyline.width.back() == 0) { + coeffSizePolyI = 0.1 + 0.9*get_coeff_from_angle_countour(polyline.points.back(), this->expolygon, std::min(min_width, (coord_t)(polyline.length() / 2))); + } + double coeffSizeOtherJ = 1; + if (other.width.back() == 0) { + coeffSizeOtherJ = 0.1 + 0.9*get_coeff_from_angle_countour(other.points.back(), this->expolygon, std::min(min_width, (coord_t)(polyline.length() / 2))); + } + if (abs(polyline.length()*coeffSizePolyI - other.length()*coeffSizeOtherJ) > max_width / 2) continue; + + + //compute angle to see if it's better than previous ones (straighter = better). + //we need to add how strait we are from our main. + float test_dot = (float)(dot(polyline.lines().front(), other.lines().front())); + + // Get the branch/line in wich we may merge, if possible + // with that, we can decide what is important, and how we can merge that. + // angle_poly - angle_candi =90° => one is useless + // both angle are equal => both are useful with same strength + // ex: Y => | both are useful to crete a nice line + // ex2: TTTTT => ----- these 90° useless lines should be discarded + bool find_main_branch = false; + size_t biggest_main_branch_id = 0; + coord_t biggest_main_branch_length = 0; + for (size_t k = 0; k < pp.size(); ++k) { + if (k == i || k == j) continue; + ThickPolyline& main = pp[k]; + if (polyline.first_point().coincides_with(main.last_point())) { + main.reverse(); + if (!main.endpoints.second) + find_main_branch = true; + else if (biggest_main_branch_length < main.length()) { + biggest_main_branch_id = k; + biggest_main_branch_length = (coord_t)main.length(); + } + } else if (polyline.first_point().coincides_with(main.first_point())) { + if (!main.endpoints.second) + find_main_branch = true; + else if (biggest_main_branch_length < main.length()) { + biggest_main_branch_id = k; + biggest_main_branch_length = (coord_t)main.length(); + } + } + if (find_main_branch) { + //use this variable to store the good index and break to compute it + biggest_main_branch_id = k; + break; + } + } + double dot_poly_branch_test = 0.707; + double dot_candidate_branch_test = 0.707; + if (!find_main_branch && biggest_main_branch_length == 0) { + // nothing -> it's impossible! + dot_poly_branch_test = 0.707; + dot_candidate_branch_test = 0.707; + } else if (!find_main_branch && ( + (pp[biggest_main_branch_id].length() < polyline.length() && (polyline.width.back() != 0 || pp[biggest_main_branch_id].width.back() ==0)) + || (pp[biggest_main_branch_id].length() < other.length() && (other.width.back() != 0 || pp[biggest_main_branch_id].width.back() == 0)))) { + //the main branch should have no endpoint or be bigger! + //here, it have an endpoint, and is not the biggest -> bad! + //std::cout << "he main branch should have no endpoint or be bigger! here, it have an endpoint, and is not the biggest -> bad!\n"; + continue; + } else { + //compute the dot (biggest_main_branch_id) + dot_poly_branch_test = -dot(Line(polyline.points[0], polyline.points[1]), Line(pp[biggest_main_branch_id].points[0], pp[biggest_main_branch_id].points[1])); + dot_candidate_branch_test = -dot(Line(other.points[0], other.points[1]), Line(pp[biggest_main_branch_id].points[0], pp[biggest_main_branch_id].points[1])); + if (dot_poly_branch_test < 0) dot_poly_branch_test = 0; + if (dot_candidate_branch_test < 0) dot_candidate_branch_test = 0; + if (pp[biggest_main_branch_id].width.back()>0) + test_dot += 2 * (float)dot_poly_branch; + } + //test if it's useful to merge or not + //ie, don't merge 'T' but ok for 'Y', merge only lines of not disproportionate different length (ratio max: 4) (or they are both with 0-width end) + if (dot_poly_branch_test < 0.1 || dot_candidate_branch_test < 0.1 || + ( + ((polyline.length()>other.length() ? polyline.length() / other.length() : other.length() / polyline.length()) > 4) + && !(polyline.width.back() == 0 && other.width.back()==0) + )) { + //std::cout << "not useful to merge\n"; + continue; + } + if (test_dot > best_dot) { + best_candidate = &other; + best_idx = j; + best_dot = test_dot; + dot_poly_branch = dot_poly_branch_test; + dot_candidate_branch = dot_candidate_branch_test; + } + } + if (best_candidate != nullptr) { + // delete very near points + remove_point_too_near(&polyline); + remove_point_too_near(best_candidate); + + // add point at the same pos than the other line to have a nicer fusion + add_point_same_percent(&polyline, best_candidate); + add_point_same_percent(best_candidate, &polyline); + + //get the angle of the nearest points of the contour to see : _| (good) \_ (average) __(bad) + //sqrt because the result are nicer this way: don't over-penalize /_ angles + //TODO: try if we can achieve a better result if we use a different algo if the angle is <90° + const double coeff_angle_poly = (coeff_angle_cache.find(polyline.points.back()) != coeff_angle_cache.end()) + ? coeff_angle_cache[polyline.points.back()] + : (get_coeff_from_angle_countour(polyline.points.back(), this->expolygon, std::min(min_width, (coord_t)(polyline.length() / 2)))); + const double coeff_angle_candi = (coeff_angle_cache.find(best_candidate->points.back()) != coeff_angle_cache.end()) + ? coeff_angle_cache[best_candidate->points.back()] + : (get_coeff_from_angle_countour(best_candidate->points.back(), this->expolygon, std::min(min_width, (coord_t)(best_candidate->length() / 2)))); + + //this will encourage to follow the curve, a little, because it's shorter near the center + //without that, it tends to go to the outter rim. + double weight_poly = 2 - (polyline.length() / std::max(polyline.length(), best_candidate->length())); + double weight_candi = 2 - (best_candidate->length() / std::max(polyline.length(), best_candidate->length())); + weight_poly *= coeff_angle_poly; + weight_candi *= coeff_angle_candi; + const double coeff_poly = (dot_poly_branch * weight_poly) / (dot_poly_branch * weight_poly + dot_candidate_branch * weight_candi); + const double coeff_candi = 1.0 - coeff_poly; + //iterate the points + // as voronoi should create symetric thing, we can iterate synchonously + size_t idx_point = 1; + while (idx_point < std::min(polyline.points.size(), best_candidate->points.size())) { + //fusion + polyline.points[idx_point].x = polyline.points[idx_point].x * coeff_poly + best_candidate->points[idx_point].x * coeff_candi; + polyline.points[idx_point].y = polyline.points[idx_point].y * coeff_poly + best_candidate->points[idx_point].y * coeff_candi; + + // The width decrease with distance from the centerline. + // This formula is what works the best, even if it's not perfect (created empirically). 0->3% error on a gap fill on some tests. + //If someone find an other formula based on the properties of the voronoi algorithm used here, and it works better, please use it. + //or maybe just use the distance to nearest edge in bounds... + double value_from_current_width = 0.5*polyline.width[idx_point] * dot_poly_branch / std::max(dot_poly_branch, dot_candidate_branch); + value_from_current_width += 0.5*best_candidate->width[idx_point] * dot_candidate_branch / std::max(dot_poly_branch, dot_candidate_branch); + double value_from_dist = 2 * polyline.points[idx_point].distance_to(best_candidate->points[idx_point]); + value_from_dist *= sqrt(std::min(dot_poly_branch, dot_candidate_branch) / std::max(dot_poly_branch, dot_candidate_branch)); + polyline.width[idx_point] = value_from_current_width + value_from_dist; + //failsafes + if (polyline.width[idx_point] > max_width) + polyline.width[idx_point] = max_width; + const coord_t max_width_contour = (coord_t) bounds.contour.closest_point(polyline.points[idx_point])->distance_to(polyline.points[idx_point]) * 2.1; + if (polyline.width[idx_point] > max_width_contour) + polyline.width[idx_point] = max_width_contour; + + ++idx_point; + } + if (idx_point < best_candidate->points.size()) { + if (idx_point + 1 < best_candidate->points.size()) { + //create a new polyline + pp.emplace_back(); + pp.back().endpoints.first = true; + pp.back().endpoints.second = best_candidate->endpoints.second; + for (size_t idx_point_new_line = idx_point; idx_point_new_line < best_candidate->points.size(); ++idx_point_new_line) { + pp.back().points.push_back(best_candidate->points[idx_point_new_line]); + pp.back().width.push_back(best_candidate->width[idx_point_new_line]); + } + } else { + //Add last point + polyline.points.push_back(best_candidate->points[idx_point]); + polyline.width.push_back(best_candidate->width[idx_point]); + //select if an end occur + polyline.endpoints.second &= best_candidate->endpoints.second; + } + + } else { + //select if an end occur + polyline.endpoints.second &= best_candidate->endpoints.second; + } + + //remove points that are the same or too close each other, ie simplify + for (size_t idx_point = 1; idx_point < polyline.points.size(); ++idx_point) { + if (polyline.points[idx_point - 1].distance_to(polyline.points[idx_point]) < SCALED_EPSILON) { + if (idx_point < polyline.points.size() - 1) { + polyline.points.erase(polyline.points.begin() + idx_point); + polyline.width.erase(polyline.width.begin() + idx_point); + } else { + polyline.points.erase(polyline.points.begin() + idx_point - 1); + polyline.width.erase(polyline.width.begin() + idx_point - 1); + } + --idx_point; + } + } + //remove points that are outside of the geometry + for (size_t idx_point = 0; idx_point < polyline.points.size(); ++idx_point) { + if (!bounds.contains_b(polyline.points[idx_point])) { + polyline.points.erase(polyline.points.begin() + idx_point); + polyline.width.erase(polyline.width.begin() + idx_point); + --idx_point; + } + } + + //update cache + coeff_angle_cache[polyline.points.back()] = coeff_angle_poly * coeff_poly + coeff_angle_candi * coeff_candi; + + + if (polyline.points.size() < 2) { + //remove self + pp.erase(pp.begin() + i); + --i; + --best_idx; + } + + pp.erase(pp.begin() + best_idx); + changes = true; + break; + } + } + } +} + +void +MedialAxis::remove_too_thin_extrusion(ThickPolylines& pp) +{ + // remove too thin extrusion at start & end of polylines + bool changes = false; + for (size_t i = 0; i < pp.size(); ++i) { + ThickPolyline& polyline = pp[i]; + // remove bits with too small extrusion + while (polyline.points.size() > 1 && polyline.width.front() < this->min_width && polyline.endpoints.first) { + //try to split if possible + if (polyline.width[1] > min_width) { + double percent_can_keep = (min_width - polyline.width[0]) / (polyline.width[1] - polyline.width[0]); + if (polyline.points.front().distance_to(polyline.points[1]) * (1 - percent_can_keep) > SCALED_RESOLUTION) { + //Can split => move the first point and assign a new weight. + //the update of endpoints wil be performed in concatThickPolylines + polyline.points.front() = polyline.points.front().interpolate(percent_can_keep, polyline.points[1]); + polyline.width.front() = min_width; + } else { + /// almost 0-length, Remove + polyline.points.erase(polyline.points.begin()); + polyline.width.erase(polyline.width.begin()); + } + changes = true; + break; + } + polyline.points.erase(polyline.points.begin()); + polyline.width.erase(polyline.width.begin()); + changes = true; + } + while (polyline.points.size() > 1 && polyline.width.back() < this->min_width && polyline.endpoints.second) { + //try to split if possible + if (polyline.width[polyline.points.size() - 2] > min_width) { + double percent_can_keep = (min_width - polyline.width.back()) / (polyline.width[polyline.points.size() - 2] - polyline.width.back()); + if (polyline.points.back().distance_to(polyline.points[polyline.points.size() - 2]) * (1 - percent_can_keep) > SCALED_RESOLUTION) { + //Can split => move the first point and assign a new weight. + //the update of endpoints wil be performed in concatThickPolylines + polyline.points.back() = polyline.points.back().interpolate(percent_can_keep, polyline.points[polyline.points.size() - 2]); + polyline.width.back() = min_width; + } else { + /// almost 0-length, Remove + polyline.points.erase(polyline.points.end() - 1); + polyline.width.erase(polyline.width.end() - 1); + } + changes = true; + break; + } + polyline.points.erase(polyline.points.end() - 1); + polyline.width.erase(polyline.width.end() - 1); + changes = true; + } + //remove points and bits that comes from a "main line" + if (polyline.points.size() < 2 || (changes && polyline.length() < max_width && polyline.points.size() ==2)) { + //remove self if too small + pp.erase(pp.begin() + i); + --i; + } + } + if (changes) concatThickPolylines(pp); +} + +void +MedialAxis::concatenate_polylines_with_crossing(ThickPolylines& pp) +{ + + // concatenate, but even where multiple thickpolyline join, to create nice long strait polylines + /* If we removed any short polylines we now try to connect consecutive polylines + in order to allow loop detection. Note that this algorithm is greedier than + MedialAxis::process_edge_neighbors() as it will connect random pairs of + polylines even when more than two start from the same point. This has no + drawbacks since we optimize later using nearest-neighbor which would do the + same, but should we use a more sophisticated optimization algorithm we should + not connect polylines when more than two meet. + Optimisation of the old algorithm : now we select the most "strait line" choice + when we merge with an other line at a point with more than two meet. + */ + for (size_t i = 0; i < pp.size(); ++i) { + ThickPolyline& polyline = pp[i]; + if (polyline.endpoints.first && polyline.endpoints.second) continue; // optimization + + ThickPolyline* best_candidate = nullptr; + float best_dot = -1; + size_t best_idx = 0; + + // find another polyline starting here + for (size_t j = 0; j < pp.size(); ++j) { + if (j == i) continue; + ThickPolyline& other = pp[j]; + if (other.endpoints.first && other.endpoints.second) continue; + bool me_reverse = false; + bool other_reverse = false; + if (polyline.last_point().coincides_with(other.last_point())) { + other_reverse = true; + } else if (polyline.first_point().coincides_with(other.last_point())) { + me_reverse = true; + other_reverse = true; + } else if (polyline.first_point().coincides_with(other.first_point())) { + me_reverse = true; + } else if (!polyline.last_point().coincides_with(other.first_point())) { + continue; + } + + Pointf v_poly(me_reverse ? polyline.lines().front().vector().x : polyline.lines().back().vector().x, + me_reverse ? polyline.lines().front().vector().y : polyline.lines().back().vector().y); + v_poly.scale(1 / std::sqrt(v_poly.x*v_poly.x + v_poly.y*v_poly.y)); + Pointf v_other(other_reverse ? other.lines().back().vector().x : other.lines().front().vector().x, + other_reverse ? other.lines().back().vector().y : other.lines().front().vector().y); + v_other.scale(1 / std::sqrt(v_other.x*v_other.x + v_other.y*v_other.y)); + float other_dot = std::abs(float(v_poly.x*v_other.x + v_poly.y*v_other.y)); + if (other_dot > best_dot) { + best_candidate = &other; + best_idx = j; + best_dot = other_dot; + } + } + if (best_candidate != nullptr && best_candidate->points.size() > 1) { + if (polyline.last_point().coincides_with(best_candidate->last_point())) { + best_candidate->reverse(); + } else if (polyline.first_point().coincides_with(best_candidate->last_point())) { + polyline.reverse(); + best_candidate->reverse(); + } else if (polyline.first_point().coincides_with(best_candidate->first_point())) { + polyline.reverse(); + } + //intersections may create over-extrusion because the included circle can be a bit larger. We have to make it short again if needed. + if (polyline.points.size() > 1 && best_candidate->points.size() > 1 + && polyline.width.back() > polyline.width[polyline.width.size() - 2] + && polyline.width.back() > best_candidate->width[1]) { + polyline.width.back() = std::min(polyline.width[polyline.width.size() - 2], best_candidate->width[1]); + } + polyline.points.insert(polyline.points.end(), best_candidate->points.begin() + 1, best_candidate->points.end()); + polyline.width.insert(polyline.width.end(), best_candidate->width.begin() + 1, best_candidate->width.end()); + polyline.endpoints.second = best_candidate->endpoints.second; + assert(polyline.width.size() == polyline.points.size()); + if (best_idx < i) i--; + pp.erase(pp.begin() + best_idx); + } + } +} + +void +MedialAxis::remove_too_thin_points(ThickPolylines& pp) +{ + //remove too thin polylines points (inside a polyline : split it) + for (size_t i = 0; i < pp.size(); ++i) { + ThickPolyline& polyline = pp[i]; + + // remove bits with too small extrusion + size_t idx_point = 0; + while (idx_point polyline.length()) { + shortest_size = polyline.length(); + shortest_idx = i; + } + + } + } + if (shortest_idx < pp.size()) { + pp.erase(pp.begin() + shortest_idx); + changes = true; + } + if (changes) concatThickPolylines(pp); + } +} + +void +MedialAxis::ensure_not_overextrude(ThickPolylines& pp) +{ + //ensure the volume extruded is correct for what we have been asked + // => don't over-extrude + double surface = 0; + double volume = 0; + for (ThickPolyline& polyline : pp) { + for (ThickLine &l : polyline.thicklines()) { + surface += l.length() * (l.a_width + l.b_width) / 2; + double width_mean = (l.a_width + l.b_width) / 2; + volume += height * (width_mean - height * (1. - 0.25 * PI)) * l.length(); + } + } + + // compute bounds volume + double boundsVolume = 0; + boundsVolume += height*bounds.area(); + // add external "perimeter gap" + double perimeterRoundGap = bounds.contour.length() * height * (1 - 0.25*PI) * 0.5; + // add holes "perimeter gaps" + double holesGaps = 0; + for (const Polygon &hole : bounds.holes) { + holesGaps += hole.length() * height * (1 - 0.25*PI) * 0.5; + } + boundsVolume += perimeterRoundGap + holesGaps; + + if (boundsVolume < volume) { + //reduce width + double reduce_by = boundsVolume / volume; + for (ThickPolyline& polyline : pp) { + for (coordf_t &width : polyline.width) { + width *= reduce_by; + } + } + } +} + +ExPolygon +MedialAxis::simplify_polygon_frontier() +{ + + //simplify the boundary between us and the bounds. + //it will remove every point in the surface contour that aren't on the bounds contour + ExPolygon simplified_poly = this->surface; + if (&this->surface != &this->bounds) { + bool need_intersect = false; + for (size_t i = 0; i < simplified_poly.contour.points.size(); i++) { + Point &p_check = simplified_poly.contour.points[i]; + //if (!find) { + if (!bounds.has_boundary_point(p_check)) { + //check if we put it at a bound point instead of delete it + size_t prev_i = i == 0 ? simplified_poly.contour.points.size() - 1 : (i - 1); + size_t next_i = i == simplified_poly.contour.points.size() - 1 ? 0 : (i + 1); + const Point* closest = bounds.contour.closest_point(p_check); + if (closest != nullptr && closest->distance_to(p_check) + SCALED_EPSILON + < std::min(p_check.distance_to(simplified_poly.contour.points[prev_i]), p_check.distance_to(simplified_poly.contour.points[next_i])) / 2) { + p_check.x = closest->x; + p_check.y = closest->y; + need_intersect = true; + } else { + simplified_poly.contour.points.erase(simplified_poly.contour.points.begin() + i); + i--; + } + } + } + if (need_intersect) { + ExPolygons simplified_polys = intersection_ex(simplified_poly, bounds); + if (simplified_polys.size() == 1) { + simplified_poly = simplified_polys[0]; + } else { + simplified_poly = this->surface; + } + } + } + + if (!simplified_poly.contour.points.empty()) + simplified_poly.remove_point_too_near((coord_t)SCALED_RESOLUTION); + return simplified_poly; +} + +/// Grow the extrusion to at least nozzle_diameter*1.05 (lowest safe extrusion width) +/// Do not grow points inside the anchor. +void +MedialAxis::grow_to_nozzle_diameter(ThickPolylines& pp, const ExPolygons& anchors) { + //ensure the width is not lower than 0.4. + for (ThickPolyline& polyline : pp) { + for (int i = 0; i < polyline.points.size(); ++i) { + bool is_anchored = false; + for (const ExPolygon &poly : anchors) { + if (poly.contains(polyline.points[i])) { + is_anchored = true; + break; + } + } + if (!is_anchored && polyline.width[i] < nozzle_diameter * 1.05) + polyline.width[i] = nozzle_diameter * 1.05; + } + } +} + +void +MedialAxis::taper_ends(ThickPolylines& pp) { + // minimum size of the taper: be sure to extrude at least the "round edges" of the extrusion (0-spacing extrusion). + const coord_t min_size = std::max(this->nozzle_diameter * 0.1, this->height * (1. - 0.25 * PI)); + const coordf_t length = std::min(this->anchor_size, (this->nozzle_diameter - min_size) / 2); + if (length <= SCALED_RESOLUTION) return; + //ensure the width is not lower than 0.4. + for (ThickPolyline& polyline : pp) { + if (polyline.length() < length * 2.2) continue; + if (polyline.endpoints.first) { + polyline.width[0] = min_size; + coord_t current_dist = min_size; + coord_t last_dist = min_size; + for (size_t i = 1; i length) { + //create a new point if not near enough + if (current_dist > polyline.width[i] + SCALED_RESOLUTION) { + coordf_t percent_dist = (polyline.width[i] - polyline.width[i - 1]) / (current_dist - last_dist); + polyline.points.insert(polyline.points.begin() + i, polyline.points[i - 1].interpolate(percent_dist, polyline.points[i])); + polyline.width.insert(polyline.width.begin() + i, polyline.width[i]); + } + break; + } + polyline.width[i] = std::max((coordf_t)min_size, min_size + (polyline.width[i] - min_size) * current_dist / length); + last_dist = current_dist; + } + } + if (polyline.endpoints.second) { + const size_t back_idx = polyline.width.size() - 1; + polyline.width[back_idx] = min_size; + coord_t current_dist = min_size; + coord_t last_dist = min_size; + for (size_t i = 1; i length) { + //create new point if not near enough + if (current_dist > polyline.width[back_idx - i] + SCALED_RESOLUTION) { + coordf_t percent_dist = (polyline.width[back_idx - i] - polyline.width[back_idx - i + 1]) / (current_dist - last_dist); + polyline.points.insert(polyline.points.begin() + back_idx - i + 1, polyline.points[back_idx - i + 1].interpolate(percent_dist, polyline.points[back_idx - i])); + polyline.width.insert(polyline.width.begin() + back_idx - i + 1, polyline.width[back_idx - i]); + } + break; + } + polyline.width[back_idx - i] = std::max((coordf_t)min_size, min_size + (polyline.width[back_idx - i] - min_size) * current_dist / length); + last_dist = current_dist; + } + } + } +} + +void +MedialAxis::build(ThickPolylines* polylines_out) +{ + this->id++; + + this->expolygon = simplify_polygon_frontier(); + //safety check + if (this->expolygon.area() < this->min_width * this->min_width) this->expolygon = this->surface; + if (this->expolygon.area() < this->min_width * this->min_width) return; + + + // compute the Voronoi diagram and extract medial axis polylines + ThickPolylines pp; + this->polyline_from_voronoi(this->expolygon.lines(), &pp); + + concatThickPolylines(pp); + + //{ + // stringstream stri; + // stri << "medial_axis_1_voronoi_" << id << ".svg"; + // SVG svg(stri.str()); + // svg.draw(bounds); + // svg.draw(this->expolygon); + // svg.draw(pp); + // svg.Close(); + //} + + /* Find the maximum width returned; we're going to use this for validating and + filtering the output segments. */ + double max_w = 0; + for (ThickPolylines::const_iterator it = pp.begin(); it != pp.end(); ++it) + max_w = fmaxf(max_w, *std::max_element(it->width.begin(), it->width.end())); + + + fusion_curve(pp); + //{ + // stringstream stri; + // stri << "medial_axis_2_curve_" << id << ".svg"; + // SVG svg(stri.str()); + // svg.draw(bounds); + // svg.draw(this->expolygon); + // svg.draw(pp); + // svg.Close(); + //} + + // Aligned fusion: Fusion the bits at the end of lines by "increasing thickness" + // For that, we have to find other lines, + // and with a next point no more distant than the max width. + // Then, we can merge the bit from the first point to the second by following the mean. + // + main_fusion(pp); + //{ + // stringstream stri; + // stri << "medial_axis_3_fusion_" << id << ".svg"; + // SVG svg(stri.str()); + // svg.draw(bounds); + // svg.draw(this->expolygon); + // svg.draw(pp); + // svg.Close(); + //} + + //fusion right-angle corners. + fusion_corners(pp); + + // Loop through all returned polylines in order to extend their endpoints to the + // expolygon boundaries (if done here, it may be cut later if not thick enough) + if (stop_at_min_width) { + extends_line_both_side(pp); + } + + //reduce extrusion when it's too thin to be printable + remove_too_thin_extrusion(pp); + //{ + // stringstream stri; + // stri << "medial_axis_4_thinok_" << id << ".svg"; + // SVG svg(stri.str()); + // svg.draw(bounds); + // svg.draw(this->expolygon); + // svg.draw(pp); + // svg.Close(); + //} + + remove_too_thin_points(pp); + //{ + // stringstream stri; + // stri << "medial_axis_5.0_thuinner_" << id << ".svg"; + // SVG svg(stri.str()); + // svg.draw(bounds); + // svg.draw(this->expolygon); + // svg.draw(pp); + // svg.Close(); + //} + + // Loop through all returned polylines in order to extend their endpoints to the + // expolygon boundaries + if (!stop_at_min_width) { + extends_line_both_side(pp); + } + //{ + // stringstream stri; + // stri << "medial_axis_5_expand_" << id << ".svg"; + // SVG svg(stri.str()); + // svg.draw(bounds); + // svg.draw(this->expolygon); + // svg.draw(pp); + // svg.Close(); + //} + + concatenate_polylines_with_crossing(pp); + //{ + // stringstream stri; + // stri << "medial_axis_6_concat_" << id << ".svg"; + // SVG svg(stri.str()); + // svg.draw(bounds); + // svg.draw(this->expolygon); + // svg.draw(pp); + // svg.Close(); + //} + + remove_too_short_polylines(pp, max_w * 2); + //{ + // stringstream stri; + // stri << "medial_axis_8_tooshort_" << id << ".svg"; + // SVG svg(stri.str()); + // svg.draw(bounds); + // svg.draw(this->expolygon); + // svg.draw(pp); + // svg.Close(); + //} + + //TODO: reduce the flow at the intersection ( + ) points ? + ensure_not_overextrude(pp); + //{ + // stringstream stri; + // stri << "medial_axis_9_endn_" << id << ".svg"; + // SVG svg(stri.str()); + // svg.draw(bounds); + // svg.draw(this->expolygon); + // svg.draw(pp); + // svg.Close(); + //} + + if (nozzle_diameter != min_width) { + grow_to_nozzle_diameter(pp, diff_ex(this->bounds, this->expolygon)); + taper_ends(pp); + } + + polylines_out->insert(polylines_out->end(), pp.begin(), pp.end()); + +} + +ExtrusionEntityCollection +discretize_variable_width(const ThickPolylines &polylines, ExtrusionRole role, Flow flow) +{ + // this value determines granularity of adaptive width, as G-code does not allow + // variable extrusion within a single move; this value shall only affect the amount + // of segments, and any pruning shall be performed before we apply this tolerance + const double tolerance = 4*SCALED_RESOLUTION;//scale_(0.05); + + ExtrusionEntityCollection coll; + for (const ThickPolyline &p : polylines) { + ExtrusionPaths paths; + ExtrusionPath path(role); + ThickLines lines = p.thicklines(); + + for (int i = 0; i < (int)lines.size(); ++i) { + ThickLine& line = lines[i]; + + const coordf_t line_len = line.length(); + if (line_len < SCALED_EPSILON) continue; + + assert(line.a_width >= 0); + assert(line.b_width >= 0); + double thickness_delta = fabs(line.a_width - line.b_width); + if (thickness_delta > tolerance && ceil(thickness_delta / tolerance) > 2) { + const size_t segments = 1 + std::min(16000.0, ceil(thickness_delta / tolerance)); + Points pp; + std::vector width; + { + for (size_t j = 0; j < segments; ++j) { + pp.push_back(line.a.interpolate(((double)j) / segments, line.b)); + double percent_width = ((double)j) / (segments-1); + width.push_back(line.a_width*(1 - percent_width) + line.b_width*percent_width); + } + pp.push_back(line.b); + + assert(pp.size() == segments + 1); + assert(width.size() == segments); + } + + // delete this line and insert new ones + lines.erase(lines.begin() + i); + for (size_t j = 0; j < segments; ++j) { + ThickLine new_line(pp[j], pp[j + 1]); + new_line.a_width = width[j]; + new_line.b_width = width[j]; + lines.insert(lines.begin() + i + j, new_line); + } + + --i; + continue; + } else if (thickness_delta > 0) { + //create a middle point + ThickLine new_line(line.a.interpolate(0.5, line.b), line.b); + new_line.a_width = line.b_width; + new_line.b_width = line.b_width; + line.b = new_line.a; + line.b_width = line.a_width; + lines.insert(lines.begin() + i + 1, new_line); + + --i; + continue; + } + //gapfill : we want to be able to fill the voids (touching the perimeters), so the spacing is what we want. + //thinwall: we want the extrusion to not go out of the polygon, so the width is what we want. + // but we can't extrude with a negative spacing, so we have to gradually fall back to spacing if the width is too small. + + // default: extrude a thin wall that doesn't go outside of the specified width. + coordf_t wanted_width = unscale(line.a_width); + if (role == erGapFill) { + // Convert from spacing to extrusion width based on the extrusion model + // of a square extrusion ended with semi circles. + wanted_width = unscale(line.a_width) + flow.height * (1. - 0.25 * PI); + } else if (unscale(line.a_width) < 2 * flow.height * (1. - 0.25 * PI)) { + //width (too) small, be sure to not extrude with negative spacing. + //we began to fall back to spacing gradually even before the spacing go into the negative + // to make extrusion1 < extrusion2 if width1 < width2 even if width2 is too small. + wanted_width = unscale(line.a_width)*0.35 + 1.3 * flow.height * (1. - 0.25 * PI); + } + + if (path.polyline.points.empty()) { + flow.width = wanted_width; + path.polyline.append(line.a); + path.polyline.append(line.b); + path.mm3_per_mm = flow.mm3_per_mm(); + path.width = flow.width; + path.height = flow.height; + } else { + thickness_delta = scale_(fabs(flow.width - wanted_width)); + if (thickness_delta <= tolerance / 2) { + // the width difference between this line and the current flow width is + // within the accepted tolerance + path.polyline.append(line.b); + } else { + // we need to initialize a new line + paths.emplace_back(std::move(path)); + path = ExtrusionPath(role); + --i; + } + } + } + if (path.polyline.is_valid()) + paths.emplace_back(std::move(path)); + // Append paths to collection. + if (!paths.empty()) { + if (paths.front().first_point().coincides_with(paths.back().last_point())) { + coll.append(ExtrusionLoop(paths)); + } else { + //TODO: add them to an unsortable collection, to be able to keep the order anchor->outside + // and to keep the order of extrusion (do not stop extruding at intersection to turn => bad) + // BUT this need the ironing code to be able to have multiple collection inside each other + // BUT this need the ironing code to be able to have the no_sort flag to be useful + //ExtrusionEntityCollection unsortable_coll(paths); + //unsortable_coll.no_sort = true; + //coll.append(unsortable_coll); + coll.append(paths); + } + } + } + + return coll; +} + +} // namespace Slic3r diff --git a/xs/src/libslic3r/MedialAxis.hpp b/xs/src/libslic3r/MedialAxis.hpp new file mode 100644 index 0000000000..635f68f2c8 --- /dev/null +++ b/xs/src/libslic3r/MedialAxis.hpp @@ -0,0 +1,76 @@ +#ifndef slic3r_MedialAxis_hpp_ +#define slic3r_MedialAxis_hpp_ + +#include "libslic3r.h" +#include "ExPolygon.hpp" +#include "Polyline.hpp" +#include "Geometry.hpp" +#include "ExtrusionEntityCollection.hpp" +#include "Flow.hpp" +#include + +#include "boost/polygon/voronoi.hpp" +using boost::polygon::voronoi_builder; +using boost::polygon::voronoi_diagram; + +namespace Slic3r { + + class MedialAxis { + public: + Lines lines; //lines is here only to avoid passing it in argument of many methods. Initialized in polyline_from_voronoi. + ExPolygon expolygon; + + const ExPolygon& surface; + const ExPolygon& bounds; + const coord_t max_width; + const coord_t min_width; + const coord_t height; + coord_t nozzle_diameter; + coord_t anchor_size; + bool stop_at_min_width = true; + MedialAxis(const ExPolygon &_expolygon, const ExPolygon &_bounds, const coord_t _max_width, const coord_t _min_width, const coord_t _height) + : surface(_expolygon), bounds(_bounds), max_width(_max_width), min_width(_min_width), height(_height) { + nozzle_diameter = _min_width; + anchor_size = 0; + }; + void build(ThickPolylines* polylines_out); + void build(Polylines* polylines); + + private: + static int id; + class VD : public voronoi_diagram { + public: + typedef double coord_type; + typedef boost::polygon::point_data point_type; + typedef boost::polygon::segment_data segment_type; + typedef boost::polygon::rectangle_data rect_type; + }; + VD vd; + std::set edges, valid_edges; + std::map > thickness; + void process_edge_neighbors(const VD::edge_type* edge, ThickPolyline* polyline); + bool validate_edge(const VD::edge_type* edge); + const Line& retrieve_segment(const VD::cell_type* cell) const; + const Point& retrieve_endpoint(const VD::cell_type* cell) const; + void polyline_from_voronoi(const Lines& voronoi_edges, ThickPolylines* polylines_out); + + ExPolygon simplify_polygon_frontier(); + void fusion_curve(ThickPolylines &pp); + void main_fusion(ThickPolylines& pp); + void fusion_corners(ThickPolylines &pp); + void extends_line_both_side(ThickPolylines& pp); + void extends_line(ThickPolyline& polyline, const ExPolygons& anchors, const coord_t join_width); + void remove_too_thin_extrusion(ThickPolylines& pp); + void concatenate_polylines_with_crossing(ThickPolylines& pp); + void remove_too_thin_points(ThickPolylines& pp); + void remove_too_short_polylines(ThickPolylines& pp, const coord_t min_size); + void ensure_not_overextrude(ThickPolylines& pp); + void grow_to_nozzle_diameter(ThickPolylines& pp, const ExPolygons& anchors); + void taper_ends(ThickPolylines& pp); + }; + + ExtrusionEntityCollection discretize_variable_width(const ThickPolylines &polylines, ExtrusionRole role, Flow flow); +} + + +#endif diff --git a/xs/src/libslic3r/MultiPoint.cpp b/xs/src/libslic3r/MultiPoint.cpp index d8ee1d09d6..4625bcffd2 100644 --- a/xs/src/libslic3r/MultiPoint.cpp +++ b/xs/src/libslic3r/MultiPoint.cpp @@ -158,6 +158,30 @@ MultiPoint::intersection(const Line& line, Point* intersection) const return false; } +bool +MultiPoint::first_intersection(const Line& line, Point* intersection) const +{ + bool found = false; + double dmin = 0.; + for (const Line &l : this->lines()) { + Point ip; + if (l.intersection(line, &ip)) { + if (! found) { + found = true; + dmin = ip.distance_to(line.a); + *intersection = ip; + } else { + double d = ip.distance_to(line.a); + if (d < dmin) { + dmin = d; + *intersection = ip; + } + } + } + } + return found; +} + std::string MultiPoint::dump_perl() const { diff --git a/xs/src/libslic3r/MultiPoint.hpp b/xs/src/libslic3r/MultiPoint.hpp index acb409b205..07abe553ac 100644 --- a/xs/src/libslic3r/MultiPoint.hpp +++ b/xs/src/libslic3r/MultiPoint.hpp @@ -31,6 +31,25 @@ class MultiPoint int find_point(const Point &point) const; bool has_boundary_point(const Point &point) const; + /// return the index of the closest point in this polygon in relation with "point" + /// \param point the point to compare with. + /// \return the index of the closest point in the points vector, or max(size_t) if points is empty. + size_t closest_point_index(const Point &point) const { + size_t idx = -1; + if (! this->points.empty()) { + idx = 0; + double dist_min = this->points.front().distance_to_sq(point); + for (size_t i = 1; i < this->points.size(); ++ i) { + double d = this->points[i].distance_to_sq(point); + if (d < dist_min) { + dist_min = d; + idx = i; + } + } + } + return idx; + } + const Point* closest_point(const Point &point) const { return this->points.empty() ? nullptr : &this->points[this->closest_point_index(point)]; } BoundingBox bounding_box() const; // Return true if there are exact duplicates. @@ -43,6 +62,7 @@ class MultiPoint void append(const Points &points); void append(const Points::const_iterator &begin, const Points::const_iterator &end); bool intersection(const Line& line, Point* intersection) const; + bool first_intersection(const Line& line, Point* intersection) const; std::string dump_perl() const; static Points _douglas_peucker(const Points &points, const double tolerance); diff --git a/xs/src/libslic3r/PerimeterGenerator.cpp b/xs/src/libslic3r/PerimeterGenerator.cpp index e789a94dce..e86a429edc 100644 --- a/xs/src/libslic3r/PerimeterGenerator.cpp +++ b/xs/src/libslic3r/PerimeterGenerator.cpp @@ -1,6 +1,7 @@ #include "PerimeterGenerator.hpp" #include "ClipperUtils.hpp" #include "ExtrusionEntityCollection.hpp" +#include "MedialAxis.hpp" #include #include @@ -26,6 +27,9 @@ PerimeterGenerator::process() // solid infill coord_t ispacing = this->solid_infill_flow.scaled_spacing(); + //nozzle diameter + const double nozzle_diameter = this->print_config->nozzle_diameter.get_at(this->config->perimeter_extruder-1); + // Calculate the minimum required spacing between two adjacent traces. // This should be equal to the nominal flow spacing but we experiment // with some tolerance in order to avoid triggering medial axis when @@ -47,8 +51,6 @@ PerimeterGenerator::process() // We consider overhang any part where the entire nozzle diameter is not supported by the // lower layer, so we take lower slices and offset them by half the nozzle diameter used // in the current layer - double nozzle_diameter = this->print_config->nozzle_diameter.get_at(this->config->perimeter_extruder-1); - this->_lower_slices_p = offset(*this->lower_slices, scale_(+nozzle_diameter/2)); } @@ -85,44 +87,78 @@ PerimeterGenerator::process() for (int i = 0; i <= loop_number+1; ++i) { // outer loop is 0 Polygons offsets; if (i == 0) { - // the minimum thickness of a single loop is: - // ext_width/2 + ext_spacing/2 + spacing/2 + width/2 + // compute next onion, without taking care of thin_walls : destroy too thin areas. + if (!this->config->thin_walls) + offsets = offset(last, -(float)(ext_pwidth / 2)); + + + // look for thin walls if (this->config->thin_walls) { + // the minimum thickness of a single loop is: + // ext_width/2 + ext_spacing/2 + spacing/2 + width/2 + //here, we shrink & grow by ext_min_spacing to remove areas where the current loop can't be extruded offsets = offset2( last, -(ext_pwidth/2 + ext_min_spacing/2 - 1), - +(ext_min_spacing/2 - 1) - ); - } else { - offsets = offset(last, -ext_pwidth/2); - } - - // look for thin walls - if (this->config->thin_walls) { - Polygons no_thin_zone = offset(offsets, +ext_pwidth/2); - Polygons diffpp = diff( - last, - no_thin_zone, - true // medial axis requires non-overlapping geometry - ); + +(ext_min_spacing/2 - 1)); + + // detect edge case where a curve can be split in multiple small chunks. + Polygons no_thin_onion = offset(last, -(float)(ext_pwidth / 2)); + float div = 2; + while (no_thin_onion.size()>0 && offsets.size() > no_thin_onion.size() && no_thin_onion.size() + offsets.size() > 3) { + div += 0.5; + //use a sightly smaller offset2 spacing to try to improve the split, but with a little bit of over-extrusion + Polygons next_onion_secondTry = offset2( + last, + -(float)(ext_pwidth / 2 + ext_min_spacing / div - 1), + +(float)(ext_min_spacing / div - 1)); + if (offsets.size() > next_onion_secondTry.size() * 1.1) { + offsets = next_onion_secondTry; + } + if (div > 3) break; + } // the following offset2 ensures almost nothing in @thin_walls is narrower than $min_width // (actually, something larger than that still may exist due to mitering or other causes) - coord_t min_width = scale_(this->ext_perimeter_flow.nozzle_diameter / 3); - ExPolygons expp = offset2_ex(diffpp, -min_width/2, +min_width/2); - - // compute a bit of overlap to anchor thin walls inside the print. - ExPolygons anchor = intersection_ex(to_polygons(offset_ex(expp, (float)(ext_pwidth / 2))), no_thin_zone, true); + coord_t min_width = scale_(this->config->thin_walls_min_width.get_abs_value(this->ext_perimeter_flow.nozzle_diameter)); - // the maximum thickness of our thin wall area is equal to the minimum thickness of a single loop - for (ExPolygons::const_iterator ex = expp.begin(); ex != expp.end(); ++ex) { - ExPolygons bounds = _clipper_ex(ClipperLib::ctUnion, (Polygons)*ex, to_polygons(anchor), true); - //search our bound + Polygons no_thin_zone = offset(offsets, ext_pwidth/2, CLIPPER_OFFSET_SCALE, jtSquare, 3); + // medial axis requires non-overlapping geometry + Polygons thin_zones = diff(last, no_thin_zone, true); + //don't use offset2_ex, because we don't want to merge the zones that have been separated. + //a very little bit of overlap can be created here with other thin polygons, but it's more useful than worisome. + ExPolygons half_thins = offset_ex(thin_zones, (float)(-min_width / 2)); + //simplify them + for (ExPolygon &half_thin : half_thins) { + half_thin.remove_point_too_near(SCALED_RESOLUTION); + } + //we push the bits removed and put them into what we will use as our anchor + if (half_thins.size() > 0) { + no_thin_zone = diff(last, to_polygons(offset_ex(half_thins, (float)(min_width / 2) - SCALED_EPSILON)), true); + } + // compute a bit of overlap to anchor thin walls inside the print. + for (ExPolygon &half_thin : half_thins) { + //growing back the polygon + ExPolygons thin = offset_ex(half_thin, (float)(min_width / 2)); + assert(thin.size() == 1); + double overlap = (coord_t)scale_(this->config->thin_walls_overlap.get_abs_value(this->ext_perimeter_flow.nozzle_diameter)); + ExPolygons anchor = intersection_ex( + to_polygons(offset_ex(half_thin, (float)(min_width / 2 + overlap), CLIPPER_OFFSET_SCALE, jtSquare, 3)), + no_thin_zone, true); + ExPolygons bounds = _clipper_ex(ClipperLib::ctUnion, to_polygons(thin), to_polygons(anchor), true); for (ExPolygon &bound : bounds) { - if (!intersection_ex(*ex, bound).empty()) { - // the maximum thickness of our thin wall area is equal to the minimum thickness of a single loop - ex->medial_axis(bound, ext_pwidth + ext_pspacing2, min_width, &thin_walls); - continue; + if (!intersection_ex(thin[0], bound).empty()) { + //be sure it's not too small to extrude reliably + thin[0].remove_point_too_near(SCALED_RESOLUTION); + if (thin[0].area() > min_width*(ext_pwidth + ext_pspacing2)) { + bound.remove_point_too_near(SCALED_RESOLUTION); + // the maximum thickness of our thin wall area is equal to the minimum thickness of a single loop + Slic3r::MedialAxis ma(thin[0], bound, ext_pwidth + ext_pspacing2, min_width, this->layer_height); + ma.nozzle_diameter = (coord_t)scale_(this->ext_perimeter_flow.nozzle_diameter); + ma.anchor_size = overlap; + ma.build(&thin_walls); + } + break; } } } @@ -284,6 +320,8 @@ PerimeterGenerator::process() // collapse double min = 0.2*pwidth * (1 - INSET_OVERLAP_TOLERANCE); + //be sure we don't gapfill where the perimeters are already touching each other (negative spacing). + min = std::max(min, double(Flow::new_from_spacing(EPSILON, nozzle_diameter, this->layer_height, false).scaled_width())); double max = 2*pspacing; ExPolygons gaps_ex = diff_ex( offset2(gaps, -min/2, +min/2), @@ -292,12 +330,15 @@ PerimeterGenerator::process() ); ThickPolylines polylines; - for (ExPolygons::const_iterator ex = gaps_ex.begin(); ex != gaps_ex.end(); ++ex) - ex->medial_axis(*ex, max, min, &polylines); - + for (const ExPolygon &ex : gaps_ex) { + //remove too small gaps that are too hard to fill. + //ie one that are smaller than an extrusion with width of min and a length of max. + if (ex.area() > min*max) { + ex.medial_axis(ex, max, min, &polylines, this->layer_height); + } + } if (!polylines.empty()) { - ExtrusionEntityCollection gap_fill = this->_variable_width(polylines, - erGapFill, this->solid_infill_flow); + ExtrusionEntityCollection gap_fill = discretize_variable_width(polylines, erGapFill, this->solid_infill_flow); this->gap_fill->append(gap_fill.entities); @@ -421,8 +462,7 @@ PerimeterGenerator::_traverse_loops(const PerimeterGeneratorLoops &loops, // append thin walls to the nearest-neighbor search (only for first iteration) if (!thin_walls.empty()) { - ExtrusionEntityCollection tw = this->_variable_width - (thin_walls, erExternalPerimeter, this->ext_perimeter_flow); + ExtrusionEntityCollection tw = discretize_variable_width(thin_walls, erExternalPerimeter, this->ext_perimeter_flow); coll.append(tw.entities); thin_walls.clear(); @@ -463,110 +503,6 @@ PerimeterGenerator::_traverse_loops(const PerimeterGeneratorLoops &loops, return entities; } -ExtrusionEntityCollection -PerimeterGenerator::_variable_width(const ThickPolylines &polylines, ExtrusionRole role, Flow flow) const -{ - // this value determines granularity of adaptive width, as G-code does not allow - // variable extrusion within a single move; this value shall only affect the amount - // of segments, and any pruning shall be performed before we apply this tolerance - const double tolerance = scale_(0.05); - - ExtrusionEntityCollection coll; - for (ThickPolylines::const_iterator p = polylines.begin(); p != polylines.end(); ++p) { - ExtrusionPaths paths; - ExtrusionPath path(role); - ThickLines lines = p->thicklines(); - - for (int i = 0; i < (int)lines.size(); ++i) { - const ThickLine& line = lines[i]; - - const coordf_t line_len = line.length(); - if (line_len < SCALED_EPSILON) continue; - - double thickness_delta = fabs(line.a_width - line.b_width); - if (thickness_delta > tolerance) { - const size_t segments = ceil(thickness_delta / tolerance); - const coordf_t seg_len = line_len / segments; - Points pp; - std::vector width; - { - pp.push_back(line.a); - width.push_back(line.a_width); - for (size_t j = 1; j < segments; ++j) { - pp.push_back(line.point_at(j*seg_len)); - - coordf_t w = line.a_width + (j*seg_len) * (line.b_width-line.a_width) / line_len; - width.push_back(w); - width.push_back(w); - } - pp.push_back(line.b); - width.push_back(line.b_width); - - assert(pp.size() == segments + 1); - assert(width.size() == segments*2); - } - - // delete this line and insert new ones - lines.erase(lines.begin() + i); - for (size_t j = 0; j < segments; ++j) { - ThickLine new_line(pp[j], pp[j+1]); - new_line.a_width = width[2*j]; - new_line.b_width = width[2*j+1]; - lines.insert(lines.begin() + i + j, new_line); - } - - --i; - continue; - } - - const double w = fmax(line.a_width, line.b_width); - - if (path.polyline.points.empty()) { - flow.width = unscale(w); - #ifdef SLIC3R_DEBUG - printf(" filling %f gap\n", flow.width); - #endif - - // make sure we don't include too thin segments which - // may cause even slightly negative mm3_per_mm because of floating point math - path.mm3_per_mm = flow.mm3_per_mm(); - if (path.mm3_per_mm < EPSILON) continue; - - path.width = flow.width; - path.height = flow.height; - path.polyline.append(line.a); - path.polyline.append(line.b); - } else { - thickness_delta = fabs(scale_(flow.width) - w); - if (thickness_delta <= tolerance) { - // the width difference between this line and the current flow width is - // within the accepted tolerance - - path.polyline.append(line.b); - } else { - // we need to initialize a new line - paths.push_back(path); - path = ExtrusionPath(role); - --i; - } - } - } - if (path.polyline.is_valid()) - paths.push_back(path); - - // append paths to collection - if (!paths.empty()) { - if (paths.front().first_point().coincides_with(paths.back().last_point())) { - coll.append(ExtrusionLoop(paths)); - } else { - coll.append(paths); - } - } - } - - return coll; -} - bool PerimeterGeneratorLoop::is_internal_contour() const { diff --git a/xs/src/libslic3r/PerimeterGenerator.hpp b/xs/src/libslic3r/PerimeterGenerator.hpp index 0e7fbd3e4b..4bd7802c3b 100644 --- a/xs/src/libslic3r/PerimeterGenerator.hpp +++ b/xs/src/libslic3r/PerimeterGenerator.hpp @@ -86,8 +86,6 @@ class PerimeterGenerator { ExtrusionEntityCollection _traverse_loops(const PerimeterGeneratorLoops &loops, ThickPolylines &thin_walls) const; - ExtrusionEntityCollection _variable_width - (const ThickPolylines &polylines, ExtrusionRole role, Flow flow) const; }; } diff --git a/xs/src/libslic3r/Point.cpp b/xs/src/libslic3r/Point.cpp index c67dadb3bf..28749c7ea8 100644 --- a/xs/src/libslic3r/Point.cpp +++ b/xs/src/libslic3r/Point.cpp @@ -187,6 +187,14 @@ Point::distance_to(const Point &point) const return sqrt(dx*dx + dy*dy); } +double +Point::distance_to_sq(const Point &point) const +{ + double dx = ((double)point.x - this->x); + double dy = ((double)point.y - this->y); + return dx*dx + dy*dy; +} + /* distance to the closest point of line */ double Point::distance_to(const Line &line) const @@ -297,6 +305,19 @@ Point::projection_onto(const Line &line) const } } +/// This method create a new point on the line defined by this and p2. +/// The new point is place at position defined by |p2-this| * percent, starting from this +/// \param percent the proportion of the segment length to place the point +/// \param p2 the second point, forming a segment with this +/// \return a new point, == this if percent is 0 and == p2 if percent is 1 +Point Point::interpolate(const double percent, const Point &p2) const +{ + Point p_out; + p_out.x = this->x*(1 - percent) + p2.x*(percent); + p_out.y = this->y*(1 - percent) + p2.y*(percent); + return p_out; +} + Point Point::negative() const { diff --git a/xs/src/libslic3r/Point.hpp b/xs/src/libslic3r/Point.hpp index d52e31aaf9..671e7775ba 100644 --- a/xs/src/libslic3r/Point.hpp +++ b/xs/src/libslic3r/Point.hpp @@ -47,6 +47,7 @@ class Point static Point new_scale(Pointf p); bool operator==(const Point& rhs) const; bool operator!=(const Point& rhs) const { return !(*this == rhs); } + bool operator<(const Point& rhs) const { return this->x < rhs.x || (this->x == rhs.x && this->y < rhs.y); } std::string wkt() const; std::string dump_perl() const; void scale(double factor); @@ -73,6 +74,7 @@ class Point bool nearest_point(const Points &points, Point* point) const; bool nearest_waypoint(const Points &points, const Point &dest, Point* point) const; double distance_to(const Point &point) const; + double distance_to_sq(const Point &point) const; double distance_to(const Line &line) const; double perp_distance_to(const Line &line) const; double ccw(const Point &p1, const Point &p2) const; @@ -80,6 +82,7 @@ class Point double ccw_angle(const Point &p1, const Point &p2) const; Point projection_onto(const MultiPoint &poly) const; Point projection_onto(const Line &line) const; + Point interpolate(const double percent, const Point &p) const; Point negative() const; Vector vector_to(const Point &point) const; void align_to_grid(const Point &spacing, const Point &base = Point(0,0)); @@ -128,6 +131,8 @@ class Pointf bool operator==(const Pointf& rhs) const; bool coincides_with_epsilon(const Pointf& rhs) const; Pointf& operator/=(const double& scalar); + bool operator!=(const Pointf &rhs) const { return ! (*this == rhs); } + bool operator< (const Pointf& rhs) const { return this->x < rhs.x || (this->x == rhs.x && this->y < rhs.y); } std::string wkt() const; std::string dump_perl() const; @@ -138,12 +143,18 @@ class Pointf void rotate(double angle, const Pointf ¢er); Pointf negative() const; Vectorf vector_to(const Pointf &point) const; + }; Pointf operator+(const Pointf& point1, const Pointf& point2); Pointf operator/(const Pointf& point1, const double& scalar); - -std::ostream& operator<<(std::ostream &stm, const Pointf3 &pointf3); +inline Pointf operator*(double scalar, const Pointf& p) { return Pointf(scalar * p.x, scalar * p.y); } +inline Pointf operator*(const Pointf& p, double scalar) { return Pointf(scalar * p.x, scalar * p.y); } +inline Vectorf normalize(const Vectorf& v) +{ + coordf_t len = sqrt((v.x*v.x) + (v.y*v.y)); + return (len != 0.0) ? 1.0 / len * v : Vectorf(0.0, 0.0); +} class Pointf3 : public Pointf { @@ -161,6 +172,8 @@ class Pointf3 : public Pointf Vectorf3 vector_to(const Pointf3 &point) const; }; +std::ostream& operator<<(std::ostream &stm, const Pointf3 &pointf3); + template inline Points to_points(const std::vector &items) diff --git a/xs/src/libslic3r/Polyline.cpp b/xs/src/libslic3r/Polyline.cpp index b80335073f..e5d8b440fd 100644 --- a/xs/src/libslic3r/Polyline.cpp +++ b/xs/src/libslic3r/Polyline.cpp @@ -6,6 +6,7 @@ #include "ClipperUtils.hpp" #include #include +#include namespace Slic3r { @@ -235,8 +236,8 @@ ThickPolyline::thicklines() const lines.reserve(this->points.size() - 1); for (size_t i = 0; i < this->points.size()-1; ++i) { ThickLine line(this->points[i], this->points[i+1]); - line.a_width = this->width[2*i]; - line.b_width = this->width[2*i+1]; + line.a_width = this->width[i]; + line.b_width = this->width[i + 1]; lines.push_back(line); } } @@ -251,4 +252,105 @@ ThickPolyline::reverse() std::swap(this->endpoints.first, this->endpoints.second); } +void +concatThickPolylines(ThickPolylines& pp) { + bool changes = true; + while (changes){ + changes = false; + //concat polyline if only 2 polyline at a point + for (size_t i = 0; i < pp.size(); ++i) { + ThickPolyline *polyline = &pp[i]; + if (polyline->first_point().coincides_with(polyline->last_point())) { + polyline->endpoints.first = false; + polyline->endpoints.second = false; + continue; + } + + size_t id_candidate_first_point = -1; + size_t id_candidate_last_point = -1; + size_t nbCandidate_first_point = 0; + size_t nbCandidate_last_point = 0; + // find another polyline starting here + for (size_t j = 0; j < pp.size(); ++j) { + if (j == i) continue; + ThickPolyline *other = &pp[j]; + if (other->first_point().coincides_with(other->last_point())) continue; + if (polyline->last_point().coincides_with(other->last_point())) { + //other->reverse(); + id_candidate_last_point = j; + nbCandidate_last_point++; + } + if (polyline->last_point().coincides_with(other->first_point())) { + id_candidate_last_point = j; + nbCandidate_last_point++; + } + if (polyline->first_point().coincides_with(other->last_point())) { + id_candidate_first_point = j; + nbCandidate_first_point++; + } + if (polyline->first_point().coincides_with(other->first_point())) { + id_candidate_first_point = j; + nbCandidate_first_point++; + //other->reverse(); + } + } + if (id_candidate_last_point == id_candidate_first_point && nbCandidate_first_point == 1 && nbCandidate_last_point == 1) { + if (polyline->first_point().coincides_with(pp[id_candidate_first_point].first_point())) pp[id_candidate_first_point].reverse(); + // it's a trap! it's a loop! + polyline->points.insert(polyline->points.end(), pp[id_candidate_first_point].points.begin() + 1, pp[id_candidate_first_point].points.end()); + polyline->width.insert(polyline->width.end(), pp[id_candidate_first_point].width.begin() + 1, pp[id_candidate_first_point].width.end()); + pp.erase(pp.begin() + id_candidate_first_point); + changes = true; + polyline->endpoints.first = false; + polyline->endpoints.second = false; + } else { + + if (nbCandidate_first_point == 1) { + if (polyline->first_point().coincides_with(pp[id_candidate_first_point].first_point())) pp[id_candidate_first_point].reverse(); + //concat at front + polyline->width[0] = std::max(polyline->width.front(), pp[id_candidate_first_point].width.back()); + polyline->points.insert(polyline->points.begin(), pp[id_candidate_first_point].points.begin(), pp[id_candidate_first_point].points.end() - 1); + polyline->width.insert(polyline->width.begin(), pp[id_candidate_first_point].width.begin(), pp[id_candidate_first_point].width.end() - 1); + polyline->endpoints.first = pp[id_candidate_first_point].endpoints.first; + pp.erase(pp.begin() + id_candidate_first_point); + changes = true; + if (id_candidate_first_point < i) { + i--; + polyline = &pp[i]; + } + if (id_candidate_last_point > id_candidate_first_point) { + id_candidate_last_point--; + } + } else if (nbCandidate_first_point == 0) { + //update endpoint + polyline->endpoints.first = true; + } + if (nbCandidate_last_point == 1) { + if (polyline->last_point().coincides_with(pp[id_candidate_last_point].last_point())) pp[id_candidate_last_point].reverse(); + //concat at back + polyline->width[polyline->width.size() - 1] = std::max(polyline->width.back(), pp[id_candidate_last_point].width.front()); + polyline->points.insert(polyline->points.end(), pp[id_candidate_last_point].points.begin() + 1, pp[id_candidate_last_point].points.end()); + polyline->width.insert(polyline->width.end(), pp[id_candidate_last_point].width.begin() + 1, pp[id_candidate_last_point].width.end()); + polyline->endpoints.second = pp[id_candidate_last_point].endpoints.second; + pp.erase(pp.begin() + id_candidate_last_point); + changes = true; + if (id_candidate_last_point < i) { + i--; + polyline = &pp[i]; + } + } else if (nbCandidate_last_point == 0) { + //update endpoint + polyline->endpoints.second = true; + } + + if (polyline->last_point().coincides_with(polyline->first_point())) { + //the concat has created a loop : update endpoints + polyline->endpoints.first = false; + polyline->endpoints.second = false; + } + } + } + } +} + } diff --git a/xs/src/libslic3r/Polyline.hpp b/xs/src/libslic3r/Polyline.hpp index 49e261bdf7..db4111db3b 100644 --- a/xs/src/libslic3r/Polyline.hpp +++ b/xs/src/libslic3r/Polyline.hpp @@ -36,15 +36,26 @@ class Polyline : public MultiPoint { Polygons grow(double delta, double scale = CLIPPER_OFFSET_SCALE, ClipperLib::JoinType joinType = ClipperLib::jtSquare, double miterLimit = 3.0) const; }; + +/// ThickPolyline : a polyline with a width for each point +/// This class has a vector of coordf_t, it must be the same size as points. +/// it's used to store the size of the line at this point. +/// Also, the endpoint let us know if the front() and back() of the polyline +/// join something or is a dead-end. class ThickPolyline : public Polyline { public: + /// width size must be == point size std::vector width; + /// if true => it's an endpoint, if false it join an other ThickPolyline. first is at front(), second is at back() std::pair endpoints; ThickPolyline() : endpoints(std::make_pair(false, false)) {}; ThickLines thicklines() const; void reverse(); }; +/// concatenate poylines if possible and refresh the endpoints +void concatThickPolylines(ThickPolylines &polylines); + inline Polylines to_polylines(const Polygons &polygons) { diff --git a/xs/src/libslic3r/PrintConfig.cpp b/xs/src/libslic3r/PrintConfig.cpp index d0113b5029..e94bcc0094 100644 --- a/xs/src/libslic3r/PrintConfig.cpp +++ b/xs/src/libslic3r/PrintConfig.cpp @@ -1596,12 +1596,28 @@ PrintConfigDef::PrintConfigDef() } def = this->add("thin_walls", coBool); - def->label = __TRANS("Detect thin walls"); + def->label = __TRANS(""); def->category = __TRANS("Layers and Perimeters"); def->tooltip = __TRANS("Detect single-width walls (parts where two extrusions don't fit and we need to collapse them into a single trace)."); def->cli = "thin-walls!"; def->default_value = new ConfigOptionBool(true); + def = this->add("thin_walls_min_width", coFloatOrPercent); + def->label = __TRANS("min width"); + def->category = __TRANS("Layers and Perimeters"); + def->tooltip = __TRANS("Minimum width for the extrusion to be extruded (widths lower than the nozzle diameter will be over-extruded at the nozzle diameter). Can be percent of the nozzle size."); + def->cli = "thin-walls-min-width=s"; + def->min = 0; + def->default_value = new ConfigOptionFloatOrPercent(33, true); + + def = this->add("thin_walls_overlap", coFloatOrPercent); + def->label = __TRANS("overlap"); + def->category = __TRANS("Layers and Perimeters"); + def->tooltip = __TRANS("Overlap between the thin walls and the perimeters. Can be a % of the external perimeter width (default 50%)"); + def->cli = "thin-walls-overlap=s"; + def->min = 0; + def->default_value = new ConfigOptionFloatOrPercent(50, true); + def = this->add("threads", coInt); def->label = __TRANS("Threads"); def->tooltip = __TRANS("Threads are used to parallelize long-running tasks. Optimal threads number is slightly above the number of available cores/processors."); diff --git a/xs/src/libslic3r/PrintConfig.hpp b/xs/src/libslic3r/PrintConfig.hpp index 8176446934..726012d7b7 100644 --- a/xs/src/libslic3r/PrintConfig.hpp +++ b/xs/src/libslic3r/PrintConfig.hpp @@ -269,6 +269,8 @@ class PrintRegionConfig : public virtual StaticPrintConfig ConfigOptionInt solid_infill_every_layers; ConfigOptionFloatOrPercent solid_infill_speed; ConfigOptionBool thin_walls; + ConfigOptionFloatOrPercent thin_walls_min_width; + ConfigOptionFloatOrPercent thin_walls_overlap; ConfigOptionFloatOrPercent top_infill_extrusion_width; ConfigOptionEnum top_infill_pattern; ConfigOptionInt top_solid_layers; @@ -311,6 +313,8 @@ class PrintRegionConfig : public virtual StaticPrintConfig OPT_PTR(solid_infill_every_layers); OPT_PTR(solid_infill_speed); OPT_PTR(thin_walls); + OPT_PTR(thin_walls_min_width); + OPT_PTR(thin_walls_overlap); OPT_PTR(top_infill_extrusion_width); OPT_PTR(top_infill_pattern); OPT_PTR(top_solid_infill_speed); diff --git a/xs/src/libslic3r/PrintRegion.cpp b/xs/src/libslic3r/PrintRegion.cpp index 6c5c6ed467..bda491383c 100644 --- a/xs/src/libslic3r/PrintRegion.cpp +++ b/xs/src/libslic3r/PrintRegion.cpp @@ -82,6 +82,8 @@ PrintRegion::invalidate_state_by_config(const PrintConfigBase &config) || opt_key == "first_layer_extrusion_width" || opt_key == "perimeter_extrusion_width" || opt_key == "thin_walls" + || opt_key == "thin_walls_min_width" + || opt_key == "thin_walls_overlap" || opt_key == "external_perimeters_first") { steps.insert(posPerimeters); } else if (opt_key == "first_layer_extrusion_width") {