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 "<name>" of "<object>"
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 <ian.bassi@outlook.com>
This commit is contained in:
Damir Galeev
2026-10-13 16:38:08 -03:00
committed by GitHub
co-authored by Ian Bassi
parent 008ee792f6
commit 5a427f2eb6
6 changed files with 243 additions and 104 deletions
+38 -22
View File
@@ -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: <causes>. 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 "<name>" of "<object>" 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 "<name>" of "<object>" 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
+10
View File
@@ -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 ""
+23 -19
View File
@@ -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<WeakModifierSegment> 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<WeakModifierSegment> &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
+20 -10
View File
@@ -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<unsigned> multiple_intersections{0}; // Center/Left/Right with several segments on a perimeter.
std::atomic<unsigned> full_containment{0}; // Skipped for a perimeter fully inside: Center/Left/Right, Blocked.
std::atomic<unsigned> full_containment{0}; // Skipped for a perimeter fully inside: Center/Left/Right.
std::atomic<unsigned> failed_types{0}; // Types with at least one discarded fragment.
std::atomic<size_t> 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<size_t> 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<bool> checked{false}; // Extracted on at least one perimeter.
std::atomic<bool> reached{false}; // Gave a segment, full containment or a discarded fragment.
std::atomic<bool> checked{false}; // Extracted on at least one perimeter.
std::atomic<bool> reached{false}; // Gave a segment, full containment or a discarded fragment.
std::atomic<bool> fully_blocked{false}; // Seam Blocked whole zone in effect on a perimeter left Blocked.
};
std::unordered_map<const ModelVolume*, ModifierUsage> 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<unsigned> &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<bool> &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<WeakModifierSegment> 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<WeakModifierSegment> &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
+41 -20
View File
@@ -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<void(void)> 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<const ModelVolume*> 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<const ModelVolume*> 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<const ModelVolume*> 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<const ModelVolume*> 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
+111 -33
View File
@@ -105,7 +105,8 @@ void require_vertex(const Polygon &polygon, const Point &point)
std::vector<SeamPlacerImpl::EnforcedBlockedSeamPoint> weak_candidate_types(
const Polygon &polygon, const std::vector<PreciseSeam::WeakModifierSegment> &segments,
const std::function<SeamPlacerImpl::EnforcedBlockedSeamPoint(const Point &)> &painted = {})
const std::function<SeamPlacerImpl::EnforcedBlockedSeamPoint(const Point &)> &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<SeamPlacerImpl::EnforcedBlockedSeamPoint> 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<SeamPlacerImpl::EnforcedBlockedSeamPoint> 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<const ModelVolume*> 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<std::string>{}, "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;