From 6a46b71dc1b72834f36e16f1bbf3e13f40e64c1d Mon Sep 17 00:00:00 2001
From: enricoturri1966 <enricoturri@seznam.cz>
Date: Wed, 3 Mar 2021 13:53:37 +0100
Subject: [PATCH 1/6] #5843 - GCodeProcessor: added processing of lines G28

---
 src/libslic3r/GCode/GCodeProcessor.cpp | 27 ++++++++++++++++++++++++++
 src/libslic3r/GCode/GCodeProcessor.hpp |  3 +++
 2 files changed, 30 insertions(+)

diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp
index 46fd3c5b3..982a5067c 100644
--- a/src/libslic3r/GCode/GCodeProcessor.cpp
+++ b/src/libslic3r/GCode/GCodeProcessor.cpp
@@ -1114,6 +1114,7 @@ void GCodeProcessor::process_gcode_line(const GCodeReader::GCodeLine& line)
                 case 21: { process_G21(line); break; } // Set Units to Millimeters
                 case 22: { process_G22(line); break; } // Firmware controlled retract
                 case 23: { process_G23(line); break; } // Firmware controlled unretract
+                case 28: { process_G28(line); break; } // Move to origin
                 case 90: { process_G90(line); break; } // Set to Absolute Positioning
                 case 91: { process_G91(line); break; } // Set to Relative Positioning
                 case 92: { process_G92(line); break; } // Set Position
@@ -2147,6 +2148,32 @@ void GCodeProcessor::process_G23(const GCodeReader::GCodeLine& line)
     store_move_vertex(EMoveType::Unretract);
 }
 
