// Precise Seam implementation. Design: docs/HLSD/precise-seam.md #include "PreciseSeam.hpp" #include "PreciseSeamInternal.hpp" #include "libslic3r/Polygon.hpp" #include "libslic3r/Polyline.hpp" #include "libslic3r/Point.hpp" #include "libslic3r/Model.hpp" #include "SeamPlacer.hpp" #include "libslic3r/libslic3r.h" #include "libslic3r/BoundingBox.hpp" #include "libslic3r/ClipperUtils.hpp" #include "libslic3r/Layer.hpp" #include "libslic3r/Print.hpp" #include #include #include #include #include #include #include #include #include #include #include namespace Slic3r { namespace PreciseSeam { ModifierRegions prepare_modifier_regions(ExPolygons regions) { ModifierRegions cached; cached.reserve(regions.size()); // Each exterior is measured once; its holes remain in the same cached region. for (ExPolygon ®ion : regions) cached.emplace_back(std::move(region)); return cached; } ModifierSlices prepare_modifier_slices(std::vector slices) { ModifierSlices cached; cached.reserve(slices.size()); // Keep empty layers so callers can index by the original object layer number. for (ExPolygons &layer : slices) cached.push_back(prepare_modifier_regions(std::move(layer))); return cached; } PreparedPerimeter::PreparedPerimeter(const Polygon &perimeter) : polygon(perimeter) { const size_t count = polygon.size(); if (count < 3) return; for (size_t edge = 0; edge < count; ++edge) if (polygon.points[edge] == polygon.points[(edge + 1) % count]) return; bounds = BoundingBox(polygon.points); // Close an open polyline explicitly; prepare its storage once for all modifier queries. line.points.reserve(count + 1); line.points.insert(line.points.end(), polygon.points.begin(), polygon.points.end()); line.points.push_back(polygon.points.front()); valid = true; } // Import EnforcedBlockedSeamPoint from SeamPlacerImpl namespace for convenience using SeamPlacerImpl::EnforcedBlockedSeamPoint; // Squared on-edge tolerance in coordinate units: Clipper truncates cuts to whole units, under 1 unit // per axis, so under sqrt(2) from the edge (squared < 2); 2.5 keeps a margin. Independent of SCALING_FACTOR. static constexpr double MACHINE_PRECISION_SQUARED = 2.5; // Snapping radius for point insertion, shared by every rule that must match insertion. Deliberately in // scaled units (1 um by default, 10 um on large beds), well above single-precision candidate steps. static constexpr coord_t TOLERANCE_LINEAR = 1000; static constexpr coord_t TOLERANCE_SQUARED = TOLERANCE_LINEAR * TOLERANCE_LINEAR; namespace detail { // Use the existing clipping tolerance only to recover rounded coordinates, not to // bridge gaps between intervals: even a small uncovered interval must remain a gap. static std::optional parameter_on_edge(const Point &point, const Point &a, const Point &b) { // Original vertices need no floating-point projection. if (point == a) return 0.; if (point == b) return 1.; const Vec2d direction = b.cast() - a.cast(); const Vec2d offset = point.cast() - a.cast(); const double squared_length = direction.squaredNorm(); if (squared_length == 0.) return std::nullopt; const double parameter = std::clamp(offset.dot(direction) / squared_length, 0., 1.); if ((offset - parameter * direction).squaredNorm() > MACHINE_PRECISION_SQUARED) return std::nullopt; return parameter; } static std::optional interval_on_edge( const Point &first, const Point &last, size_t edge, const Polygon &perimeter) { const Point &a = perimeter.points[edge]; const Point &b = perimeter.points[(edge + 1) % perimeter.size()]; const auto t0 = parameter_on_edge(first, a, b); // Reject this edge before projecting the second point when the first is outside. if (!t0) return std::nullopt; const auto t1 = parameter_on_edge(last, a, b); if (!t1 || *t0 == *t1) return std::nullopt; ClippedEdgeInterval interval{edge, *t0, *t1, first, last}; if (interval.begin > interval.end) { std::swap(interval.begin, interval.end); std::swap(interval.first, interval.last); } // Canonical endpoints make adjacent original edges join at their shared vertex. if (interval.begin == 0.) interval.first = a; if (interval.end == 1.) interval.last = b; return interval; } // Interior clipping vertices normally retain the exact source coordinates. Only // the two cut endpoints need projection; a failed sequence tries the next anchor. static bool append_exact_fragment(const Polyline &fragment, const Polygon &perimeter, std::vector &intervals) { const size_t size = fragment.size(); const size_t count = perimeter.size(); assert(size >= 3); // bind_fragment() sends only fragments with interior points here. // A longer sequence cannot be one exact pass over this contour. if (size > count + 2) return false; for (size_t anchor = 0; anchor < count; ++anchor) { if (fragment.points[1] != perimeter.points[anchor]) continue; // Clipping may return either direction, independent of contour winding. for (bool forward : {true, false}) { const auto next = [count, forward](size_t index) { return forward ? (index + 1) % count : (index + count - 1) % count; }; size_t last = anchor; bool matches = true; for (size_t i = 2; i + 1 < size; ++i) { last = next(last); if (fragment.points[i] != perimeter.points[last]) { matches = false; break; } } if (!matches) continue; const size_t first_edge = forward ? (anchor + count - 1) % count : anchor; const size_t last_edge = forward ? last : (last + count - 1) % count; const auto first = interval_on_edge(fragment.points.front(), fragment.points[1], first_edge, perimeter); const auto end = interval_on_edge(fragment.points[size - 2], fragment.points.back(), last_edge, perimeter); if (!first || !end) continue; // Commit only a complete match, so rejected anchors leave no intervals. intervals.push_back(*first); size_t vertex = anchor; for (size_t i = 2; i + 1 < size; ++i) { const size_t adjacent = next(vertex); const size_t edge = forward ? vertex : adjacent; intervals.push_back({edge, 0., 1., perimeter.points[edge], perimeter.points[(edge + 1) % count]}); vertex = adjacent; } intervals.push_back(*end); return true; } } return false; } // Find the first edge once, then follow the contour without restarting a global search. bool append_projected_fragment(const Polyline &fragment, const Polygon &perimeter, std::vector &intervals, FragmentBindingFailure &failure) { const size_t original_size = intervals.size(); bool forward = true; const size_t count = perimeter.size(); for (size_t i = 1; i < fragment.size(); ++i) { if (fragment.points[i - 1] == fragment.points[i]) continue; failure.pair_index = i - 1; std::optional matched; const auto is_forward = [&](const ClippedEdgeInterval &candidate) { const Vec2d movement = fragment.points[i].cast() - fragment.points[i - 1].cast(); const Vec2d edge = perimeter.points[(candidate.edge + 1) % count].cast() - perimeter.points[candidate.edge].cast(); return movement.dot(edge) > 0.; }; if (intervals.size() == original_size) { failure.reason = "initial edge not found"; for (size_t edge = 0; edge < count; ++edge) { matched = interval_on_edge(fragment.points[i - 1], fragment.points[i], edge, perimeter); if (matched) { // The first suitable edge defines orientation; overlapping visits are unsupported. forward = is_forward(*matched); break; } } } else { failure.reason = "non-continuous binding"; const ClippedEdgeInterval &previous = intervals.back(); const double end = forward ? previous.end : previous.begin; matched = interval_on_edge(fragment.points[i - 1], fragment.points[i], previous.edge, perimeter); if (matched && is_forward(*matched) != forward) matched.reset(); // The shared point is the previous pair's last point on the same edge, so continuity holds by // construction. Reuse its parameter rather than comparing a recomputed one: the two projections // may be compiled differently (e.g. FMA contraction) and differ by an ulp on some platforms. if (matched) (forward ? matched->begin : matched->end) = end; // Crossing to the neighbor is allowed only at their actual shared vertex. const size_t vertex = forward ? (previous.edge + 1) % count : previous.edge; if (!matched && end == (forward ? 1. : 0.) && fragment.points[i - 1] == perimeter.points[vertex]) { const size_t edge = forward ? vertex : (previous.edge + count - 1) % count; matched = interval_on_edge(fragment.points[i - 1], fragment.points[i], edge, perimeter); if (matched && is_forward(*matched) != forward) matched.reset(); // The shared point is exactly the neighbor's start (forward) or end (backward) vertex, // so parameter_on_edge() returns exactly 0 or 1 for it. assert(!matched || (forward ? matched->begin == 0. : matched->end == 1.)); } } if (!matched) { // Never leave a partial fragment or discard earlier successful fragments. intervals.resize(original_size); return false; } intervals.push_back(*matched); } return intervals.size() > original_size; } // Exact binding first, projection as the general path; both leave intervals unchanged on failure. bool bind_fragment(const Polyline &fragment, const Polygon &perimeter, std::vector &intervals, FragmentBindingFailure &failure) { if (fragment.size() > 2 && append_exact_fragment(fragment, perimeter, intervals)) return true; return append_projected_fragment(fragment, perimeter, intervals, failure); } // Rare-case repair, run only after binding has failed: Clipper may place an end cut a unit or two beside // a vertex, so it lies on neither edge. Move such an end onto that vertex, taken from the fragment's own // chain, and let the caller retry with the same strict rules, so a wrong move can only fail again. // Returns false when nothing was changed. static bool snap_cuts_to_adjacent_vertices(Polyline &fragment, const Polygon &perimeter) { const size_t count = perimeter.size(); // Strict comparison matches the vertex snapping in insert_point_into_perimeter. const auto close = [](const Point &a, const Point &b) { return (a - b).cast().squaredNorm() < double(TOLERANCE_SQUARED); }; const auto snap_target = [&](const Point &cut, const Point &neighbour) -> std::optional { if (cut == neighbour) return std::nullopt; bool neighbour_is_vertex = false; bool ambiguous = false; std::optional target; // Scan the whole perimeter before deciding: a cut that is a real vertex must be detected anywhere, // and an ambiguity must not pre-empt the neighbour rule below. for (size_t j = 0; j < count; ++j) { if (perimeter[j] == cut) return std::nullopt; // A real vertex, not a rounded cut. if (perimeter[j] != neighbour) continue; neighbour_is_vertex = true; for (const Point &candidate : {perimeter[(j + count - 1) % count], perimeter[(j + 1) % count]}) { if (!close(cut, candidate)) continue; if (target && *target != candidate) ambiguous = true; target = candidate; } } // A cut between two arbitrary points of one edge is not this rounding case. if (!neighbour_is_vertex) return std::nullopt; // The neighbour itself takes precedence, even when both of its chain vertices are close too // (edges shorter than the radius on both sides): the cut is then just a rounded copy of it. if (close(cut, neighbour)) return neighbour; // Two different edge-start candidates: keep the failure rather than guess. if (ambiguous) return std::nullopt; return target; }; bool changed = false; if (fragment.size() >= 2) if (const auto target = snap_target(fragment.points.front(), fragment.points[1])) { if (*target == fragment.points[1]) fragment.points.erase(fragment.points.begin()); else fragment.points.front() = *target; changed = true; } if (fragment.size() >= 2) if (const auto target = snap_target(fragment.points.back(), fragment.points[fragment.size() - 2])) { if (*target == fragment.points[fragment.size() - 2]) fragment.points.pop_back(); else fragment.points.back() = *target; changed = true; } return changed; } // Location fields shared by the failure and recovery markers, enough to find the layer in a saved project. static std::string fragment_location(const ExtractionContext &context, size_t fragment_index) { const Layer *layer = context.layer; const ModelObject *object = layer && layer->object() ? layer->object()->model_object() : nullptr; return " object=" + std::to_string(object ? object->id().id : 0) + " modifier=" + std::to_string(context.modifier ? context.modifier->id().id : 0) + " layer=" + (layer ? std::to_string(layer->id()) : std::string("unknown")) + " z=" + (layer ? std::to_string(layer->slice_z) : std::string("unknown")) + " fragment=" + std::to_string(fragment_index); } bool append_fragment(const Polyline &fragment, const Polygon &perimeter, std::vector &intervals, const ExtractionContext &context, size_t fragment_index) { FragmentBindingFailure failure; if (bind_fragment(fragment, perimeter, intervals, failure)) return true; // Rare-case fallback: repair the end cuts and retry once. A recovery is logged but is not a failure // (no failure count, no user warning). const char *outcome = nullptr; Polyline cleaned = fragment; if (snap_cuts_to_adjacent_vertices(cleaned, perimeter)) { FragmentBindingFailure retry_failure; // Only a contact shorter than the snapping distance may remain: nothing to bind, nothing lost. if (cleaned.size() < 2) outcome = "contact"; else if (bind_fragment(cleaned, perimeter, intervals, retry_failure)) outcome = "bound"; } // A failed fragment shorter than the snapping radius is a contact: insertion would collapse it to one // point anyway, so accept it instead of reporting a failure. if (outcome == nullptr && fragment.length() < double(TOLERANCE_LINEAR)) outcome = "contact"; if (outcome != nullptr) { // Same log budget as failures, counted separately; SeamPlacer::init() reports the total. if (context.warnings == nullptr || context.warnings->recovered_fragments.fetch_add(1, std::memory_order_relaxed) < failed_fragment_log_limit) BOOST_LOG_TRIVIAL(warning) << "[PreciseSeamFragmentRecovered] Rare case resolved after a binding failure" << " outcome=" << outcome << fragment_location(context, fragment_index) << " original_pair=" << failure.pair_index << " original_reason=" << failure.reason << " fragment_points=" << fragment.size() << " cleaned_points=" << cleaned.size() << " perimeter_points=" << perimeter.size(); return true; } // Reserve a log slot atomically before formatting; callers without shared state log every failure. if (context.warnings && context.warnings->failed_fragments.fetch_add(1, std::memory_order_relaxed) >= failed_fragment_log_limit) return false; // Keep a small marker for investigating a saved project, not a full geometry dump. BOOST_LOG_TRIVIAL(warning) << "[PreciseSeamIntersectionFailed] Unable to process intersection" << fragment_location(context, fragment_index) << " pair=" << failure.pair_index << " reason=" << failure.reason << " fragment_points=" << fragment.size() << " perimeter_points=" << perimeter.size(); return false; } } // namespace detail // Finalize one joined strong segment; only Center needs temporary per-edge lengths. static void prepare_strong_segment(PerimeterSegment &segment, ModelVolumeType mode, bool compare_lengths) { const bool center = mode == ModelVolumeType::PRECISE_SEAM_CENTER; std::vector edge_lengths; if (center) edge_lengths.reserve(segment.edge_indices.size()); // A single Left/Right segment needs only its endpoint; Center always needs arc length. if (center || compare_lengths) { for (size_t i = 0; i < segment.edge_indices.size(); ++i) { const double length = (segment.polyline.points[i + 1].cast() - segment.polyline.points[i].cast()).norm(); segment.length += length; if (center) edge_lengths.push_back(length); } } if (mode == ModelVolumeType::PRECISE_SEAM_LEFT) { segment.strong_target = StrongSeamTarget{segment.polyline.points.front(), segment.begin.edge_index}; return; } segment.strong_target = StrongSeamTarget{segment.polyline.points.back(), segment.end.edge_index}; if (!center) return; // The total is now known; reuse local lengths to locate its half without new square roots. double remaining = segment.length * 0.5; for (size_t i = 0; i < edge_lengths.size(); ++i) { const double length = edge_lengths[i]; if (remaining <= length && length > 0.) { const Point &a = segment.polyline.points[i]; const Point &b = segment.polyline.points[i + 1]; segment.strong_target = StrongSeamTarget{ a + ((remaining / length) * (b - a).cast()).cast(), segment.edge_indices[i]}; return; } remaining -= length; } } SegmentExtraction extract_perimeter_segments(const PreparedPerimeter &prepared, const ModifierRegions &modifier, ModelVolumeType mode, const ExtractionContext &context) { using detail::ClippedEdgeInterval; SegmentExtraction result; if (!prepared.valid) { result.valid = false; return result; } const Polygon &perimeter = prepared.polygon; const size_t count = perimeter.size(); std::vector nearby_regions; for (const ModifierRegion ®ion : modifier) if (!region.polygon.empty() && prepared.bounds.overlap(region.bounds)) nearby_regions.push_back(®ion.polygon); if (nearby_regions.empty()) return result; // Clip as an open line, without offsets or contact rules: modifiers should cross the perimeter // clearly, and the open path keeps self-touching vertices apart. std::vector intervals; Polylines fragments; // Clip only accepted regions, with their holes still attached to the exterior. for (const ExPolygon *region : nearby_regions) { Polylines clipped = intersection_pl(prepared.line, *region); for (Polyline &fragment : clipped) // Clipper returns a single point where a border only touches the line: nothing to bind. if (fragment.size() >= 2) fragments.push_back(std::move(fragment)); } for (size_t i = 0; i < fragments.size(); ++i) if (!detail::append_fragment(fragments[i], perimeter, intervals, context, i)) ++result.discarded_fragments; std::sort(intervals.begin(), intervals.end(), [](const auto &a, const auto &b) { if (a.edge != b.edge) return a.edge < b.edge; if (a.begin != b.begin) return a.begin < b.begin; return a.end < b.end; }); // Union overlaps on the same occurrence of an edge, never across equal coordinates. std::vector merged; for (const ClippedEdgeInterval &interval : intervals) { // Fragments touching on an edge meet at the same integer point. Comparing that point as well keeps // them joined even if its two projections differ by an ulp on some platforms (e.g. FMA contraction). if (!merged.empty() && merged.back().edge == interval.edge && (interval.begin <= merged.back().end || interval.first == merged.back().last)) { if (interval.end > merged.back().end) { merged.back().end = interval.end; merged.back().last = interval.last; } } else merged.push_back(interval); } const auto position = [count](size_t edge, double parameter) { return parameter == 1. ? PerimeterPosition{(edge + 1) % count, 0.} : PerimeterPosition{edge, parameter}; }; const auto same_position = [](const PerimeterPosition &a, const PerimeterPosition &b) { return a.edge_index == b.edge_index && a.parameter == b.parameter; }; for (const ClippedEdgeInterval &interval : merged) { const auto begin = position(interval.edge, interval.begin); const auto end = position(interval.edge, interval.end); if (result.segments.empty() || !same_position(result.segments.back().end, begin)) { PerimeterSegment segment; segment.begin = begin; segment.polyline.points.push_back(interval.first); result.segments.push_back(std::move(segment)); } PerimeterSegment &segment = result.segments.back(); segment.polyline.points.push_back(interval.last); segment.edge_indices.push_back(interval.edge); segment.end = end; } if (result.segments.size() > 1 && same_position(result.segments.back().end, result.segments.front().begin)) { // Only the artificial cut at vertex zero can join the last and first intervals. PerimeterSegment tail = std::move(result.segments.back()); result.segments.pop_back(); PerimeterSegment &head = result.segments.front(); tail.polyline.points.insert(tail.polyline.points.end(), head.polyline.points.begin() + 1, head.polyline.points.end()); tail.edge_indices.insert(tail.edge_indices.end(), head.edge_indices.begin(), head.edge_indices.end()); tail.end = head.end; head = std::move(tail); } // A full loop must cover every original edge, not merely have equal endpoint coordinates. result.full_containment = merged.size() == count; for (size_t i = 0; result.full_containment && i < count; ++i) result.full_containment = merged[i].edge == i && merged[i].begin == 0. && merged[i].end == 1.; // A touch on an inclined edge leaves a sub-micron gap. Treat the single segment as full containment // when the gap is shorter than the snapping radius, or spans one vertex with both ends within it // (exact for edges of 2 um and longer). The 2 um end distance is a cheap pre-filter. if (!result.full_containment && result.segments.size() == 1) { const PerimeterSegment &only = result.segments.front(); const Point &first = only.polyline.points.front(); const Point &last = only.polyline.points.back(); const double limit = double(TOLERANCE_LINEAR); if ((first - last).cast().squaredNorm() < 4. * limit * limit) { const auto edge_length = [&perimeter, count](size_t edge) { return (perimeter[(edge + 1) % count] - perimeter[edge]).cast().norm(); }; // Uncovered length from the segment's end forward to its begin; stops early once it exceeds the limit. const auto gap_length = [&](const PerimeterPosition &from, const PerimeterPosition &to) { if (from.edge_index == to.edge_index && from.parameter <= to.parameter) return (to.parameter - from.parameter) * edge_length(from.edge_index); double gap = (1. - from.parameter) * edge_length(from.edge_index); for (size_t edge = (from.edge_index + 1) % count; gap < limit && edge != to.edge_index; edge = (edge + 1) % count) gap += edge_length(edge); return gap + to.parameter * edge_length(to.edge_index); }; // Strict comparisons match the vertex snapping in insert_point_into_perimeter. const auto within_snap = [limit](const Point &a, const Point &b) { return (a - b).cast().squaredNorm() < limit * limit; }; const Point &vertex = perimeter[only.begin.edge_index]; const bool around_one_vertex = (only.end.edge_index + 1) % count == only.begin.edge_index && within_snap(last, vertex) && within_snap(first, vertex); if (around_one_vertex || gap_length(only.end, only.begin) < limit) result.full_containment = true; } } // Strong skips full containment and weak handles it by type; neither needs strong data for it. if (!result.full_containment && is_precise_seam_strong(mode)) for (PerimeterSegment &segment : result.segments) prepare_strong_segment(segment, mode, result.segments.size() > 1); return result; } void init_precise_seam_data( std::vector& strong_volumes_out, std::vector& weak_volumes_out, bool& has_strong_out, const ModelObject* model_object) { // Clear output vectors strong_volumes_out.clear(); weak_volumes_out.clear(); if (model_object == nullptr) { has_strong_out = false; return; } // Collect and categorize precise seam modifiers for (const ModelVolume* volume : model_object->volumes) { if (volume->is_precise_seam()) { // Strong (Center/Left/Right) and weak (Enforced/Blocked/Neutral) go to separate lists. if (volume->is_precise_seam_strong()) strong_volumes_out.push_back(volume); else weak_volumes_out.push_back(volume); } } has_strong_out = !strong_volumes_out.empty(); // Collection already preserves model order, with higher-priority strong modifiers first. // Weak modifiers use last-write-wins, so apply the higher-priority ones last. std::reverse(weak_volumes_out.begin(), weak_volumes_out.end()); } // Inserts a point on edge `edge_start_idx`, or reuses a vertex closer than TOLERANCE_LINEAR. // Returns {final coordinates, index}. Requires at least three vertices (callers ensure it). static std::pair insert_point_into_perimeter( const Point &point, size_t edge_start_idx, Polygon &perimeter_polygon ) { assert(perimeter_polygon.points.size() >= 3); size_t perim_max = perimeter_polygon.points.size(); // Determine edge start and end size_t vtx_start = edge_start_idx; size_t vtx_end = (edge_start_idx + 1) % perim_max; const Point &perim_p_start = perimeter_polygon.points[vtx_start]; const Point &perim_p_end = perimeter_polygon.points[vtx_end]; // Check proximity to edge vertices coord_t dist_sq_start = (point - perim_p_start).squaredNorm(); if (dist_sq_start < TOLERANCE_SQUARED) { return std::make_pair(perim_p_start, vtx_start); } coord_t dist_sq_end = (point - perim_p_end).squaredNorm(); if (dist_sq_end < TOLERANCE_SQUARED) { return std::make_pair(perim_p_end, vtx_end); } // On the closing edge, append instead of inserting at index 0: callers insert in descending source // order and rely on earlier indices staying valid. size_t insert_pos; if (edge_start_idx == perim_max - 1) { // Last edge: add to end of vector perimeter_polygon.points.push_back(point); insert_pos = perimeter_polygon.points.size() - 1; } else { // Regular edge: insert before end vertex insert_pos = vtx_end; perimeter_polygon.points.insert( perimeter_polygon.points.begin() + insert_pos, point ); } return std::make_pair(perimeter_polygon.points[insert_pos], insert_pos); } // Adds a helper point TOLERANCE_LINEAR after (+1) or before (-1) vertex `point_idx`; skips edges shorter // than 2 * TOLERANCE_LINEAR. Requires at least three vertices. static void refine_at_vertex( size_t point_idx, int direction, Polygon &perimeter_polygon ) { assert(perimeter_polygon.points.size() >= 3); assert(direction == 1 || direction == -1); size_t perim_max = perimeter_polygon.points.size(); // Determine target edge based on direction size_t edge_start_idx, edge_end_idx; if (direction == 1) { // Direction +1: insertion AFTER point_idx (edge point_idx → point_idx+1) edge_start_idx = point_idx; edge_end_idx = (point_idx + 1) % perim_max; } else { // Direction -1: insertion BEFORE point_idx (edge point_idx-1 → point_idx) edge_start_idx = (point_idx + perim_max - 1) % perim_max; edge_end_idx = point_idx; } const Point &edge_start = perimeter_polygon.points[edge_start_idx]; const Point &edge_end = perimeter_polygon.points[edge_end_idx]; // Calculate edge length Vec2d edge_vector = (edge_end - edge_start).cast(); double edge_length = edge_vector.norm(); // The helper needs TOLERANCE_LINEAR of edge on both sides. if (edge_length < 2.0 * TOLERANCE_LINEAR) { return; // Edge too short - new point would be too close to end } // Calculate new point coordinates: edge_start + TOLERANCE_LINEAR * direction_normalized Vec2d direction_normalized = edge_vector / edge_length; // Place helper point near point_idx: after it for +1, before it for -1 auto offset = (TOLERANCE_LINEAR * direction_normalized).cast(); Point new_point = (direction == 1) ? Point(edge_start + offset) : Point(edge_end - offset); // On the closing edge, append instead of inserting at index 0: callers insert in descending source // order and rely on earlier indices staying valid. if (edge_start_idx == perim_max - 1) { // Last edge: add to end of vector perimeter_polygon.points.push_back(new_point); } else { // Regular edge: insert before end vertex perimeter_polygon.points.insert( perimeter_polygon.points.begin() + edge_end_idx, new_point ); } } // Records one extraction for the warnings: the type of a modifier with a discarded fragment, and the // evaluated/reached flags for "had no effect" (a discarded fragment counts as reached, a contact not). static void record_extraction(PreciseSeamWarnings *warnings, const ModelVolume *modifier, const SegmentExtraction &extracted) { if (warnings == nullptr || !extracted.valid) return; if (extracted.discarded_fragments > 0) PreciseSeamWarnings::mark(warnings->failed_types, modifier->type()); const auto it = warnings->modifier_usage.find(modifier); if (it == warnings->modifier_usage.end()) return; // Load before store: most calls find the flag already set, so shared cache lines stay clean. PreciseSeamWarnings::ModifierUsage &usage = it->second; if (!usage.checked.load(std::memory_order_relaxed)) usage.checked.store(true, std::memory_order_relaxed); const bool reached = !extracted.segments.empty() || extracted.full_containment || extracted.discarded_fragments > 0; if (reached && !usage.reached.load(std::memory_order_relaxed)) usage.reached.store(true, std::memory_order_relaxed); } // Strong priority is per modifier, never a global maximum across different modifiers. std::optional insert_strong_seam_point( const std::vector &strong_volumes, Polygon &polygon, const PreparedPerimeter &prepared, const Layer *layer, const ModifierRegionsCache &slices_cache, PreciseSeamWarnings* warnings) { assert(&prepared.polygon == &polygon); if (strong_volumes.empty() || layer == nullptr || !prepared.valid) return std::nullopt; const size_t layer_id = layer->id() - layer->object()->slicing_parameters().raft_layers(); for (const ModelVolume *modifier : strong_volumes) { const auto it = slices_cache.find(modifier); if (it == slices_cache.end() || layer_id >= it->second.size()) continue; const SegmentExtraction extracted = extract_perimeter_segments( prepared, it->second[layer_id], modifier->type(), {layer, modifier, warnings}); record_extraction(warnings, modifier, extracted); // Full containment leaves no intersection to place the point on: skipped with a warning. if (extracted.full_containment) { if (warnings) PreciseSeamWarnings::mark(warnings->full_containment, modifier->type()); continue; } // No usable segment, even if all fragments were discarded: pass the turn to the next modifier. if (extracted.segments.empty()) continue; if (warnings && extracted.segments.size() > 1) PreciseSeamWarnings::mark(warnings->multiple_intersections, modifier->type()); // Lengths are compared exactly: for a symmetric modifier the chosen face may alternate between // layers, which is accepted (the user gets the multiple-intersections warning). StrongSeamTarget target{Point(0, 0), 0}; double longest = -1.; for (const PerimeterSegment &segment : extracted.segments) { assert(segment.strong_target.has_value()); const StrongSeamTarget &candidate = *segment.strong_target; // Slice coordinates already include object rotation and retain bed axes. // Centering/instance translation cannot change rear (+Y), then left (-X) ordering. const bool farther_or_left = candidate.point.y() > target.point.y() || (candidate.point.y() == target.point.y() && candidate.point.x() < target.point.x()); if (segment.length > longest || (segment.length == longest && farther_or_left)) { longest = segment.length; target = candidate; } } const auto [seam_point, seam_index] = insert_point_into_perimeter(target.point, target.edge_index, polygon); // Preserve the insertion order: refining before first would shift the seam index. refine_at_vertex(seam_index, +1, polygon); refine_at_vertex(seam_index, -1, polygon); return seam_point; // No later strong modifier or weak processing for this perimeter. } return std::nullopt; } // Convert ModelVolumeType of weak modifier to EnforcedBlockedSeamPoint. // Precondition: a weak precise-seam type; init_precise_seam_data() puts only those in the weak list. // Exhaustive switch (no default) so -Wswitch flags any future PRECISE_SEAM_* additions. static EnforcedBlockedSeamPoint convert_weak_modifier_type(ModelVolumeType type) { assert(is_precise_seam_weak(type)); switch (type) { case ModelVolumeType::PRECISE_SEAM_ENFORCED: return EnforcedBlockedSeamPoint::Enforced; case ModelVolumeType::PRECISE_SEAM_BLOCKED: return EnforcedBlockedSeamPoint::Blocked; case ModelVolumeType::PRECISE_SEAM_NEUTRAL: return EnforcedBlockedSeamPoint::Neutral; // Non-weak types are unreachable by precondition; listed to keep the switch exhaustive. case ModelVolumeType::INVALID: case ModelVolumeType::MODEL_PART: case ModelVolumeType::NEGATIVE_VOLUME: case ModelVolumeType::PARAMETER_MODIFIER: case ModelVolumeType::SUPPORT_BLOCKER: case ModelVolumeType::SUPPORT_ENFORCER: case ModelVolumeType::PRECISE_SEAM_CENTER: case ModelVolumeType::PRECISE_SEAM_LEFT: case ModelVolumeType::PRECISE_SEAM_RIGHT: break; } assert(false && "convert_weak_modifier_type called with non-weak type"); return EnforcedBlockedSeamPoint::Neutral; } // Consume ready boundaries in modifier priority order; geometry extraction is separate. static std::vector prepare_weak_modifier_segments( std::vector result, Polygon &polygon) { // If no segments, return empty vector if (result.empty()) { return result; } // Source positions have the same order as arc length, without measuring the perimeter. struct PointToInsert { size_t segment_idx; bool is_left; PerimeterPosition position; }; std::vector points_to_insert; points_to_insert.reserve(result.size() * 2); for (size_t seg_idx = 0; seg_idx < result.size(); ++seg_idx) { const WeakModifierSegment &seg = result[seg_idx]; // A whole-perimeter zone has no boundaries to insert. if (seg.whole_perimeter) continue; points_to_insert.push_back({seg_idx, true, seg.left_position}); points_to_insert.push_back({seg_idx, false, seg.right_position}); } // Descending order preserves pending source indices; vertex zero is canonical (0, 0). // Only insertion events are reordered: modifier priority in result remains unchanged. std::sort(points_to_insert.begin(), points_to_insert.end(), [](const PointToInsert &a, const PointToInsert &b) { if (a.position.edge_index != b.position.edge_index) return a.position.edge_index > b.position.edge_index; return a.position.parameter > b.position.parameter; }); for (const PointToInsert &pt : points_to_insert) { WeakModifierSegment &seg = result[pt.segment_idx]; Point &point_coords = pt.is_left ? seg.left_point : seg.right_point; // Take the final coordinates: a boundary close to an existing vertex snaps to it. point_coords = insert_point_into_perimeter(point_coords, pt.position.edge_index, polygon).first; } // Add helpers at boundary vertices, scanning original vertices backwards so insertions do not shift // pending indices. for (size_t poly_idx = polygon.size(); poly_idx-- > 0; ) { bool refine_before = false; bool refine_after = false; for (const WeakModifierSegment &segment : result) { // Its placeholder points must not match a real vertex. if (segment.whole_perimeter) continue; refine_before |= polygon[poly_idx] == segment.left_point; refine_after |= polygon[poly_idx] == segment.right_point; } // Insert after first: inserting before would shift the current vertex index. if (refine_after) refine_at_vertex(poly_idx, +1, polygon); if (refine_before) refine_at_vertex(poly_idx, -1, polygon); } // Split edges in enforced zones into segments ≤ enforcer_oversampling_distance // Determine type pattern for each polygon edge (sequential application of hierarchy) std::vector edge_types(polygon.size(), EnforcedBlockedSeamPoint::Neutral); // Every boundary was inserted or snapped above and later steps only add vertices, so it is found. // A linear scan is fine: there are only a few zones per perimeter. auto find_point_index = [&](const Point &pt) -> size_t { const auto it = std::find(polygon.points.begin(), polygon.points.end(), pt); assert(it != polygon.points.end()); return size_t(it - polygon.points.begin()); }; // Last write wins: zones are sorted lowest priority first. for (const auto &segment : result) { // A whole-perimeter zone types every edge, so Enforced subdivides the whole perimeter, as // painting it green all round would. if (segment.whole_perimeter) { std::fill(edge_types.begin(), edge_types.end(), segment.type); continue; } const size_t left_idx = find_point_index(segment.left_point); const size_t right_idx = find_point_index(segment.right_point); // Practically unreachable, but a miss would write past edge_types in Release builds: skip the zone. if (left_idx == polygon.size() || right_idx == polygon.size()) { BOOST_LOG_TRIVIAL(error) << "PreciseSeam: weak boundary not found in perimeter, skipping zone"; continue; } // A zone covers edges [left_idx, right_idx): the edge from the right boundary is outside and must // not be subdivided. Edge types only drive subdivision; candidates are typed per point later. for (size_t idx = left_idx; idx != right_idx; idx = (idx + 1) % polygon.size()) edge_types[idx] = segment.type; } // Boundaries and helpers are already in place; without enforced edges nothing is subdivided, // so skip rebuilding the polygon. if (std::find(edge_types.begin(), edge_types.end(), EnforcedBlockedSeamPoint::Enforced) == edge_types.end()) return result; // Split enforced edges into small segments const double STEP_SCALED = scale_(SeamPlacer::enforcer_oversampling_distance); // Collect new list of polygon points with split enforced edges Points new_points; new_points.reserve(polygon.size() * 10); // Approximate estimate for (size_t i = 0; i < polygon.size(); ++i) { size_t next_i = (i + 1) % polygon.size(); Point p_start = polygon[i]; Point p_end = polygon[next_i]; // Add current vertex new_points.push_back(p_start); // Check edge type [i, next_i] if (edge_types[i] != EnforcedBlockedSeamPoint::Enforced) { continue; // Not enforced - don't split } // Calculate subdivision parameters Vec2d edge_vec = (p_end - p_start).cast(); double edge_length = edge_vec.norm(); if (edge_length <= STEP_SCALED) { continue; // Edge too short - don't split } // Number of segments: ceil(L / S) size_t num_segments = static_cast(std::ceil(edge_length / STEP_SCALED)); // Uniform step: L / num_segments (each segment ≤ S) double actual_step = edge_length / num_segments; Vec2d step_vec = edge_vec.normalized() * actual_step; // Add intermediate points incrementally Vec2d current_pos = p_start.cast(); for (size_t j = 1; j < num_segments; ++j) { current_pos += step_vec; new_points.push_back(current_pos.cast()); } } // Replace polygon points with new ones (with split enforced edges) polygon.points = std::move(new_points); return result; } // Collect every modifier's ready segments before insertions can shift source edge indices. std::vector collect_weak_modifier_segments( const std::vector &weak_volumes, Polygon &polygon, const PreparedPerimeter &prepared, const Layer *layer, const ModifierRegionsCache &slices_cache, PreciseSeamWarnings* warnings) { assert(&prepared.polygon == &polygon); std::vector result; if (weak_volumes.empty() || layer == nullptr || !prepared.valid) return result; // Modifier cache indices exclude raft layers, unlike Layer::id(). const size_t raft_layers = layer->object()->slicing_parameters().raft_layers(); const size_t layer_id = layer->id() - raft_layers; for (const ModelVolume *modifier_volume : weak_volumes) { const auto it = slices_cache.find(modifier_volume); if (it == slices_cache.end() || layer_id >= it->second.size()) continue; const ExtractionContext context{layer, modifier_volume, warnings}; const SegmentExtraction extracted = extract_perimeter_segments(prepared, it->second[layer_id], modifier_volume->type(), context); record_extraction(warnings, modifier_volume, extracted); const auto type = convert_weak_modifier_type(modifier_volume->type()); // Full containment works like seam painting: Enforced and Neutral type the whole perimeter; // Blocked is skipped with a warning, since the seam cannot avoid the whole perimeter. if (extracted.full_containment) { if (type == EnforcedBlockedSeamPoint::Blocked) { if (warnings) PreciseSeamWarnings::mark(warnings->full_containment, modifier_volume->type()); continue; } WeakModifierSegment whole{type, Point(), PerimeterPosition{0, 0.}, Point(), PerimeterPosition{0, 0.}}; whole.whole_perimeter = true; result.push_back(whole); continue; } for (const PerimeterSegment &segment : extracted.segments) { result.push_back({type, segment.polyline.points.front(), segment.begin, segment.polyline.points.back(), segment.end}); } } // Boundary insertion, refinement and last-write-wins priority use the established path. return prepare_weak_modifier_segments(std::move(result), polygon); } void apply_weak_modifiers_to_perimeter( const std::vector &weak_segments, PrintObjectSeamData::LayerSeams &result, const SeamPlacerImpl::Perimeter &perimeter, bool &some_point_enforced) { // Get z-coordinate for unscaling boundary points const float z_coord = result.points[perimeter.start_index].position.z(); const size_t perimeter_size = perimeter.end_index - perimeter.start_index; // Helper lambda: search for point index in result.points by unscaled coordinates. // Linear scan is intentional — O(N×M) is acceptable for typical M ≤ 5 weak segments. auto find_point_index = [&](const Point &pt) -> std::optional { Vec2f unscaled_pt = unscale(pt).cast(); Vec3f target(unscaled_pt.x(), unscaled_pt.y(), z_coord); for (size_t i = perimeter.start_index; i < perimeter.end_index; ++i) { if (result.points[i].position == target) return i - perimeter.start_index; } return std::nullopt; }; // Apply weak modifiers sequentially: sorted low-priority-first, // so higher-priority modifiers (higher in object tree) overwrite via last-write-wins. for (size_t seg_idx = 0; seg_idx < weak_segments.size(); ++seg_idx) { const auto &segment = weak_segments[seg_idx]; // A whole-perimeter zone types every candidate, including painting's oversampled points. if (segment.whole_perimeter) { for (size_t i = perimeter.start_index; i < perimeter.end_index; ++i) result.points[i].type = segment.type; if (segment.type == EnforcedBlockedSeamPoint::Enforced) some_point_enforced = true; continue; } // Find boundary point indices in result.points std::optional left_idx = find_point_index(segment.left_point); std::optional right_idx = find_point_index(segment.right_point); if (!left_idx.has_value() || !right_idx.has_value()) { BOOST_LOG_TRIVIAL(error) << "PreciseSeam: boundary point not found in perimeter, skipping segment"; continue; } // Apply modifier type to range [left_idx, right_idx] with wraparound for (size_t idx = left_idx.value(); ; idx = (idx + 1) % perimeter_size) { result.points[perimeter.start_index + idx].type = segment.type; if (idx == right_idx.value()) break; } if (segment.type == EnforcedBlockedSeamPoint::Enforced) { some_point_enforced = true; } } } // Restore precise seam positions that may have been modified void restore_precise_seam_positions(std::vector &layers) { using SeamPlacerImpl::SeamCandidate; using SeamPlacerImpl::Perimeter; tbb::parallel_for(tbb::blocked_range(0, layers.size()), [&layers](tbb::blocked_range r) { for (size_t layer_idx = r.begin(); layer_idx < r.end(); ++layer_idx) { std::vector &layer_perimeter_points = layers[layer_idx].points; // Iterate over perimeters (jump by end_index) for (size_t current = 0; current < layer_perimeter_points.size(); current = layer_perimeter_points[current].perimeter.end_index) { Perimeter &perimeter = layer_perimeter_points[current].perimeter; if (perimeter.precise_seam_point.has_value()) { perimeter.final_seam_position = perimeter.precise_seam_point.value(); perimeter.seam_index = perimeter.precise_seam_index; } } } }); } } // namespace PreciseSeam } // namespace Slic3r