diff --git a/src/libslic3r/Fill/FillConcentricInternal.cpp b/src/libslic3r/Fill/FillConcentricInternal.cpp index 702f01711..d565992ea 100644 --- a/src/libslic3r/Fill/FillConcentricInternal.cpp +++ b/src/libslic3r/Fill/FillConcentricInternal.cpp @@ -23,7 +23,7 @@ void FillConcentricInternal::fill_surface_extrusion(const Surface* surface, cons coord_t min_spacing = params.flow.scaled_spacing(); coord_t loops_count = std::max(bbox_size.x(), bbox_size.y()) / min_spacing + 1; - Polygons polygons = offset(expolygon, 0); + Polygons polygons = to_polygons(expolygon); double min_nozzle_diameter = *std::min_element(print_config->nozzle_diameter.values.begin(), print_config->nozzle_diameter.values.end()); Arachne::WallToolPathsParams input_params; diff --git a/src/libslic3r/LayerRegion.cpp b/src/libslic3r/LayerRegion.cpp index c84786c63..a0fe6dadd 100644 --- a/src/libslic3r/LayerRegion.cpp +++ b/src/libslic3r/LayerRegion.cpp @@ -105,8 +105,11 @@ void LayerRegion::make_perimeters(const SurfaceCollection &slices, SurfaceCollec g.ext_perimeter_flow = this->flow(frExternalPerimeter); g.overhang_flow = this->bridging_flow(frPerimeter, object_config.thick_bridges); g.solid_infill_flow = this->flow(frSolidInfill); - - g.process(); + + if (this->layer()->object()->config().wall_generator.value == PerimeterGeneratorType::Arachne && !spiral_mode) + g.process_arachne(); + else + g.process_classic(); } //#define EXTERNAL_SURFACES_OFFSET_PARAMETERS ClipperLib::jtMiter, 3. diff --git a/src/libslic3r/PerimeterGenerator.cpp b/src/libslic3r/PerimeterGenerator.cpp index f6f064bd5..f3066141d 100644 --- a/src/libslic3r/PerimeterGenerator.cpp +++ b/src/libslic3r/PerimeterGenerator.cpp @@ -5,6 +5,7 @@ #include "VariableWidth.hpp" #include "CurveAnalyzer.hpp" #include "Clipper2Utils.hpp" +#include "Arachne/WallToolPaths.hpp" #include #include @@ -79,6 +80,51 @@ static void fuzzy_polygon(Polygon &poly, double fuzzy_skin_thickness, double fuz poly.points = std::move(out); } +// Thanks Cura developers for this function. +static void fuzzy_extrusion_line(Arachne::ExtrusionLine& ext_lines, double fuzzy_skin_thickness, double fuzzy_skin_point_dist) +{ + const double min_dist_between_points = fuzzy_skin_point_dist * 3. / 4.; // hardcoded: the point distance may vary between 3/4 and 5/4 the supplied value + const double range_random_point_dist = fuzzy_skin_point_dist / 2.; + double dist_left_over = double(rand()) * (min_dist_between_points / 2) / double(RAND_MAX); // the distance to be traversed on the line before making the first new point + + auto* p0 = &ext_lines.front(); + std::vector out; + out.reserve(ext_lines.size()); + for (auto& p1 : ext_lines) { + if (p0->p == p1.p) { // Connect endpoints. + out.emplace_back(p1.p, p1.w, p1.perimeter_index); + continue; + } + + // 'a' is the (next) new point between p0 and p1 + Vec2d p0p1 = (p1.p - p0->p).cast(); + double p0p1_size = p0p1.norm(); + // so that p0p1_size - dist_last_point evaulates to dist_left_over - p0p1_size + double dist_last_point = dist_left_over + p0p1_size * 2.; + for (double p0pa_dist = dist_left_over; p0pa_dist < p0p1_size; p0pa_dist += min_dist_between_points + double(rand()) * range_random_point_dist / double(RAND_MAX)) { + double r = double(rand()) * (fuzzy_skin_thickness * 2.) / double(RAND_MAX) - fuzzy_skin_thickness; + out.emplace_back(p0->p + (p0p1 * (p0pa_dist / p0p1_size) + perp(p0p1).cast().normalized() * r).cast(), p1.w, p1.perimeter_index); + dist_last_point = p0pa_dist; + } + dist_left_over = p0p1_size - dist_last_point; + p0 = &p1; + } + + while (out.size() < 3) { + size_t point_idx = ext_lines.size() - 2; + out.emplace_back(ext_lines[point_idx].p, ext_lines[point_idx].w, ext_lines[point_idx].perimeter_index); + if (point_idx == 0) + break; + --point_idx; + } + + if (ext_lines.back().p == ext_lines.front().p) // Connect endpoints. + out.front().p = out.back().p; + + if (out.size() >= 3) + ext_lines.junctions = std::move(out); +} + using PerimeterGeneratorLoops = std::vector; static void lowpass_filter_by_paths_overhang_degree(ExtrusionPaths& paths) { @@ -356,7 +402,229 @@ static ExtrusionEntityCollection traverse_loops(const PerimeterGenerator &perime return out; } -void PerimeterGenerator::process() +static ClipperLib_Z::Paths clip_extrusion(const ClipperLib_Z::Path& subject, const ClipperLib_Z::Paths& clip, ClipperLib_Z::ClipType clipType) +{ + ClipperLib_Z::Clipper clipper; + 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& pt) { + ClipperLib_Z::IntPoint start = e1bot; + ClipperLib_Z::IntPoint end = e1top; + + if (start.z() <= 0 && end.z() <= 0) { + start = e2bot; + end = e2top; + } + + assert(start.z() > 0 && end.z() > 0); + + // Interpolate extrusion line width. + double length_sqr = (end - start).cast().squaredNorm(); + double dist_sqr = (pt - start).cast().squaredNorm(); + double t = std::sqrt(dist_sqr / length_sqr); + + pt.z() = start.z() + coord_t((end.z() - start.z()) * t); + }); + + clipper.AddPath(subject, ClipperLib_Z::ptSubject, false); + clipper.AddPaths(clip, ClipperLib_Z::ptClip, true); + + ClipperLib_Z::PolyTree clipped_polytree; + ClipperLib_Z::Paths clipped_paths; + clipper.Execute(clipType, clipped_polytree, ClipperLib_Z::pftNonZero, ClipperLib_Z::pftNonZero); + ClipperLib_Z::PolyTreeToPaths(clipped_polytree, clipped_paths); + + // Clipped path could contain vertices from the clip with a Z coordinate equal to zero. + // For those vertices, we must assign value based on the subject. + // This happens only in sporadic cases. + for (ClipperLib_Z::Path& path : clipped_paths) + for (ClipperLib_Z::IntPoint& c_pt : path) + if (c_pt.z() == 0) { + // Now we must find the corresponding line on with this point is located and compute line width (Z coordinate). + if (subject.size() <= 2) + continue; + + const Point pt(c_pt.x(), c_pt.y()); + Point projected_pt_min; + auto it_min = subject.begin(); + auto dist_sqr_min = std::numeric_limits::max(); + Point prev(subject.front().x(), subject.front().y()); + for (auto it = std::next(subject.begin()); it != subject.end(); ++it) { + Point curr(it->x(), it->y()); + Point projected_pt = pt.projection_onto(Line(prev, curr)); + if (double dist_sqr = (projected_pt - pt).cast().squaredNorm(); dist_sqr < dist_sqr_min) { + dist_sqr_min = dist_sqr; + projected_pt_min = projected_pt; + it_min = std::prev(it); + } + prev = curr; + } + + assert(dist_sqr_min <= SCALED_EPSILON); + assert(std::next(it_min) != subject.end()); + + const Point pt_a(it_min->x(), it_min->y()); + const Point pt_b(std::next(it_min)->x(), std::next(it_min)->y()); + const double line_len = (pt_b - pt_a).cast().norm(); + const double dist = (projected_pt_min - pt_a).cast().norm(); + c_pt.z() = coord_t(double(it_min->z()) + (dist / line_len) * double(std::next(it_min)->z() - it_min->z())); + } + + assert([&clipped_paths = std::as_const(clipped_paths)]() -> bool { + for (const ClipperLib_Z::Path& path : clipped_paths) + for (const ClipperLib_Z::IntPoint& pt : path) + if (pt.z() <= 0) + return false; + return true; + }()); + + return clipped_paths; +} + +struct PerimeterGeneratorArachneExtrusion +{ + Arachne::ExtrusionLine* extrusion = nullptr; + // Indicates if closed ExtrusionLine is a contour or a hole. Used it only when ExtrusionLine is a closed loop. + bool is_contour = false; + // Should this extrusion be fuzzyfied on path generation? + bool fuzzify = false; +}; + +static ExtrusionEntityCollection traverse_extrusions(const PerimeterGenerator& perimeter_generator, std::vector& pg_extrusions) +{ + ExtrusionEntityCollection extrusion_coll; + for (PerimeterGeneratorArachneExtrusion& pg_extrusion : pg_extrusions) { + Arachne::ExtrusionLine* extrusion = pg_extrusion.extrusion; + if (extrusion->empty()) + continue; + + const bool is_external = extrusion->inset_idx == 0; + ExtrusionRole role = is_external ? erExternalPerimeter : erPerimeter; + + if (pg_extrusion.fuzzify) + fuzzy_extrusion_line(*extrusion, scaled(perimeter_generator.config->fuzzy_skin_thickness.value), scaled(perimeter_generator.config->fuzzy_skin_point_distance.value)); + + ExtrusionPaths paths; + // detect overhanging/bridging perimeters + if (perimeter_generator.config->detect_overhang_wall && perimeter_generator.layer_id > perimeter_generator.object_config->raft_layers + && !((perimeter_generator.object_config->enable_support || perimeter_generator.object_config->enforce_support_layers > 0) && + perimeter_generator.object_config->support_top_z_distance.value == 0)) { + + ClipperLib_Z::Path extrusion_path; + extrusion_path.reserve(extrusion->size()); + for (const Arachne::ExtrusionJunction& ej : extrusion->junctions) + extrusion_path.emplace_back(ej.p.x(), ej.p.y(), ej.w); + + ClipperLib_Z::Paths lower_slices_paths; + lower_slices_paths.reserve(perimeter_generator.lower_slices_polygons().size()); + for (const Polygon& poly : perimeter_generator.lower_slices_polygons()) { + lower_slices_paths.emplace_back(); + ClipperLib_Z::Path& out = lower_slices_paths.back(); + out.reserve(poly.points.size()); + for (const Point& pt : poly.points) + out.emplace_back(pt.x(), pt.y(), 0); + } + + // get non-overhang paths by intersecting this loop with the grown lower slices + extrusion_paths_append(paths, clip_extrusion(extrusion_path, lower_slices_paths, ClipperLib_Z::ctIntersection), role, + is_external ? perimeter_generator.ext_perimeter_flow : perimeter_generator.perimeter_flow); + + // get overhang paths by checking what parts of this loop fall + // outside the grown lower slices (thus where the distance between + // the loop centerline and original lower slices is >= half nozzle diameter + extrusion_paths_append(paths, clip_extrusion(extrusion_path, lower_slices_paths, ClipperLib_Z::ctDifference), erOverhangPerimeter, + perimeter_generator.overhang_flow); + + // Reapply the nearest point search for starting point. + // We allow polyline reversal because Clipper may have randomly reversed polylines during clipping. + // Arachne sometimes creates extrusion with zero-length (just two same endpoints); + if (!paths.empty()) { + Point start_point = paths.front().first_point(); + if (!extrusion->is_closed) { + // Especially for open extrusion, we need to select a starting point that is at the start + // or the end of the extrusions to make one continuous line. Also, we prefer a non-overhang + // starting point. + struct PointInfo + { + size_t occurrence = 0; + bool is_overhang = false; + }; + std::unordered_map point_occurrence; + for (const ExtrusionPath& path : paths) { + ++point_occurrence[path.polyline.first_point()].occurrence; + ++point_occurrence[path.polyline.last_point()].occurrence; + if (path.role() == erOverhangPerimeter) { + point_occurrence[path.polyline.first_point()].is_overhang = true; + point_occurrence[path.polyline.last_point()].is_overhang = true; + } + } + + // Prefer non-overhang point as a starting point. + for (const std::pair pt : point_occurrence) + if (pt.second.occurrence == 1) { + start_point = pt.first; + if (!pt.second.is_overhang) { + start_point = pt.first; + break; + } + } + } + + chain_and_reorder_extrusion_paths(paths, &start_point); + } + } + else { + extrusion_paths_append(paths, *extrusion, role, is_external ? perimeter_generator.ext_perimeter_flow : perimeter_generator.perimeter_flow); + } + + // Append paths to collection. + if (!paths.empty()) { + if (extrusion->is_closed) { + ExtrusionLoop extrusion_loop(std::move(paths)); + // Restore the orientation of the extrusion loop. + if (pg_extrusion.is_contour) + extrusion_loop.make_counter_clockwise(); + else + extrusion_loop.make_clockwise(); + + for (auto it = std::next(extrusion_loop.paths.begin()); it != extrusion_loop.paths.end(); ++it) { + assert(it->polyline.points.size() >= 2); + assert(std::prev(it)->polyline.last_point() == it->polyline.first_point()); + } + assert(extrusion_loop.paths.front().first_point() == extrusion_loop.paths.back().last_point()); + + extrusion_coll.append(std::move(extrusion_loop)); + } + else { + // Because we are processing one ExtrusionLine all ExtrusionPaths should form one connected path. + // But there is possibility that due to numerical issue there is poss + assert([&paths = std::as_const(paths)]() -> bool { + for (auto it = std::next(paths.begin()); it != paths.end(); ++it) + if (std::prev(it)->polyline.last_point() != it->polyline.first_point()) + return false; + return true; + }()); + ExtrusionMultiPath multi_path; + multi_path.paths.emplace_back(std::move(paths.front())); + + for (auto it_path = std::next(paths.begin()); it_path != paths.end(); ++it_path) { + if (multi_path.paths.back().last_point() != it_path->first_point()) { + extrusion_coll.append(ExtrusionMultiPath(std::move(multi_path))); + multi_path = ExtrusionMultiPath(); + } + multi_path.paths.emplace_back(std::move(*it_path)); + } + + extrusion_coll.append(ExtrusionMultiPath(std::move(multi_path))); + } + } + } + + return extrusion_coll; +} + + + +void PerimeterGenerator::process_classic() { // other perimeters m_mm3_per_mm = this->perimeter_flow.mm3_per_mm(); @@ -798,6 +1066,261 @@ void PerimeterGenerator::process() } // for each island } +// Thanks, Cura developers, for implementing an algorithm for generating perimeters with variable width (Arachne) that is based on the paper +// "A framework for adaptive width control of dense contour-parallel toolpaths in fused deposition modeling" +void PerimeterGenerator::process_arachne() +{ + // other perimeters + m_mm3_per_mm = this->perimeter_flow.mm3_per_mm(); + coord_t perimeter_spacing = this->perimeter_flow.scaled_spacing(); + + // external perimeters + m_ext_mm3_per_mm = this->ext_perimeter_flow.mm3_per_mm(); + coord_t ext_perimeter_width = this->ext_perimeter_flow.scaled_width(); + coord_t ext_perimeter_spacing = this->ext_perimeter_flow.scaled_spacing(); + coord_t ext_perimeter_spacing2 = scaled(0.5f * (this->ext_perimeter_flow.spacing() + this->perimeter_flow.spacing())); + + // overhang perimeters + m_mm3_per_mm_overhang = this->overhang_flow.mm3_per_mm(); + + // solid infill + coord_t solid_infill_spacing = this->solid_infill_flow.scaled_spacing(); + + // prepare grown lower layer slices for overhang detection + if (this->lower_slices != nullptr && this->config->detect_overhang_wall) { + // We consider overhang any part where the entire nozzle diameter is not supported by the + // lower layer, so we take lower slices and offset them by half the nozzle diameter used + // in the current layer + double nozzle_diameter = this->print_config->nozzle_diameter.get_at(this->config->wall_filament - 1); + m_lower_slices_polygons = offset(*this->lower_slices, float(scale_(+nozzle_diameter / 2))); + } + + // we need to process each island separately because we might have different + // extra perimeters for each one + for (const Surface& surface : this->slices->surfaces) { + // detect how many perimeters must be generated for this island + int loop_number = this->config->wall_loops + surface.extra_perimeters - 1; // 0-indexed loops + ExPolygons last = offset_ex(surface.expolygon.simplify_p(m_scaled_resolution), -float(ext_perimeter_width / 2. - ext_perimeter_spacing / 2.)); + Polygons last_p = to_polygons(last); + + double min_nozzle_diameter = *std::min_element(print_config->nozzle_diameter.values.begin(), print_config->nozzle_diameter.values.end()); + Arachne::WallToolPathsParams input_params; + { + if (const auto& min_feature_size_opt = object_config->min_feature_size) + input_params.min_feature_size = min_feature_size_opt.value * 0.01 * min_nozzle_diameter; + + if (const auto& min_bead_width_opt = object_config->min_bead_width) + input_params.min_bead_width = min_bead_width_opt.value * 0.01 * min_nozzle_diameter; + + if (const auto& wall_transition_filter_deviation_opt = object_config->wall_transition_filter_deviation) + input_params.wall_transition_filter_deviation = wall_transition_filter_deviation_opt.value * 0.01 * min_nozzle_diameter; + + if (const auto& wall_transition_length_opt = object_config->wall_transition_length) + input_params.wall_transition_length = wall_transition_length_opt.value * 0.01 * min_nozzle_diameter; + + input_params.wall_transition_angle = this->object_config->wall_transition_angle.value; + input_params.wall_distribution_count = this->object_config->wall_distribution_count.value; + } + + Arachne::WallToolPaths wallToolPaths(last_p, ext_perimeter_spacing, perimeter_spacing, coord_t(loop_number + 1), 0, layer_height, input_params); + std::vector perimeters = wallToolPaths.getToolPaths(); + loop_number = int(perimeters.size()) - 1; + +#ifdef ARACHNE_DEBUG + { + static int iRun = 0; + export_perimeters_to_svg(debug_out_path("arachne-perimeters-%d-%d.svg", layer_id, iRun++), to_polygons(last), perimeters, union_ex(wallToolPaths.getInnerContour())); + } +#endif + + // All closed ExtrusionLine should have the same the first and the last point. + // But in rare cases, Arachne produce ExtrusionLine marked as closed but without + // equal the first and the last point. + assert([&perimeters = std::as_const(perimeters)]() -> bool { + for (const Arachne::VariableWidthLines& perimeter : perimeters) + for (const Arachne::ExtrusionLine& el : perimeter) + if (el.is_closed && el.junctions.front().p != el.junctions.back().p) + return false; + return true; + }()); + + int start_perimeter = int(perimeters.size()) - 1; + int end_perimeter = -1; + int direction = -1; + + bool is_outer_wall_first = + this->print_config->wall_infill_order == WallInfillOrder::OuterInnerInfill || + this->print_config->wall_infill_order == WallInfillOrder::InfillOuterInner; + if (is_outer_wall_first) { + start_perimeter = 0; + end_perimeter = int(perimeters.size()); + direction = 1; + } + + std::vector all_extrusions; + for (int perimeter_idx = start_perimeter; perimeter_idx != end_perimeter; perimeter_idx += direction) { + if (perimeters[perimeter_idx].empty()) + continue; + for (Arachne::ExtrusionLine& wall : perimeters[perimeter_idx]) + all_extrusions.emplace_back(&wall); + } + + // Find topological order with constraints from extrusions_constrains. + std::vector blocked(all_extrusions.size(), 0); // Value indicating how many extrusions it is blocking (preceding extrusions) an extrusion. + std::vector> blocking(all_extrusions.size()); // Each extrusion contains a vector of extrusions that are blocked by this extrusion. + std::unordered_map map_extrusion_to_idx; + for (size_t idx = 0; idx < all_extrusions.size(); idx++) + map_extrusion_to_idx.emplace(all_extrusions[idx], idx); + + auto extrusions_constrains = Arachne::WallToolPaths::getRegionOrder(all_extrusions, is_outer_wall_first); + for (auto [before, after] : extrusions_constrains) { + auto after_it = map_extrusion_to_idx.find(after); + ++blocked[after_it->second]; + blocking[map_extrusion_to_idx.find(before)->second].emplace_back(after_it->second); + } + + std::vector processed(all_extrusions.size(), false); // Indicate that the extrusion was already processed. + Point current_position = all_extrusions.empty() ? Point::Zero() : all_extrusions.front()->junctions.front().p; // Some starting position. + std::vector ordered_extrusions; // To store our result in. At the end we'll std::swap. + ordered_extrusions.reserve(all_extrusions.size()); + + while (ordered_extrusions.size() < all_extrusions.size()) { + size_t best_candidate = 0; + double best_distance_sqr = std::numeric_limits::max(); + bool is_best_closed = false; + + std::vector available_candidates; + for (size_t candidate = 0; candidate < all_extrusions.size(); ++candidate) { + if (processed[candidate] || blocked[candidate]) + continue; // Not a valid candidate. + available_candidates.push_back(candidate); + } + + std::sort(available_candidates.begin(), available_candidates.end(), [&all_extrusions](const size_t a_idx, const size_t b_idx) -> bool { + return all_extrusions[a_idx]->is_closed < all_extrusions[b_idx]->is_closed; + }); + + for (const size_t candidate_path_idx : available_candidates) { + auto& path = all_extrusions[candidate_path_idx]; + + if (path->junctions.empty()) { // No vertices in the path. Can't find the start position then or really plan it in. Put that at the end. + if (best_distance_sqr == std::numeric_limits::max()) { + best_candidate = candidate_path_idx; + is_best_closed = path->is_closed; + } + continue; + } + + const Point candidate_position = path->junctions.front().p; + double distance_sqr = (current_position - candidate_position).cast().norm(); + if (distance_sqr < best_distance_sqr) { // Closer than the best candidate so far. + if (path->is_closed || (!path->is_closed && best_distance_sqr != std::numeric_limits::max()) || (!path->is_closed && !is_best_closed)) { + best_candidate = candidate_path_idx; + best_distance_sqr = distance_sqr; + is_best_closed = path->is_closed; + } + } + } + + auto& best_path = all_extrusions[best_candidate]; + ordered_extrusions.push_back({ best_path, best_path->is_contour(), false }); + processed[best_candidate] = true; + for (size_t unlocked_idx : blocking[best_candidate]) + blocked[unlocked_idx]--; + + if (!best_path->junctions.empty()) { //If all paths were empty, the best path is still empty. We don't upate the current position then. + if (best_path->is_closed) + current_position = best_path->junctions[0].p; //We end where we started. + else + current_position = best_path->junctions.back().p; //Pick the other end from where we started. + } + } + + if (this->layer_id > 0 && this->config->fuzzy_skin != FuzzySkinType::None) { + std::vector closed_loop_extrusions; + for (PerimeterGeneratorArachneExtrusion& extrusion : ordered_extrusions) + if (extrusion.extrusion->inset_idx == 0) { + if (extrusion.extrusion->is_closed && this->config->fuzzy_skin == FuzzySkinType::External) { + closed_loop_extrusions.emplace_back(&extrusion); + } + else { + extrusion.fuzzify = true; + } + } + + if (this->config->fuzzy_skin == FuzzySkinType::External) { + ClipperLib_Z::Paths loops_paths; + loops_paths.reserve(closed_loop_extrusions.size()); + for (const auto& cl_extrusion : closed_loop_extrusions) { + assert(cl_extrusion->extrusion->junctions.front() == cl_extrusion->extrusion->junctions.back()); + size_t loop_idx = &cl_extrusion - &closed_loop_extrusions.front(); + ClipperLib_Z::Path loop_path; + loop_path.reserve(cl_extrusion->extrusion->junctions.size() - 1); + for (auto junction_it = cl_extrusion->extrusion->junctions.begin(); junction_it != std::prev(cl_extrusion->extrusion->junctions.end()); ++junction_it) + loop_path.emplace_back(junction_it->p.x(), junction_it->p.y(), loop_idx); + loops_paths.emplace_back(loop_path); + } + + ClipperLib_Z::Clipper clipper; + clipper.AddPaths(loops_paths, ClipperLib_Z::ptSubject, true); + ClipperLib_Z::PolyTree loops_polytree; + clipper.Execute(ClipperLib_Z::ctUnion, loops_polytree, ClipperLib_Z::pftEvenOdd, ClipperLib_Z::pftEvenOdd); + + for (const ClipperLib_Z::PolyNode* child_node : loops_polytree.Childs) { + // The whole contour must have the same index. + coord_t polygon_idx = child_node->Contour.front().z(); + bool has_same_idx = std::all_of(child_node->Contour.begin(), child_node->Contour.end(), + [&polygon_idx](const ClipperLib_Z::IntPoint& point) -> bool { return polygon_idx == point.z(); }); + if (has_same_idx) + closed_loop_extrusions[polygon_idx]->fuzzify = true; + } + } + } + + if (ExtrusionEntityCollection extrusion_coll = traverse_extrusions(*this, ordered_extrusions); !extrusion_coll.empty()) + this->loops->append(extrusion_coll); + + ExPolygons infill_contour = union_ex(wallToolPaths.getInnerContour()); + const coord_t spacing = (perimeters.size() == 1) ? ext_perimeter_spacing2 : perimeter_spacing; + if (offset_ex(infill_contour, -float(spacing / 2.)).empty()) + infill_contour.clear(); // Infill region is too small, so let's filter it out. + + // create one more offset to be used as boundary for fill + // we offset by half the perimeter spacing (to get to the actual infill boundary) + // and then we offset back and forth by half the infill spacing to only consider the + // non-collapsing regions + coord_t inset = + (loop_number < 0) ? 0 : + (loop_number == 0) ? + // one loop + ext_perimeter_spacing : + // two or more loops? + perimeter_spacing; + + inset = coord_t(scale_(this->config->infill_wall_overlap.get_abs_value(unscale(inset)))); + Polygons pp; + for (ExPolygon& ex : infill_contour) + ex.simplify_p(m_scaled_resolution, &pp); + // collapse too narrow infill areas + const auto min_perimeter_infill_spacing = coord_t(solid_infill_spacing * (1. - INSET_OVERLAP_TOLERANCE)); + // append infill areas to fill_surfaces + this->fill_surfaces->append( + offset2_ex( + union_ex(pp), + float(-min_perimeter_infill_spacing / 2.), + float(inset + min_perimeter_infill_spacing / 2.)), + stInternal); + + // BBS: get the no-overlap infill expolygons + { + append(*this->fill_no_overlap, offset2_ex( + union_ex(pp), + float(-min_perimeter_infill_spacing / 2.), + float(+min_perimeter_infill_spacing / 2.))); + } + } +} + bool PerimeterGeneratorLoop::is_internal_contour() const { // An internal contour is a contour containing no other contours diff --git a/src/libslic3r/PerimeterGenerator.hpp b/src/libslic3r/PerimeterGenerator.hpp index 2d64de497..c772a0563 100644 --- a/src/libslic3r/PerimeterGenerator.hpp +++ b/src/libslic3r/PerimeterGenerator.hpp @@ -66,13 +66,15 @@ public: m_ext_mm3_per_mm(-1), m_mm3_per_mm(-1), m_mm3_per_mm_overhang(-1), m_ext_mm3_per_mm_smaller_width(-1) {} - void process(); + void process_classic(); + void process_arachne(); double ext_mm3_per_mm() const { return m_ext_mm3_per_mm; } double mm3_per_mm() const { return m_mm3_per_mm; } double mm3_per_mm_overhang() const { return m_mm3_per_mm_overhang; } //BBS double smaller_width_ext_mm3_per_mm() const { return m_ext_mm3_per_mm_smaller_width; } + Polygons lower_slices_polygons() const { return m_lower_slices_polygons; } private: std::map generate_lower_polygons_series(float width); @@ -85,6 +87,7 @@ private: double m_mm3_per_mm_overhang; //BBS double m_ext_mm3_per_mm_smaller_width; + Polygons m_lower_slices_polygons; }; } diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 47f8d149b..53a939fd3 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -745,7 +745,9 @@ static std::vector s_Preset_print_options { "gcode_add_line_number", "enable_arc_fitting", "infill_combination", /*"adaptive_layer_height",*/ "support_bottom_interface_spacing", "enable_overhang_speed", "overhang_1_4_speed", "overhang_2_4_speed", "overhang_3_4_speed", "overhang_4_4_speed", "initial_layer_infill_speed", "only_one_wall_top", - "timelapse_type", "internal_bridge_support_thickness" + "timelapse_type", "internal_bridge_support_thickness", + "wall_generator", "wall_transition_length", "wall_transition_filter_deviation", "wall_transition_angle", + "wall_distribution_count", "min_feature_size", "min_bead_width" }; static std::vector s_Preset_filament_options { diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 2262191ed..2271ec1f2 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -283,6 +283,12 @@ static t_config_enum_values s_keys_map_NozzleType { }; CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(NozzleType) +static t_config_enum_values s_keys_map_PerimeterGeneratorType{ + { "classic", int(PerimeterGeneratorType::Classic) }, + { "arachne", int(PerimeterGeneratorType::Arachne) } +}; +CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(PerimeterGeneratorType) + static void assign_printer_technology_to_unknown(t_optiondef_map &options, PrinterTechnology printer_technology) { for (std::pair &kvp : options) @@ -2940,6 +2946,92 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionFloat(0)); + def = this->add("wall_generator", coEnum); + def->label = L("Wall generator"); + def->category = L("Quality"); + def->tooltip = L("Classic wall generator produces walls with constant extrusion width and for " + "very thin areas is used gap-fill. " + "Arachne engine produces walls with variable extrusion width"); + def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); + def->enum_values.push_back("classic"); + def->enum_values.push_back("arachne"); + def->enum_labels.push_back(L("Classic")); + def->enum_labels.push_back(L("Arachne")); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionEnum(PerimeterGeneratorType::Classic)); + + def = this->add("wall_transition_length", coPercent); + def->label = L("Wall transition length"); + def->category = L("Quality"); + def->tooltip = L("When transitioning between different numbers of walls as the part becomes " + "thinner, a certain amount of space is allotted to split or join the wall segments. " + "It's expressed as a percentage over nozzle diameter"); + def->sidetext = L("%"); + def->mode = comAdvanced; + def->min = 0; + def->set_default_value(new ConfigOptionPercent(100)); + + def = this->add("wall_transition_filter_deviation", coPercent); + def->label = L("Wall transitioning filter margin"); + def->category = L("Quality"); + def->tooltip = L("Prevent transitioning back and forth between one extra wall and one less. This " + "margin extends the range of extrusion widths which follow to [Minimum perimeter width " + "- margin, 2 * Minimum perimeter width + margin]. Increasing this margin " + "reduces the number of transitions, which reduces the number of extrusion " + "starts/stops and travel time. However, large extrusion width variation can lead to " + "under- or overextrusion problems. " + "It's expressed as a percentage over nozzle diameter"); + def->sidetext = L("%"); + def->mode = comAdvanced; + def->min = 0; + def->set_default_value(new ConfigOptionPercent(25)); + + def = this->add("wall_transition_angle", coFloat); + def->label = L("Perimeter transitioning threshold angle"); + def->category = L("Quality"); + def->tooltip = L("When to create transitions between even and odd numbers of perimeters. A wedge shape with" + " an angle greater than this setting will not have transitions and no perimeters will be " + "printed in the center to fill the remaining space. Reducing this setting reduces " + "the number and length of these center perimeters, but may leave gaps or overextrude"); + def->sidetext = L("°"); + def->mode = comAdvanced; + def->min = 1.; + def->max = 59.; + def->set_default_value(new ConfigOptionFloat(10.)); + + def = this->add("wall_distribution_count", coInt); + def->label = L("Perimeter distribution count"); + def->category = L("Quality"); + def->tooltip = L("The number of perimeters, counted from the center, over which the variation needs to be " + "spread. Lower values mean that the outer perimeters don't change in width"); + def->mode = comAdvanced; + def->min = 1; + def->set_default_value(new ConfigOptionInt(1)); + + def = this->add("min_feature_size", coPercent); + def->label = L("Minimum feature size"); + def->category = L("Quality"); + def->tooltip = L("Minimum thickness of thin features. Model features that are thinner than this value will " + "not be printed, while features thicker than the Minimum feature size will be widened to " + "the Minimum perimeter width. " + "It's expressed as a percentage over nozzle diameter"); + def->sidetext = L("%"); + def->mode = comAdvanced; + def->min = 0; + def->set_default_value(new ConfigOptionPercent(25)); + + def = this->add("min_bead_width", coPercent); + def->label = L("Minimum perimeter width"); + def->category = L("Quality"); + def->tooltip = L("Width of the perimeter that will replace thin features (according to the Minimum feature size) " + "of the model. If the Minimum perimeter width is thinner than the thickness of the feature," + " the perimeter will become as thick as the feature itself. " + "It's expressed as a percentage over nozzle diameter"); + def->sidetext = L("%"); + def->mode = comAdvanced; + def->min = 0; + def->set_default_value(new ConfigOptionPercent(85)); + // Declare retract values for filament profile, overriding the printer's extruder profile. for (const char *opt_key : { // floats diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 8a7cdef49..f88bf0a2e 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -150,6 +150,15 @@ enum DraftShield { dsDisabled, dsLimited, dsEnabled }; +enum class PerimeterGeneratorType +{ + // Classic perimeter generator using Clipper offsets with constant extrusion width. + Classic, + // Perimeter generator with variable extrusion width based on the paper + // "A framework for adaptive width control of dense contour-parallel toolpaths in fused deposition modeling" ported from Cura. + Arachne +}; + // BBS enum OverhangFanThreshold { Overhang_threshold_none = 0, @@ -264,6 +273,7 @@ CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(ForwardCompatibilitySubstitutionRule) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(PrintHostType) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(AuthorizationType) +CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(PerimeterGeneratorType) #undef CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS @@ -654,6 +664,13 @@ PRINT_CONFIG_CLASS_DEFINE( // ((ConfigOptionBool, adaptive_layer_height)) ((ConfigOptionFloat, support_bottom_interface_spacing)) ((ConfigOptionFloat, internal_bridge_support_thickness)) + ((ConfigOptionEnum, wall_generator)) + ((ConfigOptionPercent, wall_transition_length)) + ((ConfigOptionPercent, wall_transition_filter_deviation)) + ((ConfigOptionFloat, wall_transition_angle)) + ((ConfigOptionInt, wall_distribution_count)) + ((ConfigOptionPercent, min_feature_size)) + ((ConfigOptionPercent, min_bead_width)) ) // This object is mapped to Perl as Slic3r::Config::PrintRegion. diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 9abda0ba1..c9ceae688 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -827,6 +827,15 @@ bool PrintObject::invalidate_state_by_config_options( steps.emplace_back(posInfill); steps.emplace_back(posSupportMaterial); } + } else if ( + opt_key == "wall_generator" + || opt_key == "wall_transition_length" + || opt_key == "wall_transition_filter_deviation" + || opt_key == "wall_transition_angle" + || opt_key == "wall_distribution_count" + || opt_key == "min_feature_size" + || opt_key == "min_bead_width") { + steps.emplace_back(posSlice); } else if ( opt_key == "seam_position" || opt_key == "support_speed" diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp index d75e98808..6085b986b 100644 --- a/src/slic3r/GUI/ConfigManipulation.cpp +++ b/src/slic3r/GUI/ConfigManipulation.cpp @@ -313,6 +313,37 @@ void ConfigManipulation::update_print_fff_config(DynamicPrintConfig* config, con is_msg_dlg_already_exist = false; } + //BBS + if (config->opt_enum("wall_generator") == PerimeterGeneratorType::Arachne && + config->opt_bool("enable_overhang_speed")) + { + wxString msg_text = _(L("Arachne engine only works when overhang slowing down is disabled.\n" + "This may cause decline in the quality of overhang surface when print fastly\n")); + if (is_global_config) + msg_text += "\n" + _(L("Disable overhang slowing down automatically? \n" + "Yes - Enable arachne and disable overhang slowing down\n" + "No - Give up using arachne this time")); + MessageDialog dialog(m_msg_dlg_parent, msg_text, "", + wxICON_WARNING | (is_global_config ? wxYES | wxNO : wxOK)); + DynamicPrintConfig new_conf = *config; + is_msg_dlg_already_exist = true; + auto answer = dialog.ShowModal(); + bool enable_overhang_slow_down = true; + if (!is_global_config || answer == wxID_YES) { + new_conf.set_key_value("enable_overhang_speed", new ConfigOptionBool(false)); + enable_overhang_slow_down = false; + } + else { + new_conf.set_key_value("wall_generator", new ConfigOptionEnum(PerimeterGeneratorType::Classic)); + } + apply(config, &new_conf); + if (cb_value_change) { + if (!enable_overhang_slow_down) + cb_value_change("enable_overhang_speed", false); + } + is_msg_dlg_already_exist = false; + } + // BBS int filament_cnt = wxGetApp().preset_bundle->filament_presets.size(); #if 0 @@ -607,6 +638,14 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, co PresetBundle *preset_bundle = wxGetApp().preset_bundle; std::string str_preset_type = preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle); toggle_field("timelapse_type", str_preset_type != "C11"); + + bool have_arachne = config->opt_enum("wall_generator") == PerimeterGeneratorType::Arachne; + for (auto el : { "wall_transition_length", "wall_transition_filter_deviation", "wall_transition_angle", + "min_feature_size", "min_bead_width", "wall_distribution_count" }) + toggle_line(el, have_arachne); + toggle_field("detect_thin_wall", !have_arachne); + toggle_field("enable_overhang_speed", !have_arachne); + toggle_field("only_one_wall_top", !have_arachne); } void ConfigManipulation::update_print_sla_config(DynamicPrintConfig* config, const bool is_global_config/* = false*/) diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index b543ecd5b..7c8a9d909 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -1788,6 +1788,15 @@ void TabPrint::build() optgroup->append_single_option_line("ironing_flow"); optgroup->append_single_option_line("ironing_spacing"); + optgroup = page->new_optgroup(L("Wall generator"), L"param_wall"); + optgroup->append_single_option_line("wall_generator"); + optgroup->append_single_option_line("wall_transition_angle"); + optgroup->append_single_option_line("wall_transition_filter_deviation"); + optgroup->append_single_option_line("wall_transition_length"); + optgroup->append_single_option_line("wall_distribution_count"); + optgroup->append_single_option_line("min_bead_width"); + optgroup->append_single_option_line("min_feature_size"); + optgroup = page->new_optgroup(L("Advanced"), L"param_advanced"); optgroup->append_single_option_line("wall_infill_order"); optgroup->append_single_option_line("bridge_flow");