+void GCodeProcessor::process_G28(const GCodeReader::GCodeLine& line)
+{
+    std::string_view cmd = line.cmd();
+    std::string new_line_raw = { cmd.data(), cmd.size() };
+    bool found = false;
+    if (line.has_x()) {
+        new_line_raw += " X0";
+        found = true;
+    }
+    if (line.has_y()) {
+        new_line_raw += " Y0";
+        found = true;
+    }
+    if (line.has_z()) {
+        new_line_raw += " Z0";
+        found = true;
+    }
+    if (!found)
+        new_line_raw += " X0  Y0  Z0";
+
+    GCodeReader::GCodeLine new_line;
+    GCodeReader reader;
+    reader.parse_line(new_line_raw.c_str(), new_line, [](GCodeReader&, const GCodeReader::GCodeLine&) {});
+    process_G1(new_line);
+}
+
 void GCodeProcessor::process_G90(const GCodeReader::GCodeLine& line)
 {
     m_global_positioning_type = EPositioningType::Absolute;
diff --git a/src/libslic3r/GCode/GCodeProcessor.hpp b/src/libslic3r/GCode/GCodeProcessor.hpp
index f619864c4..c1497edda 100644
--- a/src/libslic3r/GCode/GCodeProcessor.hpp
+++ b/src/libslic3r/GCode/GCodeProcessor.hpp
@@ -572,6 +572,9 @@ namespace Slic3r {
         // Firmware controlled Unretract
         void process_G23(const GCodeReader::GCodeLine& line);
 
+        // Move to origin
+        void process_G28(const GCodeReader::GCodeLine& line);
+
         // Set to Absolute Positioning
         void process_G90(const GCodeReader::GCodeLine& line);
 

From 5f5de1c81279895be5c62bce524eb0b87998c18c Mon Sep 17 00:00:00 2001
From: Vojtech Bubnik <bubnikv@gmail.com>
Date: Wed, 3 Mar 2021 15:04:15 +0100
Subject: [PATCH 2/6] Follow-up to 5276bd98d7afaf953138d5e46963d9b4fab044c6:
 WIP: MutablePolygon - linked list based polygon implementation allowing rapid
 insertion and removal of points. WIP: porting smooth_outward() from Cura.

---
 src/libslic3r/MutablePolygon.cpp         | 445 ++++++++++++++---------
 src/libslic3r/MutablePolygon.hpp         | 146 +++++++-
 tests/libslic3r/test_mutable_polygon.cpp |  34 ++
 3 files changed, 428 insertions(+), 197 deletions(-)

diff --git a/src/libslic3r/MutablePolygon.cpp b/src/libslic3r/MutablePolygon.cpp
index 951485259..f166ce701 100644
--- a/src/libslic3r/MutablePolygon.cpp
+++ b/src/libslic3r/MutablePolygon.cpp
@@ -1,5 +1,6 @@
 #include "MutablePolygon.hpp"
 #include "Line.hpp"
+#include "libslic3r.h"
 
 namespace Slic3r {
 
@@ -36,207 +37,295 @@ void remove_duplicates(MutablePolygon &polygon, double eps)
     }
 }
 
-// Sample a point on line (a, b) at distance "dist" from ref_pt.
-// If two points fulfill the condition, then the first one (closer to point a) is taken.
-// If none of the two points falls on line (a, b), return false.
-template<typename VectorType>
-static inline VectorType point_on_line_at_dist(const VectorType &a, const VectorType &b, const VectorType &ref_pt, const double dist)
+// Adapted from Cura ConstPolygonRef::smooth_corner_complex() by Tim Kuipers.
+// A concave corner at it1 with position p1 has been removed by the caller between it0 and it2, where |p2 - p0| < shortcut_length.
+// Now try to close a concave crack by walking left from it0 and right from it2 as long as the new clipping edge is smaller than shortcut_length
+// and the new clipping edge is still inside the polygon (it is a diagonal, it does not intersect polygon boundary).
+// Once the traversal stops (always at a clipping edge shorter than shortcut_length), the final trapezoid is clipped with a new clipping edge of shortcut_length.
+// Return true if a hole was completely closed (degenerated to an empty polygon) or a single CCW triangle was left, which is not to be simplified any further.
+// it0, it2 are updated to the final clipping edge.
+static bool clip_narrow_corner(
+    const Vec2i64               p1, 
+    MutablePolygon::iterator   &it0, 
+    MutablePolygon::iterator   &it2,
+    MutablePolygon::range      &unprocessed_range,
+    int64_t                     dist2_current, 
+    const int64_t               shortcut_length)
 {
-    using T = typename VectorType::Scalar;
-    auto   v   = b - a;
-    auto   l2  = v.squaredNorm();
-    assert(l2 > T(0));
-    auto   vpt = ref_pt - a;
-    // Parameter of the foot point of ref_pt on line (a, b).
-    auto   t   = v.dot(vpt) / l2;
-    // Foot point of ref_pt on line (a, b).
-    auto   foot_pt = a + t * v;
-    auto   dfoot2 = vpt.squaredNorm() - (foot_pt - ref_pt).squaredNorm();
-    // Distance of the result point from the foot point, normalized to length of (a, b).
-    auto   dfoot  = dfoot2 > T(0) ? sqrt(dfoot2) / sqrt(l2) : T(0);
-    auto   t_result = t - dfoot;
-    if (t_result < T(0))
-        t_result = t + dfoot;
-    t_result = Slic3r::clamp(0., 1., t_result);
-    return a + v * t;
-}
+    MutablePolygon &polygon = it0.polygon();
+    assert(polygon.size() >= 2);
 
-static bool smooth_corner_complex(const Vec2d p1, MutablePolygon::iterator &it0, MutablePolygon::iterator &it2, const double shortcut_length)
-{
-    // walk away from the corner until the shortcut > shortcut_length or it would smooth a piece inward
-    // - walk in both directions untill shortcut > shortcut_length
-    // - stop walking in one direction if it would otherwise cut off a corner in that direction
-    // - same in the other direction
-    // - stop if both are cut off
-    // walk by updating p0_it and p2_it
-    double shortcut_length2    = shortcut_length * shortcut_length;
-    bool   forward_is_blocked  = false;
-    bool   forward_is_too_far  = false;
-    bool   backward_is_blocked = false;
-    bool   backward_is_too_far = false;
-    for (;;) {
-        const bool forward_has_converged  = forward_is_blocked  || forward_is_too_far;
-        const bool backward_has_converged = backward_is_blocked || backward_is_too_far;
-        if (forward_has_converged && backward_has_converged) {
-            if (forward_is_too_far && backward_is_too_far && (*it0.prev() - *it2.next()).cast<double>().squaredNorm() < shortcut_length2) {
-                // Trim the narrowing region.
-                -- it0;
-                ++ it2;
-                forward_is_too_far  = false;
-                backward_is_too_far = false;
-                continue;
-            } else
-                break;
-        }
+    const int64_t shortcut_length2 = sqr(shortcut_length);
 
-        const Vec2d p0 = it0->cast<double>();
-        const Vec2d p2 = it2->cast<double>();
-        if (! forward_has_converged && (backward_has_converged || (p2 - p1).squaredNorm() < (p0 - p1).squaredNorm())) {
-            // walk forward
-            const auto  it2_2 = it2.next();
-            const Vec2d p2_2  = it2_2->cast<double>();
-            if (cross2(p2 - p0, p2_2 - p0) > 0) {
-                forward_is_blocked  = true;
-            } else if ((p2_2 - p0).squaredNorm() > shortcut_length2) {
-                forward_is_too_far  = true;
+    enum Status {
+        Free,
+        Blocked,
+        Far,
+    };
+    Status  forward  = Free;
+    Status  backward = Free;
+
+    Vec2i64 p0 = it0->cast<int64_t>();
+    Vec2i64 p2 = it2->cast<int64_t>();
+    Vec2i64 p02;
+    Vec2i64 p22;
+    int64_t dist2_next;
+
+    // As long as there is at least a single triangle left in the polygon.
+    while (polygon.size() >= 3) {
+        assert(dist2_current <= shortcut_length2);
+        if (forward == Far && backward == Far) {
+            p02 = it0.prev()->cast<int64_t>();
+            p22 = it2.next()->cast<int64_t>();
+            auto d2 = (p22 - p02).squaredNorm();
+            if (d2 <= shortcut_length2) {
+                // The region was narrow until now and it is still narrow. Trim at both sides.
+                it0 = unprocessed_range.remove_back(it0).prev();
+                it2 = unprocessed_range.remove_front(it2);
+                if (polygon.size() <= 2)
+                    // A hole degenerated to an empty polygon.
+                    return true;
+                forward       = Free;
+                backward      = Free;
+                dist2_current = d2;
+                p0            = p02;
+                p2            = p22;
             } else {
-                it2                 = it2_2; // make one step in the forward direction
-                backward_is_blocked = false; // invalidate data about backward walking
-                backward_is_too_far = false;
+                // The region is widening. Stop traversal and trim the final trapezoid.
+                dist2_next    = d2;
+                break;
+            }
+        } else if (forward != Free && backward != Free)
+            // One of the corners is blocked, the other is blocked or too far. Stop traversal.
+            break;
+        // Try to proceed by flipping a diagonal.
+        // Progress by keeping the distance of the clipping edge end points equal to initial p1.
+        //FIXME This is an arbitrary condition, maybe a more local condition will be better (take a shorter diagonal?).
+        if (forward == Free && (backward != Free || (p2 - p1).squaredNorm() < (p0 - p1).cast<int64_t>().squaredNorm())) {
+            p22 = it2.next()->cast<int64_t>();
+            if (cross2(p2 - p0, p22 - p0) > 0)
+                forward = Blocked;
+            else {
+                // New clipping edge lenght.
+                auto d2 = (p22 - p0).squaredNorm();
+                if (d2 > shortcut_length2) {
+                    forward    = Far;
+                    dist2_next = d2;
+                } else {
+                    forward    = Free;
+                    // Make one step in the forward direction.
+                    it2        = unprocessed_range.remove_front(it2);
+                    p2         = p22;
+                    dist2_current = d2;
+                }
             }
         } else {
-            // walk backward
-            const auto  it0_2 = it0.prev();
-            const Vec2d p0_2  = it0_2->cast<double>();
-            if (cross2(p0_2 - p0, p2 - p0_2) > 0) {
-                backward_is_blocked = true;
-            } else if ((p2 - p0_2).squaredNorm() > shortcut_length2) {
-                backward_is_too_far = true;
-            } else {
-                it0                = it0_2; // make one step in the backward direction
-                forward_is_blocked = false; // invalidate data about forward walking
-                forward_is_too_far = false;
-            }
-        }
-
-        if (it0.prev() == it2 || it0 == it2) {
-            // stop if we went all the way around the polygon
-            // this should only be the case for hole polygons (?)
-            if (forward_is_too_far && backward_is_too_far) {
-                // in case p0_it.prev() == p2_it :
-                //     /                                                .
-                //    /                /|
-                //   |       becomes  | |
-                //    \                \|
-                //     \                                                .
-                // in case p0_it == p2_it :
-                //     /                                                .
-                //    /    becomes     /|
-                //    \                \|
-                //     \                                                .
-                break;
-            } else {
-                // this whole polygon can be removed
-                return true;
+            assert(backward == Free);
+            p02 = it0.prev()->cast<int64_t>();
+            if (cross2(p0 - p2, p02 - p2) > 0)
+                backward = Blocked;
+            else {
+                // New clipping edge lenght.
+                auto d2 = (p2 - p02).squaredNorm();
+                if (d2 > shortcut_length2) {
+                    backward   = Far;
+                    dist2_next = d2;
+                } else {
+                    backward   = Free;
+                    // Make one step in the backward direction.
+                    it0        = unprocessed_range.remove_back(it0).prev();
+                    p0         = p02;
+                    dist2_current = d2;
+                }
             }
         }
     }
 
-    const Vec2d   p0     = it0->cast<double>();
-    const Vec2d   p2     = it2->cast<double>();
-    const Vec2d   v02    = p2 - p0;
-    const int64_t l2_v02 = v02.squaredNorm();
-    if (std::abs(l2_v02 - shortcut_length2) < shortcut_length * 10) // i.e. if (size2 < l * (l+10) && size2 > l * (l-10))
-    { // v02 is approximately shortcut length
-        // handle this separately to avoid rounding problems below in the getPointOnLineWithDist function
-        // p0_it and p2_it are already correct
-    } else if (! backward_is_blocked && ! forward_is_blocked) {
-        const auto  l_v02 = sqrt(l2_v02);
-        const Vec2d p0_2  = it0.prev()->cast<double>();
-        const Vec2d p2_2  = it2.next()->cast<double>();
-        double t = Slic3r::clamp(0., 1., (shortcut_length - l_v02) / ((p2_2 - p0_2).norm() - l_v02));
-        it0 = it0.prev().insert((p0 + (p0_2 - p0) * t).cast<coord_t>());
-        it2 = it2.insert((p2 + (p2_2 - p2) * t).cast<coord_t>());
-    } else if (! backward_is_blocked) {
-        it0 = it0.prev().insert(point_on_line_at_dist(p0, Vec2d(it0.prev()->cast<double>()), p2, shortcut_length).cast<coord_t>());
-    } else if (! forward_is_blocked) {
-        it2 = it2.insert(point_on_line_at_dist(p2, Vec2d(it2.next()->cast<double>()), p0, shortcut_length).cast<coord_t>());
+    if (polygon.size() <= 3) {
+        // A hole degenerated to an empty polygon, or a tiny triangle remained.
+        assert(polygon.size() < 3 || (forward == Blocked && backward == Blocked) || (forward == Far && backward == Far));
+        if (polygon.size() < 3 || forward == Far) {
+            assert(polygon.size() < 3 || dist2_current <= shortcut_length2);
+            polygon.clear();
+        } else {
+            // The remaining triangle is CCW oriented, keep it.
+        }
+        return true;
+    }
+
+    assert(dist2_current <= shortcut_length2);
+    if ((forward == Blocked && backward == Blocked) || dist2_current > sqr(shortcut_length - int64_t(SCALED_EPSILON))) {
+        // The crack is filled, keep the last clipping edge.
+    } else if (dist2_next < sqr(shortcut_length - int64_t(SCALED_EPSILON))) {
+        // To avoid creating tiny edges.
+        if (forward == Far)
+            it0 = unprocessed_range.remove_back(it0).prev();
+        if (backward == Far)
+            it2 = unprocessed_range.remove_front(it2);
+        if (polygon.size() <= 2)
+            // A hole degenerated to an empty polygon.
+            return true;
+    } else if (forward == Blocked || backward == Blocked) {
+        // One side is far, the other blocked.
+        assert(forward == Far || backward == Far);
+        if (backward == Far) {
+            // Sort, so we will clip the 1st edge.
+            std::swap(p0,  p2);
+            std::swap(p02, p22);
+        }
+        // Find point on (p0, p02) at distance shortcut_length from p2.
+        // Circle intersects a line at two points, however because |p2 - p0| < shortcut_length,
+        // only the second intersection is valid. Because |p2 - p02| > shortcut_length, such
+        // intersection should always be found on (p0, p02).
+        const Vec2d     v = (p02 - p0).cast<double>();
+        const Vec2d     d = (p0 - p2).cast<double>();
+        const double    a = v.squaredNorm();
+        const double    b = 2. * double(d.dot(v));
+        double          u = b * b - 4. * a * (d.squaredNorm() - shortcut_length2);
+        assert(u > 0.);
+        u = sqrt(u);
+        double t = (- b + u) / (2. * a);
+        assert(t > 0. && t < 1.);
+        (backward == Far ? *it2 : *it0) += (v.cast<double>() * t).cast<coord_t>();
     } else {
-        //        |
-        //      __|2
-        //     | /  > shortcut cannot be of the desired length
-        //  ___|/                                                       .
-        //     0
-        // both are blocked and p0_it and p2_it are already correct
+        // The trapezoid (it0.prev(), it0, it2, it2.next()) is widening. Trim it.
+        assert(forward == Far && backward == Far);
+        assert(dist2_next > shortcut_length2);
+        const double dcurrent = sqrt(double(dist2_current));
+        double t = (shortcut_length - dcurrent) / (sqrt(double(dist2_next)) - dcurrent);
+        assert(t > 0. && t < 1.);
+        *it0 += ((p02 - p0).cast<double>() * t).cast<coord_t>();
+        *it2 += ((p22 - p2).cast<double>() * t).cast<coord_t>();
     }
-    // Delete all the points between it0 and it2.
-    while (it0.next() != it2)
-        it0.next().remove();
     return false;
 }
 
-void smooth_outward(MutablePolygon &polygon, double shortcut_length)
+// adapted from Cura ConstPolygonRef::smooth_outward() by Tim Kuipers.
+void smooth_outward(MutablePolygon &polygon, coord_t clip_dist_scaled)
 {
     remove_duplicates(polygon, scaled<double>(0.01));
 
-    const int                     shortcut_length2 = shortcut_length * shortcut_length;
-    static constexpr const double cos_min_angle    = -0.70710678118654752440084436210485; // cos(135 degrees)
+    const auto clip_dist_scaled2    = sqr<int64_t>(clip_dist_scaled);
+    const auto clip_dist_scaled2eps = sqr(clip_dist_scaled + int64_t(SCALED_EPSILON));
+    const auto foot_dist_min2       = sqr(SCALED_EPSILON);
 
-    MutablePolygon::iterator it1 = polygon.begin();
-    do {
-        const Vec2d p1  = it1->cast<double>();
-        auto        it0 = it1.prev();
-        auto        it2 = it1.next();
-        const Vec2d p0  = it0->cast<double>();
-        const Vec2d p2  = it2->cast<double>();
-        const Vec2d v1  = p0 - p1;
-        const Vec2d v2  = p2 - p1;
-        const double cos_angle = v1.dot(v2);
-        if (cos_angle < cos_min_angle && cross2(v1, v2) < 0) {
-            // Simplify the sharp angle.
-            const Vec2d  v02   = p2 - p0;
-            const double l2_v02 = v02.squaredNorm();
-            if (l2_v02 >= shortcut_length2) {
-                // Trim an obtuse corner.
+    // Each source point will be visited exactly once.
+    MutablePolygon::range unprocessed_range(polygon);
+    while (! unprocessed_range.empty() && polygon.size() > 2) {
+        auto          it1  = unprocessed_range.process_next();
+        auto          it0  = it1.prev();
+        auto          it2  = it1.next();
+        const Point   p0   = *it0;
+        const Point   p1   = *it1;
+        const Point   p2   = *it2;
+        const Vec2i64 v1   = (p0 - p1).cast<int64_t>();
+        const Vec2i64 v2   = (p2 - p1).cast<int64_t>();
+        if (cross2(v1, v2) > 0) {
+            // Concave corner.
+            int64_t dot  = v1.dot(v2);
+            auto    l2v1 = double(v1.squaredNorm());
+            auto    l2v2 = double(v2.squaredNorm());
+            if (dot > 0 || Slic3r::sqr(double(dot)) * 2. < l2v1 * l2v2) {
+                // Angle between v1 and v2 bigger than 135 degrees.
+                // Simplify the sharp angle.
+                Vec2i64 v02   = (p2 - p0).cast<int64_t>();
+                int64_t l2v02 = v02.squaredNorm();
                 it1.remove();
-                if (l2_v02 > Slic3r::sqr(shortcut_length + SCALED_EPSILON)) {
-                    double l2_1 = v1.squaredNorm();
-                    double l2_2 = v2.squaredNorm();
-                    bool trim = true;
-                    if (cos_angle > 0.9999) {
-                        // The triangle p0, p1, p2 is likely degenerate.
-                        // Measure height of the triangle.
-                        double d2 = l2_1 > l2_2 ? line_alg::distance_to_squared(Linef{ p0, p1 }, p2) : line_alg::distance_to_squared(Linef{ p2, p1 }, p0);
-                        if (d2 < Slic3r::sqr(scaled<double>(0.02)))
-                            trim = false;
-                    }
-                    if (trim) {
-                        Vec2d  bisector  = v1 / l2_1 + v2 / l2_2;
-                        double d1        = v1.dot(bisector) / l2_1;
-                        double d2        = v2.dot(bisector) / l2_2;
-                        double lbisector = bisector.norm();
-                        if (d1 < shortcut_length && d2 < shortcut_length) {
-                            it0.insert((p1 + v1 * (shortcut_length / d1)).cast<coord_t>())
-                               .insert((p1 + v2 * (shortcut_length / d2)).cast<coord_t>());
-                        } else if (v1.squaredNorm() < v2.squaredNorm())
-                            it0.insert(point_on_line_at_dist(p1, p2, p0, shortcut_length).cast<coord_t>());
-                        else
-                            it0.insert(point_on_line_at_dist(p1, p0, p2, shortcut_length).cast<coord_t>());
+                if (l2v02 < clip_dist_scaled2) {
+                    // (p0, p2) is short.
+                    // Clip a sharp concave corner by possibly expanding the trimming region left of it0 and right of it2.
+                    // Updates it0, it2 and num_to_process.
+                    if (clip_narrow_corner(p1.cast<int64_t>(), it0, it2, unprocessed_range, l2v02, clip_dist_scaled))
+                        // Trimmed down to an empty polygon or to a single CCW triangle.
+                        return;
+                } else {
+                    // Clip an obtuse corner.
+                    if (l2v02 > clip_dist_scaled2eps) {
+                        Vec2d  v1d  = v1.cast<double>();
+                        Vec2d  v2d  = v2.cast<double>();
+                        // Sort v1d, v2d, shorter first.
+                        bool   swap = l2v1 > l2v2;
+                        if (swap) {
+                            std::swap(v1d, v2d);
+                            std::swap(l2v1, l2v2);
+                        }
+                        double lv1  = sqrt(l2v1);
+                        double lv2  = sqrt(l2v2);
+                        // Bisector between v1 and v2.
+                        Vec2d  bisector   = v1d / lv1 + v2d / lv2;
+                        double l2bisector = bisector.squaredNorm();
+                        // Squared distance of the end point of v1 to the bisector.
+                        double d2         = l2v1 - sqr(v1d.dot(bisector)) / l2bisector;
+                        if (d2 < foot_dist_min2) {
+                            // Height of the p1, p0, p2 triangle is tiny. Just remove p1.
+                        } else if (d2 < 0.25 * clip_dist_scaled2 + SCALED_EPSILON) {
+                            // The shorter vector is too close to the bisector. Trim the shorter vector fully,
+                            // trim the longer vector partially.
+                            // Intersection of a circle at p2 of radius = clip_dist_scaled
+                            // with a ray (p1, p0), take the intersection after the foot point.
+                            // The intersection shall always exist because |p2 - p1| > clip_dist_scaled.
+                            const double    b = - 2. * v1d.cast<double>().dot(v2d);
+                            double          u = b * b - 4. * l2v2 * (double(l2v1) - clip_dist_scaled2);
+                            assert(u > 0.);
+                            // Take the second intersection along v2.
+                            double          t = (- b + sqrt(u)) / (2. * l2v2);
+                            assert(t > 0. && t < 1.);
+                            Point           pt_new = p1 + (t * v2d).cast<coord_t>();
+#ifndef NDEBUG
+                            double d2new = (pt_new - (swap ? p2 : p0)).cast<double>().squaredNorm();
+                            assert(std::abs(d2new - clip_dist_scaled2) < sqr(10. * SCALED_EPSILON));
+#endif // NDEBUG
+                            it2.insert(pt_new);
+                        } else {
+                            // Cut the corner with a line perpendicular to the bisector.
+                            double t  = sqrt(0.25 * clip_dist_scaled2 / d2);
+                            assert(t > 0. && t < 1.);
+                            Point  p0 = p1 + (v1d * t).cast<coord_t>();
+                            Point  p2 = p1 + (v2d * (t * lv2 / lv1)).cast<coord_t>();
+                            if (swap)
+                                std::swap(p0, p2);
+                            it2.insert(p2).insert(p0);
+                        }
+                    } else {
+                        // Just remove p1.
+                        assert(l2v02 >= clip_dist_scaled2 && l2v02 <= clip_dist_scaled2eps);
                     }
                 }
-            } else {
-                bool remove_poly = smooth_corner_complex(p1, it0, it2, shortcut_length); // edits p0_it and p2_it!
-                if (remove_poly) {
-                    // don't convert ListPolygon into result
-                    return;
-                }
-            }
-            // update:
-            it1 = it2; // next point to consider for whether it's an internal corner
-        }
-        else
+                it1 = it2;
+            } else
+                ++ it1;
+        } else
             ++ it1;
-    } while (it1 != polygon.begin());
+    }
+
+    if (polygon.size() == 3) {
+        // Check whether the last triangle is clockwise oriented (it is a hole) and its height is below clip_dist_scaled.
+        // If so, fill in the hole.
+        const Point   p0   = *polygon.begin().prev();
+        const Point   p1   = *polygon.begin();
+        const Point   p2   = *polygon.begin().next();
+        Vec2i64 v1   = (p0 - p1).cast<int64_t>();
+        Vec2i64 v2   = (p2 - p1).cast<int64_t>();
+        if (cross2(v1, v2) > 0) {
+            // CW triangle. Measure its height.
+            const Vec2i64 v3 = (p2 - p0).cast<int64_t>();
+            int64_t l12 = v1.squaredNorm();
+            int64_t l22 = v2.squaredNorm();
+            int64_t l32 = v3.squaredNorm();
+            if (l22 > l12 && l22 > l32) {
+                std::swap(v1,  v2);
+                std::swap(l12, l22);
+            } else if (l32 > l12 && l32 > l22) {
+                v1  = v3;
+                l12 = l32;
+            }
+            auto h2 = l22 - sqr(double(v1.dot(v2))) / double(l12);
+            if (h2 < clip_dist_scaled2)
+                // CW triangle with a low height. Close the hole.
+                polygon.clear();
+        }
+    } else if (polygon.size() < 3)
+        polygon.clear();
 }
 
 } // namespace Slic3r
diff --git a/src/libslic3r/MutablePolygon.hpp b/src/libslic3r/MutablePolygon.hpp
index f40b89e74..a601f19e3 100644
--- a/src/libslic3r/MutablePolygon.hpp
+++ b/src/libslic3r/MutablePolygon.hpp
@@ -6,6 +6,10 @@
 
 namespace Slic3r {
 
+// Polygon implemented as a loop of double linked elements.
+// All elements are allocated in a single std::vector<>, thus integer indices are used for
+// referencing the previous and next element and inside iterators to survive reallocation
+// of the vector.
 class MutablePolygon
 {
 public:
@@ -55,6 +59,69 @@ public:
         friend class MutablePolygon;
         MutablePolygon  *m_data;
         IndexType        m_idx;
+        friend class range;
+    };
+
+    // Iterator range for maintaining a range of unprocessed items, see smooth_outward().
+    class range
+    {
+    public:
+        range(MutablePolygon& poly) : range(poly.begin(), poly.end()) {}
+        range(MutablePolygon::iterator begin, MutablePolygon::iterator end) : m_begin(begin), m_end(end) {}
+
+        // Start of a range, inclusive. If range is empty, then ! begin().valid().
+        MutablePolygon::iterator    begin() const { return m_begin; }
+        // End of a range, inclusive. If range is empty, then ! end().valid().
+        MutablePolygon::iterator    end()   const { return m_end; }
+        // Is the range empty?
+        bool                        empty() const { return !m_begin.valid(); }
+
+        // Return begin() and shorten the range by advancing front.
+        MutablePolygon::iterator    process_next() {
+            assert(!this->empty());
+            MutablePolygon::iterator out = m_begin;
+            this->advance_front();
+            return out;
+        }
+
+        void advance_front() {
+            assert(!this->empty());
+            if (m_begin == m_end)
+                this->make_empty();
+            else
+                ++ m_begin;
+        }
+
+        void retract_back() {
+            assert(!this->empty());
+            if (m_begin == m_end)
+                this->make_empty();
+            else
+                -- m_end;
+        }
+
+        MutablePolygon::iterator remove_front(MutablePolygon::iterator it) {
+            if (m_begin == it)
+                this->advance_front();
+            return it.remove();
+        }
+
+        MutablePolygon::iterator remove_back(MutablePolygon::iterator it) {
+            if (m_end == it)
+                this->retract_back();
+            return it.remove();
+        }
+
+    private:
+        // Range from begin to end, inclusive.
+        // If the range is valid, then both m_begin and m_end are invalid.
+        MutablePolygon::iterator    m_begin;
+        MutablePolygon::iterator    m_end;
+
+        void make_empty() {
+            m_begin.m_idx = -1;
+            m_end.m_idx = -1;
+        }
     };
 
     MutablePolygon() = default;
@@ -63,26 +130,35 @@ public:
 
     template<typename IT>
     MutablePolygon(IT begin, IT end, size_t reserve = 0) {
-        m_size = IndexType(end - begin);
-        if (m_size > 0) {
-            m_head = 0;
-            m_data.reserve(std::max<size_t>(m_size, reserve));
-            auto i = IndexType(-1);
-            auto j = IndexType(1);
-            for (auto it = begin; it != end; ++ it)
-                m_data.push_back({ *it, i ++, j ++ });
-            m_data.front().prev = m_size - 1;
-            m_data.back ().next = 0;
+        this->assign_inner(begin, end, reserve);
+    };
+
+    template<typename IT>
+    void assign(IT begin, IT end, size_t reserve = 0) {
+        m_data.clear();
+        m_head      = IndexType(-1);
+        m_head_free = { IndexType(-1) };
+        this->assign_inner(begin, end, reserve);  
+    };
+
+    void assign(const Polygon &rhs, size_t reserve = 0) {
+        assign(rhs.points.begin(), rhs.points.end(), reserve);
+    }
+
+    void polygon(Polygon &out) const {
+        out.points.clear();
+        if (this->valid()) {
+            out.points.reserve(this->size());
+            auto it = this->cbegin();
+            out.points.emplace_back(*it);
+            for (++ it; it != this->cbegin(); ++ it)
+                out.points.emplace_back(*it);
         }
     };
 
     Polygon polygon() const {
         Polygon out;
-        if (this->valid()) {
-            out.points.reserve(this->size());
-            for (auto it = this->cbegin(); it != this->cend(); ++ it)
-                out.points.emplace_back(*it);
-        }
+        this->polygon(out);
         return out;
     };
 
@@ -90,6 +166,7 @@ public:
     size_t          size()   const { return this->m_size; }
     size_t          capacity() const { return this->m_data.capacity(); }
     bool            valid()  const { return this->m_size >= 3; }
+    void            clear()        { m_data.clear(); m_size = 0; m_head = IndexType(-1); m_head_free = IndexType(-1); }
 
     iterator        begin()        { return { this, m_head }; }
     const_iterator  cbegin() const { return { this, m_head }; }
@@ -108,8 +185,11 @@ public:
 
 private:
     struct LinkedPoint {
+        // 8 bytes
         PointType point;
+        // 4 bytes
         IndexType prev;
+        // 4 bytes
         IndexType next;
     };
     std::vector<LinkedPoint>    m_data;
@@ -122,6 +202,21 @@ private:
     LinkedPoint&          at(IndexType i)       { return m_data[i]; }
     const LinkedPoint&    at(IndexType i) const { return m_data[i]; }
 
+    template<typename IT>
+    void assign_inner(IT begin, IT end, size_t reserve) {
+        m_size = IndexType(end - begin);
+        if (m_size > 0) {
+            m_head = 0;
+            m_data.reserve(std::max<size_t>(m_size, reserve));
+            auto i = IndexType(-1);
+            auto j = IndexType(1);
+            for (auto it = begin; it != end; ++ it)
+                m_data.push_back({ *it, i ++, j ++ });
+            m_data.front().prev = m_size - 1;
+            m_data.back ().next = 0;
+        }
+    };
+
     IndexType remove(const IndexType i) {
         assert(i >= 0);
         assert(m_size > 0);
@@ -213,13 +308,26 @@ inline bool operator!=(const MutablePolygon &p1, const MutablePolygon &p2) { ret
 void remove_duplicates(MutablePolygon &polygon);
 void remove_duplicates(MutablePolygon &polygon, double eps);
 
-void smooth_outward(MutablePolygon &polygon, double shortcut_length);
+void smooth_outward(MutablePolygon &polygon, coord_t clip_dist_scaled);
 
-inline Polygon smooth_outward(const Polygon &polygon, double shortcut_length) 
+inline Polygon smooth_outward(Polygon polygon, coord_t clip_dist_scaled)
 { 
     MutablePolygon mp(polygon, polygon.size() * 2);
-    smooth_outward(mp, shortcut_length);
-    return mp.polygon();
+    smooth_outward(mp, clip_dist_scaled);
+    mp.polygon(polygon);
+    return polygon;
+}
+
+inline Polygons smooth_outward(Polygons polygons, coord_t clip_dist_scaled)
+{ 
+    MutablePolygon mp;
+    for (Polygon &polygon : polygons) {
+        mp.assign(polygon, polygon.size() * 2);
+        smooth_outward(mp, clip_dist_scaled);
+        mp.polygon(polygon);
+    }
+    polygons.erase(std::remove_if(polygons.begin(), polygons.end(), [](const auto &p){ return p.empty(); }), polygons.end());
+    return polygons;
 }
 
 }
diff --git a/tests/libslic3r/test_mutable_polygon.cpp b/tests/libslic3r/test_mutable_polygon.cpp
index 2214da6ef..238e3604f 100644
--- a/tests/libslic3r/test_mutable_polygon.cpp
+++ b/tests/libslic3r/test_mutable_polygon.cpp
@@ -1,5 +1,6 @@
 #include <catch2/catch.hpp>
 
+#include "libslic3r/Point.hpp"
 #include "libslic3r/MutablePolygon.hpp"
 
 using namespace Slic3r;
@@ -143,3 +144,36 @@ SCENARIO("Remove degenerate points from MutablePolygon", "[MutablePolygon]") {
         }
     }
 }
+
+SCENARIO("smooth_outward", "[MutablePolygon]") {
+    GIVEN("Convex polygon") {
+        MutablePolygon p{ { 0, 0 }, { scaled<coord_t>(10.), 0 }, { 0, scaled<coord_t>(10.) } };
+        WHEN("smooth_outward") {
+            MutablePolygon p2{ p };
+            smooth_outward(p2, scaled<double>(10.));
+            THEN("Polygon is unmodified") {
+                REQUIRE(p == p2);
+            }
+        }
+    }
+    GIVEN("Sharp tiny concave polygon (hole)") {
+        MutablePolygon p{ { 0, 0 }, { 0, scaled<coord_t>(5.) }, { scaled<coord_t>(10.), 0 } };
+        WHEN("smooth_outward") {
+            MutablePolygon p2{ p };
+            smooth_outward(p2, scaled<double>(10.));
+            THEN("Hole is closed") {
+                REQUIRE(p2.empty());
+            }
+        }
+    }
+    GIVEN("Two polygons") {
+        Polygons p{ { { 0, 0 }, { scaled<coord_t>(10.), 0 }, { 0, scaled<coord_t>(10.) } },
+                    { { 0, 0 }, { 0, scaled<coord_t>(5.) }, { scaled<coord_t>(10.), 0 } } };
+        WHEN("smooth_outward") {
+            p = smooth_outward(p, scaled<double>(10.));
+            THEN("CCW contour unmodified, CW contour removed.") {
+                REQUIRE(p == Polygons{ { { 0, 0 }, { scaled<coord_t>(10.), 0 }, { 0, scaled<coord_t>(10.) } } });
+            }
+        }
+    }
+}

From d99895805ce50534f5ba9c28c926e646f0031ac9 Mon Sep 17 00:00:00 2001
From: enricoturri1966 <enricoturri@seznam.cz>
Date: Wed, 3 Mar 2021 15:17:45 +0100
Subject: [PATCH 3/6] Follow-up of 6a46b71dc1b72834f36e16f1bbf3e13f40e64c1d -
 Fix build on non-Windows platforms

---
 src/libslic3r/GCode/GCodeProcessor.cpp | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp
index 982a5067c..73ad25496 100644
--- a/src/libslic3r/GCode/GCodeProcessor.cpp
+++ b/src/libslic3r/GCode/GCodeProcessor.cpp
@@ -2168,10 +2168,10 @@ void GCodeProcessor::process_G28(const GCodeReader::GCodeLine& line)
     if (!found)
         new_line_raw += " X0  Y0  Z0";
 
-    GCodeReader::GCodeLine new_line;
+    GCodeReader::GCodeLine new_gline;
     GCodeReader reader;
-    reader.parse_line(new_line_raw.c_str(), new_line, [](GCodeReader&, const GCodeReader::GCodeLine&) {});
-    process_G1(new_line);
+    reader.parse_line(new_line_raw, [&](GCodeReader& reader, const GCodeReader::GCodeLine& gline) { new_gline = gline; });
+    process_G1(new_gline);
 }
 
 void GCodeProcessor::process_G90(const GCodeReader::GCodeLine& line)

From a7255235e5e010a545d68f3932efc11c33e5f216 Mon Sep 17 00:00:00 2001
From: tamasmeszaros <meszaros.q@gmail.com>
Date: Wed, 3 Mar 2021 15:19:24 +0100
Subject: [PATCH 4/6] Remove gcc warning about uninitialized values

---
 src/libslic3r/SLA/IndexedMesh.cpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/libslic3r/SLA/IndexedMesh.cpp b/src/libslic3r/SLA/IndexedMesh.cpp
index 485fa98ed..2f47c6387 100644
--- a/src/libslic3r/SLA/IndexedMesh.cpp
+++ b/src/libslic3r/SLA/IndexedMesh.cpp
@@ -122,7 +122,7 @@ IndexedMesh::hit_result
 IndexedMesh::query_ray_hit(const Vec3d &s, const Vec3d &dir) const
 {
     assert(is_approx(dir.norm(), 1.));
-    igl::Hit hit;
+    igl::Hit hit{-1, -1, 0.f, 0.f, 0.f};
     hit.t = std::numeric_limits<float>::infinity();
 
 #ifdef SLIC3R_HOLE_RAYCASTER

From abd5a9a46e16bab66988bf338516b1e0d1f5b4e8 Mon Sep 17 00:00:00 2001
From: Lukas Matena <lukasmatena@seznam.cz>
Date: Fri, 26 Feb 2021 08:23:37 +0100
Subject: [PATCH 5/6] Add a notification when custom support enforcers are not
 used due to supports being off It is now emitted from Print::validate and has
 a hyperlink to enable supports

---
 src/libslic3r/Print.cpp                     | 19 ++++++++-
 src/libslic3r/Print.hpp                     |  2 +-
 src/libslic3r/PrintBase.hpp                 |  2 +-
 src/libslic3r/PrintObject.cpp               | 18 +++++++++
 src/libslic3r/SLAPrint.cpp                  |  2 +-
 src/libslic3r/SLAPrint.hpp                  |  2 +-
 src/slic3r/GUI/BackgroundSlicingProcess.cpp |  4 +-
 src/slic3r/GUI/BackgroundSlicingProcess.hpp |  2 +-
 src/slic3r/GUI/NotificationManager.hpp      |  4 +-
 src/slic3r/GUI/Plater.cpp                   | 45 ++++++++++++++++++++-
 10 files changed, 89 insertions(+), 11 deletions(-)

diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp
index 643cc9a97..89a8dc77b 100644
--- a/src/libslic3r/Print.cpp
+++ b/src/libslic3r/Print.cpp
@@ -1245,7 +1245,7 @@ static inline bool sequential_print_vertical_clearance_valid(const Print &print)
 }
 
 // Precondition: Print::validate() requires the Print::apply() to be called its invocation.
-std::string Print::validate() const
+std::string Print::validate(std::string* warning) const
 {
     if (m_objects.empty())
         return L("All objects are outside of the print volume.");
@@ -1440,7 +1440,22 @@ std::string Print::validate() const
     				}
                 }
             }
-            
+
+            // Do we have custom support data that would not be used?
+            // Notify the user in that case.
+            if (! object->has_support() && warning) {
+                for (const ModelVolume* mv : object->model_object()->volumes) {
+                    bool has_enforcers = mv->is_support_enforcer()
+                        || (mv->is_model_part()
+                            && ! mv->supported_facets.empty()
+                            && ! mv->supported_facets.get_facets(*mv, EnforcerBlockerType::ENFORCER).indices.empty());
+                    if (has_enforcers) {
+                        *warning = "_SUPPORTS_OFF";
+                        break;
+                    }
+                }
+            }
+
             // validate first_layer_height
             double first_layer_height = object->config().get_abs_value("first_layer_height");
             double first_layer_min_nozzle_diameter;
diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp
index e8e81a529..9f22ddf13 100644
--- a/src/libslic3r/Print.hpp
+++ b/src/libslic3r/Print.hpp
@@ -444,7 +444,7 @@ public:
     bool                has_brim() const;
 
     // Returns an empty string if valid, otherwise returns an error message.
-    std::string         validate() const override;
+    std::string         validate(std::string* warning = nullptr) const override;
     double              skirt_first_layer_height() const;
     Flow                brim_flow() const;
     Flow                skirt_flow() const;
diff --git a/src/libslic3r/PrintBase.hpp b/src/libslic3r/PrintBase.hpp
index 2aff13ae9..e0aa56ba5 100644
--- a/src/libslic3r/PrintBase.hpp
+++ b/src/libslic3r/PrintBase.hpp
@@ -366,7 +366,7 @@ public:
     virtual std::vector<ObjectID> print_object_ids() const = 0;
 
     // Validate the print, return empty string if valid, return error if process() cannot (or should not) be started.
-    virtual std::string     validate() const { return std::string(); }
+    virtual std::string     validate(std::string* warning = nullptr) const { return std::string(); }
 
     enum ApplyStatus {
         // No change after the Print::apply() call.
diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp
index c963418c3..52bdc87e7 100644
--- a/src/libslic3r/PrintObject.cpp
+++ b/src/libslic3r/PrintObject.cpp
@@ -430,6 +430,24 @@ void PrintObject::generate_support_material()
                 if (layer->empty())
                     throw Slic3r::SlicingError("Levitating objects cannot be printed without supports.");
 #endif
+
+            // Do we have custom support data that would not be used?
+            // Notify the user in that case.
+            if (! this->has_support()) {
+                for (const ModelVolume* mv : this->model_object()->volumes) {
+                    bool has_enforcers = mv->is_support_enforcer()
+                        || (mv->is_model_part()
+                            && ! mv->supported_facets.empty()
+                            && ! mv->supported_facets.get_facets(*mv, EnforcerBlockerType::ENFORCER).indices.empty());
+                    if (has_enforcers) {
+                        this->active_step_add_warning(PrintStateBase::WarningLevel::CRITICAL,
+                            L("An object has custom support enforcers which will not be used "
+                              "because supports are off. Consider turning them on.") + "\n" +
+                            (L("Object name")) + ": " + this->model_object()->name);
+                        break;
+                    }
+                }
+            }
         }
         this->set_done(posSupportMaterial);
     }
diff --git a/src/libslic3r/SLAPrint.cpp b/src/libslic3r/SLAPrint.cpp
index 65fac73f3..16b068cb9 100644
--- a/src/libslic3r/SLAPrint.cpp
+++ b/src/libslic3r/SLAPrint.cpp
@@ -617,7 +617,7 @@ std::string SLAPrint::output_filename(const std::string &filename_base) const
     return this->PrintBase::output_filename(m_print_config.output_filename_format.value, ".sl1", filename_base, &config);
 }
 
-std::string SLAPrint::validate() const
+std::string SLAPrint::validate(std::string*) const
 {
     for(SLAPrintObject * po : m_objects) {
 
diff --git a/src/libslic3r/SLAPrint.hpp b/src/libslic3r/SLAPrint.hpp
index 742670e2c..bed66ab4f 100644
--- a/src/libslic3r/SLAPrint.hpp
+++ b/src/libslic3r/SLAPrint.hpp
@@ -458,7 +458,7 @@ public:
 
     const SLAPrintStatistics&   print_statistics() const { return m_print_statistics; }
 
-    std::string validate() const override;
+    std::string validate(std::string* warning = nullptr) const override;
 
     // An aggregation of SliceRecord-s from all the print objects for each
     // occupied layer. Slice record levels dont have to match exactly.
diff --git a/src/slic3r/GUI/BackgroundSlicingProcess.cpp b/src/slic3r/GUI/BackgroundSlicingProcess.cpp
index 87a322023..089fba656 100644
--- a/src/slic3r/GUI/BackgroundSlicingProcess.cpp
+++ b/src/slic3r/GUI/BackgroundSlicingProcess.cpp
@@ -430,10 +430,10 @@ bool BackgroundSlicingProcess::empty() const
 	return m_print->empty();
 }
 
-std::string BackgroundSlicingProcess::validate()
+std::string BackgroundSlicingProcess::validate(std::string* warning)
 {
 	assert(m_print != nullptr);
-	return m_print->validate();
+    return m_print->validate(warning);
 }
 
 // Apply config over the print. Returns false, if the new config values caused any of the already
diff --git a/src/slic3r/GUI/BackgroundSlicingProcess.hpp b/src/slic3r/GUI/BackgroundSlicingProcess.hpp
index b3f8a0a6b..d3819f15c 100644
--- a/src/slic3r/GUI/BackgroundSlicingProcess.hpp
+++ b/src/slic3r/GUI/BackgroundSlicingProcess.hpp
@@ -131,7 +131,7 @@ public:
 	bool 		empty() const;
 	// Validate the print. Returns an empty string if valid, returns an error message if invalid.
 	// Call validate before calling start().
-	std::string validate();
+    std::string validate(std::string* warning = nullptr);
 
 	// Set the export path of the G-code.
 	// Once the path is set, the G-code 
diff --git a/src/slic3r/GUI/NotificationManager.hpp b/src/slic3r/GUI/NotificationManager.hpp
index 31dace42e..9ce278792 100644
--- a/src/slic3r/GUI/NotificationManager.hpp
+++ b/src/slic3r/GUI/NotificationManager.hpp
@@ -71,7 +71,9 @@ enum class NotificationType
     // Notification that custom supports/seams were deleted after mesh repair.
     CustomSupportsAndSeamRemovedAfterRepair,
     // Notification that auto adding of color changes is impossible
-	EmptyAutoColorChange
+    EmptyAutoColorChange,
+    // Notification emitted by Print::validate
+    PrintValidateWarning
 };
 
 class NotificationManager
diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp
index 762acfdf1..93d0e0881 100644
--- a/src/slic3r/GUI/Plater.cpp
+++ b/src/slic3r/GUI/Plater.cpp
@@ -1599,6 +1599,8 @@ struct Plater::priv
     void suppress_snapshots()   { this->m_prevent_snapshots++; }
     void allow_snapshots()      { this->m_prevent_snapshots--; }
 
+    void process_validation_warning(const std::string& warning) const;
+
     bool background_processing_enabled() const { return this->get_config("background_processing") == "1"; }
     void update_print_volume_state();
     void schedule_background_process();
@@ -2787,6 +2789,41 @@ void Plater::priv::update_print_volume_state()
     this->q->model().update_print_volume_state(print_volume);
 }
 
+
+void Plater::priv::process_validation_warning(const std::string& warning) const
+{
+    if (warning.empty())
+        notification_manager->close_notification_of_type(NotificationType::PrintValidateWarning);
+    else {
+        std::string text = warning;
+        std::string hypertext = "";
+        std::function<bool(wxEvtHandler*)> action_fn = [](wxEvtHandler*){ return false; };
+
+        if (text == "_SUPPORTS_OFF") {
+            text = _u8L("An object has custom support enforcers which will not be used "
+                        "because supports are disabled.")+"\n";
+            hypertext = _u8L("Enable supports for enforcers only");
+            action_fn = [](wxEvtHandler*) {
+                Tab* print_tab = wxGetApp().get_tab(Preset::TYPE_PRINT);
+                assert(print_tab);
+                DynamicPrintConfig& config = wxGetApp().preset_bundle->prints.get_edited_preset().config;
+                config.set_key_value("support_material", new ConfigOptionBool(true));
+                config.set_key_value("support_material_auto", new ConfigOptionBool(false));
+                print_tab->on_value_change("support_material", config.opt_bool("support_material"));
+                print_tab->on_value_change("support_material_auto", config.opt_bool("support_material_auto"));
+                return true;
+            };
+        }
+
+        notification_manager->push_notification(
+            NotificationType::PrintValidateWarning,
+            NotificationManager::NotificationLevel::ImportantNotification,
+            text, hypertext, action_fn
+        );
+    }
+}
+
+
 // Update background processing thread from the current config and Model.
 // Returns a bitmask of UpdateBackgroundProcessReturnState.
 unsigned int Plater::priv::update_background_process(bool force_validation, bool postpone_error_messages)
@@ -2829,17 +2866,23 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool
 		// The delayed error message is no more valid.
 		this->delayed_error_message.clear();
 		// The state of the Print changed, and it is non-zero. Let's validate it and give the user feedback on errors.
-        std::string err = this->background_process.validate();
+        std::string warning;
+        std::string err = this->background_process.validate(&warning);
         if (err.empty()) {
 			notification_manager->set_all_slicing_errors_gray(true);
             if (invalidated != Print::APPLY_STATUS_UNCHANGED && this->background_processing_enabled())
                 return_state |= UPDATE_BACKGROUND_PROCESS_RESTART;
+
+            // Pass a warning from validation and either show a notification,
+            // or hide the old one.
+            process_validation_warning(warning);
         } else {
 			// The print is not valid.
 			// Show error as notification.
             notification_manager->push_slicing_error_notification(err);
             return_state |= UPDATE_BACKGROUND_PROCESS_INVALID;
         }
+
     } else if (! this->delayed_error_message.empty()) {
     	// Reusing the old state.
         return_state |= UPDATE_BACKGROUND_PROCESS_INVALID;

From 70573484c29fc7f80a634a4eedbbb9c1d495c23e Mon Sep 17 00:00:00 2001
From: Lukas Matena <lukasmatena@seznam.cz>
Date: Tue, 2 Mar 2021 11:47:09 +0100
Subject: [PATCH 6/6] Show an error notification when attempting to save 3MF
 while editing SLA support points The notification disappears when it is no
 longer valid.

---
 src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp | 2 ++
 src/slic3r/GUI/Gizmos/GLGizmosManager.cpp    | 8 +++++---
 src/slic3r/GUI/NotificationManager.hpp       | 4 +++-
 src/slic3r/GUI/Plater.cpp                    | 4 +++-
 4 files changed, 13 insertions(+), 5 deletions(-)

diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp b/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp
index b5dbab284..b200623f4 100644
--- a/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp
+++ b/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp
@@ -16,6 +16,7 @@
 #include "slic3r/GUI/GUI_ObjectSettings.hpp"
 #include "slic3r/GUI/GUI_ObjectList.hpp"
 #include "slic3r/GUI/Plater.hpp"
+#include "slic3r/GUI/NotificationManager.hpp"
 #include "libslic3r/PresetBundle.hpp"
 #include "libslic3r/SLAPrint.hpp"
 
@@ -1161,6 +1162,7 @@ void GLGizmoSlaSupports::disable_editing_mode()
         m_c->instances_hider()->show_supports(true);
         m_parent.set_as_dirty();
     }
+    wxGetApp().plater()->get_notification_manager()->close_notification_of_type(NotificationType::QuitSLAManualMode);
 }
 
 
