From 5a427f2eb65a7758d20467c2be3d1996288ed4db Mon Sep 17 00:00:00 2001 From: Damir Galeev <49319928+DImagine@users.noreply.github.com> Date: Tue, 13 Oct 2026 22:38:08 +0300 Subject: [PATCH] Precise Seam: new full-coverage policy for Seam Blocked (#16170) * Precise Seam: new full-coverage policy for Seam Blocked A Seam Blocked modifier that covers a whole perimeter now blocks it, like a perimeter painted red all round, instead of being skipped. It overrides painting and lower-priority modifiers, like Enforced and Neutral. A new warning names the modifier: "Seam Blocked modifier "" of "" covers an entire perimeter". It is shown only when a perimeter stays entirely Blocked after all weak modifiers. The full-containment warning now covers only Center, Left and Right. * Precise Seam: fix seams on perimeters blocked all round - SeamComparator::is_first_not_much_worse() compares the types first, so two Blocked candidates fall through like Neutral ones. On a perimeter blocked all round, by a whole Seam Blocked modifier or by red painting, Random put the seam on the closing vertex of every layer and Aligned stopped aligning. - The whole-perimeter Blocked warning flags only the highest whole zone, the one in effect. A lower whole zone it overwrites is no longer reported, and the candidate scan runs only when that zone is Blocked. - The warning msgids take the type name from the menu's "Seam Blocked" msgid. - PreciseSeamWarnings::set() shares the load-before-store idiom, and every weak segment records its modifier. - Tests: a whole Blocked perimeter gets the seams of an unmarked one under every seam position; overwritten whole Blocked zones are not reported. --------- Co-authored-by: Ian Bassi --- docs/HLSD/precise-seam.md | 60 +++++++---- localization/i18n/OrcaSlicer.pot | 10 ++ src/libslic3r/GCode/PreciseSeam.cpp | 42 ++++---- src/libslic3r/GCode/PreciseSeam.hpp | 30 ++++-- src/libslic3r/GCode/SeamPlacer.cpp | 61 +++++++---- tests/fff_print/test_precise_seam.cpp | 144 ++++++++++++++++++++------ 6 files changed, 243 insertions(+), 104 deletions(-) diff --git a/docs/HLSD/precise-seam.md b/docs/HLSD/precise-seam.md index 8728f0518b..3168b1884e 100644 --- a/docs/HLSD/precise-seam.md +++ b/docs/HLSD/precise-seam.md @@ -82,9 +82,7 @@ there. strong point is placed, no later strong modifier and no weak modifier is processed for that perimeter. - **Weak:** every weak modifier applies. They are applied from the lowest - priority to the highest, so the highest one overwrites overlapping zones. A - Blocked modifier that fully contains a perimeter is the exception: it is - skipped there (see [Full containment](#full-containment)). + priority to the highest, so the highest one overwrites overlapping zones. A strong modifier without a usable segment, even one whose fragments were all discarded, passes the turn to the next one. @@ -232,20 +230,23 @@ begin and end positions on the source contour. ### Full containment A modifier that covers the whole perimeter has no boundaries on it. The policy -follows seam painting, where painting a whole perimeter green is a meaningful -choice and forbidding the seam all round is not: +follows seam painting, where a whole perimeter can be painted in either color: - **Seam Enforced** types the whole perimeter, like a perimeter painted green all round, with subdivision applied as described under [Weak modifiers](#weak-modifiers). - **Seam Neutral** types the whole perimeter Neutral, like an unmarked perimeter, clearing painting and lower zones. -- **Seam Blocked** is skipped for the perimeter, with the full-containment - warning. The seam cannot avoid the whole perimeter, so the modifier does not - override anything below it: lower zones and painting stay in effect. -- **Seam Center, Left and Right** are skipped with the same warning: there is no - intersection to place the point on. +- **Seam Blocked** types the whole perimeter Blocked, like a perimeter painted + red all round, overriding painting and lower zones. The seam cannot avoid such + a perimeter: the seam placer compares Blocked candidates with each other like + Neutral ones, so the seam is placed as on an unmarked perimeter. If the + perimeter stays Blocked after all weak zones, it is reported in the user + warning (see [Diagnostics and warnings](#diagnostics-and-warnings)), so the + user can check the modifier. +- **Seam Center, Left and Right** are skipped with the full-containment warning: + there is no intersection to place the point on. -Enforced and Neutral take part in the usual priority order (see +Enforced, Neutral and Blocked take part in the usual priority order (see [Weak modifiers](#weak-modifiers)). The perimeter is fully contained when the united intervals cover every source @@ -311,9 +312,9 @@ into every perimeter that has a strong seam. `collect_weak_modifier_segments()` extracts the segments of every weak modifier before the polygon is modified, so all positions refer to the same contour. Each segment becomes a zone with a type and two boundaries, kept in application -order, lowest priority first. Full containment of an Enforced or Neutral -modifier becomes a whole-perimeter zone at its place in that order: it has no -boundaries and takes part in no insertion or helper step below. The boundaries +order, lowest priority first. Full containment of a weak modifier becomes a +whole-perimeter zone at its place in that order: it has no boundaries and takes +part in no insertion or helper step below. The boundaries carry their positions on the source contour; these remain as provenance after insertion and are not indices into the modified polygon. @@ -384,9 +385,10 @@ G-code export issues it as one non-critical warning with the ID `SlicingPreciseSeamWarning`. It is a single line, "Precise Seam: . Seam placement may differ from expected.", because the export warnings dialog shows only the first line of each warning. Repeated warning events replace the -notification instead of appending to it. Except for the "had no effect" cause, -the causes name the modifier types involved, as the menu names them, in menu -order and each type once, for example "(Seam Left, Seam Enforced)". +notification instead of appending to it. Except for the "covers an entire +perimeter" and "had no effect" causes, the causes name the modifier types +involved, as the menu names them, in menu order and each type once, for example +"(Seam Left, Seam Enforced)". The causes are: - **failed to process some intersections (types):** at least one fragment was @@ -395,8 +397,18 @@ The causes are: a Seam Center, Left or Right modifier had more than one segment on a perimeter (see [Strong modifiers](#strong-modifiers)). - **a perimeter is fully inside a modifier, the modifier was not applied to it - (types):** a Seam Center, Left, Right or Blocked modifier was skipped for a + (types):** a Seam Center, Left or Right modifier was skipped for a perimeter (see [Full containment](#full-containment)). +- **Seam Blocked modifier "" of "" covers an entire perimeter:** + a Seam Blocked modifier covered a whole perimeter (see + [Full containment](#full-containment)). Only perimeters whose candidates are + all Blocked after all weak zones contribute to this warning, and only through + the zone in effect there: `apply_weak_modifiers_to_perimeter()` checks the + final types and flags the modifier of the highest whole-perimeter zone when it + is Blocked. A lower whole zone is overwritten by it and is not reported. Only + the first flagged modifier in print and volume order is named, followed by + "(N in total)" when there are several. The type name reuses the menu's + translation. - **modifier "" of "" had no effect on the seam (it might not reach the centerline of the printed perimeter):** a modifier was evaluated on at least one perimeter and never gave a segment, full containment or a discarded @@ -420,6 +432,8 @@ The log records the following diagnostic markers: - `[PreciseSeamNoEffect]` for every modifier of the "had no effect" cause, with the object and modifier names. Unlike the user warning, the log lists all of them. +- `[PreciseSeamFullyBlocked]` for every modifier of the "covers an entire + perimeter" cause, listed the same way. Failures and recoveries are counted separately. The first 10 of each per `init()` call are logged in detail, in parallel processing order; if a limit is @@ -563,12 +577,14 @@ last modifier would. priorities, weak boundaries that coincide or share an edge, enforced subdivision, whole-perimeter weak zones with painting and priorities, weak zones over painting's oversampled candidates, the warning type masks, usage - tracking for the "had no effect" warning, volume sorting of strong and weak - groups, restoration of strong points after alignment, raft layer indexing and - structured slices. End-to-end tests slice a real object with Precise Seam + tracking for the "had no effect" and whole-perimeter Blocked warnings, volume + sorting of strong and weak groups, restoration of strong points after + alignment, raft layer indexing and structured slices. End-to-end tests slice + a real object with Precise Seam volumes and check the outer wall starts in the exported G-code: every strong mode under several seam positions and with a raft, Enforced and Blocked zones, - a modifier with a hole, and the user warning. + a perimeter blocked all round under every seam position, a modifier with a + hole, and the user warning. - [Seam placer tests](../../tests/fff_print/test_seam_placer.cpp) cover enforced-patch selection independent of the contour start, fully painted contours, duplicate removal, and `Print::apply()` synchronization through type diff --git a/localization/i18n/OrcaSlicer.pot b/localization/i18n/OrcaSlicer.pot index 4163bd30a6..eb533cc49e 100644 --- a/localization/i18n/OrcaSlicer.pot +++ b/localization/i18n/OrcaSlicer.pot @@ -12665,6 +12665,16 @@ msgstr "" msgid "a perimeter is fully inside a modifier, the modifier was not applied to it (%1%)" msgstr "" +#. TRN: %1% is the modifier type "Seam Blocked" as in the menu, %2% the modifier name, %3% the object name. +#, possible-boost-format +msgid "%1% modifier \"%2%\" of \"%3%\" covers an entire perimeter" +msgstr "" + +#. TRN: %1% is the modifier type "Seam Blocked" as in the menu, %2% the modifier name, %3% the object name, %4% the number of modifiers. +#, possible-boost-format +msgid "%1% modifier \"%2%\" of \"%3%\" (%4% in total) covers an entire perimeter" +msgstr "" + #, possible-boost-format msgid "modifier \"%1%\" of \"%2%\" had no effect on the seam (it might not reach the centerline of the printed perimeter)" msgstr "" diff --git a/src/libslic3r/GCode/PreciseSeam.cpp b/src/libslic3r/GCode/PreciseSeam.cpp index 4cf7dc0978..de62fe1cfd 100644 --- a/src/libslic3r/GCode/PreciseSeam.cpp +++ b/src/libslic3r/GCode/PreciseSeam.cpp @@ -673,7 +673,7 @@ static void refine_at_vertex( } } -// Records one extraction for the warnings: the type of a modifier with a discarded fragment, and the +// Records one extraction for the warnings: the type of a modifier with a discarded fragment, 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) @@ -685,13 +685,10 @@ static void record_extraction(PreciseSeamWarnings *warnings, const ModelVolume * 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); + PreciseSeamWarnings::set(usage.checked); + if (!extracted.segments.empty() || extracted.full_containment || extracted.discarded_fragments > 0) + PreciseSeamWarnings::set(usage.reached); } // Strong priority is per modifier, never a global maximum across different modifiers. @@ -953,22 +950,15 @@ std::vector collect_weak_modifier_segments( 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. + // Full containment works like seam painting all round: the whole perimeter takes the type. + // A perimeter left whole Blocked also gets a warning (see apply_weak_modifiers_to_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); + result.push_back({type, Point(), PerimeterPosition{0, 0.}, Point(), PerimeterPosition{0, 0.}, true, modifier_volume}); continue; } for (const PerimeterSegment &segment : extracted.segments) { result.push_back({type, segment.polyline.points.front(), segment.begin, - segment.polyline.points.back(), segment.end}); + segment.polyline.points.back(), segment.end, false, modifier_volume}); } } // Boundary insertion, refinement and last-write-wins priority use the established path. @@ -979,7 +969,8 @@ void apply_weak_modifiers_to_perimeter( const std::vector &weak_segments, PrintObjectSeamData::LayerSeams &result, const SeamPlacerImpl::Perimeter &perimeter, - bool &some_point_enforced) + bool &some_point_enforced, + PreciseSeamWarnings *warnings) { // Get z-coordinate for unscaling boundary points const float z_coord = result.points[perimeter.start_index].position.z(); @@ -1029,6 +1020,19 @@ void apply_weak_modifiers_to_perimeter( some_point_enforced = true; } } + + // Only the highest whole zone is in effect, and a higher band may still unblock part of it. + if (warnings == nullptr) + return; + const auto last_whole = std::find_if(weak_segments.rbegin(), weak_segments.rend(), + [](const WeakModifierSegment &segment) { return segment.whole_perimeter; }); + if (last_whole == weak_segments.rend() || last_whole->type != EnforcedBlockedSeamPoint::Blocked) + return; + for (size_t i = perimeter.start_index; i < perimeter.end_index; ++i) + if (result.points[i].type != EnforcedBlockedSeamPoint::Blocked) + return; + if (const auto usage = warnings->modifier_usage.find(last_whole->modifier); usage != warnings->modifier_usage.end()) + PreciseSeamWarnings::set(usage->second.fully_blocked); } // Restore precise seam positions that may have been modified diff --git a/src/libslic3r/GCode/PreciseSeam.hpp b/src/libslic3r/GCode/PreciseSeam.hpp index f9e1b1ddf3..486af1cb2e 100644 --- a/src/libslic3r/GCode/PreciseSeam.hpp +++ b/src/libslic3r/GCode/PreciseSeam.hpp @@ -49,18 +49,20 @@ struct PreciseSeamWarnings { // Masks of the Precise Seam types that caused each warning reason, one bit per type (type_bit()). // The user warning lists the types instead of naming modifiers. std::atomic multiple_intersections{0}; // Center/Left/Right with several segments on a perimeter. - std::atomic full_containment{0}; // Skipped for a perimeter fully inside: Center/Left/Right, Blocked. + std::atomic full_containment{0}; // Skipped for a perimeter fully inside: Center/Left/Right. std::atomic failed_types{0}; // Types with at least one discarded fragment. std::atomic failed_fragments{0}; // Total discarded fragments, for the log summary. // Fragments saved by the rare-case fallback or accepted as contacts; log only, no user warning. // Clipper is deterministic, so a prismatic model can repeat the same case on every layer. std::atomic recovered_fragments{0}; - // Per-modifier flags for the "had no effect" warning. Modifiers are registered before the parallel - // phase, so workers only set flags; unregistered ones (e.g. in tests) are not tracked. + // Per-modifier flags for the "had no effect" and "covers an entire perimeter" warnings. Modifiers are + // registered before the parallel phase, so workers only set flags; unregistered ones (e.g. in tests) + // are not tracked. struct ModifierUsage { - std::atomic checked{false}; // Extracted on at least one perimeter. - std::atomic reached{false}; // Gave a segment, full containment or a discarded fragment. + std::atomic checked{false}; // Extracted on at least one perimeter. + std::atomic reached{false}; // Gave a segment, full containment or a discarded fragment. + std::atomic fully_blocked{false}; // Seam Blocked whole zone in effect on a perimeter left Blocked. }; std::unordered_map modifier_usage; @@ -70,13 +72,18 @@ struct PreciseSeamWarnings { assert(is_precise_seam(type)); return 1u << (int(type) - int(ModelVolumeType::PRECISE_SEAM_CENTER)); } - // Load before fetch_or: most calls find the bit already set, so shared cache lines stay clean. + // Load before writing: most calls find the bit or flag already set, so shared cache lines stay clean. static void mark(std::atomic &mask, ModelVolumeType type) { const unsigned bit = type_bit(type); if ((mask.load(std::memory_order_relaxed) & bit) == 0) mask.fetch_or(bit, std::memory_order_relaxed); } + static void set(std::atomic &flag) + { + if (!flag.load(std::memory_order_relaxed)) + flag.store(true, std::memory_order_relaxed); + } }; // Optional caller identity for concise diagnostics when an intersection is discarded. @@ -139,9 +146,10 @@ struct WeakModifierSegment { PerimeterPosition left_position; // Position on the source perimeter before insertion/refinement. Point right_point; // Coordinates of right (last) point of segment PerimeterPosition right_position; // Retained provenance, not an index into the modified polygon. - // Full containment of an Enforced or Neutral modifier: the zone is the whole perimeter, without - // boundaries (the points and positions above are unused and nothing is inserted for it). + // Full containment of a weak modifier: the zone is the whole perimeter, without boundaries + // (the points and positions above are unused and nothing is inserted for it). bool whole_perimeter = false; + const ModelVolume *modifier = nullptr; // Source modifier, for the warnings. }; // Collects the object's Precise Seam volumes: strong ones in priority order, weak ones in application @@ -173,12 +181,14 @@ std::vector collect_weak_modifier_segments( PreciseSeamWarnings* warnings = nullptr); // Retypes the candidates inside each zone in the given order (pass zones lowest priority first); -// sets some_point_enforced when an Enforced zone applies. +// sets some_point_enforced when an Enforced zone applies. When the whole perimeter ends up Blocked and +// the highest whole-perimeter zone is Blocked, flags that zone's modifier for the warning. void apply_weak_modifiers_to_perimeter( const std::vector &weak_segments, PrintObjectSeamData::LayerSeams &result, const SeamPlacerImpl::Perimeter &perimeter, - bool &some_point_enforced); + bool &some_point_enforced, + PreciseSeamWarnings *warnings = nullptr); // Restore precise seam positions that may have been modified by alignment // Iterates through all perimeters and restores precise_seam_point positions diff --git a/src/libslic3r/GCode/SeamPlacer.cpp b/src/libslic3r/GCode/SeamPlacer.cpp index 24fed0493c..932f8fb99e 100644 --- a/src/libslic3r/GCode/SeamPlacer.cpp +++ b/src/libslic3r/GCode/SeamPlacer.cpp @@ -608,7 +608,7 @@ void process_perimeter_polygon(const Polygon &orig_polygon, float z_coord, const // Apply weak modifiers if no strong modifier was inserted if (!inserted_seam_position.has_value() && !weak_segments.empty()) { PreciseSeam::apply_weak_modifiers_to_perimeter( - weak_segments, result, perimeter, some_point_enforced); + weak_segments, result, perimeter, some_point_enforced, warnings); } if (some_point_enforced) { @@ -896,18 +896,15 @@ struct SeamComparator { return a.central_enforcer; } - if (a.type == EnforcedBlockedSeamPoint::Enforced) { - return true; - } - - if (a.type == EnforcedBlockedSeamPoint::Blocked) { - return false; - } - if (a.type != b.type) { return a.type > b.type; } + // Two Blocked points fall through like Neutral ones, so a perimeter blocked all round still gets a proper seam. + if (a.type == EnforcedBlockedSeamPoint::Enforced) { + return true; + } + //avoid overhangs if ((a.overhang > 0.0f || b.overhang > 0.0f) && abs(a.overhang - b.overhang) > (0.1f * a.perimeter.flow_width)) { @@ -1660,18 +1657,42 @@ void SeamPlacer::init(const Print &print, std::function throw_if_can parts.push_back((boost::format(_u8L("multiple intersections with a perimeter, only one was used (%1%)")) % type_list(mi)).str()); if (fc != 0) parts.push_back((boost::format(_u8L("a perimeter is fully inside a modifier, the modifier was not applied to it (%1%)")) % type_list(fc)).str()); - // Modifiers evaluated somewhere that never reached a perimeter; never-evaluated ones are not reported. - // Print and volume order make the named one deterministic; the log lists them all. - std::vector no_effect; - for (const PrintObject *po : print.objects()) - for (const ModelVolume *volume : po->model_object()->volumes) { - const auto it = precise_seam_warnings.modifier_usage.find(volume); - if (it != precise_seam_warnings.modifier_usage.end() && - it->second.checked.load(std::memory_order_relaxed) && - !it->second.reached.load(std::memory_order_relaxed) && - std::find(no_effect.begin(), no_effect.end(), volume) == no_effect.end()) - no_effect.push_back(volume); + // Modifiers whose usage flags match, in print and volume order, so the named one is deterministic. + const auto collect_modifiers = [&print, &precise_seam_warnings](const auto &matches) { + std::vector found; + for (const PrintObject *po : print.objects()) + for (const ModelVolume *volume : po->model_object()->volumes) { + const auto it = precise_seam_warnings.modifier_usage.find(volume); + if (it != precise_seam_warnings.modifier_usage.end() && matches(it->second) && + std::find(found.begin(), found.end(), volume) == found.end()) + found.push_back(volume); + } + return found; + }; + // Seam Blocked modifiers whose whole perimeter stayed Blocked; the user warning names only the + // first one, the log lists them all. + const std::vector fully_blocked = collect_modifiers( + [](const PreciseSeam::PreciseSeamWarnings::ModifierUsage &usage) { return usage.fully_blocked.load(std::memory_order_relaxed); }); + for (const ModelVolume *volume : fully_blocked) + BOOST_LOG_TRIVIAL(warning) << "[PreciseSeamFullyBlocked] object=\"" << volume->get_object()->name + << "\" modifier=\"" << volume->name << "\""; + if (!fully_blocked.empty()) { + const ModelVolume *first = fully_blocked.front(); + if (fully_blocked.size() == 1) { + // TRN: %1% is the modifier type "Seam Blocked" as in the menu, %2% the modifier name, %3% the object name. + parts.push_back((boost::format(_u8L("%1% modifier \"%2%\" of \"%3%\" covers an entire perimeter")) + % _u8L("Seam Blocked") % first->name % first->get_object()->name).str()); + } else { + // TRN: %1% is the modifier type "Seam Blocked" as in the menu, %2% the modifier name, %3% the object name, %4% the number of modifiers. + parts.push_back((boost::format(_u8L("%1% modifier \"%2%\" of \"%3%\" (%4% in total) covers an entire perimeter")) + % _u8L("Seam Blocked") % first->name % first->get_object()->name % fully_blocked.size()).str()); } + } + // Modifiers evaluated somewhere that never reached a perimeter; never-evaluated ones are not reported. + const std::vector no_effect = collect_modifiers( + [](const PreciseSeam::PreciseSeamWarnings::ModifierUsage &usage) { + return usage.checked.load(std::memory_order_relaxed) && !usage.reached.load(std::memory_order_relaxed); + }); // The user warning names only the first one; the log lists them all. for (const ModelVolume *volume : no_effect) BOOST_LOG_TRIVIAL(warning) << "[PreciseSeamNoEffect] object=\"" << volume->get_object()->name diff --git a/tests/fff_print/test_precise_seam.cpp b/tests/fff_print/test_precise_seam.cpp index b5cb913904..78e2709afb 100644 --- a/tests/fff_print/test_precise_seam.cpp +++ b/tests/fff_print/test_precise_seam.cpp @@ -105,7 +105,8 @@ void require_vertex(const Polygon &polygon, const Point &point) std::vector weak_candidate_types( const Polygon &polygon, const std::vector &segments, - const std::function &painted = {}) + const std::function &painted = {}, + PreciseSeam::PreciseSeamWarnings *warnings = nullptr) { // Use the same coordinate conversion as production when applying prepared boundaries. // Optional painted types stand in for seam painting, which production assigns before weak zones. @@ -120,7 +121,7 @@ std::vector weak_candidate_types( } loop.end_index = candidates.points.size(); bool enforced = false; - PreciseSeam::apply_weak_modifiers_to_perimeter(segments, candidates, loop, enforced); + PreciseSeam::apply_weak_modifiers_to_perimeter(segments, candidates, loop, enforced, warnings); std::vector types; for (const auto &candidate : candidates.points) types.push_back(candidate.type); @@ -664,7 +665,7 @@ TEST_CASE("Weak priority and enforcement refinement apply on both crossed sides" check_square_boundary(perimeter); } -TEST_CASE("Weak full containment follows painting: Enforced and Neutral type the whole perimeter, Blocked is skipped", "[PreciseSeam]") +TEST_CASE("Weak full containment follows painting: the whole perimeter takes the zone's type", "[PreciseSeam]") { const auto type = GENERATE(ModelVolumeType::PRECISE_SEAM_ENFORCED, ModelVolumeType::PRECISE_SEAM_BLOCKED, ModelVolumeType::PRECISE_SEAM_NEUTRAL); @@ -674,6 +675,8 @@ TEST_CASE("Weak full containment follows painting: Enforced and Neutral type the const Points original = perimeter.points; const auto *modifier = fixture.add(type, {rectangle(-2, -2, 22, 22)}); PreciseSeam::PreciseSeamWarnings warnings; + // Registered as SeamPlacer::init() does, so the per-modifier warning flag is tracked. + warnings.modifier_usage.try_emplace(modifier); const auto segments = PreciseSeam::collect_weak_modifier_segments( {modifier}, perimeter, PreciseSeam::PreparedPerimeter(perimeter), fixture.layer, fixture.cache, &warnings); // Painting of both kinds, so that each type's effect on it is visible. @@ -681,23 +684,17 @@ TEST_CASE("Weak full containment follows painting: Enforced and Neutral type the return p.x() < scale_(10.) ? SeamPlacerImpl::EnforcedBlockedSeamPoint::Enforced : SeamPlacerImpl::EnforcedBlockedSeamPoint::Blocked; }; - const auto types = weak_candidate_types(perimeter, segments, painted); + const auto types = weak_candidate_types(perimeter, segments, painted, &warnings); CHECK_FALSE(warnings.multiple_intersections.load()); CHECK(warnings.failed_fragments.load() == 0); - if (type == ModelVolumeType::PRECISE_SEAM_BLOCKED) { - // Forbidding the seam all round cannot be honoured: skipped with a warning, painting stays. - CHECK(segments.empty()); - CHECK(perimeter.points == original); - CHECK(warnings.full_containment.load() == PreciseSeam::PreciseSeamWarnings::type_bit(type)); - for (size_t i = 0; i < perimeter.size(); ++i) - CHECK(types[i] == painted(perimeter[i])); - return; - } REQUIRE(segments.size() == 1); CHECK(segments.front().whole_perimeter); CHECK(warnings.full_containment.load() == 0); + // Only a whole Blocked perimeter is reported, as the seam cannot avoid it. + CHECK(warnings.modifier_usage.at(modifier).fully_blocked.load() == (type == ModelVolumeType::PRECISE_SEAM_BLOCKED)); // Every candidate takes the zone's type, overriding painting. const auto expected = type == ModelVolumeType::PRECISE_SEAM_ENFORCED ? SeamPlacerImpl::EnforcedBlockedSeamPoint::Enforced : + type == ModelVolumeType::PRECISE_SEAM_BLOCKED ? SeamPlacerImpl::EnforcedBlockedSeamPoint::Blocked : SeamPlacerImpl::EnforcedBlockedSeamPoint::Neutral; for (size_t i = 0; i < perimeter.size(); ++i) CHECK(types[i] == expected); @@ -706,29 +703,37 @@ TEST_CASE("Weak full containment follows painting: Enforced and Neutral type the CHECK(perimeter.size() >= size_t(80.f / SeamPlacer::enforcer_oversampling_distance)); check_square_boundary(perimeter); } else - CHECK(perimeter.points == original); // Like an unmarked perimeter: no subdivision. + CHECK(perimeter.points == original); // Like a perimeter unmarked or painted red all round: no subdivision. } TEST_CASE("Whole-perimeter weak zones take part in the usual priority order", "[PreciseSeam]") { // Volumes are listed low priority first; a later zone overwrites an earlier one. - const int scenario = GENERATE(0, 1, 2); + const int scenario = GENERATE(0, 1, 2, 3, 4, 5, 6); CAPTURE(scenario); SeamFixture fixture; const auto *whole_enforced = fixture.add(ModelVolumeType::PRECISE_SEAM_ENFORCED, {rectangle(-2, -2, 22, 22)}); const auto *whole_neutral = fixture.add(ModelVolumeType::PRECISE_SEAM_NEUTRAL, {rectangle(-2, -2, 22, 22)}); const auto *whole_blocked = fixture.add(ModelVolumeType::PRECISE_SEAM_BLOCKED, {rectangle(-2, -2, 22, 22)}); + const auto *lower_whole_blocked = fixture.add(ModelVolumeType::PRECISE_SEAM_BLOCKED, {rectangle(-2, -2, 22, 22)}); const auto *band_blocked = fixture.add(ModelVolumeType::PRECISE_SEAM_BLOCKED, {rectangle(8, -2, 12, 2)}); const auto *band_enforced = fixture.add(ModelVolumeType::PRECISE_SEAM_ENFORCED, {rectangle(8, -2, 12, 2)}); std::vector volumes; if (scenario == 0) volumes = {whole_enforced, band_blocked}; // A band forbidden inside a green perimeter. if (scenario == 1) volumes = {band_enforced, whole_neutral}; // A whole Neutral clears the lower band. - if (scenario == 2) volumes = {band_enforced, whole_blocked}; // A whole Blocked is skipped; the band stays. + if (scenario == 2) volumes = {band_enforced, whole_blocked}; // A whole Blocked overrides the lower band. + if (scenario == 3) volumes = {whole_blocked, band_enforced}; // A higher band unblocks part of a whole Blocked. + if (scenario == 4) volumes = {whole_blocked, band_blocked}; // A higher Blocked band keeps it whole Blocked. + if (scenario == 5) volumes = {lower_whole_blocked, whole_enforced, whole_blocked}; // The lower one is overwritten. + if (scenario == 6) volumes = {lower_whole_blocked, whole_blocked}; // Only the higher one is in effect. Polygon perimeter = rectangle(0, 0, 20, 20); PreciseSeam::PreciseSeamWarnings warnings; + // Registered as SeamPlacer::init() does, so the per-modifier warning flag is tracked. + warnings.modifier_usage.try_emplace(whole_blocked); + warnings.modifier_usage.try_emplace(lower_whole_blocked); const auto segments = PreciseSeam::collect_weak_modifier_segments( volumes, perimeter, PreciseSeam::PreparedPerimeter(perimeter), fixture.layer, fixture.cache, &warnings); - const auto types = weak_candidate_types(perimeter, segments); + const auto types = weak_candidate_types(perimeter, segments, {}, &warnings); using Type = SeamPlacerImpl::EnforcedBlockedSeamPoint; for (size_t i = 0; i < perimeter.size(); ++i) { const Point &p = perimeter[i]; @@ -736,14 +741,38 @@ TEST_CASE("Whole-perimeter weak zones take part in the usual priority order", "[ const bool in_band = p.y() == 0 && p.x() >= scale_(8.) && p.x() <= scale_(12.); const Type expected = scenario == 0 ? (in_band ? Type::Blocked : Type::Enforced) : scenario == 1 ? Type::Neutral : - (in_band ? Type::Enforced : Type::Neutral); + scenario == 3 ? (in_band ? Type::Enforced : Type::Blocked) : + Type::Blocked; CHECK(types[i] == expected); } - CHECK(warnings.full_containment.load() == - (scenario == 2 ? PreciseSeam::PreciseSeamWarnings::type_bit(ModelVolumeType::PRECISE_SEAM_BLOCKED) : 0u)); + CHECK(warnings.full_containment.load() == 0); + // The warning follows the final typing: only a perimeter left whole Blocked is reported, through the + // highest whole zone. + const bool expected_fully_blocked = scenario == 2 || scenario == 4 || scenario == 5 || scenario == 6; + CHECK(warnings.modifier_usage.at(whole_blocked).fully_blocked.load() == expected_fully_blocked); + CHECK_FALSE(warnings.modifier_usage.at(lower_whole_blocked).fully_blocked.load()); check_square_boundary(perimeter); } +TEST_CASE("A partial Blocked zone on a perimeter painted red all round gives no whole-perimeter warning", "[PreciseSeam]") +{ + SeamFixture fixture; + const auto *band_blocked = fixture.add(ModelVolumeType::PRECISE_SEAM_BLOCKED, {rectangle(8, -2, 12, 2)}); + Polygon perimeter = rectangle(0, 0, 20, 20); + PreciseSeam::PreciseSeamWarnings warnings; + warnings.modifier_usage.try_emplace(band_blocked); + const auto segments = PreciseSeam::collect_weak_modifier_segments( + {band_blocked}, perimeter, PreciseSeam::PreparedPerimeter(perimeter), fixture.layer, fixture.cache, &warnings); + REQUIRE(segments.size() == 1); + // Red painting makes every candidate Blocked, so the check reaches the whole-zone filter. + const auto painted = [](const Point &) { return SeamPlacerImpl::EnforcedBlockedSeamPoint::Blocked; }; + const auto types = weak_candidate_types(perimeter, segments, painted, &warnings); + for (const auto type : types) + CHECK(type == SeamPlacerImpl::EnforcedBlockedSeamPoint::Blocked); + // Only a modifier that covers the whole perimeter is reported. + CHECK_FALSE(warnings.modifier_usage.at(band_blocked).fully_blocked.load()); +} + TEST_CASE("Coverage short by a gap under 1 um is full containment while a narrow band keeps its point", "[PreciseSeam][Regression]") { // A tip pokes through the bottom edge at x = 10 mm by `depth` nm, so the perimeter line crosses it @@ -829,19 +858,15 @@ TEST_CASE("A modifier touching the perimeter at one point keeps the full contain else { const auto segments = PreciseSeam::collect_weak_modifier_segments( {modifier}, perimeter, PreciseSeam::PreparedPerimeter(perimeter), fixture.layer, fixture.cache, &warnings); - // Enforced and Neutral type the whole perimeter; Blocked is skipped. - if (type == ModelVolumeType::PRECISE_SEAM_BLOCKED) - CHECK(segments.empty()); - else { - REQUIRE(segments.size() == 1); - CHECK(segments.front().whole_perimeter); - } + // A weak modifier gives its type to the whole perimeter. + REQUIRE(segments.size() == 1); + CHECK(segments.front().whole_perimeter); } // Only a whole-perimeter Enforced zone subdivides the edges. if (type != ModelVolumeType::PRECISE_SEAM_ENFORCED) CHECK(perimeter.points == original); - const bool skipped = is_precise_seam_strong(type) || type == ModelVolumeType::PRECISE_SEAM_BLOCKED; - CHECK(warnings.full_containment.load() == (skipped ? PreciseSeam::PreciseSeamWarnings::type_bit(type) : 0u)); + // Only strong types are skipped with the full-containment warning. + CHECK(warnings.full_containment.load() == (is_precise_seam_strong(type) ? PreciseSeam::PreciseSeamWarnings::type_bit(type) : 0u)); CHECK_FALSE(warnings.multiple_intersections.load()); CHECK(warnings.failed_fragments.load() == 0); } @@ -1176,7 +1201,7 @@ TEST_CASE("Modifier usage marks evaluated modifiers that never reach a perimeter CHECK(checked(short_weak)); CHECK_FALSE(reached(short_weak)); CHECK(reached(crossing_weak)); - // Full containment reached the perimeter even though it is skipped. + // Full containment counts as reaching the perimeter. CHECK(reached(containing_weak)); // Strong: evaluation stops at the first modifier with a segment. The one ending 0.5 mm before the @@ -1393,6 +1418,24 @@ TEST_CASE("Weak modifiers move the exported seam into or out of their zone", "[P CHECK(fixture.warning().empty()); } +TEST_CASE("A higher-priority Enforced band suppresses the whole-perimeter Blocked export warning", "[PreciseSeam][Regression]") +{ + ExportFixture fixture("back"); + // The earlier model volume has higher priority; weak application reverses this order. + fixture.add(ModelVolumeType::PRECISE_SEAM_ENFORCED, make_cube(2, 6, 4), Vec3d(4, -3, -1), "front enforcer"); + fixture.add(ModelVolumeType::PRECISE_SEAM_BLOCKED, make_cube(30, 30, 4), Vec3d(-5, -5, -1), "whole blocker"); + const auto loops = outer_wall_loops(fixture.export_gcode()); + REQUIRE(loops.size() == 10); + for (const OuterLoop &loop : loops) { + CAPTURE(loop.seam.x(), loop.seam.y()); + // The band wins on every layer, so the empty warning is not from modifiers that did nothing. + CHECK_THAT(loop.seam.y(), Catch::Matchers::WithinAbs(loop.bounds.min.y() - loop.bounds.center().y(), 0.01)); + CHECK(loop.seam.x() >= -6.01); + CHECK(loop.seam.x() <= -3.99); + } + CHECK(fixture.warning().empty()); +} + TEST_CASE("A strong modifier with a hole uses the left of two equal crossings", "[PreciseSeam]") { // The hole splits the front face crossing into x = 2..4 and x = 8..10 mm. The lengths tie, both @@ -1422,14 +1465,49 @@ TEST_CASE("Full containment warns for Blocked but not for Enforced", "[PreciseSe { const auto type = GENERATE(ModelVolumeType::PRECISE_SEAM_ENFORCED, ModelVolumeType::PRECISE_SEAM_BLOCKED); ExportFixture fixture("back"); - fixture.add(type, make_cube(30, 30, 4), Vec3d(-5, -5, -1)); - fixture.export_gcode(); + fixture.add(type, make_cube(30, 30, 4), Vec3d(-5, -5, -1), "whole helper"); + const auto loops = outer_wall_loops(fixture.export_gcode()); + // The whole perimeter takes the type either way, so the seam still follows the back preference. + REQUIRE(loops.size() == 10); + for (const OuterLoop &loop : loops) + CHECK(loop.seam.y() > 7.); if (type == ModelVolumeType::PRECISE_SEAM_BLOCKED) - CHECK(fixture.warning().find("Seam Blocked") != std::string::npos); + CHECK(fixture.warning().find("Seam Blocked modifier \"whole helper\" of") != std::string::npos); else CHECK(fixture.warning().empty()); } +TEST_CASE("A perimeter blocked all round gets the seam of an unmarked one", "[PreciseSeam][Regression]") +{ + // Equal Blocked candidates compare like Neutral ones, so random and aligned seams still vary and align. + const std::string seam_position = GENERATE(as{}, "random", "aligned", "aligned_back", "back", "nearest"); + CAPTURE(seam_position); + ExportFixture unmarked(seam_position); + const auto expected = outer_wall_loops(unmarked.export_gcode()); + ExportFixture blocked(seam_position); + blocked.add(ModelVolumeType::PRECISE_SEAM_BLOCKED, make_cube(30, 30, 4), Vec3d(-5, -5, -1)); + const auto loops = outer_wall_loops(blocked.export_gcode()); + REQUIRE(loops.size() == expected.size()); + for (size_t i = 0; i < loops.size(); ++i) { + CAPTURE(i, loops[i].seam.x(), loops[i].seam.y(), expected[i].seam.x(), expected[i].seam.y()); + CHECK((loops[i].seam - expected[i].seam).norm() < 0.01); + } +} + +TEST_CASE("The whole-perimeter Blocked warning names the first modifier and counts them all", "[PreciseSeam]") +{ + ExportFixture fixture("back"); + // Separate layers: where both covered the same perimeter, only the higher-priority one would count. + fixture.add(ModelVolumeType::PRECISE_SEAM_BLOCKED, make_cube(30, 30, 1.4), Vec3d(-5, -5, -1), "first blocker"); + fixture.add(ModelVolumeType::PRECISE_SEAM_BLOCKED, make_cube(30, 30, 2), Vec3d(-5, -5, 1), "second blocker"); + fixture.export_gcode(); + const std::string warning = fixture.warning(); + CAPTURE(warning); + CHECK(warning.find("Seam Blocked modifier \"first blocker\" of") != std::string::npos); + CHECK(warning.find("(2 in total) covers an entire perimeter") != std::string::npos); + CHECK(warning.find("second blocker") == std::string::npos); +} + TEST_CASE("Belt printers slice Precise Seam modifiers in the frame the object was sliced in", "[PreciseSeam][belt]") { Model model;