diff --git a/src/libslic3r/Algorithm/LineSegmentation/LineSegmentation.cpp b/src/libslic3r/Algorithm/LineSegmentation/LineSegmentation.cpp new file mode 100644 index 0000000000..4b41a624c9 --- /dev/null +++ b/src/libslic3r/Algorithm/LineSegmentation/LineSegmentation.cpp @@ -0,0 +1,574 @@ +#include +#include +#include + +#include "clipper/clipper_z.hpp" +#include "libslic3r/Arachne/utils/ExtrusionLine.hpp" +#include "libslic3r/Arachne/utils/ExtrusionJunction.hpp" +#include "libslic3r/ClipperZUtils.hpp" +#include "libslic3r/ExPolygon.hpp" +#include "libslic3r/PerimeterGenerator.hpp" +#include "libslic3r/Point.hpp" +#include "libslic3r/Polygon.hpp" +#include "libslic3r/Polyline.hpp" +#include "libslic3r/Print.hpp" +#include "libslic3r/libslic3r.h" + +#include "LineSegmentation.hpp" + +namespace Slic3r::Algorithm::LineSegmentation { + +const constexpr coord_t POINT_IS_ON_LINE_THRESHOLD_SQR = Slic3r::sqr(scaled(EPSILON)); + +struct ZAttributes +{ + bool is_clip_point = false; + bool is_new_point = false; + uint32_t point_index = 0; + + ZAttributes() = default; + + explicit ZAttributes(const uint32_t clipper_coord) : + is_clip_point((clipper_coord >> 31) & 0x1), is_new_point((clipper_coord >> 30) & 0x1), point_index(clipper_coord & 0x3FFFFFFF) {} + + explicit ZAttributes(const ClipperLib_Z::IntPoint &clipper_pt) : ZAttributes(clipper_pt.z()) {} + + ZAttributes(const bool is_clip_point, const bool is_new_point, const uint32_t point_index) : + is_clip_point(is_clip_point), is_new_point(is_new_point), point_index(point_index) + { + assert(this->point_index < (1u << 30) && "point_index exceeds 30 bits!"); + } + + // Encode the structure to uint32_t. + constexpr uint32_t encode() const + { + assert(this->point_index < (1u << 30) && "point_index exceeds 30 bits!"); + return (this->is_clip_point << 31) | (this->is_new_point << 30) | (this->point_index & 0x3FFFFFFF); + } + + // Decode the uint32_t to the structure. + static ZAttributes decode(const uint32_t clipper_coord) + { + return { bool((clipper_coord >> 31) & 0x1), bool((clipper_coord >> 30) & 0x1), clipper_coord & 0x3FFFFFFF }; + } + + static ZAttributes decode(const ClipperLib_Z::IntPoint &clipper_pt) { return ZAttributes::decode(clipper_pt.z()); } +}; + +struct LineRegionRange +{ + size_t begin_idx; // Index of the line on which the region begins. + double begin_t; // Scalar position on the begin_idx line in which the region begins. The value is from range <0., 1.>. + size_t end_idx; // Index of the line on which the region ends. + double end_t; // Scalar position on the end_idx line in which the region ends. The value is from range <0., 1.>. + size_t clip_idx; // Index of clipping ExPolygons to identified which ExPolygons group contains this line. + + LineRegionRange(size_t begin_idx, double begin_t, size_t end_idx, double end_t, size_t clip_idx) + : begin_idx(begin_idx), begin_t(begin_t), end_idx(end_idx), end_t(end_t), clip_idx(clip_idx) {} + + // Check if 'other' overlaps with this LineRegionRange. + bool is_overlap(const LineRegionRange &other) const + { + if (this->end_idx < other.begin_idx || this->begin_idx > other.end_idx) { + return false; + } else if (this->end_idx == other.begin_idx && this->end_t <= other.begin_t) { + return false; + } else if (this->begin_idx == other.end_idx && this->begin_t >= other.end_t) { + return false; + } + + return true; + } + + // Check if 'inner' is whole inside this LineRegionRange. + bool is_inside(const LineRegionRange &inner) const + { + if (!this->is_overlap(inner)) { + return false; + } + + const bool starts_after = (this->begin_idx < inner.begin_idx) || (this->begin_idx == inner.begin_idx && this->begin_t <= inner.begin_t); + const bool ends_before = (this->end_idx > inner.end_idx) || (this->end_idx == inner.end_idx && this->end_t >= inner.end_t); + + return starts_after && ends_before; + } + + bool is_zero_length() const { return this->begin_idx == this->end_idx && this->begin_t == this->end_t; } + + bool operator<(const LineRegionRange &rhs) const + { + return this->begin_idx < rhs.begin_idx || (this->begin_idx == rhs.begin_idx && this->begin_t < rhs.begin_t); + } +}; + +using LineRegionRanges = std::vector; + +inline Point make_point(const ClipperLib_Z::IntPoint &clipper_pt) { return { clipper_pt.x(), clipper_pt.y() }; } + +inline ClipperLib_Z::Paths to_clip_zpaths(const ExPolygons &clips) { return ClipperZUtils::expolygons_to_zpaths_with_same_z(clips, coord_t(ZAttributes(true, false, 0).encode())); } + +static ClipperLib_Z::Path subject_to_zpath(const Points &subject, const bool is_closed) +{ + ZAttributes z_attributes(false, false, 0); + + ClipperLib_Z::Path out; + if (!subject.empty()) { + out.reserve((subject.size() + is_closed) ? 1 : 0); + for (const Point &p : subject) { + out.emplace_back(p.x(), p.y(), z_attributes.encode()); + ++z_attributes.point_index; + } + + if (is_closed) { + // If it is closed, then duplicate the first point at the end to make a closed path open. + out.emplace_back(subject.front().x(), subject.front().y(), z_attributes.encode()); + } + } + + return out; +} + +static ClipperLib_Z::Path subject_to_zpath(const Arachne::ExtrusionLine &subject) +{ + // Closed Arachne::ExtrusionLine already has duplicated the last point. + ZAttributes z_attributes(false, false, 0); + + ClipperLib_Z::Path out; + if (!subject.empty()) { + out.reserve(subject.size()); + for (const Arachne::ExtrusionJunction &junction : subject) { + out.emplace_back(junction.p.x(), junction.p.y(), z_attributes.encode()); + ++z_attributes.point_index; + } + } + + return out; +} + +static ClipperLib_Z::Path subject_to_zpath(const Polyline &subject) { return subject_to_zpath(subject.points, false); } + +[[maybe_unused]] static ClipperLib_Z::Path subject_to_zpath(const Polygon &subject) { return subject_to_zpath(subject.points, true); } + +struct ProjectionInfo +{ + double projected_t; + double distance_sqr; +}; + +static ProjectionInfo project_point_on_line(const Point &line_from_pt, const Point &line_to_pt, const Point &query_pt) +{ + const Vec2d line_vec = (line_to_pt - line_from_pt).template cast(); + const Vec2d query_vec = (query_pt - line_from_pt).template cast(); + const double line_length_sqr = line_vec.squaredNorm(); + + if (line_length_sqr <= 0.) { + return { std::numeric_limits::max(), std::numeric_limits::max() }; + } + + const double projected_t = query_vec.dot(line_vec); + const double projected_t_normalized = std::clamp(projected_t / line_length_sqr, 0., 1.); + // Projected point have to line on the line. + if (projected_t < 0. || projected_t > line_length_sqr) { + return { projected_t_normalized, std::numeric_limits::max() }; + } + + const Vec2d projected_vec = projected_t_normalized * line_vec; + const double distance_sqr = (projected_vec - query_vec).squaredNorm(); + + return { projected_t_normalized, distance_sqr }; +} + +static int32_t find_closest_line_to_point(const ClipperLib_Z::Path &subject, const ClipperLib_Z::IntPoint &query) +{ + auto it_min = subject.end(); + double distance_sqr_min = std::numeric_limits::max(); + + const Point query_pt = make_point(query); + Point prev_pt = make_point(subject.front()); + for (auto it_curr = std::next(subject.begin()); it_curr != subject.end(); ++it_curr) { + const Point curr_pt = make_point(*it_curr); + + const double distance_sqr = project_point_on_line(prev_pt, curr_pt, query_pt).distance_sqr; + if (distance_sqr <= POINT_IS_ON_LINE_THRESHOLD_SQR) { + return int32_t(std::distance(subject.begin(), std::prev(it_curr))); + } + + if (distance_sqr < distance_sqr_min) { + distance_sqr_min = distance_sqr; + it_min = std::prev(it_curr); + } + + prev_pt = curr_pt; + } + + if (it_min != subject.end()) { + return int32_t(std::distance(subject.begin(), it_min)); + } + + return -1; +} + +std::optional create_line_region_range(ClipperLib_Z::Path &&intersection, const ClipperLib_Z::Path &subject, const size_t region_idx) +{ + if (intersection.size() < 2) { + return std::nullopt; + } + + auto need_reverse = [&subject](const ClipperLib_Z::Path &intersection) -> bool { + for (size_t curr_idx = 1; curr_idx < intersection.size(); ++curr_idx) { + ZAttributes prev_z(intersection[curr_idx - 1]); + ZAttributes curr_z(intersection[curr_idx]); + + if (!prev_z.is_clip_point && !curr_z.is_clip_point) { + if (prev_z.point_index > curr_z.point_index) { + return true; + } else if (curr_z.point_index == prev_z.point_index) { + assert(curr_z.point_index < subject.size()); + const Point subject_pt = make_point(subject[curr_z.point_index]); + const Point prev_pt = make_point(intersection[curr_idx - 1]); + const Point curr_pt = make_point(intersection[curr_idx]); + + const double prev_dist = (prev_pt - subject_pt).cast().squaredNorm(); + const double curr_dist = (curr_pt - subject_pt).cast().squaredNorm(); + if (prev_dist > curr_dist) { + return true; + } + } + } + } + + return false; + }; + + for (ClipperLib_Z::IntPoint &clipper_pt : intersection) { + const ZAttributes clipper_pt_z(clipper_pt); + if (!clipper_pt_z.is_clip_point) { + continue; + } + + // FIXME @hejllukas: We could save searing for the source line in some cases using other intersection points, + // but in reality, the clip point will be inside the intersection in very rare cases. + if (int32_t subject_line_idx = find_closest_line_to_point(subject, clipper_pt); subject_line_idx != -1) { + clipper_pt.z() = coord_t(ZAttributes(false, true, subject_line_idx).encode()); + } + + assert(!ZAttributes(clipper_pt).is_clip_point); + if (ZAttributes(clipper_pt).is_clip_point) { + return std::nullopt; + } + } + + // Ensure that indices of source input are ordered in increasing order. + if (need_reverse(intersection)) { + std::reverse(intersection.begin(), intersection.end()); + } + + ZAttributes begin_z(intersection.front()); + ZAttributes end_z(intersection.back()); + + assert(begin_z.point_index <= subject.size() && end_z.point_index <= subject.size()); + const size_t begin_idx = begin_z.point_index; + const size_t end_idx = end_z.point_index; + const double begin_t = begin_z.is_new_point ? project_point_on_line(make_point(subject[begin_idx]), make_point(subject[begin_idx + 1]), make_point(intersection.front())).projected_t : 0.; + const double end_t = end_z.is_new_point ? project_point_on_line(make_point(subject[end_idx]), make_point(subject[end_idx + 1]), make_point(intersection.back())).projected_t : 0.; + + if (begin_t == std::numeric_limits::max() || end_t == std::numeric_limits::max()) { + return std::nullopt; + } + + return LineRegionRange{ begin_idx, begin_t, end_idx, end_t, region_idx }; +} + +LineRegionRanges intersection_with_region(const ClipperLib_Z::Path &subject, const ClipperLib_Z::Paths &clips, const size_t region_config_idx) +{ + ClipperLib_Z::Clipper clipper; + clipper.PreserveCollinear(true); // Especially with Arachne, we don't want to remove collinear edges. + clipper.ZFillFunction([](const ClipperLib_Z::IntPoint &e1bot, const ClipperLib_Z::IntPoint &e1top, + const ClipperLib_Z::IntPoint &e2bot, const ClipperLib_Z::IntPoint &e2top, + ClipperLib_Z::IntPoint &new_pt) { + const ZAttributes e1bot_z(e1bot), e1top_z(e1top), e2bot_z(e2bot), e2top_z(e2top); + + assert(e1bot_z.is_clip_point == e1top_z.is_clip_point); + assert(e2bot_z.is_clip_point == e2top_z.is_clip_point); + + if (!e1bot_z.is_clip_point && !e1top_z.is_clip_point) { + assert(e1bot_z.point_index + 1 == e1top_z.point_index || e1bot_z.point_index == e1top_z.point_index + 1); + new_pt.z() = coord_t(ZAttributes(false, true, std::min(e1bot_z.point_index, e1top_z.point_index)).encode()); + } else if (!e2bot_z.is_clip_point && !e2top_z.is_clip_point) { + assert(e2bot_z.point_index + 1 == e2top_z.point_index || e2bot_z.point_index == e2top_z.point_index + 1); + new_pt.z() = coord_t(ZAttributes(false, true, std::min(e2bot_z.point_index, e2top_z.point_index)).encode()); + } else { + assert(false && "At least one of the conditions above has to be met."); + } + }); + + clipper.AddPath(subject, ClipperLib_Z::ptSubject, false); + clipper.AddPaths(clips, ClipperLib_Z::ptClip, true); + + ClipperLib_Z::Paths intersections; + { + ClipperLib_Z::PolyTree clipped_polytree; + clipper.Execute(ClipperLib_Z::ctIntersection, clipped_polytree, ClipperLib_Z::pftNonZero, ClipperLib_Z::pftNonZero); + ClipperLib_Z::PolyTreeToPaths(std::move(clipped_polytree), intersections); + } + + LineRegionRanges line_region_ranges; + line_region_ranges.reserve(intersections.size()); + for (ClipperLib_Z::Path &intersection : intersections) { + if (std::optional region_range = create_line_region_range(std::move(intersection), subject, region_config_idx); region_range.has_value()) { + line_region_ranges.emplace_back(*region_range); + } + } + + return line_region_ranges; +} + +LineRegionRanges create_continues_line_region_ranges(LineRegionRanges &&line_region_ranges, const size_t default_clip_idx, const size_t total_lines_cnt) +{ + if (line_region_ranges.empty()) { + return line_region_ranges; + } + + std::sort(line_region_ranges.begin(), line_region_ranges.end()); + + // Resolve overlapping regions if it happens, but it should never happen. + for (size_t region_range_idx = 1; region_range_idx < line_region_ranges.size(); ++region_range_idx) { + LineRegionRange &prev_range = line_region_ranges[region_range_idx - 1]; + LineRegionRange &curr_range = line_region_ranges[region_range_idx]; + + assert(!prev_range.is_overlap(curr_range)); + if (prev_range.is_inside(curr_range)) { + // Make the previous range zero length to remove it later. + curr_range = prev_range; + prev_range.begin_idx = curr_range.begin_idx; + prev_range.begin_t = curr_range.begin_t; + prev_range.end_idx = curr_range.begin_idx; + prev_range.end_t = curr_range.begin_t; + } else if (prev_range.is_overlap(curr_range)) { + curr_range.begin_idx = prev_range.end_idx; + curr_range.begin_t = prev_range.end_t; + } + } + + // Fill all gaps between regions with the default region. + LineRegionRanges line_region_ranges_out; + size_t prev_line_idx = 0.; + double prev_t = 0.; + for (const LineRegionRange &curr_line_region : line_region_ranges) { + if (curr_line_region.is_zero_length()) { + continue; + } + + assert(prev_line_idx < curr_line_region.begin_idx || (prev_line_idx == curr_line_region.begin_idx && prev_t <= curr_line_region.begin_t)); + + // Fill the gap if it is necessary. + if (prev_line_idx != curr_line_region.begin_idx || prev_t != curr_line_region.begin_t) { + line_region_ranges_out.emplace_back(prev_line_idx, prev_t, curr_line_region.begin_idx, curr_line_region.begin_t, default_clip_idx); + } + + // Add the current region. + line_region_ranges_out.emplace_back(curr_line_region); + prev_line_idx = curr_line_region.end_idx; + prev_t = curr_line_region.end_t; + } + + // Fill the last remaining gap if it exists. + const size_t last_line_idx = total_lines_cnt - 1; + if ((prev_line_idx == last_line_idx && prev_t == 1.) || ((prev_line_idx == total_lines_cnt && prev_t == 0.))) { + // There is no gap at the end. + return line_region_ranges_out; + } + + // Fill the last remaining gap. + line_region_ranges_out.emplace_back(prev_line_idx, prev_t, last_line_idx, 1., default_clip_idx); + + return line_region_ranges_out; +} + +LineRegionRanges subject_segmentation(const ClipperLib_Z::Path &subject, const std::vector &expolygons_clips, const size_t default_clip_idx = 0) +{ + LineRegionRanges line_region_ranges; + for (const ExPolygons &expolygons_clip : expolygons_clips) { + const size_t expolygons_clip_idx = &expolygons_clip - expolygons_clips.data(); + const ClipperLib_Z::Paths clips = to_clip_zpaths(expolygons_clip); + Slic3r::append(line_region_ranges, intersection_with_region(subject, clips, expolygons_clip_idx + default_clip_idx + 1)); + } + + return create_continues_line_region_ranges(std::move(line_region_ranges), default_clip_idx, subject.size() - 1); +} + +PolylineSegment create_polyline_segment(const LineRegionRange &line_region_range, const Polyline &subject) +{ + Polyline polyline_out; + if (line_region_range.begin_t == 0.) { + polyline_out.points.emplace_back(subject[line_region_range.begin_idx]); + } else { + assert(line_region_range.begin_idx <= subject.size()); + Point interpolated_start_pt = lerp(subject[line_region_range.begin_idx], subject[line_region_range.begin_idx + 1], line_region_range.begin_t); + polyline_out.points.emplace_back(interpolated_start_pt); + } + + for (size_t line_idx = line_region_range.begin_idx + 1; line_idx <= line_region_range.end_idx; ++line_idx) { + polyline_out.points.emplace_back(subject[line_idx]); + } + + if (line_region_range.end_t == 0.) { + polyline_out.points.emplace_back(subject[line_region_range.end_idx]); + } else if (line_region_range.end_t == 1.) { + assert(line_region_range.end_idx <= subject.size()); + polyline_out.points.emplace_back(subject[line_region_range.end_idx + 1]); + } else { + assert(line_region_range.end_idx <= subject.size()); + Point interpolated_end_pt = lerp(subject[line_region_range.end_idx], subject[line_region_range.end_idx + 1], line_region_range.end_t); + polyline_out.points.emplace_back(interpolated_end_pt); + } + + return { polyline_out, line_region_range.clip_idx }; +} + +PolylineSegments create_polyline_segments(const LineRegionRanges &line_region_ranges, const Polyline &subject) +{ + PolylineSegments polyline_segments; + polyline_segments.reserve(line_region_ranges.size()); + for (const LineRegionRange ®ion_range : line_region_ranges) { + polyline_segments.emplace_back(create_polyline_segment(region_range, subject)); + } + + return polyline_segments; +} + +ExtrusionSegment create_extrusion_segment(const LineRegionRange &line_region_range, const Arachne::ExtrusionLine &subject) +{ + // When we call this function, we split ExtrusionLine into at least two segments, so none of those segments are closed. + Arachne::ExtrusionLine extrusion_out(subject.inset_idx, subject.is_odd); + if (line_region_range.begin_t == 0.) { + extrusion_out.junctions.emplace_back(subject[line_region_range.begin_idx]); + } else { + assert(line_region_range.begin_idx <= subject.size()); + const Arachne::ExtrusionJunction &junction_from = subject[line_region_range.begin_idx]; + const Arachne::ExtrusionJunction &junction_to = subject[line_region_range.begin_idx + 1]; + + const Point interpolated_start_pt = lerp(junction_from.p, junction_to.p, line_region_range.begin_t); + const coord_t interpolated_start_w = lerp(junction_from.w, junction_to.w, line_region_range.begin_t); + + assert(junction_from.perimeter_index == junction_to.perimeter_index); + extrusion_out.junctions.emplace_back(interpolated_start_pt, interpolated_start_w, junction_from.perimeter_index); + } + + for (size_t line_idx = line_region_range.begin_idx + 1; line_idx <= line_region_range.end_idx; ++line_idx) { + extrusion_out.junctions.emplace_back(subject[line_idx]); + } + + if (line_region_range.end_t == 0.) { + extrusion_out.junctions.emplace_back(subject[line_region_range.end_idx]); + } else if (line_region_range.end_t == 1.) { + assert(line_region_range.end_idx <= subject.size()); + extrusion_out.junctions.emplace_back(subject[line_region_range.end_idx + 1]); + } else { + assert(line_region_range.end_idx <= subject.size()); + const Arachne::ExtrusionJunction &junction_from = subject[line_region_range.end_idx]; + const Arachne::ExtrusionJunction &junction_to = subject[line_region_range.end_idx + 1]; + + const Point interpolated_end_pt = lerp(junction_from.p, junction_to.p, line_region_range.end_t); + const coord_t interpolated_end_w = lerp(junction_from.w, junction_to.w, line_region_range.end_t); + + assert(junction_from.perimeter_index == junction_to.perimeter_index); + extrusion_out.junctions.emplace_back(interpolated_end_pt, interpolated_end_w, junction_from.perimeter_index); + } + + return { extrusion_out, line_region_range.clip_idx }; +} + +ExtrusionSegments create_extrusion_segments(const LineRegionRanges &line_region_ranges, const Arachne::ExtrusionLine &subject) +{ + ExtrusionSegments extrusion_segments; + extrusion_segments.reserve(line_region_ranges.size()); + for (const LineRegionRange ®ion_range : line_region_ranges) { + extrusion_segments.emplace_back(create_extrusion_segment(region_range, subject)); + } + + return extrusion_segments; +} + +PolylineSegments polyline_segmentation(const Polyline &subject, const std::vector &expolygons_clips, const size_t default_clip_idx) +{ + const LineRegionRanges line_region_ranges = subject_segmentation(subject_to_zpath(subject), expolygons_clips, default_clip_idx); + if (line_region_ranges.empty()) { + return { PolylineSegment{subject, default_clip_idx} }; + } else if (line_region_ranges.size() == 1) { + return { PolylineSegment{subject, line_region_ranges.front().clip_idx} }; + } + + return create_polyline_segments(line_region_ranges, subject); +} + +PolylineSegments polygon_segmentation(const Polygon &subject, const std::vector &expolygons_clips, const size_t default_clip_idx) +{ + return polyline_segmentation(to_polyline(subject), expolygons_clips, default_clip_idx); +} + +ExtrusionSegments extrusion_segmentation(const Arachne::ExtrusionLine &subject, const std::vector &expolygons_clips, const size_t default_clip_idx) +{ + const LineRegionRanges line_region_ranges = subject_segmentation(subject_to_zpath(subject), expolygons_clips, default_clip_idx); + if (line_region_ranges.empty()) { + return { ExtrusionSegment{subject, default_clip_idx} }; + } else if (line_region_ranges.size() == 1) { + return { ExtrusionSegment{subject, line_region_ranges.front().clip_idx} }; + } + + return create_extrusion_segments(line_region_ranges, subject); +} + +inline std::vector to_expolygons_clips(const PerimeterRegions &perimeter_regions_clips) +{ + std::vector expolygons_clips; + expolygons_clips.reserve(perimeter_regions_clips.size()); + for (const PerimeterRegion &perimeter_region_clip : perimeter_regions_clips) { + expolygons_clips.emplace_back(perimeter_region_clip.expolygons); + } + + return expolygons_clips; +} + +PolylineRegionSegments polyline_segmentation(const Polyline &subject, const PrintRegionConfig &base_config, const PerimeterRegions &perimeter_regions_clips) +{ + const LineRegionRanges line_region_ranges = subject_segmentation(subject_to_zpath(subject), to_expolygons_clips(perimeter_regions_clips)); + if (line_region_ranges.empty()) { + return { PolylineRegionSegment{subject, base_config} }; + } else if (line_region_ranges.size() == 1) { + return { PolylineRegionSegment{subject, perimeter_regions_clips[line_region_ranges.front().clip_idx - 1].region->config()} }; + } + + PolylineRegionSegments segments_out; + for (PolylineSegment &segment : create_polyline_segments(line_region_ranges, subject)) { + const PrintRegionConfig &config = segment.clip_idx == 0 ? base_config : perimeter_regions_clips[segment.clip_idx - 1].region->config(); + segments_out.emplace_back(std::move(segment.polyline), config); + } + + return segments_out; +} + +PolylineRegionSegments polygon_segmentation(const Polygon &subject, const PrintRegionConfig &base_config, const PerimeterRegions &perimeter_regions_clips) +{ + return polyline_segmentation(to_polyline(subject), base_config, perimeter_regions_clips); +} + +ExtrusionRegionSegments extrusion_segmentation(const Arachne::ExtrusionLine &subject, const PrintRegionConfig &base_config, const PerimeterRegions &perimeter_regions_clips) +{ + const LineRegionRanges line_region_ranges = subject_segmentation(subject_to_zpath(subject), to_expolygons_clips(perimeter_regions_clips)); + if (line_region_ranges.empty()) { + return { ExtrusionRegionSegment{subject, base_config} }; + } else if (line_region_ranges.size() == 1) { + return { ExtrusionRegionSegment{subject, perimeter_regions_clips[line_region_ranges.front().clip_idx - 1].region->config()} }; + } + + ExtrusionRegionSegments segments_out; + for (ExtrusionSegment &segment : create_extrusion_segments(line_region_ranges, subject)) { + const PrintRegionConfig &config = segment.clip_idx == 0 ? base_config : perimeter_regions_clips[segment.clip_idx - 1].region->config(); + segments_out.emplace_back(std::move(segment.extrusion), config); + } + + return segments_out; +} + +} // namespace Slic3r::Algorithm::LineSegmentation diff --git a/src/libslic3r/Algorithm/LineSegmentation/LineSegmentation.hpp b/src/libslic3r/Algorithm/LineSegmentation/LineSegmentation.hpp new file mode 100644 index 0000000000..5e53e0add0 --- /dev/null +++ b/src/libslic3r/Algorithm/LineSegmentation/LineSegmentation.hpp @@ -0,0 +1,69 @@ +#ifndef libslic3r_LineSegmentation_hpp_ +#define libslic3r_LineSegmentation_hpp_ + +#include + +#include "libslic3r/Arachne/utils/ExtrusionLine.hpp" + +namespace Slic3r { +class ExPolygon; +class Polyline; +class Polygon; +class PrintRegionConfig; + +struct PerimeterRegion; + +using ExPolygons = std::vector; +using PerimeterRegions = std::vector; +} // namespace Slic3r + +namespace Slic3r::Arachne { +struct ExtrusionLine; +} + +namespace Slic3r::Algorithm::LineSegmentation { + +struct PolylineSegment +{ + Polyline polyline; + size_t clip_idx; +}; + +struct PolylineRegionSegment +{ + Polyline polyline; + const PrintRegionConfig &config; + + PolylineRegionSegment(const Polyline &polyline, const PrintRegionConfig &config) : polyline(polyline), config(config) {} +}; + +struct ExtrusionSegment +{ + Arachne::ExtrusionLine extrusion; + size_t clip_idx; +}; + +struct ExtrusionRegionSegment +{ + Arachne::ExtrusionLine extrusion; + const PrintRegionConfig &config; + + ExtrusionRegionSegment(const Arachne::ExtrusionLine &extrusion, const PrintRegionConfig &config) : extrusion(extrusion), config(config) {} +}; + +using PolylineSegments = std::vector; +using ExtrusionSegments = std::vector; +using PolylineRegionSegments = std::vector; +using ExtrusionRegionSegments = std::vector; + +PolylineSegments polyline_segmentation(const Polyline &subject, const std::vector &expolygons_clips, size_t default_clip_idx = 0); +PolylineSegments polygon_segmentation(const Polygon &subject, const std::vector &expolygons_clips, size_t default_clip_idx = 0); +ExtrusionSegments extrusion_segmentation(const Arachne::ExtrusionLine &subject, const std::vector &expolygons_clips, size_t default_clip_idx = 0); + +PolylineRegionSegments polyline_segmentation(const Polyline &subject, const PrintRegionConfig &base_config, const PerimeterRegions &perimeter_regions_clips); +PolylineRegionSegments polygon_segmentation(const Polygon &subject, const PrintRegionConfig &base_config, const PerimeterRegions &perimeter_regions_clips); +ExtrusionRegionSegments extrusion_segmentation(const Arachne::ExtrusionLine &subject, const PrintRegionConfig &base_config, const PerimeterRegions &perimeter_regions_clips); + +} // namespace Slic3r::Algorithm::LineSegmentation + +#endif // libslic3r_LineSegmentation_hpp_ diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index 2e65c77921..de31aaba16 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -34,6 +34,8 @@ set(SLIC3R_SOURCES AABBTreeLines.hpp AABBMesh.hpp AABBMesh.cpp + Algorithm/LineSegmentation/LineSegmentation.cpp + Algorithm/LineSegmentation/LineSegmentation.hpp Algorithm/PathSorting.hpp Algorithm/RegionExpansion.hpp Algorithm/RegionExpansion.cpp diff --git a/src/libslic3r/ClipperZUtils.hpp b/src/libslic3r/ClipperZUtils.hpp index f6b249b47f..2c71f4bfd5 100644 --- a/src/libslic3r/ClipperZUtils.hpp +++ b/src/libslic3r/ClipperZUtils.hpp @@ -71,6 +71,24 @@ inline ZPaths expolygons_to_zpaths(const ExPolygons &src, coord_t &base_idx) return out; } +// Convert multiple expolygons into z-paths with a given Z coordinate. +// If Open, then duplicate the first point of each path at its end. +template +inline ZPaths expolygons_to_zpaths_with_same_z(const ExPolygons &src, const coord_t z) +{ + ZPaths out; + out.reserve(std::accumulate(src.begin(), src.end(), size_t(0), + [](const size_t acc, const ExPolygon &expoly) { return acc + expoly.num_contours(); })); + for (const ExPolygon &expoly : src) { + out.emplace_back(to_zpath(expoly.contour.points, z)); + for (const Polygon &hole : expoly.holes) { + out.emplace_back(to_zpath(hole.points, z)); + } + } + + return out; +} + // Convert a single path to path with a given Z coordinate. // If Open, then duplicate the first point at the end. template diff --git a/src/libslic3r/Layer.cpp b/src/libslic3r/Layer.cpp index 2a8ce5d7be..1cf9e1882d 100644 --- a/src/libslic3r/Layer.cpp +++ b/src/libslic3r/Layer.cpp @@ -29,6 +29,7 @@ #include "libslic3r/ExtrusionEntity.hpp" #include "libslic3r/ExtrusionEntityCollection.hpp" #include "libslic3r/LayerRegion.hpp" +#include "libslic3r/PerimeterGenerator.hpp" #include "libslic3r/PrintConfig.hpp" #include "libslic3r/Surface.hpp" #include "libslic3r/SurfaceCollection.hpp" @@ -632,6 +633,36 @@ ExPolygons Layer::merged(float offset_scaled) const return out; } +// If there is any incompatibility, separate LayerRegions have to be created. +inline bool has_compatible_dynamic_overhang_speed(const PrintRegionConfig &config, const PrintRegionConfig &other_config) +{ + bool dynamic_overhang_speed_compatibility = config.enable_dynamic_overhang_speeds == other_config.enable_dynamic_overhang_speeds; + if (dynamic_overhang_speed_compatibility && config.enable_dynamic_overhang_speeds) { + dynamic_overhang_speed_compatibility = config.overhang_speed_0 == other_config.overhang_speed_0 && + config.overhang_speed_1 == other_config.overhang_speed_1 && + config.overhang_speed_2 == other_config.overhang_speed_2 && + config.overhang_speed_3 == other_config.overhang_speed_3; + } + + return dynamic_overhang_speed_compatibility; +} + +// If there is any incompatibility, separate LayerRegions have to be created. +inline bool has_compatible_layer_regions(const PrintRegionConfig &config, const PrintRegionConfig &other_config) +{ + return config.perimeter_extruder == other_config.perimeter_extruder && + config.perimeters == other_config.perimeters && + config.perimeter_speed == other_config.perimeter_speed && + config.external_perimeter_speed == other_config.external_perimeter_speed && + (config.gap_fill_enabled ? config.gap_fill_speed.value : 0.) == (other_config.gap_fill_enabled ? other_config.gap_fill_speed.value : 0.) && + config.overhangs == other_config.overhangs && + config.opt_serialize("perimeter_extrusion_width") == other_config.opt_serialize("perimeter_extrusion_width") && + config.thin_walls == other_config.thin_walls && + config.external_perimeters_first == other_config.external_perimeters_first && + config.infill_overlap == other_config.infill_overlap && + has_compatible_dynamic_overhang_speed(config, other_config); +} + // Here the perimeters are created cummulatively for all layer regions sharing the same parameters influencing the perimeters. // The perimeter paths and the thin fills (ExtrusionEntityCollection) are assigned to the first compatible layer region. // The resulting fill surface is split back among the originating regions. @@ -662,98 +693,105 @@ void Layer::make_perimeters() for (LayerSlice &lslice : this->lslices_ex) lslice.islands.clear(); - for (LayerRegionPtrs::iterator layerm = m_regions.begin(); layerm != m_regions.end(); ++ layerm) - if (size_t region_id = layerm - m_regions.begin(); ! done[region_id]) { - layer_region_reset_perimeters(**layerm); - if (! (*layerm)->slices().empty()) { - BOOST_LOG_TRIVIAL(trace) << "Generating perimeters for layer " << this->id() << ", region " << region_id; - done[region_id] = true; - const PrintRegionConfig &config = (*layerm)->region().config(); - - perimeter_and_gapfill_ranges.clear(); - fill_expolygons.clear(); - fill_expolygons_ranges.clear(); - surfaces_to_merge.clear(); - - // find compatible regions - layer_region_ids.clear(); - layer_region_ids.push_back(region_id); - for (LayerRegionPtrs::const_iterator it = layerm + 1; it != m_regions.end(); ++it) - if (! (*it)->slices().empty()) { - LayerRegion *other_layerm = *it; - const PrintRegionConfig &other_config = other_layerm->region().config(); - bool dynamic_overhang_speed_compatibility = config.enable_dynamic_overhang_speeds == - other_config.enable_dynamic_overhang_speeds; - if (dynamic_overhang_speed_compatibility && config.enable_dynamic_overhang_speeds) { - dynamic_overhang_speed_compatibility = config.overhang_speed_0 == other_config.overhang_speed_0 && - config.overhang_speed_1 == other_config.overhang_speed_1 && - config.overhang_speed_2 == other_config.overhang_speed_2 && - config.overhang_speed_3 == other_config.overhang_speed_3; - } - - if (config.perimeter_extruder == other_config.perimeter_extruder - && config.perimeters == other_config.perimeters - && config.perimeter_speed == other_config.perimeter_speed - && config.external_perimeter_speed == other_config.external_perimeter_speed - && dynamic_overhang_speed_compatibility - && (config.gap_fill_enabled ? config.gap_fill_speed.value : 0.) == - (other_config.gap_fill_enabled ? other_config.gap_fill_speed.value : 0.) - && config.overhangs == other_config.overhangs - && config.opt_serialize("perimeter_extrusion_width") == other_config.opt_serialize("perimeter_extrusion_width") - && config.thin_walls == other_config.thin_walls - && config.external_perimeters_first == other_config.external_perimeters_first - && config.infill_overlap == other_config.infill_overlap - && config.fuzzy_skin == other_config.fuzzy_skin - && config.fuzzy_skin_thickness == other_config.fuzzy_skin_thickness - && config.fuzzy_skin_point_dist == other_config.fuzzy_skin_point_dist) - { - layer_region_reset_perimeters(*other_layerm); - layer_region_ids.push_back(it - m_regions.begin()); - done[it - m_regions.begin()] = true; - } - } - - if (layer_region_ids.size() == 1) { // optimization - (*layerm)->make_perimeters((*layerm)->slices(), perimeter_and_gapfill_ranges, fill_expolygons, fill_expolygons_ranges); - this->sort_perimeters_into_islands((*layerm)->slices(), region_id, perimeter_and_gapfill_ranges, std::move(fill_expolygons), fill_expolygons_ranges, layer_region_ids); - } else { - SurfaceCollection new_slices; - // Use the region with highest infill rate, as the make_perimeters() function below decides on the gap fill based on the infill existence. - uint32_t region_id_config = layer_region_ids.front(); - LayerRegion* layerm_config = m_regions[region_id_config]; - { - // Merge slices (surfaces) according to number of extra perimeters. - for (uint32_t region_id : layer_region_ids) { - LayerRegion &layerm = *m_regions[region_id]; - for (const Surface &surface : layerm.slices()) - surfaces_to_merge.emplace_back(&surface); - if (layerm.region().config().fill_density > layerm_config->region().config().fill_density) { - region_id_config = region_id; - layerm_config = &layerm; - } - } - std::sort(surfaces_to_merge.begin(), surfaces_to_merge.end(), [](const Surface *l, const Surface *r){ return l->extra_perimeters < r->extra_perimeters; }); - for (size_t i = 0; i < surfaces_to_merge.size();) { - size_t j = i; - const Surface &first = *surfaces_to_merge[i]; - size_t extra_perimeters = first.extra_perimeters; - for (; j < surfaces_to_merge.size() && surfaces_to_merge[j]->extra_perimeters == extra_perimeters; ++ j) ; - if (i + 1 == j) - // Nothing to merge, just copy. - new_slices.surfaces.emplace_back(*surfaces_to_merge[i]); - else { - surfaces_to_merge_temp.assign(surfaces_to_merge.begin() + i, surfaces_to_merge.begin() + j); - new_slices.append(offset_ex(surfaces_to_merge_temp, ClipperSafetyOffset), first); - } - i = j; - } - } - // make perimeters - layerm_config->make_perimeters(new_slices, perimeter_and_gapfill_ranges, fill_expolygons, fill_expolygons_ranges); - this->sort_perimeters_into_islands(new_slices, region_id_config, perimeter_and_gapfill_ranges, std::move(fill_expolygons), fill_expolygons_ranges, layer_region_ids); - } - } + for (auto it_curr_region = m_regions.cbegin(); it_curr_region != m_regions.cend(); ++it_curr_region) { + const size_t curr_region_id = std::distance(m_regions.cbegin(), it_curr_region); + if (done[curr_region_id]) { + continue; } + + LayerRegion &curr_region = **it_curr_region; + layer_region_reset_perimeters(curr_region); + if (curr_region.slices().empty()) { + continue; + } + + BOOST_LOG_TRIVIAL(trace) << "Generating perimeters for layer " << this->id() << ", region " << curr_region_id; + done[curr_region_id] = true; + const PrintRegionConfig &curr_config = curr_region.region().config(); + + perimeter_and_gapfill_ranges.clear(); + fill_expolygons.clear(); + fill_expolygons_ranges.clear(); + surfaces_to_merge.clear(); + + // Find compatible regions. + layer_region_ids.clear(); + layer_region_ids.push_back(curr_region_id); + + PerimeterRegions perimeter_regions; + for (auto it_next_region = std::next(it_curr_region); it_next_region != m_regions.cend(); ++it_next_region) { + const size_t next_region_id = std::distance(m_regions.cbegin(), it_next_region); + LayerRegion &next_region = **it_next_region; + const PrintRegionConfig &next_config = next_region.region().config(); + if (next_region.slices().empty()) { + continue; + } + + if (!has_compatible_layer_regions(curr_config, next_config)) { + continue; + } + + // Now, we are sure that we want to merge LayerRegions in any case. + layer_region_reset_perimeters(next_region); + layer_region_ids.push_back(next_region_id); + done[next_region_id] = true; + + // If any parameters affecting just perimeters are incompatible, then we also create PerimeterRegion. + if (!PerimeterRegion::has_compatible_perimeter_regions(curr_config, next_config)) { + perimeter_regions.emplace_back(next_region); + } + } + + if (layer_region_ids.size() == 1) { // Optimization. + curr_region.make_perimeters(curr_region.slices(), perimeter_regions, perimeter_and_gapfill_ranges, fill_expolygons, fill_expolygons_ranges); + this->sort_perimeters_into_islands(curr_region.slices(), curr_region_id, perimeter_and_gapfill_ranges, std::move(fill_expolygons), fill_expolygons_ranges, layer_region_ids); + } else { + SurfaceCollection new_slices; + // Use the region with highest infill rate, as the make_perimeters() function below decides on the gap fill based on the infill existence. + uint32_t region_id_config = layer_region_ids.front(); + LayerRegion *layerm_config = m_regions[region_id_config]; + { + // Merge slices (surfaces) according to number of extra perimeters. + for (uint32_t region_id : layer_region_ids) { + LayerRegion &layerm = *m_regions[region_id]; + for (const Surface &surface : layerm.slices()) + surfaces_to_merge.emplace_back(&surface); + if (layerm.region().config().fill_density > layerm_config->region().config().fill_density) { + region_id_config = region_id; + layerm_config = &layerm; + } + } + + std::sort(surfaces_to_merge.begin(), surfaces_to_merge.end(), [](const Surface *l, const Surface *r) { return l->extra_perimeters < r->extra_perimeters; }); + for (size_t i = 0; i < surfaces_to_merge.size();) { + size_t j = i; + const Surface &first = *surfaces_to_merge[i]; + size_t extra_perimeters = first.extra_perimeters; + for (; j < surfaces_to_merge.size() && surfaces_to_merge[j]->extra_perimeters == extra_perimeters; ++j); + + if (i + 1 == j) { + // Nothing to merge, just copy. + new_slices.surfaces.emplace_back(*surfaces_to_merge[i]); + } else { + surfaces_to_merge_temp.assign(surfaces_to_merge.begin() + i, surfaces_to_merge.begin() + j); + new_slices.append(offset_ex(surfaces_to_merge_temp, ClipperSafetyOffset), first); + } + + i = j; + } + } + + // Try to merge compatible PerimeterRegions. + if (perimeter_regions.size() > 1) { + PerimeterRegion::merge_compatible_perimeter_regions(perimeter_regions); + } + + // Make perimeters. + layerm_config->make_perimeters(new_slices, perimeter_regions, perimeter_and_gapfill_ranges, fill_expolygons, fill_expolygons_ranges); + this->sort_perimeters_into_islands(new_slices, region_id_config, perimeter_and_gapfill_ranges, std::move(fill_expolygons), fill_expolygons_ranges, layer_region_ids); + } + } + BOOST_LOG_TRIVIAL(trace) << "Generating perimeters for layer " << this->id() << " - Done"; } diff --git a/src/libslic3r/LayerRegion.cpp b/src/libslic3r/LayerRegion.cpp index 89e27cd8bc..c913082636 100644 --- a/src/libslic3r/LayerRegion.cpp +++ b/src/libslic3r/LayerRegion.cpp @@ -89,6 +89,8 @@ void LayerRegion::slices_to_fill_surfaces_clipped() void LayerRegion::make_perimeters( // Input slices for which the perimeters, gap fills and fill expolygons are to be generated. const SurfaceCollection &slices, + // Configuration regions that will be applied to parts of created perimeters. + const PerimeterRegions &perimeter_regions, // Ranges of perimeter extrusions and gap fill extrusions per suface, referencing // newly created extrusions stored at this LayerRegion. std::vector> &perimeter_and_gapfill_ranges, @@ -123,6 +125,7 @@ void LayerRegion::make_perimeters( region_config, this->layer()->object()->config(), print_config, + perimeter_regions, spiral_vase ); diff --git a/src/libslic3r/LayerRegion.hpp b/src/libslic3r/LayerRegion.hpp index b05c750241..30e9f18e8d 100644 --- a/src/libslic3r/LayerRegion.hpp +++ b/src/libslic3r/LayerRegion.hpp @@ -29,6 +29,9 @@ class PrintObject; using LayerPtrs = std::vector; class PrintRegion; +struct PerimeterRegion; +using PerimeterRegions = std::vector; + // Range of indices, providing support for range based loops. template class IndexRange @@ -119,6 +122,8 @@ public: void make_perimeters( // Input slices for which the perimeters, gap fills and fill expolygons are to be generated. const SurfaceCollection &slices, + // Configuration regions that will be applied to parts of created perimeters. + const PerimeterRegions &perimeter_regions, // Ranges of perimeter extrusions and gap fill extrusions per suface, referencing // newly created extrusions stored at this LayerRegion. std::vector> &perimeter_and_gapfill_ranges, diff --git a/src/libslic3r/PerimeterGenerator.cpp b/src/libslic3r/PerimeterGenerator.cpp index 2f73a704c9..01f8dd7f8f 100644 --- a/src/libslic3r/PerimeterGenerator.cpp +++ b/src/libslic3r/PerimeterGenerator.cpp @@ -42,7 +42,9 @@ #include "Arachne/utils/ExtrusionJunction.hpp" #include "libslic3r.h" #include "libslic3r/Flow.hpp" +#include "libslic3r/LayerRegion.hpp" #include "libslic3r/Line.hpp" +#include "libslic3r/Print.hpp" //#define ARACHNE_DEBUG @@ -1544,4 +1546,43 @@ void PerimeterGenerator::process_classic( append(out_fill_expolygons, std::move(infill_areas)); } +PerimeterRegion::PerimeterRegion(const LayerRegion &layer_region) : region(&layer_region.region()) +{ + this->expolygons = to_expolygons(layer_region.slices().surfaces); + this->bbox = get_extents(this->expolygons); +} + +bool PerimeterRegion::has_compatible_perimeter_regions(const PrintRegionConfig &config, const PrintRegionConfig &other_config) +{ + return config.fuzzy_skin == other_config.fuzzy_skin && + config.fuzzy_skin_thickness == other_config.fuzzy_skin_thickness && + config.fuzzy_skin_point_dist == other_config.fuzzy_skin_point_dist; +} + +void PerimeterRegion::merge_compatible_perimeter_regions(PerimeterRegions &perimeter_regions) +{ + if (perimeter_regions.size() <= 1) { + return; + } + + PerimeterRegions perimeter_regions_merged; + for (auto it_curr_region = perimeter_regions.begin(); it_curr_region != perimeter_regions.end();) { + PerimeterRegion current_merge = *it_curr_region; + auto it_next_region = std::next(it_curr_region); + for (; it_next_region != perimeter_regions.end() && has_compatible_perimeter_regions(it_next_region->region->config(), it_curr_region->region->config()); ++it_next_region) { + Slic3r::append(current_merge.expolygons, std::move(it_next_region->expolygons)); + current_merge.bbox.merge(it_next_region->bbox); + } + + if (std::distance(it_curr_region, it_next_region) > 1) { + current_merge.expolygons = union_ex(current_merge.expolygons); + } + + perimeter_regions_merged.emplace_back(std::move(current_merge)); + it_curr_region = it_next_region; + } + + perimeter_regions = perimeter_regions_merged; +} + } diff --git a/src/libslic3r/PerimeterGenerator.hpp b/src/libslic3r/PerimeterGenerator.hpp index 6df0923635..1e699abb79 100644 --- a/src/libslic3r/PerimeterGenerator.hpp +++ b/src/libslic3r/PerimeterGenerator.hpp @@ -22,11 +22,31 @@ namespace Slic3r { class ExtrusionEntityCollection; +class LayerRegion; class Surface; +class PrintRegion; struct ThickPolyline; -namespace PerimeterGenerator +struct PerimeterRegion { + const PrintRegion *region; + ExPolygons expolygons; + BoundingBox bbox; + + explicit PerimeterRegion(const LayerRegion &layer_region); + + // If there is any incompatibility, we don't need to create separate LayerRegions. + // Because it is enough to split perimeters by PerimeterRegions. + static bool has_compatible_perimeter_regions(const PrintRegionConfig &config, const PrintRegionConfig &other_config); + + static void merge_compatible_perimeter_regions(std::vector &perimeter_regions); +}; + +using PerimeterRegions = std::vector; + +} // namespace Slic3r + +namespace Slic3r::PerimeterGenerator { struct Parameters { Parameters( @@ -39,6 +59,7 @@ struct Parameters { const PrintRegionConfig &config, const PrintObjectConfig &object_config, const PrintConfig &print_config, + const PerimeterRegions &perimeter_regions, const bool spiral_vase) : layer_height(layer_height), layer_id(layer_id), @@ -49,6 +70,7 @@ struct Parameters { config(config), object_config(object_config), print_config(print_config), + perimeter_regions(perimeter_regions), spiral_vase(spiral_vase), scaled_resolution(scaled(print_config.gcode_resolution.value)), mm3_per_mm(perimeter_flow.mm3_per_mm()), @@ -67,6 +89,7 @@ struct Parameters { const PrintRegionConfig &config; const PrintObjectConfig &object_config; const PrintConfig &print_config; + const PerimeterRegions &perimeter_regions; // Derived parameters bool spiral_vase; @@ -113,7 +136,6 @@ void process_arachne( ExtrusionMultiPath thick_polyline_to_multi_path(const ThickPolyline &thick_polyline, ExtrusionRole role, const Flow &flow, float tolerance, float merge_tolerance); -} // namespace PerimeterGenerator -} // namespace Slic3r +} // namespace Slic3r::PerimeterGenerator #endif diff --git a/tests/fff_print/test_perimeters.cpp b/tests/fff_print/test_perimeters.cpp index c154f5a0c2..fadebe76d4 100644 --- a/tests/fff_print/test_perimeters.cpp +++ b/tests/fff_print/test_perimeters.cpp @@ -46,6 +46,7 @@ SCENARIO("Perimeter nesting", "[Perimeters]") ExtrusionEntityCollection gap_fill; ExPolygons fill_expolygons; Flow flow(1., 1., 1.); + PerimeterRegions perimeter_regions; PerimeterGenerator::Parameters perimeter_generator_params( 1., // layer height -1, // layer ID @@ -53,6 +54,7 @@ SCENARIO("Perimeter nesting", "[Perimeters]") static_cast(config), static_cast(config), static_cast(config), + perimeter_regions, false); // spiral_vase Polygons lower_layer_polygons_cache; for (const Surface &surface : slices)