diff --git a/src/libslic3r/ClipperUtils.cpp b/src/libslic3r/ClipperUtils.cpp index b863b4712..55bb4b446 100644 --- a/src/libslic3r/ClipperUtils.cpp +++ b/src/libslic3r/ClipperUtils.cpp @@ -107,8 +107,7 @@ void AddOuterPolyNodeToExPolygons(ClipperLib::PolyNode& polynode, ExPolygons* ex } } -ExPolygons -PolyTreeToExPolygons(ClipperLib::PolyTree& polytree) +ExPolygons PolyTreeToExPolygons(ClipperLib::PolyTree& polytree) { ExPolygons retval; for (int i = 0; i < polytree.ChildCount(); ++i) @@ -151,8 +150,7 @@ Slic3r::Polylines ClipperPaths_to_Slic3rPolylines(const ClipperLib::Paths &input return retval; } -ExPolygons -ClipperPaths_to_Slic3rExPolygons(const ClipperLib::Paths &input) +ExPolygons ClipperPaths_to_Slic3rExPolygons(const ClipperLib::Paths &input) { // init Clipper ClipperLib::Clipper clipper; @@ -167,8 +165,7 @@ ClipperPaths_to_Slic3rExPolygons(const ClipperLib::Paths &input) return PolyTreeToExPolygons(polytree); } -ClipperLib::Path -Slic3rMultiPoint_to_ClipperPath(const MultiPoint &input) +ClipperLib::Path Slic3rMultiPoint_to_ClipperPath(const MultiPoint &input) { ClipperLib::Path retval; for (Points::const_iterator pit = input.points.begin(); pit != input.points.end(); ++pit) @@ -176,8 +173,7 @@ Slic3rMultiPoint_to_ClipperPath(const MultiPoint &input) return retval; } -ClipperLib::Path -Slic3rMultiPoint_to_ClipperPath_reversed(const Slic3r::MultiPoint &input) +ClipperLib::Path Slic3rMultiPoint_to_ClipperPath_reversed(const Slic3r::MultiPoint &input) { ClipperLib::Path output; output.reserve(input.points.size()); @@ -521,7 +517,7 @@ T _clipper_do(const ClipperLib::ClipType clipType, // Fix of #117: A large fractal pyramid takes ages to slice // The Clipper library has difficulties processing overlapping polygons. -// Namely, the function Clipper::JoinCommonEdges() has potentially a terrible time complexity if the output +// Namely, the function ClipperLib::JoinCommonEdges() has potentially a terrible time complexity if the output // of the operation is of the PolyTree type. // This function implmenets a following workaround: // 1) Peform the Clipper operation with the output to Paths. This method handles overlaps in a reasonable time. @@ -918,4 +914,304 @@ Polygons top_level_islands(const Slic3r::Polygons &polygons) return out; } +// Outer offset shall not split the input contour into multiples. It is expected, that the solution will be non empty and it will contain just a single polygon. +ClipperLib::Paths fix_after_outer_offset(const ClipperLib::Path &input, ClipperLib::PolyFillType filltype, bool reverse_result) +{ + ClipperLib::Paths solution; + if (! input.empty()) { + ClipperLib::Clipper clipper; + clipper.AddPath(input, ClipperLib::ptSubject, true); + clipper.ReverseSolution(reverse_result); + clipper.Execute(ClipperLib::ctUnion, solution, filltype, filltype); + } + return solution; +} + +// Inner offset may split the source contour into multiple contours, but one shall not be inside the other. +ClipperLib::Paths fix_after_inner_offset(const ClipperLib::Path &input, ClipperLib::PolyFillType filltype, bool reverse_result) +{ + ClipperLib::Paths solution; + if (! input.empty()) { + ClipperLib::Clipper clipper; + clipper.AddPath(input, ClipperLib::ptSubject, true); + ClipperLib::IntRect r = clipper.GetBounds(); + r.left -= 10; r.top -= 10; r.right += 10; r.bottom += 10; + if (filltype == ClipperLib::pftPositive) + clipper.AddPath({ ClipperLib::IntPoint(r.left, r.bottom), ClipperLib::IntPoint(r.left, r.top), ClipperLib::IntPoint(r.right, r.top), ClipperLib::IntPoint(r.right, r.bottom) }, ClipperLib::ptSubject, true); + else + clipper.AddPath({ ClipperLib::IntPoint(r.left, r.bottom), ClipperLib::IntPoint(r.right, r.bottom), ClipperLib::IntPoint(r.right, r.top), ClipperLib::IntPoint(r.left, r.top) }, ClipperLib::ptSubject, true); + clipper.ReverseSolution(reverse_result); + clipper.Execute(ClipperLib::ctUnion, solution, filltype, filltype); + if (! solution.empty()) + solution.erase(solution.begin()); + } + return solution; +} + +ClipperLib::Path mittered_offset_path_scaled(const Points &contour, const std::vector &deltas, double miter_limit) +{ + assert(contour.size() == deltas.size()); +#ifndef NDEBUG + // Verify that the deltas are either all positive, or all negative. + bool positive = false; + bool negative = false; + for (float delta : deltas) + if (delta < 0.f) + negative = true; + else if (delta > 0.f) + positive = true; + assert(! (negative && positive)); +#endif /* NDEBUG */ + + ClipperLib::Path out; + + if (deltas.size() > 2) + { + out.reserve(contour.size() * 2); + + // Clamp miter limit to 2. + miter_limit = (miter_limit > 2.) ? 2. / (miter_limit * miter_limit) : 0.5; + + // perpenduclar vector + auto perp = [](const Vec2d &v) -> Vec2d { return Vec2d(v.y(), - v.x()); }; + + // Add a new point to the output, scale by CLIPPER_OFFSET_SCALE and round to ClipperLib::cInt. + auto add_offset_point = [&out](Vec2d pt) { + pt *= double(CLIPPER_OFFSET_SCALE); + pt += Vec2d(0.5 - (pt.x() < 0), 0.5 - (pt.y() < 0)); + out.emplace_back(ClipperLib::cInt(pt.x()), ClipperLib::cInt(pt.y())); + }; + + // Minimum edge length, squared. + double lmin = *std::max_element(deltas.begin(), deltas.end()) * CLIPPER_OFFSET_SHORTEST_EDGE_FACTOR; + double l2min = lmin * lmin; + // Minimum angle to consider two edges to be parallel. + double sin_min_parallel = EPSILON + 1. / double(CLIPPER_OFFSET_SCALE); + + // Find the last point further from pt by l2min. + Vec2d pt = contour.front().cast(); + size_t iprev = contour.size() - 1; + Vec2d ptprev; + for (; iprev > 0; -- iprev) { + ptprev = contour[iprev].cast(); + if ((ptprev - pt).squaredNorm() > l2min) + break; + } + + if (iprev != 0) { + size_t ilast = iprev; + // Normal to the (pt - ptprev) segment. + Vec2d nprev = perp(pt - ptprev).normalized(); + for (size_t i = 0; ; ) { + // Find the next point further from pt by l2min. + size_t j = i + 1; + Vec2d ptnext; + for (; j <= ilast; ++ j) { + ptnext = contour[j].cast(); + double l2 = (ptnext - pt).squaredNorm(); + if (l2 > l2min) + break; + } + if (j > ilast) + ptnext = contour.front().cast(); + + // Normal to the (ptnext - pt) segment. + Vec2d nnext = perp(ptnext - pt).normalized(); + + double delta = deltas[i]; + double sin_a = clamp(-1., 1., cross2(nprev, nnext)); + double convex = sin_a * delta; + if (convex <= - sin_min_parallel) { + // Concave corner. + add_offset_point(pt + nprev * delta); + add_offset_point(pt); + add_offset_point(pt + nnext * delta); + } else if (convex < sin_min_parallel) { + // Nearly parallel. + add_offset_point((nprev.dot(nnext) > 0.) ? (pt + nprev * delta) : pt); + } else { + // Convex corner + double dot = nprev.dot(nnext); + double r = 1. + dot; + if (r >= miter_limit) + add_offset_point(pt + (nprev + nnext) * (delta / r)); + else { + double dx = std::tan(std::atan2(sin_a, dot) / 4.); + Vec2d newpt1 = pt + (nprev - perp(nprev) * dx) * delta; + Vec2d newpt2 = pt + (nnext + perp(nnext) * dx) * delta; +#ifndef NDEBUG + Vec2d vedge = 0.5 * (newpt1 + newpt2) - pt; + double dist_norm = vedge.norm(); + assert(std::abs(dist_norm - delta) < EPSILON); +#endif /* NDEBUG */ + add_offset_point(newpt1); + add_offset_point(newpt2); + } + } + + if (i == ilast) + break; + + ptprev = pt; + nprev = nnext; + pt = ptnext; + i = j; + } + } + } + + return out; +} + +Polygons variable_offset_inner(const ExPolygon &expoly, const std::vector> &deltas, double miter_limit) +{ +#ifndef NDEBUG + // Verify that the deltas are all non positive. + for (const std::vector &ds : deltas) + for (float delta : ds) + assert(delta <= 0.); + assert(expoly.holes.size() + 1 == deltas.size()); +#endif /* NDEBUG */ + + // 1) Offset the outer contour. + ClipperLib::Paths contours = fix_after_inner_offset(mittered_offset_path_scaled(expoly.contour.points, deltas.front(), miter_limit), ClipperLib::pftNegative, true); + + // 2) Offset the holes one by one, collect the results. + ClipperLib::Paths holes; + holes.reserve(expoly.holes.size()); + for (const Polygon& hole : expoly.holes) + append(holes, fix_after_outer_offset(mittered_offset_path_scaled(hole, deltas[1 + &hole - expoly.holes.data()], miter_limit), ClipperLib::pftPositive, false)); + + // 3) Subtract holes from the contours. + ClipperLib::Paths output; + if (holes.empty()) + output = std::move(contours); + else { + ClipperLib::Clipper clipper; + clipper.Clear(); + clipper.AddPaths(contours, ClipperLib::ptSubject, true); + clipper.AddPaths(holes, ClipperLib::ptClip, true); + clipper.Execute(ClipperLib::ctDifference, output, ClipperLib::pftNonZero, ClipperLib::pftNonZero); + } + + // 4) Unscale the output. + unscaleClipperPolygons(output); + return ClipperPaths_to_Slic3rPolygons(output); +} + +Polygons variable_offset_outer(const ExPolygon &expoly, const std::vector> &deltas, double miter_limit) +{ +#ifndef NDEBUG + // Verify that the deltas are all non positive. +for (const std::vector& ds : deltas) + for (float delta : ds) + assert(delta >= 0.); + assert(expoly.holes.size() + 1 == deltas.size()); +#endif /* NDEBUG */ + + // 1) Offset the outer contour. + ClipperLib::Paths contours = fix_after_outer_offset(mittered_offset_path_scaled(expoly.contour.points, deltas.front(), miter_limit), ClipperLib::pftPositive, false); + + // 2) Offset the holes one by one, collect the results. + ClipperLib::Paths holes; + holes.reserve(expoly.holes.size()); + for (const Polygon& hole : expoly.holes) + append(holes, fix_after_inner_offset(mittered_offset_path_scaled(hole, deltas[1 + &hole - expoly.holes.data()], miter_limit), ClipperLib::pftPositive, true)); + + // 3) Subtract holes from the contours. + ClipperLib::Paths output; + if (holes.empty()) + output = std::move(contours); + else { + ClipperLib::Clipper clipper; + clipper.Clear(); + clipper.AddPaths(contours, ClipperLib::ptSubject, true); + clipper.AddPaths(holes, ClipperLib::ptClip, true); + clipper.Execute(ClipperLib::ctDifference, output, ClipperLib::pftNonZero, ClipperLib::pftNonZero); + } + + // 4) Unscale the output. + unscaleClipperPolygons(output); + return ClipperPaths_to_Slic3rPolygons(output); +} + +ExPolygons variable_offset_outer_ex(const ExPolygon &expoly, const std::vector> &deltas, double miter_limit) +{ +#ifndef NDEBUG + // Verify that the deltas are all non positive. +for (const std::vector& ds : deltas) + for (float delta : ds) + assert(delta >= 0.); + assert(expoly.holes.size() + 1 == deltas.size()); +#endif /* NDEBUG */ + + // 1) Offset the outer contour. + ClipperLib::Paths contours = fix_after_outer_offset(mittered_offset_path_scaled(expoly.contour.points, deltas.front(), miter_limit), ClipperLib::pftPositive, false); + + // 2) Offset the holes one by one, collect the results. + ClipperLib::Paths holes; + holes.reserve(expoly.holes.size()); + for (const Polygon& hole : expoly.holes) + append(holes, fix_after_inner_offset(mittered_offset_path_scaled(hole, deltas[1 + &hole - expoly.holes.data()], miter_limit), ClipperLib::pftPositive, true)); + + // 3) Subtract holes from the contours. + unscaleClipperPolygons(contours); + ExPolygons output; + if (holes.empty()) { + output.reserve(contours.size()); + for (ClipperLib::Path &path : contours) + output.emplace_back(ClipperPath_to_Slic3rPolygon(path)); + } else { + ClipperLib::Clipper clipper; + unscaleClipperPolygons(holes); + clipper.AddPaths(contours, ClipperLib::ptSubject, true); + clipper.AddPaths(holes, ClipperLib::ptClip, true); + ClipperLib::PolyTree polytree; + clipper.Execute(ClipperLib::ctDifference, polytree, ClipperLib::pftNonZero, ClipperLib::pftNonZero); + output = PolyTreeToExPolygons(polytree); + } + + return output; +} + + +ExPolygons variable_offset_inner_ex(const ExPolygon &expoly, const std::vector> &deltas, double miter_limit) +{ +#ifndef NDEBUG + // Verify that the deltas are all non positive. +for (const std::vector& ds : deltas) + for (float delta : ds) + assert(delta <= 0.); + assert(expoly.holes.size() + 1 == deltas.size()); +#endif /* NDEBUG */ + + // 1) Offset the outer contour. + ClipperLib::Paths contours = fix_after_inner_offset(mittered_offset_path_scaled(expoly.contour.points, deltas.front(), miter_limit), ClipperLib::pftNegative, false); + + // 2) Offset the holes one by one, collect the results. + ClipperLib::Paths holes; + holes.reserve(expoly.holes.size()); + for (const Polygon& hole : expoly.holes) + append(holes, fix_after_outer_offset(mittered_offset_path_scaled(hole, deltas[1 + &hole - expoly.holes.data()], miter_limit), ClipperLib::pftNegative, true)); + + // 3) Subtract holes from the contours. + unscaleClipperPolygons(contours); + ExPolygons output; + if (holes.empty()) { + output.reserve(contours.size()); + for (ClipperLib::Path &path : contours) + output.emplace_back(ClipperPath_to_Slic3rPolygon(path)); + } else { + ClipperLib::Clipper clipper; + unscaleClipperPolygons(holes); + clipper.AddPaths(contours, ClipperLib::ptSubject, true); + clipper.AddPaths(holes, ClipperLib::ptClip, true); + ClipperLib::PolyTree polytree; + clipper.Execute(ClipperLib::ctDifference, polytree, ClipperLib::pftNonZero, ClipperLib::pftNonZero); + output = PolyTreeToExPolygons(polytree); + } + + return output; +} + } diff --git a/src/libslic3r/ClipperUtils.hpp b/src/libslic3r/ClipperUtils.hpp index d8f8a8f94..5a41a6a90 100644 --- a/src/libslic3r/ClipperUtils.hpp +++ b/src/libslic3r/ClipperUtils.hpp @@ -238,6 +238,11 @@ void safety_offset(ClipperLib::Paths* paths); Polygons top_level_islands(const Slic3r::Polygons &polygons); +Polygons variable_offset_inner(const ExPolygon &expoly, const std::vector> &deltas, double miter_limit = 2.); +Polygons variable_offset_outer(const ExPolygon &expoly, const std::vector> &deltas, double miter_limit = 2.); +ExPolygons variable_offset_outer_ex(const ExPolygon &expoly, const std::vector> &deltas, double miter_limit = 2.); +ExPolygons variable_offset_inner_ex(const ExPolygon &expoly, const std::vector> &deltas, double miter_limit = 2.); + } #endif diff --git a/src/libslic3r/ExPolygon.hpp b/src/libslic3r/ExPolygon.hpp index c510b848f..08c4f7a07 100644 --- a/src/libslic3r/ExPolygon.hpp +++ b/src/libslic3r/ExPolygon.hpp @@ -301,6 +301,15 @@ inline bool expolygons_contain(ExPolygons &expolys, const Point &pt) return false; } +inline ExPolygons expolygons_simplify(const ExPolygons &expolys, double tolerance) +{ + ExPolygons out; + out.reserve(expolys.size()); + for (const ExPolygon &exp : expolys) + exp.simplify(tolerance, &out); + return out; +} + extern BoundingBox get_extents(const ExPolygon &expolygon); extern BoundingBox get_extents(const ExPolygons &expolygons); extern BoundingBox get_extents_rotated(const ExPolygon &poly, double angle); diff --git a/src/libslic3r/Polygon.hpp b/src/libslic3r/Polygon.hpp index 19be3068b..ad1b32feb 100644 --- a/src/libslic3r/Polygon.hpp +++ b/src/libslic3r/Polygon.hpp @@ -102,6 +102,15 @@ inline void polygons_append(Polygons &dst, Polygons &&src) } } +inline Polygons polygons_simplify(const Polygons &polys, double tolerance) +{ + Polygons out; + out.reserve(polys.size()); + for (const Polygon &p : polys) + polygons_append(out, p.simplify(tolerance)); + return out; +} + inline void polygons_rotate(Polygons &polys, double angle) { const double cos_angle = cos(angle); diff --git a/tests/fff_print/test_flow.cpp b/tests/fff_print/test_flow.cpp index 4541868dc..969ae3c82 100644 --- a/tests/fff_print/test_flow.cpp +++ b/tests/fff_print/test_flow.cpp @@ -95,7 +95,6 @@ SCENARIO(" Bridge flow specifics.", "[Flow]") { SCENARIO("Flow: Flow math for non-bridges", "[Flow]") { GIVEN("Nozzle Diameter of 0.4, a desired width of 1mm and layer height of 0.5") { ConfigOptionFloatOrPercent width(1.0, false); - float spacing = 0.4f; float nozzle_diameter = 0.4f; float bridge_flow = 0.f; float layer_height = 0.5f; @@ -119,7 +118,6 @@ SCENARIO("Flow: Flow math for non-bridges", "[Flow]") { } /// Check the min/max GIVEN("Nozzle Diameter of 0.25") { - float spacing = 0.4f; float nozzle_diameter = 0.25f; float bridge_flow = 0.f; float layer_height = 0.5f; @@ -161,7 +159,6 @@ SCENARIO("Flow: Flow math for non-bridges", "[Flow]") { SCENARIO("Flow: Flow math for bridges", "[Flow]") { GIVEN("Nozzle Diameter of 0.4, a desired width of 1mm and layer height of 0.5") { auto width = ConfigOptionFloatOrPercent(1.0, false); - float spacing = 0.4f; float nozzle_diameter = 0.4f; float bridge_flow = 1.0f; float layer_height = 0.5f; diff --git a/tests/fff_print/test_skirt_brim.cpp b/tests/fff_print/test_skirt_brim.cpp index 9fabb7aa6..11a83fe29 100644 --- a/tests/fff_print/test_skirt_brim.cpp +++ b/tests/fff_print/test_skirt_brim.cpp @@ -14,8 +14,8 @@ using namespace Slic3r; /// Helper method to find the tool used for the brim (always the first extrusion) static int get_brim_tool(const std::string &gcode) { - int brim_tool = -1; - int tool = -1; + int brim_tool = -1; + int tool = -1; GCodeReader parser; parser.parse_buffer(gcode, [&tool, &brim_tool] (Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line) { @@ -29,7 +29,7 @@ static int get_brim_tool(const std::string &gcode) return brim_tool; } -TEST_CASE("Skirt height is honored") { +TEST_CASE("Skirt height is honored", "[Skirt]") { DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config(); config.set_deserialize({ { "skirts", 1 }, @@ -60,7 +60,7 @@ TEST_CASE("Skirt height is honored") { REQUIRE(layers_with_skirt.size() == (size_t)config.opt_int("skirt_height")); } -SCENARIO("Original Slic3r Skirt/Brim tests", "[!mayfail]") { +SCENARIO("Original Slic3r Skirt/Brim tests", "[SkirtBrim]") { GIVEN("A default configuration") { DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config(); config.set_num_extruders(4); @@ -73,7 +73,8 @@ SCENARIO("Original Slic3r Skirt/Brim tests", "[!mayfail]") { { "first_layer_speed", "100%" }, // remove noise from top/solid layers { "top_solid_layers", 0 }, - { "bottom_solid_layers", 1 } + { "bottom_solid_layers", 1 }, + { "start_gcode", "T[initial_tool]\n" } }); WHEN("Brim width is set to 5") { @@ -120,25 +121,29 @@ SCENARIO("Original Slic3r Skirt/Brim tests", "[!mayfail]") { WHEN("Perimeter extruder = 2 and support extruders = 3") { THEN("Brim is printed with the extruder used for the perimeters of first object") { - std::string gcode = Slic3r::Test::slice({TestMesh::cube_20x20x20}, { + config.set_deserialize({ { "skirts", 0 }, { "brim_width", 5 }, { "perimeter_extruder", 2 }, - { "support_material_extruder", 3 } - }); + { "support_material_extruder", 3 }, + { "infill_extruder", 4 } + }); + std::string gcode = Slic3r::Test::slice({TestMesh::cube_20x20x20}, config); int tool = get_brim_tool(gcode); REQUIRE(tool == config.opt_int("perimeter_extruder") - 1); } } WHEN("Perimeter extruder = 2, support extruders = 3, raft is enabled") { THEN("brim is printed with same extruder as skirt") { - std::string gcode = Slic3r::Test::slice({TestMesh::cube_20x20x20}, { - { "skirts", 0 }, - { "brim_width", 5 }, - { "perimeter_extruder", 2 }, - { "support_material_extruder", 3 }, - { "raft_layers", 1 } - }); + config.set_deserialize({ + { "skirts", 0 }, + { "brim_width", 5 }, + { "perimeter_extruder", 2 }, + { "support_material_extruder", 3 }, + { "infill_extruder", 4 }, + { "raft_layers", 1 } + }); + std::string gcode = Slic3r::Test::slice({TestMesh::cube_20x20x20}, config); int tool = get_brim_tool(gcode); REQUIRE(tool == config.opt_int("support_material_extruder") - 1); } @@ -200,6 +205,7 @@ SCENARIO("Original Slic3r Skirt/Brim tests", "[!mayfail]") { { "infill_extruder", 3 }, // ensure that a tool command gets emitted. { "cooling", false }, // to prevent speeds to be altered { "first_layer_speed", "100%" }, // to prevent speeds to be altered + { "start_gcode", "T[initial_tool]\n" } }); THEN("overhang generates?") { diff --git a/tests/libslic3r/CMakeLists.txt b/tests/libslic3r/CMakeLists.txt index 2dd2b34a3..b9d7fc898 100644 --- a/tests/libslic3r/CMakeLists.txt +++ b/tests/libslic3r/CMakeLists.txt @@ -2,7 +2,9 @@ get_filename_component(_TEST_NAME ${CMAKE_CURRENT_LIST_DIR} NAME) add_executable(${_TEST_NAME}_tests ${_TEST_NAME}_tests.cpp test_3mf.cpp + test_clipper_offset.cpp test_config.cpp +# test_elephant_foot_compensation.cpp test_geometry.cpp test_polygon.cpp test_stl.cpp diff --git a/tests/libslic3r/test_clipper_offset.cpp b/tests/libslic3r/test_clipper_offset.cpp new file mode 100644 index 000000000..fed89c66c --- /dev/null +++ b/tests/libslic3r/test_clipper_offset.cpp @@ -0,0 +1,214 @@ +#include + +#include +#include + +#include "libslic3r/ClipperUtils.hpp" +#include "libslic3r/ExPolygon.hpp" +#include "libslic3r/SVG.hpp" + +using namespace Slic3r; + +#define TESTS_EXPORT_SVGS + +SCENARIO("Constant offset", "[ClipperUtils]") { + coord_t s = 1000000; + GIVEN("20mm box") { + ExPolygon box20mm; + box20mm.contour.points = { { 0, 0 }, { 20 * s, 0 }, { 20 * s, 20 * s}, { 0, 20 * s} }; + std::vector deltas_plus(box20mm.contour.points.size(), 1. * s); + std::vector deltas_minus(box20mm.contour.points.size(), - 1. * s); + Polygons output; + WHEN("Slic3r::offset()") { + for (double miter : { 2.0, 1.5, 1.2 }) { + DYNAMIC_SECTION("plus 1mm, miter " << miter << "x") { + output = Slic3r::offset(box20mm, 1. * s, ClipperLib::jtMiter, miter); +#ifdef TESTS_EXPORT_SVGS + { + SVG svg(debug_out_path("constant_offset_box20mm_plus1mm_miter%lf.svg", miter).c_str(), get_extents(output)); + svg.draw(box20mm, "blue"); + svg.draw_outline(output, "black", coord_t(scale_(0.01))); + } +#endif + THEN("Area is 22^2mm2") { + REQUIRE(output.size() == 1); + REQUIRE(output.front().area() == Approx(22. * 22. * s * s)); + } + } + DYNAMIC_SECTION("minus 1mm, miter " << miter << "x") { + output = Slic3r::offset(box20mm, - 1. * s, ClipperLib::jtMiter, miter); +#ifdef TESTS_EXPORT_SVGS + { + SVG svg(debug_out_path("constant_offset_box20mm_minus1mm_miter%lf.svg", miter).c_str(), get_extents(output)); + svg.draw(box20mm, "blue"); + svg.draw_outline(output, "black", coord_t(scale_(0.01))); + } +#endif + THEN("Area is 18^2mm2") { + REQUIRE(output.size() == 1); + REQUIRE(output.front().area() == Approx(18. * 18. * s * s)); + } + } + } + } + WHEN("Slic3r::variable_offset_outer/inner") { + for (double miter : { 2.0, 1.5, 1.2 }) { + DYNAMIC_SECTION("plus 1mm, miter " << miter << "x") { + output = Slic3r::variable_offset_outer(box20mm, { deltas_plus }, miter); +#ifdef TESTS_EXPORT_SVGS + { + SVG svg(debug_out_path("variable_offset_box20mm_plus1mm_miter%lf.svg", miter).c_str(), get_extents(output)); + svg.draw(box20mm, "blue"); + svg.draw_outline(output, "black", coord_t(scale_(0.01))); + } +#endif + THEN("Area is 22^2mm2") { + REQUIRE(output.size() == 1); + REQUIRE(output.front().area() == Approx(22. * 22. * s * s)); + } + } + DYNAMIC_SECTION("minus 1mm, miter " << miter << "x") { + output = Slic3r::variable_offset_inner(box20mm, { deltas_minus }, miter); +#ifdef TESTS_EXPORT_SVGS + { + SVG svg(debug_out_path("variable_offset_box20mm_minus1mm_miter%lf.svg", miter).c_str(), get_extents(output)); + svg.draw(box20mm, "blue"); + svg.draw_outline(output, "black", coord_t(scale_(0.01))); + } +#endif + THEN("Area is 18^2mm2") { + REQUIRE(output.size() == 1); + REQUIRE(output.front().area() == Approx(18. * 18. * s * s)); + } + } + } + } + } + + GIVEN("20mm box with 10mm hole") { + ExPolygon box20mm; + box20mm.contour.points = { { 0, 0 }, { 20 * s, 0 }, { 20 * s, 20 * s}, { 0, 20 * s} }; + box20mm.holes.emplace_back(Slic3r::Polygon({ { 5 * s, 5 * s }, { 5 * s, 15 * s}, { 15 * s, 15 * s}, { 15 * s, 5 * s } })); + std::vector deltas_plus(box20mm.contour.points.size(), 1. * s); + std::vector deltas_minus(box20mm.contour.points.size(), -1. * s); + ExPolygons output; + SECTION("Slic3r::offset()") { + for (double miter : { 2.0, 1.5, 1.2 }) { + DYNAMIC_SECTION("miter " << miter << "x") { + WHEN("plus 1mm") { + output = Slic3r::offset_ex(box20mm, 1. * s, ClipperLib::jtMiter, miter); +#ifdef TESTS_EXPORT_SVGS + { + SVG svg(debug_out_path("constant_offset_box20mm_10mm_hole_plus1mm_miter%lf.svg", miter).c_str(), get_extents(output)); + svg.draw(box20mm, "blue"); + svg.draw_outline(to_polygons(output), "black", coord_t(scale_(0.01))); + } +#endif + THEN("Area is 22^2-8^2 mm2") { + REQUIRE(output.size() == 1); + REQUIRE(output.front().area() == Approx((22. * 22. - 8. * 8.) * s * s)); + } + } + WHEN("minus 1mm") { + output = Slic3r::offset_ex(box20mm, - 1. * s, ClipperLib::jtMiter, miter); +#ifdef TESTS_EXPORT_SVGS + { + SVG svg(debug_out_path("constant_offset_box20mm_10mm_hole_minus1mm_miter%lf.svg", miter).c_str(), get_extents(output)); + svg.draw(box20mm, "blue"); + svg.draw_outline(to_polygons(output), "black", coord_t(scale_(0.01))); + } +#endif + THEN("Area is 18^2-12^2 mm2") { + REQUIRE(output.size() == 1); + REQUIRE(output.front().area() == Approx((18. * 18. - 12. * 12.) * s * s)); + } + } + } + } + } + SECTION("Slic3r::variable_offset_outer()") { + for (double miter : { 2.0, 1.5, 1.2 }) { + DYNAMIC_SECTION("miter " << miter << "x") { + WHEN("plus 1mm") { + output = Slic3r::variable_offset_outer_ex(box20mm, { deltas_plus, deltas_plus }, miter); +#ifdef TESTS_EXPORT_SVGS + { + SVG svg(debug_out_path("variable_offset_box20mm_10mm_hole_plus1mm_miter%lf.svg", miter).c_str(), get_extents(output)); + svg.draw(box20mm, "blue"); + svg.draw_outline(to_polygons(output), "black", coord_t(scale_(0.01))); + } +#endif + THEN("Area is 22^2-8^2 mm2") { + REQUIRE(output.size() == 1); + REQUIRE(output.front().area() == Approx((22. * 22. - 8. * 8.) * s * s)); + } + } + WHEN("minus 1mm") { + output = Slic3r::variable_offset_inner_ex(box20mm, { deltas_minus, deltas_minus }, miter); +#ifdef TESTS_EXPORT_SVGS + { + SVG svg(debug_out_path("variable_offset_box20mm_10mm_hole_minus1mm_miter%lf.svg", miter).c_str(), get_extents(output)); + svg.draw(box20mm, "blue"); + svg.draw_outline(to_polygons(output), "black", coord_t(scale_(0.01))); + } +#endif + THEN("Area is 18^2-12^2 mm2") { + REQUIRE(output.size() == 1); + REQUIRE(output.front().area() == Approx((18. * 18. - 12. * 12.) * s * s)); + } + } + } + } + } + } + + GIVEN("20mm right angle triangle") { + ExPolygon triangle20mm; + triangle20mm.contour.points = { { 0, 0 }, { 20 * s, 0 }, { 0, 20 * s} }; + Polygons output; + double offset = 1.; + // Angle of the sharp corner bisector. + double angle_bisector = M_PI / 8.; + // Area tapered by mitering one sharp corner. + double area_tapered = pow(offset * (1. / sin(angle_bisector) - 1.), 2.) * tan(angle_bisector); + double l_triangle_side_offsetted = 20. + offset * (1. + 1. / tan(angle_bisector)); + double area_offsetted = (0.5 * l_triangle_side_offsetted * l_triangle_side_offsetted - 2. * area_tapered) * s * s; + SECTION("Slic3r::offset()") { + for (double miter : { 2.0, 1.5, 1.2 }) { + DYNAMIC_SECTION("Outer offset 1mm, miter " << miter << "x") { + output = Slic3r::offset(triangle20mm, offset * s, ClipperLib::jtMiter, 2.0); +#ifdef TESTS_EXPORT_SVGS + { + SVG svg(debug_out_path("constant_offset_triangle20mm_plus1mm_miter%lf.svg", miter).c_str(), get_extents(output)); + svg.draw(triangle20mm, "blue"); + svg.draw_outline(output, "black", coord_t(scale_(0.01))); + } +#endif + THEN("Area matches") { + REQUIRE(output.size() == 1); + REQUIRE(output.front().area() == Approx(area_offsetted)); + } + } + } + } + SECTION("Slic3r::variable_offset_outer()") { + std::vector deltas(triangle20mm.contour.points.size(), 1. * s); + for (double miter : { 2.0, 1.5, 1.2 }) { + DYNAMIC_SECTION("Outer offset 1mm, miter " << miter << "x") { + output = Slic3r::variable_offset_outer(triangle20mm, { deltas }, 2.0); +#ifdef TESTS_EXPORT_SVGS + { + SVG svg(debug_out_path("variable_offset_triangle20mm_plus1mm_miter%lf.svg", miter).c_str(), get_extents(output)); + svg.draw(triangle20mm, "blue"); + svg.draw_outline(output, "black", coord_t(scale_(0.01))); + } +#endif + THEN("Area matches") { + REQUIRE(output.size() == 1); + REQUIRE(output.front().area() == Approx(area_offsetted)); + } + } + } + } + } +}