Precise Seam: remove known limitations and rework perimeter intersection (#16072)

Co-authored-by: Ian Bassi <ian.bassi@outlook.com>
This commit is contained in:
Damir Galeev
2026-10-04 14:47:56 -03:00
committed by GitHub
co-authored by Ian Bassi
parent 73a4ff9b16
commit b6d11b2b3a
27 changed files with 3931 additions and 1416 deletions
+1
View File
@@ -260,6 +260,7 @@ set(lisbslic3r_sources
GCode/SeamPlacer.hpp
GCode/PreciseSeam.cpp
GCode/PreciseSeam.hpp
GCode/PreciseSeamInternal.hpp
#GCodeSender.cpp
#GCodeSender.hpp
GCode/SmallAreaInfillFlowCompensator.cpp
+5
View File
@@ -3888,6 +3888,11 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
// Collect custom seam data from all objects.
std::function<void(void)> throw_if_canceled_func = [&print]() { print.throw_if_canceled(); };
m_seam_placer.init(print, throw_if_canceled_func);
// Precise Seam: init() only prepares its warning; issue it here, inside the active export step.
if (!m_seam_placer.precise_seam_warning().empty())
print.active_step_add_warning(PrintStateBase::WarningLevel::NON_CRITICAL,
m_seam_placer.precise_seam_warning(),
PrintStateBase::SlicingPreciseSeamWarning);
// BBS: get path for change filament
if (m_writer.multiple_extruders) {
File diff suppressed because it is too large Load Diff
+127 -82
View File
@@ -2,47 +2,22 @@
#define slic3r_PreciseSeam_hpp_
#include <atomic>
#include <cassert>
#include <cstddef>
#include <optional>
#include <vector>
#include <unordered_map>
#include <utility>
#include "libslic3r/BoundingBox.hpp"
#include "libslic3r/ExPolygon.hpp"
#include "libslic3r/Point.hpp"
#include "libslic3r/Polygon.hpp"
#include "libslic3r/Polyline.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/Layer.hpp"
#include "libslic3r/Print.hpp"
#include "libslic3r/ClipperUtils.hpp"
#include "SeamPlacer.hpp"
// CURRENT STATUS:
// Strong modifiers (Center/Left/Right): only one intersection per perimeter is supported,
// since there can be only one seam. Additional intersections are ignored.
//
// Weak modifiers (Enforced/Blocked/Neutral): multiple intersections are supported,
// but none of them should pass through the model entirely. A through-body intersection
// produces multiple segments, of which only one will be processed.
//
// In both cases, a pop-up warning is shown when unsupported intersections are detected.
//
// If any modifier has a multiply-connected cross-section (e.g. a hollow shape),
// it is skipped and a corresponding notification is shown.
//
// Full containment of the perimeter within modifier is not handled.
//
// FUTURE DIRECTION:
// A lightweight algorithm is needed to detect and handle through-body intersections
// for Weak modifiers. The algorithm must not slow down the 99.9% common case.
// Possible approach: if intersection passes the diff check (no through-body),
// use the current fast algorithm. If diff check fails, fall back to a heavier
// method: compute midpoints of intersection polygon edges, then check which
// midpoints lie strictly inside the modifier (not on boundary) using
// point_in_polygon. Those edges originate from the perimeter; the rest
// originate from the modifier boundary. Collect perimeter edges into a polyline.
// Additionally, multiply-connected cross-sections could be supported instead of
// being skipped entirely (e.g. by decomposing them into simple polygons).
// For Enforced and Neutral weak modifiers, full containment of the perimeter
// within modifier could be handled (currently ignored).
// Precise Seam: helper volumes that decide where the seam goes on external perimeters.
// Design: docs/HLSD/precise-seam.md
namespace Slic3r {
namespace PreciseSeam {
@@ -50,88 +25,158 @@ namespace PreciseSeam {
// Import EnforcedBlockedSeamPoint from SeamPlacerImpl namespace for convenience
using SeamPlacerImpl::EnforcedBlockedSeamPoint;
// Pre-sliced modifier cache: ModelVolume pointer → per-layer Polygons.
// Built once in SeamPlacer::init(), then passed read-only into per-perimeter functions.
using ModifierSlicesCache = std::unordered_map<const ModelVolume*, std::vector<Polygons>>;
// Geometry and its exterior bounds are prepared together, then treated as read-only.
struct ModifierRegion {
ExPolygon polygon;
BoundingBox bounds;
// Warning flags set during Precise Seam processing (thread-safe)
explicit ModifierRegion(ExPolygon region)
: polygon(std::move(region)), bounds(polygon.contour.points) {}
};
using ModifierRegions = std::vector<ModifierRegion>;
using ModifierSlices = std::vector<ModifierRegions>;
// Per-volume slices with cached bounds, shared read-only by both modifier kinds.
using ModifierRegionsCache = std::unordered_map<const ModelVolume*, ModifierSlices>;
// Move sliced geometry into the cache without detaching holes or changing layer indices.
ModifierRegions prepare_modifier_regions(ExPolygons regions);
ModifierSlices prepare_modifier_slices(std::vector<ExPolygons> slices);
// Bound diagnostic volume only; every failed or recovered fragment is still counted and handled.
// The same limit applies separately to failure and recovery markers.
inline constexpr size_t failed_fragment_log_limit = 10;
// Shared by all layers and objects in one SeamPlacer::init(); a new pass starts fresh.
struct PreciseSeamWarnings {
std::atomic<bool> multiple_intersections{false}; // modifier intersects perimeter in multiple separate places (strong only)
std::atomic<bool> through_body{false}; // modifier passes through the model body entirely
std::atomic<bool> multiply_connected{false}; // modifier has holes (multiply-connected cross-section)
std::atomic<bool> full_containment{false}; // modifier fully contains perimeter, no intersection edges
// 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> 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.
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::unordered_map<const ModelVolume*, ModifierUsage> modifier_usage;
// Bit of a Precise Seam type in the masks above, in menu order (Center is bit 0).
static unsigned type_bit(ModelVolumeType type)
{
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.
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);
}
};
// Result of finding common segment between perimeter and intersection
struct SegmentData {
Polyline segment; // Points from intersection_polygon forming the segment
std::vector<size_t> perimeter_edge_indices; // edge_index for each point in segment
// Optional caller identity for concise diagnostics when an intersection is discarded.
struct ExtractionContext {
const Layer *layer = nullptr;
const ModelVolume *modifier = nullptr;
PreciseSeamWarnings *warnings = nullptr;
};
// Borrows the source polygon; use only until insertion/refinement changes that polygon.
struct PreparedPerimeter {
const Polygon &polygon;
BoundingBox bounds;
Polyline line;
bool valid = false;
explicit PreparedPerimeter(const Polygon &perimeter);
};
// A vertex is represented by its outgoing edge and parameter zero, including vertex 0.
struct PerimeterPosition {
size_t edge_index;
double parameter;
};
// Prepared against the immutable perimeter, before insertion shifts its edge indices.
struct StrongSeamTarget {
Point point;
size_t edge_index;
};
struct PerimeterSegment {
Polyline polyline;
// One bound source edge per polyline interval.
std::vector<size_t> edge_indices;
PerimeterPosition begin;
PerimeterPosition end;
std::optional<StrongSeamTarget> strong_target; // Absent for weak modifiers and full containment.
// Scaled arc length, calculated only for Center or comparison of multiple strong segments.
double length = 0.; // Zero means unmeasured for weak, full containment, and a single Left/Right segment.
};
struct SegmentExtraction {
std::vector<PerimeterSegment> segments;
bool full_containment = false;
bool valid = true; // Invalid perimeter input; discarded fragments do not invalidate other segments.
// Clipped fragments whose binding failed; one segment may consist of several fragments.
size_t discarded_fragments = 0; // Failed bindings are ignored, with a warning and diagnostic marker.
};
// Clips a prepared, unchanged perimeter (>= 3 vertices, no consecutive duplicates, either direction)
// against a modifier's regions and returns its segments; strong targets are prepared only where needed.
SegmentExtraction extract_perimeter_segments(const PreparedPerimeter &prepared, const ModifierRegions &modifier,
ModelVolumeType mode, const ExtractionContext &context = {});
// Result of weak modifier segment processing
struct WeakModifierSegment {
EnforcedBlockedSeamPoint type; // Enforced/Blocked/Neutral
Point left_point; // Coordinates of left (first) point of segment
size_t left_idx; // Perimeter vertex index for left_point
PerimeterPosition left_position; // Position on the source perimeter before insertion/refinement.
Point right_point; // Coordinates of right (last) point of segment
size_t right_idx; // Perimeter vertex index for right_point
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).
bool whole_perimeter = false;
};
// Initialize Precise Seam data by populating provided vectors and flag
// Collects precise seam modifiers and fills output parameters
// Call once during SeamPlacer::init() before gather_seam_candidates()
// Parameters:
// strong_volumes_out - output vector for strong modifiers (CENTER/LEFT/RIGHT)
// weak_volumes_out - output vector for weak modifiers (ENFORCED/BLOCKED/NEUTRAL)
// has_strong_out - output flag indicating presence of strong modifiers
// model_object - model object containing volumes
// Collects the object's Precise Seam volumes: strong ones in priority order, weak ones in application
// order. Call once per object in SeamPlacer::init() before gathering candidates.
void init_precise_seam_data(
std::vector<const ModelVolume*>& strong_volumes_out,
std::vector<const ModelVolume*>& weak_volumes_out,
bool& has_strong_out,
const ModelObject* model_object);
// Insert strong seam point into perimeter polygon
// Processes strong modifiers (CENTER/LEFT/RIGHT) and inserts seam point into polygon
// Parameters:
// strong_volumes - list of strong precise seam modifiers
// polygon - perimeter polygon (will be modified if point inserted)
// layer - current layer
// slices_cache - pre-sliced modifier polygons (built once in SeamPlacer::init)
// Returns:
// Coordinates of inserted point (internal units) or std::nullopt if nothing inserted
// Inserts the seam point of the first strong modifier with a usable segment on this perimeter and returns
// it, or nullopt. `prepared` must describe `polygon` before any change.
std::optional<Point> insert_strong_seam_point(
const std::vector<const ModelVolume*> &strong_volumes,
Polygon &polygon,
const PreparedPerimeter &prepared,
const Layer *layer,
const ModifierSlicesCache &slices_cache,
const ModifierRegionsCache &slices_cache,
PreciseSeamWarnings* warnings = nullptr);
// Collect all weak modifier segments for a perimeter polygon
// Processes weak modifiers (ENFORCED/BLOCKED/NEUTRAL) and collects segment boundaries
// Also inserts boundary points into the perimeter polygon (sorted by descending arc length)
// Refines enforced edges by subdividing them into segments ≤ enforcer_oversampling_distance
// Parameters:
// weak_volumes - list of weak precise seam modifiers
// polygon - perimeter polygon (will be modified with inserted points and refined edges)
// layer - current layer
// slices_cache - pre-sliced modifier polygons (built once in SeamPlacer::init)
// Returns:
// Ordered vector of segments with updated coordinates (same order as weak_volumes list)
// Collects weak zones, inserts their boundaries into `polygon` and subdivides enforced edges. Pass
// modifiers lowest priority first; `prepared` must describe the unchanged `polygon`.
std::vector<WeakModifierSegment> collect_weak_modifier_segments(
const std::vector<const ModelVolume*> &weak_volumes,
Polygon &polygon,
const PreparedPerimeter &prepared,
const Layer *layer,
const ModifierSlicesCache &slices_cache,
const ModifierRegionsCache &slices_cache,
PreciseSeamWarnings* warnings = nullptr);
// Apply weak modifier types to perimeter points based on segment boundaries
// Finds boundary points in refined polygon and sets types for points within segments
// Parameters:
// weak_segments - segments with boundary coordinates and types
// result - layer seams data to modify
// perimeter - perimeter info (start/end indices)
// some_point_enforced - flag to update if Enforced points are set
// Retypes the candidates inside each zone in the given order (pass zones lowest priority first);
// sets some_point_enforced when an Enforced zone applies.
void apply_weak_modifiers_to_perimeter(
const std::vector<WeakModifierSegment> &weak_segments,
PrintObjectSeamData::LayerSeams &result,
@@ -0,0 +1,42 @@
#pragma once
#include "PreciseSeam.hpp"
#include "libslic3r/Point.hpp"
#include "libslic3r/Polygon.hpp"
#include "libslic3r/Polyline.hpp"
#include <cstddef>
#include <vector>
namespace Slic3r::PreciseSeam::detail {
// Binding intermediates retain source edge identity until segment assembly.
struct ClippedEdgeInterval {
size_t edge;
double begin;
double end;
Point first;
Point last;
};
struct FragmentBindingFailure {
size_t pair_index = 0;
const char *reason = "empty fragment";
};
// Failure rolls back this fragment only; earlier bindings remain intact.
bool append_projected_fragment(const Polyline &fragment, const Polygon &perimeter,
std::vector<ClippedEdgeInterval> &intervals,
FragmentBindingFailure &failure);
// Exact path, then projection path, without the fallback; intervals are unchanged on failure.
// Exposed so tests can show that a fragment needs the fallback in append_fragment().
bool bind_fragment(const Polyline &fragment, const Polygon &perimeter,
std::vector<ClippedEdgeInterval> &intervals, FragmentBindingFailure &failure);
// Binds one fragment. After a failure it tries the rare-case repair and the contact rule, both logged
// as recoveries. Returns false when the fragment is discarded.
bool append_fragment(const Polyline &fragment, const Polygon &perimeter,
std::vector<ClippedEdgeInterval> &intervals,
const ExtractionContext &context, size_t fragment_index);
} // namespace Slic3r::PreciseSeam::detail
+102 -40
View File
@@ -17,6 +17,7 @@
#include "tbb/parallel_reduce.h"
#include <atomic>
#include <boost/log/trivial.hpp>
#include <boost/format.hpp>
#include <cmath>
#include <cstdlib>
#include <cstddef>
@@ -334,10 +335,8 @@ struct GlobalModelInfo {
// Precise Seam modifiers: weak modifiers (ENFORCED/BLOCKED/NEUTRAL) provide hints for seam placement
std::vector<const ModelVolume*> precise_seam_weak_volumes;
// Pre-sliced modifier polygons, keyed by ModelVolume pointer.
// Populated once in SeamPlacer::init() to avoid re-slicing on every perimeter.
// Each value is a per-layer vector of Polygons for that modifier volume.
std::unordered_map<const ModelVolume*, std::vector<Polygons>> precise_seam_slices;
// Slice each modifier once; both consumers share structured regions and source provenance.
PreciseSeam::ModifierRegionsCache precise_seam_slices;
bool is_enforced(const Vec3f &position, float radius) const {
if (enforcers.empty()) {
@@ -518,22 +517,26 @@ void process_perimeter_polygon(const Polygon &orig_polygon, float z_coord, const
const auto& strong_volumes = global_model_info.precise_seam_strong_volumes;
const auto& weak_volumes = global_model_info.precise_seam_weak_volumes;
// Use pre-sliced cache from global_model_info instead of re-slicing on every call
auto seam_point = PreciseSeam::insert_strong_seam_point(strong_volumes, polygon, layer, global_model_info.precise_seam_slices, warnings);
std::optional<Point> seam_point;
std::vector<PreciseSeam::WeakModifierSegment> weak_segments;
if (layer != nullptr && (!strong_volumes.empty() || !weak_volumes.empty())) {
// Share validation, bounds and clipping line across all modifiers while the polygon is unchanged.
// A strong insertion ends processing; otherwise weak reads the same preparation before inserting.
const PreciseSeam::PreparedPerimeter prepared(polygon);
seam_point = PreciseSeam::insert_strong_seam_point(
strong_volumes, polygon, prepared, layer, global_model_info.precise_seam_slices, warnings);
if (!seam_point.has_value())
weak_segments = PreciseSeam::collect_weak_modifier_segments(
weak_volumes, polygon, prepared, layer, global_model_info.precise_seam_slices, warnings);
}
// Store the inserted point position for marking as central_enforcer later
// Store the inserted point position for marking as central_enforcer later.
std::optional<Vec3f> inserted_seam_position;
if (seam_point.has_value()) {
Vec2f unscaled_p = unscale(seam_point.value()).cast<float>();
inserted_seam_position = Vec3f(unscaled_p.x(), unscaled_p.y(), z_coord);
}
// Process weak modifiers (ENFORCED/BLOCKED/NEUTRAL) only if no strong modifier was inserted
std::vector<PreciseSeam::WeakModifierSegment> weak_segments;
if (!inserted_seam_position.has_value()) {
weak_segments = PreciseSeam::collect_weak_modifier_segments(weak_volumes, polygon, layer, global_model_info.precise_seam_slices, warnings);
}
float angle_arm_len = region != nullptr ? region->flow(FlowRole::frExternalPerimeter).nozzle_diameter() : 0.5f;
std::vector<float> lengths { };
@@ -627,7 +630,8 @@ void process_perimeter_polygon(const Polygon &orig_polygon, float z_coord, const
patches_starts_ends.push_back(next_index(i));
}
}
//if patches_starts_ends are empty, it means that the whole perimeter is enforced.. don't do anything in that case
// If patches_starts_ends are empty, the whole perimeter is enforced, or no point is enforced any more
// (Precise Seam weak zones retyped every painted enforcer); don't do anything in either case.
if (!patches_starts_ends.empty()) {
//if the first point in the patches is not enforced, it marks a patch end. in that case, put it to the end and start on next
// to simplify the processing
@@ -795,6 +799,12 @@ void gather_enforcers_blockers(GlobalModelInfo &result, const PrintObject *po) {
auto obj_transform = po->trafo_centered();
for (const ModelVolume *mv : po->model_object()->volumes) {
// Collect painting only from model parts (what the gizmo edits) and negative volumes (the only way
// to paint a hole's wall); painting left on modifiers and helpers after a type change is ignored.
// TODO: painting on negative volumes still affects the seam, but the gizmo neither shows nor edits it;
// making it editable also needs model_custom_seam_data_changed() to track it.
if (!mv->is_model_part() && !mv->is_negative_volume())
continue;
if (mv->is_seam_painted()) {
auto model_transformation = obj_transform * mv->get_matrix();
@@ -1508,9 +1518,10 @@ void SeamPlacer::align_seam_points(const PrintObject *po, const SeamPlacerImpl::
}
void SeamPlacer::init(Print &print, std::function<void(void)> throw_if_canceled_func) {
void SeamPlacer::init(const Print &print, std::function<void(void)> throw_if_canceled_func) {
using namespace SeamPlacerImpl;
m_seam_per_object.clear();
m_precise_seam_warning.clear();
// Warning flags for Precise Seam processing — shared across all objects
PreciseSeam::PreciseSeamWarnings precise_seam_warnings;
@@ -1529,13 +1540,17 @@ void SeamPlacer::init(Print &print, std::function<void(void)> throw_if_canceled_
m_seam_per_object[po].has_precise_seam_strong_volumes,
po->model_object());
// Pre-slice all precise seam modifier volumes once per object.
// Without this cache, slice_single_volume() would be called for every
// modifier × every perimeter × every layer — thousands of redundant slicing operations.
// Slice each Precise Seam modifier once per object; both consumers read the cache.
for (const ModelVolume* vol : global_model_info.precise_seam_strong_volumes)
global_model_info.precise_seam_slices[vol] = po->slice_single_volume(vol);
global_model_info.precise_seam_slices[vol] = PreciseSeam::prepare_modifier_slices(po->slice_single_volume_regions(vol));
for (const ModelVolume* vol : global_model_info.precise_seam_weak_volumes)
global_model_info.precise_seam_slices[vol] = po->slice_single_volume(vol);
global_model_info.precise_seam_slices[vol] = PreciseSeam::prepare_modifier_slices(po->slice_single_volume_regions(vol));
// Register usage tracking before the parallel phase; workers only set its flags. Several print
// objects of one model object share volumes, and try_emplace keeps what earlier ones recorded.
for (const ModelVolume* vol : global_model_info.precise_seam_strong_volumes)
precise_seam_warnings.modifier_usage.try_emplace(vol);
for (const ModelVolume* vol : global_model_info.precise_seam_weak_volumes)
precise_seam_warnings.modifier_usage.try_emplace(vol);
throw_if_canceled_func();
if (configured_seam_preference == spAligned || configured_seam_preference == spNearest || configured_seam_preference == spAlignedBack) {
@@ -1603,24 +1618,74 @@ void SeamPlacer::init(Print &print, std::function<void(void)> throw_if_canceled_
#endif
}
// Show Precise Seam warnings (once for all objects).
// Only ONE active_step_add_warning() call — multiple calls generate multiple UI events,
// each re-pushing ALL current warnings via Plater handler, causing NotificationManager::append()
// to duplicate text within each popup.
// Prepare one combined Precise Seam warning; G-code export issues it. Keep it single: separate
// warnings would each re-push all warnings and duplicate text in the notification.
{
const bool mi = precise_seam_warnings.multiple_intersections.load(std::memory_order_relaxed);
const bool tb = precise_seam_warnings.through_body.load(std::memory_order_relaxed);
const bool mc = precise_seam_warnings.multiply_connected.load(std::memory_order_relaxed);
const bool fc = precise_seam_warnings.full_containment.load(std::memory_order_relaxed);
const unsigned failed_types = precise_seam_warnings.failed_types.load(std::memory_order_relaxed);
const unsigned mi = precise_seam_warnings.multiple_intersections.load(std::memory_order_relaxed);
const unsigned fc = precise_seam_warnings.full_containment.load(std::memory_order_relaxed);
const size_t failed = precise_seam_warnings.failed_fragments.load(std::memory_order_relaxed);
// All workers have finished; cancellation before this point may omit the summary.
if (failed > PreciseSeam::failed_fragment_log_limit)
BOOST_LOG_TRIVIAL(warning) << "[PreciseSeamIntersectionFailed] " << failed
<< " fragments discarded; first " << PreciseSeam::failed_fragment_log_limit
<< " logged (parallel processing order), " << (failed - PreciseSeam::failed_fragment_log_limit)
<< " omitted";
// Recoveries are log-only: no user warning, but the same bounded detail and a total.
const size_t recovered = precise_seam_warnings.recovered_fragments.load(std::memory_order_relaxed);
if (recovered > PreciseSeam::failed_fragment_log_limit)
BOOST_LOG_TRIVIAL(warning) << "[PreciseSeamFragmentRecovered] " << recovered
<< " fragments recovered; first " << PreciseSeam::failed_fragment_log_limit
<< " logged (parallel processing order), " << (recovered - PreciseSeam::failed_fragment_log_limit)
<< " omitted";
// Reasons name the modifier types, as the menu does, not individual modifiers: "Seam Left, Seam
// Enforced" in menu order, each type once. The same msgids as the menu share its translations.
const auto type_list = [](unsigned mask) {
const std::pair<ModelVolumeType, std::string> types[] = {
{ModelVolumeType::PRECISE_SEAM_CENTER, _u8L("Seam Center")},
{ModelVolumeType::PRECISE_SEAM_LEFT, _u8L("Seam Left")},
{ModelVolumeType::PRECISE_SEAM_RIGHT, _u8L("Seam Right")},
{ModelVolumeType::PRECISE_SEAM_ENFORCED, _u8L("Seam Enforced")},
{ModelVolumeType::PRECISE_SEAM_BLOCKED, _u8L("Seam Blocked")},
{ModelVolumeType::PRECISE_SEAM_NEUTRAL, _u8L("Seam Neutral")}};
std::string list;
for (const auto &[type, name] : types)
if (mask & PreciseSeam::PreciseSeamWarnings::type_bit(type))
list += (list.empty() ? "" : ", ") + name;
return list;
};
std::vector<std::string> parts;
if (mi)
parts.push_back(_u8L("multiple intersections with a perimeter detected"));
if (tb)
parts.push_back(_u8L("modifier fully crosses the printable perimeter"));
if (mc)
parts.push_back(_u8L("modifier shape is not solid (has holes inside) and was ignored"));
if (fc)
parts.push_back(_u8L("perimeter is fully contained inside modifier and was ignored"));
if (failed_types != 0)
parts.push_back((boost::format(_u8L("failed to process some intersections (%1%)")) % type_list(failed_types)).str());
if (mi != 0)
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);
}
// 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
<< "\" modifier=\"" << volume->name << "\"";
if (!no_effect.empty()) {
const ModelVolume *first = no_effect.front();
if (no_effect.size() == 1)
parts.push_back((boost::format(_u8L("modifier \"%1%\" of \"%2%\" had no effect on the seam (it might not reach the centerline of the printed perimeter)"))
% first->name % first->get_object()->name).str());
else
parts.push_back((boost::format(_u8L("modifier \"%1%\" of \"%2%\" (%3% in total) had no effect on the seam (it might not reach the centerline of the printed perimeter)"))
% first->name % first->get_object()->name % no_effect.size()).str());
}
if (!parts.empty()) {
// One line: the export warnings dialog shows only the first line of each warning.
std::string warning_text = _u8L("Precise Seam") + ": ";
@@ -1630,10 +1695,7 @@ void SeamPlacer::init(Print &print, std::function<void(void)> throw_if_canceled_
}
warning_text += ". ";
warning_text += _u8L("Seam placement may differ from expected.");
print.active_step_add_warning(
PrintStateBase::WarningLevel::NON_CRITICAL,
warning_text,
PrintStateBase::SlicingPreciseSeamWarning);
m_precise_seam_warning = std::move(warning_text);
}
}
}
+8 -1
View File
@@ -10,6 +10,7 @@
#include <vector>
#include <memory>
#include <atomic>
#include <string>
#include "libslic3r/Point.hpp"
#include "libslic3r/libslic3r.h"
@@ -154,10 +155,16 @@ public:
//The following data structures hold all perimeter points for all PrintObject.
std::unordered_map<const PrintObject*, PrintObjectSeamData> m_seam_per_object;
void init(Print &print, std::function<void(void)> throw_if_canceled_func);
void init(const Print &print, std::function<void(void)> throw_if_canceled_func);
// Precise Seam user warning prepared by the last init(), empty if there is none. init() does not
// change the Print: the caller issues the warning where a print step is active (G-code export).
const std::string &precise_seam_warning() const { return m_precise_seam_warning; }
void place_seam(const Layer *layer, ExtrusionLoop &loop, const Point &last_pos, float& overhang) const;
private:
std::string m_precise_seam_warning;
void gather_seam_candidates(const PrintObject *po, const SeamPlacerImpl::GlobalModelInfo &global_model_info,
PreciseSeam::PreciseSeamWarnings* warnings = nullptr);
void calculate_candidates_visibility(const PrintObject *po,
+2 -6
View File
@@ -1272,7 +1272,6 @@ ModelObject& ModelObject::assign_copy(const ModelObject &rhs)
this->volumes.emplace_back(new ModelVolume(*model_volume));
this->volumes.back()->set_model_object(this);
}
this->clear_instances();
this->instances.reserve(rhs.instances.size());
for (const ModelInstance *model_instance : rhs.instances) {
@@ -1311,7 +1310,6 @@ ModelObject& ModelObject::assign_copy(ModelObject &&rhs)
rhs.volumes.clear();
for (ModelVolume *model_volume : this->volumes)
model_volume->set_model_object(this);
this->clear_instances();
this->instances = std::move(rhs.instances);
rhs.instances.clear();
@@ -1435,9 +1433,7 @@ ModelVolume* ModelObject::add_volume_with_shared_mesh(const ModelVolume &other,
void ModelObject::delete_volume(size_t idx)
{
ModelVolumePtrs::iterator i = this->volumes.begin() + idx;
ModelVolume* volume_to_delete = *i;
delete volume_to_delete;
delete *i;
this->volumes.erase(i);
if (this->volumes.size() == 1)
@@ -1530,6 +1526,7 @@ void ModelObject::sort_volumes(bool full_sort)
return vl_type < vr_type;
});
}
ModelInstance* ModelObject::add_instance()
{
ModelInstance* i = new ModelInstance(this);
@@ -3889,7 +3886,6 @@ bool model_volume_list_changed(const ModelObject &model_object_old, const ModelO
});
}
template< typename TypeFilterFn, typename CompareFn>
bool model_property_changed(const ModelObject &model_object_old, const ModelObject &model_object_new, TypeFilterFn type_filter, CompareFn compare)
{
+3 -4
View File
@@ -370,7 +370,6 @@ enum class ModelVolumeType : int {
};
// Free functions for checking ModelVolumeType without a ModelVolume object.
// Keep in sync with ModelVolume::is_precise_seam*() methods below.
inline bool is_precise_seam(ModelVolumeType t) { return t >= ModelVolumeType::PRECISE_SEAM_CENTER && t <= ModelVolumeType::PRECISE_SEAM_NEUTRAL; }
inline bool is_precise_seam_strong(ModelVolumeType t) { return t >= ModelVolumeType::PRECISE_SEAM_CENTER && t <= ModelVolumeType::PRECISE_SEAM_RIGHT; }
inline bool is_precise_seam_weak(ModelVolumeType t) { return t >= ModelVolumeType::PRECISE_SEAM_ENFORCED && t <= ModelVolumeType::PRECISE_SEAM_NEUTRAL; }
@@ -1007,13 +1006,13 @@ public:
bool is_support_blocker() const { return m_type == ModelVolumeType::SUPPORT_BLOCKER; }
bool is_support_modifier() const { return m_type == ModelVolumeType::SUPPORT_BLOCKER || m_type == ModelVolumeType::SUPPORT_ENFORCER; }
// Check if this volume is any of the precise seam modifier subtypes
bool is_precise_seam() const { return m_type >= ModelVolumeType::PRECISE_SEAM_CENTER && m_type <= ModelVolumeType::PRECISE_SEAM_NEUTRAL; }
bool is_precise_seam() const { return Slic3r::is_precise_seam(m_type); }
// Helper to check if volume is a "strong" Precise Seam type (center, left, right)
// Strong modifiers have priority and always appear above weak modifiers in UI
bool is_precise_seam_strong() const { return m_type >= ModelVolumeType::PRECISE_SEAM_CENTER && m_type <= ModelVolumeType::PRECISE_SEAM_RIGHT; }
bool is_precise_seam_strong() const { return Slic3r::is_precise_seam_strong(m_type); }
// Helper to check if volume is a "weak" Precise Seam type (enforced, blocked, neutral)
// Weak modifiers always appear below strong modifiers in UI
bool is_precise_seam_weak() const { return m_type >= ModelVolumeType::PRECISE_SEAM_ENFORCED && m_type <= ModelVolumeType::PRECISE_SEAM_NEUTRAL; }
bool is_precise_seam_weak() const { return Slic3r::is_precise_seam_weak(m_type); }
bool is_text() const { return text_configuration.has_value(); }
bool is_svg() const { return emboss_shape.has_value() && !text_configuration.has_value(); }
bool is_the_only_one_part() const; // behave like an object
+1 -10
View File
@@ -331,13 +331,10 @@ Points Polygon::concave_points(double angle_threshold) const
}
// Projection of a point onto the polygon.
Point Polygon::point_projection(const Point &point, size_t *edge_index) const
Point Polygon::point_projection(const Point &point) const
{
Point proj = point;
double dmin = std::numeric_limits<double>::max();
// Preserve the existing projection and tie order while optionally tracking its edge.
if (edge_index)
*edge_index = std::numeric_limits<size_t>::max();
if (! this->points.empty()) {
for (size_t i = 0; i < this->points.size(); ++ i) {
const Point &pt0 = this->points[i];
@@ -346,15 +343,11 @@ Point Polygon::point_projection(const Point &point, size_t *edge_index) const
if (d < dmin) {
dmin = d;
proj = pt0;
if (edge_index)
*edge_index = i;
}
d = (point - pt1).cast<double>().norm();
if (d < dmin) {
dmin = d;
proj = pt1;
if (edge_index)
*edge_index = (i + 1) % this->points.size();
}
Vec2d v1(coordf_t(pt1(0) - pt0(0)), coordf_t(pt1(1) - pt0(1)));
coordf_t div = v1.squaredNorm();
@@ -367,8 +360,6 @@ Point Polygon::point_projection(const Point &point, size_t *edge_index) const
if (d < dmin) {
dmin = d;
proj = foot;
if (edge_index)
*edge_index = i;
}
}
}
+1 -3
View File
@@ -92,9 +92,7 @@ public:
Points convex_points(double angle_threshold = 0.) const;
Points concave_points(double angle_threshold = 0.) const;
// Projection of a point onto the polygon.
// Optional index: start of the closest edge, or the vertex itself for an endpoint.
// Empty polygons return the query point and std::numeric_limits<size_t>::max() as the index.
Point point_projection(const Point &point, size_t *edge_index = nullptr) const;
Point point_projection(const Point &point) const;
std::vector<float> parameter_by_length() const;
//BBS
+2 -4
View File
@@ -492,10 +492,8 @@ public:
std::vector<Polygons> slice_support_volumes(const ModelVolumeType model_volume_type) const;
std::vector<Polygons> slice_support_blockers() const { return this->slice_support_volumes(ModelVolumeType::SUPPORT_BLOCKER); }
std::vector<Polygons> slice_support_enforcers() const { return this->slice_support_volumes(ModelVolumeType::SUPPORT_ENFORCER); }
// Shared slicing path; multiple volumes are united per layer.
std::vector<Polygons> slice_modifier_volumes(const std::vector<const ModelVolume*> &volumes) const;
// Keep Precise Seam volumes separate so their individual priority is preserved.
std::vector<Polygons> slice_single_volume(const ModelVolume* volume) const { return this->slice_modifier_volumes({volume}); }
// Preserve each connected region and its holes for perimeter clipping.
std::vector<ExPolygons> slice_single_volume_regions(const ModelVolume* volume) const;
// Helpers to project custom facets on slices
void project_and_append_custom_facets(bool seam, EnforcerBlockerType type, std::vector<Polygons>& expolys, std::vector<std::pair<Vec3f,Vec3f>>* vertical_points=nullptr) const;
+4 -1
View File
@@ -740,7 +740,10 @@ void print_objects_regions_invalidate_keep_some_volumes(PrintObjectRegions &prin
for (; i_old < old_volumes.size(); ++ i_old)
if (old_volumes[i_old]->id() >= new_volumes[i_new]->id())
break;
if (i_old != old_volumes.size() && old_volumes[i_old]->id() == new_volumes[i_new]->id()) {
// IDs survive type changes: an old volume that was not a solid or modifier was never cached,
// so treat it as new instead of looking it up.
if (i_old != old_volumes.size() && old_volumes[i_old]->id() == new_volumes[i_new]->id() &&
model_volume_solid_or_modifier(*old_volumes[i_old])) {
if (old_volumes[i_old]->get_matrix().isApprox(new_volumes[i_new]->get_matrix())) {
// Reuse the volume.
for (; print_object_regions.cached_volume_ids[i_cached_volume] < old_volumes[i_old]->id(); ++ i_cached_volume)
+35 -29
View File
@@ -1577,19 +1577,12 @@ ExPolygons PrintObject::_shrink_contour_holes(double contour_delta, double hole_
std::vector<Polygons> PrintObject::slice_support_volumes(const ModelVolumeType model_volume_type) const
{
// Supports merge every matching volume; Precise Seam calls the shared slicer one volume at a time.
std::vector<const ModelVolume*> volumes;
for (const ModelVolume *volume : this->model_object()->volumes)
if (volume->type() == model_volume_type)
volumes.push_back(volume);
return this->slice_modifier_volumes(volumes);
}
std::vector<Polygons> PrintObject::slice_modifier_volumes(const std::vector<const ModelVolume*> &volumes) const
{
auto it_volume = this->model_object()->volumes.begin();
auto it_volume_end = this->model_object()->volumes.end();
for (; it_volume != it_volume_end && (*it_volume)->type() != model_volume_type; ++ it_volume) ;
std::vector<Polygons> slices;
if (!volumes.empty()) {
// Share layer heights, transforms and cancellation handling across the selected volumes.
if (it_volume != it_volume_end) {
// Found at least a single support volume of model_volume_type.
std::vector<float> zs = zs_from_layers(this->layers());
std::vector<char> merge_layers;
bool merge = false;
@@ -1597,26 +1590,27 @@ std::vector<Polygons> PrintObject::slice_modifier_volumes(const std::vector<cons
auto throw_on_cancel_callback = std::function<void()>([print](){ print->throw_if_canceled(); });
MeshSlicingParamsEx params;
params.trafo = this->trafo_centered();
for (const ModelVolume *volume : volumes) {
std::vector<ExPolygons> slices2 = slice_volume(*volume, zs, params, throw_on_cancel_callback);
if (slices.empty()) {
slices.reserve(slices2.size());
for (ExPolygons &src : slices2)
slices.emplace_back(to_polygons(std::move(src)));
} else if (!slices2.empty()) {
if (merge_layers.empty())
merge_layers.assign(zs.size(), false);
for (size_t i = 0; i < zs.size(); ++ i) {
if (slices[i].empty())
slices[i] = to_polygons(std::move(slices2[i]));
else if (! slices2[i].empty()) {
append(slices[i], to_polygons(std::move(slices2[i])));
merge_layers[i] = true;
merge = true;
for (; it_volume != it_volume_end; ++ it_volume)
if ((*it_volume)->type() == model_volume_type) {
std::vector<ExPolygons> slices2 = slice_volume(*(*it_volume), zs, params, throw_on_cancel_callback);
if (slices.empty()) {
slices.reserve(slices2.size());
for (ExPolygons &src : slices2)
slices.emplace_back(to_polygons(std::move(src)));
} else if (!slices2.empty()) {
if (merge_layers.empty())
merge_layers.assign(zs.size(), false);
for (size_t i = 0; i < zs.size(); ++ i) {
if (slices[i].empty())
slices[i] = to_polygons(std::move(slices2[i]));
else if (! slices2[i].empty()) {
append(slices[i], to_polygons(std::move(slices2[i])));
merge_layers[i] = true;
merge = true;
}
}
}
}
}
if (merge) {
std::vector<Polygons*> to_merge;
to_merge.reserve(zs.size());
@@ -1634,4 +1628,16 @@ std::vector<Polygons> PrintObject::slice_modifier_volumes(const std::vector<cons
return slices;
}
std::vector<ExPolygons> PrintObject::slice_single_volume_regions(const ModelVolume* volume) const
{
if (volume == nullptr)
return {};
// Match the existing slicing heights and centered transform without flattening holes.
const std::vector<float> zs = zs_from_layers(this->layers());
MeshSlicingParamsEx params;
params.trafo = this->trafo_centered();
const Print *print = this->print();
return slice_volume(*volume, zs, params, [print]() { print->throw_if_canceled(); });
}
} // namespace Slic3r
+3 -1
View File
@@ -193,7 +193,9 @@ ColorRGBA GLVolume::SUPPORT_BLOCKER_COL = {1.0f, 0.3f, 0.3f, 0.4f};
ColorRGBA GLVolume::MODEL_HIDDEN_COL = {0.f, 0.f, 0.f, 0.3f};
// Precise Seam modifier colors
// Precise Seam modifier colors. Center, Left and Right are deliberately close shades of one orange:
// all three are strong modifiers, and distinct hues per mode would turn the scene into a rainbow.
// The object list icons tell the modes apart.
ColorRGBA GLVolume::PRECISE_SEAM_CENTER_COL = {1.0f, 0.627f, 0.082f, 0.6f}; // FFA015 - orange
ColorRGBA GLVolume::PRECISE_SEAM_LEFT_COL = {1.0f, 0.753f, 0.0f, 0.6f}; // FFC000 - golden
ColorRGBA GLVolume::PRECISE_SEAM_RIGHT_COL = {1.0f, 0.514f, 0.0f, 0.6f}; // FF8300 - dark orange
+1
View File
@@ -5804,6 +5804,7 @@ void ObjectList::change_part_type()
return;
}
#endif
ModelVolumeType ObjectList::get_selected_volume_type()
{
ModelVolume* volume = get_selected_model_volume();
-1
View File
@@ -98,7 +98,6 @@ struct MeshErrorsInfo
class ObjectList : public wxDataViewCtrl
{
public:
enum SELECTION_MODE
{
smUndef = 0,