{"action":"created","comment":{"url":"https://api.github.com/repos/slic3r/Slic3r/pulls/comments/240583915","pull_request_review_id":183587661,"id":240583915,"node_id":"MDI0OlB1bGxSZXF1ZXN0UmV2aWV3Q29tbWVudDI0MDU4MzkxNQ==","diff_hunk":"@@ -0,0 +1,1477 @@\n+#include \"BoundingBox.hpp\"\n+#include \"ExPolygon.hpp\"\n+#include \"Geometry.hpp\"\n+#include \"Polygon.hpp\"\n+#include \"Line.hpp\"\n+#include \"ClipperUtils.hpp\"\n+#include \"SVG.hpp\"\n+#include \"polypartition.h\"\n+#include \"poly2tri/poly2tri.h\"\n+#include <algorithm>\n+#include <cassert>\n+#include <list>\n+\n+namespace Slic3r {\n+\n+int MedialAxis::id = 0;\n+    \n+void\n+MedialAxis::build(Polylines* polylines)\n+{\n+    ThickPolylines tp;\n+    this->build(&tp);\n+    polylines->insert(polylines->end(), tp.begin(), tp.end());\n+}\n+\n+void\n+MedialAxis::polyline_from_voronoi(const Lines& voronoi_edges, ThickPolylines* polylines)\n+{\n+    this->lines = voronoi_edges;\n+    construct_voronoi(lines.begin(), lines.end(), &this->vd);\n+\n+    /*\n+    // DEBUG: dump all Voronoi edges\n+    {\n+        for (VD::const_edge_iterator edge = this->vd.edges().begin(); edge != this->vd.edges().end(); ++edge) {\n+            if (edge->is_infinite()) continue;\n+            \n+            ThickPolyline polyline;\n+            polyline.points.push_back(Point( edge->vertex0()->x(), edge->vertex0()->y() ));\n+            polyline.points.push_back(Point( edge->vertex1()->x(), edge->vertex1()->y() ));\n+            polylines->push_back(polyline);\n+        }\n+        return;\n+    }\n+    */\n+    \n+    // typedef const VD::vertex_type vert_t;\n+    typedef const VD::edge_type   edge_t;\n+    \n+    // collect valid edges (i.e. prune those not belonging to MAT)\n+    // note: this keeps twins, so it inserts twice the number of the valid edges\n+    this->valid_edges.clear();\n+    {\n+        std::set<const VD::edge_type*> seen_edges;\n+        for (VD::const_edge_iterator edge = this->vd.edges().begin(); edge != this->vd.edges().end(); ++edge) {\n+            // if we only process segments representing closed loops, none if the\n+            // infinite edges (if any) would be part of our MAT anyway\n+            if (edge->is_secondary() || edge->is_infinite()) continue;\n+        \n+            // don't re-validate twins\n+            if (seen_edges.find(&*edge) != seen_edges.end()) continue;  // TODO: is this needed?\n+            seen_edges.insert(&*edge);\n+            seen_edges.insert(edge->twin());\n+            \n+            if (!this->validate_edge(&*edge)) continue;\n+            this->valid_edges.insert(&*edge);\n+            this->valid_edges.insert(edge->twin());\n+        }\n+    }\n+    this->edges = this->valid_edges;\n+    \n+    // iterate through the valid edges to build polylines\n+    while (!this->edges.empty()) {\n+        const edge_t* edge = *this->edges.begin();\n+        \n+        // start a polyline\n+        ThickPolyline polyline;\n+        polyline.points.push_back(Point( edge->vertex0()->x(), edge->vertex0()->y() ));\n+        polyline.points.push_back(Point( edge->vertex1()->x(), edge->vertex1()->y() ));\n+        polyline.width.push_back(this->thickness[edge].first);\n+        polyline.width.push_back(this->thickness[edge].second);\n+        \n+        // remove this edge and its twin from the available edges\n+        (void)this->edges.erase(edge);\n+        (void)this->edges.erase(edge->twin());\n+        \n+        // get next points\n+        this->process_edge_neighbors(edge, &polyline);\n+        \n+        // get previous points\n+        {\n+            ThickPolyline rpolyline;\n+            this->process_edge_neighbors(edge->twin(), &rpolyline);\n+            polyline.points.insert(polyline.points.begin(), rpolyline.points.rbegin(), rpolyline.points.rend());\n+            polyline.width.insert(polyline.width.begin(), rpolyline.width.rbegin(), rpolyline.width.rend());\n+            polyline.endpoints.first = rpolyline.endpoints.second;\n+        }\n+        \n+        assert(polyline.width.size() == polyline.points.size());\n+        \n+        // prevent loop endpoints from being extended\n+        if (polyline.first_point().coincides_with(polyline.last_point())) {\n+            polyline.endpoints.first = false;\n+            polyline.endpoints.second = false;\n+        }\n+        \n+        // append polyline to result\n+        polylines->push_back(polyline);\n+    }\n+\n+    #ifdef SLIC3R_DEBUG\n+    {\n+        static int iRun = 0;\n+        dump_voronoi_to_svg(this->lines, this->vd, polylines, debug_out_path(\"MedialAxis-%d.svg\", iRun ++).c_str());\n+        printf(\"Thick lines: \");\n+        for (ThickPolylines::const_iterator it = polylines->begin(); it != polylines->end(); ++ it) {\n+            ThickLines lines = it->thicklines();\n+            for (ThickLines::const_iterator it2 = lines.begin(); it2 != lines.end(); ++ it2) {\n+                printf(\"%f,%f \", it2->a_width, it2->b_width);\n+            }\n+        }\n+        printf(\"\\n\");\n+    }\n+    #endif /* SLIC3R_DEBUG */\n+}\n+\n+void\n+MedialAxis::process_edge_neighbors(const VD::edge_type* edge, ThickPolyline* polyline)\n+{\n+    while (true) {\n+        // Since rot_next() works on the edge starting point but we want\n+        // to find neighbors on the ending point, we just swap edge with\n+        // its twin.\n+        const VD::edge_type* twin = edge->twin();\n+    \n+        // count neighbors for this edge\n+        std::vector<const VD::edge_type*> neighbors;\n+        for (const VD::edge_type* neighbor = twin->rot_next(); neighbor != twin;\n+            neighbor = neighbor->rot_next()) {\n+            if (this->valid_edges.count(neighbor) > 0) neighbors.push_back(neighbor);\n+        }\n+    \n+        // if we have a single neighbor then we can continue recursively\n+        if (neighbors.size() == 1) {\n+            const VD::edge_type* neighbor = neighbors.front();\n+            \n+            // break if this is a closed loop\n+            if (this->edges.count(neighbor) == 0) return;\n+            \n+            Point new_point(neighbor->vertex1()->x(), neighbor->vertex1()->y());\n+            polyline->points.push_back(new_point);\n+            polyline->width.push_back(this->thickness[neighbor].second);\n+            \n+            (void)this->edges.erase(neighbor);\n+            (void)this->edges.erase(neighbor->twin());\n+            edge = neighbor;\n+        } else if (neighbors.size() == 0) {\n+            polyline->endpoints.second = true;\n+            return;\n+        } else {\n+            // T-shaped or star-shaped joint\n+            return;\n+        }\n+    }\n+}\n+\n+bool\n+MedialAxis::validate_edge(const VD::edge_type* edge)\n+{\n+    // prevent overflows and detect almost-infinite edges\n+    if (std::abs(edge->vertex0()->x()) > double(MAX_COORD) || \n+        std::abs(edge->vertex0()->y()) > double(MAX_COORD) || \n+        std::abs(edge->vertex1()->x()) > double(MAX_COORD) ||\n+        std::abs(edge->vertex1()->y()) > double(MAX_COORD))\n+        return false;\n+\n+    // construct the line representing this edge of the Voronoi diagram\n+    const Line line(\n+        Point( edge->vertex0()->x(), edge->vertex0()->y() ),\n+        Point( edge->vertex1()->x(), edge->vertex1()->y() )\n+    );\n+    \n+    // discard edge if it lies outside the supplied shape\n+    // this could maybe be optimized (checking inclusion of the endpoints\n+    // might give false positives as they might belong to the contour itself)\n+    if (line.a.coincides_with(line.b)) {\n+        // in this case, contains(line) returns a false positive\n+        if (!this->expolygon.contains(line.a)) return false;\n+    } else {\n+        if (!this->expolygon.contains(line)) return false;\n+    }\n+    \n+    // retrieve the original line segments which generated the edge we're checking\n+    const VD::cell_type* cell_l = edge->cell();\n+    const VD::cell_type* cell_r = edge->twin()->cell();\n+    const Line &segment_l = this->retrieve_segment(cell_l);\n+    const Line &segment_r = this->retrieve_segment(cell_r);\n+    \n+    \n+    //SVG svg(\"edge.svg\");\n+    //svg.draw(this->expolygon.expolygon);\n+    //svg.draw(line);\n+    //svg.draw(segment_l, \"red\");\n+    //svg.draw(segment_r, \"blue\");\n+    //svg.Close();\n+    //\n+    \n+    /*  Calculate thickness of the cross-section at both the endpoints of this edge.\n+        Our Voronoi edge is part of a CCW sequence going around its Voronoi cell \n+        located on the left side. (segment_l).\n+        This edge's twin goes around segment_r. Thus, segment_r is \n+        oriented in the same direction as our main edge, and segment_l is oriented\n+        in the same direction as our twin edge.\n+        We used to only consider the (half-)distances to segment_r, and that works\n+        whenever segment_l and segment_r are almost specular and facing. However, \n+        at curves they are staggered and they only face for a very little length\n+        (our very short edge represents such visibility).\n+        Both w0 and w1 can be calculated either towards cell_l or cell_r with equal\n+        results by Voronoi definition.\n+        When cell_l or cell_r don't refer to the segment but only to an endpoint, we\n+        calculate the distance to that endpoint instead.  */\n+    \n+    coordf_t w0 = cell_r->contains_segment()\n+        ? line.a.distance_to(segment_r)*2\n+        : line.a.distance_to(this->retrieve_endpoint(cell_r))*2;\n+    \n+    coordf_t w1 = cell_l->contains_segment()\n+        ? line.b.distance_to(segment_l)*2\n+        : line.b.distance_to(this->retrieve_endpoint(cell_l))*2;\n+    \n+    //don't remove the line that goes to the intersection of the contour\n+    // we use them to create nicer thin wall lines\n+    //if (cell_l->contains_segment() && cell_r->contains_segment()) {\n+    //    // calculate the relative angle between the two boundary segments\n+    //    double angle = fabs(segment_r.orientation() - segment_l.orientation());\n+    //    if (angle > PI) angle = 2*PI - angle;\n+    //    assert(angle >= 0 && angle <= PI);\n+    //    \n+    //    // fabs(angle) ranges from 0 (collinear, same direction) to PI (collinear, opposite direction)\n+    //    // we're interested only in segments close to the second case (facing segments)\n+    //    // so we allow some tolerance.\n+    //    // this filter ensures that we're dealing with a narrow/oriented area (longer than thick)\n+    //    // we don't run it on edges not generated by two segments (thus generated by one segment\n+    //    // and the endpoint of another segment), since their orientation would not be meaningful\n+    //    if (PI - angle > PI/8) {\n+    //        // angle is not narrow enough\n+    //        \n+    //        // only apply this filter to segments that are not too short otherwise their \n+    //        // angle could possibly be not meaningful\n+    //        if (w0 < SCALED_EPSILON || w1 < SCALED_EPSILON || line.length() >= this->min_width)\n+    //            return false;\n+    //    }\n+    //} else {\n+    //    if (w0 < SCALED_EPSILON || w1 < SCALED_EPSILON)\n+    //        return false;\n+    //}\n+\n+    // don't do that before we try to fusion them\n+    //if (w0 < this->min_width && w1 < this->min_width)\n+    //    return false;\n+    //\n+\n+    //shouldn't occur if perimeter_generator is well made\n+    if (w0 > this->max_width && w1 > this->max_width)\n+        return false;\n+    \n+    this->thickness[edge]         = std::make_pair(w0, w1);\n+    this->thickness[edge->twin()] = std::make_pair(w1, w0);\n+    \n+    return true;\n+}\n+\n+const Line&\n+MedialAxis::retrieve_segment(const VD::cell_type* cell) const\n+{\n+    return lines[cell->source_index()];\n+}\n+\n+const Point&\n+MedialAxis::retrieve_endpoint(const VD::cell_type* cell) const\n+{\n+    const Line& line = this->retrieve_segment(cell);\n+    if (cell->source_category() == boost::polygon::SOURCE_CATEGORY_SEGMENT_START_POINT) {\n+        return line.a;\n+    } else {\n+        return line.b;\n+    }\n+}\n+\n+\n+/// remove point that are at SCALED_EPSILON * 2 distance.\n+void\n+remove_point_too_near(ThickPolyline* to_reduce)\n+{\n+    const coord_t smallest = SCALED_EPSILON * 2;\n+    size_t id = 1;\n+    while (id < to_reduce->points.size() - 1) {\n+        size_t newdist = min(to_reduce->points[id].distance_to(to_reduce->points[id - 1])\n+            , to_reduce->points[id].distance_to(to_reduce->points[id + 1]));\n+        if (newdist < smallest) {\n+            to_reduce->points.erase(to_reduce->points.begin() + id);\n+            to_reduce->width.erase(to_reduce->width.begin() + id);\n+            newdist = to_reduce->points[id].distance_to(to_reduce->points[id - 1]);\n+            //if you removed a point, it check if the next one isn't too near from the previous one.\n+            // if not, it bypass it.\n+            if (newdist > smallest) {\n+                ++id;\n+            }\n+        }\n+        //go to next one\n+        else ++id;\n+    }\n+}\n+\n+/// add points  from pattern to to_modify at the same % of the length\n+/// so not add if an other point is present at the correct position\n+void\n+add_point_same_percent(ThickPolyline* pattern, ThickPolyline* to_modify)\n+{\n+    const double to_modify_length = to_modify->length();\n+    const double percent_epsilon = SCALED_EPSILON / to_modify_length;\n+    const double pattern_length = pattern->length();\n+\n+    double percent_length = 0;\n+    for (size_t idx_point = 1; idx_point < pattern->points.size() - 1; ++idx_point) {\n+        percent_length += pattern->points[idx_point-1].distance_to(pattern->points[idx_point]) / pattern_length;\n+        //find position \n+        size_t idx_other = 1;\n+        double percent_length_other_before = 0;\n+        double percent_length_other = 0;\n+        while (idx_other < to_modify->points.size()) {\n+            percent_length_other_before = percent_length_other;\n+            percent_length_other += to_modify->points[idx_other-1].distance_to(to_modify->points[idx_other])\n+                / to_modify_length;\n+            if (percent_length_other > percent_length - percent_epsilon) {\n+                //if higher (we have gone over it)\n+                break;\n+            }\n+            ++idx_other;\n+        }\n+        if (percent_length_other > percent_length + percent_epsilon) {\n+            //insert a new point before the position\n+            double percent_dist = (percent_length - percent_length_other_before) / (percent_length_other - percent_length_other_before);\n+            coordf_t new_width = to_modify->width[idx_other - 1] * (1 - percent_dist);\n+            new_width += to_modify->width[idx_other] * (percent_dist);\n+            Point new_point;\n+            new_point.x = (coord_t)((double)(to_modify->points[idx_other - 1].x) * (1 - percent_dist));\n+            new_point.x += (coord_t)((double)(to_modify->points[idx_other].x) * (percent_dist));\n+            new_point.y = (coord_t)((double)(to_modify->points[idx_other - 1].y) * (1 - percent_dist));\n+            new_point.y += (coord_t)((double)(to_modify->points[idx_other].y) * (percent_dist));\n+            to_modify->width.insert(to_modify->width.begin() + idx_other, new_width);\n+            to_modify->points.insert(to_modify->points.begin() + idx_other, new_point);\n+        }\n+    }\n+}\n+\n+/// find the nearest angle in the contour (or 2 nearest if it's difficult to choose) \n+/// return 1 for an angle of 90� and 0 for an angle of 0� or 180�\n+/// find the nearest angle in the contour (or 2 nearest if it's difficult to choose) \n+/// return 1 for an angle of 90� and 0 for an angle of 0� or 180�\n+double\n+get_coeff_from_angle_countour(Point &point, const ExPolygon &contour, coord_t min_dist_between_point) {\n+    double nearest_dist = point.distance_to(contour.contour.points.front());\n+    Point point_nearest = contour.contour.points.front();\n+    size_t id_nearest = 0;\n+    double near_dist = nearest_dist;\n+    Point point_near = point_nearest;\n+    size_t id_near = 0;\n+    for (size_t id_point = 1; id_point < contour.contour.points.size(); ++id_point) {\n+        if (nearest_dist > point.distance_to(contour.contour.points[id_point])) {\n+            //update point_near\n+            id_near = id_nearest;\n+            point_near = point_nearest;\n+            near_dist = nearest_dist;\n+            //update nearest\n+            nearest_dist = point.distance_to(contour.contour.points[id_point]);\n+            point_nearest = contour.contour.points[id_point];\n+            id_nearest = id_point;\n+        }\n+    }\n+    double angle = 0;\n+    size_t id_before = id_nearest == 0 ? contour.contour.points.size() - 1 : id_nearest - 1;\n+    Point point_before = id_nearest == 0 ? contour.contour.points.back() : contour.contour.points[id_nearest - 1];\n+    //Search one point far enough to be relevant\n+    while (point_nearest.distance_to(point_before) < min_dist_between_point) {\n+        point_before = id_before == 0 ? contour.contour.points.back() : contour.contour.points[id_before - 1];\n+        id_before = id_before == 0 ? contour.contour.points.size() - 1 : id_before - 1;\n+        //don't loop\n+        if (id_before == id_nearest) {\n+            id_before = id_nearest == 0 ? contour.contour.points.size() - 1 : id_nearest - 1;\n+            point_before = id_nearest == 0 ? contour.contour.points.back() : contour.contour.points[id_nearest - 1];\n+            break;\n+        }\n+    }\n+    size_t id_after = id_nearest == contour.contour.points.size() - 1 ? 0 : id_nearest + 1;\n+    Point point_after = id_nearest == contour.contour.points.size() - 1 ? contour.contour.points.front() : contour.contour.points[id_nearest + 1];\n+    //Search one point far enough to be relevant\n+    while (point_nearest.distance_to(point_after) < min_dist_between_point) {\n+        point_after = id_after == contour.contour.points.size() - 1 ? contour.contour.points.front() : contour.contour.points[id_after + 1];\n+        id_after = id_after == contour.contour.points.size() - 1 ? 0 : id_after + 1;\n+        //don't loop\n+        if (id_after == id_nearest) {\n+            id_after = id_nearest == contour.contour.points.size() - 1 ? 0 : id_nearest + 1;\n+            point_after = id_nearest == contour.contour.points.size() - 1 ? contour.contour.points.front() : contour.contour.points[id_nearest + 1];\n+            break;\n+        }\n+    }\n+    //compute angle\n+    angle = point_nearest.ccw_angle(point_before, point_after);\n+    if (angle >= PI) angle = 2 * PI - angle;  // smaller angle\n+    //compute the diff from 90�\n+    angle = abs(angle - PI / 2);\n+    if (point_near.coincides_with(point_nearest) && max(nearest_dist, near_dist) + SCALED_EPSILON < point_nearest.distance_to(point_near)) {\n+        //not only nearest\n+        Point point_before = id_near == 0 ? contour.contour.points.back() : contour.contour.points[id_near - 1];\n+        Point point_after = id_near == contour.contour.points.size() - 1 ? contour.contour.points.front() : contour.contour.points[id_near + 1];\n+        double angle2 = min(point_nearest.ccw_angle(point_before, point_after), point_nearest.ccw_angle(point_after, point_before));\n+        angle2 = abs(angle - PI / 2);\n+        angle = (angle + angle2) / 2;\n+    }\n+\n+    return 1 - (angle / (PI / 2));\n+}\n+\n+double\n+dot(Line l1, Line l2)\n+{\n+    Vectorf v_1 = normalize(Vectorf(l1.b.x - l1.a.x, l1.b.y - l1.a.y));\n+    Vectorf v_2 = normalize(Vectorf(l2.b.x - l2.a.x, l2.b.y - l2.a.y));\n+    return v_1.x*v_2.x + v_1.y*v_2.y;\n+}\n+\n+void\n+MedialAxis::fusion_curve(ThickPolylines &pp)\n+{\n+    //fusion Y with only 1 '0' value => the \"0\" branch \"pull\" the cross-point\n+    bool changes = false;\n+    for (size_t i = 0; i < pp.size(); ++i) {\n+        ThickPolyline& polyline = pp[i];\n+        // only consider 2-point polyline with endpoint\n+        if (polyline.points.size() != 2) continue;\n+        if (polyline.endpoints.first) polyline.reverse();\n+        else if (!polyline.endpoints.second) continue;\n+        if (polyline.width.back() > EPSILON) continue;\n+\n+        //check my length is small\n+        coord_t length = (coord_t)polyline.length();\n+        if (length > max_width) continue;\n+\n+        size_t closest_point_idx = this->expolygon.contour.closest_point_index(polyline.points.back());\n+\n+        //check the 0-wodth point is on the contour.\n+        if (closest_point_idx == (size_t)-1) continue;\n+\n+        size_t prev_idx = closest_point_idx == 0 ? this->expolygon.contour.points.size() - 1 : closest_point_idx - 1;\n+        size_t next_idx = closest_point_idx == this->expolygon.contour.points.size() - 1 ? 0 : closest_point_idx + 1;\n+        double mindot = 1;\n+        mindot = min(mindot, abs(dot(Line(polyline.points[polyline.points.size() - 1], polyline.points[polyline.points.size() - 2]),\n+            (Line(this->expolygon.contour.points[closest_point_idx], this->expolygon.contour.points[prev_idx])))));\n+        mindot = min(mindot, abs(dot(Line(polyline.points[polyline.points.size() - 1], polyline.points[polyline.points.size() - 2]),\n+            (Line(this->expolygon.contour.points[closest_point_idx], this->expolygon.contour.points[next_idx])))));\n+\n+        //compute angle\n+        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]);\n+        if (coeff_contour_angle >= PI) coeff_contour_angle = 2 * PI - coeff_contour_angle;  // smaller angle\n+        //compute the diff from 90�\n+        coeff_contour_angle = abs(coeff_contour_angle - PI / 2);\n+\n+\n+        // look if other end is a cross point with almost 90� angle\n+        double sum_dot = 0;\n+        double min_dot = 0;\n+        // look if other end is a cross point with multiple other branch\n+        vector<size_t> crosspoint;\n+        for (size_t j = 0; j < pp.size(); ++j) {\n+            if (j == i) continue;\n+            ThickPolyline& other = pp[j];\n+            if (polyline.first_point().coincides_with(other.last_point())) {\n+                other.reverse();\n+                crosspoint.push_back(j);\n+                double dot_temp = dot(Line(polyline.points[0], polyline.points[1]), (Line(other.points[0], other.points[1])));\n+                min_dot = min(min_dot, abs(dot_temp));\n+                sum_dot += dot_temp;\n+            } else if (polyline.first_point().coincides_with(other.first_point())) {\n+                crosspoint.push_back(j);\n+                double dot_temp = dot(Line(polyline.points[0], polyline.points[1]), (Line(other.points[0], other.points[1])));\n+                min_dot = min(min_dot, abs(dot_temp));\n+                sum_dot += dot_temp;\n+            }\n+        }\n+        //only consider very shallow angle for contour\n+        if (mindot > 0.15 &&\n+            (1 - (coeff_contour_angle / (PI / 2))) > 0.2) continue;\n+\n+        //check if it's a line that we can pull\n+        if (crosspoint.size() != 2) continue;\n+        if (sum_dot > 0.2) continue;\n+        if (min_dot > 0.5) continue;\n+\n+        //don't pull, it distords the line if there are too many points.\n+        //// 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)\n+        //coord_t length_pull = polyline.length();\n+        //length_pull *= 0.144 * get_coeff_from_angle_countour(polyline.points.back(), this->expolygon, min(min_width, polyline.length() / 2));\n+\n+        ////compute dir\n+        //Vectorf pull_direction(polyline.points[1].x - polyline.points[0].x, polyline.points[1].y - polyline.points[0].y);\n+        //pull_direction = normalize(pull_direction);\n+        //pull_direction.x *= length_pull;\n+        //pull_direction.y *= length_pull;\n+\n+        ////pull the points\n+        //Point &p1 = pp[crosspoint[0]].points[0];\n+        //p1.x = p1.x + (coord_t)pull_direction.x;\n+        //p1.y = p1.y + (coord_t)pull_direction.y;\n+\n+        //Point &p2 = pp[crosspoint[1]].points[0];\n+        //p2.x = p2.x + (coord_t)pull_direction.x;\n+        //p2.y = p2.y + (coord_t)pull_direction.y;\n+\n+        //delete the now unused polyline\n+        pp.erase(pp.begin() + i);\n+        --i;\n+        changes = true;\n+    }\n+    if (changes) {\n+        concatThickPolylines(pp);\n+        ///reorder, in case of change\n+        std::sort(pp.begin(), pp.end(), [](const ThickPolyline & a, const ThickPolyline & b) { return a.length() < b.length(); });\n+    }\n+}\n+\n+void\n+MedialAxis::fusion_corners(ThickPolylines &pp)\n+{\n+\n+    //fusion Y with only 1 '0' value => the \"0\" branch \"pull\" the cross-point\n+    bool changes = false;\n+    for (size_t i = 0; i < pp.size(); ++i) {\n+        ThickPolyline& polyline = pp[i];\n+        // only consider polyline with 0-end\n+        if (polyline.points.size() != 2) continue;\n+        if (polyline.endpoints.first) polyline.reverse();\n+        else if (!polyline.endpoints.second) continue;\n+        if (polyline.width.back() > 0) continue;\n+\n+        //check my length is small\n+        coord_t length = polyline.length();\n+        if (length > max_width) continue;\n+\n+        // look if other end is a cross point with multiple other branch\n+        vector<size_t> crosspoint;\n+        for (size_t j = 0; j < pp.size(); ++j) {\n+            if (j == i) continue;\n+            ThickPolyline& other = pp[j];\n+            if (polyline.first_point().coincides_with(other.last_point())) {\n+                other.reverse();\n+                crosspoint.push_back(j);\n+            } else if (polyline.first_point().coincides_with(other.first_point())) {\n+                crosspoint.push_back(j);\n+            }\n+        }\n+        //check if it's a line that we can pull\n+        if (crosspoint.size() != 2) continue;\n+\n+        // check if i am at the external side of a curve\n+        double angle1 = polyline.points[0].ccw_angle(polyline.points[1], pp[crosspoint[0]].points[1]);\n+        if (angle1 >= PI) angle1 = 2 * PI - angle1;  // smaller angle\n+        double angle2 = polyline.points[0].ccw_angle(polyline.points[1], pp[crosspoint[1]].points[1]);\n+        if (angle2 >= PI) angle2 = 2 * PI - angle2;  // smaller angle\n+        if (angle1 + angle2 < PI) continue;\n+\n+        //check if is smaller or the other ones are not endpoits\n+        if (pp[crosspoint[0]].endpoints.second && length > pp[crosspoint[0]].length()) continue;\n+        if (pp[crosspoint[1]].endpoints.second && length > pp[crosspoint[1]].length()) continue;\n+\n+        //FIXME: also pull (a bit less) points that are near to this one.\n+        // 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)\n+        coord_t length_pull = polyline.length();\n+        length_pull *= 0.144 * get_coeff_from_angle_countour(polyline.points.back(), this->expolygon, min(min_width, polyline.length() / 2));\n+\n+        //compute dir\n+        Vectorf pull_direction(polyline.points[1].x - polyline.points[0].x, polyline.points[1].y - polyline.points[0].y);\n+        pull_direction = normalize(pull_direction);\n+        pull_direction.x *= length_pull;\n+        pull_direction.y *= length_pull;\n+\n+        //pull the points\n+        Point &p1 = pp[crosspoint[0]].points[0];\n+        p1.x = p1.x + pull_direction.x;\n+        p1.y = p1.y + pull_direction.y;\n+\n+        Point &p2 = pp[crosspoint[1]].points[0];\n+        p2.x = p2.x + pull_direction.x;\n+        p2.y = p2.y + pull_direction.y;\n+\n+        //delete the now unused polyline\n+        pp.erase(pp.begin() + i);\n+        --i;\n+        changes = true;\n+    }\n+    if (changes) {\n+        concatThickPolylines(pp);\n+        ///reorder, in case of change\n+        std::sort(pp.begin(), pp.end(), [](const ThickPolyline & a, const ThickPolyline & b) { return a.length() < b.length(); });\n+    }\n+}\n+\n+\n+void\n+MedialAxis::extends_line(ThickPolyline& polyline, const ExPolygons& anchors, const coord_t join_width)\n+{\n+    // extend initial and final segments of each polyline if they're actual endpoints\n+    // We assign new endpoints to temporary variables because in case of a single-line\n+    // polyline, after we extend the start point it will be caught by the intersection()\n+    // call, so we keep the inner point until we perform the second intersection() as well\n+    if (polyline.endpoints.second && !bounds.has_boundary_point(polyline.points.back())) {\n+        size_t first_idx = polyline.points.size() - 2;\n+        Line line(*(polyline.points.begin() + first_idx), polyline.points.back());\n+        while (line.length() < SCALED_RESOLUTION && first_idx>0) {\n+            first_idx--;\n+            line.a = *(polyline.points.begin() + first_idx);\n+        }\n+        // prevent the line from touching on the other side, otherwise intersection() might return that solution\n+        if (polyline.points.size() == 2 && this->expolygon.contains(line.midpoint())) line.a = line.midpoint();\n+\n+        line.extend_end(max_width);\n+        Point new_back;\n+        if (this->expolygon.contour.has_boundary_point(polyline.points.back())) {\n+            new_back = polyline.points.back();\n+        } else {\n+            //TODO: verify also for holes.\n+            bool finded = this->expolygon.contour.first_intersection(line, &new_back);\n+            //verify also for holes.\n+            Point new_back_temp;\n+            for (Polygon hole : this->expolygon.holes) {\n+                if (hole.first_intersection(line, &new_back_temp)) {\n+                    if (!finded || line.a.distance_to(new_back_temp) < line.a.distance_to(new_back)) {\n+                        finded = true;\n+                        new_back = new_back_temp;\n+                    }\n+                }\n+            }\n+            // safety check if no intersection\n+            if (!finded) new_back = line.b;\n+\n+            polyline.points.push_back(new_back);\n+            polyline.width.push_back(polyline.width.back());\n+        }\n+        Point new_bound;\n+        bool finded = bounds.contour.first_intersection(line, &new_bound);\n+        //verify also for holes.\n+        Point new_bound_temp;\n+        for (Polygon hole : bounds.holes) {\n+            if (hole.first_intersection(line, &new_bound_temp)) {\n+                if (!finded || line.a.distance_to(new_bound_temp) < line.a.distance_to(new_bound)) {\n+                    finded = true;\n+                    new_bound = new_bound_temp;\n+                }\n+            }\n+        }\n+        // safety check if no intersection\n+        if (!finded) {\n+            if (line.b.coincides_with_epsilon(polyline.points.back())) return;\n+            //check if we don't over-shoot inside us\n+            bool is_in_anchor = false;\n+            for (const ExPolygon& a : anchors) {\n+                if (a.contains(line.b)) {\n+                    is_in_anchor = true;\n+                    break;\n+                }\n+            }\n+            if (!is_in_anchor) std::cout << \"not in anchor:\\n\";\n+            if (!is_in_anchor) return;\n+            new_bound = line.b;\n+        }\n+        // find anchor\n+        Point best_anchor;\n+        double shortest_dist = max_width;\n+        for (const ExPolygon& a : anchors) {\n+            Point p_maybe_inside = a.contour.centroid();\n+            double test_dist = new_bound.distance_to(p_maybe_inside) + new_back.distance_to(p_maybe_inside);\n+            //if (test_dist < max_width / 2 && (test_dist < shortest_dist || shortest_dist < 0)) {\n+            double angle_test = new_back.ccw_angle(p_maybe_inside, line.a);\n+            if (angle_test > PI) angle_test = 2 * PI - angle_test;\n+            if (test_dist < max_width && test_dist<shortest_dist && abs(angle_test) > PI / 2) {\n+                shortest_dist = test_dist;\n+                best_anchor = p_maybe_inside;\n+            }\n+        }\n+        if (best_anchor.x != 0 && best_anchor.y != 0) {\n+            Point p_obj = best_anchor + new_bound;\n+            p_obj.x /= 2;\n+            p_obj.y /= 2;\n+            Line l2 = Line(new_back, p_obj);\n+            l2.extend_end(max_width);\n+            (void)bounds.contour.first_intersection(l2, &new_bound);\n+        }\n+        if (new_bound.coincides_with_epsilon(new_back)) {\n+            return;\n+        }\n+        polyline.points.push_back(new_bound);\n+        //polyline.width.push_back(join_width);\n+        //it thickens the line a bit too early, imo\n+        polyline.width.push_back(polyline.width.back());\n+    }\n+}\n+\n+void\n+MedialAxis::main_fusion(ThickPolylines& pp)\n+{\n+    //int idf = 0;\n+    //reoder pp by length (ascending) It's really important to do that to avoid building the line from the width insteand of the length\n+    std::sort(pp.begin(), pp.end(), [](const ThickPolyline & a, const ThickPolyline & b) {\n+        bool ahas0 = a.width.front() == 0 || a.width.back() == 0;\n+        bool bhas0 = b.width.front() == 0 || b.width.back() == 0;\n+        if (ahas0 && !bhas0) return true;\n+        if (!ahas0 && bhas0) return false;\n+        return a.length() < b.length();\n+    });\n+\n+    bool changes = true;\n+    map<Point, double> coeff_angle_cache;\n+    while (changes) {\n+        changes = false;\n+        for (size_t i = 0; i < pp.size(); ++i) {\n+            ThickPolyline& polyline = pp[i];\n+\n+            //simple check to see if i can be fusionned\n+            if (!polyline.endpoints.first && !polyline.endpoints.second) continue;\n+\n+\n+            ThickPolyline* best_candidate = nullptr;\n+            float best_dot = -1;\n+            size_t best_idx = 0;\n+            double dot_poly_branch = 0;\n+            double dot_candidate_branch = 0;\n+\n+            // find another polyline starting here\n+            for (size_t j = i + 1; j < pp.size(); ++j) {\n+                ThickPolyline& other = pp[j];\n+                if (polyline.last_point().coincides_with(other.last_point())) {\n+                    polyline.reverse();\n+                    other.reverse();\n+                } else if (polyline.first_point().coincides_with(other.last_point())) {\n+                    other.reverse();\n+                } else if (polyline.first_point().coincides_with(other.first_point())) {\n+                } else if (polyline.last_point().coincides_with(other.first_point())) {\n+                    polyline.reverse();\n+                } else {\n+                    continue;\n+                }\n+                //std::cout << \" try : \" << i << \":\" << j << \" : \" << \n+                //    (polyline.points.size() < 2 && other.points.size() < 2) <<\n+                //    (!polyline.endpoints.second || !other.endpoints.second) <<\n+                //    ((polyline.points.back().distance_to(other.points.back())\n+                //    + (polyline.width.back() + other.width.back()) / 4)\n+                //    > max_width*1.05) <<\n+                //    (abs(polyline.length() - other.length()) > max_width) << \"\\n\";\n+\n+                //// mergeable tests\n+                if (polyline.points.size() < 2 && other.points.size() < 2) continue;\n+                if (!polyline.endpoints.second || !other.endpoints.second) continue;\n+                // test if the new width will not be too big if a fusion occur\n+                //note that this isn't the real calcul. It's just to avoid merging lines too far apart.\n+                if (\n+                    ((polyline.points.back().distance_to(other.points.back())\n+                    + (polyline.width.back() + other.width.back()) / 4)\n+                > max_width*1.05))\n+                    continue;\n+                // test if the lines are not too different in length.\n+                if (abs(polyline.length() - other.length()) > max_width) continue;\n+\n+\n+                //test if we don't merge with something too different and without any relevance.\n+                double coeffSizePolyI = 1;\n+                if (polyline.width.back() == 0) {\n+                    coeffSizePolyI = 0.1 + 0.9*get_coeff_from_angle_countour(polyline.points.back(), this->expolygon, min(min_width, polyline.length() / 2));\n+                }\n+                double coeffSizeOtherJ = 1;\n+                if (other.width.back() == 0) {\n+                    coeffSizeOtherJ = 0.1 + 0.9*get_coeff_from_angle_countour(other.points.back(), this->expolygon, min(min_width, polyline.length() / 2));\n+                }\n+                //std::cout << \" try2 : \" << i << \":\" << j << \" : \"\n+                //    << (abs(polyline.length()*coeffSizePolyI - other.length()*coeffSizeOtherJ) > max_width / 2)\n+                //    << (abs(polyline.length()*coeffSizePolyI - other.length()*coeffSizeOtherJ) > max_width)\n+                //    << \"\\n\";\n+                if (abs(polyline.length()*coeffSizePolyI - other.length()*coeffSizeOtherJ) > max_width / 2) continue;\n+\n+\n+                //compute angle to see if it's better than previous ones (straighter = better).\n+                //we need to add how strait we are from our main.\n+                float test_dot = dot(polyline.lines().front(), other.lines().front());\n+\n+                // Get the branch/line in wich we may merge, if possible\n+                // with that, we can decide what is important, and how we can merge that.\n+                // angle_poly - angle_candi =90� => one is useless\n+                // both angle are equal => both are useful with same strength\n+                // ex: Y => | both are useful to crete a nice line\n+                // ex2: TTTTT => -----  these 90� useless lines should be discarded\n+                bool find_main_branch = false;\n+                size_t biggest_main_branch_id = 0;\n+                coord_t biggest_main_branch_length = 0;\n+                for (size_t k = 0; k < pp.size(); ++k) {\n+                    //std::cout << \"try to find main : \" << k << \" ? \" << i << \" \" << j << \" \";\n+                    if (k == i || k == j) continue;\n+                    ThickPolyline& main = pp[k];\n+                    if (polyline.first_point().coincides_with(main.last_point())) {\n+                        main.reverse();\n+                        if (!main.endpoints.second)\n+                            find_main_branch = true;\n+                        else if (biggest_main_branch_length < main.length()) {\n+                            biggest_main_branch_id = k;\n+                            biggest_main_branch_length = main.length();\n+                        }\n+                    } else if (polyline.first_point().coincides_with(main.first_point())) {\n+                        if (!main.endpoints.second)\n+                            find_main_branch = true;\n+                        else if (biggest_main_branch_length < main.length()) {\n+                            biggest_main_branch_id = k;\n+                            biggest_main_branch_length = main.length();\n+                        }\n+                    }\n+                    if (find_main_branch) {\n+                        //use this variable to store the good index and break to compute it\n+                        biggest_main_branch_id = k;\n+                        break;\n+                    }\n+                }\n+                if (!find_main_branch && biggest_main_branch_length == 0) {\n+                    // nothing -> it's impossible!\n+                    dot_poly_branch = 0.707;\n+                    dot_candidate_branch = 0.707;\n+                    //std::cout << \"no main branch... impossible!!\\n\";\n+                } else if (!find_main_branch && (\n+                    (pp[biggest_main_branch_id].length() < polyline.length() && (polyline.width.back() != 0 || pp[biggest_main_branch_id].width.back() ==0)) \n+                    || (pp[biggest_main_branch_id].length() < other.length() && (other.width.back() != 0 || pp[biggest_main_branch_id].width.back() == 0)))) {\n+                    //the main branch should have no endpoint or be bigger!\n+                    //here, it have an endpoint, and is not the biggest -> bad!\n+                    continue;\n+                } else {\n+                    //compute the dot (biggest_main_branch_id)\n+                    dot_poly_branch = -dot(Line(polyline.points[0], polyline.points[1]), Line(pp[biggest_main_branch_id].points[0], pp[biggest_main_branch_id].points[1]));\n+                    dot_candidate_branch = -dot(Line(other.points[0], other.points[1]), Line(pp[biggest_main_branch_id].points[0], pp[biggest_main_branch_id].points[1]));\n+                    if (dot_poly_branch < 0) dot_poly_branch = 0;\n+                    if (dot_candidate_branch < 0) dot_candidate_branch = 0;\n+                    if (pp[biggest_main_branch_id].width.back()>0)\n+                        test_dot += 2 * dot_poly_branch ;\n+                }\n+                //test if it's useful to merge or not\n+                //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)\n+                if (dot_poly_branch < 0.1 || dot_candidate_branch < 0.1 ||\n+                    (\n+                        ((polyline.length()>other.length() ? polyline.length() / other.length() : other.length() / polyline.length()) > 4) \n+                        && !(polyline.width.back() == 0 && other.width.back()==0)\n+                    ) ){\n+                    continue;\n+                }\n+                if (test_dot > best_dot) {\n+                    best_candidate = &other;\n+                    best_idx = j;\n+                    best_dot = test_dot;\n+                }\n+            }\n+            if (best_candidate != nullptr) {\n+                //idf++;\n+                //std::cout << \" == fusion \" << id <<\" : \"<< idf << \" ==\\n\";\n+                // delete very near points\n+                remove_point_too_near(&polyline);\n+                remove_point_too_near(best_candidate);\n+\n+                // add point at the same pos than the other line to have a nicer fusion\n+                add_point_same_percent(&polyline, best_candidate);\n+                add_point_same_percent(best_candidate, &polyline);\n+\n+                //get the angle of the nearest points of the contour to see : _| (good) \\_ (average) __(bad)\n+                //sqrt because the result are nicer this way: don't over-penalize /_ angles\n+                //TODO: try if we can achieve a better result if we use a different algo if the angle is <90�\n+                const double coeff_angle_poly = (coeff_angle_cache.find(polyline.points.back()) != coeff_angle_cache.end())\n+                    ? coeff_angle_cache[polyline.points.back()]\n+                    : (get_coeff_from_angle_countour(polyline.points.back(), this->expolygon, min(min_width, polyline.length() / 2)));\n+                const double coeff_angle_candi = (coeff_angle_cache.find(best_candidate->points.back()) != coeff_angle_cache.end())\n+                    ? coeff_angle_cache[best_candidate->points.back()]\n+                    : (get_coeff_from_angle_countour(best_candidate->points.back(), this->expolygon, min(min_width, best_candidate->length() / 2)));\n+\n+                //this will encourage to follow the curve, a little, because it's shorter near the center\n+                //without that, it tends to go to the outter rim.\n+                //std::cout << \" max(polyline.length(), best_candidate->length())=\" << max(polyline.length(), best_candidate->length())\n+                //    << \", polyline.length()=\" << polyline.length()\n+                //    << \", best_candidate->length()=\" << best_candidate->length()\n+                //    << \", polyline.length() / max=\" << (polyline.length() / max(polyline.length(), best_candidate->length()))\n+                //    << \", best_candidate->length() / max=\" << (best_candidate->length() / max(polyline.length(), best_candidate->length()))\n+                //    << \"\\n\";\n+                double weight_poly = 2 - (polyline.length() / max(polyline.length(), best_candidate->length()));\n+                double weight_candi = 2 - (best_candidate->length() / max(polyline.length(), best_candidate->length()));\n+                weight_poly *= coeff_angle_poly;\n+                weight_candi *= coeff_angle_candi;\n+                const double coeff_poly = (dot_poly_branch * weight_poly) / (dot_poly_branch * weight_poly + dot_candidate_branch * weight_candi);\n+                const double coeff_candi = 1.0 - coeff_poly;\n+                //std::cout << \"coeff_angle_poly=\" << coeff_angle_poly\n+                //    << \", coeff_angle_candi=\" << coeff_angle_candi\n+                //    << \", weight_poly=\" << (2 - (polyline.length() / max(polyline.length(), best_candidate->length())))\n+                //    << \", weight_candi=\" << (2 - (best_candidate->length() / max(polyline.length(), best_candidate->length())))\n+                //    << \", sumpoly=\" << weight_poly\n+                //    << \", sumcandi=\" << weight_candi\n+                //    << \", dot_poly_branch=\" << dot_poly_branch\n+                //    << \", dot_candidate_branch=\" << dot_candidate_branch\n+                //    << \", coeff_poly=\" << coeff_poly\n+                //    << \", coeff_candi=\" << coeff_candi\n+                //    << \"\\n\";\n+                //iterate the points\n+                // as voronoi should create symetric thing, we can iterate synchonously\n+                size_t idx_point = 1;\n+                while (idx_point < min(polyline.points.size(), best_candidate->points.size())) {\n+                    //fusion\n+                    polyline.points[idx_point].x = polyline.points[idx_point].x * coeff_poly + best_candidate->points[idx_point].x * coeff_candi;\n+                    polyline.points[idx_point].y = polyline.points[idx_point].y * coeff_poly + best_candidate->points[idx_point].y * coeff_candi;\n+\n+                    // The width decrease with distance from the centerline.\n+                    // 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.\n+                    //If someone find  an other formula based on the properties of the voronoi algorithm used here, and it works better, please use it.\n+                    //or maybe just use the distance to nearest edge in bounds...\n+                    double value_from_current_width = 0.5*polyline.width[idx_point] * dot_poly_branch / max(dot_poly_branch, dot_candidate_branch);\n+                    value_from_current_width += 0.5*best_candidate->width[idx_point] * dot_candidate_branch / max(dot_poly_branch, dot_candidate_branch);\n+                    double value_from_dist = 2 * polyline.points[idx_point].distance_to(best_candidate->points[idx_point]);\n+                    value_from_dist *= sqrt(min(dot_poly_branch, dot_candidate_branch) / max(dot_poly_branch, dot_candidate_branch));\n+                    polyline.width[idx_point] = value_from_current_width + value_from_dist;\n+                    //std::cout << \"width:\" << polyline.width[idx_point] << \" = \" << value_from_current_width << \" + \" << value_from_dist \n+                    //    << \" (<\" << max_width << \" && \" << (bounds.contour.closest_point(polyline.points[idx_point])->distance_to(polyline.points[idx_point]) * 2.1)<<\")\\n\";\n+                    //failsafes\n+                    if (polyline.width[idx_point] > max_width) \n+                        polyline.width[idx_point] = max_width;\n+                    const coord_t max_width_contour = bounds.contour.closest_point(polyline.points[idx_point])->distance_to(polyline.points[idx_point]) * 2.1;\n+                    if (polyline.width[idx_point] > max_width_contour)\n+                        polyline.width[idx_point] = max_width_contour;\n+\n+                    ++idx_point;\n+                }\n+                if (idx_point < best_candidate->points.size()) {\n+                    if (idx_point + 1 < best_candidate->points.size()) {\n+                        //create a new polyline\n+                        pp.emplace_back();\n+                        pp.back().endpoints.first = true;\n+                        pp.back().endpoints.second = best_candidate->endpoints.second;\n+                        for (size_t idx_point_new_line = idx_point; idx_point_new_line < best_candidate->points.size(); ++idx_point_new_line) {\n+                            pp.back().points.push_back(best_candidate->points[idx_point_new_line]);\n+                            pp.back().width.push_back(best_candidate->width[idx_point_new_line]);\n+                        }\n+                    } else {\n+                        //Add last point\n+                        polyline.points.push_back(best_candidate->points[idx_point]);\n+                        polyline.width.push_back(best_candidate->width[idx_point]);\n+                        //select if an end opccur\n+                        polyline.endpoints.second &= best_candidate->endpoints.second;\n+                    }\n+\n+                } else {\n+                    //select if an end opccur\n+                    polyline.endpoints.second &= best_candidate->endpoints.second;\n+                }\n+\n+                //remove points that are the same or too close each other, ie simplify\n+                for (size_t idx_point = 1; idx_point < polyline.points.size(); ++idx_point) {\n+                    if (polyline.points[idx_point - 1].distance_to(polyline.points[idx_point]) < SCALED_EPSILON) {\n+                        if (idx_point < polyline.points.size() - 1) {\n+                            polyline.points.erase(polyline.points.begin() + idx_point);\n+                            polyline.width.erase(polyline.width.begin() + idx_point);\n+                        } else {\n+                            polyline.points.erase(polyline.points.begin() + idx_point - 1);\n+                            polyline.width.erase(polyline.width.begin() + idx_point - 1);\n+                        }\n+                        --idx_point;\n+                    }\n+                }\n+                //remove points that are outside of the geometry\n+                for (size_t idx_point = 0; idx_point < polyline.points.size(); ++idx_point) {\n+                    if (!bounds.contains_b(polyline.points[idx_point])) {\n+                        polyline.points.erase(polyline.points.begin() + idx_point);\n+                        polyline.width.erase(polyline.width.begin() + idx_point);\n+                        --idx_point;\n+                    }\n+                }\n+\n+                //update cache\n+                coeff_angle_cache[polyline.points.back()] = coeff_angle_poly * coeff_poly + coeff_angle_candi * coeff_candi;\n+\n+\n+                if (polyline.points.size() < 2) {\n+                    //remove self\n+                    pp.erase(pp.begin() + i);\n+                    --i;\n+                    --best_idx;\n+                }\n+\n+                pp.erase(pp.begin() + best_idx);\n+                //{\n+                //    stringstream stri;\n+                //    stri << \"medial_axis_2.0_aft_fus_\" << id << \"_\" << idf << \".svg\";\n+                //    SVG svg(stri.str());\n+                //    svg.draw(bounds);\n+                //    svg.draw(this->expolygon);\n+                //    svg.draw(pp);\n+                //    svg.Close();\n+                //}\n+                changes = true;\n+                break;\n+            }\n+        }\n+        if (changes) {\n+            concatThickPolylines(pp);\n+            ///reorder, in case of change\n+            std::sort(pp.begin(), pp.end(), [](const ThickPolyline & a, const ThickPolyline & b) {\n+                bool ahas0 = a.width.front() == 0 || a.width.back() == 0;\n+                bool bhas0 = b.width.front() == 0 || b.width.back() == 0;\n+                if (ahas0 && !bhas0) return true;\n+                if (!ahas0 && bhas0) return false;\n+                return a.length() < b.length();\n+            });\n+        }\n+    }\n+}\n+\n+void\n+MedialAxis::remove_too_thin_extrusion(ThickPolylines& pp)\n+{\n+    // remove too thin extrusion at start & end of polylines\n+    bool changes = false;\n+    for (size_t i = 0; i < pp.size(); ++i) {\n+        ThickPolyline& polyline = pp[i];\n+        // remove bits with too small extrusion\n+        while (polyline.points.size() > 1 && polyline.width.front() < this->min_width && polyline.endpoints.first) {\n+            //try to split if possible\n+            if (polyline.width[1] > min_width) {\n+                double percent_can_keep = 1 - (min_width - polyline.width[0]) / (polyline.width[1] - polyline.width[0]);\n+                if (polyline.points.front().distance_to(polyline.points[1]) * percent_can_keep > SCALED_RESOLUTION) {\n+                    //Can split => move the first point and assign a new weight.\n+                    //the update of endpoints wil be performed in concatThickPolylines\n+                    polyline.points.front().x = polyline.points.front().x +\n+                        (coord_t)((polyline.points[1].x - polyline.points.front().x) * (1 - percent_can_keep));\n+                    polyline.points.front().y = polyline.points.front().y +\n+                        (coord_t)((polyline.points[1].y - polyline.points.front().y) * (1 - percent_can_keep));\n+                    polyline.width.front() = min_width;\n+                } else {\n+                    /// almost 0-length, Remove\n+                    polyline.points.erase(polyline.points.begin());\n+                    polyline.width.erase(polyline.width.begin());\n+                }\n+                changes = true;\n+                break;\n+            }\n+            polyline.points.erase(polyline.points.begin());\n+            polyline.width.erase(polyline.width.begin());\n+            changes = true;\n+        }\n+        while (polyline.points.size() > 1 && polyline.width.back() < this->min_width && polyline.endpoints.second) {\n+            //try to split if possible\n+            if (polyline.width[polyline.points.size() - 2] > min_width) {\n+                double percent_can_keep = 1 - (min_width - polyline.width.back()) / (polyline.width[polyline.points.size() - 2] - polyline.width.back());\n+                if (polyline.points.back().distance_to(polyline.points[polyline.points.size() - 2]) * percent_can_keep > SCALED_RESOLUTION) {\n+                    //Can split => move the first point and assign a new weight.\n+                    //the update of endpoints wil be performed in concatThickPolylines\n+                    polyline.points.back().x = polyline.points.back().x +\n+                        (coord_t)((polyline.points[polyline.points.size() - 2].x - polyline.points.back().x) * (1 - percent_can_keep));\n+                    polyline.points.back().y = polyline.points.back().y +\n+                        (coord_t)((polyline.points[polyline.points.size() - 2].y - polyline.points.back().y) * (1 - percent_can_keep));\n+                    polyline.width.back() = min_width;\n+                } else {\n+                    /// almost 0-length, Remove\n+                    polyline.points.erase(polyline.points.end() - 1);\n+                    polyline.width.erase(polyline.width.end() - 1);\n+                }\n+                changes = true;\n+                break;\n+            }\n+            polyline.points.erase(polyline.points.end() - 1);\n+            polyline.width.erase(polyline.width.end() - 1);\n+            changes = true;\n+        }\n+        //remove empty lines and bits that comes from a \"main line\"\n+        if (polyline.points.size() < 2 || (changes && polyline.length() < max_width && polyline.points.size() ==2)) {\n+            //remove self if too small\n+            pp.erase(pp.begin() + i);\n+            --i;\n+        }\n+    }\n+    if (changes) concatThickPolylines(pp);\n+}\n+\n+void\n+MedialAxis::concatenate_polylines_with_crossing(ThickPolylines& pp)\n+{\n+\n+    // concatenate, but even where multiple thickpolyline join, to create nice long strait polylines\n+    /*  If we removed any short polylines we now try to connect consecutive polylines\n+    in order to allow loop detection. Note that this algorithm is greedier than\n+    MedialAxis::process_edge_neighbors() as it will connect random pairs of\n+    polylines even when more than two start from the same point. This has no\n+    drawbacks since we optimize later using nearest-neighbor which would do the\n+    same, but should we use a more sophisticated optimization algorithm we should\n+    not connect polylines when more than two meet.\n+    Optimisation of the old algorithm : now we select the most \"strait line\" choice\n+    when we merge with an other line at a point with more than two meet.\n+    */\n+    for (size_t i = 0; i < pp.size(); ++i) {\n+        ThickPolyline& polyline = pp[i];\n+        if (polyline.endpoints.first && polyline.endpoints.second) continue; // optimization\n+\n+        ThickPolyline* best_candidate = nullptr;\n+        float best_dot = -1;\n+        size_t best_idx = 0;\n+\n+        // find another polyline starting here\n+        for (size_t j = 0; j < pp.size(); ++j) {\n+            if (j == i) continue;\n+            ThickPolyline& other = pp[j];\n+            if (other.endpoints.first && other.endpoints.second) continue;\n+\n+            if (polyline.last_point().coincides_with(other.last_point())) {\n+                other.reverse();\n+            } else if (polyline.first_point().coincides_with(other.last_point())) {\n+                polyline.reverse();\n+                other.reverse();\n+            } else if (polyline.first_point().coincides_with(other.first_point())) {\n+                polyline.reverse();\n+            } else if (!polyline.last_point().coincides_with(other.first_point())) {\n+                continue;\n+            }\n+\n+            Pointf v_poly(polyline.lines().back().vector().x, polyline.lines().back().vector().y);\n+            v_poly.scale(1 / std::sqrt(v_poly.x*v_poly.x + v_poly.y*v_poly.y));\n+            Pointf v_other(other.lines().front().vector().x, other.lines().front().vector().y);\n+            v_other.scale(1 / std::sqrt(v_other.x*v_other.x + v_other.y*v_other.y));\n+            float other_dot = v_poly.x*v_other.x + v_poly.y*v_other.y;\n+            if (other_dot > best_dot) {\n+                best_candidate = &other;\n+                best_idx = j;\n+                best_dot = other_dot;\n+            }\n+        }\n+        if (best_candidate != nullptr) {\n+            polyline.points.insert(polyline.points.end(), best_candidate->points.begin() + 1, best_candidate->points.end());\n+            polyline.width.insert(polyline.width.end(), best_candidate->width.begin() + 1, best_candidate->width.end());\n+            polyline.endpoints.second = best_candidate->endpoints.second;\n+            assert(polyline.width.size() == polyline.points.size());\n+            if (best_idx < i) i--;\n+            pp.erase(pp.begin() + best_idx);\n+        }\n+    }\n+}\n+\n+void\n+MedialAxis::remove_too_thin_points(ThickPolylines& pp)\n+{\n+    //remove too thin polylines points (inside a polyline : split it)\n+    for (size_t i = 0; i < pp.size(); ++i) {\n+        ThickPolyline& polyline = pp[i];\n+\n+        // remove bits with too small extrusion\n+        size_t idx_point = 0;\n+        while (idx_point<polyline.points.size()) {\n+            if (polyline.width[idx_point] < min_width) {\n+                if (idx_point == 0) {\n+                    //too thin at start\n+                    polyline.points.erase(polyline.points.begin());\n+                    polyline.width.erase(polyline.width.begin());\n+                    idx_point = 0;\n+                } else if (idx_point == 1) {\n+                    //too thin at start\n+                    polyline.points.erase(polyline.points.begin());\n+                    polyline.width.erase(polyline.width.begin());\n+                    polyline.points.erase(polyline.points.begin());\n+                    polyline.width.erase(polyline.width.begin());\n+                    idx_point = 0;\n+                } else if (idx_point == polyline.points.size() - 2) {\n+                    //too thin at (near) end\n+                    polyline.points.erase(polyline.points.end() - 1);\n+                    polyline.width.erase(polyline.width.end() - 1);\n+                    polyline.points.erase(polyline.points.end() - 1);\n+                    polyline.width.erase(polyline.width.end() - 1);\n+                } else if (idx_point == polyline.points.size() - 1) {\n+                    //too thin at end\n+                    polyline.points.erase(polyline.points.end() - 1);\n+                    polyline.width.erase(polyline.width.end() - 1);\n+                } else {\n+                    //too thin in middle : split\n+                    pp.emplace_back();\n+                    ThickPolyline &newone = pp.back();\n+                    newone.points.insert(newone.points.begin(), polyline.points.begin() + idx_point + 1, polyline.points.end());\n+                    newone.width.insert(newone.width.begin(), polyline.width.begin() + idx_point + 1, polyline.width.end());\n+                    polyline.points.erase(polyline.points.begin() + idx_point, polyline.points.end());\n+                    polyline.width.erase(polyline.width.begin() + idx_point, polyline.width.end());\n+                }\n+            } else idx_point++;\n+\n+            if (polyline.points.size() < 2) {\n+                //remove self if too small\n+                pp.erase(pp.begin() + i);\n+                --i;\n+                break;\n+            }\n+        }\n+    }\n+}\n+\n+void\n+MedialAxis::remove_too_short_polylines(ThickPolylines& pp, const coord_t min_size)\n+{\n+    //remove too short polyline\n+    bool changes = true;\n+    while (changes) {\n+        changes = false;\n+\n+        double shortest_size = min_size;\n+        size_t shortest_idx = -1;\n+        for (size_t i = 0; i < pp.size(); ++i) {\n+            ThickPolyline& polyline = pp[i];\n+            // Remove the shortest polylines : polyline that are shorter than wider\n+            // (we can't do this check before endpoints extension and clipping because we don't\n+            // know how long will the endpoints be extended since it depends on polygon thickness\n+            // which is variable - extension will be <= max_width/2 on each side) \n+            if ((polyline.endpoints.first || polyline.endpoints.second)\n+                && polyline.length() < max_width / 2) {\n+                if (shortest_size > polyline.length()) {\n+                    shortest_size = polyline.length();\n+                    shortest_idx = i;\n+                }\n+\n+            }\n+        }\n+        if (shortest_idx >= 0 && shortest_idx < pp.size()) {\n+            pp.erase(pp.begin() + shortest_idx);\n+            changes = true;\n+        }\n+        if (changes) concatThickPolylines(pp);\n+    }\n+}\n+\n+void\n+MedialAxis::ensure_not_overextrude(ThickPolylines& pp)\n+{\n+    //ensure the volume extruded is correct for what we have been asked\n+    // => don't over-extrude\n+    double surface = 0;\n+    double volume = 0;\n+    for (ThickPolyline& polyline : pp) {\n+        for (ThickLine l : polyline.thicklines()) {\n+            surface += l.length() * (l.a_width + l.b_width) / 2;\n+            double width_mean = (l.a_width + l.b_width) / 2;\n+            volume += height * (width_mean - height * (1. - 0.25 * PI)) * l.length();\n+        }\n+    }\n+\n+    // compute bounds volume\n+    double boundsVolume = 0;\n+    boundsVolume += height*bounds.area();\n+    // add external \"perimeter gap\"\n+    double perimeterRoundGap = bounds.contour.length() * height * (1 - 0.25*PI) * 0.5;\n+    // add holes \"perimeter gaps\"\n+    double holesGaps = 0;\n+    for (auto hole = bounds.holes.begin(); hole != bounds.holes.end(); ++hole) {\n+        holesGaps += hole->length() * height * (1 - 0.25*PI) * 0.5;\n+    }\n+    boundsVolume += perimeterRoundGap + holesGaps;\n+\n+    if (boundsVolume < volume) {\n+        //reduce width\n+        double reduce_by = boundsVolume / volume;\n+        for (ThickPolyline& polyline : pp) {\n+            for (ThickLine l : polyline.thicklines()) {\n+                l.a_width *= reduce_by;\n+                l.b_width *= reduce_by;\n+            }\n+        }\n+    }\n+}\n+\n+ExPolygon \n+MedialAxis::simplify_polygon_frontier()\n+{\n+\n+    //simplify the boundary between us and the bounds.\n+    //int firstIdx = 0;\n+    //while (firstIdx < contour.points.size() && bounds.contour.contains(contour.points[firstIdx])) firstIdx++;\n+    ExPolygon simplified_poly = this->surface;\n+    if (&this->surface != &this->bounds) {\n+        bool need_intersect = false;\n+        for (size_t i = 0; i < simplified_poly.contour.points.size(); i++) {\n+            Point &p_check = simplified_poly.contour.points[i];\n+            //if (!find) {\n+            if (!bounds.has_boundary_point(p_check)) {\n+                //check if we put it at a bound point instead of delete it\n+                size_t prev_i = i == 0 ? simplified_poly.contour.points.size() - 1 : (i - 1);\n+                size_t next_i = i == simplified_poly.contour.points.size() - 1 ? 0 : (i + 1);\n+                const Point* closest = bounds.contour.closest_point(p_check);\n+                if (closest != nullptr && closest->distance_to(p_check) + SCALED_EPSILON\n+                    < min(p_check.distance_to(simplified_poly.contour.points[prev_i]), p_check.distance_to(simplified_poly.contour.points[next_i])) / 2) {\n+                    p_check.x = closest->x;\n+                    p_check.y = closest->y;\n+                    need_intersect = true;\n+                } else {\n+                    simplified_poly.contour.points.erase(simplified_poly.contour.points.begin() + i);\n+                    i--;\n+                }\n+            }\n+        }\n+        if (need_intersect) {\n+            ExPolygons simplified_polys = intersection_ex(simplified_poly, bounds);\n+            if (simplified_polys.size() == 1) {\n+                simplified_poly = simplified_polys[0];\n+            } else {\n+                simplified_poly = this->surface;\n+            }\n+        }\n+    }\n+\n+    simplified_poly.remove_point_too_near(SCALED_RESOLUTION);\n+    return simplified_poly;\n+}\n+\n+void\n+MedialAxis::build(ThickPolylines* polylines_out)\n+","path":"xs/src/libslic3r/MedialAxis.cpp","position":1420,"original_position":1420,"commit_id":"137c99cbe173d28b3d6e0c83f68ace512ea612c8","original_commit_id":"137c99cbe173d28b3d6e0c83f68ace512ea612c8","user":{"login":"ledvinap","id":2318015,"node_id":"MDQ6VXNlcjIzMTgwMTU=","avatar_url":"https://avatars2.githubusercontent.com/u/2318015?v=4","gravatar_id":"","url":"https://api.github.com/users/ledvinap","html_url":"https://github.com/ledvinap","followers_url":"https://api.github.com/users/ledvinap/followers","following_url":"https://api.github.com/users/ledvinap/following{/other_user}","gists_url":"https://api.github.com/users/ledvinap/gists{/gist_id}","starred_url":"https://api.github.com/users/ledvinap/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ledvinap/subscriptions","organizations_url":"https://api.github.com/users/ledvinap/orgs","repos_url":"https://api.github.com/users/ledvinap/repos","events_url":"https://api.github.com/users/ledvinap/events{/privacy}","received_events_url":"https://api.github.com/users/ledvinap/received_events","type":"User","site_admin":false},"body":"(duplicate code)","created_at":"2018-12-11T12:08:55Z","updated_at":"2018-12-11T12:59:35Z","html_url":"https://github.com/slic3r/Slic3r/pull/4534#discussion_r240583915","pull_request_url":"https://api.github.com/repos/slic3r/Slic3r/pulls/4534","author_association":"COLLABORATOR","_links":{"self":{"href":"https://api.github.com/repos/slic3r/Slic3r/pulls/comments/240583915"},"html":{"href":"https://github.com/slic3r/Slic3r/pull/4534#discussion_r240583915"},"pull_request":{"href":"https://api.github.com/repos/slic3r/Slic3r/pulls/4534"}}},"pull_request":{"url":"https://api.github.com/repos/slic3r/Slic3r/pulls/4534","id":213659659,"node_id":"MDExOlB1bGxSZXF1ZXN0MjEzNjU5NjU5","html_url":"https://github.com/slic3r/Slic3r/pull/4534","diff_url":"https://github.com/slic3r/Slic3r/pull/4534.diff","patch_url":"https://github.com/slic3r/Slic3r/pull/4534.patch","issue_url":"https://api.github.com/repos/slic3r/Slic3r/issues/4534","number":4534,"state":"open","locked":false,"title":"Thin wall improvements (v2)","user":{"login":"supermerill","id":6536403,"node_id":"MDQ6VXNlcjY1MzY0MDM=","avatar_url":"https://avatars1.githubusercontent.com/u/6536403?v=4","gravatar_id":"","url":"https://api.github.com/users/supermerill","html_url":"https://github.com/supermerill","followers_url":"https://api.github.com/users/supermerill/followers","following_url":"https://api.github.com/users/supermerill/following{/other_user}","gists_url":"https://api.github.com/users/supermerill/gists{/gist_id}","starred_url":"https://api.github.com/users/supermerill/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/supermerill/subscriptions","organizations_url":"https://api.github.com/users/supermerill/orgs","repos_url":"https://api.github.com/users/supermerill/repos","events_url":"https://api.github.com/users/supermerill/events{/privacy}","received_events_url":"https://api.github.com/users/supermerill/received_events","type":"User","site_admin":false},"body":"![Thin walls improvements image](https://i.imgur.com/4S1QfEE.jpg) \r\n\r\n- now keep the bit with 0 width to merge them instead of just delete them\r\n - merging algorithm re-coded (extensively)\r\n - change behavior of thick polyline to only 1 width value per point instead of 2 per pair of adjacent points.\r\n - do not post-process the voronoi diagram in polylines, now it's don in expolygon.medial_axis.\r\n - failsafes in perimeter_generator (too many splits, too small)\r\n - some new tests.\r\n\r\nalso, a test will fail (the one that check if the half-circle has only angle of the same direction:\r\n![test error image](https://i.imgur.com/PgEtNKJ.jpg)\r\nFrom my test, changing my port-processing algorithm to push it more to the inner side has many unwanted side-effects. It's intended that it's going a bit more to the outer rim because there are more space to fill, and i can't know for sure the circle continue and it's not a strait after that.\r\n\r\nTell me if i forgot some formatting.","created_at":"2018-09-06T15:27:49Z","updated_at":"2018-12-11T12:59:34Z","closed_at":null,"merged_at":null,"merge_commit_sha":"d41c862d9b9ba1da99c4ba552c1b4722e2129c02","assignee":null,"assignees":[],"requested_reviewers":[{"login":"alranel","id":594957,"node_id":"MDQ6VXNlcjU5NDk1Nw==","avatar_url":"https://avatars2.githubusercontent.com/u/594957?v=4","gravatar_id":"","url":"https://api.github.com/users/alranel","html_url":"https://github.com/alranel","followers_url":"https://api.github.com/users/alranel/followers","following_url":"https://api.github.com/users/alranel/following{/other_user}","gists_url":"https://api.github.com/users/alranel/gists{/gist_id}","starred_url":"https://api.github.com/users/alranel/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/alranel/subscriptions","organizations_url":"https://api.github.com/users/alranel/orgs","repos_url":"https://api.github.com/users/alranel/repos","events_url":"https://api.github.com/users/alranel/events{/privacy}","received_events_url":"https://api.github.com/users/alranel/received_events","type":"User","site_admin":false}],"requested_teams":[],"labels":[],"milestone":null,"commits_url":"https://api.github.com/repos/slic3r/Slic3r/pulls/4534/commits","review_comments_url":"https://api.github.com/repos/slic3r/Slic3r/pulls/4534/comments","review_comment_url":"https://api.github.com/repos/slic3r/Slic3r/pulls/comments{/number}","comments_url":"https://api.github.com/repos/slic3r/Slic3r/issues/4534/comments","statuses_url":"https://api.github.com/repos/slic3r/Slic3r/statuses/137c99cbe173d28b3d6e0c83f68ace512ea612c8","head":{"label":"supermerill:alexrj_thinwall","ref":"alexrj_thinwall","sha":"137c99cbe173d28b3d6e0c83f68ace512ea612c8","user":{"login":"supermerill","id":6536403,"node_id":"MDQ6VXNlcjY1MzY0MDM=","avatar_url":"https://avatars1.githubusercontent.com/u/6536403?v=4","gravatar_id":"","url":"https://api.github.com/users/supermerill","html_url":"https://github.com/supermerill","followers_url":"https://api.github.com/users/supermerill/followers","following_url":"https://api.github.com/users/supermerill/following{/other_user}","gists_url":"https://api.github.com/users/supermerill/gists{/gist_id}","starred_url":"https://api.github.com/users/supermerill/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/supermerill/subscriptions","organizations_url":"https://api.github.com/users/supermerill/orgs","repos_url":"https://api.github.com/users/supermerill/repos","events_url":"https://api.github.com/users/supermerill/events{/privacy}","received_events_url":"https://api.github.com/users/supermerill/received_events","type":"User","site_admin":false},"repo":{"id":116708603,"node_id":"MDEwOlJlcG9zaXRvcnkxMTY3MDg2MDM=","name":"Slic3r","full_name":"supermerill/Slic3r","private":false,"owner":{"login":"supermerill","id":6536403,"node_id":"MDQ6VXNlcjY1MzY0MDM=","avatar_url":"https://avatars1.githubusercontent.com/u/6536403?v=4","gravatar_id":"","url":"https://api.github.com/users/supermerill","html_url":"https://github.com/supermerill","followers_url":"https://api.github.com/users/supermerill/followers","following_url":"https://api.github.com/users/supermerill/following{/other_user}","gists_url":"https://api.github.com/users/supermerill/gists{/gist_id}","starred_url":"https://api.github.com/users/supermerill/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/supermerill/subscriptions","organizations_url":"https://api.github.com/users/supermerill/orgs","repos_url":"https://api.github.com/users/supermerill/repos","events_url":"https://api.github.com/users/supermerill/events{/privacy}","received_events_url":"https://api.github.com/users/supermerill/received_events","type":"User","site_admin":false},"html_url":"https://github.com/supermerill/Slic3r","description":"G-code generator for 3D printers (RepRap, Makerbot, Ultimaker etc.)","fork":true,"url":"https://api.github.com/repos/supermerill/Slic3r","forks_url":"https://api.github.com/repos/supermerill/Slic3r/forks","keys_url":"https://api.github.com/repos/supermerill/Slic3r/keys{/key_id}","collaborators_url":"https://api.github.com/repos/supermerill/Slic3r/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/supermerill/Slic3r/teams","hooks_url":"https://api.github.com/repos/supermerill/Slic3r/hooks","issue_events_url":"https://api.github.com/repos/supermerill/Slic3r/issues/events{/number}","events_url":"https://api.github.com/repos/supermerill/Slic3r/events","assignees_url":"https://api.github.com/repos/supermerill/Slic3r/assignees{/user}","branches_url":"https://api.github.com/repos/supermerill/Slic3r/branches{/branch}","tags_url":"https://api.github.com/repos/supermerill/Slic3r/tags","blobs_url":"https://api.github.com/repos/supermerill/Slic3r/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/supermerill/Slic3r/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/supermerill/Slic3r/git/refs{/sha}","trees_url":"https://api.github.com/repos/supermerill/Slic3r/git/trees{/sha}","statuses_url":"https://api.github.com/repos/supermerill/Slic3r/statuses/{sha}","languages_url":"https://api.github.com/repos/supermerill/Slic3r/languages","stargazers_url":"https://api.github.com/repos/supermerill/Slic3r/stargazers","contributors_url":"https://api.github.com/repos/supermerill/Slic3r/contributors","subscribers_url":"https://api.github.com/repos/supermerill/Slic3r/subscribers","subscription_url":"https://api.github.com/repos/supermerill/Slic3r/subscription","commits_url":"https://api.github.com/repos/supermerill/Slic3r/commits{/sha}","git_commits_url":"https://api.github.com/repos/supermerill/Slic3r/git/commits{/sha}","comments_url":"https://api.github.com/repos/supermerill/Slic3r/comments{/number}","issue_comment_url":"https://api.github.com/repos/supermerill/Slic3r/issues/comments{/number}","contents_url":"https://api.github.com/repos/supermerill/Slic3r/contents/{+path}","compare_url":"https://api.github.com/repos/supermerill/Slic3r/compare/{base}...{head}","merges_url":"https://api.github.com/repos/supermerill/Slic3r/merges","archive_url":"https://api.github.com/repos/supermerill/Slic3r/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/supermerill/Slic3r/downloads","issues_url":"https://api.github.com/repos/supermerill/Slic3r/issues{/number}","pulls_url":"https://api.github.com/repos/supermerill/Slic3r/pulls{/number}","milestones_url":"https://api.github.com/repos/supermerill/Slic3r/milestones{/number}","notifications_url":"https://api.github.com/repos/supermerill/Slic3r/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/supermerill/Slic3r/labels{/name}","releases_url":"https://api.github.com/repos/supermerill/Slic3r/releases{/id}","deployments_url":"https://api.github.com/repos/supermerill/Slic3r/deployments","created_at":"2018-01-08T17:45:19Z","updated_at":"2018-12-07T14:16:23Z","pushed_at":"2018-12-10T13:57:55Z","git_url":"git://github.com/supermerill/Slic3r.git","ssh_url":"git@github.com:supermerill/Slic3r.git","clone_url":"https://github.com/supermerill/Slic3r.git","svn_url":"https://github.com/supermerill/Slic3r","homepage":"http://slic3r.org/","size":34026,"stargazers_count":10,"watchers_count":10,"language":"C++","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"open_issues_count":4,"license":{"key":"agpl-3.0","name":"GNU Affero General Public License v3.0","spdx_id":"AGPL-3.0","url":"https://api.github.com/licenses/agpl-3.0","node_id":"MDc6TGljZW5zZTE="},"forks":0,"open_issues":4,"watchers":10,"default_branch":"master"}},"base":{"label":"slic3r:master","ref":"master","sha":"21eb603cc16946b14e77d3c10cbee2f1163503c6","user":{"login":"slic3r","id":38015467,"node_id":"MDEyOk9yZ2FuaXphdGlvbjM4MDE1NDY3","avatar_url":"https://avatars0.githubusercontent.com/u/38015467?v=4","gravatar_id":"","url":"https://api.github.com/users/slic3r","html_url":"https://github.com/slic3r","followers_url":"https://api.github.com/users/slic3r/followers","following_url":"https://api.github.com/users/slic3r/following{/other_user}","gists_url":"https://api.github.com/users/slic3r/gists{/gist_id}","starred_url":"https://api.github.com/users/slic3r/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/slic3r/subscriptions","organizations_url":"https://api.github.com/users/slic3r/orgs","repos_url":"https://api.github.com/users/slic3r/repos","events_url":"https://api.github.com/users/slic3r/events{/privacy}","received_events_url":"https://api.github.com/users/slic3r/received_events","type":"Organization","site_admin":false},"repo":{"id":2309696,"node_id":"MDEwOlJlcG9zaXRvcnkyMzA5Njk2","name":"Slic3r","full_name":"slic3r/Slic3r","private":false,"owner":{"login":"slic3r","id":38015467,"node_id":"MDEyOk9yZ2FuaXphdGlvbjM4MDE1NDY3","avatar_url":"https://avatars0.githubusercontent.com/u/38015467?v=4","gravatar_id":"","url":"https://api.github.com/users/slic3r","html_url":"https://github.com/slic3r","followers_url":"https://api.github.com/users/slic3r/followers","following_url":"https://api.github.com/users/slic3r/following{/other_user}","gists_url":"https://api.github.com/users/slic3r/gists{/gist_id}","starred_url":"https://api.github.com/users/slic3r/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/slic3r/subscriptions","organizations_url":"https://api.github.com/users/slic3r/orgs","repos_url":"https://api.github.com/users/slic3r/repos","events_url":"https://api.github.com/users/slic3r/events{/privacy}","received_events_url":"https://api.github.com/users/slic3r/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/slic3r/Slic3r","description":"Open Source toolpath generator for 3D printers","fork":false,"url":"https://api.github.com/repos/slic3r/Slic3r","forks_url":"https://api.github.com/repos/slic3r/Slic3r/forks","keys_url":"https://api.github.com/repos/slic3r/Slic3r/keys{/key_id}","collaborators_url":"https://api.github.com/repos/slic3r/Slic3r/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/slic3r/Slic3r/teams","hooks_url":"https://api.github.com/repos/slic3r/Slic3r/hooks","issue_events_url":"https://api.github.com/repos/slic3r/Slic3r/issues/events{/number}","events_url":"https://api.github.com/repos/slic3r/Slic3r/events","assignees_url":"https://api.github.com/repos/slic3r/Slic3r/assignees{/user}","branches_url":"https://api.github.com/repos/slic3r/Slic3r/branches{/branch}","tags_url":"https://api.github.com/repos/slic3r/Slic3r/tags","blobs_url":"https://api.github.com/repos/slic3r/Slic3r/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/slic3r/Slic3r/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/slic3r/Slic3r/git/refs{/sha}","trees_url":"https://api.github.com/repos/slic3r/Slic3r/git/trees{/sha}","statuses_url":"https://api.github.com/repos/slic3r/Slic3r/statuses/{sha}","languages_url":"https://api.github.com/repos/slic3r/Slic3r/languages","stargazers_url":"https://api.github.com/repos/slic3r/Slic3r/stargazers","contributors_url":"https://api.github.com/repos/slic3r/Slic3r/contributors","subscribers_url":"https://api.github.com/repos/slic3r/Slic3r/subscribers","subscription_url":"https://api.github.com/repos/slic3r/Slic3r/subscription","commits_url":"https://api.github.com/repos/slic3r/Slic3r/commits{/sha}","git_commits_url":"https://api.github.com/repos/slic3r/Slic3r/git/commits{/sha}","comments_url":"https://api.github.com/repos/slic3r/Slic3r/comments{/number}","issue_comment_url":"https://api.github.com/repos/slic3r/Slic3r/issues/comments{/number}","contents_url":"https://api.github.com/repos/slic3r/Slic3r/contents/{+path}","compare_url":"https://api.github.com/repos/slic3r/Slic3r/compare/{base}...{head}","merges_url":"https://api.github.com/repos/slic3r/Slic3r/merges","archive_url":"https://api.github.com/repos/slic3r/Slic3r/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/slic3r/Slic3r/downloads","issues_url":"https://api.github.com/repos/slic3r/Slic3r/issues{/number}","pulls_url":"https://api.github.com/repos/slic3r/Slic3r/pulls{/number}","milestones_url":"https://api.github.com/repos/slic3r/Slic3r/milestones{/number}","notifications_url":"https://api.github.com/repos/slic3r/Slic3r/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/slic3r/Slic3r/labels{/name}","releases_url":"https://api.github.com/repos/slic3r/Slic3r/releases{/id}","deployments_url":"https://api.github.com/repos/slic3r/Slic3r/deployments","created_at":"2011-09-01T19:05:05Z","updated_at":"2018-12-11T08:16:36Z","pushed_at":"2018-12-10T18:06:05Z","git_url":"git://github.com/slic3r/Slic3r.git","ssh_url":"git@github.com:slic3r/Slic3r.git","clone_url":"https://github.com/slic3r/Slic3r.git","svn_url":"https://github.com/slic3r/Slic3r","homepage":"https://slic3r.org/","size":16963,"stargazers_count":1957,"watchers_count":1957,"language":"C++","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":1093,"mirror_url":null,"archived":false,"open_issues_count":869,"license":{"key":"agpl-3.0","name":"GNU Affero General Public License v3.0","spdx_id":"AGPL-3.0","url":"https://api.github.com/licenses/agpl-3.0","node_id":"MDc6TGljZW5zZTE="},"forks":1093,"open_issues":869,"watchers":1957,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/slic3r/Slic3r/pulls/4534"},"html":{"href":"https://github.com/slic3r/Slic3r/pull/4534"},"issue":{"href":"https://api.github.com/repos/slic3r/Slic3r/issues/4534"},"comments":{"href":"https://api.github.com/repos/slic3r/Slic3r/issues/4534/comments"},"review_comments":{"href":"https://api.github.com/repos/slic3r/Slic3r/pulls/4534/comments"},"review_comment":{"href":"https://api.github.com/repos/slic3r/Slic3r/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/slic3r/Slic3r/pulls/4534/commits"},"statuses":{"href":"https://api.github.com/repos/slic3r/Slic3r/statuses/137c99cbe173d28b3d6e0c83f68ace512ea612c8"}},"author_association":"COLLABORATOR"}}
{"actor":{"display_login":"ledvinap"}}