From 6a46b71dc1b72834f36e16f1bbf3e13f40e64c1d Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Wed, 3 Mar 2021 13:53:37 +0100 Subject: [PATCH 1/6] #5843 - GCodeProcessor: added processing of lines G28 --- src/libslic3r/GCode/GCodeProcessor.cpp | 27 ++++++++++++++++++++++++++ src/libslic3r/GCode/GCodeProcessor.hpp | 3 +++ 2 files changed, 30 insertions(+) diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index 46fd3c5b3..982a5067c 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -1114,6 +1114,7 @@ void GCodeProcessor::process_gcode_line(const GCodeReader::GCodeLine& line) case 21: { process_G21(line); break; } // Set Units to Millimeters case 22: { process_G22(line); break; } // Firmware controlled retract case 23: { process_G23(line); break; } // Firmware controlled unretract + case 28: { process_G28(line); break; } // Move to origin case 90: { process_G90(line); break; } // Set to Absolute Positioning case 91: { process_G91(line); break; } // Set to Relative Positioning case 92: { process_G92(line); break; } // Set Position @@ -2147,6 +2148,32 @@ void GCodeProcessor::process_G23(const GCodeReader::GCodeLine& line) store_move_vertex(EMoveType::Unretract); } +void GCodeProcessor::process_G28(const GCodeReader::GCodeLine& line) +{ + std::string_view cmd = line.cmd(); + std::string new_line_raw = { cmd.data(), cmd.size() }; + bool found = false; + if (line.has_x()) { + new_line_raw += " X0"; + found = true; + } + if (line.has_y()) { + new_line_raw += " Y0"; + found = true; + } + if (line.has_z()) { + new_line_raw += " Z0"; + found = true; + } + if (!found) + new_line_raw += " X0 Y0 Z0"; + + GCodeReader::GCodeLine new_line; + GCodeReader reader; + reader.parse_line(new_line_raw.c_str(), new_line, [](GCodeReader&, const GCodeReader::GCodeLine&) {}); + process_G1(new_line); +} + void GCodeProcessor::process_G90(const GCodeReader::GCodeLine& line) { m_global_positioning_type = EPositioningType::Absolute; diff --git a/src/libslic3r/GCode/GCodeProcessor.hpp b/src/libslic3r/GCode/GCodeProcessor.hpp index f619864c4..c1497edda 100644 --- a/src/libslic3r/GCode/GCodeProcessor.hpp +++ b/src/libslic3r/GCode/GCodeProcessor.hpp @@ -572,6 +572,9 @@ namespace Slic3r { // Firmware controlled Unretract void process_G23(const GCodeReader::GCodeLine& line); + // Move to origin + void process_G28(const GCodeReader::GCodeLine& line); + // Set to Absolute Positioning void process_G90(const GCodeReader::GCodeLine& line); From 5f5de1c81279895be5c62bce524eb0b87998c18c Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Wed, 3 Mar 2021 15:04:15 +0100 Subject: [PATCH 2/6] Follow-up to 5276bd98d7afaf953138d5e46963d9b4fab044c6: WIP: MutablePolygon - linked list based polygon implementation allowing rapid insertion and removal of points. WIP: porting smooth_outward() from Cura. --- src/libslic3r/MutablePolygon.cpp | 445 ++++++++++++++--------- src/libslic3r/MutablePolygon.hpp | 146 +++++++- tests/libslic3r/test_mutable_polygon.cpp | 34 ++ 3 files changed, 428 insertions(+), 197 deletions(-) diff --git a/src/libslic3r/MutablePolygon.cpp b/src/libslic3r/MutablePolygon.cpp index 951485259..f166ce701 100644 --- a/src/libslic3r/MutablePolygon.cpp +++ b/src/libslic3r/MutablePolygon.cpp @@ -1,5 +1,6 @@ #include "MutablePolygon.hpp" #include "Line.hpp" +#include "libslic3r.h" namespace Slic3r { @@ -36,207 +37,295 @@ void remove_duplicates(MutablePolygon &polygon, double eps) } } -// Sample a point on line (a, b) at distance "dist" from ref_pt. -// If two points fulfill the condition, then the first one (closer to point a) is taken. -// If none of the two points falls on line (a, b), return false. -template -static inline VectorType point_on_line_at_dist(const VectorType &a, const VectorType &b, const VectorType &ref_pt, const double dist) +// Adapted from Cura ConstPolygonRef::smooth_corner_complex() by Tim Kuipers. +// A concave corner at it1 with position p1 has been removed by the caller between it0 and it2, where |p2 - p0| < shortcut_length. +// Now try to close a concave crack by walking left from it0 and right from it2 as long as the new clipping edge is smaller than shortcut_length +// and the new clipping edge is still inside the polygon (it is a diagonal, it does not intersect polygon boundary). +// Once the traversal stops (always at a clipping edge shorter than shortcut_length), the final trapezoid is clipped with a new clipping edge of shortcut_length. +// Return true if a hole was completely closed (degenerated to an empty polygon) or a single CCW triangle was left, which is not to be simplified any further. +// it0, it2 are updated to the final clipping edge. +static bool clip_narrow_corner( + const Vec2i64 p1, + MutablePolygon::iterator &it0, + MutablePolygon::iterator &it2, + MutablePolygon::range &unprocessed_range, + int64_t dist2_current, + const int64_t shortcut_length) { - using T = typename VectorType::Scalar; - auto v = b - a; - auto l2 = v.squaredNorm(); - assert(l2 > T(0)); - auto vpt = ref_pt - a; - // Parameter of the foot point of ref_pt on line (a, b). - auto t = v.dot(vpt) / l2; - // Foot point of ref_pt on line (a, b). - auto foot_pt = a + t * v; - auto dfoot2 = vpt.squaredNorm() - (foot_pt - ref_pt).squaredNorm(); - // Distance of the result point from the foot point, normalized to length of (a, b). - auto dfoot = dfoot2 > T(0) ? sqrt(dfoot2) / sqrt(l2) : T(0); - auto t_result = t - dfoot; - if (t_result < T(0)) - t_result = t + dfoot; - t_result = Slic3r::clamp(0., 1., t_result); - return a + v * t; -} + MutablePolygon &polygon = it0.polygon(); + assert(polygon.size() >= 2); -static bool smooth_corner_complex(const Vec2d p1, MutablePolygon::iterator &it0, MutablePolygon::iterator &it2, const double shortcut_length) -{ - // walk away from the corner until the shortcut > shortcut_length or it would smooth a piece inward - // - walk in both directions untill shortcut > shortcut_length - // - stop walking in one direction if it would otherwise cut off a corner in that direction - // - same in the other direction - // - stop if both are cut off - // walk by updating p0_it and p2_it - double shortcut_length2 = shortcut_length * shortcut_length; - bool forward_is_blocked = false; - bool forward_is_too_far = false; - bool backward_is_blocked = false; - bool backward_is_too_far = false; - for (;;) { - const bool forward_has_converged = forward_is_blocked || forward_is_too_far; - const bool backward_has_converged = backward_is_blocked || backward_is_too_far; - if (forward_has_converged && backward_has_converged) { - if (forward_is_too_far && backward_is_too_far && (*it0.prev() - *it2.next()).cast().squaredNorm() < shortcut_length2) { - // Trim the narrowing region. - -- it0; - ++ it2; - forward_is_too_far = false; - backward_is_too_far = false; - continue; - } else - break; - } + const int64_t shortcut_length2 = sqr(shortcut_length); - const Vec2d p0 = it0->cast(); - const Vec2d p2 = it2->cast(); - if (! forward_has_converged && (backward_has_converged || (p2 - p1).squaredNorm() < (p0 - p1).squaredNorm())) { - // walk forward - const auto it2_2 = it2.next(); - const Vec2d p2_2 = it2_2->cast(); - if (cross2(p2 - p0, p2_2 - p0) > 0) { - forward_is_blocked = true; - } else if ((p2_2 - p0).squaredNorm() > shortcut_length2) { - forward_is_too_far = true; + enum Status { + Free, + Blocked, + Far, + }; + Status forward = Free; + Status backward = Free; + + Vec2i64 p0 = it0->cast(); + Vec2i64 p2 = it2->cast(); + Vec2i64 p02; + Vec2i64 p22; + int64_t dist2_next; + + // As long as there is at least a single triangle left in the polygon. + while (polygon.size() >= 3) { + assert(dist2_current <= shortcut_length2); + if (forward == Far && backward == Far) { + p02 = it0.prev()->cast(); + p22 = it2.next()->cast(); + auto d2 = (p22 - p02).squaredNorm(); + if (d2 <= shortcut_length2) { + // The region was narrow until now and it is still narrow. Trim at both sides. + it0 = unprocessed_range.remove_back(it0).prev(); + it2 = unprocessed_range.remove_front(it2); + if (polygon.size() <= 2) + // A hole degenerated to an empty polygon. + return true; + forward = Free; + backward = Free; + dist2_current = d2; + p0 = p02; + p2 = p22; } else { - it2 = it2_2; // make one step in the forward direction - backward_is_blocked = false; // invalidate data about backward walking - backward_is_too_far = false; + // The region is widening. Stop traversal and trim the final trapezoid. + dist2_next = d2; + break; + } + } else if (forward != Free && backward != Free) + // One of the corners is blocked, the other is blocked or too far. Stop traversal. + break; + // Try to proceed by flipping a diagonal. + // Progress by keeping the distance of the clipping edge end points equal to initial p1. + //FIXME This is an arbitrary condition, maybe a more local condition will be better (take a shorter diagonal?). + if (forward == Free && (backward != Free || (p2 - p1).squaredNorm() < (p0 - p1).cast().squaredNorm())) { + p22 = it2.next()->cast(); + if (cross2(p2 - p0, p22 - p0) > 0) + forward = Blocked; + else { + // New clipping edge lenght. + auto d2 = (p22 - p0).squaredNorm(); + if (d2 > shortcut_length2) { + forward = Far; + dist2_next = d2; + } else { + forward = Free; + // Make one step in the forward direction. + it2 = unprocessed_range.remove_front(it2); + p2 = p22; + dist2_current = d2; + } } } else { - // walk backward - const auto it0_2 = it0.prev(); - const Vec2d p0_2 = it0_2->cast(); - if (cross2(p0_2 - p0, p2 - p0_2) > 0) { - backward_is_blocked = true; - } else if ((p2 - p0_2).squaredNorm() > shortcut_length2) { - backward_is_too_far = true; - } else { - it0 = it0_2; // make one step in the backward direction - forward_is_blocked = false; // invalidate data about forward walking - forward_is_too_far = false; - } - } - - if (it0.prev() == it2 || it0 == it2) { - // stop if we went all the way around the polygon - // this should only be the case for hole polygons (?) - if (forward_is_too_far && backward_is_too_far) { - // in case p0_it.prev() == p2_it : - // / . - // / /| - // | becomes | | - // \ \| - // \ . - // in case p0_it == p2_it : - // / . - // / becomes /| - // \ \| - // \ . - break; - } else { - // this whole polygon can be removed - return true; + assert(backward == Free); + p02 = it0.prev()->cast(); + if (cross2(p0 - p2, p02 - p2) > 0) + backward = Blocked; + else { + // New clipping edge lenght. + auto d2 = (p2 - p02).squaredNorm(); + if (d2 > shortcut_length2) { + backward = Far; + dist2_next = d2; + } else { + backward = Free; + // Make one step in the backward direction. + it0 = unprocessed_range.remove_back(it0).prev(); + p0 = p02; + dist2_current = d2; + } } } } - const Vec2d p0 = it0->cast(); - const Vec2d p2 = it2->cast(); - const Vec2d v02 = p2 - p0; - const int64_t l2_v02 = v02.squaredNorm(); - if (std::abs(l2_v02 - shortcut_length2) < shortcut_length * 10) // i.e. if (size2 < l * (l+10) && size2 > l * (l-10)) - { // v02 is approximately shortcut length - // handle this separately to avoid rounding problems below in the getPointOnLineWithDist function - // p0_it and p2_it are already correct - } else if (! backward_is_blocked && ! forward_is_blocked) { - const auto l_v02 = sqrt(l2_v02); - const Vec2d p0_2 = it0.prev()->cast(); - const Vec2d p2_2 = it2.next()->cast(); - double t = Slic3r::clamp(0., 1., (shortcut_length - l_v02) / ((p2_2 - p0_2).norm() - l_v02)); - it0 = it0.prev().insert((p0 + (p0_2 - p0) * t).cast()); - it2 = it2.insert((p2 + (p2_2 - p2) * t).cast()); - } else if (! backward_is_blocked) { - it0 = it0.prev().insert(point_on_line_at_dist(p0, Vec2d(it0.prev()->cast()), p2, shortcut_length).cast()); - } else if (! forward_is_blocked) { - it2 = it2.insert(point_on_line_at_dist(p2, Vec2d(it2.next()->cast()), p0, shortcut_length).cast()); + if (polygon.size() <= 3) { + // A hole degenerated to an empty polygon, or a tiny triangle remained. + assert(polygon.size() < 3 || (forward == Blocked && backward == Blocked) || (forward == Far && backward == Far)); + if (polygon.size() < 3 || forward == Far) { + assert(polygon.size() < 3 || dist2_current <= shortcut_length2); + polygon.clear(); + } else { + // The remaining triangle is CCW oriented, keep it. + } + return true; + } + + assert(dist2_current <= shortcut_length2); + if ((forward == Blocked && backward == Blocked) || dist2_current > sqr(shortcut_length - int64_t(SCALED_EPSILON))) { + // The crack is filled, keep the last clipping edge. + } else if (dist2_next < sqr(shortcut_length - int64_t(SCALED_EPSILON))) { + // To avoid creating tiny edges. + if (forward == Far) + it0 = unprocessed_range.remove_back(it0).prev(); + if (backward == Far) + it2 = unprocessed_range.remove_front(it2); + if (polygon.size() <= 2) + // A hole degenerated to an empty polygon. + return true; + } else if (forward == Blocked || backward == Blocked) { + // One side is far, the other blocked. + assert(forward == Far || backward == Far); + if (backward == Far) { + // Sort, so we will clip the 1st edge. + std::swap(p0, p2); + std::swap(p02, p22); + } + // Find point on (p0, p02) at distance shortcut_length from p2. + // Circle intersects a line at two points, however because |p2 - p0| < shortcut_length, + // only the second intersection is valid. Because |p2 - p02| > shortcut_length, such + // intersection should always be found on (p0, p02). + const Vec2d v = (p02 - p0).cast(); + const Vec2d d = (p0 - p2).cast(); + const double a = v.squaredNorm(); + const double b = 2. * double(d.dot(v)); + double u = b * b - 4. * a * (d.squaredNorm() - shortcut_length2); + assert(u > 0.); + u = sqrt(u); + double t = (- b + u) / (2. * a); + assert(t > 0. && t < 1.); + (backward == Far ? *it2 : *it0) += (v.cast() * t).cast(); } else { - // | - // __|2 - // | / > shortcut cannot be of the desired length - // ___|/ . - // 0 - // both are blocked and p0_it and p2_it are already correct + // The trapezoid (it0.prev(), it0, it2, it2.next()) is widening. Trim it. + assert(forward == Far && backward == Far); + assert(dist2_next > shortcut_length2); + const double dcurrent = sqrt(double(dist2_current)); + double t = (shortcut_length - dcurrent) / (sqrt(double(dist2_next)) - dcurrent); + assert(t > 0. && t < 1.); + *it0 += ((p02 - p0).cast() * t).cast(); + *it2 += ((p22 - p2).cast() * t).cast(); } - // Delete all the points between it0 and it2. - while (it0.next() != it2) - it0.next().remove(); return false; } -void smooth_outward(MutablePolygon &polygon, double shortcut_length) +// adapted from Cura ConstPolygonRef::smooth_outward() by Tim Kuipers. +void smooth_outward(MutablePolygon &polygon, coord_t clip_dist_scaled) { remove_duplicates(polygon, scaled(0.01)); - const int shortcut_length2 = shortcut_length * shortcut_length; - static constexpr const double cos_min_angle = -0.70710678118654752440084436210485; // cos(135 degrees) + const auto clip_dist_scaled2 = sqr(clip_dist_scaled); + const auto clip_dist_scaled2eps = sqr(clip_dist_scaled + int64_t(SCALED_EPSILON)); + const auto foot_dist_min2 = sqr(SCALED_EPSILON); - MutablePolygon::iterator it1 = polygon.begin(); - do { - const Vec2d p1 = it1->cast(); - auto it0 = it1.prev(); - auto it2 = it1.next(); - const Vec2d p0 = it0->cast(); - const Vec2d p2 = it2->cast(); - const Vec2d v1 = p0 - p1; - const Vec2d v2 = p2 - p1; - const double cos_angle = v1.dot(v2); - if (cos_angle < cos_min_angle && cross2(v1, v2) < 0) { - // Simplify the sharp angle. - const Vec2d v02 = p2 - p0; - const double l2_v02 = v02.squaredNorm(); - if (l2_v02 >= shortcut_length2) { - // Trim an obtuse corner. + // Each source point will be visited exactly once. + MutablePolygon::range unprocessed_range(polygon); + while (! unprocessed_range.empty() && polygon.size() > 2) { + auto it1 = unprocessed_range.process_next(); + auto it0 = it1.prev(); + auto it2 = it1.next(); + const Point p0 = *it0; + const Point p1 = *it1; + const Point p2 = *it2; + const Vec2i64 v1 = (p0 - p1).cast(); + const Vec2i64 v2 = (p2 - p1).cast(); + if (cross2(v1, v2) > 0) { + // Concave corner. + int64_t dot = v1.dot(v2); + auto l2v1 = double(v1.squaredNorm()); + auto l2v2 = double(v2.squaredNorm()); + if (dot > 0 || Slic3r::sqr(double(dot)) * 2. < l2v1 * l2v2) { + // Angle between v1 and v2 bigger than 135 degrees. + // Simplify the sharp angle. + Vec2i64 v02 = (p2 - p0).cast(); + int64_t l2v02 = v02.squaredNorm(); it1.remove(); - if (l2_v02 > Slic3r::sqr(shortcut_length + SCALED_EPSILON)) { - double l2_1 = v1.squaredNorm(); - double l2_2 = v2.squaredNorm(); - bool trim = true; - if (cos_angle > 0.9999) { - // The triangle p0, p1, p2 is likely degenerate. - // Measure height of the triangle. - double d2 = l2_1 > l2_2 ? line_alg::distance_to_squared(Linef{ p0, p1 }, p2) : line_alg::distance_to_squared(Linef{ p2, p1 }, p0); - if (d2 < Slic3r::sqr(scaled(0.02))) - trim = false; - } - if (trim) { - Vec2d bisector = v1 / l2_1 + v2 / l2_2; - double d1 = v1.dot(bisector) / l2_1; - double d2 = v2.dot(bisector) / l2_2; - double lbisector = bisector.norm(); - if (d1 < shortcut_length && d2 < shortcut_length) { - it0.insert((p1 + v1 * (shortcut_length / d1)).cast()) - .insert((p1 + v2 * (shortcut_length / d2)).cast()); - } else if (v1.squaredNorm() < v2.squaredNorm()) - it0.insert(point_on_line_at_dist(p1, p2, p0, shortcut_length).cast()); - else - it0.insert(point_on_line_at_dist(p1, p0, p2, shortcut_length).cast()); + if (l2v02 < clip_dist_scaled2) { + // (p0, p2) is short. + // Clip a sharp concave corner by possibly expanding the trimming region left of it0 and right of it2. + // Updates it0, it2 and num_to_process. + if (clip_narrow_corner(p1.cast(), it0, it2, unprocessed_range, l2v02, clip_dist_scaled)) + // Trimmed down to an empty polygon or to a single CCW triangle. + return; + } else { + // Clip an obtuse corner. + if (l2v02 > clip_dist_scaled2eps) { + Vec2d v1d = v1.cast(); + Vec2d v2d = v2.cast(); + // Sort v1d, v2d, shorter first. + bool swap = l2v1 > l2v2; + if (swap) { + std::swap(v1d, v2d); + std::swap(l2v1, l2v2); + } + double lv1 = sqrt(l2v1); + double lv2 = sqrt(l2v2); + // Bisector between v1 and v2. + Vec2d bisector = v1d / lv1 + v2d / lv2; + double l2bisector = bisector.squaredNorm(); + // Squared distance of the end point of v1 to the bisector. + double d2 = l2v1 - sqr(v1d.dot(bisector)) / l2bisector; + if (d2 < foot_dist_min2) { + // Height of the p1, p0, p2 triangle is tiny. Just remove p1. + } else if (d2 < 0.25 * clip_dist_scaled2 + SCALED_EPSILON) { + // The shorter vector is too close to the bisector. Trim the shorter vector fully, + // trim the longer vector partially. + // Intersection of a circle at p2 of radius = clip_dist_scaled + // with a ray (p1, p0), take the intersection after the foot point. + // The intersection shall always exist because |p2 - p1| > clip_dist_scaled. + const double b = - 2. * v1d.cast().dot(v2d); + double u = b * b - 4. * l2v2 * (double(l2v1) - clip_dist_scaled2); + assert(u > 0.); + // Take the second intersection along v2. + double t = (- b + sqrt(u)) / (2. * l2v2); + assert(t > 0. && t < 1.); + Point pt_new = p1 + (t * v2d).cast(); +#ifndef NDEBUG + double d2new = (pt_new - (swap ? p2 : p0)).cast().squaredNorm(); + assert(std::abs(d2new - clip_dist_scaled2) < sqr(10. * SCALED_EPSILON)); +#endif // NDEBUG + it2.insert(pt_new); + } else { + // Cut the corner with a line perpendicular to the bisector. + double t = sqrt(0.25 * clip_dist_scaled2 / d2); + assert(t > 0. && t < 1.); + Point p0 = p1 + (v1d * t).cast(); + Point p2 = p1 + (v2d * (t * lv2 / lv1)).cast(); + if (swap) + std::swap(p0, p2); + it2.insert(p2).insert(p0); + } + } else { + // Just remove p1. + assert(l2v02 >= clip_dist_scaled2 && l2v02 <= clip_dist_scaled2eps); } } - } else { - bool remove_poly = smooth_corner_complex(p1, it0, it2, shortcut_length); // edits p0_it and p2_it! - if (remove_poly) { - // don't convert ListPolygon into result - return; - } - } - // update: - it1 = it2; // next point to consider for whether it's an internal corner - } - else + it1 = it2; + } else + ++ it1; + } else ++ it1; - } while (it1 != polygon.begin()); + } + + if (polygon.size() == 3) { + // Check whether the last triangle is clockwise oriented (it is a hole) and its height is below clip_dist_scaled. + // If so, fill in the hole. + const Point p0 = *polygon.begin().prev(); + const Point p1 = *polygon.begin(); + const Point p2 = *polygon.begin().next(); + Vec2i64 v1 = (p0 - p1).cast(); + Vec2i64 v2 = (p2 - p1).cast(); + if (cross2(v1, v2) > 0) { + // CW triangle. Measure its height. + const Vec2i64 v3 = (p2 - p0).cast(); + int64_t l12 = v1.squaredNorm(); + int64_t l22 = v2.squaredNorm(); + int64_t l32 = v3.squaredNorm(); + if (l22 > l12 && l22 > l32) { + std::swap(v1, v2); + std::swap(l12, l22); + } else if (l32 > l12 && l32 > l22) { + v1 = v3; + l12 = l32; + } + auto h2 = l22 - sqr(double(v1.dot(v2))) / double(l12); + if (h2 < clip_dist_scaled2) + // CW triangle with a low height. Close the hole. + polygon.clear(); + } + } else if (polygon.size() < 3) + polygon.clear(); } } // namespace Slic3r diff --git a/src/libslic3r/MutablePolygon.hpp b/src/libslic3r/MutablePolygon.hpp index f40b89e74..a601f19e3 100644 --- a/src/libslic3r/MutablePolygon.hpp +++ b/src/libslic3r/MutablePolygon.hpp @@ -6,6 +6,10 @@ namespace Slic3r { +// Polygon implemented as a loop of double linked elements. +// All elements are allocated in a single std::vector<>, thus integer indices are used for +// referencing the previous and next element and inside iterators to survive reallocation +// of the vector. class MutablePolygon { public: @@ -55,6 +59,69 @@ public: friend class MutablePolygon; MutablePolygon *m_data; IndexType m_idx; + friend class range; + }; + + // Iterator range for maintaining a range of unprocessed items, see smooth_outward(). + class range + { + public: + range(MutablePolygon& poly) : range(poly.begin(), poly.end()) {} + range(MutablePolygon::iterator begin, MutablePolygon::iterator end) : m_begin(begin), m_end(end) {} + + // Start of a range, inclusive. If range is empty, then ! begin().valid(). + MutablePolygon::iterator begin() const { return m_begin; } + // End of a range, inclusive. If range is empty, then ! end().valid(). + MutablePolygon::iterator end() const { return m_end; } + // Is the range empty? + bool empty() const { return !m_begin.valid(); } + + // Return begin() and shorten the range by advancing front. + MutablePolygon::iterator process_next() { + assert(!this->empty()); + MutablePolygon::iterator out = m_begin; + this->advance_front(); + return out; + } + + void advance_front() { + assert(!this->empty()); + if (m_begin == m_end) + this->make_empty(); + else + ++ m_begin; + } + + void retract_back() { + assert(!this->empty()); + if (m_begin == m_end) + this->make_empty(); + else + -- m_end; + } + + MutablePolygon::iterator remove_front(MutablePolygon::iterator it) { + if (m_begin == it) + this->advance_front(); + return it.remove(); + } + + MutablePolygon::iterator remove_back(MutablePolygon::iterator it) { + if (m_end == it) + this->retract_back(); + return it.remove(); + } + + private: + // Range from begin to end, inclusive. + // If the range is valid, then both m_begin and m_end are invalid. + MutablePolygon::iterator m_begin; + MutablePolygon::iterator m_end; + + void make_empty() { + m_begin.m_idx = -1; + m_end.m_idx = -1; + } }; MutablePolygon() = default; @@ -63,26 +130,35 @@ public: template MutablePolygon(IT begin, IT end, size_t reserve = 0) { - m_size = IndexType(end - begin); - if (m_size > 0) { - m_head = 0; - m_data.reserve(std::max(m_size, reserve)); - auto i = IndexType(-1); - auto j = IndexType(1); - for (auto it = begin; it != end; ++ it) - m_data.push_back({ *it, i ++, j ++ }); - m_data.front().prev = m_size - 1; - m_data.back ().next = 0; + this->assign_inner(begin, end, reserve); + }; + + template + void assign(IT begin, IT end, size_t reserve = 0) { + m_data.clear(); + m_head = IndexType(-1); + m_head_free = { IndexType(-1) }; + this->assign_inner(begin, end, reserve); + }; + + void assign(const Polygon &rhs, size_t reserve = 0) { + assign(rhs.points.begin(), rhs.points.end(), reserve); + } + + void polygon(Polygon &out) const { + out.points.clear(); + if (this->valid()) { + out.points.reserve(this->size()); + auto it = this->cbegin(); + out.points.emplace_back(*it); + for (++ it; it != this->cbegin(); ++ it) + out.points.emplace_back(*it); } }; Polygon polygon() const { Polygon out; - if (this->valid()) { - out.points.reserve(this->size()); - for (auto it = this->cbegin(); it != this->cend(); ++ it) - out.points.emplace_back(*it); - } + this->polygon(out); return out; }; @@ -90,6 +166,7 @@ public: size_t size() const { return this->m_size; } size_t capacity() const { return this->m_data.capacity(); } bool valid() const { return this->m_size >= 3; } + void clear() { m_data.clear(); m_size = 0; m_head = IndexType(-1); m_head_free = IndexType(-1); } iterator begin() { return { this, m_head }; } const_iterator cbegin() const { return { this, m_head }; } @@ -108,8 +185,11 @@ public: private: struct LinkedPoint { + // 8 bytes PointType point; + // 4 bytes IndexType prev; + // 4 bytes IndexType next; }; std::vector m_data; @@ -122,6 +202,21 @@ private: LinkedPoint& at(IndexType i) { return m_data[i]; } const LinkedPoint& at(IndexType i) const { return m_data[i]; } + template + void assign_inner(IT begin, IT end, size_t reserve) { + m_size = IndexType(end - begin); + if (m_size > 0) { + m_head = 0; + m_data.reserve(std::max(m_size, reserve)); + auto i = IndexType(-1); + auto j = IndexType(1); + for (auto it = begin; it != end; ++ it) + m_data.push_back({ *it, i ++, j ++ }); + m_data.front().prev = m_size - 1; + m_data.back ().next = 0; + } + }; + IndexType remove(const IndexType i) { assert(i >= 0); assert(m_size > 0); @@ -213,13 +308,26 @@ inline bool operator!=(const MutablePolygon &p1, const MutablePolygon &p2) { ret void remove_duplicates(MutablePolygon &polygon); void remove_duplicates(MutablePolygon &polygon, double eps); -void smooth_outward(MutablePolygon &polygon, double shortcut_length); +void smooth_outward(MutablePolygon &polygon, coord_t clip_dist_scaled); -inline Polygon smooth_outward(const Polygon &polygon, double shortcut_length) +inline Polygon smooth_outward(Polygon polygon, coord_t clip_dist_scaled) { MutablePolygon mp(polygon, polygon.size() * 2); - smooth_outward(mp, shortcut_length); - return mp.polygon(); + smooth_outward(mp, clip_dist_scaled); + mp.polygon(polygon); + return polygon; +} + +inline Polygons smooth_outward(Polygons polygons, coord_t clip_dist_scaled) +{ + MutablePolygon mp; + for (Polygon &polygon : polygons) { + mp.assign(polygon, polygon.size() * 2); + smooth_outward(mp, clip_dist_scaled); + mp.polygon(polygon); + } + polygons.erase(std::remove_if(polygons.begin(), polygons.end(), [](const auto &p){ return p.empty(); }), polygons.end()); + return polygons; } } diff --git a/tests/libslic3r/test_mutable_polygon.cpp b/tests/libslic3r/test_mutable_polygon.cpp index 2214da6ef..238e3604f 100644 --- a/tests/libslic3r/test_mutable_polygon.cpp +++ b/tests/libslic3r/test_mutable_polygon.cpp @@ -1,5 +1,6 @@ #include +#include "libslic3r/Point.hpp" #include "libslic3r/MutablePolygon.hpp" using namespace Slic3r; @@ -143,3 +144,36 @@ SCENARIO("Remove degenerate points from MutablePolygon", "[MutablePolygon]") { } } } + +SCENARIO("smooth_outward", "[MutablePolygon]") { + GIVEN("Convex polygon") { + MutablePolygon p{ { 0, 0 }, { scaled(10.), 0 }, { 0, scaled(10.) } }; + WHEN("smooth_outward") { + MutablePolygon p2{ p }; + smooth_outward(p2, scaled(10.)); + THEN("Polygon is unmodified") { + REQUIRE(p == p2); + } + } + } + GIVEN("Sharp tiny concave polygon (hole)") { + MutablePolygon p{ { 0, 0 }, { 0, scaled(5.) }, { scaled(10.), 0 } }; + WHEN("smooth_outward") { + MutablePolygon p2{ p }; + smooth_outward(p2, scaled(10.)); + THEN("Hole is closed") { + REQUIRE(p2.empty()); + } + } + } + GIVEN("Two polygons") { + Polygons p{ { { 0, 0 }, { scaled(10.), 0 }, { 0, scaled(10.) } }, + { { 0, 0 }, { 0, scaled(5.) }, { scaled(10.), 0 } } }; + WHEN("smooth_outward") { + p = smooth_outward(p, scaled(10.)); + THEN("CCW contour unmodified, CW contour removed.") { + REQUIRE(p == Polygons{ { { 0, 0 }, { scaled(10.), 0 }, { 0, scaled(10.) } } }); + } + } + } +} From d99895805ce50534f5ba9c28c926e646f0031ac9 Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Wed, 3 Mar 2021 15:17:45 +0100 Subject: [PATCH 3/6] Follow-up of 6a46b71dc1b72834f36e16f1bbf3e13f40e64c1d - Fix build on non-Windows platforms --- src/libslic3r/GCode/GCodeProcessor.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index 982a5067c..73ad25496 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -2168,10 +2168,10 @@ void GCodeProcessor::process_G28(const GCodeReader::GCodeLine& line) if (!found) new_line_raw += " X0 Y0 Z0"; - GCodeReader::GCodeLine new_line; + GCodeReader::GCodeLine new_gline; GCodeReader reader; - reader.parse_line(new_line_raw.c_str(), new_line, [](GCodeReader&, const GCodeReader::GCodeLine&) {}); - process_G1(new_line); + reader.parse_line(new_line_raw, [&](GCodeReader& reader, const GCodeReader::GCodeLine& gline) { new_gline = gline; }); + process_G1(new_gline); } void GCodeProcessor::process_G90(const GCodeReader::GCodeLine& line) From a7255235e5e010a545d68f3932efc11c33e5f216 Mon Sep 17 00:00:00 2001 From: tamasmeszaros Date: Wed, 3 Mar 2021 15:19:24 +0100 Subject: [PATCH 4/6] Remove gcc warning about uninitialized values --- src/libslic3r/SLA/IndexedMesh.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libslic3r/SLA/IndexedMesh.cpp b/src/libslic3r/SLA/IndexedMesh.cpp index 485fa98ed..2f47c6387 100644 --- a/src/libslic3r/SLA/IndexedMesh.cpp +++ b/src/libslic3r/SLA/IndexedMesh.cpp @@ -122,7 +122,7 @@ IndexedMesh::hit_result IndexedMesh::query_ray_hit(const Vec3d &s, const Vec3d &dir) const { assert(is_approx(dir.norm(), 1.)); - igl::Hit hit; + igl::Hit hit{-1, -1, 0.f, 0.f, 0.f}; hit.t = std::numeric_limits::infinity(); #ifdef SLIC3R_HOLE_RAYCASTER From abd5a9a46e16bab66988bf338516b1e0d1f5b4e8 Mon Sep 17 00:00:00 2001 From: Lukas Matena Date: Fri, 26 Feb 2021 08:23:37 +0100 Subject: [PATCH 5/6] Add a notification when custom support enforcers are not used due to supports being off It is now emitted from Print::validate and has a hyperlink to enable supports --- src/libslic3r/Print.cpp | 19 ++++++++- src/libslic3r/Print.hpp | 2 +- src/libslic3r/PrintBase.hpp | 2 +- src/libslic3r/PrintObject.cpp | 18 +++++++++ src/libslic3r/SLAPrint.cpp | 2 +- src/libslic3r/SLAPrint.hpp | 2 +- src/slic3r/GUI/BackgroundSlicingProcess.cpp | 4 +- src/slic3r/GUI/BackgroundSlicingProcess.hpp | 2 +- src/slic3r/GUI/NotificationManager.hpp | 4 +- src/slic3r/GUI/Plater.cpp | 45 ++++++++++++++++++++- 10 files changed, 89 insertions(+), 11 deletions(-) diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 643cc9a97..89a8dc77b 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -1245,7 +1245,7 @@ static inline bool sequential_print_vertical_clearance_valid(const Print &print) } // Precondition: Print::validate() requires the Print::apply() to be called its invocation. -std::string Print::validate() const +std::string Print::validate(std::string* warning) const { if (m_objects.empty()) return L("All objects are outside of the print volume."); @@ -1440,7 +1440,22 @@ std::string Print::validate() const } } } - + + // Do we have custom support data that would not be used? + // Notify the user in that case. + if (! object->has_support() && warning) { + for (const ModelVolume* mv : object->model_object()->volumes) { + bool has_enforcers = mv->is_support_enforcer() + || (mv->is_model_part() + && ! mv->supported_facets.empty() + && ! mv->supported_facets.get_facets(*mv, EnforcerBlockerType::ENFORCER).indices.empty()); + if (has_enforcers) { + *warning = "_SUPPORTS_OFF"; + break; + } + } + } + // validate first_layer_height double first_layer_height = object->config().get_abs_value("first_layer_height"); double first_layer_min_nozzle_diameter; diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index e8e81a529..9f22ddf13 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -444,7 +444,7 @@ public: bool has_brim() const; // Returns an empty string if valid, otherwise returns an error message. - std::string validate() const override; + std::string validate(std::string* warning = nullptr) const override; double skirt_first_layer_height() const; Flow brim_flow() const; Flow skirt_flow() const; diff --git a/src/libslic3r/PrintBase.hpp b/src/libslic3r/PrintBase.hpp index 2aff13ae9..e0aa56ba5 100644 --- a/src/libslic3r/PrintBase.hpp +++ b/src/libslic3r/PrintBase.hpp @@ -366,7 +366,7 @@ public: virtual std::vector print_object_ids() const = 0; // Validate the print, return empty string if valid, return error if process() cannot (or should not) be started. - virtual std::string validate() const { return std::string(); } + virtual std::string validate(std::string* warning = nullptr) const { return std::string(); } enum ApplyStatus { // No change after the Print::apply() call. diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index c963418c3..52bdc87e7 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -430,6 +430,24 @@ void PrintObject::generate_support_material() if (layer->empty()) throw Slic3r::SlicingError("Levitating objects cannot be printed without supports."); #endif + + // Do we have custom support data that would not be used? + // Notify the user in that case. + if (! this->has_support()) { + for (const ModelVolume* mv : this->model_object()->volumes) { + bool has_enforcers = mv->is_support_enforcer() + || (mv->is_model_part() + && ! mv->supported_facets.empty() + && ! mv->supported_facets.get_facets(*mv, EnforcerBlockerType::ENFORCER).indices.empty()); + if (has_enforcers) { + this->active_step_add_warning(PrintStateBase::WarningLevel::CRITICAL, + L("An object has custom support enforcers which will not be used " + "because supports are off. Consider turning them on.") + "\n" + + (L("Object name")) + ": " + this->model_object()->name); + break; + } + } + } } this->set_done(posSupportMaterial); } diff --git a/src/libslic3r/SLAPrint.cpp b/src/libslic3r/SLAPrint.cpp index 65fac73f3..16b068cb9 100644 --- a/src/libslic3r/SLAPrint.cpp +++ b/src/libslic3r/SLAPrint.cpp @@ -617,7 +617,7 @@ std::string SLAPrint::output_filename(const std::string &filename_base) const return this->PrintBase::output_filename(m_print_config.output_filename_format.value, ".sl1", filename_base, &config); } -std::string SLAPrint::validate() const +std::string SLAPrint::validate(std::string*) const { for(SLAPrintObject * po : m_objects) { diff --git a/src/libslic3r/SLAPrint.hpp b/src/libslic3r/SLAPrint.hpp index 742670e2c..bed66ab4f 100644 --- a/src/libslic3r/SLAPrint.hpp +++ b/src/libslic3r/SLAPrint.hpp @@ -458,7 +458,7 @@ public: const SLAPrintStatistics& print_statistics() const { return m_print_statistics; } - std::string validate() const override; + std::string validate(std::string* warning = nullptr) const override; // An aggregation of SliceRecord-s from all the print objects for each // occupied layer. Slice record levels dont have to match exactly. diff --git a/src/slic3r/GUI/BackgroundSlicingProcess.cpp b/src/slic3r/GUI/BackgroundSlicingProcess.cpp index 87a322023..089fba656 100644 --- a/src/slic3r/GUI/BackgroundSlicingProcess.cpp +++ b/src/slic3r/GUI/BackgroundSlicingProcess.cpp @@ -430,10 +430,10 @@ bool BackgroundSlicingProcess::empty() const return m_print->empty(); } -std::string BackgroundSlicingProcess::validate() +std::string BackgroundSlicingProcess::validate(std::string* warning) { assert(m_print != nullptr); - return m_print->validate(); + return m_print->validate(warning); } // Apply config over the print. Returns false, if the new config values caused any of the already diff --git a/src/slic3r/GUI/BackgroundSlicingProcess.hpp b/src/slic3r/GUI/BackgroundSlicingProcess.hpp index b3f8a0a6b..d3819f15c 100644 --- a/src/slic3r/GUI/BackgroundSlicingProcess.hpp +++ b/src/slic3r/GUI/BackgroundSlicingProcess.hpp @@ -131,7 +131,7 @@ public: bool empty() const; // Validate the print. Returns an empty string if valid, returns an error message if invalid. // Call validate before calling start(). - std::string validate(); + std::string validate(std::string* warning = nullptr); // Set the export path of the G-code. // Once the path is set, the G-code diff --git a/src/slic3r/GUI/NotificationManager.hpp b/src/slic3r/GUI/NotificationManager.hpp index 31dace42e..9ce278792 100644 --- a/src/slic3r/GUI/NotificationManager.hpp +++ b/src/slic3r/GUI/NotificationManager.hpp @@ -71,7 +71,9 @@ enum class NotificationType // Notification that custom supports/seams were deleted after mesh repair. CustomSupportsAndSeamRemovedAfterRepair, // Notification that auto adding of color changes is impossible - EmptyAutoColorChange + EmptyAutoColorChange, + // Notification emitted by Print::validate + PrintValidateWarning }; class NotificationManager diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 762acfdf1..93d0e0881 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -1599,6 +1599,8 @@ struct Plater::priv void suppress_snapshots() { this->m_prevent_snapshots++; } void allow_snapshots() { this->m_prevent_snapshots--; } + void process_validation_warning(const std::string& warning) const; + bool background_processing_enabled() const { return this->get_config("background_processing") == "1"; } void update_print_volume_state(); void schedule_background_process(); @@ -2787,6 +2789,41 @@ void Plater::priv::update_print_volume_state() this->q->model().update_print_volume_state(print_volume); } + +void Plater::priv::process_validation_warning(const std::string& warning) const +{ + if (warning.empty()) + notification_manager->close_notification_of_type(NotificationType::PrintValidateWarning); + else { + std::string text = warning; + std::string hypertext = ""; + std::function action_fn = [](wxEvtHandler*){ return false; }; + + if (text == "_SUPPORTS_OFF") { + text = _u8L("An object has custom support enforcers which will not be used " + "because supports are disabled.")+"\n"; + hypertext = _u8L("Enable supports for enforcers only"); + action_fn = [](wxEvtHandler*) { + Tab* print_tab = wxGetApp().get_tab(Preset::TYPE_PRINT); + assert(print_tab); + DynamicPrintConfig& config = wxGetApp().preset_bundle->prints.get_edited_preset().config; + config.set_key_value("support_material", new ConfigOptionBool(true)); + config.set_key_value("support_material_auto", new ConfigOptionBool(false)); + print_tab->on_value_change("support_material", config.opt_bool("support_material")); + print_tab->on_value_change("support_material_auto", config.opt_bool("support_material_auto")); + return true; + }; + } + + notification_manager->push_notification( + NotificationType::PrintValidateWarning, + NotificationManager::NotificationLevel::ImportantNotification, + text, hypertext, action_fn + ); + } +} + + // Update background processing thread from the current config and Model. // Returns a bitmask of UpdateBackgroundProcessReturnState. unsigned int Plater::priv::update_background_process(bool force_validation, bool postpone_error_messages) @@ -2829,17 +2866,23 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool // The delayed error message is no more valid. this->delayed_error_message.clear(); // The state of the Print changed, and it is non-zero. Let's validate it and give the user feedback on errors. - std::string err = this->background_process.validate(); + std::string warning; + std::string err = this->background_process.validate(&warning); if (err.empty()) { notification_manager->set_all_slicing_errors_gray(true); if (invalidated != Print::APPLY_STATUS_UNCHANGED && this->background_processing_enabled()) return_state |= UPDATE_BACKGROUND_PROCESS_RESTART; + + // Pass a warning from validation and either show a notification, + // or hide the old one. + process_validation_warning(warning); } else { // The print is not valid. // Show error as notification. notification_manager->push_slicing_error_notification(err); return_state |= UPDATE_BACKGROUND_PROCESS_INVALID; } + } else if (! this->delayed_error_message.empty()) { // Reusing the old state. return_state |= UPDATE_BACKGROUND_PROCESS_INVALID; From 70573484c29fc7f80a634a4eedbbb9c1d495c23e Mon Sep 17 00:00:00 2001 From: Lukas Matena Date: Tue, 2 Mar 2021 11:47:09 +0100 Subject: [PATCH 6/6] Show an error notification when attempting to save 3MF while editing SLA support points The notification disappears when it is no longer valid. --- src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp | 2 ++ src/slic3r/GUI/Gizmos/GLGizmosManager.cpp | 8 +++++--- src/slic3r/GUI/NotificationManager.hpp | 4 +++- src/slic3r/GUI/Plater.cpp | 4 +++- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp b/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp index b5dbab284..b200623f4 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp @@ -16,6 +16,7 @@ #include "slic3r/GUI/GUI_ObjectSettings.hpp" #include "slic3r/GUI/GUI_ObjectList.hpp" #include "slic3r/GUI/Plater.hpp" +#include "slic3r/GUI/NotificationManager.hpp" #include "libslic3r/PresetBundle.hpp" #include "libslic3r/SLAPrint.hpp" @@ -1161,6 +1162,7 @@ void GLGizmoSlaSupports::disable_editing_mode() m_c->instances_hider()->show_supports(true); m_parent.set_as_dirty(); } + wxGetApp().plater()->get_notification_manager()->close_notification_of_type(NotificationType::QuitSLAManualMode); } diff --git a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp index abbc8599b..9dc785b3f 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosManager.cpp @@ -1147,9 +1147,11 @@ bool GLGizmosManager::is_in_editing_mode(bool error_notification) const return false; if (error_notification) - wxGetApp().plater()->get_notification_manager()->push_slicing_error_notification( - _u8L("You are currently editing SLA support points. Please, apply or discard " - "your changes first.")); + wxGetApp().plater()->get_notification_manager()->push_notification( + NotificationType::QuitSLAManualMode, + NotificationManager::NotificationLevel::ErrorNotification, + _u8L("You are currently editing SLA support points. Please, " + "apply or discard your changes first.")); return true; } diff --git a/src/slic3r/GUI/NotificationManager.hpp b/src/slic3r/GUI/NotificationManager.hpp index 9ce278792..62c4ea845 100644 --- a/src/slic3r/GUI/NotificationManager.hpp +++ b/src/slic3r/GUI/NotificationManager.hpp @@ -73,7 +73,9 @@ enum class NotificationType // Notification that auto adding of color changes is impossible EmptyAutoColorChange, // Notification emitted by Print::validate - PrintValidateWarning + PrintValidateWarning, + // Notification telling user to quit SLA supports manual editing + QuitSLAManualMode }; class NotificationManager diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 93d0e0881..15b3a17c7 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -5418,7 +5418,9 @@ void Plater::export_amf() void Plater::export_3mf(const boost::filesystem::path& output_path) { - if (p->model.objects.empty()) { return; } + if (p->model.objects.empty() + || canvas3D()->get_gizmos_manager().is_in_editing_mode(true)) + return; wxString path; bool export_config = true;