mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-14 11:21:18 +00:00
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:
co-authored by
Ian Bassi
parent
008ee792f6
commit
5a427f2eb6
+38
-22
@@ -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
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user