From 3687bc28d551f612c82c5a8dad9257d4f3a11f27 Mon Sep 17 00:00:00 2001
From: Vojtech Bubnik <bubnikv@gmail.com>
Date: Thu, 28 Apr 2022 15:59:13 +0200
Subject: [PATCH 01/23] Some reduction of Perl dependencies on ClipperLib,
 ported some ClipperLib polyline clipping tests to C++.

---
 lib/Slic3r/ExPolygon.pm                |   5 -
 lib/Slic3r/Geometry.pm                 |  37 --------
 lib/Slic3r/Geometry/Clipper.pm         |   4 +-
 lib/Slic3r/Print/Object.pm             |   2 +-
 t/geometry.t                           |  40 +-------
 t/polyclip.t                           | 121 -------------------------
 tests/libslic3r/test_clipper_utils.cpp |  78 ++++++++++++++++
 xs/xsp/Clipper.xsp                     |  20 ----
 xs/xsp/Surface.xsp                     |  10 --
 9 files changed, 82 insertions(+), 235 deletions(-)
 delete mode 100644 t/polyclip.t

diff --git a/lib/Slic3r/ExPolygon.pm b/lib/Slic3r/ExPolygon.pm
index 6adb650c2..6337cb9a1 100644
--- a/lib/Slic3r/ExPolygon.pm
+++ b/lib/Slic3r/ExPolygon.pm
@@ -12,11 +12,6 @@ sub offset {
     return Slic3r::Geometry::Clipper::offset(\@$self, @_);
 }
 
-sub offset_ex {
-    my $self = shift;
-    return Slic3r::Geometry::Clipper::offset_ex(\@$self, @_);
-}
-
 sub noncollapsing_offset_ex {
     my $self = shift;
     my ($distance, @params) = @_;
diff --git a/lib/Slic3r/Geometry.pm b/lib/Slic3r/Geometry.pm
index 286a73e2d..ca262fc76 100644
--- a/lib/Slic3r/Geometry.pm
+++ b/lib/Slic3r/Geometry.pm
@@ -14,10 +14,8 @@ our @EXPORT_OK = qw(
     dot
     line_intersection
     normalize
-    point_in_segment
     polyline_lines
     polygon_is_convex
-    polygon_segment_having_point
     scale
     unscale
     scaled_epsilon
@@ -45,30 +43,6 @@ sub scaled_epsilon () { epsilon / &Slic3r::SCALING_FACTOR }
 sub scale   ($) { $_[0] / &Slic3r::SCALING_FACTOR }
 sub unscale ($) { $_[0] * &Slic3r::SCALING_FACTOR }
 
-# used by geometry.t, polygon_segment_having_point
-sub point_in_segment {
-    my ($point, $line) = @_;
-    
-    my ($x, $y) = @$point;
-    my $line_p = $line->pp;
-    my @line_x = sort { $a <=> $b } $line_p->[A][X], $line_p->[B][X];
-    my @line_y = sort { $a <=> $b } $line_p->[A][Y], $line_p->[B][Y];
-    
-    # check whether the point is in the segment bounding box
-    return 0 unless $x >= ($line_x[0] - epsilon) && $x <= ($line_x[1] + epsilon)
-        && $y >= ($line_y[0] - epsilon) && $y <= ($line_y[1] + epsilon);
-    
-    # if line is vertical, check whether point's X is the same as the line
-    if ($line_p->[A][X] == $line_p->[B][X]) {
-        return abs($x - $line_p->[A][X]) < epsilon ? 1 : 0;
-    }
-    
-    # calculate the Y in line at X of the point
-    my $y3 = $line_p->[A][Y] + ($line_p->[B][Y] - $line_p->[A][Y])
-        * ($x - $line_p->[A][X]) / ($line_p->[B][X] - $line_p->[A][X]);
-    return abs($y3 - $y) < epsilon ? 1 : 0;
-}
-
 # used by geometry.t
 sub polyline_lines {
     my ($polyline) = @_;
@@ -76,17 +50,6 @@ sub polyline_lines {
     return map Slic3r::Line->new(@points[$_, $_+1]), 0 .. $#points-1;
 }
 
-# given a $polygon, return the (first) segment having $point
-# used by geometry.t
-sub polygon_segment_having_point {
-    my ($polygon, $point) = @_;
-    
-    foreach my $line (@{ $polygon->lines }) {
-        return $line if point_in_segment($point, $line);
-    }
-    return undef;
-}
-
 # polygon must be simple (non complex) and ccw
 sub polygon_is_convex {
     my ($points) = @_;
diff --git a/lib/Slic3r/Geometry/Clipper.pm b/lib/Slic3r/Geometry/Clipper.pm
index b7a7da772..cfcb622fd 100644
--- a/lib/Slic3r/Geometry/Clipper.pm
+++ b/lib/Slic3r/Geometry/Clipper.pm
@@ -6,9 +6,9 @@ require Exporter;
 our @ISA = qw(Exporter);
 our @EXPORT_OK = qw(
 	offset 
-	offset_ex offset2_ex
+	offset2_ex
     diff_ex diff union_ex intersection_ex 
     JT_ROUND JT_MITER JT_SQUARE 
-    intersection intersection_pl diff_pl union);
+    intersection diff_pl union);
 
 1;
diff --git a/lib/Slic3r/Print/Object.pm b/lib/Slic3r/Print/Object.pm
index 0385f88b8..f03a97ea3 100644
--- a/lib/Slic3r/Print/Object.pm
+++ b/lib/Slic3r/Print/Object.pm
@@ -7,7 +7,7 @@ use List::Util qw(min max sum first);
 use Slic3r::Flow ':roles';
 use Slic3r::Geometry qw(scale epsilon);
 use Slic3r::Geometry::Clipper qw(diff diff_ex intersection intersection_ex union union_ex 
-    offset offset_ex offset2_ex JT_MITER);
+    offset offset2_ex JT_MITER);
 use Slic3r::Print::State ':steps';
 use Slic3r::Surface ':types';
 
diff --git a/t/geometry.t b/t/geometry.t
index bb72b22e1..874dab987 100644
--- a/t/geometry.t
+++ b/t/geometry.t
@@ -2,7 +2,7 @@ use Test::More;
 use strict;
 use warnings;
 
-plan tests => 30;
+plan tests => 27;
 
 BEGIN {
     use FindBin;
@@ -39,44 +39,6 @@ isnt Slic3r::Geometry::line_intersection($line1, $line2, 1), undef, 'line_inters
 
 #==========================================================
 
-{
-    my $polygon = Slic3r::Polygon->new(
-        [45919000, 515273900], [14726100, 461246400], [14726100, 348753500], [33988700, 315389800], 
-        [43749700, 343843000], [45422300, 352251500], [52362100, 362637800], [62748400, 369577600], 
-        [75000000, 372014700], [87251500, 369577600], [97637800, 362637800], [104577600, 352251500], 
-        [107014700, 340000000], [104577600, 327748400], [97637800, 317362100], [87251500, 310422300], 
-        [82789200, 309534700], [69846100, 294726100], [254081000, 294726100], [285273900, 348753500], 
-        [285273900, 461246400], [254081000, 515273900],
-    );
-    
-    # this points belongs to $polyline
-    # note: it's actually a vertex, while we should better check an intermediate point
-    my $point = Slic3r::Point->new(104577600, 327748400);
-    
-    local $Slic3r::Geometry::epsilon = 1E-5;
-    is_deeply Slic3r::Geometry::polygon_segment_having_point($polygon, $point)->pp, 
-        [ [107014700, 340000000], [104577600, 327748400] ],
-        'polygon_segment_having_point';
-}
-
-#==========================================================
-
-{
-    my $point = Slic3r::Point->new(736310778.185108, 5017423926.8924);
-    my $line = Slic3r::Line->new([627484000, 3695776000], [750000000, 3720147000]);
-    is Slic3r::Geometry::point_in_segment($point, $line), 0, 'point_in_segment';
-}
-
-#==========================================================
-
-{
-    my $point = Slic3r::Point->new(736310778.185108, 5017423926.8924);
-    my $line = Slic3r::Line->new([627484000, 3695776000], [750000000, 3720147000]);
-    is Slic3r::Geometry::point_in_segment($point, $line), 0, 'point_in_segment';
-}
-
-#==========================================================
-
 my $polygons = [
     Slic3r::Polygon->new( # contour, ccw
         [45919000, 515273900], [14726100, 461246400], [14726100, 348753500], [33988700, 315389800], 
diff --git a/t/polyclip.t b/t/polyclip.t
deleted file mode 100644
index 0808c7be9..000000000
--- a/t/polyclip.t
+++ /dev/null
@@ -1,121 +0,0 @@
-use Test::More;
-use strict;
-use warnings;
-
-plan tests => 18;
-
-BEGIN {
-    use FindBin;
-    use lib "$FindBin::Bin/../lib";
-    use local::lib "$FindBin::Bin/../local-lib";
-}
-
-use Slic3r;
-use Slic3r::Geometry::Clipper qw(intersection_pl);
-
-#==========================================================
-
-is Slic3r::Geometry::point_in_segment(Slic3r::Point->new(10, 10), Slic3r::Line->new([5, 10], [20, 10])), 1, 'point in horizontal segment';
-is Slic3r::Geometry::point_in_segment(Slic3r::Point->new(30, 10), Slic3r::Line->new([5, 10], [20, 10])), 0, 'point not in horizontal segment';
-is Slic3r::Geometry::point_in_segment(Slic3r::Point->new(10, 10), Slic3r::Line->new([10, 5], [10, 20])), 1, 'point in vertical segment';
-is Slic3r::Geometry::point_in_segment(Slic3r::Point->new(10, 30), Slic3r::Line->new([10, 5], [10, 20])), 0, 'point not in vertical segment';
-is Slic3r::Geometry::point_in_segment(Slic3r::Point->new(15, 15), Slic3r::Line->new([10, 10], [20, 20])), 1, 'point in diagonal segment';
-is Slic3r::Geometry::point_in_segment(Slic3r::Point->new(20, 15), Slic3r::Line->new([10, 10], [20, 20])), 0, 'point not in diagonal segment';
-
-#==========================================================
-
-my $square = Slic3r::Polygon->new(  # ccw
-    [100, 100],
-    [200, 100],
-    [200, 200],
-    [100, 200],
-);
-
-#==========================================================
-
-{
-    my $hole_in_square = [  # cw
-        [140, 140],
-        [140, 160],
-        [160, 160],
-        [160, 140],
-    ];
-    my $expolygon = Slic3r::ExPolygon->new($square, $hole_in_square);
-    #is $expolygon->contains_point(Slic3r::Point->new(100, 100)), 1, 'corner point is recognized';
-    #is $expolygon->contains_point(Slic3r::Point->new(100, 180)), 1, 'point on contour is recognized';
-    #is $expolygon->contains_point(Slic3r::Point->new(140, 150)), 1, 'point on hole contour is recognized';
-    #is $expolygon->contains_point(Slic3r::Point->new(140, 140)), 1, 'point on hole corner is recognized';
-    {
-        my $intersection = intersection_pl([Slic3r::Polyline->new([150,180], [150,150])], \@$expolygon);
-        is $intersection->[0]->length, Slic3r::Line->new([150, 180], [150, 160])->length,
-            'line is clipped to square with hole';
-    }
-    {
-        my $intersection = intersection_pl([Slic3r::Polyline->new([150,150], [150,120])], \@$expolygon);
-        is $intersection->[0]->length, Slic3r::Line->new([150, 140], [150, 120])->length,
-            'line is clipped to square with hole';
-    }
-    {
-        my $intersection = intersection_pl([Slic3r::Polyline->new([120,180], [180,180])], \@$expolygon);
-        is $intersection->[0]->length, Slic3r::Line->new([120,180], [180,180])->length,
-            'line is clipped to square with hole';
-    }
-    {
-        my $intersection = intersection_pl([Slic3r::Polyline->new([50, 150], [300, 150])], \@$expolygon);
-        is $intersection->[0]->length, Slic3r::Line->new([100, 150], [140, 150])->length,
-            'line is clipped to square with hole';
-        is $intersection->[1]->length, Slic3r::Line->new([160, 150], [200, 150])->length,
-            'line is clipped to square with hole';
-    }
-    {
-        my $intersection = intersection_pl([Slic3r::Polyline->new([300, 150], [50, 150])], \@$expolygon);
-        is $intersection->[0]->length, Slic3r::Line->new([200, 150], [160, 150])->length,
-            'reverse line is clipped to square with hole';
-        is $intersection->[1]->length, Slic3r::Line->new([140, 150], [100, 150])->length,
-            'reverse line is clipped to square with hole';
-    }
-    {
-        my $intersection = intersection_pl([Slic3r::Polyline->new([100,180], [200,180])], \@$expolygon);
-        is $intersection->[0]->length, Slic3r::Line->new([100,180], [200,180])->length,
-            'tangent line is clipped to square with hole';
-    }
-}
-
-#==========================================================
-
-{
-    my $large_circle = Slic3r::Polygon->new_scale(  # ccw
-        [151.8639,288.1192], [133.2778,284.6011], [115.0091,279.6997], [98.2859,270.8606], [82.2734,260.7933], 
-        [68.8974,247.4181], [56.5622,233.0777], [47.7228,216.3558], [40.1617,199.0172], [36.6431,180.4328], 
-        [34.932,165.2312], [37.5567,165.1101], [41.0547,142.9903], [36.9056,141.4295], [40.199,124.1277], 
-        [47.7776,106.7972], [56.6335,90.084], [68.9831,75.7557], [82.3712,62.3948], [98.395,52.3429], 
-        [115.1281,43.5199], [133.4004,38.6374], [151.9884,35.1378], [170.8905,35.8571], [189.6847,37.991], 
-        [207.5349,44.2488], [224.8662,51.8273], [240.0786,63.067], [254.407,75.4169], [265.6311,90.6406], 
-        [275.6832,106.6636], [281.9225,124.52], [286.8064,142.795], [287.5061,161.696], [286.7874,180.5972], 
-        [281.8856,198.8664], [275.6283,216.7169], [265.5604,232.7294], [254.3211,247.942], [239.9802,260.2776], 
-        [224.757,271.5022], [207.4179,279.0635], [189.5605,285.3035], [170.7649,287.4188],
-    );
-    ok $large_circle->is_counter_clockwise, "contour is counter-clockwise";
-    
-    my $small_circle = Slic3r::Polygon->new_scale(  # cw
-        [158.227,215.9007], [164.5136,215.9007], [175.15,214.5007], [184.5576,210.6044], [190.2268,207.8743], 
-        [199.1462,201.0306], [209.0146,188.346], [213.5135,177.4829], [214.6979,168.4866], [216.1025,162.3325], 
-        [214.6463,151.2703], [213.2471,145.1399], [209.0146,134.9203], [199.1462,122.2357], [189.8944,115.1366], 
-        [181.2504,111.5567], [175.5684,108.8205], [164.5136,107.3655], [158.2269,107.3655], [147.5907,108.7656], 
-        [138.183,112.6616], [132.5135,115.3919], [123.5943,122.2357], [113.7259,134.92], [109.2269,145.7834], 
-        [108.0426,154.7799], [106.638,160.9339], [108.0941,171.9957], [109.4933,178.1264], [113.7259,188.3463], 
-        [123.5943,201.0306], [132.8461,208.1296], [141.4901,211.7094], [147.172,214.4458],
-    );
-    ok $small_circle->is_clockwise, "hole is clockwise";
-    
-    my $expolygon = Slic3r::ExPolygon->new($large_circle, $small_circle);
-    my $line = Slic3r::Polyline->new_scale([152.742,288.086671142818], [152.742,34.166466971035]);
-    
-    my $intersection = intersection_pl([$line], \@$expolygon);
-    is $intersection->[0]->length, Slic3r::Line->new([152742000, 288086661], [152742000, 215178843])->length,
-        'line is clipped to square with hole';
-    is $intersection->[1]->length, Slic3r::Line->new([152742000, 108087507], [152742000, 35166477])->length,
-        'line is clipped to square with hole';
-}
-
-#==========================================================
diff --git a/tests/libslic3r/test_clipper_utils.cpp b/tests/libslic3r/test_clipper_utils.cpp
index 1b2b45eca..b357d8ca8 100644
--- a/tests/libslic3r/test_clipper_utils.cpp
+++ b/tests/libslic3r/test_clipper_utils.cpp
@@ -188,6 +188,46 @@ SCENARIO("Various Clipper operations - t/clipper.t", "[ClipperUtils]") {
                 REQUIRE(intersection.front().area() == Approx(match.area()));
             }
         }
+
+        ExPolygons expolygons { ExPolygon { square, hole_in_square } };
+        WHEN("Clipping line 1") {
+            Polylines intersection = intersection_pl({ Polyline { { 15, 18 }, { 15, 15 } } }, expolygons);
+            THEN("line is clipped to square with hole") {
+                REQUIRE((Vec2f(15, 18) - Vec2f(15, 16)).norm() == Approx(intersection.front().length()));
+            }
+        }
+        WHEN("Clipping line 2") {
+            Polylines intersection = intersection_pl({ Polyline { { 15, 15 }, { 15, 12 } } }, expolygons);
+            THEN("line is clipped to square with hole") {
+                REQUIRE((Vec2f(15, 14) - Vec2f(15, 12)).norm() == Approx(intersection.front().length()));
+            }
+        }
+        WHEN("Clipping line 3") {
+            Polylines intersection = intersection_pl({ Polyline { { 12, 18 }, { 18, 18 } } }, expolygons);
+            THEN("line is clipped to square with hole") {
+                REQUIRE((Vec2f(18, 18) - Vec2f(12, 18)).norm() == Approx(intersection.front().length()));
+            }
+        }
+        WHEN("Clipping line 4") {
+            Polylines intersection = intersection_pl({ Polyline { { 5, 15 }, { 30, 15 } } }, expolygons);
+            THEN("line is clipped to square with hole") {
+                REQUIRE((Vec2f(14, 15) - Vec2f(10, 15)).norm() == Approx(intersection.front().length()));
+                REQUIRE((Vec2f(20, 15) - Vec2f(16, 15)).norm() == Approx(intersection[1].length()));
+            }
+        }
+        WHEN("Clipping line 5") {
+            Polylines intersection = intersection_pl({ Polyline { { 30, 15 }, { 5, 15 } } }, expolygons);
+            THEN("reverse line is clipped to square with hole") {
+                REQUIRE((Vec2f(20, 15) - Vec2f(16, 15)).norm() == Approx(intersection.front().length()));
+                REQUIRE((Vec2f(14, 15) - Vec2f(10, 15)).norm() == Approx(intersection[1].length()));
+            }
+        }
+        WHEN("Clipping line 6") {
+            Polylines intersection = intersection_pl({ Polyline { { 10, 18 }, { 20, 18 } } }, expolygons);
+            THEN("tangent line is clipped to square with hole") {
+                REQUIRE((Vec2f(20, 18) - Vec2f(10, 18)).norm() == Approx(intersection.front().length()));
+            }
+        }
     }
     GIVEN("square with hole 2") {
         // CCW oriented contour
@@ -223,6 +263,44 @@ SCENARIO("Various Clipper operations - t/clipper.t", "[ClipperUtils]") {
             }
         }
     }
+    GIVEN("circle") {
+        Slic3r::ExPolygon circle_with_hole { Polygon::new_scale({
+                { 151.8639,288.1192 }, {133.2778,284.6011}, { 115.0091,279.6997 }, { 98.2859,270.8606 }, { 82.2734,260.7933 }, 
+                { 68.8974,247.4181 }, { 56.5622,233.0777 }, { 47.7228,216.3558 }, { 40.1617,199.0172 }, { 36.6431,180.4328 }, 
+                { 34.932,165.2312 }, { 37.5567,165.1101 }, { 41.0547,142.9903 }, { 36.9056,141.4295 }, { 40.199,124.1277 }, 
+                { 47.7776,106.7972 }, { 56.6335,90.084 }, { 68.9831,75.7557 }, { 82.3712,62.3948 }, { 98.395,52.3429 }, 
+                { 115.1281,43.5199 }, { 133.4004,38.6374 }, { 151.9884,35.1378 }, { 170.8905,35.8571 }, { 189.6847,37.991 }, 
+                { 207.5349,44.2488 }, { 224.8662,51.8273 }, { 240.0786,63.067 }, { 254.407,75.4169 }, { 265.6311,90.6406 }, 
+                { 275.6832,106.6636 }, { 281.9225,124.52 }, { 286.8064,142.795 }, { 287.5061,161.696 }, { 286.7874,180.5972 }, 
+                { 281.8856,198.8664 }, { 275.6283,216.7169 }, { 265.5604,232.7294 }, { 254.3211,247.942 }, { 239.9802,260.2776 }, 
+                { 224.757,271.5022 }, { 207.4179,279.0635 }, { 189.5605,285.3035 }, { 170.7649,287.4188 }
+            }) };
+        circle_with_hole.holes = { Polygon::new_scale({
+                { 158.227,215.9007 }, { 164.5136,215.9007 }, { 175.15,214.5007 }, { 184.5576,210.6044 }, { 190.2268,207.8743 }, 
+                { 199.1462,201.0306 }, { 209.0146,188.346 }, { 213.5135,177.4829 }, { 214.6979,168.4866 }, { 216.1025,162.3325 }, 
+                { 214.6463,151.2703 }, { 213.2471,145.1399 }, { 209.0146,134.9203 }, { 199.1462,122.2357 }, { 189.8944,115.1366 }, 
+                { 181.2504,111.5567 }, { 175.5684,108.8205 }, { 164.5136,107.3655 }, { 158.2269,107.3655 }, { 147.5907,108.7656 }, 
+                { 138.183,112.6616 }, { 132.5135,115.3919 }, { 123.5943,122.2357 }, { 113.7259,134.92 }, { 109.2269,145.7834 }, 
+                { 108.0426,154.7799 }, { 106.638,160.9339 }, { 108.0941,171.9957 }, { 109.4933,178.1264 }, { 113.7259,188.3463 }, 
+                { 123.5943,201.0306 }, { 132.8461,208.1296 }, { 141.4901,211.7094 }, { 147.172,214.4458 }
+            }) };
+        THEN("contour is counter-clockwise") {
+            REQUIRE(circle_with_hole.contour.is_counter_clockwise());
+        }
+        THEN("hole is counter-clockwise") {
+            REQUIRE(circle_with_hole.holes.size() == 1);
+            REQUIRE(circle_with_hole.holes.front().is_clockwise());
+        }
+    
+        WHEN("clipping a line") {
+            auto line = Polyline::new_scale({ { 152.742,288.086671142818 }, { 152.742,34.166466971035 } });    
+            Polylines intersection = intersection_pl({ line }, { circle_with_hole });
+            THEN("clipped to two pieces") {
+                REQUIRE(intersection.front().length() == Approx((Vec2d(152742000, 215178843) - Vec2d(152742000, 288086661)).norm()));
+                REQUIRE(intersection[1].length() == Approx((Vec2d(152742000, 35166477) - Vec2d(152742000, 108087507)).norm()));
+            }
+        }
+    }
 }
 
 template<e_ordering o = e_ordering::OFF, class P, class Tree> 
diff --git a/xs/xsp/Clipper.xsp b/xs/xsp/Clipper.xsp
index eae3afeff..18f8dec07 100644
--- a/xs/xsp/Clipper.xsp
+++ b/xs/xsp/Clipper.xsp
@@ -30,17 +30,6 @@ offset(polygons, delta, joinType = Slic3r::ClipperLib::jtMiter, miterLimit = 3)
     OUTPUT:
         RETVAL
 
-ExPolygons
-offset_ex(polygons, delta, joinType = Slic3r::ClipperLib::jtMiter, miterLimit = 3)
-    Polygons                polygons
-    const float             delta
-    Slic3r::ClipperLib::JoinType    joinType
-    double                  miterLimit
-    CODE:
-        RETVAL = offset_ex(polygons, delta, joinType, miterLimit);
-    OUTPUT:
-        RETVAL
-
 ExPolygons
 offset2_ex(polygons, delta1, delta2, joinType = Slic3r::ClipperLib::jtMiter, miterLimit = 3)
     Polygons                polygons
@@ -102,15 +91,6 @@ intersection_ex(subject, clip, safety_offset = false)
     OUTPUT:
         RETVAL
 
-Polylines
-intersection_pl(subject, clip)
-    Polylines                   subject
-    Polygons                    clip
-    CODE:
-        RETVAL = intersection_pl(subject, clip);
-    OUTPUT:
-        RETVAL
-
 Polygons
 union(subject, safety_offset = false)
     Polygons    subject
diff --git a/xs/xsp/Surface.xsp b/xs/xsp/Surface.xsp
index 49d988333..8804b851b 100644
--- a/xs/xsp/Surface.xsp
+++ b/xs/xsp/Surface.xsp
@@ -82,16 +82,6 @@ Surface::polygons()
     OUTPUT:
         RETVAL
 
-Surfaces
-Surface::offset(delta, joinType = ClipperLib::jtMiter, miterLimit = 3)
-    const float             delta
-    Slic3r::ClipperLib::JoinType    joinType
-    double                  miterLimit
-    CODE:
-        surfaces_append(RETVAL, offset_ex(THIS->expolygon, delta, joinType, miterLimit), *THIS);
-    OUTPUT:
-        RETVAL
-
 %}
 };
 

From d88e9634f511714dc03072164b4f340efda0331d Mon Sep 17 00:00:00 2001
From: Vojtech Bubnik <bubnikv@gmail.com>
Date: Thu, 28 Apr 2022 17:02:33 +0200
Subject: [PATCH 02/23] Little more reduce of Perl bindings

---
 lib/Slic3r.pm                  |  1 -
 lib/Slic3r/ExPolygon.pm        |  8 -------
 lib/Slic3r/Geometry/Clipper.pm |  5 ++---
 lib/Slic3r/Line.pm             |  6 ------
 lib/Slic3r/Print/Object.pm     |  3 ---
 t/perimeters.t                 |  2 +-
 xs/xsp/Clipper.xsp             | 39 ----------------------------------
 xs/xsp/Polygon.xsp             |  6 ------
 8 files changed, 3 insertions(+), 67 deletions(-)

diff --git a/lib/Slic3r.pm b/lib/Slic3r.pm
index 94f0b5658..d9274bea9 100644
--- a/lib/Slic3r.pm
+++ b/lib/Slic3r.pm
@@ -42,7 +42,6 @@ use Slic3r::ExtrusionLoop;
 use Slic3r::ExtrusionPath;
 use Slic3r::Flow;
 use Slic3r::GCode::Reader;
-use Slic3r::Geometry::Clipper;
 use Slic3r::Layer;
 use Slic3r::Line;
 use Slic3r::Model;
diff --git a/lib/Slic3r/ExPolygon.pm b/lib/Slic3r/ExPolygon.pm
index 6337cb9a1..4ab1bc8ab 100644
--- a/lib/Slic3r/ExPolygon.pm
+++ b/lib/Slic3r/ExPolygon.pm
@@ -4,14 +4,6 @@ use warnings;
 
 # an ExPolygon is a polygon with holes
 
-use List::Util qw(first);
-use Slic3r::Geometry::Clipper qw(union_ex diff_pl);
-
-sub offset {
-    my $self = shift;
-    return Slic3r::Geometry::Clipper::offset(\@$self, @_);
-}
-
 sub noncollapsing_offset_ex {
     my $self = shift;
     my ($distance, @params) = @_;
diff --git a/lib/Slic3r/Geometry/Clipper.pm b/lib/Slic3r/Geometry/Clipper.pm
index cfcb622fd..c1fa81d9f 100644
--- a/lib/Slic3r/Geometry/Clipper.pm
+++ b/lib/Slic3r/Geometry/Clipper.pm
@@ -7,8 +7,7 @@ our @ISA = qw(Exporter);
 our @EXPORT_OK = qw(
 	offset 
 	offset2_ex
-    diff_ex diff union_ex intersection_ex 
-    JT_ROUND JT_MITER JT_SQUARE 
-    intersection diff_pl union);
+    diff_ex diff union_ex 
+    union);
 
 1;
diff --git a/lib/Slic3r/Line.pm b/lib/Slic3r/Line.pm
index bf53520d2..c06f9a1fa 100644
--- a/lib/Slic3r/Line.pm
+++ b/lib/Slic3r/Line.pm
@@ -5,12 +5,6 @@ use warnings;
 # a line is a two-points line
 use parent 'Slic3r::Polyline';
 
-sub intersection {
-    my $self = shift;
-    my ($line, $require_crossing) = @_;
-    return Slic3r::Geometry::line_intersection($self, $line, $require_crossing);
-}
-
 sub grow {
     my $self = shift;
     return Slic3r::Polyline->new(@$self)->grow(@_);
diff --git a/lib/Slic3r/Print/Object.pm b/lib/Slic3r/Print/Object.pm
index f03a97ea3..64480e08f 100644
--- a/lib/Slic3r/Print/Object.pm
+++ b/lib/Slic3r/Print/Object.pm
@@ -5,9 +5,6 @@ use warnings;
 
 use List::Util qw(min max sum first);
 use Slic3r::Flow ':roles';
-use Slic3r::Geometry qw(scale epsilon);
-use Slic3r::Geometry::Clipper qw(diff diff_ex intersection intersection_ex union union_ex 
-    offset offset2_ex JT_MITER);
 use Slic3r::Print::State ':steps';
 use Slic3r::Surface ':types';
 
diff --git a/t/perimeters.t b/t/perimeters.t
index adc2a6cec..c4aef6e7e 100644
--- a/t/perimeters.t
+++ b/t/perimeters.t
@@ -14,7 +14,7 @@ use List::Util qw(first);
 use Slic3r;
 use Slic3r::Flow ':roles';
 use Slic3r::Geometry qw(PI scale unscale);
-use Slic3r::Geometry::Clipper qw(union_ex diff union offset);
+use Slic3r::Geometry::Clipper qw(union_ex diff);
 use Slic3r::Surface ':types';
 use Slic3r::Test;
 
diff --git a/xs/xsp/Clipper.xsp b/xs/xsp/Clipper.xsp
index 18f8dec07..5f9f35906 100644
--- a/xs/xsp/Clipper.xsp
+++ b/xs/xsp/Clipper.xsp
@@ -9,16 +9,6 @@
 
 %{
 
-IV
-_constant()
-  ALIAS:
-    JT_MITER        = jtMiter
-    JT_ROUND        = jtRound
-    JT_SQUARE       = jtSquare
-  CODE:
-    RETVAL = ix;
-  OUTPUT: RETVAL
-
 Polygons
 offset(polygons, delta, joinType = Slic3r::ClipperLib::jtMiter, miterLimit = 3)
     Polygons                polygons
@@ -62,35 +52,6 @@ diff_ex(subject, clip, safety_offset = false)
     OUTPUT:
         RETVAL
 
-Polylines
-diff_pl(subject, clip)
-    Polylines   subject
-    Polygons    clip
-    CODE:
-        RETVAL = diff_pl(subject, clip);
-    OUTPUT:
-        RETVAL
-
-Polygons
-intersection(subject, clip, safety_offset = false)
-    Polygons                    subject
-    Polygons                    clip
-    bool                        safety_offset
-    CODE:
-        RETVAL = intersection(subject, clip, safety_offset ? ApplySafetyOffset::Yes : ApplySafetyOffset::No);
-    OUTPUT:
-        RETVAL
-
-ExPolygons
-intersection_ex(subject, clip, safety_offset = false)
-    Polygons                    subject
-    Polygons                    clip
-    bool                        safety_offset
-    CODE:
-        RETVAL = intersection_ex(subject, clip, safety_offset ? ApplySafetyOffset::Yes : ApplySafetyOffset::No);
-    OUTPUT:
-        RETVAL
-
 Polygons
 union(subject, safety_offset = false)
     Polygons    subject
diff --git a/xs/xsp/Polygon.xsp b/xs/xsp/Polygon.xsp
index 6b6d52524..984eb16a9 100644
--- a/xs/xsp/Polygon.xsp
+++ b/xs/xsp/Polygon.xsp
@@ -41,12 +41,6 @@
     Clone<BoundingBox> bounding_box();
     Clone<Point> point_projection(Point* point)
         %code{% RETVAL = THIS->point_projection(*point); %};
-    Clone<Point> intersection(Line* line)
-        %code{%
-            Point p;
-            (void)THIS->intersection(*line, &p);
-            RETVAL = p;
-        %};
     Clone<Point> first_intersection(Line* line)
         %code{%
             Point p;

From 33b2478b6907c7e888cc11ecf6dfbdf818437993 Mon Sep 17 00:00:00 2001
From: Vojtech Bubnik <bubnikv@gmail.com>
Date: Mon, 2 May 2022 14:34:39 +0200
Subject: [PATCH 03/23] Ported Infill unit tests from Perl to C++.

---
 src/libslic3r/Config.hpp                  |   2 +
 src/libslic3r/ExtrusionEntity.hpp         |   4 +-
 src/libslic3r/GCodeReader.hpp             |   4 +
 t/fill.t                                  | 318 -------------
 tests/fff_print/test_data.cpp             |  13 +
 tests/fff_print/test_extrusion_entity.cpp |  58 +++
 tests/fff_print/test_fill.cpp             | 516 +++++++++++++---------
 xs/CMakeLists.txt                         |   3 -
 xs/lib/Slic3r/XS.pm                       |  24 -
 xs/main.xs.in                             |   1 -
 xs/src/perlglue.cpp                       |   2 -
 xs/xsp/ExtrusionSimulator.xsp             |  50 ---
 xs/xsp/Filler.xsp                         |  65 ---
 xs/xsp/GCodeSender.xsp                    |  24 -
 xs/xsp/my.map                             |   8 -
 xs/xsp/typemap.xspt                       |   6 -
 16 files changed, 387 insertions(+), 711 deletions(-)
 delete mode 100644 t/fill.t
 delete mode 100644 xs/xsp/ExtrusionSimulator.xsp
 delete mode 100644 xs/xsp/Filler.xsp
 delete mode 100644 xs/xsp/GCodeSender.xsp

diff --git a/src/libslic3r/Config.hpp b/src/libslic3r/Config.hpp
index a087dd854..bfd307de3 100644
--- a/src/libslic3r/Config.hpp
+++ b/src/libslic3r/Config.hpp
@@ -10,6 +10,7 @@
 #include <iostream>
 #include <stdexcept>
 #include <string>
+#include <string_view>
 #include <vector>
 #include "libslic3r.h"
 #include "clonable_ptr.hpp"
@@ -1989,6 +1990,7 @@ public:
     struct SetDeserializeItem {
     	SetDeserializeItem(const char *opt_key, const char *opt_value, bool append = false) : opt_key(opt_key), opt_value(opt_value), append(append) {}
     	SetDeserializeItem(const std::string &opt_key, const std::string &opt_value, bool append = false) : opt_key(opt_key), opt_value(opt_value), append(append) {}
+        SetDeserializeItem(const std::string &opt_key, const std::string_view opt_value, bool append = false) : opt_key(opt_key), opt_value(opt_value), append(append) {}
     	SetDeserializeItem(const char *opt_key, const bool value, bool append = false) : opt_key(opt_key), opt_value(value ? "1" : "0"), append(append) {}
     	SetDeserializeItem(const std::string &opt_key, const bool value, bool append = false) : opt_key(opt_key), opt_value(value ? "1" : "0"), append(append) {}
     	SetDeserializeItem(const char *opt_key, const int value, bool append = false) : opt_key(opt_key), opt_value(std::to_string(value)), append(append) {}
diff --git a/src/libslic3r/ExtrusionEntity.hpp b/src/libslic3r/ExtrusionEntity.hpp
index 5e6a51d20..2e9e46789 100644
--- a/src/libslic3r/ExtrusionEntity.hpp
+++ b/src/libslic3r/ExtrusionEntity.hpp
@@ -324,10 +324,10 @@ inline void extrusion_paths_append(ExtrusionPaths &dst, Polylines &&polylines, E
     polylines.clear();
 }
 
-inline void extrusion_entities_append_paths(ExtrusionEntitiesPtr &dst, Polylines &polylines, ExtrusionRole role, double mm3_per_mm, float width, float height)
+inline void extrusion_entities_append_paths(ExtrusionEntitiesPtr &dst, const Polylines &polylines, ExtrusionRole role, double mm3_per_mm, float width, float height)
 {
     dst.reserve(dst.size() + polylines.size());
-    for (Polyline &polyline : polylines)
+    for (const Polyline &polyline : polylines)
         if (polyline.is_valid()) {
             ExtrusionPath *extrusion_path = new ExtrusionPath(role, mm3_per_mm, width, height);
             dst.push_back(extrusion_path);
diff --git a/src/libslic3r/GCodeReader.hpp b/src/libslic3r/GCodeReader.hpp
index 0ab268139..40a099229 100644
--- a/src/libslic3r/GCodeReader.hpp
+++ b/src/libslic3r/GCodeReader.hpp
@@ -35,6 +35,8 @@ public:
         float new_Z(const GCodeReader &reader) const { return this->has(Z) ? this->z() : reader.z(); }
         float new_E(const GCodeReader &reader) const { return this->has(E) ? this->e() : reader.e(); }
         float new_F(const GCodeReader &reader) const { return this->has(F) ? this->f() : reader.f(); }
+        Point new_XY_scaled(const GCodeReader &reader) const 
+            { return Point::new_scale(this->new_X(reader), this->new_Y(reader)); }
         float dist_X(const GCodeReader &reader) const { return this->has(X) ? (this->x() - reader.x()) : 0; }
         float dist_Y(const GCodeReader &reader) const { return this->has(Y) ? (this->y() - reader.y()) : 0; }
         float dist_Z(const GCodeReader &reader) const { return this->has(Z) ? (this->z() - reader.z()) : 0; }
@@ -134,6 +136,8 @@ public:
     float  e() const { return m_position[E]; }
     float& f()       { return m_position[F]; }
     float  f() const { return m_position[F]; }
+    Point  xy_scaled() const { return Point::new_scale(this->x(), this->y()); }
+
 
     // Returns 0 for gcfNoExtrusion.
     char   extrusion_axis() const { return m_extrusion_axis; }
diff --git a/t/fill.t b/t/fill.t
deleted file mode 100644
index 88cc35801..000000000
--- a/t/fill.t
+++ /dev/null
@@ -1,318 +0,0 @@
-use Test::More;
-use strict;
-use warnings;
-
-#plan tests => 43;
-# Test of a 100% coverage is off.
-plan tests => 19;
-
-BEGIN {
-    use FindBin;
-    use lib "$FindBin::Bin/../lib";
-    use local::lib "$FindBin::Bin/../local-lib";
-}
-
-use List::Util qw(first sum);
-use Slic3r;
-use Slic3r::Geometry qw(X Y scale unscale convex_hull);
-use Slic3r::Geometry::Clipper qw(union diff diff_ex offset offset2_ex);
-use Slic3r::Surface qw(:types);
-use Slic3r::Test;
-
-sub scale_points (@) { map [scale $_->[X], scale $_->[Y]], @_ }
-
-{
-    my $expolygon = Slic3r::ExPolygon->new([ scale_points [0,0], [50,0], [50,50], [0,50] ]);
-    my $filler = Slic3r::Filler->new_from_type('rectilinear');
-    $filler->set_bounding_box($expolygon->bounding_box);
-    $filler->set_angle(0);
-    my $surface = Slic3r::Surface->new(
-        surface_type    => S_TYPE_TOP,
-        expolygon       => $expolygon,
-    );
-    my $flow = Slic3r::Flow->new(
-        width           => 0.69,
-        height          => 0.4,
-        nozzle_diameter => 0.50,
-    );
-    $filler->set_spacing($flow->spacing);
-    foreach my $angle (0, 45) {
-        $surface->expolygon->rotate(Slic3r::Geometry::deg2rad($angle), [0,0]);
-        my $paths = $filler->fill_surface($surface, layer_height => 0.4, density => 0.4);
-        is scalar @$paths, 1, 'one continuous path';
-    }
-}
-
-SKIP:
-{
-    skip "The FillRectilinear2 does not fill the surface completely", 1;
-
-    my $test = sub {
-        my ($expolygon, $flow_spacing, $angle, $density) = @_;
-        
-        my $filler = Slic3r::Filler->new_from_type('rectilinear');
-        $filler->set_bounding_box($expolygon->bounding_box);
-        $filler->set_angle($angle // 0);
-        # Adjust line spacing to fill the region.
-        $filler->set_dont_adjust(0);
-        $filler->set_link_max_length(scale(1.2*$flow_spacing));
-        my $surface = Slic3r::Surface->new(
-            surface_type    => S_TYPE_BOTTOM,
-            expolygon       => $expolygon,
-        );
-        my $flow = Slic3r::Flow->new(
-            width           => $flow_spacing,
-            height          => 0.4,
-            nozzle_diameter => $flow_spacing,
-        );
-        $filler->set_spacing($flow->spacing);
-        my $paths = $filler->fill_surface(
-            $surface,
-            layer_height    => $flow->height,
-            density         => $density // 1,
-        );
-        
-        # check whether any part was left uncovered
-        my @grown_paths = map @{Slic3r::Polyline->new(@$_)->grow(scale $filler->spacing/2)}, @$paths;
-        my $uncovered = diff_ex([ @$expolygon ], [ @grown_paths ], 1);
-        
-        # ignore very small dots
-        my $uncovered_filtered = [ grep $_->area > (scale $flow_spacing)**2, @$uncovered ];
-
-        is scalar(@$uncovered_filtered), 0, 'solid surface is fully filled';
-        
-        if (0 && @$uncovered_filtered) {
-            require "Slic3r/SVG.pm";
-            Slic3r::SVG::output("uncovered.svg", 
-                no_arrows       => 1,
-                expolygons      => [ $expolygon ],
-                blue_expolygons => [ @$uncovered ],
-                red_expolygons  => [ @$uncovered_filtered ],
-                polylines       => [ @$paths ],
-            );
-            exit;
-        }
-    };
-    
-    my $expolygon = Slic3r::ExPolygon->new([
-        [6883102, 9598327.01296997],
-        [6883102, 20327272.01297],
-        [3116896, 20327272.01297],
-        [3116896, 9598327.01296997],
-    ]);
-    $test->($expolygon, 0.55);
-    
-    for (1..20) {
-        $expolygon->scale(1.05);
-        $test->($expolygon, 0.55);
-    }
-    
-    $expolygon = Slic3r::ExPolygon->new(
-        [[59515297,5422499],[59531249,5578697],[59695801,6123186],[59965713,6630228],[60328214,7070685],[60773285,7434379],[61274561,7702115],[61819378,7866770],[62390306,7924789],[62958700,7866744],[63503012,7702244],[64007365,7434357],[64449960,7070398],[64809327,6634999],[65082143,6123325],[65245005,5584454],[65266967,5422499],[66267307,5422499],[66269190,8310081],[66275379,17810072],[66277259,20697500],[65267237,20697500],[65245004,20533538],[65082082,19994444],[64811462,19488579],[64450624,19048208],[64012101,18686514],[63503122,18415781],[62959151,18251378],[62453416,18198442],[62390147,18197355],[62200087,18200576],[61813519,18252990],[61274433,18415918],[60768598,18686517],[60327567,19047892],[59963609,19493297],[59695865,19994587],[59531222,20539379],[59515153,20697500],[58502480,20697500],[58502480,5422499]]
-    );
-    $test->($expolygon, 0.524341649025257);
-    
-    $expolygon = Slic3r::ExPolygon->new([ scale_points [0,0], [98,0], [98,10], [0,10] ]);
-    $test->($expolygon, 0.5, 45, 0.99);  # non-solid infill
-}
-
-{
-    my $collection = Slic3r::Polyline::Collection->new(
-        Slic3r::Polyline->new([0,15], [0,18], [0,20]),
-        Slic3r::Polyline->new([0,10], [0,8], [0,5]),
-    );
-    is_deeply
-        [ map $_->[Y], map @$_, @{$collection->chained_path_from(Slic3r::Point->new(0,30), 0)} ],
-        [20, 18, 15, 10, 8, 5],
-        'chained path';
-}
-
-{
-    my $collection = Slic3r::Polyline::Collection->new(
-        Slic3r::Polyline->new([4,0], [10,0], [15,0]),
-        Slic3r::Polyline->new([10,5], [15,5], [20,5]),
-    );
-    is_deeply
-        [ map $_->[X], map @$_, @{$collection->chained_path_from(Slic3r::Point->new(30,0), 0)} ],
-        [reverse 4, 10, 15, 10, 15, 20],
-        'chained path';
-}
-
-{
-    my $collection = Slic3r::ExtrusionPath::Collection->new(
-        map Slic3r::ExtrusionPath->new(polyline => $_, role => 0, mm3_per_mm => 1),
-            Slic3r::Polyline->new([0,15], [0,18], [0,20]),
-            Slic3r::Polyline->new([0,10], [0,8], [0,5]),
-    );
-    is_deeply
-        [ map $_->[Y], map @{$_->polyline}, @{$collection->chained_path_from(Slic3r::Point->new(0,30), 0)} ],
-        [20, 18, 15, 10, 8, 5],
-        'chained path';
-}
-
-{
-    my $collection = Slic3r::ExtrusionPath::Collection->new(
-        map Slic3r::ExtrusionPath->new(polyline => $_, role => 0, mm3_per_mm => 1),
-            Slic3r::Polyline->new([15,0], [10,0], [4,0]),
-            Slic3r::Polyline->new([10,5], [15,5], [20,5]),
-    );
-    is_deeply
-        [ map $_->[X], map @{$_->polyline}, @{$collection->chained_path_from(Slic3r::Point->new(30,0), 0)} ],
-        [reverse 4, 10, 15, 10, 15, 20],
-        'chained path';
-}
-
-for my $pattern (qw(rectilinear honeycomb hilbertcurve concentric)) {
-    my $config = Slic3r::Config::new_from_defaults;
-    $config->set('nozzle_diameter', [0.4,0.4,0.4,0.4]);
-    $config->set('fill_pattern', $pattern);
-    $config->set('top_fill_pattern', $pattern);
-    $config->set('bottom_fill_pattern', $pattern);
-    $config->set('perimeters', 1);
-    $config->set('skirts', 0);
-    $config->set('fill_density', 20);
-    $config->set('layer_height', 0.05);
-    $config->set('perimeter_extruder', 1);
-    $config->set('infill_extruder', 2);
-    my $print = Slic3r::Test::init_print('20mm_cube', config => $config, scale => 2);
-    ok my $gcode = Slic3r::Test::gcode($print), "successful $pattern infill generation";
-    my $tool = undef;
-    my @perimeter_points = my @infill_points = ();
-    Slic3r::GCode::Reader->new->parse($gcode, sub {
-        my ($self, $cmd, $args, $info) = @_;
-        
-        if ($cmd =~ /^T(\d+)/) {
-            $tool = $1;
-        } elsif ($cmd eq 'G1' && $info->{extruding} && $info->{dist_XY} > 0) {
-            if ($tool == $config->perimeter_extruder-1) {
-                push @perimeter_points, Slic3r::Point->new_scale($args->{X}, $args->{Y});
-            } elsif ($tool == $config->infill_extruder-1) {
-                push @infill_points, Slic3r::Point->new_scale($args->{X}, $args->{Y});
-            }
-        }
-    });
-    my $convex_hull = convex_hull(\@perimeter_points);
-    ok !(defined first { !$convex_hull->contains_point($_) } @infill_points), "infill does not exceed perimeters ($pattern)";
-}
-
-{
-    my $config = Slic3r::Config::new_from_defaults;
-    $config->set('nozzle_diameter', [0.4,0.4,0.4,0.4]);
-    $config->set('infill_only_where_needed', 1);
-    $config->set('bottom_solid_layers', 0);
-    $config->set('infill_extruder', 2);
-    $config->set('infill_extrusion_width', 0.5);
-    $config->set('wipe_into_infill', 0);
-    $config->set('fill_density', 40);
-    $config->set('cooling', [ 0 ]);             # for preventing speeds from being altered
-    $config->set('first_layer_speed', '100%');  # for preventing speeds from being altered
-    
-    my $test = sub {
-        my $print = Slic3r::Test::init_print('pyramid', config => $config);
-        
-        my $tool = undef;
-        my @infill_extrusions = ();  # array of polylines
-        Slic3r::GCode::Reader->new->parse(Slic3r::Test::gcode($print), sub {
-            my ($self, $cmd, $args, $info) = @_;
-            
-            if ($cmd =~ /^T(\d+)/) {
-                $tool = $1;
-            } elsif ($cmd eq 'G1' && $info->{extruding} && $info->{dist_XY} > 0) {
-                if ($tool == $config->infill_extruder-1) {
-                    push @infill_extrusions, Slic3r::Line->new_scale(
-                        [ $self->X, $self->Y ],
-                        [ $info->{new_X}, $info->{new_Y} ],
-                    );
-                }
-            }
-        });
-        return 0 if !@infill_extrusions;  # prevent calling convex_hull() with no points
-        
-        my $convex_hull = convex_hull([ map $_->pp, map @$_, @infill_extrusions ]);
-        return unscale unscale sum(map $_->area, @{offset([$convex_hull], scale(+$config->infill_extrusion_width/2))});
-    };
-    
-    my $tolerance = 5;  # mm^2
-    
-    $config->set('solid_infill_below_area', 0);
-    ok $test->() < $tolerance,
-        'no infill is generated when using infill_only_where_needed on a pyramid';
-    
-    $config->set('solid_infill_below_area', 70);
-    ok abs($test->() - $config->solid_infill_below_area) < $tolerance,
-        'infill is only generated under the forced solid shells';
-}
-
-{
-    my $config = Slic3r::Config::new_from_defaults;
-    $config->set('skirts', 0);
-    $config->set('perimeters', 1);
-    $config->set('fill_density', 0);
-    $config->set('top_solid_layers', 0);
-    $config->set('bottom_solid_layers', 0);
-    $config->set('solid_infill_below_area', 20000000);
-    $config->set('solid_infill_every_layers', 2);
-    $config->set('perimeter_speed', 99);
-    $config->set('external_perimeter_speed', 99);
-    $config->set('cooling', [ 0 ]);
-    $config->set('first_layer_speed', '100%');
-    
-    my $print = Slic3r::Test::init_print('20mm_cube', config => $config);
-    my %layers_with_extrusion = ();
-    Slic3r::GCode::Reader->new->parse(Slic3r::Test::gcode($print), sub {
-        my ($self, $cmd, $args, $info) = @_;
-        
-        if ($cmd eq 'G1' && $info->{dist_XY} > 0 && $info->{extruding}) {
-            if (($args->{F} // $self->F) != $config->perimeter_speed*60) {
-                $layers_with_extrusion{$self->Z} = ($args->{F} // $self->F);
-            }
-        }
-    });
-    
-    ok !%layers_with_extrusion,
-        "solid_infill_below_area and solid_infill_every_layers are ignored when fill_density is 0";
-}
-
-{
-    my $config = Slic3r::Config::new_from_defaults;
-    $config->set('skirts', 0);
-    $config->set('perimeters', 3);
-    $config->set('fill_density', 0);
-    $config->set('layer_height', 0.2);
-    $config->set('first_layer_height', 0.2);
-    $config->set('nozzle_diameter', [0.35,0.35,0.35,0.35]);
-    $config->set('infill_extruder', 2);
-    $config->set('solid_infill_extruder', 2);
-    $config->set('infill_extrusion_width', 0.52);
-    $config->set('solid_infill_extrusion_width', 0.52);
-    $config->set('first_layer_extrusion_width', 0);
-    
-    my $print = Slic3r::Test::init_print('A', config => $config);
-    my %infill = ();  # Z => [ Line, Line ... ]
-    my $tool = undef;
-    Slic3r::GCode::Reader->new->parse(Slic3r::Test::gcode($print), sub {
-        my ($self, $cmd, $args, $info) = @_;
-        
-        if ($cmd =~ /^T(\d+)/) {
-            $tool = $1;
-        } elsif ($cmd eq 'G1' && $info->{extruding} && $info->{dist_XY} > 0) {
-            if ($tool == $config->infill_extruder-1) {
-                my $z = 1 * $self->Z;
-                $infill{$z} ||= [];
-                push @{$infill{$z}}, Slic3r::Line->new_scale(
-                    [ $self->X, $self->Y ],
-                    [ $info->{new_X}, $info->{new_Y} ],
-                );
-            }
-        }
-    });
-    my $grow_d = scale($config->infill_extrusion_width)/2;
-    my $layer0_infill = union([ map @{$_->grow($grow_d)}, @{ $infill{0.2} } ]);
-    my $layer1_infill = union([ map @{$_->grow($grow_d)}, @{ $infill{0.4} } ]);
-    my $diff = diff($layer0_infill, $layer1_infill);
-    $diff = offset2_ex($diff, -$grow_d, +$grow_d);
-    $diff = [ grep { $_->area > 2*(($grow_d*2)**2) } @$diff ];
-    is scalar(@$diff), 0, 'no missing parts in solid shell when fill_density is 0';
-}
-
-__END__
diff --git a/tests/fff_print/test_data.cpp b/tests/fff_print/test_data.cpp
index f7077007d..6be45d238 100644
--- a/tests/fff_print/test_data.cpp
+++ b/tests/fff_print/test_data.cpp
@@ -187,6 +187,19 @@ TriangleMesh mesh(TestMesh m)
     return mesh;
 }
 
+TriangleMesh mesh(TestMesh min, Vec3d translate, Vec3d scale)
+{
+    TriangleMesh m = mesh(min);
+    m.translate(translate.cast<float>());
+    m.scale(scale.cast<float>());
+    return m;
+}
+
+TriangleMesh mesh(TestMesh m, Vec3d translate, double scale)
+{
+    return mesh(m, translate, Vec3d(scale, scale, scale));
+}
+
 static bool verbose_gcode() 
 {
     const char *v = std::getenv("SLIC3R_TESTS_GCODE");
diff --git a/tests/fff_print/test_extrusion_entity.cpp b/tests/fff_print/test_extrusion_entity.cpp
index 9bef52a5c..7e0ca822f 100644
--- a/tests/fff_print/test_extrusion_entity.cpp
+++ b/tests/fff_print/test_extrusion_entity.cpp
@@ -5,6 +5,7 @@
 #include "libslic3r/ExtrusionEntityCollection.hpp"
 #include "libslic3r/ExtrusionEntity.hpp"
 #include "libslic3r/Point.hpp"
+#include "libslic3r/ShortestPath.hpp"
 #include "libslic3r/libslic3r.h"
 
 #include "test_data.hpp"
@@ -83,3 +84,60 @@ SCENARIO("ExtrusionEntityCollection: Polygon flattening", "[ExtrusionEntity]") {
         }
     }
 }
+
+TEST_CASE("ExtrusionEntityCollection: Chained path", "[ExtrusionEntity]") {
+    struct Test {
+        Polylines unchained;
+        Polylines chained;
+        Point     initial_point;
+    };
+    std::vector<Test> tests { 
+        {
+            { 
+                { {0,15}, {0,18}, {0,20} },
+                { {0,10}, {0,8}, {0,5} }
+            },
+            {
+                { {0,20}, {0,18}, {0,15} },
+                { {0,10}, {0,8}, {0,5} }
+            },
+            { 0,30 }
+        },
+        {
+            { 
+                { {4,0}, {10,0}, {15,0} },
+                { {10,5}, {15,5}, {20,5} }
+            },
+            {
+                { {20,5}, {15,5}, {10,5} },
+                { {15,0}, {10,0}, {4,0} }
+            },
+            { 30,0 }
+        },
+        {
+            { 
+                { {15,0}, {10,0}, {4,0} },
+                { {10,5}, {15,5}, {20,5} }
+            },
+            {
+                { {20,5}, {15,5}, {10,5} },
+                { {15,0}, {10,0}, {4,0} }
+            },
+            { 30,0 }
+        },
+    };
+    for (const Test &test : tests) {
+        Polylines chained = chain_polylines(test.unchained, &test.initial_point);
+        REQUIRE(chained == test.chained);
+        ExtrusionEntityCollection unchained_extrusions;
+        extrusion_entities_append_paths(unchained_extrusions.entities, test.unchained,
+            erInternalInfill, 0., 0.4f, 0.3f);
+        ExtrusionEntityCollection chained_extrusions = unchained_extrusions.chained_path_from(test.initial_point);
+        REQUIRE(chained_extrusions.entities.size() == test.chained.size());
+        for (size_t i = 0; i < chained_extrusions.entities.size(); ++ i) {
+            const Points &p1 = test.chained[i].points;
+            const Points &p2 = dynamic_cast<const ExtrusionPath*>(chained_extrusions.entities[i])->polyline.points;
+            REQUIRE(p1 == p2);
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/fff_print/test_fill.cpp b/tests/fff_print/test_fill.cpp
index 8e311282e..9f30bf8be 100644
--- a/tests/fff_print/test_fill.cpp
+++ b/tests/fff_print/test_fill.cpp
@@ -7,6 +7,7 @@
 #include "libslic3r/Fill/Fill.hpp"
 #include "libslic3r/Flow.hpp"
 #include "libslic3r/Geometry.hpp"
+#include "libslic3r/Geometry/ConvexHull.hpp"
 #include "libslic3r/Print.hpp"
 #include "libslic3r/SVG.hpp"
 #include "libslic3r/libslic3r.h"
@@ -14,6 +15,7 @@
 #include "test_data.hpp"
 
 using namespace Slic3r;
+using namespace std::literals;
 
 bool test_if_solid_surface_filled(const ExPolygon& expolygon, double flow_spacing, double angle = 0, double density = 1.0);
 
@@ -43,7 +45,7 @@ TEST_CASE("Fill: Pattern Path Length", "[Fill]") {
     SECTION("Square") {
         Slic3r::Points test_set;
         test_set.reserve(4);
-        std::vector<Vec2d> points {Vec2d(0,0), Vec2d(100,0), Vec2d(100,100), Vec2d(0,100)};
+        std::vector<Vec2d> points { {0,0}, {100,0}, {100,100}, {0,100} };
         for (size_t i = 0; i < 4; ++i) {
             std::transform(points.cbegin()+i, points.cend(),   std::back_inserter(test_set), [] (const Vec2d& a) -> Point { return Point::new_scale(a.x(), a.y()); } ); 
             std::transform(points.cbegin(), points.cbegin()+i, std::back_inserter(test_set), [] (const Vec2d& a) -> Point { return Point::new_scale(a.x(), a.y()); } );
@@ -118,24 +120,26 @@ TEST_CASE("Fill: Pattern Path Length", "[Fill]") {
         REQUIRE(std::abs(paths[0].length() - static_cast<double>(scale_(3*100 + 2*50))) - SCALED_EPSILON > 0); // path has expected length
     }
 
-    SECTION("Rotated Square") {
-        Slic3r::Points square { Point::new_scale(0,0), Point::new_scale(50,0), Point::new_scale(50,50), Point::new_scale(0,50)};
-        Slic3r::ExPolygon expolygon(square);
+    SECTION("Rotated Square produces one continuous path") {
+        Slic3r::ExPolygon expolygon(Polygon::new_scale({ {0, 0}, {50, 0}, {50, 50}, {0, 50} }));
         std::unique_ptr<Slic3r::Fill> filler(Slic3r::Fill::new_from_type("rectilinear"));
-		filler->bounding_box = get_extents(expolygon.contour);
+		filler->bounding_box = get_extents(expolygon);
         filler->angle = 0;
         
         Surface surface(stTop, expolygon);
-        auto flow = Slic3r::Flow(0.69f, 0.4f, 0.50f);
+        // width, height, nozzle_dmr
+        auto flow = Slic3r::Flow(0.69f, 0.4f, 0.5f);
 
 		FillParams fill_params;
-		fill_params.density = 1.0;
-		filler->spacing = flow.spacing();
-
-        for (auto angle : { 0.0, 45.0}) {
-            surface.expolygon.rotate(angle, Point(0,0));
-            Polylines paths = filler->fill_surface(&surface, fill_params);
-            REQUIRE(paths.size() == 1);
+        for (auto density : { 0.4, 1.0 }) {
+            fill_params.density = density;
+            filler->spacing = flow.spacing();
+            for (auto angle : { 0.0, 45.0}) {
+                surface.expolygon.rotate(angle, Point(0,0));
+                Polylines paths = filler->fill_surface(&surface, fill_params);
+                // one continuous path
+                REQUIRE(paths.size() == 1);
+            }
         }
     }
 
@@ -190,202 +194,226 @@ TEST_CASE("Fill: Pattern Path Length", "[Fill]") {
     }
 }
 
+SCENARIO("Infill does not exceed perimeters", "[Fill]") 
+{
+    auto test = [](const std::string_view pattern) {
+        DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
+        config.set_deserialize_strict({
+            { "nozzle_diameter",        "0.4, 0.4, 0.4, 0.4" },
+            { "fill_pattern",           pattern },
+            { "top_fill_pattern",       pattern },
+            { "bottom_fill_pattern",    pattern },
+            { "perimeters",             1 },
+            { "skirts",                 0 },
+            { "fill_density",           0.2 },
+            { "layer_height",           0.05 },
+            { "perimeter_extruder",     1 },
+            { "infill_extruder",        2 }
+        });
+        
+        WHEN("40mm cube sliced") {
+            std::string gcode = Slic3r::Test::slice({ mesh(Slic3r::Test::TestMesh::cube_20x20x20, Vec3d::Zero(), 2.0) }, config);
+            THEN("gcode not empty") {
+                REQUIRE(! gcode.empty());
+            }
+            THEN("infill does not exceed perimeters") {
+                GCodeReader parser;
+                const int   perimeter_extruder = config.opt_int("perimeter_extruder");
+                const int   infill_extruder    = config.opt_int("infill_extruder");
+                int         tool = -1;
+                Points      perimeter_points;
+                Points      infill_points;
+                parser.parse_buffer(gcode, [&tool, &perimeter_points, &infill_points, perimeter_extruder, infill_extruder]
+                    (Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line)
+                {
+                    // if the command is a T command, set the the current tool
+                    if (boost::starts_with(line.cmd(), "T")) {
+                        tool = atoi(line.cmd().data() + 1) + 1;
+                    } else if (line.cmd() == "G1" && line.extruding(self) && line.dist_XY(self) > 0) {
+                        if (tool == perimeter_extruder)
+                            perimeter_points.emplace_back(line.new_XY_scaled(self));
+                        else if (tool == infill_extruder)
+                            infill_points.emplace_back(line.new_XY_scaled(self));
+                    }
+                });
+                auto convex_hull = Geometry::convex_hull(perimeter_points);
+                int num_inside = std::count_if(infill_points.begin(), infill_points.end(), [&convex_hull](const Point &pt){ return convex_hull.contains(pt); });
+                REQUIRE(num_inside == infill_points.size());
+            }
+        }
+    };
+
+    GIVEN("Rectilinear") { test("rectilinear"sv); }
+    GIVEN("Honeycomb") { test("honeycomb"sv); }
+    GIVEN("HilbertCurve") { test("hilbertcurve"sv); }
+    GIVEN("Concentric") { test("concentric"sv); }
+}
+
+SCENARIO("Infill only where needed", "[Fill]")
+{
+    DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
+    config.set_deserialize_strict({
+        { "nozzle_diameter",                "0.4, 0.4, 0.4, 0.4" },
+        { "infill_only_where_needed",       true },
+        { "bottom_solid_layers",            0 },
+        { "infill_extruder",                2 },
+        { "infill_extrusion_width",         0.5 },
+        { "wipe_into_infill",               false },
+        { "fill_density",                   0.4 },
+        // for preventing speeds from being altered
+        { "cooling",                        "0, 0, 0, 0" },
+        // for preventing speeds from being altered
+        { "first_layer_speed",              "100%" }
+    });
+
+    auto test = [&config]() -> double {
+        std::string gcode = Slic3r::Test::slice({ Slic3r::Test::TestMesh::pyramid }, config);
+        THEN("gcode not empty") {
+            REQUIRE(! gcode.empty());
+        }
+
+        GCodeReader parser;
+        int         tool = -1;
+        const int   infill_extruder = config.opt_int("infill_extruder");
+        Points      infill_points;
+        parser.parse_buffer(gcode, [&tool, &infill_points, infill_extruder](Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line)
+        {
+            // if the command is a T command, set the the current tool
+            if (boost::starts_with(line.cmd(), "T")) {
+                tool = atoi(line.cmd().data() + 1) + 1;
+            } else if (line.cmd() == "G1" && line.extruding(self) && line.dist_XY(self) > 0) {
+                if (tool == infill_extruder) {
+                    infill_points.emplace_back(self.xy_scaled());
+                    infill_points.emplace_back(line.new_XY_scaled(self));
+                }
+            }
+        });
+        // prevent calling convex_hull() with no points
+        THEN("infill not empty") {
+            REQUIRE(! infill_points.empty());
+        }
+
+        auto opt_width = config.opt<ConfigOptionFloatOrPercent>("infill_extrusion_width");
+        REQUIRE(! opt_width->percent);
+        Polygons convex_hull = expand(Geometry::convex_hull(infill_points), scaled<float>(opt_width->value / 2));
+        return SCALING_FACTOR * SCALING_FACTOR * std::accumulate(convex_hull.begin(), convex_hull.end(), 0., [](double acc, const Polygon &poly){ return acc + poly.area(); });
+    };
+
+    double tolerance = 5; // mm^2
+    
+    GIVEN("solid_infill_below_area == 0") {
+        config.opt_float("solid_infill_below_area") = 0;
+        WHEN("pyramid is sliced ") {
+            auto area = test();
+            THEN("no infill is generated when using infill_only_where_needed on a pyramid") {
+                REQUIRE(area < tolerance);
+            }
+        }
+    }
+    GIVEN("solid_infill_below_area == 70") {
+        config.opt_float("solid_infill_below_area") = 70;
+        WHEN("pyramid is sliced ") {
+            auto area = test();
+            THEN("infill is only generated under the forced solid shells") {
+                REQUIRE(std::abs(area - 70) < tolerance);
+            }
+        }
+    }
+}
+
+SCENARIO("Infill density zero", "[Fill]")
+{
+    WHEN("20mm cube is sliced") {
+        DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
+        config.set_deserialize_strict({
+            { "skirts",                         0 },
+            { "perimeters",                     1 },
+            { "fill_density",                   0 },
+            { "top_solid_layers",               0 },
+            { "bottom_solid_layers",            0 },
+            { "solid_infill_below_area",        20000000 },
+            { "solid_infill_every_layers",      2 },
+            { "perimeter_speed",                99 },
+            { "external_perimeter_speed",       99 },
+            { "cooling",                        "0" },
+            { "first_layer_speed",              "100%" }
+        });
+
+        std::string gcode = Slic3r::Test::slice({ Slic3r::Test::TestMesh::cube_20x20x20 }, config);
+        THEN("gcode not empty") {
+            REQUIRE(! gcode.empty());
+        }
+
+        THEN("solid_infill_below_area and solid_infill_every_layers are ignored when fill_density is 0") {
+            GCodeReader  parser;
+            const double perimeter_speed = config.opt_float("perimeter_speed");
+            std::map<double, double> layers_with_extrusion;
+            parser.parse_buffer(gcode, [&layers_with_extrusion, perimeter_speed](Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line) {
+                if (line.cmd() == "G1" && line.extruding(self) && line.dist_XY(self) > 0) {
+                    double f = line.new_F(self);
+                    if (std::abs(f - perimeter_speed * 60.) > 0.01)
+                        // It is a perimeter.
+                        layers_with_extrusion[self.z()] = f;
+                }
+            });
+            REQUIRE(layers_with_extrusion.empty());
+        }
+    }
+
+    WHEN("A is sliced") {
+        DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
+        config.set_deserialize_strict({
+            { "skirts",                         0 },
+            { "perimeters",                     3 },
+            { "fill_density",                   0 },
+            { "layer_height",                   0.2 },
+            { "first_layer_height",             0.2 },
+            { "nozzle_diameter",                "0.35,0.35,0.35,0.35" },
+            { "infill_extruder",                2 },
+            { "solid_infill_extruder",          2 },
+            { "infill_extrusion_width",         0.52 },
+            { "solid_infill_extrusion_width",   0.52 },
+            { "first_layer_extrusion_width",    0 }
+        });
+
+        std::string gcode = Slic3r::Test::slice({ Slic3r::Test::TestMesh::A }, config);
+        THEN("gcode not empty") {
+            REQUIRE(! gcode.empty());
+        }
+
+        THEN("no missing parts in solid shell when fill_density is 0") {
+            GCodeReader  parser;
+            int          tool = -1;
+            const int    infill_extruder = config.opt_int("infill_extruder");
+            std::map<coord_t, Lines> infill;
+            parser.parse_buffer(gcode, [&tool, &infill, infill_extruder](Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line) {
+                if (boost::starts_with(line.cmd(), "T")) {
+                    tool = atoi(line.cmd().data() + 1) + 1;
+                } else if (line.cmd() == "G1" && line.extruding(self) && line.dist_XY(self) > 0) {
+                    if (tool == infill_extruder)
+                        infill[scaled<coord_t>(self.z())].emplace_back(self.xy_scaled(), line.new_XY_scaled(self));
+                }
+            });
+            auto opt_width = config.opt<ConfigOptionFloatOrPercent>("infill_extrusion_width");
+            REQUIRE(! opt_width->percent);
+            auto grow_d = scaled<float>(opt_width->value / 2);
+            auto inflate_lines = [grow_d](const Lines &lines) {
+                Polygons out;
+                for (const Line &line : lines)
+                    append(out, offset(Polyline{ line.a, line.b }, grow_d, Slic3r::ClipperLib::jtSquare, 3.));
+                return union_(out);
+            };
+            Polygons     layer0_infill = inflate_lines(infill[scaled<coord_t>(0.2)]);
+            Polygons     layer1_infill = inflate_lines(infill[scaled<coord_t>(0.4)]);
+            ExPolygons   poly          = opening_ex(diff_ex(layer0_infill, layer1_infill), grow_d);
+            const double threshold     = 2. * sqr(grow_d * 2.);
+            int          missing_parts = std::count_if(poly.begin(), poly.end(), [threshold](const ExPolygon &poly){ return poly.area() > threshold; });
+            REQUIRE(missing_parts == 0);
+        }
+    }
+}
+
 /*
-{
-    my $collection = Slic3r::Polyline::Collection->new(
-            Slic3r::Polyline->new([0,15], [0,18], [0,20]),
-            Slic3r::Polyline->new([0,10], [0,8], [0,5]),
-            );
-    is_deeply
-        [ map $_->[Y], map @$_, @{$collection->chained_path_from(Slic3r::Point->new(0,30), 0)} ],
-        [20, 18, 15, 10, 8, 5],
-        'chained path';
-}
-
-{
-    my $collection = Slic3r::Polyline::Collection->new(
-            Slic3r::Polyline->new([4,0], [10,0], [15,0]),
-            Slic3r::Polyline->new([10,5], [15,5], [20,5]),
-            );
-    is_deeply
-        [ map $_->[X], map @$_, @{$collection->chained_path_from(Slic3r::Point->new(30,0), 0)} ],
-        [reverse 4, 10, 15, 10, 15, 20],
-        'chained path';
-}
-
-{
-    my $collection = Slic3r::ExtrusionPath::Collection->new(
-            map Slic3r::ExtrusionPath->new(polyline => $_, role => 0, mm3_per_mm => 1),
-            Slic3r::Polyline->new([0,15], [0,18], [0,20]),
-            Slic3r::Polyline->new([0,10], [0,8], [0,5]),
-            );
-    is_deeply
-        [ map $_->[Y], map @{$_->polyline}, @{$collection->chained_path_from(Slic3r::Point->new(0,30), 0)} ],
-        [20, 18, 15, 10, 8, 5],
-        'chained path';
-}
-
-{
-    my $collection = Slic3r::ExtrusionPath::Collection->new(
-            map Slic3r::ExtrusionPath->new(polyline => $_, role => 0, mm3_per_mm => 1),
-            Slic3r::Polyline->new([15,0], [10,0], [4,0]),
-            Slic3r::Polyline->new([10,5], [15,5], [20,5]),
-            );
-    is_deeply
-        [ map $_->[X], map @{$_->polyline}, @{$collection->chained_path_from(Slic3r::Point->new(30,0), 0)} ],
-        [reverse 4, 10, 15, 10, 15, 20],
-        'chained path';
-}
-
-for my $pattern (qw(rectilinear honeycomb hilbertcurve concentric)) {
-    my $config = Slic3r::Config->new_from_defaults;
-    $config->set('fill_pattern', $pattern);
-    $config->set('external_fill_pattern', $pattern);
-    $config->set('perimeters', 1);
-    $config->set('skirts', 0);
-    $config->set('fill_density', 20);
-    $config->set('layer_height', 0.05);
-    $config->set('perimeter_extruder', 1);
-    $config->set('infill_extruder', 2);
-    my $print = Slic3r::Test::init_print('20mm_cube', config => $config, scale => 2);
-    ok my $gcode = Slic3r::Test::gcode($print), "successful $pattern infill generation";
-    my $tool = undef;
-    my @perimeter_points = my @infill_points = ();
-    Slic3r::GCode::Reader->new->parse($gcode, sub {
-            my ($self, $cmd, $args, $info) = @_;
-
-            if ($cmd =~ /^T(\d+)/) {
-            $tool = $1;
-            } elsif ($cmd eq 'G1' && $info->{extruding} && $info->{dist_XY} > 0) {
-            if ($tool == $config->perimeter_extruder-1) {
-            push @perimeter_points, Slic3r::Point->new_scale($args->{X}, $args->{Y});
-            } elsif ($tool == $config->infill_extruder-1) {
-            push @infill_points, Slic3r::Point->new_scale($args->{X}, $args->{Y});
-            }
-            }
-            });
-    my $convex_hull = convex_hull(\@perimeter_points);
-    ok !(defined first { !$convex_hull->contains_point($_) } @infill_points), "infill does not exceed perimeters ($pattern)";
-}
-
-{
-    my $config = Slic3r::Config->new_from_defaults;
-    $config->set('infill_only_where_needed', 1);
-    $config->set('bottom_solid_layers', 0);
-    $config->set('infill_extruder', 2);
-    $config->set('infill_extrusion_width', 0.5);
-    $config->set('fill_density', 40);
-    $config->set('cooling', 0);                 # for preventing speeds from being altered
-        $config->set('first_layer_speed', '100%');  # for preventing speeds from being altered
-
-        my $test = sub {
-            my $print = Slic3r::Test::init_print('pyramid', config => $config);
-
-            my $tool = undef;
-            my @infill_extrusions = ();  # array of polylines
-                Slic3r::GCode::Reader->new->parse(Slic3r::Test::gcode($print), sub {
-                        my ($self, $cmd, $args, $info) = @_;
-
-                        if ($cmd =~ /^T(\d+)/) {
-                        $tool = $1;
-                        } elsif ($cmd eq 'G1' && $info->{extruding} && $info->{dist_XY} > 0) {
-                        if ($tool == $config->infill_extruder-1) {
-                        push @infill_extrusions, Slic3r::Line->new_scale(
-                                [ $self->X, $self->Y ],
-                                [ $info->{new_X}, $info->{new_Y} ],
-                                );
-                        }
-                        }
-                        });
-            return 0 if !@infill_extrusions;  # prevent calling convex_hull() with no points
-
-                my $convex_hull = convex_hull([ map $_->pp, map @$_, @infill_extrusions ]);
-            return unscale unscale sum(map $_->area, @{offset([$convex_hull], scale(+$config->infill_extrusion_width/2))});
-        };
-
-    my $tolerance = 5;  # mm^2
-
-        $config->set('solid_infill_below_area', 0);
-    ok $test->() < $tolerance,
-       'no infill is generated when using infill_only_where_needed on a pyramid';
-
-    $config->set('solid_infill_below_area', 70);
-    ok abs($test->() - $config->solid_infill_below_area) < $tolerance,
-       'infill is only generated under the forced solid shells';
-}
-
-{
-    my $config = Slic3r::Config->new_from_defaults;
-    $config->set('skirts', 0);
-    $config->set('perimeters', 1);
-    $config->set('fill_density', 0);
-    $config->set('top_solid_layers', 0);
-    $config->set('bottom_solid_layers', 0);
-    $config->set('solid_infill_below_area', 20000000);
-    $config->set('solid_infill_every_layers', 2);
-    $config->set('perimeter_speed', 99);
-    $config->set('external_perimeter_speed', 99);
-    $config->set('cooling', 0);
-    $config->set('first_layer_speed', '100%');
-
-    my $print = Slic3r::Test::init_print('20mm_cube', config => $config);
-    my %layers_with_extrusion = ();
-    Slic3r::GCode::Reader->new->parse(Slic3r::Test::gcode($print), sub {
-            my ($self, $cmd, $args, $info) = @_;
-
-            if ($cmd eq 'G1' && $info->{dist_XY} > 0 && $info->{extruding}) {
-            if (($args->{F} // $self->F) != $config->perimeter_speed*60) {
-            $layers_with_extrusion{$self->Z} = ($args->{F} // $self->F);
-            }
-            }
-            });
-
-    ok !%layers_with_extrusion,
-       "solid_infill_below_area and solid_infill_every_layers are ignored when fill_density is 0";
-}
-
-{
-    my $config = Slic3r::Config->new_from_defaults;
-    $config->set('skirts', 0);
-    $config->set('perimeters', 3);
-    $config->set('fill_density', 0);
-    $config->set('layer_height', 0.2);
-    $config->set('first_layer_height', 0.2);
-    $config->set('nozzle_diameter', [0.35]);
-    $config->set('infill_extruder', 2);
-    $config->set('solid_infill_extruder', 2);
-    $config->set('infill_extrusion_width', 0.52);
-    $config->set('solid_infill_extrusion_width', 0.52);
-    $config->set('first_layer_extrusion_width', 0);
-
-    my $print = Slic3r::Test::init_print('A', config => $config);
-    my %infill = ();  # Z => [ Line, Line ... ]
-        my $tool = undef;
-    Slic3r::GCode::Reader->new->parse(Slic3r::Test::gcode($print), sub {
-            my ($self, $cmd, $args, $info) = @_;
-
-            if ($cmd =~ /^T(\d+)/) {
-            $tool = $1;
-            } elsif ($cmd eq 'G1' && $info->{extruding} && $info->{dist_XY} > 0) {
-            if ($tool == $config->infill_extruder-1) {
-            my $z = 1 * $self->Z;
-            $infill{$z} ||= [];
-            push @{$infill{$z}}, Slic3r::Line->new_scale(
-                    [ $self->X, $self->Y ],
-                    [ $info->{new_X}, $info->{new_Y} ],
-                    );
-            }
-            }
-            });
-    my $grow_d = scale($config->infill_extrusion_width)/2;
-    my $layer0_infill = union([ map @{$_->grow($grow_d)}, @{ $infill{0.2} } ]);
-    my $layer1_infill = union([ map @{$_->grow($grow_d)}, @{ $infill{0.4} } ]);
-    my $diff = diff($layer0_infill, $layer1_infill);
-    $diff = offset2_ex($diff, -$grow_d, +$grow_d);
-    $diff = [ grep { $_->area > 2*(($grow_d*2)**2) } @$diff ];
-    is scalar(@$diff), 0, 'no missing parts in solid shell when fill_density is 0';
-}
-
 {
     # GH: #2697
     my $config = Slic3r::Config->new_from_defaults;
@@ -427,6 +455,78 @@ for my $pattern (qw(rectilinear honeycomb hilbertcurve concentric)) {
     my @holes = map @{$_->holes}, @$covered;
     ok sum(map unscale unscale $_->area*-1, @holes) < 1, 'no gaps between top solid infill and perimeters';
 }
+
+{
+    skip "The FillRectilinear2 does not fill the surface completely", 1;
+
+    my $test = sub {
+        my ($expolygon, $flow_spacing, $angle, $density) = @_;
+        
+        my $filler = Slic3r::Filler->new_from_type('rectilinear');
+        $filler->set_bounding_box($expolygon->bounding_box);
+        $filler->set_angle($angle // 0);
+        # Adjust line spacing to fill the region.
+        $filler->set_dont_adjust(0);
+        $filler->set_link_max_length(scale(1.2*$flow_spacing));
+        my $surface = Slic3r::Surface->new(
+            surface_type    => S_TYPE_BOTTOM,
+            expolygon       => $expolygon,
+        );
+        my $flow = Slic3r::Flow->new(
+            width           => $flow_spacing,
+            height          => 0.4,
+            nozzle_diameter => $flow_spacing,
+        );
+        $filler->set_spacing($flow->spacing);
+        my $paths = $filler->fill_surface(
+            $surface,
+            layer_height    => $flow->height,
+            density         => $density // 1,
+        );
+        
+        # check whether any part was left uncovered
+        my @grown_paths = map @{Slic3r::Polyline->new(@$_)->grow(scale $filler->spacing/2)}, @$paths;
+        my $uncovered = diff_ex([ @$expolygon ], [ @grown_paths ], 1);
+        
+        # ignore very small dots
+        my $uncovered_filtered = [ grep $_->area > (scale $flow_spacing)**2, @$uncovered ];
+
+        is scalar(@$uncovered_filtered), 0, 'solid surface is fully filled';
+        
+        if (0 && @$uncovered_filtered) {
+            require "Slic3r/SVG.pm";
+            Slic3r::SVG::output("uncovered.svg", 
+                no_arrows       => 1,
+                expolygons      => [ $expolygon ],
+                blue_expolygons => [ @$uncovered ],
+                red_expolygons  => [ @$uncovered_filtered ],
+                polylines       => [ @$paths ],
+            );
+            exit;
+        }
+    };
+    
+    my $expolygon = Slic3r::ExPolygon->new([
+        [6883102, 9598327.01296997],
+        [6883102, 20327272.01297],
+        [3116896, 20327272.01297],
+        [3116896, 9598327.01296997],
+    ]);
+    $test->($expolygon, 0.55);
+    
+    for (1..20) {
+        $expolygon->scale(1.05);
+        $test->($expolygon, 0.55);
+    }
+    
+    $expolygon = Slic3r::ExPolygon->new(
+        [[59515297,5422499],[59531249,5578697],[59695801,6123186],[59965713,6630228],[60328214,7070685],[60773285,7434379],[61274561,7702115],[61819378,7866770],[62390306,7924789],[62958700,7866744],[63503012,7702244],[64007365,7434357],[64449960,7070398],[64809327,6634999],[65082143,6123325],[65245005,5584454],[65266967,5422499],[66267307,5422499],[66269190,8310081],[66275379,17810072],[66277259,20697500],[65267237,20697500],[65245004,20533538],[65082082,19994444],[64811462,19488579],[64450624,19048208],[64012101,18686514],[63503122,18415781],[62959151,18251378],[62453416,18198442],[62390147,18197355],[62200087,18200576],[61813519,18252990],[61274433,18415918],[60768598,18686517],[60327567,19047892],[59963609,19493297],[59695865,19994587],[59531222,20539379],[59515153,20697500],[58502480,20697500],[58502480,5422499]]
+    );
+    $test->($expolygon, 0.524341649025257);
+    
+    $expolygon = Slic3r::ExPolygon->new([ scale_points [0,0], [98,0], [98,10], [0,10] ]);
+    $test->($expolygon, 0.5, 45, 0.99);  # non-solid infill
+}
 */
 
 bool test_if_solid_surface_filled(const ExPolygon& expolygon, double flow_spacing, double angle, double density)
diff --git a/xs/CMakeLists.txt b/xs/CMakeLists.txt
index 06fc98322..022ba2c01 100644
--- a/xs/CMakeLists.txt
+++ b/xs/CMakeLists.txt
@@ -53,11 +53,8 @@ set(XS_XSP_FILES
     ${XSP_DIR}/ExtrusionLoop.xsp
     ${XSP_DIR}/ExtrusionMultiPath.xsp
     ${XSP_DIR}/ExtrusionPath.xsp
-    ${XSP_DIR}/ExtrusionSimulator.xsp
-    ${XSP_DIR}/Filler.xsp
     ${XSP_DIR}/Flow.xsp
     ${XSP_DIR}/GCode.xsp
-    # ${XSP_DIR}/GCodeSender.xsp
     ${XSP_DIR}/Geometry.xsp
     ${XSP_DIR}/Layer.xsp
     ${XSP_DIR}/Line.xsp
diff --git a/xs/lib/Slic3r/XS.pm b/xs/lib/Slic3r/XS.pm
index 6d3bf35cf..83bc0ee70 100644
--- a/xs/lib/Slic3r/XS.pm
+++ b/xs/lib/Slic3r/XS.pm
@@ -133,23 +133,6 @@ sub clone {
     );
 }
 
-package Slic3r::ExtrusionSimulator;
-
-sub new {
-    my ($class, %args) = @_;
-    return $class->_new();
-}
-
-package Slic3r::Filler;
-
-sub fill_surface {
-    my ($self, $surface, %args) = @_;
-    $self->set_density($args{density}) if defined($args{density});
-    $self->set_dont_adjust($args{dont_adjust}) if defined($args{dont_adjust});
-    $self->set_complete($args{complete}) if defined($args{complete});
-    return $self->_fill_surface($surface);
-}
-
 package Slic3r::Flow;
 
 sub new {
@@ -255,19 +238,12 @@ for my $class (qw(
         Slic3r::ExtrusionMultiPath
         Slic3r::ExtrusionPath
         Slic3r::ExtrusionPath::Collection
-        Slic3r::ExtrusionSimulator
-        Slic3r::Filler
         Slic3r::Flow
         Slic3r::GCode
         Slic3r::GCode::PlaceholderParser
         Slic3r::Geometry::BoundingBox
         Slic3r::Geometry::BoundingBoxf
         Slic3r::Geometry::BoundingBoxf3
-        Slic3r::GUI::_3DScene::GLShader        
-        Slic3r::GUI::_3DScene::GLVolume
-        Slic3r::GUI::Preset
-        Slic3r::GUI::PresetCollection
-        Slic3r::GUI::Tab
         Slic3r::Layer
         Slic3r::Layer::Region
         Slic3r::Layer::Support
diff --git a/xs/main.xs.in b/xs/main.xs.in
index c10f432d8..d8db108be 100644
--- a/xs/main.xs.in
+++ b/xs/main.xs.in
@@ -2,7 +2,6 @@
 #include <cstdlib>
 #include <ostream>
 #include <sstream>
-// #include <libslic3r/GCodeSender.hpp>
 
 #ifdef __cplusplus
 /* extern "C" { */
diff --git a/xs/src/perlglue.cpp b/xs/src/perlglue.cpp
index 20288243e..2d996120d 100644
--- a/xs/src/perlglue.cpp
+++ b/xs/src/perlglue.cpp
@@ -9,8 +9,6 @@ REGISTER_CLASS(ExtrusionMultiPath, "ExtrusionMultiPath");
 REGISTER_CLASS(ExtrusionPath, "ExtrusionPath");
 REGISTER_CLASS(ExtrusionLoop, "ExtrusionLoop");
 REGISTER_CLASS(ExtrusionEntityCollection, "ExtrusionPath::Collection");
-REGISTER_CLASS(ExtrusionSimulator, "ExtrusionSimulator");
-REGISTER_CLASS(Filler, "Filler");
 REGISTER_CLASS(Flow, "Flow");
 REGISTER_CLASS(CoolingBuffer, "GCode::CoolingBuffer");
 REGISTER_CLASS(GCode, "GCode");
diff --git a/xs/xsp/ExtrusionSimulator.xsp b/xs/xsp/ExtrusionSimulator.xsp
deleted file mode 100644
index 9395913b4..000000000
--- a/xs/xsp/ExtrusionSimulator.xsp
+++ /dev/null
@@ -1,50 +0,0 @@
-%module{Slic3r::XS};
-
-%{
-#include <xsinit.h>
-#include "libslic3r/ExtrusionSimulator.hpp"
-%}
-
-%name{Slic3r::ExtrusionSimulator} class ExtrusionSimulator {
-    ~ExtrusionSimulator();
-    %name{_new} ExtrusionSimulator();
-
-    Clone<ExtrusionSimulator> clone()
-        %code{% RETVAL = THIS; %};
-    
-    void  set_image_size(Point *image_size)
-        %code{% THIS->set_image_size(*image_size); %};
-    void  set_viewport(BoundingBox *viewport)
-        %code{% THIS->set_viewport(*viewport); %};
-    void  set_bounding_box(BoundingBox *bbox)
-        %code{% THIS->set_bounding_box(*bbox); %};
-
-    void  reset_accumulator();
-    void  extrude_to_accumulator(ExtrusionPath *path, Point *shift, ExtrusionSimulationType simulationType)
-        %code{% THIS->extrude_to_accumulator(*path, *shift, simulationType); %};
-    void  evaluate_accumulator(ExtrusionSimulationType simulationType);
-    void* image_ptr()
-        %code{% RETVAL = const_cast<void*>(const_cast<Slic3r::ExtrusionSimulator*>(THIS)->image_ptr()); %};
-
-%{
-
-%}
-};
-
-%package{Slic3r::ExtrusionSimulator};
-%{
-
-IV
-_constant()
-  ALIAS:
-    EXTRSIM_SIMPLE          = ExtrusionSimulationSimple
-    EXTRSIM_DONT_SPREAD     = ExtrusionSimulationDontSpread
-    EXTRSIM_SPREAD_NFULL     = ExtrisopmSimulationSpreadNotOverfilled
-    EXTRSIM_SPREAD_FULL     = ExtrusionSimulationSpreadFull
-    EXTRSIM_SPREAD_EXCESS   = ExtrusionSimulationSpreadExcess
-  PROTOTYPE:
-  CODE:
-    RETVAL = ix;
-  OUTPUT: RETVAL
-
-%}
diff --git a/xs/xsp/Filler.xsp b/xs/xsp/Filler.xsp
deleted file mode 100644
index 2ea2b34d5..000000000
--- a/xs/xsp/Filler.xsp
+++ /dev/null
@@ -1,65 +0,0 @@
-%module{Slic3r::XS};
-
-%{
-#include <xsinit.h>
-#include "libslic3r/Fill/Fill.hpp"
-#include "libslic3r/ExtrusionEntity.hpp"
-#include "libslic3r/ExtrusionEntityCollection.hpp"
-%}
-
-%name{Slic3r::Filler} class Filler {
-    ~Filler();
-
-    void set_bounding_box(BoundingBox *bbox)
-        %code{% THIS->fill->set_bounding_box(*bbox); %};
-    void set_spacing(coordf_t spacing)
-        %code{% THIS->fill->spacing = spacing; %};
-    coordf_t spacing()
-        %code{% RETVAL = THIS->fill->spacing; %};
-    void set_layer_id(size_t layer_id)
-        %code{% THIS->fill->layer_id = layer_id; %};
-    void set_z(coordf_t z)
-        %code{% THIS->fill->z = z; %};
-    void set_angle(float angle)
-        %code{% THIS->fill->angle = angle; %};
-    void set_link_max_length(coordf_t len)
-        %code{% THIS->fill->link_max_length = len; %};
-    void set_loop_clipping(coordf_t clipping)
-        %code{% THIS->fill->loop_clipping = clipping; %};
-
-    bool use_bridge_flow()
-        %code{% RETVAL = THIS->fill->use_bridge_flow(); %};
-    bool no_sort()
-        %code{% RETVAL = THIS->fill->no_sort(); %};
-
-    void set_density(float density)
-        %code{% THIS->params.density = density; %};
-    void set_dont_adjust(bool dont_adjust)
-        %code{% THIS->params.dont_adjust = dont_adjust; %};
-
-    PolylineCollection* _fill_surface(Surface *surface)
-        %code{% 
-            PolylineCollection *pc = NULL;
-            if (THIS->fill != NULL) {
-                pc = new PolylineCollection();
-                pc->polylines = THIS->fill->fill_surface(surface, THIS->params);
-            }
-            RETVAL =  pc;
-        %};
-
-%{
-
-Filler*
-new_from_type(CLASS, type)
-    char*               CLASS;
-    std::string         type;
-    CODE:
-        Filler *filler = new Filler();
-        filler->fill = Fill::new_from_type(type);
-        RETVAL = filler;
-    OUTPUT:
-        RETVAL
-
-%}
-
-};
diff --git a/xs/xsp/GCodeSender.xsp b/xs/xsp/GCodeSender.xsp
deleted file mode 100644
index f99244a1f..000000000
--- a/xs/xsp/GCodeSender.xsp
+++ /dev/null
@@ -1,24 +0,0 @@
-%module{Slic3r::XS};
-
-%{
-#include <xsinit.h>
-#include "libslic3r/GCodeSender.hpp"
-%}
-
-%name{Slic3r::GCode::Sender} class GCodeSender {
-    GCodeSender();
-    ~GCodeSender();
-    
-    bool connect(std::string port, unsigned int baud_rate);
-    void disconnect();
-    bool is_connected();
-    bool wait_connected(unsigned int timeout = 3);
-    int queue_size();
-    void send(std::string s, bool priority = false);
-    void pause_queue();
-    void resume_queue();
-    void purge_queue(bool priority = false);
-    std::vector<std::string> purge_log();
-    std::string getT();
-    std::string getB();
-};
diff --git a/xs/xsp/my.map b/xs/xsp/my.map
index ca26750dc..a660041bc 100644
--- a/xs/xsp/my.map
+++ b/xs/xsp/my.map
@@ -118,14 +118,6 @@ ExtrusionLoop*             O_OBJECT_SLIC3R
 Ref<ExtrusionLoop>         O_OBJECT_SLIC3R_T
 Clone<ExtrusionLoop>       O_OBJECT_SLIC3R_T
 
-ExtrusionSimulator*        O_OBJECT_SLIC3R
-Ref<ExtrusionSimulator>    O_OBJECT_SLIC3R_T
-Clone<ExtrusionSimulator>  O_OBJECT_SLIC3R_T
-
-Filler*                    O_OBJECT_SLIC3R
-Ref<Filler>                O_OBJECT_SLIC3R_T
-Clone<Filler>              O_OBJECT_SLIC3R_T
-
 Flow*                      O_OBJECT_SLIC3R
 Ref<Flow>                  O_OBJECT_SLIC3R_T
 Clone<Flow>                O_OBJECT_SLIC3R_T
diff --git a/xs/xsp/typemap.xspt b/xs/xsp/typemap.xspt
index f9e61c6a0..cc7109445 100644
--- a/xs/xsp/typemap.xspt
+++ b/xs/xsp/typemap.xspt
@@ -58,9 +58,6 @@
 %typemap{ExPolygonCollection*};
 %typemap{Ref<ExPolygonCollection>}{simple};
 %typemap{Clone<ExPolygonCollection>}{simple};
-%typemap{Filler*};
-%typemap{Ref<Filler>}{simple};
-%typemap{Clone<Filler>}{simple};
 %typemap{Flow*};
 %typemap{Ref<Flow>}{simple};
 %typemap{Clone<Flow>}{simple};
@@ -88,9 +85,6 @@
 %typemap{ExtrusionLoop*};
 %typemap{Ref<ExtrusionLoop>}{simple};
 %typemap{Clone<ExtrusionLoop>}{simple};
-%typemap{ExtrusionSimulator*};
-%typemap{Ref<ExtrusionSimulator>}{simple};
-%typemap{Clone<ExtrusionSimulator>}{simple};
 %typemap{TriangleMesh*};
 %typemap{Ref<TriangleMesh>}{simple};
 %typemap{Clone<TriangleMesh>}{simple};

From 1bf4d61e36fe07015bfdceaaf03d4cf6cf5c2b80 Mon Sep 17 00:00:00 2001
From: YuSanka <yusanka@gmail.com>
Date: Mon, 2 May 2022 14:36:23 +0200
Subject: [PATCH 04/23] Fix for #8261 - view mode gets reset when exiting the
 configuration wizard early

ConfigWizard::PageMode:: Code for initialization of values is moved to constructor from on_activate().
On Linux wxRadioButton values have to be set otherwise first value in group will be selected by default.
---
 src/slic3r/GUI/ConfigWizard.cpp         | 25 +++++++++----------------
 src/slic3r/GUI/ConfigWizard_private.hpp |  2 --
 2 files changed, 9 insertions(+), 18 deletions(-)

diff --git a/src/slic3r/GUI/ConfigWizard.cpp b/src/slic3r/GUI/ConfigWizard.cpp
index 4ff089882..86768dfc5 100644
--- a/src/slic3r/GUI/ConfigWizard.cpp
+++ b/src/slic3r/GUI/ConfigWizard.cpp
@@ -1281,17 +1281,6 @@ PageMode::PageMode(ConfigWizard *parent)
     radio_advanced = new wxRadioButton(this, wxID_ANY, _L("Advanced mode"));
     radio_expert = new wxRadioButton(this, wxID_ANY, _L("Expert mode"));
 
-    append(radio_simple);
-    append(radio_advanced);
-    append(radio_expert);
-
-    append_text("\n" + _L("The size of the object can be specified in inches"));
-    check_inch = new wxCheckBox(this, wxID_ANY, _L("Use inches"));
-    append(check_inch);
-}
-
-void PageMode::on_activate()
-{
     std::string mode { "simple" };
     wxGetApp().app_config->get("", "view_mode", mode);
 
@@ -1299,7 +1288,16 @@ void PageMode::on_activate()
     else if (mode == "expert") { radio_expert->SetValue(true); }
     else { radio_simple->SetValue(true); }
 
+    append(radio_simple);
+    append(radio_advanced);
+    append(radio_expert);
+
+    append_text("\n" + _L("The size of the object can be specified in inches"));
+    check_inch = new wxCheckBox(this, wxID_ANY, _L("Use inches"));
     check_inch->SetValue(wxGetApp().app_config->get("use_inches") == "1");
+    append(check_inch);
+
+    on_activate();
 }
 
 void PageMode::serialize_mode(AppConfig *app_config) const
@@ -1310,11 +1308,6 @@ void PageMode::serialize_mode(AppConfig *app_config) const
     if (radio_advanced->GetValue()) { mode = "advanced"; }
     if (radio_expert->GetValue()) { mode = "expert"; }
 
-    // If "Mode" page wasn't selected (no one radiobutton is checked),
-    // we shouldn't to update a view_mode value in app_config
-    if (mode.empty())
-        return; 
-
     app_config->set("view_mode", mode);
     app_config->set("use_inches", check_inch->GetValue() ? "1" : "0");
 }
diff --git a/src/slic3r/GUI/ConfigWizard_private.hpp b/src/slic3r/GUI/ConfigWizard_private.hpp
index c822a2be8..4de8381ff 100644
--- a/src/slic3r/GUI/ConfigWizard_private.hpp
+++ b/src/slic3r/GUI/ConfigWizard_private.hpp
@@ -426,8 +426,6 @@ struct PageMode: ConfigWizardPage
     PageMode(ConfigWizard *parent);
 
     void serialize_mode(AppConfig *app_config) const;
-
-    virtual void on_activate();
 };
 
 struct PageVendors: ConfigWizardPage

From 881a5dbf3725e0c9b4467cadc9c21815aec99623 Mon Sep 17 00:00:00 2001
From: YuSanka <yusanka@gmail.com>
Date: Mon, 2 May 2022 15:10:24 +0200
Subject: [PATCH 05/23] Fix for #8228 - Model errors icon does not go away
 after proper model reloading

---
 src/slic3r/GUI/Plater.cpp | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp
index d542c18e7..0456936e4 100644
--- a/src/slic3r/GUI/Plater.cpp
+++ b/src/slic3r/GUI/Plater.cpp
@@ -3854,6 +3854,9 @@ void Plater::priv::reload_from_disk()
                 old_model_object->sort_volumes(wxGetApp().app_config->get("order_volumes") == "1");
 
                 sla::reproject_points_and_holes(old_model_object);
+
+                // Fix warning icon in object list
+                wxGetApp().obj_list()->update_item_error_icon(obj_idx, vol_idx);
             }
         }
 #else

From c4b6381acfcde002a6fc0cc7ecc3578fcfecaebe Mon Sep 17 00:00:00 2001
From: YuSanka <yusanka@gmail.com>
Date: Tue, 3 May 2022 09:13:53 +0200
Subject: [PATCH 06/23] Fix for #8254 - Change of Print/Filament/Printer
 Settings not detected

---
 src/slic3r/GUI/MainFrame.cpp        | 11 ++++++++++-
 src/slic3r/GUI/PrintHostDialogs.cpp |  7 +++++--
 2 files changed, 15 insertions(+), 3 deletions(-)

diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp
index 570877716..cde7a05a2 100644
--- a/src/slic3r/GUI/MainFrame.cpp
+++ b/src/slic3r/GUI/MainFrame.cpp
@@ -706,6 +706,10 @@ void MainFrame::init_tabpanel()
             // before the MainFrame is fully set up.
             tab->OnActivate();
             m_last_selected_tab = m_tabpanel->GetSelection();
+#ifdef _MSW_DARK_MODE
+            if (wxGetApp().tabs_as_menu())
+                tab->SetFocus();
+#endif
         }
         else
             select_tab(size_t(0)); // select Plater
@@ -2018,8 +2022,13 @@ void MainFrame::select_tab(size_t tab/* = size_t(-1)*/)
             m_plater->SetFocus();
         Layout();
     }
-    else
+    else {
         select(false);
+#ifdef _MSW_DARK_MODE
+        if (wxGetApp().tabs_as_menu() && tab == 0)
+            m_plater->SetFocus();
+#endif
+    }
 
     // When we run application in ESettingsLayout::New or ESettingsLayout::Dlg mode, tabpanel is hidden from the very beginning
     // and as a result Tab::update_changed_tree_ui() function couldn't update m_is_nonsys_values values,
diff --git a/src/slic3r/GUI/PrintHostDialogs.cpp b/src/slic3r/GUI/PrintHostDialogs.cpp
index b53fe5c47..a09d15882 100644
--- a/src/slic3r/GUI/PrintHostDialogs.cpp
+++ b/src/slic3r/GUI/PrintHostDialogs.cpp
@@ -490,8 +490,11 @@ bool PrintHostQueueDialog::load_user_data(int udt, std::vector<int>& vector)
     auto* app_config = wxGetApp().app_config;
     auto hasget = [app_config](const std::string& name, std::vector<int>& vector)->bool {
         if (app_config->has(name)) {
-            vector.push_back(std::stoi(app_config->get(name)));
-            return true;
+            std::string val = app_config->get(name);
+            if (!val.empty() || val[0]!='\0') {
+                vector.push_back(std::stoi(val));
+                return true;
+            }
         }
         return false;
     };

From 1805f3c370db81444eec8ccce1c5162c73bcecf6 Mon Sep 17 00:00:00 2001
From: enricoturri1966 <enricoturri@seznam.cz>
Date: Tue, 3 May 2022 09:19:01 +0200
Subject: [PATCH 07/23] Tech ENABLE_NEW_RECTANGLE_SELECTION - Fixed object
 selection while a gizmo is open

---
 src/slic3r/GUI/GLCanvas3D.cpp | 15298 ++++++++++++++++----------------
 1 file changed, 7649 insertions(+), 7649 deletions(-)

diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp
index 14dfe3220..8a8ccfe1c 100644
--- a/src/slic3r/GUI/GLCanvas3D.cpp
+++ b/src/slic3r/GUI/GLCanvas3D.cpp
@@ -1,7649 +1,7649 @@
-#include "libslic3r/libslic3r.h"
-#include "GLCanvas3D.hpp"
-
-#include <igl/unproject.h>
-
-#include "libslic3r/BuildVolume.hpp"
-#include "libslic3r/ClipperUtils.hpp"
-#include "libslic3r/PrintConfig.hpp"
-#include "libslic3r/GCode/ThumbnailData.hpp"
-#include "libslic3r/Geometry/ConvexHull.hpp"
-#include "libslic3r/ExtrusionEntity.hpp"
-#include "libslic3r/Layer.hpp"
-#include "libslic3r/Utils.hpp"
-#include "libslic3r/Technologies.hpp"
-#include "libslic3r/Tesselate.hpp"
-#include "libslic3r/PresetBundle.hpp"
-#include "3DBed.hpp"
-#include "3DScene.hpp"
-#include "BackgroundSlicingProcess.hpp"
-#include "GLShader.hpp"
-#include "GUI.hpp"
-#include "Tab.hpp"
-#include "GUI_Preview.hpp"
-#include "OpenGLManager.hpp"
-#include "Plater.hpp"
-#include "MainFrame.hpp"
-#include "GUI_App.hpp"
-#include "GUI_ObjectList.hpp"
-#include "GUI_ObjectManipulation.hpp"
-#include "Mouse3DController.hpp"
-#include "I18N.hpp"
-#include "NotificationManager.hpp"
-#include "format.hpp"
-
-#include "slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp"
-#include "slic3r/Utils/UndoRedo.hpp"
-
-#if ENABLE_RETINA_GL
-#include "slic3r/Utils/RetinaHelper.hpp"
-#endif
-
-#include <GL/glew.h>
-
-#include <wx/glcanvas.h>
-#include <wx/bitmap.h>
-#include <wx/dcmemory.h>
-#include <wx/image.h>
-#include <wx/settings.h>
-#include <wx/tooltip.h>
-#include <wx/debug.h>
-#include <wx/fontutil.h>
-
-// Print now includes tbb, and tbb includes Windows. This breaks compilation of wxWidgets if included before wx.
-#include "libslic3r/Print.hpp"
-#include "libslic3r/SLAPrint.hpp"
-
-#include "wxExtensions.hpp"
-
-#include <tbb/parallel_for.h>
-#include <tbb/spin_mutex.h>
-
-#include <boost/log/trivial.hpp>
-#include <boost/algorithm/string/predicate.hpp>
-
-#include <iostream>
-#include <float.h>
-#include <algorithm>
-#include <cmath>
-#include "DoubleSlider.hpp"
-
-#include <imgui/imgui_internal.h>
-
-static constexpr const float TRACKBALLSIZE = 0.8f;
-
-#if ENABLE_LEGACY_OPENGL_REMOVAL
-static const Slic3r::ColorRGBA DEFAULT_BG_DARK_COLOR  = { 0.478f, 0.478f, 0.478f, 1.0f };
-static const Slic3r::ColorRGBA DEFAULT_BG_LIGHT_COLOR = { 0.753f, 0.753f, 0.753f, 1.0f };
-static const Slic3r::ColorRGBA ERROR_BG_DARK_COLOR    = { 0.478f, 0.192f, 0.039f, 1.0f };
-static const Slic3r::ColorRGBA ERROR_BG_LIGHT_COLOR   = { 0.753f, 0.192f, 0.039f, 1.0f };
-#else
-static const Slic3r::ColorRGB DEFAULT_BG_DARK_COLOR  = { 0.478f, 0.478f, 0.478f };
-static const Slic3r::ColorRGB DEFAULT_BG_LIGHT_COLOR = { 0.753f, 0.753f, 0.753f };
-static const Slic3r::ColorRGB ERROR_BG_DARK_COLOR    = { 0.478f, 0.192f, 0.039f };
-static const Slic3r::ColorRGB ERROR_BG_LIGHT_COLOR   = { 0.753f, 0.192f, 0.039f };
-#endif // ENABLE_LEGACY_OPENGL_REMOVAL
-
-// Number of floats
-static constexpr const size_t MAX_VERTEX_BUFFER_SIZE     = 131072 * 6; // 3.15MB
-// Reserve size in number of floats.
-#if !ENABLE_LEGACY_OPENGL_REMOVAL
-static constexpr const size_t VERTEX_BUFFER_RESERVE_SIZE = 131072 * 2; // 1.05MB
-// Reserve size in number of floats, maximum sum of all preallocated buffers.
-//static constexpr const size_t VERTEX_BUFFER_RESERVE_SIZE_SUM_MAX = 1024 * 1024 * 128 / 4; // 128MB
-#endif // !ENABLE_LEGACY_OPENGL_REMOVAL
-
-namespace Slic3r {
-namespace GUI {
-
-#ifdef __WXGTK3__
-// wxGTK3 seems to simulate OSX behavior in regard to HiDPI scaling support.
-RetinaHelper::RetinaHelper(wxWindow* window) : m_window(window), m_self(nullptr) {}
-RetinaHelper::~RetinaHelper() {}
-float RetinaHelper::get_scale_factor() { return float(m_window->GetContentScaleFactor()); }
-#endif // __WXGTK3__
-
-// Fixed the collision between BuildVolume::Type::Convex and macro Convex defined inside /usr/include/X11/X.h that is included by WxWidgets 3.0.
-#if defined(__linux__) && defined(Convex)
-#undef Convex
-#endif
-
-GLCanvas3D::LayersEditing::~LayersEditing()
-{
-    if (m_z_texture_id != 0) {
-        glsafe(::glDeleteTextures(1, &m_z_texture_id));
-        m_z_texture_id = 0;
-    }
-    delete m_slicing_parameters;
-}
-
-const float GLCanvas3D::LayersEditing::THICKNESS_BAR_WIDTH = 70.0f;
-
-void GLCanvas3D::LayersEditing::init()
-{
-    glsafe(::glGenTextures(1, (GLuint*)&m_z_texture_id));
-    glsafe(::glBindTexture(GL_TEXTURE_2D, m_z_texture_id));
-    glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP));
-    glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP));
-    glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR));
-    glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_NEAREST));
-    glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 1));
-    glsafe(::glBindTexture(GL_TEXTURE_2D, 0));
-}
-
-void GLCanvas3D::LayersEditing::set_config(const DynamicPrintConfig* config)
-{
-    m_config = config;
-    delete m_slicing_parameters;
-    m_slicing_parameters = nullptr;
-    m_layers_texture.valid = false;
-}
-
-void GLCanvas3D::LayersEditing::select_object(const Model &model, int object_id)
-{
-    const ModelObject *model_object_new = (object_id >= 0) ? model.objects[object_id] : nullptr;
-    // Maximum height of an object changes when the object gets rotated or scaled.
-    // Changing maximum height of an object will invalidate the layer heigth editing profile.
-    // m_model_object->bounding_box() is cached, therefore it is cheap even if this method is called frequently.
-    const float new_max_z = (model_object_new == nullptr) ? 0.0f : static_cast<float>(model_object_new->bounding_box().max.z());
-    if (m_model_object != model_object_new || this->last_object_id != object_id || m_object_max_z != new_max_z ||
-        (model_object_new != nullptr && m_model_object->id() != model_object_new->id())) {
-        m_layer_height_profile.clear();
-        m_layer_height_profile_modified = false;
-        delete m_slicing_parameters;
-        m_slicing_parameters   = nullptr;
-        m_layers_texture.valid = false;
-        this->last_object_id   = object_id;
-        m_model_object         = model_object_new;
-        m_object_max_z         = new_max_z;
-    }
-}
-
-bool GLCanvas3D::LayersEditing::is_allowed() const
-{
-    return wxGetApp().get_shader("variable_layer_height") != nullptr && m_z_texture_id > 0;
-}
-
-bool GLCanvas3D::LayersEditing::is_enabled() const
-{
-    return m_enabled;
-}
-
-void GLCanvas3D::LayersEditing::set_enabled(bool enabled)
-{
-    m_enabled = is_allowed() && enabled;
-}
-
-float GLCanvas3D::LayersEditing::s_overlay_window_width;
-
-void GLCanvas3D::LayersEditing::render_overlay(const GLCanvas3D& canvas)
-{
-    if (!m_enabled)
-        return;
-
-    const Size& cnv_size = canvas.get_canvas_size();
-
-    ImGuiWrapper& imgui = *wxGetApp().imgui();
-    imgui.set_next_window_pos(static_cast<float>(cnv_size.get_width()) - imgui.get_style_scaling() * THICKNESS_BAR_WIDTH, 
-        static_cast<float>(cnv_size.get_height()), ImGuiCond_Always, 1.0f, 1.0f);
-
-    imgui.begin(_L("Variable layer height"), ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse);
-
-    imgui.text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, _L("Left mouse button:"));
-    ImGui::SameLine();
-    imgui.text(_L("Add detail"));
-
-    imgui.text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, _L("Right mouse button:"));
-    ImGui::SameLine();
-    imgui.text(_L("Remove detail"));
-
-    imgui.text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, _L("Shift + Left mouse button:"));
-    ImGui::SameLine();
-    imgui.text(_L("Reset to base"));
-
-    imgui.text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, _L("Shift + Right mouse button:"));
-    ImGui::SameLine();
-    imgui.text(_L("Smoothing"));
-
-    imgui.text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, _L("Mouse wheel:"));
-    ImGui::SameLine();
-    imgui.text(_L("Increase/decrease edit area"));
-    
-    ImGui::Separator();
-    if (imgui.button(_L("Adaptive")))
-        wxPostEvent((wxEvtHandler*)canvas.get_wxglcanvas(), Event<float>(EVT_GLCANVAS_ADAPTIVE_LAYER_HEIGHT_PROFILE, m_adaptive_quality));
-
-    ImGui::SameLine();
-    float text_align = ImGui::GetCursorPosX();
-    ImGui::AlignTextToFramePadding();
-    imgui.text(_L("Quality / Speed"));
-    if (ImGui::IsItemHovered()) {
-        ImGui::BeginTooltip();
-        ImGui::TextUnformatted(_L("Higher print quality versus higher print speed.").ToUTF8());
-        ImGui::EndTooltip();
-    }
-
-    ImGui::SameLine();
-    float widget_align = ImGui::GetCursorPosX();
-    ImGui::PushItemWidth(imgui.get_style_scaling() * 120.0f);
-    m_adaptive_quality = std::clamp(m_adaptive_quality, 0.0f, 1.f);
-    imgui.slider_float("", &m_adaptive_quality, 0.0f, 1.f, "%.2f");
-
-    ImGui::Separator();
-    if (imgui.button(_L("Smooth")))
-        wxPostEvent((wxEvtHandler*)canvas.get_wxglcanvas(), HeightProfileSmoothEvent(EVT_GLCANVAS_SMOOTH_LAYER_HEIGHT_PROFILE, m_smooth_params));
-
-    ImGui::SameLine();
-    ImGui::SetCursorPosX(text_align);
-    ImGui::AlignTextToFramePadding();
-    imgui.text(_L("Radius"));
-    ImGui::SameLine();
-    ImGui::SetCursorPosX(widget_align);
-    ImGui::PushItemWidth(imgui.get_style_scaling() * 120.0f);
-    int radius = (int)m_smooth_params.radius;
-    if (ImGui::SliderInt("##1", &radius, 1, 10)) {
-        radius = std::clamp(radius, 1, 10);
-        m_smooth_params.radius = (unsigned int)radius;
-    }
-
-    ImGui::SetCursorPosX(text_align);
-    ImGui::AlignTextToFramePadding();
-    imgui.text(_L("Keep min"));
-    ImGui::SameLine();
-    if (ImGui::GetCursorPosX() < widget_align)  // because of line lenght after localization
-        ImGui::SetCursorPosX(widget_align);
-
-    ImGui::PushItemWidth(imgui.get_style_scaling() * 120.0f);
-    imgui.checkbox("##2", m_smooth_params.keep_min);
-
-    ImGui::Separator();
-    if (imgui.button(_L("Reset")))
-        wxPostEvent((wxEvtHandler*)canvas.get_wxglcanvas(), SimpleEvent(EVT_GLCANVAS_RESET_LAYER_HEIGHT_PROFILE));
-
-    GLCanvas3D::LayersEditing::s_overlay_window_width = ImGui::GetWindowSize().x /*+ (float)m_layers_texture.width/4*/;
-    imgui.end();
-
-#if ENABLE_GL_SHADERS_ATTRIBUTES
-    render_active_object_annotations(canvas);
-    render_profile(canvas);
-#else
-    const Rect& bar_rect = get_bar_rect_viewport(canvas);
-#if ENABLE_LEGACY_OPENGL_REMOVAL
-    m_profile.dirty = m_profile.old_bar_rect != bar_rect;
-#endif // ENABLE_LEGACY_OPENGL_REMOVAL
-    render_active_object_annotations(canvas, bar_rect);
-    render_profile(bar_rect);
-#if ENABLE_LEGACY_OPENGL_REMOVAL
-    m_profile.old_bar_rect = bar_rect;
-    m_profile.dirty = false;
-#endif // ENABLE_LEGACY_OPENGL_REMOVAL
-#endif // ENABLE_GL_SHADERS_ATTRIBUTES
-}
-
-float GLCanvas3D::LayersEditing::get_cursor_z_relative(const GLCanvas3D& canvas)
-{
-    const Vec2d mouse_pos = canvas.get_local_mouse_position();
-    const Rect& rect = get_bar_rect_screen(canvas);
-    float x = (float)mouse_pos.x();
-    float y = (float)mouse_pos.y();
-    float t = rect.get_top();
-    float b = rect.get_bottom();
-
-    return (rect.get_left() <= x && x <= rect.get_right() && t <= y && y <= b) ?
-        // Inside the bar.
-        (b - y - 1.0f) / (b - t - 1.0f) :
-        // Outside the bar.
-        -1000.0f;
-}
-
-bool GLCanvas3D::LayersEditing::bar_rect_contains(const GLCanvas3D& canvas, float x, float y)
-{
-    const Rect& rect = get_bar_rect_screen(canvas);
-    return rect.get_left() <= x && x <= rect.get_right() && rect.get_top() <= y && y <= rect.get_bottom();
-}
-
-Rect GLCanvas3D::LayersEditing::get_bar_rect_screen(const GLCanvas3D& canvas)
-{
-    const Size& cnv_size = canvas.get_canvas_size();
-    float w = (float)cnv_size.get_width();
-    float h = (float)cnv_size.get_height();
-
-    return { w - thickness_bar_width(canvas), 0.0f, w, h };
-}
-
-#if !ENABLE_GL_SHADERS_ATTRIBUTES
-Rect GLCanvas3D::LayersEditing::get_bar_rect_viewport(const GLCanvas3D& canvas)
-{
-    const Size& cnv_size = canvas.get_canvas_size();
-    float half_w = 0.5f * (float)cnv_size.get_width();
-    float half_h = 0.5f * (float)cnv_size.get_height();
-    float inv_zoom = (float)wxGetApp().plater()->get_camera().get_inv_zoom();
-    return { (half_w - thickness_bar_width(canvas)) * inv_zoom, half_h * inv_zoom, half_w * inv_zoom, -half_h * inv_zoom };
-}
-#endif // !ENABLE_GL_SHADERS_ATTRIBUTES
-
-bool GLCanvas3D::LayersEditing::is_initialized() const
-{
-    return wxGetApp().get_shader("variable_layer_height") != nullptr;
-}
-
-std::string GLCanvas3D::LayersEditing::get_tooltip(const GLCanvas3D& canvas) const
-{
-    std::string ret;
-    if (m_enabled && m_layer_height_profile.size() >= 4) {
-        float z = get_cursor_z_relative(canvas);
-        if (z != -1000.0f) {
-            z *= m_object_max_z;
-
-            float h = 0.0f;
-            for (size_t i = m_layer_height_profile.size() - 2; i >= 2; i -= 2) {
-                const float zi = static_cast<float>(m_layer_height_profile[i]);
-                const float zi_1 = static_cast<float>(m_layer_height_profile[i - 2]);
-                if (zi_1 <= z && z <= zi) {
-                    float dz = zi - zi_1;
-                    h = (dz != 0.0f) ? static_cast<float>(lerp(m_layer_height_profile[i - 1], m_layer_height_profile[i + 1], (z - zi_1) / dz)) :
-                        static_cast<float>(m_layer_height_profile[i + 1]);
-                    break;
-                }
-            }
-            if (h > 0.0f)
-                ret = std::to_string(h);
-        }
-    }
-    return ret;
-}
-
-#if ENABLE_GL_SHADERS_ATTRIBUTES
-void GLCanvas3D::LayersEditing::render_active_object_annotations(const GLCanvas3D& canvas)
-#else
-void GLCanvas3D::LayersEditing::render_active_object_annotations(const GLCanvas3D& canvas, const Rect& bar_rect)
-#endif // ENABLE_GL_SHADERS_ATTRIBUTES
-{
-#if ENABLE_GL_SHADERS_ATTRIBUTES
-    const Size cnv_size = canvas.get_canvas_size();
-    const float cnv_width  = (float)cnv_size.get_width();
-    const float cnv_height = (float)cnv_size.get_height();
-    if (cnv_width == 0.0f || cnv_height == 0.0f)
-        return;
-
-    const float cnv_inv_width = 1.0f / cnv_width;
-#endif // ENABLE_GL_SHADERS_ATTRIBUTES
-    GLShaderProgram* shader = wxGetApp().get_shader("variable_layer_height");
-    if (shader == nullptr)
-        return;
-
-    shader->start_using();
-
-    shader->set_uniform("z_to_texture_row", float(m_layers_texture.cells - 1) / (float(m_layers_texture.width) * m_object_max_z));
-    shader->set_uniform("z_texture_row_to_normalized", 1.0f / (float)m_layers_texture.height);
-    shader->set_uniform("z_cursor", m_object_max_z * this->get_cursor_z_relative(canvas));
-    shader->set_uniform("z_cursor_band_width", band_width);
-    shader->set_uniform("object_max_z", m_object_max_z);
-#if ENABLE_GL_SHADERS_ATTRIBUTES
-    shader->set_uniform("view_model_matrix", Transform3d::Identity());
-    shader->set_uniform("projection_matrix", Transform3d::Identity());
-    shader->set_uniform("normal_matrix", (Matrix3d)Eigen::Matrix3d::Identity());
-#endif // ENABLE_GL_SHADERS_ATTRIBUTES
-
-    glsafe(::glPixelStorei(GL_UNPACK_ALIGNMENT, 1));
-    glsafe(::glBindTexture(GL_TEXTURE_2D, m_z_texture_id));
-
-    // Render the color bar
-#if ENABLE_LEGACY_OPENGL_REMOVAL
-#if ENABLE_GL_SHADERS_ATTRIBUTES
-    if (!m_profile.background.is_initialized() || m_profile.old_canvas_width != cnv_width) {
-        m_profile.old_canvas_width = cnv_width;
-#else
-    if (!m_profile.background.is_initialized() || m_profile.dirty) {
-#endif // ENABLE_GL_SHADERS_ATTRIBUTES
-        m_profile.background.reset();
-
-        GLModel::Geometry init_data;
-        init_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P2T2 };
-        init_data.reserve_vertices(4);
-        init_data.reserve_indices(6);
-
-        // vertices
-#if ENABLE_GL_SHADERS_ATTRIBUTES
-        const float l = 1.0f - 2.0f * THICKNESS_BAR_WIDTH * cnv_inv_width;
-        const float r = 1.0f;
-        const float t = 1.0f;
-        const float b = -1.0f;
-#else
-        const float l = bar_rect.get_left();
-        const float r = bar_rect.get_right();
-        const float t = bar_rect.get_top();
-        const float b = bar_rect.get_bottom();
-#endif // ENABLE_GL_SHADERS_ATTRIBUTES
-        init_data.add_vertex(Vec2f(l, b), Vec2f(0.0f, 0.0f));
-        init_data.add_vertex(Vec2f(r, b), Vec2f(1.0f, 0.0f));
-        init_data.add_vertex(Vec2f(r, t), Vec2f(1.0f, 1.0f));
-        init_data.add_vertex(Vec2f(l, t), Vec2f(0.0f, 1.0f));
-
-        // indices
-        init_data.add_triangle(0, 1, 2);
-        init_data.add_triangle(2, 3, 0);
-
-        m_profile.background.init_from(std::move(init_data));
-    }
-
-    m_profile.background.render();
-#else
-    const float l = bar_rect.get_left();
-    const float r = bar_rect.get_right();
-    const float t = bar_rect.get_top();
-    const float b = bar_rect.get_bottom();
-
-    ::glBegin(GL_QUADS);
-    ::glNormal3f(0.0f, 0.0f, 1.0f);
-    ::glTexCoord2f(0.0f, 0.0f); ::glVertex2f(l, b);
-    ::glTexCoord2f(1.0f, 0.0f); ::glVertex2f(r, b);
-    ::glTexCoord2f(1.0f, 1.0f); ::glVertex2f(r, t);
-    ::glTexCoord2f(0.0f, 1.0f); ::glVertex2f(l, t);
-    glsafe(::glEnd());
-#endif // ENABLE_LEGACY_OPENGL_REMOVAL
-
-    glsafe(::glBindTexture(GL_TEXTURE_2D, 0));
-
-    shader->stop_using();
-}
-
-#if ENABLE_GL_SHADERS_ATTRIBUTES
-void GLCanvas3D::LayersEditing::render_profile(const GLCanvas3D& canvas)
-#else
-void GLCanvas3D::LayersEditing::render_profile(const Rect& bar_rect)
-#endif // ENABLE_GL_SHADERS_ATTRIBUTES
-{
-    //FIXME show some kind of legend.
-
-    if (!m_slicing_parameters)
-        return;
-
-#if ENABLE_GL_SHADERS_ATTRIBUTES
-    const Size cnv_size = canvas.get_canvas_size();
-    const float cnv_width  = (float)cnv_size.get_width();
-    const float cnv_height = (float)cnv_size.get_height();
-    if (cnv_width == 0.0f || cnv_height == 0.0f)
-        return;
-
-    // Make the vertical bar a bit wider so the layer height curve does not touch the edge of the bar region.
-    const float scale_x = THICKNESS_BAR_WIDTH / float(1.12 * m_slicing_parameters->max_layer_height);
-    const float scale_y = cnv_height / m_object_max_z;
-
-    const float cnv_inv_width  = 1.0f / cnv_width;
-    const float cnv_inv_height = 1.0f / cnv_height;
-#else
-    // Make the vertical bar a bit wider so the layer height curve does not touch the edge of the bar region.
-    const float scale_x = bar_rect.get_width() / float(1.12 * m_slicing_parameters->max_layer_height);
-    const float scale_y = bar_rect.get_height() / m_object_max_z;
-#endif // ENABLE_GL_SHADERS_ATTRIBUTES
-
-#if ENABLE_LEGACY_OPENGL_REMOVAL
-    // Baseline
-#if ENABLE_GL_SHADERS_ATTRIBUTES
-    if (!m_profile.baseline.is_initialized() || m_profile.old_layer_height_profile != m_layer_height_profile) {
-#else
-    if (!m_profile.baseline.is_initialized() || m_profile.dirty) {
-#endif // ENABLE_GL_SHADERS_ATTRIBUTES
-        m_profile.baseline.reset();
-
-        GLModel::Geometry init_data;
-        init_data.format = { GLModel::Geometry::EPrimitiveType::Lines, GLModel::Geometry::EVertexLayout::P2 };
-        init_data.color = ColorRGBA::BLACK();
-        init_data.reserve_vertices(2);
-        init_data.reserve_indices(2);
-
-        // vertices
-#if ENABLE_GL_SHADERS_ATTRIBUTES
-        const float axis_x = 2.0f * ((cnv_width - THICKNESS_BAR_WIDTH + float(m_slicing_parameters->layer_height) * scale_x) * cnv_inv_width - 0.5f);
-        init_data.add_vertex(Vec2f(axis_x, -1.0f));
-        init_data.add_vertex(Vec2f(axis_x, 1.0f));
-#else
-        const float x = bar_rect.get_left() + float(m_slicing_parameters->layer_height) * scale_x;
-        init_data.add_vertex(Vec2f(x, bar_rect.get_bottom()));
-        init_data.add_vertex(Vec2f(x, bar_rect.get_top()));
-#endif // ENABLE_GL_SHADERS_ATTRIBUTES
-
-        // indices
-        init_data.add_line(0, 1);
-
-        m_profile.baseline.init_from(std::move(init_data));
-    }
-
-#if ENABLE_GL_SHADERS_ATTRIBUTES
-    if (!m_profile.profile.is_initialized() || m_profile.old_layer_height_profile != m_layer_height_profile) {
-#else
-    if (!m_profile.profile.is_initialized() || m_profile.dirty || m_profile.old_layer_height_profile != m_layer_height_profile) {
-#endif // ENABLE_GL_SHADERS_ATTRIBUTES
-        m_profile.old_layer_height_profile = m_layer_height_profile;
-        m_profile.profile.reset();
-
-        GLModel::Geometry init_data;
-        init_data.format = { GLModel::Geometry::EPrimitiveType::LineStrip, GLModel::Geometry::EVertexLayout::P2 };
-        init_data.color = ColorRGBA::BLUE();
-        init_data.reserve_vertices(m_layer_height_profile.size() / 2);
-        init_data.reserve_indices(m_layer_height_profile.size() / 2);
-
-        // vertices + indices
-        for (unsigned int i = 0; i < (unsigned int)m_layer_height_profile.size(); i += 2) {
-#if ENABLE_GL_SHADERS_ATTRIBUTES
-            init_data.add_vertex(Vec2f(2.0f * ((cnv_width - THICKNESS_BAR_WIDTH + float(m_layer_height_profile[i + 1]) * scale_x) * cnv_inv_width - 0.5f),
-                                       2.0f * (float(m_layer_height_profile[i]) * scale_y * cnv_inv_height - 0.5)));
-#else
-            init_data.add_vertex(Vec2f(bar_rect.get_left() + float(m_layer_height_profile[i + 1]) * scale_x,
-                                       bar_rect.get_bottom() + float(m_layer_height_profile[i]) * scale_y));
-#endif // ENABLE_GL_SHADERS_ATTRIBUTES
-            init_data.add_index(i / 2);
-        }
-
-        m_profile.profile.init_from(std::move(init_data));
-    }
-
-    GLShaderProgram* shader = wxGetApp().get_shader("flat");
-    if (shader != nullptr) {
-        shader->start_using();
-#if ENABLE_GL_SHADERS_ATTRIBUTES
-        shader->set_uniform("view_model_matrix", Transform3d::Identity());
-        shader->set_uniform("projection_matrix", Transform3d::Identity());
-#endif // ENABLE_GL_SHADERS_ATTRIBUTES
-        m_profile.baseline.render();
-        m_profile.profile.render();
-        shader->stop_using();
-    }
-#else
-    const float x = bar_rect.get_left() + float(m_slicing_parameters->layer_height) * scale_x;
-
-    // Baseline
-    glsafe(::glColor3f(0.0f, 0.0f, 0.0f));
-    ::glBegin(GL_LINE_STRIP);
-    ::glVertex2f(x, bar_rect.get_bottom());
-    ::glVertex2f(x, bar_rect.get_top());
-    glsafe(::glEnd());
-
-    // Curve
-    glsafe(::glColor3f(0.0f, 0.0f, 1.0f));
-    ::glBegin(GL_LINE_STRIP);
-    for (unsigned int i = 0; i < m_layer_height_profile.size(); i += 2)
-        ::glVertex2f(bar_rect.get_left() + (float)m_layer_height_profile[i + 1] * scale_x, bar_rect.get_bottom() + (float)m_layer_height_profile[i] * scale_y);
-    glsafe(::glEnd());
-#endif // ENABLE_LEGACY_OPENGL_REMOVAL
-}
-
-void GLCanvas3D::LayersEditing::render_volumes(const GLCanvas3D& canvas, const GLVolumeCollection& volumes)
-{
-    assert(this->is_allowed());
-    assert(this->last_object_id != -1);
-
-    GLShaderProgram* current_shader = wxGetApp().get_current_shader();
-    ScopeGuard guard([current_shader]() { if (current_shader != nullptr) current_shader->start_using(); });
-    if (current_shader != nullptr)
-        current_shader->stop_using();
-
-    GLShaderProgram* shader = wxGetApp().get_shader("variable_layer_height");
-    if (shader == nullptr)
-        return;
-
-    shader->start_using();
-
-    generate_layer_height_texture();
-
-    // Uniforms were resolved, go ahead using the layer editing shader.
-    shader->set_uniform("z_to_texture_row", float(m_layers_texture.cells - 1) / (float(m_layers_texture.width) * float(m_object_max_z)));
-    shader->set_uniform("z_texture_row_to_normalized", 1.0f / float(m_layers_texture.height));
-    shader->set_uniform("z_cursor", float(m_object_max_z) * float(this->get_cursor_z_relative(canvas)));
-    shader->set_uniform("z_cursor_band_width", float(this->band_width));
-
-#if ENABLE_GL_SHADERS_ATTRIBUTES
-    const Camera& camera = wxGetApp().plater()->get_camera();
-    shader->set_uniform("projection_matrix", camera.get_projection_matrix());
-#endif // ENABLE_GL_SHADERS_ATTRIBUTES
-
-    // Initialize the layer height texture mapping.
-    const GLsizei w = (GLsizei)m_layers_texture.width;
-    const GLsizei h = (GLsizei)m_layers_texture.height;
-    const GLsizei half_w = w / 2;
-    const GLsizei half_h = h / 2;
-    glsafe(::glPixelStorei(GL_UNPACK_ALIGNMENT, 1));
-    glsafe(::glBindTexture(GL_TEXTURE_2D, m_z_texture_id));
-    glsafe(::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0));
-    glsafe(::glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA, half_w, half_h, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0));
-    glsafe(::glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, m_layers_texture.data.data()));
-    glsafe(::glTexSubImage2D(GL_TEXTURE_2D, 1, 0, 0, half_w, half_h, GL_RGBA, GL_UNSIGNED_BYTE, m_layers_texture.data.data() + m_layers_texture.width * m_layers_texture.height * 4));
-    for (GLVolume* glvolume : volumes.volumes) {
-        // Render the object using the layer editing shader and texture.
-        if (!glvolume->is_active || glvolume->composite_id.object_id != this->last_object_id || glvolume->is_modifier)
-            continue;
-
-        shader->set_uniform("volume_world_matrix", glvolume->world_matrix());
-        shader->set_uniform("object_max_z", 0.0f);
-#if ENABLE_GL_SHADERS_ATTRIBUTES
-        const Transform3d view_model_matrix = camera.get_view_matrix() * glvolume->world_matrix();
-        shader->set_uniform("view_model_matrix", view_model_matrix);
-        shader->set_uniform("normal_matrix", (Matrix3d)view_model_matrix.matrix().block(0, 0, 3, 3).inverse().transpose());
-#endif // ENABLE_GL_SHADERS_ATTRIBUTES
-
-        glvolume->render();
-    }
-    // Revert back to the previous shader.
-    glBindTexture(GL_TEXTURE_2D, 0);
-}
-
-void GLCanvas3D::LayersEditing::adjust_layer_height_profile()
-{
-	this->update_slicing_parameters();
-	PrintObject::update_layer_height_profile(*m_model_object, *m_slicing_parameters, m_layer_height_profile);
-	Slic3r::adjust_layer_height_profile(*m_slicing_parameters, m_layer_height_profile, this->last_z, this->strength, this->band_width, this->last_action);
-	m_layer_height_profile_modified = true;
-    m_layers_texture.valid = false;
-}
-
-void GLCanvas3D::LayersEditing::reset_layer_height_profile(GLCanvas3D& canvas)
-{
-    const_cast<ModelObject*>(m_model_object)->layer_height_profile.clear();
-    m_layer_height_profile.clear();
-    m_layers_texture.valid = false;
-    canvas.post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS));
-    wxGetApp().obj_list()->update_info_items(last_object_id);
-}
-
-void GLCanvas3D::LayersEditing::adaptive_layer_height_profile(GLCanvas3D& canvas, float quality_factor)
-{
-    this->update_slicing_parameters();
-    m_layer_height_profile = layer_height_profile_adaptive(*m_slicing_parameters, *m_model_object, quality_factor);
-    const_cast<ModelObject*>(m_model_object)->layer_height_profile.set(m_layer_height_profile);
-    m_layers_texture.valid = false;
-    canvas.post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS));
-    wxGetApp().obj_list()->update_info_items(last_object_id);
-}
-
-void GLCanvas3D::LayersEditing::smooth_layer_height_profile(GLCanvas3D& canvas, const HeightProfileSmoothingParams& smoothing_params)
-{
-    this->update_slicing_parameters();
-    m_layer_height_profile = smooth_height_profile(m_layer_height_profile, *m_slicing_parameters, smoothing_params);
-    const_cast<ModelObject*>(m_model_object)->layer_height_profile.set(m_layer_height_profile);
-    m_layers_texture.valid = false;
-    canvas.post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS));
-    wxGetApp().obj_list()->update_info_items(last_object_id);
-}
-
-void GLCanvas3D::LayersEditing::generate_layer_height_texture()
-{
-	this->update_slicing_parameters();
-	// Always try to update the layer height profile.
-    bool update = ! m_layers_texture.valid;
-    if (PrintObject::update_layer_height_profile(*m_model_object, *m_slicing_parameters, m_layer_height_profile)) {
-        // Initialized to the default value.
-        m_layer_height_profile_modified = false;
-        update = true;
-    }
-    // Update if the layer height profile was changed, or when the texture is not valid.
-    if (! update && ! m_layers_texture.data.empty() && m_layers_texture.cells > 0)
-        // Texture is valid, don't update.
-        return; 
-
-    if (m_layers_texture.data.empty()) {
-        m_layers_texture.width  = 1024;
-        m_layers_texture.height = 1024;
-        m_layers_texture.levels = 2;
-        m_layers_texture.data.assign(m_layers_texture.width * m_layers_texture.height * 5, 0);
-    }
-
-    bool level_of_detail_2nd_level = true;
-    m_layers_texture.cells = Slic3r::generate_layer_height_texture(
-        *m_slicing_parameters, 
-        Slic3r::generate_object_layers(*m_slicing_parameters, m_layer_height_profile), 
-		m_layers_texture.data.data(), m_layers_texture.height, m_layers_texture.width, level_of_detail_2nd_level);
-	m_layers_texture.valid = true;
-}
-
-void GLCanvas3D::LayersEditing::accept_changes(GLCanvas3D& canvas)
-{
-    if (last_object_id >= 0) {
-        if (m_layer_height_profile_modified) {
-            wxGetApp().plater()->take_snapshot(_L("Variable layer height - Manual edit"));
-            const_cast<ModelObject*>(m_model_object)->layer_height_profile.set(m_layer_height_profile);
-			canvas.post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS));
-            wxGetApp().obj_list()->update_info_items(last_object_id);
-        }
-    }
-    m_layer_height_profile_modified = false;
-}
-
-void GLCanvas3D::LayersEditing::update_slicing_parameters()
-{
-	if (m_slicing_parameters == nullptr) {
-		m_slicing_parameters = new SlicingParameters();
-        *m_slicing_parameters = PrintObject::slicing_parameters(*m_config, *m_model_object, m_object_max_z);
-    }
-}
-
-float GLCanvas3D::LayersEditing::thickness_bar_width(const GLCanvas3D &canvas)
-{
-    return
-#if ENABLE_RETINA_GL
-        canvas.get_canvas_size().get_scale_factor()
-#else
-        canvas.get_wxglcanvas()->GetContentScaleFactor()
-#endif
-         * THICKNESS_BAR_WIDTH;
-}
-
-
-const Point GLCanvas3D::Mouse::Drag::Invalid_2D_Point(INT_MAX, INT_MAX);
-const Vec3d GLCanvas3D::Mouse::Drag::Invalid_3D_Point(DBL_MAX, DBL_MAX, DBL_MAX);
-const int GLCanvas3D::Mouse::Drag::MoveThresholdPx = 5;
-
-GLCanvas3D::Mouse::Drag::Drag()
-    : start_position_2D(Invalid_2D_Point)
-    , start_position_3D(Invalid_3D_Point)
-    , move_volume_idx(-1)
-    , move_requires_threshold(false)
-    , move_start_threshold_position_2D(Invalid_2D_Point)
-{
-}
-
-GLCanvas3D::Mouse::Mouse()
-    : dragging(false)
-    , position(DBL_MAX, DBL_MAX)
-    , scene_position(DBL_MAX, DBL_MAX, DBL_MAX)
-    , ignore_left_up(false)
-{
-}
-
-void GLCanvas3D::Labels::render(const std::vector<const ModelInstance*>& sorted_instances) const
-{
-    if (!m_enabled || !is_shown())
-        return;
-
-    const Camera& camera = wxGetApp().plater()->get_camera();
-    const Model* model = m_canvas.get_model();
-    if (model == nullptr)
-        return;
-
-    Transform3d world_to_eye = camera.get_view_matrix();
-    Transform3d world_to_screen = camera.get_projection_matrix() * world_to_eye;
-    const std::array<int, 4>& viewport = camera.get_viewport();
-
-    struct Owner
-    {
-        int obj_idx;
-        int inst_idx;
-        size_t model_instance_id;
-        BoundingBoxf3 world_box;
-        double eye_center_z;
-        std::string title;
-        std::string label;
-        std::string print_order;
-        bool selected;
-    };
-
-    // collect owners world bounding boxes and data from volumes
-    std::vector<Owner> owners;
-    const GLVolumeCollection& volumes = m_canvas.get_volumes();
-    for (const GLVolume* volume : volumes.volumes) {
-        int obj_idx = volume->object_idx();
-        if (0 <= obj_idx && obj_idx < (int)model->objects.size()) {
-            int inst_idx = volume->instance_idx();
-            std::vector<Owner>::iterator it = std::find_if(owners.begin(), owners.end(), [obj_idx, inst_idx](const Owner& owner) {
-                return (owner.obj_idx == obj_idx) && (owner.inst_idx == inst_idx);
-                });
-            if (it != owners.end()) {
-                it->world_box.merge(volume->transformed_bounding_box());
-                it->selected &= volume->selected;
-            } else {
-                const ModelObject* model_object = model->objects[obj_idx];
-                Owner owner;
-                owner.obj_idx = obj_idx;
-                owner.inst_idx = inst_idx;
-                owner.model_instance_id = model_object->instances[inst_idx]->id().id;
-                owner.world_box = volume->transformed_bounding_box();
-                owner.title = "object" + std::to_string(obj_idx) + "_inst##" + std::to_string(inst_idx);
-                owner.label = model_object->name;
-                if (model_object->instances.size() > 1)
-                    owner.label += " (" + std::to_string(inst_idx + 1) + ")";
-                owner.selected = volume->selected;
-                owners.emplace_back(owner);
-            }
-        }
-    }
-
-    // updates print order strings
-    if (sorted_instances.size() > 1) {
-        for (size_t i = 0; i < sorted_instances.size(); ++i) {
-            size_t id = sorted_instances[i]->id().id;
-            std::vector<Owner>::iterator it = std::find_if(owners.begin(), owners.end(), [id](const Owner& owner) {
-                return owner.model_instance_id == id;
-                });
-            if (it != owners.end())
-                it->print_order = std::string((_(L("Seq."))).ToUTF8()) + "#: " + std::to_string(i + 1);
-        }
-    }
-
-    // calculate eye bounding boxes center zs
-    for (Owner& owner : owners) {
-        owner.eye_center_z = (world_to_eye * owner.world_box.center())(2);
-    }
-
-    // sort owners by center eye zs and selection
-    std::sort(owners.begin(), owners.end(), [](const Owner& owner1, const Owner& owner2) {
-        if (!owner1.selected && owner2.selected)
-            return true;
-        else if (owner1.selected && !owner2.selected)
-            return false;
-        else
-            return (owner1.eye_center_z < owner2.eye_center_z);
-        });
-
-    ImGuiWrapper& imgui = *wxGetApp().imgui();
-
-    // render info windows
-    for (const Owner& owner : owners) {
-        Vec3d screen_box_center = world_to_screen * owner.world_box.center();
-        float x = 0.0f;
-        float y = 0.0f;
-        if (camera.get_type() == Camera::EType::Perspective) {
-            x = (0.5f + 0.001f * 0.5f * (float)screen_box_center(0)) * viewport[2];
-            y = (0.5f - 0.001f * 0.5f * (float)screen_box_center(1)) * viewport[3];
-        } else {
-            x = (0.5f + 0.5f * (float)screen_box_center(0)) * viewport[2];
-            y = (0.5f - 0.5f * (float)screen_box_center(1)) * viewport[3];
-        }
-
-        if (x < 0.0f || viewport[2] < x || y < 0.0f || viewport[3] < y)
-            continue;
-
-        ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, owner.selected ? 3.0f : 1.5f);
-        ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
-        ImGui::PushStyleColor(ImGuiCol_Border, owner.selected ? ImVec4(0.757f, 0.404f, 0.216f, 1.0f) : ImVec4(0.75f, 0.75f, 0.75f, 1.0f));
-        imgui.set_next_window_pos(x, y, ImGuiCond_Always, 0.5f, 0.5f);
-        imgui.begin(owner.title, ImGuiWindowFlags_NoMouseInputs | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove);
-        ImGui::BringWindowToDisplayFront(ImGui::GetCurrentWindow());
-        float win_w = ImGui::GetWindowWidth();
-        float label_len = imgui.calc_text_size(owner.label).x;
-        ImGui::SetCursorPosX(0.5f * (win_w - label_len));
-        ImGui::AlignTextToFramePadding();
-        imgui.text(owner.label);
-
-        if (!owner.print_order.empty()) {
-            ImGui::Separator();
-            float po_len = imgui.calc_text_size(owner.print_order).x;
-            ImGui::SetCursorPosX(0.5f * (win_w - po_len));
-            ImGui::AlignTextToFramePadding();
-            imgui.text(owner.print_order);
-        }
-
-        // force re-render while the windows gets to its final size (it takes several frames)
-        if (ImGui::GetWindowContentRegionWidth() + 2.0f * ImGui::GetStyle().WindowPadding.x != ImGui::CalcWindowNextAutoFitSize(ImGui::GetCurrentWindow()).x)
-            imgui.set_requires_extra_frame();
-
-        imgui.end();
-        ImGui::PopStyleColor();
-        ImGui::PopStyleVar(2);
-    }
-}
-
-void GLCanvas3D::Tooltip::set_text(const std::string& text)
-{
-    // If the mouse is inside an ImGUI dialog, then the tooltip is suppressed.
-    m_text = m_in_imgui ? std::string() : text;
-}
-
-void GLCanvas3D::Tooltip::render(const Vec2d& mouse_position, GLCanvas3D& canvas)
-{
-    static ImVec2 size(0.0f, 0.0f);
-
-    auto validate_position = [](const Vec2d& position, const GLCanvas3D& canvas, const ImVec2& wnd_size) {
-        auto calc_cursor_height = []() {
-            float ret = 16.0f;
-#ifdef _WIN32
-            // see: https://forums.codeguru.com/showthread.php?449040-get-the-system-current-cursor-size
-            // this code is not perfect because it returns a maximum height equal to 31 even if the cursor bitmap shown on screen is bigger
-            // but at least it gives the same result as wxWidgets in the settings tabs
-            ICONINFO ii;
-            if (::GetIconInfo((HICON)GetCursor(), &ii) != 0) {
-                BITMAP bitmap;
-                ::GetObject(ii.hbmMask, sizeof(BITMAP), &bitmap);
-                int width = bitmap.bmWidth;
-                int height = (ii.hbmColor == nullptr) ? bitmap.bmHeight / 2 : bitmap.bmHeight;
-                HDC dc = ::CreateCompatibleDC(nullptr);
-                if (dc != nullptr) {
-                    if (::SelectObject(dc, ii.hbmMask) != nullptr) {
-                        for (int i = 0; i < width; ++i) {
-                            for (int j = 0; j < height; ++j) {
-                                if (::GetPixel(dc, i, j) != RGB(255, 255, 255)) {
-                                    if (ret < float(j))
-                                        ret = float(j);
-                                }
-                            }
-                        }
-                        ::DeleteDC(dc);
-                    }
-                }
-                ::DeleteObject(ii.hbmColor);
-                ::DeleteObject(ii.hbmMask);
-            }
-#endif //  _WIN32
-            return ret;
-        };
-
-        const Size cnv_size = canvas.get_canvas_size();
-        const float x = std::clamp(float(position.x()), 0.0f, float(cnv_size.get_width()) - wnd_size.x);
-        const float y = std::clamp(float(position.y()) + calc_cursor_height(), 0.0f, float(cnv_size.get_height()) - wnd_size.y);
-        return Vec2f(x, y);
-    };
-
-    if (m_text.empty()) {
-        m_start_time = std::chrono::steady_clock::now();
-        return;
-    }
-
-    // draw the tooltip as hidden until the delay is expired
-    // use a value of alpha slightly different from 0.0f because newer imgui does not calculate properly the window size if alpha == 0.0f
-    const float alpha = (std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - m_start_time).count() < 500) ? 0.01f : 1.0f;
-
-    const Vec2f position = validate_position(mouse_position, canvas, size);
-
-    ImGuiWrapper& imgui = *wxGetApp().imgui();
-    ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
-    ImGui::PushStyleVar(ImGuiStyleVar_Alpha, alpha);
-    imgui.set_next_window_pos(position.x(), position.y(), ImGuiCond_Always, 0.0f, 0.0f);
-
-    imgui.begin(wxString("canvas_tooltip"), ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMouseInputs | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoFocusOnAppearing);
-    ImGui::BringWindowToDisplayFront(ImGui::GetCurrentWindow());
-    ImGui::TextUnformatted(m_text.c_str());
-
-    // force re-render while the windows gets to its final size (it may take several frames) or while hidden
-    if (alpha < 1.0f || ImGui::GetWindowContentRegionWidth() + 2.0f * ImGui::GetStyle().WindowPadding.x != ImGui::CalcWindowNextAutoFitSize(ImGui::GetCurrentWindow()).x)
-        imgui.set_requires_extra_frame();
-
-    size = ImGui::GetWindowSize();
-
-    imgui.end();
-    ImGui::PopStyleVar(2);
-}
-
-void GLCanvas3D::SequentialPrintClearance::set_polygons(const Polygons& polygons)
-{
-    m_perimeter.reset();
-    m_fill.reset();
-    if (polygons.empty())
-        return;
-
-#if !ENABLE_LEGACY_OPENGL_REMOVAL
-    size_t triangles_count = 0;
-    for (const Polygon& poly : polygons) {
-        triangles_count += poly.points.size() - 2;
-    }
-    const size_t vertices_count = 3 * triangles_count;
-#endif // !ENABLE_LEGACY_OPENGL_REMOVAL
-
-    if (m_render_fill) {
-        GLModel::Geometry fill_data;
-#if ENABLE_LEGACY_OPENGL_REMOVAL
-        fill_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3 };
-        fill_data.color  = { 0.3333f, 0.0f, 0.0f, 0.5f };
-
-        // vertices + indices
-        const ExPolygons polygons_union = union_ex(polygons);
-        unsigned int vertices_counter = 0;
-        for (const ExPolygon& poly : polygons_union) {
-            const std::vector<Vec3d> triangulation = triangulate_expolygon_3d(poly);
-            fill_data.reserve_vertices(fill_data.vertices_count() + triangulation.size());
-            fill_data.reserve_indices(fill_data.indices_count() + triangulation.size());
-            for (const Vec3d& v : triangulation) {
-                fill_data.add_vertex((Vec3f)(v.cast<float>() + 0.0125f * Vec3f::UnitZ())); // add a small positive z to avoid z-fighting
-                ++vertices_counter;
-                if (vertices_counter % 3 == 0)
-                    fill_data.add_triangle(vertices_counter - 3, vertices_counter - 2, vertices_counter - 1);
-            }
-        }
-
-        m_fill.init_from(std::move(fill_data));
-#else
-        GLModel::Geometry::Entity entity;
-        entity.type = GLModel::EPrimitiveType::Triangles;
-        entity.color = { 0.3333f, 0.0f, 0.0f, 0.5f };
-        entity.positions.reserve(vertices_count);
-        entity.normals.reserve(vertices_count);
-        entity.indices.reserve(vertices_count);
-
-        const ExPolygons polygons_union = union_ex(polygons);
-        for (const ExPolygon& poly : polygons_union) {
-            const std::vector<Vec3d> triangulation = triangulate_expolygon_3d(poly);
-            for (const Vec3d& v : triangulation) {
-                entity.positions.emplace_back(v.cast<float>() + Vec3f(0.0f, 0.0f, 0.0125f)); // add a small positive z to avoid z-fighting
-                entity.normals.emplace_back(Vec3f::UnitZ());
-                const size_t positions_count = entity.positions.size();
-                if (positions_count % 3 == 0) {
-                    entity.indices.emplace_back(positions_count - 3);
-                    entity.indices.emplace_back(positions_count - 2);
-                    entity.indices.emplace_back(positions_count - 1);
-                }
-            }
-        }
-
-        fill_data.entities.emplace_back(entity);
-        m_fill.init_from(fill_data);
-#endif // ENABLE_LEGACY_OPENGL_REMOVAL
-    }
-
-#if ENABLE_LEGACY_OPENGL_REMOVAL
-    m_perimeter.init_from(polygons, 0.025f); // add a small positive z to avoid z-fighting
-#else
-    GLModel::Geometry perimeter_data;
-    for (const Polygon& poly : polygons) {
-        GLModel::Geometry::Entity ent;
-        ent.type = GLModel::EPrimitiveType::LineLoop;
-        ent.positions.reserve(poly.points.size());
-        ent.indices.reserve(poly.points.size());
-        unsigned int id_count = 0;
-        for (const Point& p : poly.points) {
-            ent.positions.emplace_back(unscale<float>(p.x()), unscale<float>(p.y()), 0.025f); // add a small positive z to avoid z-fighting
-            ent.normals.emplace_back(Vec3f::UnitZ());
-            ent.indices.emplace_back(id_count++);
-        }
-
-        perimeter_data.entities.emplace_back(ent);
-    }
-
-    m_perimeter.init_from(perimeter_data);
-#endif // ENABLE_LEGACY_OPENGL_REMOVAL
-}
-
-void GLCanvas3D::SequentialPrintClearance::render()
-{
-    const ColorRGBA FILL_COLOR    = { 1.0f, 0.0f, 0.0f, 0.5f };
-    const ColorRGBA NO_FILL_COLOR = { 1.0f, 1.0f, 1.0f, 0.75f };
-
-#if ENABLE_LEGACY_OPENGL_REMOVAL
-    GLShaderProgram* shader = wxGetApp().get_shader("flat");
-#else
-    GLShaderProgram* shader = wxGetApp().get_shader("gouraud_light");
-#endif // ENABLE_LEGACY_OPENGL_REMOVAL
-    if (shader == nullptr)
-        return;
-
-    shader->start_using();
-
-#if ENABLE_GL_SHADERS_ATTRIBUTES
-    const Camera& camera = wxGetApp().plater()->get_camera();
-    shader->set_uniform("view_model_matrix", camera.get_view_matrix());
-    shader->set_uniform("projection_matrix", camera.get_projection_matrix());
-#endif // ENABLE_GL_SHADERS_ATTRIBUTES
-
-    glsafe(::glEnable(GL_DEPTH_TEST));
-    glsafe(::glDisable(GL_CULL_FACE));
-    glsafe(::glEnable(GL_BLEND));
-    glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA));
-
-#if ENABLE_LEGACY_OPENGL_REMOVAL
-    m_perimeter.set_color(m_render_fill ? FILL_COLOR : NO_FILL_COLOR);
-#else
-    m_perimeter.set_color(-1, m_render_fill ? FILL_COLOR : NO_FILL_COLOR);
-#endif // ENABLE_LEGACY_OPENGL_REMOVAL
-    m_perimeter.render();
-    m_fill.render();
-
-    glsafe(::glDisable(GL_BLEND));
-    glsafe(::glEnable(GL_CULL_FACE));
-    glsafe(::glDisable(GL_DEPTH_TEST));
-
-    shader->stop_using();
-}
-
-wxDEFINE_EVENT(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS, SimpleEvent);
-wxDEFINE_EVENT(EVT_GLCANVAS_OBJECT_SELECT, SimpleEvent);
-wxDEFINE_EVENT(EVT_GLCANVAS_RIGHT_CLICK, RBtnEvent);
-wxDEFINE_EVENT(EVT_GLCANVAS_REMOVE_OBJECT, SimpleEvent);
-wxDEFINE_EVENT(EVT_GLCANVAS_ARRANGE, SimpleEvent);
-wxDEFINE_EVENT(EVT_GLCANVAS_SELECT_ALL, SimpleEvent);
-wxDEFINE_EVENT(EVT_GLCANVAS_QUESTION_MARK, SimpleEvent);
-wxDEFINE_EVENT(EVT_GLCANVAS_INCREASE_INSTANCES, Event<int>);
-wxDEFINE_EVENT(EVT_GLCANVAS_INSTANCE_MOVED, SimpleEvent);
-wxDEFINE_EVENT(EVT_GLCANVAS_INSTANCE_ROTATED, SimpleEvent);
-wxDEFINE_EVENT(EVT_GLCANVAS_INSTANCE_SCALED, SimpleEvent);
-wxDEFINE_EVENT(EVT_GLCANVAS_FORCE_UPDATE, SimpleEvent);
-wxDEFINE_EVENT(EVT_GLCANVAS_WIPETOWER_MOVED, Vec3dEvent);
-wxDEFINE_EVENT(EVT_GLCANVAS_WIPETOWER_ROTATED, Vec3dEvent);
-wxDEFINE_EVENT(EVT_GLCANVAS_ENABLE_ACTION_BUTTONS, Event<bool>);
-wxDEFINE_EVENT(EVT_GLCANVAS_UPDATE_GEOMETRY, Vec3dsEvent<2>);
-wxDEFINE_EVENT(EVT_GLCANVAS_MOUSE_DRAGGING_STARTED, SimpleEvent);
-wxDEFINE_EVENT(EVT_GLCANVAS_MOUSE_DRAGGING_FINISHED, SimpleEvent);
-wxDEFINE_EVENT(EVT_GLCANVAS_UPDATE_BED_SHAPE, SimpleEvent);
-wxDEFINE_EVENT(EVT_GLCANVAS_TAB, SimpleEvent);
-wxDEFINE_EVENT(EVT_GLCANVAS_RESETGIZMOS, SimpleEvent);
-wxDEFINE_EVENT(EVT_GLCANVAS_MOVE_SLIDERS, wxKeyEvent);
-wxDEFINE_EVENT(EVT_GLCANVAS_EDIT_COLOR_CHANGE, wxKeyEvent);
-wxDEFINE_EVENT(EVT_GLCANVAS_JUMP_TO, wxKeyEvent);
-wxDEFINE_EVENT(EVT_GLCANVAS_UNDO, SimpleEvent);
-wxDEFINE_EVENT(EVT_GLCANVAS_REDO, SimpleEvent);
-wxDEFINE_EVENT(EVT_GLCANVAS_COLLAPSE_SIDEBAR, SimpleEvent);
-wxDEFINE_EVENT(EVT_GLCANVAS_RESET_LAYER_HEIGHT_PROFILE, SimpleEvent);
-wxDEFINE_EVENT(EVT_GLCANVAS_ADAPTIVE_LAYER_HEIGHT_PROFILE, Event<float>);
-wxDEFINE_EVENT(EVT_GLCANVAS_SMOOTH_LAYER_HEIGHT_PROFILE, HeightProfileSmoothEvent);
-wxDEFINE_EVENT(EVT_GLCANVAS_RELOAD_FROM_DISK, SimpleEvent);
-wxDEFINE_EVENT(EVT_GLCANVAS_RENDER_TIMER, wxTimerEvent/*RenderTimerEvent*/);
-wxDEFINE_EVENT(EVT_GLCANVAS_TOOLBAR_HIGHLIGHTER_TIMER, wxTimerEvent);
-wxDEFINE_EVENT(EVT_GLCANVAS_GIZMO_HIGHLIGHTER_TIMER, wxTimerEvent);
-
-const double GLCanvas3D::DefaultCameraZoomToBoxMarginFactor = 1.25;
-
-void GLCanvas3D::load_arrange_settings()
-{
-    std::string dist_fff_str =
-        wxGetApp().app_config->get("arrange", "min_object_distance_fff");
-
-    std::string dist_fff_seq_print_str =
-        wxGetApp().app_config->get("arrange", "min_object_distance_fff_seq_print");
-
-    std::string dist_sla_str =
-        wxGetApp().app_config->get("arrange", "min_object_distance_sla");
-
-    std::string en_rot_fff_str =
-        wxGetApp().app_config->get("arrange", "enable_rotation_fff");
-
-    std::string en_rot_fff_seqp_str =
-        wxGetApp().app_config->get("arrange", "enable_rotation_fff_seq_print");
-
-    std::string en_rot_sla_str =
-        wxGetApp().app_config->get("arrange", "enable_rotation_sla");
-
-    if (!dist_fff_str.empty())
-        m_arrange_settings_fff.distance = std::stof(dist_fff_str);
-
-    if (!dist_fff_seq_print_str.empty())
-        m_arrange_settings_fff_seq_print.distance = std::stof(dist_fff_seq_print_str);
-
-    if (!dist_sla_str.empty())
-        m_arrange_settings_sla.distance = std::stof(dist_sla_str);
-
-    if (!en_rot_fff_str.empty())
-        m_arrange_settings_fff.enable_rotation = (en_rot_fff_str == "1" || en_rot_fff_str == "yes");
-
-    if (!en_rot_fff_seqp_str.empty())
-        m_arrange_settings_fff_seq_print.enable_rotation = (en_rot_fff_seqp_str == "1" || en_rot_fff_seqp_str == "yes");
-
-    if (!en_rot_sla_str.empty())
-        m_arrange_settings_sla.enable_rotation = (en_rot_sla_str == "1" || en_rot_sla_str == "yes");
-}
-
-PrinterTechnology GLCanvas3D::current_printer_technology() const
-{
-    return m_process->current_printer_technology();
-}
-
-GLCanvas3D::GLCanvas3D(wxGLCanvas* canvas, Bed3D &bed)
-    : m_canvas(canvas)
-    , m_context(nullptr)
-    , m_bed(bed)
-#if ENABLE_RETINA_GL
-    , m_retina_helper(nullptr)
-#endif
-    , m_in_render(false)
-    , m_main_toolbar(GLToolbar::Normal, "Main")
-    , m_undoredo_toolbar(GLToolbar::Normal, "Undo_Redo")
-    , m_gizmos(*this)
-    , m_use_clipping_planes(false)
-    , m_sidebar_field("")
-    , m_extra_frame_requested(false)
-    , m_config(nullptr)
-    , m_process(nullptr)
-    , m_model(nullptr)
-    , m_dirty(true)
-    , m_initialized(false)
-    , m_apply_zoom_to_volumes_filter(false)
-    , m_picking_enabled(false)
-    , m_moving_enabled(false)
-    , m_dynamic_background_enabled(false)
-    , m_multisample_allowed(false)
-    , m_moving(false)
-    , m_tab_down(false)
-    , m_cursor_type(Standard)
-    , m_reload_delayed(false)
-#if ENABLE_RENDER_PICKING_PASS
-    , m_show_picking_texture(false)
-#endif // ENABLE_RENDER_PICKING_PASS
-    , m_render_sla_auxiliaries(true)
-    , m_labels(*this)
-    , m_slope(m_volumes)
-{
-    if (m_canvas != nullptr) {
-        m_timer.SetOwner(m_canvas);
-        m_render_timer.SetOwner(m_canvas);
-#if ENABLE_RETINA_GL
-        m_retina_helper.reset(new RetinaHelper(canvas));
-#endif // ENABLE_RETINA_GL
-    }
-
-    load_arrange_settings();
-
-    m_selection.set_volumes(&m_volumes.volumes);
-}
-
-GLCanvas3D::~GLCanvas3D()
-{
-    reset_volumes();
-}
-
-void GLCanvas3D::post_event(wxEvent &&event)
-{
-    event.SetEventObject(m_canvas);
-    wxPostEvent(m_canvas, event);
-}
-
-bool GLCanvas3D::init()
-{
-    if (m_initialized)
-        return true;
-
-    if (m_canvas == nullptr || m_context == nullptr)
-        return false;
-
-    glsafe(::glClearColor(1.0f, 1.0f, 1.0f, 1.0f));
-    glsafe(::glClearDepth(1.0f));
-
-    glsafe(::glDepthFunc(GL_LESS));
-
-    glsafe(::glEnable(GL_DEPTH_TEST));
-    glsafe(::glEnable(GL_CULL_FACE));
-    glsafe(::glEnable(GL_BLEND));
-    glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA));
-
-#if !ENABLE_LEGACY_OPENGL_REMOVAL
-    // Set antialiasing / multisampling
-    glsafe(::glDisable(GL_LINE_SMOOTH));
-    glsafe(::glDisable(GL_POLYGON_SMOOTH));
-
-    // ambient lighting
-    GLfloat ambient[4] = { 0.3f, 0.3f, 0.3f, 1.0f };
-    glsafe(::glLightModelfv(GL_LIGHT_MODEL_AMBIENT, ambient));
-
-    glsafe(::glEnable(GL_LIGHT0));
-    glsafe(::glEnable(GL_LIGHT1));
-
-    // light from camera
-    GLfloat specular_cam[4] = { 0.3f, 0.3f, 0.3f, 1.0f };
-    glsafe(::glLightfv(GL_LIGHT1, GL_SPECULAR, specular_cam));
-    GLfloat diffuse_cam[4] = { 0.2f, 0.2f, 0.2f, 1.0f };
-    glsafe(::glLightfv(GL_LIGHT1, GL_DIFFUSE, diffuse_cam));
-
-    // light from above
-    GLfloat specular_top[4] = { 0.2f, 0.2f, 0.2f, 1.0f };
-    glsafe(::glLightfv(GL_LIGHT0, GL_SPECULAR, specular_top));
-    GLfloat diffuse_top[4] = { 0.5f, 0.5f, 0.5f, 1.0f };
-    glsafe(::glLightfv(GL_LIGHT0, GL_DIFFUSE, diffuse_top));
-
-    // Enables Smooth Color Shading; try GL_FLAT for (lack of) fun.
-    glsafe(::glShadeModel(GL_SMOOTH));
-
-    // A handy trick -- have surface material mirror the color.
-    glsafe(::glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE));
-    glsafe(::glEnable(GL_COLOR_MATERIAL));
-#endif // !ENABLE_LEGACY_OPENGL_REMOVAL
-
-    if (m_multisample_allowed)
-        glsafe(::glEnable(GL_MULTISAMPLE));
-
-    if (m_main_toolbar.is_enabled())
-        m_layers_editing.init();
-
-#if !ENABLE_LEGACY_OPENGL_REMOVAL
-    // on linux the gl context is not valid until the canvas is not shown on screen
-    // we defer the geometry finalization of volumes until the first call to render()
-    m_volumes.finalize_geometry(true);
-#endif // !ENABLE_LEGACY_OPENGL_REMOVAL
-
-    if (m_gizmos.is_enabled() && !m_gizmos.init())
-        std::cout << "Unable to initialize gizmos: please, check that all the required textures are available" << std::endl;
-
-    if (!_init_toolbars())
-        return false;
-
-    if (m_selection.is_enabled() && !m_selection.init())
-        return false;
-
-    m_initialized = true;
-
-    return true;
-}
-
-void GLCanvas3D::set_as_dirty()
-{
-    m_dirty = true;
-}
-
-unsigned int GLCanvas3D::get_volumes_count() const
-{
-    return (unsigned int)m_volumes.volumes.size();
-}
-
-void GLCanvas3D::reset_volumes()
-{
-    if (!m_initialized)
-        return;
-
-    if (m_volumes.empty())
-        return;
-
-    _set_current();
-
-    m_selection.clear();
-    m_volumes.clear();
-    m_dirty = true;
-
-    _set_warning_notification(EWarning::ObjectOutside, false);
-}
-
-ModelInstanceEPrintVolumeState GLCanvas3D::check_volumes_outside_state() const
-{
-    ModelInstanceEPrintVolumeState state = ModelInstanceEPrintVolumeState::ModelInstancePVS_Inside;
-    if (m_initialized)
-        m_volumes.check_outside_state(m_bed.build_volume(), &state);
-    return state;
-}
-
-void GLCanvas3D::toggle_sla_auxiliaries_visibility(bool visible, const ModelObject* mo, int instance_idx)
-{
-#if ENABLE_WIPETOWER_OBJECTID_1000_REMOVAL
-    if (current_printer_technology() != ptSLA)
-        return;
-#endif // ENABLE_WIPETOWER_OBJECTID_1000_REMOVAL
-
-    m_render_sla_auxiliaries = visible;
-
-    for (GLVolume* vol : m_volumes.volumes) {
-#if !ENABLE_WIPETOWER_OBJECTID_1000_REMOVAL
-        if (vol->composite_id.object_id == 1000)
-            continue; // the wipe tower
-#endif // !ENABLE_WIPETOWER_OBJECTID_1000_REMOVAL
-        if ((mo == nullptr || m_model->objects[vol->composite_id.object_id] == mo)
-        && (instance_idx == -1 || vol->composite_id.instance_id == instance_idx)
-        && vol->composite_id.volume_id < 0)
-            vol->is_active = visible;
-    }
-}
-
-void GLCanvas3D::toggle_model_objects_visibility(bool visible, const ModelObject* mo, int instance_idx, const ModelVolume* mv)
-{
-    for (GLVolume* vol : m_volumes.volumes) {
-#if ENABLE_WIPETOWER_OBJECTID_1000_REMOVAL
-        if (vol->is_wipe_tower)
-            vol->is_active = (visible && mo == nullptr);
-#else
-        if (vol->composite_id.object_id == 1000) { // wipe tower
-            vol->is_active = (visible && mo == nullptr);
-        }
-#endif // ENABLE_WIPETOWER_OBJECTID_1000_REMOVAL
-        else {
-            if ((mo == nullptr || m_model->objects[vol->composite_id.object_id] == mo)
-            && (instance_idx == -1 || vol->composite_id.instance_id == instance_idx)
-            && (mv == nullptr || m_model->objects[vol->composite_id.object_id]->volumes[vol->composite_id.volume_id] == mv)) {
-                vol->is_active = visible;
-
-                if (instance_idx == -1) {
-                    vol->force_native_color = false;
-                    vol->force_neutral_color = false;
-                } else {
-                    const GLGizmosManager& gm = get_gizmos_manager();
-                    auto gizmo_type = gm.get_current_type();
-                    if (    (gizmo_type == GLGizmosManager::FdmSupports
-                          || gizmo_type == GLGizmosManager::Seam)
-                        && ! vol->is_modifier)
-                        vol->force_neutral_color = true;
-                    else if (gizmo_type == GLGizmosManager::MmuSegmentation)
-                        vol->is_active = false;
-                    else
-                        vol->force_native_color = true;
-                }
-            }
-        }
-    }
-
-    if (visible && !mo)
-        toggle_sla_auxiliaries_visibility(true, mo, instance_idx);
-
-    if (!mo && !visible && !m_model->objects.empty() && (m_model->objects.size() > 1 || m_model->objects.front()->instances.size() > 1))
-        _set_warning_notification(EWarning::SomethingNotShown, true);
-
-    if (!mo && visible)
-        _set_warning_notification(EWarning::SomethingNotShown, false);
-}
-
-void GLCanvas3D::update_instance_printable_state_for_object(const size_t obj_idx)
-{
-    ModelObject* model_object = m_model->objects[obj_idx];
-    for (int inst_idx = 0; inst_idx < (int)model_object->instances.size(); ++inst_idx) {
-        ModelInstance* instance = model_object->instances[inst_idx];
-
-        for (GLVolume* volume : m_volumes.volumes) {
-            if (volume->object_idx() == (int)obj_idx && volume->instance_idx() == inst_idx)
-                volume->printable = instance->printable;
-        }
-    }
-}
-
-void GLCanvas3D::update_instance_printable_state_for_objects(const std::vector<size_t>& object_idxs)
-{
-    for (size_t obj_idx : object_idxs)
-        update_instance_printable_state_for_object(obj_idx);
-}
-
-void GLCanvas3D::set_config(const DynamicPrintConfig* config)
-{
-    m_config = config;
-    m_layers_editing.set_config(config);
-}
-
-void GLCanvas3D::set_process(BackgroundSlicingProcess *process)
-{
-    m_process = process;
-}
-
-void GLCanvas3D::set_model(Model* model)
-{
-    m_model = model;
-    m_selection.set_model(m_model);
-}
-
-void GLCanvas3D::bed_shape_changed()
-{
-    refresh_camera_scene_box();
-    wxGetApp().plater()->get_camera().requires_zoom_to_bed = true;
-    m_dirty = true;
-}
-
-void GLCanvas3D::refresh_camera_scene_box()
-{
-    wxGetApp().plater()->get_camera().set_scene_box(scene_bounding_box());
-}
-
-BoundingBoxf3 GLCanvas3D::volumes_bounding_box() const
-{
-    BoundingBoxf3 bb;
-    for (const GLVolume* volume : m_volumes.volumes) {
-        if (!m_apply_zoom_to_volumes_filter || ((volume != nullptr) && volume->zoom_to_volumes))
-            bb.merge(volume->transformed_bounding_box());
-    }
-    return bb;
-}
-
-BoundingBoxf3 GLCanvas3D::scene_bounding_box() const
-{
-    BoundingBoxf3 bb = volumes_bounding_box();
-    bb.merge(m_bed.extended_bounding_box());
-    double h = m_bed.build_volume().max_print_height();
-    //FIXME why -h?
-    bb.min.z() = std::min(bb.min.z(), -h);
-    bb.max.z() = std::max(bb.max.z(), h);
-    return bb;
-}
-
-bool GLCanvas3D::is_layers_editing_enabled() const
-{
-    return m_layers_editing.is_enabled();
-}
-
-bool GLCanvas3D::is_layers_editing_allowed() const
-{
-    return m_layers_editing.is_allowed();
-}
-
-void GLCanvas3D::reset_layer_height_profile()
-{
-    wxGetApp().plater()->take_snapshot(_L("Variable layer height - Reset"));
-    m_layers_editing.reset_layer_height_profile(*this);
-    m_layers_editing.state = LayersEditing::Completed;
-    m_dirty = true;
-}
-
-void GLCanvas3D::adaptive_layer_height_profile(float quality_factor)
-{
-    wxGetApp().plater()->take_snapshot(_L("Variable layer height - Adaptive"));
-    m_layers_editing.adaptive_layer_height_profile(*this, quality_factor);
-    m_layers_editing.state = LayersEditing::Completed;
-    m_dirty = true;
-}
-
-void GLCanvas3D::smooth_layer_height_profile(const HeightProfileSmoothingParams& smoothing_params)
-{
-    wxGetApp().plater()->take_snapshot(_L("Variable layer height - Smooth all"));
-    m_layers_editing.smooth_layer_height_profile(*this, smoothing_params);
-    m_layers_editing.state = LayersEditing::Completed;
-    m_dirty = true;
-}
-
-bool GLCanvas3D::is_reload_delayed() const
-{
-    return m_reload_delayed;
-}
-
-void GLCanvas3D::enable_layers_editing(bool enable)
-{
-    m_layers_editing.set_enabled(enable);
-    set_as_dirty();
-}
-
-void GLCanvas3D::enable_legend_texture(bool enable)
-{
-    m_gcode_viewer.enable_legend(enable);
-}
-
-void GLCanvas3D::enable_picking(bool enable)
-{
-    m_picking_enabled = enable;
-    m_selection.set_mode(Selection::Instance);
-}
-
-void GLCanvas3D::enable_moving(bool enable)
-{
-    m_moving_enabled = enable;
-}
-
-void GLCanvas3D::enable_gizmos(bool enable)
-{
-    m_gizmos.set_enabled(enable);
-}
-
-void GLCanvas3D::enable_selection(bool enable)
-{
-    m_selection.set_enabled(enable);
-}
-
-void GLCanvas3D::enable_main_toolbar(bool enable)
-{
-    m_main_toolbar.set_enabled(enable);
-}
-
-void GLCanvas3D::enable_undoredo_toolbar(bool enable)
-{
-    m_undoredo_toolbar.set_enabled(enable);
-}
-
-void GLCanvas3D::enable_dynamic_background(bool enable)
-{
-    m_dynamic_background_enabled = enable;
-}
-
-void GLCanvas3D::allow_multisample(bool allow)
-{
-    m_multisample_allowed = allow;
-}
-
-void GLCanvas3D::zoom_to_bed()
-{
-    BoundingBoxf3 box = m_bed.build_volume().bounding_volume();
-    box.min.z() = 0.0;
-    box.max.z() = 0.0;
-    _zoom_to_box(box);
-}
-
-void GLCanvas3D::zoom_to_volumes()
-{
-    m_apply_zoom_to_volumes_filter = true;
-    _zoom_to_box(volumes_bounding_box());
-    m_apply_zoom_to_volumes_filter = false;
-}
-
-void GLCanvas3D::zoom_to_selection()
-{
-    if (!m_selection.is_empty())
-        _zoom_to_box(m_selection.get_bounding_box());
-}
-
-void GLCanvas3D::zoom_to_gcode()
-{
-    _zoom_to_box(m_gcode_viewer.get_paths_bounding_box(), 1.05);
-}
-
-void GLCanvas3D::select_view(const std::string& direction)
-{
-    wxGetApp().plater()->get_camera().select_view(direction);
-    if (m_canvas != nullptr)
-        m_canvas->Refresh();
-}
-
-void GLCanvas3D::update_volumes_colors_by_extruder()
-{
-    if (m_config != nullptr)
-        m_volumes.update_colors_by_extruder(m_config);
-}
-
-void GLCanvas3D::render()
-{
-    if (m_in_render) {
-        // if called recursively, return
-        m_dirty = true;
-        return;
-    }
-
-    m_in_render = true;
-    Slic3r::ScopeGuard in_render_guard([this]() { m_in_render = false; });
-    (void)in_render_guard;
-
-    if (m_canvas == nullptr)
-        return;
-
-    // ensures this canvas is current and initialized
-    if (!_is_shown_on_screen() || !_set_current() || !wxGetApp().init_opengl())
-        return;
-
-    if (!is_initialized() && !init())
-        return;
-
-    if (!m_main_toolbar.is_enabled())
-        m_gcode_viewer.init();
-
-    if (! m_bed.build_volume().valid()) {
-        // this happens at startup when no data is still saved under <>\AppData\Roaming\Slic3rPE
-        post_event(SimpleEvent(EVT_GLCANVAS_UPDATE_BED_SHAPE));
-        return;
-    }
-
-#if ENABLE_ENVIRONMENT_MAP
-    if (wxGetApp().is_editor())
-        wxGetApp().plater()->init_environment_texture();
-#endif // ENABLE_ENVIRONMENT_MAP
-
-#if ENABLE_GLMODEL_STATISTICS
-    GLModel::reset_statistics_counters();
-#endif // ENABLE_GLMODEL_STATISTICS
-
-    const Size& cnv_size = get_canvas_size();
-    // Probably due to different order of events on Linux/GTK2, when one switched from 3D scene
-    // to preview, this was called before canvas had its final size. It reported zero width
-    // and the viewport was set incorrectly, leading to tripping glAsserts further down
-    // the road (in apply_projection). That's why the minimum size is forced to 10.
-    Camera& camera = wxGetApp().plater()->get_camera();
-    camera.apply_viewport(0, 0, std::max(10u, (unsigned int)cnv_size.get_width()), std::max(10u, (unsigned int)cnv_size.get_height()));
-
-    if (camera.requires_zoom_to_bed) {
-        zoom_to_bed();
-        _resize((unsigned int)cnv_size.get_width(), (unsigned int)cnv_size.get_height());
-        camera.requires_zoom_to_bed = false;
-    }
-
-#if !ENABLE_LEGACY_OPENGL_REMOVAL
-    camera.apply_view_matrix();
-#endif // !ENABLE_LEGACY_OPENGL_REMOVAL
-    camera.apply_projection(_max_bounding_box(true, true));
-
-#if !ENABLE_LEGACY_OPENGL_REMOVAL
-    GLfloat position_cam[4] = { 1.0f, 0.0f, 1.0f, 0.0f };
-    glsafe(::glLightfv(GL_LIGHT1, GL_POSITION, position_cam));
-    GLfloat position_top[4] = { -0.5f, -0.5f, 1.0f, 0.0f };
-    glsafe(::glLightfv(GL_LIGHT0, GL_POSITION, position_top));
-#endif // !ENABLE_LEGACY_OPENGL_REMOVAL
-
-    wxGetApp().imgui()->new_frame();
-
-    if (m_picking_enabled) {
-#if ENABLE_NEW_RECTANGLE_SELECTION
-        if (m_rectangle_selection.is_dragging() && !m_rectangle_selection.is_empty())
-#else
-        if (m_rectangle_selection.is_dragging())
-#endif // ENABLE_NEW_RECTANGLE_SELECTION
-            // picking pass using rectangle selection
-            _rectangular_selection_picking_pass();
-        else if (!m_volumes.empty())
-            // regular picking pass
-            _picking_pass();
-    }
-
-#if ENABLE_RENDER_PICKING_PASS
-    if (!m_picking_enabled || !m_show_picking_texture) {
-#endif // ENABLE_RENDER_PICKING_PASS
-
-    const bool is_looking_downward = camera.is_looking_downward();
-
-    // draw scene
-    glsafe(::glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT));
-    _render_background();
-
-    _render_objects(GLVolumeCollection::ERenderType::Opaque);
-    if (!m_main_toolbar.is_enabled())
-        _render_gcode();
-    _render_sla_slices();
-    _render_selection();
-    if (is_looking_downward)
-#if ENABLE_GL_SHADERS_ATTRIBUTES
-        _render_bed(camera.get_view_matrix(), camera.get_projection_matrix(), false, true);
-#else
-        _render_bed(false, true);
-#endif // ENABLE_GL_SHADERS_ATTRIBUTES
-    _render_objects(GLVolumeCollection::ERenderType::Transparent);
-
-    _render_sequential_clearance();
-#if ENABLE_RENDER_SELECTION_CENTER
-    _render_selection_center();
-#endif // ENABLE_RENDER_SELECTION_CENTER
-#if ENABLE_SHOW_TOOLPATHS_COG
-    if (!m_main_toolbar.is_enabled())
-        _render_gcode_cog();
-#endif // ENABLE_SHOW_TOOLPATHS_COG
-
-    // we need to set the mouse's scene position here because the depth buffer
-    // could be invalidated by the following gizmo render methods
-    // this position is used later into on_mouse() to drag the objects
-    if (m_picking_enabled)
-        m_mouse.scene_position = _mouse_to_3d(m_mouse.position.cast<coord_t>());
-
-    // sidebar hints need to be rendered before the gizmos because the depth buffer
-    // could be invalidated by the following gizmo render methods
-    _render_selection_sidebar_hints();
-    _render_current_gizmo();
-    if (!is_looking_downward)
-#if ENABLE_GL_SHADERS_ATTRIBUTES
-        _render_bed(camera.get_view_matrix(), camera.get_projection_matrix(), true, true);
-#else
-        _render_bed(true, true);
-#endif // ENABLE_GL_SHADERS_ATTRIBUTES
-#if ENABLE_RENDER_PICKING_PASS
-    }
-#endif // ENABLE_RENDER_PICKING_PASS
-
-#if ENABLE_SHOW_CAMERA_TARGET
-    _render_camera_target();
-#endif // ENABLE_SHOW_CAMERA_TARGET
-
-    if (m_picking_enabled && m_rectangle_selection.is_dragging())
-        m_rectangle_selection.render(*this);
-
-    // draw overlays
-    _render_overlays();
-
-    if (wxGetApp().plater()->is_render_statistic_dialog_visible()) {
-        ImGuiWrapper& imgui = *wxGetApp().imgui();
-        imgui.begin(std::string("Render statistics"), ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse);
-        imgui.text("FPS (SwapBuffers() calls per second):");
-        ImGui::SameLine();
-        imgui.text(std::to_string(m_render_stats.get_fps_and_reset_if_needed()));
-        ImGui::Separator();
-        imgui.text("Compressed textures:");
-        ImGui::SameLine();
-        imgui.text(OpenGLManager::are_compressed_textures_supported() ? "supported" : "not supported");
-        imgui.text("Max texture size:");
-        ImGui::SameLine();
-        imgui.text(std::to_string(OpenGLManager::get_gl_info().get_max_tex_size()));
-        imgui.end();
-    }
-
-#if ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW
-    if (wxGetApp().is_editor() && wxGetApp().plater()->is_view3D_shown())
-        wxGetApp().plater()->render_project_state_debug_window();
-#endif // ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW
-
-#if ENABLE_CAMERA_STATISTICS
-    camera.debug_render();
-#endif // ENABLE_CAMERA_STATISTICS
-#if ENABLE_GLMODEL_STATISTICS
-    GLModel::render_statistics();
-#endif // ENABLE_GLMODEL_STATISTICS
-
-    std::string tooltip;
-
-	// Negative coordinate means out of the window, likely because the window was deactivated.
-	// In that case the tooltip should be hidden.
-    if (m_mouse.position.x() >= 0. && m_mouse.position.y() >= 0.) {
-	    if (tooltip.empty())
-	        tooltip = m_layers_editing.get_tooltip(*this);
-
-	    if (tooltip.empty())
-	        tooltip = m_gizmos.get_tooltip();
-
-	    if (tooltip.empty())
-	        tooltip = m_main_toolbar.get_tooltip();
-
-	    if (tooltip.empty())
-	        tooltip = m_undoredo_toolbar.get_tooltip();
-
-	    if (tooltip.empty())
-            tooltip = wxGetApp().plater()->get_collapse_toolbar().get_tooltip();
-
-	    if (tooltip.empty())
-            tooltip = wxGetApp().plater()->get_view_toolbar().get_tooltip();
-    }
-
-    set_tooltip(tooltip);
-
-    if (m_tooltip_enabled)
-        m_tooltip.render(m_mouse.position, *this);
-
-    wxGetApp().plater()->get_mouse3d_controller().render_settings_dialog(*this);
-
-    wxGetApp().plater()->get_notification_manager()->render_notifications(*this, get_overlay_window_width());
-
-    wxGetApp().imgui()->render();
-
-    m_canvas->SwapBuffers();
-    m_render_stats.increment_fps_counter();
-}
-
-void GLCanvas3D::render_thumbnail(ThumbnailData& thumbnail_data, unsigned int w, unsigned int h, const ThumbnailsParams& thumbnail_params, Camera::EType camera_type)
-{
-    render_thumbnail(thumbnail_data, w, h, thumbnail_params, m_volumes, camera_type);
-}
-
-void GLCanvas3D::render_thumbnail(ThumbnailData& thumbnail_data, unsigned int w, unsigned int h, const ThumbnailsParams& thumbnail_params, const GLVolumeCollection& volumes, Camera::EType camera_type)
-{
-    switch (OpenGLManager::get_framebuffers_type())
-    {
-    case OpenGLManager::EFramebufferType::Arb: { _render_thumbnail_framebuffer(thumbnail_data, w, h, thumbnail_params, volumes, camera_type); break; }
-    case OpenGLManager::EFramebufferType::Ext: { _render_thumbnail_framebuffer_ext(thumbnail_data, w, h, thumbnail_params, volumes, camera_type); break; }
-    default: { _render_thumbnail_legacy(thumbnail_data, w, h, thumbnail_params, volumes, camera_type); break; }
-    }
-}
-
-void GLCanvas3D::select_all()
-{
-    m_selection.add_all();
-    m_dirty = true;
-}
-
-void GLCanvas3D::deselect_all()
-{
-    m_selection.remove_all();
-    wxGetApp().obj_manipul()->set_dirty();
-    m_gizmos.reset_all_states();
-    m_gizmos.update_data();
-    post_event(SimpleEvent(EVT_GLCANVAS_OBJECT_SELECT));
-}
-
-void GLCanvas3D::delete_selected()
-{
-    m_selection.erase();
-}
-
-void GLCanvas3D::ensure_on_bed(unsigned int object_idx, bool allow_negative_z)
-{
-    if (allow_negative_z)
-        return;
-
-    typedef std::map<std::pair<int, int>, double> InstancesToZMap;
-    InstancesToZMap instances_min_z;
-
-    for (GLVolume* volume : m_volumes.volumes) {
-        if (volume->object_idx() == (int)object_idx && !volume->is_modifier) {
-            double min_z = volume->transformed_convex_hull_bounding_box().min.z();
-            std::pair<int, int> instance = std::make_pair(volume->object_idx(), volume->instance_idx());
-            InstancesToZMap::iterator it = instances_min_z.find(instance);
-            if (it == instances_min_z.end())
-                it = instances_min_z.insert(InstancesToZMap::value_type(instance, DBL_MAX)).first;
-
-            it->second = std::min(it->second, min_z);
-        }
-    }
-
-    for (GLVolume* volume : m_volumes.volumes) {
-        std::pair<int, int> instance = std::make_pair(volume->object_idx(), volume->instance_idx());
-        InstancesToZMap::iterator it = instances_min_z.find(instance);
-        if (it != instances_min_z.end())
-            volume->set_instance_offset(Z, volume->get_instance_offset(Z) - it->second);
-    }
-}
-
-
-const std::vector<double>& GLCanvas3D::get_gcode_layers_zs() const
-{
-    return m_gcode_viewer.get_layers_zs();
-}
-
-std::vector<double> GLCanvas3D::get_volumes_print_zs(bool active_only) const
-{
-    return m_volumes.get_current_print_zs(active_only);
-}
-
-void GLCanvas3D::set_gcode_options_visibility_from_flags(unsigned int flags)
-{
-    m_gcode_viewer.set_options_visibility_from_flags(flags);
-}
-
-void GLCanvas3D::set_toolpath_role_visibility_flags(unsigned int flags)
-{
-    m_gcode_viewer.set_toolpath_role_visibility_flags(flags);
-}
-
-void GLCanvas3D::set_toolpath_view_type(GCodeViewer::EViewType type)
-{
-    m_gcode_viewer.set_view_type(type);
-}
-
-void GLCanvas3D::set_volumes_z_range(const std::array<double, 2>& range)
-{
-    m_volumes.set_range(range[0] - 1e-6, range[1] + 1e-6);
-}
-
-void GLCanvas3D::set_toolpaths_z_range(const std::array<unsigned int, 2>& range)
-{
-    if (m_gcode_viewer.has_data())
-        m_gcode_viewer.set_layers_z_range(range);
-}
-
-std::vector<int> GLCanvas3D::load_object(const ModelObject& model_object, int obj_idx, std::vector<int> instance_idxs)
-{
-    if (instance_idxs.empty()) {
-        for (unsigned int i = 0; i < model_object.instances.size(); ++i) {
-            instance_idxs.emplace_back(i);
-        }
-    }
-#if ENABLE_LEGACY_OPENGL_REMOVAL
-    return m_volumes.load_object(&model_object, obj_idx, instance_idxs);
-#else
-    return m_volumes.load_object(&model_object, obj_idx, instance_idxs, m_initialized);
-#endif // ENABLE_LEGACY_OPENGL_REMOVAL
-}
-
-std::vector<int> GLCanvas3D::load_object(const Model& model, int obj_idx)
-{
-    if (0 <= obj_idx && obj_idx < (int)model.objects.size()) {
-        const ModelObject* model_object = model.objects[obj_idx];
-        if (model_object != nullptr)
-            return load_object(*model_object, obj_idx, std::vector<int>());
-    }
-
-    return std::vector<int>();
-}
-
-void GLCanvas3D::mirror_selection(Axis axis)
-{
-    m_selection.mirror(axis);
-    do_mirror(L("Mirror Object"));
-    wxGetApp().obj_manipul()->set_dirty();
-}
-
-// Reload the 3D scene of 
-// 1) Model / ModelObjects / ModelInstances / ModelVolumes
-// 2) Print bed
-// 3) SLA support meshes for their respective ModelObjects / ModelInstances
-// 4) Wipe tower preview
-// 5) Out of bed collision status & message overlay (texture)
-void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_refresh)
-{
-    if (m_canvas == nullptr || m_config == nullptr || m_model == nullptr)
-        return;
-
-    if (!m_initialized)
-        return;
-    
-    _set_current();
-
-    m_hover_volume_idxs.clear();
-
-    struct ModelVolumeState {
-        ModelVolumeState(const GLVolume* volume) :
-            model_volume(nullptr), geometry_id(volume->geometry_id), volume_idx(-1) {}
-        ModelVolumeState(const ModelVolume* model_volume, const ObjectID& instance_id, const GLVolume::CompositeID& composite_id) :
-            model_volume(model_volume), geometry_id(std::make_pair(model_volume->id().id, instance_id.id)), composite_id(composite_id), volume_idx(-1) {}
-        ModelVolumeState(const ObjectID& volume_id, const ObjectID& instance_id) :
-            model_volume(nullptr), geometry_id(std::make_pair(volume_id.id, instance_id.id)), volume_idx(-1) {}
-        bool new_geometry() const { return this->volume_idx == size_t(-1); }
-        const ModelVolume* model_volume;
-        // ObjectID of ModelVolume + ObjectID of ModelInstance
-        // or timestamp of an SLAPrintObjectStep + ObjectID of ModelInstance
-        std::pair<size_t, size_t>   geometry_id;
-        GLVolume::CompositeID       composite_id;
-        // Volume index in the new GLVolume vector.
-        size_t                      volume_idx;
-    };
-    std::vector<ModelVolumeState> model_volume_state;
-    std::vector<ModelVolumeState> aux_volume_state;
-
-    struct GLVolumeState {
-        GLVolumeState() :
-            volume_idx(size_t(-1)) {}
-        GLVolumeState(const GLVolume* volume, unsigned int volume_idx) :
-            composite_id(volume->composite_id), volume_idx(volume_idx) {}
-        GLVolumeState(const GLVolume::CompositeID &composite_id) :
-            composite_id(composite_id), volume_idx(size_t(-1)) {}
-
-        GLVolume::CompositeID       composite_id;
-        // Volume index in the old GLVolume vector.
-        size_t                      volume_idx;
-    };
-
-    // SLA steps to pull the preview meshes for.
-	typedef std::array<SLAPrintObjectStep, 3> SLASteps;
-    SLASteps sla_steps = { slaposDrillHoles, slaposSupportTree, slaposPad };
-    struct SLASupportState {
-        std::array<PrintStateBase::StateWithTimeStamp, std::tuple_size<SLASteps>::value> step;
-    };
-    // State of the sla_steps for all SLAPrintObjects.
-    std::vector<SLASupportState>   sla_support_state;
-
-    std::vector<size_t> instance_ids_selected;
-    std::vector<size_t> map_glvolume_old_to_new(m_volumes.volumes.size(), size_t(-1));
-    std::vector<GLVolumeState> deleted_volumes;
-    std::vector<GLVolume*> glvolumes_new;
-    glvolumes_new.reserve(m_volumes.volumes.size());
-    auto model_volume_state_lower = [](const ModelVolumeState& m1, const ModelVolumeState& m2) { return m1.geometry_id < m2.geometry_id; };
-
-    m_reload_delayed = !m_canvas->IsShown() && !refresh_immediately && !force_full_scene_refresh;
-
-    PrinterTechnology printer_technology = current_printer_technology();
-    int               volume_idx_wipe_tower_old = -1;
-
-    // Release invalidated volumes to conserve GPU memory in case of delayed refresh (see m_reload_delayed).
-    // First initialize model_volumes_new_sorted & model_instances_new_sorted.
-    for (int object_idx = 0; object_idx < (int)m_model->objects.size(); ++object_idx) {
-        const ModelObject* model_object = m_model->objects[object_idx];
-        for (int instance_idx = 0; instance_idx < (int)model_object->instances.size(); ++instance_idx) {
-            const ModelInstance* model_instance = model_object->instances[instance_idx];
-            for (int volume_idx = 0; volume_idx < (int)model_object->volumes.size(); ++volume_idx) {
-                const ModelVolume* model_volume = model_object->volumes[volume_idx];
-                model_volume_state.emplace_back(model_volume, model_instance->id(), GLVolume::CompositeID(object_idx, volume_idx, instance_idx));
-            }
-        }
-    }
-    if (printer_technology == ptSLA) {
-        const SLAPrint* sla_print = this->sla_print();
-#ifndef NDEBUG
-        // Verify that the SLAPrint object is synchronized with m_model.
-        check_model_ids_equal(*m_model, sla_print->model());
-#endif /* NDEBUG */
-        sla_support_state.reserve(sla_print->objects().size());
-        for (const SLAPrintObject* print_object : sla_print->objects()) {
-            SLASupportState state;
-            for (size_t istep = 0; istep < sla_steps.size(); ++istep) {
-                state.step[istep] = print_object->step_state_with_timestamp(sla_steps[istep]);
-                if (state.step[istep].state == PrintStateBase::DONE) {
-                    if (!print_object->has_mesh(sla_steps[istep]))
-                        // Consider the DONE step without a valid mesh as invalid for the purpose
-                        // of mesh visualization.
-                        state.step[istep].state = PrintStateBase::INVALID;
-                    else if (sla_steps[istep] != slaposDrillHoles)
-                        for (const ModelInstance* model_instance : print_object->model_object()->instances)
-                            // Only the instances, which are currently printable, will have the SLA support structures kept.
-                            // The instances outside the print bed will have the GLVolumes of their support structures released.
-                            if (model_instance->is_printable())
-                                aux_volume_state.emplace_back(state.step[istep].timestamp, model_instance->id());
-                }
-            }
-            sla_support_state.emplace_back(state);
-        }
-    }
-    std::sort(model_volume_state.begin(), model_volume_state.end(), model_volume_state_lower);
-    std::sort(aux_volume_state.begin(), aux_volume_state.end(), model_volume_state_lower);
-    // Release all ModelVolume based GLVolumes not found in the current Model. Find the GLVolume of a hollowed mesh.
-    for (size_t volume_id = 0; volume_id < m_volumes.volumes.size(); ++volume_id) {
-        GLVolume* volume = m_volumes.volumes[volume_id];
-        ModelVolumeState  key(volume);
-        ModelVolumeState* mvs = nullptr;
-        if (volume->volume_idx() < 0) {
-            auto it = std::lower_bound(aux_volume_state.begin(), aux_volume_state.end(), key, model_volume_state_lower);
-            if (it != aux_volume_state.end() && it->geometry_id == key.geometry_id)
-                // This can be an SLA support structure that should not be rendered (in case someone used undo
-                // to revert to before it was generated). We only reuse the volume if that's not the case.
-                if (m_model->objects[volume->composite_id.object_id]->sla_points_status != sla::PointsStatus::NoPoints)
-                    mvs = &(*it);
-        }
-        else {
-            auto it = std::lower_bound(model_volume_state.begin(), model_volume_state.end(), key, model_volume_state_lower);
-            if (it != model_volume_state.end() && it->geometry_id == key.geometry_id)
-                mvs = &(*it);
-        }
-        // Emplace instance ID of the volume. Both the aux volumes and model volumes share the same instance ID.
-        // The wipe tower has its own wipe_tower_instance_id().
-        if (m_selection.contains_volume(volume_id))
-            instance_ids_selected.emplace_back(volume->geometry_id.second);
-        if (mvs == nullptr || force_full_scene_refresh) {
-            // This GLVolume will be released.
-            if (volume->is_wipe_tower) {
-                // There is only one wipe tower.
-                assert(volume_idx_wipe_tower_old == -1);
-                volume_idx_wipe_tower_old = (int)volume_id;
-            }
-            if (!m_reload_delayed) {
-                deleted_volumes.emplace_back(volume, volume_id);
-                delete volume;
-            }
-        }
-        else {
-            // This GLVolume will be reused.
-            volume->set_sla_shift_z(0.0);
-            map_glvolume_old_to_new[volume_id] = glvolumes_new.size();
-            mvs->volume_idx = glvolumes_new.size();
-            glvolumes_new.emplace_back(volume);
-            // Update color of the volume based on the current extruder.
-            if (mvs->model_volume != nullptr) {
-                int extruder_id = mvs->model_volume->extruder_id();
-                if (extruder_id != -1)
-                    volume->extruder_id = extruder_id;
-
-                volume->is_modifier = !mvs->model_volume->is_model_part();
-                volume->set_color(color_from_model_volume(*mvs->model_volume));
-                // force update of render_color alpha channel 
-                volume->set_render_color(volume->color.is_transparent());
-
-                // updates volumes transformations
-                volume->set_instance_transformation(mvs->model_volume->get_object()->instances[mvs->composite_id.instance_id]->get_transformation());
-                volume->set_volume_transformation(mvs->model_volume->get_transformation());
-
-                // updates volumes convex hull
-                if (mvs->model_volume->is_model_part() && ! volume->convex_hull())
-                    // Model volume was likely changed from modifier or support blocker / enforcer to a model part.
-                    // Only model parts require convex hulls.
-                    volume->set_convex_hull(mvs->model_volume->get_convex_hull_shared_ptr());
-            }
-        }
-    }
-    sort_remove_duplicates(instance_ids_selected);
-    auto deleted_volumes_lower = [](const GLVolumeState &v1, const GLVolumeState &v2) { return v1.composite_id < v2.composite_id; };
-    std::sort(deleted_volumes.begin(), deleted_volumes.end(), deleted_volumes_lower);
-
-    if (m_reload_delayed)
-        return;
-
-    bool update_object_list = false;
-    if (m_volumes.volumes != glvolumes_new)
-		update_object_list = true;
-    m_volumes.volumes = std::move(glvolumes_new);
-    for (unsigned int obj_idx = 0; obj_idx < (unsigned int)m_model->objects.size(); ++ obj_idx) {
-        const ModelObject &model_object = *m_model->objects[obj_idx];
-        for (int volume_idx = 0; volume_idx < (int)model_object.volumes.size(); ++ volume_idx) {
-			const ModelVolume &model_volume = *model_object.volumes[volume_idx];
-            for (int instance_idx = 0; instance_idx < (int)model_object.instances.size(); ++ instance_idx) {
-				const ModelInstance &model_instance = *model_object.instances[instance_idx];
-				ModelVolumeState key(model_volume.id(), model_instance.id());
-				auto it = std::lower_bound(model_volume_state.begin(), model_volume_state.end(), key, model_volume_state_lower);
-				assert(it != model_volume_state.end() && it->geometry_id == key.geometry_id);
-                if (it->new_geometry()) {
-                    // New volume.
-                    auto it_old_volume = std::lower_bound(deleted_volumes.begin(), deleted_volumes.end(), GLVolumeState(it->composite_id), deleted_volumes_lower);
-                    if (it_old_volume != deleted_volumes.end() && it_old_volume->composite_id == it->composite_id)
-                        // If a volume changed its ObjectID, but it reuses a GLVolume's CompositeID, maintain its selection.
-                        map_glvolume_old_to_new[it_old_volume->volume_idx] = m_volumes.volumes.size();
-                    // Note the index of the loaded volume, so that we can reload the main model GLVolume with the hollowed mesh
-                    // later in this function.
-                    it->volume_idx = m_volumes.volumes.size();
-#if ENABLE_LEGACY_OPENGL_REMOVAL
-                    m_volumes.load_object_volume(&model_object, obj_idx, volume_idx, instance_idx);
-#else
-                    m_volumes.load_object_volume(&model_object, obj_idx, volume_idx, instance_idx, m_initialized);
-#endif // ENABLE_LEGACY_OPENGL_REMOVAL
-                    m_volumes.volumes.back()->geometry_id = key.geometry_id;
-                    update_object_list = true;
-                } else {
-					// Recycling an old GLVolume.
-					GLVolume &existing_volume = *m_volumes.volumes[it->volume_idx];
-                    assert(existing_volume.geometry_id == key.geometry_id);
-					// Update the Object/Volume/Instance indices into the current Model.
-					if (existing_volume.composite_id != it->composite_id) {
-						existing_volume.composite_id = it->composite_id;
-						update_object_list = true;
-					}
-                }
-            }
-        }
-    }
-    if (printer_technology == ptSLA) {
-        size_t idx = 0;
-        const SLAPrint *sla_print = this->sla_print();
-		std::vector<double> shift_zs(m_model->objects.size(), 0);
-        double relative_correction_z = sla_print->relative_correction().z();
-        if (relative_correction_z <= EPSILON)
-            relative_correction_z = 1.;
-		for (const SLAPrintObject *print_object : sla_print->objects()) {
-            SLASupportState   &state        = sla_support_state[idx ++];
-            const ModelObject *model_object = print_object->model_object();
-            // Find an index of the ModelObject
-            int object_idx;
-            // There may be new SLA volumes added to the scene for this print_object.
-            // Find the object index of this print_object in the Model::objects list.
-            auto it = std::find(sla_print->model().objects.begin(), sla_print->model().objects.end(), model_object);
-            assert(it != sla_print->model().objects.end());
-			object_idx = it - sla_print->model().objects.begin();
-			// Cache the Z offset to be applied to all volumes with this object_idx.
-			shift_zs[object_idx] = print_object->get_current_elevation() / relative_correction_z;
-            // Collect indices of this print_object's instances, for which the SLA support meshes are to be added to the scene.
-            // pairs of <instance_idx, print_instance_idx>
-			std::vector<std::pair<size_t, size_t>> instances[std::tuple_size<SLASteps>::value];
-            for (size_t print_instance_idx = 0; print_instance_idx < print_object->instances().size(); ++ print_instance_idx) {
-                const SLAPrintObject::Instance &instance = print_object->instances()[print_instance_idx];
-                // Find index of ModelInstance corresponding to this SLAPrintObject::Instance.
-				auto it = std::find_if(model_object->instances.begin(), model_object->instances.end(), 
-                    [&instance](const ModelInstance *mi) { return mi->id() == instance.instance_id; });
-                assert(it != model_object->instances.end());
-                int instance_idx = it - model_object->instances.begin();
-                for (size_t istep = 0; istep < sla_steps.size(); ++ istep)
-                    if (sla_steps[istep] == slaposDrillHoles) {
-                    	// Hollowing is a special case, where the mesh from the backend is being loaded into the 1st volume of an instance,
-                    	// not into its own GLVolume.
-                        // There shall always be such a GLVolume allocated.
-                        ModelVolumeState key(model_object->volumes.front()->id(), instance.instance_id);
-                        auto it = std::lower_bound(model_volume_state.begin(), model_volume_state.end(), key, model_volume_state_lower);
-                        assert(it != model_volume_state.end() && it->geometry_id == key.geometry_id);
-                        assert(!it->new_geometry());
-                        GLVolume &volume = *m_volumes.volumes[it->volume_idx];
-                        if (! volume.offsets.empty() && state.step[istep].timestamp != volume.offsets.front()) {
-                        	// The backend either produced a new hollowed mesh, or it invalidated the one that the front end has seen.
-#if ENABLE_LEGACY_OPENGL_REMOVAL
-                            volume.model.reset();
-#else
-                            volume.indexed_vertex_array.release_geometry();
-#endif // ENABLE_LEGACY_OPENGL_REMOVAL
-                            if (state.step[istep].state == PrintStateBase::DONE) {
-                                TriangleMesh mesh = print_object->get_mesh(slaposDrillHoles);
-	                            assert(! mesh.empty());
-
-                                // sla_trafo does not contain volume trafo. To get a mesh to create
-                                // a new volume from, we have to apply vol trafo inverse separately.
-                                const ModelObject& mo = *m_model->objects[volume.object_idx()];
-                                Transform3d trafo = sla_print->sla_trafo(mo)
-                                    * mo.volumes.front()->get_transformation().get_matrix();
-                                mesh.transform(trafo.inverse());
-#if ENABLE_SMOOTH_NORMALS
-#if ENABLE_LEGACY_OPENGL_REMOVAL
-                                volume.model.init_from(mesh, true);
-#else
-                                volume.indexed_vertex_array.load_mesh(mesh, true);
-#endif // ENABLE_LEGACY_OPENGL_REMOVAL
-#else
-#if ENABLE_LEGACY_OPENGL_REMOVAL
-                                volume.model.init_from(mesh);
-#else
-                                volume.indexed_vertex_array.load_mesh(mesh);
-#endif // ENABLE_LEGACY_OPENGL_REMOVAL
-#endif // ENABLE_SMOOTH_NORMALS
-                            }
-                            else {
-	                        	// Reload the original volume.
-#if ENABLE_SMOOTH_NORMALS
-#if ENABLE_LEGACY_OPENGL_REMOVAL
-                                volume.model.init_from(m_model->objects[volume.object_idx()]->volumes[volume.volume_idx()]->mesh(), true);
-#else
-                                volume.indexed_vertex_array.load_mesh(m_model->objects[volume.object_idx()]->volumes[volume.volume_idx()]->mesh(), true);
-#endif // ENABLE_LEGACY_OPENGL_REMOVAL
-#else
-#if ENABLE_LEGACY_OPENGL_REMOVAL
-                                volume.model.init_from(m_model->objects[volume.object_idx()]->volumes[volume.volume_idx()]->mesh());
-#else
-                                volume.indexed_vertex_array.load_mesh(m_model->objects[volume.object_idx()]->volumes[volume.volume_idx()]->mesh());
-#endif // ENABLE_LEGACY_OPENGL_REMOVAL
-#endif // ENABLE_SMOOTH_NORMALS
-                            }
-#if !ENABLE_LEGACY_OPENGL_REMOVAL
-                            volume.finalize_geometry(true);
-#endif // !ENABLE_LEGACY_OPENGL_REMOVAL
-                        }
-                    	//FIXME it is an ugly hack to write the timestamp into the "offsets" field to not have to add another member variable
-                    	// to the GLVolume. We should refactor GLVolume significantly, so that the GLVolume will not contain member variables
-                    	// of various concenrs (model vs. 3D print path).
-                    	volume.offsets = { state.step[istep].timestamp };
-                    }
-                    else if (state.step[istep].state == PrintStateBase::DONE) {
-                        // Check whether there is an existing auxiliary volume to be updated, or a new auxiliary volume to be created.
-						ModelVolumeState key(state.step[istep].timestamp, instance.instance_id.id);
-						auto it = std::lower_bound(aux_volume_state.begin(), aux_volume_state.end(), key, model_volume_state_lower);
-						assert(it != aux_volume_state.end() && it->geometry_id == key.geometry_id);
-                    	if (it->new_geometry()) {
-                            // This can be an SLA support structure that should not be rendered (in case someone used undo
-                            // to revert to before it was generated). If that's the case, we should not generate anything.
-                            if (model_object->sla_points_status != sla::PointsStatus::NoPoints)
-                                instances[istep].emplace_back(std::pair<size_t, size_t>(instance_idx, print_instance_idx));
-                            else
-                                shift_zs[object_idx] = 0.;
-                        }
-                        else {
-                            // Recycling an old GLVolume. Update the Object/Instance indices into the current Model.
-                            m_volumes.volumes[it->volume_idx]->composite_id = GLVolume::CompositeID(object_idx, m_volumes.volumes[it->volume_idx]->volume_idx(), instance_idx);
-                            m_volumes.volumes[it->volume_idx]->set_instance_transformation(model_object->instances[instance_idx]->get_transformation());
-                        }
-                    }
-            }
-
-            for (size_t istep = 0; istep < sla_steps.size(); ++istep)
-                if (!instances[istep].empty())
-#if ENABLE_LEGACY_OPENGL_REMOVAL
-                    m_volumes.load_object_auxiliary(print_object, object_idx, instances[istep], sla_steps[istep], state.step[istep].timestamp);
-#else
-                    m_volumes.load_object_auxiliary(print_object, object_idx, instances[istep], sla_steps[istep], state.step[istep].timestamp, m_initialized);
-#endif // ENABLE_LEGACY_OPENGL_REMOVAL
-        }
-
-		// Shift-up all volumes of the object so that it has the right elevation with respect to the print bed
-		for (GLVolume* volume : m_volumes.volumes)
-			if (volume->object_idx() < (int)m_model->objects.size() && m_model->objects[volume->object_idx()]->instances[volume->instance_idx()]->is_printable())
-				volume->set_sla_shift_z(shift_zs[volume->object_idx()]);
-    }
-
-    if (printer_technology == ptFFF && m_config->has("nozzle_diameter")) {
-        // Should the wipe tower be visualized ?
-        unsigned int extruders_count = (unsigned int)dynamic_cast<const ConfigOptionFloats*>(m_config->option("nozzle_diameter"))->values.size();
-
-        bool wt = dynamic_cast<const ConfigOptionBool*>(m_config->option("wipe_tower"))->value;
-        bool co = dynamic_cast<const ConfigOptionBool*>(m_config->option("complete_objects"))->value;
-
-        if (extruders_count > 1 && wt && !co) {
-            // Height of a print (Show at least a slab)
-            double height = std::max(m_model->bounding_box().max(2), 10.0);
-
-            float x = dynamic_cast<const ConfigOptionFloat*>(m_config->option("wipe_tower_x"))->value;
-            float y = dynamic_cast<const ConfigOptionFloat*>(m_config->option("wipe_tower_y"))->value;
-            float w = dynamic_cast<const ConfigOptionFloat*>(m_config->option("wipe_tower_width"))->value;
-            float a = dynamic_cast<const ConfigOptionFloat*>(m_config->option("wipe_tower_rotation_angle"))->value;
-
-            const Print *print = m_process->fff_print();
-
-            float depth = print->wipe_tower_data(extruders_count).depth;
-            float brim_width = print->wipe_tower_data(extruders_count).brim_width;
-
-#if ENABLE_LEGACY_OPENGL_REMOVAL
-#if ENABLE_WIPETOWER_OBJECTID_1000_REMOVAL
-            int volume_idx_wipe_tower_new = m_volumes.load_wipe_tower_preview(
-                x, y, w, depth, (float)height, a, !print->is_step_done(psWipeTower),
-                brim_width);
-#else
-            int volume_idx_wipe_tower_new = m_volumes.load_wipe_tower_preview(
-                1000, x, y, w, depth, (float)height, a, !print->is_step_done(psWipeTower),
-                brim_width);
-#endif // ENABLE_WIPETOWER_OBJECTID_1000_REMOVAL
-#else
-#if ENABLE_WIPETOWER_OBJECTID_1000_REMOVAL
-            int volume_idx_wipe_tower_new = m_volumes.load_wipe_tower_preview(
-                x, y, w, depth, (float)height, a, !print->is_step_done(psWipeTower),
-                brim_width, m_initialized);
-#else
-            int volume_idx_wipe_tower_new = m_volumes.load_wipe_tower_preview(
-                1000, x, y, w, depth, (float)height, a, !print->is_step_done(psWipeTower),
-                brim_width, m_initialized);
-#endif // ENABLE_WIPETOWER_OBJECTID_1000_REMOVAL
-#endif // ENABLE_LEGACY_OPENGL_REMOVAL
-            if (volume_idx_wipe_tower_old != -1)
-                map_glvolume_old_to_new[volume_idx_wipe_tower_old] = volume_idx_wipe_tower_new;
-        }
-    }
-
-    update_volumes_colors_by_extruder();
-	// Update selection indices based on the old/new GLVolumeCollection.
-    if (m_selection.get_mode() == Selection::Instance)
-        m_selection.instances_changed(instance_ids_selected);
-    else
-        m_selection.volumes_changed(map_glvolume_old_to_new);
-
-    m_gizmos.update_data();
-    m_gizmos.refresh_on_off_state();
-
-    // Update the toolbar
-	if (update_object_list)
-		post_event(SimpleEvent(EVT_GLCANVAS_OBJECT_SELECT));
-
-    // checks for geometry outside the print volume to render it accordingly
-    if (!m_volumes.empty()) {
-        ModelInstanceEPrintVolumeState state;
-        const bool contained_min_one = m_volumes.check_outside_state(m_bed.build_volume(), &state);
-        const bool partlyOut = (state == ModelInstanceEPrintVolumeState::ModelInstancePVS_Partly_Outside);
-        const bool fullyOut = (state == ModelInstanceEPrintVolumeState::ModelInstancePVS_Fully_Outside);
-
-        _set_warning_notification(EWarning::ObjectClashed, partlyOut);
-        _set_warning_notification(EWarning::ObjectOutside, fullyOut);
-        if (printer_technology != ptSLA || !contained_min_one)
-            _set_warning_notification(EWarning::SlaSupportsOutside, false);
-
-        post_event(Event<bool>(EVT_GLCANVAS_ENABLE_ACTION_BUTTONS, 
-                               contained_min_one && !m_model->objects.empty() && !partlyOut));
-    }
-    else {
-        _set_warning_notification(EWarning::ObjectOutside, false);
-        _set_warning_notification(EWarning::ObjectClashed, false);
-        _set_warning_notification(EWarning::SlaSupportsOutside, false);
-        post_event(Event<bool>(EVT_GLCANVAS_ENABLE_ACTION_BUTTONS, false));
-    }
-
-    refresh_camera_scene_box();
-
-    if (m_selection.is_empty()) {
-        // If no object is selected, deactivate the active gizmo, if any
-        // Otherwise it may be shown after cleaning the scene (if it was active while the objects were deleted)
-        m_gizmos.reset_all_states();
-
-        // If no object is selected, reset the objects manipulator on the sidebar
-        // to force a reset of its cache
-        auto manip = wxGetApp().obj_manipul();
-        if (manip != nullptr)
-            manip->set_dirty();
-    }
-
-    // and force this canvas to be redrawn.
-    m_dirty = true;
-}
-
-#if !ENABLE_LEGACY_OPENGL_REMOVAL
-static void reserve_new_volume_finalize_old_volume(GLVolume& vol_new, GLVolume& vol_old, bool gl_initialized, size_t prealloc_size = VERTEX_BUFFER_RESERVE_SIZE)
-{
-    // Assign the large pre-allocated buffers to the new GLVolume.
-	vol_new.indexed_vertex_array = std::move(vol_old.indexed_vertex_array);
-	// Copy the content back to the old GLVolume.
-	vol_old.indexed_vertex_array = vol_new.indexed_vertex_array;
-	// Clear the buffers, but keep them pre-allocated.
-	vol_new.indexed_vertex_array.clear();
-	// Just make sure that clear did not clear the reserved memory.
-	// Reserving number of vertices (3x position + 3x color)
-	vol_new.indexed_vertex_array.reserve(prealloc_size / 6);
-	// Finalize the old geometry, possibly move data to the graphics card.
-	vol_old.finalize_geometry(gl_initialized);
-}
-#endif // !ENABLE_LEGACY_OPENGL_REMOVAL
-
-void GLCanvas3D::load_gcode_preview(const GCodeProcessorResult& gcode_result, const std::vector<std::string>& str_tool_colors)
-{
-#if ENABLE_LEGACY_OPENGL_REMOVAL
-    m_gcode_viewer.load(gcode_result, *this->fff_print());
-#else
-    m_gcode_viewer.load(gcode_result, *this->fff_print(), m_initialized);
-#endif // ENABLE_LEGACY_OPENGL_REMOVAL
-
-    if (wxGetApp().is_editor()) {
-        m_gcode_viewer.update_shells_color_by_extruder(m_config);
-        _set_warning_notification_if_needed(EWarning::ToolpathOutside);
-    }
-
-    m_gcode_viewer.refresh(gcode_result, str_tool_colors);
-    set_as_dirty();
-    request_extra_frame();
-}
-
-#if ENABLE_PREVIEW_LAYOUT
-void GLCanvas3D::refresh_gcode_preview_render_paths(bool keep_sequential_current_first, bool keep_sequential_current_last)
-{
-    m_gcode_viewer.refresh_render_paths(keep_sequential_current_first, keep_sequential_current_last);
-    set_as_dirty();
-    request_extra_frame();
-}
-#else
-void GLCanvas3D::refresh_gcode_preview_render_paths()
-{
-    m_gcode_viewer.refresh_render_paths();
-    set_as_dirty();
-    request_extra_frame();
-}
-#endif // ENABLE_PREVIEW_LAYOUT
-
-void GLCanvas3D::load_sla_preview()
-{
-    const SLAPrint* print = sla_print();
-    if (m_canvas != nullptr && print != nullptr) {
-        _set_current();
-	    // Release OpenGL data before generating new data.
-	    reset_volumes();
-        _load_sla_shells();
-        _update_sla_shells_outside_state();
-        _set_warning_notification_if_needed(EWarning::SlaSupportsOutside);
-    }
-}
-
-void GLCanvas3D::load_preview(const std::vector<std::string>& str_tool_colors, const std::vector<CustomGCode::Item>& color_print_values)
-{
-    const Print *print = this->fff_print();
-    if (print == nullptr)
-        return;
-
-    _set_current();
-
-    // Release OpenGL data before generating new data.
-    this->reset_volumes();
-
-    const BuildVolume &build_volume = m_bed.build_volume();
-    _load_print_toolpaths(build_volume);
-    _load_wipe_tower_toolpaths(build_volume, str_tool_colors);
-    for (const PrintObject* object : print->objects())
-        _load_print_object_toolpaths(*object, build_volume, str_tool_colors, color_print_values);
-
-    _set_warning_notification_if_needed(EWarning::ToolpathOutside);
-}
-
-void GLCanvas3D::bind_event_handlers()
-{
-    if (m_canvas != nullptr) {
-        m_canvas->Bind(wxEVT_SIZE, &GLCanvas3D::on_size, this);
-        m_canvas->Bind(wxEVT_IDLE, &GLCanvas3D::on_idle, this);
-        m_canvas->Bind(wxEVT_CHAR, &GLCanvas3D::on_char, this);
-        m_canvas->Bind(wxEVT_KEY_DOWN, &GLCanvas3D::on_key, this);
-        m_canvas->Bind(wxEVT_KEY_UP, &GLCanvas3D::on_key, this);
-        m_canvas->Bind(wxEVT_MOUSEWHEEL, &GLCanvas3D::on_mouse_wheel, this);
-        m_canvas->Bind(wxEVT_TIMER, &GLCanvas3D::on_timer, this);
-        m_canvas->Bind(EVT_GLCANVAS_RENDER_TIMER, &GLCanvas3D::on_render_timer, this);
-        m_toolbar_highlighter.set_timer_owner(m_canvas, 0);
-        m_canvas->Bind(EVT_GLCANVAS_TOOLBAR_HIGHLIGHTER_TIMER, [this](wxTimerEvent&) { m_toolbar_highlighter.blink(); });
-        m_gizmo_highlighter.set_timer_owner(m_canvas, 0);
-        m_canvas->Bind(EVT_GLCANVAS_GIZMO_HIGHLIGHTER_TIMER, [this](wxTimerEvent&) { m_gizmo_highlighter.blink(); });
-        m_canvas->Bind(wxEVT_LEFT_DOWN, &GLCanvas3D::on_mouse, this);
-        m_canvas->Bind(wxEVT_LEFT_UP, &GLCanvas3D::on_mouse, this);
-        m_canvas->Bind(wxEVT_MIDDLE_DOWN, &GLCanvas3D::on_mouse, this);
-        m_canvas->Bind(wxEVT_MIDDLE_UP, &GLCanvas3D::on_mouse, this);
-        m_canvas->Bind(wxEVT_RIGHT_DOWN, &GLCanvas3D::on_mouse, this);
-        m_canvas->Bind(wxEVT_RIGHT_UP, &GLCanvas3D::on_mouse, this);
-        m_canvas->Bind(wxEVT_MOTION, &GLCanvas3D::on_mouse, this);
-        m_canvas->Bind(wxEVT_ENTER_WINDOW, &GLCanvas3D::on_mouse, this);
-        m_canvas->Bind(wxEVT_LEAVE_WINDOW, &GLCanvas3D::on_mouse, this);
-        m_canvas->Bind(wxEVT_LEFT_DCLICK, &GLCanvas3D::on_mouse, this);
-        m_canvas->Bind(wxEVT_MIDDLE_DCLICK, &GLCanvas3D::on_mouse, this);
-        m_canvas->Bind(wxEVT_RIGHT_DCLICK, &GLCanvas3D::on_mouse, this);
-        m_canvas->Bind(wxEVT_PAINT, &GLCanvas3D::on_paint, this);
-        m_canvas->Bind(wxEVT_SET_FOCUS, &GLCanvas3D::on_set_focus, this);
-
-        m_event_handlers_bound = true;
-    }
-}
-
-void GLCanvas3D::unbind_event_handlers()
-{
-    if (m_canvas != nullptr && m_event_handlers_bound) {
-        m_canvas->Unbind(wxEVT_SIZE, &GLCanvas3D::on_size, this);
-        m_canvas->Unbind(wxEVT_IDLE, &GLCanvas3D::on_idle, this);
-        m_canvas->Unbind(wxEVT_CHAR, &GLCanvas3D::on_char, this);
-        m_canvas->Unbind(wxEVT_KEY_DOWN, &GLCanvas3D::on_key, this);
-        m_canvas->Unbind(wxEVT_KEY_UP, &GLCanvas3D::on_key, this);
-        m_canvas->Unbind(wxEVT_MOUSEWHEEL, &GLCanvas3D::on_mouse_wheel, this);
-        m_canvas->Unbind(wxEVT_TIMER, &GLCanvas3D::on_timer, this);
-        m_canvas->Unbind(EVT_GLCANVAS_RENDER_TIMER, &GLCanvas3D::on_render_timer, this);
-        m_canvas->Unbind(wxEVT_LEFT_DOWN, &GLCanvas3D::on_mouse, this);
-		m_canvas->Unbind(wxEVT_LEFT_UP, &GLCanvas3D::on_mouse, this);
-        m_canvas->Unbind(wxEVT_MIDDLE_DOWN, &GLCanvas3D::on_mouse, this);
-        m_canvas->Unbind(wxEVT_MIDDLE_UP, &GLCanvas3D::on_mouse, this);
-        m_canvas->Unbind(wxEVT_RIGHT_DOWN, &GLCanvas3D::on_mouse, this);
-        m_canvas->Unbind(wxEVT_RIGHT_UP, &GLCanvas3D::on_mouse, this);
-        m_canvas->Unbind(wxEVT_MOTION, &GLCanvas3D::on_mouse, this);
-        m_canvas->Unbind(wxEVT_ENTER_WINDOW, &GLCanvas3D::on_mouse, this);
-        m_canvas->Unbind(wxEVT_LEAVE_WINDOW, &GLCanvas3D::on_mouse, this);
-        m_canvas->Unbind(wxEVT_LEFT_DCLICK, &GLCanvas3D::on_mouse, this);
-        m_canvas->Unbind(wxEVT_MIDDLE_DCLICK, &GLCanvas3D::on_mouse, this);
-        m_canvas->Unbind(wxEVT_RIGHT_DCLICK, &GLCanvas3D::on_mouse, this);
-        m_canvas->Unbind(wxEVT_PAINT, &GLCanvas3D::on_paint, this);
-        m_canvas->Unbind(wxEVT_SET_FOCUS, &GLCanvas3D::on_set_focus, this);
-
-        m_event_handlers_bound = false;
-    }
-}
-
-void GLCanvas3D::on_size(wxSizeEvent& evt)
-{
-    m_dirty = true;
-}
- 
-void GLCanvas3D::on_idle(wxIdleEvent& evt)
-{
-    if (!m_initialized)
-        return;
-
-    m_dirty |= m_main_toolbar.update_items_state();
-    m_dirty |= m_undoredo_toolbar.update_items_state();
-    m_dirty |= wxGetApp().plater()->get_view_toolbar().update_items_state();
-    m_dirty |= wxGetApp().plater()->get_collapse_toolbar().update_items_state();
-    bool mouse3d_controller_applied = wxGetApp().plater()->get_mouse3d_controller().apply(wxGetApp().plater()->get_camera());
-    m_dirty |= mouse3d_controller_applied;
-    m_dirty |= wxGetApp().plater()->get_notification_manager()->update_notifications(*this);
-    auto gizmo = wxGetApp().plater()->canvas3D()->get_gizmos_manager().get_current();
-    if (gizmo != nullptr) m_dirty |= gizmo->update_items_state();
-    // ImGuiWrapper::m_requires_extra_frame may have been set by a render made outside of the OnIdle mechanism
-    bool imgui_requires_extra_frame = wxGetApp().imgui()->requires_extra_frame();
-    m_dirty |= imgui_requires_extra_frame;
-
-    if (!m_dirty)
-        return;
-
-    // this needs to be done here.
-    // during the render launched by the refresh the value may be set again 
-    wxGetApp().imgui()->reset_requires_extra_frame();
-
-    _refresh_if_shown_on_screen();
-
-    if (m_extra_frame_requested || mouse3d_controller_applied || imgui_requires_extra_frame || wxGetApp().imgui()->requires_extra_frame()) {
-        m_extra_frame_requested = false;
-        evt.RequestMore();
-    }
-    else
-        m_dirty = false;
-}
-
-void GLCanvas3D::on_char(wxKeyEvent& evt)
-{
-    if (!m_initialized)
-        return;
-
-    // see include/wx/defs.h enum wxKeyCode
-    int keyCode = evt.GetKeyCode();
-    int ctrlMask = wxMOD_CONTROL;
-    int shiftMask = wxMOD_SHIFT;
-
-    auto imgui = wxGetApp().imgui();
-    if (imgui->update_key_data(evt)) {
-        render();
-        return;
-    }
-
-    if (keyCode == WXK_ESCAPE && (_deactivate_undo_redo_toolbar_items() || _deactivate_search_toolbar_item() || _deactivate_arrange_menu()))
-        return;
-
-    if (m_gizmos.on_char(evt))
-        return;
-
-    if ((evt.GetModifiers() & ctrlMask) != 0) {
-        // CTRL is pressed
-        switch (keyCode) {
-#ifdef __APPLE__
-        case 'a':
-        case 'A':
-#else /* __APPLE__ */
-        case WXK_CONTROL_A:
-#endif /* __APPLE__ */
-            post_event(SimpleEvent(EVT_GLCANVAS_SELECT_ALL));
-        break;
-#ifdef __APPLE__
-        case 'c':
-        case 'C':
-#else /* __APPLE__ */
-        case WXK_CONTROL_C:
-#endif /* __APPLE__ */
-            post_event(SimpleEvent(EVT_GLTOOLBAR_COPY));
-        break;
-#ifdef __APPLE__
-        case 'm':
-        case 'M':
-#else /* __APPLE__ */
-        case WXK_CONTROL_M:
-#endif /* __APPLE__ */
-        {
-#ifdef _WIN32
-            if (wxGetApp().app_config->get("use_legacy_3DConnexion") == "1") {
-#endif //_WIN32
-#ifdef __APPLE__
-            // On OSX use Cmd+Shift+M to "Show/Hide 3Dconnexion devices settings dialog"
-            if ((evt.GetModifiers() & shiftMask) != 0) {
-#endif // __APPLE__
-                Mouse3DController& controller = wxGetApp().plater()->get_mouse3d_controller();
-                controller.show_settings_dialog(!controller.is_settings_dialog_shown());
-                m_dirty = true;
-#ifdef __APPLE__
-            } 
-            else 
-            // and Cmd+M to minimize application
-                wxGetApp().mainframe->Iconize();
-#endif // __APPLE__
-#ifdef _WIN32
-            }
-#endif //_WIN32
-            break;
-        }
-#ifdef __APPLE__
-        case 'v':
-        case 'V':
-#else /* __APPLE__ */
-        case WXK_CONTROL_V:
-#endif /* __APPLE__ */
-            post_event(SimpleEvent(EVT_GLTOOLBAR_PASTE));
-        break;
-
-
-#ifdef __APPLE__
-        case 'f':
-        case 'F':
-#else /* __APPLE__ */
-        case WXK_CONTROL_F:
-#endif /* __APPLE__ */
-            _activate_search_toolbar_item();
-            break;
-
-
-#ifdef __APPLE__
-        case 'y':
-        case 'Y':
-#else /* __APPLE__ */
-        case WXK_CONTROL_Y:
-#endif /* __APPLE__ */
-            post_event(SimpleEvent(EVT_GLCANVAS_REDO));
-        break;
-#ifdef __APPLE__
-        case 'z':
-        case 'Z':
-#else /* __APPLE__ */
-        case WXK_CONTROL_Z:
-#endif /* __APPLE__ */
-            post_event(SimpleEvent(EVT_GLCANVAS_UNDO));
-        break;
-
-        case WXK_BACK:
-        case WXK_DELETE:
-             post_event(SimpleEvent(EVT_GLTOOLBAR_DELETE_ALL)); break;
-        default:            evt.Skip();
-        }
-    } else {
-        switch (keyCode)
-        {
-        case WXK_BACK:
-        case WXK_DELETE: { post_event(SimpleEvent(EVT_GLTOOLBAR_DELETE)); break; }
-        case WXK_ESCAPE: { deselect_all(); break; }
-        case WXK_F5: {
-            if ((wxGetApp().is_editor() && !wxGetApp().plater()->model().objects.empty()) ||
-                (wxGetApp().is_gcode_viewer() && !wxGetApp().plater()->get_last_loaded_gcode().empty()))
-                post_event(SimpleEvent(EVT_GLCANVAS_RELOAD_FROM_DISK));
-            break;
-        }
-        case '0': { select_view("iso"); break; }
-        case '1': { select_view("top"); break; }
-        case '2': { select_view("bottom"); break; }
-        case '3': { select_view("front"); break; }
-        case '4': { select_view("rear"); break; }
-        case '5': { select_view("left"); break; }
-        case '6': { select_view("right"); break; }
-        case '+': {
-            if (dynamic_cast<Preview*>(m_canvas->GetParent()) != nullptr)
-                post_event(wxKeyEvent(EVT_GLCANVAS_EDIT_COLOR_CHANGE, evt));
-            else
-                post_event(Event<int>(EVT_GLCANVAS_INCREASE_INSTANCES, +1));
-            break;
-        }
-        case '-': {
-            if (dynamic_cast<Preview*>(m_canvas->GetParent()) != nullptr)
-                post_event(wxKeyEvent(EVT_GLCANVAS_EDIT_COLOR_CHANGE, evt)); 
-            else
-                post_event(Event<int>(EVT_GLCANVAS_INCREASE_INSTANCES, -1)); 
-            break;
-        }
-        case '?': { post_event(SimpleEvent(EVT_GLCANVAS_QUESTION_MARK)); break; }
-        case 'A':
-        case 'a': { post_event(SimpleEvent(EVT_GLCANVAS_ARRANGE)); break; }
-        case 'B':
-        case 'b': { zoom_to_bed(); break; }
-        case 'C':
-        case 'c': { m_gcode_viewer.toggle_gcode_window_visibility(); m_dirty = true; request_extra_frame(); break; }
-        case 'E':
-        case 'e': { m_labels.show(!m_labels.is_shown()); m_dirty = true; break; }
-        case 'G':
-        case 'g': {
-            if ((evt.GetModifiers() & shiftMask) != 0) {
-                if (dynamic_cast<Preview*>(m_canvas->GetParent()) != nullptr)
-                    post_event(wxKeyEvent(EVT_GLCANVAS_JUMP_TO, evt));
-            }
-            break;
-        }
-        case 'I':
-        case 'i': { _update_camera_zoom(1.0); break; }
-        case 'K':
-        case 'k': { wxGetApp().plater()->get_camera().select_next_type(); m_dirty = true; break; }
-        case 'L': 
-        case 'l': { 
-            if (!m_main_toolbar.is_enabled()) { 
-#if ENABLE_PREVIEW_LAYOUT
-                show_legend(!is_legend_shown());
-#else
-                m_gcode_viewer.enable_legend(!m_gcode_viewer.is_legend_enabled());
-                m_dirty = true;
-                wxGetApp().plater()->update_preview_bottom_toolbar();
-#endif // ENABLE_PREVIEW_LAYOUT
-            }
-            break;
-        }
-        case 'O':
-        case 'o': { _update_camera_zoom(-1.0); break; }
-#if ENABLE_RENDER_PICKING_PASS
-        case 'T':
-        case 't': {
-            m_show_picking_texture = !m_show_picking_texture;
-            m_dirty = true;
-            break;
-        }
-#endif // ENABLE_RENDER_PICKING_PASS
-        case 'Z':
-        case 'z': {
-            if (!m_selection.is_empty())
-                zoom_to_selection();
-            else {
-                if (!m_volumes.empty())
-                    zoom_to_volumes();
-                else
-                    _zoom_to_box(m_gcode_viewer.get_paths_bounding_box());
-            }
-            break;
-        }
-        default:  { evt.Skip(); break; }
-        }
-    }
-}
-
-class TranslationProcessor
-{
-    using UpAction = std::function<void(void)>;
-    using DownAction = std::function<void(const Vec3d&, bool, bool)>;
-
-    UpAction m_up_action{ nullptr };
-    DownAction m_down_action{ nullptr };
-
-    bool m_running{ false };
-    Vec3d m_direction{ Vec3d::UnitX() };
-
-public:
-    TranslationProcessor(UpAction up_action, DownAction down_action)
-        : m_up_action(up_action), m_down_action(down_action)
-    {
-    }
-
-    void process(wxKeyEvent& evt)
-    {
-        const int keyCode = evt.GetKeyCode();
-        wxEventType type = evt.GetEventType();
-        if (type == wxEVT_KEY_UP) {
-            switch (keyCode)
-            {
-            case WXK_NUMPAD_LEFT:  case WXK_LEFT:
-            case WXK_NUMPAD_RIGHT: case WXK_RIGHT:
-            case WXK_NUMPAD_UP:    case WXK_UP:
-            case WXK_NUMPAD_DOWN:  case WXK_DOWN:
-            {
-                m_running = false;
-                m_up_action();
-                break;
-            }
-            default: { break; }
-            }
-        }
-        else if (type == wxEVT_KEY_DOWN) {
-            bool apply = false;
-
-            switch (keyCode)
-            {
-            case WXK_SHIFT:
-            {
-                if (m_running) 
-                    apply = true;
-
-                break;
-            }
-            case WXK_NUMPAD_LEFT:
-            case WXK_LEFT:
-            {
-                m_direction = -Vec3d::UnitX();
-                apply = true;
-                break;
-            }
-            case WXK_NUMPAD_RIGHT:
-            case WXK_RIGHT:
-            {
-                m_direction = Vec3d::UnitX();
-                apply = true;
-                break;
-            }
-            case WXK_NUMPAD_UP:
-            case WXK_UP:
-            {
-                m_direction = Vec3d::UnitY();
-                apply = true;
-                break;
-            }
-            case WXK_NUMPAD_DOWN:
-            case WXK_DOWN:
-            {
-                m_direction = -Vec3d::UnitY();
-                apply = true;
-                break;
-            }
-            default: { break; }
-            }
-
-            if (apply) {
-                m_running = true;
-                m_down_action(m_direction, evt.ShiftDown(), evt.CmdDown());
-            }
-        }
-    }
-};
-
-void GLCanvas3D::on_key(wxKeyEvent& evt)
-{
-    static TranslationProcessor translationProcessor(
-        [this]() {
-            do_move(L("Gizmo-Move"));
-            m_gizmos.update_data();
-
-            wxGetApp().obj_manipul()->set_dirty();
-            // Let the plater know that the dragging finished, so a delayed refresh
-            // of the scene with the background processing data should be performed.
-            post_event(SimpleEvent(EVT_GLCANVAS_MOUSE_DRAGGING_FINISHED));
-            // updates camera target constraints
-            refresh_camera_scene_box();
-            m_dirty = true;
-        },
-        [this](const Vec3d& direction, bool slow, bool camera_space) {
-            m_selection.setup_cache();
-            double multiplier = slow ? 1.0 : 10.0;
-
-            Vec3d displacement;
-            if (camera_space) {
-                Eigen::Matrix<double, 3, 3, Eigen::DontAlign> inv_view_3x3 = wxGetApp().plater()->get_camera().get_view_matrix().inverse().matrix().block(0, 0, 3, 3);
-                displacement = multiplier * (inv_view_3x3 * direction);
-                displacement.z() = 0.0;
-            }
-            else
-                displacement = multiplier * direction;
-
-            m_selection.translate(displacement);
-            m_dirty = true;
-        }
-    );
-
-    const int keyCode = evt.GetKeyCode();
-
-    auto imgui = wxGetApp().imgui();
-    if (imgui->update_key_data(evt)) {
-        render();
-    }
-    else {
-        if (!m_gizmos.on_key(evt)) {
-            if (evt.GetEventType() == wxEVT_KEY_UP) {
-                if (evt.ShiftDown() && evt.ControlDown() && keyCode == WXK_SPACE) {
-                    wxGetApp().plater()->toggle_render_statistic_dialog();
-                    m_dirty = true;
-                }
-                if (m_tab_down && keyCode == WXK_TAB && !evt.HasAnyModifiers()) {
-                    // Enable switching between 3D and Preview with Tab
-                    // m_canvas->HandleAsNavigationKey(evt);   // XXX: Doesn't work in some cases / on Linux
-                    post_event(SimpleEvent(EVT_GLCANVAS_TAB));
-                }
-                else if (keyCode == WXK_TAB && evt.ShiftDown() && ! wxGetApp().is_gcode_viewer()) {
-                    // Collapse side-panel with Shift+Tab
-                    post_event(SimpleEvent(EVT_GLCANVAS_COLLAPSE_SIDEBAR));
-                }
-                else if (keyCode == WXK_SHIFT) {
-                    translationProcessor.process(evt);
-
-                    if (m_picking_enabled && m_rectangle_selection.is_dragging()) {
-                        _update_selection_from_hover();
-                        m_rectangle_selection.stop_dragging();
-                        m_mouse.ignore_left_up = true;
-#if !ENABLE_NEW_RECTANGLE_SELECTION
-                        m_dirty = true;
-#endif // !ENABLE_NEW_RECTANGLE_SELECTION
-                    }
-#if ENABLE_NEW_RECTANGLE_SELECTION
-                    m_dirty = true;
-#endif // ENABLE_NEW_RECTANGLE_SELECTION
-//                    set_cursor(Standard);
-                }
-                else if (keyCode == WXK_ALT) {
-                    if (m_picking_enabled && m_rectangle_selection.is_dragging()) {
-                        _update_selection_from_hover();
-                        m_rectangle_selection.stop_dragging();
-                        m_mouse.ignore_left_up = true;
-                        m_dirty = true;
-                    }
-//                    set_cursor(Standard);
-                }
-                else if (keyCode == WXK_CONTROL) {
-#if ENABLE_NEW_CAMERA_MOVEMENTS
-                    if (m_mouse.dragging) {
-                        // if the user releases CTRL while rotating the 3D scene
-                        // prevent from moving the selected volume
-                        m_mouse.drag.move_volume_idx = -1;
-                        m_mouse.set_start_position_3D_as_invalid();
-                    }
-#endif // ENABLE_NEW_CAMERA_MOVEMENTS
-                    m_dirty = true;
-                }
-                else if (m_gizmos.is_enabled() && !m_selection.is_empty()) {
-                    translationProcessor.process(evt);
-
-                    switch (keyCode)
-                    {
-                    case WXK_NUMPAD_PAGEUP:   case WXK_PAGEUP:
-                    case WXK_NUMPAD_PAGEDOWN: case WXK_PAGEDOWN:
-                    {
-                        do_rotate(L("Gizmo-Rotate"));
-                        m_gizmos.update_data();
-
-                        wxGetApp().obj_manipul()->set_dirty();
-                        // Let the plater know that the dragging finished, so a delayed refresh
-                        // of the scene with the background processing data should be performed.
-                        post_event(SimpleEvent(EVT_GLCANVAS_MOUSE_DRAGGING_FINISHED));
-                        // updates camera target constraints
-                        refresh_camera_scene_box();
-                        m_dirty = true;
-
-                        break;
-                    }
-                    default: { break; }
-                    }
-                }
-            }
-            else if (evt.GetEventType() == wxEVT_KEY_DOWN) {
-                m_tab_down = keyCode == WXK_TAB && !evt.HasAnyModifiers();
-                if (keyCode == WXK_SHIFT) {
-                    translationProcessor.process(evt);
-
-                    if (m_picking_enabled && (m_gizmos.get_current_type() != GLGizmosManager::SlaSupports)) {
-                        m_mouse.ignore_left_up = false;
-//                        set_cursor(Cross);
-                    }
-#if ENABLE_NEW_RECTANGLE_SELECTION
-                    m_dirty = true;
-#endif // ENABLE_NEW_RECTANGLE_SELECTION
-                }
-                else if (keyCode == WXK_ALT) {
-                    if (m_picking_enabled && (m_gizmos.get_current_type() != GLGizmosManager::SlaSupports)) {
-                        m_mouse.ignore_left_up = false;
-//                        set_cursor(Cross);
-                    }
-                }
-                else if (keyCode == WXK_CONTROL)
-                    m_dirty = true;
-                else if (m_gizmos.is_enabled() && !m_selection.is_empty()) {
-                    auto do_rotate = [this](double angle_z_rad) {
-                        m_selection.setup_cache();
-                        m_selection.rotate(Vec3d(0.0, 0.0, angle_z_rad), TransformationType(TransformationType::World_Relative_Joint));
-                        m_dirty = true;
-//                        wxGetApp().obj_manipul()->set_dirty();
-                    };
-
-                    translationProcessor.process(evt);
-
-                    switch (keyCode)
-                    {
-                    case WXK_NUMPAD_PAGEUP:   case WXK_PAGEUP:   { do_rotate(0.25 * M_PI); break; }
-                    case WXK_NUMPAD_PAGEDOWN: case WXK_PAGEDOWN: { do_rotate(-0.25 * M_PI); break; }
-                    default: { break; }
-                    }
-                }
-                else if (!m_gizmos.is_enabled()) {
-                    // DoubleSlider navigation in Preview
-                    if (keyCode == WXK_LEFT ||
-                        keyCode == WXK_RIGHT ||
-                        keyCode == WXK_UP ||
-                        keyCode == WXK_DOWN) {
-                        if (dynamic_cast<Preview*>(m_canvas->GetParent()) != nullptr)
-                            post_event(wxKeyEvent(EVT_GLCANVAS_MOVE_SLIDERS, evt));
-                    }
-                }
-            }
-        }
-    }
-
-    if (keyCode != WXK_TAB
-        && keyCode != WXK_LEFT
-        && keyCode != WXK_UP
-        && keyCode != WXK_RIGHT
-        && keyCode != WXK_DOWN) {
-        evt.Skip();   // Needed to have EVT_CHAR generated as well
-    }
-}
-
-void GLCanvas3D::on_mouse_wheel(wxMouseEvent& evt)
-{
-#ifdef WIN32
-    // Try to filter out spurious mouse wheel events comming from 3D mouse.
-    if (wxGetApp().plater()->get_mouse3d_controller().process_mouse_wheel())
-        return;
-#endif
-
-    if (!m_initialized)
-        return;
-
-    // Ignore the wheel events if the middle button is pressed.
-    if (evt.MiddleIsDown())
-        return;
-
-#if ENABLE_RETINA_GL
-    const float scale = m_retina_helper->get_scale_factor();
-    evt.SetX(evt.GetX() * scale);
-    evt.SetY(evt.GetY() * scale);
-#endif
-
-    if (wxGetApp().imgui()->update_mouse_data(evt)) {
-        m_dirty = true;
-        return;
-    }
-
-#ifdef __WXMSW__
-	// For some reason the Idle event is not being generated after the mouse scroll event in case of scrolling with the two fingers on the touch pad,
-	// if the event is not allowed to be passed further.
-	// https://github.com/prusa3d/PrusaSlicer/issues/2750
-    // evt.Skip() used to trigger the needed screen refresh, but it does no more. wxWakeUpIdle() seem to work now.
-    wxWakeUpIdle();
-#endif /* __WXMSW__ */
-
-    // Performs layers editing updates, if enabled
-    if (is_layers_editing_enabled()) {
-        int object_idx_selected = m_selection.get_object_idx();
-        if (object_idx_selected != -1) {
-            // A volume is selected. Test, whether hovering over a layer thickness bar.
-            if (m_layers_editing.bar_rect_contains(*this, (float)evt.GetX(), (float)evt.GetY())) {
-                // Adjust the width of the selection.
-                m_layers_editing.band_width = std::max(std::min(m_layers_editing.band_width * (1.0f + 0.1f * (float)evt.GetWheelRotation() / (float)evt.GetWheelDelta()), 10.0f), 1.5f);
-                if (m_canvas != nullptr)
-                    m_canvas->Refresh();
-
-                return;
-            }
-        }
-    }
-
-    // If the Search window or Undo/Redo list is opened, 
-    // update them according to the event
-    if (m_main_toolbar.is_item_pressed("search")    || 
-        m_undoredo_toolbar.is_item_pressed("undo")  || 
-        m_undoredo_toolbar.is_item_pressed("redo")) {
-        m_mouse_wheel = int((double)evt.GetWheelRotation() / (double)evt.GetWheelDelta());
-        return;
-    }
-
-    // Inform gizmos about the event so they have the opportunity to react.
-    if (m_gizmos.on_mouse_wheel(evt))
-        return;
-
-    // Calculate the zoom delta and apply it to the current zoom factor
-    double direction_factor = (wxGetApp().app_config->get("reverse_mouse_wheel_zoom") == "1") ? -1.0 : 1.0;
-    _update_camera_zoom(direction_factor * (double)evt.GetWheelRotation() / (double)evt.GetWheelDelta());
-}
-
-void GLCanvas3D::on_timer(wxTimerEvent& evt)
-{
-    if (m_layers_editing.state == LayersEditing::Editing)
-        _perform_layer_editing_action();
-}
-
-void GLCanvas3D::on_render_timer(wxTimerEvent& evt)
-{
-    // no need to wake up idle
-    // right after this event, idle event is fired
-    // m_dirty = true; 
-    // wxWakeUpIdle(); 
-}
-
-
-void GLCanvas3D::schedule_extra_frame(int miliseconds)
-{
-    // Schedule idle event right now
-    if (miliseconds == 0)
-    {
-        // We want to wakeup idle evnt but most likely this is call inside render cycle so we need to wait
-        if (m_in_render)
-            miliseconds = 33;
-        else {
-            m_dirty = true;
-            wxWakeUpIdle();
-            return;
-        }
-    } 
-    int remaining_time = m_render_timer.GetInterval();
-    // Timer is not running
-    if (!m_render_timer.IsRunning()) {
-        m_render_timer.StartOnce(miliseconds);
-    // Timer is running - restart only if new period is shorter than remaning period
-    } else {
-        if (miliseconds + 20 < remaining_time) {
-            m_render_timer.Stop(); 
-            m_render_timer.StartOnce(miliseconds);
-        }
-    }
-}
-
-#ifndef NDEBUG
-// #define SLIC3R_DEBUG_MOUSE_EVENTS
-#endif
-
-#ifdef SLIC3R_DEBUG_MOUSE_EVENTS
-std::string format_mouse_event_debug_message(const wxMouseEvent &evt)
-{
-	static int idx = 0;
-	char buf[2048];
-	std::string out;
-	sprintf(buf, "Mouse Event %d - ", idx ++);
-	out = buf;
-
-	if (evt.Entering())
-		out += "Entering ";
-	if (evt.Leaving())
-		out += "Leaving ";
-	if (evt.Dragging())
-		out += "Dragging ";
-	if (evt.Moving())
-		out += "Moving ";
-	if (evt.Magnify())
-		out += "Magnify ";
-	if (evt.LeftDown())
-		out += "LeftDown ";
-	if (evt.LeftUp())
-		out += "LeftUp ";
-	if (evt.LeftDClick())
-		out += "LeftDClick ";
-	if (evt.MiddleDown())
-		out += "MiddleDown ";
-	if (evt.MiddleUp())
-		out += "MiddleUp ";
-	if (evt.MiddleDClick())
-		out += "MiddleDClick ";
-	if (evt.RightDown())
-		out += "RightDown ";
-	if (evt.RightUp())
-		out += "RightUp ";
-	if (evt.RightDClick())
-		out += "RightDClick ";
-
-	sprintf(buf, "(%d, %d)", evt.GetX(), evt.GetY());
-	out += buf;
-	return out;
-}
-#endif /* SLIC3R_DEBUG_MOUSE_EVENTS */
-
-void GLCanvas3D::on_mouse(wxMouseEvent& evt)
-{
-    if (!m_initialized || !_set_current())
-        return;
-
-#if ENABLE_RETINA_GL
-    const float scale = m_retina_helper->get_scale_factor();
-    evt.SetX(evt.GetX() * scale);
-    evt.SetY(evt.GetY() * scale);
-#endif
-
-    Point pos(evt.GetX(), evt.GetY());
-
-    ImGuiWrapper* imgui = wxGetApp().imgui();
-    if (m_tooltip.is_in_imgui() && evt.LeftUp())
-        // ignore left up events coming from imgui windows and not processed by them
-        m_mouse.ignore_left_up = true;
-    m_tooltip.set_in_imgui(false);
-    if (imgui->update_mouse_data(evt)) {
-        m_mouse.position = evt.Leaving() ? Vec2d(-1.0, -1.0) : pos.cast<double>();
-        m_tooltip.set_in_imgui(true);
-        render();
-#ifdef SLIC3R_DEBUG_MOUSE_EVENTS
-        printf((format_mouse_event_debug_message(evt) + " - Consumed by ImGUI\n").c_str());
-#endif /* SLIC3R_DEBUG_MOUSE_EVENTS */
-        m_dirty = true;
-        // do not return if dragging or tooltip not empty to allow for tooltip update
-        // also, do not return if the mouse is moving and also is inside MM gizmo to allow update seed fill selection
-        if (!m_mouse.dragging && m_tooltip.is_empty() && (m_gizmos.get_current_type() != GLGizmosManager::MmuSegmentation || !evt.Moving()))
-            return;
-    }
-
-#ifdef __WXMSW__
-	bool on_enter_workaround = false;
-    if (! evt.Entering() && ! evt.Leaving() && m_mouse.position.x() == -1.0) {
-        // Workaround for SPE-832: There seems to be a mouse event sent to the window before evt.Entering()
-        m_mouse.position = pos.cast<double>();
-        render();
-#ifdef SLIC3R_DEBUG_MOUSE_EVENTS
-		printf((format_mouse_event_debug_message(evt) + " - OnEnter workaround\n").c_str());
-#endif /* SLIC3R_DEBUG_MOUSE_EVENTS */
-		on_enter_workaround = true;
-    } else 
-#endif /* __WXMSW__ */
-    {
-#ifdef SLIC3R_DEBUG_MOUSE_EVENTS
-		printf((format_mouse_event_debug_message(evt) + " - other\n").c_str());
-#endif /* SLIC3R_DEBUG_MOUSE_EVENTS */
-	}
-
-    if (m_main_toolbar.on_mouse(evt, *this)) {
-        if (evt.LeftUp() || evt.MiddleUp() || evt.RightUp())
-            mouse_up_cleanup();
-        m_mouse.set_start_position_3D_as_invalid();
-#if ENABLE_OBJECT_MANIPULATOR_FOCUS
-            handle_sidebar_focus_event("", false);
-#endif // ENABLE_OBJECT_MANIPULATOR_FOCUS
-        return;
-    }
-
-    if (m_undoredo_toolbar.on_mouse(evt, *this)) {
-        if (evt.LeftUp() || evt.MiddleUp() || evt.RightUp())
-            mouse_up_cleanup();
-        m_mouse.set_start_position_3D_as_invalid();
-#if ENABLE_OBJECT_MANIPULATOR_FOCUS
-        handle_sidebar_focus_event("", false);
-#endif // ENABLE_OBJECT_MANIPULATOR_FOCUS
-        return;
-    }
-
-    if (wxGetApp().plater()->get_collapse_toolbar().on_mouse(evt, *this)) {
-        if (evt.LeftUp() || evt.MiddleUp() || evt.RightUp())
-            mouse_up_cleanup();
-        m_mouse.set_start_position_3D_as_invalid();
-#if ENABLE_OBJECT_MANIPULATOR_FOCUS
-        handle_sidebar_focus_event("", false);
-#endif // ENABLE_OBJECT_MANIPULATOR_FOCUS
-        return;
-    }
-
-    if (wxGetApp().plater()->get_view_toolbar().on_mouse(evt, *this)) {
-        if (evt.LeftUp() || evt.MiddleUp() || evt.RightUp())
-            mouse_up_cleanup();
-        m_mouse.set_start_position_3D_as_invalid();
-#if ENABLE_OBJECT_MANIPULATOR_FOCUS
-        handle_sidebar_focus_event("", false);
-#endif // ENABLE_OBJECT_MANIPULATOR_FOCUS
-        return;
-    }
-
-    for (GLVolume* volume : m_volumes.volumes) {
-        volume->force_sinking_contours = false;
-    }
-
-    auto show_sinking_contours = [this]() {
-        const Selection::IndicesList& idxs = m_selection.get_volume_idxs();
-        for (unsigned int idx : idxs) {
-            m_volumes.volumes[idx]->force_sinking_contours = true;
-        }
-        m_dirty = true;
-    };
-
-    if (m_gizmos.on_mouse(evt)) {
-        if (wxWindow::FindFocus() != m_canvas)
-            // Grab keyboard focus for input in gizmo dialogs.
-            m_canvas->SetFocus();
-
-        if (evt.LeftUp() || evt.MiddleUp() || evt.RightUp())
-            mouse_up_cleanup();
-
-        m_mouse.set_start_position_3D_as_invalid();
-        m_mouse.position = pos.cast<double>();
-
-        // It should be detection of volume change
-        // Not only detection of some modifiers !!!
-        if (evt.Dragging()) {
-            GLGizmosManager::EType c = m_gizmos.get_current_type();
-            if (current_printer_technology() == ptFFF &&
-                fff_print()->config().complete_objects){
-                if (c == GLGizmosManager::EType::Move ||
-                    c == GLGizmosManager::EType::Scale ||
-                    c == GLGizmosManager::EType::Rotate )
-                    update_sequential_clearance();
-            } else {
-                if (c == GLGizmosManager::EType::Move ||
-                    c == GLGizmosManager::EType::Scale ||
-                    c == GLGizmosManager::EType::Rotate)
-                    show_sinking_contours();
-            }
-        }
-
-#if ENABLE_OBJECT_MANIPULATOR_FOCUS
-        handle_sidebar_focus_event("", false);
-#endif // ENABLE_OBJECT_MANIPULATOR_FOCUS
-        return;
-    }
-
-    bool any_gizmo_active = m_gizmos.get_current() != nullptr;
-
-    int selected_object_idx = m_selection.get_object_idx();
-    int layer_editing_object_idx = is_layers_editing_enabled() ? selected_object_idx : -1;
-    m_layers_editing.select_object(*m_model, layer_editing_object_idx);
-
-    if (m_mouse.drag.move_requires_threshold && m_mouse.is_move_start_threshold_position_2D_defined() && m_mouse.is_move_threshold_met(pos)) {
-        m_mouse.drag.move_requires_threshold = false;
-        m_mouse.set_move_start_threshold_position_2D_as_invalid();
-    }
-
-#if ENABLE_OBJECT_MANIPULATOR_FOCUS
-    if (evt.ButtonDown()) {
-        std::string curr_sidebar_field = m_sidebar_field;
-        handle_sidebar_focus_event("", false);
-        if (boost::algorithm::istarts_with(curr_sidebar_field, "layer"))
-            // restore visibility of layers hints after left clicking on the 3D scene
-            m_sidebar_field = curr_sidebar_field;
-        if (wxWindow::FindFocus() != m_canvas)
-            // Grab keyboard focus on any mouse click event.
-            m_canvas->SetFocus();
-    }
-#else
-    if (evt.ButtonDown() && wxWindow::FindFocus() != m_canvas)
-        // Grab keyboard focus on any mouse click event.
-        m_canvas->SetFocus();
-#endif // ENABLE_OBJECT_MANIPULATOR_FOCUS
-
-    if (evt.Entering()) {
-//#if defined(__WXMSW__) || defined(__linux__)
-//        // On Windows and Linux needs focus in order to catch key events
-#if !ENABLE_OBJECT_MANIPULATOR_FOCUS
-        // Set focus in order to remove it from sidebar fields
-#endif // !ENABLE_OBJECT_MANIPULATOR_FOCUS
-        if (m_canvas != nullptr) {
-#if !ENABLE_OBJECT_MANIPULATOR_FOCUS
-            // Only set focus, if the top level window of this canvas is active.
-            auto p = dynamic_cast<wxWindow*>(evt.GetEventObject());
-            while (p->GetParent())
-                p = p->GetParent();
-            auto *top_level_wnd = dynamic_cast<wxTopLevelWindow*>(p);
-            if (top_level_wnd && top_level_wnd->IsActive())
-                m_canvas->SetFocus();
-#endif // !ENABLE_OBJECT_MANIPULATOR_FOCUS
-            m_mouse.position = pos.cast<double>();
-            m_tooltip_enabled = false;
-            // 1) forces a frame render to ensure that m_hover_volume_idxs is updated even when the user right clicks while
-            // the context menu is shown, ensuring it to disappear if the mouse is outside any volume and to
-            // change the volume hover state if any is under the mouse 
-            // 2) when switching between 3d view and preview the size of the canvas changes if the side panels are visible,
-            // so forces a resize to avoid multiple renders with different sizes (seen as flickering)
-            _refresh_if_shown_on_screen();
-            m_tooltip_enabled = true;
-        }
-        m_mouse.set_start_position_2D_as_invalid();
-//#endif
-    }
-    else if (evt.Leaving()) {
-        _deactivate_undo_redo_toolbar_items();
-
-        // to remove hover on objects when the mouse goes out of this canvas
-        m_mouse.position = Vec2d(-1.0, -1.0);
-        m_dirty = true;
-    }
-    else if (evt.LeftDown() || evt.RightDown() || evt.MiddleDown()) {
-        if (_deactivate_undo_redo_toolbar_items() || _deactivate_search_toolbar_item() || _deactivate_arrange_menu())
-            return;
-
-        // If user pressed left or right button we first check whether this happened
-        // on a volume or not.
-        m_layers_editing.state = LayersEditing::Unknown;
-        if (layer_editing_object_idx != -1 && m_layers_editing.bar_rect_contains(*this, pos(0), pos(1))) {
-            // A volume is selected and the mouse is inside the layer thickness bar.
-            // Start editing the layer height.
-            m_layers_editing.state = LayersEditing::Editing;
-            _perform_layer_editing_action(&evt);
-        }
-#if !ENABLE_NEW_RECTANGLE_SELECTION
-        else if (evt.LeftDown() && (evt.ShiftDown() || evt.AltDown()) && m_picking_enabled) {
-            if (m_gizmos.get_current_type() != GLGizmosManager::SlaSupports
-             && m_gizmos.get_current_type() != GLGizmosManager::FdmSupports
-             && m_gizmos.get_current_type() != GLGizmosManager::Seam
-             && m_gizmos.get_current_type() != GLGizmosManager::MmuSegmentation) {
-                m_rectangle_selection.start_dragging(m_mouse.position, evt.ShiftDown() ? GLSelectionRectangle::Select : GLSelectionRectangle::Deselect);
-                m_dirty = true;
-            }
-        }
-#endif // !ENABLE_NEW_RECTANGLE_SELECTION
-        else {
-#if ENABLE_NEW_RECTANGLE_SELECTION
-            const bool rectangle_selection_dragging = m_rectangle_selection.is_dragging();
-            if (evt.LeftDown() && (evt.ShiftDown() || evt.AltDown()) && m_picking_enabled) {
-                if (m_gizmos.get_current_type() != GLGizmosManager::SlaSupports &&
-                    m_gizmos.get_current_type() != GLGizmosManager::FdmSupports &&
-                    m_gizmos.get_current_type() != GLGizmosManager::Seam &&
-                    m_gizmos.get_current_type() != GLGizmosManager::MmuSegmentation) {
-                    m_rectangle_selection.start_dragging(m_mouse.position, evt.ShiftDown() ? GLSelectionRectangle::EState::Select : GLSelectionRectangle::EState::Deselect);
-                    m_dirty = true;
-                }
-            }
-#endif // ENABLE_NEW_RECTANGLE_SELECTION
-
-            // Select volume in this 3D canvas.
-            // Don't deselect a volume if layer editing is enabled or any gizmo is active. We want the object to stay selected
-            // during the scene manipulation.
-
-#if ENABLE_NEW_RECTANGLE_SELECTION
-            if (m_picking_enabled && !any_gizmo_active && (!m_hover_volume_idxs.empty() || !is_layers_editing_enabled()) && !rectangle_selection_dragging) {
-#else
-            if (m_picking_enabled && (!any_gizmo_active || !evt.CmdDown()) && (!m_hover_volume_idxs.empty() || !is_layers_editing_enabled())) {
-#endif // ENABLE_NEW_RECTANGLE_SELECTION
-                if (evt.LeftDown() && !m_hover_volume_idxs.empty()) {
-                    int volume_idx = get_first_hover_volume_idx();
-                    bool already_selected = m_selection.contains_volume(volume_idx);
-#if ENABLE_NEW_RECTANGLE_SELECTION
-                    bool shift_down = evt.ShiftDown();
-#else
-                    bool ctrl_down = evt.CmdDown();
-#endif // ENABLE_NEW_RECTANGLE_SELECTION
-
-                    Selection::IndicesList curr_idxs = m_selection.get_volume_idxs();
-
-#if ENABLE_NEW_RECTANGLE_SELECTION
-                    if (already_selected && shift_down)
-                        m_selection.remove(volume_idx);
-                    else {
-                        m_selection.add(volume_idx, !shift_down, true);
-#else
-                    if (already_selected && ctrl_down)
-                        m_selection.remove(volume_idx);
-                    else {
-                        m_selection.add(volume_idx, !ctrl_down, true);
-#endif // ENABLE_NEW_RECTANGLE_SELECTION
-                        m_mouse.drag.move_requires_threshold = !already_selected;
-                        if (already_selected)
-                            m_mouse.set_move_start_threshold_position_2D_as_invalid();
-                        else
-                            m_mouse.drag.move_start_threshold_position_2D = pos;
-                    }
-
-                    // propagate event through callback
-                    if (curr_idxs != m_selection.get_volume_idxs()) {
-                        if (m_selection.is_empty())
-                            m_gizmos.reset_all_states();
-                        else
-                            m_gizmos.refresh_on_off_state();
-
-                        m_gizmos.update_data();
-                        post_event(SimpleEvent(EVT_GLCANVAS_OBJECT_SELECT));
-                        m_dirty = true;
-                    }
-                }
-            }
-
-#if ENABLE_NEW_RECTANGLE_SELECTION
-            if (!m_hover_volume_idxs.empty() && !m_rectangle_selection.is_dragging()) {
-#else
-            if (!m_hover_volume_idxs.empty()) {
-#endif // ENABLE_NEW_RECTANGLE_SELECTION
-                if (evt.LeftDown() && m_moving_enabled && m_mouse.drag.move_volume_idx == -1) {
-                    // Only accept the initial position, if it is inside the volume bounding box.
-                    const int volume_idx = get_first_hover_volume_idx();
-                    BoundingBoxf3 volume_bbox = m_volumes.volumes[volume_idx]->transformed_bounding_box();
-                    volume_bbox.offset(1.0);
-                    if ((!any_gizmo_active || !evt.CmdDown()) && volume_bbox.contains(m_mouse.scene_position)) {
-                        m_volumes.volumes[volume_idx]->hover = GLVolume::HS_None;
-                        // The dragging operation is initiated.
-                        m_mouse.drag.move_volume_idx = volume_idx;
-#if ENABLE_NEW_CAMERA_MOVEMENTS
-                        m_selection.setup_cache();
-                        if (!evt.CmdDown())
-#endif // ENABLE_NEW_CAMERA_MOVEMENTS
-                            m_mouse.drag.start_position_3D = m_mouse.scene_position;
-                        m_sequential_print_clearance_first_displacement = true;
-                        m_moving = true;
-                    }
-                }
-            }
-        }
-    }
-#if ENABLE_NEW_CAMERA_MOVEMENTS
-    else if (evt.Dragging() && evt.LeftIsDown() && !evt.CmdDown() && m_layers_editing.state == LayersEditing::Unknown &&
-             m_mouse.drag.move_volume_idx != -1 && m_mouse.is_start_position_3D_defined()) {
-#else
-    else if (evt.Dragging() && evt.LeftIsDown() && m_layers_editing.state == LayersEditing::Unknown && m_mouse.drag.move_volume_idx != -1) {
-#endif // ENABLE_NEW_CAMERA_MOVEMENTS
-    if (!m_mouse.drag.move_requires_threshold) {
-            m_mouse.dragging = true;
-            Vec3d cur_pos = m_mouse.drag.start_position_3D;
-            // we do not want to translate objects if the user just clicked on an object while pressing shift to remove it from the selection and then drag
-            if (m_selection.contains_volume(get_first_hover_volume_idx())) {
-                const Camera& camera = wxGetApp().plater()->get_camera();
-                if (std::abs(camera.get_dir_forward().z()) < EPSILON) {
-                    // side view -> move selected volumes orthogonally to camera view direction
-                    const Linef3 ray = mouse_ray(pos);
-                    const Vec3d dir = ray.unit_vector();
-                    // finds the intersection of the mouse ray with the plane parallel to the camera viewport and passing throught the starting position
-                    // use ray-plane intersection see i.e. https://en.wikipedia.org/wiki/Line%E2%80%93plane_intersection algebric form
-                    // in our case plane normal and ray direction are the same (orthogonal view)
-                    // when moving to perspective camera the negative z unit axis of the camera needs to be transformed in world space and used as plane normal
-                    const Vec3d inters = ray.a + (m_mouse.drag.start_position_3D - ray.a).dot(dir) / dir.squaredNorm() * dir;
-                    // vector from the starting position to the found intersection
-                    const Vec3d inters_vec = inters - m_mouse.drag.start_position_3D;
-
-                    const Vec3d camera_right = camera.get_dir_right();
-                    const Vec3d camera_up = camera.get_dir_up();
-
-                    // finds projection of the vector along the camera axes
-                    const double projection_x = inters_vec.dot(camera_right);
-                    const double projection_z = inters_vec.dot(camera_up);
-
-                    // apply offset
-                    cur_pos = m_mouse.drag.start_position_3D + projection_x * camera_right + projection_z * camera_up;
-                }
-                else {
-                    // Generic view
-                    // Get new position at the same Z of the initial click point.
-                    cur_pos = mouse_ray(pos).intersect_plane(m_mouse.drag.start_position_3D.z());
-                }
-            }
-
-            m_selection.translate(cur_pos - m_mouse.drag.start_position_3D);
-            if (current_printer_technology() == ptFFF && fff_print()->config().complete_objects)
-                update_sequential_clearance();
-            wxGetApp().obj_manipul()->set_dirty();
-            m_dirty = true;
-        }
-    }
-    else if (evt.Dragging() && evt.LeftIsDown() && m_picking_enabled && m_rectangle_selection.is_dragging()) {
-#if ENABLE_NEW_RECTANGLE_SELECTION
-        // keeps the mouse position updated while dragging the selection rectangle
-        m_mouse.position = pos.cast<double>();
-        m_rectangle_selection.dragging(m_mouse.position);
-#else
-        m_rectangle_selection.dragging(pos.cast<double>());
-#endif // ENABLE_NEW_RECTANGLE_SELECTION
-        m_dirty = true;
-    }
-    else if (evt.Dragging()) {
-        m_mouse.dragging = true;
-
-        if (m_layers_editing.state != LayersEditing::Unknown && layer_editing_object_idx != -1) {
-            if (m_layers_editing.state == LayersEditing::Editing) {
-                _perform_layer_editing_action(&evt);
-                m_mouse.position = pos.cast<double>();
-            }
-        }
-        // do not process the dragging if the left mouse was set down in another canvas
-#if ENABLE_NEW_CAMERA_MOVEMENTS
-        else if (evt.LeftIsDown()) {
-            // if dragging over blank area with left button, rotate
-            if ((any_gizmo_active || evt.CmdDown() || m_hover_volume_idxs.empty()) && m_mouse.is_start_position_3D_defined()) {
-#else
-            // if dragging over blank area with left button, rotate
-        else if (evt.LeftIsDown()) {
-            if ((any_gizmo_active || m_hover_volume_idxs.empty()) && m_mouse.is_start_position_3D_defined()) {
-#endif // ENABLE_NEW_CAMERA_MOVEMENTS
-                const Vec3d rot = (Vec3d(pos.x(), pos.y(), 0.0) - m_mouse.drag.start_position_3D) * (PI * TRACKBALLSIZE / 180.0);
-                if (wxGetApp().app_config->get("use_free_camera") == "1")
-                    // Virtual track ball (similar to the 3DConnexion mouse).
-                    wxGetApp().plater()->get_camera().rotate_local_around_target(Vec3d(rot.y(), rot.x(), 0.0));
-                else {
-                    // Forces camera right vector to be parallel to XY plane in case it has been misaligned using the 3D mouse free rotation.
-                    // It is cheaper to call this function right away instead of testing wxGetApp().plater()->get_mouse3d_controller().connected(),
-                    // which checks an atomics (flushes CPU caches).
-                    // See GH issue #3816.
-                    Camera& camera = wxGetApp().plater()->get_camera();
-                    camera.recover_from_free_camera();
-                    camera.rotate_on_sphere(rot.x(), rot.y(), current_printer_technology() != ptSLA);
-                }
-
-                m_dirty = true;
-            }
-            m_mouse.drag.start_position_3D = Vec3d((double)pos.x(), (double)pos.y(), 0.0);
-        }
-        else if (evt.MiddleIsDown() || evt.RightIsDown()) {
-            // If dragging over blank area with right/middle button, pan.
-            if (m_mouse.is_start_position_2D_defined()) {
-                // get point in model space at Z = 0
-                float z = 0.0f;
-                const Vec3d cur_pos = _mouse_to_3d(pos, &z);
-                const Vec3d orig = _mouse_to_3d(m_mouse.drag.start_position_2D, &z);
-                Camera& camera = wxGetApp().plater()->get_camera();
-                if (wxGetApp().app_config->get("use_free_camera") != "1")
-                    // Forces camera right vector to be parallel to XY plane in case it has been misaligned using the 3D mouse free rotation.
-                    // It is cheaper to call this function right away instead of testing wxGetApp().plater()->get_mouse3d_controller().connected(),
-                    // which checks an atomics (flushes CPU caches).
-                    // See GH issue #3816.
-                    camera.recover_from_free_camera();
-
-                camera.set_target(camera.get_target() + orig - cur_pos);
-                m_dirty = true;
-            }
-
-            m_mouse.drag.start_position_2D = pos;
-        }
-    }
-    else if (evt.LeftUp() || evt.MiddleUp() || evt.RightUp()) {
-        if (m_layers_editing.state != LayersEditing::Unknown) {
-            m_layers_editing.state = LayersEditing::Unknown;
-            _stop_timer();
-            m_layers_editing.accept_changes(*this);
-        }
-        else if (m_mouse.drag.move_volume_idx != -1 && m_mouse.dragging) {
-            do_move(L("Move Object"));
-            wxGetApp().obj_manipul()->set_dirty();
-            // Let the plater know that the dragging finished, so a delayed refresh
-            // of the scene with the background processing data should be performed.
-            post_event(SimpleEvent(EVT_GLCANVAS_MOUSE_DRAGGING_FINISHED));
-        }
-        else if (evt.LeftUp() && m_picking_enabled && m_rectangle_selection.is_dragging()) {
-            if (evt.ShiftDown() || evt.AltDown())
-                _update_selection_from_hover();
-
-            m_rectangle_selection.stop_dragging();
-        }
-        else if (evt.LeftUp() && !m_mouse.ignore_left_up && !m_mouse.dragging && m_hover_volume_idxs.empty() && !is_layers_editing_enabled()) {
-            // deselect and propagate event through callback
-            if (!evt.ShiftDown() && (!any_gizmo_active || !evt.CmdDown()) && m_picking_enabled)
-                deselect_all();
-        }
-        else if (evt.RightUp()) {
-            m_mouse.position = pos.cast<double>();
-            // forces a frame render to ensure that m_hover_volume_idxs is updated even when the user right clicks while
-            // the context menu is already shown
-            render();
-            if (!m_hover_volume_idxs.empty()) {
-                // if right clicking on volume, propagate event through callback (shows context menu)
-                int volume_idx = get_first_hover_volume_idx();
-                if (!m_volumes.volumes[volume_idx]->is_wipe_tower // no context menu for the wipe tower
-                    && m_gizmos.get_current_type() != GLGizmosManager::SlaSupports)  // disable context menu when the gizmo is open
-                {
-                    // forces the selection of the volume
-                    /* m_selection.add(volume_idx); // #et_FIXME_if_needed
-                     * To avoid extra "Add-Selection" snapshots,
-                     * call add() with check_for_already_contained=true
-                     * */
-                    m_selection.add(volume_idx, true, true); 
-                    m_gizmos.refresh_on_off_state();
-                    post_event(SimpleEvent(EVT_GLCANVAS_OBJECT_SELECT));
-                    m_gizmos.update_data();
-                    wxGetApp().obj_manipul()->set_dirty();
-                    // forces a frame render to update the view before the context menu is shown
-                    render();
-                }
-            }
-            Vec2d logical_pos = pos.cast<double>();
-#if ENABLE_RETINA_GL
-            const float factor = m_retina_helper->get_scale_factor();
-            logical_pos = logical_pos.cwiseQuotient(Vec2d(factor, factor));
-#endif // ENABLE_RETINA_GL
-            if (!m_mouse.dragging) {
-                // do not post the event if the user is panning the scene
-                // or if right click was done over the wipe tower
-                const bool post_right_click_event = m_hover_volume_idxs.empty() || !m_volumes.volumes[get_first_hover_volume_idx()]->is_wipe_tower;
-                if (post_right_click_event)
-                    post_event(RBtnEvent(EVT_GLCANVAS_RIGHT_CLICK, { logical_pos, m_hover_volume_idxs.empty() }));
-            }
-        }
-
-        mouse_up_cleanup();
-    }
-    else if (evt.Moving()) {
-        m_mouse.position = pos.cast<double>();
-
-        // updates gizmos overlay
-        if (m_selection.is_empty())
-            m_gizmos.reset_all_states();
-
-        m_dirty = true;
-    }
-    else
-        evt.Skip();
-
-    if (m_moving)
-        show_sinking_contours();
-
-#ifdef __WXMSW__
-	if (on_enter_workaround)
-		m_mouse.position = Vec2d(-1., -1.);
-#endif /* __WXMSW__ */
-}
-
-void GLCanvas3D::on_paint(wxPaintEvent& evt)
-{
-    if (m_initialized)
-        m_dirty = true;
-    else
-        // Call render directly, so it gets initialized immediately, not from On Idle handler.
-        this->render();
-}
-
-void GLCanvas3D::on_set_focus(wxFocusEvent& evt)
-{
-    m_tooltip_enabled = false;
-    _refresh_if_shown_on_screen();
-    m_tooltip_enabled = true;
-}
-
-Size GLCanvas3D::get_canvas_size() const
-{
-    int w = 0;
-    int h = 0;
-
-    if (m_canvas != nullptr)
-        m_canvas->GetSize(&w, &h);
-
-#if ENABLE_RETINA_GL
-    const float factor = m_retina_helper->get_scale_factor();
-    w *= factor;
-    h *= factor;
-#else
-    const float factor = 1.0f;
-#endif
-
-    return Size(w, h, factor);
-}
-
-Vec2d GLCanvas3D::get_local_mouse_position() const
-{
-    if (m_canvas == nullptr)
-		return Vec2d::Zero();
-
-    wxPoint mouse_pos = m_canvas->ScreenToClient(wxGetMousePosition());
-    const double factor = 
-#if ENABLE_RETINA_GL
-        m_retina_helper->get_scale_factor();
-#else
-        1.0;
-#endif
-    return Vec2d(factor * mouse_pos.x, factor * mouse_pos.y);
-}
-
-void GLCanvas3D::set_tooltip(const std::string& tooltip)
-{
-    if (m_canvas != nullptr)
-        m_tooltip.set_text(tooltip);
-}
-
-void GLCanvas3D::do_move(const std::string& snapshot_type)
-{
-    if (m_model == nullptr)
-        return;
-
-    if (!snapshot_type.empty())
-        wxGetApp().plater()->take_snapshot(_(snapshot_type));
-
-    std::set<std::pair<int, int>> done;  // keeps track of modified instances
-    bool object_moved = false;
-    Vec3d wipe_tower_origin = Vec3d::Zero();
-
-    Selection::EMode selection_mode = m_selection.get_mode();
-
-    for (const GLVolume* v : m_volumes.volumes) {
-        int object_idx = v->object_idx();
-        int instance_idx = v->instance_idx();
-        int volume_idx = v->volume_idx();
-
-        std::pair<int, int> done_id(object_idx, instance_idx);
-
-        if (0 <= object_idx && object_idx < (int)m_model->objects.size()) {
-            done.insert(done_id);
-
-            // Move instances/volumes
-            ModelObject* model_object = m_model->objects[object_idx];
-            if (model_object != nullptr) {
-                if (selection_mode == Selection::Instance)
-                    model_object->instances[instance_idx]->set_offset(v->get_instance_offset());
-                else if (selection_mode == Selection::Volume)
-                    model_object->volumes[volume_idx]->set_offset(v->get_volume_offset());
-
-                object_moved = true;
-                model_object->invalidate_bounding_box();
-            }
-        }
-#if ENABLE_WIPETOWER_OBJECTID_1000_REMOVAL
-        else if (v->is_wipe_tower)
-            // Move a wipe tower proxy.
-            wipe_tower_origin = v->get_volume_offset();
-#else
-        else if (object_idx == 1000)
-            // Move a wipe tower proxy.
-            wipe_tower_origin = v->get_volume_offset();
-#endif // ENABLE_WIPETOWER_OBJECTID_1000_REMOVAL
-    }
-
-    // Fixes flying instances
-    for (const std::pair<int, int>& i : done) {
-        ModelObject* m = m_model->objects[i.first];
-        const double shift_z = m->get_instance_min_z(i.second);
-        if (current_printer_technology() == ptSLA || shift_z > SINKING_Z_THRESHOLD) {
-            const Vec3d shift(0.0, 0.0, -shift_z);
-            m_selection.translate(i.first, i.second, shift);
-            m->translate_instance(i.second, shift);
-        }
-        wxGetApp().obj_list()->update_info_items(static_cast<size_t>(i.first));
-    }
-
-    // if the selection is not valid to allow for layer editing after the move, we need to turn off the tool if it is running
-    // similar to void Plater::priv::selection_changed()
-    if (!wxGetApp().plater()->can_layers_editing() && is_layers_editing_enabled())
-        post_event(SimpleEvent(EVT_GLTOOLBAR_LAYERSEDITING));
-
-    if (object_moved)
-        post_event(SimpleEvent(EVT_GLCANVAS_INSTANCE_MOVED));
-
-    if (wipe_tower_origin != Vec3d::Zero())
-        post_event(Vec3dEvent(EVT_GLCANVAS_WIPETOWER_MOVED, std::move(wipe_tower_origin)));
-
-    reset_sequential_print_clearance();
-
-    m_dirty = true;
-}
-
-void GLCanvas3D::do_rotate(const std::string& snapshot_type)
-{
-    if (m_model == nullptr)
-        return;
-
-    if (!snapshot_type.empty())
-        wxGetApp().plater()->take_snapshot(_(snapshot_type));
-
-    // stores current min_z of instances
-    std::map<std::pair<int, int>, double> min_zs;
-    for (int i = 0; i < static_cast<int>(m_model->objects.size()); ++i) {
-        const ModelObject* obj = m_model->objects[i];
-        for (int j = 0; j < static_cast<int>(obj->instances.size()); ++j) {
-            if (snapshot_type.empty() && m_selection.get_object_idx() == i) {
-                // This means we are flattening this object. In that case pretend
-                // that it is not sinking (even if it is), so it is placed on bed
-                // later on (whatever is sinking will be left sinking).
-                min_zs[{ i, j }] = SINKING_Z_THRESHOLD;
-            } else
-                min_zs[{ i, j }] = obj->instance_bounding_box(j).min.z();
-
-        }
-    }
-
-    std::set<std::pair<int, int>> done;  // keeps track of modified instances
-
-    Selection::EMode selection_mode = m_selection.get_mode();
-
-    for (const GLVolume* v : m_volumes.volumes) {
-#if ENABLE_WIPETOWER_OBJECTID_1000_REMOVAL
-        if (v->is_wipe_tower) {
-#else
-        int object_idx = v->object_idx();
-        if (object_idx == 1000) { // the wipe tower
-#endif // ENABLE_WIPETOWER_OBJECTID_1000_REMOVAL
-            Vec3d offset = v->get_volume_offset();
-            post_event(Vec3dEvent(EVT_GLCANVAS_WIPETOWER_ROTATED, Vec3d(offset(0), offset(1), v->get_volume_rotation()(2))));
-        }
-#if ENABLE_WIPETOWER_OBJECTID_1000_REMOVAL
-        int object_idx = v->object_idx();
-#endif // ENABLE_WIPETOWER_OBJECTID_1000_REMOVAL
-        if (object_idx < 0 || (int)m_model->objects.size() <= object_idx)
-            continue;
-
-        int instance_idx = v->instance_idx();
-        int volume_idx = v->volume_idx();
-
-        done.insert(std::pair<int, int>(object_idx, instance_idx));
-
-        // Rotate instances/volumes.
-        ModelObject* model_object = m_model->objects[object_idx];
-        if (model_object != nullptr) {
-            if (selection_mode == Selection::Instance) {
-                model_object->instances[instance_idx]->set_rotation(v->get_instance_rotation());
-                model_object->instances[instance_idx]->set_offset(v->get_instance_offset());
-            }
-            else if (selection_mode == Selection::Volume) {
-                model_object->volumes[volume_idx]->set_rotation(v->get_volume_rotation());
-                model_object->volumes[volume_idx]->set_offset(v->get_volume_offset());
-            }
-            model_object->invalidate_bounding_box();
-        }
-    }
-
-    // Fixes sinking/flying instances
-    for (const std::pair<int, int>& i : done) {
-        ModelObject* m = m_model->objects[i.first];
-        const double shift_z = m->get_instance_min_z(i.second);
-        // leave sinking instances as sinking
-        if (min_zs.find({ i.first, i.second })->second >= SINKING_Z_THRESHOLD || shift_z > SINKING_Z_THRESHOLD) {
-            const Vec3d shift(0.0, 0.0, -shift_z);
-            m_selection.translate(i.first, i.second, shift);
-            m->translate_instance(i.second, shift);
-        }
-
-        wxGetApp().obj_list()->update_info_items(static_cast<size_t>(i.first));
-    }
-
-    if (!done.empty())
-        post_event(SimpleEvent(EVT_GLCANVAS_INSTANCE_ROTATED));
-
-    m_dirty = true;
-}
-
-void GLCanvas3D::do_scale(const std::string& snapshot_type)
-{
-    if (m_model == nullptr)
-        return;
-
-    if (!snapshot_type.empty())
-        wxGetApp().plater()->take_snapshot(_(snapshot_type));
-
-    // stores current min_z of instances
-    std::map<std::pair<int, int>, double> min_zs;
-    if (!snapshot_type.empty()) {
-        for (int i = 0; i < static_cast<int>(m_model->objects.size()); ++i) {
-            const ModelObject* obj = m_model->objects[i];
-            for (int j = 0; j < static_cast<int>(obj->instances.size()); ++j) {
-                min_zs[{ i, j }] = obj->instance_bounding_box(j).min.z();
-            }
-        }
-    }
-
-    std::set<std::pair<int, int>> done;  // keeps track of modified instances
-
-    Selection::EMode selection_mode = m_selection.get_mode();
-
-    for (const GLVolume* v : m_volumes.volumes) {
-        int object_idx = v->object_idx();
-        if (object_idx < 0 || (int)m_model->objects.size() <= object_idx)
-            continue;
-
-        int instance_idx = v->instance_idx();
-        int volume_idx = v->volume_idx();
-
-        done.insert(std::pair<int, int>(object_idx, instance_idx));
-
-        // Rotate instances/volumes
-        ModelObject* model_object = m_model->objects[object_idx];
-        if (model_object != nullptr) {
-            if (selection_mode == Selection::Instance) {
-                model_object->instances[instance_idx]->set_scaling_factor(v->get_instance_scaling_factor());
-                model_object->instances[instance_idx]->set_offset(v->get_instance_offset());
-            }
-            else if (selection_mode == Selection::Volume) {
-                model_object->instances[instance_idx]->set_offset(v->get_instance_offset());
-                model_object->volumes[volume_idx]->set_scaling_factor(v->get_volume_scaling_factor());
-                model_object->volumes[volume_idx]->set_offset(v->get_volume_offset());
-            }
-            model_object->invalidate_bounding_box();
-        }
-    }
-
-    // Fixes sinking/flying instances
-    for (const std::pair<int, int>& i : done) {
-        ModelObject* m = m_model->objects[i.first];
-        double shift_z = m->get_instance_min_z(i.second);
-        // leave sinking instances as sinking
-        if (min_zs.empty() || min_zs.find({ i.first, i.second })->second >= SINKING_Z_THRESHOLD || shift_z > SINKING_Z_THRESHOLD) {
-            Vec3d shift(0.0, 0.0, -shift_z);
-            m_selection.translate(i.first, i.second, shift);
-            m->translate_instance(i.second, shift);
-        }
-        wxGetApp().obj_list()->update_info_items(static_cast<size_t>(i.first));
-    }
-
-    if (!done.empty())
-        post_event(SimpleEvent(EVT_GLCANVAS_INSTANCE_SCALED));
-
-    m_dirty = true;
-}
-
-void GLCanvas3D::do_mirror(const std::string& snapshot_type)
-{
-    if (m_model == nullptr)
-        return;
-
-    if (!snapshot_type.empty())
-        wxGetApp().plater()->take_snapshot(_(snapshot_type));
-
-    // stores current min_z of instances
-    std::map<std::pair<int, int>, double> min_zs;
-    if (!snapshot_type.empty()) {
-        for (int i = 0; i < static_cast<int>(m_model->objects.size()); ++i) {
-            const ModelObject* obj = m_model->objects[i];
-            for (int j = 0; j < static_cast<int>(obj->instances.size()); ++j) {
-                min_zs[{ i, j }] = obj->instance_bounding_box(j).min.z();
-            }
-        }
-    }
-
-    std::set<std::pair<int, int>> done;  // keeps track of modified instances
-
-    Selection::EMode selection_mode = m_selection.get_mode();
-
-    for (const GLVolume* v : m_volumes.volumes) {
-        int object_idx = v->object_idx();
-        if (object_idx < 0 || (int)m_model->objects.size() <= object_idx)
-            continue;
-
-        int instance_idx = v->instance_idx();
-        int volume_idx = v->volume_idx();
-
-        done.insert(std::pair<int, int>(object_idx, instance_idx));
-
-        // Mirror instances/volumes
-        ModelObject* model_object = m_model->objects[object_idx];
-        if (model_object != nullptr) {
-            if (selection_mode == Selection::Instance)
-                model_object->instances[instance_idx]->set_mirror(v->get_instance_mirror());
-            else if (selection_mode == Selection::Volume)
-                model_object->volumes[volume_idx]->set_mirror(v->get_volume_mirror());
-
-            model_object->invalidate_bounding_box();
-        }
-    }
-
-    // Fixes sinking/flying instances
-    for (const std::pair<int, int>& i : done) {
-        ModelObject* m = m_model->objects[i.first];
-        double shift_z = m->get_instance_min_z(i.second);
-        // leave sinking instances as sinking
-        if (min_zs.empty() || min_zs.find({ i.first, i.second })->second >= SINKING_Z_THRESHOLD || shift_z > SINKING_Z_THRESHOLD) {
-            Vec3d shift(0.0, 0.0, -shift_z);
-            m_selection.translate(i.first, i.second, shift);
-            m->translate_instance(i.second, shift);
-        }
-        wxGetApp().obj_list()->update_info_items(static_cast<size_t>(i.first));
-    }
-
-    post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS));
-
-    m_dirty = true;
-}
-
-void GLCanvas3D::update_gizmos_on_off_state()
-{
-    set_as_dirty();
-    m_gizmos.update_data();
-    m_gizmos.refresh_on_off_state();
-}
-
-void GLCanvas3D::handle_sidebar_focus_event(const std::string& opt_key, bool focus_on)
-{
-    m_sidebar_field = focus_on ? opt_key : "";
-    if (!m_sidebar_field.empty())
-        m_gizmos.reset_all_states();
-
-    m_dirty = true;
-}
-
-void GLCanvas3D::handle_layers_data_focus_event(const t_layer_height_range range, const EditorType type)
-{
-    std::string field = "layer_" + std::to_string(type) + "_" + std::to_string(range.first) + "_" + std::to_string(range.second);
-    handle_sidebar_focus_event(field, true);
-}
-
-void GLCanvas3D::update_ui_from_settings()
-{
-    m_dirty = true;
-
-#if __APPLE__
-    // Update OpenGL scaling on OSX after the user toggled the "use_retina_opengl" settings in Preferences dialog.
-    const float orig_scaling = m_retina_helper->get_scale_factor();
-
-    const bool use_retina = wxGetApp().app_config->get("use_retina_opengl") == "1";
-    BOOST_LOG_TRIVIAL(debug) << "GLCanvas3D: Use Retina OpenGL: " << use_retina;
-    m_retina_helper->set_use_retina(use_retina);
-    const float new_scaling = m_retina_helper->get_scale_factor();
-
-    if (new_scaling != orig_scaling) {
-        BOOST_LOG_TRIVIAL(debug) << "GLCanvas3D: Scaling factor: " << new_scaling;
-
-        Camera& camera = wxGetApp().plater()->get_camera();
-        camera.set_zoom(camera.get_zoom() * new_scaling / orig_scaling);
-        _refresh_if_shown_on_screen();
-    }
-#endif // ENABLE_RETINA_GL
-
-    if (wxGetApp().is_editor())
-        wxGetApp().plater()->enable_collapse_toolbar(wxGetApp().app_config->get("show_collapse_button") == "1");
-}
-
-GLCanvas3D::WipeTowerInfo GLCanvas3D::get_wipe_tower_info() const
-{
-    WipeTowerInfo wti;
-    
-    for (const GLVolume* vol : m_volumes.volumes) {
-        if (vol->is_wipe_tower) {
-            wti.m_pos = Vec2d(m_config->opt_float("wipe_tower_x"),
-                            m_config->opt_float("wipe_tower_y"));
-            wti.m_rotation = (M_PI/180.) * m_config->opt_float("wipe_tower_rotation_angle");
-            const BoundingBoxf3& bb = vol->bounding_box();
-            wti.m_bb = BoundingBoxf{to_2d(bb.min), to_2d(bb.max)};
-            break;
-        }
-    }
-    
-    return wti;
-}
-
-Linef3 GLCanvas3D::mouse_ray(const Point& mouse_pos)
-{
-    float z0 = 0.0f;
-    float z1 = 1.0f;
-    return Linef3(_mouse_to_3d(mouse_pos, &z0), _mouse_to_3d(mouse_pos, &z1));
-}
-
-double GLCanvas3D::get_size_proportional_to_max_bed_size(double factor) const
-{
-    const BoundingBoxf& bbox = m_bed.build_volume().bounding_volume2d();
-    return factor * std::max(bbox.size()[0], bbox.size()[1]);
-}
-
-void GLCanvas3D::set_cursor(ECursorType type)
-{
-    if ((m_canvas != nullptr) && (m_cursor_type != type))
-    {
-        switch (type)
-        {
-        case Standard: { m_canvas->SetCursor(*wxSTANDARD_CURSOR); break; }
-        case Cross: { m_canvas->SetCursor(*wxCROSS_CURSOR); break; }
-        }
-
-        m_cursor_type = type;
-    }
-}
-
-void GLCanvas3D::msw_rescale()
-{
-#if ENABLE_PREVIEW_LAYOUT
-    m_gcode_viewer.invalidate_legend();
-#endif // ENABLE_PREVIEW_LAYOUT
-}
-
-void GLCanvas3D::update_tooltip_for_settings_item_in_main_toolbar()
-{
-    std::string new_tooltip = _u8L("Switch to Settings") + 
-                             "\n" + "[" + GUI::shortkey_ctrl_prefix() + "2] - " + _u8L("Print Settings Tab")    + 
-                             "\n" + "[" + GUI::shortkey_ctrl_prefix() + "3] - " + (current_printer_technology() == ptFFF ? _u8L("Filament Settings Tab") : _u8L("Material Settings Tab")) +
-                             "\n" + "[" + GUI::shortkey_ctrl_prefix() + "4] - " + _u8L("Printer Settings Tab") ;
-
-    m_main_toolbar.set_tooltip(get_main_toolbar_item_id("settings"), new_tooltip);
-}
-
-bool GLCanvas3D::has_toolpaths_to_export() const
-{
-    return m_gcode_viewer.can_export_toolpaths();
-}
-
-void GLCanvas3D::export_toolpaths_to_obj(const char* filename) const
-{
-    m_gcode_viewer.export_toolpaths_to_obj(filename);
-}
-
-void GLCanvas3D::mouse_up_cleanup()
-{
-    m_moving = false;
-    m_mouse.drag.move_volume_idx = -1;
-    m_mouse.set_start_position_3D_as_invalid();
-    m_mouse.set_start_position_2D_as_invalid();
-    m_mouse.dragging = false;
-    m_mouse.ignore_left_up = false;
-    m_dirty = true;
-
-    if (m_canvas->HasCapture())
-        m_canvas->ReleaseMouse();
-}
-
-void GLCanvas3D::update_sequential_clearance()
-{
-    if (current_printer_technology() != ptFFF || !fff_print()->config().complete_objects)
-        return;
-
-    if (m_layers_editing.is_enabled() || m_gizmos.is_dragging())
-        return;
-
-    // collects instance transformations from volumes
-    // first define temporary cache
-    unsigned int instances_count = 0;
-    std::vector<std::vector<std::optional<Geometry::Transformation>>> instance_transforms;
-    for (size_t obj = 0; obj < m_model->objects.size(); ++obj) {
-        instance_transforms.emplace_back(std::vector<std::optional<Geometry::Transformation>>());
-        const ModelObject* model_object = m_model->objects[obj];
-        for (size_t i = 0; i < model_object->instances.size(); ++i) {
-            instance_transforms[obj].emplace_back(std::optional<Geometry::Transformation>());
-            ++instances_count;
-        }
-    }
-
-    if (instances_count == 1)
-        return;
-
-    // second fill temporary cache with data from volumes
-    for (const GLVolume* v : m_volumes.volumes) {
-        if (v->is_modifier || v->is_wipe_tower)
-            continue;
-
-        auto& transform = instance_transforms[v->object_idx()][v->instance_idx()];
-        if (!transform.has_value())
-            transform = v->get_instance_transformation();
-    }
-
-    // calculates objects 2d hulls (see also: Print::sequential_print_horizontal_clearance_valid())
-    // this is done only the first time this method is called while moving the mouse,
-    // the results are then cached for following displacements
-    if (m_sequential_print_clearance_first_displacement) {
-        m_sequential_print_clearance.m_hull_2d_cache.clear();
-        float shrink_factor = static_cast<float>(scale_(0.5 * fff_print()->config().extruder_clearance_radius.value - EPSILON));
-        double mitter_limit = scale_(0.1);
-        m_sequential_print_clearance.m_hull_2d_cache.reserve(m_model->objects.size());
-        for (size_t i = 0; i < m_model->objects.size(); ++i) {
-            ModelObject* model_object = m_model->objects[i];
-            ModelInstance* model_instance0 = model_object->instances.front();
-            Polygon hull_2d = offset(model_object->convex_hull_2d(Geometry::assemble_transform({ 0.0, 0.0, model_instance0->get_offset().z() }, model_instance0->get_rotation(),
-                model_instance0->get_scaling_factor(), model_instance0->get_mirror())),
-                // Shrink the extruder_clearance_radius a tiny bit, so that if the object arrangement algorithm placed the objects
-                // exactly by satisfying the extruder_clearance_radius, this test will not trigger collision.
-                shrink_factor,
-                jtRound, mitter_limit).front();
-
-            Pointf3s& cache_hull_2d = m_sequential_print_clearance.m_hull_2d_cache.emplace_back(Pointf3s());
-            cache_hull_2d.reserve(hull_2d.points.size());
-            for (const Point& p : hull_2d.points) {
-                cache_hull_2d.emplace_back(unscale<double>(p.x()), unscale<double>(p.y()), 0.0);
-            }
-        }
-        m_sequential_print_clearance_first_displacement = false;
-    }
-
-    // calculates instances 2d hulls (see also: Print::sequential_print_horizontal_clearance_valid())
-    Polygons polygons;
-    polygons.reserve(instances_count);
-    for (size_t i = 0; i < instance_transforms.size(); ++i) {
-        const auto& instances = instance_transforms[i];
-        double rotation_z0 = instances.front()->get_rotation().z();
-        for (const auto& instance : instances) {
-            Geometry::Transformation transformation;
-            const Vec3d& offset = instance->get_offset();
-            transformation.set_offset({ offset.x(), offset.y(), 0.0 });
-            transformation.set_rotation(Z, instance->get_rotation().z() - rotation_z0);
-            const Transform3d& trafo = transformation.get_matrix();
-            const Pointf3s& hull_2d = m_sequential_print_clearance.m_hull_2d_cache[i];
-            Points inst_pts;
-            inst_pts.reserve(hull_2d.size());
-            for (size_t j = 0; j < hull_2d.size(); ++j) {
-                const Vec3d p = trafo * hull_2d[j];
-                inst_pts.emplace_back(scaled<double>(p.x()), scaled<double>(p.y()));
-            }
-            polygons.emplace_back(Geometry::convex_hull(std::move(inst_pts)));
-        }
-    }
-
-    // sends instances 2d hulls to be rendered
-    set_sequential_print_clearance_visible(true);
-    set_sequential_print_clearance_render_fill(false);
-    set_sequential_print_clearance_polygons(polygons);
-}
-
-bool GLCanvas3D::is_object_sinking(int object_idx) const
-{
-    for (const GLVolume* v : m_volumes.volumes) {
-        if (v->object_idx() == object_idx && (v->is_sinking() || (!v->is_modifier && v->is_below_printbed())))
-            return true;
-    }
-    return false;
-}
-
-bool GLCanvas3D::_is_shown_on_screen() const
-{
-    return (m_canvas != nullptr) ? m_canvas->IsShownOnScreen() : false;
-}
-
-// Getter for the const char*[]
-static bool string_getter(const bool is_undo, int idx, const char** out_text)
-{
-    return wxGetApp().plater()->undo_redo_string_getter(is_undo, idx, out_text);
-}
-
-bool GLCanvas3D::_render_undo_redo_stack(const bool is_undo, float pos_x)
-{
-    bool action_taken = false;
-
-    ImGuiWrapper* imgui = wxGetApp().imgui();
-
-#if ENABLE_GL_SHADERS_ATTRIBUTES
-    imgui->set_next_window_pos(pos_x, m_undoredo_toolbar.get_height(), ImGuiCond_Always, 0.5f, 0.0f);
-#else
-    const float x = pos_x * (float)wxGetApp().plater()->get_camera().get_zoom() + 0.5f * (float)get_canvas_size().get_width();
-    imgui->set_next_window_pos(x, m_undoredo_toolbar.get_height(), ImGuiCond_Always, 0.5f, 0.0f);
-#endif // ENABLE_GL_SHADERS_ATTRIBUTES
-    std::string title = is_undo ? L("Undo History") : L("Redo History");
-    imgui->begin(_(title), ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse);
-
-    int hovered = m_imgui_undo_redo_hovered_pos;
-    int selected = -1;
-    float em = static_cast<float>(wxGetApp().em_unit());
-#if ENABLE_RETINA_GL
-	em *= m_retina_helper->get_scale_factor();
-#endif
-
-    if (imgui->undo_redo_list(ImVec2(18 * em, 26 * em), is_undo, &string_getter, hovered, selected, m_mouse_wheel))
-        m_imgui_undo_redo_hovered_pos = hovered;
-    else
-        m_imgui_undo_redo_hovered_pos = -1;
-
-    if (selected >= 0) {
-        is_undo ? wxGetApp().plater()->undo_to(selected) : wxGetApp().plater()->redo_to(selected);
-        action_taken = true;
-    }
-
-    imgui->text(wxString::Format(is_undo ? _L_PLURAL("Undo %1$d Action", "Undo %1$d Actions", hovered + 1) : _L_PLURAL("Redo %1$d Action", "Redo %1$d Actions", hovered + 1), hovered + 1));
-
-    imgui->end();
-
-    return action_taken;
-}
-
-// Getter for the const char*[] for the search list 
-static bool search_string_getter(int idx, const char** label, const char** tooltip)
-{
-    return wxGetApp().plater()->search_string_getter(idx, label, tooltip);
-}
-
-bool GLCanvas3D::_render_search_list(float pos_x)
-{
-    bool action_taken = false;
-    ImGuiWrapper* imgui = wxGetApp().imgui();
-
-#if ENABLE_GL_SHADERS_ATTRIBUTES
-    imgui->set_next_window_pos(pos_x, m_main_toolbar.get_height(), ImGuiCond_Always, 0.5f, 0.0f);
-#else
-    const float x = /*pos_x * (float)wxGetApp().plater()->get_camera().get_zoom() + */0.5f * (float)get_canvas_size().get_width();
-    imgui->set_next_window_pos(x, m_main_toolbar.get_height(), ImGuiCond_Always, 0.5f, 0.0f);
-#endif // ENABLE_GL_SHADERS_ATTRIBUTES
-    std::string title = L("Search");
-    imgui->begin(_(title), ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse);
-
-    int selected = -1;
-    bool edited = false;
-    float em = static_cast<float>(wxGetApp().em_unit());
-#if ENABLE_RETINA_GL
-	em *= m_retina_helper->get_scale_factor();
-#endif // ENABLE_RETINA_GL
-
-    Sidebar& sidebar = wxGetApp().sidebar();
-
-    std::string& search_line = sidebar.get_search_line();
-    char *s = new char[255];
-    strcpy(s, search_line.empty() ? _u8L("Enter a search term").c_str() : search_line.c_str());
-
-    imgui->search_list(ImVec2(45 * em, 30 * em), &search_string_getter, s,
-        sidebar.get_searcher().view_params,
-        selected, edited, m_mouse_wheel, wxGetApp().is_localized());
-
-    search_line = s;
-    delete [] s;
-    if (search_line == _u8L("Enter a search term"))
-        search_line.clear();
-
-    if (edited)
-        sidebar.search();
-
-    if (selected >= 0) {
-        // selected == 9999 means that Esc kye was pressed
-        /*// revert commit https://github.com/prusa3d/PrusaSlicer/commit/91897589928789b261ca0dc735ffd46f2b0b99f2
-        if (selected == 9999)
-            action_taken = true;
-        else
-            sidebar.jump_to_option(selected);*/
-        if (selected != 9999) {
-            imgui->end(); // end imgui before the jump to option
-            sidebar.jump_to_option(selected);
-            return true;
-        }
-        action_taken = true;
-    }
-
-    imgui->end();
-
-    return action_taken;
-}
-
-bool GLCanvas3D::_render_arrange_menu(float pos_x)
-{
-    ImGuiWrapper *imgui = wxGetApp().imgui();
-
-#if ENABLE_GL_SHADERS_ATTRIBUTES
-    imgui->set_next_window_pos(pos_x, m_main_toolbar.get_height(), ImGuiCond_Always, 0.5f, 0.0f);
-#else
-    auto canvas_w = float(get_canvas_size().get_width());
-    const float x = pos_x * float(wxGetApp().plater()->get_camera().get_zoom()) + 0.5f * canvas_w;
-    imgui->set_next_window_pos(x, m_main_toolbar.get_height(), ImGuiCond_Always, 0.5f, 0.0f);
-#endif // ENABLE_GL_SHADERS_ATTRIBUTES
-
-    imgui->begin(_L("Arrange options"), ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoCollapse);
-
-    ArrangeSettings settings = get_arrange_settings();
-    ArrangeSettings &settings_out = get_arrange_settings();
-
-    auto &appcfg = wxGetApp().app_config;
-    PrinterTechnology ptech = current_printer_technology();
-
-    bool settings_changed = false;
-    float dist_min = 0.f;
-    std::string dist_key = "min_object_distance", rot_key = "enable_rotation";
-    std::string postfix;
-
-    if (ptech == ptSLA) {
-        dist_min     = 0.f;
-        postfix      = "_sla";
-    } else if (ptech == ptFFF) {
-        auto co_opt = m_config->option<ConfigOptionBool>("complete_objects");
-        if (co_opt && co_opt->value) {
-            dist_min     = float(min_object_distance(*m_config));
-            postfix      = "_fff_seq_print";
-        } else {
-            dist_min     = 0.f;
-            postfix     = "_fff";
-        }
-    }
-
-    dist_key += postfix;
-    rot_key  += postfix;
-
-    imgui->text(GUI::format_wxstr(_L("Press %1%left mouse button to enter the exact value"), shortkey_ctrl_prefix()));
-
-    if (imgui->slider_float(_L("Spacing"), &settings.distance, dist_min, 100.0f, "%5.2f") || dist_min > settings.distance) {
-        settings.distance = std::max(dist_min, settings.distance);
-        settings_out.distance = settings.distance;
-        appcfg->set("arrange", dist_key.c_str(), float_to_string_decimal_point(settings_out.distance));
-        settings_changed = true;
-    }
-
-    if (imgui->checkbox(_L("Enable rotations (slow)"), settings.enable_rotation)) {
-        settings_out.enable_rotation = settings.enable_rotation;
-        appcfg->set("arrange", rot_key.c_str(), settings_out.enable_rotation? "1" : "0");
-        settings_changed = true;
-    }
-
-    ImGui::Separator();
-
-    if (imgui->button(_L("Reset"))) {
-        settings_out = ArrangeSettings{};
-        settings_out.distance = std::max(dist_min, settings_out.distance);
-        appcfg->set("arrange", dist_key.c_str(), float_to_string_decimal_point(settings_out.distance));
-        appcfg->set("arrange", rot_key.c_str(), settings_out.enable_rotation? "1" : "0");
-        settings_changed = true;
-    }
-
-    ImGui::SameLine();
-
-    if (imgui->button(_L("Arrange"))) {
-        wxGetApp().plater()->arrange();
-    }
-
-    imgui->end();
-
-    return settings_changed;
-}
-
-#define ENABLE_THUMBNAIL_GENERATOR_DEBUG_OUTPUT 0
-#if ENABLE_THUMBNAIL_GENERATOR_DEBUG_OUTPUT
-static void debug_output_thumbnail(const ThumbnailData& thumbnail_data)
-{
-    // debug export of generated image
-    wxImage image(thumbnail_data.width, thumbnail_data.height);
-    image.InitAlpha();
-
-    for (unsigned int r = 0; r < thumbnail_data.height; ++r)
-    {
-        unsigned int rr = (thumbnail_data.height - 1 - r) * thumbnail_data.width;
-        for (unsigned int c = 0; c < thumbnail_data.width; ++c)
-        {
-            unsigned char* px = (unsigned char*)thumbnail_data.pixels.data() + 4 * (rr + c);
-            image.SetRGB((int)c, (int)r, px[0], px[1], px[2]);
-            image.SetAlpha((int)c, (int)r, px[3]);
-        }
-    }
-
-    image.SaveFile("C:/prusa/test/test.png", wxBITMAP_TYPE_PNG);
-}
-#endif // ENABLE_THUMBNAIL_GENERATOR_DEBUG_OUTPUT
-
-void GLCanvas3D::_render_thumbnail_internal(ThumbnailData& thumbnail_data, const ThumbnailsParams& thumbnail_params, const GLVolumeCollection& volumes, Camera::EType camera_type)
-{
-    auto is_visible = [](const GLVolume& v) {
-        bool ret = v.printable;
-        ret &= (!v.shader_outside_printer_detection_enabled || !v.is_outside);
-        return ret;
-    };
-
-    GLVolumePtrs visible_volumes;
-
-    for (GLVolume* vol : volumes.volumes) {
-        if (!vol->is_modifier && !vol->is_wipe_tower && (!thumbnail_params.parts_only || vol->composite_id.volume_id >= 0)) {
-            if (!thumbnail_params.printable_only || is_visible(*vol))
-                visible_volumes.emplace_back(vol);
-        }
-    }
-
-    BoundingBoxf3 volumes_box;
-    if (!visible_volumes.empty()) {
-        for (const GLVolume* vol : visible_volumes) {
-            volumes_box.merge(vol->transformed_bounding_box());
-        }
-    }
-    else
-        // This happens for empty projects
-        volumes_box = m_bed.extended_bounding_box();
-
-    Camera camera;
-    camera.set_type(camera_type);
-    camera.set_scene_box(scene_bounding_box());
-    camera.apply_viewport(0, 0, thumbnail_data.width, thumbnail_data.height);
-    camera.zoom_to_box(volumes_box);
-#if !ENABLE_LEGACY_OPENGL_REMOVAL
-    camera.apply_view_matrix();
-#endif // !ENABLE_LEGACY_OPENGL_REMOVAL
-
-#if ENABLE_GL_SHADERS_ATTRIBUTES
-    const Transform3d& view_matrix = camera.get_view_matrix();
-#endif // ENABLE_GL_SHADERS_ATTRIBUTES
-
-    double near_z = -1.0;
-    double far_z = -1.0;
-
-    if (thumbnail_params.show_bed) {
-        // extends the near and far z of the frustrum to avoid the bed being clipped
-
-        // box in eye space
-#if ENABLE_GL_SHADERS_ATTRIBUTES
-        const BoundingBoxf3 t_bed_box = m_bed.extended_bounding_box().transformed(view_matrix);
-#else
-        const BoundingBoxf3 t_bed_box = m_bed.extended_bounding_box().transformed(camera.get_view_matrix());
-#endif // ENABLE_GL_SHADERS_ATTRIBUTES
-        near_z = -t_bed_box.max.z();
-        far_z = -t_bed_box.min.z();
-    }
-
-    camera.apply_projection(volumes_box, near_z, far_z);
-
-    GLShaderProgram* shader = wxGetApp().get_shader("gouraud_light");
-    if (shader == nullptr)
-        return;
-
-    if (thumbnail_params.transparent_background)
-        glsafe(::glClearColor(0.0f, 0.0f, 0.0f, 0.0f));
-
-    glsafe(::glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT));
-    glsafe(::glEnable(GL_DEPTH_TEST));
-
-    shader->start_using();
-    shader->set_uniform("emission_factor", 0.0f);
-
-#if ENABLE_GL_SHADERS_ATTRIBUTES
-    const Transform3d& projection_matrix = camera.get_projection_matrix();
-#endif // ENABLE_GL_SHADERS_ATTRIBUTES
-
-    for (GLVolume* vol : visible_volumes) {
-#if ENABLE_LEGACY_OPENGL_REMOVAL
-        vol->model.set_color((vol->printable && !vol->is_outside) ? (current_printer_technology() == ptSLA ? vol->color : ColorRGBA::ORANGE()) : ColorRGBA::GRAY());
-#else
-        shader->set_uniform("uniform_color", (vol->printable && !vol->is_outside) ? (current_printer_technology() == ptSLA ? vol->color : ColorRGBA::ORANGE()) : ColorRGBA::GRAY());
-#endif // ENABLE_LEGACY_OPENGL_REMOVAL
-        // the volume may have been deactivated by an active gizmo
-        const bool is_active = vol->is_active;
-        vol->is_active = true;
-#if ENABLE_GL_SHADERS_ATTRIBUTES
-        const Transform3d matrix = view_matrix * vol->world_matrix();
-        shader->set_uniform("view_model_matrix", matrix);
-        shader->set_uniform("projection_matrix", projection_matrix);
-        shader->set_uniform("normal_matrix", (Matrix3d)matrix.matrix().block(0, 0, 3, 3).inverse().transpose());
-#endif // ENABLE_GL_SHADERS_ATTRIBUTES
-        vol->render();
-        vol->is_active = is_active;
-    }
-
-    shader->stop_using();
-
-    glsafe(::glDisable(GL_DEPTH_TEST));
-
-    if (thumbnail_params.show_bed)
-#if ENABLE_GL_SHADERS_ATTRIBUTES
-        _render_bed(view_matrix, projection_matrix, !camera.is_looking_downward(), false);
-#else
-        _render_bed(!camera.is_looking_downward(), false);
-#endif // ENABLE_GL_SHADERS_ATTRIBUTES
-
-    // restore background color
-    if (thumbnail_params.transparent_background)
-        glsafe(::glClearColor(1.0f, 1.0f, 1.0f, 1.0f));
-}
-
-void GLCanvas3D::_render_thumbnail_framebuffer(ThumbnailData& thumbnail_data, unsigned int w, unsigned int h, const ThumbnailsParams& thumbnail_params, const GLVolumeCollection& volumes, Camera::EType camera_type)
-{
-    thumbnail_data.set(w, h);
-    if (!thumbnail_data.is_valid())
-        return;
-
-    bool multisample = m_multisample_allowed;
-    if (multisample)
-        glsafe(::glEnable(GL_MULTISAMPLE));
-
-    GLint max_samples;
-    glsafe(::glGetIntegerv(GL_MAX_SAMPLES, &max_samples));
-    GLsizei num_samples = max_samples / 2;
-
-    GLuint render_fbo;
-    glsafe(::glGenFramebuffers(1, &render_fbo));
-    glsafe(::glBindFramebuffer(GL_FRAMEBUFFER, render_fbo));
-
-    GLuint render_tex = 0;
-    GLuint render_tex_buffer = 0;
-    if (multisample) {
-        // use renderbuffer instead of texture to avoid the need to use glTexImage2DMultisample which is available only since OpenGL 3.2
-        glsafe(::glGenRenderbuffers(1, &render_tex_buffer));
-        glsafe(::glBindRenderbuffer(GL_RENDERBUFFER, render_tex_buffer));
-        glsafe(::glRenderbufferStorageMultisample(GL_RENDERBUFFER, num_samples, GL_RGBA8, w, h));
-        glsafe(::glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, render_tex_buffer));
-    }
-    else {
-        glsafe(::glGenTextures(1, &render_tex));
-        glsafe(::glBindTexture(GL_TEXTURE_2D, render_tex));
-        glsafe(::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr));
-        glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR));
-        glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR));
-        glsafe(::glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, render_tex, 0));
-    }
-
-    GLuint render_depth;
-    glsafe(::glGenRenderbuffers(1, &render_depth));
-    glsafe(::glBindRenderbuffer(GL_RENDERBUFFER, render_depth));
-    if (multisample)
-        glsafe(::glRenderbufferStorageMultisample(GL_RENDERBUFFER, num_samples, GL_DEPTH_COMPONENT24, w, h));
-    else
-        glsafe(::glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT, w, h));
-
-    glsafe(::glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, render_depth));
-
-    GLenum drawBufs[] = { GL_COLOR_ATTACHMENT0 };
-    glsafe(::glDrawBuffers(1, drawBufs));
-
-    if (::glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE) {
-        _render_thumbnail_internal(thumbnail_data, thumbnail_params, volumes, camera_type);
-
-        if (multisample) {
-            GLuint resolve_fbo;
-            glsafe(::glGenFramebuffers(1, &resolve_fbo));
-            glsafe(::glBindFramebuffer(GL_FRAMEBUFFER, resolve_fbo));
-
-            GLuint resolve_tex;
-            glsafe(::glGenTextures(1, &resolve_tex));
-            glsafe(::glBindTexture(GL_TEXTURE_2D, resolve_tex));
-            glsafe(::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr));
-            glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR));
-            glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR));
-            glsafe(::glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, resolve_tex, 0));
-
-            glsafe(::glDrawBuffers(1, drawBufs));
-
-            if (::glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE) {
-                glsafe(::glBindFramebuffer(GL_READ_FRAMEBUFFER, render_fbo));
-                glsafe(::glBindFramebuffer(GL_DRAW_FRAMEBUFFER, resolve_fbo));
-                glsafe(::glBlitFramebuffer(0, 0, w, h, 0, 0, w, h, GL_COLOR_BUFFER_BIT, GL_LINEAR));
-
-                glsafe(::glBindFramebuffer(GL_READ_FRAMEBUFFER, resolve_fbo));
-                glsafe(::glReadPixels(0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, (void*)thumbnail_data.pixels.data()));
-            }
-
-            glsafe(::glDeleteTextures(1, &resolve_tex));
-            glsafe(::glDeleteFramebuffers(1, &resolve_fbo));
-        }
-        else
-            glsafe(::glReadPixels(0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, (void*)thumbnail_data.pixels.data()));
-
-#if ENABLE_THUMBNAIL_GENERATOR_DEBUG_OUTPUT
-        debug_output_thumbnail(thumbnail_data);
-#endif // ENABLE_THUMBNAIL_GENERATOR_DEBUG_OUTPUT
-    }
-
-    glsafe(::glBindFramebuffer(GL_FRAMEBUFFER, 0));
-    glsafe(::glDeleteRenderbuffers(1, &render_depth));
-    if (render_tex_buffer != 0)
-        glsafe(::glDeleteRenderbuffers(1, &render_tex_buffer));
-    if (render_tex != 0)
-        glsafe(::glDeleteTextures(1, &render_tex));
-    glsafe(::glDeleteFramebuffers(1, &render_fbo));
-
-    if (multisample)
-        glsafe(::glDisable(GL_MULTISAMPLE));
-}
-
-void GLCanvas3D::_render_thumbnail_framebuffer_ext(ThumbnailData& thumbnail_data, unsigned int w, unsigned int h, const ThumbnailsParams& thumbnail_params, const GLVolumeCollection& volumes, Camera::EType camera_type)
-{
-    thumbnail_data.set(w, h);
-    if (!thumbnail_data.is_valid())
-        return;
-
-    bool multisample = m_multisample_allowed;
-    if (multisample)
-        glsafe(::glEnable(GL_MULTISAMPLE));
-
-    GLint max_samples;
-    glsafe(::glGetIntegerv(GL_MAX_SAMPLES_EXT, &max_samples));
-    GLsizei num_samples = max_samples / 2;
-
-    GLuint render_fbo;
-    glsafe(::glGenFramebuffersEXT(1, &render_fbo));
-    glsafe(::glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, render_fbo));
-
-    GLuint render_tex = 0;
-    GLuint render_tex_buffer = 0;
-    if (multisample) {
-        // use renderbuffer instead of texture to avoid the need to use glTexImage2DMultisample which is available only since OpenGL 3.2
-        glsafe(::glGenRenderbuffersEXT(1, &render_tex_buffer));
-        glsafe(::glBindRenderbufferEXT(GL_RENDERBUFFER_EXT, render_tex_buffer));
-        glsafe(::glRenderbufferStorageMultisampleEXT(GL_RENDERBUFFER_EXT, num_samples, GL_RGBA8, w, h));
-        glsafe(::glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, GL_RENDERBUFFER_EXT, render_tex_buffer));
-    }
-    else {
-        glsafe(::glGenTextures(1, &render_tex));
-        glsafe(::glBindTexture(GL_TEXTURE_2D, render_tex));
-        glsafe(::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr));
-        glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR));
-        glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR));
-        glsafe(::glFramebufferTexture2D(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, GL_TEXTURE_2D, render_tex, 0));
-    }
-
-    GLuint render_depth;
-    glsafe(::glGenRenderbuffersEXT(1, &render_depth));
-    glsafe(::glBindRenderbufferEXT(GL_RENDERBUFFER_EXT, render_depth));
-    if (multisample)
-        glsafe(::glRenderbufferStorageMultisampleEXT(GL_RENDERBUFFER_EXT, num_samples, GL_DEPTH_COMPONENT24, w, h));
-    else
-        glsafe(::glRenderbufferStorageEXT(GL_RENDERBUFFER_EXT, GL_DEPTH_COMPONENT, w, h));
-
-    glsafe(::glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, GL_RENDERBUFFER_EXT, render_depth));
-
-    GLenum drawBufs[] = { GL_COLOR_ATTACHMENT0 };
-    glsafe(::glDrawBuffers(1, drawBufs));
-
-    if (::glCheckFramebufferStatusEXT(GL_FRAMEBUFFER_EXT) == GL_FRAMEBUFFER_COMPLETE_EXT) {
-        _render_thumbnail_internal(thumbnail_data, thumbnail_params, volumes, camera_type);
-
-        if (multisample) {
-            GLuint resolve_fbo;
-            glsafe(::glGenFramebuffersEXT(1, &resolve_fbo));
-            glsafe(::glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, resolve_fbo));
-
-            GLuint resolve_tex;
-            glsafe(::glGenTextures(1, &resolve_tex));
-            glsafe(::glBindTexture(GL_TEXTURE_2D, resolve_tex));
-            glsafe(::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr));
-            glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR));
-            glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR));
-            glsafe(::glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, GL_TEXTURE_2D, resolve_tex, 0));
-
-            glsafe(::glDrawBuffers(1, drawBufs));
-
-            if (::glCheckFramebufferStatusEXT(GL_FRAMEBUFFER_EXT) == GL_FRAMEBUFFER_COMPLETE_EXT) {
-                glsafe(::glBindFramebufferEXT(GL_READ_FRAMEBUFFER_EXT, render_fbo));
-                glsafe(::glBindFramebufferEXT(GL_DRAW_FRAMEBUFFER_EXT, resolve_fbo));
-                glsafe(::glBlitFramebufferEXT(0, 0, w, h, 0, 0, w, h, GL_COLOR_BUFFER_BIT, GL_LINEAR));
-
-                glsafe(::glBindFramebufferEXT(GL_READ_FRAMEBUFFER_EXT, resolve_fbo));
-                glsafe(::glReadPixels(0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, (void*)thumbnail_data.pixels.data()));
-            }
-
-            glsafe(::glDeleteTextures(1, &resolve_tex));
-            glsafe(::glDeleteFramebuffersEXT(1, &resolve_fbo));
-        }
-        else
-            glsafe(::glReadPixels(0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, (void*)thumbnail_data.pixels.data()));
-
-#if ENABLE_THUMBNAIL_GENERATOR_DEBUG_OUTPUT
-        debug_output_thumbnail(thumbnail_data);
-#endif // ENABLE_THUMBNAIL_GENERATOR_DEBUG_OUTPUT
-    }
-
-    glsafe(::glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0));
-    glsafe(::glDeleteRenderbuffersEXT(1, &render_depth));
-    if (render_tex_buffer != 0)
-        glsafe(::glDeleteRenderbuffersEXT(1, &render_tex_buffer));
-    if (render_tex != 0)
-        glsafe(::glDeleteTextures(1, &render_tex));
-    glsafe(::glDeleteFramebuffersEXT(1, &render_fbo));
-
-    if (multisample)
-        glsafe(::glDisable(GL_MULTISAMPLE));
-}
-
-void GLCanvas3D::_render_thumbnail_legacy(ThumbnailData& thumbnail_data, unsigned int w, unsigned int h, const ThumbnailsParams& thumbnail_params, const GLVolumeCollection& volumes, Camera::EType camera_type)
-{
-    // check that thumbnail size does not exceed the default framebuffer size
-    const Size& cnv_size = get_canvas_size();
-    unsigned int cnv_w = (unsigned int)cnv_size.get_width();
-    unsigned int cnv_h = (unsigned int)cnv_size.get_height();
-    if (w > cnv_w || h > cnv_h) {
-        float ratio = std::min((float)cnv_w / (float)w, (float)cnv_h / (float)h);
-        w = (unsigned int)(ratio * (float)w);
-        h = (unsigned int)(ratio * (float)h);
-    }
-
-    thumbnail_data.set(w, h);
-    if (!thumbnail_data.is_valid())
-        return;
-
-    _render_thumbnail_internal(thumbnail_data, thumbnail_params, volumes, camera_type);
-
-    glsafe(::glReadPixels(0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, (void*)thumbnail_data.pixels.data()));
-#if ENABLE_THUMBNAIL_GENERATOR_DEBUG_OUTPUT
-    debug_output_thumbnail(thumbnail_data);
-#endif // ENABLE_THUMBNAIL_GENERATOR_DEBUG_OUTPUT
-
-    // restore the default framebuffer size to avoid flickering on the 3D scene
-    wxGetApp().plater()->get_camera().apply_viewport(0, 0, cnv_size.get_width(), cnv_size.get_height());
-}
-
-bool GLCanvas3D::_init_toolbars()
-{
-    if (!_init_main_toolbar())
-        return false;
-
-    if (!_init_undoredo_toolbar())
-        return false;
-
-    if (!_init_view_toolbar())
-        return false;
-
-    if (!_init_collapse_toolbar())
-        return false;
-
-    return true;
-}
-
-bool GLCanvas3D::_init_main_toolbar()
-{
-    if (!m_main_toolbar.is_enabled())
-        return true;
-
-    BackgroundTexture::Metadata background_data;
-    background_data.filename = "toolbar_background.png";
-    background_data.left = 16;
-    background_data.top = 16;
-    background_data.right = 16;
-    background_data.bottom = 16;
-
-    if (!m_main_toolbar.init(background_data)) {
-        // unable to init the toolbar texture, disable it
-        m_main_toolbar.set_enabled(false);
-        return true;
-    }
-    // init arrow
-#if ENABLE_GL_SHADERS_ATTRIBUTES
-    if (!m_main_toolbar.init_arrow("toolbar_arrow_2.svg"))
-#else
-    BackgroundTexture::Metadata arrow_data;
-    arrow_data.filename = "toolbar_arrow.svg";
-    arrow_data.left = 0;
-    arrow_data.top = 0;
-    arrow_data.right = 0;
-    arrow_data.bottom = 0;
-    if (!m_main_toolbar.init_arrow(arrow_data))
-#endif // ENABLE_GL_SHADERS_ATTRIBUTES
-        BOOST_LOG_TRIVIAL(error) << "Main toolbar failed to load arrow texture.";
-
-    // m_gizmos is created at constructor, thus we can init arrow here.
-#if ENABLE_GL_SHADERS_ATTRIBUTES
-    if (!m_gizmos.init_arrow("toolbar_arrow_2.svg"))
-#else
-    if (!m_gizmos.init_arrow(arrow_data))
-#endif // ENABLE_GL_SHADERS_ATTRIBUTES
-        BOOST_LOG_TRIVIAL(error) << "Gizmos manager failed to load arrow texture.";
-
-//    m_main_toolbar.set_layout_type(GLToolbar::Layout::Vertical);
-    m_main_toolbar.set_layout_type(GLToolbar::Layout::Horizontal);
-    m_main_toolbar.set_horizontal_orientation(GLToolbar::Layout::HO_Right);
-    m_main_toolbar.set_vertical_orientation(GLToolbar::Layout::VO_Top);
-    m_main_toolbar.set_border(5.0f);
-    m_main_toolbar.set_separator_size(5);
-    m_main_toolbar.set_gap_size(4);
-
-    GLToolbarItem::Data item;
-
-    item.name = "add";
-    item.icon_filename = "add.svg";
-    item.tooltip = _utf8(L("Add...")) + " [" + GUI::shortkey_ctrl_prefix() + "I]";
-    item.sprite_id = 0;
-    item.left.action_callback = [this]() { if (m_canvas != nullptr) wxPostEvent(m_canvas, SimpleEvent(EVT_GLTOOLBAR_ADD)); };
-    if (!m_main_toolbar.add_item(item))
-        return false;
-
-    item.name = "delete";
-    item.icon_filename = "remove.svg";
-    item.tooltip = _utf8(L("Delete")) + " [Del]";
-    item.sprite_id = 1;
-    item.left.action_callback = [this]() { if (m_canvas != nullptr) wxPostEvent(m_canvas, SimpleEvent(EVT_GLTOOLBAR_DELETE)); };
-    item.enabling_callback = []()->bool { return wxGetApp().plater()->can_delete(); };
-    if (!m_main_toolbar.add_item(item))
-        return false;
-
-    item.name = "deleteall";
-    item.icon_filename = "delete_all.svg";
-    item.tooltip = _utf8(L("Delete all")) + " [" + GUI::shortkey_ctrl_prefix() + "Del]";
-    item.sprite_id = 2;
-    item.left.action_callback = [this]() { if (m_canvas != nullptr) wxPostEvent(m_canvas, SimpleEvent(EVT_GLTOOLBAR_DELETE_ALL)); };
-    item.enabling_callback = []()->bool { return wxGetApp().plater()->can_delete_all(); };
-    if (!m_main_toolbar.add_item(item))
-        return false;
-
-    item.name = "arrange";
-    item.icon_filename = "arrange.svg";
-    item.tooltip = _utf8(L("Arrange")) + " [A]\n" + _utf8(L("Arrange selection")) + " [Shift+A]\n" + _utf8(L("Click right mouse button to show arrangement options"));
-    item.sprite_id = 3;
-    item.left.action_callback = [this]() { if (m_canvas != nullptr) wxPostEvent(m_canvas, SimpleEvent(EVT_GLTOOLBAR_ARRANGE)); };
-    item.enabling_callback = []()->bool { return wxGetApp().plater()->can_arrange(); };
-    item.right.toggable = true;
-    item.right.render_callback = [this](float left, float right, float, float) {
-        if (m_canvas != nullptr)
-            _render_arrange_menu(0.5f * (left + right));
-    };
-    if (!m_main_toolbar.add_item(item))
-        return false;
-
-    item.right.toggable = false;
-    item.right.render_callback = GLToolbarItem::Default_Render_Callback;
-
-    if (!m_main_toolbar.add_separator())
-        return false;
-
-    item.name = "copy";
-    item.icon_filename = "copy.svg";
-    item.tooltip = _utf8(L("Copy")) + " [" + GUI::shortkey_ctrl_prefix() + "C]";
-    item.sprite_id = 4;
-    item.left.action_callback = [this]() { if (m_canvas != nullptr) wxPostEvent(m_canvas, SimpleEvent(EVT_GLTOOLBAR_COPY)); };
-    item.enabling_callback = []()->bool { return wxGetApp().plater()->can_copy_to_clipboard(); };
-    if (!m_main_toolbar.add_item(item))
-        return false;
-
-    item.name = "paste";
-    item.icon_filename = "paste.svg";
-    item.tooltip = _utf8(L("Paste")) + " [" + GUI::shortkey_ctrl_prefix() + "V]";
-    item.sprite_id = 5;
-    item.left.action_callback = [this]() { if (m_canvas != nullptr) wxPostEvent(m_canvas, SimpleEvent(EVT_GLTOOLBAR_PASTE)); };
-    item.enabling_callback = []()->bool { return wxGetApp().plater()->can_paste_from_clipboard(); };
-    if (!m_main_toolbar.add_item(item))
-        return false;
-
-    if (!m_main_toolbar.add_separator())
-        return false;
-
-    item.name = "more";
-    item.icon_filename = "instance_add.svg";
-    item.tooltip = _utf8(L("Add instance")) + " [+]";
-    item.sprite_id = 6;
-    item.left.action_callback = [this]() { if (m_canvas != nullptr) wxPostEvent(m_canvas, SimpleEvent(EVT_GLTOOLBAR_MORE)); };
-    item.visibility_callback = []()->bool { return wxGetApp().get_mode() != comSimple; };
-    item.enabling_callback = []()->bool { return wxGetApp().plater()->can_increase_instances(); };
-
-    if (!m_main_toolbar.add_item(item))
-        return false;
-
-    item.name = "fewer";
-    item.icon_filename = "instance_remove.svg";
-    item.tooltip = _utf8(L("Remove instance")) + " [-]";
-    item.sprite_id = 7;
-    item.left.action_callback = [this]() { if (m_canvas != nullptr) wxPostEvent(m_canvas, SimpleEvent(EVT_GLTOOLBAR_FEWER)); };
-    item.visibility_callback = []()->bool { return wxGetApp().get_mode() != comSimple; };
-    item.enabling_callback = []()->bool { return wxGetApp().plater()->can_decrease_instances(); };
-    if (!m_main_toolbar.add_item(item))
-        return false;
-
-    if (!m_main_toolbar.add_separator())
-        return false;
-
-    item.name = "splitobjects";
-    item.icon_filename = "split_objects.svg";
-    item.tooltip = _utf8(L("Split to objects"));
-    item.sprite_id = 8;
-    item.left.action_callback = [this]() { if (m_canvas != nullptr) wxPostEvent(m_canvas, SimpleEvent(EVT_GLTOOLBAR_SPLIT_OBJECTS)); };
-    item.visibility_callback = GLToolbarItem::Default_Visibility_Callback;
-    item.enabling_callback = []()->bool { return wxGetApp().plater()->can_split_to_objects(); };
-    if (!m_main_toolbar.add_item(item))
-        return false;
-
-    item.name = "splitvolumes";
-    item.icon_filename = "split_parts.svg";
-    item.tooltip = _utf8(L("Split to parts"));
-    item.sprite_id = 9;
-    item.left.action_callback = [this]() { if (m_canvas != nullptr) wxPostEvent(m_canvas, SimpleEvent(EVT_GLTOOLBAR_SPLIT_VOLUMES)); };
-    item.visibility_callback = []()->bool { return wxGetApp().get_mode() != comSimple; };
-    item.enabling_callback = []()->bool { return wxGetApp().plater()->can_split_to_volumes(); };
-    if (!m_main_toolbar.add_item(item))
-        return false;
-
-    if (!m_main_toolbar.add_separator())
-        return false;
-
-    item.name = "settings";
-    item.icon_filename = "settings.svg";
-    item.tooltip = _u8L("Switch to Settings") + "\n" + "[" + GUI::shortkey_ctrl_prefix() + "2] - " + _u8L("Print Settings Tab")    + 
-                                                "\n" + "[" + GUI::shortkey_ctrl_prefix() + "3] - " + (current_printer_technology() == ptFFF ? _u8L("Filament Settings Tab") : _u8L("Material Settings Tab")) +
-                                                "\n" + "[" + GUI::shortkey_ctrl_prefix() + "4] - " + _u8L("Printer Settings Tab") ;
-    item.sprite_id = 10;
-    item.enabling_callback    = GLToolbarItem::Default_Enabling_Callback;
-    item.visibility_callback  = []() { return (wxGetApp().app_config->get("new_settings_layout_mode") == "1" ||
-                                               wxGetApp().app_config->get("dlg_settings_layout_mode") == "1"); };
-    item.left.action_callback = []() { wxGetApp().mainframe->select_tab(); };
-    if (!m_main_toolbar.add_item(item))
-        return false;
-
-    /*
-    if (!m_main_toolbar.add_separator())
-        return false;
-        */
-
-    item.name = "search";
-    item.icon_filename = "search_.svg";
-    item.tooltip = _utf8(L("Search")) + " [" + GUI::shortkey_ctrl_prefix() + "F]";
-    item.sprite_id = 11;
-    item.left.toggable = true;
-    item.left.render_callback = [this](float left, float right, float, float) {
-        if (m_canvas != nullptr) {
-            if (_render_search_list(0.5f * (left + right)))
-                _deactivate_search_toolbar_item();
-        }
-    };
-    item.left.action_callback   = GLToolbarItem::Default_Action_Callback;
-    item.visibility_callback    = GLToolbarItem::Default_Visibility_Callback;
-    item.enabling_callback      = GLToolbarItem::Default_Enabling_Callback;
-    if (!m_main_toolbar.add_item(item))
-        return false;
-
-    if (!m_main_toolbar.add_separator())
-        return false;
-
-    item.name = "layersediting";
-    item.icon_filename = "layers_white.svg";
-    item.tooltip = _utf8(L("Variable layer height"));
-    item.sprite_id = 12;
-    item.left.action_callback = [this]() { if (m_canvas != nullptr) wxPostEvent(m_canvas, SimpleEvent(EVT_GLTOOLBAR_LAYERSEDITING)); };
-    item.visibility_callback = [this]()->bool {
-        bool res = current_printer_technology() == ptFFF;
-        // turns off if changing printer technology
-        if (!res && m_main_toolbar.is_item_visible("layersediting") && m_main_toolbar.is_item_pressed("layersediting"))
-            force_main_toolbar_left_action(get_main_toolbar_item_id("layersediting"));
-
-        return res;
-    };
-    item.enabling_callback      = []()->bool { return wxGetApp().plater()->can_layers_editing(); };
-    item.left.render_callback   = GLToolbarItem::Default_Render_Callback;
-    if (!m_main_toolbar.add_item(item))
-        return false;
-
-    return true;
-}
-
-bool GLCanvas3D::_init_undoredo_toolbar()
-{
-    if (!m_undoredo_toolbar.is_enabled())
-        return true;
-
-    BackgroundTexture::Metadata background_data;
-    background_data.filename = "toolbar_background.png";
-    background_data.left = 16;
-    background_data.top = 16;
-    background_data.right = 16;
-    background_data.bottom = 16;
-
-    if (!m_undoredo_toolbar.init(background_data)) {
-        // unable to init the toolbar texture, disable it
-        m_undoredo_toolbar.set_enabled(false);
-        return true;
-    }
-
-    // init arrow
-#if ENABLE_GL_SHADERS_ATTRIBUTES
-    if (!m_undoredo_toolbar.init_arrow("toolbar_arrow_2.svg"))
-#else
-    BackgroundTexture::Metadata arrow_data;
-    arrow_data.filename = "toolbar_arrow.svg";
-    arrow_data.left = 0;
-    arrow_data.top = 0;
-    arrow_data.right = 0;
-    arrow_data.bottom = 0;
-    if (!m_undoredo_toolbar.init_arrow(arrow_data))
-#endif // ENABLE_GL_SHADERS_ATTRIBUTES
-        BOOST_LOG_TRIVIAL(error) << "Undo/Redo toolbar failed to load arrow texture.";
-
-//    m_undoredo_toolbar.set_layout_type(GLToolbar::Layout::Vertical);
-    m_undoredo_toolbar.set_layout_type(GLToolbar::Layout::Horizontal);
-    m_undoredo_toolbar.set_horizontal_orientation(GLToolbar::Layout::HO_Left);
-    m_undoredo_toolbar.set_vertical_orientation(GLToolbar::Layout::VO_Top);
-    m_undoredo_toolbar.set_border(5.0f);
-    m_undoredo_toolbar.set_separator_size(5);
-    m_undoredo_toolbar.set_gap_size(4);
-
-    GLToolbarItem::Data item;
-
-    item.name = "undo";
-    item.icon_filename = "undo_toolbar.svg";
-    item.tooltip = _utf8(L("Undo")) + " [" + GUI::shortkey_ctrl_prefix() + "Z]\n" + _utf8(L("Click right mouse button to open/close History"));
-    item.sprite_id = 0;
-    item.left.action_callback = [this]() { post_event(SimpleEvent(EVT_GLCANVAS_UNDO)); };
-    item.right.toggable = true;
-    item.right.action_callback = [this]() { m_imgui_undo_redo_hovered_pos = -1; };
-    item.right.render_callback = [this](float left, float right, float, float) {
-        if (m_canvas != nullptr) {
-            if (_render_undo_redo_stack(true, 0.5f * (left + right)))
-                _deactivate_undo_redo_toolbar_items();
-        }
-    };
-    item.enabling_callback = [this]()->bool {
-        bool can_undo = wxGetApp().plater()->can_undo();
-        int id = m_undoredo_toolbar.get_item_id("undo");
-
-        std::string curr_additional_tooltip;
-        m_undoredo_toolbar.get_additional_tooltip(id, curr_additional_tooltip);
-
-        std::string new_additional_tooltip;
-        if (can_undo) {
-        	std::string action;
-            wxGetApp().plater()->undo_redo_topmost_string_getter(true, action);
-            new_additional_tooltip = (boost::format(_utf8(L("Next Undo action: %1%"))) % action).str();
-        }
-
-        if (new_additional_tooltip != curr_additional_tooltip) {
-            m_undoredo_toolbar.set_additional_tooltip(id, new_additional_tooltip);
-            set_tooltip("");
-        }
-        return can_undo;
-    };
-
-    if (!m_undoredo_toolbar.add_item(item))
-        return false;
-
-    item.name = "redo";
-    item.icon_filename = "redo_toolbar.svg";
-    item.tooltip = _utf8(L("Redo")) + " [" + GUI::shortkey_ctrl_prefix() + "Y]\n" + _utf8(L("Click right mouse button to open/close History"));
-    item.sprite_id = 1;
-    item.left.action_callback = [this]() { post_event(SimpleEvent(EVT_GLCANVAS_REDO)); };
-    item.right.action_callback = [this]() { m_imgui_undo_redo_hovered_pos = -1; };
-    item.right.render_callback = [this](float left, float right, float, float) {
-        if (m_canvas != nullptr) {
-            if (_render_undo_redo_stack(false, 0.5f * (left + right)))
-                _deactivate_undo_redo_toolbar_items();
-        }
-    };
-    item.enabling_callback = [this]()->bool {
-        bool can_redo = wxGetApp().plater()->can_redo();
-        int id = m_undoredo_toolbar.get_item_id("redo");
-
-        std::string curr_additional_tooltip;
-        m_undoredo_toolbar.get_additional_tooltip(id, curr_additional_tooltip);
-
-        std::string new_additional_tooltip;
-        if (can_redo) {
-        	std::string action;
-            wxGetApp().plater()->undo_redo_topmost_string_getter(false, action);
-            new_additional_tooltip = (boost::format(_utf8(L("Next Redo action: %1%"))) % action).str();
-        }
-
-        if (new_additional_tooltip != curr_additional_tooltip) {
-            m_undoredo_toolbar.set_additional_tooltip(id, new_additional_tooltip);
-            set_tooltip("");
-        }
-        return can_redo;
-    };
-
-    if (!m_undoredo_toolbar.add_item(item))
-        return false;
-    /*
-    if (!m_undoredo_toolbar.add_separator())
-        return false;
-        */
-    return true;
-}
-
-bool GLCanvas3D::_init_view_toolbar()
-{
-    return wxGetApp().plater()->init_view_toolbar();
-}
-
-bool GLCanvas3D::_init_collapse_toolbar()
-{
-    return wxGetApp().plater()->init_collapse_toolbar();
-}
-
-bool GLCanvas3D::_set_current()
-{
-    return m_context != nullptr && m_canvas->SetCurrent(*m_context);
-}
-
-void GLCanvas3D::_resize(unsigned int w, unsigned int h)
-{
-    if (m_canvas == nullptr && m_context == nullptr)
-        return;
-
-    const std::array<unsigned int, 2> new_size = { w, h };
-    if (m_old_size == new_size)
-        return;
-
-    m_old_size = new_size;
-
-    auto *imgui = wxGetApp().imgui();
-    imgui->set_display_size(static_cast<float>(w), static_cast<float>(h));
-    const float font_size = 1.5f * wxGetApp().em_unit();
-#if ENABLE_RETINA_GL
-    imgui->set_scaling(font_size, 1.0f, m_retina_helper->get_scale_factor());
-#else
-    imgui->set_scaling(font_size, m_canvas->GetContentScaleFactor(), 1.0f);
-#endif
-
-    this->request_extra_frame();
-
-    // ensures that this canvas is current
-    _set_current();
-}
-
-BoundingBoxf3 GLCanvas3D::_max_bounding_box(bool include_gizmos, bool include_bed_model) const
-{
-    BoundingBoxf3 bb = volumes_bounding_box();
-
-    // The following is a workaround for gizmos not being taken in account when calculating the tight camera frustrum
-    // A better solution would ask the gizmo manager for the bounding box of the current active gizmo, if any
-    if (include_gizmos && m_gizmos.is_running()) {
-        const BoundingBoxf3 sel_bb = m_selection.get_bounding_box();
-        const Vec3d sel_bb_center = sel_bb.center();
-        const Vec3d extend_by = sel_bb.max_size() * Vec3d::Ones();
-        bb.merge(BoundingBoxf3(sel_bb_center - extend_by, sel_bb_center + extend_by));
-    }
-
-    const BoundingBoxf3 bed_bb = include_bed_model ? m_bed.extended_bounding_box() : m_bed.build_volume().bounding_volume();
-    bb.merge(bed_bb);
-
-    if (!m_main_toolbar.is_enabled())
-        bb.merge(m_gcode_viewer.get_max_bounding_box());
-
-    // clamp max bb size with respect to bed bb size
-    if (!m_picking_enabled) {
-        static const double max_scale_factor = 2.0;
-        const Vec3d bb_size = bb.size();
-        const Vec3d bed_bb_size = m_bed.build_volume().bounding_volume().size();
-        if (bb_size.x() > max_scale_factor * bed_bb_size.x() ||
-            bb_size.y() > max_scale_factor * bed_bb_size.y() ||
-            bb_size.z() > max_scale_factor * bed_bb_size.z()) {
-            const Vec3d bed_bb_center = bed_bb.center();
-            const Vec3d extend_by = max_scale_factor * bed_bb_size;
-            bb = BoundingBoxf3(bed_bb_center - extend_by, bed_bb_center + extend_by);
-        }
-    }
-
-    return bb;
-}
-
-void GLCanvas3D::_zoom_to_box(const BoundingBoxf3& box, double margin_factor)
-{
-    wxGetApp().plater()->get_camera().zoom_to_box(box, margin_factor);
-    m_dirty = true;
-}
-
-void GLCanvas3D::_update_camera_zoom(double zoom)
-{
-    wxGetApp().plater()->get_camera().update_zoom(zoom);
-    m_dirty = true;
-}
-
-void GLCanvas3D::_refresh_if_shown_on_screen()
-{
-    if (_is_shown_on_screen()) {
-        const Size& cnv_size = get_canvas_size();
-        _resize((unsigned int)cnv_size.get_width(), (unsigned int)cnv_size.get_height());
-
-        // Because of performance problems on macOS, where PaintEvents are not delivered
-        // frequently enough, we call render() here directly when we can.
-        render();
-    }
-}
-
-void GLCanvas3D::_picking_pass()
-{
-    if (m_picking_enabled && !m_mouse.dragging && m_mouse.position != Vec2d(DBL_MAX, DBL_MAX)) {
-        m_hover_volume_idxs.clear();
-
-        // Render the object for picking.
-        // FIXME This cannot possibly work in a multi - sampled context as the color gets mangled by the anti - aliasing.
-        // Better to use software ray - casting on a bounding - box hierarchy.
-
-        if (m_multisample_allowed)
-        	// This flag is often ignored by NVIDIA drivers if rendering into a screen buffer.
-            glsafe(::glDisable(GL_MULTISAMPLE));
-
-        glsafe(::glDisable(GL_BLEND));
-        glsafe(::glEnable(GL_DEPTH_TEST));
-
-        glsafe(::glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT));
-
-#if !ENABLE_GL_SHADERS_ATTRIBUTES
-        m_camera_clipping_plane = m_gizmos.get_clipping_plane();
-        if (m_camera_clipping_plane.is_active()) {
-            ::glClipPlane(GL_CLIP_PLANE0, (GLdouble*)m_camera_clipping_plane.get_data().data());
-            ::glEnable(GL_CLIP_PLANE0);
-        }
-#endif // !ENABLE_GL_SHADERS_ATTRIBUTES
-        _render_volumes_for_picking();
-#if !ENABLE_GL_SHADERS_ATTRIBUTES
-        if (m_camera_clipping_plane.is_active())
-            ::glDisable(GL_CLIP_PLANE0);
-#endif // !ENABLE_GL_SHADERS_ATTRIBUTES
-
-#if ENABLE_GL_SHADERS_ATTRIBUTES
-        const Camera& camera = wxGetApp().plater()->get_camera();
-        _render_bed_for_picking(camera.get_view_matrix(), camera.get_projection_matrix(), !camera.is_looking_downward());
-#else
-        _render_bed_for_picking(!wxGetApp().plater()->get_camera().is_looking_downward());
-#endif // ENABLE_GL_SHADERS_ATTRIBUTES
-
-        m_gizmos.render_current_gizmo_for_picking_pass();
-
-        if (m_multisample_allowed)
-            glsafe(::glEnable(GL_MULTISAMPLE));
-
-        int volume_id = -1;
-        int gizmo_id = -1;
-
-        std::array<GLubyte, 4> color = { 0, 0, 0, 0 };
-        const Size& cnv_size = get_canvas_size();
-        bool inside = 0 <= m_mouse.position(0) && m_mouse.position(0) < cnv_size.get_width() && 0 <= m_mouse.position(1) && m_mouse.position(1) < cnv_size.get_height();
-        if (inside) {
-            glsafe(::glReadPixels(m_mouse.position(0), cnv_size.get_height() - m_mouse.position.y() - 1, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, (void*)color.data()));
-            if (picking_checksum_alpha_channel(color[0], color[1], color[2]) == color[3]) {
-                // Only non-interpolated colors are valid, those have their lowest three bits zeroed.
-                // we reserve color = (0,0,0) for occluders (as the printbed) 
-                // volumes' id are shifted by 1
-                // see: _render_volumes_for_picking()
-                unsigned int id = picking_encode(color[0], color[1], color[2]);
-                volume_id = id - 1;
-                // gizmos' id are instead properly encoded by the color
-                gizmo_id = id;
-            }
-        }
-        if (0 <= volume_id && volume_id < (int)m_volumes.volumes.size()) {
-            // do not add the volume id if any gizmo is active and CTRL is pressed
-            if (m_gizmos.get_current_type() == GLGizmosManager::EType::Undefined || !wxGetKeyState(WXK_CONTROL))
-                m_hover_volume_idxs.emplace_back(volume_id);
-            m_gizmos.set_hover_id(-1);
-        }
-        else
-            m_gizmos.set_hover_id(inside && (unsigned int)gizmo_id <= GLGizmoBase::BASE_ID ? ((int)GLGizmoBase::BASE_ID - gizmo_id) : -1);
-
-        _update_volumes_hover_state();
-    }
-}
-
-void GLCanvas3D::_rectangular_selection_picking_pass()
-{
-    m_gizmos.set_hover_id(-1);
-
-    std::set<int> idxs;
-
-    if (m_picking_enabled) {
-        if (m_multisample_allowed)
-        	// This flag is often ignored by NVIDIA drivers if rendering into a screen buffer.
-            glsafe(::glDisable(GL_MULTISAMPLE));
-
-        glsafe(::glDisable(GL_BLEND));
-        glsafe(::glEnable(GL_DEPTH_TEST));
-
-        glsafe(::glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT));
-
-        _render_volumes_for_picking();
-#if ENABLE_GL_SHADERS_ATTRIBUTES
-        const Camera& camera = wxGetApp().plater()->get_camera();
-        _render_bed_for_picking(camera.get_view_matrix(), camera.get_projection_matrix(), !camera.is_looking_downward());
-#else
-        _render_bed_for_picking(!wxGetApp().plater()->get_camera().is_looking_downward());
-#endif // ENABLE_GL_SHADERS_ATTRIBUTES
-
-        if (m_multisample_allowed)
-            glsafe(::glEnable(GL_MULTISAMPLE));
-
-        int width = std::max((int)m_rectangle_selection.get_width(), 1);
-        int height = std::max((int)m_rectangle_selection.get_height(), 1);
-        int px_count = width * height;
-
-        int left = (int)m_rectangle_selection.get_left();
-        int top = get_canvas_size().get_height() - (int)m_rectangle_selection.get_top();
-        if (left >= 0 && top >= 0) {
-#define USE_PARALLEL 1
-#if USE_PARALLEL
-            struct Pixel
-            {
-                std::array<GLubyte, 4> data;
-            	// Only non-interpolated colors are valid, those have their lowest three bits zeroed.
-                bool valid() const { return picking_checksum_alpha_channel(data[0], data[1], data[2]) == data[3]; }
-                // we reserve color = (0,0,0) for occluders (as the printbed) 
-                // volumes' id are shifted by 1
-                // see: _render_volumes_for_picking()
-                int id() const { return data[0] + (data[1] << 8) + (data[2] << 16) - 1; }
-            };
-
-            std::vector<Pixel> frame(px_count);
-            glsafe(::glReadPixels(left, top, width, height, GL_RGBA, GL_UNSIGNED_BYTE, (void*)frame.data()));
-
-            tbb::spin_mutex mutex;
-            tbb::parallel_for(tbb::blocked_range<size_t>(0, frame.size(), (size_t)width),
-                [this, &frame, &idxs, &mutex](const tbb::blocked_range<size_t>& range) {
-                for (size_t i = range.begin(); i < range.end(); ++i)
-                	if (frame[i].valid()) {
-                    	int volume_id = frame[i].id();
-                    	if (0 <= volume_id && volume_id < (int)m_volumes.volumes.size()) {
-                        	mutex.lock();
-                        	idxs.insert(volume_id);
-                        	mutex.unlock();
-                    	}
-                	}
-            });
-#else
-            std::vector<GLubyte> frame(4 * px_count);
-            glsafe(::glReadPixels(left, top, width, height, GL_RGBA, GL_UNSIGNED_BYTE, (void*)frame.data()));
-
-            for (int i = 0; i < px_count; ++i)
-            {
-                int px_id = 4 * i;
-                int volume_id = frame[px_id] + (frame[px_id + 1] << 8) + (frame[px_id + 2] << 16);
-                if (0 <= volume_id && volume_id < (int)m_volumes.volumes.size())
-                    idxs.insert(volume_id);
-            }
-#endif // USE_PARALLEL
-        }
-    }
-
-    m_hover_volume_idxs.assign(idxs.begin(), idxs.end());
-    _update_volumes_hover_state();
-}
-
-void GLCanvas3D::_render_background()
-{
-    bool use_error_color = false;
-    if (wxGetApp().is_editor()) {
-        use_error_color = m_dynamic_background_enabled &&
-        (current_printer_technology() != ptSLA || !m_volumes.empty());
-
-        if (!m_volumes.empty())
-            use_error_color &= _is_any_volume_outside();
-        else
-            use_error_color &= m_gcode_viewer.has_data() && !m_gcode_viewer.is_contained_in_bed();
-    }
-
-#if !ENABLE_GL_SHADERS_ATTRIBUTES
-    glsafe(::glPushMatrix());
-    glsafe(::glLoadIdentity());
-    glsafe(::glMatrixMode(GL_PROJECTION));
-    glsafe(::glPushMatrix());
-    glsafe(::glLoadIdentity());
-#endif // !ENABLE_GL_SHADERS_ATTRIBUTES
-
-    // Draws a bottom to top gradient over the complete screen.
-    glsafe(::glDisable(GL_DEPTH_TEST));
-
-#if ENABLE_LEGACY_OPENGL_REMOVAL
-    const ColorRGBA bottom_color = use_error_color ? ERROR_BG_DARK_COLOR : DEFAULT_BG_DARK_COLOR;
-
-    if (!m_background.is_initialized()) {
-        m_background.reset();
-
-        GLModel::Geometry init_data;
-        init_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P2T2 };
-        init_data.reserve_vertices(4);
-        init_data.reserve_indices(6);
-
-        // vertices
-        init_data.add_vertex(Vec2f(-1.0f, -1.0f), Vec2f(0.0f, 0.0f));
-        init_data.add_vertex(Vec2f(1.0f, -1.0f),  Vec2f(1.0f, 0.0f));
-        init_data.add_vertex(Vec2f(1.0f, 1.0f),   Vec2f(1.0f, 1.0f));
-        init_data.add_vertex(Vec2f(-1.0f, 1.0f),  Vec2f(0.0f, 1.0f));
-
-        // indices
-        init_data.add_triangle(0, 1, 2);
-        init_data.add_triangle(2, 3, 0);
-
-        m_background.init_from(std::move(init_data));
-    }
-
-    GLShaderProgram* shader = wxGetApp().get_shader("background");
-    if (shader != nullptr) {
-        shader->start_using();
-        shader->set_uniform("top_color", use_error_color ? ERROR_BG_LIGHT_COLOR : DEFAULT_BG_LIGHT_COLOR);
-        shader->set_uniform("bottom_color", bottom_color);
-        m_background.render();
-        shader->stop_using();
-    }
-#else
-    ::glBegin(GL_QUADS);
-    ::glColor3fv(use_error_color ? ERROR_BG_DARK_COLOR.data(): DEFAULT_BG_DARK_COLOR.data());
-    ::glVertex2f(-1.0f, -1.0f);
-    ::glVertex2f(1.0f, -1.0f);
-
-    ::glColor3fv(use_error_color ? ERROR_BG_LIGHT_COLOR.data() : DEFAULT_BG_LIGHT_COLOR.data());
-    ::glVertex2f(1.0f, 1.0f);
-    ::glVertex2f(-1.0f, 1.0f);
-    glsafe(::glEnd());
-#endif // ENABLE_LEGACY_OPENGL_REMOVAL
-
-    glsafe(::glEnable(GL_DEPTH_TEST));
-
-#if !ENABLE_GL_SHADERS_ATTRIBUTES
-    glsafe(::glPopMatrix());
-    glsafe(::glMatrixMode(GL_MODELVIEW));
-    glsafe(::glPopMatrix());
-#endif // !ENABLE_GL_SHADERS_ATTRIBUTES
-}
-
-#if ENABLE_GL_SHADERS_ATTRIBUTES
-void GLCanvas3D::_render_bed(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool show_axes)
-#else
-void GLCanvas3D::_render_bed(bool bottom, bool show_axes)
-#endif // ENABLE_GL_SHADERS_ATTRIBUTES
-{
-    float scale_factor = 1.0;
-#if ENABLE_RETINA_GL
-    scale_factor = m_retina_helper->get_scale_factor();
-#endif // ENABLE_RETINA_GL
-
-    bool show_texture = ! bottom ||
-            (m_gizmos.get_current_type() != GLGizmosManager::FdmSupports
-          && m_gizmos.get_current_type() != GLGizmosManager::SlaSupports
-          && m_gizmos.get_current_type() != GLGizmosManager::Hollow
-          && m_gizmos.get_current_type() != GLGizmosManager::Seam
-          && m_gizmos.get_current_type() != GLGizmosManager::MmuSegmentation);
-
-#if ENABLE_GL_SHADERS_ATTRIBUTES
-    m_bed.render(*this, view_matrix, projection_matrix, bottom, scale_factor, show_axes, show_texture);
-#else
-    m_bed.render(*this, bottom, scale_factor, show_axes, show_texture);
-#endif // ENABLE_GL_SHADERS_ATTRIBUTES
-}
-
-#if ENABLE_GL_SHADERS_ATTRIBUTES
-void GLCanvas3D::_render_bed_for_picking(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom)
-#else
-void GLCanvas3D::_render_bed_for_picking(bool bottom)
-#endif // ENABLE_GL_SHADERS_ATTRIBUTES
-{
-    float scale_factor = 1.0;
-#if ENABLE_RETINA_GL
-    scale_factor = m_retina_helper->get_scale_factor();
-#endif // ENABLE_RETINA_GL
-
-#if ENABLE_GL_SHADERS_ATTRIBUTES
-    m_bed.render_for_picking(*this, view_matrix, projection_matrix, bottom, scale_factor);
-#else
-    m_bed.render_for_picking(*this, bottom, scale_factor);
-#endif // ENABLE_GL_SHADERS_ATTRIBUTES
-}
-
-void GLCanvas3D::_render_objects(GLVolumeCollection::ERenderType type)
-{
-    if (m_volumes.empty())
-        return;
-
-    glsafe(::glEnable(GL_DEPTH_TEST));
-
-    m_camera_clipping_plane = m_gizmos.get_clipping_plane();
-
-    if (m_picking_enabled)
-        // Update the layer editing selection to the first object selected, update the current object maximum Z.
-        m_layers_editing.select_object(*m_model, this->is_layers_editing_enabled() ? m_selection.get_object_idx() : -1);
-
-    if (const BuildVolume &build_volume = m_bed.build_volume(); build_volume.valid()) {
-        switch (build_volume.type()) {
-        case BuildVolume::Type::Rectangle: {
-            const BoundingBox3Base<Vec3d> bed_bb = build_volume.bounding_volume().inflated(BuildVolume::SceneEpsilon);
-            m_volumes.set_print_volume({ 0, // circle
-                { float(bed_bb.min.x()), float(bed_bb.min.y()), float(bed_bb.max.x()), float(bed_bb.max.y()) },
-                { 0.0f, float(build_volume.max_print_height()) } });
-            break;
-        }
-        case BuildVolume::Type::Circle: {
-            m_volumes.set_print_volume({ 1, // rectangle
-                { unscaled<float>(build_volume.circle().center.x()), unscaled<float>(build_volume.circle().center.y()), unscaled<float>(build_volume.circle().radius + BuildVolume::SceneEpsilon), 0.0f },
-                { 0.0f, float(build_volume.max_print_height() + BuildVolume::SceneEpsilon) } });
-            break;
-        }
-        default:
-        case BuildVolume::Type::Convex:
-        case BuildVolume::Type::Custom: {
-            m_volumes.set_print_volume({ static_cast<int>(type),
-                { -FLT_MAX, -FLT_MAX, FLT_MAX, FLT_MAX },
-                { -FLT_MAX, FLT_MAX } }
-            );
-        }
-        }
-        if (m_requires_check_outside_state) {
-            m_volumes.check_outside_state(build_volume, nullptr);
-            m_requires_check_outside_state = false;
-        }
-    }
-
-    if (m_use_clipping_planes)
-        m_volumes.set_z_range(-m_clipping_planes[0].get_data()[3], m_clipping_planes[1].get_data()[3]);
-    else
-        m_volumes.set_z_range(-FLT_MAX, FLT_MAX);
-
-    m_volumes.set_clipping_plane(m_camera_clipping_plane.get_data());
-    m_volumes.set_show_sinking_contours(! m_gizmos.is_hiding_instances());
-#if ENABLE_SHOW_NON_MANIFOLD_EDGES
-    m_volumes.set_show_non_manifold_edges(!m_gizmos.is_hiding_instances() && m_gizmos.get_current_type() != GLGizmosManager::Simplify);
-#endif // ENABLE_SHOW_NON_MANIFOLD_EDGES
-
-    GLShaderProgram* shader = wxGetApp().get_shader("gouraud");
-    if (shader != nullptr) {
-        shader->start_using();
-
-        switch (type)
-        {
-        default:
-        case GLVolumeCollection::ERenderType::Opaque:
-        {
-            if (m_picking_enabled && !m_gizmos.is_dragging() && m_layers_editing.is_enabled() && (m_layers_editing.last_object_id != -1) && (m_layers_editing.object_max_z() > 0.0f)) {
-                int object_id = m_layers_editing.last_object_id;
-#if ENABLE_GL_SHADERS_ATTRIBUTES
-                const Camera& camera = wxGetApp().plater()->get_camera();
-                m_volumes.render(type, false, camera.get_view_matrix(), camera.get_projection_matrix(), [object_id](const GLVolume& volume) {
-                    // Which volume to paint without the layer height profile shader?
-                    return volume.is_active && (volume.is_modifier || volume.composite_id.object_id != object_id);
-                    });
-#else
-                m_volumes.render(type, false, wxGetApp().plater()->get_camera().get_view_matrix(), [object_id](const GLVolume& volume) {
-                    // Which volume to paint without the layer height profile shader?
-                    return volume.is_active && (volume.is_modifier || volume.composite_id.object_id != object_id);
-                    });
-#endif // ENABLE_GL_SHADERS_ATTRIBUTES
-                // Let LayersEditing handle rendering of the active object using the layer height profile shader.
-                m_layers_editing.render_volumes(*this, m_volumes);
-            }
-            else {
-                // do not cull backfaces to show broken geometry, if any
-#if ENABLE_GL_SHADERS_ATTRIBUTES
-                const Camera& camera = wxGetApp().plater()->get_camera();
-                m_volumes.render(type, m_picking_enabled, camera.get_view_matrix(), camera.get_projection_matrix(), [this](const GLVolume& volume) {
-                    return (m_render_sla_auxiliaries || volume.composite_id.volume_id >= 0);
-                    });
-#else
-                m_volumes.render(type, m_picking_enabled, wxGetApp().plater()->get_camera().get_view_matrix(), [this](const GLVolume& volume) {
-                    return (m_render_sla_auxiliaries || volume.composite_id.volume_id >= 0);
-                    });
-#endif // ENABLE_GL_SHADERS_ATTRIBUTES
-            }
-
-            // In case a painting gizmo is open, it should render the painted triangles
-            // before transparent objects are rendered. Otherwise they would not be
-            // visible when inside modifier meshes etc.
-            {
-                GLGizmosManager& gm = get_gizmos_manager();
-//                GLGizmosManager::EType type = gm.get_current_type();
-                if (dynamic_cast<GLGizmoPainterBase*>(gm.get_current())) {
-                    shader->stop_using();
-                    gm.render_painter_gizmo();
-                    shader->start_using();
-                }
-            }
-            break;
-        }
-        case GLVolumeCollection::ERenderType::Transparent:
-        {
-#if ENABLE_GL_SHADERS_ATTRIBUTES
-            const Camera& camera = wxGetApp().plater()->get_camera();
-            m_volumes.render(type, false, camera.get_view_matrix(), camera.get_projection_matrix());
-#else
-            m_volumes.render(type, false, wxGetApp().plater()->get_camera().get_view_matrix());
-#endif // ENABLE_GL_SHADERS_ATTRIBUTES
-            break;
-        }
-        }
-        shader->stop_using();
-    }
-
-    m_camera_clipping_plane = ClippingPlane::ClipsNothing();
-}
-
-void GLCanvas3D::_render_gcode()
-{
-    m_gcode_viewer.render();
-}
-
-#if ENABLE_SHOW_TOOLPATHS_COG
-void GLCanvas3D::_render_gcode_cog()
-{
-    m_gcode_viewer.render_cog();
-}
-#endif // ENABLE_SHOW_TOOLPATHS_COG
-
-void GLCanvas3D::_render_selection()
-{
-    float scale_factor = 1.0;
-#if ENABLE_RETINA_GL
-    scale_factor = m_retina_helper->get_scale_factor();
-#endif // ENABLE_RETINA_GL
-
-    if (!m_gizmos.is_running())
-        m_selection.render(scale_factor);
-}
-
-void GLCanvas3D::_render_sequential_clearance()
-{
-    if (m_layers_editing.is_enabled() || m_gizmos.is_dragging())
-        return;
-
-    switch (m_gizmos.get_current_type())
-    {
-    case GLGizmosManager::EType::Flatten:
-    case GLGizmosManager::EType::Cut:
-    case GLGizmosManager::EType::Hollow:
-    case GLGizmosManager::EType::SlaSupports:
-    case GLGizmosManager::EType::FdmSupports:
-    case GLGizmosManager::EType::Seam: { return; }
-    default: { break; }
-    }
- 
-    m_sequential_print_clearance.render();
-}
-
-#if ENABLE_RENDER_SELECTION_CENTER
-void GLCanvas3D::_render_selection_center()
-{
-    m_selection.render_center(m_gizmos.is_dragging());
-}
-#endif // ENABLE_RENDER_SELECTION_CENTER
-
-void GLCanvas3D::_check_and_update_toolbar_icon_scale()
-{
-    // Don't update a toolbar scale, when we are on a Preview
-    if (wxGetApp().plater()->is_preview_shown())
-        return;
-
-    float scale = wxGetApp().toolbar_icon_scale();
-    Size cnv_size = get_canvas_size();
-
-    float size = GLToolbar::Default_Icons_Size * scale;
-
-    // Set current size for all top toolbars. It will be used for next calculations
-    GLToolbar& collapse_toolbar = wxGetApp().plater()->get_collapse_toolbar();
-#if ENABLE_RETINA_GL
-    const float sc = m_retina_helper->get_scale_factor() * scale;
-    m_main_toolbar.set_scale(sc);
-    m_undoredo_toolbar.set_scale(sc);
-    collapse_toolbar.set_scale(sc);
-    size *= m_retina_helper->get_scale_factor();
-#else
-    m_main_toolbar.set_icons_size(size);
-    m_undoredo_toolbar.set_icons_size(size);
-    collapse_toolbar.set_icons_size(size);
-#endif // ENABLE_RETINA_GL
-
-    float top_tb_width = m_main_toolbar.get_width() + m_undoredo_toolbar.get_width() + collapse_toolbar.get_width();
-    int   items_cnt = m_main_toolbar.get_visible_items_cnt() + m_undoredo_toolbar.get_visible_items_cnt() + collapse_toolbar.get_visible_items_cnt();
-    float noitems_width = top_tb_width - size * items_cnt; // width of separators and borders in top toolbars 
-
-    // calculate scale needed for items in all top toolbars
-    float new_h_scale = (cnv_size.get_width() - noitems_width) / (items_cnt * GLToolbar::Default_Icons_Size);
-
-    items_cnt = m_gizmos.get_selectable_icons_cnt() + 3; // +3 means a place for top and view toolbars and separators in gizmos toolbar
-
-    // calculate scale needed for items in the gizmos toolbar
-    float new_v_scale = cnv_size.get_height() / (items_cnt * GLGizmosManager::Default_Icons_Size);
-
-    // set minimum scale as a auto scale for the toolbars
-    float new_scale = std::min(new_h_scale, new_v_scale);
-#if ENABLE_RETINA_GL
-    new_scale /= m_retina_helper->get_scale_factor();
-#endif
-    if (fabs(new_scale - scale) > 0.01) // scale is changed by 1% and more
-        wxGetApp().set_auto_toolbar_icon_scale(new_scale);
-}
-
-void GLCanvas3D::_render_overlays()
-{
-    glsafe(::glDisable(GL_DEPTH_TEST));
-#if !ENABLE_GL_SHADERS_ATTRIBUTES
-    glsafe(::glPushMatrix());
-    glsafe(::glLoadIdentity());
-    // ensure that the textures are renderered inside the frustrum
-    const Camera& camera = wxGetApp().plater()->get_camera();
-    glsafe(::glTranslated(0.0, 0.0, -(camera.get_near_z() + 0.005)));
-    // ensure that the overlay fits the frustrum near z plane
-    double gui_scale = camera.get_gui_scale();
-    glsafe(::glScaled(gui_scale, gui_scale, 1.0));
-#endif // !ENABLE_GL_SHADERS_ATTRIBUTES
-
-    _check_and_update_toolbar_icon_scale();
-
-    _render_gizmos_overlay();
-
-    // main toolbar and undoredo toolbar need to be both updated before rendering because both their sizes are needed
-    // to correctly place them
-#if ENABLE_RETINA_GL
-    const float scale = m_retina_helper->get_scale_factor() * wxGetApp().toolbar_icon_scale(/*true*/);
-    m_main_toolbar.set_scale(scale);
-    m_undoredo_toolbar.set_scale(scale);
-    wxGetApp().plater()->get_collapse_toolbar().set_scale(scale);
-#else
-    const float size = int(GLToolbar::Default_Icons_Size * wxGetApp().toolbar_icon_scale(/*true*/));
-    m_main_toolbar.set_icons_size(size);
-    m_undoredo_toolbar.set_icons_size(size);
-    wxGetApp().plater()->get_collapse_toolbar().set_icons_size(size);
-#endif // ENABLE_RETINA_GL
-
-    _render_main_toolbar();
-    _render_undoredo_toolbar();
-    _render_collapse_toolbar();
-    _render_view_toolbar();
-
-    if (m_layers_editing.last_object_id >= 0 && m_layers_editing.object_max_z() > 0.0f)
-        m_layers_editing.render_overlay(*this);
-
-    const ConfigOptionBool* opt = dynamic_cast<const ConfigOptionBool*>(m_config->option("complete_objects"));
-    bool sequential_print = opt != nullptr && opt->value;
-    std::vector<const ModelInstance*> sorted_instances;
-    if (sequential_print) {
-        for (ModelObject* model_object : m_model->objects)
-            for (ModelInstance* model_instance : model_object->instances) {
-                sorted_instances.emplace_back(model_instance);
-            }
-    }
-    m_labels.render(sorted_instances);
-
-#if !ENABLE_GL_SHADERS_ATTRIBUTES
-    glsafe(::glPopMatrix());
-#endif // !ENABLE_GL_SHADERS_ATTRIBUTES
-}
-
-void GLCanvas3D::_render_volumes_for_picking() const
-{
-#if ENABLE_LEGACY_OPENGL_REMOVAL
-#if ENABLE_GL_SHADERS_ATTRIBUTES
-    GLShaderProgram* shader = wxGetApp().get_shader("flat_clip");
-#else
-    GLShaderProgram* shader = wxGetApp().get_shader("flat");
-#endif // ENABLE_GL_SHADERS_ATTRIBUTES
-    if (shader == nullptr)
-        return;
-#endif // ENABLE_LEGACY_OPENGL_REMOVAL
-
-    // do not cull backfaces to show broken geometry, if any
-    glsafe(::glDisable(GL_CULL_FACE));
-
-#if !ENABLE_GL_SHADERS_ATTRIBUTES
-    glsafe(::glEnableClientState(GL_VERTEX_ARRAY));
-    glsafe(::glEnableClientState(GL_NORMAL_ARRAY));
-#endif // !ENABLE_GL_SHADERS_ATTRIBUTES
-
-    const Transform3d& view_matrix = wxGetApp().plater()->get_camera().get_view_matrix();
-    for (size_t type = 0; type < 2; ++ type) {
-        GLVolumeWithIdAndZList to_render = volumes_to_render(m_volumes.volumes, (type == 0) ? GLVolumeCollection::ERenderType::Opaque : GLVolumeCollection::ERenderType::Transparent, view_matrix);
-        for (const GLVolumeWithIdAndZ& volume : to_render)
-	        if (!volume.first->disabled && (volume.first->composite_id.volume_id >= 0 || m_render_sla_auxiliaries)) {
-		        // Object picking mode. Render the object with a color encoding the object index.
-                // we reserve color = (0,0,0) for occluders (as the printbed) 
-                // so we shift volumes' id by 1 to get the proper color
-                const unsigned int id = 1 + volume.second.first;
-#if ENABLE_LEGACY_OPENGL_REMOVAL
-                volume.first->model.set_color(picking_decode(id));
-                shader->start_using();
-#else
-                glsafe(::glColor4fv(picking_decode(id).data()));
-#endif // ENABLE_LEGACY_OPENGL_REMOVAL
-#if ENABLE_GL_SHADERS_ATTRIBUTES
-                const Camera& camera = wxGetApp().plater()->get_camera();
-                shader->set_uniform("view_model_matrix", camera.get_view_matrix() * volume.first->world_matrix());
-                shader->set_uniform("projection_matrix", camera.get_projection_matrix());
-                shader->set_uniform("volume_world_matrix", volume.first->world_matrix());
-                shader->set_uniform("z_range", m_volumes.get_z_range());
-                shader->set_uniform("clipping_plane", m_volumes.get_clipping_plane());
-#endif // ENABLE_GL_SHADERS_ATTRIBUTES
-                volume.first->render();
-#if ENABLE_LEGACY_OPENGL_REMOVAL
-                shader->stop_using();
-#endif // ENABLE_LEGACY_OPENGL_REMOVAL
-            }
-	}
-
-#if !ENABLE_GL_SHADERS_ATTRIBUTES
-    glsafe(::glDisableClientState(GL_NORMAL_ARRAY));
-    glsafe(::glDisableClientState(GL_VERTEX_ARRAY));
-#endif // !ENABLE_GL_SHADERS_ATTRIBUTES
-
-    glsafe(::glEnable(GL_CULL_FACE));
-}
-
-void GLCanvas3D::_render_current_gizmo() const
-{
-    m_gizmos.render_current_gizmo();
-}
-
-void GLCanvas3D::_render_gizmos_overlay()
-{
-#if ENABLE_RETINA_GL
-//     m_gizmos.set_overlay_scale(m_retina_helper->get_scale_factor());
-    const float scale = m_retina_helper->get_scale_factor()*wxGetApp().toolbar_icon_scale();
-    m_gizmos.set_overlay_scale(scale); //! #ys_FIXME_experiment
-#else
-//     m_gizmos.set_overlay_scale(m_canvas->GetContentScaleFactor());
-//     m_gizmos.set_overlay_scale(wxGetApp().em_unit()*0.1f);
-    const float size = int(GLGizmosManager::Default_Icons_Size * wxGetApp().toolbar_icon_scale());
-    m_gizmos.set_overlay_icon_size(size); //! #ys_FIXME_experiment
-#endif /* __WXMSW__ */
-
-    m_gizmos.render_overlay();
-
-    if (m_gizmo_highlighter.m_render_arrow)
-        m_gizmos.render_arrow(*this, m_gizmo_highlighter.m_gizmo_type);
-}
-
-void GLCanvas3D::_render_main_toolbar()
-{
-    if (!m_main_toolbar.is_enabled())
-        return;
-
-    const Size cnv_size = get_canvas_size();
-#if ENABLE_GL_SHADERS_ATTRIBUTES
-    const float top = 0.5f * (float)cnv_size.get_height();
-#else
-    const float inv_zoom = (float)wxGetApp().plater()->get_camera().get_inv_zoom();
-    const float top = 0.5f * (float)cnv_size.get_height() * inv_zoom;
-#endif // ENABLE_GL_SHADERS_ATTRIBUTES
-
-    GLToolbar& collapse_toolbar = wxGetApp().plater()->get_collapse_toolbar();
-    const float collapse_toolbar_width = collapse_toolbar.is_enabled() ? collapse_toolbar.get_width() : 0.0f;
-#if ENABLE_GL_SHADERS_ATTRIBUTES
-    const float left = -0.5f * (m_main_toolbar.get_width() + m_undoredo_toolbar.get_width() + collapse_toolbar_width);
-#else
-    const float left = -0.5f * (m_main_toolbar.get_width() + m_undoredo_toolbar.get_width() + collapse_toolbar_width) * inv_zoom;
-#endif // ENABLE_GL_SHADERS_ATTRIBUTES
-
-    m_main_toolbar.set_position(top, left);
-    m_main_toolbar.render(*this);
-    if (m_toolbar_highlighter.m_render_arrow)
-        m_main_toolbar.render_arrow(*this, m_toolbar_highlighter.m_toolbar_item);
-}
-
-void GLCanvas3D::_render_undoredo_toolbar()
-{
-    if (!m_undoredo_toolbar.is_enabled())
-        return;
-
-    const Size cnv_size = get_canvas_size();
-#if ENABLE_GL_SHADERS_ATTRIBUTES
-    const float top = 0.5f * (float)cnv_size.get_height();
-#else
-    float inv_zoom = (float)wxGetApp().plater()->get_camera().get_inv_zoom();
-
-    const float top = 0.5f * (float)cnv_size.get_height() * inv_zoom;
-#endif // ENABLE_GL_SHADERS_ATTRIBUTES
-    GLToolbar& collapse_toolbar = wxGetApp().plater()->get_collapse_toolbar();
-    const float collapse_toolbar_width = collapse_toolbar.is_enabled() ? collapse_toolbar.get_width() : 0.0f;
-#if ENABLE_GL_SHADERS_ATTRIBUTES
-    const float left = m_main_toolbar.get_width() - 0.5f * (m_main_toolbar.get_width() + m_undoredo_toolbar.get_width() + collapse_toolbar_width);
-#else
-    const float left = (m_main_toolbar.get_width() - 0.5f * (m_main_toolbar.get_width() + m_undoredo_toolbar.get_width() + collapse_toolbar_width)) * inv_zoom;
-#endif // ENABLE_GL_SHADERS_ATTRIBUTES
-
-    m_undoredo_toolbar.set_position(top, left);
-    m_undoredo_toolbar.render(*this);
-    if (m_toolbar_highlighter.m_render_arrow)
-        m_undoredo_toolbar.render_arrow(*this, m_toolbar_highlighter.m_toolbar_item);
-}
-
-void GLCanvas3D::_render_collapse_toolbar() const
-{
-    GLToolbar& collapse_toolbar = wxGetApp().plater()->get_collapse_toolbar();
-
-    const Size cnv_size = get_canvas_size();
-    const float band = m_layers_editing.is_enabled() ? (wxGetApp().imgui()->get_style_scaling() * LayersEditing::THICKNESS_BAR_WIDTH) : 0.0;
-#if ENABLE_GL_SHADERS_ATTRIBUTES
-    const float top  = 0.5f * (float)cnv_size.get_height();
-    const float left = 0.5f * (float)cnv_size.get_width() - collapse_toolbar.get_width() - band;
-#else
-    const float inv_zoom = (float)wxGetApp().plater()->get_camera().get_inv_zoom();
-
-    const float top = 0.5f * (float)cnv_size.get_height() * inv_zoom;
-    const float left = (0.5f * (float)cnv_size.get_width() - (float)collapse_toolbar.get_width() - band) * inv_zoom;
-#endif // ENABLE_GL_SHADERS_ATTRIBUTES
-
-    collapse_toolbar.set_position(top, left);
-    collapse_toolbar.render(*this);
-}
-
-void GLCanvas3D::_render_view_toolbar() const
-{
-    GLToolbar& view_toolbar = wxGetApp().plater()->get_view_toolbar();
-
-#if ENABLE_RETINA_GL
-    const float scale = m_retina_helper->get_scale_factor() * wxGetApp().toolbar_icon_scale();
-#if __APPLE__
-    view_toolbar.set_scale(scale);
-#else // if GTK3
-    const float size = int(GLGizmosManager::Default_Icons_Size * scale);
-    view_toolbar.set_icons_size(size);
-#endif // __APPLE__
-#else
-    const float size = int(GLGizmosManager::Default_Icons_Size * wxGetApp().toolbar_icon_scale());
-    view_toolbar.set_icons_size(size);
-#endif // ENABLE_RETINA_GL
-
-    const Size cnv_size = get_canvas_size();
-#if ENABLE_GL_SHADERS_ATTRIBUTES
-    // places the toolbar on the bottom-left corner of the 3d scene
-    float top = -0.5f * (float)cnv_size.get_height() + view_toolbar.get_height();
-    float left = -0.5f * (float)cnv_size.get_width();
-#else
-    float inv_zoom = (float)wxGetApp().plater()->get_camera().get_inv_zoom();
-
-    // places the toolbar on the bottom-left corner of the 3d scene
-    float top = (-0.5f * (float)cnv_size.get_height() + view_toolbar.get_height()) * inv_zoom;
-    float left = -0.5f * (float)cnv_size.get_width() * inv_zoom;
-#endif // ENABLE_GL_SHADERS_ATTRIBUTES
-    view_toolbar.set_position(top, left);
-    view_toolbar.render(*this);
-}
-
-#if ENABLE_SHOW_CAMERA_TARGET
-void GLCanvas3D::_render_camera_target()
-{
-    static const float half_length = 5.0f;
-
-    glsafe(::glDisable(GL_DEPTH_TEST));
-    glsafe(::glLineWidth(2.0f));
-
-#if ENABLE_LEGACY_OPENGL_REMOVAL
-    const Vec3f& target = wxGetApp().plater()->get_camera().get_target().cast<float>();
-    m_camera_target.target = target.cast<double>();
-
-    for (int i = 0; i < 3; ++i) {
-        if (!m_camera_target.axis[i].is_initialized()) {
-            m_camera_target.axis[i].reset();
-
-            GLModel::Geometry init_data;
-            init_data.format = { GLModel::Geometry::EPrimitiveType::Lines, GLModel::Geometry::EVertexLayout::P3 };
-            init_data.color = (i == X) ? ColorRGBA::X() : ((i == Y) ? ColorRGBA::Y() : ColorRGBA::Z());
-            init_data.reserve_vertices(2);
-            init_data.reserve_indices(2);
-
-            // vertices
-            if (i == X) {
-                init_data.add_vertex(Vec3f(-half_length, 0.0f, 0.0f));
-                init_data.add_vertex(Vec3f(+half_length, 0.0f, 0.0f));
-            }
-            else if (i == Y) {
-                init_data.add_vertex(Vec3f(0.0f, -half_length, 0.0f));
-                init_data.add_vertex(Vec3f(0.0f, +half_length, 0.0f));
-            }
-            else {
-                init_data.add_vertex(Vec3f(0.0f, 0.0f, -half_length));
-                init_data.add_vertex(Vec3f(0.0f, 0.0f, +half_length));
-            }
-
-            // indices
-            init_data.add_line(0, 1);
-
-            m_camera_target.axis[i].init_from(std::move(init_data));
-        }
-    }
-
-    GLShaderProgram* shader = wxGetApp().get_shader("flat");
-    if (shader != nullptr) {
-        shader->start_using();
-#if ENABLE_GL_SHADERS_ATTRIBUTES
-        const Camera& camera = wxGetApp().plater()->get_camera();
-        shader->set_uniform("view_model_matrix", camera.get_view_matrix() * Geometry::assemble_transform(m_camera_target.target));
-        shader->set_uniform("projection_matrix", camera.get_projection_matrix());
-#endif // ENABLE_GL_SHADERS_ATTRIBUTES
-        for (int i = 0; i < 3; ++i) {
-            m_camera_target.axis[i].render();
-        }
-        shader->stop_using();
-    }
-#else
-    ::glBegin(GL_LINES);
-    const Vec3d& target = wxGetApp().plater()->get_camera().get_target();
-    // draw line for x axis
-    ::glColor3f(1.0f, 0.0f, 0.0f);
-    ::glVertex3d(target.x() - half_length, target.y(), target.z());
-    ::glVertex3d(target.x() + half_length, target.y(), target.z());
-    // draw line for y axis
-    ::glColor3f(0.0f, 1.0f, 0.0f);
-    ::glVertex3d(target.x(), target.y() - half_length, target.z());
-    ::glVertex3d(target.x(), target.y() + half_length, target.z());
-    // draw line for z axis
-    ::glColor3f(0.0f, 0.0f, 1.0f);
-    ::glVertex3d(target.x(), target.y(), target.z() - half_length);
-    ::glVertex3d(target.x(), target.y(), target.z() + half_length);
-    glsafe(::glEnd());
-#endif // ENABLE_LEGACY_OPENGL_REMOVAL
-}
-#endif // ENABLE_SHOW_CAMERA_TARGET
-
-void GLCanvas3D::_render_sla_slices()
-{
-    if (!m_use_clipping_planes || current_printer_technology() != ptSLA)
-        return;
-
-    const SLAPrint* print = this->sla_print();
-    const PrintObjects& print_objects = print->objects();
-    if (print_objects.empty())
-        // nothing to render, return
-        return;
-
-    double clip_min_z = -m_clipping_planes[0].get_data()[3];
-    double clip_max_z = m_clipping_planes[1].get_data()[3];
-    for (unsigned int i = 0; i < (unsigned int)print_objects.size(); ++i) {
-        const SLAPrintObject* obj = print_objects[i];
-
-        if (!obj->is_step_done(slaposSliceSupports))
-            continue;
-
-#if ENABLE_LEGACY_OPENGL_REMOVAL
-        SlaCap::ObjectIdToModelsMap::iterator it_caps_bottom = m_sla_caps[0].triangles.find(i);
-        SlaCap::ObjectIdToModelsMap::iterator it_caps_top = m_sla_caps[1].triangles.find(i);
-#else
-        SlaCap::ObjectIdToTrianglesMap::iterator it_caps_bottom = m_sla_caps[0].triangles.find(i);
-        SlaCap::ObjectIdToTrianglesMap::iterator it_caps_top = m_sla_caps[1].triangles.find(i);
-#endif // ENABLE_LEGACY_OPENGL_REMOVAL
-        {
-            if (it_caps_bottom == m_sla_caps[0].triangles.end())
-                it_caps_bottom = m_sla_caps[0].triangles.emplace(i, SlaCap::Triangles()).first;
-            if (!m_sla_caps[0].matches(clip_min_z)) {
-                m_sla_caps[0].z = clip_min_z;
-#if ENABLE_LEGACY_OPENGL_REMOVAL
-                it_caps_bottom->second.object.reset();
-                it_caps_bottom->second.supports.reset();
-#else
-                it_caps_bottom->second.object.clear();
-                it_caps_bottom->second.supports.clear();
-#endif // ENABLE_LEGACY_OPENGL_REMOVAL
-            }
-            if (it_caps_top == m_sla_caps[1].triangles.end())
-                it_caps_top = m_sla_caps[1].triangles.emplace(i, SlaCap::Triangles()).first;
-            if (!m_sla_caps[1].matches(clip_max_z)) {
-                m_sla_caps[1].z = clip_max_z;
-#if ENABLE_LEGACY_OPENGL_REMOVAL
-                it_caps_top->second.object.reset();
-                it_caps_top->second.supports.reset();
-#else
-                it_caps_top->second.object.clear();
-                it_caps_top->second.supports.clear();
-#endif // ENABLE_LEGACY_OPENGL_REMOVAL
-            }
-        }
-#if ENABLE_LEGACY_OPENGL_REMOVAL
-        GLModel& bottom_obj_triangles = it_caps_bottom->second.object;
-        GLModel& bottom_sup_triangles = it_caps_bottom->second.supports;
-        GLModel& top_obj_triangles = it_caps_top->second.object;
-        GLModel& top_sup_triangles = it_caps_top->second.supports;
-#else
-        Pointf3s &bottom_obj_triangles = it_caps_bottom->second.object;
-        Pointf3s &bottom_sup_triangles = it_caps_bottom->second.supports;
-        Pointf3s &top_obj_triangles    = it_caps_top->second.object;
-        Pointf3s &top_sup_triangles    = it_caps_top->second.supports;
-#endif // ENABLE_LEGACY_OPENGL_REMOVAL
-
-#if ENABLE_LEGACY_OPENGL_REMOVAL
-        auto init_model = [](GLModel& model, const Pointf3s& triangles, const ColorRGBA& color) {
-            GLModel::Geometry init_data;
-            init_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3 };
-            init_data.reserve_vertices(triangles.size());
-            init_data.reserve_indices(triangles.size() / 3);
-            init_data.color = color;
-
-            unsigned int vertices_count = 0;
-            for (const Vec3d& v : triangles) {
-                init_data.add_vertex((Vec3f)v.cast<float>());
-                ++vertices_count;
-                if (vertices_count % 3 == 0)
-                    init_data.add_triangle(vertices_count - 3, vertices_count - 2, vertices_count - 1);
-            }
-
-            if (!init_data.is_empty())
-                model.init_from(std::move(init_data));
-        };
-
-        if ((!bottom_obj_triangles.is_initialized() || !bottom_sup_triangles.is_initialized() ||
-            !top_obj_triangles.is_initialized() || !top_sup_triangles.is_initialized()) && !obj->get_slice_index().empty()) {
-#else
-        if ((bottom_obj_triangles.empty() || bottom_sup_triangles.empty() || top_obj_triangles.empty() || top_sup_triangles.empty()) &&
-            !obj->get_slice_index().empty()) {
-#endif // ENABLE_LEGACY_OPENGL_REMOVAL
-            double layer_height         = print->default_object_config().layer_height.value;
-            double initial_layer_height = print->material_config().initial_layer_height.value;
-            bool   left_handed          = obj->is_left_handed();
-
-            coord_t key_zero = obj->get_slice_index().front().print_level();
-            // Slice at the center of the slab starting at clip_min_z will be rendered for the lower plane.
-            coord_t key_low  = coord_t((clip_min_z - initial_layer_height + layer_height) / SCALING_FACTOR) + key_zero;
-            // Slice at the center of the slab ending at clip_max_z will be rendered for the upper plane.
-            coord_t key_high = coord_t((clip_max_z - initial_layer_height) / SCALING_FACTOR) + key_zero;
-
-            const SliceRecord& slice_low  = obj->closest_slice_to_print_level(key_low, coord_t(SCALED_EPSILON));
-            const SliceRecord& slice_high = obj->closest_slice_to_print_level(key_high, coord_t(SCALED_EPSILON));
-
-            // Offset to avoid OpenGL Z fighting between the object's horizontal surfaces and the triangluated surfaces of the cuts.
-            const double plane_shift_z = 0.002;
-
-            if (slice_low.is_valid()) {
-                const ExPolygons& obj_bottom = slice_low.get_slice(soModel);
-                const ExPolygons& sup_bottom = slice_low.get_slice(soSupport);
-#if ENABLE_LEGACY_OPENGL_REMOVAL
-                // calculate model bottom cap
-                if (!bottom_obj_triangles.is_initialized() && !obj_bottom.empty())
-                    init_model(bottom_obj_triangles, triangulate_expolygons_3d(obj_bottom, clip_min_z - plane_shift_z, !left_handed), { 1.0f, 0.37f, 0.0f, 1.0f });
-                // calculate support bottom cap
-                if (!bottom_sup_triangles.is_initialized() && !sup_bottom.empty())
-                    init_model(bottom_sup_triangles, triangulate_expolygons_3d(sup_bottom, clip_min_z - plane_shift_z, !left_handed), { 1.0f, 0.0f, 0.37f, 1.0f });
-#else
-                // calculate model bottom cap
-                if (bottom_obj_triangles.empty() && !obj_bottom.empty())
-                    bottom_obj_triangles = triangulate_expolygons_3d(obj_bottom, clip_min_z - plane_shift_z, ! left_handed);
-                // calculate support bottom cap
-                if (bottom_sup_triangles.empty() && !sup_bottom.empty())
-                    bottom_sup_triangles = triangulate_expolygons_3d(sup_bottom, clip_min_z - plane_shift_z, !left_handed);
-#endif // ENABLE_LEGACY_OPENGL_REMOVAL
-            }
-
-            if (slice_high.is_valid()) {
-                const ExPolygons& obj_top = slice_high.get_slice(soModel);
-                const ExPolygons& sup_top = slice_high.get_slice(soSupport);
-#if ENABLE_LEGACY_OPENGL_REMOVAL
-                // calculate model top cap
-                if (!top_obj_triangles.is_initialized() && !obj_top.empty())
-                    init_model(top_obj_triangles, triangulate_expolygons_3d(obj_top, clip_max_z + plane_shift_z, left_handed), { 1.0f, 0.37f, 0.0f, 1.0f });
-                // calculate support top cap
-                if (!top_sup_triangles.is_initialized() && !sup_top.empty())
-                    init_model(top_sup_triangles, triangulate_expolygons_3d(sup_top, clip_max_z + plane_shift_z, left_handed), { 1.0f, 0.0f, 0.37f, 1.0f });
-#else
-                // calculate model top cap
-                if (top_obj_triangles.empty() && !obj_top.empty())
-                    top_obj_triangles = triangulate_expolygons_3d(obj_top, clip_max_z + plane_shift_z, left_handed);
-                // calculate support top cap
-                if (top_sup_triangles.empty() && !sup_top.empty())
-                    top_sup_triangles = triangulate_expolygons_3d(sup_top, clip_max_z + plane_shift_z, left_handed);
-#endif // ENABLE_LEGACY_OPENGL_REMOVAL
-            }
-        }
-
-#if ENABLE_LEGACY_OPENGL_REMOVAL
-        GLShaderProgram* shader = wxGetApp().get_shader("flat");
-        if (shader != nullptr) {
-            shader->start_using();
-
-            for (const SLAPrintObject::Instance& inst : obj->instances()) {
-#if ENABLE_GL_SHADERS_ATTRIBUTES
-                const Camera& camera = wxGetApp().plater()->get_camera();
-                const Transform3d view_model_matrix = camera.get_view_matrix() *
-                    Geometry::assemble_transform(Vec3d(unscale<double>(inst.shift.x()), unscale<double>(inst.shift.y()), 0.0),
-                        inst.rotation * Vec3d::UnitZ(), Vec3d::Ones(),
-                        obj->is_left_handed() ? Vec3d(-1.0f, 1.0f, 1.0f) : Vec3d::Ones());
-
-                shader->set_uniform("view_model_matrix", view_model_matrix);
-                shader->set_uniform("projection_matrix", camera.get_projection_matrix());
-#else
-                glsafe(::glPushMatrix());
-                glsafe(::glTranslated(unscale<double>(inst.shift.x()), unscale<double>(inst.shift.y()), 0.0));
-                glsafe(::glRotatef(Geometry::rad2deg(inst.rotation), 0.0f, 0.0f, 1.0f));
-                if (obj->is_left_handed())
-                    // The polygons are mirrored by X.
-                    glsafe(::glScalef(-1.0f, 1.0f, 1.0f));
-#endif // ENABLE_GL_SHADERS_ATTRIBUTES
-
-                bottom_obj_triangles.render();
-                top_obj_triangles.render();
-                bottom_sup_triangles.render();
-                top_sup_triangles.render();
-
-#if !ENABLE_GL_SHADERS_ATTRIBUTES
-                glsafe(::glPopMatrix());
-#endif // !ENABLE_GL_SHADERS_ATTRIBUTES
-            }
-
-            shader->stop_using();
-        }
-#else
-        if (!bottom_obj_triangles.empty() || !top_obj_triangles.empty() || !bottom_sup_triangles.empty() || !top_sup_triangles.empty()) {
-			for (const SLAPrintObject::Instance& inst : obj->instances()) {
-                glsafe(::glPushMatrix());
-                glsafe(::glTranslated(unscale<double>(inst.shift.x()), unscale<double>(inst.shift.y()), 0.0));
-                glsafe(::glRotatef(Geometry::rad2deg(inst.rotation), 0.0f, 0.0f, 1.0f));
-				if (obj->is_left_handed())
-                    // The polygons are mirrored by X.
-                    glsafe(::glScalef(-1.0f, 1.0f, 1.0f));
-                glsafe(::glEnableClientState(GL_VERTEX_ARRAY));
-                glsafe(::glColor3f(1.0f, 0.37f, 0.0f));
-				if (!bottom_obj_triangles.empty()) {
-                    glsafe(::glVertexPointer(3, GL_DOUBLE, 0, (GLdouble*)bottom_obj_triangles.front().data()));
-                    glsafe(::glDrawArrays(GL_TRIANGLES, 0, bottom_obj_triangles.size()));
-				}
-				if (! top_obj_triangles.empty()) {
-                    glsafe(::glVertexPointer(3, GL_DOUBLE, 0, (GLdouble*)top_obj_triangles.front().data()));
-                    glsafe(::glDrawArrays(GL_TRIANGLES, 0, top_obj_triangles.size()));
-				}
-                glsafe(::glColor3f(1.0f, 0.0f, 0.37f));
-				if (! bottom_sup_triangles.empty()) {
-                    glsafe(::glVertexPointer(3, GL_DOUBLE, 0, (GLdouble*)bottom_sup_triangles.front().data()));
-                    glsafe(::glDrawArrays(GL_TRIANGLES, 0, bottom_sup_triangles.size()));
-				}
-				if (! top_sup_triangles.empty()) {
-                    glsafe(::glVertexPointer(3, GL_DOUBLE, 0, (GLdouble*)top_sup_triangles.front().data()));
-                    glsafe(::glDrawArrays(GL_TRIANGLES, 0, top_sup_triangles.size()));
-				}
-                glsafe(::glDisableClientState(GL_VERTEX_ARRAY));
-                glsafe(::glPopMatrix());
-            }
-        }
-#endif // ENABLE_LEGACY_OPENGL_REMOVAL
-    }
-}
-
-void GLCanvas3D::_render_selection_sidebar_hints()
-{
-    m_selection.render_sidebar_hints(m_sidebar_field);
-}
-
-void GLCanvas3D::_update_volumes_hover_state()
-{
-    for (GLVolume* v : m_volumes.volumes) {
-        v->hover = GLVolume::HS_None;
-    }
-
-    if (m_hover_volume_idxs.empty())
-        return;
-
-    bool ctrl_pressed = wxGetKeyState(WXK_CONTROL); // additive select/deselect
-    bool shift_pressed = wxGetKeyState(WXK_SHIFT);  // select by rectangle
-    bool alt_pressed = wxGetKeyState(WXK_ALT);      // deselect by rectangle
-
-    if (alt_pressed && (shift_pressed || ctrl_pressed)) {
-        // illegal combinations of keys
-        m_hover_volume_idxs.clear();
-        return;
-    }
-
-#if !ENABLE_NEW_RECTANGLE_SELECTION
-    bool selection_modifiers_only = m_selection.is_empty() || m_selection.is_any_modifier();
-#endif // !ENABLE_NEW_RECTANGLE_SELECTION
-
-    bool hover_modifiers_only = true;
-    for (int i : m_hover_volume_idxs) {
-        if (!m_volumes.volumes[i]->is_modifier) {
-            hover_modifiers_only = false;
-            break;
-        }
-    }
-
-    std::set<std::pair<int, int>> hover_instances;
-    for (int i : m_hover_volume_idxs) {
-        const GLVolume& v = *m_volumes.volumes[i];
-        hover_instances.insert(std::make_pair(v.object_idx(), v.instance_idx()));
-    }
-
-    bool hover_from_single_instance = hover_instances.size() == 1;
-
-    if (hover_modifiers_only && !hover_from_single_instance) {
-        // do not allow to select volumes from different instances
-        m_hover_volume_idxs.clear();
-        return;
-    }
-
-    for (int i : m_hover_volume_idxs) {
-        GLVolume& volume = *m_volumes.volumes[i];
-        if (volume.hover != GLVolume::HS_None)
-            continue;
-
-#if ENABLE_NEW_RECTANGLE_SELECTION
-        bool deselect = volume.selected && ((shift_pressed && m_rectangle_selection.is_empty()) || (alt_pressed && !m_rectangle_selection.is_empty()));
-        bool select   = !volume.selected && (m_rectangle_selection.is_empty() || (shift_pressed && !m_rectangle_selection.is_empty()));
-#else
-        bool deselect = volume.selected && ((ctrl_pressed && !shift_pressed) || alt_pressed);
-        // (volume->is_modifier && !selection_modifiers_only && !is_ctrl_pressed) -> allows hovering on selected modifiers belonging to selection of type Instance
-        bool select = (!volume.selected || (volume.is_modifier && !selection_modifiers_only && !ctrl_pressed)) && !alt_pressed;
-#endif // ENABLE_NEW_RECTANGLE_SELECTION
-
-        if (select || deselect) {
-            bool as_volume =
-                volume.is_modifier && hover_from_single_instance && !ctrl_pressed &&
-                (
-                (!deselect) ||
-                (deselect && !m_selection.is_single_full_instance() && (volume.object_idx() == m_selection.get_object_idx()) && (volume.instance_idx() == m_selection.get_instance_idx()))
-                );
-
-            if (as_volume)
-                volume.hover = deselect ? GLVolume::HS_Deselect : GLVolume::HS_Select;
-            else {
-                int object_idx = volume.object_idx();
-                int instance_idx = volume.instance_idx();
-
-                for (GLVolume* v : m_volumes.volumes) {
-                    if (v->object_idx() == object_idx && v->instance_idx() == instance_idx)
-                        v->hover = deselect ? GLVolume::HS_Deselect : GLVolume::HS_Select;
-                }
-            }
-        }
-        else if (volume.selected)
-            volume.hover = GLVolume::HS_Hover;
-    }
-}
-
-void GLCanvas3D::_perform_layer_editing_action(wxMouseEvent* evt)
-{
-    int object_idx_selected = m_layers_editing.last_object_id;
-    if (object_idx_selected == -1)
-        return;
-
-    // A volume is selected. Test, whether hovering over a layer thickness bar.
-    if (evt != nullptr) {
-        const Rect& rect = LayersEditing::get_bar_rect_screen(*this);
-        float b = rect.get_bottom();
-        m_layers_editing.last_z = m_layers_editing.object_max_z() * (b - evt->GetY() - 1.0f) / (b - rect.get_top());
-        m_layers_editing.last_action = 
-            evt->ShiftDown() ? (evt->RightIsDown() ? LAYER_HEIGHT_EDIT_ACTION_SMOOTH : LAYER_HEIGHT_EDIT_ACTION_REDUCE) : 
-                               (evt->RightIsDown() ? LAYER_HEIGHT_EDIT_ACTION_INCREASE : LAYER_HEIGHT_EDIT_ACTION_DECREASE);
-    }
-
-    m_layers_editing.adjust_layer_height_profile();
-    _refresh_if_shown_on_screen();
-
-    // Automatic action on mouse down with the same coordinate.
-    _start_timer();
-}
-
-Vec3d GLCanvas3D::_mouse_to_3d(const Point& mouse_pos, float* z)
-{
-    if (m_canvas == nullptr)
-        return Vec3d(DBL_MAX, DBL_MAX, DBL_MAX);
-
-    const Camera& camera = wxGetApp().plater()->get_camera();
-    const Matrix4d modelview  = camera.get_view_matrix().matrix();
-    const Matrix4d projection = camera.get_projection_matrix().matrix();
-    const Vec4i viewport(camera.get_viewport().data());
-
-    const int y = viewport[3] - mouse_pos.y();
-    float mouse_z;
-    if (z == nullptr)
-        glsafe(::glReadPixels(mouse_pos.x(), y, 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT, (void*)&mouse_z));
-    else
-        mouse_z = *z;
-
-    Vec3d out;
-    igl::unproject(Vec3d(mouse_pos.x(), y, mouse_z), modelview, projection, viewport, out);
-    return out;
-}
-
-Vec3d GLCanvas3D::_mouse_to_bed_3d(const Point& mouse_pos)
-{
-    return mouse_ray(mouse_pos).intersect_plane(0.0);
-}
-
-void GLCanvas3D::_start_timer()
-{
-    m_timer.Start(100, wxTIMER_CONTINUOUS);
-}
-
-void GLCanvas3D::_stop_timer()
-{
-    m_timer.Stop();
-}
-
-void GLCanvas3D::_load_print_toolpaths(const BuildVolume &build_volume)
-{
-    const Print *print = this->fff_print();
-    if (print == nullptr)
-        return;
-
-    if (! print->is_step_done(psSkirtBrim))
-        return;
-
-    if (!print->has_skirt() && !print->has_brim())
-        return;
-
-    const ColorRGBA color = ColorRGBA::GREENISH();
-
-    // number of skirt layers
-    size_t total_layer_count = 0;
-    for (const PrintObject* print_object : print->objects()) {
-        total_layer_count = std::max(total_layer_count, print_object->total_layer_count());
-    }
-    size_t skirt_height = print->has_infinite_skirt() ? total_layer_count : std::min<size_t>(print->config().skirt_height.value, total_layer_count);
-    if (skirt_height == 0 && print->has_brim())
-        skirt_height = 1;
-
-    // Get first skirt_height layers.
-    //FIXME This code is fishy. It may not work for multiple objects with different layering due to variable layer height feature.
-    // This is not critical as this is just an initial preview.
-    const PrintObject* highest_object = *std::max_element(print->objects().begin(), print->objects().end(), [](auto l, auto r){ return l->layers().size() < r->layers().size(); });
-    std::vector<float> print_zs;
-    print_zs.reserve(skirt_height * 2);
-    for (size_t i = 0; i < std::min(skirt_height, highest_object->layers().size()); ++ i)
-        print_zs.emplace_back(float(highest_object->layers()[i]->print_z));
-    // Only add skirt for the raft layers.
-    for (size_t i = 0; i < std::min(skirt_height, std::min(highest_object->slicing_parameters().raft_layers(), highest_object->support_layers().size())); ++ i)
-        print_zs.emplace_back(float(highest_object->support_layers()[i]->print_z));
-    sort_remove_duplicates(print_zs);
-    skirt_height = std::min(skirt_height, print_zs.size());
-    print_zs.erase(print_zs.begin() + skirt_height, print_zs.end());
-
-#if ENABLE_LEGACY_OPENGL_REMOVAL
-    GLVolume* volume = m_volumes.new_toolpath_volume(color);
-    GLModel::Geometry init_data;
-    init_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3N3 };
-#else
-    GLVolume *volume = m_volumes.new_toolpath_volume(color, VERTEX_BUFFER_RESERVE_SIZE);
-#endif // ENABLE_LEGACY_OPENGL_REMOVAL
-    for (size_t i = 0; i < skirt_height; ++ i) {
-        volume->print_zs.emplace_back(print_zs[i]);
-#if ENABLE_LEGACY_OPENGL_REMOVAL
-        volume->offsets.emplace_back(init_data.indices_count());
-        if (i == 0)
-            _3DScene::extrusionentity_to_verts(print->brim(), print_zs[i], Point(0, 0), init_data);
-        _3DScene::extrusionentity_to_verts(print->skirt(), print_zs[i], Point(0, 0), init_data);
-#else
-        volume->offsets.emplace_back(volume->indexed_vertex_array.quad_indices.size());
-        volume->offsets.emplace_back(volume->indexed_vertex_array.triangle_indices.size());
-        if (i == 0)
-            _3DScene::extrusionentity_to_verts(print->brim(), print_zs[i], Point(0, 0), *volume);
-        _3DScene::extrusionentity_to_verts(print->skirt(), print_zs[i], Point(0, 0), *volume);
-#endif // ENABLE_LEGACY_OPENGL_REMOVAL
-        // Ensure that no volume grows over the limits. If the volume is too large, allocate a new one.
-#if ENABLE_LEGACY_OPENGL_REMOVAL
-        if (init_data.vertices_size_bytes() > MAX_VERTEX_BUFFER_SIZE) {
-            volume->model.init_from(std::move(init_data));
-#else
-        if (volume->indexed_vertex_array.vertices_and_normals_interleaved.size() > MAX_VERTEX_BUFFER_SIZE) {
-#endif // ENABLE_LEGACY_OPENGL_REMOVAL
-            GLVolume &vol = *volume;
-            volume = m_volumes.new_toolpath_volume(vol.color);
-#if !ENABLE_LEGACY_OPENGL_REMOVAL
-            reserve_new_volume_finalize_old_volume(*volume, vol, m_initialized);
-#endif // !ENABLE_LEGACY_OPENGL_REMOVAL
-        }
-    }
-#if ENABLE_LEGACY_OPENGL_REMOVAL
-    volume->model.init_from(std::move(init_data));
-    volume->is_outside = !contains(build_volume, volume->model);
-#else
-    volume->is_outside = ! build_volume.all_paths_inside_vertices_and_normals_interleaved(volume->indexed_vertex_array.vertices_and_normals_interleaved, volume->indexed_vertex_array.bounding_box());
-    volume->indexed_vertex_array.finalize_geometry(m_initialized);
-#endif // ENABLE_LEGACY_OPENGL_REMOVAL
-}
-
-void GLCanvas3D::_load_print_object_toolpaths(const PrintObject& print_object, const BuildVolume& build_volume, const std::vector<std::string>& str_tool_colors, const std::vector<CustomGCode::Item>& color_print_values)
-{
-    std::vector<ColorRGBA> tool_colors;
-    decode_colors(str_tool_colors, tool_colors);
-
-    struct Ctxt
-    {
-        const PrintInstances        *shifted_copies;
-        std::vector<const Layer*>    layers;
-        bool                         has_perimeters;
-        bool                         has_infill;
-        bool                         has_support;
-        const std::vector<ColorRGBA>* tool_colors;
-        bool                         is_single_material_print;
-        int                          extruders_cnt;
-        const std::vector<CustomGCode::Item>*   color_print_values;
-
-        static ColorRGBA color_perimeters()           { return ColorRGBA::YELLOW(); }
-        static ColorRGBA color_infill()               { return ColorRGBA::REDISH(); }
-        static ColorRGBA color_support()              { return ColorRGBA::GREENISH(); }
-        static ColorRGBA color_pause_or_custom_code() { return ColorRGBA::GRAY(); }
-
-        // For cloring by a tool, return a parsed color.
-        bool                         color_by_tool() const { return tool_colors != nullptr; }
-        size_t                       number_tools() const { return color_by_tool() ? tool_colors->size() : 0; }
-        const ColorRGBA&             color_tool(size_t tool) const { return (*tool_colors)[tool]; }
-
-        // For coloring by a color_print(M600), return a parsed color.
-        bool                         color_by_color_print() const { return color_print_values!=nullptr; }
-        const size_t                 color_print_color_idx_by_layer_idx(const size_t layer_idx) const {
-            const CustomGCode::Item value{layers[layer_idx]->print_z + EPSILON, CustomGCode::Custom, 0, ""};
-            auto it = std::lower_bound(color_print_values->begin(), color_print_values->end(), value);
-            return (it - color_print_values->begin()) % number_tools();
-        }
-
-        const size_t                 color_print_color_idx_by_layer_idx_and_extruder(const size_t layer_idx, const int extruder) const
-        {
-            const coordf_t print_z = layers[layer_idx]->print_z;
-
-            auto it = std::find_if(color_print_values->begin(), color_print_values->end(),
-                [print_z](const CustomGCode::Item& code)
-                { return fabs(code.print_z - print_z) < EPSILON; });
-            if (it != color_print_values->end()) {
-                CustomGCode::Type type = it->type;
-                // pause print or custom Gcode
-                if (type == CustomGCode::PausePrint ||
-                    (type != CustomGCode::ColorChange && type != CustomGCode::ToolChange))
-                    return number_tools()-1; // last color item is a gray color for pause print or custom G-code 
-
-                // change tool (extruder) 
-                if (type == CustomGCode::ToolChange)
-                    return get_color_idx_for_tool_change(it, extruder);
-                // change color for current extruder
-                if (type == CustomGCode::ColorChange) {
-                    int color_idx = get_color_idx_for_color_change(it, extruder);
-                    if (color_idx >= 0)
-                        return color_idx;
-                }
-            }
-
-            const CustomGCode::Item value{print_z + EPSILON, CustomGCode::Custom, 0, ""};
-            it = std::lower_bound(color_print_values->begin(), color_print_values->end(), value);
-            while (it != color_print_values->begin()) {
-                --it;
-                // change color for current extruder
-                if (it->type == CustomGCode::ColorChange) {
-                    int color_idx = get_color_idx_for_color_change(it, extruder);
-                    if (color_idx >= 0)
-                        return color_idx;
-                }
-                // change tool (extruder) 
-                if (it->type == CustomGCode::ToolChange)
-                    return get_color_idx_for_tool_change(it, extruder);
-            }
-
-            return std::min<int>(extruders_cnt - 1, std::max<int>(extruder - 1, 0));;
-        }
-
-    private:
-        int get_m600_color_idx(std::vector<CustomGCode::Item>::const_iterator it) const
-        {
-            int shift = 0;
-            while (it != color_print_values->begin()) {
-                --it;
-                if (it->type == CustomGCode::ColorChange)
-                    shift++;
-            }
-            return extruders_cnt + shift;
-        }
-
-        int get_color_idx_for_tool_change(std::vector<CustomGCode::Item>::const_iterator it, const int extruder) const
-        {
-            const int current_extruder = it->extruder == 0 ? extruder : it->extruder;
-            if (number_tools() == size_t(extruders_cnt + 1)) // there is no one "M600"
-                return std::min<int>(extruders_cnt - 1, std::max<int>(current_extruder - 1, 0));
-
-            auto it_n = it;
-            while (it_n != color_print_values->begin()) {
-                --it_n;
-                if (it_n->type == CustomGCode::ColorChange && it_n->extruder == current_extruder)
-                    return get_m600_color_idx(it_n);
-            }
-
-            return std::min<int>(extruders_cnt - 1, std::max<int>(current_extruder - 1, 0));
-        }
-
-        int get_color_idx_for_color_change(std::vector<CustomGCode::Item>::const_iterator it, const int extruder) const
-        {
-            if (extruders_cnt == 1)
-                return get_m600_color_idx(it);
-
-            auto it_n = it;
-            bool is_tool_change = false;
-            while (it_n != color_print_values->begin()) {
-                --it_n;
-                if (it_n->type == CustomGCode::ToolChange) {
-                    is_tool_change = true;
-                    if (it_n->extruder == it->extruder || (it_n->extruder == 0 && it->extruder == extruder))
-                        return get_m600_color_idx(it);
-                    break;
-                }
-            }
-            if (!is_tool_change && it->extruder == extruder)
-                return get_m600_color_idx(it);
-
-            return -1;
-        }
-
-    } ctxt;
-
-    ctxt.has_perimeters = print_object.is_step_done(posPerimeters);
-    ctxt.has_infill = print_object.is_step_done(posInfill);
-    ctxt.has_support = print_object.is_step_done(posSupportMaterial);
-    ctxt.tool_colors = tool_colors.empty() ? nullptr : &tool_colors;
-    ctxt.color_print_values = color_print_values.empty() ? nullptr : &color_print_values;
-    ctxt.is_single_material_print = this->fff_print()->extruders().size()==1;
-    ctxt.extruders_cnt = wxGetApp().extruders_edited_cnt();
-
-    ctxt.shifted_copies = &print_object.instances();
-
-    // order layers by print_z
-    {
-        size_t nlayers = 0;
-        if (ctxt.has_perimeters || ctxt.has_infill)
-            nlayers = print_object.layers().size();
-        if (ctxt.has_support)
-            nlayers += print_object.support_layers().size();
-        ctxt.layers.reserve(nlayers);
-    }
-    if (ctxt.has_perimeters || ctxt.has_infill)
-        for (const Layer *layer : print_object.layers())
-            ctxt.layers.emplace_back(layer);
-    if (ctxt.has_support)
-        for (const Layer *layer : print_object.support_layers())
-            ctxt.layers.emplace_back(layer);
-    std::sort(ctxt.layers.begin(), ctxt.layers.end(), [](const Layer *l1, const Layer *l2) { return l1->print_z < l2->print_z; });
-
-    // Maximum size of an allocation block: 32MB / sizeof(float)
-    BOOST_LOG_TRIVIAL(debug) << "Loading print object toolpaths in parallel - start" << m_volumes.log_memory_info() << log_memory_info();
-
-    const bool is_selected_separate_extruder = m_selected_extruder > 0 && ctxt.color_by_color_print();
-
-    //FIXME Improve the heuristics for a grain size.
-    size_t          grain_size = std::max(ctxt.layers.size() / 16, size_t(1));
-    tbb::spin_mutex new_volume_mutex;
-    auto            new_volume = [this, &new_volume_mutex](const ColorRGBA& color) {
-        // Allocate the volume before locking.
-		GLVolume *volume = new GLVolume(color);
-		volume->is_extrusion_path = true;
-#if ENABLE_LEGACY_OPENGL_REMOVAL
-        // to prevent sending data to gpu (in the main thread) while
-        // editing the model geometry
-        volume->model.disable_render();
-#endif // ENABLE_LEGACY_OPENGL_REMOVAL
-        tbb::spin_mutex::scoped_lock lock;
-    	// Lock by ROII, so if the emplace_back() fails, the lock will be released.
-        lock.acquire(new_volume_mutex);
-        m_volumes.volumes.emplace_back(volume);
-        lock.release();
-        return volume;
-    };
-    const size_t    volumes_cnt_initial = m_volumes.volumes.size();
-    tbb::parallel_for(
-        tbb::blocked_range<size_t>(0, ctxt.layers.size(), grain_size),
-        [&ctxt, &new_volume, is_selected_separate_extruder, this](const tbb::blocked_range<size_t>& range) {
-        GLVolumePtrs 		vols;
-#if ENABLE_LEGACY_OPENGL_REMOVAL
-        std::vector<GLModel::Geometry> geometries;
-        auto select_geometry = [&ctxt, &geometries](size_t layer_idx, int extruder, int feature) -> GLModel::Geometry& {
-            return geometries[ctxt.color_by_color_print() ?
-                ctxt.color_print_color_idx_by_layer_idx_and_extruder(layer_idx, extruder) :
-                ctxt.color_by_tool() ?
-                std::min<int>(ctxt.number_tools() - 1, std::max<int>(extruder - 1, 0)) :
-                feature
-            ];
-        };
-#else
-        auto                volume = [&ctxt, &vols](size_t layer_idx, int extruder, int feature) -> GLVolume& {
-            return *vols[ctxt.color_by_color_print() ?
-                ctxt.color_print_color_idx_by_layer_idx_and_extruder(layer_idx, extruder) :
-                ctxt.color_by_tool() ?
-                std::min<int>(ctxt.number_tools() - 1, std::max<int>(extruder - 1, 0)) :
-                feature
-            ];
-        };
-#endif // ENABLE_LEGACY_OPENGL_REMOVAL
-        if (ctxt.color_by_color_print() || ctxt.color_by_tool()) {
-            for (size_t i = 0; i < ctxt.number_tools(); ++i) {
-                vols.emplace_back(new_volume(ctxt.color_tool(i)));
-#if ENABLE_LEGACY_OPENGL_REMOVAL
-                geometries.emplace_back(GLModel::Geometry());
-#endif // ENABLE_LEGACY_OPENGL_REMOVAL
-            }
-        }
-        else {
-            vols = { new_volume(ctxt.color_perimeters()), new_volume(ctxt.color_infill()), new_volume(ctxt.color_support()) };
-#if ENABLE_LEGACY_OPENGL_REMOVAL
-            geometries = { GLModel::Geometry(), GLModel::Geometry(), GLModel::Geometry() };
-#endif // ENABLE_LEGACY_OPENGL_REMOVAL
-        }
-
-#if ENABLE_LEGACY_OPENGL_REMOVAL
-        assert(vols.size() == geometries.size());
-        for (GLModel::Geometry& g : geometries) {
-            g.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3N3 };
-        }
-#else
-        for (GLVolume *vol : vols)
-			// Reserving number of vertices (3x position + 3x color)
-        	vol->indexed_vertex_array.reserve(VERTEX_BUFFER_RESERVE_SIZE / 6);
-#endif // ENABLE_LEGACY_OPENGL_REMOVAL
-        for (size_t idx_layer = range.begin(); idx_layer < range.end(); ++ idx_layer) {
-            const Layer *layer = ctxt.layers[idx_layer];
-
-            if (is_selected_separate_extruder) {
-                bool at_least_one_has_correct_extruder = false;
-                for (const LayerRegion* layerm : layer->regions()) {
-                    if (layerm->slices.surfaces.empty())
-                        continue;
-                    const PrintRegionConfig& cfg = layerm->region().config();
-                    if (cfg.perimeter_extruder.value    == m_selected_extruder ||
-                        cfg.infill_extruder.value       == m_selected_extruder ||
-                        cfg.solid_infill_extruder.value == m_selected_extruder ) {
-                        at_least_one_has_correct_extruder = true;
-                        break;
-                    }
-                }
-                if (!at_least_one_has_correct_extruder)
-                    continue;
-            }
-
-#if ENABLE_LEGACY_OPENGL_REMOVAL
-            for (size_t i = 0; i < vols.size(); ++i) {
-                GLVolume* vol = vols[i];
-                if (vol->print_zs.empty() || vol->print_zs.back() != layer->print_z) {
-                    vol->print_zs.emplace_back(layer->print_z);
-                    vol->offsets.emplace_back(geometries[i].indices_count());
-                }
-            }
-#else
-            for (GLVolume* vol : vols)
-                if (vol->print_zs.empty() || vol->print_zs.back() != layer->print_z) {
-                    vol->print_zs.emplace_back(layer->print_z);
-                    vol->offsets.emplace_back(vol->indexed_vertex_array.quad_indices.size());
-                    vol->offsets.emplace_back(vol->indexed_vertex_array.triangle_indices.size());
-                }
-#endif // ENABLE_LEGACY_OPENGL_REMOVAL
-
-            for (const PrintInstance &instance : *ctxt.shifted_copies) {
-                const Point &copy = instance.shift;
-                for (const LayerRegion *layerm : layer->regions()) {
-                    if (is_selected_separate_extruder) {
-                        const PrintRegionConfig& cfg = layerm->region().config();
-                        if (cfg.perimeter_extruder.value    != m_selected_extruder ||
-                            cfg.infill_extruder.value       != m_selected_extruder ||
-                            cfg.solid_infill_extruder.value != m_selected_extruder)
-                            continue;
-                    }
-                    if (ctxt.has_perimeters)
-#if ENABLE_LEGACY_OPENGL_REMOVAL
-                        _3DScene::extrusionentity_to_verts(layerm->perimeters, float(layer->print_z), copy,
-                            select_geometry(idx_layer, layerm->region().config().perimeter_extruder.value, 0));
-#else
-                        _3DScene::extrusionentity_to_verts(layerm->perimeters, float(layer->print_z), copy,
-                        	volume(idx_layer, layerm->region().config().perimeter_extruder.value, 0));
-#endif // ENABLE_LEGACY_OPENGL_REMOVAL
-                    if (ctxt.has_infill) {
-                        for (const ExtrusionEntity *ee : layerm->fills.entities) {
-                            // fill represents infill extrusions of a single island.
-                            const auto *fill = dynamic_cast<const ExtrusionEntityCollection*>(ee);
-                            if (! fill->entities.empty())
-#if ENABLE_LEGACY_OPENGL_REMOVAL
-                                _3DScene::extrusionentity_to_verts(*fill, float(layer->print_z), copy,
-                                    select_geometry(idx_layer, is_solid_infill(fill->entities.front()->role()) ?
-                                                    layerm->region().config().solid_infill_extruder :
-                                                    layerm->region().config().infill_extruder, 1));
-#else
-                                _3DScene::extrusionentity_to_verts(*fill, float(layer->print_z), copy,
-	                                volume(idx_layer, 
-		                                is_solid_infill(fill->entities.front()->role()) ?
-			                                layerm->region().config().solid_infill_extruder :
-			                                layerm->region().config().infill_extruder,
-		                                1));
-#endif // ENABLE_LEGACY_OPENGL_REMOVAL
-                        }
-                    }
-                }
-                if (ctxt.has_support) {
-                    const SupportLayer *support_layer = dynamic_cast<const SupportLayer*>(layer);
-                    if (support_layer) {
-                        for (const ExtrusionEntity *extrusion_entity : support_layer->support_fills.entities)
-#if ENABLE_LEGACY_OPENGL_REMOVAL
-                            _3DScene::extrusionentity_to_verts(extrusion_entity, float(layer->print_z), copy,
-                                select_geometry(idx_layer, (extrusion_entity->role() == erSupportMaterial) ?
-                                                support_layer->object()->config().support_material_extruder :
-                                                support_layer->object()->config().support_material_interface_extruder, 2));
-#else
-                            _3DScene::extrusionentity_to_verts(extrusion_entity, float(layer->print_z), copy,
-	                            volume(idx_layer, 
-		                            (extrusion_entity->role() == erSupportMaterial) ?
-			                            support_layer->object()->config().support_material_extruder :
-			                            support_layer->object()->config().support_material_interface_extruder,
-		                            2));
-#endif // ENABLE_LEGACY_OPENGL_REMOVAL
-                    }
-                }
-            }
-            // Ensure that no volume grows over the limits. If the volume is too large, allocate a new one.
-	        for (size_t i = 0; i < vols.size(); ++i) {
-	            GLVolume &vol = *vols[i];
-#if ENABLE_LEGACY_OPENGL_REMOVAL
-                if (geometries[i].vertices_size_bytes() > MAX_VERTEX_BUFFER_SIZE) {
-                    vol.model.init_from(std::move(geometries[i]));
-#else
-                if (vol.indexed_vertex_array.vertices_and_normals_interleaved.size() > MAX_VERTEX_BUFFER_SIZE) {
-#endif // ENABLE_LEGACY_OPENGL_REMOVAL
-                    vols[i] = new_volume(vol.color);
-#if !ENABLE_LEGACY_OPENGL_REMOVAL
-                    reserve_new_volume_finalize_old_volume(*vols[i], vol, false);
-#endif // !ENABLE_LEGACY_OPENGL_REMOVAL
-                }
-	        }
-        }
-
-#if ENABLE_LEGACY_OPENGL_REMOVAL
-        for (size_t i = 0; i < vols.size(); ++i) {
-            if (!geometries[i].is_empty())
-                vols[i]->model.init_from(std::move(geometries[i]));
-        }
-#else
-        for (GLVolume *vol : vols)
-        	// Ideally one would call vol->indexed_vertex_array.finalize() here to move the buffers to the OpenGL driver,
-        	// but this code runs in parallel and the OpenGL driver is not thread safe.
-            vol->indexed_vertex_array.shrink_to_fit();
-#endif // ENABLE_LEGACY_OPENGL_REMOVAL
-    });
-
-    BOOST_LOG_TRIVIAL(debug) << "Loading print object toolpaths in parallel - finalizing results" << m_volumes.log_memory_info() << log_memory_info();
-    // Remove empty volumes from the newly added volumes.
-    {
-        for (auto ptr_it = m_volumes.volumes.begin() + volumes_cnt_initial; ptr_it != m_volumes.volumes.end(); ++ptr_it)
-            if ((*ptr_it)->empty()) {
-                delete *ptr_it;
-                *ptr_it = nullptr;
-            }
-        m_volumes.volumes.erase(std::remove(m_volumes.volumes.begin() + volumes_cnt_initial, m_volumes.volumes.end(), nullptr), m_volumes.volumes.end());
-    }
-    for (size_t i = volumes_cnt_initial; i < m_volumes.volumes.size(); ++i) {
-        GLVolume* v = m_volumes.volumes[i];
-#if ENABLE_LEGACY_OPENGL_REMOVAL
-        v->is_outside = !contains(build_volume, v->model);
-        // We are done editinig the model, now it can be sent to gpu
-        v->model.enable_render();
-#else
-        v->is_outside = ! build_volume.all_paths_inside_vertices_and_normals_interleaved(v->indexed_vertex_array.vertices_and_normals_interleaved, v->indexed_vertex_array.bounding_box());
-        v->indexed_vertex_array.finalize_geometry(m_initialized);
-#endif // ENABLE_LEGACY_OPENGL_REMOVAL
-    }
-
-    BOOST_LOG_TRIVIAL(debug) << "Loading print object toolpaths in parallel - end" << m_volumes.log_memory_info() << log_memory_info();
-}
-
-void GLCanvas3D::_load_wipe_tower_toolpaths(const BuildVolume& build_volume, const std::vector<std::string>& str_tool_colors)
-{
-    const Print *print = this->fff_print();
-    if (print == nullptr || print->wipe_tower_data().tool_changes.empty())
-        return;
-
-    if (!print->is_step_done(psWipeTower))
-        return;
-
-    std::vector<ColorRGBA> tool_colors;
-    decode_colors(str_tool_colors, tool_colors);
-
-    struct Ctxt
-    {
-        const Print                  *print;
-        const std::vector<ColorRGBA> *tool_colors;
-        Vec2f                         wipe_tower_pos;
-        float                         wipe_tower_angle;
-
-        static ColorRGBA color_support() { return ColorRGBA::GREENISH(); }
-
-        // For cloring by a tool, return a parsed color.
-        bool                         color_by_tool() const { return tool_colors != nullptr; }
-        size_t                       number_tools() const { return this->color_by_tool() ? tool_colors->size() : 0; }
-        const ColorRGBA&             color_tool(size_t tool) const { return (*tool_colors)[tool]; }
-        int                          volume_idx(int tool, int feature) const {
-            return this->color_by_tool() ? std::min<int>(this->number_tools() - 1, std::max<int>(tool, 0)) : feature;
-        }
-
-        const std::vector<WipeTower::ToolChangeResult>& tool_change(size_t idx) {
-            const auto &tool_changes = print->wipe_tower_data().tool_changes;
-            return priming.empty() ?
-                ((idx == tool_changes.size()) ? final : tool_changes[idx]) :
-                ((idx == 0) ? priming : (idx == tool_changes.size() + 1) ? final : tool_changes[idx - 1]);
-        }
-        std::vector<WipeTower::ToolChangeResult> priming;
-        std::vector<WipeTower::ToolChangeResult> final;
-    } ctxt;
-
-    ctxt.print = print;
-    ctxt.tool_colors = tool_colors.empty() ? nullptr : &tool_colors;
-    if (print->wipe_tower_data().priming && print->config().single_extruder_multi_material_priming)
-        for (int i=0; i<(int)print->wipe_tower_data().priming.get()->size(); ++i)
-            ctxt.priming.emplace_back(print->wipe_tower_data().priming.get()->at(i));
-    if (print->wipe_tower_data().final_purge)
-        ctxt.final.emplace_back(*print->wipe_tower_data().final_purge.get());
-
-    ctxt.wipe_tower_angle = ctxt.print->config().wipe_tower_rotation_angle.value/180.f * PI;
-    ctxt.wipe_tower_pos = Vec2f(ctxt.print->config().wipe_tower_x.value, ctxt.print->config().wipe_tower_y.value);
-
-    BOOST_LOG_TRIVIAL(debug) << "Loading wipe tower toolpaths in parallel - start" << m_volumes.log_memory_info() << log_memory_info();
-
-    //FIXME Improve the heuristics for a grain size.
-    size_t          n_items = print->wipe_tower_data().tool_changes.size() + (ctxt.priming.empty() ? 0 : 1);
-    size_t          grain_size = std::max(n_items / 128, size_t(1));
-    tbb::spin_mutex new_volume_mutex;
-    auto            new_volume = [this, &new_volume_mutex](const ColorRGBA& color) {
-        auto *volume = new GLVolume(color);
-		volume->is_extrusion_path = true;
-#if ENABLE_LEGACY_OPENGL_REMOVAL
-        // to prevent sending data to gpu (in the main thread) while
-        // editing the model geometry
-        volume->model.disable_render();
-#endif // ENABLE_LEGACY_OPENGL_REMOVAL
-        tbb::spin_mutex::scoped_lock lock;
-        lock.acquire(new_volume_mutex);
-        m_volumes.volumes.emplace_back(volume);
-        lock.release();
-        return volume;
-    };
-    const size_t   volumes_cnt_initial = m_volumes.volumes.size();
-    std::vector<GLVolumeCollection> volumes_per_thread(n_items);
-    tbb::parallel_for(
-        tbb::blocked_range<size_t>(0, n_items, grain_size),
-        [&ctxt, &new_volume](const tbb::blocked_range<size_t>& range) {
-        // Bounding box of this slab of a wipe tower.
-        GLVolumePtrs vols;
-#if ENABLE_LEGACY_OPENGL_REMOVAL
-        std::vector<GLModel::Geometry> geometries;
-#endif // ENABLE_LEGACY_OPENGL_REMOVAL
-        if (ctxt.color_by_tool()) {
-            for (size_t i = 0; i < ctxt.number_tools(); ++i) {
-                vols.emplace_back(new_volume(ctxt.color_tool(i)));
-#if ENABLE_LEGACY_OPENGL_REMOVAL
-                geometries.emplace_back(GLModel::Geometry());
-#endif // ENABLE_LEGACY_OPENGL_REMOVAL
-            }
-        }
-        else {
-            vols = { new_volume(ctxt.color_support()) };
-#if ENABLE_LEGACY_OPENGL_REMOVAL
-            geometries = { GLModel::Geometry() };
-#endif // ENABLE_LEGACY_OPENGL_REMOVAL
-        }
-
-#if ENABLE_LEGACY_OPENGL_REMOVAL
-        assert(vols.size() == geometries.size());
-        for (GLModel::Geometry& g : geometries) {
-            g.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3N3 };
-        }
-#else
-        for (GLVolume *volume : vols)
-			// Reserving number of vertices (3x position + 3x color)
-            volume->indexed_vertex_array.reserve(VERTEX_BUFFER_RESERVE_SIZE / 6);
-#endif // ENABLE_LEGACY_OPENGL_REMOVAL
-        for (size_t idx_layer = range.begin(); idx_layer < range.end(); ++idx_layer) {
-            const std::vector<WipeTower::ToolChangeResult> &layer = ctxt.tool_change(idx_layer);
-            for (size_t i = 0; i < vols.size(); ++i) {
-                GLVolume &vol = *vols[i];
-                if (vol.print_zs.empty() || vol.print_zs.back() != layer.front().print_z) {
-                    vol.print_zs.emplace_back(layer.front().print_z);
-#if ENABLE_LEGACY_OPENGL_REMOVAL
-                    vol.offsets.emplace_back(geometries[i].indices_count());
-#else
-                    vol.offsets.emplace_back(vol.indexed_vertex_array.quad_indices.size());
-                    vol.offsets.emplace_back(vol.indexed_vertex_array.triangle_indices.size());
-#endif // ENABLE_LEGACY_OPENGL_REMOVAL
-                }
-            }
-            for (const WipeTower::ToolChangeResult &extrusions : layer) {
-                for (size_t i = 1; i < extrusions.extrusions.size();) {
-                    const WipeTower::Extrusion &e = extrusions.extrusions[i];
-                    if (e.width == 0.) {
-                        ++i;
-                        continue;
-                    }
-                    size_t j = i + 1;
-                    if (ctxt.color_by_tool())
-                        for (; j < extrusions.extrusions.size() && extrusions.extrusions[j].tool == e.tool && extrusions.extrusions[j].width > 0.f; ++j);
-                    else
-                        for (; j < extrusions.extrusions.size() && extrusions.extrusions[j].width > 0.f; ++j);
-                    size_t              n_lines = j - i;
-                    Lines               lines;
-                    std::vector<double> widths;
-                    std::vector<double> heights;
-                    lines.reserve(n_lines);
-                    widths.reserve(n_lines);
-                    heights.assign(n_lines, extrusions.layer_height);
-                    WipeTower::Extrusion e_prev = extrusions.extrusions[i-1];
-
-                    if (!extrusions.priming) { // wipe tower extrusions describe the wipe tower at the origin with no rotation
-                        e_prev.pos = Eigen::Rotation2Df(ctxt.wipe_tower_angle) * e_prev.pos;
-                        e_prev.pos += ctxt.wipe_tower_pos;
-                    }
-
-                    for (; i < j; ++i) {
-                        WipeTower::Extrusion e = extrusions.extrusions[i];
-                        assert(e.width > 0.f);
-                        if (!extrusions.priming) {
-                            e.pos = Eigen::Rotation2Df(ctxt.wipe_tower_angle) * e.pos;
-                            e.pos += ctxt.wipe_tower_pos;
-                        }
-
-                        lines.emplace_back(Point::new_scale(e_prev.pos.x(), e_prev.pos.y()), Point::new_scale(e.pos.x(), e.pos.y()));
-                        widths.emplace_back(e.width);
-
-                        e_prev = e;
-                    }
-
-#if ENABLE_LEGACY_OPENGL_REMOVAL
-                    _3DScene::thick_lines_to_verts(lines, widths, heights, lines.front().a == lines.back().b, extrusions.print_z,
-                        geometries[ctxt.volume_idx(e.tool, 0)]);
-#else
-                    _3DScene::thick_lines_to_verts(lines, widths, heights, lines.front().a == lines.back().b, extrusions.print_z,
-                        *vols[ctxt.volume_idx(e.tool, 0)]);
-#endif // ENABLE_LEGACY_OPENGL_REMOVAL
-                }
-            }
-        }
-        for (size_t i = 0; i < vols.size(); ++i) {
-            GLVolume &vol = *vols[i];
-#if ENABLE_LEGACY_OPENGL_REMOVAL
-            if (geometries[i].vertices_size_bytes() > MAX_VERTEX_BUFFER_SIZE) {
-                vol.model.init_from(std::move(geometries[i]));
-#else
-            if (vol.indexed_vertex_array.vertices_and_normals_interleaved.size() > MAX_VERTEX_BUFFER_SIZE) {
-#endif // ENABLE_LEGACY_OPENGL_REMOVAL
-                vols[i] = new_volume(vol.color);
-#if !ENABLE_LEGACY_OPENGL_REMOVAL
-                reserve_new_volume_finalize_old_volume(*vols[i], vol, false);
-#endif // !ENABLE_LEGACY_OPENGL_REMOVAL
-            }
-        }
-
-#if ENABLE_LEGACY_OPENGL_REMOVAL
-        for (size_t i = 0; i < vols.size(); ++i) {
-            if (!geometries[i].is_empty())
-                vols[i]->model.init_from(std::move(geometries[i]));
-        }
-#else
-        for (GLVolume *vol : vols)
-            vol->indexed_vertex_array.shrink_to_fit();
-#endif // ENABLE_LEGACY_OPENGL_REMOVAL
-        });
-
-    BOOST_LOG_TRIVIAL(debug) << "Loading wipe tower toolpaths in parallel - finalizing results" << m_volumes.log_memory_info() << log_memory_info();
-    // Remove empty volumes from the newly added volumes.
-    {
-        for (auto ptr_it = m_volumes.volumes.begin() + volumes_cnt_initial; ptr_it != m_volumes.volumes.end(); ++ptr_it)
-            if ((*ptr_it)->empty()) {
-                delete *ptr_it;
-                *ptr_it = nullptr;
-            }
-        m_volumes.volumes.erase(std::remove(m_volumes.volumes.begin() + volumes_cnt_initial, m_volumes.volumes.end(), nullptr), m_volumes.volumes.end());
-    }
-    for (size_t i = volumes_cnt_initial; i < m_volumes.volumes.size(); ++i) {
-        GLVolume* v = m_volumes.volumes[i];
-#if ENABLE_LEGACY_OPENGL_REMOVAL
-        v->is_outside = !contains(build_volume, v->model);
-        // We are done editinig the model, now it can be sent to gpu
-        v->model.enable_render();
-#else
-        v->is_outside = ! build_volume.all_paths_inside_vertices_and_normals_interleaved(v->indexed_vertex_array.vertices_and_normals_interleaved, v->indexed_vertex_array.bounding_box());
-        v->indexed_vertex_array.finalize_geometry(m_initialized);
-#endif // ENABLE_LEGACY_OPENGL_REMOVAL
-    }
-
-    BOOST_LOG_TRIVIAL(debug) << "Loading wipe tower toolpaths in parallel - end" << m_volumes.log_memory_info() << log_memory_info();
-}
-
-// While it looks like we can call 
-// this->reload_scene(true, true)
-// the two functions are quite different:
-// 1) This function only loads objects, for which the step slaposSliceSupports already finished. Therefore objects outside of the print bed never load.
-// 2) This function loads object mesh with the relative scaling correction (the "relative_correction" parameter) was applied,
-// 	  therefore the mesh may be slightly larger or smaller than the mesh shown in the 3D scene.
-void GLCanvas3D::_load_sla_shells()
-{
-    const SLAPrint* print = this->sla_print();
-    if (print->objects().empty())
-        // nothing to render, return
-        return;
-
-    auto add_volume = [this](const SLAPrintObject &object, int volume_id, const SLAPrintObject::Instance& instance,
-        const TriangleMesh& mesh, const ColorRGBA& color, bool outside_printer_detection_enabled) {
-        m_volumes.volumes.emplace_back(new GLVolume(color));
-        GLVolume& v = *m_volumes.volumes.back();
-#if ENABLE_SMOOTH_NORMALS
-#if ENABLE_LEGACY_OPENGL_REMOVAL
-        v.model.init_from(mesh, true);
-#else
-        v.indexed_vertex_array.load_mesh(mesh, true);
-#endif // ENABLE_LEGACY_OPENGL_REMOVAL
-#else
-#if ENABLE_LEGACY_OPENGL_REMOVAL
-        v.model.init_from(mesh);
-#else
-        v.indexed_vertex_array.load_mesh(mesh);
-#endif // ENABLE_LEGACY_OPENGL_REMOVAL
-#endif // ENABLE_SMOOTH_NORMALS
-#if !ENABLE_LEGACY_OPENGL_REMOVAL
-        v.indexed_vertex_array.finalize_geometry(m_initialized);
-#endif // !ENABLE_LEGACY_OPENGL_REMOVAL
-        v.shader_outside_printer_detection_enabled = outside_printer_detection_enabled;
-        v.composite_id.volume_id = volume_id;
-        v.set_instance_offset(unscale(instance.shift.x(), instance.shift.y(), 0.0));
-        v.set_instance_rotation({ 0.0, 0.0, (double)instance.rotation });
-        v.set_instance_mirror(X, object.is_left_handed() ? -1. : 1.);
-        v.set_convex_hull(mesh.convex_hull_3d());
-    };
-
-    // adds objects' volumes 
-    for (const SLAPrintObject* obj : print->objects())
-        if (obj->is_step_done(slaposSliceSupports)) {
-            unsigned int initial_volumes_count = (unsigned int)m_volumes.volumes.size();
-            for (const SLAPrintObject::Instance& instance : obj->instances()) {
-                add_volume(*obj, 0, instance, obj->get_mesh_to_print(), GLVolume::MODEL_COLOR[0], true);
-                // Set the extruder_id and volume_id to achieve the same color as in the 3D scene when
-                // through the update_volumes_colors_by_extruder() call.
-                m_volumes.volumes.back()->extruder_id = obj->model_object()->volumes.front()->extruder_id();
-                if (obj->is_step_done(slaposSupportTree) && obj->has_mesh(slaposSupportTree))
-                    add_volume(*obj, -int(slaposSupportTree), instance, obj->support_mesh(), GLVolume::SLA_SUPPORT_COLOR, true);
-                if (obj->is_step_done(slaposPad) && obj->has_mesh(slaposPad))
-                    add_volume(*obj, -int(slaposPad), instance, obj->pad_mesh(), GLVolume::SLA_PAD_COLOR, false);
-            }
-            double shift_z = obj->get_current_elevation();
-            for (unsigned int i = initial_volumes_count; i < m_volumes.volumes.size(); ++ i) {
-                // apply shift z
-                m_volumes.volumes[i]->set_sla_shift_z(shift_z);
-            }
-        }
-
-    update_volumes_colors_by_extruder();
-}
-
-void GLCanvas3D::_update_sla_shells_outside_state()
-{
-    check_volumes_outside_state();
-}
-
-void GLCanvas3D::_set_warning_notification_if_needed(EWarning warning)
-{
-    _set_current();
-    bool show = false;
-    if (!m_volumes.empty())
-        show = _is_any_volume_outside();
-    else {
-        if (wxGetApp().is_editor()) {
-            if (current_printer_technology() != ptSLA)
-                show = m_gcode_viewer.has_data() && !m_gcode_viewer.is_contained_in_bed();
-        }
-    }
-
-    _set_warning_notification(warning, show);
-}
-
-void GLCanvas3D::_set_warning_notification(EWarning warning, bool state)
-{
-    enum ErrorType{
-        PLATER_WARNING,
-        PLATER_ERROR,
-        SLICING_ERROR
-    };
-    std::string text;
-    ErrorType error = ErrorType::PLATER_WARNING;
-    switch (warning) {
-    case EWarning::ObjectOutside:      text = _u8L("An object outside the print area was detected."); break;
-    case EWarning::ToolpathOutside:    text = _u8L("A toolpath outside the print area was detected."); error = ErrorType::SLICING_ERROR; break;
-    case EWarning::SlaSupportsOutside: text = _u8L("SLA supports outside the print area were detected."); error = ErrorType::PLATER_ERROR; break;
-    case EWarning::SomethingNotShown:  text = _u8L("Some objects are not visible during editing."); break;
-    case EWarning::ObjectClashed:
-        text = _u8L("An object outside the print area was detected.\n"
-            "Resolve the current problem to continue slicing.");
-        error = ErrorType::PLATER_ERROR;
-        break;
-    }
-    auto& notification_manager = *wxGetApp().plater()->get_notification_manager();
-    switch (error)
-    {
-    case PLATER_WARNING:
-        if (state)
-            notification_manager.push_plater_warning_notification(text);
-        else
-            notification_manager.close_plater_warning_notification(text);
-        break;
-    case PLATER_ERROR:
-        if (state)
-            notification_manager.push_plater_error_notification(text);
-        else
-            notification_manager.close_plater_error_notification(text);
-        break;
-    case SLICING_ERROR:
-        if (state)
-            notification_manager.push_slicing_error_notification(text);
-        else
-            notification_manager.close_slicing_error_notification(text);
-        break;
-    default:
-        break;
-    }
-}
-
-bool GLCanvas3D::_is_any_volume_outside() const
-{
-    for (const GLVolume* volume : m_volumes.volumes) {
-        if (volume != nullptr && volume->is_outside)
-            return true;
-    }
-
-    return false;
-}
-
-void GLCanvas3D::_update_selection_from_hover()
-{
-    bool ctrl_pressed = wxGetKeyState(WXK_CONTROL);
-
-    if (m_hover_volume_idxs.empty()) {
-        if (!ctrl_pressed && m_rectangle_selection.get_state() == GLSelectionRectangle::EState::Select)
-            m_selection.remove_all();
-
-        return;
-    }
-
-    GLSelectionRectangle::EState state = m_rectangle_selection.get_state();
-
-    bool hover_modifiers_only = true;
-    for (int i : m_hover_volume_idxs) {
-        if (!m_volumes.volumes[i]->is_modifier) {
-            hover_modifiers_only = false;
-            break;
-        }
-    }
-
-    bool selection_changed = false;
-#if ENABLE_NEW_RECTANGLE_SELECTION
-    if (!m_rectangle_selection.is_empty()) {
-#endif // ENABLE_NEW_RECTANGLE_SELECTION
-    if (state == GLSelectionRectangle::EState::Select) {
-        bool contains_all = true;
-        for (int i : m_hover_volume_idxs) {
-            if (!m_selection.contains_volume((unsigned int)i)) {
-                contains_all = false;
-                break;
-            }
-        }
-
-        // the selection is going to be modified (Add)
-        if (!contains_all) {
-            wxGetApp().plater()->take_snapshot(_L("Selection-Add from rectangle"), UndoRedo::SnapshotType::Selection);
-            selection_changed = true;
-        }
-    }
-    else {
-        bool contains_any = false;
-        for (int i : m_hover_volume_idxs) {
-            if (m_selection.contains_volume((unsigned int)i)) {
-                contains_any = true;
-                break;
-            }
-        }
-
-        // the selection is going to be modified (Remove)
-        if (contains_any) {
-            wxGetApp().plater()->take_snapshot(_L("Selection-Remove from rectangle"), UndoRedo::SnapshotType::Selection);
-            selection_changed = true;
-        }
-    }
-#if ENABLE_NEW_RECTANGLE_SELECTION
-    }
-#endif // ENABLE_NEW_RECTANGLE_SELECTION
-
-    if (!selection_changed)
-        return;
-
-    Plater::SuppressSnapshots suppress(wxGetApp().plater());
-
-    if (state == GLSelectionRectangle::EState::Select && !ctrl_pressed)
-        m_selection.clear();
-
-    for (int i : m_hover_volume_idxs) {
-        if (state == GLSelectionRectangle::EState::Select) {
-            if (hover_modifiers_only) {
-                const GLVolume& v = *m_volumes.volumes[i];
-                m_selection.add_volume(v.object_idx(), v.volume_idx(), v.instance_idx(), false);
-            }
-            else
-                m_selection.add(i, false);
-        }
-        else
-            m_selection.remove(i);
-    }
-
-    if (m_selection.is_empty())
-        m_gizmos.reset_all_states();
-    else
-        m_gizmos.refresh_on_off_state();
-
-    m_gizmos.update_data();
-    post_event(SimpleEvent(EVT_GLCANVAS_OBJECT_SELECT));
-    m_dirty = true;
-}
-
-bool GLCanvas3D::_deactivate_undo_redo_toolbar_items()
-{
-    if (m_undoredo_toolbar.is_item_pressed("undo")) {
-        m_undoredo_toolbar.force_right_action(m_undoredo_toolbar.get_item_id("undo"), *this);
-        return true;
-    }
-    else if (m_undoredo_toolbar.is_item_pressed("redo")) {
-        m_undoredo_toolbar.force_right_action(m_undoredo_toolbar.get_item_id("redo"), *this);
-        return true;
-    }
-
-    return false;
-}
-
-bool GLCanvas3D::is_search_pressed() const
-{
-    return m_main_toolbar.is_item_pressed("search");
-}
-
-bool GLCanvas3D::_deactivate_arrange_menu()
-{
-    if (m_main_toolbar.is_item_pressed("arrange")) {
-        m_main_toolbar.force_right_action(m_main_toolbar.get_item_id("arrange"), *this);
-        return true;
-    }
-
-    return false;
-}
-
-bool GLCanvas3D::_deactivate_search_toolbar_item()
-{
-    if (is_search_pressed()) {
-        m_main_toolbar.force_left_action(m_main_toolbar.get_item_id("search"), *this);
-        return true;
-    }
-
-    return false;
-}
-
-bool GLCanvas3D::_activate_search_toolbar_item()
-{
-    if (!m_main_toolbar.is_item_pressed("search")) {
-        m_main_toolbar.force_left_action(m_main_toolbar.get_item_id("search"), *this);
-        return true;
-    }
-
-    return false;
-}
-
-bool GLCanvas3D::_deactivate_collapse_toolbar_items()
-{
-    GLToolbar& collapse_toolbar = wxGetApp().plater()->get_collapse_toolbar();
-    if (collapse_toolbar.is_item_pressed("print")) {
-        collapse_toolbar.force_left_action(collapse_toolbar.get_item_id("print"), *this);
-        return true;
-    }
-
-    return false;
-}
-
-void GLCanvas3D::highlight_toolbar_item(const std::string& item_name)
-{
-    GLToolbarItem* item = m_main_toolbar.get_item(item_name);
-    if (!item)
-        item = m_undoredo_toolbar.get_item(item_name);
-    if (!item || !item->is_visible())
-        return;
-    m_toolbar_highlighter.init(item, this);
-}
-
-void GLCanvas3D::highlight_gizmo(const std::string& gizmo_name)
-{
-    GLGizmosManager::EType gizmo = m_gizmos.get_gizmo_from_name(gizmo_name);
-    if(gizmo == GLGizmosManager::EType::Undefined)
-        return;
-    m_gizmo_highlighter.init(&m_gizmos, gizmo, this);
-}
-
-const Print* GLCanvas3D::fff_print() const
-{
-    return (m_process == nullptr) ? nullptr : m_process->fff_print();
-}
-
-const SLAPrint* GLCanvas3D::sla_print() const
-{
-    return (m_process == nullptr) ? nullptr : m_process->sla_print();
-}
-
-void GLCanvas3D::WipeTowerInfo::apply_wipe_tower() const
-{
-    DynamicPrintConfig cfg;
-    cfg.opt<ConfigOptionFloat>("wipe_tower_x", true)->value = m_pos(X);
-    cfg.opt<ConfigOptionFloat>("wipe_tower_y", true)->value = m_pos(Y);
-    cfg.opt<ConfigOptionFloat>("wipe_tower_rotation_angle", true)->value = (180./M_PI) * m_rotation;
-    wxGetApp().get_tab(Preset::TYPE_PRINT)->load_config(cfg);
-}
-
-void GLCanvas3D::RenderTimer::Notify()
-{
-    wxPostEvent((wxEvtHandler*)GetOwner(), RenderTimerEvent( EVT_GLCANVAS_RENDER_TIMER, *this));
-}
-
-void GLCanvas3D::ToolbarHighlighterTimer::Notify()
-{
-    wxPostEvent((wxEvtHandler*)GetOwner(), ToolbarHighlighterTimerEvent(EVT_GLCANVAS_TOOLBAR_HIGHLIGHTER_TIMER, *this));
-}
-
-void GLCanvas3D::GizmoHighlighterTimer::Notify()
-{
-    wxPostEvent((wxEvtHandler*)GetOwner(), GizmoHighlighterTimerEvent(EVT_GLCANVAS_GIZMO_HIGHLIGHTER_TIMER, *this));
-}
-
-void GLCanvas3D::ToolbarHighlighter::set_timer_owner(wxEvtHandler* owner, int timerid/* = wxID_ANY*/)
-{
-    m_timer.SetOwner(owner, timerid);
-}
-
-void GLCanvas3D::ToolbarHighlighter::init(GLToolbarItem* toolbar_item, GLCanvas3D* canvas)
-{
-    if (m_timer.IsRunning())
-        invalidate();
-    if (!toolbar_item || !canvas)
-        return;
-
-    m_timer.Start(300, false);
-
-    m_toolbar_item = toolbar_item;
-    m_canvas       = canvas;
-}
-
-void GLCanvas3D::ToolbarHighlighter::invalidate()
-{
-    m_timer.Stop();
-
-    if (m_toolbar_item) {
-        m_toolbar_item->set_highlight(GLToolbarItem::EHighlightState::NotHighlighted);
-    }
-    m_toolbar_item = nullptr;
-    m_blink_counter = 0;
-    m_render_arrow = false;
-}
-
-void GLCanvas3D::ToolbarHighlighter::blink()
-{
-    if (m_toolbar_item) {
-        char state = m_toolbar_item->get_highlight();
-        if (state != (char)GLToolbarItem::EHighlightState::HighlightedShown)
-            m_toolbar_item->set_highlight(GLToolbarItem::EHighlightState::HighlightedShown);
-        else 
-            m_toolbar_item->set_highlight(GLToolbarItem::EHighlightState::HighlightedHidden);
-
-        m_render_arrow = !m_render_arrow;
-        m_canvas->set_as_dirty();
-    }
-    else
-        invalidate();
-
-    if ((++m_blink_counter) >= 11)
-        invalidate();
-}
-
-void GLCanvas3D::GizmoHighlighter::set_timer_owner(wxEvtHandler* owner, int timerid/* = wxID_ANY*/)
-{
-    m_timer.SetOwner(owner, timerid);
-}
-
-void GLCanvas3D::GizmoHighlighter::init(GLGizmosManager* manager, GLGizmosManager::EType gizmo, GLCanvas3D* canvas)
-{
-    if (m_timer.IsRunning())
-        invalidate();
-    if (!gizmo || !canvas)
-        return;
-
-    m_timer.Start(300, false);
-
-    m_gizmo_manager = manager;
-    m_gizmo_type    = gizmo;
-    m_canvas        = canvas;
-}
-
-void GLCanvas3D::GizmoHighlighter::invalidate()
-{
-    m_timer.Stop();
-
-    if (m_gizmo_manager) {
-        m_gizmo_manager->set_highlight(GLGizmosManager::EType::Undefined, false);
-    }
-    m_gizmo_manager = nullptr;
-    m_gizmo_type = GLGizmosManager::EType::Undefined;
-    m_blink_counter = 0;
-    m_render_arrow = false;
-}
-
-void GLCanvas3D::GizmoHighlighter::blink()
-{
-    if (m_gizmo_manager) {
-        if (m_blink_counter % 2 == 0)
-            m_gizmo_manager->set_highlight(m_gizmo_type, true);
-        else
-            m_gizmo_manager->set_highlight(m_gizmo_type, false);
-
-        m_render_arrow = !m_render_arrow;
-        m_canvas->set_as_dirty();
-    }
-    else
-        invalidate();
-
-    if ((++m_blink_counter) >= 11)
-        invalidate();
-}
-
-} // namespace GUI
-} // namespace Slic3r
+#include "libslic3r/libslic3r.h"
+#include "GLCanvas3D.hpp"
+
+#include <igl/unproject.h>
+
+#include "libslic3r/BuildVolume.hpp"
+#include "libslic3r/ClipperUtils.hpp"
+#include "libslic3r/PrintConfig.hpp"
+#include "libslic3r/GCode/ThumbnailData.hpp"
+#include "libslic3r/Geometry/ConvexHull.hpp"
+#include "libslic3r/ExtrusionEntity.hpp"
+#include "libslic3r/Layer.hpp"
+#include "libslic3r/Utils.hpp"
+#include "libslic3r/Technologies.hpp"
+#include "libslic3r/Tesselate.hpp"
+#include "libslic3r/PresetBundle.hpp"
+#include "3DBed.hpp"
+#include "3DScene.hpp"
+#include "BackgroundSlicingProcess.hpp"
+#include "GLShader.hpp"
+#include "GUI.hpp"
+#include "Tab.hpp"
+#include "GUI_Preview.hpp"
+#include "OpenGLManager.hpp"
+#include "Plater.hpp"
+#include "MainFrame.hpp"
+#include "GUI_App.hpp"
+#include "GUI_ObjectList.hpp"
+#include "GUI_ObjectManipulation.hpp"
+#include "Mouse3DController.hpp"
+#include "I18N.hpp"
+#include "NotificationManager.hpp"
+#include "format.hpp"
+
+#include "slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp"
+#include "slic3r/Utils/UndoRedo.hpp"
+
+#if ENABLE_RETINA_GL
+#include "slic3r/Utils/RetinaHelper.hpp"
+#endif
+
+#include <GL/glew.h>
+
+#include <wx/glcanvas.h>
+#include <wx/bitmap.h>
+#include <wx/dcmemory.h>
+#include <wx/image.h>
+#include <wx/settings.h>
+#include <wx/tooltip.h>
+#include <wx/debug.h>
+#include <wx/fontutil.h>
+
+// Print now includes tbb, and tbb includes Windows. This breaks compilation of wxWidgets if included before wx.
+#include "libslic3r/Print.hpp"
+#include "libslic3r/SLAPrint.hpp"
+
+#include "wxExtensions.hpp"
+
+#include <tbb/parallel_for.h>
+#include <tbb/spin_mutex.h>
+
+#include <boost/log/trivial.hpp>
+#include <boost/algorithm/string/predicate.hpp>
+
+#include <iostream>
+#include <float.h>
+#include <algorithm>
+#include <cmath>
+#include "DoubleSlider.hpp"
+
+#include <imgui/imgui_internal.h>
+
+static constexpr const float TRACKBALLSIZE = 0.8f;
+
+#if ENABLE_LEGACY_OPENGL_REMOVAL
+static const Slic3r::ColorRGBA DEFAULT_BG_DARK_COLOR  = { 0.478f, 0.478f, 0.478f, 1.0f };
+static const Slic3r::ColorRGBA DEFAULT_BG_LIGHT_COLOR = { 0.753f, 0.753f, 0.753f, 1.0f };
+static const Slic3r::ColorRGBA ERROR_BG_DARK_COLOR    = { 0.478f, 0.192f, 0.039f, 1.0f };
+static const Slic3r::ColorRGBA ERROR_BG_LIGHT_COLOR   = { 0.753f, 0.192f, 0.039f, 1.0f };
+#else
+static const Slic3r::ColorRGB DEFAULT_BG_DARK_COLOR  = { 0.478f, 0.478f, 0.478f };
+static const Slic3r::ColorRGB DEFAULT_BG_LIGHT_COLOR = { 0.753f, 0.753f, 0.753f };
+static const Slic3r::ColorRGB ERROR_BG_DARK_COLOR    = { 0.478f, 0.192f, 0.039f };
+static const Slic3r::ColorRGB ERROR_BG_LIGHT_COLOR   = { 0.753f, 0.192f, 0.039f };
+#endif // ENABLE_LEGACY_OPENGL_REMOVAL
+
+// Number of floats
+static constexpr const size_t MAX_VERTEX_BUFFER_SIZE     = 131072 * 6; // 3.15MB
+// Reserve size in number of floats.
+#if !ENABLE_LEGACY_OPENGL_REMOVAL
+static constexpr const size_t VERTEX_BUFFER_RESERVE_SIZE = 131072 * 2; // 1.05MB
+// Reserve size in number of floats, maximum sum of all preallocated buffers.
+//static constexpr const size_t VERTEX_BUFFER_RESERVE_SIZE_SUM_MAX = 1024 * 1024 * 128 / 4; // 128MB
+#endif // !ENABLE_LEGACY_OPENGL_REMOVAL
+
+namespace Slic3r {
+namespace GUI {
+
+#ifdef __WXGTK3__
+// wxGTK3 seems to simulate OSX behavior in regard to HiDPI scaling support.
+RetinaHelper::RetinaHelper(wxWindow* window) : m_window(window), m_self(nullptr) {}
+RetinaHelper::~RetinaHelper() {}
+float RetinaHelper::get_scale_factor() { return float(m_window->GetContentScaleFactor()); }
+#endif // __WXGTK3__
+
+// Fixed the collision between BuildVolume::Type::Convex and macro Convex defined inside /usr/include/X11/X.h that is included by WxWidgets 3.0.
+#if defined(__linux__) && defined(Convex)
+#undef Convex
+#endif
+
+GLCanvas3D::LayersEditing::~LayersEditing()
+{
+    if (m_z_texture_id != 0) {
+        glsafe(::glDeleteTextures(1, &m_z_texture_id));
+        m_z_texture_id = 0;
+    }
+    delete m_slicing_parameters;
+}
+
+const float GLCanvas3D::LayersEditing::THICKNESS_BAR_WIDTH = 70.0f;
+
+void GLCanvas3D::LayersEditing::init()
+{
+    glsafe(::glGenTextures(1, (GLuint*)&m_z_texture_id));
+    glsafe(::glBindTexture(GL_TEXTURE_2D, m_z_texture_id));
+    glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP));
+    glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP));
+    glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR));
+    glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_NEAREST));
+    glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 1));
+    glsafe(::glBindTexture(GL_TEXTURE_2D, 0));
+}
+
+void GLCanvas3D::LayersEditing::set_config(const DynamicPrintConfig* config)
+{
+    m_config = config;
+    delete m_slicing_parameters;
+    m_slicing_parameters = nullptr;
+    m_layers_texture.valid = false;
+}
+
+void GLCanvas3D::LayersEditing::select_object(const Model &model, int object_id)
+{
+    const ModelObject *model_object_new = (object_id >= 0) ? model.objects[object_id] : nullptr;
+    // Maximum height of an object changes when the object gets rotated or scaled.
+    // Changing maximum height of an object will invalidate the layer heigth editing profile.
+    // m_model_object->bounding_box() is cached, therefore it is cheap even if this method is called frequently.
+    const float new_max_z = (model_object_new == nullptr) ? 0.0f : static_cast<float>(model_object_new->bounding_box().max.z());
+    if (m_model_object != model_object_new || this->last_object_id != object_id || m_object_max_z != new_max_z ||
+        (model_object_new != nullptr && m_model_object->id() != model_object_new->id())) {
+        m_layer_height_profile.clear();
+        m_layer_height_profile_modified = false;
+        delete m_slicing_parameters;
+        m_slicing_parameters   = nullptr;
+        m_layers_texture.valid = false;
+        this->last_object_id   = object_id;
+        m_model_object         = model_object_new;
+        m_object_max_z         = new_max_z;
+    }
+}
+
+bool GLCanvas3D::LayersEditing::is_allowed() const
+{
+    return wxGetApp().get_shader("variable_layer_height") != nullptr && m_z_texture_id > 0;
+}
+
+bool GLCanvas3D::LayersEditing::is_enabled() const
+{
+    return m_enabled;
+}
+
+void GLCanvas3D::LayersEditing::set_enabled(bool enabled)
+{
+    m_enabled = is_allowed() && enabled;
+}
+
+float GLCanvas3D::LayersEditing::s_overlay_window_width;
+
+void GLCanvas3D::LayersEditing::render_overlay(const GLCanvas3D& canvas)
+{
+    if (!m_enabled)
+        return;
+
+    const Size& cnv_size = canvas.get_canvas_size();
+
+    ImGuiWrapper& imgui = *wxGetApp().imgui();
+    imgui.set_next_window_pos(static_cast<float>(cnv_size.get_width()) - imgui.get_style_scaling() * THICKNESS_BAR_WIDTH, 
+        static_cast<float>(cnv_size.get_height()), ImGuiCond_Always, 1.0f, 1.0f);
+
+    imgui.begin(_L("Variable layer height"), ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse);
+
+    imgui.text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, _L("Left mouse button:"));
+    ImGui::SameLine();
+    imgui.text(_L("Add detail"));
+
+    imgui.text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, _L("Right mouse button:"));
+    ImGui::SameLine();
+    imgui.text(_L("Remove detail"));
+
+    imgui.text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, _L("Shift + Left mouse button:"));
+    ImGui::SameLine();
+    imgui.text(_L("Reset to base"));
+
+    imgui.text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, _L("Shift + Right mouse button:"));
+    ImGui::SameLine();
+    imgui.text(_L("Smoothing"));
+
+    imgui.text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, _L("Mouse wheel:"));
+    ImGui::SameLine();
+    imgui.text(_L("Increase/decrease edit area"));
+    
+    ImGui::Separator();
+    if (imgui.button(_L("Adaptive")))
+        wxPostEvent((wxEvtHandler*)canvas.get_wxglcanvas(), Event<float>(EVT_GLCANVAS_ADAPTIVE_LAYER_HEIGHT_PROFILE, m_adaptive_quality));
+
+    ImGui::SameLine();
+    float text_align = ImGui::GetCursorPosX();
+    ImGui::AlignTextToFramePadding();
+    imgui.text(_L("Quality / Speed"));
+    if (ImGui::IsItemHovered()) {
+        ImGui::BeginTooltip();
+        ImGui::TextUnformatted(_L("Higher print quality versus higher print speed.").ToUTF8());
+        ImGui::EndTooltip();
+    }
+
+    ImGui::SameLine();
+    float widget_align = ImGui::GetCursorPosX();
+    ImGui::PushItemWidth(imgui.get_style_scaling() * 120.0f);
+    m_adaptive_quality = std::clamp(m_adaptive_quality, 0.0f, 1.f);
+    imgui.slider_float("", &m_adaptive_quality, 0.0f, 1.f, "%.2f");
+
+    ImGui::Separator();
+    if (imgui.button(_L("Smooth")))
+        wxPostEvent((wxEvtHandler*)canvas.get_wxglcanvas(), HeightProfileSmoothEvent(EVT_GLCANVAS_SMOOTH_LAYER_HEIGHT_PROFILE, m_smooth_params));
+
+    ImGui::SameLine();
+    ImGui::SetCursorPosX(text_align);
+    ImGui::AlignTextToFramePadding();
+    imgui.text(_L("Radius"));
+    ImGui::SameLine();
+    ImGui::SetCursorPosX(widget_align);
+    ImGui::PushItemWidth(imgui.get_style_scaling() * 120.0f);
+    int radius = (int)m_smooth_params.radius;
+    if (ImGui::SliderInt("##1", &radius, 1, 10)) {
+        radius = std::clamp(radius, 1, 10);
+        m_smooth_params.radius = (unsigned int)radius;
+    }
+
+    ImGui::SetCursorPosX(text_align);
+    ImGui::AlignTextToFramePadding();
+    imgui.text(_L("Keep min"));
+    ImGui::SameLine();
+    if (ImGui::GetCursorPosX() < widget_align)  // because of line lenght after localization
+        ImGui::SetCursorPosX(widget_align);
+
+    ImGui::PushItemWidth(imgui.get_style_scaling() * 120.0f);
+    imgui.checkbox("##2", m_smooth_params.keep_min);
+
+    ImGui::Separator();
+    if (imgui.button(_L("Reset")))
+        wxPostEvent((wxEvtHandler*)canvas.get_wxglcanvas(), SimpleEvent(EVT_GLCANVAS_RESET_LAYER_HEIGHT_PROFILE));
+
+    GLCanvas3D::LayersEditing::s_overlay_window_width = ImGui::GetWindowSize().x /*+ (float)m_layers_texture.width/4*/;
+    imgui.end();
+
+#if ENABLE_GL_SHADERS_ATTRIBUTES
+    render_active_object_annotations(canvas);
+    render_profile(canvas);
+#else
+    const Rect& bar_rect = get_bar_rect_viewport(canvas);
+#if ENABLE_LEGACY_OPENGL_REMOVAL
+    m_profile.dirty = m_profile.old_bar_rect != bar_rect;
+#endif // ENABLE_LEGACY_OPENGL_REMOVAL
+    render_active_object_annotations(canvas, bar_rect);
+    render_profile(bar_rect);
+#if ENABLE_LEGACY_OPENGL_REMOVAL
+    m_profile.old_bar_rect = bar_rect;
+    m_profile.dirty = false;
+#endif // ENABLE_LEGACY_OPENGL_REMOVAL
+#endif // ENABLE_GL_SHADERS_ATTRIBUTES
+}
+
+float GLCanvas3D::LayersEditing::get_cursor_z_relative(const GLCanvas3D& canvas)
+{
+    const Vec2d mouse_pos = canvas.get_local_mouse_position();
+    const Rect& rect = get_bar_rect_screen(canvas);
+    float x = (float)mouse_pos.x();
+    float y = (float)mouse_pos.y();
+    float t = rect.get_top();
+    float b = rect.get_bottom();
+
+    return (rect.get_left() <= x && x <= rect.get_right() && t <= y && y <= b) ?
+        // Inside the bar.
+        (b - y - 1.0f) / (b - t - 1.0f) :
+        // Outside the bar.
+        -1000.0f;
+}
+
+bool GLCanvas3D::LayersEditing::bar_rect_contains(const GLCanvas3D& canvas, float x, float y)
+{
+    const Rect& rect = get_bar_rect_screen(canvas);
+    return rect.get_left() <= x && x <= rect.get_right() && rect.get_top() <= y && y <= rect.get_bottom();
+}
+
+Rect GLCanvas3D::LayersEditing::get_bar_rect_screen(const GLCanvas3D& canvas)
+{
+    const Size& cnv_size = canvas.get_canvas_size();
+    float w = (float)cnv_size.get_width();
+    float h = (float)cnv_size.get_height();
+
+    return { w - thickness_bar_width(canvas), 0.0f, w, h };
+}
+
+#if !ENABLE_GL_SHADERS_ATTRIBUTES
+Rect GLCanvas3D::LayersEditing::get_bar_rect_viewport(const GLCanvas3D& canvas)
+{
+    const Size& cnv_size = canvas.get_canvas_size();
+    float half_w = 0.5f * (float)cnv_size.get_width();
+    float half_h = 0.5f * (float)cnv_size.get_height();
+    float inv_zoom = (float)wxGetApp().plater()->get_camera().get_inv_zoom();
+    return { (half_w - thickness_bar_width(canvas)) * inv_zoom, half_h * inv_zoom, half_w * inv_zoom, -half_h * inv_zoom };
+}
+#endif // !ENABLE_GL_SHADERS_ATTRIBUTES
+
+bool GLCanvas3D::LayersEditing::is_initialized() const
+{
+    return wxGetApp().get_shader("variable_layer_height") != nullptr;
+}
+
+std::string GLCanvas3D::LayersEditing::get_tooltip(const GLCanvas3D& canvas) const
+{
+    std::string ret;
+    if (m_enabled && m_layer_height_profile.size() >= 4) {
+        float z = get_cursor_z_relative(canvas);
+        if (z != -1000.0f) {
+            z *= m_object_max_z;
+
+            float h = 0.0f;
+            for (size_t i = m_layer_height_profile.size() - 2; i >= 2; i -= 2) {
+                const float zi = static_cast<float>(m_layer_height_profile[i]);
+                const float zi_1 = static_cast<float>(m_layer_height_profile[i - 2]);
+                if (zi_1 <= z && z <= zi) {
+                    float dz = zi - zi_1;
+                    h = (dz != 0.0f) ? static_cast<float>(lerp(m_layer_height_profile[i - 1], m_layer_height_profile[i + 1], (z - zi_1) / dz)) :
+                        static_cast<float>(m_layer_height_profile[i + 1]);
+                    break;
+                }
+            }
+            if (h > 0.0f)
+                ret = std::to_string(h);
+        }
+    }
+    return ret;
+}
+
+#if ENABLE_GL_SHADERS_ATTRIBUTES
+void GLCanvas3D::LayersEditing::render_active_object_annotations(const GLCanvas3D& canvas)
+#else
+void GLCanvas3D::LayersEditing::render_active_object_annotations(const GLCanvas3D& canvas, const Rect& bar_rect)
+#endif // ENABLE_GL_SHADERS_ATTRIBUTES
+{
+#if ENABLE_GL_SHADERS_ATTRIBUTES
+    const Size cnv_size = canvas.get_canvas_size();
+    const float cnv_width  = (float)cnv_size.get_width();
+    const float cnv_height = (float)cnv_size.get_height();
+    if (cnv_width == 0.0f || cnv_height == 0.0f)
+        return;
+
+    const float cnv_inv_width = 1.0f / cnv_width;
+#endif // ENABLE_GL_SHADERS_ATTRIBUTES
+    GLShaderProgram* shader = wxGetApp().get_shader("variable_layer_height");
+    if (shader == nullptr)
+        return;
+
+    shader->start_using();
+
+    shader->set_uniform("z_to_texture_row", float(m_layers_texture.cells - 1) / (float(m_layers_texture.width) * m_object_max_z));
+    shader->set_uniform("z_texture_row_to_normalized", 1.0f / (float)m_layers_texture.height);
+    shader->set_uniform("z_cursor", m_object_max_z * this->get_cursor_z_relative(canvas));
+    shader->set_uniform("z_cursor_band_width", band_width);
+    shader->set_uniform("object_max_z", m_object_max_z);
+#if ENABLE_GL_SHADERS_ATTRIBUTES
+    shader->set_uniform("view_model_matrix", Transform3d::Identity());
+    shader->set_uniform("projection_matrix", Transform3d::Identity());
+    shader->set_uniform("normal_matrix", (Matrix3d)Eigen::Matrix3d::Identity());
+#endif // ENABLE_GL_SHADERS_ATTRIBUTES
+
+    glsafe(::glPixelStorei(GL_UNPACK_ALIGNMENT, 1));
+    glsafe(::glBindTexture(GL_TEXTURE_2D, m_z_texture_id));
+
+    // Render the color bar
+#if ENABLE_LEGACY_OPENGL_REMOVAL
+#if ENABLE_GL_SHADERS_ATTRIBUTES
+    if (!m_profile.background.is_initialized() || m_profile.old_canvas_width != cnv_width) {
+        m_profile.old_canvas_width = cnv_width;
+#else
+    if (!m_profile.background.is_initialized() || m_profile.dirty) {
+#endif // ENABLE_GL_SHADERS_ATTRIBUTES
+        m_profile.background.reset();
+
+        GLModel::Geometry init_data;
+        init_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P2T2 };
+        init_data.reserve_vertices(4);
+        init_data.reserve_indices(6);
+
+        // vertices
+#if ENABLE_GL_SHADERS_ATTRIBUTES
+        const float l = 1.0f - 2.0f * THICKNESS_BAR_WIDTH * cnv_inv_width;
+        const float r = 1.0f;
+        const float t = 1.0f;
+        const float b = -1.0f;
+#else
+        const float l = bar_rect.get_left();
+        const float r = bar_rect.get_right();
+        const float t = bar_rect.get_top();
+        const float b = bar_rect.get_bottom();
+#endif // ENABLE_GL_SHADERS_ATTRIBUTES
+        init_data.add_vertex(Vec2f(l, b), Vec2f(0.0f, 0.0f));
+        init_data.add_vertex(Vec2f(r, b), Vec2f(1.0f, 0.0f));
+        init_data.add_vertex(Vec2f(r, t), Vec2f(1.0f, 1.0f));
+        init_data.add_vertex(Vec2f(l, t), Vec2f(0.0f, 1.0f));
+
+        // indices
+        init_data.add_triangle(0, 1, 2);
+        init_data.add_triangle(2, 3, 0);
+
+        m_profile.background.init_from(std::move(init_data));
+    }
+
+    m_profile.background.render();
+#else
+    const float l = bar_rect.get_left();
+    const float r = bar_rect.get_right();
+    const float t = bar_rect.get_top();
+    const float b = bar_rect.get_bottom();
+
+    ::glBegin(GL_QUADS);
+    ::glNormal3f(0.0f, 0.0f, 1.0f);
+    ::glTexCoord2f(0.0f, 0.0f); ::glVertex2f(l, b);
+    ::glTexCoord2f(1.0f, 0.0f); ::glVertex2f(r, b);
+    ::glTexCoord2f(1.0f, 1.0f); ::glVertex2f(r, t);
+    ::glTexCoord2f(0.0f, 1.0f); ::glVertex2f(l, t);
+    glsafe(::glEnd());
+#endif // ENABLE_LEGACY_OPENGL_REMOVAL
+
+    glsafe(::glBindTexture(GL_TEXTURE_2D, 0));
+
+    shader->stop_using();
+}
+
+#if ENABLE_GL_SHADERS_ATTRIBUTES
+void GLCanvas3D::LayersEditing::render_profile(const GLCanvas3D& canvas)
+#else
+void GLCanvas3D::LayersEditing::render_profile(const Rect& bar_rect)
+#endif // ENABLE_GL_SHADERS_ATTRIBUTES
+{
+    //FIXME show some kind of legend.
+
+    if (!m_slicing_parameters)
+        return;
+
+#if ENABLE_GL_SHADERS_ATTRIBUTES
+    const Size cnv_size = canvas.get_canvas_size();
+    const float cnv_width  = (float)cnv_size.get_width();
+    const float cnv_height = (float)cnv_size.get_height();
+    if (cnv_width == 0.0f || cnv_height == 0.0f)
+        return;
+
+    // Make the vertical bar a bit wider so the layer height curve does not touch the edge of the bar region.
+    const float scale_x = THICKNESS_BAR_WIDTH / float(1.12 * m_slicing_parameters->max_layer_height);
+    const float scale_y = cnv_height / m_object_max_z;
+
+    const float cnv_inv_width  = 1.0f / cnv_width;
+    const float cnv_inv_height = 1.0f / cnv_height;
+#else
+    // Make the vertical bar a bit wider so the layer height curve does not touch the edge of the bar region.
+    const float scale_x = bar_rect.get_width() / float(1.12 * m_slicing_parameters->max_layer_height);
+    const float scale_y = bar_rect.get_height() / m_object_max_z;
+#endif // ENABLE_GL_SHADERS_ATTRIBUTES
+
+#if ENABLE_LEGACY_OPENGL_REMOVAL
+    // Baseline
+#if ENABLE_GL_SHADERS_ATTRIBUTES
+    if (!m_profile.baseline.is_initialized() || m_profile.old_layer_height_profile != m_layer_height_profile) {
+#else
+    if (!m_profile.baseline.is_initialized() || m_profile.dirty) {
+#endif // ENABLE_GL_SHADERS_ATTRIBUTES
+        m_profile.baseline.reset();
+
+        GLModel::Geometry init_data;
+        init_data.format = { GLModel::Geometry::EPrimitiveType::Lines, GLModel::Geometry::EVertexLayout::P2 };
+        init_data.color = ColorRGBA::BLACK();
+        init_data.reserve_vertices(2);
+        init_data.reserve_indices(2);
+
+        // vertices
+#if ENABLE_GL_SHADERS_ATTRIBUTES
+        const float axis_x = 2.0f * ((cnv_width - THICKNESS_BAR_WIDTH + float(m_slicing_parameters->layer_height) * scale_x) * cnv_inv_width - 0.5f);
+        init_data.add_vertex(Vec2f(axis_x, -1.0f));
+        init_data.add_vertex(Vec2f(axis_x, 1.0f));
+#else
+        const float x = bar_rect.get_left() + float(m_slicing_parameters->layer_height) * scale_x;
+        init_data.add_vertex(Vec2f(x, bar_rect.get_bottom()));
+        init_data.add_vertex(Vec2f(x, bar_rect.get_top()));
+#endif // ENABLE_GL_SHADERS_ATTRIBUTES
+
+        // indices
+        init_data.add_line(0, 1);
+
+        m_profile.baseline.init_from(std::move(init_data));
+    }
+
+#if ENABLE_GL_SHADERS_ATTRIBUTES
+    if (!m_profile.profile.is_initialized() || m_profile.old_layer_height_profile != m_layer_height_profile) {
+#else
+    if (!m_profile.profile.is_initialized() || m_profile.dirty || m_profile.old_layer_height_profile != m_layer_height_profile) {
+#endif // ENABLE_GL_SHADERS_ATTRIBUTES
+        m_profile.old_layer_height_profile = m_layer_height_profile;
+        m_profile.profile.reset();
+
+        GLModel::Geometry init_data;
+        init_data.format = { GLModel::Geometry::EPrimitiveType::LineStrip, GLModel::Geometry::EVertexLayout::P2 };
+        init_data.color = ColorRGBA::BLUE();
+        init_data.reserve_vertices(m_layer_height_profile.size() / 2);
+        init_data.reserve_indices(m_layer_height_profile.size() / 2);
+
+        // vertices + indices
+        for (unsigned int i = 0; i < (unsigned int)m_layer_height_profile.size(); i += 2) {
+#if ENABLE_GL_SHADERS_ATTRIBUTES
+            init_data.add_vertex(Vec2f(2.0f * ((cnv_width - THICKNESS_BAR_WIDTH + float(m_layer_height_profile[i + 1]) * scale_x) * cnv_inv_width - 0.5f),
+                                       2.0f * (float(m_layer_height_profile[i]) * scale_y * cnv_inv_height - 0.5)));
+#else
+            init_data.add_vertex(Vec2f(bar_rect.get_left() + float(m_layer_height_profile[i + 1]) * scale_x,
+                                       bar_rect.get_bottom() + float(m_layer_height_profile[i]) * scale_y));
+#endif // ENABLE_GL_SHADERS_ATTRIBUTES
+            init_data.add_index(i / 2);
+        }
+
+        m_profile.profile.init_from(std::move(init_data));
+    }
+
+    GLShaderProgram* shader = wxGetApp().get_shader("flat");
+    if (shader != nullptr) {
+        shader->start_using();
+#if ENABLE_GL_SHADERS_ATTRIBUTES
+        shader->set_uniform("view_model_matrix", Transform3d::Identity());
+        shader->set_uniform("projection_matrix", Transform3d::Identity());
+#endif // ENABLE_GL_SHADERS_ATTRIBUTES
+        m_profile.baseline.render();
+        m_profile.profile.render();
+        shader->stop_using();
+    }
+#else
+    const float x = bar_rect.get_left() + float(m_slicing_parameters->layer_height) * scale_x;
+
+    // Baseline
+    glsafe(::glColor3f(0.0f, 0.0f, 0.0f));
+    ::glBegin(GL_LINE_STRIP);
+    ::glVertex2f(x, bar_rect.get_bottom());
+    ::glVertex2f(x, bar_rect.get_top());
+    glsafe(::glEnd());
+
+    // Curve
+    glsafe(::glColor3f(0.0f, 0.0f, 1.0f));
+    ::glBegin(GL_LINE_STRIP);
+    for (unsigned int i = 0; i < m_layer_height_profile.size(); i += 2)
+        ::glVertex2f(bar_rect.get_left() + (float)m_layer_height_profile[i + 1] * scale_x, bar_rect.get_bottom() + (float)m_layer_height_profile[i] * scale_y);
+    glsafe(::glEnd());
+#endif // ENABLE_LEGACY_OPENGL_REMOVAL
+}
+
+void GLCanvas3D::LayersEditing::render_volumes(const GLCanvas3D& canvas, const GLVolumeCollection& volumes)
+{
+    assert(this->is_allowed());
+    assert(this->last_object_id != -1);
+
+    GLShaderProgram* current_shader = wxGetApp().get_current_shader();
+    ScopeGuard guard([current_shader]() { if (current_shader != nullptr) current_shader->start_using(); });
+    if (current_shader != nullptr)
+        current_shader->stop_using();
+
+    GLShaderProgram* shader = wxGetApp().get_shader("variable_layer_height");
+    if (shader == nullptr)
+        return;
+
+    shader->start_using();
+
+    generate_layer_height_texture();
+
+    // Uniforms were resolved, go ahead using the layer editing shader.
+    shader->set_uniform("z_to_texture_row", float(m_layers_texture.cells - 1) / (float(m_layers_texture.width) * float(m_object_max_z)));
+    shader->set_uniform("z_texture_row_to_normalized", 1.0f / float(m_layers_texture.height));
+    shader->set_uniform("z_cursor", float(m_object_max_z) * float(this->get_cursor_z_relative(canvas)));
+    shader->set_uniform("z_cursor_band_width", float(this->band_width));
+
+#if ENABLE_GL_SHADERS_ATTRIBUTES
+    const Camera& camera = wxGetApp().plater()->get_camera();
+    shader->set_uniform("projection_matrix", camera.get_projection_matrix());
+#endif // ENABLE_GL_SHADERS_ATTRIBUTES
+
+    // Initialize the layer height texture mapping.
+    const GLsizei w = (GLsizei)m_layers_texture.width;
+    const GLsizei h = (GLsizei)m_layers_texture.height;
+    const GLsizei half_w = w / 2;
+    const GLsizei half_h = h / 2;
+    glsafe(::glPixelStorei(GL_UNPACK_ALIGNMENT, 1));
+    glsafe(::glBindTexture(GL_TEXTURE_2D, m_z_texture_id));
+    glsafe(::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0));
+    glsafe(::glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA, half_w, half_h, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0));
+    glsafe(::glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, m_layers_texture.data.data()));
+    glsafe(::glTexSubImage2D(GL_TEXTURE_2D, 1, 0, 0, half_w, half_h, GL_RGBA, GL_UNSIGNED_BYTE, m_layers_texture.data.data() + m_layers_texture.width * m_layers_texture.height * 4));
+    for (GLVolume* glvolume : volumes.volumes) {
+        // Render the object using the layer editing shader and texture.
+        if (!glvolume->is_active || glvolume->composite_id.object_id != this->last_object_id || glvolume->is_modifier)
+            continue;
+
+        shader->set_uniform("volume_world_matrix", glvolume->world_matrix());
+        shader->set_uniform("object_max_z", 0.0f);
+#if ENABLE_GL_SHADERS_ATTRIBUTES
+        const Transform3d view_model_matrix = camera.get_view_matrix() * glvolume->world_matrix();
+        shader->set_uniform("view_model_matrix", view_model_matrix);
+        shader->set_uniform("normal_matrix", (Matrix3d)view_model_matrix.matrix().block(0, 0, 3, 3).inverse().transpose());
+#endif // ENABLE_GL_SHADERS_ATTRIBUTES
+
+        glvolume->render();
+    }
+    // Revert back to the previous shader.
+    glBindTexture(GL_TEXTURE_2D, 0);
+}
+
+void GLCanvas3D::LayersEditing::adjust_layer_height_profile()
+{
+	this->update_slicing_parameters();
+	PrintObject::update_layer_height_profile(*m_model_object, *m_slicing_parameters, m_layer_height_profile);
+	Slic3r::adjust_layer_height_profile(*m_slicing_parameters, m_layer_height_profile, this->last_z, this->strength, this->band_width, this->last_action);
+	m_layer_height_profile_modified = true;
+    m_layers_texture.valid = false;
+}
+
+void GLCanvas3D::LayersEditing::reset_layer_height_profile(GLCanvas3D& canvas)
+{
+    const_cast<ModelObject*>(m_model_object)->layer_height_profile.clear();
+    m_layer_height_profile.clear();
+    m_layers_texture.valid = false;
+    canvas.post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS));
+    wxGetApp().obj_list()->update_info_items(last_object_id);
+}
+
+void GLCanvas3D::LayersEditing::adaptive_layer_height_profile(GLCanvas3D& canvas, float quality_factor)
+{
+    this->update_slicing_parameters();
+    m_layer_height_profile = layer_height_profile_adaptive(*m_slicing_parameters, *m_model_object, quality_factor);
+    const_cast<ModelObject*>(m_model_object)->layer_height_profile.set(m_layer_height_profile);
+    m_layers_texture.valid = false;
+    canvas.post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS));
+    wxGetApp().obj_list()->update_info_items(last_object_id);
+}
+
+void GLCanvas3D::LayersEditing::smooth_layer_height_profile(GLCanvas3D& canvas, const HeightProfileSmoothingParams& smoothing_params)
+{
+    this->update_slicing_parameters();
+    m_layer_height_profile = smooth_height_profile(m_layer_height_profile, *m_slicing_parameters, smoothing_params);
+    const_cast<ModelObject*>(m_model_object)->layer_height_profile.set(m_layer_height_profile);
+    m_layers_texture.valid = false;
+    canvas.post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS));
+    wxGetApp().obj_list()->update_info_items(last_object_id);
+}
+
+void GLCanvas3D::LayersEditing::generate_layer_height_texture()
+{
+	this->update_slicing_parameters();
+	// Always try to update the layer height profile.
+    bool update = ! m_layers_texture.valid;
+    if (PrintObject::update_layer_height_profile(*m_model_object, *m_slicing_parameters, m_layer_height_profile)) {
+        // Initialized to the default value.
+        m_layer_height_profile_modified = false;
+        update = true;
+    }
+    // Update if the layer height profile was changed, or when the texture is not valid.
+    if (! update && ! m_layers_texture.data.empty() && m_layers_texture.cells > 0)
+        // Texture is valid, don't update.
+        return; 
+
+    if (m_layers_texture.data.empty()) {
+        m_layers_texture.width  = 1024;
+        m_layers_texture.height = 1024;
+        m_layers_texture.levels = 2;
+        m_layers_texture.data.assign(m_layers_texture.width * m_layers_texture.height * 5, 0);
+    }
+
+    bool level_of_detail_2nd_level = true;
+    m_layers_texture.cells = Slic3r::generate_layer_height_texture(
+        *m_slicing_parameters, 
+        Slic3r::generate_object_layers(*m_slicing_parameters, m_layer_height_profile), 
+		m_layers_texture.data.data(), m_layers_texture.height, m_layers_texture.width, level_of_detail_2nd_level);
+	m_layers_texture.valid = true;
+}
+
+void GLCanvas3D::LayersEditing::accept_changes(GLCanvas3D& canvas)
+{
+    if (last_object_id >= 0) {
+        if (m_layer_height_profile_modified) {
+            wxGetApp().plater()->take_snapshot(_L("Variable layer height - Manual edit"));
+            const_cast<ModelObject*>(m_model_object)->layer_height_profile.set(m_layer_height_profile);
+			canvas.post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS));
+            wxGetApp().obj_list()->update_info_items(last_object_id);
+        }
+    }
+    m_layer_height_profile_modified = false;
+}
+
+void GLCanvas3D::LayersEditing::update_slicing_parameters()
+{
+	if (m_slicing_parameters == nullptr) {
+		m_slicing_parameters = new SlicingParameters();
+        *m_slicing_parameters = PrintObject::slicing_parameters(*m_config, *m_model_object, m_object_max_z);
+    }
+}
+
+float GLCanvas3D::LayersEditing::thickness_bar_width(const GLCanvas3D &canvas)
+{
+    return
+#if ENABLE_RETINA_GL
+        canvas.get_canvas_size().get_scale_factor()
+#else
+        canvas.get_wxglcanvas()->GetContentScaleFactor()
+#endif
+         * THICKNESS_BAR_WIDTH;
+}
+
+
+const Point GLCanvas3D::Mouse::Drag::Invalid_2D_Point(INT_MAX, INT_MAX);
+const Vec3d GLCanvas3D::Mouse::Drag::Invalid_3D_Point(DBL_MAX, DBL_MAX, DBL_MAX);
+const int GLCanvas3D::Mouse::Drag::MoveThresholdPx = 5;
+
+GLCanvas3D::Mouse::Drag::Drag()
+    : start_position_2D(Invalid_2D_Point)
+    , start_position_3D(Invalid_3D_Point)
+    , move_volume_idx(-1)
+    , move_requires_threshold(false)
+    , move_start_threshold_position_2D(Invalid_2D_Point)
+{
+}
+
+GLCanvas3D::Mouse::Mouse()
+    : dragging(false)
+    , position(DBL_MAX, DBL_MAX)
+    , scene_position(DBL_MAX, DBL_MAX, DBL_MAX)
+    , ignore_left_up(false)
+{
+}
+
+void GLCanvas3D::Labels::render(const std::vector<const ModelInstance*>& sorted_instances) const
+{
+    if (!m_enabled || !is_shown())
+        return;
+
+    const Camera& camera = wxGetApp().plater()->get_camera();
+    const Model* model = m_canvas.get_model();
+    if (model == nullptr)
+        return;
+
+    Transform3d world_to_eye = camera.get_view_matrix();
+    Transform3d world_to_screen = camera.get_projection_matrix() * world_to_eye;
+    const std::array<int, 4>& viewport = camera.get_viewport();
+
+    struct Owner
+    {
+        int obj_idx;
+        int inst_idx;
+        size_t model_instance_id;
+        BoundingBoxf3 world_box;
+        double eye_center_z;
+        std::string title;
+        std::string label;
+        std::string print_order;
+        bool selected;
+    };
+
+    // collect owners world bounding boxes and data from volumes
+    std::vector<Owner> owners;
+    const GLVolumeCollection& volumes = m_canvas.get_volumes();
+    for (const GLVolume* volume : volumes.volumes) {
+        int obj_idx = volume->object_idx();
+        if (0 <= obj_idx && obj_idx < (int)model->objects.size()) {
+            int inst_idx = volume->instance_idx();
+            std::vector<Owner>::iterator it = std::find_if(owners.begin(), owners.end(), [obj_idx, inst_idx](const Owner& owner) {
+                return (owner.obj_idx == obj_idx) && (owner.inst_idx == inst_idx);
+                });
+            if (it != owners.end()) {
+                it->world_box.merge(volume->transformed_bounding_box());
+                it->selected &= volume->selected;
+            } else {
+                const ModelObject* model_object = model->objects[obj_idx];
+                Owner owner;
+                owner.obj_idx = obj_idx;
+                owner.inst_idx = inst_idx;
+                owner.model_instance_id = model_object->instances[inst_idx]->id().id;
+                owner.world_box = volume->transformed_bounding_box();
+                owner.title = "object" + std::to_string(obj_idx) + "_inst##" + std::to_string(inst_idx);
+                owner.label = model_object->name;
+                if (model_object->instances.size() > 1)
+                    owner.label += " (" + std::to_string(inst_idx + 1) + ")";
+                owner.selected = volume->selected;
+                owners.emplace_back(owner);
+            }
+        }
+    }
+
+    // updates print order strings
+    if (sorted_instances.size() > 1) {
+        for (size_t i = 0; i < sorted_instances.size(); ++i) {
+            size_t id = sorted_instances[i]->id().id;
+            std::vector<Owner>::iterator it = std::find_if(owners.begin(), owners.end(), [id](const Owner& owner) {
+                return owner.model_instance_id == id;
+                });
+            if (it != owners.end())
+                it->print_order = std::string((_(L("Seq."))).ToUTF8()) + "#: " + std::to_string(i + 1);
+        }
+    }
+
+    // calculate eye bounding boxes center zs
+    for (Owner& owner : owners) {
+        owner.eye_center_z = (world_to_eye * owner.world_box.center())(2);
+    }
+
+    // sort owners by center eye zs and selection
+    std::sort(owners.begin(), owners.end(), [](const Owner& owner1, const Owner& owner2) {
+        if (!owner1.selected && owner2.selected)
+            return true;
+        else if (owner1.selected && !owner2.selected)
+            return false;
+        else
+            return (owner1.eye_center_z < owner2.eye_center_z);
+        });
+
+    ImGuiWrapper& imgui = *wxGetApp().imgui();
+
+    // render info windows
+    for (const Owner& owner : owners) {
+        Vec3d screen_box_center = world_to_screen * owner.world_box.center();
+        float x = 0.0f;
+        float y = 0.0f;
+        if (camera.get_type() == Camera::EType::Perspective) {
+            x = (0.5f + 0.001f * 0.5f * (float)screen_box_center(0)) * viewport[2];
+            y = (0.5f - 0.001f * 0.5f * (float)screen_box_center(1)) * viewport[3];
+        } else {
+            x = (0.5f + 0.5f * (float)screen_box_center(0)) * viewport[2];
+            y = (0.5f - 0.5f * (float)screen_box_center(1)) * viewport[3];
+        }
+
+        if (x < 0.0f || viewport[2] < x || y < 0.0f || viewport[3] < y)
+            continue;
+
+        ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, owner.selected ? 3.0f : 1.5f);
+        ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
+        ImGui::PushStyleColor(ImGuiCol_Border, owner.selected ? ImVec4(0.757f, 0.404f, 0.216f, 1.0f) : ImVec4(0.75f, 0.75f, 0.75f, 1.0f));
+        imgui.set_next_window_pos(x, y, ImGuiCond_Always, 0.5f, 0.5f);
+        imgui.begin(owner.title, ImGuiWindowFlags_NoMouseInputs | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove);
+        ImGui::BringWindowToDisplayFront(ImGui::GetCurrentWindow());
+        float win_w = ImGui::GetWindowWidth();
+        float label_len = imgui.calc_text_size(owner.label).x;
+        ImGui::SetCursorPosX(0.5f * (win_w - label_len));
+        ImGui::AlignTextToFramePadding();
+        imgui.text(owner.label);
+
+        if (!owner.print_order.empty()) {
+            ImGui::Separator();
+            float po_len = imgui.calc_text_size(owner.print_order).x;
+            ImGui::SetCursorPosX(0.5f * (win_w - po_len));
+            ImGui::AlignTextToFramePadding();
+            imgui.text(owner.print_order);
+        }
+
+        // force re-render while the windows gets to its final size (it takes several frames)
+        if (ImGui::GetWindowContentRegionWidth() + 2.0f * ImGui::GetStyle().WindowPadding.x != ImGui::CalcWindowNextAutoFitSize(ImGui::GetCurrentWindow()).x)
+            imgui.set_requires_extra_frame();
+
+        imgui.end();
+        ImGui::PopStyleColor();
+        ImGui::PopStyleVar(2);
+    }
+}
+
+void GLCanvas3D::Tooltip::set_text(const std::string& text)
+{
+    // If the mouse is inside an ImGUI dialog, then the tooltip is suppressed.
+    m_text = m_in_imgui ? std::string() : text;
+}
+
+void GLCanvas3D::Tooltip::render(const Vec2d& mouse_position, GLCanvas3D& canvas)
+{
+    static ImVec2 size(0.0f, 0.0f);
+
+    auto validate_position = [](const Vec2d& position, const GLCanvas3D& canvas, const ImVec2& wnd_size) {
+        auto calc_cursor_height = []() {
+            float ret = 16.0f;
+#ifdef _WIN32
+            // see: https://forums.codeguru.com/showthread.php?449040-get-the-system-current-cursor-size
+            // this code is not perfect because it returns a maximum height equal to 31 even if the cursor bitmap shown on screen is bigger
+            // but at least it gives the same result as wxWidgets in the settings tabs
+            ICONINFO ii;
+            if (::GetIconInfo((HICON)GetCursor(), &ii) != 0) {
+                BITMAP bitmap;
+                ::GetObject(ii.hbmMask, sizeof(BITMAP), &bitmap);
+                int width = bitmap.bmWidth;
+                int height = (ii.hbmColor == nullptr) ? bitmap.bmHeight / 2 : bitmap.bmHeight;
+                HDC dc = ::CreateCompatibleDC(nullptr);
+                if (dc != nullptr) {
+                    if (::SelectObject(dc, ii.hbmMask) != nullptr) {
+                        for (int i = 0; i < width; ++i) {
+                            for (int j = 0; j < height; ++j) {
+                                if (::GetPixel(dc, i, j) != RGB(255, 255, 255)) {
+                                    if (ret < float(j))
+                                        ret = float(j);
+                                }
+                            }
+                        }
+                        ::DeleteDC(dc);
+                    }
+                }
+                ::DeleteObject(ii.hbmColor);
+                ::DeleteObject(ii.hbmMask);
+            }
+#endif //  _WIN32
+            return ret;
+        };
+
+        const Size cnv_size = canvas.get_canvas_size();
+        const float x = std::clamp(float(position.x()), 0.0f, float(cnv_size.get_width()) - wnd_size.x);
+        const float y = std::clamp(float(position.y()) + calc_cursor_height(), 0.0f, float(cnv_size.get_height()) - wnd_size.y);
+        return Vec2f(x, y);
+    };
+
+    if (m_text.empty()) {
+        m_start_time = std::chrono::steady_clock::now();
+        return;
+    }
+
+    // draw the tooltip as hidden until the delay is expired
+    // use a value of alpha slightly different from 0.0f because newer imgui does not calculate properly the window size if alpha == 0.0f
+    const float alpha = (std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - m_start_time).count() < 500) ? 0.01f : 1.0f;
+
+    const Vec2f position = validate_position(mouse_position, canvas, size);
+
+    ImGuiWrapper& imgui = *wxGetApp().imgui();
+    ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
+    ImGui::PushStyleVar(ImGuiStyleVar_Alpha, alpha);
+    imgui.set_next_window_pos(position.x(), position.y(), ImGuiCond_Always, 0.0f, 0.0f);
+
+    imgui.begin(wxString("canvas_tooltip"), ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMouseInputs | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoFocusOnAppearing);
+    ImGui::BringWindowToDisplayFront(ImGui::GetCurrentWindow());
+    ImGui::TextUnformatted(m_text.c_str());
+
+    // force re-render while the windows gets to its final size (it may take several frames) or while hidden
+    if (alpha < 1.0f || ImGui::GetWindowContentRegionWidth() + 2.0f * ImGui::GetStyle().WindowPadding.x != ImGui::CalcWindowNextAutoFitSize(ImGui::GetCurrentWindow()).x)
+        imgui.set_requires_extra_frame();
+
+    size = ImGui::GetWindowSize();
+
+    imgui.end();
+    ImGui::PopStyleVar(2);
+}
+
+void GLCanvas3D::SequentialPrintClearance::set_polygons(const Polygons& polygons)
+{
+    m_perimeter.reset();
+    m_fill.reset();
+    if (polygons.empty())
+        return;
+
+#if !ENABLE_LEGACY_OPENGL_REMOVAL
+    size_t triangles_count = 0;
+    for (const Polygon& poly : polygons) {
+        triangles_count += poly.points.size() - 2;
+    }
+    const size_t vertices_count = 3 * triangles_count;
+#endif // !ENABLE_LEGACY_OPENGL_REMOVAL
+
+    if (m_render_fill) {
+        GLModel::Geometry fill_data;
+#if ENABLE_LEGACY_OPENGL_REMOVAL
+        fill_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3 };
+        fill_data.color  = { 0.3333f, 0.0f, 0.0f, 0.5f };
+
+        // vertices + indices
+        const ExPolygons polygons_union = union_ex(polygons);
+        unsigned int vertices_counter = 0;
+        for (const ExPolygon& poly : polygons_union) {
+            const std::vector<Vec3d> triangulation = triangulate_expolygon_3d(poly);
+            fill_data.reserve_vertices(fill_data.vertices_count() + triangulation.size());
+            fill_data.reserve_indices(fill_data.indices_count() + triangulation.size());
+            for (const Vec3d& v : triangulation) {
+                fill_data.add_vertex((Vec3f)(v.cast<float>() + 0.0125f * Vec3f::UnitZ())); // add a small positive z to avoid z-fighting
+                ++vertices_counter;
+                if (vertices_counter % 3 == 0)
+                    fill_data.add_triangle(vertices_counter - 3, vertices_counter - 2, vertices_counter - 1);
+            }
+        }
+
+        m_fill.init_from(std::move(fill_data));
+#else
+        GLModel::Geometry::Entity entity;
+        entity.type = GLModel::EPrimitiveType::Triangles;
+        entity.color = { 0.3333f, 0.0f, 0.0f, 0.5f };
+        entity.positions.reserve(vertices_count);
+        entity.normals.reserve(vertices_count);
+        entity.indices.reserve(vertices_count);
+
+        const ExPolygons polygons_union = union_ex(polygons);
+        for (const ExPolygon& poly : polygons_union) {
+            const std::vector<Vec3d> triangulation = triangulate_expolygon_3d(poly);
+            for (const Vec3d& v : triangulation) {
+                entity.positions.emplace_back(v.cast<float>() + Vec3f(0.0f, 0.0f, 0.0125f)); // add a small positive z to avoid z-fighting
+                entity.normals.emplace_back(Vec3f::UnitZ());
+                const size_t positions_count = entity.positions.size();
+                if (positions_count % 3 == 0) {
+                    entity.indices.emplace_back(positions_count - 3);
+                    entity.indices.emplace_back(positions_count - 2);
+                    entity.indices.emplace_back(positions_count - 1);
+                }
+            }
+        }
+
+        fill_data.entities.emplace_back(entity);
+        m_fill.init_from(fill_data);
+#endif // ENABLE_LEGACY_OPENGL_REMOVAL
+    }
+
+#if ENABLE_LEGACY_OPENGL_REMOVAL
+    m_perimeter.init_from(polygons, 0.025f); // add a small positive z to avoid z-fighting
+#else
+    GLModel::Geometry perimeter_data;
+    for (const Polygon& poly : polygons) {
+        GLModel::Geometry::Entity ent;
+        ent.type = GLModel::EPrimitiveType::LineLoop;
+        ent.positions.reserve(poly.points.size());
+        ent.indices.reserve(poly.points.size());
+        unsigned int id_count = 0;
+        for (const Point& p : poly.points) {
+            ent.positions.emplace_back(unscale<float>(p.x()), unscale<float>(p.y()), 0.025f); // add a small positive z to avoid z-fighting
+            ent.normals.emplace_back(Vec3f::UnitZ());
+            ent.indices.emplace_back(id_count++);
+        }
+
+        perimeter_data.entities.emplace_back(ent);
+    }
+
+    m_perimeter.init_from(perimeter_data);
+#endif // ENABLE_LEGACY_OPENGL_REMOVAL
+}
+
+void GLCanvas3D::SequentialPrintClearance::render()
+{
+    const ColorRGBA FILL_COLOR    = { 1.0f, 0.0f, 0.0f, 0.5f };
+    const ColorRGBA NO_FILL_COLOR = { 1.0f, 1.0f, 1.0f, 0.75f };
+
+#if ENABLE_LEGACY_OPENGL_REMOVAL
+    GLShaderProgram* shader = wxGetApp().get_shader("flat");
+#else
+    GLShaderProgram* shader = wxGetApp().get_shader("gouraud_light");
+#endif // ENABLE_LEGACY_OPENGL_REMOVAL
+    if (shader == nullptr)
+        return;
+
+    shader->start_using();
+
+#if ENABLE_GL_SHADERS_ATTRIBUTES
+    const Camera& camera = wxGetApp().plater()->get_camera();
+    shader->set_uniform("view_model_matrix", camera.get_view_matrix());
+    shader->set_uniform("projection_matrix", camera.get_projection_matrix());
+#endif // ENABLE_GL_SHADERS_ATTRIBUTES
+
+    glsafe(::glEnable(GL_DEPTH_TEST));
+    glsafe(::glDisable(GL_CULL_FACE));
+    glsafe(::glEnable(GL_BLEND));
+    glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA));
+
+#if ENABLE_LEGACY_OPENGL_REMOVAL
+    m_perimeter.set_color(m_render_fill ? FILL_COLOR : NO_FILL_COLOR);
+#else
+    m_perimeter.set_color(-1, m_render_fill ? FILL_COLOR : NO_FILL_COLOR);
+#endif // ENABLE_LEGACY_OPENGL_REMOVAL
+    m_perimeter.render();
+    m_fill.render();
+
+    glsafe(::glDisable(GL_BLEND));
+    glsafe(::glEnable(GL_CULL_FACE));
+    glsafe(::glDisable(GL_DEPTH_TEST));
+
+    shader->stop_using();
+}
+
+wxDEFINE_EVENT(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS, SimpleEvent);
+wxDEFINE_EVENT(EVT_GLCANVAS_OBJECT_SELECT, SimpleEvent);
+wxDEFINE_EVENT(EVT_GLCANVAS_RIGHT_CLICK, RBtnEvent);
+wxDEFINE_EVENT(EVT_GLCANVAS_REMOVE_OBJECT, SimpleEvent);
+wxDEFINE_EVENT(EVT_GLCANVAS_ARRANGE, SimpleEvent);
+wxDEFINE_EVENT(EVT_GLCANVAS_SELECT_ALL, SimpleEvent);
+wxDEFINE_EVENT(EVT_GLCANVAS_QUESTION_MARK, SimpleEvent);
+wxDEFINE_EVENT(EVT_GLCANVAS_INCREASE_INSTANCES, Event<int>);
+wxDEFINE_EVENT(EVT_GLCANVAS_INSTANCE_MOVED, SimpleEvent);
+wxDEFINE_EVENT(EVT_GLCANVAS_INSTANCE_ROTATED, SimpleEvent);
+wxDEFINE_EVENT(EVT_GLCANVAS_INSTANCE_SCALED, SimpleEvent);
+wxDEFINE_EVENT(EVT_GLCANVAS_FORCE_UPDATE, SimpleEvent);
+wxDEFINE_EVENT(EVT_GLCANVAS_WIPETOWER_MOVED, Vec3dEvent);
+wxDEFINE_EVENT(EVT_GLCANVAS_WIPETOWER_ROTATED, Vec3dEvent);
+wxDEFINE_EVENT(EVT_GLCANVAS_ENABLE_ACTION_BUTTONS, Event<bool>);
+wxDEFINE_EVENT(EVT_GLCANVAS_UPDATE_GEOMETRY, Vec3dsEvent<2>);
+wxDEFINE_EVENT(EVT_GLCANVAS_MOUSE_DRAGGING_STARTED, SimpleEvent);
+wxDEFINE_EVENT(EVT_GLCANVAS_MOUSE_DRAGGING_FINISHED, SimpleEvent);
+wxDEFINE_EVENT(EVT_GLCANVAS_UPDATE_BED_SHAPE, SimpleEvent);
+wxDEFINE_EVENT(EVT_GLCANVAS_TAB, SimpleEvent);
+wxDEFINE_EVENT(EVT_GLCANVAS_RESETGIZMOS, SimpleEvent);
+wxDEFINE_EVENT(EVT_GLCANVAS_MOVE_SLIDERS, wxKeyEvent);
+wxDEFINE_EVENT(EVT_GLCANVAS_EDIT_COLOR_CHANGE, wxKeyEvent);
+wxDEFINE_EVENT(EVT_GLCANVAS_JUMP_TO, wxKeyEvent);
+wxDEFINE_EVENT(EVT_GLCANVAS_UNDO, SimpleEvent);
+wxDEFINE_EVENT(EVT_GLCANVAS_REDO, SimpleEvent);
+wxDEFINE_EVENT(EVT_GLCANVAS_COLLAPSE_SIDEBAR, SimpleEvent);
+wxDEFINE_EVENT(EVT_GLCANVAS_RESET_LAYER_HEIGHT_PROFILE, SimpleEvent);
+wxDEFINE_EVENT(EVT_GLCANVAS_ADAPTIVE_LAYER_HEIGHT_PROFILE, Event<float>);
+wxDEFINE_EVENT(EVT_GLCANVAS_SMOOTH_LAYER_HEIGHT_PROFILE, HeightProfileSmoothEvent);
+wxDEFINE_EVENT(EVT_GLCANVAS_RELOAD_FROM_DISK, SimpleEvent);
+wxDEFINE_EVENT(EVT_GLCANVAS_RENDER_TIMER, wxTimerEvent/*RenderTimerEvent*/);
+wxDEFINE_EVENT(EVT_GLCANVAS_TOOLBAR_HIGHLIGHTER_TIMER, wxTimerEvent);
+wxDEFINE_EVENT(EVT_GLCANVAS_GIZMO_HIGHLIGHTER_TIMER, wxTimerEvent);
+
+const double GLCanvas3D::DefaultCameraZoomToBoxMarginFactor = 1.25;
+
+void GLCanvas3D::load_arrange_settings()
+{
+    std::string dist_fff_str =
+        wxGetApp().app_config->get("arrange", "min_object_distance_fff");
+
+    std::string dist_fff_seq_print_str =
+        wxGetApp().app_config->get("arrange", "min_object_distance_fff_seq_print");
+
+    std::string dist_sla_str =
+        wxGetApp().app_config->get("arrange", "min_object_distance_sla");
+
+    std::string en_rot_fff_str =
+        wxGetApp().app_config->get("arrange", "enable_rotation_fff");
+
+    std::string en_rot_fff_seqp_str =
+        wxGetApp().app_config->get("arrange", "enable_rotation_fff_seq_print");
+
+    std::string en_rot_sla_str =
+        wxGetApp().app_config->get("arrange", "enable_rotation_sla");
+
+    if (!dist_fff_str.empty())
+        m_arrange_settings_fff.distance = std::stof(dist_fff_str);
+
+    if (!dist_fff_seq_print_str.empty())
+        m_arrange_settings_fff_seq_print.distance = std::stof(dist_fff_seq_print_str);
+
+    if (!dist_sla_str.empty())
+        m_arrange_settings_sla.distance = std::stof(dist_sla_str);
+
+    if (!en_rot_fff_str.empty())
+        m_arrange_settings_fff.enable_rotation = (en_rot_fff_str == "1" || en_rot_fff_str == "yes");
+
+    if (!en_rot_fff_seqp_str.empty())
+        m_arrange_settings_fff_seq_print.enable_rotation = (en_rot_fff_seqp_str == "1" || en_rot_fff_seqp_str == "yes");
+
+    if (!en_rot_sla_str.empty())
+        m_arrange_settings_sla.enable_rotation = (en_rot_sla_str == "1" || en_rot_sla_str == "yes");
+}
+
+PrinterTechnology GLCanvas3D::current_printer_technology() const
+{
+    return m_process->current_printer_technology();
+}
+
+GLCanvas3D::GLCanvas3D(wxGLCanvas* canvas, Bed3D &bed)
+    : m_canvas(canvas)
+    , m_context(nullptr)
+    , m_bed(bed)
+#if ENABLE_RETINA_GL
+    , m_retina_helper(nullptr)
+#endif
+    , m_in_render(false)
+    , m_main_toolbar(GLToolbar::Normal, "Main")
+    , m_undoredo_toolbar(GLToolbar::Normal, "Undo_Redo")
+    , m_gizmos(*this)
+    , m_use_clipping_planes(false)
+    , m_sidebar_field("")
+    , m_extra_frame_requested(false)
+    , m_config(nullptr)
+    , m_process(nullptr)
+    , m_model(nullptr)
+    , m_dirty(true)
+    , m_initialized(false)
+    , m_apply_zoom_to_volumes_filter(false)
+    , m_picking_enabled(false)
+    , m_moving_enabled(false)
+    , m_dynamic_background_enabled(false)
+    , m_multisample_allowed(false)
+    , m_moving(false)
+    , m_tab_down(false)
+    , m_cursor_type(Standard)
+    , m_reload_delayed(false)
+#if ENABLE_RENDER_PICKING_PASS
+    , m_show_picking_texture(false)
+#endif // ENABLE_RENDER_PICKING_PASS
+    , m_render_sla_auxiliaries(true)
+    , m_labels(*this)
+    , m_slope(m_volumes)
+{
+    if (m_canvas != nullptr) {
+        m_timer.SetOwner(m_canvas);
+        m_render_timer.SetOwner(m_canvas);
+#if ENABLE_RETINA_GL
+        m_retina_helper.reset(new RetinaHelper(canvas));
+#endif // ENABLE_RETINA_GL
+    }
+
+    load_arrange_settings();
+
+    m_selection.set_volumes(&m_volumes.volumes);
+}
+
+GLCanvas3D::~GLCanvas3D()
+{
+    reset_volumes();
+}
+
+void GLCanvas3D::post_event(wxEvent &&event)
+{
+    event.SetEventObject(m_canvas);
+    wxPostEvent(m_canvas, event);
+}
+
+bool GLCanvas3D::init()
+{
+    if (m_initialized)
+        return true;
+
+    if (m_canvas == nullptr || m_context == nullptr)
+        return false;
+
+    glsafe(::glClearColor(1.0f, 1.0f, 1.0f, 1.0f));
+    glsafe(::glClearDepth(1.0f));
+
+    glsafe(::glDepthFunc(GL_LESS));
+
+    glsafe(::glEnable(GL_DEPTH_TEST));
+    glsafe(::glEnable(GL_CULL_FACE));
+    glsafe(::glEnable(GL_BLEND));
+    glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA));
+
+#if !ENABLE_LEGACY_OPENGL_REMOVAL
+    // Set antialiasing / multisampling
+    glsafe(::glDisable(GL_LINE_SMOOTH));
+    glsafe(::glDisable(GL_POLYGON_SMOOTH));
+
+    // ambient lighting
+    GLfloat ambient[4] = { 0.3f, 0.3f, 0.3f, 1.0f };
+    glsafe(::glLightModelfv(GL_LIGHT_MODEL_AMBIENT, ambient));
+
+    glsafe(::glEnable(GL_LIGHT0));
+    glsafe(::glEnable(GL_LIGHT1));
+
+    // light from camera
+    GLfloat specular_cam[4] = { 0.3f, 0.3f, 0.3f, 1.0f };
+    glsafe(::glLightfv(GL_LIGHT1, GL_SPECULAR, specular_cam));
+    GLfloat diffuse_cam[4] = { 0.2f, 0.2f, 0.2f, 1.0f };
+    glsafe(::glLightfv(GL_LIGHT1, GL_DIFFUSE, diffuse_cam));
+
+    // light from above
+    GLfloat specular_top[4] = { 0.2f, 0.2f, 0.2f, 1.0f };
+    glsafe(::glLightfv(GL_LIGHT0, GL_SPECULAR, specular_top));
+    GLfloat diffuse_top[4] = { 0.5f, 0.5f, 0.5f, 1.0f };
+    glsafe(::glLightfv(GL_LIGHT0, GL_DIFFUSE, diffuse_top));
+
+    // Enables Smooth Color Shading; try GL_FLAT for (lack of) fun.
+    glsafe(::glShadeModel(GL_SMOOTH));
+
+    // A handy trick -- have surface material mirror the color.
+    glsafe(::glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE));
+    glsafe(::glEnable(GL_COLOR_MATERIAL));
+#endif // !ENABLE_LEGACY_OPENGL_REMOVAL
+
+    if (m_multisample_allowed)
+        glsafe(::glEnable(GL_MULTISAMPLE));
+
+    if (m_main_toolbar.is_enabled())
+        m_layers_editing.init();
+
+#if !ENABLE_LEGACY_OPENGL_REMOVAL
+    // on linux the gl context is not valid until the canvas is not shown on screen
+    // we defer the geometry finalization of volumes until the first call to render()
+    m_volumes.finalize_geometry(true);
+#endif // !ENABLE_LEGACY_OPENGL_REMOVAL
+
+    if (m_gizmos.is_enabled() && !m_gizmos.init())
+        std::cout << "Unable to initialize gizmos: please, check that all the required textures are available" << std::endl;
+
+    if (!_init_toolbars())
+        return false;
+
+    if (m_selection.is_enabled() && !m_selection.init())
+        return false;
+
+    m_initialized = true;
+
+    return true;
+}
+
+void GLCanvas3D::set_as_dirty()
+{
+    m_dirty = true;
+}
+
+unsigned int GLCanvas3D::get_volumes_count() const
+{
+    return (unsigned int)m_volumes.volumes.size();
+}
+
+void GLCanvas3D::reset_volumes()
+{
+    if (!m_initialized)
+        return;
+
+    if (m_volumes.empty())
+        return;
+
+    _set_current();
+
+    m_selection.clear();
+    m_volumes.clear();
+    m_dirty = true;
+
+    _set_warning_notification(EWarning::ObjectOutside, false);
+}
+
+ModelInstanceEPrintVolumeState GLCanvas3D::check_volumes_outside_state() const
+{
+    ModelInstanceEPrintVolumeState state = ModelInstanceEPrintVolumeState::ModelInstancePVS_Inside;
+    if (m_initialized)
+        m_volumes.check_outside_state(m_bed.build_volume(), &state);
+    return state;
+}
+
+void GLCanvas3D::toggle_sla_auxiliaries_visibility(bool visible, const ModelObject* mo, int instance_idx)
+{
+#if ENABLE_WIPETOWER_OBJECTID_1000_REMOVAL
+    if (current_printer_technology() != ptSLA)
+        return;
+#endif // ENABLE_WIPETOWER_OBJECTID_1000_REMOVAL
+
+    m_render_sla_auxiliaries = visible;
+
+    for (GLVolume* vol : m_volumes.volumes) {
+#if !ENABLE_WIPETOWER_OBJECTID_1000_REMOVAL
+        if (vol->composite_id.object_id == 1000)
+            continue; // the wipe tower
+#endif // !ENABLE_WIPETOWER_OBJECTID_1000_REMOVAL
+        if ((mo == nullptr || m_model->objects[vol->composite_id.object_id] == mo)
+        && (instance_idx == -1 || vol->composite_id.instance_id == instance_idx)
+        && vol->composite_id.volume_id < 0)
+            vol->is_active = visible;
+    }
+}
+
+void GLCanvas3D::toggle_model_objects_visibility(bool visible, const ModelObject* mo, int instance_idx, const ModelVolume* mv)
+{
+    for (GLVolume* vol : m_volumes.volumes) {
+#if ENABLE_WIPETOWER_OBJECTID_1000_REMOVAL
+        if (vol->is_wipe_tower)
+            vol->is_active = (visible && mo == nullptr);
+#else
+        if (vol->composite_id.object_id == 1000) { // wipe tower
+            vol->is_active = (visible && mo == nullptr);
+        }
+#endif // ENABLE_WIPETOWER_OBJECTID_1000_REMOVAL
+        else {
+            if ((mo == nullptr || m_model->objects[vol->composite_id.object_id] == mo)
+            && (instance_idx == -1 || vol->composite_id.instance_id == instance_idx)
+            && (mv == nullptr || m_model->objects[vol->composite_id.object_id]->volumes[vol->composite_id.volume_id] == mv)) {
+                vol->is_active = visible;
+
+                if (instance_idx == -1) {
+                    vol->force_native_color = false;
+                    vol->force_neutral_color = false;
+                } else {
+                    const GLGizmosManager& gm = get_gizmos_manager();
+                    auto gizmo_type = gm.get_current_type();
+                    if (    (gizmo_type == GLGizmosManager::FdmSupports
+                          || gizmo_type == GLGizmosManager::Seam)
+                        && ! vol->is_modifier)
+                        vol->force_neutral_color = true;
+                    else if (gizmo_type == GLGizmosManager::MmuSegmentation)
+                        vol->is_active = false;
+                    else
+                        vol->force_native_color = true;
+                }
+            }
+        }
+    }
+
+    if (visible && !mo)
+        toggle_sla_auxiliaries_visibility(true, mo, instance_idx);
+
+    if (!mo && !visible && !m_model->objects.empty() && (m_model->objects.size() > 1 || m_model->objects.front()->instances.size() > 1))
+        _set_warning_notification(EWarning::SomethingNotShown, true);
+
+    if (!mo && visible)
+        _set_warning_notification(EWarning::SomethingNotShown, false);
+}
+
+void GLCanvas3D::update_instance_printable_state_for_object(const size_t obj_idx)
+{
+    ModelObject* model_object = m_model->objects[obj_idx];
+    for (int inst_idx = 0; inst_idx < (int)model_object->instances.size(); ++inst_idx) {
+        ModelInstance* instance = model_object->instances[inst_idx];
+
+        for (GLVolume* volume : m_volumes.volumes) {
+            if (volume->object_idx() == (int)obj_idx && volume->instance_idx() == inst_idx)
+                volume->printable = instance->printable;
+        }
+    }
+}
+
+void GLCanvas3D::update_instance_printable_state_for_objects(const std::vector<size_t>& object_idxs)
+{
+    for (size_t obj_idx : object_idxs)
+        update_instance_printable_state_for_object(obj_idx);
+}
+
+void GLCanvas3D::set_config(const DynamicPrintConfig* config)
+{
+    m_config = config;
+    m_layers_editing.set_config(config);
+}
+
+void GLCanvas3D::set_process(BackgroundSlicingProcess *process)
+{
+    m_process = process;
+}
+
+void GLCanvas3D::set_model(Model* model)
+{
+    m_model = model;
+    m_selection.set_model(m_model);
+}
+
+void GLCanvas3D::bed_shape_changed()
+{
+    refresh_camera_scene_box();
+    wxGetApp().plater()->get_camera().requires_zoom_to_bed = true;
+    m_dirty = true;
+}
+
+void GLCanvas3D::refresh_camera_scene_box()
+{
+    wxGetApp().plater()->get_camera().set_scene_box(scene_bounding_box());
+}
+
+BoundingBoxf3 GLCanvas3D::volumes_bounding_box() const
+{
+    BoundingBoxf3 bb;
+    for (const GLVolume* volume : m_volumes.volumes) {
+        if (!m_apply_zoom_to_volumes_filter || ((volume != nullptr) && volume->zoom_to_volumes))
+            bb.merge(volume->transformed_bounding_box());
+    }
+    return bb;
+}
+
+BoundingBoxf3 GLCanvas3D::scene_bounding_box() const
+{
+    BoundingBoxf3 bb = volumes_bounding_box();
+    bb.merge(m_bed.extended_bounding_box());
+    double h = m_bed.build_volume().max_print_height();
+    //FIXME why -h?
+    bb.min.z() = std::min(bb.min.z(), -h);
+    bb.max.z() = std::max(bb.max.z(), h);
+    return bb;
+}
+
+bool GLCanvas3D::is_layers_editing_enabled() const
+{
+    return m_layers_editing.is_enabled();
+}
+
+bool GLCanvas3D::is_layers_editing_allowed() const
+{
+    return m_layers_editing.is_allowed();
+}
+
+void GLCanvas3D::reset_layer_height_profile()
+{
+    wxGetApp().plater()->take_snapshot(_L("Variable layer height - Reset"));
+    m_layers_editing.reset_layer_height_profile(*this);
+    m_layers_editing.state = LayersEditing::Completed;
+    m_dirty = true;
+}
+
+void GLCanvas3D::adaptive_layer_height_profile(float quality_factor)
+{
+    wxGetApp().plater()->take_snapshot(_L("Variable layer height - Adaptive"));
+    m_layers_editing.adaptive_layer_height_profile(*this, quality_factor);
+    m_layers_editing.state = LayersEditing::Completed;
+    m_dirty = true;
+}
+
+void GLCanvas3D::smooth_layer_height_profile(const HeightProfileSmoothingParams& smoothing_params)
+{
+    wxGetApp().plater()->take_snapshot(_L("Variable layer height - Smooth all"));
+    m_layers_editing.smooth_layer_height_profile(*this, smoothing_params);
+    m_layers_editing.state = LayersEditing::Completed;
+    m_dirty = true;
+}
+
+bool GLCanvas3D::is_reload_delayed() const
+{
+    return m_reload_delayed;
+}
+
+void GLCanvas3D::enable_layers_editing(bool enable)
+{
+    m_layers_editing.set_enabled(enable);
+    set_as_dirty();
+}
+
+void GLCanvas3D::enable_legend_texture(bool enable)
+{
+    m_gcode_viewer.enable_legend(enable);
+}
+
+void GLCanvas3D::enable_picking(bool enable)
+{
+    m_picking_enabled = enable;
+    m_selection.set_mode(Selection::Instance);
+}
+
+void GLCanvas3D::enable_moving(bool enable)
+{
+    m_moving_enabled = enable;
+}
+
+void GLCanvas3D::enable_gizmos(bool enable)
+{
+    m_gizmos.set_enabled(enable);
+}
+
+void GLCanvas3D::enable_selection(bool enable)
+{
+    m_selection.set_enabled(enable);
+}
+
+void GLCanvas3D::enable_main_toolbar(bool enable)
+{
+    m_main_toolbar.set_enabled(enable);
+}
+
+void GLCanvas3D::enable_undoredo_toolbar(bool enable)
+{
+    m_undoredo_toolbar.set_enabled(enable);
+}
+
+void GLCanvas3D::enable_dynamic_background(bool enable)
+{
+    m_dynamic_background_enabled = enable;
+}
+
+void GLCanvas3D::allow_multisample(bool allow)
+{
+    m_multisample_allowed = allow;
+}
+
+void GLCanvas3D::zoom_to_bed()
+{
+    BoundingBoxf3 box = m_bed.build_volume().bounding_volume();
+    box.min.z() = 0.0;
+    box.max.z() = 0.0;
+    _zoom_to_box(box);
+}
+
+void GLCanvas3D::zoom_to_volumes()
+{
+    m_apply_zoom_to_volumes_filter = true;
+    _zoom_to_box(volumes_bounding_box());
+    m_apply_zoom_to_volumes_filter = false;
+}
+
+void GLCanvas3D::zoom_to_selection()
+{
+    if (!m_selection.is_empty())
+        _zoom_to_box(m_selection.get_bounding_box());
+}
+
+void GLCanvas3D::zoom_to_gcode()
+{
+    _zoom_to_box(m_gcode_viewer.get_paths_bounding_box(), 1.05);
+}
+
+void GLCanvas3D::select_view(const std::string& direction)
+{
+    wxGetApp().plater()->get_camera().select_view(direction);
+    if (m_canvas != nullptr)
+        m_canvas->Refresh();
+}
+
+void GLCanvas3D::update_volumes_colors_by_extruder()
+{
+    if (m_config != nullptr)
+        m_volumes.update_colors_by_extruder(m_config);
+}
+
+void GLCanvas3D::render()
+{
+    if (m_in_render) {
+        // if called recursively, return
+        m_dirty = true;
+        return;
+    }
+
+    m_in_render = true;
+    Slic3r::ScopeGuard in_render_guard([this]() { m_in_render = false; });
+    (void)in_render_guard;
+
+    if (m_canvas == nullptr)
+        return;
+
+    // ensures this canvas is current and initialized
+    if (!_is_shown_on_screen() || !_set_current() || !wxGetApp().init_opengl())
+        return;
+
+    if (!is_initialized() && !init())
+        return;
+
+    if (!m_main_toolbar.is_enabled())
+        m_gcode_viewer.init();
+
+    if (! m_bed.build_volume().valid()) {
+        // this happens at startup when no data is still saved under <>\AppData\Roaming\Slic3rPE
+        post_event(SimpleEvent(EVT_GLCANVAS_UPDATE_BED_SHAPE));
+        return;
+    }
+
+#if ENABLE_ENVIRONMENT_MAP
+    if (wxGetApp().is_editor())
+        wxGetApp().plater()->init_environment_texture();
+#endif // ENABLE_ENVIRONMENT_MAP
+
+#if ENABLE_GLMODEL_STATISTICS
+    GLModel::reset_statistics_counters();
+#endif // ENABLE_GLMODEL_STATISTICS
+
+    const Size& cnv_size = get_canvas_size();
+    // Probably due to different order of events on Linux/GTK2, when one switched from 3D scene
+    // to preview, this was called before canvas had its final size. It reported zero width
+    // and the viewport was set incorrectly, leading to tripping glAsserts further down
+    // the road (in apply_projection). That's why the minimum size is forced to 10.
+    Camera& camera = wxGetApp().plater()->get_camera();
+    camera.apply_viewport(0, 0, std::max(10u, (unsigned int)cnv_size.get_width()), std::max(10u, (unsigned int)cnv_size.get_height()));
+
+    if (camera.requires_zoom_to_bed) {
+        zoom_to_bed();
+        _resize((unsigned int)cnv_size.get_width(), (unsigned int)cnv_size.get_height());
+        camera.requires_zoom_to_bed = false;
+    }
+
+#if !ENABLE_LEGACY_OPENGL_REMOVAL
+    camera.apply_view_matrix();
+#endif // !ENABLE_LEGACY_OPENGL_REMOVAL
+    camera.apply_projection(_max_bounding_box(true, true));
+
+#if !ENABLE_LEGACY_OPENGL_REMOVAL
+    GLfloat position_cam[4] = { 1.0f, 0.0f, 1.0f, 0.0f };
+    glsafe(::glLightfv(GL_LIGHT1, GL_POSITION, position_cam));
+    GLfloat position_top[4] = { -0.5f, -0.5f, 1.0f, 0.0f };
+    glsafe(::glLightfv(GL_LIGHT0, GL_POSITION, position_top));
+#endif // !ENABLE_LEGACY_OPENGL_REMOVAL
+
+    wxGetApp().imgui()->new_frame();
+
+    if (m_picking_enabled) {
+#if ENABLE_NEW_RECTANGLE_SELECTION
+        if (m_rectangle_selection.is_dragging() && !m_rectangle_selection.is_empty())
+#else
+        if (m_rectangle_selection.is_dragging())
+#endif // ENABLE_NEW_RECTANGLE_SELECTION
+            // picking pass using rectangle selection
+            _rectangular_selection_picking_pass();
+        else if (!m_volumes.empty())
+            // regular picking pass
+            _picking_pass();
+    }
+
+#if ENABLE_RENDER_PICKING_PASS
+    if (!m_picking_enabled || !m_show_picking_texture) {
+#endif // ENABLE_RENDER_PICKING_PASS
+
+    const bool is_looking_downward = camera.is_looking_downward();
+
+    // draw scene
+    glsafe(::glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT));
+    _render_background();
+
+    _render_objects(GLVolumeCollection::ERenderType::Opaque);
+    if (!m_main_toolbar.is_enabled())
+        _render_gcode();
+    _render_sla_slices();
+    _render_selection();
+    if (is_looking_downward)
+#if ENABLE_GL_SHADERS_ATTRIBUTES
+        _render_bed(camera.get_view_matrix(), camera.get_projection_matrix(), false, true);
+#else
+        _render_bed(false, true);
+#endif // ENABLE_GL_SHADERS_ATTRIBUTES
+    _render_objects(GLVolumeCollection::ERenderType::Transparent);
+
+    _render_sequential_clearance();
+#if ENABLE_RENDER_SELECTION_CENTER
+    _render_selection_center();
+#endif // ENABLE_RENDER_SELECTION_CENTER
+#if ENABLE_SHOW_TOOLPATHS_COG
+    if (!m_main_toolbar.is_enabled())
+        _render_gcode_cog();
+#endif // ENABLE_SHOW_TOOLPATHS_COG
+
+    // we need to set the mouse's scene position here because the depth buffer
+    // could be invalidated by the following gizmo render methods
+    // this position is used later into on_mouse() to drag the objects
+    if (m_picking_enabled)
+        m_mouse.scene_position = _mouse_to_3d(m_mouse.position.cast<coord_t>());
+
+    // sidebar hints need to be rendered before the gizmos because the depth buffer
+    // could be invalidated by the following gizmo render methods
+    _render_selection_sidebar_hints();
+    _render_current_gizmo();
+    if (!is_looking_downward)
+#if ENABLE_GL_SHADERS_ATTRIBUTES
+        _render_bed(camera.get_view_matrix(), camera.get_projection_matrix(), true, true);
+#else
+        _render_bed(true, true);
+#endif // ENABLE_GL_SHADERS_ATTRIBUTES
+#if ENABLE_RENDER_PICKING_PASS
+    }
+#endif // ENABLE_RENDER_PICKING_PASS
+
+#if ENABLE_SHOW_CAMERA_TARGET
+    _render_camera_target();
+#endif // ENABLE_SHOW_CAMERA_TARGET
+
+    if (m_picking_enabled && m_rectangle_selection.is_dragging())
+        m_rectangle_selection.render(*this);
+
+    // draw overlays
+    _render_overlays();
+
+    if (wxGetApp().plater()->is_render_statistic_dialog_visible()) {
+        ImGuiWrapper& imgui = *wxGetApp().imgui();
+        imgui.begin(std::string("Render statistics"), ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse);
+        imgui.text("FPS (SwapBuffers() calls per second):");
+        ImGui::SameLine();
+        imgui.text(std::to_string(m_render_stats.get_fps_and_reset_if_needed()));
+        ImGui::Separator();
+        imgui.text("Compressed textures:");
+        ImGui::SameLine();
+        imgui.text(OpenGLManager::are_compressed_textures_supported() ? "supported" : "not supported");
+        imgui.text("Max texture size:");
+        ImGui::SameLine();
+        imgui.text(std::to_string(OpenGLManager::get_gl_info().get_max_tex_size()));
+        imgui.end();
+    }
+
+#if ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW
+    if (wxGetApp().is_editor() && wxGetApp().plater()->is_view3D_shown())
+        wxGetApp().plater()->render_project_state_debug_window();
+#endif // ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW
+
+#if ENABLE_CAMERA_STATISTICS
+    camera.debug_render();
+#endif // ENABLE_CAMERA_STATISTICS
+#if ENABLE_GLMODEL_STATISTICS
+    GLModel::render_statistics();
+#endif // ENABLE_GLMODEL_STATISTICS
+
+    std::string tooltip;
+
+	// Negative coordinate means out of the window, likely because the window was deactivated.
+	// In that case the tooltip should be hidden.
+    if (m_mouse.position.x() >= 0. && m_mouse.position.y() >= 0.) {
+	    if (tooltip.empty())
+	        tooltip = m_layers_editing.get_tooltip(*this);
+
+	    if (tooltip.empty())
+	        tooltip = m_gizmos.get_tooltip();
+
+	    if (tooltip.empty())
+	        tooltip = m_main_toolbar.get_tooltip();
+
+	    if (tooltip.empty())
+	        tooltip = m_undoredo_toolbar.get_tooltip();
+
+	    if (tooltip.empty())
+            tooltip = wxGetApp().plater()->get_collapse_toolbar().get_tooltip();
+
+	    if (tooltip.empty())
+            tooltip = wxGetApp().plater()->get_view_toolbar().get_tooltip();
+    }
+
+    set_tooltip(tooltip);
+
+    if (m_tooltip_enabled)
+        m_tooltip.render(m_mouse.position, *this);
+
+    wxGetApp().plater()->get_mouse3d_controller().render_settings_dialog(*this);
+
+    wxGetApp().plater()->get_notification_manager()->render_notifications(*this, get_overlay_window_width());
+
+    wxGetApp().imgui()->render();
+
+    m_canvas->SwapBuffers();
+    m_render_stats.increment_fps_counter();
+}
+
+void GLCanvas3D::render_thumbnail(ThumbnailData& thumbnail_data, unsigned int w, unsigned int h, const ThumbnailsParams& thumbnail_params, Camera::EType camera_type)
+{
+    render_thumbnail(thumbnail_data, w, h, thumbnail_params, m_volumes, camera_type);
+}
+
+void GLCanvas3D::render_thumbnail(ThumbnailData& thumbnail_data, unsigned int w, unsigned int h, const ThumbnailsParams& thumbnail_params, const GLVolumeCollection& volumes, Camera::EType camera_type)
+{
+    switch (OpenGLManager::get_framebuffers_type())
+    {
+    case OpenGLManager::EFramebufferType::Arb: { _render_thumbnail_framebuffer(thumbnail_data, w, h, thumbnail_params, volumes, camera_type); break; }
+    case OpenGLManager::EFramebufferType::Ext: { _render_thumbnail_framebuffer_ext(thumbnail_data, w, h, thumbnail_params, volumes, camera_type); break; }
+    default: { _render_thumbnail_legacy(thumbnail_data, w, h, thumbnail_params, volumes, camera_type); break; }
+    }
+}
+
+void GLCanvas3D::select_all()
+{
+    m_selection.add_all();
+    m_dirty = true;
+}
+
+void GLCanvas3D::deselect_all()
+{
+    m_selection.remove_all();
+    wxGetApp().obj_manipul()->set_dirty();
+    m_gizmos.reset_all_states();
+    m_gizmos.update_data();
+    post_event(SimpleEvent(EVT_GLCANVAS_OBJECT_SELECT));
+}
+
+void GLCanvas3D::delete_selected()
+{
+    m_selection.erase();
+}
+
+void GLCanvas3D::ensure_on_bed(unsigned int object_idx, bool allow_negative_z)
+{
+    if (allow_negative_z)
+        return;
+
+    typedef std::map<std::pair<int, int>, double> InstancesToZMap;
+    InstancesToZMap instances_min_z;
+
+    for (GLVolume* volume : m_volumes.volumes) {
+        if (volume->object_idx() == (int)object_idx && !volume->is_modifier) {
+            double min_z = volume->transformed_convex_hull_bounding_box().min.z();
+            std::pair<int, int> instance = std::make_pair(volume->object_idx(), volume->instance_idx());
+            InstancesToZMap::iterator it = instances_min_z.find(instance);
+            if (it == instances_min_z.end())
+                it = instances_min_z.insert(InstancesToZMap::value_type(instance, DBL_MAX)).first;
+
+            it->second = std::min(it->second, min_z);
+        }
+    }
+
+    for (GLVolume* volume : m_volumes.volumes) {
+        std::pair<int, int> instance = std::make_pair(volume->object_idx(), volume->instance_idx());
+        InstancesToZMap::iterator it = instances_min_z.find(instance);
+        if (it != instances_min_z.end())
+            volume->set_instance_offset(Z, volume->get_instance_offset(Z) - it->second);
+    }
+}
+
+
+const std::vector<double>& GLCanvas3D::get_gcode_layers_zs() const
+{
+    return m_gcode_viewer.get_layers_zs();
+}
+
+std::vector<double> GLCanvas3D::get_volumes_print_zs(bool active_only) const
+{
+    return m_volumes.get_current_print_zs(active_only);
+}
+
+void GLCanvas3D::set_gcode_options_visibility_from_flags(unsigned int flags)
+{
+    m_gcode_viewer.set_options_visibility_from_flags(flags);
+}
+
+void GLCanvas3D::set_toolpath_role_visibility_flags(unsigned int flags)
+{
+    m_gcode_viewer.set_toolpath_role_visibility_flags(flags);
+}
+
+void GLCanvas3D::set_toolpath_view_type(GCodeViewer::EViewType type)
+{
+    m_gcode_viewer.set_view_type(type);
+}
+
+void GLCanvas3D::set_volumes_z_range(const std::array<double, 2>& range)
+{
+    m_volumes.set_range(range[0] - 1e-6, range[1] + 1e-6);
+}
+
+void GLCanvas3D::set_toolpaths_z_range(const std::array<unsigned int, 2>& range)
+{
+    if (m_gcode_viewer.has_data())
+        m_gcode_viewer.set_layers_z_range(range);
+}
+
+std::vector<int> GLCanvas3D::load_object(const ModelObject& model_object, int obj_idx, std::vector<int> instance_idxs)
+{
+    if (instance_idxs.empty()) {
+        for (unsigned int i = 0; i < model_object.instances.size(); ++i) {
+            instance_idxs.emplace_back(i);
+        }
+    }
+#if ENABLE_LEGACY_OPENGL_REMOVAL
+    return m_volumes.load_object(&model_object, obj_idx, instance_idxs);
+#else
+    return m_volumes.load_object(&model_object, obj_idx, instance_idxs, m_initialized);
+#endif // ENABLE_LEGACY_OPENGL_REMOVAL
+}
+
+std::vector<int> GLCanvas3D::load_object(const Model& model, int obj_idx)
+{
+    if (0 <= obj_idx && obj_idx < (int)model.objects.size()) {
+        const ModelObject* model_object = model.objects[obj_idx];
+        if (model_object != nullptr)
+            return load_object(*model_object, obj_idx, std::vector<int>());
+    }
+
+    return std::vector<int>();
+}
+
+void GLCanvas3D::mirror_selection(Axis axis)
+{
+    m_selection.mirror(axis);
+    do_mirror(L("Mirror Object"));
+    wxGetApp().obj_manipul()->set_dirty();
+}
+
+// Reload the 3D scene of 
+// 1) Model / ModelObjects / ModelInstances / ModelVolumes
+// 2) Print bed
+// 3) SLA support meshes for their respective ModelObjects / ModelInstances
+// 4) Wipe tower preview
+// 5) Out of bed collision status & message overlay (texture)
+void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_refresh)
+{
+    if (m_canvas == nullptr || m_config == nullptr || m_model == nullptr)
+        return;
+
+    if (!m_initialized)
+        return;
+    
+    _set_current();
+
+    m_hover_volume_idxs.clear();
+
+    struct ModelVolumeState {
+        ModelVolumeState(const GLVolume* volume) :
+            model_volume(nullptr), geometry_id(volume->geometry_id), volume_idx(-1) {}
+        ModelVolumeState(const ModelVolume* model_volume, const ObjectID& instance_id, const GLVolume::CompositeID& composite_id) :
+            model_volume(model_volume), geometry_id(std::make_pair(model_volume->id().id, instance_id.id)), composite_id(composite_id), volume_idx(-1) {}
+        ModelVolumeState(const ObjectID& volume_id, const ObjectID& instance_id) :
+            model_volume(nullptr), geometry_id(std::make_pair(volume_id.id, instance_id.id)), volume_idx(-1) {}
+        bool new_geometry() const { return this->volume_idx == size_t(-1); }
+        const ModelVolume* model_volume;
+        // ObjectID of ModelVolume + ObjectID of ModelInstance
+        // or timestamp of an SLAPrintObjectStep + ObjectID of ModelInstance
+        std::pair<size_t, size_t>   geometry_id;
+        GLVolume::CompositeID       composite_id;
+        // Volume index in the new GLVolume vector.
+        size_t                      volume_idx;
+    };
+    std::vector<ModelVolumeState> model_volume_state;
+    std::vector<ModelVolumeState> aux_volume_state;
+
+    struct GLVolumeState {
+        GLVolumeState() :
+            volume_idx(size_t(-1)) {}
+        GLVolumeState(const GLVolume* volume, unsigned int volume_idx) :
+            composite_id(volume->composite_id), volume_idx(volume_idx) {}
+        GLVolumeState(const GLVolume::CompositeID &composite_id) :
+            composite_id(composite_id), volume_idx(size_t(-1)) {}
+
+        GLVolume::CompositeID       composite_id;
+        // Volume index in the old GLVolume vector.
+        size_t                      volume_idx;
+    };
+
+    // SLA steps to pull the preview meshes for.
+	typedef std::array<SLAPrintObjectStep, 3> SLASteps;
+    SLASteps sla_steps = { slaposDrillHoles, slaposSupportTree, slaposPad };
+    struct SLASupportState {
+        std::array<PrintStateBase::StateWithTimeStamp, std::tuple_size<SLASteps>::value> step;
+    };
+    // State of the sla_steps for all SLAPrintObjects.
+    std::vector<SLASupportState>   sla_support_state;
+
+    std::vector<size_t> instance_ids_selected;
+    std::vector<size_t> map_glvolume_old_to_new(m_volumes.volumes.size(), size_t(-1));
+    std::vector<GLVolumeState> deleted_volumes;
+    std::vector<GLVolume*> glvolumes_new;
+    glvolumes_new.reserve(m_volumes.volumes.size());
+    auto model_volume_state_lower = [](const ModelVolumeState& m1, const ModelVolumeState& m2) { return m1.geometry_id < m2.geometry_id; };
+
+    m_reload_delayed = !m_canvas->IsShown() && !refresh_immediately && !force_full_scene_refresh;
+
+    PrinterTechnology printer_technology = current_printer_technology();
+    int               volume_idx_wipe_tower_old = -1;
+
+    // Release invalidated volumes to conserve GPU memory in case of delayed refresh (see m_reload_delayed).
+    // First initialize model_volumes_new_sorted & model_instances_new_sorted.
+    for (int object_idx = 0; object_idx < (int)m_model->objects.size(); ++object_idx) {
+        const ModelObject* model_object = m_model->objects[object_idx];
+        for (int instance_idx = 0; instance_idx < (int)model_object->instances.size(); ++instance_idx) {
+            const ModelInstance* model_instance = model_object->instances[instance_idx];
+            for (int volume_idx = 0; volume_idx < (int)model_object->volumes.size(); ++volume_idx) {
+                const ModelVolume* model_volume = model_object->volumes[volume_idx];
+                model_volume_state.emplace_back(model_volume, model_instance->id(), GLVolume::CompositeID(object_idx, volume_idx, instance_idx));
+            }
+        }
+    }
+    if (printer_technology == ptSLA) {
+        const SLAPrint* sla_print = this->sla_print();
+#ifndef NDEBUG
+        // Verify that the SLAPrint object is synchronized with m_model.
+        check_model_ids_equal(*m_model, sla_print->model());
+#endif /* NDEBUG */
+        sla_support_state.reserve(sla_print->objects().size());
+        for (const SLAPrintObject* print_object : sla_print->objects()) {
+            SLASupportState state;
+            for (size_t istep = 0; istep < sla_steps.size(); ++istep) {
+                state.step[istep] = print_object->step_state_with_timestamp(sla_steps[istep]);
+                if (state.step[istep].state == PrintStateBase::DONE) {
+                    if (!print_object->has_mesh(sla_steps[istep]))
+                        // Consider the DONE step without a valid mesh as invalid for the purpose
+                        // of mesh visualization.
+                        state.step[istep].state = PrintStateBase::INVALID;
+                    else if (sla_steps[istep] != slaposDrillHoles)
+                        for (const ModelInstance* model_instance : print_object->model_object()->instances)
+                            // Only the instances, which are currently printable, will have the SLA support structures kept.
+                            // The instances outside the print bed will have the GLVolumes of their support structures released.
+                            if (model_instance->is_printable())
+                                aux_volume_state.emplace_back(state.step[istep].timestamp, model_instance->id());
+                }
+            }
+            sla_support_state.emplace_back(state);
+        }
+    }
+    std::sort(model_volume_state.begin(), model_volume_state.end(), model_volume_state_lower);
+    std::sort(aux_volume_state.begin(), aux_volume_state.end(), model_volume_state_lower);
+    // Release all ModelVolume based GLVolumes not found in the current Model. Find the GLVolume of a hollowed mesh.
+    for (size_t volume_id = 0; volume_id < m_volumes.volumes.size(); ++volume_id) {
+        GLVolume* volume = m_volumes.volumes[volume_id];
+        ModelVolumeState  key(volume);
+        ModelVolumeState* mvs = nullptr;
+        if (volume->volume_idx() < 0) {
+            auto it = std::lower_bound(aux_volume_state.begin(), aux_volume_state.end(), key, model_volume_state_lower);
+            if (it != aux_volume_state.end() && it->geometry_id == key.geometry_id)
+                // This can be an SLA support structure that should not be rendered (in case someone used undo
+                // to revert to before it was generated). We only reuse the volume if that's not the case.
+                if (m_model->objects[volume->composite_id.object_id]->sla_points_status != sla::PointsStatus::NoPoints)
+                    mvs = &(*it);
+        }
+        else {
+            auto it = std::lower_bound(model_volume_state.begin(), model_volume_state.end(), key, model_volume_state_lower);
+            if (it != model_volume_state.end() && it->geometry_id == key.geometry_id)
+                mvs = &(*it);
+        }
+        // Emplace instance ID of the volume. Both the aux volumes and model volumes share the same instance ID.
+        // The wipe tower has its own wipe_tower_instance_id().
+        if (m_selection.contains_volume(volume_id))
+            instance_ids_selected.emplace_back(volume->geometry_id.second);
+        if (mvs == nullptr || force_full_scene_refresh) {
+            // This GLVolume will be released.
+            if (volume->is_wipe_tower) {
+                // There is only one wipe tower.
+                assert(volume_idx_wipe_tower_old == -1);
+                volume_idx_wipe_tower_old = (int)volume_id;
+            }
+            if (!m_reload_delayed) {
+                deleted_volumes.emplace_back(volume, volume_id);
+                delete volume;
+            }
+        }
+        else {
+            // This GLVolume will be reused.
+            volume->set_sla_shift_z(0.0);
+            map_glvolume_old_to_new[volume_id] = glvolumes_new.size();
+            mvs->volume_idx = glvolumes_new.size();
+            glvolumes_new.emplace_back(volume);
+            // Update color of the volume based on the current extruder.
+            if (mvs->model_volume != nullptr) {
+                int extruder_id = mvs->model_volume->extruder_id();
+                if (extruder_id != -1)
+                    volume->extruder_id = extruder_id;
+
+                volume->is_modifier = !mvs->model_volume->is_model_part();
+                volume->set_color(color_from_model_volume(*mvs->model_volume));
+                // force update of render_color alpha channel 
+                volume->set_render_color(volume->color.is_transparent());
+
+                // updates volumes transformations
+                volume->set_instance_transformation(mvs->model_volume->get_object()->instances[mvs->composite_id.instance_id]->get_transformation());
+                volume->set_volume_transformation(mvs->model_volume->get_transformation());
+
+                // updates volumes convex hull
+                if (mvs->model_volume->is_model_part() && ! volume->convex_hull())
+                    // Model volume was likely changed from modifier or support blocker / enforcer to a model part.
+                    // Only model parts require convex hulls.
+                    volume->set_convex_hull(mvs->model_volume->get_convex_hull_shared_ptr());
+            }
+        }
+    }
+    sort_remove_duplicates(instance_ids_selected);
+    auto deleted_volumes_lower = [](const GLVolumeState &v1, const GLVolumeState &v2) { return v1.composite_id < v2.composite_id; };
+    std::sort(deleted_volumes.begin(), deleted_volumes.end(), deleted_volumes_lower);
+
+    if (m_reload_delayed)
+        return;
+
+    bool update_object_list = false;
+    if (m_volumes.volumes != glvolumes_new)
+		update_object_list = true;
+    m_volumes.volumes = std::move(glvolumes_new);
+    for (unsigned int obj_idx = 0; obj_idx < (unsigned int)m_model->objects.size(); ++ obj_idx) {
+        const ModelObject &model_object = *m_model->objects[obj_idx];
+        for (int volume_idx = 0; volume_idx < (int)model_object.volumes.size(); ++ volume_idx) {
+			const ModelVolume &model_volume = *model_object.volumes[volume_idx];
+            for (int instance_idx = 0; instance_idx < (int)model_object.instances.size(); ++ instance_idx) {
+				const ModelInstance &model_instance = *model_object.instances[instance_idx];
+				ModelVolumeState key(model_volume.id(), model_instance.id());
+				auto it = std::lower_bound(model_volume_state.begin(), model_volume_state.end(), key, model_volume_state_lower);
+				assert(it != model_volume_state.end() && it->geometry_id == key.geometry_id);
+                if (it->new_geometry()) {
+                    // New volume.
+                    auto it_old_volume = std::lower_bound(deleted_volumes.begin(), deleted_volumes.end(), GLVolumeState(it->composite_id), deleted_volumes_lower);
+                    if (it_old_volume != deleted_volumes.end() && it_old_volume->composite_id == it->composite_id)
+                        // If a volume changed its ObjectID, but it reuses a GLVolume's CompositeID, maintain its selection.
+                        map_glvolume_old_to_new[it_old_volume->volume_idx] = m_volumes.volumes.size();
+                    // Note the index of the loaded volume, so that we can reload the main model GLVolume with the hollowed mesh
+                    // later in this function.
+                    it->volume_idx = m_volumes.volumes.size();
+#if ENABLE_LEGACY_OPENGL_REMOVAL
+                    m_volumes.load_object_volume(&model_object, obj_idx, volume_idx, instance_idx);
+#else
+                    m_volumes.load_object_volume(&model_object, obj_idx, volume_idx, instance_idx, m_initialized);
+#endif // ENABLE_LEGACY_OPENGL_REMOVAL
+                    m_volumes.volumes.back()->geometry_id = key.geometry_id;
+                    update_object_list = true;
+                } else {
+					// Recycling an old GLVolume.
+					GLVolume &existing_volume = *m_volumes.volumes[it->volume_idx];
+                    assert(existing_volume.geometry_id == key.geometry_id);
+					// Update the Object/Volume/Instance indices into the current Model.
+					if (existing_volume.composite_id != it->composite_id) {
+						existing_volume.composite_id = it->composite_id;
+						update_object_list = true;
+					}
+                }
+            }
+        }
+    }
+    if (printer_technology == ptSLA) {
+        size_t idx = 0;
+        const SLAPrint *sla_print = this->sla_print();
+		std::vector<double> shift_zs(m_model->objects.size(), 0);
+        double relative_correction_z = sla_print->relative_correction().z();
+        if (relative_correction_z <= EPSILON)
+            relative_correction_z = 1.;
+		for (const SLAPrintObject *print_object : sla_print->objects()) {
+            SLASupportState   &state        = sla_support_state[idx ++];
+            const ModelObject *model_object = print_object->model_object();
+            // Find an index of the ModelObject
+            int object_idx;
+            // There may be new SLA volumes added to the scene for this print_object.
+            // Find the object index of this print_object in the Model::objects list.
+            auto it = std::find(sla_print->model().objects.begin(), sla_print->model().objects.end(), model_object);
+            assert(it != sla_print->model().objects.end());
+			object_idx = it - sla_print->model().objects.begin();
+			// Cache the Z offset to be applied to all volumes with this object_idx.
+			shift_zs[object_idx] = print_object->get_current_elevation() / relative_correction_z;
+            // Collect indices of this print_object's instances, for which the SLA support meshes are to be added to the scene.
+            // pairs of <instance_idx, print_instance_idx>
+			std::vector<std::pair<size_t, size_t>> instances[std::tuple_size<SLASteps>::value];
+            for (size_t print_instance_idx = 0; print_instance_idx < print_object->instances().size(); ++ print_instance_idx) {
+                const SLAPrintObject::Instance &instance = print_object->instances()[print_instance_idx];
+                // Find index of ModelInstance corresponding to this SLAPrintObject::Instance.
+				auto it = std::find_if(model_object->instances.begin(), model_object->instances.end(), 
+                    [&instance](const ModelInstance *mi) { return mi->id() == instance.instance_id; });
+                assert(it != model_object->instances.end());
+                int instance_idx = it - model_object->instances.begin();
+                for (size_t istep = 0; istep < sla_steps.size(); ++ istep)
+                    if (sla_steps[istep] == slaposDrillHoles) {
+                    	// Hollowing is a special case, where the mesh from the backend is being loaded into the 1st volume of an instance,
+                    	// not into its own GLVolume.
+                        // There shall always be such a GLVolume allocated.
+                        ModelVolumeState key(model_object->volumes.front()->id(), instance.instance_id);
+                        auto it = std::lower_bound(model_volume_state.begin(), model_volume_state.end(), key, model_volume_state_lower);
+                        assert(it != model_volume_state.end() && it->geometry_id == key.geometry_id);
+                        assert(!it->new_geometry());
+                        GLVolume &volume = *m_volumes.volumes[it->volume_idx];
+                        if (! volume.offsets.empty() && state.step[istep].timestamp != volume.offsets.front()) {
+                        	// The backend either produced a new hollowed mesh, or it invalidated the one that the front end has seen.
+#if ENABLE_LEGACY_OPENGL_REMOVAL
+                            volume.model.reset();
+#else
+                            volume.indexed_vertex_array.release_geometry();
+#endif // ENABLE_LEGACY_OPENGL_REMOVAL
+                            if (state.step[istep].state == PrintStateBase::DONE) {
+                                TriangleMesh mesh = print_object->get_mesh(slaposDrillHoles);
+	                            assert(! mesh.empty());
+
+                                // sla_trafo does not contain volume trafo. To get a mesh to create
+                                // a new volume from, we have to apply vol trafo inverse separately.
+                                const ModelObject& mo = *m_model->objects[volume.object_idx()];
+                                Transform3d trafo = sla_print->sla_trafo(mo)
+                                    * mo.volumes.front()->get_transformation().get_matrix();
+                                mesh.transform(trafo.inverse());
+#if ENABLE_SMOOTH_NORMALS
+#if ENABLE_LEGACY_OPENGL_REMOVAL
+                                volume.model.init_from(mesh, true);
+#else
+                                volume.indexed_vertex_array.load_mesh(mesh, true);
+#endif // ENABLE_LEGACY_OPENGL_REMOVAL
+#else
+#if ENABLE_LEGACY_OPENGL_REMOVAL
+                                volume.model.init_from(mesh);
+#else
+                                volume.indexed_vertex_array.load_mesh(mesh);
+#endif // ENABLE_LEGACY_OPENGL_REMOVAL
+#endif // ENABLE_SMOOTH_NORMALS
+                            }
+                            else {
+	                        	// Reload the original volume.
+#if ENABLE_SMOOTH_NORMALS
+#if ENABLE_LEGACY_OPENGL_REMOVAL
+                                volume.model.init_from(m_model->objects[volume.object_idx()]->volumes[volume.volume_idx()]->mesh(), true);
+#else
+                                volume.indexed_vertex_array.load_mesh(m_model->objects[volume.object_idx()]->volumes[volume.volume_idx()]->mesh(), true);
+#endif // ENABLE_LEGACY_OPENGL_REMOVAL
+#else
+#if ENABLE_LEGACY_OPENGL_REMOVAL
+                                volume.model.init_from(m_model->objects[volume.object_idx()]->volumes[volume.volume_idx()]->mesh());
+#else
+                                volume.indexed_vertex_array.load_mesh(m_model->objects[volume.object_idx()]->volumes[volume.volume_idx()]->mesh());
+#endif // ENABLE_LEGACY_OPENGL_REMOVAL
+#endif // ENABLE_SMOOTH_NORMALS
+                            }
+#if !ENABLE_LEGACY_OPENGL_REMOVAL
+                            volume.finalize_geometry(true);
+#endif // !ENABLE_LEGACY_OPENGL_REMOVAL
+                        }
+                    	//FIXME it is an ugly hack to write the timestamp into the "offsets" field to not have to add another member variable
+                    	// to the GLVolume. We should refactor GLVolume significantly, so that the GLVolume will not contain member variables
+                    	// of various concenrs (model vs. 3D print path).
+                    	volume.offsets = { state.step[istep].timestamp };
+                    }
+                    else if (state.step[istep].state == PrintStateBase::DONE) {
+                        // Check whether there is an existing auxiliary volume to be updated, or a new auxiliary volume to be created.
+						ModelVolumeState key(state.step[istep].timestamp, instance.instance_id.id);
+						auto it = std::lower_bound(aux_volume_state.begin(), aux_volume_state.end(), key, model_volume_state_lower);
+						assert(it != aux_volume_state.end() && it->geometry_id == key.geometry_id);
+                    	if (it->new_geometry()) {
+                            // This can be an SLA support structure that should not be rendered (in case someone used undo
+                            // to revert to before it was generated). If that's the case, we should not generate anything.
+                            if (model_object->sla_points_status != sla::PointsStatus::NoPoints)
+                                instances[istep].emplace_back(std::pair<size_t, size_t>(instance_idx, print_instance_idx));
+                            else
+                                shift_zs[object_idx] = 0.;
+                        }
+                        else {
+                            // Recycling an old GLVolume. Update the Object/Instance indices into the current Model.
+                            m_volumes.volumes[it->volume_idx]->composite_id = GLVolume::CompositeID(object_idx, m_volumes.volumes[it->volume_idx]->volume_idx(), instance_idx);
+                            m_volumes.volumes[it->volume_idx]->set_instance_transformation(model_object->instances[instance_idx]->get_transformation());
+                        }
+                    }
+            }
+
+            for (size_t istep = 0; istep < sla_steps.size(); ++istep)
+                if (!instances[istep].empty())
+#if ENABLE_LEGACY_OPENGL_REMOVAL
+                    m_volumes.load_object_auxiliary(print_object, object_idx, instances[istep], sla_steps[istep], state.step[istep].timestamp);
+#else
+                    m_volumes.load_object_auxiliary(print_object, object_idx, instances[istep], sla_steps[istep], state.step[istep].timestamp, m_initialized);
+#endif // ENABLE_LEGACY_OPENGL_REMOVAL
+        }
+
+		// Shift-up all volumes of the object so that it has the right elevation with respect to the print bed
+		for (GLVolume* volume : m_volumes.volumes)
+			if (volume->object_idx() < (int)m_model->objects.size() && m_model->objects[volume->object_idx()]->instances[volume->instance_idx()]->is_printable())
+				volume->set_sla_shift_z(shift_zs[volume->object_idx()]);
+    }
+
+    if (printer_technology == ptFFF && m_config->has("nozzle_diameter")) {
+        // Should the wipe tower be visualized ?
+        unsigned int extruders_count = (unsigned int)dynamic_cast<const ConfigOptionFloats*>(m_config->option("nozzle_diameter"))->values.size();
+
+        bool wt = dynamic_cast<const ConfigOptionBool*>(m_config->option("wipe_tower"))->value;
+        bool co = dynamic_cast<const ConfigOptionBool*>(m_config->option("complete_objects"))->value;
+
+        if (extruders_count > 1 && wt && !co) {
+            // Height of a print (Show at least a slab)
+            double height = std::max(m_model->bounding_box().max(2), 10.0);
+
+            float x = dynamic_cast<const ConfigOptionFloat*>(m_config->option("wipe_tower_x"))->value;
+            float y = dynamic_cast<const ConfigOptionFloat*>(m_config->option("wipe_tower_y"))->value;
+            float w = dynamic_cast<const ConfigOptionFloat*>(m_config->option("wipe_tower_width"))->value;
+            float a = dynamic_cast<const ConfigOptionFloat*>(m_config->option("wipe_tower_rotation_angle"))->value;
+
+            const Print *print = m_process->fff_print();
+
+            float depth = print->wipe_tower_data(extruders_count).depth;
+            float brim_width = print->wipe_tower_data(extruders_count).brim_width;
+
+#if ENABLE_LEGACY_OPENGL_REMOVAL
+#if ENABLE_WIPETOWER_OBJECTID_1000_REMOVAL
+            int volume_idx_wipe_tower_new = m_volumes.load_wipe_tower_preview(
+                x, y, w, depth, (float)height, a, !print->is_step_done(psWipeTower),
+                brim_width);
+#else
+            int volume_idx_wipe_tower_new = m_volumes.load_wipe_tower_preview(
+                1000, x, y, w, depth, (float)height, a, !print->is_step_done(psWipeTower),
+                brim_width);
+#endif // ENABLE_WIPETOWER_OBJECTID_1000_REMOVAL
+#else
+#if ENABLE_WIPETOWER_OBJECTID_1000_REMOVAL
+            int volume_idx_wipe_tower_new = m_volumes.load_wipe_tower_preview(
+                x, y, w, depth, (float)height, a, !print->is_step_done(psWipeTower),
+                brim_width, m_initialized);
+#else
+            int volume_idx_wipe_tower_new = m_volumes.load_wipe_tower_preview(
+                1000, x, y, w, depth, (float)height, a, !print->is_step_done(psWipeTower),
+                brim_width, m_initialized);
+#endif // ENABLE_WIPETOWER_OBJECTID_1000_REMOVAL
+#endif // ENABLE_LEGACY_OPENGL_REMOVAL
+            if (volume_idx_wipe_tower_old != -1)
+                map_glvolume_old_to_new[volume_idx_wipe_tower_old] = volume_idx_wipe_tower_new;
+        }
+    }
+
+    update_volumes_colors_by_extruder();
+	// Update selection indices based on the old/new GLVolumeCollection.
+    if (m_selection.get_mode() == Selection::Instance)
+        m_selection.instances_changed(instance_ids_selected);
+    else
+        m_selection.volumes_changed(map_glvolume_old_to_new);
+
+    m_gizmos.update_data();
+    m_gizmos.refresh_on_off_state();
+
+    // Update the toolbar
+	if (update_object_list)
+		post_event(SimpleEvent(EVT_GLCANVAS_OBJECT_SELECT));
+
+    // checks for geometry outside the print volume to render it accordingly
+    if (!m_volumes.empty()) {
+        ModelInstanceEPrintVolumeState state;
+        const bool contained_min_one = m_volumes.check_outside_state(m_bed.build_volume(), &state);
+        const bool partlyOut = (state == ModelInstanceEPrintVolumeState::ModelInstancePVS_Partly_Outside);
+        const bool fullyOut = (state == ModelInstanceEPrintVolumeState::ModelInstancePVS_Fully_Outside);
+
+        _set_warning_notification(EWarning::ObjectClashed, partlyOut);
+        _set_warning_notification(EWarning::ObjectOutside, fullyOut);
+        if (printer_technology != ptSLA || !contained_min_one)
+            _set_warning_notification(EWarning::SlaSupportsOutside, false);
+
+        post_event(Event<bool>(EVT_GLCANVAS_ENABLE_ACTION_BUTTONS, 
+                               contained_min_one && !m_model->objects.empty() && !partlyOut));
+    }
+    else {
+        _set_warning_notification(EWarning::ObjectOutside, false);
+        _set_warning_notification(EWarning::ObjectClashed, false);
+        _set_warning_notification(EWarning::SlaSupportsOutside, false);
+        post_event(Event<bool>(EVT_GLCANVAS_ENABLE_ACTION_BUTTONS, false));
+    }
+
+    refresh_camera_scene_box();
+
+    if (m_selection.is_empty()) {
+        // If no object is selected, deactivate the active gizmo, if any
+        // Otherwise it may be shown after cleaning the scene (if it was active while the objects were deleted)
+        m_gizmos.reset_all_states();
+
+        // If no object is selected, reset the objects manipulator on the sidebar
+        // to force a reset of its cache
+        auto manip = wxGetApp().obj_manipul();
+        if (manip != nullptr)
+            manip->set_dirty();
+    }
+
+    // and force this canvas to be redrawn.
+    m_dirty = true;
+}
+
+#if !ENABLE_LEGACY_OPENGL_REMOVAL
+static void reserve_new_volume_finalize_old_volume(GLVolume& vol_new, GLVolume& vol_old, bool gl_initialized, size_t prealloc_size = VERTEX_BUFFER_RESERVE_SIZE)
+{
+    // Assign the large pre-allocated buffers to the new GLVolume.
+	vol_new.indexed_vertex_array = std::move(vol_old.indexed_vertex_array);
+	// Copy the content back to the old GLVolume.
+	vol_old.indexed_vertex_array = vol_new.indexed_vertex_array;
+	// Clear the buffers, but keep them pre-allocated.
+	vol_new.indexed_vertex_array.clear();
+	// Just make sure that clear did not clear the reserved memory.
+	// Reserving number of vertices (3x position + 3x color)
+	vol_new.indexed_vertex_array.reserve(prealloc_size / 6);
+	// Finalize the old geometry, possibly move data to the graphics card.
+	vol_old.finalize_geometry(gl_initialized);
+}
+#endif // !ENABLE_LEGACY_OPENGL_REMOVAL
+
+void GLCanvas3D::load_gcode_preview(const GCodeProcessorResult& gcode_result, const std::vector<std::string>& str_tool_colors)
+{
+#if ENABLE_LEGACY_OPENGL_REMOVAL
+    m_gcode_viewer.load(gcode_result, *this->fff_print());
+#else
+    m_gcode_viewer.load(gcode_result, *this->fff_print(), m_initialized);
+#endif // ENABLE_LEGACY_OPENGL_REMOVAL
+
+    if (wxGetApp().is_editor()) {
+        m_gcode_viewer.update_shells_color_by_extruder(m_config);
+        _set_warning_notification_if_needed(EWarning::ToolpathOutside);
+    }
+
+    m_gcode_viewer.refresh(gcode_result, str_tool_colors);
+    set_as_dirty();
+    request_extra_frame();
+}
+
+#if ENABLE_PREVIEW_LAYOUT
+void GLCanvas3D::refresh_gcode_preview_render_paths(bool keep_sequential_current_first, bool keep_sequential_current_last)
+{
+    m_gcode_viewer.refresh_render_paths(keep_sequential_current_first, keep_sequential_current_last);
+    set_as_dirty();
+    request_extra_frame();
+}
+#else
+void GLCanvas3D::refresh_gcode_preview_render_paths()
+{
+    m_gcode_viewer.refresh_render_paths();
+    set_as_dirty();
+    request_extra_frame();
+}
+#endif // ENABLE_PREVIEW_LAYOUT
+
+void GLCanvas3D::load_sla_preview()
+{
+    const SLAPrint* print = sla_print();
+    if (m_canvas != nullptr && print != nullptr) {
+        _set_current();
+	    // Release OpenGL data before generating new data.
+	    reset_volumes();
+        _load_sla_shells();
+        _update_sla_shells_outside_state();
+        _set_warning_notification_if_needed(EWarning::SlaSupportsOutside);
+    }
+}
+
+void GLCanvas3D::load_preview(const std::vector<std::string>& str_tool_colors, const std::vector<CustomGCode::Item>& color_print_values)
+{
+    const Print *print = this->fff_print();
+    if (print == nullptr)
+        return;
+
+    _set_current();
+
+    // Release OpenGL data before generating new data.
+    this->reset_volumes();
+
+    const BuildVolume &build_volume = m_bed.build_volume();
+    _load_print_toolpaths(build_volume);
+    _load_wipe_tower_toolpaths(build_volume, str_tool_colors);
+    for (const PrintObject* object : print->objects())
+        _load_print_object_toolpaths(*object, build_volume, str_tool_colors, color_print_values);
+
+    _set_warning_notification_if_needed(EWarning::ToolpathOutside);
+}
+
+void GLCanvas3D::bind_event_handlers()
+{
+    if (m_canvas != nullptr) {
+        m_canvas->Bind(wxEVT_SIZE, &GLCanvas3D::on_size, this);
+        m_canvas->Bind(wxEVT_IDLE, &GLCanvas3D::on_idle, this);
+        m_canvas->Bind(wxEVT_CHAR, &GLCanvas3D::on_char, this);
+        m_canvas->Bind(wxEVT_KEY_DOWN, &GLCanvas3D::on_key, this);
+        m_canvas->Bind(wxEVT_KEY_UP, &GLCanvas3D::on_key, this);
+        m_canvas->Bind(wxEVT_MOUSEWHEEL, &GLCanvas3D::on_mouse_wheel, this);
+        m_canvas->Bind(wxEVT_TIMER, &GLCanvas3D::on_timer, this);
+        m_canvas->Bind(EVT_GLCANVAS_RENDER_TIMER, &GLCanvas3D::on_render_timer, this);
+        m_toolbar_highlighter.set_timer_owner(m_canvas, 0);
+        m_canvas->Bind(EVT_GLCANVAS_TOOLBAR_HIGHLIGHTER_TIMER, [this](wxTimerEvent&) { m_toolbar_highlighter.blink(); });
+        m_gizmo_highlighter.set_timer_owner(m_canvas, 0);
+        m_canvas->Bind(EVT_GLCANVAS_GIZMO_HIGHLIGHTER_TIMER, [this](wxTimerEvent&) { m_gizmo_highlighter.blink(); });
+        m_canvas->Bind(wxEVT_LEFT_DOWN, &GLCanvas3D::on_mouse, this);
+        m_canvas->Bind(wxEVT_LEFT_UP, &GLCanvas3D::on_mouse, this);
+        m_canvas->Bind(wxEVT_MIDDLE_DOWN, &GLCanvas3D::on_mouse, this);
+        m_canvas->Bind(wxEVT_MIDDLE_UP, &GLCanvas3D::on_mouse, this);
+        m_canvas->Bind(wxEVT_RIGHT_DOWN, &GLCanvas3D::on_mouse, this);
+        m_canvas->Bind(wxEVT_RIGHT_UP, &GLCanvas3D::on_mouse, this);
+        m_canvas->Bind(wxEVT_MOTION, &GLCanvas3D::on_mouse, this);
+        m_canvas->Bind(wxEVT_ENTER_WINDOW, &GLCanvas3D::on_mouse, this);
+        m_canvas->Bind(wxEVT_LEAVE_WINDOW, &GLCanvas3D::on_mouse, this);
+        m_canvas->Bind(wxEVT_LEFT_DCLICK, &GLCanvas3D::on_mouse, this);
+        m_canvas->Bind(wxEVT_MIDDLE_DCLICK, &GLCanvas3D::on_mouse, this);
+        m_canvas->Bind(wxEVT_RIGHT_DCLICK, &GLCanvas3D::on_mouse, this);
+        m_canvas->Bind(wxEVT_PAINT, &GLCanvas3D::on_paint, this);
+        m_canvas->Bind(wxEVT_SET_FOCUS, &GLCanvas3D::on_set_focus, this);
+
+        m_event_handlers_bound = true;
+    }
+}
+
+void GLCanvas3D::unbind_event_handlers()
+{
+    if (m_canvas != nullptr && m_event_handlers_bound) {
+        m_canvas->Unbind(wxEVT_SIZE, &GLCanvas3D::on_size, this);
+        m_canvas->Unbind(wxEVT_IDLE, &GLCanvas3D::on_idle, this);
+        m_canvas->Unbind(wxEVT_CHAR, &GLCanvas3D::on_char, this);
+        m_canvas->Unbind(wxEVT_KEY_DOWN, &GLCanvas3D::on_key, this);
+        m_canvas->Unbind(wxEVT_KEY_UP, &GLCanvas3D::on_key, this);
+        m_canvas->Unbind(wxEVT_MOUSEWHEEL, &GLCanvas3D::on_mouse_wheel, this);
+        m_canvas->Unbind(wxEVT_TIMER, &GLCanvas3D::on_timer, this);
+        m_canvas->Unbind(EVT_GLCANVAS_RENDER_TIMER, &GLCanvas3D::on_render_timer, this);
+        m_canvas->Unbind(wxEVT_LEFT_DOWN, &GLCanvas3D::on_mouse, this);
+		m_canvas->Unbind(wxEVT_LEFT_UP, &GLCanvas3D::on_mouse, this);
+        m_canvas->Unbind(wxEVT_MIDDLE_DOWN, &GLCanvas3D::on_mouse, this);
+        m_canvas->Unbind(wxEVT_MIDDLE_UP, &GLCanvas3D::on_mouse, this);
+        m_canvas->Unbind(wxEVT_RIGHT_DOWN, &GLCanvas3D::on_mouse, this);
+        m_canvas->Unbind(wxEVT_RIGHT_UP, &GLCanvas3D::on_mouse, this);
+        m_canvas->Unbind(wxEVT_MOTION, &GLCanvas3D::on_mouse, this);
+        m_canvas->Unbind(wxEVT_ENTER_WINDOW, &GLCanvas3D::on_mouse, this);
+        m_canvas->Unbind(wxEVT_LEAVE_WINDOW, &GLCanvas3D::on_mouse, this);
+        m_canvas->Unbind(wxEVT_LEFT_DCLICK, &GLCanvas3D::on_mouse, this);
+        m_canvas->Unbind(wxEVT_MIDDLE_DCLICK, &GLCanvas3D::on_mouse, this);
+        m_canvas->Unbind(wxEVT_RIGHT_DCLICK, &GLCanvas3D::on_mouse, this);
+        m_canvas->Unbind(wxEVT_PAINT, &GLCanvas3D::on_paint, this);
+        m_canvas->Unbind(wxEVT_SET_FOCUS, &GLCanvas3D::on_set_focus, this);
+
+        m_event_handlers_bound = false;
+    }
+}
+
+void GLCanvas3D::on_size(wxSizeEvent& evt)
+{
+    m_dirty = true;
+}
+ 
+void GLCanvas3D::on_idle(wxIdleEvent& evt)
+{
+    if (!m_initialized)
+        return;
+
+    m_dirty |= m_main_toolbar.update_items_state();
+    m_dirty |= m_undoredo_toolbar.update_items_state();
+    m_dirty |= wxGetApp().plater()->get_view_toolbar().update_items_state();
+    m_dirty |= wxGetApp().plater()->get_collapse_toolbar().update_items_state();
+    bool mouse3d_controller_applied = wxGetApp().plater()->get_mouse3d_controller().apply(wxGetApp().plater()->get_camera());
+    m_dirty |= mouse3d_controller_applied;
+    m_dirty |= wxGetApp().plater()->get_notification_manager()->update_notifications(*this);
+    auto gizmo = wxGetApp().plater()->canvas3D()->get_gizmos_manager().get_current();
+    if (gizmo != nullptr) m_dirty |= gizmo->update_items_state();
+    // ImGuiWrapper::m_requires_extra_frame may have been set by a render made outside of the OnIdle mechanism
+    bool imgui_requires_extra_frame = wxGetApp().imgui()->requires_extra_frame();
+    m_dirty |= imgui_requires_extra_frame;
+
+    if (!m_dirty)
+        return;
+
+    // this needs to be done here.
+    // during the render launched by the refresh the value may be set again 
+    wxGetApp().imgui()->reset_requires_extra_frame();
+
+    _refresh_if_shown_on_screen();
+
+    if (m_extra_frame_requested || mouse3d_controller_applied || imgui_requires_extra_frame || wxGetApp().imgui()->requires_extra_frame()) {
+        m_extra_frame_requested = false;
+        evt.RequestMore();
+    }
+    else
+        m_dirty = false;
+}
+
+void GLCanvas3D::on_char(wxKeyEvent& evt)
+{
+    if (!m_initialized)
+        return;
+
+    // see include/wx/defs.h enum wxKeyCode
+    int keyCode = evt.GetKeyCode();
+    int ctrlMask = wxMOD_CONTROL;
+    int shiftMask = wxMOD_SHIFT;
+
+    auto imgui = wxGetApp().imgui();
+    if (imgui->update_key_data(evt)) {
+        render();
+        return;
+    }
+
+    if (keyCode == WXK_ESCAPE && (_deactivate_undo_redo_toolbar_items() || _deactivate_search_toolbar_item() || _deactivate_arrange_menu()))
+        return;
+
+    if (m_gizmos.on_char(evt))
+        return;
+
+    if ((evt.GetModifiers() & ctrlMask) != 0) {
+        // CTRL is pressed
+        switch (keyCode) {
+#ifdef __APPLE__
+        case 'a':
+        case 'A':
+#else /* __APPLE__ */
+        case WXK_CONTROL_A:
+#endif /* __APPLE__ */
+            post_event(SimpleEvent(EVT_GLCANVAS_SELECT_ALL));
+        break;
+#ifdef __APPLE__
+        case 'c':
+        case 'C':
+#else /* __APPLE__ */
+        case WXK_CONTROL_C:
+#endif /* __APPLE__ */
+            post_event(SimpleEvent(EVT_GLTOOLBAR_COPY));
+        break;
+#ifdef __APPLE__
+        case 'm':
+        case 'M':
+#else /* __APPLE__ */
+        case WXK_CONTROL_M:
+#endif /* __APPLE__ */
+        {
+#ifdef _WIN32
+            if (wxGetApp().app_config->get("use_legacy_3DConnexion") == "1") {
+#endif //_WIN32
+#ifdef __APPLE__
+            // On OSX use Cmd+Shift+M to "Show/Hide 3Dconnexion devices settings dialog"
+            if ((evt.GetModifiers() & shiftMask) != 0) {
+#endif // __APPLE__
+                Mouse3DController& controller = wxGetApp().plater()->get_mouse3d_controller();
+                controller.show_settings_dialog(!controller.is_settings_dialog_shown());
+                m_dirty = true;
+#ifdef __APPLE__
+            } 
+            else 
+            // and Cmd+M to minimize application
+                wxGetApp().mainframe->Iconize();
+#endif // __APPLE__
+#ifdef _WIN32
+            }
+#endif //_WIN32
+            break;
+        }
+#ifdef __APPLE__
+        case 'v':
+        case 'V':
+#else /* __APPLE__ */
+        case WXK_CONTROL_V:
+#endif /* __APPLE__ */
+            post_event(SimpleEvent(EVT_GLTOOLBAR_PASTE));
+        break;
+
+
+#ifdef __APPLE__
+        case 'f':
+        case 'F':
+#else /* __APPLE__ */
+        case WXK_CONTROL_F:
+#endif /* __APPLE__ */
+            _activate_search_toolbar_item();
+            break;
+
+
+#ifdef __APPLE__
+        case 'y':
+        case 'Y':
+#else /* __APPLE__ */
+        case WXK_CONTROL_Y:
+#endif /* __APPLE__ */
+            post_event(SimpleEvent(EVT_GLCANVAS_REDO));
+        break;
+#ifdef __APPLE__
+        case 'z':
+        case 'Z':
+#else /* __APPLE__ */
+        case WXK_CONTROL_Z:
+#endif /* __APPLE__ */
+            post_event(SimpleEvent(EVT_GLCANVAS_UNDO));
+        break;
+
+        case WXK_BACK:
+        case WXK_DELETE:
+             post_event(SimpleEvent(EVT_GLTOOLBAR_DELETE_ALL)); break;
+        default:            evt.Skip();
+        }
+    } else {
+        switch (keyCode)
+        {
+        case WXK_BACK:
+        case WXK_DELETE: { post_event(SimpleEvent(EVT_GLTOOLBAR_DELETE)); break; }
+        case WXK_ESCAPE: { deselect_all(); break; }
+        case WXK_F5: {
+            if ((wxGetApp().is_editor() && !wxGetApp().plater()->model().objects.empty()) ||
+                (wxGetApp().is_gcode_viewer() && !wxGetApp().plater()->get_last_loaded_gcode().empty()))
+                post_event(SimpleEvent(EVT_GLCANVAS_RELOAD_FROM_DISK));
+            break;
+        }
+        case '0': { select_view("iso"); break; }
+        case '1': { select_view("top"); break; }
+        case '2': { select_view("bottom"); break; }
+        case '3': { select_view("front"); break; }
+        case '4': { select_view("rear"); break; }
+        case '5': { select_view("left"); break; }
+        case '6': { select_view("right"); break; }
+        case '+': {
+            if (dynamic_cast<Preview*>(m_canvas->GetParent()) != nullptr)
+                post_event(wxKeyEvent(EVT_GLCANVAS_EDIT_COLOR_CHANGE, evt));
+            else
+                post_event(Event<int>(EVT_GLCANVAS_INCREASE_INSTANCES, +1));
+            break;
+        }
+        case '-': {
+            if (dynamic_cast<Preview*>(m_canvas->GetParent()) != nullptr)
+                post_event(wxKeyEvent(EVT_GLCANVAS_EDIT_COLOR_CHANGE, evt)); 
+            else
+                post_event(Event<int>(EVT_GLCANVAS_INCREASE_INSTANCES, -1)); 
+            break;
+        }
+        case '?': { post_event(SimpleEvent(EVT_GLCANVAS_QUESTION_MARK)); break; }
+        case 'A':
+        case 'a': { post_event(SimpleEvent(EVT_GLCANVAS_ARRANGE)); break; }
+        case 'B':
+        case 'b': { zoom_to_bed(); break; }
+        case 'C':
+        case 'c': { m_gcode_viewer.toggle_gcode_window_visibility(); m_dirty = true; request_extra_frame(); break; }
+        case 'E':
+        case 'e': { m_labels.show(!m_labels.is_shown()); m_dirty = true; break; }
+        case 'G':
+        case 'g': {
+            if ((evt.GetModifiers() & shiftMask) != 0) {
+                if (dynamic_cast<Preview*>(m_canvas->GetParent()) != nullptr)
+                    post_event(wxKeyEvent(EVT_GLCANVAS_JUMP_TO, evt));
+            }
+            break;
+        }
+        case 'I':
+        case 'i': { _update_camera_zoom(1.0); break; }
+        case 'K':
+        case 'k': { wxGetApp().plater()->get_camera().select_next_type(); m_dirty = true; break; }
+        case 'L': 
+        case 'l': { 
+            if (!m_main_toolbar.is_enabled()) { 
+#if ENABLE_PREVIEW_LAYOUT
+                show_legend(!is_legend_shown());
+#else
+                m_gcode_viewer.enable_legend(!m_gcode_viewer.is_legend_enabled());
+                m_dirty = true;
+                wxGetApp().plater()->update_preview_bottom_toolbar();
+#endif // ENABLE_PREVIEW_LAYOUT
+            }
+            break;
+        }
+        case 'O':
+        case 'o': { _update_camera_zoom(-1.0); break; }
+#if ENABLE_RENDER_PICKING_PASS
+        case 'T':
+        case 't': {
+            m_show_picking_texture = !m_show_picking_texture;
+            m_dirty = true;
+            break;
+        }
+#endif // ENABLE_RENDER_PICKING_PASS
+        case 'Z':
+        case 'z': {
+            if (!m_selection.is_empty())
+                zoom_to_selection();
+            else {
+                if (!m_volumes.empty())
+                    zoom_to_volumes();
+                else
+                    _zoom_to_box(m_gcode_viewer.get_paths_bounding_box());
+            }
+            break;
+        }
+        default:  { evt.Skip(); break; }
+        }
+    }
+}
+
+class TranslationProcessor
+{
+    using UpAction = std::function<void(void)>;
+    using DownAction = std::function<void(const Vec3d&, bool, bool)>;
+
+    UpAction m_up_action{ nullptr };
+    DownAction m_down_action{ nullptr };
+
+    bool m_running{ false };
+    Vec3d m_direction{ Vec3d::UnitX() };
+
+public:
+    TranslationProcessor(UpAction up_action, DownAction down_action)
+        : m_up_action(up_action), m_down_action(down_action)
+    {
+    }
+
+    void process(wxKeyEvent& evt)
+    {
+        const int keyCode = evt.GetKeyCode();
+        wxEventType type = evt.GetEventType();
+        if (type == wxEVT_KEY_UP) {
+            switch (keyCode)
+            {
+            case WXK_NUMPAD_LEFT:  case WXK_LEFT:
+            case WXK_NUMPAD_RIGHT: case WXK_RIGHT:
+            case WXK_NUMPAD_UP:    case WXK_UP:
+            case WXK_NUMPAD_DOWN:  case WXK_DOWN:
+            {
+                m_running = false;
+                m_up_action();
+                break;
+            }
+            default: { break; }
+            }
+        }
+        else if (type == wxEVT_KEY_DOWN) {
+            bool apply = false;
+
+            switch (keyCode)
+            {
+            case WXK_SHIFT:
+            {
+                if (m_running) 
+                    apply = true;
+
+                break;
+            }
+            case WXK_NUMPAD_LEFT:
+            case WXK_LEFT:
+            {
+                m_direction = -Vec3d::UnitX();
+                apply = true;
+                break;
+            }
+            case WXK_NUMPAD_RIGHT:
+            case WXK_RIGHT:
+            {
+                m_direction = Vec3d::UnitX();
+                apply = true;
+                break;
+            }
+            case WXK_NUMPAD_UP:
+            case WXK_UP:
+            {
+                m_direction = Vec3d::UnitY();
+                apply = true;
+                break;
+            }
+            case WXK_NUMPAD_DOWN:
+            case WXK_DOWN:
+            {
+                m_direction = -Vec3d::UnitY();
+                apply = true;
+                break;
+            }
+            default: { break; }
+            }
+
+            if (apply) {
+                m_running = true;
+                m_down_action(m_direction, evt.ShiftDown(), evt.CmdDown());
+            }
+        }
+    }
+};
+
+void GLCanvas3D::on_key(wxKeyEvent& evt)
+{
+    static TranslationProcessor translationProcessor(
+        [this]() {
+            do_move(L("Gizmo-Move"));
+            m_gizmos.update_data();
+
+            wxGetApp().obj_manipul()->set_dirty();
+            // Let the plater know that the dragging finished, so a delayed refresh
+            // of the scene with the background processing data should be performed.
+            post_event(SimpleEvent(EVT_GLCANVAS_MOUSE_DRAGGING_FINISHED));
+            // updates camera target constraints
+            refresh_camera_scene_box();
+            m_dirty = true;
+        },
+        [this](const Vec3d& direction, bool slow, bool camera_space) {
+            m_selection.setup_cache();
+            double multiplier = slow ? 1.0 : 10.0;
+
+            Vec3d displacement;
+            if (camera_space) {
+                Eigen::Matrix<double, 3, 3, Eigen::DontAlign> inv_view_3x3 = wxGetApp().plater()->get_camera().get_view_matrix().inverse().matrix().block(0, 0, 3, 3);
+                displacement = multiplier * (inv_view_3x3 * direction);
+                displacement.z() = 0.0;
+            }
+            else
+                displacement = multiplier * direction;
+
+            m_selection.translate(displacement);
+            m_dirty = true;
+        }
+    );
+
+    const int keyCode = evt.GetKeyCode();
+
+    auto imgui = wxGetApp().imgui();
+    if (imgui->update_key_data(evt)) {
+        render();
+    }
+    else {
+        if (!m_gizmos.on_key(evt)) {
+            if (evt.GetEventType() == wxEVT_KEY_UP) {
+                if (evt.ShiftDown() && evt.ControlDown() && keyCode == WXK_SPACE) {
+                    wxGetApp().plater()->toggle_render_statistic_dialog();
+                    m_dirty = true;
+                }
+                if (m_tab_down && keyCode == WXK_TAB && !evt.HasAnyModifiers()) {
+                    // Enable switching between 3D and Preview with Tab
+                    // m_canvas->HandleAsNavigationKey(evt);   // XXX: Doesn't work in some cases / on Linux
+                    post_event(SimpleEvent(EVT_GLCANVAS_TAB));
+                }
+                else if (keyCode == WXK_TAB && evt.ShiftDown() && ! wxGetApp().is_gcode_viewer()) {
+                    // Collapse side-panel with Shift+Tab
+                    post_event(SimpleEvent(EVT_GLCANVAS_COLLAPSE_SIDEBAR));
+                }
+                else if (keyCode == WXK_SHIFT) {
+                    translationProcessor.process(evt);
+
+                    if (m_picking_enabled && m_rectangle_selection.is_dragging()) {
+                        _update_selection_from_hover();
+                        m_rectangle_selection.stop_dragging();
+                        m_mouse.ignore_left_up = true;
+#if !ENABLE_NEW_RECTANGLE_SELECTION
+                        m_dirty = true;
+#endif // !ENABLE_NEW_RECTANGLE_SELECTION
+                    }
+#if ENABLE_NEW_RECTANGLE_SELECTION
+                    m_dirty = true;
+#endif // ENABLE_NEW_RECTANGLE_SELECTION
+//                    set_cursor(Standard);
+                }
+                else if (keyCode == WXK_ALT) {
+                    if (m_picking_enabled && m_rectangle_selection.is_dragging()) {
+                        _update_selection_from_hover();
+                        m_rectangle_selection.stop_dragging();
+                        m_mouse.ignore_left_up = true;
+                        m_dirty = true;
+                    }
+//                    set_cursor(Standard);
+                }
+                else if (keyCode == WXK_CONTROL) {
+#if ENABLE_NEW_CAMERA_MOVEMENTS
+                    if (m_mouse.dragging) {
+                        // if the user releases CTRL while rotating the 3D scene
+                        // prevent from moving the selected volume
+                        m_mouse.drag.move_volume_idx = -1;
+                        m_mouse.set_start_position_3D_as_invalid();
+                    }
+#endif // ENABLE_NEW_CAMERA_MOVEMENTS
+                    m_dirty = true;
+                }
+                else if (m_gizmos.is_enabled() && !m_selection.is_empty()) {
+                    translationProcessor.process(evt);
+
+                    switch (keyCode)
+                    {
+                    case WXK_NUMPAD_PAGEUP:   case WXK_PAGEUP:
+                    case WXK_NUMPAD_PAGEDOWN: case WXK_PAGEDOWN:
+                    {
+                        do_rotate(L("Gizmo-Rotate"));
+                        m_gizmos.update_data();
+
+                        wxGetApp().obj_manipul()->set_dirty();
+                        // Let the plater know that the dragging finished, so a delayed refresh
+                        // of the scene with the background processing data should be performed.
+                        post_event(SimpleEvent(EVT_GLCANVAS_MOUSE_DRAGGING_FINISHED));
+                        // updates camera target constraints
+                        refresh_camera_scene_box();
+                        m_dirty = true;
+
+                        break;
+                    }
+                    default: { break; }
+                    }
+                }
+            }
+            else if (evt.GetEventType() == wxEVT_KEY_DOWN) {
+                m_tab_down = keyCode == WXK_TAB && !evt.HasAnyModifiers();
+                if (keyCode == WXK_SHIFT) {
+                    translationProcessor.process(evt);
+
+                    if (m_picking_enabled && (m_gizmos.get_current_type() != GLGizmosManager::SlaSupports)) {
+                        m_mouse.ignore_left_up = false;
+//                        set_cursor(Cross);
+                    }
+#if ENABLE_NEW_RECTANGLE_SELECTION
+                    m_dirty = true;
+#endif // ENABLE_NEW_RECTANGLE_SELECTION
+                }
+                else if (keyCode == WXK_ALT) {
+                    if (m_picking_enabled && (m_gizmos.get_current_type() != GLGizmosManager::SlaSupports)) {
+                        m_mouse.ignore_left_up = false;
+//                        set_cursor(Cross);
+                    }
+                }
+                else if (keyCode == WXK_CONTROL)
+                    m_dirty = true;
+                else if (m_gizmos.is_enabled() && !m_selection.is_empty()) {
+                    auto do_rotate = [this](double angle_z_rad) {
+                        m_selection.setup_cache();
+                        m_selection.rotate(Vec3d(0.0, 0.0, angle_z_rad), TransformationType(TransformationType::World_Relative_Joint));
+                        m_dirty = true;
+//                        wxGetApp().obj_manipul()->set_dirty();
+                    };
+
+                    translationProcessor.process(evt);
+
+                    switch (keyCode)
+                    {
+                    case WXK_NUMPAD_PAGEUP:   case WXK_PAGEUP:   { do_rotate(0.25 * M_PI); break; }
+                    case WXK_NUMPAD_PAGEDOWN: case WXK_PAGEDOWN: { do_rotate(-0.25 * M_PI); break; }
+                    default: { break; }
+                    }
+                }
+                else if (!m_gizmos.is_enabled()) {
+                    // DoubleSlider navigation in Preview
+                    if (keyCode == WXK_LEFT ||
+                        keyCode == WXK_RIGHT ||
+                        keyCode == WXK_UP ||
+                        keyCode == WXK_DOWN) {
+                        if (dynamic_cast<Preview*>(m_canvas->GetParent()) != nullptr)
+                            post_event(wxKeyEvent(EVT_GLCANVAS_MOVE_SLIDERS, evt));
+                    }
+                }
+            }
+        }
+    }
+
+    if (keyCode != WXK_TAB
+        && keyCode != WXK_LEFT
+        && keyCode != WXK_UP
+        && keyCode != WXK_RIGHT
+        && keyCode != WXK_DOWN) {
+        evt.Skip();   // Needed to have EVT_CHAR generated as well
+    }
+}
+
+void GLCanvas3D::on_mouse_wheel(wxMouseEvent& evt)
+{
+#ifdef WIN32
+    // Try to filter out spurious mouse wheel events comming from 3D mouse.
+    if (wxGetApp().plater()->get_mouse3d_controller().process_mouse_wheel())
+        return;
+#endif
+
+    if (!m_initialized)
+        return;
+
+    // Ignore the wheel events if the middle button is pressed.
+    if (evt.MiddleIsDown())
+        return;
+
+#if ENABLE_RETINA_GL
+    const float scale = m_retina_helper->get_scale_factor();
+    evt.SetX(evt.GetX() * scale);
+    evt.SetY(evt.GetY() * scale);
+#endif
+
+    if (wxGetApp().imgui()->update_mouse_data(evt)) {
+        m_dirty = true;
+        return;
+    }
+
+#ifdef __WXMSW__
+	// For some reason the Idle event is not being generated after the mouse scroll event in case of scrolling with the two fingers on the touch pad,
+	// if the event is not allowed to be passed further.
+	// https://github.com/prusa3d/PrusaSlicer/issues/2750
+    // evt.Skip() used to trigger the needed screen refresh, but it does no more. wxWakeUpIdle() seem to work now.
+    wxWakeUpIdle();
+#endif /* __WXMSW__ */
+
+    // Performs layers editing updates, if enabled
+    if (is_layers_editing_enabled()) {
+        int object_idx_selected = m_selection.get_object_idx();
+        if (object_idx_selected != -1) {
+            // A volume is selected. Test, whether hovering over a layer thickness bar.
+            if (m_layers_editing.bar_rect_contains(*this, (float)evt.GetX(), (float)evt.GetY())) {
+                // Adjust the width of the selection.
+                m_layers_editing.band_width = std::max(std::min(m_layers_editing.band_width * (1.0f + 0.1f * (float)evt.GetWheelRotation() / (float)evt.GetWheelDelta()), 10.0f), 1.5f);
+                if (m_canvas != nullptr)
+                    m_canvas->Refresh();
+
+                return;
+            }
+        }
+    }
+
+    // If the Search window or Undo/Redo list is opened, 
+    // update them according to the event
+    if (m_main_toolbar.is_item_pressed("search")    || 
+        m_undoredo_toolbar.is_item_pressed("undo")  || 
+        m_undoredo_toolbar.is_item_pressed("redo")) {
+        m_mouse_wheel = int((double)evt.GetWheelRotation() / (double)evt.GetWheelDelta());
+        return;
+    }
+
+    // Inform gizmos about the event so they have the opportunity to react.
+    if (m_gizmos.on_mouse_wheel(evt))
+        return;
+
+    // Calculate the zoom delta and apply it to the current zoom factor
+    double direction_factor = (wxGetApp().app_config->get("reverse_mouse_wheel_zoom") == "1") ? -1.0 : 1.0;
+    _update_camera_zoom(direction_factor * (double)evt.GetWheelRotation() / (double)evt.GetWheelDelta());
+}
+
+void GLCanvas3D::on_timer(wxTimerEvent& evt)
+{
+    if (m_layers_editing.state == LayersEditing::Editing)
+        _perform_layer_editing_action();
+}
+
+void GLCanvas3D::on_render_timer(wxTimerEvent& evt)
+{
+    // no need to wake up idle
+    // right after this event, idle event is fired
+    // m_dirty = true; 
+    // wxWakeUpIdle(); 
+}
+
+
+void GLCanvas3D::schedule_extra_frame(int miliseconds)
+{
+    // Schedule idle event right now
+    if (miliseconds == 0)
+    {
+        // We want to wakeup idle evnt but most likely this is call inside render cycle so we need to wait
+        if (m_in_render)
+            miliseconds = 33;
+        else {
+            m_dirty = true;
+            wxWakeUpIdle();
+            return;
+        }
+    } 
+    int remaining_time = m_render_timer.GetInterval();
+    // Timer is not running
+    if (!m_render_timer.IsRunning()) {
+        m_render_timer.StartOnce(miliseconds);
+    // Timer is running - restart only if new period is shorter than remaning period
+    } else {
+        if (miliseconds + 20 < remaining_time) {
+            m_render_timer.Stop(); 
+            m_render_timer.StartOnce(miliseconds);
+        }
+    }
+}
+
+#ifndef NDEBUG
+// #define SLIC3R_DEBUG_MOUSE_EVENTS
+#endif
+
+#ifdef SLIC3R_DEBUG_MOUSE_EVENTS
+std::string format_mouse_event_debug_message(const wxMouseEvent &evt)
+{
+	static int idx = 0;
+	char buf[2048];
+	std::string out;
+	sprintf(buf, "Mouse Event %d - ", idx ++);
+	out = buf;
+
+	if (evt.Entering())
+		out += "Entering ";
+	if (evt.Leaving())
+		out += "Leaving ";
+	if (evt.Dragging())
+		out += "Dragging ";
+	if (evt.Moving())
+		out += "Moving ";
+	if (evt.Magnify())
+		out += "Magnify ";
+	if (evt.LeftDown())
+		out += "LeftDown ";
+	if (evt.LeftUp())
+		out += "LeftUp ";
+	if (evt.LeftDClick())
+		out += "LeftDClick ";
+	if (evt.MiddleDown())
+		out += "MiddleDown ";
+	if (evt.MiddleUp())
+		out += "MiddleUp ";
+	if (evt.MiddleDClick())
+		out += "MiddleDClick ";
+	if (evt.RightDown())
+		out += "RightDown ";
+	if (evt.RightUp())
+		out += "RightUp ";
+	if (evt.RightDClick())
+		out += "RightDClick ";
+
+	sprintf(buf, "(%d, %d)", evt.GetX(), evt.GetY());
+	out += buf;
+	return out;
+}
+#endif /* SLIC3R_DEBUG_MOUSE_EVENTS */
+
+void GLCanvas3D::on_mouse(wxMouseEvent& evt)
+{
+    if (!m_initialized || !_set_current())
+        return;
+
+#if ENABLE_RETINA_GL
+    const float scale = m_retina_helper->get_scale_factor();
+    evt.SetX(evt.GetX() * scale);
+    evt.SetY(evt.GetY() * scale);
+#endif
+
+    Point pos(evt.GetX(), evt.GetY());
+
+    ImGuiWrapper* imgui = wxGetApp().imgui();
+    if (m_tooltip.is_in_imgui() && evt.LeftUp())
+        // ignore left up events coming from imgui windows and not processed by them
+        m_mouse.ignore_left_up = true;
+    m_tooltip.set_in_imgui(false);
+    if (imgui->update_mouse_data(evt)) {
+        m_mouse.position = evt.Leaving() ? Vec2d(-1.0, -1.0) : pos.cast<double>();
+        m_tooltip.set_in_imgui(true);
+        render();
+#ifdef SLIC3R_DEBUG_MOUSE_EVENTS
+        printf((format_mouse_event_debug_message(evt) + " - Consumed by ImGUI\n").c_str());
+#endif /* SLIC3R_DEBUG_MOUSE_EVENTS */
+        m_dirty = true;
+        // do not return if dragging or tooltip not empty to allow for tooltip update
+        // also, do not return if the mouse is moving and also is inside MM gizmo to allow update seed fill selection
+        if (!m_mouse.dragging && m_tooltip.is_empty() && (m_gizmos.get_current_type() != GLGizmosManager::MmuSegmentation || !evt.Moving()))
+            return;
+    }
+
+#ifdef __WXMSW__
+	bool on_enter_workaround = false;
+    if (! evt.Entering() && ! evt.Leaving() && m_mouse.position.x() == -1.0) {
+        // Workaround for SPE-832: There seems to be a mouse event sent to the window before evt.Entering()
+        m_mouse.position = pos.cast<double>();
+        render();
+#ifdef SLIC3R_DEBUG_MOUSE_EVENTS
+		printf((format_mouse_event_debug_message(evt) + " - OnEnter workaround\n").c_str());
+#endif /* SLIC3R_DEBUG_MOUSE_EVENTS */
+		on_enter_workaround = true;
+    } else 
+#endif /* __WXMSW__ */
+    {
+#ifdef SLIC3R_DEBUG_MOUSE_EVENTS
+		printf((format_mouse_event_debug_message(evt) + " - other\n").c_str());
+#endif /* SLIC3R_DEBUG_MOUSE_EVENTS */
+	}
+
+    if (m_main_toolbar.on_mouse(evt, *this)) {
+        if (evt.LeftUp() || evt.MiddleUp() || evt.RightUp())
+            mouse_up_cleanup();
+        m_mouse.set_start_position_3D_as_invalid();
+#if ENABLE_OBJECT_MANIPULATOR_FOCUS
+            handle_sidebar_focus_event("", false);
+#endif // ENABLE_OBJECT_MANIPULATOR_FOCUS
+        return;
+    }
+
+    if (m_undoredo_toolbar.on_mouse(evt, *this)) {
+        if (evt.LeftUp() || evt.MiddleUp() || evt.RightUp())
+            mouse_up_cleanup();
+        m_mouse.set_start_position_3D_as_invalid();
+#if ENABLE_OBJECT_MANIPULATOR_FOCUS
+        handle_sidebar_focus_event("", false);
+#endif // ENABLE_OBJECT_MANIPULATOR_FOCUS
+        return;
+    }
+
+    if (wxGetApp().plater()->get_collapse_toolbar().on_mouse(evt, *this)) {
+        if (evt.LeftUp() || evt.MiddleUp() || evt.RightUp())
+            mouse_up_cleanup();
+        m_mouse.set_start_position_3D_as_invalid();
+#if ENABLE_OBJECT_MANIPULATOR_FOCUS
+        handle_sidebar_focus_event("", false);
+#endif // ENABLE_OBJECT_MANIPULATOR_FOCUS
+        return;
+    }
+
+    if (wxGetApp().plater()->get_view_toolbar().on_mouse(evt, *this)) {
+        if (evt.LeftUp() || evt.MiddleUp() || evt.RightUp())
+            mouse_up_cleanup();
+        m_mouse.set_start_position_3D_as_invalid();
+#if ENABLE_OBJECT_MANIPULATOR_FOCUS
+        handle_sidebar_focus_event("", false);
+#endif // ENABLE_OBJECT_MANIPULATOR_FOCUS
+        return;
+    }
+
+    for (GLVolume* volume : m_volumes.volumes) {
+        volume->force_sinking_contours = false;
+    }
+
+    auto show_sinking_contours = [this]() {
+        const Selection::IndicesList& idxs = m_selection.get_volume_idxs();
+        for (unsigned int idx : idxs) {
+            m_volumes.volumes[idx]->force_sinking_contours = true;
+        }
+        m_dirty = true;
+    };
+
+    if (m_gizmos.on_mouse(evt)) {
+        if (wxWindow::FindFocus() != m_canvas)
+            // Grab keyboard focus for input in gizmo dialogs.
+            m_canvas->SetFocus();
+
+        if (evt.LeftUp() || evt.MiddleUp() || evt.RightUp())
+            mouse_up_cleanup();
+
+        m_mouse.set_start_position_3D_as_invalid();
+        m_mouse.position = pos.cast<double>();
+
+        // It should be detection of volume change
+        // Not only detection of some modifiers !!!
+        if (evt.Dragging()) {
+            GLGizmosManager::EType c = m_gizmos.get_current_type();
+            if (current_printer_technology() == ptFFF &&
+                fff_print()->config().complete_objects){
+                if (c == GLGizmosManager::EType::Move ||
+                    c == GLGizmosManager::EType::Scale ||
+                    c == GLGizmosManager::EType::Rotate )
+                    update_sequential_clearance();
+            } else {
+                if (c == GLGizmosManager::EType::Move ||
+                    c == GLGizmosManager::EType::Scale ||
+                    c == GLGizmosManager::EType::Rotate)
+                    show_sinking_contours();
+            }
+        }
+
+#if ENABLE_OBJECT_MANIPULATOR_FOCUS
+        handle_sidebar_focus_event("", false);
+#endif // ENABLE_OBJECT_MANIPULATOR_FOCUS
+        return;
+    }
+
+    bool any_gizmo_active = m_gizmos.get_current() != nullptr;
+
+    int selected_object_idx = m_selection.get_object_idx();
+    int layer_editing_object_idx = is_layers_editing_enabled() ? selected_object_idx : -1;
+    m_layers_editing.select_object(*m_model, layer_editing_object_idx);
+
+    if (m_mouse.drag.move_requires_threshold && m_mouse.is_move_start_threshold_position_2D_defined() && m_mouse.is_move_threshold_met(pos)) {
+        m_mouse.drag.move_requires_threshold = false;
+        m_mouse.set_move_start_threshold_position_2D_as_invalid();
+    }
+
+#if ENABLE_OBJECT_MANIPULATOR_FOCUS
+    if (evt.ButtonDown()) {
+        std::string curr_sidebar_field = m_sidebar_field;
+        handle_sidebar_focus_event("", false);
+        if (boost::algorithm::istarts_with(curr_sidebar_field, "layer"))
+            // restore visibility of layers hints after left clicking on the 3D scene
+            m_sidebar_field = curr_sidebar_field;
+        if (wxWindow::FindFocus() != m_canvas)
+            // Grab keyboard focus on any mouse click event.
+            m_canvas->SetFocus();
+    }
+#else
+    if (evt.ButtonDown() && wxWindow::FindFocus() != m_canvas)
+        // Grab keyboard focus on any mouse click event.
+        m_canvas->SetFocus();
+#endif // ENABLE_OBJECT_MANIPULATOR_FOCUS
+
+    if (evt.Entering()) {
+//#if defined(__WXMSW__) || defined(__linux__)
+//        // On Windows and Linux needs focus in order to catch key events
+#if !ENABLE_OBJECT_MANIPULATOR_FOCUS
+        // Set focus in order to remove it from sidebar fields
+#endif // !ENABLE_OBJECT_MANIPULATOR_FOCUS
+        if (m_canvas != nullptr) {
+#if !ENABLE_OBJECT_MANIPULATOR_FOCUS
+            // Only set focus, if the top level window of this canvas is active.
+            auto p = dynamic_cast<wxWindow*>(evt.GetEventObject());
+            while (p->GetParent())
+                p = p->GetParent();
+            auto *top_level_wnd = dynamic_cast<wxTopLevelWindow*>(p);
+            if (top_level_wnd && top_level_wnd->IsActive())
+                m_canvas->SetFocus();
+#endif // !ENABLE_OBJECT_MANIPULATOR_FOCUS
+            m_mouse.position = pos.cast<double>();
+            m_tooltip_enabled = false;
+            // 1) forces a frame render to ensure that m_hover_volume_idxs is updated even when the user right clicks while
+            // the context menu is shown, ensuring it to disappear if the mouse is outside any volume and to
+            // change the volume hover state if any is under the mouse 
+            // 2) when switching between 3d view and preview the size of the canvas changes if the side panels are visible,
+            // so forces a resize to avoid multiple renders with different sizes (seen as flickering)
+            _refresh_if_shown_on_screen();
+            m_tooltip_enabled = true;
+        }
+        m_mouse.set_start_position_2D_as_invalid();
+//#endif
+    }
+    else if (evt.Leaving()) {
+        _deactivate_undo_redo_toolbar_items();
+
+        // to remove hover on objects when the mouse goes out of this canvas
+        m_mouse.position = Vec2d(-1.0, -1.0);
+        m_dirty = true;
+    }
+    else if (evt.LeftDown() || evt.RightDown() || evt.MiddleDown()) {
+        if (_deactivate_undo_redo_toolbar_items() || _deactivate_search_toolbar_item() || _deactivate_arrange_menu())
+            return;
+
+        // If user pressed left or right button we first check whether this happened
+        // on a volume or not.
+        m_layers_editing.state = LayersEditing::Unknown;
+        if (layer_editing_object_idx != -1 && m_layers_editing.bar_rect_contains(*this, pos(0), pos(1))) {
+            // A volume is selected and the mouse is inside the layer thickness bar.
+            // Start editing the layer height.
+            m_layers_editing.state = LayersEditing::Editing;
+            _perform_layer_editing_action(&evt);
+        }
+#if !ENABLE_NEW_RECTANGLE_SELECTION
+        else if (evt.LeftDown() && (evt.ShiftDown() || evt.AltDown()) && m_picking_enabled) {
+            if (m_gizmos.get_current_type() != GLGizmosManager::SlaSupports
+             && m_gizmos.get_current_type() != GLGizmosManager::FdmSupports
+             && m_gizmos.get_current_type() != GLGizmosManager::Seam
+             && m_gizmos.get_current_type() != GLGizmosManager::MmuSegmentation) {
+                m_rectangle_selection.start_dragging(m_mouse.position, evt.ShiftDown() ? GLSelectionRectangle::Select : GLSelectionRectangle::Deselect);
+                m_dirty = true;
+            }
+        }
+#endif // !ENABLE_NEW_RECTANGLE_SELECTION
+        else {
+#if ENABLE_NEW_RECTANGLE_SELECTION
+            const bool rectangle_selection_dragging = m_rectangle_selection.is_dragging();
+            if (evt.LeftDown() && (evt.ShiftDown() || evt.AltDown()) && m_picking_enabled) {
+                if (m_gizmos.get_current_type() != GLGizmosManager::SlaSupports &&
+                    m_gizmos.get_current_type() != GLGizmosManager::FdmSupports &&
+                    m_gizmos.get_current_type() != GLGizmosManager::Seam &&
+                    m_gizmos.get_current_type() != GLGizmosManager::MmuSegmentation) {
+                    m_rectangle_selection.start_dragging(m_mouse.position, evt.ShiftDown() ? GLSelectionRectangle::EState::Select : GLSelectionRectangle::EState::Deselect);
+                    m_dirty = true;
+                }
+            }
+#endif // ENABLE_NEW_RECTANGLE_SELECTION
+
+            // Select volume in this 3D canvas.
+            // Don't deselect a volume if layer editing is enabled or any gizmo is active. We want the object to stay selected
+            // during the scene manipulation.
+
+#if ENABLE_NEW_RECTANGLE_SELECTION
+            if (m_picking_enabled && (!any_gizmo_active || !evt.CmdDown()) && (!m_hover_volume_idxs.empty() || !is_layers_editing_enabled()) && !rectangle_selection_dragging) {
+#else
+            if (m_picking_enabled && (!any_gizmo_active || !evt.CmdDown()) && (!m_hover_volume_idxs.empty() || !is_layers_editing_enabled())) {
+#endif // ENABLE_NEW_RECTANGLE_SELECTION
+                if (evt.LeftDown() && !m_hover_volume_idxs.empty()) {
+                    int volume_idx = get_first_hover_volume_idx();
+                    bool already_selected = m_selection.contains_volume(volume_idx);
+#if ENABLE_NEW_RECTANGLE_SELECTION
+                    bool shift_down = evt.ShiftDown();
+#else
+                    bool ctrl_down = evt.CmdDown();
+#endif // ENABLE_NEW_RECTANGLE_SELECTION
+
+                    Selection::IndicesList curr_idxs = m_selection.get_volume_idxs();
+
+#if ENABLE_NEW_RECTANGLE_SELECTION
+                    if (already_selected && shift_down)
+                        m_selection.remove(volume_idx);
+                    else {
+                        m_selection.add(volume_idx, !shift_down, true);
+#else
+                    if (already_selected && ctrl_down)
+                        m_selection.remove(volume_idx);
+                    else {
+                        m_selection.add(volume_idx, !ctrl_down, true);
+#endif // ENABLE_NEW_RECTANGLE_SELECTION
+                        m_mouse.drag.move_requires_threshold = !already_selected;
+                        if (already_selected)
+                            m_mouse.set_move_start_threshold_position_2D_as_invalid();
+                        else
+                            m_mouse.drag.move_start_threshold_position_2D = pos;
+                    }
+
+                    // propagate event through callback
+                    if (curr_idxs != m_selection.get_volume_idxs()) {
+                        if (m_selection.is_empty())
+                            m_gizmos.reset_all_states();
+                        else
+                            m_gizmos.refresh_on_off_state();
+
+                        m_gizmos.update_data();
+                        post_event(SimpleEvent(EVT_GLCANVAS_OBJECT_SELECT));
+                        m_dirty = true;
+                    }
+                }
+            }
+
+#if ENABLE_NEW_RECTANGLE_SELECTION
+            if (!m_hover_volume_idxs.empty() && !m_rectangle_selection.is_dragging()) {
+#else
+            if (!m_hover_volume_idxs.empty()) {
+#endif // ENABLE_NEW_RECTANGLE_SELECTION
+                if (evt.LeftDown() && m_moving_enabled && m_mouse.drag.move_volume_idx == -1) {
+                    // Only accept the initial position, if it is inside the volume bounding box.
+                    const int volume_idx = get_first_hover_volume_idx();
+                    BoundingBoxf3 volume_bbox = m_volumes.volumes[volume_idx]->transformed_bounding_box();
+                    volume_bbox.offset(1.0);
+                    if ((!any_gizmo_active || !evt.CmdDown()) && volume_bbox.contains(m_mouse.scene_position)) {
+                        m_volumes.volumes[volume_idx]->hover = GLVolume::HS_None;
+                        // The dragging operation is initiated.
+                        m_mouse.drag.move_volume_idx = volume_idx;
+#if ENABLE_NEW_CAMERA_MOVEMENTS
+                        m_selection.setup_cache();
+                        if (!evt.CmdDown())
+#endif // ENABLE_NEW_CAMERA_MOVEMENTS
+                            m_mouse.drag.start_position_3D = m_mouse.scene_position;
+                        m_sequential_print_clearance_first_displacement = true;
+                        m_moving = true;
+                    }
+                }
+            }
+        }
+    }
+#if ENABLE_NEW_CAMERA_MOVEMENTS
+    else if (evt.Dragging() && evt.LeftIsDown() && !evt.CmdDown() && m_layers_editing.state == LayersEditing::Unknown &&
+             m_mouse.drag.move_volume_idx != -1 && m_mouse.is_start_position_3D_defined()) {
+#else
+    else if (evt.Dragging() && evt.LeftIsDown() && m_layers_editing.state == LayersEditing::Unknown && m_mouse.drag.move_volume_idx != -1) {
+#endif // ENABLE_NEW_CAMERA_MOVEMENTS
+    if (!m_mouse.drag.move_requires_threshold) {
+            m_mouse.dragging = true;
+            Vec3d cur_pos = m_mouse.drag.start_position_3D;
+            // we do not want to translate objects if the user just clicked on an object while pressing shift to remove it from the selection and then drag
+            if (m_selection.contains_volume(get_first_hover_volume_idx())) {
+                const Camera& camera = wxGetApp().plater()->get_camera();
+                if (std::abs(camera.get_dir_forward().z()) < EPSILON) {
+                    // side view -> move selected volumes orthogonally to camera view direction
+                    const Linef3 ray = mouse_ray(pos);
+                    const Vec3d dir = ray.unit_vector();
+                    // finds the intersection of the mouse ray with the plane parallel to the camera viewport and passing throught the starting position
+                    // use ray-plane intersection see i.e. https://en.wikipedia.org/wiki/Line%E2%80%93plane_intersection algebric form
+                    // in our case plane normal and ray direction are the same (orthogonal view)
+                    // when moving to perspective camera the negative z unit axis of the camera needs to be transformed in world space and used as plane normal
+                    const Vec3d inters = ray.a + (m_mouse.drag.start_position_3D - ray.a).dot(dir) / dir.squaredNorm() * dir;
+                    // vector from the starting position to the found intersection
+                    const Vec3d inters_vec = inters - m_mouse.drag.start_position_3D;
+
+                    const Vec3d camera_right = camera.get_dir_right();
+                    const Vec3d camera_up = camera.get_dir_up();
+
+                    // finds projection of the vector along the camera axes
+                    const double projection_x = inters_vec.dot(camera_right);
+                    const double projection_z = inters_vec.dot(camera_up);
+
+                    // apply offset
+                    cur_pos = m_mouse.drag.start_position_3D + projection_x * camera_right + projection_z * camera_up;
+                }
+                else {
+                    // Generic view
+                    // Get new position at the same Z of the initial click point.
+                    cur_pos = mouse_ray(pos).intersect_plane(m_mouse.drag.start_position_3D.z());
+                }
+            }
+
+            m_selection.translate(cur_pos - m_mouse.drag.start_position_3D);
+            if (current_printer_technology() == ptFFF && fff_print()->config().complete_objects)
+                update_sequential_clearance();
+            wxGetApp().obj_manipul()->set_dirty();
+            m_dirty = true;
+        }
+    }
+    else if (evt.Dragging() && evt.LeftIsDown() && m_picking_enabled && m_rectangle_selection.is_dragging()) {
+#if ENABLE_NEW_RECTANGLE_SELECTION
+        // keeps the mouse position updated while dragging the selection rectangle
+        m_mouse.position = pos.cast<double>();
+        m_rectangle_selection.dragging(m_mouse.position);
+#else
+        m_rectangle_selection.dragging(pos.cast<double>());
+#endif // ENABLE_NEW_RECTANGLE_SELECTION
+        m_dirty = true;
+    }
+    else if (evt.Dragging()) {
+        m_mouse.dragging = true;
+
+        if (m_layers_editing.state != LayersEditing::Unknown && layer_editing_object_idx != -1) {
+            if (m_layers_editing.state == LayersEditing::Editing) {
+                _perform_layer_editing_action(&evt);
+                m_mouse.position = pos.cast<double>();
+            }
+        }
+        // do not process the dragging if the left mouse was set down in another canvas
+#if ENABLE_NEW_CAMERA_MOVEMENTS
+        else if (evt.LeftIsDown()) {
+            // if dragging over blank area with left button, rotate
+            if ((any_gizmo_active || evt.CmdDown() || m_hover_volume_idxs.empty()) && m_mouse.is_start_position_3D_defined()) {
+#else
+            // if dragging over blank area with left button, rotate
+        else if (evt.LeftIsDown()) {
+            if ((any_gizmo_active || m_hover_volume_idxs.empty()) && m_mouse.is_start_position_3D_defined()) {
+#endif // ENABLE_NEW_CAMERA_MOVEMENTS
+                const Vec3d rot = (Vec3d(pos.x(), pos.y(), 0.0) - m_mouse.drag.start_position_3D) * (PI * TRACKBALLSIZE / 180.0);
+                if (wxGetApp().app_config->get("use_free_camera") == "1")
+                    // Virtual track ball (similar to the 3DConnexion mouse).
+                    wxGetApp().plater()->get_camera().rotate_local_around_target(Vec3d(rot.y(), rot.x(), 0.0));
+                else {
+                    // Forces camera right vector to be parallel to XY plane in case it has been misaligned using the 3D mouse free rotation.
+                    // It is cheaper to call this function right away instead of testing wxGetApp().plater()->get_mouse3d_controller().connected(),
+                    // which checks an atomics (flushes CPU caches).
+                    // See GH issue #3816.
+                    Camera& camera = wxGetApp().plater()->get_camera();
+                    camera.recover_from_free_camera();
+                    camera.rotate_on_sphere(rot.x(), rot.y(), current_printer_technology() != ptSLA);
+                }
+
+                m_dirty = true;
+            }
+            m_mouse.drag.start_position_3D = Vec3d((double)pos.x(), (double)pos.y(), 0.0);
+        }
+        else if (evt.MiddleIsDown() || evt.RightIsDown()) {
+            // If dragging over blank area with right/middle button, pan.
+            if (m_mouse.is_start_position_2D_defined()) {
+                // get point in model space at Z = 0
+                float z = 0.0f;
+                const Vec3d cur_pos = _mouse_to_3d(pos, &z);
+                const Vec3d orig = _mouse_to_3d(m_mouse.drag.start_position_2D, &z);
+                Camera& camera = wxGetApp().plater()->get_camera();
+                if (wxGetApp().app_config->get("use_free_camera") != "1")
+                    // Forces camera right vector to be parallel to XY plane in case it has been misaligned using the 3D mouse free rotation.
+                    // It is cheaper to call this function right away instead of testing wxGetApp().plater()->get_mouse3d_controller().connected(),
+                    // which checks an atomics (flushes CPU caches).
+                    // See GH issue #3816.
+                    camera.recover_from_free_camera();
+
+                camera.set_target(camera.get_target() + orig - cur_pos);
+                m_dirty = true;
+            }
+
+            m_mouse.drag.start_position_2D = pos;
+        }
+    }
+    else if (evt.LeftUp() || evt.MiddleUp() || evt.RightUp()) {
+        if (m_layers_editing.state != LayersEditing::Unknown) {
+            m_layers_editing.state = LayersEditing::Unknown;
+            _stop_timer();
+            m_layers_editing.accept_changes(*this);
+        }
+        else if (m_mouse.drag.move_volume_idx != -1 && m_mouse.dragging) {
+            do_move(L("Move Object"));
+            wxGetApp().obj_manipul()->set_dirty();
+            // Let the plater know that the dragging finished, so a delayed refresh
+            // of the scene with the background processing data should be performed.
+            post_event(SimpleEvent(EVT_GLCANVAS_MOUSE_DRAGGING_FINISHED));
+        }
+        else if (evt.LeftUp() && m_picking_enabled && m_rectangle_selection.is_dragging()) {
+            if (evt.ShiftDown() || evt.AltDown())
+                _update_selection_from_hover();
+
+            m_rectangle_selection.stop_dragging();
+        }
+        else if (evt.LeftUp() && !m_mouse.ignore_left_up && !m_mouse.dragging && m_hover_volume_idxs.empty() && !is_layers_editing_enabled()) {
+            // deselect and propagate event through callback
+            if (!evt.ShiftDown() && (!any_gizmo_active || !evt.CmdDown()) && m_picking_enabled)
+                deselect_all();
+        }
+        else if (evt.RightUp()) {
+            m_mouse.position = pos.cast<double>();
+            // forces a frame render to ensure that m_hover_volume_idxs is updated even when the user right clicks while
+            // the context menu is already shown
+            render();
+            if (!m_hover_volume_idxs.empty()) {
+                // if right clicking on volume, propagate event through callback (shows context menu)
+                int volume_idx = get_first_hover_volume_idx();
+                if (!m_volumes.volumes[volume_idx]->is_wipe_tower // no context menu for the wipe tower
+                    && m_gizmos.get_current_type() != GLGizmosManager::SlaSupports)  // disable context menu when the gizmo is open
+                {
+                    // forces the selection of the volume
+                    /* m_selection.add(volume_idx); // #et_FIXME_if_needed
+                     * To avoid extra "Add-Selection" snapshots,
+                     * call add() with check_for_already_contained=true
+                     * */
+                    m_selection.add(volume_idx, true, true); 
+                    m_gizmos.refresh_on_off_state();
+                    post_event(SimpleEvent(EVT_GLCANVAS_OBJECT_SELECT));
+                    m_gizmos.update_data();
+                    wxGetApp().obj_manipul()->set_dirty();
+                    // forces a frame render to update the view before the context menu is shown
+                    render();
+                }
+            }
+            Vec2d logical_pos = pos.cast<double>();
+#if ENABLE_RETINA_GL
+            const float factor = m_retina_helper->get_scale_factor();
+            logical_pos = logical_pos.cwiseQuotient(Vec2d(factor, factor));
+#endif // ENABLE_RETINA_GL
+            if (!m_mouse.dragging) {
+                // do not post the event if the user is panning the scene
+                // or if right click was done over the wipe tower
+                const bool post_right_click_event = m_hover_volume_idxs.empty() || !m_volumes.volumes[get_first_hover_volume_idx()]->is_wipe_tower;
+                if (post_right_click_event)
+                    post_event(RBtnEvent(EVT_GLCANVAS_RIGHT_CLICK, { logical_pos, m_hover_volume_idxs.empty() }));
+            }
+        }
+
+        mouse_up_cleanup();
+    }
+    else if (evt.Moving()) {
+        m_mouse.position = pos.cast<double>();
+
+        // updates gizmos overlay
+        if (m_selection.is_empty())
+            m_gizmos.reset_all_states();
+
+        m_dirty = true;
+    }
+    else
+        evt.Skip();
+
+    if (m_moving)
+        show_sinking_contours();
+
+#ifdef __WXMSW__
+	if (on_enter_workaround)
+		m_mouse.position = Vec2d(-1., -1.);
+#endif /* __WXMSW__ */
+}
+
+void GLCanvas3D::on_paint(wxPaintEvent& evt)
+{
+    if (m_initialized)
+        m_dirty = true;
+    else
+        // Call render directly, so it gets initialized immediately, not from On Idle handler.
+        this->render();
+}
+
+void GLCanvas3D::on_set_focus(wxFocusEvent& evt)
+{
+    m_tooltip_enabled = false;
+    _refresh_if_shown_on_screen();
+    m_tooltip_enabled = true;
+}
+
+Size GLCanvas3D::get_canvas_size() const
+{
+    int w = 0;
+    int h = 0;
+
+    if (m_canvas != nullptr)
+        m_canvas->GetSize(&w, &h);
+
+#if ENABLE_RETINA_GL
+    const float factor = m_retina_helper->get_scale_factor();
+    w *= factor;
+    h *= factor;
+#else
+    const float factor = 1.0f;
+#endif
+
+    return Size(w, h, factor);
+}
+
+Vec2d GLCanvas3D::get_local_mouse_position() const
+{
+    if (m_canvas == nullptr)
+		return Vec2d::Zero();
+
+    wxPoint mouse_pos = m_canvas->ScreenToClient(wxGetMousePosition());
+    const double factor = 
+#if ENABLE_RETINA_GL
+        m_retina_helper->get_scale_factor();
+#else
+        1.0;
+#endif
+    return Vec2d(factor * mouse_pos.x, factor * mouse_pos.y);
+}
+
+void GLCanvas3D::set_tooltip(const std::string& tooltip)
+{
+    if (m_canvas != nullptr)
+        m_tooltip.set_text(tooltip);
+}
+
+void GLCanvas3D::do_move(const std::string& snapshot_type)
+{
+    if (m_model == nullptr)
+        return;
+
+    if (!snapshot_type.empty())
+        wxGetApp().plater()->take_snapshot(_(snapshot_type));
+
+    std::set<std::pair<int, int>> done;  // keeps track of modified instances
+    bool object_moved = false;
+    Vec3d wipe_tower_origin = Vec3d::Zero();
+
+    Selection::EMode selection_mode = m_selection.get_mode();
+
+    for (const GLVolume* v : m_volumes.volumes) {
+        int object_idx = v->object_idx();
+        int instance_idx = v->instance_idx();
+        int volume_idx = v->volume_idx();
+
+        std::pair<int, int> done_id(object_idx, instance_idx);
+
+        if (0 <= object_idx && object_idx < (int)m_model->objects.size()) {
+            done.insert(done_id);
+
+            // Move instances/volumes
+            ModelObject* model_object = m_model->objects[object_idx];
+            if (model_object != nullptr) {
+                if (selection_mode == Selection::Instance)
+                    model_object->instances[instance_idx]->set_offset(v->get_instance_offset());
+                else if (selection_mode == Selection::Volume)
+                    model_object->volumes[volume_idx]->set_offset(v->get_volume_offset());
+
+                object_moved = true;
+                model_object->invalidate_bounding_box();
+            }
+        }
+#if ENABLE_WIPETOWER_OBJECTID_1000_REMOVAL
+        else if (v->is_wipe_tower)
+            // Move a wipe tower proxy.
+            wipe_tower_origin = v->get_volume_offset();
+#else
+        else if (object_idx == 1000)
+            // Move a wipe tower proxy.
+            wipe_tower_origin = v->get_volume_offset();
+#endif // ENABLE_WIPETOWER_OBJECTID_1000_REMOVAL
+    }
+
+    // Fixes flying instances
+    for (const std::pair<int, int>& i : done) {
+        ModelObject* m = m_model->objects[i.first];
+        const double shift_z = m->get_instance_min_z(i.second);
+        if (current_printer_technology() == ptSLA || shift_z > SINKING_Z_THRESHOLD) {
+            const Vec3d shift(0.0, 0.0, -shift_z);
+            m_selection.translate(i.first, i.second, shift);
+            m->translate_instance(i.second, shift);
+        }
+        wxGetApp().obj_list()->update_info_items(static_cast<size_t>(i.first));
+    }
+
+    // if the selection is not valid to allow for layer editing after the move, we need to turn off the tool if it is running
+    // similar to void Plater::priv::selection_changed()
+    if (!wxGetApp().plater()->can_layers_editing() && is_layers_editing_enabled())
+        post_event(SimpleEvent(EVT_GLTOOLBAR_LAYERSEDITING));
+
+    if (object_moved)
+        post_event(SimpleEvent(EVT_GLCANVAS_INSTANCE_MOVED));
+
+    if (wipe_tower_origin != Vec3d::Zero())
+        post_event(Vec3dEvent(EVT_GLCANVAS_WIPETOWER_MOVED, std::move(wipe_tower_origin)));
+
+    reset_sequential_print_clearance();
+
+    m_dirty = true;
+}
+
+void GLCanvas3D::do_rotate(const std::string& snapshot_type)
+{
+    if (m_model == nullptr)
+        return;
+
+    if (!snapshot_type.empty())
+        wxGetApp().plater()->take_snapshot(_(snapshot_type));
+
+    // stores current min_z of instances
+    std::map<std::pair<int, int>, double> min_zs;
+    for (int i = 0; i < static_cast<int>(m_model->objects.size()); ++i) {
+        const ModelObject* obj = m_model->objects[i];
+        for (int j = 0; j < static_cast<int>(obj->instances.size()); ++j) {
+            if (snapshot_type.empty() && m_selection.get_object_idx() == i) {
+                // This means we are flattening this object. In that case pretend
+                // that it is not sinking (even if it is), so it is placed on bed
+                // later on (whatever is sinking will be left sinking).
+                min_zs[{ i, j }] = SINKING_Z_THRESHOLD;
+            } else
+                min_zs[{ i, j }] = obj->instance_bounding_box(j).min.z();
+
+        }
+    }
+
+    std::set<std::pair<int, int>> done;  // keeps track of modified instances
+
+    Selection::EMode selection_mode = m_selection.get_mode();
+
+    for (const GLVolume* v : m_volumes.volumes) {
+#if ENABLE_WIPETOWER_OBJECTID_1000_REMOVAL
+        if (v->is_wipe_tower) {
+#else
+        int object_idx = v->object_idx();
+        if (object_idx == 1000) { // the wipe tower
+#endif // ENABLE_WIPETOWER_OBJECTID_1000_REMOVAL
+            Vec3d offset = v->get_volume_offset();
+            post_event(Vec3dEvent(EVT_GLCANVAS_WIPETOWER_ROTATED, Vec3d(offset(0), offset(1), v->get_volume_rotation()(2))));
+        }
+#if ENABLE_WIPETOWER_OBJECTID_1000_REMOVAL
+        int object_idx = v->object_idx();
+#endif // ENABLE_WIPETOWER_OBJECTID_1000_REMOVAL
+        if (object_idx < 0 || (int)m_model->objects.size() <= object_idx)
+            continue;
+
+        int instance_idx = v->instance_idx();
+        int volume_idx = v->volume_idx();
+
+        done.insert(std::pair<int, int>(object_idx, instance_idx));
+
+        // Rotate instances/volumes.
+        ModelObject* model_object = m_model->objects[object_idx];
+        if (model_object != nullptr) {
+            if (selection_mode == Selection::Instance) {
+                model_object->instances[instance_idx]->set_rotation(v->get_instance_rotation());
+                model_object->instances[instance_idx]->set_offset(v->get_instance_offset());
+            }
+            else if (selection_mode == Selection::Volume) {
+                model_object->volumes[volume_idx]->set_rotation(v->get_volume_rotation());
+                model_object->volumes[volume_idx]->set_offset(v->get_volume_offset());
+            }
+            model_object->invalidate_bounding_box();
+        }
+    }
+
+    // Fixes sinking/flying instances
+    for (const std::pair<int, int>& i : done) {
+        ModelObject* m = m_model->objects[i.first];
+        const double shift_z = m->get_instance_min_z(i.second);
+        // leave sinking instances as sinking
+        if (min_zs.find({ i.first, i.second })->second >= SINKING_Z_THRESHOLD || shift_z > SINKING_Z_THRESHOLD) {
+            const Vec3d shift(0.0, 0.0, -shift_z);
+            m_selection.translate(i.first, i.second, shift);
+            m->translate_instance(i.second, shift);
+        }
+
+        wxGetApp().obj_list()->update_info_items(static_cast<size_t>(i.first));
+    }
+
+    if (!done.empty())
+        post_event(SimpleEvent(EVT_GLCANVAS_INSTANCE_ROTATED));
+
+    m_dirty = true;
+}
+
+void GLCanvas3D::do_scale(const std::string& snapshot_type)
+{
+    if (m_model == nullptr)
+        return;
+
+    if (!snapshot_type.empty())
+        wxGetApp().plater()->take_snapshot(_(snapshot_type));
+
+    // stores current min_z of instances
+    std::map<std::pair<int, int>, double> min_zs;
+    if (!snapshot_type.empty()) {
+        for (int i = 0; i < static_cast<int>(m_model->objects.size()); ++i) {
+            const ModelObject* obj = m_model->objects[i];
+            for (int j = 0; j < static_cast<int>(obj->instances.size()); ++j) {
+                min_zs[{ i, j }] = obj->instance_bounding_box(j).min.z();
+            }
+        }
+    }
+
+    std::set<std::pair<int, int>> done;  // keeps track of modified instances
+
+    Selection::EMode selection_mode = m_selection.get_mode();
+
+    for (const GLVolume* v : m_volumes.volumes) {
+        int object_idx = v->object_idx();
+        if (object_idx < 0 || (int)m_model->objects.size() <= object_idx)
+            continue;
+
+        int instance_idx = v->instance_idx();
+        int volume_idx = v->volume_idx();
+
+        done.insert(std::pair<int, int>(object_idx, instance_idx));
+
+        // Rotate instances/volumes
+        ModelObject* model_object = m_model->objects[object_idx];
+        if (model_object != nullptr) {
+            if (selection_mode == Selection::Instance) {
+                model_object->instances[instance_idx]->set_scaling_factor(v->get_instance_scaling_factor());
+                model_object->instances[instance_idx]->set_offset(v->get_instance_offset());
+            }
+            else if (selection_mode == Selection::Volume) {
+                model_object->instances[instance_idx]->set_offset(v->get_instance_offset());
+                model_object->volumes[volume_idx]->set_scaling_factor(v->get_volume_scaling_factor());
+                model_object->volumes[volume_idx]->set_offset(v->get_volume_offset());
+            }
+            model_object->invalidate_bounding_box();
+        }
+    }
+
+    // Fixes sinking/flying instances
+    for (const std::pair<int, int>& i : done) {
+        ModelObject* m = m_model->objects[i.first];
+        double shift_z = m->get_instance_min_z(i.second);
+        // leave sinking instances as sinking
+        if (min_zs.empty() || min_zs.find({ i.first, i.second })->second >= SINKING_Z_THRESHOLD || shift_z > SINKING_Z_THRESHOLD) {
+            Vec3d shift(0.0, 0.0, -shift_z);
+            m_selection.translate(i.first, i.second, shift);
+            m->translate_instance(i.second, shift);
+        }
+        wxGetApp().obj_list()->update_info_items(static_cast<size_t>(i.first));
+    }
+
+    if (!done.empty())
+        post_event(SimpleEvent(EVT_GLCANVAS_INSTANCE_SCALED));
+
+    m_dirty = true;
+}
+
+void GLCanvas3D::do_mirror(const std::string& snapshot_type)
+{
+    if (m_model == nullptr)
+        return;
+
+    if (!snapshot_type.empty())
+        wxGetApp().plater()->take_snapshot(_(snapshot_type));
+
+    // stores current min_z of instances
+    std::map<std::pair<int, int>, double> min_zs;
+    if (!snapshot_type.empty()) {
+        for (int i = 0; i < static_cast<int>(m_model->objects.size()); ++i) {
+            const ModelObject* obj = m_model->objects[i];
+            for (int j = 0; j < static_cast<int>(obj->instances.size()); ++j) {
+                min_zs[{ i, j }] = obj->instance_bounding_box(j).min.z();
+            }
+        }
+    }
+
+    std::set<std::pair<int, int>> done;  // keeps track of modified instances
+
+    Selection::EMode selection_mode = m_selection.get_mode();
+
+    for (const GLVolume* v : m_volumes.volumes) {
+        int object_idx = v->object_idx();
+        if (object_idx < 0 || (int)m_model->objects.size() <= object_idx)
+            continue;
+
+        int instance_idx = v->instance_idx();
+        int volume_idx = v->volume_idx();
+
+        done.insert(std::pair<int, int>(object_idx, instance_idx));
+
+        // Mirror instances/volumes
+        ModelObject* model_object = m_model->objects[object_idx];
+        if (model_object != nullptr) {
+            if (selection_mode == Selection::Instance)
+                model_object->instances[instance_idx]->set_mirror(v->get_instance_mirror());
+            else if (selection_mode == Selection::Volume)
+                model_object->volumes[volume_idx]->set_mirror(v->get_volume_mirror());
+
+            model_object->invalidate_bounding_box();
+        }
+    }
+
+    // Fixes sinking/flying instances
+    for (const std::pair<int, int>& i : done) {
+        ModelObject* m = m_model->objects[i.first];
+        double shift_z = m->get_instance_min_z(i.second);
+        // leave sinking instances as sinking
+        if (min_zs.empty() || min_zs.find({ i.first, i.second })->second >= SINKING_Z_THRESHOLD || shift_z > SINKING_Z_THRESHOLD) {
+            Vec3d shift(0.0, 0.0, -shift_z);
+            m_selection.translate(i.first, i.second, shift);
+            m->translate_instance(i.second, shift);
+        }
+        wxGetApp().obj_list()->update_info_items(static_cast<size_t>(i.first));
+    }
+
+    post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS));
+
+    m_dirty = true;
+}
+
+void GLCanvas3D::update_gizmos_on_off_state()
+{
+    set_as_dirty();
+    m_gizmos.update_data();
+    m_gizmos.refresh_on_off_state();
+}
+
+void GLCanvas3D::handle_sidebar_focus_event(const std::string& opt_key, bool focus_on)
+{
+    m_sidebar_field = focus_on ? opt_key : "";
+    if (!m_sidebar_field.empty())
+        m_gizmos.reset_all_states();
+
+    m_dirty = true;
+}
+
+void GLCanvas3D::handle_layers_data_focus_event(const t_layer_height_range range, const EditorType type)
+{
+    std::string field = "layer_" + std::to_string(type) + "_" + std::to_string(range.first) + "_" + std::to_string(range.second);
+    handle_sidebar_focus_event(field, true);
+}
+
+void GLCanvas3D::update_ui_from_settings()
+{
+    m_dirty = true;
+
+#if __APPLE__
+    // Update OpenGL scaling on OSX after the user toggled the "use_retina_opengl" settings in Preferences dialog.
+    const float orig_scaling = m_retina_helper->get_scale_factor();
+
+    const bool use_retina = wxGetApp().app_config->get("use_retina_opengl") == "1";
+    BOOST_LOG_TRIVIAL(debug) << "GLCanvas3D: Use Retina OpenGL: " << use_retina;
+    m_retina_helper->set_use_retina(use_retina);
+    const float new_scaling = m_retina_helper->get_scale_factor();
+
+    if (new_scaling != orig_scaling) {
+        BOOST_LOG_TRIVIAL(debug) << "GLCanvas3D: Scaling factor: " << new_scaling;
+
+        Camera& camera = wxGetApp().plater()->get_camera();
+        camera.set_zoom(camera.get_zoom() * new_scaling / orig_scaling);
+        _refresh_if_shown_on_screen();
+    }
+#endif // ENABLE_RETINA_GL
+
+    if (wxGetApp().is_editor())
+        wxGetApp().plater()->enable_collapse_toolbar(wxGetApp().app_config->get("show_collapse_button") == "1");
+}
+
+GLCanvas3D::WipeTowerInfo GLCanvas3D::get_wipe_tower_info() const
+{
+    WipeTowerInfo wti;
+    
+    for (const GLVolume* vol : m_volumes.volumes) {
+        if (vol->is_wipe_tower) {
+            wti.m_pos = Vec2d(m_config->opt_float("wipe_tower_x"),
+                            m_config->opt_float("wipe_tower_y"));
+            wti.m_rotation = (M_PI/180.) * m_config->opt_float("wipe_tower_rotation_angle");
+            const BoundingBoxf3& bb = vol->bounding_box();
+            wti.m_bb = BoundingBoxf{to_2d(bb.min), to_2d(bb.max)};
+            break;
+        }
+    }
+    
+    return wti;
+}
+
+Linef3 GLCanvas3D::mouse_ray(const Point& mouse_pos)
+{
+    float z0 = 0.0f;
+    float z1 = 1.0f;
+    return Linef3(_mouse_to_3d(mouse_pos, &z0), _mouse_to_3d(mouse_pos, &z1));
+}
+
+double GLCanvas3D::get_size_proportional_to_max_bed_size(double factor) const
+{
+    const BoundingBoxf& bbox = m_bed.build_volume().bounding_volume2d();
+    return factor * std::max(bbox.size()[0], bbox.size()[1]);
+}
+
+void GLCanvas3D::set_cursor(ECursorType type)
+{
+    if ((m_canvas != nullptr) && (m_cursor_type != type))
+    {
+        switch (type)
+        {
+        case Standard: { m_canvas->SetCursor(*wxSTANDARD_CURSOR); break; }
+        case Cross: { m_canvas->SetCursor(*wxCROSS_CURSOR); break; }
+        }
+
+        m_cursor_type = type;
+    }
+}
+
+void GLCanvas3D::msw_rescale()
+{
+#if ENABLE_PREVIEW_LAYOUT
+    m_gcode_viewer.invalidate_legend();
+#endif // ENABLE_PREVIEW_LAYOUT
+}
+
+void GLCanvas3D::update_tooltip_for_settings_item_in_main_toolbar()
+{
+    std::string new_tooltip = _u8L("Switch to Settings") + 
+                             "\n" + "[" + GUI::shortkey_ctrl_prefix() + "2] - " + _u8L("Print Settings Tab")    + 
+                             "\n" + "[" + GUI::shortkey_ctrl_prefix() + "3] - " + (current_printer_technology() == ptFFF ? _u8L("Filament Settings Tab") : _u8L("Material Settings Tab")) +
+                             "\n" + "[" + GUI::shortkey_ctrl_prefix() + "4] - " + _u8L("Printer Settings Tab") ;
+
+    m_main_toolbar.set_tooltip(get_main_toolbar_item_id("settings"), new_tooltip);
+}
+
+bool GLCanvas3D::has_toolpaths_to_export() const
+{
+    return m_gcode_viewer.can_export_toolpaths();
+}
+
+void GLCanvas3D::export_toolpaths_to_obj(const char* filename) const
+{
+    m_gcode_viewer.export_toolpaths_to_obj(filename);
+}
+
+void GLCanvas3D::mouse_up_cleanup()
+{
+    m_moving = false;
+    m_mouse.drag.move_volume_idx = -1;
+    m_mouse.set_start_position_3D_as_invalid();
+    m_mouse.set_start_position_2D_as_invalid();
+    m_mouse.dragging = false;
+    m_mouse.ignore_left_up = false;
+    m_dirty = true;
+
+    if (m_canvas->HasCapture())
+        m_canvas->ReleaseMouse();
+}
+
+void GLCanvas3D::update_sequential_clearance()
+{
+    if (current_printer_technology() != ptFFF || !fff_print()->config().complete_objects)
+        return;
+
+    if (m_layers_editing.is_enabled() || m_gizmos.is_dragging())
+        return;
+
+    // collects instance transformations from volumes
+    // first define temporary cache
+    unsigned int instances_count = 0;
+    std::vector<std::vector<std::optional<Geometry::Transformation>>> instance_transforms;
+    for (size_t obj = 0; obj < m_model->objects.size(); ++obj) {
+        instance_transforms.emplace_back(std::vector<std::optional<Geometry::Transformation>>());
+        const ModelObject* model_object = m_model->objects[obj];
+        for (size_t i = 0; i < model_object->instances.size(); ++i) {
+            instance_transforms[obj].emplace_back(std::optional<Geometry::Transformation>());
+            ++instances_count;
+        }
+    }
+
+    if (instances_count == 1)
+        return;
+
+    // second fill temporary cache with data from volumes
+    for (const GLVolume* v : m_volumes.volumes) {
+        if (v->is_modifier || v->is_wipe_tower)
+            continue;
+
+        auto& transform = instance_transforms[v->object_idx()][v->instance_idx()];
+        if (!transform.has_value())
+            transform = v->get_instance_transformation();
+    }
+
+    // calculates objects 2d hulls (see also: Print::sequential_print_horizontal_clearance_valid())
+    // this is done only the first time this method is called while moving the mouse,
+    // the results are then cached for following displacements
+    if (m_sequential_print_clearance_first_displacement) {
+        m_sequential_print_clearance.m_hull_2d_cache.clear();
+        float shrink_factor = static_cast<float>(scale_(0.5 * fff_print()->config().extruder_clearance_radius.value - EPSILON));
+        double mitter_limit = scale_(0.1);
+        m_sequential_print_clearance.m_hull_2d_cache.reserve(m_model->objects.size());
+        for (size_t i = 0; i < m_model->objects.size(); ++i) {
+            ModelObject* model_object = m_model->objects[i];
+            ModelInstance* model_instance0 = model_object->instances.front();
+            Polygon hull_2d = offset(model_object->convex_hull_2d(Geometry::assemble_transform({ 0.0, 0.0, model_instance0->get_offset().z() }, model_instance0->get_rotation(),
+                model_instance0->get_scaling_factor(), model_instance0->get_mirror())),
+                // Shrink the extruder_clearance_radius a tiny bit, so that if the object arrangement algorithm placed the objects
+                // exactly by satisfying the extruder_clearance_radius, this test will not trigger collision.
+                shrink_factor,
+                jtRound, mitter_limit).front();
+
+            Pointf3s& cache_hull_2d = m_sequential_print_clearance.m_hull_2d_cache.emplace_back(Pointf3s());
+            cache_hull_2d.reserve(hull_2d.points.size());
+            for (const Point& p : hull_2d.points) {
+                cache_hull_2d.emplace_back(unscale<double>(p.x()), unscale<double>(p.y()), 0.0);
+            }
+        }
+        m_sequential_print_clearance_first_displacement = false;
+    }
+
+    // calculates instances 2d hulls (see also: Print::sequential_print_horizontal_clearance_valid())
+    Polygons polygons;
+    polygons.reserve(instances_count);
+    for (size_t i = 0; i < instance_transforms.size(); ++i) {
+        const auto& instances = instance_transforms[i];
+        double rotation_z0 = instances.front()->get_rotation().z();
+        for (const auto& instance : instances) {
+            Geometry::Transformation transformation;
+            const Vec3d& offset = instance->get_offset();
+            transformation.set_offset({ offset.x(), offset.y(), 0.0 });
+            transformation.set_rotation(Z, instance->get_rotation().z() - rotation_z0);
+            const Transform3d& trafo = transformation.get_matrix();
+            const Pointf3s& hull_2d = m_sequential_print_clearance.m_hull_2d_cache[i];
+            Points inst_pts;
+            inst_pts.reserve(hull_2d.size());
+            for (size_t j = 0; j < hull_2d.size(); ++j) {
+                const Vec3d p = trafo * hull_2d[j];
+                inst_pts.emplace_back(scaled<double>(p.x()), scaled<double>(p.y()));
+            }
+            polygons.emplace_back(Geometry::convex_hull(std::move(inst_pts)));
+        }
+    }
+
+    // sends instances 2d hulls to be rendered
+    set_sequential_print_clearance_visible(true);
+    set_sequential_print_clearance_render_fill(false);
+    set_sequential_print_clearance_polygons(polygons);
+}
+
+bool GLCanvas3D::is_object_sinking(int object_idx) const
+{
+    for (const GLVolume* v : m_volumes.volumes) {
+        if (v->object_idx() == object_idx && (v->is_sinking() || (!v->is_modifier && v->is_below_printbed())))
+            return true;
+    }
+    return false;
+}
+
+bool GLCanvas3D::_is_shown_on_screen() const
+{
+    return (m_canvas != nullptr) ? m_canvas->IsShownOnScreen() : false;
+}
+
+// Getter for the const char*[]
+static bool string_getter(const bool is_undo, int idx, const char** out_text)
+{
+    return wxGetApp().plater()->undo_redo_string_getter(is_undo, idx, out_text);
+}
+
+bool GLCanvas3D::_render_undo_redo_stack(const bool is_undo, float pos_x)
+{
+    bool action_taken = false;
+
+    ImGuiWrapper* imgui = wxGetApp().imgui();
+
+#if ENABLE_GL_SHADERS_ATTRIBUTES
+    imgui->set_next_window_pos(pos_x, m_undoredo_toolbar.get_height(), ImGuiCond_Always, 0.5f, 0.0f);
+#else
+    const float x = pos_x * (float)wxGetApp().plater()->get_camera().get_zoom() + 0.5f * (float)get_canvas_size().get_width();
+    imgui->set_next_window_pos(x, m_undoredo_toolbar.get_height(), ImGuiCond_Always, 0.5f, 0.0f);
+#endif // ENABLE_GL_SHADERS_ATTRIBUTES
+    std::string title = is_undo ? L("Undo History") : L("Redo History");
+    imgui->begin(_(title), ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse);
+
+    int hovered = m_imgui_undo_redo_hovered_pos;
+    int selected = -1;
+    float em = static_cast<float>(wxGetApp().em_unit());
+#if ENABLE_RETINA_GL
+	em *= m_retina_helper->get_scale_factor();
+#endif
+
+    if (imgui->undo_redo_list(ImVec2(18 * em, 26 * em), is_undo, &string_getter, hovered, selected, m_mouse_wheel))
+        m_imgui_undo_redo_hovered_pos = hovered;
+    else
+        m_imgui_undo_redo_hovered_pos = -1;
+
+    if (selected >= 0) {
+        is_undo ? wxGetApp().plater()->undo_to(selected) : wxGetApp().plater()->redo_to(selected);
+        action_taken = true;
+    }
+
+    imgui->text(wxString::Format(is_undo ? _L_PLURAL("Undo %1$d Action", "Undo %1$d Actions", hovered + 1) : _L_PLURAL("Redo %1$d Action", "Redo %1$d Actions", hovered + 1), hovered + 1));
+
+    imgui->end();
+
+    return action_taken;
+}
+
+// Getter for the const char*[] for the search list 
+static bool search_string_getter(int idx, const char** label, const char** tooltip)
+{
+    return wxGetApp().plater()->search_string_getter(idx, label, tooltip);
+}
+
+bool GLCanvas3D::_render_search_list(float pos_x)
+{
+    bool action_taken = false;
+    ImGuiWrapper* imgui = wxGetApp().imgui();
+
+#if ENABLE_GL_SHADERS_ATTRIBUTES
+    imgui->set_next_window_pos(pos_x, m_main_toolbar.get_height(), ImGuiCond_Always, 0.5f, 0.0f);
+#else
+    const float x = /*pos_x * (float)wxGetApp().plater()->get_camera().get_zoom() + */0.5f * (float)get_canvas_size().get_width();
+    imgui->set_next_window_pos(x, m_main_toolbar.get_height(), ImGuiCond_Always, 0.5f, 0.0f);
+#endif // ENABLE_GL_SHADERS_ATTRIBUTES
+    std::string title = L("Search");
+    imgui->begin(_(title), ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse);
+
+    int selected = -1;
+    bool edited = false;
+    float em = static_cast<float>(wxGetApp().em_unit());
+#if ENABLE_RETINA_GL
+	em *= m_retina_helper->get_scale_factor();
+#endif // ENABLE_RETINA_GL
+
+    Sidebar& sidebar = wxGetApp().sidebar();
+
+    std::string& search_line = sidebar.get_search_line();
+    char *s = new char[255];
+    strcpy(s, search_line.empty() ? _u8L("Enter a search term").c_str() : search_line.c_str());
+
+    imgui->search_list(ImVec2(45 * em, 30 * em), &search_string_getter, s,
+        sidebar.get_searcher().view_params,
+        selected, edited, m_mouse_wheel, wxGetApp().is_localized());
+
+    search_line = s;
+    delete [] s;
+    if (search_line == _u8L("Enter a search term"))
+        search_line.clear();
+
+    if (edited)
+        sidebar.search();
+
+    if (selected >= 0) {
+        // selected == 9999 means that Esc kye was pressed
+        /*// revert commit https://github.com/prusa3d/PrusaSlicer/commit/91897589928789b261ca0dc735ffd46f2b0b99f2
+        if (selected == 9999)
+            action_taken = true;
+        else
+            sidebar.jump_to_option(selected);*/
+        if (selected != 9999) {
+            imgui->end(); // end imgui before the jump to option
+            sidebar.jump_to_option(selected);
+            return true;
+        }
+        action_taken = true;
+    }
+
+    imgui->end();
+
+    return action_taken;
+}
+
+bool GLCanvas3D::_render_arrange_menu(float pos_x)
+{
+    ImGuiWrapper *imgui = wxGetApp().imgui();
+
+#if ENABLE_GL_SHADERS_ATTRIBUTES
+    imgui->set_next_window_pos(pos_x, m_main_toolbar.get_height(), ImGuiCond_Always, 0.5f, 0.0f);
+#else
+    auto canvas_w = float(get_canvas_size().get_width());
+    const float x = pos_x * float(wxGetApp().plater()->get_camera().get_zoom()) + 0.5f * canvas_w;
+    imgui->set_next_window_pos(x, m_main_toolbar.get_height(), ImGuiCond_Always, 0.5f, 0.0f);
+#endif // ENABLE_GL_SHADERS_ATTRIBUTES
+
+    imgui->begin(_L("Arrange options"), ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoCollapse);
+
+    ArrangeSettings settings = get_arrange_settings();
+    ArrangeSettings &settings_out = get_arrange_settings();
+
+    auto &appcfg = wxGetApp().app_config;
+    PrinterTechnology ptech = current_printer_technology();
+
+    bool settings_changed = false;
+    float dist_min = 0.f;
+    std::string dist_key = "min_object_distance", rot_key = "enable_rotation";
+    std::string postfix;
+
+    if (ptech == ptSLA) {
+        dist_min     = 0.f;
+        postfix      = "_sla";
+    } else if (ptech == ptFFF) {
+        auto co_opt = m_config->option<ConfigOptionBool>("complete_objects");
+        if (co_opt && co_opt->value) {
+            dist_min     = float(min_object_distance(*m_config));
+            postfix      = "_fff_seq_print";
+        } else {
+            dist_min     = 0.f;
+            postfix     = "_fff";
+        }
+    }
+
+    dist_key += postfix;
+    rot_key  += postfix;
+
+    imgui->text(GUI::format_wxstr(_L("Press %1%left mouse button to enter the exact value"), shortkey_ctrl_prefix()));
+
+    if (imgui->slider_float(_L("Spacing"), &settings.distance, dist_min, 100.0f, "%5.2f") || dist_min > settings.distance) {
+        settings.distance = std::max(dist_min, settings.distance);
+        settings_out.distance = settings.distance;
+        appcfg->set("arrange", dist_key.c_str(), float_to_string_decimal_point(settings_out.distance));
+        settings_changed = true;
+    }
+
+    if (imgui->checkbox(_L("Enable rotations (slow)"), settings.enable_rotation)) {
+        settings_out.enable_rotation = settings.enable_rotation;
+        appcfg->set("arrange", rot_key.c_str(), settings_out.enable_rotation? "1" : "0");
+        settings_changed = true;
+    }
+
+    ImGui::Separator();
+
+    if (imgui->button(_L("Reset"))) {
+        settings_out = ArrangeSettings{};
+        settings_out.distance = std::max(dist_min, settings_out.distance);
+        appcfg->set("arrange", dist_key.c_str(), float_to_string_decimal_point(settings_out.distance));
+        appcfg->set("arrange", rot_key.c_str(), settings_out.enable_rotation? "1" : "0");
+        settings_changed = true;
+    }
+
+    ImGui::SameLine();
+
+    if (imgui->button(_L("Arrange"))) {
+        wxGetApp().plater()->arrange();
+    }
+
+    imgui->end();
+
+    return settings_changed;
+}
+
+#define ENABLE_THUMBNAIL_GENERATOR_DEBUG_OUTPUT 0
+#if ENABLE_THUMBNAIL_GENERATOR_DEBUG_OUTPUT
+static void debug_output_thumbnail(const ThumbnailData& thumbnail_data)
+{
+    // debug export of generated image
+    wxImage image(thumbnail_data.width, thumbnail_data.height);
+    image.InitAlpha();
+
+    for (unsigned int r = 0; r < thumbnail_data.height; ++r)
+    {
+        unsigned int rr = (thumbnail_data.height - 1 - r) * thumbnail_data.width;
+        for (unsigned int c = 0; c < thumbnail_data.width; ++c)
+        {
+            unsigned char* px = (unsigned char*)thumbnail_data.pixels.data() + 4 * (rr + c);
+            image.SetRGB((int)c, (int)r, px[0], px[1], px[2]);
+            image.SetAlpha((int)c, (int)r, px[3]);
+        }
+    }
+
+    image.SaveFile("C:/prusa/test/test.png", wxBITMAP_TYPE_PNG);
+}
+#endif // ENABLE_THUMBNAIL_GENERATOR_DEBUG_OUTPUT
+
+void GLCanvas3D::_render_thumbnail_internal(ThumbnailData& thumbnail_data, const ThumbnailsParams& thumbnail_params, const GLVolumeCollection& volumes, Camera::EType camera_type)
+{
+    auto is_visible = [](const GLVolume& v) {
+        bool ret = v.printable;
+        ret &= (!v.shader_outside_printer_detection_enabled || !v.is_outside);
+        return ret;
+    };
+
+    GLVolumePtrs visible_volumes;
+
+    for (GLVolume* vol : volumes.volumes) {
+        if (!vol->is_modifier && !vol->is_wipe_tower && (!thumbnail_params.parts_only || vol->composite_id.volume_id >= 0)) {
+            if (!thumbnail_params.printable_only || is_visible(*vol))
+                visible_volumes.emplace_back(vol);
+        }
+    }
+
+    BoundingBoxf3 volumes_box;
+    if (!visible_volumes.empty()) {
+        for (const GLVolume* vol : visible_volumes) {
+            volumes_box.merge(vol->transformed_bounding_box());
+        }
+    }
+    else
+        // This happens for empty projects
+        volumes_box = m_bed.extended_bounding_box();
+
+    Camera camera;
+    camera.set_type(camera_type);
+    camera.set_scene_box(scene_bounding_box());
+    camera.apply_viewport(0, 0, thumbnail_data.width, thumbnail_data.height);
+    camera.zoom_to_box(volumes_box);
+#if !ENABLE_LEGACY_OPENGL_REMOVAL
+    camera.apply_view_matrix();
+#endif // !ENABLE_LEGACY_OPENGL_REMOVAL
+
+#if ENABLE_GL_SHADERS_ATTRIBUTES
+    const Transform3d& view_matrix = camera.get_view_matrix();
+#endif // ENABLE_GL_SHADERS_ATTRIBUTES
+
+    double near_z = -1.0;
+    double far_z = -1.0;
+
+    if (thumbnail_params.show_bed) {
+        // extends the near and far z of the frustrum to avoid the bed being clipped
+
+        // box in eye space
+#if ENABLE_GL_SHADERS_ATTRIBUTES
+        const BoundingBoxf3 t_bed_box = m_bed.extended_bounding_box().transformed(view_matrix);
+#else
+        const BoundingBoxf3 t_bed_box = m_bed.extended_bounding_box().transformed(camera.get_view_matrix());
+#endif // ENABLE_GL_SHADERS_ATTRIBUTES
+        near_z = -t_bed_box.max.z();
+        far_z = -t_bed_box.min.z();
+    }
+
+    camera.apply_projection(volumes_box, near_z, far_z);
+
+    GLShaderProgram* shader = wxGetApp().get_shader("gouraud_light");
+    if (shader == nullptr)
+        return;
+
+    if (thumbnail_params.transparent_background)
+        glsafe(::glClearColor(0.0f, 0.0f, 0.0f, 0.0f));
+
+    glsafe(::glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT));
+    glsafe(::glEnable(GL_DEPTH_TEST));
+
+    shader->start_using();
+    shader->set_uniform("emission_factor", 0.0f);
+
+#if ENABLE_GL_SHADERS_ATTRIBUTES
+    const Transform3d& projection_matrix = camera.get_projection_matrix();
+#endif // ENABLE_GL_SHADERS_ATTRIBUTES
+
+    for (GLVolume* vol : visible_volumes) {
+#if ENABLE_LEGACY_OPENGL_REMOVAL
+        vol->model.set_color((vol->printable && !vol->is_outside) ? (current_printer_technology() == ptSLA ? vol->color : ColorRGBA::ORANGE()) : ColorRGBA::GRAY());
+#else
+        shader->set_uniform("uniform_color", (vol->printable && !vol->is_outside) ? (current_printer_technology() == ptSLA ? vol->color : ColorRGBA::ORANGE()) : ColorRGBA::GRAY());
+#endif // ENABLE_LEGACY_OPENGL_REMOVAL
+        // the volume may have been deactivated by an active gizmo
+        const bool is_active = vol->is_active;
+        vol->is_active = true;
+#if ENABLE_GL_SHADERS_ATTRIBUTES
+        const Transform3d matrix = view_matrix * vol->world_matrix();
+        shader->set_uniform("view_model_matrix", matrix);
+        shader->set_uniform("projection_matrix", projection_matrix);
+        shader->set_uniform("normal_matrix", (Matrix3d)matrix.matrix().block(0, 0, 3, 3).inverse().transpose());
+#endif // ENABLE_GL_SHADERS_ATTRIBUTES
+        vol->render();
+        vol->is_active = is_active;
+    }
+
+    shader->stop_using();
+
+    glsafe(::glDisable(GL_DEPTH_TEST));
+
+    if (thumbnail_params.show_bed)
+#if ENABLE_GL_SHADERS_ATTRIBUTES
+        _render_bed(view_matrix, projection_matrix, !camera.is_looking_downward(), false);
+#else
+        _render_bed(!camera.is_looking_downward(), false);
+#endif // ENABLE_GL_SHADERS_ATTRIBUTES
+
+    // restore background color
+    if (thumbnail_params.transparent_background)
+        glsafe(::glClearColor(1.0f, 1.0f, 1.0f, 1.0f));
+}
+
+void GLCanvas3D::_render_thumbnail_framebuffer(ThumbnailData& thumbnail_data, unsigned int w, unsigned int h, const ThumbnailsParams& thumbnail_params, const GLVolumeCollection& volumes, Camera::EType camera_type)
+{
+    thumbnail_data.set(w, h);
+    if (!thumbnail_data.is_valid())
+        return;
+
+    bool multisample = m_multisample_allowed;
+    if (multisample)
+        glsafe(::glEnable(GL_MULTISAMPLE));
+
+    GLint max_samples;
+    glsafe(::glGetIntegerv(GL_MAX_SAMPLES, &max_samples));
+    GLsizei num_samples = max_samples / 2;
+
+    GLuint render_fbo;
+    glsafe(::glGenFramebuffers(1, &render_fbo));
+    glsafe(::glBindFramebuffer(GL_FRAMEBUFFER, render_fbo));
+
+    GLuint render_tex = 0;
+    GLuint render_tex_buffer = 0;
+    if (multisample) {
+        // use renderbuffer instead of texture to avoid the need to use glTexImage2DMultisample which is available only since OpenGL 3.2
+        glsafe(::glGenRenderbuffers(1, &render_tex_buffer));
+        glsafe(::glBindRenderbuffer(GL_RENDERBUFFER, render_tex_buffer));
+        glsafe(::glRenderbufferStorageMultisample(GL_RENDERBUFFER, num_samples, GL_RGBA8, w, h));
+        glsafe(::glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, render_tex_buffer));
+    }
+    else {
+        glsafe(::glGenTextures(1, &render_tex));
+        glsafe(::glBindTexture(GL_TEXTURE_2D, render_tex));
+        glsafe(::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr));
+        glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR));
+        glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR));
+        glsafe(::glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, render_tex, 0));
+    }
+
+    GLuint render_depth;
+    glsafe(::glGenRenderbuffers(1, &render_depth));
+    glsafe(::glBindRenderbuffer(GL_RENDERBUFFER, render_depth));
+    if (multisample)
+        glsafe(::glRenderbufferStorageMultisample(GL_RENDERBUFFER, num_samples, GL_DEPTH_COMPONENT24, w, h));
+    else
+        glsafe(::glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT, w, h));
+
+    glsafe(::glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, render_depth));
+
+    GLenum drawBufs[] = { GL_COLOR_ATTACHMENT0 };
+    glsafe(::glDrawBuffers(1, drawBufs));
+
+    if (::glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE) {
+        _render_thumbnail_internal(thumbnail_data, thumbnail_params, volumes, camera_type);
+
+        if (multisample) {
+            GLuint resolve_fbo;
+            glsafe(::glGenFramebuffers(1, &resolve_fbo));
+            glsafe(::glBindFramebuffer(GL_FRAMEBUFFER, resolve_fbo));
+
+            GLuint resolve_tex;
+            glsafe(::glGenTextures(1, &resolve_tex));
+            glsafe(::glBindTexture(GL_TEXTURE_2D, resolve_tex));
+            glsafe(::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr));
+            glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR));
+            glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR));
+            glsafe(::glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, resolve_tex, 0));
+
+            glsafe(::glDrawBuffers(1, drawBufs));
+
+            if (::glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE) {
+                glsafe(::glBindFramebuffer(GL_READ_FRAMEBUFFER, render_fbo));
+                glsafe(::glBindFramebuffer(GL_DRAW_FRAMEBUFFER, resolve_fbo));
+                glsafe(::glBlitFramebuffer(0, 0, w, h, 0, 0, w, h, GL_COLOR_BUFFER_BIT, GL_LINEAR));
+
+                glsafe(::glBindFramebuffer(GL_READ_FRAMEBUFFER, resolve_fbo));
+                glsafe(::glReadPixels(0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, (void*)thumbnail_data.pixels.data()));
+            }
+
+            glsafe(::glDeleteTextures(1, &resolve_tex));
+            glsafe(::glDeleteFramebuffers(1, &resolve_fbo));
+        }
+        else
+            glsafe(::glReadPixels(0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, (void*)thumbnail_data.pixels.data()));
+
+#if ENABLE_THUMBNAIL_GENERATOR_DEBUG_OUTPUT
+        debug_output_thumbnail(thumbnail_data);
+#endif // ENABLE_THUMBNAIL_GENERATOR_DEBUG_OUTPUT
+    }
+
+    glsafe(::glBindFramebuffer(GL_FRAMEBUFFER, 0));
+    glsafe(::glDeleteRenderbuffers(1, &render_depth));
+    if (render_tex_buffer != 0)
+        glsafe(::glDeleteRenderbuffers(1, &render_tex_buffer));
+    if (render_tex != 0)
+        glsafe(::glDeleteTextures(1, &render_tex));
+    glsafe(::glDeleteFramebuffers(1, &render_fbo));
+
+    if (multisample)
+        glsafe(::glDisable(GL_MULTISAMPLE));
+}
+
+void GLCanvas3D::_render_thumbnail_framebuffer_ext(ThumbnailData& thumbnail_data, unsigned int w, unsigned int h, const ThumbnailsParams& thumbnail_params, const GLVolumeCollection& volumes, Camera::EType camera_type)
+{
+    thumbnail_data.set(w, h);
+    if (!thumbnail_data.is_valid())
+        return;
+
+    bool multisample = m_multisample_allowed;
+    if (multisample)
+        glsafe(::glEnable(GL_MULTISAMPLE));
+
+    GLint max_samples;
+    glsafe(::glGetIntegerv(GL_MAX_SAMPLES_EXT, &max_samples));
+    GLsizei num_samples = max_samples / 2;
+
+    GLuint render_fbo;
+    glsafe(::glGenFramebuffersEXT(1, &render_fbo));
+    glsafe(::glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, render_fbo));
+
+    GLuint render_tex = 0;
+    GLuint render_tex_buffer = 0;
+    if (multisample) {
+        // use renderbuffer instead of texture to avoid the need to use glTexImage2DMultisample which is available only since OpenGL 3.2
+        glsafe(::glGenRenderbuffersEXT(1, &render_tex_buffer));
+        glsafe(::glBindRenderbufferEXT(GL_RENDERBUFFER_EXT, render_tex_buffer));
+        glsafe(::glRenderbufferStorageMultisampleEXT(GL_RENDERBUFFER_EXT, num_samples, GL_RGBA8, w, h));
+        glsafe(::glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, GL_RENDERBUFFER_EXT, render_tex_buffer));
+    }
+    else {
+        glsafe(::glGenTextures(1, &render_tex));
+        glsafe(::glBindTexture(GL_TEXTURE_2D, render_tex));
+        glsafe(::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr));
+        glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR));
+        glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR));
+        glsafe(::glFramebufferTexture2D(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, GL_TEXTURE_2D, render_tex, 0));
+    }
+
+    GLuint render_depth;
+    glsafe(::glGenRenderbuffersEXT(1, &render_depth));
+    glsafe(::glBindRenderbufferEXT(GL_RENDERBUFFER_EXT, render_depth));
+    if (multisample)
+        glsafe(::glRenderbufferStorageMultisampleEXT(GL_RENDERBUFFER_EXT, num_samples, GL_DEPTH_COMPONENT24, w, h));
+    else
+        glsafe(::glRenderbufferStorageEXT(GL_RENDERBUFFER_EXT, GL_DEPTH_COMPONENT, w, h));
+
+    glsafe(::glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, GL_RENDERBUFFER_EXT, render_depth));
+
+    GLenum drawBufs[] = { GL_COLOR_ATTACHMENT0 };
+    glsafe(::glDrawBuffers(1, drawBufs));
+
+    if (::glCheckFramebufferStatusEXT(GL_FRAMEBUFFER_EXT) == GL_FRAMEBUFFER_COMPLETE_EXT) {
+        _render_thumbnail_internal(thumbnail_data, thumbnail_params, volumes, camera_type);
+
+        if (multisample) {
+            GLuint resolve_fbo;
+            glsafe(::glGenFramebuffersEXT(1, &resolve_fbo));
+            glsafe(::glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, resolve_fbo));
+
+            GLuint resolve_tex;
+            glsafe(::glGenTextures(1, &resolve_tex));
+            glsafe(::glBindTexture(GL_TEXTURE_2D, resolve_tex));
+            glsafe(::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr));
+            glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR));
+            glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR));
+            glsafe(::glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, GL_TEXTURE_2D, resolve_tex, 0));
+
+            glsafe(::glDrawBuffers(1, drawBufs));
+
+            if (::glCheckFramebufferStatusEXT(GL_FRAMEBUFFER_EXT) == GL_FRAMEBUFFER_COMPLETE_EXT) {
+                glsafe(::glBindFramebufferEXT(GL_READ_FRAMEBUFFER_EXT, render_fbo));
+                glsafe(::glBindFramebufferEXT(GL_DRAW_FRAMEBUFFER_EXT, resolve_fbo));
+                glsafe(::glBlitFramebufferEXT(0, 0, w, h, 0, 0, w, h, GL_COLOR_BUFFER_BIT, GL_LINEAR));
+
+                glsafe(::glBindFramebufferEXT(GL_READ_FRAMEBUFFER_EXT, resolve_fbo));
+                glsafe(::glReadPixels(0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, (void*)thumbnail_data.pixels.data()));
+            }
+
+            glsafe(::glDeleteTextures(1, &resolve_tex));
+            glsafe(::glDeleteFramebuffersEXT(1, &resolve_fbo));
+        }
+        else
+            glsafe(::glReadPixels(0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, (void*)thumbnail_data.pixels.data()));
+
+#if ENABLE_THUMBNAIL_GENERATOR_DEBUG_OUTPUT
+        debug_output_thumbnail(thumbnail_data);
+#endif // ENABLE_THUMBNAIL_GENERATOR_DEBUG_OUTPUT
+    }
+
+    glsafe(::glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0));
+    glsafe(::glDeleteRenderbuffersEXT(1, &render_depth));
+    if (render_tex_buffer != 0)
+        glsafe(::glDeleteRenderbuffersEXT(1, &render_tex_buffer));
+    if (render_tex != 0)
+        glsafe(::glDeleteTextures(1, &render_tex));
+    glsafe(::glDeleteFramebuffersEXT(1, &render_fbo));
+
+    if (multisample)
+        glsafe(::glDisable(GL_MULTISAMPLE));
+}
+
+void GLCanvas3D::_render_thumbnail_legacy(ThumbnailData& thumbnail_data, unsigned int w, unsigned int h, const ThumbnailsParams& thumbnail_params, const GLVolumeCollection& volumes, Camera::EType camera_type)
+{
+    // check that thumbnail size does not exceed the default framebuffer size
+    const Size& cnv_size = get_canvas_size();
+    unsigned int cnv_w = (unsigned int)cnv_size.get_width();
+    unsigned int cnv_h = (unsigned int)cnv_size.get_height();
+    if (w > cnv_w || h > cnv_h) {
+        float ratio = std::min((float)cnv_w / (float)w, (float)cnv_h / (float)h);
+        w = (unsigned int)(ratio * (float)w);
+        h = (unsigned int)(ratio * (float)h);
+    }
+
+    thumbnail_data.set(w, h);
+    if (!thumbnail_data.is_valid())
+        return;
+
+    _render_thumbnail_internal(thumbnail_data, thumbnail_params, volumes, camera_type);
+
+    glsafe(::glReadPixels(0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, (void*)thumbnail_data.pixels.data()));
+#if ENABLE_THUMBNAIL_GENERATOR_DEBUG_OUTPUT
+    debug_output_thumbnail(thumbnail_data);
+#endif // ENABLE_THUMBNAIL_GENERATOR_DEBUG_OUTPUT
+
+    // restore the default framebuffer size to avoid flickering on the 3D scene
+    wxGetApp().plater()->get_camera().apply_viewport(0, 0, cnv_size.get_width(), cnv_size.get_height());
+}
+
+bool GLCanvas3D::_init_toolbars()
+{
+    if (!_init_main_toolbar())
+        return false;
+
+    if (!_init_undoredo_toolbar())
+        return false;
+
+    if (!_init_view_toolbar())
+        return false;
+
+    if (!_init_collapse_toolbar())
+        return false;
+
+    return true;
+}
+
+bool GLCanvas3D::_init_main_toolbar()
+{
+    if (!m_main_toolbar.is_enabled())
+        return true;
+
+    BackgroundTexture::Metadata background_data;
+    background_data.filename = "toolbar_background.png";
+    background_data.left = 16;
+    background_data.top = 16;
+    background_data.right = 16;
+    background_data.bottom = 16;
+
+    if (!m_main_toolbar.init(background_data)) {
+        // unable to init the toolbar texture, disable it
+        m_main_toolbar.set_enabled(false);
+        return true;
+    }
+    // init arrow
+#if ENABLE_GL_SHADERS_ATTRIBUTES
+    if (!m_main_toolbar.init_arrow("toolbar_arrow_2.svg"))
+#else
+    BackgroundTexture::Metadata arrow_data;
+    arrow_data.filename = "toolbar_arrow.svg";
+    arrow_data.left = 0;
+    arrow_data.top = 0;
+    arrow_data.right = 0;
+    arrow_data.bottom = 0;
+    if (!m_main_toolbar.init_arrow(arrow_data))
+#endif // ENABLE_GL_SHADERS_ATTRIBUTES
+        BOOST_LOG_TRIVIAL(error) << "Main toolbar failed to load arrow texture.";
+
+    // m_gizmos is created at constructor, thus we can init arrow here.
+#if ENABLE_GL_SHADERS_ATTRIBUTES
+    if (!m_gizmos.init_arrow("toolbar_arrow_2.svg"))
+#else
+    if (!m_gizmos.init_arrow(arrow_data))
+#endif // ENABLE_GL_SHADERS_ATTRIBUTES
+        BOOST_LOG_TRIVIAL(error) << "Gizmos manager failed to load arrow texture.";
+
+//    m_main_toolbar.set_layout_type(GLToolbar::Layout::Vertical);
+    m_main_toolbar.set_layout_type(GLToolbar::Layout::Horizontal);
+    m_main_toolbar.set_horizontal_orientation(GLToolbar::Layout::HO_Right);
+    m_main_toolbar.set_vertical_orientation(GLToolbar::Layout::VO_Top);
+    m_main_toolbar.set_border(5.0f);
+    m_main_toolbar.set_separator_size(5);
+    m_main_toolbar.set_gap_size(4);
+
+    GLToolbarItem::Data item;
+
+    item.name = "add";
+    item.icon_filename = "add.svg";
+    item.tooltip = _utf8(L("Add...")) + " [" + GUI::shortkey_ctrl_prefix() + "I]";
+    item.sprite_id = 0;
+    item.left.action_callback = [this]() { if (m_canvas != nullptr) wxPostEvent(m_canvas, SimpleEvent(EVT_GLTOOLBAR_ADD)); };
+    if (!m_main_toolbar.add_item(item))
+        return false;
+
+    item.name = "delete";
+    item.icon_filename = "remove.svg";
+    item.tooltip = _utf8(L("Delete")) + " [Del]";
+    item.sprite_id = 1;
+    item.left.action_callback = [this]() { if (m_canvas != nullptr) wxPostEvent(m_canvas, SimpleEvent(EVT_GLTOOLBAR_DELETE)); };
+    item.enabling_callback = []()->bool { return wxGetApp().plater()->can_delete(); };
+    if (!m_main_toolbar.add_item(item))
+        return false;
+
+    item.name = "deleteall";
+    item.icon_filename = "delete_all.svg";
+    item.tooltip = _utf8(L("Delete all")) + " [" + GUI::shortkey_ctrl_prefix() + "Del]";
+    item.sprite_id = 2;
+    item.left.action_callback = [this]() { if (m_canvas != nullptr) wxPostEvent(m_canvas, SimpleEvent(EVT_GLTOOLBAR_DELETE_ALL)); };
+    item.enabling_callback = []()->bool { return wxGetApp().plater()->can_delete_all(); };
+    if (!m_main_toolbar.add_item(item))
+        return false;
+
+    item.name = "arrange";
+    item.icon_filename = "arrange.svg";
+    item.tooltip = _utf8(L("Arrange")) + " [A]\n" + _utf8(L("Arrange selection")) + " [Shift+A]\n" + _utf8(L("Click right mouse button to show arrangement options"));
+    item.sprite_id = 3;
+    item.left.action_callback = [this]() { if (m_canvas != nullptr) wxPostEvent(m_canvas, SimpleEvent(EVT_GLTOOLBAR_ARRANGE)); };
+    item.enabling_callback = []()->bool { return wxGetApp().plater()->can_arrange(); };
+    item.right.toggable = true;
+    item.right.render_callback = [this](float left, float right, float, float) {
+        if (m_canvas != nullptr)
+            _render_arrange_menu(0.5f * (left + right));
+    };
+    if (!m_main_toolbar.add_item(item))
+        return false;
+
+    item.right.toggable = false;
+    item.right.render_callback = GLToolbarItem::Default_Render_Callback;
+
+    if (!m_main_toolbar.add_separator())
+        return false;
+
+    item.name = "copy";
+    item.icon_filename = "copy.svg";
+    item.tooltip = _utf8(L("Copy")) + " [" + GUI::shortkey_ctrl_prefix() + "C]";
+    item.sprite_id = 4;
+    item.left.action_callback = [this]() { if (m_canvas != nullptr) wxPostEvent(m_canvas, SimpleEvent(EVT_GLTOOLBAR_COPY)); };
+    item.enabling_callback = []()->bool { return wxGetApp().plater()->can_copy_to_clipboard(); };
+    if (!m_main_toolbar.add_item(item))
+        return false;
+
+    item.name = "paste";
+    item.icon_filename = "paste.svg";
+    item.tooltip = _utf8(L("Paste")) + " [" + GUI::shortkey_ctrl_prefix() + "V]";
+    item.sprite_id = 5;
+    item.left.action_callback = [this]() { if (m_canvas != nullptr) wxPostEvent(m_canvas, SimpleEvent(EVT_GLTOOLBAR_PASTE)); };
+    item.enabling_callback = []()->bool { return wxGetApp().plater()->can_paste_from_clipboard(); };
+    if (!m_main_toolbar.add_item(item))
+        return false;
+
+    if (!m_main_toolbar.add_separator())
+        return false;
+
+    item.name = "more";
+    item.icon_filename = "instance_add.svg";
+    item.tooltip = _utf8(L("Add instance")) + " [+]";
+    item.sprite_id = 6;
+    item.left.action_callback = [this]() { if (m_canvas != nullptr) wxPostEvent(m_canvas, SimpleEvent(EVT_GLTOOLBAR_MORE)); };
+    item.visibility_callback = []()->bool { return wxGetApp().get_mode() != comSimple; };
+    item.enabling_callback = []()->bool { return wxGetApp().plater()->can_increase_instances(); };
+
+    if (!m_main_toolbar.add_item(item))
+        return false;
+
+    item.name = "fewer";
+    item.icon_filename = "instance_remove.svg";
+    item.tooltip = _utf8(L("Remove instance")) + " [-]";
+    item.sprite_id = 7;
+    item.left.action_callback = [this]() { if (m_canvas != nullptr) wxPostEvent(m_canvas, SimpleEvent(EVT_GLTOOLBAR_FEWER)); };
+    item.visibility_callback = []()->bool { return wxGetApp().get_mode() != comSimple; };
+    item.enabling_callback = []()->bool { return wxGetApp().plater()->can_decrease_instances(); };
+    if (!m_main_toolbar.add_item(item))
+        return false;
+
+    if (!m_main_toolbar.add_separator())
+        return false;
+
+    item.name = "splitobjects";
+    item.icon_filename = "split_objects.svg";
+    item.tooltip = _utf8(L("Split to objects"));
+    item.sprite_id = 8;
+    item.left.action_callback = [this]() { if (m_canvas != nullptr) wxPostEvent(m_canvas, SimpleEvent(EVT_GLTOOLBAR_SPLIT_OBJECTS)); };
+    item.visibility_callback = GLToolbarItem::Default_Visibility_Callback;
+    item.enabling_callback = []()->bool { return wxGetApp().plater()->can_split_to_objects(); };
+    if (!m_main_toolbar.add_item(item))
+        return false;
+
+    item.name = "splitvolumes";
+    item.icon_filename = "split_parts.svg";
+    item.tooltip = _utf8(L("Split to parts"));
+    item.sprite_id = 9;
+    item.left.action_callback = [this]() { if (m_canvas != nullptr) wxPostEvent(m_canvas, SimpleEvent(EVT_GLTOOLBAR_SPLIT_VOLUMES)); };
+    item.visibility_callback = []()->bool { return wxGetApp().get_mode() != comSimple; };
+    item.enabling_callback = []()->bool { return wxGetApp().plater()->can_split_to_volumes(); };
+    if (!m_main_toolbar.add_item(item))
+        return false;
+
+    if (!m_main_toolbar.add_separator())
+        return false;
+
+    item.name = "settings";
+    item.icon_filename = "settings.svg";
+    item.tooltip = _u8L("Switch to Settings") + "\n" + "[" + GUI::shortkey_ctrl_prefix() + "2] - " + _u8L("Print Settings Tab")    + 
+                                                "\n" + "[" + GUI::shortkey_ctrl_prefix() + "3] - " + (current_printer_technology() == ptFFF ? _u8L("Filament Settings Tab") : _u8L("Material Settings Tab")) +
+                                                "\n" + "[" + GUI::shortkey_ctrl_prefix() + "4] - " + _u8L("Printer Settings Tab") ;
+    item.sprite_id = 10;
+    item.enabling_callback    = GLToolbarItem::Default_Enabling_Callback;
+    item.visibility_callback  = []() { return (wxGetApp().app_config->get("new_settings_layout_mode") == "1" ||
+                                               wxGetApp().app_config->get("dlg_settings_layout_mode") == "1"); };
+    item.left.action_callback = []() { wxGetApp().mainframe->select_tab(); };
+    if (!m_main_toolbar.add_item(item))
+        return false;
+
+    /*
+    if (!m_main_toolbar.add_separator())
+        return false;
+        */
+
+    item.name = "search";
+    item.icon_filename = "search_.svg";
+    item.tooltip = _utf8(L("Search")) + " [" + GUI::shortkey_ctrl_prefix() + "F]";
+    item.sprite_id = 11;
+    item.left.toggable = true;
+    item.left.render_callback = [this](float left, float right, float, float) {
+        if (m_canvas != nullptr) {
+            if (_render_search_list(0.5f * (left + right)))
+                _deactivate_search_toolbar_item();
+        }
+    };
+    item.left.action_callback   = GLToolbarItem::Default_Action_Callback;
+    item.visibility_callback    = GLToolbarItem::Default_Visibility_Callback;
+    item.enabling_callback      = GLToolbarItem::Default_Enabling_Callback;
+    if (!m_main_toolbar.add_item(item))
+        return false;
+
+    if (!m_main_toolbar.add_separator())
+        return false;
+
+    item.name = "layersediting";
+    item.icon_filename = "layers_white.svg";
+    item.tooltip = _utf8(L("Variable layer height"));
+    item.sprite_id = 12;
+    item.left.action_callback = [this]() { if (m_canvas != nullptr) wxPostEvent(m_canvas, SimpleEvent(EVT_GLTOOLBAR_LAYERSEDITING)); };
+    item.visibility_callback = [this]()->bool {
+        bool res = current_printer_technology() == ptFFF;
+        // turns off if changing printer technology
+        if (!res && m_main_toolbar.is_item_visible("layersediting") && m_main_toolbar.is_item_pressed("layersediting"))
+            force_main_toolbar_left_action(get_main_toolbar_item_id("layersediting"));
+
+        return res;
+    };
+    item.enabling_callback      = []()->bool { return wxGetApp().plater()->can_layers_editing(); };
+    item.left.render_callback   = GLToolbarItem::Default_Render_Callback;
+    if (!m_main_toolbar.add_item(item))
+        return false;
+
+    return true;
+}
+
+bool GLCanvas3D::_init_undoredo_toolbar()
+{
+    if (!m_undoredo_toolbar.is_enabled())
+        return true;
+
+    BackgroundTexture::Metadata background_data;
+    background_data.filename = "toolbar_background.png";
+    background_data.left = 16;
+    background_data.top = 16;
+    background_data.right = 16;
+    background_data.bottom = 16;
+
+    if (!m_undoredo_toolbar.init(background_data)) {
+        // unable to init the toolbar texture, disable it
+        m_undoredo_toolbar.set_enabled(false);
+        return true;
+    }
+
+    // init arrow
+#if ENABLE_GL_SHADERS_ATTRIBUTES
+    if (!m_undoredo_toolbar.init_arrow("toolbar_arrow_2.svg"))
+#else
+    BackgroundTexture::Metadata arrow_data;
+    arrow_data.filename = "toolbar_arrow.svg";
+    arrow_data.left = 0;
+    arrow_data.top = 0;
+    arrow_data.right = 0;
+    arrow_data.bottom = 0;
+    if (!m_undoredo_toolbar.init_arrow(arrow_data))
+#endif // ENABLE_GL_SHADERS_ATTRIBUTES
+        BOOST_LOG_TRIVIAL(error) << "Undo/Redo toolbar failed to load arrow texture.";
+
+//    m_undoredo_toolbar.set_layout_type(GLToolbar::Layout::Vertical);
+    m_undoredo_toolbar.set_layout_type(GLToolbar::Layout::Horizontal);
+    m_undoredo_toolbar.set_horizontal_orientation(GLToolbar::Layout::HO_Left);
+    m_undoredo_toolbar.set_vertical_orientation(GLToolbar::Layout::VO_Top);
+    m_undoredo_toolbar.set_border(5.0f);
+    m_undoredo_toolbar.set_separator_size(5);
+    m_undoredo_toolbar.set_gap_size(4);
+
+    GLToolbarItem::Data item;
+
+    item.name = "undo";
+    item.icon_filename = "undo_toolbar.svg";
+    item.tooltip = _utf8(L("Undo")) + " [" + GUI::shortkey_ctrl_prefix() + "Z]\n" + _utf8(L("Click right mouse button to open/close History"));
+    item.sprite_id = 0;
+    item.left.action_callback = [this]() { post_event(SimpleEvent(EVT_GLCANVAS_UNDO)); };
+    item.right.toggable = true;
+    item.right.action_callback = [this]() { m_imgui_undo_redo_hovered_pos = -1; };
+    item.right.render_callback = [this](float left, float right, float, float) {
+        if (m_canvas != nullptr) {
+            if (_render_undo_redo_stack(true, 0.5f * (left + right)))
+                _deactivate_undo_redo_toolbar_items();
+        }
+    };
+    item.enabling_callback = [this]()->bool {
+        bool can_undo = wxGetApp().plater()->can_undo();
+        int id = m_undoredo_toolbar.get_item_id("undo");
+
+        std::string curr_additional_tooltip;
+        m_undoredo_toolbar.get_additional_tooltip(id, curr_additional_tooltip);
+
+        std::string new_additional_tooltip;
+        if (can_undo) {
+        	std::string action;
+            wxGetApp().plater()->undo_redo_topmost_string_getter(true, action);
+            new_additional_tooltip = (boost::format(_utf8(L("Next Undo action: %1%"))) % action).str();
+        }
+
+        if (new_additional_tooltip != curr_additional_tooltip) {
+            m_undoredo_toolbar.set_additional_tooltip(id, new_additional_tooltip);
+            set_tooltip("");
+        }
+        return can_undo;
+    };
+
+    if (!m_undoredo_toolbar.add_item(item))
+        return false;
+
+    item.name = "redo";
+    item.icon_filename = "redo_toolbar.svg";
+    item.tooltip = _utf8(L("Redo")) + " [" + GUI::shortkey_ctrl_prefix() + "Y]\n" + _utf8(L("Click right mouse button to open/close History"));
+    item.sprite_id = 1;
+    item.left.action_callback = [this]() { post_event(SimpleEvent(EVT_GLCANVAS_REDO)); };
+    item.right.action_callback = [this]() { m_imgui_undo_redo_hovered_pos = -1; };
+    item.right.render_callback = [this](float left, float right, float, float) {
+        if (m_canvas != nullptr) {
+            if (_render_undo_redo_stack(false, 0.5f * (left + right)))
+                _deactivate_undo_redo_toolbar_items();
+        }
+    };
+    item.enabling_callback = [this]()->bool {
+        bool can_redo = wxGetApp().plater()->can_redo();
+        int id = m_undoredo_toolbar.get_item_id("redo");
+
+        std::string curr_additional_tooltip;
+        m_undoredo_toolbar.get_additional_tooltip(id, curr_additional_tooltip);
+
+        std::string new_additional_tooltip;
+        if (can_redo) {
+        	std::string action;
+            wxGetApp().plater()->undo_redo_topmost_string_getter(false, action);
+            new_additional_tooltip = (boost::format(_utf8(L("Next Redo action: %1%"))) % action).str();
+        }
+
+        if (new_additional_tooltip != curr_additional_tooltip) {
+            m_undoredo_toolbar.set_additional_tooltip(id, new_additional_tooltip);
+            set_tooltip("");
+        }
+        return can_redo;
+    };
+
+    if (!m_undoredo_toolbar.add_item(item))
+        return false;
+    /*
+    if (!m_undoredo_toolbar.add_separator())
+        return false;
+        */
+    return true;
+}
+
+bool GLCanvas3D::_init_view_toolbar()
+{
+    return wxGetApp().plater()->init_view_toolbar();
+}
+
+bool GLCanvas3D::_init_collapse_toolbar()
+{
+    return wxGetApp().plater()->init_collapse_toolbar();
+}
+
+bool GLCanvas3D::_set_current()
+{
+    return m_context != nullptr && m_canvas->SetCurrent(*m_context);
+}
+
+void GLCanvas3D::_resize(unsigned int w, unsigned int h)
+{
+    if (m_canvas == nullptr && m_context == nullptr)
+        return;
+
+    const std::array<unsigned int, 2> new_size = { w, h };
+    if (m_old_size == new_size)
+        return;
+
+    m_old_size = new_size;
+
+    auto *imgui = wxGetApp().imgui();
+    imgui->set_display_size(static_cast<float>(w), static_cast<float>(h));
+    const float font_size = 1.5f * wxGetApp().em_unit();
+#if ENABLE_RETINA_GL
+    imgui->set_scaling(font_size, 1.0f, m_retina_helper->get_scale_factor());
+#else
+    imgui->set_scaling(font_size, m_canvas->GetContentScaleFactor(), 1.0f);
+#endif
+
+    this->request_extra_frame();
+
+    // ensures that this canvas is current
+    _set_current();
+}
+
+BoundingBoxf3 GLCanvas3D::_max_bounding_box(bool include_gizmos, bool include_bed_model) const
+{
+    BoundingBoxf3 bb = volumes_bounding_box();
+
+    // The following is a workaround for gizmos not being taken in account when calculating the tight camera frustrum
+    // A better solution would ask the gizmo manager for the bounding box of the current active gizmo, if any
+    if (include_gizmos && m_gizmos.is_running()) {
+        const BoundingBoxf3 sel_bb = m_selection.get_bounding_box();
+        const Vec3d sel_bb_center = sel_bb.center();
+        const Vec3d extend_by = sel_bb.max_size() * Vec3d::Ones();
+        bb.merge(BoundingBoxf3(sel_bb_center - extend_by, sel_bb_center + extend_by));
+    }
+
+    const BoundingBoxf3 bed_bb = include_bed_model ? m_bed.extended_bounding_box() : m_bed.build_volume().bounding_volume();
+    bb.merge(bed_bb);
+
+    if (!m_main_toolbar.is_enabled())
+        bb.merge(m_gcode_viewer.get_max_bounding_box());
+
+    // clamp max bb size with respect to bed bb size
+    if (!m_picking_enabled) {
+        static const double max_scale_factor = 2.0;
+        const Vec3d bb_size = bb.size();
+        const Vec3d bed_bb_size = m_bed.build_volume().bounding_volume().size();
+        if (bb_size.x() > max_scale_factor * bed_bb_size.x() ||
+            bb_size.y() > max_scale_factor * bed_bb_size.y() ||
+            bb_size.z() > max_scale_factor * bed_bb_size.z()) {
+            const Vec3d bed_bb_center = bed_bb.center();
+            const Vec3d extend_by = max_scale_factor * bed_bb_size;
+            bb = BoundingBoxf3(bed_bb_center - extend_by, bed_bb_center + extend_by);
+        }
+    }
+
+    return bb;
+}
+
+void GLCanvas3D::_zoom_to_box(const BoundingBoxf3& box, double margin_factor)
+{
+    wxGetApp().plater()->get_camera().zoom_to_box(box, margin_factor);
+    m_dirty = true;
+}
+
+void GLCanvas3D::_update_camera_zoom(double zoom)
+{
+    wxGetApp().plater()->get_camera().update_zoom(zoom);
+    m_dirty = true;
+}
+
+void GLCanvas3D::_refresh_if_shown_on_screen()
+{
+    if (_is_shown_on_screen()) {
+        const Size& cnv_size = get_canvas_size();
+        _resize((unsigned int)cnv_size.get_width(), (unsigned int)cnv_size.get_height());
+
+        // Because of performance problems on macOS, where PaintEvents are not delivered
+        // frequently enough, we call render() here directly when we can.
+        render();
+    }
+}
+
+void GLCanvas3D::_picking_pass()
+{
+    if (m_picking_enabled && !m_mouse.dragging && m_mouse.position != Vec2d(DBL_MAX, DBL_MAX)) {
+        m_hover_volume_idxs.clear();
+
+        // Render the object for picking.
+        // FIXME This cannot possibly work in a multi - sampled context as the color gets mangled by the anti - aliasing.
+        // Better to use software ray - casting on a bounding - box hierarchy.
+
+        if (m_multisample_allowed)
+        	// This flag is often ignored by NVIDIA drivers if rendering into a screen buffer.
+            glsafe(::glDisable(GL_MULTISAMPLE));
+
+        glsafe(::glDisable(GL_BLEND));
+        glsafe(::glEnable(GL_DEPTH_TEST));
+
+        glsafe(::glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT));
+
+#if !ENABLE_GL_SHADERS_ATTRIBUTES
+        m_camera_clipping_plane = m_gizmos.get_clipping_plane();
+        if (m_camera_clipping_plane.is_active()) {
+            ::glClipPlane(GL_CLIP_PLANE0, (GLdouble*)m_camera_clipping_plane.get_data().data());
+            ::glEnable(GL_CLIP_PLANE0);
+        }
+#endif // !ENABLE_GL_SHADERS_ATTRIBUTES
+        _render_volumes_for_picking();
+#if !ENABLE_GL_SHADERS_ATTRIBUTES
+        if (m_camera_clipping_plane.is_active())
+            ::glDisable(GL_CLIP_PLANE0);
+#endif // !ENABLE_GL_SHADERS_ATTRIBUTES
+
+#if ENABLE_GL_SHADERS_ATTRIBUTES
+        const Camera& camera = wxGetApp().plater()->get_camera();
+        _render_bed_for_picking(camera.get_view_matrix(), camera.get_projection_matrix(), !camera.is_looking_downward());
+#else
+        _render_bed_for_picking(!wxGetApp().plater()->get_camera().is_looking_downward());
+#endif // ENABLE_GL_SHADERS_ATTRIBUTES
+
+        m_gizmos.render_current_gizmo_for_picking_pass();
+
+        if (m_multisample_allowed)
+            glsafe(::glEnable(GL_MULTISAMPLE));
+
+        int volume_id = -1;
+        int gizmo_id = -1;
+
+        std::array<GLubyte, 4> color = { 0, 0, 0, 0 };
+        const Size& cnv_size = get_canvas_size();
+        bool inside = 0 <= m_mouse.position(0) && m_mouse.position(0) < cnv_size.get_width() && 0 <= m_mouse.position(1) && m_mouse.position(1) < cnv_size.get_height();
+        if (inside) {
+            glsafe(::glReadPixels(m_mouse.position(0), cnv_size.get_height() - m_mouse.position.y() - 1, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, (void*)color.data()));
+            if (picking_checksum_alpha_channel(color[0], color[1], color[2]) == color[3]) {
+                // Only non-interpolated colors are valid, those have their lowest three bits zeroed.
+                // we reserve color = (0,0,0) for occluders (as the printbed) 
+                // volumes' id are shifted by 1
+                // see: _render_volumes_for_picking()
+                unsigned int id = picking_encode(color[0], color[1], color[2]);
+                volume_id = id - 1;
+                // gizmos' id are instead properly encoded by the color
+                gizmo_id = id;
+            }
+        }
+        if (0 <= volume_id && volume_id < (int)m_volumes.volumes.size()) {
+            // do not add the volume id if any gizmo is active and CTRL is pressed
+            if (m_gizmos.get_current_type() == GLGizmosManager::EType::Undefined || !wxGetKeyState(WXK_CONTROL))
+                m_hover_volume_idxs.emplace_back(volume_id);
+            m_gizmos.set_hover_id(-1);
+        }
+        else
+            m_gizmos.set_hover_id(inside && (unsigned int)gizmo_id <= GLGizmoBase::BASE_ID ? ((int)GLGizmoBase::BASE_ID - gizmo_id) : -1);
+
+        _update_volumes_hover_state();
+    }
+}
+
+void GLCanvas3D::_rectangular_selection_picking_pass()
+{
+    m_gizmos.set_hover_id(-1);
+
+    std::set<int> idxs;
+
+    if (m_picking_enabled) {
+        if (m_multisample_allowed)
+        	// This flag is often ignored by NVIDIA drivers if rendering into a screen buffer.
+            glsafe(::glDisable(GL_MULTISAMPLE));
+
+        glsafe(::glDisable(GL_BLEND));
+        glsafe(::glEnable(GL_DEPTH_TEST));
+
+        glsafe(::glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT));
+
+        _render_volumes_for_picking();
+#if ENABLE_GL_SHADERS_ATTRIBUTES
+        const Camera& camera = wxGetApp().plater()->get_camera();
+        _render_bed_for_picking(camera.get_view_matrix(), camera.get_projection_matrix(), !camera.is_looking_downward());
+#else
+        _render_bed_for_picking(!wxGetApp().plater()->get_camera().is_looking_downward());
+#endif // ENABLE_GL_SHADERS_ATTRIBUTES
+
+        if (m_multisample_allowed)
+            glsafe(::glEnable(GL_MULTISAMPLE));
+
+        int width = std::max((int)m_rectangle_selection.get_width(), 1);
+        int height = std::max((int)m_rectangle_selection.get_height(), 1);
+        int px_count = width * height;
+
+        int left = (int)m_rectangle_selection.get_left();
+        int top = get_canvas_size().get_height() - (int)m_rectangle_selection.get_top();
+        if (left >= 0 && top >= 0) {
+#define USE_PARALLEL 1
+#if USE_PARALLEL
+            struct Pixel
+            {
+                std::array<GLubyte, 4> data;
+            	// Only non-interpolated colors are valid, those have their lowest three bits zeroed.
+                bool valid() const { return picking_checksum_alpha_channel(data[0], data[1], data[2]) == data[3]; }
+                // we reserve color = (0,0,0) for occluders (as the printbed) 
+                // volumes' id are shifted by 1
+                // see: _render_volumes_for_picking()
+                int id() const { return data[0] + (data[1] << 8) + (data[2] << 16) - 1; }
+            };
+
+            std::vector<Pixel> frame(px_count);
+            glsafe(::glReadPixels(left, top, width, height, GL_RGBA, GL_UNSIGNED_BYTE, (void*)frame.data()));
+
+            tbb::spin_mutex mutex;
+            tbb::parallel_for(tbb::blocked_range<size_t>(0, frame.size(), (size_t)width),
+                [this, &frame, &idxs, &mutex](const tbb::blocked_range<size_t>& range) {
+                for (size_t i = range.begin(); i < range.end(); ++i)
+                	if (frame[i].valid()) {
+                    	int volume_id = frame[i].id();
+                    	if (0 <= volume_id && volume_id < (int)m_volumes.volumes.size()) {
+                        	mutex.lock();
+                        	idxs.insert(volume_id);
+                        	mutex.unlock();
+                    	}
+                	}
+            });
+#else
+            std::vector<GLubyte> frame(4 * px_count);
+            glsafe(::glReadPixels(left, top, width, height, GL_RGBA, GL_UNSIGNED_BYTE, (void*)frame.data()));
+
+            for (int i = 0; i < px_count; ++i)
+            {
+                int px_id = 4 * i;
+                int volume_id = frame[px_id] + (frame[px_id + 1] << 8) + (frame[px_id + 2] << 16);
+                if (0 <= volume_id && volume_id < (int)m_volumes.volumes.size())
+                    idxs.insert(volume_id);
+            }
+#endif // USE_PARALLEL
+        }
+    }
+
+    m_hover_volume_idxs.assign(idxs.begin(), idxs.end());
+    _update_volumes_hover_state();
+}
+
+void GLCanvas3D::_render_background()
+{
+    bool use_error_color = false;
+    if (wxGetApp().is_editor()) {
+        use_error_color = m_dynamic_background_enabled &&
+        (current_printer_technology() != ptSLA || !m_volumes.empty());
+
+        if (!m_volumes.empty())
+            use_error_color &= _is_any_volume_outside();
+        else
+            use_error_color &= m_gcode_viewer.has_data() && !m_gcode_viewer.is_contained_in_bed();
+    }
+
+#if !ENABLE_GL_SHADERS_ATTRIBUTES
+    glsafe(::glPushMatrix());
+    glsafe(::glLoadIdentity());
+    glsafe(::glMatrixMode(GL_PROJECTION));
+    glsafe(::glPushMatrix());
+    glsafe(::glLoadIdentity());
+#endif // !ENABLE_GL_SHADERS_ATTRIBUTES
+
+    // Draws a bottom to top gradient over the complete screen.
+    glsafe(::glDisable(GL_DEPTH_TEST));
+
+#if ENABLE_LEGACY_OPENGL_REMOVAL
+    const ColorRGBA bottom_color = use_error_color ? ERROR_BG_DARK_COLOR : DEFAULT_BG_DARK_COLOR;
+
+    if (!m_background.is_initialized()) {
+        m_background.reset();
+
+        GLModel::Geometry init_data;
+        init_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P2T2 };
+        init_data.reserve_vertices(4);
+        init_data.reserve_indices(6);
+
+        // vertices
+        init_data.add_vertex(Vec2f(-1.0f, -1.0f), Vec2f(0.0f, 0.0f));
+        init_data.add_vertex(Vec2f(1.0f, -1.0f),  Vec2f(1.0f, 0.0f));
+        init_data.add_vertex(Vec2f(1.0f, 1.0f),   Vec2f(1.0f, 1.0f));
+        init_data.add_vertex(Vec2f(-1.0f, 1.0f),  Vec2f(0.0f, 1.0f));
+
+        // indices
+        init_data.add_triangle(0, 1, 2);
+        init_data.add_triangle(2, 3, 0);
+
+        m_background.init_from(std::move(init_data));
+    }
+
+    GLShaderProgram* shader = wxGetApp().get_shader("background");
+    if (shader != nullptr) {
+        shader->start_using();
+        shader->set_uniform("top_color", use_error_color ? ERROR_BG_LIGHT_COLOR : DEFAULT_BG_LIGHT_COLOR);
+        shader->set_uniform("bottom_color", bottom_color);
+        m_background.render();
+        shader->stop_using();
+    }
+#else
+    ::glBegin(GL_QUADS);
+    ::glColor3fv(use_error_color ? ERROR_BG_DARK_COLOR.data(): DEFAULT_BG_DARK_COLOR.data());
+    ::glVertex2f(-1.0f, -1.0f);
+    ::glVertex2f(1.0f, -1.0f);
+
+    ::glColor3fv(use_error_color ? ERROR_BG_LIGHT_COLOR.data() : DEFAULT_BG_LIGHT_COLOR.data());
+    ::glVertex2f(1.0f, 1.0f);
+    ::glVertex2f(-1.0f, 1.0f);
+    glsafe(::glEnd());
+#endif // ENABLE_LEGACY_OPENGL_REMOVAL
+
+    glsafe(::glEnable(GL_DEPTH_TEST));
+
+#if !ENABLE_GL_SHADERS_ATTRIBUTES
+    glsafe(::glPopMatrix());
+    glsafe(::glMatrixMode(GL_MODELVIEW));
+    glsafe(::glPopMatrix());
+#endif // !ENABLE_GL_SHADERS_ATTRIBUTES
+}
+
+#if ENABLE_GL_SHADERS_ATTRIBUTES
+void GLCanvas3D::_render_bed(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool show_axes)
+#else
+void GLCanvas3D::_render_bed(bool bottom, bool show_axes)
+#endif // ENABLE_GL_SHADERS_ATTRIBUTES
+{
+    float scale_factor = 1.0;
+#if ENABLE_RETINA_GL
+    scale_factor = m_retina_helper->get_scale_factor();
+#endif // ENABLE_RETINA_GL
+
+    bool show_texture = ! bottom ||
+            (m_gizmos.get_current_type() != GLGizmosManager::FdmSupports
+          && m_gizmos.get_current_type() != GLGizmosManager::SlaSupports
+          && m_gizmos.get_current_type() != GLGizmosManager::Hollow
+          && m_gizmos.get_current_type() != GLGizmosManager::Seam
+          && m_gizmos.get_current_type() != GLGizmosManager::MmuSegmentation);
+
+#if ENABLE_GL_SHADERS_ATTRIBUTES
+    m_bed.render(*this, view_matrix, projection_matrix, bottom, scale_factor, show_axes, show_texture);
+#else
+    m_bed.render(*this, bottom, scale_factor, show_axes, show_texture);
+#endif // ENABLE_GL_SHADERS_ATTRIBUTES
+}
+
+#if ENABLE_GL_SHADERS_ATTRIBUTES
+void GLCanvas3D::_render_bed_for_picking(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom)
+#else
+void GLCanvas3D::_render_bed_for_picking(bool bottom)
+#endif // ENABLE_GL_SHADERS_ATTRIBUTES
+{
+    float scale_factor = 1.0;
+#if ENABLE_RETINA_GL
+    scale_factor = m_retina_helper->get_scale_factor();
+#endif // ENABLE_RETINA_GL
+
+#if ENABLE_GL_SHADERS_ATTRIBUTES
+    m_bed.render_for_picking(*this, view_matrix, projection_matrix, bottom, scale_factor);
+#else
+    m_bed.render_for_picking(*this, bottom, scale_factor);
+#endif // ENABLE_GL_SHADERS_ATTRIBUTES
+}
+
+void GLCanvas3D::_render_objects(GLVolumeCollection::ERenderType type)
+{
+    if (m_volumes.empty())
+        return;
+
+    glsafe(::glEnable(GL_DEPTH_TEST));
+
+    m_camera_clipping_plane = m_gizmos.get_clipping_plane();
+
+    if (m_picking_enabled)
+        // Update the layer editing selection to the first object selected, update the current object maximum Z.
+        m_layers_editing.select_object(*m_model, this->is_layers_editing_enabled() ? m_selection.get_object_idx() : -1);
+
+    if (const BuildVolume &build_volume = m_bed.build_volume(); build_volume.valid()) {
+        switch (build_volume.type()) {
+        case BuildVolume::Type::Rectangle: {
+            const BoundingBox3Base<Vec3d> bed_bb = build_volume.bounding_volume().inflated(BuildVolume::SceneEpsilon);
+            m_volumes.set_print_volume({ 0, // circle
+                { float(bed_bb.min.x()), float(bed_bb.min.y()), float(bed_bb.max.x()), float(bed_bb.max.y()) },
+                { 0.0f, float(build_volume.max_print_height()) } });
+            break;
+        }
+        case BuildVolume::Type::Circle: {
+            m_volumes.set_print_volume({ 1, // rectangle
+                { unscaled<float>(build_volume.circle().center.x()), unscaled<float>(build_volume.circle().center.y()), unscaled<float>(build_volume.circle().radius + BuildVolume::SceneEpsilon), 0.0f },
+                { 0.0f, float(build_volume.max_print_height() + BuildVolume::SceneEpsilon) } });
+            break;
+        }
+        default:
+        case BuildVolume::Type::Convex:
+        case BuildVolume::Type::Custom: {
+            m_volumes.set_print_volume({ static_cast<int>(type),
+                { -FLT_MAX, -FLT_MAX, FLT_MAX, FLT_MAX },
+                { -FLT_MAX, FLT_MAX } }
+            );
+        }
+        }
+        if (m_requires_check_outside_state) {
+            m_volumes.check_outside_state(build_volume, nullptr);
+            m_requires_check_outside_state = false;
+        }
+    }
+
+    if (m_use_clipping_planes)
+        m_volumes.set_z_range(-m_clipping_planes[0].get_data()[3], m_clipping_planes[1].get_data()[3]);
+    else
+        m_volumes.set_z_range(-FLT_MAX, FLT_MAX);
+
+    m_volumes.set_clipping_plane(m_camera_clipping_plane.get_data());
+    m_volumes.set_show_sinking_contours(! m_gizmos.is_hiding_instances());
+#if ENABLE_SHOW_NON_MANIFOLD_EDGES
+    m_volumes.set_show_non_manifold_edges(!m_gizmos.is_hiding_instances() && m_gizmos.get_current_type() != GLGizmosManager::Simplify);
+#endif // ENABLE_SHOW_NON_MANIFOLD_EDGES
+
+    GLShaderProgram* shader = wxGetApp().get_shader("gouraud");
+    if (shader != nullptr) {
+        shader->start_using();
+
+        switch (type)
+        {
+        default:
+        case GLVolumeCollection::ERenderType::Opaque:
+        {
+            if (m_picking_enabled && !m_gizmos.is_dragging() && m_layers_editing.is_enabled() && (m_layers_editing.last_object_id != -1) && (m_layers_editing.object_max_z() > 0.0f)) {
+                int object_id = m_layers_editing.last_object_id;
+#if ENABLE_GL_SHADERS_ATTRIBUTES
+                const Camera& camera = wxGetApp().plater()->get_camera();
+                m_volumes.render(type, false, camera.get_view_matrix(), camera.get_projection_matrix(), [object_id](const GLVolume& volume) {
+                    // Which volume to paint without the layer height profile shader?
+                    return volume.is_active && (volume.is_modifier || volume.composite_id.object_id != object_id);
+                    });
+#else
+                m_volumes.render(type, false, wxGetApp().plater()->get_camera().get_view_matrix(), [object_id](const GLVolume& volume) {
+                    // Which volume to paint without the layer height profile shader?
+                    return volume.is_active && (volume.is_modifier || volume.composite_id.object_id != object_id);
+                    });
+#endif // ENABLE_GL_SHADERS_ATTRIBUTES
+                // Let LayersEditing handle rendering of the active object using the layer height profile shader.
+                m_layers_editing.render_volumes(*this, m_volumes);
+            }
+            else {
+                // do not cull backfaces to show broken geometry, if any
+#if ENABLE_GL_SHADERS_ATTRIBUTES
+                const Camera& camera = wxGetApp().plater()->get_camera();
+                m_volumes.render(type, m_picking_enabled, camera.get_view_matrix(), camera.get_projection_matrix(), [this](const GLVolume& volume) {
+                    return (m_render_sla_auxiliaries || volume.composite_id.volume_id >= 0);
+                    });
+#else
+                m_volumes.render(type, m_picking_enabled, wxGetApp().plater()->get_camera().get_view_matrix(), [this](const GLVolume& volume) {
+                    return (m_render_sla_auxiliaries || volume.composite_id.volume_id >= 0);
+                    });
+#endif // ENABLE_GL_SHADERS_ATTRIBUTES
+            }
+
+            // In case a painting gizmo is open, it should render the painted triangles
+            // before transparent objects are rendered. Otherwise they would not be
+            // visible when inside modifier meshes etc.
+            {
+                GLGizmosManager& gm = get_gizmos_manager();
+//                GLGizmosManager::EType type = gm.get_current_type();
+                if (dynamic_cast<GLGizmoPainterBase*>(gm.get_current())) {
+                    shader->stop_using();
+                    gm.render_painter_gizmo();
+                    shader->start_using();
+                }
+            }
+            break;
+        }
+        case GLVolumeCollection::ERenderType::Transparent:
+        {
+#if ENABLE_GL_SHADERS_ATTRIBUTES
+            const Camera& camera = wxGetApp().plater()->get_camera();
+            m_volumes.render(type, false, camera.get_view_matrix(), camera.get_projection_matrix());
+#else
+            m_volumes.render(type, false, wxGetApp().plater()->get_camera().get_view_matrix());
+#endif // ENABLE_GL_SHADERS_ATTRIBUTES
+            break;
+        }
+        }
+        shader->stop_using();
+    }
+
+    m_camera_clipping_plane = ClippingPlane::ClipsNothing();
+}
+
+void GLCanvas3D::_render_gcode()
+{
+    m_gcode_viewer.render();
+}
+
+#if ENABLE_SHOW_TOOLPATHS_COG
+void GLCanvas3D::_render_gcode_cog()
+{
+    m_gcode_viewer.render_cog();
+}
+#endif // ENABLE_SHOW_TOOLPATHS_COG
+
+void GLCanvas3D::_render_selection()
+{
+    float scale_factor = 1.0;
+#if ENABLE_RETINA_GL
+    scale_factor = m_retina_helper->get_scale_factor();
+#endif // ENABLE_RETINA_GL
+
+    if (!m_gizmos.is_running())
+        m_selection.render(scale_factor);
+}
+
+void GLCanvas3D::_render_sequential_clearance()
+{
+    if (m_layers_editing.is_enabled() || m_gizmos.is_dragging())
+        return;
+
+    switch (m_gizmos.get_current_type())
+    {
+    case GLGizmosManager::EType::Flatten:
+    case GLGizmosManager::EType::Cut:
+    case GLGizmosManager::EType::Hollow:
+    case GLGizmosManager::EType::SlaSupports:
+    case GLGizmosManager::EType::FdmSupports:
+    case GLGizmosManager::EType::Seam: { return; }
+    default: { break; }
+    }
+ 
+    m_sequential_print_clearance.render();
+}
+
+#if ENABLE_RENDER_SELECTION_CENTER
+void GLCanvas3D::_render_selection_center()
+{
+    m_selection.render_center(m_gizmos.is_dragging());
+}
+#endif // ENABLE_RENDER_SELECTION_CENTER
+
+void GLCanvas3D::_check_and_update_toolbar_icon_scale()
+{
+    // Don't update a toolbar scale, when we are on a Preview
+    if (wxGetApp().plater()->is_preview_shown())
+        return;
+
+    float scale = wxGetApp().toolbar_icon_scale();
+    Size cnv_size = get_canvas_size();
+
+    float size = GLToolbar::Default_Icons_Size * scale;
+
+    // Set current size for all top toolbars. It will be used for next calculations
+    GLToolbar& collapse_toolbar = wxGetApp().plater()->get_collapse_toolbar();
+#if ENABLE_RETINA_GL
+    const float sc = m_retina_helper->get_scale_factor() * scale;
+    m_main_toolbar.set_scale(sc);
+    m_undoredo_toolbar.set_scale(sc);
+    collapse_toolbar.set_scale(sc);
+    size *= m_retina_helper->get_scale_factor();
+#else
+    m_main_toolbar.set_icons_size(size);
+    m_undoredo_toolbar.set_icons_size(size);
+    collapse_toolbar.set_icons_size(size);
+#endif // ENABLE_RETINA_GL
+
+    float top_tb_width = m_main_toolbar.get_width() + m_undoredo_toolbar.get_width() + collapse_toolbar.get_width();
+    int   items_cnt = m_main_toolbar.get_visible_items_cnt() + m_undoredo_toolbar.get_visible_items_cnt() + collapse_toolbar.get_visible_items_cnt();
+    float noitems_width = top_tb_width - size * items_cnt; // width of separators and borders in top toolbars 
+
+    // calculate scale needed for items in all top toolbars
+    float new_h_scale = (cnv_size.get_width() - noitems_width) / (items_cnt * GLToolbar::Default_Icons_Size);
+
+    items_cnt = m_gizmos.get_selectable_icons_cnt() + 3; // +3 means a place for top and view toolbars and separators in gizmos toolbar
+
+    // calculate scale needed for items in the gizmos toolbar
+    float new_v_scale = cnv_size.get_height() / (items_cnt * GLGizmosManager::Default_Icons_Size);
+
+    // set minimum scale as a auto scale for the toolbars
+    float new_scale = std::min(new_h_scale, new_v_scale);
+#if ENABLE_RETINA_GL
+    new_scale /= m_retina_helper->get_scale_factor();
+#endif
+    if (fabs(new_scale - scale) > 0.01) // scale is changed by 1% and more
+        wxGetApp().set_auto_toolbar_icon_scale(new_scale);
+}
+
+void GLCanvas3D::_render_overlays()
+{
+    glsafe(::glDisable(GL_DEPTH_TEST));
+#if !ENABLE_GL_SHADERS_ATTRIBUTES
+    glsafe(::glPushMatrix());
+    glsafe(::glLoadIdentity());
+    // ensure that the textures are renderered inside the frustrum
+    const Camera& camera = wxGetApp().plater()->get_camera();
+    glsafe(::glTranslated(0.0, 0.0, -(camera.get_near_z() + 0.005)));
+    // ensure that the overlay fits the frustrum near z plane
+    double gui_scale = camera.get_gui_scale();
+    glsafe(::glScaled(gui_scale, gui_scale, 1.0));
+#endif // !ENABLE_GL_SHADERS_ATTRIBUTES
+
+    _check_and_update_toolbar_icon_scale();
+
+    _render_gizmos_overlay();
+
+    // main toolbar and undoredo toolbar need to be both updated before rendering because both their sizes are needed
+    // to correctly place them
+#if ENABLE_RETINA_GL
+    const float scale = m_retina_helper->get_scale_factor() * wxGetApp().toolbar_icon_scale(/*true*/);
+    m_main_toolbar.set_scale(scale);
+    m_undoredo_toolbar.set_scale(scale);
+    wxGetApp().plater()->get_collapse_toolbar().set_scale(scale);
+#else
+    const float size = int(GLToolbar::Default_Icons_Size * wxGetApp().toolbar_icon_scale(/*true*/));
+    m_main_toolbar.set_icons_size(size);
+    m_undoredo_toolbar.set_icons_size(size);
+    wxGetApp().plater()->get_collapse_toolbar().set_icons_size(size);
+#endif // ENABLE_RETINA_GL
+
+    _render_main_toolbar();
+    _render_undoredo_toolbar();
+    _render_collapse_toolbar();
+    _render_view_toolbar();
+
+    if (m_layers_editing.last_object_id >= 0 && m_layers_editing.object_max_z() > 0.0f)
+        m_layers_editing.render_overlay(*this);
+
+    const ConfigOptionBool* opt = dynamic_cast<const ConfigOptionBool*>(m_config->option("complete_objects"));
+    bool sequential_print = opt != nullptr && opt->value;
+    std::vector<const ModelInstance*> sorted_instances;
+    if (sequential_print) {
+        for (ModelObject* model_object : m_model->objects)
+            for (ModelInstance* model_instance : model_object->instances) {
+                sorted_instances.emplace_back(model_instance);
+            }
+    }
+    m_labels.render(sorted_instances);
+
+#if !ENABLE_GL_SHADERS_ATTRIBUTES
+    glsafe(::glPopMatrix());
+#endif // !ENABLE_GL_SHADERS_ATTRIBUTES
+}
+
+void GLCanvas3D::_render_volumes_for_picking() const
+{
+#if ENABLE_LEGACY_OPENGL_REMOVAL
+#if ENABLE_GL_SHADERS_ATTRIBUTES
+    GLShaderProgram* shader = wxGetApp().get_shader("flat_clip");
+#else
+    GLShaderProgram* shader = wxGetApp().get_shader("flat");
+#endif // ENABLE_GL_SHADERS_ATTRIBUTES
+    if (shader == nullptr)
+        return;
+#endif // ENABLE_LEGACY_OPENGL_REMOVAL
+
+    // do not cull backfaces to show broken geometry, if any
+    glsafe(::glDisable(GL_CULL_FACE));
+
+#if !ENABLE_GL_SHADERS_ATTRIBUTES
+    glsafe(::glEnableClientState(GL_VERTEX_ARRAY));
+    glsafe(::glEnableClientState(GL_NORMAL_ARRAY));
+#endif // !ENABLE_GL_SHADERS_ATTRIBUTES
+
+    const Transform3d& view_matrix = wxGetApp().plater()->get_camera().get_view_matrix();
+    for (size_t type = 0; type < 2; ++ type) {
+        GLVolumeWithIdAndZList to_render = volumes_to_render(m_volumes.volumes, (type == 0) ? GLVolumeCollection::ERenderType::Opaque : GLVolumeCollection::ERenderType::Transparent, view_matrix);
+        for (const GLVolumeWithIdAndZ& volume : to_render)
+	        if (!volume.first->disabled && (volume.first->composite_id.volume_id >= 0 || m_render_sla_auxiliaries)) {
+		        // Object picking mode. Render the object with a color encoding the object index.
+                // we reserve color = (0,0,0) for occluders (as the printbed) 
+                // so we shift volumes' id by 1 to get the proper color
+                const unsigned int id = 1 + volume.second.first;
+#if ENABLE_LEGACY_OPENGL_REMOVAL
+                volume.first->model.set_color(picking_decode(id));
+                shader->start_using();
+#else
+                glsafe(::glColor4fv(picking_decode(id).data()));
+#endif // ENABLE_LEGACY_OPENGL_REMOVAL
+#if ENABLE_GL_SHADERS_ATTRIBUTES
+                const Camera& camera = wxGetApp().plater()->get_camera();
+                shader->set_uniform("view_model_matrix", camera.get_view_matrix() * volume.first->world_matrix());
+                shader->set_uniform("projection_matrix", camera.get_projection_matrix());
+                shader->set_uniform("volume_world_matrix", volume.first->world_matrix());
+                shader->set_uniform("z_range", m_volumes.get_z_range());
+                shader->set_uniform("clipping_plane", m_volumes.get_clipping_plane());
+#endif // ENABLE_GL_SHADERS_ATTRIBUTES
+                volume.first->render();
+#if ENABLE_LEGACY_OPENGL_REMOVAL
+                shader->stop_using();
+#endif // ENABLE_LEGACY_OPENGL_REMOVAL
+            }
+	}
+
+#if !ENABLE_GL_SHADERS_ATTRIBUTES
+    glsafe(::glDisableClientState(GL_NORMAL_ARRAY));
+    glsafe(::glDisableClientState(GL_VERTEX_ARRAY));
+#endif // !ENABLE_GL_SHADERS_ATTRIBUTES
+
+    glsafe(::glEnable(GL_CULL_FACE));
+}
+
+void GLCanvas3D::_render_current_gizmo() const
+{
+    m_gizmos.render_current_gizmo();
+}
+
+void GLCanvas3D::_render_gizmos_overlay()
+{
+#if ENABLE_RETINA_GL
+//     m_gizmos.set_overlay_scale(m_retina_helper->get_scale_factor());
+    const float scale = m_retina_helper->get_scale_factor()*wxGetApp().toolbar_icon_scale();
+    m_gizmos.set_overlay_scale(scale); //! #ys_FIXME_experiment
+#else
+//     m_gizmos.set_overlay_scale(m_canvas->GetContentScaleFactor());
+//     m_gizmos.set_overlay_scale(wxGetApp().em_unit()*0.1f);
+    const float size = int(GLGizmosManager::Default_Icons_Size * wxGetApp().toolbar_icon_scale());
+    m_gizmos.set_overlay_icon_size(size); //! #ys_FIXME_experiment
+#endif /* __WXMSW__ */
+
+    m_gizmos.render_overlay();
+
+    if (m_gizmo_highlighter.m_render_arrow)
+        m_gizmos.render_arrow(*this, m_gizmo_highlighter.m_gizmo_type);
+}
+
+void GLCanvas3D::_render_main_toolbar()
+{
+    if (!m_main_toolbar.is_enabled())
+        return;
+
+    const Size cnv_size = get_canvas_size();
+#if ENABLE_GL_SHADERS_ATTRIBUTES
+    const float top = 0.5f * (float)cnv_size.get_height();
+#else
+    const float inv_zoom = (float)wxGetApp().plater()->get_camera().get_inv_zoom();
+    const float top = 0.5f * (float)cnv_size.get_height() * inv_zoom;
+#endif // ENABLE_GL_SHADERS_ATTRIBUTES
+
+    GLToolbar& collapse_toolbar = wxGetApp().plater()->get_collapse_toolbar();
+    const float collapse_toolbar_width = collapse_toolbar.is_enabled() ? collapse_toolbar.get_width() : 0.0f;
+#if ENABLE_GL_SHADERS_ATTRIBUTES
+    const float left = -0.5f * (m_main_toolbar.get_width() + m_undoredo_toolbar.get_width() + collapse_toolbar_width);
+#else
+    const float left = -0.5f * (m_main_toolbar.get_width() + m_undoredo_toolbar.get_width() + collapse_toolbar_width) * inv_zoom;
+#endif // ENABLE_GL_SHADERS_ATTRIBUTES
+
+    m_main_toolbar.set_position(top, left);
+    m_main_toolbar.render(*this);
+    if (m_toolbar_highlighter.m_render_arrow)
+        m_main_toolbar.render_arrow(*this, m_toolbar_highlighter.m_toolbar_item);
+}
+
+void GLCanvas3D::_render_undoredo_toolbar()
+{
+    if (!m_undoredo_toolbar.is_enabled())
+        return;
+
+    const Size cnv_size = get_canvas_size();
+#if ENABLE_GL_SHADERS_ATTRIBUTES
+    const float top = 0.5f * (float)cnv_size.get_height();
+#else
+    float inv_zoom = (float)wxGetApp().plater()->get_camera().get_inv_zoom();
+
+    const float top = 0.5f * (float)cnv_size.get_height() * inv_zoom;
+#endif // ENABLE_GL_SHADERS_ATTRIBUTES
+    GLToolbar& collapse_toolbar = wxGetApp().plater()->get_collapse_toolbar();
+    const float collapse_toolbar_width = collapse_toolbar.is_enabled() ? collapse_toolbar.get_width() : 0.0f;
+#if ENABLE_GL_SHADERS_ATTRIBUTES
+    const float left = m_main_toolbar.get_width() - 0.5f * (m_main_toolbar.get_width() + m_undoredo_toolbar.get_width() + collapse_toolbar_width);
+#else
+    const float left = (m_main_toolbar.get_width() - 0.5f * (m_main_toolbar.get_width() + m_undoredo_toolbar.get_width() + collapse_toolbar_width)) * inv_zoom;
+#endif // ENABLE_GL_SHADERS_ATTRIBUTES
+
+    m_undoredo_toolbar.set_position(top, left);
+    m_undoredo_toolbar.render(*this);
+    if (m_toolbar_highlighter.m_render_arrow)
+        m_undoredo_toolbar.render_arrow(*this, m_toolbar_highlighter.m_toolbar_item);
+}
+
+void GLCanvas3D::_render_collapse_toolbar() const
+{
+    GLToolbar& collapse_toolbar = wxGetApp().plater()->get_collapse_toolbar();
+
+    const Size cnv_size = get_canvas_size();
+    const float band = m_layers_editing.is_enabled() ? (wxGetApp().imgui()->get_style_scaling() * LayersEditing::THICKNESS_BAR_WIDTH) : 0.0;
+#if ENABLE_GL_SHADERS_ATTRIBUTES
+    const float top  = 0.5f * (float)cnv_size.get_height();
+    const float left = 0.5f * (float)cnv_size.get_width() - collapse_toolbar.get_width() - band;
+#else
+    const float inv_zoom = (float)wxGetApp().plater()->get_camera().get_inv_zoom();
+
+    const float top = 0.5f * (float)cnv_size.get_height() * inv_zoom;
+    const float left = (0.5f * (float)cnv_size.get_width() - (float)collapse_toolbar.get_width() - band) * inv_zoom;
+#endif // ENABLE_GL_SHADERS_ATTRIBUTES
+
+    collapse_toolbar.set_position(top, left);
+    collapse_toolbar.render(*this);
+}
+
+void GLCanvas3D::_render_view_toolbar() const
+{
+    GLToolbar& view_toolbar = wxGetApp().plater()->get_view_toolbar();
+
+#if ENABLE_RETINA_GL
+    const float scale = m_retina_helper->get_scale_factor() * wxGetApp().toolbar_icon_scale();
+#if __APPLE__
+    view_toolbar.set_scale(scale);
+#else // if GTK3
+    const float size = int(GLGizmosManager::Default_Icons_Size * scale);
+    view_toolbar.set_icons_size(size);
+#endif // __APPLE__
+#else
+    const float size = int(GLGizmosManager::Default_Icons_Size * wxGetApp().toolbar_icon_scale());
+    view_toolbar.set_icons_size(size);
+#endif // ENABLE_RETINA_GL
+
+    const Size cnv_size = get_canvas_size();
+#if ENABLE_GL_SHADERS_ATTRIBUTES
+    // places the toolbar on the bottom-left corner of the 3d scene
+    float top = -0.5f * (float)cnv_size.get_height() + view_toolbar.get_height();
+    float left = -0.5f * (float)cnv_size.get_width();
+#else
+    float inv_zoom = (float)wxGetApp().plater()->get_camera().get_inv_zoom();
+
+    // places the toolbar on the bottom-left corner of the 3d scene
+    float top = (-0.5f * (float)cnv_size.get_height() + view_toolbar.get_height()) * inv_zoom;
+    float left = -0.5f * (float)cnv_size.get_width() * inv_zoom;
+#endif // ENABLE_GL_SHADERS_ATTRIBUTES
+    view_toolbar.set_position(top, left);
+    view_toolbar.render(*this);
+}
+
+#if ENABLE_SHOW_CAMERA_TARGET
+void GLCanvas3D::_render_camera_target()
+{
+    static const float half_length = 5.0f;
+
+    glsafe(::glDisable(GL_DEPTH_TEST));
+    glsafe(::glLineWidth(2.0f));
+
+#if ENABLE_LEGACY_OPENGL_REMOVAL
+    const Vec3f& target = wxGetApp().plater()->get_camera().get_target().cast<float>();
+    m_camera_target.target = target.cast<double>();
+
+    for (int i = 0; i < 3; ++i) {
+        if (!m_camera_target.axis[i].is_initialized()) {
+            m_camera_target.axis[i].reset();
+
+            GLModel::Geometry init_data;
+            init_data.format = { GLModel::Geometry::EPrimitiveType::Lines, GLModel::Geometry::EVertexLayout::P3 };
+            init_data.color = (i == X) ? ColorRGBA::X() : ((i == Y) ? ColorRGBA::Y() : ColorRGBA::Z());
+            init_data.reserve_vertices(2);
+            init_data.reserve_indices(2);
+
+            // vertices
+            if (i == X) {
+                init_data.add_vertex(Vec3f(-half_length, 0.0f, 0.0f));
+                init_data.add_vertex(Vec3f(+half_length, 0.0f, 0.0f));
+            }
+            else if (i == Y) {
+                init_data.add_vertex(Vec3f(0.0f, -half_length, 0.0f));
+                init_data.add_vertex(Vec3f(0.0f, +half_length, 0.0f));
+            }
+            else {
+                init_data.add_vertex(Vec3f(0.0f, 0.0f, -half_length));
+                init_data.add_vertex(Vec3f(0.0f, 0.0f, +half_length));
+            }
+
+            // indices
+            init_data.add_line(0, 1);
+
+            m_camera_target.axis[i].init_from(std::move(init_data));
+        }
+    }
+
+    GLShaderProgram* shader = wxGetApp().get_shader("flat");
+    if (shader != nullptr) {
+        shader->start_using();
+#if ENABLE_GL_SHADERS_ATTRIBUTES
+        const Camera& camera = wxGetApp().plater()->get_camera();
+        shader->set_uniform("view_model_matrix", camera.get_view_matrix() * Geometry::assemble_transform(m_camera_target.target));
+        shader->set_uniform("projection_matrix", camera.get_projection_matrix());
+#endif // ENABLE_GL_SHADERS_ATTRIBUTES
+        for (int i = 0; i < 3; ++i) {
+            m_camera_target.axis[i].render();
+        }
+        shader->stop_using();
+    }
+#else
+    ::glBegin(GL_LINES);
+    const Vec3d& target = wxGetApp().plater()->get_camera().get_target();
+    // draw line for x axis
+    ::glColor3f(1.0f, 0.0f, 0.0f);
+    ::glVertex3d(target.x() - half_length, target.y(), target.z());
+    ::glVertex3d(target.x() + half_length, target.y(), target.z());
+    // draw line for y axis
+    ::glColor3f(0.0f, 1.0f, 0.0f);
+    ::glVertex3d(target.x(), target.y() - half_length, target.z());
+    ::glVertex3d(target.x(), target.y() + half_length, target.z());
+    // draw line for z axis
+    ::glColor3f(0.0f, 0.0f, 1.0f);
+    ::glVertex3d(target.x(), target.y(), target.z() - half_length);
+    ::glVertex3d(target.x(), target.y(), target.z() + half_length);
+    glsafe(::glEnd());
+#endif // ENABLE_LEGACY_OPENGL_REMOVAL
+}
+#endif // ENABLE_SHOW_CAMERA_TARGET
+
+void GLCanvas3D::_render_sla_slices()
+{
+    if (!m_use_clipping_planes || current_printer_technology() != ptSLA)
+        return;
+
+    const SLAPrint* print = this->sla_print();
+    const PrintObjects& print_objects = print->objects();
+    if (print_objects.empty())
+        // nothing to render, return
+        return;
+
+    double clip_min_z = -m_clipping_planes[0].get_data()[3];
+    double clip_max_z = m_clipping_planes[1].get_data()[3];
+    for (unsigned int i = 0; i < (unsigned int)print_objects.size(); ++i) {
+        const SLAPrintObject* obj = print_objects[i];
+
+        if (!obj->is_step_done(slaposSliceSupports))
+            continue;
+
+#if ENABLE_LEGACY_OPENGL_REMOVAL
+        SlaCap::ObjectIdToModelsMap::iterator it_caps_bottom = m_sla_caps[0].triangles.find(i);
+        SlaCap::ObjectIdToModelsMap::iterator it_caps_top = m_sla_caps[1].triangles.find(i);
+#else
+        SlaCap::ObjectIdToTrianglesMap::iterator it_caps_bottom = m_sla_caps[0].triangles.find(i);
+        SlaCap::ObjectIdToTrianglesMap::iterator it_caps_top = m_sla_caps[1].triangles.find(i);
+#endif // ENABLE_LEGACY_OPENGL_REMOVAL
+        {
+            if (it_caps_bottom == m_sla_caps[0].triangles.end())
+                it_caps_bottom = m_sla_caps[0].triangles.emplace(i, SlaCap::Triangles()).first;
+            if (!m_sla_caps[0].matches(clip_min_z)) {
+                m_sla_caps[0].z = clip_min_z;
+#if ENABLE_LEGACY_OPENGL_REMOVAL
+                it_caps_bottom->second.object.reset();
+                it_caps_bottom->second.supports.reset();
+#else
+                it_caps_bottom->second.object.clear();
+                it_caps_bottom->second.supports.clear();
+#endif // ENABLE_LEGACY_OPENGL_REMOVAL
+            }
+            if (it_caps_top == m_sla_caps[1].triangles.end())
+                it_caps_top = m_sla_caps[1].triangles.emplace(i, SlaCap::Triangles()).first;
+            if (!m_sla_caps[1].matches(clip_max_z)) {
+                m_sla_caps[1].z = clip_max_z;
+#if ENABLE_LEGACY_OPENGL_REMOVAL
+                it_caps_top->second.object.reset();
+                it_caps_top->second.supports.reset();
+#else
+                it_caps_top->second.object.clear();
+                it_caps_top->second.supports.clear();
+#endif // ENABLE_LEGACY_OPENGL_REMOVAL
+            }
+        }
+#if ENABLE_LEGACY_OPENGL_REMOVAL
+        GLModel& bottom_obj_triangles = it_caps_bottom->second.object;
+        GLModel& bottom_sup_triangles = it_caps_bottom->second.supports;
+        GLModel& top_obj_triangles = it_caps_top->second.object;
+        GLModel& top_sup_triangles = it_caps_top->second.supports;
+#else
+        Pointf3s &bottom_obj_triangles = it_caps_bottom->second.object;
+        Pointf3s &bottom_sup_triangles = it_caps_bottom->second.supports;
+        Pointf3s &top_obj_triangles    = it_caps_top->second.object;
+        Pointf3s &top_sup_triangles    = it_caps_top->second.supports;
+#endif // ENABLE_LEGACY_OPENGL_REMOVAL
+
+#if ENABLE_LEGACY_OPENGL_REMOVAL
+        auto init_model = [](GLModel& model, const Pointf3s& triangles, const ColorRGBA& color) {
+            GLModel::Geometry init_data;
+            init_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3 };
+            init_data.reserve_vertices(triangles.size());
+            init_data.reserve_indices(triangles.size() / 3);
+            init_data.color = color;
+
+            unsigned int vertices_count = 0;
+            for (const Vec3d& v : triangles) {
+                init_data.add_vertex((Vec3f)v.cast<float>());
+                ++vertices_count;
+                if (vertices_count % 3 == 0)
+                    init_data.add_triangle(vertices_count - 3, vertices_count - 2, vertices_count - 1);
+            }
+
+            if (!init_data.is_empty())
+                model.init_from(std::move(init_data));
+        };
+
+        if ((!bottom_obj_triangles.is_initialized() || !bottom_sup_triangles.is_initialized() ||
+            !top_obj_triangles.is_initialized() || !top_sup_triangles.is_initialized()) && !obj->get_slice_index().empty()) {
+#else
+        if ((bottom_obj_triangles.empty() || bottom_sup_triangles.empty() || top_obj_triangles.empty() || top_sup_triangles.empty()) &&
+            !obj->get_slice_index().empty()) {
+#endif // ENABLE_LEGACY_OPENGL_REMOVAL
+            double layer_height         = print->default_object_config().layer_height.value;
+            double initial_layer_height = print->material_config().initial_layer_height.value;
+            bool   left_handed          = obj->is_left_handed();
+
+            coord_t key_zero = obj->get_slice_index().front().print_level();
+            // Slice at the center of the slab starting at clip_min_z will be rendered for the lower plane.
+            coord_t key_low  = coord_t((clip_min_z - initial_layer_height + layer_height) / SCALING_FACTOR) + key_zero;
+            // Slice at the center of the slab ending at clip_max_z will be rendered for the upper plane.
+            coord_t key_high = coord_t((clip_max_z - initial_layer_height) / SCALING_FACTOR) + key_zero;
+
+            const SliceRecord& slice_low  = obj->closest_slice_to_print_level(key_low, coord_t(SCALED_EPSILON));
+            const SliceRecord& slice_high = obj->closest_slice_to_print_level(key_high, coord_t(SCALED_EPSILON));
+
+            // Offset to avoid OpenGL Z fighting between the object's horizontal surfaces and the triangluated surfaces of the cuts.
+            const double plane_shift_z = 0.002;
+
+            if (slice_low.is_valid()) {
+                const ExPolygons& obj_bottom = slice_low.get_slice(soModel);
+                const ExPolygons& sup_bottom = slice_low.get_slice(soSupport);
+#if ENABLE_LEGACY_OPENGL_REMOVAL
+                // calculate model bottom cap
+                if (!bottom_obj_triangles.is_initialized() && !obj_bottom.empty())
+                    init_model(bottom_obj_triangles, triangulate_expolygons_3d(obj_bottom, clip_min_z - plane_shift_z, !left_handed), { 1.0f, 0.37f, 0.0f, 1.0f });
+                // calculate support bottom cap
+                if (!bottom_sup_triangles.is_initialized() && !sup_bottom.empty())
+                    init_model(bottom_sup_triangles, triangulate_expolygons_3d(sup_bottom, clip_min_z - plane_shift_z, !left_handed), { 1.0f, 0.0f, 0.37f, 1.0f });
+#else
+                // calculate model bottom cap
+                if (bottom_obj_triangles.empty() && !obj_bottom.empty())
+                    bottom_obj_triangles = triangulate_expolygons_3d(obj_bottom, clip_min_z - plane_shift_z, ! left_handed);
+                // calculate support bottom cap
+                if (bottom_sup_triangles.empty() && !sup_bottom.empty())
+                    bottom_sup_triangles = triangulate_expolygons_3d(sup_bottom, clip_min_z - plane_shift_z, !left_handed);
+#endif // ENABLE_LEGACY_OPENGL_REMOVAL
+            }
+
+            if (slice_high.is_valid()) {
+                const ExPolygons& obj_top = slice_high.get_slice(soModel);
+                const ExPolygons& sup_top = slice_high.get_slice(soSupport);
+#if ENABLE_LEGACY_OPENGL_REMOVAL
+                // calculate model top cap
+                if (!top_obj_triangles.is_initialized() && !obj_top.empty())
+                    init_model(top_obj_triangles, triangulate_expolygons_3d(obj_top, clip_max_z + plane_shift_z, left_handed), { 1.0f, 0.37f, 0.0f, 1.0f });
+                // calculate support top cap
+                if (!top_sup_triangles.is_initialized() && !sup_top.empty())
+                    init_model(top_sup_triangles, triangulate_expolygons_3d(sup_top, clip_max_z + plane_shift_z, left_handed), { 1.0f, 0.0f, 0.37f, 1.0f });
+#else
+                // calculate model top cap
+                if (top_obj_triangles.empty() && !obj_top.empty())
+                    top_obj_triangles = triangulate_expolygons_3d(obj_top, clip_max_z + plane_shift_z, left_handed);
+                // calculate support top cap
+                if (top_sup_triangles.empty() && !sup_top.empty())
+                    top_sup_triangles = triangulate_expolygons_3d(sup_top, clip_max_z + plane_shift_z, left_handed);
+#endif // ENABLE_LEGACY_OPENGL_REMOVAL
+            }
+        }
+
+#if ENABLE_LEGACY_OPENGL_REMOVAL
+        GLShaderProgram* shader = wxGetApp().get_shader("flat");
+        if (shader != nullptr) {
+            shader->start_using();
+
+            for (const SLAPrintObject::Instance& inst : obj->instances()) {
+#if ENABLE_GL_SHADERS_ATTRIBUTES
+                const Camera& camera = wxGetApp().plater()->get_camera();
+                const Transform3d view_model_matrix = camera.get_view_matrix() *
+                    Geometry::assemble_transform(Vec3d(unscale<double>(inst.shift.x()), unscale<double>(inst.shift.y()), 0.0),
+                        inst.rotation * Vec3d::UnitZ(), Vec3d::Ones(),
+                        obj->is_left_handed() ? Vec3d(-1.0f, 1.0f, 1.0f) : Vec3d::Ones());
+
+                shader->set_uniform("view_model_matrix", view_model_matrix);
+                shader->set_uniform("projection_matrix", camera.get_projection_matrix());
+#else
+                glsafe(::glPushMatrix());
+                glsafe(::glTranslated(unscale<double>(inst.shift.x()), unscale<double>(inst.shift.y()), 0.0));
+                glsafe(::glRotatef(Geometry::rad2deg(inst.rotation), 0.0f, 0.0f, 1.0f));
+                if (obj->is_left_handed())
+                    // The polygons are mirrored by X.
+                    glsafe(::glScalef(-1.0f, 1.0f, 1.0f));
+#endif // ENABLE_GL_SHADERS_ATTRIBUTES
+
+                bottom_obj_triangles.render();
+                top_obj_triangles.render();
+                bottom_sup_triangles.render();
+                top_sup_triangles.render();
+
+#if !ENABLE_GL_SHADERS_ATTRIBUTES
+                glsafe(::glPopMatrix());
+#endif // !ENABLE_GL_SHADERS_ATTRIBUTES
+            }
+
+            shader->stop_using();
+        }
+#else
+        if (!bottom_obj_triangles.empty() || !top_obj_triangles.empty() || !bottom_sup_triangles.empty() || !top_sup_triangles.empty()) {
+			for (const SLAPrintObject::Instance& inst : obj->instances()) {
+                glsafe(::glPushMatrix());
+                glsafe(::glTranslated(unscale<double>(inst.shift.x()), unscale<double>(inst.shift.y()), 0.0));
+                glsafe(::glRotatef(Geometry::rad2deg(inst.rotation), 0.0f, 0.0f, 1.0f));
+				if (obj->is_left_handed())
+                    // The polygons are mirrored by X.
+                    glsafe(::glScalef(-1.0f, 1.0f, 1.0f));
+                glsafe(::glEnableClientState(GL_VERTEX_ARRAY));
+                glsafe(::glColor3f(1.0f, 0.37f, 0.0f));
+				if (!bottom_obj_triangles.empty()) {
+                    glsafe(::glVertexPointer(3, GL_DOUBLE, 0, (GLdouble*)bottom_obj_triangles.front().data()));
+                    glsafe(::glDrawArrays(GL_TRIANGLES, 0, bottom_obj_triangles.size()));
+				}
+				if (! top_obj_triangles.empty()) {
+                    glsafe(::glVertexPointer(3, GL_DOUBLE, 0, (GLdouble*)top_obj_triangles.front().data()));
+                    glsafe(::glDrawArrays(GL_TRIANGLES, 0, top_obj_triangles.size()));
+				}
+                glsafe(::glColor3f(1.0f, 0.0f, 0.37f));
+				if (! bottom_sup_triangles.empty()) {
+                    glsafe(::glVertexPointer(3, GL_DOUBLE, 0, (GLdouble*)bottom_sup_triangles.front().data()));
+                    glsafe(::glDrawArrays(GL_TRIANGLES, 0, bottom_sup_triangles.size()));
+				}
+				if (! top_sup_triangles.empty()) {
+                    glsafe(::glVertexPointer(3, GL_DOUBLE, 0, (GLdouble*)top_sup_triangles.front().data()));
+                    glsafe(::glDrawArrays(GL_TRIANGLES, 0, top_sup_triangles.size()));
+				}
+                glsafe(::glDisableClientState(GL_VERTEX_ARRAY));
+                glsafe(::glPopMatrix());
+            }
+        }
+#endif // ENABLE_LEGACY_OPENGL_REMOVAL
+    }
+}
+
+void GLCanvas3D::_render_selection_sidebar_hints()
+{
+    m_selection.render_sidebar_hints(m_sidebar_field);
+}
+
+void GLCanvas3D::_update_volumes_hover_state()
+{
+    for (GLVolume* v : m_volumes.volumes) {
+        v->hover = GLVolume::HS_None;
+    }
+
+    if (m_hover_volume_idxs.empty())
+        return;
+
+    bool ctrl_pressed = wxGetKeyState(WXK_CONTROL); // additive select/deselect
+    bool shift_pressed = wxGetKeyState(WXK_SHIFT);  // select by rectangle
+    bool alt_pressed = wxGetKeyState(WXK_ALT);      // deselect by rectangle
+
+    if (alt_pressed && (shift_pressed || ctrl_pressed)) {
+        // illegal combinations of keys
+        m_hover_volume_idxs.clear();
+        return;
+    }
+
+#if !ENABLE_NEW_RECTANGLE_SELECTION
+    bool selection_modifiers_only = m_selection.is_empty() || m_selection.is_any_modifier();
+#endif // !ENABLE_NEW_RECTANGLE_SELECTION
+
+    bool hover_modifiers_only = true;
+    for (int i : m_hover_volume_idxs) {
+        if (!m_volumes.volumes[i]->is_modifier) {
+            hover_modifiers_only = false;
+            break;
+        }
+    }
+
+    std::set<std::pair<int, int>> hover_instances;
+    for (int i : m_hover_volume_idxs) {
+        const GLVolume& v = *m_volumes.volumes[i];
+        hover_instances.insert(std::make_pair(v.object_idx(), v.instance_idx()));
+    }
+
+    bool hover_from_single_instance = hover_instances.size() == 1;
+
+    if (hover_modifiers_only && !hover_from_single_instance) {
+        // do not allow to select volumes from different instances
+        m_hover_volume_idxs.clear();
+        return;
+    }
+
+    for (int i : m_hover_volume_idxs) {
+        GLVolume& volume = *m_volumes.volumes[i];
+        if (volume.hover != GLVolume::HS_None)
+            continue;
+
+#if ENABLE_NEW_RECTANGLE_SELECTION
+        bool deselect = volume.selected && ((shift_pressed && m_rectangle_selection.is_empty()) || (alt_pressed && !m_rectangle_selection.is_empty()));
+        bool select   = !volume.selected && (m_rectangle_selection.is_empty() || (shift_pressed && !m_rectangle_selection.is_empty()));
+#else
+        bool deselect = volume.selected && ((ctrl_pressed && !shift_pressed) || alt_pressed);
+        // (volume->is_modifier && !selection_modifiers_only && !is_ctrl_pressed) -> allows hovering on selected modifiers belonging to selection of type Instance
+        bool select = (!volume.selected || (volume.is_modifier && !selection_modifiers_only && !ctrl_pressed)) && !alt_pressed;
+#endif // ENABLE_NEW_RECTANGLE_SELECTION
+
+        if (select || deselect) {
+            bool as_volume =
+                volume.is_modifier && hover_from_single_instance && !ctrl_pressed &&
+                (
+                (!deselect) ||
+                (deselect && !m_selection.is_single_full_instance() && (volume.object_idx() == m_selection.get_object_idx()) && (volume.instance_idx() == m_selection.get_instance_idx()))
+                );
+
+            if (as_volume)
+                volume.hover = deselect ? GLVolume::HS_Deselect : GLVolume::HS_Select;
+            else {
+                int object_idx = volume.object_idx();
+                int instance_idx = volume.instance_idx();
+
+                for (GLVolume* v : m_volumes.volumes) {
+                    if (v->object_idx() == object_idx && v->instance_idx() == instance_idx)
+                        v->hover = deselect ? GLVolume::HS_Deselect : GLVolume::HS_Select;
+                }
+            }
+        }
+        else if (volume.selected)
+            volume.hover = GLVolume::HS_Hover;
+    }
+}
+
+void GLCanvas3D::_perform_layer_editing_action(wxMouseEvent* evt)
+{
+    int object_idx_selected = m_layers_editing.last_object_id;
+    if (object_idx_selected == -1)
+        return;
+
+    // A volume is selected. Test, whether hovering over a layer thickness bar.
+    if (evt != nullptr) {
+        const Rect& rect = LayersEditing::get_bar_rect_screen(*this);
+        float b = rect.get_bottom();
+        m_layers_editing.last_z = m_layers_editing.object_max_z() * (b - evt->GetY() - 1.0f) / (b - rect.get_top());
+        m_layers_editing.last_action = 
+            evt->ShiftDown() ? (evt->RightIsDown() ? LAYER_HEIGHT_EDIT_ACTION_SMOOTH : LAYER_HEIGHT_EDIT_ACTION_REDUCE) : 
+                               (evt->RightIsDown() ? LAYER_HEIGHT_EDIT_ACTION_INCREASE : LAYER_HEIGHT_EDIT_ACTION_DECREASE);
+    }
+
+    m_layers_editing.adjust_layer_height_profile();
+    _refresh_if_shown_on_screen();
+
+    // Automatic action on mouse down with the same coordinate.
+    _start_timer();
+}
+
+Vec3d GLCanvas3D::_mouse_to_3d(const Point& mouse_pos, float* z)
+{
+    if (m_canvas == nullptr)
+        return Vec3d(DBL_MAX, DBL_MAX, DBL_MAX);
+
+    const Camera& camera = wxGetApp().plater()->get_camera();
+    const Matrix4d modelview  = camera.get_view_matrix().matrix();
+    const Matrix4d projection = camera.get_projection_matrix().matrix();
+    const Vec4i viewport(camera.get_viewport().data());
+
+    const int y = viewport[3] - mouse_pos.y();
+    float mouse_z;
+    if (z == nullptr)
+        glsafe(::glReadPixels(mouse_pos.x(), y, 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT, (void*)&mouse_z));
+    else
+        mouse_z = *z;
+
+    Vec3d out;
+    igl::unproject(Vec3d(mouse_pos.x(), y, mouse_z), modelview, projection, viewport, out);
+    return out;
+}
+
+Vec3d GLCanvas3D::_mouse_to_bed_3d(const Point& mouse_pos)
+{
+    return mouse_ray(mouse_pos).intersect_plane(0.0);
+}
+
+void GLCanvas3D::_start_timer()
+{
+    m_timer.Start(100, wxTIMER_CONTINUOUS);
+}
+
+void GLCanvas3D::_stop_timer()
+{
+    m_timer.Stop();
+}
+
+void GLCanvas3D::_load_print_toolpaths(const BuildVolume &build_volume)
+{
+    const Print *print = this->fff_print();
+    if (print == nullptr)
+        return;
+
+    if (! print->is_step_done(psSkirtBrim))
+        return;
+
+    if (!print->has_skirt() && !print->has_brim())
+        return;
+
+    const ColorRGBA color = ColorRGBA::GREENISH();
+
+    // number of skirt layers
+    size_t total_layer_count = 0;
+    for (const PrintObject* print_object : print->objects()) {
+        total_layer_count = std::max(total_layer_count, print_object->total_layer_count());
+    }
+    size_t skirt_height = print->has_infinite_skirt() ? total_layer_count : std::min<size_t>(print->config().skirt_height.value, total_layer_count);
+    if (skirt_height == 0 && print->has_brim())
+        skirt_height = 1;
+
+    // Get first skirt_height layers.
+    //FIXME This code is fishy. It may not work for multiple objects with different layering due to variable layer height feature.
+    // This is not critical as this is just an initial preview.
+    const PrintObject* highest_object = *std::max_element(print->objects().begin(), print->objects().end(), [](auto l, auto r){ return l->layers().size() < r->layers().size(); });
+    std::vector<float> print_zs;
+    print_zs.reserve(skirt_height * 2);
+    for (size_t i = 0; i < std::min(skirt_height, highest_object->layers().size()); ++ i)
+        print_zs.emplace_back(float(highest_object->layers()[i]->print_z));
+    // Only add skirt for the raft layers.
+    for (size_t i = 0; i < std::min(skirt_height, std::min(highest_object->slicing_parameters().raft_layers(), highest_object->support_layers().size())); ++ i)
+        print_zs.emplace_back(float(highest_object->support_layers()[i]->print_z));
+    sort_remove_duplicates(print_zs);
+    skirt_height = std::min(skirt_height, print_zs.size());
+    print_zs.erase(print_zs.begin() + skirt_height, print_zs.end());
+
+#if ENABLE_LEGACY_OPENGL_REMOVAL
+    GLVolume* volume = m_volumes.new_toolpath_volume(color);
+    GLModel::Geometry init_data;
+    init_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3N3 };
+#else
+    GLVolume *volume = m_volumes.new_toolpath_volume(color, VERTEX_BUFFER_RESERVE_SIZE);
+#endif // ENABLE_LEGACY_OPENGL_REMOVAL
+    for (size_t i = 0; i < skirt_height; ++ i) {
+        volume->print_zs.emplace_back(print_zs[i]);
+#if ENABLE_LEGACY_OPENGL_REMOVAL
+        volume->offsets.emplace_back(init_data.indices_count());
+        if (i == 0)
+            _3DScene::extrusionentity_to_verts(print->brim(), print_zs[i], Point(0, 0), init_data);
+        _3DScene::extrusionentity_to_verts(print->skirt(), print_zs[i], Point(0, 0), init_data);
+#else
+        volume->offsets.emplace_back(volume->indexed_vertex_array.quad_indices.size());
+        volume->offsets.emplace_back(volume->indexed_vertex_array.triangle_indices.size());
+        if (i == 0)
+            _3DScene::extrusionentity_to_verts(print->brim(), print_zs[i], Point(0, 0), *volume);
+        _3DScene::extrusionentity_to_verts(print->skirt(), print_zs[i], Point(0, 0), *volume);
+#endif // ENABLE_LEGACY_OPENGL_REMOVAL
+        // Ensure that no volume grows over the limits. If the volume is too large, allocate a new one.
+#if ENABLE_LEGACY_OPENGL_REMOVAL
+        if (init_data.vertices_size_bytes() > MAX_VERTEX_BUFFER_SIZE) {
+            volume->model.init_from(std::move(init_data));
+#else
+        if (volume->indexed_vertex_array.vertices_and_normals_interleaved.size() > MAX_VERTEX_BUFFER_SIZE) {
+#endif // ENABLE_LEGACY_OPENGL_REMOVAL
+            GLVolume &vol = *volume;
+            volume = m_volumes.new_toolpath_volume(vol.color);
+#if !ENABLE_LEGACY_OPENGL_REMOVAL
+            reserve_new_volume_finalize_old_volume(*volume, vol, m_initialized);
+#endif // !ENABLE_LEGACY_OPENGL_REMOVAL
+        }
+    }
+#if ENABLE_LEGACY_OPENGL_REMOVAL
+    volume->model.init_from(std::move(init_data));
+    volume->is_outside = !contains(build_volume, volume->model);
+#else
+    volume->is_outside = ! build_volume.all_paths_inside_vertices_and_normals_interleaved(volume->indexed_vertex_array.vertices_and_normals_interleaved, volume->indexed_vertex_array.bounding_box());
+    volume->indexed_vertex_array.finalize_geometry(m_initialized);
+#endif // ENABLE_LEGACY_OPENGL_REMOVAL
+}
+
+void GLCanvas3D::_load_print_object_toolpaths(const PrintObject& print_object, const BuildVolume& build_volume, const std::vector<std::string>& str_tool_colors, const std::vector<CustomGCode::Item>& color_print_values)
+{
+    std::vector<ColorRGBA> tool_colors;
+    decode_colors(str_tool_colors, tool_colors);
+
+    struct Ctxt
+    {
+        const PrintInstances        *shifted_copies;
+        std::vector<const Layer*>    layers;
+        bool                         has_perimeters;
+        bool                         has_infill;
+        bool                         has_support;
+        const std::vector<ColorRGBA>* tool_colors;
+        bool                         is_single_material_print;
+        int                          extruders_cnt;
+        const std::vector<CustomGCode::Item>*   color_print_values;
+
+        static ColorRGBA color_perimeters()           { return ColorRGBA::YELLOW(); }
+        static ColorRGBA color_infill()               { return ColorRGBA::REDISH(); }
+        static ColorRGBA color_support()              { return ColorRGBA::GREENISH(); }
+        static ColorRGBA color_pause_or_custom_code() { return ColorRGBA::GRAY(); }
+
+        // For cloring by a tool, return a parsed color.
+        bool                         color_by_tool() const { return tool_colors != nullptr; }
+        size_t                       number_tools() const { return color_by_tool() ? tool_colors->size() : 0; }
+        const ColorRGBA&             color_tool(size_t tool) const { return (*tool_colors)[tool]; }
+
+        // For coloring by a color_print(M600), return a parsed color.
+        bool                         color_by_color_print() const { return color_print_values!=nullptr; }
+        const size_t                 color_print_color_idx_by_layer_idx(const size_t layer_idx) const {
+            const CustomGCode::Item value{layers[layer_idx]->print_z + EPSILON, CustomGCode::Custom, 0, ""};
+            auto it = std::lower_bound(color_print_values->begin(), color_print_values->end(), value);
+            return (it - color_print_values->begin()) % number_tools();
+        }
+
+        const size_t                 color_print_color_idx_by_layer_idx_and_extruder(const size_t layer_idx, const int extruder) const
+        {
+            const coordf_t print_z = layers[layer_idx]->print_z;
+
+            auto it = std::find_if(color_print_values->begin(), color_print_values->end(),
+                [print_z](const CustomGCode::Item& code)
+                { return fabs(code.print_z - print_z) < EPSILON; });
+            if (it != color_print_values->end()) {
+                CustomGCode::Type type = it->type;
+                // pause print or custom Gcode
+                if (type == CustomGCode::PausePrint ||
+                    (type != CustomGCode::ColorChange && type != CustomGCode::ToolChange))
+                    return number_tools()-1; // last color item is a gray color for pause print or custom G-code 
+
+                // change tool (extruder) 
+                if (type == CustomGCode::ToolChange)
+                    return get_color_idx_for_tool_change(it, extruder);
+                // change color for current extruder
+                if (type == CustomGCode::ColorChange) {
+                    int color_idx = get_color_idx_for_color_change(it, extruder);
+                    if (color_idx >= 0)
+                        return color_idx;
+                }
+            }
+
+            const CustomGCode::Item value{print_z + EPSILON, CustomGCode::Custom, 0, ""};
+            it = std::lower_bound(color_print_values->begin(), color_print_values->end(), value);
+            while (it != color_print_values->begin()) {
+                --it;
+                // change color for current extruder
+                if (it->type == CustomGCode::ColorChange) {
+                    int color_idx = get_color_idx_for_color_change(it, extruder);
+                    if (color_idx >= 0)
+                        return color_idx;
+                }
+                // change tool (extruder) 
+                if (it->type == CustomGCode::ToolChange)
+                    return get_color_idx_for_tool_change(it, extruder);
+            }
+
+            return std::min<int>(extruders_cnt - 1, std::max<int>(extruder - 1, 0));;
+        }
+
+    private:
+        int get_m600_color_idx(std::vector<CustomGCode::Item>::const_iterator it) const
+        {
+            int shift = 0;
+            while (it != color_print_values->begin()) {
+                --it;
+                if (it->type == CustomGCode::ColorChange)
+                    shift++;
+            }
+            return extruders_cnt + shift;
+        }
+
+        int get_color_idx_for_tool_change(std::vector<CustomGCode::Item>::const_iterator it, const int extruder) const
+        {
+            const int current_extruder = it->extruder == 0 ? extruder : it->extruder;
+            if (number_tools() == size_t(extruders_cnt + 1)) // there is no one "M600"
+                return std::min<int>(extruders_cnt - 1, std::max<int>(current_extruder - 1, 0));
+
+            auto it_n = it;
+            while (it_n != color_print_values->begin()) {
+                --it_n;
+                if (it_n->type == CustomGCode::ColorChange && it_n->extruder == current_extruder)
+                    return get_m600_color_idx(it_n);
+            }
+
+            return std::min<int>(extruders_cnt - 1, std::max<int>(current_extruder - 1, 0));
+        }
+
+        int get_color_idx_for_color_change(std::vector<CustomGCode::Item>::const_iterator it, const int extruder) const
+        {
+            if (extruders_cnt == 1)
+                return get_m600_color_idx(it);
+
+            auto it_n = it;
+            bool is_tool_change = false;
+            while (it_n != color_print_values->begin()) {
+                --it_n;
+                if (it_n->type == CustomGCode::ToolChange) {
+                    is_tool_change = true;
+                    if (it_n->extruder == it->extruder || (it_n->extruder == 0 && it->extruder == extruder))
+                        return get_m600_color_idx(it);
+                    break;
+                }
+            }
+            if (!is_tool_change && it->extruder == extruder)
+                return get_m600_color_idx(it);
+
+            return -1;
+        }
+
+    } ctxt;
+
+    ctxt.has_perimeters = print_object.is_step_done(posPerimeters);
+    ctxt.has_infill = print_object.is_step_done(posInfill);
+    ctxt.has_support = print_object.is_step_done(posSupportMaterial);
+    ctxt.tool_colors = tool_colors.empty() ? nullptr : &tool_colors;
+    ctxt.color_print_values = color_print_values.empty() ? nullptr : &color_print_values;
+    ctxt.is_single_material_print = this->fff_print()->extruders().size()==1;
+    ctxt.extruders_cnt = wxGetApp().extruders_edited_cnt();
+
+    ctxt.shifted_copies = &print_object.instances();
+
+    // order layers by print_z
+    {
+        size_t nlayers = 0;
+        if (ctxt.has_perimeters || ctxt.has_infill)
+            nlayers = print_object.layers().size();
+        if (ctxt.has_support)
+            nlayers += print_object.support_layers().size();
+        ctxt.layers.reserve(nlayers);
+    }
+    if (ctxt.has_perimeters || ctxt.has_infill)
+        for (const Layer *layer : print_object.layers())
+            ctxt.layers.emplace_back(layer);
+    if (ctxt.has_support)
+        for (const Layer *layer : print_object.support_layers())
+            ctxt.layers.emplace_back(layer);
+    std::sort(ctxt.layers.begin(), ctxt.layers.end(), [](const Layer *l1, const Layer *l2) { return l1->print_z < l2->print_z; });
+
+    // Maximum size of an allocation block: 32MB / sizeof(float)
+    BOOST_LOG_TRIVIAL(debug) << "Loading print object toolpaths in parallel - start" << m_volumes.log_memory_info() << log_memory_info();
+
+    const bool is_selected_separate_extruder = m_selected_extruder > 0 && ctxt.color_by_color_print();
+
+    //FIXME Improve the heuristics for a grain size.
+    size_t          grain_size = std::max(ctxt.layers.size() / 16, size_t(1));
+    tbb::spin_mutex new_volume_mutex;
+    auto            new_volume = [this, &new_volume_mutex](const ColorRGBA& color) {
+        // Allocate the volume before locking.
+		GLVolume *volume = new GLVolume(color);
+		volume->is_extrusion_path = true;
+#if ENABLE_LEGACY_OPENGL_REMOVAL
+        // to prevent sending data to gpu (in the main thread) while
+        // editing the model geometry
+        volume->model.disable_render();
+#endif // ENABLE_LEGACY_OPENGL_REMOVAL
+        tbb::spin_mutex::scoped_lock lock;
+    	// Lock by ROII, so if the emplace_back() fails, the lock will be released.
+        lock.acquire(new_volume_mutex);
+        m_volumes.volumes.emplace_back(volume);
+        lock.release();
+        return volume;
+    };
+    const size_t    volumes_cnt_initial = m_volumes.volumes.size();
+    tbb::parallel_for(
+        tbb::blocked_range<size_t>(0, ctxt.layers.size(), grain_size),
+        [&ctxt, &new_volume, is_selected_separate_extruder, this](const tbb::blocked_range<size_t>& range) {
+        GLVolumePtrs 		vols;
+#if ENABLE_LEGACY_OPENGL_REMOVAL
+        std::vector<GLModel::Geometry> geometries;
+        auto select_geometry = [&ctxt, &geometries](size_t layer_idx, int extruder, int feature) -> GLModel::Geometry& {
+            return geometries[ctxt.color_by_color_print() ?
+                ctxt.color_print_color_idx_by_layer_idx_and_extruder(layer_idx, extruder) :
+                ctxt.color_by_tool() ?
+                std::min<int>(ctxt.number_tools() - 1, std::max<int>(extruder - 1, 0)) :
+                feature
+            ];
+        };
+#else
+        auto                volume = [&ctxt, &vols](size_t layer_idx, int extruder, int feature) -> GLVolume& {
+            return *vols[ctxt.color_by_color_print() ?
+                ctxt.color_print_color_idx_by_layer_idx_and_extruder(layer_idx, extruder) :
+                ctxt.color_by_tool() ?
+                std::min<int>(ctxt.number_tools() - 1, std::max<int>(extruder - 1, 0)) :
+                feature
+            ];
+        };
+#endif // ENABLE_LEGACY_OPENGL_REMOVAL
+        if (ctxt.color_by_color_print() || ctxt.color_by_tool()) {
+            for (size_t i = 0; i < ctxt.number_tools(); ++i) {
+                vols.emplace_back(new_volume(ctxt.color_tool(i)));
+#if ENABLE_LEGACY_OPENGL_REMOVAL
+                geometries.emplace_back(GLModel::Geometry());
+#endif // ENABLE_LEGACY_OPENGL_REMOVAL
+            }
+        }
+        else {
+            vols = { new_volume(ctxt.color_perimeters()), new_volume(ctxt.color_infill()), new_volume(ctxt.color_support()) };
+#if ENABLE_LEGACY_OPENGL_REMOVAL
+            geometries = { GLModel::Geometry(), GLModel::Geometry(), GLModel::Geometry() };
+#endif // ENABLE_LEGACY_OPENGL_REMOVAL
+        }
+
+#if ENABLE_LEGACY_OPENGL_REMOVAL
+        assert(vols.size() == geometries.size());
+        for (GLModel::Geometry& g : geometries) {
+            g.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3N3 };
+        }
+#else
+        for (GLVolume *vol : vols)
+			// Reserving number of vertices (3x position + 3x color)
+        	vol->indexed_vertex_array.reserve(VERTEX_BUFFER_RESERVE_SIZE / 6);
+#endif // ENABLE_LEGACY_OPENGL_REMOVAL
+        for (size_t idx_layer = range.begin(); idx_layer < range.end(); ++ idx_layer) {
+            const Layer *layer = ctxt.layers[idx_layer];
+
+            if (is_selected_separate_extruder) {
+                bool at_least_one_has_correct_extruder = false;
+                for (const LayerRegion* layerm : layer->regions()) {
+                    if (layerm->slices.surfaces.empty())
+                        continue;
+                    const PrintRegionConfig& cfg = layerm->region().config();
+                    if (cfg.perimeter_extruder.value    == m_selected_extruder ||
+                        cfg.infill_extruder.value       == m_selected_extruder ||
+                        cfg.solid_infill_extruder.value == m_selected_extruder ) {
+                        at_least_one_has_correct_extruder = true;
+                        break;
+                    }
+                }
+                if (!at_least_one_has_correct_extruder)
+                    continue;
+            }
+
+#if ENABLE_LEGACY_OPENGL_REMOVAL
+            for (size_t i = 0; i < vols.size(); ++i) {
+                GLVolume* vol = vols[i];
+                if (vol->print_zs.empty() || vol->print_zs.back() != layer->print_z) {
+                    vol->print_zs.emplace_back(layer->print_z);
+                    vol->offsets.emplace_back(geometries[i].indices_count());
+                }
+            }
+#else
+            for (GLVolume* vol : vols)
+                if (vol->print_zs.empty() || vol->print_zs.back() != layer->print_z) {
+                    vol->print_zs.emplace_back(layer->print_z);
+                    vol->offsets.emplace_back(vol->indexed_vertex_array.quad_indices.size());
+                    vol->offsets.emplace_back(vol->indexed_vertex_array.triangle_indices.size());
+                }
+#endif // ENABLE_LEGACY_OPENGL_REMOVAL
+
+            for (const PrintInstance &instance : *ctxt.shifted_copies) {
+                const Point &copy = instance.shift;
+                for (const LayerRegion *layerm : layer->regions()) {
+                    if (is_selected_separate_extruder) {
+                        const PrintRegionConfig& cfg = layerm->region().config();
+                        if (cfg.perimeter_extruder.value    != m_selected_extruder ||
+                            cfg.infill_extruder.value       != m_selected_extruder ||
+                            cfg.solid_infill_extruder.value != m_selected_extruder)
+                            continue;
+                    }
+                    if (ctxt.has_perimeters)
+#if ENABLE_LEGACY_OPENGL_REMOVAL
+                        _3DScene::extrusionentity_to_verts(layerm->perimeters, float(layer->print_z), copy,
+                            select_geometry(idx_layer, layerm->region().config().perimeter_extruder.value, 0));
+#else
+                        _3DScene::extrusionentity_to_verts(layerm->perimeters, float(layer->print_z), copy,
+                        	volume(idx_layer, layerm->region().config().perimeter_extruder.value, 0));
+#endif // ENABLE_LEGACY_OPENGL_REMOVAL
+                    if (ctxt.has_infill) {
+                        for (const ExtrusionEntity *ee : layerm->fills.entities) {
+                            // fill represents infill extrusions of a single island.
+                            const auto *fill = dynamic_cast<const ExtrusionEntityCollection*>(ee);
+                            if (! fill->entities.empty())
+#if ENABLE_LEGACY_OPENGL_REMOVAL
+                                _3DScene::extrusionentity_to_verts(*fill, float(layer->print_z), copy,
+                                    select_geometry(idx_layer, is_solid_infill(fill->entities.front()->role()) ?
+                                                    layerm->region().config().solid_infill_extruder :
+                                                    layerm->region().config().infill_extruder, 1));
+#else
+                                _3DScene::extrusionentity_to_verts(*fill, float(layer->print_z), copy,
+	                                volume(idx_layer, 
+		                                is_solid_infill(fill->entities.front()->role()) ?
+			                                layerm->region().config().solid_infill_extruder :
+			                                layerm->region().config().infill_extruder,
+		                                1));
+#endif // ENABLE_LEGACY_OPENGL_REMOVAL
+                        }
+                    }
+                }
+                if (ctxt.has_support) {
+                    const SupportLayer *support_layer = dynamic_cast<const SupportLayer*>(layer);
+                    if (support_layer) {
+                        for (const ExtrusionEntity *extrusion_entity : support_layer->support_fills.entities)
+#if ENABLE_LEGACY_OPENGL_REMOVAL
+                            _3DScene::extrusionentity_to_verts(extrusion_entity, float(layer->print_z), copy,
+                                select_geometry(idx_layer, (extrusion_entity->role() == erSupportMaterial) ?
+                                                support_layer->object()->config().support_material_extruder :
+                                                support_layer->object()->config().support_material_interface_extruder, 2));
+#else
+                            _3DScene::extrusionentity_to_verts(extrusion_entity, float(layer->print_z), copy,
+	                            volume(idx_layer, 
+		                            (extrusion_entity->role() == erSupportMaterial) ?
+			                            support_layer->object()->config().support_material_extruder :
+			                            support_layer->object()->config().support_material_interface_extruder,
+		                            2));
+#endif // ENABLE_LEGACY_OPENGL_REMOVAL
+                    }
+                }
+            }
+            // Ensure that no volume grows over the limits. If the volume is too large, allocate a new one.
+	        for (size_t i = 0; i < vols.size(); ++i) {
+	            GLVolume &vol = *vols[i];
+#if ENABLE_LEGACY_OPENGL_REMOVAL
+                if (geometries[i].vertices_size_bytes() > MAX_VERTEX_BUFFER_SIZE) {
+                    vol.model.init_from(std::move(geometries[i]));
+#else
+                if (vol.indexed_vertex_array.vertices_and_normals_interleaved.size() > MAX_VERTEX_BUFFER_SIZE) {
+#endif // ENABLE_LEGACY_OPENGL_REMOVAL
+                    vols[i] = new_volume(vol.color);
+#if !ENABLE_LEGACY_OPENGL_REMOVAL
+                    reserve_new_volume_finalize_old_volume(*vols[i], vol, false);
+#endif // !ENABLE_LEGACY_OPENGL_REMOVAL
+                }
+	        }
+        }
+
+#if ENABLE_LEGACY_OPENGL_REMOVAL
+        for (size_t i = 0; i < vols.size(); ++i) {
+            if (!geometries[i].is_empty())
+                vols[i]->model.init_from(std::move(geometries[i]));
+        }
+#else
+        for (GLVolume *vol : vols)
+        	// Ideally one would call vol->indexed_vertex_array.finalize() here to move the buffers to the OpenGL driver,
+        	// but this code runs in parallel and the OpenGL driver is not thread safe.
+            vol->indexed_vertex_array.shrink_to_fit();
+#endif // ENABLE_LEGACY_OPENGL_REMOVAL
+    });
+
+    BOOST_LOG_TRIVIAL(debug) << "Loading print object toolpaths in parallel - finalizing results" << m_volumes.log_memory_info() << log_memory_info();
+    // Remove empty volumes from the newly added volumes.
+    {
+        for (auto ptr_it = m_volumes.volumes.begin() + volumes_cnt_initial; ptr_it != m_volumes.volumes.end(); ++ptr_it)
+            if ((*ptr_it)->empty()) {
+                delete *ptr_it;
+                *ptr_it = nullptr;
+            }
+        m_volumes.volumes.erase(std::remove(m_volumes.volumes.begin() + volumes_cnt_initial, m_volumes.volumes.end(), nullptr), m_volumes.volumes.end());
+    }
+    for (size_t i = volumes_cnt_initial; i < m_volumes.volumes.size(); ++i) {
+        GLVolume* v = m_volumes.volumes[i];
+#if ENABLE_LEGACY_OPENGL_REMOVAL
+        v->is_outside = !contains(build_volume, v->model);
+        // We are done editinig the model, now it can be sent to gpu
+        v->model.enable_render();
+#else
+        v->is_outside = ! build_volume.all_paths_inside_vertices_and_normals_interleaved(v->indexed_vertex_array.vertices_and_normals_interleaved, v->indexed_vertex_array.bounding_box());
+        v->indexed_vertex_array.finalize_geometry(m_initialized);
+#endif // ENABLE_LEGACY_OPENGL_REMOVAL
+    }
+
+    BOOST_LOG_TRIVIAL(debug) << "Loading print object toolpaths in parallel - end" << m_volumes.log_memory_info() << log_memory_info();
+}
+
+void GLCanvas3D::_load_wipe_tower_toolpaths(const BuildVolume& build_volume, const std::vector<std::string>& str_tool_colors)
+{
+    const Print *print = this->fff_print();
+    if (print == nullptr || print->wipe_tower_data().tool_changes.empty())
+        return;
+
+    if (!print->is_step_done(psWipeTower))
+        return;
+
+    std::vector<ColorRGBA> tool_colors;
+    decode_colors(str_tool_colors, tool_colors);
+
+    struct Ctxt
+    {
+        const Print                  *print;
+        const std::vector<ColorRGBA> *tool_colors;
+        Vec2f                         wipe_tower_pos;
+        float                         wipe_tower_angle;
+
+        static ColorRGBA color_support() { return ColorRGBA::GREENISH(); }
+
+        // For cloring by a tool, return a parsed color.
+        bool                         color_by_tool() const { return tool_colors != nullptr; }
+        size_t                       number_tools() const { return this->color_by_tool() ? tool_colors->size() : 0; }
+        const ColorRGBA&             color_tool(size_t tool) const { return (*tool_colors)[tool]; }
+        int                          volume_idx(int tool, int feature) const {
+            return this->color_by_tool() ? std::min<int>(this->number_tools() - 1, std::max<int>(tool, 0)) : feature;
+        }
+
+        const std::vector<WipeTower::ToolChangeResult>& tool_change(size_t idx) {
+            const auto &tool_changes = print->wipe_tower_data().tool_changes;
+            return priming.empty() ?
+                ((idx == tool_changes.size()) ? final : tool_changes[idx]) :
+                ((idx == 0) ? priming : (idx == tool_changes.size() + 1) ? final : tool_changes[idx - 1]);
+        }
+        std::vector<WipeTower::ToolChangeResult> priming;
+        std::vector<WipeTower::ToolChangeResult> final;
+    } ctxt;
+
+    ctxt.print = print;
+    ctxt.tool_colors = tool_colors.empty() ? nullptr : &tool_colors;
+    if (print->wipe_tower_data().priming && print->config().single_extruder_multi_material_priming)
+        for (int i=0; i<(int)print->wipe_tower_data().priming.get()->size(); ++i)
+            ctxt.priming.emplace_back(print->wipe_tower_data().priming.get()->at(i));
+    if (print->wipe_tower_data().final_purge)
+        ctxt.final.emplace_back(*print->wipe_tower_data().final_purge.get());
+
+    ctxt.wipe_tower_angle = ctxt.print->config().wipe_tower_rotation_angle.value/180.f * PI;
+    ctxt.wipe_tower_pos = Vec2f(ctxt.print->config().wipe_tower_x.value, ctxt.print->config().wipe_tower_y.value);
+
+    BOOST_LOG_TRIVIAL(debug) << "Loading wipe tower toolpaths in parallel - start" << m_volumes.log_memory_info() << log_memory_info();
+
+    //FIXME Improve the heuristics for a grain size.
+    size_t          n_items = print->wipe_tower_data().tool_changes.size() + (ctxt.priming.empty() ? 0 : 1);
+    size_t          grain_size = std::max(n_items / 128, size_t(1));
+    tbb::spin_mutex new_volume_mutex;
+    auto            new_volume = [this, &new_volume_mutex](const ColorRGBA& color) {
+        auto *volume = new GLVolume(color);
+		volume->is_extrusion_path = true;
+#if ENABLE_LEGACY_OPENGL_REMOVAL
+        // to prevent sending data to gpu (in the main thread) while
+        // editing the model geometry
+        volume->model.disable_render();
+#endif // ENABLE_LEGACY_OPENGL_REMOVAL
+        tbb::spin_mutex::scoped_lock lock;
+        lock.acquire(new_volume_mutex);
+        m_volumes.volumes.emplace_back(volume);
+        lock.release();
+        return volume;
+    };
+    const size_t   volumes_cnt_initial = m_volumes.volumes.size();
+    std::vector<GLVolumeCollection> volumes_per_thread(n_items);
+    tbb::parallel_for(
+        tbb::blocked_range<size_t>(0, n_items, grain_size),
+        [&ctxt, &new_volume](const tbb::blocked_range<size_t>& range) {
+        // Bounding box of this slab of a wipe tower.
+        GLVolumePtrs vols;
+#if ENABLE_LEGACY_OPENGL_REMOVAL
+        std::vector<GLModel::Geometry> geometries;
+#endif // ENABLE_LEGACY_OPENGL_REMOVAL
+        if (ctxt.color_by_tool()) {
+            for (size_t i = 0; i < ctxt.number_tools(); ++i) {
+                vols.emplace_back(new_volume(ctxt.color_tool(i)));
+#if ENABLE_LEGACY_OPENGL_REMOVAL
+                geometries.emplace_back(GLModel::Geometry());
+#endif // ENABLE_LEGACY_OPENGL_REMOVAL
+            }
+        }
+        else {
+            vols = { new_volume(ctxt.color_support()) };
+#if ENABLE_LEGACY_OPENGL_REMOVAL
+            geometries = { GLModel::Geometry() };
+#endif // ENABLE_LEGACY_OPENGL_REMOVAL
+        }
+
+#if ENABLE_LEGACY_OPENGL_REMOVAL
+        assert(vols.size() == geometries.size());
+        for (GLModel::Geometry& g : geometries) {
+            g.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3N3 };
+        }
+#else
+        for (GLVolume *volume : vols)
+			// Reserving number of vertices (3x position + 3x color)
+            volume->indexed_vertex_array.reserve(VERTEX_BUFFER_RESERVE_SIZE / 6);
+#endif // ENABLE_LEGACY_OPENGL_REMOVAL
+        for (size_t idx_layer = range.begin(); idx_layer < range.end(); ++idx_layer) {
+            const std::vector<WipeTower::ToolChangeResult> &layer = ctxt.tool_change(idx_layer);
+            for (size_t i = 0; i < vols.size(); ++i) {
+                GLVolume &vol = *vols[i];
+                if (vol.print_zs.empty() || vol.print_zs.back() != layer.front().print_z) {
+                    vol.print_zs.emplace_back(layer.front().print_z);
+#if ENABLE_LEGACY_OPENGL_REMOVAL
+                    vol.offsets.emplace_back(geometries[i].indices_count());
+#else
+                    vol.offsets.emplace_back(vol.indexed_vertex_array.quad_indices.size());
+                    vol.offsets.emplace_back(vol.indexed_vertex_array.triangle_indices.size());
+#endif // ENABLE_LEGACY_OPENGL_REMOVAL
+                }
+            }
+            for (const WipeTower::ToolChangeResult &extrusions : layer) {
+                for (size_t i = 1; i < extrusions.extrusions.size();) {
+                    const WipeTower::Extrusion &e = extrusions.extrusions[i];
+                    if (e.width == 0.) {
+                        ++i;
+                        continue;
+                    }
+                    size_t j = i + 1;
+                    if (ctxt.color_by_tool())
+                        for (; j < extrusions.extrusions.size() && extrusions.extrusions[j].tool == e.tool && extrusions.extrusions[j].width > 0.f; ++j);
+                    else
+                        for (; j < extrusions.extrusions.size() && extrusions.extrusions[j].width > 0.f; ++j);
+                    size_t              n_lines = j - i;
+                    Lines               lines;
+                    std::vector<double> widths;
+                    std::vector<double> heights;
+                    lines.reserve(n_lines);
+                    widths.reserve(n_lines);
+                    heights.assign(n_lines, extrusions.layer_height);
+                    WipeTower::Extrusion e_prev = extrusions.extrusions[i-1];
+
+                    if (!extrusions.priming) { // wipe tower extrusions describe the wipe tower at the origin with no rotation
+                        e_prev.pos = Eigen::Rotation2Df(ctxt.wipe_tower_angle) * e_prev.pos;
+                        e_prev.pos += ctxt.wipe_tower_pos;
+                    }
+
+                    for (; i < j; ++i) {
+                        WipeTower::Extrusion e = extrusions.extrusions[i];
+                        assert(e.width > 0.f);
+                        if (!extrusions.priming) {
+                            e.pos = Eigen::Rotation2Df(ctxt.wipe_tower_angle) * e.pos;
+                            e.pos += ctxt.wipe_tower_pos;
+                        }
+
+                        lines.emplace_back(Point::new_scale(e_prev.pos.x(), e_prev.pos.y()), Point::new_scale(e.pos.x(), e.pos.y()));
+                        widths.emplace_back(e.width);
+
+                        e_prev = e;
+                    }
+
+#if ENABLE_LEGACY_OPENGL_REMOVAL
+                    _3DScene::thick_lines_to_verts(lines, widths, heights, lines.front().a == lines.back().b, extrusions.print_z,
+                        geometries[ctxt.volume_idx(e.tool, 0)]);
+#else
+                    _3DScene::thick_lines_to_verts(lines, widths, heights, lines.front().a == lines.back().b, extrusions.print_z,
+                        *vols[ctxt.volume_idx(e.tool, 0)]);
+#endif // ENABLE_LEGACY_OPENGL_REMOVAL
+                }
+            }
+        }
+        for (size_t i = 0; i < vols.size(); ++i) {
+            GLVolume &vol = *vols[i];
+#if ENABLE_LEGACY_OPENGL_REMOVAL
+            if (geometries[i].vertices_size_bytes() > MAX_VERTEX_BUFFER_SIZE) {
+                vol.model.init_from(std::move(geometries[i]));
+#else
+            if (vol.indexed_vertex_array.vertices_and_normals_interleaved.size() > MAX_VERTEX_BUFFER_SIZE) {
+#endif // ENABLE_LEGACY_OPENGL_REMOVAL
+                vols[i] = new_volume(vol.color);
+#if !ENABLE_LEGACY_OPENGL_REMOVAL
+                reserve_new_volume_finalize_old_volume(*vols[i], vol, false);
+#endif // !ENABLE_LEGACY_OPENGL_REMOVAL
+            }
+        }
+
+#if ENABLE_LEGACY_OPENGL_REMOVAL
+        for (size_t i = 0; i < vols.size(); ++i) {
+            if (!geometries[i].is_empty())
+                vols[i]->model.init_from(std::move(geometries[i]));
+        }
+#else
+        for (GLVolume *vol : vols)
+            vol->indexed_vertex_array.shrink_to_fit();
+#endif // ENABLE_LEGACY_OPENGL_REMOVAL
+        });
+
+    BOOST_LOG_TRIVIAL(debug) << "Loading wipe tower toolpaths in parallel - finalizing results" << m_volumes.log_memory_info() << log_memory_info();
+    // Remove empty volumes from the newly added volumes.
+    {
+        for (auto ptr_it = m_volumes.volumes.begin() + volumes_cnt_initial; ptr_it != m_volumes.volumes.end(); ++ptr_it)
+            if ((*ptr_it)->empty()) {
+                delete *ptr_it;
+                *ptr_it = nullptr;
+            }
+        m_volumes.volumes.erase(std::remove(m_volumes.volumes.begin() + volumes_cnt_initial, m_volumes.volumes.end(), nullptr), m_volumes.volumes.end());
+    }
+    for (size_t i = volumes_cnt_initial; i < m_volumes.volumes.size(); ++i) {
+        GLVolume* v = m_volumes.volumes[i];
+#if ENABLE_LEGACY_OPENGL_REMOVAL
+        v->is_outside = !contains(build_volume, v->model);
+        // We are done editinig the model, now it can be sent to gpu
+        v->model.enable_render();
+#else
+        v->is_outside = ! build_volume.all_paths_inside_vertices_and_normals_interleaved(v->indexed_vertex_array.vertices_and_normals_interleaved, v->indexed_vertex_array.bounding_box());
+        v->indexed_vertex_array.finalize_geometry(m_initialized);
+#endif // ENABLE_LEGACY_OPENGL_REMOVAL
+    }
+
+    BOOST_LOG_TRIVIAL(debug) << "Loading wipe tower toolpaths in parallel - end" << m_volumes.log_memory_info() << log_memory_info();
+}
+
+// While it looks like we can call 
+// this->reload_scene(true, true)
+// the two functions are quite different:
+// 1) This function only loads objects, for which the step slaposSliceSupports already finished. Therefore objects outside of the print bed never load.
+// 2) This function loads object mesh with the relative scaling correction (the "relative_correction" parameter) was applied,
+// 	  therefore the mesh may be slightly larger or smaller than the mesh shown in the 3D scene.
+void GLCanvas3D::_load_sla_shells()
+{
+    const SLAPrint* print = this->sla_print();
+    if (print->objects().empty())
+        // nothing to render, return
+        return;
+
+    auto add_volume = [this](const SLAPrintObject &object, int volume_id, const SLAPrintObject::Instance& instance,
+        const TriangleMesh& mesh, const ColorRGBA& color, bool outside_printer_detection_enabled) {
+        m_volumes.volumes.emplace_back(new GLVolume(color));
+        GLVolume& v = *m_volumes.volumes.back();
+#if ENABLE_SMOOTH_NORMALS
+#if ENABLE_LEGACY_OPENGL_REMOVAL
+        v.model.init_from(mesh, true);
+#else
+        v.indexed_vertex_array.load_mesh(mesh, true);
+#endif // ENABLE_LEGACY_OPENGL_REMOVAL
+#else
+#if ENABLE_LEGACY_OPENGL_REMOVAL
+        v.model.init_from(mesh);
+#else
+        v.indexed_vertex_array.load_mesh(mesh);
+#endif // ENABLE_LEGACY_OPENGL_REMOVAL
+#endif // ENABLE_SMOOTH_NORMALS
+#if !ENABLE_LEGACY_OPENGL_REMOVAL
+        v.indexed_vertex_array.finalize_geometry(m_initialized);
+#endif // !ENABLE_LEGACY_OPENGL_REMOVAL
+        v.shader_outside_printer_detection_enabled = outside_printer_detection_enabled;
+        v.composite_id.volume_id = volume_id;
+        v.set_instance_offset(unscale(instance.shift.x(), instance.shift.y(), 0.0));
+        v.set_instance_rotation({ 0.0, 0.0, (double)instance.rotation });
+        v.set_instance_mirror(X, object.is_left_handed() ? -1. : 1.);
+        v.set_convex_hull(mesh.convex_hull_3d());
+    };
+
+    // adds objects' volumes 
+    for (const SLAPrintObject* obj : print->objects())
+        if (obj->is_step_done(slaposSliceSupports)) {
+            unsigned int initial_volumes_count = (unsigned int)m_volumes.volumes.size();
+            for (const SLAPrintObject::Instance& instance : obj->instances()) {
+                add_volume(*obj, 0, instance, obj->get_mesh_to_print(), GLVolume::MODEL_COLOR[0], true);
+                // Set the extruder_id and volume_id to achieve the same color as in the 3D scene when
+                // through the update_volumes_colors_by_extruder() call.
+                m_volumes.volumes.back()->extruder_id = obj->model_object()->volumes.front()->extruder_id();
+                if (obj->is_step_done(slaposSupportTree) && obj->has_mesh(slaposSupportTree))
+                    add_volume(*obj, -int(slaposSupportTree), instance, obj->support_mesh(), GLVolume::SLA_SUPPORT_COLOR, true);
+                if (obj->is_step_done(slaposPad) && obj->has_mesh(slaposPad))
+                    add_volume(*obj, -int(slaposPad), instance, obj->pad_mesh(), GLVolume::SLA_PAD_COLOR, false);
+            }
+            double shift_z = obj->get_current_elevation();
+            for (unsigned int i = initial_volumes_count; i < m_volumes.volumes.size(); ++ i) {
+                // apply shift z
+                m_volumes.volumes[i]->set_sla_shift_z(shift_z);
+            }
+        }
+
+    update_volumes_colors_by_extruder();
+}
+
+void GLCanvas3D::_update_sla_shells_outside_state()
+{
+    check_volumes_outside_state();
+}
+
+void GLCanvas3D::_set_warning_notification_if_needed(EWarning warning)
+{
+    _set_current();
+    bool show = false;
+    if (!m_volumes.empty())
+        show = _is_any_volume_outside();
+    else {
+        if (wxGetApp().is_editor()) {
+            if (current_printer_technology() != ptSLA)
+                show = m_gcode_viewer.has_data() && !m_gcode_viewer.is_contained_in_bed();
+        }
+    }
+
+    _set_warning_notification(warning, show);
+}
+
+void GLCanvas3D::_set_warning_notification(EWarning warning, bool state)
+{
+    enum ErrorType{
+        PLATER_WARNING,
+        PLATER_ERROR,
+        SLICING_ERROR
+    };
+    std::string text;
+    ErrorType error = ErrorType::PLATER_WARNING;
+    switch (warning) {
+    case EWarning::ObjectOutside:      text = _u8L("An object outside the print area was detected."); break;
+    case EWarning::ToolpathOutside:    text = _u8L("A toolpath outside the print area was detected."); error = ErrorType::SLICING_ERROR; break;
+    case EWarning::SlaSupportsOutside: text = _u8L("SLA supports outside the print area were detected."); error = ErrorType::PLATER_ERROR; break;
+    case EWarning::SomethingNotShown:  text = _u8L("Some objects are not visible during editing."); break;
+    case EWarning::ObjectClashed:
+        text = _u8L("An object outside the print area was detected.\n"
+            "Resolve the current problem to continue slicing.");
+        error = ErrorType::PLATER_ERROR;
+        break;
+    }
+    auto& notification_manager = *wxGetApp().plater()->get_notification_manager();
+    switch (error)
+    {
+    case PLATER_WARNING:
+        if (state)
+            notification_manager.push_plater_warning_notification(text);
+        else
+            notification_manager.close_plater_warning_notification(text);
+        break;
+    case PLATER_ERROR:
+        if (state)
+            notification_manager.push_plater_error_notification(text);
+        else
+            notification_manager.close_plater_error_notification(text);
+        break;
+    case SLICING_ERROR:
+        if (state)
+            notification_manager.push_slicing_error_notification(text);
+        else
+            notification_manager.close_slicing_error_notification(text);
+        break;
+    default:
+        break;
+    }
+}
+
+bool GLCanvas3D::_is_any_volume_outside() const
+{
+    for (const GLVolume* volume : m_volumes.volumes) {
+        if (volume != nullptr && volume->is_outside)
+            return true;
+    }
+
+    return false;
+}
+
+void GLCanvas3D::_update_selection_from_hover()
+{
+    bool ctrl_pressed = wxGetKeyState(WXK_CONTROL);
+
+    if (m_hover_volume_idxs.empty()) {
+        if (!ctrl_pressed && m_rectangle_selection.get_state() == GLSelectionRectangle::EState::Select)
+            m_selection.remove_all();
+
+        return;
+    }
+
+    GLSelectionRectangle::EState state = m_rectangle_selection.get_state();
+
+    bool hover_modifiers_only = true;
+    for (int i : m_hover_volume_idxs) {
+        if (!m_volumes.volumes[i]->is_modifier) {
+            hover_modifiers_only = false;
+            break;
+        }
+    }
+
+    bool selection_changed = false;
+#if ENABLE_NEW_RECTANGLE_SELECTION
+    if (!m_rectangle_selection.is_empty()) {
+#endif // ENABLE_NEW_RECTANGLE_SELECTION
+    if (state == GLSelectionRectangle::EState::Select) {
+        bool contains_all = true;
+        for (int i : m_hover_volume_idxs) {
+            if (!m_selection.contains_volume((unsigned int)i)) {
+                contains_all = false;
+                break;
+            }
+        }
+
+        // the selection is going to be modified (Add)
+        if (!contains_all) {
+            wxGetApp().plater()->take_snapshot(_L("Selection-Add from rectangle"), UndoRedo::SnapshotType::Selection);
+            selection_changed = true;
+        }
+    }
+    else {
+        bool contains_any = false;
+        for (int i : m_hover_volume_idxs) {
+            if (m_selection.contains_volume((unsigned int)i)) {
+                contains_any = true;
+                break;
+            }
+        }
+
+        // the selection is going to be modified (Remove)
+        if (contains_any) {
+            wxGetApp().plater()->take_snapshot(_L("Selection-Remove from rectangle"), UndoRedo::SnapshotType::Selection);
+            selection_changed = true;
+        }
+    }
+#if ENABLE_NEW_RECTANGLE_SELECTION
+    }
+#endif // ENABLE_NEW_RECTANGLE_SELECTION
+
+    if (!selection_changed)
+        return;
+
+    Plater::SuppressSnapshots suppress(wxGetApp().plater());
+
+    if (state == GLSelectionRectangle::EState::Select && !ctrl_pressed)
+        m_selection.clear();
+
+    for (int i : m_hover_volume_idxs) {
+        if (state == GLSelectionRectangle::EState::Select) {
+            if (hover_modifiers_only) {
+                const GLVolume& v = *m_volumes.volumes[i];
+                m_selection.add_volume(v.object_idx(), v.volume_idx(), v.instance_idx(), false);
+            }
+            else
+                m_selection.add(i, false);
+        }
+        else
+            m_selection.remove(i);
+    }
+
+    if (m_selection.is_empty())
+        m_gizmos.reset_all_states();
+    else
+        m_gizmos.refresh_on_off_state();
+
+    m_gizmos.update_data();
+    post_event(SimpleEvent(EVT_GLCANVAS_OBJECT_SELECT));
+    m_dirty = true;
+}
+
+bool GLCanvas3D::_deactivate_undo_redo_toolbar_items()
+{
+    if (m_undoredo_toolbar.is_item_pressed("undo")) {
+        m_undoredo_toolbar.force_right_action(m_undoredo_toolbar.get_item_id("undo"), *this);
+        return true;
+    }
+    else if (m_undoredo_toolbar.is_item_pressed("redo")) {
+        m_undoredo_toolbar.force_right_action(m_undoredo_toolbar.get_item_id("redo"), *this);
+        return true;
+    }
+
+    return false;
+}
+
+bool GLCanvas3D::is_search_pressed() const
+{
+    return m_main_toolbar.is_item_pressed("search");
+}
+
+bool GLCanvas3D::_deactivate_arrange_menu()
+{
+    if (m_main_toolbar.is_item_pressed("arrange")) {
+        m_main_toolbar.force_right_action(m_main_toolbar.get_item_id("arrange"), *this);
+        return true;
+    }
+
+    return false;
+}
+
+bool GLCanvas3D::_deactivate_search_toolbar_item()
+{
+    if (is_search_pressed()) {
+        m_main_toolbar.force_left_action(m_main_toolbar.get_item_id("search"), *this);
+        return true;
+    }
+
+    return false;
+}
+
+bool GLCanvas3D::_activate_search_toolbar_item()
+{
+    if (!m_main_toolbar.is_item_pressed("search")) {
+        m_main_toolbar.force_left_action(m_main_toolbar.get_item_id("search"), *this);
+        return true;
+    }
+
+    return false;
+}
+
+bool GLCanvas3D::_deactivate_collapse_toolbar_items()
+{
+    GLToolbar& collapse_toolbar = wxGetApp().plater()->get_collapse_toolbar();
+    if (collapse_toolbar.is_item_pressed("print")) {
+        collapse_toolbar.force_left_action(collapse_toolbar.get_item_id("print"), *this);
+        return true;
+    }
+
+    return false;
+}
+
+void GLCanvas3D::highlight_toolbar_item(const std::string& item_name)
+{
+    GLToolbarItem* item = m_main_toolbar.get_item(item_name);
+    if (!item)
+        item = m_undoredo_toolbar.get_item(item_name);
+    if (!item || !item->is_visible())
+        return;
+    m_toolbar_highlighter.init(item, this);
+}
+
+void GLCanvas3D::highlight_gizmo(const std::string& gizmo_name)
+{
+    GLGizmosManager::EType gizmo = m_gizmos.get_gizmo_from_name(gizmo_name);
+    if(gizmo == GLGizmosManager::EType::Undefined)
+        return;
+    m_gizmo_highlighter.init(&m_gizmos, gizmo, this);
+}
+
+const Print* GLCanvas3D::fff_print() const
+{
+    return (m_process == nullptr) ? nullptr : m_process->fff_print();
+}
+
+const SLAPrint* GLCanvas3D::sla_print() const
+{
+    return (m_process == nullptr) ? nullptr : m_process->sla_print();
+}
+
+void GLCanvas3D::WipeTowerInfo::apply_wipe_tower() const
+{
+    DynamicPrintConfig cfg;
+    cfg.opt<ConfigOptionFloat>("wipe_tower_x", true)->value = m_pos(X);
+    cfg.opt<ConfigOptionFloat>("wipe_tower_y", true)->value = m_pos(Y);
+    cfg.opt<ConfigOptionFloat>("wipe_tower_rotation_angle", true)->value = (180./M_PI) * m_rotation;
+    wxGetApp().get_tab(Preset::TYPE_PRINT)->load_config(cfg);
+}
+
+void GLCanvas3D::RenderTimer::Notify()
+{
+    wxPostEvent((wxEvtHandler*)GetOwner(), RenderTimerEvent( EVT_GLCANVAS_RENDER_TIMER, *this));
+}
+
+void GLCanvas3D::ToolbarHighlighterTimer::Notify()
+{
+    wxPostEvent((wxEvtHandler*)GetOwner(), ToolbarHighlighterTimerEvent(EVT_GLCANVAS_TOOLBAR_HIGHLIGHTER_TIMER, *this));
+}
+
+void GLCanvas3D::GizmoHighlighterTimer::Notify()
+{
+    wxPostEvent((wxEvtHandler*)GetOwner(), GizmoHighlighterTimerEvent(EVT_GLCANVAS_GIZMO_HIGHLIGHTER_TIMER, *this));
+}
+
+void GLCanvas3D::ToolbarHighlighter::set_timer_owner(wxEvtHandler* owner, int timerid/* = wxID_ANY*/)
+{
+    m_timer.SetOwner(owner, timerid);
+}
+
+void GLCanvas3D::ToolbarHighlighter::init(GLToolbarItem* toolbar_item, GLCanvas3D* canvas)
+{
+    if (m_timer.IsRunning())
+        invalidate();
+    if (!toolbar_item || !canvas)
+        return;
+
+    m_timer.Start(300, false);
+
+    m_toolbar_item = toolbar_item;
+    m_canvas       = canvas;
+}
+
+void GLCanvas3D::ToolbarHighlighter::invalidate()
+{
+    m_timer.Stop();
+
+    if (m_toolbar_item) {
+        m_toolbar_item->set_highlight(GLToolbarItem::EHighlightState::NotHighlighted);
+    }
+    m_toolbar_item = nullptr;
+    m_blink_counter = 0;
+    m_render_arrow = false;
+}
+
+void GLCanvas3D::ToolbarHighlighter::blink()
+{
+    if (m_toolbar_item) {
+        char state = m_toolbar_item->get_highlight();
+        if (state != (char)GLToolbarItem::EHighlightState::HighlightedShown)
+            m_toolbar_item->set_highlight(GLToolbarItem::EHighlightState::HighlightedShown);
+        else 
+            m_toolbar_item->set_highlight(GLToolbarItem::EHighlightState::HighlightedHidden);
+
+        m_render_arrow = !m_render_arrow;
+        m_canvas->set_as_dirty();
+    }
+    else
+        invalidate();
+
+    if ((++m_blink_counter) >= 11)
+        invalidate();
+}
+
+void GLCanvas3D::GizmoHighlighter::set_timer_owner(wxEvtHandler* owner, int timerid/* = wxID_ANY*/)
+{
+    m_timer.SetOwner(owner, timerid);
+}
+
+void GLCanvas3D::GizmoHighlighter::init(GLGizmosManager* manager, GLGizmosManager::EType gizmo, GLCanvas3D* canvas)
+{
+    if (m_timer.IsRunning())
+        invalidate();
+    if (!gizmo || !canvas)
+        return;
+
+    m_timer.Start(300, false);
+
+    m_gizmo_manager = manager;
+    m_gizmo_type    = gizmo;
+    m_canvas        = canvas;
+}
+
+void GLCanvas3D::GizmoHighlighter::invalidate()
+{
+    m_timer.Stop();
+
+    if (m_gizmo_manager) {
+        m_gizmo_manager->set_highlight(GLGizmosManager::EType::Undefined, false);
+    }
+    m_gizmo_manager = nullptr;
+    m_gizmo_type = GLGizmosManager::EType::Undefined;
+    m_blink_counter = 0;
+    m_render_arrow = false;
+}
+
+void GLCanvas3D::GizmoHighlighter::blink()
+{
+    if (m_gizmo_manager) {
+        if (m_blink_counter % 2 == 0)
+            m_gizmo_manager->set_highlight(m_gizmo_type, true);
+        else
+            m_gizmo_manager->set_highlight(m_gizmo_type, false);
+
+        m_render_arrow = !m_render_arrow;
+        m_canvas->set_as_dirty();
+    }
+    else
+        invalidate();
+
+    if ((++m_blink_counter) >= 11)
+        invalidate();
+}
+
+} // namespace GUI
+} // namespace Slic3r

From a2a85af4ddde1351f6a54a0f468fed13540e85f5 Mon Sep 17 00:00:00 2001
From: enricoturri1966 <enricoturri@seznam.cz>
Date: Tue, 3 May 2022 13:54:23 +0200
Subject: [PATCH 08/23] Fixed out of bounds when showing color prints in gcode
 preview legend

---
 src/slic3r/GUI/GCodeViewer.cpp | 5 ++---
 1 file changed, 2 insertions(+), 3 deletions(-)

diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp
index 993070b45..afcc3f954 100644
--- a/src/slic3r/GUI/GCodeViewer.cpp
+++ b/src/slic3r/GUI/GCodeViewer.cpp
@@ -3941,9 +3941,8 @@ void GCodeViewer::render_legend(float& legend_height)
             PartialTimes items;
 
             std::vector<CustomGCode::Item> custom_gcode_per_print_z = wxGetApp().is_editor() ? wxGetApp().plater()->model().custom_gcode_per_print_z.gcodes : m_custom_gcode_per_print_z;
-            int extruders_count = wxGetApp().extruders_edited_cnt();
-            std::vector<ColorRGBA> last_color(extruders_count);
-            for (int i = 0; i < extruders_count; ++i) {
+            std::vector<ColorRGBA> last_color(m_extruders_count);
+            for (int i = 0; i < m_extruders_count; ++i) {
                 last_color[i] = m_tool_colors[i];
             }
             int last_extruder_id = 1;

From ff58e73ef2d0197cb15f7c0915ee662aecc96dc4 Mon Sep 17 00:00:00 2001
From: rtyr <36745189+rtyr@users.noreply.github.com>
Date: Wed, 4 May 2022 09:05:00 +0200
Subject: [PATCH 09/23] Sync with PrusaSlicer-settings

---
 resources/profiles/TriLAB.idx |  1 +
 resources/profiles/TriLAB.ini | 14 +++++++++-----
 2 files changed, 10 insertions(+), 5 deletions(-)

diff --git a/resources/profiles/TriLAB.idx b/resources/profiles/TriLAB.idx
index 1b12439f7..75819e135 100644
--- a/resources/profiles/TriLAB.idx
+++ b/resources/profiles/TriLAB.idx
@@ -1,4 +1,5 @@
 min_slic3r_version = 2.4.1-rc1
+1.0.1 Fix missing AzteQ Industrial ABS material for 0.6, 0.8 nozzle, enabled elefant foot compensation
 1.0.0 Added AzteQ Industrial profiles for 0.8 nozzle, updated spool weight and filament cost, some minor setting improvements
 min_slic3r_version = 2.3.2-alpha0
 0.0.9 Added AzteQ Industrial materials PC/ABS (Fillamentum), PC-Max (Polymaker), Nylon FX256 (Fillamentum), Added DeltiQ 2 materials Nylon PA12 (Fiberlogy), Nylon CF15 Carbon (Fillamentum), PEBA 90A - FlexFill (Fillamentum), MoldLay (Wax-Alike), disabled retract only when crossing perimeters, some minor setting improvements
diff --git a/resources/profiles/TriLAB.ini b/resources/profiles/TriLAB.ini
index a5b194eca..b5c31d183 100644
--- a/resources/profiles/TriLAB.ini
+++ b/resources/profiles/TriLAB.ini
@@ -6,7 +6,7 @@
 name = TriLAB
 # Configuration version of this file. Config file will only be installed, if the config_version differs.
 # This means, the server may force the PrusaSlicer configuration to be downgraded.
-config_version = 1.0.0
+config_version = 1.0.1
 # Where to get the updates from?
 config_update_url = https://files.prusa3d.com/wp-content/uploads/repository/PrusaSlicer-settings-master/live/TriLAB/
 # changelog_url = https://files.prusa3d.com/?latest=slicer-profiles&lng=%1%
@@ -118,7 +118,7 @@ complete_objects = 0
 default_acceleration = 2000
 dont_support_bridges = 0
 draft_shield = 0
-elefant_foot_compensation = 0.0
+elefant_foot_compensation = 0.1
 ensure_vertical_shell_thickness = 0
 external_perimeter_extrusion_width = 0.45
 external_perimeter_speed = 30
@@ -338,6 +338,7 @@ support_material_extrusion_width = 0.55
 support_material_interface_spacing = 0.6
 support_material_xy_spacing = 0.9
 top_infill_extrusion_width = 0.6
+elefant_foot_compensation = 0.2
 
 [print:DeltiQ 0.30mm Strong @0.6 nozzle]
 inherits = DeltiQ 0.30mm Normal @0.6 nozzle
@@ -356,7 +357,7 @@ bottom_solid_min_thickness = 1.2
 bridge_flow_ratio = 0.90
 bridge_speed = 20
 compatible_printers_condition = printer_notes=~/.*PRINTER_VENDOR_TRILAB.*/ and printer_notes=~/.*PRINTER_FAMILY_DQ.*/ and nozzle_diameter[0]==0.8
-elefant_foot_compensation = 0.0
+elefant_foot_compensation = 0.2
 external_perimeter_extrusion_width = 0.80
 external_perimeter_speed = 30
 extrusion_width = 0.80
@@ -415,7 +416,7 @@ complete_objects = 0
 default_acceleration = 2000
 dont_support_bridges = 0
 draft_shield = 0
-elefant_foot_compensation = 0
+elefant_foot_compensation = 0.1
 ensure_vertical_shell_thickness = 0
 external_perimeter_extrusion_width = 0.45
 external_perimeter_speed = 30
@@ -558,6 +559,7 @@ support_material_extrusion_width = 0.55
 support_material_interface_spacing = 0.6
 support_material_xy_spacing = 0.9
 top_infill_extrusion_width = 0.6
+elefant_foot_compensation = 0.2
 
 [print:AzteQ Industrial 0.30mm Strong @0.6 nozzle]
 inherits = AzteQ Industrial 0.30mm Normal @0.6 nozzle
@@ -579,7 +581,7 @@ bottom_solid_min_thickness = 0.7
 bridge_flow_ratio = 0.95
 bridge_speed = 30
 dont_support_bridges = 0
-elefant_foot_compensation = 0
+elefant_foot_compensation = 0.2
 ensure_vertical_shell_thickness = 0
 external_perimeter_extrusion_width = 0.8
 external_perimeter_speed = 30
@@ -1413,9 +1415,11 @@ filament_spool_weight = 229
 
 [filament:AzteQ Industrial - ABS - ExtraFill (Fillamentum) @0.6 nozzle]
 inherits = AzteQ Industrial - ABS - ExtraFill (Fillamentum)
+compatible_printers_condition = printer_notes=~/.*PRINTER_VENDOR_TRILAB.*/ and printer_notes=~/.*PRINTER_MODEL_AQI.*/ and nozzle_diameter[0]==0.6
 
 [filament:AzteQ Industrial - ABS - ExtraFill (Fillamentum) @0.8 nozzle]
 inherits = AzteQ Industrial - ABS - ExtraFill (Fillamentum)
+compatible_printers_condition = printer_notes=~/.*PRINTER_VENDOR_TRILAB.*/ and printer_notes=~/.*PRINTER_MODEL_AQI.*/ and nozzle_diameter[0]==0.8
 extrusion_multiplier = 0.95
 
 [filament:AzteQ Industrial - PC/ABS - (Fillamentum)]

From 7380787b3a53252beb4650ccd1edb8ce2027a1db Mon Sep 17 00:00:00 2001
From: YuSanka <yusanka@gmail.com>
Date: Wed, 4 May 2022 12:44:31 +0200
Subject: [PATCH 10/23] Settings Tab: Fix for
 https://dev.prusa3d.com/browse/SPE-1229

---
 src/slic3r/GUI/Tab.cpp | 24 +++++++++++++++++++++++-
 1 file changed, 23 insertions(+), 1 deletion(-)

diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp
index 86394819e..96151d8c7 100644
--- a/src/slic3r/GUI/Tab.cpp
+++ b/src/slic3r/GUI/Tab.cpp
@@ -3481,6 +3481,28 @@ void Tab::activate_selected_page(std::function<void()> throw_if_canceled)
     toggle_options();
 }
 
+#ifdef WIN32
+// Override the wxCheckForInterrupt to process inperruptions just from key or mouse
+// and to avoid an unwanted early call of CallAfter()
+static bool CheckForInterrupt(wxWindow* wnd)
+{
+    wxCHECK(wnd, false);
+
+    MSG msg;
+    while (::PeekMessage(&msg, ((HWND)((wnd)->GetHWND())), WM_KEYFIRST, WM_KEYLAST, PM_REMOVE))
+    {
+        ::TranslateMessage(&msg);
+        ::DispatchMessage(&msg);
+    }
+    while (::PeekMessage(&msg, ((HWND)((wnd)->GetHWND())), WM_MOUSEFIRST, WM_MOUSELAST, PM_REMOVE))
+    {
+        ::TranslateMessage(&msg);
+        ::DispatchMessage(&msg);
+    }
+    return true;
+}
+#endif //WIN32
+
 bool Tab::tree_sel_change_delayed()
 {
     // There is a bug related to Ubuntu overlay scrollbars, see https://github.com/prusa3d/PrusaSlicer/issues/898 and https://github.com/prusa3d/PrusaSlicer/issues/952.
@@ -3517,7 +3539,7 @@ bool Tab::tree_sel_change_delayed()
     
     auto throw_if_canceled = std::function<void()>([this](){
 #ifdef WIN32
-            wxCheckForInterrupt(m_treectrl);
+            CheckForInterrupt(m_treectrl);
             if (m_page_switch_planned)
                 throw UIBuildCanceled();
 #else // WIN32

From a627614b58bbc6bde9cb1afabaebfbd629492723 Mon Sep 17 00:00:00 2001
From: Vojtech Bubnik <bubnikv@gmail.com>
Date: Wed, 4 May 2022 15:05:56 +0200
Subject: [PATCH 11/23] Perl unit tests for perimeters and multi-material were
 rewritten to C++. Perl binding was slimmed down, namely Clipper is no more
 linked by Perl.

---
 lib/Slic3r.pm                             |  83 ---
 lib/Slic3r/Config.pm                      |  41 --
 lib/Slic3r/ExPolygon.pm                   |   7 -
 lib/Slic3r/Geometry/Clipper.pm            |  13 -
 lib/Slic3r/Layer.pm                       |   3 -
 lib/Slic3r/Line.pm                        |   5 -
 lib/Slic3r/Model.pm                       |   7 -
 lib/Slic3r/Polygon.pm                     |   7 +-
 lib/Slic3r/Print/Object.pm                |   6 -
 lib/Slic3r/Print/State.pm                 |  12 -
 src/libslic3r/GCodeReader.cpp             |  64 ++-
 src/libslic3r/GCodeReader.hpp             |   2 +
 src/libslic3r/Polyline.cpp                |   2 +-
 src/libslic3r/Polyline.hpp                |   9 -
 src/libslic3r/PrintConfig.hpp             |  10 +
 src/libslic3r/Utils.hpp                   |  21 -
 src/libslic3r/utils.cpp                   |  63 ---
 t/dynamic.t                               |  93 ----
 t/geometry.t                              |   9 +-
 t/multi.t                                 | 221 --------
 t/perimeters.t                            | 444 ----------------
 t/slice.t                                 | 152 ------
 t/support.t                               | 272 ----------
 tests/fff_print/CMakeLists.txt            |   2 +
 tests/fff_print/test_extrusion_entity.cpp |   9 +-
 tests/fff_print/test_multi.cpp            | 268 ++++++++++
 tests/fff_print/test_perimeters.cpp       | 599 ++++++++++++++++++++++
 tests/fff_print/test_support_material.cpp | 259 ++++++++++
 tests/libslic3r/test_clipper_utils.cpp    |   5 +
 xs/CMakeLists.txt                         |   4 -
 xs/lib/Slic3r/XS.pm                       |  49 --
 xs/src/perlglue.cpp                       |   5 -
 xs/t/01_trianglemesh.t                    |   7 +-
 xs/t/13_polylinecollection.t              |  35 --
 xs/xsp/Clipper.xsp                        |  73 ---
 xs/xsp/Geometry.xsp                       |   8 -
 xs/xsp/Layer.xsp                          |  41 --
 xs/xsp/PerimeterGenerator.xsp             |  40 --
 xs/xsp/PlaceholderParser.xsp              |  33 --
 xs/xsp/Polygon.xsp                        |   1 -
 xs/xsp/Polyline.xsp                       |  14 -
 xs/xsp/PolylineCollection.xsp             |  81 ---
 xs/xsp/Print.xsp                          |  56 --
 xs/xsp/Surface.xsp                        |   1 -
 xs/xsp/TriangleMesh.xsp                   |  89 ----
 xs/xsp/XS.xsp                             | 127 +----
 xs/xsp/my.map                             | 113 ----
 xs/xsp/typemap.xspt                       |  39 --
 48 files changed, 1194 insertions(+), 2310 deletions(-)
 delete mode 100644 lib/Slic3r/Geometry/Clipper.pm
 delete mode 100644 lib/Slic3r/Print/State.pm
 delete mode 100644 t/dynamic.t
 delete mode 100644 t/multi.t
 delete mode 100644 t/perimeters.t
 delete mode 100644 t/slice.t
 delete mode 100644 t/support.t
 create mode 100644 tests/fff_print/test_multi.cpp
 create mode 100644 tests/fff_print/test_perimeters.cpp
 delete mode 100644 xs/t/13_polylinecollection.t
 delete mode 100644 xs/xsp/Clipper.xsp
 delete mode 100644 xs/xsp/PerimeterGenerator.xsp
 delete mode 100644 xs/xsp/PlaceholderParser.xsp
 delete mode 100644 xs/xsp/PolylineCollection.xsp

diff --git a/lib/Slic3r.pm b/lib/Slic3r.pm
index d9274bea9..cd8f151f8 100644
--- a/lib/Slic3r.pm
+++ b/lib/Slic3r.pm
@@ -28,11 +28,6 @@ BEGIN {
 
 use FindBin;
 
-# Let the XS module know where the GUI resources reside.
-set_resources_dir(decode_path($FindBin::Bin) . (($^O eq 'darwin') ? '/../Resources' : '/resources'));
-set_var_dir(resources_dir() . "/icons");
-set_local_dir(resources_dir() . "/localization/");
-
 use Moo 1.003001;
 
 use Slic3r::XS;   # import all symbols (constants etc.) before they get parsed
@@ -60,82 +55,4 @@ use constant SCALING_FACTOR         => 0.000001;
 $Slic3r::loglevel = (defined($ENV{'SLIC3R_LOGLEVEL'}) && $ENV{'SLIC3R_LOGLEVEL'} =~ /^[1-9]/) ? $ENV{'SLIC3R_LOGLEVEL'} : 0;
 set_logging_level($Slic3r::loglevel);
 
-# Let the palceholder parser evaluate one expression to initialize its local static macro_processor 
-# class instance in a thread safe manner.
-Slic3r::GCode::PlaceholderParser->new->evaluate_boolean_expression('1==1');
-
-# Open a file by converting $filename to local file system locales.
-sub open {
-    my ($fh, $mode, $filename) = @_;
-    return CORE::open $$fh, $mode, encode_path($filename);
-}
-
-sub tags {
-    my ($format) = @_;
-    $format //= '';
-    my %tags;
-    # End of line
-    $tags{eol}     = ($format eq 'html') ? '<br>'   : "\n";
-    # Heading
-    $tags{h2start} = ($format eq 'html') ? '<b>'   : '';
-    $tags{h2end}   = ($format eq 'html') ? '</b>'  : '';
-    # Bold font
-    $tags{bstart}  = ($format eq 'html') ? '<b>'    : '';
-    $tags{bend}    = ($format eq 'html') ? '</b>'   : '';
-    # Verbatim
-    $tags{vstart}  = ($format eq 'html') ? '<pre>'  : '';
-    $tags{vend}    = ($format eq 'html') ? '</pre>' : '';
-    return %tags;
-}
-
-sub slic3r_info
-{
-    my (%params) = @_;
-    my %tag = Slic3r::tags($params{format});
-    my $out = '';
-    $out .= "$tag{bstart}$Slic3r::FORK_NAME$tag{bend}$tag{eol}";
-    $out .= "$tag{bstart}Version: $tag{bend}$Slic3r::VERSION$tag{eol}";
-    $out .= "$tag{bstart}Build:   $tag{bend}$Slic3r::BUILD$tag{eol}";
-    return $out;
-}
-
-sub copyright_info
-{
-    my (%params) = @_;
-    my %tag = Slic3r::tags($params{format});
-    my $out =
-        'Copyright &copy; 2016 Vojtech Bubnik, Prusa Research. <br />' .
-        'Copyright &copy; 2011-2016 Alessandro Ranellucci. <br />' .
-        '<a href="http://slic3r.org/">Slic3r</a> is licensed under the ' .
-        '<a href="http://www.gnu.org/licenses/agpl-3.0.html">GNU Affero General Public License, version 3</a>.' .
-        '<br /><br /><br />' .
-        'Contributions by Henrik Brix Andersen, Nicolas Dandrimont, Mark Hindess, Petr Ledvina, Y. Sapir, Mike Sheldrake and numerous others. ' .
-        'Manual by Gary Hodgson. Inspired by the RepRap community. <br />' .
-        'Slic3r logo designed by Corey Daniels, <a href="http://www.famfamfam.com/lab/icons/silk/">Silk Icon Set</a> designed by Mark James. ';
-    return $out;
-}
-
-sub system_info
-{
-    my (%params) = @_;
-    my %tag = Slic3r::tags($params{format});
-
-    my $out = '';
-    $out .= "$tag{bstart}Operating System:    $tag{bend}$Config{osname}$tag{eol}";
-    $out .= "$tag{bstart}System Architecture: $tag{bend}$Config{archname}$tag{eol}";        
-    if ($^O eq 'MSWin32') {
-        $out .= "$tag{bstart}Windows Version: $tag{bend}" . `ver` . $tag{eol};
-    } else {
-        # Hopefully some kind of unix / linux.
-        $out .= "$tag{bstart}System Version: $tag{bend}" . `uname -a` . $tag{eol};
-    }
-    $out .= $tag{vstart} . Config::myconfig . $tag{vend};
-    $out .= "  $tag{bstart}\@INC:$tag{bend}$tag{eol}$tag{vstart}";
-    foreach my $i (@INC) {
-        $out .= "    $i\n";
-    }
-    $out .= "$tag{vend}";
-    return $out;
-}
-
 1;
diff --git a/lib/Slic3r/Config.pm b/lib/Slic3r/Config.pm
index a9c822b96..aeaca998f 100644
--- a/lib/Slic3r/Config.pm
+++ b/lib/Slic3r/Config.pm
@@ -23,47 +23,6 @@ our $Options = print_config_def();
     }
 }
 
-# From command line parameters, used by slic3r.pl
-sub new_from_cli {
-    my $class = shift;
-    my %args = @_;
-    
-    # Delete hash keys with undefined value.
-    delete $args{$_} for grep !defined $args{$_}, keys %args;
-    
-    # Replace the start_gcode, end_gcode ... hash values
-    # with the content of the files they reference.
-    for (qw(start end layer toolchange)) {
-        my $opt_key = "${_}_gcode";
-        if ($args{$opt_key}) {
-            if (-e $args{$opt_key}) {
-                Slic3r::open(\my $fh, "<", $args{$opt_key})
-                    or die "Failed to open $args{$opt_key}\n";
-                binmode $fh, ':utf8';
-                $args{$opt_key} = do { local $/; <$fh> };
-                close $fh;
-            }
-        }
-    }
-
-    my $self = $class->new;
-    foreach my $opt_key (keys %args) {
-        my $opt_def = $Options->{$opt_key};
-        
-        # we use set_deserialize() for bool options since GetOpt::Long doesn't handle 
-        # arrays of boolean values
-        if ($opt_key =~ /^(?:bed_shape|duplicate_grid|extruder_offset)$/ || $opt_def->{type} eq 'bool') {
-            $self->set_deserialize($opt_key, $args{$opt_key});
-        } elsif (my $shortcut = $opt_def->{shortcut}) {
-            $self->set($_, $args{$opt_key}) for @$shortcut;
-        } else {
-            $self->set($opt_key, $args{$opt_key});
-        }
-    }
-    
-    return $self;
-}
-
 package Slic3r::Config::Static;
 use parent 'Slic3r::Config';
 
diff --git a/lib/Slic3r/ExPolygon.pm b/lib/Slic3r/ExPolygon.pm
index 4ab1bc8ab..6090a073b 100644
--- a/lib/Slic3r/ExPolygon.pm
+++ b/lib/Slic3r/ExPolygon.pm
@@ -4,13 +4,6 @@ use warnings;
 
 # an ExPolygon is a polygon with holes
 
-sub noncollapsing_offset_ex {
-    my $self = shift;
-    my ($distance, @params) = @_;
-    
-    return $self->offset_ex($distance + 1, @params);
-}
-
 sub bounding_box {
     my $self = shift;
     return $self->contour->bounding_box;
diff --git a/lib/Slic3r/Geometry/Clipper.pm b/lib/Slic3r/Geometry/Clipper.pm
deleted file mode 100644
index c1fa81d9f..000000000
--- a/lib/Slic3r/Geometry/Clipper.pm
+++ /dev/null
@@ -1,13 +0,0 @@
-package Slic3r::Geometry::Clipper;
-use strict;
-use warnings;
-
-require Exporter;
-our @ISA = qw(Exporter);
-our @EXPORT_OK = qw(
-	offset 
-	offset2_ex
-    diff_ex diff union_ex 
-    union);
-
-1;
diff --git a/lib/Slic3r/Layer.pm b/lib/Slic3r/Layer.pm
index 4bff9b61f..7f7b589d0 100644
--- a/lib/Slic3r/Layer.pm
+++ b/lib/Slic3r/Layer.pm
@@ -31,7 +31,4 @@ sub regions {
     return [ map $self->get_region($_), 0..($self->region_count-1) ];
 }
 
-package Slic3r::Layer::Support;
-our @ISA = qw(Slic3r::Layer);
-
 1;
diff --git a/lib/Slic3r/Line.pm b/lib/Slic3r/Line.pm
index c06f9a1fa..16b609c95 100644
--- a/lib/Slic3r/Line.pm
+++ b/lib/Slic3r/Line.pm
@@ -5,9 +5,4 @@ use warnings;
 # a line is a two-points line
 use parent 'Slic3r::Polyline';
 
-sub grow {
-    my $self = shift;
-    return Slic3r::Polyline->new(@$self)->grow(@_);
-}
-
 1;
diff --git a/lib/Slic3r/Model.pm b/lib/Slic3r/Model.pm
index ec31f6c8a..d44e9878d 100644
--- a/lib/Slic3r/Model.pm
+++ b/lib/Slic3r/Model.pm
@@ -133,11 +133,4 @@ sub add_instance {
     }
 }
 
-sub mesh_stats {
-    my $self = shift;
-    
-    # TODO: sum values from all volumes
-    return $self->volumes->[0]->mesh->stats;
-}
-
 1;
diff --git a/lib/Slic3r/Polygon.pm b/lib/Slic3r/Polygon.pm
index 16222141a..d5445047d 100644
--- a/lib/Slic3r/Polygon.pm
+++ b/lib/Slic3r/Polygon.pm
@@ -5,9 +5,4 @@ use warnings;
 # a polygon is a closed polyline.
 use parent 'Slic3r::Polyline';
 
-sub grow {
-    my $self = shift;
-    return $self->split_at_first_point->grow(@_);
-}
-
-1;
\ No newline at end of file
+1;
diff --git a/lib/Slic3r/Print/Object.pm b/lib/Slic3r/Print/Object.pm
index 64480e08f..35da7afe7 100644
--- a/lib/Slic3r/Print/Object.pm
+++ b/lib/Slic3r/Print/Object.pm
@@ -5,7 +5,6 @@ use warnings;
 
 use List::Util qw(min max sum first);
 use Slic3r::Flow ':roles';
-use Slic3r::Print::State ':steps';
 use Slic3r::Surface ':types';
 
 sub layers {
@@ -13,9 +12,4 @@ sub layers {
     return [ map $self->get_layer($_), 0..($self->layer_count - 1) ];
 }
 
-sub support_layers {
-    my $self = shift;
-    return [ map $self->get_support_layer($_), 0..($self->support_layer_count - 1) ];
-}
-
 1;
diff --git a/lib/Slic3r/Print/State.pm b/lib/Slic3r/Print/State.pm
deleted file mode 100644
index 17c614f1b..000000000
--- a/lib/Slic3r/Print/State.pm
+++ /dev/null
@@ -1,12 +0,0 @@
-# Wraps C++ enums Slic3r::PrintStep and Slic3r::PrintObjectStep
-package Slic3r::Print::State;
-use strict;
-use warnings;
-
-require Exporter;
-our @ISA = qw(Exporter);
-our @EXPORT_OK   = qw(STEP_SLICE STEP_PERIMETERS STEP_PREPARE_INFILL 
-                    STEP_INFILL STEP_SUPPORTMATERIAL STEP_SKIRT STEP_BRIM STEP_WIPE_TOWER);
-our %EXPORT_TAGS = (steps => \@EXPORT_OK);
-
-1;
diff --git a/src/libslic3r/GCodeReader.cpp b/src/libslic3r/GCodeReader.cpp
index c8a9b790f..44b0b89f6 100644
--- a/src/libslic3r/GCodeReader.cpp
+++ b/src/libslic3r/GCodeReader.cpp
@@ -215,9 +215,9 @@ bool GCodeReader::parse_file_raw(const std::string &filename, raw_line_callback_
         [](size_t){});
 }
 
-bool GCodeReader::GCodeLine::has(char axis) const
+const char* GCodeReader::axis_pos(const char *raw_str, char axis)
 {
-    const char *c = m_raw.c_str();
+    const char *c = raw_str;
     // Skip the whitespaces.
     c = skip_whitespaces(c);
     // Skip the command.
@@ -230,40 +230,48 @@ bool GCodeReader::GCodeLine::has(char axis) const
             break;
         // Check the name of the axis.
         if (*c == axis)
-            return true;
+            return c;
         // Skip the rest of the word.
         c = skip_word(c);
     }
-    return false;
+    return nullptr;
+}
+
+bool GCodeReader::GCodeLine::has(char axis) const
+{
+    const char *c = axis_pos(m_raw.c_str(), axis);
+    return c != nullptr;
 }
 
 bool GCodeReader::GCodeLine::has_value(char axis, float &value) const
 {
     assert(is_decimal_separator_point());
-    const char *c = m_raw.c_str();
-    // Skip the whitespaces.
-    c = skip_whitespaces(c);
-    // Skip the command.
-    c = skip_word(c);
-    // Up to the end of line or comment.
-    while (! is_end_of_gcode_line(*c)) {
-        // Skip whitespaces.
-        c = skip_whitespaces(c);
-        if (is_end_of_gcode_line(*c))
-            break;
-        // Check the name of the axis.
-        if (*c == axis) {
-            // Try to parse the numeric value.
-            char   *pend = nullptr;
-            double  v = strtod(++ c, &pend);
-            if (pend != nullptr && is_end_of_word(*pend)) {
-                // The axis value has been parsed correctly.
-                value = float(v);
-                return true;
-            }
-        }
-        // Skip the rest of the word.
-        c = skip_word(c);
+    const char *c = axis_pos(m_raw.c_str(), axis);
+    if (c == nullptr)
+        return false;
+    // Try to parse the numeric value.
+    char   *pend = nullptr;
+    double  v = strtod(++ c, &pend);
+    if (pend != nullptr && is_end_of_word(*pend)) {
+        // The axis value has been parsed correctly.
+        value = float(v);
+        return true;
+    }
+    return false;
+}
+
+bool GCodeReader::GCodeLine::has_value(char axis, int &value) const
+{
+    const char *c = axis_pos(m_raw.c_str(), axis);
+    if (c == nullptr)
+        return false;
+    // Try to parse the numeric value.
+    char   *pend = nullptr;
+    long    v = strtol(++ c, &pend, 10);
+    if (pend != nullptr && is_end_of_word(*pend)) {
+        // The axis value has been parsed correctly.
+        value = int(v);
+        return true;
     }
     return false;
 }
diff --git a/src/libslic3r/GCodeReader.hpp b/src/libslic3r/GCodeReader.hpp
index 40a099229..25ba6ee0b 100644
--- a/src/libslic3r/GCodeReader.hpp
+++ b/src/libslic3r/GCodeReader.hpp
@@ -30,6 +30,7 @@ public:
         float value(Axis axis) const { return m_axis[axis]; }
         bool  has(char axis) const;
         bool  has_value(char axis, float &value) const;
+        bool  has_value(char axis, int &value) const;
         float new_X(const GCodeReader &reader) const { return this->has(X) ? this->x() : reader.x(); }
         float new_Y(const GCodeReader &reader) const { return this->has(Y) ? this->y() : reader.y(); }
         float new_Z(const GCodeReader &reader) const { return this->has(Z) ? this->z() : reader.z(); }
@@ -166,6 +167,7 @@ private:
             ; // silence -Wempty-body
         return c;
     }
+    static const char*  axis_pos(const char *raw_str, char axis);
 
     GCodeConfig m_config;
     char        m_extrusion_axis;
diff --git a/src/libslic3r/Polyline.cpp b/src/libslic3r/Polyline.cpp
index 1255d5473..43c5afe73 100644
--- a/src/libslic3r/Polyline.cpp
+++ b/src/libslic3r/Polyline.cpp
@@ -196,7 +196,7 @@ BoundingBox get_extents(const Polylines &polylines)
 const Point& leftmost_point(const Polylines &polylines)
 {
     if (polylines.empty())
-        throw Slic3r::InvalidArgument("leftmost_point() called on empty PolylineCollection");
+        throw Slic3r::InvalidArgument("leftmost_point() called on empty Polylines");
     Polylines::const_iterator it = polylines.begin();
     const Point *p = &it->leftmost_point();
     for (++ it; it != polylines.end(); ++it) {
diff --git a/src/libslic3r/Polyline.hpp b/src/libslic3r/Polyline.hpp
index 256dca28c..e0379e869 100644
--- a/src/libslic3r/Polyline.hpp
+++ b/src/libslic3r/Polyline.hpp
@@ -80,15 +80,6 @@ public:
 inline bool operator==(const Polyline &lhs, const Polyline &rhs) { return lhs.points == rhs.points; }
 inline bool operator!=(const Polyline &lhs, const Polyline &rhs) { return lhs.points != rhs.points; }
 
-// Don't use this class in production code, it is used exclusively by the Perl binding for unit tests!
-#ifdef PERL_UCHAR_MIN
-class PolylineCollection
-{
-public:
-    Polylines polylines;
-};
-#endif /* PERL_UCHAR_MIN */
-
 extern BoundingBox get_extents(const Polyline &polyline);
 extern BoundingBox get_extents(const Polylines &polylines);
 
diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp
index 950816fa9..be2e2dbef 100644
--- a/src/libslic3r/PrintConfig.hpp
+++ b/src/libslic3r/PrintConfig.hpp
@@ -211,6 +211,16 @@ public:
     DynamicPrintConfig& operator=(DynamicPrintConfig &&rhs) noexcept { DynamicConfig::operator=(std::move(rhs)); return *this; }
 
     static DynamicPrintConfig  full_print_config();
+    static DynamicPrintConfig  full_print_config_with(const t_config_option_key &opt_key, const std::string &str, bool append = false) {
+        auto config = DynamicPrintConfig::full_print_config();
+        config.set_deserialize_strict(opt_key, str, append);
+        return config;
+    }
+    static DynamicPrintConfig  full_print_config_with(std::initializer_list<SetDeserializeItem> items) {
+        auto config = DynamicPrintConfig::full_print_config();
+        config.set_deserialize_strict(items);
+        return config;
+    }
     static DynamicPrintConfig* new_from_defaults_keys(const std::vector<std::string> &keys);
 
     // Overrides ConfigBase::def(). Static configuration definition. Any value stored into this ConfigBase shall have its definition here.
diff --git a/src/libslic3r/Utils.hpp b/src/libslic3r/Utils.hpp
index 10790ef49..6a09e7fbf 100644
--- a/src/libslic3r/Utils.hpp
+++ b/src/libslic3r/Utils.hpp
@@ -18,7 +18,6 @@ namespace Slic3r {
 
 extern void set_logging_level(unsigned int level);
 extern unsigned get_logging_level();
-extern void trace(unsigned int level, const char *message);
 // Format memory allocated, separate thousands by comma.
 extern std::string format_memsize_MB(size_t n);
 // Return string to be added to the boost::log output to inform about the current process memory allocation.
@@ -68,13 +67,6 @@ std::string debug_out_path(const char *name, ...);
 // This type is only needed for Perl bindings to relay to Perl that the string is raw, not UTF-8 encoded.
 typedef std::string local_encoded_string;
 
-// Convert an UTF-8 encoded string into local coding.
-// On Windows, the UTF-8 string is converted to a local 8-bit code page.
-// On OSX and Linux, this function does no conversion and returns a copy of the source string.
-extern local_encoded_string encode_path(const char *src);
-extern std::string decode_path(const char *src);
-extern std::string normalize_utf8_nfc(const char *src);
-
 // Returns next utf8 sequence length. =number of bytes in string, that creates together one utf-8 character. 
 // Starting at pos. ASCII characters returns 1. Works also if pos is in the middle of the sequence.
 extern size_t get_utf8_sequence_length(const std::string& text, size_t pos = 0);
@@ -115,19 +107,6 @@ extern bool is_gallery_file(const boost::filesystem::directory_entry& path, char
 extern bool is_gallery_file(const std::string& path, char const* type);
 extern bool is_shapes_dir(const std::string& dir);
 
-// File path / name / extension splitting utilities, working with UTF-8,
-// to be published to Perl.
-namespace PerlUtils {
-    // Get a file name including the extension.
-    extern std::string path_to_filename(const char *src);
-    // Get a file name without the extension.
-    extern std::string path_to_stem(const char *src);
-    // Get just the extension.
-    extern std::string path_to_extension(const char *src);
-    // Get a directory without the trailing slash.
-    extern std::string path_to_parent_path(const char *src);
-};
-
 std::string string_printf(const char *format, ...);
 
 // Standard "generated by Slic3r version xxx timestamp xxx" header string, 
diff --git a/src/libslic3r/utils.cpp b/src/libslic3r/utils.cpp
index ddc9a4081..371b8eb11 100644
--- a/src/libslic3r/utils.cpp
+++ b/src/libslic3r/utils.cpp
@@ -125,14 +125,6 @@ static struct RunOnInit {
     }
 } g_RunOnInit;
 
-void trace(unsigned int level, const char *message)
-{
-    boost::log::trivial::severity_level severity = level_to_boost(level);
-
-    BOOST_LOG_STREAM_WITH_PARAMS(::boost::log::trivial::logger::get(),\
-        (::boost::log::keywords::severity = severity)) << message;
-}
-
 void disable_multi_threading()
 {
     // Disable parallelization so the Shiny profiler works
@@ -820,49 +812,6 @@ bool is_shapes_dir(const std::string& dir)
 
 namespace Slic3r {
 
-// Encode an UTF-8 string to the local code page.
-std::string encode_path(const char *src)
-{    
-#ifdef WIN32
-    // Convert the source utf8 encoded string to a wide string.
-    std::wstring wstr_src = boost::nowide::widen(src);
-    if (wstr_src.length() == 0)
-        return std::string();
-    // Convert a wide string to a local code page.
-    int size_needed = ::WideCharToMultiByte(0, 0, wstr_src.data(), (int)wstr_src.size(), nullptr, 0, nullptr, nullptr);
-    std::string str_dst(size_needed, 0);
-    ::WideCharToMultiByte(0, 0, wstr_src.data(), (int)wstr_src.size(), str_dst.data(), size_needed, nullptr, nullptr);
-    return str_dst;
-#else /* WIN32 */
-    return src;
-#endif /* WIN32 */
-}
-
-// Encode an 8-bit string from a local code page to UTF-8.
-// Multibyte to utf8
-std::string decode_path(const char *src)
-{  
-#ifdef WIN32
-    int len = int(strlen(src));
-    if (len == 0)
-        return std::string();
-    // Convert the string encoded using the local code page to a wide string.
-    int size_needed = ::MultiByteToWideChar(0, 0, src, len, nullptr, 0);
-    std::wstring wstr_dst(size_needed, 0);
-    ::MultiByteToWideChar(0, 0, src, len, wstr_dst.data(), size_needed);
-    // Convert a wide string to utf8.
-    return boost::nowide::narrow(wstr_dst.c_str());
-#else /* WIN32 */
-    return src;
-#endif /* WIN32 */
-}
-
-std::string normalize_utf8_nfc(const char *src)
-{
-    static std::locale locale_utf8(boost::locale::generator().generate(""));
-    return boost::locale::normalize(src, boost::locale::norm_nfc, locale_utf8);
-}
-
 size_t get_utf8_sequence_length(const std::string& text, size_t pos)
 {
 	assert(pos < text.size());
@@ -933,18 +882,6 @@ size_t get_utf8_sequence_length(const char *seq, size_t size)
 	return length;
 }
 
-namespace PerlUtils {
-    // Get a file name including the extension.
-    std::string path_to_filename(const char *src)       { return boost::filesystem::path(src).filename().string(); }
-    // Get a file name without the extension.
-    std::string path_to_stem(const char *src)           { return boost::filesystem::path(src).stem().string(); }
-    // Get just the extension.
-    std::string path_to_extension(const char *src)      { return boost::filesystem::path(src).extension().string(); }
-    // Get a directory without the trailing slash.
-    std::string path_to_parent_path(const char *src)    { return boost::filesystem::path(src).parent_path().string(); }
-};
-
-
 std::string string_printf(const char *format, ...)
 {
     va_list args1;
diff --git a/t/dynamic.t b/t/dynamic.t
deleted file mode 100644
index 5d4d3ceb4..000000000
--- a/t/dynamic.t
+++ /dev/null
@@ -1,93 +0,0 @@
-use Test::More;
-use strict;
-use warnings;
-
-plan skip_all => 'variable-width paths are currently disabled';
-plan tests => 20;
-
-BEGIN {
-    use FindBin;
-    use lib "$FindBin::Bin/../lib";
-    use local::lib "$FindBin::Bin/../local-lib";
-}
-
-use List::Util qw(first);
-use Slic3r;
-use Slic3r::Geometry qw(X Y scale epsilon);
-use Slic3r::Surface ':types';
-
-sub scale_points (@) { map [scale $_->[X], scale $_->[Y]], @_ }
-
-{
-    my $square = Slic3r::ExPolygon->new([
-        scale_points [0,0], [10,0], [10,10], [0,10],
-    ]);
-    
-    my @offsets = @{$square->noncollapsing_offset_ex(- scale 5)};
-    is scalar @offsets, 1, 'non-collapsing offset';
-}
-
-{
-    local $Slic3r::Config = Slic3r::Config->new(
-        perimeters      => 3,
-    );
-    my $w = 0.7;
-    my $perimeter_flow = Slic3r::Flow->new(
-        nozzle_diameter => 0.5,
-        layer_height    => 0.4,
-        width           => $w,
-    );
-    
-    my $print = Slic3r::Print->new;
-    my $region = Slic3r::Print::Region->new(
-        print => $print,
-        flows => { perimeter => $perimeter_flow },
-    );
-    push @{$print->regions}, $region;
-    my $object = Slic3r::Print::Object->new(
-        print => $print,
-        size  => [1,1],
-    );
-    my $make_layer = sub {
-        my ($width) = @_;
-        my $layer = Slic3r::Layer->new(
-            object => $object,
-            id => 1,
-            slices => [
-                Slic3r::Surface->new(
-                    surface_type    => S_TYPE_INTERNAL,
-                    expolygon       => Slic3r::ExPolygon->new([ scale_points [0,0], [50,0], [50,$width], [0,$width] ]),
-                ),
-            ],
-            thin_walls => [],
-        );
-        my $layerm = $layer->region(0);
-        $layer->make_perimeters;
-        return $layerm;
-    };
-    
-    my %widths = (
-        1   * $w => { perimeters => 1, gaps => 0 },
-        1.3 * $w => { perimeters => 1, gaps => 1, gap_flow_spacing => $perimeter_flow->clone(width => 0.2 * $w)->spacing },
-        1.5 * $w => { perimeters => 1, gaps => 1, gap_flow_spacing => $perimeter_flow->clone(width => 0.5 * $w)->spacing },
-        2   * $w => { perimeters => 1, gaps => 1, gap_flow_spacing => $perimeter_flow->spacing },
-        2.5 * $w => { perimeters => 1, gaps => 1, gap_flow_spacing => $perimeter_flow->clone(width => 1.5 * $w)->spacing },
-        3   * $w => { perimeters => 2, gaps => 0 },
-        4   * $w => { perimeters => 2, gaps => 1, gap_flow_spacing => $perimeter_flow->spacing },
-    );
-    
-    foreach my $width (sort keys %widths) {
-        my $layerm = $make_layer->($width);
-        is scalar @{$layerm->perimeters}, $widths{$width}{perimeters}, 'right number of perimeters';
-        is scalar @{$layerm->thin_fills} ? 1 : 0, $widths{$width}{gaps},
-            ($widths{$width}{gaps} ? 'gaps were filled' : 'no gaps detected');  # TODO: we should check the exact number of gaps, but we need a better medial axis algorithm
-        
-        my @gaps = map $_, @{$layerm->thin_fills};
-        if (@gaps) {
-            ok +(!first { abs($_->flow_spacing - $widths{$width}{gap_flow_spacing}) > epsilon } @gaps),
-                'flow spacing was dynamically adjusted';
-        }
-    }
-}
-
-__END__
diff --git a/t/geometry.t b/t/geometry.t
index 874dab987..12a1ca743 100644
--- a/t/geometry.t
+++ b/t/geometry.t
@@ -2,7 +2,7 @@ use Test::More;
 use strict;
 use warnings;
 
-plan tests => 27;
+plan tests => 26;
 
 BEGIN {
     use FindBin;
@@ -125,13 +125,6 @@ my $polygons = [
 
 #==========================================================
 
-{
-    my $line = Slic3r::Line->new([10,10], [20,10]);
-    is $line->grow(5)->[0]->area, Slic3r::Polygon->new([10,5], [20,5], [20,15], [10,15])->area, 'grow line';
-}
-
-#==========================================================
-
 {
     # if chained_path() works correctly, these points should be joined with no diagonal paths
     # (thus 26 units long)
diff --git a/t/multi.t b/t/multi.t
deleted file mode 100644
index e74a7a1a8..000000000
--- a/t/multi.t
+++ /dev/null
@@ -1,221 +0,0 @@
-use Test::More tests => 13;
-use strict;
-use warnings;
-
-BEGIN {
-    use FindBin;
-    use lib "$FindBin::Bin/../lib";
-    use local::lib "$FindBin::Bin/../local-lib";
-}
-
-use List::Util qw(first);
-use Slic3r;
-use Slic3r::Geometry qw(scale convex_hull);
-use Slic3r::Geometry::Clipper qw(offset);
-use Slic3r::Test;
-
-{
-    my $config = Slic3r::Config::new_from_defaults;
-    $config->set('nozzle_diameter', [0.6,0.6,0.6,0.6]);
-    $config->set('raft_layers', 2);
-    $config->set('infill_extruder', 2);
-    $config->set('solid_infill_extruder', 3);
-    $config->set('support_material_extruder', 4);
-    $config->set('ooze_prevention', 1);
-    $config->set('extruder_offset', [ [0,0], [20,0], [0,20], [20,20] ]);
-    $config->set('temperature', [200, 180, 170, 160]);
-    $config->set('first_layer_temperature', [206, 186, 166, 156]);
-    $config->set('toolchange_gcode', 'T[next_extruder] ;toolchange');  # test that it doesn't crash when this is supplied
-    # Since July 2019, PrusaSlicer only emits automatic Tn command in case that the toolchange_gcode is empty
-    # The "T[next_extruder]" is therefore needed in this test.
-    
-    my $print = Slic3r::Test::init_print('20mm_cube', config => $config);
-    
-    my $tool = undef;
-    my @tool_temp = (0,0,0,0);
-    my @toolchange_points = ();
-    my @extrusion_points = ();
-    Slic3r::GCode::Reader->new->parse(Slic3r::Test::gcode($print), sub {
-        my ($self, $cmd, $args, $info) = @_;
-        
-        if ($cmd =~ /^T(\d+)/) {
-            # ignore initial toolchange
-            if (defined $tool) {
-                my $expected_temp = $self->Z == ($config->get_value('first_layer_height') + $config->z_offset)
-                    ? $config->first_layer_temperature->[$tool]
-                    : $config->temperature->[$tool];
-                die 'standby temperature was not set before toolchange'
-                    if $tool_temp[$tool] != $expected_temp + $config->standby_temperature_delta;
-                
-                push @toolchange_points, my $point = Slic3r::Point->new_scale($self->X, $self->Y);
-            }
-            $tool = $1;
-        } elsif ($cmd eq 'M104' || $cmd eq 'M109') {
-            my $t = $args->{T} // $tool;
-            if ($tool_temp[$t] == 0) {
-                fail 'initial temperature is not equal to first layer temperature + standby delta'
-                    unless $args->{S} == $config->first_layer_temperature->[$t] + $config->standby_temperature_delta;
-            }
-            $tool_temp[$t] = $args->{S};
-        } elsif ($cmd eq 'G1' && $info->{extruding} && $info->{dist_XY} > 0) {
-            push @extrusion_points, my $point = Slic3r::Point->new_scale($args->{X}, $args->{Y});
-            $point->translate(map +scale($_), @{ $config->extruder_offset->[$tool] });
-        }
-    });
-    my $convex_hull = convex_hull(\@extrusion_points);
-    
-    my @t = ();
-    foreach my $point (@toolchange_points) {
-        foreach my $offset (@{$config->extruder_offset}) {
-            push @t, my $p = $point->clone;
-            $p->translate(map +scale($_), @$offset);
-        }
-    }
-    ok !(defined first { $convex_hull->contains_point($_) } @t), 'all nozzles are outside skirt at toolchange';
-    
-    if (0) {
-        require "Slic3r/SVG.pm";
-        Slic3r::SVG::output(
-            "ooze_prevention_test.svg",
-            no_arrows   => 1,
-            polygons    => [$convex_hull],
-            red_points  => \@t,
-            points      => \@toolchange_points,
-        );
-    }
-    
-    # offset the skirt by the maximum displacement between extruders plus a safety extra margin
-    my $delta = scale(20 * sqrt(2) + 1);
-    my $outer_convex_hull = offset([$convex_hull], +$delta)->[0];
-    ok !(defined first { !$outer_convex_hull->contains_point($_) } @toolchange_points), 'all toolchanges happen within expected area';
-}
-
-{
-    my $config = Slic3r::Config::new_from_defaults;
-    $config->set('nozzle_diameter', [0.6,0.6,0.6,0.6]);
-    $config->set('support_material_extruder', 3);
-    
-    my $print = Slic3r::Test::init_print('20mm_cube', config => $config);
-    ok Slic3r::Test::gcode($print), 'no errors when using non-consecutive extruders';
-}
-
-{
-    my $config = Slic3r::Config->new;
-    $config->set('nozzle_diameter', [0.6,0.6,0.6,0.6]);
-    $config->set('extruder', 2);
-    
-    my $print = Slic3r::Test::init_print('20mm_cube', config => $config);
-    like Slic3r::Test::gcode($print), qr/ T1/, 'extruder shortcut';
-}
-
-{
-    my $config = Slic3r::Config->new;
-    $config->set('nozzle_diameter', [0.6,0.6,0.6,0.6]);
-    $config->set('perimeter_extruder', 2);
-    $config->set('infill_extruder', 2);
-    $config->set('support_material_extruder', 2);
-    $config->set('support_material_interface_extruder', 2);
-    
-    my $print = Slic3r::Test::init_print('20mm_cube', config => $config);
-    ok Slic3r::Test::gcode($print), 'no errors when using multiple skirts with a single, non-zero, extruder';
-}
-
-{
-    my $model = stacked_cubes();
-    my $lower_config = $model->get_material('lower')->config;
-    my $upper_config = $model->get_material('upper')->config;
-    
-    $lower_config->set('extruder', 1);
-    $lower_config->set('bottom_solid_layers', 0);
-    $lower_config->set('top_solid_layers', 1);
-    $upper_config->set('extruder', 2);
-    $upper_config->set('bottom_solid_layers', 1);
-    $upper_config->set('top_solid_layers', 0);
-    my $config = Slic3r::Config::new_from_defaults;
-    $config->set('nozzle_diameter', [0.6,0.6,0.6,0.6]);
-    $config->set('fill_density', 0);
-    $config->set('solid_infill_speed', 99);
-    $config->set('top_solid_infill_speed', 99);
-    $config->set('cooling', [ 0 ]);             # for preventing speeds from being altered
-    $config->set('first_layer_speed', '100%');  # for preventing speeds from being altered
-    
-    my $test = sub {
-        my $print = Slic3r::Test::init_print($model, config => $config);
-        my $tool = undef;
-        my %T0_shells = my %T1_shells = ();  # Z => 1
-        Slic3r::GCode::Reader->new->parse(my $gcode = Slic3r::Test::gcode($print), sub {
-            my ($self, $cmd, $args, $info) = @_;
-        
-            if ($cmd =~ /^T(\d+)/) {
-                $tool = $1;
-            } elsif ($cmd eq 'G1' && $info->{extruding} && $info->{dist_XY} > 0) {
-                if (($args->{F} // $self->F) == $config->solid_infill_speed*60) {
-                    if ($tool == 0) {
-                        $T0_shells{$self->Z} = 1;
-                    } elsif ($tool == 1) {
-                        $T1_shells{$self->Z} = 1;
-                    }
-                }
-            }
-        });
-        return [ sort keys %T0_shells ], [ sort keys %T1_shells ];
-    };
-    
-    {
-        my ($t0, $t1) = $test->();
-        is scalar(@$t0), 0, 'no interface shells';
-        is scalar(@$t1), 0, 'no interface shells';
-    }
-    {
-        $config->set('interface_shells', 1);
-        my ($t0, $t1) = $test->();
-        is scalar(@$t0), $lower_config->top_solid_layers,    'top interface shells';
-        is scalar(@$t1), $upper_config->bottom_solid_layers, 'bottom interface shells';
-    }
-}
-
-{
-    my $model = stacked_cubes();
-    my $object = $model->objects->[0];
-    
-    my $config = Slic3r::Config::new_from_defaults;
-    $config->set('nozzle_diameter', [0.6,0.6,0.6,0.6]);
-    $config->set('layer_height', 0.4);
-    $config->set('first_layer_height', $config->layer_height);
-    $config->set('skirts', 0);
-    my $print = Slic3r::Test::init_print($model, config => $config);
-    
-    is $object->volumes->[0]->config->extruder, 1, 'auto_assign_extruders() assigned correct extruder to first volume';
-    is $object->volumes->[1]->config->extruder, 2, 'auto_assign_extruders() assigned correct extruder to second volume';
-    
-    my $tool = undef;
-    my %T0 = my %T1 = ();  # Z => 1
-    Slic3r::GCode::Reader->new->parse(my $gcode = Slic3r::Test::gcode($print), sub {
-        my ($self, $cmd, $args, $info) = @_;
-    
-        if ($cmd =~ /^T(\d+)/) {
-            $tool = $1;
-        } elsif ($cmd eq 'G1' && $info->{extruding} && $info->{dist_XY} > 0) {
-            if ($tool == 0) {
-                $T0{$self->Z} = 1;
-            } elsif ($tool == 1) {
-                $T1{$self->Z} = 1;
-            }
-        }
-    });
-    
-    ok !(defined first { $_ > 20 } keys %T0), 'T0 is never used for upper object';
-    ok !(defined first { $_ < 20 } keys %T1), 'T1 is never used for lower object';
-}
-
-sub stacked_cubes {
-    my $model = Slic3r::Model->new;
-    my $object = $model->add_object;
-    $object->add_volume(mesh => Slic3r::Test::mesh('20mm_cube'), material_id => 'lower');
-    $object->add_volume(mesh => Slic3r::Test::mesh('20mm_cube', translate => [0,0,20]), material_id => 'upper');
-    $object->add_instance(offset => Slic3r::Pointf->new(0,0));
-    
-    return $model;
-}
-
-__END__
diff --git a/t/perimeters.t b/t/perimeters.t
deleted file mode 100644
index c4aef6e7e..000000000
--- a/t/perimeters.t
+++ /dev/null
@@ -1,444 +0,0 @@
-use Test::More tests => 59;
-use strict;
-use warnings;
-
-BEGIN {
-    use FindBin;
-    use lib "$FindBin::Bin/../lib";
-    use local::lib "$FindBin::Bin/../local-lib";
-}
-
-use Slic3r::ExtrusionLoop ':roles';
-use Slic3r::ExtrusionPath ':roles';
-use List::Util qw(first);
-use Slic3r;
-use Slic3r::Flow ':roles';
-use Slic3r::Geometry qw(PI scale unscale);
-use Slic3r::Geometry::Clipper qw(union_ex diff);
-use Slic3r::Surface ':types';
-use Slic3r::Test;
-
-{
-    my $flow = Slic3r::Flow->new(
-        width           => 1,
-        height          => 1,
-        nozzle_diameter => 1,
-    );
-    
-    my $config = Slic3r::Config->new;
-    my $test = sub {
-        my ($expolygons, %expected) = @_;
-        
-        my $slices = Slic3r::Surface::Collection->new;
-        $slices->append(Slic3r::Surface->new(
-            surface_type => S_TYPE_INTERNAL,
-            expolygon => $_,
-        )) for @$expolygons;
-        
-        my ($region_config, $object_config, $print_config, $loops, $gap_fill, $fill_surfaces);
-        my $g = Slic3r::Layer::PerimeterGenerator->new(
-            # input:
-            $slices,
-            1,  # layer height
-            $flow,
-            ($region_config = Slic3r::Config::PrintRegion->new),
-            ($object_config = Slic3r::Config::PrintObject->new),
-            ($print_config  = Slic3r::Config::Print->new),
-            
-            # output:
-            ($loops         = Slic3r::ExtrusionPath::Collection->new),
-            ($gap_fill      = Slic3r::ExtrusionPath::Collection->new),
-            ($fill_surfaces = Slic3r::Surface::Collection->new),
-        );
-        $g->config->apply_dynamic($config);
-        $g->process;
-        
-        is scalar(@$loops),
-            scalar(@$expolygons), 'expected number of collections';
-        ok !defined(first { !$_->isa('Slic3r::ExtrusionPath::Collection') } @$loops),
-            'everything is returned as collections';
-        
-        my $flattened_loops = $loops->flatten;
-        my @loops = @$flattened_loops;
-        is scalar(@loops),
-            $expected{total}, 'expected number of loops';
-        is scalar(grep $_->role == EXTR_ROLE_EXTERNAL_PERIMETER, map @$_, @loops),
-            $expected{external}, 'expected number of external loops';
-        is_deeply [ map { ($_->role == EXTR_ROLE_EXTERNAL_PERIMETER) || 0 } map @$_, @loops ],
-            $expected{ext_order}, 'expected external order';
-        is scalar(grep $_->loop_role == EXTRL_ROLE_CONTOUR_INTERNAL_PERIMETER, @loops),
-            $expected{cinternal}, 'expected number of internal contour loops';
-        is scalar(grep $_->polygon->is_counter_clockwise, @loops),
-            $expected{ccw}, 'expected number of ccw loops';
-        is_deeply [ map $_->polygon->is_counter_clockwise, @loops ],
-            $expected{ccw_order}, 'expected ccw/cw order';
-        
-        if ($expected{nesting}) {
-            foreach my $nesting (@{ $expected{nesting} }) {
-                for my $i (1..$#$nesting) {
-                    ok $loops[$nesting->[$i-1]]->polygon->contains_point($loops[$nesting->[$i]]->first_point),
-                        'expected nesting order';
-                }
-            }
-        }
-    };
-    
-    $config->set('perimeters', 3);
-    $test->(
-        [
-            Slic3r::ExPolygon->new(
-                Slic3r::Polygon->new_scale([0,0], [100,0], [100,100], [0,100]),
-            ),
-        ],
-        total       => 3,
-        external    => 1,
-        ext_order   => [0,0,1],
-        cinternal   => 1,
-        ccw         => 3,
-        ccw_order   => [1,1,1],
-        nesting     => [ [2,1,0] ],
-    );
-    $test->(
-        [
-            Slic3r::ExPolygon->new(
-                Slic3r::Polygon->new_scale([0,0], [100,0], [100,100], [0,100]),
-                Slic3r::Polygon->new_scale([40,40], [40,60], [60,60], [60,40]),
-            ),
-        ],
-        total       => 6,
-        external    => 2,
-        ext_order   => [0,0,1,0,0,1],
-        cinternal   => 1,
-        ccw         => 3,
-        ccw_order   => [0,0,0,1,1,1],
-        nesting     => [ [5,4,3,0,1,2] ],
-    );
-    $test->(
-        [
-            Slic3r::ExPolygon->new(
-                Slic3r::Polygon->new_scale([0,0], [200,0], [200,200], [0,200]),
-                Slic3r::Polygon->new_scale([20,20], [20,180], [180,180], [180,20]),
-            ),
-            # nested:
-            Slic3r::ExPolygon->new(
-                Slic3r::Polygon->new_scale([50,50], [150,50], [150,150], [50,150]),
-                Slic3r::Polygon->new_scale([80,80], [80,120], [120,120], [120,80]),
-            ),
-        ],
-        total       => 4*3,
-        external    => 4,
-        ext_order   => [0,0,1,0,0,1,0,0,1,0,0,1],
-        cinternal   => 2,
-        ccw         => 2*3,
-        ccw_order   => [0,0,0,1,1,1,0,0,0,1,1,1],
-    );
-    
-    $config->set('perimeters', 2);
-    $test->(
-        [
-            Slic3r::ExPolygon->new(
-                Slic3r::Polygon->new_scale([0,0], [50,0], [50,50], [0,50]),
-                Slic3r::Polygon->new_scale([7.5,7.5], [7.5,12.5], [12.5,12.5], [12.5,7.5]),
-                Slic3r::Polygon->new_scale([7.5,17.5], [7.5,22.5], [12.5,22.5], [12.5,17.5]),
-                Slic3r::Polygon->new_scale([7.5,27.5], [7.5,32.5], [12.5,32.5], [12.5,27.5]),
-                Slic3r::Polygon->new_scale([7.5,37.5], [7.5,42.5], [12.5,42.5], [12.5,37.5]),
-                Slic3r::Polygon->new_scale([17.5,7.5], [17.5,12.5], [22.5,12.5], [22.5,7.5]),
-            ),
-        ],
-        total       => 12,
-        external    => 6,
-        ext_order   => [0,1,0,1,0,1,0,1,0,1,0,1],
-        cinternal   => 1,
-        ccw         => 2,
-        ccw_order   => [0,0,0,0,0,0,0,0,0,0,1,1],
-        nesting     => [ [0,1],[2,3],[4,5],[6,7],[8,9] ],
-    );
-}
-
-{
-    my $config = Slic3r::Config::new_from_defaults;
-    $config->set('skirts', 0);
-    $config->set('fill_density', 0);
-    $config->set('perimeters', 3);
-    $config->set('top_solid_layers', 0);
-    $config->set('bottom_solid_layers', 0);
-    $config->set('cooling', [ 0 ]);                 # to prevent speeds from being altered
-    $config->set('first_layer_speed', '100%');      # to prevent speeds from being altered
-    
-    {
-        my $print = Slic3r::Test::init_print('overhang', config => $config);
-        my $has_cw_loops = 0;
-        my $cur_loop;
-        Slic3r::GCode::Reader->new->parse(Slic3r::Test::gcode($print), sub {
-            my ($self, $cmd, $args, $info) = @_;
-            
-            if ($info->{extruding} && $info->{dist_XY} > 0) {
-                $cur_loop ||= [ [$self->X, $self->Y] ];
-                push @$cur_loop, [ @$info{qw(new_X new_Y)} ];
-            } elsif ($cmd ne 'M73') { # skips remaining time lines (M73)
-                if ($cur_loop) {
-                    $has_cw_loops = 1 if Slic3r::Polygon->new(@$cur_loop)->is_clockwise;
-                    $cur_loop = undef;
-                }
-            }
-        });
-        ok !$has_cw_loops, 'all perimeters extruded ccw';
-    }
-    
-    foreach my $model (qw(cube_with_hole cube_with_concave_hole)) {
-        $config->set('external_perimeter_speed', 68);
-        my $print = Slic3r::Test::init_print(
-            $model,
-            config      => $config,
-            duplicate   => 2,  # we test two copies to make sure ExtrusionLoop objects are not modified in-place (the second object would not detect cw loops and thus would calculate wrong inwards moves)
-        );
-        my $has_cw_loops = my $has_outwards_move = my $starts_on_convex_point = 0;
-        my $cur_loop;
-        my %external_loops = ();  # print_z => count of external loops
-        Slic3r::GCode::Reader->new->parse(Slic3r::Test::gcode($print), sub {
-            my ($self, $cmd, $args, $info) = @_;
-            
-            if ($info->{extruding} && $info->{dist_XY} > 0) {
-                $cur_loop ||= [ [$self->X, $self->Y] ];
-                push @$cur_loop, [ @$info{qw(new_X new_Y)} ];
-            } elsif ($cmd ne 'M73') { # skips remaining time lines (M73)
-                if ($cur_loop) {
-                    $has_cw_loops = 1 if Slic3r::Polygon->new_scale(@$cur_loop)->is_clockwise;
-                    if ($self->F == $config->external_perimeter_speed*60) {
-                        my $move_dest = Slic3r::Point->new_scale(@$info{qw(new_X new_Y)});
-                        
-                        # reset counter for second object
-                        $external_loops{$self->Z} = 0
-                            if defined($external_loops{$self->Z}) && $external_loops{$self->Z} == 2;
-                        
-                        $external_loops{$self->Z}++;
-                        my $is_contour  = $external_loops{$self->Z} == 2;
-                        my $is_hole     = $external_loops{$self->Z} == 1;
-                        
-                        my $loop = Slic3r::Polygon->new_scale(@$cur_loop);
-                        my $loop_contains_point = $loop->contains_point($move_dest);
-                        $has_outwards_move = 1
-                            if (!$loop_contains_point && $is_contour)  # contour should include destination
-                             || ($loop_contains_point && $is_hole);    # hole should not
-                        
-                        if ($model eq 'cube_with_concave_hole') {
-                            # check that loop starts at a concave vertex
-                            my $ccw_angle = $loop->[-2]->ccw($loop->first_point, $loop->[1]);
-                            my $convex = ($ccw_angle > PI);  # whether the angle on the *right* side is convex
-                            $starts_on_convex_point = 1
-                                if ($convex && $is_contour) || (!$convex && $is_hole);
-                        }
-                    }
-                    $cur_loop = undef;
-                }
-            }
-        });
-        ok !$has_cw_loops, 'all perimeters extruded ccw';
-        ok !$has_outwards_move, 'move inwards after completing external loop';
-        ok !$starts_on_convex_point, 'loops start on concave point if any';
-    }
-    
-    {
-        $config->set('perimeters', 1);
-        $config->set('perimeter_speed', 77);
-        $config->set('external_perimeter_speed', 66);
-        $config->set('bridge_speed', 99);
-        $config->set('cooling', [ 1 ]);
-        $config->set('fan_below_layer_time', [ 0 ]);
-        $config->set('slowdown_below_layer_time', [ 0 ]);
-        $config->set('bridge_fan_speed', [ 100 ]);
-        $config->set('bridge_flow_ratio', 33);  # arbitrary value
-        $config->set('overhangs', 1);
-        my $print = Slic3r::Test::init_print('overhang', config => $config);
-        my %layer_speeds = ();  # print Z => [ speeds ]
-        my $fan_speed = 0;
-        my $bridge_mm_per_mm = ($config->nozzle_diameter->[0]**2) / ($config->filament_diameter->[0]**2) * $config->bridge_flow_ratio;
-        Slic3r::GCode::Reader->new->parse(Slic3r::Test::gcode($print), sub {
-            my ($self, $cmd, $args, $info) = @_;
-            
-            $fan_speed = 0 if $cmd eq 'M107';
-            $fan_speed = $args->{S} if $cmd eq 'M106';
-            if ($info->{extruding} && $info->{dist_XY} > 0) {
-                $layer_speeds{$self->Z} ||= {};
-                $layer_speeds{$self->Z}{my $feedrate = $args->{F} // $self->F} = 1;
-                
-                fail 'wrong speed found'
-                    if $feedrate != $config->perimeter_speed*60
-                        && $feedrate != $config->external_perimeter_speed*60
-                        && $feedrate != $config->bridge_speed*60;
-                
-                if ($feedrate == $config->bridge_speed*60) {
-                    fail 'printing overhang but fan is not enabled or running at wrong speed'
-                        if $fan_speed != 255;
-                    my $mm_per_mm = $info->{dist_E} / $info->{dist_XY};
-                    fail 'wrong bridge flow' if abs($mm_per_mm - $bridge_mm_per_mm) > 0.01;
-                } else {
-                    fail 'fan is running when not supposed to'
-                        if $fan_speed > 0;
-                }
-            }
-        });
-        is scalar(grep { keys %$_ > 1 } values %layer_speeds), 1,
-            'only overhang layer has more than one speed';
-    }
-}
-
-{
-    my $config = Slic3r::Config::new_from_defaults;
-    $config->set('skirts', 0);
-    $config->set('perimeters', 3);
-    $config->set('layer_height', 0.4);
-    $config->set('first_layer_height', 0.35);
-    $config->set('extra_perimeters', 1);
-    $config->set('cooling', [ 0 ]);                 # to prevent speeds from being altered
-    $config->set('first_layer_speed', '100%');      # to prevent speeds from being altered
-    $config->set('perimeter_speed', 99);
-    $config->set('external_perimeter_speed', 99);
-    $config->set('small_perimeter_speed', 99);
-    $config->set('thin_walls', 0);
-    
-    my $print = Slic3r::Test::init_print('ipadstand', config => $config);
-    my %perimeters = ();  # z => number of loops
-    my $in_loop = 0;
-    Slic3r::GCode::Reader->new->parse(Slic3r::Test::gcode($print), sub {
-        my ($self, $cmd, $args, $info) = @_;
-        
-        if ($info->{extruding} && $info->{dist_XY} > 0 && ($args->{F} // $self->F) == $config->perimeter_speed*60) {
-            $perimeters{$self->Z}++ if !$in_loop;
-            $in_loop = 1;
-        } elsif ($cmd ne 'M73') { # skips remaining time lines (M73)
-            $in_loop = 0;
-        }
-    });
-    ok !(grep { $_ % $config->perimeters } values %perimeters), 'no superfluous extra perimeters';
-}
-
-{
-    my $config = Slic3r::Config::new_from_defaults;
-    $config->set('nozzle_diameter', [0.4]);
-    $config->set('perimeters', 2);
-    $config->set('perimeter_extrusion_width', 0.4);
-    $config->set('external_perimeter_extrusion_width', 0.4);
-    $config->set('infill_extrusion_width', 0.53);
-    $config->set('solid_infill_extrusion_width', 0.53);
-    
-    # we just need a pre-filled Print object
-    my $print = Slic3r::Test::init_print('20mm_cube', config => $config);
-    
-    # override a layer's slices
-    my $expolygon = Slic3r::ExPolygon->new([[-71974463,-139999376],[-71731792,-139987456],[-71706544,-139985616],[-71682119,-139982639],[-71441248,-139946912],[-71417487,-139942895],[-71379384,-139933984],[-71141800,-139874480],[-71105247,-139862895],[-70873544,-139779984],[-70838592,-139765856],[-70614943,-139660064],[-70581783,-139643567],[-70368368,-139515680],[-70323751,-139487872],[-70122160,-139338352],[-70082399,-139306639],[-69894800,-139136624],[-69878679,-139121327],[-69707992,-138933008],[-69668575,-138887343],[-69518775,-138685359],[-69484336,-138631632],[-69356423,-138418207],[-69250040,-138193296],[-69220920,-138128976],[-69137992,-137897168],[-69126095,-137860255],[-69066568,-137622608],[-69057104,-137582511],[-69053079,-137558751],[-69017352,-137317872],[-69014392,-137293456],[-69012543,-137268207],[-68999369,-137000000],[-63999999,-137000000],[-63705947,-136985551],[-63654984,-136977984],[-63414731,-136942351],[-63364756,-136929840],[-63129151,-136870815],[-62851950,-136771631],[-62585807,-136645743],[-62377483,-136520895],[-62333291,-136494415],[-62291908,-136463728],[-62096819,-136319023],[-62058644,-136284432],[-61878676,-136121328],[-61680968,-135903184],[-61650275,-135861807],[-61505591,-135666719],[-61354239,-135414191],[-61332211,-135367615],[-61228359,-135148063],[-61129179,-134870847],[-61057639,-134585262],[-61014451,-134294047],[-61000000,-134000000],[-61000000,-107999999],[-61014451,-107705944],[-61057639,-107414736],[-61129179,-107129152],[-61228359,-106851953],[-61354239,-106585808],[-61505591,-106333288],[-61680967,-106096816],[-61878675,-105878680],[-62096820,-105680967],[-62138204,-105650279],[-62333292,-105505591],[-62585808,-105354239],[-62632384,-105332207],[-62851951,-105228360],[-62900463,-105211008],[-63129152,-105129183],[-63414731,-105057640],[-63705947,-105014448],[-63999999,-105000000],[-68999369,-105000000],[-69012543,-104731792],[-69014392,-104706544],[-69017352,-104682119],[-69053079,-104441248],[-69057104,-104417487],[-69066008,-104379383],[-69125528,-104141799],[-69137111,-104105248],[-69220007,-103873544],[-69234136,-103838591],[-69339920,-103614943],[-69356415,-103581784],[-69484328,-103368367],[-69512143,-103323752],[-69661647,-103122160],[-69693352,-103082399],[-69863383,-102894800],[-69878680,-102878679],[-70066999,-102707992],[-70112656,-102668576],[-70314648,-102518775],[-70368367,-102484336],[-70581783,-102356424],[-70806711,-102250040],[-70871040,-102220919],[-71102823,-102137992],[-71139752,-102126095],[-71377383,-102066568],[-71417487,-102057104],[-71441248,-102053079],[-71682119,-102017352],[-71706535,-102014392],[-71731784,-102012543],[-71974456,-102000624],[-71999999,-102000000],[-104000000,-102000000],[-104025536,-102000624],[-104268207,-102012543],[-104293455,-102014392],[-104317880,-102017352],[-104558751,-102053079],[-104582512,-102057104],[-104620616,-102066008],[-104858200,-102125528],[-104894751,-102137111],[-105126455,-102220007],[-105161408,-102234136],[-105385056,-102339920],[-105418215,-102356415],[-105631632,-102484328],[-105676247,-102512143],[-105877839,-102661647],[-105917600,-102693352],[-106105199,-102863383],[-106121320,-102878680],[-106292007,-103066999],[-106331424,-103112656],[-106481224,-103314648],[-106515663,-103368367],[-106643575,-103581783],[-106749959,-103806711],[-106779080,-103871040],[-106862007,-104102823],[-106873904,-104139752],[-106933431,-104377383],[-106942896,-104417487],[-106946920,-104441248],[-106982648,-104682119],[-106985607,-104706535],[-106987456,-104731784],[-107000630,-105000000],[-112000000,-105000000],[-112294056,-105014448],[-112585264,-105057640],[-112870848,-105129184],[-112919359,-105146535],[-113148048,-105228360],[-113194624,-105250392],[-113414191,-105354239],[-113666711,-105505591],[-113708095,-105536279],[-113903183,-105680967],[-114121320,-105878679],[-114319032,-106096816],[-114349720,-106138200],[-114494408,-106333288],[-114645760,-106585808],[-114667792,-106632384],[-114771640,-106851952],[-114788991,-106900463],[-114870815,-107129151],[-114942359,-107414735],[-114985551,-107705943],[-115000000,-107999999],[-115000000,-134000000],[-114985551,-134294048],[-114942359,-134585263],[-114870816,-134870847],[-114853464,-134919359],[-114771639,-135148064],[-114645759,-135414192],[-114494407,-135666720],[-114319031,-135903184],[-114121320,-136121327],[-114083144,-136155919],[-113903184,-136319023],[-113861799,-136349712],[-113666711,-136494416],[-113458383,-136619264],[-113414192,-136645743],[-113148049,-136771631],[-112870848,-136870815],[-112820872,-136883327],[-112585264,-136942351],[-112534303,-136949920],[-112294056,-136985551],[-112000000,-137000000],[-107000630,-137000000],[-106987456,-137268207],[-106985608,-137293440],[-106982647,-137317872],[-106946920,-137558751],[-106942896,-137582511],[-106933991,-137620624],[-106874471,-137858208],[-106862888,-137894751],[-106779992,-138126463],[-106765863,-138161424],[-106660080,-138385055],[-106643584,-138418223],[-106515671,-138631648],[-106487855,-138676256],[-106338352,-138877839],[-106306647,-138917600],[-106136616,-139105199],[-106121320,-139121328],[-105933000,-139291999],[-105887344,-139331407],[-105685351,-139481232],[-105631632,-139515663],[-105418216,-139643567],[-105193288,-139749951],[-105128959,-139779072],[-104897175,-139862016],[-104860247,-139873904],[-104622616,-139933423],[-104582511,-139942896],[-104558751,-139946912],[-104317880,-139982656],[-104293463,-139985616],[-104268216,-139987456],[-104025544,-139999376],[-104000000,-140000000],[-71999999,-140000000]],[[-105000000,-138000000],[-105000000,-104000000],[-71000000,-104000000],[-71000000,-138000000]],[[-69000000,-132000000],[-69000000,-110000000],[-64991180,-110000000],[-64991180,-132000000]],[[-111008824,-132000000],[-111008824,-110000000],[-107000000,-110000000],[-107000000,-132000000]]);
-    my $object = $print->print->objects->[0];
-    $object->slice;
-    my $layer = $object->get_layer(1);
-    my $layerm = $layer->regions->[0];
-    $layerm->slices->clear;
-    $layerm->slices->append(Slic3r::Surface->new(surface_type => S_TYPE_INTERNAL, expolygon => $expolygon));
-    
-    # make perimeters
-    $layer->make_perimeters;
-    
-    # compute the covered area
-    my $pflow = $layerm->flow(FLOW_ROLE_PERIMETER);
-    my $iflow = $layerm->flow(FLOW_ROLE_INFILL);
-    my $covered_by_perimeters = union_ex([
-        (map @{$_->polygon->split_at_first_point->grow($pflow->scaled_width/2)}, map @$_, @{$layerm->perimeters}),
-    ]);
-    my $covered_by_infill = union_ex([
-        (map $_->p, @{$layerm->fill_surfaces}),
-        (map @{$_->polyline->grow($iflow->scaled_width/2)}, @{$layerm->thin_fills}),
-    ]);
-    
-    # compute the non covered area
-    my $non_covered = diff(
-        [ map @{$_->expolygon}, @{$layerm->slices} ],
-        [ map @$_, (@$covered_by_perimeters, @$covered_by_infill) ],
-    );
-    
-    if (0) {
-        printf "max non covered = %f\n", List::Util::max(map unscale unscale $_->area, @$non_covered);
-        require "Slic3r/SVG.pm";
-        Slic3r::SVG::output(
-            "gaps.svg",
-            expolygons          => [ map $_->expolygon, @{$layerm->slices} ],
-            red_expolygons      => union_ex([ map @$_, (@$covered_by_perimeters, @$covered_by_infill) ]),
-            green_expolygons    => union_ex($non_covered),
-            no_arrows           => 1,
-            polylines           => [
-                map $_->polygon->split_at_first_point, map @$_, @{$layerm->perimeters},
-            ],
-        );
-    }
-    ok !(defined first { $_->area > ($iflow->scaled_width**2) } @$non_covered), 'no gap between perimeters and infill';
-}
-
-{
-    my $config = Slic3r::Config::new_from_defaults;
-    $config->set('skirts', 0);
-    $config->set('perimeters', 3);
-    $config->set('layer_height', 0.4);
-    $config->set('bridge_speed', 99);
-    $config->set('fill_density', 0);                # to prevent bridging over sparse infill
-    $config->set('overhangs', 1);
-    $config->set('cooling', [ 0 ]);                 # to prevent speeds from being altered
-    $config->set('first_layer_speed', '100%');      # to prevent speeds from being altered
-    
-    my $test = sub {
-        my ($print) = @_;
-        my %z_with_bridges = ();  # z => 1
-        Slic3r::GCode::Reader->new->parse(Slic3r::Test::gcode($print), sub {
-            my ($self, $cmd, $args, $info) = @_;
-        
-            if ($info->{extruding} && $info->{dist_XY} > 0) {
-                $z_with_bridges{$self->Z} = 1 if ($args->{F} // $self->F) == $config->bridge_speed*60;
-            }
-        });
-        return scalar keys %z_with_bridges;
-    };
-    ok $test->(Slic3r::Test::init_print('V', config => $config)) == 1,
-        'no overhangs printed with bridge speed';  # except for the two internal solid layers above void
-    ok $test->(Slic3r::Test::init_print('V', config => $config, scale_xyz => [3,1,1])) > 2,
-        'overhangs printed with bridge speed';
-}
-
-{
-    my $config = Slic3r::Config::new_from_defaults;
-    $config->set('seam_position', 'random');
-    my $print = Slic3r::Test::init_print('20mm_cube', config => $config);
-    ok Slic3r::Test::gcode($print), 'successful generation of G-code with seam_position = random';
-}
-
-{
-    my $test = sub {
-        my ($model_name) = @_;
-        my $config = Slic3r::Config::new_from_defaults;
-        $config->set('seam_position', 'aligned');
-        $config->set('skirts', 0);
-        $config->set('perimeters', 1);
-        $config->set('fill_density', 0);
-        $config->set('top_solid_layers', 0);
-        $config->set('bottom_solid_layers', 0);
-        $config->set('retract_layer_change', [0]);
-    
-        my $was_extruding = 0;
-        my @seam_points = ();
-        my $print = Slic3r::Test::init_print($model_name, config => $config);
-        Slic3r::GCode::Reader->new->parse(my $gcode = Slic3r::Test::gcode($print), sub {
-            my ($self, $cmd, $args, $info) = @_;
-    
-            if ($info->{extruding}) {
-                if (!$was_extruding) {
-                    push @seam_points, Slic3r::Point->new_scale($self->X, $self->Y);
-                }
-                $was_extruding = 1;
-            } elsif ($cmd ne 'M73') { # skips remaining time lines (M73)
-                $was_extruding = 0;
-            }
-        });
-        my @dist = map unscale($_), map $seam_points[$_]->distance_to($seam_points[$_+1]), 0..($#seam_points-1);
-        ok !(defined first { $_ > 3 } @dist), 'seam is aligned';
-    };
-    $test->('20mm_cube');
-    $test->('small_dorito');
-}
-
-__END__
diff --git a/t/slice.t b/t/slice.t
deleted file mode 100644
index 2f193aae3..000000000
--- a/t/slice.t
+++ /dev/null
@@ -1,152 +0,0 @@
-use Test::More;
-use strict;
-use warnings;
-
-plan skip_all => 'temporarily disabled';
-plan tests => 16;
-
-BEGIN {
-    use FindBin;
-    use lib "$FindBin::Bin/../lib";
-    use local::lib "$FindBin::Bin/../local-lib";
-}
-
-# temporarily disable compilation errors due to constant not being exported anymore
-sub Slic3r::TriangleMesh::I_B {}
-sub Slic3r::TriangleMesh::I_FACET_EDGE {}
-sub Slic3r::TriangleMesh::FE_BOTTOM {
-sub Slic3r::TriangleMesh::FE_TOP {}}
-
-use Slic3r;
-use Slic3r::Geometry qw(X Y Z);
-
-my @lines;
-my $z = 20;
-my @points = ([3, 4], [8, 5], [1, 9]);  # XY coordinates of the facet vertices
-
-# NOTE:
-# the first point of the intersection lines is replaced by -1 because TriangleMesh.pm
-# is saving memory and doesn't store point A anymore since it's not actually needed.
-
-# We disable this test because intersect_facet() now assumes we never feed a horizontal
-# facet to it.
-# is_deeply lines(20, 20, 20), [
-#     [ -1, $points[1] ],  # $points[0]
-#     [ -1, $points[2] ],  # $points[1]
-#     [ -1, $points[0] ],  # $points[2]
-# ], 'horizontal';
-
-is_deeply lines(22, 20, 20), [ [ -1, $points[2] ] ], 'lower edge on layer';  # $points[1]
-is_deeply lines(20, 20, 22), [ [ -1, $points[1] ] ], 'lower edge on layer';  # $points[0]
-is_deeply lines(20, 22, 20), [ [ -1, $points[0] ] ], 'lower edge on layer';  # $points[2]
-
-is_deeply lines(20, 20, 10), [ [ -1, $points[0] ] ], 'upper edge on layer';  # $points[1]
-is_deeply lines(10, 20, 20), [ [ -1, $points[1] ] ], 'upper edge on layer';  # $points[2]
-is_deeply lines(20, 10, 20), [ [ -1, $points[2] ] ], 'upper edge on layer';  # $points[0]
-
-is_deeply lines(20, 15, 10), [                            ], 'upper vertex on layer';
-is_deeply lines(28, 20, 30), [                            ], 'lower vertex on layer';
-
-{
-    my @z = (24, 10, 16);
-    is_deeply lines(@z), [
-        [
-            -1, # line_plane_intersection([ vertices(@z)->[0], vertices(@z)->[1] ]),
-            line_plane_intersection([ vertices(@z)->[2], vertices(@z)->[0] ]),
-        ]
-    ], 'two edges intersect';
-}
-
-{
-    my @z = (16, 24, 10);
-    is_deeply lines(@z), [
-        [
-            -1, # line_plane_intersection([ vertices(@z)->[1], vertices(@z)->[2] ]),
-            line_plane_intersection([ vertices(@z)->[0], vertices(@z)->[1] ]),
-        ]
-    ], 'two edges intersect';
-}
-
-{
-    my @z = (10, 16, 24);
-    is_deeply lines(@z), [
-        [
-            -1, # line_plane_intersection([ vertices(@z)->[2], vertices(@z)->[0] ]),
-            line_plane_intersection([ vertices(@z)->[1], vertices(@z)->[2] ]),
-        ]
-    ], 'two edges intersect';
-}
-
-{
-    my @z = (24, 10, 20);
-    is_deeply lines(@z), [
-        [
-            -1, # line_plane_intersection([ vertices(@z)->[0], vertices(@z)->[1] ]),
-            $points[2],
-        ]
-    ], 'one vertex on plane and one edge intersects';
-}
-
-{
-    my @z = (10, 20, 24);
-    is_deeply lines(@z), [
-        [
-            -1, # line_plane_intersection([ vertices(@z)->[2], vertices(@z)->[0] ]),
-            $points[1],
-        ]
-    ], 'one vertex on plane and one edge intersects';
-}
-
-{
-    my @z = (20, 24, 10);
-    is_deeply lines(@z), [
-        [
-            -1, # line_plane_intersection([ vertices(@z)->[1], vertices(@z)->[2] ]),
-            $points[0],
-        ]
-    ], 'one vertex on plane and one edge intersects';
-}
-
-my @lower = intersect(22, 20, 20);
-my @upper = intersect(20, 20, 10);
-is $lower[0][Slic3r::TriangleMesh::I_FACET_EDGE], Slic3r::TriangleMesh::FE_BOTTOM, 'bottom edge on layer';
-is $upper[0][Slic3r::TriangleMesh::I_FACET_EDGE], Slic3r::TriangleMesh::FE_TOP, 'upper edge on layer';
-
-my $mesh;
-
-sub intersect {
-    $mesh = Slic3r::TriangleMesh->new(
-        facets      => [],
-        vertices    => [],
-    );
-    push @{$mesh->facets}, [ [0,0,0], @{vertices(@_)} ];
-    $mesh->analyze;
-    return map Slic3r::TriangleMesh::unpack_line($_), $mesh->intersect_facet($#{$mesh->facets}, $z);
-}
-
-sub vertices {
-    push @{$mesh->vertices}, map [ @{$points[$_]}, $_[$_] ], 0..2;
-    [ ($#{$mesh->vertices}-2) .. $#{$mesh->vertices} ]
-}
-
-sub lines {
-    my @lines = intersect(@_);
-    #$_->a->[X] = sprintf('%.0f', $_->a->[X]) for @lines;
-    #$_->a->[Y] = sprintf('%.0f', $_->a->[Y]) for @lines;
-    $_->[Slic3r::TriangleMesh::I_B][X] = sprintf('%.0f', $_->[Slic3r::TriangleMesh::I_B][X]) for @lines;
-    $_->[Slic3r::TriangleMesh::I_B][Y] = sprintf('%.0f', $_->[Slic3r::TriangleMesh::I_B][Y]) for @lines;
-    return [ map [ -1, $_->[Slic3r::TriangleMesh::I_B] ], @lines ];
-}
-
-sub line_plane_intersection {
-    my ($line) = @_;
-    @$line = map $mesh->vertices->[$_], @$line;
-    
-    return [
-        map sprintf('%.0f', $_),
-            map +($line->[1][$_] + ($line->[0][$_] - $line->[1][$_]) * ($z - $line->[1][Z]) / ($line->[0][Z] - $line->[1][Z])),
-                (X,Y)
-    ];
-}
-
-__END__
diff --git a/t/support.t b/t/support.t
deleted file mode 100644
index 0283df22b..000000000
--- a/t/support.t
+++ /dev/null
@@ -1,272 +0,0 @@
-use Test::More;
-use strict;
-use warnings;
-
-plan skip_all => 'temporarily disabled';
-plan tests => 27;
-
-BEGIN {
-    use FindBin;
-    use lib "$FindBin::Bin/../lib";
-    use local::lib "$FindBin::Bin/../local-lib";
-}
-
-use List::Util qw(first);
-use Slic3r;
-use Slic3r::Flow ':roles';
-use Slic3r::Geometry qw(epsilon scale);
-use Slic3r::Geometry::Clipper qw(diff);
-use Slic3r::Test;
-
-{
-    my $config = Slic3r::Config::new_from_defaults;
-    $config->set('support_material', 1);
-    my @contact_z = my @top_z = ();
-    
-    my $test = sub {
-        my $print = Slic3r::Test::init_print('20mm_cube', config => $config);
-        my $object_config = $print->print->objects->[0]->config;
-        my $flow = Slic3r::Flow->new_from_width(
-            width               => $object_config->support_material_extrusion_width || $object_config->extrusion_width,
-            role                => FLOW_ROLE_SUPPORT_MATERIAL,
-            nozzle_diameter     => $print->config->nozzle_diameter->[$object_config->support_material_extruder-1] // $print->config->nozzle_diameter->[0],
-            layer_height        => $object_config->layer_height,
-        );
-        my $support = Slic3r::Print::SupportMaterial->new(
-            object_config       => $print->print->objects->[0]->config,
-            print_config        => $print->print->config,
-            flow                => $flow,
-            interface_flow      => $flow,
-            first_layer_flow    => $flow,
-        );
-        my $support_z = $support->support_layers_z($print->print->objects->[0], \@contact_z, \@top_z, $config->layer_height);
-        my $expected_top_spacing = $support->contact_distance($config->layer_height, $config->nozzle_diameter->[0]);
-        
-        is $support_z->[0], $config->first_layer_height,
-            'first layer height is honored';
-        is scalar(grep { $support_z->[$_]-$support_z->[$_-1] <= 0 } 1..$#$support_z), 0,
-            'no null or negative support layers';
-        is scalar(grep { $support_z->[$_]-$support_z->[$_-1] > $config->nozzle_diameter->[0] + epsilon } 1..$#$support_z), 0,
-            'no layers thicker than nozzle diameter';
-        
-        my $wrong_top_spacing = 0;
-        foreach my $top_z (@top_z) {
-            # find layer index of this top surface
-            my $layer_id = first { abs($support_z->[$_] - $top_z) < epsilon } 0..$#$support_z;
-            
-            # check that first support layer above this top surface (or the next one) is spaced with nozzle diameter
-            $wrong_top_spacing = 1
-                if ($support_z->[$layer_id+1] - $support_z->[$layer_id]) != $expected_top_spacing
-                && ($support_z->[$layer_id+2] - $support_z->[$layer_id]) != $expected_top_spacing;
-        }
-        ok !$wrong_top_spacing, 'layers above top surfaces are spaced correctly';
-    };
-    
-    $config->set('layer_height', 0.2);
-    $config->set('first_layer_height', 0.3);
-    @contact_z = (1.9);
-    @top_z = (1.1);
-    $test->();
-    
-    $config->set('first_layer_height', 0.4);
-    $test->();
-    
-    $config->set('layer_height', $config->nozzle_diameter->[0]);
-    $test->();
-}
-
-{
-    my $config = Slic3r::Config::new_from_defaults;
-    $config->set('raft_layers', 3);
-    $config->set('brim_width',  0);
-    $config->set('skirts', 0);
-    $config->set('support_material_extruder', 2);
-    $config->set('support_material_interface_extruder', 2);
-    $config->set('layer_height', 0.4);
-    $config->set('first_layer_height', 0.4);
-    my $print = Slic3r::Test::init_print('overhang', config => $config);
-    ok my $gcode = Slic3r::Test::gcode($print), 'no conflict between raft/support and brim';
-    
-    my $tool = 0;
-    Slic3r::GCode::Reader->new->parse($gcode, sub {
-        my ($self, $cmd, $args, $info) = @_;
-        
-        if ($cmd =~ /^T(\d+)/) {
-            $tool = $1;
-        } elsif ($info->{extruding}) {
-            if ($self->Z <= ($config->raft_layers * $config->layer_height)) {
-                fail 'not extruding raft with support material extruder'
-                    if $tool != ($config->support_material_extruder-1);
-            } else {
-                fail 'support material exceeds raft layers'
-                    if $tool == $config->support_material_extruder-1;
-                # TODO: we should test that full support is generated when we use raft too
-            }
-        }
-    });
-}
-
-{
-    my $config = Slic3r::Config::new_from_defaults;
-    $config->set('skirts', 0);
-    $config->set('raft_layers', 3);
-    $config->set('support_material_pattern', 'honeycomb');
-    $config->set('support_material_extrusion_width', 0.6);
-    $config->set('first_layer_extrusion_width', '100%');
-    $config->set('bridge_speed', 99);
-    $config->set('cooling', [ 0 ]);             # prevent speed alteration
-    $config->set('first_layer_speed', '100%');  # prevent speed alteration
-    $config->set('start_gcode', '');            # prevent any unexpected Z move
-    my $print = Slic3r::Test::init_print('20mm_cube', config => $config);
-    
-    my $layer_id = -1;  # so that first Z move sets this to 0
-    my @raft = my @first_object_layer = ();
-    my %first_object_layer_speeds = ();  # F => 1
-    Slic3r::GCode::Reader->new->parse(Slic3r::Test::gcode($print), sub {
-        my ($self, $cmd, $args, $info) = @_;
-        
-        if ($info->{extruding} && $info->{dist_XY} > 0) {
-            if ($layer_id <= $config->raft_layers) {
-                # this is a raft layer or the first object layer
-                my $line = Slic3r::Line->new_scale([ $self->X, $self->Y ], [ $info->{new_X}, $info->{new_Y} ]);
-                my @path = @{$line->grow(scale($config->support_material_extrusion_width/2))};
-                if ($layer_id < $config->raft_layers) {
-                    # this is a raft layer
-                    push @raft, @path;
-                } else {
-                    push @first_object_layer, @path;
-                    $first_object_layer_speeds{ $args->{F} // $self->F } = 1;
-                }
-            }
-        } elsif ($cmd eq 'G1' && $info->{dist_Z} > 0) {
-            $layer_id++;
-        }
-    });
-    
-    ok !@{diff(\@first_object_layer, \@raft)},
-        'first object layer is completely supported by raft';
-    is scalar(keys %first_object_layer_speeds), 1,
-        'only one speed used in first object layer';
-    ok +(keys %first_object_layer_speeds)[0] == $config->bridge_speed*60,
-        'bridge speed used in first object layer';
-}
-
-{
-    my $config = Slic3r::Config::new_from_defaults;
-    $config->set('skirts', 0);
-    $config->set('layer_height', 0.35);
-    $config->set('first_layer_height', 0.3);
-    $config->set('nozzle_diameter', [0.5]);
-    $config->set('support_material_extruder', 2);
-    $config->set('support_material_interface_extruder', 2);
-    
-    my $test = sub {
-        my ($raft_layers) = @_;
-        $config->set('raft_layers', $raft_layers);
-        my $print = Slic3r::Test::init_print('20mm_cube', config => $config);
-        my %raft_z = ();  # z => 1
-        my $tool = undef;
-        Slic3r::GCode::Reader->new->parse(Slic3r::Test::gcode($print), sub {
-            my ($self, $cmd, $args, $info) = @_;
-        
-            if ($cmd =~ /^T(\d+)/) {
-                $tool = $1;
-            } elsif ($info->{extruding} && $info->{dist_XY} > 0) {
-                if ($tool == $config->support_material_extruder-1) {
-                    $raft_z{$self->Z} = 1;
-                }
-            }
-        });
-    
-        is scalar(keys %raft_z), $config->raft_layers, 'correct number of raft layers is generated';
-    };
-    
-    $test->(2);
-    $test->(70);
-    
-    $config->set('layer_height', 0.4);
-    $config->set('first_layer_height', 0.35);
-    $test->(3);
-    $test->(70);
-}
-
-{
-    my $config = Slic3r::Config::new_from_defaults;
-    $config->set('brim_width',  0);
-    $config->set('skirts', 0);
-    $config->set('support_material', 1);
-    $config->set('top_solid_layers', 0); # so that we don't have the internal bridge over infill
-    $config->set('bridge_speed', 99);
-    $config->set('cooling', [ 0 ]);
-    $config->set('first_layer_speed', '100%');
-    
-    my $test = sub {
-        my $print = Slic3r::Test::init_print('overhang', config => $config);
-    
-        my $has_bridge_speed = 0;
-        Slic3r::GCode::Reader->new->parse(Slic3r::Test::gcode($print), sub {
-            my ($self, $cmd, $args, $info) = @_;
-        
-            if ($info->{extruding}) {
-                if (($args->{F} // $self->F) == $config->bridge_speed*60) {
-                    $has_bridge_speed = 1;
-                }
-            }
-        });
-        return $has_bridge_speed;
-    };
-    
-    $config->set('support_material_contact_distance', 0.2);
-    ok $test->(), 'bridge speed is used when support_material_contact_distance > 0';
-    
-    $config->set('support_material_contact_distance', 0);
-    ok !$test->(), 'bridge speed is not used when support_material_contact_distance == 0';
-    
-    $config->set('raft_layers', 5);
-    $config->set('support_material_contact_distance', 0.2);
-    ok $test->(), 'bridge speed is used when raft_layers > 0 and support_material_contact_distance > 0';
-    
-    $config->set('support_material_contact_distance', 0);
-    ok !$test->(), 'bridge speed is not used when raft_layers > 0 and support_material_contact_distance == 0';
-}
-
-{
-    my $config = Slic3r::Config::new_from_defaults;
-    $config->set('skirts', 0);
-    $config->set('start_gcode', '');
-    $config->set('raft_layers', 8);
-    $config->set('nozzle_diameter', [0.4, 1]);
-    $config->set('layer_height', 0.1);
-    $config->set('first_layer_height', 0.8);
-    $config->set('support_material_extruder', 2);
-    $config->set('support_material_interface_extruder', 2);
-    $config->set('support_material_contact_distance', 0);
-    my $print = Slic3r::Test::init_print('20mm_cube', config => $config);
-    ok my $gcode = Slic3r::Test::gcode($print), 'first_layer_height is validated with support material extruder nozzle diameter when using raft layers';
-    
-    my $tool = undef;
-    my @z = (0);
-    my %layer_heights_by_tool = ();  # tool => [ lh, lh... ]
-    Slic3r::GCode::Reader->new->parse($gcode, sub {
-        my ($self, $cmd, $args, $info) = @_;
-    
-        if ($cmd =~ /^T(\d+)/) {
-            $tool = $1;
-        } elsif ($cmd eq 'G1' && exists $args->{Z} && $args->{Z} != $self->Z) {
-            push @z, $args->{Z};
-        } elsif ($info->{extruding} && $info->{dist_XY} > 0) {
-            $layer_heights_by_tool{$tool} ||= [];
-            push @{ $layer_heights_by_tool{$tool} }, $z[-1] - $z[-2];
-        }
-    });
-    
-    ok !defined(first { $_ > $config->nozzle_diameter->[0] + epsilon }
-        @{ $layer_heights_by_tool{$config->perimeter_extruder-1} }),
-        'no object layer is thicker than nozzle diameter';
-    
-    ok !defined(first { abs($_ - $config->layer_height) < epsilon }
-        @{ $layer_heights_by_tool{$config->support_material_extruder-1} }),
-        'no support material layer is as thin as object layers';
-}
-
-__END__
diff --git a/tests/fff_print/CMakeLists.txt b/tests/fff_print/CMakeLists.txt
index 50b45e384..28f75bf2c 100644
--- a/tests/fff_print/CMakeLists.txt
+++ b/tests/fff_print/CMakeLists.txt
@@ -10,6 +10,8 @@ add_executable(${_TEST_NAME}_tests
 	test_gcodefindreplace.cpp
 	test_gcodewriter.cpp
 	test_model.cpp
+	test_multi.cpp
+	test_perimeters.cpp
 	test_print.cpp
 	test_printgcode.cpp
 	test_printobject.cpp
diff --git a/tests/fff_print/test_extrusion_entity.cpp b/tests/fff_print/test_extrusion_entity.cpp
index 7e0ca822f..c98e0ec1a 100644
--- a/tests/fff_print/test_extrusion_entity.cpp
+++ b/tests/fff_print/test_extrusion_entity.cpp
@@ -140,4 +140,11 @@ TEST_CASE("ExtrusionEntityCollection: Chained path", "[ExtrusionEntity]") {
             REQUIRE(p1 == p2);
         }
     }
-}
\ No newline at end of file
+}
+
+TEST_CASE("ExtrusionEntityCollection: Chained path with no explicit starting point", "[ExtrusionEntity]") {
+    auto polylines = Polylines { { { 0, 15 }, {0, 18}, {0, 20} }, { { 0, 10 }, {0, 8}, {0, 5} } };
+    auto target    = Polylines { { {0, 5}, {0, 8}, { 0, 10 } }, { { 0, 15 }, {0, 18}, {0, 20} } };
+    auto chained   = chain_polylines(polylines);
+    REQUIRE(chained == target);
+}
diff --git a/tests/fff_print/test_multi.cpp b/tests/fff_print/test_multi.cpp
new file mode 100644
index 000000000..03c7f644b
--- /dev/null
+++ b/tests/fff_print/test_multi.cpp
@@ -0,0 +1,268 @@
+#include <catch2/catch.hpp>
+
+#include <numeric>
+#include <sstream>
+
+#include "libslic3r/ClipperUtils.hpp"
+#include "libslic3r/Geometry.hpp"
+#include "libslic3r/Geometry/ConvexHull.hpp"
+#include "libslic3r/Print.hpp"
+#include "libslic3r/libslic3r.h"
+
+#include "test_data.hpp"
+
+using namespace Slic3r;
+using namespace std::literals;
+
+SCENARIO("Basic tests", "[Multi]")
+{
+    WHEN("Slicing multi-material print with non-consecutive extruders") {
+        std::string gcode = Slic3r::Test::slice({ Slic3r::Test::TestMesh::cube_20x20x20 }, 
+            {
+                { "nozzle_diameter",                "0.6, 0.6, 0.6, 0.6" },
+                { "extruder",                       2 },
+                { "infill_extruder",                4 },
+                { "support_material_extruder",      0 }
+            });
+        THEN("Sliced successfully") {
+            REQUIRE(! gcode.empty());
+        }
+        THEN("T3 toolchange command found") {
+            bool T1_found = gcode.find("\nT3\n") != gcode.npos;
+            REQUIRE(T1_found);
+        }
+    }
+    WHEN("Slicing with multiple skirts with a single, non-zero extruder") {
+        std::string gcode = Slic3r::Test::slice({ Slic3r::Test::TestMesh::cube_20x20x20 }, 
+            {
+                { "nozzle_diameter",                        "0.6, 0.6, 0.6, 0.6" },
+                { "perimeter_extruder",                     2 },
+                { "infill_extruder",                        2 },
+                { "support_material_extruder",              2 },
+                { "support_material_interface_extruder",    2 },
+            });
+        THEN("Sliced successfully") {
+            REQUIRE(! gcode.empty());
+        }
+    }
+}
+
+SCENARIO("Ooze prevention", "[Multi]")
+{
+    DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config_with({
+        { "nozzle_diameter",                "0.6, 0.6, 0.6, 0.6" },
+        { "raft_layers",                    2 },
+        { "infill_extruder",                2 },
+        { "solid_infill_extruder",          3 },
+        { "support_material_extruder",      4 },
+        { "ooze_prevention",                1 },
+        { "extruder_offset",                "0x0, 20x0, 0x20, 20x20" },
+        { "temperature",                    "200, 180, 170, 160" },
+        { "first_layer_temperature",        "206, 186, 166, 156" },
+        // test that it doesn't crash when this is supplied
+        { "toolchange_gcode",               "T[next_extruder] ;toolchange" }
+    });
+    FullPrintConfig print_config;
+    print_config.apply(config);
+
+    // Since July 2019, PrusaSlicer only emits automatic Tn command in case that the toolchange_gcode is empty
+    // The "T[next_extruder]" is therefore needed in this test.
+    
+    std::string gcode = Slic3r::Test::slice({ Slic3r::Test::TestMesh::cube_20x20x20 }, config);
+
+    GCodeReader parser;
+    int         tool = -1;
+    int         tool_temp[] = { 0, 0, 0, 0};
+    Points      toolchange_points;
+    Points      extrusion_points;
+    parser.parse_buffer(gcode, [&tool, &tool_temp, &toolchange_points, &extrusion_points, &print_config]
+        (Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line)
+    {
+        // if the command is a T command, set the the current tool
+        if (boost::starts_with(line.cmd(), "T")) {
+            // Ignore initial toolchange.
+            if (tool != -1) {
+                int expected_temp = is_approx<double>(self.z(), print_config.get_abs_value("first_layer_height") + print_config.z_offset) ?
+                    print_config.first_layer_temperature.get_at(tool) :
+                    print_config.temperature.get_at(tool);
+                if (tool_temp[tool] != expected_temp + print_config.standby_temperature_delta)
+                    throw std::runtime_error("Standby temperature was not set before toolchange.");
+                toolchange_points.emplace_back(self.xy_scaled());
+            }
+            tool = atoi(line.cmd().data() + 1);
+        } else if (line.cmd_is("M104") || line.cmd_is("M109")) {
+            // May not be defined on this line.
+            int t = tool;
+            line.has_value('T', t);
+            // Should be available on this line.
+            int s;
+            if (! line.has_value('S', s))
+                throw std::runtime_error("M104 or M109 without S");
+            if (tool_temp[t] == 0 && s != print_config.first_layer_temperature.get_at(t) + print_config.standby_temperature_delta)
+                throw std::runtime_error("initial temperature is not equal to first layer temperature + standby delta");
+            tool_temp[t] = s;
+        } else if (line.cmd_is("G1") && line.extruding(self) && line.dist_XY(self) > 0) {
+            extrusion_points.emplace_back(line.new_XY_scaled(self) + scaled<coord_t>(print_config.extruder_offset.get_at(tool)));
+        }
+    });
+
+    Polygon convex_hull = Geometry::convex_hull(extrusion_points);
+    
+    THEN("all nozzles are outside skirt at toolchange") {
+        Points t;
+        sort_remove_duplicates(toolchange_points);
+        size_t inside = 0;
+        for (const auto &point : toolchange_points)
+            for (const Vec2d &offset : print_config.extruder_offset.values) {
+                Point p = point + scaled<coord_t>(offset);
+                if (convex_hull.contains(p))
+                    ++ inside;
+            }
+        REQUIRE(inside == 0);
+    }
+
+#if 0
+    require "Slic3r/SVG.pm";
+    Slic3r::SVG::output(
+        "ooze_prevention_test.svg",
+        no_arrows   => 1,
+        polygons    => [$convex_hull],
+        red_points  => \@t,
+        points      => \@toolchange_points,
+    );
+#endif
+    
+    THEN("all toolchanges happen within expected area") {
+        // offset the skirt by the maximum displacement between extruders plus a safety extra margin
+        const float delta = scaled<float>(20. * sqrt(2.) + 1.);
+        Polygon outer_convex_hull = expand(convex_hull, delta).front();
+        size_t inside = std::count_if(toolchange_points.begin(), toolchange_points.end(), [&outer_convex_hull](const Point &p){ return outer_convex_hull.contains(p); });
+        REQUIRE(inside == toolchange_points.size());
+    }
+}
+
+std::string slice_stacked_cubes(const DynamicPrintConfig &config, const DynamicPrintConfig &volume1config, const DynamicPrintConfig &volume2config)
+{
+    Model        model;
+    ModelObject *object = model.add_object();
+    object->name = "object.stl";
+    ModelVolume *v1 = object->add_volume(Test::mesh(Test::TestMesh::cube_20x20x20));
+    v1->set_material_id("lower_material");
+    v1->config.assign_config(volume1config);
+    ModelVolume *v2 = object->add_volume(Test::mesh(Test::TestMesh::cube_20x20x20));
+    v2->set_material_id("upper_material");
+    v2->translate(0., 0., 20.);
+    v2->config.assign_config(volume2config);
+    object->add_instance();
+    object->ensure_on_bed();
+    Print print;
+    print.auto_assign_extruders(object);
+    THEN("auto_assign_extruders() assigned correct extruder to first volume") {
+        REQUIRE(v1->config.extruder() == 1);
+    }
+    THEN("auto_assign_extruders() assigned correct extruder to second volume") {
+        REQUIRE(v2->config.extruder() == 2);
+    }
+    print.apply(model, config);
+    print.validate();
+    return Test::gcode(print);
+}
+
+SCENARIO("Stacked cubes", "[Multi]")
+{
+    DynamicPrintConfig lower_config;
+    lower_config.set_deserialize_strict({
+        { "extruder",               1 },
+        { "bottom_solid_layers",    0 },
+        { "top_solid_layers",       1 },
+    });
+
+    DynamicPrintConfig upper_config;
+    upper_config.set_deserialize_strict({
+        { "extruder",               2 },
+        { "bottom_solid_layers",    1 },
+        { "top_solid_layers",       0 }
+    });
+
+    static constexpr const double solid_infill_speed = 99;
+    auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
+        { "nozzle_diameter",        "0.6, 0.6, 0.6, 0.6" },
+        { "fill_density",           0 },
+        { "solid_infill_speed",     solid_infill_speed },
+        { "top_solid_infill_speed", solid_infill_speed },
+        // for preventing speeds from being altered
+        { "cooling",                "0, 0, 0, 0" },
+        // for preventing speeds from being altered
+        { "first_layer_speed",      "100%" }
+    });
+
+    auto test_shells = [](const std::string &gcode) {
+        GCodeReader       parser;
+        int               tool = -1;
+        // Scaled Z heights.
+        std::set<coord_t> T0_shells, T1_shells;
+        parser.parse_buffer(gcode, [&tool, &T0_shells, &T1_shells]
+            (Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line)
+        {
+            if (boost::starts_with(line.cmd(), "T")) {
+                tool = atoi(line.cmd().data() + 1);
+            } else if (line.cmd() == "G1" && line.extruding(self) && line.dist_XY(self) > 0) {
+                if (is_approx<double>(line.new_F(self), solid_infill_speed * 60.) && (tool == 0 || tool == 1))
+                    (tool == 0 ? T0_shells : T1_shells).insert(scaled<coord_t>(self.z()));
+            }
+        });
+        return std::make_pair(T0_shells, T1_shells);
+    };
+    
+    WHEN("Interface shells disabled") {
+        std::string gcode = slice_stacked_cubes(config, lower_config, upper_config);
+        auto [t0, t1] = test_shells(gcode);
+        THEN("no interface shells") {
+            REQUIRE(t0.empty());
+            REQUIRE(t1.empty());
+        }
+    }
+    WHEN("Interface shells enabled") {
+        config.set_deserialize_strict("interface_shells", "1");
+        std::string gcode = slice_stacked_cubes(config, lower_config, upper_config);
+        auto [t0, t1] = test_shells(gcode);
+        THEN("top interface shells") {
+            REQUIRE(t0.size() == lower_config.opt_int("top_solid_layers"));
+        }
+        THEN("bottom interface shells") {
+            REQUIRE(t1.size() == upper_config.opt_int("bottom_solid_layers"));
+        }
+    }
+    WHEN("Slicing with auto-assigned extruders") {
+        auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
+            { "nozzle_diameter",        "0.6,0.6,0.6,0.6" },
+            { "layer_height",           0.4 },
+            { "first_layer_height",     0.4 },
+            { "skirts",                 0 }
+        });
+        std::string gcode = slice_stacked_cubes(config, DynamicPrintConfig{}, DynamicPrintConfig{});
+        GCodeReader       parser;
+        int               tool = -1;
+        // Scaled Z heights.
+        std::set<coord_t> T0_shells, T1_shells;
+        parser.parse_buffer(gcode, [&tool, &T0_shells, &T1_shells](Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line)
+        {
+            if (boost::starts_with(line.cmd(), "T")) {
+                tool = atoi(line.cmd().data() + 1);
+            } else if (line.cmd() == "G1" && line.extruding(self) && line.dist_XY(self) > 0) {
+                if (tool == 0 && self.z() > 20)
+                    // Layers incorrectly extruded with T0 at the top object.
+                    T0_shells.insert(scaled<coord_t>(self.z()));
+                else if (tool == 1 && self.z() < 20)
+                    // Layers incorrectly extruded with T1 at the bottom object.
+                    T1_shells.insert(scaled<coord_t>(self.z()));
+            }
+        });
+        THEN("T0 is never used for upper object") {
+            REQUIRE(T0_shells.empty());
+        }
+        THEN("T0 is never used for lower object") {
+            REQUIRE(T1_shells.empty());
+        }
+    }
+}
diff --git a/tests/fff_print/test_perimeters.cpp b/tests/fff_print/test_perimeters.cpp
new file mode 100644
index 000000000..a3f11113a
--- /dev/null
+++ b/tests/fff_print/test_perimeters.cpp
@@ -0,0 +1,599 @@
+#include <catch2/catch.hpp>
+
+#include <numeric>
+#include <sstream>
+
+#include "libslic3r/Config.hpp"
+#include "libslic3r/ClipperUtils.hpp"
+#include "libslic3r/Layer.hpp"
+#include "libslic3r/PerimeterGenerator.hpp"
+#include "libslic3r/Print.hpp"
+#include "libslic3r/PrintConfig.hpp"
+#include "libslic3r/SurfaceCollection.hpp"
+#include "libslic3r/libslic3r.h"
+
+#include "test_data.hpp"
+
+using namespace Slic3r;
+
+SCENARIO("Perimeter nesting", "[Perimeters]")
+{
+    struct TestData {
+        ExPolygons          expolygons;
+        // expected number of loops
+        int                 total;
+        // expected number of external loops
+        int                 external;
+        // expected external perimeter
+        std::vector<bool>   ext_order;
+        // expected number of internal contour loops
+        int                 cinternal;
+        // expected number of ccw loops
+        int                 ccw;
+        // expected ccw/cw order
+        std::vector<bool>   ccw_order;
+        // expected nesting order
+        std::vector<std::vector<int>> nesting;
+    };
+
+    FullPrintConfig config;
+
+    auto test = [&config](const TestData &data) {
+        SurfaceCollection slices;
+        slices.append(data.expolygons, stInternal);
+        
+        ExtrusionEntityCollection loops;
+        ExtrusionEntityCollection gap_fill;
+        SurfaceCollection         fill_surfaces;
+        PerimeterGenerator        perimeter_generator(
+            &slices, 
+            1., // layer height
+            Flow(1., 1., 1.),
+            static_cast<const PrintRegionConfig*>(&config),
+            static_cast<const PrintObjectConfig*>(&config),
+            static_cast<const PrintConfig*>(&config),
+            false, // spiral_vase
+            // output:
+            &loops, &gap_fill, &fill_surfaces);
+        perimeter_generator.process();
+
+        THEN("expected number of collections") {
+            REQUIRE(loops.entities.size() == data.expolygons.size());
+        }        
+        
+        loops = loops.flatten();
+        THEN("expected number of loops") {
+            REQUIRE(loops.entities.size() == data.total);
+        }
+        THEN("expected number of external loops") {
+            size_t num_external = std::count_if(loops.entities.begin(), loops.entities.end(), 
+                [](const ExtrusionEntity *ee){ return ee->role() == erExternalPerimeter; });
+            REQUIRE(num_external == data.external);
+        }
+        THEN("expected external order") {
+            std::vector<bool> ext_order;
+            for (auto *ee : loops.entities)
+                ext_order.emplace_back(ee->role() == erExternalPerimeter);
+            REQUIRE(ext_order == data.ext_order);
+        }
+        THEN("expected number of internal contour loops") {
+            size_t cinternal = std::count_if(loops.entities.begin(), loops.entities.end(), 
+                [](const ExtrusionEntity *ee){ return dynamic_cast<const ExtrusionLoop*>(ee)->loop_role() == elrContourInternalPerimeter; });
+            REQUIRE(cinternal == data.cinternal);
+        }
+        THEN("expected number of ccw loops") {
+            size_t ccw = std::count_if(loops.entities.begin(), loops.entities.end(), 
+                [](const ExtrusionEntity *ee){ return dynamic_cast<const ExtrusionLoop*>(ee)->polygon().is_counter_clockwise(); });
+            REQUIRE(ccw == data.ccw);
+        }
+        THEN("expected ccw/cw order") {
+            std::vector<bool> ccw_order;
+            for (auto *ee : loops.entities)
+                ccw_order.emplace_back(dynamic_cast<const ExtrusionLoop*>(ee)->polygon().is_counter_clockwise());
+            REQUIRE(ccw_order == data.ccw_order);
+        }
+        THEN("expected nesting order") {
+            for (const std::vector<int> &nesting : data.nesting) {
+                for (size_t i = 1; i < nesting.size(); ++ i)
+                    REQUIRE(dynamic_cast<const ExtrusionLoop*>(loops.entities[nesting[i - 1]])->polygon().contains(loops.entities[nesting[i]]->first_point()));
+            }
+        }
+    };
+
+    WHEN("Rectangle") {
+        config.perimeters.value = 3;
+        TestData data;
+        data.expolygons  = { 
+            ExPolygon{ Polygon::new_scale({ {0,0}, {100,0}, {100,100}, {0,100} }) }
+        };
+        data.total       = 3;
+        data.external    = 1;
+        data.ext_order   = { false, false, true };
+        data.cinternal   = 1;
+        data.ccw         = 3;
+        data.ccw_order   = { true, true, true };
+        data.nesting     = { { 2, 1, 0 } };
+        test(data);
+    }
+    WHEN("Rectangle with hole") {
+        config.perimeters.value = 3;
+        TestData data;
+        data.expolygons  = { 
+            ExPolygon{ Polygon::new_scale({ {0,0}, {100,0}, {100,100}, {0,100} }), 
+                       Polygon::new_scale({ {40,40}, {40,60}, {60,60}, {60,40} }) } 
+        };
+        data.total       = 6;
+        data.external    = 2;
+        data.ext_order   = { false, false, true, false, false, true };
+        data.cinternal   = 1;
+        data.ccw         = 3;
+        data.ccw_order   = { false, false, false, true, true, true };
+        data.nesting     = { { 5, 4, 3, 0, 1, 2 } };
+        test(data);
+    }
+    WHEN("Nested rectangles with holes") {
+        config.perimeters.value = 3;
+        TestData data;
+        data.expolygons  = {
+            ExPolygon{ Polygon::new_scale({ {0,0}, {200,0}, {200,200}, {0,200} }), 
+                       Polygon::new_scale({ {20,20}, {20,180}, {180,180}, {180,20} }) },
+            ExPolygon{ Polygon::new_scale({ {50,50}, {150,50}, {150,150}, {50,150} }), 
+                       Polygon::new_scale({ {80,80}, {80,120}, {120,120}, {120,80} }) }
+        };
+        data.total       = 4*3;
+        data.external    = 4;
+        data.ext_order   = { false, false, true, false, false, true, false, false, true, false, false, true };
+        data.cinternal   = 2;
+        data.ccw         = 2*3;
+        data.ccw_order   = { false, false, false, true, true, true, false, false, false, true, true, true };
+        test(data);
+    }
+    WHEN("Rectangle with multiple holes") {
+        config.perimeters.value = 2;
+        TestData data;
+        ExPolygon expoly{ Polygon::new_scale({ {0,0}, {50,0}, {50,50}, {0,50} }) };
+        expoly.holes.emplace_back(Polygon::new_scale({ {7.5,7.5},  {7.5,12.5},  {12.5,12.5}, {12.5,7.5}  }));
+        expoly.holes.emplace_back(Polygon::new_scale({ {7.5,17.5}, {7.5,22.5},  {12.5,22.5}, {12.5,17.5} }));
+        expoly.holes.emplace_back(Polygon::new_scale({ {7.5,27.5}, {7.5,32.5},  {12.5,32.5}, {12.5,27.5} }));
+        expoly.holes.emplace_back(Polygon::new_scale({ {7.5,37.5}, {7.5,42.5},  {12.5,42.5}, {12.5,37.5} }));
+        expoly.holes.emplace_back(Polygon::new_scale({ {17.5,7.5}, {17.5,12.5}, {22.5,12.5}, {22.5,7.5}  }));
+        data.expolygons  = { expoly };
+        data.total       = 12;
+        data.external    = 6;
+        data.ext_order   = { false, true, false, true, false, true, false, true, false, true, false, true };
+        data.cinternal   = 1;
+        data.ccw         = 2;
+        data.ccw_order   = { false, false, false, false, false, false, false, false, false, false, true, true };
+        data.nesting     = { {0,1},{2,3},{4,5},{6,7},{8,9} };
+        test(data);
+    };
+}
+
+SCENARIO("Perimeters", "[Perimeters]")
+{
+    auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
+        { "skirts",                 0 },
+        { "fill_density",           0 },
+        { "perimeters",             3 },
+        { "top_solid_layers",       0 },
+        { "bottom_solid_layers",    0 },
+        // to prevent speeds from being altered
+        { "cooling",                "0" },
+        // to prevent speeds from being altered
+        { "first_layer_speed",      "100%" }
+    });
+
+    WHEN("Bridging perimeters disabled") {
+        std::string gcode = Slic3r::Test::slice({ Slic3r::Test::TestMesh::overhang }, config);
+
+        THEN("all perimeters extruded ccw") {
+            GCodeReader parser;
+            bool        has_cw_loops = false;
+            Polygon     current_loop;
+            parser.parse_buffer(gcode, [&has_cw_loops, &current_loop](Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line)
+            {
+                if (line.extruding(self) && line.dist_XY(self) > 0) {
+                    if (current_loop.empty())
+                        current_loop.points.emplace_back(self.xy_scaled());
+                    current_loop.points.emplace_back(line.new_XY_scaled(self));
+                } else if (! line.cmd_is("M73")) {
+                    // skips remaining time lines (M73)
+                    if (! current_loop.empty() && current_loop.is_clockwise())
+                        has_cw_loops = true;
+                    current_loop.clear();
+                }
+            });
+            REQUIRE(! has_cw_loops);
+        }
+    }
+    
+    auto test = [&config](Test::TestMesh model) {    
+        // we test two copies to make sure ExtrusionLoop objects are not modified in-place (the second object would not detect cw loops and thus would calculate wrong)
+        std::string gcode = Slic3r::Test::slice({ model, model }, config);
+        GCodeReader parser;
+        bool        has_cw_loops = false;
+        bool        has_outwards_move = false;
+        bool        starts_on_convex_point = false;
+        // print_z => count of external loops
+        std::map<coord_t, int> external_loops;
+        Polygon     current_loop;
+        const double external_perimeter_speed = config.get_abs_value("external_perimeter_speed") * 60.;
+        parser.parse_buffer(gcode, [&has_cw_loops, &has_outwards_move, &starts_on_convex_point, &external_loops, &current_loop, external_perimeter_speed, model]
+            (Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line)
+        {
+            if (line.extruding(self) && line.dist_XY(self) > 0) {
+                if (current_loop.empty())
+                    current_loop.points.emplace_back(self.xy_scaled());
+                current_loop.points.emplace_back(line.new_XY_scaled(self));
+            } else if (! line.cmd_is("M73")) {
+                // skips remaining time lines (M73)
+                if (! current_loop.empty()) {
+                    if (current_loop.is_clockwise())
+                        has_cw_loops = true;
+                    if (is_approx<double>(self.f(), external_perimeter_speed)) {
+                        // reset counter for second object
+                        coord_t z = scaled<coord_t>(self.z());
+                        auto it = external_loops.find(z);
+                        if (it == external_loops.end())
+                            it = external_loops.insert(std::make_pair(z, 0)).first;
+                        else if (it->second == 2)
+                            it->second = 0;
+                        ++ it->second;
+                        bool is_contour          = it->second == 2;
+                        bool is_hole             = it->second == 1;
+                        // Testing whether the move point after loop ends up inside the extruded loop.
+                        bool loop_contains_point = current_loop.contains(line.new_XY_scaled(self));
+                        if (// contour should include destination
+                            (! loop_contains_point && is_contour) ||
+                            // hole should not
+                            (loop_contains_point && is_hole))
+                            has_outwards_move = true;
+                        if (model == Test::TestMesh::cube_with_concave_hole) {
+                            // check that loop starts at a concave vertex
+                            double cross = cross2((current_loop.points.front() - current_loop.points[current_loop.points.size() - 2]).cast<double>(), (current_loop.points[1] - current_loop.points.front()).cast<double>());
+                            bool   convex = cross > 0.;
+                            if ((convex && is_contour) || (! convex && is_hole))
+                                starts_on_convex_point = true;
+                        }
+                    }
+                    current_loop.clear();
+                }
+            }
+        });
+        THEN("all perimeters extruded ccw") {
+            REQUIRE(! has_cw_loops);
+        }
+        THEN("move inwards after completing external loop") {
+            REQUIRE(! has_outwards_move);
+        }
+        THEN("loops start on concave point if any") {
+            REQUIRE(! starts_on_convex_point);
+        }
+
+    };
+    // Reusing the config above.
+    config.set_deserialize_strict({
+        { "external_perimeter_speed", 68 }
+    });
+    GIVEN("Cube with hole") { test(Test::TestMesh::cube_with_hole); }
+    GIVEN("Cube with concave hole") { test(Test::TestMesh::cube_with_concave_hole); }
+    
+    WHEN("Bridging perimeters enabled") {
+        // Reusing the config above.
+        config.set_deserialize_strict({
+            { "perimeters",                 1 },
+            { "perimeter_speed",            77 },
+            { "external_perimeter_speed",   66 },
+            { "bridge_speed",               99 },
+            { "cooling",                    "1" },
+            { "fan_below_layer_time",       "0" },
+            { "slowdown_below_layer_time",  "0" },
+            { "bridge_fan_speed",           "100" },
+            // arbitrary value
+            { "bridge_flow_ratio",          33 },
+            { "overhangs",                  true }
+        });
+    
+        std::string gcode = Slic3r::Test::slice({ mesh(Slic3r::Test::TestMesh::overhang) }, config);
+
+        THEN("Bridging is applied to bridging perimeters") {
+            GCodeReader  parser;
+            // print Z => speeds
+            std::map<coord_t, std::set<double>> layer_speeds;
+            int          fan_speed = 0;
+            const double perimeter_speed            = config.opt_float("perimeter_speed") * 60.;
+            const double external_perimeter_speed   = config.get_abs_value("external_perimeter_speed") * 60.;
+            const double bridge_speed               = config.opt_float("bridge_speed") * 60.;
+            const double nozzle_dmr                 = config.opt<ConfigOptionFloats>("nozzle_diameter")->get_at(0);
+            const double filament_dmr               = config.opt<ConfigOptionFloats>("filament_diameter")->get_at(0);
+            const double bridge_mm_per_mm           = sqr(nozzle_dmr / filament_dmr) * config.opt_float("bridge_flow_ratio");
+            parser.parse_buffer(gcode, [&layer_speeds, &fan_speed, perimeter_speed, external_perimeter_speed, bridge_speed, nozzle_dmr, filament_dmr, bridge_mm_per_mm]
+                (Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line)
+            {
+                if (line.cmd_is("M107"))
+                    fan_speed = 0;
+                else if (line.cmd_is("M106"))
+                    line.has_value('S', fan_speed);
+                else if (line.extruding(self) && line.dist_XY(self) > 0) {
+                    double feedrate = line.new_F(self);
+                    REQUIRE((is_approx(feedrate, perimeter_speed) || is_approx(feedrate, external_perimeter_speed) || is_approx(feedrate, bridge_speed)));
+                    layer_speeds[self.z()].insert(feedrate);
+                    bool   bridging  = is_approx(feedrate, bridge_speed);
+                    double mm_per_mm = line.dist_E(self) / line.dist_XY(self);
+                    // Fan enabled at full speed when bridging, disabled when not bridging.
+                    REQUIRE((! bridging || fan_speed == 255));
+                    REQUIRE((bridging || fan_speed == 0));
+                    // When bridging, bridge flow is applied.
+                    REQUIRE((! bridging || std::abs(mm_per_mm - bridge_mm_per_mm) <= 0.01));
+                }
+            });
+            // only overhang layer has more than one speed
+            size_t num_overhangs = std::count_if(layer_speeds.begin(), layer_speeds.end(), [](const std::pair<double, std::set<double>> &v){ return v.second.size() > 1; });
+            REQUIRE(num_overhangs == 1);
+        }
+    }
+
+    GIVEN("iPad stand") {
+        WHEN("Extra perimeters enabled") {
+            auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
+                { "skirts",                     0 },
+                { "perimeters",                 3 },
+                { "layer_height",               0.4 },
+                { "first_layer_height",         0.35 },
+                { "extra_perimeters",           1 },
+                // to prevent speeds from being altered
+                { "cooling",                    "0" },
+                // to prevent speeds from being altered
+                { "first_layer_speed",          "100%" },
+                { "perimeter_speed",            99 },
+                { "external_perimeter_speed",   99 },
+                { "small_perimeter_speed",      99 },
+                { "thin_walls",                 0 },
+            });
+        
+            std::string gcode = Slic3r::Test::slice({ Slic3r::Test::TestMesh::ipadstand }, config);
+            // z => number of loops
+            std::map<coord_t, int> perimeters;
+            bool                   in_loop         = false;
+            const double           perimeter_speed = config.opt_float("perimeter_speed") * 60.;
+            GCodeReader            parser;
+            parser.parse_buffer(gcode, [&perimeters, &in_loop, perimeter_speed](Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line)
+            {
+                if (line.extruding(self) && line.dist_XY(self) > 0 && is_approx<double>(line.new_F(self), perimeter_speed)) {
+                    if (! in_loop) {
+                        coord_t z = scaled<coord_t>(self.z());
+                        auto it = perimeters.find(z);
+                        if (it == perimeters.end())
+                            it = perimeters.insert(std::make_pair(z, 0)).first;
+                        ++ it->second;
+                    }
+                    in_loop = true;
+                } else if (! line.cmd_is("M73")) {
+                    // skips remaining time lines (M73)
+                    in_loop = false;
+                }
+            });
+            THEN("no superfluous extra perimeters") {
+                const int num_perimeters = config.opt_int("perimeters");
+                size_t extra_perimeters = std::count_if(perimeters.begin(), perimeters.end(), [num_perimeters](const std::pair<const coord_t, int> &v){ return (v.second % num_perimeters) > 0; });
+                REQUIRE(extra_perimeters == 0);
+            }
+        }
+    }
+}
+
+SCENARIO("Some weird coverage test", "[Perimeters]")
+{
+    auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
+        { "nozzle_diameter",                    "0.4" },
+        { "perimeters",                         2 },
+        { "perimeter_extrusion_width",          0.4 },
+        { "external_perimeter_extrusion_width", 0.4 },
+        { "infill_extrusion_width",             0.53 },
+        { "solid_infill_extrusion_width",       0.53 }
+    });
+
+    // we just need a pre-filled Print object
+    Print print;
+    Model model;
+    Slic3r::Test::init_print({ Test::TestMesh::cube_20x20x20 }, print, model, config);
+    
+    // override a layer's slices
+    ExPolygon expolygon;
+    expolygon.contour = {
+        {-71974463,-139999376},{-71731792,-139987456},{-71706544,-139985616},{-71682119,-139982639},{-71441248,-139946912},{-71417487,-139942895},{-71379384,-139933984},{-71141800,-139874480},
+        {-71105247,-139862895},{-70873544,-139779984},{-70838592,-139765856},{-70614943,-139660064},{-70581783,-139643567},{-70368368,-139515680},{-70323751,-139487872},{-70122160,-139338352},
+        {-70082399,-139306639},{-69894800,-139136624},{-69878679,-139121327},{-69707992,-138933008},{-69668575,-138887343},{-69518775,-138685359},{-69484336,-138631632},{-69356423,-138418207},
+        {-69250040,-138193296},{-69220920,-138128976},{-69137992,-137897168},{-69126095,-137860255},{-69066568,-137622608},{-69057104,-137582511},{-69053079,-137558751},{-69017352,-137317872},
+        {-69014392,-137293456},{-69012543,-137268207},{-68999369,-137000000},{-63999999,-137000000},{-63705947,-136985551},{-63654984,-136977984},{-63414731,-136942351},{-63364756,-136929840},
+        {-63129151,-136870815},{-62851950,-136771631},{-62585807,-136645743},{-62377483,-136520895},{-62333291,-136494415},{-62291908,-136463728},{-62096819,-136319023},{-62058644,-136284432},
+        {-61878676,-136121328},{-61680968,-135903184},{-61650275,-135861807},{-61505591,-135666719},{-61354239,-135414191},{-61332211,-135367615},{-61228359,-135148063},{-61129179,-134870847},
+        {-61057639,-134585262},{-61014451,-134294047},{-61000000,-134000000},{-61000000,-107999999},{-61014451,-107705944},{-61057639,-107414736},{-61129179,-107129152},{-61228359,-106851953},
+        {-61354239,-106585808},{-61505591,-106333288},{-61680967,-106096816},{-61878675,-105878680},{-62096820,-105680967},{-62138204,-105650279},{-62333292,-105505591},{-62585808,-105354239},
+        {-62632384,-105332207},{-62851951,-105228360},{-62900463,-105211008},{-63129152,-105129183},{-63414731,-105057640},{-63705947,-105014448},{-63999999,-105000000},{-68999369,-105000000},
+        {-69012543,-104731792},{-69014392,-104706544},{-69017352,-104682119},{-69053079,-104441248},{-69057104,-104417487},{-69066008,-104379383},{-69125528,-104141799},{-69137111,-104105248},
+        {-69220007,-103873544},{-69234136,-103838591},{-69339920,-103614943},{-69356415,-103581784},{-69484328,-103368367},{-69512143,-103323752},{-69661647,-103122160},{-69693352,-103082399},
+        {-69863383,-102894800},{-69878680,-102878679},{-70066999,-102707992},{-70112656,-102668576},{-70314648,-102518775},{-70368367,-102484336},{-70581783,-102356424},{-70806711,-102250040},
+        {-70871040,-102220919},{-71102823,-102137992},{-71139752,-102126095},{-71377383,-102066568},{-71417487,-102057104},{-71441248,-102053079},{-71682119,-102017352},{-71706535,-102014392},
+        {-71731784,-102012543},{-71974456,-102000624},{-71999999,-102000000},{-104000000,-102000000},{-104025536,-102000624},{-104268207,-102012543},{-104293455,-102014392},
+        {-104317880,-102017352},{-104558751,-102053079},{-104582512,-102057104},{-104620616,-102066008},{-104858200,-102125528},{-104894751,-102137111},{-105126455,-102220007},
+        {-105161408,-102234136},{-105385056,-102339920},{-105418215,-102356415},{-105631632,-102484328},{-105676247,-102512143},{-105877839,-102661647},{-105917600,-102693352},
+        {-106105199,-102863383},{-106121320,-102878680},{-106292007,-103066999},{-106331424,-103112656},{-106481224,-103314648},{-106515663,-103368367},{-106643575,-103581783},
+        {-106749959,-103806711},{-106779080,-103871040},{-106862007,-104102823},{-106873904,-104139752},{-106933431,-104377383},{-106942896,-104417487},{-106946920,-104441248},
+        {-106982648,-104682119},{-106985607,-104706535},{-106987456,-104731784},{-107000630,-105000000},{-112000000,-105000000},{-112294056,-105014448},{-112585264,-105057640},
+        {-112870848,-105129184},{-112919359,-105146535},{-113148048,-105228360},{-113194624,-105250392},{-113414191,-105354239},{-113666711,-105505591},{-113708095,-105536279},
+        {-113903183,-105680967},{-114121320,-105878679},{-114319032,-106096816},{-114349720,-106138200},{-114494408,-106333288},{-114645760,-106585808},{-114667792,-106632384},
+        {-114771640,-106851952},{-114788991,-106900463},{-114870815,-107129151},{-114942359,-107414735},{-114985551,-107705943},{-115000000,-107999999},{-115000000,-134000000},
+        {-114985551,-134294048},{-114942359,-134585263},{-114870816,-134870847},{-114853464,-134919359},{-114771639,-135148064},{-114645759,-135414192},{-114494407,-135666720},
+        {-114319031,-135903184},{-114121320,-136121327},{-114083144,-136155919},{-113903184,-136319023},{-113861799,-136349712},{-113666711,-136494416},{-113458383,-136619264},
+        {-113414192,-136645743},{-113148049,-136771631},{-112870848,-136870815},{-112820872,-136883327},{-112585264,-136942351},{-112534303,-136949920},{-112294056,-136985551},
+        {-112000000,-137000000},{-107000630,-137000000},{-106987456,-137268207},{-106985608,-137293440},{-106982647,-137317872},{-106946920,-137558751},{-106942896,-137582511},
+        {-106933991,-137620624},{-106874471,-137858208},{-106862888,-137894751},{-106779992,-138126463},{-106765863,-138161424},{-106660080,-138385055},{-106643584,-138418223},
+        {-106515671,-138631648},{-106487855,-138676256},{-106338352,-138877839},{-106306647,-138917600},{-106136616,-139105199},{-106121320,-139121328},{-105933000,-139291999},
+        {-105887344,-139331407},{-105685351,-139481232},{-105631632,-139515663},{-105418216,-139643567},{-105193288,-139749951},{-105128959,-139779072},{-104897175,-139862016},
+        {-104860247,-139873904},{-104622616,-139933423},{-104582511,-139942896},{-104558751,-139946912},{-104317880,-139982656},{-104293463,-139985616},{-104268216,-139987456},
+        {-104025544,-139999376},{-104000000,-140000000},{-71999999,-140000000}
+    };
+    expolygon.holes = {
+        {{-105000000,-138000000},{-105000000,-104000000},{-71000000,-104000000},{-71000000,-138000000}},
+        {{-69000000,-132000000},{-69000000,-110000000},{-64991180,-110000000},{-64991180,-132000000}},
+        {{-111008824,-132000000},{-111008824,-110000000},{-107000000,-110000000},{-107000000,-132000000}}
+    };
+    PrintObject *object = print.get_object(0);
+    object->slice();
+    Layer       *layer = object->get_layer(1);
+    LayerRegion *layerm = layer->get_region(0);
+    layerm->slices.clear();
+    layerm->slices.append({ expolygon }, stInternal);
+    
+    // make perimeters
+    layer->make_perimeters();
+    
+    // compute the covered area
+    Flow pflow = layerm->flow(frPerimeter);
+    Flow iflow = layerm->flow(frInfill);
+    Polygons covered_by_perimeters;
+    Polygons covered_by_infill;
+    {
+        Polygons acc;
+        for (const ExtrusionEntity *ee : layerm->perimeters.entities)
+            for (const ExtrusionEntity *ee : dynamic_cast<const ExtrusionEntityCollection*>(ee)->entities)
+                append(acc, offset(dynamic_cast<const ExtrusionLoop*>(ee)->polygon().split_at_first_point(), float(pflow.scaled_width() / 2.f + SCALED_EPSILON)));
+        covered_by_perimeters = union_(acc);
+    }
+    {
+        Polygons acc;
+        for (const Surface &surface : layerm->fill_surfaces.surfaces)
+            append(acc, to_polygons(surface.expolygon));
+        for (const ExtrusionEntity *ee : layerm->thin_fills.entities)
+            append(acc, offset(dynamic_cast<const ExtrusionPath*>(ee)->polyline, float(iflow.scaled_width() / 2.f + SCALED_EPSILON)));
+        covered_by_infill = union_(acc);
+    }
+    
+    // compute the non covered area
+    ExPolygons non_covered = diff_ex(to_polygons(layerm->slices.surfaces), union_(covered_by_perimeters, covered_by_infill));
+    
+    /*
+    if (0) {
+        printf "max non covered = %f\n", List::Util::max(map unscale unscale $_->area, @$non_covered);
+        require "Slic3r/SVG.pm";
+        Slic3r::SVG::output(
+            "gaps.svg",
+            expolygons          => [ map $_->expolygon, @{$layerm->slices} ],
+            red_expolygons      => union_ex([ map @$_, (@$covered_by_perimeters, @$covered_by_infill) ]),
+            green_expolygons    => union_ex($non_covered),
+            no_arrows           => 1,
+            polylines           => [
+                map $_->polygon->split_at_first_point, map @$_, @{$layerm->perimeters},
+            ],
+        );
+    }
+    */
+    THEN("no gap between perimeters and infill") {
+        size_t num_non_convered = std::count_if(non_covered.begin(), non_covered.end(), [&iflow](const ExPolygon &ex){ return ex.area() > sqr(double(iflow.scaled_width())); });
+        REQUIRE(num_non_convered == 0);
+    }
+}
+
+SCENARIO("Perimeters3", "[Perimeters]")
+{
+    auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
+        { "skirts",                 0 },
+        { "perimeters",             3 },
+        { "layer_height",           0.4 },
+        { "bridge_speed",           99 },
+        // to prevent bridging over sparse infill
+        { "fill_density",           0 },
+        { "overhangs",              true },
+        // to prevent speeds from being altered
+        { "cooling",                "0" },
+        // to prevent speeds from being altered
+        { "first_layer_speed",      "100%" }
+    });
+
+    auto test = [&config](const Vec3d &scale) {
+        std::string         gcode = Slic3r::Test::slice({ mesh(Slic3r::Test::TestMesh::V, Vec3d::Zero(), scale) }, config);
+        GCodeReader         parser;
+        std::set<coord_t>   z_with_bridges;
+        const double        bridge_speed = config.opt_float("bridge_speed") * 60.;
+        parser.parse_buffer(gcode, [&z_with_bridges, bridge_speed](Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line)
+        {
+            if (line.extruding(self) && line.dist_XY(self) > 0 && is_approx<double>(line.new_F(self), bridge_speed))
+                z_with_bridges.insert(scaled<coord_t>(self.z()));
+        });
+        return z_with_bridges.size();
+    };
+
+    GIVEN("V shape, unscaled") {
+        int n = test(Vec3d(1., 1., 1.));
+        // except for the two internal solid layers above void
+        THEN("no overhangs printed with bridge speed") {
+            REQUIRE(n == 1);
+        }
+    }
+    GIVEN("V shape, scaled 3x in X") {
+        int n = test(Vec3d(3., 1., 1.));
+        // except for the two internal solid layers above void
+        THEN("overhangs printed with bridge speed") {
+            REQUIRE(n > 2);
+        }
+    }
+}
+
+SCENARIO("Perimeters4", "[Perimeters]")
+{
+    auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
+        { "seam_position",        "random" }
+    });
+    std::string gcode = Slic3r::Test::slice({ Slic3r::Test::TestMesh::cube_20x20x20 }, config);
+    THEN("successful generation of G-code with seam_position = random") {
+        REQUIRE(! gcode.empty());
+    }
+}
+
+SCENARIO("Seam alignment", "[Perimeters]")
+{
+    auto test = [](Test::TestMesh model) {
+        auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
+            { "seam_position",          "aligned" },
+            { "skirts",                 0 },
+            { "perimeters",             1 },
+            { "fill_density",           0 },
+            { "top_solid_layers",       0 },
+            { "bottom_solid_layers",    0 },
+            { "retract_layer_change",   "0" }
+        });
+        std::string gcode = Slic3r::Test::slice({ model }, config);
+        bool        was_extruding = false;
+        Points      seam_points;
+        GCodeReader parser;
+        parser.parse_buffer(gcode, [&was_extruding, &seam_points](Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line)
+        {
+            if (line.extruding(self)) {
+                if (! was_extruding)
+                    seam_points.emplace_back(self.xy_scaled());
+                was_extruding = true;
+            } else if (! line.cmd_is("M73")) {
+                // skips remaining time lines (M73)
+                was_extruding = false;
+            }
+        });
+        THEN("seam is aligned") {
+            size_t num_not_aligned = 0;
+            for (size_t i = 1; i < seam_points.size(); ++ i) {
+                double d = (seam_points[i] - seam_points[i - 1]).cast<double>().norm();
+                // Seams shall be aligned up to 3mm.
+                if (d > scaled<double>(3.))
+                    ++ num_not_aligned;
+            }
+            REQUIRE(num_not_aligned == 0);
+        }
+    };
+
+    GIVEN("20mm cube") {
+        test(Slic3r::Test::TestMesh::cube_20x20x20);
+    }
+    GIVEN("small_dorito") {
+        test(Slic3r::Test::TestMesh::small_dorito);
+    }
+}
diff --git a/tests/fff_print/test_support_material.cpp b/tests/fff_print/test_support_material.cpp
index 442db7654..c5087263c 100644
--- a/tests/fff_print/test_support_material.cpp
+++ b/tests/fff_print/test_support_material.cpp
@@ -236,3 +236,262 @@ SCENARIO("SupportMaterial: Checking bridge speed", "[SupportMaterial]")
 }
 
 #endif
+
+
+/* 
+
+Old Perl tests, which were disabled by Vojtech at the time of first Support Generator refactoring.
+
+#if 0
+{
+    my $config = Slic3r::Config::new_from_defaults;
+    $config->set('support_material', 1);
+    my @contact_z = my @top_z = ();
+    
+    my $test = sub {
+        my $print = Slic3r::Test::init_print('20mm_cube', config => $config);
+        my $object_config = $print->print->objects->[0]->config;
+        my $flow = Slic3r::Flow->new_from_width(
+            width               => $object_config->support_material_extrusion_width || $object_config->extrusion_width,
+            role                => FLOW_ROLE_SUPPORT_MATERIAL,
+            nozzle_diameter     => $print->config->nozzle_diameter->[$object_config->support_material_extruder-1] // $print->config->nozzle_diameter->[0],
+            layer_height        => $object_config->layer_height,
+        );
+        my $support = Slic3r::Print::SupportMaterial->new(
+            object_config       => $print->print->objects->[0]->config,
+            print_config        => $print->print->config,
+            flow                => $flow,
+            interface_flow      => $flow,
+            first_layer_flow    => $flow,
+        );
+        my $support_z = $support->support_layers_z($print->print->objects->[0], \@contact_z, \@top_z, $config->layer_height);
+        my $expected_top_spacing = $support->contact_distance($config->layer_height, $config->nozzle_diameter->[0]);
+        
+        is $support_z->[0], $config->first_layer_height,
+            'first layer height is honored';
+        is scalar(grep { $support_z->[$_]-$support_z->[$_-1] <= 0 } 1..$#$support_z), 0,
+            'no null or negative support layers';
+        is scalar(grep { $support_z->[$_]-$support_z->[$_-1] > $config->nozzle_diameter->[0] + epsilon } 1..$#$support_z), 0,
+            'no layers thicker than nozzle diameter';
+        
+        my $wrong_top_spacing = 0;
+        foreach my $top_z (@top_z) {
+            # find layer index of this top surface
+            my $layer_id = first { abs($support_z->[$_] - $top_z) < epsilon } 0..$#$support_z;
+            
+            # check that first support layer above this top surface (or the next one) is spaced with nozzle diameter
+            $wrong_top_spacing = 1
+                if ($support_z->[$layer_id+1] - $support_z->[$layer_id]) != $expected_top_spacing
+                && ($support_z->[$layer_id+2] - $support_z->[$layer_id]) != $expected_top_spacing;
+        }
+        ok !$wrong_top_spacing, 'layers above top surfaces are spaced correctly';
+    };
+    
+    $config->set('layer_height', 0.2);
+    $config->set('first_layer_height', 0.3);
+    @contact_z = (1.9);
+    @top_z = (1.1);
+    $test->();
+    
+    $config->set('first_layer_height', 0.4);
+    $test->();
+    
+    $config->set('layer_height', $config->nozzle_diameter->[0]);
+    $test->();
+}
+
+{
+    my $config = Slic3r::Config::new_from_defaults;
+    $config->set('raft_layers', 3);
+    $config->set('brim_width',  0);
+    $config->set('skirts', 0);
+    $config->set('support_material_extruder', 2);
+    $config->set('support_material_interface_extruder', 2);
+    $config->set('layer_height', 0.4);
+    $config->set('first_layer_height', 0.4);
+    my $print = Slic3r::Test::init_print('overhang', config => $config);
+    ok my $gcode = Slic3r::Test::gcode($print), 'no conflict between raft/support and brim';
+    
+    my $tool = 0;
+    Slic3r::GCode::Reader->new->parse($gcode, sub {
+        my ($self, $cmd, $args, $info) = @_;
+        
+        if ($cmd =~ /^T(\d+)/) {
+            $tool = $1;
+        } elsif ($info->{extruding}) {
+            if ($self->Z <= ($config->raft_layers * $config->layer_height)) {
+                fail 'not extruding raft with support material extruder'
+                    if $tool != ($config->support_material_extruder-1);
+            } else {
+                fail 'support material exceeds raft layers'
+                    if $tool == $config->support_material_extruder-1;
+                # TODO: we should test that full support is generated when we use raft too
+            }
+        }
+    });
+}
+
+{
+    my $config = Slic3r::Config::new_from_defaults;
+    $config->set('skirts', 0);
+    $config->set('raft_layers', 3);
+    $config->set('support_material_pattern', 'honeycomb');
+    $config->set('support_material_extrusion_width', 0.6);
+    $config->set('first_layer_extrusion_width', '100%');
+    $config->set('bridge_speed', 99);
+    $config->set('cooling', [ 0 ]);             # prevent speed alteration
+    $config->set('first_layer_speed', '100%');  # prevent speed alteration
+    $config->set('start_gcode', '');            # prevent any unexpected Z move
+    my $print = Slic3r::Test::init_print('20mm_cube', config => $config);
+    
+    my $layer_id = -1;  # so that first Z move sets this to 0
+    my @raft = my @first_object_layer = ();
+    my %first_object_layer_speeds = ();  # F => 1
+    Slic3r::GCode::Reader->new->parse(Slic3r::Test::gcode($print), sub {
+        my ($self, $cmd, $args, $info) = @_;
+        
+        if ($info->{extruding} && $info->{dist_XY} > 0) {
+            if ($layer_id <= $config->raft_layers) {
+                # this is a raft layer or the first object layer
+                my $line = Slic3r::Line->new_scale([ $self->X, $self->Y ], [ $info->{new_X}, $info->{new_Y} ]);
+                my @path = @{$line->grow(scale($config->support_material_extrusion_width/2))};
+                if ($layer_id < $config->raft_layers) {
+                    # this is a raft layer
+                    push @raft, @path;
+                } else {
+                    push @first_object_layer, @path;
+                    $first_object_layer_speeds{ $args->{F} // $self->F } = 1;
+                }
+            }
+        } elsif ($cmd eq 'G1' && $info->{dist_Z} > 0) {
+            $layer_id++;
+        }
+    });
+    
+    ok !@{diff(\@first_object_layer, \@raft)},
+        'first object layer is completely supported by raft';
+    is scalar(keys %first_object_layer_speeds), 1,
+        'only one speed used in first object layer';
+    ok +(keys %first_object_layer_speeds)[0] == $config->bridge_speed*60,
+        'bridge speed used in first object layer';
+}
+
+{
+    my $config = Slic3r::Config::new_from_defaults;
+    $config->set('skirts', 0);
+    $config->set('layer_height', 0.35);
+    $config->set('first_layer_height', 0.3);
+    $config->set('nozzle_diameter', [0.5]);
+    $config->set('support_material_extruder', 2);
+    $config->set('support_material_interface_extruder', 2);
+    
+    my $test = sub {
+        my ($raft_layers) = @_;
+        $config->set('raft_layers', $raft_layers);
+        my $print = Slic3r::Test::init_print('20mm_cube', config => $config);
+        my %raft_z = ();  # z => 1
+        my $tool = undef;
+        Slic3r::GCode::Reader->new->parse(Slic3r::Test::gcode($print), sub {
+            my ($self, $cmd, $args, $info) = @_;
+        
+            if ($cmd =~ /^T(\d+)/) {
+                $tool = $1;
+            } elsif ($info->{extruding} && $info->{dist_XY} > 0) {
+                if ($tool == $config->support_material_extruder-1) {
+                    $raft_z{$self->Z} = 1;
+                }
+            }
+        });
+    
+        is scalar(keys %raft_z), $config->raft_layers, 'correct number of raft layers is generated';
+    };
+    
+    $test->(2);
+    $test->(70);
+    
+    $config->set('layer_height', 0.4);
+    $config->set('first_layer_height', 0.35);
+    $test->(3);
+    $test->(70);
+}
+
+{
+    my $config = Slic3r::Config::new_from_defaults;
+    $config->set('brim_width',  0);
+    $config->set('skirts', 0);
+    $config->set('support_material', 1);
+    $config->set('top_solid_layers', 0); # so that we don't have the internal bridge over infill
+    $config->set('bridge_speed', 99);
+    $config->set('cooling', [ 0 ]);
+    $config->set('first_layer_speed', '100%');
+    
+    my $test = sub {
+        my $print = Slic3r::Test::init_print('overhang', config => $config);
+    
+        my $has_bridge_speed = 0;
+        Slic3r::GCode::Reader->new->parse(Slic3r::Test::gcode($print), sub {
+            my ($self, $cmd, $args, $info) = @_;
+        
+            if ($info->{extruding}) {
+                if (($args->{F} // $self->F) == $config->bridge_speed*60) {
+                    $has_bridge_speed = 1;
+                }
+            }
+        });
+        return $has_bridge_speed;
+    };
+    
+    $config->set('support_material_contact_distance', 0.2);
+    ok $test->(), 'bridge speed is used when support_material_contact_distance > 0';
+    
+    $config->set('support_material_contact_distance', 0);
+    ok !$test->(), 'bridge speed is not used when support_material_contact_distance == 0';
+    
+    $config->set('raft_layers', 5);
+    $config->set('support_material_contact_distance', 0.2);
+    ok $test->(), 'bridge speed is used when raft_layers > 0 and support_material_contact_distance > 0';
+    
+    $config->set('support_material_contact_distance', 0);
+    ok !$test->(), 'bridge speed is not used when raft_layers > 0 and support_material_contact_distance == 0';
+}
+
+{
+    my $config = Slic3r::Config::new_from_defaults;
+    $config->set('skirts', 0);
+    $config->set('start_gcode', '');
+    $config->set('raft_layers', 8);
+    $config->set('nozzle_diameter', [0.4, 1]);
+    $config->set('layer_height', 0.1);
+    $config->set('first_layer_height', 0.8);
+    $config->set('support_material_extruder', 2);
+    $config->set('support_material_interface_extruder', 2);
+    $config->set('support_material_contact_distance', 0);
+    my $print = Slic3r::Test::init_print('20mm_cube', config => $config);
+    ok my $gcode = Slic3r::Test::gcode($print), 'first_layer_height is validated with support material extruder nozzle diameter when using raft layers';
+    
+    my $tool = undef;
+    my @z = (0);
+    my %layer_heights_by_tool = ();  # tool => [ lh, lh... ]
+    Slic3r::GCode::Reader->new->parse($gcode, sub {
+        my ($self, $cmd, $args, $info) = @_;
+    
+        if ($cmd =~ /^T(\d+)/) {
+            $tool = $1;
+        } elsif ($cmd eq 'G1' && exists $args->{Z} && $args->{Z} != $self->Z) {
+            push @z, $args->{Z};
+        } elsif ($info->{extruding} && $info->{dist_XY} > 0) {
+            $layer_heights_by_tool{$tool} ||= [];
+            push @{ $layer_heights_by_tool{$tool} }, $z[-1] - $z[-2];
+        }
+    });
+    
+    ok !defined(first { $_ > $config->nozzle_diameter->[0] + epsilon }
+        @{ $layer_heights_by_tool{$config->perimeter_extruder-1} }),
+        'no object layer is thicker than nozzle diameter';
+    
+    ok !defined(first { abs($_ - $config->layer_height) < epsilon }
+        @{ $layer_heights_by_tool{$config->support_material_extruder-1} }),
+        'no support material layer is as thin as object layers';
+}
+
+*/
diff --git a/tests/libslic3r/test_clipper_utils.cpp b/tests/libslic3r/test_clipper_utils.cpp
index b357d8ca8..24d949be6 100644
--- a/tests/libslic3r/test_clipper_utils.cpp
+++ b/tests/libslic3r/test_clipper_utils.cpp
@@ -301,6 +301,11 @@ SCENARIO("Various Clipper operations - t/clipper.t", "[ClipperUtils]") {
             }
         }
     }
+    GIVEN("line") {
+        THEN("expand by 5") {
+            REQUIRE(offset(Polyline({10,10}, {20,10}), 5).front().area() == Polygon({ {10,5}, {20,5}, {20,15}, {10,15} }).area());
+        }
+    }
 }
 
 template<e_ordering o = e_ordering::OFF, class P, class Tree> 
diff --git a/xs/CMakeLists.txt b/xs/CMakeLists.txt
index 022ba2c01..ab11e4989 100644
--- a/xs/CMakeLists.txt
+++ b/xs/CMakeLists.txt
@@ -45,7 +45,6 @@ set(XSP_DIR ${CMAKE_CURRENT_SOURCE_DIR}/xsp)
 set(XS_XSP_FILES
     ${XSP_DIR}/BoundingBox.xsp
     ${XSP_DIR}/BridgeDetector.xsp
-    ${XSP_DIR}/Clipper.xsp
     ${XSP_DIR}/Config.xsp
     ${XSP_DIR}/ExPolygon.xsp
     ${XSP_DIR}/ExPolygonCollection.xsp
@@ -59,12 +58,9 @@ set(XS_XSP_FILES
     ${XSP_DIR}/Layer.xsp
     ${XSP_DIR}/Line.xsp
     ${XSP_DIR}/Model.xsp
-    ${XSP_DIR}/PerimeterGenerator.xsp
-    ${XSP_DIR}/PlaceholderParser.xsp
     ${XSP_DIR}/Point.xsp
     ${XSP_DIR}/Polygon.xsp
     ${XSP_DIR}/Polyline.xsp
-    ${XSP_DIR}/PolylineCollection.xsp
     ${XSP_DIR}/Print.xsp
     ${XSP_DIR}/Surface.xsp
     ${XSP_DIR}/SurfaceCollection.xsp
diff --git a/xs/lib/Slic3r/XS.pm b/xs/lib/Slic3r/XS.pm
index 83bc0ee70..9a0181b65 100644
--- a/xs/lib/Slic3r/XS.pm
+++ b/xs/lib/Slic3r/XS.pm
@@ -4,21 +4,6 @@ use strict;
 
 our $VERSION = '0.01';
 
-# We have to load these modules in order to have Wx.pm find the correct paths
-# for wxWidgets dlls on MSW.
-# We avoid loading these on OS X because Wx::Load() initializes a Wx App
-# automatically and it steals focus even when we're not running Slic3r in GUI mode.
-# TODO: only load these when compiling with GUI support
-BEGIN {
-    if ($^O eq 'MSWin32') {
-        eval "use Wx";
-        eval "use Wx::GLCanvas";
-        eval "use Wx::GLContext";
-        eval "use Wx::Html";
-        eval "use Wx::Print";  # because of some Wx bug, thread creation fails if we don't have this (looks like Wx::Printout is hard-coded in some thread cleanup code)
-    }
-}
-
 use Carp qw();
 use XSLoader;
 XSLoader::load(__PACKAGE__, $VERSION);
@@ -58,11 +43,6 @@ use overload
     '@{}' => sub { $_[0]->arrayref },
     'fallback' => 1;
 
-package Slic3r::Polyline::Collection;
-use overload
-    '@{}' => sub { $_[0]->arrayref },
-    'fallback' => 1;
-
 package Slic3r::Polygon;
 use overload
     '@{}' => sub { $_[0]->arrayref },
@@ -197,31 +177,6 @@ sub new {
     return $self;
 }
 
-package Slic3r::Print::SupportMaterial2;
-
-sub new {
-    my ($class, %args) = @_;
-    
-    return $class->_new(
-        $args{print_config},        # required
-        $args{object_config},       # required
-        $args{first_layer_flow},    # required
-        $args{flow},                # required
-        $args{interface_flow},      # required
-        $args{soluble_interface}    // 0
-    );
-}
-
-package Slic3r::GUI::_3DScene::GLVolume::Collection;
-use overload
-    '@{}' => sub { $_[0]->arrayref },
-    'fallback' => 1;
-
-package Slic3r::GUI::PresetCollection;
-use overload
-    '@{}' => sub { $_[0]->arrayref },
-    'fallback' => 1;
-
 package main;
 for my $class (qw(
         Slic3r::BridgeDetector
@@ -240,13 +195,11 @@ for my $class (qw(
         Slic3r::ExtrusionPath::Collection
         Slic3r::Flow
         Slic3r::GCode
-        Slic3r::GCode::PlaceholderParser
         Slic3r::Geometry::BoundingBox
         Slic3r::Geometry::BoundingBoxf
         Slic3r::Geometry::BoundingBoxf3
         Slic3r::Layer
         Slic3r::Layer::Region
-        Slic3r::Layer::Support
         Slic3r::Line
         Slic3r::Linef3
         Slic3r::Model
@@ -264,10 +217,8 @@ for my $class (qw(
         Slic3r::Print
         Slic3r::Print::Object
         Slic3r::Print::Region
-        Slic3r::Print::State
         Slic3r::Surface
         Slic3r::Surface::Collection
-        Slic3r::Print::SupportMaterial2
         Slic3r::TriangleMesh
     ))
 {
diff --git a/xs/src/perlglue.cpp b/xs/src/perlglue.cpp
index 2d996120d..e9f84b8c0 100644
--- a/xs/src/perlglue.cpp
+++ b/xs/src/perlglue.cpp
@@ -13,15 +13,11 @@ REGISTER_CLASS(Flow, "Flow");
 REGISTER_CLASS(CoolingBuffer, "GCode::CoolingBuffer");
 REGISTER_CLASS(GCode, "GCode");
 REGISTER_CLASS(Layer, "Layer");
-REGISTER_CLASS(SupportLayer, "Layer::Support");
 REGISTER_CLASS(LayerRegion, "Layer::Region");
 REGISTER_CLASS(Line, "Line");
 REGISTER_CLASS(Linef3, "Linef3");
-REGISTER_CLASS(PerimeterGenerator, "Layer::PerimeterGenerator");
-REGISTER_CLASS(PlaceholderParser, "GCode::PlaceholderParser");
 REGISTER_CLASS(Polygon, "Polygon");
 REGISTER_CLASS(Polyline, "Polyline");
-REGISTER_CLASS(PolylineCollection, "Polyline::Collection");
 REGISTER_CLASS(Print, "Print");
 REGISTER_CLASS(PrintObject, "Print::Object");
 REGISTER_CLASS(PrintRegion, "Print::Region");
@@ -46,7 +42,6 @@ REGISTER_CLASS(PrintConfig, "Config::Print");
 REGISTER_CLASS(FullPrintConfig, "Config::Full");
 REGISTER_CLASS(Surface, "Surface");
 REGISTER_CLASS(SurfaceCollection, "Surface::Collection");
-REGISTER_CLASS(PrintObjectSupportMaterial, "Print::SupportMaterial2");
 REGISTER_CLASS(TriangleMesh, "TriangleMesh");
 
 SV* ConfigBase__as_hash(ConfigBase* THIS)
diff --git a/xs/t/01_trianglemesh.t b/xs/t/01_trianglemesh.t
index 453cc9218..a071a75a2 100644
--- a/xs/t/01_trianglemesh.t
+++ b/xs/t/01_trianglemesh.t
@@ -4,7 +4,7 @@ use strict;
 use warnings;
 
 use Slic3r::XS;
-use Test::More tests => 5;
+use Test::More tests => 4;
 
 my $cube = {
     vertices    => [ [20,20,0], [20,0,0], [0,0,0], [0,20,0], [20,20,20], [0,20,20], [0,0,20], [20,0,20] ],
@@ -25,11 +25,6 @@ my $cube = {
         is_deeply $m2->facets, $cube->{facets}, 'cloned facets arrayref roundtrip';
         $m2->scale(3);  # check that it does not affect $m
     }
-    
-    {
-        my $stats = $m->stats;
-        is $stats->{number_of_facets}, scalar(@{ $cube->{facets} }), 'stats.number_of_facets';
-    }
 }
 
 __END__
diff --git a/xs/t/13_polylinecollection.t b/xs/t/13_polylinecollection.t
deleted file mode 100644
index 9b36e7ffa..000000000
--- a/xs/t/13_polylinecollection.t
+++ /dev/null
@@ -1,35 +0,0 @@
-#!/usr/bin/perl
-
-use strict;
-use warnings;
-
-use Slic3r::XS;
-use Test::More tests => 3;
-
-{
-    my $collection = Slic3r::Polyline::Collection->new(
-        Slic3r::Polyline->new([0,15], [0,18], [0,20]),
-        Slic3r::Polyline->new([0,10], [0,8], [0,5]),
-    );
-    is_deeply
-        [ map $_->y, map @$_, @{$collection->chained_path_from(Slic3r::Point->new(0,30), 0)} ],
-        [20, 18, 15, 10, 8, 5],
-        'chained_path_from';
-    is_deeply
-        [ map $_->y, map @$_, @{$collection->chained_path(0)} ],
-        [15, 18, 20, 10, 8, 5],
-        'chained_path';
-}
-
-{
-    my $collection = Slic3r::Polyline::Collection->new(
-        Slic3r::Polyline->new([15,0], [10,0], [4,0]),
-        Slic3r::Polyline->new([10,5], [15,5], [20,5]),
-    );
-    is_deeply
-        [ map $_->x, map @$_, @{$collection->chained_path_from(Slic3r::Point->new(30,0), 0)} ],
-        [reverse 4, 10, 15, 10, 15, 20],
-        'chained_path_from';
-}
-
-__END__
diff --git a/xs/xsp/Clipper.xsp b/xs/xsp/Clipper.xsp
deleted file mode 100644
index 5f9f35906..000000000
--- a/xs/xsp/Clipper.xsp
+++ /dev/null
@@ -1,73 +0,0 @@
-%module{Slic3r::XS};
-
-%{
-#include <xsinit.h>
-#include "libslic3r/ClipperUtils.hpp"
-%}
-
-%package{Slic3r::Geometry::Clipper};
-
-%{
-
-Polygons
-offset(polygons, delta, joinType = Slic3r::ClipperLib::jtMiter, miterLimit = 3)
-    Polygons                polygons
-    const float             delta
-    Slic3r::ClipperLib::JoinType    joinType
-    double                  miterLimit
-    CODE:
-        RETVAL = offset(polygons, delta, joinType, miterLimit);
-    OUTPUT:
-        RETVAL
-
-ExPolygons
-offset2_ex(polygons, delta1, delta2, joinType = Slic3r::ClipperLib::jtMiter, miterLimit = 3)
-    Polygons                polygons
-    const float             delta1
-    const float             delta2
-    Slic3r::ClipperLib::JoinType    joinType
-    double                  miterLimit
-    CODE:
-        RETVAL = offset2_ex(union_ex(polygons), delta1, delta2, joinType, miterLimit);
-    OUTPUT:
-        RETVAL
-
-Polygons
-diff(subject, clip, safety_offset = false)
-    Polygons    subject
-    Polygons    clip
-    bool        safety_offset
-    CODE:
-        RETVAL = diff(subject, clip, safety_offset ? ApplySafetyOffset::Yes : ApplySafetyOffset::No);
-    OUTPUT:
-        RETVAL
-
-ExPolygons
-diff_ex(subject, clip, safety_offset = false)
-    Polygons    subject
-    Polygons    clip
-    bool        safety_offset
-    CODE:
-        RETVAL = diff_ex(subject, clip, safety_offset ? ApplySafetyOffset::Yes : ApplySafetyOffset::No);
-    OUTPUT:
-        RETVAL
-
-Polygons
-union(subject, safety_offset = false)
-    Polygons    subject
-    bool        safety_offset
-    CODE:
-        RETVAL = safety_offset ? union_safety_offset(subject) : union_(subject);
-    OUTPUT:
-        RETVAL
-
-ExPolygons
-union_ex(subject, safety_offset = false)
-    Polygons                    subject
-    bool                        safety_offset
-    CODE:
-        RETVAL = safety_offset ? union_safety_offset_ex(subject) : union_ex(subject);
-    OUTPUT:
-        RETVAL
-
-%}
diff --git a/xs/xsp/Geometry.xsp b/xs/xsp/Geometry.xsp
index e59e8793b..6e808fd8c 100644
--- a/xs/xsp/Geometry.xsp
+++ b/xs/xsp/Geometry.xsp
@@ -47,14 +47,6 @@ convex_hull(points)
     OUTPUT:
         RETVAL
 
-std::vector<Points::size_type>
-chained_path(points)
-    Points      points
-    CODE:
-        RETVAL = chain_points(points);
-    OUTPUT:
-        RETVAL
-
 std::vector<Points::size_type>
 chained_path_from(points, start_from)
     Points      points
diff --git a/xs/xsp/Layer.xsp b/xs/xsp/Layer.xsp
index b97e340bd..4ab7fe188 100644
--- a/xs/xsp/Layer.xsp
+++ b/xs/xsp/Layer.xsp
@@ -65,9 +65,6 @@
     int ptr()
         %code%{ RETVAL = (int)(intptr_t)THIS; %};
     
-    Ref<SupportLayer> as_support_layer()
-        %code%{ RETVAL = dynamic_cast<SupportLayer*>(THIS); %};
-    
     void make_slices();
     void backup_untyped_slices();
     void restore_untyped_slices();
@@ -79,41 +76,3 @@
     void export_region_slices_to_svg_debug(const char *name);
     void export_region_fill_surfaces_to_svg_debug(const char *name);
 };
-
-%name{Slic3r::Layer::Support} class SupportLayer {
-    // owned by PrintObject, no constructor/destructor
-    
-    Ref<Layer> as_layer()
-        %code%{ RETVAL = THIS; %};
-    
-    Ref<ExPolygonCollection> support_islands()
-        %code%{ RETVAL = &THIS->support_islands; %};
-    Ref<ExtrusionEntityCollection> support_fills()
-        %code%{ RETVAL = &THIS->support_fills; %};
-
-    // copies of some Layer methods, because the parameter wrapper code
-    // gets confused about getting a Layer::Support instead of a Layer
-    int id();
-    void set_id(int id);
-    Ref<PrintObject> object();
-    bool slicing_errors()
-        %code%{ RETVAL = THIS->slicing_errors; %};
-    coordf_t slice_z()
-        %code%{ RETVAL = THIS->slice_z; %};
-    coordf_t print_z()
-        %code%{ RETVAL = THIS->print_z; %};
-    coordf_t height()
-        %code%{ RETVAL = THIS->height; %};
-
-    size_t region_count();
-    Ref<LayerRegion> get_region(int idx);
-    Ref<LayerRegion> add_region(PrintRegion* print_region);
-
-    ExPolygonCollection* slices()
-        %code%{ RETVAL = new ExPolygonCollection(THIS->lslices); %};
-    
-    void export_region_slices_to_svg(const char *path);
-    void export_region_fill_surfaces_to_svg(const char *path);
-    void export_region_slices_to_svg_debug(const char *name);
-    void export_region_fill_surfaces_to_svg_debug(const char *name);
-};
diff --git a/xs/xsp/PerimeterGenerator.xsp b/xs/xsp/PerimeterGenerator.xsp
deleted file mode 100644
index a2f589d0b..000000000
--- a/xs/xsp/PerimeterGenerator.xsp
+++ /dev/null
@@ -1,40 +0,0 @@
-%module{Slic3r::XS};
-
-%{
-#include <xsinit.h>
-#include "libslic3r/PerimeterGenerator.hpp"
-#include "libslic3r/Layer.hpp"
-%}
-
-%name{Slic3r::Layer::PerimeterGenerator} class PerimeterGenerator {
-    PerimeterGenerator(SurfaceCollection* slices, double layer_height, Flow* flow,
-        StaticPrintConfig* region_config, StaticPrintConfig* object_config, 
-        StaticPrintConfig* print_config, ExtrusionEntityCollection* loops, 
-        ExtrusionEntityCollection* gap_fill, 
-        SurfaceCollection* fill_surfaces)
-        %code{% RETVAL = new PerimeterGenerator(slices, layer_height, *flow,
-            dynamic_cast<PrintRegionConfig*>(region_config),
-            dynamic_cast<PrintObjectConfig*>(object_config),
-            dynamic_cast<PrintConfig*>(print_config),
-            false,
-            loops, gap_fill, fill_surfaces); %};
-    ~PerimeterGenerator();
-    
-    void set_lower_slices(ExPolygonCollection* lower_slices)
-        %code{% THIS->lower_slices = &lower_slices->expolygons; %};
-    void set_layer_id(int layer_id)
-        %code{% THIS->layer_id = layer_id; %};
-    void set_perimeter_flow(Flow* flow)
-        %code{% THIS->perimeter_flow = *flow; %};
-    void set_ext_perimeter_flow(Flow* flow)
-        %code{% THIS->ext_perimeter_flow = *flow; %};
-    void set_overhang_flow(Flow* flow)
-        %code{% THIS->overhang_flow = *flow; %};
-    void set_solid_infill_flow(Flow* flow)
-        %code{% THIS->solid_infill_flow = *flow; %};
-    
-    Ref<StaticPrintConfig> config()
-        %code{% RETVAL = THIS->config; %};
-    
-    void process();
-};
diff --git a/xs/xsp/PlaceholderParser.xsp b/xs/xsp/PlaceholderParser.xsp
deleted file mode 100644
index 5fa4e33aa..000000000
--- a/xs/xsp/PlaceholderParser.xsp
+++ /dev/null
@@ -1,33 +0,0 @@
-%module{Slic3r::XS};
-
-%{
-#include <xsinit.h>
-#include <vector>
-#include "libslic3r/PlaceholderParser.hpp"
-%}
-
-%name{Slic3r::GCode::PlaceholderParser} class PlaceholderParser {
-    PlaceholderParser();
-    ~PlaceholderParser();
-    
-    void apply_config(DynamicPrintConfig *config)
-        %code%{ THIS->apply_config(*config); %};
-    void set(std::string key, int value);
-    std::string process(std::string str) const
-        %code%{
-            try {
-                RETVAL = THIS->process(str, 0);
-            } catch (std::exception& e) {
-                croak("%s\n", e.what());
-            }
-        %};
-
-    bool evaluate_boolean_expression(const char *str) const
-        %code%{
-            try {
-                RETVAL = THIS->evaluate_boolean_expression(str, THIS->config());
-            } catch (std::exception& e) {
-                croak("%s\n", e.what());
-            }
-        %};        
-};
diff --git a/xs/xsp/Polygon.xsp b/xs/xsp/Polygon.xsp
index 984eb16a9..bc4d412d9 100644
--- a/xs/xsp/Polygon.xsp
+++ b/xs/xsp/Polygon.xsp
@@ -23,7 +23,6 @@
         %code{% RETVAL = THIS->split_at_vertex(*point); %};
     Clone<Polyline> split_at_index(int index);
     Clone<Polyline> split_at_first_point();
-    Points equally_spaced_points(double distance);
     double length();
     double area();
     bool is_counter_clockwise();
diff --git a/xs/xsp/Polyline.xsp b/xs/xsp/Polyline.xsp
index 7846ea5f4..3534e329e 100644
--- a/xs/xsp/Polyline.xsp
+++ b/xs/xsp/Polyline.xsp
@@ -3,7 +3,6 @@
 %{
 #include <xsinit.h>
 #include "libslic3r/BoundingBox.hpp"
-#include "libslic3r/ClipperUtils.hpp"
 #include "libslic3r/Polyline.hpp"
 %}
 
@@ -23,7 +22,6 @@
     Lines lines();
     Clone<Point> first_point();
     Clone<Point> last_point();
-    Points equally_spaced_points(double distance);
     double length();
     bool is_valid();
     void clip_end(double distance);
@@ -33,9 +31,7 @@
     void simplify(double tolerance);
     void split_at(Point* point, Polyline* p1, Polyline* p2)
         %code{% THIS->split_at(*point, p1, p2); %};
-    bool is_straight();
     Clone<BoundingBox> bounding_box();
-    void remove_duplicate_points();
 %{
 
 Polyline*
@@ -76,15 +72,5 @@ Polyline::rotate(angle, center_sv)
         from_SV_check(center_sv, &center);
         THIS->rotate(angle, center);
 
-Polygons
-Polyline::grow(delta, joinType = Slic3r::ClipperLib::jtSquare, miterLimit = 3)
-    const float             delta
-    Slic3r::ClipperLib::JoinType    joinType
-    double                  miterLimit
-    CODE:
-        RETVAL = offset(*THIS, delta, joinType, miterLimit);
-    OUTPUT:
-        RETVAL
-
 %}
 };
diff --git a/xs/xsp/PolylineCollection.xsp b/xs/xsp/PolylineCollection.xsp
deleted file mode 100644
index d8bb41b49..000000000
--- a/xs/xsp/PolylineCollection.xsp
+++ /dev/null
@@ -1,81 +0,0 @@
-%module{Slic3r::XS};
-
-%{
-#include <xsinit.h>
-
-#include "libslic3r.h"
-#include "Polyline.hpp"
-#include "ShortestPath.hpp"
-
-%}
-
-%name{Slic3r::Polyline::Collection} class PolylineCollection {
-    ~PolylineCollection();
-    Clone<PolylineCollection> clone()
-        %code{% RETVAL = THIS; %};
-    void clear()
-        %code{% THIS->polylines.clear(); %};
-    PolylineCollection* chained_path(bool no_reverse)
-        %code{%
-            RETVAL = new PolylineCollection();
-            RETVAL->polylines = chain_polylines(THIS->polylines, &THIS->polylines.front().first_point());
-        %};
-    PolylineCollection* chained_path_from(Point* start_near, bool no_reverse)
-        %code{%
-            RETVAL = new PolylineCollection();
-            RETVAL->polylines = chain_polylines(THIS->polylines, start_near);
-        %};
-    int count()
-        %code{% RETVAL = THIS->polylines.size(); %};
-%{
-
-PolylineCollection*
-PolylineCollection::new(...)
-    CODE:
-        RETVAL = new PolylineCollection ();
-        // ST(0) is class name, others are Polylines
-        RETVAL->polylines.resize(items-1);
-        for (unsigned int i = 1; i < items; i++) {
-            // Note: a COPY of the input is stored
-            from_SV_check(ST(i), &RETVAL->polylines[i-1]);
-        }
-    OUTPUT:
-        RETVAL
-
-SV*
-PolylineCollection::arrayref()
-    CODE:
-        AV* av = newAV();
-        av_fill(av, THIS->polylines.size()-1);
-        int i = 0;
-        for (Polylines::iterator it = THIS->polylines.begin(); it != THIS->polylines.end(); ++it) {
-            av_store(av, i++, perl_to_SV_ref(*it));
-        }
-        RETVAL = newRV_noinc((SV*)av);
-    OUTPUT:
-        RETVAL
-
-SV*
-PolylineCollection::pp()
-    CODE:
-        AV* av = newAV();
-        av_fill(av, THIS->polylines.size()-1);
-        int i = 0;
-        for (Polylines::iterator it = THIS->polylines.begin(); it != THIS->polylines.end(); ++it) {
-            av_store(av, i++, to_SV_pureperl(&*it));
-        }
-        RETVAL = newRV_noinc((SV*)av);
-    OUTPUT:
-        RETVAL
-
-void
-PolylineCollection::append(...)
-    CODE:
-        for (unsigned int i = 1; i < items; i++) {
-            Polyline polyline;
-            from_SV_check(ST(i), &polyline);
-            THIS->polylines.push_back(polyline);
-        }
-
-%}
-};
diff --git a/xs/xsp/Print.xsp b/xs/xsp/Print.xsp
index bffdd933f..97cbdafe3 100644
--- a/xs/xsp/Print.xsp
+++ b/xs/xsp/Print.xsp
@@ -3,27 +3,6 @@
 %{
 #include <xsinit.h>
 #include "libslic3r/Print.hpp"
-#include "libslic3r/PlaceholderParser.hpp"
-%}
-
-%package{Slic3r::Print::State};
-%{
-
-IV
-_constant()
-  ALIAS:
-    STEP_SLICE              = posSlice
-    STEP_PERIMETERS         = posPerimeters
-    STEP_PREPARE_INFILL     = posPrepareInfill
-    STEP_INFILL             = posInfill
-    STEP_SUPPORTMATERIAL    = posSupportMaterial
-    STEP_SKIRTBRIM          = psSkirtBrim
-    STEP_WIPE_TOWER         = psWipeTower
-  PROTOTYPE:
-  CODE:
-    RETVAL = ix;
-  OUTPUT: RETVAL
-
 %}
 
 %name{Slic3r::Print::Region} class PrintRegion {
@@ -45,12 +24,6 @@ _constant()
     size_t layer_count();
     Ref<Layer> get_layer(int idx);
 
-    size_t support_layer_count();
-    Ref<SupportLayer> get_support_layer(int idx);
-
-    bool step_done(PrintObjectStep step)
-        %code%{ RETVAL = THIS->is_step_done(step); %};
-
     void slice();
 };
 
@@ -62,16 +35,10 @@ _constant()
         %code%{ RETVAL = const_cast<Model*>(&THIS->model()); %};
     Ref<StaticPrintConfig> config()
         %code%{ RETVAL = const_cast<GCodeConfig*>(static_cast<const GCodeConfig*>(&THIS->config())); %};
-    Ref<PlaceholderParser> placeholder_parser()
-        %code%{ RETVAL = const_cast<PlaceholderParser*>(&THIS->placeholder_parser()); %};
     Ref<ExtrusionEntityCollection> skirt()
         %code%{ RETVAL = const_cast<ExtrusionEntityCollection*>(&THIS->skirt()); %};
     Ref<ExtrusionEntityCollection> brim()
         %code%{ RETVAL = const_cast<ExtrusionEntityCollection*>(&THIS->brim()); %};
-//    std::string estimated_normal_print_time()
-//        %code%{ RETVAL = THIS->print_statistics().estimated_normal_print_time; %};
-//    std::string estimated_silent_print_time()
-//        %code%{ RETVAL = THIS->print_statistics().estimated_silent_print_time; %};
     double total_used_filament()
         %code%{ RETVAL = THIS->print_statistics().total_used_filament; %};
     double total_extruded_volume()
@@ -96,25 +63,6 @@ _constant()
     PrintRegionPtrs* regions()
         %code%{ RETVAL = const_cast<PrintRegionPtrs*>(&THIS->print_regions_mutable()); %};
     
-    bool step_done(PrintStep step)
-        %code%{ RETVAL = THIS->is_step_done(step); %};
-    bool object_step_done(PrintObjectStep step)
-        %code%{ RETVAL = THIS->is_step_done(step); %};
-    
-    SV* filament_stats()
-        %code%{
-            HV* hv = newHV();
-            for (std::map<size_t,double>::const_iterator it = THIS->print_statistics().filament_stats.begin(); it != THIS->print_statistics().filament_stats.end(); ++it) {
-                // stringify extruder_id
-                std::ostringstream ss;
-                ss << it->first;
-                std::string str = ss.str();
-                
-                (void)hv_store( hv, str.c_str(), str.length(), newSViv(it->second), 0 );
-                RETVAL = newRV_noinc((SV*)hv);
-            }
-        %};
-    bool has_support_material() const;
     void auto_assign_extruders(ModelObject* model_object);
     std::string output_filepath(std::string path = "")
         %code%{
@@ -138,7 +86,6 @@ _constant()
                 mat.second->config.touch();
             RETVAL = THIS->apply(*model, *config);
         %};
-    bool has_infinite_skirt();
     std::vector<unsigned int> extruders() const;
     int validate() %code%{ 
             std::string err = THIS->validate(); 
@@ -147,10 +94,7 @@ _constant()
             RETVAL = 1;
         %};
 
-    void set_callback_event(int evt) %code%{
-        %};
     void set_status_silent();
-    void set_status(int percent, const char *message);
 
     void process() %code%{
             try {
diff --git a/xs/xsp/Surface.xsp b/xs/xsp/Surface.xsp
index 8804b851b..3fffea9ab 100644
--- a/xs/xsp/Surface.xsp
+++ b/xs/xsp/Surface.xsp
@@ -3,7 +3,6 @@
 %{
 #include <xsinit.h>
 #include "libslic3r/Surface.hpp"
-#include "libslic3r/ClipperUtils.hpp"
 %}
 
 %name{Slic3r::Surface} class Surface {
diff --git a/xs/xsp/TriangleMesh.xsp b/xs/xsp/TriangleMesh.xsp
index a9dbe654d..91b3b40a1 100644
--- a/xs/xsp/TriangleMesh.xsp
+++ b/xs/xsp/TriangleMesh.xsp
@@ -62,22 +62,6 @@ TriangleMesh::ReadFromPerl(vertices, facets)
         }
         *THIS = TriangleMesh(std::move(out_vertices), std::move(out_indices));
 
-SV*
-TriangleMesh::stats()
-    CODE:
-        HV* hv = newHV();
-        (void)hv_stores( hv, "number_of_facets",    newSViv(THIS->facets_count()) );
-        (void)hv_stores( hv, "number_of_parts",     newSViv(THIS->stats().number_of_parts) );
-        (void)hv_stores( hv, "volume",              newSVnv(THIS->stats().volume) );
-        (void)hv_stores( hv, "degenerate_facets",   newSViv(THIS->stats().repaired_errors.degenerate_facets) );
-        (void)hv_stores( hv, "edges_fixed",         newSViv(THIS->stats().repaired_errors.edges_fixed) );
-        (void)hv_stores( hv, "facets_removed",      newSViv(THIS->stats().repaired_errors.facets_removed) );
-        (void)hv_stores( hv, "facets_reversed",     newSViv(THIS->stats().repaired_errors.facets_reversed) );
-        (void)hv_stores( hv, "backwards_edges",     newSViv(THIS->stats().repaired_errors.backwards_edges) );
-        RETVAL = (SV*)newRV_noinc((SV*)hv);
-    OUTPUT:
-        RETVAL
-
 SV*
 TriangleMesh::vertices()
     CODE:
@@ -128,78 +112,5 @@ TriangleMesh::size()
     OUTPUT:
         RETVAL
 
-SV*
-TriangleMesh::slice(z)
-    std::vector<double> z
-    CODE:
-        // convert doubles to floats
-        std::vector<float> z_f = cast<float>(z);
-        
-        std::vector<ExPolygons> layers = slice_mesh_ex(THIS->its, z_f, 0.049f);
-        
-        AV* layers_av = newAV();
-        size_t len = layers.size();
-        if (len > 0) av_extend(layers_av, len-1);
-        for (unsigned int i = 0; i < layers.size(); i++) {
-            AV* expolygons_av = newAV();
-            len = layers[i].size();
-            if (len > 0) av_extend(expolygons_av, len-1);
-            unsigned int j = 0;
-            for (ExPolygons::iterator it = layers[i].begin(); it != layers[i].end(); ++it) {
-                av_store(expolygons_av, j++, perl_to_SV_clone_ref(*it));
-            }
-            av_store(layers_av, i, newRV_noinc((SV*)expolygons_av));
-        }
-        RETVAL = (SV*)newRV_noinc((SV*)layers_av);
-    OUTPUT:
-        RETVAL
-
-void
-TriangleMesh::cut(z, upper_mesh, lower_mesh)
-    float           z;
-    TriangleMesh*   upper_mesh;
-    TriangleMesh*   lower_mesh;
-    CODE:
-        indexed_triangle_set upper, lower;
-        cut_mesh(THIS->its, z, upper_mesh ? &upper : nullptr, lower_mesh ? &lower : nullptr);
-        if (upper_mesh)
-            *upper_mesh = TriangleMesh(upper);
-        if (lower_mesh)
-            *lower_mesh = TriangleMesh(lower);
-
-std::vector<double>
-TriangleMesh::bb3()
-    CODE:
-        RETVAL.push_back(THIS->stats().min(0));
-        RETVAL.push_back(THIS->stats().min(1));
-        RETVAL.push_back(THIS->stats().max(0));
-        RETVAL.push_back(THIS->stats().max(1));
-        RETVAL.push_back(THIS->stats().min(2));
-        RETVAL.push_back(THIS->stats().max(2));
-    OUTPUT:
-        RETVAL
-
-
-Clone<TriangleMesh>
-cube(double x, double y, double z) 
-    CODE:
-        RETVAL = make_cube(x,y,z);
-    OUTPUT:
-        RETVAL
-
-Clone<TriangleMesh> 
-cylinder(double r, double h) 
-    CODE:
-        RETVAL = make_cylinder(r, h);
-    OUTPUT:
-        RETVAL
-
-Clone<TriangleMesh>
-sphere(double rho)
-    CODE:
-        RETVAL = make_sphere(rho);
-    OUTPUT:
-        RETVAL
-
 %}
 };
diff --git a/xs/xsp/XS.xsp b/xs/xsp/XS.xsp
index 68ea282bc..66a35366b 100644
--- a/xs/xsp/XS.xsp
+++ b/xs/xsp/XS.xsp
@@ -35,129 +35,4 @@ set_logging_level(level)
     CODE:
         Slic3r::set_logging_level(level);
 
-void
-trace(level, message)
-    unsigned int level;
-    char        *message;
-    CODE:
-        Slic3r::trace(level, message);
-
-void
-disable_multi_threading()
-    CODE:
-        Slic3r::disable_multi_threading();
-
-void
-set_var_dir(dir)
-    char  *dir;
-    CODE:
-        Slic3r::set_var_dir(dir);
-
-void
-set_local_dir(dir)
-    char  *dir;
-    CODE:
-        Slic3r::set_local_dir(dir);
-
-char*
-var_dir()
-    CODE:
-        RETVAL = const_cast<char*>(Slic3r::var_dir().c_str());
-    OUTPUT: RETVAL
-
-void
-set_resources_dir(dir)
-    char  *dir;
-    CODE:
-        Slic3r::set_resources_dir(dir);
-
-char*
-resources_dir()
-    CODE:
-        RETVAL = const_cast<char*>(Slic3r::resources_dir().c_str());
-    OUTPUT: RETVAL
-
-std::string
-var(file_name)
-    const char *file_name;
-    CODE:
-        RETVAL = Slic3r::var(file_name);
-    OUTPUT: RETVAL
-
-void
-set_data_dir(dir)
-    char  *dir;
-    CODE:
-        Slic3r::set_data_dir(dir);
-
-char*
-data_dir()
-    CODE:
-        RETVAL = const_cast<char*>(Slic3r::data_dir().c_str());
-    OUTPUT: RETVAL
-
-local_encoded_string
-encode_path(src)
-    const char *src;
-    CODE:
-        RETVAL = Slic3r::encode_path(src);
-    OUTPUT: RETVAL
-
-std::string
-decode_path(src)
-    const char *src;
-    CODE:
-        RETVAL = Slic3r::decode_path(src);
-    OUTPUT: RETVAL
-
-std::string
-normalize_utf8_nfc(src)
-    const char *src;
-    CODE:
-        RETVAL = Slic3r::normalize_utf8_nfc(src);
-    OUTPUT: RETVAL
-
-std::string
-path_to_filename(src)
-    const char *src;
-    CODE:
-        RETVAL = Slic3r::PerlUtils::path_to_filename(src);
-    OUTPUT: RETVAL
-
-local_encoded_string
-path_to_filename_raw(src)
-    const char *src;
-    CODE:
-        RETVAL = Slic3r::PerlUtils::path_to_filename(src);
-    OUTPUT: RETVAL
-
-std::string
-path_to_stem(src)
-    const char *src;
-    CODE:
-        RETVAL = Slic3r::PerlUtils::path_to_stem(src);
-    OUTPUT: RETVAL
-
-std::string
-path_to_extension(src)
-    const char *src;
-    CODE:
-        RETVAL = Slic3r::PerlUtils::path_to_extension(src);
-    OUTPUT: RETVAL
-
-std::string
-path_to_parent_path(src)
-    const char *src;
-    CODE:
-        RETVAL = Slic3r::PerlUtils::path_to_parent_path(src);
-    OUTPUT: RETVAL
-
-void
-xspp_test_croak_hangs_on_strawberry()
-    CODE:
-    	try {
-    		throw 1;
-    	} catch (...) {
-    		croak("xspp_test_croak_hangs_on_strawberry: exception catched\n");
-    	}
-%}
\ No newline at end of file
+%}
diff --git a/xs/xsp/my.map b/xs/xsp/my.map
index a660041bc..131f82e79 100644
--- a/xs/xsp/my.map
+++ b/xs/xsp/my.map
@@ -1,7 +1,6 @@
 coordf_t T_NV
 
 std::string                     T_STD_STRING
-local_encoded_string     		T_STD_STRING_LOCAL_ENCODING
 t_config_option_key             T_STD_STRING
 t_model_material_id             T_STD_STRING
 
@@ -15,9 +14,6 @@ std::vector<unsigned int>	    T_STD_VECTOR_UINT
 
 std::vector<double>	            T_STD_VECTOR_DOUBLE
 
-t_layer_height_ranges           T_LAYER_HEIGHT_RANGES
-
-
 BoundingBox*               O_OBJECT_SLIC3R
 Ref<BoundingBox>           O_OBJECT_SLIC3R_T
 Clone<BoundingBox>         O_OBJECT_SLIC3R_T
@@ -52,8 +48,6 @@ Ref<PrintConfig>           O_OBJECT_SLIC3R_T
 FullPrintConfig*           O_OBJECT_SLIC3R
 Ref<FullPrintConfig>       O_OBJECT_SLIC3R_T
 
-ZTable*                    O_OBJECT
-
 TriangleMesh*              O_OBJECT_SLIC3R
 Ref<TriangleMesh>          O_OBJECT_SLIC3R_T
 Clone<TriangleMesh>        O_OBJECT_SLIC3R_T
@@ -86,10 +80,6 @@ Polyline*                  O_OBJECT_SLIC3R
 Ref<Polyline>              O_OBJECT_SLIC3R_T
 Clone<Polyline>            O_OBJECT_SLIC3R_T
 
-PolylineCollection*        O_OBJECT_SLIC3R
-Ref<PolylineCollection>    O_OBJECT_SLIC3R_T
-Clone<PolylineCollection>  O_OBJECT_SLIC3R_T
-
 Polygon*                   O_OBJECT_SLIC3R
 Ref<Polygon>               O_OBJECT_SLIC3R_T
 Clone<Polygon>             O_OBJECT_SLIC3R_T
@@ -122,9 +112,6 @@ Flow*                      O_OBJECT_SLIC3R
 Ref<Flow>                  O_OBJECT_SLIC3R_T
 Clone<Flow>                O_OBJECT_SLIC3R_T
 
-PrintState*                O_OBJECT_SLIC3R
-Ref<PrintState>            O_OBJECT_SLIC3R_T
-
 Surface*                   O_OBJECT_SLIC3R
 Ref<Surface>               O_OBJECT_SLIC3R_T
 Clone<Surface>             O_OBJECT_SLIC3R_T
@@ -168,13 +155,6 @@ Ref<LayerRegion>           O_OBJECT_SLIC3R_T
 Layer*                     O_OBJECT_SLIC3R
 Ref<Layer>                 O_OBJECT_SLIC3R_T
 
-SupportLayer*              O_OBJECT_SLIC3R
-Ref<SupportLayer>          O_OBJECT_SLIC3R_T
-
-PlaceholderParser*         O_OBJECT_SLIC3R
-Ref<PlaceholderParser>     O_OBJECT_SLIC3R_T
-Clone<PlaceholderParser>   O_OBJECT_SLIC3R_T
-
 CoolingBuffer*             O_OBJECT_SLIC3R
 Ref<CoolingBuffer>         O_OBJECT_SLIC3R_T
 Clone<CoolingBuffer>       O_OBJECT_SLIC3R_T
@@ -187,21 +167,10 @@ BridgeDetector*            O_OBJECT_SLIC3R
 Ref<BridgeDetector>        O_OBJECT_SLIC3R_T
 Clone<BridgeDetector>      O_OBJECT_SLIC3R_T
 
-PerimeterGenerator*         O_OBJECT_SLIC3R
-Ref<PerimeterGenerator>     O_OBJECT_SLIC3R_T
-Clone<PerimeterGenerator>   O_OBJECT_SLIC3R_T
-
-PrintObjectSupportMaterial*       O_OBJECT_SLIC3R
-Ref<PrintObjectSupportMaterial>   O_OBJECT_SLIC3R_T
-Clone<PrintObjectSupportMaterial> O_OBJECT_SLIC3R_T
-
 Axis                  T_UV
 ExtrusionLoopRole     T_UV
 ExtrusionRole     T_UV
-ExtrusionSimulationType T_UV
 FlowRole     T_UV
-PrintStep     T_UV
-PrintObjectStep     T_UV
 SurfaceType     T_UV
 Slic3r::ClipperLib::JoinType		T_UV
 Slic3r::ClipperLib::PolyFillType		T_UV
@@ -226,7 +195,6 @@ ModelInstancePtrs*  T_PTR_ARRAYREF_PTR
 PrintRegionPtrs*    T_PTR_ARRAYREF_PTR
 PrintObjectPtrs*    T_PTR_ARRAYREF_PTR
 LayerPtrs*          T_PTR_ARRAYREF_PTR
-SupportLayerPtrs*   T_PTR_ARRAYREF_PTR
 
 # we return these types whenever we want the items to be returned
 # by reference and not marked ::Ref because they're newly allocated
@@ -244,14 +212,6 @@ T_STD_STRING
       $var = std::string(c, len);
     }
 
-INPUT
-T_STD_STRING_LOCAL_ENCODING
-    {
-      size_t len;
-      const char * c = SvPV($arg, len);
-      $var = std::string(c, len);
-    }
-
 T_STD_VECTOR_STD_STRING
 	if (SvROK($arg) && SvTYPE(SvRV($arg))==SVt_PVAV) {
 	  AV* av = (AV*)SvRV($arg);
@@ -359,61 +319,11 @@ T_ARRAYREF
 	             ${$ALIAS?\q[GvNAME(CvGV(cv))]:\qq[\"$pname\"]},
 	             \"$var\");
 
-T_LAYER_HEIGHT_RANGES
-    {
-        if (!SvROK($arg) || SvTYPE(SvRV($arg)) != SVt_PVAV) {
-            Perl_croak(aTHX_ \"%s: %s is not an array reference\",
-                     ${$ALIAS?\q[GvNAME(CvGV(cv))]:\qq[\"$pname\"]},
-                     \"$var\");
-        }
-
-        AV* av = (AV*)SvRV($arg);
-        const unsigned int len = av_len(av)+1;
-        t_layer_height_ranges tmp_ranges;
-        for (unsigned int i = 0; i < len; i++) {
-            SV* elem = *av_fetch(av, i, 0);
-            if (!SvROK(elem) || SvTYPE(SvRV(elem)) != SVt_PVAV) {
-                Perl_croak(
-                    aTHX_ \"%s: %s contains something that is not an array reference\",
-                        ${$ALIAS?\q[GvNAME(CvGV(cv))]:\qq[\"$pname\"]},
-                        \"$var\");
-            }
-
-            AV* elemAV = (AV*)SvRV(elem);
-            if (av_len(elemAV) + 1 != 3) {
-                Perl_croak(
-                    aTHX_ \"%s: %s contains an array that isn't 3 elements long\",
-                        ${$ALIAS?\q[GvNAME(CvGV(cv))]:\qq[\"$pname\"]},
-                        \"$var\");
-            }
-
-            coordf_t vals[3];
-            for (unsigned int j = 0; j < 3; ++j) {
-                SV *elem_elem = *av_fetch(elemAV, j, 0);
-                if (!looks_like_number(elem_elem)) {
-                    Perl_croak(
-                        aTHX_ \"%s: layer ranges and heights must be numbers\",
-                            ${$ALIAS?\q[GvNAME(CvGV(cv))]:\qq[\"$pname\"]});
-                }
-
-                vals[j] = SvNV(elem_elem);
-            }
-
-            tmp_ranges[t_layer_height_range(vals[0], vals[1])] = vals[2];
-        }
-
-        $var = tmp_ranges;
-    }
-
-
 OUTPUT
 
 T_STD_STRING
     $arg = newSVpvn_utf8( $var.c_str(), $var.length(), true );
 
-T_STD_STRING_LOCAL_ENCODING
-    $arg = newSVpvn( $var.c_str(), $var.length() );
-
 T_STD_VECTOR_STD_STRING
 	AV* av = newAV();
 	$arg = newRV_noinc((SV*)av);
@@ -517,26 +427,3 @@ T_PTR_ARRAYREF
         av_store(av, i++, to_SV(*it));
 	}
 
-T_LAYER_HEIGHT_RANGES
-    AV* av = newAV();
-    $arg = newRV_noinc((SV*)av);
-    sv_2mortal($arg);
-	const unsigned int len = $var.size();
-	if (len > 0) av_extend(av, len-1);
-    // map is sorted, so we can just copy it in order
-    int i = 0;
-    for (${type}::iterator it = $var.begin(); it != $var.end(); ++it) {
-        const coordf_t range_values[] = {
-                it->first.first,    // key's first = minz
-                it->first.second,   // key's second = maxz
-                it->second,         // value = height
-            };
-
-        AV *rangeAV = newAV();
-        av_extend(rangeAV, 2);
-        for (int j = 0; j < 3; ++j) {
-            av_store(rangeAV, j, newSVnv(range_values[j]));
-        }
-
-        av_store(av, i++, (SV*)newRV_noinc((SV*)rangeAV));
-    }
diff --git a/xs/xsp/typemap.xspt b/xs/xsp/typemap.xspt
index cc7109445..b59f4b74c 100644
--- a/xs/xsp/typemap.xspt
+++ b/xs/xsp/typemap.xspt
@@ -12,7 +12,6 @@
 %typemap{std::vector<unsigned int>};
 %typemap{std::vector<unsigned int>*};
 %typemap{std::vector<std::string>};
-%typemap{t_layer_height_ranges};
 %typemap{void*};
 %typemap{SV*};
 %typemap{AV*};
@@ -88,18 +87,12 @@
 %typemap{TriangleMesh*};
 %typemap{Ref<TriangleMesh>}{simple};
 %typemap{Clone<TriangleMesh>}{simple};
-%typemap{PolylineCollection*};
-%typemap{Ref<PolylineCollection>}{simple};
-%typemap{Clone<PolylineCollection>}{simple};
 %typemap{BridgeDetector*};
 %typemap{Ref<BridgeDetector>}{simple};
 %typemap{Clone<BridgeDetector>}{simple};
 %typemap{SurfaceCollection*};
 %typemap{Ref<SurfaceCollection>}{simple};
 %typemap{Clone<SurfaceCollection>}{simple};
-%typemap{PerimeterGenerator*};
-%typemap{Ref<PerimeterGenerator>}{simple};
-%typemap{Clone<PerimeterGenerator>}{simple};
 
 %typemap{Surface*};
 %typemap{Ref<Surface>}{simple};
@@ -124,17 +117,6 @@
 %typemap{Layer*};
 %typemap{Ref<Layer>}{simple};
 
-%typemap{SupportLayer*};
-%typemap{Ref<SupportLayer>}{simple};
-
-%typemap{PrintObjectSupportMaterial*};
-%typemap{Ref<PrintObjectSupportMaterial>}{simple};
-%typemap{Clone<PrintObjectSupportMaterial>}{simple};
-
-%typemap{PlaceholderParser*};
-%typemap{Ref<PlaceholderParser>}{simple};
-%typemap{Clone<PlaceholderParser>}{simple};
-
 %typemap{CoolingBuffer*};
 %typemap{Ref<CoolingBuffer>}{simple};
 %typemap{Clone<CoolingBuffer>}{simple};
@@ -181,13 +163,10 @@
 %typemap{ModelInstancePtrs*};
 %typemap{Ref<ModelInstancePtrs>}{simple};
 %typemap{Clone<ModelInstancePtrs>}{simple};
-%typemap{PresetHints*};
-%typemap{Ref<PresetHints>}{simple};
 
 %typemap{PrintRegionPtrs*};
 %typemap{PrintObjectPtrs*};
 %typemap{LayerPtrs*};
-%typemap{SupportLayerPtrs*};
 
 %typemap{Axis}{parsed}{
   %cpp_type{Axis};
@@ -213,27 +192,9 @@
     $CVar = (ExtrusionRole)SvUV($PerlVar);
   %};
 };
-%typemap{ExtrusionSimulationType}{parsed}{
-  %cpp_type{ExtrusionSimulationType};
-  %precall_code{%
-    $CVar = (ExtrusionSimulationType)SvUV($PerlVar);
-  %};
-};
 %typemap{FlowRole}{parsed}{
   %cpp_type{FlowRole};
   %precall_code{%
     $CVar = (FlowRole)SvUV($PerlVar);
   %};
 };
-%typemap{PrintStep}{parsed}{
-  %cpp_type{PrintStep};
-  %precall_code{%
-    $CVar = (PrintStep)SvUV($PerlVar);
-  %};
-};
-%typemap{PrintObjectStep}{parsed}{
-  %cpp_type{PrintObjectStep};
-  %precall_code{%
-    $CVar = (PrintObjectStep)SvUV($PerlVar);
-  %};
-};

From 576c167bd52ea4f377a83c093be9d927a7cd2c75 Mon Sep 17 00:00:00 2001
From: Vojtech Bubnik <bubnikv@gmail.com>
Date: Wed, 4 May 2022 18:21:08 +0200
Subject: [PATCH 12/23] Ported "avoid crossing perimeters" and bridging unit
 tests from Perl to C++. Further reduced Perl bindings. Got rid of the
 ExPolygonCollection wrapper, replaced with ExPolygons.

---
 lib/Slic3r/ExPolygon.pm                       |   7 -
 lib/Slic3r/Geometry.pm                        | 170 ------------------
 src/libslic3r/CMakeLists.txt                  |   2 -
 src/libslic3r/ClipperUtils.cpp                |   6 +-
 src/libslic3r/ClipperUtils.hpp                |  16 +-
 src/libslic3r/EdgeGrid.cpp                    |   5 -
 src/libslic3r/EdgeGrid.hpp                    |   5 +-
 src/libslic3r/ExPolygon.hpp                   |   2 +-
 src/libslic3r/ExPolygonCollection.cpp         | 136 --------------
 src/libslic3r/ExPolygonCollection.hpp         |  41 -----
 src/libslic3r/ExtrusionEntity.cpp             |  10 +-
 src/libslic3r/ExtrusionEntity.hpp             |  11 +-
 src/libslic3r/GCode.cpp                       |   2 +-
 src/libslic3r/Geometry.cpp                    |   7 -
 src/libslic3r/Geometry.hpp                    |   1 -
 src/libslic3r/Geometry/ConvexHull.cpp         |  11 ++
 src/libslic3r/Geometry/ConvexHull.hpp         |   1 +
 src/libslic3r/Layer.hpp                       |   5 +-
 src/libslic3r/Polyline.cpp                    |   1 -
 src/libslic3r/SLA/SupportPointGenerator.cpp   |   7 +-
 src/libslic3r/SupportMaterial.cpp             |   2 +-
 t/angles.t                                    |  93 ----------
 t/avoid_crossing_perimeters.t                 |  22 ---
 t/bridges.t                                   | 137 --------------
 t/collinear.t                                 |  87 ---------
 t/geometry.t                                  |  64 +------
 tests/fff_print/CMakeLists.txt                |   2 +
 .../test_avoid_crossing_perimeters.cpp        |  16 ++
 tests/fff_print/test_bridges.cpp              | 133 ++++++++++++++
 tests/fff_print/test_fill.cpp                 |   3 +-
 tests/libslic3r/test_clipper_utils.cpp        |   2 +-
 xs/CMakeLists.txt                             |   2 -
 xs/lib/Slic3r/XS.pm                           |   7 -
 xs/src/perlglue.cpp                           |   2 -
 xs/t/04_expolygon.t                           |  26 +--
 xs/xsp/BridgeDetector.xsp                     |  43 -----
 xs/xsp/ExPolygonCollection.xsp                |  81 ---------
 xs/xsp/ExtrusionPath.xsp                      |  16 --
 xs/xsp/Geometry.xsp                           |   8 -
 xs/xsp/Layer.xsp                              |   4 -
 xs/xsp/my.map                                 |   8 -
 xs/xsp/typemap.xspt                           |   6 -
 42 files changed, 204 insertions(+), 1006 deletions(-)
 delete mode 100644 src/libslic3r/ExPolygonCollection.cpp
 delete mode 100644 src/libslic3r/ExPolygonCollection.hpp
 delete mode 100644 t/angles.t
 delete mode 100644 t/avoid_crossing_perimeters.t
 delete mode 100644 t/bridges.t
 delete mode 100644 t/collinear.t
 create mode 100644 tests/fff_print/test_avoid_crossing_perimeters.cpp
 create mode 100644 tests/fff_print/test_bridges.cpp
 delete mode 100644 xs/xsp/BridgeDetector.xsp
 delete mode 100644 xs/xsp/ExPolygonCollection.xsp

diff --git a/lib/Slic3r/ExPolygon.pm b/lib/Slic3r/ExPolygon.pm
index 6090a073b..1b55849eb 100644
--- a/lib/Slic3r/ExPolygon.pm
+++ b/lib/Slic3r/ExPolygon.pm
@@ -9,11 +9,4 @@ sub bounding_box {
     return $self->contour->bounding_box;
 }
 
-package Slic3r::ExPolygon::Collection;
-
-sub size {
-    my $self = shift;
-    return [ Slic3r::Geometry::size_2D([ map @$_, map @$_, @$self ]) ];
-}
-
 1;
diff --git a/lib/Slic3r/Geometry.pm b/lib/Slic3r/Geometry.pm
index ca262fc76..b3c57fcd0 100644
--- a/lib/Slic3r/Geometry.pm
+++ b/lib/Slic3r/Geometry.pm
@@ -9,13 +9,6 @@ our @ISA = qw(Exporter);
 our @EXPORT_OK = qw(
     PI epsilon 
 
-    angle3points
-    collinear
-    dot
-    line_intersection
-    normalize
-    polyline_lines
-    polygon_is_convex
     scale
     unscale
     scaled_epsilon
@@ -26,7 +19,6 @@ our @EXPORT_OK = qw(
     chained_path_from
     deg2rad
     rad2deg
-    rad2deg_dir
 );
 
 use constant PI => 4 * atan2(1, 1);
@@ -43,136 +35,6 @@ sub scaled_epsilon () { epsilon / &Slic3r::SCALING_FACTOR }
 sub scale   ($) { $_[0] / &Slic3r::SCALING_FACTOR }
 sub unscale ($) { $_[0] * &Slic3r::SCALING_FACTOR }
 
-# used by geometry.t
-sub polyline_lines {
-    my ($polyline) = @_;
-    my @points = @$polyline;
-    return map Slic3r::Line->new(@points[$_, $_+1]), 0 .. $#points-1;
-}
-
-# polygon must be simple (non complex) and ccw
-sub polygon_is_convex {
-    my ($points) = @_;
-    for (my $i = 0; $i <= $#$points; $i++) {
-        my $angle = angle3points($points->[$i-1], $points->[$i-2], $points->[$i]);
-        return 0 if $angle < PI;
-    }
-    return 1;
-}
-
-sub normalize {
-    my ($line) = @_;
-    
-    my $len = sqrt( ($line->[X]**2) + ($line->[Y]**2) + ($line->[Z]**2) )
-        or return [0, 0, 0];  # to avoid illegal division by zero
-    return [ map $_ / $len, @$line ];
-}
-
-# 2D dot product
-# used by 3DScene.pm
-sub dot {
-    my ($u, $v) = @_;
-    return $u->[X] * $v->[X] + $u->[Y] * $v->[Y];
-}
-
-sub line_intersection {
-    my ($line1, $line2, $require_crossing) = @_;
-    $require_crossing ||= 0;
-    
-    my $intersection = _line_intersection(map @$_, @$line1, @$line2);
-    return (ref $intersection && $intersection->[1] == $require_crossing) 
-        ? $intersection->[0] 
-        : undef;
-}
-
-# Used by test cases.
-sub collinear {
-    my ($line1, $line2, $require_overlapping) = @_;
-    my $intersection = _line_intersection(map @$_, @$line1, @$line2);
-    return 0 unless !ref($intersection) 
-        && ($intersection eq 'parallel collinear'
-            || ($intersection eq 'parallel vertical' && abs($line1->[A][X] - $line2->[A][X]) < epsilon));
-    
-    if ($require_overlapping) {
-        my @box_a = bounding_box([ $line1->[0], $line1->[1] ]);
-        my @box_b = bounding_box([ $line2->[0], $line2->[1] ]);
-        return 0 unless bounding_box_intersect( 2, @box_a, @box_b );
-    }
-    
-    return 1;
-}
-
-sub _line_intersection {
-  my ( $x0, $y0, $x1, $y1, $x2, $y2, $x3, $y3 ) = @_;
-
-  my ($x, $y);  # The as-yet-undetermined intersection point.
-
-  my $dy10 = $y1 - $y0; # dyPQ, dxPQ are the coordinate differences
-  my $dx10 = $x1 - $x0; # between the points P and Q.
-  my $dy32 = $y3 - $y2;
-  my $dx32 = $x3 - $x2;
-
-  my $dy10z = abs( $dy10 ) < epsilon; # Is the difference $dy10 "zero"?
-  my $dx10z = abs( $dx10 ) < epsilon;
-  my $dy32z = abs( $dy32 ) < epsilon;
-  my $dx32z = abs( $dx32 ) < epsilon;
-
-  my $dyx10;                            # The slopes.
-  my $dyx32;
-  
-  $dyx10 = $dy10 / $dx10 unless $dx10z;
-  $dyx32 = $dy32 / $dx32 unless $dx32z;
-
-  # Now we know all differences and the slopes;
-  # we can detect horizontal/vertical special cases.
-  # E.g., slope = 0 means a horizontal line.
-
-  unless ( defined $dyx10 or defined $dyx32 ) {
-    return "parallel vertical";
-  }
-  elsif ( $dy10z and not $dy32z ) { # First line horizontal.
-    $y = $y0;
-    $x = $x2 + ( $y - $y2 ) * $dx32 / $dy32;
-  }
-  elsif ( not $dy10z and $dy32z ) { # Second line horizontal.
-    $y = $y2;
-    $x = $x0 + ( $y - $y0 ) * $dx10 / $dy10;
-  }
-  elsif ( $dx10z and not $dx32z ) { # First line vertical.
-    $x = $x0;
-    $y = $y2 + $dyx32 * ( $x - $x2 );
-  }
-  elsif ( not $dx10z and $dx32z ) { # Second line vertical.
-    $x = $x2;
-    $y = $y0 + $dyx10 * ( $x - $x0 );
-  }
-  elsif ( abs( $dyx10 - $dyx32 ) < epsilon ) {
-    # The slopes are suspiciously close to each other.
-    # Either we have parallel collinear or just parallel lines.
-
-    # The bounding box checks have already weeded the cases
-    # "parallel horizontal" and "parallel vertical" away.
-
-    my $ya = $y0 - $dyx10 * $x0;
-    my $yb = $y2 - $dyx32 * $x2;
-    
-    return "parallel collinear" if abs( $ya - $yb ) < epsilon;
-    return "parallel";
-  }
-  else {
-    # None of the special cases matched.
-    # We have a "honest" line intersection.
-
-    $x = ($y2 - $y0 + $dyx10*$x0 - $dyx32*$x2)/($dyx10 - $dyx32);
-    $y = $y0 + $dyx10 * ($x - $x0);
-  }
-
-  my $h10 = $dx10 ? ($x - $x0) / $dx10 : ($dy10 ? ($y - $y0) / $dy10 : 1);
-  my $h32 = $dx32 ? ($x - $x2) / $dx32 : ($dy32 ? ($y - $y2) / $dy32 : 1);
-
-  return [Slic3r::Point->new($x, $y), $h10 >= 0 && $h10 <= 1 && $h32 >= 0 && $h32 <= 1];
-}
-
 # 2D
 sub bounding_box {
     my ($points) = @_;
@@ -199,36 +61,4 @@ sub size_2D {
     );
 }
 
-# Used by sub collinear, which is used by test cases.
-# bounding_box_intersect($d, @a, @b)
-#   Return true if the given bounding boxes @a and @b intersect
-#   in $d dimensions.  Used by sub collinear.
-sub bounding_box_intersect {
-    my ( $d, @bb ) = @_; # Number of dimensions and box coordinates.
-    my @aa = splice( @bb, 0, 2 * $d ); # The first box.
-    # (@bb is the second one.)
-    
-    # Must intersect in all dimensions.
-    for ( my $i_min = 0; $i_min < $d; $i_min++ ) {
-        my $i_max = $i_min + $d; # The index for the maximum.
-        return 0 if ( $aa[ $i_max ] + epsilon ) < $bb[ $i_min ];
-        return 0 if ( $bb[ $i_max ] + epsilon ) < $aa[ $i_min ];
-    }
-    
-    return 1;
-}
-
-# Used by test cases.
-# this assumes a CCW rotation from $p2 to $p3 around $p1
-sub angle3points {
-    my ($p1, $p2, $p3) = @_;
-    # p1 is the center
-    
-    my $angle = atan2($p2->[X] - $p1->[X], $p2->[Y] - $p1->[Y])
-              - atan2($p3->[X] - $p1->[X], $p3->[Y] - $p1->[Y]);
-    
-    # we only want to return only positive angles
-    return $angle <= 0 ? $angle + 2*PI() : $angle;
-}
-
 1;
diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt
index 643afe69e..d2ecefb9c 100644
--- a/src/libslic3r/CMakeLists.txt
+++ b/src/libslic3r/CMakeLists.txt
@@ -40,8 +40,6 @@ set(SLIC3R_SOURCES
     enum_bitmask.hpp
     ExPolygon.cpp
     ExPolygon.hpp
-    ExPolygonCollection.cpp
-    ExPolygonCollection.hpp
     Extruder.cpp
     Extruder.hpp
     ExtrusionEntity.cpp
diff --git a/src/libslic3r/ClipperUtils.cpp b/src/libslic3r/ClipperUtils.cpp
index 25454d500..5c4b5ac17 100644
--- a/src/libslic3r/ClipperUtils.cpp
+++ b/src/libslic3r/ClipperUtils.cpp
@@ -240,7 +240,7 @@ TResult clipper_union(
 
 // Perform union of input polygons using the positive rule, convert to ExPolygons.
 //FIXME is there any benefit of not doing the boolean / using pftEvenOdd?
-ExPolygons ClipperPaths_to_Slic3rExPolygons(const ClipperLib::Paths &input, bool do_union)
+inline ExPolygons ClipperPaths_to_Slic3rExPolygons(const ClipperLib::Paths &input, bool do_union)
 {
     return PolyTreeToExPolygons(clipper_union<ClipperLib::PolyTree>(input, do_union ? ClipperLib::pftNonZero : ClipperLib::pftEvenOdd));
 }
@@ -438,7 +438,7 @@ Slic3r::Polygons offset(const Slic3r::SurfacesPtr &surfaces, const float delta,
     { return to_polygons(expolygons_offset(surfaces, delta, joinType, miterLimit)); }
 Slic3r::ExPolygons offset_ex(const Slic3r::ExPolygon &expolygon, const float delta, ClipperLib::JoinType joinType, double miterLimit)
     //FIXME one may spare one Clipper Union call.
-    { return ClipperPaths_to_Slic3rExPolygons(expolygon_offset(expolygon, delta, joinType, miterLimit)); }
+    { return ClipperPaths_to_Slic3rExPolygons(expolygon_offset(expolygon, delta, joinType, miterLimit), /* do union */ false); }
 Slic3r::ExPolygons offset_ex(const Slic3r::ExPolygons &expolygons, const float delta, ClipperLib::JoinType joinType, double miterLimit)
     { return PolyTreeToExPolygons(expolygons_offset_pt(expolygons, delta, joinType, miterLimit)); }
 Slic3r::ExPolygons offset_ex(const Slic3r::Surfaces &surfaces, const float delta, ClipperLib::JoinType joinType, double miterLimit)
@@ -713,6 +713,8 @@ Slic3r::Polylines intersection_pl(const Slic3r::Polylines &subject, const Slic3r
     { return _clipper_pl_open(ClipperLib::ctIntersection, ClipperUtils::PolylinesProvider(subject), ClipperUtils::SinglePathProvider(clip.points)); }
 Slic3r::Polylines intersection_pl(const Slic3r::Polyline &subject, const Slic3r::Polygons &clip)
     { return _clipper_pl_open(ClipperLib::ctIntersection, ClipperUtils::SinglePathProvider(subject.points), ClipperUtils::PolygonsProvider(clip)); }
+Slic3r::Polylines intersection_pl(const Slic3r::Polyline &subject, const Slic3r::ExPolygons &clip)
+    { return _clipper_pl_open(ClipperLib::ctIntersection, ClipperUtils::SinglePathProvider(subject.points), ClipperUtils::ExPolygonsProvider(clip)); }
 Slic3r::Polylines intersection_pl(const Slic3r::Polylines &subject, const Slic3r::Polygons &clip)
     { return _clipper_pl_open(ClipperLib::ctIntersection, ClipperUtils::PolylinesProvider(subject), ClipperUtils::PolygonsProvider(clip)); }
 Slic3r::Polylines intersection_pl(const Slic3r::Polylines &subject, const Slic3r::ExPolygons &clip)
diff --git a/src/libslic3r/ClipperUtils.hpp b/src/libslic3r/ClipperUtils.hpp
index d7027e0ec..484229f72 100644
--- a/src/libslic3r/ClipperUtils.hpp
+++ b/src/libslic3r/ClipperUtils.hpp
@@ -1,17 +1,27 @@
 #ifndef slic3r_ClipperUtils_hpp_
 #define slic3r_ClipperUtils_hpp_
 
+//#define SLIC3R_USE_CLIPPER2
+
 #include "libslic3r.h"
-#include "clipper.hpp"
 #include "ExPolygon.hpp"
 #include "Polygon.hpp"
 #include "Surface.hpp"
 
+#ifdef SLIC3R_USE_CLIPPER2
+
+#include <clipper2.clipper.h>
+
+#else /* SLIC3R_USE_CLIPPER2 */
+
+#include "clipper.hpp"
 // import these wherever we're included
 using Slic3r::ClipperLib::jtMiter;
 using Slic3r::ClipperLib::jtRound;
 using Slic3r::ClipperLib::jtSquare;
 
+#endif /* SLIC3R_USE_CLIPPER2 */
+
 namespace Slic3r {
 
 static constexpr const float                        ClipperSafetyOffset     = 10.f;
@@ -298,9 +308,6 @@ namespace ClipperUtils {
     };
 }
 
-// Perform union of input polygons using the non-zero rule, convert to ExPolygons.
-ExPolygons ClipperPaths_to_Slic3rExPolygons(const ClipperLib::Paths &input, bool do_union = false);
-
 // offset Polygons
 // Wherever applicable, please use the expand() / shrink() variants instead, they convey their purpose better.
 Slic3r::Polygons offset(const Slic3r::Polygon &polygon, const float delta, ClipperLib::JoinType joinType = DefaultJoinType, double miterLimit = DefaultMiterLimit);
@@ -429,6 +436,7 @@ Slic3r::ExPolygons intersection_ex(const Slic3r::Surfaces &subject, const Slic3r
 Slic3r::ExPolygons intersection_ex(const Slic3r::SurfacesPtr &subject, const Slic3r::ExPolygons &clip, ApplySafetyOffset do_safety_offset = ApplySafetyOffset::No);
 Slic3r::Polylines  intersection_pl(const Slic3r::Polylines &subject, const Slic3r::Polygon &clip);
 Slic3r::Polylines  intersection_pl(const Slic3r::Polyline &subject, const Slic3r::Polygons &clip);
+Slic3r::Polylines  intersection_pl(const Slic3r::Polyline &subject, const Slic3r::ExPolygons &clip);
 Slic3r::Polylines  intersection_pl(const Slic3r::Polylines &subject, const Slic3r::Polygons &clip);
 Slic3r::Polylines  intersection_pl(const Slic3r::Polylines &subject, const Slic3r::ExPolygons &clip);
 Slic3r::Polylines  intersection_pl(const Slic3r::Polygons &subject, const Slic3r::Polygons &clip);
diff --git a/src/libslic3r/EdgeGrid.cpp b/src/libslic3r/EdgeGrid.cpp
index 1385a51d8..4985b788e 100644
--- a/src/libslic3r/EdgeGrid.cpp
+++ b/src/libslic3r/EdgeGrid.cpp
@@ -136,11 +136,6 @@ void EdgeGrid::Grid::create(const ExPolygons &expolygons, coord_t resolution)
 	create_from_m_contours(resolution);
 }
 
-void EdgeGrid::Grid::create(const ExPolygonCollection &expolygons, coord_t resolution)
-{
-	create(expolygons.expolygons, resolution);
-}
-
 // m_contours has been initialized. Now fill in the edge grid.
 void EdgeGrid::Grid::create_from_m_contours(coord_t resolution)
 {
diff --git a/src/libslic3r/EdgeGrid.hpp b/src/libslic3r/EdgeGrid.hpp
index 3c9929149..4be2bdd07 100644
--- a/src/libslic3r/EdgeGrid.hpp
+++ b/src/libslic3r/EdgeGrid.hpp
@@ -7,7 +7,6 @@
 #include "Point.hpp"
 #include "BoundingBox.hpp"
 #include "ExPolygon.hpp"
-#include "ExPolygonCollection.hpp"
 
 namespace Slic3r {
 namespace EdgeGrid {
@@ -112,7 +111,6 @@ public:
 	void create(const std::vector<Points> &polygons, coord_t resolution) { this->create(polygons, resolution, false); }
 	void create(const ExPolygon &expoly, coord_t resolution);
 	void create(const ExPolygons &expolygons, coord_t resolution);
-	void create(const ExPolygonCollection &expolygons, coord_t resolution);
 
 	const std::vector<Contour>& contours() const { return m_contours; }
 
@@ -123,7 +121,6 @@ public:
 	bool intersect(const Polygons &polygons) { for (size_t i = 0; i < polygons.size(); ++ i) if (intersect(polygons[i])) return true; return false; }
 	bool intersect(const ExPolygon &expoly) { if (intersect(expoly.contour)) return true; for (size_t i = 0; i < expoly.holes.size(); ++ i) if (intersect(expoly.holes[i])) return true; return false; }
 	bool intersect(const ExPolygons &expolygons) { for (size_t i = 0; i < expolygons.size(); ++ i) if (intersect(expolygons[i])) return true; return false; }
-	bool intersect(const ExPolygonCollection &expolygons) { return intersect(expolygons.expolygons); }
 
 	// Test, whether a point is inside a contour.
 	bool inside(const Point &pt);
@@ -391,7 +388,7 @@ protected:
 
 	// Referencing the source contours.
 	// This format allows one to work with any Slic3r fixed point contour format
-	// (Polygon, ExPolygon, ExPolygonCollection etc).
+	// (Polygon, ExPolygon, ExPolygons etc).
 	std::vector<Contour>						m_contours;
 
 	// Referencing a contour and a line segment of m_contours.
diff --git a/src/libslic3r/ExPolygon.hpp b/src/libslic3r/ExPolygon.hpp
index cbf6b1c1a..344450c4a 100644
--- a/src/libslic3r/ExPolygon.hpp
+++ b/src/libslic3r/ExPolygon.hpp
@@ -9,7 +9,7 @@
 namespace Slic3r {
 
 class ExPolygon;
-typedef std::vector<ExPolygon> ExPolygons;
+using ExPolygons = std::vector<ExPolygon>;
 
 class ExPolygon
 {
diff --git a/src/libslic3r/ExPolygonCollection.cpp b/src/libslic3r/ExPolygonCollection.cpp
deleted file mode 100644
index a0de8f6de..000000000
--- a/src/libslic3r/ExPolygonCollection.cpp
+++ /dev/null
@@ -1,136 +0,0 @@
-#include "ExPolygonCollection.hpp"
-#include "Geometry/ConvexHull.hpp"
-#include "BoundingBox.hpp"
-
-namespace Slic3r {
-
-ExPolygonCollection::ExPolygonCollection(const ExPolygon &expolygon)
-{
-    this->expolygons.push_back(expolygon);
-}
-
-ExPolygonCollection::operator Points() const
-{
-    Points points;
-    Polygons pp = (Polygons)*this;
-    for (Polygons::const_iterator poly = pp.begin(); poly != pp.end(); ++poly) {
-        for (Points::const_iterator point = poly->points.begin(); point != poly->points.end(); ++point)
-            points.push_back(*point);
-    }
-    return points;
-}
-
-ExPolygonCollection::operator Polygons() const
-{
-    Polygons polygons;
-    for (ExPolygons::const_iterator it = this->expolygons.begin(); it != this->expolygons.end(); ++it) {
-        polygons.push_back(it->contour);
-        for (Polygons::const_iterator ith = it->holes.begin(); ith != it->holes.end(); ++ith) {
-            polygons.push_back(*ith);
-        }
-    }
-    return polygons;
-}
-
-ExPolygonCollection::operator ExPolygons&()
-{
-    return this->expolygons;
-}
-
-void
-ExPolygonCollection::scale(double factor)
-{
-    for (ExPolygons::iterator it = expolygons.begin(); it != expolygons.end(); ++it) {
-        (*it).scale(factor);
-    }
-}
-
-void
-ExPolygonCollection::translate(double x, double y)
-{
-   for (ExPolygons::iterator it = expolygons.begin(); it != expolygons.end(); ++it) {
-        (*it).translate(x, y);
-    }
-}
-
-void
-ExPolygonCollection::rotate(double angle, const Point &center)
-{
-    for (ExPolygons::iterator it = expolygons.begin(); it != expolygons.end(); ++it) {
-        (*it).rotate(angle, center);
-    }
-}
-
-template <class T>
-bool ExPolygonCollection::contains(const T &item) const
-{
-    for (const ExPolygon &poly : this->expolygons)
-        if (poly.contains(item))
-            return true;
-    return false;
-}
-template bool ExPolygonCollection::contains<Point>(const Point &item) const;
-template bool ExPolygonCollection::contains<Line>(const Line &item) const;
-template bool ExPolygonCollection::contains<Polyline>(const Polyline &item) const;
-
-bool
-ExPolygonCollection::contains_b(const Point &point) const
-{
-    for (ExPolygons::const_iterator it = this->expolygons.begin(); it != this->expolygons.end(); ++it) {
-        if (it->contains_b(point)) return true;
-    }
-    return false;
-}
-
-void
-ExPolygonCollection::simplify(double tolerance)
-{
-    ExPolygons expp;
-    for (ExPolygons::const_iterator it = this->expolygons.begin(); it != this->expolygons.end(); ++it) {
-        it->simplify(tolerance, &expp);
-    }
-    this->expolygons = expp;
-}
-
-Polygon
-ExPolygonCollection::convex_hull() const
-{
-    Points pp;
-    for (ExPolygons::const_iterator it = this->expolygons.begin(); it != this->expolygons.end(); ++it)
-        pp.insert(pp.end(), it->contour.points.begin(), it->contour.points.end());
-    return Slic3r::Geometry::convex_hull(pp);
-}
-
-Lines
-ExPolygonCollection::lines() const
-{
-    Lines lines;
-    for (ExPolygons::const_iterator it = this->expolygons.begin(); it != this->expolygons.end(); ++it) {
-        Lines ex_lines = it->lines();
-        lines.insert(lines.end(), ex_lines.begin(), ex_lines.end());
-    }
-    return lines;
-}
-
-Polygons
-ExPolygonCollection::contours() const
-{
-    Polygons contours;
-    contours.reserve(this->expolygons.size());
-    for (ExPolygons::const_iterator it = this->expolygons.begin(); it != this->expolygons.end(); ++it)
-        contours.push_back(it->contour);
-    return contours;
-}
-
-void
-ExPolygonCollection::append(const ExPolygons &expp)
-{
-    this->expolygons.insert(this->expolygons.end(), expp.begin(), expp.end());
-}
-
-BoundingBox get_extents(const ExPolygonCollection &expolygon)
-{
-    return get_extents(expolygon.expolygons);
-}
-
-}
diff --git a/src/libslic3r/ExPolygonCollection.hpp b/src/libslic3r/ExPolygonCollection.hpp
deleted file mode 100644
index 35e1eef4e..000000000
--- a/src/libslic3r/ExPolygonCollection.hpp
+++ /dev/null
@@ -1,41 +0,0 @@
-#ifndef slic3r_ExPolygonCollection_hpp_
-#define slic3r_ExPolygonCollection_hpp_
-
-#include "libslic3r.h"
-#include "ExPolygon.hpp"
-#include "Line.hpp"
-#include "Polyline.hpp"
-
-namespace Slic3r {
-
-class ExPolygonCollection;
-typedef std::vector<ExPolygonCollection> ExPolygonCollections;
-
-class ExPolygonCollection
-{
-public:
-    ExPolygons expolygons;
-    
-    ExPolygonCollection() {}
-    explicit ExPolygonCollection(const ExPolygon &expolygon);
-    explicit ExPolygonCollection(const ExPolygons &expolygons) : expolygons(expolygons) {}
-    explicit operator Points() const;
-    explicit operator Polygons() const;
-    explicit operator ExPolygons&();
-    void scale(double factor);
-    void translate(double x, double y);
-    void rotate(double angle, const Point &center);
-    template <class T> bool contains(const T &item) const;
-    bool contains_b(const Point &point) const;
-    void simplify(double tolerance);
-    Polygon convex_hull() const;
-    Lines lines() const;
-    Polygons contours() const;
-    void append(const ExPolygons &expolygons);
-};
-
-extern BoundingBox get_extents(const ExPolygonCollection &expolygon);
-
-}
-
-#endif
diff --git a/src/libslic3r/ExtrusionEntity.cpp b/src/libslic3r/ExtrusionEntity.cpp
index 7b2506a22..0c1165316 100644
--- a/src/libslic3r/ExtrusionEntity.cpp
+++ b/src/libslic3r/ExtrusionEntity.cpp
@@ -1,6 +1,6 @@
 #include "ExtrusionEntity.hpp"
 #include "ExtrusionEntityCollection.hpp"
-#include "ExPolygonCollection.hpp"
+#include "ExPolygon.hpp"
 #include "ClipperUtils.hpp"
 #include "Extruder.hpp"
 #include "Flow.hpp"
@@ -12,14 +12,14 @@
 
 namespace Slic3r {
     
-void ExtrusionPath::intersect_expolygons(const ExPolygonCollection &collection, ExtrusionEntityCollection* retval) const
+void ExtrusionPath::intersect_expolygons(const ExPolygons &collection, ExtrusionEntityCollection* retval) const
 {
-    this->_inflate_collection(intersection_pl(Polylines{ polyline }, collection.expolygons), retval);
+    this->_inflate_collection(intersection_pl(Polylines{ polyline }, collection), retval);
 }
 
-void ExtrusionPath::subtract_expolygons(const ExPolygonCollection &collection, ExtrusionEntityCollection* retval) const
+void ExtrusionPath::subtract_expolygons(const ExPolygons &collection, ExtrusionEntityCollection* retval) const
 {
-    this->_inflate_collection(diff_pl(Polylines{ this->polyline }, collection.expolygons), retval);
+    this->_inflate_collection(diff_pl(Polylines{ this->polyline }, collection), retval);
 }
 
 void ExtrusionPath::clip_end(double distance)
diff --git a/src/libslic3r/ExtrusionEntity.hpp b/src/libslic3r/ExtrusionEntity.hpp
index 2e9e46789..52a8a563c 100644
--- a/src/libslic3r/ExtrusionEntity.hpp
+++ b/src/libslic3r/ExtrusionEntity.hpp
@@ -11,7 +11,8 @@
 
 namespace Slic3r {
 
-class ExPolygonCollection;
+class ExPolygon;
+using ExPolygons = std::vector<ExPolygon>;
 class ExtrusionEntityCollection;
 class Extruder;
 
@@ -144,12 +145,12 @@ public:
     size_t size() const { return this->polyline.size(); }
     bool empty() const { return this->polyline.empty(); }
     bool is_closed() const { return ! this->empty() && this->polyline.points.front() == this->polyline.points.back(); }
-    // Produce a list of extrusion paths into retval by clipping this path by ExPolygonCollection.
+    // Produce a list of extrusion paths into retval by clipping this path by ExPolygons.
     // Currently not used.
-    void intersect_expolygons(const ExPolygonCollection &collection, ExtrusionEntityCollection* retval) const;
-    // Produce a list of extrusion paths into retval by removing parts of this path by ExPolygonCollection.
+    void intersect_expolygons(const ExPolygons &collection, ExtrusionEntityCollection* retval) const;
+    // Produce a list of extrusion paths into retval by removing parts of this path by ExPolygons.
     // Currently not used.
-    void subtract_expolygons(const ExPolygonCollection &collection, ExtrusionEntityCollection* retval) const;
+    void subtract_expolygons(const ExPolygons &collection, ExtrusionEntityCollection* retval) const;
     void clip_end(double distance);
     void simplify(double tolerance);
     double length() const override;
diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp
index 1a6ee4b80..aef83f21f 100644
--- a/src/libslic3r/GCode.cpp
+++ b/src/libslic3r/GCode.cpp
@@ -3063,7 +3063,7 @@ bool GCode::needs_retraction(const Polyline &travel, ExtrusionRole role)
     if (role == erSupportMaterial) {
         const SupportLayer* support_layer = dynamic_cast<const SupportLayer*>(m_layer);
         //FIXME support_layer->support_islands.contains should use some search structure!
-        if (support_layer != NULL && support_layer->support_islands.contains(travel))
+        if (support_layer != NULL && ! intersection_pl(travel, support_layer->support_islands).empty())
             // skip retraction if this is a travel move inside a support material island
             //FIXME not retracting over a long path may cause oozing, which in turn may result in missing material
             // at the end of the extrusion path!
diff --git a/src/libslic3r/Geometry.cpp b/src/libslic3r/Geometry.cpp
index 00fac6c38..6eae83277 100644
--- a/src/libslic3r/Geometry.cpp
+++ b/src/libslic3r/Geometry.cpp
@@ -50,13 +50,6 @@ bool contains(const std::vector<T> &vector, const Point &point)
 }
 template bool contains(const ExPolygons &vector, const Point &point);
 
-double rad2deg_dir(double angle)
-{
-    angle = (angle < PI) ? (-angle + PI/2.0) : (angle + PI/2.0);
-    if (angle < 0) angle += PI;
-    return rad2deg(angle);
-}
-
 void simplify_polygons(const Polygons &polygons, double tolerance, Polygons* retval)
 {
     Polygons pp;
diff --git a/src/libslic3r/Geometry.hpp b/src/libslic3r/Geometry.hpp
index 82ffbd8d1..2ca4ef884 100644
--- a/src/libslic3r/Geometry.hpp
+++ b/src/libslic3r/Geometry.hpp
@@ -291,7 +291,6 @@ bool directions_parallel(double angle1, double angle2, double max_diff = 0);
 bool directions_perpendicular(double angle1, double angle2, double max_diff = 0);
 template<class T> bool contains(const std::vector<T> &vector, const Point &point);
 template<typename T> T rad2deg(T angle) { return T(180.0) * angle / T(PI); }
-double rad2deg_dir(double angle);
 template<typename T> constexpr T deg2rad(const T angle) { return T(PI) * angle / T(180.0); }
 template<typename T> T angle_to_0_2PI(T angle)
 {
diff --git a/src/libslic3r/Geometry/ConvexHull.cpp b/src/libslic3r/Geometry/ConvexHull.cpp
index 2e92535f2..9601069b5 100644
--- a/src/libslic3r/Geometry/ConvexHull.cpp
+++ b/src/libslic3r/Geometry/ConvexHull.cpp
@@ -104,6 +104,17 @@ Polygon convex_hull(const Polygons &polygons)
     return convex_hull(std::move(pp));
 }
 
+Polygon convex_hull(const ExPolygons &expolygons)
+{
+    Points pp;
+    size_t sz = 0;
+    for (const auto &expoly : expolygons)
+        sz += expoly.contour.size();
+    pp.reserve(sz);
+    for (const auto &expoly : expolygons)
+        pp.insert(pp.end(), expoly.contour.points.begin(), expoly.contour.points.end());
+    return convex_hull(pp);
+}
 
 namespace rotcalip {
 
diff --git a/src/libslic3r/Geometry/ConvexHull.hpp b/src/libslic3r/Geometry/ConvexHull.hpp
index 03f00af6a..9ba957824 100644
--- a/src/libslic3r/Geometry/ConvexHull.hpp
+++ b/src/libslic3r/Geometry/ConvexHull.hpp
@@ -9,6 +9,7 @@ namespace Geometry {
 Pointf3s convex_hull(Pointf3s points);
 Polygon convex_hull(Points points);
 Polygon convex_hull(const Polygons &polygons);
+Polygon convex_hull(const ExPolygons &expolygons);
 
 // Returns true if the intersection of the two convex polygons A and B
 // is not an empty set.
diff --git a/src/libslic3r/Layer.hpp b/src/libslic3r/Layer.hpp
index 0fe4952f4..2e3affec7 100644
--- a/src/libslic3r/Layer.hpp
+++ b/src/libslic3r/Layer.hpp
@@ -5,10 +5,11 @@
 #include "Flow.hpp"
 #include "SurfaceCollection.hpp"
 #include "ExtrusionEntityCollection.hpp"
-#include "ExPolygonCollection.hpp"
 
 namespace Slic3r {
 
+class ExPolygon;
+using ExPolygons = std::vector<ExPolygon>;
 class Layer;
 using LayerPtrs = std::vector<Layer*>;
 class LayerRegion;
@@ -191,7 +192,7 @@ class SupportLayer : public Layer
 public:
     // Polygons covered by the supports: base, interface and contact areas.
     // Used to suppress retraction if moving for a support extrusion over these support_islands.
-    ExPolygonCollection         support_islands;
+    ExPolygons                  support_islands;
     // Extrusion paths for the support base and for the support interface and contacts.
     ExtrusionEntityCollection   support_fills;
 
diff --git a/src/libslic3r/Polyline.cpp b/src/libslic3r/Polyline.cpp
index 43c5afe73..6994ef425 100644
--- a/src/libslic3r/Polyline.cpp
+++ b/src/libslic3r/Polyline.cpp
@@ -2,7 +2,6 @@
 #include "Polyline.hpp"
 #include "Exception.hpp"
 #include "ExPolygon.hpp"
-#include "ExPolygonCollection.hpp"
 #include "Line.hpp"
 #include "Polygon.hpp"
 #include <iostream>
diff --git a/src/libslic3r/SLA/SupportPointGenerator.cpp b/src/libslic3r/SLA/SupportPointGenerator.cpp
index c32da0431..4c1af03eb 100644
--- a/src/libslic3r/SLA/SupportPointGenerator.cpp
+++ b/src/libslic3r/SLA/SupportPointGenerator.cpp
@@ -1,17 +1,14 @@
-//#include "igl/random_points_on_mesh.h"
-//#include "igl/AABB.h"
-
 #include <tbb/parallel_for.h>
 
 #include "SupportPointGenerator.hpp"
 #include "Concurrency.hpp"
+#include "Geometry/ConvexHull.hpp"
 #include "Model.hpp"
 #include "ExPolygon.hpp"
 #include "SVG.hpp"
 #include "Point.hpp"
 #include "ClipperUtils.hpp"
 #include "Tesselate.hpp"
-#include "ExPolygonCollection.hpp"
 #include "MinAreaBoundingBox.hpp"
 #include "libslic3r.h"
 
@@ -550,7 +547,7 @@ void SupportPointGenerator::uniformly_cover(const ExPolygons& islands, Structure
 //    auto bb = get_extents(islands);
 
     if (flags & icfIsNew) {
-        auto chull = ExPolygonCollection{islands}.convex_hull();
+        auto chull = Geometry::convex_hull(islands);
         auto rotbox = MinAreaBoundigBox{chull, MinAreaBoundigBox::pcConvex};
         Vec2d bbdim = {unscaled(rotbox.width()), unscaled(rotbox.height())};
 
diff --git a/src/libslic3r/SupportMaterial.cpp b/src/libslic3r/SupportMaterial.cpp
index 195fc9e17..52cb177bb 100644
--- a/src/libslic3r/SupportMaterial.cpp
+++ b/src/libslic3r/SupportMaterial.cpp
@@ -4283,7 +4283,7 @@ void PrintObjectSupportMaterial::generate_toolpaths(
                 std::stable_sort(layer_cache_item.overlapping.begin(), layer_cache_item.overlapping.end(), [](auto *l1, auto *l2) { return *l1 < *l2; });
             }
             if (! polys.empty())
-                expolygons_append(support_layer.support_islands.expolygons, union_ex(polys));
+                expolygons_append(support_layer.support_islands, union_ex(polys));
         } // for each support_layer_id
     });
 
diff --git a/t/angles.t b/t/angles.t
deleted file mode 100644
index 9dc690dea..000000000
--- a/t/angles.t
+++ /dev/null
@@ -1,93 +0,0 @@
-use Test::More;
-use strict;
-use warnings;
-
-plan tests => 34;
-
-BEGIN {
-    use FindBin;
-    use lib "$FindBin::Bin/../lib";
-    use local::lib "$FindBin::Bin/../local-lib";
-}
-
-use Slic3r;
-use Slic3r::Geometry qw(rad2deg_dir angle3points PI);
-
-#==========================================================
-
-{
-    is line_atan([ [0, 0],  [10, 0] ]),  (0),      'E atan2';
-    is line_atan([ [10, 0], [0, 0]  ]),  (PI),     'W atan2';
-    is line_atan([ [0, 0],  [0, 10] ]),  (PI/2),   'N atan2';
-    is line_atan([ [0, 10], [0, 0]  ]), -(PI/2),   'S atan2';
-    
-    is line_atan([ [10, 10], [0, 0] ]), -(PI*3/4), 'SW atan2';
-    is line_atan([ [0, 0], [10, 10] ]),  (PI*1/4), 'NE atan2';
-    is line_atan([ [0, 10], [10, 0] ]), -(PI*1/4), 'SE atan2';
-    is line_atan([ [10, 0], [0, 10] ]),  (PI*3/4), 'NW atan2';
-}
-
-#==========================================================
-
-{
-    is line_orientation([ [0, 0],  [10, 0] ]),  (0),      'E orientation';
-    is line_orientation([ [0, 0],  [0, 10] ]),  (PI/2),   'N orientation';
-    is line_orientation([ [10, 0], [0, 0]  ]),  (PI),     'W orientation';
-    is line_orientation([ [0, 10], [0, 0]  ]),  (PI*3/2), 'S orientation';
-    
-    is line_orientation([ [0, 0], [10, 10] ]),  (PI*1/4), 'NE orientation';
-    is line_orientation([ [10, 0], [0, 10] ]),  (PI*3/4), 'NW orientation';
-    is line_orientation([ [10, 10], [0, 0] ]),  (PI*5/4), 'SW orientation';
-    is line_orientation([ [0, 10], [10, 0] ]),  (PI*7/4), 'SE orientation';
-}
-
-#==========================================================
-
-{
-    is line_direction([ [0, 0],  [10, 0] ]), (0),      'E direction';
-    is line_direction([ [10, 0], [0, 0]  ]), (0),      'W direction';
-    is line_direction([ [0, 0],  [0, 10] ]), (PI/2),   'N direction';
-    is line_direction([ [0, 10], [0, 0]  ]), (PI/2),   'S direction';
-    
-    is line_direction([ [10, 10], [0, 0] ]), (PI*1/4), 'SW direction';
-    is line_direction([ [0, 0], [10, 10] ]), (PI*1/4), 'NE direction';
-    is line_direction([ [0, 10], [10, 0] ]), (PI*3/4), 'SE direction';
-    is line_direction([ [10, 0], [0, 10] ]), (PI*3/4), 'NW direction';
-}
-
-#==========================================================
-
-{
-    is rad2deg_dir(0),        90, 'E (degrees)';
-    is rad2deg_dir(PI),      270, 'W (degrees)';
-    is rad2deg_dir(PI/2),      0, 'N (degrees)';
-    is rad2deg_dir(-(PI/2)), 180, 'S (degrees)';
-    is rad2deg_dir(PI*1/4),   45, 'NE (degrees)';
-    is rad2deg_dir(PI*3/4),  135, 'NW (degrees)';
-    is rad2deg_dir(PI/6),     60, '30°';
-    is rad2deg_dir(PI/6*2),   30, '60°';
-}
-
-#==========================================================
-
-{
-    is angle3points([0,0], [10,0], [0,10]), PI/2, 'CW angle3points';
-    is angle3points([0,0], [0,10], [10,0]), PI/2*3, 'CCW angle3points';
-}
-
-#==========================================================
-
-sub line_atan {
-    my ($l) = @_;
-    return Slic3r::Line->new(@$l)->atan2_;
-}
-
-sub line_orientation {
-    my ($l) = @_;
-    return Slic3r::Line->new(@$l)->orientation;
-}
-
-sub line_direction {
-    my ($l) = @_;
-    return Slic3r::Line->new(@$l)->direction;
-}
\ No newline at end of file
diff --git a/t/avoid_crossing_perimeters.t b/t/avoid_crossing_perimeters.t
deleted file mode 100644
index 86c3e91cb..000000000
--- a/t/avoid_crossing_perimeters.t
+++ /dev/null
@@ -1,22 +0,0 @@
-use Test::More tests => 1;
-use strict;
-use warnings;
-
-BEGIN {
-    use FindBin;
-    use lib "$FindBin::Bin/../lib";
-    use local::lib "$FindBin::Bin/../local-lib";    
-}
-
-use List::Util qw(first sum);
-use Slic3r;
-use Slic3r::Test;
-
-{
-    my $config = Slic3r::Config::new_from_defaults;
-    $config->set('avoid_crossing_perimeters', 2);
-    my $print = Slic3r::Test::init_print('20mm_cube', config => $config, duplicate => 2);
-    ok my $gcode = Slic3r::Test::gcode($print), "no crash with avoid_crossing_perimeters and multiple objects";
-}
-
-__END__
diff --git a/t/bridges.t b/t/bridges.t
deleted file mode 100644
index ca55862b6..000000000
--- a/t/bridges.t
+++ /dev/null
@@ -1,137 +0,0 @@
-use Test::More tests => 16;
-use strict;
-use warnings;
-
-BEGIN {
-    use FindBin;
-    use lib "$FindBin::Bin/../lib";
-    use local::lib "$FindBin::Bin/../local-lib";
-}
-
-use List::Util qw(first sum);
-use Slic3r;
-use Slic3r::Geometry qw(scale epsilon deg2rad rad2deg);
-use Slic3r::Test;
-
-{
-    my $test = sub {
-        my ($bridge_size, $rotate, $expected_angle, $tolerance) = @_;
-    
-        my ($x, $y) = @$bridge_size;
-        my $lower = Slic3r::ExPolygon->new(
-            Slic3r::Polygon->new_scale([-2,-2], [$x+2,-2], [$x+2,$y+2], [-2,$y+2]),
-            Slic3r::Polygon->new_scale([0,0], [0,$y], [$x,$y], [$x,0]),
-        );
-        $lower->translate(scale 20, scale 20); # avoid negative coordinates for easier SVG preview
-        $lower->rotate(deg2rad($rotate), [$x/2,$y/2]);
-        my $bridge = $lower->[1]->clone;
-        $bridge->reverse;
-        $bridge = Slic3r::ExPolygon->new($bridge);
-        
-        ok check_angle([$lower], $bridge, $expected_angle, $tolerance), 'correct bridge angle for O-shaped overhang';
-    };
-
-    $test->([20,10], 0, 90);
-    $test->([10,20], 0, 0);
-    $test->([20,10], 45, 135, 20);
-    $test->([20,10], 135, 45, 20);
-}
-
-{
-    my $bridge = Slic3r::ExPolygon->new(
-        Slic3r::Polygon->new_scale([0,0], [20,0], [20,10], [0,10]),
-    );
-    my $lower = [
-        Slic3r::ExPolygon->new(
-            Slic3r::Polygon->new_scale([-2,0], [0,0], [0,10], [-2,10]),
-        ),
-    ];
-    $_->translate(scale 20, scale 20) for $bridge, @$lower; # avoid negative coordinates for easier SVG preview
-
-    $lower->[1] = $lower->[0]->clone;
-    $lower->[1]->translate(scale 22, 0);
-    
-    ok check_angle($lower, $bridge, 0), 'correct bridge angle for two-sided bridge';
-}
-
-{
-    my $bridge = Slic3r::ExPolygon->new(
-        Slic3r::Polygon->new_scale([0,0], [20,0], [10,10], [0,10]),
-    );
-    my $lower = [
-        Slic3r::ExPolygon->new(
-            Slic3r::Polygon->new_scale([0,0], [0,10], [10,10], [10,12], [-2,12], [-2,-2], [22,-2], [22,0]),
-        ),
-    ];
-    $_->translate(scale 20, scale 20) for $bridge, @$lower; # avoid negative coordinates for easier SVG preview
-    
-    ok check_angle($lower, $bridge, 135), 'correct bridge angle for C-shaped overhang';
-}
-
-{
-    my $bridge = Slic3r::ExPolygon->new(
-        Slic3r::Polygon->new_scale([10,10],[20,10],[20,20], [10,20]),
-    );
-    my $lower = [
-        Slic3r::ExPolygon->new(
-            Slic3r::Polygon->new_scale([10,10],[10,20],[20,20],[30,30],[0,30],[0,0]),
-        ),
-    ];
-    $_->translate(scale 20, scale 20) for $bridge, @$lower; # avoid negative coordinates for easier SVG preview
-    
-    ok check_angle($lower, $bridge, 45, undef, $bridge->area/2), 'correct bridge angle for square overhang with L-shaped anchors';
-}
-
-sub check_angle {
-    my ($lower, $bridge, $expected, $tolerance, $expected_coverage) = @_;
-    
-    if (ref($lower) eq 'ARRAY') {
-        $lower = Slic3r::ExPolygon::Collection->new(@$lower);
-    }
-    
-    $expected_coverage //= -1;
-    $expected_coverage = $bridge->area if $expected_coverage == -1;
-    
-    my $bd = Slic3r::BridgeDetector->new($bridge, $lower, scale 0.5);
-    
-    $tolerance //= rad2deg($bd->resolution) + epsilon;
-    $bd->detect_angle;
-    my $result = $bd->angle;
-    my $coverage = $bd->coverage;
-    is sum(map $_->area, @$coverage), $expected_coverage, 'correct coverage area';
-    
-    # our epsilon is equal to the steps used by the bridge detection algorithm
-    ###use XXX; YYY [ rad2deg($result), $expected ];
-    # returned value must be non-negative, check for that too
-    my $delta=rad2deg($result) - $expected;
-    $delta-=180 if $delta>=180 - epsilon;
-    return defined $result && $result>=0 && abs($delta) < $tolerance;
-}
-
-{
-    my $config = Slic3r::Config::new_from_defaults;
-    $config->set('top_solid_layers', 0);            # to prevent bridging on sparse infill
-    $config->set('bridge_speed', 99);
-    
-    my $print = Slic3r::Test::init_print('bridge', config => $config);
-    
-    my %extrusions = ();  # angle => length
-    Slic3r::GCode::Reader->new->parse(Slic3r::Test::gcode($print), sub {
-        my ($self, $cmd, $args, $info) = @_;
-        
-        if ($cmd eq 'G1' && ($args->{F} // $self->F)/60 == $config->bridge_speed) {
-            my $line = Slic3r::Line->new_scale(
-                [ $self->X, $self->Y ],
-                [ $info->{new_X}, $info->{new_Y} ],
-            );
-            my $angle = $line->direction;
-            $extrusions{$angle} //= 0;
-            $extrusions{$angle} += $line->length;
-        }
-    });
-    ok !!%extrusions, "bridge is generated";
-    my ($main_angle) = sort { $extrusions{$b} <=> $extrusions{$a} } keys %extrusions;
-    is $main_angle, 0, "bridge has the expected direction";
-}
-
-__END__
diff --git a/t/collinear.t b/t/collinear.t
deleted file mode 100644
index b28a3602c..000000000
--- a/t/collinear.t
+++ /dev/null
@@ -1,87 +0,0 @@
-use Test::More;
-use strict;
-use warnings;
-
-plan tests => 11;
-
-BEGIN {
-    use FindBin;
-    use lib "$FindBin::Bin/../lib";
-    use local::lib "$FindBin::Bin/../local-lib";
-}
-
-use Slic3r;
-use Slic3r::Geometry qw(collinear);
-
-#==========================================================
-
-{
-    my @lines = (
-        Slic3r::Line->new([0,4], [4,2]),
-        Slic3r::Line->new([2,3], [8,0]),
-        Slic3r::Line->new([6,1], [8,0]),
-    );
-    is collinear($lines[0], $lines[1]), 1, 'collinear';
-    is collinear($lines[1], $lines[2]), 1, 'collinear';
-    is collinear($lines[0], $lines[2]), 1, 'collinear';
-}
-
-#==========================================================
-
-{
-    # horizontal
-    my @lines = (
-        Slic3r::Line->new([0,1], [5,1]),
-        Slic3r::Line->new([2,1], [8,1]),
-    );
-    is collinear($lines[0], $lines[1]), 1, 'collinear';
-}
-
-#==========================================================
-
-{
-    # vertical
-    my @lines = (
-        Slic3r::Line->new([1,0], [1,5]),
-        Slic3r::Line->new([1,2], [1,8]),
-    );
-    is collinear($lines[0], $lines[1]), 1, 'collinear';
-}
-
-#==========================================================
-
-{
-    # non overlapping
-    my @lines = (
-        Slic3r::Line->new([0,1], [5,1]),
-        Slic3r::Line->new([7,1], [10,1]),
-    );
-    is collinear($lines[0], $lines[1], 1), 0, 'non overlapping';
-    is collinear($lines[0], $lines[1], 0), 1, 'overlapping';
-}
-
-#==========================================================
-
-{
-    # with one common point
-    my @lines = (
-        Slic3r::Line->new([0,4], [4,2]),
-        Slic3r::Line->new([4,2], [8,0]),
-    );
-    is collinear($lines[0], $lines[1], 1), 1, 'one common point';
-    is collinear($lines[0], $lines[1], 0), 1, 'one common point';
-}
-
-#==========================================================
-
-{
-    # not collinear
-    my @lines = (
-        Slic3r::Line->new([290000000,690525600], [285163380,684761540]),
-        Slic3r::Line->new([285163380,684761540], [193267599,575244400]),
-    );
-    is collinear($lines[0], $lines[1], 0), 0, 'not collinear';
-    is collinear($lines[0], $lines[1], 1), 0, 'not collinear';
-}
-
-#==========================================================
diff --git a/t/geometry.t b/t/geometry.t
index 12a1ca743..981621ee8 100644
--- a/t/geometry.t
+++ b/t/geometry.t
@@ -2,7 +2,7 @@ use Test::More;
 use strict;
 use warnings;
 
-plan tests => 26;
+plan tests => 11;
 
 BEGIN {
     use FindBin;
@@ -11,7 +11,7 @@ BEGIN {
 }
 
 use Slic3r;
-use Slic3r::Geometry qw(PI polygon_is_convex
+use Slic3r::Geometry qw(PI
     chained_path_from epsilon scale);
 
 {
@@ -27,18 +27,6 @@ use Slic3r::Geometry qw(PI polygon_is_convex
 
 #==========================================================
 
-my $line1 = [ [5, 15], [30, 15] ];
-my $line2 = [ [10, 20], [10, 10] ];
-is_deeply Slic3r::Geometry::line_intersection($line1, $line2, 1)->arrayref, [10, 15], 'line_intersection';
-
-#==========================================================
-
-$line1 = [ [73.6310778185108/0.0000001, 371.74239268924/0.0000001], [73.6310778185108/0.0000001, 501.74239268924/0.0000001] ];
-$line2 = [ [75/0.0000001, 437.9853/0.0000001], [62.7484/0.0000001, 440.4223/0.0000001] ];
-isnt Slic3r::Geometry::line_intersection($line1, $line2, 1), undef, 'line_intersection';
-
-#==========================================================
-
 my $polygons = [
     Slic3r::Polygon->new( # contour, ccw
         [45919000, 515273900], [14726100, 461246400], [14726100, 348753500], [33988700, 315389800], 
@@ -57,54 +45,6 @@ my $polygons = [
     ),
 ];
 
-#==========================================================
-
-{
-    my $p1 = [10, 10];
-    my $p2 = [10, 20];
-    my $p3 = [10, 30];
-    my $p4 = [20, 20];
-    my $p5 = [0,  20];
-    
-    is Slic3r::Geometry::angle3points($p2, $p3, $p1),  PI(),   'angle3points';
-    is Slic3r::Geometry::angle3points($p2, $p1, $p3),  PI(),   'angle3points';
-    is Slic3r::Geometry::angle3points($p2, $p3, $p4),  PI()/2*3, 'angle3points';
-    is Slic3r::Geometry::angle3points($p2, $p4, $p3),  PI()/2, 'angle3points';
-    is Slic3r::Geometry::angle3points($p2, $p1, $p4),  PI()/2, 'angle3points';
-    is Slic3r::Geometry::angle3points($p2, $p1, $p5),  PI()/2*3, 'angle3points';
-}
-
-{
-    my $p1 = [30, 30];
-    my $p2 = [20, 20];
-    my $p3 = [10, 10];
-    my $p4 = [30, 10];
-    
-    is Slic3r::Geometry::angle3points($p2, $p1, $p3), PI(),       'angle3points';
-    is Slic3r::Geometry::angle3points($p2, $p1, $p4), PI()/2*3,   'angle3points';
-    is Slic3r::Geometry::angle3points($p2, $p1, $p1), 2*PI(),     'angle3points';
-}
-
-#==========================================================
-
-{
-    my $cw_square = [ [0,0], [0,10], [10,10], [10,0] ];
-    is polygon_is_convex($cw_square), 0, 'cw square is not convex';
-    is polygon_is_convex([ reverse @$cw_square ]), 1, 'ccw square is convex';
-    
-    my $convex1 = [ [0,0], [10,0], [10,10], [0,10], [0,6], [4,6], [4,4], [0,4] ];
-    is polygon_is_convex($convex1), 0, 'concave polygon';
-}
-
-#==========================================================
-
-{
-    my $polyline = Slic3r::Polyline->new([0, 0], [10, 0], [20, 0]);
-    is_deeply [ map $_->pp, @{$polyline->lines} ], [
-        [ [0, 0], [10, 0] ],
-        [ [10, 0], [20, 0] ],
-    ], 'polyline_lines';
-}
 
 #==========================================================
 
diff --git a/tests/fff_print/CMakeLists.txt b/tests/fff_print/CMakeLists.txt
index 28f75bf2c..9e039c913 100644
--- a/tests/fff_print/CMakeLists.txt
+++ b/tests/fff_print/CMakeLists.txt
@@ -1,6 +1,8 @@
 get_filename_component(_TEST_NAME ${CMAKE_CURRENT_LIST_DIR} NAME)
 add_executable(${_TEST_NAME}_tests 
 	${_TEST_NAME}_tests.cpp
+	test_avoid_crossing_perimeters.cpp
+	test_bridges.cpp
 	test_data.cpp
 	test_data.hpp
 	test_extrusion_entity.cpp
diff --git a/tests/fff_print/test_avoid_crossing_perimeters.cpp b/tests/fff_print/test_avoid_crossing_perimeters.cpp
new file mode 100644
index 000000000..a76ac12b5
--- /dev/null
+++ b/tests/fff_print/test_avoid_crossing_perimeters.cpp
@@ -0,0 +1,16 @@
+#include <catch2/catch.hpp>
+
+#include "test_data.hpp"
+
+using namespace Slic3r;
+
+SCENARIO("Avoid crossing perimeters", "[AvoidCrossingPerimeters]") {
+	WHEN("Two 20mm cubes sliced") {
+        std::string gcode = Slic3r::Test::slice(
+    	    { Slic3r::Test::TestMesh::cube_20x20x20, Slic3r::Test::TestMesh::cube_20x20x20 },
+            { { "avoid_crossing_perimeters", true } });
+        THEN("gcode not empty") {
+            REQUIRE(! gcode.empty());
+        }
+    }
+}
diff --git a/tests/fff_print/test_bridges.cpp b/tests/fff_print/test_bridges.cpp
new file mode 100644
index 000000000..91ab9b6b2
--- /dev/null
+++ b/tests/fff_print/test_bridges.cpp
@@ -0,0 +1,133 @@
+#include <catch2/catch.hpp>
+
+#include <libslic3r/BridgeDetector.hpp>
+#include <libslic3r/Geometry.hpp>
+
+#include "test_data.hpp"
+
+using namespace Slic3r;
+
+SCENARIO("Bridge detector", "[Bridging]") 
+{
+    auto check_angle = [](const ExPolygons &lower, const ExPolygon &bridge, double expected, double tolerance = -1, double expected_coverage = -1) 
+    {
+        if (expected_coverage < 0)
+            expected_coverage = bridge.area();
+        
+        BridgeDetector bridge_detector(bridge, lower, scaled<coord_t>(0.5)); // extrusion width
+        if (tolerance < 0)
+            tolerance = Geometry::rad2deg(bridge_detector.resolution) + EPSILON;
+
+        bridge_detector.detect_angle();
+        double   result   = bridge_detector.angle;
+        Polygons coverage = bridge_detector.coverage();
+        THEN("correct coverage area") {
+            REQUIRE(is_approx(area(coverage), expected_coverage));
+        }
+        // our epsilon is equal to the steps used by the bridge detection algorithm
+        //##use XXX; YYY [ rad2deg($result), $expected ];
+        // returned value must be non-negative, check for that too
+        double delta = Geometry::rad2deg(result) - expected;
+        if (delta >= 180. - EPSILON)
+            delta -= 180;
+        return result >= 0. && std::abs(delta) < tolerance;
+    };
+    GIVEN("O-shaped overhang") {
+        auto test = [&check_angle](const Point &size, double rotate, double expected_angle, double tolerance = -1) {
+            ExPolygon lower{
+                Polygon::new_scale({ {-2,-2}, {size.x()+2,-2}, {size.x()+2,size.y()+2}, {-2,size.y()+2} }),
+                Polygon::new_scale({ {0,0}, {0,size.y()}, {size.x(),size.y()}, {size.x(),0} } )
+            };
+            lower.rotate(Geometry::deg2rad(rotate), size / 2);
+            ExPolygon bridge_expoly(lower.holes.front());
+            bridge_expoly.contour.reverse();
+            return check_angle({ lower }, bridge_expoly, expected_angle, tolerance);
+        };
+        WHEN("Bridge size 20x10") {
+            bool valid = test({20,10}, 0., 90.);
+            THEN("bridging angle is 90 degrees") {
+                REQUIRE(valid);
+            }
+        }
+        WHEN("Bridge size 10x20") {
+            bool valid = test({10,20}, 0., 0.);
+            THEN("bridging angle is 0 degrees") {
+                REQUIRE(valid);
+            }
+        }
+        WHEN("Bridge size 20x10, rotated by 45 degrees") {
+            bool valid = test({20,10}, 45., 135., 20.);
+            THEN("bridging angle is 135 degrees") {
+                REQUIRE(valid);
+            }
+        }
+        WHEN("Bridge size 20x10, rotated by 135 degrees") {
+            bool valid = test({20,10}, 135., 45., 20.);
+            THEN("bridging angle is 45 degrees") {
+                REQUIRE(valid);
+            }
+        }
+    }
+    GIVEN("two-sided bridge") {
+        ExPolygon bridge{ Polygon::new_scale({ {0,0}, {20,0}, {20,10}, {0,10} }) };
+        ExPolygons lower { ExPolygon{ Polygon::new_scale({ {-2,0}, {0,0}, {0,10}, {-2,10} }) } };
+        lower.emplace_back(lower.front());
+        lower.back().translate(Point::new_scale(22, 0));
+        THEN("Bridging angle 0 degrees") {
+            REQUIRE(check_angle(lower, bridge, 0));
+        }
+    }
+    GIVEN("for C-shaped overhang") {
+        ExPolygon bridge{ Polygon::new_scale({ {0,0}, {20,0}, {10,10}, {0,10} }) };
+        ExPolygon lower{ Polygon::new_scale({ {0,0}, {0,10}, {10,10}, {10,12}, {-2,12}, {-2,-2}, {22,-2}, {22,0} }) };
+        bool valid = check_angle({ lower }, bridge, 135);
+        THEN("Bridging angle is 135 degrees") {
+            REQUIRE(valid);
+        }
+    }
+    GIVEN("square overhang with L-shaped anchors") {
+        ExPolygon bridge{ Polygon::new_scale({ {10,10}, {20,10}, {20,20}, {10,20} }) };
+        ExPolygon lower{ Polygon::new_scale({ {10,10}, {10,20}, {20,20}, {30,30}, {0,30}, {0,0} }) };
+        bool valid = check_angle({ lower }, bridge, 45., -1., bridge.area() / 2.);
+        THEN("Bridging angle is 45 degrees") {
+            REQUIRE(valid);
+        }
+    }
+}
+
+SCENARIO("Bridging integration", "[Bridging]") {
+    DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config_with({
+        { "top_solid_layers",       0 },
+        // to prevent bridging on sparse infill
+        { "bridge_speed",           99 }
+    });
+
+    std::string gcode = Slic3r::Test::slice({ Slic3r::Test::TestMesh::bridge }, config);
+    
+    GCodeReader                 parser;
+    const double                bridge_speed = config.opt_float("bridge_speed") * 60.;
+    // angle => length
+    std::map<coord_t, double>   extrusions;
+    parser.parse_buffer(gcode, [&extrusions, bridge_speed](Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line)
+    {
+        // if the command is a T command, set the the current tool
+        if (line.cmd() == "G1" && is_approx<double>(bridge_speed, line.new_F(self))) {
+            // Accumulate lengths of bridging extrusions according to bridging angle.
+            Line l{ self.xy_scaled(), line.new_XY_scaled(self) };
+            size_t angle = scaled<coord_t>(l.direction());
+            auto it = extrusions.find(angle);
+            if (it == extrusions.end())
+                it = extrusions.insert(std::make_pair(angle, 0.)).first;
+            it->second += l.length();
+        }
+    });
+    THEN("bridge is generated") {
+        REQUIRE(! extrusions.empty());
+    }
+    THEN("bridge has the expected direction 0 degrees") {
+        // Bridging with the longest extrusion.
+        auto it_longest_extrusion = std::max_element(extrusions.begin(), extrusions.end(), 
+            [](const auto &e1, const auto &e2){ return e1.second < e2.second; });
+        REQUIRE(it_longest_extrusion->first == 0);
+    }
+}
diff --git a/tests/fff_print/test_fill.cpp b/tests/fff_print/test_fill.cpp
index 9f30bf8be..c3af6e8fc 100644
--- a/tests/fff_print/test_fill.cpp
+++ b/tests/fff_print/test_fill.cpp
@@ -197,8 +197,7 @@ TEST_CASE("Fill: Pattern Path Length", "[Fill]") {
 SCENARIO("Infill does not exceed perimeters", "[Fill]") 
 {
     auto test = [](const std::string_view pattern) {
-        DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
-        config.set_deserialize_strict({
+        DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config_with({
             { "nozzle_diameter",        "0.4, 0.4, 0.4, 0.4" },
             { "fill_pattern",           pattern },
             { "top_fill_pattern",       pattern },
diff --git a/tests/libslic3r/test_clipper_utils.cpp b/tests/libslic3r/test_clipper_utils.cpp
index 24d949be6..048ebba10 100644
--- a/tests/libslic3r/test_clipper_utils.cpp
+++ b/tests/libslic3r/test_clipper_utils.cpp
@@ -294,7 +294,7 @@ SCENARIO("Various Clipper operations - t/clipper.t", "[ClipperUtils]") {
     
         WHEN("clipping a line") {
             auto line = Polyline::new_scale({ { 152.742,288.086671142818 }, { 152.742,34.166466971035 } });    
-            Polylines intersection = intersection_pl({ line }, { circle_with_hole });
+            Polylines intersection = intersection_pl(line, Polygons{ circle_with_hole });
             THEN("clipped to two pieces") {
                 REQUIRE(intersection.front().length() == Approx((Vec2d(152742000, 215178843) - Vec2d(152742000, 288086661)).norm()));
                 REQUIRE(intersection[1].length() == Approx((Vec2d(152742000, 35166477) - Vec2d(152742000, 108087507)).norm()));
diff --git a/xs/CMakeLists.txt b/xs/CMakeLists.txt
index ab11e4989..8551aaff1 100644
--- a/xs/CMakeLists.txt
+++ b/xs/CMakeLists.txt
@@ -44,10 +44,8 @@ set(XSP_DIR ${CMAKE_CURRENT_SOURCE_DIR}/xsp)
 #FIXME list the dependecies explicitely, add dependency on the typemap.
 set(XS_XSP_FILES
     ${XSP_DIR}/BoundingBox.xsp
-    ${XSP_DIR}/BridgeDetector.xsp
     ${XSP_DIR}/Config.xsp
     ${XSP_DIR}/ExPolygon.xsp
-    ${XSP_DIR}/ExPolygonCollection.xsp
     ${XSP_DIR}/ExtrusionEntityCollection.xsp
     ${XSP_DIR}/ExtrusionLoop.xsp
     ${XSP_DIR}/ExtrusionMultiPath.xsp
diff --git a/xs/lib/Slic3r/XS.pm b/xs/lib/Slic3r/XS.pm
index 9a0181b65..5578a70ab 100644
--- a/xs/lib/Slic3r/XS.pm
+++ b/xs/lib/Slic3r/XS.pm
@@ -48,11 +48,6 @@ use overload
     '@{}' => sub { $_[0]->arrayref },
     'fallback' => 1;
 
-package Slic3r::ExPolygon::Collection;
-use overload
-    '@{}' => sub { $_[0]->arrayref },
-    'fallback' => 1;
-
 package Slic3r::ExtrusionPath::Collection;
 use overload
     '@{}' => sub { $_[0]->arrayref },
@@ -179,7 +174,6 @@ sub new {
 
 package main;
 for my $class (qw(
-        Slic3r::BridgeDetector
         Slic3r::Config
         Slic3r::Config::Full
         Slic3r::Config::GCode
@@ -188,7 +182,6 @@ for my $class (qw(
         Slic3r::Config::PrintRegion
         Slic3r::Config::Static
         Slic3r::ExPolygon
-        Slic3r::ExPolygon::Collection
         Slic3r::ExtrusionLoop
         Slic3r::ExtrusionMultiPath
         Slic3r::ExtrusionPath
diff --git a/xs/src/perlglue.cpp b/xs/src/perlglue.cpp
index e9f84b8c0..e513ec462 100644
--- a/xs/src/perlglue.cpp
+++ b/xs/src/perlglue.cpp
@@ -4,7 +4,6 @@
 namespace Slic3r {
 
 REGISTER_CLASS(ExPolygon, "ExPolygon");
-REGISTER_CLASS(ExPolygonCollection, "ExPolygon::Collection");
 REGISTER_CLASS(ExtrusionMultiPath, "ExtrusionMultiPath");
 REGISTER_CLASS(ExtrusionPath, "ExtrusionPath");
 REGISTER_CLASS(ExtrusionLoop, "ExtrusionLoop");
@@ -29,7 +28,6 @@ REGISTER_CLASS(ModelInstance, "Model::Instance");
 REGISTER_CLASS(BoundingBox, "Geometry::BoundingBox");
 REGISTER_CLASS(BoundingBoxf, "Geometry::BoundingBoxf");
 REGISTER_CLASS(BoundingBoxf3, "Geometry::BoundingBoxf3");
-REGISTER_CLASS(BridgeDetector, "BridgeDetector");
 REGISTER_CLASS(Point, "Point");
 __REGISTER_CLASS(Vec2d, "Pointf");
 __REGISTER_CLASS(Vec3d, "Pointf3");
diff --git a/xs/t/04_expolygon.t b/xs/t/04_expolygon.t
index 65e274ab9..48eaed551 100644
--- a/xs/t/04_expolygon.t
+++ b/xs/t/04_expolygon.t
@@ -5,7 +5,7 @@ use warnings;
 
 use List::Util qw(first sum);
 use Slic3r::XS;
-use Test::More tests => 21;
+use Test::More tests => 15;
 
 use constant PI => 4 * atan2(1, 1);
 
@@ -81,28 +81,4 @@ is $expolygon->area, 100*100-20*20, 'area';
         ], 'rotate around pure-Perl Point';
 }
 
-{
-    my $expolygon2 = $expolygon->clone;
-    $expolygon2->scale(2);
-    my $collection = Slic3r::ExPolygon::Collection->new($expolygon->pp, $expolygon2->pp);
-    is_deeply $collection->pp, [ $expolygon->pp, $expolygon2->pp ],
-        'expolygon collection (pure Perl) roundtrip';
-    
-    my $collection2 = Slic3r::ExPolygon::Collection->new($expolygon, $expolygon2);
-    is_deeply $collection->pp, $collection2->pp,
-        'expolygon collection (XS) roundtrip';
-    
-    $collection->clear;
-    is scalar(@$collection), 0, 'clear collection';
-    
-    $collection->append($expolygon);
-    is scalar(@$collection), 1, 'append to collection';
-    
-    my $exp = $collection->[0];
-    $exp->scale(3);
-    is $collection->[0][0][0][0], $exp->[0][0][0], 'collection items are returned by reference';
-    
-    is_deeply $collection->[0]->clone->pp, $collection->[0]->pp, 'clone collection item';
-}
-
 __END__
diff --git a/xs/xsp/BridgeDetector.xsp b/xs/xsp/BridgeDetector.xsp
deleted file mode 100644
index eb3793cf7..000000000
--- a/xs/xsp/BridgeDetector.xsp
+++ /dev/null
@@ -1,43 +0,0 @@
-%module{Slic3r::XS};
-
-%{
-#include <xsinit.h>
-#include "libslic3r/BridgeDetector.hpp"
-%}
-
-%name{Slic3r::BridgeDetector} class BridgeDetector {
-    ~BridgeDetector();
-    
-    bool detect_angle();
-    Polygons coverage();
-    %name{coverage_by_angle} Polygons coverage(double angle);
-    Polylines unsupported_edges();
-    %name{unsupported_edges_by_angle} Polylines unsupported_edges(double angle);
-    double angle()
-        %code{% RETVAL = THIS->angle; %};
-    double resolution()
-        %code{% RETVAL = THIS->resolution; %};
-%{
-
-BridgeDetector*
-BridgeDetector::new(expolygon, lower_slices, extrusion_width)
-    ExPolygon*              expolygon;
-    ExPolygonCollection*    lower_slices;
-    int                     extrusion_width;
-    CODE:
-        RETVAL = new BridgeDetector(*expolygon, lower_slices->expolygons, extrusion_width);
-    OUTPUT:
-        RETVAL
-
-BridgeDetector*
-BridgeDetector::new_expolygons(expolygons, lower_slices, extrusion_width)
-    ExPolygonCollection*    expolygons;
-    ExPolygonCollection*    lower_slices;
-    int                     extrusion_width;
-    CODE:
-        RETVAL = new BridgeDetector(expolygons->expolygons, lower_slices->expolygons, extrusion_width);
-    OUTPUT:
-        RETVAL
-
-%}
-};
diff --git a/xs/xsp/ExPolygonCollection.xsp b/xs/xsp/ExPolygonCollection.xsp
deleted file mode 100644
index 5321afb15..000000000
--- a/xs/xsp/ExPolygonCollection.xsp
+++ /dev/null
@@ -1,81 +0,0 @@
-%module{Slic3r::XS};
-
-%{
-#include <xsinit.h>
-#include "libslic3r/ExPolygonCollection.hpp"
-%}
-
-%name{Slic3r::ExPolygon::Collection} class ExPolygonCollection {
-    ~ExPolygonCollection();
-    Clone<ExPolygonCollection> clone() 
-        %code{% RETVAL = THIS; %};
-    void clear()
-        %code{% THIS->expolygons.clear(); %};
-    void scale(double factor);
-    void translate(double x, double y);
-    void rotate(double angle, Point* center)
-        %code{% THIS->rotate(angle, *center); %};
-    int count()
-        %code{% RETVAL = THIS->expolygons.size(); %};
-    bool contains_point(Point* point)
-        %code{% RETVAL = THIS->contains(*point); %};
-    bool contains_line(Line* line)
-        %code{% RETVAL = THIS->contains(*line); %};
-    bool contains_polyline(Polyline* polyline)
-        %code{% RETVAL = THIS->contains(*polyline); %};
-    void simplify(double tolerance);
-    Polygons polygons()
-        %code{% RETVAL = (Polygons)*THIS; %};
-    Clone<Polygon> convex_hull();
-%{
-
-ExPolygonCollection*
-ExPolygonCollection::new(...)
-    CODE:
-        RETVAL = new ExPolygonCollection ();
-        // ST(0) is class name, others are expolygons
-        RETVAL->expolygons.resize(items-1);
-        for (unsigned int i = 1; i < items; i++) {
-            // Note: a COPY of the input is stored
-            from_SV_check(ST(i), &RETVAL->expolygons[i-1]);
-        }
-    OUTPUT:
-        RETVAL
-
-SV*
-ExPolygonCollection::arrayref()
-    CODE:
-        AV* av = newAV();
-        av_fill(av, THIS->expolygons.size()-1);
-        int i = 0;
-        for (ExPolygons::iterator it = THIS->expolygons.begin(); it != THIS->expolygons.end(); ++it) {
-            av_store(av, i++, perl_to_SV_ref(*it));
-        }
-        RETVAL = newRV_noinc((SV*)av);
-    OUTPUT:
-        RETVAL
-
-SV*
-ExPolygonCollection::pp()
-    CODE:
-        AV* av = newAV();
-        av_fill(av, THIS->expolygons.size()-1);
-        int i = 0;
-        for (ExPolygons::iterator it = THIS->expolygons.begin(); it != THIS->expolygons.end(); ++it) {
-            av_store(av, i++, to_SV_pureperl(&*it));
-        }
-        RETVAL = newRV_noinc((SV*)av);
-    OUTPUT:
-        RETVAL
-
-void
-ExPolygonCollection::append(...)
-    CODE:
-        for (unsigned int i = 1; i < items; i++) {
-            ExPolygon expolygon;
-            from_SV_check(ST(i), &expolygon);
-            THIS->expolygons.push_back(expolygon);
-        }
-
-%}
-};
diff --git a/xs/xsp/ExtrusionPath.xsp b/xs/xsp/ExtrusionPath.xsp
index 078e6fe72..1dbc00917 100644
--- a/xs/xsp/ExtrusionPath.xsp
+++ b/xs/xsp/ExtrusionPath.xsp
@@ -95,22 +95,6 @@ ExtrusionPath::append(...)
             THIS->polyline.points.push_back(p);
         }
 
-ExtrusionEntityCollection*
-ExtrusionPath::intersect_expolygons(ExPolygonCollection* collection)
-    CODE:
-        RETVAL = new ExtrusionEntityCollection ();
-        THIS->intersect_expolygons(*collection, RETVAL);
-    OUTPUT:
-        RETVAL
-
-ExtrusionEntityCollection*
-ExtrusionPath::subtract_expolygons(ExPolygonCollection* collection)
-    CODE:
-        RETVAL = new ExtrusionEntityCollection ();
-        THIS->subtract_expolygons(*collection, RETVAL);
-    OUTPUT:
-        RETVAL
-
 %}
 };
 
diff --git a/xs/xsp/Geometry.xsp b/xs/xsp/Geometry.xsp
index 6e808fd8c..23daa4510 100644
--- a/xs/xsp/Geometry.xsp
+++ b/xs/xsp/Geometry.xsp
@@ -64,14 +64,6 @@ rad2deg(angle)
     OUTPUT:
         RETVAL
 
-double
-rad2deg_dir(angle)
-    double      angle
-    CODE:
-        RETVAL = Slic3r::Geometry::rad2deg_dir(angle);
-    OUTPUT:
-        RETVAL
-
 double
 deg2rad(angle)
     double      angle
diff --git a/xs/xsp/Layer.xsp b/xs/xsp/Layer.xsp
index 4ab7fe188..da35734ff 100644
--- a/xs/xsp/Layer.xsp
+++ b/xs/xsp/Layer.xsp
@@ -3,7 +3,6 @@
 %{
 #include <xsinit.h>
 #include "libslic3r/Layer.hpp"
-#include "libslic3r/ExPolygonCollection.hpp"
 %}
 
 %name{Slic3r::Layer::Region} class LayerRegion {
@@ -59,9 +58,6 @@
     Ref<LayerRegion> get_region(int idx);
     Ref<LayerRegion> add_region(PrintRegion* print_region);
 
-    ExPolygonCollection* slices()
-        %code%{ RETVAL = new ExPolygonCollection(THIS->lslices); %};
-
     int ptr()
         %code%{ RETVAL = (int)(intptr_t)THIS; %};
     
diff --git a/xs/xsp/my.map b/xs/xsp/my.map
index 131f82e79..fce0c47ad 100644
--- a/xs/xsp/my.map
+++ b/xs/xsp/my.map
@@ -88,10 +88,6 @@ ExPolygon*                 O_OBJECT_SLIC3R
 Ref<ExPolygon>             O_OBJECT_SLIC3R_T
 Clone<ExPolygon>           O_OBJECT_SLIC3R_T
 
-ExPolygonCollection*       O_OBJECT_SLIC3R
-Ref<ExPolygonCollection>   O_OBJECT_SLIC3R_T
-Clone<ExPolygonCollection> O_OBJECT_SLIC3R_T
-
 ExtrusionEntityCollection*              O_OBJECT_SLIC3R
 Ref<ExtrusionEntityCollection>          O_OBJECT_SLIC3R_T
 Clone<ExtrusionEntityCollection>        O_OBJECT_SLIC3R_T
@@ -163,10 +159,6 @@ GCode*                      O_OBJECT_SLIC3R
 Ref<GCode>                  O_OBJECT_SLIC3R_T
 Clone<GCode>                O_OBJECT_SLIC3R_T
 
-BridgeDetector*            O_OBJECT_SLIC3R
-Ref<BridgeDetector>        O_OBJECT_SLIC3R_T
-Clone<BridgeDetector>      O_OBJECT_SLIC3R_T
-
 Axis                  T_UV
 ExtrusionLoopRole     T_UV
 ExtrusionRole     T_UV
diff --git a/xs/xsp/typemap.xspt b/xs/xsp/typemap.xspt
index b59f4b74c..d4801fc39 100644
--- a/xs/xsp/typemap.xspt
+++ b/xs/xsp/typemap.xspt
@@ -54,9 +54,6 @@
 %typemap{ExPolygon*};
 %typemap{Ref<ExPolygon>}{simple};
 %typemap{Clone<ExPolygon>}{simple};
-%typemap{ExPolygonCollection*};
-%typemap{Ref<ExPolygonCollection>}{simple};
-%typemap{Clone<ExPolygonCollection>}{simple};
 %typemap{Flow*};
 %typemap{Ref<Flow>}{simple};
 %typemap{Clone<Flow>}{simple};
@@ -87,9 +84,6 @@
 %typemap{TriangleMesh*};
 %typemap{Ref<TriangleMesh>}{simple};
 %typemap{Clone<TriangleMesh>}{simple};
-%typemap{BridgeDetector*};
-%typemap{Ref<BridgeDetector>}{simple};
-%typemap{Clone<BridgeDetector>}{simple};
 %typemap{SurfaceCollection*};
 %typemap{Ref<SurfaceCollection>}{simple};
 %typemap{Clone<SurfaceCollection>}{simple};

From aa3231e2c5ed8a5d8c2125161a2f5f298964dbee Mon Sep 17 00:00:00 2001
From: Vojtech Bubnik <bubnikv@gmail.com>
Date: Wed, 4 May 2022 19:10:34 +0200
Subject: [PATCH 13/23] Further slimming of Perl bindings.

---
 lib/Slic3r.pm                        |  1 -
 lib/Slic3r/Config.pm                 |  3 --
 lib/Slic3r/Flow.pm                   | 13 ------
 lib/Slic3r/Geometry.pm               | 10 -----
 lib/Slic3r/Print/Object.pm           |  1 -
 src/libslic3r/PrintConfig.hpp        |  6 ---
 t/gaps.t                             |  1 -
 xs/CMakeLists.txt                    |  2 -
 xs/lib/Slic3r/XS.pm                  | 32 --------------
 xs/src/perlglue.cpp                  |  9 +---
 xs/xsp/BoundingBox.xsp               | 66 ----------------------------
 xs/xsp/Config.xsp                    |  4 --
 xs/xsp/ExtrusionEntityCollection.xsp |  4 --
 xs/xsp/ExtrusionMultiPath.xsp        | 38 ----------------
 xs/xsp/Flow.xsp                      | 60 -------------------------
 xs/xsp/Geometry.xsp                  | 37 ----------------
 xs/xsp/Layer.xsp                     |  2 -
 xs/xsp/Line.xsp                      | 14 ------
 xs/xsp/Model.xsp                     | 14 +-----
 xs/xsp/Polygon.xsp                   |  4 --
 xs/xsp/SurfaceCollection.xsp         | 14 ------
 xs/xsp/TriangleMesh.xsp              |  1 -
 xs/xsp/my.map                        | 28 ------------
 xs/xsp/typemap.xspt                  | 19 --------
 24 files changed, 3 insertions(+), 380 deletions(-)
 delete mode 100644 lib/Slic3r/Flow.pm
 delete mode 100644 xs/xsp/ExtrusionMultiPath.xsp
 delete mode 100644 xs/xsp/Flow.xsp

diff --git a/lib/Slic3r.pm b/lib/Slic3r.pm
index cd8f151f8..1e94e0f48 100644
--- a/lib/Slic3r.pm
+++ b/lib/Slic3r.pm
@@ -35,7 +35,6 @@ use Slic3r::Config;
 use Slic3r::ExPolygon;
 use Slic3r::ExtrusionLoop;
 use Slic3r::ExtrusionPath;
-use Slic3r::Flow;
 use Slic3r::GCode::Reader;
 use Slic3r::Layer;
 use Slic3r::Line;
diff --git a/lib/Slic3r/Config.pm b/lib/Slic3r/Config.pm
index aeaca998f..dadf76a0a 100644
--- a/lib/Slic3r/Config.pm
+++ b/lib/Slic3r/Config.pm
@@ -28,8 +28,5 @@ use parent 'Slic3r::Config';
 
 sub Slic3r::Config::GCode::new { Slic3r::Config::Static::new_GCodeConfig }
 sub Slic3r::Config::Print::new { Slic3r::Config::Static::new_PrintConfig }
-sub Slic3r::Config::PrintObject::new { Slic3r::Config::Static::new_PrintObjectConfig }
-sub Slic3r::Config::PrintRegion::new { Slic3r::Config::Static::new_PrintRegionConfig }
-sub Slic3r::Config::Full::new { Slic3r::Config::Static::new_FullPrintConfig }
 
 1;
diff --git a/lib/Slic3r/Flow.pm b/lib/Slic3r/Flow.pm
deleted file mode 100644
index fed894e97..000000000
--- a/lib/Slic3r/Flow.pm
+++ /dev/null
@@ -1,13 +0,0 @@
-package Slic3r::Flow;
-use strict;
-use warnings;
-
-use parent qw(Exporter);
-
-our @EXPORT_OK = qw(FLOW_ROLE_EXTERNAL_PERIMETER FLOW_ROLE_PERIMETER FLOW_ROLE_INFILL
-    FLOW_ROLE_SOLID_INFILL
-    FLOW_ROLE_TOP_SOLID_INFILL FLOW_ROLE_SUPPORT_MATERIAL 
-    FLOW_ROLE_SUPPORT_MATERIAL_INTERFACE);
-our %EXPORT_TAGS = (roles => \@EXPORT_OK);
-
-1;
diff --git a/lib/Slic3r/Geometry.pm b/lib/Slic3r/Geometry.pm
index b3c57fcd0..2e4f5a097 100644
--- a/lib/Slic3r/Geometry.pm
+++ b/lib/Slic3r/Geometry.pm
@@ -12,7 +12,6 @@ our @EXPORT_OK = qw(
     scale
     unscale
     scaled_epsilon
-    size_2D
 
     X Y Z
     convex_hull
@@ -52,13 +51,4 @@ sub bounding_box {
     return @bb[X1,Y1,X2,Y2];
 }
 
-# used by ExPolygon::size
-sub size_2D {
-    my @bounding_box = bounding_box(@_);
-    return (
-        ($bounding_box[X2] - $bounding_box[X1]),
-        ($bounding_box[Y2] - $bounding_box[Y1]),
-    );
-}
-
 1;
diff --git a/lib/Slic3r/Print/Object.pm b/lib/Slic3r/Print/Object.pm
index 35da7afe7..b4e53245a 100644
--- a/lib/Slic3r/Print/Object.pm
+++ b/lib/Slic3r/Print/Object.pm
@@ -4,7 +4,6 @@ use strict;
 use warnings;
 
 use List::Util qw(min max sum first);
-use Slic3r::Flow ':roles';
 use Slic3r::Surface ':types';
 
 sub layers {
diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp
index be2e2dbef..ab12bc8ce 100644
--- a/src/libslic3r/PrintConfig.hpp
+++ b/src/libslic3r/PrintConfig.hpp
@@ -197,7 +197,6 @@ double min_object_distance(const ConfigBase &cfg);
 // The dynamic configuration is also used to store user modifications of the print global parameters,
 // so the modified configuration values may be diffed against the active configuration
 // to invalidate the proper slicing resp. g-code generation processing steps.
-// This object is mapped to Perl as Slic3r::Config.
 class DynamicPrintConfig : public DynamicConfig
 {
 public:
@@ -464,7 +463,6 @@ protected: \
         BOOST_PP_SEQ_FOR_EACH(PRINT_CONFIG_CLASS_ELEMENT_HASH, _, PARAMETER_DEFINITION_SEQ), \
         BOOST_PP_SEQ_FOR_EACH(PRINT_CONFIG_CLASS_ELEMENT_EQUAL, _, PARAMETER_DEFINITION_SEQ))
 
-// This object is mapped to Perl as Slic3r::Config::PrintObject.
 PRINT_CONFIG_CLASS_DEFINE(
     PrintObjectConfig,
 
@@ -528,7 +526,6 @@ PRINT_CONFIG_CLASS_DEFINE(
     ((ConfigOptionBool,                wipe_into_objects))
 )
 
-// This object is mapped to Perl as Slic3r::Config::PrintRegion.
 PRINT_CONFIG_CLASS_DEFINE(
     PrintRegionConfig,
 
@@ -619,7 +616,6 @@ PRINT_CONFIG_CLASS_DEFINE(
     ((ConfigOptionFloats,               machine_min_extruding_rate))
 )
 
-// This object is mapped to Perl as Slic3r::Config::GCode.
 PRINT_CONFIG_CLASS_DEFINE(
     GCodeConfig,
 
@@ -705,7 +701,6 @@ static inline std::string get_extrusion_axis(const GCodeConfig &cfg)
         (cfg.gcode_flavor.value == gcfNoExtrusion) ? "" : cfg.extrusion_axis.value;
 }
 
-// This object is mapped to Perl as Slic3r::Config::Print.
 PRINT_CONFIG_CLASS_DERIVED_DEFINE(
     PrintConfig, 
     (MachineEnvelopeConfig, GCodeConfig),
@@ -784,7 +779,6 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE(
     ((ConfigOptionFloat,              z_offset))
 )
 
-// This object is mapped to Perl as Slic3r::Config::Full.
 PRINT_CONFIG_CLASS_DERIVED_DEFINE0(
     FullPrintConfig,
     (PrintObjectConfig, PrintRegionConfig, PrintConfig)
diff --git a/t/gaps.t b/t/gaps.t
index f0c75c353..2594ac087 100644
--- a/t/gaps.t
+++ b/t/gaps.t
@@ -10,7 +10,6 @@ BEGIN {
 
 use List::Util qw(first);
 use Slic3r;
-use Slic3r::Flow ':roles';
 use Slic3r::Geometry qw(PI scale unscale convex_hull);
 use Slic3r::Surface ':types';
 use Slic3r::Test;
diff --git a/xs/CMakeLists.txt b/xs/CMakeLists.txt
index 8551aaff1..1a58aab12 100644
--- a/xs/CMakeLists.txt
+++ b/xs/CMakeLists.txt
@@ -48,9 +48,7 @@ set(XS_XSP_FILES
     ${XSP_DIR}/ExPolygon.xsp
     ${XSP_DIR}/ExtrusionEntityCollection.xsp
     ${XSP_DIR}/ExtrusionLoop.xsp
-    ${XSP_DIR}/ExtrusionMultiPath.xsp
     ${XSP_DIR}/ExtrusionPath.xsp
-    ${XSP_DIR}/Flow.xsp
     ${XSP_DIR}/GCode.xsp
     ${XSP_DIR}/Geometry.xsp
     ${XSP_DIR}/Layer.xsp
diff --git a/xs/lib/Slic3r/XS.pm b/xs/lib/Slic3r/XS.pm
index 5578a70ab..1675ac193 100644
--- a/xs/lib/Slic3r/XS.pm
+++ b/xs/lib/Slic3r/XS.pm
@@ -74,11 +74,6 @@ sub new_from_paths {
     return $loop;
 }
 
-package Slic3r::ExtrusionMultiPath;
-use overload
-    '@{}' => sub { $_[0]->arrayref },
-    'fallback' => 1;
-
 package Slic3r::ExtrusionPath;
 use overload
     '@{}' => sub { $_[0]->arrayref },
@@ -108,25 +103,6 @@ sub clone {
     );
 }
 
-package Slic3r::Flow;
-
-sub new {
-    my ($class, %args) = @_;
-    
-    my $self = $class->_new(
-        @args{qw(width height nozzle_diameter)},
-    );
-    return $self;
-}
-
-sub new_from_width {
-    my ($class, %args) = @_;
-    
-    return $class->_new_from_width(
-        @args{qw(role width nozzle_diameter layer_height)},
-    );
-}
-
 package Slic3r::Surface;
 
 sub new {
@@ -175,26 +151,18 @@ sub new {
 package main;
 for my $class (qw(
         Slic3r::Config
-        Slic3r::Config::Full
         Slic3r::Config::GCode
         Slic3r::Config::Print
-        Slic3r::Config::PrintObject
-        Slic3r::Config::PrintRegion
         Slic3r::Config::Static
         Slic3r::ExPolygon
         Slic3r::ExtrusionLoop
-        Slic3r::ExtrusionMultiPath
         Slic3r::ExtrusionPath
         Slic3r::ExtrusionPath::Collection
-        Slic3r::Flow
         Slic3r::GCode
         Slic3r::Geometry::BoundingBox
-        Slic3r::Geometry::BoundingBoxf
-        Slic3r::Geometry::BoundingBoxf3
         Slic3r::Layer
         Slic3r::Layer::Region
         Slic3r::Line
-        Slic3r::Linef3
         Slic3r::Model
         Slic3r::Model::Instance
         Slic3r::Model::Material
diff --git a/xs/src/perlglue.cpp b/xs/src/perlglue.cpp
index e513ec462..4e293a2f9 100644
--- a/xs/src/perlglue.cpp
+++ b/xs/src/perlglue.cpp
@@ -4,17 +4,14 @@
 namespace Slic3r {
 
 REGISTER_CLASS(ExPolygon, "ExPolygon");
-REGISTER_CLASS(ExtrusionMultiPath, "ExtrusionMultiPath");
 REGISTER_CLASS(ExtrusionPath, "ExtrusionPath");
 REGISTER_CLASS(ExtrusionLoop, "ExtrusionLoop");
 REGISTER_CLASS(ExtrusionEntityCollection, "ExtrusionPath::Collection");
-REGISTER_CLASS(Flow, "Flow");
 REGISTER_CLASS(CoolingBuffer, "GCode::CoolingBuffer");
 REGISTER_CLASS(GCode, "GCode");
 REGISTER_CLASS(Layer, "Layer");
 REGISTER_CLASS(LayerRegion, "Layer::Region");
 REGISTER_CLASS(Line, "Line");
-REGISTER_CLASS(Linef3, "Linef3");
 REGISTER_CLASS(Polygon, "Polygon");
 REGISTER_CLASS(Polyline, "Polyline");
 REGISTER_CLASS(Print, "Print");
@@ -26,20 +23,16 @@ REGISTER_CLASS(ModelObject, "Model::Object");
 REGISTER_CLASS(ModelVolume, "Model::Volume");
 REGISTER_CLASS(ModelInstance, "Model::Instance");
 REGISTER_CLASS(BoundingBox, "Geometry::BoundingBox");
-REGISTER_CLASS(BoundingBoxf, "Geometry::BoundingBoxf");
-REGISTER_CLASS(BoundingBoxf3, "Geometry::BoundingBoxf3");
 REGISTER_CLASS(Point, "Point");
 __REGISTER_CLASS(Vec2d, "Pointf");
 __REGISTER_CLASS(Vec3d, "Pointf3");
 REGISTER_CLASS(DynamicPrintConfig, "Config");
 REGISTER_CLASS(StaticPrintConfig, "Config::Static");
-REGISTER_CLASS(PrintObjectConfig, "Config::PrintObject");
-REGISTER_CLASS(PrintRegionConfig, "Config::PrintRegion");
 REGISTER_CLASS(GCodeConfig, "Config::GCode");
 REGISTER_CLASS(PrintConfig, "Config::Print");
-REGISTER_CLASS(FullPrintConfig, "Config::Full");
 REGISTER_CLASS(Surface, "Surface");
 REGISTER_CLASS(SurfaceCollection, "Surface::Collection");
+REGISTER_CLASS(FullPrintConfig, "Config::Full");
 REGISTER_CLASS(TriangleMesh, "TriangleMesh");
 
 SV* ConfigBase__as_hash(ConfigBase* THIS)
diff --git a/xs/xsp/BoundingBox.xsp b/xs/xsp/BoundingBox.xsp
index a34cad0bc..75592e7c3 100644
--- a/xs/xsp/BoundingBox.xsp
+++ b/xs/xsp/BoundingBox.xsp
@@ -50,69 +50,3 @@ new_from_points(CLASS, points)
 %}
 };
 
-%name{Slic3r::Geometry::BoundingBoxf} class BoundingBoxf {
-    BoundingBoxf();
-    ~BoundingBoxf();
-    Clone<BoundingBoxf> clone()
-        %code{% RETVAL = THIS; %};
-    void merge(BoundingBoxf* bb) %code{% THIS->merge(*bb); %};
-    void merge_point(Vec2d* point) %code{% THIS->merge(*point); %};
-    void scale(double factor);
-    void translate(double x, double y);
-    Clone<Vec2d> size();
-    Clone<Vec2d> center();
-    double radius();
-    bool empty() %code{% RETVAL = empty(*THIS); %};
-    Clone<Vec2d> min_point() %code{% RETVAL = THIS->min; %};
-    Clone<Vec2d> max_point() %code{% RETVAL = THIS->max; %};
-    double x_min() %code{% RETVAL = THIS->min(0); %};
-    double x_max() %code{% RETVAL = THIS->max(0); %};
-    double y_min() %code{% RETVAL = THIS->min(1); %};
-    double y_max() %code{% RETVAL = THIS->max(1); %};
-    void set_x_min(double val) %code{% THIS->min(0) = val; %};
-    void set_x_max(double val) %code{% THIS->max(0) = val; %};
-    void set_y_min(double val) %code{% THIS->min(1) = val; %};
-    void set_y_max(double val) %code{% THIS->max(1) = val; %};
-    std::string serialize() %code{% char buf[2048]; sprintf(buf, "%lf,%lf;%lf,%lf", THIS->min(0), THIS->min(1), THIS->max(0), THIS->max(1)); RETVAL = buf; %};
-    bool defined() %code{% RETVAL = THIS->defined; %};
-    
-%{
-
-BoundingBoxf*
-new_from_points(CLASS, points)
-    char*   CLASS
-    Pointfs points
-    CODE:
-        RETVAL = new BoundingBoxf(points);
-    OUTPUT:
-        RETVAL
-
-%}
-};
-
-%name{Slic3r::Geometry::BoundingBoxf3} class BoundingBoxf3 {
-    BoundingBoxf3();
-    ~BoundingBoxf3();
-    Clone<BoundingBoxf3> clone()
-        %code{% RETVAL = THIS; %};
-    void merge(BoundingBoxf3* bb) %code{% THIS->merge(*bb); %};
-    void merge_point(Vec3d* point) %code{% THIS->merge(*point); %};
-    void scale(double factor);
-    void translate(double x, double y, double z);
-    void offset(double delta);
-    bool contains_point(Vec3d* point) %code{% RETVAL = THIS->contains(*point); %};
-    Clone<Vec3d> size();
-    Clone<Vec3d> center();
-    double radius();
-    bool empty() %code{% RETVAL = empty(*THIS); %};
-    Clone<Vec3d> min_point() %code{% RETVAL = THIS->min; %};
-    Clone<Vec3d> max_point() %code{% RETVAL = THIS->max; %};
-    double x_min() %code{% RETVAL = THIS->min(0); %};
-    double x_max() %code{% RETVAL = THIS->max(0); %};
-    double y_min() %code{% RETVAL = THIS->min(1); %};
-    double y_max() %code{% RETVAL = THIS->max(1); %};
-    double z_min() %code{% RETVAL = THIS->min(2); %};
-    double z_max() %code{% RETVAL = THIS->max(2); %};
-    std::string serialize() %code{% char buf[2048]; sprintf(buf, "%lf,%lf,%lf;%lf,%lf,%lf", THIS->min(0), THIS->min(1), THIS->min(2), THIS->max(0), THIS->max(1), THIS->max(2)); RETVAL = buf; %};
-    bool defined() %code{% RETVAL = THIS->defined; %};
-};
diff --git a/xs/xsp/Config.xsp b/xs/xsp/Config.xsp
index 703427035..14f235d93 100644
--- a/xs/xsp/Config.xsp
+++ b/xs/xsp/Config.xsp
@@ -76,10 +76,6 @@
         %code{% RETVAL = new GCodeConfig(); %};
     static StaticPrintConfig* new_PrintConfig()
         %code{% RETVAL = static_cast<GCodeConfig*>(new PrintConfig()); %};
-    static StaticPrintConfig* new_PrintObjectConfig()
-        %code{% RETVAL = new PrintObjectConfig(); %};
-    static StaticPrintConfig* new_PrintRegionConfig()
-        %code{% RETVAL = new PrintRegionConfig(); %};
     static StaticPrintConfig* new_FullPrintConfig()
         %code{% RETVAL = static_cast<GCodeConfig*>(new FullPrintConfig()); %};
     ~StaticPrintConfig();
diff --git a/xs/xsp/ExtrusionEntityCollection.xsp b/xs/xsp/ExtrusionEntityCollection.xsp
index a01788c56..1c3337303 100644
--- a/xs/xsp/ExtrusionEntityCollection.xsp
+++ b/xs/xsp/ExtrusionEntityCollection.xsp
@@ -55,8 +55,6 @@ ExtrusionEntityCollection::arrayref()
             // return our item by reference
             if (ExtrusionPath* path = dynamic_cast<ExtrusionPath*>(*it)) {
                 sv_setref_pv( sv, perl_class_name_ref(path), path );
-            } else if (ExtrusionMultiPath* multipath = dynamic_cast<ExtrusionMultiPath*>(*it)) {
-                sv_setref_pv( sv, perl_class_name_ref(multipath), multipath );
             } else if (ExtrusionLoop* loop = dynamic_cast<ExtrusionLoop*>(*it)) {
                 sv_setref_pv( sv, perl_class_name_ref(loop), loop );
             } else if (ExtrusionEntityCollection* collection = dynamic_cast<ExtrusionEntityCollection*>(*it)) {
@@ -81,8 +79,6 @@ ExtrusionEntityCollection::append(...)
             // append COPIES
             if (ExtrusionPath* path = dynamic_cast<ExtrusionPath*>(entity)) {
                 THIS->entities.push_back( new ExtrusionPath(*path) );
-            } else if (ExtrusionMultiPath* multipath = dynamic_cast<ExtrusionMultiPath*>(entity)) {
-                THIS->entities.push_back( new ExtrusionMultiPath(*multipath) );
             } else if (ExtrusionLoop* loop = dynamic_cast<ExtrusionLoop*>(entity)) {
                 THIS->entities.push_back( new ExtrusionLoop(*loop) );
             } else if(ExtrusionEntityCollection* collection = dynamic_cast<ExtrusionEntityCollection*>(entity)) {
diff --git a/xs/xsp/ExtrusionMultiPath.xsp b/xs/xsp/ExtrusionMultiPath.xsp
deleted file mode 100644
index 5dd938245..000000000
--- a/xs/xsp/ExtrusionMultiPath.xsp
+++ /dev/null
@@ -1,38 +0,0 @@
-%module{Slic3r::XS};
-
-%{
-#include <xsinit.h>
-#include "libslic3r/ExtrusionEntity.hpp"
-%}
-
-%name{Slic3r::ExtrusionMultiPath} class ExtrusionMultiPath {
-    ExtrusionMultiPath();
-    ~ExtrusionMultiPath();
-    Clone<ExtrusionMultiPath> clone()
-        %code{% RETVAL = THIS; %};
-    void reverse();
-    Clone<Point> first_point();
-    Clone<Point> last_point();
-    void append(ExtrusionPath* path)
-        %code{% THIS->paths.push_back(*path); %};
-    double length();
-    Polygons polygons_covered_by_width();
-    Polygons polygons_covered_by_spacing();
-    Clone<Polyline> polyline()
-        %code{% RETVAL = THIS->as_polyline(); %};
-%{
-
-SV*
-ExtrusionMultiPath::arrayref()
-    CODE:
-        AV* av = newAV();
-        av_fill(av, THIS->paths.size()-1);
-        for (ExtrusionPaths::iterator it = THIS->paths.begin(); it != THIS->paths.end(); ++it) {
-            av_store(av, it - THIS->paths.begin(), perl_to_SV_ref(*it));
-        }
-        RETVAL = newRV_noinc((SV*)av);
-    OUTPUT:
-        RETVAL
-
-%}
-};
diff --git a/xs/xsp/Flow.xsp b/xs/xsp/Flow.xsp
deleted file mode 100644
index 3056b4001..000000000
--- a/xs/xsp/Flow.xsp
+++ /dev/null
@@ -1,60 +0,0 @@
-%module{Slic3r::XS};
-
-%{
-#include <xsinit.h>
-#include "libslic3r/Flow.hpp"
-%}
-
-%name{Slic3r::Flow} class Flow {
-    ~Flow();
-    %name{_new} Flow(float width, float height, float nozzle_diameter);
-    Clone<Flow> clone()
-        %code{% RETVAL = THIS; %};
-    
-    float width();
-    float height();
-    float nozzle_diameter();
-    bool bridge();
-    float spacing();
-    int   scaled_width();
-    int   scaled_spacing();
-    double mm3_per_mm();
-%{
-
-Flow*
-_new_from_width(CLASS, role, width, nozzle_diameter, height)
-    char*           CLASS;
-    FlowRole        role;
-    std::string     width;
-    float           nozzle_diameter;
-    float           height;
-    CODE:
-        ConfigOptionFloatOrPercent optwidth;
-        optwidth.deserialize(width, ForwardCompatibilitySubstitutionRule::Disable);
-        RETVAL = new Flow(Flow::new_from_config_width(role, optwidth, nozzle_diameter, height));
-    OUTPUT:
-        RETVAL
-
-%}
-};
-
-%package{Slic3r::Flow};
-%{
-
-IV
-_constant()
-  ALIAS:
-    FLOW_ROLE_EXTERNAL_PERIMETER            = frExternalPerimeter
-    FLOW_ROLE_PERIMETER                     = frPerimeter
-    FLOW_ROLE_INFILL                        = frInfill
-    FLOW_ROLE_SOLID_INFILL                  = frSolidInfill
-    FLOW_ROLE_TOP_SOLID_INFILL              = frTopSolidInfill
-    FLOW_ROLE_SUPPORT_MATERIAL              = frSupportMaterial
-    FLOW_ROLE_SUPPORT_MATERIAL_INTERFACE    = frSupportMaterialInterface
-  PROTOTYPE:
-  CODE:
-    RETVAL = ix;
-  OUTPUT: RETVAL
-
-%}
-
diff --git a/xs/xsp/Geometry.xsp b/xs/xsp/Geometry.xsp
index 23daa4510..f6365a20a 100644
--- a/xs/xsp/Geometry.xsp
+++ b/xs/xsp/Geometry.xsp
@@ -10,35 +10,8 @@
 
 %package{Slic3r::Geometry};
 
-Pointfs arrange(size_t total_parts, Vec2d* part, coordf_t dist, BoundingBoxf* bb = NULL)
-    %code{% 
-        Pointfs points;
-        if (! Slic3r::Geometry::arrange(total_parts, *part, dist, bb, points))
-            CONFESS("%zu parts won't fit in your print area!\n", total_parts);
-        RETVAL = points;
-    %};
-
 %{
 
-bool
-directions_parallel(angle1, angle2)
-    double      angle1
-    double      angle2
-    CODE:
-        RETVAL = Slic3r::Geometry::directions_parallel(angle1, angle2);
-    OUTPUT:
-        RETVAL
-
-bool
-directions_parallel_within(angle1, angle2, max_diff)
-    double      angle1
-    double      angle2
-    double      max_diff
-    CODE:
-        RETVAL = Slic3r::Geometry::directions_parallel(angle1, angle2, max_diff);
-    OUTPUT:
-        RETVAL
-
 Clone<Polygon>
 convex_hull(points)
     Points      points
@@ -72,16 +45,6 @@ deg2rad(angle)
     OUTPUT:
         RETVAL
 
-Polygons
-simplify_polygons(polygons, tolerance)
-    Polygons    polygons
-    double      tolerance
-    CODE:
-        Slic3r::Geometry::simplify_polygons(polygons, tolerance, &RETVAL);
-    OUTPUT:
-        RETVAL
-
-
 IV
 _constant()
   ALIAS:
diff --git a/xs/xsp/Layer.xsp b/xs/xsp/Layer.xsp
index da35734ff..e42486985 100644
--- a/xs/xsp/Layer.xsp
+++ b/xs/xsp/Layer.xsp
@@ -23,8 +23,6 @@
     Ref<ExtrusionEntityCollection> fills()
         %code%{ RETVAL = &THIS->fills; %};
     
-    Clone<Flow> flow(FlowRole role)
-        %code%{ RETVAL = THIS->flow(role); %};
     void prepare_fill_surfaces();
     void make_perimeters(SurfaceCollection* slices, SurfaceCollection* fill_surfaces)
         %code%{ THIS->make_perimeters(*slices, fill_surfaces); %};
diff --git a/xs/xsp/Line.xsp b/xs/xsp/Line.xsp
index 36181c3ba..67308721a 100644
--- a/xs/xsp/Line.xsp
+++ b/xs/xsp/Line.xsp
@@ -76,17 +76,3 @@ Line::coincides_with(line_sv)
 %}
 };
 
-
-%name{Slic3r::Linef3} class Linef3 {
-    Linef3(Vec3d* a, Vec3d* b)
-        %code{% RETVAL = new Linef3(*a, *b); %};
-    ~Linef3();
-    Clone<Linef3> clone()
-        %code{% RETVAL = THIS; %};
-    Ref<Vec3d> a()
-        %code{% RETVAL = &THIS->a; %};
-    Ref<Vec3d> b()
-        %code{% RETVAL = &THIS->b; %};
-    Clone<Vec3d> intersect_plane(double z);
-    void scale(double factor);
-};
diff --git a/xs/xsp/Model.xsp b/xs/xsp/Model.xsp
index 34795681e..2194089a5 100644
--- a/xs/xsp/Model.xsp
+++ b/xs/xsp/Model.xsp
@@ -71,7 +71,6 @@
         %code%{ RETVAL = THIS->materials.size(); %};
 
     bool add_default_instances();
-    Clone<BoundingBoxf3> bounding_box();
     void center_instances_around_point(Vec2d* point)
         %code%{ THIS->center_instances_around_point(*point); %};
     void translate(double x, double y, double z);
@@ -80,11 +79,8 @@
     ModelObjectPtrs* objects()
         %code%{ RETVAL = &THIS->objects; %};
     
-    bool arrange_objects(double dist, BoundingBoxf* bb = NULL) %code%{ ArrangeParams ap{scaled(dist)}; if (bb) arrange_objects(*THIS, scaled(*bb), ap); else arrange_objects(*THIS, InfiniteBed{}, ap); %};
-    void duplicate(unsigned int copies_num, double dist, BoundingBoxf* bb = NULL) %code%{ ArrangeParams ap{scaled(dist)}; if (bb) duplicate(*THIS, copies_num, scaled(*bb), ap); else duplicate(*THIS, copies_num, InfiniteBed{}, ap); %};
-    void duplicate_objects(unsigned int copies_num, double dist, BoundingBoxf* bb = NULL) %code%{ ArrangeParams ap{scaled(dist)}; if (bb) duplicate_objects(*THIS, copies_num, scaled(*bb), ap); else duplicate_objects(*THIS, copies_num, InfiniteBed{}, ap); %};
-    void duplicate_objects_grid(unsigned int x, unsigned int y, double dist);
-
+    bool arrange_objects(double dist) %code%{ ArrangeParams ap{scaled(dist)}; arrange_objects(*THIS, InfiniteBed{}, ap); %};
+    void duplicate(unsigned int copies_num, double dist) %code%{ ArrangeParams ap{scaled(dist)};  duplicate(*THIS, copies_num, InfiniteBed{}, ap); %};
     bool looks_like_multipart_object() const;
     void convert_multipart_object(unsigned int max_extruders);
 
@@ -150,9 +146,6 @@ ModelMaterial::attributes()
     void invalidate_bounding_box();
     Clone<TriangleMesh> mesh();
     Clone<TriangleMesh> raw_mesh();
-    Clone<BoundingBoxf3> instance_bounding_box(int idx)
-        %code%{ RETVAL = THIS->instance_bounding_box(idx, true); %};
-    Clone<BoundingBoxf3> bounding_box();
 
     %name{_add_volume} Ref<ModelVolume> add_volume(TriangleMesh* mesh)
         %code%{ RETVAL = THIS->add_volume(*mesh); %};
@@ -290,7 +283,4 @@ ModelMaterial::attributes()
             THIS->set_offset(X, (*offset)(0));
             THIS->set_offset(Y, (*offset)(1));
         %};
-
-    void transform_mesh(TriangleMesh* mesh, bool dont_translate = false) const;
-    void transform_polygon(Polygon* polygon) const;
 };
diff --git a/xs/xsp/Polygon.xsp b/xs/xsp/Polygon.xsp
index bc4d412d9..873c4bcc9 100644
--- a/xs/xsp/Polygon.xsp
+++ b/xs/xsp/Polygon.xsp
@@ -34,12 +34,8 @@
     bool contains_point(Point* point)
         %code{% RETVAL = THIS->contains(*point); %};
     Polygons simplify(double tolerance);
-    Polygons triangulate_convex()
-        %code{% THIS->triangulate_convex(&RETVAL); %};
     Clone<Point> centroid();
     Clone<BoundingBox> bounding_box();
-    Clone<Point> point_projection(Point* point)
-        %code{% RETVAL = THIS->point_projection(*point); %};
     Clone<Point> first_intersection(Line* line)
         %code{%
             Point p;
diff --git a/xs/xsp/SurfaceCollection.xsp b/xs/xsp/SurfaceCollection.xsp
index 19cf3f828..0d31c5ae3 100644
--- a/xs/xsp/SurfaceCollection.xsp
+++ b/xs/xsp/SurfaceCollection.xsp
@@ -42,20 +42,6 @@ SurfaceCollection::filter_by_type(surface_type)
     OUTPUT:
         RETVAL
 
-void
-SurfaceCollection::replace(index, surface)
-    int         index
-    Surface*    surface
-    CODE:
-        THIS->surfaces[index] = *surface;
-
-void
-SurfaceCollection::set_surface_type(index, surface_type)
-    int             index
-    SurfaceType     surface_type;
-    CODE:
-        THIS->surfaces[index].surface_type = surface_type;
-
 SV*
 SurfaceCollection::group()
     CODE:
diff --git a/xs/xsp/TriangleMesh.xsp b/xs/xsp/TriangleMesh.xsp
index 91b3b40a1..5dc0df746 100644
--- a/xs/xsp/TriangleMesh.xsp
+++ b/xs/xsp/TriangleMesh.xsp
@@ -28,7 +28,6 @@
     void merge(TriangleMesh* mesh)
         %code{% THIS->merge(*mesh); %};
     Clone<Polygon> convex_hull();
-    Clone<BoundingBoxf3> bounding_box();
     Clone<Vec3d> center()
         %code{% RETVAL = THIS->bounding_box().center(); %};
     int facets_count();
diff --git a/xs/xsp/my.map b/xs/xsp/my.map
index fce0c47ad..5174bdfa9 100644
--- a/xs/xsp/my.map
+++ b/xs/xsp/my.map
@@ -18,14 +18,6 @@ BoundingBox*               O_OBJECT_SLIC3R
 Ref<BoundingBox>           O_OBJECT_SLIC3R_T
 Clone<BoundingBox>         O_OBJECT_SLIC3R_T
 
-BoundingBoxf*              O_OBJECT_SLIC3R
-Ref<BoundingBoxf>          O_OBJECT_SLIC3R_T
-Clone<BoundingBoxf>        O_OBJECT_SLIC3R_T
-
-BoundingBoxf3*             O_OBJECT_SLIC3R
-Ref<BoundingBoxf3>         O_OBJECT_SLIC3R_T
-Clone<BoundingBoxf3>       O_OBJECT_SLIC3R_T
-
 DynamicPrintConfig*        O_OBJECT_SLIC3R
 Ref<DynamicPrintConfig>    O_OBJECT_SLIC3R_T
 Clone<DynamicPrintConfig>  O_OBJECT_SLIC3R_T
@@ -33,12 +25,6 @@ Clone<DynamicPrintConfig>  O_OBJECT_SLIC3R_T
 StaticPrintConfig*         O_OBJECT_SLIC3R
 Ref<StaticPrintConfig>     O_OBJECT_SLIC3R_T
 
-PrintObjectConfig*         O_OBJECT_SLIC3R
-Ref<PrintObjectConfig>     O_OBJECT_SLIC3R_T
-
-PrintRegionConfig*         O_OBJECT_SLIC3R
-Ref<PrintRegionConfig>     O_OBJECT_SLIC3R_T
-
 GCodeConfig*               O_OBJECT_SLIC3R
 Ref<GCodeConfig>           O_OBJECT_SLIC3R_T
 
@@ -72,10 +58,6 @@ Line*                      O_OBJECT_SLIC3R
 Ref<Line>                  O_OBJECT_SLIC3R_T
 Clone<Line>                O_OBJECT_SLIC3R_T
 
-Linef3*                    O_OBJECT_SLIC3R
-Ref<Linef3>                O_OBJECT_SLIC3R_T
-Clone<Linef3>              O_OBJECT_SLIC3R_T
-
 Polyline*                  O_OBJECT_SLIC3R
 Ref<Polyline>              O_OBJECT_SLIC3R_T
 Clone<Polyline>            O_OBJECT_SLIC3R_T
@@ -92,10 +74,6 @@ ExtrusionEntityCollection*              O_OBJECT_SLIC3R
 Ref<ExtrusionEntityCollection>          O_OBJECT_SLIC3R_T
 Clone<ExtrusionEntityCollection>        O_OBJECT_SLIC3R_T
 
-ExtrusionMultiPath*        O_OBJECT_SLIC3R
-Ref<ExtrusionMultiPath>    O_OBJECT_SLIC3R_T
-Clone<ExtrusionMultiPath>  O_OBJECT_SLIC3R_T
-
 ExtrusionPath*             O_OBJECT_SLIC3R
 Ref<ExtrusionPath>         O_OBJECT_SLIC3R_T
 Clone<ExtrusionPath>       O_OBJECT_SLIC3R_T
@@ -104,10 +82,6 @@ ExtrusionLoop*             O_OBJECT_SLIC3R
 Ref<ExtrusionLoop>         O_OBJECT_SLIC3R_T
 Clone<ExtrusionLoop>       O_OBJECT_SLIC3R_T
 
-Flow*                      O_OBJECT_SLIC3R
-Ref<Flow>                  O_OBJECT_SLIC3R_T
-Clone<Flow>                O_OBJECT_SLIC3R_T
-
 Surface*                   O_OBJECT_SLIC3R
 Ref<Surface>               O_OBJECT_SLIC3R_T
 Clone<Surface>             O_OBJECT_SLIC3R_T
@@ -164,8 +138,6 @@ ExtrusionLoopRole     T_UV
 ExtrusionRole     T_UV
 FlowRole     T_UV
 SurfaceType     T_UV
-Slic3r::ClipperLib::JoinType		T_UV
-Slic3r::ClipperLib::PolyFillType		T_UV
 
 # we return these types whenever we want the items to be cloned
 Points          T_ARRAYREF
diff --git a/xs/xsp/typemap.xspt b/xs/xsp/typemap.xspt
index d4801fc39..243e80dbc 100644
--- a/xs/xsp/typemap.xspt
+++ b/xs/xsp/typemap.xspt
@@ -30,21 +30,11 @@
 %typemap{BoundingBox*};
 %typemap{Ref<BoundingBox>}{simple};
 %typemap{Clone<BoundingBox>}{simple};
-%typemap{BoundingBoxf*};
-%typemap{Ref<BoundingBoxf>}{simple};
-%typemap{Clone<BoundingBoxf>}{simple};
-%typemap{BoundingBoxf3*};
-%typemap{Ref<BoundingBoxf3>}{simple};
-%typemap{Clone<BoundingBoxf3>}{simple};
 %typemap{DynamicPrintConfig*};
 %typemap{Ref<DynamicPrintConfig>}{simple};
 %typemap{Clone<DynamicPrintConfig>}{simple};
 %typemap{StaticPrintConfig*};
 %typemap{Ref<StaticPrintConfig>}{simple};
-%typemap{PrintObjectConfig*};
-%typemap{Ref<PrintObjectConfig>}{simple};
-%typemap{PrintRegionConfig*};
-%typemap{Ref<PrintRegionConfig>}{simple};
 %typemap{GCodeConfig*};
 %typemap{Ref<GCodeConfig>}{simple};
 %typemap{PrintConfig*};
@@ -54,15 +44,9 @@
 %typemap{ExPolygon*};
 %typemap{Ref<ExPolygon>}{simple};
 %typemap{Clone<ExPolygon>}{simple};
-%typemap{Flow*};
-%typemap{Ref<Flow>}{simple};
-%typemap{Clone<Flow>}{simple};
 %typemap{Line*};
 %typemap{Ref<Line>}{simple};
 %typemap{Clone<Line>}{simple};
-%typemap{Linef3*};
-%typemap{Ref<Linef3>}{simple};
-%typemap{Clone<Linef3>}{simple};
 %typemap{Polyline*};
 %typemap{Ref<Polyline>}{simple};
 %typemap{Clone<Polyline>}{simple};
@@ -72,9 +56,6 @@
 %typemap{ExtrusionEntityCollection*};
 %typemap{Ref<ExtrusionEntityCollection>}{simple};
 %typemap{Clone<ExtrusionEntityCollection>}{simple};
-%typemap{ExtrusionMultiPath*};
-%typemap{Ref<ExtrusionMultiPath>}{simple};
-%typemap{Clone<ExtrusionMultiPath>}{simple};
 %typemap{ExtrusionPath*};
 %typemap{Ref<ExtrusionPath>}{simple};
 %typemap{Clone<ExtrusionPath>}{simple};

From d0b4a4a87da65ba079456863092a335f192bf6dd Mon Sep 17 00:00:00 2001
From: YuSanka <yusanka@gmail.com>
Date: Wed, 4 May 2022 17:28:57 +0200
Subject: [PATCH 14/23] Preferences Dialog: Revert values, when "Cancel" button
 is clicked ([SPE-1230|https://dev.prusa3d.com/browse/SPE-1230]) + Fixed bug:
 If change "dark mode" checkbox and "Settings layout mode", than dark mode
 wouldn't processed. + Code refactoring for create_settings_mode_widget()

---
 src/slic3r/GUI/Preferences.cpp | 243 ++++++++++++++++++++++-----------
 src/slic3r/GUI/Preferences.hpp |   9 ++
 2 files changed, 171 insertions(+), 81 deletions(-)

diff --git a/src/slic3r/GUI/Preferences.cpp b/src/slic3r/GUI/Preferences.cpp
index 75c5c116f..c92909a8d 100644
--- a/src/slic3r/GUI/Preferences.cpp
+++ b/src/slic3r/GUI/Preferences.cpp
@@ -173,6 +173,10 @@ void PreferencesDialog::build()
 	// Add "General" tab
 	m_optgroup_general = create_options_tab(L("General"), tabs);
 	m_optgroup_general->m_on_change = [this](t_config_option_key opt_key, boost::any value) {
+		if (auto it = m_values.find(opt_key); it != m_values.end()) {
+			m_values.erase(it); // we shouldn't change value, if some of those parameters were selected, and then deselected
+			return;
+		}
 		if (opt_key == "default_action_on_close_application" || opt_key == "default_action_on_select_preset" || opt_key == "default_action_on_new_project")
 			m_values[opt_key] = boost::any_cast<bool>(value) ? "none" : "discard";
 		else if (opt_key == "default_action_on_dirty_project")
@@ -335,6 +339,10 @@ void PreferencesDialog::build()
 	// Add "Camera" tab
 	m_optgroup_camera = create_options_tab(L("Camera"), tabs);
 	m_optgroup_camera->m_on_change = [this](t_config_option_key opt_key, boost::any value) {
+		if (auto it = m_values.find(opt_key);it != m_values.end()) {
+			m_values.erase(it); // we shouldn't change value, if some of those parameters were selected, and then deselected
+			return;
+		}
 		m_values[opt_key] = boost::any_cast<bool>(value) ? "1" : "0";
 	};
 
@@ -358,25 +366,38 @@ void PreferencesDialog::build()
 	// Add "GUI" tab
 	m_optgroup_gui = create_options_tab(L("GUI"), tabs);
 	m_optgroup_gui->m_on_change = [this](t_config_option_key opt_key, boost::any value) {
-        if (opt_key == "suppress_hyperlinks")
-            m_values[opt_key] = boost::any_cast<bool>(value) ? "1" : "";
-		else if (opt_key == "notify_release") {
+		if (opt_key == "notify_release") {
 			int val_int = boost::any_cast<int>(value);
 			for (const auto& item : s_keys_map_NotifyReleaseMode) {
 				if (item.second == val_int) {
 					m_values[opt_key] = item.first;
-					break;
+					return;
 				}
 			}
-		} else
-            m_values[opt_key] = boost::any_cast<bool>(value) ? "1" : "0";
-
+		}
 		if (opt_key == "use_custom_toolbar_size") {
 			m_icon_size_sizer->ShowItems(boost::any_cast<bool>(value));
-			m_optgroup_gui->parent()->Layout();
-			tabs->Layout();
-			this->layout();
+			refresh_og(m_optgroup_gui);
 		}
+		if (opt_key == "tabs_as_menu") {
+			bool disable_new_layout = boost::any_cast<bool>(value);
+			m_rb_new_settings_layout_mode->Show(!disable_new_layout);
+			if (disable_new_layout && m_rb_new_settings_layout_mode->GetValue()) {
+				m_rb_new_settings_layout_mode->SetValue(false);
+				m_rb_old_settings_layout_mode->SetValue(true);
+			}
+			refresh_og(m_optgroup_gui);
+		}
+
+		if (auto it = m_values.find(opt_key); it != m_values.end()) {
+			m_values.erase(it); // we shouldn't change value, if some of those parameters were selected, and then deselected
+			return;
+		}
+
+		if (opt_key == "suppress_hyperlinks")
+			m_values[opt_key] = boost::any_cast<bool>(value) ? "1" : "";
+		else
+			m_values[opt_key] = boost::any_cast<bool>(value) ? "1" : "0";
 	};
 
 	append_bool_option(m_optgroup_gui, "seq_top_layer_only",
@@ -464,6 +485,10 @@ void PreferencesDialog::build()
 		// Add "Render" tab
 		m_optgroup_render = create_options_tab(L("Render"), tabs);
 		m_optgroup_render->m_on_change = [this](t_config_option_key opt_key, boost::any value) {
+			if (auto it = m_values.find(opt_key); it != m_values.end()) {
+				m_values.erase(it); // we shouldn't change value, if some of those parameters were selected, and then deselected
+				return;
+			}
 			m_values[opt_key] = boost::any_cast<bool>(value) ? "1" : "0";
 		};
 
@@ -479,6 +504,10 @@ void PreferencesDialog::build()
 		// Add "Dark Mode" tab
 		m_optgroup_dark_mode = create_options_tab(_L("Dark mode (experimental)"), tabs);
 		m_optgroup_dark_mode->m_on_change = [this](t_config_option_key opt_key, boost::any value) {
+			if (auto it = m_values.find(opt_key); it != m_values.end()) {
+				m_values.erase(it); // we shouldn't change value, if some of those parameters were selected, and then deselected
+				return;
+			}
 			m_values[opt_key] = boost::any_cast<bool>(value) ? "1" : "0";
 		};
 
@@ -509,6 +538,7 @@ void PreferencesDialog::build()
 
 	auto buttons = CreateStdDialogButtonSizer(wxOK | wxCANCEL);
 	this->Bind(wxEVT_BUTTON, &PreferencesDialog::accept, this, wxID_OK);
+	this->Bind(wxEVT_BUTTON, &PreferencesDialog::revert, this, wxID_CANCEL);
 
 	for (int id : {wxID_OK, wxID_CANCEL})
 		wxGetApp().UpdateDarkUI(static_cast<wxButton*>(FindWindowById(id, this)));
@@ -592,19 +622,6 @@ void PreferencesDialog::accept(wxEvent&)
 	    }
 	}
 
-	for (const std::string& key : {	"default_action_on_close_application", 
-									"default_action_on_select_preset", 
-									"default_action_on_new_project" }) {
-	    auto it = m_values.find(key);
-		if (it != m_values.end() && it->second != "none" && app_config->get(key) != "none")
-			m_values.erase(it); // we shouldn't change value, if some of those parameters were selected, and then deselected
-	}
-	{
-	    auto it = m_values.find("default_action_on_dirty_project");
-		if (it != m_values.end() && !it->second.empty() && !app_config->get("default_action_on_dirty_project").empty())
-			m_values.erase(it); // we shouldn't change value, if this parameter was selected, and then deselected
-	}
-
 #if 0 //#ifdef _WIN32 // #ysDarkMSW - Allow it when we deside to support the sustem colors for application
 	if (m_values.find("always_dark_color_mode") != m_values.end())
 		wxGetApp().force_sys_colors_update();
@@ -629,11 +646,85 @@ void PreferencesDialog::accept(wxEvent&)
 		wxGetApp().force_menu_update();
 #endif //_MSW_DARK_MODE
 #endif // _WIN32
-	if (m_settings_layout_changed)
-		;// application will be recreated after Preference dialog will be destroyed
-	else
-	    // Nothify the UI to update itself from the ini file.
-        wxGetApp().update_ui_from_settings();
+	
+	wxGetApp().update_ui_from_settings();
+	m_values.clear();
+}
+
+void PreferencesDialog::revert(wxEvent&)
+{
+	auto app_config = get_app_config();
+
+	for (auto value : m_values) {
+		bool reverted = false;
+		const std::string& key = value.first;
+
+		if (key == "default_action_on_dirty_project") {
+			m_optgroup_general->set_value(key, app_config->get(key).empty());
+			continue;
+		}
+		if (key == "default_action_on_close_application" || key == "default_action_on_select_preset" || key == "default_action_on_new_project") {
+			m_optgroup_general->set_value(key, app_config->get(key) == "none");
+			continue;
+		}
+		if (key == "notify_release") {
+			m_optgroup_gui->set_value(key, s_keys_map_NotifyReleaseMode.at(app_config->get(key)));
+			continue;
+		}
+		if (key == "custom_toolbar_size") {
+			m_icon_size_slider->SetValue(atoi(app_config->get("custom_toolbar_size").c_str()));
+			continue;
+		}
+		if (key == "old_settings_layout_mode") {
+			m_rb_old_settings_layout_mode->SetValue(app_config->get(key) == "1");
+			continue;
+		}
+		if (key == "new_settings_layout_mode") {
+			m_rb_new_settings_layout_mode->SetValue(app_config->get(key) == "1");
+			continue;
+		}
+		if (key == "dlg_settings_layout_mode") {
+			m_rb_dlg_settings_layout_mode->SetValue(app_config->get(key) == "1");
+			continue;
+		}
+
+		for (auto opt_group : { m_optgroup_general, m_optgroup_camera, m_optgroup_gui
+#ifdef _WIN32
+			, m_optgroup_dark_mode
+#endif // _WIN32
+#if ENABLE_ENVIRONMENT_MAP
+			, m_optgroup_render
+#endif // ENABLE_ENVIRONMENT_MAP
+			}) {
+			if (reverted = opt_group->set_value(key, app_config->get(key) == "1"))
+				break;
+		}
+		if (!reverted)
+			int i=0;
+		if (key == "tabs_as_menu") {
+			m_rb_new_settings_layout_mode->Show(app_config->get(key) != "1");
+			refresh_og(m_optgroup_gui);
+			continue;
+		}
+
+		if (key == "use_custom_toolbar_size") {
+			m_icon_size_sizer->ShowItems(app_config->get(key) == "1");
+			refresh_og(m_optgroup_gui);
+		}
+	}
+
+	m_values.clear();
+
+	auto revert_colors = [](wxColourPickerCtrl* color_pckr, const wxColour& color) {
+		if (color_pckr->GetColour() != color) {
+			color_pckr->SetColour(color);
+			wxPostEvent(color_pckr, wxCommandEvent(wxEVT_COLOURPICKER_CHANGED));
+		}
+	};
+	revert_colors(m_sys_colour, wxGetApp().get_label_clr_sys());
+	revert_colors(m_mod_colour, wxGetApp().get_label_clr_modified());
+
+	EndModal(wxID_CANCEL);
 }
 
 void PreferencesDialog::msw_rescale()
@@ -669,6 +760,13 @@ void PreferencesDialog::layout()
     Refresh();
 }
 
+void PreferencesDialog::refresh_og(std::shared_ptr<ConfigOptionsGroup> og)
+{
+	og->parent()->Layout();
+	tabs->Layout();
+	this->layout();
+}
+
 void PreferencesDialog::create_icon_size_slider()
 {
     const auto app_config = get_app_config();
@@ -695,14 +793,14 @@ void PreferencesDialog::create_icon_size_slider()
     if (!isOSX)
         style |= wxSL_LABELS | wxSL_AUTOTICKS;
 
-    auto slider = new wxSlider(parent, wxID_ANY, def_val, 30, 100, 
+    m_icon_size_slider = new wxSlider(parent, wxID_ANY, def_val, 30, 100, 
                                wxDefaultPosition, wxDefaultSize, style);
 
-    slider->SetTickFreq(10);
-    slider->SetPageSize(10);
-    slider->SetToolTip(_L("Select toolbar icon size in respect to the default one."));
+    m_icon_size_slider->SetTickFreq(10);
+    m_icon_size_slider->SetPageSize(10);
+    m_icon_size_slider->SetToolTip(_L("Select toolbar icon size in respect to the default one."));
 
-    m_icon_size_sizer->Add(slider, 1, wxEXPAND);
+    m_icon_size_sizer->Add(m_icon_size_slider, 1, wxEXPAND);
 
     wxStaticText* val_label{ nullptr };
     if (isOSX) {
@@ -710,15 +808,15 @@ void PreferencesDialog::create_icon_size_slider()
         m_icon_size_sizer->Add(val_label, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, em);
     }
 
-    slider->Bind(wxEVT_SLIDER, ([this, slider, val_label](wxCommandEvent e) {
-        auto val = slider->GetValue();
+    m_icon_size_slider->Bind(wxEVT_SLIDER, ([this, val_label](wxCommandEvent e) {
+        auto val = m_icon_size_slider->GetValue();
         m_values["custom_toolbar_size"] = (boost::format("%d") % val).str();
 
         if (val_label)
             val_label->SetLabelText(wxString::Format("%d", val));
-    }), slider->GetId());
+    }), m_icon_size_slider->GetId());
 
-    for (wxWindow* win : std::vector<wxWindow*>{ slider, label, val_label }) {
+    for (wxWindow* win : std::vector<wxWindow*>{ m_icon_size_slider, label, val_label }) {
         if (!win) continue;         
         win->SetFont(wxGetApp().normal_font());
 
@@ -731,26 +829,6 @@ void PreferencesDialog::create_icon_size_slider()
 
 void PreferencesDialog::create_settings_mode_widget()
 {
-#ifdef _MSW_DARK_MODE
-	bool disable_new_layout = wxGetApp().tabs_as_menu();
-#endif
-	std::vector<wxString> choices = {  _L("Old regular layout with the tab bar"),
-                                       _L("New layout, access via settings button in the top menu"),
-                                       _L("Settings in non-modal window") };
-
-	auto app_config = get_app_config();
-    int selection = app_config->get("old_settings_layout_mode") == "1" ? 0 :
-                    app_config->get("new_settings_layout_mode") == "1" ? 1 :
-                    app_config->get("dlg_settings_layout_mode") == "1" ? 2 : 0;
-
-#ifdef _MSW_DARK_MODE
-	if (disable_new_layout) {
-		choices = { _L("Old regular layout with the tab bar"),
-					_L("Settings in non-modal window") };
-		selection = app_config->get("dlg_settings_layout_mode") == "1" ? 1 : 0;
-	}
-#endif
-
 	wxWindow* parent = m_optgroup_gui->parent();
 	wxGetApp().UpdateDarkUI(parent);
 
@@ -762,32 +840,35 @@ void PreferencesDialog::create_settings_mode_widget()
 
 	wxSizer* stb_sizer = new wxStaticBoxSizer(stb, wxVERTICAL);
 
-	int id = 0;
-	for (const wxString& label : choices) {
-		wxRadioButton* btn = new wxRadioButton(parent, wxID_ANY, label, wxDefaultPosition, wxDefaultSize, id==0 ? wxRB_GROUP : 0);
-		stb_sizer->Add(btn);
-		btn->SetValue(id == selection);
-
-        int dlg_id = 2;
-#ifdef _MSW_DARK_MODE
-		if (disable_new_layout)
-			dlg_id = 1;
-#endif
-
-        btn->Bind(wxEVT_RADIOBUTTON, [this, id, dlg_id
-#ifdef _MSW_DARK_MODE
-			, disable_new_layout
-#endif
-		](wxCommandEvent& ) {
-            m_values["old_settings_layout_mode"] = (id == 0) ? "1" : "0";
-#ifdef _MSW_DARK_MODE
-			if (!disable_new_layout)
-#endif
-            m_values["new_settings_layout_mode"] = (id == 1) ? "1" : "0";
-            m_values["dlg_settings_layout_mode"] = (id == dlg_id) ? "1" : "0";
+	auto app_config = get_app_config();
+	std::vector<wxString> choices = {	_L("Old regular layout with the tab bar"),
+										_L("New layout, access via settings button in the top menu"),
+										_L("Settings in non-modal window") };
+	int id = -1;
+	auto add_radio = [this, parent, stb_sizer, choices](wxRadioButton** rb, int id, bool select) {
+		*rb = new wxRadioButton(parent, wxID_ANY, choices[id], wxDefaultPosition, wxDefaultSize, id == 0 ? wxRB_GROUP : 0);
+		stb_sizer->Add(*rb);
+		(*rb)->SetValue(select);
+		(*rb)->Bind(wxEVT_RADIOBUTTON, [this, id](wxCommandEvent&) {
+			m_values["old_settings_layout_mode"] = (id == 0) ? "1" : "0";
+			m_values["new_settings_layout_mode"] = (id == 1) ? "1" : "0";
+			m_values["dlg_settings_layout_mode"] = (id == 2) ? "1" : "0";
 		});
-		id++;
+	};
+
+	add_radio(&m_rb_old_settings_layout_mode, ++id, app_config->get("old_settings_layout_mode") == "1");
+	add_radio(&m_rb_new_settings_layout_mode, ++id, app_config->get("new_settings_layout_mode") == "1");
+	add_radio(&m_rb_dlg_settings_layout_mode, ++id, app_config->get("dlg_settings_layout_mode") == "1");
+
+#ifdef _MSW_DARK_MODE
+	if (app_config->get("tabs_as_menu") == "1") {
+		m_rb_new_settings_layout_mode->Hide();
+		if (m_rb_new_settings_layout_mode->GetValue()) {
+			m_rb_new_settings_layout_mode->SetValue(false);
+			m_rb_old_settings_layout_mode->SetValue(true);
+		}
 	}
+#endif
 
 	std::string opt_key = "settings_layout_mode";
 	m_blinkers[opt_key] = new BlinkingBitmap(parent);
diff --git a/src/slic3r/GUI/Preferences.hpp b/src/slic3r/GUI/Preferences.hpp
index f8b1d1237..2f8cafeb2 100644
--- a/src/slic3r/GUI/Preferences.hpp
+++ b/src/slic3r/GUI/Preferences.hpp
@@ -12,6 +12,8 @@
 
 class wxColourPickerCtrl;
 class wxBookCtrlBase;
+class wxSlider;
+class wxRadioButton;
 
 namespace Slic3r {
 
@@ -39,6 +41,11 @@ class PreferencesDialog : public DPIDialog
 	std::shared_ptr<ConfigOptionsGroup>	m_optgroup_render;
 #endif // ENABLE_ENVIRONMENT_MAP
 	wxSizer*                            m_icon_size_sizer;
+	wxSlider*							m_icon_size_slider {nullptr};
+	wxRadioButton*						m_rb_old_settings_layout_mode {nullptr};
+	wxRadioButton*						m_rb_new_settings_layout_mode {nullptr};
+	wxRadioButton*						m_rb_dlg_settings_layout_mode {nullptr};
+
 	wxColourPickerCtrl*					m_sys_colour {nullptr};
 	wxColourPickerCtrl*					m_mod_colour {nullptr};
 	wxBookCtrlBase*						tabs {nullptr};
@@ -58,6 +65,7 @@ public:
 	void	build();
 	void	update_ctrls_alignment();
 	void	accept(wxEvent&);
+	void    revert(wxEvent&);
 	void	show(const std::string& highlight_option = std::string(), const std::string& tab_name = std::string());
 
 protected:
@@ -65,6 +73,7 @@ protected:
 	void on_dpi_changed(const wxRect& suggested_rect) override { msw_rescale(); }
 	void on_sys_color_changed() override;
     void layout();
+	void refresh_og(std::shared_ptr<ConfigOptionsGroup> og);
     void create_icon_size_slider();
     void create_settings_mode_widget();
     void create_settings_text_color_widget();

From 8d1a2a8fb3d6210b41fcd032f38165eed42955d1 Mon Sep 17 00:00:00 2001
From: YuSanka <yusanka@gmail.com>
Date: Thu, 5 May 2022 11:38:48 +0200
Subject: [PATCH 15/23] Implemented FR: Update toolbars "on fly", when custom
 toolbar size is editing from Preferences dialog
 (https://dev.prusa3d.com/browse/SPE-1232) + Fixed update of the color pickers

---
 src/slic3r/GUI/Preferences.cpp | 75 +++++++++++++++++++++++-----------
 src/slic3r/GUI/Preferences.hpp |  4 ++
 2 files changed, 56 insertions(+), 23 deletions(-)

diff --git a/src/slic3r/GUI/Preferences.cpp b/src/slic3r/GUI/Preferences.cpp
index c92909a8d..8ad387367 100644
--- a/src/slic3r/GUI/Preferences.cpp
+++ b/src/slic3r/GUI/Preferences.cpp
@@ -9,6 +9,7 @@
 #include "Notebook.hpp"
 #include "ButtonsDescription.hpp"
 #include "OG_CustomCtrl.hpp"
+#include "GLCanvas3D.hpp"
 
 namespace Slic3r {
 
@@ -54,6 +55,14 @@ PreferencesDialog::PreferencesDialog(wxWindow* parent) :
 	m_highlighter.set_timer_owner(this, 0);
 }
 
+static void update_color(wxColourPickerCtrl* color_pckr, const wxColour& color) 
+{
+	if (color_pckr->GetColour() != color) {
+		color_pckr->SetColour(color);
+		wxPostEvent(color_pckr, wxCommandEvent(wxEVT_COLOURPICKER_CHANGED));
+	}
+}
+
 void PreferencesDialog::show(const std::string& highlight_opt_key /*= std::string()*/, const std::string& tab_name/*= std::string()*/)
 {
 	int selected_tab = 0;
@@ -66,6 +75,14 @@ void PreferencesDialog::show(const std::string& highlight_opt_key /*= std::strin
 	if (!highlight_opt_key.empty())
 		init_highlighter(highlight_opt_key);
 
+	// cache input values for custom toolbar size
+	m_custom_toolbar_size		= atoi(get_app_config()->get("custom_toolbar_size").c_str());
+	m_use_custom_toolbar_size	= get_app_config()->get("use_custom_toolbar_size") == "1";
+
+	// update colors for color pickers
+	update_color(m_sys_colour, wxGetApp().get_label_clr_sys());
+	update_color(m_mod_colour, wxGetApp().get_label_clr_modified());
+
 	this->ShowModal();
 }
 
@@ -378,6 +395,10 @@ void PreferencesDialog::build()
 		if (opt_key == "use_custom_toolbar_size") {
 			m_icon_size_sizer->ShowItems(boost::any_cast<bool>(value));
 			refresh_og(m_optgroup_gui);
+			get_app_config()->set("use_custom_toolbar_size", boost::any_cast<bool>(value) ? "1" : "0");
+			get_app_config()->save();
+			wxGetApp().plater()->get_current_canvas3D()->render();
+			return;
 		}
 		if (opt_key == "tabs_as_menu") {
 			bool disable_new_layout = boost::any_cast<bool>(value);
@@ -648,13 +669,31 @@ void PreferencesDialog::accept(wxEvent&)
 #endif // _WIN32
 	
 	wxGetApp().update_ui_from_settings();
-	m_values.clear();
+	clear_cache();
 }
 
 void PreferencesDialog::revert(wxEvent&)
 {
 	auto app_config = get_app_config();
 
+	bool save_app_config = false;
+	if (m_custom_toolbar_size != atoi(app_config->get("custom_toolbar_size").c_str())) {
+		app_config->set("custom_toolbar_size", (boost::format("%d") % m_custom_toolbar_size).str());
+		m_icon_size_slider->SetValue(m_custom_toolbar_size);
+		save_app_config |= true;
+	}
+	if (m_use_custom_toolbar_size != (get_app_config()->get("use_custom_toolbar_size") == "1")) {
+		app_config->set("use_custom_toolbar_size", m_use_custom_toolbar_size ? "1" : "0");
+		save_app_config |= true;
+
+		m_optgroup_gui->set_value("use_custom_toolbar_size", m_use_custom_toolbar_size);
+		m_icon_size_sizer->ShowItems(m_use_custom_toolbar_size);
+		refresh_og(m_optgroup_gui);
+	}
+	if (save_app_config)
+		app_config->save();
+
+
 	for (auto value : m_values) {
 		bool reverted = false;
 		const std::string& key = value.first;
@@ -671,10 +710,6 @@ void PreferencesDialog::revert(wxEvent&)
 			m_optgroup_gui->set_value(key, s_keys_map_NotifyReleaseMode.at(app_config->get(key)));
 			continue;
 		}
-		if (key == "custom_toolbar_size") {
-			m_icon_size_slider->SetValue(atoi(app_config->get("custom_toolbar_size").c_str()));
-			continue;
-		}
 		if (key == "old_settings_layout_mode") {
 			m_rb_old_settings_layout_mode->SetValue(app_config->get(key) == "1");
 			continue;
@@ -706,24 +741,9 @@ void PreferencesDialog::revert(wxEvent&)
 			refresh_og(m_optgroup_gui);
 			continue;
 		}
-
-		if (key == "use_custom_toolbar_size") {
-			m_icon_size_sizer->ShowItems(app_config->get(key) == "1");
-			refresh_og(m_optgroup_gui);
-		}
 	}
 
-	m_values.clear();
-
-	auto revert_colors = [](wxColourPickerCtrl* color_pckr, const wxColour& color) {
-		if (color_pckr->GetColour() != color) {
-			color_pckr->SetColour(color);
-			wxPostEvent(color_pckr, wxCommandEvent(wxEVT_COLOURPICKER_CHANGED));
-		}
-	};
-	revert_colors(m_sys_colour, wxGetApp().get_label_clr_sys());
-	revert_colors(m_mod_colour, wxGetApp().get_label_clr_modified());
-
+	clear_cache();
 	EndModal(wxID_CANCEL);
 }
 
@@ -760,6 +780,12 @@ void PreferencesDialog::layout()
     Refresh();
 }
 
+void PreferencesDialog::clear_cache()
+{
+	m_values.clear();
+	m_custom_toolbar_size = -1;
+}
+
 void PreferencesDialog::refresh_og(std::shared_ptr<ConfigOptionsGroup> og)
 {
 	og->parent()->Layout();
@@ -808,9 +834,12 @@ void PreferencesDialog::create_icon_size_slider()
         m_icon_size_sizer->Add(val_label, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, em);
     }
 
-    m_icon_size_slider->Bind(wxEVT_SLIDER, ([this, val_label](wxCommandEvent e) {
+    m_icon_size_slider->Bind(wxEVT_SLIDER, ([this, val_label, app_config](wxCommandEvent e) {
         auto val = m_icon_size_slider->GetValue();
-        m_values["custom_toolbar_size"] = (boost::format("%d") % val).str();
+
+		app_config->set("custom_toolbar_size", (boost::format("%d") % val).str());
+		app_config->save();
+		wxGetApp().plater()->get_current_canvas3D()->render();
 
         if (val_label)
             val_label->SetLabelText(wxString::Format("%d", val));
diff --git a/src/slic3r/GUI/Preferences.hpp b/src/slic3r/GUI/Preferences.hpp
index 2f8cafeb2..4a82cee00 100644
--- a/src/slic3r/GUI/Preferences.hpp
+++ b/src/slic3r/GUI/Preferences.hpp
@@ -55,6 +55,9 @@ class PreferencesDialog : public DPIDialog
 	bool								m_seq_top_layer_only_changed{ false };
 	bool								m_recreate_GUI{false};
 
+	int									m_custom_toolbar_size{-1};
+	bool								m_use_custom_toolbar_size{false};
+
 public:
 	explicit PreferencesDialog(wxWindow* paren);
 	~PreferencesDialog() = default;
@@ -73,6 +76,7 @@ protected:
 	void on_dpi_changed(const wxRect& suggested_rect) override { msw_rescale(); }
 	void on_sys_color_changed() override;
     void layout();
+	void clear_cache();
 	void refresh_og(std::shared_ptr<ConfigOptionsGroup> og);
     void create_icon_size_slider();
     void create_settings_mode_widget();

From bd644df2f75a7203fe543cc109a8564a43008610 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Hejl?= <hejl.lukas@gmail.com>
Date: Fri, 29 Apr 2022 08:05:52 +0200
Subject: [PATCH 16/23] Suppressed reports of memory leaks from AMD driver and
 D-Bus library.

---
 src/PrusaSlicer.cpp | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/src/PrusaSlicer.cpp b/src/PrusaSlicer.cpp
index 4483d6010..c79d08843 100644
--- a/src/PrusaSlicer.cpp
+++ b/src/PrusaSlicer.cpp
@@ -837,6 +837,9 @@ extern "C" {
                "leak:libnvidia-tls.so\n"        // For NVidia driver.
                "leak:terminator_CreateDevice\n" // For Intel Vulkan drivers.
                "leak:swrast_dri.so\n"           // For Mesa 3D software driver.
+               "leak:amdgpu_dri.so\n"           // For AMD driver.
+               "leak:libdrm_amdgpu.so\n"        // For AMD driver.
+               "leak:libdbus-1.so\n"            // For D-Bus library. Unsure if it is a leak or not.
             ;
     }
 }

From 356bec6e5f10b8062c17cad3bb9210daf50256cd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Hejl?= <hejl.lukas@gmail.com>
Date: Fri, 29 Apr 2022 08:06:24 +0200
Subject: [PATCH 17/23] Added deallocation of wxBoxSizer into
 OptionsGroup::activate_line() when is not used.

---
 src/slic3r/GUI/OptionsGroup.cpp | 12 +++++++++---
 1 file changed, 9 insertions(+), 3 deletions(-)

diff --git a/src/slic3r/GUI/OptionsGroup.cpp b/src/slic3r/GUI/OptionsGroup.cpp
index a861f478f..257fe2532 100644
--- a/src/slic3r/GUI/OptionsGroup.cpp
+++ b/src/slic3r/GUI/OptionsGroup.cpp
@@ -338,10 +338,12 @@ void OptionsGroup::activate_line(Line& line)
                     wxBOTTOM | wxTOP | (option.opt.full_width ? int(wxEXPAND) : int(wxALIGN_CENTER_VERTICAL)), (wxOSX || !staticbox) ? 0 : 2);
             if (is_sizer_field(field))
                 sizer->Add(field->getSizer(), 1, (option.opt.full_width ? int(wxEXPAND) : int(wxALIGN_CENTER_VERTICAL)), 0);
-        }
+        } else
+            delete sizer;
         return;
 	}
 
+    bool sizer_is_used       = false;
     bool is_multioption_line = option_set.size() > 1;
     for (auto opt : option_set) {
 		ConfigOptionDef option = opt.opt;
@@ -356,8 +358,9 @@ void OptionsGroup::activate_line(Line& line)
 				wxSize(sublabel_width != -1 ? sublabel_width * wxGetApp().em_unit() : -1, -1), wxALIGN_RIGHT);
 			label->SetBackgroundStyle(wxBG_STYLE_PAINT);
             label->SetFont(wxGetApp().normal_font());
-			sizer_tmp->Add(label, 0, wxALIGN_CENTER_VERTICAL, 0);
-		}
+            sizer_tmp->Add(label, 0, wxALIGN_CENTER_VERTICAL, 0);
+            sizer_is_used = true;
+        }
 
 		// add field
 		const Option& opt_ref = opt;
@@ -412,6 +415,9 @@ void OptionsGroup::activate_line(Line& line)
         if (!custom_ctrl)
             sizer->Add(line.extra_widget_sizer, 0, wxLEFT | wxALIGN_CENTER_VERTICAL, 4);        //! requires verification
 	}
+
+    if (custom_ctrl && !sizer_is_used)
+        delete sizer;
 }
 
 // create all controls for the option group from the m_lines

From 17e74141ceaf6e14b8279665f31e58031ef67c2a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Hejl?= <hejl.lukas@gmail.com>
Date: Fri, 29 Apr 2022 08:07:30 +0200
Subject: [PATCH 18/23] Fixed a crash in Lightning infill.

---
 src/libslic3r/Fill/Lightning/Generator.cpp | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/src/libslic3r/Fill/Lightning/Generator.cpp b/src/libslic3r/Fill/Lightning/Generator.cpp
index 6a4a675cb..e0cf9680a 100644
--- a/src/libslic3r/Fill/Lightning/Generator.cpp
+++ b/src/libslic3r/Fill/Lightning/Generator.cpp
@@ -116,8 +116,12 @@ void Generator::generateTrees(const PrintObject &print_object)
         if (layer_id == 0)
             return;
 
-        const Polygons& below_outlines = infill_outlines[layer_id - 1];
-        outlines_locator.set_bbox(get_extents(below_outlines).inflated(SCALED_EPSILON));
+        const Polygons &below_outlines      = infill_outlines[layer_id - 1];
+        BoundingBox     below_outlines_bbox = get_extents(below_outlines).inflated(SCALED_EPSILON);
+        if (const BoundingBox &outlines_locator_bbox = outlines_locator.bbox(); outlines_locator_bbox.defined)
+            below_outlines_bbox.merge(outlines_locator_bbox);
+
+        outlines_locator.set_bbox(below_outlines_bbox);
         outlines_locator.create(below_outlines, locator_cell_size);
 
         std::vector<NodeSPtr>& lower_trees = m_lightning_layers[layer_id - 1].tree_roots;

From d069befa1fe0cdad32ded2cbd67bfa5f47224881 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Hejl?= <hejl.lukas@gmail.com>
Date: Fri, 29 Apr 2022 08:08:10 +0200
Subject: [PATCH 19/23] Fixed missing layers of Lightning infill.

---
 src/libslic3r/Fill/Lightning/Layer.cpp    | 31 +++++------------------
 src/libslic3r/Fill/Lightning/TreeNode.cpp | 12 ++++-----
 src/libslic3r/Fill/Lightning/TreeNode.hpp |  6 ++---
 3 files changed, 16 insertions(+), 33 deletions(-)

diff --git a/src/libslic3r/Fill/Lightning/Layer.cpp b/src/libslic3r/Fill/Lightning/Layer.cpp
index 1e1127a79..c996b9b7b 100644
--- a/src/libslic3r/Fill/Lightning/Layer.cpp
+++ b/src/libslic3r/Fill/Lightning/Layer.cpp
@@ -6,6 +6,7 @@
 #include "DistanceField.hpp"
 #include "TreeNode.hpp"
 
+#include "../../ClipperUtils.hpp"
 #include "../../Geometry.hpp"
 #include "Utils.hpp"
 
@@ -271,6 +272,7 @@ void Layer::reconnectRoots
     }
 }
 
+#if 0
 /*!
     * Moves the point \p from onto the nearest polygon or leaves the point as-is, when the comb boundary is not within the root of \p max_dist2 distance.
     * Given a \p distance more than zero, the point will end up inside, and conversely outside.
@@ -398,6 +400,7 @@ static unsigned int moveInside(const Polygons& polygons, Point& from, int distan
     }
     return static_cast<unsigned int>(-1);
 }
+#endif
 
 // Returns 'added someting'.
 Polylines Layer::convertToLines(const Polygons& limit_to_outline, const coord_t line_width) const
@@ -405,31 +408,11 @@ Polylines Layer::convertToLines(const Polygons& limit_to_outline, const coord_t
     if (tree_roots.empty())
         return {};
 
-    Polygons result_lines;
-    for (const auto& tree : tree_roots) {
-        // If even the furthest location in the tree is inside the polygon, the entire tree must be inside of the polygon.
-        // (Don't take the root as that may be on the edge and cause rounding errors to register as 'outside'.)
-        constexpr coord_t epsilon = 5;
-        Point should_be_inside = tree->getLocation();
-        moveInside(limit_to_outline, should_be_inside, epsilon, epsilon * epsilon);
-        if (inside(limit_to_outline, should_be_inside))
-            tree->convertToPolylines(result_lines, line_width);
-    }
+    Polylines result_lines;
+    for (const auto &tree : tree_roots)
+        tree->convertToPolylines(result_lines, line_width);
 
-    // TODO: allow for polylines!
-    Polylines split_lines;
-    for (Polygon &line : result_lines) {
-        if (line.size() <= 1)
-            continue;
-        Point last = line[0];
-        for (size_t point_idx = 1; point_idx < line.size(); point_idx++) {
-            Point here = line[point_idx];
-            split_lines.push_back({ last, here });
-            last = here;
-        }
-    }
-
-    return split_lines;
+    return intersection_pl(result_lines, limit_to_outline);
 }
 
 } // namespace Slic3r::Lightning
diff --git a/src/libslic3r/Fill/Lightning/TreeNode.cpp b/src/libslic3r/Fill/Lightning/TreeNode.cpp
index d1820410e..822550fc4 100644
--- a/src/libslic3r/Fill/Lightning/TreeNode.cpp
+++ b/src/libslic3r/Fill/Lightning/TreeNode.cpp
@@ -343,16 +343,16 @@ coord_t Node::prune(const coord_t& pruning_distance)
     return max_distance_pruned;
 }
 
-void Node::convertToPolylines(Polygons& output, const coord_t line_width) const
+void Node::convertToPolylines(Polylines &output, const coord_t line_width) const
 {
-    Polygons result;
+    Polylines result;
     result.emplace_back();
     convertToPolylines(0, result);
     removeJunctionOverlap(result, line_width);
     append(output, std::move(result));
 }
 
-void Node::convertToPolylines(size_t long_line_idx, Polygons& output) const
+void Node::convertToPolylines(size_t long_line_idx, Polylines &output) const
 {
     if (m_children.empty()) {
         output[long_line_idx].points.push_back(m_p);
@@ -372,12 +372,12 @@ void Node::convertToPolylines(size_t long_line_idx, Polygons& output) const
     }
 }
 
-void Node::removeJunctionOverlap(Polygons& result_lines, const coord_t line_width) const
+void Node::removeJunctionOverlap(Polylines &result_lines, const coord_t line_width) const
 {
     const coord_t reduction = line_width / 2; // TODO make configurable?
     size_t res_line_idx = 0;
     while (res_line_idx < result_lines.size()) {
-        Polygon &polyline = result_lines[res_line_idx];
+        Polyline &polyline = result_lines[res_line_idx];
         if (polyline.size() <= 1) {
             polyline = std::move(result_lines.back());
             result_lines.pop_back();
@@ -387,7 +387,7 @@ void Node::removeJunctionOverlap(Polygons& result_lines, const coord_t line_widt
         coord_t to_be_reduced = reduction;
         Point a = polyline.back();
         for (int point_idx = int(polyline.size()) - 2; point_idx >= 0; point_idx--) {
-            const Point b = polyline[point_idx];
+            const Point b = polyline.points[point_idx];
             const Point ab = b - a;
             const auto ab_len = coord_t(ab.cast<double>().norm());
             if (ab_len >= to_be_reduced) {
diff --git a/src/libslic3r/Fill/Lightning/TreeNode.hpp b/src/libslic3r/Fill/Lightning/TreeNode.hpp
index 55ef35e0a..fdb80d2e6 100644
--- a/src/libslic3r/Fill/Lightning/TreeNode.hpp
+++ b/src/libslic3r/Fill/Lightning/TreeNode.hpp
@@ -239,7 +239,7 @@ public:
      * 
      * \param output all branches in this tree connected into polylines
      */
-    void convertToPolylines(Polygons& output, coord_t line_width) const;
+    void convertToPolylines(Polylines &output, coord_t line_width) const;
 
     /*! If this was ever a direct child of the root, it'll have a previous grounding location.
      *
@@ -258,9 +258,9 @@ protected:
      * \param long_line a reference to a polyline in \p output which to continue building on in the recursion
      * \param output all branches in this tree connected into polylines
      */
-    void convertToPolylines(size_t long_line_idx, Polygons& output) const;
+    void convertToPolylines(size_t long_line_idx, Polylines &output) const;
 
-    void removeJunctionOverlap(Polygons& polylines, coord_t line_width) const;
+    void removeJunctionOverlap(Polylines &polylines, coord_t line_width) const;
 
     bool m_is_root;
     Point m_p;

From 09a9d79e993baa55975e853affd25c22414c45f2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Hejl?= <hejl.lukas@gmail.com>
Date: Fri, 29 Apr 2022 08:09:58 +0200
Subject: [PATCH 20/23] Fix of #8227 (Lightning infill wasn't working when
 "Combine infill every X layers" was set to a different value than one.)

---
 src/libslic3r/Fill/Lightning/Generator.cpp | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/libslic3r/Fill/Lightning/Generator.cpp b/src/libslic3r/Fill/Lightning/Generator.cpp
index e0cf9680a..0bdd1c7e8 100644
--- a/src/libslic3r/Fill/Lightning/Generator.cpp
+++ b/src/libslic3r/Fill/Lightning/Generator.cpp
@@ -63,7 +63,7 @@ void Generator::generateInitialInternalOverhangs(const PrintObject &print_object
         Polygons infill_area_here;
         for (const LayerRegion* layerm : print_object.get_layer(layer_nr)->regions())
             for (const Surface& surface : layerm->fill_surfaces.surfaces)
-                if (surface.surface_type == stInternal)
+                if (surface.surface_type == stInternal || surface.surface_type == stInternalVoid)
                     append(infill_area_here, infill_wall_offset == 0 ? surface.expolygon : offset(surface.expolygon, infill_wall_offset));
 
         //Remove the part of the infill area that is already supported by the walls.
@@ -92,7 +92,7 @@ void Generator::generateTrees(const PrintObject &print_object)
     for (int layer_id = int(print_object.layers().size()) - 1; layer_id >= 0; layer_id--)
         for (const LayerRegion *layerm : print_object.get_layer(layer_id)->regions())
             for (const Surface &surface : layerm->fill_surfaces.surfaces)
-                if (surface.surface_type == stInternal)
+                if (surface.surface_type == stInternal || surface.surface_type == stInternalVoid)
                     append(infill_outlines[layer_id], infill_wall_offset == 0 ? surface.expolygon : offset(surface.expolygon, infill_wall_offset));
 
     // For various operations its beneficial to quickly locate nearby features on the polygon:

From 5a67d0e183d1342a4bebf0780365dd172dea9a5c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Hejl?= <hejl.lukas@gmail.com>
Date: Thu, 5 May 2022 13:52:52 +0200
Subject: [PATCH 21/23] Fixed build on Linux (GCC 11.2).

---
 src/libslic3r/Geometry/ConvexHull.hpp | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/src/libslic3r/Geometry/ConvexHull.hpp b/src/libslic3r/Geometry/ConvexHull.hpp
index 9ba957824..9e9088f1e 100644
--- a/src/libslic3r/Geometry/ConvexHull.hpp
+++ b/src/libslic3r/Geometry/ConvexHull.hpp
@@ -4,6 +4,10 @@
 #include "../Polygon.hpp"
 
 namespace Slic3r {
+
+class ExPolygon;
+using ExPolygons = std::vector<ExPolygon>;
+
 namespace Geometry {
 
 Pointf3s convex_hull(Pointf3s points);

From d4b8d4d0f3abc6be73dfd9aa9b79a035a62229d2 Mon Sep 17 00:00:00 2001
From: Vojtech Bubnik <bubnikv@gmail.com>
Date: Thu, 5 May 2022 17:57:48 +0200
Subject: [PATCH 22/23] Further Perl unit test porting to C++ and Perl
 interface reduction: Ported cooling, gap fill, thin walls and polyline unit
 tests.

---
 src/libslic3r/Config.cpp              |  36 ++++
 src/libslic3r/Config.hpp              |  74 ++++---
 src/libslic3r/ExPolygon.hpp           |   2 +
 src/libslic3r/GCode/CoolingBuffer.hpp |   2 +
 src/libslic3r/Geometry/ConvexHull.hpp |   2 +
 t/clean_polylines.t                   |  83 --------
 t/combineinfill.t                     |  56 ------
 t/config.t                            |  20 --
 t/cooling.t                           | 214 --------------------
 t/flow.t                              |  83 --------
 t/gaps.t                              |  59 ------
 t/gcode.t                             |   9 +-
 t/loops.t                             |  57 ------
 t/thin.t                              | 185 -----------------
 tests/fff_print/CMakeLists.txt        |   4 +
 tests/fff_print/test_cooling.cpp      | 274 ++++++++++++++++++++++++++
 tests/fff_print/test_custom_gcode.cpp | 220 +++++++++++++++++++++
 tests/fff_print/test_data.cpp         |   4 +
 tests/fff_print/test_data.hpp         |   1 +
 tests/fff_print/test_fill.cpp         |   2 +-
 tests/fff_print/test_flow.cpp         | 141 +++++++++----
 tests/fff_print/test_gaps.cpp         |  60 ++++++
 tests/fff_print/test_thin_walls.cpp   | 191 ++++++++++++++++++
 tests/libslic3r/CMakeLists.txt        |   1 +
 tests/libslic3r/test_config.cpp       |  22 ++-
 tests/libslic3r/test_geometry.cpp     |  32 ++-
 tests/libslic3r/test_polygon.cpp      |  62 ++++++
 tests/libslic3r/test_polyline.cpp     |  28 +++
 xs/CMakeLists.txt                     |   1 -
 xs/lib/Slic3r/XS.pm                   |   1 -
 xs/src/perlglue.cpp                   |   1 -
 xs/t/01_trianglemesh.t                |  30 ---
 xs/t/03_point.t                       |   6 +-
 xs/t/04_expolygon.t                   |  17 +-
 xs/t/05_surface.t                     |   7 +-
 xs/t/06_polygon.t                     |  21 --
 xs/t/07_extrusionpath.t               |   4 +-
 xs/t/08_extrusionloop.t               |   3 +-
 xs/t/09_polyline.t                    |  44 +----
 xs/t/10_line.t                        |  17 +-
 xs/t/12_extrusionpathcollection.t     |   8 +-
 xs/t/17_boundingbox.t                 |  27 ---
 xs/xsp/ExPolygon.xsp                  |   2 -
 xs/xsp/GCode.xsp                      |  53 -----
 xs/xsp/my.map                         |   9 -
 xs/xsp/typemap.xspt                   |  18 --
 46 files changed, 1080 insertions(+), 1113 deletions(-)
 delete mode 100644 t/clean_polylines.t
 delete mode 100644 t/config.t
 delete mode 100644 t/cooling.t
 delete mode 100644 t/flow.t
 delete mode 100644 t/gaps.t
 delete mode 100644 t/loops.t
 delete mode 100644 t/thin.t
 create mode 100644 tests/fff_print/test_cooling.cpp
 create mode 100644 tests/fff_print/test_custom_gcode.cpp
 create mode 100644 tests/fff_print/test_gaps.cpp
 create mode 100644 tests/fff_print/test_thin_walls.cpp
 create mode 100644 tests/libslic3r/test_polyline.cpp
 delete mode 100644 xs/t/01_trianglemesh.t
 delete mode 100644 xs/t/06_polygon.t
 delete mode 100644 xs/t/17_boundingbox.t
 delete mode 100644 xs/xsp/GCode.xsp

diff --git a/src/libslic3r/Config.cpp b/src/libslic3r/Config.cpp
index 4b8dfa234..2cfb0740c 100644
--- a/src/libslic3r/Config.cpp
+++ b/src/libslic3r/Config.cpp
@@ -402,6 +402,42 @@ std::ostream& ConfigDef::print_cli_help(std::ostream& out, bool show_defaults, s
     return out;
 }
 
+std::string ConfigBase::SetDeserializeItem::format(std::initializer_list<int> values)
+{
+    std::string out;
+    int i = 0;
+    for (int v : values) {
+        if (i ++ > 0)
+            out += ", ";
+        out += std::to_string(v);
+    }
+    return out;
+}
+
+std::string ConfigBase::SetDeserializeItem::format(std::initializer_list<float> values)
+{
+    std::string out;
+    int i = 0;
+    for (float v : values) {
+        if (i ++ > 0)
+            out += ", ";
+        out += float_to_string_decimal_point(double(v));
+    }
+    return out;
+}
+
+std::string ConfigBase::SetDeserializeItem::format(std::initializer_list<double> values)
+{
+    std::string out;
+    int i = 0;
+    for (float v : values) {
+        if (i ++ > 0)
+            out += ", ";
+        out += float_to_string_decimal_point(v);
+    }
+    return out;
+}
+
 void ConfigBase::apply_only(const ConfigBase &other, const t_config_option_keys &keys, bool ignore_nonexistent)
 {
     // loop through options and apply them
diff --git a/src/libslic3r/Config.hpp b/src/libslic3r/Config.hpp
index bfd307de3..b8c046ceb 100644
--- a/src/libslic3r/Config.hpp
+++ b/src/libslic3r/Config.hpp
@@ -1950,6 +1950,11 @@ public:
             throw BadOptionTypeException("Conversion to a wrong type");
         return static_cast<TYPE*>(opt);
     }
+
+    template<class T> T*       opt(const t_config_option_key &opt_key, bool create = false)
+        { return dynamic_cast<T*>(this->optptr(opt_key, create)); }
+    template<class T> const T* opt(const t_config_option_key &opt_key) const
+        { return dynamic_cast<const T*>(this->optptr(opt_key)); }
     
     // Apply all keys of other ConfigBase defined by this->def() to this ConfigBase.
     // An UnknownOptionException is thrown in case some option keys of other are not defined by this->def(),
@@ -1995,11 +2000,23 @@ public:
     	SetDeserializeItem(const std::string &opt_key, const bool value, bool append = false) : opt_key(opt_key), opt_value(value ? "1" : "0"), append(append) {}
     	SetDeserializeItem(const char *opt_key, const int value, bool append = false) : opt_key(opt_key), opt_value(std::to_string(value)), append(append) {}
     	SetDeserializeItem(const std::string &opt_key, const int value, bool append = false) : opt_key(opt_key), opt_value(std::to_string(value)), append(append) {}
+        SetDeserializeItem(const char *opt_key, const std::initializer_list<int> values, bool append = false) : opt_key(opt_key), opt_value(format(values)), append(append) {}
+        SetDeserializeItem(const std::string &opt_key, const std::initializer_list<int> values, bool append = false) : opt_key(opt_key), opt_value(format(values)), append(append) {}
         SetDeserializeItem(const char *opt_key, const float value, bool append = false) : opt_key(opt_key), opt_value(float_to_string_decimal_point(value)), append(append) {}
         SetDeserializeItem(const std::string &opt_key, const float value, bool append = false) : opt_key(opt_key), opt_value(float_to_string_decimal_point(value)), append(append) {}
         SetDeserializeItem(const char *opt_key, const double value, bool append = false) : opt_key(opt_key), opt_value(float_to_string_decimal_point(value)), append(append) {}
         SetDeserializeItem(const std::string &opt_key, const double value, bool append = false) : opt_key(opt_key), opt_value(float_to_string_decimal_point(value)), append(append) {}
+        SetDeserializeItem(const char *opt_key, const std::initializer_list<float> values, bool append = false) : opt_key(opt_key), opt_value(format(values)), append(append) {}
+        SetDeserializeItem(const std::string &opt_key, const std::initializer_list<float> values, bool append = false) : opt_key(opt_key), opt_value(format(values)), append(append) {}
+        SetDeserializeItem(const char *opt_key, const std::initializer_list<double> values, bool append = false) : opt_key(opt_key), opt_value(format(values)), append(append) {}
+        SetDeserializeItem(const std::string &opt_key, const std::initializer_list<double> values, bool append = false) : opt_key(opt_key), opt_value(format(values)), append(append) {}
+
     	std::string opt_key; std::string opt_value; bool append = false;
+
+    private:
+        static std::string format(std::initializer_list<int> values);
+        static std::string format(std::initializer_list<float> values);
+        static std::string format(std::initializer_list<double> values);
     };
 	// May throw BadOptionTypeException() if the operation fails.
     void set_deserialize(std::initializer_list<SetDeserializeItem> items, ConfigSubstitutionContext& substitutions);
@@ -2008,7 +2025,31 @@ public:
 
     double get_abs_value(const t_config_option_key &opt_key) const;
     double get_abs_value(const t_config_option_key &opt_key, double ratio_over) const;
-    void setenv_() const;
+
+    std::string&        opt_string(const t_config_option_key &opt_key, bool create = false)     { return this->option<ConfigOptionString>(opt_key, create)->value; }
+    const std::string&  opt_string(const t_config_option_key &opt_key) const                    { return const_cast<ConfigBase*>(this)->opt_string(opt_key); }
+    std::string&        opt_string(const t_config_option_key &opt_key, unsigned int idx)        { return this->option<ConfigOptionStrings>(opt_key)->get_at(idx); }
+    const std::string&  opt_string(const t_config_option_key &opt_key, unsigned int idx) const  { return const_cast<ConfigBase*>(this)->opt_string(opt_key, idx); }
+
+    double&             opt_float(const t_config_option_key &opt_key)                           { return this->option<ConfigOptionFloat>(opt_key)->value; }
+    const double&       opt_float(const t_config_option_key &opt_key) const                     { return dynamic_cast<const ConfigOptionFloat*>(this->option(opt_key))->value; }
+    double&             opt_float(const t_config_option_key &opt_key, unsigned int idx)         { return this->option<ConfigOptionFloats>(opt_key)->get_at(idx); }
+    const double&       opt_float(const t_config_option_key &opt_key, unsigned int idx) const   { return dynamic_cast<const ConfigOptionFloats*>(this->option(opt_key))->get_at(idx); }
+
+    int&                opt_int(const t_config_option_key &opt_key)                             { return this->option<ConfigOptionInt>(opt_key)->value; }
+    int                 opt_int(const t_config_option_key &opt_key) const                       { return dynamic_cast<const ConfigOptionInt*>(this->option(opt_key))->value; }
+    int&                opt_int(const t_config_option_key &opt_key, unsigned int idx)           { return this->option<ConfigOptionInts>(opt_key)->get_at(idx); }
+    int                 opt_int(const t_config_option_key &opt_key, unsigned int idx) const     { return dynamic_cast<const ConfigOptionInts*>(this->option(opt_key))->get_at(idx); }
+
+    // In ConfigManipulation::toggle_print_fff_options, it is called on option with type ConfigOptionEnumGeneric* and also ConfigOptionEnum*.
+    // Thus the virtual method getInt() is used to retrieve the enum value.
+    template<typename ENUM>
+    ENUM                opt_enum(const t_config_option_key &opt_key) const                      { return static_cast<ENUM>(this->option(opt_key)->getInt()); }
+
+    bool                opt_bool(const t_config_option_key &opt_key) const                      { return this->option<ConfigOptionBool>(opt_key)->value != 0; }
+    bool                opt_bool(const t_config_option_key &opt_key, unsigned int idx) const    { return this->option<ConfigOptionBools>(opt_key)->get_at(idx) != 0; }
+
+    void                setenv_() const;
     ConfigSubstitutions load(const std::string &file, ForwardCompatibilitySubstitutionRule compatibility_rule);
     ConfigSubstitutions load_from_ini(const std::string &file, ForwardCompatibilitySubstitutionRule compatibility_rule);
     ConfigSubstitutions load_from_ini_string(const std::string &data, ForwardCompatibilitySubstitutionRule compatibility_rule);
@@ -2017,10 +2058,10 @@ public:
     ConfigSubstitutions load_from_ini_string_commented(std::string &&data, ForwardCompatibilitySubstitutionRule compatibility_rule);
     ConfigSubstitutions load_from_gcode_file(const std::string &file, ForwardCompatibilitySubstitutionRule compatibility_rule);
     ConfigSubstitutions load(const boost::property_tree::ptree &tree, ForwardCompatibilitySubstitutionRule compatibility_rule);
-    void save(const std::string &file) const;
+    void                save(const std::string &file) const;
 
 	// Set all the nullable values to nils.
-    void null_nullables();
+    void                null_nullables();
 
     static size_t load_from_gcode_string_legacy(ConfigBase& config, const char* str, ConfigSubstitutionContext& substitutions);
 
@@ -2129,10 +2170,6 @@ public:
     // Allow DynamicConfig to be instantiated on ints own without a definition.
     // If the definition is not defined, the method requiring the definition will throw NoDefinitionException.
     const ConfigDef*        def() const override { return nullptr; }
-    template<class T> T*    opt(const t_config_option_key &opt_key, bool create = false)
-        { return dynamic_cast<T*>(this->option(opt_key, create)); }
-    template<class T> const T* opt(const t_config_option_key &opt_key) const
-        { return dynamic_cast<const T*>(this->option(opt_key)); }
     // Overrides ConfigResolver::optptr().
     const ConfigOption*     optptr(const t_config_option_key &opt_key) const override;
     // Overrides ConfigBase::optptr(). Find ando/or create a ConfigOption instance for a given name.
@@ -2163,29 +2200,6 @@ public:
     // Returns options being equal in the two configs, ignoring options not present in both configs.
     t_config_option_keys equal(const DynamicConfig &other) const;
 
-    std::string&        opt_string(const t_config_option_key &opt_key, bool create = false)     { return this->option<ConfigOptionString>(opt_key, create)->value; }
-    const std::string&  opt_string(const t_config_option_key &opt_key) const                    { return const_cast<DynamicConfig*>(this)->opt_string(opt_key); }
-    std::string&        opt_string(const t_config_option_key &opt_key, unsigned int idx)        { return this->option<ConfigOptionStrings>(opt_key)->get_at(idx); }
-    const std::string&  opt_string(const t_config_option_key &opt_key, unsigned int idx) const  { return const_cast<DynamicConfig*>(this)->opt_string(opt_key, idx); }
-
-    double&             opt_float(const t_config_option_key &opt_key)                           { return this->option<ConfigOptionFloat>(opt_key)->value; }
-    const double&       opt_float(const t_config_option_key &opt_key) const                     { return dynamic_cast<const ConfigOptionFloat*>(this->option(opt_key))->value; }
-    double&             opt_float(const t_config_option_key &opt_key, unsigned int idx)         { return this->option<ConfigOptionFloats>(opt_key)->get_at(idx); }
-    const double&       opt_float(const t_config_option_key &opt_key, unsigned int idx) const   { return dynamic_cast<const ConfigOptionFloats*>(this->option(opt_key))->get_at(idx); }
-
-    int&                opt_int(const t_config_option_key &opt_key)                             { return this->option<ConfigOptionInt>(opt_key)->value; }
-    int                 opt_int(const t_config_option_key &opt_key) const                       { return dynamic_cast<const ConfigOptionInt*>(this->option(opt_key))->value; }
-    int&                opt_int(const t_config_option_key &opt_key, unsigned int idx)           { return this->option<ConfigOptionInts>(opt_key)->get_at(idx); }
-    int                 opt_int(const t_config_option_key &opt_key, unsigned int idx) const     { return dynamic_cast<const ConfigOptionInts*>(this->option(opt_key))->get_at(idx); }
-
-    // In ConfigManipulation::toggle_print_fff_options, it is called on option with type ConfigOptionEnumGeneric* and also ConfigOptionEnum*.
-    // Thus the virtual method getInt() is used to retrieve the enum value.
-    template<typename ENUM>
-    ENUM                opt_enum(const t_config_option_key &opt_key) const                      { return static_cast<ENUM>(this->option(opt_key)->getInt()); }
-
-    bool                opt_bool(const t_config_option_key &opt_key) const                      { return this->option<ConfigOptionBool>(opt_key)->value != 0; }
-    bool                opt_bool(const t_config_option_key &opt_key, unsigned int idx) const    { return this->option<ConfigOptionBools>(opt_key)->get_at(idx) != 0; }
-
     // Command line processing
     bool                read_cli(int argc, const char* const argv[], t_config_option_keys* extra, t_config_option_keys* keys = nullptr);
 
diff --git a/src/libslic3r/ExPolygon.hpp b/src/libslic3r/ExPolygon.hpp
index 344450c4a..7eccf2ec8 100644
--- a/src/libslic3r/ExPolygon.hpp
+++ b/src/libslic3r/ExPolygon.hpp
@@ -67,6 +67,8 @@ public:
     void simplify(double tolerance, ExPolygons* expolygons) const;
     void medial_axis(double max_width, double min_width, ThickPolylines* polylines) const;
     void medial_axis(double max_width, double min_width, Polylines* polylines) const;
+    Polylines medial_axis(double max_width, double min_width) const 
+        { Polylines out; this->medial_axis(max_width, min_width, &out); return out; }
     Lines lines() const;
 
     // Number of contours (outer contour with holes).
diff --git a/src/libslic3r/GCode/CoolingBuffer.hpp b/src/libslic3r/GCode/CoolingBuffer.hpp
index 1fe040518..91a81c7f3 100644
--- a/src/libslic3r/GCode/CoolingBuffer.hpp
+++ b/src/libslic3r/GCode/CoolingBuffer.hpp
@@ -26,6 +26,8 @@ public:
     void        reset(const Vec3d &position);
     void        set_current_extruder(unsigned int extruder_id) { m_current_extruder = extruder_id; }
     std::string process_layer(std::string &&gcode, size_t layer_id, bool flush);
+    std::string process_layer(const std::string &gcode, size_t layer_id, bool flush)
+        { return this->process_layer(std::string(gcode), layer_id, flush); }
 
 private:
 	CoolingBuffer& operator=(const CoolingBuffer&) = delete;
diff --git a/src/libslic3r/Geometry/ConvexHull.hpp b/src/libslic3r/Geometry/ConvexHull.hpp
index 9e9088f1e..94f4e4cf2 100644
--- a/src/libslic3r/Geometry/ConvexHull.hpp
+++ b/src/libslic3r/Geometry/ConvexHull.hpp
@@ -1,6 +1,8 @@
 #ifndef slic3r_Geometry_ConvexHull_hpp_
 #define slic3r_Geometry_ConvexHull_hpp_
 
+#include <vector>
+
 #include "../Polygon.hpp"
 
 namespace Slic3r {
diff --git a/t/clean_polylines.t b/t/clean_polylines.t
deleted file mode 100644
index 50c6f5bbd..000000000
--- a/t/clean_polylines.t
+++ /dev/null
@@ -1,83 +0,0 @@
-use Test::More;
-use strict;
-use warnings;
-
-plan tests => 6;
-
-BEGIN {
-    use FindBin;
-    use lib "$FindBin::Bin/../lib";
-    use local::lib "$FindBin::Bin/../local-lib";
-}
-
-use Slic3r;
-
-{
-    my $polyline = Slic3r::Polyline->new(
-        [0,0],[1,0],[2,0],[2,1],[2,2],[1,2],[0,2],[0,1],[0,0],
-    );
-    $polyline->simplify(1);
-    is_deeply $polyline->pp, [ [0, 0], [2, 0], [2, 2], [0, 2], [0, 0] ], 'Douglas-Peucker';
-}
-
-{
-    my $polyline = Slic3r::Polyline->new(
-        [0,0], [50,50], [100,0], [125,-25], [150,50],
-    );
-    $polyline->simplify(25);
-    is_deeply $polyline->pp, [ [0, 0], [50, 50], [125, -25], [150, 50] ], 'Douglas-Peucker';
-}
-
-{
-    my $gear = Slic3r::Polygon->new_scale(
-        [144.9694,317.1543], [145.4181,301.5633], [146.3466,296.921], [131.8436,294.1643], [131.7467,294.1464], 
-        [121.7238,291.5082], [117.1631,290.2776], [107.9198,308.2068], [100.1735,304.5101], [104.9896,290.3672], 
-        [106.6511,286.2133], [93.453,279.2327], [81.0065,271.4171], [67.7886,286.5055], [60.7927,280.1127], 
-        [69.3928,268.2566], [72.7271,264.9224], [61.8152,253.9959], [52.2273,242.8494], [47.5799,245.7224], 
-        [34.6577,252.6559], [30.3369,245.2236], [42.1712,236.3251], [46.1122,233.9605], [43.2099,228.4876], 
-        [35.0862,211.5672], [33.1441,207.0856], [13.3923,212.1895], [10.6572,203.3273], [6.0707,204.8561], 
-        [7.2775,204.4259], [29.6713,196.3631], [25.9815,172.1277], [25.4589,167.2745], [19.8337,167.0129], 
-        [5.0625,166.3346], [5.0625,156.9425], [5.3701,156.9282], [21.8636,156.1628], [25.3713,156.4613], 
-        [25.4243,155.9976], [29.3432,155.8157], [30.3838,149.3549], [26.3596,147.8137], [27.1085,141.2604], 
-        [29.8466,126.8337], [24.5841,124.9201], [10.6664,119.8989], [13.4454,110.9264], [33.1886,116.0691], 
-        [38.817,103.1819], [45.8311,89.8133], [30.4286,76.81], [35.7686,70.0812], [48.0879,77.6873], 
-        [51.564,81.1635], [61.9006,69.1791], [72.3019,58.7916], [60.5509,42.5416], [68.3369,37.1532], 
-        [77.9524,48.1338], [80.405,52.2215], [92.5632,44.5992], [93.0123,44.3223], [106.3561,37.2056], 
-        [100.8631,17.4679], [108.759,14.3778], [107.3148,11.1283], [117.0002,32.8627], [140.9109,27.3974], 
-        [145.7004,26.4994], [145.1346,6.1011], [154.502,5.4063], [156.9398,25.6501], [171.0557,26.2017], 
-        [181.3139,27.323], [186.2377,27.8532], [191.6031,8.5474], [200.6724,11.2756], [197.2362,30.2334], 
-        [220.0789,39.1906], [224.3261,41.031], [236.3506,24.4291], [243.6897,28.6723], [234.2956,46.7747], 
-        [245.6562,55.1643], [257.2523,65.0901], [261.4374,61.5679], [273.1709,52.8031], [278.555,59.5164], 
-        [268.4334,69.8001], [264.1615,72.3633], [268.2763,77.9442], [278.8488,93.5305], [281.4596,97.6332], 
-        [286.4487,95.5191], [300.2821,90.5903], [303.4456,98.5849], [286.4523,107.7253], [293.7063,131.1779], 
-        [294.9748,135.8787], [314.918,133.8172], [315.6941,143.2589], [300.9234,146.1746], [296.6419,147.0309], 
-        [297.1839,161.7052], [296.6136,176.3942], [302.1147,177.4857], [316.603,180.3608], [317.1658,176.7341], 
-        [315.215,189.6589], [315.1749,189.6548], [294.9411,187.5222], [291.13,201.7233], [286.2615,215.5916], 
-        [291.1944,218.2545], [303.9158,225.1271], [299.2384,233.3694], [285.7165,227.6001], [281.7091,225.1956], 
-        [273.8981,237.6457], [268.3486,245.2248], [267.4538,246.4414], [264.8496,250.0221], [268.6392,253.896], 
-        [278.5017,265.2131], [272.721,271.4403], [257.2776,258.3579], [234.4345,276.5687], [242.6222,294.8315], 
-        [234.9061,298.5798], [227.0321,286.2841], [225.2505,281.8301], [211.5387,287.8187], [202.3025,291.0935], 
-        [197.307,292.831], [199.808,313.1906], [191.5298,315.0787], [187.3082,299.8172], [186.4201,295.3766], 
-        [180.595,296.0487], [161.7854,297.4248], [156.8058,297.6214], [154.3395,317.8592],
-    );
-    
-    my $num_points = scalar @$gear;
-    my $simplified = $gear->simplify(1000);
-    ok @$simplified == 1, 'gear simplified to a single polygon';
-    ###note sprintf "original points: %d\nnew points: %d", $num_points, scalar(@{$simplified->[0]});
-    ok @{$simplified->[0]} < $num_points, 'gear was further simplified using Douglas-Peucker';
-}
-
-{
-
-    my $hole_in_square = Slic3r::Polygon->new(  # cw
-        [140, 140],
-        [140, 160],
-        [160, 160],
-        [160, 140],
-    );
-    my $simplified = $hole_in_square->simplify(2);
-    is scalar(@$simplified), 1, 'hole simplification returns one polygon';
-    ok $simplified->[0]->is_counter_clockwise, 'hole simplification turns cw polygon into ccw polygon';
-}
-
diff --git a/t/combineinfill.t b/t/combineinfill.t
index a19e817a1..ebb430419 100644
--- a/t/combineinfill.t
+++ b/t/combineinfill.t
@@ -107,60 +107,4 @@ plan tests => 8;
             'infill combination is idempotent';
 }
 
-# the following needs to be adapted to the new API
-if (0) {
-    my $config = Slic3r::Config::new_from_defaults;
-    $config->set('skirts', 0);
-    $config->set('solid_layers', 0);
-    $config->set('bottom_solid_layers', 0);
-    $config->set('top_solid_layers', 0);
-    $config->set('infill_every_layers', 6);
-    $config->set('layer_height', 0.06);
-    $config->set('perimeters', 1);
-    
-    my $test = sub {
-        my ($shift) = @_;
-        
-        my $self = Slic3r::Test::init_print('20mm_cube', config => $config);
-        
-        $shift /= &Slic3r::SCALING_FACTOR;
-        my $scale = 4; # make room for fat infill lines with low layer height
-
-        # Put a slope on the box's sides by shifting x and y coords by $tilt * (z / boxheight).
-        # The test here is to put such a slight slope on the walls that it should
-        # not trigger any extra fill on fill layers that should be empty when 
-        # combine infill is enabled.
-        $_->[0] += $shift * ($_->[2] / (20 / &Slic3r::SCALING_FACTOR)) for @{$self->objects->[0]->meshes->[0]->vertices};
-        $_->[1] += $shift * ($_->[2] / (20 / &Slic3r::SCALING_FACTOR)) for @{$self->objects->[0]->meshes->[0]->vertices};
-        $_ = [$_->[0]*$scale, $_->[1]*$scale, $_->[2]] for @{$self->objects->[0]->meshes->[0]->vertices};
-                
-        # copy of Print::export_gcode() up to the point 
-        # after fill surfaces are combined
-        $_->slice for @{$self->objects};
-        $_->make_perimeters for @{$self->objects};
-        $_->detect_surfaces_type for @{$self->objects};
-        $_->prepare_fill_surfaces for map @{$_->regions}, map @{$_->layers}, @{$self->objects};
-        $_->process_external_surfaces for map @{$_->regions}, map @{$_->layers}, @{$self->objects};
-        $_->discover_horizontal_shells for @{$self->objects};
-        $_->combine_infill for @{$self->objects};
-
-        # Only layers with id % 6 == 0 should have fill.
-        my $spurious_infill = 0;
-        foreach my $layer (map @{$_->layers}, @{$self->objects}) {
-            ++$spurious_infill if ($layer->id % 6 && grep @{$_->fill_surfaces} > 0, @{$layer->regions});
-        }
-
-        $spurious_infill -= scalar(@{$self->objects->[0]->layers} - 1) % 6;
-        
-        fail "spurious fill surfaces found on layers that should have none (walls " . sprintf("%.4f", Slic3r::Geometry::rad2deg(atan2($shift, 20/&Slic3r::SCALING_FACTOR))) . " degrees off vertical)"
-            unless $spurious_infill == 0;
-        1;
-    };
-    
-    # Test with mm skew offsets for the top of the 20mm-high box
-    for my $shift (0, 0.0001, 1) {
-        ok $test->($shift), "no spurious fill surfaces with box walls " . sprintf("%.4f",Slic3r::Geometry::rad2deg(atan2($shift, 20))) . " degrees off of vertical";
-    }
-}
-
 __END__
diff --git a/t/config.t b/t/config.t
deleted file mode 100644
index f4a1867de..000000000
--- a/t/config.t
+++ /dev/null
@@ -1,20 +0,0 @@
-use Test::More tests => 1;
-use strict;
-use warnings;
-
-BEGIN {
-    use FindBin;
-    use lib "$FindBin::Bin/../lib";
-    use local::lib "$FindBin::Bin/../local-lib";
-}
-
-use Slic3r;
-use Slic3r::Test;
-
-{
-    my $config = Slic3r::Config::new_from_defaults;
-    $config->set('perimeter_extrusion_width', '250%');
-    ok $config->validate, 'percent extrusion width is validated';
-}
-
-__END__
diff --git a/t/cooling.t b/t/cooling.t
deleted file mode 100644
index e46cfa2f7..000000000
--- a/t/cooling.t
+++ /dev/null
@@ -1,214 +0,0 @@
-use Test::More;
-use strict;
-use warnings;
-
-plan tests => 14;
-
-BEGIN {
-    use FindBin;
-    use lib "$FindBin::Bin/../lib";
-    use local::lib "$FindBin::Bin/../local-lib";
-}
-
-use List::Util qw(none all);
-use Slic3r;
-use Slic3r::Test;
-
-my $gcodegen;
-sub buffer {
-    my $config = shift;
-    if (defined($config)) {
-        $config = $config->clone();
-    } else {
-        $config = Slic3r::Config->new;
-    }
-    my $config_override = shift;
-    foreach my $key (keys %{$config_override}) {
-        $config->set($key, ${$config_override}{$key});
-    }
-
-    my $print_config = Slic3r::Config::Print->new;
-    $print_config->apply_dynamic($config);
-    
-    $gcodegen = Slic3r::GCode->new;
-    $gcodegen->apply_print_config($print_config);
-    $gcodegen->set_layer_count(10);
-
-    my $extruders_ref = shift;
-    $extruders_ref = [ 0 ] if !defined $extruders_ref;
-    $gcodegen->set_extruders($extruders_ref);
-    return Slic3r::GCode::CoolingBuffer->new($gcodegen);
-}
-
-my $gcode1      = "G1 X100 E1 F3000\n";
-my $print_time1 = 100 / (3000 / 60); # 2 sec
-my $gcode2      = $gcode1 . "G1 X0 E1 F3000\n";
-my $print_time2 = 2 * $print_time1; # 4 sec
-
-my $config = Slic3r::Config::new_from_defaults;
-# Default cooling settings.
-$config->set('bridge_fan_speed',            [ 100 ]);
-$config->set('cooling',                     [ 1 ]);
-$config->set('fan_always_on',               [ 0 ]);
-$config->set('fan_below_layer_time',        [ 60 ]);
-$config->set('max_fan_speed',               [ 100 ]);
-$config->set('min_print_speed',             [ 10 ]);
-$config->set('slowdown_below_layer_time',   [ 5 ]);
-# Default print speeds.
-$config->set('bridge_speed',                60);
-$config->set('external_perimeter_speed',    '50%');
-$config->set('first_layer_speed',           30);
-$config->set('gap_fill_speed',              20);
-$config->set('infill_speed',                80);
-$config->set('perimeter_speed',             60);
-$config->set('small_perimeter_speed',       15);
-$config->set('solid_infill_speed',          20);
-$config->set('top_solid_infill_speed',      15);
-$config->set('max_print_speed',             80);
-# Override for tests.
-$config->set('disable_fan_first_layers',    [ 0 ]);
-
-{
-    my $gcode_src  = "G1 F3000;_EXTRUDE_SET_SPEED\nG1 X100 E1";
-    # Print time of $gcode.
-    my $print_time = 100 / (3000 / 60);
-    my $buffer = buffer($config, { 'slowdown_below_layer_time' => [ $print_time * 0.999 ] });
-    my $gcode = $buffer->process_layer($gcode_src, 0);
-    like $gcode, qr/F3000/, 'speed is not altered when elapsed time is greater than slowdown threshold';
-}
-
-{
-    my $gcode_src  = 
-        "G1 X50 F2500\n" .
-        "G1 F3000;_EXTRUDE_SET_SPEED\n" .
-        "G1 X100 E1\n" .
-        ";_EXTRUDE_END\n" .
-        "G1 E4 F400",
-    # Print time of $gcode.
-    my $print_time = 50 / (2500 / 60) + 100 / (3000 / 60) + 4 / (400 / 60);
-    my $buffer = buffer($config, { 'slowdown_below_layer_time' => [ $print_time * 1.001 ] });
-    my $gcode  = $buffer->process_layer($gcode_src, 0);
-    unlike $gcode, qr/F3000/, 'speed is altered when elapsed time is lower than slowdown threshold';
-    like $gcode, qr/F2500/, 'speed is not altered for travel moves';
-    like $gcode, qr/F400/, 'speed is not altered for extruder-only moves';
-}
-
-{
-    my $buffer = buffer($config, {
-            'fan_below_layer_time'      => [ $print_time1 * 0.88 ],
-            'slowdown_below_layer_time' => [ $print_time1 * 0.99 ]
-        });
-    my $gcode = $buffer->process_layer($gcode1, 0);
-    unlike $gcode, qr/M106/, 'fan is not activated when elapsed time is greater than fan threshold';
-}
-
-{
-    my $gcode .= buffer($config, { 'slowdown_below_layer_time', [ $print_time2 * 0.99 ] })->process_layer($gcode2, 0);
-    like $gcode, qr/F3000/, 'slowdown is computed on all objects printing at the same Z';
-}
-
-{
-    # use an elapsed time which is < the threshold but greater than it when summed twice
-    my $buffer = buffer($config, {
-            'fan_below_layer_time'      => [ $print_time2 * 0.65], 
-            'slowdown_below_layer_time' => [ $print_time2 * 0.7 ] 
-        });
-    my $gcode = $buffer->process_layer($gcode2, 0) .
-                $buffer->process_layer($gcode2, 1);
-    unlike $gcode, qr/M106/, 'fan is not activated on all objects printing at different Z';
-}
-
-{
-    # use an elapsed time which is < the threshold even when summed twice
-    my $buffer = buffer($config, {
-            'fan_below_layer_time'      => [ $print_time2 + 1 ], 
-            'slowdown_below_layer_time' => [ $print_time2 + 2 ] 
-        });
-    my $gcode = $buffer->process_layer($gcode2, 0) .
-                $buffer->process_layer($gcode2, 1);
-    like $gcode, qr/M106/, 'fan is activated on all objects printing at different Z';
-}
-
-{
-    my $buffer = buffer($config, {
-            'cooling'                   => [ 1               , 0                ],
-            'fan_below_layer_time'      => [ $print_time2 + 1, $print_time2 + 1 ], 
-            'slowdown_below_layer_time' => [ $print_time2 + 2, $print_time2 + 2 ]
-        },
-	[ 0, 1]);
-    my $gcode = $buffer->process_layer($gcode1 . "T1\nG1 X0 E1 F3000\n", 0);
-    like $gcode, qr/^M106/, 'fan is activated for the 1st tool';
-    like $gcode, qr/.*M107/, 'fan is disabled for the 2nd tool';
-}
-
-{
-    my $config = Slic3r::Config::new_from_defaults;
-    $config->set('cooling', [ 1 ]);
-    $config->set('bridge_fan_speed', [ 100 ]);
-    $config->set('fan_below_layer_time', [ 0 ]);
-    $config->set('slowdown_below_layer_time', [ 0 ]);
-    $config->set('bridge_speed', 99);
-    $config->set('top_solid_layers', 1);     # internal bridges use solid_infil speed
-    $config->set('bottom_solid_layers', 1);  # internal bridges use solid_infil speed
-    
-    my $print = Slic3r::Test::init_print('overhang', config => $config);
-    my $fan = 0;
-    my $fan_with_incorrect_speeds = my $fan_with_incorrect_print_speeds = 0;
-    my $bridge_with_no_fan = 0;
-    Slic3r::GCode::Reader->new->parse(Slic3r::Test::gcode($print), sub {
-        my ($self, $cmd, $args, $info) = @_;
-        
-        if ($cmd eq 'M106') {
-            $fan = $args->{S};
-            $fan_with_incorrect_speeds++ if $fan != 255;
-        } elsif ($cmd eq 'M107') {
-            $fan = 0;
-        } elsif ($info->{extruding} && $info->{dist_XY} > 0) {
-            $fan_with_incorrect_print_speeds++
-                if ($fan > 0) && ($args->{F} // $self->F) != 60*$config->bridge_speed;
-            $bridge_with_no_fan++
-                if !$fan && ($args->{F} // $self->F) == 60*$config->bridge_speed;
-        }
-    });
-    ok !$fan_with_incorrect_speeds, 'bridge fan speed is applied correctly';
-    ok !$fan_with_incorrect_print_speeds, 'bridge fan is only turned on for bridges';
-    ok !$bridge_with_no_fan, 'bridge fan is turned on for all bridges';
-}
-
-{
-    my $config = Slic3r::Config::new_from_defaults;
-    $config->set('cooling', [ 1 ]);
-    $config->set('fan_below_layer_time', [ 0 ]);
-    $config->set('slowdown_below_layer_time', [ 10 ]);
-    $config->set('min_print_speed', [ 0 ]);
-    $config->set('start_gcode', '');
-    $config->set('first_layer_speed', '100%');
-    $config->set('external_perimeter_speed', 99);
-    
-    my $print = Slic3r::Test::init_print('20mm_cube', config => $config);
-    my @layer_times = (0);  # in seconds
-    my %layer_external = ();  # z => 1
-    Slic3r::GCode::Reader->new->parse(my $gcode = Slic3r::Test::gcode($print), sub {
-        my ($self, $cmd, $args, $info) = @_;
-        
-        if ($cmd eq 'G1') {
-            if ($info->{dist_Z}) {
-                push @layer_times, 0;
-                $layer_external{ $args->{Z} } = 0;
-            }
-            $layer_times[-1] += abs($info->{dist_XY} || $info->{dist_E} || $info->{dist_Z} || 0) / ($args->{F} // $self->F) * 60;
-            if ($args->{F} && $args->{F} == $config->external_perimeter_speed*60) {
-                $layer_external{ $self->Z }++;
-            }
-        }
-    });
-    @layer_times = grep $_, @layer_times;
-    my $all_below = none { $_ < $config->slowdown_below_layer_time->[0] } @layer_times;
-    ok $all_below, 'slowdown_below_layer_time is honored';
-    
-    # check that all layers have at least one unaltered external perimeter speed
-#    my $external = all { $_ > 0 } values %layer_external;
-#    ok $external, 'slowdown_below_layer_time does not alter external perimeters';
-}
-
-__END__
diff --git a/t/flow.t b/t/flow.t
deleted file mode 100644
index 50c491604..000000000
--- a/t/flow.t
+++ /dev/null
@@ -1,83 +0,0 @@
-use Test::More tests => 6;
-use strict;
-use warnings;
-
-BEGIN {
-    use FindBin;
-    use lib "$FindBin::Bin/../lib";
-    use local::lib "$FindBin::Bin/../local-lib";
-}
-
-use List::Util qw(first sum);
-use Slic3r;
-use Slic3r::Geometry qw(scale PI);
-use Slic3r::Test;
-
-{
-    my $config = Slic3r::Config::new_from_defaults;
-    $config->set('skirts', 1);
-    $config->set('brim_width', 2);
-    $config->set('perimeters', 3);
-    $config->set('fill_density', 0.4);
-    $config->set('bottom_solid_layers', 1);
-    $config->set('first_layer_extrusion_width', 2);
-    $config->set('first_layer_height', $config->layer_height);
-    $config->set('filament_diameter', [ 3.0 ]);
-    $config->set('nozzle_diameter', [ 0.5 ]);
-    
-    my $print = Slic3r::Test::init_print('20mm_cube', config => $config);
-    my @E_per_mm = ();
-    Slic3r::GCode::Reader->new->parse(my $gcode = Slic3r::Test::gcode($print), sub {
-        my ($self, $cmd, $args, $info) = @_;
-        
-        if ($self->Z == $config->layer_height) {  # only consider first layer
-            if ($info->{extruding} && $info->{dist_XY} > 0) {
-                push @E_per_mm, $info->{dist_E} / $info->{dist_XY};
-            }
-        }
-    });
-    my $E_per_mm_avg = sum(@E_per_mm) / @E_per_mm;
-    # allow some tolerance because solid rectilinear infill might be adjusted/stretched
-    ok !(defined first { abs($_ - $E_per_mm_avg) > 0.015 } @E_per_mm),
-        'first_layer_extrusion_width applies to everything on first layer';
-}
-
-{
-    my $config = Slic3r::Config::new_from_defaults;
-    $config->set('bridge_speed', 99);
-    $config->set('bridge_flow_ratio', 1);
-    $config->set('cooling', [ 0 ]);             # to prevent speeds from being altered
-    $config->set('first_layer_speed', '100%');  # to prevent speeds from being altered
-    
-    my $test = sub {
-        my $print = Slic3r::Test::init_print('overhang', config => $config);
-        my @E_per_mm = ();
-        Slic3r::GCode::Reader->new->parse(my $gcode = Slic3r::Test::gcode($print), sub {
-            my ($self, $cmd, $args, $info) = @_;
-        
-            if ($info->{extruding} && $info->{dist_XY} > 0) {
-                if (($args->{F} // $self->F) == $config->bridge_speed*60) {
-                    push @E_per_mm, $info->{dist_E} / $info->{dist_XY};
-                }
-            }
-        });
-        my $expected_mm3_per_mm = ($config->nozzle_diameter->[0]**2) * PI/4 * $config->bridge_flow_ratio;
-        my $expected_E_per_mm = $expected_mm3_per_mm / ((($config->filament_diameter->[0]/2)**2)*PI);
-        ok !(defined first { abs($_ - $expected_E_per_mm) > 0.01 } @E_per_mm),
-            'expected flow when using bridge_flow_ratio = ' . $config->bridge_flow_ratio;
-    };
-    
-    $config->set('bridge_flow_ratio', 0.5);
-    $test->();
-    $config->set('bridge_flow_ratio', 2);
-    $test->();
-    $config->set('extrusion_width', 0.4);
-    $config->set('bridge_flow_ratio', 1);
-    $test->();
-    $config->set('bridge_flow_ratio', 0.5);
-    $test->();
-    $config->set('bridge_flow_ratio', 2);
-    $test->();
-}
-
-__END__
diff --git a/t/gaps.t b/t/gaps.t
deleted file mode 100644
index 2594ac087..000000000
--- a/t/gaps.t
+++ /dev/null
@@ -1,59 +0,0 @@
-use Test::More tests => 1;
-use strict;
-use warnings;
-
-BEGIN {
-    use FindBin;
-    use lib "$FindBin::Bin/../lib";
-    use local::lib "$FindBin::Bin/../local-lib";
-}
-
-use List::Util qw(first);
-use Slic3r;
-use Slic3r::Geometry qw(PI scale unscale convex_hull);
-use Slic3r::Surface ':types';
-use Slic3r::Test;
-
-{
-    my $config = Slic3r::Config::new_from_defaults;
-    $config->set('skirts', 0);
-    $config->set('perimeter_speed', 66);
-    $config->set('external_perimeter_speed', 66);
-    $config->set('small_perimeter_speed', 66);
-    $config->set('gap_fill_speed', 99);
-    $config->set('perimeters', 1);
-    $config->set('cooling', [ 0 ]);                 # to prevent speeds from being altered
-    $config->set('first_layer_speed', '100%');      # to prevent speeds from being altered
-    $config->set('perimeter_extrusion_width', 0.35);
-    $config->set('first_layer_extrusion_width', 0.35);
-    
-    my $print = Slic3r::Test::init_print('two_hollow_squares', config => $config);
-    my @perimeter_points = ();
-    my $last = '';  # perimeter | gap
-    my $gap_fills_outside_last_perimeters = 0;
-    Slic3r::GCode::Reader->new->parse(Slic3r::Test::gcode($print), sub {
-        my ($self, $cmd, $args, $info) = @_;
-        
-        if ($info->{extruding} && $info->{dist_XY} > 0) {
-            my $F = $args->{F} // $self->F;
-            my $point = Slic3r::Point->new_scale($info->{new_X}, $info->{new_Y});
-            if ($F == $config->perimeter_speed*60) {
-                if ($last eq 'gap') {
-                    @perimeter_points = ();
-                }
-                push @perimeter_points, $point;
-                $last = 'perimeter';
-            } elsif ($F == $config->gap_fill_speed*60) {
-                my $convex_hull = convex_hull(\@perimeter_points);
-                if (!$convex_hull->contains_point($point)) {
-                    $gap_fills_outside_last_perimeters++;
-                }
-                
-                $last = 'gap';
-            }
-        }
-    });
-    is $gap_fills_outside_last_perimeters, 0, 'gap fills are printed before leaving islands';
-}
-
-__END__
diff --git a/t/gcode.t b/t/gcode.t
index b95505e43..902c40b83 100644
--- a/t/gcode.t
+++ b/t/gcode.t
@@ -1,4 +1,4 @@
-use Test::More tests => 24;
+use Test::More tests => 23;
 use strict;
 use warnings;
 
@@ -13,13 +13,6 @@ use Slic3r;
 use Slic3r::Geometry qw(scale convex_hull);
 use Slic3r::Test;
 
-{
-    my $gcodegen = Slic3r::GCode->new();
-    $gcodegen->set_layer_count(1);
-    $gcodegen->set_origin(Slic3r::Pointf->new(10, 10));
-    is_deeply $gcodegen->last_pos->arrayref, [scale -10, scale -10], 'last_pos is shifted correctly';
-}
-
 {
     my $config = Slic3r::Config::new_from_defaults;
     $config->set('wipe', [1]);
diff --git a/t/loops.t b/t/loops.t
deleted file mode 100644
index e662469ca..000000000
--- a/t/loops.t
+++ /dev/null
@@ -1,57 +0,0 @@
-use Test::More;
-use strict;
-use warnings;
-
-plan skip_all => 'temporarily disabled';
-plan tests => 4;
-
-BEGIN {
-    use FindBin;
-    use lib "$FindBin::Bin/../lib";
-    use local::lib "$FindBin::Bin/../local-lib";
-}
-
-use Slic3r;
-use Slic3r::Test;
-
-{
-    # We only need to slice at one height, so we'll build a non-manifold mesh
-    # that still produces complete loops at that height. Triangular walls are 
-    # enough for this purpose.
-    # Basically we want to check what happens when three concentric loops happen
-    # to be at the same height, the two external ones being ccw and the other being
-    # a hole, thus cw.
-    my (@vertices, @facets) = ();
-    Slic3r::Test::add_facet($_, \@vertices, \@facets) for
-        # external surface below the slicing Z
-        [ [0,0,0],   [20,0,10],   [0,0,10]    ],
-        [ [20,0,0],  [20,20,10],  [20,0,10]   ],
-        [ [20,20,0], [0,20,10],   [20,20,10]  ],
-        [ [0,20,0],  [0,0,10],    [0,20,10]   ],
-        
-        # external insetted surface above the slicing Z
-        [ [2,2,10],   [18,2,10],  [2,2,20]    ],
-        [ [18,2,10],  [18,18,10], [18,2,20]   ],
-        [ [18,18,10], [2,18,10],  [18,18,20]  ],
-        [ [2,18,10],  [2,2,10],   [2,18,20]   ],
-        
-        # insetted hole below the slicing Z
-        [ [15,5,0],   [5,5,10],   [15,5,10]   ],
-        [ [15,15,0],  [15,5,10],  [15,15,10]  ],
-        [ [5,15,0],   [15,15,10], [5,15,10]   ],
-        [ [5,5,0],    [5,15,10],  [5,5,10]    ];
-    
-    my $mesh = Slic3r::TriangleMesh->new;
-    $mesh->ReadFromPerl(\@vertices, \@facets);
-    $mesh->analyze;
-    my @lines = map $mesh->intersect_facet($_, 10), 0..$#facets;
-    my $loops = Slic3r::TriangleMesh::make_loops(\@lines);
-    is scalar(@$loops), 3, 'correct number of loops detected';
-    is scalar(grep $_->is_counter_clockwise, @$loops), 2, 'correct number of ccw loops detected';
-    
-    my @surfaces = Slic3r::Layer::Region::_merge_loops($loops, 0);
-    is scalar(@surfaces), 1, 'one surface detected';
-    is scalar(@{$surfaces[0]->expolygon})-1, 1, 'surface has one hole';
-}
-
-__END__
diff --git a/t/thin.t b/t/thin.t
deleted file mode 100644
index 50e7abc95..000000000
--- a/t/thin.t
+++ /dev/null
@@ -1,185 +0,0 @@
-use Test::More tests => 23;
-use strict;
-use warnings;
-
-BEGIN {
-    use FindBin;
-    use lib "$FindBin::Bin/../lib";
-    use local::lib "$FindBin::Bin/../local-lib";
-}
-
-use Slic3r;
-use List::Util qw(first sum none);
-use Slic3r::Geometry qw(epsilon scale unscale scaled_epsilon Y);
-use Slic3r::Test;
-
-# Disable this until a more robust implementation is provided. It currently
-# fails on Linux 32bit because some spurious extrudates are generated.
-if (0) {
-    my $config = Slic3r::Config::new_from_defaults;
-    $config->set('layer_height', 0.2);
-    $config->set('first_layer_height', $config->layer_height);
-    $config->set('extrusion_width', 0.5);
-    $config->set('first_layer_extrusion_width', '200%'); # check this one too
-    $config->set('skirts', 0);
-    $config->set('thin_walls', 1);
-    
-    my $print = Slic3r::Test::init_print('gt2_teeth', config => $config);
-    
-    my %extrusion_paths = ();  # Z => count of continuous extrusions
-    my $extruding = 0;
-    Slic3r::GCode::Reader->new->parse(Slic3r::Test::gcode($print), sub {
-        my ($self, $cmd, $args, $info) = @_;
-        
-        if ($cmd eq 'G1') {
-            if ($info->{extruding} && $info->{dist_XY}) {
-                if (!$extruding) {
-                    $extrusion_paths{$self->Z} //= 0;
-                    $extrusion_paths{$self->Z}++;
-                }
-                $extruding = 1;
-            } else {
-                $extruding = 0;
-            }
-        }
-    });
-    
-    ok !(first { $_ != 3 } values %extrusion_paths),
-        'no superfluous thin walls are generated for toothed profile';
-}
-
-{
-    my $square = Slic3r::Polygon->new_scale(  # ccw
-        [100, 100],
-        [200, 100],
-        [200, 200],
-        [100, 200],
-    );
-    my $hole_in_square = Slic3r::Polygon->new_scale(  # cw
-        [140, 140],
-        [140, 160],
-        [160, 160],
-        [160, 140],
-    );
-    my $expolygon = Slic3r::ExPolygon->new($square, $hole_in_square);
-    my $res = $expolygon->medial_axis(scale 40, scale 0.5);
-    is scalar(@$res), 1, 'medial axis of a square shape is a single path';
-    isa_ok $res->[0], 'Slic3r::Polyline', 'medial axis result is a polyline';
-    ok $res->[0]->first_point->coincides_with($res->[0]->last_point), 'polyline forms a closed loop';
-    ok $res->[0]->length > $hole_in_square->length && $res->[0]->length < $square->length,
-        'medial axis loop has reasonable length';
-}
-
-{
-    my $expolygon = Slic3r::ExPolygon->new(Slic3r::Polygon->new_scale(
-        [100, 100],
-        [120, 100],
-        [120, 200],
-        [100, 200],
-    ));
-    my $res = $expolygon->medial_axis(scale 20, scale 0.5);
-    is scalar(@$res), 1, 'medial axis of a narrow rectangle is a single line';
-    ok unscale($res->[0]->length) >= (200-100 - (120-100)) - epsilon, 'medial axis has reasonable length';
-    
-    $expolygon = Slic3r::ExPolygon->new(Slic3r::Polygon->new_scale(
-        [100, 100],
-        [120, 100],
-        [120, 200],
-        [105, 200],  # extra point in the short side
-        [100, 200],
-    ));
-    my $res2 = $expolygon->medial_axis(scale 1, scale 0.5);
-    is scalar(@$res), 1, 'medial axis of a narrow rectangle with an extra vertex is still a single line';
-    ok unscale($res->[0]->length) >= (200-100 - (120-100)) - epsilon, 'medial axis has still a reasonable length';
-    ok !(grep { abs($_ - scale 150) < scaled_epsilon } map $_->[Y], map @$_, @$res2), "extra vertices don't influence medial axis";
-}
-
-{
-    my $expolygon = Slic3r::ExPolygon->new(
-        Slic3r::Polygon->new([1185881,829367],[1421988,1578184],[1722442,2303558],[2084981,2999998],[2506843,3662186],[2984809,4285086],[3515250,4863959],[4094122,5394400],[4717018,5872368],[5379210,6294226],[6075653,6656769],[6801033,6957229],[7549842,7193328],[8316383,7363266],[9094809,7465751],[9879211,7500000],[10663611,7465750],[11442038,7363265],[12208580,7193327],[12957389,6957228],[13682769,6656768],[14379209,6294227],[15041405,5872366],[15664297,5394401],[16243171,4863960],[16758641,4301424],[17251579,3662185],[17673439,3000000],[18035980,2303556],[18336441,1578177],[18572539,829368],[18750748,0],[19758422,0],[19727293,236479],[19538467,1088188],[19276136,1920196],[18942292,2726179],[18539460,3499999],[18070731,4235755],[17539650,4927877],[16950279,5571067],[16307090,6160437],[15614974,6691519],[14879209,7160248],[14105392,7563079],[13299407,7896927],[12467399,8159255],[11615691,8348082],[10750769,8461952],[9879211,8500000],[9007652,8461952],[8142729,8348082],[7291022,8159255],[6459015,7896927],[5653029,7563079],[4879210,7160247],[4143447,6691519],[3451331,6160437],[2808141,5571066],[2218773,4927878],[1687689,4235755],[1218962,3499999],[827499,2748020],[482284,1920196],[219954,1088186],[31126,236479],[0,0],[1005754,0]),
-    );
-    my $res = $expolygon->medial_axis(scale 1.324888, scale 0.25);
-    is scalar(@$res), 1, 'medial axis of a semicircumference is a single line';
-    
-    # check whether turns are all CCW or all CW
-    my @lines = @{$res->[0]->lines};
-    my @angles = map { $lines[$_-1]->ccw($lines[$_]->b) } 1..$#lines;
-    ok !!(none { $_ < 0 } @angles) || (none { $_ > 0 } @angles),
-        'all medial axis segments of a semicircumference have the same orientation';
-}
-
-{
-    my $expolygon = Slic3r::ExPolygon->new(Slic3r::Polygon->new_scale(
-        [100, 100],
-        [120, 100],
-        [112, 200],
-        [108, 200],
-    ));
-    my $res = $expolygon->medial_axis(scale 20, scale 0.5);
-    is scalar(@$res), 1, 'medial axis of a narrow trapezoid is a single line';
-    ok unscale($res->[0]->length) >= (200-100 - (120-100)) - epsilon, 'medial axis has reasonable length';
-}
-
-{
-    my $expolygon = Slic3r::ExPolygon->new(Slic3r::Polygon->new_scale(
-        [100, 100],
-        [120, 100],
-        [120, 180],
-        [200, 180],
-        [200, 200],
-        [100, 200],
-    ));
-    my $res = $expolygon->medial_axis(scale 20, scale 0.5);
-    is scalar(@$res), 1, 'medial axis of a L shape is a single polyline';
-    my $len = unscale($res->[0]->length) + 20;  # 20 is the thickness of the expolygon, which is subtracted from the ends
-    ok $len > 80*2 && $len < 100*2, 'medial axis has reasonable length';
-}
-
-{
-    my $expolygon = Slic3r::ExPolygon->new(Slic3r::Polygon->new(
-        [-203064906,-51459966],[-219312231,-51459966],[-219335477,-51459962],[-219376095,-51459962],[-219412047,-51459966],
-        [-219572094,-51459966],[-219624814,-51459962],[-219642183,-51459962],[-219656665,-51459966],[-220815482,-51459966],
-        [-220815482,-37738966],[-221117540,-37738966],[-221117540,-51762024],[-203064906,-51762024],
-    ));
-    my $polylines = $expolygon->medial_axis(819998, 102499.75);
-    
-    my $perimeter = $expolygon->contour->split_at_first_point->length;
-    ok sum(map $_->length, @$polylines) > $perimeter/2/4*3, 'medial axis has a reasonable length';
-}
-
-{
-    my $expolygon = Slic3r::ExPolygon->new(Slic3r::Polygon->new_scale(
-        [50, 100],
-        [1000, 102],
-        [50, 104],
-    ));
-    my $res = $expolygon->medial_axis(scale 4, scale 0.5);
-    is scalar(@$res), 1, 'medial axis of a narrow triangle is a single line';
-    ok unscale($res->[0]->length) >= (200-100 - (120-100)) - epsilon, 'medial axis has reasonable length';
-}
-
-{
-    # GH #2474
-    my $expolygon = Slic3r::ExPolygon->new(Slic3r::Polygon->new(
-        [91294454,31032190],[11294481,31032190],[11294481,29967810],[44969182,29967810],[89909960,29967808],[91294454,29967808]
-    ));
-    my $polylines = $expolygon->medial_axis(1871238, 500000);
-    is scalar(@$polylines), 1, 'medial axis is a single polyline';
-    my $polyline = $polylines->[0];
-    
-    my $expected_y = $expolygon->bounding_box->center->y; #;;
-    ok abs(sum(map $_->y, @$polyline) / @$polyline - $expected_y) < scaled_epsilon, #,,
-        'medial axis is horizontal and is centered';
-    
-    # order polyline from left to right
-    $polyline->reverse if $polyline->first_point->x > $polyline->last_point->x;
-    
-    my $polyline_bb = $polyline->bounding_box;
-    is $polyline->first_point->x, $polyline_bb->x_min, 'expected x_min';
-    is $polyline->last_point->x,  $polyline_bb->x_max, 'expected x_max';
-    
-    is_deeply [ map $_->x, @$polyline ], [ sort map $_->x, @$polyline ],
-        'medial axis is not self-overlapping';
-}
-
-__END__
diff --git a/tests/fff_print/CMakeLists.txt b/tests/fff_print/CMakeLists.txt
index 9e039c913..4e8821287 100644
--- a/tests/fff_print/CMakeLists.txt
+++ b/tests/fff_print/CMakeLists.txt
@@ -3,11 +3,14 @@ add_executable(${_TEST_NAME}_tests
 	${_TEST_NAME}_tests.cpp
 	test_avoid_crossing_perimeters.cpp
 	test_bridges.cpp
+	test_cooling.cpp
+	test_custom_gcode.cpp
 	test_data.cpp
 	test_data.hpp
 	test_extrusion_entity.cpp
 	test_fill.cpp
 	test_flow.cpp
+	test_gaps.cpp
 	test_gcode.cpp
 	test_gcodefindreplace.cpp
 	test_gcodewriter.cpp
@@ -19,6 +22,7 @@ add_executable(${_TEST_NAME}_tests
 	test_printobject.cpp
 	test_skirt_brim.cpp
 	test_support_material.cpp
+	test_thin_walls.cpp
 	test_trianglemesh.cpp
 	)
 target_link_libraries(${_TEST_NAME}_tests test_common libslic3r)
diff --git a/tests/fff_print/test_cooling.cpp b/tests/fff_print/test_cooling.cpp
new file mode 100644
index 000000000..f743783b1
--- /dev/null
+++ b/tests/fff_print/test_cooling.cpp
@@ -0,0 +1,274 @@
+#include <catch2/catch.hpp>
+
+#include <numeric>
+#include <sstream>
+
+#include "test_data.hpp" // get access to init_print, etc
+
+#include "libslic3r/Config.hpp"
+#include "libslic3r/GCode.hpp"
+#include "libslic3r/GCodeReader.hpp"
+#include "libslic3r/GCode/CoolingBuffer.hpp"
+#include "libslic3r/libslic3r.h"
+
+using namespace Slic3r;
+
+std::unique_ptr<CoolingBuffer> make_cooling_buffer(
+    GCode                           &gcode,
+    const DynamicPrintConfig        &config         = DynamicPrintConfig{}, 
+    const std::vector<unsigned int> &extruder_ids   = { 0 })
+{
+    PrintConfig print_config;
+    print_config.apply(config, true); // ignore_nonexistent
+    gcode.apply_print_config(print_config);
+    gcode.set_layer_count(10);
+    gcode.writer().set_extruders(extruder_ids);
+    gcode.writer().set_extruder(0);
+    return std::make_unique<CoolingBuffer>(gcode);
+}
+
+SCENARIO("Cooling unit tests", "[Cooling]") {
+    const std::string   gcode1        = "G1 X100 E1 F3000\n";
+    // 2 sec
+    const double        print_time1   = 100. / (3000. / 60.);
+    const std::string   gcode2        = gcode1 + "G1 X0 E1 F3000\n";
+    // 4 sec
+    const double        print_time2   = 2. * print_time1;
+
+    auto config = DynamicPrintConfig::full_print_config_with({
+        // Default cooling settings.
+        { "bridge_fan_speed",            "100" },
+        { "cooling",                     "1" },
+        { "fan_always_on",               "0" },
+        { "fan_below_layer_time",        "60" },
+        { "max_fan_speed",               "100" },
+        { "min_print_speed",             "10" },
+        { "slowdown_below_layer_time",   "5" },
+        // Default print speeds.
+        { "bridge_speed",                60 },
+        { "external_perimeter_speed",    "50%" },
+        { "first_layer_speed",           30 },
+        { "gap_fill_speed",              20 },
+        { "infill_speed",                80 },
+        { "perimeter_speed",             60 },
+        { "small_perimeter_speed",       15 },
+        { "solid_infill_speed",          20 },
+        { "top_solid_infill_speed",      15 },
+        { "max_print_speed",             80 },
+        // Override for tests.
+        { "disable_fan_first_layers",    "0" }
+    });
+
+    WHEN("G-code block 3") {
+        THEN("speed is not altered when elapsed time is greater than slowdown threshold") {
+            // Print time of gcode.
+            const double print_time = 100. / (3000. / 60.);
+            //FIXME slowdown_below_layer_time is rounded down significantly from 1.8s to 1s.
+            config.set_deserialize_strict({ { "slowdown_below_layer_time", { int(print_time * 0.999) } } });
+            GCode gcodegen;
+            auto buffer = make_cooling_buffer(gcodegen, config);
+            std::string gcode = buffer->process_layer("G1 F3000;_EXTRUDE_SET_SPEED\nG1 X100 E1", 0, true);
+            bool speed_not_altered = gcode.find("F3000") != gcode.npos;
+            REQUIRE(speed_not_altered);
+        }
+    }
+
+    WHEN("G-code block 4") {
+        const std::string gcode_src = 
+            "G1 X50 F2500\n"
+            "G1 F3000;_EXTRUDE_SET_SPEED\n"
+            "G1 X100 E1\n"
+            ";_EXTRUDE_END\n"
+            "G1 E4 F400";
+        // Print time of gcode.
+        const double print_time = 50. / (2500. / 60.) + 100. / (3000. / 60.) + 4. / (400. / 60.);
+        config.set_deserialize_strict({ { "slowdown_below_layer_time", { int(print_time * 1.001) } } });
+        GCode gcodegen;
+        auto buffer = make_cooling_buffer(gcodegen, config);
+        std::string gcode = buffer->process_layer(gcode_src, 0, true);
+        THEN("speed is altered when elapsed time is lower than slowdown threshold") {
+            bool speed_is_altered = gcode.find("F3000") == gcode.npos;
+            REQUIRE(speed_is_altered);
+        }
+        THEN("speed is not altered for travel moves") {
+            bool speed_not_altered = gcode.find("F2500") != gcode.npos;
+            REQUIRE(speed_not_altered);
+        }
+        THEN("speed is not altered for extruder-only moves") {
+            bool speed_not_altered = gcode.find("F400") != gcode.npos;
+            REQUIRE(speed_not_altered);   
+        }
+    }
+
+    WHEN("G-code block 1") {
+        THEN("fan is not activated when elapsed time is greater than fan threshold") {
+            config.set_deserialize_strict({
+                { "fan_below_layer_time"      , int(print_time1 * 0.88) },
+                { "slowdown_below_layer_time" , int(print_time1 * 0.99) }
+            });
+            GCode gcodegen;
+            auto buffer = make_cooling_buffer(gcodegen, config);
+            std::string gcode = buffer->process_layer(gcode1, 0, true);
+            bool fan_not_activated = gcode.find("M106") == gcode.npos;
+            REQUIRE(fan_not_activated);      
+        }
+    }
+    WHEN("G-code block 1 with two extruders") {
+        config.set_deserialize_strict({
+            { "cooling",                   "1, 0" },
+            { "fan_below_layer_time",      { int(print_time2 + 1.), int(print_time2 + 1.) } },
+            { "slowdown_below_layer_time", { int(print_time2 + 2.), int(print_time2 + 2.) } }
+        });
+        GCode gcodegen;
+        auto buffer = make_cooling_buffer(gcodegen, config, { 0, 1 });
+        std::string gcode = buffer->process_layer(gcode1 + "T1\nG1 X0 E1 F3000\n", 0, true);
+        THEN("fan is activated for the 1st tool") {
+            bool ok = gcode.find("M106") == 0;
+            REQUIRE(ok);      
+        }
+        THEN("fan is disabled for the 2nd tool") {
+            bool ok = gcode.find("\nM107") > 0;
+            REQUIRE(ok);      
+        }
+    }
+    WHEN("G-code block 2") {
+        THEN("slowdown is computed on all objects printing at the same Z") {
+            config.set_deserialize_strict({ { "slowdown_below_layer_time", int(print_time2 * 0.99) } });
+            GCode gcodegen;
+            auto buffer = make_cooling_buffer(gcodegen, config);
+            std::string gcode = buffer->process_layer(gcode2, 0, true);
+            bool ok = gcode.find("F3000") != gcode.npos;
+            REQUIRE(ok);      
+        }
+        THEN("fan is not activated on all objects printing at different Z") {
+            config.set_deserialize_strict({ 
+                { "fan_below_layer_time",      int(print_time2 * 0.65) },
+                { "slowdown_below_layer_time", int(print_time2 * 0.7) }
+            });
+            GCode gcodegen;
+            auto buffer = make_cooling_buffer(gcodegen, config);
+            // use an elapsed time which is < the threshold but greater than it when summed twice
+            std::string gcode = buffer->process_layer(gcode2, 0, true) + buffer->process_layer(gcode2, 1, true);
+            bool fan_not_activated = gcode.find("M106") == gcode.npos;
+            REQUIRE(fan_not_activated);      
+        }
+        THEN("fan is activated on all objects printing at different Z") {
+            // use an elapsed time which is < the threshold even when summed twice
+            config.set_deserialize_strict({ 
+                { "fan_below_layer_time",      int(print_time2 + 1) },
+                { "slowdown_below_layer_time", int(print_time2 + 1) }
+            });
+            GCode gcodegen;
+            auto buffer = make_cooling_buffer(gcodegen, config);
+            // use an elapsed time which is < the threshold but greater than it when summed twice
+            std::string gcode = buffer->process_layer(gcode2, 0, true) + buffer->process_layer(gcode2, 1, true);
+            bool fan_activated = gcode.find("M106") != gcode.npos;
+            REQUIRE(fan_activated);      
+        }
+    }
+}
+
+SCENARIO("Cooling integration tests", "[Cooling]") {
+    GIVEN("overhang") {
+        auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
+            { "cooling",                    { 1 } },
+            { "bridge_fan_speed",           { 100 } },
+            { "fan_below_layer_time",       { 0 } },
+            { "slowdown_below_layer_time",  { 0 } },
+            { "bridge_speed",               99 },
+            // internal bridges use solid_infil speed
+            { "bottom_solid_layers",        1 },
+            // internal bridges use solid_infil speed
+        });
+    
+        GCodeReader parser;
+        int fan = 0;
+        int fan_with_incorrect_speeds = 0;
+        int fan_with_incorrect_print_speeds = 0;
+        int bridge_with_no_fan = 0;
+        const double bridge_speed = config.opt_float("bridge_speed") * 60;
+        parser.parse_buffer(
+            Slic3r::Test::slice({ Slic3r::Test::TestMesh::overhang }, config),
+            [&fan, &fan_with_incorrect_speeds, &fan_with_incorrect_print_speeds, &bridge_with_no_fan, bridge_speed]
+                (Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line)
+        {
+            if (line.cmd_is("M106")) {
+                line.has_value('S', fan);
+                if (fan != 255)
+                    ++ fan_with_incorrect_speeds;
+            } else if (line.cmd_is("M107")) {
+                fan = 0;
+            } else if (line.extruding(self) && line.dist_XY(self) > 0) {
+                if (is_approx<double>(line.new_F(self), bridge_speed)) {
+                    if (fan != 255)
+                        ++ bridge_with_no_fan;
+                } else {
+                    if (fan != 0)
+                        ++ fan_with_incorrect_print_speeds;
+                }
+            }
+        });
+        THEN("bridge fan speed is applied correctly") {
+            REQUIRE(fan_with_incorrect_speeds == 0);
+        }
+        THEN("bridge fan is only turned on for bridges") {
+            REQUIRE(fan_with_incorrect_print_speeds == 0);
+        }
+        THEN("bridge fan is turned on for all bridges") {
+            REQUIRE(bridge_with_no_fan == 0);
+        }
+    }
+    GIVEN("20mm cube") {
+        auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
+            { "cooling",                    { 1 } },
+            { "fan_below_layer_time",       { 0 } },
+            { "slowdown_below_layer_time",  { 10 } },
+            { "min_print_speed",            { 0 } },
+            { "start_gcode",                "" },
+            { "first_layer_speed",          "100%" },
+            { "external_perimeter_speed",   99 }
+        });
+        GCodeReader parser;
+        const double external_perimeter_speed = config.opt<ConfigOptionFloatOrPercent>("external_perimeter_speed")->value * 60;
+        std::vector<double> layer_times;
+        // z => 1
+        std::map<coord_t, int> layer_external;
+        parser.parse_buffer(
+            Slic3r::Test::slice({ Slic3r::Test::TestMesh::cube_20x20x20 }, config),
+            [&layer_times, &layer_external, external_perimeter_speed]
+                (Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line)
+        {
+            if (line.cmd_is("G1")) {
+                if (line.dist_Z(self) != 0) {
+                    layer_times.emplace_back(0.);
+                    layer_external[scaled<coord_t>(line.new_Z(self))] = 0;
+                }
+                double l = line.dist_XY(self);
+                if (l == 0)
+                    l = line.dist_E(self);
+                if (l == 0)
+                    l = line.dist_Z(self);
+                if (l > 0.) {
+                    if (layer_times.empty())
+                        layer_times.emplace_back(0.);
+                    layer_times.back() += 60. * std::abs(l) / line.new_F(self);
+                }
+                if (line.has('F') && line.f() == external_perimeter_speed)
+                    ++ layer_external[scaled<coord_t>(self.z())];
+            }
+        });            
+        THEN("slowdown_below_layer_time is honored") {
+            // Account for some inaccuracies.
+            const double slowdown_below_layer_time = config.opt<ConfigOptionInts>("slowdown_below_layer_time")->values.front() - 0.2;
+            size_t minimum_time_honored = std::count_if(layer_times.begin(), layer_times.end(), 
+                [slowdown_below_layer_time](double t){ return t > slowdown_below_layer_time; });
+            REQUIRE(minimum_time_honored == layer_times.size());
+        }
+        THEN("slowdown_below_layer_time does not alter external perimeters") {
+            // Broken by Vojtech
+            // check that all layers have at least one unaltered external perimeter speed
+            // my $external = all { $_ > 0 } values %layer_external;
+            // ok $external, '';
+        }
+    }
+}
diff --git a/tests/fff_print/test_custom_gcode.cpp b/tests/fff_print/test_custom_gcode.cpp
new file mode 100644
index 000000000..0368b9604
--- /dev/null
+++ b/tests/fff_print/test_custom_gcode.cpp
@@ -0,0 +1,220 @@
+#include <catch2/catch.hpp>
+
+#include <numeric>
+#include <sstream>
+
+#include "libslic3r/Config.hpp"
+#include "libslic3r/Print.hpp"
+#include "libslic3r/PrintConfig.hpp"
+#include "libslic3r/libslic3r.h"
+
+#include "test_data.hpp"
+
+using namespace Slic3r;
+
+#if 0
+SCENARIO("Output file format", "[CustomGCode]")
+{
+    WHEN("output_file_format set") {
+        auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
+            { "output_filename_format",         "ts_[travel_speed]_lh_[layer_height].gcode" },
+            { "start_gcode",                    "TRAVEL:[travel_speed] HEIGHT:[layer_height]\n" }
+        });
+
+        Print print;
+        Model model;
+        Test::init_print({ Test::TestMesh::cube_2x20x10 }, print, model, config);
+    
+        std::string output_file = print.output_filepath();
+        THEN("print config options are replaced in output filename") {
+            output_file.find(std::string("ts_") + )
+        }
+        my ($t, $h) = map $config->$_, qw(travel_speed layer_height);
+        ok $output_file =~ /ts_${t}_/, '';
+        ok $output_file =~ /lh_$h\./, 'region config options are replaced in output filename';
+    
+        std::string gcode = print.gcode(print);
+        THEN("print config options are replaced in custom G-code") {
+            ok $gcode =~ /TRAVEL:$t/, '';    
+        }
+        THEN("region config options are replaced in custom G-code") {
+            ok $gcode =~ /HEIGHT:$h/, '';
+        }
+    }
+}
+
+SCENARIO("Custom G-code", "[CustomGCode]")
+{
+    WHEN("start_gcode and layer_gcode set") {
+        auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
+            { "start_gcode", "_MY_CUSTOM_START_GCODE_" },  // to avoid dealing with the nozzle lift in start G-code
+            { "layer_gcode", "_MY_CUSTOM_LAYER_GCODE_" }
+        });
+        GCodeReader parser;
+        bool        last_move_was_z_change = false;
+        int         num_layer_changes_not_applied = 0;
+        parser.parse_buffer(Slic3r::Test::slice({ Test::TestMesh::cube_2x20x10 }, config), 
+            [&last_move_was_z_change, &num_layer_changes_not_applied](Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line)
+        {
+            if (line.extruding(self)) {
+                if (! was_extruding)
+                    seam_points.emplace_back(self.xy_scaled());
+                was_extruding = true;
+            } else if (! line.cmd_is("M73")) {
+                // skips remaining time lines (M73)
+                was_extruding = false;
+            }
+            if (last_move_was_z_change != line.cmd_is("_MY_CUSTOM_LAYER_GCODE_"))
+                ++ num_layer_changes_not_applied;
+            last_move_was_z_change = line.dist_Z(self) > 0;
+        });
+        THEN("custom layer G-code is applied after Z move and before other moves");
+    };
+
+{
+    my $config = Slic3r::Config->new;
+    $config->set('nozzle_diameter', [0.6,0.6,0.6,0.6]);
+    $config->set('extruder', 2);
+    $config->set('first_layer_temperature', [200,205]);
+    
+    {
+        my $print = Slic3r::Test::init_print('20mm_cube', config => $config);
+        my $gcode = Slic3r::Test::gcode($print);
+        ok $gcode =~ /M104 S205 T1/, 'temperature set correctly for non-zero yet single extruder';
+        ok $gcode !~ /M104 S\d+ T0/, 'unused extruder correctly ignored';
+    }
+    
+    $config->set('infill_extruder', 1);
+    {
+        my $print = Slic3r::Test::init_print('20mm_cube', config => $config);
+        my $gcode = Slic3r::Test::gcode($print);
+        ok $gcode =~ /M104 S200 T0/, 'temperature set correctly for first extruder';
+        ok $gcode =~ /M104 S205 T1/, 'temperature set correctly for second extruder';
+    }
+    
+    my @start_gcode = (qq!
+;__temp0:[first_layer_temperature_0]__
+;__temp1:[first_layer_temperature_1]__
+;__temp2:[first_layer_temperature_2]__
+    !, qq!
+;__temp0:{first_layer_temperature[0]}__
+;__temp1:{first_layer_temperature[1]}__
+;__temp2:{first_layer_temperature[2]}__
+    !);
+    my @syntax_description = (' (legacy syntax)', ' (new syntax)');
+    for my $i (0, 1) {
+        $config->set('start_gcode', $start_gcode[$i]);
+        {
+            my $print = Slic3r::Test::init_print('20mm_cube', config => $config);
+            my $gcode = Slic3r::Test::gcode($print);
+            # we use the [infill_extruder] placeholder to make sure this test doesn't
+            # catch a false positive caused by the unparsed start G-code option itself
+            # being embedded in the G-code
+            ok $gcode =~ /temp0:200/, 'temperature placeholder for first extruder correctly populated' . $syntax_description[$i];
+            ok $gcode =~ /temp1:205/, 'temperature placeholder for second extruder correctly populated' . $syntax_description[$i];
+            ok $gcode =~ /temp2:200/, 'temperature placeholder for unused extruder populated with first value' . $syntax_description[$i];
+        }
+    }
+
+    $config->set('start_gcode', qq!
+;substitution:{if infill_extruder==1}extruder1
+         {elsif infill_extruder==2}extruder2
+         {else}extruder3{endif}
+    !);
+    {
+        my $print = Slic3r::Test::init_print('20mm_cube', config => $config);
+        my $gcode = Slic3r::Test::gcode($print);
+        ok $gcode =~ /substitution:extruder1/, 'if / else / endif - first block returned';
+    }
+}
+
+{
+    my $config = Slic3r::Config::new_from_defaults;
+    $config->set('before_layer_gcode', ';BEFORE [layer_num]');
+    $config->set('layer_gcode', ';CHANGE [layer_num]');
+    $config->set('support_material', 1);
+    $config->set('layer_height', 0.2);
+    my $print = Slic3r::Test::init_print('overhang', config => $config);
+    my $gcode = Slic3r::Test::gcode($print);
+    
+    my @before = ();
+    my @change = ();
+    foreach my $line (split /\R+/, $gcode) {
+        if ($line =~ /;BEFORE (\d+)/) {
+            push @before, $1;
+        } elsif ($line =~ /;CHANGE (\d+)/) {
+            push @change, $1;
+            fail 'inconsistent layer_num before and after layer change'
+                if $1 != $before[-1];
+        }
+    }
+    is_deeply \@before, \@change, 'layer_num is consistent before and after layer changes';
+    ok !defined(first { $change[$_] != $change[$_-1]+1 } 1..$#change),
+        'layer_num grows continously';  # i.e. no duplicates or regressions
+}
+
+{
+    my $config = Slic3r::Config->new;
+    $config->set('nozzle_diameter', [0.6,0.6,0.6,0.6,0.6]);
+    $config->set('start_gcode', qq!
+;substitution:{if infill_extruder==1}if block
+         {elsif infill_extruder==2}elsif block 1
+         {elsif infill_extruder==3}elsif block 2
+         {elsif infill_extruder==4}elsif block 3
+         {else}endif block{endif}
+    !);
+    my @returned = ('', 'if block', 'elsif block 1', 'elsif block 2', 'elsif block 3', 'endif block');
+    for my $i (1,2,3,4,5) {
+        $config->set('infill_extruder', $i);
+        my $print = Slic3r::Test::init_print('20mm_cube', config => $config);
+        my $gcode = Slic3r::Test::gcode($print);
+        my $found_other = 0;
+        for my $j (1,2,3,4,5) {
+            next if $i == $j;
+            $found_other = 1 if $gcode =~ /substitution:$returned[$j]/;
+        }
+        ok $gcode =~ /substitution:$returned[$i]/, 'if / else / endif - ' . $returned[$i] . ' returned';
+        ok !$found_other, 'if / else / endif - only ' . $returned[$i] . ' returned';
+    }
+}
+
+{
+    my $config = Slic3r::Config->new;
+    $config->set('nozzle_diameter', [0.6,0.6,0.6,0.6]);
+    $config->set('start_gcode', 
+        ';substitution:{if infill_extruder==1}{if perimeter_extruder==1}block11{else}block12{endif}' .
+        '{elsif infill_extruder==2}{if perimeter_extruder==1}block21{else}block22{endif}' .
+        '{else}{if perimeter_extruder==1}block31{else}block32{endif}{endif}:end');
+    for my $i (1,2,3) {
+        $config->set('infill_extruder', $i);
+        for my $j (1,2) {
+            $config->set('perimeter_extruder', $j);
+            my $print = Slic3r::Test::init_print('20mm_cube', config => $config);
+            my $gcode = Slic3r::Test::gcode($print);
+            ok $gcode =~ /substitution:block$i$j:end/, "two level if / else / endif - block$i$j returned";
+        }
+    }
+}
+
+{
+    my $config = Slic3r::Config->new;
+    $config->set('start_gcode', 
+        ';substitution:{if notes=="MK2"}MK2{elsif notes=="MK3"}MK3{else}MK1{endif}:end');
+    for my $printer_name ("MK2", "MK3", "MK1") {
+        $config->set('notes', $printer_name);
+        my $print = Slic3r::Test::init_print('20mm_cube', config => $config);
+        my $gcode = Slic3r::Test::gcode($print);
+        ok $gcode =~ /substitution:$printer_name:end/, "printer name $printer_name matched";
+    }
+}
+
+{
+    my $config = Slic3r::Config::new_from_defaults;
+    $config->set('complete_objects', 1);
+    $config->set('between_objects_gcode', '_MY_CUSTOM_GCODE_');
+    my $print = Slic3r::Test::init_print('20mm_cube', config => $config, duplicate => 3);
+    my $gcode = Slic3r::Test::gcode($print);
+    is scalar(() = $gcode =~ /^_MY_CUSTOM_GCODE_/gm), 2, 'between_objects_gcode is applied correctly';
+}
+
+#endif
\ No newline at end of file
diff --git a/tests/fff_print/test_data.cpp b/tests/fff_print/test_data.cpp
index 6be45d238..a52583cfc 100644
--- a/tests/fff_print/test_data.cpp
+++ b/tests/fff_print/test_data.cpp
@@ -26,6 +26,7 @@ const std::unordered_map<TestMesh, const char*, TestMeshHash> mesh_names {
     std::pair<TestMesh, const char*>(TestMesh::V,						"V"), 
     std::pair<TestMesh, const char*>(TestMesh::_40x10,					"40x10"), 
     std::pair<TestMesh, const char*>(TestMesh::cube_20x20x20,			"cube_20x20x20"), 
+    std::pair<TestMesh, const char*>(TestMesh::cube_2x20x10,            "cube_2x20x10"), 
     std::pair<TestMesh, const char*>(TestMesh::sphere_50mm,				"sphere_50mm"), 
     std::pair<TestMesh, const char*>(TestMesh::bridge,					"bridge"), 
     std::pair<TestMesh, const char*>(TestMesh::bridge_with_hole,		"bridge_with_hole"), 
@@ -49,6 +50,9 @@ TriangleMesh mesh(TestMesh m)
         case TestMesh::cube_20x20x20:
             mesh = Slic3r::make_cube(20, 20, 20);
             break;
+        case TestMesh::cube_2x20x10:
+            mesh = Slic3r::make_cube(2, 20, 10);
+            break;
         case TestMesh::sphere_50mm:
             mesh = Slic3r::make_sphere(50, PI / 243.0);
             break;
diff --git a/tests/fff_print/test_data.hpp b/tests/fff_print/test_data.hpp
index 573ae58a5..b699e5e4e 100644
--- a/tests/fff_print/test_data.hpp
+++ b/tests/fff_print/test_data.hpp
@@ -21,6 +21,7 @@ enum class TestMesh {
     V,
     _40x10,
     cube_20x20x20,
+    cube_2x20x10,
     sphere_50mm,
     bridge,
     bridge_with_hole,
diff --git a/tests/fff_print/test_fill.cpp b/tests/fff_print/test_fill.cpp
index c3af6e8fc..0ccb27d9a 100644
--- a/tests/fff_print/test_fill.cpp
+++ b/tests/fff_print/test_fill.cpp
@@ -197,7 +197,7 @@ TEST_CASE("Fill: Pattern Path Length", "[Fill]") {
 SCENARIO("Infill does not exceed perimeters", "[Fill]") 
 {
     auto test = [](const std::string_view pattern) {
-        DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config_with({
+        auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
             { "nozzle_diameter",        "0.4, 0.4, 0.4, 0.4" },
             { "fill_pattern",           pattern },
             { "top_fill_pattern",       pattern },
diff --git a/tests/fff_print/test_flow.cpp b/tests/fff_print/test_flow.cpp
index 81f748e19..b24b59cc6 100644
--- a/tests/fff_print/test_flow.cpp
+++ b/tests/fff_print/test_flow.cpp
@@ -5,8 +5,6 @@
 
 #include "test_data.hpp" // get access to init_print, etc
 
-#include "libslic3r/Config.hpp"
-#include "libslic3r/Model.hpp"
 #include "libslic3r/Config.hpp"
 #include "libslic3r/GCodeReader.hpp"
 #include "libslic3r/Flow.hpp"
@@ -16,61 +14,118 @@ using namespace Slic3r::Test;
 using namespace Slic3r;
 
 SCENARIO("Extrusion width specifics", "[Flow]") {
-    GIVEN("A config with a skirt, brim, some fill density, 3 perimeters, and 1 bottom solid layer and a 20mm cube mesh") {
-        // this is a sharedptr
-        DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
-		config.set_deserialize_strict({
-			{ "brim_width",			2 },
-			{ "skirts",				1 },
-			{ "perimeters",			3 },
-			{ "fill_density",		"40%" },
-			{ "first_layer_height", 0.3 }
-			});
 
-        WHEN("first layer width set to 2mm") {
-            Slic3r::Model model;
-            config.set("first_layer_extrusion_width", 2);
-            Slic3r::Print print;
-            Slic3r::Test::init_print({TestMesh::cube_20x20x20}, print, model, config);
-
-            std::vector<double> E_per_mm_bottom;
-            std::string gcode = Test::gcode(print);
-            Slic3r::GCodeReader parser;
-            const double layer_height = config.opt_float("layer_height");
-            parser.parse_buffer(gcode, [&E_per_mm_bottom, layer_height] (Slic3r::GCodeReader& self, const Slic3r::GCodeReader::GCodeLine& line)
-            { 
-                if (self.z() == Approx(layer_height).margin(0.01)) { // only consider first layer
-                    if (line.extruding(self) && line.dist_XY(self) > 0) {
-                        E_per_mm_bottom.emplace_back(line.dist_E(self) / line.dist_XY(self));
-                    }
-                }
-            });
-            THEN(" First layer width applies to everything on first layer.") {
-                bool pass = false;
-                double avg_E = std::accumulate(E_per_mm_bottom.cbegin(), E_per_mm_bottom.cend(), 0.0) / static_cast<double>(E_per_mm_bottom.size());
-
-                pass = (std::count_if(E_per_mm_bottom.cbegin(), E_per_mm_bottom.cend(), [avg_E] (const double& v) { return v == Approx(avg_E); }) == 0);
-                REQUIRE(pass == true);
-                REQUIRE(E_per_mm_bottom.size() > 0); // make sure it actually passed because of extrusion
-            }
-            THEN(" First layer width does not apply to upper layer.") {
+    auto test = [](const DynamicPrintConfig &config) {
+        Slic3r::GCodeReader parser;
+        const double        layer_height = config.opt_float("layer_height");
+        std::vector<double> E_per_mm_bottom;
+        parser.parse_buffer(Slic3r::Test::slice({ Slic3r::Test::TestMesh::cube_20x20x20 }, config),
+            [&E_per_mm_bottom, layer_height] (Slic3r::GCodeReader& self, const Slic3r::GCodeReader::GCodeLine& line)
+        { 
+            if (self.z() == Approx(layer_height).margin(0.01)) { // only consider first layer
+                if (line.extruding(self) && line.dist_XY(self) > 0)
+                    E_per_mm_bottom.emplace_back(line.dist_E(self) / line.dist_XY(self));
             }
+        });
+        THEN("First layer width applies to everything on first layer.") {
+            REQUIRE(E_per_mm_bottom.size() > 0);
+            const double E_per_mm_avg = std::accumulate(E_per_mm_bottom.cbegin(), E_per_mm_bottom.cend(), 0.0) / static_cast<double>(E_per_mm_bottom.size());
+            bool pass = (std::count_if(E_per_mm_bottom.cbegin(), E_per_mm_bottom.cend(), [E_per_mm_avg] (const double& v) { return v == Approx(E_per_mm_avg); }) == 0);
+            REQUIRE(pass);
+        }
+        THEN("First layer width does not apply to upper layer.") {
+        }
+    };
+    GIVEN("A config with a skirt, brim, some fill density, 3 perimeters, and 1 bottom solid layer") {
+        auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
+            { "skirts",                         1 },
+            { "brim_width",                     2 },
+            { "perimeters",                     3 },
+            { "fill_density",                   "40%" },
+            { "first_layer_height",             0.3 },
+            { "first_layer_extrusion_width",    "2" },
+        });
+        WHEN("Slicing a 20mm cube") {
+            test(config);
+        }
+    }
+    GIVEN("A config with more options and a 20mm cube ") {
+        auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
+            { "skirts",                         1 },
+            { "brim_width",                     2 },
+            { "perimeters",                     3 },
+            { "fill_density",                   "40%" },
+            { "layer_height",                   "0.35" },
+            { "first_layer_height",             "0.35" },
+            { "bottom_solid_layers",            1 },
+            { "first_layer_extrusion_width",    "2" },
+            { "filament_diameter",              "3" },
+            { "nozzle_diameter",                "0.5" }
+        });
+        WHEN("Slicing a 20mm cube") {
+            test(config);            
         }
     }
 }
-// needs gcode export
+
 SCENARIO(" Bridge flow specifics.", "[Flow]") {
+    auto config = DynamicPrintConfig::full_print_config_with({
+        { "bridge_speed",           99 },
+        { "bridge_flow_ratio",      1 },
+        // to prevent speeds from being altered
+        { "cooling",                "0" },
+        // to prevent speeds from being altered
+        { "first_layer_speed",      "100%" }
+    });
+
+    auto test = [](const DynamicPrintConfig &config) {
+        GCodeReader         parser;
+        const double        bridge_speed = config.opt_float("bridge_speed") * 60.;
+        std::vector<double> E_per_mm;
+        parser.parse_buffer(Slic3r::Test::slice({ Slic3r::Test::TestMesh::overhang }, config), 
+            [&E_per_mm, bridge_speed](Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line) {
+            if (line.extruding(self) && line.dist_XY(self) > 0) {
+                if (is_approx<double>(line.new_F(self), bridge_speed))
+                    E_per_mm.emplace_back(line.dist_E(self) / line.dist_XY(self));
+            }
+        });
+        const double nozzle_dmr                 = config.opt<ConfigOptionFloats>("nozzle_diameter")->get_at(0);
+        const double filament_dmr               = config.opt<ConfigOptionFloats>("filament_diameter")->get_at(0);
+        const double bridge_mm_per_mm           = sqr(nozzle_dmr / filament_dmr) * config.opt_float("bridge_flow_ratio");
+        size_t num_errors = std::count_if(E_per_mm.begin(), E_per_mm.end(), 
+            [bridge_mm_per_mm](double v){ return std::abs(v - bridge_mm_per_mm) > 0.01; });
+        return num_errors == 0;
+    };
+
     GIVEN("A default config with no cooling and a fixed bridge speed, flow ratio and an overhang mesh.") {
-        WHEN("bridge_flow_ratio is set to 1.0") {
+        WHEN("bridge_flow_ratio is set to 0.5 and extrusion width to default") {
+            config.set_deserialize_strict({ { "bridge_flow_ratio", 0.5}, { "extrusion_width", "0" } });
             THEN("Output flow is as expected.") {
+                REQUIRE(test(config));
             }
         }
-        WHEN("bridge_flow_ratio is set to 0.5") {
+        WHEN("bridge_flow_ratio is set to 2.0 and extrusion width to default") {
+            config.set_deserialize_strict({ { "bridge_flow_ratio", 2.0}, { "extrusion_width", "0" } });
             THEN("Output flow is as expected.") {
+                REQUIRE(test(config));
             }
         }
-        WHEN("bridge_flow_ratio is set to 2.0") {
+        WHEN("bridge_flow_ratio is set to 0.5 and extrusion_width to 0.4") {
+            config.set_deserialize_strict({ { "bridge_flow_ratio", 0.5}, { "extrusion_width", 0.4 } });
             THEN("Output flow is as expected.") {
+                REQUIRE(test(config));
+            }
+        }
+        WHEN("bridge_flow_ratio is set to 1.0 and extrusion_width to 0.4") {
+            config.set_deserialize_strict({ { "bridge_flow_ratio", 1.0}, { "extrusion_width", 0.4 } });
+            THEN("Output flow is as expected.") {
+                REQUIRE(test(config));
+            }
+        }
+        WHEN("bridge_flow_ratio is set to 2 and extrusion_width to 0.4") {
+            config.set_deserialize_strict({ { "bridge_flow_ratio", 2.}, { "extrusion_width", 0.4 } });
+            THEN("Output flow is as expected.") {
+                REQUIRE(test(config));
             }
         }
     }
diff --git a/tests/fff_print/test_gaps.cpp b/tests/fff_print/test_gaps.cpp
new file mode 100644
index 000000000..a096087ce
--- /dev/null
+++ b/tests/fff_print/test_gaps.cpp
@@ -0,0 +1,60 @@
+#include <catch2/catch.hpp>
+
+#include "libslic3r/GCodeReader.hpp"
+#include "libslic3r/Geometry/ConvexHull.hpp"
+#include "libslic3r/Layer.hpp"
+
+#include "test_data.hpp" // get access to init_print, etc
+
+using namespace Slic3r::Test;
+using namespace Slic3r;
+
+SCENARIO("Gaps", "[Gaps]") {
+    GIVEN("Two hollow squares") {
+        auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
+            { "skirts",                         0 },
+            { "perimeter_speed",                66 },
+            { "external_perimeter_speed",       66 },
+            { "small_perimeter_speed",          66 },
+            { "gap_fill_speed",                 99 },
+            { "perimeters",                     1 },
+            // to prevent speeds from being altered
+            { "cooling",                        0 },
+            // to prevent speeds from being altered
+            { "first_layer_speed",              "100%" },
+            { "perimeter_extrusion_width",      0.35 },
+            { "first_layer_extrusion_width",    0.35 }
+        });
+    
+        GCodeReader parser;
+        const double perimeter_speed = config.opt_float("perimeter_speed") * 60;
+        const double gap_fill_speed  = config.opt_float("gap_fill_speed") * 60;
+        std::string  last; // perimeter or gap
+        Points       perimeter_points;
+        int          gap_fills_outside_last_perimeters = 0;
+        parser.parse_buffer(
+            Slic3r::Test::slice({ Slic3r::Test::TestMesh::two_hollow_squares }, config),
+            [&perimeter_points, &gap_fills_outside_last_perimeters, &last, perimeter_speed, gap_fill_speed]
+                (Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line)
+        {
+            if (line.extruding(self) && line.dist_XY(self) > 0) {
+                double f = line.new_F(self);
+                Point point = line.new_XY_scaled(self);
+                if (is_approx(f, perimeter_speed)) {
+                    if (last == "gap")
+                        perimeter_points.clear();
+                    perimeter_points.emplace_back(point);
+                    last = "perimeter";
+                } else if (is_approx(f, gap_fill_speed)) {
+                    Polygon convex_hull = Geometry::convex_hull(perimeter_points);
+                    if (! convex_hull.contains(point))
+                        ++ gap_fills_outside_last_perimeters;
+                    last = "gap";
+                }
+            }
+        });
+        THEN("gap fills are printed before leaving islands") {
+            REQUIRE(gap_fills_outside_last_perimeters == 0);
+        }
+    }
+}
diff --git a/tests/fff_print/test_thin_walls.cpp b/tests/fff_print/test_thin_walls.cpp
new file mode 100644
index 000000000..cd80f3bd6
--- /dev/null
+++ b/tests/fff_print/test_thin_walls.cpp
@@ -0,0 +1,191 @@
+#include <catch2/catch.hpp>
+
+#include <numeric>
+#include <sstream>
+
+#include "test_data.hpp" // get access to init_print, etc
+
+#include "libslic3r/ExPolygon.hpp"
+#include "libslic3r/libslic3r.h"
+
+using namespace Slic3r;
+
+SCENARIO("Medial Axis", "[ThinWalls]") {
+    GIVEN("Square with hole") {
+        auto square = Polygon::new_scale({ {100, 100}, {200, 100}, {200, 200}, {100, 200} });
+        auto hole_in_square = Polygon::new_scale({ {140, 140}, {140, 160}, {160, 160}, {160, 140} });
+        ExPolygon expolygon{ square, hole_in_square };
+        WHEN("Medial axis is extracted") {
+            Polylines res = expolygon.medial_axis(scaled<double>(40.), scaled<double>(0.5));
+            THEN("medial axis of a square shape is a single path") {
+                REQUIRE(res.size() == 1);
+            }
+            THEN("polyline forms a closed loop") {
+                REQUIRE(res.front().first_point() ==  res.front().last_point());
+            }
+            THEN("medial axis loop has reasonable length") {
+                REQUIRE(res.front().length() > hole_in_square.length());
+                REQUIRE(res.front().length() < square.length());
+            }
+        }
+    }
+    GIVEN("narrow rectangle") {
+        ExPolygon expolygon{ Polygon::new_scale({ {100, 100}, {120, 100}, {120, 200}, {100, 200} }) };
+        WHEN("Medial axis is extracted") {
+            Polylines res = expolygon.medial_axis(scaled<double>(20.), scaled<double>(0.5));
+            THEN("medial axis of a narrow rectangle is a single line") {
+                REQUIRE(res.size() == 1);
+            }
+            THEN("medial axis has reasonable length") {
+                REQUIRE(res.front().length() >= scaled<double>(200.-100. - (120.-100.)) - SCALED_EPSILON);
+            }
+        }
+    }
+#if 0
+    //FIXME this test never worked
+    GIVEN("narrow rectangle with an extra vertex") {
+        ExPolygon expolygon{ Polygon::new_scale({ 
+            {100, 100}, {120, 100}, {120, 200}, 
+            {105, 200} /* extra point in the short side*/, 
+            {100, 200} 
+        })};
+        WHEN("Medial axis is extracted") {
+            Polylines res = expolygon.medial_axis(scaled<double>(1.), scaled<double>(0.5));
+            THEN("medial axis of a narrow rectangle with an extra vertex is still a single line") {
+                REQUIRE(res.size() == 1);
+            }
+            THEN("medial axis has still a reasonable length") {
+                REQUIRE(res.front().length() >= scaled<double>(200.-100. - (120.-100.)) - SCALED_EPSILON);
+            }
+            THEN("extra vertices don't influence medial axis") {
+                size_t invalid = 0;
+                for (const Polyline &pl : res)
+                    for (const Point &p : pl.points)
+                        if (std::abs(p.y() - scaled<coord_t>(150.)) < SCALED_EPSILON)
+                            ++ invalid;
+                REQUIRE(invalid == 0);
+            }
+        }
+    }
+#endif
+    GIVEN("semicircumference") {
+        ExPolygon expolygon{{ 
+            {1185881,829367},{1421988,1578184},{1722442,2303558},{2084981,2999998},{2506843,3662186},{2984809,4285086},{3515250,4863959},{4094122,5394400},
+            {4717018,5872368},{5379210,6294226},{6075653,6656769},{6801033,6957229},{7549842,7193328},{8316383,7363266},{9094809,7465751},{9879211,7500000},
+            {10663611,7465750},{11442038,7363265},{12208580,7193327},{12957389,6957228},{13682769,6656768},{14379209,6294227},{15041405,5872366},
+            {15664297,5394401},{16243171,4863960},{16758641,4301424},{17251579,3662185},{17673439,3000000},{18035980,2303556},{18336441,1578177},
+            {18572539,829368},{18750748,0},{19758422,0},{19727293,236479},{19538467,1088188},{19276136,1920196},{18942292,2726179},{18539460,3499999},
+            {18070731,4235755},{17539650,4927877},{16950279,5571067},{16307090,6160437},{15614974,6691519},{14879209,7160248},{14105392,7563079},
+            {13299407,7896927},{12467399,8159255},{11615691,8348082},{10750769,8461952},{9879211,8500000},{9007652,8461952},{8142729,8348082},
+            {7291022,8159255},{6459015,7896927},{5653029,7563079},{4879210,7160247},{4143447,6691519},{3451331,6160437},{2808141,5571066},{2218773,4927878},
+            {1687689,4235755},{1218962,3499999},{827499,2748020},{482284,1920196},{219954,1088186},{31126,236479},{0,0},{1005754,0}
+        }};
+        WHEN("Medial axis is extracted") {
+            Polylines res = expolygon.medial_axis(scaled<double>(1.324888), scaled<double>(0.25));
+            THEN("medial axis of a semicircumference is a single line") {
+                REQUIRE(res.size() == 1);
+            }
+            THEN("all medial axis segments of a semicircumference have the same orientation") {
+                int nccw = 0;
+                int ncw = 0;
+                for (const Polyline &pl : res)
+                    for (size_t i = 1; i + 1 < pl.size(); ++ i) {
+                        double cross = cross2((pl.points[i] - pl.points[i - 1]).cast<double>(), (pl.points[i + 1] - pl.points[i]).cast<double>());
+                        if (cross > 0.)
+                            ++ nccw;
+                        else if (cross < 0.)
+                            ++ ncw;
+                    }
+                REQUIRE((ncw == 0 || nccw == 0));
+            }
+        }
+    }
+    GIVEN("narrow trapezoid") {
+        ExPolygon expolygon{ Polygon::new_scale({ {100, 100}, {120, 100}, {112, 200}, {108, 200} }) };
+        WHEN("Medial axis is extracted") {
+            Polylines res = expolygon.medial_axis(scaled<double>(20.), scaled<double>(0.5));
+            THEN("medial axis of a narrow trapezoid is a single line") {
+                REQUIRE(res.size() == 1);
+            }
+            THEN("medial axis has reasonable length") {
+                REQUIRE(res.front().length() >= scaled<double>(200.-100. - (120.-100.)) - SCALED_EPSILON);
+            }
+        }
+    }
+    GIVEN("L shape") {
+        ExPolygon expolygon{ Polygon::new_scale({ {100, 100}, {120, 100}, {120, 180}, {200, 180}, {200, 200}, {100, 200}, }) };
+        WHEN("Medial axis is extracted") {
+            Polylines res = expolygon.medial_axis(scaled<double>(20.), scaled<double>(0.5));
+            THEN("medial axis of an L shape is a single line") {
+                REQUIRE(res.size() == 1);
+            }
+            THEN("medial axis has reasonable length") {
+                // 20 is the thickness of the expolygon, which is subtracted from the ends
+                auto len = unscale<double>(res.front().length()) + 20;
+                REQUIRE(len > 80. * 2.);
+                REQUIRE(len < 100. * 2.);
+            }
+        }
+    }
+    GIVEN("whatever shape") {
+        ExPolygon expolygon{{ 
+            {-203064906,-51459966},{-219312231,-51459966},{-219335477,-51459962},{-219376095,-51459962},{-219412047,-51459966},
+            {-219572094,-51459966},{-219624814,-51459962},{-219642183,-51459962},{-219656665,-51459966},{-220815482,-51459966},
+            {-220815482,-37738966},{-221117540,-37738966},{-221117540,-51762024},{-203064906,-51762024},
+        }};
+        WHEN("Medial axis is extracted") {
+            Polylines res = expolygon.medial_axis(819998., 102499.75);
+            THEN("medial axis is a single line") {
+                REQUIRE(res.size() == 1);
+            }
+            THEN("medial axis has reasonable length") {
+                double perimeter = expolygon.contour.split_at_first_point().length();
+                REQUIRE(total_length(res) > perimeter / 2. / 4. * 3.);
+            }
+        }
+    }
+    GIVEN("narrow triangle") {
+        ExPolygon expolygon{ Polygon::new_scale({ {50, 100}, {1000, 102}, {50, 104} }) };
+        WHEN("Medial axis is extracted") {
+            Polylines res = expolygon.medial_axis(scaled<double>(4.), scaled<double>(0.5));
+            THEN("medial axis of a narrow triangle is a single line") {
+                REQUIRE(res.size() == 1);
+            }
+            THEN("medial axis has reasonable length") {
+                REQUIRE(res.front().length() >= scaled<double>(200.-100. - (120.-100.)) - SCALED_EPSILON);
+            }
+        }
+    }
+    GIVEN("GH #2474") {
+        ExPolygon expolygon{{ {91294454,31032190},{11294481,31032190},{11294481,29967810},{44969182,29967810},{89909960,29967808},{91294454,29967808} }};
+        WHEN("Medial axis is extracted") {
+            Polylines res = expolygon.medial_axis(1871238, 500000);
+            THEN("medial axis is a single line") {
+                REQUIRE(res.size() == 1);
+            }
+            Polyline &polyline = res.front();
+            THEN("medial axis is horizontal and is centered") {
+                double expected_y = expolygon.contour.bounding_box().center().y();
+                double center_y   = 0.;
+                for (auto &p : polyline.points)
+                    center_y += double(p.y());
+                REQUIRE(std::abs(center_y / polyline.size() - expected_y) < SCALED_EPSILON);
+            }
+            // order polyline from left to right
+            if (polyline.first_point().x() > polyline.last_point().x())
+                polyline.reverse();
+            BoundingBox polyline_bb = polyline.bounding_box();
+            THEN("expected x_min") {
+                REQUIRE(polyline.first_point().x() == polyline_bb.min.x());
+            }
+            THEN("expected x_max") {
+                REQUIRE(polyline.last_point().x() == polyline_bb.max.x());
+            }
+            THEN("medial axis is monotonous in x (not self intersecting)") {
+                Polyline sorted { polyline };
+                std::sort(sorted.begin(), sorted.end());
+                REQUIRE(polyline == sorted);
+            }
+        }
+    }
+}
diff --git a/tests/libslic3r/CMakeLists.txt b/tests/libslic3r/CMakeLists.txt
index 248245182..a3984b34b 100644
--- a/tests/libslic3r/CMakeLists.txt
+++ b/tests/libslic3r/CMakeLists.txt
@@ -13,6 +13,7 @@ add_executable(${_TEST_NAME}_tests
 	test_geometry.cpp
 	test_placeholder_parser.cpp
 	test_polygon.cpp
+	test_polyline.cpp
 	test_mutable_polygon.cpp
 	test_mutable_priority_queue.cpp
 	test_stl.cpp
diff --git a/tests/libslic3r/test_config.cpp b/tests/libslic3r/test_config.cpp
index 025459d68..50e61b3b0 100644
--- a/tests/libslic3r/test_config.cpp
+++ b/tests/libslic3r/test_config.cpp
@@ -1,5 +1,6 @@
 #include <catch2/catch.hpp>
 
+#include "libslic3r/Config.hpp"
 #include "libslic3r/PrintConfig.hpp"
 #include "libslic3r/LocalesUtils.hpp"
 
@@ -13,20 +14,20 @@ using namespace Slic3r;
 SCENARIO("Generic config validation performs as expected.", "[Config]") {
     GIVEN("A config generated from default options") {
         Slic3r::DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
-        WHEN( "perimeter_extrusion_width is set to 250%, a valid value") {
+        WHEN("perimeter_extrusion_width is set to 250%, a valid value") {
             config.set_deserialize_strict("perimeter_extrusion_width", "250%");
             THEN( "The config is read as valid.") {
                 REQUIRE(config.validate().empty());
             }
         }
-        WHEN( "perimeter_extrusion_width is set to -10, an invalid value") {
+        WHEN("perimeter_extrusion_width is set to -10, an invalid value") {
             config.set("perimeter_extrusion_width", -10);
             THEN( "Validate returns error") {
                 REQUIRE(! config.validate().empty());
             }
         }
 
-        WHEN( "perimeters is set to -10, an invalid value") {
+        WHEN("perimeters is set to -10, an invalid value") {
             config.set("perimeters", -10);
             THEN( "Validate returns error") {
                 REQUIRE(! config.validate().empty());
@@ -36,8 +37,7 @@ SCENARIO("Generic config validation performs as expected.", "[Config]") {
 }
 
 SCENARIO("Config accessor functions perform as expected.", "[Config]") {
-    GIVEN("A config generated from default options") {
-        Slic3r::DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
+    auto test = [](ConfigBase &config) {
         WHEN("A boolean option is set to a boolean value") {
             REQUIRE_NOTHROW(config.set("gcode_comments", true));
             THEN("The underlying value is set correctly.") {
@@ -70,8 +70,8 @@ SCENARIO("Config accessor functions perform as expected.", "[Config]") {
             }
         }
 #if 0
-		//FIXME better design accessors for vector elements.
-		WHEN("An integer-based option is set through the integer interface") {
+        //FIXME better design accessors for vector elements.
+        WHEN("An integer-based option is set through the integer interface") {
             config.set("bed_temperature", 100);
             THEN("The underlying value is set correctly.") {
                 REQUIRE(config.opt<ConfigOptionInts>("bed_temperature")->get_at(0) == 100);
@@ -193,6 +193,14 @@ SCENARIO("Config accessor functions perform as expected.", "[Config]") {
                 REQUIRE(config.opt_float("layer_height") == 0.5);
             }
         }
+    };
+    GIVEN("DynamicPrintConfig generated from default options") {
+        auto config = Slic3r::DynamicPrintConfig::full_print_config();
+        test(config);
+    }
+    GIVEN("FullPrintConfig generated from default options") {
+        Slic3r::FullPrintConfig config;
+        test(config);
     }
 }
 
diff --git a/tests/libslic3r/test_geometry.cpp b/tests/libslic3r/test_geometry.cpp
index 34e625e6d..41ef69aaa 100644
--- a/tests/libslic3r/test_geometry.cpp
+++ b/tests/libslic3r/test_geometry.cpp
@@ -162,14 +162,34 @@ TEST_CASE("Splitting a Polygon generates a polyline correctly", "[Geometry]"){
 }
 
 
-TEST_CASE("Bounding boxes are scaled appropriately", "[Geometry]"){
-    BoundingBox bb(std::vector<Point>({Point(0, 1), Point(10, 2), Point(20, 2)}));
-    bb.scale(2);
-    REQUIRE(bb.min == Point(0,2));
-    REQUIRE(bb.max == Point(40,4));
+SCENARIO("BoundingBox", "[Geometry]") {
+    WHEN("Bounding boxes are scaled") {
+        BoundingBox bb(std::vector<Point>({Point(0, 1), Point(10, 2), Point(20, 2)}));
+        bb.scale(2);
+        REQUIRE(bb.min == Point(0,2));
+        REQUIRE(bb.max == Point(40,4));
+    }
+    WHEN("BoundingBox constructed from points") {
+        BoundingBox bb(Points{ {100,200}, {100, 200}, {500, -600} });
+        THEN("minimum is correct") {
+            REQUIRE(bb.min == Point{100,-600});
+        }
+        THEN("maximum is correct") {
+            REQUIRE(bb.max == Point{500,200});
+        }
+    }
+    WHEN("BoundingBox constructed from a single point") {
+        BoundingBox bb;
+        bb.merge({10, 10});
+        THEN("minimum equals to the only defined point") {
+            REQUIRE(bb.min == Point{10,10});
+        }
+        THEN("maximum equals to the only defined point") {
+            REQUIRE(bb.max == Point{10,10});
+        }
+    }
 }
 
-
 TEST_CASE("Offseting a line generates a polygon correctly", "[Geometry]"){
 	Slic3r::Polyline tmp = { Point(10,10), Point(20,10) };
     Slic3r::Polygon area = offset(tmp,5).at(0);
diff --git a/tests/libslic3r/test_polygon.cpp b/tests/libslic3r/test_polygon.cpp
index f2c78cace..7774b44a6 100644
--- a/tests/libslic3r/test_polygon.cpp
+++ b/tests/libslic3r/test_polygon.cpp
@@ -148,3 +148,65 @@ SCENARIO("Remove collinear points from Polygon", "[Polygon]") {
         }
     }
 }
+
+SCENARIO("Simplify polygon", "[Polygon]")
+{
+    GIVEN("gear") {
+        auto gear = Polygon::new_scale({
+            {144.9694,317.1543}, {145.4181,301.5633}, {146.3466,296.921}, {131.8436,294.1643}, {131.7467,294.1464},
+            {121.7238,291.5082}, {117.1631,290.2776}, {107.9198,308.2068}, {100.1735,304.5101}, {104.9896,290.3672},
+            {106.6511,286.2133}, {93.453,279.2327}, {81.0065,271.4171}, {67.7886,286.5055}, {60.7927,280.1127},
+            {69.3928,268.2566}, {72.7271,264.9224}, {61.8152,253.9959}, {52.2273,242.8494}, {47.5799,245.7224},
+            {34.6577,252.6559}, {30.3369,245.2236}, {42.1712,236.3251}, {46.1122,233.9605}, {43.2099,228.4876},
+            {35.0862,211.5672}, {33.1441,207.0856}, {13.3923,212.1895}, {10.6572,203.3273}, {6.0707,204.8561},
+            {7.2775,204.4259}, {29.6713,196.3631}, {25.9815,172.1277}, {25.4589,167.2745}, {19.8337,167.0129},
+            {5.0625,166.3346}, {5.0625,156.9425}, {5.3701,156.9282}, {21.8636,156.1628}, {25.3713,156.4613},
+            {25.4243,155.9976}, {29.3432,155.8157}, {30.3838,149.3549}, {26.3596,147.8137}, {27.1085,141.2604},
+            {29.8466,126.8337}, {24.5841,124.9201}, {10.6664,119.8989}, {13.4454,110.9264}, {33.1886,116.0691},
+            {38.817,103.1819}, {45.8311,89.8133}, {30.4286,76.81}, {35.7686,70.0812}, {48.0879,77.6873},
+            {51.564,81.1635}, {61.9006,69.1791}, {72.3019,58.7916}, {60.5509,42.5416}, {68.3369,37.1532},
+            {77.9524,48.1338}, {80.405,52.2215}, {92.5632,44.5992}, {93.0123,44.3223}, {106.3561,37.2056},
+            {100.8631,17.4679}, {108.759,14.3778}, {107.3148,11.1283}, {117.0002,32.8627}, {140.9109,27.3974},
+            {145.7004,26.4994}, {145.1346,6.1011}, {154.502,5.4063}, {156.9398,25.6501}, {171.0557,26.2017},
+            {181.3139,27.323}, {186.2377,27.8532}, {191.6031,8.5474}, {200.6724,11.2756}, {197.2362,30.2334},
+            {220.0789,39.1906}, {224.3261,41.031}, {236.3506,24.4291}, {243.6897,28.6723}, {234.2956,46.7747},
+            {245.6562,55.1643}, {257.2523,65.0901}, {261.4374,61.5679}, {273.1709,52.8031}, {278.555,59.5164},
+            {268.4334,69.8001}, {264.1615,72.3633}, {268.2763,77.9442}, {278.8488,93.5305}, {281.4596,97.6332},
+            {286.4487,95.5191}, {300.2821,90.5903}, {303.4456,98.5849}, {286.4523,107.7253}, {293.7063,131.1779},
+            {294.9748,135.8787}, {314.918,133.8172}, {315.6941,143.2589}, {300.9234,146.1746}, {296.6419,147.0309},
+            {297.1839,161.7052}, {296.6136,176.3942}, {302.1147,177.4857}, {316.603,180.3608}, {317.1658,176.7341},
+            {315.215,189.6589}, {315.1749,189.6548}, {294.9411,187.5222}, {291.13,201.7233}, {286.2615,215.5916},
+            {291.1944,218.2545}, {303.9158,225.1271}, {299.2384,233.3694}, {285.7165,227.6001}, {281.7091,225.1956},
+            {273.8981,237.6457}, {268.3486,245.2248}, {267.4538,246.4414}, {264.8496,250.0221}, {268.6392,253.896},
+            {278.5017,265.2131}, {272.721,271.4403}, {257.2776,258.3579}, {234.4345,276.5687}, {242.6222,294.8315},
+            {234.9061,298.5798}, {227.0321,286.2841}, {225.2505,281.8301}, {211.5387,287.8187}, {202.3025,291.0935},
+            {197.307,292.831}, {199.808,313.1906}, {191.5298,315.0787}, {187.3082,299.8172}, {186.4201,295.3766},
+            {180.595,296.0487}, {161.7854,297.4248}, {156.8058,297.6214}, {154.3395,317.8592}
+        });
+     
+        WHEN("simplified") {
+            size_t num_points = gear.size();
+            Polygons simplified = gear.simplify(1000.);
+            THEN("gear simplified to a single polygon") {
+                REQUIRE(simplified.size() == 1);
+            }
+            THEN("gear was reduced using Douglas-Peucker") {
+                //note printf "original points: %d\nnew points: %d", $num_points, scalar(@{$simplified->[0]});
+                REQUIRE(simplified.front().size() < num_points);
+            }
+        }
+    }
+    GIVEN("hole in square") {
+        // CW oriented
+        auto hole_in_square = Polygon{ {140, 140}, {140, 160}, {160, 160}, {160, 140} };
+        WHEN("simplified") {
+            Polygons simplified = hole_in_square.simplify(2.);
+            THEN("hole simplification returns one polygon") {
+                REQUIRE(simplified.size() == 1);
+            }
+            THEN("hole simplification turns cw polygon into ccw polygon") {
+                REQUIRE(simplified.front().is_counter_clockwise());
+            }
+        }
+    }
+}
diff --git a/tests/libslic3r/test_polyline.cpp b/tests/libslic3r/test_polyline.cpp
new file mode 100644
index 000000000..827155404
--- /dev/null
+++ b/tests/libslic3r/test_polyline.cpp
@@ -0,0 +1,28 @@
+#include <catch2/catch.hpp>
+
+#include "libslic3r/Point.hpp"
+#include "libslic3r/Polyline.hpp"
+
+using namespace Slic3r;
+
+SCENARIO("Simplify polyline", "[Polyline]")
+{
+    GIVEN("polyline 1") {
+        auto polyline = Polyline{ {0,0},{1,0},{2,0},{2,1},{2,2},{1,2},{0,2},{0,1},{0,0} };
+        WHEN("simplified with Douglas-Peucker") {
+            polyline.simplify(1.);
+            THEN("simplified correctly") {
+                REQUIRE(polyline == Polyline{ {0,0}, {2,0}, {2,2}, {0,2}, {0,0} });
+            }
+        }
+    }
+    GIVEN("polyline 2") {
+        auto polyline = Polyline{ {0,0}, {50,50}, {100,0}, {125,-25}, {150,50} };
+        WHEN("simplified with Douglas-Peucker") {
+            polyline.simplify(25.);
+            THEN("not simplified") {
+                REQUIRE(polyline == Polyline{ {0,0}, {50,50}, {125,-25}, {150,50} });
+            }
+        }
+    }
+}
diff --git a/xs/CMakeLists.txt b/xs/CMakeLists.txt
index 1a58aab12..c2343b032 100644
--- a/xs/CMakeLists.txt
+++ b/xs/CMakeLists.txt
@@ -49,7 +49,6 @@ set(XS_XSP_FILES
     ${XSP_DIR}/ExtrusionEntityCollection.xsp
     ${XSP_DIR}/ExtrusionLoop.xsp
     ${XSP_DIR}/ExtrusionPath.xsp
-    ${XSP_DIR}/GCode.xsp
     ${XSP_DIR}/Geometry.xsp
     ${XSP_DIR}/Layer.xsp
     ${XSP_DIR}/Line.xsp
diff --git a/xs/lib/Slic3r/XS.pm b/xs/lib/Slic3r/XS.pm
index 1675ac193..87fb267c5 100644
--- a/xs/lib/Slic3r/XS.pm
+++ b/xs/lib/Slic3r/XS.pm
@@ -158,7 +158,6 @@ for my $class (qw(
         Slic3r::ExtrusionLoop
         Slic3r::ExtrusionPath
         Slic3r::ExtrusionPath::Collection
-        Slic3r::GCode
         Slic3r::Geometry::BoundingBox
         Slic3r::Layer
         Slic3r::Layer::Region
diff --git a/xs/src/perlglue.cpp b/xs/src/perlglue.cpp
index 4e293a2f9..8484b1d64 100644
--- a/xs/src/perlglue.cpp
+++ b/xs/src/perlglue.cpp
@@ -7,7 +7,6 @@ REGISTER_CLASS(ExPolygon, "ExPolygon");
 REGISTER_CLASS(ExtrusionPath, "ExtrusionPath");
 REGISTER_CLASS(ExtrusionLoop, "ExtrusionLoop");
 REGISTER_CLASS(ExtrusionEntityCollection, "ExtrusionPath::Collection");
-REGISTER_CLASS(CoolingBuffer, "GCode::CoolingBuffer");
 REGISTER_CLASS(GCode, "GCode");
 REGISTER_CLASS(Layer, "Layer");
 REGISTER_CLASS(LayerRegion, "Layer::Region");
diff --git a/xs/t/01_trianglemesh.t b/xs/t/01_trianglemesh.t
deleted file mode 100644
index a071a75a2..000000000
--- a/xs/t/01_trianglemesh.t
+++ /dev/null
@@ -1,30 +0,0 @@
-#!/usr/bin/perl
-
-use strict;
-use warnings;
-
-use Slic3r::XS;
-use Test::More tests => 4;
-
-my $cube = {
-    vertices    => [ [20,20,0], [20,0,0], [0,0,0], [0,20,0], [20,20,20], [0,20,20], [0,0,20], [20,0,20] ],
-    facets      => [ [0,1,2], [0,2,3], [4,5,6], [4,6,7], [0,4,7], [0,7,1], [1,7,6], [1,6,2], [2,6,5], [2,5,3], [4,0,3], [4,3,5] ],
-};
-
-{
-    my $m = Slic3r::TriangleMesh->new;
-    $m->ReadFromPerl($cube->{vertices}, $cube->{facets});
-    my ($vertices, $facets) = ($m->vertices, $m->facets);
-    
-    is_deeply $vertices, $cube->{vertices}, 'vertices arrayref roundtrip';
-    is_deeply $facets, $cube->{facets}, 'facets arrayref roundtrip';
-    
-    {
-        my $m2 = $m->clone;
-        is_deeply $m2->vertices, $cube->{vertices}, 'cloned vertices arrayref roundtrip';
-        is_deeply $m2->facets, $cube->{facets}, 'cloned facets arrayref roundtrip';
-        $m2->scale(3);  # check that it does not affect $m
-    }
-}
-
-__END__
diff --git a/xs/t/03_point.t b/xs/t/03_point.t
index c950998fb..f888349b3 100644
--- a/xs/t/03_point.t
+++ b/xs/t/03_point.t
@@ -4,10 +4,9 @@ use strict;
 use warnings;
 
 use Slic3r::XS;
-use Test::More tests => 24;
+use Test::More tests => 21;
 
 my $point = Slic3r::Point->new(10, 15);
-is_deeply [ @$point ], [10, 15], 'point roundtrip';
 
 my $point2 = $point->clone;
 $point2->scale(2);
@@ -16,9 +15,6 @@ is_deeply [ @$point2 ], [20, 30], 'scale';
 $point2->translate(10, -15);
 is_deeply [ @$point2 ], [30, 15], 'translate';
 
-ok $point->coincides_with($point->clone), 'coincides_with';
-ok !$point->coincides_with($point2), 'coincides_with';
-
 {
     my $point3 = Slic3r::Point->new(4300000, -9880845);
     is $point->[0], $point->x, 'x accessor';
diff --git a/xs/t/04_expolygon.t b/xs/t/04_expolygon.t
index 48eaed551..9132c44b9 100644
--- a/xs/t/04_expolygon.t
+++ b/xs/t/04_expolygon.t
@@ -5,7 +5,7 @@ use warnings;
 
 use List::Util qw(first sum);
 use Slic3r::XS;
-use Test::More tests => 15;
+use Test::More tests => 7;
 
 use constant PI => 4 * atan2(1, 1);
 
@@ -25,21 +25,6 @@ my $hole_in_square = [  # cw
 my $expolygon = Slic3r::ExPolygon->new($square, $hole_in_square);
 
 ok $expolygon->is_valid, 'is_valid';
-is ref($expolygon->pp), 'ARRAY', 'expolygon pp is unblessed';
-is_deeply $expolygon->pp, [$square, $hole_in_square], 'expolygon roundtrip';
-
-is ref($expolygon->arrayref), 'ARRAY', 'expolygon arrayref is unblessed';
-isa_ok $expolygon->[0], 'Slic3r::Polygon::Ref', 'expolygon polygon is blessed';
-isa_ok $expolygon->contour, 'Slic3r::Polygon::Ref', 'expolygon contour is blessed';
-isa_ok $expolygon->holes->[0], 'Slic3r::Polygon::Ref', 'expolygon hole is blessed';
-isa_ok $expolygon->[0][0], 'Slic3r::Point::Ref', 'expolygon point is blessed';
-
-{
-    my $expolygon2 = $expolygon->clone;
-    my $polygon = $expolygon2->[0];
-    $polygon->scale(2);
-    is $expolygon2->[0][0][0], $polygon->[0][0], 'polygons are returned by reference';
-}
 
 is_deeply $expolygon->clone->pp, [$square, $hole_in_square], 'clone';
 
diff --git a/xs/t/05_surface.t b/xs/t/05_surface.t
index 34feb4734..4d9eb5b89 100644
--- a/xs/t/05_surface.t
+++ b/xs/t/05_surface.t
@@ -4,7 +4,7 @@ use strict;
 use warnings;
 
 use Slic3r::XS;
-use Test::More tests => 15;
+use Test::More tests => 11;
 
 my $square = [  # ccw
     [100, 100],
@@ -27,10 +27,6 @@ my $surface = Slic3r::Surface->new(
 
 $surface = $surface->clone;
 
-isa_ok $surface->expolygon, 'Slic3r::ExPolygon::Ref', 'expolygon';
-is_deeply [ @{$surface->expolygon->pp} ], [$square, $hole_in_square], 'expolygon roundtrip';
-is scalar(@{$surface->polygons}), 2, 'polygons roundtrip';
-
 is $surface->surface_type, Slic3r::Surface::S_TYPE_INTERNAL, 'surface_type';
 $surface->surface_type(Slic3r::Surface::S_TYPE_BOTTOM);
 is $surface->surface_type, Slic3r::Surface::S_TYPE_BOTTOM, 'modify surface_type';
@@ -59,7 +55,6 @@ is $surface->extra_perimeters, 2, 'extra_perimeters';
     is scalar(@$collection), 1, 'append to collection';
     
     my $item = $collection->[0];
-    isa_ok $item, 'Slic3r::Surface::Ref';
     $item->surface_type(Slic3r::Surface::S_TYPE_INTERNAL);
     is $item->surface_type, $collection->[0]->surface_type, 'collection returns items by reference';
 }
diff --git a/xs/t/06_polygon.t b/xs/t/06_polygon.t
deleted file mode 100644
index 7bbcd5356..000000000
--- a/xs/t/06_polygon.t
+++ /dev/null
@@ -1,21 +0,0 @@
-#!/usr/bin/perl
-
-use strict;
-use warnings;
-
-use Slic3r::XS;
-use Test::More tests => 3;
-
-my $square = [  # ccw
-    [100, 100],
-    [200, 100],
-    [200, 200],
-    [100, 200],
-];
-
-my $polygon = Slic3r::Polygon->new(@$square);
-is ref($polygon->arrayref), 'ARRAY', 'polygon arrayref is unblessed';
-isa_ok $polygon->[0], 'Slic3r::Point::Ref', 'polygon point is blessed';
-ok ref($polygon->first_point) eq 'Slic3r::Point', 'first_point';
-
-__END__
diff --git a/xs/t/07_extrusionpath.t b/xs/t/07_extrusionpath.t
index 008b51b00..084b4f03e 100644
--- a/xs/t/07_extrusionpath.t
+++ b/xs/t/07_extrusionpath.t
@@ -4,7 +4,7 @@ use strict;
 use warnings;
 
 use Slic3r::XS;
-use Test::More tests => 7;
+use Test::More tests => 5;
 
 my $points = [
     [100, 100],
@@ -17,8 +17,6 @@ my $path = Slic3r::ExtrusionPath->new(
     role     => Slic3r::ExtrusionPath::EXTR_ROLE_EXTERNAL_PERIMETER,
     mm3_per_mm => 1,
 );
-isa_ok $path->polyline, 'Slic3r::Polyline::Ref', 'path polyline';
-is_deeply $path->polyline->pp, $points, 'path points roundtrip';
 
 $path->reverse;
 is_deeply $path->polyline->pp, [ reverse @$points ], 'reverse path';
diff --git a/xs/t/08_extrusionloop.t b/xs/t/08_extrusionloop.t
index e0660a9fd..3abfbd728 100644
--- a/xs/t/08_extrusionloop.t
+++ b/xs/t/08_extrusionloop.t
@@ -5,7 +5,7 @@ use warnings;
 
 use List::Util qw(sum);
 use Slic3r::XS;
-use Test::More tests => 47;
+use Test::More tests => 46;
 
 {
     my $square = [
@@ -33,7 +33,6 @@ use Test::More tests => 47;
     is scalar(@$loop), 1, 'loop contains one path';
     {
         my $path = $loop->[0];
-        isa_ok $path, 'Slic3r::ExtrusionPath::Ref';
         is $path->role, Slic3r::ExtrusionPath::EXTR_ROLE_EXTERNAL_PERIMETER, 'role';
     }
 
diff --git a/xs/t/09_polyline.t b/xs/t/09_polyline.t
index 5203ec5ef..7da74b93d 100644
--- a/xs/t/09_polyline.t
+++ b/xs/t/09_polyline.t
@@ -4,7 +4,7 @@ use strict;
 use warnings;
 
 use Slic3r::XS;
-use Test::More tests => 18;
+use Test::More tests => 15;
 
 my $points = [
     [100, 100],
@@ -14,11 +14,6 @@ my $points = [
 
 my $polyline = Slic3r::Polyline->new(@$points);
 
-is_deeply $polyline->pp, $points, 'polyline roundtrip';
-
-is ref($polyline->arrayref), 'ARRAY', 'polyline arrayref is unblessed';
-isa_ok $polyline->[0], 'Slic3r::Point::Ref', 'polyline point is blessed';
-
 my $lines = $polyline->lines;
 is_deeply [ map $_->pp, @$lines ], [
     [ [100, 100], [200, 100] ],
@@ -88,41 +83,4 @@ is_deeply $polyline->pp, [ @$points, @$points ], 'append_polyline';
     is scalar(@$p2), 4, 'split_at';
 }
 
-# disabled because we now use a more efficient but incomplete algorithm
-#if (0) {
-#    my $polyline = Slic3r::Polyline->new(
-#        map [$_,10], (0,10,20,30,40,50,60)
-#    );
-#    {
-#        my $expolygon = Slic3r::ExPolygon->new(Slic3r::Polygon->new(
-#            [25,0], [55,0], [55,30], [25,30],
-#        ));
-#        my $p = $polyline->clone;
-#        $p->simplify_by_visibility($expolygon);
-#        is_deeply $p->pp, [
-#            map [$_,10], (0,10,20,30,50,60)
-#        ], 'simplify_by_visibility()';
-#    }
-#    {
-#        my $expolygon = Slic3r::ExPolygon->new(Slic3r::Polygon->new(
-#            [-15,0], [75,0], [75,30], [-15,30],
-#        ));
-#        my $p = $polyline->clone;
-#        $p->simplify_by_visibility($expolygon);
-#        is_deeply $p->pp, [
-#            map [$_,10], (0,60)
-#        ], 'simplify_by_visibility()';
-#    }
-#    {
-#        my $expolygon = Slic3r::ExPolygon->new(Slic3r::Polygon->new(
-#            [-15,0], [25,0], [25,30], [-15,30],
-#        ));
-#        my $p = $polyline->clone;
-#        $p->simplify_by_visibility($expolygon);
-#        is_deeply $p->pp, [
-#            map [$_,10], (0,20,30,40,50,60)
-#        ], 'simplify_by_visibility()';
-#    }
-#}
-
 __END__
diff --git a/xs/t/10_line.t b/xs/t/10_line.t
index 8f82e988c..886573f7b 100644
--- a/xs/t/10_line.t
+++ b/xs/t/10_line.t
@@ -4,7 +4,7 @@ use strict;
 use warnings;
 
 use Slic3r::XS;
-use Test::More tests => 40;
+use Test::More tests => 35;
 
 use constant PI         => 4 * atan2(1, 1);
 use constant EPSILON    => 1E-4;
@@ -15,21 +15,6 @@ my $points = [
 ];
 
 my $line = Slic3r::Line->new(@$points);
-is_deeply $line->pp, $points, 'line roundtrip';
-
-is ref($line->arrayref), 'ARRAY', 'line arrayref is unblessed';
-isa_ok $line->[0], 'Slic3r::Point::Ref', 'line point is blessed';
-
-{
-    my $clone = $line->clone;
-    $clone->reverse;
-    is_deeply $clone->pp, [ reverse @$points ], 'reverse';
-}
-
-{
-    my $line2 = Slic3r::Line->new($line->a->clone, $line->b->clone);
-    is_deeply $line2->pp, $points, 'line roundtrip with cloned points';
-}
 
 {
     my $clone = $line->clone;
diff --git a/xs/t/12_extrusionpathcollection.t b/xs/t/12_extrusionpathcollection.t
index e7e0b1316..e02854245 100644
--- a/xs/t/12_extrusionpathcollection.t
+++ b/xs/t/12_extrusionpathcollection.t
@@ -4,7 +4,7 @@ use strict;
 use warnings;
 
 use Slic3r::XS;
-use Test::More tests => 18;
+use Test::More tests => 13;
 
 my $points = [
     [100, 100],
@@ -41,12 +41,6 @@ is scalar(@$collection), 3, 'append ExtrusionPath';
 $collection->append($loop);
 is scalar(@$collection), 4, 'append ExtrusionLoop';
 
-isa_ok $collection->[1], 'Slic3r::ExtrusionPath::Collection::Ref', 'correct object returned for collection';
-isa_ok $collection->[2], 'Slic3r::ExtrusionPath::Ref', 'correct object returned for path';
-isa_ok $collection->[3], 'Slic3r::ExtrusionLoop::Ref', 'correct object returned for loop';
-is ref($collection->[2]->clone), 'Slic3r::ExtrusionPath', 'correct object returned for cloned path';
-is ref($collection->[3]->clone), 'Slic3r::ExtrusionLoop', 'correct object returned for cloned loop';
-
 is scalar(@{$collection->[1]}), 1, 'appended collection was duplicated';
 
 {
diff --git a/xs/t/17_boundingbox.t b/xs/t/17_boundingbox.t
deleted file mode 100644
index 349e0024d..000000000
--- a/xs/t/17_boundingbox.t
+++ /dev/null
@@ -1,27 +0,0 @@
-#!/usr/bin/perl
-
-use strict;
-use warnings;
-
-use Slic3r::XS;
-use Test::More tests => 5;
-
-{
-    my @points = (
-        Slic3r::Point->new(100, 200),
-        Slic3r::Point->new(500, -600),
-    );
-    my $bb = Slic3r::Geometry::BoundingBox->new_from_points(\@points);
-    isa_ok $bb, 'Slic3r::Geometry::BoundingBox', 'new_from_points';
-    is_deeply $bb->min_point->pp, [100,-600], 'min_point';
-    is_deeply $bb->max_point->pp, [500,200], 'max_point';
-}
-
-{
-    my $bb = Slic3r::Geometry::BoundingBox->new;
-    $bb->merge_point(Slic3r::Point->new(10, 10));
-    is_deeply $bb->min_point->pp, [10,10], 'min_point equals to the only defined point';
-    is_deeply $bb->max_point->pp, [10,10], 'max_point equals to the only defined point';
-}
-
-__END__
diff --git a/xs/xsp/ExPolygon.xsp b/xs/xsp/ExPolygon.xsp
index a57bcfbcb..50b32544e 100644
--- a/xs/xsp/ExPolygon.xsp
+++ b/xs/xsp/ExPolygon.xsp
@@ -29,8 +29,6 @@
         %code{% RETVAL = THIS->contains(*point); %};
     ExPolygons simplify(double tolerance);
     Polygons simplify_p(double tolerance);
-    Polylines medial_axis(double max_width, double min_width)
-        %code{% THIS->medial_axis(max_width, min_width, &RETVAL); %};
 %{
 
 ExPolygon*
diff --git a/xs/xsp/GCode.xsp b/xs/xsp/GCode.xsp
deleted file mode 100644
index 4c2583894..000000000
--- a/xs/xsp/GCode.xsp
+++ /dev/null
@@ -1,53 +0,0 @@
-%module{Slic3r::XS};
-
-%{
-#include <xsinit.h>
-#include "libslic3r/GCode.hpp"
-#include "libslic3r/GCode/CoolingBuffer.hpp"
-%}
-
-%name{Slic3r::GCode::CoolingBuffer} class CoolingBuffer {
-    CoolingBuffer(GCode* gcode)
-        %code{% RETVAL = new CoolingBuffer(*gcode); %};
-    ~CoolingBuffer();
-    std::string process_layer(std::string gcode, size_t layer_id)
-        %code{% RETVAL = THIS->process_layer(std::move(gcode), layer_id, true); %};
-
-};
-
-%name{Slic3r::GCode} class GCode {
-    GCode();
-    ~GCode();
-    void do_export(Print *print, const char *path)
-        %code%{
-            try {
-                THIS->do_export(print, path);
-            } catch (std::exception& e) {
-                croak("%s\n", e.what());
-            }
-        %};
-
-    Ref<Vec2d> origin()
-        %code{% RETVAL = &(THIS->origin()); %};
-    void set_origin(Vec2d* pointf)
-        %code{% THIS->set_origin(*pointf); %};
-    Ref<Point> last_pos()
-        %code{% RETVAL = &(THIS->last_pos()); %};
-
-    unsigned int    layer_count() const;
-    void            set_layer_count(unsigned int value);
-    void            set_extruders(std::vector<unsigned int> extruders) 
-        %code{% THIS->writer().set_extruders(extruders); THIS->writer().set_extruder(0); %};
-
-    void apply_print_config(StaticPrintConfig* print_config)
-        %code{%
-            if (const PrintConfig* config = dynamic_cast<PrintConfig*>(print_config)) {
-                THIS->apply_print_config(*config);
-            } else {
-                CONFESS("A PrintConfig object was not supplied to apply_print_config()");
-            }
-        %};
-
-    Ref<StaticPrintConfig> config()
-        %code{% RETVAL = const_cast<StaticPrintConfig*>(static_cast<const StaticPrintConfig*>(static_cast<const PrintObjectConfig*>(&THIS->config()))); %};
-};
diff --git a/xs/xsp/my.map b/xs/xsp/my.map
index 5174bdfa9..42ca74292 100644
--- a/xs/xsp/my.map
+++ b/xs/xsp/my.map
@@ -125,18 +125,9 @@ Ref<LayerRegion>           O_OBJECT_SLIC3R_T
 Layer*                     O_OBJECT_SLIC3R
 Ref<Layer>                 O_OBJECT_SLIC3R_T
 
-CoolingBuffer*             O_OBJECT_SLIC3R
-Ref<CoolingBuffer>         O_OBJECT_SLIC3R_T
-Clone<CoolingBuffer>       O_OBJECT_SLIC3R_T
-
-GCode*                      O_OBJECT_SLIC3R
-Ref<GCode>                  O_OBJECT_SLIC3R_T
-Clone<GCode>                O_OBJECT_SLIC3R_T
-
 Axis                  T_UV
 ExtrusionLoopRole     T_UV
 ExtrusionRole     T_UV
-FlowRole     T_UV
 SurfaceType     T_UV
 
 # we return these types whenever we want the items to be cloned
diff --git a/xs/xsp/typemap.xspt b/xs/xsp/typemap.xspt
index 243e80dbc..bf1e8e2ac 100644
--- a/xs/xsp/typemap.xspt
+++ b/xs/xsp/typemap.xspt
@@ -92,18 +92,6 @@
 %typemap{Layer*};
 %typemap{Ref<Layer>}{simple};
 
-%typemap{CoolingBuffer*};
-%typemap{Ref<CoolingBuffer>}{simple};
-%typemap{Clone<CoolingBuffer>}{simple};
-
-%typemap{GCode*};
-%typemap{Ref<GCode>}{simple};
-%typemap{Clone<GCode>}{simple};
-
-//%typemap{GCodePreviewData*};
-//%typemap{Ref<GCodePreviewData>}{simple};
-//%typemap{Clone<GCodePreviewData>}{simple};
-
 %typemap{Points};
 %typemap{Pointfs};
 %typemap{Lines};
@@ -167,9 +155,3 @@
     $CVar = (ExtrusionRole)SvUV($PerlVar);
   %};
 };
-%typemap{FlowRole}{parsed}{
-  %cpp_type{FlowRole};
-  %precall_code{%
-    $CVar = (FlowRole)SvUV($PerlVar);
-  %};
-};

From ff20ad1052987b79196430ab61928c2bbdc3fa44 Mon Sep 17 00:00:00 2001
From: enricoturri1966 <enricoturri@seznam.cz>
Date: Fri, 6 May 2022 07:30:03 +0200
Subject: [PATCH 23/23] Fixed warning

---
 src/slic3r/GUI/GCodeViewer.cpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp
index afcc3f954..652410589 100644
--- a/src/slic3r/GUI/GCodeViewer.cpp
+++ b/src/slic3r/GUI/GCodeViewer.cpp
@@ -3942,7 +3942,7 @@ void GCodeViewer::render_legend(float& legend_height)
 
             std::vector<CustomGCode::Item> custom_gcode_per_print_z = wxGetApp().is_editor() ? wxGetApp().plater()->model().custom_gcode_per_print_z.gcodes : m_custom_gcode_per_print_z;
             std::vector<ColorRGBA> last_color(m_extruders_count);
-            for (int i = 0; i < m_extruders_count; ++i) {
+            for (size_t i = 0; i < m_extruders_count; ++i) {
                 last_color[i] = m_tool_colors[i];
             }
             int last_extruder_id = 1;