mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-16 13:32:44 +00:00
Feature: Add inward wipe for external perimeters (#15407)
This commit is contained in:
@@ -0,0 +1,170 @@
|
|||||||
|
# Wipe inward — High Level Design
|
||||||
|
|
||||||
|
## Purpose and scope
|
||||||
|
|
||||||
|
Wipe inward reduces reheating of fresh plastic and visible seam artifacts by
|
||||||
|
moving the hot nozzle toward adjacent printed material during the external-wall
|
||||||
|
wipe. Wipe marks are especially visible at layer heights below 0.1 mm.
|
||||||
|
The option applies only to wipes after external walls, including walls around
|
||||||
|
holes. It does not offset wipes after inner walls, infill or supports. For an
|
||||||
|
outer contour the move is inward; for a hole it is away from the hole, toward
|
||||||
|
the surrounding material. The path must remain supported by material that is
|
||||||
|
already present when the wipe executes.
|
||||||
|
|
||||||
|
The operation belongs to G-code generation. It uses extrusion paths, their actual
|
||||||
|
widths and their print order. Changing its settings invalidates G-code export
|
||||||
|
while preserving the sliced geometry.
|
||||||
|
|
||||||
|
## Settings and eligibility
|
||||||
|
|
||||||
|
`wipe_inward` defaults to disabled and requires Wipe while retracting to be
|
||||||
|
enabled for the active filament. `wipe_inward_distance` defaults to 50% of the
|
||||||
|
actual external-wall extrusion width; it also accepts an absolute distance in
|
||||||
|
millimeters. Using the path width makes Auto width and Arachne's variable widths
|
||||||
|
meaningful. The effective offset is limited by that width and the spacing to the
|
||||||
|
adjacent wall. A zero distance disables the offset.
|
||||||
|
|
||||||
|
Only external perimeters with a suitable, previously printed inner perimeter
|
||||||
|
are eligible. A configured wall count alone cannot establish eligibility:
|
||||||
|
the local geometry may contain fewer walls, and walls scheduled later do not
|
||||||
|
provide support. Outer/Inner wall order therefore normally retains the regular
|
||||||
|
wipe path.
|
||||||
|
|
||||||
|
Retraction and pressure advance calibrations disable inward wiping so it cannot
|
||||||
|
mask the behavior being measured. The calibration settings turn it off, and
|
||||||
|
G-code generation enforces this even if a profile or object override enables it.
|
||||||
|
|
||||||
|
## Path selection and support
|
||||||
|
|
||||||
|
The planner identifies an adjacent inner perimeter on the material side of the
|
||||||
|
outgoing wall. Contour winding and the distinction between outer contours and
|
||||||
|
holes establish a preferred direction; local printed geometry resolves ambiguous
|
||||||
|
or self-touching contours.
|
||||||
|
|
||||||
|
Candidate paths offset or translate the portion needed for the configured wipe
|
||||||
|
distance. A wide seam gap can prevent a supported forward path; following the
|
||||||
|
incoming printed wall backwards is also a candidate. If translating that wall
|
||||||
|
cannot provide a complete wipe around a curve, the planner tries an offset of
|
||||||
|
the reversed wall. Direction checks allow coordinate-rounding error at a
|
||||||
|
perpendicular entry, while rejecting actual backtracking. The planner checks the
|
||||||
|
complete executable path, including its connector from the nozzle position,
|
||||||
|
against the current and earlier printed perimeters. Nearby endpoints alone do
|
||||||
|
not establish support across a gap.
|
||||||
|
|
||||||
|
Each region accumulates its printed perimeter prefix once, in extrusion order.
|
||||||
|
Every entity contributes its geometry only after it is printed, and the prefix
|
||||||
|
is discarded when the region ends. This collection is skipped when inward wiping
|
||||||
|
is disabled or its configured distance is zero. A mixed inner-wall loop remains
|
||||||
|
an eligible target even when its first path is an overhang: ordinary inner-wall
|
||||||
|
paths elsewhere in the loop identify it. Likewise, an external loop with an
|
||||||
|
overhanging start remains eligible when other segments identify the external
|
||||||
|
wall. It is available for support checks but is not an inner-wall target.
|
||||||
|
Candidate-specific support filtering and AABB trees are built only for eligible
|
||||||
|
external loops, then reused across their candidate paths.
|
||||||
|
|
||||||
|
Material-side validation applies with or without a seam gap. Along each
|
||||||
|
candidate, local wall normals point toward the adjacent printed inner wall;
|
||||||
|
samples on the opposite side are rejected even when they remain close enough
|
||||||
|
to the external wall to pass the support check. This uses the open wall geometry
|
||||||
|
without treating it as a closed polygon. Full paths at a zero-gap seam also
|
||||||
|
retain clearance from the external wall after their initial connector. At a
|
||||||
|
clipped corner, another branch can be closer than the requested offset, so
|
||||||
|
material-side and support checks apply without that additional clearance rule.
|
||||||
|
|
||||||
|
An accepted candidate replaces the stored wipe path as a whole. A short direct
|
||||||
|
inward move is also eligible when longer candidates fail validation. It may
|
||||||
|
waive full wall clearance, but must pass the material-side check. Its initial
|
||||||
|
direction is checked from the actual nozzle position after any loop pre-move;
|
||||||
|
the original wall endpoint is retained separately for intersection checks. It takes
|
||||||
|
priority over the alternate offset when the preferred and translated paths
|
||||||
|
are unusable. A longer reversed path may replace the selected candidate only
|
||||||
|
when its distance to the target inner wall is no worse within tolerance.
|
||||||
|
|
||||||
|
## Fallback to the regular wipe
|
||||||
|
|
||||||
|
The original wipe path is retained when:
|
||||||
|
|
||||||
|
- No suitable adjacent inner wall has already been printed near the seam. This
|
||||||
|
includes single-wall areas, locally missing inner walls and normally Outer/Inner
|
||||||
|
wall order. A distant wall or a wall on the air side does not qualify.
|
||||||
|
- The requested or available offset, or the configured wipe distance, is zero
|
||||||
|
or too small at the geometry's coordinate precision.
|
||||||
|
- Degenerate geometry prevents construction of a usable candidate, or all
|
||||||
|
candidates fail the checks for printed support, direction, wall clearance or
|
||||||
|
the connector from the actual nozzle position. This can occur at tight corners,
|
||||||
|
narrow features or seam gaps.
|
||||||
|
|
||||||
|
Corners and seam gaps do not automatically trigger fallback: an offset,
|
||||||
|
translated, reversed or short direct inward path may still be valid. The regular
|
||||||
|
wipe is retained only when no candidate is accepted.
|
||||||
|
|
||||||
|
Fallback uses the path and retraction rules for `wipe_inward` disabled.
|
||||||
|
Wipe while retracting must still be enabled for a wipe to occur; `wipe_on_loops`
|
||||||
|
remains controlled by its own setting.
|
||||||
|
|
||||||
|
## Interaction with Wipe on loop
|
||||||
|
|
||||||
|
`wipe_on_loops` is an independent option that makes a short move before leaving
|
||||||
|
an external loop. It can operate with `wipe_inward` disabled. When both options
|
||||||
|
are enabled, its destination is the starting position for the inward wipe.
|
||||||
|
|
||||||
|
The loop move samples the outgoing and incoming paths by distance across path
|
||||||
|
boundaries. The sampling distance is bounded by the nozzle diameter and one
|
||||||
|
quarter of the total path length. It samples the outgoing path at up to 20% of
|
||||||
|
the nozzle diameter and rotates that point around the seam through one third
|
||||||
|
of the material-side corner angle. For a closed square outer contour, this
|
||||||
|
produces a move of 20% of the nozzle diameter at 30 degrees into the corner.
|
||||||
|
Coincident samples or degenerate angles suppress the move.
|
||||||
|
|
||||||
|
The nozzle position stored by G-code generation must match the emitted loop
|
||||||
|
move. Both travel planning and wipe execution depend on this position, including
|
||||||
|
when Wipe inward is disabled.
|
||||||
|
|
||||||
|
With a seam gap, a loop move may advance past the inward offset's original entry.
|
||||||
|
If that alone makes the connector backtrack, the entry advances to the nozzle's
|
||||||
|
projection on the offset. The planner extends the source as needed to preserve
|
||||||
|
the configured wipe length and validates the new connector and complete path.
|
||||||
|
Joins that already backtrack across the seam gap are not adjusted this way.
|
||||||
|
|
||||||
|
## Execution and retraction
|
||||||
|
|
||||||
|
The stored wipe path uses a sentinel first point. Execution starts from the
|
||||||
|
actual nozzle position and proceeds to the second stored point. Path selection,
|
||||||
|
support validation and wipe-length calculation must all use this same executable
|
||||||
|
geometry, especially after a Wipe on loop move.
|
||||||
|
|
||||||
|
An accepted inward path executes at the end of the external loop, after any
|
||||||
|
Wipe on loop move, without retracting filament. It consumes the stored path and
|
||||||
|
updates the nozzle position before travel planning. A short travel to the next
|
||||||
|
wall cannot discard this wipe or force a retraction or Z-hop. Subsequent travel
|
||||||
|
uses the normal minimum-travel threshold and retraction/lift settings from the
|
||||||
|
new position. The regular wipe, including fallback, remains deferred until a
|
||||||
|
normal retraction uses it.
|
||||||
|
|
||||||
|
Retraction is divided into portions before, during and after wiping. The amount
|
||||||
|
that can be retracted during the wipe depends on its executable length, wipe
|
||||||
|
speed and the active filament's retraction speed. Fractional retraction speeds
|
||||||
|
are retained in this calculation. For a 2 mm wipe at 100 mm/s and a retraction
|
||||||
|
speed of 25.5 mm/s, the wipe can retract 0.51 mm. With a total retraction of 0.8 mm
|
||||||
|
and both before/after percentages set to zero, the remaining 0.29 mm is retracted
|
||||||
|
before wiping. This split applies to regular deferred wipes, including fallback;
|
||||||
|
an accepted inward wipe executes separately without retraction.
|
||||||
|
|
||||||
|
## Implementation and verification
|
||||||
|
|
||||||
|
- [GCode.cpp](../../src/libslic3r/GCode.cpp) integrates path selection, nozzle
|
||||||
|
position and retraction; [Print.cpp](../../src/libslic3r/Print.cpp) controls
|
||||||
|
invalidation, and [PrintConfig.cpp](../../src/libslic3r/PrintConfig.cpp) defines
|
||||||
|
the settings.
|
||||||
|
- [WipePathHelpers](../../src/libslic3r/GCode/WipePathHelpers.hpp) implements path
|
||||||
|
sampling, offset selection and support checks.
|
||||||
|
- [Geometry tests](../../tests/libslic3r/test_wipe_path.cpp) cover support,
|
||||||
|
degenerate paths, contour and hole orientations, and exact loop-move geometry
|
||||||
|
across path subdivisions.
|
||||||
|
- [FFF tests](../../tests/fff_print/test_wipe.cpp) cover emitted trajectories,
|
||||||
|
fallback, minimum-travel retraction and Z-hop rules, and export invalidation.
|
||||||
|
With Wipe inward disabled, they check the loop move's direction and magnitude
|
||||||
|
for Classic and Arachne, the subsequent wipe's start and length, and fractional
|
||||||
|
retraction splitting in absolute and relative E modes.
|
||||||
|
Loop-move checks use reserved role/wipe markers and extrusion state, and run
|
||||||
|
with human-readable G-code comments both enabled and disabled.
|
||||||
@@ -260,6 +260,8 @@ set(lisbslic3r_sources
|
|||||||
GCode/SmallAreaInfillFlowCompensator.hpp
|
GCode/SmallAreaInfillFlowCompensator.hpp
|
||||||
GCode/SpiralVase.cpp
|
GCode/SpiralVase.cpp
|
||||||
GCode/SpiralVase.hpp
|
GCode/SpiralVase.hpp
|
||||||
|
GCode/WipePathHelpers.cpp
|
||||||
|
GCode/WipePathHelpers.hpp
|
||||||
GCode/ThumbnailData.cpp
|
GCode/ThumbnailData.cpp
|
||||||
GCode/ThumbnailData.hpp
|
GCode/ThumbnailData.hpp
|
||||||
GCode/Thumbnails.cpp
|
GCode/Thumbnails.cpp
|
||||||
|
|||||||
+119
-79
@@ -1,5 +1,6 @@
|
|||||||
#include "BoundingBox.hpp"
|
#include "BoundingBox.hpp"
|
||||||
#include "Config.hpp"
|
#include "Config.hpp"
|
||||||
|
#include "GCode/WipePathHelpers.hpp"
|
||||||
#include "GCodeWriter.hpp"
|
#include "GCodeWriter.hpp"
|
||||||
#include "Polygon.hpp"
|
#include "Polygon.hpp"
|
||||||
#include "PrintConfig.hpp"
|
#include "PrintConfig.hpp"
|
||||||
@@ -438,7 +439,6 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
|
|||||||
auto& writer = gcodegen.writer();
|
auto& writer = gcodegen.writer();
|
||||||
auto& config = gcodegen.config();
|
auto& config = gcodegen.config();
|
||||||
auto extruder = writer.filament();
|
auto extruder = writer.filament();
|
||||||
auto extruder_id = extruder->extruder_id();
|
|
||||||
auto last_pos = gcodegen.last_pos();
|
auto last_pos = gcodegen.last_pos();
|
||||||
|
|
||||||
// Declare & initialize retraction lengths
|
// Declare & initialize retraction lengths
|
||||||
@@ -475,13 +475,13 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
|
|||||||
wipe_speed = std::max(wipe_speed, 10.0);
|
wipe_speed = std::max(wipe_speed, 10.0);
|
||||||
|
|
||||||
// Process wipe path & calculate wipe path length
|
// Process wipe path & calculate wipe path length
|
||||||
double wipe_dist = scale_(config.wipe_distance.get_at(extruder_id));
|
double wipe_dist = scale_(config.wipe_distance.get_at(extruder->config_index()));
|
||||||
Polyline wipe_path = {last_pos};
|
Polyline wipe_path = {last_pos};
|
||||||
wipe_path.append(this->path.points.begin() + 1, this->path.points.end());
|
wipe_path.append(this->path.points.begin() + 1, this->path.points.end());
|
||||||
double wipe_path_length = std::min(wipe_path.length(), wipe_dist);
|
double wipe_path_length = std::min(wipe_path.length(), wipe_dist);
|
||||||
|
|
||||||
// Calculate the maximum retraction amount during wipe
|
// Calculate the maximum retraction amount during wipe
|
||||||
retraction_length_during_wipe = config.retraction_speed.get_at(extruder_id) *
|
retraction_length_during_wipe = config.retraction_speed.get_at(extruder->config_index()) *
|
||||||
unscale_(wipe_path_length) / wipe_speed;
|
unscale_(wipe_path_length) / wipe_speed;
|
||||||
|
|
||||||
// If the maximum retraction amount during wipe is too small,
|
// If the maximum retraction amount during wipe is too small,
|
||||||
@@ -564,6 +564,16 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
|
|||||||
return default_value;
|
return default_value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Orca: rebuild the stored wipe path while preserving Polyline's boundary deduplication.
|
||||||
|
void Wipe::update_path(const ExtrusionPaths &paths, bool reverse)
|
||||||
|
{
|
||||||
|
reset_path();
|
||||||
|
for (const ExtrusionPath& extrusion_path : paths)
|
||||||
|
path.append(extrusion_path.polyline.to_polyline());
|
||||||
|
if (reverse)
|
||||||
|
path.reverse();
|
||||||
|
}
|
||||||
|
|
||||||
std::string Wipe::wipe(GCode& gcodegen,double length, bool toolchange, bool is_last)
|
std::string Wipe::wipe(GCode& gcodegen,double length, bool toolchange, bool is_last)
|
||||||
{
|
{
|
||||||
std::string gcode;
|
std::string gcode;
|
||||||
@@ -616,14 +626,11 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
|
|||||||
if (gcodegen.enable_cooling_markers() && !is_last)
|
if (gcodegen.enable_cooling_markers() && !is_last)
|
||||||
cooling_mark = /*gcodegen.config().role_based_wipe_speed ? ";_EXTERNAL_PERIMETER" : */";_WIPE";
|
cooling_mark = /*gcodegen.config().role_based_wipe_speed ? ";_EXTERNAL_PERIMETER" : */";_WIPE";
|
||||||
|
|
||||||
|
// Orca: set speed once because wipe_speed is constant for all segments.
|
||||||
gcode += gcodegen.writer().set_speed(_wipe_speed * 60, "", cooling_mark);
|
gcode += gcodegen.writer().set_speed(_wipe_speed * 60, "", cooling_mark);
|
||||||
for (const Line& line : wipe_path.lines()) {
|
for (const Line& line : wipe_path.lines()) {
|
||||||
double segment_length = line.length();
|
double segment_length = line.length();
|
||||||
double dE = length * (segment_length / wipe_dist);
|
double dE = length * (segment_length / wipe_dist);
|
||||||
//BBS: fix this FIXME
|
|
||||||
//FIXME one shall not generate the unnecessary G1 Fxxx commands, here wipe_speed is a constant inside this cycle.
|
|
||||||
// Is it here for the cooling markers? Or should it be outside of the cycle?
|
|
||||||
//gcode += gcodegen.writer().set_speed(wipe_speed * 60, "", gcodegen.enable_cooling_markers() ? ";_WIPE" : "");
|
|
||||||
gcode += gcodegen.writer().extrude_to_xy(
|
gcode += gcodegen.writer().extrude_to_xy(
|
||||||
gcodegen.point_to_gcode(line.b),
|
gcodegen.point_to_gcode(line.b),
|
||||||
-dE,
|
-dE,
|
||||||
@@ -2901,6 +2908,19 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
|
|||||||
const bool skip_config_block = print.config().gcode_skip_config_block;
|
const bool skip_config_block = print.config().gcode_skip_config_block;
|
||||||
const WipeTowerType wipe_tower_type = print.wipe_tower_type();
|
const WipeTowerType wipe_tower_type = print.wipe_tower_type();
|
||||||
m_calib_config.clear();
|
m_calib_config.clear();
|
||||||
|
// Orca: Calibration overrides are reapplied after object/region settings in _extrude().
|
||||||
|
// Keep inward wiping from masking retraction and pressure advance artifacts.
|
||||||
|
switch (print.calib_mode()) {
|
||||||
|
case CalibMode::Calib_PA_Line:
|
||||||
|
case CalibMode::Calib_PA_Pattern:
|
||||||
|
case CalibMode::Calib_PA_Tower:
|
||||||
|
case CalibMode::Calib_Auto_PA_Line:
|
||||||
|
case CalibMode::Calib_Retraction_tower:
|
||||||
|
m_calib_config.set_key_value("wipe_inward", new ConfigOptionBool(false));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
// resets analyzer's tracking data
|
// resets analyzer's tracking data
|
||||||
m_last_height = 0.f;
|
m_last_height = 0.f;
|
||||||
m_last_layer_z = 0.f;
|
m_last_layer_z = 0.f;
|
||||||
@@ -7204,7 +7224,8 @@ std::string GCode::extrude_loop(const ExtrusionLoop& loop_ref,
|
|||||||
const std::string& description,
|
const std::string& description,
|
||||||
double speed,
|
double speed,
|
||||||
const ExtrusionEntitiesPtr& region_perimeters,
|
const ExtrusionEntitiesPtr& region_perimeters,
|
||||||
const Point* start_point)
|
const Point* start_point,
|
||||||
|
const WipeInwardSupport* wipe_support)
|
||||||
{
|
{
|
||||||
// get a copy; don't modify the orientation of the original loop object otherwise
|
// get a copy; don't modify the orientation of the original loop object otherwise
|
||||||
// next copies (if any) would not detect the correct orientation
|
// next copies (if any) would not detect the correct orientation
|
||||||
@@ -7434,63 +7455,80 @@ std::string GCode::extrude_loop(const ExtrusionLoop& loop_ref,
|
|||||||
m_processor.result().print_statistics.total_seam_scarf_distance += static_cast<float>(seam_scarf_distance_mm);
|
m_processor.result().print_statistics.total_seam_scarf_distance += static_cast<float>(seam_scarf_distance_mm);
|
||||||
}
|
}
|
||||||
|
|
||||||
// BBS
|
// Orca: share the post-extrusion nozzle position between wipe_inward and wipe_on_loops.
|
||||||
|
const bool is_ccw = loop.is_counter_clockwise();
|
||||||
|
|
||||||
|
std::optional<Point> wipe_on_loops_dest;
|
||||||
|
if (m_config.wipe_on_loops.value && paths.back().role() == erExternalPerimeter &&
|
||||||
|
m_layer != nullptr && m_config.wall_loops.value > 1 && paths.front().size() >= 2 &&
|
||||||
|
paths.back().polyline.points.size() >= 2)
|
||||||
|
wipe_on_loops_dest = wipe_on_loops_destination(paths, scale_(nozzle_diameter), is_ccw, is_hole);
|
||||||
|
|
||||||
|
bool wipe_inward_applied = false;
|
||||||
|
// Orca: store loop paths in print order because inward offsets use this orientation.
|
||||||
if (m_wipe.enable && FILAMENT_CONFIG(wipe)) {
|
if (m_wipe.enable && FILAMENT_CONFIG(wipe)) {
|
||||||
m_wipe.path = Polyline();
|
m_wipe.update_path(paths);
|
||||||
for (ExtrusionPath &path : paths) {
|
|
||||||
//BBS: Don't need to save duplicated point into wipe path
|
// Orca: loop wipe paths retain print direction. Their material side is
|
||||||
if (!m_wipe.path.empty() && !path.empty() &&
|
// therefore left for CCW contours and right for CW contours, with the
|
||||||
m_wipe.path.last_point() == Point(path.first_point().x(), path.first_point().y())) {
|
// result inverted for holes. Only external perimeters are eligible.
|
||||||
// Convert Points3 to Points
|
// Calibration overrides are applied during extrusion, after the region
|
||||||
for (auto it = path.polyline.points.begin() + 1; it != path.polyline.points.end(); ++it)
|
// context was created. Check the effective setting again at execution.
|
||||||
m_wipe.path.append(Point(it->x(), it->y()));
|
if (m_config.wipe_inward && m_config.wipe_inward_distance.value > 0. &&
|
||||||
} else
|
wipe_support != nullptr && !wipe_support->inner_lines.empty() &&
|
||||||
m_wipe.path.append(path.polyline.to_polyline()); // TODO: don't limit wipe to last path
|
// A loop's role is its first path's role. An overhanging start must
|
||||||
|
// not hide ordinary external-wall segments elsewhere in the loop.
|
||||||
|
std::any_of(paths.begin(), paths.end(),
|
||||||
|
[](const ExtrusionPath &path) { return is_external_perimeter(path.role()); }) &&
|
||||||
|
m_wipe.path.points.size() >= 2) {
|
||||||
|
// Orca: use the actual extrusion width from the path, not the config
|
||||||
|
// value — outer_wall_line_width=0 (Auto) would make get_abs_value
|
||||||
|
// return 0 and silently disable the feature, and Arachne may produce
|
||||||
|
// a different width than the config default.
|
||||||
|
const double outer_wall_line_width = paths.front().width;
|
||||||
|
const double requested_offset = m_config.wipe_inward_distance.get_abs_value(outer_wall_line_width);
|
||||||
|
const double offset_dist = scale_(std::min(requested_offset, outer_wall_line_width));
|
||||||
|
if (offset_dist > SCALED_EPSILON) {
|
||||||
|
const Point seam_start = paths.front().first_point();
|
||||||
|
const Point seam_end = paths.back().last_point();
|
||||||
|
const Point wipe_start = wipe_on_loops_dest.value_or(seam_end);
|
||||||
|
const double max_wipe_length = scale_(FILAMENT_CONFIG(wipe_distance));
|
||||||
|
// Orca: Wipe::wipe() replaces points[0] with last_pos and executes
|
||||||
|
// from points[1]. The helper preserves that sentinel and atomically
|
||||||
|
// replaces the remaining points, or leaves the path untouched.
|
||||||
|
// Orca: a configured wall count does not guarantee that Arachne
|
||||||
|
// generated an adjacent wall for this particular loop. Only
|
||||||
|
// earlier entities are considered because later walls have
|
||||||
|
// not been printed yet (for example with Outer/Inner order).
|
||||||
|
// Inner walls determine the material side; every earlier wall
|
||||||
|
// remains available to validate the executable wipe path.
|
||||||
|
const double support_distance = scale_(std::max(nozzle_diameter, outer_wall_line_width));
|
||||||
|
Polyline inward_path = m_wipe.path;
|
||||||
|
if (offset_wipe_path_toward_support(
|
||||||
|
inward_path, seam_start, seam_end, wipe_start,
|
||||||
|
wipe_offset_direction(is_ccw, is_hole), offset_dist, max_wipe_length,
|
||||||
|
wipe_support->inner_lines, wipe_support->printed_lines,
|
||||||
|
m_wipe.path.lines(), support_distance)) {
|
||||||
|
m_wipe.path = std::move(inward_path);
|
||||||
|
wipe_inward_applied = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// make a little move inwards before leaving loop
|
// Orca: make the configured inward move before leaving the loop.
|
||||||
if (m_config.wipe_on_loops.value && paths.back().role() == erExternalPerimeter && m_layer != NULL && m_config.wall_loops.value > 1 && paths.front().size() >= 2 && paths.back().polyline.points.size() >= 3) {
|
if (wipe_on_loops_dest) {
|
||||||
// detect angle between last and first segment
|
gcode += m_writer.extrude_to_xy(
|
||||||
// the side depends on the original winding order of the polygon (inwards for contours, outwards for holes)
|
this->point_to_gcode(*wipe_on_loops_dest), 0, "move inwards before travel", true);
|
||||||
//FIXME improve the algorithm in case the loop is tiny.
|
this->set_last_pos(*wipe_on_loops_dest);
|
||||||
//FIXME improve the algorithm in case the loop is split into segments with a low number of points (see the Point b query).
|
|
||||||
const Point3 &a3 = paths.front().polyline.points[1]; // second point
|
|
||||||
Point a = Point(a3.x(), a3.y());
|
|
||||||
const Point3 &b3 = *(paths.back().polyline.points.end()-3); // second to last point
|
|
||||||
Point b = Point(b3.x(), b3.y());
|
|
||||||
if (is_hole == loop.is_counter_clockwise()) {
|
|
||||||
// swap points
|
|
||||||
Point c = a; a = b; b = c;
|
|
||||||
}
|
|
||||||
|
|
||||||
double angle = paths.front().first_point().ccw_angle(a, b) / 3;
|
|
||||||
|
|
||||||
// turn inwards if contour, turn outwards if hole
|
|
||||||
if (is_hole == loop.is_counter_clockwise()) angle *= -1;
|
|
||||||
|
|
||||||
// create the destination point along the first segment and rotate it
|
|
||||||
// we make sure we don't exceed the segment length because we don't know
|
|
||||||
// the rotation of the second segment so we might cross the object boundary
|
|
||||||
Vec2d p1 = paths.front().polyline.points.front().cast<double>().head<2>();
|
|
||||||
Vec2d p2 = paths.front().polyline.points[1].cast<double>().head<2>();
|
|
||||||
Vec2d v = p2 - p1;
|
|
||||||
double nd = scale_(EXTRUDER_CONFIG(nozzle_diameter));
|
|
||||||
double l2 = v.squaredNorm();
|
|
||||||
// Shift by no more than a nozzle diameter.
|
|
||||||
//FIXME Hiding the seams will not work nicely for very densely discretized contours!
|
|
||||||
//BBS. shorten the travel distant before the wipe path
|
|
||||||
double threshold = 0.2;
|
|
||||||
Point pt = (p1 + v * threshold).cast<coord_t>();
|
|
||||||
if (nd * nd < l2)
|
|
||||||
pt = (p1 + threshold * v * (nd / sqrt(l2))).cast<coord_t>();
|
|
||||||
//Point pt = ((nd * nd >= l2) ? (p1+v*0.4): (p1 + 0.2 * v * (nd / sqrt(l2)))).cast<coord_t>();
|
|
||||||
const Point3 ¢er3 = paths.front().polyline.points.front();
|
|
||||||
pt.rotate(angle, Point(center3.x(), center3.y()));
|
|
||||||
// generate the travel move
|
|
||||||
gcode += m_writer.extrude_to_xy(this->point_to_gcode(pt), 0, "move inwards before travel", true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Execute the accepted path before another extrusion replaces it. Wiping
|
||||||
|
// must not force retraction or Z-hop across a short travel to the next wall.
|
||||||
|
// Ordinary travel planning decides whether to retract from the new position.
|
||||||
|
if (wipe_inward_applied)
|
||||||
|
gcode += m_wipe.wipe(*this, 0.);
|
||||||
|
|
||||||
return gcode;
|
return gcode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7524,21 +7562,9 @@ std::string GCode::extrude_multi_path(const ExtrusionMultiPath& multipath, const
|
|||||||
m_multi_flow_segment_path_pa_set = true;
|
m_multi_flow_segment_path_pa_set = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// BBS
|
// Orca: multipath wipes retrace the extrusion in reverse order.
|
||||||
if (m_wipe.enable && FILAMENT_CONFIG(wipe)) {
|
if (m_wipe.enable && FILAMENT_CONFIG(wipe))
|
||||||
m_wipe.path = Polyline();
|
m_wipe.update_path(multipath.paths, true);
|
||||||
for (const ExtrusionPath &path : multipath.paths) {
|
|
||||||
//BBS: Don't need to save duplicated point into wipe path
|
|
||||||
if (!m_wipe.path.empty() && !path.empty() &&
|
|
||||||
m_wipe.path.last_point() == Point(path.first_point().x(), path.first_point().y())) {
|
|
||||||
// Convert Points3 to Points
|
|
||||||
for (auto it = path.polyline.points.begin() + 1; it != path.polyline.points.end(); ++it)
|
|
||||||
m_wipe.path.append(Point(it->x(), it->y()));
|
|
||||||
} else
|
|
||||||
m_wipe.path.append(path.polyline.to_polyline()); // TODO: don't limit wipe to last path
|
|
||||||
}
|
|
||||||
m_wipe.path.reverse();
|
|
||||||
}
|
|
||||||
|
|
||||||
return gcode;
|
return gcode;
|
||||||
}
|
}
|
||||||
@@ -7546,14 +7572,15 @@ std::string GCode::extrude_multi_path(const ExtrusionMultiPath& multipath, const
|
|||||||
std::string GCode::extrude_entity(const ExtrusionEntity& entity,
|
std::string GCode::extrude_entity(const ExtrusionEntity& entity,
|
||||||
const std::string& description,
|
const std::string& description,
|
||||||
double speed,
|
double speed,
|
||||||
const ExtrusionEntitiesPtr& region_perimeters)
|
const ExtrusionEntitiesPtr& region_perimeters,
|
||||||
|
const WipeInwardSupport* wipe_support)
|
||||||
{
|
{
|
||||||
if (const ExtrusionPath* path = dynamic_cast<const ExtrusionPath*>(&entity))
|
if (const ExtrusionPath* path = dynamic_cast<const ExtrusionPath*>(&entity))
|
||||||
return this->extrude_path(*path, description, speed);
|
return this->extrude_path(*path, description, speed);
|
||||||
else if (const ExtrusionMultiPath* multipath = dynamic_cast<const ExtrusionMultiPath*>(&entity))
|
else if (const ExtrusionMultiPath* multipath = dynamic_cast<const ExtrusionMultiPath*>(&entity))
|
||||||
return this->extrude_multi_path(*multipath, description, speed);
|
return this->extrude_multi_path(*multipath, description, speed);
|
||||||
else if (const ExtrusionLoop* loop = dynamic_cast<const ExtrusionLoop*>(&entity))
|
else if (const ExtrusionLoop* loop = dynamic_cast<const ExtrusionLoop*>(&entity))
|
||||||
return this->extrude_loop(*loop, description, speed, region_perimeters);
|
return this->extrude_loop(*loop, description, speed, region_perimeters, nullptr, wipe_support);
|
||||||
else
|
else
|
||||||
throw Slic3r::InvalidArgument("Invalid argument supplied to extrude()");
|
throw Slic3r::InvalidArgument("Invalid argument supplied to extrude()");
|
||||||
return "";
|
return "";
|
||||||
@@ -7567,6 +7594,7 @@ std::string GCode::extrude_path(const ExtrusionPath& path, const std::string& de
|
|||||||
// description += ExtrusionEntity::role_to_string(path.role());
|
// description += ExtrusionEntity::role_to_string(path.role());
|
||||||
std::string gcode = this->_extrude(path, description, speed);
|
std::string gcode = this->_extrude(path, description, speed);
|
||||||
if (m_wipe.enable && FILAMENT_CONFIG(wipe)) {
|
if (m_wipe.enable && FILAMENT_CONFIG(wipe)) {
|
||||||
|
m_wipe.reset_path();
|
||||||
m_wipe.path = path.polyline.to_polyline();
|
m_wipe.path = path.polyline.to_polyline();
|
||||||
if (is_tree(this->config().support_type) && is_support(path.role())) {
|
if (is_tree(this->config().support_type) && is_support(path.role())) {
|
||||||
if ((m_wipe.path.first_point() - m_wipe.path.last_point()).cast<double>().norm() > scale_(0.2)) {
|
if ((m_wipe.path.first_point() - m_wipe.path.last_point()).cast<double>().norm() > scale_(0.2)) {
|
||||||
@@ -7599,8 +7627,19 @@ std::string GCode::extrude_perimeters(const Print &print, const std::vector<Obje
|
|||||||
: (m_config.is_infill_first == is_infill_first);
|
: (m_config.is_infill_first == is_infill_first);
|
||||||
if (!should_print) continue;
|
if (!should_print) continue;
|
||||||
|
|
||||||
for (const ExtrusionEntity* ee : region.perimeters)
|
// Build the printed prefix once in emission order, scoped to this
|
||||||
gcode += this->extrude_entity(*ee, "perimeter", -1., region.perimeters);
|
// region. Disabled or zero-length wipes need no support geometry.
|
||||||
|
std::optional<WipeInwardSupport> wipe_support;
|
||||||
|
if (m_wipe.enable && FILAMENT_CONFIG(wipe) && m_config.wipe_inward &&
|
||||||
|
m_config.wipe_inward_distance.value > 0. &&
|
||||||
|
scale_(FILAMENT_CONFIG(wipe_distance)) > SCALED_EPSILON)
|
||||||
|
wipe_support.emplace();
|
||||||
|
for (const ExtrusionEntity* ee : region.perimeters) {
|
||||||
|
gcode += this->extrude_entity(*ee, "perimeter", -1., region.perimeters,
|
||||||
|
wipe_support ? &*wipe_support : nullptr);
|
||||||
|
if (wipe_support)
|
||||||
|
wipe_support->append(*ee);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return gcode;
|
return gcode;
|
||||||
}
|
}
|
||||||
@@ -7841,7 +7880,8 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description,
|
|||||||
// path is 2D. But in slope lift case, lift z is done in travel_to function.
|
// path is 2D. But in slope lift case, lift z is done in travel_to function.
|
||||||
// Add m_need_change_layer_lift_z when change_layer in case of no lift if m_last_pos is equal to path.first_point() by chance
|
// Add m_need_change_layer_lift_z when change_layer in case of no lift if m_last_pos is equal to path.first_point() by chance
|
||||||
Point first_point = path.first_point();
|
Point first_point = path.first_point();
|
||||||
if (!m_last_pos_defined || m_last_pos.to_point() != first_point || m_need_change_layer_lift_z || slope_need_z_travel) {
|
if (!m_last_pos_defined || m_last_pos.to_point() != first_point || m_need_change_layer_lift_z ||
|
||||||
|
slope_need_z_travel) {
|
||||||
const bool _last_pos_undefined = !m_last_pos_defined;
|
const bool _last_pos_undefined = !m_last_pos_defined;
|
||||||
|
|
||||||
double z = DBL_MAX;
|
double z = DBL_MAX;
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ namespace Slic3r {
|
|||||||
|
|
||||||
// Forward declarations.
|
// Forward declarations.
|
||||||
class GCode;
|
class GCode;
|
||||||
|
struct WipeInwardSupport;
|
||||||
|
|
||||||
namespace CustomGCode{ struct Item; }
|
namespace CustomGCode{ struct Item; }
|
||||||
struct PrintInstance;
|
struct PrintInstance;
|
||||||
@@ -61,7 +62,7 @@ public:
|
|||||||
bool enable;
|
bool enable;
|
||||||
Polyline path;
|
Polyline path;
|
||||||
|
|
||||||
// Orca:
|
// Orca: retraction portions emitted before, during, and after the wipe move.
|
||||||
struct RetractionValues{
|
struct RetractionValues{
|
||||||
double retraction_length_before_wipe = 0.;
|
double retraction_length_before_wipe = 0.;
|
||||||
double retraction_length_during_wipe = 0.;
|
double retraction_length_during_wipe = 0.;
|
||||||
@@ -73,8 +74,10 @@ public:
|
|||||||
void reset_path() { this->path = Polyline(); }
|
void reset_path() { this->path = Polyline(); }
|
||||||
std::string wipe(GCode &gcodegen, double length, bool toolchange = false, bool is_last = false);
|
std::string wipe(GCode &gcodegen, double length, bool toolchange = false, bool is_last = false);
|
||||||
|
|
||||||
// Orca:
|
// Orca: calculate the retraction portions that can be emitted at wipe speed.
|
||||||
RetractionValues calculateWipeRetractionLengths(GCode& gcodegen, bool toolchange);
|
RetractionValues calculateWipeRetractionLengths(GCode& gcodegen, bool toolchange);
|
||||||
|
// Orca: rebuild the stored path while deduplicating shared path boundaries.
|
||||||
|
void update_path(const ExtrusionPaths &paths, bool reverse = false);
|
||||||
};
|
};
|
||||||
|
|
||||||
class WipeTowerIntegration {
|
class WipeTowerIntegration {
|
||||||
@@ -430,14 +433,16 @@ private:
|
|||||||
std::string extrude_entity(const ExtrusionEntity& entity,
|
std::string extrude_entity(const ExtrusionEntity& entity,
|
||||||
const std::string& description = "",
|
const std::string& description = "",
|
||||||
double speed = -1.,
|
double speed = -1.,
|
||||||
const ExtrusionEntitiesPtr& region_perimeters = ExtrusionEntitiesPtr());
|
const ExtrusionEntitiesPtr& region_perimeters = ExtrusionEntitiesPtr(),
|
||||||
|
const WipeInwardSupport* wipe_support = nullptr);
|
||||||
// Orca: pass the complete collection of region perimeters to the extrude loop to check whether the wipe before external loop
|
// Orca: pass the complete collection of region perimeters to the extrude loop to check whether the wipe before external loop
|
||||||
// should be executed
|
// should be executed
|
||||||
std::string extrude_loop(const ExtrusionLoop& loop,
|
std::string extrude_loop(const ExtrusionLoop& loop,
|
||||||
const std::string& description,
|
const std::string& description,
|
||||||
double speed = -1.,
|
double speed = -1.,
|
||||||
const ExtrusionEntitiesPtr& region_perimeters = ExtrusionEntitiesPtr(),
|
const ExtrusionEntitiesPtr& region_perimeters = ExtrusionEntitiesPtr(),
|
||||||
const Point* start_point = nullptr);
|
const Point* start_point = nullptr,
|
||||||
|
const WipeInwardSupport* wipe_support = nullptr);
|
||||||
std::string extrude_multi_path(const ExtrusionMultiPath& multipath, const std::string& description = "", double speed = -1.);
|
std::string extrude_multi_path(const ExtrusionMultiPath& multipath, const std::string& description = "", double speed = -1.);
|
||||||
std::string extrude_path(const ExtrusionPath& path, const std::string& description = "", double speed = -1.);
|
std::string extrude_path(const ExtrusionPath& path, const std::string& description = "", double speed = -1.);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,920 @@
|
|||||||
|
#include "WipePathHelpers.hpp"
|
||||||
|
|
||||||
|
#include "../AABBTreeLines.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cassert>
|
||||||
|
#include <cmath>
|
||||||
|
#include <limits>
|
||||||
|
#include <tuple>
|
||||||
|
|
||||||
|
namespace Slic3r {
|
||||||
|
|
||||||
|
void WipeInwardSupport::append(const ExtrusionEntity &entity)
|
||||||
|
{
|
||||||
|
const ExtrusionPaths *paths = nullptr;
|
||||||
|
if (const auto *loop = dynamic_cast<const ExtrusionLoop *>(&entity))
|
||||||
|
paths = &loop->paths;
|
||||||
|
else if (const auto *multipath = dynamic_cast<const ExtrusionMultiPath *>(&entity))
|
||||||
|
paths = &multipath->paths;
|
||||||
|
|
||||||
|
// A loop's role is its first path's role. An overhanging start must not
|
||||||
|
// hide the ordinary inner-wall segments elsewhere in the same loop.
|
||||||
|
const bool is_inner = paths ? std::any_of(paths->begin(), paths->end(),
|
||||||
|
[](const ExtrusionPath &path) { return is_internal_perimeter(path.role()); }) :
|
||||||
|
is_internal_perimeter(entity.role());
|
||||||
|
const Lines lines = entity.as_polyline().lines();
|
||||||
|
printed_lines.insert(printed_lines.end(), lines.begin(), lines.end());
|
||||||
|
if (is_inner)
|
||||||
|
inner_lines.insert(inner_lines.end(), lines.begin(), lines.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Orca: miter limit ratio. Matches DefaultMiterLimit from ClipperUtils.hpp.
|
||||||
|
// When the miter join extends more than miter_limit * offset_dist from the
|
||||||
|
// original vertex, the miter is replaced by a bevel join.
|
||||||
|
static constexpr double miter_limit = 3.0;
|
||||||
|
|
||||||
|
// Orca: threshold for detecting near-reversal (backtracking spike).
|
||||||
|
// Normalized dot product below this means the segments point in nearly
|
||||||
|
// opposite directions (angle > ~172°). Offsetting such a path is unsafe.
|
||||||
|
static constexpr double reversal_dot_threshold = -0.99;
|
||||||
|
|
||||||
|
// Orca: candidates pointing more than 60 degrees away from the selected inner
|
||||||
|
// wall are too tangent to distinguish the material side reliably at a cusp.
|
||||||
|
static constexpr double min_support_alignment = 0.5;
|
||||||
|
|
||||||
|
// Keep a scaled-coordinate rounding floor while allowing the tolerance to
|
||||||
|
// follow the relevant offset or path length. Clearance allows a larger fraction.
|
||||||
|
static double wipe_tolerance(double distance, double relative_tolerance = 0.1)
|
||||||
|
{
|
||||||
|
return std::max(4. * SCALED_EPSILON, relative_tolerance * distance);
|
||||||
|
}
|
||||||
|
|
||||||
|
Point sample_path_at_distance(const ExtrusionPaths &paths, bool forward, double target)
|
||||||
|
{
|
||||||
|
assert(!paths.empty());
|
||||||
|
if (paths.empty())
|
||||||
|
return Point(0, 0);
|
||||||
|
|
||||||
|
double remaining = target;
|
||||||
|
Point result = forward ? paths.front().first_point() : paths.back().last_point();
|
||||||
|
for (int pi = forward ? 0 : (int)paths.size() - 1;
|
||||||
|
pi >= 0 && pi < (int)paths.size() && remaining > 0.;
|
||||||
|
pi += forward ? 1 : -1) {
|
||||||
|
const Points3 &pts = paths[pi].polyline.points;
|
||||||
|
for (int i = forward ? 0 : (int)pts.size() - 1;
|
||||||
|
remaining > 0. && (forward ? i + 1 < (int)pts.size() : i > 0);
|
||||||
|
i += forward ? 1 : -1) {
|
||||||
|
const int j = forward ? i + 1 : i - 1;
|
||||||
|
const Point cur(pts[i].x(), pts[i].y());
|
||||||
|
const Point next(pts[j].x(), pts[j].y());
|
||||||
|
const double segment_length = (next - cur).cast<double>().norm();
|
||||||
|
if (segment_length < SCALED_EPSILON)
|
||||||
|
continue;
|
||||||
|
if (remaining <= segment_length) {
|
||||||
|
const double ratio = remaining / segment_length;
|
||||||
|
return Point(coord_t(cur.x() + ratio * (next.x() - cur.x())),
|
||||||
|
coord_t(cur.y() + ratio * (next.y() - cur.y())));
|
||||||
|
}
|
||||||
|
remaining -= segment_length;
|
||||||
|
result = next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Orca: consecutive duplicates carry no path length and can be removed safely.
|
||||||
|
// A reversal, however, is real travelled distance: removing its vertex would
|
||||||
|
// replace a long backtracking wipe with a short, unrelated shortcut.
|
||||||
|
static bool prepare_source(Points &pts)
|
||||||
|
{
|
||||||
|
pts.erase(std::unique(pts.begin(), pts.end()), pts.end());
|
||||||
|
|
||||||
|
if (pts.size() < 2)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
for (size_t i = 1; i + 1 < pts.size(); ++i) {
|
||||||
|
const Vec2d v_prev = (pts[i] - pts[i - 1]).cast<double>();
|
||||||
|
const Vec2d v_next = (pts[i + 1] - pts[i]).cast<double>();
|
||||||
|
const double dot = v_prev.dot(v_next) / (v_prev.norm() * v_next.norm());
|
||||||
|
if (dot < reversal_dot_threshold)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool build_offset_polyline(const Points &original, int dir, double offset_dist,
|
||||||
|
Points &result, size_t &first_join_index)
|
||||||
|
{
|
||||||
|
if (original.size() < 2)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Orca: collapse all consecutive duplicates first, then reject any
|
||||||
|
// backtracking in the cleaned path instead of replacing travelled distance
|
||||||
|
// with a shortcut.
|
||||||
|
Points source = original;
|
||||||
|
if (! prepare_source(source))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
const size_t n = source.size();
|
||||||
|
|
||||||
|
// Orca: compute the perpendicular offset for segment i->i+1 as an infinite Line.
|
||||||
|
auto offset_segment = [dir, offset_dist](const Point &a, const Point &b) -> Line {
|
||||||
|
Vec2d v = (b - a).cast<double>();
|
||||||
|
double len = v.norm();
|
||||||
|
Vec2d perp(0, 0);
|
||||||
|
if (len > SCALED_EPSILON)
|
||||||
|
perp = Vec2d(-v.y(), v.x()) * (dir * offset_dist / len);
|
||||||
|
return Line(Point(coord_t(a.x() + perp.x()), coord_t(a.y() + perp.y())),
|
||||||
|
Point(coord_t(b.x() + perp.x()), coord_t(b.y() + perp.y())));
|
||||||
|
};
|
||||||
|
|
||||||
|
result.clear();
|
||||||
|
result.reserve(n);
|
||||||
|
first_join_index = 0;
|
||||||
|
|
||||||
|
// Orca: the first point is perpendicular to the first segment.
|
||||||
|
Line l_prev = offset_segment(source[0], source[1]);
|
||||||
|
result.push_back(l_prev.a);
|
||||||
|
|
||||||
|
// Orca: use the analytic intersection of adjacent offset segments for a
|
||||||
|
// miter join. Intersecting the already rounded Line endpoints amplifies
|
||||||
|
// coordinate quantization when the source segments are nearly parallel.
|
||||||
|
for (size_t i = 1; i + 1 < n; ++i) {
|
||||||
|
Line l_next = offset_segment(source[i], source[i + 1]);
|
||||||
|
const Vec2d previous = (source[i] - source[i - 1]).cast<double>().normalized();
|
||||||
|
const Vec2d next = (source[i + 1] - source[i]).cast<double>().normalized();
|
||||||
|
const double denominator = 1. + previous.dot(next);
|
||||||
|
|
||||||
|
bool need_bevel = denominator <= EPSILON;
|
||||||
|
Point pt;
|
||||||
|
if (! need_bevel) {
|
||||||
|
const Vec2d previous_normal(-previous.y(), previous.x());
|
||||||
|
const Vec2d next_normal(-next.y(), next.x());
|
||||||
|
const Vec2d miter = (previous_normal + next_normal) * (dir * offset_dist / denominator);
|
||||||
|
if (miter.norm() > miter_limit * offset_dist) {
|
||||||
|
need_bevel = true;
|
||||||
|
} else {
|
||||||
|
pt = Point(coord_t(source[i].x() + miter.x()),
|
||||||
|
coord_t(source[i].y() + miter.y()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (need_bevel) {
|
||||||
|
result.push_back(l_prev.b);
|
||||||
|
if (l_next.a != result.back())
|
||||||
|
result.push_back(l_next.a);
|
||||||
|
} else {
|
||||||
|
result.push_back(pt);
|
||||||
|
}
|
||||||
|
if (i == 1)
|
||||||
|
first_join_index = result.size() - 1;
|
||||||
|
l_prev = l_next;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Orca: the last point is perpendicular to the last segment.
|
||||||
|
result.push_back(l_prev.b);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int wipe_offset_direction(bool is_ccw, bool is_hole)
|
||||||
|
{
|
||||||
|
const int loop_inside = is_ccw ? +1 : -1;
|
||||||
|
return is_hole ? -loop_inside : loop_inside;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool starts_by_backtracking(const Polyline &path, Point actual_start)
|
||||||
|
{
|
||||||
|
if (path.points.size() < 3)
|
||||||
|
return false;
|
||||||
|
// Orca: points[0] is only a storage sentinel; use the nozzle position for
|
||||||
|
// the executable connector, particularly after a wipe_on_loops pre-move.
|
||||||
|
const Vec2d connector = (path.points[1] - actual_start).cast<double>();
|
||||||
|
const Vec2d outgoing = (path.points[2] - path.points[1]).cast<double>();
|
||||||
|
// An inward connector may be perpendicular to the outgoing offset edge.
|
||||||
|
// Rounded joins must not turn that right angle into a false backtrack.
|
||||||
|
return connector.dot(outgoing) < -4. * SCALED_EPSILON * outgoing.norm();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Orca: sample the outgoing perimeter without copying or clipping its full loop.
|
||||||
|
static Point sample_polyline_at_distance(const Polyline &polyline, double target)
|
||||||
|
{
|
||||||
|
assert(! polyline.points.empty());
|
||||||
|
Point result = polyline.first_point();
|
||||||
|
for (size_t i = 1; i < polyline.points.size() && target > 0.; ++i) {
|
||||||
|
const Vec2d segment = (polyline.points[i] - result).cast<double>();
|
||||||
|
const double length = segment.norm();
|
||||||
|
if (length <= SCALED_EPSILON)
|
||||||
|
continue;
|
||||||
|
if (target <= length)
|
||||||
|
return (result.cast<double>() + segment * (target / length)).cast<coord_t>();
|
||||||
|
target -= length;
|
||||||
|
result = polyline.points[i];
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Orca: convert an executable path into Wipe::wipe()'s stored representation.
|
||||||
|
// The first point is a dummy replaced by the actual nozzle position, while the
|
||||||
|
// remaining points are clipped to the configured wipe distance.
|
||||||
|
static bool store_wipe_path(Polyline &destination, Point seam_start,
|
||||||
|
Polyline actual_path, double max_wipe_length)
|
||||||
|
{
|
||||||
|
if (actual_path.points.size() < 2 || max_wipe_length <= SCALED_EPSILON)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
const double actual_length = actual_path.length();
|
||||||
|
if (actual_length <= SCALED_EPSILON)
|
||||||
|
return false;
|
||||||
|
if (actual_length - max_wipe_length > SCALED_EPSILON)
|
||||||
|
actual_path.clip_end(actual_length - max_wipe_length);
|
||||||
|
if (actual_path.points.size() < 2)
|
||||||
|
return false;
|
||||||
|
for (size_t i = 1; i < actual_path.points.size(); ++i)
|
||||||
|
if (actual_path.points[i - 1] == actual_path.points[i])
|
||||||
|
return false;
|
||||||
|
|
||||||
|
Polyline stored_path;
|
||||||
|
stored_path.points.reserve(actual_path.points.size());
|
||||||
|
stored_path.points.push_back(seam_start);
|
||||||
|
stored_path.points.insert(stored_path.points.end(), actual_path.points.begin() + 1, actual_path.points.end());
|
||||||
|
stored_path.reset_to_linear_move();
|
||||||
|
destination = std::move(stored_path);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool offset_wipe_path(Polyline &polyline, Point seam_start, Point seam_end, Point wipe_start,
|
||||||
|
int dir, double offset_dist, double max_wipe_length)
|
||||||
|
{
|
||||||
|
assert(dir == +1 || dir == -1);
|
||||||
|
assert(offset_dist > 0);
|
||||||
|
if (polyline.points.empty() || polyline.first_point() != seam_start ||
|
||||||
|
max_wipe_length <= SCALED_EPSILON)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
const Polyline original = polyline;
|
||||||
|
const double original_length = original.length();
|
||||||
|
if (original_length <= SCALED_EPSILON)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
double source_length = std::min(original_length, max_wipe_length);
|
||||||
|
for (;;) {
|
||||||
|
Polyline source = original;
|
||||||
|
const double clip_distance = original_length - source_length;
|
||||||
|
if (clip_distance > SCALED_EPSILON)
|
||||||
|
source.clip_end(clip_distance);
|
||||||
|
|
||||||
|
Points wrapped_source;
|
||||||
|
wrapped_source.reserve(source.points.size() + 1);
|
||||||
|
if (seam_start == seam_end) {
|
||||||
|
// Orca: the stored loop is open at seam_start even when the seam gap is
|
||||||
|
// zero. Prepend the closing edge so build_offset_polyline() creates
|
||||||
|
// the proper join between that edge and the first outgoing edge,
|
||||||
|
// instead of leaving the first offset point on the closing wall.
|
||||||
|
size_t closing_index = original.points.size();
|
||||||
|
while (closing_index > 0 && original.points[closing_index - 1] == seam_start)
|
||||||
|
--closing_index;
|
||||||
|
if (closing_index == 0)
|
||||||
|
return false; // Orca: the entire path is a single point.
|
||||||
|
wrapped_source.push_back(original.points[closing_index - 1]);
|
||||||
|
} else {
|
||||||
|
// Orca: use the unextruded seam-gap edge to determine the incoming
|
||||||
|
// direction at the seam. Its offset is construction geometry only;
|
||||||
|
// wiping along it would create a Z-shaped detour before the outgoing
|
||||||
|
// perimeter offset.
|
||||||
|
wrapped_source.push_back(seam_end);
|
||||||
|
}
|
||||||
|
wrapped_source.insert(wrapped_source.end(), source.points.begin(), source.points.end());
|
||||||
|
|
||||||
|
Points offset_points;
|
||||||
|
size_t first_join_index = 0;
|
||||||
|
if (! build_offset_polyline(wrapped_source, dir, offset_dist, offset_points, first_join_index) ||
|
||||||
|
first_join_index == 0 || first_join_index >= offset_points.size())
|
||||||
|
return false;
|
||||||
|
// Orca: discard the offset of the prepended edge and, for a bevel, its
|
||||||
|
// incoming endpoint. The executable wipe starts at the seam join and
|
||||||
|
// then follows only the already printed outgoing perimeter.
|
||||||
|
offset_points.erase(offset_points.begin(), offset_points.begin() + first_join_index);
|
||||||
|
|
||||||
|
Polyline actual_path;
|
||||||
|
actual_path.points.reserve(offset_points.size() + 1);
|
||||||
|
actual_path.points.push_back(wipe_start);
|
||||||
|
actual_path.points.insert(actual_path.points.end(), offset_points.begin(), offset_points.end());
|
||||||
|
|
||||||
|
// A loop pre-move may advance past an otherwise valid offset join.
|
||||||
|
// Enter at the nozzle's projection instead of returning to the join.
|
||||||
|
// Do not repair a join that already backtracks across the seam gap;
|
||||||
|
// the caller must still validate wall crossings, material side and support.
|
||||||
|
if (seam_start != seam_end && wipe_start != seam_start && wipe_start != seam_end &&
|
||||||
|
starts_by_backtracking(actual_path, wipe_start) && ! starts_by_backtracking(actual_path, seam_end)) {
|
||||||
|
size_t entry = 1;
|
||||||
|
while (entry + 1 < actual_path.points.size()) {
|
||||||
|
const Vec2d edge = (actual_path.points[entry + 1] - actual_path.points[entry]).cast<double>();
|
||||||
|
const double projection = (wipe_start - actual_path.points[entry]).cast<double>().dot(edge);
|
||||||
|
if (projection <= 0.)
|
||||||
|
break;
|
||||||
|
if (projection < edge.squaredNorm()) {
|
||||||
|
actual_path.points[entry] = (actual_path.points[entry].cast<double>() +
|
||||||
|
edge * (projection / edge.squaredNorm())).cast<coord_t>();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
++entry;
|
||||||
|
}
|
||||||
|
actual_path.points.erase(actual_path.points.begin() + 1, actual_path.points.begin() + entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seam_start != seam_end && wipe_start == seam_end &&
|
||||||
|
starts_by_backtracking(actual_path, wipe_start)) {
|
||||||
|
// Orca: a wide seam gap or a sharp cusp may put the first miter
|
||||||
|
// behind its outgoing edge. Reject this offset candidate so the
|
||||||
|
// caller can try the opposite side or the translated fallback.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const double actual_length = actual_path.length();
|
||||||
|
const bool source_exhausted = original_length - source_length <= SCALED_EPSILON;
|
||||||
|
if (actual_length + SCALED_EPSILON < max_wipe_length && ! source_exhausted) {
|
||||||
|
// Orca: offset joins may shorten the path at every corner. Grow the
|
||||||
|
// source until the executable offset path, not a heuristic source
|
||||||
|
// margin, reaches the configured wipe distance.
|
||||||
|
const double deficit = max_wipe_length - actual_length;
|
||||||
|
const double next_length = std::min(original_length,
|
||||||
|
source_length + std::max(deficit, 2. * SCALED_EPSILON));
|
||||||
|
if (next_length - source_length <= SCALED_EPSILON)
|
||||||
|
return false;
|
||||||
|
source_length = next_length;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Orca: unlike an extruded offset, a wipe may safely cross or retrace the
|
||||||
|
// just-printed perimeter. The caller validates the complete executable
|
||||||
|
// path against current and earlier printed perimeter geometry.
|
||||||
|
return store_wipe_path(polyline, seam_start, std::move(actual_path), max_wipe_length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool translated_wipe_path(Polyline &polyline, Point seam_start, Point seam_end, Point wipe_start,
|
||||||
|
const Vec2d &translation, double max_wipe_length)
|
||||||
|
{
|
||||||
|
if (translation.norm() <= SCALED_EPSILON || max_wipe_length <= SCALED_EPSILON)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
const Polyline original = polyline;
|
||||||
|
Polyline actual_path;
|
||||||
|
actual_path.points.reserve(original.points.size() + 2);
|
||||||
|
actual_path.points.push_back(wipe_start);
|
||||||
|
|
||||||
|
const auto append_translated = [&actual_path, &translation](const Point &point) {
|
||||||
|
const Point translated = (point.cast<double>() + translation).cast<coord_t>();
|
||||||
|
if (translated != actual_path.points.back())
|
||||||
|
actual_path.points.push_back(translated);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Orca: translate the seam join directly. Translating seam_end and then
|
||||||
|
// following the unextruded gap back to seam_start makes the wipe double
|
||||||
|
// back whenever a gap ends near a sharp corner.
|
||||||
|
append_translated(seam_start);
|
||||||
|
for (const Point &point : original.points)
|
||||||
|
append_translated(point);
|
||||||
|
|
||||||
|
if (seam_start != seam_end && wipe_start == seam_end &&
|
||||||
|
starts_by_backtracking(actual_path, wipe_start)) {
|
||||||
|
// Orca: at a wide gap next to a cusp, the translated seam join may
|
||||||
|
// lie behind the outgoing edge. Prefer a shorter local inward move
|
||||||
|
// at the actual extrusion end over a longer lightning-shaped wipe.
|
||||||
|
actual_path.points.resize(1);
|
||||||
|
append_translated(seam_end);
|
||||||
|
}
|
||||||
|
|
||||||
|
return store_wipe_path(polyline, seam_start, std::move(actual_path), max_wipe_length);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A segment whose endpoints lie within one line's distance capsule is fully
|
||||||
|
// supported, since that capsule is convex. Subdivide only when support changes
|
||||||
|
// between lines; fixed-distance sampling can miss an unsupported gap.
|
||||||
|
static bool segment_is_supported(Point start, Point end,
|
||||||
|
const AABBTreeLines::LinesDistancer<Line> &distancer,
|
||||||
|
double max_distance)
|
||||||
|
{
|
||||||
|
const Point midpoint = ((start.cast<double>() + end.cast<double>()) * 0.5).cast<coord_t>();
|
||||||
|
const auto [distance, line_index, nearest] = distancer.distance_from_lines_extra<false>(midpoint);
|
||||||
|
if (distance > max_distance)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
const Line &line = distancer.get_line(line_index);
|
||||||
|
if (line.distance_to(start) <= max_distance && line.distance_to(end) <= max_distance)
|
||||||
|
return true;
|
||||||
|
if (distancer.distance_from_lines<false>(start) > max_distance ||
|
||||||
|
distancer.distance_from_lines<false>(end) > max_distance)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Conservatively reject an unresolved transition at coordinate precision.
|
||||||
|
if ((end - start).cast<double>().norm() <= SCALED_EPSILON)
|
||||||
|
return false;
|
||||||
|
return segment_is_supported(start, midpoint, distancer, max_distance) &&
|
||||||
|
segment_is_supported(midpoint, end, distancer, max_distance);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<double> wipe_path_support_score(
|
||||||
|
const Polyline &polyline, Point wipe_start,
|
||||||
|
const AABBTreeLines::LinesDistancer<Line> &target_distancer,
|
||||||
|
const AABBTreeLines::LinesDistancer<Line> &all_support_distancer,
|
||||||
|
double max_distance)
|
||||||
|
{
|
||||||
|
if (polyline.points.size() < 2 || target_distancer.get_lines().empty() || max_distance <= 0)
|
||||||
|
return std::nullopt;
|
||||||
|
|
||||||
|
// Orca: require a local neighbour, not merely an earlier perimeter elsewhere in
|
||||||
|
// the region. At a convex corner, an inner wall's miter is farther from the
|
||||||
|
// external seam than its normal wall spacing, so allow the same bounded miter
|
||||||
|
// reach as the offset construction without accepting a remote island.
|
||||||
|
if (target_distancer.distance_from_lines<false>(wipe_start) >
|
||||||
|
miter_limit * max_distance + 4. * SCALED_EPSILON)
|
||||||
|
return std::nullopt;
|
||||||
|
|
||||||
|
Point previous = wipe_start;
|
||||||
|
for (size_t i = 1; i < polyline.points.size(); ++i) {
|
||||||
|
// Orca: a tightly curved inward path may cross back over the current wall.
|
||||||
|
// This is safe for a non-extruding wipe as long as the complete path
|
||||||
|
// remains over current or earlier printed perimeter geometry.
|
||||||
|
// Allow the same coordinate-rounding tolerance at every point, including
|
||||||
|
// the actual start substituted for the stored sentinel.
|
||||||
|
if (! segment_is_supported(previous, polyline.points[i], all_support_distancer,
|
||||||
|
max_distance + 4. * SCALED_EPSILON))
|
||||||
|
return std::nullopt;
|
||||||
|
previous = polyline.points[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Orca: decide direction at the seam. Scoring the complete path may select
|
||||||
|
// the wrong initial side when two contours converge and the later prefix
|
||||||
|
// happens to run closer to unrelated support.
|
||||||
|
return target_distancer.distance_from_lines<false>(polyline.points[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool initial_connector_is_clear(
|
||||||
|
const Polyline &polyline, Point wipe_start, Point seam_start,
|
||||||
|
AABBTreeLines::LinesDistancer<Line> ¤t_perimeter_distancer,
|
||||||
|
double contact_tolerance)
|
||||||
|
{
|
||||||
|
if (polyline.points.size() < 2 || polyline.points[1] == wipe_start)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Orca: without a seam gap, the connector necessarily starts at the wall
|
||||||
|
// and a self-touching cusp may share that same endpoint on several edges.
|
||||||
|
if (seam_start == wipe_start)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
const Line connector(wipe_start, polyline.points[1]);
|
||||||
|
const auto intersections = current_perimeter_distancer.intersections_with_line<false>(connector);
|
||||||
|
for (const auto &intersection : intersections) {
|
||||||
|
if ((intersection.first - wipe_start).cast<double>().norm() > contact_tolerance)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Point closest;
|
||||||
|
// Orca: integer offset joins may miss the exact seam-start coordinate by
|
||||||
|
// a few microns. Treat a close pass through that point as retracing the
|
||||||
|
// external wall, but keep the unavoidable contact at the actual start.
|
||||||
|
if (connector.distance_to_squared(seam_start, &closest) <= contact_tolerance * contact_tolerance &&
|
||||||
|
(closest - wipe_start).cast<double>().norm() > contact_tolerance)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::optional<Vec2d> support_offset_at_start(
|
||||||
|
const Polyline &source, Point local_origin, bool disambiguate_branch,
|
||||||
|
AABBTreeLines::LinesDistancer<Line> &support_distancer,
|
||||||
|
double max_support_distance)
|
||||||
|
{
|
||||||
|
if (source.points.size() < 2)
|
||||||
|
return std::nullopt;
|
||||||
|
|
||||||
|
// Orca: a nonzero gap may put the seam beside the wrong branch of a cusp.
|
||||||
|
// Sample farther along the path to identify its actual neighbouring wall.
|
||||||
|
const Point support_query = disambiguate_branch ?
|
||||||
|
sample_polyline_at_distance(source, 2. * max_support_distance) : source.first_point();
|
||||||
|
const auto nearest_result = support_distancer.distance_from_lines_extra<false>(support_query);
|
||||||
|
const Line &nearest_line = support_distancer.get_line(std::get<1>(nearest_result));
|
||||||
|
Vec2d sampled_offset = std::get<2>(nearest_result) - support_query.cast<double>();
|
||||||
|
|
||||||
|
if (disambiguate_branch) {
|
||||||
|
// Orca: an endpoint projection also contains distance along the support
|
||||||
|
// segment. Remove that tangent component before comparing wall sides.
|
||||||
|
const Vec2d support_edge = (nearest_line.b - nearest_line.a).cast<double>();
|
||||||
|
if (support_edge.norm() > SCALED_EPSILON) {
|
||||||
|
const Vec2d support_tangent = support_edge.normalized();
|
||||||
|
sampled_offset -= support_tangent * sampled_offset.dot(support_tangent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (sampled_offset.norm() <= SCALED_EPSILON)
|
||||||
|
return std::nullopt;
|
||||||
|
|
||||||
|
if (! disambiguate_branch)
|
||||||
|
return sampled_offset;
|
||||||
|
|
||||||
|
// Orca: find the local point on the same material-side branch. Using the
|
||||||
|
// sampled point itself would add the distance already travelled along the
|
||||||
|
// perimeter and turn a normal transition into a long diagonal move.
|
||||||
|
const Vec2d sampled_direction = sampled_offset.normalized();
|
||||||
|
Vec2d local_offset = sampled_offset;
|
||||||
|
double best_local_score = std::numeric_limits<double>::infinity();
|
||||||
|
for (size_t line_index : support_distancer.all_lines_in_radius(
|
||||||
|
local_origin, 2. * max_support_distance + 4. * SCALED_EPSILON)) {
|
||||||
|
Point local_support;
|
||||||
|
const Line &line = support_distancer.get_line(line_index);
|
||||||
|
const double distance_squared = line.distance_to_squared(local_origin, &local_support);
|
||||||
|
const Vec2d candidate_offset = local_support.cast<double>() - local_origin.cast<double>();
|
||||||
|
const double candidate_distance = std::sqrt(distance_squared);
|
||||||
|
if (candidate_distance <= SCALED_EPSILON)
|
||||||
|
continue;
|
||||||
|
const double alignment = candidate_offset.normalized().dot(sampled_direction);
|
||||||
|
if (alignment < min_support_alignment)
|
||||||
|
continue;
|
||||||
|
const double score = candidate_distance / alignment;
|
||||||
|
if (score < best_local_score) {
|
||||||
|
best_local_score = score;
|
||||||
|
local_offset = candidate_offset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return local_offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
static double executable_path_length(const Polyline &stored_path, Point wipe_start)
|
||||||
|
{
|
||||||
|
if (stored_path.points.size() < 2)
|
||||||
|
return 0.;
|
||||||
|
|
||||||
|
// Orca: points[0] is the storage sentinel, so measure the first segment
|
||||||
|
// from the actual nozzle position and the remaining stored segments normally.
|
||||||
|
double length = (stored_path.points[1] - wipe_start).cast<double>().norm();
|
||||||
|
for (size_t index = 2; index < stored_path.points.size(); ++index)
|
||||||
|
length += (stored_path.points[index] - stored_path.points[index - 1]).cast<double>().norm();
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Lines material_side_support_lines(const Polyline &path, Point seam, int preferred_dir,
|
||||||
|
const Lines &support_lines)
|
||||||
|
{
|
||||||
|
if (path.points.size() < 4 || path.first_point() != path.last_point())
|
||||||
|
return {};
|
||||||
|
|
||||||
|
// Orca: the bisector of the incoming and outgoing material-side normals is
|
||||||
|
// a local side test that remains valid for globally self-touching Arachne
|
||||||
|
// contours. Ignore repeated seam points when obtaining both tangents.
|
||||||
|
const auto outgoing_it = std::find_if(
|
||||||
|
path.points.begin() + 1, path.points.end(), [seam](const Point &point) { return point != seam; });
|
||||||
|
const auto incoming_it = std::find_if(
|
||||||
|
path.points.rbegin() + 1, path.points.rend(), [seam](const Point &point) { return point != seam; });
|
||||||
|
if (outgoing_it == path.points.end() || incoming_it == path.points.rend())
|
||||||
|
return {};
|
||||||
|
|
||||||
|
const Vec2d outgoing = (*outgoing_it - seam).cast<double>().normalized();
|
||||||
|
const Vec2d incoming = (seam - *incoming_it).cast<double>().normalized();
|
||||||
|
const Vec2d material_direction =
|
||||||
|
(Vec2d(-outgoing.y(), outgoing.x()) + Vec2d(-incoming.y(), incoming.x())) * preferred_dir;
|
||||||
|
if (material_direction.norm() <= EPSILON)
|
||||||
|
return {};
|
||||||
|
|
||||||
|
Lines result;
|
||||||
|
result.reserve(support_lines.size());
|
||||||
|
for (const Line &line : support_lines) {
|
||||||
|
Point closest;
|
||||||
|
line.distance_to_squared(seam, &closest);
|
||||||
|
if ((closest - seam).cast<double>().dot(material_direction) > SCALED_EPSILON)
|
||||||
|
result.push_back(line);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool wipe_path_stays_on_material_side(
|
||||||
|
const Polyline &path, Point path_start, const Vec2d &support_direction,
|
||||||
|
const AABBTreeLines::LinesDistancer<Line> &target_perimeter_distancer,
|
||||||
|
const AABBTreeLines::LinesDistancer<Line> ¤t_perimeter_distancer,
|
||||||
|
double effective_offset, bool require_clearance)
|
||||||
|
{
|
||||||
|
if (path.points.size() < 2 || support_direction.norm() <= EPSILON ||
|
||||||
|
target_perimeter_distancer.get_lines().empty() || current_perimeter_distancer.get_lines().empty() ||
|
||||||
|
effective_offset <= SCALED_EPSILON)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
const Vec2d initial_offset = (path.points[1] - path_start).cast<double>();
|
||||||
|
if (initial_offset.norm() <= SCALED_EPSILON ||
|
||||||
|
initial_offset.normalized().dot(support_direction.normalized()) < min_support_alignment)
|
||||||
|
return false;
|
||||||
|
// Orca: after the connector has left the extrusion endpoint, an inward
|
||||||
|
// offset must retain most of its requested clearance from the current
|
||||||
|
// external wall. Otherwise a tight turn may send an initially correct path
|
||||||
|
// back onto that wall, or make the opposite-side candidate look supported.
|
||||||
|
const double clearance_tolerance = wipe_tolerance(effective_offset, 0.25);
|
||||||
|
const double minimum_clearance = effective_offset - clearance_tolerance;
|
||||||
|
const Lines &lines = current_perimeter_distancer.get_lines();
|
||||||
|
const auto left_normal = [](const Line &line) -> Vec2d {
|
||||||
|
const Vec2d edge = (line.b - line.a).cast<double>();
|
||||||
|
if (edge.norm() <= SCALED_EPSILON)
|
||||||
|
return Vec2d::Zero();
|
||||||
|
return Vec2d(-edge.y(), edge.x()).normalized();
|
||||||
|
};
|
||||||
|
const auto on_material_side = [&](const Point &point, bool check_clearance) {
|
||||||
|
const auto [distance, line_index, nearest] =
|
||||||
|
current_perimeter_distancer.distance_from_lines_extra<false>(point);
|
||||||
|
if (line_index >= lines.size())
|
||||||
|
return false;
|
||||||
|
const Line &line = lines[line_index];
|
||||||
|
Vec2d normal = left_normal(line);
|
||||||
|
// At a shared vertex use both incident edges, so the result does not
|
||||||
|
// depend on which equally close edge the AABB query happens to return.
|
||||||
|
const Line &previous = lines[(line_index + lines.size() - 1) % lines.size()];
|
||||||
|
const Line &next = lines[(line_index + 1) % lines.size()];
|
||||||
|
if ((nearest - line.a.cast<double>()).norm() <= SCALED_EPSILON && previous.b == line.a)
|
||||||
|
normal += left_normal(previous);
|
||||||
|
if ((nearest - line.b.cast<double>()).norm() <= SCALED_EPSILON && next.a == line.b)
|
||||||
|
normal += left_normal(next);
|
||||||
|
if (normal.norm() <= EPSILON)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// An open or self-touching wall has no reliable polygon-wide sign.
|
||||||
|
// Orient its local normal toward the neighbouring printed inner wall,
|
||||||
|
// then test the candidate on that side at every sample.
|
||||||
|
normal.normalize();
|
||||||
|
const Point wall_point = nearest.cast<coord_t>();
|
||||||
|
const Vec2d support_point = std::get<2>(
|
||||||
|
target_perimeter_distancer.distance_from_lines_extra<false>(wall_point));
|
||||||
|
const double support_side = (support_point - nearest).dot(normal);
|
||||||
|
if (std::abs(support_side) <= 4. * SCALED_EPSILON)
|
||||||
|
return false;
|
||||||
|
const double side = (point.cast<double>() - nearest).dot(normal) * (support_side > 0. ? 1. : -1.);
|
||||||
|
return side >= -4. * SCALED_EPSILON &&
|
||||||
|
(! check_clearance || distance + 4. * SCALED_EPSILON >= minimum_clearance);
|
||||||
|
};
|
||||||
|
|
||||||
|
Point previous = path.points[1];
|
||||||
|
if (! on_material_side(previous, require_clearance))
|
||||||
|
return false;
|
||||||
|
for (size_t index = 2; index < path.points.size(); ++index) {
|
||||||
|
const Vec2d segment = (path.points[index] - previous).cast<double>();
|
||||||
|
const size_t samples = std::max<size_t>(1, size_t(std::ceil(segment.norm() / effective_offset)));
|
||||||
|
for (size_t sample = 1; sample <= samples; ++sample) {
|
||||||
|
const Point point = (previous.cast<double>() +
|
||||||
|
segment * (double(sample) / double(samples))).cast<coord_t>();
|
||||||
|
if (! on_material_side(point, require_clearance))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
previous = path.points[index];
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool offset_wipe_path_toward_support(Polyline &polyline, Point seam_start, Point seam_end, Point wipe_start,
|
||||||
|
int preferred_dir, double offset_dist, double max_wipe_length,
|
||||||
|
const Lines &target_perimeter_lines, const Lines &printed_perimeter_lines,
|
||||||
|
const Lines ¤t_perimeter_lines,
|
||||||
|
double max_support_distance)
|
||||||
|
{
|
||||||
|
assert(preferred_dir == +1 || preferred_dir == -1);
|
||||||
|
if (polyline.points.size() < 2 || target_perimeter_lines.empty() || current_perimeter_lines.empty() ||
|
||||||
|
offset_dist <= SCALED_EPSILON ||
|
||||||
|
max_wipe_length <= SCALED_EPSILON || max_support_distance <= SCALED_EPSILON)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
Lines material_support_lines;
|
||||||
|
const Lines *candidate_support_lines = &target_perimeter_lines;
|
||||||
|
if (seam_start == seam_end) {
|
||||||
|
// Orca: another contour may have a geometrically closer inner wall on
|
||||||
|
// this loop's air side. Restrict zero-gap support using the local seam
|
||||||
|
// normals before choosing the nearest wall.
|
||||||
|
material_support_lines = material_side_support_lines(
|
||||||
|
polyline, seam_start, preferred_dir, target_perimeter_lines);
|
||||||
|
if (material_support_lines.empty())
|
||||||
|
return false;
|
||||||
|
candidate_support_lines = &material_support_lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
AABBTreeLines::LinesDistancer<Line> support_distancer(*candidate_support_lines);
|
||||||
|
const std::optional<Vec2d> support_offset = support_offset_at_start(
|
||||||
|
polyline, seam_end, seam_start != seam_end,
|
||||||
|
support_distancer, max_support_distance);
|
||||||
|
if (! support_offset)
|
||||||
|
return false;
|
||||||
|
const Vec2d toward_support = *support_offset;
|
||||||
|
const double local_support_distance = toward_support.norm();
|
||||||
|
const double effective_offset = std::min(offset_dist, local_support_distance);
|
||||||
|
if (effective_offset <= SCALED_EPSILON)
|
||||||
|
return false;
|
||||||
|
const Vec2d support_direction = toward_support / local_support_distance;
|
||||||
|
|
||||||
|
// Orca: every candidate is validated against the same generated geometry.
|
||||||
|
// Build these AABB trees once per loop instead of rebuilding them for each
|
||||||
|
// preferred, alternate, translated, direct, or reversed candidate.
|
||||||
|
Lines all_support_lines = printed_perimeter_lines;
|
||||||
|
all_support_lines.insert(all_support_lines.end(), current_perimeter_lines.begin(), current_perimeter_lines.end());
|
||||||
|
AABBTreeLines::LinesDistancer<Line> all_support_distancer(std::move(all_support_lines));
|
||||||
|
AABBTreeLines::LinesDistancer<Line> current_perimeter_distancer(current_perimeter_lines);
|
||||||
|
|
||||||
|
// Orca: allow only the contact needed to leave the extrusion endpoint. A
|
||||||
|
// connector that meets the current wall again is a seam-gap retrace, even
|
||||||
|
// if the rest of the non-extruding wipe remains over printed material.
|
||||||
|
const double contact_tolerance = wipe_tolerance(effective_offset);
|
||||||
|
|
||||||
|
struct Candidate {
|
||||||
|
Polyline path;
|
||||||
|
// Orca: support score chooses the material-side path; length is used
|
||||||
|
// only to replace a corner-truncated path with the reverse fallback.
|
||||||
|
double support_score;
|
||||||
|
double path_length;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Direction and wall contact have different origins after a loop pre-move.
|
||||||
|
// Keep the construction's wall endpoint for intersection checks even when
|
||||||
|
// the candidate's direction must be checked from the current nozzle position.
|
||||||
|
const auto validate_candidate = [&](Polyline path, Point path_start, Point direction_start,
|
||||||
|
double path_contact_tolerance,
|
||||||
|
const Vec2d &candidate_support_direction,
|
||||||
|
double candidate_offset,
|
||||||
|
bool require_clearance = true) -> std::optional<Candidate> {
|
||||||
|
// Orca: backtracking indicates a wrong join only across a nonzero gap.
|
||||||
|
// A closed zero-gap offset may initially turn back at its miter while
|
||||||
|
// still remaining on the supported material side of the perimeter.
|
||||||
|
const bool backtracks_across_gap = seam_start != seam_end && starts_by_backtracking(path, wipe_start);
|
||||||
|
// At a clipped corner another branch of the current wall may be closer
|
||||||
|
// than the requested offset. Preserve the zero-gap clearance rule, but
|
||||||
|
// check direction and local material side independently for every gap.
|
||||||
|
const bool material_side = wipe_path_stays_on_material_side(
|
||||||
|
path, direction_start, candidate_support_direction,
|
||||||
|
support_distancer, current_perimeter_distancer, candidate_offset,
|
||||||
|
require_clearance && seam_start == seam_end);
|
||||||
|
const bool connector_clear = initial_connector_is_clear(
|
||||||
|
path, wipe_start, path_start, current_perimeter_distancer, path_contact_tolerance);
|
||||||
|
if (backtracks_across_gap || ! material_side || ! connector_clear)
|
||||||
|
return std::nullopt;
|
||||||
|
const std::optional<double> score = wipe_path_support_score(
|
||||||
|
path, wipe_start, support_distancer, all_support_distancer, max_support_distance);
|
||||||
|
if (! score)
|
||||||
|
return std::nullopt;
|
||||||
|
const double path_length = executable_path_length(path, wipe_start);
|
||||||
|
return Candidate{std::move(path), *score, path_length};
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto offset_candidate = [&](int dir) -> std::optional<Candidate> {
|
||||||
|
Polyline path = polyline;
|
||||||
|
if (! offset_wipe_path(path, seam_start, seam_end, wipe_start, dir,
|
||||||
|
effective_offset, max_wipe_length))
|
||||||
|
return std::nullopt;
|
||||||
|
return validate_candidate(std::move(path), seam_start, seam_start,
|
||||||
|
contact_tolerance, support_direction, effective_offset);
|
||||||
|
};
|
||||||
|
|
||||||
|
std::optional<Candidate> preferred = offset_candidate(preferred_dir);
|
||||||
|
std::optional<Candidate> alternate = offset_candidate(-preferred_dir);
|
||||||
|
|
||||||
|
// Orca: forward and reverse fallbacks share the same clamping, translation,
|
||||||
|
// connector tolerance, and complete-path validation.
|
||||||
|
const auto translated_candidate = [&](Polyline source, Point source_start, Point source_end,
|
||||||
|
const Vec2d &candidate_support_offset) -> std::optional<Candidate> {
|
||||||
|
const double support_distance = candidate_support_offset.norm();
|
||||||
|
const double candidate_offset = std::min(offset_dist, support_distance);
|
||||||
|
if (candidate_offset <= SCALED_EPSILON)
|
||||||
|
return std::nullopt;
|
||||||
|
|
||||||
|
const Vec2d candidate_translation = candidate_support_offset * (candidate_offset / support_distance);
|
||||||
|
if (! translated_wipe_path(source, source_start, source_end, wipe_start,
|
||||||
|
candidate_translation, max_wipe_length))
|
||||||
|
return std::nullopt;
|
||||||
|
const double candidate_tolerance = wipe_tolerance(candidate_offset);
|
||||||
|
return validate_candidate(std::move(source), source_start, source_start, candidate_tolerance,
|
||||||
|
candidate_support_offset / support_distance, candidate_offset);
|
||||||
|
};
|
||||||
|
|
||||||
|
std::optional<Candidate> translated = translated_candidate(polyline, seam_start, seam_end, toward_support);
|
||||||
|
|
||||||
|
// Orca: if every full-length construction folds back onto the external
|
||||||
|
// wall, retain a short direct inward move instead of accepting an outward
|
||||||
|
// candidate or falling back to the standard wipe along the outer wall.
|
||||||
|
const auto direct_candidate = [&](Point origin, const Vec2d &candidate_support_offset) -> std::optional<Candidate> {
|
||||||
|
const double support_distance = candidate_support_offset.norm();
|
||||||
|
const double candidate_offset = std::min(offset_dist, support_distance);
|
||||||
|
if (candidate_offset <= SCALED_EPSILON)
|
||||||
|
return std::nullopt;
|
||||||
|
const Vec2d direction = candidate_support_offset / support_distance;
|
||||||
|
const Point destination = (origin.cast<double>() + direction * candidate_offset).cast<coord_t>();
|
||||||
|
if (destination == wipe_start)
|
||||||
|
return std::nullopt;
|
||||||
|
|
||||||
|
Polyline path;
|
||||||
|
if (! store_wipe_path(path, seam_start, Polyline{wipe_start, destination}, max_wipe_length))
|
||||||
|
return std::nullopt;
|
||||||
|
const double candidate_tolerance = wipe_tolerance(candidate_offset);
|
||||||
|
// Check the executed direction from the nozzle after any loop pre-move,
|
||||||
|
// but retain the wall origin for the connector's intersection checks.
|
||||||
|
return validate_candidate(std::move(path), origin, wipe_start,
|
||||||
|
candidate_tolerance, direction, candidate_offset, false);
|
||||||
|
};
|
||||||
|
std::optional<Candidate> direct = direct_candidate(seam_end, toward_support);
|
||||||
|
|
||||||
|
const double length_margin = wipe_tolerance(max_wipe_length);
|
||||||
|
std::optional<Candidate> reversed;
|
||||||
|
if (seam_start != seam_end && polyline.last_point() == seam_end) {
|
||||||
|
// Orca: when a large gap straddles a sharp corner, connecting the
|
||||||
|
// extrusion end to the forward offset may either reverse or leave only
|
||||||
|
// a short local move. The already printed incoming wall is equally safe:
|
||||||
|
// follow it backwards and determine its own material-side support.
|
||||||
|
Polyline reversed_source = polyline;
|
||||||
|
reversed_source.reverse();
|
||||||
|
const std::optional<Vec2d> reversed_support_offset = support_offset_at_start(
|
||||||
|
reversed_source, seam_end, true, support_distancer, max_support_distance);
|
||||||
|
if (reversed_support_offset) {
|
||||||
|
reversed = translated_candidate(reversed_source, seam_end, seam_end, *reversed_support_offset);
|
||||||
|
// A translated reverse path can backtrack or leave the material on
|
||||||
|
// a curved wall. Offset the incoming wall itself when translation
|
||||||
|
// cannot supply a complete wipe, retaining all candidate checks.
|
||||||
|
if (! reversed || reversed->path_length + length_margin < max_wipe_length) {
|
||||||
|
const double reverse_offset = std::min(offset_dist, reversed_support_offset->norm());
|
||||||
|
if (reverse_offset > SCALED_EPSILON &&
|
||||||
|
offset_wipe_path(reversed_source, seam_end, seam_start, wipe_start,
|
||||||
|
-preferred_dir, reverse_offset, max_wipe_length)) {
|
||||||
|
reversed_source.points.front() = seam_start;
|
||||||
|
auto candidate = validate_candidate(std::move(reversed_source), seam_end, seam_end,
|
||||||
|
wipe_tolerance(reverse_offset), reversed_support_offset->normalized(), reverse_offset);
|
||||||
|
if (candidate && (! reversed ||
|
||||||
|
(candidate->path_length > reversed->path_length + length_margin &&
|
||||||
|
candidate->support_score <= reversed->support_score + wipe_tolerance(reverse_offset))))
|
||||||
|
reversed = std::move(candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Orca: conventional offsets at a narrow cusp may form a bevel across the
|
||||||
|
// cusp. Candidates pointing away from the actual inner wall are rejected
|
||||||
|
// during validation; among the remaining paths, prefer the one whose first
|
||||||
|
// point is materially closer to that wall.
|
||||||
|
const double direction_change_margin = wipe_tolerance(effective_offset);
|
||||||
|
std::optional<Candidate> selected = std::move(preferred);
|
||||||
|
if (translated) {
|
||||||
|
if (! selected || translated->support_score + direction_change_margin < selected->support_score)
|
||||||
|
selected = std::move(translated);
|
||||||
|
}
|
||||||
|
if (! selected)
|
||||||
|
selected = std::move(direct);
|
||||||
|
// Prefer a direct inward move when the normal offset cannot be used.
|
||||||
|
// An alternate offset is eligible only after the same material-side checks.
|
||||||
|
if (! selected)
|
||||||
|
selected = std::move(alternate);
|
||||||
|
|
||||||
|
// Orca: prefer a complete reverse wipe over a forward fallback that had to
|
||||||
|
// stop at the corner. Equal-length paths keep the normal forward behavior.
|
||||||
|
if (reversed && (! selected ||
|
||||||
|
(reversed->path_length > selected->path_length + length_margin &&
|
||||||
|
reversed->support_score <= selected->support_score + direction_change_margin)))
|
||||||
|
selected = std::move(reversed);
|
||||||
|
if (! selected)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
polyline = std::move(selected->path);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<Point> wipe_on_loops_destination(const ExtrusionPaths &paths, double nozzle_diam_scaled,
|
||||||
|
bool is_ccw, bool is_hole)
|
||||||
|
{
|
||||||
|
assert(!paths.empty());
|
||||||
|
assert(nozzle_diam_scaled > 0);
|
||||||
|
if (paths.empty() || nozzle_diam_scaled <= 0)
|
||||||
|
return std::nullopt;
|
||||||
|
|
||||||
|
// Orca: clamp sample distance to L/4 so forward/backward samples cannot meet.
|
||||||
|
double total_length = 0.;
|
||||||
|
for (const ExtrusionPath &path : paths)
|
||||||
|
total_length += path.length();
|
||||||
|
const double sample_distance = std::min(nozzle_diam_scaled, total_length * 0.25);
|
||||||
|
|
||||||
|
Point a = sample_path_at_distance(paths, true, sample_distance);
|
||||||
|
Point b = sample_path_at_distance(paths, false, sample_distance);
|
||||||
|
|
||||||
|
const Point seam_start = paths.front().first_point();
|
||||||
|
|
||||||
|
// Orca: skip the inward move for degenerate geometry.
|
||||||
|
if (a == b || a == seam_start || b == seam_start)
|
||||||
|
return std::nullopt;
|
||||||
|
|
||||||
|
const bool reverse_turn = is_hole == is_ccw;
|
||||||
|
if (reverse_turn)
|
||||||
|
std::swap(a, b);
|
||||||
|
|
||||||
|
double angle = seam_start.ccw_angle(a, b) / 3;
|
||||||
|
|
||||||
|
// Orca: reject degenerate angles near 0 or 2π.
|
||||||
|
static constexpr double angle_epsilon = 0.01;
|
||||||
|
if (angle < angle_epsilon || angle > 2 * PI / 3 - angle_epsilon)
|
||||||
|
return std::nullopt;
|
||||||
|
|
||||||
|
if (reverse_turn)
|
||||||
|
angle *= -1;
|
||||||
|
|
||||||
|
Point pt = sample_path_at_distance(paths, true, std::min(0.2 * nozzle_diam_scaled, sample_distance));
|
||||||
|
pt.rotate(angle, seam_start);
|
||||||
|
return pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Slic3r
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
#include "../ExtrusionEntity.hpp"
|
||||||
|
#include "../Polyline.hpp"
|
||||||
|
#include "../Line.hpp"
|
||||||
|
|
||||||
|
namespace Slic3r {
|
||||||
|
|
||||||
|
// Printed prefix of one region's perimeter sequence. Append each entity only
|
||||||
|
// after extrusion; later walls and other regions cannot support an inward wipe.
|
||||||
|
struct WipeInwardSupport {
|
||||||
|
Lines printed_lines;
|
||||||
|
Lines inner_lines;
|
||||||
|
void append(const ExtrusionEntity &entity);
|
||||||
|
};
|
||||||
|
|
||||||
|
namespace AABBTreeLines {
|
||||||
|
template <typename LineType> class LinesDistancer;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Orca: sample a point at a given distance along ExtrusionPaths, walking
|
||||||
|
// across segment boundaries. forward=true walks from paths.front, false from
|
||||||
|
// paths.back. For tiny loops the walk stops early and returns the last
|
||||||
|
// reachable point. Returns the start point if target is zero.
|
||||||
|
// Precondition: paths must be non-empty.
|
||||||
|
Point sample_path_at_distance(const ExtrusionPaths &paths, bool forward, double target);
|
||||||
|
|
||||||
|
// Orca: return the side of the printed path on which the material lies.
|
||||||
|
// dir +1 is left and -1 is right, matching the offset-builder convention.
|
||||||
|
int wipe_offset_direction(bool is_ccw, bool is_hole);
|
||||||
|
|
||||||
|
// Orca: atomically offset a stored wipe path. The seam-gap or closing edge
|
||||||
|
// determines the join with the first outgoing perimeter edge, but its offset
|
||||||
|
// is not part of the executable wipe. Only the prefix needed by Wipe::wipe()
|
||||||
|
// is offset. Returns false and leaves polyline unchanged if that path cannot
|
||||||
|
// be constructed without degenerate segments. This only constructs a candidate;
|
||||||
|
// offset_wipe_path_toward_support() validates its support, material side and
|
||||||
|
// connector before accepting it. The first stored point
|
||||||
|
// remains a dummy preserving Wipe::wipe()'s convention of skipping points[0].
|
||||||
|
// Precondition: polyline starts at seam_start, dir is +1 or -1, and
|
||||||
|
// offset_dist > 0. A non-positive max_wipe_length returns false.
|
||||||
|
bool offset_wipe_path(Polyline &polyline, Point seam_start, Point seam_end, Point wipe_start,
|
||||||
|
int dir, double offset_dist, double max_wipe_length);
|
||||||
|
|
||||||
|
// Orca: score a candidate's first destination by distance to the target inner
|
||||||
|
// walls. Return nullopt if no target wall is near wipe_start or any executable
|
||||||
|
// segment lacks support. target_distancer contains eligible earlier walls;
|
||||||
|
// all_support_distancer includes the current wall and all earlier walls.
|
||||||
|
// The stored first point is a dummy: the first segment starts at wipe_start.
|
||||||
|
// This checks support only; material-side and connector checks belong to
|
||||||
|
// offset_wipe_path_toward_support(). Trees are reused across its candidates.
|
||||||
|
std::optional<double> wipe_path_support_score(
|
||||||
|
const Polyline &polyline, Point wipe_start,
|
||||||
|
const AABBTreeLines::LinesDistancer<Line> &target_distancer,
|
||||||
|
const AABBTreeLines::LinesDistancer<Line> &all_support_distancer,
|
||||||
|
double max_distance);
|
||||||
|
|
||||||
|
// Validate the initial inward direction and the local material side along the
|
||||||
|
// executable path, using the inner wall to orient the open current wall's
|
||||||
|
// normals. Clearance is optional for clipped corners and short direct fallbacks;
|
||||||
|
// the material-side check is mandatory. The straight connector is checked by
|
||||||
|
// its initial direction and separately by support and intersection validation.
|
||||||
|
// path_start is the construction origin; points[0] is only a storage sentinel.
|
||||||
|
bool wipe_path_stays_on_material_side(
|
||||||
|
const Polyline &path, Point path_start, const Vec2d &support_direction,
|
||||||
|
const AABBTreeLines::LinesDistancer<Line> &target_perimeter_distancer,
|
||||||
|
const AABBTreeLines::LinesDistancer<Line> ¤t_perimeter_distancer,
|
||||||
|
double effective_offset, bool require_clearance);
|
||||||
|
|
||||||
|
// Orca: identify the adjacent inner perimeter from the outgoing wall, excluding
|
||||||
|
// support on the air side of a closed zero-gap loop. Clamp the requested offset
|
||||||
|
// to the distance from the seam end to that support, then select the safest
|
||||||
|
// supported offset or translated path. If a wide seam gap at a corner truncates
|
||||||
|
// every forward candidate, the incoming printed wall may be followed backwards
|
||||||
|
// instead. All earlier printed perimeters still participate in the complete-path
|
||||||
|
// safety check. This handles converging, locally ambiguous, or self-touching
|
||||||
|
// contours whose global winding alone does not identify the material side.
|
||||||
|
// Returns false and leaves polyline unchanged when no candidate is supported.
|
||||||
|
// Precondition: preferred_dir is +1 or -1. Distances must be positive.
|
||||||
|
bool offset_wipe_path_toward_support(Polyline &polyline, Point seam_start, Point seam_end, Point wipe_start,
|
||||||
|
int preferred_dir, double offset_dist, double max_wipe_length,
|
||||||
|
const Lines &target_perimeter_lines, const Lines &printed_perimeter_lines,
|
||||||
|
const Lines ¤t_perimeter_lines,
|
||||||
|
double max_support_distance);
|
||||||
|
|
||||||
|
// Orca: compute the inward destination point for wipe_on_loops, or
|
||||||
|
// std::nullopt when the geometry is degenerate (tiny loop, coincident samples,
|
||||||
|
// angle near 0 or 2π). Returns the rotated destination or nullopt to skip the
|
||||||
|
// inward move entirely.
|
||||||
|
// Precondition: paths non-empty, nozzle_diam_scaled > 0.
|
||||||
|
std::optional<Point> wipe_on_loops_destination(const ExtrusionPaths &paths, double nozzle_diam_scaled,
|
||||||
|
bool is_ccw, bool is_hole);
|
||||||
|
|
||||||
|
} // namespace Slic3r
|
||||||
@@ -1282,6 +1282,8 @@ static std::vector<std::string> s_Preset_print_options{
|
|||||||
"accel_to_decel_enable",
|
"accel_to_decel_enable",
|
||||||
"accel_to_decel_factor",
|
"accel_to_decel_factor",
|
||||||
"wipe_on_loops",
|
"wipe_on_loops",
|
||||||
|
"wipe_inward",
|
||||||
|
"wipe_inward_distance",
|
||||||
"wipe_before_external_loop",
|
"wipe_before_external_loop",
|
||||||
"bridge_density",
|
"bridge_density",
|
||||||
"internal_bridge_density",
|
"internal_bridge_density",
|
||||||
|
|||||||
@@ -233,6 +233,8 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n
|
|||||||
"accel_to_decel_enable",
|
"accel_to_decel_enable",
|
||||||
"accel_to_decel_factor",
|
"accel_to_decel_factor",
|
||||||
"wipe_on_loops",
|
"wipe_on_loops",
|
||||||
|
"wipe_inward",
|
||||||
|
"wipe_inward_distance",
|
||||||
"gcode_comments",
|
"gcode_comments",
|
||||||
"gcode_label_objects",
|
"gcode_label_objects",
|
||||||
"exclude_object",
|
"exclude_object",
|
||||||
|
|||||||
@@ -6267,6 +6267,35 @@ void PrintConfigDef::init_fff_params()
|
|||||||
def->mode = comAdvanced;
|
def->mode = comAdvanced;
|
||||||
def->set_default_value(new ConfigOptionBool(false));
|
def->set_default_value(new ConfigOptionBool(false));
|
||||||
|
|
||||||
|
def = this->add("wipe_inward", coBool);
|
||||||
|
def->label = L("Wipe inward");
|
||||||
|
def->category = L("Quality");
|
||||||
|
def->tooltip = L("Applies only to external walls, including hole boundaries. Moves the hot nozzle toward printed "
|
||||||
|
"inner walls during wiping to reduce reheating of freshly printed plastic and seam marks.\n\n"
|
||||||
|
"Especially useful at layer heights below 0.1 mm, where wipe marks are more visible.\n\n"
|
||||||
|
"Uses the regular wipe if no adjacent inner wall is already printed (single-wall areas or "
|
||||||
|
"Outer/Inner wall order), or if no supported inward path can be found, for example at tight "
|
||||||
|
"corners or seam gaps.");
|
||||||
|
def->mode = comExpert;
|
||||||
|
def->set_default_value(new ConfigOptionBool(false));
|
||||||
|
|
||||||
|
def = this->add("wipe_inward_distance", coFloatOrPercent);
|
||||||
|
def->label = L("Wipe inward distance");
|
||||||
|
def->category = L("Quality");
|
||||||
|
def->tooltip = L("The distance the wipe path is shifted away from the external perimeter, specified in millimeters "
|
||||||
|
"or as a percentage of the actual outer-wall extrusion width.\n\n"
|
||||||
|
"For example, 50% shifts the path by half of the outer-wall width. The effective offset is limited "
|
||||||
|
"by both the actual outer-wall width and the available spacing to the adjacent wall, so values "
|
||||||
|
"above 100% or an equivalent absolute distance have no additional effect. "
|
||||||
|
"Set to 0 to disable the offset.");
|
||||||
|
def->sidetext = L("mm or %");
|
||||||
|
def->ratio_over = "outer_wall_line_width";
|
||||||
|
def->min = 0;
|
||||||
|
def->max = 100;
|
||||||
|
def->max_literal = 2; // Orca: G-code generation also clamps literal values to the actual outer-wall width.
|
||||||
|
def->mode = comExpert;
|
||||||
|
def->set_default_value(new ConfigOptionFloatOrPercent(50, true));
|
||||||
|
|
||||||
def = this->add("wipe_before_external_loop", coBool);
|
def = this->add("wipe_before_external_loop", coBool);
|
||||||
def->label = L("Wipe before external loop");
|
def->label = L("Wipe before external loop");
|
||||||
def->category = L("Quality");
|
def->category = L("Quality");
|
||||||
|
|||||||
@@ -1391,6 +1391,8 @@ PRINT_CONFIG_CLASS_DEFINE(
|
|||||||
((ConfigOptionBool, role_based_wipe_speed))
|
((ConfigOptionBool, role_based_wipe_speed))
|
||||||
((ConfigOptionFloatOrPercent, wipe_speed))
|
((ConfigOptionFloatOrPercent, wipe_speed))
|
||||||
((ConfigOptionBool, wipe_on_loops))
|
((ConfigOptionBool, wipe_on_loops))
|
||||||
|
((ConfigOptionBool, wipe_inward))
|
||||||
|
((ConfigOptionFloatOrPercent, wipe_inward_distance))
|
||||||
((ConfigOptionBool, wipe_before_external_loop))
|
((ConfigOptionBool, wipe_before_external_loop))
|
||||||
((ConfigOptionEnum<WallInfillOrder>, wall_infill_order))
|
((ConfigOptionEnum<WallInfillOrder>, wall_infill_order))
|
||||||
((ConfigOptionBool, precise_outer_wall))
|
((ConfigOptionBool, precise_outer_wall))
|
||||||
|
|||||||
@@ -1574,6 +1574,8 @@ bool PrintObject::invalidate_state_by_config_options(
|
|||||||
|| opt_key == "brim_flow_ratio"
|
|| opt_key == "brim_flow_ratio"
|
||||||
|| opt_key == "filament_flow_ratio"
|
|| opt_key == "filament_flow_ratio"
|
||||||
|| opt_key == "scarf_joint_flow_ratio"
|
|| opt_key == "scarf_joint_flow_ratio"
|
||||||
|
|| opt_key == "wipe_inward"
|
||||||
|
|| opt_key == "wipe_inward_distance"
|
||||||
|| opt_key == "spiral_starting_flow_ratio"
|
|| opt_key == "spiral_starting_flow_ratio"
|
||||||
|| opt_key == "spiral_finishing_flow_ratio") {
|
|| opt_key == "spiral_finishing_flow_ratio") {
|
||||||
invalidated |= m_print->invalidate_step(psGCodeExport);
|
invalidated |= m_print->invalidate_step(psGCodeExport);
|
||||||
|
|||||||
@@ -1104,6 +1104,9 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in
|
|||||||
auto is_role_based_wipe_speed = config->opt_bool("role_based_wipe_speed");
|
auto is_role_based_wipe_speed = config->opt_bool("role_based_wipe_speed");
|
||||||
toggle_field("wipe_speed",!is_role_based_wipe_speed);
|
toggle_field("wipe_speed",!is_role_based_wipe_speed);
|
||||||
|
|
||||||
|
const bool have_wipe_inward = config->opt_bool("wipe_inward");
|
||||||
|
toggle_line("wipe_inward_distance", have_wipe_inward);
|
||||||
|
|
||||||
for (auto el : {"accel_to_decel_enable", "accel_to_decel_factor"})
|
for (auto el : {"accel_to_decel_enable", "accel_to_decel_factor"})
|
||||||
toggle_line(el, gcf_is_klipper);
|
toggle_line(el, gcf_is_klipper);
|
||||||
if(gcf_is_klipper)
|
if(gcf_is_klipper)
|
||||||
|
|||||||
@@ -15755,6 +15755,7 @@ void Plater::calib_pa(const Calib_Params& params)
|
|||||||
auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config;
|
auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config;
|
||||||
print_config->set_key_value("overhang_reverse", new ConfigOptionBool(false));
|
print_config->set_key_value("overhang_reverse", new ConfigOptionBool(false));
|
||||||
print_config->set_key_value("precise_z_height", new ConfigOptionBool(false));
|
print_config->set_key_value("precise_z_height", new ConfigOptionBool(false));
|
||||||
|
print_config->set_key_value("wipe_inward", new ConfigOptionBool(false));
|
||||||
printer_config->set_key_value("resonance_avoidance", new ConfigOptionBool{false});
|
printer_config->set_key_value("resonance_avoidance", new ConfigOptionBool{false});
|
||||||
switch (params.mode) {
|
switch (params.mode) {
|
||||||
case CalibMode::Calib_PA_Line:
|
case CalibMode::Calib_PA_Line:
|
||||||
@@ -16440,6 +16441,7 @@ void Plater::calib_retraction(const Calib_Params& params)
|
|||||||
auto obj = model().objects[0];
|
auto obj = model().objects[0];
|
||||||
|
|
||||||
print_config->set_key_value("enable_wrapping_detection", new ConfigOptionBool(false));
|
print_config->set_key_value("enable_wrapping_detection", new ConfigOptionBool(false));
|
||||||
|
print_config->set_key_value("wipe_inward", new ConfigOptionBool(false));
|
||||||
|
|
||||||
float nozzle_diameter = printer_config->option<ConfigOptionFloats>("nozzle_diameter")->get_at(0);
|
float nozzle_diameter = printer_config->option<ConfigOptionFloats>("nozzle_diameter")->get_at(0);
|
||||||
float layer_height;
|
float layer_height;
|
||||||
|
|||||||
@@ -2669,6 +2669,8 @@ void TabPrint::build()
|
|||||||
optgroup->append_single_option_line("role_based_wipe_speed","quality_settings_seam#role-based-wipe-speed");
|
optgroup->append_single_option_line("role_based_wipe_speed","quality_settings_seam#role-based-wipe-speed");
|
||||||
optgroup->append_single_option_line("wipe_speed", "quality_settings_seam#wipe-speed");
|
optgroup->append_single_option_line("wipe_speed", "quality_settings_seam#wipe-speed");
|
||||||
optgroup->append_single_option_line("wipe_on_loops","quality_settings_seam#wipe-on-loop-inward-movement");
|
optgroup->append_single_option_line("wipe_on_loops","quality_settings_seam#wipe-on-loop-inward-movement");
|
||||||
|
optgroup->append_single_option_line("wipe_inward", "quality_settings_seam#wipe-inward");
|
||||||
|
optgroup->append_single_option_line("wipe_inward_distance", "quality_settings_seam#wipe-inward");
|
||||||
optgroup->append_single_option_line("wipe_before_external_loop","quality_settings_seam#wipe-before-external");
|
optgroup->append_single_option_line("wipe_before_external_loop","quality_settings_seam#wipe-before-external");
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1096,6 +1096,7 @@ bool CalibUtils::calib_generic_PA(const CalibInfo &calib_info, wxString &error_m
|
|||||||
calib_pa_pattern(calib_info, model);
|
calib_pa_pattern(calib_info, model);
|
||||||
|
|
||||||
DynamicPrintConfig print_config = calib_info.print_prest->config;
|
DynamicPrintConfig print_config = calib_info.print_prest->config;
|
||||||
|
print_config.set_key_value("wipe_inward", new ConfigOptionBool(false));
|
||||||
DynamicPrintConfig filament_config = calib_info.filament_prest->config;
|
DynamicPrintConfig filament_config = calib_info.filament_prest->config;
|
||||||
DynamicPrintConfig printer_config = calib_info.printer_prest->config;
|
DynamicPrintConfig printer_config = calib_info.printer_prest->config;
|
||||||
|
|
||||||
@@ -1357,6 +1358,7 @@ void CalibUtils::calib_retraction(const CalibInfo &calib_info, wxString &error_m
|
|||||||
read_model_from_file(input_file, model);
|
read_model_from_file(input_file, model);
|
||||||
|
|
||||||
DynamicPrintConfig print_config = calib_info.print_prest->config;
|
DynamicPrintConfig print_config = calib_info.print_prest->config;
|
||||||
|
print_config.set_key_value("wipe_inward", new ConfigOptionBool(false));
|
||||||
DynamicPrintConfig filament_config = calib_info.filament_prest->config;
|
DynamicPrintConfig filament_config = calib_info.filament_prest->config;
|
||||||
DynamicPrintConfig printer_config = calib_info.printer_prest->config;
|
DynamicPrintConfig printer_config = calib_info.printer_prest->config;
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ add_executable(${_TEST_NAME}_tests
|
|||||||
test_support_material.cpp
|
test_support_material.cpp
|
||||||
test_tree_support.cpp
|
test_tree_support.cpp
|
||||||
test_trianglemesh.cpp
|
test_trianglemesh.cpp
|
||||||
|
test_wipe.cpp
|
||||||
test_wipe_tower.cpp
|
test_wipe_tower.cpp
|
||||||
)
|
)
|
||||||
target_link_libraries(${_TEST_NAME}_tests test_common libslic3r Catch2::Catch2WithMain)
|
target_link_libraries(${_TEST_NAME}_tests test_common libslic3r Catch2::Catch2WithMain)
|
||||||
|
|||||||
@@ -0,0 +1,653 @@
|
|||||||
|
#include <catch2/catch_all.hpp>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <map>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "libslic3r/GCode/GCodeProcessor.hpp"
|
||||||
|
#include "libslic3r/GCodeReader.hpp"
|
||||||
|
#include "libslic3r/PrintConfig.hpp"
|
||||||
|
#include "libslic3r/Layer.hpp"
|
||||||
|
|
||||||
|
#include "test_helpers.hpp"
|
||||||
|
|
||||||
|
using namespace Slic3r;
|
||||||
|
using namespace Slic3r::Test;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
DynamicPrintConfig wipe_config(const char *wall_generator, bool wipe_inward,
|
||||||
|
const char *wipe_inward_distance = "50%",
|
||||||
|
const char *seam_gap = "10%", bool wipe_on_loops = false,
|
||||||
|
const char *wall_loops = "2",
|
||||||
|
const char *wall_sequence = "inner wall/outer wall",
|
||||||
|
bool alternate_extra_wall = false,
|
||||||
|
const char *sparse_infill_density = "0%",
|
||||||
|
const char *seam_position = "aligned")
|
||||||
|
{
|
||||||
|
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
|
||||||
|
config.set_deserialize_strict({
|
||||||
|
{ "nozzle_diameter", "0.4" },
|
||||||
|
{ "layer_height", "0.2" },
|
||||||
|
{ "initial_layer_print_height", "0.2" },
|
||||||
|
{ "line_width", "0.45" },
|
||||||
|
{ "outer_wall_line_width", "0" }, // Orca: Auto must use the actual path width.
|
||||||
|
{ "wall_loops", wall_loops },
|
||||||
|
{ "wall_generator", wall_generator },
|
||||||
|
{ "wall_sequence", wall_sequence },
|
||||||
|
{ "top_shell_layers", "0" },
|
||||||
|
{ "bottom_shell_layers", "0" },
|
||||||
|
{ "sparse_infill_density", sparse_infill_density },
|
||||||
|
{ "seam_position", seam_position },
|
||||||
|
{ "seam_gap", seam_gap },
|
||||||
|
{ "wipe", "1" },
|
||||||
|
{ "wipe_distance", "2" },
|
||||||
|
{ "retraction_length", "0.8" },
|
||||||
|
{ "retract_when_changing_layer", "1" },
|
||||||
|
{ "wipe_inward", wipe_inward ? "1" : "0" },
|
||||||
|
{ "wipe_inward_distance", wipe_inward_distance },
|
||||||
|
{ "wipe_on_loops", wipe_on_loops ? "1" : "0" },
|
||||||
|
{ "alternate_extra_wall", alternate_extra_wall ? "1" : "0" },
|
||||||
|
{ "gcode_comments", "1" },
|
||||||
|
{ "machine_start_gcode", "" },
|
||||||
|
{ "machine_end_gcode", "" },
|
||||||
|
});
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct WipeTrajectory {
|
||||||
|
Vec2d start;
|
||||||
|
double z;
|
||||||
|
std::vector<Vec2d> destinations;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<WipeTrajectory> wipe_trajectories(const std::string &gcode)
|
||||||
|
{
|
||||||
|
const std::string &start_tag = GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Wipe_Start);
|
||||||
|
const std::string &end_tag = GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Wipe_End);
|
||||||
|
std::vector<WipeTrajectory> trajectories;
|
||||||
|
bool in_wipe = false;
|
||||||
|
|
||||||
|
GCodeReader parser;
|
||||||
|
parser.parse_buffer(gcode, [&](GCodeReader &self, const GCodeReader::GCodeLine &line) {
|
||||||
|
const std::string_view comment = line.comment();
|
||||||
|
if (comment.find(start_tag) != std::string_view::npos) {
|
||||||
|
in_wipe = true;
|
||||||
|
trajectories.push_back({Vec2d(self.x(), self.y()), self.z(), {}});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (comment.find(end_tag) != std::string_view::npos) {
|
||||||
|
in_wipe = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (in_wipe && line.dist_XY(self) > EPSILON)
|
||||||
|
trajectories.back().destinations.emplace_back(line.new_X(self), line.new_Y(self));
|
||||||
|
});
|
||||||
|
return trajectories;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Vec2d> wipe_destinations(const std::string &gcode)
|
||||||
|
{
|
||||||
|
std::vector<Vec2d> destinations;
|
||||||
|
for (const WipeTrajectory &trajectory : wipe_trajectories(gcode))
|
||||||
|
destinations.insert(destinations.end(), trajectory.destinations.begin(), trajectory.destinations.end());
|
||||||
|
return destinations;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool trajectories_differ(const std::vector<Vec2d> &lhs, const std::vector<Vec2d> &rhs)
|
||||||
|
{
|
||||||
|
if (lhs.size() != rhs.size())
|
||||||
|
return true;
|
||||||
|
for (size_t i = 0; i < lhs.size(); ++i)
|
||||||
|
if ((lhs[i] - rhs[i]).norm() > 0.01)
|
||||||
|
return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
double trajectory_length(const WipeTrajectory &trajectory)
|
||||||
|
{
|
||||||
|
double length = 0.;
|
||||||
|
Vec2d previous = trajectory.start;
|
||||||
|
for (const Vec2d &destination : trajectory.destinations) {
|
||||||
|
length += (destination - previous).norm();
|
||||||
|
previous = destination;
|
||||||
|
}
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST_CASE("Wipe retraction preserves fractional speed with inward wipe disabled", "[Wipe][Regression]")
|
||||||
|
{
|
||||||
|
const char *retraction_speed = GENERATE("25.25", "25.5", "25.75");
|
||||||
|
const char *relative_e = GENERATE("0", "1");
|
||||||
|
INFO("retraction speed: " << retraction_speed);
|
||||||
|
INFO("relative E: " << relative_e);
|
||||||
|
DynamicPrintConfig config = wipe_config("classic", false);
|
||||||
|
config.set_deserialize_strict({
|
||||||
|
{"gcode_flavor", "marlin2"},
|
||||||
|
{"use_relative_e_distances", relative_e},
|
||||||
|
{"retraction_speed", retraction_speed},
|
||||||
|
{"retraction_length", "0.8"},
|
||||||
|
{"retract_before_wipe", "0%"},
|
||||||
|
{"retract_after_wipe", "0%"},
|
||||||
|
{"role_based_wipe_speed", "0"},
|
||||||
|
{"wipe_speed", "100"},
|
||||||
|
{"wipe_distance", "2"},
|
||||||
|
});
|
||||||
|
const std::string output = slice({make_cube(10., 10., 1.)}, config);
|
||||||
|
const auto &start_tag = GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Wipe_Start);
|
||||||
|
const auto &end_tag = GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Wipe_End);
|
||||||
|
double before_wipe = 0.;
|
||||||
|
double during_wipe = 0.;
|
||||||
|
bool in_wipe = false;
|
||||||
|
bool complete = false;
|
||||||
|
GCodeReader parser;
|
||||||
|
parser.apply_config(config);
|
||||||
|
parser.parse_buffer(output, [&](GCodeReader &self, const GCodeReader::GCodeLine &line) {
|
||||||
|
if (complete)
|
||||||
|
return;
|
||||||
|
if (line.comment().find(start_tag) != std::string_view::npos) {
|
||||||
|
in_wipe = true;
|
||||||
|
} else if (in_wipe && line.comment().find(end_tag) != std::string_view::npos) {
|
||||||
|
complete = true;
|
||||||
|
} else if (line.retracting(self)) {
|
||||||
|
(in_wipe ? during_wipe : before_wipe) -= line.dist_E(self);
|
||||||
|
} else if (line.extruding(self)) {
|
||||||
|
before_wipe = 0.;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
REQUIRE(complete);
|
||||||
|
// At 100 mm/s, the 2 mm wipe lasts 0.02 seconds. The remaining part of
|
||||||
|
// the configured 0.8 mm retraction must be emitted before that wipe.
|
||||||
|
const double expected_during = std::stod(retraction_speed) * 2. / 100.;
|
||||||
|
CHECK_THAT(during_wipe, Catch::Matchers::WithinAbs(expected_during, 0.00005));
|
||||||
|
CHECK_THAT(before_wipe, Catch::Matchers::WithinAbs(0.8 - expected_during, 0.00005));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Inward wipe respects the minimum travel for retraction and Z hop", "[Wipe][Regression]")
|
||||||
|
{
|
||||||
|
const char *wall_generator = GENERATE("classic", "arachne");
|
||||||
|
const char *relative_e = GENERATE("0", "1");
|
||||||
|
const char *reduce_crossing_wall = GENERATE("0", "1");
|
||||||
|
const char *minimum_travel = GENERATE("5", "0");
|
||||||
|
CAPTURE(wall_generator, relative_e, reduce_crossing_wall, minimum_travel);
|
||||||
|
DynamicPrintConfig config = wipe_config(
|
||||||
|
wall_generator, true, "50%", "10%", false, "3", "inner-outer-inner wall");
|
||||||
|
config.set_deserialize_strict({
|
||||||
|
{"gcode_flavor", "marlin2"},
|
||||||
|
{"use_relative_e_distances", relative_e},
|
||||||
|
{"reduce_crossing_wall", reduce_crossing_wall},
|
||||||
|
{"retraction_minimum_travel", minimum_travel},
|
||||||
|
{"retract_when_changing_layer", "0"},
|
||||||
|
{"use_firmware_retraction", "0"},
|
||||||
|
{"retract_before_wipe", "0%"},
|
||||||
|
{"retract_after_wipe", "0%"},
|
||||||
|
{"retraction_speed", "25.5"},
|
||||||
|
{"role_based_wipe_speed", "0"},
|
||||||
|
{"wipe_speed", "100"},
|
||||||
|
{"z_hop", "0.4"},
|
||||||
|
{"retract_lift_above", "0"},
|
||||||
|
{"retract_lift_below", "0"},
|
||||||
|
});
|
||||||
|
config.set_key_value("z_hop_types", new ConfigOptionEnumsGeneric{zhtNormal});
|
||||||
|
config.set_key_value("retract_lift_enforce", new ConfigOptionEnumsGeneric{rletAllSurfaces});
|
||||||
|
const std::string output = slice({make_cube(10., 10., 1.)}, config);
|
||||||
|
const auto &role_tag = GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Role);
|
||||||
|
const auto &start_tag = GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Wipe_Start);
|
||||||
|
const auto &end_tag = GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Wipe_End);
|
||||||
|
ExtrusionRole role = erNone;
|
||||||
|
bool after_outer_wall = false;
|
||||||
|
bool in_wipe = false;
|
||||||
|
size_t transitions = 0;
|
||||||
|
size_t same_layer_transitions = 0;
|
||||||
|
size_t inward_wipes = 0;
|
||||||
|
double retraction = 0.;
|
||||||
|
double lift = 0.;
|
||||||
|
double outer_z = 0.;
|
||||||
|
GCodeReader parser;
|
||||||
|
parser.apply_config(config);
|
||||||
|
parser.parse_buffer(output, [&](GCodeReader &self, const GCodeReader::GCodeLine &line) {
|
||||||
|
if (line.comment().find(role_tag) == 0)
|
||||||
|
role = ExtrusionEntity::string_to_role(line.comment().substr(role_tag.size()));
|
||||||
|
if (line.comment().find(start_tag) == 0) {
|
||||||
|
in_wipe = true;
|
||||||
|
if (after_outer_wall)
|
||||||
|
++inward_wipes;
|
||||||
|
} else if (line.comment().find(end_tag) == 0) {
|
||||||
|
in_wipe = false;
|
||||||
|
}
|
||||||
|
if (line.extruding(self) && line.dist_XY(self) > EPSILON) {
|
||||||
|
if (role == erExternalPerimeter) {
|
||||||
|
after_outer_wall = true;
|
||||||
|
retraction = lift = 0.;
|
||||||
|
outer_z = line.new_Z(self);
|
||||||
|
} else if (after_outer_wall) {
|
||||||
|
REQUIRE(role == erPerimeter);
|
||||||
|
++transitions;
|
||||||
|
const double layer_rise = std::max(0., double(self.z()) - outer_z);
|
||||||
|
if (layer_rise < EPSILON)
|
||||||
|
++same_layer_transitions;
|
||||||
|
// A 5 mm threshold suppresses retraction across a few wall widths.
|
||||||
|
// A zero threshold still permits the ordinary retract and lift.
|
||||||
|
const bool retract = std::stod(minimum_travel) == 0.;
|
||||||
|
CHECK_THAT(retraction, Catch::Matchers::WithinAbs(retract ? 0.8 : 0., 0.00005));
|
||||||
|
// Exclude an ordinary layer change from the accumulated upward motion.
|
||||||
|
CHECK_THAT(lift - layer_rise, Catch::Matchers::WithinAbs(retract ? 0.4 : 0., 0.001));
|
||||||
|
after_outer_wall = false;
|
||||||
|
}
|
||||||
|
} else if (after_outer_wall) {
|
||||||
|
if (line.retracting(self))
|
||||||
|
retraction -= line.dist_E(self);
|
||||||
|
lift += std::max(0., double(line.dist_Z(self)));
|
||||||
|
if (in_wipe)
|
||||||
|
CHECK_THAT(line.dist_E(self), Catch::Matchers::WithinAbs(0., 0.00005));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// The 1 mm cube has five 0.2 mm layers: every outer wall must still wipe.
|
||||||
|
REQUIRE(transitions == 5);
|
||||||
|
REQUIRE(same_layer_transitions >= 4);
|
||||||
|
REQUIRE(inward_wipes == transitions);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Changing inward wipe settings preserves the sliced geometry", "[Wipe][Regression]")
|
||||||
|
{
|
||||||
|
const char *key = GENERATE("wipe_inward", "wipe_inward_distance");
|
||||||
|
DynamicPrintConfig config = wipe_config("classic", false);
|
||||||
|
Print print;
|
||||||
|
Model model;
|
||||||
|
init_print({make_cube(10., 10., 1.)}, print, model, config);
|
||||||
|
gcode(print);
|
||||||
|
const PrintObject &object = *print.objects().front();
|
||||||
|
REQUIRE(object.is_step_done(posPerimeters));
|
||||||
|
REQUIRE(object.is_step_done(posInfill));
|
||||||
|
REQUIRE(print.is_step_done(psWipeTower));
|
||||||
|
REQUIRE(print.is_step_done(psGCodeExport));
|
||||||
|
|
||||||
|
DynamicPrintConfig changed = config;
|
||||||
|
changed.set_deserialize_strict({{key, std::string(key) == "wipe_inward" ? "1" : "75%"}});
|
||||||
|
print.apply(model, changed);
|
||||||
|
|
||||||
|
CHECK(print.objects().front()->is_step_done(posPerimeters));
|
||||||
|
CHECK(print.objects().front()->is_step_done(posInfill));
|
||||||
|
CHECK(print.is_step_done(psWipeTower));
|
||||||
|
CHECK_FALSE(print.is_step_done(psGCodeExport));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Retraction and pressure advance calibration suppress inward wipe overrides", "[Wipe][Regression]")
|
||||||
|
{
|
||||||
|
const auto mode = GENERATE(CalibMode::Calib_None, CalibMode::Calib_PA_Tower,
|
||||||
|
CalibMode::Calib_Auto_PA_Line, CalibMode::Calib_Retraction_tower,
|
||||||
|
CalibMode::Calib_Flow_Rate);
|
||||||
|
const char *wall_generator = GENERATE("classic", "arachne");
|
||||||
|
const bool per_object = GENERATE(false, true);
|
||||||
|
INFO("calibration mode: " << int(mode) << ", wall generator: " << wall_generator
|
||||||
|
<< ", per-object override: " << per_object);
|
||||||
|
|
||||||
|
const auto trajectories = [&](bool inward) {
|
||||||
|
DynamicPrintConfig config = wipe_config(wall_generator, inward && !per_object);
|
||||||
|
const std::vector<std::vector<ConfigBase::SetDeserializeItem>> overrides{
|
||||||
|
{{"wipe_inward", inward ? "1" : "0"}}
|
||||||
|
};
|
||||||
|
Print print;
|
||||||
|
Model model;
|
||||||
|
init_print({make_cube(10., 10., 1.)}, print, model, config, per_object ? &overrides : nullptr);
|
||||||
|
Calib_Params params;
|
||||||
|
params.mode = mode;
|
||||||
|
params.start = 0.2;
|
||||||
|
params.end = 0.4;
|
||||||
|
params.step = 0.1;
|
||||||
|
print.set_calib_params(params);
|
||||||
|
return wipe_destinations(gcode(print));
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto regular = trajectories(false);
|
||||||
|
const auto inward = trajectories(true);
|
||||||
|
REQUIRE_FALSE(regular.empty());
|
||||||
|
REQUIRE_FALSE(inward.empty());
|
||||||
|
// Other calibration modes and ordinary prints must still honor the option.
|
||||||
|
const bool should_differ = mode == CalibMode::Calib_None || mode == CalibMode::Calib_Flow_Rate;
|
||||||
|
CHECK(trajectories_differ(regular, inward) == should_differ);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Inactive inward wipe settings preserve the exported trajectory", "[Wipe][Regression]")
|
||||||
|
{
|
||||||
|
const char *wall_generator = GENERATE("classic", "arachne");
|
||||||
|
const bool disable_wiping = GENERATE(false, true);
|
||||||
|
DynamicPrintConfig regular = wipe_config(wall_generator, false);
|
||||||
|
DynamicPrintConfig inward = wipe_config(wall_generator, true, disable_wiping ? "50%" : "0");
|
||||||
|
if (disable_wiping) {
|
||||||
|
regular.set_deserialize_strict({{"wipe", "0"}});
|
||||||
|
inward.set_deserialize_strict({{"wipe", "0"}});
|
||||||
|
}
|
||||||
|
const auto regular_paths = wipe_destinations(slice({make_cube(10., 10., 1.)}, regular));
|
||||||
|
const auto inward_paths = wipe_destinations(slice({make_cube(10., 10., 1.)}, inward));
|
||||||
|
if (!disable_wiping)
|
||||||
|
REQUIRE_FALSE(regular_paths.empty());
|
||||||
|
CHECK_FALSE(trajectories_differ(regular_paths, inward_paths));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Inward wipe changes the exported trajectory when outer wall width is Auto", "[Wipe][Regression]")
|
||||||
|
{
|
||||||
|
const char *wall_generator = GENERATE("classic", "arachne");
|
||||||
|
INFO("wall generator: " << wall_generator);
|
||||||
|
|
||||||
|
const std::vector<Vec2d> regular = wipe_destinations(
|
||||||
|
slice({make_cube(10., 10., 1.)}, wipe_config(wall_generator, false)));
|
||||||
|
const std::vector<Vec2d> inward = wipe_destinations(
|
||||||
|
slice({make_cube(10., 10., 1.)}, wipe_config(wall_generator, true)));
|
||||||
|
|
||||||
|
REQUIRE_FALSE(regular.empty());
|
||||||
|
REQUIRE_FALSE(inward.empty());
|
||||||
|
REQUIRE(trajectories_differ(regular, inward));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Inward wipe recognizes an external wall starting on an overhang", "[Wipe][Regression]")
|
||||||
|
{
|
||||||
|
const char *wall_generator = GENERATE("classic", "arachne");
|
||||||
|
const bool inward = GENERATE(false, true);
|
||||||
|
CAPTURE(wall_generator, inward);
|
||||||
|
const auto config = wipe_config(wall_generator, inward, "50%", "0%", false,
|
||||||
|
"3", "inner-outer-inner wall", false, "0%", "back");
|
||||||
|
Print print;
|
||||||
|
Model model;
|
||||||
|
init_print({make_cube(10., 10., 1.)}, print, model, config);
|
||||||
|
print.process();
|
||||||
|
size_t mixed_loops = 0;
|
||||||
|
const auto mark_overhangs = [&](auto &&self, ExtrusionEntity *entity) -> void {
|
||||||
|
if (auto *collection = dynamic_cast<ExtrusionEntityCollection *>(entity)) {
|
||||||
|
for (ExtrusionEntity *child : collection->entities)
|
||||||
|
self(self, child);
|
||||||
|
} else if (auto *loop = dynamic_cast<ExtrusionLoop *>(entity); loop && is_external_perimeter(loop->role())) {
|
||||||
|
// Keep the printed geometry intact and give the back seam overhang
|
||||||
|
// roles. The front edge remains an ordinary external-wall segment.
|
||||||
|
ExtrusionPaths paths;
|
||||||
|
bool has_overhang = false;
|
||||||
|
bool has_external = false;
|
||||||
|
for (const ExtrusionPath &source : loop->paths) {
|
||||||
|
for (size_t i = 1; i < source.polyline.points.size(); ++i) {
|
||||||
|
ExtrusionPath path = source;
|
||||||
|
path.polyline.points = {source.polyline.points[i - 1], source.polyline.points[i]};
|
||||||
|
const bool overhang = path.polyline.points.front().y() > 0 || path.polyline.points.back().y() > 0;
|
||||||
|
path.set_extrusion_role(overhang ? erOverhangPerimeter : erExternalPerimeter);
|
||||||
|
has_overhang |= overhang;
|
||||||
|
has_external |= !overhang;
|
||||||
|
paths.push_back(std::move(path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
REQUIRE(has_overhang);
|
||||||
|
REQUIRE(has_external);
|
||||||
|
loop->paths = std::move(paths);
|
||||||
|
++mixed_loops;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
for (const PrintObject *object : print.objects())
|
||||||
|
for (Layer *layer : object->layers())
|
||||||
|
for (LayerRegion *region : layer->regions())
|
||||||
|
mark_overhangs(mark_overhangs, ®ion->perimeters);
|
||||||
|
REQUIRE(mixed_loops > 0);
|
||||||
|
|
||||||
|
bool has_inward_wipe = false;
|
||||||
|
for (const WipeTrajectory &trajectory : wipe_trajectories(gcode(print))) {
|
||||||
|
if (trajectory.destinations.empty())
|
||||||
|
continue;
|
||||||
|
const Vec2d move = trajectory.destinations.front() - trajectory.start;
|
||||||
|
if (trajectory.start.x() > 4. && trajectory.start.y() > 4. && move.x() < -0.05 && move.y() < -0.05)
|
||||||
|
has_inward_wipe = true;
|
||||||
|
}
|
||||||
|
CHECK(has_inward_wipe == inward);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Inward wipe keeps its offset when seam gap is zero", "[Wipe][Regression]")
|
||||||
|
{
|
||||||
|
const char *wall_generator = GENERATE("classic", "arachne");
|
||||||
|
INFO("wall generator: " << wall_generator);
|
||||||
|
|
||||||
|
const std::vector<Vec2d> regular = wipe_destinations(
|
||||||
|
slice({make_cube(10., 10., 1.)}, wipe_config(wall_generator, false, "50%", "0%")));
|
||||||
|
const std::vector<Vec2d> inward = wipe_destinations(
|
||||||
|
slice({make_cube(10., 10., 1.)}, wipe_config(wall_generator, true, "50%", "0%")));
|
||||||
|
|
||||||
|
REQUIRE_FALSE(regular.empty());
|
||||||
|
REQUIRE_FALSE(inward.empty());
|
||||||
|
REQUIRE(trajectories_differ(regular, inward));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Inward wipe is retained across layers with a back seam", "[Wipe][Regression]")
|
||||||
|
{
|
||||||
|
const char *wall_generator = GENERATE("classic", "arachne");
|
||||||
|
INFO("wall generator: " << wall_generator);
|
||||||
|
|
||||||
|
const DynamicPrintConfig inward_config = wipe_config(
|
||||||
|
wall_generator, true, "50%", "0%", false, "3", "inner-outer-inner wall", false, "0%", "back");
|
||||||
|
const std::vector<WipeTrajectory> inward = wipe_trajectories(slice({make_cube(27., 27., 1.)}, inward_config));
|
||||||
|
|
||||||
|
REQUIRE_FALSE(inward.empty());
|
||||||
|
std::map<double, bool> inward_wipe_by_layer;
|
||||||
|
for (const WipeTrajectory &trajectory : inward) {
|
||||||
|
bool &has_inward_wipe = inward_wipe_by_layer[trajectory.z];
|
||||||
|
if (trajectory.destinations.empty())
|
||||||
|
continue;
|
||||||
|
const Vec2d first_move = trajectory.destinations.front() - trajectory.start;
|
||||||
|
// Orca: a back seam lands on the cube's positive-X/positive-Y corner.
|
||||||
|
// Its inward wipe must move diagonally away from both external faces.
|
||||||
|
has_inward_wipe = has_inward_wipe ||
|
||||||
|
(trajectory.start.x() > 13. && trajectory.start.y() > 13. &&
|
||||||
|
first_move.x() < -0.05 && first_move.y() < -0.05);
|
||||||
|
}
|
||||||
|
REQUIRE(inward_wipe_by_layer.size() == 5);
|
||||||
|
for (const auto &[z, has_inward_wipe] : inward_wipe_by_layer) {
|
||||||
|
INFO("layer Z: " << z);
|
||||||
|
REQUIRE(has_inward_wipe);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Literal inward wipe distance is clamped to the outer wall width", "[Wipe][Regression]")
|
||||||
|
{
|
||||||
|
const char *wall_generator = GENERATE("classic", "arachne");
|
||||||
|
INFO("wall generator: " << wall_generator);
|
||||||
|
|
||||||
|
const std::vector<Vec2d> regular = wipe_destinations(
|
||||||
|
slice({make_cube(10., 10., 1.)}, wipe_config(wall_generator, false)));
|
||||||
|
const std::vector<Vec2d> full_width = wipe_destinations(
|
||||||
|
slice({make_cube(10., 10., 1.)}, wipe_config(wall_generator, true, "100%")));
|
||||||
|
const std::vector<Vec2d> oversized = wipe_destinations(
|
||||||
|
slice({make_cube(10., 10., 1.)}, wipe_config(wall_generator, true, "2")));
|
||||||
|
|
||||||
|
REQUIRE_FALSE(full_width.empty());
|
||||||
|
REQUIRE(trajectories_differ(regular, full_width));
|
||||||
|
REQUIRE(oversized.size() == full_width.size());
|
||||||
|
for (size_t i = 0; i < full_width.size(); ++i)
|
||||||
|
REQUIRE_THAT((oversized[i] - full_width[i]).norm(), Catch::Matchers::WithinAbs(0., 0.01));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Inward wipe is not applied without an adjacent wall", "[Wipe][Regression]")
|
||||||
|
{
|
||||||
|
const char *wall_generator = GENERATE("classic", "arachne");
|
||||||
|
INFO("wall generator: " << wall_generator);
|
||||||
|
|
||||||
|
const std::vector<Vec2d> regular = wipe_destinations(
|
||||||
|
slice({make_cube(10., 10., 1.)}, wipe_config(wall_generator, false, "50%", "10%", false, "1")));
|
||||||
|
const std::vector<Vec2d> inward = wipe_destinations(
|
||||||
|
slice({make_cube(10., 10., 1.)}, wipe_config(wall_generator, true, "50%", "10%", false, "1")));
|
||||||
|
|
||||||
|
REQUIRE_FALSE(regular.empty());
|
||||||
|
REQUIRE_FALSE(trajectories_differ(regular, inward));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Inward wipe uses an alternate extra wall when the configured wall count is one", "[Wipe][Regression]")
|
||||||
|
{
|
||||||
|
const char *wall_generator = GENERATE("classic", "arachne");
|
||||||
|
INFO("wall generator: " << wall_generator);
|
||||||
|
|
||||||
|
const DynamicPrintConfig regular_config = wipe_config(
|
||||||
|
wall_generator, false, "50%", "10%", false, "1", "inner wall/outer wall", true, "15%");
|
||||||
|
const DynamicPrintConfig inward_config = wipe_config(
|
||||||
|
wall_generator, true, "50%", "10%", false, "1", "inner wall/outer wall", true, "15%");
|
||||||
|
const std::vector<Vec2d> regular = wipe_destinations(
|
||||||
|
slice({make_cube(10., 10., 1.)}, regular_config));
|
||||||
|
const std::vector<Vec2d> inward = wipe_destinations(
|
||||||
|
slice({make_cube(10., 10., 1.)}, inward_config));
|
||||||
|
|
||||||
|
REQUIRE_FALSE(regular.empty());
|
||||||
|
REQUIRE_FALSE(inward.empty());
|
||||||
|
REQUIRE(trajectories_differ(regular, inward));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Inward wipe is not applied before the adjacent wall is printed", "[Wipe][Regression]")
|
||||||
|
{
|
||||||
|
const char *wall_generator = GENERATE("classic", "arachne");
|
||||||
|
INFO("wall generator: " << wall_generator);
|
||||||
|
|
||||||
|
const std::vector<Vec2d> regular = wipe_destinations(
|
||||||
|
slice({make_cube(10., 10., 1.)}, wipe_config(
|
||||||
|
wall_generator, false, "50%", "10%", false, "2", "outer wall/inner wall")));
|
||||||
|
const std::vector<Vec2d> inward = wipe_destinations(
|
||||||
|
slice({make_cube(10., 10., 1.)}, wipe_config(
|
||||||
|
wall_generator, true, "50%", "10%", false, "2", "outer wall/inner wall")));
|
||||||
|
|
||||||
|
REQUIRE_FALSE(regular.empty());
|
||||||
|
REQUIRE_FALSE(trajectories_differ(regular, inward));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Wipe on loops preserves the corner move with inward wipe disabled", "[Wipe][Regression]")
|
||||||
|
{
|
||||||
|
const char *wall_generator = GENERATE("classic", "arachne");
|
||||||
|
const char *nozzle_diameter = GENERATE("0.4", "0.8");
|
||||||
|
const char *comments = GENERATE("0", "1");
|
||||||
|
CAPTURE(comments);
|
||||||
|
INFO("wall generator: " << wall_generator << ", nozzle diameter: " << nozzle_diameter);
|
||||||
|
// A closed square gives a 90-degree material-side corner at the seam.
|
||||||
|
DynamicPrintConfig config = wipe_config(wall_generator, false, "50%", "0", true);
|
||||||
|
config.set_deserialize_strict({{"nozzle_diameter", nozzle_diameter}, {"seam_position", "nearest"},
|
||||||
|
{"gcode_comments", comments}});
|
||||||
|
const std::string output = slice({make_cube(10., 10., 1.)}, config);
|
||||||
|
const auto &role_tag = GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Role);
|
||||||
|
const auto &wipe_tag = GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Wipe_Start);
|
||||||
|
ExtrusionRole role = erNone;
|
||||||
|
std::vector<Vec2d> loop;
|
||||||
|
bool after_extrusion = false;
|
||||||
|
size_t moves = 0;
|
||||||
|
GCodeReader parser;
|
||||||
|
parser.apply_config(config);
|
||||||
|
parser.parse_buffer(output, [&](GCodeReader &self, const GCodeReader::GCodeLine &line) {
|
||||||
|
if (line.comment().find(role_tag) == 0) {
|
||||||
|
role = ExtrusionEntity::string_to_role(line.comment().substr(role_tag.size()));
|
||||||
|
loop.clear();
|
||||||
|
after_extrusion = false;
|
||||||
|
}
|
||||||
|
if (line.comment().find(wipe_tag) == 0)
|
||||||
|
after_extrusion = false;
|
||||||
|
if (role != erExternalPerimeter)
|
||||||
|
return;
|
||||||
|
if (line.extruding(self) && line.dist_XY(self) > EPSILON) {
|
||||||
|
if (loop.empty())
|
||||||
|
loop.emplace_back(self.x(), self.y());
|
||||||
|
loop.emplace_back(line.new_X(self), line.new_Y(self));
|
||||||
|
after_extrusion = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// The loop move is the first non-extruding XY move after the external
|
||||||
|
// wall and before the reserved wipe marker, regardless of comment text.
|
||||||
|
if (!after_extrusion || line.dist_XY(self) <= EPSILON)
|
||||||
|
return;
|
||||||
|
after_extrusion = false;
|
||||||
|
|
||||||
|
++moves;
|
||||||
|
INFO("layer Z: " << self.z());
|
||||||
|
REQUIRE(loop.size() >= 4);
|
||||||
|
const Vec2d seam = loop.front();
|
||||||
|
REQUIRE_THAT((loop.back() - seam).norm(), Catch::Matchers::WithinAbs(0., 0.003));
|
||||||
|
const Vec2d outgoing = (loop[1] - seam).normalized();
|
||||||
|
const Vec2d into_corner = (loop[loop.size() - 2] - seam).normalized();
|
||||||
|
REQUIRE_THAT(outgoing.dot(into_corner), Catch::Matchers::WithinAbs(0., 0.01));
|
||||||
|
const Vec2d move = Vec2d(line.new_X(self), line.new_Y(self)) - seam;
|
||||||
|
// The legacy corner move is 20% of the nozzle diameter, turned 30 degrees
|
||||||
|
// from the outgoing edge into the square. Check both components independently.
|
||||||
|
const double distance = 0.2 * std::stod(nozzle_diameter);
|
||||||
|
CHECK_THAT(move.dot(outgoing), Catch::Matchers::WithinAbs(distance * std::sqrt(3.) / 2., 0.003));
|
||||||
|
CHECK_THAT(move.dot(into_corner), Catch::Matchers::WithinAbs(distance / 2., 0.003));
|
||||||
|
});
|
||||||
|
REQUIRE(moves == 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Inward wipe remains valid after wipe on loops moves the nozzle", "[Wipe][Regression]")
|
||||||
|
{
|
||||||
|
const char *wall_generator = GENERATE("classic", "arachne");
|
||||||
|
const char *comments = GENERATE("0", "1");
|
||||||
|
CAPTURE(comments);
|
||||||
|
INFO("wall generator: " << wall_generator);
|
||||||
|
|
||||||
|
DynamicPrintConfig config = wipe_config(wall_generator, false, "50%", "10%", true);
|
||||||
|
config.set_deserialize_strict({{"gcode_comments", comments}});
|
||||||
|
const std::string loop_move = slice({make_cube(10., 10., 1.)}, config);
|
||||||
|
config.set_deserialize_strict({{"wipe_inward", "1"}});
|
||||||
|
const std::string combined = slice({make_cube(10., 10., 1.)}, config);
|
||||||
|
config.set_deserialize_strict({{"wipe_on_loops", "0"}});
|
||||||
|
const std::string inward_only = slice({make_cube(10., 10., 1.)}, config);
|
||||||
|
|
||||||
|
for (const std::string *output : {&loop_move, &combined}) {
|
||||||
|
INFO("wipe_inward: " << (output == &combined));
|
||||||
|
std::map<double, std::vector<Vec2d>> loop_moves_by_layer;
|
||||||
|
const auto &role_tag = GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Role);
|
||||||
|
const auto &wipe_tag = GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Wipe_Start);
|
||||||
|
ExtrusionRole role = erNone;
|
||||||
|
bool after_extrusion = false;
|
||||||
|
GCodeReader parser;
|
||||||
|
parser.apply_config(config);
|
||||||
|
parser.parse_buffer(*output, [&](GCodeReader &self, const GCodeReader::GCodeLine &line) {
|
||||||
|
if (line.comment().find(role_tag) == 0) {
|
||||||
|
role = ExtrusionEntity::string_to_role(line.comment().substr(role_tag.size()));
|
||||||
|
after_extrusion = false;
|
||||||
|
}
|
||||||
|
if (line.comment().find(wipe_tag) == 0)
|
||||||
|
after_extrusion = false;
|
||||||
|
if (role != erExternalPerimeter || line.dist_XY(self) <= EPSILON)
|
||||||
|
return;
|
||||||
|
if (line.extruding(self)) {
|
||||||
|
after_extrusion = true;
|
||||||
|
} else if (after_extrusion) {
|
||||||
|
loop_moves_by_layer[line.new_Z(self)].emplace_back(line.new_X(self), line.new_Y(self));
|
||||||
|
after_extrusion = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// The 1 mm cube at 0.2 mm layer height has one external loop on each of five layers.
|
||||||
|
const auto trajectories = wipe_trajectories(*output);
|
||||||
|
REQUIRE(loop_moves_by_layer.size() == 5);
|
||||||
|
for (size_t layer = 1; layer <= 5; ++layer) {
|
||||||
|
const double z = layer * 0.2;
|
||||||
|
const auto moves = std::find_if(loop_moves_by_layer.begin(), loop_moves_by_layer.end(),
|
||||||
|
[z](const auto &entry) { return std::abs(entry.first - z) < 0.001; });
|
||||||
|
REQUIRE(moves != loop_moves_by_layer.end());
|
||||||
|
REQUIRE(moves->second.size() == 1);
|
||||||
|
const auto wipe = std::find_if(trajectories.begin(), trajectories.end(), [&](const WipeTrajectory &trajectory) {
|
||||||
|
return std::abs(trajectory.z - z) < 0.001 &&
|
||||||
|
(trajectory.start - moves->second.front()).norm() < 0.001;
|
||||||
|
});
|
||||||
|
REQUIRE(wipe != trajectories.end());
|
||||||
|
// The configured 2 mm wipe must be measured from the inward move's
|
||||||
|
// endpoint, including when wipe_inward is off (set_last_pos regression).
|
||||||
|
CHECK_THAT(trajectory_length(*wipe), Catch::Matchers::WithinAbs(2., 0.003));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<WipeTrajectory> combined_trajectories = wipe_trajectories(combined);
|
||||||
|
const std::vector<WipeTrajectory> inward_trajectories = wipe_trajectories(inward_only);
|
||||||
|
REQUIRE_FALSE(combined_trajectories.empty());
|
||||||
|
REQUIRE(combined_trajectories.size() == inward_trajectories.size());
|
||||||
|
REQUIRE(trajectories_differ(wipe_destinations(combined), wipe_destinations(loop_move)));
|
||||||
|
|
||||||
|
bool start_changed = false;
|
||||||
|
for (size_t i = 0; i < combined_trajectories.size(); ++i) {
|
||||||
|
start_changed = start_changed ||
|
||||||
|
(combined_trajectories[i].start - inward_trajectories[i].start).norm() > 0.01;
|
||||||
|
REQUIRE_THAT(trajectory_length(combined_trajectories[i]),
|
||||||
|
Catch::Matchers::WithinAbs(trajectory_length(inward_trajectories[i]), 0.01));
|
||||||
|
}
|
||||||
|
REQUIRE(start_changed);
|
||||||
|
}
|
||||||
@@ -43,6 +43,7 @@ add_executable(${_TEST_NAME}_tests
|
|||||||
test_voronoi.cpp
|
test_voronoi.cpp
|
||||||
test_wipe_tower_estimate.cpp
|
test_wipe_tower_estimate.cpp
|
||||||
test_wipe_tower.cpp
|
test_wipe_tower.cpp
|
||||||
|
test_wipe_path.cpp
|
||||||
test_optimizers.cpp
|
test_optimizers.cpp
|
||||||
test_ordering_strategies.cpp
|
test_ordering_strategies.cpp
|
||||||
# test_png_io.cpp
|
# test_png_io.cpp
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user