diff --git a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp
index abbc8599b..9dc785b3f 100644
--- a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp
+++ b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp
@@ -1147,9 +1147,11 @@ bool GLGizmosManager::is_in_editing_mode(bool error_notification) const
         return false;
 
     if (error_notification)
-        wxGetApp().plater()->get_notification_manager()->push_slicing_error_notification(
-            _u8L("You are currently editing SLA support points. Please, apply or discard "
-                 "your changes first."));
+        wxGetApp().plater()->get_notification_manager()->push_notification(
+                    NotificationType::QuitSLAManualMode,
+                    NotificationManager::NotificationLevel::ErrorNotification,
+                    _u8L("You are currently editing SLA support points. Please, "
+                         "apply or discard your changes first."));
 
     return true;
 }
diff --git a/src/slic3r/GUI/NotificationManager.hpp b/src/slic3r/GUI/NotificationManager.hpp
index 9ce278792..62c4ea845 100644
--- a/src/slic3r/GUI/NotificationManager.hpp
+++ b/src/slic3r/GUI/NotificationManager.hpp
@@ -73,7 +73,9 @@ enum class NotificationType
     // Notification that auto adding of color changes is impossible
     EmptyAutoColorChange,
     // Notification emitted by Print::validate
-    PrintValidateWarning
+    PrintValidateWarning,
+    // Notification telling user to quit SLA supports manual editing
+    QuitSLAManualMode
 };
 
 class NotificationManager
diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp
index 93d0e0881..15b3a17c7 100644
--- a/src/slic3r/GUI/Plater.cpp
+++ b/src/slic3r/GUI/Plater.cpp
@@ -5418,7 +5418,9 @@ void Plater::export_amf()
 
 void Plater::export_3mf(const boost::filesystem::path& output_path)
 {
-    if (p->model.objects.empty()) { return; }
+    if (p->model.objects.empty()
+     || canvas3D()->get_gizmos_manager().is_in_editing_mode(true))
+        return;
 
     wxString path;
     bool export_config = true;