#include "WipePathHelpers.hpp" #include "../AABBTreeLines.hpp" #include "libslic3r/ExtrusionEntity.hpp" #include "libslic3r/Line.hpp" #include "libslic3r/Point.hpp" #include "libslic3r/libslic3r.h" #include #include #include #include #include #include #include #include namespace Slic3r { void WipeInwardSupport::append(const ExtrusionEntity &entity) { const ExtrusionPaths *paths = nullptr; if (const auto *loop = dynamic_cast(&entity)) paths = &loop->paths; else if (const auto *multipath = dynamic_cast(&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().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(); const Vec2d v_next = (pts[i + 1] - pts[i]).cast(); 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 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().normalized(); const Vec2d next = (source[i + 1] - source[i]).cast().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(); const Vec2d outgoing = (path.points[2] - path.points[1]).cast(); // 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(); const double length = segment.norm(); if (length <= SCALED_EPSILON) continue; if (target <= length) return (result.cast() + segment * (target / length)).cast(); 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(); const double projection = (wipe_start - actual_path.points[entry]).cast().dot(edge); if (projection <= 0.) break; if (projection < edge.squaredNorm()) { actual_path.points[entry] = (actual_path.points[entry].cast() + edge * (projection / edge.squaredNorm())).cast(); 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() + translation).cast(); 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 &distancer, double max_distance) { const Point midpoint = ((start.cast() + end.cast()) * 0.5).cast(); const auto [distance, line_index, nearest] = distancer.distance_from_lines_extra(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(start) > max_distance || distancer.distance_from_lines(end) > max_distance) return false; // Conservatively reject an unresolved transition at coordinate precision. if ((end - start).cast().norm() <= SCALED_EPSILON) return false; return segment_is_supported(start, midpoint, distancer, max_distance) && segment_is_supported(midpoint, end, distancer, max_distance); } std::optional wipe_path_support_score( const Polyline &polyline, Point wipe_start, const AABBTreeLines::LinesDistancer &target_distancer, const AABBTreeLines::LinesDistancer &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(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(polyline.points[1]); } static bool initial_connector_is_clear( const Polyline &polyline, Point wipe_start, Point seam_start, AABBTreeLines::LinesDistancer ¤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(connector); for (const auto &intersection : intersections) { if ((intersection.first - wipe_start).cast().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().norm() > contact_tolerance) return false; return true; } static std::optional support_offset_at_start( const Polyline &source, Point local_origin, bool disambiguate_branch, AABBTreeLines::LinesDistancer &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(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(); 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(); 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::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() - local_origin.cast(); 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().norm(); for (size_t index = 2; index < stored_path.points.size(); ++index) length += (stored_path.points[index] - stored_path.points[index - 1]).cast().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().normalized(); const Vec2d incoming = (seam - *incoming_it).cast().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().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 &target_perimeter_distancer, const AABBTreeLines::LinesDistancer ¤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(); 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(); 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(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()).norm() <= SCALED_EPSILON && previous.b == line.a) normal += left_normal(previous); if ((nearest - line.b.cast()).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(); const Vec2d support_point = std::get<2>( target_perimeter_distancer.distance_from_lines_extra(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() - 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(); const size_t samples = std::max(1, size_t(std::ceil(segment.norm() / effective_offset))); for (size_t sample = 1; sample <= samples; ++sample) { const Point point = (previous.cast() + segment * (double(sample) / double(samples))).cast(); 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 support_distancer(*candidate_support_lines); const std::optional 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 all_support_distancer(std::move(all_support_lines)); AABBTreeLines::LinesDistancer 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 { // 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 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 { 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 preferred = offset_candidate(preferred_dir); std::optional 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 { 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 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 { 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() + direction * candidate_offset).cast(); 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 direct = direct_candidate(seam_end, toward_support); const double length_margin = wipe_tolerance(max_wipe_length); std::optional 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 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 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 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