mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-14 19:31:14 +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
@@ -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