mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-06 15:21:09 +00:00
* Remove Unused Project Includes and Forward-Declare Where a Type Is Only Referenced Generated with include-what-you-use and applied conservatively. Only OrcaSlicer's own headers, the ones under src/ and tests/, are removed or forward-declared; standard-library and third-party includes are left alone. An include is removed only when both the Release and the Debug configuration leave it unused, never from inside a conditional block, and never from a file with platform-specific blocks, which only gain includes. Files whose only use of a header sits behind a feature or debug macro (libvgcode's OpenGL ES and marker code, the ARACHNE/TESTS_EXPORT_SVGS debug output) keep their includes. clonable_ptr.hpp gains #pragma once; it had no include guard and was only safe while Config.hpp was its sole includer. * Remove Unused Project Includes From Files With Platform-Specific Code A Linux include-what-you-use run cannot see the code inside _WIN32, __APPLE__ or __linux__ blocks, so its verdict is only taken where nothing the removed header declares, directly or through what it includes, is named inside those blocks. Removals also have to hold in both the Release and Debug configuration and never touch a line inside a conditional block. * Restore the libslic3r Precompiled Header and Direct Includes Lost in the Platform Pass The platform-file pass treated pchheader.hpp as an ordinary header and emptied it, and left GUI_Preview.hpp and 14 other files relying on headers they no longer reached directly. * Restore MainFrame.hpp in ParamsDialog.cpp for the Windows-Only Reparent Call * Include Headers That Files Reached Through Ones the Cleanup Removed * Drop Includes Duplicated by the Cleanup or by Main's Own Additions * Leave PreciseSeam.cpp as Main Has It After the Precise Seam Rework
1036 lines
50 KiB
C++
1036 lines
50 KiB
C++
#include <catch2/catch_all.hpp>
|
|
|
|
#include <catch2/catch_test_macros.hpp>
|
|
#include <catch2/catch_message.hpp>
|
|
#include <catch2/matchers/catch_matchers.hpp>
|
|
#include <catch2/matchers/catch_matchers_floating_point.hpp>
|
|
#include <catch2/generators/catch_generators.hpp>
|
|
#include "libslic3r/GCode/WipePathHelpers.hpp"
|
|
#include "libslic3r/AABBTreeLines.hpp"
|
|
#include "libslic3r/Polyline.hpp"
|
|
#include "libslic3r/Point.hpp"
|
|
#include "libslic3r/Line.hpp"
|
|
#include "libslic3r/libslic3r.h"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include "libslic3r/ExtrusionEntity.hpp"
|
|
#include <cstddef>
|
|
#include <limits>
|
|
#include <vector>
|
|
#include <utility>
|
|
#include <optional>
|
|
#include "libslic3r/ArcFitter.hpp"
|
|
|
|
using namespace Slic3r;
|
|
using Slic3r::AABBTreeLines::LinesDistancer;
|
|
|
|
TEST_CASE("Stored wipe path retains its length around a curved wall after a seam gap", "[WipePath][Regression]")
|
|
{
|
|
const int mirror = GENERATE(1, -1);
|
|
const double wipe_length = GENERATE(0.8, 1.0);
|
|
CAPTURE(mirror, wipe_length);
|
|
// A 0.02 mm seam gap on a curved 0.24 mm wall leaves a short outgoing
|
|
// segment whose inward offset backtracks. Coordinates use internal scaling
|
|
// from the affected loop; the adjacent inner wall is already printed.
|
|
const auto point = [mirror](coord_t x, coord_t y) { return Point(mirror * x, y); };
|
|
const Polyline original{
|
|
point(671861, 7772276), point(688586, 7765098), point(781082, 7687014),
|
|
point(852059, 7608861), point(889773, 7556912), point(958919, 7382963),
|
|
point(977048, 7259018), point(977325, 7173839), point(944250, 7039370),
|
|
point(911230, 6952087), point(880323, 6894944), point(760243, 6763860),
|
|
point(598587, 6641719), point(492626, 6593610), point(362533, 6543938),
|
|
point(170173, 6513482), point(114917, 6509380), point(18418, 6513666),
|
|
point(-145550, 6537251), point(-259087, 6580797), point(-413987, 6690495),
|
|
point(-485220, 6767022), point(-561189, 6893573), point(-576897, 6965717),
|
|
point(-595201, 7089303), point(-597977, 7164172), point(-590614, 7239553),
|
|
point(-574031, 7305533), point(-539668, 7383293), point(-442552, 7550465),
|
|
point(-332173, 7659644), point(-257819, 7717153), point(-209522, 7749563),
|
|
point(-121695, 7793438), point(399, 7844160), point(228137, 7880068),
|
|
point(363721, 7881585), point(431909, 7865985), point(569648, 7816149),
|
|
point(653482, 7780164),
|
|
};
|
|
const Polyline inner{
|
|
point(739251, 7241332), point(739377, 7202281), point(716708, 7110113),
|
|
point(694416, 7051190), point(685088, 7033943), point(599527, 6940541),
|
|
point(476243, 6847393), point(400952, 6813209), point(300851, 6774988),
|
|
point(142724, 6749952), point(111379, 6747625), point(40681, 6750765),
|
|
point(-85281, 6768883), point(-146010, 6792175), point(-256555, 6870462),
|
|
point(-295251, 6912034), point(-336173, 6978125), point(-357996, 7111200),
|
|
point(-359693, 7156985), point(-355612, 7198777), point(-348288, 7227916),
|
|
point(-327412, 7275156), point(-252784, 7403618), point(-175201, 7480358),
|
|
point(-89616, 7543582), point(-22802, 7576960), point(65459, 7613627),
|
|
point(248096, 7642423), point(338175, 7643431), point(364673, 7637369),
|
|
point(482204, 7594844), point(562221, 7560499), point(615604, 7515433),
|
|
point(679764, 7441323), point(727601, 7320981), point(739251, 7241332),
|
|
};
|
|
const Point seam_start = original.first_point();
|
|
const Point seam_end = original.last_point();
|
|
const double offset = scale_(0.239999);
|
|
Polyline forward = original;
|
|
REQUIRE_FALSE(offset_wipe_path(forward, seam_start, seam_end, seam_end,
|
|
-mirror, offset, scale_(wipe_length)));
|
|
|
|
Polyline path = original;
|
|
REQUIRE(offset_wipe_path_toward_support(path, seam_start, seam_end, seam_end,
|
|
-mirror, offset, scale_(wipe_length), inner.lines(), inner.lines(), original.lines(), offset));
|
|
REQUIRE(path.first_point() == seam_start);
|
|
REQUIRE(path.points.size() > 2);
|
|
// Wipe::wipe replaces the sentinel with the actual extrusion endpoint.
|
|
path.points.front() = seam_end;
|
|
CHECK_THAT(unscale_(path.length()), Catch::Matchers::WithinAbs(wipe_length, 0.0004));
|
|
CHECK(mirror * (path.points[1].x() - seam_end.x()) < 0);
|
|
CHECK(path.points[1].y() < seam_end.y());
|
|
Lines support = inner.lines();
|
|
const Lines current = original.lines();
|
|
support.insert(support.end(), current.begin(), current.end());
|
|
REQUIRE(wipe_path_support_score(path, seam_end, LinesDistancer<Line>(inner.lines()),
|
|
LinesDistancer<Line>(support), offset).has_value());
|
|
}
|
|
|
|
TEST_CASE("Stored wipe path retains its length after a loop pre-move at a curved seam", "[WipePath][Regression]")
|
|
{
|
|
const int mirror = GENERATE(1, -1);
|
|
const bool pre_move = GENERATE(false, true);
|
|
CAPTURE(mirror, pre_move);
|
|
const auto point = [mirror](coord_t x, coord_t y) { return Point(mirror * x, y); };
|
|
// A 0.02 mm seam gap on a curved 0.24 mm wall, with the adjacent inner
|
|
// wall already printed. The loop pre-move advances the nozzle near the seam.
|
|
const Polyline original{
|
|
point(686772, 7813199), point(516334, 7887188), point(411017, 7915098),
|
|
point(346190, 7926015), point(246957, 7925330), point(-16945, 7881611),
|
|
point(-116443, 7842513), point(-255907, 7773886), point(-378126, 7681053),
|
|
point(-499258, 7552879), point(-574414, 7438250), point(-613558, 7370259),
|
|
point(-626313, 7341394), point(-650774, 7263424), point(-669964, 7169919),
|
|
point(-666798, 7058662), point(-631336, 6876547), point(-624410, 6852946),
|
|
point(-577832, 6762704), point(-517493, 6693143), point(-455794, 6631765),
|
|
point(-315549, 6531304), point(-169627, 6468424), point(-1908, 6443726),
|
|
point(143562, 6438395), point(314277, 6465470), point(380448, 6480795),
|
|
point(519922, 6526617), point(673990, 6611110), point(801581, 6705610),
|
|
point(927505, 6840928), point(969616, 6912718), point(1004269, 7009155),
|
|
point(1044144, 7171737), point(1046617, 7228598), point(1028598, 7358360),
|
|
point(950280, 7560914), point(867133, 7675444), point(732946, 7788889),
|
|
point(706168, 7804780), point(705118, 7805235),
|
|
};
|
|
const Polyline inner{
|
|
point(808905, 7211140), point(796801, 7298317), point(739603, 7446245),
|
|
point(691575, 7512401), point(595745, 7593416), point(438069, 7661865),
|
|
point(360694, 7682370), point(327119, 7688024), point(266586, 7687607),
|
|
point(53302, 7653657), point(-20276, 7624745), point(-130307, 7570601),
|
|
point(-218687, 7503470), point(-311907, 7404832), point(-371722, 7313599),
|
|
point(-401113, 7262547), point(-420207, 7203763), point(-431422, 7149118),
|
|
point(-429596, 7084952), point(-401187, 6939055), point(-379519, 6897073),
|
|
point(-343546, 6855603), point(-301665, 6813940), point(-197879, 6739595),
|
|
point(-104131, 6699197), point(19838, 6680942), point(129154, 6676936),
|
|
point(268758, 6699077), point(316366, 6710103), point(424812, 6745731),
|
|
point(545430, 6811879), point(642396, 6883697), point(735572, 6983824),
|
|
point(753259, 7013976), point(776223, 7077887), point(808905, 7211140),
|
|
};
|
|
const Point seam_start = original.first_point();
|
|
const Point seam_end = original.last_point();
|
|
const Point wipe_start = pre_move ? point(652751, 7792162) : seam_end;
|
|
const double offset = scale_(0.239999);
|
|
Polyline path = original;
|
|
REQUIRE(offset_wipe_path_toward_support(path, seam_start, seam_end, wipe_start,
|
|
mirror, offset, scale_(0.8), inner.lines(), inner.lines(), original.lines(), offset));
|
|
REQUIRE(path.first_point() == seam_start);
|
|
path.points.front() = wipe_start;
|
|
CHECK_THAT(unscale_(path.length()), Catch::Matchers::WithinAbs(0.8, 0.0004));
|
|
Lines support = inner.lines();
|
|
const Lines current = original.lines();
|
|
support.insert(support.end(), current.begin(), current.end());
|
|
REQUIRE(wipe_path_support_score(path, wipe_start, LinesDistancer<Line>(inner.lines()),
|
|
LinesDistancer<Line>(support), offset).has_value());
|
|
}
|
|
|
|
// Orca: helpers for constructing the extrusion geometry used by wipe tests.
|
|
|
|
static ExtrusionPath make_path(const std::vector<Point> &pts, ExtrusionRole role = erExternalPerimeter,
|
|
float width = 0.4f, float height = 0.2f)
|
|
{
|
|
ExtrusionPath p(role, 0.5, width, height);
|
|
for (const Point &pt : pts)
|
|
p.polyline.append(Point3(pt.x(), pt.y(), coord_t(0)));
|
|
return p;
|
|
}
|
|
|
|
static ExtrusionPaths make_paths(const std::vector<Point> &pts, ExtrusionRole role = erExternalPerimeter,
|
|
float width = 0.4f)
|
|
{
|
|
ExtrusionPaths paths;
|
|
paths.push_back(make_path(pts, role, width));
|
|
return paths;
|
|
}
|
|
|
|
TEST_CASE("Inward wipe support recognizes an inner wall starting on an overhang", "[WipePath][Regression]")
|
|
{
|
|
const bool overhang_first = GENERATE(false, true);
|
|
const auto point = [](double x, double y) { return Point::new_scale(x, y); };
|
|
ExtrusionPaths paths{
|
|
make_path({point(0.4, 0.4), point(0.4, 2.)}, erOverhangPerimeter),
|
|
make_path({point(0.4, 2.), point(0.4, 9.6), point(5.6, 9.6), point(5.6, 0.4), point(0.4, 0.4)}, erPerimeter)
|
|
};
|
|
if (!overhang_first)
|
|
std::rotate(paths.begin(), paths.begin() + 1, paths.end());
|
|
const ExtrusionLoop inner(paths);
|
|
REQUIRE(inner.role() == (overhang_first ? erOverhangPerimeter : erPerimeter));
|
|
|
|
WipeInwardSupport support;
|
|
support.append(inner);
|
|
REQUIRE(support.inner_lines.size() == inner.as_polyline().lines().size());
|
|
// The overhanging portion itself is already printed and can support the wipe.
|
|
const LinesDistancer<Line> inner_distancer(support.inner_lines);
|
|
CHECK_THAT(inner_distancer.distance_from_lines<false>(point(0.4, 1.)),
|
|
Catch::Matchers::WithinAbs(0., SCALED_EPSILON));
|
|
const Polyline original{point(0., 0.), point(0., 10.), point(6., 10.), point(6., 0.), point(0., 0.)};
|
|
Polyline wipe = original;
|
|
REQUIRE(offset_wipe_path_toward_support(wipe, original.first_point(), original.first_point(),
|
|
original.first_point(), -1, scale_(0.2), scale_(2.), support.inner_lines,
|
|
support.printed_lines, original.lines(), scale_(0.6)));
|
|
CHECK(wipe.points[1].x() > original.first_point().x());
|
|
}
|
|
|
|
TEST_CASE("Inward wipe support accumulates earlier walls without treating outer walls as targets", "[WipePath][Regression]")
|
|
{
|
|
const auto point = [](double x, double y) { return Point::new_scale(x, y); };
|
|
WipeInwardSupport support;
|
|
const ExtrusionPath inner = make_path({point(0.4, 0.), point(0.4, 5.)}, erPerimeter);
|
|
support.append(inner);
|
|
const ExtrusionLoop outer(ExtrusionPaths{
|
|
make_path({point(0., 0.), point(0., 5.)}, erOverhangPerimeter),
|
|
make_path({point(0., 5.), point(-5., 5.), point(-5., 0.), point(0., 0.)})
|
|
});
|
|
support.append(outer);
|
|
REQUIRE(support.inner_lines.size() == 1);
|
|
REQUIRE(support.printed_lines.size() == 5);
|
|
const LinesDistancer<Line> targets(support.inner_lines);
|
|
CHECK_THAT(targets.distance_from_lines<false>(point(0., 2.)),
|
|
Catch::Matchers::WithinAbs(scale_(0.4), SCALED_EPSILON));
|
|
}
|
|
|
|
static ExtrusionPaths make_loop_paths(const std::vector<Point> &contour_pts, float width = 0.4f)
|
|
{
|
|
ExtrusionPaths paths;
|
|
size_t mid = contour_pts.size() / 2;
|
|
ExtrusionPath first(erExternalPerimeter, 0.5, width, 0.2f);
|
|
for (size_t i = 0; i <= mid; ++i)
|
|
first.polyline.append(Point3(contour_pts[i].x(), contour_pts[i].y(), coord_t(0)));
|
|
ExtrusionPath second(erExternalPerimeter, 0.5, width, 0.2f);
|
|
for (size_t i = mid; i < contour_pts.size(); ++i)
|
|
second.polyline.append(Point3(contour_pts[i].x(), contour_pts[i].y(), coord_t(0)));
|
|
second.polyline.append(Point3(contour_pts[0].x(), contour_pts[0].y(), coord_t(0)));
|
|
paths.push_back(std::move(first));
|
|
paths.push_back(std::move(second));
|
|
return paths;
|
|
}
|
|
|
|
// Orca: sample_path_at_distance coverage.
|
|
|
|
TEST_CASE("sample_path_at_distance forward returns start for zero target", "[WipePath]")
|
|
{
|
|
const coord_t s = scale_(1.0);
|
|
auto paths = make_paths({Point(0, 0), Point(100 * s, 0), Point(100 * s, 100 * s)});
|
|
REQUIRE(sample_path_at_distance(paths, true, 0.0) == Point(0, 0));
|
|
}
|
|
|
|
TEST_CASE("sample_path_at_distance forward samples along path", "[WipePath]")
|
|
{
|
|
const coord_t s = scale_(1.0);
|
|
auto paths = make_paths({Point(0, 0), Point(100 * s, 0), Point(100 * s, 100 * s)});
|
|
|
|
Point result = sample_path_at_distance(paths, true, 50 * s);
|
|
REQUIRE_THAT(result.x(), Catch::Matchers::WithinAbs(50 * s, 2));
|
|
REQUIRE_THAT(result.y(), Catch::Matchers::WithinAbs(0, 2));
|
|
}
|
|
|
|
TEST_CASE("sample_path_at_distance forward crosses segment boundary", "[WipePath]")
|
|
{
|
|
const coord_t s = scale_(1.0);
|
|
auto paths = make_paths({Point(0, 0), Point(100 * s, 0), Point(100 * s, 100 * s)});
|
|
|
|
Point result = sample_path_at_distance(paths, true, 150 * s);
|
|
REQUIRE_THAT(result.x(), Catch::Matchers::WithinAbs(100 * s, 2));
|
|
REQUIRE_THAT(result.y(), Catch::Matchers::WithinAbs(50 * s, 2));
|
|
}
|
|
|
|
TEST_CASE("sample_path_at_distance backward from end", "[WipePath]")
|
|
{
|
|
const coord_t s = scale_(1.0);
|
|
auto paths = make_paths({Point(0, 0), Point(100 * s, 0), Point(100 * s, 100 * s)});
|
|
|
|
Point result = sample_path_at_distance(paths, false, 50 * s);
|
|
REQUIRE_THAT(result.x(), Catch::Matchers::WithinAbs(100 * s, 2));
|
|
REQUIRE_THAT(result.y(), Catch::Matchers::WithinAbs(50 * s, 2));
|
|
}
|
|
|
|
TEST_CASE("sample_path_at_distance on short path returns reachable point", "[WipePath]")
|
|
{
|
|
const coord_t s = scale_(1.0);
|
|
auto paths = make_paths({Point(0, 0), Point(10 * s, 0)});
|
|
|
|
Point result = sample_path_at_distance(paths, true, 1000 * s);
|
|
REQUIRE(result == Point(10 * s, 0));
|
|
}
|
|
|
|
TEST_CASE("sample_path_at_distance on zero-length path returns start", "[WipePath]")
|
|
{
|
|
const coord_t s = scale_(1.0);
|
|
auto paths = make_paths({Point(50 * s, 50 * s)});
|
|
|
|
REQUIRE(sample_path_at_distance(paths, true, 100 * s) == Point(50 * s, 50 * s));
|
|
REQUIRE(sample_path_at_distance(paths, false, 100 * s) == Point(50 * s, 50 * s));
|
|
}
|
|
|
|
TEST_CASE("Wipe offset direction follows the material side", "[WipePath]")
|
|
{
|
|
REQUIRE(wipe_offset_direction(true, false) == +1);
|
|
REQUIRE(wipe_offset_direction(false, false) == -1);
|
|
REQUIRE(wipe_offset_direction(true, true) == -1);
|
|
REQUIRE(wipe_offset_direction(false, true) == +1);
|
|
}
|
|
|
|
TEST_CASE("Stored wipe path leaves source crossings to support validation", "[WipePath]")
|
|
{
|
|
const coord_t s = scale_(1.0);
|
|
Polyline path{Point(10 * s, 0), Point(100 * s, 0), Point(coord_t(13.4 * s), coord_t(50 * s))};
|
|
|
|
// Orca: crossing the just-printed wall is harmless for a non-extruding wipe.
|
|
// The caller decides whether the result is supported by printed geometry.
|
|
REQUIRE(offset_wipe_path(path, Point(10 * s, 0), Point(0, 0), Point(0, 0), +1, 5 * s, 1000 * s));
|
|
}
|
|
|
|
TEST_CASE("Stored wipe path builds the join after a nonzero seam gap", "[WipePath]")
|
|
{
|
|
const coord_t s = scale_(1.0);
|
|
Polyline path{Point(10 * s, 0), Point(10 * s, 0), Point(10 * s, 100 * s)};
|
|
|
|
REQUIRE(offset_wipe_path(path, Point(10 * s, 0), Point(0, 0), Point(0, 0), +1, 5 * s, 1000 * s));
|
|
REQUIRE(path.points.size() == 3);
|
|
REQUIRE(path.points[1] == Point(5 * s, 5 * s));
|
|
REQUIRE(path.points[2] == Point(5 * s, 100 * s));
|
|
REQUIRE(path.fitting_result.size() == 1);
|
|
REQUIRE(path.fitting_result.front().end_point_index == path.points.size() - 1);
|
|
}
|
|
|
|
TEST_CASE("Stored wipe path rejects an offset seam join that turns backward", "[WipePath][Regression]")
|
|
{
|
|
const coord_t s = scale_(1.0);
|
|
const Point seam_start(s, s);
|
|
const Point seam_end(0, 0);
|
|
Polyline path{seam_start, Point(s, -10 * s), Point(s, -20 * s)};
|
|
const Polyline original = path;
|
|
|
|
REQUIRE_FALSE(offset_wipe_path(path, seam_start, seam_end, seam_end, +1, s, 5 * s));
|
|
REQUIRE(path.points == original.points);
|
|
}
|
|
|
|
TEST_CASE("Stored wipe path continues after an inward pre-move", "[WipePath][Regression]")
|
|
{
|
|
const coord_t s = scale_(1.0);
|
|
const Point seam_start(s, s);
|
|
const Point seam_end(0, 0);
|
|
const Point wipe_start(2 * s, 2 * s);
|
|
Polyline path{seam_start, Point(s, -10 * s), Point(s, -20 * s)};
|
|
|
|
REQUIRE(offset_wipe_path(path, seam_start, seam_end, wipe_start, +1, s, 5 * s));
|
|
REQUIRE(path.points.size() >= 3);
|
|
path.points.front() = wipe_start; // Orca: reproduce Wipe::wipe()'s executable representation.
|
|
CHECK_THAT(path.length(), Catch::Matchers::WithinAbs(5. * s, 2.));
|
|
}
|
|
|
|
TEST_CASE("Stored wipe path does not retrace a translated seam gap", "[WipePath][Regression]")
|
|
{
|
|
const coord_t s = scale_(1.0);
|
|
const Point seam_start(s, 0);
|
|
const Point seam_end(0, 0);
|
|
Polyline path{seam_start, seam_end, Point(-10 * s, 0)};
|
|
const Polyline original = path;
|
|
const Lines support{Line(Point(-10 * s, s), Point(10 * s, s))};
|
|
|
|
// Orca: the exact reversal at seam_start forces the translated fallback.
|
|
// The seam gap supplies its incoming direction but must not become an
|
|
// inward-outward-inward detour in the executable path.
|
|
REQUIRE(offset_wipe_path_toward_support(
|
|
path, seam_start, seam_end, seam_end, +1, s, 5 * s,
|
|
support, support, original.lines(), s));
|
|
REQUIRE(path.points.size() == 2);
|
|
CHECK(path.points[1].y() > seam_end.y());
|
|
}
|
|
|
|
TEST_CASE("Stored wipe path keeps its first offset point when seam gap is zero", "[WipePath]")
|
|
{
|
|
const coord_t s = scale_(1.0);
|
|
Polyline path{Point(0, 0), Point(100 * s, 0), Point(100 * s, 100 * s),
|
|
Point(0, 100 * s), Point(0, 0)};
|
|
|
|
REQUIRE(offset_wipe_path(path, Point(0, 0), Point(0, 0), Point(0, 0), +1, 5 * s, 20 * s));
|
|
REQUIRE(path.points.size() >= 3);
|
|
REQUIRE(path.points[0] == Point(0, 0));
|
|
REQUIRE_THAT(path.points[1].x(), Catch::Matchers::WithinAbs(5 * s, 2));
|
|
REQUIRE_THAT(path.points[1].y(), Catch::Matchers::WithinAbs(5 * s, 2));
|
|
}
|
|
|
|
TEST_CASE("Stored wipe path ignores unsafe geometry beyond the used prefix", "[WipePath]")
|
|
{
|
|
const coord_t s = scale_(1.0);
|
|
Polyline path{Point(0, 0), Point(1000 * s, 0), Point(1000 * s, 20 * s),
|
|
Point(900 * s, 20 * s), Point(0, 20 * s), Point(0, 0)};
|
|
|
|
REQUIRE(offset_wipe_path(path, Point(0, 0), Point(0, 0), Point(0, 0), +1, 30 * s, 10 * s));
|
|
REQUIRE(path.points.size() == 2);
|
|
REQUIRE_THAT(path.length(), Catch::Matchers::WithinAbs(10 * s, 2));
|
|
}
|
|
|
|
TEST_CASE("Stored wipe path grows its source until the offset reaches the requested length", "[WipePath]")
|
|
{
|
|
const coord_t s = scale_(1.0);
|
|
Polyline path{Point(0, 0), Point(100 * s, 0), Point(100 * s, 100 * s), Point(0, 100 * s)};
|
|
const double wipe_length = 250 * s;
|
|
|
|
// Orca: two inward corners shorten this offset by more than 2 * offset_dist.
|
|
REQUIRE(offset_wipe_path(path, Point(0, 0), Point(0, 0), Point(0, 0),
|
|
+1, 10 * s, wipe_length));
|
|
REQUIRE_THAT(path.length(), Catch::Matchers::WithinAbs(wipe_length, 2));
|
|
}
|
|
|
|
TEST_CASE("Stored wipe path is unchanged when wipe distance is zero", "[WipePath]")
|
|
{
|
|
const coord_t s = scale_(1.0);
|
|
Polyline path{Point(0, 0), Point(100 * s, 0)};
|
|
const Polyline orig = path;
|
|
|
|
REQUIRE_FALSE(offset_wipe_path(path, Point(0, 0), Point(0, 0), Point(0, 0), +1, 5 * s, 0));
|
|
REQUIRE(path.points == orig.points);
|
|
}
|
|
|
|
TEST_CASE("Stored wipe path defers actual-start crossings to support validation", "[WipePath]")
|
|
{
|
|
const coord_t s = scale_(1.0);
|
|
Polyline path{Point(0, 0), Point(100 * s, 0), Point(100 * s, 100 * s),
|
|
Point(0, 100 * s), Point(0, 0)};
|
|
const Lines current = path.lines();
|
|
const Lines remote{Line(Point(0, 50 * s), Point(100 * s, 50 * s))};
|
|
const Point wipe_start(50 * s, -10 * s);
|
|
|
|
REQUIRE(offset_wipe_path(path, Point(0, 0), Point(0, 0), wipe_start, +1, 5 * s, 100 * s));
|
|
Lines all_support = remote;
|
|
all_support.insert(all_support.end(), current.begin(), current.end());
|
|
REQUIRE_FALSE(wipe_path_support_score(path, wipe_start,
|
|
LinesDistancer<Line>(remote), LinesDistancer<Line>(all_support), 5 * s).has_value());
|
|
}
|
|
|
|
TEST_CASE("Stored wipe path keeps the closing join when its prefix ends at the closing vertex", "[WipePath]")
|
|
{
|
|
const coord_t s = scale_(1.0);
|
|
Polyline path{Point(0, 0), Point(0, 100 * s), Point(100 * s, 100 * s),
|
|
Point(100 * s, 0), Point(0, 0)};
|
|
|
|
REQUIRE(offset_wipe_path(path, Point(0, 0), Point(0, 0), Point(0, 0),
|
|
+1, 5 * s, 300 * s));
|
|
REQUIRE(path.points.size() >= 2);
|
|
REQUIRE(path.points[1] == Point(-5 * s, -5 * s));
|
|
}
|
|
|
|
TEST_CASE("Stored wipe path rejects a two-point zero-gap loop", "[WipePath]")
|
|
{
|
|
const coord_t s = scale_(1.0);
|
|
Polyline path{Point(0, 0), Point(100 * s, 0), Point(0, 0)};
|
|
const Polyline original = path;
|
|
|
|
REQUIRE_FALSE(offset_wipe_path(path, Point(0, 0), Point(0, 0), Point(0, 0),
|
|
+1, 5 * s, 100 * s));
|
|
REQUIRE(path.points == original.points);
|
|
}
|
|
|
|
TEST_CASE("Stored wipe path tolerates quantized contact at its actual start", "[WipePath]")
|
|
{
|
|
const coord_t s = scale_(1.0);
|
|
const coord_t quantization = coord_t(SCALED_EPSILON / 2);
|
|
Polyline path{Point(0, quantization), Point(100 * s, quantization),
|
|
Point(100 * s, 100 * s + quantization), Point(0, 100 * s + quantization),
|
|
Point(0, quantization)};
|
|
|
|
// Orca: the executable transition starts within the geometry epsilon of the
|
|
// source endpoint. Treat this as the allowed start contact, while contacts
|
|
// farther along the transition remain unsafe.
|
|
REQUIRE(offset_wipe_path(path, Point(0, quantization), Point(0, quantization),
|
|
Point(0, 0), +1, 5 * s, 20 * s));
|
|
}
|
|
|
|
TEST_CASE("Stored wipe path requires nearby generated perimeter geometry", "[WipePath]")
|
|
{
|
|
const coord_t s = scale_(1.0);
|
|
const Polyline path{Point(0, 0), Point(0, 2 * s), Point(10 * s, 2 * s)};
|
|
const Lines adjacent{Line(Point(0, 4 * s), Point(10 * s, 4 * s))};
|
|
const Lines remote{Line(Point(0, 20 * s), Point(10 * s, 20 * s))};
|
|
const Lines current = path.lines();
|
|
|
|
const LinesDistancer<Line> adjacent_distancer(adjacent);
|
|
const LinesDistancer<Line> remote_distancer(remote);
|
|
Lines all_support = remote;
|
|
all_support.insert(all_support.end(), current.begin(), current.end());
|
|
const LinesDistancer<Line> all_support_distancer(all_support);
|
|
|
|
const auto score = wipe_path_support_score(path, Point(0, 2 * s), adjacent_distancer, adjacent_distancer, 3 * s);
|
|
REQUIRE(score.has_value());
|
|
CHECK_THAT(*score, Catch::Matchers::WithinAbs(2. * s, 2.));
|
|
REQUIRE_FALSE(wipe_path_support_score(path, Point(0, 2 * s), adjacent_distancer, adjacent_distancer, 0).has_value());
|
|
REQUIRE_FALSE(wipe_path_support_score(path, Point(0, 2 * s), remote_distancer, remote_distancer, 3 * s).has_value());
|
|
REQUIRE_FALSE(wipe_path_support_score(path, Point(0, 2 * s), remote_distancer, all_support_distancer, 3 * s).has_value());
|
|
REQUIRE_FALSE(wipe_path_support_score(path, Point(0, 2 * s), LinesDistancer<Line>(Lines{}),
|
|
all_support_distancer, 3 * s).has_value());
|
|
}
|
|
|
|
TEST_CASE("Stored wipe path checks the first segment from its actual start", "[WipePath]")
|
|
{
|
|
const coord_t s = scale_(1.0);
|
|
const Polyline path{Point(0, 0), Point(10 * s, 0)};
|
|
const Lines support_near_ends{
|
|
Line(Point(0, -s), Point(0, s)),
|
|
Line(Point(10 * s, -s), Point(10 * s, s))
|
|
};
|
|
|
|
// Orca: both endpoints are supported, but the middle of the executable segment
|
|
// from wipe_start is not. The dummy path[0] must not hide that segment.
|
|
const LinesDistancer<Line> support_distancer(support_near_ends);
|
|
REQUIRE_FALSE(wipe_path_support_score(path, Point(0, 0), support_distancer, support_distancer, 2 * s).has_value());
|
|
}
|
|
|
|
TEST_CASE("Stored wipe path rejects unsupported gaps between nearby samples", "[WipePath][Regression]")
|
|
{
|
|
const Point start = Point::new_scale(0., 0.);
|
|
const Point end = Point::new_scale(0.8, 0.);
|
|
const Polyline path{start, end};
|
|
const double support_y = GENERATE(0.8, 0.95);
|
|
const Lines support{
|
|
Line(Point::new_scale(0., support_y), Point::new_scale(0., 2.)),
|
|
Line(Point::new_scale(0.8, support_y), Point::new_scale(0.8, 2.))
|
|
};
|
|
|
|
// Both endpoints are within 1 mm of support and the move is shorter than
|
|
// the old sampling interval. Only the 0.8 mm case supports its midpoint.
|
|
const LinesDistancer<Line> support_distancer(support);
|
|
const bool supported = wipe_path_support_score(path, start, support_distancer, support_distancer, scale_(1.)).has_value();
|
|
CHECK(supported == (support_y < 0.9));
|
|
}
|
|
|
|
TEST_CASE("Stored wipe path checks support at the actual nozzle position", "[WipePath][Regression]")
|
|
{
|
|
const Point end = Point::new_scale(0., 0.);
|
|
const Polyline path{end, end};
|
|
const Lines support{Line(Point::new_scale(-1., 0.), Point::new_scale(1., 0.))};
|
|
|
|
const LinesDistancer<Line> support_distancer(support);
|
|
REQUIRE_FALSE(wipe_path_support_score(path, Point::new_scale(0., -2.),
|
|
support_distancer, support_distancer, scale_(1.)).has_value());
|
|
}
|
|
|
|
TEST_CASE("Direct inward fallback respects a short wipe distance before validation", "[WipePath][Regression]")
|
|
{
|
|
const Point seam = Point::new_scale(0., 0.);
|
|
Polyline path{seam, Point::new_scale(10., 0.), Point::new_scale(10., 10.),
|
|
Point::new_scale(0., 10.), seam};
|
|
const Lines current = path.lines();
|
|
const Lines support{Line(Point::new_scale(0.4, 0.4), Point::new_scale(9.6, 0.4))};
|
|
const bool pre_move = GENERATE(false, true);
|
|
const Point wipe_start = pre_move ? Point::new_scale(0.02, 0.02) : seam;
|
|
const double wipe_length = scale_(0.05);
|
|
|
|
REQUIRE(offset_wipe_path_toward_support(
|
|
path, seam, seam, wipe_start, +1, scale_(0.2), wipe_length,
|
|
support, support, current, scale_(0.4)));
|
|
REQUIRE(path.points.size() == 2);
|
|
path.points.front() = wipe_start;
|
|
CHECK_THAT(path.length(), Catch::Matchers::WithinAbs(wipe_length, 2.));
|
|
CHECK(path.last_point().x() > wipe_start.x());
|
|
CHECK(path.last_point().y() > wipe_start.y());
|
|
}
|
|
|
|
TEST_CASE("Stored wipe path uses a stable zero-gap join for nearly parallel segments", "[WipePath][Regression]")
|
|
{
|
|
const auto point = [](double x, double y) { return Point::new_scale(x, y); };
|
|
const Point seam = point(58.777, 61.985);
|
|
Polyline path{
|
|
seam, point(58.822, 61.918), point(58.900, 61.789), point(58.980, 61.641),
|
|
point(59.054, 61.480), point(59.260, 60.980), point(58.412, 62.485),
|
|
point(58.631, 62.202), seam,
|
|
};
|
|
|
|
REQUIRE(offset_wipe_path(path, seam, seam, seam, -1, scale_(0.23), scale_(0.8)));
|
|
REQUIRE(path.points.size() >= 3);
|
|
|
|
const Vec2d first = (path.points[1] - seam).cast<double>();
|
|
const Vec2d second = (path.points[2] - path.points[1]).cast<double>();
|
|
CHECK(first.dot(second) >= 0.);
|
|
}
|
|
|
|
TEST_CASE("Stored wipe path follows the inner wall at a narrow external cusp", "[WipePath][Regression]")
|
|
{
|
|
const auto point = [](double x, double y) { return Point::new_scale(x, y); };
|
|
const Point seam = point(55.139, 60.077);
|
|
Polyline path{
|
|
seam, point(55.156, 60.010), point(55.205, 59.961), point(55.237, 59.934),
|
|
point(55.304, 59.907), point(55.392, 59.872), point(55.630, 59.791),
|
|
point(56.564, 59.430), point(55.061, 59.956), point(55.108, 60.008), seam,
|
|
};
|
|
const Polyline original = path;
|
|
const Lines target_support{
|
|
Line(point(54.983, 59.648), point(55.121, 59.745)),
|
|
};
|
|
Lines printed_support = target_support;
|
|
printed_support.emplace_back(point(54.75, 60.25), point(55.50, 60.10));
|
|
|
|
REQUIRE(offset_wipe_path_toward_support(
|
|
path, seam, seam, seam, -1, scale_(0.270341), scale_(0.8),
|
|
target_support, printed_support, original.lines(), scale_(0.4)));
|
|
REQUIRE(path.points.size() >= 2);
|
|
CHECK(path.points[1].y() < seam.y() - scale_(0.2));
|
|
CHECK(std::abs(path.points[1].x() - seam.x()) < scale_(0.1));
|
|
}
|
|
|
|
TEST_CASE("Stored wipe path keeps a supported zero-gap join that initially backtracks", "[WipePath][Regression]")
|
|
{
|
|
const auto point = [](double x, double y) { return Point::new_scale(x, y); };
|
|
const Point seam = point(56.737, 62.049);
|
|
Polyline path{
|
|
seam, point(56.759, 62.142), point(56.727, 62.294), point(56.682, 62.447),
|
|
point(56.631, 62.570), point(56.581, 62.669), point(56.512, 62.776),
|
|
point(54.0, 64.0), point(50.0, 60.0), point(54.0, 58.0),
|
|
point(56.773, 62.031), seam,
|
|
};
|
|
const Polyline original = path;
|
|
const Lines target_support{
|
|
Line(point(56.546, 62.012), point(56.534, 62.104)),
|
|
Line(point(56.534, 62.104), point(56.506, 62.238)),
|
|
Line(point(56.506, 62.238), point(56.467, 62.371)),
|
|
Line(point(56.467, 62.371), point(56.424, 62.474)),
|
|
Line(point(56.424, 62.474), point(56.382, 62.556)),
|
|
};
|
|
|
|
Polyline inward = path;
|
|
REQUIRE(offset_wipe_path(inward, seam, seam, seam, +1, scale_(0.23), scale_(0.8)));
|
|
REQUIRE(inward.points.size() >= 3);
|
|
const Vec2d connector = (inward.points[1] - seam).cast<double>();
|
|
const Vec2d outgoing = (inward.points[2] - inward.points[1]).cast<double>();
|
|
REQUIRE(connector.dot(outgoing) < 0.);
|
|
|
|
REQUIRE(offset_wipe_path_toward_support(
|
|
path, seam, seam, seam, +1, scale_(0.23), scale_(0.8),
|
|
target_support, target_support, original.lines(), scale_(0.4)));
|
|
CHECK(path.points[1].x() < seam.x() - scale_(0.1));
|
|
}
|
|
|
|
TEST_CASE("Stored wipe path leaves a narrow cusp directly after a seam gap", "[WipePath][Regression]")
|
|
{
|
|
const auto point = [](double x, double y) { return Point::new_scale(x, y); };
|
|
const Point seam_start = point(55.139, 60.077);
|
|
const Point seam_end = point(55.141, 60.067);
|
|
Polyline path{
|
|
seam_start, point(55.107, 60.008), point(55.061, 59.956), point(55.027, 59.943),
|
|
point(54.982, 59.924), point(54.922, 59.879), point(54.868, 59.845),
|
|
point(54.754, 59.783), point(54.391, 59.635), point(54.053, 59.471),
|
|
};
|
|
const Polyline original = path;
|
|
const Lines target_support{
|
|
Line(point(55.132, 59.744), point(55.121, 59.745)),
|
|
Line(point(55.121, 59.745), point(54.983, 59.648)),
|
|
Line(point(54.983, 59.648), point(54.938, 59.623)),
|
|
Line(point(54.938, 59.623), point(54.866, 59.584)),
|
|
Line(point(54.866, 59.584), point(54.483, 59.427)),
|
|
Line(point(54.483, 59.427), point(54.157, 59.268)),
|
|
};
|
|
|
|
REQUIRE(offset_wipe_path_toward_support(
|
|
path, seam_start, seam_end, seam_end, -1, scale_(0.270341), scale_(0.8),
|
|
target_support, target_support, original.lines(), scale_(0.4)));
|
|
REQUIRE(path.points.size() >= 2);
|
|
CHECK(path.points[1].y() < seam_end.y() - scale_(0.2));
|
|
CHECK(std::abs(path.points[1].x() - seam_end.x()) < scale_(0.05));
|
|
|
|
// Orca: the inward connector must not run back through the first extruded
|
|
// point after the gap, which would put the wipe on the external wall.
|
|
const Line connector(seam_end, path.points[1]);
|
|
CHECK(connector.distance_to(original.points[1]) > scale_(0.02));
|
|
}
|
|
|
|
TEST_CASE("Stored wipe path does not reverse after an inward pre-move at a wide gap", "[WipePath][Regression]")
|
|
{
|
|
const auto point = [](double x, double y) { return Point::new_scale(x, y); };
|
|
const Point seam_start = point(55.139, 60.077);
|
|
const Point seam_end = point(55.163, 60.002);
|
|
const Point wipe_start = point(55.142, 60.037);
|
|
Polyline path{
|
|
seam_start, point(55.107, 60.008), point(55.061, 59.956), point(55.027, 59.943),
|
|
point(54.982, 59.924), point(54.922, 59.879), point(54.868, 59.845),
|
|
point(54.754, 59.783), point(54.391, 59.635), point(54.053, 59.471),
|
|
point(50.2, 55.0), point(50.2, 50.0), point(60.8, 50.0), point(60.8, 55.0),
|
|
point(56.564, 59.430), point(55.824, 59.708), point(55.392, 59.872),
|
|
point(55.237, 59.934), point(55.205, 59.961), seam_end,
|
|
};
|
|
const Polyline original = path;
|
|
const Lines target_support{
|
|
Line(point(55.132, 59.744), point(55.121, 59.745)),
|
|
Line(point(55.121, 59.745), point(54.983, 59.648)),
|
|
Line(point(54.983, 59.648), point(54.866, 59.584)),
|
|
Line(point(54.866, 59.584), point(54.483, 59.427)),
|
|
Line(point(54.483, 59.427), point(54.157, 59.268)),
|
|
};
|
|
|
|
REQUIRE(offset_wipe_path_toward_support(
|
|
path, seam_start, seam_end, wipe_start, -1, scale_(0.270341), scale_(0.8),
|
|
target_support, target_support, original.lines(), scale_(0.4)));
|
|
REQUIRE(path.points.size() >= 3);
|
|
|
|
const Vec2d connector = (path.points[1] - wipe_start).cast<double>();
|
|
const Vec2d outgoing = (path.points[2] - path.points[1]).cast<double>();
|
|
CHECK(connector.dot(outgoing) >= 0.);
|
|
path.points.front() = wipe_start;
|
|
CHECK_THAT(path.length(), Catch::Matchers::WithinAbs(scale_(0.8), 2.));
|
|
}
|
|
|
|
TEST_CASE("Stored wipe path follows the incoming wall when a corner gap truncates the forward path",
|
|
"[WipePath][Regression]")
|
|
{
|
|
const auto point = [](double x, double y) { return Point::new_scale(x, y); };
|
|
const Point seam_start = point(46.047, 61.988);
|
|
const Point seam_end = point(46.118, 61.917);
|
|
Polyline path{
|
|
seam_start, point(39.139, 55.080), point(46.047, 48.171),
|
|
point(52.956, 55.080), seam_end,
|
|
};
|
|
const Polyline original = path;
|
|
const Lines target_support{
|
|
Line(point(46.047, 61.672), point(39.461, 55.080)),
|
|
Line(point(39.461, 55.080), point(46.047, 48.493)),
|
|
Line(point(46.047, 48.493), point(52.633, 55.080)),
|
|
Line(point(52.633, 55.080), point(46.047, 61.672)),
|
|
};
|
|
|
|
REQUIRE(offset_wipe_path_toward_support(
|
|
path, seam_start, seam_end, seam_end, +1, scale_(0.23), scale_(0.8),
|
|
target_support, target_support, original.lines(), scale_(0.4)));
|
|
path.points.front() = seam_end;
|
|
CHECK_THAT(path.length(), Catch::Matchers::WithinAbs(scale_(0.8), 2.));
|
|
REQUIRE(path.points.size() >= 3);
|
|
CHECK(path.points[1].x() < seam_end.x());
|
|
CHECK(path.points[1].y() < seam_end.y());
|
|
}
|
|
|
|
TEST_CASE("Stored wipe path prefers support on the material side of a seam gap", "[WipePath][Regression]")
|
|
{
|
|
const coord_t s = scale_(1.0);
|
|
const Point seam_start(s, 0);
|
|
const Point seam_end(0, 0);
|
|
Polyline path{seam_start, Point(s, 10 * s), Point(s, 20 * s)};
|
|
const Polyline original = path;
|
|
const Lines target_support{
|
|
Line(Point(0, s), Point(0, 3 * s)),
|
|
Line(Point(s / 2, -s / 10), Point(3 * s / 2, -s / 10)),
|
|
};
|
|
|
|
// Orca: the lower line is closest at the cusp and the preferred winding
|
|
// points toward it, but the outgoing wall is adjacent to the upper line.
|
|
REQUIRE(offset_wipe_path_toward_support(
|
|
path, seam_start, seam_end, seam_end, -1, s, 5 * s,
|
|
target_support, target_support, original.lines(), 2 * s));
|
|
CHECK(path.points[1].y() > seam_end.y());
|
|
}
|
|
|
|
TEST_CASE("Stored wipe path rejects an outward offset at a reflex seam gap", "[WipePath][Regression]")
|
|
{
|
|
const double offset = GENERATE(0.2, 0.4); // 50% and 100% of a 0.4 mm wall.
|
|
const double mirror = GENERATE(1., -1.);
|
|
CAPTURE(offset, mirror);
|
|
const auto point = [mirror](double x, double y) { return Point::new_scale(mirror * x, y); };
|
|
const Point seam_start = point(0., 0.);
|
|
const double gap_component = 0.04 / std::sqrt(2.); // Default 10% seam gap for a 0.4 mm nozzle.
|
|
const Point seam_end = point(-gap_component, -gap_component);
|
|
const Polyline original{seam_start, point(0., -10.)};
|
|
const Lines support{Line(point(0.4, -10.), point(0.4, 1.))};
|
|
const int preferred_dir = mirror > 0. ? +1 : -1;
|
|
|
|
// The inward miter backtracks. The opposite offset can still be supported
|
|
// by the outer bead, so support alone must not make it an inward candidate.
|
|
Polyline outward = original;
|
|
REQUIRE(offset_wipe_path(outward, seam_start, seam_end, seam_end,
|
|
-preferred_dir, scale_(offset), scale_(2.)));
|
|
Lines all_support = support;
|
|
const Lines current = original.lines();
|
|
all_support.insert(all_support.end(), current.begin(), current.end());
|
|
REQUIRE(wipe_path_support_score(outward, seam_end,
|
|
LinesDistancer<Line>(support), LinesDistancer<Line>(all_support), scale_(0.4)).has_value());
|
|
REQUIRE(mirror * outward.points[1].x() < 0.);
|
|
|
|
Polyline path = original;
|
|
if (offset_wipe_path_toward_support(path, seam_start, seam_end, seam_end,
|
|
preferred_dir, scale_(offset), scale_(2.), support, support, current, scale_(0.4))) {
|
|
REQUIRE(path.points.size() >= 2);
|
|
CHECK(mirror * path.points[1].x() > 0.);
|
|
} else {
|
|
CHECK(path.points == original.points);
|
|
}
|
|
|
|
// An inward pre-move provides a clear connector to the direct fallback.
|
|
// The fix must retain this usable inward path, rather than reject all wipes.
|
|
const Point wipe_start = point(0.05, -0.04);
|
|
path = original;
|
|
REQUIRE(offset_wipe_path_toward_support(path, seam_start, seam_end, wipe_start,
|
|
preferred_dir, scale_(offset), scale_(2.), support, support, current, scale_(0.4)));
|
|
REQUIRE(path.points.size() == 2);
|
|
CHECK(mirror * path.points[1].x() > mirror * wipe_start.x());
|
|
}
|
|
|
|
TEST_CASE("Direct inward wipes respect the nozzle position and intervening walls", "[WipePath][Regression]")
|
|
{
|
|
const int mirror = GENERATE(1, -1);
|
|
const bool crossing_wall = GENERATE(false, true);
|
|
CAPTURE(mirror, crossing_wall);
|
|
const auto point = [mirror](double x, double y) { return Point::new_scale(mirror * x, y); };
|
|
const Point seam_start = point(0., 0.);
|
|
const double gap_component = 0.04 / std::sqrt(2.);
|
|
const Point seam_end = point(-gap_component, -gap_component);
|
|
const Polyline original{seam_start, point(0., -10.)};
|
|
const Lines support{Line(point(0.4, -10.), point(0.4, 1.))};
|
|
Lines current = original.lines();
|
|
// The direct destination is near x=0.172. A nozzle already farther inward
|
|
// must not return toward the wall. An inward connector from x=0.05 must
|
|
// still be rejected when another wall lies between it and the destination.
|
|
const Point wipe_start = point(crossing_wall ? 0.05 : 0.3, -0.04);
|
|
if (crossing_wall)
|
|
current.emplace_back(point(0.1, -0.2), point(0.1, 0.2));
|
|
Polyline path = original;
|
|
REQUIRE_FALSE(offset_wipe_path_toward_support(path, seam_start, seam_end, wipe_start,
|
|
mirror, scale_(0.2), scale_(2.), support, support, current, scale_(0.4)));
|
|
CHECK(path.points == original.points);
|
|
}
|
|
|
|
TEST_CASE("Inward wipe checks the material side after leaving an open wall endpoint", "[WipePath][Regression]")
|
|
{
|
|
const bool require_clearance = GENERATE(false, true);
|
|
CAPTURE(require_clearance);
|
|
const auto point = [](double x, double y) { return Point::new_scale(x, y); };
|
|
const Point seam = point(0., 0.);
|
|
const LinesDistancer<Line> current(Lines{Line(seam, point(2., 0.))});
|
|
const LinesDistancer<Line> support(Lines{Line(point(0., 0.4), point(2., 0.4))});
|
|
Polyline path{seam, point(0.1, 0.2), point(0.5, 0.2), point(-0.2, 0.2)};
|
|
REQUIRE(wipe_path_stays_on_material_side(
|
|
path, seam, Vec2d(0., 1.), support, current, scale_(0.2), require_clearance));
|
|
|
|
// Rounding the open endpoint keeps 0.2 mm of unsigned clearance while
|
|
// moving to the air side. Checking only the first direction cannot catch it.
|
|
path.points.push_back(point(-0.2, -0.2));
|
|
path.points.push_back(point(0.5, -0.2));
|
|
REQUIRE_FALSE(wipe_path_stays_on_material_side(
|
|
path, seam, Vec2d(0., 1.), support, current, scale_(0.2), require_clearance));
|
|
}
|
|
|
|
TEST_CASE("Direct inward fallbacks check the material side without requiring clearance", "[WipePath][Regression]")
|
|
{
|
|
const auto point = [](double x, double y) { return Point::new_scale(x, y); };
|
|
const Point seam = point(0., 0.);
|
|
const LinesDistancer<Line> current(Lines{Line(point(-2., 0.), point(2., 0.))});
|
|
const LinesDistancer<Line> support(Lines{Line(point(-2., 0.4), point(2., 0.4))});
|
|
REQUIRE(wipe_path_stays_on_material_side(
|
|
Polyline{seam, point(0., 0.05)}, seam, Vec2d(0., 1.), support, current, scale_(0.2), false));
|
|
|
|
// Even if the construction's initial direction points outward, the nearby
|
|
// inner wall still identifies the material side independently of that hint.
|
|
REQUIRE_FALSE(wipe_path_stays_on_material_side(
|
|
Polyline{seam, point(0., -0.05)}, seam, Vec2d(0., -1.), support, current, scale_(0.2), false));
|
|
}
|
|
|
|
TEST_CASE("Stored wipe path may return to the current wall after reaching an earlier wall", "[WipePath]")
|
|
{
|
|
const coord_t s = scale_(1.0);
|
|
const Polyline path{Point(0, 0), Point(0, 2 * s), Point(10 * s, 0)};
|
|
const Lines earlier{Line(Point(0, 2 * s), Point(10 * s, 2 * s))};
|
|
const Lines current{Line(Point(0, 0), Point(10 * s, 0))};
|
|
|
|
Lines all_support = earlier;
|
|
all_support.insert(all_support.end(), current.begin(), current.end());
|
|
REQUIRE(wipe_path_support_score(path, Point(0, 0),
|
|
LinesDistancer<Line>(earlier), LinesDistancer<Line>(all_support), s).has_value());
|
|
}
|
|
|
|
TEST_CASE("Stored wipe path tolerates compounded coordinate quantization", "[WipePath]")
|
|
{
|
|
const coord_t s = scale_(1.0);
|
|
const coord_t rounding = coord_t(3.5 * SCALED_EPSILON);
|
|
const Point destination(0, 2 * s + rounding);
|
|
const Polyline path{Point(0, 0), destination};
|
|
const Lines earlier{Line(Point(-s, 0), Point(s, 0))};
|
|
|
|
const LinesDistancer<Line> support_distancer(earlier);
|
|
REQUIRE(wipe_path_support_score(path, destination, support_distancer, support_distancer, 2 * s).has_value());
|
|
}
|
|
|
|
TEST_CASE("Stored wipe path stays on the inner side of a short external loop", "[WipePath][Regression]")
|
|
{
|
|
const auto point = [](double x, double y) { return Point::new_scale(x, y); };
|
|
const Point seam = point(55.270, 41.666);
|
|
Polyline path{
|
|
seam, point(55.241, 41.568), point(55.210, 41.518), point(55.195, 41.506),
|
|
point(55.173, 41.496), point(55.141, 41.479), point(55.126, 41.473),
|
|
point(55.068, 41.421), point(55.055, 41.416), point(55.006, 41.382),
|
|
point(54.808, 41.231), point(54.687, 41.153), point(54.590, 41.069),
|
|
point(54.529, 41.027), point(54.441, 40.949), point(54.299, 40.803),
|
|
point(54.219, 40.674), point(54.183, 40.581), point(54.172, 40.511),
|
|
point(54.182, 40.450), point(54.225, 40.358), point(54.256, 40.318),
|
|
point(54.341, 40.251), point(54.418, 40.211), point(54.499, 40.176),
|
|
point(54.675, 40.124), point(54.797, 40.106), point(54.978, 40.092),
|
|
point(55.245, 40.093), point(55.443, 40.103), point(55.591, 40.128),
|
|
point(55.771, 40.164), point(55.962, 40.217), point(56.103, 40.264),
|
|
point(56.167, 40.295), point(56.246, 40.341), point(56.338, 40.412),
|
|
point(56.382, 40.469), point(56.396, 40.527), point(56.386, 40.609),
|
|
point(56.313, 40.740), point(56.208, 40.867), point(56.071, 40.991),
|
|
point(55.946, 41.094), point(55.812, 41.198), point(55.722, 41.262),
|
|
point(55.665, 41.294), point(55.556, 41.398), point(55.520, 41.414),
|
|
point(55.495, 41.424), point(55.478, 41.437), point(55.442, 41.469),
|
|
point(55.407, 41.505), point(55.386, 41.510), point(55.367, 41.516),
|
|
point(55.335, 41.535), point(55.292, 41.575), seam,
|
|
};
|
|
const Polyline original = path;
|
|
const Polyline inner{
|
|
point(55.111, 41.176), point(54.946, 41.050), point(54.824, 40.970),
|
|
point(54.733, 40.892), point(54.668, 40.846), point(54.598, 40.784),
|
|
point(54.480, 40.662), point(54.424, 40.572), point(54.403, 40.516),
|
|
point(54.420, 40.479), point(54.465, 40.443), point(54.577, 40.390),
|
|
point(54.723, 40.347), point(54.823, 40.333), point(54.986, 40.320),
|
|
point(55.239, 40.321), point(55.418, 40.330), point(55.550, 40.352),
|
|
point(55.718, 40.386), point(55.896, 40.435), point(56.017, 40.476),
|
|
point(56.061, 40.497), point(56.118, 40.530), point(56.154, 40.558),
|
|
point(56.124, 40.611), point(56.043, 40.709), point(55.921, 40.819),
|
|
point(55.804, 40.916), point(55.676, 41.015), point(55.600, 41.069),
|
|
point(55.526, 41.114), point(55.426, 41.207), point(55.379, 41.235),
|
|
point(55.349, 41.259), point(55.291, 41.317), point(55.252, 41.293),
|
|
point(55.192, 41.238), point(55.111, 41.176),
|
|
};
|
|
Lines target_support = inner.lines();
|
|
// Orca: a different contour has a slightly closer inner wall on the air
|
|
// side of this short loop. It must not override the loop's material side.
|
|
target_support.emplace_back(point(55.159, 42.147), point(55.299, 42.011));
|
|
|
|
REQUIRE(offset_wipe_path_toward_support(
|
|
path, seam, seam, seam, +1, scale_(0.293166), scale_(0.8),
|
|
target_support, target_support, original.lines(), scale_(0.4)));
|
|
REQUIRE(path.points.size() >= 2);
|
|
|
|
// Orca: the nearest inner wall is below the seam; accepting the opposite
|
|
// offset would send the wipe into air outside this small contour.
|
|
CHECK(path.points[1].y() < seam.y());
|
|
}
|
|
|
|
TEST_CASE("Stored wipe path does not return to the external wall after moving inward", "[WipePath][Regression]")
|
|
{
|
|
const auto point = [](double x, double y) { return Point::new_scale(x, y); };
|
|
const Point seam = point(47.451, 54.647);
|
|
Polyline path{
|
|
seam, point(47.370, 54.634), point(47.345, 54.619), point(47.333, 54.604),
|
|
point(47.322, 54.572), point(47.312, 54.518), point(47.315, 54.445),
|
|
point(47.345, 54.257), point(47.357, 54.206), point(47.380, 54.135),
|
|
point(47.418, 54.065), point(47.514, 53.917), point(47.537, 53.886),
|
|
point(47.597, 53.834), point(47.705, 53.769), point(47.747, 53.748),
|
|
point(47.785, 53.734), point(47.825, 53.735), point(47.862, 53.746),
|
|
point(47.889, 53.763), point(47.939, 53.817), point(47.964, 53.856),
|
|
point(47.979, 53.897), point(47.986, 53.943), point(47.986, 54.005),
|
|
point(47.977, 54.075), point(47.949, 54.188), point(47.902, 54.321),
|
|
point(47.871, 54.388), point(47.835, 54.444), point(47.765, 54.521),
|
|
point(47.741, 54.542), point(47.675, 54.589), point(47.615, 54.620),
|
|
point(47.518, 54.642), seam,
|
|
};
|
|
const Polyline original = path;
|
|
const Polyline inner{
|
|
point(47.541, 54.281), point(47.545, 54.258), point(47.560, 54.212),
|
|
point(47.577, 54.181), point(47.682, 54.017), point(47.707, 53.995),
|
|
point(47.789, 53.946), point(47.791, 53.958), point(47.791, 53.992),
|
|
point(47.785, 54.039), point(47.762, 54.132), point(47.721, 54.249),
|
|
point(47.700, 54.294), point(47.680, 54.324), point(47.628, 54.382),
|
|
point(47.574, 54.422), point(47.548, 54.435), point(47.513, 54.443),
|
|
point(47.541, 54.281),
|
|
};
|
|
const double offset = scale_(0.229999);
|
|
|
|
REQUIRE(offset_wipe_path_toward_support(
|
|
path, seam, seam, seam, +1, offset, scale_(0.8),
|
|
inner.lines(), inner.lines(), original.lines(), scale_(0.4)));
|
|
|
|
// Orca: after reaching the inner wall, a full-width inward wipe must not
|
|
// collapse back onto the external perimeter at a tight turn.
|
|
for (size_t index = 1; index < path.points.size(); ++index) {
|
|
double clearance = std::numeric_limits<double>::infinity();
|
|
for (const Line &line : original.lines())
|
|
clearance = std::min(clearance, line.distance_to(path.points[index]));
|
|
CHECK(clearance >= 0.75 * offset);
|
|
}
|
|
}
|
|
|
|
// Orca: wipe_on_loops_destination coverage for every orientation.
|
|
|
|
TEST_CASE("wipe_on_loops destination is on the material side for every orientation", "[WipePath]")
|
|
{
|
|
const auto [is_ccw, is_hole] = GENERATE(
|
|
table<bool, bool>({{true, false}, {false, false}, {false, true}, {true, true}}));
|
|
INFO("is_ccw=" << is_ccw << ", is_hole=" << is_hole);
|
|
const double nozzle_diameter = GENERATE(0.4, 0.8);
|
|
const bool subdivided = GENERATE(false, true);
|
|
INFO("nozzle diameter=" << nozzle_diameter << ", subdivided=" << subdivided);
|
|
|
|
const coord_t s = scale_(1.0);
|
|
std::vector<Point> contour = {Point(0, 0), Point(20 * s, 0), Point(20 * s, 20 * s), Point(0, 20 * s)};
|
|
if (subdivided) {
|
|
// The same square, with path boundaries inside both sampling distances near the seam.
|
|
contour = {Point(0, 0), Point(scale_(0.03), 0.), Point(scale_(0.2), 0.),
|
|
Point(20 * s, 0), Point(20 * s, 20 * s), Point(0, 20 * s),
|
|
Point(0., scale_(0.2)), Point(0., scale_(0.03))};
|
|
}
|
|
if (!is_ccw)
|
|
for (Point &point : contour)
|
|
std::swap(point.x(), point.y());
|
|
ExtrusionPaths paths;
|
|
if (subdivided) {
|
|
for (size_t i = 0; i < contour.size(); ++i)
|
|
paths.push_back(make_path({contour[i], contour[(i + 1) % contour.size()]}));
|
|
} else {
|
|
paths = make_loop_paths(contour);
|
|
}
|
|
|
|
const std::optional<Point> destination =
|
|
wipe_on_loops_destination(paths, scale_(nozzle_diameter), is_ccw, is_hole);
|
|
REQUIRE(destination.has_value());
|
|
|
|
const Point seam_start = paths.front().first_point();
|
|
const Vec2d first_edge = (paths.front().polyline.points[1].to_point() - seam_start).cast<double>();
|
|
Vec2d material_normal(-first_edge.y(), first_edge.x());
|
|
if (is_ccw == is_hole)
|
|
material_normal = -material_normal;
|
|
|
|
// Orca: contours use their winding's inside; holes use the opposite side.
|
|
const Vec2d move = destination->cast<double>() - seam_start.cast<double>();
|
|
REQUIRE(move.dot(material_normal) > 0.);
|
|
// Move 20% of the nozzle diameter, turning through one third of the material-side
|
|
// corner: 90 degrees for a contour, 270 degrees for a hole.
|
|
const double distance = scale_(0.2 * nozzle_diameter);
|
|
const double angle = is_hole ? PI / 2. : PI / 6.;
|
|
CHECK_THAT(move.dot(first_edge.normalized()), Catch::Matchers::WithinAbs(distance * std::cos(angle), 2.));
|
|
CHECK_THAT(move.dot(material_normal.normalized()), Catch::Matchers::WithinAbs(distance * std::sin(angle), 2.));
|
|
}
|
|
|
|
TEST_CASE("wipe_on_loops returns destination for small but nonzero loop", "[WipePath]")
|
|
{
|
|
// Orca: a 0.5 mm square is tight for a 0.4 mm nozzle but remains valid.
|
|
const coord_t s = scale_(1.0);
|
|
auto paths = make_loop_paths({Point(0, 0), Point(s / 2, 0), Point(s / 2, s / 2), Point(0, s / 2)});
|
|
|
|
auto dest = wipe_on_loops_destination(paths, scale_(0.4), true, false);
|
|
REQUIRE(dest.has_value());
|
|
}
|
|
|
|
TEST_CASE("wipe_on_loops destination is nullopt for degenerate single-point path", "[WipePath]")
|
|
{
|
|
const coord_t s = scale_(1.0);
|
|
auto paths = make_paths({Point(50 * s, 50 * s)});
|
|
|
|
auto dest = wipe_on_loops_destination(paths, scale_(0.4), true, false);
|
|
REQUIRE_FALSE(dest.has_value());
|
|
}
|