From adf39805bce53ca896b24690703879229f946ee7 Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Mon, 4 Apr 2022 12:35:29 +0200 Subject: [PATCH 001/100] work in progress: hooked in new step: posSupportableIssuesSearch created layout of the processing --- src/libslic3r/CMakeLists.txt | 2 + src/libslic3r/Print.cpp | 2 + src/libslic3r/Print.hpp | 3 +- src/libslic3r/PrintObject.cpp | 150 +++++++++++++--------- src/libslic3r/SupportableIssuesSearch.cpp | 149 +++++++++++++++++++++ src/libslic3r/SupportableIssuesSearch.hpp | 16 +++ 6 files changed, 257 insertions(+), 65 deletions(-) create mode 100644 src/libslic3r/SupportableIssuesSearch.cpp create mode 100644 src/libslic3r/SupportableIssuesSearch.hpp diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index 01a6a3aa2a..a342da8319 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -245,6 +245,8 @@ set(SLIC3R_SOURCES SlicingAdaptive.hpp Subdivide.cpp Subdivide.hpp + SupportableIssuesSearch.cpp + SupportableIssuesSearch.hpp SupportMaterial.cpp SupportMaterial.hpp Surface.cpp diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 9792a6968b..a50974cafd 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -825,6 +825,8 @@ void Print::process() obj->infill(); for (PrintObject *obj : m_objects) obj->ironing(); + for (PrintObject *obj : m_objects) + obj->find_supportable_issues(); for (PrintObject *obj : m_objects) obj->generate_support_material(); if (this->set_started(psWipeTower)) { diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index cb01600fa5..c89b463a8f 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -61,7 +61,7 @@ enum PrintStep : unsigned int { enum PrintObjectStep : unsigned int { posSlice, posPerimeters, posPrepareInfill, - posInfill, posIroning, posSupportMaterial, posCount, + posInfill, posIroning, posSupportableIssuesSearch, posSupportMaterial, posCount, }; // A PrintRegion object represents a group of volumes to print @@ -381,6 +381,7 @@ private: void prepare_infill(); void infill(); void ironing(); + void find_supportable_issues(); void generate_support_material(); void slice_volumes(); diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 6ec27ea95b..4bcf74fe5e 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -16,6 +16,7 @@ #include "Fill/FillAdaptive.hpp" #include "Fill/FillLightning.hpp" #include "Format/STL.hpp" +#include "SupportableIssuesSearch.hpp" #include #include @@ -89,7 +90,7 @@ PrintBase::ApplyStatus PrintObject::set_instances(PrintInstances &&instances) // Invalidate and set copies. PrintBase::ApplyStatus status = PrintBase::APPLY_STATUS_UNCHANGED; bool equal_length = instances.size() == m_instances.size(); - bool equal = equal_length && std::equal(instances.begin(), instances.end(), m_instances.begin(), + bool equal = equal_length && std::equal(instances.begin(), instances.end(), m_instances.begin(), [](const PrintInstance& lhs, const PrintInstance& rhs) { return lhs.model_instance == rhs.model_instance && lhs.shift == rhs.shift; }); if (! equal) { status = PrintBase::APPLY_STATUS_CHANGED; @@ -125,7 +126,7 @@ void PrintObject::make_perimeters() m_print->set_status(20, L("Generating perimeters")); BOOST_LOG_TRIVIAL(info) << "Generating perimeters..." << log_memory_info(); - + // Revert the typed slices into untyped slices. if (m_typed_slices) { for (Layer *layer : m_layers) { @@ -134,10 +135,10 @@ void PrintObject::make_perimeters() } m_typed_slices = false; } - + // compare each layer to the one below, and mark those slices needing // one additional inner perimeter, like the top of domed objects- - + // this algorithm makes sure that at least one perimeter is overlapping // but we don't generate any extra perimeter if fill density is zero, as they would be floating // inside the object - infill_only_where_needed should be the method of choice for printing @@ -245,7 +246,7 @@ void PrintObject::prepare_infill() // by the cummulative area of the previous $layerm->fill_surfaces. this->detect_surfaces_type(); m_print->throw_if_canceled(); - + // Decide what surfaces are to be filled. // Here the stTop / stBottomBridge / stBottom infill is turned to just stInternal if zero top / bottom infill layers are configured. // Also tiny stInternal surfaces are turned to stInternalSolid. @@ -320,7 +321,7 @@ void PrintObject::prepare_infill() } // for each layer } // for each region #endif /* SLIC3R_DEBUG_SLICE_PROCESSING */ - + // the following step needs to be done before combination because it may need // to remove only half of the combined infill this->bridge_over_infill(); @@ -395,12 +396,33 @@ void PrintObject::ironing() } } +void PrintObject::find_supportable_issues() +{ + if (this->set_started(posSupportableIssuesSearch)) { + BOOST_LOG_TRIVIAL(debug) + << "Searching supportable issues - start"; + //TODO status number? + m_print->set_status(70, L("Searching supportable issues")); + + if (this->has_support()) { + + } else { + SupportableIssues::quick_search(this); + } + + m_print->throw_if_canceled(); + BOOST_LOG_TRIVIAL(debug) + << "Searching supportable issues - end"; + this->set_done(posSupportableIssuesSearch); + } +} + void PrintObject::generate_support_material() { if (this->set_started(posSupportMaterial)) { this->clear_support_layers(); if ((this->has_support() && m_layers.size() > 1) || (this->has_raft() && ! m_layers.empty())) { - m_print->set_status(85, L("Generating support material")); + m_print->set_status(85, L("Generating support material")); this->_generate_support_material(); m_print->throw_if_canceled(); } else { @@ -560,7 +582,7 @@ bool PrintObject::invalidate_state_by_config_options( } else if ( opt_key == "clip_multipart_objects" || opt_key == "elefant_foot_compensation" - || opt_key == "support_material_contact_distance" + || opt_key == "support_material_contact_distance" || opt_key == "xy_size_compensation") { steps.emplace_back(posSlice); } else if (opt_key == "support_material") { @@ -711,7 +733,7 @@ bool PrintObject::invalidate_state_by_config_options( bool PrintObject::invalidate_step(PrintObjectStep step) { bool invalidated = Inherited::invalidate_step(step); - + // propagate to dependent steps if (step == posPerimeters) { invalidated |= this->invalidate_steps({ posPrepareInfill, posInfill, posIroning }); @@ -784,7 +806,7 @@ void PrintObject::detect_surfaces_type() surfaces_new.assign(num_layers, Surfaces()); tbb::parallel_for( - tbb::blocked_range(0, + tbb::blocked_range(0, spiral_vase ? // In spiral vase mode, reserve the last layer for the top surface if more than 1 layer is planned for the vase bottom. ((num_layers > 1) ? num_layers - 1 : num_layers) : @@ -812,7 +834,7 @@ void PrintObject::detect_surfaces_type() // of current layer and upper one) Surfaces top; if (upper_layer) { - ExPolygons upper_slices = interface_shells ? + ExPolygons upper_slices = interface_shells ? diff_ex(layerm->slices.surfaces, upper_layer->m_regions[region_id]->slices.surfaces, ApplySafetyOffset::Yes) : diff_ex(layerm->slices.surfaces, upper_layer->lslices, ApplySafetyOffset::Yes); surfaces_append(top, opening_ex(upper_slices, offset), stTop); @@ -823,7 +845,7 @@ void PrintObject::detect_surfaces_type() for (Surface &surface : top) surface.surface_type = stTop; } - + // Find bottom surfaces (difference between current surfaces of current layer and lower one). Surfaces bottom; if (lower_layer) { @@ -846,7 +868,7 @@ void PrintObject::detect_surfaces_type() // if user requested internal shells, we need to identify surfaces // lying on other slices not belonging to this region if (interface_shells) { - // non-bridging bottom surfaces: any part of this layer lying + // non-bridging bottom surfaces: any part of this layer lying // on something else, excluding those lying on our own region surfaces_append( bottom, @@ -866,7 +888,7 @@ void PrintObject::detect_surfaces_type() for (Surface &surface : bottom) surface.surface_type = stBottom; } - + // now, if the object contained a thin membrane, we could have overlapping bottom // and top surfaces; let's do an intersection to discover them and consider them // as bottom surfaces (to allow for bridge detection) @@ -889,7 +911,7 @@ void PrintObject::detect_surfaces_type() SVG::export_expolygons(debug_out_path("1_detect_surfaces_type_%d_region%d-layer_%f.svg", iRun ++, region_id, layer->print_z).c_str(), expolygons_with_attributes); } #endif /* SLIC3R_DEBUG_SLICE_PROCESSING */ - + // save surfaces to layer Surfaces &surfaces_out = interface_shells ? surfaces_new[idx_layer] : layerm->slices.surfaces; Surfaces surfaces_backup; @@ -908,7 +930,7 @@ void PrintObject::detect_surfaces_type() surfaces_append(surfaces_out, std::move(top)); surfaces_append(surfaces_out, std::move(bottom)); - + // Slic3r::debugf " layer %d has %d bottom, %d top and %d internal surfaces\n", // $layerm->layer->id, scalar(@bottom), scalar(@top), scalar(@internal) if $Slic3r::debug; @@ -1216,7 +1238,7 @@ void PrintObject::discover_vertical_shells() #endif /* SLIC3R_DEBUG_SLICE_PROCESSING */ Flow solid_infill_flow = layerm->flow(frSolidInfill); - coord_t infill_line_spacing = solid_infill_flow.scaled_spacing(); + coord_t infill_line_spacing = solid_infill_flow.scaled_spacing(); // Find a union of perimeters below / above this surface to guarantee a minimum shell thickness. Polygons shell; Polygons holes; @@ -1252,8 +1274,8 @@ void PrintObject::discover_vertical_shells() if (int n_top_layers = region_config.top_solid_layers.value; n_top_layers > 0) { // Gather top regions projected to this layer. coordf_t print_z = layer->print_z; - for (int i = int(idx_layer) + 1; - i < int(cache_top_botom_regions.size()) && + for (int i = int(idx_layer) + 1; + i < int(cache_top_botom_regions.size()) && (i < int(idx_layer) + n_top_layers || m_layers[i]->print_z - print_z < region_config.top_solid_min_thickness - EPSILON); ++ i) { @@ -1262,7 +1284,7 @@ void PrintObject::discover_vertical_shells() holes = intersection(holes, cache.holes); if (! cache.top_surfaces.empty()) { polygons_append(shell, cache.top_surfaces); - // Running the union_ using the Clipper library piece by piece is cheaper + // Running the union_ using the Clipper library piece by piece is cheaper // than running the union_ all at once. shell = union_(shell); } @@ -1281,7 +1303,7 @@ void PrintObject::discover_vertical_shells() holes = intersection(holes, cache.holes); if (! cache.bottom_surfaces.empty()) { polygons_append(shell, cache.bottom_surfaces); - // Running the union_ using the Clipper library piece by piece is cheaper + // Running the union_ using the Clipper library piece by piece is cheaper // than running the union_ all at once. shell = union_(shell); } @@ -1292,14 +1314,14 @@ void PrintObject::discover_vertical_shells() Slic3r::SVG svg(debug_out_path("discover_vertical_shells-perimeters-before-union-%d.svg", debug_idx), get_extents(shell)); svg.draw(shell); svg.draw_outline(shell, "black", scale_(0.05)); - svg.Close(); + svg.Close(); } #endif /* SLIC3R_DEBUG_SLICE_PROCESSING */ #if 0 { PROFILE_BLOCK(discover_vertical_shells_region_layer_shell_); // shell = union_(shell, true); - shell = union_(shell, false); + shell = union_(shell, false); } #endif #ifdef SLIC3R_DEBUG_SLICE_PROCESSING @@ -1315,7 +1337,7 @@ void PrintObject::discover_vertical_shells() Slic3r::SVG svg(debug_out_path("discover_vertical_shells-perimeters-after-union-%d.svg", debug_idx), get_extents(shell)); svg.draw(shell_ex); svg.draw_outline(shell_ex, "black", "blue", scale_(0.05)); - svg.Close(); + svg.Close(); } #endif /* SLIC3R_DEBUG_SLICE_PROCESSING */ @@ -1327,7 +1349,7 @@ void PrintObject::discover_vertical_shells() svg.draw(shell_ex, "blue", 0.5); svg.draw_outline(shell_ex, "black", "blue", scale_(0.05)); svg.Close(); - } + } { Slic3r::SVG svg(debug_out_path("discover_vertical_shells-internalvoid-wshell-%d.svg", debug_idx), get_extents(shell)); svg.draw(layerm->fill_surfaces.filter_by_type(stInternalVoid), "yellow", 0.5); @@ -1335,15 +1357,15 @@ void PrintObject::discover_vertical_shells() svg.draw(shell_ex, "blue", 0.5); svg.draw_outline(shell_ex, "black", "blue", scale_(0.05)); svg.Close(); - } + } { Slic3r::SVG svg(debug_out_path("discover_vertical_shells-internalvoid-wshell-%d.svg", debug_idx), get_extents(shell)); svg.draw(layerm->fill_surfaces.filter_by_type(stInternalVoid), "yellow", 0.5); svg.draw_outline(layerm->fill_surfaces.filter_by_type(stInternalVoid), "black", "blue", scale_(0.05)); svg.draw(shell_ex, "blue", 0.5); - svg.draw_outline(shell_ex, "black", "blue", scale_(0.05)); + svg.draw_outline(shell_ex, "black", "blue", scale_(0.05)); svg.Close(); - } + } #endif /* SLIC3R_DEBUG_SLICE_PROCESSING */ // Trim the shells region by the internal & internal void surfaces. @@ -1364,7 +1386,7 @@ void PrintObject::discover_vertical_shells() Polygons shell_before = shell; #endif /* SLIC3R_DEBUG_SLICE_PROCESSING */ #if 1 - // Intentionally inflate a bit more than how much the region has been shrunk, + // Intentionally inflate a bit more than how much the region has been shrunk, // so there will be some overlap between this solid infill and the other infill regions (mainly the sparse infill). shell = opening(union_(shell), 0.5f * min_perimeter_infill_spacing, 0.8f * min_perimeter_infill_spacing, ClipperLib::jtSquare); if (shell.empty()) @@ -1446,7 +1468,7 @@ void PrintObject::bridge_over_infill() for (size_t region_id = 0; region_id < this->num_printing_regions(); ++ region_id) { const PrintRegion ®ion = this->printing_region(region_id); - + // skip bridging in case there are no voids if (region.config().fill_density.value == 100) continue; @@ -1455,7 +1477,7 @@ void PrintObject::bridge_over_infill() // skip first layer if (layer_it == m_layers.begin()) continue; - + Layer *layer = *layer_it; LayerRegion *layerm = layer->m_regions[region_id]; Flow bridge_flow = layerm->bridging_flow(frSolidInfill); @@ -1463,31 +1485,31 @@ void PrintObject::bridge_over_infill() // extract the stInternalSolid surfaces that might be transformed into bridges Polygons internal_solid; layerm->fill_surfaces.filter_by_type(stInternalSolid, &internal_solid); - + // check whether the lower area is deep enough for absorbing the extra flow // (for obvious physical reasons but also for preventing the bridge extrudates // from overflowing in 3D preview) ExPolygons to_bridge; { Polygons to_bridge_pp = internal_solid; - + // iterate through lower layers spanned by bridge_flow double bottom_z = layer->print_z - bridge_flow.height() - EPSILON; for (int i = int(layer_it - m_layers.begin()) - 1; i >= 0; --i) { const Layer* lower_layer = m_layers[i]; - + // stop iterating if layer is lower than bottom_z if (lower_layer->print_z < bottom_z) break; - + // iterate through regions and collect internal surfaces Polygons lower_internal; for (LayerRegion *lower_layerm : lower_layer->m_regions) lower_layerm->fill_surfaces.filter_by_type(stInternal, &lower_internal); - + // intersect such lower internal surfaces with the candidate solid surfaces to_bridge_pp = intersection(to_bridge_pp, lower_internal); } - + // there's no point in bridging too thin/short regions //FIXME Vojtech: The offset2 function is not a geometric offset, // therefore it may create 1) gaps, and 2) sharp corners, which are outside the original contour. @@ -1496,17 +1518,17 @@ void PrintObject::bridge_over_infill() float min_width = float(bridge_flow.scaled_width()) * 3.f; to_bridge_pp = opening(to_bridge_pp, min_width); } - + if (to_bridge_pp.empty()) continue; - + // convert into ExPolygons to_bridge = union_ex(to_bridge_pp); } - + #ifdef SLIC3R_DEBUG printf("Bridging %zu internal areas at layer %zu\n", to_bridge.size(), layer->id()); #endif - + // compute the remaning internal solid surfaces as difference ExPolygons not_to_bridge = diff_ex(internal_solid, to_bridge, ApplySafetyOffset::Yes); to_bridge = intersection_ex(to_bridge, internal_solid, ApplySafetyOffset::Yes); @@ -1515,7 +1537,7 @@ void PrintObject::bridge_over_infill() for (ExPolygon &ex : to_bridge) layerm->fill_surfaces.surfaces.push_back(Surface(stInternalBridge, ex)); for (ExPolygon &ex : not_to_bridge) - layerm->fill_surfaces.surfaces.push_back(Surface(stInternalSolid, ex)); + layerm->fill_surfaces.surfaces.push_back(Surface(stInternalSolid, ex)); /* # exclude infill from the layers below if needed # see discussion at https://github.com/alexrj/Slic3r/issues/240 @@ -1544,7 +1566,7 @@ void PrintObject::bridge_over_infill() $lower_layerm->fill_surfaces->clear; $lower_layerm->fill_surfaces->append($_) for @new_surfaces; } - + $excess -= $self->get_layer($i)->height; } } @@ -1634,7 +1656,7 @@ PrintRegionConfig region_config_from_model_volume(const PrintRegionConfig &defau // Switch of infill for very low infill rates, also avoid division by zero in infill generator for these very low rates. // See GH issue #5910. config.fill_density.value = 0; - else + else config.fill_density.value = std::min(config.fill_density.value, 100.); if (config.fuzzy_skin.value != FuzzySkinType::None && (config.fuzzy_skin_point_dist.value < 0.01 || config.fuzzy_skin_thickness.value < 0.001)) config.fuzzy_skin.value = FuzzySkinType::None; @@ -1793,7 +1815,7 @@ void PrintObject::clip_fill_surfaces() // Regularize the overhang regions, so that the infill areas will not become excessively jagged. smooth_outward( closing(upper_internal, closing_radius, ClipperLib::jtSquare, 0.), - scaled(0.1)), + scaled(0.1)), lower_layer_internal_surfaces); // Apply new internal infill to regions. for (LayerRegion *layerm : lower_layer->m_regions) { @@ -1821,7 +1843,7 @@ void PrintObject::clip_fill_surfaces() void PrintObject::discover_horizontal_shells() { BOOST_LOG_TRIVIAL(trace) << "discover_horizontal_shells()"; - + for (size_t region_id = 0; region_id < this->num_printing_regions(); ++ region_id) { for (size_t i = 0; i < m_layers.size(); ++ i) { m_print->throw_if_canceled(); @@ -1840,7 +1862,7 @@ void PrintObject::discover_horizontal_shells() // If ensure_vertical_shell_thickness, then the rest has already been performed by discover_vertical_shells(). if (region_config.ensure_vertical_shell_thickness.value) continue; - + coordf_t print_z = layer->print_z; coordf_t bottom_z = layer->bottom_z(); for (size_t idx_surface_type = 0; idx_surface_type < 3; ++ idx_surface_type) { @@ -1873,11 +1895,11 @@ void PrintObject::discover_horizontal_shells() if (solid.empty()) continue; // Slic3r::debugf "Layer %d has %s surfaces\n", $i, ($type == stTop) ? 'top' : 'bottom'; - + // Scatter top / bottom regions to other layers. Scattering process is inherently serial, it is difficult to parallelize without locking. for (int n = (type == stTop) ? int(i) - 1 : int(i) + 1; (type == stTop) ? - (n >= 0 && (int(i) - n < num_solid_layers || + (n >= 0 && (int(i) - n < num_solid_layers || print_z - m_layers[n]->print_z < region_config.top_solid_min_thickness.value - EPSILON)) : (n < int(m_layers.size()) && (n - int(i) < num_solid_layers || m_layers[n]->bottom_z() - bottom_z < region_config.bottom_solid_min_thickness.value - EPSILON)); @@ -1886,7 +1908,7 @@ void PrintObject::discover_horizontal_shells() // Slic3r::debugf " looking for neighbors on layer %d...\n", $n; // Reference to the lower layer of a TOP surface, or an upper layer of a BOTTOM surface. LayerRegion *neighbor_layerm = m_layers[n]->regions()[region_id]; - + // find intersection between neighbor and current layer's surfaces // intersections have contours and holes // we update $solid so that we limit the next neighbor layer to the areas that were @@ -1921,7 +1943,7 @@ void PrintObject::discover_horizontal_shells() continue; } } - + if (region_config.fill_density.value == 0) { // if we're printing a hollow object we discard any solid shell thinner // than a perimeter width, since it's probably just crossing a sloping wall @@ -1929,7 +1951,7 @@ void PrintObject::discover_horizontal_shells() // obeying the solid shell count option strictly (DWIM!) float margin = float(neighbor_layerm->flow(frExternalPerimeter).scaled_width()); Polygons too_narrow = diff( - new_internal_solid, + new_internal_solid, opening(new_internal_solid, margin, margin + ClipperSafetyOffset, jtMiter, 5)); // Trim the regularized region by the original region. if (! too_narrow.empty()) @@ -1959,20 +1981,20 @@ void PrintObject::discover_horizontal_shells() for (const Surface &surface : neighbor_layerm->fill_surfaces.surfaces) if (surface.is_internal() && !surface.is_bridge()) polygons_append(internal, to_polygons(surface.expolygon)); - polygons_append(new_internal_solid, + polygons_append(new_internal_solid, intersection( expand(too_narrow, +margin), // Discard bridges as they are grown for anchoring and we can't - // remove such anchors. (This may happen when a bridge is being + // remove such anchors. (This may happen when a bridge is being // anchored onto a wall where little space remains after the bridge - // is grown, and that little space is an internal solid shell so + // is grown, and that little space is an internal solid shell so // it triggers this too_narrow logic.) internal)); // see https://github.com/prusa3d/PrusaSlicer/pull/3426 // solid = new_internal_solid; } } - + // internal-solid are the union of the existing internal-solid surfaces // and new ones SurfaceCollection backup = std::move(neighbor_layerm->fill_surfaces); @@ -2051,11 +2073,11 @@ void PrintObject::combine_infill() current_height += layer->height; ++ num_layers; } - + // Append lower layers (if any) to uppermost layer. combine[m_layers.size() - 1] = num_layers; } - + // loop through layers to which we have assigned layers to combine for (size_t layer_idx = 0; layer_idx < m_layers.size(); ++ layer_idx) { m_print->throw_if_canceled(); @@ -2075,8 +2097,8 @@ void PrintObject::combine_infill() intersection = intersection_ex(layerms[i]->fill_surfaces.filter_by_type(stInternal), intersection); double area_threshold = layerms.front()->infill_area_threshold(); if (! intersection.empty() && area_threshold > 0.) - intersection.erase(std::remove_if(intersection.begin(), intersection.end(), - [area_threshold](const ExPolygon &expoly) { return expoly.area() <= area_threshold; }), + intersection.erase(std::remove_if(intersection.begin(), intersection.end(), + [area_threshold](const ExPolygon &expoly) { return expoly.area() <= area_threshold; }), intersection.end()); if (intersection.empty()) continue; @@ -2088,15 +2110,15 @@ void PrintObject::combine_infill() // so let's remove those areas from all layers. Polygons intersection_with_clearance; intersection_with_clearance.reserve(intersection.size()); - float clearance_offset = + float clearance_offset = 0.5f * layerms.back()->flow(frPerimeter).scaled_width() + - // Because fill areas for rectilinear and honeycomb are grown + // Because fill areas for rectilinear and honeycomb are grown // later to overlap perimeters, we need to counteract that too. ((region.config().fill_pattern == ipRectilinear || region.config().fill_pattern == ipMonotonic || region.config().fill_pattern == ipGrid || region.config().fill_pattern == ipLine || - region.config().fill_pattern == ipHoneycomb) ? 1.5f : 0.5f) * + region.config().fill_pattern == ipHoneycomb) ? 1.5f : 0.5f) * layerms.back()->flow(frSolidInfill).scaled_width(); for (ExPolygon &expoly : intersection) polygons_append(intersection_with_clearance, offset(expoly, clearance_offset)); @@ -2308,7 +2330,7 @@ static void project_triangles_to_slabs(ConstLayerPtrsAdaptor layers, const index // The resulting triangles are fed to the Clipper library, which seem to handle flipped triangles well. // if (cross2(Vec2d((poly.pts[1] - poly.pts[0]).cast()), Vec2d((poly.pts[2] - poly.pts[1]).cast())) < 0) // std::swap(poly.pts.front(), poly.pts.back()); - + out[layer_id].emplace_back(std::move(poly.pts)); ++layer_id; } diff --git a/src/libslic3r/SupportableIssuesSearch.cpp b/src/libslic3r/SupportableIssuesSearch.cpp new file mode 100644 index 0000000000..bace1aea00 --- /dev/null +++ b/src/libslic3r/SupportableIssuesSearch.cpp @@ -0,0 +1,149 @@ +#include "SupportableIssuesSearch.hpp" + +#include "tbb/parallel_for.h" +#include "tbb/blocked_range.h" +#include "tbb/parallel_reduce.h" +#include + +#include "libslic3r/Layer.hpp" +#include "libslic3r/EdgeGrid.hpp" +#include "libslic3r/ClipperUtils.hpp" + +namespace Slic3r { +namespace SupportableIssues { + +struct Params { + float bridge_distance = 5.0f; + float printable_protrusion_distance = 1.0f; +}; + +namespace Impl { + +struct LayerDescriptor { + Vec2f centroid { 0.0f, 0.0f }; + size_t segments_count { 0 }; + float perimeter_length { 0.0f }; +}; + +struct EdgeGridWrapper { + EdgeGridWrapper(coord_t resolution, ExPolygons ex_polys) : + ex_polys(ex_polys) { + + grid.create(this->ex_polys, resolution); + grid.calculate_sdf(); + } + EdgeGrid::Grid grid; + ExPolygons ex_polys; +} +; + +EdgeGridWrapper compute_layer_merged_edge_grid(const Layer *layer) { + static const float eps = float(scale_(layer->object()->config().slice_closing_radius.value)); + // merge with offset + ExPolygons merged = layer->merged(eps); + // ofsset back + ExPolygons layer_outline = offset_ex(merged, -eps); + + float min_region_flow_width { }; + for (const auto *region : layer->regions()) { + min_region_flow_width = std::max(min_region_flow_width, region->flow(FlowRole::frExternalPerimeter).width()); + } + std::cout << "min_region_flow_width: " << min_region_flow_width << std::endl; + return EdgeGridWrapper(scale_(min_region_flow_width), layer_outline); +} + +void check_extrusion_entity_stability(const ExtrusionEntity *entity, const EdgeGridWrapper &supported_grid, + const Params ¶ms) { + if (entity->is_collection()){ + for (const auto* e: static_cast(entity).entities){ + check_extrusion_entity_stability(e, supported_grid, params); + } + } else { //single extrusion path, with possible varying parameters + entity->as_polyline().points; + } + + +} + +void check_layer_stability(const PrintObject *po, size_t layer_idx, const Params ¶ms) { + if (layer_idx == 0) { + // first layer is usually ok + return; + } + const Layer *layer = po->get_layer(layer_idx); + const Layer *prev_layer = layer->lower_layer; + EdgeGridWrapper supported_grid = compute_layer_merged_edge_grid(prev_layer); + + for (const LayerRegion *layer_region : layer->regions()) { + coordf_t flow_width = coordf_t( + scale_(layer_region->flow(FlowRole::frExternalPerimeter).width())); + for (const ExtrusionEntity *ex_entity : layer_region->perimeters.entities) { + for (const ExtrusionEntity *perimeter : static_cast(ex_entity)->entities) { + if (perimeter->role() == ExtrusionRole::erExternalPerimeter) { + check_extrusion_entity_stability(perimeter, supported_grid, params); + } // ex_perimeter + } // perimeter + } // ex_entity + } +} + +} //Impl End + +void quick_search(const PrintObject *po, const Params ¶ms = Params { }) { + using namespace Impl; + std::vector descriptors(po->layer_count()); + + tbb::parallel_for(tbb::blocked_range(0, po->layer_count()), + [&](tbb::blocked_range r) { + for (size_t layer_idx = r.begin(); layer_idx < r.end(); ++layer_idx) { + const Layer *layer = po->get_layer(layer_idx); + + LayerDescriptor &descriptor = descriptors[layer_idx]; + size_t point_count { 0 }; + + for (const LayerRegion *layer_region : layer->regions()) { + for (const ExtrusionEntity *ex_entity : layer_region->perimeters.entities) { + for (const ExtrusionEntity *perimeter : static_cast(ex_entity)->entities) { + if (perimeter->role() == ExtrusionRole::erExternalPerimeter) { + assert(perimeter->is_loop()); + descriptor.segments_count++; + const ExtrusionLoop *loop = static_cast(perimeter); + for (const ExtrusionPath& path : loop->paths) { + Vec2f prev_pos = unscale(path.polyline.last_point()).cast(); + for (size_t p_idx = 0; p_idx < path.polyline.points.size(); ++p_idx) { + point_count++; + Vec2f point_pos = unscale(path.polyline.points[p_idx]).cast(); + descriptor.centroid += point_pos; + descriptor.perimeter_length += (point_pos - prev_pos).norm(); + prev_pos = point_pos; + } //point + } //path + } // ex_perimeter + } // perimeter +} // ex_entity +} // region + + descriptor.centroid /= float(point_count); + + } // layer + } // thread + ); + + for (size_t desc_idx = 0; desc_idx < descriptors.size(); ++desc_idx) { + const LayerDescriptor &descriptor = descriptors[desc_idx]; + std::cout << "SIS layer idx: " << desc_idx << " reg count: " << descriptor.segments_count << " len: " + << descriptor.perimeter_length << + " centroid: " << descriptor.centroid.x() << " | " << descriptor.centroid.y() << std::endl; + if (desc_idx > 0) { + const LayerDescriptor &prev = descriptors[desc_idx - 1]; + std::cout << "SIS diff: " << desc_idx << " reg count: " + << (int(descriptor.segments_count) - int(prev.segments_count)) << + " len: " << (descriptor.perimeter_length - prev.perimeter_length) << + " centroid: " << (descriptor.centroid - prev.centroid).norm() << std::endl; + } + } + +} + +} +} diff --git a/src/libslic3r/SupportableIssuesSearch.hpp b/src/libslic3r/SupportableIssuesSearch.hpp new file mode 100644 index 0000000000..dbec52aff0 --- /dev/null +++ b/src/libslic3r/SupportableIssuesSearch.hpp @@ -0,0 +1,16 @@ +#ifndef SRC_LIBSLIC3R_SUPPORTABLEISSUESSEARCH_HPP_ +#define SRC_LIBSLIC3R_SUPPORTABLEISSUESSEARCH_HPP_ + +#include "libslic3r/Print.hpp" + +namespace Slic3r { + +namespace SupportableIssues { + +void quick_search(const PrintObject *po); + +} + +} + +#endif /* SRC_LIBSLIC3R_SUPPORTABLEISSUESSEARCH_HPP_ */ From 706cd63e610949f383937c4f71cf44c9aefe194d Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Thu, 7 Apr 2022 12:44:50 +0200 Subject: [PATCH 002/100] Beta version of the algorithm Implemented long unsupported segments detection, which considers also curvature Implemented detection of curved segments at the edge of the previous layer - danger of warping/curling --- src/libslic3r/CMakeLists.txt | 2 + src/libslic3r/PrintObject.cpp | 35 ++- src/libslic3r/SupportableIssuesSearch.cpp | 318 ++++++++++++++++++---- src/libslic3r/SupportableIssuesSearch.hpp | 20 +- src/libslic3r/TriangleSelectorWrapper.cpp | 1 + src/libslic3r/TriangleSelectorWrapper.hpp | 42 +++ 6 files changed, 366 insertions(+), 52 deletions(-) create mode 100644 src/libslic3r/TriangleSelectorWrapper.cpp create mode 100644 src/libslic3r/TriangleSelectorWrapper.hpp diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index a342da8319..0a0062a754 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -275,6 +275,8 @@ set(SLIC3R_SOURCES TriangleSelector.hpp TriangleSetSampling.cpp TriangleSetSampling.hpp + TriangleSelectorWrapper.cpp + TriangleSelectorWrapper.hpp MTUtils.hpp Zipper.hpp Zipper.cpp diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 4bcf74fe5e..cf6f168dff 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -402,12 +402,38 @@ void PrintObject::find_supportable_issues() BOOST_LOG_TRIVIAL(debug) << "Searching supportable issues - start"; //TODO status number? - m_print->set_status(70, L("Searching supportable issues")); - - if (this->has_support()) { + m_print->set_status(75, L("Searching supportable issues")); + if (!this->m_config.support_material) { + std::vector problematic_layers = SupportableIssues::quick_search(this); + if (!problematic_layers.empty()){ + //TODO report problems + } } else { - SupportableIssues::quick_search(this); + SupportableIssues::Issues issues = SupportableIssues::full_search(this); + if (!issues.supports_nedded.empty()) { + auto obj_transform = this->trafo_centered(); + for (ModelVolume *model_volume : this->model_object()->volumes) { + if (model_volume->type() == ModelVolumeType::MODEL_PART) { + Transform3d model_transformation = model_volume->get_matrix(); + Transform3d inv_transform = (obj_transform * model_transformation).inverse(); + TriangleSelectorWrapper selector { model_volume->mesh() }; + + for (const Vec3f &support_point : issues.supports_nedded) { + selector.enforce_spot(Vec3f(inv_transform.cast() * support_point), 1.0f); + } + + model_volume->supported_facets.set(selector.selector); + +#if 1 + indexed_triangle_set copy = model_volume->mesh().its; + its_transform(copy, obj_transform * model_transformation); + its_write_obj(copy, + debug_out_path(("model" + std::to_string(model_volume->id().id) + ".obj").c_str()).c_str()); +#endif + } + } + } } m_print->throw_if_canceled(); @@ -417,6 +443,7 @@ void PrintObject::find_supportable_issues() } } + void PrintObject::generate_support_material() { if (this->set_started(posSupportMaterial)) { diff --git a/src/libslic3r/SupportableIssuesSearch.cpp b/src/libslic3r/SupportableIssuesSearch.cpp index bace1aea00..cdd3130a0a 100644 --- a/src/libslic3r/SupportableIssuesSearch.cpp +++ b/src/libslic3r/SupportableIssuesSearch.cpp @@ -4,18 +4,31 @@ #include "tbb/blocked_range.h" #include "tbb/parallel_reduce.h" #include +#include #include "libslic3r/Layer.hpp" #include "libslic3r/EdgeGrid.hpp" #include "libslic3r/ClipperUtils.hpp" +#define DEBUG_FILES + +#ifdef DEBUG_FILES +#include +#endif + namespace Slic3r { namespace SupportableIssues { -struct Params { - float bridge_distance = 5.0f; - float printable_protrusion_distance = 1.0f; -}; +void Issues::add(const Issues &layer_issues) { + supports_nedded.insert(supports_nedded.end(), + layer_issues.supports_nedded.begin(), layer_issues.supports_nedded.end()); + curling_up.insert(curling_up.end(), layer_issues.curling_up.begin(), + layer_issues.curling_up.end()); +} + +bool Issues::empty() const { + return supports_nedded.empty() && curling_up.empty(); +} namespace Impl { @@ -34,62 +47,218 @@ struct EdgeGridWrapper { } EdgeGrid::Grid grid; ExPolygons ex_polys; -} -; +}; -EdgeGridWrapper compute_layer_merged_edge_grid(const Layer *layer) { - static const float eps = float(scale_(layer->object()->config().slice_closing_radius.value)); - // merge with offset - ExPolygons merged = layer->merged(eps); - // ofsset back - ExPolygons layer_outline = offset_ex(merged, -eps); +#ifdef DEBUG_FILES +void debug_export(Issues issues, std::string file_name) { + Slic3r::CNumericLocalesSetter locales_setter; - float min_region_flow_width { }; - for (const auto *region : layer->regions()) { - min_region_flow_width = std::max(min_region_flow_width, region->flow(FlowRole::frExternalPerimeter).width()); - } - std::cout << "min_region_flow_width: " << min_region_flow_width << std::endl; - return EdgeGridWrapper(scale_(min_region_flow_width), layer_outline); -} - -void check_extrusion_entity_stability(const ExtrusionEntity *entity, const EdgeGridWrapper &supported_grid, - const Params ¶ms) { - if (entity->is_collection()){ - for (const auto* e: static_cast(entity).entities){ - check_extrusion_entity_stability(e, supported_grid, params); + { + FILE *fp = boost::nowide::fopen(debug_out_path((file_name + "_supports.obj").c_str()).c_str(), "w"); + if (fp == nullptr) { + BOOST_LOG_TRIVIAL(error) + << "Debug files: Couldn't open " << file_name << " for writing"; + return; } - } else { //single extrusion path, with possible varying parameters - entity->as_polyline().points; - } + for (size_t i = 0; i < issues.supports_nedded.size(); ++i) { + fprintf(fp, "v %f %f %f %f %f %f\n", + issues.supports_nedded[i](0), issues.supports_nedded[i](1), issues.supports_nedded[i](2), + 1.0, 0.0, 0.0 + ); + } + + fclose(fp); + } + { + FILE *fp = boost::nowide::fopen(debug_out_path((file_name + "_curling.obj").c_str()).c_str(), "w"); + if (fp == nullptr) { + BOOST_LOG_TRIVIAL(error) + << "Debug files: Couldn't open " << file_name << " for writing"; + return; + } + + for (size_t i = 0; i < issues.curling_up.size(); ++i) { + fprintf(fp, "v %f %f %f %f %f %f\n", + issues.curling_up[i](0), issues.curling_up[i](1), issues.curling_up[i](2), + 0.0, 1.0, 0.0 + ); + } + fclose(fp); + } } +#endif -void check_layer_stability(const PrintObject *po, size_t layer_idx, const Params ¶ms) { - if (layer_idx == 0) { - // first layer is usually ok - return; +EdgeGridWrapper compute_layer_edge_grid(const Layer *layer) { + float min_region_flow_width { 1.0f }; + for (const auto *region : layer->regions()) { + min_region_flow_width = std::min(min_region_flow_width, region->flow(FlowRole::frExternalPerimeter).width()); } - const Layer *layer = po->get_layer(layer_idx); - const Layer *prev_layer = layer->lower_layer; - EdgeGridWrapper supported_grid = compute_layer_merged_edge_grid(prev_layer); - + ExPolygons ex_polygons; for (const LayerRegion *layer_region : layer->regions()) { - coordf_t flow_width = coordf_t( - scale_(layer_region->flow(FlowRole::frExternalPerimeter).width())); for (const ExtrusionEntity *ex_entity : layer_region->perimeters.entities) { for (const ExtrusionEntity *perimeter : static_cast(ex_entity)->entities) { if (perimeter->role() == ExtrusionRole::erExternalPerimeter) { - check_extrusion_entity_stability(perimeter, supported_grid, params); + ex_polygons.push_back(ExPolygon { perimeter->as_polyline().points }); } // ex_perimeter } // perimeter } // ex_entity } + + return EdgeGridWrapper(scale_(min_region_flow_width), ex_polygons); +} + +Issues check_extrusion_entity_stability(const ExtrusionEntity *entity, size_t layer_idx, + float slice_z, + coordf_t flow_width, + const EdgeGridWrapper &supported_grid, + const Params ¶ms) { + + Issues issues { }; + if (entity->is_collection()) { + for (const auto *e : static_cast(entity)->entities) { + issues.add(check_extrusion_entity_stability(e, layer_idx, slice_z, flow_width, supported_grid, params)); + } + } else { //single extrusion path, with possible varying parameters + Points points = entity->as_polyline().points; + float unsupported_distance = params.bridge_distance + 1.0f; + float curvature = 0; + float max_curvature = 0; + Vec2f tmp = unscale(points[0]).cast(); + Vec3f prev_point = Vec3f(tmp.x(), tmp.y(), slice_z); + + for (size_t point_index = 0; point_index < points.size(); ++point_index) { + std::cout << "index: " << point_index << " dist: " << unsupported_distance << " curvature: " + << curvature << " max curvature: " << max_curvature << std::endl; + + Vec2f tmp = unscale(points[point_index]).cast(); + Vec3f u_point = Vec3f(tmp.x(), tmp.y(), slice_z); + + coordf_t dist_from_prev_layer { 0 }; + if (!supported_grid.grid.signed_distance(points[point_index], flow_width, dist_from_prev_layer)) { + issues.supports_nedded.push_back(u_point); + continue; + } + + constexpr float limit_overlap_factor = 0.5; + + if (dist_from_prev_layer > flow_width) { //unsupported + std::cout << "index: " << point_index << " unsupported " << std::endl; + unsupported_distance += (u_point - prev_point).norm(); + } else { + std::cout << "index: " << point_index << " grounded " << std::endl; + unsupported_distance = 0; + } + + std::cout << "index: " << point_index << " dfromprev: " << dist_from_prev_layer << std::endl; + + if (dist_from_prev_layer > flow_width * limit_overlap_factor && point_index < points.size() - 1) { + const Vec2f v1 = (u_point - prev_point).head<2>(); + const Vec2f v2 = unscale(points[point_index + 1]).cast() - u_point.head<2>(); + float dot = v1(0) * v2(0) + v1(1) * v2(1); + float cross = v1(0) * v2(1) - v1(1) * v2(0); + float angle = float(atan2(float(cross), float(dot))); + + std::cout << "index: " << point_index << " angle: " << angle << std::endl; + + curvature += angle; + max_curvature = std::max(abs(curvature), max_curvature); + } + + if (!(dist_from_prev_layer > flow_width * limit_overlap_factor)) { + std::cout << "index: " << point_index << " reset curvature" << std::endl; + max_curvature = 0; + curvature = 0; + } + + if (unsupported_distance > params.bridge_distance / (1 + int(max_curvature * 9 / PI))) { + issues.supports_nedded.push_back(u_point); + unsupported_distance = 0; + curvature = 0; + max_curvature = 0; + } + + if (max_curvature / (PI * unsupported_distance) > params.limit_curvature) { + issues.curling_up.push_back(u_point); + curvature = 0; + max_curvature = 0; + } + + prev_point = u_point; + } + } + return issues; +} + +//TODO needs revision +coordf_t get_flow_width(const LayerRegion *region, ExtrusionRole role) { + switch (role) { + case ExtrusionRole::erBridgeInfill: + return region->flow(FlowRole::frExternalPerimeter).scaled_width(); + case ExtrusionRole::erExternalPerimeter: + return region->flow(FlowRole::frExternalPerimeter).scaled_width(); + case ExtrusionRole::erGapFill: + return region->flow(FlowRole::frInfill).scaled_width(); + case ExtrusionRole::erPerimeter: + return region->flow(FlowRole::frPerimeter).scaled_width(); + case ExtrusionRole::erSolidInfill: + return region->flow(FlowRole::frSolidInfill).scaled_width(); + default: + return region->flow(FlowRole::frExternalPerimeter).scaled_width(); + } +} + +Issues check_layer_stability(const PrintObject *po, size_t layer_idx, bool full_check, const Params ¶ms) { + std::cout << "Checking: " << layer_idx << std::endl; + if (layer_idx == 0) { + // first layer is usually ok + return {}; + } + const Layer *layer = po->get_layer(layer_idx); + EdgeGridWrapper supported_grid = compute_layer_edge_grid(layer->lower_layer); + + Issues issues { }; + if (full_check) { + for (const LayerRegion *layer_region : layer->regions()) { + for (const ExtrusionEntity *ex_entity : layer_region->perimeters.entities) { + for (const ExtrusionEntity *perimeter : static_cast(ex_entity)->entities) { + issues.add(check_extrusion_entity_stability(perimeter, layer_idx, + layer->slice_z, get_flow_width(layer_region, perimeter->role()), + supported_grid, params)); + } // perimeter + } // ex_entity + for (const ExtrusionEntity *ex_entity : layer_region->fills.entities) { + for (const ExtrusionEntity *fill : static_cast(ex_entity)->entities) { + if (fill->role() == ExtrusionRole::erGapFill || fill->role() == ExtrusionRole::erBridgeInfill) { + issues.add(check_extrusion_entity_stability(fill, layer_idx, + layer->slice_z, get_flow_width(layer_region, fill->role()), + supported_grid, params)); + } + } // fill + } // ex_entity + } // region + } else { //check only external perimeters + for (const LayerRegion *layer_region : layer->regions()) { + for (const ExtrusionEntity *ex_entity : layer_region->perimeters.entities) { + for (const ExtrusionEntity *perimeter : static_cast(ex_entity)->entities) { + if (perimeter->role() == ExtrusionRole::erExternalPerimeter) { + std::cout << "checking ex perimeter " << std::endl; + issues.add(check_extrusion_entity_stability(perimeter, layer_idx, + layer->slice_z, get_flow_width(layer_region, perimeter->role()), + supported_grid, params)); + }; // ex_perimeter + } // perimeter + } // ex_entity + } //region + } + + return issues; } } //Impl End -void quick_search(const PrintObject *po, const Params ¶ms = Params { }) { +std::vector quick_search(const PrintObject *po, const Params ¶ms) { using namespace Impl; std::vector descriptors(po->layer_count()); @@ -129,20 +298,75 @@ void quick_search(const PrintObject *po, const Params ¶ms = Params { }) { } // thread ); - for (size_t desc_idx = 0; desc_idx < descriptors.size(); ++desc_idx) { + std::vector suspicious_layers_indices { }; + + for (size_t desc_idx = 1; desc_idx < descriptors.size(); ++desc_idx) { + const LayerDescriptor &prev = descriptors[desc_idx - 1]; const LayerDescriptor &descriptor = descriptors[desc_idx]; + if (descriptor.segments_count - prev.segments_count != 0 + || + std::abs(descriptor.perimeter_length - prev.perimeter_length) + > params.perimeter_length_diff_tolerance || + (descriptor.centroid - prev.centroid).norm() > params.centroid_offset_tolerance + ) { + suspicious_layers_indices.push_back(desc_idx); + } +#ifdef DEBUG_FILES std::cout << "SIS layer idx: " << desc_idx << " reg count: " << descriptor.segments_count << " len: " << descriptor.perimeter_length << " centroid: " << descriptor.centroid.x() << " | " << descriptor.centroid.y() << std::endl; - if (desc_idx > 0) { - const LayerDescriptor &prev = descriptors[desc_idx - 1]; - std::cout << "SIS diff: " << desc_idx << " reg count: " - << (int(descriptor.segments_count) - int(prev.segments_count)) << - " len: " << (descriptor.perimeter_length - prev.perimeter_length) << - " centroid: " << (descriptor.centroid - prev.centroid).norm() << std::endl; + std::cout << "SIS diff: " << desc_idx << " reg count: " + << (int(descriptor.segments_count) - int(prev.segments_count)) << + " len: " << (descriptor.perimeter_length - prev.perimeter_length) << + " centroid: " << (descriptor.centroid - prev.centroid).norm() << std::endl; +#endif + } + + std::vector layer_needs_supports(suspicious_layers_indices.size(), false); + tbb::parallel_for(tbb::blocked_range(0, suspicious_layers_indices.size()), + [&](tbb::blocked_range r) { + for (size_t suspicious_index = r.begin(); suspicious_index < r.end(); ++suspicious_index) { + auto layer_issues = check_layer_stability(po, suspicious_layers_indices[suspicious_index], + false, + params); + if (!layer_issues.supports_nedded.empty()) { + layer_needs_supports[suspicious_index] = true; + } + } + }); + + std::vector problematic_layers; + + for (size_t index = suspicious_layers_indices.size() - 1; index <= 0; ++index) { + if (!layer_needs_supports[index]) { + problematic_layers.push_back(suspicious_layers_indices[index]); + } + } + return problematic_layers; +} + +Issues full_search(const PrintObject *po, const Params ¶ms) { + using namespace Impl; + Issues issues { }; + for (size_t layer_idx = 1; layer_idx < po->layer_count(); ++layer_idx) { + auto layer_issues = check_layer_stability(po, layer_idx, true, params); + if (!layer_issues.empty()) { + issues.add(layer_issues); } } +#ifdef DEBUG_FILES + Impl::debug_export(issues, "issues"); +#endif + +// tbb::parallel_for(tbb::blocked_range(0, suspicious_layers_indices.size()), +// [&](tbb::blocked_range r) { +// for (size_t layer_idx = r.begin(); layer_idx < r.end(); ++layer_idx) { +// check_layer_stability(po, suspicious_layers_indices[layer_idx], params); +// } +// }); + + return issues; } } diff --git a/src/libslic3r/SupportableIssuesSearch.hpp b/src/libslic3r/SupportableIssuesSearch.hpp index dbec52aff0..22289c923c 100644 --- a/src/libslic3r/SupportableIssuesSearch.hpp +++ b/src/libslic3r/SupportableIssuesSearch.hpp @@ -7,7 +7,25 @@ namespace Slic3r { namespace SupportableIssues { -void quick_search(const PrintObject *po); +struct Params { + float bridge_distance = 5.0f; + float limit_curvature = 0.25f; + + float perimeter_length_diff_tolerance = 8.0f; + float centroid_offset_tolerance = 1.0f; +}; + + +struct Issues { + std::vector supports_nedded; + std::vector curling_up; + + void add(const Issues &layer_issues); + bool empty() const; +}; + +std::vector quick_search(const PrintObject *po, const Params ¶ms = Params { }); +Issues full_search(const PrintObject *po, const Params ¶ms = Params { }); } diff --git a/src/libslic3r/TriangleSelectorWrapper.cpp b/src/libslic3r/TriangleSelectorWrapper.cpp new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/src/libslic3r/TriangleSelectorWrapper.cpp @@ -0,0 +1 @@ + diff --git a/src/libslic3r/TriangleSelectorWrapper.hpp b/src/libslic3r/TriangleSelectorWrapper.hpp new file mode 100644 index 0000000000..f818d8be2b --- /dev/null +++ b/src/libslic3r/TriangleSelectorWrapper.hpp @@ -0,0 +1,42 @@ +#ifndef SRC_LIBSLIC3R_TRIANGLESELECTORWRAPPER_HPP_ +#define SRC_LIBSLIC3R_TRIANGLESELECTORWRAPPER_HPP_ + +#include + +#include "TriangleSelector.hpp" +#include "AABBTreeIndirect.hpp" + +namespace Slic3r { + +class TriangleSelectorWrapper { +public: + const TriangleMesh &mesh; + TriangleSelector selector; + AABBTreeIndirect::Tree<3, float> triangles_tree; + + TriangleSelectorWrapper(const TriangleMesh &mesh) : + mesh(mesh), selector(mesh), triangles_tree(AABBTreeIndirect::build_aabb_tree_over_indexed_triangle_set(mesh.its.vertices, mesh.its.indices)) { + + } + + void enforce_spot(const Vec3f &point, float radius) { + size_t hit_face_index; + Vec3f hit_point; + auto dist = AABBTreeIndirect::squared_distance_to_indexed_triangle_set(mesh.its.vertices, mesh.its.indices, + triangles_tree, + point, hit_face_index, hit_point); + if (dist < 0 || dist > radius) + return; + + std::unique_ptr cursor = std::make_unique(point, point, + radius, Transform3d::Identity(), TriangleSelector::ClippingPlane { }); + + selector.select_patch(hit_face_index, std::move(cursor), EnforcerBlockerType::ENFORCER, Transform3d::Identity(), true, + 0.0f); + } + +}; + +} + +#endif /* SRC_LIBSLIC3R_TRIANGLESELECTORWRAPPER_HPP_ */ From e516ba0dd0ea0c598d8ee8b912b2e4c3782bf6da Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Thu, 7 Apr 2022 13:52:59 +0200 Subject: [PATCH 003/100] Moved TriangleSelectorWrapper methods to cpp file, added comment describing problems with FacetsAnnotations/TriangleSelector structure --- src/libslic3r/PrintObject.cpp | 1 + src/libslic3r/SupportableIssuesSearch.cpp | 2 +- src/libslic3r/TriangleSelectorWrapper.cpp | 29 +++++++++++++++++++++++ src/libslic3r/TriangleSelectorWrapper.hpp | 29 +++++++---------------- 4 files changed, 39 insertions(+), 22 deletions(-) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index cf6f168dff..5c1d918aea 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -17,6 +17,7 @@ #include "Fill/FillLightning.hpp" #include "Format/STL.hpp" #include "SupportableIssuesSearch.hpp" +#include "TriangleSelectorWrapper.hpp" #include #include diff --git a/src/libslic3r/SupportableIssuesSearch.cpp b/src/libslic3r/SupportableIssuesSearch.cpp index cdd3130a0a..51a425a7ed 100644 --- a/src/libslic3r/SupportableIssuesSearch.cpp +++ b/src/libslic3r/SupportableIssuesSearch.cpp @@ -172,7 +172,7 @@ Issues check_extrusion_entity_stability(const ExtrusionEntity *entity, size_t la curvature = 0; } - if (unsupported_distance > params.bridge_distance / (1 + int(max_curvature * 9 / PI))) { + if (unsupported_distance > params.bridge_distance / (1 + int(max_curvature * 7 / PI))) { issues.supports_nedded.push_back(u_point); unsupported_distance = 0; curvature = 0; diff --git a/src/libslic3r/TriangleSelectorWrapper.cpp b/src/libslic3r/TriangleSelectorWrapper.cpp index 8b13789179..98d00bc985 100644 --- a/src/libslic3r/TriangleSelectorWrapper.cpp +++ b/src/libslic3r/TriangleSelectorWrapper.cpp @@ -1 +1,30 @@ +#include "Model.hpp" +#include "TriangleSelectorWrapper.hpp" +#include +namespace Slic3r { + +TriangleSelectorWrapper::TriangleSelectorWrapper(const TriangleMesh &mesh) : + mesh(mesh), selector(mesh), triangles_tree( + AABBTreeIndirect::build_aabb_tree_over_indexed_triangle_set(mesh.its.vertices, mesh.its.indices)) { + +} + +void TriangleSelectorWrapper::enforce_spot(const Vec3f &point, float radius) { + size_t hit_face_index; + Vec3f hit_point; + auto dist = AABBTreeIndirect::squared_distance_to_indexed_triangle_set(mesh.its.vertices, mesh.its.indices, + triangles_tree, + point, hit_face_index, hit_point); + if (dist < 0 || dist > radius) + return; + + std::unique_ptr cursor = std::make_unique(point, point, + radius, Transform3d::Identity(), TriangleSelector::ClippingPlane { }); + + selector.select_patch(hit_face_index, std::move(cursor), EnforcerBlockerType::ENFORCER, Transform3d::Identity(), + true, + 0.0f); +} + +} diff --git a/src/libslic3r/TriangleSelectorWrapper.hpp b/src/libslic3r/TriangleSelectorWrapper.hpp index f818d8be2b..f3b56205fa 100644 --- a/src/libslic3r/TriangleSelectorWrapper.hpp +++ b/src/libslic3r/TriangleSelectorWrapper.hpp @@ -1,39 +1,26 @@ #ifndef SRC_LIBSLIC3R_TRIANGLESELECTORWRAPPER_HPP_ #define SRC_LIBSLIC3R_TRIANGLESELECTORWRAPPER_HPP_ -#include - #include "TriangleSelector.hpp" #include "AABBTreeIndirect.hpp" namespace Slic3r { +//NOTE: We need to replace the FacetsAnnotation struct for support storage (or extend/add another) +// Problems: Does not support negative volumes, strange usage for supports computed from extrusion - +// expensively converted back to triangles and then sliced again. +// Another problem is weird and very limited interface when painting supports via algorithms + + class TriangleSelectorWrapper { public: const TriangleMesh &mesh; TriangleSelector selector; AABBTreeIndirect::Tree<3, float> triangles_tree; - TriangleSelectorWrapper(const TriangleMesh &mesh) : - mesh(mesh), selector(mesh), triangles_tree(AABBTreeIndirect::build_aabb_tree_over_indexed_triangle_set(mesh.its.vertices, mesh.its.indices)) { + TriangleSelectorWrapper(const TriangleMesh &mesh); - } - - void enforce_spot(const Vec3f &point, float radius) { - size_t hit_face_index; - Vec3f hit_point; - auto dist = AABBTreeIndirect::squared_distance_to_indexed_triangle_set(mesh.its.vertices, mesh.its.indices, - triangles_tree, - point, hit_face_index, hit_point); - if (dist < 0 || dist > radius) - return; - - std::unique_ptr cursor = std::make_unique(point, point, - radius, Transform3d::Identity(), TriangleSelector::ClippingPlane { }); - - selector.select_patch(hit_face_index, std::move(cursor), EnforcerBlockerType::ENFORCER, Transform3d::Identity(), true, - 0.0f); - } + void enforce_spot(const Vec3f &point, float radius); }; From d41b20547decc38e03e2e19eeb0ebcf41db909a6 Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Mon, 11 Apr 2022 17:20:29 +0200 Subject: [PATCH 004/100] greatly upgraded the algorithm for support placement - added dynamic splitting of long paths, included flow width of current and previous layer, refactored and renamed parameters --- src/libslic3r/PrintObject.cpp | 19 +- src/libslic3r/SupportableIssuesSearch.cpp | 198 +++++++++++-------- src/libslic3r/SupportableIssuesSearch.hpp | 9 +- src/libslic3r/TriangleSelectorWrapper.cpp | 5 +- src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp | 4 + 5 files changed, 147 insertions(+), 88 deletions(-) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 5c1d918aea..c9f64cfb3c 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -18,6 +18,7 @@ #include "Format/STL.hpp" #include "SupportableIssuesSearch.hpp" #include "TriangleSelectorWrapper.hpp" +#include "format.hpp" #include #include @@ -397,18 +398,26 @@ void PrintObject::ironing() } } + void PrintObject::find_supportable_issues() { if (this->set_started(posSupportableIssuesSearch)) { BOOST_LOG_TRIVIAL(debug) << "Searching supportable issues - start"; - //TODO status number? m_print->set_status(75, L("Searching supportable issues")); if (!this->m_config.support_material) { std::vector problematic_layers = SupportableIssues::quick_search(this); - if (!problematic_layers.empty()){ - //TODO report problems + if (!problematic_layers.empty()) { + std::cout << "Object needs supports" << std::endl; + this->active_step_add_warning(PrintStateBase::WarningLevel::CRITICAL, + L("Supportable issues found. Consider enabling supports for this object")); + this->active_step_add_warning(PrintStateBase::WarningLevel::CRITICAL, + L("Supportable issues found. Consider enabling supports for this object")); + for (size_t index = 0; index < std::min(problematic_layers.size(), size_t(4)); ++index) { + this->active_step_add_warning(PrintStateBase::WarningLevel::CRITICAL, + format(L("Layer with issues: %1%"), problematic_layers[index] + 1)); + } } } else { SupportableIssues::Issues issues = SupportableIssues::full_search(this); @@ -421,7 +430,7 @@ void PrintObject::find_supportable_issues() TriangleSelectorWrapper selector { model_volume->mesh() }; for (const Vec3f &support_point : issues.supports_nedded) { - selector.enforce_spot(Vec3f(inv_transform.cast() * support_point), 1.0f); + selector.enforce_spot(Vec3f(inv_transform.cast() * support_point), 0.3f); } model_volume->supported_facets.set(selector.selector); @@ -430,7 +439,7 @@ void PrintObject::find_supportable_issues() indexed_triangle_set copy = model_volume->mesh().its; its_transform(copy, obj_transform * model_transformation); its_write_obj(copy, - debug_out_path(("model" + std::to_string(model_volume->id().id) + ".obj").c_str()).c_str()); + debug_out_path("model.obj").c_str()); #endif } } diff --git a/src/libslic3r/SupportableIssuesSearch.cpp b/src/libslic3r/SupportableIssuesSearch.cpp index 51a425a7ed..ea2ba41063 100644 --- a/src/libslic3r/SupportableIssuesSearch.cpp +++ b/src/libslic3r/SupportableIssuesSearch.cpp @@ -5,6 +5,7 @@ #include "tbb/parallel_reduce.h" #include #include +#include #include "libslic3r/Layer.hpp" #include "libslic3r/EdgeGrid.hpp" @@ -39,14 +40,24 @@ struct LayerDescriptor { }; struct EdgeGridWrapper { - EdgeGridWrapper(coord_t resolution, ExPolygons ex_polys) : - ex_polys(ex_polys) { + EdgeGridWrapper(coord_t edge_width, ExPolygons ex_polys) : + ex_polys(ex_polys), edge_width(edge_width) { - grid.create(this->ex_polys, resolution); + grid.create(this->ex_polys, edge_width); grid.calculate_sdf(); } + + bool signed_distance(const Point &point, coordf_t point_width, coordf_t &dist_out) const { + coordf_t tmp_dist_out; + bool found = grid.signed_distance(point, edge_width, tmp_dist_out); + dist_out = tmp_dist_out - edge_width / 2 - point_width / 2; + return found; + + } + EdgeGrid::Grid grid; ExPolygons ex_polys; + coord_t edge_width; }; #ifdef DEBUG_FILES @@ -99,8 +110,13 @@ EdgeGridWrapper compute_layer_edge_grid(const Layer *layer) { for (const LayerRegion *layer_region : layer->regions()) { for (const ExtrusionEntity *ex_entity : layer_region->perimeters.entities) { for (const ExtrusionEntity *perimeter : static_cast(ex_entity)->entities) { - if (perimeter->role() == ExtrusionRole::erExternalPerimeter) { - ex_polygons.push_back(ExPolygon { perimeter->as_polyline().points }); + if (perimeter->role() == ExtrusionRole::erExternalPerimeter + || perimeter->role() == ExtrusionRole::erOverhangPerimeter) { + Points perimeter_points { }; + perimeter->collect_points(perimeter_points); + assert(perimeter->is_loop()); + perimeter_points.pop_back(); + ex_polygons.push_back(ExPolygon { perimeter_points }); } // ex_perimeter } // perimeter } // ex_entity @@ -109,7 +125,16 @@ EdgeGridWrapper compute_layer_edge_grid(const Layer *layer) { return EdgeGridWrapper(scale_(min_region_flow_width), ex_polygons); } -Issues check_extrusion_entity_stability(const ExtrusionEntity *entity, size_t layer_idx, +coordf_t get_max_allowed_distance(ExtrusionRole role, coord_t flow_width, const Params ¶ms) { // <= distance / flow_width (can be larger for perimeter, if not external perimeter first) + if (!params.external_perimeter_first + && (role == ExtrusionRole::erExternalPerimeter || role == ExtrusionRole::erOverhangPerimeter)) { + return params.max_ex_perim_unsupported_distance_factor * flow_width; + } else { + return params.max_unsupported_distance_factor * flow_width; + } +} + +Issues check_extrusion_entity_stability(const ExtrusionEntity *entity, float slice_z, coordf_t flow_width, const EdgeGridWrapper &supported_grid, @@ -118,74 +143,87 @@ Issues check_extrusion_entity_stability(const ExtrusionEntity *entity, size_t la Issues issues { }; if (entity->is_collection()) { for (const auto *e : static_cast(entity)->entities) { - issues.add(check_extrusion_entity_stability(e, layer_idx, slice_z, flow_width, supported_grid, params)); + issues.add(check_extrusion_entity_stability(e, slice_z, flow_width, supported_grid, params)); } } else { //single extrusion path, with possible varying parameters - Points points = entity->as_polyline().points; + std::stack points { }; + for (const auto &p : entity->as_polyline().points) { + points.push(p); + } + float unsupported_distance = params.bridge_distance + 1.0f; float curvature = 0; float max_curvature = 0; - Vec2f tmp = unscale(points[0]).cast(); - Vec3f prev_point = Vec3f(tmp.x(), tmp.y(), slice_z); + Vec2f tmp = unscale(points.top()).cast(); + Vec3f prev_fpoint = Vec3f(tmp.x(), tmp.y(), slice_z); - for (size_t point_index = 0; point_index < points.size(); ++point_index) { - std::cout << "index: " << point_index << " dist: " << unsupported_distance << " curvature: " - << curvature << " max curvature: " << max_curvature << std::endl; + const coordf_t max_allowed_dist_from_prev_layer = get_max_allowed_distance(entity->role(), flow_width, + params); - Vec2f tmp = unscale(points[point_index]).cast(); - Vec3f u_point = Vec3f(tmp.x(), tmp.y(), slice_z); + while (!points.empty()) { + Point point = points.top(); + points.pop(); + Vec2f tmp = unscale(point).cast(); + Vec3f fpoint = Vec3f(tmp.x(), tmp.y(), slice_z); coordf_t dist_from_prev_layer { 0 }; - if (!supported_grid.grid.signed_distance(points[point_index], flow_width, dist_from_prev_layer)) { - issues.supports_nedded.push_back(u_point); + if (!supported_grid.signed_distance(point, flow_width, dist_from_prev_layer)) { + issues.supports_nedded.push_back(fpoint); continue; } - constexpr float limit_overlap_factor = 0.5; + if (dist_from_prev_layer > max_allowed_dist_from_prev_layer) { //unsupported + unsupported_distance += (fpoint - prev_fpoint).norm(); - if (dist_from_prev_layer > flow_width) { //unsupported - std::cout << "index: " << point_index << " unsupported " << std::endl; - unsupported_distance += (u_point - prev_point).norm(); + if (!points.empty()) { + const Vec2f v1 = (fpoint - prev_fpoint).head<2>(); + const Vec2f v2 = unscale(points.top()).cast() - fpoint.head<2>(); + float dot = v1(0) * v2(0) + v1(1) * v2(1); + float cross = v1(0) * v2(1) - v1(1) * v2(0); + float angle = float(atan2(float(cross), float(dot))); + + curvature += angle; + max_curvature = std::max(abs(curvature), max_curvature); + } + + if (unsupported_distance + > params.bridge_distance + / (1.0f + (max_curvature * params.bridge_distance_decrease_by_curvature_factor / PI))) { + issues.supports_nedded.push_back(fpoint); + + std::cout << "SUPP: " << "udis: " << unsupported_distance << " curv: " << curvature << " max curv: " + << max_curvature << std::endl; + std::cout << "max dist from layer: " << max_allowed_dist_from_prev_layer << " measured dist: " + << dist_from_prev_layer << " FW: " << flow_width << std::endl; + + unsupported_distance = 0; + curvature = 0; + max_curvature = 0; + } } else { - std::cout << "index: " << point_index << " grounded " << std::endl; - unsupported_distance = 0; - } - - std::cout << "index: " << point_index << " dfromprev: " << dist_from_prev_layer << std::endl; - - if (dist_from_prev_layer > flow_width * limit_overlap_factor && point_index < points.size() - 1) { - const Vec2f v1 = (u_point - prev_point).head<2>(); - const Vec2f v2 = unscale(points[point_index + 1]).cast() - u_point.head<2>(); - float dot = v1(0) * v2(0) + v1(1) * v2(1); - float cross = v1(0) * v2(1) - v1(1) * v2(0); - float angle = float(atan2(float(cross), float(dot))); - - std::cout << "index: " << point_index << " angle: " << angle << std::endl; - - curvature += angle; - max_curvature = std::max(abs(curvature), max_curvature); - } - - if (!(dist_from_prev_layer > flow_width * limit_overlap_factor)) { - std::cout << "index: " << point_index << " reset curvature" << std::endl; - max_curvature = 0; - curvature = 0; - } - - if (unsupported_distance > params.bridge_distance / (1 + int(max_curvature * 7 / PI))) { - issues.supports_nedded.push_back(u_point); unsupported_distance = 0; curvature = 0; max_curvature = 0; } if (max_curvature / (PI * unsupported_distance) > params.limit_curvature) { - issues.curling_up.push_back(u_point); - curvature = 0; - max_curvature = 0; + issues.curling_up.push_back(fpoint); + } + + prev_fpoint = fpoint; + + if (!points.empty()) { + Vec2f next = unscale(points.top()).cast(); + Vec2f reverse_v = fpoint.head<2>() - next; + float dist_to_next = reverse_v.norm(); + reverse_v.normalize(); + int new_points_count = dist_to_next / params.bridge_distance; + float step_size = dist_to_next / (new_points_count + 1); + for (int i = 1; i <= new_points_count; ++i) { + points.push(Point::new_scale(Vec2f(next + reverse_v * (i * step_size)))); + } } - prev_point = u_point; } } return issues; @@ -205,7 +243,7 @@ coordf_t get_flow_width(const LayerRegion *region, ExtrusionRole role) { case ExtrusionRole::erSolidInfill: return region->flow(FlowRole::frSolidInfill).scaled_width(); default: - return region->flow(FlowRole::frExternalPerimeter).scaled_width(); + return region->flow(FlowRole::frPerimeter).scaled_width(); } } @@ -223,7 +261,7 @@ Issues check_layer_stability(const PrintObject *po, size_t layer_idx, bool full_ for (const LayerRegion *layer_region : layer->regions()) { for (const ExtrusionEntity *ex_entity : layer_region->perimeters.entities) { for (const ExtrusionEntity *perimeter : static_cast(ex_entity)->entities) { - issues.add(check_extrusion_entity_stability(perimeter, layer_idx, + issues.add(check_extrusion_entity_stability(perimeter, layer->slice_z, get_flow_width(layer_region, perimeter->role()), supported_grid, params)); } // perimeter @@ -231,7 +269,7 @@ Issues check_layer_stability(const PrintObject *po, size_t layer_idx, bool full_ for (const ExtrusionEntity *ex_entity : layer_region->fills.entities) { for (const ExtrusionEntity *fill : static_cast(ex_entity)->entities) { if (fill->role() == ExtrusionRole::erGapFill || fill->role() == ExtrusionRole::erBridgeInfill) { - issues.add(check_extrusion_entity_stability(fill, layer_idx, + issues.add(check_extrusion_entity_stability(fill, layer->slice_z, get_flow_width(layer_region, fill->role()), supported_grid, params)); } @@ -242,9 +280,9 @@ Issues check_layer_stability(const PrintObject *po, size_t layer_idx, bool full_ for (const LayerRegion *layer_region : layer->regions()) { for (const ExtrusionEntity *ex_entity : layer_region->perimeters.entities) { for (const ExtrusionEntity *perimeter : static_cast(ex_entity)->entities) { - if (perimeter->role() == ExtrusionRole::erExternalPerimeter) { - std::cout << "checking ex perimeter " << std::endl; - issues.add(check_extrusion_entity_stability(perimeter, layer_idx, + if (perimeter->role() == ExtrusionRole::erExternalPerimeter + || perimeter->role() == ExtrusionRole::erOverhangPerimeter) { + issues.add(check_extrusion_entity_stability(perimeter, layer->slice_z, get_flow_width(layer_region, perimeter->role()), supported_grid, params)); }; // ex_perimeter @@ -273,7 +311,7 @@ std::vector quick_search(const PrintObject *po, const Params ¶ms) { for (const LayerRegion *layer_region : layer->regions()) { for (const ExtrusionEntity *ex_entity : layer_region->perimeters.entities) { for (const ExtrusionEntity *perimeter : static_cast(ex_entity)->entities) { - if (perimeter->role() == ExtrusionRole::erExternalPerimeter) { + if (perimeter->role() == ExtrusionRole::erExternalPerimeter || perimeter->role() == ExtrusionRole::erOverhangPerimeter) { assert(perimeter->is_loop()); descriptor.segments_count++; const ExtrusionLoop *loop = static_cast(perimeter); @@ -327,8 +365,7 @@ std::vector quick_search(const PrintObject *po, const Params ¶ms) { [&](tbb::blocked_range r) { for (size_t suspicious_index = r.begin(); suspicious_index < r.end(); ++suspicious_index) { auto layer_issues = check_layer_stability(po, suspicious_layers_indices[suspicious_index], - false, - params); + false, params); if (!layer_issues.supports_nedded.empty()) { layer_needs_supports[suspicious_index] = true; } @@ -337,8 +374,8 @@ std::vector quick_search(const PrintObject *po, const Params ¶ms) { std::vector problematic_layers; - for (size_t index = suspicious_layers_indices.size() - 1; index <= 0; ++index) { - if (!layer_needs_supports[index]) { + for (size_t index = 0; index < suspicious_layers_indices.size(); ++index) { + if (layer_needs_supports[index]) { problematic_layers.push_back(suspicious_layers_indices[index]); } } @@ -347,26 +384,29 @@ std::vector quick_search(const PrintObject *po, const Params ¶ms) { Issues full_search(const PrintObject *po, const Params ¶ms) { using namespace Impl; - Issues issues { }; - for (size_t layer_idx = 1; layer_idx < po->layer_count(); ++layer_idx) { - auto layer_issues = check_layer_stability(po, layer_idx, true, params); - if (!layer_issues.empty()) { - issues.add(layer_issues); - } - } + size_t layer_count = po->layer_count(); + Issues found_issues = tbb::parallel_reduce(tbb::blocked_range(1, layer_count), Issues { }, + [&](tbb::blocked_range r, const Issues &init) { + Issues issues = init; + for (size_t layer_idx = r.begin(); layer_idx < r.end(); ++layer_idx) { + auto layer_issues = check_layer_stability(po, layer_idx, true, params); + if (!layer_issues.empty()) { + issues.add(layer_issues); + } + } + return issues; + }, + [](Issues left, const Issues &right) { + left.add(right); + return left; + } + ); #ifdef DEBUG_FILES - Impl::debug_export(issues, "issues"); + Impl::debug_export(found_issues, "issues"); #endif -// tbb::parallel_for(tbb::blocked_range(0, suspicious_layers_indices.size()), -// [&](tbb::blocked_range r) { -// for (size_t layer_idx = r.begin(); layer_idx < r.end(); ++layer_idx) { -// check_layer_stability(po, suspicious_layers_indices[layer_idx], params); -// } -// }); - - return issues; + return found_issues; } } diff --git a/src/libslic3r/SupportableIssuesSearch.hpp b/src/libslic3r/SupportableIssuesSearch.hpp index 22289c923c..4ff0e32faa 100644 --- a/src/libslic3r/SupportableIssuesSearch.hpp +++ b/src/libslic3r/SupportableIssuesSearch.hpp @@ -8,8 +8,13 @@ namespace Slic3r { namespace SupportableIssues { struct Params { - float bridge_distance = 5.0f; - float limit_curvature = 0.25f; + float bridge_distance = 10.0f; + float limit_curvature = 0.3f; + + bool external_perimeter_first = false; + float max_unsupported_distance_factor = 0.0f; + float max_ex_perim_unsupported_distance_factor = 1.0f; + float bridge_distance_decrease_by_curvature_factor = 5.0f; float perimeter_length_diff_tolerance = 8.0f; float centroid_offset_tolerance = 1.0f; diff --git a/src/libslic3r/TriangleSelectorWrapper.cpp b/src/libslic3r/TriangleSelectorWrapper.cpp index 98d00bc985..02a3b8a1ab 100644 --- a/src/libslic3r/TriangleSelectorWrapper.cpp +++ b/src/libslic3r/TriangleSelectorWrapper.cpp @@ -16,10 +16,11 @@ void TriangleSelectorWrapper::enforce_spot(const Vec3f &point, float radius) { auto dist = AABBTreeIndirect::squared_distance_to_indexed_triangle_set(mesh.its.vertices, mesh.its.indices, triangles_tree, point, hit_face_index, hit_point); - if (dist < 0 || dist > radius) + if (dist < 0 || dist > radius * radius) { return; + } - std::unique_ptr cursor = std::make_unique(point, point, + std::unique_ptr cursor = std::make_unique(hit_point, point, radius, Transform3d::Identity(), TriangleSelector::ClippingPlane { }); selector.select_patch(hit_face_index, std::move(cursor), EnforcerBlockerType::ENFORCER, Transform3d::Identity(), diff --git a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp index df9cdce56f..f4c21a174b 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp @@ -39,6 +39,10 @@ private: // This map holds all translated description texts, so they can be easily referenced during layout calculations // etc. When language changes, GUI is recreated and this class constructed again, so the change takes effect. std::map m_desc; + + + bool has_backend_supports() const; + void reslice_FDM_supports(bool postpone_error_messages = false) const; }; From 1955cd066e14865755e4273064701158e0904d12 Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Tue, 12 Apr 2022 09:54:44 +0200 Subject: [PATCH 005/100] include external_perimeters_first option, change paramters accordingly --- src/libslic3r/SupportableIssuesSearch.cpp | 60 +++++++++++++---------- src/libslic3r/SupportableIssuesSearch.hpp | 1 - 2 files changed, 33 insertions(+), 28 deletions(-) diff --git a/src/libslic3r/SupportableIssuesSearch.cpp b/src/libslic3r/SupportableIssuesSearch.cpp index ea2ba41063..e60f859302 100644 --- a/src/libslic3r/SupportableIssuesSearch.cpp +++ b/src/libslic3r/SupportableIssuesSearch.cpp @@ -125,9 +125,29 @@ EdgeGridWrapper compute_layer_edge_grid(const Layer *layer) { return EdgeGridWrapper(scale_(min_region_flow_width), ex_polygons); } -coordf_t get_max_allowed_distance(ExtrusionRole role, coord_t flow_width, const Params ¶ms) { // <= distance / flow_width (can be larger for perimeter, if not external perimeter first) - if (!params.external_perimeter_first - && (role == ExtrusionRole::erExternalPerimeter || role == ExtrusionRole::erOverhangPerimeter)) { +//TODO needs revision +coordf_t get_flow_width(const LayerRegion *region, ExtrusionRole role) { + switch (role) { + case ExtrusionRole::erBridgeInfill: + return region->flow(FlowRole::frExternalPerimeter).scaled_width(); + case ExtrusionRole::erExternalPerimeter: + return region->flow(FlowRole::frExternalPerimeter).scaled_width(); + case ExtrusionRole::erGapFill: + return region->flow(FlowRole::frInfill).scaled_width(); + case ExtrusionRole::erPerimeter: + return region->flow(FlowRole::frPerimeter).scaled_width(); + case ExtrusionRole::erSolidInfill: + return region->flow(FlowRole::frSolidInfill).scaled_width(); + default: + return region->flow(FlowRole::frPerimeter).scaled_width(); + } +} + +coordf_t get_max_allowed_distance(ExtrusionRole role, coord_t flow_width, bool external_perimeters_first, + const Params ¶ms) { // <= distance / flow_width (can be larger for perimeter, if not external perimeter first) + if ((role == ExtrusionRole::erExternalPerimeter || role == ExtrusionRole::erOverhangPerimeter) + && !(external_perimeters_first) + ) { return params.max_ex_perim_unsupported_distance_factor * flow_width; } else { return params.max_unsupported_distance_factor * flow_width; @@ -136,16 +156,17 @@ coordf_t get_max_allowed_distance(ExtrusionRole role, coord_t flow_width, const Issues check_extrusion_entity_stability(const ExtrusionEntity *entity, float slice_z, - coordf_t flow_width, + const LayerRegion *layer_region, const EdgeGridWrapper &supported_grid, const Params ¶ms) { Issues issues { }; if (entity->is_collection()) { for (const auto *e : static_cast(entity)->entities) { - issues.add(check_extrusion_entity_stability(e, slice_z, flow_width, supported_grid, params)); + issues.add(check_extrusion_entity_stability(e, slice_z, layer_region, supported_grid, params)); } } else { //single extrusion path, with possible varying parameters + std::stack points { }; for (const auto &p : entity->as_polyline().points) { points.push(p); @@ -157,8 +178,10 @@ Issues check_extrusion_entity_stability(const ExtrusionEntity *entity, Vec2f tmp = unscale(points.top()).cast(); Vec3f prev_fpoint = Vec3f(tmp.x(), tmp.y(), slice_z); + coordf_t flow_width = get_flow_width(layer_region, entity->role()); + bool external_perimters_first = layer_region->region().config().external_perimeters_first; const coordf_t max_allowed_dist_from_prev_layer = get_max_allowed_distance(entity->role(), flow_width, - params); + external_perimters_first, params); while (!points.empty()) { Point point = points.top(); @@ -229,24 +252,6 @@ Issues check_extrusion_entity_stability(const ExtrusionEntity *entity, return issues; } -//TODO needs revision -coordf_t get_flow_width(const LayerRegion *region, ExtrusionRole role) { - switch (role) { - case ExtrusionRole::erBridgeInfill: - return region->flow(FlowRole::frExternalPerimeter).scaled_width(); - case ExtrusionRole::erExternalPerimeter: - return region->flow(FlowRole::frExternalPerimeter).scaled_width(); - case ExtrusionRole::erGapFill: - return region->flow(FlowRole::frInfill).scaled_width(); - case ExtrusionRole::erPerimeter: - return region->flow(FlowRole::frPerimeter).scaled_width(); - case ExtrusionRole::erSolidInfill: - return region->flow(FlowRole::frSolidInfill).scaled_width(); - default: - return region->flow(FlowRole::frPerimeter).scaled_width(); - } -} - Issues check_layer_stability(const PrintObject *po, size_t layer_idx, bool full_check, const Params ¶ms) { std::cout << "Checking: " << layer_idx << std::endl; if (layer_idx == 0) { @@ -262,7 +267,7 @@ Issues check_layer_stability(const PrintObject *po, size_t layer_idx, bool full_ for (const ExtrusionEntity *ex_entity : layer_region->perimeters.entities) { for (const ExtrusionEntity *perimeter : static_cast(ex_entity)->entities) { issues.add(check_extrusion_entity_stability(perimeter, - layer->slice_z, get_flow_width(layer_region, perimeter->role()), + layer->slice_z, layer_region, supported_grid, params)); } // perimeter } // ex_entity @@ -270,12 +275,13 @@ Issues check_layer_stability(const PrintObject *po, size_t layer_idx, bool full_ for (const ExtrusionEntity *fill : static_cast(ex_entity)->entities) { if (fill->role() == ExtrusionRole::erGapFill || fill->role() == ExtrusionRole::erBridgeInfill) { issues.add(check_extrusion_entity_stability(fill, - layer->slice_z, get_flow_width(layer_region, fill->role()), + layer->slice_z, layer_region, supported_grid, params)); } } // fill } // ex_entity } // region + } else { //check only external perimeters for (const LayerRegion *layer_region : layer->regions()) { for (const ExtrusionEntity *ex_entity : layer_region->perimeters.entities) { @@ -283,7 +289,7 @@ Issues check_layer_stability(const PrintObject *po, size_t layer_idx, bool full_ if (perimeter->role() == ExtrusionRole::erExternalPerimeter || perimeter->role() == ExtrusionRole::erOverhangPerimeter) { issues.add(check_extrusion_entity_stability(perimeter, - layer->slice_z, get_flow_width(layer_region, perimeter->role()), + layer->slice_z, layer_region, supported_grid, params)); }; // ex_perimeter } // perimeter diff --git a/src/libslic3r/SupportableIssuesSearch.hpp b/src/libslic3r/SupportableIssuesSearch.hpp index 4ff0e32faa..80127520af 100644 --- a/src/libslic3r/SupportableIssuesSearch.hpp +++ b/src/libslic3r/SupportableIssuesSearch.hpp @@ -11,7 +11,6 @@ struct Params { float bridge_distance = 10.0f; float limit_curvature = 0.3f; - bool external_perimeter_first = false; float max_unsupported_distance_factor = 0.0f; float max_ex_perim_unsupported_distance_factor = 1.0f; float bridge_distance_decrease_by_curvature_factor = 5.0f; From c14b4a5d2e44e53c4c5e4e797b9eafa2f7337318 Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Tue, 12 Apr 2022 13:58:01 +0200 Subject: [PATCH 006/100] quick search simplified, removed expensive layer estimators added explanations and comments --- src/libslic3r/SupportableIssuesSearch.cpp | 125 ++++++---------------- src/libslic3r/SupportableIssuesSearch.hpp | 8 +- 2 files changed, 36 insertions(+), 97 deletions(-) diff --git a/src/libslic3r/SupportableIssuesSearch.cpp b/src/libslic3r/SupportableIssuesSearch.cpp index e60f859302..6510c35468 100644 --- a/src/libslic3r/SupportableIssuesSearch.cpp +++ b/src/libslic3r/SupportableIssuesSearch.cpp @@ -33,12 +33,6 @@ bool Issues::empty() const { namespace Impl { -struct LayerDescriptor { - Vec2f centroid { 0.0f, 0.0f }; - size_t segments_count { 0 }; - float perimeter_length { 0.0f }; -}; - struct EdgeGridWrapper { EdgeGridWrapper(coord_t edge_width, ExPolygons ex_polys) : ex_polys(ex_polys), edge_width(edge_width) { @@ -50,6 +44,7 @@ struct EdgeGridWrapper { bool signed_distance(const Point &point, coordf_t point_width, coordf_t &dist_out) const { coordf_t tmp_dist_out; bool found = grid.signed_distance(point, edge_width, tmp_dist_out); + // decrease the distance by half of edge width of previous layer and half of flow width of current layer dist_out = tmp_dist_out - edge_width / 2 - point_width / 2; return found; @@ -115,7 +110,7 @@ EdgeGridWrapper compute_layer_edge_grid(const Layer *layer) { Points perimeter_points { }; perimeter->collect_points(perimeter_points); assert(perimeter->is_loop()); - perimeter_points.pop_back(); + perimeter_points.pop_back(); // EdgeGrid structure does not like repetition of the first/last point ex_polygons.push_back(ExPolygon { perimeter_points }); } // ex_perimeter } // perimeter @@ -166,17 +161,18 @@ Issues check_extrusion_entity_stability(const ExtrusionEntity *entity, issues.add(check_extrusion_entity_stability(e, slice_z, layer_region, supported_grid, params)); } } else { //single extrusion path, with possible varying parameters - + //prepare stack of points on the extrusion path. If there are long segments, additional points might be pushed onto the stack during the algorithm. std::stack points { }; for (const auto &p : entity->as_polyline().points) { points.push(p); } - float unsupported_distance = params.bridge_distance + 1.0f; - float curvature = 0; - float max_curvature = 0; + float unsupported_distance = params.bridge_distance + 1.0f; // initialize unsupported distance with larger than tolerable distance -> + // -> it prevents extruding perimeter start and short loops into air. + float curvature = 0; // current curvature of the unsupported part of the extrusion - it is accumulated value of signed ccw angles of continuously unsupported points. + float max_curvature = 0; // max curvature (in abs value) for the current unsupported segment. Vec2f tmp = unscale(points.top()).cast(); - Vec3f prev_fpoint = Vec3f(tmp.x(), tmp.y(), slice_z); + Vec3f prev_fpoint = Vec3f(tmp.x(), tmp.y(), slice_z); // prev point of the path. Initialize with first point. coordf_t flow_width = get_flow_width(layer_region, entity->role()); bool external_perimters_first = layer_region->region().config().external_perimeters_first; @@ -190,30 +186,33 @@ Issues check_extrusion_entity_stability(const ExtrusionEntity *entity, Vec3f fpoint = Vec3f(tmp.x(), tmp.y(), slice_z); coordf_t dist_from_prev_layer { 0 }; - if (!supported_grid.signed_distance(point, flow_width, dist_from_prev_layer)) { + if (!supported_grid.signed_distance(point, flow_width, dist_from_prev_layer)) { // dist from prev layer not found, assume empty layer issues.supports_nedded.push_back(fpoint); - continue; + unsupported_distance = 0; + curvature = 0; + max_curvature = 0; } - if (dist_from_prev_layer > max_allowed_dist_from_prev_layer) { //unsupported - unsupported_distance += (fpoint - prev_fpoint).norm(); + if (dist_from_prev_layer > max_allowed_dist_from_prev_layer) { //extrusion point is unsupported + unsupported_distance += (fpoint - prev_fpoint).norm(); // for algortihm simplicity, expect that the whole line between prev and current point is unsupported if (!points.empty()) { const Vec2f v1 = (fpoint - prev_fpoint).head<2>(); const Vec2f v2 = unscale(points.top()).cast() - fpoint.head<2>(); float dot = v1(0) * v2(0) + v1(1) * v2(1); float cross = v1(0) * v2(1) - v1(1) * v2(0); - float angle = float(atan2(float(cross), float(dot))); + float angle = float(atan2(float(cross), float(dot))); // ccw angle, TODO replace with angle func, once it gets into master curvature += angle; max_curvature = std::max(abs(curvature), max_curvature); } - if (unsupported_distance - > params.bridge_distance - / (1.0f + (max_curvature * params.bridge_distance_decrease_by_curvature_factor / PI))) { + if (unsupported_distance // if unsupported distance is larger than bridge distance linearly decreased by curvature, enforce supports. + > params.bridge_distance + / (1.0f + (max_curvature * params.bridge_distance_decrease_by_curvature_factor / PI))) { issues.supports_nedded.push_back(fpoint); + //DEBUG stuff TODO remove std::cout << "SUPP: " << "udis: " << unsupported_distance << " curv: " << curvature << " max curv: " << max_curvature << std::endl; std::cout << "max dist from layer: " << max_allowed_dist_from_prev_layer << " measured dist: " @@ -229,15 +228,17 @@ Issues check_extrusion_entity_stability(const ExtrusionEntity *entity, max_curvature = 0; } + // Estimation of short curvy segments which are not supported -> problems with curling + // Currently the curling issues are ignored if (max_curvature / (PI * unsupported_distance) > params.limit_curvature) { issues.curling_up.push_back(fpoint); } prev_fpoint = fpoint; - if (!points.empty()) { + if (!points.empty()) { //oversampling if necessary Vec2f next = unscale(points.top()).cast(); - Vec2f reverse_v = fpoint.head<2>() - next; + Vec2f reverse_v = fpoint.head<2>() - next; // vector from next to current float dist_to_next = reverse_v.norm(); reverse_v.normalize(); int new_points_count = dist_to_next / params.bridge_distance; @@ -259,10 +260,11 @@ Issues check_layer_stability(const PrintObject *po, size_t layer_idx, bool full_ return {}; } const Layer *layer = po->get_layer(layer_idx); + //Prepare edge grid of previous layer, will be used to check if the extrusion path is supported EdgeGridWrapper supported_grid = compute_layer_edge_grid(layer->lower_layer); Issues issues { }; - if (full_check) { + if (full_check) { // If full checkm check stability of perimeters, gap fills, and bridges. for (const LayerRegion *layer_region : layer->regions()) { for (const ExtrusionEntity *ex_entity : layer_region->perimeters.entities) { for (const ExtrusionEntity *perimeter : static_cast(ex_entity)->entities) { @@ -282,7 +284,7 @@ Issues check_layer_stability(const PrintObject *po, size_t layer_idx, bool full_ } // ex_entity } // region - } else { //check only external perimeters + } else { // If NOT full check, check only external perimeters for (const LayerRegion *layer_region : layer->regions()) { for (const ExtrusionEntity *ex_entity : layer_region->perimeters.entities) { for (const ExtrusionEntity *perimeter : static_cast(ex_entity)->entities) { @@ -304,85 +306,24 @@ Issues check_layer_stability(const PrintObject *po, size_t layer_idx, bool full_ std::vector quick_search(const PrintObject *po, const Params ¶ms) { using namespace Impl; - std::vector descriptors(po->layer_count()); - tbb::parallel_for(tbb::blocked_range(0, po->layer_count()), + size_t layer_count = po->layer_count(); + std::vector layer_needs_supports(layer_count, false); + tbb::parallel_for(tbb::blocked_range(1, layer_count), [&](tbb::blocked_range r) { for (size_t layer_idx = r.begin(); layer_idx < r.end(); ++layer_idx) { - const Layer *layer = po->get_layer(layer_idx); - - LayerDescriptor &descriptor = descriptors[layer_idx]; - size_t point_count { 0 }; - - for (const LayerRegion *layer_region : layer->regions()) { - for (const ExtrusionEntity *ex_entity : layer_region->perimeters.entities) { - for (const ExtrusionEntity *perimeter : static_cast(ex_entity)->entities) { - if (perimeter->role() == ExtrusionRole::erExternalPerimeter || perimeter->role() == ExtrusionRole::erOverhangPerimeter) { - assert(perimeter->is_loop()); - descriptor.segments_count++; - const ExtrusionLoop *loop = static_cast(perimeter); - for (const ExtrusionPath& path : loop->paths) { - Vec2f prev_pos = unscale(path.polyline.last_point()).cast(); - for (size_t p_idx = 0; p_idx < path.polyline.points.size(); ++p_idx) { - point_count++; - Vec2f point_pos = unscale(path.polyline.points[p_idx]).cast(); - descriptor.centroid += point_pos; - descriptor.perimeter_length += (point_pos - prev_pos).norm(); - prev_pos = point_pos; - } //point - } //path - } // ex_perimeter - } // perimeter -} // ex_entity -} // region - - descriptor.centroid /= float(point_count); - - } // layer - } // thread - ); - - std::vector suspicious_layers_indices { }; - - for (size_t desc_idx = 1; desc_idx < descriptors.size(); ++desc_idx) { - const LayerDescriptor &prev = descriptors[desc_idx - 1]; - const LayerDescriptor &descriptor = descriptors[desc_idx]; - if (descriptor.segments_count - prev.segments_count != 0 - || - std::abs(descriptor.perimeter_length - prev.perimeter_length) - > params.perimeter_length_diff_tolerance || - (descriptor.centroid - prev.centroid).norm() > params.centroid_offset_tolerance - ) { - suspicious_layers_indices.push_back(desc_idx); - } -#ifdef DEBUG_FILES - std::cout << "SIS layer idx: " << desc_idx << " reg count: " << descriptor.segments_count << " len: " - << descriptor.perimeter_length << - " centroid: " << descriptor.centroid.x() << " | " << descriptor.centroid.y() << std::endl; - std::cout << "SIS diff: " << desc_idx << " reg count: " - << (int(descriptor.segments_count) - int(prev.segments_count)) << - " len: " << (descriptor.perimeter_length - prev.perimeter_length) << - " centroid: " << (descriptor.centroid - prev.centroid).norm() << std::endl; -#endif - } - - std::vector layer_needs_supports(suspicious_layers_indices.size(), false); - tbb::parallel_for(tbb::blocked_range(0, suspicious_layers_indices.size()), - [&](tbb::blocked_range r) { - for (size_t suspicious_index = r.begin(); suspicious_index < r.end(); ++suspicious_index) { - auto layer_issues = check_layer_stability(po, suspicious_layers_indices[suspicious_index], + auto layer_issues = check_layer_stability(po, layer_idx, false, params); if (!layer_issues.supports_nedded.empty()) { - layer_needs_supports[suspicious_index] = true; + layer_needs_supports[layer_idx] = true; } } }); std::vector problematic_layers; - - for (size_t index = 0; index < suspicious_layers_indices.size(); ++index) { + for (size_t index = 0; index < layer_needs_supports.size(); ++index) { if (layer_needs_supports[index]) { - problematic_layers.push_back(suspicious_layers_indices[index]); + problematic_layers.push_back(index); } } return problematic_layers; diff --git a/src/libslic3r/SupportableIssuesSearch.hpp b/src/libslic3r/SupportableIssuesSearch.hpp index 80127520af..4e8a541f94 100644 --- a/src/libslic3r/SupportableIssuesSearch.hpp +++ b/src/libslic3r/SupportableIssuesSearch.hpp @@ -9,14 +9,12 @@ namespace SupportableIssues { struct Params { float bridge_distance = 10.0f; - float limit_curvature = 0.3f; + float limit_curvature = 0.3f; // used to detect curling issues, but they are currently not considered anyway float max_unsupported_distance_factor = 0.0f; + // allow printing external perimeter in the air to some extent. it hopefully attaches to the internal perimeter. float max_ex_perim_unsupported_distance_factor = 1.0f; - float bridge_distance_decrease_by_curvature_factor = 5.0f; - - float perimeter_length_diff_tolerance = 8.0f; - float centroid_offset_tolerance = 1.0f; + float bridge_distance_decrease_by_curvature_factor = 5.0f; // allowed bridge distance = bridge_distance / ( 1 + this factor * (curvature / PI) ) }; From a46e1dc79c023897e781c3158e6754ea4bd37aaf Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Thu, 14 Apr 2022 17:21:14 +0200 Subject: [PATCH 007/100] initial works on EdgeGrid alternative --- src/libslic3r/CMakeLists.txt | 2 + src/libslic3r/PolygonPointTest.cpp | 1 + src/libslic3r/PolygonPointTest.hpp | 73 +++++++++++++++++++++++ src/libslic3r/SupportableIssuesSearch.cpp | 26 +------- 4 files changed, 78 insertions(+), 24 deletions(-) create mode 100644 src/libslic3r/PolygonPointTest.cpp create mode 100644 src/libslic3r/PolygonPointTest.hpp diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index 0a0062a754..443c81b486 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -201,6 +201,8 @@ set(SLIC3R_SOURCES Point.hpp Polygon.cpp Polygon.hpp + PolygonPointTest.cpp + PolygonPointTest.hpp MutablePolygon.cpp MutablePolygon.hpp PolygonTrimmer.cpp diff --git a/src/libslic3r/PolygonPointTest.cpp b/src/libslic3r/PolygonPointTest.cpp new file mode 100644 index 0000000000..dcab9e87eb --- /dev/null +++ b/src/libslic3r/PolygonPointTest.cpp @@ -0,0 +1 @@ +#include "PolygonPointTest.hpp" diff --git a/src/libslic3r/PolygonPointTest.hpp b/src/libslic3r/PolygonPointTest.hpp new file mode 100644 index 0000000000..79e6b4ab5c --- /dev/null +++ b/src/libslic3r/PolygonPointTest.hpp @@ -0,0 +1,73 @@ +#ifndef SRC_LIBSLIC3R_POLYGONPOINTTEST_HPP_ +#define SRC_LIBSLIC3R_POLYGONPOINTTEST_HPP_ + +#include "libslic3r/Point.hpp" +#include "libslic3r/EdgeGrid.hpp" + +namespace Slic3r { + +struct EdgeGridWrapper { + EdgeGridWrapper(coord_t resolution, ExPolygons ex_polys) : + ex_polys(ex_polys) { + + grid.create(this->ex_polys, resolution); + grid.calculate_sdf(); + } + + bool signed_distance(const Point &point, coordf_t point_width, coordf_t &dist_out) const { + coordf_t tmp_dist_out; + bool found = grid.signed_distance(point, point_width, tmp_dist_out); + // decrease the distance by half of edge width of previous layer and half of flow width of current layer + dist_out = tmp_dist_out - point_width / 2; + return found; + + } + + EdgeGrid::Grid grid; + ExPolygons ex_polys; +}; + +class PolygonPointTest { +public: + PolygonPointTest(const ExPolygons &ex_polygons) { + std::vector lines; + for (const auto &exp : ex_polygons) { + Lines contour = exp.contour.lines(); + lines.insert(lines.end(), contour.begin(), contour.end()); + for (const auto &hole : exp.holes) { + Lines hole_lines = hole.lines(); + for (Line &line : hole_lines) { + line.reverse(); // reverse hole lines, so that we can use normal to deduce where the object is + } + lines.insert(lines.end(), hole_lines.begin(), hole_lines.end()); + } + } + + std::vector> sweeping_data(lines.size()); + sweeping_data.reserve(lines.size() * 2); + for (int line_index = 0; line_index < lines.size(); ++line_index) { + sweeping_data[line_index].first = line_index; + sweeping_data[line_index].second = true; + } + + const auto data_comparator = [&lines](const std::pair &left, + const std::pair &right) { + return std::min(lines[left.first].a.x(), lines[left.first].b.x()) + < std::min(lines[right.first].a.x(), lines[right.first].b.x()); + }; + + std::make_heap(sweeping_data.begin(), sweeping_data.end(), data_comparator); + std::set active_lines; + + //TODO continue + + + + + } + +}; + +} + +#endif /* SRC_LIBSLIC3R_POLYGONPOINTTEST_HPP_ */ diff --git a/src/libslic3r/SupportableIssuesSearch.cpp b/src/libslic3r/SupportableIssuesSearch.cpp index 6510c35468..39ded7648b 100644 --- a/src/libslic3r/SupportableIssuesSearch.cpp +++ b/src/libslic3r/SupportableIssuesSearch.cpp @@ -8,8 +8,8 @@ #include #include "libslic3r/Layer.hpp" -#include "libslic3r/EdgeGrid.hpp" #include "libslic3r/ClipperUtils.hpp" +#include "PolygonPointTest.hpp" #define DEBUG_FILES @@ -33,28 +33,6 @@ bool Issues::empty() const { namespace Impl { -struct EdgeGridWrapper { - EdgeGridWrapper(coord_t edge_width, ExPolygons ex_polys) : - ex_polys(ex_polys), edge_width(edge_width) { - - grid.create(this->ex_polys, edge_width); - grid.calculate_sdf(); - } - - bool signed_distance(const Point &point, coordf_t point_width, coordf_t &dist_out) const { - coordf_t tmp_dist_out; - bool found = grid.signed_distance(point, edge_width, tmp_dist_out); - // decrease the distance by half of edge width of previous layer and half of flow width of current layer - dist_out = tmp_dist_out - edge_width / 2 - point_width / 2; - return found; - - } - - EdgeGrid::Grid grid; - ExPolygons ex_polys; - coord_t edge_width; -}; - #ifdef DEBUG_FILES void debug_export(Issues issues, std::string file_name) { Slic3r::CNumericLocalesSetter locales_setter; @@ -194,7 +172,7 @@ Issues check_extrusion_entity_stability(const ExtrusionEntity *entity, } if (dist_from_prev_layer > max_allowed_dist_from_prev_layer) { //extrusion point is unsupported - unsupported_distance += (fpoint - prev_fpoint).norm(); // for algortihm simplicity, expect that the whole line between prev and current point is unsupported + unsupported_distance += (fpoint - prev_fpoint).norm(); // for algorithm simplicity, expect that the whole line between prev and current point is unsupported if (!points.empty()) { const Vec2f v1 = (fpoint - prev_fpoint).head<2>(); From cfe9b27a6d1f9889196ae901d7751c20548b5c17 Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Fri, 22 Apr 2022 09:47:30 +0200 Subject: [PATCH 008/100] refactoring, initial work on weight distribution matrix --- src/libslic3r/PolygonPointTest.hpp | 49 +++-- src/libslic3r/SupportableIssuesSearch.cpp | 240 +++++++++++++++++----- src/libslic3r/SupportableIssuesSearch.hpp | 8 +- 3 files changed, 224 insertions(+), 73 deletions(-) diff --git a/src/libslic3r/PolygonPointTest.hpp b/src/libslic3r/PolygonPointTest.hpp index 79e6b4ab5c..d95b191bf6 100644 --- a/src/libslic3r/PolygonPointTest.hpp +++ b/src/libslic3r/PolygonPointTest.hpp @@ -7,27 +7,37 @@ namespace Slic3r { struct EdgeGridWrapper { - EdgeGridWrapper(coord_t resolution, ExPolygons ex_polys) : - ex_polys(ex_polys) { + EdgeGridWrapper(coord_t edge_width, std::vector lines) : + lines(lines), edge_width(edge_width) { - grid.create(this->ex_polys, resolution); + grid.create(this->lines, edge_width, true); grid.calculate_sdf(); } bool signed_distance(const Point &point, coordf_t point_width, coordf_t &dist_out) const { coordf_t tmp_dist_out; - bool found = grid.signed_distance(point, point_width, tmp_dist_out); - // decrease the distance by half of edge width of previous layer and half of flow width of current layer - dist_out = tmp_dist_out - point_width / 2; + bool found = grid.signed_distance(point, edge_width, tmp_dist_out); + dist_out = tmp_dist_out - edge_width / 2 - point_width / 2; return found; } EdgeGrid::Grid grid; - ExPolygons ex_polys; + std::vector lines; + coord_t edge_width; }; +namespace TODO { + class PolygonPointTest { + + struct Segment { + coord_t start; + std::vector lines; + }; + + std::vector x_coord_segments; + public: PolygonPointTest(const ExPolygons &ex_polygons) { std::vector lines; @@ -43,30 +53,37 @@ public: } } - std::vector> sweeping_data(lines.size()); - sweeping_data.reserve(lines.size() * 2); - for (int line_index = 0; line_index < lines.size(); ++line_index) { + std::vector> sweeping_data(lines.size() * 2); + for (size_t line_index = 0; line_index < lines.size(); ++line_index) { sweeping_data[line_index].first = line_index; sweeping_data[line_index].second = true; + sweeping_data[line_index * 2 + 1].first = line_index; + sweeping_data[line_index * 2 + 1].second = false; } const auto data_comparator = [&lines](const std::pair &left, const std::pair &right) { - return std::min(lines[left.first].a.x(), lines[left.first].b.x()) - < std::min(lines[right.first].a.x(), lines[right.first].b.x()); + const auto left_x = + left.second ? + std::min(lines[left.first].a.x(), lines[left.first].b.x()) : + std::max(lines[left.first].a.x(), lines[left.first].b.x()); + const auto right_x = + right.second ? + std::min(lines[right.first].a.x(), lines[right.first].b.x()) : + std::max(lines[right.first].a.x(), lines[right.first].b.x()); + + return left_x < right_x; }; - std::make_heap(sweeping_data.begin(), sweeping_data.end(), data_comparator); + std::sort(sweeping_data.begin(), sweeping_data.end(), data_comparator); std::set active_lines; //TODO continue - - - } }; +} } diff --git a/src/libslic3r/SupportableIssuesSearch.cpp b/src/libslic3r/SupportableIssuesSearch.cpp index 39ded7648b..ee1f440608 100644 --- a/src/libslic3r/SupportableIssuesSearch.cpp +++ b/src/libslic3r/SupportableIssuesSearch.cpp @@ -31,6 +31,119 @@ bool Issues::empty() const { return supports_nedded.empty() && curling_up.empty(); } +struct Cell { + float weight; + char last_extrusion_id; +}; + +struct WeightDistributionMatrix { + // Lets make Z coord half the size of X (and Y). + // This corresponds to angle of ~26 degrees between center of one cell and other one up and sideways + // which is approximately a limiting printable angle. + + WeightDistributionMatrix(const PrintObject *po, size_t layer_idx_begin, size_t layer_idx_end) { + Vec3crd object_origin = scaled(po->trafo_centered() * Vec3d::Zero()); + Vec3crd min = object_origin - po->size() / 2 - Vec3crd::Ones(); + Vec3crd max = object_origin + po->size() / 2 + Vec3crd::Ones(); + + cell_size = Vec3crd { int(cell_height * 2), int(cell_height * 2), int(cell_height) }; + + global_origin = min; + global_size = max - min; + global_cell_count = global_size.cwiseQuotient(cell_size); + + coord_t local_min_z = scale_(po->layers()[layer_idx_begin]->slice_z); + coord_t local_max_z = scale_(po->layers()[layer_idx_end]->slice_z); + coord_t local_min_z_index = local_min_z / cell_size.z(); + coord_t local_max_z_index = local_max_z / cell_size.z(); + + local_z_index_offset = local_min_z_index; + local_z_cell_count = local_max_z_index - local_min_z_index + 1; + + cells.resize(local_z_cell_count * global_cell_count.y() * global_cell_count.x()); + } + + Vec3i to_global_cell_coords(const Point &p, float slice_z) const { + Vec3crd position = Vec3crd { p.x(), p.y(), coord_t(scale_(slice_z)) }; + Vec3i cell_coords = position.cwiseQuotient(cell_size); + return cell_coords; + } + + Vec3i to_local_cell_coords(const Point &p, float slice_z) const { + Vec3i cell_coords = to_global_cell_coords(p, slice_z); + Vec3i local_cell_coords = cell_coords - local_z_index_offset * Vec3i::UnitZ(); + return local_cell_coords; + } + + size_t to_cell_index(const Vec3i &local_cell_coords) { + assert(local_cell_coords.x() >= 0); + assert(local_cell_coords.x() < global_cell_count.x()); + assert(local_cell_coords.y() >= 0); + assert(local_cell_coords.y() < global_cell_count.y()); + assert(local_cell_coords.z() >= 0); + assert(local_cell_coords.z() < local_z_cell_count); + return local_cell_coords.z() * global_cell_count.x() * global_cell_count.y() + + local_cell_coords.y() * global_cell_count.x() + + local_cell_coords.x(); + } + + Vec3crd cell_center(const Vec3i &global_cell_coords) { + return global_origin + global_cell_coords.cwiseProduct(cell_size); + } + + Cell& access_cell(const Point &p, float slice_z) { + return cells[to_cell_index(to_local_cell_coords(p, slice_z))]; + } + + Cell& access_cell(const Vec3i& local_cell_coords) { + return cells[to_cell_index(local_cell_coords)]; + } + + +#ifdef DEBUG_FILES + void debug_export(std::string file_name) { + Slic3r::CNumericLocalesSetter locales_setter; + { + FILE *fp = boost::nowide::fopen(debug_out_path((file_name + "_matrix.obj").c_str()).c_str(), "w"); + if (fp == nullptr) { + BOOST_LOG_TRIVIAL(error) + << "Debug files: Couldn't open " << file_name << " for writing"; + return; + } + + for (int x = 0; x < global_cell_count.x(); ++x) { + for (int y = 0; y < global_cell_count.y(); ++y) { + for (int z = 0; z < local_z_cell_count; ++z) { + Vec3f center = unscale(cell_center(Vec3i(x, y, z + local_z_index_offset))).cast(); + Cell &cell = access_cell(Vec3i(x, y, z)); + fprintf(fp, "v %f %f %f %f %f %f\n", + center(0), center(1), + center(2), + cell.weight, 0.0, 0.0 + ); + } + } + } + + fclose(fp); + } + } +#endif + + static constexpr float cell_height = scale_(0.15f); + + Vec3crd cell_size; + + Vec3crd global_origin; + Vec3crd global_size; + Vec3i global_cell_count; + + int local_z_index_offset; + int local_z_cell_count; + std::vector cells; + +}; + namespace Impl { #ifdef DEBUG_FILES @@ -79,23 +192,20 @@ EdgeGridWrapper compute_layer_edge_grid(const Layer *layer) { for (const auto *region : layer->regions()) { min_region_flow_width = std::min(min_region_flow_width, region->flow(FlowRole::frExternalPerimeter).width()); } - ExPolygons ex_polygons; + std::vector lines; for (const LayerRegion *layer_region : layer->regions()) { for (const ExtrusionEntity *ex_entity : layer_region->perimeters.entities) { - for (const ExtrusionEntity *perimeter : static_cast(ex_entity)->entities) { - if (perimeter->role() == ExtrusionRole::erExternalPerimeter - || perimeter->role() == ExtrusionRole::erOverhangPerimeter) { - Points perimeter_points { }; - perimeter->collect_points(perimeter_points); - assert(perimeter->is_loop()); - perimeter_points.pop_back(); // EdgeGrid structure does not like repetition of the first/last point - ex_polygons.push_back(ExPolygon { perimeter_points }); - } // ex_perimeter - } // perimeter + lines.push_back(Points { }); + ex_entity->collect_points(lines.back()); + } // ex_entity + + for (const ExtrusionEntity *ex_entity : layer_region->fills.entities) { + lines.push_back(Points { }); + ex_entity->collect_points(lines.back()); } // ex_entity } - return EdgeGridWrapper(scale_(min_region_flow_width), ex_polygons); + return EdgeGridWrapper(scale_(min_region_flow_width), lines); } //TODO needs revision @@ -119,14 +229,36 @@ coordf_t get_flow_width(const LayerRegion *region, ExtrusionRole role) { coordf_t get_max_allowed_distance(ExtrusionRole role, coord_t flow_width, bool external_perimeters_first, const Params ¶ms) { // <= distance / flow_width (can be larger for perimeter, if not external perimeter first) if ((role == ExtrusionRole::erExternalPerimeter || role == ExtrusionRole::erOverhangPerimeter) - && !(external_perimeters_first) + && (external_perimeters_first) ) { - return params.max_ex_perim_unsupported_distance_factor * flow_width; + return params.max_first_ex_perim_unsupported_distance_factor * flow_width; } else { return params.max_unsupported_distance_factor * flow_width; } } +struct SegmentAccumulator { + float distance = 0; //accumulated distance + float curvature = 0; //accumulated signed ccw angles + float max_curvature = 0; //max absolute accumulated value + + void add_distance(float dist) { + distance += dist; + } + + void add_angle(float ccw_angle) { + curvature += ccw_angle; + max_curvature = std::max(max_curvature, std::abs(curvature)); + } + + void reset() { + distance = 0; + curvature = 0; + max_curvature = 0; + } + +}; + Issues check_extrusion_entity_stability(const ExtrusionEntity *entity, float slice_z, const LayerRegion *layer_region, @@ -145,13 +277,17 @@ Issues check_extrusion_entity_stability(const ExtrusionEntity *entity, points.push(p); } - float unsupported_distance = params.bridge_distance + 1.0f; // initialize unsupported distance with larger than tolerable distance -> + SegmentAccumulator supports_acc { }; + supports_acc.add_distance(params.bridge_distance + 1.0f); // initialize unsupported distance with larger than tolerable distance -> // -> it prevents extruding perimeter start and short loops into air. - float curvature = 0; // current curvature of the unsupported part of the extrusion - it is accumulated value of signed ccw angles of continuously unsupported points. - float max_curvature = 0; // max curvature (in abs value) for the current unsupported segment. - Vec2f tmp = unscale(points.top()).cast(); - Vec3f prev_fpoint = Vec3f(tmp.x(), tmp.y(), slice_z); // prev point of the path. Initialize with first point. + SegmentAccumulator curling_acc { }; + const auto to_vec3f = [slice_z](const Point &point) { + Vec2f tmp = unscale(point).cast(); + return Vec3f(tmp.x(), tmp.y(), slice_z); + }; + + Vec3f prev_fpoint = to_vec3f(points.top()); // prev point of the path. Initialize with first point. coordf_t flow_width = get_flow_width(layer_region, entity->role()); bool external_perimters_first = layer_region->region().config().external_perimeters_first; const coordf_t max_allowed_dist_from_prev_layer = get_max_allowed_distance(entity->role(), flow_width, @@ -162,54 +298,50 @@ Issues check_extrusion_entity_stability(const ExtrusionEntity *entity, points.pop(); Vec2f tmp = unscale(point).cast(); Vec3f fpoint = Vec3f(tmp.x(), tmp.y(), slice_z); + float edge_len = (fpoint - prev_fpoint).norm(); coordf_t dist_from_prev_layer { 0 }; if (!supported_grid.signed_distance(point, flow_width, dist_from_prev_layer)) { // dist from prev layer not found, assume empty layer issues.supports_nedded.push_back(fpoint); - unsupported_distance = 0; - curvature = 0; - max_curvature = 0; + supports_acc.reset(); } + float angle = 0; + if (!points.empty()) { + const Vec2f v1 = (fpoint - prev_fpoint).head<2>(); + const Vec2f v2 = unscale(points.top()).cast() - fpoint.head<2>(); + float dot = v1(0) * v2(0) + v1(1) * v2(1); + float cross = v1(0) * v2(1) - v1(1) * v2(0); + angle = float(atan2(float(cross), float(dot))); // ccw angle, TODO replace with angle func, once it gets into master + } + + supports_acc.add_angle(angle); + curling_acc.add_angle(angle); + if (dist_from_prev_layer > max_allowed_dist_from_prev_layer) { //extrusion point is unsupported - unsupported_distance += (fpoint - prev_fpoint).norm(); // for algorithm simplicity, expect that the whole line between prev and current point is unsupported + supports_acc.add_distance(edge_len); // for algorithm simplicity, expect that the whole line between prev and current point is unsupported - if (!points.empty()) { - const Vec2f v1 = (fpoint - prev_fpoint).head<2>(); - const Vec2f v2 = unscale(points.top()).cast() - fpoint.head<2>(); - float dot = v1(0) * v2(0) + v1(1) * v2(1); - float cross = v1(0) * v2(1) - v1(1) * v2(0); - float angle = float(atan2(float(cross), float(dot))); // ccw angle, TODO replace with angle func, once it gets into master - - curvature += angle; - max_curvature = std::max(abs(curvature), max_curvature); - } - - if (unsupported_distance // if unsupported distance is larger than bridge distance linearly decreased by curvature, enforce supports. + if (supports_acc.distance // if unsupported distance is larger than bridge distance linearly decreased by curvature, enforce supports. > params.bridge_distance - / (1.0f + (max_curvature * params.bridge_distance_decrease_by_curvature_factor / PI))) { + / (1.0f + + (supports_acc.max_curvature + * params.bridge_distance_decrease_by_curvature_factor / PI))) { issues.supports_nedded.push_back(fpoint); - - //DEBUG stuff TODO remove - std::cout << "SUPP: " << "udis: " << unsupported_distance << " curv: " << curvature << " max curv: " - << max_curvature << std::endl; - std::cout << "max dist from layer: " << max_allowed_dist_from_prev_layer << " measured dist: " - << dist_from_prev_layer << " FW: " << flow_width << std::endl; - - unsupported_distance = 0; - curvature = 0; - max_curvature = 0; + supports_acc.reset(); } } else { - unsupported_distance = 0; - curvature = 0; - max_curvature = 0; + supports_acc.reset(); } // Estimation of short curvy segments which are not supported -> problems with curling - // Currently the curling issues are ignored - if (max_curvature / (PI * unsupported_distance) > params.limit_curvature) { - issues.curling_up.push_back(fpoint); + if (dist_from_prev_layer > 0.0f) { //extrusion point is unsupported or poorly supported + curling_acc.add_distance(edge_len); + if (curling_acc.max_curvature / (PI * curling_acc.distance) > params.limit_curvature) { + issues.curling_up.push_back(fpoint); + curling_acc.reset(); + } + } else { + curling_acc.reset(); } prev_fpoint = fpoint; @@ -309,6 +441,10 @@ std::vector quick_search(const PrintObject *po, const Params ¶ms) { Issues full_search(const PrintObject *po, const Params ¶ms) { using namespace Impl; + + WeightDistributionMatrix matrix { po, 0, po->layers().size() }; + matrix.debug_export("matrix"); + size_t layer_count = po->layer_count(); Issues found_issues = tbb::parallel_reduce(tbb::blocked_range(1, layer_count), Issues { }, [&](tbb::blocked_range r, const Issues &init) { diff --git a/src/libslic3r/SupportableIssuesSearch.hpp b/src/libslic3r/SupportableIssuesSearch.hpp index 4e8a541f94..08b09d143f 100644 --- a/src/libslic3r/SupportableIssuesSearch.hpp +++ b/src/libslic3r/SupportableIssuesSearch.hpp @@ -9,15 +9,13 @@ namespace SupportableIssues { struct Params { float bridge_distance = 10.0f; - float limit_curvature = 0.3f; // used to detect curling issues, but they are currently not considered anyway + float limit_curvature = 0.15f; // used to detect curling issues - float max_unsupported_distance_factor = 0.0f; - // allow printing external perimeter in the air to some extent. it hopefully attaches to the internal perimeter. - float max_ex_perim_unsupported_distance_factor = 1.0f; + float max_first_ex_perim_unsupported_distance_factor = 0.0f; // if external perim first, return tighter max allowed distance from previous layer extrusion + float max_unsupported_distance_factor = 1.0f; // For internal perimeters, infill, bridges etc, allow gap of [extrusion width] size, these extrusions have usually something to stick to. float bridge_distance_decrease_by_curvature_factor = 5.0f; // allowed bridge distance = bridge_distance / ( 1 + this factor * (curvature / PI) ) }; - struct Issues { std::vector supports_nedded; std::vector curling_up; From f0bdf2760c3262629f7fbdf782fa67475fde7c0e Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Mon, 25 Apr 2022 17:28:13 +0200 Subject: [PATCH 009/100] improved voxelization - fixed bugs with sinking objects. testing version of flooding the weight matrix --- src/libslic3r/SupportableIssuesSearch.cpp | 268 +++++++++++++++++----- 1 file changed, 209 insertions(+), 59 deletions(-) diff --git a/src/libslic3r/SupportableIssuesSearch.cpp b/src/libslic3r/SupportableIssuesSearch.cpp index ee1f440608..4082f17ca9 100644 --- a/src/libslic3r/SupportableIssuesSearch.cpp +++ b/src/libslic3r/SupportableIssuesSearch.cpp @@ -33,7 +33,7 @@ bool Issues::empty() const { struct Cell { float weight; - char last_extrusion_id; + int island_id; }; struct WeightDistributionMatrix { @@ -41,67 +41,173 @@ struct WeightDistributionMatrix { // This corresponds to angle of ~26 degrees between center of one cell and other one up and sideways // which is approximately a limiting printable angle. - WeightDistributionMatrix(const PrintObject *po, size_t layer_idx_begin, size_t layer_idx_end) { - Vec3crd object_origin = scaled(po->trafo_centered() * Vec3d::Zero()); - Vec3crd min = object_origin - po->size() / 2 - Vec3crd::Ones(); - Vec3crd max = object_origin + po->size() / 2 + Vec3crd::Ones(); + WeightDistributionMatrix() = default; + + void init(const PrintObject *po, size_t layer_idx_begin, size_t layer_idx_end) { + Vec2crd size_half = po->size().head<2>().cwiseQuotient(Vec2crd(2, 2)) + Vec2crd::Ones(); + Vec3crd min = Vec3crd(-size_half.x(), -size_half.y(), 0); + Vec3crd max = Vec3crd(size_half.x(), size_half.y(), po->height()); cell_size = Vec3crd { int(cell_height * 2), int(cell_height * 2), int(cell_height) }; + assert(cell_size.x() == cell_size.y()); global_origin = min; global_size = max - min; - global_cell_count = global_size.cwiseQuotient(cell_size); + global_cell_count = global_size.cwiseQuotient(cell_size) + Vec3i::Ones(); - coord_t local_min_z = scale_(po->layers()[layer_idx_begin]->slice_z); - coord_t local_max_z = scale_(po->layers()[layer_idx_end]->slice_z); - coord_t local_min_z_index = local_min_z / cell_size.z(); - coord_t local_max_z_index = local_max_z / cell_size.z(); + coord_t local_min_z = scale_(po->layers()[layer_idx_begin]->print_z); + coord_t local_max_z = scale_(po->layers()[layer_idx_end > 0 ? layer_idx_end - 1 : 0]->print_z); + int local_min_z_index = local_min_z / cell_size.z(); + int local_max_z_index = local_max_z / cell_size.z() + 1; local_z_index_offset = local_min_z_index; - local_z_cell_count = local_max_z_index - local_min_z_index + 1; + local_z_cell_count = local_max_z_index + 1 - local_min_z_index; cells.resize(local_z_cell_count * global_cell_count.y() * global_cell_count.x()); } - Vec3i to_global_cell_coords(const Point &p, float slice_z) const { - Vec3crd position = Vec3crd { p.x(), p.y(), coord_t(scale_(slice_z)) }; - Vec3i cell_coords = position.cwiseQuotient(cell_size); + Vec3i to_global_cell_coords(const Vec3i &local_cell_coords) const { + return local_cell_coords + local_z_index_offset * Vec3i::UnitZ(); + } + + Vec3i to_local_cell_coords(const Vec3i &global_cell_coords) const { + return global_cell_coords - local_z_index_offset * Vec3i::UnitZ(); + } + + Vec3i to_global_cell_coords(const Point &p, float print_z) const { + Vec3i position = Vec3crd { p.x(), p.y(), int(scale_(print_z)) }; + Vec3i cell_coords = (position - this->global_origin).cwiseQuotient(this->cell_size); return cell_coords; } - Vec3i to_local_cell_coords(const Point &p, float slice_z) const { - Vec3i cell_coords = to_global_cell_coords(p, slice_z); - Vec3i local_cell_coords = cell_coords - local_z_index_offset * Vec3i::UnitZ(); - return local_cell_coords; + Vec3i to_local_cell_coords(const Point &p, float print_z) const { + Vec3i cell_coords = this->to_global_cell_coords(p, print_z); + return this->to_local_cell_coords(cell_coords); } - size_t to_cell_index(const Vec3i &local_cell_coords) { + size_t to_cell_index(const Vec3i &local_cell_coords) const { assert(local_cell_coords.x() >= 0); assert(local_cell_coords.x() < global_cell_count.x()); assert(local_cell_coords.y() >= 0); assert(local_cell_coords.y() < global_cell_count.y()); assert(local_cell_coords.z() >= 0); assert(local_cell_coords.z() < local_z_cell_count); + return local_cell_coords.z() * global_cell_count.x() * global_cell_count.y() + local_cell_coords.y() * global_cell_count.x() + local_cell_coords.x(); } - Vec3crd cell_center(const Vec3i &global_cell_coords) { - return global_origin + global_cell_coords.cwiseProduct(cell_size); + Vec3crd get_cell_center(const Vec3i &global_cell_coords) const { + return global_origin + global_cell_coords.cwiseProduct(this->cell_size) + + this->cell_size.cwiseQuotient(Vec3crd(2, 2, 2)); } - Cell& access_cell(const Point &p, float slice_z) { - return cells[to_cell_index(to_local_cell_coords(p, slice_z))]; + Cell& access_cell(const Point &p, float print_z) { + return cells[this->to_cell_index(to_local_cell_coords(p, print_z))]; } - Cell& access_cell(const Vec3i& local_cell_coords) { - return cells[to_cell_index(local_cell_coords)]; + Cell& access_cell(const Vec3i &local_cell_coords) { + return cells[this->to_cell_index(local_cell_coords)]; } + const Cell& access_cell(const Vec3i &local_cell_coords) const { + return cells[this->to_cell_index(local_cell_coords)]; + } + + void ditribute_edge_weight(const Point &p1, const Point &p2, float print_z, coordf_t width) { + Vec2d dir = (p2 - p1).cast(); + double length = dir.norm(); + if (length < 0.01) { + return; + } + dir /= length; + double step_size = this->cell_size.x() / 2.0; + + double distributed_length = 0; + while (distributed_length < length) { + double next_len = std::min(length, distributed_length + step_size); + double current_dist_payload = next_len - distributed_length; + + Point location = p1 + ((next_len / length) * dir).cast(); + double payload = current_dist_payload * width; + + Vec3i local_index = this->to_local_cell_coords(location, print_z); + + if (this->to_cell_index(local_index) >= this->cells.size() || this->to_cell_index(local_index) < 0) { + std::cout << "loc: " << local_index.x() << " " << local_index.y() << " " << local_index.z() + << " globals: " << this->global_cell_count.x() << " " + << this->global_cell_count.y() << " " << this->local_z_cell_count << + "+" << this->local_z_cell_count << std::endl; + return; + } + this->access_cell(location, print_z).weight += payload; + + distributed_length = next_len; + } + } + + void merge(const WeightDistributionMatrix &other) { + int z_start = std::max(local_z_index_offset, other.local_z_index_offset); + int z_end = std::min(local_z_index_offset + local_z_cell_count, + other.local_z_index_offset + other.local_z_cell_count); + + for (int x = 0; x < global_cell_count.x(); ++x) { + for (int y = 0; y < global_cell_count.y(); ++y) { + for (int z = z_start; z < z_end; ++z) { + Vec3i global_coords { x, y, z }; + Vec3i local_coords = this->to_local_cell_coords(global_coords); + Vec3i other_local_coords = other.to_local_cell_coords(global_coords); + this->access_cell(local_coords).weight += other.access_cell(other_local_coords).weight; + } + } + } + } + + void distribute_top_down() { + const auto validate_xy_coords = [&](const Vec2i &local_coords) { + return local_coords.x() >= 0 && local_coords.y() >= 0 && + local_coords.x() < this->global_cell_count.x() && local_coords.y() < this->global_cell_count.y(); + }; + + Vec2i valid_coords[9]; + + for (int x = 0; x < global_cell_count.x(); ++x) { + for (int y = 0; y < global_cell_count.y(); ++y) { + for (int z = local_z_cell_count - 1; z > local_z_index_offset; --z) { + Cell ¤t = this->access_cell(Vec3i(x, y, z)); + size_t valid_coords_count = 0; + if (current.weight > 0) { + for (int y_offset = -1; y_offset <= 1; ++y_offset) { + for (int x_offset = -1; x_offset <= 1; ++x_offset) { + Vec2i xy_coords { x + x_offset, y + y_offset }; + if (validate_xy_coords(xy_coords) + && + this->access_cell(Vec3i(xy_coords.x(), xy_coords.y(), z - 1)).weight != 0) { + valid_coords[valid_coords_count] = xy_coords; + valid_coords_count++; + } + } + } + + float distribution = current.weight / valid_coords_count; + for (size_t index = 0; index < valid_coords_count; ++index) { + this->access_cell(Vec3i(valid_coords[index].x(), valid_coords[index].y(), z - 1)).weight += + distribution; + } + + if (valid_coords_count > 0) { + current.weight = 0; + } + } + } + + } + } + } #ifdef DEBUG_FILES - void debug_export(std::string file_name) { + void debug_export(std::string file_name) const { Slic3r::CNumericLocalesSetter locales_setter; { FILE *fp = boost::nowide::fopen(debug_out_path((file_name + "_matrix.obj").c_str()).c_str(), "w"); @@ -111,16 +217,30 @@ struct WeightDistributionMatrix { return; } + float max_weight = 0; for (int x = 0; x < global_cell_count.x(); ++x) { for (int y = 0; y < global_cell_count.y(); ++y) { for (int z = 0; z < local_z_cell_count; ++z) { - Vec3f center = unscale(cell_center(Vec3i(x, y, z + local_z_index_offset))).cast(); - Cell &cell = access_cell(Vec3i(x, y, z)); - fprintf(fp, "v %f %f %f %f %f %f\n", - center(0), center(1), - center(2), - cell.weight, 0.0, 0.0 - ); + const Cell &cell = access_cell(Vec3i(x, y, z)); + max_weight = std::max(max_weight, cell.weight); + } + } + } + + max_weight *= 0.8; + + for (int x = 0; x < global_cell_count.x(); ++x) { + for (int y = 0; y < global_cell_count.y(); ++y) { + for (int z = 0; z < local_z_cell_count; ++z) { + Vec3f center = unscale(get_cell_center(to_global_cell_coords(Vec3i { x, y, z }))).cast(); + const Cell &cell = access_cell(Vec3i(x, y, z)); + if (cell.weight != 0) { + fprintf(fp, "v %f %f %f %f %f %f\n", + center(0), center(1), + center(2), + cell.weight / max_weight, 0.0, 0.0 + ); + } } } } @@ -130,17 +250,17 @@ struct WeightDistributionMatrix { } #endif - static constexpr float cell_height = scale_(0.15f); + static constexpr float cell_height = scale_(0.3f); - Vec3crd cell_size; + Vec3crd cell_size { }; - Vec3crd global_origin; - Vec3crd global_size; - Vec3i global_cell_count; + Vec3crd global_origin { }; + Vec3crd global_size { }; + Vec3i global_cell_count { }; - int local_z_index_offset; - int local_z_cell_count; - std::vector cells; + int local_z_index_offset { }; + int local_z_cell_count { }; + std::vector cells { }; }; @@ -226,7 +346,7 @@ coordf_t get_flow_width(const LayerRegion *region, ExtrusionRole role) { } } -coordf_t get_max_allowed_distance(ExtrusionRole role, coord_t flow_width, bool external_perimeters_first, +coordf_t get_max_allowed_distance(ExtrusionRole role, coordf_t flow_width, bool external_perimeters_first, const Params ¶ms) { // <= distance / flow_width (can be larger for perimeter, if not external perimeter first) if ((role == ExtrusionRole::erExternalPerimeter || role == ExtrusionRole::erOverhangPerimeter) && (external_perimeters_first) @@ -260,15 +380,17 @@ struct SegmentAccumulator { }; Issues check_extrusion_entity_stability(const ExtrusionEntity *entity, - float slice_z, + float print_z, const LayerRegion *layer_region, const EdgeGridWrapper &supported_grid, + WeightDistributionMatrix &weight_matrix, const Params ¶ms) { Issues issues { }; if (entity->is_collection()) { for (const auto *e : static_cast(entity)->entities) { - issues.add(check_extrusion_entity_stability(e, slice_z, layer_region, supported_grid, params)); + issues.add( + check_extrusion_entity_stability(e, print_z, layer_region, supported_grid, weight_matrix, params)); } } else { //single extrusion path, with possible varying parameters //prepare stack of points on the extrusion path. If there are long segments, additional points might be pushed onto the stack during the algorithm. @@ -282,12 +404,13 @@ Issues check_extrusion_entity_stability(const ExtrusionEntity *entity, // -> it prevents extruding perimeter start and short loops into air. SegmentAccumulator curling_acc { }; - const auto to_vec3f = [slice_z](const Point &point) { + const auto to_vec3f = [print_z](const Point &point) { Vec2f tmp = unscale(point).cast(); - return Vec3f(tmp.x(), tmp.y(), slice_z); + return Vec3f(tmp.x(), tmp.y(), print_z); }; - Vec3f prev_fpoint = to_vec3f(points.top()); // prev point of the path. Initialize with first point. + Point prev_point = points.top(); // prev point of the path. Initialize with first point. + Vec3f prev_fpoint = to_vec3f(prev_point); coordf_t flow_width = get_flow_width(layer_region, entity->role()); bool external_perimters_first = layer_region->region().config().external_perimeters_first; const coordf_t max_allowed_dist_from_prev_layer = get_max_allowed_distance(entity->role(), flow_width, @@ -297,9 +420,11 @@ Issues check_extrusion_entity_stability(const ExtrusionEntity *entity, Point point = points.top(); points.pop(); Vec2f tmp = unscale(point).cast(); - Vec3f fpoint = Vec3f(tmp.x(), tmp.y(), slice_z); + Vec3f fpoint = Vec3f(tmp.x(), tmp.y(), print_z); float edge_len = (fpoint - prev_fpoint).norm(); + weight_matrix.ditribute_edge_weight(prev_point, point, print_z, flow_width); + coordf_t dist_from_prev_layer { 0 }; if (!supported_grid.signed_distance(point, flow_width, dist_from_prev_layer)) { // dist from prev layer not found, assume empty layer issues.supports_nedded.push_back(fpoint); @@ -344,6 +469,7 @@ Issues check_extrusion_entity_stability(const ExtrusionEntity *entity, curling_acc.reset(); } + prev_point = point; prev_fpoint = fpoint; if (!points.empty()) { //oversampling if necessary @@ -363,7 +489,8 @@ Issues check_extrusion_entity_stability(const ExtrusionEntity *entity, return issues; } -Issues check_layer_stability(const PrintObject *po, size_t layer_idx, bool full_check, const Params ¶ms) { +Issues check_layer_stability(const PrintObject *po, size_t layer_idx, bool full_check, + WeightDistributionMatrix &weight_matrix, const Params ¶ms) { std::cout << "Checking: " << layer_idx << std::endl; if (layer_idx == 0) { // first layer is usually ok @@ -379,16 +506,16 @@ Issues check_layer_stability(const PrintObject *po, size_t layer_idx, bool full_ for (const ExtrusionEntity *ex_entity : layer_region->perimeters.entities) { for (const ExtrusionEntity *perimeter : static_cast(ex_entity)->entities) { issues.add(check_extrusion_entity_stability(perimeter, - layer->slice_z, layer_region, - supported_grid, params)); + layer->print_z, layer_region, + supported_grid, weight_matrix, params)); } // perimeter } // ex_entity for (const ExtrusionEntity *ex_entity : layer_region->fills.entities) { for (const ExtrusionEntity *fill : static_cast(ex_entity)->entities) { if (fill->role() == ExtrusionRole::erGapFill || fill->role() == ExtrusionRole::erBridgeInfill) { issues.add(check_extrusion_entity_stability(fill, - layer->slice_z, layer_region, - supported_grid, params)); + layer->print_z, layer_region, + supported_grid, weight_matrix, params)); } } // fill } // ex_entity @@ -401,8 +528,8 @@ Issues check_layer_stability(const PrintObject *po, size_t layer_idx, bool full_ if (perimeter->role() == ExtrusionRole::erExternalPerimeter || perimeter->role() == ExtrusionRole::erOverhangPerimeter) { issues.add(check_extrusion_entity_stability(perimeter, - layer->slice_z, layer_region, - supported_grid, params)); + layer->print_z, layer_region, + supported_grid, weight_matrix, params)); }; // ex_perimeter } // perimeter } // ex_entity @@ -417,17 +544,28 @@ Issues check_layer_stability(const PrintObject *po, size_t layer_idx, bool full_ std::vector quick_search(const PrintObject *po, const Params ¶ms) { using namespace Impl; + WeightDistributionMatrix matrix { }; + matrix.init(po, 0, po->layers().size()); + std::mutex matrix_mutex; + size_t layer_count = po->layer_count(); std::vector layer_needs_supports(layer_count, false); tbb::parallel_for(tbb::blocked_range(1, layer_count), [&](tbb::blocked_range r) { + WeightDistributionMatrix weight_matrix { }; + weight_matrix.init(po, r.begin(), r.end()); + for (size_t layer_idx = r.begin(); layer_idx < r.end(); ++layer_idx) { auto layer_issues = check_layer_stability(po, layer_idx, - false, params); + false, weight_matrix, params); if (!layer_issues.supports_nedded.empty()) { layer_needs_supports[layer_idx] = true; } } + + matrix_mutex.lock(); + matrix.merge(weight_matrix); + matrix_mutex.unlock(); }); std::vector problematic_layers; @@ -442,19 +580,27 @@ std::vector quick_search(const PrintObject *po, const Params ¶ms) { Issues full_search(const PrintObject *po, const Params ¶ms) { using namespace Impl; - WeightDistributionMatrix matrix { po, 0, po->layers().size() }; - matrix.debug_export("matrix"); + WeightDistributionMatrix matrix { }; + matrix.init(po, 0, po->layers().size()); + std::mutex matrix_mutex; size_t layer_count = po->layer_count(); Issues found_issues = tbb::parallel_reduce(tbb::blocked_range(1, layer_count), Issues { }, [&](tbb::blocked_range r, const Issues &init) { + WeightDistributionMatrix weight_matrix { }; + weight_matrix.init(po, r.begin(), r.end()); Issues issues = init; for (size_t layer_idx = r.begin(); layer_idx < r.end(); ++layer_idx) { - auto layer_issues = check_layer_stability(po, layer_idx, true, params); + auto layer_issues = check_layer_stability(po, layer_idx, true, weight_matrix, params); if (!layer_issues.empty()) { issues.add(layer_issues); } } + + matrix_mutex.lock(); + matrix.merge(weight_matrix); + matrix_mutex.unlock(); + return issues; }, [](Issues left, const Issues &right) { @@ -463,6 +609,10 @@ Issues full_search(const PrintObject *po, const Params ¶ms) { } ); + matrix.distribute_top_down(); + + matrix.debug_export("weight"); + #ifdef DEBUG_FILES Impl::debug_export(found_issues, "issues"); #endif From d9bd1080da644dd90eef613a1d678cdf7abfe51d Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Tue, 26 Apr 2022 17:13:46 +0200 Subject: [PATCH 010/100] UNFINISHED! refactoring of algorithm to bottom up propagation of support islands Added CentroidAccumulators for balance issues checking --- src/libslic3r/PrintObject.cpp | 4 +- src/libslic3r/SupportableIssuesSearch.cpp | 225 +++++++++++++++++----- src/libslic3r/SupportableIssuesSearch.hpp | 18 +- 3 files changed, 192 insertions(+), 55 deletions(-) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index c9f64cfb3c..5dbd119124 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -429,8 +429,8 @@ void PrintObject::find_supportable_issues() Transform3d inv_transform = (obj_transform * model_transformation).inverse(); TriangleSelectorWrapper selector { model_volume->mesh() }; - for (const Vec3f &support_point : issues.supports_nedded) { - selector.enforce_spot(Vec3f(inv_transform.cast() * support_point), 0.3f); + for (const SupportableIssues::SupportPoint &support_point : issues.supports_nedded) { + selector.enforce_spot(Vec3f(inv_transform.cast() * support_point.position), 0.3f); } model_volume->supported_facets.set(selector.selector); diff --git a/src/libslic3r/SupportableIssuesSearch.cpp b/src/libslic3r/SupportableIssuesSearch.cpp index 4082f17ca9..87301278cd 100644 --- a/src/libslic3r/SupportableIssuesSearch.cpp +++ b/src/libslic3r/SupportableIssuesSearch.cpp @@ -5,10 +5,12 @@ #include "tbb/parallel_reduce.h" #include #include +#include #include #include "libslic3r/Layer.hpp" #include "libslic3r/ClipperUtils.hpp" +#include "Geometry/ConvexHull.hpp" #include "PolygonPointTest.hpp" #define DEBUG_FILES @@ -31,9 +33,68 @@ bool Issues::empty() const { return supports_nedded.empty() && curling_up.empty(); } +SupportPoint::SupportPoint(const Vec3f &position, float weight) : + position(position), weight(weight) { +} + +SupportPoint::SupportPoint(const Vec3f &position) : + position(position), weight(0.0f) { +} + +CurledFilament::CurledFilament(const Vec3f &position, float estimated_height) : + position(position), estimated_height(estimated_height) { +} + +CurledFilament::CurledFilament(const Vec3f &position) : + position(position), estimated_height(0.0f) { +} + struct Cell { float weight; - int island_id; + float curled_height; + int island_id = std::numeric_limits::max(); +}; + +struct CentroidAccumulator { + //TODO add base height + + Polygon convex_hull; + Points points; + Vec3d accumulated_value; + float accumulated_weight; + + void calculate_base_hull() { + convex_hull = Geometry::convex_hull(points); + } +}; + +struct CentroidAccumulators { + std::unordered_map mapping; + std::vector acccumulators; + + explicit CentroidAccumulators(int count) { + acccumulators.resize(count); + for (int index = 0; index < count; ++index) { + mapping[index - 1] = index; + } + } + + CentroidAccumulator& access(int id) { + return acccumulators[mapping[id]]; + } + + void merge_to(int from_id, int to_id) { + if (from_id == to_id) { + return; + } + CentroidAccumulator &from_acc = this->access(from_id); + CentroidAccumulator &to_acc = this->access(to_id); + to_acc.accumulated_value += from_acc.accumulated_value; + to_acc.accumulated_weight += from_acc.accumulated_weight; + to_acc.points.insert(to_acc.points.end(), from_acc.points.begin(), from_acc.points.end()); + to_acc.calculate_base_hull(); + mapping[from_id] = mapping[to_id]; + } }; struct WeightDistributionMatrix { @@ -75,11 +136,17 @@ struct WeightDistributionMatrix { } Vec3i to_global_cell_coords(const Point &p, float print_z) const { - Vec3i position = Vec3crd { p.x(), p.y(), int(scale_(print_z)) }; + Vec3crd position = Vec3crd { p.x(), p.y(), int(scale_(print_z)) }; Vec3i cell_coords = (position - this->global_origin).cwiseQuotient(this->cell_size); return cell_coords; } + Vec3i to_global_cell_coords(const Vec3f &position) const { + Vec3crd scaled_position = scaled(position); + Vec3i cell_coords = (scaled_position - this->global_origin).cwiseQuotient(this->cell_size); + return cell_coords; + } + Vec3i to_local_cell_coords(const Point &p, float print_z) const { Vec3i cell_coords = this->to_global_cell_coords(p, print_z); return this->to_local_cell_coords(cell_coords); @@ -107,6 +174,10 @@ struct WeightDistributionMatrix { return cells[this->to_cell_index(to_local_cell_coords(p, print_z))]; } + Cell& access_cell(const Vec3f &unscaled_position) { + return cells[this->to_cell_index(this->to_local_cell_coords(this->to_global_cell_coords(unscaled_position)))]; + } + Cell& access_cell(const Vec3i &local_cell_coords) { return cells[this->to_cell_index(local_cell_coords)]; } @@ -115,7 +186,7 @@ struct WeightDistributionMatrix { return cells[this->to_cell_index(local_cell_coords)]; } - void ditribute_edge_weight(const Point &p1, const Point &p2, float print_z, coordf_t width) { + void distribute_edge_weight(const Point &p1, const Point &p2, float print_z, float unscaled_width) { Vec2d dir = (p2 - p1).cast(); double length = dir.norm(); if (length < 0.01) { @@ -130,17 +201,7 @@ struct WeightDistributionMatrix { double current_dist_payload = next_len - distributed_length; Point location = p1 + ((next_len / length) * dir).cast(); - double payload = current_dist_payload * width; - - Vec3i local_index = this->to_local_cell_coords(location, print_z); - - if (this->to_cell_index(local_index) >= this->cells.size() || this->to_cell_index(local_index) < 0) { - std::cout << "loc: " << local_index.x() << " " << local_index.y() << " " << local_index.z() - << " globals: " << this->global_cell_count.x() << " " - << this->global_cell_count.y() << " " << this->local_z_cell_count << - "+" << this->local_z_cell_count << std::endl; - return; - } + float payload = unscale(current_dist_payload) * unscaled_width; this->access_cell(location, print_z).weight += payload; distributed_length = next_len; @@ -164,42 +225,97 @@ struct WeightDistributionMatrix { } } - void distribute_top_down() { + void analyze(Issues &issues) { + CentroidAccumulators accumulators(issues.supports_nedded.size() + 1); + + + //TODO split base support points by connectivity!! + for (int x = 0; x < global_cell_count.x(); ++x) { + for (int y = 0; y < global_cell_count.y(); ++y) { + Cell &cell = this->access_cell(Vec3i(x, y, 0)); + if (cell.weight > 0) { + cell.island_id = -1; + Vec3crd cell_center = this->get_cell_center(Vec3i(x, y, local_z_index_offset)); + CentroidAccumulator &acc = accumulators.access(-1); + acc.points.push_back(Point(cell_center.head<2>())); + acc.accumulated_value += cell_center.cast() * cell.weight; + acc.accumulated_weight += cell.weight; + } + } + } + + std::sort(issues.supports_nedded.begin(), issues.supports_nedded.end(), + [](const SupportPoint &left, const SupportPoint &right) { + return left.position.z() < right.position.z(); + }); + for (int index = 0; index < int(issues.supports_nedded.size()); ++index) { + Vec3i local_coords = this->to_local_cell_coords( + this->to_global_cell_coords(issues.supports_nedded[index].position)); + this->access_cell(local_coords).island_id = index; + accumulators.access(index).points.push_back( + Point(scaled(Vec2f(issues.supports_nedded[index].position.head<2>())))); + } + + for (const CurledFilament &curling : issues.curling_up) { + this->access_cell(curling.position).curled_height += curling.estimated_height; + } + const auto validate_xy_coords = [&](const Vec2i &local_coords) { return local_coords.x() >= 0 && local_coords.y() >= 0 && local_coords.x() < this->global_cell_count.x() && local_coords.y() < this->global_cell_count.y(); }; - Vec2i valid_coords[9]; + std::unordered_set modified_acc_ids; + modified_acc_ids.reserve(issues.supports_nedded.size() + 1); + for (int z = 1; z < local_z_cell_count; ++z) { + modified_acc_ids.clear(); + + for (int x = 0; x < global_cell_count.x(); ++x) { + for (int y = 0; y < global_cell_count.y(); ++y) { - for (int x = 0; x < global_cell_count.x(); ++x) { - for (int y = 0; y < global_cell_count.y(); ++y) { - for (int z = local_z_cell_count - 1; z > local_z_index_offset; --z) { Cell ¤t = this->access_cell(Vec3i(x, y, z)); - size_t valid_coords_count = 0; - if (current.weight > 0) { + //first determine island id + if (current.island_id == std::numeric_limits::max()) { for (int y_offset = -1; y_offset <= 1; ++y_offset) { for (int x_offset = -1; x_offset <= 1; ++x_offset) { Vec2i xy_coords { x + x_offset, y + y_offset }; - if (validate_xy_coords(xy_coords) - && - this->access_cell(Vec3i(xy_coords.x(), xy_coords.y(), z - 1)).weight != 0) { - valid_coords[valid_coords_count] = xy_coords; - valid_coords_count++; + if (validate_xy_coords(xy_coords)) { + Cell &under = this->access_cell(Vec3i(x, y, z - 1)); + int island_id = std::min(under.island_id, current.island_id); + int merging_id = std::max(under.island_id, current.island_id); + if (merging_id != std::numeric_limits::max() + && island_id != merging_id) { + accumulators.merge_to(merging_id, island_id); + } + if (island_id != std::numeric_limits::max()) { + current.island_id = island_id; + modified_acc_ids.insert(current.island_id); + } + + current.curled_height += under.curled_height + / (2 + std::abs(x_offset) + std::abs(y_offset)); } } } - - float distribution = current.weight / valid_coords_count; - for (size_t index = 0; index < valid_coords_count; ++index) { - this->access_cell(Vec3i(valid_coords[index].x(), valid_coords[index].y(), z - 1)).weight += - distribution; - } - - if (valid_coords_count > 0) { - current.weight = 0; - } } + + //Propagate to accumulators. TODO what to do if no supporter is found? + if (current.island_id != std::numeric_limits::max()) { + CentroidAccumulator &acc = accumulators.access(current.island_id); + acc.accumulated_value += current.weight * this->get_cell_center( + this->to_global_cell_coords(Vec3i(x, y, z))).cast(); + acc.accumulated_weight += current.weight; + } + } + } + + // check stability of modified centroid accumulators + for (int acc_index : modified_acc_ids) { + CentroidAccumulator &acc = accumulators.access(acc_index); + Vec3d centroid = acc.accumulated_value / acc.accumulated_weight; + + if (!acc.convex_hull.contains(Point(centroid.head<2>().cast()))) { + //TODO :] } } @@ -232,7 +348,8 @@ struct WeightDistributionMatrix { for (int x = 0; x < global_cell_count.x(); ++x) { for (int y = 0; y < global_cell_count.y(); ++y) { for (int z = 0; z < local_z_cell_count; ++z) { - Vec3f center = unscale(get_cell_center(to_global_cell_coords(Vec3i { x, y, z }))).cast(); + Vec3f center = unscale(get_cell_center(to_global_cell_coords(Vec3i { x, y, z }))).cast< + float>(); const Cell &cell = access_cell(Vec3i(x, y, z)); if (cell.weight != 0) { fprintf(fp, "v %f %f %f %f %f %f\n", @@ -262,7 +379,8 @@ struct WeightDistributionMatrix { int local_z_cell_count { }; std::vector cells { }; -}; +} +; namespace Impl { @@ -280,7 +398,8 @@ void debug_export(Issues issues, std::string file_name) { for (size_t i = 0; i < issues.supports_nedded.size(); ++i) { fprintf(fp, "v %f %f %f %f %f %f\n", - issues.supports_nedded[i](0), issues.supports_nedded[i](1), issues.supports_nedded[i](2), + issues.supports_nedded[i].position(0), issues.supports_nedded[i].position(1), + issues.supports_nedded[i].position(2), 1.0, 0.0, 0.0 ); } @@ -297,7 +416,8 @@ void debug_export(Issues issues, std::string file_name) { for (size_t i = 0; i < issues.curling_up.size(); ++i) { fprintf(fp, "v %f %f %f %f %f %f\n", - issues.curling_up[i](0), issues.curling_up[i](1), issues.curling_up[i](2), + issues.curling_up[i].position(0), issues.curling_up[i].position(1), + issues.curling_up[i].position(2), 0.0, 1.0, 0.0 ); } @@ -310,7 +430,8 @@ void debug_export(Issues issues, std::string file_name) { EdgeGridWrapper compute_layer_edge_grid(const Layer *layer) { float min_region_flow_width { 1.0f }; for (const auto *region : layer->regions()) { - min_region_flow_width = std::min(min_region_flow_width, region->flow(FlowRole::frExternalPerimeter).width()); + min_region_flow_width = std::min(min_region_flow_width, + region->flow(FlowRole::frExternalPerimeter).width()); } std::vector lines; for (const LayerRegion *layer_region : layer->regions()) { @@ -390,7 +511,8 @@ Issues check_extrusion_entity_stability(const ExtrusionEntity *entity, if (entity->is_collection()) { for (const auto *e : static_cast(entity)->entities) { issues.add( - check_extrusion_entity_stability(e, print_z, layer_region, supported_grid, weight_matrix, params)); + check_extrusion_entity_stability(e, print_z, layer_region, supported_grid, weight_matrix, + params)); } } else { //single extrusion path, with possible varying parameters //prepare stack of points on the extrusion path. If there are long segments, additional points might be pushed onto the stack during the algorithm. @@ -419,15 +541,14 @@ Issues check_extrusion_entity_stability(const ExtrusionEntity *entity, while (!points.empty()) { Point point = points.top(); points.pop(); - Vec2f tmp = unscale(point).cast(); - Vec3f fpoint = Vec3f(tmp.x(), tmp.y(), print_z); + Vec3f fpoint = to_vec3f(point); float edge_len = (fpoint - prev_fpoint).norm(); - weight_matrix.ditribute_edge_weight(prev_point, point, print_z, flow_width); + weight_matrix.distribute_edge_weight(prev_point, point, print_z, unscale(flow_width)); coordf_t dist_from_prev_layer { 0 }; if (!supported_grid.signed_distance(point, flow_width, dist_from_prev_layer)) { // dist from prev layer not found, assume empty layer - issues.supports_nedded.push_back(fpoint); + issues.supports_nedded.push_back(SupportPoint(fpoint)); supports_acc.reset(); } @@ -451,7 +572,7 @@ Issues check_extrusion_entity_stability(const ExtrusionEntity *entity, / (1.0f + (supports_acc.max_curvature * params.bridge_distance_decrease_by_curvature_factor / PI))) { - issues.supports_nedded.push_back(fpoint); + issues.supports_nedded.push_back(SupportPoint(fpoint)); supports_acc.reset(); } } else { @@ -462,7 +583,7 @@ Issues check_extrusion_entity_stability(const ExtrusionEntity *entity, if (dist_from_prev_layer > 0.0f) { //extrusion point is unsupported or poorly supported curling_acc.add_distance(edge_len); if (curling_acc.max_curvature / (PI * curling_acc.distance) > params.limit_curvature) { - issues.curling_up.push_back(fpoint); + issues.curling_up.push_back(CurledFilament(fpoint, layer_region->layer()->height)); curling_acc.reset(); } } else { @@ -512,7 +633,8 @@ Issues check_layer_stability(const PrintObject *po, size_t layer_idx, bool full_ } // ex_entity for (const ExtrusionEntity *ex_entity : layer_region->fills.entities) { for (const ExtrusionEntity *fill : static_cast(ex_entity)->entities) { - if (fill->role() == ExtrusionRole::erGapFill || fill->role() == ExtrusionRole::erBridgeInfill) { + if (fill->role() == ExtrusionRole::erGapFill + || fill->role() == ExtrusionRole::erBridgeInfill) { issues.add(check_extrusion_entity_stability(fill, layer->print_z, layer_region, supported_grid, weight_matrix, params)); @@ -577,7 +699,8 @@ std::vector quick_search(const PrintObject *po, const Params ¶ms) { return problematic_layers; } -Issues full_search(const PrintObject *po, const Params ¶ms) { +Issues +full_search(const PrintObject *po, const Params ¶ms) { using namespace Impl; WeightDistributionMatrix matrix { }; @@ -609,7 +732,7 @@ Issues full_search(const PrintObject *po, const Params ¶ms) { } ); - matrix.distribute_top_down(); + matrix.analyze(found_issues); matrix.debug_export("weight"); diff --git a/src/libslic3r/SupportableIssuesSearch.hpp b/src/libslic3r/SupportableIssuesSearch.hpp index 08b09d143f..fef4eecd5e 100644 --- a/src/libslic3r/SupportableIssuesSearch.hpp +++ b/src/libslic3r/SupportableIssuesSearch.hpp @@ -16,9 +16,23 @@ struct Params { float bridge_distance_decrease_by_curvature_factor = 5.0f; // allowed bridge distance = bridge_distance / ( 1 + this factor * (curvature / PI) ) }; +struct SupportPoint { + SupportPoint(const Vec3f &position, float weight); + explicit SupportPoint(const Vec3f &position); + Vec3f position; + float weight; +}; + +struct CurledFilament { + CurledFilament(const Vec3f &position, float estimated_height); + explicit CurledFilament(const Vec3f &position); + Vec3f position; + float estimated_height; +}; + struct Issues { - std::vector supports_nedded; - std::vector curling_up; + std::vector supports_nedded; + std::vector curling_up; void add(const Issues &layer_issues); bool empty() const; From 148b24bd93081bd34f1660ff04704d445066d87c Mon Sep 17 00:00:00 2001 From: Godrak Date: Wed, 27 Apr 2022 14:15:21 +0200 Subject: [PATCH 011/100] accumulators given base height; object base split to separate islands by connectivity --- src/libslic3r/SupportableIssuesSearch.cpp | 264 +++++++++++----------- 1 file changed, 129 insertions(+), 135 deletions(-) diff --git a/src/libslic3r/SupportableIssuesSearch.cpp b/src/libslic3r/SupportableIssuesSearch.cpp index 87301278cd..7309c462f7 100644 --- a/src/libslic3r/SupportableIssuesSearch.cpp +++ b/src/libslic3r/SupportableIssuesSearch.cpp @@ -23,10 +23,8 @@ namespace Slic3r { namespace SupportableIssues { void Issues::add(const Issues &layer_issues) { - supports_nedded.insert(supports_nedded.end(), - layer_issues.supports_nedded.begin(), layer_issues.supports_nedded.end()); - curling_up.insert(curling_up.end(), layer_issues.curling_up.begin(), - layer_issues.curling_up.end()); + supports_nedded.insert(supports_nedded.end(), layer_issues.supports_nedded.begin(), layer_issues.supports_nedded.end()); + curling_up.insert(curling_up.end(), layer_issues.curling_up.begin(), layer_issues.curling_up.end()); } bool Issues::empty() const { @@ -56,12 +54,15 @@ struct Cell { }; struct CentroidAccumulator { - //TODO add base height + Polygon convex_hull { }; + Points points { }; + Vec3d accumulated_value { }; + float accumulated_weight { }; + const double base_height { }; - Polygon convex_hull; - Points points; - Vec3d accumulated_value; - float accumulated_weight; + explicit CentroidAccumulator(double base_height) : + base_height(base_height) { + } void calculate_base_hull() { convex_hull = Geometry::convex_hull(points); @@ -72,11 +73,14 @@ struct CentroidAccumulators { std::unordered_map mapping; std::vector acccumulators; - explicit CentroidAccumulators(int count) { - acccumulators.resize(count); - for (int index = 0; index < count; ++index) { - mapping[index - 1] = index; - } + explicit CentroidAccumulators(size_t reserve_count) { + acccumulators.reserve(reserve_count); + } + + CentroidAccumulator& create_accumulator(int id, double base_height) { + mapping[id] = acccumulators.size(); + acccumulators.push_back(CentroidAccumulator { base_height }); + return this->access(id); } CentroidAccumulator& access(int id) { @@ -160,14 +164,12 @@ struct WeightDistributionMatrix { assert(local_cell_coords.z() >= 0); assert(local_cell_coords.z() < local_z_cell_count); - return local_cell_coords.z() * global_cell_count.x() * global_cell_count.y() - + local_cell_coords.y() * global_cell_count.x() + - local_cell_coords.x(); + return local_cell_coords.z() * global_cell_count.x() * global_cell_count.y() + local_cell_coords.y() * global_cell_count.x() + + local_cell_coords.x(); } Vec3crd get_cell_center(const Vec3i &global_cell_coords) const { - return global_origin + global_cell_coords.cwiseProduct(this->cell_size) - + this->cell_size.cwiseQuotient(Vec3crd(2, 2, 2)); + return global_origin + global_cell_coords.cwiseProduct(this->cell_size) + this->cell_size.cwiseQuotient(Vec3crd(2, 2, 2)); } Cell& access_cell(const Point &p, float print_z) { @@ -210,8 +212,7 @@ struct WeightDistributionMatrix { void merge(const WeightDistributionMatrix &other) { int z_start = std::max(local_z_index_offset, other.local_z_index_offset); - int z_end = std::min(local_z_index_offset + local_z_cell_count, - other.local_z_index_offset + other.local_z_cell_count); + int z_end = std::min(local_z_index_offset + local_z_cell_count, other.local_z_index_offset + other.local_z_cell_count); for (int x = 0; x < global_cell_count.x(); ++x) { for (int y = 0; y < global_cell_count.y(); ++y) { @@ -226,34 +227,50 @@ struct WeightDistributionMatrix { } void analyze(Issues &issues) { - CentroidAccumulators accumulators(issues.supports_nedded.size() + 1); + CentroidAccumulators accumulators(issues.supports_nedded.size() + 4); - - //TODO split base support points by connectivity!! - for (int x = 0; x < global_cell_count.x(); ++x) { - for (int y = 0; y < global_cell_count.y(); ++y) { + int next_island_id = -1; + for (int y = 0; y < global_cell_count.y(); ++y) { + for (int x = 0; x < global_cell_count.x(); ++x) { Cell &cell = this->access_cell(Vec3i(x, y, 0)); - if (cell.weight > 0) { - cell.island_id = -1; - Vec3crd cell_center = this->get_cell_center(Vec3i(x, y, local_z_index_offset)); - CentroidAccumulator &acc = accumulators.access(-1); - acc.points.push_back(Point(cell_center.head<2>())); - acc.accumulated_value += cell_center.cast() * cell.weight; - acc.accumulated_weight += cell.weight; + if (cell.weight > 0 && cell.island_id == std::numeric_limits::max()) { + CentroidAccumulator &acc = accumulators.create_accumulator(next_island_id, 0); + std::set coords_to_check { Vec2i(x, y) }; + while (!coords_to_check.empty()) { + Vec2i current_coords = *coords_to_check.begin(); + coords_to_check.erase(coords_to_check.begin()); + cell = this->access_cell(Vec3i(current_coords.x(), current_coords.y(), 0)); + if (cell.weight > 0 && cell.island_id == std::numeric_limits::max()) { + cell.island_id = next_island_id; + Vec3crd cell_center = this->get_cell_center( + Vec3i(current_coords.x(), current_coords.y(), local_z_index_offset)); + acc.points.push_back(Point(cell_center.head<2>())); + acc.accumulated_value += cell_center.cast() * cell.weight; + acc.accumulated_weight += cell.weight; + for (int y_offset = -1; y_offset <= 1; ++y_offset) { + for (int x_offset = -1; x_offset <= 1; ++x_offset) { + if (y_offset != 0 || x_offset != 0) { + coords_to_check.insert(Vec2i(current_coords.x() + x_offset, current_coords.y() + y_offset)); + } + } + } + } + } + next_island_id--; + acc.calculate_base_hull(); } } } - std::sort(issues.supports_nedded.begin(), issues.supports_nedded.end(), - [](const SupportPoint &left, const SupportPoint &right) { - return left.position.z() < right.position.z(); - }); + std::sort(issues.supports_nedded.begin(), issues.supports_nedded.end(), [](const SupportPoint &left, const SupportPoint &right) { + return left.position.z() < right.position.z(); + }); for (int index = 0; index < int(issues.supports_nedded.size()); ++index) { - Vec3i local_coords = this->to_local_cell_coords( - this->to_global_cell_coords(issues.supports_nedded[index].position)); + Vec3i local_coords = this->to_local_cell_coords(this->to_global_cell_coords(issues.supports_nedded[index].position)); this->access_cell(local_coords).island_id = index; - accumulators.access(index).points.push_back( - Point(scaled(Vec2f(issues.supports_nedded[index].position.head<2>())))); + CentroidAccumulator &acc = accumulators.create_accumulator(index, issues.supports_nedded[index].position.z()); + acc.points.push_back(Point(scaled(Vec2f(issues.supports_nedded[index].position.head<2>())))); + acc.calculate_base_hull(); } for (const CurledFilament &curling : issues.curling_up) { @@ -261,8 +278,8 @@ struct WeightDistributionMatrix { } const auto validate_xy_coords = [&](const Vec2i &local_coords) { - return local_coords.x() >= 0 && local_coords.y() >= 0 && - local_coords.x() < this->global_cell_count.x() && local_coords.y() < this->global_cell_count.y(); + return local_coords.x() >= 0 && local_coords.y() >= 0 && local_coords.x() < this->global_cell_count.x() + && local_coords.y() < this->global_cell_count.y(); }; std::unordered_set modified_acc_ids; @@ -283,8 +300,7 @@ struct WeightDistributionMatrix { Cell &under = this->access_cell(Vec3i(x, y, z - 1)); int island_id = std::min(under.island_id, current.island_id); int merging_id = std::max(under.island_id, current.island_id); - if (merging_id != std::numeric_limits::max() - && island_id != merging_id) { + if (merging_id != std::numeric_limits::max() && island_id != merging_id) { accumulators.merge_to(merging_id, island_id); } if (island_id != std::numeric_limits::max()) { @@ -292,8 +308,7 @@ struct WeightDistributionMatrix { modified_acc_ids.insert(current.island_id); } - current.curled_height += under.curled_height - / (2 + std::abs(x_offset) + std::abs(y_offset)); + current.curled_height += under.curled_height / (2 + std::abs(x_offset) + std::abs(y_offset)); } } } @@ -302,22 +317,28 @@ struct WeightDistributionMatrix { //Propagate to accumulators. TODO what to do if no supporter is found? if (current.island_id != std::numeric_limits::max()) { CentroidAccumulator &acc = accumulators.access(current.island_id); - acc.accumulated_value += current.weight * this->get_cell_center( - this->to_global_cell_coords(Vec3i(x, y, z))).cast(); + acc.accumulated_value += current.weight + * this->get_cell_center(this->to_global_cell_coords(Vec3i(x, y, z))).cast(); acc.accumulated_weight += current.weight; } } } - // check stability of modified centroid accumulators + // check stability of modified centroid accumulators. + // Stability is the amount of work needed to push the object from stable position into unstable. + // This amount of work is proportional to the increase of height of the centroid during toppling. + // image here: https://hgphysics.com/gph/c-forces/2-force-effects/1-moment/stability/ + // better image in Czech here in the first question: https://www.priklady.eu/cs/fyzika/mechanika-tuheho-telesa/stabilita-teles.alej for (int acc_index : modified_acc_ids) { CentroidAccumulator &acc = accumulators.access(acc_index); Vec3d centroid = acc.accumulated_value / acc.accumulated_weight; - - if (!acc.convex_hull.contains(Point(centroid.head<2>().cast()))) { - //TODO :] + //determine signed shortest distance to the convex hull + Point centroid_base_projection = Point(centroid.head<2>().cast()); + double distance_sq = std::numeric_limits::max(); + bool inside = true; + for (Line line : acc.convex_hull.lines()) { + distance_sq = std::min(line.distance_to_squared(centroid_base_projection), distance_sq); } - } } } @@ -348,15 +369,10 @@ struct WeightDistributionMatrix { for (int x = 0; x < global_cell_count.x(); ++x) { for (int y = 0; y < global_cell_count.y(); ++y) { for (int z = 0; z < local_z_cell_count; ++z) { - Vec3f center = unscale(get_cell_center(to_global_cell_coords(Vec3i { x, y, z }))).cast< - float>(); + Vec3f center = unscale(get_cell_center(to_global_cell_coords(Vec3i { x, y, z }))).cast(); const Cell &cell = access_cell(Vec3i(x, y, z)); if (cell.weight != 0) { - fprintf(fp, "v %f %f %f %f %f %f\n", - center(0), center(1), - center(2), - cell.weight / max_weight, 0.0, 0.0 - ); + fprintf(fp, "v %f %f %f %f %f %f\n", center(0), center(1), center(2), cell.weight / max_weight, 0.0, 0.0); } } } @@ -397,11 +413,8 @@ void debug_export(Issues issues, std::string file_name) { } for (size_t i = 0; i < issues.supports_nedded.size(); ++i) { - fprintf(fp, "v %f %f %f %f %f %f\n", - issues.supports_nedded[i].position(0), issues.supports_nedded[i].position(1), - issues.supports_nedded[i].position(2), - 1.0, 0.0, 0.0 - ); + fprintf(fp, "v %f %f %f %f %f %f\n", issues.supports_nedded[i].position(0), issues.supports_nedded[i].position(1), + issues.supports_nedded[i].position(2), 1.0, 0.0, 0.0); } fclose(fp); @@ -415,11 +428,8 @@ void debug_export(Issues issues, std::string file_name) { } for (size_t i = 0; i < issues.curling_up.size(); ++i) { - fprintf(fp, "v %f %f %f %f %f %f\n", - issues.curling_up[i].position(0), issues.curling_up[i].position(1), - issues.curling_up[i].position(2), - 0.0, 1.0, 0.0 - ); + fprintf(fp, "v %f %f %f %f %f %f\n", issues.curling_up[i].position(0), issues.curling_up[i].position(1), + issues.curling_up[i].position(2), 0.0, 1.0, 0.0); } fclose(fp); } @@ -430,8 +440,7 @@ void debug_export(Issues issues, std::string file_name) { EdgeGridWrapper compute_layer_edge_grid(const Layer *layer) { float min_region_flow_width { 1.0f }; for (const auto *region : layer->regions()) { - min_region_flow_width = std::min(min_region_flow_width, - region->flow(FlowRole::frExternalPerimeter).width()); + min_region_flow_width = std::min(min_region_flow_width, region->flow(FlowRole::frExternalPerimeter).width()); } std::vector lines; for (const LayerRegion *layer_region : layer->regions()) { @@ -452,26 +461,23 @@ EdgeGridWrapper compute_layer_edge_grid(const Layer *layer) { //TODO needs revision coordf_t get_flow_width(const LayerRegion *region, ExtrusionRole role) { switch (role) { - case ExtrusionRole::erBridgeInfill: - return region->flow(FlowRole::frExternalPerimeter).scaled_width(); - case ExtrusionRole::erExternalPerimeter: - return region->flow(FlowRole::frExternalPerimeter).scaled_width(); - case ExtrusionRole::erGapFill: - return region->flow(FlowRole::frInfill).scaled_width(); - case ExtrusionRole::erPerimeter: - return region->flow(FlowRole::frPerimeter).scaled_width(); - case ExtrusionRole::erSolidInfill: - return region->flow(FlowRole::frSolidInfill).scaled_width(); - default: - return region->flow(FlowRole::frPerimeter).scaled_width(); + case ExtrusionRole::erBridgeInfill: + return region->flow(FlowRole::frExternalPerimeter).scaled_width(); + case ExtrusionRole::erExternalPerimeter: + return region->flow(FlowRole::frExternalPerimeter).scaled_width(); + case ExtrusionRole::erGapFill: + return region->flow(FlowRole::frInfill).scaled_width(); + case ExtrusionRole::erPerimeter: + return region->flow(FlowRole::frPerimeter).scaled_width(); + case ExtrusionRole::erSolidInfill: + return region->flow(FlowRole::frSolidInfill).scaled_width(); + default: + return region->flow(FlowRole::frPerimeter).scaled_width(); } } -coordf_t get_max_allowed_distance(ExtrusionRole role, coordf_t flow_width, bool external_perimeters_first, - const Params ¶ms) { // <= distance / flow_width (can be larger for perimeter, if not external perimeter first) - if ((role == ExtrusionRole::erExternalPerimeter || role == ExtrusionRole::erOverhangPerimeter) - && (external_perimeters_first) - ) { +coordf_t get_max_allowed_distance(ExtrusionRole role, coordf_t flow_width, bool external_perimeters_first, const Params ¶ms) { // <= distance / flow_width (can be larger for perimeter, if not external perimeter first) + if ((role == ExtrusionRole::erExternalPerimeter || role == ExtrusionRole::erOverhangPerimeter) && (external_perimeters_first)) { return params.max_first_ex_perim_unsupported_distance_factor * flow_width; } else { return params.max_unsupported_distance_factor * flow_width; @@ -500,19 +506,13 @@ struct SegmentAccumulator { }; -Issues check_extrusion_entity_stability(const ExtrusionEntity *entity, - float print_z, - const LayerRegion *layer_region, - const EdgeGridWrapper &supported_grid, - WeightDistributionMatrix &weight_matrix, - const Params ¶ms) { +Issues check_extrusion_entity_stability(const ExtrusionEntity *entity, float print_z, const LayerRegion *layer_region, + const EdgeGridWrapper &supported_grid, WeightDistributionMatrix &weight_matrix, const Params ¶ms) { Issues issues { }; if (entity->is_collection()) { for (const auto *e : static_cast(entity)->entities) { - issues.add( - check_extrusion_entity_stability(e, print_z, layer_region, supported_grid, weight_matrix, - params)); + issues.add(check_extrusion_entity_stability(e, print_z, layer_region, supported_grid, weight_matrix, params)); } } else { //single extrusion path, with possible varying parameters //prepare stack of points on the extrusion path. If there are long segments, additional points might be pushed onto the stack during the algorithm. @@ -535,8 +535,8 @@ Issues check_extrusion_entity_stability(const ExtrusionEntity *entity, Vec3f prev_fpoint = to_vec3f(prev_point); coordf_t flow_width = get_flow_width(layer_region, entity->role()); bool external_perimters_first = layer_region->region().config().external_perimeters_first; - const coordf_t max_allowed_dist_from_prev_layer = get_max_allowed_distance(entity->role(), flow_width, - external_perimters_first, params); + const coordf_t max_allowed_dist_from_prev_layer = get_max_allowed_distance(entity->role(), flow_width, external_perimters_first, + params); while (!points.empty()) { Point point = points.top(); @@ -568,10 +568,8 @@ Issues check_extrusion_entity_stability(const ExtrusionEntity *entity, supports_acc.add_distance(edge_len); // for algorithm simplicity, expect that the whole line between prev and current point is unsupported if (supports_acc.distance // if unsupported distance is larger than bridge distance linearly decreased by curvature, enforce supports. - > params.bridge_distance - / (1.0f - + (supports_acc.max_curvature - * params.bridge_distance_decrease_by_curvature_factor / PI))) { + > params.bridge_distance + / (1.0f + (supports_acc.max_curvature * params.bridge_distance_decrease_by_curvature_factor / PI))) { issues.supports_nedded.push_back(SupportPoint(fpoint)); supports_acc.reset(); } @@ -610,8 +608,8 @@ Issues check_extrusion_entity_stability(const ExtrusionEntity *entity, return issues; } -Issues check_layer_stability(const PrintObject *po, size_t layer_idx, bool full_check, - WeightDistributionMatrix &weight_matrix, const Params ¶ms) { +Issues check_layer_stability(const PrintObject *po, size_t layer_idx, bool full_check, WeightDistributionMatrix &weight_matrix, + const Params ¶ms) { std::cout << "Checking: " << layer_idx << std::endl; if (layer_idx == 0) { // first layer is usually ok @@ -626,18 +624,17 @@ Issues check_layer_stability(const PrintObject *po, size_t layer_idx, bool full_ for (const LayerRegion *layer_region : layer->regions()) { for (const ExtrusionEntity *ex_entity : layer_region->perimeters.entities) { for (const ExtrusionEntity *perimeter : static_cast(ex_entity)->entities) { - issues.add(check_extrusion_entity_stability(perimeter, - layer->print_z, layer_region, - supported_grid, weight_matrix, params)); + issues.add( + check_extrusion_entity_stability(perimeter, layer->print_z, layer_region, supported_grid, weight_matrix, + params)); } // perimeter } // ex_entity for (const ExtrusionEntity *ex_entity : layer_region->fills.entities) { for (const ExtrusionEntity *fill : static_cast(ex_entity)->entities) { - if (fill->role() == ExtrusionRole::erGapFill - || fill->role() == ExtrusionRole::erBridgeInfill) { - issues.add(check_extrusion_entity_stability(fill, - layer->print_z, layer_region, - supported_grid, weight_matrix, params)); + if (fill->role() == ExtrusionRole::erGapFill || fill->role() == ExtrusionRole::erBridgeInfill) { + issues.add( + check_extrusion_entity_stability(fill, layer->print_z, layer_region, supported_grid, weight_matrix, + params)); } } // fill } // ex_entity @@ -649,9 +646,9 @@ Issues check_layer_stability(const PrintObject *po, size_t layer_idx, bool full_ for (const ExtrusionEntity *perimeter : static_cast(ex_entity)->entities) { if (perimeter->role() == ExtrusionRole::erExternalPerimeter || perimeter->role() == ExtrusionRole::erOverhangPerimeter) { - issues.add(check_extrusion_entity_stability(perimeter, - layer->print_z, layer_region, - supported_grid, weight_matrix, params)); + issues.add( + check_extrusion_entity_stability(perimeter, layer->print_z, layer_region, supported_grid, weight_matrix, + params)); }; // ex_perimeter } // perimeter } // ex_entity @@ -672,23 +669,21 @@ std::vector quick_search(const PrintObject *po, const Params ¶ms) { size_t layer_count = po->layer_count(); std::vector layer_needs_supports(layer_count, false); - tbb::parallel_for(tbb::blocked_range(1, layer_count), - [&](tbb::blocked_range r) { - WeightDistributionMatrix weight_matrix { }; - weight_matrix.init(po, r.begin(), r.end()); + tbb::parallel_for(tbb::blocked_range(1, layer_count), [&](tbb::blocked_range r) { + WeightDistributionMatrix weight_matrix { }; + weight_matrix.init(po, r.begin(), r.end()); - for (size_t layer_idx = r.begin(); layer_idx < r.end(); ++layer_idx) { - auto layer_issues = check_layer_stability(po, layer_idx, - false, weight_matrix, params); - if (!layer_issues.supports_nedded.empty()) { - layer_needs_supports[layer_idx] = true; - } - } + for (size_t layer_idx = r.begin(); layer_idx < r.end(); ++layer_idx) { + auto layer_issues = check_layer_stability(po, layer_idx, false, weight_matrix, params); + if (!layer_issues.supports_nedded.empty()) { + layer_needs_supports[layer_idx] = true; + } + } - matrix_mutex.lock(); - matrix.merge(weight_matrix); - matrix_mutex.unlock(); - }); + matrix_mutex.lock(); + matrix.merge(weight_matrix); + matrix_mutex.unlock(); + }); std::vector problematic_layers; for (size_t index = 0; index < layer_needs_supports.size(); ++index) { @@ -699,8 +694,7 @@ std::vector quick_search(const PrintObject *po, const Params ¶ms) { return problematic_layers; } -Issues -full_search(const PrintObject *po, const Params ¶ms) { +Issues full_search(const PrintObject *po, const Params ¶ms) { using namespace Impl; WeightDistributionMatrix matrix { }; From 5cc9bd380b3c625eaacaa485a9d7d3cbcb79cb7e Mon Sep 17 00:00:00 2001 From: Godrak Date: Wed, 27 Apr 2022 15:18:46 +0200 Subject: [PATCH 012/100] Compilation fixes after rebase --- src/libslic3r/PrintObject.cpp | 128 +++++++++++----------- src/libslic3r/SupportableIssuesSearch.cpp | 11 +- 2 files changed, 74 insertions(+), 65 deletions(-) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 5dbd119124..fce5d00570 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -92,7 +92,7 @@ PrintBase::ApplyStatus PrintObject::set_instances(PrintInstances &&instances) // Invalidate and set copies. PrintBase::ApplyStatus status = PrintBase::APPLY_STATUS_UNCHANGED; bool equal_length = instances.size() == m_instances.size(); - bool equal = equal_length && std::equal(instances.begin(), instances.end(), m_instances.begin(), + bool equal = equal_length && std::equal(instances.begin(), instances.end(), m_instances.begin(), [](const PrintInstance& lhs, const PrintInstance& rhs) { return lhs.model_instance == rhs.model_instance && lhs.shift == rhs.shift; }); if (! equal) { status = PrintBase::APPLY_STATUS_CHANGED; @@ -128,7 +128,7 @@ void PrintObject::make_perimeters() m_print->set_status(20, L("Generating perimeters")); BOOST_LOG_TRIVIAL(info) << "Generating perimeters..." << log_memory_info(); - + // Revert the typed slices into untyped slices. if (m_typed_slices) { for (Layer *layer : m_layers) { @@ -137,10 +137,10 @@ void PrintObject::make_perimeters() } m_typed_slices = false; } - + // compare each layer to the one below, and mark those slices needing // one additional inner perimeter, like the top of domed objects- - + // this algorithm makes sure that at least one perimeter is overlapping // but we don't generate any extra perimeter if fill density is zero, as they would be floating // inside the object - infill_only_where_needed should be the method of choice for printing @@ -248,7 +248,7 @@ void PrintObject::prepare_infill() // by the cummulative area of the previous $layerm->fill_surfaces. this->detect_surfaces_type(); m_print->throw_if_canceled(); - + // Decide what surfaces are to be filled. // Here the stTop / stBottomBridge / stBottom infill is turned to just stInternal if zero top / bottom infill layers are configured. // Also tiny stInternal surfaces are turned to stInternalSolid. @@ -323,7 +323,7 @@ void PrintObject::prepare_infill() } // for each layer } // for each region #endif /* SLIC3R_DEBUG_SLICE_PROCESSING */ - + // the following step needs to be done before combination because it may need // to remove only half of the combined infill this->bridge_over_infill(); @@ -459,7 +459,7 @@ void PrintObject::generate_support_material() if (this->set_started(posSupportMaterial)) { this->clear_support_layers(); if ((this->has_support() && m_layers.size() > 1) || (this->has_raft() && ! m_layers.empty())) { - m_print->set_status(85, L("Generating support material")); + m_print->set_status(85, L("Generating support material")); this->_generate_support_material(); m_print->throw_if_canceled(); } else { @@ -619,7 +619,7 @@ bool PrintObject::invalidate_state_by_config_options( } else if ( opt_key == "clip_multipart_objects" || opt_key == "elefant_foot_compensation" - || opt_key == "support_material_contact_distance" + || opt_key == "support_material_contact_distance" || opt_key == "xy_size_compensation") { steps.emplace_back(posSlice); } else if (opt_key == "support_material") { @@ -770,7 +770,7 @@ bool PrintObject::invalidate_state_by_config_options( bool PrintObject::invalidate_step(PrintObjectStep step) { bool invalidated = Inherited::invalidate_step(step); - + // propagate to dependent steps if (step == posPerimeters) { invalidated |= this->invalidate_steps({ posPrepareInfill, posInfill, posIroning }); @@ -843,7 +843,7 @@ void PrintObject::detect_surfaces_type() surfaces_new.assign(num_layers, Surfaces()); tbb::parallel_for( - tbb::blocked_range(0, + tbb::blocked_range(0, spiral_vase ? // In spiral vase mode, reserve the last layer for the top surface if more than 1 layer is planned for the vase bottom. ((num_layers > 1) ? num_layers - 1 : num_layers) : @@ -871,7 +871,7 @@ void PrintObject::detect_surfaces_type() // of current layer and upper one) Surfaces top; if (upper_layer) { - ExPolygons upper_slices = interface_shells ? + ExPolygons upper_slices = interface_shells ? diff_ex(layerm->slices.surfaces, upper_layer->m_regions[region_id]->slices.surfaces, ApplySafetyOffset::Yes) : diff_ex(layerm->slices.surfaces, upper_layer->lslices, ApplySafetyOffset::Yes); surfaces_append(top, opening_ex(upper_slices, offset), stTop); @@ -882,7 +882,7 @@ void PrintObject::detect_surfaces_type() for (Surface &surface : top) surface.surface_type = stTop; } - + // Find bottom surfaces (difference between current surfaces of current layer and lower one). Surfaces bottom; if (lower_layer) { @@ -905,7 +905,7 @@ void PrintObject::detect_surfaces_type() // if user requested internal shells, we need to identify surfaces // lying on other slices not belonging to this region if (interface_shells) { - // non-bridging bottom surfaces: any part of this layer lying + // non-bridging bottom surfaces: any part of this layer lying // on something else, excluding those lying on our own region surfaces_append( bottom, @@ -925,7 +925,7 @@ void PrintObject::detect_surfaces_type() for (Surface &surface : bottom) surface.surface_type = stBottom; } - + // now, if the object contained a thin membrane, we could have overlapping bottom // and top surfaces; let's do an intersection to discover them and consider them // as bottom surfaces (to allow for bridge detection) @@ -948,7 +948,7 @@ void PrintObject::detect_surfaces_type() SVG::export_expolygons(debug_out_path("1_detect_surfaces_type_%d_region%d-layer_%f.svg", iRun ++, region_id, layer->print_z).c_str(), expolygons_with_attributes); } #endif /* SLIC3R_DEBUG_SLICE_PROCESSING */ - + // save surfaces to layer Surfaces &surfaces_out = interface_shells ? surfaces_new[idx_layer] : layerm->slices.surfaces; Surfaces surfaces_backup; @@ -967,7 +967,7 @@ void PrintObject::detect_surfaces_type() surfaces_append(surfaces_out, std::move(top)); surfaces_append(surfaces_out, std::move(bottom)); - + // Slic3r::debugf " layer %d has %d bottom, %d top and %d internal surfaces\n", // $layerm->layer->id, scalar(@bottom), scalar(@top), scalar(@internal) if $Slic3r::debug; @@ -1275,7 +1275,7 @@ void PrintObject::discover_vertical_shells() #endif /* SLIC3R_DEBUG_SLICE_PROCESSING */ Flow solid_infill_flow = layerm->flow(frSolidInfill); - coord_t infill_line_spacing = solid_infill_flow.scaled_spacing(); + coord_t infill_line_spacing = solid_infill_flow.scaled_spacing(); // Find a union of perimeters below / above this surface to guarantee a minimum shell thickness. Polygons shell; Polygons holes; @@ -1311,8 +1311,8 @@ void PrintObject::discover_vertical_shells() if (int n_top_layers = region_config.top_solid_layers.value; n_top_layers > 0) { // Gather top regions projected to this layer. coordf_t print_z = layer->print_z; - for (int i = int(idx_layer) + 1; - i < int(cache_top_botom_regions.size()) && + for (int i = int(idx_layer) + 1; + i < int(cache_top_botom_regions.size()) && (i < int(idx_layer) + n_top_layers || m_layers[i]->print_z - print_z < region_config.top_solid_min_thickness - EPSILON); ++ i) { @@ -1321,7 +1321,7 @@ void PrintObject::discover_vertical_shells() holes = intersection(holes, cache.holes); if (! cache.top_surfaces.empty()) { polygons_append(shell, cache.top_surfaces); - // Running the union_ using the Clipper library piece by piece is cheaper + // Running the union_ using the Clipper library piece by piece is cheaper // than running the union_ all at once. shell = union_(shell); } @@ -1340,7 +1340,7 @@ void PrintObject::discover_vertical_shells() holes = intersection(holes, cache.holes); if (! cache.bottom_surfaces.empty()) { polygons_append(shell, cache.bottom_surfaces); - // Running the union_ using the Clipper library piece by piece is cheaper + // Running the union_ using the Clipper library piece by piece is cheaper // than running the union_ all at once. shell = union_(shell); } @@ -1351,14 +1351,14 @@ void PrintObject::discover_vertical_shells() Slic3r::SVG svg(debug_out_path("discover_vertical_shells-perimeters-before-union-%d.svg", debug_idx), get_extents(shell)); svg.draw(shell); svg.draw_outline(shell, "black", scale_(0.05)); - svg.Close(); + svg.Close(); } #endif /* SLIC3R_DEBUG_SLICE_PROCESSING */ #if 0 { PROFILE_BLOCK(discover_vertical_shells_region_layer_shell_); // shell = union_(shell, true); - shell = union_(shell, false); + shell = union_(shell, false); } #endif #ifdef SLIC3R_DEBUG_SLICE_PROCESSING @@ -1374,7 +1374,7 @@ void PrintObject::discover_vertical_shells() Slic3r::SVG svg(debug_out_path("discover_vertical_shells-perimeters-after-union-%d.svg", debug_idx), get_extents(shell)); svg.draw(shell_ex); svg.draw_outline(shell_ex, "black", "blue", scale_(0.05)); - svg.Close(); + svg.Close(); } #endif /* SLIC3R_DEBUG_SLICE_PROCESSING */ @@ -1386,7 +1386,7 @@ void PrintObject::discover_vertical_shells() svg.draw(shell_ex, "blue", 0.5); svg.draw_outline(shell_ex, "black", "blue", scale_(0.05)); svg.Close(); - } + } { Slic3r::SVG svg(debug_out_path("discover_vertical_shells-internalvoid-wshell-%d.svg", debug_idx), get_extents(shell)); svg.draw(layerm->fill_surfaces.filter_by_type(stInternalVoid), "yellow", 0.5); @@ -1394,15 +1394,15 @@ void PrintObject::discover_vertical_shells() svg.draw(shell_ex, "blue", 0.5); svg.draw_outline(shell_ex, "black", "blue", scale_(0.05)); svg.Close(); - } + } { Slic3r::SVG svg(debug_out_path("discover_vertical_shells-internalvoid-wshell-%d.svg", debug_idx), get_extents(shell)); svg.draw(layerm->fill_surfaces.filter_by_type(stInternalVoid), "yellow", 0.5); svg.draw_outline(layerm->fill_surfaces.filter_by_type(stInternalVoid), "black", "blue", scale_(0.05)); svg.draw(shell_ex, "blue", 0.5); - svg.draw_outline(shell_ex, "black", "blue", scale_(0.05)); + svg.draw_outline(shell_ex, "black", "blue", scale_(0.05)); svg.Close(); - } + } #endif /* SLIC3R_DEBUG_SLICE_PROCESSING */ // Trim the shells region by the internal & internal void surfaces. @@ -1423,7 +1423,7 @@ void PrintObject::discover_vertical_shells() Polygons shell_before = shell; #endif /* SLIC3R_DEBUG_SLICE_PROCESSING */ #if 1 - // Intentionally inflate a bit more than how much the region has been shrunk, + // Intentionally inflate a bit more than how much the region has been shrunk, // so there will be some overlap between this solid infill and the other infill regions (mainly the sparse infill). shell = opening(union_(shell), 0.5f * min_perimeter_infill_spacing, 0.8f * min_perimeter_infill_spacing, ClipperLib::jtSquare); if (shell.empty()) @@ -1505,7 +1505,7 @@ void PrintObject::bridge_over_infill() for (size_t region_id = 0; region_id < this->num_printing_regions(); ++ region_id) { const PrintRegion ®ion = this->printing_region(region_id); - + // skip bridging in case there are no voids if (region.config().fill_density.value == 100) continue; @@ -1514,7 +1514,7 @@ void PrintObject::bridge_over_infill() // skip first layer if (layer_it == m_layers.begin()) continue; - + Layer *layer = *layer_it; LayerRegion *layerm = layer->m_regions[region_id]; Flow bridge_flow = layerm->bridging_flow(frSolidInfill); @@ -1522,31 +1522,31 @@ void PrintObject::bridge_over_infill() // extract the stInternalSolid surfaces that might be transformed into bridges Polygons internal_solid; layerm->fill_surfaces.filter_by_type(stInternalSolid, &internal_solid); - + // check whether the lower area is deep enough for absorbing the extra flow // (for obvious physical reasons but also for preventing the bridge extrudates // from overflowing in 3D preview) ExPolygons to_bridge; { Polygons to_bridge_pp = internal_solid; - + // iterate through lower layers spanned by bridge_flow double bottom_z = layer->print_z - bridge_flow.height() - EPSILON; for (int i = int(layer_it - m_layers.begin()) - 1; i >= 0; --i) { const Layer* lower_layer = m_layers[i]; - + // stop iterating if layer is lower than bottom_z if (lower_layer->print_z < bottom_z) break; - + // iterate through regions and collect internal surfaces Polygons lower_internal; for (LayerRegion *lower_layerm : lower_layer->m_regions) lower_layerm->fill_surfaces.filter_by_type(stInternal, &lower_internal); - + // intersect such lower internal surfaces with the candidate solid surfaces to_bridge_pp = intersection(to_bridge_pp, lower_internal); } - + // there's no point in bridging too thin/short regions //FIXME Vojtech: The offset2 function is not a geometric offset, // therefore it may create 1) gaps, and 2) sharp corners, which are outside the original contour. @@ -1555,17 +1555,17 @@ void PrintObject::bridge_over_infill() float min_width = float(bridge_flow.scaled_width()) * 3.f; to_bridge_pp = opening(to_bridge_pp, min_width); } - + if (to_bridge_pp.empty()) continue; - + // convert into ExPolygons to_bridge = union_ex(to_bridge_pp); } - + #ifdef SLIC3R_DEBUG printf("Bridging %zu internal areas at layer %zu\n", to_bridge.size(), layer->id()); #endif - + // compute the remaning internal solid surfaces as difference ExPolygons not_to_bridge = diff_ex(internal_solid, to_bridge, ApplySafetyOffset::Yes); to_bridge = intersection_ex(to_bridge, internal_solid, ApplySafetyOffset::Yes); @@ -1574,7 +1574,7 @@ void PrintObject::bridge_over_infill() for (ExPolygon &ex : to_bridge) layerm->fill_surfaces.surfaces.push_back(Surface(stInternalBridge, ex)); for (ExPolygon &ex : not_to_bridge) - layerm->fill_surfaces.surfaces.push_back(Surface(stInternalSolid, ex)); + layerm->fill_surfaces.surfaces.push_back(Surface(stInternalSolid, ex)); /* # exclude infill from the layers below if needed # see discussion at https://github.com/alexrj/Slic3r/issues/240 @@ -1603,7 +1603,7 @@ void PrintObject::bridge_over_infill() $lower_layerm->fill_surfaces->clear; $lower_layerm->fill_surfaces->append($_) for @new_surfaces; } - + $excess -= $self->get_layer($i)->height; } } @@ -1693,7 +1693,7 @@ PrintRegionConfig region_config_from_model_volume(const PrintRegionConfig &defau // Switch of infill for very low infill rates, also avoid division by zero in infill generator for these very low rates. // See GH issue #5910. config.fill_density.value = 0; - else + else config.fill_density.value = std::min(config.fill_density.value, 100.); if (config.fuzzy_skin.value != FuzzySkinType::None && (config.fuzzy_skin_point_dist.value < 0.01 || config.fuzzy_skin_thickness.value < 0.001)) config.fuzzy_skin.value = FuzzySkinType::None; @@ -1852,7 +1852,7 @@ void PrintObject::clip_fill_surfaces() // Regularize the overhang regions, so that the infill areas will not become excessively jagged. smooth_outward( closing(upper_internal, closing_radius, ClipperLib::jtSquare, 0.), - scaled(0.1)), + scaled(0.1)), lower_layer_internal_surfaces); // Apply new internal infill to regions. for (LayerRegion *layerm : lower_layer->m_regions) { @@ -1880,7 +1880,7 @@ void PrintObject::clip_fill_surfaces() void PrintObject::discover_horizontal_shells() { BOOST_LOG_TRIVIAL(trace) << "discover_horizontal_shells()"; - + for (size_t region_id = 0; region_id < this->num_printing_regions(); ++ region_id) { for (size_t i = 0; i < m_layers.size(); ++ i) { m_print->throw_if_canceled(); @@ -1899,7 +1899,7 @@ void PrintObject::discover_horizontal_shells() // If ensure_vertical_shell_thickness, then the rest has already been performed by discover_vertical_shells(). if (region_config.ensure_vertical_shell_thickness.value) continue; - + coordf_t print_z = layer->print_z; coordf_t bottom_z = layer->bottom_z(); for (size_t idx_surface_type = 0; idx_surface_type < 3; ++ idx_surface_type) { @@ -1932,11 +1932,11 @@ void PrintObject::discover_horizontal_shells() if (solid.empty()) continue; // Slic3r::debugf "Layer %d has %s surfaces\n", $i, ($type == stTop) ? 'top' : 'bottom'; - + // Scatter top / bottom regions to other layers. Scattering process is inherently serial, it is difficult to parallelize without locking. for (int n = (type == stTop) ? int(i) - 1 : int(i) + 1; (type == stTop) ? - (n >= 0 && (int(i) - n < num_solid_layers || + (n >= 0 && (int(i) - n < num_solid_layers || print_z - m_layers[n]->print_z < region_config.top_solid_min_thickness.value - EPSILON)) : (n < int(m_layers.size()) && (n - int(i) < num_solid_layers || m_layers[n]->bottom_z() - bottom_z < region_config.bottom_solid_min_thickness.value - EPSILON)); @@ -1945,7 +1945,7 @@ void PrintObject::discover_horizontal_shells() // Slic3r::debugf " looking for neighbors on layer %d...\n", $n; // Reference to the lower layer of a TOP surface, or an upper layer of a BOTTOM surface. LayerRegion *neighbor_layerm = m_layers[n]->regions()[region_id]; - + // find intersection between neighbor and current layer's surfaces // intersections have contours and holes // we update $solid so that we limit the next neighbor layer to the areas that were @@ -1980,7 +1980,7 @@ void PrintObject::discover_horizontal_shells() continue; } } - + if (region_config.fill_density.value == 0) { // if we're printing a hollow object we discard any solid shell thinner // than a perimeter width, since it's probably just crossing a sloping wall @@ -1988,7 +1988,7 @@ void PrintObject::discover_horizontal_shells() // obeying the solid shell count option strictly (DWIM!) float margin = float(neighbor_layerm->flow(frExternalPerimeter).scaled_width()); Polygons too_narrow = diff( - new_internal_solid, + new_internal_solid, opening(new_internal_solid, margin, margin + ClipperSafetyOffset, jtMiter, 5)); // Trim the regularized region by the original region. if (! too_narrow.empty()) @@ -2018,20 +2018,20 @@ void PrintObject::discover_horizontal_shells() for (const Surface &surface : neighbor_layerm->fill_surfaces.surfaces) if (surface.is_internal() && !surface.is_bridge()) polygons_append(internal, to_polygons(surface.expolygon)); - polygons_append(new_internal_solid, + polygons_append(new_internal_solid, intersection( expand(too_narrow, +margin), // Discard bridges as they are grown for anchoring and we can't - // remove such anchors. (This may happen when a bridge is being + // remove such anchors. (This may happen when a bridge is being // anchored onto a wall where little space remains after the bridge - // is grown, and that little space is an internal solid shell so + // is grown, and that little space is an internal solid shell so // it triggers this too_narrow logic.) internal)); // see https://github.com/prusa3d/PrusaSlicer/pull/3426 // solid = new_internal_solid; } } - + // internal-solid are the union of the existing internal-solid surfaces // and new ones SurfaceCollection backup = std::move(neighbor_layerm->fill_surfaces); @@ -2110,11 +2110,11 @@ void PrintObject::combine_infill() current_height += layer->height; ++ num_layers; } - + // Append lower layers (if any) to uppermost layer. combine[m_layers.size() - 1] = num_layers; } - + // loop through layers to which we have assigned layers to combine for (size_t layer_idx = 0; layer_idx < m_layers.size(); ++ layer_idx) { m_print->throw_if_canceled(); @@ -2134,8 +2134,8 @@ void PrintObject::combine_infill() intersection = intersection_ex(layerms[i]->fill_surfaces.filter_by_type(stInternal), intersection); double area_threshold = layerms.front()->infill_area_threshold(); if (! intersection.empty() && area_threshold > 0.) - intersection.erase(std::remove_if(intersection.begin(), intersection.end(), - [area_threshold](const ExPolygon &expoly) { return expoly.area() <= area_threshold; }), + intersection.erase(std::remove_if(intersection.begin(), intersection.end(), + [area_threshold](const ExPolygon &expoly) { return expoly.area() <= area_threshold; }), intersection.end()); if (intersection.empty()) continue; @@ -2147,15 +2147,15 @@ void PrintObject::combine_infill() // so let's remove those areas from all layers. Polygons intersection_with_clearance; intersection_with_clearance.reserve(intersection.size()); - float clearance_offset = + float clearance_offset = 0.5f * layerms.back()->flow(frPerimeter).scaled_width() + - // Because fill areas for rectilinear and honeycomb are grown + // Because fill areas for rectilinear and honeycomb are grown // later to overlap perimeters, we need to counteract that too. ((region.config().fill_pattern == ipRectilinear || region.config().fill_pattern == ipMonotonic || region.config().fill_pattern == ipGrid || region.config().fill_pattern == ipLine || - region.config().fill_pattern == ipHoneycomb) ? 1.5f : 0.5f) * + region.config().fill_pattern == ipHoneycomb) ? 1.5f : 0.5f) * layerms.back()->flow(frSolidInfill).scaled_width(); for (ExPolygon &expoly : intersection) polygons_append(intersection_with_clearance, offset(expoly, clearance_offset)); @@ -2367,7 +2367,7 @@ static void project_triangles_to_slabs(ConstLayerPtrsAdaptor layers, const index // The resulting triangles are fed to the Clipper library, which seem to handle flipped triangles well. // if (cross2(Vec2d((poly.pts[1] - poly.pts[0]).cast()), Vec2d((poly.pts[2] - poly.pts[1]).cast())) < 0) // std::swap(poly.pts.front(), poly.pts.back()); - + out[layer_id].emplace_back(std::move(poly.pts)); ++layer_id; } diff --git a/src/libslic3r/SupportableIssuesSearch.cpp b/src/libslic3r/SupportableIssuesSearch.cpp index 7309c462f7..93c10e7bb1 100644 --- a/src/libslic3r/SupportableIssuesSearch.cpp +++ b/src/libslic3r/SupportableIssuesSearch.cpp @@ -230,12 +230,21 @@ struct WeightDistributionMatrix { CentroidAccumulators accumulators(issues.supports_nedded.size() + 4); int next_island_id = -1; + auto custom_comparator = [](const Vec2i& left,const Vec2i& right){ + if (left.x() == right.x()) { + return left.y() < right.y(); + } + return left.x() < right.x(); + }; + std::set coords_to_check(custom_comparator); + for (int y = 0; y < global_cell_count.y(); ++y) { for (int x = 0; x < global_cell_count.x(); ++x) { Cell &cell = this->access_cell(Vec3i(x, y, 0)); if (cell.weight > 0 && cell.island_id == std::numeric_limits::max()) { CentroidAccumulator &acc = accumulators.create_accumulator(next_island_id, 0); - std::set coords_to_check { Vec2i(x, y) }; + coords_to_check.clear(); + coords_to_check.insert(Vec2i(x,y)); while (!coords_to_check.empty()) { Vec2i current_coords = *coords_to_check.begin(); coords_to_check.erase(coords_to_check.begin()); From 824e3f111e537ef8c84333674febc328ae3ff547 Mon Sep 17 00:00:00 2001 From: Godrak Date: Wed, 27 Apr 2022 18:37:16 +0200 Subject: [PATCH 013/100] extended model with balance checking - centroids of segments, bed adhesion, supports adhesion, model stability --- src/libslic3r/SupportableIssuesSearch.cpp | 179 +++++++++++++++------- src/libslic3r/SupportableIssuesSearch.hpp | 4 + 2 files changed, 125 insertions(+), 58 deletions(-) diff --git a/src/libslic3r/SupportableIssuesSearch.cpp b/src/libslic3r/SupportableIssuesSearch.cpp index 93c10e7bb1..5ae1547457 100644 --- a/src/libslic3r/SupportableIssuesSearch.cpp +++ b/src/libslic3r/SupportableIssuesSearch.cpp @@ -48,7 +48,7 @@ CurledFilament::CurledFilament(const Vec3f &position) : } struct Cell { - float weight; + float volume; float curled_height; int island_id = std::numeric_limits::max(); }; @@ -57,7 +57,7 @@ struct CentroidAccumulator { Polygon convex_hull { }; Points points { }; Vec3d accumulated_value { }; - float accumulated_weight { }; + float accumulated_volume { }; const double base_height { }; explicit CentroidAccumulator(double base_height) : @@ -66,6 +66,7 @@ struct CentroidAccumulator { void calculate_base_hull() { convex_hull = Geometry::convex_hull(points); + assert(convex_hull.is_counter_clockwise()); } }; @@ -94,19 +95,19 @@ struct CentroidAccumulators { CentroidAccumulator &from_acc = this->access(from_id); CentroidAccumulator &to_acc = this->access(to_id); to_acc.accumulated_value += from_acc.accumulated_value; - to_acc.accumulated_weight += from_acc.accumulated_weight; + to_acc.accumulated_volume += from_acc.accumulated_volume; to_acc.points.insert(to_acc.points.end(), from_acc.points.begin(), from_acc.points.end()); to_acc.calculate_base_hull(); mapping[from_id] = mapping[to_id]; } }; -struct WeightDistributionMatrix { +struct BalanceDistributionGrid { // Lets make Z coord half the size of X (and Y). // This corresponds to angle of ~26 degrees between center of one cell and other one up and sideways // which is approximately a limiting printable angle. - WeightDistributionMatrix() = default; + BalanceDistributionGrid() = default; void init(const PrintObject *po, size_t layer_idx_begin, size_t layer_idx_end) { Vec2crd size_half = po->size().head<2>().cwiseQuotient(Vec2crd(2, 2)) + Vec2crd::Ones(); @@ -188,7 +189,7 @@ struct WeightDistributionMatrix { return cells[this->to_cell_index(local_cell_coords)]; } - void distribute_edge_weight(const Point &p1, const Point &p2, float print_z, float unscaled_width) { + void distribute_edge(const Point &p1, const Point &p2, float print_z, float unscaled_width, float unscaled_height) { Vec2d dir = (p2 - p1).cast(); double length = dir.norm(); if (length < 0.01) { @@ -197,20 +198,22 @@ struct WeightDistributionMatrix { dir /= length; double step_size = this->cell_size.x() / 2.0; + float diameter = unscaled_height * unscaled_width * 0.7071f; // approximate constant to consider eliptical shape (1/sqrt(2)) + double distributed_length = 0; while (distributed_length < length) { double next_len = std::min(length, distributed_length + step_size); double current_dist_payload = next_len - distributed_length; Point location = p1 + ((next_len / length) * dir).cast(); - float payload = unscale(current_dist_payload) * unscaled_width; - this->access_cell(location, print_z).weight += payload; + float payload = unscale(current_dist_payload) * diameter; + this->access_cell(location, print_z).volume += payload; distributed_length = next_len; } } - void merge(const WeightDistributionMatrix &other) { + void merge(const BalanceDistributionGrid &other) { int z_start = std::max(local_z_index_offset, other.local_z_index_offset); int z_end = std::min(local_z_index_offset + local_z_cell_count, other.local_z_index_offset + other.local_z_cell_count); @@ -220,17 +223,17 @@ struct WeightDistributionMatrix { Vec3i global_coords { x, y, z }; Vec3i local_coords = this->to_local_cell_coords(global_coords); Vec3i other_local_coords = other.to_local_cell_coords(global_coords); - this->access_cell(local_coords).weight += other.access_cell(other_local_coords).weight; + this->access_cell(local_coords).volume += other.access_cell(other_local_coords).volume; } } } } - void analyze(Issues &issues) { + void analyze(Issues &issues, const Params ¶ms) { CentroidAccumulators accumulators(issues.supports_nedded.size() + 4); int next_island_id = -1; - auto custom_comparator = [](const Vec2i& left,const Vec2i& right){ + auto custom_comparator = [](const Vec2i &left, const Vec2i &right) { if (left.x() == right.x()) { return left.y() < right.y(); } @@ -241,21 +244,21 @@ struct WeightDistributionMatrix { for (int y = 0; y < global_cell_count.y(); ++y) { for (int x = 0; x < global_cell_count.x(); ++x) { Cell &cell = this->access_cell(Vec3i(x, y, 0)); - if (cell.weight > 0 && cell.island_id == std::numeric_limits::max()) { + if (cell.volume > 0 && cell.island_id == std::numeric_limits::max()) { CentroidAccumulator &acc = accumulators.create_accumulator(next_island_id, 0); coords_to_check.clear(); - coords_to_check.insert(Vec2i(x,y)); + coords_to_check.insert(Vec2i(x, y)); while (!coords_to_check.empty()) { Vec2i current_coords = *coords_to_check.begin(); coords_to_check.erase(coords_to_check.begin()); cell = this->access_cell(Vec3i(current_coords.x(), current_coords.y(), 0)); - if (cell.weight > 0 && cell.island_id == std::numeric_limits::max()) { + if (cell.volume > 0 && cell.island_id == std::numeric_limits::max()) { cell.island_id = next_island_id; Vec3crd cell_center = this->get_cell_center( Vec3i(current_coords.x(), current_coords.y(), local_z_index_offset)); acc.points.push_back(Point(cell_center.head<2>())); - acc.accumulated_value += cell_center.cast() * cell.weight; - acc.accumulated_weight += cell.weight; + acc.accumulated_value += cell_center.cast() * cell.volume; + acc.accumulated_volume += cell.volume; for (int y_offset = -1; y_offset <= 1; ++y_offset) { for (int x_offset = -1; x_offset <= 1; ++x_offset) { if (y_offset != 0 || x_offset != 0) { @@ -326,9 +329,9 @@ struct WeightDistributionMatrix { //Propagate to accumulators. TODO what to do if no supporter is found? if (current.island_id != std::numeric_limits::max()) { CentroidAccumulator &acc = accumulators.access(current.island_id); - acc.accumulated_value += current.weight + acc.accumulated_value += current.volume * this->get_cell_center(this->to_global_cell_coords(Vec3i(x, y, z))).cast(); - acc.accumulated_weight += current.weight; + acc.accumulated_volume += current.volume; } } } @@ -338,16 +341,76 @@ struct WeightDistributionMatrix { // This amount of work is proportional to the increase of height of the centroid during toppling. // image here: https://hgphysics.com/gph/c-forces/2-force-effects/1-moment/stability/ // better image in Czech here in the first question: https://www.priklady.eu/cs/fyzika/mechanika-tuheho-telesa/stabilita-teles.alej - for (int acc_index : modified_acc_ids) { - CentroidAccumulator &acc = accumulators.access(acc_index); - Vec3d centroid = acc.accumulated_value / acc.accumulated_weight; + for (int acc_id : modified_acc_ids) { + CentroidAccumulator &acc = accumulators.access(acc_id); + Vec3d centroid = acc.accumulated_value / acc.accumulated_volume; //determine signed shortest distance to the convex hull Point centroid_base_projection = Point(centroid.head<2>().cast()); + Point pivot; double distance_sq = std::numeric_limits::max(); bool inside = true; - for (Line line : acc.convex_hull.lines()) { - distance_sq = std::min(line.distance_to_squared(centroid_base_projection), distance_sq); + if (acc.convex_hull.points.size() == 1) { + pivot = acc.convex_hull.points[0]; + distance_sq = (pivot - centroid_base_projection).squaredNorm(); + inside = true; + } else { + for (Line line : acc.convex_hull.lines()) { + Point closest_point; + double dist_sq = line.distance_to_squared(centroid_base_projection, &closest_point); + if (dist_sq < distance_sq) { + pivot = closest_point; + distance_sq = dist_sq; + } + if (float(angle(line.b - line.a, centroid_base_projection - line.b)) < 0) { + inside = false; + } + } } + + bool additional_supports_needed = false; + double base_area = std::max(acc.convex_hull.area(), scale_(5.0)); // assume 5 mm area for support points and other degenerate bases + double sticking_force = base_area * (acc.base_height == 0 ? params.base_adhesion : params.support_adhesion); + if (inside) { + double toppling_force = (Vec2d(sqrt(distance_sq), acc.base_height).norm() - acc.base_height) * acc.accumulated_volume; + sticking_force += toppling_force; + } + double y_movement_force = 0.5f * acc.accumulated_volume * params.top_object_movement_speed + * params.top_object_movement_speed; + if (sticking_force < y_movement_force) { + additional_supports_needed = true; + } + + if (!inside) { + double torque = sqrt(distance_sq) * acc.accumulated_volume; + if (torque > sticking_force) { //comparing apples and oranges; but we are far beyond physical simulation + additional_supports_needed = true; + } + } + + if (additional_supports_needed) { + Vec2crd attractor_dir = inside ? pivot - centroid_base_projection : centroid_base_projection - pivot; + Vec2d attractor = centroid_base_projection.cast() + (1e9 * attractor_dir.cast().normalized()); + double min_dist = std::numeric_limits::max(); + Vec3d support_point = centroid; + for (int y = 0; y < global_cell_count.y(); ++y) { + for (int x = 0; x < global_cell_count.x(); ++x) { + Cell &cell = this->access_cell(Vec3i(x, y, 0)); + if (cell.island_id == acc_id) { + Vec3d cell_center = this->get_cell_center(this->to_global_cell_coords(Vec3i(x, y, z))).cast(); + double dist_sq = (cell_center.head<2>() - attractor).squaredNorm(); + if (dist_sq < min_dist) { + min_dist = dist_sq; + support_point = cell_center; + } + } + } + } + + issues.supports_nedded.emplace_back(support_point.cast()); + acc.points.push_back(Point(support_point.head<2>().cast())); + acc.calculate_base_hull(); + } + } } } @@ -356,32 +419,32 @@ struct WeightDistributionMatrix { void debug_export(std::string file_name) const { Slic3r::CNumericLocalesSetter locales_setter; { - FILE *fp = boost::nowide::fopen(debug_out_path((file_name + "_matrix.obj").c_str()).c_str(), "w"); + FILE *fp = boost::nowide::fopen(debug_out_path((file_name + "_grid.obj").c_str()).c_str(), "w"); if (fp == nullptr) { BOOST_LOG_TRIVIAL(error) << "Debug files: Couldn't open " << file_name << " for writing"; return; } - float max_weight = 0; + float max_volume = 0; for (int x = 0; x < global_cell_count.x(); ++x) { for (int y = 0; y < global_cell_count.y(); ++y) { for (int z = 0; z < local_z_cell_count; ++z) { const Cell &cell = access_cell(Vec3i(x, y, z)); - max_weight = std::max(max_weight, cell.weight); + max_volume = std::max(max_volume, cell.volume); } } } - max_weight *= 0.8; + max_volume *= 0.8; for (int x = 0; x < global_cell_count.x(); ++x) { for (int y = 0; y < global_cell_count.y(); ++y) { for (int z = 0; z < local_z_cell_count; ++z) { Vec3f center = unscale(get_cell_center(to_global_cell_coords(Vec3i { x, y, z }))).cast(); const Cell &cell = access_cell(Vec3i(x, y, z)); - if (cell.weight != 0) { - fprintf(fp, "v %f %f %f %f %f %f\n", center(0), center(1), center(2), cell.weight / max_weight, 0.0, 0.0); + if (cell.volume != 0) { + fprintf(fp, "v %f %f %f %f %f %f\n", center(0), center(1), center(2), cell.volume / max_volume, 0.0, 0.0); } } } @@ -516,12 +579,12 @@ struct SegmentAccumulator { }; Issues check_extrusion_entity_stability(const ExtrusionEntity *entity, float print_z, const LayerRegion *layer_region, - const EdgeGridWrapper &supported_grid, WeightDistributionMatrix &weight_matrix, const Params ¶ms) { + const EdgeGridWrapper &supported_grid, BalanceDistributionGrid &balance_grid, const Params ¶ms) { Issues issues { }; if (entity->is_collection()) { for (const auto *e : static_cast(entity)->entities) { - issues.add(check_extrusion_entity_stability(e, print_z, layer_region, supported_grid, weight_matrix, params)); + issues.add(check_extrusion_entity_stability(e, print_z, layer_region, supported_grid, balance_grid, params)); } } else { //single extrusion path, with possible varying parameters //prepare stack of points on the extrusion path. If there are long segments, additional points might be pushed onto the stack during the algorithm. @@ -543,6 +606,7 @@ Issues check_extrusion_entity_stability(const ExtrusionEntity *entity, float pri Point prev_point = points.top(); // prev point of the path. Initialize with first point. Vec3f prev_fpoint = to_vec3f(prev_point); coordf_t flow_width = get_flow_width(layer_region, entity->role()); + coordf_t layer_height = layer_region->layer()->height; bool external_perimters_first = layer_region->region().config().external_perimeters_first; const coordf_t max_allowed_dist_from_prev_layer = get_max_allowed_distance(entity->role(), flow_width, external_perimters_first, params); @@ -553,7 +617,7 @@ Issues check_extrusion_entity_stability(const ExtrusionEntity *entity, float pri Vec3f fpoint = to_vec3f(point); float edge_len = (fpoint - prev_fpoint).norm(); - weight_matrix.distribute_edge_weight(prev_point, point, print_z, unscale(flow_width)); + balance_grid.distribute_edge(prev_point, point, print_z, unscale(flow_width), unscale(layer_height)); coordf_t dist_from_prev_layer { 0 }; if (!supported_grid.signed_distance(point, flow_width, dist_from_prev_layer)) { // dist from prev layer not found, assume empty layer @@ -617,7 +681,7 @@ Issues check_extrusion_entity_stability(const ExtrusionEntity *entity, float pri return issues; } -Issues check_layer_stability(const PrintObject *po, size_t layer_idx, bool full_check, WeightDistributionMatrix &weight_matrix, +Issues check_layer_stability(const PrintObject *po, size_t layer_idx, bool full_check, BalanceDistributionGrid &balance_grid, const Params ¶ms) { std::cout << "Checking: " << layer_idx << std::endl; if (layer_idx == 0) { @@ -634,7 +698,7 @@ Issues check_layer_stability(const PrintObject *po, size_t layer_idx, bool full_ for (const ExtrusionEntity *ex_entity : layer_region->perimeters.entities) { for (const ExtrusionEntity *perimeter : static_cast(ex_entity)->entities) { issues.add( - check_extrusion_entity_stability(perimeter, layer->print_z, layer_region, supported_grid, weight_matrix, + check_extrusion_entity_stability(perimeter, layer->print_z, layer_region, supported_grid, balance_grid, params)); } // perimeter } // ex_entity @@ -642,8 +706,7 @@ Issues check_layer_stability(const PrintObject *po, size_t layer_idx, bool full_ for (const ExtrusionEntity *fill : static_cast(ex_entity)->entities) { if (fill->role() == ExtrusionRole::erGapFill || fill->role() == ExtrusionRole::erBridgeInfill) { issues.add( - check_extrusion_entity_stability(fill, layer->print_z, layer_region, supported_grid, weight_matrix, - params)); + check_extrusion_entity_stability(fill, layer->print_z, layer_region, supported_grid, balance_grid, params)); } } // fill } // ex_entity @@ -656,7 +719,7 @@ Issues check_layer_stability(const PrintObject *po, size_t layer_idx, bool full_ if (perimeter->role() == ExtrusionRole::erExternalPerimeter || perimeter->role() == ExtrusionRole::erOverhangPerimeter) { issues.add( - check_extrusion_entity_stability(perimeter, layer->print_z, layer_region, supported_grid, weight_matrix, + check_extrusion_entity_stability(perimeter, layer->print_z, layer_region, supported_grid, balance_grid, params)); }; // ex_perimeter } // perimeter @@ -672,26 +735,26 @@ Issues check_layer_stability(const PrintObject *po, size_t layer_idx, bool full_ std::vector quick_search(const PrintObject *po, const Params ¶ms) { using namespace Impl; - WeightDistributionMatrix matrix { }; - matrix.init(po, 0, po->layers().size()); - std::mutex matrix_mutex; + BalanceDistributionGrid grid { }; + grid.init(po, 0, po->layers().size()); + std::mutex grid_mutex; size_t layer_count = po->layer_count(); std::vector layer_needs_supports(layer_count, false); tbb::parallel_for(tbb::blocked_range(1, layer_count), [&](tbb::blocked_range r) { - WeightDistributionMatrix weight_matrix { }; - weight_matrix.init(po, r.begin(), r.end()); + BalanceDistributionGrid balance_grid { }; + balance_grid.init(po, r.begin(), r.end()); for (size_t layer_idx = r.begin(); layer_idx < r.end(); ++layer_idx) { - auto layer_issues = check_layer_stability(po, layer_idx, false, weight_matrix, params); + auto layer_issues = check_layer_stability(po, layer_idx, false, balance_grid, params); if (!layer_issues.supports_nedded.empty()) { layer_needs_supports[layer_idx] = true; } } - matrix_mutex.lock(); - matrix.merge(weight_matrix); - matrix_mutex.unlock(); + grid_mutex.lock(); + grid.merge(balance_grid); + grid_mutex.unlock(); }); std::vector problematic_layers; @@ -706,26 +769,26 @@ std::vector quick_search(const PrintObject *po, const Params ¶ms) { Issues full_search(const PrintObject *po, const Params ¶ms) { using namespace Impl; - WeightDistributionMatrix matrix { }; - matrix.init(po, 0, po->layers().size()); - std::mutex matrix_mutex; + BalanceDistributionGrid grid { }; + grid.init(po, 0, po->layers().size()); + std::mutex grid_mutex; size_t layer_count = po->layer_count(); Issues found_issues = tbb::parallel_reduce(tbb::blocked_range(1, layer_count), Issues { }, [&](tbb::blocked_range r, const Issues &init) { - WeightDistributionMatrix weight_matrix { }; - weight_matrix.init(po, r.begin(), r.end()); + BalanceDistributionGrid balance_grid { }; + balance_grid.init(po, r.begin(), r.end()); Issues issues = init; for (size_t layer_idx = r.begin(); layer_idx < r.end(); ++layer_idx) { - auto layer_issues = check_layer_stability(po, layer_idx, true, weight_matrix, params); + auto layer_issues = check_layer_stability(po, layer_idx, true, balance_grid, params); if (!layer_issues.empty()) { issues.add(layer_issues); } } - matrix_mutex.lock(); - matrix.merge(weight_matrix); - matrix_mutex.unlock(); + grid_mutex.lock(); + grid.merge(balance_grid); + grid_mutex.unlock(); return issues; }, @@ -735,9 +798,9 @@ Issues full_search(const PrintObject *po, const Params ¶ms) { } ); - matrix.analyze(found_issues); + grid.analyze(found_issues, params); - matrix.debug_export("weight"); + grid.debug_export("volume"); #ifdef DEBUG_FILES Impl::debug_export(found_issues, "issues"); diff --git a/src/libslic3r/SupportableIssuesSearch.hpp b/src/libslic3r/SupportableIssuesSearch.hpp index fef4eecd5e..867ccd21b5 100644 --- a/src/libslic3r/SupportableIssuesSearch.hpp +++ b/src/libslic3r/SupportableIssuesSearch.hpp @@ -14,6 +14,10 @@ struct Params { float max_first_ex_perim_unsupported_distance_factor = 0.0f; // if external perim first, return tighter max allowed distance from previous layer extrusion float max_unsupported_distance_factor = 1.0f; // For internal perimeters, infill, bridges etc, allow gap of [extrusion width] size, these extrusions have usually something to stick to. float bridge_distance_decrease_by_curvature_factor = 5.0f; // allowed bridge distance = bridge_distance / ( 1 + this factor * (curvature / PI) ) + + float base_adhesion = 60.0f; // adhesion per mm^2 of first layer; the value should say how much *volume* it can hold per one square millimiter + float support_adhesion = 300.0f; // adhesion per mm^2 of support interface layer + float top_object_movement_speed = 200.0f; // movement speed of 200 mm/s in Y }; struct SupportPoint { From 6caec6926cd837a2c70e07ae3a7df001acc13c33 Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Thu, 28 Apr 2022 17:16:58 +0200 Subject: [PATCH 014/100] TON of bugfixes, balancing still does not work --- src/libslic3r/PrintObject.cpp | 5 +- src/libslic3r/SupportableIssuesSearch.cpp | 353 ++++++++++++++-------- 2 files changed, 230 insertions(+), 128 deletions(-) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index fce5d00570..f8fcc22039 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -421,7 +421,8 @@ void PrintObject::find_supportable_issues() } } else { SupportableIssues::Issues issues = SupportableIssues::full_search(this); - if (!issues.supports_nedded.empty()) { + //TODO fix +// if (!issues.supports_nedded.empty()) { auto obj_transform = this->trafo_centered(); for (ModelVolume *model_volume : this->model_object()->volumes) { if (model_volume->type() == ModelVolumeType::MODEL_PART) { @@ -443,7 +444,7 @@ void PrintObject::find_supportable_issues() #endif } } - } +// } } m_print->throw_if_canceled(); diff --git a/src/libslic3r/SupportableIssuesSearch.cpp b/src/libslic3r/SupportableIssuesSearch.cpp index 5ae1547457..f2612035d9 100644 --- a/src/libslic3r/SupportableIssuesSearch.cpp +++ b/src/libslic3r/SupportableIssuesSearch.cpp @@ -17,13 +17,15 @@ #ifdef DEBUG_FILES #include +#include "libslic3r/Color.hpp" #endif namespace Slic3r { namespace SupportableIssues { void Issues::add(const Issues &layer_issues) { - supports_nedded.insert(supports_nedded.end(), layer_issues.supports_nedded.begin(), layer_issues.supports_nedded.end()); + supports_nedded.insert(supports_nedded.end(), layer_issues.supports_nedded.begin(), + layer_issues.supports_nedded.end()); curling_up.insert(curling_up.end(), layer_issues.curling_up.begin(), layer_issues.curling_up.end()); } @@ -56,7 +58,7 @@ struct Cell { struct CentroidAccumulator { Polygon convex_hull { }; Points points { }; - Vec3d accumulated_value { }; + Vec3d accumulated_value = Vec3d::Zero(); float accumulated_volume { }; const double base_height { }; @@ -89,11 +91,11 @@ struct CentroidAccumulators { } void merge_to(int from_id, int to_id) { - if (from_id == to_id) { - return; - } CentroidAccumulator &from_acc = this->access(from_id); CentroidAccumulator &to_acc = this->access(to_id); + if (&from_acc == &to_acc) { + return; + } to_acc.accumulated_value += from_acc.accumulated_value; to_acc.accumulated_volume += from_acc.accumulated_volume; to_acc.points.insert(to_acc.points.end(), from_acc.points.begin(), from_acc.points.end()); @@ -103,10 +105,6 @@ struct CentroidAccumulators { }; struct BalanceDistributionGrid { - // Lets make Z coord half the size of X (and Y). - // This corresponds to angle of ~26 degrees between center of one cell and other one up and sideways - // which is approximately a limiting printable angle. - BalanceDistributionGrid() = default; void init(const PrintObject *po, size_t layer_idx_begin, size_t layer_idx_end) { @@ -165,12 +163,14 @@ struct BalanceDistributionGrid { assert(local_cell_coords.z() >= 0); assert(local_cell_coords.z() < local_z_cell_count); - return local_cell_coords.z() * global_cell_count.x() * global_cell_count.y() + local_cell_coords.y() * global_cell_count.x() + return local_cell_coords.z() * global_cell_count.x() * global_cell_count.y() + + local_cell_coords.y() * global_cell_count.x() + local_cell_coords.x(); } Vec3crd get_cell_center(const Vec3i &global_cell_coords) const { - return global_origin + global_cell_coords.cwiseProduct(this->cell_size) + this->cell_size.cwiseQuotient(Vec3crd(2, 2, 2)); + return global_origin + global_cell_coords.cwiseProduct(this->cell_size) + + this->cell_size.cwiseQuotient(Vec3crd(2, 2, 2)); } Cell& access_cell(const Point &p, float print_z) { @@ -192,13 +192,12 @@ struct BalanceDistributionGrid { void distribute_edge(const Point &p1, const Point &p2, float print_z, float unscaled_width, float unscaled_height) { Vec2d dir = (p2 - p1).cast(); double length = dir.norm(); - if (length < 0.01) { + if (length < 0.1) { return; } - dir /= length; double step_size = this->cell_size.x() / 2.0; - float diameter = unscaled_height * unscaled_width * 0.7071f; // approximate constant to consider eliptical shape (1/sqrt(2)) + float diameter = unscaled_height * unscaled_width * 0.7071f; // constant to simulate somewhat elliptical shape (1/sqrt(2)) double distributed_length = 0; while (distributed_length < length) { @@ -215,7 +214,8 @@ struct BalanceDistributionGrid { void merge(const BalanceDistributionGrid &other) { int z_start = std::max(local_z_index_offset, other.local_z_index_offset); - int z_end = std::min(local_z_index_offset + local_z_cell_count, other.local_z_index_offset + other.local_z_cell_count); + int z_end = std::min(local_z_index_offset + local_z_cell_count, + other.local_z_index_offset + other.local_z_cell_count); for (int x = 0; x < global_cell_count.x(); ++x) { for (int y = 0; y < global_cell_count.y(); ++y) { @@ -230,40 +230,49 @@ struct BalanceDistributionGrid { } void analyze(Issues &issues, const Params ¶ms) { + const auto validate_xy_coords = [&](const Vec2i &local_coords) { + return local_coords.x() >= 0 && local_coords.y() >= 0 && local_coords.x() < this->global_cell_count.x() + && local_coords.y() < this->global_cell_count.y(); + }; CentroidAccumulators accumulators(issues.supports_nedded.size() + 4); - - int next_island_id = -1; auto custom_comparator = [](const Vec2i &left, const Vec2i &right) { if (left.x() == right.x()) { return left.y() < right.y(); } return left.x() < right.x(); }; - std::set coords_to_check(custom_comparator); + int next_island_id = -1; + std::set coords_to_check(custom_comparator); for (int y = 0; y < global_cell_count.y(); ++y) { for (int x = 0; x < global_cell_count.x(); ++x) { - Cell &cell = this->access_cell(Vec3i(x, y, 0)); - if (cell.volume > 0 && cell.island_id == std::numeric_limits::max()) { + Cell &origin_cell = this->access_cell(Vec3i(x, y, 0)); + if (origin_cell.volume > 0 && origin_cell.island_id == std::numeric_limits::max()) { CentroidAccumulator &acc = accumulators.create_accumulator(next_island_id, 0); coords_to_check.clear(); coords_to_check.insert(Vec2i(x, y)); while (!coords_to_check.empty()) { Vec2i current_coords = *coords_to_check.begin(); coords_to_check.erase(coords_to_check.begin()); - cell = this->access_cell(Vec3i(current_coords.x(), current_coords.y(), 0)); - if (cell.volume > 0 && cell.island_id == std::numeric_limits::max()) { - cell.island_id = next_island_id; - Vec3crd cell_center = this->get_cell_center( - Vec3i(current_coords.x(), current_coords.y(), local_z_index_offset)); - acc.points.push_back(Point(cell_center.head<2>())); - acc.accumulated_value += cell_center.cast() * cell.volume; - acc.accumulated_volume += cell.volume; - for (int y_offset = -1; y_offset <= 1; ++y_offset) { - for (int x_offset = -1; x_offset <= 1; ++x_offset) { - if (y_offset != 0 || x_offset != 0) { - coords_to_check.insert(Vec2i(current_coords.x() + x_offset, current_coords.y() + y_offset)); - } + if (!validate_xy_coords(current_coords)) { + continue; + } + Cell &cell = this->access_cell(Vec3i(current_coords.x(), current_coords.y(), 0)); + if (cell.volume <= 0 || cell.island_id != std::numeric_limits::max()) { + continue; + } + cell.island_id = next_island_id; + Vec3crd cell_center = this->get_cell_center( + Vec3i(current_coords.x(), current_coords.y(), local_z_index_offset)); + acc.points.push_back(Point(cell_center.head<2>())); + acc.accumulated_value += cell_center.cast() * cell.volume; + acc.accumulated_volume += cell.volume; + + for (int y_offset = -1; y_offset <= 1; ++y_offset) { + for (int x_offset = -1; x_offset <= 1; ++x_offset) { + if (y_offset != 0 || x_offset != 0) { + coords_to_check.insert( + Vec2i(current_coords.x() + x_offset, current_coords.y() + y_offset)); } } } @@ -274,13 +283,16 @@ struct BalanceDistributionGrid { } } - std::sort(issues.supports_nedded.begin(), issues.supports_nedded.end(), [](const SupportPoint &left, const SupportPoint &right) { - return left.position.z() < right.position.z(); - }); + std::sort(issues.supports_nedded.begin(), issues.supports_nedded.end(), + [](const SupportPoint &left, const SupportPoint &right) { + return left.position.z() < right.position.z(); + }); for (int index = 0; index < int(issues.supports_nedded.size()); ++index) { - Vec3i local_coords = this->to_local_cell_coords(this->to_global_cell_coords(issues.supports_nedded[index].position)); + Vec3i local_coords = this->to_local_cell_coords( + this->to_global_cell_coords(issues.supports_nedded[index].position)); this->access_cell(local_coords).island_id = index; - CentroidAccumulator &acc = accumulators.create_accumulator(index, issues.supports_nedded[index].position.z()); + CentroidAccumulator &acc = accumulators.create_accumulator(index, + issues.supports_nedded[index].position.z()); acc.points.push_back(Point(scaled(Vec2f(issues.supports_nedded[index].position.head<2>())))); acc.calculate_base_hull(); } @@ -289,11 +301,6 @@ struct BalanceDistributionGrid { this->access_cell(curling.position).curled_height += curling.estimated_height; } - const auto validate_xy_coords = [&](const Vec2i &local_coords) { - return local_coords.x() >= 0 && local_coords.y() >= 0 && local_coords.x() < this->global_cell_count.x() - && local_coords.y() < this->global_cell_count.y(); - }; - std::unordered_set modified_acc_ids; modified_acc_ids.reserve(issues.supports_nedded.size() + 1); for (int z = 1; z < local_z_cell_count; ++z) { @@ -301,37 +308,47 @@ struct BalanceDistributionGrid { for (int x = 0; x < global_cell_count.x(); ++x) { for (int y = 0; y < global_cell_count.y(); ++y) { - Cell ¤t = this->access_cell(Vec3i(x, y, z)); - //first determine island id - if (current.island_id == std::numeric_limits::max()) { + if (current.volume > 0) { for (int y_offset = -1; y_offset <= 1; ++y_offset) { for (int x_offset = -1; x_offset <= 1; ++x_offset) { - Vec2i xy_coords { x + x_offset, y + y_offset }; - if (validate_xy_coords(xy_coords)) { - Cell &under = this->access_cell(Vec3i(x, y, z - 1)); - int island_id = std::min(under.island_id, current.island_id); - int merging_id = std::max(under.island_id, current.island_id); - if (merging_id != std::numeric_limits::max() && island_id != merging_id) { - accumulators.merge_to(merging_id, island_id); - } - if (island_id != std::numeric_limits::max()) { - current.island_id = island_id; - modified_acc_ids.insert(current.island_id); - } - - current.curled_height += under.curled_height / (2 + std::abs(x_offset) + std::abs(y_offset)); + if (validate_xy_coords(Vec2i(x_offset, y_offset))) { + Cell &under = this->access_cell(Vec3i(x + x_offset, y + y_offset, z - 1)); + current.curled_height += under.curled_height + / (2 + std::abs(x_offset) + std::abs(y_offset)); } } } } - //Propagate to accumulators. TODO what to do if no supporter is found? - if (current.island_id != std::numeric_limits::max()) { - CentroidAccumulator &acc = accumulators.access(current.island_id); - acc.accumulated_value += current.volume - * this->get_cell_center(this->to_global_cell_coords(Vec3i(x, y, z))).cast(); - acc.accumulated_volume += current.volume; + if (current.volume > 0 && current.island_id == std::numeric_limits::max()) { + int min_island_id_found = std::numeric_limits::max(); + std::unordered_set ids_to_merge { }; + for (int y_offset = -2; y_offset <= 2; ++y_offset) { + for (int x_offset = -2; x_offset <= 2; ++x_offset) { + if (validate_xy_coords(Vec2i(x + x_offset, y + y_offset))) { + Cell &under = this->access_cell(Vec3i(x + x_offset, y + y_offset, z - 1)); + if (under.island_id < min_island_id_found) { + min_island_id_found = under.island_id; + } + ids_to_merge.insert(under.island_id); + } + } + } + if (min_island_id_found < std::numeric_limits::max()) { + ids_to_merge.erase(std::numeric_limits::max()); + ids_to_merge.erase(min_island_id_found); + current.island_id = min_island_id_found; + for (auto id : ids_to_merge) { + accumulators.merge_to(id, min_island_id_found); + } + + CentroidAccumulator &acc = accumulators.access(current.island_id); + acc.accumulated_value += current.volume + * this->get_cell_center(this->to_global_cell_coords(Vec3i(x, y, z))).cast(); + acc.accumulated_volume += current.volume; + modified_acc_ids.insert(min_island_id_found); + } } } } @@ -342,8 +359,17 @@ struct BalanceDistributionGrid { // image here: https://hgphysics.com/gph/c-forces/2-force-effects/1-moment/stability/ // better image in Czech here in the first question: https://www.priklady.eu/cs/fyzika/mechanika-tuheho-telesa/stabilita-teles.alej for (int acc_id : modified_acc_ids) { + + std::cout << "controlling acc id: " << acc_id << std::endl; + CentroidAccumulator &acc = accumulators.access(acc_id); Vec3d centroid = acc.accumulated_value / acc.accumulated_volume; + + std::cout << "acc.accumulated_value : " << acc.accumulated_value.x() << " " + << acc.accumulated_value.y() << " " << acc.accumulated_value.z() << std::endl; + std::cout << "acc.accumulated_volume : " << acc.accumulated_volume << std::endl; + std::cout << "centorid: " << centroid.x() << " " << centroid.y() << " " << centroid.z() << std::endl; + //determine signed shortest distance to the convex hull Point centroid_base_projection = Point(centroid.head<2>().cast()); Point pivot; @@ -367,36 +393,58 @@ struct BalanceDistributionGrid { } } + std::cout << "centoroid inside ? " << inside << " and distance is: " << distance_sq << std::endl; + bool additional_supports_needed = false; - double base_area = std::max(acc.convex_hull.area(), scale_(5.0)); // assume 5 mm area for support points and other degenerate bases - double sticking_force = base_area * (acc.base_height == 0 ? params.base_adhesion : params.support_adhesion); + double base_area = std::max(acc.convex_hull.area(), scale_(5.0)); // assume 5 mm^2 area for support points and other degenerate bases + double sticking_force = base_area + * (acc.base_height == 0 ? scale_(params.base_adhesion) : scale_(params.support_adhesion)); + + std::cout << "stcking force: " << sticking_force << std::endl; + if (inside) { - double toppling_force = (Vec2d(sqrt(distance_sq), acc.base_height).norm() - acc.base_height) * acc.accumulated_volume; + double toppling_force = (Vec2d(sqrt(distance_sq), centroid.z() - acc.base_height).norm() + - centroid.z()) + * acc.accumulated_volume; sticking_force += toppling_force; + std::cout << "toppling force: " << toppling_force << std::endl; } - double y_movement_force = 0.5f * acc.accumulated_volume * params.top_object_movement_speed - * params.top_object_movement_speed; + + double y_movement_force = 0.5f * acc.accumulated_volume * scale_(params.top_object_movement_speed) + * scale_(params.top_object_movement_speed); + + std::cout << "y_movement_force: " << y_movement_force << std::endl; + if (sticking_force < y_movement_force) { additional_supports_needed = true; } if (!inside) { - double torque = sqrt(distance_sq) * acc.accumulated_volume; + double torque = sqrt(distance_sq) * scale_(scale_(scale_(acc.accumulated_volume))); // if sticking force is computed with scaled adhesion, than torque needs scaled volume as well + + std::cout << "torque: " << torque << std::endl; + if (torque > sticking_force) { //comparing apples and oranges; but we are far beyond physical simulation additional_supports_needed = true; } } + std::cout << "additional supports needed: " << additional_supports_needed << std::endl; + if (additional_supports_needed) { - Vec2crd attractor_dir = inside ? pivot - centroid_base_projection : centroid_base_projection - pivot; - Vec2d attractor = centroid_base_projection.cast() + (1e9 * attractor_dir.cast().normalized()); + Vec2crd attractor_dir = + inside ? pivot - centroid_base_projection : centroid_base_projection - pivot; + Vec2d attractor = centroid_base_projection.cast() + + (1e9 * attractor_dir.cast().normalized()); double min_dist = std::numeric_limits::max(); Vec3d support_point = centroid; for (int y = 0; y < global_cell_count.y(); ++y) { for (int x = 0; x < global_cell_count.x(); ++x) { Cell &cell = this->access_cell(Vec3i(x, y, 0)); if (cell.island_id == acc_id) { - Vec3d cell_center = this->get_cell_center(this->to_global_cell_coords(Vec3i(x, y, z))).cast(); + Vec3d cell_center = + this->get_cell_center(this->to_global_cell_coords(Vec3i(x, y, z))).cast< + double>(); double dist_sq = (cell_center.head<2>() - attractor).squaredNorm(); if (dist_sq < min_dist) { min_dist = dist_sq; @@ -406,7 +454,7 @@ struct BalanceDistributionGrid { } } - issues.supports_nedded.emplace_back(support_point.cast()); + issues.supports_nedded.emplace_back(unscale(support_point).cast()); acc.points.push_back(Point(support_point.head<2>().cast())); acc.calculate_base_hull(); } @@ -416,41 +464,65 @@ struct BalanceDistributionGrid { } #ifdef DEBUG_FILES - void debug_export(std::string file_name) const { + void debug_export() const { Slic3r::CNumericLocalesSetter locales_setter; { - FILE *fp = boost::nowide::fopen(debug_out_path((file_name + "_grid.obj").c_str()).c_str(), "w"); - if (fp == nullptr) { + FILE *volume_grid_file = boost::nowide::fopen(debug_out_path("volume_grid.obj").c_str(), "w"); + FILE *islands_grid_file = boost::nowide::fopen(debug_out_path("islands_grid.obj").c_str(), "w"); + FILE *curling_grid_file = boost::nowide::fopen(debug_out_path("curling_grid.obj").c_str(), "w"); + + if (volume_grid_file == nullptr || islands_grid_file == nullptr || curling_grid_file == nullptr) { BOOST_LOG_TRIVIAL(error) - << "Debug files: Couldn't open " << file_name << " for writing"; + << "Debug files: Couldn't open debug file for writing, destination: " << debug_out_path(""); return; } float max_volume = 0; + int min_island_id = 0; + int max_island_id = 0; + float max_curling_height = 0; + for (int x = 0; x < global_cell_count.x(); ++x) { for (int y = 0; y < global_cell_count.y(); ++y) { for (int z = 0; z < local_z_cell_count; ++z) { const Cell &cell = access_cell(Vec3i(x, y, z)); max_volume = std::max(max_volume, cell.volume); + if (cell.island_id != std::numeric_limits::max()) { + min_island_id = std::min(min_island_id, cell.island_id); + max_island_id = std::max(max_island_id, cell.island_id); + } + max_curling_height = std::max(max_curling_height, cell.curled_height); } } } - max_volume *= 0.8; - for (int x = 0; x < global_cell_count.x(); ++x) { for (int y = 0; y < global_cell_count.y(); ++y) { for (int z = 0; z < local_z_cell_count; ++z) { Vec3f center = unscale(get_cell_center(to_global_cell_coords(Vec3i { x, y, z }))).cast(); const Cell &cell = access_cell(Vec3i(x, y, z)); if (cell.volume != 0) { - fprintf(fp, "v %f %f %f %f %f %f\n", center(0), center(1), center(2), cell.volume / max_volume, 0.0, 0.0); + auto volume_color = value_to_rgbf(0, cell.volume, cell.volume); + fprintf(volume_grid_file, "v %f %f %f %f %f %f\n", center(0), center(1), center(2), + volume_color.x(), volume_color.y(), volume_color.z()); + } + if (cell.island_id != std::numeric_limits::max()) { + auto island_color = value_to_rgbf(min_island_id, max_island_id + 1, cell.island_id); + fprintf(islands_grid_file, "v %f %f %f %f %f %f\n", center(0), center(1), center(2), + island_color.x(), island_color.y(), island_color.z()); + } + if (cell.curled_height > 0) { + auto curling_color = value_to_rgbf(0, max_curling_height, cell.curled_height); + fprintf(curling_grid_file, "v %f %f %f %f %f %f\n", center(0), center(1), center(2), + curling_color.x(), curling_color.y(), curling_color.z()); } } } } - fclose(fp); + fclose(volume_grid_file); + fclose(islands_grid_file); + fclose(curling_grid_file); } } #endif @@ -485,7 +557,8 @@ void debug_export(Issues issues, std::string file_name) { } for (size_t i = 0; i < issues.supports_nedded.size(); ++i) { - fprintf(fp, "v %f %f %f %f %f %f\n", issues.supports_nedded[i].position(0), issues.supports_nedded[i].position(1), + fprintf(fp, "v %f %f %f %f %f %f\n", issues.supports_nedded[i].position(0), + issues.supports_nedded[i].position(1), issues.supports_nedded[i].position(2), 1.0, 0.0, 0.0); } @@ -500,7 +573,8 @@ void debug_export(Issues issues, std::string file_name) { } for (size_t i = 0; i < issues.curling_up.size(); ++i) { - fprintf(fp, "v %f %f %f %f %f %f\n", issues.curling_up[i].position(0), issues.curling_up[i].position(1), + fprintf(fp, "v %f %f %f %f %f %f\n", issues.curling_up[i].position(0), + issues.curling_up[i].position(1), issues.curling_up[i].position(2), 0.0, 1.0, 0.0); } fclose(fp); @@ -512,7 +586,8 @@ void debug_export(Issues issues, std::string file_name) { EdgeGridWrapper compute_layer_edge_grid(const Layer *layer) { float min_region_flow_width { 1.0f }; for (const auto *region : layer->regions()) { - min_region_flow_width = std::min(min_region_flow_width, region->flow(FlowRole::frExternalPerimeter).width()); + min_region_flow_width = std::min(min_region_flow_width, + region->flow(FlowRole::frExternalPerimeter).width()); } std::vector lines; for (const LayerRegion *layer_region : layer->regions()) { @@ -533,23 +608,29 @@ EdgeGridWrapper compute_layer_edge_grid(const Layer *layer) { //TODO needs revision coordf_t get_flow_width(const LayerRegion *region, ExtrusionRole role) { switch (role) { - case ExtrusionRole::erBridgeInfill: - return region->flow(FlowRole::frExternalPerimeter).scaled_width(); - case ExtrusionRole::erExternalPerimeter: - return region->flow(FlowRole::frExternalPerimeter).scaled_width(); - case ExtrusionRole::erGapFill: - return region->flow(FlowRole::frInfill).scaled_width(); - case ExtrusionRole::erPerimeter: - return region->flow(FlowRole::frPerimeter).scaled_width(); - case ExtrusionRole::erSolidInfill: - return region->flow(FlowRole::frSolidInfill).scaled_width(); - default: - return region->flow(FlowRole::frPerimeter).scaled_width(); + case ExtrusionRole::erBridgeInfill: + return region->flow(FlowRole::frExternalPerimeter).scaled_width(); + case ExtrusionRole::erExternalPerimeter: + return region->flow(FlowRole::frExternalPerimeter).scaled_width(); + case ExtrusionRole::erGapFill: + return region->flow(FlowRole::frInfill).scaled_width(); + case ExtrusionRole::erPerimeter: + return region->flow(FlowRole::frPerimeter).scaled_width(); + case ExtrusionRole::erSolidInfill: + return region->flow(FlowRole::frSolidInfill).scaled_width(); + case ExtrusionRole::erInternalInfill: + return region->flow(FlowRole::frInfill).scaled_width(); + case ExtrusionRole::erTopSolidInfill: + return region->flow(FlowRole::frTopSolidInfill).scaled_width(); + default: + return region->flow(FlowRole::frPerimeter).scaled_width(); } } -coordf_t get_max_allowed_distance(ExtrusionRole role, coordf_t flow_width, bool external_perimeters_first, const Params ¶ms) { // <= distance / flow_width (can be larger for perimeter, if not external perimeter first) - if ((role == ExtrusionRole::erExternalPerimeter || role == ExtrusionRole::erOverhangPerimeter) && (external_perimeters_first)) { +coordf_t get_max_allowed_distance(ExtrusionRole role, coordf_t flow_width, bool external_perimeters_first, + const Params ¶ms) { // <= distance / flow_width (can be larger for perimeter, if not external perimeter first) + if ((role == ExtrusionRole::erExternalPerimeter || role == ExtrusionRole::erOverhangPerimeter) + && (external_perimeters_first)) { return params.max_first_ex_perim_unsupported_distance_factor * flow_width; } else { return params.max_unsupported_distance_factor * flow_width; @@ -579,12 +660,13 @@ struct SegmentAccumulator { }; Issues check_extrusion_entity_stability(const ExtrusionEntity *entity, float print_z, const LayerRegion *layer_region, - const EdgeGridWrapper &supported_grid, BalanceDistributionGrid &balance_grid, const Params ¶ms) { + const EdgeGridWrapper &supported_grid, const Params ¶ms) { Issues issues { }; if (entity->is_collection()) { for (const auto *e : static_cast(entity)->entities) { - issues.add(check_extrusion_entity_stability(e, print_z, layer_region, supported_grid, balance_grid, params)); + issues.add( + check_extrusion_entity_stability(e, print_z, layer_region, supported_grid, params)); } } else { //single extrusion path, with possible varying parameters //prepare stack of points on the extrusion path. If there are long segments, additional points might be pushed onto the stack during the algorithm. @@ -606,10 +688,9 @@ Issues check_extrusion_entity_stability(const ExtrusionEntity *entity, float pri Point prev_point = points.top(); // prev point of the path. Initialize with first point. Vec3f prev_fpoint = to_vec3f(prev_point); coordf_t flow_width = get_flow_width(layer_region, entity->role()); - coordf_t layer_height = layer_region->layer()->height; bool external_perimters_first = layer_region->region().config().external_perimeters_first; - const coordf_t max_allowed_dist_from_prev_layer = get_max_allowed_distance(entity->role(), flow_width, external_perimters_first, - params); + const coordf_t max_allowed_dist_from_prev_layer = get_max_allowed_distance(entity->role(), flow_width, + external_perimters_first, params); while (!points.empty()) { Point point = points.top(); @@ -617,8 +698,6 @@ Issues check_extrusion_entity_stability(const ExtrusionEntity *entity, float pri Vec3f fpoint = to_vec3f(point); float edge_len = (fpoint - prev_fpoint).norm(); - balance_grid.distribute_edge(prev_point, point, print_z, unscale(flow_width), unscale(layer_height)); - coordf_t dist_from_prev_layer { 0 }; if (!supported_grid.signed_distance(point, flow_width, dist_from_prev_layer)) { // dist from prev layer not found, assume empty layer issues.supports_nedded.push_back(SupportPoint(fpoint)); @@ -641,8 +720,10 @@ Issues check_extrusion_entity_stability(const ExtrusionEntity *entity, float pri supports_acc.add_distance(edge_len); // for algorithm simplicity, expect that the whole line between prev and current point is unsupported if (supports_acc.distance // if unsupported distance is larger than bridge distance linearly decreased by curvature, enforce supports. - > params.bridge_distance - / (1.0f + (supports_acc.max_curvature * params.bridge_distance_decrease_by_curvature_factor / PI))) { + > params.bridge_distance + / (1.0f + + (supports_acc.max_curvature + * params.bridge_distance_decrease_by_curvature_factor / PI))) { issues.supports_nedded.push_back(SupportPoint(fpoint)); supports_acc.reset(); } @@ -681,32 +762,49 @@ Issues check_extrusion_entity_stability(const ExtrusionEntity *entity, float pri return issues; } -Issues check_layer_stability(const PrintObject *po, size_t layer_idx, bool full_check, BalanceDistributionGrid &balance_grid, - const Params ¶ms) { - std::cout << "Checking: " << layer_idx << std::endl; - if (layer_idx == 0) { - // first layer is usually ok - return {}; +void distribute_layer_volume(const PrintObject *po, size_t layer_idx, BalanceDistributionGrid &balance_grid) { + const Layer *layer = po->get_layer(layer_idx); + for (const LayerRegion *region : layer->regions()) { + for (const ExtrusionEntity *collections : region->fills.entities) { + for (const ExtrusionEntity *entity : static_cast(collections)->entities) { + for (const Line &line : entity->as_polyline().lines()) { + balance_grid.distribute_edge(line.a, line.b, layer->print_z, + unscale(get_flow_width(region, entity->role())), layer->height); + } + } + } + for (const ExtrusionEntity *collections : region->perimeters.entities) { + for (const ExtrusionEntity *entity : static_cast(collections)->entities) { + for (const Line &line : entity->as_polyline().lines()) { + balance_grid.distribute_edge(line.a, line.b, layer->print_z, + unscale(get_flow_width(region, entity->role())), layer->height); + } + } + } } +} + +Issues check_layer_stability(const PrintObject *po, size_t layer_idx, bool full_check, const Params ¶ms) { const Layer *layer = po->get_layer(layer_idx); //Prepare edge grid of previous layer, will be used to check if the extrusion path is supported EdgeGridWrapper supported_grid = compute_layer_edge_grid(layer->lower_layer); Issues issues { }; - if (full_check) { // If full checkm check stability of perimeters, gap fills, and bridges. + if (full_check) { // If full check; check stability of perimeters, gap fills, and bridges. for (const LayerRegion *layer_region : layer->regions()) { for (const ExtrusionEntity *ex_entity : layer_region->perimeters.entities) { for (const ExtrusionEntity *perimeter : static_cast(ex_entity)->entities) { issues.add( - check_extrusion_entity_stability(perimeter, layer->print_z, layer_region, supported_grid, balance_grid, - params)); + check_extrusion_entity_stability(perimeter, layer->print_z, layer_region, + supported_grid, params)); } // perimeter } // ex_entity for (const ExtrusionEntity *ex_entity : layer_region->fills.entities) { for (const ExtrusionEntity *fill : static_cast(ex_entity)->entities) { if (fill->role() == ExtrusionRole::erGapFill || fill->role() == ExtrusionRole::erBridgeInfill) { issues.add( - check_extrusion_entity_stability(fill, layer->print_z, layer_region, supported_grid, balance_grid, params)); + check_extrusion_entity_stability(fill, layer->print_z, layer_region, supported_grid, + params)); } } // fill } // ex_entity @@ -719,8 +817,8 @@ Issues check_layer_stability(const PrintObject *po, size_t layer_idx, bool full_ if (perimeter->role() == ExtrusionRole::erExternalPerimeter || perimeter->role() == ExtrusionRole::erOverhangPerimeter) { issues.add( - check_extrusion_entity_stability(perimeter, layer->print_z, layer_region, supported_grid, balance_grid, - params)); + check_extrusion_entity_stability(perimeter, layer->print_z, layer_region, + supported_grid, params)); }; // ex_perimeter } // perimeter } // ex_entity @@ -737,6 +835,7 @@ std::vector quick_search(const PrintObject *po, const Params ¶ms) { BalanceDistributionGrid grid { }; grid.init(po, 0, po->layers().size()); + distribute_layer_volume(po, 0, grid); std::mutex grid_mutex; size_t layer_count = po->layer_count(); @@ -746,7 +845,8 @@ std::vector quick_search(const PrintObject *po, const Params ¶ms) { balance_grid.init(po, r.begin(), r.end()); for (size_t layer_idx = r.begin(); layer_idx < r.end(); ++layer_idx) { - auto layer_issues = check_layer_stability(po, layer_idx, false, balance_grid, params); + distribute_layer_volume(po, layer_idx, balance_grid); + auto layer_issues = check_layer_stability(po, layer_idx, false, params); if (!layer_issues.supports_nedded.empty()) { layer_needs_supports[layer_idx] = true; } @@ -771,6 +871,7 @@ Issues full_search(const PrintObject *po, const Params ¶ms) { BalanceDistributionGrid grid { }; grid.init(po, 0, po->layers().size()); + distribute_layer_volume(po, 0, grid); std::mutex grid_mutex; size_t layer_count = po->layer_count(); @@ -780,7 +881,8 @@ Issues full_search(const PrintObject *po, const Params ¶ms) { balance_grid.init(po, r.begin(), r.end()); Issues issues = init; for (size_t layer_idx = r.begin(); layer_idx < r.end(); ++layer_idx) { - auto layer_issues = check_layer_stability(po, layer_idx, true, balance_grid, params); + distribute_layer_volume(po, layer_idx, balance_grid); + auto layer_issues = check_layer_stability(po, layer_idx, true, params); if (!layer_issues.empty()) { issues.add(layer_issues); } @@ -800,9 +902,8 @@ Issues full_search(const PrintObject *po, const Params ¶ms) { grid.analyze(found_issues, params); - grid.debug_export("volume"); - #ifdef DEBUG_FILES + grid.debug_export(); Impl::debug_export(found_issues, "issues"); #endif From 6f6a0e7efdebba4beb9323ac300f50d19d07f599 Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Fri, 29 Apr 2022 17:19:26 +0200 Subject: [PATCH 015/100] Another bulk of bug fixes. Some problems however persist, support points are still placed on weird spots --- src/libslic3r/SupportableIssuesSearch.cpp | 133 +++++++++++----------- src/libslic3r/SupportableIssuesSearch.hpp | 7 +- 2 files changed, 74 insertions(+), 66 deletions(-) diff --git a/src/libslic3r/SupportableIssuesSearch.cpp b/src/libslic3r/SupportableIssuesSearch.cpp index f2612035d9..72ee7a1176 100644 --- a/src/libslic3r/SupportableIssuesSearch.cpp +++ b/src/libslic3r/SupportableIssuesSearch.cpp @@ -58,11 +58,12 @@ struct Cell { struct CentroidAccumulator { Polygon convex_hull { }; Points points { }; - Vec3d accumulated_value = Vec3d::Zero(); + Vec3f accumulated_value = Vec3f::Zero(); float accumulated_volume { }; - const double base_height { }; + float base_area { }; + const float base_height { }; - explicit CentroidAccumulator(double base_height) : + explicit CentroidAccumulator(float base_height) : base_height(base_height) { } @@ -80,7 +81,7 @@ struct CentroidAccumulators { acccumulators.reserve(reserve_count); } - CentroidAccumulator& create_accumulator(int id, double base_height) { + CentroidAccumulator& create_accumulator(int id, float base_height) { mapping[id] = acccumulators.size(); acccumulators.push_back(CentroidAccumulator { base_height }); return this->access(id); @@ -100,6 +101,7 @@ struct CentroidAccumulators { to_acc.accumulated_volume += from_acc.accumulated_volume; to_acc.points.insert(to_acc.points.end(), from_acc.points.begin(), from_acc.points.end()); to_acc.calculate_base_hull(); + to_acc.base_area += from_acc.base_area; mapping[from_id] = mapping[to_id]; } }; @@ -265,7 +267,7 @@ struct BalanceDistributionGrid { Vec3crd cell_center = this->get_cell_center( Vec3i(current_coords.x(), current_coords.y(), local_z_index_offset)); acc.points.push_back(Point(cell_center.head<2>())); - acc.accumulated_value += cell_center.cast() * cell.volume; + acc.accumulated_value += unscale(cell_center).cast() * cell.volume; acc.accumulated_volume += cell.volume; for (int y_offset = -1; y_offset <= 1; ++y_offset) { @@ -279,6 +281,7 @@ struct BalanceDistributionGrid { } next_island_id--; acc.calculate_base_hull(); + acc.base_area = unscale(unscale(acc.convex_hull.area())); //apply unscale 2x, it has units of area } } } @@ -295,6 +298,7 @@ struct BalanceDistributionGrid { issues.supports_nedded[index].position.z()); acc.points.push_back(Point(scaled(Vec2f(issues.supports_nedded[index].position.head<2>())))); acc.calculate_base_hull(); + acc.base_area = params.support_points_inteface_area; } for (const CurledFilament &curling : issues.curling_up) { @@ -343,9 +347,10 @@ struct BalanceDistributionGrid { accumulators.merge_to(id, min_island_id_found); } - CentroidAccumulator &acc = accumulators.access(current.island_id); + CentroidAccumulator &acc = accumulators.access(min_island_id_found); acc.accumulated_value += current.volume - * this->get_cell_center(this->to_global_cell_coords(Vec3i(x, y, z))).cast(); + * unscale(this->get_cell_center(this->to_global_cell_coords(Vec3i(x, y, z)))).cast< + float>(); acc.accumulated_volume += current.volume; modified_acc_ids.insert(min_island_id_found); } @@ -353,109 +358,107 @@ struct BalanceDistributionGrid { } } - // check stability of modified centroid accumulators. - // Stability is the amount of work needed to push the object from stable position into unstable. - // This amount of work is proportional to the increase of height of the centroid during toppling. - // image here: https://hgphysics.com/gph/c-forces/2-force-effects/1-moment/stability/ - // better image in Czech here in the first question: https://www.priklady.eu/cs/fyzika/mechanika-tuheho-telesa/stabilita-teles.alej for (int acc_id : modified_acc_ids) { - std::cout << "controlling acc id: " << acc_id << std::endl; + std::cout << "Z: " << z << " controlling acc id: " << acc_id << std::endl; CentroidAccumulator &acc = accumulators.access(acc_id); - Vec3d centroid = acc.accumulated_value / acc.accumulated_volume; + Vec3f centroid = acc.accumulated_value / acc.accumulated_volume; std::cout << "acc.accumulated_value : " << acc.accumulated_value.x() << " " << acc.accumulated_value.y() << " " << acc.accumulated_value.z() << std::endl; std::cout << "acc.accumulated_volume : " << acc.accumulated_volume << std::endl; - std::cout << "centorid: " << centroid.x() << " " << centroid.y() << " " << centroid.z() << std::endl; + std::cout << "centroid: " << centroid.x() << " " << centroid.y() << " " << centroid.z() << std::endl; //determine signed shortest distance to the convex hull - Point centroid_base_projection = Point(centroid.head<2>().cast()); + Point centroid_base_projection = Point(scaled(Vec2f(centroid.head<2>()))); + Point pivot; - double distance_sq = std::numeric_limits::max(); + double distance_scaled_sq = std::numeric_limits::max(); bool inside = true; if (acc.convex_hull.points.size() == 1) { pivot = acc.convex_hull.points[0]; - distance_sq = (pivot - centroid_base_projection).squaredNorm(); + distance_scaled_sq = (pivot - centroid_base_projection).squaredNorm(); inside = true; } else { for (Line line : acc.convex_hull.lines()) { Point closest_point; double dist_sq = line.distance_to_squared(centroid_base_projection, &closest_point); - if (dist_sq < distance_sq) { + if (dist_sq < distance_scaled_sq) { pivot = closest_point; - distance_sq = dist_sq; + distance_scaled_sq = dist_sq; } - if (float(angle(line.b - line.a, centroid_base_projection - line.b)) < 0) { + if ((centroid_base_projection - closest_point).cast().dot(line.normal().cast()) + > 0) { inside = false; } } } - std::cout << "centoroid inside ? " << inside << " and distance is: " << distance_sq << std::endl; + float centroid_pivot_distance = unscaled(sqrt(distance_scaled_sq)); + float base_center_pivot_distance = float(unscale(Vec2crd(acc.convex_hull.centroid() - pivot)).norm()); + + std::cout << "centroid inside ? " << inside << " and distance is: " << centroid_pivot_distance + << std::endl; bool additional_supports_needed = false; - double base_area = std::max(acc.convex_hull.area(), scale_(5.0)); // assume 5 mm^2 area for support points and other degenerate bases - double sticking_force = base_area - * (acc.base_height == 0 ? scale_(params.base_adhesion) : scale_(params.support_adhesion)); + float sticking_force = acc.base_area + * (acc.base_height == 0 ? params.base_adhesion : params.support_adhesion); + float sticking_torque = base_center_pivot_distance * sticking_force; - std::cout << "stcking force: " << sticking_force << std::endl; + std::cout << "sticking force: " << sticking_force << " sticking torque: " << sticking_torque + << std::endl; - if (inside) { - double toppling_force = (Vec2d(sqrt(distance_sq), centroid.z() - acc.base_height).norm() - - centroid.z()) - * acc.accumulated_volume; - sticking_force += toppling_force; - std::cout << "toppling force: " << toppling_force << std::endl; - } + float xy_movement_force = acc.accumulated_volume * params.filament_density * params.max_acceleration; + float xy_movement_torque = xy_movement_force * centroid_pivot_distance; - double y_movement_force = 0.5f * acc.accumulated_volume * scale_(params.top_object_movement_speed) - * scale_(params.top_object_movement_speed); - - std::cout << "y_movement_force: " << y_movement_force << std::endl; - - if (sticking_force < y_movement_force) { - additional_supports_needed = true; - } + std::cout << "xy_movement_force: " << xy_movement_force << " xy_movement_torque: " + << xy_movement_torque << std::endl; + float weight_torque = 0; if (!inside) { - double torque = sqrt(distance_sq) * scale_(scale_(scale_(acc.accumulated_volume))); // if sticking force is computed with scaled adhesion, than torque needs scaled volume as well + float weight = acc.accumulated_volume * params.filament_density * 10000.0f; + weight_torque = centroid_pivot_distance * weight; - std::cout << "torque: " << torque << std::endl; - - if (torque > sticking_force) { //comparing apples and oranges; but we are far beyond physical simulation - additional_supports_needed = true; - } + std::cout << "weight: " << weight << " weight_torque: " << weight_torque << std::endl; } + float total_momentum = sticking_torque - xy_movement_torque - weight_torque; + additional_supports_needed = total_momentum < 0; + + std::cout << "total_momentum: " << total_momentum << std::endl; std::cout << "additional supports needed: " << additional_supports_needed << std::endl; if (additional_supports_needed) { - Vec2crd attractor_dir = - inside ? pivot - centroid_base_projection : centroid_base_projection - pivot; - Vec2d attractor = centroid_base_projection.cast() - + (1e9 * attractor_dir.cast().normalized()); + Vec2f attractor_dir = + unscale(Vec2crd(inside ? + pivot - centroid_base_projection : + centroid_base_projection - pivot)).cast().normalized(); + Vec2f attractor = unscale(centroid_base_projection).cast() + 10000 * attractor_dir; + + std::cout << " attractor: " << attractor.x() << " | " << attractor.y() << std::endl; + double min_dist = std::numeric_limits::max(); - Vec3d support_point = centroid; + Vec3f support_point = centroid; for (int y = 0; y < global_cell_count.y(); ++y) { for (int x = 0; x < global_cell_count.x(); ++x) { - Cell &cell = this->access_cell(Vec3i(x, y, 0)); - if (cell.island_id == acc_id) { - Vec3d cell_center = - this->get_cell_center(this->to_global_cell_coords(Vec3i(x, y, z))).cast< - double>(); - double dist_sq = (cell_center.head<2>() - attractor).squaredNorm(); + Cell &cell = this->access_cell(Vec3i(x, y, z)); + if (&accumulators.access(cell.island_id) == &acc) { + Vec3f cell_center = + unscale(this->get_cell_center(this->to_global_cell_coords(Vec3i(x, y, z)))).cast< + float>(); + float dist_sq = (cell_center.head<2>() - attractor).squaredNorm(); if (dist_sq < min_dist) { min_dist = dist_sq; support_point = cell_center; + std::cout << " new support point: " << support_point.x() << " | " << support_point.y() << " | " << support_point.z() << std::endl; } } } } - issues.supports_nedded.emplace_back(unscale(support_point).cast()); - acc.points.push_back(Point(support_point.head<2>().cast())); + issues.supports_nedded.emplace_back(support_point); + acc.points.push_back(Point::new_scale(Vec2f(support_point.head<2>()))); acc.calculate_base_hull(); } @@ -559,7 +562,7 @@ void debug_export(Issues issues, std::string file_name) { for (size_t i = 0; i < issues.supports_nedded.size(); ++i) { fprintf(fp, "v %f %f %f %f %f %f\n", issues.supports_nedded[i].position(0), issues.supports_nedded[i].position(1), - issues.supports_nedded[i].position(2), 1.0, 0.0, 0.0); + issues.supports_nedded[i].position(2), 1.0, 0.0, 1.0); } fclose(fp); @@ -721,9 +724,8 @@ Issues check_extrusion_entity_stability(const ExtrusionEntity *entity, float pri if (supports_acc.distance // if unsupported distance is larger than bridge distance linearly decreased by curvature, enforce supports. > params.bridge_distance - / (1.0f - + (supports_acc.max_curvature - * params.bridge_distance_decrease_by_curvature_factor / PI))) { + / (1.0f + (supports_acc.max_curvature + * params.bridge_distance_decrease_by_curvature_factor / PI))) { issues.supports_nedded.push_back(SupportPoint(fpoint)); supports_acc.reset(); } @@ -899,6 +901,9 @@ Issues full_search(const PrintObject *po, const Params ¶ms) { return left; } ); +#ifdef DEBUG_FILES + Impl::debug_export(found_issues, "pre_issues"); +#endif grid.analyze(found_issues, params); diff --git a/src/libslic3r/SupportableIssuesSearch.hpp b/src/libslic3r/SupportableIssuesSearch.hpp index 867ccd21b5..f16d454e0f 100644 --- a/src/libslic3r/SupportableIssuesSearch.hpp +++ b/src/libslic3r/SupportableIssuesSearch.hpp @@ -15,9 +15,12 @@ struct Params { float max_unsupported_distance_factor = 1.0f; // For internal perimeters, infill, bridges etc, allow gap of [extrusion width] size, these extrusions have usually something to stick to. float bridge_distance_decrease_by_curvature_factor = 5.0f; // allowed bridge distance = bridge_distance / ( 1 + this factor * (curvature / PI) ) - float base_adhesion = 60.0f; // adhesion per mm^2 of first layer; the value should say how much *volume* it can hold per one square millimiter + float base_adhesion = 100.0f; // adhesion per mm^2 of first layer; Force needed to remove the object from the bed, divided by the adhesion area, so in Pascal units N/mm^2 float support_adhesion = 300.0f; // adhesion per mm^2 of support interface layer - float top_object_movement_speed = 200.0f; // movement speed of 200 mm/s in Y + float support_points_inteface_area = 5.0f; // mm^2 + float max_acceleration = 1000.0f; // mm/s^2 ; max acceleration in XY + float filament_density = 1.25f * 0.001f; // g/mm^3 + }; struct SupportPoint { From 91a4047586f0eef30c59c388f2c5b4d86e9b0273 Mon Sep 17 00:00:00 2001 From: Godrak Date: Mon, 2 May 2022 14:48:07 +0200 Subject: [PATCH 016/100] Fixed various problems with support placement. --- src/libslic3r/PrintObject.cpp | 2 +- src/libslic3r/SupportableIssuesSearch.cpp | 53 ++++++++++++++--------- src/libslic3r/SupportableIssuesSearch.hpp | 10 +++-- 3 files changed, 40 insertions(+), 25 deletions(-) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index f8fcc22039..ee311b5610 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -431,7 +431,7 @@ void PrintObject::find_supportable_issues() TriangleSelectorWrapper selector { model_volume->mesh() }; for (const SupportableIssues::SupportPoint &support_point : issues.supports_nedded) { - selector.enforce_spot(Vec3f(inv_transform.cast() * support_point.position), 0.3f); + selector.enforce_spot(Vec3f(inv_transform.cast() * support_point.position), 2.0f); } model_volume->supported_facets.set(selector.selector); diff --git a/src/libslic3r/SupportableIssuesSearch.cpp b/src/libslic3r/SupportableIssuesSearch.cpp index 72ee7a1176..6adf615e80 100644 --- a/src/libslic3r/SupportableIssuesSearch.cpp +++ b/src/libslic3r/SupportableIssuesSearch.cpp @@ -37,10 +37,6 @@ SupportPoint::SupportPoint(const Vec3f &position, float weight) : position(position), weight(weight) { } -SupportPoint::SupportPoint(const Vec3f &position) : - position(position), weight(0.0f) { -} - CurledFilament::CurledFilament(const Vec3f &position, float estimated_height) : position(position), estimated_height(estimated_height) { } @@ -298,7 +294,7 @@ struct BalanceDistributionGrid { issues.supports_nedded[index].position.z()); acc.points.push_back(Point(scaled(Vec2f(issues.supports_nedded[index].position.head<2>())))); acc.calculate_base_hull(); - acc.base_area = params.support_points_inteface_area; + acc.base_area = params.support_points_interface_area; } for (const CurledFilament &curling : issues.curling_up) { @@ -372,7 +368,6 @@ struct BalanceDistributionGrid { //determine signed shortest distance to the convex hull Point centroid_base_projection = Point(scaled(Vec2f(centroid.head<2>()))); - Point pivot; double distance_scaled_sq = std::numeric_limits::max(); bool inside = true; @@ -395,10 +390,13 @@ struct BalanceDistributionGrid { } } - float centroid_pivot_distance = unscaled(sqrt(distance_scaled_sq)); + Vec3f pivot_3d; + pivot_3d << unscale(pivot).cast(), acc.base_height; + float embedded_distance = unscaled(sqrt(distance_scaled_sq)); + float centroid_pivot_distance = (centroid - pivot_3d).norm(); float base_center_pivot_distance = float(unscale(Vec2crd(acc.convex_hull.centroid() - pivot)).norm()); - std::cout << "centroid inside ? " << inside << " and distance is: " << centroid_pivot_distance + std::cout << "centroid inside ? " << inside << " and embedded distance is: " << embedded_distance << std::endl; bool additional_supports_needed = false; @@ -415,15 +413,17 @@ struct BalanceDistributionGrid { std::cout << "xy_movement_force: " << xy_movement_force << " xy_movement_torque: " << xy_movement_torque << std::endl; - float weight_torque = 0; - if (!inside) { - float weight = acc.accumulated_volume * params.filament_density * 10000.0f; - weight_torque = centroid_pivot_distance * weight; - - std::cout << "weight: " << weight << " weight_torque: " << weight_torque << std::endl; + float weight = acc.accumulated_volume * params.filament_density * params.gravity_constant; + float weight_torque = embedded_distance * weight; + if (!inside){ + weight_torque*=-1; } + std::cout << "weight: " << weight << " weight_torque: " << weight_torque << std::endl; - float total_momentum = sticking_torque - xy_movement_torque - weight_torque; + float extruder_conflict_torque = params.tolerable_extruder_conflict_force * 2.0f * centroid_pivot_distance; + std::cout << "extruder_conflict_torque: " << extruder_conflict_torque << std::endl; + + float total_momentum = sticking_torque + weight_torque - xy_movement_torque - extruder_conflict_torque; additional_supports_needed = total_momentum < 0; std::cout << "total_momentum: " << total_momentum << std::endl; @@ -440,10 +440,12 @@ struct BalanceDistributionGrid { double min_dist = std::numeric_limits::max(); Vec3f support_point = centroid; + Vec2i coords = Vec2i(0,0); for (int y = 0; y < global_cell_count.y(); ++y) { for (int x = 0; x < global_cell_count.x(); ++x) { Cell &cell = this->access_cell(Vec3i(x, y, z)); - if (&accumulators.access(cell.island_id) == &acc) { + if (cell.island_id != std::numeric_limits::max() && + &accumulators.access(cell.island_id) == &acc) { Vec3f cell_center = unscale(this->get_cell_center(this->to_global_cell_coords(Vec3i(x, y, z)))).cast< float>(); @@ -451,14 +453,25 @@ struct BalanceDistributionGrid { if (dist_sq < min_dist) { min_dist = dist_sq; support_point = cell_center; - std::cout << " new support point: " << support_point.x() << " | " << support_point.y() << " | " << support_point.z() << std::endl; + coords = Vec2i(x,y); } } } } - issues.supports_nedded.emplace_back(support_point); + int final_height_coords = z; + while (final_height_coords > 0 && this->access_cell(Vec3i(coords.x(), coords.y(), final_height_coords)).volume > 0){ + final_height_coords --; + } + support_point.z() = unscaled((final_height_coords + this->local_z_index_offset) * this->cell_size.z()); + float expected_force = total_momentum / (support_point - pivot_3d).norm(); + + std::cout << " new support point: " << support_point.x() << " | " << support_point.y() << " | " << support_point.z() << std::endl; + std::cout << " expected_force: " << expected_force << std::endl; + + issues.supports_nedded.emplace_back(support_point, expected_force); acc.points.push_back(Point::new_scale(Vec2f(support_point.head<2>()))); + acc.base_area += params.support_points_interface_area; acc.calculate_base_hull(); } @@ -703,7 +716,7 @@ Issues check_extrusion_entity_stability(const ExtrusionEntity *entity, float pri coordf_t dist_from_prev_layer { 0 }; if (!supported_grid.signed_distance(point, flow_width, dist_from_prev_layer)) { // dist from prev layer not found, assume empty layer - issues.supports_nedded.push_back(SupportPoint(fpoint)); + issues.supports_nedded.push_back(SupportPoint(fpoint, 1.0f)); supports_acc.reset(); } @@ -726,7 +739,7 @@ Issues check_extrusion_entity_stability(const ExtrusionEntity *entity, float pri > params.bridge_distance / (1.0f + (supports_acc.max_curvature * params.bridge_distance_decrease_by_curvature_factor / PI))) { - issues.supports_nedded.push_back(SupportPoint(fpoint)); + issues.supports_nedded.push_back(SupportPoint(fpoint, 1.0f)); supports_acc.reset(); } } else { diff --git a/src/libslic3r/SupportableIssuesSearch.hpp b/src/libslic3r/SupportableIssuesSearch.hpp index f16d454e0f..a34d1577f3 100644 --- a/src/libslic3r/SupportableIssuesSearch.hpp +++ b/src/libslic3r/SupportableIssuesSearch.hpp @@ -8,6 +8,8 @@ namespace Slic3r { namespace SupportableIssues { struct Params { + const float gravity_constant = 9806.65f; // mm/s^2 ; gravity acceleration on Earth's surface, and assuming printer is in upwards position. + float bridge_distance = 10.0f; float limit_curvature = 0.15f; // used to detect curling issues @@ -15,17 +17,17 @@ struct Params { float max_unsupported_distance_factor = 1.0f; // For internal perimeters, infill, bridges etc, allow gap of [extrusion width] size, these extrusions have usually something to stick to. float bridge_distance_decrease_by_curvature_factor = 5.0f; // allowed bridge distance = bridge_distance / ( 1 + this factor * (curvature / PI) ) - float base_adhesion = 100.0f; // adhesion per mm^2 of first layer; Force needed to remove the object from the bed, divided by the adhesion area, so in Pascal units N/mm^2 - float support_adhesion = 300.0f; // adhesion per mm^2 of support interface layer - float support_points_inteface_area = 5.0f; // mm^2 + float base_adhesion = 2000.0f; // adhesion per mm^2 of first layer; Force needed to remove the object from the bed, divided by the adhesion area (g/mm*s^2) + float support_adhesion = 1000.0f; // adhesion per mm^2 of support interface layer + float support_points_interface_area = 5.0f; // mm^2 float max_acceleration = 1000.0f; // mm/s^2 ; max acceleration in XY float filament_density = 1.25f * 0.001f; // g/mm^3 + float tolerable_extruder_conflict_force = 50.0f * gravity_constant; // force that can occasionally push the model due to various factors (filament leaks, small curling, ... ); current value corresponds to weight of 200g }; struct SupportPoint { SupportPoint(const Vec3f &position, float weight); - explicit SupportPoint(const Vec3f &position); Vec3f position; float weight; }; From 9b290bd21184391f09ddefdaaad05ee06fc6ac68 Mon Sep 17 00:00:00 2001 From: Godrak Date: Mon, 2 May 2022 16:47:19 +0200 Subject: [PATCH 017/100] debug info, problem with random freezing, also support point downward projection still has issues --- src/libslic3r/SupportableIssuesSearch.cpp | 13 ++++++++++++- src/libslic3r/SupportableIssuesSearch.hpp | 9 +++++---- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/libslic3r/SupportableIssuesSearch.cpp b/src/libslic3r/SupportableIssuesSearch.cpp index 6adf615e80..21dbb0e1fb 100644 --- a/src/libslic3r/SupportableIssuesSearch.cpp +++ b/src/libslic3r/SupportableIssuesSearch.cpp @@ -304,15 +304,20 @@ struct BalanceDistributionGrid { std::unordered_set modified_acc_ids; modified_acc_ids.reserve(issues.supports_nedded.size() + 1); for (int z = 1; z < local_z_cell_count; ++z) { + std::cout << "current z: " << z << std::endl; + modified_acc_ids.clear(); for (int x = 0; x < global_cell_count.x(); ++x) { for (int y = 0; y < global_cell_count.y(); ++y) { Cell ¤t = this->access_cell(Vec3i(x, y, z)); + + // distribute curling + std::cout << "distribute curling " << std::endl; if (current.volume > 0) { for (int y_offset = -1; y_offset <= 1; ++y_offset) { for (int x_offset = -1; x_offset <= 1; ++x_offset) { - if (validate_xy_coords(Vec2i(x_offset, y_offset))) { + if (validate_xy_coords(Vec2i(x+x_offset,y+ y_offset))) { Cell &under = this->access_cell(Vec3i(x + x_offset, y + y_offset, z - 1)); current.curled_height += under.curled_height / (2 + std::abs(x_offset) + std::abs(y_offset)); @@ -321,6 +326,8 @@ struct BalanceDistributionGrid { } } + // distribute islands info + std::cout << "distribute islands info " << std::endl; if (current.volume > 0 && current.island_id == std::numeric_limits::max()) { int min_island_id_found = std::numeric_limits::max(); std::unordered_set ids_to_merge { }; @@ -335,6 +342,8 @@ struct BalanceDistributionGrid { } } } + // assign island and update its info + std::cout << "assign island and update its info " << std::endl; if (min_island_id_found < std::numeric_limits::max()) { ids_to_merge.erase(std::numeric_limits::max()); ids_to_merge.erase(min_island_id_found); @@ -354,6 +363,8 @@ struct BalanceDistributionGrid { } } + std::cout << " check all active accumulators " << std::endl; + for (int acc_id : modified_acc_ids) { std::cout << "Z: " << z << " controlling acc id: " << acc_id << std::endl; diff --git a/src/libslic3r/SupportableIssuesSearch.hpp b/src/libslic3r/SupportableIssuesSearch.hpp index a34d1577f3..cdaa55ed5f 100644 --- a/src/libslic3r/SupportableIssuesSearch.hpp +++ b/src/libslic3r/SupportableIssuesSearch.hpp @@ -8,9 +8,9 @@ namespace Slic3r { namespace SupportableIssues { struct Params { - const float gravity_constant = 9806.65f; // mm/s^2 ; gravity acceleration on Earth's surface, and assuming printer is in upwards position. + const float gravity_constant = 9806.65f; // mm/s^2; gravity acceleration on Earth's surface, algorithm assumes that printer is in upwards position. - float bridge_distance = 10.0f; + float bridge_distance = 10.0f; //mm float limit_curvature = 0.15f; // used to detect curling issues float max_first_ex_perim_unsupported_distance_factor = 0.0f; // if external perim first, return tighter max allowed distance from previous layer extrusion @@ -20,10 +20,11 @@ struct Params { float base_adhesion = 2000.0f; // adhesion per mm^2 of first layer; Force needed to remove the object from the bed, divided by the adhesion area (g/mm*s^2) float support_adhesion = 1000.0f; // adhesion per mm^2 of support interface layer float support_points_interface_area = 5.0f; // mm^2 - float max_acceleration = 1000.0f; // mm/s^2 ; max acceleration in XY - float filament_density = 1.25f * 0.001f; // g/mm^3 + float max_acceleration = 1000.0f; // mm/s^2 ; max acceleration of object (bed) in XY + float filament_density = 1.25f * 0.001f; // g/mm^3 ; Common filaments are very lightweight, so precise number is not that important float tolerable_extruder_conflict_force = 50.0f * gravity_constant; // force that can occasionally push the model due to various factors (filament leaks, small curling, ... ); current value corresponds to weight of 200g + }; struct SupportPoint { From 49e6d15a67459cb089fc2209b27a6ad67881564c Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Tue, 3 May 2022 17:26:41 +0200 Subject: [PATCH 018/100] vastly improved curling detection, 3d histogram of curled height now corresponds with real prints --- src/libslic3r/SupportableIssuesSearch.cpp | 71 ++++++++++++----------- src/libslic3r/SupportableIssuesSearch.hpp | 3 +- 2 files changed, 39 insertions(+), 35 deletions(-) diff --git a/src/libslic3r/SupportableIssuesSearch.cpp b/src/libslic3r/SupportableIssuesSearch.cpp index 21dbb0e1fb..4496e9a25f 100644 --- a/src/libslic3r/SupportableIssuesSearch.cpp +++ b/src/libslic3r/SupportableIssuesSearch.cpp @@ -57,6 +57,7 @@ struct CentroidAccumulator { Vec3f accumulated_value = Vec3f::Zero(); float accumulated_volume { }; float base_area { }; + float additional_supports_adhesion { }; const float base_height { }; explicit CentroidAccumulator(float base_height) : @@ -304,7 +305,7 @@ struct BalanceDistributionGrid { std::unordered_set modified_acc_ids; modified_acc_ids.reserve(issues.supports_nedded.size() + 1); for (int z = 1; z < local_z_cell_count; ++z) { - std::cout << "current z: " << z << std::endl; + std::cout << "current z: " << z << std::endl; modified_acc_ids.clear(); @@ -313,21 +314,24 @@ struct BalanceDistributionGrid { Cell ¤t = this->access_cell(Vec3i(x, y, z)); // distribute curling - std::cout << "distribute curling " << std::endl; if (current.volume > 0) { - for (int y_offset = -1; y_offset <= 1; ++y_offset) { - for (int x_offset = -1; x_offset <= 1; ++x_offset) { - if (validate_xy_coords(Vec2i(x+x_offset,y+ y_offset))) { + float curled_height = 0; + for (int y_offset = -2; y_offset <= 2; ++y_offset) { + for (int x_offset = -2; x_offset <= 2; ++x_offset) { + if (validate_xy_coords(Vec2i(x + x_offset, y + y_offset))) { Cell &under = this->access_cell(Vec3i(x + x_offset, y + y_offset, z - 1)); - current.curled_height += under.curled_height - / (2 + std::abs(x_offset) + std::abs(y_offset)); + curled_height = std::max(curled_height, under.curled_height); } } } + bool curled = current.curled_height > 0; + current.curled_height += std::max(0.0f, float(curled_height - unscaled(this->cell_size.z()))); + if (!curled) { + current.curled_height /= 4.0f; + } } // distribute islands info - std::cout << "distribute islands info " << std::endl; if (current.volume > 0 && current.island_id == std::numeric_limits::max()) { int min_island_id_found = std::numeric_limits::max(); std::unordered_set ids_to_merge { }; @@ -343,7 +347,6 @@ struct BalanceDistributionGrid { } } // assign island and update its info - std::cout << "assign island and update its info " << std::endl; if (min_island_id_found < std::numeric_limits::max()) { ids_to_merge.erase(std::numeric_limits::max()); ids_to_merge.erase(min_island_id_found); @@ -426,12 +429,13 @@ struct BalanceDistributionGrid { float weight = acc.accumulated_volume * params.filament_density * params.gravity_constant; float weight_torque = embedded_distance * weight; - if (!inside){ - weight_torque*=-1; + if (!inside) { + weight_torque *= -1; } std::cout << "weight: " << weight << " weight_torque: " << weight_torque << std::endl; - float extruder_conflict_torque = params.tolerable_extruder_conflict_force * 2.0f * centroid_pivot_distance; + float extruder_conflict_torque = params.tolerable_extruder_conflict_force * 2.0f + * centroid_pivot_distance; std::cout << "extruder_conflict_torque: " << extruder_conflict_torque << std::endl; float total_momentum = sticking_torque + weight_torque - xy_movement_torque - extruder_conflict_torque; @@ -451,7 +455,7 @@ struct BalanceDistributionGrid { double min_dist = std::numeric_limits::max(); Vec3f support_point = centroid; - Vec2i coords = Vec2i(0,0); + Vec2i coords = Vec2i(0, 0); for (int y = 0; y < global_cell_count.y(); ++y) { for (int x = 0; x < global_cell_count.x(); ++x) { Cell &cell = this->access_cell(Vec3i(x, y, z)); @@ -464,20 +468,23 @@ struct BalanceDistributionGrid { if (dist_sq < min_dist) { min_dist = dist_sq; support_point = cell_center; - coords = Vec2i(x,y); + coords = Vec2i(x, y); } } } } int final_height_coords = z; - while (final_height_coords > 0 && this->access_cell(Vec3i(coords.x(), coords.y(), final_height_coords)).volume > 0){ - final_height_coords --; + while (final_height_coords > 0 + && this->access_cell(Vec3i(coords.x(), coords.y(), final_height_coords)).volume > 0) { + final_height_coords--; } - support_point.z() = unscaled((final_height_coords + this->local_z_index_offset) * this->cell_size.z()); - float expected_force = total_momentum / (support_point - pivot_3d).norm(); + support_point.z() = unscaled( + (final_height_coords + this->local_z_index_offset) * this->cell_size.z()); + float expected_force = total_momentum / (support_point - pivot_3d).norm(); - std::cout << " new support point: " << support_point.x() << " | " << support_point.y() << " | " << support_point.z() << std::endl; + std::cout << " new support point: " << support_point.x() << " | " << support_point.y() << " | " + << support_point.z() << std::endl; std::cout << " expected_force: " << expected_force << std::endl; issues.supports_nedded.emplace_back(support_point, expected_force); @@ -705,12 +712,12 @@ Issues check_extrusion_entity_stability(const ExtrusionEntity *entity, float pri SegmentAccumulator supports_acc { }; supports_acc.add_distance(params.bridge_distance + 1.0f); // initialize unsupported distance with larger than tolerable distance -> // -> it prevents extruding perimeter start and short loops into air. - SegmentAccumulator curling_acc { }; const auto to_vec3f = [print_z](const Point &point) { Vec2f tmp = unscale(point).cast(); return Vec3f(tmp.x(), tmp.y(), print_z); }; + float region_height = layer_region->layer()->height; Point prev_point = points.top(); // prev point of the path. Initialize with first point. Vec3f prev_fpoint = to_vec3f(prev_point); @@ -741,7 +748,6 @@ Issues check_extrusion_entity_stability(const ExtrusionEntity *entity, float pri } supports_acc.add_angle(angle); - curling_acc.add_angle(angle); if (dist_from_prev_layer > max_allowed_dist_from_prev_layer) { //extrusion point is unsupported supports_acc.add_distance(edge_len); // for algorithm simplicity, expect that the whole line between prev and current point is unsupported @@ -758,14 +764,9 @@ Issues check_extrusion_entity_stability(const ExtrusionEntity *entity, float pri } // Estimation of short curvy segments which are not supported -> problems with curling - if (dist_from_prev_layer > 0.0f) { //extrusion point is unsupported or poorly supported - curling_acc.add_distance(edge_len); - if (curling_acc.max_curvature / (PI * curling_acc.distance) > params.limit_curvature) { - issues.curling_up.push_back(CurledFilament(fpoint, layer_region->layer()->height)); - curling_acc.reset(); - } - } else { - curling_acc.reset(); + if (dist_from_prev_layer > -max_allowed_dist_from_prev_layer * 0.7071) { //extrusion point is unsupported or poorly supported + issues.curling_up.push_back( + CurledFilament(fpoint, 2.0f * region_height + region_height * 6.0f * std::abs(angle) / PI)); } prev_point = point; @@ -788,7 +789,8 @@ Issues check_extrusion_entity_stability(const ExtrusionEntity *entity, float pri return issues; } -void distribute_layer_volume(const PrintObject *po, size_t layer_idx, BalanceDistributionGrid &balance_grid) { +void distribute_layer_volume(const PrintObject *po, size_t layer_idx, + BalanceDistributionGrid &balance_grid) { const Layer *layer = po->get_layer(layer_idx); for (const LayerRegion *region : layer->regions()) { for (const ExtrusionEntity *collections : region->fills.entities) { @@ -810,7 +812,8 @@ void distribute_layer_volume(const PrintObject *po, size_t layer_idx, BalanceDis } } -Issues check_layer_stability(const PrintObject *po, size_t layer_idx, bool full_check, const Params ¶ms) { +Issues check_layer_stability(const PrintObject *po, size_t layer_idx, bool full_check, + const Params ¶ms) { const Layer *layer = po->get_layer(layer_idx); //Prepare edge grid of previous layer, will be used to check if the extrusion path is supported EdgeGridWrapper supported_grid = compute_layer_edge_grid(layer->lower_layer); @@ -827,9 +830,11 @@ Issues check_layer_stability(const PrintObject *po, size_t layer_idx, bool full_ } // ex_entity for (const ExtrusionEntity *ex_entity : layer_region->fills.entities) { for (const ExtrusionEntity *fill : static_cast(ex_entity)->entities) { - if (fill->role() == ExtrusionRole::erGapFill || fill->role() == ExtrusionRole::erBridgeInfill) { + if (fill->role() == ExtrusionRole::erGapFill + || fill->role() == ExtrusionRole::erBridgeInfill) { issues.add( - check_extrusion_entity_stability(fill, layer->print_z, layer_region, supported_grid, + check_extrusion_entity_stability(fill, layer->print_z, layer_region, + supported_grid, params)); } } // fill diff --git a/src/libslic3r/SupportableIssuesSearch.hpp b/src/libslic3r/SupportableIssuesSearch.hpp index cdaa55ed5f..6586a97d5b 100644 --- a/src/libslic3r/SupportableIssuesSearch.hpp +++ b/src/libslic3r/SupportableIssuesSearch.hpp @@ -10,9 +10,8 @@ namespace SupportableIssues { struct Params { const float gravity_constant = 9806.65f; // mm/s^2; gravity acceleration on Earth's surface, algorithm assumes that printer is in upwards position. - float bridge_distance = 10.0f; //mm - float limit_curvature = 0.15f; // used to detect curling issues + float bridge_distance = 10.0f; //mm float max_first_ex_perim_unsupported_distance_factor = 0.0f; // if external perim first, return tighter max allowed distance from previous layer extrusion float max_unsupported_distance_factor = 1.0f; // For internal perimeters, infill, bridges etc, allow gap of [extrusion width] size, these extrusions have usually something to stick to. float bridge_distance_decrease_by_curvature_factor = 5.0f; // allowed bridge distance = bridge_distance / ( 1 + this factor * (curvature / PI) ) From 68243edc65f21fd0ea7990d48cf33771221a8daa Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Thu, 5 May 2022 17:27:07 +0200 Subject: [PATCH 019/100] vastly improved computational time by optimizing the convex hull computations --- src/libslic3r/SupportableIssuesSearch.cpp | 54 ++++++++++++++--------- src/libslic3r/SupportableIssuesSearch.hpp | 3 +- 2 files changed, 35 insertions(+), 22 deletions(-) diff --git a/src/libslic3r/SupportableIssuesSearch.cpp b/src/libslic3r/SupportableIssuesSearch.cpp index 4496e9a25f..d9e78b79bb 100644 --- a/src/libslic3r/SupportableIssuesSearch.cpp +++ b/src/libslic3r/SupportableIssuesSearch.cpp @@ -51,23 +51,37 @@ struct Cell { int island_id = std::numeric_limits::max(); }; -struct CentroidAccumulator { +class CentroidAccumulator { +private: Polygon convex_hull { }; Points points { }; +public: Vec3f accumulated_value = Vec3f::Zero(); float accumulated_volume { }; float base_area { }; float additional_supports_adhesion { }; - const float base_height { }; + float base_height { }; explicit CentroidAccumulator(float base_height) : base_height(base_height) { } - void calculate_base_hull() { - convex_hull = Geometry::convex_hull(points); - assert(convex_hull.is_counter_clockwise()); + const Polygon& base_hull(){ + if (this->convex_hull.empty()) { + this->convex_hull = Geometry::convex_hull(this->points); + } + return this->convex_hull; } + + void add_base_points(const Points& other) { + this->points.insert(this->points.end(), other.begin(), other.end()); + convex_hull.clear(); + } + + const Points& get_base_points(){ + return this->points; + } + }; struct CentroidAccumulators { @@ -96,10 +110,11 @@ struct CentroidAccumulators { } to_acc.accumulated_value += from_acc.accumulated_value; to_acc.accumulated_volume += from_acc.accumulated_volume; - to_acc.points.insert(to_acc.points.end(), from_acc.points.begin(), from_acc.points.end()); - to_acc.calculate_base_hull(); + to_acc.add_base_points(from_acc.get_base_points()); to_acc.base_area += from_acc.base_area; mapping[from_id] = mapping[to_id]; + from_acc = CentroidAccumulator{0.0f}; + } }; @@ -263,7 +278,7 @@ struct BalanceDistributionGrid { cell.island_id = next_island_id; Vec3crd cell_center = this->get_cell_center( Vec3i(current_coords.x(), current_coords.y(), local_z_index_offset)); - acc.points.push_back(Point(cell_center.head<2>())); + acc.add_base_points({Point(cell_center.head<2>())}); acc.accumulated_value += unscale(cell_center).cast() * cell.volume; acc.accumulated_volume += cell.volume; @@ -277,8 +292,7 @@ struct BalanceDistributionGrid { } } next_island_id--; - acc.calculate_base_hull(); - acc.base_area = unscale(unscale(acc.convex_hull.area())); //apply unscale 2x, it has units of area + acc.base_area = unscale(unscale(acc.base_hull().area())); //apply unscale 2x, it has units of area } } } @@ -293,8 +307,7 @@ struct BalanceDistributionGrid { this->access_cell(local_coords).island_id = index; CentroidAccumulator &acc = accumulators.create_accumulator(index, issues.supports_nedded[index].position.z()); - acc.points.push_back(Point(scaled(Vec2f(issues.supports_nedded[index].position.head<2>())))); - acc.calculate_base_hull(); + acc.add_base_points({Point(scaled(Vec2f(issues.supports_nedded[index].position.head<2>())))}); acc.base_area = params.support_points_interface_area; } @@ -329,8 +342,9 @@ struct BalanceDistributionGrid { if (!curled) { current.curled_height /= 4.0f; } - } + std::cout << "Curling: " << current.curled_height << std::endl; + } // distribute islands info if (current.volume > 0 && current.island_id == std::numeric_limits::max()) { int min_island_id_found = std::numeric_limits::max(); @@ -354,7 +368,6 @@ struct BalanceDistributionGrid { for (auto id : ids_to_merge) { accumulators.merge_to(id, min_island_id_found); } - CentroidAccumulator &acc = accumulators.access(min_island_id_found); acc.accumulated_value += current.volume * unscale(this->get_cell_center(this->to_global_cell_coords(Vec3i(x, y, z)))).cast< @@ -385,12 +398,12 @@ struct BalanceDistributionGrid { Point pivot; double distance_scaled_sq = std::numeric_limits::max(); bool inside = true; - if (acc.convex_hull.points.size() == 1) { - pivot = acc.convex_hull.points[0]; + if (acc.base_hull().points.size() == 1) { + pivot = acc.base_hull().points[0]; distance_scaled_sq = (pivot - centroid_base_projection).squaredNorm(); inside = true; } else { - for (Line line : acc.convex_hull.lines()) { + for (Line line : acc.base_hull().lines()) { Point closest_point; double dist_sq = line.distance_to_squared(centroid_base_projection, &closest_point); if (dist_sq < distance_scaled_sq) { @@ -408,7 +421,7 @@ struct BalanceDistributionGrid { pivot_3d << unscale(pivot).cast(), acc.base_height; float embedded_distance = unscaled(sqrt(distance_scaled_sq)); float centroid_pivot_distance = (centroid - pivot_3d).norm(); - float base_center_pivot_distance = float(unscale(Vec2crd(acc.convex_hull.centroid() - pivot)).norm()); + float base_center_pivot_distance = float(unscale(Vec2crd(acc.base_hull().centroid() - pivot)).norm()); std::cout << "centroid inside ? " << inside << " and embedded distance is: " << embedded_distance << std::endl; @@ -488,9 +501,8 @@ struct BalanceDistributionGrid { std::cout << " expected_force: " << expected_force << std::endl; issues.supports_nedded.emplace_back(support_point, expected_force); - acc.points.push_back(Point::new_scale(Vec2f(support_point.head<2>()))); + acc.add_base_points({Point::new_scale(Vec2f(support_point.head<2>()))}); acc.base_area += params.support_points_interface_area; - acc.calculate_base_hull(); } } @@ -766,7 +778,7 @@ Issues check_extrusion_entity_stability(const ExtrusionEntity *entity, float pri // Estimation of short curvy segments which are not supported -> problems with curling if (dist_from_prev_layer > -max_allowed_dist_from_prev_layer * 0.7071) { //extrusion point is unsupported or poorly supported issues.curling_up.push_back( - CurledFilament(fpoint, 2.0f * region_height + region_height * 6.0f * std::abs(angle) / PI)); + CurledFilament(fpoint, 0.2f * region_height + region_height * 0.6f * std::abs(angle) / PI)); } prev_point = point; diff --git a/src/libslic3r/SupportableIssuesSearch.hpp b/src/libslic3r/SupportableIssuesSearch.hpp index 6586a97d5b..1e90e8cde0 100644 --- a/src/libslic3r/SupportableIssuesSearch.hpp +++ b/src/libslic3r/SupportableIssuesSearch.hpp @@ -22,7 +22,8 @@ struct Params { float max_acceleration = 1000.0f; // mm/s^2 ; max acceleration of object (bed) in XY float filament_density = 1.25f * 0.001f; // g/mm^3 ; Common filaments are very lightweight, so precise number is not that important - float tolerable_extruder_conflict_force = 50.0f * gravity_constant; // force that can occasionally push the model due to various factors (filament leaks, small curling, ... ); current value corresponds to weight of 200g + float tolerable_extruder_conflict_force = 50.0f * gravity_constant; // force that can occasionally push the model due to various factors (filament leaks, small curling, ... ); current value corresponds to weight of 50g + float max_curled_conflict_extruder_force = 200.0f * gravity_constant; // for areas with possible high layered curled filaments, max force to account fo ; current value corresponds to weight of 200g }; From 609f42fb18a59e56ad0454cf9a9d3320caffcc04 Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Fri, 6 May 2022 13:30:40 +0200 Subject: [PATCH 020/100] refactoring, pressure points extracted but not accounted for --- src/libslic3r/SupportableIssuesSearch.cpp | 313 ++++++++++++---------- 1 file changed, 166 insertions(+), 147 deletions(-) diff --git a/src/libslic3r/SupportableIssuesSearch.cpp b/src/libslic3r/SupportableIssuesSearch.cpp index d9e78b79bb..a097d220f5 100644 --- a/src/libslic3r/SupportableIssuesSearch.cpp +++ b/src/libslic3r/SupportableIssuesSearch.cpp @@ -55,7 +55,7 @@ class CentroidAccumulator { private: Polygon convex_hull { }; Points points { }; -public: + public: Vec3f accumulated_value = Vec3f::Zero(); float accumulated_volume { }; float base_area { }; @@ -66,19 +66,19 @@ public: base_height(base_height) { } - const Polygon& base_hull(){ + const Polygon& base_hull() { if (this->convex_hull.empty()) { this->convex_hull = Geometry::convex_hull(this->points); } return this->convex_hull; } - void add_base_points(const Points& other) { + void add_base_points(const Points &other) { this->points.insert(this->points.end(), other.begin(), other.end()); convex_hull.clear(); } - const Points& get_base_points(){ + const Points& get_base_points() { return this->points; } @@ -113,12 +113,27 @@ struct CentroidAccumulators { to_acc.add_base_points(from_acc.get_base_points()); to_acc.base_area += from_acc.base_area; mapping[from_id] = mapping[to_id]; - from_acc = CentroidAccumulator{0.0f}; + from_acc = CentroidAccumulator { 0.0f }; } }; -struct BalanceDistributionGrid { +class BalanceDistributionGrid { + + static constexpr float cell_height = scale_(0.3f); + + Vec3crd cell_size { }; + + Vec3crd global_origin { }; + Vec3crd global_size { }; + Vec3i global_cell_count { }; + + int local_z_index_offset { }; + int local_z_cell_count { }; + std::vector cells { }; + +public: + BalanceDistributionGrid() = default; void init(const PrintObject *po, size_t layer_idx_begin, size_t layer_idx_end) { @@ -278,7 +293,7 @@ struct BalanceDistributionGrid { cell.island_id = next_island_id; Vec3crd cell_center = this->get_cell_center( Vec3i(current_coords.x(), current_coords.y(), local_z_index_offset)); - acc.add_base_points({Point(cell_center.head<2>())}); + acc.add_base_points( { Point(cell_center.head<2>()) }); acc.accumulated_value += unscale(cell_center).cast() * cell.volume; acc.accumulated_volume += cell.volume; @@ -307,7 +322,7 @@ struct BalanceDistributionGrid { this->access_cell(local_coords).island_id = index; CentroidAccumulator &acc = accumulators.create_accumulator(index, issues.supports_nedded[index].position.z()); - acc.add_base_points({Point(scaled(Vec2f(issues.supports_nedded[index].position.head<2>())))}); + acc.add_base_points( { Point(scaled(Vec2f(issues.supports_nedded[index].position.head<2>()))) }); acc.base_area = params.support_points_interface_area; } @@ -315,8 +330,10 @@ struct BalanceDistributionGrid { this->access_cell(curling.position).curled_height += curling.estimated_height; } - std::unordered_set modified_acc_ids; + std::unordered_map> modified_acc_ids; + std::unordered_map> filtered_active_accumulators; modified_acc_ids.reserve(issues.supports_nedded.size() + 1); + for (int z = 1; z < local_z_cell_count; ++z) { std::cout << "current z: " << z << std::endl; @@ -325,26 +342,6 @@ struct BalanceDistributionGrid { for (int x = 0; x < global_cell_count.x(); ++x) { for (int y = 0; y < global_cell_count.y(); ++y) { Cell ¤t = this->access_cell(Vec3i(x, y, z)); - - // distribute curling - if (current.volume > 0) { - float curled_height = 0; - for (int y_offset = -2; y_offset <= 2; ++y_offset) { - for (int x_offset = -2; x_offset <= 2; ++x_offset) { - if (validate_xy_coords(Vec2i(x + x_offset, y + y_offset))) { - Cell &under = this->access_cell(Vec3i(x + x_offset, y + y_offset, z - 1)); - curled_height = std::max(curled_height, under.curled_height); - } - } - } - bool curled = current.curled_height > 0; - current.curled_height += std::max(0.0f, float(curled_height - unscaled(this->cell_size.z()))); - if (!curled) { - current.curled_height /= 4.0f; - } - - std::cout << "Curling: " << current.curled_height << std::endl; - } // distribute islands info if (current.volume > 0 && current.island_id == std::numeric_limits::max()) { int min_island_id_found = std::numeric_limits::max(); @@ -373,143 +370,176 @@ struct BalanceDistributionGrid { * unscale(this->get_cell_center(this->to_global_cell_coords(Vec3i(x, y, z)))).cast< float>(); acc.accumulated_volume += current.volume; - modified_acc_ids.insert(min_island_id_found); + modified_acc_ids.emplace(min_island_id_found, std::vector { }); + } + } + + // distribute curling + if (current.volume > 0) { + float curled_height = 0; + for (int y_offset = -2; y_offset <= 2; ++y_offset) { + for (int x_offset = -2; x_offset <= 2; ++x_offset) { + if (validate_xy_coords(Vec2i(x + x_offset, y + y_offset))) { + Cell &under = this->access_cell(Vec3i(x + x_offset, y + y_offset, z - 1)); + curled_height = std::max(curled_height, under.curled_height); + } + } + } + bool curled = current.curled_height > 0; + current.curled_height += curled_height; + if (!curled) { + current.curled_height -= unscaled(this->cell_size.z()); + } + std::cout << "Curling: " << current.curled_height << std::endl; + + if (current.curled_height / unscaled(this->cell_size.z()) > 0.3f) { + modified_acc_ids[current.island_id].push_back( { x, y }); } } } } - std::cout << " check all active accumulators " << std::endl; + filtered_active_accumulators.clear(); + for (const auto &pair : modified_acc_ids) { + CentroidAccumulator *acc = &accumulators.access(pair.first); + filtered_active_accumulators[acc].insert(filtered_active_accumulators[acc].end(), pair.second.begin(), + pair.second.end()); + } - for (int acc_id : modified_acc_ids) { + check_accumulators_stability(z, accumulators, filtered_active_accumulators, issues, params); + } + } +private: + void check_accumulators_stability(int z, CentroidAccumulators &accumulators, + std::unordered_map> filtered_active_accumulators, + Issues &issues, const Params ¶ms) { - std::cout << "Z: " << z << " controlling acc id: " << acc_id << std::endl; + for (const auto &pair : filtered_active_accumulators) { + std::cout << "Z: " << z << std::endl; + CentroidAccumulator &acc = *pair.first; + Vec3f centroid = acc.accumulated_value / acc.accumulated_volume; - CentroidAccumulator &acc = accumulators.access(acc_id); - Vec3f centroid = acc.accumulated_value / acc.accumulated_volume; + std::cout << "acc.accumulated_value : " << acc.accumulated_value.x() << " " + << acc.accumulated_value.y() << " " << acc.accumulated_value.z() << std::endl; + std::cout << "acc.accumulated_volume : " << acc.accumulated_volume << std::endl; + std::cout << "centroid: " << centroid.x() << " " << centroid.y() << " " << centroid.z() << std::endl; - std::cout << "acc.accumulated_value : " << acc.accumulated_value.x() << " " - << acc.accumulated_value.y() << " " << acc.accumulated_value.z() << std::endl; - std::cout << "acc.accumulated_volume : " << acc.accumulated_volume << std::endl; - std::cout << "centroid: " << centroid.x() << " " << centroid.y() << " " << centroid.z() << std::endl; - - //determine signed shortest distance to the convex hull - Point centroid_base_projection = Point(scaled(Vec2f(centroid.head<2>()))); - Point pivot; - double distance_scaled_sq = std::numeric_limits::max(); - bool inside = true; - if (acc.base_hull().points.size() == 1) { - pivot = acc.base_hull().points[0]; - distance_scaled_sq = (pivot - centroid_base_projection).squaredNorm(); - inside = true; - } else { - for (Line line : acc.base_hull().lines()) { - Point closest_point; - double dist_sq = line.distance_to_squared(centroid_base_projection, &closest_point); - if (dist_sq < distance_scaled_sq) { - pivot = closest_point; - distance_scaled_sq = dist_sq; - } - if ((centroid_base_projection - closest_point).cast().dot(line.normal().cast()) - > 0) { - inside = false; - } + //determine signed shortest distance to the convex hull + Point centroid_base_projection = Point(scaled(Vec2f(centroid.head<2>()))); + Point pivot; + double distance_scaled_sq = std::numeric_limits::max(); + bool inside = true; + if (acc.base_hull().points.size() == 1) { + pivot = acc.base_hull().points[0]; + distance_scaled_sq = (pivot - centroid_base_projection).squaredNorm(); + inside = distance_scaled_sq < params.support_points_interface_area; + } else { + for (Line line : acc.base_hull().lines()) { + Point closest_point; + double dist_sq = line.distance_to_squared(centroid_base_projection, &closest_point); + if (dist_sq < distance_scaled_sq) { + pivot = closest_point; + distance_scaled_sq = dist_sq; + } + if ((centroid_base_projection - closest_point).cast().dot(line.normal().cast()) + > 0) { + inside = false; } } + } - Vec3f pivot_3d; - pivot_3d << unscale(pivot).cast(), acc.base_height; - float embedded_distance = unscaled(sqrt(distance_scaled_sq)); - float centroid_pivot_distance = (centroid - pivot_3d).norm(); - float base_center_pivot_distance = float(unscale(Vec2crd(acc.base_hull().centroid() - pivot)).norm()); + Vec3f pivot_3d; + pivot_3d << unscale(pivot).cast(), acc.base_height; + float embedded_distance = unscaled(sqrt(distance_scaled_sq)); + float centroid_pivot_distance = (centroid - pivot_3d).norm(); + float base_center_pivot_distance = float(unscale(Vec2crd(acc.base_hull().centroid() - pivot)).norm()); - std::cout << "centroid inside ? " << inside << " and embedded distance is: " << embedded_distance - << std::endl; + std::cout << "centroid inside ? " << inside << " and embedded distance is: " << embedded_distance + << std::endl; - bool additional_supports_needed = false; - float sticking_force = acc.base_area - * (acc.base_height == 0 ? params.base_adhesion : params.support_adhesion); - float sticking_torque = base_center_pivot_distance * sticking_force; + bool additional_supports_needed = false; + float sticking_force = acc.base_area + * (acc.base_height == 0 ? params.base_adhesion : params.support_adhesion); + float sticking_torque = base_center_pivot_distance * sticking_force; - std::cout << "sticking force: " << sticking_force << " sticking torque: " << sticking_torque - << std::endl; + std::cout << "sticking force: " << sticking_force << " sticking torque: " << sticking_torque + << std::endl; - float xy_movement_force = acc.accumulated_volume * params.filament_density * params.max_acceleration; - float xy_movement_torque = xy_movement_force * centroid_pivot_distance; + float xy_movement_force = acc.accumulated_volume * params.filament_density * params.max_acceleration; + float xy_movement_torque = xy_movement_force * centroid_pivot_distance; - std::cout << "xy_movement_force: " << xy_movement_force << " xy_movement_torque: " - << xy_movement_torque << std::endl; + std::cout << "xy_movement_force: " << xy_movement_force << " xy_movement_torque: " + << xy_movement_torque << std::endl; - float weight = acc.accumulated_volume * params.filament_density * params.gravity_constant; - float weight_torque = embedded_distance * weight; - if (!inside) { - weight_torque *= -1; - } - std::cout << "weight: " << weight << " weight_torque: " << weight_torque << std::endl; + float weight = acc.accumulated_volume * params.filament_density * params.gravity_constant; + float weight_torque = embedded_distance * weight; + if (!inside) { + weight_torque *= -1; + } + std::cout << "weight: " << weight << " weight_torque: " << weight_torque << std::endl; - float extruder_conflict_torque = params.tolerable_extruder_conflict_force * 2.0f - * centroid_pivot_distance; - std::cout << "extruder_conflict_torque: " << extruder_conflict_torque << std::endl; + float extruder_conflict_torque = params.tolerable_extruder_conflict_force * 2.0f + * centroid_pivot_distance; + std::cout << "extruder_conflict_torque: " << extruder_conflict_torque << std::endl; - float total_momentum = sticking_torque + weight_torque - xy_movement_torque - extruder_conflict_torque; - additional_supports_needed = total_momentum < 0; + float total_momentum = sticking_torque + weight_torque - xy_movement_torque - extruder_conflict_torque; + additional_supports_needed = total_momentum < 0; - std::cout << "total_momentum: " << total_momentum << std::endl; - std::cout << "additional supports needed: " << additional_supports_needed << std::endl; + std::cout << "total_momentum: " << total_momentum << std::endl; + std::cout << "additional supports needed: " << additional_supports_needed << std::endl; - if (additional_supports_needed) { - Vec2f attractor_dir = - unscale(Vec2crd(inside ? - pivot - centroid_base_projection : - centroid_base_projection - pivot)).cast().normalized(); - Vec2f attractor = unscale(centroid_base_projection).cast() + 10000 * attractor_dir; + if (additional_supports_needed) { + Vec2f attractor_dir = + unscale(Vec2crd(inside ? + pivot - centroid_base_projection : + centroid_base_projection - pivot)).cast().normalized(); + Vec2f attractor = unscale(centroid_base_projection).cast() + 10000 * attractor_dir; - std::cout << " attractor: " << attractor.x() << " | " << attractor.y() << std::endl; + std::cout << " attractor: " << attractor.x() << " | " << attractor.y() << std::endl; - double min_dist = std::numeric_limits::max(); - Vec3f support_point = centroid; - Vec2i coords = Vec2i(0, 0); - for (int y = 0; y < global_cell_count.y(); ++y) { - for (int x = 0; x < global_cell_count.x(); ++x) { - Cell &cell = this->access_cell(Vec3i(x, y, z)); - if (cell.island_id != std::numeric_limits::max() && - &accumulators.access(cell.island_id) == &acc) { - Vec3f cell_center = - unscale(this->get_cell_center(this->to_global_cell_coords(Vec3i(x, y, z)))).cast< - float>(); - float dist_sq = (cell_center.head<2>() - attractor).squaredNorm(); - if (dist_sq < min_dist) { - min_dist = dist_sq; - support_point = cell_center; - coords = Vec2i(x, y); - } + double min_dist = std::numeric_limits::max(); + Vec3f support_point = centroid; + Vec2i coords = Vec2i(0, 0); + for (int y = 0; y < global_cell_count.y(); ++y) { + for (int x = 0; x < global_cell_count.x(); ++x) { + Cell &cell = this->access_cell(Vec3i(x, y, z)); + if (cell.island_id != std::numeric_limits::max() && + &accumulators.access(cell.island_id) == &acc) { + Vec3f cell_center = + unscale(this->get_cell_center(this->to_global_cell_coords(Vec3i(x, y, z)))).cast< + float>(); + float dist_sq = (cell_center.head<2>() - attractor).squaredNorm(); + if (dist_sq < min_dist) { + min_dist = dist_sq; + support_point = cell_center; + coords = Vec2i(x, y); } } } - - int final_height_coords = z; - while (final_height_coords > 0 - && this->access_cell(Vec3i(coords.x(), coords.y(), final_height_coords)).volume > 0) { - final_height_coords--; - } - support_point.z() = unscaled( - (final_height_coords + this->local_z_index_offset) * this->cell_size.z()); - float expected_force = total_momentum / (support_point - pivot_3d).norm(); - - std::cout << " new support point: " << support_point.x() << " | " << support_point.y() << " | " - << support_point.z() << std::endl; - std::cout << " expected_force: " << expected_force << std::endl; - - issues.supports_nedded.emplace_back(support_point, expected_force); - acc.add_base_points({Point::new_scale(Vec2f(support_point.head<2>()))}); - acc.base_area += params.support_points_interface_area; } + int final_height_coords = z; + while (final_height_coords > 0 + && this->access_cell(Vec3i(coords.x(), coords.y(), final_height_coords)).volume > 0) { + final_height_coords--; + } + support_point.z() = unscaled( + (final_height_coords + this->local_z_index_offset) * this->cell_size.z()); + float expected_force = total_momentum / (support_point - pivot_3d).norm(); + + std::cout << " new support point: " << support_point.x() << " | " << support_point.y() << " | " + << support_point.z() << std::endl; + std::cout << " expected_force: " << expected_force << std::endl; + + issues.supports_nedded.emplace_back(support_point, expected_force); + acc.add_base_points( { Point::new_scale(Vec2f(support_point.head<2>())) }); + acc.base_area += params.support_points_interface_area; } } } #ifdef DEBUG_FILES +public: void debug_export() const { Slic3r::CNumericLocalesSetter locales_setter; { @@ -572,19 +602,6 @@ struct BalanceDistributionGrid { } } #endif - - static constexpr float cell_height = scale_(0.3f); - - Vec3crd cell_size { }; - - Vec3crd global_origin { }; - Vec3crd global_size { }; - Vec3i global_cell_count { }; - - int local_z_index_offset { }; - int local_z_cell_count { }; - std::vector cells { }; - } ; @@ -777,8 +794,11 @@ Issues check_extrusion_entity_stability(const ExtrusionEntity *entity, float pri // Estimation of short curvy segments which are not supported -> problems with curling if (dist_from_prev_layer > -max_allowed_dist_from_prev_layer * 0.7071) { //extrusion point is unsupported or poorly supported + float dist_factor = (dist_from_prev_layer + max_allowed_dist_from_prev_layer * 0.7071) + / max_allowed_dist_from_prev_layer; issues.curling_up.push_back( - CurledFilament(fpoint, 0.2f * region_height + region_height * 0.6f * std::abs(angle) / PI)); + CurledFilament(fpoint, + dist_factor * (0.25f * region_height + region_height * 0.75f * std::abs(angle) / PI))); } prev_point = point; @@ -795,7 +815,6 @@ Issues check_extrusion_entity_stability(const ExtrusionEntity *entity, float pri points.push(Point::new_scale(Vec2f(next + reverse_v * (i * step_size)))); } } - } } return issues; From 4144b73ccdbc836e20b1dc6c762466809cf8523d Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Fri, 6 May 2022 17:01:26 +0200 Subject: [PATCH 021/100] curling estimation improvements --- src/libslic3r/SupportableIssuesSearch.cpp | 90 ++++++++++++++++++----- 1 file changed, 71 insertions(+), 19 deletions(-) diff --git a/src/libslic3r/SupportableIssuesSearch.cpp b/src/libslic3r/SupportableIssuesSearch.cpp index a097d220f5..1f28a5f074 100644 --- a/src/libslic3r/SupportableIssuesSearch.cpp +++ b/src/libslic3r/SupportableIssuesSearch.cpp @@ -81,7 +81,6 @@ private: const Points& get_base_points() { return this->points; } - }; struct CentroidAccumulators { @@ -385,11 +384,10 @@ public: } } } - bool curled = current.curled_height > 0; - current.curled_height += curled_height; - if (!curled) { - current.curled_height -= unscaled(this->cell_size.z()); - } + current.curled_height += std::max(0.0f, + float(curled_height - unscaled(this->cell_size.z()) / 2.0f)); + current.curled_height = std::min(current.curled_height, + float(unscaled(this->cell_size.z()) * 2.0f)); std::cout << "Curling: " << current.curled_height << std::endl; if (current.curled_height / unscaled(this->cell_size.z()) > 0.3f) { @@ -402,7 +400,8 @@ public: filtered_active_accumulators.clear(); for (const auto &pair : modified_acc_ids) { CentroidAccumulator *acc = &accumulators.access(pair.first); - filtered_active_accumulators[acc].insert(filtered_active_accumulators[acc].end(), pair.second.begin(), + filtered_active_accumulators[acc].insert(filtered_active_accumulators[acc].end(), + pair.second.begin(), pair.second.end()); } @@ -422,9 +421,9 @@ private: std::cout << "acc.accumulated_value : " << acc.accumulated_value.x() << " " << acc.accumulated_value.y() << " " << acc.accumulated_value.z() << std::endl; std::cout << "acc.accumulated_volume : " << acc.accumulated_volume << std::endl; - std::cout << "centroid: " << centroid.x() << " " << centroid.y() << " " << centroid.z() << std::endl; + std::cout << "centroid: " << centroid.x() << " " << centroid.y() << " " << centroid.z() + << std::endl; - //determine signed shortest distance to the convex hull Point centroid_base_projection = Point(scaled(Vec2f(centroid.head<2>()))); Point pivot; double distance_scaled_sq = std::numeric_limits::max(); @@ -441,7 +440,8 @@ private: pivot = closest_point; distance_scaled_sq = dist_sq; } - if ((centroid_base_projection - closest_point).cast().dot(line.normal().cast()) + if ((centroid_base_projection - closest_point).cast().dot( + line.normal().cast()) > 0) { inside = false; } @@ -452,7 +452,8 @@ private: pivot_3d << unscale(pivot).cast(), acc.base_height; float embedded_distance = unscaled(sqrt(distance_scaled_sq)); float centroid_pivot_distance = (centroid - pivot_3d).norm(); - float base_center_pivot_distance = float(unscale(Vec2crd(acc.base_hull().centroid() - pivot)).norm()); + float base_center_pivot_distance = float( + unscale(Vec2crd(acc.base_hull().centroid() - pivot)).norm()); std::cout << "centroid inside ? " << inside << " and embedded distance is: " << embedded_distance << std::endl; @@ -465,7 +466,8 @@ private: std::cout << "sticking force: " << sticking_force << " sticking torque: " << sticking_torque << std::endl; - float xy_movement_force = acc.accumulated_volume * params.filament_density * params.max_acceleration; + float xy_movement_force = acc.accumulated_volume * params.filament_density + * params.max_acceleration; float xy_movement_torque = xy_movement_force * centroid_pivot_distance; std::cout << "xy_movement_force: " << xy_movement_force << " xy_movement_torque: " @@ -482,7 +484,8 @@ private: * centroid_pivot_distance; std::cout << "extruder_conflict_torque: " << extruder_conflict_torque << std::endl; - float total_momentum = sticking_torque + weight_torque - xy_movement_torque - extruder_conflict_torque; + float total_momentum = sticking_torque + weight_torque - xy_movement_torque + - extruder_conflict_torque; additional_supports_needed = total_momentum < 0; std::cout << "total_momentum: " << total_momentum << std::endl; @@ -538,6 +541,50 @@ private: } } + float check_point_stability_under_pressure(CentroidAccumulator &acc, const Point &base_centroid, + const Vec3f &pressure_point, const Params ¶ms) { + Point pressure_base_projection = Point(scaled(Vec2f(pressure_point.head<2>()))); + Point pivot; + double distance_scaled_sq = std::numeric_limits::max(); + bool inside = true; + if (acc.base_hull().points.size() == 1) { + pivot = acc.base_hull().points[0]; + distance_scaled_sq = (pivot - pressure_base_projection).squaredNorm(); + inside = distance_scaled_sq < params.support_points_interface_area; + } else { + for (Line line : acc.base_hull().lines()) { + Point closest_point; + double dist_sq = line.distance_to_squared(pressure_base_projection, &closest_point); + if (dist_sq < distance_scaled_sq) { + pivot = closest_point; + distance_scaled_sq = dist_sq; + } + if ((pressure_base_projection - closest_point).cast().dot(line.normal().cast()) + > 0) { + inside = false; + } + } + } + float embedded_distance = unscaled(sqrt(distance_scaled_sq)); + float base_center_pivot_distance = float(unscale(Vec2crd(base_centroid - pivot)).norm()); + Vec3f pivot_3d; + pivot_3d << unscale(pivot).cast(), acc.base_height; + + float sticking_force = acc.base_area + * (acc.base_height == 0 ? params.base_adhesion : params.support_adhesion); + float sticking_torque = sticking_force * base_center_pivot_distance; + + float pressure_arm = inside ? pressure_point.z() - pivot_3d.z() : (pressure_point - pivot_3d).norm(); + float pressure_force = 1.0f; + float pressure_torque = pressure_arm * pressure_force; + + if (sticking_torque < pressure_torque) { + return pressure_force; + } else { + return 0.0f; + } + } + #ifdef DEBUG_FILES public: void debug_export() const { @@ -575,7 +622,8 @@ public: for (int x = 0; x < global_cell_count.x(); ++x) { for (int y = 0; y < global_cell_count.y(); ++y) { for (int z = 0; z < local_z_cell_count; ++z) { - Vec3f center = unscale(get_cell_center(to_global_cell_coords(Vec3i { x, y, z }))).cast(); + Vec3f center = unscale(get_cell_center(to_global_cell_coords(Vec3i { x, y, z }))).cast< + float>(); const Cell &cell = access_cell(Vec3i(x, y, z)); if (cell.volume != 0) { auto volume_color = value_to_rgbf(0, cell.volume, cell.volume); @@ -584,12 +632,14 @@ public: } if (cell.island_id != std::numeric_limits::max()) { auto island_color = value_to_rgbf(min_island_id, max_island_id + 1, cell.island_id); - fprintf(islands_grid_file, "v %f %f %f %f %f %f\n", center(0), center(1), center(2), + fprintf(islands_grid_file, "v %f %f %f %f %f %f\n", center(0), center(1), + center(2), island_color.x(), island_color.y(), island_color.z()); } if (cell.curled_height > 0) { auto curling_color = value_to_rgbf(0, max_curling_height, cell.curled_height); - fprintf(curling_grid_file, "v %f %f %f %f %f %f\n", center(0), center(1), center(2), + fprintf(curling_grid_file, "v %f %f %f %f %f %f\n", center(0), center(1), + center(2), curling_color.x(), curling_color.y(), curling_color.z()); } } @@ -722,7 +772,8 @@ struct SegmentAccumulator { }; -Issues check_extrusion_entity_stability(const ExtrusionEntity *entity, float print_z, const LayerRegion *layer_region, +Issues check_extrusion_entity_stability(const ExtrusionEntity *entity, float print_z, + const LayerRegion *layer_region, const EdgeGridWrapper &supported_grid, const Params ¶ms) { Issues issues { }; @@ -794,11 +845,12 @@ Issues check_extrusion_entity_stability(const ExtrusionEntity *entity, float pri // Estimation of short curvy segments which are not supported -> problems with curling if (dist_from_prev_layer > -max_allowed_dist_from_prev_layer * 0.7071) { //extrusion point is unsupported or poorly supported - float dist_factor = (dist_from_prev_layer + max_allowed_dist_from_prev_layer * 0.7071) + float dist_factor = 0.5f + 0.5f * (dist_from_prev_layer + max_allowed_dist_from_prev_layer * 0.7071) / max_allowed_dist_from_prev_layer; issues.curling_up.push_back( CurledFilament(fpoint, - dist_factor * (0.25f * region_height + region_height * 0.75f * std::abs(angle) / PI))); + dist_factor + * (0.25f * region_height + region_height * 0.75f * std::abs(angle) / PI))); } prev_point = point; From e39d14bf989862950287b64a5d0394e96587c4ea Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Wed, 11 May 2022 13:17:01 +0200 Subject: [PATCH 022/100] finished base for curling stability tests added comments --- src/libslic3r/SupportableIssuesSearch.cpp | 287 +++++++++++----------- 1 file changed, 137 insertions(+), 150 deletions(-) diff --git a/src/libslic3r/SupportableIssuesSearch.cpp b/src/libslic3r/SupportableIssuesSearch.cpp index 1f28a5f074..5e3e1822de 100644 --- a/src/libslic3r/SupportableIssuesSearch.cpp +++ b/src/libslic3r/SupportableIssuesSearch.cpp @@ -48,7 +48,7 @@ CurledFilament::CurledFilament(const Vec3f &position) : struct Cell { float volume; float curled_height; - int island_id = std::numeric_limits::max(); + int accumulator_id = std::numeric_limits::max(); }; class CentroidAccumulator { @@ -262,7 +262,7 @@ public: return local_coords.x() >= 0 && local_coords.y() >= 0 && local_coords.x() < this->global_cell_count.x() && local_coords.y() < this->global_cell_count.y(); }; - CentroidAccumulators accumulators(issues.supports_nedded.size() + 4); + CentroidAccumulators accumulators(issues.supports_nedded.size() + 4); // just an estimation; one for each support point from prev step, and 4 for the base auto custom_comparator = [](const Vec2i &left, const Vec2i &right) { if (left.x() == right.x()) { return left.y() < right.y(); @@ -270,26 +270,28 @@ public: return left.x() < right.x(); }; - int next_island_id = -1; - std::set coords_to_check(custom_comparator); + //initialization of the bed accumulators from the bed cells (first layer of grid) + int next_accumulator_id = -1; // accumulators from the bed have negative ids, starting with -1. Accumulators generated by support points have nonegative ids, starting with 0, and sorted by height + // The reason is, that during merging of accumulators (when they meet at the upper cells), algorithm always keeps the one with the lower id (so bed is preffered), and discards the other + std::set coords_to_check(custom_comparator); // set of coords to check for the current accumulator (search based on connectivity) for (int y = 0; y < global_cell_count.y(); ++y) { for (int x = 0; x < global_cell_count.x(); ++x) { Cell &origin_cell = this->access_cell(Vec3i(x, y, 0)); - if (origin_cell.volume > 0 && origin_cell.island_id == std::numeric_limits::max()) { - CentroidAccumulator &acc = accumulators.create_accumulator(next_island_id, 0); + if (origin_cell.volume > 0 && origin_cell.accumulator_id == std::numeric_limits::max()) { // cell has volume and no accumulator assigned yet + CentroidAccumulator &acc = accumulators.create_accumulator(next_accumulator_id, 0); // create new accumulator, height is 0, because we are on the bed coords_to_check.clear(); coords_to_check.insert(Vec2i(x, y)); - while (!coords_to_check.empty()) { + while (!coords_to_check.empty()) { // insert the origin cell coords in to the set, and search all connected cells with volume and without assigned accumulator, assign them to acc Vec2i current_coords = *coords_to_check.begin(); coords_to_check.erase(coords_to_check.begin()); - if (!validate_xy_coords(current_coords)) { + if (!validate_xy_coords(current_coords)) { // invalid coords, drop continue; } Cell &cell = this->access_cell(Vec3i(current_coords.x(), current_coords.y(), 0)); - if (cell.volume <= 0 || cell.island_id != std::numeric_limits::max()) { + if (cell.volume <= 0 || cell.accumulator_id != std::numeric_limits::max()) { // empty cell or already assigned, drop continue; } - cell.island_id = next_island_id; + cell.accumulator_id = next_accumulator_id; // update cell accumulator id, update the accumulator with the new cell data, add neighbours to queue Vec3crd cell_center = this->get_cell_center( Vec3i(current_coords.x(), current_coords.y(), local_z_index_offset)); acc.add_base_points( { Point(cell_center.head<2>()) }); @@ -305,12 +307,15 @@ public: } } } - next_island_id--; + next_accumulator_id--; + //base area is separated from the base convex hull - bed accumulators are initialized with convex hull area (TODO compute from number of covered cells instead ) + // but support points are initialized with constant, and during merging, the base_areas are added. acc.base_area = unscale(unscale(acc.base_hull().area())); //apply unscale 2x, it has units of area } } } + // sort support points by height, so that their accumulators ids are also sorted by height std::sort(issues.supports_nedded.begin(), issues.supports_nedded.end(), [](const SupportPoint &left, const SupportPoint &right) { return left.position.z() < right.position.z(); @@ -318,21 +323,27 @@ public: for (int index = 0; index < int(issues.supports_nedded.size()); ++index) { Vec3i local_coords = this->to_local_cell_coords( this->to_global_cell_coords(issues.supports_nedded[index].position)); - this->access_cell(local_coords).island_id = index; + this->access_cell(local_coords).accumulator_id = index; // assign accumulator id (in case that multiple support points fall into the same cell, they are just overriden) CentroidAccumulator &acc = accumulators.create_accumulator(index, issues.supports_nedded[index].position.z()); acc.add_base_points( { Point(scaled(Vec2f(issues.supports_nedded[index].position.head<2>()))) }); acc.base_area = params.support_points_interface_area; } + // add curling data to each cell for (const CurledFilament &curling : issues.curling_up) { this->access_cell(curling.position).curled_height += curling.estimated_height; } + // keep map of modified accumulator for each layer, so that discarded accumulators are not further checked for stability + // the value of the map is list of cells with curling, that should be further checked for pressure stability with repsect to the accumulator std::unordered_map> modified_acc_ids; + // At the end of the layer check, accumulators are further filtered, since merging causes that single accumulator can have mutliple ids. + // But each accumulator should be checked only once. std::unordered_map> filtered_active_accumulators; modified_acc_ids.reserve(issues.supports_nedded.size() + 1); + // For each grid layer, cells are added to the accumulators and all active accumulators are checked of stability. for (int z = 1; z < local_z_cell_count; ++z) { std::cout << "current z: " << z << std::endl; @@ -341,39 +352,41 @@ public: for (int x = 0; x < global_cell_count.x(); ++x) { for (int y = 0; y < global_cell_count.y(); ++y) { Cell ¤t = this->access_cell(Vec3i(x, y, z)); - // distribute islands info - if (current.volume > 0 && current.island_id == std::numeric_limits::max()) { - int min_island_id_found = std::numeric_limits::max(); + // distribute islands info - look for neighbours under the cell, and pick the smallest accumulator id + // also gather all ids, they will be merged to the smallest id accumualtor + if (current.volume > 0 && current.accumulator_id == std::numeric_limits::max()) { + int min_accumulator_id_found = std::numeric_limits::max(); std::unordered_set ids_to_merge { }; for (int y_offset = -2; y_offset <= 2; ++y_offset) { for (int x_offset = -2; x_offset <= 2; ++x_offset) { if (validate_xy_coords(Vec2i(x + x_offset, y + y_offset))) { Cell &under = this->access_cell(Vec3i(x + x_offset, y + y_offset, z - 1)); - if (under.island_id < min_island_id_found) { - min_island_id_found = under.island_id; + if (under.accumulator_id < min_accumulator_id_found) { + min_accumulator_id_found = under.accumulator_id; } - ids_to_merge.insert(under.island_id); + ids_to_merge.insert(under.accumulator_id); } } } - // assign island and update its info - if (min_island_id_found < std::numeric_limits::max()) { + // assign accumulator and update its info + if (min_accumulator_id_found < std::numeric_limits::max()) { // accumulator id found ids_to_merge.erase(std::numeric_limits::max()); - ids_to_merge.erase(min_island_id_found); - current.island_id = min_island_id_found; + ids_to_merge.erase(min_accumulator_id_found); + current.accumulator_id = min_accumulator_id_found; //assign accumualtor id to the cell for (auto id : ids_to_merge) { - accumulators.merge_to(id, min_island_id_found); + accumulators.merge_to(id, min_accumulator_id_found); //merge other ids to the smallest id } - CentroidAccumulator &acc = accumulators.access(min_island_id_found); + //update the acc with new point, and store in the modified accumulators map + CentroidAccumulator &acc = accumulators.access(min_accumulator_id_found); acc.accumulated_value += current.volume * unscale(this->get_cell_center(this->to_global_cell_coords(Vec3i(x, y, z)))).cast< float>(); acc.accumulated_volume += current.volume; - modified_acc_ids.emplace(min_island_id_found, std::vector { }); + modified_acc_ids.emplace(min_accumulator_id_found, std::vector { }); } } - // distribute curling + // distribute curling (add curling from neighbours under, but also decrease but some factor) if (current.volume > 0) { float curled_height = 0; for (int y_offset = -2; y_offset <= 2; ++y_offset) { @@ -385,18 +398,18 @@ public: } } current.curled_height += std::max(0.0f, - float(curled_height - unscaled(this->cell_size.z()) / 2.0f)); + float(curled_height - unscaled(this->cell_size.z()) / 1.5f)); current.curled_height = std::min(current.curled_height, float(unscaled(this->cell_size.z()) * 2.0f)); - std::cout << "Curling: " << current.curled_height << std::endl; - if (current.curled_height / unscaled(this->cell_size.z()) > 0.3f) { - modified_acc_ids[current.island_id].push_back( { x, y }); + if (current.curled_height / unscaled(this->cell_size.z()) > 1.5f) { // just a magic threshold number. + modified_acc_ids[current.accumulator_id].push_back( { x, y }); } } } } + //all cells of the grid layer checked, now further filter the modified accumulators, because multiple ids can point to the same acc filtered_active_accumulators.clear(); for (const auto &pair : modified_acc_ids) { CentroidAccumulator *acc = &accumulators.access(pair.first); @@ -411,142 +424,89 @@ public: private: void check_accumulators_stability(int z, CentroidAccumulators &accumulators, std::unordered_map> filtered_active_accumulators, - Issues &issues, const Params ¶ms) { + Issues &issues, const Params ¶ms) const { for (const auto &pair : filtered_active_accumulators) { std::cout << "Z: " << z << std::endl; CentroidAccumulator &acc = *pair.first; Vec3f centroid = acc.accumulated_value / acc.accumulated_volume; + Point base_centroid = acc.base_hull().centroid(); + Vec3f base_centroid_3d { }; + base_centroid_3d << unscale(base_centroid).cast(), acc.base_height; std::cout << "acc.accumulated_value : " << acc.accumulated_value.x() << " " << acc.accumulated_value.y() << " " << acc.accumulated_value.z() << std::endl; std::cout << "acc.accumulated_volume : " << acc.accumulated_volume << std::endl; std::cout << "centroid: " << centroid.x() << " " << centroid.y() << " " << centroid.z() << std::endl; - - Point centroid_base_projection = Point(scaled(Vec2f(centroid.head<2>()))); - Point pivot; - double distance_scaled_sq = std::numeric_limits::max(); - bool inside = true; - if (acc.base_hull().points.size() == 1) { - pivot = acc.base_hull().points[0]; - distance_scaled_sq = (pivot - centroid_base_projection).squaredNorm(); - inside = distance_scaled_sq < params.support_points_interface_area; - } else { - for (Line line : acc.base_hull().lines()) { - Point closest_point; - double dist_sq = line.distance_to_squared(centroid_base_projection, &closest_point); - if (dist_sq < distance_scaled_sq) { - pivot = closest_point; - distance_scaled_sq = dist_sq; - } - if ((centroid_base_projection - closest_point).cast().dot( - line.normal().cast()) - > 0) { - inside = false; - } - } - } - - Vec3f pivot_3d; - pivot_3d << unscale(pivot).cast(), acc.base_height; - float embedded_distance = unscaled(sqrt(distance_scaled_sq)); - float centroid_pivot_distance = (centroid - pivot_3d).norm(); - float base_center_pivot_distance = float( - unscale(Vec2crd(acc.base_hull().centroid() - pivot)).norm()); - - std::cout << "centroid inside ? " << inside << " and embedded distance is: " << embedded_distance + std::cout << "base_centroid_3d: " << base_centroid_3d.x() << " " << base_centroid_3d.y() << " " + << base_centroid_3d.z() << std::endl; - bool additional_supports_needed = false; - float sticking_force = acc.base_area - * (acc.base_height == 0 ? params.base_adhesion : params.support_adhesion); - float sticking_torque = base_center_pivot_distance * sticking_force; - - std::cout << "sticking force: " << sticking_force << " sticking torque: " << sticking_torque - << std::endl; - - float xy_movement_force = acc.accumulated_volume * params.filament_density - * params.max_acceleration; - float xy_movement_torque = xy_movement_force * centroid_pivot_distance; - - std::cout << "xy_movement_force: " << xy_movement_force << " xy_movement_torque: " - << xy_movement_torque << std::endl; - - float weight = acc.accumulated_volume * params.filament_density * params.gravity_constant; - float weight_torque = embedded_distance * weight; - if (!inside) { - weight_torque *= -1; - } - std::cout << "weight: " << weight << " weight_torque: " << weight_torque << std::endl; - - float extruder_conflict_torque = params.tolerable_extruder_conflict_force * 2.0f - * centroid_pivot_distance; - std::cout << "extruder_conflict_torque: " << extruder_conflict_torque << std::endl; - - float total_momentum = sticking_torque + weight_torque - xy_movement_torque - - extruder_conflict_torque; - additional_supports_needed = total_momentum < 0; - - std::cout << "total_momentum: " << total_momentum << std::endl; - std::cout << "additional supports needed: " << additional_supports_needed << std::endl; - - if (additional_supports_needed) { - Vec2f attractor_dir = - unscale(Vec2crd(inside ? - pivot - centroid_base_projection : - centroid_base_projection - pivot)).cast().normalized(); - Vec2f attractor = unscale(centroid_base_projection).cast() + 10000 * attractor_dir; - - std::cout << " attractor: " << attractor.x() << " | " << attractor.y() << std::endl; - - double min_dist = std::numeric_limits::max(); - Vec3f support_point = centroid; - Vec2i coords = Vec2i(0, 0); - for (int y = 0; y < global_cell_count.y(); ++y) { - for (int x = 0; x < global_cell_count.x(); ++x) { - Cell &cell = this->access_cell(Vec3i(x, y, z)); - if (cell.island_id != std::numeric_limits::max() && - &accumulators.access(cell.island_id) == &acc) { - Vec3f cell_center = - unscale(this->get_cell_center(this->to_global_cell_coords(Vec3i(x, y, z)))).cast< - float>(); - float dist_sq = (cell_center.head<2>() - attractor).squaredNorm(); - if (dist_sq < min_dist) { - min_dist = dist_sq; - support_point = cell_center; - coords = Vec2i(x, y); - } + // find the cell that is furthest from the base centroid ( its a heurstic to find a possible problems with balance without checking all layer cells) + //TODO better result are if first pivot is chosen as the closest point of the convex hull to the base centroid, and then cell furthest in the direction defined by + // the vector from base centroid to this pivot is taken. + double max_dist_sqr = 0; + Vec3f suspicious_point = centroid; + Vec2i coords = Vec2i(0, 0); + for (int y = 0; y < global_cell_count.y(); ++y) { + for (int x = 0; x < global_cell_count.x(); ++x) { + const Cell &cell = this->access_cell(Vec3i(x, y, z)); + if (cell.accumulator_id != std::numeric_limits::max() && + &accumulators.access(cell.accumulator_id) == &acc) { + Vec3f cell_center = + unscale(this->get_cell_center(this->to_global_cell_coords(Vec3i(x, y, z)))).cast< + float>(); + float dist_sq = (cell_center - base_centroid_3d).squaredNorm(); + if (dist_sq > max_dist_sqr) { + max_dist_sqr = dist_sq; + suspicious_point = cell_center; + coords = Vec2i(x, y); } } } + } - int final_height_coords = z; - while (final_height_coords > 0 - && this->access_cell(Vec3i(coords.x(), coords.y(), final_height_coords)).volume > 0) { - final_height_coords--; + // for the suspicious point, add movement force in xy (bed sliding, it is assumed that the worst direction is taken, for simplicity) + float xy_movement_force = acc.accumulated_volume * params.filament_density + * params.max_acceleration; + + std::cout << "xy_movement_force: " << xy_movement_force << std::endl; + + // also add weight (weight is the small factor, because the materials are very light. The weight torque will be computed much higher then what is real, + //since it does not push in the suspicoius point, but in centroid. Its approximation) + float weight = acc.accumulated_volume * params.filament_density * params.gravity_constant; + + std::cout << "weight: " << weight << std::endl; + + float force = this->check_point_stability_under_pressure(acc, base_centroid, suspicious_point, + xy_movement_force + weight + params.tolerable_extruder_conflict_force, + params); + if (force > 0) { + this->add_suppport_point(Vec3i(coords.x(), coords.y(), z), force, acc, issues, params); + } + + for (const Vec2i &cell : pair.second) { + Vec3f pressure_point = unscale( + this->get_cell_center( + this->to_global_cell_coords(Vec3i(cell.x(), cell.y(), z)))).cast(); + float force = this->check_point_stability_under_pressure(acc, base_centroid, pressure_point, + params.max_curled_conflict_extruder_force, //TODO add linear scaling of the extruder force based on the curled height (but current data about curled height are kind of unreliable in scale) + params); + if (force > 0) { + this->add_suppport_point(Vec3i(cell.x(), cell.y(), z), force, acc, issues, params); } - support_point.z() = unscaled( - (final_height_coords + this->local_z_index_offset) * this->cell_size.z()); - float expected_force = total_momentum / (support_point - pivot_3d).norm(); - - std::cout << " new support point: " << support_point.x() << " | " << support_point.y() << " | " - << support_point.z() << std::endl; - std::cout << " expected_force: " << expected_force << std::endl; - - issues.supports_nedded.emplace_back(support_point, expected_force); - acc.add_base_points( { Point::new_scale(Vec2f(support_point.head<2>())) }); - acc.base_area += params.support_points_interface_area; } } } float check_point_stability_under_pressure(CentroidAccumulator &acc, const Point &base_centroid, - const Vec3f &pressure_point, const Params ¶ms) { + const Vec3f &pressure_point, float pressure_force, const Params ¶ms) const { Point pressure_base_projection = Point(scaled(Vec2f(pressure_point.head<2>()))); Point pivot; double distance_scaled_sq = std::numeric_limits::max(); bool inside = true; + // find pivot, which is the closest point of the accumulator base hull to pressure point (if the object should fall, it would be over this point) if (acc.base_hull().points.size() == 1) { pivot = acc.base_hull().points[0]; distance_scaled_sq = (pivot - pressure_base_projection).squaredNorm(); @@ -565,19 +525,25 @@ private: } } } - float embedded_distance = unscaled(sqrt(distance_scaled_sq)); +// float embedded_distance = unscaled(sqrt(distance_scaled_sq)); float base_center_pivot_distance = float(unscale(Vec2crd(base_centroid - pivot)).norm()); Vec3f pivot_3d; pivot_3d << unscale(pivot).cast(), acc.base_height; + //sticking force estimated from the base area and support points float sticking_force = acc.base_area * (acc.base_height == 0 ? params.base_adhesion : params.support_adhesion); - float sticking_torque = sticking_force * base_center_pivot_distance; - float pressure_arm = inside ? pressure_point.z() - pivot_3d.z() : (pressure_point - pivot_3d).norm(); - float pressure_force = 1.0f; + std::cout << "sticking force: " << sticking_force << std::endl; + std::cout << "pressure force: " << pressure_force << std::endl; + float sticking_torque = sticking_force * base_center_pivot_distance; // sticking torque is computed from the distance to the centroid + + float pressure_arm = inside ? pressure_point.z() - pivot_3d.z() : (pressure_point - pivot_3d).norm(); // pressure arm is again higher then in reality, + // since it assumes the worst direction of the pressure force (perpendicular to the vector between pivot and pressure point) float pressure_torque = pressure_arm * pressure_force; + std::cout << "sticking_torque: " << sticking_torque << std::endl; + std::cout << "pressure_torque: " << pressure_torque << std::endl; if (sticking_torque < pressure_torque) { return pressure_force; } else { @@ -585,6 +551,27 @@ private: } } + void add_suppport_point(const Vec3i &local_coords, float expected_force, CentroidAccumulator &acc, Issues &issues, + const Params ¶ms) const { + //add support point - but first finding the lowest full cell is needed, since putting support point to the current cell may end up inside the model, + // essentially doing nothing. + int final_height_coords = local_coords.z(); + while (final_height_coords > 0 + && this->access_cell(this->to_global_cell_coords(Vec3i(local_coords.x(), local_coords.y(), final_height_coords))).volume > 0) { + final_height_coords--; + } + Vec3f support_point = unscale(this->get_cell_center(this->to_global_cell_coords(Vec3i(local_coords.x(), local_coords.y(), final_height_coords)))).cast< + float>(); + + std::cout << " new support point: " << support_point.x() << " | " << support_point.y() << " | " + << support_point.z() << std::endl; + std::cout << " expected_force: " << expected_force << std::endl; + + issues.supports_nedded.emplace_back(support_point, expected_force); + acc.add_base_points( { Point::new_scale(Vec2f(support_point.head<2>())) }); + acc.base_area += params.support_points_interface_area; + } + #ifdef DEBUG_FILES public: void debug_export() const { @@ -603,18 +590,18 @@ public: float max_volume = 0; int min_island_id = 0; int max_island_id = 0; - float max_curling_height = 0; + float max_curling_height = 0.5f; for (int x = 0; x < global_cell_count.x(); ++x) { for (int y = 0; y < global_cell_count.y(); ++y) { for (int z = 0; z < local_z_cell_count; ++z) { const Cell &cell = access_cell(Vec3i(x, y, z)); max_volume = std::max(max_volume, cell.volume); - if (cell.island_id != std::numeric_limits::max()) { - min_island_id = std::min(min_island_id, cell.island_id); - max_island_id = std::max(max_island_id, cell.island_id); + if (cell.accumulator_id != std::numeric_limits::max()) { + min_island_id = std::min(min_island_id, cell.accumulator_id); + max_island_id = std::max(max_island_id, cell.accumulator_id); } - max_curling_height = std::max(max_curling_height, cell.curled_height); +// max_curling_height = std::max(max_curling_height, cell.curled_height); } } } @@ -630,8 +617,8 @@ public: fprintf(volume_grid_file, "v %f %f %f %f %f %f\n", center(0), center(1), center(2), volume_color.x(), volume_color.y(), volume_color.z()); } - if (cell.island_id != std::numeric_limits::max()) { - auto island_color = value_to_rgbf(min_island_id, max_island_id + 1, cell.island_id); + if (cell.accumulator_id != std::numeric_limits::max()) { + auto island_color = value_to_rgbf(min_island_id, max_island_id + 1, cell.accumulator_id); fprintf(islands_grid_file, "v %f %f %f %f %f %f\n", center(0), center(1), center(2), island_color.x(), island_color.y(), island_color.z()); From ad4502d96ed8be4ebe8f9b2e90688c3d44fc59c1 Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Wed, 11 May 2022 15:32:46 +0200 Subject: [PATCH 023/100] implemented AABBTree version for lines --- src/libslic3r/AABBTreeLines.hpp | 22 ++++++ src/libslic3r/CMakeLists.txt | 2 - src/libslic3r/PolygonPointTest.cpp | 1 - src/libslic3r/PolygonPointTest.hpp | 90 ----------------------- src/libslic3r/SupportableIssuesSearch.cpp | 2 +- 5 files changed, 23 insertions(+), 94 deletions(-) delete mode 100644 src/libslic3r/PolygonPointTest.cpp delete mode 100644 src/libslic3r/PolygonPointTest.hpp diff --git a/src/libslic3r/AABBTreeLines.hpp b/src/libslic3r/AABBTreeLines.hpp index 7b9595419a..7eb7ab56a4 100644 --- a/src/libslic3r/AABBTreeLines.hpp +++ b/src/libslic3r/AABBTreeLines.hpp @@ -8,6 +8,28 @@ namespace Slic3r { + +struct EdgeGridWrapper { + EdgeGridWrapper(coord_t edge_width, std::vector lines) : + lines(lines), edge_width(edge_width) { + + grid.create(this->lines, edge_width, true); + grid.calculate_sdf(); + } + + bool signed_distance(const Point &point, coordf_t point_width, coordf_t &dist_out) const { + coordf_t tmp_dist_out; + bool found = grid.signed_distance(point, edge_width, tmp_dist_out); + dist_out = tmp_dist_out - edge_width / 2 - point_width / 2; + return found; + } + + EdgeGrid::Grid grid; + std::vector lines; + coord_t edge_width; +}; + + namespace AABBTreeLines { namespace detail { diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index 443c81b486..0a0062a754 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -201,8 +201,6 @@ set(SLIC3R_SOURCES Point.hpp Polygon.cpp Polygon.hpp - PolygonPointTest.cpp - PolygonPointTest.hpp MutablePolygon.cpp MutablePolygon.hpp PolygonTrimmer.cpp diff --git a/src/libslic3r/PolygonPointTest.cpp b/src/libslic3r/PolygonPointTest.cpp deleted file mode 100644 index dcab9e87eb..0000000000 --- a/src/libslic3r/PolygonPointTest.cpp +++ /dev/null @@ -1 +0,0 @@ -#include "PolygonPointTest.hpp" diff --git a/src/libslic3r/PolygonPointTest.hpp b/src/libslic3r/PolygonPointTest.hpp deleted file mode 100644 index d95b191bf6..0000000000 --- a/src/libslic3r/PolygonPointTest.hpp +++ /dev/null @@ -1,90 +0,0 @@ -#ifndef SRC_LIBSLIC3R_POLYGONPOINTTEST_HPP_ -#define SRC_LIBSLIC3R_POLYGONPOINTTEST_HPP_ - -#include "libslic3r/Point.hpp" -#include "libslic3r/EdgeGrid.hpp" - -namespace Slic3r { - -struct EdgeGridWrapper { - EdgeGridWrapper(coord_t edge_width, std::vector lines) : - lines(lines), edge_width(edge_width) { - - grid.create(this->lines, edge_width, true); - grid.calculate_sdf(); - } - - bool signed_distance(const Point &point, coordf_t point_width, coordf_t &dist_out) const { - coordf_t tmp_dist_out; - bool found = grid.signed_distance(point, edge_width, tmp_dist_out); - dist_out = tmp_dist_out - edge_width / 2 - point_width / 2; - return found; - - } - - EdgeGrid::Grid grid; - std::vector lines; - coord_t edge_width; -}; - -namespace TODO { - -class PolygonPointTest { - - struct Segment { - coord_t start; - std::vector lines; - }; - - std::vector x_coord_segments; - -public: - PolygonPointTest(const ExPolygons &ex_polygons) { - std::vector lines; - for (const auto &exp : ex_polygons) { - Lines contour = exp.contour.lines(); - lines.insert(lines.end(), contour.begin(), contour.end()); - for (const auto &hole : exp.holes) { - Lines hole_lines = hole.lines(); - for (Line &line : hole_lines) { - line.reverse(); // reverse hole lines, so that we can use normal to deduce where the object is - } - lines.insert(lines.end(), hole_lines.begin(), hole_lines.end()); - } - } - - std::vector> sweeping_data(lines.size() * 2); - for (size_t line_index = 0; line_index < lines.size(); ++line_index) { - sweeping_data[line_index].first = line_index; - sweeping_data[line_index].second = true; - sweeping_data[line_index * 2 + 1].first = line_index; - sweeping_data[line_index * 2 + 1].second = false; - } - - const auto data_comparator = [&lines](const std::pair &left, - const std::pair &right) { - const auto left_x = - left.second ? - std::min(lines[left.first].a.x(), lines[left.first].b.x()) : - std::max(lines[left.first].a.x(), lines[left.first].b.x()); - const auto right_x = - right.second ? - std::min(lines[right.first].a.x(), lines[right.first].b.x()) : - std::max(lines[right.first].a.x(), lines[right.first].b.x()); - - return left_x < right_x; - }; - - std::sort(sweeping_data.begin(), sweeping_data.end(), data_comparator); - std::set active_lines; - - //TODO continue - - } - -}; -} - -} - -#endif /* SRC_LIBSLIC3R_POLYGONPOINTTEST_HPP_ */ diff --git a/src/libslic3r/SupportableIssuesSearch.cpp b/src/libslic3r/SupportableIssuesSearch.cpp index 5e3e1822de..d8ff5b7172 100644 --- a/src/libslic3r/SupportableIssuesSearch.cpp +++ b/src/libslic3r/SupportableIssuesSearch.cpp @@ -8,10 +8,10 @@ #include #include +#include "AABBTreeLines.hpp" #include "libslic3r/Layer.hpp" #include "libslic3r/ClipperUtils.hpp" #include "Geometry/ConvexHull.hpp" -#include "PolygonPointTest.hpp" #define DEBUG_FILES From 51d738c5642ff78c5d0dcb74bc8e37bd493f4d56 Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Thu, 12 May 2022 14:13:23 +0200 Subject: [PATCH 024/100] refactored algorithm to use AABB tree instead of edge grid --- src/libslic3r/AABBTreeLines.hpp | 24 ----- src/libslic3r/SupportableIssuesSearch.cpp | 117 ++++++++++++++-------- 2 files changed, 74 insertions(+), 67 deletions(-) diff --git a/src/libslic3r/AABBTreeLines.hpp b/src/libslic3r/AABBTreeLines.hpp index 7eb7ab56a4..39f828b558 100644 --- a/src/libslic3r/AABBTreeLines.hpp +++ b/src/libslic3r/AABBTreeLines.hpp @@ -1,35 +1,11 @@ #ifndef SRC_LIBSLIC3R_AABBTREELINES_HPP_ #define SRC_LIBSLIC3R_AABBTREELINES_HPP_ -#include "libslic3r/Point.hpp" -#include "libslic3r/EdgeGrid.hpp" #include "libslic3r/AABBTreeIndirect.hpp" #include "libslic3r/Line.hpp" namespace Slic3r { - -struct EdgeGridWrapper { - EdgeGridWrapper(coord_t edge_width, std::vector lines) : - lines(lines), edge_width(edge_width) { - - grid.create(this->lines, edge_width, true); - grid.calculate_sdf(); - } - - bool signed_distance(const Point &point, coordf_t point_width, coordf_t &dist_out) const { - coordf_t tmp_dist_out; - bool found = grid.signed_distance(point, edge_width, tmp_dist_out); - dist_out = tmp_dist_out - edge_width / 2 - point_width / 2; - return found; - } - - EdgeGrid::Grid grid; - std::vector lines; - coord_t edge_width; -}; - - namespace AABBTreeLines { namespace detail { diff --git a/src/libslic3r/SupportableIssuesSearch.cpp b/src/libslic3r/SupportableIssuesSearch.cpp index d8ff5b7172..55d883f0a9 100644 --- a/src/libslic3r/SupportableIssuesSearch.cpp +++ b/src/libslic3r/SupportableIssuesSearch.cpp @@ -557,11 +557,17 @@ private: // essentially doing nothing. int final_height_coords = local_coords.z(); while (final_height_coords > 0 - && this->access_cell(this->to_global_cell_coords(Vec3i(local_coords.x(), local_coords.y(), final_height_coords))).volume > 0) { + && this->access_cell( + this->to_global_cell_coords(Vec3i(local_coords.x(), local_coords.y(), final_height_coords))).volume + > 0) { final_height_coords--; } - Vec3f support_point = unscale(this->get_cell_center(this->to_global_cell_coords(Vec3i(local_coords.x(), local_coords.y(), final_height_coords)))).cast< - float>(); + Vec3f support_point = + unscale( + this->get_cell_center( + this->to_global_cell_coords( + Vec3i(local_coords.x(), local_coords.y(), final_height_coords)))).cast< + float>(); std::cout << " new support point: " << support_point.x() << " | " << support_point.y() << " | " << support_point.z() << std::endl; @@ -683,51 +689,76 @@ void debug_export(Issues issues, std::string file_name) { } #endif -EdgeGridWrapper compute_layer_edge_grid(const Layer *layer) { - float min_region_flow_width { 1.0f }; - for (const auto *region : layer->regions()) { - min_region_flow_width = std::min(min_region_flow_width, - region->flow(FlowRole::frExternalPerimeter).width()); - } - std::vector lines; - for (const LayerRegion *layer_region : layer->regions()) { - for (const ExtrusionEntity *ex_entity : layer_region->perimeters.entities) { - lines.push_back(Points { }); - ex_entity->collect_points(lines.back()); - } // ex_entity +struct LayerLinesDistancer { + std::vector lines; + AABBTreeIndirect::Tree<2, double> tree; + float line_width; - for (const ExtrusionEntity *ex_entity : layer_region->fills.entities) { - lines.push_back(Points { }); - ex_entity->collect_points(lines.back()); - } // ex_entity + LayerLinesDistancer(const Layer *layer) { + float min_region_flow_width { 1.0f }; + for (const auto *region : layer->regions()) { + min_region_flow_width = std::min(min_region_flow_width, + region->flow(FlowRole::frExternalPerimeter).width()); + } + for (const LayerRegion *layer_region : layer->regions()) { + for (const ExtrusionEntity *ex_entity : layer_region->perimeters.entities) { + Points points { }; + ex_entity->collect_points(points); + Vec2d prev = points.size() > 0 ? unscaled(points.front()) : Vec2d::Zero(); + for (size_t idx = 1; idx < points.size(); ++idx) { + lines.emplace_back(prev, unscaled(points[idx])); + } + } // ex_entity + + for (const ExtrusionEntity *ex_entity : layer_region->fills.entities) { + Points points { }; + ex_entity->collect_points(points); + Vec2d prev = points.size() > 0 ? unscaled(points.front()) : Vec2d::Zero(); + for (size_t idx = 1; idx < points.size(); ++idx) { + lines.emplace_back(prev, unscaled(points[idx])); + } + } // ex_entity + } + tree = AABBTreeLines::build_aabb_tree_over_indexed_lines(lines); + line_width = min_region_flow_width; } - return EdgeGridWrapper(scale_(min_region_flow_width), lines); -} + float distance_from_lines(const Point &point, float point_width) const { + Vec2d p = unscaled(point); + size_t hit_idx_out; + Vec2d hit_point_out; + auto distance = AABBTreeLines::squared_distance_to_indexed_lines(lines, tree, p, hit_idx_out, hit_point_out); + if (distance < 0) + return distance; + + distance = sqrt(distance); + return std::max(float(distance - point_width / 2 - line_width / 20), 0.0f); + } +}; //TODO needs revision -coordf_t get_flow_width(const LayerRegion *region, ExtrusionRole role) { +float get_flow_width(const LayerRegion *region, ExtrusionRole role) { switch (role) { case ExtrusionRole::erBridgeInfill: - return region->flow(FlowRole::frExternalPerimeter).scaled_width(); + return region->flow(FlowRole::frExternalPerimeter).width(); case ExtrusionRole::erExternalPerimeter: - return region->flow(FlowRole::frExternalPerimeter).scaled_width(); + return region->flow(FlowRole::frExternalPerimeter).width(); case ExtrusionRole::erGapFill: - return region->flow(FlowRole::frInfill).scaled_width(); + return region->flow(FlowRole::frInfill).width(); case ExtrusionRole::erPerimeter: - return region->flow(FlowRole::frPerimeter).scaled_width(); + return region->flow(FlowRole::frPerimeter).width(); case ExtrusionRole::erSolidInfill: - return region->flow(FlowRole::frSolidInfill).scaled_width(); + return region->flow(FlowRole::frSolidInfill).width(); case ExtrusionRole::erInternalInfill: - return region->flow(FlowRole::frInfill).scaled_width(); + return region->flow(FlowRole::frInfill).width(); case ExtrusionRole::erTopSolidInfill: - return region->flow(FlowRole::frTopSolidInfill).scaled_width(); + return region->flow(FlowRole::frTopSolidInfill).width(); default: - return region->flow(FlowRole::frPerimeter).scaled_width(); + return region->flow(FlowRole::frPerimeter).width(); } } -coordf_t get_max_allowed_distance(ExtrusionRole role, coordf_t flow_width, bool external_perimeters_first, +float get_max_allowed_distance(ExtrusionRole role, float flow_width, bool external_perimeters_first, const Params ¶ms) { // <= distance / flow_width (can be larger for perimeter, if not external perimeter first) if ((role == ExtrusionRole::erExternalPerimeter || role == ExtrusionRole::erOverhangPerimeter) && (external_perimeters_first)) { @@ -761,13 +792,13 @@ struct SegmentAccumulator { Issues check_extrusion_entity_stability(const ExtrusionEntity *entity, float print_z, const LayerRegion *layer_region, - const EdgeGridWrapper &supported_grid, const Params ¶ms) { + const LayerLinesDistancer &support_layer, const Params ¶ms) { Issues issues { }; if (entity->is_collection()) { for (const auto *e : static_cast(entity)->entities) { issues.add( - check_extrusion_entity_stability(e, print_z, layer_region, supported_grid, params)); + check_extrusion_entity_stability(e, print_z, layer_region, support_layer, params)); } } else { //single extrusion path, with possible varying parameters //prepare stack of points on the extrusion path. If there are long segments, additional points might be pushed onto the stack during the algorithm. @@ -788,9 +819,9 @@ Issues check_extrusion_entity_stability(const ExtrusionEntity *entity, float pri Point prev_point = points.top(); // prev point of the path. Initialize with first point. Vec3f prev_fpoint = to_vec3f(prev_point); - coordf_t flow_width = get_flow_width(layer_region, entity->role()); + float flow_width = get_flow_width(layer_region, entity->role()); bool external_perimters_first = layer_region->region().config().external_perimeters_first; - const coordf_t max_allowed_dist_from_prev_layer = get_max_allowed_distance(entity->role(), flow_width, + const float max_allowed_dist_from_prev_layer = get_max_allowed_distance(entity->role(), flow_width, external_perimters_first, params); while (!points.empty()) { @@ -799,8 +830,8 @@ Issues check_extrusion_entity_stability(const ExtrusionEntity *entity, float pri Vec3f fpoint = to_vec3f(point); float edge_len = (fpoint - prev_fpoint).norm(); - coordf_t dist_from_prev_layer { 0 }; - if (!supported_grid.signed_distance(point, flow_width, dist_from_prev_layer)) { // dist from prev layer not found, assume empty layer + float dist_from_prev_layer = support_layer.distance_from_lines(point, flow_width); + if (dist_from_prev_layer < 0) { // dist from prev layer not found, assume empty layer issues.supports_nedded.push_back(SupportPoint(fpoint, 1.0f)); supports_acc.reset(); } @@ -867,7 +898,7 @@ void distribute_layer_volume(const PrintObject *po, size_t layer_idx, for (const ExtrusionEntity *entity : static_cast(collections)->entities) { for (const Line &line : entity->as_polyline().lines()) { balance_grid.distribute_edge(line.a, line.b, layer->print_z, - unscale(get_flow_width(region, entity->role())), layer->height); + get_flow_width(region, entity->role()), layer->height); } } } @@ -875,7 +906,7 @@ void distribute_layer_volume(const PrintObject *po, size_t layer_idx, for (const ExtrusionEntity *entity : static_cast(collections)->entities) { for (const Line &line : entity->as_polyline().lines()) { balance_grid.distribute_edge(line.a, line.b, layer->print_z, - unscale(get_flow_width(region, entity->role())), layer->height); + get_flow_width(region, entity->role()), layer->height); } } } @@ -886,7 +917,7 @@ Issues check_layer_stability(const PrintObject *po, size_t layer_idx, bool full_ const Params ¶ms) { const Layer *layer = po->get_layer(layer_idx); //Prepare edge grid of previous layer, will be used to check if the extrusion path is supported - EdgeGridWrapper supported_grid = compute_layer_edge_grid(layer->lower_layer); + LayerLinesDistancer support_layer(layer->lower_layer); Issues issues { }; if (full_check) { // If full check; check stability of perimeters, gap fills, and bridges. @@ -895,7 +926,7 @@ Issues check_layer_stability(const PrintObject *po, size_t layer_idx, bool full_ for (const ExtrusionEntity *perimeter : static_cast(ex_entity)->entities) { issues.add( check_extrusion_entity_stability(perimeter, layer->print_z, layer_region, - supported_grid, params)); + support_layer, params)); } // perimeter } // ex_entity for (const ExtrusionEntity *ex_entity : layer_region->fills.entities) { @@ -904,7 +935,7 @@ Issues check_layer_stability(const PrintObject *po, size_t layer_idx, bool full_ || fill->role() == ExtrusionRole::erBridgeInfill) { issues.add( check_extrusion_entity_stability(fill, layer->print_z, layer_region, - supported_grid, + support_layer, params)); } } // fill @@ -919,7 +950,7 @@ Issues check_layer_stability(const PrintObject *po, size_t layer_idx, bool full_ || perimeter->role() == ExtrusionRole::erOverhangPerimeter) { issues.add( check_extrusion_entity_stability(perimeter, layer->print_z, layer_region, - supported_grid, params)); + support_layer, params)); }; // ex_perimeter } // perimeter } // ex_entity From 30f072457f8325c05235ea8bb4b1bb3b324aedbc Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Fri, 17 Jun 2022 12:11:12 +0200 Subject: [PATCH 025/100] Refactored version without voxel grid, init commit --- src/libslic3r/CMakeLists.txt | 3 +- src/libslic3r/PrintObject.cpp | 2 +- .../SupportableIssuesSearchRefactoring.cpp | 514 ++++++++++++++++++ 3 files changed, 517 insertions(+), 2 deletions(-) create mode 100644 src/libslic3r/SupportableIssuesSearchRefactoring.cpp diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index 0a0062a754..e866ac34e5 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -245,7 +245,8 @@ set(SLIC3R_SOURCES SlicingAdaptive.hpp Subdivide.cpp Subdivide.hpp - SupportableIssuesSearch.cpp +# SupportableIssuesSearch.cpp + SupportableIssuesSearchRefactoring.cpp SupportableIssuesSearch.hpp SupportMaterial.cpp SupportMaterial.hpp diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index ee311b5610..f0e435dea8 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -431,7 +431,7 @@ void PrintObject::find_supportable_issues() TriangleSelectorWrapper selector { model_volume->mesh() }; for (const SupportableIssues::SupportPoint &support_point : issues.supports_nedded) { - selector.enforce_spot(Vec3f(inv_transform.cast() * support_point.position), 2.0f); + selector.enforce_spot(Vec3f(inv_transform.cast() * support_point.position), 0.5f); } model_volume->supported_facets.set(selector.selector); diff --git a/src/libslic3r/SupportableIssuesSearchRefactoring.cpp b/src/libslic3r/SupportableIssuesSearchRefactoring.cpp new file mode 100644 index 0000000000..b988976bb5 --- /dev/null +++ b/src/libslic3r/SupportableIssuesSearchRefactoring.cpp @@ -0,0 +1,514 @@ +#include "SupportableIssuesSearch.hpp" + +#include "tbb/parallel_for.h" +#include "tbb/blocked_range.h" +#include "tbb/parallel_reduce.h" +#include +#include +#include +#include + +#include "AABBTreeLines.hpp" +#include "libslic3r/Layer.hpp" +#include "libslic3r/ClipperUtils.hpp" +#include "Geometry/ConvexHull.hpp" + +#define DEBUG_FILES + +#ifdef DEBUG_FILES +#include +#include "libslic3r/Color.hpp" +#endif + +namespace Slic3r { + +static const size_t NULL_ACC_ID = std::numeric_limits::max(); + +class ExtrusionLine +{ +public: + ExtrusionLine() : + a(Vec2f::Zero()), b(Vec2f::Zero()), len(0.0f) { + } + ExtrusionLine(const Vec2f &_a, const Vec2f &_b) : + a(_a), b(_b), len((_a - _b).norm()) { + } + + float length() { + return (a - b).norm(); + } + + Vec2f a; + Vec2f b; + float len; + + size_t supported_segment_accumulator_id = NULL_ACC_ID; + + static const constexpr int Dim = 2; + using Scalar = Vec2f::Scalar; +}; + +auto get_a(ExtrusionLine &&l) { + return l.a; +} +auto get_b(ExtrusionLine &&l) { + return l.b; +} + +namespace SupportableIssues { + +void Issues::add(const Issues &layer_issues) { + supports_nedded.insert(supports_nedded.end(), layer_issues.supports_nedded.begin(), + layer_issues.supports_nedded.end()); + curling_up.insert(curling_up.end(), layer_issues.curling_up.begin(), layer_issues.curling_up.end()); +} + +bool Issues::empty() const { + return supports_nedded.empty() && curling_up.empty(); +} + +SupportPoint::SupportPoint(const Vec3f &position, float weight) : + position(position), weight(weight) { +} + +CurledFilament::CurledFilament(const Vec3f &position, float estimated_height) : + position(position), estimated_height(estimated_height) { +} + +CurledFilament::CurledFilament(const Vec3f &position) : + position(position), estimated_height(0.0f) { +} + +class LayerLinesDistancer { +private: + std::vector lines; + AABBTreeIndirect::Tree<2, float> tree; + +public: + explicit LayerLinesDistancer(std::vector &&lines) : + lines(lines) { + tree = AABBTreeLines::build_aabb_tree_over_indexed_lines(lines); + } + + // negative sign means inside + float signed_distance_from_lines(const Point &point, size_t &nearest_line_index_out, + Vec2f &nearest_point_out) const { + Vec2f p = unscaled(point).cast(); + auto distance = AABBTreeLines::squared_distance_to_indexed_lines(lines, tree, p, nearest_line_index_out, + nearest_point_out); + if (distance < 0) + return std::numeric_limits::infinity(); + + distance = sqrt(distance); + const ExtrusionLine &line = lines[nearest_line_index_out]; + Vec2f v1 = line.b - line.a; + Vec2f v2 = p - line.a; + if ((v1.x() * v2.y()) - (v1.y() * v2.x()) > 0.0) { + distance *= -1; + } + return distance; + } + + const ExtrusionLine& get_line(size_t line_idx) const { + return lines[line_idx]; + } +}; + +class SupportedSegmentAccumulator { +private: + Polygon base_convex_hull { }; + Points supported_points { }; + Vec3f centroid_accumulator = Vec3f::Zero(); + float accumulated_volume { }; + float base_area { }; + float base_height { }; + +public: + explicit SupportedSegmentAccumulator(float base_height) : + base_height(base_height) { + } + + void add_base_extrusion(const ExtrusionLine &line, float width, float print_z, float cross_section) { + base_area += line.len * width; + supported_points.push_back(Point::new_scale(line.a)); + supported_points.push_back(Point::new_scale(line.b)); + base_convex_hull.clear(); + add_extrusion(line, print_z, cross_section); + } + + void add_support_point(const Point &position, float area) { + supported_points.push_back(position); + base_convex_hull.clear(); + base_area += area; + } + + void add_extrusion(const ExtrusionLine &line, float print_z, float cross_section) { + float volume = line.len * cross_section; + accumulated_volume += volume; + Vec2f center = (line.a + line.b) / 2.0f; + centroid_accumulator += volume * Vec3f(center.x(), center.y(), print_z); + } + + const Polygon& segment_base_hull() { + if (this->base_convex_hull.empty()) { + this->base_convex_hull = Geometry::convex_hull(this->supported_points); + } + return this->base_convex_hull; + } + + void add_from(const SupportedSegmentAccumulator &acc) { + this->supported_points.insert(this->supported_points.end(), acc.supported_points.begin(), + acc.supported_points.end()); + base_convex_hull.clear(); + this->centroid_accumulator += acc.centroid_accumulator; + this->accumulated_volume += acc.accumulated_volume; + this->base_area += acc.base_area; + } + + bool check_stability() { + return true; + } +}; + +struct SupportedSegmentAccumulators { +private: + size_t next_id = 0; + std::unordered_map mapping; + std::vector acccumulators; + + void merge_to(size_t from_id, size_t to_id) { + SupportedSegmentAccumulator &from_acc = this->access(from_id); + SupportedSegmentAccumulator &to_acc = this->access(to_id); + if (&from_acc == &to_acc) { + return; + } + to_acc.add_from(from_acc); + mapping[from_id] = mapping[to_id]; + from_acc = SupportedSegmentAccumulator { 0.0f }; + + } + +public: + SupportedSegmentAccumulators() = default; + + int create_accumulator(float base_height) { + size_t id = next_id; + next_id++; + mapping[id] = acccumulators.size(); + acccumulators.push_back(SupportedSegmentAccumulator { base_height }); + return id; + } + + SupportedSegmentAccumulator& access(size_t id) { + return acccumulators[mapping[id]]; + } + + void merge_accumulators(size_t from_id, size_t to_id) { + if (from_id == NULL_ACC_ID || to_id == NULL_ACC_ID) { + return; + } + SupportedSegmentAccumulator &from_acc = this->access(from_id); + SupportedSegmentAccumulator &to_acc = this->access(to_id); + if (&from_acc == &to_acc) { + return; + } + to_acc.add_from(from_acc); + mapping[from_id] = mapping[to_id]; + from_acc = SupportedSegmentAccumulator { 0.0f }; + } + + std::unordered_set get_active_acc_indices() const { + std::unordered_set result; + for (const auto &pair : mapping) { + result.insert(pair.second); + } + return result; + } +}; + +float get_flow_width(const LayerRegion *region, ExtrusionRole role) { + switch (role) { + case ExtrusionRole::erBridgeInfill: + return region->flow(FlowRole::frExternalPerimeter).width(); + case ExtrusionRole::erExternalPerimeter: + return region->flow(FlowRole::frExternalPerimeter).width(); + case ExtrusionRole::erGapFill: + return region->flow(FlowRole::frInfill).width(); + case ExtrusionRole::erPerimeter: + return region->flow(FlowRole::frPerimeter).width(); + case ExtrusionRole::erSolidInfill: + return region->flow(FlowRole::frSolidInfill).width(); + case ExtrusionRole::erInternalInfill: + return region->flow(FlowRole::frInfill).width(); + case ExtrusionRole::erTopSolidInfill: + return region->flow(FlowRole::frTopSolidInfill).width(); + default: + return region->flow(FlowRole::frPerimeter).width(); + } +} + +float get_max_allowed_distance(ExtrusionRole role, float flow_width, bool external_perimeters_first, + const Params ¶ms) { // <= distance / flow_width (can be larger for perimeter, if not external perimeter first) + if ((role == ExtrusionRole::erExternalPerimeter || role == ExtrusionRole::erOverhangPerimeter) + && (external_perimeters_first)) { + return params.max_first_ex_perim_unsupported_distance_factor * flow_width; + } else { + return params.max_unsupported_distance_factor * flow_width; + } +} + +struct ExtrusionPropertiesAccumulator { + float distance = 0; //accumulated distance + float curvature = 0; //accumulated signed ccw angles + float max_curvature = 0; //max absolute accumulated value + + void add_distance(float dist) { + distance += dist; + } + + void add_angle(float ccw_angle) { + curvature += ccw_angle; + max_curvature = std::max(max_curvature, std::abs(curvature)); + } + + void reset() { + distance = 0; + curvature = 0; + max_curvature = 0; + } +}; + +void check_extrusion_entity_stability(const ExtrusionEntity *entity, + SupportedSegmentAccumulators &stability_accs, + Issues &issues, + std::vector &checked_lines, + float print_z, + const LayerRegion *layer_region, + const LayerLinesDistancer &prev_layer_lines, + const Params ¶ms) { + + if (entity->is_collection()) { + for (const auto *e : static_cast(entity)->entities) { + check_extrusion_entity_stability(e, stability_accs, issues, checked_lines, print_z, layer_region, + prev_layer_lines, + params); + } + } else { //single extrusion path, with possible varying parameters + const auto to_vec3f = [print_z](const Point &point) { + Vec2f tmp = unscale(point).cast(); + return Vec3f(tmp.x(), tmp.y(), print_z); + }; + Points points { }; + entity->collect_points(points); + std::vector lines; + lines.reserve(points.size() * 1.5); + lines.emplace_back(unscaled(points[0]).cast(), unscaled(points[0]).cast()); + for (int point_idx = 0; point_idx < int(points.size() - 1); ++point_idx) { + Vec2f start = unscaled(points[point_idx]).cast(); + Vec2f next = unscaled(points[point_idx]).cast(); + Vec2f v = next - start; // vector from next to current + float dist_to_next = v.norm(); + v.normalize(); + int lines_count = int(std::ceil(dist_to_next / params.bridge_distance)); + float step_size = dist_to_next / lines_count; + for (int i = 0; i < lines_count; ++i) { + Vec2f a(start + v * (i * step_size)); + Vec2f b(start + v * ((i + 1) * step_size)); + lines.emplace_back(a, b); + } + } + + checked_lines.insert(checked_lines.end(), lines.begin(), lines.end()); + + size_t current_stability_acc = NULL_ACC_ID; + ExtrusionPropertiesAccumulator bridging_acc { }; + bridging_acc.add_distance(params.bridge_distance + 1.0f); // Initialise unsupported distance with larger than tolerable distance -> + // -> it prevents extruding perimeter start and short loops into air. + const float flow_width = get_flow_width(layer_region, entity->role()); + const float region_height = layer_region->layer()->height; + const float max_allowed_dist_from_prev_layer = get_max_allowed_distance(entity->role(), flow_width, + layer_region->region().config().external_perimeters_first, params); + + for (size_t line_idx = 0; line_idx < lines.size(); ++line_idx) { + ExtrusionLine current_line = lines[line_idx]; + Point current = Point::new_scale(current_line.b); + float cross_section = region_height * flow_width * 0.7071f; + + float angle = 0; + if (line_idx + 1 < lines.size()) { + const Vec2f v1 = current_line.b - current_line.a; + const Vec2f v2 = lines[line_idx + 1].b - lines[line_idx + 1].a; + float dot = v1(0) * v2(0) + v1(1) * v2(1); + float cross = v1(0) * v2(1) - v1(1) * v2(0); + angle = float(atan2(float(cross), float(dot))); // ccw angle, TODO replace with angle func, once it gets into master + } + bridging_acc.add_angle(angle); + + size_t nearest_line_idx; + Vec2f nearest_point; + float dist_from_prev_layer = prev_layer_lines.signed_distance_from_lines(current, nearest_line_idx, + nearest_point); + if (dist_from_prev_layer - flow_width < max_allowed_dist_from_prev_layer) { + const ExtrusionLine &nearest_line = prev_layer_lines.get_line(nearest_line_idx); + size_t acc_id = nearest_line.supported_segment_accumulator_id; + stability_accs.merge_accumulators(std::max(acc_id, current_stability_acc), + std::min(acc_id, current_stability_acc)); + current_stability_acc = std::min(acc_id, current_stability_acc); + current_line.supported_segment_accumulator_id = current_stability_acc; + stability_accs.access(current_stability_acc).add_extrusion(current_line, print_z, cross_section); + bridging_acc.reset(); + // TODO curving here + } else { + bridging_acc.add_distance(current_line.len); + if (current_stability_acc < 0) { + size_t acc_id = stability_accs.create_accumulator(print_z); + stability_accs.merge_accumulators(std::max(acc_id, current_stability_acc), + std::min(acc_id, current_stability_acc)); + current_stability_acc = std::min(acc_id, current_stability_acc); + } + SupportedSegmentAccumulator ¤t_segment = stability_accs.access(current_stability_acc); + current_segment.add_extrusion(current_line, print_z, cross_section); + if (bridging_acc.distance // if unsupported distance is larger than bridge distance linearly decreased by curvature, enforce supports. + > params.bridge_distance + / (1.0f + (bridging_acc.max_curvature + * params.bridge_distance_decrease_by_curvature_factor / PI))) { + current_segment.add_support_point(current, 5.0f); + issues.supports_nedded.emplace_back(to_vec3f(current), 1.0); + bridging_acc.reset(); + } + } + } + } +} + +Issues check_object_stability(const PrintObject *po, const Params ¶ms) { + SupportedSegmentAccumulators stability_accs; + LayerLinesDistancer prev_layer_lines { { } }; + Issues issues { }; + std::vector checked_lines; + + const Layer *layer = po->layers()[0]; + float base_print_z = layer->print_z; + for (const LayerRegion *layer_region : layer->regions()) { + for (const ExtrusionEntity *ex_entity : layer_region->perimeters.entities) { + for (const ExtrusionEntity *perimeter : static_cast(ex_entity)->entities) { + const float flow_width = get_flow_width(layer_region, perimeter->role()); + const float region_height = layer_region->layer()->height; + const float cross_section = region_height * flow_width * 0.7071f; + int id = stability_accs.create_accumulator(base_print_z); + SupportedSegmentAccumulator &acc = stability_accs.access(id); + Points points { }; + perimeter->collect_points(points); + for (int point_idx = 0; point_idx < int(points.size() - 1); ++point_idx) { + Vec2f start = unscaled(points[point_idx]).cast(); + Vec2f next = unscaled(points[point_idx]).cast(); + ExtrusionLine line{start, next}; + line.supported_segment_accumulator_id = id; + acc.add_base_extrusion( line, flow_width, base_print_z, cross_section); + checked_lines.push_back(line); + } + } // perimeter + } // ex_entity + for (const ExtrusionEntity *ex_entity : layer_region->fills.entities) { + for (const ExtrusionEntity *fill : static_cast(ex_entity)->entities) { + const float flow_width = get_flow_width(layer_region, fill->role()); + const float region_height = layer_region->layer()->height; + const float cross_section = region_height * flow_width * 0.7071f; + int id = stability_accs.create_accumulator(base_print_z); + SupportedSegmentAccumulator &acc = stability_accs.access(id); + Points points { }; + fill->collect_points(points); + for (int point_idx = 0; point_idx < int(points.size() - 1); ++point_idx) { + Vec2f start = unscaled(points[point_idx]).cast(); + Vec2f next = unscaled(points[point_idx]).cast(); + acc.add_base_extrusion( { start, next }, flow_width, base_print_z, cross_section); + } + } // fill + } // ex_entity + } // region + + for (size_t layer_idx = 1; layer_idx < po->layer_count(); ++layer_idx) { + const Layer *layer = po->layers()[layer_idx]; + prev_layer_lines = LayerLinesDistancer{std::move(checked_lines)}; + checked_lines = std::vector{}; + + float print_z = layer->print_z; + for (const LayerRegion *layer_region : layer->regions()) { + for (const ExtrusionEntity *ex_entity : layer_region->perimeters.entities) { + for (const ExtrusionEntity *perimeter : static_cast(ex_entity)->entities) { + check_extrusion_entity_stability(perimeter, stability_accs, issues, checked_lines, print_z, + layer_region, + prev_layer_lines, params); + } // perimeter + } // ex_entity + for (const ExtrusionEntity *ex_entity : layer_region->fills.entities) { + for (const ExtrusionEntity *fill : static_cast(ex_entity)->entities) { + if (fill->role() == ExtrusionRole::erGapFill + || fill->role() == ExtrusionRole::erBridgeInfill) { + check_extrusion_entity_stability(fill, stability_accs, issues, checked_lines, print_z, + layer_region, + prev_layer_lines, params); + } + } // fill + } // ex_entity + } // region + } + + std::cout << " SUPP: " << issues.supports_nedded.size() << std::endl; + return issues; +} + + +#ifdef DEBUG_FILES +void debug_export(Issues issues, std::string file_name) { + Slic3r::CNumericLocalesSetter locales_setter; + + { + FILE *fp = boost::nowide::fopen(debug_out_path((file_name + "_supports.obj").c_str()).c_str(), "w"); + if (fp == nullptr) { + BOOST_LOG_TRIVIAL(error) + << "Debug files: Couldn't open " << file_name << " for writing"; + return; + } + + for (size_t i = 0; i < issues.supports_nedded.size(); ++i) { + fprintf(fp, "v %f %f %f %f %f %f\n", issues.supports_nedded[i].position(0), + issues.supports_nedded[i].position(1), + issues.supports_nedded[i].position(2), 1.0, 0.0, 1.0); + } + + fclose(fp); + } + { + FILE *fp = boost::nowide::fopen(debug_out_path((file_name + "_curling.obj").c_str()).c_str(), "w"); + if (fp == nullptr) { + BOOST_LOG_TRIVIAL(error) + << "Debug files: Couldn't open " << file_name << " for writing"; + return; + } + + for (size_t i = 0; i < issues.curling_up.size(); ++i) { + fprintf(fp, "v %f %f %f %f %f %f\n", issues.curling_up[i].position(0), + issues.curling_up[i].position(1), + issues.curling_up[i].position(2), 0.0, 1.0, 0.0); + } + fclose(fp); + } + +} +#endif + +std::vector quick_search(const PrintObject *po, const Params ¶ms) { + check_object_stability(po, params); + return {}; +} + +Issues full_search(const PrintObject *po, const Params ¶ms) { + auto issues = check_object_stability(po, params); + debug_export(issues, "issues"); + return issues; + +} +} //SupportableIssues End +} + From 08071d85ee889071cd4060a17bd88b97c660e88b Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Fri, 17 Jun 2022 17:41:48 +0200 Subject: [PATCH 026/100] integration of the simple physical model into the refactored version --- src/libslic3r/SupportableIssuesSearch.hpp | 2 - .../SupportableIssuesSearchRefactoring.cpp | 277 +++++++++++++----- 2 files changed, 199 insertions(+), 80 deletions(-) diff --git a/src/libslic3r/SupportableIssuesSearch.hpp b/src/libslic3r/SupportableIssuesSearch.hpp index 1e90e8cde0..d5736a3bde 100644 --- a/src/libslic3r/SupportableIssuesSearch.hpp +++ b/src/libslic3r/SupportableIssuesSearch.hpp @@ -12,8 +12,6 @@ struct Params { float bridge_distance = 10.0f; //mm - float max_first_ex_perim_unsupported_distance_factor = 0.0f; // if external perim first, return tighter max allowed distance from previous layer extrusion - float max_unsupported_distance_factor = 1.0f; // For internal perimeters, infill, bridges etc, allow gap of [extrusion width] size, these extrusions have usually something to stick to. float bridge_distance_decrease_by_curvature_factor = 5.0f; // allowed bridge distance = bridge_distance / ( 1 + this factor * (curvature / PI) ) float base_adhesion = 2000.0f; // adhesion per mm^2 of first layer; Force needed to remove the object from the bed, divided by the adhesion area (g/mm*s^2) diff --git a/src/libslic3r/SupportableIssuesSearchRefactoring.cpp b/src/libslic3r/SupportableIssuesSearchRefactoring.cpp index b988976bb5..35b567c55e 100644 --- a/src/libslic3r/SupportableIssuesSearchRefactoring.cpp +++ b/src/libslic3r/SupportableIssuesSearchRefactoring.cpp @@ -91,10 +91,9 @@ public: } // negative sign means inside - float signed_distance_from_lines(const Point &point, size_t &nearest_line_index_out, + float signed_distance_from_lines(const Vec2f &point, size_t &nearest_line_index_out, Vec2f &nearest_point_out) const { - Vec2f p = unscaled(point).cast(); - auto distance = AABBTreeLines::squared_distance_to_indexed_lines(lines, tree, p, nearest_line_index_out, + auto distance = AABBTreeLines::squared_distance_to_indexed_lines(lines, tree, point, nearest_line_index_out, nearest_point_out); if (distance < 0) return std::numeric_limits::infinity(); @@ -102,7 +101,7 @@ public: distance = sqrt(distance); const ExtrusionLine &line = lines[nearest_line_index_out]; Vec2f v1 = line.b - line.a; - Vec2f v2 = p - line.a; + Vec2f v2 = point - line.a; if ((v1.x() * v2.y()) - (v1.y() * v2.x()) > 0.0) { distance *= -1; } @@ -114,30 +113,30 @@ public: } }; -class SupportedSegmentAccumulator { +class StabilityAccumulator { private: Polygon base_convex_hull { }; - Points supported_points { }; + Points support_points { }; Vec3f centroid_accumulator = Vec3f::Zero(); float accumulated_volume { }; float base_area { }; float base_height { }; public: - explicit SupportedSegmentAccumulator(float base_height) : + explicit StabilityAccumulator(float base_height) : base_height(base_height) { } void add_base_extrusion(const ExtrusionLine &line, float width, float print_z, float cross_section) { base_area += line.len * width; - supported_points.push_back(Point::new_scale(line.a)); - supported_points.push_back(Point::new_scale(line.b)); + support_points.push_back(Point::new_scale(line.a)); + support_points.push_back(Point::new_scale(line.b)); base_convex_hull.clear(); add_extrusion(line, print_z, cross_section); } void add_support_point(const Point &position, float area) { - supported_points.push_back(position); + support_points.push_back(position); base_convex_hull.clear(); base_area += area; } @@ -149,57 +148,68 @@ public: centroid_accumulator += volume * Vec3f(center.x(), center.y(), print_z); } + Vec3f get_centroid() const { + return centroid_accumulator / accumulated_volume; + } + + float get_base_area() const { + return base_area; + } + float get_base_height() const { + return base_height; + } + const Polygon& segment_base_hull() { if (this->base_convex_hull.empty()) { - this->base_convex_hull = Geometry::convex_hull(this->supported_points); + this->base_convex_hull = Geometry::convex_hull(this->support_points); } return this->base_convex_hull; } - void add_from(const SupportedSegmentAccumulator &acc) { - this->supported_points.insert(this->supported_points.end(), acc.supported_points.begin(), - acc.supported_points.end()); + const Points& get_support_points() { + return support_points; + } + + void add_from(const StabilityAccumulator &acc) { + this->support_points.insert(this->support_points.end(), acc.support_points.begin(), + acc.support_points.end()); base_convex_hull.clear(); this->centroid_accumulator += acc.centroid_accumulator; this->accumulated_volume += acc.accumulated_volume; this->base_area += acc.base_area; } - - bool check_stability() { - return true; - } }; -struct SupportedSegmentAccumulators { +struct StabilityAccumulators { private: size_t next_id = 0; std::unordered_map mapping; - std::vector acccumulators; + std::vector acccumulators; void merge_to(size_t from_id, size_t to_id) { - SupportedSegmentAccumulator &from_acc = this->access(from_id); - SupportedSegmentAccumulator &to_acc = this->access(to_id); + StabilityAccumulator &from_acc = this->access(from_id); + StabilityAccumulator &to_acc = this->access(to_id); if (&from_acc == &to_acc) { return; } to_acc.add_from(from_acc); mapping[from_id] = mapping[to_id]; - from_acc = SupportedSegmentAccumulator { 0.0f }; + from_acc = StabilityAccumulator { 0.0f }; } public: - SupportedSegmentAccumulators() = default; + StabilityAccumulators() = default; int create_accumulator(float base_height) { size_t id = next_id; next_id++; mapping[id] = acccumulators.size(); - acccumulators.push_back(SupportedSegmentAccumulator { base_height }); + acccumulators.push_back(StabilityAccumulator { base_height }); return id; } - SupportedSegmentAccumulator& access(size_t id) { + StabilityAccumulator& access(size_t id) { return acccumulators[mapping[id]]; } @@ -207,23 +217,37 @@ public: if (from_id == NULL_ACC_ID || to_id == NULL_ACC_ID) { return; } - SupportedSegmentAccumulator &from_acc = this->access(from_id); - SupportedSegmentAccumulator &to_acc = this->access(to_id); + StabilityAccumulator &from_acc = this->access(from_id); + StabilityAccumulator &to_acc = this->access(to_id); if (&from_acc == &to_acc) { return; } to_acc.add_from(from_acc); mapping[from_id] = mapping[to_id]; - from_acc = SupportedSegmentAccumulator { 0.0f }; + from_acc = StabilityAccumulator { 0.0f }; } - std::unordered_set get_active_acc_indices() const { - std::unordered_set result; - for (const auto &pair : mapping) { - result.insert(pair.second); +#ifdef DEBUG_FILES + Vec3f get_emerging_color(size_t id) { + if (mapping.find(id) == mapping.end()) { + std::cerr << " ERROR: uknown accumulator ID: " << id << std::endl; + return Vec3f(1.0f, 1.0f, 1.0f); } - return result; + + size_t pseudornd = ((id + 127) * 33331 + 6907) % 13; + return value_to_rgbf(0.0f, 13.0f, float(pseudornd)); } + + Vec3f get_final_color(size_t id) { + if (mapping.find(id) == mapping.end()) { + std::cerr << " ERROR: uknown accumulator ID: " << id << std::endl; + return Vec3f(1.0f, 1.0f, 1.0f); + } + + size_t pseudornd = ((mapping[id] + 127) * 33331 + 6907) % 13; + return value_to_rgbf(0.0f, 13.0f, float(pseudornd)); + } +#endif DEBUG_FILES }; float get_flow_width(const LayerRegion *region, ExtrusionRole role) { @@ -247,16 +271,6 @@ float get_flow_width(const LayerRegion *region, ExtrusionRole role) { } } -float get_max_allowed_distance(ExtrusionRole role, float flow_width, bool external_perimeters_first, - const Params ¶ms) { // <= distance / flow_width (can be larger for perimeter, if not external perimeter first) - if ((role == ExtrusionRole::erExternalPerimeter || role == ExtrusionRole::erOverhangPerimeter) - && (external_perimeters_first)) { - return params.max_first_ex_perim_unsupported_distance_factor * flow_width; - } else { - return params.max_unsupported_distance_factor * flow_width; - } -} - struct ExtrusionPropertiesAccumulator { float distance = 0; //accumulated distance float curvature = 0; //accumulated signed ccw angles @@ -279,7 +293,7 @@ struct ExtrusionPropertiesAccumulator { }; void check_extrusion_entity_stability(const ExtrusionEntity *entity, - SupportedSegmentAccumulators &stability_accs, + StabilityAccumulators &stability_accs, Issues &issues, std::vector &checked_lines, float print_z, @@ -305,7 +319,7 @@ void check_extrusion_entity_stability(const ExtrusionEntity *entity, lines.emplace_back(unscaled(points[0]).cast(), unscaled(points[0]).cast()); for (int point_idx = 0; point_idx < int(points.size() - 1); ++point_idx) { Vec2f start = unscaled(points[point_idx]).cast(); - Vec2f next = unscaled(points[point_idx]).cast(); + Vec2f next = unscaled(points[point_idx + 1]).cast(); Vec2f v = next - start; // vector from next to current float dist_to_next = v.norm(); v.normalize(); @@ -318,37 +332,32 @@ void check_extrusion_entity_stability(const ExtrusionEntity *entity, } } - checked_lines.insert(checked_lines.end(), lines.begin(), lines.end()); - size_t current_stability_acc = NULL_ACC_ID; ExtrusionPropertiesAccumulator bridging_acc { }; bridging_acc.add_distance(params.bridge_distance + 1.0f); // Initialise unsupported distance with larger than tolerable distance -> // -> it prevents extruding perimeter start and short loops into air. const float flow_width = get_flow_width(layer_region, entity->role()); const float region_height = layer_region->layer()->height; - const float max_allowed_dist_from_prev_layer = get_max_allowed_distance(entity->role(), flow_width, - layer_region->region().config().external_perimeters_first, params); + const float max_allowed_dist_from_prev_layer = flow_width; for (size_t line_idx = 0; line_idx < lines.size(); ++line_idx) { - ExtrusionLine current_line = lines[line_idx]; + ExtrusionLine ¤t_line = lines[line_idx]; Point current = Point::new_scale(current_line.b); float cross_section = region_height * flow_width * 0.7071f; - float angle = 0; + float curr_angle = 0; if (line_idx + 1 < lines.size()) { const Vec2f v1 = current_line.b - current_line.a; const Vec2f v2 = lines[line_idx + 1].b - lines[line_idx + 1].a; - float dot = v1(0) * v2(0) + v1(1) * v2(1); - float cross = v1(0) * v2(1) - v1(1) * v2(0); - angle = float(atan2(float(cross), float(dot))); // ccw angle, TODO replace with angle func, once it gets into master + curr_angle = angle(v1, v2); } - bridging_acc.add_angle(angle); + bridging_acc.add_angle(curr_angle); size_t nearest_line_idx; Vec2f nearest_point; - float dist_from_prev_layer = prev_layer_lines.signed_distance_from_lines(current, nearest_line_idx, + float dist_from_prev_layer = prev_layer_lines.signed_distance_from_lines(current_line.b, nearest_line_idx, nearest_point); - if (dist_from_prev_layer - flow_width < max_allowed_dist_from_prev_layer) { + if (dist_from_prev_layer < max_allowed_dist_from_prev_layer) { const ExtrusionLine &nearest_line = prev_layer_lines.get_line(nearest_line_idx); size_t acc_id = nearest_line.supported_segment_accumulator_id; stability_accs.merge_accumulators(std::max(acc_id, current_stability_acc), @@ -360,29 +369,110 @@ void check_extrusion_entity_stability(const ExtrusionEntity *entity, // TODO curving here } else { bridging_acc.add_distance(current_line.len); - if (current_stability_acc < 0) { - size_t acc_id = stability_accs.create_accumulator(print_z); - stability_accs.merge_accumulators(std::max(acc_id, current_stability_acc), - std::min(acc_id, current_stability_acc)); - current_stability_acc = std::min(acc_id, current_stability_acc); + if (current_stability_acc == NULL_ACC_ID) { + current_stability_acc = stability_accs.create_accumulator(print_z); } - SupportedSegmentAccumulator ¤t_segment = stability_accs.access(current_stability_acc); + StabilityAccumulator ¤t_segment = stability_accs.access(current_stability_acc); + current_line.supported_segment_accumulator_id = current_stability_acc; current_segment.add_extrusion(current_line, print_z, cross_section); if (bridging_acc.distance // if unsupported distance is larger than bridge distance linearly decreased by curvature, enforce supports. > params.bridge_distance / (1.0f + (bridging_acc.max_curvature * params.bridge_distance_decrease_by_curvature_factor / PI))) { - current_segment.add_support_point(current, 5.0f); + current_segment.add_support_point(current, params.support_points_interface_area); issues.supports_nedded.emplace_back(to_vec3f(current), 1.0); bridging_acc.reset(); } } } + checked_lines.insert(checked_lines.end(), lines.begin(), lines.end()); + } +} + +void check_layer_global_stability(StabilityAccumulators &stability_accs, + Issues &issues, + const std::vector &checked_lines, + float print_z, + const Params ¶ms) { + std::unordered_map> layer_accs_lines; + for (size_t i = 0; i < checked_lines.size(); ++i) { + layer_accs_lines[&stability_accs.access(checked_lines[i].supported_segment_accumulator_id)].push_back(i); + } + + for (auto &acc_lines : layer_accs_lines) { + StabilityAccumulator *acc = acc_lines.first; + Vec3f centroid = acc->get_centroid(); + Vec2f hull_centroid = unscaled(acc->segment_base_hull().centroid()).cast(); + std::vector hull_lines; + for (const Line &line : acc->segment_base_hull().lines()) { + Vec2f start = unscaled(line.a).cast(); + Vec2f next = unscaled(line.b).cast(); + hull_lines.push_back( { start, next }); + } + if (hull_lines.empty()) { + if (acc->get_support_points().empty()) { + acc->add_support_point(Point::new_scale(checked_lines[acc_lines.second[0]].a), + params.support_points_interface_area); + issues.supports_nedded.emplace_back(to_3d(checked_lines[acc_lines.second[0]].a, print_z), 1.0); + } + hull_lines.push_back( { unscaled(acc->get_support_points()[0]).cast(), + unscaled(acc->get_support_points()[0]).cast() }); + hull_centroid = unscaled(acc->get_support_points()[0]).cast(); + } + + LayerLinesDistancer hull_distancer(std::move(hull_lines)); + + size_t _li; + Vec2f _p; + bool centroid_inside_hull = hull_distancer.signed_distance_from_lines(centroid.head<2>(), _li, _p) < 0; + + float sticking_force = acc->get_base_area() + * (acc->get_base_height() == 0 ? params.base_adhesion : params.support_adhesion); +// float weight = acc-> * params.filament_density * params.gravity_constant; +// float weight_torque = embedded_distance * weight; +// if (!inside) { +// weight_torque *= -1; +// } + + for (size_t line_idx : acc_lines.second){ + const ExtrusionLine &line = checked_lines[line_idx]; + + size_t nearest_line_idx; + Vec2f nearest_hull_point; + float hull_distance = hull_distancer.signed_distance_from_lines(line.b, nearest_line_idx, + nearest_hull_point); + + float sticking_torque = (nearest_hull_point - hull_centroid).norm() * sticking_force; + + std::cout << "sticking_torque: " << sticking_torque << std::endl; + + + Vec3f extruder_pressure_direction = to_3d(Vec2f(line.b - line.a), 0.0f).normalized(); + if (hull_distance > 0) { + extruder_pressure_direction.z() = -0.333f; + extruder_pressure_direction.normalize(); + } + float pressure_torque_arm = (to_3d(Vec2f(nearest_hull_point - line.b), print_z).cross(extruder_pressure_direction)).norm(); + + float extruder_conflict_torque = params.tolerable_extruder_conflict_force * pressure_torque_arm; + + std::cout << "extruder_conflict_torque: " << extruder_conflict_torque << std::endl; + + if (extruder_conflict_torque > sticking_torque) { + acc->add_support_point(Point::new_scale(line.b), params.support_points_interface_area); + issues.supports_nedded.emplace_back(to_3d(line.b, print_z), extruder_conflict_torque - sticking_torque); + } + + } } } Issues check_object_stability(const PrintObject *po, const Params ¶ms) { - SupportedSegmentAccumulators stability_accs; +#ifdef DEBUG_FILES + FILE *eacc = boost::nowide::fopen(debug_out_path("emerging_accumulators.obj").c_str(), "w"); + FILE *facc = boost::nowide::fopen(debug_out_path("final_accumulators.obj").c_str(), "w"); +#endif DEBUG_FILES + StabilityAccumulators stability_accs; LayerLinesDistancer prev_layer_lines { { } }; Issues issues { }; std::vector checked_lines; @@ -396,15 +486,15 @@ Issues check_object_stability(const PrintObject *po, const Params ¶ms) { const float region_height = layer_region->layer()->height; const float cross_section = region_height * flow_width * 0.7071f; int id = stability_accs.create_accumulator(base_print_z); - SupportedSegmentAccumulator &acc = stability_accs.access(id); + StabilityAccumulator &acc = stability_accs.access(id); Points points { }; perimeter->collect_points(points); for (int point_idx = 0; point_idx < int(points.size() - 1); ++point_idx) { Vec2f start = unscaled(points[point_idx]).cast(); - Vec2f next = unscaled(points[point_idx]).cast(); - ExtrusionLine line{start, next}; + Vec2f next = unscaled(points[point_idx + 1]).cast(); + ExtrusionLine line { start, next }; line.supported_segment_accumulator_id = id; - acc.add_base_extrusion( line, flow_width, base_print_z, cross_section); + acc.add_base_extrusion(line, flow_width, base_print_z, cross_section); checked_lines.push_back(line); } } // perimeter @@ -415,22 +505,37 @@ Issues check_object_stability(const PrintObject *po, const Params ¶ms) { const float region_height = layer_region->layer()->height; const float cross_section = region_height * flow_width * 0.7071f; int id = stability_accs.create_accumulator(base_print_z); - SupportedSegmentAccumulator &acc = stability_accs.access(id); + StabilityAccumulator &acc = stability_accs.access(id); Points points { }; fill->collect_points(points); for (int point_idx = 0; point_idx < int(points.size() - 1); ++point_idx) { Vec2f start = unscaled(points[point_idx]).cast(); - Vec2f next = unscaled(points[point_idx]).cast(); - acc.add_base_extrusion( { start, next }, flow_width, base_print_z, cross_section); + Vec2f next = unscaled(points[point_idx + 1]).cast(); + ExtrusionLine line { start, next }; + line.supported_segment_accumulator_id = id; + acc.add_base_extrusion(line, flow_width, base_print_z, cross_section); + checked_lines.push_back(line); } } // fill } // ex_entity } // region +#ifdef DEBUG_FILES + for (const auto &line : checked_lines) { + Vec3f ecolor = stability_accs.get_emerging_color(line.supported_segment_accumulator_id); + fprintf(eacc, "v %f %f %f %f %f %f\n", line.b[0], + line.b[1], base_print_z, ecolor[0], ecolor[1], ecolor[2]); + + Vec3f fcolor = stability_accs.get_final_color(line.supported_segment_accumulator_id); + fprintf(facc, "v %f %f %f %f %f %f\n", line.b[0], + line.b[1], base_print_z, fcolor[0], fcolor[1], fcolor[2]); + } +#endif DEBUG_FILES + for (size_t layer_idx = 1; layer_idx < po->layer_count(); ++layer_idx) { const Layer *layer = po->layers()[layer_idx]; - prev_layer_lines = LayerLinesDistancer{std::move(checked_lines)}; - checked_lines = std::vector{}; + prev_layer_lines = LayerLinesDistancer { std::move(checked_lines) }; + checked_lines = std::vector { }; float print_z = layer->print_z; for (const LayerRegion *layer_region : layer->regions()) { @@ -452,17 +557,34 @@ Issues check_object_stability(const PrintObject *po, const Params ¶ms) { } // fill } // ex_entity } // region + + check_layer_global_stability(stability_accs, issues, checked_lines, print_z, params); + +#ifdef DEBUG_FILES + for (const auto &line : checked_lines) { + Vec3f ecolor = stability_accs.get_emerging_color(line.supported_segment_accumulator_id); + fprintf(eacc, "v %f %f %f %f %f %f\n", line.b[0], + line.b[1], print_z, ecolor[0], ecolor[1], ecolor[2]); + + Vec3f fcolor = stability_accs.get_final_color(line.supported_segment_accumulator_id); + fprintf(facc, "v %f %f %f %f %f %f\n", line.b[0], + line.b[1], print_z, fcolor[0], fcolor[1], fcolor[2]); + } +#endif DEBUG_FILES } +#ifdef DEBUG_FILES + fclose(eacc); + fclose(facc); +#endif DEBUG_FILES + std::cout << " SUPP: " << issues.supports_nedded.size() << std::endl; return issues; } - #ifdef DEBUG_FILES void debug_export(Issues issues, std::string file_name) { Slic3r::CNumericLocalesSetter locales_setter; - { FILE *fp = boost::nowide::fopen(debug_out_path((file_name + "_supports.obj").c_str()).c_str(), "w"); if (fp == nullptr) { @@ -494,7 +616,6 @@ void debug_export(Issues issues, std::string file_name) { } fclose(fp); } - } #endif From bef26fee2bf593efa694f8756029a5c459acf6f4 Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Mon, 20 Jun 2022 17:43:04 +0200 Subject: [PATCH 027/100] Bugfixing and refactoring --- src/libslic3r/CMakeLists.txt | 3 +- src/libslic3r/SupportableIssuesSearch.cpp | 1500 +++++++---------- src/libslic3r/SupportableIssuesSearch.hpp | 10 +- .../SupportableIssuesSearchRefactoring.cpp | 635 ------- 4 files changed, 591 insertions(+), 1557 deletions(-) delete mode 100644 src/libslic3r/SupportableIssuesSearchRefactoring.cpp diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index e866ac34e5..0a0062a754 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -245,8 +245,7 @@ set(SLIC3R_SOURCES SlicingAdaptive.hpp Subdivide.cpp Subdivide.hpp -# SupportableIssuesSearch.cpp - SupportableIssuesSearchRefactoring.cpp + SupportableIssuesSearch.cpp SupportableIssuesSearch.hpp SupportMaterial.cpp SupportMaterial.hpp diff --git a/src/libslic3r/SupportableIssuesSearch.cpp b/src/libslic3r/SupportableIssuesSearch.cpp index 55d883f0a9..73f89e0d63 100644 --- a/src/libslic3r/SupportableIssuesSearch.cpp +++ b/src/libslic3r/SupportableIssuesSearch.cpp @@ -21,6 +21,40 @@ #endif namespace Slic3r { + +static const size_t NULL_ACC_ID = std::numeric_limits::max(); + +class ExtrusionLine +{ +public: + ExtrusionLine() : + a(Vec2f::Zero()), b(Vec2f::Zero()), len(0.0f) { + } + ExtrusionLine(const Vec2f &_a, const Vec2f &_b) : + a(_a), b(_b), len((_a - _b).norm()) { + } + + float length() { + return (a - b).norm(); + } + + Vec2f a; + Vec2f b; + float len; + + size_t stability_accumulator_id = NULL_ACC_ID; + + static const constexpr int Dim = 2; + using Scalar = Vec2f::Scalar; +}; + +auto get_a(ExtrusionLine &&l) { + return l.a; +} +auto get_b(ExtrusionLine &&l) { + return l.b; +} + namespace SupportableIssues { void Issues::add(const Issues &layer_issues) { @@ -45,615 +79,596 @@ CurledFilament::CurledFilament(const Vec3f &position) : position(position), estimated_height(0.0f) { } -struct Cell { - float volume; - float curled_height; - int accumulator_id = std::numeric_limits::max(); +class LayerLinesDistancer { +private: + std::vector lines; + AABBTreeIndirect::Tree<2, float> tree; + +public: + explicit LayerLinesDistancer(std::vector &&lines) : + lines(lines) { + tree = AABBTreeLines::build_aabb_tree_over_indexed_lines(lines); + } + + // negative sign means inside + float signed_distance_from_lines(const Vec2f &point, size_t &nearest_line_index_out, + Vec2f &nearest_point_out) const { + auto distance = AABBTreeLines::squared_distance_to_indexed_lines(lines, tree, point, nearest_line_index_out, + nearest_point_out); + if (distance < 0) + return std::numeric_limits::infinity(); + + distance = sqrt(distance); + const ExtrusionLine &line = lines[nearest_line_index_out]; + Vec2f v1 = line.b - line.a; + Vec2f v2 = point - line.a; + if ((v1.x() * v2.y()) - (v1.y() * v2.x()) > 0.0) { + distance *= -1; + } + return distance; + } + + const ExtrusionLine& get_line(size_t line_idx) const { + return lines[line_idx]; + } + + const std::vector& get_lines() const { + return lines; + } }; -class CentroidAccumulator { +class StabilityAccumulator { private: - Polygon convex_hull { }; - Points points { }; - public: - Vec3f accumulated_value = Vec3f::Zero(); + Polygon base_convex_hull { }; + Points support_points { }; + Vec3f centroid_accumulator = Vec3f::Zero(); float accumulated_volume { }; float base_area { }; - float additional_supports_adhesion { }; float base_height { }; - explicit CentroidAccumulator(float base_height) : +public: + explicit StabilityAccumulator(float base_height) : base_height(base_height) { } - const Polygon& base_hull() { - if (this->convex_hull.empty()) { - this->convex_hull = Geometry::convex_hull(this->points); + void add_base_extrusion(const ExtrusionLine &line, float width, float print_z, float mm3_per_mm) { + base_area += line.len * width; + support_points.push_back(Point::new_scale(line.a)); + support_points.push_back(Point::new_scale(line.b)); + base_convex_hull.clear(); + add_extrusion(line, print_z, mm3_per_mm); + } + + void add_support_point(const Point &position, float area) { + support_points.push_back(position); + base_convex_hull.clear(); + base_area += area; + } + + void add_extrusion(const ExtrusionLine &line, float print_z, float mm3_per_mm) { + float volume = line.len * mm3_per_mm; + accumulated_volume += volume; + Vec2f center = (line.a + line.b) / 2.0f; + centroid_accumulator += volume * Vec3f(center.x(), center.y(), print_z); + } + + Vec3f get_centroid() const { + return centroid_accumulator / accumulated_volume; + } + + float get_base_area() const { + return base_area; + } + float get_base_height() const { + return base_height; + } + float get_accumulated_volume() const { + return accumulated_volume; + } + + const Polygon& segment_base_hull() { + if (this->base_convex_hull.empty()) { + this->base_convex_hull = Geometry::convex_hull(this->support_points); } - return this->convex_hull; + return this->base_convex_hull; } - void add_base_points(const Points &other) { - this->points.insert(this->points.end(), other.begin(), other.end()); - convex_hull.clear(); + const Points& get_support_points() { + return support_points; } - const Points& get_base_points() { - return this->points; + void add_from(const StabilityAccumulator &acc) { + this->support_points.insert(this->support_points.end(), acc.support_points.begin(), + acc.support_points.end()); + base_convex_hull.clear(); + this->centroid_accumulator += acc.centroid_accumulator; + this->accumulated_volume += acc.accumulated_volume; + this->base_area += acc.base_area; } }; -struct CentroidAccumulators { - std::unordered_map mapping; - std::vector acccumulators; +struct StabilityAccumulators { +private: + size_t next_id = 0; + std::unordered_map mapping; + std::vector acccumulators; - explicit CentroidAccumulators(size_t reserve_count) { - acccumulators.reserve(reserve_count); - } - - CentroidAccumulator& create_accumulator(int id, float base_height) { - mapping[id] = acccumulators.size(); - acccumulators.push_back(CentroidAccumulator { base_height }); - return this->access(id); - } - - CentroidAccumulator& access(int id) { - return acccumulators[mapping[id]]; - } - - void merge_to(int from_id, int to_id) { - CentroidAccumulator &from_acc = this->access(from_id); - CentroidAccumulator &to_acc = this->access(to_id); + void merge_to(size_t from_id, size_t to_id) { + StabilityAccumulator &from_acc = this->access(from_id); + StabilityAccumulator &to_acc = this->access(to_id); if (&from_acc == &to_acc) { return; } - to_acc.accumulated_value += from_acc.accumulated_value; - to_acc.accumulated_volume += from_acc.accumulated_volume; - to_acc.add_base_points(from_acc.get_base_points()); - to_acc.base_area += from_acc.base_area; + to_acc.add_from(from_acc); mapping[from_id] = mapping[to_id]; - from_acc = CentroidAccumulator { 0.0f }; + from_acc = StabilityAccumulator { 0.0f }; } -}; - -class BalanceDistributionGrid { - - static constexpr float cell_height = scale_(0.3f); - - Vec3crd cell_size { }; - - Vec3crd global_origin { }; - Vec3crd global_size { }; - Vec3i global_cell_count { }; - - int local_z_index_offset { }; - int local_z_cell_count { }; - std::vector cells { }; public: + StabilityAccumulators() = default; - BalanceDistributionGrid() = default; - - void init(const PrintObject *po, size_t layer_idx_begin, size_t layer_idx_end) { - Vec2crd size_half = po->size().head<2>().cwiseQuotient(Vec2crd(2, 2)) + Vec2crd::Ones(); - Vec3crd min = Vec3crd(-size_half.x(), -size_half.y(), 0); - Vec3crd max = Vec3crd(size_half.x(), size_half.y(), po->height()); - - cell_size = Vec3crd { int(cell_height * 2), int(cell_height * 2), int(cell_height) }; - assert(cell_size.x() == cell_size.y()); - - global_origin = min; - global_size = max - min; - global_cell_count = global_size.cwiseQuotient(cell_size) + Vec3i::Ones(); - - coord_t local_min_z = scale_(po->layers()[layer_idx_begin]->print_z); - coord_t local_max_z = scale_(po->layers()[layer_idx_end > 0 ? layer_idx_end - 1 : 0]->print_z); - int local_min_z_index = local_min_z / cell_size.z(); - int local_max_z_index = local_max_z / cell_size.z() + 1; - - local_z_index_offset = local_min_z_index; - local_z_cell_count = local_max_z_index + 1 - local_min_z_index; - - cells.resize(local_z_cell_count * global_cell_count.y() * global_cell_count.x()); + int create_accumulator(float base_height) { + size_t id = next_id; + next_id++; + mapping[id] = acccumulators.size(); + acccumulators.push_back(StabilityAccumulator { base_height }); + return id; } - Vec3i to_global_cell_coords(const Vec3i &local_cell_coords) const { - return local_cell_coords + local_z_index_offset * Vec3i::UnitZ(); + StabilityAccumulator& access(size_t id) { + return acccumulators[mapping[id]]; } - Vec3i to_local_cell_coords(const Vec3i &global_cell_coords) const { - return global_cell_coords - local_z_index_offset * Vec3i::UnitZ(); - } - - Vec3i to_global_cell_coords(const Point &p, float print_z) const { - Vec3crd position = Vec3crd { p.x(), p.y(), int(scale_(print_z)) }; - Vec3i cell_coords = (position - this->global_origin).cwiseQuotient(this->cell_size); - return cell_coords; - } - - Vec3i to_global_cell_coords(const Vec3f &position) const { - Vec3crd scaled_position = scaled(position); - Vec3i cell_coords = (scaled_position - this->global_origin).cwiseQuotient(this->cell_size); - return cell_coords; - } - - Vec3i to_local_cell_coords(const Point &p, float print_z) const { - Vec3i cell_coords = this->to_global_cell_coords(p, print_z); - return this->to_local_cell_coords(cell_coords); - } - - size_t to_cell_index(const Vec3i &local_cell_coords) const { - assert(local_cell_coords.x() >= 0); - assert(local_cell_coords.x() < global_cell_count.x()); - assert(local_cell_coords.y() >= 0); - assert(local_cell_coords.y() < global_cell_count.y()); - assert(local_cell_coords.z() >= 0); - assert(local_cell_coords.z() < local_z_cell_count); - - return local_cell_coords.z() * global_cell_count.x() * global_cell_count.y() - + local_cell_coords.y() * global_cell_count.x() - + local_cell_coords.x(); - } - - Vec3crd get_cell_center(const Vec3i &global_cell_coords) const { - return global_origin + global_cell_coords.cwiseProduct(this->cell_size) - + this->cell_size.cwiseQuotient(Vec3crd(2, 2, 2)); - } - - Cell& access_cell(const Point &p, float print_z) { - return cells[this->to_cell_index(to_local_cell_coords(p, print_z))]; - } - - Cell& access_cell(const Vec3f &unscaled_position) { - return cells[this->to_cell_index(this->to_local_cell_coords(this->to_global_cell_coords(unscaled_position)))]; - } - - Cell& access_cell(const Vec3i &local_cell_coords) { - return cells[this->to_cell_index(local_cell_coords)]; - } - - const Cell& access_cell(const Vec3i &local_cell_coords) const { - return cells[this->to_cell_index(local_cell_coords)]; - } - - void distribute_edge(const Point &p1, const Point &p2, float print_z, float unscaled_width, float unscaled_height) { - Vec2d dir = (p2 - p1).cast(); - double length = dir.norm(); - if (length < 0.1) { + void merge_accumulators(size_t from_id, size_t to_id) { + if (from_id == NULL_ACC_ID || to_id == NULL_ACC_ID) { return; } - double step_size = this->cell_size.x() / 2.0; - - float diameter = unscaled_height * unscaled_width * 0.7071f; // constant to simulate somewhat elliptical shape (1/sqrt(2)) - - double distributed_length = 0; - while (distributed_length < length) { - double next_len = std::min(length, distributed_length + step_size); - double current_dist_payload = next_len - distributed_length; - - Point location = p1 + ((next_len / length) * dir).cast(); - float payload = unscale(current_dist_payload) * diameter; - this->access_cell(location, print_z).volume += payload; - - distributed_length = next_len; + StabilityAccumulator &from_acc = this->access(from_id); + StabilityAccumulator &to_acc = this->access(to_id); + if (&from_acc == &to_acc) { + return; } - } - - void merge(const BalanceDistributionGrid &other) { - int z_start = std::max(local_z_index_offset, other.local_z_index_offset); - int z_end = std::min(local_z_index_offset + local_z_cell_count, - other.local_z_index_offset + other.local_z_cell_count); - - for (int x = 0; x < global_cell_count.x(); ++x) { - for (int y = 0; y < global_cell_count.y(); ++y) { - for (int z = z_start; z < z_end; ++z) { - Vec3i global_coords { x, y, z }; - Vec3i local_coords = this->to_local_cell_coords(global_coords); - Vec3i other_local_coords = other.to_local_cell_coords(global_coords); - this->access_cell(local_coords).volume += other.access_cell(other_local_coords).volume; - } - } - } - } - - void analyze(Issues &issues, const Params ¶ms) { - const auto validate_xy_coords = [&](const Vec2i &local_coords) { - return local_coords.x() >= 0 && local_coords.y() >= 0 && local_coords.x() < this->global_cell_count.x() - && local_coords.y() < this->global_cell_count.y(); - }; - CentroidAccumulators accumulators(issues.supports_nedded.size() + 4); // just an estimation; one for each support point from prev step, and 4 for the base - auto custom_comparator = [](const Vec2i &left, const Vec2i &right) { - if (left.x() == right.x()) { - return left.y() < right.y(); - } - return left.x() < right.x(); - }; - - //initialization of the bed accumulators from the bed cells (first layer of grid) - int next_accumulator_id = -1; // accumulators from the bed have negative ids, starting with -1. Accumulators generated by support points have nonegative ids, starting with 0, and sorted by height - // The reason is, that during merging of accumulators (when they meet at the upper cells), algorithm always keeps the one with the lower id (so bed is preffered), and discards the other - std::set coords_to_check(custom_comparator); // set of coords to check for the current accumulator (search based on connectivity) - for (int y = 0; y < global_cell_count.y(); ++y) { - for (int x = 0; x < global_cell_count.x(); ++x) { - Cell &origin_cell = this->access_cell(Vec3i(x, y, 0)); - if (origin_cell.volume > 0 && origin_cell.accumulator_id == std::numeric_limits::max()) { // cell has volume and no accumulator assigned yet - CentroidAccumulator &acc = accumulators.create_accumulator(next_accumulator_id, 0); // create new accumulator, height is 0, because we are on the bed - coords_to_check.clear(); - coords_to_check.insert(Vec2i(x, y)); - while (!coords_to_check.empty()) { // insert the origin cell coords in to the set, and search all connected cells with volume and without assigned accumulator, assign them to acc - Vec2i current_coords = *coords_to_check.begin(); - coords_to_check.erase(coords_to_check.begin()); - if (!validate_xy_coords(current_coords)) { // invalid coords, drop - continue; - } - Cell &cell = this->access_cell(Vec3i(current_coords.x(), current_coords.y(), 0)); - if (cell.volume <= 0 || cell.accumulator_id != std::numeric_limits::max()) { // empty cell or already assigned, drop - continue; - } - cell.accumulator_id = next_accumulator_id; // update cell accumulator id, update the accumulator with the new cell data, add neighbours to queue - Vec3crd cell_center = this->get_cell_center( - Vec3i(current_coords.x(), current_coords.y(), local_z_index_offset)); - acc.add_base_points( { Point(cell_center.head<2>()) }); - acc.accumulated_value += unscale(cell_center).cast() * cell.volume; - acc.accumulated_volume += cell.volume; - - for (int y_offset = -1; y_offset <= 1; ++y_offset) { - for (int x_offset = -1; x_offset <= 1; ++x_offset) { - if (y_offset != 0 || x_offset != 0) { - coords_to_check.insert( - Vec2i(current_coords.x() + x_offset, current_coords.y() + y_offset)); - } - } - } - } - next_accumulator_id--; - //base area is separated from the base convex hull - bed accumulators are initialized with convex hull area (TODO compute from number of covered cells instead ) - // but support points are initialized with constant, and during merging, the base_areas are added. - acc.base_area = unscale(unscale(acc.base_hull().area())); //apply unscale 2x, it has units of area - } - } - } - - // sort support points by height, so that their accumulators ids are also sorted by height - std::sort(issues.supports_nedded.begin(), issues.supports_nedded.end(), - [](const SupportPoint &left, const SupportPoint &right) { - return left.position.z() < right.position.z(); - }); - for (int index = 0; index < int(issues.supports_nedded.size()); ++index) { - Vec3i local_coords = this->to_local_cell_coords( - this->to_global_cell_coords(issues.supports_nedded[index].position)); - this->access_cell(local_coords).accumulator_id = index; // assign accumulator id (in case that multiple support points fall into the same cell, they are just overriden) - CentroidAccumulator &acc = accumulators.create_accumulator(index, - issues.supports_nedded[index].position.z()); - acc.add_base_points( { Point(scaled(Vec2f(issues.supports_nedded[index].position.head<2>()))) }); - acc.base_area = params.support_points_interface_area; - } - - // add curling data to each cell - for (const CurledFilament &curling : issues.curling_up) { - this->access_cell(curling.position).curled_height += curling.estimated_height; - } - - // keep map of modified accumulator for each layer, so that discarded accumulators are not further checked for stability - // the value of the map is list of cells with curling, that should be further checked for pressure stability with repsect to the accumulator - std::unordered_map> modified_acc_ids; - // At the end of the layer check, accumulators are further filtered, since merging causes that single accumulator can have mutliple ids. - // But each accumulator should be checked only once. - std::unordered_map> filtered_active_accumulators; - modified_acc_ids.reserve(issues.supports_nedded.size() + 1); - - // For each grid layer, cells are added to the accumulators and all active accumulators are checked of stability. - for (int z = 1; z < local_z_cell_count; ++z) { - std::cout << "current z: " << z << std::endl; - - modified_acc_ids.clear(); - - for (int x = 0; x < global_cell_count.x(); ++x) { - for (int y = 0; y < global_cell_count.y(); ++y) { - Cell ¤t = this->access_cell(Vec3i(x, y, z)); - // distribute islands info - look for neighbours under the cell, and pick the smallest accumulator id - // also gather all ids, they will be merged to the smallest id accumualtor - if (current.volume > 0 && current.accumulator_id == std::numeric_limits::max()) { - int min_accumulator_id_found = std::numeric_limits::max(); - std::unordered_set ids_to_merge { }; - for (int y_offset = -2; y_offset <= 2; ++y_offset) { - for (int x_offset = -2; x_offset <= 2; ++x_offset) { - if (validate_xy_coords(Vec2i(x + x_offset, y + y_offset))) { - Cell &under = this->access_cell(Vec3i(x + x_offset, y + y_offset, z - 1)); - if (under.accumulator_id < min_accumulator_id_found) { - min_accumulator_id_found = under.accumulator_id; - } - ids_to_merge.insert(under.accumulator_id); - } - } - } - // assign accumulator and update its info - if (min_accumulator_id_found < std::numeric_limits::max()) { // accumulator id found - ids_to_merge.erase(std::numeric_limits::max()); - ids_to_merge.erase(min_accumulator_id_found); - current.accumulator_id = min_accumulator_id_found; //assign accumualtor id to the cell - for (auto id : ids_to_merge) { - accumulators.merge_to(id, min_accumulator_id_found); //merge other ids to the smallest id - } - //update the acc with new point, and store in the modified accumulators map - CentroidAccumulator &acc = accumulators.access(min_accumulator_id_found); - acc.accumulated_value += current.volume - * unscale(this->get_cell_center(this->to_global_cell_coords(Vec3i(x, y, z)))).cast< - float>(); - acc.accumulated_volume += current.volume; - modified_acc_ids.emplace(min_accumulator_id_found, std::vector { }); - } - } - - // distribute curling (add curling from neighbours under, but also decrease but some factor) - if (current.volume > 0) { - float curled_height = 0; - for (int y_offset = -2; y_offset <= 2; ++y_offset) { - for (int x_offset = -2; x_offset <= 2; ++x_offset) { - if (validate_xy_coords(Vec2i(x + x_offset, y + y_offset))) { - Cell &under = this->access_cell(Vec3i(x + x_offset, y + y_offset, z - 1)); - curled_height = std::max(curled_height, under.curled_height); - } - } - } - current.curled_height += std::max(0.0f, - float(curled_height - unscaled(this->cell_size.z()) / 1.5f)); - current.curled_height = std::min(current.curled_height, - float(unscaled(this->cell_size.z()) * 2.0f)); - - if (current.curled_height / unscaled(this->cell_size.z()) > 1.5f) { // just a magic threshold number. - modified_acc_ids[current.accumulator_id].push_back( { x, y }); - } - } - } - } - - //all cells of the grid layer checked, now further filter the modified accumulators, because multiple ids can point to the same acc - filtered_active_accumulators.clear(); - for (const auto &pair : modified_acc_ids) { - CentroidAccumulator *acc = &accumulators.access(pair.first); - filtered_active_accumulators[acc].insert(filtered_active_accumulators[acc].end(), - pair.second.begin(), - pair.second.end()); - } - - check_accumulators_stability(z, accumulators, filtered_active_accumulators, issues, params); - } - } -private: - void check_accumulators_stability(int z, CentroidAccumulators &accumulators, - std::unordered_map> filtered_active_accumulators, - Issues &issues, const Params ¶ms) const { - - for (const auto &pair : filtered_active_accumulators) { - std::cout << "Z: " << z << std::endl; - CentroidAccumulator &acc = *pair.first; - Vec3f centroid = acc.accumulated_value / acc.accumulated_volume; - Point base_centroid = acc.base_hull().centroid(); - Vec3f base_centroid_3d { }; - base_centroid_3d << unscale(base_centroid).cast(), acc.base_height; - - std::cout << "acc.accumulated_value : " << acc.accumulated_value.x() << " " - << acc.accumulated_value.y() << " " << acc.accumulated_value.z() << std::endl; - std::cout << "acc.accumulated_volume : " << acc.accumulated_volume << std::endl; - std::cout << "centroid: " << centroid.x() << " " << centroid.y() << " " << centroid.z() - << std::endl; - std::cout << "base_centroid_3d: " << base_centroid_3d.x() << " " << base_centroid_3d.y() << " " - << base_centroid_3d.z() - << std::endl; - - // find the cell that is furthest from the base centroid ( its a heurstic to find a possible problems with balance without checking all layer cells) - //TODO better result are if first pivot is chosen as the closest point of the convex hull to the base centroid, and then cell furthest in the direction defined by - // the vector from base centroid to this pivot is taken. - double max_dist_sqr = 0; - Vec3f suspicious_point = centroid; - Vec2i coords = Vec2i(0, 0); - for (int y = 0; y < global_cell_count.y(); ++y) { - for (int x = 0; x < global_cell_count.x(); ++x) { - const Cell &cell = this->access_cell(Vec3i(x, y, z)); - if (cell.accumulator_id != std::numeric_limits::max() && - &accumulators.access(cell.accumulator_id) == &acc) { - Vec3f cell_center = - unscale(this->get_cell_center(this->to_global_cell_coords(Vec3i(x, y, z)))).cast< - float>(); - float dist_sq = (cell_center - base_centroid_3d).squaredNorm(); - if (dist_sq > max_dist_sqr) { - max_dist_sqr = dist_sq; - suspicious_point = cell_center; - coords = Vec2i(x, y); - } - } - } - } - - // for the suspicious point, add movement force in xy (bed sliding, it is assumed that the worst direction is taken, for simplicity) - float xy_movement_force = acc.accumulated_volume * params.filament_density - * params.max_acceleration; - - std::cout << "xy_movement_force: " << xy_movement_force << std::endl; - - // also add weight (weight is the small factor, because the materials are very light. The weight torque will be computed much higher then what is real, - //since it does not push in the suspicoius point, but in centroid. Its approximation) - float weight = acc.accumulated_volume * params.filament_density * params.gravity_constant; - - std::cout << "weight: " << weight << std::endl; - - float force = this->check_point_stability_under_pressure(acc, base_centroid, suspicious_point, - xy_movement_force + weight + params.tolerable_extruder_conflict_force, - params); - if (force > 0) { - this->add_suppport_point(Vec3i(coords.x(), coords.y(), z), force, acc, issues, params); - } - - for (const Vec2i &cell : pair.second) { - Vec3f pressure_point = unscale( - this->get_cell_center( - this->to_global_cell_coords(Vec3i(cell.x(), cell.y(), z)))).cast(); - float force = this->check_point_stability_under_pressure(acc, base_centroid, pressure_point, - params.max_curled_conflict_extruder_force, //TODO add linear scaling of the extruder force based on the curled height (but current data about curled height are kind of unreliable in scale) - params); - if (force > 0) { - this->add_suppport_point(Vec3i(cell.x(), cell.y(), z), force, acc, issues, params); - } - } - } - } - - float check_point_stability_under_pressure(CentroidAccumulator &acc, const Point &base_centroid, - const Vec3f &pressure_point, float pressure_force, const Params ¶ms) const { - Point pressure_base_projection = Point(scaled(Vec2f(pressure_point.head<2>()))); - Point pivot; - double distance_scaled_sq = std::numeric_limits::max(); - bool inside = true; - // find pivot, which is the closest point of the accumulator base hull to pressure point (if the object should fall, it would be over this point) - if (acc.base_hull().points.size() == 1) { - pivot = acc.base_hull().points[0]; - distance_scaled_sq = (pivot - pressure_base_projection).squaredNorm(); - inside = distance_scaled_sq < params.support_points_interface_area; - } else { - for (Line line : acc.base_hull().lines()) { - Point closest_point; - double dist_sq = line.distance_to_squared(pressure_base_projection, &closest_point); - if (dist_sq < distance_scaled_sq) { - pivot = closest_point; - distance_scaled_sq = dist_sq; - } - if ((pressure_base_projection - closest_point).cast().dot(line.normal().cast()) - > 0) { - inside = false; - } - } - } -// float embedded_distance = unscaled(sqrt(distance_scaled_sq)); - float base_center_pivot_distance = float(unscale(Vec2crd(base_centroid - pivot)).norm()); - Vec3f pivot_3d; - pivot_3d << unscale(pivot).cast(), acc.base_height; - - //sticking force estimated from the base area and support points - float sticking_force = acc.base_area - * (acc.base_height == 0 ? params.base_adhesion : params.support_adhesion); - - std::cout << "sticking force: " << sticking_force << std::endl; - std::cout << "pressure force: " << pressure_force << std::endl; - float sticking_torque = sticking_force * base_center_pivot_distance; // sticking torque is computed from the distance to the centroid - - float pressure_arm = inside ? pressure_point.z() - pivot_3d.z() : (pressure_point - pivot_3d).norm(); // pressure arm is again higher then in reality, - // since it assumes the worst direction of the pressure force (perpendicular to the vector between pivot and pressure point) - float pressure_torque = pressure_arm * pressure_force; - - std::cout << "sticking_torque: " << sticking_torque << std::endl; - std::cout << "pressure_torque: " << pressure_torque << std::endl; - if (sticking_torque < pressure_torque) { - return pressure_force; - } else { - return 0.0f; - } - } - - void add_suppport_point(const Vec3i &local_coords, float expected_force, CentroidAccumulator &acc, Issues &issues, - const Params ¶ms) const { - //add support point - but first finding the lowest full cell is needed, since putting support point to the current cell may end up inside the model, - // essentially doing nothing. - int final_height_coords = local_coords.z(); - while (final_height_coords > 0 - && this->access_cell( - this->to_global_cell_coords(Vec3i(local_coords.x(), local_coords.y(), final_height_coords))).volume - > 0) { - final_height_coords--; - } - Vec3f support_point = - unscale( - this->get_cell_center( - this->to_global_cell_coords( - Vec3i(local_coords.x(), local_coords.y(), final_height_coords)))).cast< - float>(); - - std::cout << " new support point: " << support_point.x() << " | " << support_point.y() << " | " - << support_point.z() << std::endl; - std::cout << " expected_force: " << expected_force << std::endl; - - issues.supports_nedded.emplace_back(support_point, expected_force); - acc.add_base_points( { Point::new_scale(Vec2f(support_point.head<2>())) }); - acc.base_area += params.support_points_interface_area; + to_acc.add_from(from_acc); + mapping[from_id] = mapping[to_id]; + from_acc = StabilityAccumulator { 0.0f }; } #ifdef DEBUG_FILES -public: - void debug_export() const { - Slic3r::CNumericLocalesSetter locales_setter; - { - FILE *volume_grid_file = boost::nowide::fopen(debug_out_path("volume_grid.obj").c_str(), "w"); - FILE *islands_grid_file = boost::nowide::fopen(debug_out_path("islands_grid.obj").c_str(), "w"); - FILE *curling_grid_file = boost::nowide::fopen(debug_out_path("curling_grid.obj").c_str(), "w"); - - if (volume_grid_file == nullptr || islands_grid_file == nullptr || curling_grid_file == nullptr) { - BOOST_LOG_TRIVIAL(error) - << "Debug files: Couldn't open debug file for writing, destination: " << debug_out_path(""); - return; - } - - float max_volume = 0; - int min_island_id = 0; - int max_island_id = 0; - float max_curling_height = 0.5f; - - for (int x = 0; x < global_cell_count.x(); ++x) { - for (int y = 0; y < global_cell_count.y(); ++y) { - for (int z = 0; z < local_z_cell_count; ++z) { - const Cell &cell = access_cell(Vec3i(x, y, z)); - max_volume = std::max(max_volume, cell.volume); - if (cell.accumulator_id != std::numeric_limits::max()) { - min_island_id = std::min(min_island_id, cell.accumulator_id); - max_island_id = std::max(max_island_id, cell.accumulator_id); - } -// max_curling_height = std::max(max_curling_height, cell.curled_height); - } - } - } - - for (int x = 0; x < global_cell_count.x(); ++x) { - for (int y = 0; y < global_cell_count.y(); ++y) { - for (int z = 0; z < local_z_cell_count; ++z) { - Vec3f center = unscale(get_cell_center(to_global_cell_coords(Vec3i { x, y, z }))).cast< - float>(); - const Cell &cell = access_cell(Vec3i(x, y, z)); - if (cell.volume != 0) { - auto volume_color = value_to_rgbf(0, cell.volume, cell.volume); - fprintf(volume_grid_file, "v %f %f %f %f %f %f\n", center(0), center(1), center(2), - volume_color.x(), volume_color.y(), volume_color.z()); - } - if (cell.accumulator_id != std::numeric_limits::max()) { - auto island_color = value_to_rgbf(min_island_id, max_island_id + 1, cell.accumulator_id); - fprintf(islands_grid_file, "v %f %f %f %f %f %f\n", center(0), center(1), - center(2), - island_color.x(), island_color.y(), island_color.z()); - } - if (cell.curled_height > 0) { - auto curling_color = value_to_rgbf(0, max_curling_height, cell.curled_height); - fprintf(curling_grid_file, "v %f %f %f %f %f %f\n", center(0), center(1), - center(2), - curling_color.x(), curling_color.y(), curling_color.z()); - } - } - } - } - - fclose(volume_grid_file); - fclose(islands_grid_file); - fclose(curling_grid_file); + Vec3f get_emerging_color(size_t id) { + if (mapping.find(id) == mapping.end()) { + std::cerr << " ERROR: uknown accumulator ID: " << id << std::endl; + return Vec3f(1.0f, 1.0f, 1.0f); } + + size_t pseudornd = ((id + 127) * 33331 + 6907) % 13; + return value_to_rgbf(0.0f, 13.0f, float(pseudornd)); + } + + Vec3f get_final_color(size_t id) { + if (mapping.find(id) == mapping.end()) { + std::cerr << " ERROR: uknown accumulator ID: " << id << std::endl; + return Vec3f(1.0f, 1.0f, 1.0f); + } + + size_t pseudornd = ((mapping[id] + 127) * 33331 + 6907) % 13; + return value_to_rgbf(0.0f, 13.0f, float(pseudornd)); } #endif -} -; +}; -namespace Impl { +float get_flow_width(const LayerRegion *region, ExtrusionRole role) { + switch (role) { + case ExtrusionRole::erBridgeInfill: + return region->flow(FlowRole::frExternalPerimeter).width(); + case ExtrusionRole::erExternalPerimeter: + return region->flow(FlowRole::frExternalPerimeter).width(); + case ExtrusionRole::erGapFill: + return region->flow(FlowRole::frInfill).width(); + case ExtrusionRole::erPerimeter: + return region->flow(FlowRole::frPerimeter).width(); + case ExtrusionRole::erSolidInfill: + return region->flow(FlowRole::frSolidInfill).width(); + case ExtrusionRole::erInternalInfill: + return region->flow(FlowRole::frInfill).width(); + case ExtrusionRole::erTopSolidInfill: + return region->flow(FlowRole::frTopSolidInfill).width(); + default: + return region->flow(FlowRole::frPerimeter).width(); + } +} + +struct ExtrusionPropertiesAccumulator { + float distance = 0; //accumulated distance + float curvature = 0; //accumulated signed ccw angles + float max_curvature = 0; //max absolute accumulated value + + void add_distance(float dist) { + distance += dist; + } + + void add_angle(float ccw_angle) { + curvature += ccw_angle; + max_curvature = std::max(max_curvature, std::abs(curvature)); + } + + void reset() { + distance = 0; + curvature = 0; + max_curvature = 0; + } +}; + +void check_extrusion_entity_stability(const ExtrusionEntity *entity, + StabilityAccumulators &stability_accs, + Issues &issues, + std::vector &checked_lines, + float print_z, + const LayerRegion *layer_region, + const LayerLinesDistancer &prev_layer_lines, + const Params ¶ms) { + + if (entity->is_collection()) { + for (const auto *e : static_cast(entity)->entities) { + check_extrusion_entity_stability(e, stability_accs, issues, checked_lines, print_z, layer_region, + prev_layer_lines, + params); + } + } else { //single extrusion path, with possible varying parameters + const auto to_vec3f = [print_z](const Point &point) { + Vec2f tmp = unscale(point).cast(); + return Vec3f(tmp.x(), tmp.y(), print_z); + }; + Points points { }; + entity->collect_points(points); + std::vector lines; + lines.reserve(points.size() * 1.5); + lines.emplace_back(unscaled(points[0]).cast(), unscaled(points[0]).cast()); + for (int point_idx = 0; point_idx < int(points.size() - 1); ++point_idx) { + Vec2f start = unscaled(points[point_idx]).cast(); + Vec2f next = unscaled(points[point_idx + 1]).cast(); + Vec2f v = next - start; // vector from next to current + float dist_to_next = v.norm(); + v.normalize(); + int lines_count = int(std::ceil(dist_to_next / params.bridge_distance)); + float step_size = dist_to_next / lines_count; + for (int i = 0; i < lines_count; ++i) { + Vec2f a(start + v * (i * step_size)); + Vec2f b(start + v * ((i + 1) * step_size)); + lines.emplace_back(a, b); + } + } + + size_t current_stability_acc = NULL_ACC_ID; + ExtrusionPropertiesAccumulator bridging_acc { }; + bridging_acc.add_distance(params.bridge_distance + 1.0f); // Initialise unsupported distance with larger than tolerable distance -> + // -> it prevents extruding perimeter start and short loops into air. + const float flow_width = get_flow_width(layer_region, entity->role()); + const float max_allowed_dist_from_prev_layer = flow_width; + + for (size_t line_idx = 0; line_idx < lines.size(); ++line_idx) { + ExtrusionLine ¤t_line = lines[line_idx]; + Point current = Point::new_scale(current_line.b); + float mm3_per_mm = float(entity->min_mm3_per_mm()); + + float curr_angle = 0; + if (line_idx + 1 < lines.size()) { + const Vec2f v1 = current_line.b - current_line.a; + const Vec2f v2 = lines[line_idx + 1].b - lines[line_idx + 1].a; + curr_angle = angle(v1, v2); + } + bridging_acc.add_angle(curr_angle); + + size_t nearest_line_idx; + Vec2f nearest_point; + float dist_from_prev_layer = prev_layer_lines.signed_distance_from_lines(current_line.b, nearest_line_idx, + nearest_point); + if (dist_from_prev_layer < max_allowed_dist_from_prev_layer) { + const ExtrusionLine &nearest_line = prev_layer_lines.get_line(nearest_line_idx); + size_t acc_id = nearest_line.stability_accumulator_id; + stability_accs.merge_accumulators(std::max(acc_id, current_stability_acc), + std::min(acc_id, current_stability_acc)); + current_stability_acc = std::min(acc_id, current_stability_acc); + current_line.stability_accumulator_id = current_stability_acc; + stability_accs.access(current_stability_acc).add_extrusion(current_line, print_z, mm3_per_mm); + bridging_acc.reset(); + // TODO curving here + } else { + bridging_acc.add_distance(current_line.len); + if (current_stability_acc == NULL_ACC_ID) { + current_stability_acc = stability_accs.create_accumulator(print_z); + } + StabilityAccumulator ¤t_segment = stability_accs.access(current_stability_acc); + current_line.stability_accumulator_id = current_stability_acc; + current_segment.add_extrusion(current_line, print_z, mm3_per_mm); + if (bridging_acc.distance // if unsupported distance is larger than bridge distance linearly decreased by curvature, enforce supports. + > params.bridge_distance + / (1.0f + (bridging_acc.max_curvature + * params.bridge_distance_decrease_by_curvature_factor / PI))) { + current_segment.add_support_point(current, params.support_points_interface_area); + issues.supports_nedded.emplace_back(to_vec3f(current), 1.0); + bridging_acc.reset(); + } + } + } + checked_lines.insert(checked_lines.end(), lines.begin(), lines.end()); + } +} + +void check_layer_global_stability(StabilityAccumulators &stability_accs, + Issues &issues, + const std::vector &checked_lines, + float print_z, + const Params ¶ms) { + std::unordered_map> layer_accs_lines; + for (size_t i = 0; i < checked_lines.size(); ++i) { + layer_accs_lines[&stability_accs.access(checked_lines[i].stability_accumulator_id)].push_back(i); + } + + for (auto &acc_lines : layer_accs_lines) { + StabilityAccumulator *acc = acc_lines.first; + Vec3f centroid = acc->get_centroid(); + Vec2f hull_centroid = unscaled(acc->segment_base_hull().centroid()).cast(); + std::vector hull_lines; + for (const Line &line : acc->segment_base_hull().lines()) { + Vec2f start = unscaled(line.a).cast(); + Vec2f next = unscaled(line.b).cast(); + hull_lines.push_back( { start, next }); + } + if (hull_lines.empty()) { + if (acc->get_support_points().empty()) { + acc->add_support_point(Point::new_scale(checked_lines[acc_lines.second[0]].a), + params.support_points_interface_area); + issues.supports_nedded.emplace_back(to_3d(checked_lines[acc_lines.second[0]].a, print_z), 1.0); + } + hull_lines.push_back( { unscaled(acc->get_support_points()[0]).cast(), + unscaled(acc->get_support_points()[0]).cast() }); + hull_centroid = unscaled(acc->get_support_points()[0]).cast(); + } + + LayerLinesDistancer hull_distancer(std::move(hull_lines)); + + float sticking_force = acc->get_base_area() + * (acc->get_base_height() == 0 ? params.base_adhesion : params.support_adhesion); + float weight = acc->get_accumulated_volume() * params.filament_density * params.gravity_constant; + + for (size_t line_idx : acc_lines.second) { + const ExtrusionLine &line = checked_lines[line_idx]; + + Vec3f extruder_pressure_direction = to_3d(Vec2f(line.b - line.a), 0.0f).normalized(); + Vec2f pivot_site_search = line.b + extruder_pressure_direction.head<2>() * 1000.0f; + extruder_pressure_direction.z() = -0.1f; + extruder_pressure_direction.normalize(); + + size_t nearest_line_idx; + Vec2f pivot; + hull_distancer.signed_distance_from_lines(pivot_site_search, nearest_line_idx, pivot); + + float sticking_arm = (pivot - hull_centroid).norm(); + float sticking_torque = sticking_arm * sticking_force; + + std::cout << "sticking_arm: " << sticking_arm << std::endl; + std::cout << "sticking_torque: " << sticking_torque << std::endl; + + float weight_arm = (pivot - centroid.head<2>()).norm(); + float weight_torque = weight_arm * weight; + + std::cout << "weight_arm: " << sticking_arm << std::endl; + std::cout << "weight_torque: " << weight_torque << std::endl; + + float bed_movement_arm = centroid.z() - acc->get_base_height(); + float bed_movement_force = params.max_acceleration * weight; + float bed_movement_torque = bed_movement_force * bed_movement_arm; + + std::cout << "bed_movement_arm: " << bed_movement_arm << std::endl; + std::cout << "bed_movement_torque: " << bed_movement_torque << std::endl; + + float conflict_torque_arm = (to_3d(Vec2f(pivot - line.b), print_z).cross( + extruder_pressure_direction)).norm(); + float extruder_conflict_torque = params.tolerable_extruder_conflict_force * conflict_torque_arm; + + std::cout << "conflict_torque_arm: " << conflict_torque_arm << std::endl; + std::cout << "extruder_conflict_torque: " << extruder_conflict_torque << std::endl; + + float total_torque = bed_movement_torque + extruder_conflict_torque - weight_torque - sticking_torque; + + std::cout << "total_torque: " << total_torque << " printz: " << print_z << std::endl; + + if (total_torque > 0) { + acc->add_support_point(Point::new_scale(line.b), params.support_points_interface_area); + issues.supports_nedded.emplace_back(to_3d(line.b, print_z), extruder_conflict_torque - sticking_torque); + } + + } + } +} + +Issues check_object_stability(const PrintObject *po, const Params ¶ms) { +#ifdef DEBUG_FILES + FILE *eacc = boost::nowide::fopen(debug_out_path("emerging_accumulators.obj").c_str(), "w"); + FILE *facc = boost::nowide::fopen(debug_out_path("final_accumulators.obj").c_str(), "w"); +#endif + StabilityAccumulators stability_accs; + LayerLinesDistancer prev_layer_lines { { } }; + Issues issues { }; + std::vector checked_lines; + + // PREPARE BASE LAYER + float max_flow_width = 0.0f; + const Layer *layer = po->layers()[0]; + float base_print_z = layer->print_z; + for (const LayerRegion *layer_region : layer->regions()) { + for (const ExtrusionEntity *ex_entity : layer_region->perimeters.entities) { + for (const ExtrusionEntity *perimeter : static_cast(ex_entity)->entities) { + const float flow_width = get_flow_width(layer_region, perimeter->role()); + max_flow_width = std::max(flow_width, max_flow_width); + const float mm3_per_mm = float(perimeter->min_mm3_per_mm()); + int id = stability_accs.create_accumulator(base_print_z); + StabilityAccumulator &acc = stability_accs.access(id); + Points points { }; + perimeter->collect_points(points); + for (int point_idx = 0; point_idx < int(points.size() - 1); ++point_idx) { + Vec2f start = unscaled(points[point_idx]).cast(); + Vec2f next = unscaled(points[point_idx + 1]).cast(); + ExtrusionLine line { start, next }; + line.stability_accumulator_id = id; + acc.add_base_extrusion(line, flow_width, base_print_z, mm3_per_mm); + checked_lines.push_back(line); + } + if (perimeter->is_loop()) { + Vec2f start = unscaled(points[points.size() - 1]).cast(); + Vec2f next = unscaled(points[0]).cast(); + ExtrusionLine line { start, next }; + line.stability_accumulator_id = id; + acc.add_base_extrusion(line, flow_width, base_print_z, mm3_per_mm); + checked_lines.push_back(line); + } + } // perimeter + } // ex_entity + for (const ExtrusionEntity *ex_entity : layer_region->fills.entities) { + for (const ExtrusionEntity *fill : static_cast(ex_entity)->entities) { + const float flow_width = get_flow_width(layer_region, fill->role()); + max_flow_width = std::max(flow_width, max_flow_width); + const float mm3_per_mm = float(fill->min_mm3_per_mm()); + int id = stability_accs.create_accumulator(base_print_z); + StabilityAccumulator &acc = stability_accs.access(id); + Points points { }; + fill->collect_points(points); + for (int point_idx = 0; point_idx < int(points.size() - 1); ++point_idx) { + Vec2f start = unscaled(points[point_idx]).cast(); + Vec2f next = unscaled(points[point_idx + 1]).cast(); + ExtrusionLine line { start, next }; + line.stability_accumulator_id = id; + acc.add_base_extrusion(line, flow_width, base_print_z, mm3_per_mm); + checked_lines.push_back(line); + } + } // fill + } // ex_entity + } // region + +#ifdef DEBUG_FILES + for (const auto &line : checked_lines) { + Vec3f ecolor = stability_accs.get_emerging_color(line.stability_accumulator_id); + fprintf(eacc, "v %f %f %f %f %f %f\n", line.b[0], + line.b[1], base_print_z, ecolor[0], ecolor[1], ecolor[2]); + + Vec3f fcolor = stability_accs.get_final_color(line.stability_accumulator_id); + fprintf(facc, "v %f %f %f %f %f %f\n", line.b[0], + line.b[1], base_print_z, fcolor[0], fcolor[1], fcolor[2]); + } +#endif DEBUG_FILES + + //MERGE BASE LAYER STABILITY ACCS + prev_layer_lines = LayerLinesDistancer { std::move(checked_lines) }; + for (const ExtrusionLine &l : prev_layer_lines.get_lines()) { + size_t nearest_line_idx; + Vec2f nearest_pt; + float dist = prev_layer_lines.signed_distance_from_lines(l.a, nearest_line_idx, nearest_pt); + if (std::abs(dist) < max_flow_width) { + size_t other_line_acc_id = prev_layer_lines.get_line(nearest_line_idx).stability_accumulator_id; + size_t from_id = std::max(other_line_acc_id, l.stability_accumulator_id); + size_t to_id = std::min(other_line_acc_id, l.stability_accumulator_id); + stability_accs.merge_accumulators(from_id, to_id); + } + } + + //CHECK STABILITY OF ALL LAYERS + for (size_t layer_idx = 1; layer_idx < po->layer_count(); ++layer_idx) { + const Layer *layer = po->layers()[layer_idx]; + checked_lines = std::vector { }; + std::vector> fill_points; + float max_fill_flow_width = 0.0f; + + float print_z = layer->print_z; + for (const LayerRegion *layer_region : layer->regions()) { + for (const ExtrusionEntity *ex_entity : layer_region->perimeters.entities) { + for (const ExtrusionEntity *perimeter : static_cast(ex_entity)->entities) { + check_extrusion_entity_stability(perimeter, stability_accs, issues, checked_lines, print_z, + layer_region, + prev_layer_lines, params); + } // perimeter + } // ex_entity + for (const ExtrusionEntity *ex_entity : layer_region->fills.entities) { + for (const ExtrusionEntity *fill : static_cast(ex_entity)->entities) { + if (fill->role() == ExtrusionRole::erGapFill + || fill->role() == ExtrusionRole::erBridgeInfill) { + check_extrusion_entity_stability(fill, stability_accs, issues, checked_lines, print_z, + layer_region, + prev_layer_lines, params); + } else { + const float flow_width = get_flow_width(layer_region, fill->role()); + max_fill_flow_width = std::max(max_fill_flow_width, flow_width); + Vec2f start = unscaled(fill->first_point()).cast(); + size_t nearest_line_idx; + Vec2f nearest_pt; + float dist = prev_layer_lines.signed_distance_from_lines(start, nearest_line_idx, nearest_pt); + if (dist < flow_width) { + size_t acc_id = prev_layer_lines.get_line(nearest_line_idx).stability_accumulator_id; + StabilityAccumulator &acc = stability_accs.access(acc_id); + Points points { }; + const float mm3_per_mm = float(fill->min_mm3_per_mm()); + fill->collect_points(points); + for (int point_idx = 0; point_idx < int(points.size() - 1); ++point_idx) { + Vec2f start = unscaled(points[point_idx]).cast(); + Vec2f next = unscaled(points[point_idx + 1]).cast(); + ExtrusionLine line { start, next }; + line.stability_accumulator_id = acc_id; + acc.add_extrusion(line, print_z, mm3_per_mm); + } + fill_points.emplace_back(start, acc_id); + } else { + std::cout << " SUPPORTS POINT GEN, start infill in the air? on printz: " << print_z + << std::endl; + } + } + } // fill + } // ex_entity + } // region + + prev_layer_lines = LayerLinesDistancer { std::move(checked_lines) }; + + for (const std::pair &fill_point : fill_points) { + size_t nearest_line_idx; + Vec2f nearest_pt; + float dist = prev_layer_lines.signed_distance_from_lines(fill_point.first, nearest_line_idx, nearest_pt); + if (dist < max_fill_flow_width) { + size_t other_line_acc_id = prev_layer_lines.get_line(nearest_line_idx).stability_accumulator_id; + size_t from_id = std::max(other_line_acc_id, fill_point.second); + size_t to_id = std::min(other_line_acc_id, fill_point.second); + stability_accs.merge_accumulators(from_id, to_id); + } else { + std::cout << " SUPPORTS POINT GEN, no connection on current layer for infill? on printz: " << print_z + << std::endl; + } + } + + check_layer_global_stability(stability_accs, issues, prev_layer_lines.get_lines(), print_z, params); + +#ifdef DEBUG_FILES + for (const auto &line : prev_layer_lines.get_lines()) { + Vec3f ecolor = stability_accs.get_emerging_color(line.stability_accumulator_id); + fprintf(eacc, "v %f %f %f %f %f %f\n", line.b[0], + line.b[1], print_z, ecolor[0], ecolor[1], ecolor[2]); + + Vec3f fcolor = stability_accs.get_final_color(line.stability_accumulator_id); + fprintf(facc, "v %f %f %f %f %f %f\n", line.b[0], + line.b[1], print_z, fcolor[0], fcolor[1], fcolor[2]); + } +#endif + } + +#ifdef DEBUG_FILES + fclose(eacc); + fclose(facc); +#endif + + std::cout << " SUPP: " << issues.supports_nedded.size() << std::endl; + return issues; +} #ifdef DEBUG_FILES void debug_export(Issues issues, std::string file_name) { Slic3r::CNumericLocalesSetter locales_setter; - { FILE *fp = boost::nowide::fopen(debug_out_path((file_name + "_supports.obj").c_str()).c_str(), "w"); if (fp == nullptr) { @@ -685,365 +700,20 @@ void debug_export(Issues issues, std::string file_name) { } fclose(fp); } - } #endif -struct LayerLinesDistancer { - std::vector lines; - AABBTreeIndirect::Tree<2, double> tree; - float line_width; - - LayerLinesDistancer(const Layer *layer) { - float min_region_flow_width { 1.0f }; - for (const auto *region : layer->regions()) { - min_region_flow_width = std::min(min_region_flow_width, - region->flow(FlowRole::frExternalPerimeter).width()); - } - for (const LayerRegion *layer_region : layer->regions()) { - for (const ExtrusionEntity *ex_entity : layer_region->perimeters.entities) { - Points points { }; - ex_entity->collect_points(points); - Vec2d prev = points.size() > 0 ? unscaled(points.front()) : Vec2d::Zero(); - for (size_t idx = 1; idx < points.size(); ++idx) { - lines.emplace_back(prev, unscaled(points[idx])); - } - } // ex_entity - - for (const ExtrusionEntity *ex_entity : layer_region->fills.entities) { - Points points { }; - ex_entity->collect_points(points); - Vec2d prev = points.size() > 0 ? unscaled(points.front()) : Vec2d::Zero(); - for (size_t idx = 1; idx < points.size(); ++idx) { - lines.emplace_back(prev, unscaled(points[idx])); - } - } // ex_entity - } - tree = AABBTreeLines::build_aabb_tree_over_indexed_lines(lines); - line_width = min_region_flow_width; - } - - float distance_from_lines(const Point &point, float point_width) const { - Vec2d p = unscaled(point); - size_t hit_idx_out; - Vec2d hit_point_out; - auto distance = AABBTreeLines::squared_distance_to_indexed_lines(lines, tree, p, hit_idx_out, hit_point_out); - if (distance < 0) - return distance; - - distance = sqrt(distance); - return std::max(float(distance - point_width / 2 - line_width / 20), 0.0f); - } -}; - -//TODO needs revision -float get_flow_width(const LayerRegion *region, ExtrusionRole role) { - switch (role) { - case ExtrusionRole::erBridgeInfill: - return region->flow(FlowRole::frExternalPerimeter).width(); - case ExtrusionRole::erExternalPerimeter: - return region->flow(FlowRole::frExternalPerimeter).width(); - case ExtrusionRole::erGapFill: - return region->flow(FlowRole::frInfill).width(); - case ExtrusionRole::erPerimeter: - return region->flow(FlowRole::frPerimeter).width(); - case ExtrusionRole::erSolidInfill: - return region->flow(FlowRole::frSolidInfill).width(); - case ExtrusionRole::erInternalInfill: - return region->flow(FlowRole::frInfill).width(); - case ExtrusionRole::erTopSolidInfill: - return region->flow(FlowRole::frTopSolidInfill).width(); - default: - return region->flow(FlowRole::frPerimeter).width(); - } -} - -float get_max_allowed_distance(ExtrusionRole role, float flow_width, bool external_perimeters_first, - const Params ¶ms) { // <= distance / flow_width (can be larger for perimeter, if not external perimeter first) - if ((role == ExtrusionRole::erExternalPerimeter || role == ExtrusionRole::erOverhangPerimeter) - && (external_perimeters_first)) { - return params.max_first_ex_perim_unsupported_distance_factor * flow_width; - } else { - return params.max_unsupported_distance_factor * flow_width; - } -} - -struct SegmentAccumulator { - float distance = 0; //accumulated distance - float curvature = 0; //accumulated signed ccw angles - float max_curvature = 0; //max absolute accumulated value - - void add_distance(float dist) { - distance += dist; - } - - void add_angle(float ccw_angle) { - curvature += ccw_angle; - max_curvature = std::max(max_curvature, std::abs(curvature)); - } - - void reset() { - distance = 0; - curvature = 0; - max_curvature = 0; - } - -}; - -Issues check_extrusion_entity_stability(const ExtrusionEntity *entity, float print_z, - const LayerRegion *layer_region, - const LayerLinesDistancer &support_layer, const Params ¶ms) { - - Issues issues { }; - if (entity->is_collection()) { - for (const auto *e : static_cast(entity)->entities) { - issues.add( - check_extrusion_entity_stability(e, print_z, layer_region, support_layer, params)); - } - } else { //single extrusion path, with possible varying parameters - //prepare stack of points on the extrusion path. If there are long segments, additional points might be pushed onto the stack during the algorithm. - std::stack points { }; - for (const auto &p : entity->as_polyline().points) { - points.push(p); - } - - SegmentAccumulator supports_acc { }; - supports_acc.add_distance(params.bridge_distance + 1.0f); // initialize unsupported distance with larger than tolerable distance -> - // -> it prevents extruding perimeter start and short loops into air. - - const auto to_vec3f = [print_z](const Point &point) { - Vec2f tmp = unscale(point).cast(); - return Vec3f(tmp.x(), tmp.y(), print_z); - }; - float region_height = layer_region->layer()->height; - - Point prev_point = points.top(); // prev point of the path. Initialize with first point. - Vec3f prev_fpoint = to_vec3f(prev_point); - float flow_width = get_flow_width(layer_region, entity->role()); - bool external_perimters_first = layer_region->region().config().external_perimeters_first; - const float max_allowed_dist_from_prev_layer = get_max_allowed_distance(entity->role(), flow_width, - external_perimters_first, params); - - while (!points.empty()) { - Point point = points.top(); - points.pop(); - Vec3f fpoint = to_vec3f(point); - float edge_len = (fpoint - prev_fpoint).norm(); - - float dist_from_prev_layer = support_layer.distance_from_lines(point, flow_width); - if (dist_from_prev_layer < 0) { // dist from prev layer not found, assume empty layer - issues.supports_nedded.push_back(SupportPoint(fpoint, 1.0f)); - supports_acc.reset(); - } - - float angle = 0; - if (!points.empty()) { - const Vec2f v1 = (fpoint - prev_fpoint).head<2>(); - const Vec2f v2 = unscale(points.top()).cast() - fpoint.head<2>(); - float dot = v1(0) * v2(0) + v1(1) * v2(1); - float cross = v1(0) * v2(1) - v1(1) * v2(0); - angle = float(atan2(float(cross), float(dot))); // ccw angle, TODO replace with angle func, once it gets into master - } - - supports_acc.add_angle(angle); - - if (dist_from_prev_layer > max_allowed_dist_from_prev_layer) { //extrusion point is unsupported - supports_acc.add_distance(edge_len); // for algorithm simplicity, expect that the whole line between prev and current point is unsupported - - if (supports_acc.distance // if unsupported distance is larger than bridge distance linearly decreased by curvature, enforce supports. - > params.bridge_distance - / (1.0f + (supports_acc.max_curvature - * params.bridge_distance_decrease_by_curvature_factor / PI))) { - issues.supports_nedded.push_back(SupportPoint(fpoint, 1.0f)); - supports_acc.reset(); - } - } else { - supports_acc.reset(); - } - - // Estimation of short curvy segments which are not supported -> problems with curling - if (dist_from_prev_layer > -max_allowed_dist_from_prev_layer * 0.7071) { //extrusion point is unsupported or poorly supported - float dist_factor = 0.5f + 0.5f * (dist_from_prev_layer + max_allowed_dist_from_prev_layer * 0.7071) - / max_allowed_dist_from_prev_layer; - issues.curling_up.push_back( - CurledFilament(fpoint, - dist_factor - * (0.25f * region_height + region_height * 0.75f * std::abs(angle) / PI))); - } - - prev_point = point; - prev_fpoint = fpoint; - - if (!points.empty()) { //oversampling if necessary - Vec2f next = unscale(points.top()).cast(); - Vec2f reverse_v = fpoint.head<2>() - next; // vector from next to current - float dist_to_next = reverse_v.norm(); - reverse_v.normalize(); - int new_points_count = dist_to_next / params.bridge_distance; - float step_size = dist_to_next / (new_points_count + 1); - for (int i = 1; i <= new_points_count; ++i) { - points.push(Point::new_scale(Vec2f(next + reverse_v * (i * step_size)))); - } - } - } - } - return issues; -} - -void distribute_layer_volume(const PrintObject *po, size_t layer_idx, - BalanceDistributionGrid &balance_grid) { - const Layer *layer = po->get_layer(layer_idx); - for (const LayerRegion *region : layer->regions()) { - for (const ExtrusionEntity *collections : region->fills.entities) { - for (const ExtrusionEntity *entity : static_cast(collections)->entities) { - for (const Line &line : entity->as_polyline().lines()) { - balance_grid.distribute_edge(line.a, line.b, layer->print_z, - get_flow_width(region, entity->role()), layer->height); - } - } - } - for (const ExtrusionEntity *collections : region->perimeters.entities) { - for (const ExtrusionEntity *entity : static_cast(collections)->entities) { - for (const Line &line : entity->as_polyline().lines()) { - balance_grid.distribute_edge(line.a, line.b, layer->print_z, - get_flow_width(region, entity->role()), layer->height); - } - } - } - } -} - -Issues check_layer_stability(const PrintObject *po, size_t layer_idx, bool full_check, - const Params ¶ms) { - const Layer *layer = po->get_layer(layer_idx); - //Prepare edge grid of previous layer, will be used to check if the extrusion path is supported - LayerLinesDistancer support_layer(layer->lower_layer); - - Issues issues { }; - if (full_check) { // If full check; check stability of perimeters, gap fills, and bridges. - for (const LayerRegion *layer_region : layer->regions()) { - for (const ExtrusionEntity *ex_entity : layer_region->perimeters.entities) { - for (const ExtrusionEntity *perimeter : static_cast(ex_entity)->entities) { - issues.add( - check_extrusion_entity_stability(perimeter, layer->print_z, layer_region, - support_layer, params)); - } // perimeter - } // ex_entity - for (const ExtrusionEntity *ex_entity : layer_region->fills.entities) { - for (const ExtrusionEntity *fill : static_cast(ex_entity)->entities) { - if (fill->role() == ExtrusionRole::erGapFill - || fill->role() == ExtrusionRole::erBridgeInfill) { - issues.add( - check_extrusion_entity_stability(fill, layer->print_z, layer_region, - support_layer, - params)); - } - } // fill - } // ex_entity - } // region - - } else { // If NOT full check, check only external perimeters - for (const LayerRegion *layer_region : layer->regions()) { - for (const ExtrusionEntity *ex_entity : layer_region->perimeters.entities) { - for (const ExtrusionEntity *perimeter : static_cast(ex_entity)->entities) { - if (perimeter->role() == ExtrusionRole::erExternalPerimeter - || perimeter->role() == ExtrusionRole::erOverhangPerimeter) { - issues.add( - check_extrusion_entity_stability(perimeter, layer->print_z, layer_region, - support_layer, params)); - }; // ex_perimeter - } // perimeter - } // ex_entity - } //region - } - - return issues; -} - -} //Impl End - std::vector quick_search(const PrintObject *po, const Params ¶ms) { - using namespace Impl; - - BalanceDistributionGrid grid { }; - grid.init(po, 0, po->layers().size()); - distribute_layer_volume(po, 0, grid); - std::mutex grid_mutex; - - size_t layer_count = po->layer_count(); - std::vector layer_needs_supports(layer_count, false); - tbb::parallel_for(tbb::blocked_range(1, layer_count), [&](tbb::blocked_range r) { - BalanceDistributionGrid balance_grid { }; - balance_grid.init(po, r.begin(), r.end()); - - for (size_t layer_idx = r.begin(); layer_idx < r.end(); ++layer_idx) { - distribute_layer_volume(po, layer_idx, balance_grid); - auto layer_issues = check_layer_stability(po, layer_idx, false, params); - if (!layer_issues.supports_nedded.empty()) { - layer_needs_supports[layer_idx] = true; - } - } - - grid_mutex.lock(); - grid.merge(balance_grid); - grid_mutex.unlock(); - }); - - std::vector problematic_layers; - for (size_t index = 0; index < layer_needs_supports.size(); ++index) { - if (layer_needs_supports[index]) { - problematic_layers.push_back(index); - } - } - return problematic_layers; + check_object_stability(po, params); + return {}; } Issues full_search(const PrintObject *po, const Params ¶ms) { - using namespace Impl; - - BalanceDistributionGrid grid { }; - grid.init(po, 0, po->layers().size()); - distribute_layer_volume(po, 0, grid); - std::mutex grid_mutex; - - size_t layer_count = po->layer_count(); - Issues found_issues = tbb::parallel_reduce(tbb::blocked_range(1, layer_count), Issues { }, - [&](tbb::blocked_range r, const Issues &init) { - BalanceDistributionGrid balance_grid { }; - balance_grid.init(po, r.begin(), r.end()); - Issues issues = init; - for (size_t layer_idx = r.begin(); layer_idx < r.end(); ++layer_idx) { - distribute_layer_volume(po, layer_idx, balance_grid); - auto layer_issues = check_layer_stability(po, layer_idx, true, params); - if (!layer_issues.empty()) { - issues.add(layer_issues); - } - } - - grid_mutex.lock(); - grid.merge(balance_grid); - grid_mutex.unlock(); - - return issues; - }, - [](Issues left, const Issues &right) { - left.add(right); - return left; - } - ); -#ifdef DEBUG_FILES - Impl::debug_export(found_issues, "pre_issues"); -#endif - - grid.analyze(found_issues, params); - -#ifdef DEBUG_FILES - grid.debug_export(); - Impl::debug_export(found_issues, "issues"); -#endif - - return found_issues; -} + auto issues = check_object_stability(po, params); + debug_export(issues, "issues"); + return issues; } +} //SupportableIssues End } + diff --git a/src/libslic3r/SupportableIssuesSearch.hpp b/src/libslic3r/SupportableIssuesSearch.hpp index d5736a3bde..e8a9f0e0cd 100644 --- a/src/libslic3r/SupportableIssuesSearch.hpp +++ b/src/libslic3r/SupportableIssuesSearch.hpp @@ -14,15 +14,15 @@ struct Params { float bridge_distance = 10.0f; //mm float bridge_distance_decrease_by_curvature_factor = 5.0f; // allowed bridge distance = bridge_distance / ( 1 + this factor * (curvature / PI) ) - float base_adhesion = 2000.0f; // adhesion per mm^2 of first layer; Force needed to remove the object from the bed, divided by the adhesion area (g/mm*s^2) - float support_adhesion = 1000.0f; // adhesion per mm^2 of support interface layer - float support_points_interface_area = 5.0f; // mm^2 + // Adhesion computation : from experiment, PLA holds about 3g per mm^2 of base area (with reserve); So it can withstand about 3*gravity_constant force per mm^2 + float base_adhesion = 3.0f * gravity_constant; // adhesion per mm^2 of first layer + float support_adhesion = 1.0f * gravity_constant; // adhesion per mm^2 of support interface layer + float support_points_interface_area = 2.0f; // mm^2 float max_acceleration = 1000.0f; // mm/s^2 ; max acceleration of object (bed) in XY float filament_density = 1.25f * 0.001f; // g/mm^3 ; Common filaments are very lightweight, so precise number is not that important float tolerable_extruder_conflict_force = 50.0f * gravity_constant; // force that can occasionally push the model due to various factors (filament leaks, small curling, ... ); current value corresponds to weight of 50g - float max_curled_conflict_extruder_force = 200.0f * gravity_constant; // for areas with possible high layered curled filaments, max force to account fo ; current value corresponds to weight of 200g - + float max_curled_conflict_extruder_force = 200.0f * gravity_constant; // for areas with possible high layered curled filaments, max force to account for; current value corresponds to weight of 200g }; struct SupportPoint { diff --git a/src/libslic3r/SupportableIssuesSearchRefactoring.cpp b/src/libslic3r/SupportableIssuesSearchRefactoring.cpp deleted file mode 100644 index 35b567c55e..0000000000 --- a/src/libslic3r/SupportableIssuesSearchRefactoring.cpp +++ /dev/null @@ -1,635 +0,0 @@ -#include "SupportableIssuesSearch.hpp" - -#include "tbb/parallel_for.h" -#include "tbb/blocked_range.h" -#include "tbb/parallel_reduce.h" -#include -#include -#include -#include - -#include "AABBTreeLines.hpp" -#include "libslic3r/Layer.hpp" -#include "libslic3r/ClipperUtils.hpp" -#include "Geometry/ConvexHull.hpp" - -#define DEBUG_FILES - -#ifdef DEBUG_FILES -#include -#include "libslic3r/Color.hpp" -#endif - -namespace Slic3r { - -static const size_t NULL_ACC_ID = std::numeric_limits::max(); - -class ExtrusionLine -{ -public: - ExtrusionLine() : - a(Vec2f::Zero()), b(Vec2f::Zero()), len(0.0f) { - } - ExtrusionLine(const Vec2f &_a, const Vec2f &_b) : - a(_a), b(_b), len((_a - _b).norm()) { - } - - float length() { - return (a - b).norm(); - } - - Vec2f a; - Vec2f b; - float len; - - size_t supported_segment_accumulator_id = NULL_ACC_ID; - - static const constexpr int Dim = 2; - using Scalar = Vec2f::Scalar; -}; - -auto get_a(ExtrusionLine &&l) { - return l.a; -} -auto get_b(ExtrusionLine &&l) { - return l.b; -} - -namespace SupportableIssues { - -void Issues::add(const Issues &layer_issues) { - supports_nedded.insert(supports_nedded.end(), layer_issues.supports_nedded.begin(), - layer_issues.supports_nedded.end()); - curling_up.insert(curling_up.end(), layer_issues.curling_up.begin(), layer_issues.curling_up.end()); -} - -bool Issues::empty() const { - return supports_nedded.empty() && curling_up.empty(); -} - -SupportPoint::SupportPoint(const Vec3f &position, float weight) : - position(position), weight(weight) { -} - -CurledFilament::CurledFilament(const Vec3f &position, float estimated_height) : - position(position), estimated_height(estimated_height) { -} - -CurledFilament::CurledFilament(const Vec3f &position) : - position(position), estimated_height(0.0f) { -} - -class LayerLinesDistancer { -private: - std::vector lines; - AABBTreeIndirect::Tree<2, float> tree; - -public: - explicit LayerLinesDistancer(std::vector &&lines) : - lines(lines) { - tree = AABBTreeLines::build_aabb_tree_over_indexed_lines(lines); - } - - // negative sign means inside - float signed_distance_from_lines(const Vec2f &point, size_t &nearest_line_index_out, - Vec2f &nearest_point_out) const { - auto distance = AABBTreeLines::squared_distance_to_indexed_lines(lines, tree, point, nearest_line_index_out, - nearest_point_out); - if (distance < 0) - return std::numeric_limits::infinity(); - - distance = sqrt(distance); - const ExtrusionLine &line = lines[nearest_line_index_out]; - Vec2f v1 = line.b - line.a; - Vec2f v2 = point - line.a; - if ((v1.x() * v2.y()) - (v1.y() * v2.x()) > 0.0) { - distance *= -1; - } - return distance; - } - - const ExtrusionLine& get_line(size_t line_idx) const { - return lines[line_idx]; - } -}; - -class StabilityAccumulator { -private: - Polygon base_convex_hull { }; - Points support_points { }; - Vec3f centroid_accumulator = Vec3f::Zero(); - float accumulated_volume { }; - float base_area { }; - float base_height { }; - -public: - explicit StabilityAccumulator(float base_height) : - base_height(base_height) { - } - - void add_base_extrusion(const ExtrusionLine &line, float width, float print_z, float cross_section) { - base_area += line.len * width; - support_points.push_back(Point::new_scale(line.a)); - support_points.push_back(Point::new_scale(line.b)); - base_convex_hull.clear(); - add_extrusion(line, print_z, cross_section); - } - - void add_support_point(const Point &position, float area) { - support_points.push_back(position); - base_convex_hull.clear(); - base_area += area; - } - - void add_extrusion(const ExtrusionLine &line, float print_z, float cross_section) { - float volume = line.len * cross_section; - accumulated_volume += volume; - Vec2f center = (line.a + line.b) / 2.0f; - centroid_accumulator += volume * Vec3f(center.x(), center.y(), print_z); - } - - Vec3f get_centroid() const { - return centroid_accumulator / accumulated_volume; - } - - float get_base_area() const { - return base_area; - } - float get_base_height() const { - return base_height; - } - - const Polygon& segment_base_hull() { - if (this->base_convex_hull.empty()) { - this->base_convex_hull = Geometry::convex_hull(this->support_points); - } - return this->base_convex_hull; - } - - const Points& get_support_points() { - return support_points; - } - - void add_from(const StabilityAccumulator &acc) { - this->support_points.insert(this->support_points.end(), acc.support_points.begin(), - acc.support_points.end()); - base_convex_hull.clear(); - this->centroid_accumulator += acc.centroid_accumulator; - this->accumulated_volume += acc.accumulated_volume; - this->base_area += acc.base_area; - } -}; - -struct StabilityAccumulators { -private: - size_t next_id = 0; - std::unordered_map mapping; - std::vector acccumulators; - - void merge_to(size_t from_id, size_t to_id) { - StabilityAccumulator &from_acc = this->access(from_id); - StabilityAccumulator &to_acc = this->access(to_id); - if (&from_acc == &to_acc) { - return; - } - to_acc.add_from(from_acc); - mapping[from_id] = mapping[to_id]; - from_acc = StabilityAccumulator { 0.0f }; - - } - -public: - StabilityAccumulators() = default; - - int create_accumulator(float base_height) { - size_t id = next_id; - next_id++; - mapping[id] = acccumulators.size(); - acccumulators.push_back(StabilityAccumulator { base_height }); - return id; - } - - StabilityAccumulator& access(size_t id) { - return acccumulators[mapping[id]]; - } - - void merge_accumulators(size_t from_id, size_t to_id) { - if (from_id == NULL_ACC_ID || to_id == NULL_ACC_ID) { - return; - } - StabilityAccumulator &from_acc = this->access(from_id); - StabilityAccumulator &to_acc = this->access(to_id); - if (&from_acc == &to_acc) { - return; - } - to_acc.add_from(from_acc); - mapping[from_id] = mapping[to_id]; - from_acc = StabilityAccumulator { 0.0f }; - } - -#ifdef DEBUG_FILES - Vec3f get_emerging_color(size_t id) { - if (mapping.find(id) == mapping.end()) { - std::cerr << " ERROR: uknown accumulator ID: " << id << std::endl; - return Vec3f(1.0f, 1.0f, 1.0f); - } - - size_t pseudornd = ((id + 127) * 33331 + 6907) % 13; - return value_to_rgbf(0.0f, 13.0f, float(pseudornd)); - } - - Vec3f get_final_color(size_t id) { - if (mapping.find(id) == mapping.end()) { - std::cerr << " ERROR: uknown accumulator ID: " << id << std::endl; - return Vec3f(1.0f, 1.0f, 1.0f); - } - - size_t pseudornd = ((mapping[id] + 127) * 33331 + 6907) % 13; - return value_to_rgbf(0.0f, 13.0f, float(pseudornd)); - } -#endif DEBUG_FILES -}; - -float get_flow_width(const LayerRegion *region, ExtrusionRole role) { - switch (role) { - case ExtrusionRole::erBridgeInfill: - return region->flow(FlowRole::frExternalPerimeter).width(); - case ExtrusionRole::erExternalPerimeter: - return region->flow(FlowRole::frExternalPerimeter).width(); - case ExtrusionRole::erGapFill: - return region->flow(FlowRole::frInfill).width(); - case ExtrusionRole::erPerimeter: - return region->flow(FlowRole::frPerimeter).width(); - case ExtrusionRole::erSolidInfill: - return region->flow(FlowRole::frSolidInfill).width(); - case ExtrusionRole::erInternalInfill: - return region->flow(FlowRole::frInfill).width(); - case ExtrusionRole::erTopSolidInfill: - return region->flow(FlowRole::frTopSolidInfill).width(); - default: - return region->flow(FlowRole::frPerimeter).width(); - } -} - -struct ExtrusionPropertiesAccumulator { - float distance = 0; //accumulated distance - float curvature = 0; //accumulated signed ccw angles - float max_curvature = 0; //max absolute accumulated value - - void add_distance(float dist) { - distance += dist; - } - - void add_angle(float ccw_angle) { - curvature += ccw_angle; - max_curvature = std::max(max_curvature, std::abs(curvature)); - } - - void reset() { - distance = 0; - curvature = 0; - max_curvature = 0; - } -}; - -void check_extrusion_entity_stability(const ExtrusionEntity *entity, - StabilityAccumulators &stability_accs, - Issues &issues, - std::vector &checked_lines, - float print_z, - const LayerRegion *layer_region, - const LayerLinesDistancer &prev_layer_lines, - const Params ¶ms) { - - if (entity->is_collection()) { - for (const auto *e : static_cast(entity)->entities) { - check_extrusion_entity_stability(e, stability_accs, issues, checked_lines, print_z, layer_region, - prev_layer_lines, - params); - } - } else { //single extrusion path, with possible varying parameters - const auto to_vec3f = [print_z](const Point &point) { - Vec2f tmp = unscale(point).cast(); - return Vec3f(tmp.x(), tmp.y(), print_z); - }; - Points points { }; - entity->collect_points(points); - std::vector lines; - lines.reserve(points.size() * 1.5); - lines.emplace_back(unscaled(points[0]).cast(), unscaled(points[0]).cast()); - for (int point_idx = 0; point_idx < int(points.size() - 1); ++point_idx) { - Vec2f start = unscaled(points[point_idx]).cast(); - Vec2f next = unscaled(points[point_idx + 1]).cast(); - Vec2f v = next - start; // vector from next to current - float dist_to_next = v.norm(); - v.normalize(); - int lines_count = int(std::ceil(dist_to_next / params.bridge_distance)); - float step_size = dist_to_next / lines_count; - for (int i = 0; i < lines_count; ++i) { - Vec2f a(start + v * (i * step_size)); - Vec2f b(start + v * ((i + 1) * step_size)); - lines.emplace_back(a, b); - } - } - - size_t current_stability_acc = NULL_ACC_ID; - ExtrusionPropertiesAccumulator bridging_acc { }; - bridging_acc.add_distance(params.bridge_distance + 1.0f); // Initialise unsupported distance with larger than tolerable distance -> - // -> it prevents extruding perimeter start and short loops into air. - const float flow_width = get_flow_width(layer_region, entity->role()); - const float region_height = layer_region->layer()->height; - const float max_allowed_dist_from_prev_layer = flow_width; - - for (size_t line_idx = 0; line_idx < lines.size(); ++line_idx) { - ExtrusionLine ¤t_line = lines[line_idx]; - Point current = Point::new_scale(current_line.b); - float cross_section = region_height * flow_width * 0.7071f; - - float curr_angle = 0; - if (line_idx + 1 < lines.size()) { - const Vec2f v1 = current_line.b - current_line.a; - const Vec2f v2 = lines[line_idx + 1].b - lines[line_idx + 1].a; - curr_angle = angle(v1, v2); - } - bridging_acc.add_angle(curr_angle); - - size_t nearest_line_idx; - Vec2f nearest_point; - float dist_from_prev_layer = prev_layer_lines.signed_distance_from_lines(current_line.b, nearest_line_idx, - nearest_point); - if (dist_from_prev_layer < max_allowed_dist_from_prev_layer) { - const ExtrusionLine &nearest_line = prev_layer_lines.get_line(nearest_line_idx); - size_t acc_id = nearest_line.supported_segment_accumulator_id; - stability_accs.merge_accumulators(std::max(acc_id, current_stability_acc), - std::min(acc_id, current_stability_acc)); - current_stability_acc = std::min(acc_id, current_stability_acc); - current_line.supported_segment_accumulator_id = current_stability_acc; - stability_accs.access(current_stability_acc).add_extrusion(current_line, print_z, cross_section); - bridging_acc.reset(); - // TODO curving here - } else { - bridging_acc.add_distance(current_line.len); - if (current_stability_acc == NULL_ACC_ID) { - current_stability_acc = stability_accs.create_accumulator(print_z); - } - StabilityAccumulator ¤t_segment = stability_accs.access(current_stability_acc); - current_line.supported_segment_accumulator_id = current_stability_acc; - current_segment.add_extrusion(current_line, print_z, cross_section); - if (bridging_acc.distance // if unsupported distance is larger than bridge distance linearly decreased by curvature, enforce supports. - > params.bridge_distance - / (1.0f + (bridging_acc.max_curvature - * params.bridge_distance_decrease_by_curvature_factor / PI))) { - current_segment.add_support_point(current, params.support_points_interface_area); - issues.supports_nedded.emplace_back(to_vec3f(current), 1.0); - bridging_acc.reset(); - } - } - } - checked_lines.insert(checked_lines.end(), lines.begin(), lines.end()); - } -} - -void check_layer_global_stability(StabilityAccumulators &stability_accs, - Issues &issues, - const std::vector &checked_lines, - float print_z, - const Params ¶ms) { - std::unordered_map> layer_accs_lines; - for (size_t i = 0; i < checked_lines.size(); ++i) { - layer_accs_lines[&stability_accs.access(checked_lines[i].supported_segment_accumulator_id)].push_back(i); - } - - for (auto &acc_lines : layer_accs_lines) { - StabilityAccumulator *acc = acc_lines.first; - Vec3f centroid = acc->get_centroid(); - Vec2f hull_centroid = unscaled(acc->segment_base_hull().centroid()).cast(); - std::vector hull_lines; - for (const Line &line : acc->segment_base_hull().lines()) { - Vec2f start = unscaled(line.a).cast(); - Vec2f next = unscaled(line.b).cast(); - hull_lines.push_back( { start, next }); - } - if (hull_lines.empty()) { - if (acc->get_support_points().empty()) { - acc->add_support_point(Point::new_scale(checked_lines[acc_lines.second[0]].a), - params.support_points_interface_area); - issues.supports_nedded.emplace_back(to_3d(checked_lines[acc_lines.second[0]].a, print_z), 1.0); - } - hull_lines.push_back( { unscaled(acc->get_support_points()[0]).cast(), - unscaled(acc->get_support_points()[0]).cast() }); - hull_centroid = unscaled(acc->get_support_points()[0]).cast(); - } - - LayerLinesDistancer hull_distancer(std::move(hull_lines)); - - size_t _li; - Vec2f _p; - bool centroid_inside_hull = hull_distancer.signed_distance_from_lines(centroid.head<2>(), _li, _p) < 0; - - float sticking_force = acc->get_base_area() - * (acc->get_base_height() == 0 ? params.base_adhesion : params.support_adhesion); -// float weight = acc-> * params.filament_density * params.gravity_constant; -// float weight_torque = embedded_distance * weight; -// if (!inside) { -// weight_torque *= -1; -// } - - for (size_t line_idx : acc_lines.second){ - const ExtrusionLine &line = checked_lines[line_idx]; - - size_t nearest_line_idx; - Vec2f nearest_hull_point; - float hull_distance = hull_distancer.signed_distance_from_lines(line.b, nearest_line_idx, - nearest_hull_point); - - float sticking_torque = (nearest_hull_point - hull_centroid).norm() * sticking_force; - - std::cout << "sticking_torque: " << sticking_torque << std::endl; - - - Vec3f extruder_pressure_direction = to_3d(Vec2f(line.b - line.a), 0.0f).normalized(); - if (hull_distance > 0) { - extruder_pressure_direction.z() = -0.333f; - extruder_pressure_direction.normalize(); - } - float pressure_torque_arm = (to_3d(Vec2f(nearest_hull_point - line.b), print_z).cross(extruder_pressure_direction)).norm(); - - float extruder_conflict_torque = params.tolerable_extruder_conflict_force * pressure_torque_arm; - - std::cout << "extruder_conflict_torque: " << extruder_conflict_torque << std::endl; - - if (extruder_conflict_torque > sticking_torque) { - acc->add_support_point(Point::new_scale(line.b), params.support_points_interface_area); - issues.supports_nedded.emplace_back(to_3d(line.b, print_z), extruder_conflict_torque - sticking_torque); - } - - } - } -} - -Issues check_object_stability(const PrintObject *po, const Params ¶ms) { -#ifdef DEBUG_FILES - FILE *eacc = boost::nowide::fopen(debug_out_path("emerging_accumulators.obj").c_str(), "w"); - FILE *facc = boost::nowide::fopen(debug_out_path("final_accumulators.obj").c_str(), "w"); -#endif DEBUG_FILES - StabilityAccumulators stability_accs; - LayerLinesDistancer prev_layer_lines { { } }; - Issues issues { }; - std::vector checked_lines; - - const Layer *layer = po->layers()[0]; - float base_print_z = layer->print_z; - for (const LayerRegion *layer_region : layer->regions()) { - for (const ExtrusionEntity *ex_entity : layer_region->perimeters.entities) { - for (const ExtrusionEntity *perimeter : static_cast(ex_entity)->entities) { - const float flow_width = get_flow_width(layer_region, perimeter->role()); - const float region_height = layer_region->layer()->height; - const float cross_section = region_height * flow_width * 0.7071f; - int id = stability_accs.create_accumulator(base_print_z); - StabilityAccumulator &acc = stability_accs.access(id); - Points points { }; - perimeter->collect_points(points); - for (int point_idx = 0; point_idx < int(points.size() - 1); ++point_idx) { - Vec2f start = unscaled(points[point_idx]).cast(); - Vec2f next = unscaled(points[point_idx + 1]).cast(); - ExtrusionLine line { start, next }; - line.supported_segment_accumulator_id = id; - acc.add_base_extrusion(line, flow_width, base_print_z, cross_section); - checked_lines.push_back(line); - } - } // perimeter - } // ex_entity - for (const ExtrusionEntity *ex_entity : layer_region->fills.entities) { - for (const ExtrusionEntity *fill : static_cast(ex_entity)->entities) { - const float flow_width = get_flow_width(layer_region, fill->role()); - const float region_height = layer_region->layer()->height; - const float cross_section = region_height * flow_width * 0.7071f; - int id = stability_accs.create_accumulator(base_print_z); - StabilityAccumulator &acc = stability_accs.access(id); - Points points { }; - fill->collect_points(points); - for (int point_idx = 0; point_idx < int(points.size() - 1); ++point_idx) { - Vec2f start = unscaled(points[point_idx]).cast(); - Vec2f next = unscaled(points[point_idx + 1]).cast(); - ExtrusionLine line { start, next }; - line.supported_segment_accumulator_id = id; - acc.add_base_extrusion(line, flow_width, base_print_z, cross_section); - checked_lines.push_back(line); - } - } // fill - } // ex_entity - } // region - -#ifdef DEBUG_FILES - for (const auto &line : checked_lines) { - Vec3f ecolor = stability_accs.get_emerging_color(line.supported_segment_accumulator_id); - fprintf(eacc, "v %f %f %f %f %f %f\n", line.b[0], - line.b[1], base_print_z, ecolor[0], ecolor[1], ecolor[2]); - - Vec3f fcolor = stability_accs.get_final_color(line.supported_segment_accumulator_id); - fprintf(facc, "v %f %f %f %f %f %f\n", line.b[0], - line.b[1], base_print_z, fcolor[0], fcolor[1], fcolor[2]); - } -#endif DEBUG_FILES - - for (size_t layer_idx = 1; layer_idx < po->layer_count(); ++layer_idx) { - const Layer *layer = po->layers()[layer_idx]; - prev_layer_lines = LayerLinesDistancer { std::move(checked_lines) }; - checked_lines = std::vector { }; - - float print_z = layer->print_z; - for (const LayerRegion *layer_region : layer->regions()) { - for (const ExtrusionEntity *ex_entity : layer_region->perimeters.entities) { - for (const ExtrusionEntity *perimeter : static_cast(ex_entity)->entities) { - check_extrusion_entity_stability(perimeter, stability_accs, issues, checked_lines, print_z, - layer_region, - prev_layer_lines, params); - } // perimeter - } // ex_entity - for (const ExtrusionEntity *ex_entity : layer_region->fills.entities) { - for (const ExtrusionEntity *fill : static_cast(ex_entity)->entities) { - if (fill->role() == ExtrusionRole::erGapFill - || fill->role() == ExtrusionRole::erBridgeInfill) { - check_extrusion_entity_stability(fill, stability_accs, issues, checked_lines, print_z, - layer_region, - prev_layer_lines, params); - } - } // fill - } // ex_entity - } // region - - check_layer_global_stability(stability_accs, issues, checked_lines, print_z, params); - -#ifdef DEBUG_FILES - for (const auto &line : checked_lines) { - Vec3f ecolor = stability_accs.get_emerging_color(line.supported_segment_accumulator_id); - fprintf(eacc, "v %f %f %f %f %f %f\n", line.b[0], - line.b[1], print_z, ecolor[0], ecolor[1], ecolor[2]); - - Vec3f fcolor = stability_accs.get_final_color(line.supported_segment_accumulator_id); - fprintf(facc, "v %f %f %f %f %f %f\n", line.b[0], - line.b[1], print_z, fcolor[0], fcolor[1], fcolor[2]); - } -#endif DEBUG_FILES - } - -#ifdef DEBUG_FILES - fclose(eacc); - fclose(facc); -#endif DEBUG_FILES - - std::cout << " SUPP: " << issues.supports_nedded.size() << std::endl; - return issues; -} - -#ifdef DEBUG_FILES -void debug_export(Issues issues, std::string file_name) { - Slic3r::CNumericLocalesSetter locales_setter; - { - FILE *fp = boost::nowide::fopen(debug_out_path((file_name + "_supports.obj").c_str()).c_str(), "w"); - if (fp == nullptr) { - BOOST_LOG_TRIVIAL(error) - << "Debug files: Couldn't open " << file_name << " for writing"; - return; - } - - for (size_t i = 0; i < issues.supports_nedded.size(); ++i) { - fprintf(fp, "v %f %f %f %f %f %f\n", issues.supports_nedded[i].position(0), - issues.supports_nedded[i].position(1), - issues.supports_nedded[i].position(2), 1.0, 0.0, 1.0); - } - - fclose(fp); - } - { - FILE *fp = boost::nowide::fopen(debug_out_path((file_name + "_curling.obj").c_str()).c_str(), "w"); - if (fp == nullptr) { - BOOST_LOG_TRIVIAL(error) - << "Debug files: Couldn't open " << file_name << " for writing"; - return; - } - - for (size_t i = 0; i < issues.curling_up.size(); ++i) { - fprintf(fp, "v %f %f %f %f %f %f\n", issues.curling_up[i].position(0), - issues.curling_up[i].position(1), - issues.curling_up[i].position(2), 0.0, 1.0, 0.0); - } - fclose(fp); - } -} -#endif - -std::vector quick_search(const PrintObject *po, const Params ¶ms) { - check_object_stability(po, params); - return {}; -} - -Issues full_search(const PrintObject *po, const Params ¶ms) { - auto issues = check_object_stability(po, params); - debug_export(issues, "issues"); - return issues; - -} -} //SupportableIssues End -} - From 8dc3956b64770eed64ef263d03a8eb716d2a2314 Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Tue, 21 Jun 2022 16:04:29 +0200 Subject: [PATCH 028/100] bug fixes, raycasting to find good support spot --- src/libslic3r/PrintObject.cpp | 38 +++++------ src/libslic3r/SupportableIssuesSearch.cpp | 78 ++++++++++------------- src/libslic3r/SupportableIssuesSearch.hpp | 10 ++- src/libslic3r/TriangleSelectorWrapper.cpp | 34 +++++----- src/libslic3r/TriangleSelectorWrapper.hpp | 2 +- 5 files changed, 80 insertions(+), 82 deletions(-) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index f0e435dea8..40f740b91c 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -423,27 +423,29 @@ void PrintObject::find_supportable_issues() SupportableIssues::Issues issues = SupportableIssues::full_search(this); //TODO fix // if (!issues.supports_nedded.empty()) { - auto obj_transform = this->trafo_centered(); - for (ModelVolume *model_volume : this->model_object()->volumes) { - if (model_volume->type() == ModelVolumeType::MODEL_PART) { - Transform3d model_transformation = model_volume->get_matrix(); - Transform3d inv_transform = (obj_transform * model_transformation).inverse(); - TriangleSelectorWrapper selector { model_volume->mesh() }; + auto obj_transform = this->trafo_centered(); + for (ModelVolume *model_volume : this->model_object()->volumes) { + if (model_volume->type() == ModelVolumeType::MODEL_PART) { + Transform3d model_transformation = model_volume->get_matrix(); + Transform3f inv_transform = (obj_transform * model_transformation).inverse().cast(); + TriangleSelectorWrapper selector { model_volume->mesh() }; - for (const SupportableIssues::SupportPoint &support_point : issues.supports_nedded) { - selector.enforce_spot(Vec3f(inv_transform.cast() * support_point.position), 0.5f); - } - - model_volume->supported_facets.set(selector.selector); - -#if 1 - indexed_triangle_set copy = model_volume->mesh().its; - its_transform(copy, obj_transform * model_transformation); - its_write_obj(copy, - debug_out_path("model.obj").c_str()); -#endif + for (const SupportableIssues::SupportPoint &support_point : issues.supports_nedded) { + Vec3f point = Vec3f(inv_transform * support_point.position); + Vec3f origin = Vec3f( + inv_transform * Vec3f(support_point.position.x(), support_point.position.y(), 0.0f)); + selector.enforce_spot(point, origin, 0.5f); } + + model_volume->supported_facets.set(selector.selector); +#if 1 + indexed_triangle_set copy = model_volume->mesh().its; + its_transform(copy, obj_transform * model_transformation); + its_write_obj(copy, + debug_out_path("model.obj").c_str()); +#endif } + } // } } diff --git a/src/libslic3r/SupportableIssuesSearch.cpp b/src/libslic3r/SupportableIssuesSearch.cpp index 73f89e0d63..ce17020ed7 100644 --- a/src/libslic3r/SupportableIssuesSearch.cpp +++ b/src/libslic3r/SupportableIssuesSearch.cpp @@ -191,7 +191,7 @@ struct StabilityAccumulators { private: size_t next_id = 0; std::unordered_map mapping; - std::vector acccumulators; + std::vector accumulators; void merge_to(size_t from_id, size_t to_id) { StabilityAccumulator &from_acc = this->access(from_id); @@ -211,13 +211,13 @@ public: int create_accumulator(float base_height) { size_t id = next_id; next_id++; - mapping[id] = acccumulators.size(); - acccumulators.push_back(StabilityAccumulator { base_height }); + mapping[id] = accumulators.size(); + accumulators.push_back(StabilityAccumulator { base_height }); return id; } StabilityAccumulator& access(size_t id) { - return acccumulators[mapping[id]]; + return accumulators[mapping[id]]; } void merge_accumulators(size_t from_id, size_t to_id) { @@ -235,24 +235,14 @@ public: } #ifdef DEBUG_FILES - Vec3f get_emerging_color(size_t id) { + Vec3f get_accumulator_color(size_t id) { if (mapping.find(id) == mapping.end()) { std::cerr << " ERROR: uknown accumulator ID: " << id << std::endl; return Vec3f(1.0f, 1.0f, 1.0f); } - size_t pseudornd = ((id + 127) * 33331 + 6907) % 13; - return value_to_rgbf(0.0f, 13.0f, float(pseudornd)); - } - - Vec3f get_final_color(size_t id) { - if (mapping.find(id) == mapping.end()) { - std::cerr << " ERROR: uknown accumulator ID: " << id << std::endl; - return Vec3f(1.0f, 1.0f, 1.0f); - } - - size_t pseudornd = ((mapping[id] + 127) * 33331 + 6907) % 13; - return value_to_rgbf(0.0f, 13.0f, float(pseudornd)); + size_t pseudornd = ((mapping[id] + 127) * 33331 + 6907) % 987; + return value_to_rgbf(0.0f, float(987), float(pseudornd)); } #endif }; @@ -345,10 +335,12 @@ void check_extrusion_entity_stability(const ExtrusionEntity *entity, // -> it prevents extruding perimeter start and short loops into air. const float flow_width = get_flow_width(layer_region, entity->role()); const float max_allowed_dist_from_prev_layer = flow_width; + float distance_from_last_support_point = params.min_distance_between_support_points * 2.0f; for (size_t line_idx = 0; line_idx < lines.size(); ++line_idx) { ExtrusionLine ¤t_line = lines[line_idx]; Point current = Point::new_scale(current_line.b); + distance_from_last_support_point += current_line.len; float mm3_per_mm = float(entity->min_mm3_per_mm()); float curr_angle = 0; @@ -381,13 +373,15 @@ void check_extrusion_entity_stability(const ExtrusionEntity *entity, StabilityAccumulator ¤t_segment = stability_accs.access(current_stability_acc); current_line.stability_accumulator_id = current_stability_acc; current_segment.add_extrusion(current_line, print_z, mm3_per_mm); - if (bridging_acc.distance // if unsupported distance is larger than bridge distance linearly decreased by curvature, enforce supports. - > params.bridge_distance - / (1.0f + (bridging_acc.max_curvature - * params.bridge_distance_decrease_by_curvature_factor / PI))) { - current_segment.add_support_point(current, params.support_points_interface_area); + if (distance_from_last_support_point > params.min_distance_between_support_points && + bridging_acc.distance // if unsupported distance is larger than bridge distance linearly decreased by curvature, enforce supports. + > params.bridge_distance + / (1.0f + (bridging_acc.max_curvature + * params.bridge_distance_decrease_by_curvature_factor / PI))) { + current_segment.add_support_point(current, params.extrusion_support_points_area); issues.supports_nedded.emplace_back(to_vec3f(current), 1.0); bridging_acc.reset(); + distance_from_last_support_point = 0.0f; } } } @@ -430,10 +424,13 @@ void check_layer_global_stability(StabilityAccumulators &stability_accs, float sticking_force = acc->get_base_area() * (acc->get_base_height() == 0 ? params.base_adhesion : params.support_adhesion); - float weight = acc->get_accumulated_volume() * params.filament_density * params.gravity_constant; + float mass = acc->get_accumulated_volume() * params.filament_density; + float weight = mass * params.gravity_constant; + float distance_from_last_support_point = params.min_distance_between_support_points * 2.0f; for (size_t line_idx : acc_lines.second) { const ExtrusionLine &line = checked_lines[line_idx]; + distance_from_last_support_point += line.len; Vec3f extruder_pressure_direction = to_3d(Vec2f(line.b - line.a), 0.0f).normalized(); Vec2f pivot_site_search = line.b + extruder_pressure_direction.head<2>() * 1000.0f; @@ -457,7 +454,7 @@ void check_layer_global_stability(StabilityAccumulators &stability_accs, std::cout << "weight_torque: " << weight_torque << std::endl; float bed_movement_arm = centroid.z() - acc->get_base_height(); - float bed_movement_force = params.max_acceleration * weight; + float bed_movement_force = params.max_acceleration * mass; float bed_movement_torque = bed_movement_force * bed_movement_arm; std::cout << "bed_movement_arm: " << bed_movement_arm << std::endl; @@ -475,8 +472,9 @@ void check_layer_global_stability(StabilityAccumulators &stability_accs, std::cout << "total_torque: " << total_torque << " printz: " << print_z << std::endl; if (total_torque > 0) { - acc->add_support_point(Point::new_scale(line.b), params.support_points_interface_area); + acc->add_support_point(Point::new_scale(line.b), std::min(params.support_points_interface_area, (pivot - line.b).squaredNorm())); issues.supports_nedded.emplace_back(to_3d(line.b, print_z), extruder_conflict_torque - sticking_torque); + distance_from_last_support_point = 0.0f; } } @@ -485,8 +483,7 @@ void check_layer_global_stability(StabilityAccumulators &stability_accs, Issues check_object_stability(const PrintObject *po, const Params ¶ms) { #ifdef DEBUG_FILES - FILE *eacc = boost::nowide::fopen(debug_out_path("emerging_accumulators.obj").c_str(), "w"); - FILE *facc = boost::nowide::fopen(debug_out_path("final_accumulators.obj").c_str(), "w"); + FILE *debug_acc = boost::nowide::fopen(debug_out_path("accumulators.obj").c_str(), "w"); #endif StabilityAccumulators stability_accs; LayerLinesDistancer prev_layer_lines { { } }; @@ -548,15 +545,11 @@ Issues check_object_stability(const PrintObject *po, const Params ¶ms) { #ifdef DEBUG_FILES for (const auto &line : checked_lines) { - Vec3f ecolor = stability_accs.get_emerging_color(line.stability_accumulator_id); - fprintf(eacc, "v %f %f %f %f %f %f\n", line.b[0], - line.b[1], base_print_z, ecolor[0], ecolor[1], ecolor[2]); - - Vec3f fcolor = stability_accs.get_final_color(line.stability_accumulator_id); - fprintf(facc, "v %f %f %f %f %f %f\n", line.b[0], - line.b[1], base_print_z, fcolor[0], fcolor[1], fcolor[2]); + Vec3f color = stability_accs.get_accumulator_color(line.stability_accumulator_id); + fprintf(debug_acc, "v %f %f %f %f %f %f\n", line.b[0], + line.b[1], base_print_z, color[0], color[1], color[2]); } -#endif DEBUG_FILES +#endif //MERGE BASE LAYER STABILITY ACCS prev_layer_lines = LayerLinesDistancer { std::move(checked_lines) }; @@ -564,7 +557,7 @@ Issues check_object_stability(const PrintObject *po, const Params ¶ms) { size_t nearest_line_idx; Vec2f nearest_pt; float dist = prev_layer_lines.signed_distance_from_lines(l.a, nearest_line_idx, nearest_pt); - if (std::abs(dist) < max_flow_width) { + if (std::abs(dist) < max_flow_width*1.1f) { size_t other_line_acc_id = prev_layer_lines.get_line(nearest_line_idx).stability_accumulator_id; size_t from_id = std::max(other_line_acc_id, l.stability_accumulator_id); size_t to_id = std::min(other_line_acc_id, l.stability_accumulator_id); @@ -646,20 +639,15 @@ Issues check_object_stability(const PrintObject *po, const Params ¶ms) { #ifdef DEBUG_FILES for (const auto &line : prev_layer_lines.get_lines()) { - Vec3f ecolor = stability_accs.get_emerging_color(line.stability_accumulator_id); - fprintf(eacc, "v %f %f %f %f %f %f\n", line.b[0], - line.b[1], print_z, ecolor[0], ecolor[1], ecolor[2]); - - Vec3f fcolor = stability_accs.get_final_color(line.stability_accumulator_id); - fprintf(facc, "v %f %f %f %f %f %f\n", line.b[0], - line.b[1], print_z, fcolor[0], fcolor[1], fcolor[2]); + Vec3f color = stability_accs.get_accumulator_color(line.stability_accumulator_id); + fprintf(debug_acc, "v %f %f %f %f %f %f\n", line.b[0], + line.b[1], print_z, color[0], color[1], color[2]); } #endif } #ifdef DEBUG_FILES - fclose(eacc); - fclose(facc); + fclose(debug_acc); #endif std::cout << " SUPP: " << issues.supports_nedded.size() << std::endl; diff --git a/src/libslic3r/SupportableIssuesSearch.hpp b/src/libslic3r/SupportableIssuesSearch.hpp index e8a9f0e0cd..ccda2d77af 100644 --- a/src/libslic3r/SupportableIssuesSearch.hpp +++ b/src/libslic3r/SupportableIssuesSearch.hpp @@ -10,14 +10,18 @@ namespace SupportableIssues { struct Params { const float gravity_constant = 9806.65f; // mm/s^2; gravity acceleration on Earth's surface, algorithm assumes that printer is in upwards position. - float bridge_distance = 10.0f; //mm float bridge_distance_decrease_by_curvature_factor = 5.0f; // allowed bridge distance = bridge_distance / ( 1 + this factor * (curvature / PI) ) + float min_distance_between_support_points = 0.5f; + // Adhesion computation : from experiment, PLA holds about 3g per mm^2 of base area (with reserve); So it can withstand about 3*gravity_constant force per mm^2 float base_adhesion = 3.0f * gravity_constant; // adhesion per mm^2 of first layer - float support_adhesion = 1.0f * gravity_constant; // adhesion per mm^2 of support interface layer - float support_points_interface_area = 2.0f; // mm^2 + float support_adhesion = 3.0f * gravity_constant; // adhesion per mm^2 of support interface layer + float support_points_interface_area = 5.0f; // mm^2 + float extrusion_support_points_area = 0.5f; // much lower value, because these support points appear due to unsupported extrusion, + // not stability - they can be very densely placed, making the sticking estimation incorrect + float max_acceleration = 1000.0f; // mm/s^2 ; max acceleration of object (bed) in XY float filament_density = 1.25f * 0.001f; // g/mm^3 ; Common filaments are very lightweight, so precise number is not that important diff --git a/src/libslic3r/TriangleSelectorWrapper.cpp b/src/libslic3r/TriangleSelectorWrapper.cpp index 02a3b8a1ab..ec716ebca3 100644 --- a/src/libslic3r/TriangleSelectorWrapper.cpp +++ b/src/libslic3r/TriangleSelectorWrapper.cpp @@ -10,22 +10,26 @@ TriangleSelectorWrapper::TriangleSelectorWrapper(const TriangleMesh &mesh) : } -void TriangleSelectorWrapper::enforce_spot(const Vec3f &point, float radius) { - size_t hit_face_index; - Vec3f hit_point; - auto dist = AABBTreeIndirect::squared_distance_to_indexed_triangle_set(mesh.its.vertices, mesh.its.indices, - triangles_tree, - point, hit_face_index, hit_point); - if (dist < 0 || dist > radius * radius) { - return; +void TriangleSelectorWrapper::enforce_spot(const Vec3f &point, const Vec3f &origin, float radius) { + std::vector hits; + Vec3f dir = (point - origin).normalized(); + if (AABBTreeIndirect::intersect_ray_all_hits(mesh.its.vertices, mesh.its.indices, triangles_tree, + Vec3d(origin.cast()), + Vec3d(dir.cast()), + hits)) { + for (int hit_idx = hits.size() - 1; hit_idx >= 0; --hit_idx) { + const igl::Hit &hit = hits[hit_idx]; + Vec3f pos = origin + dir * hit.t; + Vec3f face_normal = its_face_normal(mesh.its, hit.id); + if (point.z() + radius > pos.z() && face_normal.dot(dir) < 0) { + std::unique_ptr cursor = std::make_unique( + pos, origin, radius, Transform3d::Identity(), TriangleSelector::ClippingPlane { }); + selector.select_patch(hit.id, std::move(cursor), EnforcerBlockerType::ENFORCER, Transform3d::Identity(), + true, 0.0f); + break; + } + } } - - std::unique_ptr cursor = std::make_unique(hit_point, point, - radius, Transform3d::Identity(), TriangleSelector::ClippingPlane { }); - - selector.select_patch(hit_face_index, std::move(cursor), EnforcerBlockerType::ENFORCER, Transform3d::Identity(), - true, - 0.0f); } } diff --git a/src/libslic3r/TriangleSelectorWrapper.hpp b/src/libslic3r/TriangleSelectorWrapper.hpp index f3b56205fa..10707cc257 100644 --- a/src/libslic3r/TriangleSelectorWrapper.hpp +++ b/src/libslic3r/TriangleSelectorWrapper.hpp @@ -20,7 +20,7 @@ public: TriangleSelectorWrapper(const TriangleMesh &mesh); - void enforce_spot(const Vec3f &point, float radius); + void enforce_spot(const Vec3f &point, const Vec3f& origin, float radius); }; From eaffb1492154edb31b99de591098f91e03628344 Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Wed, 22 Jun 2022 11:37:52 +0200 Subject: [PATCH 029/100] Improved stability supports - now accounts for base convex hull, decreases area of points if too close. --- src/libslic3r/CMakeLists.txt | 4 +- src/libslic3r/Print.cpp | 2 +- src/libslic3r/Print.hpp | 4 +- src/libslic3r/PrintObject.cpp | 20 ++--- ...esSearch.cpp => SupportSpotsGenerator.cpp} | 89 +++++++++++-------- ...esSearch.hpp => SupportSpotsGenerator.hpp} | 13 ++- 6 files changed, 73 insertions(+), 59 deletions(-) rename src/libslic3r/{SupportableIssuesSearch.cpp => SupportSpotsGenerator.cpp} (90%) rename src/libslic3r/{SupportableIssuesSearch.hpp => SupportSpotsGenerator.hpp} (80%) diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index 0a0062a754..e478188356 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -245,8 +245,8 @@ set(SLIC3R_SOURCES SlicingAdaptive.hpp Subdivide.cpp Subdivide.hpp - SupportableIssuesSearch.cpp - SupportableIssuesSearch.hpp + SupportSpotsGenerator.cpp + SupportSpotsGenerator.hpp SupportMaterial.cpp SupportMaterial.hpp Surface.cpp diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index a50974cafd..2979b3557b 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -826,7 +826,7 @@ void Print::process() for (PrintObject *obj : m_objects) obj->ironing(); for (PrintObject *obj : m_objects) - obj->find_supportable_issues(); + obj->generate_support_spots(); for (PrintObject *obj : m_objects) obj->generate_support_material(); if (this->set_started(psWipeTower)) { diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index c89b463a8f..c2777083d8 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -61,7 +61,7 @@ enum PrintStep : unsigned int { enum PrintObjectStep : unsigned int { posSlice, posPerimeters, posPrepareInfill, - posInfill, posIroning, posSupportableIssuesSearch, posSupportMaterial, posCount, + posInfill, posIroning, posSupportSpotsSearch, posSupportMaterial, posCount, }; // A PrintRegion object represents a group of volumes to print @@ -381,7 +381,7 @@ private: void prepare_infill(); void infill(); void ironing(); - void find_supportable_issues(); + void generate_support_spots(); void generate_support_material(); void slice_volumes(); diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 40f740b91c..322b6844ad 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -16,7 +16,7 @@ #include "Fill/FillAdaptive.hpp" #include "Fill/FillLightning.hpp" #include "Format/STL.hpp" -#include "SupportableIssuesSearch.hpp" +#include "SupportSpotsGenerator.hpp" #include "TriangleSelectorWrapper.hpp" #include "format.hpp" @@ -399,15 +399,15 @@ void PrintObject::ironing() } -void PrintObject::find_supportable_issues() +void PrintObject::generate_support_spots() { - if (this->set_started(posSupportableIssuesSearch)) { + if (this->set_started(posSupportSpotsSearch)) { BOOST_LOG_TRIVIAL(debug) - << "Searching supportable issues - start"; - m_print->set_status(75, L("Searching supportable issues")); + << "Searching support spots - start"; + m_print->set_status(75, L("Searching support spots")); if (!this->m_config.support_material) { - std::vector problematic_layers = SupportableIssues::quick_search(this); + std::vector problematic_layers = SupportSpotsGenerator::quick_search(this); if (!problematic_layers.empty()) { std::cout << "Object needs supports" << std::endl; this->active_step_add_warning(PrintStateBase::WarningLevel::CRITICAL, @@ -420,7 +420,7 @@ void PrintObject::find_supportable_issues() } } } else { - SupportableIssues::Issues issues = SupportableIssues::full_search(this); + SupportSpotsGenerator::Issues issues = SupportSpotsGenerator::full_search(this); //TODO fix // if (!issues.supports_nedded.empty()) { auto obj_transform = this->trafo_centered(); @@ -430,7 +430,7 @@ void PrintObject::find_supportable_issues() Transform3f inv_transform = (obj_transform * model_transformation).inverse().cast(); TriangleSelectorWrapper selector { model_volume->mesh() }; - for (const SupportableIssues::SupportPoint &support_point : issues.supports_nedded) { + for (const SupportSpotsGenerator::SupportPoint &support_point : issues.supports_nedded) { Vec3f point = Vec3f(inv_transform * support_point.position); Vec3f origin = Vec3f( inv_transform * Vec3f(support_point.position.x(), support_point.position.y(), 0.0f)); @@ -451,8 +451,8 @@ void PrintObject::find_supportable_issues() m_print->throw_if_canceled(); BOOST_LOG_TRIVIAL(debug) - << "Searching supportable issues - end"; - this->set_done(posSupportableIssuesSearch); + << "Searching support spots - end"; + this->set_done(posSupportSpotsSearch); } } diff --git a/src/libslic3r/SupportableIssuesSearch.cpp b/src/libslic3r/SupportSpotsGenerator.cpp similarity index 90% rename from src/libslic3r/SupportableIssuesSearch.cpp rename to src/libslic3r/SupportSpotsGenerator.cpp index ce17020ed7..1726b4cacb 100644 --- a/src/libslic3r/SupportableIssuesSearch.cpp +++ b/src/libslic3r/SupportSpotsGenerator.cpp @@ -1,4 +1,4 @@ -#include "SupportableIssuesSearch.hpp" +#include "SupportSpotsGenerator.hpp" #include "tbb/parallel_for.h" #include "tbb/blocked_range.h" @@ -55,7 +55,7 @@ auto get_b(ExtrusionLine &&l) { return l.b; } -namespace SupportableIssues { +namespace SupportSpotsGenerator { void Issues::add(const Issues &layer_issues) { supports_nedded.insert(supports_nedded.end(), layer_issues.supports_nedded.begin(), @@ -237,7 +237,8 @@ public: #ifdef DEBUG_FILES Vec3f get_accumulator_color(size_t id) { if (mapping.find(id) == mapping.end()) { - std::cerr << " ERROR: uknown accumulator ID: " << id << std::endl; + BOOST_LOG_TRIVIAL(debug) + << "SSG: ERROR: uknown accumulator ID: " << id; return Vec3f(1.0f, 1.0f, 1.0f); } @@ -332,7 +333,7 @@ void check_extrusion_entity_stability(const ExtrusionEntity *entity, size_t current_stability_acc = NULL_ACC_ID; ExtrusionPropertiesAccumulator bridging_acc { }; bridging_acc.add_distance(params.bridge_distance + 1.0f); // Initialise unsupported distance with larger than tolerable distance -> - // -> it prevents extruding perimeter start and short loops into air. + // -> it prevents extruding perimeter starts and short loops into air. const float flow_width = get_flow_width(layer_region, entity->role()); const float max_allowed_dist_from_prev_layer = flow_width; float distance_from_last_support_point = params.min_distance_between_support_points * 2.0f; @@ -376,9 +377,9 @@ void check_extrusion_entity_stability(const ExtrusionEntity *entity, if (distance_from_last_support_point > params.min_distance_between_support_points && bridging_acc.distance // if unsupported distance is larger than bridge distance linearly decreased by curvature, enforce supports. > params.bridge_distance - / (1.0f + (bridging_acc.max_curvature - * params.bridge_distance_decrease_by_curvature_factor / PI))) { - current_segment.add_support_point(current, params.extrusion_support_points_area); + / (bridging_acc.max_curvature + * params.bridge_distance_decrease_by_curvature_factor / PI)) { + current_segment.add_support_point(current, 0.0f); // Do not count extrusion supports into the support area. They can be very densely placed, causing algorithm to overestimate stickiness. issues.supports_nedded.emplace_back(to_vec3f(current), 1.0); bridging_acc.reset(); distance_from_last_support_point = 0.0f; @@ -394,13 +395,13 @@ void check_layer_global_stability(StabilityAccumulators &stability_accs, const std::vector &checked_lines, float print_z, const Params ¶ms) { - std::unordered_map> layer_accs_lines; + std::unordered_map> layer_accs_w_lines; for (size_t i = 0; i < checked_lines.size(); ++i) { - layer_accs_lines[&stability_accs.access(checked_lines[i].stability_accumulator_id)].push_back(i); + layer_accs_w_lines[&stability_accs.access(checked_lines[i].stability_accumulator_id)].push_back(i); } - for (auto &acc_lines : layer_accs_lines) { - StabilityAccumulator *acc = acc_lines.first; + for (auto &accumulator : layer_accs_w_lines) { + StabilityAccumulator *acc = accumulator.first; Vec3f centroid = acc->get_centroid(); Vec2f hull_centroid = unscaled(acc->segment_base_hull().centroid()).cast(); std::vector hull_lines; @@ -411,9 +412,9 @@ void check_layer_global_stability(StabilityAccumulators &stability_accs, } if (hull_lines.empty()) { if (acc->get_support_points().empty()) { - acc->add_support_point(Point::new_scale(checked_lines[acc_lines.second[0]].a), - params.support_points_interface_area); - issues.supports_nedded.emplace_back(to_3d(checked_lines[acc_lines.second[0]].a, print_z), 1.0); + acc->add_support_point(Point::new_scale(checked_lines[accumulator.second[0]].a), + params.support_points_interface_radius*params.support_points_interface_radius* float(PI) ); + issues.supports_nedded.emplace_back(to_3d(checked_lines[accumulator.second[0]].a, print_z), 1.0); } hull_lines.push_back( { unscaled(acc->get_support_points()[0]).cast(), unscaled(acc->get_support_points()[0]).cast() }); @@ -428,7 +429,7 @@ void check_layer_global_stability(StabilityAccumulators &stability_accs, float weight = mass * params.gravity_constant; float distance_from_last_support_point = params.min_distance_between_support_points * 2.0f; - for (size_t line_idx : acc_lines.second) { + for (size_t line_idx : accumulator.second) { const ExtrusionLine &line = checked_lines[line_idx]; distance_from_last_support_point += line.len; @@ -444,39 +445,51 @@ void check_layer_global_stability(StabilityAccumulators &stability_accs, float sticking_arm = (pivot - hull_centroid).norm(); float sticking_torque = sticking_arm * sticking_force; - std::cout << "sticking_arm: " << sticking_arm << std::endl; - std::cout << "sticking_torque: " << sticking_torque << std::endl; - float weight_arm = (pivot - centroid.head<2>()).norm(); float weight_torque = weight_arm * weight; - std::cout << "weight_arm: " << sticking_arm << std::endl; - std::cout << "weight_torque: " << weight_torque << std::endl; - float bed_movement_arm = centroid.z() - acc->get_base_height(); float bed_movement_force = params.max_acceleration * mass; float bed_movement_torque = bed_movement_force * bed_movement_arm; - std::cout << "bed_movement_arm: " << bed_movement_arm << std::endl; - std::cout << "bed_movement_torque: " << bed_movement_torque << std::endl; - float conflict_torque_arm = (to_3d(Vec2f(pivot - line.b), print_z).cross( extruder_pressure_direction)).norm(); float extruder_conflict_torque = params.tolerable_extruder_conflict_force * conflict_torque_arm; - - std::cout << "conflict_torque_arm: " << conflict_torque_arm << std::endl; - std::cout << "extruder_conflict_torque: " << extruder_conflict_torque << std::endl; - float total_torque = bed_movement_torque + extruder_conflict_torque - weight_torque - sticking_torque; - std::cout << "total_torque: " << total_torque << " printz: " << print_z << std::endl; - if (total_torque > 0) { - acc->add_support_point(Point::new_scale(line.b), std::min(params.support_points_interface_area, (pivot - line.b).squaredNorm())); + size_t _nearest_idx; + Vec2f _nearest_pt; + float area = params.support_points_interface_radius* params.support_points_interface_radius * float(PI); + float dist_from_hull = hull_distancer.signed_distance_from_lines(line.b, _nearest_idx, _nearest_pt); + if (dist_from_hull < params.support_points_interface_radius) { + area = std::max(0.0f, dist_from_hull*params.support_points_interface_radius * float(PI)); + } + + acc->add_support_point(Point::new_scale(line.b), area); issues.supports_nedded.emplace_back(to_3d(line.b, print_z), extruder_conflict_torque - sticking_torque); distance_from_last_support_point = 0.0f; } - +#if 0 + BOOST_LOG_TRIVIAL(debug) + << "SSG: sticking_arm: " << sticking_arm; + BOOST_LOG_TRIVIAL(debug) + << "SSG: sticking_torque: " << sticking_torque; + BOOST_LOG_TRIVIAL(debug) + << "SSG: weight_arm: " << sticking_arm; + BOOST_LOG_TRIVIAL(debug) + << "SSG: weight_torque: " << weight_torque; + BOOST_LOG_TRIVIAL(debug) + << "SSG: bed_movement_arm: " << bed_movement_arm; + BOOST_LOG_TRIVIAL(debug) + << "SSG: bed_movement_torque: " << bed_movement_torque; + BOOST_LOG_TRIVIAL(debug) + << "SSG: conflict_torque_arm: " << conflict_torque_arm; + BOOST_LOG_TRIVIAL(debug) + << "SSG: extruder_conflict_torque: " << extruder_conflict_torque; + BOOST_LOG_TRIVIAL(debug) + << "SSG: total_torque: " << total_torque << " printz: " << print_z; +#endif } } } @@ -557,7 +570,7 @@ Issues check_object_stability(const PrintObject *po, const Params ¶ms) { size_t nearest_line_idx; Vec2f nearest_pt; float dist = prev_layer_lines.signed_distance_from_lines(l.a, nearest_line_idx, nearest_pt); - if (std::abs(dist) < max_flow_width*1.1f) { + if (std::abs(dist) < max_flow_width) { size_t other_line_acc_id = prev_layer_lines.get_line(nearest_line_idx).stability_accumulator_id; size_t from_id = std::max(other_line_acc_id, l.stability_accumulator_id); size_t to_id = std::min(other_line_acc_id, l.stability_accumulator_id); @@ -610,8 +623,8 @@ Issues check_object_stability(const PrintObject *po, const Params ¶ms) { } fill_points.emplace_back(start, acc_id); } else { - std::cout << " SUPPORTS POINT GEN, start infill in the air? on printz: " << print_z - << std::endl; + BOOST_LOG_TRIVIAL(debug) + << "SSG: ERROR: seem that infill starts in the air? on printz: " << print_z; } } } // fill @@ -630,8 +643,8 @@ Issues check_object_stability(const PrintObject *po, const Params ¶ms) { size_t to_id = std::min(other_line_acc_id, fill_point.second); stability_accs.merge_accumulators(from_id, to_id); } else { - std::cout << " SUPPORTS POINT GEN, no connection on current layer for infill? on printz: " << print_z - << std::endl; + BOOST_LOG_TRIVIAL(debug) + << "SSG: ERROR: seem that infill starts in the air? on printz: " << print_z; } } @@ -698,7 +711,9 @@ std::vector quick_search(const PrintObject *po, const Params ¶ms) { Issues full_search(const PrintObject *po, const Params ¶ms) { auto issues = check_object_stability(po, params); +#ifdef DEBUG_FILES debug_export(issues, "issues"); +#endif return issues; } diff --git a/src/libslic3r/SupportableIssuesSearch.hpp b/src/libslic3r/SupportSpotsGenerator.hpp similarity index 80% rename from src/libslic3r/SupportableIssuesSearch.hpp rename to src/libslic3r/SupportSpotsGenerator.hpp index ccda2d77af..b7c987c928 100644 --- a/src/libslic3r/SupportableIssuesSearch.hpp +++ b/src/libslic3r/SupportSpotsGenerator.hpp @@ -5,22 +5,21 @@ namespace Slic3r { -namespace SupportableIssues { +namespace SupportSpotsGenerator { struct Params { const float gravity_constant = 9806.65f; // mm/s^2; gravity acceleration on Earth's surface, algorithm assumes that printer is in upwards position. - float bridge_distance = 10.0f; //mm - float bridge_distance_decrease_by_curvature_factor = 5.0f; // allowed bridge distance = bridge_distance / ( 1 + this factor * (curvature / PI) ) + float bridge_distance = 15.0f; //mm + float bridge_distance_decrease_by_curvature_factor = 5.0f; // >0 REQUIRED; allowed bridge distance = bridge_distance / (this factor * (curvature / PI) ) float min_distance_between_support_points = 0.5f; // Adhesion computation : from experiment, PLA holds about 3g per mm^2 of base area (with reserve); So it can withstand about 3*gravity_constant force per mm^2 float base_adhesion = 3.0f * gravity_constant; // adhesion per mm^2 of first layer - float support_adhesion = 3.0f * gravity_constant; // adhesion per mm^2 of support interface layer - float support_points_interface_area = 5.0f; // mm^2 - float extrusion_support_points_area = 0.5f; // much lower value, because these support points appear due to unsupported extrusion, - // not stability - they can be very densely placed, making the sticking estimation incorrect + float support_adhesion = 1.0f * gravity_constant; // adhesion per mm^2 of support interface layer + + float support_points_interface_radius = 0.5f; // mm float max_acceleration = 1000.0f; // mm/s^2 ; max acceleration of object (bed) in XY float filament_density = 1.25f * 0.001f; // g/mm^3 ; Common filaments are very lightweight, so precise number is not that important From d5a584a2c2bce03133395efbc7d1105fbc24a47b Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Wed, 22 Jun 2022 15:27:18 +0200 Subject: [PATCH 030/100] fixed bug with base layers merging to single accumulator --- src/libslic3r/SupportSpotsGenerator.cpp | 106 ++++++++++++++---------- src/libslic3r/SupportSpotsGenerator.hpp | 6 +- 2 files changed, 64 insertions(+), 48 deletions(-) diff --git a/src/libslic3r/SupportSpotsGenerator.cpp b/src/libslic3r/SupportSpotsGenerator.cpp index 1726b4cacb..6072b613d4 100644 --- a/src/libslic3r/SupportSpotsGenerator.cpp +++ b/src/libslic3r/SupportSpotsGenerator.cpp @@ -123,26 +123,23 @@ private: Points support_points { }; Vec3f centroid_accumulator = Vec3f::Zero(); float accumulated_volume { }; - float base_area { }; - float base_height { }; + float accumulated_sticking_force { }; public: - explicit StabilityAccumulator(float base_height) : - base_height(base_height) { - } + StabilityAccumulator() = default; - void add_base_extrusion(const ExtrusionLine &line, float width, float print_z, float mm3_per_mm) { - base_area += line.len * width; + void add_base_extrusion(const ExtrusionLine &line, float sticking_force, float print_z, float mm3_per_mm) { + accumulated_sticking_force += sticking_force; support_points.push_back(Point::new_scale(line.a)); support_points.push_back(Point::new_scale(line.b)); base_convex_hull.clear(); add_extrusion(line, print_z, mm3_per_mm); } - void add_support_point(const Point &position, float area) { + void add_support_point(const Point &position, float sticking_force) { support_points.push_back(position); base_convex_hull.clear(); - base_area += area; + accumulated_sticking_force += sticking_force; } void add_extrusion(const ExtrusionLine &line, float print_z, float mm3_per_mm) { @@ -156,12 +153,10 @@ public: return centroid_accumulator / accumulated_volume; } - float get_base_area() const { - return base_area; - } - float get_base_height() const { - return base_height; + float get_sticking_force() const { + return accumulated_sticking_force; } + float get_accumulated_volume() const { return accumulated_volume; } @@ -173,7 +168,7 @@ public: return this->base_convex_hull; } - const Points& get_support_points() { + const Points& get_support_points() const { return support_points; } @@ -183,7 +178,7 @@ public: base_convex_hull.clear(); this->centroid_accumulator += acc.centroid_accumulator; this->accumulated_volume += acc.accumulated_volume; - this->base_area += acc.base_area; + this->accumulated_sticking_force += acc.accumulated_sticking_force; } }; @@ -201,18 +196,18 @@ private: } to_acc.add_from(from_acc); mapping[from_id] = mapping[to_id]; - from_acc = StabilityAccumulator { 0.0f }; + from_acc = StabilityAccumulator { }; } public: StabilityAccumulators() = default; - int create_accumulator(float base_height) { + int create_accumulator() { size_t id = next_id; next_id++; mapping[id] = accumulators.size(); - accumulators.push_back(StabilityAccumulator { base_height }); + accumulators.push_back(StabilityAccumulator { }); return id; } @@ -231,7 +226,7 @@ public: } to_acc.add_from(from_acc); mapping[from_id] = mapping[to_id]; - from_acc = StabilityAccumulator { 0.0f }; + from_acc = StabilityAccumulator { }; } #ifdef DEBUG_FILES @@ -245,6 +240,18 @@ public: size_t pseudornd = ((mapping[id] + 127) * 33331 + 6907) % 987; return value_to_rgbf(0.0f, float(987), float(pseudornd)); } + + void log_accumulators(){ + for (size_t i = 0; i < accumulators.size(); ++i) { + const auto& acc = accumulators[i]; + BOOST_LOG_TRIVIAL(debug) + << "SSG: accumulator POS: " << i << "\n" + << "SSG: get_accumulated_volume: " << acc.get_accumulated_volume() << "\n" + << "SSG: get_sticking_force: " << acc.get_sticking_force() << "\n" + << "SSG: support points count: " << acc.get_support_points().size() << "\n"; + + } + } #endif }; @@ -369,7 +376,7 @@ void check_extrusion_entity_stability(const ExtrusionEntity *entity, } else { bridging_acc.add_distance(current_line.len); if (current_stability_acc == NULL_ACC_ID) { - current_stability_acc = stability_accs.create_accumulator(print_z); + current_stability_acc = stability_accs.create_accumulator(); } StabilityAccumulator ¤t_segment = stability_accs.access(current_stability_acc); current_line.stability_accumulator_id = current_stability_acc; @@ -379,7 +386,7 @@ void check_extrusion_entity_stability(const ExtrusionEntity *entity, > params.bridge_distance / (bridging_acc.max_curvature * params.bridge_distance_decrease_by_curvature_factor / PI)) { - current_segment.add_support_point(current, 0.0f); // Do not count extrusion supports into the support area. They can be very densely placed, causing algorithm to overestimate stickiness. + current_segment.add_support_point(current, 0.0f); // Do not count extrusion supports into the sticking force. They can be very densely placed, causing algorithm to overestimate stickiness. issues.supports_nedded.emplace_back(to_vec3f(current), 1.0); bridging_acc.reset(); distance_from_last_support_point = 0.0f; @@ -413,7 +420,8 @@ void check_layer_global_stability(StabilityAccumulators &stability_accs, if (hull_lines.empty()) { if (acc->get_support_points().empty()) { acc->add_support_point(Point::new_scale(checked_lines[accumulator.second[0]].a), - params.support_points_interface_radius*params.support_points_interface_radius* float(PI) ); + params.support_points_interface_radius * params.support_points_interface_radius * float(PI) + * params.support_adhesion); issues.supports_nedded.emplace_back(to_3d(checked_lines[accumulator.second[0]].a, print_z), 1.0); } hull_lines.push_back( { unscaled(acc->get_support_points()[0]).cast(), @@ -423,8 +431,7 @@ void check_layer_global_stability(StabilityAccumulators &stability_accs, LayerLinesDistancer hull_distancer(std::move(hull_lines)); - float sticking_force = acc->get_base_area() - * (acc->get_base_height() == 0 ? params.base_adhesion : params.support_adhesion); + float sticking_force = acc->get_sticking_force(); float mass = acc->get_accumulated_volume() * params.filament_density; float weight = mass * params.gravity_constant; @@ -435,7 +442,7 @@ void check_layer_global_stability(StabilityAccumulators &stability_accs, Vec3f extruder_pressure_direction = to_3d(Vec2f(line.b - line.a), 0.0f).normalized(); Vec2f pivot_site_search = line.b + extruder_pressure_direction.head<2>() * 1000.0f; - extruder_pressure_direction.z() = -0.1f; + extruder_pressure_direction.z() = -0.3f; extruder_pressure_direction.normalize(); size_t nearest_line_idx; @@ -448,7 +455,7 @@ void check_layer_global_stability(StabilityAccumulators &stability_accs, float weight_arm = (pivot - centroid.head<2>()).norm(); float weight_torque = weight_arm * weight; - float bed_movement_arm = centroid.z() - acc->get_base_height(); + float bed_movement_arm = centroid.z(); float bed_movement_force = params.max_acceleration * mass; float bed_movement_torque = bed_movement_force * bed_movement_arm; @@ -460,13 +467,14 @@ void check_layer_global_stability(StabilityAccumulators &stability_accs, if (total_torque > 0) { size_t _nearest_idx; Vec2f _nearest_pt; - float area = params.support_points_interface_radius* params.support_points_interface_radius * float(PI); + float area = params.support_points_interface_radius * params.support_points_interface_radius + * float(PI); float dist_from_hull = hull_distancer.signed_distance_from_lines(line.b, _nearest_idx, _nearest_pt); if (dist_from_hull < params.support_points_interface_radius) { - area = std::max(0.0f, dist_from_hull*params.support_points_interface_radius * float(PI)); + area = std::max(0.0f, dist_from_hull * params.support_points_interface_radius * float(PI)); } - - acc->add_support_point(Point::new_scale(line.b), area); + float sticking_force = area * params.support_adhesion; + acc->add_support_point(Point::new_scale(line.b), sticking_force); issues.supports_nedded.emplace_back(to_3d(line.b, print_z), extruder_conflict_torque - sticking_torque); distance_from_last_support_point = 0.0f; } @@ -513,7 +521,7 @@ Issues check_object_stability(const PrintObject *po, const Params ¶ms) { const float flow_width = get_flow_width(layer_region, perimeter->role()); max_flow_width = std::max(flow_width, max_flow_width); const float mm3_per_mm = float(perimeter->min_mm3_per_mm()); - int id = stability_accs.create_accumulator(base_print_z); + int id = stability_accs.create_accumulator(); StabilityAccumulator &acc = stability_accs.access(id); Points points { }; perimeter->collect_points(points); @@ -522,7 +530,8 @@ Issues check_object_stability(const PrintObject *po, const Params ¶ms) { Vec2f next = unscaled(points[point_idx + 1]).cast(); ExtrusionLine line { start, next }; line.stability_accumulator_id = id; - acc.add_base_extrusion(line, flow_width, base_print_z, mm3_per_mm); + float line_sticking_force = line.len * flow_width * params.base_adhesion; + acc.add_base_extrusion(line, line_sticking_force, base_print_z, mm3_per_mm); checked_lines.push_back(line); } if (perimeter->is_loop()) { @@ -530,7 +539,8 @@ Issues check_object_stability(const PrintObject *po, const Params ¶ms) { Vec2f next = unscaled(points[0]).cast(); ExtrusionLine line { start, next }; line.stability_accumulator_id = id; - acc.add_base_extrusion(line, flow_width, base_print_z, mm3_per_mm); + float line_sticking_force = line.len * flow_width * params.base_adhesion; + acc.add_base_extrusion(line, line_sticking_force, base_print_z, mm3_per_mm); checked_lines.push_back(line); } } // perimeter @@ -540,7 +550,7 @@ Issues check_object_stability(const PrintObject *po, const Params ¶ms) { const float flow_width = get_flow_width(layer_region, fill->role()); max_flow_width = std::max(flow_width, max_flow_width); const float mm3_per_mm = float(fill->min_mm3_per_mm()); - int id = stability_accs.create_accumulator(base_print_z); + int id = stability_accs.create_accumulator(); StabilityAccumulator &acc = stability_accs.access(id); Points points { }; fill->collect_points(points); @@ -549,27 +559,22 @@ Issues check_object_stability(const PrintObject *po, const Params ¶ms) { Vec2f next = unscaled(points[point_idx + 1]).cast(); ExtrusionLine line { start, next }; line.stability_accumulator_id = id; - acc.add_base_extrusion(line, flow_width, base_print_z, mm3_per_mm); + float line_sticking_force = line.len * flow_width * params.base_adhesion; + acc.add_base_extrusion(line, line_sticking_force, base_print_z, mm3_per_mm); checked_lines.push_back(line); } } // fill } // ex_entity } // region -#ifdef DEBUG_FILES - for (const auto &line : checked_lines) { - Vec3f color = stability_accs.get_accumulator_color(line.stability_accumulator_id); - fprintf(debug_acc, "v %f %f %f %f %f %f\n", line.b[0], - line.b[1], base_print_z, color[0], color[1], color[2]); - } -#endif - //MERGE BASE LAYER STABILITY ACCS prev_layer_lines = LayerLinesDistancer { std::move(checked_lines) }; for (const ExtrusionLine &l : prev_layer_lines.get_lines()) { size_t nearest_line_idx; Vec2f nearest_pt; - float dist = prev_layer_lines.signed_distance_from_lines(l.a, nearest_line_idx, nearest_pt); + Vec2f line_dir = (l.b - l.a).normalized(); + Vec2f site_search_location = l.a + Vec2f(line_dir.y(), -line_dir.x()) * max_flow_width; + float dist = prev_layer_lines.signed_distance_from_lines(site_search_location, nearest_line_idx, nearest_pt); if (std::abs(dist) < max_flow_width) { size_t other_line_acc_id = prev_layer_lines.get_line(nearest_line_idx).stability_accumulator_id; size_t from_id = std::max(other_line_acc_id, l.stability_accumulator_id); @@ -578,6 +583,16 @@ Issues check_object_stability(const PrintObject *po, const Params ¶ms) { } } +#ifdef DEBUG_FILES + for (const auto &line : prev_layer_lines.get_lines()) { + Vec3f color = stability_accs.get_accumulator_color(line.stability_accumulator_id); + fprintf(debug_acc, "v %f %f %f %f %f %f\n", line.b[0], + line.b[1], base_print_z, color[0], color[1], color[2]); + } + + stability_accs.log_accumulators(); +#endif + //CHECK STABILITY OF ALL LAYERS for (size_t layer_idx = 1; layer_idx < po->layer_count(); ++layer_idx) { const Layer *layer = po->layers()[layer_idx]; @@ -656,6 +671,7 @@ Issues check_object_stability(const PrintObject *po, const Params ¶ms) { fprintf(debug_acc, "v %f %f %f %f %f %f\n", line.b[0], line.b[1], print_z, color[0], color[1], color[2]); } + stability_accs.log_accumulators(); #endif } diff --git a/src/libslic3r/SupportSpotsGenerator.hpp b/src/libslic3r/SupportSpotsGenerator.hpp index b7c987c928..603076b291 100644 --- a/src/libslic3r/SupportSpotsGenerator.hpp +++ b/src/libslic3r/SupportSpotsGenerator.hpp @@ -10,7 +10,7 @@ namespace SupportSpotsGenerator { struct Params { const float gravity_constant = 9806.65f; // mm/s^2; gravity acceleration on Earth's surface, algorithm assumes that printer is in upwards position. - float bridge_distance = 15.0f; //mm + float bridge_distance = 10.0f; //mm float bridge_distance_decrease_by_curvature_factor = 5.0f; // >0 REQUIRED; allowed bridge distance = bridge_distance / (this factor * (curvature / PI) ) float min_distance_between_support_points = 0.5f; @@ -24,8 +24,8 @@ struct Params { float max_acceleration = 1000.0f; // mm/s^2 ; max acceleration of object (bed) in XY float filament_density = 1.25f * 0.001f; // g/mm^3 ; Common filaments are very lightweight, so precise number is not that important - float tolerable_extruder_conflict_force = 50.0f * gravity_constant; // force that can occasionally push the model due to various factors (filament leaks, small curling, ... ); current value corresponds to weight of 50g - float max_curled_conflict_extruder_force = 200.0f * gravity_constant; // for areas with possible high layered curled filaments, max force to account for; current value corresponds to weight of 200g + float tolerable_extruder_conflict_force = 50.0f * gravity_constant; // force that can occasionally push the model due to various factors (filament leaks, small curling, ... ); current value corresponds to weight of X grams + float max_curled_conflict_extruder_force = 300.0f * gravity_constant; // for areas with possible high layered curled filaments, max force to account for; }; struct SupportPoint { From 9294d5e604b81fc636ad08e50c4f8e22274b9c55 Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Thu, 23 Jun 2022 10:47:35 +0200 Subject: [PATCH 031/100] improved triangle tracking in triangle selector - if not hit registered, nearest triangle is taken instead --- src/libslic3r/SupportSpotsGenerator.cpp | 3 +-- src/libslic3r/SupportSpotsGenerator.hpp | 4 ++-- src/libslic3r/TriangleSelectorWrapper.cpp | 8 ++++++++ 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/libslic3r/SupportSpotsGenerator.cpp b/src/libslic3r/SupportSpotsGenerator.cpp index 6072b613d4..d6dff123b2 100644 --- a/src/libslic3r/SupportSpotsGenerator.cpp +++ b/src/libslic3r/SupportSpotsGenerator.cpp @@ -119,7 +119,6 @@ public: class StabilityAccumulator { private: - Polygon base_convex_hull { }; Points support_points { }; Vec3f centroid_accumulator = Vec3f::Zero(); float accumulated_volume { }; @@ -384,7 +383,7 @@ void check_extrusion_entity_stability(const ExtrusionEntity *entity, if (distance_from_last_support_point > params.min_distance_between_support_points && bridging_acc.distance // if unsupported distance is larger than bridge distance linearly decreased by curvature, enforce supports. > params.bridge_distance - / (bridging_acc.max_curvature + / (1.0f + bridging_acc.max_curvature * params.bridge_distance_decrease_by_curvature_factor / PI)) { current_segment.add_support_point(current, 0.0f); // Do not count extrusion supports into the sticking force. They can be very densely placed, causing algorithm to overestimate stickiness. issues.supports_nedded.emplace_back(to_vec3f(current), 1.0); diff --git a/src/libslic3r/SupportSpotsGenerator.hpp b/src/libslic3r/SupportSpotsGenerator.hpp index 603076b291..0390b32afd 100644 --- a/src/libslic3r/SupportSpotsGenerator.hpp +++ b/src/libslic3r/SupportSpotsGenerator.hpp @@ -11,7 +11,7 @@ struct Params { const float gravity_constant = 9806.65f; // mm/s^2; gravity acceleration on Earth's surface, algorithm assumes that printer is in upwards position. float bridge_distance = 10.0f; //mm - float bridge_distance_decrease_by_curvature_factor = 5.0f; // >0 REQUIRED; allowed bridge distance = bridge_distance / (this factor * (curvature / PI) ) + float bridge_distance_decrease_by_curvature_factor = 5.0f; // allowed bridge distance = bridge_distance / (this factor * (curvature / PI) ) float min_distance_between_support_points = 0.5f; @@ -21,7 +21,7 @@ struct Params { float support_points_interface_radius = 0.5f; // mm - float max_acceleration = 1000.0f; // mm/s^2 ; max acceleration of object (bed) in XY + float max_acceleration = 9*1000.0f; // mm/s^2 ; max acceleration of object (bed) in XY (NOTE: The max hit is received by the object in the jerk phase, so the usual machine limits are too low) float filament_density = 1.25f * 0.001f; // g/mm^3 ; Common filaments are very lightweight, so precise number is not that important float tolerable_extruder_conflict_force = 50.0f * gravity_constant; // force that can occasionally push the model due to various factors (filament leaks, small curling, ... ); current value corresponds to weight of X grams diff --git a/src/libslic3r/TriangleSelectorWrapper.cpp b/src/libslic3r/TriangleSelectorWrapper.cpp index ec716ebca3..c6040c721c 100644 --- a/src/libslic3r/TriangleSelectorWrapper.cpp +++ b/src/libslic3r/TriangleSelectorWrapper.cpp @@ -29,6 +29,14 @@ void TriangleSelectorWrapper::enforce_spot(const Vec3f &point, const Vec3f &orig break; } } + } else { + size_t hit_idx_out; + Vec3f hit_point_out; + AABBTreeIndirect::squared_distance_to_indexed_triangle_set(mesh.its.vertices, mesh.its.indices, triangles_tree, point, hit_idx_out, hit_point_out); + std::unique_ptr cursor = std::make_unique( + point, origin, radius, Transform3d::Identity(), TriangleSelector::ClippingPlane { }); + selector.select_patch(hit_idx_out, std::move(cursor), EnforcerBlockerType::ENFORCER, Transform3d::Identity(), + true, 0.0f); } } From 864c85d47e6099d9e0cd4fd3024f0a2b299e8f4b Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Fri, 24 Jun 2022 12:30:18 +0200 Subject: [PATCH 032/100] replace convex hull computation with KDTree, improve sticking centroid estimation --- src/libslic3r/PrintObject.cpp | 2 +- src/libslic3r/SupportSpotsGenerator.cpp | 131 +++++++++++------------- src/libslic3r/SupportSpotsGenerator.hpp | 6 +- 3 files changed, 65 insertions(+), 74 deletions(-) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 322b6844ad..92c002c8de 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -434,7 +434,7 @@ void PrintObject::generate_support_spots() Vec3f point = Vec3f(inv_transform * support_point.position); Vec3f origin = Vec3f( inv_transform * Vec3f(support_point.position.x(), support_point.position.y(), 0.0f)); - selector.enforce_spot(point, origin, 0.5f); + selector.enforce_spot(point, origin, 1.0f); } model_volume->supported_facets.set(selector.selector); diff --git a/src/libslic3r/SupportSpotsGenerator.cpp b/src/libslic3r/SupportSpotsGenerator.cpp index d6dff123b2..174b5bc041 100644 --- a/src/libslic3r/SupportSpotsGenerator.cpp +++ b/src/libslic3r/SupportSpotsGenerator.cpp @@ -9,6 +9,7 @@ #include #include "AABBTreeLines.hpp" +#include "KDTreeIndirect.hpp" #include "libslic3r/Layer.hpp" #include "libslic3r/ClipperUtils.hpp" #include "Geometry/ConvexHull.hpp" @@ -119,9 +120,10 @@ public: class StabilityAccumulator { private: - Points support_points { }; + std::vector support_points { }; Vec3f centroid_accumulator = Vec3f::Zero(); float accumulated_volume { }; + Vec2f sticking_centroid_accumulator = Vec2f::Zero(); float accumulated_sticking_force { }; public: @@ -129,16 +131,16 @@ public: void add_base_extrusion(const ExtrusionLine &line, float sticking_force, float print_z, float mm3_per_mm) { accumulated_sticking_force += sticking_force; - support_points.push_back(Point::new_scale(line.a)); - support_points.push_back(Point::new_scale(line.b)); - base_convex_hull.clear(); + sticking_centroid_accumulator += sticking_force * ((line.a + line.b) / 2.0f); + support_points.push_back(line.a); + support_points.push_back(line.b); add_extrusion(line, print_z, mm3_per_mm); } - void add_support_point(const Point &position, float sticking_force) { + void add_support_point(const Vec2f &position, float sticking_force) { support_points.push_back(position); - base_convex_hull.clear(); accumulated_sticking_force += sticking_force; + sticking_centroid_accumulator += sticking_force * position; } void add_extrusion(const ExtrusionLine &line, float print_z, float mm3_per_mm) { @@ -149,6 +151,9 @@ public: } Vec3f get_centroid() const { + if (accumulated_volume <= 0.0f) { + return Vec3f::Zero(); + } return centroid_accumulator / accumulated_volume; } @@ -160,24 +165,24 @@ public: return accumulated_volume; } - const Polygon& segment_base_hull() { - if (this->base_convex_hull.empty()) { - this->base_convex_hull = Geometry::convex_hull(this->support_points); - } - return this->base_convex_hull; + const std::vector& get_support_points() const { + return support_points; } - const Points& get_support_points() const { - return support_points; + Vec2f get_sticking_centroid() const { + if (accumulated_sticking_force <= 0.0f) { + return Vec2f::Zero(); + } + return sticking_centroid_accumulator / accumulated_sticking_force; } void add_from(const StabilityAccumulator &acc) { this->support_points.insert(this->support_points.end(), acc.support_points.begin(), acc.support_points.end()); - base_convex_hull.clear(); this->centroid_accumulator += acc.centroid_accumulator; this->accumulated_volume += acc.accumulated_volume; this->accumulated_sticking_force += acc.accumulated_sticking_force; + this->sticking_centroid_accumulator += acc.sticking_centroid_accumulator; } }; @@ -240,14 +245,14 @@ public: return value_to_rgbf(0.0f, float(987), float(pseudornd)); } - void log_accumulators(){ - for (size_t i = 0; i < accumulators.size(); ++i) { - const auto& acc = accumulators[i]; + void log_accumulators() { + for (size_t i = 0; i < accumulators.size(); ++i) { + const auto &acc = accumulators[i]; BOOST_LOG_TRIVIAL(debug) << "SSG: accumulator POS: " << i << "\n" - << "SSG: get_accumulated_volume: " << acc.get_accumulated_volume() << "\n" - << "SSG: get_sticking_force: " << acc.get_sticking_force() << "\n" - << "SSG: support points count: " << acc.get_support_points().size() << "\n"; + << "SSG: get_accumulated_volume: " << acc.get_accumulated_volume() << "\n" + << "SSG: get_sticking_force: " << acc.get_sticking_force() << "\n" + << "SSG: support points count: " << acc.get_support_points().size() << "\n"; } } @@ -312,9 +317,8 @@ void check_extrusion_entity_stability(const ExtrusionEntity *entity, params); } } else { //single extrusion path, with possible varying parameters - const auto to_vec3f = [print_z](const Point &point) { - Vec2f tmp = unscale(point).cast(); - return Vec3f(tmp.x(), tmp.y(), print_z); + const auto to_vec3f = [print_z](const Vec2f &point) { + return Vec3f(point.x(), point.y(), print_z); }; Points points { }; entity->collect_points(points); @@ -342,12 +346,9 @@ void check_extrusion_entity_stability(const ExtrusionEntity *entity, // -> it prevents extruding perimeter starts and short loops into air. const float flow_width = get_flow_width(layer_region, entity->role()); const float max_allowed_dist_from_prev_layer = flow_width; - float distance_from_last_support_point = params.min_distance_between_support_points * 2.0f; for (size_t line_idx = 0; line_idx < lines.size(); ++line_idx) { ExtrusionLine ¤t_line = lines[line_idx]; - Point current = Point::new_scale(current_line.b); - distance_from_last_support_point += current_line.len; float mm3_per_mm = float(entity->min_mm3_per_mm()); float curr_angle = 0; @@ -380,15 +381,13 @@ void check_extrusion_entity_stability(const ExtrusionEntity *entity, StabilityAccumulator ¤t_segment = stability_accs.access(current_stability_acc); current_line.stability_accumulator_id = current_stability_acc; current_segment.add_extrusion(current_line, print_z, mm3_per_mm); - if (distance_from_last_support_point > params.min_distance_between_support_points && - bridging_acc.distance // if unsupported distance is larger than bridge distance linearly decreased by curvature, enforce supports. + if (bridging_acc.distance // if unsupported distance is larger than bridge distance linearly decreased by curvature, enforce supports. > params.bridge_distance / (1.0f + bridging_acc.max_curvature * params.bridge_distance_decrease_by_curvature_factor / PI)) { - current_segment.add_support_point(current, 0.0f); // Do not count extrusion supports into the sticking force. They can be very densely placed, causing algorithm to overestimate stickiness. - issues.supports_nedded.emplace_back(to_vec3f(current), 1.0); + current_segment.add_support_point(current_line.b, 0.0f); // Do not count extrusion supports into the sticking force. They can be very densely placed, causing algorithm to overestimate stickiness. + issues.supports_nedded.emplace_back(to_vec3f(current_line.b), 1.0); bridging_acc.reset(); - distance_from_last_support_point = 0.0f; } } } @@ -398,6 +397,7 @@ void check_extrusion_entity_stability(const ExtrusionEntity *entity, void check_layer_global_stability(StabilityAccumulators &stability_accs, Issues &issues, + float flow_width, const std::vector &checked_lines, float print_z, const Params ¶ms) { @@ -408,53 +408,50 @@ void check_layer_global_stability(StabilityAccumulators &stability_accs, for (auto &accumulator : layer_accs_w_lines) { StabilityAccumulator *acc = accumulator.first; - Vec3f centroid = acc->get_centroid(); - Vec2f hull_centroid = unscaled(acc->segment_base_hull().centroid()).cast(); - std::vector hull_lines; - for (const Line &line : acc->segment_base_hull().lines()) { - Vec2f start = unscaled(line.a).cast(); - Vec2f next = unscaled(line.b).cast(); - hull_lines.push_back( { start, next }); - } - if (hull_lines.empty()) { - if (acc->get_support_points().empty()) { - acc->add_support_point(Point::new_scale(checked_lines[accumulator.second[0]].a), - params.support_points_interface_radius * params.support_points_interface_radius * float(PI) - * params.support_adhesion); - issues.supports_nedded.emplace_back(to_3d(checked_lines[accumulator.second[0]].a, print_z), 1.0); - } - hull_lines.push_back( { unscaled(acc->get_support_points()[0]).cast(), - unscaled(acc->get_support_points()[0]).cast() }); - hull_centroid = unscaled(acc->get_support_points()[0]).cast(); - } - LayerLinesDistancer hull_distancer(std::move(hull_lines)); + if (acc->get_support_points().empty()) { + acc->add_support_point(checked_lines[accumulator.second[0]].a, 0.0f); + issues.supports_nedded.emplace_back(to_3d(checked_lines[accumulator.second[0]].a, print_z), 0.0); + } + const std::vector &support_points = acc->get_support_points(); - float sticking_force = acc->get_sticking_force(); - float mass = acc->get_accumulated_volume() * params.filament_density; - float weight = mass * params.gravity_constant; + auto coord_fn = [&support_points](size_t idx, size_t dim) { + return support_points[idx][dim]; + }; + KDTreeIndirect<2, float, decltype(coord_fn)> tree(coord_fn, support_points.size()); float distance_from_last_support_point = params.min_distance_between_support_points * 2.0f; for (size_t line_idx : accumulator.second) { const ExtrusionLine &line = checked_lines[line_idx]; distance_from_last_support_point += line.len; + if (distance_from_last_support_point < params.min_distance_between_support_points) { + continue; + } + size_t nearest_supp_point_idx = find_closest_point(tree, line.b); + if ((line.b - support_points[nearest_supp_point_idx]).norm() < params.min_distance_between_support_points) { + continue; + } + Vec3f extruder_pressure_direction = to_3d(Vec2f(line.b - line.a), 0.0f).normalized(); Vec2f pivot_site_search = line.b + extruder_pressure_direction.head<2>() * 1000.0f; extruder_pressure_direction.z() = -0.3f; extruder_pressure_direction.normalize(); - size_t nearest_line_idx; - Vec2f pivot; - hull_distancer.signed_distance_from_lines(pivot_site_search, nearest_line_idx, pivot); + size_t pivot_idx = find_closest_point(tree, pivot_site_search); + const Vec2f &pivot = support_points[pivot_idx]; - float sticking_arm = (pivot - hull_centroid).norm(); - float sticking_torque = sticking_arm * sticking_force; + const Vec2f &sticking_centroid = acc->get_sticking_centroid(); + float sticking_arm = (pivot - sticking_centroid).norm(); + float sticking_torque = sticking_arm * acc->get_sticking_force(); - float weight_arm = (pivot - centroid.head<2>()).norm(); + float mass = acc->get_accumulated_volume() * params.filament_density; + const Vec3f &mass_centorid = acc->get_centroid(); + float weight = mass * params.gravity_constant; + float weight_arm = (pivot - mass_centorid.head<2>()).norm(); float weight_torque = weight_arm * weight; - float bed_movement_arm = centroid.z(); + float bed_movement_arm = mass_centorid.z(); float bed_movement_force = params.max_acceleration * mass; float bed_movement_torque = bed_movement_force * bed_movement_arm; @@ -464,20 +461,14 @@ void check_layer_global_stability(StabilityAccumulators &stability_accs, float total_torque = bed_movement_torque + extruder_conflict_torque - weight_torque - sticking_torque; if (total_torque > 0) { - size_t _nearest_idx; - Vec2f _nearest_pt; float area = params.support_points_interface_radius * params.support_points_interface_radius * float(PI); - float dist_from_hull = hull_distancer.signed_distance_from_lines(line.b, _nearest_idx, _nearest_pt); - if (dist_from_hull < params.support_points_interface_radius) { - area = std::max(0.0f, dist_from_hull * params.support_points_interface_radius * float(PI)); - } float sticking_force = area * params.support_adhesion; - acc->add_support_point(Point::new_scale(line.b), sticking_force); + acc->add_support_point(line.b, sticking_force); issues.supports_nedded.emplace_back(to_3d(line.b, print_z), extruder_conflict_torque - sticking_torque); distance_from_last_support_point = 0.0f; } -#if 0 +#if 1 BOOST_LOG_TRIVIAL(debug) << "SSG: sticking_arm: " << sticking_arm; BOOST_LOG_TRIVIAL(debug) @@ -662,7 +653,7 @@ Issues check_object_stability(const PrintObject *po, const Params ¶ms) { } } - check_layer_global_stability(stability_accs, issues, prev_layer_lines.get_lines(), print_z, params); + check_layer_global_stability(stability_accs, issues, max_flow_width, prev_layer_lines.get_lines(), print_z, params); #ifdef DEBUG_FILES for (const auto &line : prev_layer_lines.get_lines()) { diff --git a/src/libslic3r/SupportSpotsGenerator.hpp b/src/libslic3r/SupportSpotsGenerator.hpp index 0390b32afd..52b8b6078e 100644 --- a/src/libslic3r/SupportSpotsGenerator.hpp +++ b/src/libslic3r/SupportSpotsGenerator.hpp @@ -13,17 +13,17 @@ struct Params { float bridge_distance = 10.0f; //mm float bridge_distance_decrease_by_curvature_factor = 5.0f; // allowed bridge distance = bridge_distance / (this factor * (curvature / PI) ) - float min_distance_between_support_points = 0.5f; + float min_distance_between_support_points = 3.0f; // Adhesion computation : from experiment, PLA holds about 3g per mm^2 of base area (with reserve); So it can withstand about 3*gravity_constant force per mm^2 float base_adhesion = 3.0f * gravity_constant; // adhesion per mm^2 of first layer float support_adhesion = 1.0f * gravity_constant; // adhesion per mm^2 of support interface layer - float support_points_interface_radius = 0.5f; // mm + float support_points_interface_radius = 1.0f; // mm float max_acceleration = 9*1000.0f; // mm/s^2 ; max acceleration of object (bed) in XY (NOTE: The max hit is received by the object in the jerk phase, so the usual machine limits are too low) float filament_density = 1.25f * 0.001f; // g/mm^3 ; Common filaments are very lightweight, so precise number is not that important - + float tensile_strength = 33000.0f; // mN/mm^2; 33 MPa is tensile strength of ABS, which has the lowest tensile strength from common materials. float tolerable_extruder_conflict_force = 50.0f * gravity_constant; // force that can occasionally push the model due to various factors (filament leaks, small curling, ... ); current value corresponds to weight of X grams float max_curled_conflict_extruder_force = 300.0f * gravity_constant; // for areas with possible high layered curled filaments, max force to account for; }; From 8e2e4154bd647042eb07ee2f6911f7a041e5d2cc Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Fri, 24 Jun 2022 13:20:03 +0200 Subject: [PATCH 033/100] description of the functions --- src/libslic3r/SupportSpotsGenerator.cpp | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/libslic3r/SupportSpotsGenerator.cpp b/src/libslic3r/SupportSpotsGenerator.cpp index 174b5bc041..165467819d 100644 --- a/src/libslic3r/SupportSpotsGenerator.cpp +++ b/src/libslic3r/SupportSpotsGenerator.cpp @@ -118,6 +118,11 @@ public: } }; +// StabilityAccumulator accumulates extrusions for each connected model part from bed to current printed layer. +// If the originaly disconected parts meet in the layer, their stability accumulators get merged and continue as one. +// (think legs of table, which get connected when the top desk is being printed). +// The class gathers mass, centroid mass, sticking force (bed extrusions, support points) and sticking centroid for the +// connected part. These values are then used to check global part stability. class StabilityAccumulator { private: std::vector support_points { }; @@ -186,6 +191,9 @@ public: } }; +// StabilityAccumulators class is wrapper over the vector of stability accumualtors. It provides a level of indirection +// between accumulator ID and the accumulator instance itself. While each extrusion line has one id, which is asigned +// when algorithm reaches the line's layer, the accumulator this id points to can change due to merging. struct StabilityAccumulators { private: size_t next_id = 0; @@ -280,6 +288,9 @@ float get_flow_width(const LayerRegion *region, ExtrusionRole role) { } } +// Accumulator of current extruion path properties +// It remembers unsuported distance and maximum accumulated curvature over that distance. +// Used to determine local stability issues (too long bridges, extrusion curves into air) struct ExtrusionPropertiesAccumulator { float distance = 0; //accumulated distance float curvature = 0; //accumulated signed ccw angles @@ -301,6 +312,14 @@ struct ExtrusionPropertiesAccumulator { } }; +// check_extrusion_entity_stability checks each extrusion for local issues, appends the extrusion +// into checked lines, and gives it a stability accumulator id. If support is needed it pushes it +// into issues as well. +// Rules for stability accumulator id assigment: + // If there is close extrusion under, use min extrusion id between the id of the previous line, + // and id of line under. Also merge the accumulators of those two ids! + // If there is no close extrusion under, use id of the previous extrusion line. + // If there is no previous line, create new stability accumulator. void check_extrusion_entity_stability(const ExtrusionEntity *entity, StabilityAccumulators &stability_accs, Issues &issues, @@ -395,6 +414,11 @@ void check_extrusion_entity_stability(const ExtrusionEntity *entity, } } +//check_layer_global_stability checks stability of the accumulators that are still present on the current line +// ( this is determined from the gathered checked_lines vector) +// For each accumulator and each its extrusion, forces and torques (weight, bed movement, extruder pressure, stickness to bed) +// are computed and if stability is not sufficient, support points are added +// accumualtors are filtered by their pointer address, since one accumulator can have multiple IDs due to merging void check_layer_global_stability(StabilityAccumulators &stability_accs, Issues &issues, float flow_width, From 6a971b462dcbe44b7369509fd47296cc5ad66177 Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Mon, 27 Jun 2022 16:20:52 +0200 Subject: [PATCH 034/100] estimation of malformed and curled segments, increase extruder conflict power accordingly --- src/libslic3r/SupportSpotsGenerator.cpp | 62 ++++++++++++++++++------- src/libslic3r/SupportSpotsGenerator.hpp | 5 +- 2 files changed, 48 insertions(+), 19 deletions(-) diff --git a/src/libslic3r/SupportSpotsGenerator.cpp b/src/libslic3r/SupportSpotsGenerator.cpp index 165467819d..d89f7222a7 100644 --- a/src/libslic3r/SupportSpotsGenerator.cpp +++ b/src/libslic3r/SupportSpotsGenerator.cpp @@ -43,6 +43,7 @@ public: Vec2f b; float len; + float malformation = 0.0f; size_t stability_accumulator_id = NULL_ACC_ID; static const constexpr int Dim = 2; @@ -88,7 +89,7 @@ private: public: explicit LayerLinesDistancer(std::vector &&lines) : lines(lines) { - tree = AABBTreeLines::build_aabb_tree_over_indexed_lines(lines); + tree = AABBTreeLines::build_aabb_tree_over_indexed_lines(this->lines); } // negative sign means inside @@ -361,10 +362,10 @@ void check_extrusion_entity_stability(const ExtrusionEntity *entity, size_t current_stability_acc = NULL_ACC_ID; ExtrusionPropertiesAccumulator bridging_acc { }; + ExtrusionPropertiesAccumulator malformation_acc { }; bridging_acc.add_distance(params.bridge_distance + 1.0f); // Initialise unsupported distance with larger than tolerable distance -> // -> it prevents extruding perimeter starts and short loops into air. const float flow_width = get_flow_width(layer_region, entity->role()); - const float max_allowed_dist_from_prev_layer = flow_width; for (size_t line_idx = 0; line_idx < lines.size(); ++line_idx) { ExtrusionLine ¤t_line = lines[line_idx]; @@ -377,12 +378,14 @@ void check_extrusion_entity_stability(const ExtrusionEntity *entity, curr_angle = angle(v1, v2); } bridging_acc.add_angle(curr_angle); + malformation_acc.add_angle(curr_angle); size_t nearest_line_idx; Vec2f nearest_point; float dist_from_prev_layer = prev_layer_lines.signed_distance_from_lines(current_line.b, nearest_line_idx, nearest_point); - if (dist_from_prev_layer < max_allowed_dist_from_prev_layer) { + + if (dist_from_prev_layer < flow_width) { const ExtrusionLine &nearest_line = prev_layer_lines.get_line(nearest_line_idx); size_t acc_id = nearest_line.stability_accumulator_id; stability_accs.merge_accumulators(std::max(acc_id, current_stability_acc), @@ -409,6 +412,17 @@ void check_extrusion_entity_stability(const ExtrusionEntity *entity, bridging_acc.reset(); } } + + //malformation + if (fabs(dist_from_prev_layer) < flow_width*2.0f) { + const ExtrusionLine &nearest_line = prev_layer_lines.get_line(nearest_line_idx); + current_line.malformation += 0.7 * nearest_line.malformation; + } + if (dist_from_prev_layer > flow_width * 0.3) { + current_line.malformation += 0.6 + 0.4 * malformation_acc.max_curvature / PI; + } else { + malformation_acc.reset(); + } } checked_lines.insert(checked_lines.end(), lines.begin(), lines.end()); } @@ -425,17 +439,19 @@ void check_layer_global_stability(StabilityAccumulators &stability_accs, const std::vector &checked_lines, float print_z, const Params ¶ms) { - std::unordered_map> layer_accs_w_lines; + std::unordered_map> layer_accs_w_lines; for (size_t i = 0; i < checked_lines.size(); ++i) { - layer_accs_w_lines[&stability_accs.access(checked_lines[i].stability_accumulator_id)].push_back(i); + layer_accs_w_lines[&stability_accs.access(checked_lines[i].stability_accumulator_id)].push_back(checked_lines[i]); } for (auto &accumulator : layer_accs_w_lines) { StabilityAccumulator *acc = accumulator.first; + LayerLinesDistancer acc_lines(std::move(accumulator.second)); if (acc->get_support_points().empty()) { - acc->add_support_point(checked_lines[accumulator.second[0]].a, 0.0f); - issues.supports_nedded.emplace_back(to_3d(checked_lines[accumulator.second[0]].a, print_z), 0.0); + // acc_lines cannot be empty - if the accumulator has no extrusion in the current layer, it is not considered in stability computation + acc->add_support_point(acc_lines.get_line(0).a, 0.0f); + issues.supports_nedded.emplace_back(to_3d(acc_lines.get_line(0).a, print_z), 0.0); } const std::vector &support_points = acc->get_support_points(); @@ -445,8 +461,7 @@ void check_layer_global_stability(StabilityAccumulators &stability_accs, KDTreeIndirect<2, float, decltype(coord_fn)> tree(coord_fn, support_points.size()); float distance_from_last_support_point = params.min_distance_between_support_points * 2.0f; - for (size_t line_idx : accumulator.second) { - const ExtrusionLine &line = checked_lines[line_idx]; + for (const ExtrusionLine& line : acc_lines.get_lines()) { distance_from_last_support_point += line.len; if (distance_from_last_support_point < params.min_distance_between_support_points) { @@ -457,12 +472,9 @@ void check_layer_global_stability(StabilityAccumulators &stability_accs, continue; } - Vec3f extruder_pressure_direction = to_3d(Vec2f(line.b - line.a), 0.0f).normalized(); - Vec2f pivot_site_search = line.b + extruder_pressure_direction.head<2>() * 1000.0f; - extruder_pressure_direction.z() = -0.3f; - extruder_pressure_direction.normalize(); - - size_t pivot_idx = find_closest_point(tree, pivot_site_search); + Vec2f line_dir = (line.b - line.a).normalized(); + Vec2f pivot_site_search_point = line.b + line_dir * 300.0f; + size_t pivot_idx = find_closest_point(tree, pivot_site_search_point); const Vec2f &pivot = support_points[pivot_idx]; const Vec2f &sticking_centroid = acc->get_sticking_centroid(); @@ -479,16 +491,25 @@ void check_layer_global_stability(StabilityAccumulators &stability_accs, float bed_movement_force = params.max_acceleration * mass; float bed_movement_torque = bed_movement_force * bed_movement_arm; + Vec3f extruder_pressure_direction = to_3d(line_dir, 0.0f); + extruder_pressure_direction.z() = -0.2 - line.malformation * 0.5; + extruder_pressure_direction.normalize(); float conflict_torque_arm = (to_3d(Vec2f(pivot - line.b), print_z).cross( extruder_pressure_direction)).norm(); - float extruder_conflict_torque = params.tolerable_extruder_conflict_force * conflict_torque_arm; + float extruder_conflict_force = params.tolerable_extruder_conflict_force + + line.malformation * params.malformations_additive_conflict_extruder_force; + float extruder_conflict_torque = extruder_conflict_force * conflict_torque_arm; + float total_torque = bed_movement_torque + extruder_conflict_torque - weight_torque - sticking_torque; if (total_torque > 0) { + Vec2f target_point; + size_t _idx; + acc_lines.signed_distance_from_lines(pivot_site_search_point, _idx, target_point); float area = params.support_points_interface_radius * params.support_points_interface_radius * float(PI); float sticking_force = area * params.support_adhesion; - acc->add_support_point(line.b, sticking_force); + acc->add_support_point(target_point, sticking_force); issues.supports_nedded.emplace_back(to_3d(line.b, print_z), extruder_conflict_torque - sticking_torque); distance_from_last_support_point = 0.0f; } @@ -519,6 +540,7 @@ void check_layer_global_stability(StabilityAccumulators &stability_accs, Issues check_object_stability(const PrintObject *po, const Params ¶ms) { #ifdef DEBUG_FILES FILE *debug_acc = boost::nowide::fopen(debug_out_path("accumulators.obj").c_str(), "w"); + FILE *malform_f = boost::nowide::fopen(debug_out_path("malformations.obj").c_str(), "w"); #endif StabilityAccumulators stability_accs; LayerLinesDistancer prev_layer_lines { { } }; @@ -680,6 +702,11 @@ Issues check_object_stability(const PrintObject *po, const Params ¶ms) { check_layer_global_stability(stability_accs, issues, max_flow_width, prev_layer_lines.get_lines(), print_z, params); #ifdef DEBUG_FILES + for (const auto &line : prev_layer_lines.get_lines()) { + Vec3f color = value_to_rgbf(0, 5.0f, line.malformation); + fprintf(malform_f, "v %f %f %f %f %f %f\n", line.b[0], + line.b[1], print_z, color[0], color[1], color[2]); + } for (const auto &line : prev_layer_lines.get_lines()) { Vec3f color = stability_accs.get_accumulator_color(line.stability_accumulator_id); fprintf(debug_acc, "v %f %f %f %f %f %f\n", line.b[0], @@ -691,6 +718,7 @@ Issues check_object_stability(const PrintObject *po, const Params ¶ms) { #ifdef DEBUG_FILES fclose(debug_acc); + fclose(malform_f); #endif std::cout << " SUPP: " << issues.supports_nedded.size() << std::endl; diff --git a/src/libslic3r/SupportSpotsGenerator.hpp b/src/libslic3r/SupportSpotsGenerator.hpp index 52b8b6078e..b1aab8156d 100644 --- a/src/libslic3r/SupportSpotsGenerator.hpp +++ b/src/libslic3r/SupportSpotsGenerator.hpp @@ -13,7 +13,7 @@ struct Params { float bridge_distance = 10.0f; //mm float bridge_distance_decrease_by_curvature_factor = 5.0f; // allowed bridge distance = bridge_distance / (this factor * (curvature / PI) ) - float min_distance_between_support_points = 3.0f; + float min_distance_between_support_points = 1.5f; // Adhesion computation : from experiment, PLA holds about 3g per mm^2 of base area (with reserve); So it can withstand about 3*gravity_constant force per mm^2 float base_adhesion = 3.0f * gravity_constant; // adhesion per mm^2 of first layer @@ -25,7 +25,8 @@ struct Params { float filament_density = 1.25f * 0.001f; // g/mm^3 ; Common filaments are very lightweight, so precise number is not that important float tensile_strength = 33000.0f; // mN/mm^2; 33 MPa is tensile strength of ABS, which has the lowest tensile strength from common materials. float tolerable_extruder_conflict_force = 50.0f * gravity_constant; // force that can occasionally push the model due to various factors (filament leaks, small curling, ... ); current value corresponds to weight of X grams - float max_curled_conflict_extruder_force = 300.0f * gravity_constant; // for areas with possible high layered curled filaments, max force to account for; + float malformations_additive_conflict_extruder_force = 100.0f * gravity_constant; // for areas with possible high layered curled filaments + }; struct SupportPoint { From cf94c44fd56dd3d108ad58c21c48075c841780ee Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Mon, 27 Jun 2022 17:15:51 +0200 Subject: [PATCH 035/100] add voxel grid cache to suppress accumulation of stability support points --- src/libslic3r/SupportSpotsGenerator.cpp | 132 +++++++++++++++++------- src/libslic3r/SupportSpotsGenerator.hpp | 2 +- 2 files changed, 97 insertions(+), 37 deletions(-) diff --git a/src/libslic3r/SupportSpotsGenerator.cpp b/src/libslic3r/SupportSpotsGenerator.cpp index d89f7222a7..1af7b96aaf 100644 --- a/src/libslic3r/SupportSpotsGenerator.cpp +++ b/src/libslic3r/SupportSpotsGenerator.cpp @@ -81,6 +81,61 @@ CurledFilament::CurledFilament(const Vec3f &position) : position(position), estimated_height(0.0f) { } +struct VoxelGrid { +private: + Vec3f cell_size; + Vec3f origin; + Vec3f size; + Vec3i cell_count; + + std::unordered_set taken_cells { }; + +public: + VoxelGrid(const PrintObject *po, float voxel_size) { + cell_size = Vec3f(voxel_size, voxel_size, voxel_size); + + Vec2crd size_half = po->size().head<2>().cwiseQuotient(Vec2crd(2, 2)) + Vec2crd::Ones(); + Vec3f min = unscale(Vec3crd(-size_half.x(), -size_half.y(), 0)).cast() - cell_size; + Vec3f max = unscale(Vec3crd(size_half.x(), size_half.y(), po->height())).cast() + cell_size; + + origin = min; + size = max - min; + cell_count = size.cwiseQuotient(cell_size).cast() + Vec3i::Ones(); + } + + Vec3i to_cell_coords(const Vec3f &position) const { + Vec3i cell_coords = (position - this->origin).cwiseQuotient(this->cell_size).cast(); + return cell_coords; + } + + size_t to_cell_index(const Vec3i &cell_coords) const { + assert(cell_coords.x() >= 0); + assert(cell_coords.x() < cell_count.x()); + assert(cell_coords.y() >= 0); + assert(cell_coords.y() < cell_count.y()); + assert(cell_coords.z() >= 0); + assert(cell_coords.z() < cell_count.z()); + + return cell_coords.z() * cell_count.x() * cell_count.y() + + cell_coords.y() * cell_count.x() + + cell_coords.x(); + } + + Vec3f get_cell_center(const Vec3i &cell_coords) const { + return origin + cell_coords.cast().cwiseProduct(this->cell_size) + + this->cell_size.cwiseQuotient(Vec3f(2.0f, 2.0f, 2.0)); + } + + void take_position(const Vec3f &position) { + taken_cells.insert(to_cell_index(to_cell_coords(position))); + } + + bool position_taken(const Vec3f &position) const { + return taken_cells.find(to_cell_index(to_cell_coords(position))) != taken_cells.end(); + } + +}; + class LayerLinesDistancer { private: std::vector lines; @@ -317,10 +372,10 @@ struct ExtrusionPropertiesAccumulator { // into checked lines, and gives it a stability accumulator id. If support is needed it pushes it // into issues as well. // Rules for stability accumulator id assigment: - // If there is close extrusion under, use min extrusion id between the id of the previous line, - // and id of line under. Also merge the accumulators of those two ids! - // If there is no close extrusion under, use id of the previous extrusion line. - // If there is no previous line, create new stability accumulator. +// If there is close extrusion under, use min extrusion id between the id of the previous line, +// and id of line under. Also merge the accumulators of those two ids! +// If there is no close extrusion under, use id of the previous extrusion line. +// If there is no previous line, create new stability accumulator. void check_extrusion_entity_stability(const ExtrusionEntity *entity, StabilityAccumulators &stability_accs, Issues &issues, @@ -404,9 +459,9 @@ void check_extrusion_entity_stability(const ExtrusionEntity *entity, current_line.stability_accumulator_id = current_stability_acc; current_segment.add_extrusion(current_line, print_z, mm3_per_mm); if (bridging_acc.distance // if unsupported distance is larger than bridge distance linearly decreased by curvature, enforce supports. - > params.bridge_distance - / (1.0f + bridging_acc.max_curvature - * params.bridge_distance_decrease_by_curvature_factor / PI)) { + > params.bridge_distance + / (1.0f + bridging_acc.max_curvature + * params.bridge_distance_decrease_by_curvature_factor / PI)) { current_segment.add_support_point(current_line.b, 0.0f); // Do not count extrusion supports into the sticking force. They can be very densely placed, causing algorithm to overestimate stickiness. issues.supports_nedded.emplace_back(to_vec3f(current_line.b), 1.0); bridging_acc.reset(); @@ -414,7 +469,7 @@ void check_extrusion_entity_stability(const ExtrusionEntity *entity, } //malformation - if (fabs(dist_from_prev_layer) < flow_width*2.0f) { + if (fabs(dist_from_prev_layer) < flow_width * 2.0f) { const ExtrusionLine &nearest_line = prev_layer_lines.get_line(nearest_line_idx); current_line.malformation += 0.7 * nearest_line.malformation; } @@ -434,18 +489,22 @@ void check_extrusion_entity_stability(const ExtrusionEntity *entity, // are computed and if stability is not sufficient, support points are added // accumualtors are filtered by their pointer address, since one accumulator can have multiple IDs due to merging void check_layer_global_stability(StabilityAccumulators &stability_accs, + VoxelGrid &supports_presence_grid, Issues &issues, float flow_width, const std::vector &checked_lines, float print_z, - const Params ¶ms) { + const Params ¶ms, + std::mt19937_64& generator) { std::unordered_map> layer_accs_w_lines; for (size_t i = 0; i < checked_lines.size(); ++i) { - layer_accs_w_lines[&stability_accs.access(checked_lines[i].stability_accumulator_id)].push_back(checked_lines[i]); + layer_accs_w_lines[&stability_accs.access(checked_lines[i].stability_accumulator_id)].push_back( + checked_lines[i]); } for (auto &accumulator : layer_accs_w_lines) { StabilityAccumulator *acc = accumulator.first; + std::shuffle(accumulator.second.begin(), accumulator.second.end(), generator); LayerLinesDistancer acc_lines(std::move(accumulator.second)); if (acc->get_support_points().empty()) { @@ -458,23 +517,12 @@ void check_layer_global_stability(StabilityAccumulators &stability_accs, auto coord_fn = [&support_points](size_t idx, size_t dim) { return support_points[idx][dim]; }; - KDTreeIndirect<2, float, decltype(coord_fn)> tree(coord_fn, support_points.size()); - - float distance_from_last_support_point = params.min_distance_between_support_points * 2.0f; - for (const ExtrusionLine& line : acc_lines.get_lines()) { - distance_from_last_support_point += line.len; - - if (distance_from_last_support_point < params.min_distance_between_support_points) { - continue; - } - size_t nearest_supp_point_idx = find_closest_point(tree, line.b); - if ((line.b - support_points[nearest_supp_point_idx]).norm() < params.min_distance_between_support_points) { - continue; - } + KDTreeIndirect<2, float, decltype(coord_fn)> supports_tree(coord_fn, support_points.size()); + for (const ExtrusionLine &line : acc_lines.get_lines()) { Vec2f line_dir = (line.b - line.a).normalized(); Vec2f pivot_site_search_point = line.b + line_dir * 300.0f; - size_t pivot_idx = find_closest_point(tree, pivot_site_search_point); + size_t pivot_idx = find_closest_point(supports_tree, pivot_site_search_point); const Vec2f &pivot = support_points[pivot_idx]; const Vec2f &sticking_centroid = acc->get_sticking_centroid(); @@ -496,7 +544,7 @@ void check_layer_global_stability(StabilityAccumulators &stability_accs, extruder_pressure_direction.normalize(); float conflict_torque_arm = (to_3d(Vec2f(pivot - line.b), print_z).cross( extruder_pressure_direction)).norm(); - float extruder_conflict_force = params.tolerable_extruder_conflict_force + + float extruder_conflict_force = params.tolerable_extruder_conflict_force + line.malformation * params.malformations_additive_conflict_extruder_force; float extruder_conflict_torque = extruder_conflict_force * conflict_torque_arm; @@ -506,12 +554,15 @@ void check_layer_global_stability(StabilityAccumulators &stability_accs, Vec2f target_point; size_t _idx; acc_lines.signed_distance_from_lines(pivot_site_search_point, _idx, target_point); - float area = params.support_points_interface_radius * params.support_points_interface_radius - * float(PI); - float sticking_force = area * params.support_adhesion; - acc->add_support_point(target_point, sticking_force); - issues.supports_nedded.emplace_back(to_3d(line.b, print_z), extruder_conflict_torque - sticking_torque); - distance_from_last_support_point = 0.0f; + if (!supports_presence_grid.position_taken(to_3d(target_point, print_z))) { + float area = params.support_points_interface_radius * params.support_points_interface_radius + * float(PI); + float sticking_force = area * params.support_adhesion; + acc->add_support_point(target_point, sticking_force); + issues.supports_nedded.emplace_back(to_3d(target_point, print_z), + extruder_conflict_torque - sticking_torque); + supports_presence_grid.take_position(to_3d(target_point, print_z)); + } } #if 1 BOOST_LOG_TRIVIAL(debug) @@ -546,6 +597,8 @@ Issues check_object_stability(const PrintObject *po, const Params ¶ms) { LayerLinesDistancer prev_layer_lines { { } }; Issues issues { }; std::vector checked_lines; + VoxelGrid supports_presence_grid { po, params.min_distance_between_support_points }; + std::mt19937_64 generator { 27644437 }; // PREPARE BASE LAYER float max_flow_width = 0.0f; @@ -699,14 +752,21 @@ Issues check_object_stability(const PrintObject *po, const Params ¶ms) { } } - check_layer_global_stability(stability_accs, issues, max_flow_width, prev_layer_lines.get_lines(), print_z, params); + check_layer_global_stability(stability_accs, + supports_presence_grid, + issues, + max_flow_width, + prev_layer_lines.get_lines(), + print_z, + params, + generator); #ifdef DEBUG_FILES for (const auto &line : prev_layer_lines.get_lines()) { - Vec3f color = value_to_rgbf(0, 5.0f, line.malformation); - fprintf(malform_f, "v %f %f %f %f %f %f\n", line.b[0], - line.b[1], print_z, color[0], color[1], color[2]); - } + Vec3f color = value_to_rgbf(0, 5.0f, line.malformation); + fprintf(malform_f, "v %f %f %f %f %f %f\n", line.b[0], + line.b[1], print_z, color[0], color[1], color[2]); + } for (const auto &line : prev_layer_lines.get_lines()) { Vec3f color = stability_accs.get_accumulator_color(line.stability_accumulator_id); fprintf(debug_acc, "v %f %f %f %f %f %f\n", line.b[0], diff --git a/src/libslic3r/SupportSpotsGenerator.hpp b/src/libslic3r/SupportSpotsGenerator.hpp index b1aab8156d..63b4e55724 100644 --- a/src/libslic3r/SupportSpotsGenerator.hpp +++ b/src/libslic3r/SupportSpotsGenerator.hpp @@ -13,7 +13,7 @@ struct Params { float bridge_distance = 10.0f; //mm float bridge_distance_decrease_by_curvature_factor = 5.0f; // allowed bridge distance = bridge_distance / (this factor * (curvature / PI) ) - float min_distance_between_support_points = 1.5f; + float min_distance_between_support_points = 3.0f; // Adhesion computation : from experiment, PLA holds about 3g per mm^2 of base area (with reserve); So it can withstand about 3*gravity_constant force per mm^2 float base_adhesion = 3.0f * gravity_constant; // adhesion per mm^2 of first layer From 8e5cdf29baa8a9e01887876609788c6a896b8b27 Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Tue, 28 Jun 2022 13:40:22 +0200 Subject: [PATCH 036/100] improve curling model parameters, other small improvements --- src/libslic3r/PrintObject.cpp | 2 +- src/libslic3r/SupportSpotsGenerator.cpp | 15 +++++++-------- src/libslic3r/SupportSpotsGenerator.hpp | 22 +++++++++++----------- 3 files changed, 19 insertions(+), 20 deletions(-) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 92c002c8de..d923c810a7 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -434,7 +434,7 @@ void PrintObject::generate_support_spots() Vec3f point = Vec3f(inv_transform * support_point.position); Vec3f origin = Vec3f( inv_transform * Vec3f(support_point.position.x(), support_point.position.y(), 0.0f)); - selector.enforce_spot(point, origin, 1.0f); + selector.enforce_spot(point, origin, 0.3f); } model_volume->supported_facets.set(selector.selector); diff --git a/src/libslic3r/SupportSpotsGenerator.cpp b/src/libslic3r/SupportSpotsGenerator.cpp index 1af7b96aaf..b9b98e952b 100644 --- a/src/libslic3r/SupportSpotsGenerator.cpp +++ b/src/libslic3r/SupportSpotsGenerator.cpp @@ -433,7 +433,7 @@ void check_extrusion_entity_stability(const ExtrusionEntity *entity, curr_angle = angle(v1, v2); } bridging_acc.add_angle(curr_angle); - malformation_acc.add_angle(curr_angle); + malformation_acc.add_angle(std::max(0.0f,curr_angle)); size_t nearest_line_idx; Vec2f nearest_point; @@ -449,7 +449,6 @@ void check_extrusion_entity_stability(const ExtrusionEntity *entity, current_line.stability_accumulator_id = current_stability_acc; stability_accs.access(current_stability_acc).add_extrusion(current_line, print_z, mm3_per_mm); bridging_acc.reset(); - // TODO curving here } else { bridging_acc.add_distance(current_line.len); if (current_stability_acc == NULL_ACC_ID) { @@ -460,8 +459,8 @@ void check_extrusion_entity_stability(const ExtrusionEntity *entity, current_segment.add_extrusion(current_line, print_z, mm3_per_mm); if (bridging_acc.distance // if unsupported distance is larger than bridge distance linearly decreased by curvature, enforce supports. > params.bridge_distance - / (1.0f + bridging_acc.max_curvature - * params.bridge_distance_decrease_by_curvature_factor / PI)) { + / (1.0f + (bridging_acc.max_curvature + * params.bridge_distance_decrease_by_curvature_factor / PI))) { current_segment.add_support_point(current_line.b, 0.0f); // Do not count extrusion supports into the sticking force. They can be very densely placed, causing algorithm to overestimate stickiness. issues.supports_nedded.emplace_back(to_vec3f(current_line.b), 1.0); bridging_acc.reset(); @@ -471,10 +470,10 @@ void check_extrusion_entity_stability(const ExtrusionEntity *entity, //malformation if (fabs(dist_from_prev_layer) < flow_width * 2.0f) { const ExtrusionLine &nearest_line = prev_layer_lines.get_line(nearest_line_idx); - current_line.malformation += 0.7 * nearest_line.malformation; + current_line.malformation += 0.9 * nearest_line.malformation; } if (dist_from_prev_layer > flow_width * 0.3) { - current_line.malformation += 0.6 + 0.4 * malformation_acc.max_curvature / PI; + current_line.malformation += 0.15 * (0.6 + 0.4 * malformation_acc.max_curvature / PI); } else { malformation_acc.reset(); } @@ -545,7 +544,7 @@ void check_layer_global_stability(StabilityAccumulators &stability_accs, float conflict_torque_arm = (to_3d(Vec2f(pivot - line.b), print_z).cross( extruder_pressure_direction)).norm(); float extruder_conflict_force = params.tolerable_extruder_conflict_force + - line.malformation * params.malformations_additive_conflict_extruder_force; + std::min(line.malformation, 1.0f) * params.malformations_additive_conflict_extruder_force; float extruder_conflict_torque = extruder_conflict_force * conflict_torque_arm; float total_torque = bed_movement_torque + extruder_conflict_torque - weight_torque - sticking_torque; @@ -763,7 +762,7 @@ Issues check_object_stability(const PrintObject *po, const Params ¶ms) { #ifdef DEBUG_FILES for (const auto &line : prev_layer_lines.get_lines()) { - Vec3f color = value_to_rgbf(0, 5.0f, line.malformation); + Vec3f color = value_to_rgbf(0, 1.0f, line.malformation); fprintf(malform_f, "v %f %f %f %f %f %f\n", line.b[0], line.b[1], print_z, color[0], color[1], color[2]); } diff --git a/src/libslic3r/SupportSpotsGenerator.hpp b/src/libslic3r/SupportSpotsGenerator.hpp index 63b4e55724..ac5f0176d8 100644 --- a/src/libslic3r/SupportSpotsGenerator.hpp +++ b/src/libslic3r/SupportSpotsGenerator.hpp @@ -10,22 +10,22 @@ namespace SupportSpotsGenerator { struct Params { const float gravity_constant = 9806.65f; // mm/s^2; gravity acceleration on Earth's surface, algorithm assumes that printer is in upwards position. - float bridge_distance = 10.0f; //mm - float bridge_distance_decrease_by_curvature_factor = 5.0f; // allowed bridge distance = bridge_distance / (this factor * (curvature / PI) ) + const float bridge_distance = 12.0f; //mm + const float bridge_distance_decrease_by_curvature_factor = 5.0f; // allowed bridge distance = bridge_distance / (this factor * (curvature / PI) ) - float min_distance_between_support_points = 3.0f; + const float min_distance_between_support_points = 3.0f; // Adhesion computation : from experiment, PLA holds about 3g per mm^2 of base area (with reserve); So it can withstand about 3*gravity_constant force per mm^2 - float base_adhesion = 3.0f * gravity_constant; // adhesion per mm^2 of first layer - float support_adhesion = 1.0f * gravity_constant; // adhesion per mm^2 of support interface layer + const float base_adhesion = 3.0f * gravity_constant; // adhesion per mm^2 of first layer + const float support_adhesion = 1.0f * gravity_constant; // adhesion per mm^2 of support interface layer - float support_points_interface_radius = 1.0f; // mm + const float support_points_interface_radius = 0.3f; // mm - float max_acceleration = 9*1000.0f; // mm/s^2 ; max acceleration of object (bed) in XY (NOTE: The max hit is received by the object in the jerk phase, so the usual machine limits are too low) - float filament_density = 1.25f * 0.001f; // g/mm^3 ; Common filaments are very lightweight, so precise number is not that important - float tensile_strength = 33000.0f; // mN/mm^2; 33 MPa is tensile strength of ABS, which has the lowest tensile strength from common materials. - float tolerable_extruder_conflict_force = 50.0f * gravity_constant; // force that can occasionally push the model due to various factors (filament leaks, small curling, ... ); current value corresponds to weight of X grams - float malformations_additive_conflict_extruder_force = 100.0f * gravity_constant; // for areas with possible high layered curled filaments + const float max_acceleration = 9*1000.0f; // mm/s^2 ; max acceleration of object (bed) in XY (NOTE: The max hit is received by the object in the jerk phase, so the usual machine limits are too low) + const float filament_density = 1.25f * 0.001f; // g/mm^3 ; Common filaments are very lightweight, so precise number is not that important + const float tensile_strength = 33000.0f; // mN/mm^2; 33 MPa is tensile strength of ABS, which has the lowest tensile strength from common materials. + const float tolerable_extruder_conflict_force = 50.0f * gravity_constant; // force that can occasionally push the model due to various factors (filament leaks, small curling, ... ); current value corresponds to weight of X grams + const float malformations_additive_conflict_extruder_force = 100.0f * gravity_constant; // for areas with possible high layered curled filaments }; From 0187ed855e3f8332d7693499a29e908f3e8d0f24 Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Tue, 28 Jun 2022 14:49:08 +0200 Subject: [PATCH 037/100] do not consider concave angles for curling, they actually improve the issue --- src/libslic3r/SupportSpotsGenerator.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libslic3r/SupportSpotsGenerator.cpp b/src/libslic3r/SupportSpotsGenerator.cpp index b9b98e952b..7c84f7c847 100644 --- a/src/libslic3r/SupportSpotsGenerator.cpp +++ b/src/libslic3r/SupportSpotsGenerator.cpp @@ -473,7 +473,8 @@ void check_extrusion_entity_stability(const ExtrusionEntity *entity, current_line.malformation += 0.9 * nearest_line.malformation; } if (dist_from_prev_layer > flow_width * 0.3) { - current_line.malformation += 0.15 * (0.6 + 0.4 * malformation_acc.max_curvature / PI); + malformation_acc.add_distance(current_line.len); + current_line.malformation += 0.15 * (0.8 + 0.2 * malformation_acc.max_curvature / (1.0f + 0.5f*malformation_acc.distance)); } else { malformation_acc.reset(); } From 91ec455fa33ffec7eaf60c466d2b4965e74803cf Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Tue, 28 Jun 2022 15:03:39 +0200 Subject: [PATCH 038/100] remove unnecesary randomization --- src/libslic3r/SupportSpotsGenerator.cpp | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/libslic3r/SupportSpotsGenerator.cpp b/src/libslic3r/SupportSpotsGenerator.cpp index 7c84f7c847..ad475b9087 100644 --- a/src/libslic3r/SupportSpotsGenerator.cpp +++ b/src/libslic3r/SupportSpotsGenerator.cpp @@ -494,8 +494,7 @@ void check_layer_global_stability(StabilityAccumulators &stability_accs, float flow_width, const std::vector &checked_lines, float print_z, - const Params ¶ms, - std::mt19937_64& generator) { + const Params ¶ms) { std::unordered_map> layer_accs_w_lines; for (size_t i = 0; i < checked_lines.size(); ++i) { layer_accs_w_lines[&stability_accs.access(checked_lines[i].stability_accumulator_id)].push_back( @@ -504,7 +503,6 @@ void check_layer_global_stability(StabilityAccumulators &stability_accs, for (auto &accumulator : layer_accs_w_lines) { StabilityAccumulator *acc = accumulator.first; - std::shuffle(accumulator.second.begin(), accumulator.second.end(), generator); LayerLinesDistancer acc_lines(std::move(accumulator.second)); if (acc->get_support_points().empty()) { @@ -598,7 +596,6 @@ Issues check_object_stability(const PrintObject *po, const Params ¶ms) { Issues issues { }; std::vector checked_lines; VoxelGrid supports_presence_grid { po, params.min_distance_between_support_points }; - std::mt19937_64 generator { 27644437 }; // PREPARE BASE LAYER float max_flow_width = 0.0f; @@ -758,8 +755,7 @@ Issues check_object_stability(const PrintObject *po, const Params ¶ms) { max_flow_width, prev_layer_lines.get_lines(), print_z, - params, - generator); + params); #ifdef DEBUG_FILES for (const auto &line : prev_layer_lines.get_lines()) { From 263e16ca925f264a726f18579fe50fc67181c87a Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Wed, 29 Jun 2022 17:34:13 +0200 Subject: [PATCH 039/100] draft mode of recon_thin_islands, but not working properly --- src/libslic3r/SupportSpotsGenerator.cpp | 166 ++++++++++++++++++++---- 1 file changed, 141 insertions(+), 25 deletions(-) diff --git a/src/libslic3r/SupportSpotsGenerator.cpp b/src/libslic3r/SupportSpotsGenerator.cpp index ad475b9087..61829243be 100644 --- a/src/libslic3r/SupportSpotsGenerator.cpp +++ b/src/libslic3r/SupportSpotsGenerator.cpp @@ -29,10 +29,10 @@ class ExtrusionLine { public: ExtrusionLine() : - a(Vec2f::Zero()), b(Vec2f::Zero()), len(0.0f) { + a(Vec2f::Zero()), b(Vec2f::Zero()), len(0.0f), external_perimeter(false) { } - ExtrusionLine(const Vec2f &_a, const Vec2f &_b) : - a(_a), b(_b), len((_a - _b).norm()) { + ExtrusionLine(const Vec2f &_a, const Vec2f &_b, bool external_perimeter) : + a(_a), b(_b), len((_a - _b).norm()), external_perimeter(external_perimeter) { } float length() { @@ -42,6 +42,7 @@ public: Vec2f a; Vec2f b; float len; + bool external_perimeter; float malformation = 0.0f; size_t stability_accumulator_id = NULL_ACC_ID; @@ -172,6 +173,10 @@ public: const std::vector& get_lines() const { return lines; } + + void set_new_acc_id(size_t line_idx, size_t acc_id) { + lines[line_idx].stability_accumulator_id = acc_id; + } }; // StabilityAccumulator accumulates extrusions for each connected model part from bed to current printed layer. @@ -271,7 +276,7 @@ private: public: StabilityAccumulators() = default; - int create_accumulator() { + size_t create_accumulator() { size_t id = next_id; next_id++; mapping[id] = accumulators.size(); @@ -283,7 +288,9 @@ public: return accumulators[mapping[id]]; } - void merge_accumulators(size_t from_id, size_t to_id) { + void merge_accumulators(size_t id_a, size_t id_b) { + size_t from_id = std::max(id_a, id_b); + size_t to_id = std::min(id_a, id_b); if (from_id == NULL_ACC_ID || to_id == NULL_ACC_ID) { return; } @@ -399,7 +406,8 @@ void check_extrusion_entity_stability(const ExtrusionEntity *entity, entity->collect_points(points); std::vector lines; lines.reserve(points.size() * 1.5); - lines.emplace_back(unscaled(points[0]).cast(), unscaled(points[0]).cast()); + bool is_ex_perimeter = entity->role() == erExternalPerimeter; + lines.emplace_back(unscaled(points[0]).cast(), unscaled(points[0]).cast(), is_ex_perimeter); for (int point_idx = 0; point_idx < int(points.size() - 1); ++point_idx) { Vec2f start = unscaled(points[point_idx]).cast(); Vec2f next = unscaled(points[point_idx + 1]).cast(); @@ -411,7 +419,7 @@ void check_extrusion_entity_stability(const ExtrusionEntity *entity, for (int i = 0; i < lines_count; ++i) { Vec2f a(start + v * (i * step_size)); Vec2f b(start + v * ((i + 1) * step_size)); - lines.emplace_back(a, b); + lines.emplace_back(a, b, is_ex_perimeter); } } @@ -433,7 +441,7 @@ void check_extrusion_entity_stability(const ExtrusionEntity *entity, curr_angle = angle(v1, v2); } bridging_acc.add_angle(curr_angle); - malformation_acc.add_angle(std::max(0.0f,curr_angle)); + malformation_acc.add_angle(std::max(0.0f, curr_angle)); size_t nearest_line_idx; Vec2f nearest_point; @@ -443,8 +451,7 @@ void check_extrusion_entity_stability(const ExtrusionEntity *entity, if (dist_from_prev_layer < flow_width) { const ExtrusionLine &nearest_line = prev_layer_lines.get_line(nearest_line_idx); size_t acc_id = nearest_line.stability_accumulator_id; - stability_accs.merge_accumulators(std::max(acc_id, current_stability_acc), - std::min(acc_id, current_stability_acc)); + stability_accs.merge_accumulators(acc_id, current_stability_acc); current_stability_acc = std::min(acc_id, current_stability_acc); current_line.stability_accumulator_id = current_stability_acc; stability_accs.access(current_stability_acc).add_extrusion(current_line, print_z, mm3_per_mm); @@ -474,7 +481,8 @@ void check_extrusion_entity_stability(const ExtrusionEntity *entity, } if (dist_from_prev_layer > flow_width * 0.3) { malformation_acc.add_distance(current_line.len); - current_line.malformation += 0.15 * (0.8 + 0.2 * malformation_acc.max_curvature / (1.0f + 0.5f*malformation_acc.distance)); + current_line.malformation += 0.15 + * (0.8 + 0.2 * malformation_acc.max_curvature / (1.0f + 0.5f * malformation_acc.distance)); } else { malformation_acc.reset(); } @@ -562,7 +570,7 @@ void check_layer_global_stability(StabilityAccumulators &stability_accs, supports_presence_grid.take_position(to_3d(target_point, print_z)); } } -#if 1 +#if 0 BOOST_LOG_TRIVIAL(debug) << "SSG: sticking_arm: " << sticking_arm; BOOST_LOG_TRIVIAL(debug) @@ -586,7 +594,114 @@ void check_layer_global_stability(StabilityAccumulators &stability_accs, } } -Issues check_object_stability(const PrintObject *po, const Params ¶ms) { +void reckon_thin_islands(StabilityAccumulators &stability_accs, + float flow_width, + float mm3_per_mm, + float print_z, + LayerLinesDistancer &layer_lines, + const Params& params) { + const std::vector &lines = layer_lines.get_lines(); + std::vector> extrusions; //start and end idx (one beyond last extrusion) [start,end) + Vec2f current_pt = lines[0].a; + std::pair current_ext(0,1); + for (size_t lidx = 0; lidx < lines.size(); ++lidx) { + const ExtrusionLine& line = lines[lidx]; + if (line.a == current_pt) { + current_ext.second = lidx + 1; + } else { + extrusions.push_back(current_ext); + current_ext.first = lidx; + current_ext.second = lidx + 1; + } + current_pt = line.b; + } + + std::vector islands; + std::vector> island_extrusions; + for (size_t e = 0; e < extrusions.size(); ++e) { + if (lines[extrusions[e].first].external_perimeter) { + std::vector copy(extrusions[e].second - extrusions[e].first); + for (size_t ex_line_idx = extrusions[e].first; ex_line_idx < extrusions[e].second; ++ex_line_idx) { + copy[ex_line_idx-extrusions[e].first] = lines[ex_line_idx]; + } + islands.emplace_back(std::move(copy)); + island_extrusions.push_back({e}); + } + } + + for (size_t i = 0; i < islands.size(); ++i) { + for (size_t e = 0; e < extrusions.size(); ++e) { + if (!lines[extrusions[e].first].external_perimeter){ + size_t _idx; + Vec2f _pt; + if (islands[i].signed_distance_from_lines(lines[extrusions[e].first].a, _idx, _pt) < 0) { + island_extrusions[i].push_back(e); + } + } + } + } + + for (size_t i = 0; i < islands.size(); ++i) { + if (islands[i].get_lines().empty()) { + continue; + } + for (size_t j = 0; j < islands.size(); ++j) { + if (islands[j].get_lines().empty() || i == j) { + continue; + } + size_t _idx; + Vec2f _pt; + if (islands[i].signed_distance_from_lines(islands[j].get_line(0).a, _idx, _pt) < 0) { + island_extrusions[i].insert(island_extrusions[i].end(), island_extrusions[j].begin(), + island_extrusions[j].end()); + island_extrusions[j].clear(); + } + } + } + + size_t islands_count = 0; + for (const std::vector& island_ex : island_extrusions) { + if (island_ex.empty()) { + continue; + } + islands_count++; + float cross_section = 0.0f; + size_t acc_id = NULL_ACC_ID; + for (size_t extrusion_idx : island_ex) { + for (size_t lidx = extrusions[extrusion_idx].first; lidx < extrusions[extrusion_idx].second; ++lidx) { + const ExtrusionLine& line = lines[lidx]; + cross_section += line.len * flow_width; + stability_accs.merge_accumulators(acc_id, line.stability_accumulator_id); + acc_id = std::min(acc_id, line.stability_accumulator_id); + } + } + + float max_force = cross_section * params.tensile_strength; + + if (stability_accs.access(acc_id).get_sticking_force() > max_force) { + BOOST_LOG_TRIVIAL(debug) << "SSG: Forking new accumulator for island because tensile strenth is too low: " << max_force; + BOOST_LOG_TRIVIAL(debug) << "SSG: sticking force: " << stability_accs.access(acc_id).get_sticking_force(); + + size_t new_acc_id = stability_accs.create_accumulator(); + StabilityAccumulator& acc = stability_accs.access(new_acc_id); + + for (size_t extrusion_idx : island_ex) { + for (size_t lidx = extrusions[extrusion_idx].first; lidx < extrusions[extrusion_idx].second; ++lidx) { + const ExtrusionLine& line = lines[lidx]; + float tensile_strength = params.tensile_strength * line.len * flow_width; + acc.add_base_extrusion(line, tensile_strength, print_z, mm3_per_mm); + layer_lines.set_new_acc_id(lidx, new_acc_id); + } + } + } + } + + BOOST_LOG_TRIVIAL(debug) << "SSG: There are " << islands_count << " islands on printz: " << print_z; + +} + +Issues +check_object_stability(const PrintObject *po, const Params ¶ms) { #ifdef DEBUG_FILES FILE *debug_acc = boost::nowide::fopen(debug_out_path("accumulators.obj").c_str(), "w"); FILE *malform_f = boost::nowide::fopen(debug_out_path("malformations.obj").c_str(), "w"); @@ -614,7 +729,7 @@ Issues check_object_stability(const PrintObject *po, const Params ¶ms) { for (int point_idx = 0; point_idx < int(points.size() - 1); ++point_idx) { Vec2f start = unscaled(points[point_idx]).cast(); Vec2f next = unscaled(points[point_idx + 1]).cast(); - ExtrusionLine line { start, next }; + ExtrusionLine line { start, next, perimeter->role() == erExternalPerimeter }; line.stability_accumulator_id = id; float line_sticking_force = line.len * flow_width * params.base_adhesion; acc.add_base_extrusion(line, line_sticking_force, base_print_z, mm3_per_mm); @@ -623,7 +738,7 @@ Issues check_object_stability(const PrintObject *po, const Params ¶ms) { if (perimeter->is_loop()) { Vec2f start = unscaled(points[points.size() - 1]).cast(); Vec2f next = unscaled(points[0]).cast(); - ExtrusionLine line { start, next }; + ExtrusionLine line { start, next, perimeter->role() == erExternalPerimeter }; line.stability_accumulator_id = id; float line_sticking_force = line.len * flow_width * params.base_adhesion; acc.add_base_extrusion(line, line_sticking_force, base_print_z, mm3_per_mm); @@ -643,7 +758,7 @@ Issues check_object_stability(const PrintObject *po, const Params ¶ms) { for (int point_idx = 0; point_idx < int(points.size() - 1); ++point_idx) { Vec2f start = unscaled(points[point_idx]).cast(); Vec2f next = unscaled(points[point_idx + 1]).cast(); - ExtrusionLine line { start, next }; + ExtrusionLine line { start, next, false }; line.stability_accumulator_id = id; float line_sticking_force = line.len * flow_width * params.base_adhesion; acc.add_base_extrusion(line, line_sticking_force, base_print_z, mm3_per_mm); @@ -663,9 +778,7 @@ Issues check_object_stability(const PrintObject *po, const Params ¶ms) { float dist = prev_layer_lines.signed_distance_from_lines(site_search_location, nearest_line_idx, nearest_pt); if (std::abs(dist) < max_flow_width) { size_t other_line_acc_id = prev_layer_lines.get_line(nearest_line_idx).stability_accumulator_id; - size_t from_id = std::max(other_line_acc_id, l.stability_accumulator_id); - size_t to_id = std::min(other_line_acc_id, l.stability_accumulator_id); - stability_accs.merge_accumulators(from_id, to_id); + stability_accs.merge_accumulators(other_line_acc_id, l.stability_accumulator_id); } } @@ -718,7 +831,7 @@ Issues check_object_stability(const PrintObject *po, const Params ¶ms) { for (int point_idx = 0; point_idx < int(points.size() - 1); ++point_idx) { Vec2f start = unscaled(points[point_idx]).cast(); Vec2f next = unscaled(points[point_idx + 1]).cast(); - ExtrusionLine line { start, next }; + ExtrusionLine line { start, next, false }; line.stability_accumulator_id = acc_id; acc.add_extrusion(line, print_z, mm3_per_mm); } @@ -740,23 +853,25 @@ Issues check_object_stability(const PrintObject *po, const Params ¶ms) { float dist = prev_layer_lines.signed_distance_from_lines(fill_point.first, nearest_line_idx, nearest_pt); if (dist < max_fill_flow_width) { size_t other_line_acc_id = prev_layer_lines.get_line(nearest_line_idx).stability_accumulator_id; - size_t from_id = std::max(other_line_acc_id, fill_point.second); - size_t to_id = std::min(other_line_acc_id, fill_point.second); - stability_accs.merge_accumulators(from_id, to_id); + stability_accs.merge_accumulators(other_line_acc_id, fill_point.second); } else { BOOST_LOG_TRIVIAL(debug) << "SSG: ERROR: seem that infill starts in the air? on printz: " << print_z; } } + float flow_width = get_flow_width(layer->regions()[0], erExternalPerimeter); + check_layer_global_stability(stability_accs, supports_presence_grid, issues, - max_flow_width, + flow_width, prev_layer_lines.get_lines(), print_z, params); +// reckon_thin_islands(stability_accs, flow_width, flow_width*layer->height, print_z, prev_layer_lines, params); + #ifdef DEBUG_FILES for (const auto &line : prev_layer_lines.get_lines()) { Vec3f color = value_to_rgbf(0, 1.0f, line.malformation); @@ -823,7 +938,8 @@ std::vector quick_search(const PrintObject *po, const Params ¶ms) { return {}; } -Issues full_search(const PrintObject *po, const Params ¶ms) { +Issues +full_search(const PrintObject *po, const Params ¶ms) { auto issues = check_object_stability(po, params); #ifdef DEBUG_FILES debug_export(issues, "issues"); From 0a8f70c1bab07bcfb5b904f3173ce9fe21f88f6b Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Thu, 30 Jun 2022 17:10:51 +0200 Subject: [PATCH 040/100] inital phase of refactoring, segmentation should now build graph of connected sections --- src/libslic3r/CMakeLists.txt | 3 +- src/libslic3r/PrintObject.cpp | 2 +- src/libslic3r/SupportSpotsGenerator.cpp | 2 +- src/libslic3r/SupportSpotsGenerator.hpp | 18 +- .../SupportSpotsGeneratorRefactoring.cpp | 612 ++++++++++++++++++ 5 files changed, 620 insertions(+), 17 deletions(-) create mode 100644 src/libslic3r/SupportSpotsGeneratorRefactoring.cpp diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index e478188356..0c4fd20795 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -245,7 +245,8 @@ set(SLIC3R_SOURCES SlicingAdaptive.hpp Subdivide.cpp Subdivide.hpp - SupportSpotsGenerator.cpp +# SupportSpotsGenerator.cpp + SupportSpotsGeneratorRefactoring.cpp SupportSpotsGenerator.hpp SupportMaterial.cpp SupportMaterial.hpp diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index d923c810a7..674aaec26a 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -430,7 +430,7 @@ void PrintObject::generate_support_spots() Transform3f inv_transform = (obj_transform * model_transformation).inverse().cast(); TriangleSelectorWrapper selector { model_volume->mesh() }; - for (const SupportSpotsGenerator::SupportPoint &support_point : issues.supports_nedded) { + for (const SupportSpotsGenerator::SupportPoint &support_point : issues.support_points) { Vec3f point = Vec3f(inv_transform * support_point.position); Vec3f origin = Vec3f( inv_transform * Vec3f(support_point.position.x(), support_point.position.y(), 0.0f)); diff --git a/src/libslic3r/SupportSpotsGenerator.cpp b/src/libslic3r/SupportSpotsGenerator.cpp index 61829243be..08d734a38e 100644 --- a/src/libslic3r/SupportSpotsGenerator.cpp +++ b/src/libslic3r/SupportSpotsGenerator.cpp @@ -870,7 +870,7 @@ check_object_stability(const PrintObject *po, const Params ¶ms) { print_z, params); -// reckon_thin_islands(stability_accs, flow_width, flow_width*layer->height, print_z, prev_layer_lines, params); + reckon_thin_islands(stability_accs, flow_width, flow_width*layer->height, print_z, prev_layer_lines, params); #ifdef DEBUG_FILES for (const auto &line : prev_layer_lines.get_lines()) { diff --git a/src/libslic3r/SupportSpotsGenerator.hpp b/src/libslic3r/SupportSpotsGenerator.hpp index ac5f0176d8..2a9b48fddc 100644 --- a/src/libslic3r/SupportSpotsGenerator.hpp +++ b/src/libslic3r/SupportSpotsGenerator.hpp @@ -30,24 +30,14 @@ struct Params { }; struct SupportPoint { - SupportPoint(const Vec3f &position, float weight); + SupportPoint(const Vec3f &position, float force,const Vec3f& direction); Vec3f position; - float weight; -}; - -struct CurledFilament { - CurledFilament(const Vec3f &position, float estimated_height); - explicit CurledFilament(const Vec3f &position); - Vec3f position; - float estimated_height; + float force; + Vec3f direction; }; struct Issues { - std::vector supports_nedded; - std::vector curling_up; - - void add(const Issues &layer_issues); - bool empty() const; + std::vector support_points; }; std::vector quick_search(const PrintObject *po, const Params ¶ms = Params { }); diff --git a/src/libslic3r/SupportSpotsGeneratorRefactoring.cpp b/src/libslic3r/SupportSpotsGeneratorRefactoring.cpp new file mode 100644 index 0000000000..1fe3b6b2ab --- /dev/null +++ b/src/libslic3r/SupportSpotsGeneratorRefactoring.cpp @@ -0,0 +1,612 @@ +#include "SupportSpotsGenerator.hpp" + +#include "tbb/parallel_for.h" +#include "tbb/blocked_range.h" +#include "tbb/blocked_range2d.h" +#include "tbb/parallel_reduce.h" +#include +#include +#include +#include + +#include "AABBTreeLines.hpp" +#include "KDTreeIndirect.hpp" +#include "libslic3r/Layer.hpp" +#include "libslic3r/ClipperUtils.hpp" +#include "Geometry/ConvexHull.hpp" + +#define DEBUG_FILES + +#ifdef DEBUG_FILES +#include +#include "libslic3r/Color.hpp" +#endif + +namespace Slic3r { + +class ExtrusionLine +{ +public: + ExtrusionLine() : + a(Vec2f::Zero()), b(Vec2f::Zero()), len(0.0f), external_perimeter(false) { + } + ExtrusionLine(const Vec2f &_a, const Vec2f &_b, bool external_perimeter) : + a(_a), b(_b), len((_a - _b).norm()), external_perimeter(external_perimeter) { + } + + float length() { + return (a - b).norm(); + } + + Vec2f a; + Vec2f b; + float len; + bool external_perimeter; + + bool support_point_generated = false; + float malformation = 0.0f; + + static const constexpr int Dim = 2; + using Scalar = Vec2f::Scalar; +}; + +auto get_a(ExtrusionLine &&l) { + return l.a; +} +auto get_b(ExtrusionLine &&l) { + return l.b; +} + +namespace SupportSpotsGenerator { + +SupportPoint::SupportPoint(const Vec3f &position, float force, const Vec3f &direction) : + position(position), force(force), direction(direction) { +} + +class LinesDistancer { +private: + std::vector lines; + AABBTreeIndirect::Tree<2, float> tree; + +public: + explicit LinesDistancer(std::vector &lines) : + lines(lines) { + tree = AABBTreeLines::build_aabb_tree_over_indexed_lines(this->lines); + } + + // negative sign means inside + float signed_distance_from_lines(const Vec2f &point, size_t &nearest_line_index_out, + Vec2f &nearest_point_out) const { + auto distance = AABBTreeLines::squared_distance_to_indexed_lines(lines, tree, point, nearest_line_index_out, + nearest_point_out); + if (distance < 0) + return std::numeric_limits::infinity(); + + distance = sqrt(distance); + const ExtrusionLine &line = lines[nearest_line_index_out]; + Vec2f v1 = line.b - line.a; + Vec2f v2 = point - line.a; + if ((v1.x() * v2.y()) - (v1.y() * v2.x()) > 0.0) { + distance *= -1; + } + return distance; + } + + const ExtrusionLine& get_line(size_t line_idx) const { + return lines[line_idx]; + } + + const std::vector& get_lines() const { + return lines; + } +}; + +static const size_t NULL_ISLAND = std::numeric_limits::max(); + +class PixelGrid { + Vec2f pixel_size; + Vec2f origin; + Vec2f size; + Vec2i pixel_count; + + std::vector pixels { }; + +public: + PixelGrid(const PrintObject *po, float resolution) { + pixel_size = Vec2f(resolution, resolution); + + Vec2crd size_half = po->size().head<2>().cwiseQuotient(Vec2crd(2, 2)) + Vec2crd::Ones(); + Vec2f min = unscale(Vec2crd(-size_half.x(), -size_half.y())).cast(); + Vec2f max = unscale(Vec2crd(size_half.x(), size_half.y())).cast(); + + origin = min; + size = max - min; + pixel_count = size.cwiseQuotient(pixel_size).cast() + Vec2i::Ones(); + + pixels.resize(pixel_count.y() * pixel_count.x()); + clear(); + } + + void distribute_edge(const Vec2f &p1, const Vec2f &p2, size_t value) { + Vec2f dir = (p2 - p1); + float length = dir.norm(); + if (length < 0.1) { + return; + } + float step_size = this->pixel_size.x() / 2.0; + + float distributed_length = 0; + while (distributed_length < length) { + float next_len = std::min(length, distributed_length + step_size); + Vec2f location = p1 + ((next_len / length) * dir); + this->access_pixel(location) = value; + + distributed_length = next_len; + } + } + + void clear() { + for (size_t &val : pixels) { + val = NULL_ISLAND; + } + } + + float pixel_area() const { + return this->pixel_size.x() * this->pixel_size.y(); + } + + size_t get_pixel(const Vec2i &coords) const { + return pixels[this->to_pixel_index(coords)]; + } + + Vec2i get_pixel_count() { + return pixel_count; + } + +private: + Vec2i to_pixel_coords(const Vec2f &position) const { + Vec2i pixel_coords = (position - this->origin).cwiseQuotient(this->pixel_size).cast(); + return pixel_coords; + } + + size_t to_pixel_index(const Vec2i &pixel_coords) const { + assert(pixel_coords.x() >= 0); + assert(pixel_coords.x() < pixel_count.x()); + assert(pixel_coords.y() >= 0); + assert(pixel_coords.y() < pixel_count.y()); + + return pixel_coords.y() * pixel_count.x() + pixel_coords.x(); + } + + size_t& access_pixel(const Vec2f &position) { + return pixels[this->to_pixel_index(this->to_pixel_coords(position))]; + } +}; + +struct Island { + std::unordered_map islands_under_with_connection_area; + std::vector pivot_points; + float volume; + Vec3f volume_centroid; + float sticking_force; // for support points present on this layer (or bed extrusions) + Vec3f sticking_centroid; +}; + +struct LayerIslands { + std::vector islands; +}; + +float get_flow_width(const LayerRegion *region, ExtrusionRole role) { + switch (role) { + case ExtrusionRole::erBridgeInfill: + return region->flow(FlowRole::frExternalPerimeter).width(); + case ExtrusionRole::erExternalPerimeter: + return region->flow(FlowRole::frExternalPerimeter).width(); + case ExtrusionRole::erGapFill: + return region->flow(FlowRole::frInfill).width(); + case ExtrusionRole::erPerimeter: + return region->flow(FlowRole::frPerimeter).width(); + case ExtrusionRole::erSolidInfill: + return region->flow(FlowRole::frSolidInfill).width(); + case ExtrusionRole::erInternalInfill: + return region->flow(FlowRole::frInfill).width(); + case ExtrusionRole::erTopSolidInfill: + return region->flow(FlowRole::frTopSolidInfill).width(); + default: + return region->flow(FlowRole::frPerimeter).width(); + } +} + +// Accumulator of current extruion path properties +// It remembers unsuported distance and maximum accumulated curvature over that distance. +// Used to determine local stability issues (too long bridges, extrusion curves into air) +struct ExtrusionPropertiesAccumulator { + float distance = 0; //accumulated distance + float curvature = 0; //accumulated signed ccw angles + float max_curvature = 0; //max absolute accumulated value + + void add_distance(float dist) { + distance += dist; + } + + void add_angle(float ccw_angle) { + curvature += ccw_angle; + max_curvature = std::max(max_curvature, std::abs(curvature)); + } + + void reset() { + distance = 0; + curvature = 0; + max_curvature = 0; + } +}; + +void check_extrusion_entity_stability(const ExtrusionEntity *entity, + std::vector &checked_lines_out, + float print_z, + const LayerRegion *layer_region, + const LinesDistancer &prev_layer_lines, + Issues &issues, + const Params ¶ms) { + + if (entity->is_collection()) { + for (const auto *e : static_cast(entity)->entities) { + check_extrusion_entity_stability(e, checked_lines_out, print_z, layer_region, prev_layer_lines, + issues, params); + } + } else { //single extrusion path, with possible varying parameters + const auto to_vec3f = [print_z](const Vec2f &point) { + return Vec3f(point.x(), point.y(), print_z); + }; + Points points { }; + entity->collect_points(points); + std::vector lines; + lines.reserve(points.size() * 1.5); + bool is_ex_perimeter = entity->role() == erExternalPerimeter; + lines.emplace_back(unscaled(points[0]).cast(), unscaled(points[0]).cast(), is_ex_perimeter); + for (int point_idx = 0; point_idx < int(points.size() - 1); ++point_idx) { + Vec2f start = unscaled(points[point_idx]).cast(); + Vec2f next = unscaled(points[point_idx + 1]).cast(); + Vec2f v = next - start; // vector from next to current + float dist_to_next = v.norm(); + v.normalize(); + int lines_count = int(std::ceil(dist_to_next / params.bridge_distance)); + float step_size = dist_to_next / lines_count; + for (int i = 0; i < lines_count; ++i) { + Vec2f a(start + v * (i * step_size)); + Vec2f b(start + v * ((i + 1) * step_size)); + lines.emplace_back(a, b, is_ex_perimeter); + } + } + + ExtrusionPropertiesAccumulator bridging_acc { }; + ExtrusionPropertiesAccumulator malformation_acc { }; + bridging_acc.add_distance(params.bridge_distance + 1.0f); // Initialise unsupported distance with larger than tolerable distance -> + // -> it prevents extruding perimeter starts and short loops into air. + const float flow_width = get_flow_width(layer_region, entity->role()); + + for (size_t line_idx = 0; line_idx < lines.size(); ++line_idx) { + ExtrusionLine ¤t_line = lines[line_idx]; + float curr_angle = 0; + if (line_idx + 1 < lines.size()) { + const Vec2f v1 = current_line.b - current_line.a; + const Vec2f v2 = lines[line_idx + 1].b - lines[line_idx + 1].a; + curr_angle = angle(v1, v2); + } + bridging_acc.add_angle(curr_angle); + malformation_acc.add_angle(std::max(0.0f, curr_angle)); + + size_t nearest_line_idx; + Vec2f nearest_point; + float dist_from_prev_layer = prev_layer_lines.signed_distance_from_lines(current_line.b, nearest_line_idx, + nearest_point); + + if (dist_from_prev_layer < flow_width) { + bridging_acc.reset(); + } else { + bridging_acc.add_distance(current_line.len); + if (bridging_acc.distance // if unsupported distance is larger than bridge distance linearly decreased by curvature, enforce supports. + > params.bridge_distance + / (1.0f + (bridging_acc.max_curvature + * params.bridge_distance_decrease_by_curvature_factor / PI))) { + issues.support_points.emplace_back(to_vec3f(current_line.b), 0.0f, Vec3f(0.f, 0.0f, -1.0f)); + current_line.support_point_generated = true; + bridging_acc.reset(); + } + } + + //malformation + if (fabs(dist_from_prev_layer) < flow_width * 2.0f) { + const ExtrusionLine &nearest_line = prev_layer_lines.get_line(nearest_line_idx); + current_line.malformation += 0.9 * nearest_line.malformation; + } + if (dist_from_prev_layer > flow_width * 0.3) { + malformation_acc.add_distance(current_line.len); + current_line.malformation += 0.15 + * (0.8 + 0.2 * malformation_acc.max_curvature / (1.0f + 0.5f * malformation_acc.distance)); + } else { + malformation_acc.reset(); + } + } + checked_lines_out.insert(checked_lines_out.end(), lines.begin(), lines.end()); + } +} + +std::tuple reckon_islands( + const Layer *layer, bool first_layer, + size_t prev_layer_islands_count, + const PixelGrid &prev_layer_grid, + const std::vector &layer_lines, + const Params ¶ms) { + + BOOST_LOG_TRIVIAL(debug) << "SSG: reckon islands on printz: " << layer->print_z; + + std::vector> extrusions; //start and end idx (one beyond last extrusion) [start,end) + Vec2f current_pt = layer_lines[0].a; + std::pair current_ext(0, 1); + for (size_t lidx = 0; lidx < layer_lines.size(); ++lidx) { + const ExtrusionLine &line = layer_lines[lidx]; + if (line.a == current_pt) { + current_ext.second = lidx + 1; + } else { + extrusions.push_back(current_ext); + current_ext.first = lidx; + current_ext.second = lidx + 1; + } + current_pt = line.b; + } + + BOOST_LOG_TRIVIAL(debug) << "SSG: layer_lines size: " << layer_lines.size(); + + std::vector islands; + std::vector> island_extrusions; + for (size_t e = 0; e < extrusions.size(); ++e) { + if (layer_lines[extrusions[e].first].external_perimeter) { + std::vector copy(extrusions[e].second - extrusions[e].first); + for (size_t ex_line_idx = extrusions[e].first; ex_line_idx < extrusions[e].second; ++ex_line_idx) { + copy[ex_line_idx - extrusions[e].first] = layer_lines[ex_line_idx]; + } + islands.emplace_back(copy); + island_extrusions.push_back( { e }); + } + } + + BOOST_LOG_TRIVIAL(debug) << "SSG: external perims: " << islands.size(); + + for (size_t i = 0; i < islands.size(); ++i) { + for (size_t e = 0; e < extrusions.size(); ++e) { + if (!layer_lines[extrusions[e].first].external_perimeter) { + size_t _idx; + Vec2f _pt; + if (islands[i].signed_distance_from_lines(layer_lines[extrusions[e].first].a, _idx, _pt) < 0) { + island_extrusions[i].push_back(e); + } + } + } + } + + for (size_t i = 0; i < islands.size(); ++i) { + if (islands[i].get_lines().empty()) { + continue; + } + for (size_t j = 0; j < islands.size(); ++j) { + if (islands[j].get_lines().empty() || i == j) { + continue; + } + size_t _idx; + Vec2f _pt; + if (islands[i].signed_distance_from_lines(islands[j].get_line(0).a, _idx, _pt) < 0) { + island_extrusions[i].insert(island_extrusions[i].end(), island_extrusions[j].begin(), + island_extrusions[j].end()); + island_extrusions[j].clear(); + } + } + } + + BOOST_LOG_TRIVIAL(debug) << "SSG: filter islands"; + + float flow_width = get_flow_width(layer->regions()[0], erExternalPerimeter); + + LayerIslands result { }; + std::vector line_to_island_mapping(layer_lines.size(), NULL_ISLAND); + for (const std::vector &island_ex : island_extrusions) { + if (island_ex.empty()) { + continue; + } + + Island island { }; + for (size_t extrusion_idx : island_ex) { + for (size_t lidx = extrusions[extrusion_idx].first; lidx < extrusions[extrusion_idx].second; ++lidx) { + line_to_island_mapping[lidx] = result.islands.size(); + const ExtrusionLine &line = layer_lines[lidx]; + float volume = line.len * flow_width * layer->height * 0.7; // 1/sqrt(2) compensation for cylindrical shape + island.volume += volume; + island.volume_centroid += to_3d(Vec2f((line.a + line.b) / 2.0f), float(layer->print_z)) * volume; + + if (first_layer) { + float sticking_force = line.len * flow_width * params.base_adhesion; + island.sticking_force += sticking_force; + island.sticking_centroid += sticking_force + * to_3d(Vec2f((line.a + line.b) / 2.0f), float(layer->print_z)); + if (line.external_perimeter) { + island.pivot_points.push_back(to_3d(Vec2f(line.b), float(layer->print_z))); + } + } else if (layer_lines[lidx].support_point_generated) { + float support_interface_area = params.support_points_interface_radius + * params.support_points_interface_radius + * float(PI); + float sticking_force = support_interface_area * params.support_adhesion; + island.sticking_force += sticking_force; + island.sticking_centroid += sticking_force * to_3d(Vec2f(line.b), float(layer->print_z)); + island.pivot_points.push_back(to_3d(Vec2f(line.b), float(layer->print_z))); + } + } + } + result.islands.push_back(island); + } + + BOOST_LOG_TRIVIAL(debug) + << "SSG: There are " << result.islands.size() << " islands on printz: " << layer->print_z; + + PixelGrid current_layer_grid = prev_layer_grid; + current_layer_grid.clear(); + + tbb::parallel_for(tbb::blocked_range(0, layer_lines.size()), + [&layer_lines, ¤t_layer_grid, &line_to_island_mapping]( + tbb::blocked_range r) { + for (size_t i = r.begin(); i < r.end(); ++i) { + size_t island = line_to_island_mapping[i]; + const ExtrusionLine &line = layer_lines[i]; + current_layer_grid.distribute_edge(line.a, line.b, island); + } + }); + + BOOST_LOG_TRIVIAL(debug) << "SSG: rasterized"; + + for (size_t x = 0; x < size_t(current_layer_grid.get_pixel_count().x()); ++x) { + for (size_t y = 0; y < size_t(current_layer_grid.get_pixel_count().y()); ++y) { + Vec2i coords = Vec2i(x, y); + if (current_layer_grid.get_pixel(coords) != NULL_ISLAND + && prev_layer_grid.get_pixel(coords) != NULL_ISLAND) { + result.islands[current_layer_grid.get_pixel(coords)].islands_under_with_connection_area[prev_layer_grid.get_pixel(coords)] += + current_layer_grid.pixel_area(); + } + } + } + + BOOST_LOG_TRIVIAL(debug) << "SSG: connection area computed"; + + return {result, current_layer_grid}; +} + +Issues check_object_stability(const PrintObject *po, const Params ¶ms) { + Issues issues { }; + std::vector layer_lines; + float flow_width = get_flow_width(po->layers()[po->layer_count()-1]->regions()[0], erExternalPerimeter); + PixelGrid prev_layer_grid(po, flow_width); + BOOST_LOG_TRIVIAL(debug) << "SSG: flow width: " << flow_width; + + // PREPARE BASE LAYER + const Layer *layer = po->layers()[0]; + for (const LayerRegion *layer_region : layer->regions()) { + for (const ExtrusionEntity *ex_entity : layer_region->perimeters.entities) { + for (const ExtrusionEntity *perimeter : static_cast(ex_entity)->entities) { + Points points { }; + perimeter->collect_points(points); + for (int point_idx = 0; point_idx < int(points.size() - 1); ++point_idx) { + Vec2f start = unscaled(points[point_idx]).cast(); + Vec2f next = unscaled(points[point_idx + 1]).cast(); + ExtrusionLine line { start, next, perimeter->role() == erExternalPerimeter }; + layer_lines.push_back(line); + } + if (perimeter->is_loop()) { + Vec2f start = unscaled(points[points.size() - 1]).cast(); + Vec2f next = unscaled(points[0]).cast(); + ExtrusionLine line { start, next, perimeter->role() == erExternalPerimeter }; + layer_lines.push_back(line); + } + } // perimeter + } // ex_entity + for (const ExtrusionEntity *ex_entity : layer_region->fills.entities) { + for (const ExtrusionEntity *fill : static_cast(ex_entity)->entities) { + Points points { }; + fill->collect_points(points); + for (int point_idx = 0; point_idx < int(points.size() - 1); ++point_idx) { + Vec2f start = unscaled(points[point_idx]).cast(); + Vec2f next = unscaled(points[point_idx + 1]).cast(); + ExtrusionLine line { start, next, false }; + layer_lines.push_back(line); + } + } // fill + } // ex_entity + } // region + + auto [layer_islands, layer_grid] = reckon_islands(layer, true, 0, prev_layer_grid, layer_lines, params); + std::remove_if(layer_lines.begin(), layer_lines.end(), [](const ExtrusionLine &line) { + return !line.external_perimeter; + }); + LinesDistancer external_lines(layer_lines); + layer_lines.clear(); + prev_layer_grid = layer_grid; + + for (size_t layer_idx = 1; layer_idx < po->layer_count(); ++layer_idx) { + const Layer *layer = po->layers()[layer_idx]; + for (const LayerRegion *layer_region : layer->regions()) { + for (const ExtrusionEntity *ex_entity : layer_region->perimeters.entities) { + for (const ExtrusionEntity *perimeter : static_cast(ex_entity)->entities) { + check_extrusion_entity_stability(perimeter, layer_lines, layer->print_z, layer_region, + external_lines, issues, params); + } // perimeter + } // ex_entity + for (const ExtrusionEntity *ex_entity : layer_region->fills.entities) { + for (const ExtrusionEntity *fill : static_cast(ex_entity)->entities) { + if (fill->role() == ExtrusionRole::erGapFill + || fill->role() == ExtrusionRole::erBridgeInfill) { + check_extrusion_entity_stability(fill, layer_lines, layer->print_z, layer_region, + external_lines, issues, params); + } else { + Points points { }; + fill->collect_points(points); + for (int point_idx = 0; point_idx < int(points.size() - 1); ++point_idx) { + Vec2f start = unscaled(points[point_idx]).cast(); + Vec2f next = unscaled(points[point_idx + 1]).cast(); + ExtrusionLine line { start, next, false }; + layer_lines.push_back(line); + } + } + } // fill + } // ex_entity + } // region + + auto [layer_islands, layer_grid] = reckon_islands(layer, true, 0, prev_layer_grid, layer_lines, params); + std::remove_if(layer_lines.begin(), layer_lines.end(), [](const ExtrusionLine &line) { + return !line.external_perimeter; + }); + layer_lines = std::vector(); + LinesDistancer external_lines(layer_lines); + layer_lines.clear(); + prev_layer_grid = layer_grid; + } + + return issues; +} + +#ifdef DEBUG_FILES +void debug_export(Issues issues, std::string file_name) { + Slic3r::CNumericLocalesSetter locales_setter; + { + FILE *fp = boost::nowide::fopen(debug_out_path((file_name + "_supports.obj").c_str()).c_str(), "w"); + if (fp == nullptr) { + BOOST_LOG_TRIVIAL(error) + << "Debug files: Couldn't open " << file_name << " for writing"; + return; + } + + for (size_t i = 0; i < issues.support_points.size(); ++i) { + fprintf(fp, "v %f %f %f %f %f %f\n", issues.support_points[i].position(0), + issues.support_points[i].position(1), + issues.support_points[i].position(2), 1.0, 0.0, 1.0); + } + + fclose(fp); + } +} +#endif + +std::vector quick_search(const PrintObject *po, const Params ¶ms) { + check_object_stability(po, params); + return {}; +} + +Issues +full_search(const PrintObject *po, const Params ¶ms) { + auto issues = check_object_stability(po, params); +#ifdef DEBUG_FILES + debug_export(issues, "issues"); +#endif + return issues; + +} +} //SupportableIssues End +} + From 619309a1a4228255b5c2aed069a61193af506f8b Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Fri, 1 Jul 2022 09:26:42 +0200 Subject: [PATCH 041/100] bug fix - external extrusions were cleaned out before use --- src/libslic3r/SupportSpotsGeneratorRefactoring.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/libslic3r/SupportSpotsGeneratorRefactoring.cpp b/src/libslic3r/SupportSpotsGeneratorRefactoring.cpp index 1fe3b6b2ab..bbc4efb18e 100644 --- a/src/libslic3r/SupportSpotsGeneratorRefactoring.cpp +++ b/src/libslic3r/SupportSpotsGeneratorRefactoring.cpp @@ -562,8 +562,7 @@ Issues check_object_stability(const PrintObject *po, const Params ¶ms) { std::remove_if(layer_lines.begin(), layer_lines.end(), [](const ExtrusionLine &line) { return !line.external_perimeter; }); - layer_lines = std::vector(); - LinesDistancer external_lines(layer_lines); + external_lines = LinesDistancer(layer_lines); layer_lines.clear(); prev_layer_grid = layer_grid; } @@ -605,8 +604,8 @@ full_search(const PrintObject *po, const Params ¶ms) { debug_export(issues, "issues"); #endif return issues; - } + } //SupportableIssues End } From 7743bf250216287c1234975d8f448a578349fd1e Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Fri, 1 Jul 2022 11:51:04 +0200 Subject: [PATCH 042/100] store pointer to original extrusion in each line --- .../SupportSpotsGeneratorRefactoring.cpp | 56 ++++++++++--------- 1 file changed, 31 insertions(+), 25 deletions(-) diff --git a/src/libslic3r/SupportSpotsGeneratorRefactoring.cpp b/src/libslic3r/SupportSpotsGeneratorRefactoring.cpp index bbc4efb18e..86b8eacd4d 100644 --- a/src/libslic3r/SupportSpotsGeneratorRefactoring.cpp +++ b/src/libslic3r/SupportSpotsGeneratorRefactoring.cpp @@ -28,20 +28,25 @@ class ExtrusionLine { public: ExtrusionLine() : - a(Vec2f::Zero()), b(Vec2f::Zero()), len(0.0f), external_perimeter(false) { + a(Vec2f::Zero()), b(Vec2f::Zero()), len(0.0f), origin_entity(nullptr) { } - ExtrusionLine(const Vec2f &_a, const Vec2f &_b, bool external_perimeter) : - a(_a), b(_b), len((_a - _b).norm()), external_perimeter(external_perimeter) { + ExtrusionLine(const Vec2f &_a, const Vec2f &_b, const ExtrusionEntity* origin_entity) : + a(_a), b(_b), len((_a - _b).norm()), origin_entity(origin_entity) { } float length() { return (a - b).norm(); } + bool is_external_perimeter() const { + assert(origin_entity != nullptr); + return origin_entity->role() == erExternalPerimeter; + } + Vec2f a; Vec2f b; float len; - bool external_perimeter; + const ExtrusionEntity* origin_entity; bool support_point_generated = false; float malformation = 0.0f; @@ -262,8 +267,7 @@ void check_extrusion_entity_stability(const ExtrusionEntity *entity, entity->collect_points(points); std::vector lines; lines.reserve(points.size() * 1.5); - bool is_ex_perimeter = entity->role() == erExternalPerimeter; - lines.emplace_back(unscaled(points[0]).cast(), unscaled(points[0]).cast(), is_ex_perimeter); + lines.emplace_back(unscaled(points[0]).cast(), unscaled(points[0]).cast(), entity); for (int point_idx = 0; point_idx < int(points.size() - 1); ++point_idx) { Vec2f start = unscaled(points[point_idx]).cast(); Vec2f next = unscaled(points[point_idx + 1]).cast(); @@ -275,7 +279,7 @@ void check_extrusion_entity_stability(const ExtrusionEntity *entity, for (int i = 0; i < lines_count; ++i) { Vec2f a(start + v * (i * step_size)); Vec2f b(start + v * ((i + 1) * step_size)); - lines.emplace_back(a, b, is_ex_perimeter); + lines.emplace_back(a, b, entity); } } @@ -342,26 +346,28 @@ std::tuple reckon_islands( BOOST_LOG_TRIVIAL(debug) << "SSG: reckon islands on printz: " << layer->print_z; std::vector> extrusions; //start and end idx (one beyond last extrusion) [start,end) - Vec2f current_pt = layer_lines[0].a; - std::pair current_ext(0, 1); - for (size_t lidx = 0; lidx < layer_lines.size(); ++lidx) { + const ExtrusionEntity* current_ex = nullptr; + for (size_t lidx = 1; lidx < layer_lines.size(); ++lidx) { const ExtrusionLine &line = layer_lines[lidx]; - if (line.a == current_pt) { - current_ext.second = lidx + 1; + if (line.origin_entity == current_ex) { + extrusions.back().second = lidx + 1; } else { - extrusions.push_back(current_ext); - current_ext.first = lidx; - current_ext.second = lidx + 1; + extrusions.emplace_back(lidx, lidx + 1); + current_ex = line.origin_entity; } - current_pt = line.b; } BOOST_LOG_TRIVIAL(debug) << "SSG: layer_lines size: " << layer_lines.size(); + BOOST_LOG_TRIVIAL(debug) << "SSG: extrusions count: " << extrusions.size(); + BOOST_LOG_TRIVIAL(debug) << "SSG: extrusions sizes: "; + for (const auto& ext: extrusions) { + BOOST_LOG_TRIVIAL(debug) << "SSG: " << ext.second - ext.first; + } std::vector islands; std::vector> island_extrusions; for (size_t e = 0; e < extrusions.size(); ++e) { - if (layer_lines[extrusions[e].first].external_perimeter) { + if (layer_lines[extrusions[e].first].is_external_perimeter()) { std::vector copy(extrusions[e].second - extrusions[e].first); for (size_t ex_line_idx = extrusions[e].first; ex_line_idx < extrusions[e].second; ++ex_line_idx) { copy[ex_line_idx - extrusions[e].first] = layer_lines[ex_line_idx]; @@ -375,7 +381,7 @@ std::tuple reckon_islands( for (size_t i = 0; i < islands.size(); ++i) { for (size_t e = 0; e < extrusions.size(); ++e) { - if (!layer_lines[extrusions[e].first].external_perimeter) { + if (!layer_lines[extrusions[e].first].is_external_perimeter()) { size_t _idx; Vec2f _pt; if (islands[i].signed_distance_from_lines(layer_lines[extrusions[e].first].a, _idx, _pt) < 0) { @@ -428,7 +434,7 @@ std::tuple reckon_islands( island.sticking_force += sticking_force; island.sticking_centroid += sticking_force * to_3d(Vec2f((line.a + line.b) / 2.0f), float(layer->print_z)); - if (line.external_perimeter) { + if (line.is_external_perimeter()) { island.pivot_points.push_back(to_3d(Vec2f(line.b), float(layer->print_z))); } } else if (layer_lines[lidx].support_point_generated) { @@ -496,13 +502,13 @@ Issues check_object_stability(const PrintObject *po, const Params ¶ms) { for (int point_idx = 0; point_idx < int(points.size() - 1); ++point_idx) { Vec2f start = unscaled(points[point_idx]).cast(); Vec2f next = unscaled(points[point_idx + 1]).cast(); - ExtrusionLine line { start, next, perimeter->role() == erExternalPerimeter }; + ExtrusionLine line { start, next, perimeter }; layer_lines.push_back(line); } if (perimeter->is_loop()) { Vec2f start = unscaled(points[points.size() - 1]).cast(); Vec2f next = unscaled(points[0]).cast(); - ExtrusionLine line { start, next, perimeter->role() == erExternalPerimeter }; + ExtrusionLine line { start, next, perimeter }; layer_lines.push_back(line); } } // perimeter @@ -514,7 +520,7 @@ Issues check_object_stability(const PrintObject *po, const Params ¶ms) { for (int point_idx = 0; point_idx < int(points.size() - 1); ++point_idx) { Vec2f start = unscaled(points[point_idx]).cast(); Vec2f next = unscaled(points[point_idx + 1]).cast(); - ExtrusionLine line { start, next, false }; + ExtrusionLine line { start, next, fill }; layer_lines.push_back(line); } } // fill @@ -523,7 +529,7 @@ Issues check_object_stability(const PrintObject *po, const Params ¶ms) { auto [layer_islands, layer_grid] = reckon_islands(layer, true, 0, prev_layer_grid, layer_lines, params); std::remove_if(layer_lines.begin(), layer_lines.end(), [](const ExtrusionLine &line) { - return !line.external_perimeter; + return !line.is_external_perimeter(); }); LinesDistancer external_lines(layer_lines); layer_lines.clear(); @@ -550,7 +556,7 @@ Issues check_object_stability(const PrintObject *po, const Params ¶ms) { for (int point_idx = 0; point_idx < int(points.size() - 1); ++point_idx) { Vec2f start = unscaled(points[point_idx]).cast(); Vec2f next = unscaled(points[point_idx + 1]).cast(); - ExtrusionLine line { start, next, false }; + ExtrusionLine line { start, next, fill }; layer_lines.push_back(line); } } @@ -560,7 +566,7 @@ Issues check_object_stability(const PrintObject *po, const Params ¶ms) { auto [layer_islands, layer_grid] = reckon_islands(layer, true, 0, prev_layer_grid, layer_lines, params); std::remove_if(layer_lines.begin(), layer_lines.end(), [](const ExtrusionLine &line) { - return !line.external_perimeter; + return !line.is_external_perimeter(); }); external_lines = LinesDistancer(layer_lines); layer_lines.clear(); From 3e47b19b8614d68f9232c1f80c8dbbca6aece1fc Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Fri, 1 Jul 2022 16:13:00 +0200 Subject: [PATCH 043/100] added computation of stability accors the object graph, but not finished yet --- .../SupportSpotsGeneratorRefactoring.cpp | 258 +++++++++++++++--- src/libslic3r/TriangleSelectorWrapper.cpp | 16 +- 2 files changed, 236 insertions(+), 38 deletions(-) diff --git a/src/libslic3r/SupportSpotsGeneratorRefactoring.cpp b/src/libslic3r/SupportSpotsGeneratorRefactoring.cpp index 86b8eacd4d..23b249219a 100644 --- a/src/libslic3r/SupportSpotsGeneratorRefactoring.cpp +++ b/src/libslic3r/SupportSpotsGeneratorRefactoring.cpp @@ -30,7 +30,7 @@ public: ExtrusionLine() : a(Vec2f::Zero()), b(Vec2f::Zero()), len(0.0f), origin_entity(nullptr) { } - ExtrusionLine(const Vec2f &_a, const Vec2f &_b, const ExtrusionEntity* origin_entity) : + ExtrusionLine(const Vec2f &_a, const Vec2f &_b, const ExtrusionEntity *origin_entity) : a(_a), b(_b), len((_a - _b).norm()), origin_entity(origin_entity) { } @@ -46,7 +46,7 @@ public: Vec2f a; Vec2f b; float len; - const ExtrusionEntity* origin_entity; + const ExtrusionEntity *origin_entity; bool support_point_generated = false; float malformation = 0.0f; @@ -168,6 +168,11 @@ public: return pixel_count; } + Vec2f get_pixel_center(const Vec2i &coords) const { + return origin + coords.cast().cwiseProduct(this->pixel_size) + + this->pixel_size.cwiseQuotient(Vec2f(2.0f, 2.0f)); + } + private: Vec2i to_pixel_coords(const Vec2f &position) const { Vec2i pixel_coords = (position - this->origin).cwiseQuotient(this->pixel_size).cast(); @@ -192,9 +197,9 @@ struct Island { std::unordered_map islands_under_with_connection_area; std::vector pivot_points; float volume; - Vec3f volume_centroid; + Vec3f volume_centroid_accumulator; float sticking_force; // for support points present on this layer (or bed extrusions) - Vec3f sticking_centroid; + Vec3f sticking_centroid_accumulator; }; struct LayerIslands { @@ -305,7 +310,7 @@ void check_extrusion_entity_stability(const ExtrusionEntity *entity, float dist_from_prev_layer = prev_layer_lines.signed_distance_from_lines(current_line.b, nearest_line_idx, nearest_point); - if (dist_from_prev_layer < flow_width) { + if (fabs(dist_from_prev_layer) < flow_width) { bridging_acc.reset(); } else { bridging_acc.add_distance(current_line.len); @@ -336,17 +341,18 @@ void check_extrusion_entity_stability(const ExtrusionEntity *entity, } } -std::tuple reckon_islands( +std::tuple> reckon_islands( const Layer *layer, bool first_layer, size_t prev_layer_islands_count, const PixelGrid &prev_layer_grid, const std::vector &layer_lines, const Params ¶ms) { - BOOST_LOG_TRIVIAL(debug) << "SSG: reckon islands on printz: " << layer->print_z; + BOOST_LOG_TRIVIAL(debug) + << "SSG: reckon islands on printz: " << layer->print_z; std::vector> extrusions; //start and end idx (one beyond last extrusion) [start,end) - const ExtrusionEntity* current_ex = nullptr; + const ExtrusionEntity *current_ex = nullptr; for (size_t lidx = 1; lidx < layer_lines.size(); ++lidx) { const ExtrusionLine &line = layer_lines[lidx]; if (line.origin_entity == current_ex) { @@ -357,11 +363,15 @@ std::tuple reckon_islands( } } - BOOST_LOG_TRIVIAL(debug) << "SSG: layer_lines size: " << layer_lines.size(); - BOOST_LOG_TRIVIAL(debug) << "SSG: extrusions count: " << extrusions.size(); - BOOST_LOG_TRIVIAL(debug) << "SSG: extrusions sizes: "; - for (const auto& ext: extrusions) { - BOOST_LOG_TRIVIAL(debug) << "SSG: " << ext.second - ext.first; + BOOST_LOG_TRIVIAL(debug) + << "SSG: layer_lines size: " << layer_lines.size(); + BOOST_LOG_TRIVIAL(debug) + << "SSG: extrusions count: " << extrusions.size(); + BOOST_LOG_TRIVIAL(debug) + << "SSG: extrusions sizes: "; + for (const auto &ext : extrusions) { + BOOST_LOG_TRIVIAL(debug) + << "SSG: " << ext.second - ext.first; } std::vector islands; @@ -377,7 +387,8 @@ std::tuple reckon_islands( } } - BOOST_LOG_TRIVIAL(debug) << "SSG: external perims: " << islands.size(); + BOOST_LOG_TRIVIAL(debug) + << "SSG: external perims: " << islands.size(); for (size_t i = 0; i < islands.size(); ++i) { for (size_t e = 0; e < extrusions.size(); ++e) { @@ -409,7 +420,8 @@ std::tuple reckon_islands( } } - BOOST_LOG_TRIVIAL(debug) << "SSG: filter islands"; + BOOST_LOG_TRIVIAL(debug) + << "SSG: filter islands"; float flow_width = get_flow_width(layer->regions()[0], erExternalPerimeter); @@ -427,12 +439,12 @@ std::tuple reckon_islands( const ExtrusionLine &line = layer_lines[lidx]; float volume = line.len * flow_width * layer->height * 0.7; // 1/sqrt(2) compensation for cylindrical shape island.volume += volume; - island.volume_centroid += to_3d(Vec2f((line.a + line.b) / 2.0f), float(layer->print_z)) * volume; + island.volume_centroid_accumulator += to_3d(Vec2f((line.a + line.b) / 2.0f), float(layer->print_z)) * volume; if (first_layer) { float sticking_force = line.len * flow_width * params.base_adhesion; island.sticking_force += sticking_force; - island.sticking_centroid += sticking_force + island.sticking_centroid_accumulator += sticking_force * to_3d(Vec2f((line.a + line.b) / 2.0f), float(layer->print_z)); if (line.is_external_perimeter()) { island.pivot_points.push_back(to_3d(Vec2f(line.b), float(layer->print_z))); @@ -443,7 +455,7 @@ std::tuple reckon_islands( * float(PI); float sticking_force = support_interface_area * params.support_adhesion; island.sticking_force += sticking_force; - island.sticking_centroid += sticking_force * to_3d(Vec2f(line.b), float(layer->print_z)); + island.sticking_centroid_accumulator += sticking_force * to_3d(Vec2f(line.b), float(layer->print_z)); island.pivot_points.push_back(to_3d(Vec2f(line.b), float(layer->print_z))); } } @@ -467,30 +479,165 @@ std::tuple reckon_islands( } }); - BOOST_LOG_TRIVIAL(debug) << "SSG: rasterized"; + BOOST_LOG_TRIVIAL(debug) + << "SSG: rasterized"; for (size_t x = 0; x < size_t(current_layer_grid.get_pixel_count().x()); ++x) { for (size_t y = 0; y < size_t(current_layer_grid.get_pixel_count().y()); ++y) { Vec2i coords = Vec2i(x, y); if (current_layer_grid.get_pixel(coords) != NULL_ISLAND && prev_layer_grid.get_pixel(coords) != NULL_ISLAND) { - result.islands[current_layer_grid.get_pixel(coords)].islands_under_with_connection_area[prev_layer_grid.get_pixel(coords)] += + result.islands[current_layer_grid.get_pixel(coords)].islands_under_with_connection_area[prev_layer_grid.get_pixel( + coords)] += current_layer_grid.pixel_area(); } } } - BOOST_LOG_TRIVIAL(debug) << "SSG: connection area computed"; + BOOST_LOG_TRIVIAL(debug) + << "SSG: connection area computed"; - return {result, current_layer_grid}; + return {result, current_layer_grid, line_to_island_mapping}; +} + +void check_global_stability( + float print_z, + std::vector& islands_graph, + const std::vector &layer_lines, + const std::vector &line_to_island_mapping, + Issues& issues, + const Params& params + ) { + std::vector> islands_lines(islands_graph.back().islands.size()); + for (int lidx = 0; lidx < layer_lines.size(); ++lidx) { + if (layer_lines[lidx].origin_entity->role() == erExternalPerimeter) { + islands_lines[line_to_island_mapping[lidx]].push_back(lidx); + } + } + + using Accumulator = Island; + + for (size_t island_idx = 0; island_idx < islands_graph.back().islands.size(); ++island_idx) { + Island& island = islands_graph.back().islands[island_idx]; + + std::vector island_external_lines; + for (size_t lidx : islands_lines[island_idx]) { + island_external_lines.push_back(layer_lines[lidx]); + } + LinesDistancer island_lines_dist(island_external_lines); + + Accumulator acc = island; + int layer_idx = islands_graph.size() -1; + while (acc.islands_under_with_connection_area.size() > 0) { + //TEST for break between layer_idx and layer_idx -1; + LayerIslands below = islands_graph[layer_idx-1]; + std::vector pivot_points; + Vec2f sticking_centroid; + float connection_area = 0; + for (const auto& pair : acc.islands_under_with_connection_area) { + const Island& below_i = below.islands[pair.first]; + Vec2f centroid = (below_i.volume_centroid_accumulator / below_i.volume).head<2>(); + pivot_points.push_back(centroid); + sticking_centroid += centroid * pair.second; + connection_area += pair.second; + } + + sticking_centroid /= connection_area; + + auto coord_fn = [&pivot_points](size_t idx, size_t dim) { + return pivot_points[idx][dim]; + }; + KDTreeIndirect<2, float, decltype(coord_fn)> supports_tree(coord_fn, pivot_points.size()); + + for (const ExtrusionLine& line : island_external_lines){ + Vec2f line_dir = (line.b - line.a).normalized(); + Vec2f pivot_site_search_point = line.b + line_dir * 300.0f; + size_t pivot_idx = find_closest_point(supports_tree, pivot_site_search_point); + const Vec2f &pivot = pivot_points[pivot_idx]; + + float sticking_arm = (pivot - sticking_centroid).norm(); + float sticking_torque = sticking_arm * connection_area * params.tensile_strength; + + float mass = acc.volume * params.filament_density; + const Vec3f &mass_centorid = acc.volume_centroid_accumulator / acc.volume; + float weight = mass * params.gravity_constant; + float weight_arm = (pivot - mass_centorid.head<2>()).norm(); + float weight_torque = weight_arm * weight; + + float bed_movement_arm = mass_centorid.z(); + float bed_movement_force = params.max_acceleration * mass; + float bed_movement_torque = bed_movement_force * bed_movement_arm; + + Vec3f extruder_pressure_direction = to_3d(line_dir, 0.0f); + extruder_pressure_direction.z() = -0.2 - line.malformation * 0.5; + extruder_pressure_direction.normalize(); + float conflict_torque_arm = (to_3d(Vec2f(pivot - line.b), print_z).cross( + extruder_pressure_direction)).norm(); + float extruder_conflict_force = params.tolerable_extruder_conflict_force + + std::min(line.malformation, 1.0f) * params.malformations_additive_conflict_extruder_force; + float extruder_conflict_torque = extruder_conflict_force * conflict_torque_arm; + + float total_torque = bed_movement_torque + extruder_conflict_torque - weight_torque - sticking_torque; + + if (total_torque > 0) { + Vec2f target_point; + size_t _idx; + island_lines_dist.signed_distance_from_lines(pivot_site_search_point, _idx, target_point); +// if (!supports_presence_grid.position_taken(to_3d(target_point, print_z))) { + float area = params.support_points_interface_radius * params.support_points_interface_radius + * float(PI); + float sticking_force = area * params.support_adhesion; + Vec3f support_point = to_3d(target_point, print_z); + island.pivot_points.push_back(support_point); + island.sticking_force += sticking_force; + island.sticking_centroid_accumulator += sticking_force*support_point; + issues.support_points.emplace_back(support_point, + extruder_conflict_torque - sticking_torque, extruder_pressure_direction); +// supports_presence_grid.take_position(to_3d(target_point, print_z)); +// } + } + #if 0 + BOOST_LOG_TRIVIAL(debug) + << "SSG: sticking_arm: " << sticking_arm; + BOOST_LOG_TRIVIAL(debug) + << "SSG: sticking_torque: " << sticking_torque; + BOOST_LOG_TRIVIAL(debug) + << "SSG: weight_arm: " << sticking_arm; + BOOST_LOG_TRIVIAL(debug) + << "SSG: weight_torque: " << weight_torque; + BOOST_LOG_TRIVIAL(debug) + << "SSG: bed_movement_arm: " << bed_movement_arm; + BOOST_LOG_TRIVIAL(debug) + << "SSG: bed_movement_torque: " << bed_movement_torque; + BOOST_LOG_TRIVIAL(debug) + << "SSG: conflict_torque_arm: " << conflict_torque_arm; + BOOST_LOG_TRIVIAL(debug) + << "SSG: extruder_conflict_torque: " << extruder_conflict_torque; + BOOST_LOG_TRIVIAL(debug) + << "SSG: total_torque: " << total_torque << " printz: " << print_z; + #endif + } + + //TODO add stuf to accumulator + + + } + } } Issues check_object_stability(const PrintObject *po, const Params ¶ms) { +#ifdef DEBUG_FILES + FILE *segmentation_f = boost::nowide::fopen(debug_out_path("segmentation.obj").c_str(), "w"); + FILE *malform_f = boost::nowide::fopen(debug_out_path("malformations.obj").c_str(), "w"); +#endif + Issues issues { }; + std::vector islands_graph; std::vector layer_lines; - float flow_width = get_flow_width(po->layers()[po->layer_count()-1]->regions()[0], erExternalPerimeter); + float flow_width = get_flow_width(po->layers()[po->layer_count() - 1]->regions()[0], erExternalPerimeter); PixelGrid prev_layer_grid(po, flow_width); - BOOST_LOG_TRIVIAL(debug) << "SSG: flow width: " << flow_width; + BOOST_LOG_TRIVIAL(debug) + << "SSG: flow width: " << flow_width; // PREPARE BASE LAYER const Layer *layer = po->layers()[0]; @@ -527,10 +674,31 @@ Issues check_object_stability(const PrintObject *po, const Params ¶ms) { } // ex_entity } // region - auto [layer_islands, layer_grid] = reckon_islands(layer, true, 0, prev_layer_grid, layer_lines, params); - std::remove_if(layer_lines.begin(), layer_lines.end(), [](const ExtrusionLine &line) { - return !line.is_external_perimeter(); - }); + auto [layer_islands, layer_grid, line_to_island_mapping] = reckon_islands(layer, true, 0, prev_layer_grid, + layer_lines, params); + islands_graph.push_back(std::move(layer_islands)); +#ifdef DEBUG_FILES + for (size_t x = 0; x < size_t(layer_grid.get_pixel_count().x()); ++x) { + for (size_t y = 0; y < size_t(layer_grid.get_pixel_count().y()); ++y) { + Vec2i coords = Vec2i(x, y); + size_t island_idx = layer_grid.get_pixel(coords); + if (layer_grid.get_pixel(coords) != NULL_ISLAND) { + Vec2f pos = layer_grid.get_pixel_center(coords); + size_t pseudornd = ((island_idx + 127) * 33331 + 6907) % 23; + Vec3f color = value_to_rgbf(0.0f, float(23), float(pseudornd)); + fprintf(segmentation_f, "v %f %f %f %f %f %f\n", pos[0], + pos[1], layer->print_z, color[0], color[1], color[2]); + } + } + } + for (const auto &line : layer_lines) { + if (line.malformation > 0.0f) { + Vec3f color = value_to_rgbf(0, 1.0f, line.malformation); + fprintf(malform_f, "v %f %f %f %f %f %f\n", line.b[0], + line.b[1], layer->print_z, color[0], color[1], color[2]); + } + } +#endif LinesDistancer external_lines(layer_lines); layer_lines.clear(); prev_layer_grid = layer_grid; @@ -564,15 +732,41 @@ Issues check_object_stability(const PrintObject *po, const Params ¶ms) { } // ex_entity } // region - auto [layer_islands, layer_grid] = reckon_islands(layer, true, 0, prev_layer_grid, layer_lines, params); - std::remove_if(layer_lines.begin(), layer_lines.end(), [](const ExtrusionLine &line) { - return !line.is_external_perimeter(); - }); + auto [layer_islands, layer_grid, line_to_island_mapping] = reckon_islands(layer, true, 0, prev_layer_grid, + layer_lines, params); + islands_graph.push_back(std::move(layer_islands)); +#ifdef DEBUG_FILES + for (size_t x = 0; x < size_t(layer_grid.get_pixel_count().x()); ++x) { + for (size_t y = 0; y < size_t(layer_grid.get_pixel_count().y()); ++y) { + Vec2i coords = Vec2i(x, y); + size_t island_idx = layer_grid.get_pixel(coords); + if (layer_grid.get_pixel(coords) != NULL_ISLAND) { + Vec2f pos = layer_grid.get_pixel_center(coords); + size_t pseudornd = ((island_idx + 127) * 33331 + 6907) % 23; + Vec3f color = value_to_rgbf(0.0f, float(23), float(pseudornd)); + fprintf(segmentation_f, "v %f %f %f %f %f %f\n", pos[0], + pos[1], layer->print_z, color[0], color[1], color[2]); + } + } + } + for (const auto &line : layer_lines) { + if (line.malformation > 0.0f) { + Vec3f color = value_to_rgbf(0, 1.0f, line.malformation); + fprintf(malform_f, "v %f %f %f %f %f %f\n", line.b[0], + line.b[1], layer->print_z, color[0], color[1], color[2]); + } + } +#endif external_lines = LinesDistancer(layer_lines); layer_lines.clear(); prev_layer_grid = layer_grid; } +#ifdef DEBUG_FILES + fclose(segmentation_f); + fclose(malform_f); +#endif + return issues; } diff --git a/src/libslic3r/TriangleSelectorWrapper.cpp b/src/libslic3r/TriangleSelectorWrapper.cpp index c6040c721c..ec22ed5dd1 100644 --- a/src/libslic3r/TriangleSelectorWrapper.cpp +++ b/src/libslic3r/TriangleSelectorWrapper.cpp @@ -21,7 +21,7 @@ void TriangleSelectorWrapper::enforce_spot(const Vec3f &point, const Vec3f &orig const igl::Hit &hit = hits[hit_idx]; Vec3f pos = origin + dir * hit.t; Vec3f face_normal = its_face_normal(mesh.its, hit.id); - if (point.z() + radius > pos.z() && face_normal.dot(dir) < 0) { + if ((point - pos).norm() < radius && face_normal.dot(dir) < 0) { std::unique_ptr cursor = std::make_unique( pos, origin, radius, Transform3d::Identity(), TriangleSelector::ClippingPlane { }); selector.select_patch(hit.id, std::move(cursor), EnforcerBlockerType::ENFORCER, Transform3d::Identity(), @@ -32,11 +32,15 @@ void TriangleSelectorWrapper::enforce_spot(const Vec3f &point, const Vec3f &orig } else { size_t hit_idx_out; Vec3f hit_point_out; - AABBTreeIndirect::squared_distance_to_indexed_triangle_set(mesh.its.vertices, mesh.its.indices, triangles_tree, point, hit_idx_out, hit_point_out); - std::unique_ptr cursor = std::make_unique( - point, origin, radius, Transform3d::Identity(), TriangleSelector::ClippingPlane { }); - selector.select_patch(hit_idx_out, std::move(cursor), EnforcerBlockerType::ENFORCER, Transform3d::Identity(), - true, 0.0f); + float dist = AABBTreeIndirect::squared_distance_to_indexed_triangle_set(mesh.its.vertices, mesh.its.indices, + triangles_tree, point, hit_idx_out, hit_point_out); + if (dist < radius) { + std::unique_ptr cursor = std::make_unique( + point, origin, radius, Transform3d::Identity(), TriangleSelector::ClippingPlane { }); + selector.select_patch(hit_idx_out, std::move(cursor), EnforcerBlockerType::ENFORCER, + Transform3d::Identity(), + true, 0.0f); + } } } From f311ccbc4c90509762ecd178f80318a04cd224d8 Mon Sep 17 00:00:00 2001 From: Pavel Mikus Date: Sun, 17 Jul 2022 19:45:39 +0200 Subject: [PATCH 044/100] basic implementation should be complete, bugs not fixed, last iteration copied --- .../SupportSpotsGeneratorRefactoring.cpp | 144 +++++++++++++++--- 1 file changed, 126 insertions(+), 18 deletions(-) diff --git a/src/libslic3r/SupportSpotsGeneratorRefactoring.cpp b/src/libslic3r/SupportSpotsGeneratorRefactoring.cpp index 23b249219a..9b44594268 100644 --- a/src/libslic3r/SupportSpotsGeneratorRefactoring.cpp +++ b/src/libslic3r/SupportSpotsGeneratorRefactoring.cpp @@ -15,7 +15,7 @@ #include "libslic3r/ClipperUtils.hpp" #include "Geometry/ConvexHull.hpp" -#define DEBUG_FILES +//#define DEBUG_FILES #ifdef DEBUG_FILES #include @@ -508,8 +508,10 @@ void check_global_stability( Issues& issues, const Params& params ) { + // vector of islands, where each contains vector of line indices (to layer_lines vector) + // basically reverse of line_to_island_mapping std::vector> islands_lines(islands_graph.back().islands.size()); - for (int lidx = 0; lidx < layer_lines.size(); ++lidx) { + for (size_t lidx = 0; lidx < layer_lines.size(); ++lidx) { if (layer_lines[lidx].origin_entity->role() == erExternalPerimeter) { islands_lines[line_to_island_mapping[lidx]].push_back(lidx); } @@ -517,46 +519,56 @@ void check_global_stability( using Accumulator = Island; + // islands_graph.back() refers to the top most (currently) layer for (size_t island_idx = 0; island_idx < islands_graph.back().islands.size(); ++island_idx) { Island& island = islands_graph.back().islands[island_idx]; - std::vector island_external_lines; + std::vector island_external_lines; //TODO currently not external but all for (size_t lidx : islands_lines[island_idx]) { island_external_lines.push_back(layer_lines[lidx]); } LinesDistancer island_lines_dist(island_external_lines); - - Accumulator acc = island; + Accumulator acc = island; // in acc, we accumulate the mass and other properties of the object part as we traverse the islands down to bed + // There is one object part for each island at the top most layer, and each one is computed individually - + // Some of the calculations will be done mutliple times int layer_idx = islands_graph.size() -1; + // traverse the islands graph down, and for each connection area, calculate if it holds or breaks while (acc.islands_under_with_connection_area.size() > 0) { - //TEST for break between layer_idx and layer_idx -1; - LayerIslands below = islands_graph[layer_idx-1]; + //test for break between layer_idx and layer_idx -1; + LayerIslands below = islands_graph[layer_idx-1]; // must exist, see while condition + layer_idx--; + // initialize variables that we will accumulate over all islands, which are connected to the current object part std::vector pivot_points; Vec2f sticking_centroid; float connection_area = 0; for (const auto& pair : acc.islands_under_with_connection_area) { const Island& below_i = below.islands[pair.first]; - Vec2f centroid = (below_i.volume_centroid_accumulator / below_i.volume).head<2>(); - pivot_points.push_back(centroid); - sticking_centroid += centroid * pair.second; + Vec2f centroid = (below_i.volume_centroid_accumulator / below_i.volume).head<2>(); // centroid of the island 'below_i'; TODO it should be centroid of the connection area + pivot_points.push_back(centroid); // for object parts, we also consider breaking pivots in the centroids of the islands + sticking_centroid += centroid * pair.second; // pair.second is connection area in mm^2 connection_area += pair.second; } + sticking_centroid /= connection_area; //normalize to get final sticking centroid + for (const Vec3f& p_point: acc.pivot_points){ + pivot_points.push_back(p_point.head<2>()); + } + // Now we have accumulated pivot points, connection area and sticking centroid of the whole layer to the current object part - sticking_centroid /= connection_area; - + // create KD tree over current pivot points auto coord_fn = [&pivot_points](size_t idx, size_t dim) { return pivot_points[idx][dim]; }; - KDTreeIndirect<2, float, decltype(coord_fn)> supports_tree(coord_fn, pivot_points.size()); + KDTreeIndirect<2, float, decltype(coord_fn)> pivot_points_tree(coord_fn, pivot_points.size()); + // iterate over extrusions at top layer island, check each for stability for (const ExtrusionLine& line : island_external_lines){ Vec2f line_dir = (line.b - line.a).normalized(); Vec2f pivot_site_search_point = line.b + line_dir * 300.0f; - size_t pivot_idx = find_closest_point(supports_tree, pivot_site_search_point); + size_t pivot_idx = find_closest_point(pivot_points_tree, pivot_site_search_point); const Vec2f &pivot = pivot_points[pivot_idx]; float sticking_arm = (pivot - sticking_centroid).norm(); - float sticking_torque = sticking_arm * connection_area * params.tensile_strength; + float sticking_torque = sticking_arm * connection_area * params.tensile_strength; // For breakage in between layers, we compute with tensile strength, not bed adhesion float mass = acc.volume * params.filament_density; const Vec3f &mass_centorid = acc.volume_centroid_accumulator / acc.volume; @@ -618,10 +630,103 @@ void check_global_stability( #endif } - //TODO add stuf to accumulator - - + std::unordered_map tmp = acc.islands_under_with_connection_area; + acc.islands_under_with_connection_area.clear(); + // finally, add gathered islands to accumulator, and continue down to next layer + for (const auto& pair : tmp) { + const Island& below_i = below.islands[pair.first]; + for (const auto& below_islands : below_i.islands_under_with_connection_area) { + acc.islands_under_with_connection_area[below_islands.first] += below_islands.second; + } + for (const Vec3f& pivot_p : below_i.pivot_points) { + acc.pivot_points.push_back(pivot_p); + } + acc.sticking_centroid_accumulator += below_i.sticking_centroid_accumulator; + acc.sticking_force += below_i.sticking_force; + acc.volume += below_i.volume; + acc.volume_centroid_accumulator += below_i.volume_centroid_accumulator; + } } + + // We have arrived to the bed level, now check for stability of the object part on the bed + std::vector pivot_points; + for (const Vec3f& p_point: acc.pivot_points){ + pivot_points.push_back(p_point.head<2>()); + } + auto coord_fn = [&pivot_points](size_t idx, size_t dim) { + return pivot_points[idx][dim]; + }; + KDTreeIndirect<2, float, decltype(coord_fn)> pivot_points_tree(coord_fn, pivot_points.size()); + + for (const ExtrusionLine &line : island_external_lines) { + Vec2f line_dir = (line.b - line.a).normalized(); + Vec2f pivot_site_search_point = line.b + line_dir * 300.0f; + size_t pivot_idx = find_closest_point(pivot_points_tree, pivot_site_search_point); + const Vec2f &pivot = pivot_points[pivot_idx]; + + const Vec2f &sticking_centroid = acc.sticking_centroid_accumulator.head<2>() / acc.sticking_force; + float sticking_arm = (pivot - sticking_centroid).norm(); + float sticking_torque = sticking_arm * acc.sticking_force; + + float mass = acc.volume * params.filament_density; + const Vec3f &mass_centorid = acc.volume_centroid_accumulator / acc.volume; + float weight = mass * params.gravity_constant; + float weight_arm = (pivot - mass_centorid.head<2>()).norm(); + float weight_torque = weight_arm * weight; + + float bed_movement_arm = mass_centorid.z(); + float bed_movement_force = params.max_acceleration * mass; + float bed_movement_torque = bed_movement_force * bed_movement_arm; + + Vec3f extruder_pressure_direction = to_3d(line_dir, 0.0f); + extruder_pressure_direction.z() = -0.2 - line.malformation * 0.5; + extruder_pressure_direction.normalize(); + float conflict_torque_arm = (to_3d(Vec2f(pivot - line.b), print_z).cross( + extruder_pressure_direction)).norm(); + float extruder_conflict_force = params.tolerable_extruder_conflict_force + + std::min(line.malformation, 1.0f) * params.malformations_additive_conflict_extruder_force; + float extruder_conflict_torque = extruder_conflict_force * conflict_torque_arm; + + float total_torque = bed_movement_torque + extruder_conflict_torque - weight_torque - sticking_torque; + + if (total_torque > 0) { + Vec2f target_point; + size_t _idx; + island_lines_dist.signed_distance_from_lines(pivot_site_search_point, _idx, target_point); + // if (!supports_presence_grid.position_taken(to_3d(target_point, print_z))) { + float area = params.support_points_interface_radius * params.support_points_interface_radius + * float(PI); + float sticking_force = area * params.support_adhesion; + Vec3f support_point = to_3d(target_point, print_z); + island.pivot_points.push_back(support_point); + island.sticking_force += sticking_force; + island.sticking_centroid_accumulator += sticking_force*support_point; + issues.support_points.emplace_back(support_point, + extruder_conflict_torque - sticking_torque, extruder_pressure_direction); + // supports_presence_grid.take_position(to_3d(target_point, print_z)); + // } + } + #if 0 + BOOST_LOG_TRIVIAL(debug) + << "SSG: sticking_arm: " << sticking_arm; + BOOST_LOG_TRIVIAL(debug) + << "SSG: sticking_torque: " << sticking_torque; + BOOST_LOG_TRIVIAL(debug) + << "SSG: weight_arm: " << sticking_arm; + BOOST_LOG_TRIVIAL(debug) + << "SSG: weight_torque: " << weight_torque; + BOOST_LOG_TRIVIAL(debug) + << "SSG: bed_movement_arm: " << bed_movement_arm; + BOOST_LOG_TRIVIAL(debug) + << "SSG: bed_movement_torque: " << bed_movement_torque; + BOOST_LOG_TRIVIAL(debug) + << "SSG: conflict_torque_arm: " << conflict_torque_arm; + BOOST_LOG_TRIVIAL(debug) + << "SSG: extruder_conflict_torque: " << extruder_conflict_torque; + BOOST_LOG_TRIVIAL(debug) + << "SSG: total_torque: " << total_torque << " printz: " << print_z; + #endif + } } } @@ -735,6 +840,9 @@ Issues check_object_stability(const PrintObject *po, const Params ¶ms) { auto [layer_islands, layer_grid, line_to_island_mapping] = reckon_islands(layer, true, 0, prev_layer_grid, layer_lines, params); islands_graph.push_back(std::move(layer_islands)); + + check_global_stability(layer->print_z, islands_graph, layer_lines, line_to_island_mapping, issues, params); + #ifdef DEBUG_FILES for (size_t x = 0; x < size_t(layer_grid.get_pixel_count().x()); ++x) { for (size_t y = 0; y < size_t(layer_grid.get_pixel_count().y()); ++y) { From 1e4b56cc852eb42033b6b619ca9ea9b266c6702a Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Mon, 18 Jul 2022 16:46:10 +0200 Subject: [PATCH 045/100] fix crashing when extrusion is not assigned island Add voxel filter grid for supports padding --- .../SupportSpotsGeneratorRefactoring.cpp | 298 ++++++++++++------ 1 file changed, 196 insertions(+), 102 deletions(-) diff --git a/src/libslic3r/SupportSpotsGeneratorRefactoring.cpp b/src/libslic3r/SupportSpotsGeneratorRefactoring.cpp index 9b44594268..23d5698567 100644 --- a/src/libslic3r/SupportSpotsGeneratorRefactoring.cpp +++ b/src/libslic3r/SupportSpotsGeneratorRefactoring.cpp @@ -15,7 +15,7 @@ #include "libslic3r/ClipperUtils.hpp" #include "Geometry/ConvexHull.hpp" -//#define DEBUG_FILES +#define DEBUG_FILES #ifdef DEBUG_FILES #include @@ -193,6 +193,61 @@ private: } }; +struct SupportGridFilter { +private: + Vec3f cell_size; + Vec3f origin; + Vec3f size; + Vec3i cell_count; + + std::unordered_set taken_cells { }; + +public: + SupportGridFilter(const PrintObject *po, float voxel_size) { + cell_size = Vec3f(voxel_size, voxel_size, voxel_size); + + Vec2crd size_half = po->size().head<2>().cwiseQuotient(Vec2crd(2, 2)) + Vec2crd::Ones(); + Vec3f min = unscale(Vec3crd(-size_half.x(), -size_half.y(), 0)).cast() - cell_size; + Vec3f max = unscale(Vec3crd(size_half.x(), size_half.y(), po->height())).cast() + cell_size; + + origin = min; + size = max - min; + cell_count = size.cwiseQuotient(cell_size).cast() + Vec3i::Ones(); + } + + Vec3i to_cell_coords(const Vec3f &position) const { + Vec3i cell_coords = (position - this->origin).cwiseQuotient(this->cell_size).cast(); + return cell_coords; + } + + size_t to_cell_index(const Vec3i &cell_coords) const { + assert(cell_coords.x() >= 0); + assert(cell_coords.x() < cell_count.x()); + assert(cell_coords.y() >= 0); + assert(cell_coords.y() < cell_count.y()); + assert(cell_coords.z() >= 0); + assert(cell_coords.z() < cell_count.z()); + + return cell_coords.z() * cell_count.x() * cell_count.y() + + cell_coords.y() * cell_count.x() + + cell_coords.x(); + } + + Vec3f get_cell_center(const Vec3i &cell_coords) const { + return origin + cell_coords.cast().cwiseProduct(this->cell_size) + + this->cell_size.cwiseQuotient(Vec3f(2.0f, 2.0f, 2.0)); + } + + void take_position(const Vec3f &position) { + taken_cells.insert(to_cell_index(to_cell_coords(position))); + } + + bool position_taken(const Vec3f &position) const { + return taken_cells.find(to_cell_index(to_cell_coords(position))) != taken_cells.end(); + } + +}; + struct Island { std::unordered_map islands_under_with_connection_area; std::vector pivot_points; @@ -351,9 +406,11 @@ std::tuple> reckon_islands( BOOST_LOG_TRIVIAL(debug) << "SSG: reckon islands on printz: " << layer->print_z; + //extract extrusions (connected paths from multiple lines) from the layer_lines. belonging to single polyline is determined by origin_entity ptr. + // result is a vector of [start, end) index pairs into the layer_lines vector std::vector> extrusions; //start and end idx (one beyond last extrusion) [start,end) const ExtrusionEntity *current_ex = nullptr; - for (size_t lidx = 1; lidx < layer_lines.size(); ++lidx) { + for (size_t lidx = 0; lidx < layer_lines.size(); ++lidx) { const ExtrusionLine &line = layer_lines[lidx]; if (line.origin_entity == current_ex) { extrusions.back().second = lidx + 1; @@ -374,8 +431,10 @@ std::tuple> reckon_islands( << "SSG: " << ext.second - ext.first; } - std::vector islands; - std::vector> island_extrusions; + std::vector islands; // these search trees will be used to determine to which island does the extrusion begin + std::vector> island_extrusions; //final assigment of each extrusion to an island + // initliaze the search from external perimeters - at the beginning, there is island candidate for each external perimeter. + // some of them will disappear (e.g. holes) for (size_t e = 0; e < extrusions.size(); ++e) { if (layer_lines[extrusions[e].first].is_external_perimeter()) { std::vector copy(extrusions[e].second - extrusions[e].first); @@ -386,22 +445,40 @@ std::tuple> reckon_islands( island_extrusions.push_back( { e }); } } + // backup code if islands not found - this can currently happen, as external perimeters may be also pure overhang perimeters, and there is no + // way to distinguish external extrusions with total certainty. + // If that happens, just make the first extrusion into island - it may be wrong, but it won't crash. + if (islands.empty() && !extrusions.empty()) { + std::vector copy(extrusions[0].second - extrusions[0].first); + for (size_t ex_line_idx = extrusions[0].first; ex_line_idx < extrusions[0].second; ++ex_line_idx) { + copy[ex_line_idx - extrusions[0].first] = layer_lines[ex_line_idx]; + } + islands.emplace_back(copy); + island_extrusions.push_back( { 0 }); + } BOOST_LOG_TRIVIAL(debug) << "SSG: external perims: " << islands.size(); - - for (size_t i = 0; i < islands.size(); ++i) { - for (size_t e = 0; e < extrusions.size(); ++e) { - if (!layer_lines[extrusions[e].first].is_external_perimeter()) { + // assign non external extrusions to islands + for (size_t e = 0; e < extrusions.size(); ++e) { + if (!layer_lines[extrusions[e].first].is_external_perimeter()) { + bool island_assigned = false; + for (size_t i = 0; i < islands.size(); ++i) { size_t _idx; Vec2f _pt; if (islands[i].signed_distance_from_lines(layer_lines[extrusions[e].first].a, _idx, _pt) < 0) { island_extrusions[i].push_back(e); + island_assigned = true; + break; } } + if (!island_assigned) { // If extrusion is not assigned for some reason, push it into the first island. As with the previous backup code, + // it may be wrong, but it won't crash + island_extrusions[0].push_back(e); + } } } - + // merge islands which are embedded within each other (mainly holes) for (size_t i = 0; i < islands.size(); ++i) { if (islands[i].get_lines().empty()) { continue; @@ -424,7 +501,7 @@ std::tuple> reckon_islands( << "SSG: filter islands"; float flow_width = get_flow_width(layer->regions()[0], erExternalPerimeter); - + // after filtering the layer lines into islands, build the result LayerIslands structure. LayerIslands result { }; std::vector line_to_island_mapping(layer_lines.size(), NULL_ISLAND); for (const std::vector &island_ex : island_extrusions) { @@ -437,9 +514,10 @@ std::tuple> reckon_islands( for (size_t lidx = extrusions[extrusion_idx].first; lidx < extrusions[extrusion_idx].second; ++lidx) { line_to_island_mapping[lidx] = result.islands.size(); const ExtrusionLine &line = layer_lines[lidx]; - float volume = line.len * flow_width * layer->height * 0.7; // 1/sqrt(2) compensation for cylindrical shape + float volume = line.origin_entity->min_mm3_per_mm() * line.len; island.volume += volume; - island.volume_centroid_accumulator += to_3d(Vec2f((line.a + line.b) / 2.0f), float(layer->print_z)) * volume; + island.volume_centroid_accumulator += to_3d(Vec2f((line.a + line.b) / 2.0f), float(layer->print_z)) + * volume; if (first_layer) { float sticking_force = line.len * flow_width * params.base_adhesion; @@ -455,7 +533,8 @@ std::tuple> reckon_islands( * float(PI); float sticking_force = support_interface_area * params.support_adhesion; island.sticking_force += sticking_force; - island.sticking_centroid_accumulator += sticking_force * to_3d(Vec2f(line.b), float(layer->print_z)); + island.sticking_centroid_accumulator += sticking_force + * to_3d(Vec2f(line.b), float(layer->print_z)); island.pivot_points.push_back(to_3d(Vec2f(line.b), float(layer->print_z))); } } @@ -466,9 +545,10 @@ std::tuple> reckon_islands( BOOST_LOG_TRIVIAL(debug) << "SSG: There are " << result.islands.size() << " islands on printz: " << layer->print_z; + //LayerIslands structure built. Now determine connections and their areas to the previous layer using raterization. PixelGrid current_layer_grid = prev_layer_grid; current_layer_grid.clear(); - + // build index image of current layer tbb::parallel_for(tbb::blocked_range(0, layer_lines.size()), [&layer_lines, ¤t_layer_grid, &line_to_island_mapping]( tbb::blocked_range r) { @@ -482,6 +562,7 @@ std::tuple> reckon_islands( BOOST_LOG_TRIVIAL(debug) << "SSG: rasterized"; + //compare the image of previous layer with the current layer. For each pair of overlapping valid pixels, add pixel area to the respecitve island connection for (size_t x = 0; x < size_t(current_layer_grid.get_pixel_count().x()); ++x) { for (size_t y = 0; y < size_t(current_layer_grid.get_pixel_count().y()); ++y) { Vec2i coords = Vec2i(x, y); @@ -502,14 +583,20 @@ std::tuple> reckon_islands( void check_global_stability( float print_z, - std::vector& islands_graph, + std::vector &islands_graph, + SupportGridFilter &supports_presence_grid, const std::vector &layer_lines, const std::vector &line_to_island_mapping, - Issues& issues, - const Params& params + Issues &issues, + const Params ¶ms ) { - // vector of islands, where each contains vector of line indices (to layer_lines vector) - // basically reverse of line_to_island_mapping + + std::cout << "there are " << islands_graph.back().islands.size() << " islands, " << layer_lines.size() << " lines" << std::endl; + for (int i = 0; i < line_to_island_mapping.size(); ++i) { + std::cout << "line " << i << " belongs to island " << line_to_island_mapping[i] << std::endl; + } + // vector of islands, where each contains vector of line indices (to layer_lines vector) + // basically reverse of line_to_island_mapping std::vector> islands_lines(islands_graph.back().islands.size()); for (size_t lidx = 0; lidx < layer_lines.size(); ++lidx) { if (layer_lines[lidx].origin_entity->role() == erExternalPerimeter) { @@ -519,38 +606,41 @@ void check_global_stability( using Accumulator = Island; - // islands_graph.back() refers to the top most (currently) layer + // islands_graph.back() refers to the top most (current) layer for (size_t island_idx = 0; island_idx < islands_graph.back().islands.size(); ++island_idx) { - Island& island = islands_graph.back().islands[island_idx]; + Island &island = islands_graph.back().islands[island_idx]; - std::vector island_external_lines; //TODO currently not external but all + std::cout << "TOP LEVEL ITERATION FOR ISLAND: " << island_idx << std::endl; + + std::vector island_external_lines; for (size_t lidx : islands_lines[island_idx]) { island_external_lines.push_back(layer_lines[lidx]); } LinesDistancer island_lines_dist(island_external_lines); Accumulator acc = island; // in acc, we accumulate the mass and other properties of the object part as we traverse the islands down to bed // There is one object part for each island at the top most layer, and each one is computed individually - - // Some of the calculations will be done mutliple times - int layer_idx = islands_graph.size() -1; + // Some of the calculations will be done multiple times + int layer_idx = islands_graph.size() - 1; // traverse the islands graph down, and for each connection area, calculate if it holds or breaks while (acc.islands_under_with_connection_area.size() > 0) { + std::cout << "PARTIAL ITERATION FOR LAYER: " << layer_idx << std::endl; //test for break between layer_idx and layer_idx -1; - LayerIslands below = islands_graph[layer_idx-1]; // must exist, see while condition + LayerIslands below = islands_graph[layer_idx - 1]; // must exist, see while condition layer_idx--; // initialize variables that we will accumulate over all islands, which are connected to the current object part std::vector pivot_points; Vec2f sticking_centroid; float connection_area = 0; - for (const auto& pair : acc.islands_under_with_connection_area) { - const Island& below_i = below.islands[pair.first]; + for (const auto &pair : acc.islands_under_with_connection_area) { + const Island &below_i = below.islands[pair.first]; Vec2f centroid = (below_i.volume_centroid_accumulator / below_i.volume).head<2>(); // centroid of the island 'below_i'; TODO it should be centroid of the connection area pivot_points.push_back(centroid); // for object parts, we also consider breaking pivots in the centroids of the islands sticking_centroid += centroid * pair.second; // pair.second is connection area in mm^2 connection_area += pair.second; } sticking_centroid /= connection_area; //normalize to get final sticking centroid - for (const Vec3f& p_point: acc.pivot_points){ - pivot_points.push_back(p_point.head<2>()); + for (const Vec3f &p_point : acc.pivot_points) { + pivot_points.push_back(p_point.head<2>()); } // Now we have accumulated pivot points, connection area and sticking centroid of the whole layer to the current object part @@ -561,7 +651,7 @@ void check_global_stability( KDTreeIndirect<2, float, decltype(coord_fn)> pivot_points_tree(coord_fn, pivot_points.size()); // iterate over extrusions at top layer island, check each for stability - for (const ExtrusionLine& line : island_external_lines){ + for (const ExtrusionLine &line : island_external_lines) { Vec2f line_dir = (line.b - line.a).normalized(); Vec2f pivot_site_search_point = line.b + line_dir * 300.0f; size_t pivot_idx = find_closest_point(pivot_points_tree, pivot_site_search_point); @@ -592,23 +682,23 @@ void check_global_stability( float total_torque = bed_movement_torque + extruder_conflict_torque - weight_torque - sticking_torque; if (total_torque > 0) { - Vec2f target_point; - size_t _idx; + Vec2f target_point { }; + size_t _idx { }; island_lines_dist.signed_distance_from_lines(pivot_site_search_point, _idx, target_point); -// if (!supports_presence_grid.position_taken(to_3d(target_point, print_z))) { + if (!supports_presence_grid.position_taken(to_3d(target_point, print_z))) { float area = params.support_points_interface_radius * params.support_points_interface_radius * float(PI); float sticking_force = area * params.support_adhesion; Vec3f support_point = to_3d(target_point, print_z); - island.pivot_points.push_back(support_point); - island.sticking_force += sticking_force; - island.sticking_centroid_accumulator += sticking_force*support_point; + island.pivot_points.push_back(support_point); + island.sticking_force += sticking_force; + island.sticking_centroid_accumulator += sticking_force * support_point; issues.support_points.emplace_back(support_point, extruder_conflict_torque - sticking_torque, extruder_pressure_direction); -// supports_presence_grid.take_position(to_3d(target_point, print_z)); -// } + supports_presence_grid.take_position(to_3d(target_point, print_z)); + } } - #if 0 +#if 0 BOOST_LOG_TRIVIAL(debug) << "SSG: sticking_arm: " << sticking_arm; BOOST_LOG_TRIVIAL(debug) @@ -633,80 +723,81 @@ void check_global_stability( std::unordered_map tmp = acc.islands_under_with_connection_area; acc.islands_under_with_connection_area.clear(); // finally, add gathered islands to accumulator, and continue down to next layer - for (const auto& pair : tmp) { - const Island& below_i = below.islands[pair.first]; - for (const auto& below_islands : below_i.islands_under_with_connection_area) { - acc.islands_under_with_connection_area[below_islands.first] += below_islands.second; - } - for (const Vec3f& pivot_p : below_i.pivot_points) { - acc.pivot_points.push_back(pivot_p); - } - acc.sticking_centroid_accumulator += below_i.sticking_centroid_accumulator; - acc.sticking_force += below_i.sticking_force; - acc.volume += below_i.volume; - acc.volume_centroid_accumulator += below_i.volume_centroid_accumulator; - } + for (const auto &pair : tmp) { + const Island &below_i = below.islands[pair.first]; + for (const auto &below_islands : below_i.islands_under_with_connection_area) { + acc.islands_under_with_connection_area[below_islands.first] += below_islands.second; + } + for (const Vec3f &pivot_p : below_i.pivot_points) { + acc.pivot_points.push_back(pivot_p); + } + acc.sticking_centroid_accumulator += below_i.sticking_centroid_accumulator; + acc.sticking_force += below_i.sticking_force; + acc.volume += below_i.volume; + acc.volume_centroid_accumulator += below_i.volume_centroid_accumulator; + } } + std::cout << "FINAL ITERATION FOR THE BED LEVEL: " << acc.volume << std::endl; // We have arrived to the bed level, now check for stability of the object part on the bed std::vector pivot_points; - for (const Vec3f& p_point: acc.pivot_points){ - pivot_points.push_back(p_point.head<2>()); + for (const Vec3f &p_point : acc.pivot_points) { + pivot_points.push_back(p_point.head<2>()); } auto coord_fn = [&pivot_points](size_t idx, size_t dim) { - return pivot_points[idx][dim]; - }; - KDTreeIndirect<2, float, decltype(coord_fn)> pivot_points_tree(coord_fn, pivot_points.size()); + return pivot_points[idx][dim]; + }; + KDTreeIndirect<2, float, decltype(coord_fn)> pivot_points_tree(coord_fn, pivot_points.size()); - for (const ExtrusionLine &line : island_external_lines) { - Vec2f line_dir = (line.b - line.a).normalized(); - Vec2f pivot_site_search_point = line.b + line_dir * 300.0f; - size_t pivot_idx = find_closest_point(pivot_points_tree, pivot_site_search_point); - const Vec2f &pivot = pivot_points[pivot_idx]; + for (const ExtrusionLine &line : island_external_lines) { + Vec2f line_dir = (line.b - line.a).normalized(); + Vec2f pivot_site_search_point = line.b + line_dir * 300.0f; + size_t pivot_idx = find_closest_point(pivot_points_tree, pivot_site_search_point); + const Vec2f &pivot = pivot_points[pivot_idx]; - const Vec2f &sticking_centroid = acc.sticking_centroid_accumulator.head<2>() / acc.sticking_force; - float sticking_arm = (pivot - sticking_centroid).norm(); - float sticking_torque = sticking_arm * acc.sticking_force; + const Vec2f &sticking_centroid = acc.sticking_centroid_accumulator.head<2>() / acc.sticking_force; + float sticking_arm = (pivot - sticking_centroid).norm(); + float sticking_torque = sticking_arm * acc.sticking_force; - float mass = acc.volume * params.filament_density; - const Vec3f &mass_centorid = acc.volume_centroid_accumulator / acc.volume; - float weight = mass * params.gravity_constant; - float weight_arm = (pivot - mass_centorid.head<2>()).norm(); - float weight_torque = weight_arm * weight; + float mass = acc.volume * params.filament_density; + const Vec3f &mass_centorid = acc.volume_centroid_accumulator / acc.volume; + float weight = mass * params.gravity_constant; + float weight_arm = (pivot - mass_centorid.head<2>()).norm(); + float weight_torque = weight_arm * weight; - float bed_movement_arm = mass_centorid.z(); - float bed_movement_force = params.max_acceleration * mass; - float bed_movement_torque = bed_movement_force * bed_movement_arm; + float bed_movement_arm = mass_centorid.z(); + float bed_movement_force = params.max_acceleration * mass; + float bed_movement_torque = bed_movement_force * bed_movement_arm; - Vec3f extruder_pressure_direction = to_3d(line_dir, 0.0f); - extruder_pressure_direction.z() = -0.2 - line.malformation * 0.5; - extruder_pressure_direction.normalize(); - float conflict_torque_arm = (to_3d(Vec2f(pivot - line.b), print_z).cross( - extruder_pressure_direction)).norm(); - float extruder_conflict_force = params.tolerable_extruder_conflict_force + - std::min(line.malformation, 1.0f) * params.malformations_additive_conflict_extruder_force; - float extruder_conflict_torque = extruder_conflict_force * conflict_torque_arm; + Vec3f extruder_pressure_direction = to_3d(line_dir, 0.0f); + extruder_pressure_direction.z() = -0.2 - line.malformation * 0.5; + extruder_pressure_direction.normalize(); + float conflict_torque_arm = (to_3d(Vec2f(pivot - line.b), print_z).cross( + extruder_pressure_direction)).norm(); + float extruder_conflict_force = params.tolerable_extruder_conflict_force + + std::min(line.malformation, 1.0f) * params.malformations_additive_conflict_extruder_force; + float extruder_conflict_torque = extruder_conflict_force * conflict_torque_arm; - float total_torque = bed_movement_torque + extruder_conflict_torque - weight_torque - sticking_torque; + float total_torque = bed_movement_torque + extruder_conflict_torque - weight_torque - sticking_torque; - if (total_torque > 0) { - Vec2f target_point; - size_t _idx; - island_lines_dist.signed_distance_from_lines(pivot_site_search_point, _idx, target_point); - // if (!supports_presence_grid.position_taken(to_3d(target_point, print_z))) { - float area = params.support_points_interface_radius * params.support_points_interface_radius - * float(PI); - float sticking_force = area * params.support_adhesion; - Vec3f support_point = to_3d(target_point, print_z); - island.pivot_points.push_back(support_point); - island.sticking_force += sticking_force; - island.sticking_centroid_accumulator += sticking_force*support_point; - issues.support_points.emplace_back(support_point, - extruder_conflict_torque - sticking_torque, extruder_pressure_direction); - // supports_presence_grid.take_position(to_3d(target_point, print_z)); - // } - } - #if 0 + if (total_torque > 0) { + Vec2f target_point; + size_t _idx; + island_lines_dist.signed_distance_from_lines(pivot_site_search_point, _idx, target_point); + if (!supports_presence_grid.position_taken(to_3d(target_point, print_z))) { + float area = params.support_points_interface_radius * params.support_points_interface_radius + * float(PI); + float sticking_force = area * params.support_adhesion; + Vec3f support_point = to_3d(target_point, print_z); + island.pivot_points.push_back(support_point); + island.sticking_force += sticking_force; + island.sticking_centroid_accumulator += sticking_force * support_point; + issues.support_points.emplace_back(support_point, + extruder_conflict_torque - sticking_torque, extruder_pressure_direction); + supports_presence_grid.take_position(to_3d(target_point, print_z)); + } + } +#if 0 BOOST_LOG_TRIVIAL(debug) << "SSG: sticking_arm: " << sticking_arm; BOOST_LOG_TRIVIAL(debug) @@ -726,7 +817,7 @@ void check_global_stability( BOOST_LOG_TRIVIAL(debug) << "SSG: total_torque: " << total_torque << " printz: " << print_z; #endif - } + } } } @@ -741,6 +832,8 @@ Issues check_object_stability(const PrintObject *po, const Params ¶ms) { std::vector layer_lines; float flow_width = get_flow_width(po->layers()[po->layer_count() - 1]->regions()[0], erExternalPerimeter); PixelGrid prev_layer_grid(po, flow_width); + SupportGridFilter supports_presence_grid { po, params.min_distance_between_support_points }; + BOOST_LOG_TRIVIAL(debug) << "SSG: flow width: " << flow_width; @@ -841,7 +934,8 @@ Issues check_object_stability(const PrintObject *po, const Params ¶ms) { layer_lines, params); islands_graph.push_back(std::move(layer_islands)); - check_global_stability(layer->print_z, islands_graph, layer_lines, line_to_island_mapping, issues, params); + check_global_stability(layer->print_z, islands_graph, supports_presence_grid, layer_lines, + line_to_island_mapping, issues, params); #ifdef DEBUG_FILES for (size_t x = 0; x < size_t(layer_grid.get_pixel_count().x()); ++x) { From 9afb350cddba918faa7dacdcc0a039dbfff1181b Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Tue, 19 Jul 2022 10:09:21 +0200 Subject: [PATCH 046/100] remove noisy debug info --- .../SupportSpotsGeneratorRefactoring.cpp | 41 ------------------- 1 file changed, 41 deletions(-) diff --git a/src/libslic3r/SupportSpotsGeneratorRefactoring.cpp b/src/libslic3r/SupportSpotsGeneratorRefactoring.cpp index 23d5698567..a2275c6372 100644 --- a/src/libslic3r/SupportSpotsGeneratorRefactoring.cpp +++ b/src/libslic3r/SupportSpotsGeneratorRefactoring.cpp @@ -403,9 +403,6 @@ std::tuple> reckon_islands( const std::vector &layer_lines, const Params ¶ms) { - BOOST_LOG_TRIVIAL(debug) - << "SSG: reckon islands on printz: " << layer->print_z; - //extract extrusions (connected paths from multiple lines) from the layer_lines. belonging to single polyline is determined by origin_entity ptr. // result is a vector of [start, end) index pairs into the layer_lines vector std::vector> extrusions; //start and end idx (one beyond last extrusion) [start,end) @@ -420,17 +417,6 @@ std::tuple> reckon_islands( } } - BOOST_LOG_TRIVIAL(debug) - << "SSG: layer_lines size: " << layer_lines.size(); - BOOST_LOG_TRIVIAL(debug) - << "SSG: extrusions count: " << extrusions.size(); - BOOST_LOG_TRIVIAL(debug) - << "SSG: extrusions sizes: "; - for (const auto &ext : extrusions) { - BOOST_LOG_TRIVIAL(debug) - << "SSG: " << ext.second - ext.first; - } - std::vector islands; // these search trees will be used to determine to which island does the extrusion begin std::vector> island_extrusions; //final assigment of each extrusion to an island // initliaze the search from external perimeters - at the beginning, there is island candidate for each external perimeter. @@ -457,8 +443,6 @@ std::tuple> reckon_islands( island_extrusions.push_back( { 0 }); } - BOOST_LOG_TRIVIAL(debug) - << "SSG: external perims: " << islands.size(); // assign non external extrusions to islands for (size_t e = 0; e < extrusions.size(); ++e) { if (!layer_lines[extrusions[e].first].is_external_perimeter()) { @@ -497,9 +481,6 @@ std::tuple> reckon_islands( } } - BOOST_LOG_TRIVIAL(debug) - << "SSG: filter islands"; - float flow_width = get_flow_width(layer->regions()[0], erExternalPerimeter); // after filtering the layer lines into islands, build the result LayerIslands structure. LayerIslands result { }; @@ -542,9 +523,6 @@ std::tuple> reckon_islands( result.islands.push_back(island); } - BOOST_LOG_TRIVIAL(debug) - << "SSG: There are " << result.islands.size() << " islands on printz: " << layer->print_z; - //LayerIslands structure built. Now determine connections and their areas to the previous layer using raterization. PixelGrid current_layer_grid = prev_layer_grid; current_layer_grid.clear(); @@ -559,9 +537,6 @@ std::tuple> reckon_islands( } }); - BOOST_LOG_TRIVIAL(debug) - << "SSG: rasterized"; - //compare the image of previous layer with the current layer. For each pair of overlapping valid pixels, add pixel area to the respecitve island connection for (size_t x = 0; x < size_t(current_layer_grid.get_pixel_count().x()); ++x) { for (size_t y = 0; y < size_t(current_layer_grid.get_pixel_count().y()); ++y) { @@ -574,10 +549,6 @@ std::tuple> reckon_islands( } } } - - BOOST_LOG_TRIVIAL(debug) - << "SSG: connection area computed"; - return {result, current_layer_grid, line_to_island_mapping}; } @@ -590,11 +561,6 @@ void check_global_stability( Issues &issues, const Params ¶ms ) { - - std::cout << "there are " << islands_graph.back().islands.size() << " islands, " << layer_lines.size() << " lines" << std::endl; - for (int i = 0; i < line_to_island_mapping.size(); ++i) { - std::cout << "line " << i << " belongs to island " << line_to_island_mapping[i] << std::endl; - } // vector of islands, where each contains vector of line indices (to layer_lines vector) // basically reverse of line_to_island_mapping std::vector> islands_lines(islands_graph.back().islands.size()); @@ -610,8 +576,6 @@ void check_global_stability( for (size_t island_idx = 0; island_idx < islands_graph.back().islands.size(); ++island_idx) { Island &island = islands_graph.back().islands[island_idx]; - std::cout << "TOP LEVEL ITERATION FOR ISLAND: " << island_idx << std::endl; - std::vector island_external_lines; for (size_t lidx : islands_lines[island_idx]) { island_external_lines.push_back(layer_lines[lidx]); @@ -623,7 +587,6 @@ void check_global_stability( int layer_idx = islands_graph.size() - 1; // traverse the islands graph down, and for each connection area, calculate if it holds or breaks while (acc.islands_under_with_connection_area.size() > 0) { - std::cout << "PARTIAL ITERATION FOR LAYER: " << layer_idx << std::endl; //test for break between layer_idx and layer_idx -1; LayerIslands below = islands_graph[layer_idx - 1]; // must exist, see while condition layer_idx--; @@ -738,7 +701,6 @@ void check_global_stability( } } - std::cout << "FINAL ITERATION FOR THE BED LEVEL: " << acc.volume << std::endl; // We have arrived to the bed level, now check for stability of the object part on the bed std::vector pivot_points; for (const Vec3f &p_point : acc.pivot_points) { @@ -834,9 +796,6 @@ Issues check_object_stability(const PrintObject *po, const Params ¶ms) { PixelGrid prev_layer_grid(po, flow_width); SupportGridFilter supports_presence_grid { po, params.min_distance_between_support_points }; - BOOST_LOG_TRIVIAL(debug) - << "SSG: flow width: " << flow_width; - // PREPARE BASE LAYER const Layer *layer = po->layers()[0]; for (const LayerRegion *layer_region : layer->regions()) { From 3d1f2f0cb66e77f70c6d8c71e1f0b9424a3538c9 Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Wed, 20 Jul 2022 16:23:03 +0200 Subject: [PATCH 047/100] implemented graph traversal, keeping the segments and the location of the weakest point for each island --- src/libslic3r/CMakeLists.txt | 3 +- src/libslic3r/SupportSpotsGenerator.cpp | 1262 +++++++++-------- .../SupportSpotsGeneratorRefactoring.cpp | 972 ------------- src/slic3r/GUI/BonjourDialog.hpp | 2 + 4 files changed, 708 insertions(+), 1531 deletions(-) delete mode 100644 src/libslic3r/SupportSpotsGeneratorRefactoring.cpp diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index 0c4fd20795..e478188356 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -245,8 +245,7 @@ set(SLIC3R_SOURCES SlicingAdaptive.hpp Subdivide.cpp Subdivide.hpp -# SupportSpotsGenerator.cpp - SupportSpotsGeneratorRefactoring.cpp + SupportSpotsGenerator.cpp SupportSpotsGenerator.hpp SupportMaterial.cpp SupportMaterial.hpp diff --git a/src/libslic3r/SupportSpotsGenerator.cpp b/src/libslic3r/SupportSpotsGenerator.cpp index 08d734a38e..4512c795b4 100644 --- a/src/libslic3r/SupportSpotsGenerator.cpp +++ b/src/libslic3r/SupportSpotsGenerator.cpp @@ -2,6 +2,7 @@ #include "tbb/parallel_for.h" #include "tbb/blocked_range.h" +#include "tbb/blocked_range2d.h" #include "tbb/parallel_reduce.h" #include #include @@ -23,29 +24,32 @@ namespace Slic3r { -static const size_t NULL_ACC_ID = std::numeric_limits::max(); - class ExtrusionLine { public: ExtrusionLine() : - a(Vec2f::Zero()), b(Vec2f::Zero()), len(0.0f), external_perimeter(false) { + a(Vec2f::Zero()), b(Vec2f::Zero()), len(0.0f), origin_entity(nullptr) { } - ExtrusionLine(const Vec2f &_a, const Vec2f &_b, bool external_perimeter) : - a(_a), b(_b), len((_a - _b).norm()), external_perimeter(external_perimeter) { + ExtrusionLine(const Vec2f &_a, const Vec2f &_b, const ExtrusionEntity *origin_entity) : + a(_a), b(_b), len((_a - _b).norm()), origin_entity(origin_entity) { } float length() { return (a - b).norm(); } + bool is_external_perimeter() const { + assert(origin_entity != nullptr); + return origin_entity->role() == erExternalPerimeter; + } + Vec2f a; Vec2f b; float len; - bool external_perimeter; + const ExtrusionEntity *origin_entity; + bool support_point_generated = false; float malformation = 0.0f; - size_t stability_accumulator_id = NULL_ACC_ID; static const constexpr int Dim = 2; using Scalar = Vec2f::Scalar; @@ -60,29 +64,136 @@ auto get_b(ExtrusionLine &&l) { namespace SupportSpotsGenerator { -void Issues::add(const Issues &layer_issues) { - supports_nedded.insert(supports_nedded.end(), layer_issues.supports_nedded.begin(), - layer_issues.supports_nedded.end()); - curling_up.insert(curling_up.end(), layer_issues.curling_up.begin(), layer_issues.curling_up.end()); +SupportPoint::SupportPoint(const Vec3f &position, float force, const Vec3f &direction) : + position(position), force(force), direction(direction) { } -bool Issues::empty() const { - return supports_nedded.empty() && curling_up.empty(); -} +class LinesDistancer { +private: + std::vector lines; + AABBTreeIndirect::Tree<2, float> tree; -SupportPoint::SupportPoint(const Vec3f &position, float weight) : - position(position), weight(weight) { -} +public: + explicit LinesDistancer(std::vector &lines) : + lines(lines) { + tree = AABBTreeLines::build_aabb_tree_over_indexed_lines(this->lines); + } -CurledFilament::CurledFilament(const Vec3f &position, float estimated_height) : - position(position), estimated_height(estimated_height) { -} + // negative sign means inside + float signed_distance_from_lines(const Vec2f &point, size_t &nearest_line_index_out, + Vec2f &nearest_point_out) const { + auto distance = AABBTreeLines::squared_distance_to_indexed_lines(lines, tree, point, nearest_line_index_out, + nearest_point_out); + if (distance < 0) + return std::numeric_limits::infinity(); -CurledFilament::CurledFilament(const Vec3f &position) : - position(position), estimated_height(0.0f) { -} + distance = sqrt(distance); + const ExtrusionLine &line = lines[nearest_line_index_out]; + Vec2f v1 = line.b - line.a; + Vec2f v2 = point - line.a; + if ((v1.x() * v2.y()) - (v1.y() * v2.x()) > 0.0) { + distance *= -1; + } + return distance; + } -struct VoxelGrid { + const ExtrusionLine& get_line(size_t line_idx) const { + return lines[line_idx]; + } + + const std::vector& get_lines() const { + return lines; + } +}; + +static const size_t NULL_ISLAND = std::numeric_limits::max(); + +class PixelGrid { + Vec2f pixel_size; + Vec2f origin; + Vec2f size; + Vec2i pixel_count; + + std::vector pixels { }; + +public: + PixelGrid(const PrintObject *po, float resolution) { + pixel_size = Vec2f(resolution, resolution); + + Vec2crd size_half = po->size().head<2>().cwiseQuotient(Vec2crd(2, 2)) + Vec2crd::Ones(); + Vec2f min = unscale(Vec2crd(-size_half.x(), -size_half.y())).cast(); + Vec2f max = unscale(Vec2crd(size_half.x(), size_half.y())).cast(); + + origin = min; + size = max - min; + pixel_count = size.cwiseQuotient(pixel_size).cast() + Vec2i::Ones(); + + pixels.resize(pixel_count.y() * pixel_count.x()); + clear(); + } + + void distribute_edge(const Vec2f &p1, const Vec2f &p2, size_t value) { + Vec2f dir = (p2 - p1); + float length = dir.norm(); + if (length < 0.1) { + return; + } + float step_size = this->pixel_size.x() / 2.0; + + float distributed_length = 0; + while (distributed_length < length) { + float next_len = std::min(length, distributed_length + step_size); + Vec2f location = p1 + ((next_len / length) * dir); + this->access_pixel(location) = value; + + distributed_length = next_len; + } + } + + void clear() { + for (size_t &val : pixels) { + val = NULL_ISLAND; + } + } + + float pixel_area() const { + return this->pixel_size.x() * this->pixel_size.y(); + } + + size_t get_pixel(const Vec2i &coords) const { + return pixels[this->to_pixel_index(coords)]; + } + + Vec2i get_pixel_count() { + return pixel_count; + } + + Vec2f get_pixel_center(const Vec2i &coords) const { + return origin + coords.cast().cwiseProduct(this->pixel_size) + + this->pixel_size.cwiseQuotient(Vec2f(2.0f, 2.0f)); + } + +private: + Vec2i to_pixel_coords(const Vec2f &position) const { + Vec2i pixel_coords = (position - this->origin).cwiseQuotient(this->pixel_size).cast(); + return pixel_coords; + } + + size_t to_pixel_index(const Vec2i &pixel_coords) const { + assert(pixel_coords.x() >= 0); + assert(pixel_coords.x() < pixel_count.x()); + assert(pixel_coords.y() >= 0); + assert(pixel_coords.y() < pixel_count.y()); + + return pixel_coords.y() * pixel_count.x() + pixel_coords.x(); + } + + size_t& access_pixel(const Vec2f &position) { + return pixels[this->to_pixel_index(this->to_pixel_coords(position))]; + } +}; + +struct SupportGridFilter { private: Vec3f cell_size; Vec3f origin; @@ -92,7 +203,7 @@ private: std::unordered_set taken_cells { }; public: - VoxelGrid(const PrintObject *po, float voxel_size) { + SupportGridFilter(const PrintObject *po, float voxel_size) { cell_size = Vec3f(voxel_size, voxel_size, voxel_size); Vec2crd size_half = po->size().head<2>().cwiseQuotient(Vec2crd(2, 2)) + Vec2crd::Ones(); @@ -137,197 +248,25 @@ public: }; -class LayerLinesDistancer { -private: - std::vector lines; - AABBTreeIndirect::Tree<2, float> tree; - -public: - explicit LayerLinesDistancer(std::vector &&lines) : - lines(lines) { - tree = AABBTreeLines::build_aabb_tree_over_indexed_lines(this->lines); - } - - // negative sign means inside - float signed_distance_from_lines(const Vec2f &point, size_t &nearest_line_index_out, - Vec2f &nearest_point_out) const { - auto distance = AABBTreeLines::squared_distance_to_indexed_lines(lines, tree, point, nearest_line_index_out, - nearest_point_out); - if (distance < 0) - return std::numeric_limits::infinity(); - - distance = sqrt(distance); - const ExtrusionLine &line = lines[nearest_line_index_out]; - Vec2f v1 = line.b - line.a; - Vec2f v2 = point - line.a; - if ((v1.x() * v2.y()) - (v1.y() * v2.x()) > 0.0) { - distance *= -1; - } - return distance; - } - - const ExtrusionLine& get_line(size_t line_idx) const { - return lines[line_idx]; - } - - const std::vector& get_lines() const { - return lines; - } - - void set_new_acc_id(size_t line_idx, size_t acc_id) { - lines[line_idx].stability_accumulator_id = acc_id; - } +struct IslandConnection { + float area; + Vec3f centroid_accumulator; }; -// StabilityAccumulator accumulates extrusions for each connected model part from bed to current printed layer. -// If the originaly disconected parts meet in the layer, their stability accumulators get merged and continue as one. -// (think legs of table, which get connected when the top desk is being printed). -// The class gathers mass, centroid mass, sticking force (bed extrusions, support points) and sticking centroid for the -// connected part. These values are then used to check global part stability. -class StabilityAccumulator { -private: - std::vector support_points { }; - Vec3f centroid_accumulator = Vec3f::Zero(); - float accumulated_volume { }; - Vec2f sticking_centroid_accumulator = Vec2f::Zero(); - float accumulated_sticking_force { }; +struct Island { + std::unordered_map connected_islands; + std::vector pivot_points; // for support points present on this layer (or bed extrusions) + float volume; + Vec3f volume_centroid_accumulator; + float sticking_force; // for support points present on this layer (or bed extrusions) + Vec3f sticking_centroid_accumulator; -public: - StabilityAccumulator() = default; - - void add_base_extrusion(const ExtrusionLine &line, float sticking_force, float print_z, float mm3_per_mm) { - accumulated_sticking_force += sticking_force; - sticking_centroid_accumulator += sticking_force * ((line.a + line.b) / 2.0f); - support_points.push_back(line.a); - support_points.push_back(line.b); - add_extrusion(line, print_z, mm3_per_mm); - } - - void add_support_point(const Vec2f &position, float sticking_force) { - support_points.push_back(position); - accumulated_sticking_force += sticking_force; - sticking_centroid_accumulator += sticking_force * position; - } - - void add_extrusion(const ExtrusionLine &line, float print_z, float mm3_per_mm) { - float volume = line.len * mm3_per_mm; - accumulated_volume += volume; - Vec2f center = (line.a + line.b) / 2.0f; - centroid_accumulator += volume * Vec3f(center.x(), center.y(), print_z); - } - - Vec3f get_centroid() const { - if (accumulated_volume <= 0.0f) { - return Vec3f::Zero(); - } - return centroid_accumulator / accumulated_volume; - } - - float get_sticking_force() const { - return accumulated_sticking_force; - } - - float get_accumulated_volume() const { - return accumulated_volume; - } - - const std::vector& get_support_points() const { - return support_points; - } - - Vec2f get_sticking_centroid() const { - if (accumulated_sticking_force <= 0.0f) { - return Vec2f::Zero(); - } - return sticking_centroid_accumulator / accumulated_sticking_force; - } - - void add_from(const StabilityAccumulator &acc) { - this->support_points.insert(this->support_points.end(), acc.support_points.begin(), - acc.support_points.end()); - this->centroid_accumulator += acc.centroid_accumulator; - this->accumulated_volume += acc.accumulated_volume; - this->accumulated_sticking_force += acc.accumulated_sticking_force; - this->sticking_centroid_accumulator += acc.sticking_centroid_accumulator; - } + std::vector external_lines; }; -// StabilityAccumulators class is wrapper over the vector of stability accumualtors. It provides a level of indirection -// between accumulator ID and the accumulator instance itself. While each extrusion line has one id, which is asigned -// when algorithm reaches the line's layer, the accumulator this id points to can change due to merging. -struct StabilityAccumulators { -private: - size_t next_id = 0; - std::unordered_map mapping; - std::vector accumulators; - - void merge_to(size_t from_id, size_t to_id) { - StabilityAccumulator &from_acc = this->access(from_id); - StabilityAccumulator &to_acc = this->access(to_id); - if (&from_acc == &to_acc) { - return; - } - to_acc.add_from(from_acc); - mapping[from_id] = mapping[to_id]; - from_acc = StabilityAccumulator { }; - - } - -public: - StabilityAccumulators() = default; - - size_t create_accumulator() { - size_t id = next_id; - next_id++; - mapping[id] = accumulators.size(); - accumulators.push_back(StabilityAccumulator { }); - return id; - } - - StabilityAccumulator& access(size_t id) { - return accumulators[mapping[id]]; - } - - void merge_accumulators(size_t id_a, size_t id_b) { - size_t from_id = std::max(id_a, id_b); - size_t to_id = std::min(id_a, id_b); - if (from_id == NULL_ACC_ID || to_id == NULL_ACC_ID) { - return; - } - StabilityAccumulator &from_acc = this->access(from_id); - StabilityAccumulator &to_acc = this->access(to_id); - if (&from_acc == &to_acc) { - return; - } - to_acc.add_from(from_acc); - mapping[from_id] = mapping[to_id]; - from_acc = StabilityAccumulator { }; - } - -#ifdef DEBUG_FILES - Vec3f get_accumulator_color(size_t id) { - if (mapping.find(id) == mapping.end()) { - BOOST_LOG_TRIVIAL(debug) - << "SSG: ERROR: uknown accumulator ID: " << id; - return Vec3f(1.0f, 1.0f, 1.0f); - } - - size_t pseudornd = ((mapping[id] + 127) * 33331 + 6907) % 987; - return value_to_rgbf(0.0f, float(987), float(pseudornd)); - } - - void log_accumulators() { - for (size_t i = 0; i < accumulators.size(); ++i) { - const auto &acc = accumulators[i]; - BOOST_LOG_TRIVIAL(debug) - << "SSG: accumulator POS: " << i << "\n" - << "SSG: get_accumulated_volume: " << acc.get_accumulated_volume() << "\n" - << "SSG: get_sticking_force: " << acc.get_sticking_force() << "\n" - << "SSG: support points count: " << acc.get_support_points().size() << "\n"; - - } - } -#endif +struct LayerIslands { + std::vector islands; + float layer_z; }; float get_flow_width(const LayerRegion *region, ExtrusionRole role) { @@ -375,28 +314,18 @@ struct ExtrusionPropertiesAccumulator { } }; -// check_extrusion_entity_stability checks each extrusion for local issues, appends the extrusion -// into checked lines, and gives it a stability accumulator id. If support is needed it pushes it -// into issues as well. -// Rules for stability accumulator id assigment: -// If there is close extrusion under, use min extrusion id between the id of the previous line, -// and id of line under. Also merge the accumulators of those two ids! -// If there is no close extrusion under, use id of the previous extrusion line. -// If there is no previous line, create new stability accumulator. void check_extrusion_entity_stability(const ExtrusionEntity *entity, - StabilityAccumulators &stability_accs, - Issues &issues, - std::vector &checked_lines, + std::vector &checked_lines_out, float print_z, const LayerRegion *layer_region, - const LayerLinesDistancer &prev_layer_lines, + const LinesDistancer &prev_layer_lines, + Issues &issues, const Params ¶ms) { if (entity->is_collection()) { for (const auto *e : static_cast(entity)->entities) { - check_extrusion_entity_stability(e, stability_accs, issues, checked_lines, print_z, layer_region, - prev_layer_lines, - params); + check_extrusion_entity_stability(e, checked_lines_out, print_z, layer_region, prev_layer_lines, + issues, params); } } else { //single extrusion path, with possible varying parameters const auto to_vec3f = [print_z](const Vec2f &point) { @@ -406,8 +335,7 @@ void check_extrusion_entity_stability(const ExtrusionEntity *entity, entity->collect_points(points); std::vector lines; lines.reserve(points.size() * 1.5); - bool is_ex_perimeter = entity->role() == erExternalPerimeter; - lines.emplace_back(unscaled(points[0]).cast(), unscaled(points[0]).cast(), is_ex_perimeter); + lines.emplace_back(unscaled(points[0]).cast(), unscaled(points[0]).cast(), entity); for (int point_idx = 0; point_idx < int(points.size() - 1); ++point_idx) { Vec2f start = unscaled(points[point_idx]).cast(); Vec2f next = unscaled(points[point_idx + 1]).cast(); @@ -419,11 +347,10 @@ void check_extrusion_entity_stability(const ExtrusionEntity *entity, for (int i = 0; i < lines_count; ++i) { Vec2f a(start + v * (i * step_size)); Vec2f b(start + v * ((i + 1) * step_size)); - lines.emplace_back(a, b, is_ex_perimeter); + lines.emplace_back(a, b, entity); } } - size_t current_stability_acc = NULL_ACC_ID; ExtrusionPropertiesAccumulator bridging_acc { }; ExtrusionPropertiesAccumulator malformation_acc { }; bridging_acc.add_distance(params.bridge_distance + 1.0f); // Initialise unsupported distance with larger than tolerable distance -> @@ -432,8 +359,6 @@ void check_extrusion_entity_stability(const ExtrusionEntity *entity, for (size_t line_idx = 0; line_idx < lines.size(); ++line_idx) { ExtrusionLine ¤t_line = lines[line_idx]; - float mm3_per_mm = float(entity->min_mm3_per_mm()); - float curr_angle = 0; if (line_idx + 1 < lines.size()) { const Vec2f v1 = current_line.b - current_line.a; @@ -448,28 +373,16 @@ void check_extrusion_entity_stability(const ExtrusionEntity *entity, float dist_from_prev_layer = prev_layer_lines.signed_distance_from_lines(current_line.b, nearest_line_idx, nearest_point); - if (dist_from_prev_layer < flow_width) { - const ExtrusionLine &nearest_line = prev_layer_lines.get_line(nearest_line_idx); - size_t acc_id = nearest_line.stability_accumulator_id; - stability_accs.merge_accumulators(acc_id, current_stability_acc); - current_stability_acc = std::min(acc_id, current_stability_acc); - current_line.stability_accumulator_id = current_stability_acc; - stability_accs.access(current_stability_acc).add_extrusion(current_line, print_z, mm3_per_mm); + if (fabs(dist_from_prev_layer) < flow_width) { bridging_acc.reset(); } else { bridging_acc.add_distance(current_line.len); - if (current_stability_acc == NULL_ACC_ID) { - current_stability_acc = stability_accs.create_accumulator(); - } - StabilityAccumulator ¤t_segment = stability_accs.access(current_stability_acc); - current_line.stability_accumulator_id = current_stability_acc; - current_segment.add_extrusion(current_line, print_z, mm3_per_mm); if (bridging_acc.distance // if unsupported distance is larger than bridge distance linearly decreased by curvature, enforce supports. > params.bridge_distance / (1.0f + (bridging_acc.max_curvature * params.bridge_distance_decrease_by_curvature_factor / PI))) { - current_segment.add_support_point(current_line.b, 0.0f); // Do not count extrusion supports into the sticking force. They can be very densely placed, causing algorithm to overestimate stickiness. - issues.supports_nedded.emplace_back(to_vec3f(current_line.b), 1.0); + issues.support_points.emplace_back(to_vec3f(current_line.b), 0.0f, Vec3f(0.f, 0.0f, -1.0f)); + current_line.support_point_generated = true; bridging_acc.reset(); } } @@ -481,166 +394,83 @@ void check_extrusion_entity_stability(const ExtrusionEntity *entity, } if (dist_from_prev_layer > flow_width * 0.3) { malformation_acc.add_distance(current_line.len); - current_line.malformation += 0.15 - * (0.8 + 0.2 * malformation_acc.max_curvature / (1.0f + 0.5f * malformation_acc.distance)); + current_line.malformation += 0.15f + * (0.8f + 0.2f * malformation_acc.max_curvature / (1.0f + 0.5f * malformation_acc.distance)); } else { malformation_acc.reset(); } } - checked_lines.insert(checked_lines.end(), lines.begin(), lines.end()); + checked_lines_out.insert(checked_lines_out.end(), lines.begin(), lines.end()); } } -//check_layer_global_stability checks stability of the accumulators that are still present on the current line -// ( this is determined from the gathered checked_lines vector) -// For each accumulator and each its extrusion, forces and torques (weight, bed movement, extruder pressure, stickness to bed) -// are computed and if stability is not sufficient, support points are added -// accumualtors are filtered by their pointer address, since one accumulator can have multiple IDs due to merging -void check_layer_global_stability(StabilityAccumulators &stability_accs, - VoxelGrid &supports_presence_grid, - Issues &issues, - float flow_width, - const std::vector &checked_lines, - float print_z, +std::tuple reckon_islands( + const Layer *layer, bool first_layer, + size_t prev_layer_islands_count, + const PixelGrid &prev_layer_grid, + const std::vector &layer_lines, const Params ¶ms) { - std::unordered_map> layer_accs_w_lines; - for (size_t i = 0; i < checked_lines.size(); ++i) { - layer_accs_w_lines[&stability_accs.access(checked_lines[i].stability_accumulator_id)].push_back( - checked_lines[i]); - } - for (auto &accumulator : layer_accs_w_lines) { - StabilityAccumulator *acc = accumulator.first; - LayerLinesDistancer acc_lines(std::move(accumulator.second)); - - if (acc->get_support_points().empty()) { - // acc_lines cannot be empty - if the accumulator has no extrusion in the current layer, it is not considered in stability computation - acc->add_support_point(acc_lines.get_line(0).a, 0.0f); - issues.supports_nedded.emplace_back(to_3d(acc_lines.get_line(0).a, print_z), 0.0); - } - const std::vector &support_points = acc->get_support_points(); - - auto coord_fn = [&support_points](size_t idx, size_t dim) { - return support_points[idx][dim]; - }; - KDTreeIndirect<2, float, decltype(coord_fn)> supports_tree(coord_fn, support_points.size()); - - for (const ExtrusionLine &line : acc_lines.get_lines()) { - Vec2f line_dir = (line.b - line.a).normalized(); - Vec2f pivot_site_search_point = line.b + line_dir * 300.0f; - size_t pivot_idx = find_closest_point(supports_tree, pivot_site_search_point); - const Vec2f &pivot = support_points[pivot_idx]; - - const Vec2f &sticking_centroid = acc->get_sticking_centroid(); - float sticking_arm = (pivot - sticking_centroid).norm(); - float sticking_torque = sticking_arm * acc->get_sticking_force(); - - float mass = acc->get_accumulated_volume() * params.filament_density; - const Vec3f &mass_centorid = acc->get_centroid(); - float weight = mass * params.gravity_constant; - float weight_arm = (pivot - mass_centorid.head<2>()).norm(); - float weight_torque = weight_arm * weight; - - float bed_movement_arm = mass_centorid.z(); - float bed_movement_force = params.max_acceleration * mass; - float bed_movement_torque = bed_movement_force * bed_movement_arm; - - Vec3f extruder_pressure_direction = to_3d(line_dir, 0.0f); - extruder_pressure_direction.z() = -0.2 - line.malformation * 0.5; - extruder_pressure_direction.normalize(); - float conflict_torque_arm = (to_3d(Vec2f(pivot - line.b), print_z).cross( - extruder_pressure_direction)).norm(); - float extruder_conflict_force = params.tolerable_extruder_conflict_force + - std::min(line.malformation, 1.0f) * params.malformations_additive_conflict_extruder_force; - float extruder_conflict_torque = extruder_conflict_force * conflict_torque_arm; - - float total_torque = bed_movement_torque + extruder_conflict_torque - weight_torque - sticking_torque; - - if (total_torque > 0) { - Vec2f target_point; - size_t _idx; - acc_lines.signed_distance_from_lines(pivot_site_search_point, _idx, target_point); - if (!supports_presence_grid.position_taken(to_3d(target_point, print_z))) { - float area = params.support_points_interface_radius * params.support_points_interface_radius - * float(PI); - float sticking_force = area * params.support_adhesion; - acc->add_support_point(target_point, sticking_force); - issues.supports_nedded.emplace_back(to_3d(target_point, print_z), - extruder_conflict_torque - sticking_torque); - supports_presence_grid.take_position(to_3d(target_point, print_z)); - } - } -#if 0 - BOOST_LOG_TRIVIAL(debug) - << "SSG: sticking_arm: " << sticking_arm; - BOOST_LOG_TRIVIAL(debug) - << "SSG: sticking_torque: " << sticking_torque; - BOOST_LOG_TRIVIAL(debug) - << "SSG: weight_arm: " << sticking_arm; - BOOST_LOG_TRIVIAL(debug) - << "SSG: weight_torque: " << weight_torque; - BOOST_LOG_TRIVIAL(debug) - << "SSG: bed_movement_arm: " << bed_movement_arm; - BOOST_LOG_TRIVIAL(debug) - << "SSG: bed_movement_torque: " << bed_movement_torque; - BOOST_LOG_TRIVIAL(debug) - << "SSG: conflict_torque_arm: " << conflict_torque_arm; - BOOST_LOG_TRIVIAL(debug) - << "SSG: extruder_conflict_torque: " << extruder_conflict_torque; - BOOST_LOG_TRIVIAL(debug) - << "SSG: total_torque: " << total_torque << " printz: " << print_z; -#endif - } - } -} - -void reckon_thin_islands(StabilityAccumulators &stability_accs, - float flow_width, - float mm3_per_mm, - float print_z, - LayerLinesDistancer &layer_lines, - const Params& params) { - const std::vector &lines = layer_lines.get_lines(); - std::vector> extrusions; //start and end idx (one beyond last extrusion) [start,end) - Vec2f current_pt = lines[0].a; - std::pair current_ext(0,1); - for (size_t lidx = 0; lidx < lines.size(); ++lidx) { - const ExtrusionLine& line = lines[lidx]; - if (line.a == current_pt) { - current_ext.second = lidx + 1; + //extract extrusions (connected paths from multiple lines) from the layer_lines. belonging to single polyline is determined by origin_entity ptr. + // result is a vector of [start, end) index pairs into the layer_lines vector + std::vector> extrusions; //start and end idx (one beyond last extrusion) [start,end) + const ExtrusionEntity *current_ex = nullptr; + for (size_t lidx = 0; lidx < layer_lines.size(); ++lidx) { + const ExtrusionLine &line = layer_lines[lidx]; + if (line.origin_entity == current_ex) { + extrusions.back().second = lidx + 1; } else { - extrusions.push_back(current_ext); - current_ext.first = lidx; - current_ext.second = lidx + 1; + extrusions.emplace_back(lidx, lidx + 1); + current_ex = line.origin_entity; } - current_pt = line.b; } - std::vector islands; - std::vector> island_extrusions; + std::vector islands; // these search trees will be used to determine to which island does the extrusion begin + std::vector> island_extrusions; //final assigment of each extrusion to an island + // initliaze the search from external perimeters - at the beginning, there is island candidate for each external perimeter. + // some of them will disappear (e.g. holes) for (size_t e = 0; e < extrusions.size(); ++e) { - if (lines[extrusions[e].first].external_perimeter) { + if (layer_lines[extrusions[e].first].is_external_perimeter()) { std::vector copy(extrusions[e].second - extrusions[e].first); for (size_t ex_line_idx = extrusions[e].first; ex_line_idx < extrusions[e].second; ++ex_line_idx) { - copy[ex_line_idx-extrusions[e].first] = lines[ex_line_idx]; + copy[ex_line_idx - extrusions[e].first] = layer_lines[ex_line_idx]; } - islands.emplace_back(std::move(copy)); - island_extrusions.push_back({e}); + islands.emplace_back(copy); + island_extrusions.push_back( { e }); } } + // backup code if islands not found - this can currently happen, as external perimeters may be also pure overhang perimeters, and there is no + // way to distinguish external extrusions with total certainty. + // If that happens, just make the first extrusion into island - it may be wrong, but it won't crash. + if (islands.empty() && !extrusions.empty()) { + std::vector copy(extrusions[0].second - extrusions[0].first); + for (size_t ex_line_idx = extrusions[0].first; ex_line_idx < extrusions[0].second; ++ex_line_idx) { + copy[ex_line_idx - extrusions[0].first] = layer_lines[ex_line_idx]; + } + islands.emplace_back(copy); + island_extrusions.push_back( { 0 }); + } - for (size_t i = 0; i < islands.size(); ++i) { - for (size_t e = 0; e < extrusions.size(); ++e) { - if (!lines[extrusions[e].first].external_perimeter){ + // assign non external extrusions to islands + for (size_t e = 0; e < extrusions.size(); ++e) { + if (!layer_lines[extrusions[e].first].is_external_perimeter()) { + bool island_assigned = false; + for (size_t i = 0; i < islands.size(); ++i) { size_t _idx; Vec2f _pt; - if (islands[i].signed_distance_from_lines(lines[extrusions[e].first].a, _idx, _pt) < 0) { + if (islands[i].signed_distance_from_lines(layer_lines[extrusions[e].first].a, _idx, _pt) < 0) { island_extrusions[i].push_back(e); + island_assigned = true; + break; } } + if (!island_assigned) { // If extrusion is not assigned for some reason, push it into the first island. As with the previous backup code, + // it may be wrong, but it won't crash + island_extrusions[0].push_back(e); + } } } - + // merge islands which are embedded within each other (mainly holes) for (size_t i = 0; i < islands.size(); ++i) { if (islands[i].get_lines().empty()) { continue; @@ -659,241 +489,570 @@ void reckon_thin_islands(StabilityAccumulators &stability_accs, } } - size_t islands_count = 0; - for (const std::vector& island_ex : island_extrusions) { + float flow_width = get_flow_width(layer->regions()[0], erExternalPerimeter); + // after filtering the layer lines into islands, build the result LayerIslands structure. + LayerIslands result { }; + result.layer_z = layer->slice_z; + std::vector line_to_island_mapping(layer_lines.size(), NULL_ISLAND); + for (const std::vector &island_ex : island_extrusions) { if (island_ex.empty()) { continue; } - islands_count++; - float cross_section = 0.0f; - size_t acc_id = NULL_ACC_ID; + + Island island { }; + island.external_lines.insert(island.external_lines.end(), + layer_lines.begin() + extrusions[island_ex[0]].first, + layer_lines.begin() + extrusions[island_ex[0]].second); for (size_t extrusion_idx : island_ex) { for (size_t lidx = extrusions[extrusion_idx].first; lidx < extrusions[extrusion_idx].second; ++lidx) { - const ExtrusionLine& line = lines[lidx]; - cross_section += line.len * flow_width; - stability_accs.merge_accumulators(acc_id, line.stability_accumulator_id); - acc_id = std::min(acc_id, line.stability_accumulator_id); + line_to_island_mapping[lidx] = result.islands.size(); + const ExtrusionLine &line = layer_lines[lidx]; + float volume = line.origin_entity->min_mm3_per_mm() * line.len; + island.volume += volume; + island.volume_centroid_accumulator += to_3d(Vec2f((line.a + line.b) / 2.0f), float(layer->print_z)) + * volume; + + if (first_layer) { + float sticking_force = line.len * flow_width * params.base_adhesion; + island.sticking_force += sticking_force; + island.sticking_centroid_accumulator += sticking_force + * to_3d(Vec2f((line.a + line.b) / 2.0f), float(layer->print_z)); + if (line.is_external_perimeter()) { + island.pivot_points.push_back(to_3d(Vec2f(line.b), float(layer->print_z))); + } + } else if (layer_lines[lidx].support_point_generated) { + float support_interface_area = params.support_points_interface_radius + * params.support_points_interface_radius + * float(PI); + float sticking_force = support_interface_area * params.support_adhesion; + island.sticking_force += sticking_force; + island.sticking_centroid_accumulator += sticking_force + * to_3d(Vec2f(line.b), float(layer->print_z)); + island.pivot_points.push_back(to_3d(Vec2f(line.b), float(layer->print_z))); + } } } + result.islands.push_back(island); + } - float max_force = cross_section * params.tensile_strength; - - if (stability_accs.access(acc_id).get_sticking_force() > max_force) { - BOOST_LOG_TRIVIAL(debug) << "SSG: Forking new accumulator for island because tensile strenth is too low: " << max_force; - BOOST_LOG_TRIVIAL(debug) << "SSG: sticking force: " << stability_accs.access(acc_id).get_sticking_force(); - - size_t new_acc_id = stability_accs.create_accumulator(); - StabilityAccumulator& acc = stability_accs.access(new_acc_id); - - for (size_t extrusion_idx : island_ex) { - for (size_t lidx = extrusions[extrusion_idx].first; lidx < extrusions[extrusion_idx].second; ++lidx) { - const ExtrusionLine& line = lines[lidx]; - float tensile_strength = params.tensile_strength * line.len * flow_width; - acc.add_base_extrusion(line, tensile_strength, print_z, mm3_per_mm); - layer_lines.set_new_acc_id(lidx, new_acc_id); + //LayerIslands structure built. Now determine connections and their areas to the previous layer using raterization. + PixelGrid current_layer_grid = prev_layer_grid; + current_layer_grid.clear(); + // build index image of current layer + tbb::parallel_for(tbb::blocked_range(0, layer_lines.size()), + [&layer_lines, ¤t_layer_grid, &line_to_island_mapping]( + tbb::blocked_range r) { + for (size_t i = r.begin(); i < r.end(); ++i) { + size_t island = line_to_island_mapping[i]; + const ExtrusionLine &line = layer_lines[i]; + current_layer_grid.distribute_edge(line.a, line.b, island); } + }); + + //compare the image of previous layer with the current layer. For each pair of overlapping valid pixels, add pixel area to the respective island connection + for (size_t x = 0; x < size_t(current_layer_grid.get_pixel_count().x()); ++x) { + for (size_t y = 0; y < size_t(current_layer_grid.get_pixel_count().y()); ++y) { + Vec2i coords = Vec2i(x, y); + if (current_layer_grid.get_pixel(coords) != NULL_ISLAND + && prev_layer_grid.get_pixel(coords) != NULL_ISLAND) { + IslandConnection& connection = result.islands[current_layer_grid.get_pixel(coords)] + .connected_islands[prev_layer_grid.get_pixel(coords)]; + connection.area += current_layer_grid.pixel_area(); + connection.centroid_accumulator += to_3d(current_layer_grid.get_pixel_center(coords), result.layer_z) * current_layer_grid.pixel_area(); } } } - BOOST_LOG_TRIVIAL(debug) << "SSG: There are " << islands_count << " islands on printz: " << print_z; - + return {result, current_layer_grid}; } -Issues -check_object_stability(const PrintObject *po, const Params ¶ms) { +struct ObjectPart { + float volume { }; + Vec3f volume_centroid_accumulator = Vec3f::Zero(); + float sticking_force { }; + Vec3f sticking_centroid_accumulator = Vec3f::Zero(); + std::vector pivot_points { }; + + void add(const ObjectPart &other) { + this->volume_centroid_accumulator += other.volume_centroid_accumulator; + this->volume += other.volume; + this->sticking_force += other.sticking_force; + this->sticking_centroid_accumulator += other.sticking_centroid_accumulator; + this->pivot_points.insert(this->pivot_points.end(), other.pivot_points.begin(), other.pivot_points.end()); + } + + ObjectPart(const Island &island) { + this->volume = island.volume; + this->volume_centroid_accumulator = island.volume_centroid_accumulator; + this->sticking_force = island.sticking_force; + this->sticking_centroid_accumulator = island.sticking_centroid_accumulator; + this->pivot_points = island.pivot_points; + } + + ObjectPart() = default; +}; + +struct WeakestConnection { + float area = 0.0f; + Vec3f centroid_accumulator = Vec3f::Zero(); + + void add(const WeakestConnection& other) { + this->area += other.area; + this->centroid_accumulator += other.centroid_accumulator; + } +}; + +Issues check_global_stability(const std::vector &islands_graph, const Params ¶ms) { + Issues issues { }; + size_t next_part_idx = 0; + std::unordered_map active_object_parts; + std::unordered_map prev_island_to_object_part_mapping; + std::unordered_map next_island_to_object_part_mapping; + + std::unordered_map prev_island_to_weakest_connection; + std::unordered_map next_island_to_weakest_connection; + + for (size_t layer_idx = 0; layer_idx < islands_graph.size(); ++layer_idx) { + float layer_z = islands_graph[layer_idx].layer_z; + std::unordered_set layer_active_parts; + std::cout << "at layer: " << layer_idx << " the following island to object mapping is used:" << std::endl; + for (const auto &m : prev_island_to_object_part_mapping) { + std::cout << "island " << m.first << " maps to part " << m.second << std::endl; + Vec3f connection_center = prev_island_to_weakest_connection[m.first].centroid_accumulator / prev_island_to_weakest_connection[m.first].area; + std::cout << " island has weak point with connection area: " << + prev_island_to_weakest_connection[m.first].area << " and center: " << + connection_center.x() << " " << connection_center.y() << " " << connection_center.z() << std::endl; + } + + for (size_t island_idx = 0; island_idx < islands_graph[layer_idx].islands.size(); ++island_idx) { + const Island &island = islands_graph[layer_idx].islands[island_idx]; + if (island.connected_islands.empty()) { //new object part emerging + size_t part_idx = next_part_idx; + next_part_idx++; + active_object_parts.emplace(part_idx, ObjectPart(island)); + next_island_to_object_part_mapping.emplace(island_idx, part_idx); + next_island_to_weakest_connection.emplace(island_idx, + WeakestConnection{INFINITY, Vec3f::Zero()}); + layer_active_parts.insert(part_idx); + } else { + size_t final_part_idx{}; + WeakestConnection transfered_weakest_connection{}; + WeakestConnection new_weakest_connection{}; + // MERGE parts + { + std::unordered_set part_indices; + for (const auto &connection : island.connected_islands) { + part_indices.insert(prev_island_to_object_part_mapping.at(connection.first)); + transfered_weakest_connection.add(prev_island_to_weakest_connection.at(connection.first)); + new_weakest_connection.area += connection.second.area; + new_weakest_connection.centroid_accumulator += connection.second.centroid_accumulator; + } + final_part_idx = *part_indices.begin(); + for (size_t part_idx : part_indices) { + if (final_part_idx != part_idx) { + std::cout << "at layer: " << layer_idx << " merging object part: " << part_idx + << " into final part: " << final_part_idx << std::endl; + active_object_parts.at(final_part_idx).add(active_object_parts.at(part_idx)); + active_object_parts.erase(part_idx); + } + } + } + auto estimate_strength = [layer_z](const WeakestConnection& conn){ + float radius = fsqrt(conn.area / PI); + float arm_len_estimate = std::max(0.001f, layer_z - (conn.centroid_accumulator.z() / conn.area)); + return radius * conn.area / arm_len_estimate; + }; + + if (estimate_strength(transfered_weakest_connection) < estimate_strength(new_weakest_connection)) { + new_weakest_connection = transfered_weakest_connection; + } + next_island_to_weakest_connection.emplace(island_idx, new_weakest_connection); + next_island_to_object_part_mapping.emplace(island_idx, final_part_idx); + ObjectPart &part = active_object_parts[final_part_idx]; + part.add(ObjectPart(island)); + layer_active_parts.insert(final_part_idx); + } + } + + std::unordered_set parts_to_delete; + for (const auto &part : active_object_parts) { + if (layer_active_parts.find(part.first) == layer_active_parts.end()) { + parts_to_delete.insert(part.first); + } else { + std::cout << "at layer " << layer_idx << " part is still active: " << part.first << std::endl; + } + } + for (size_t part_id : parts_to_delete) { + active_object_parts.erase(part_id); + std::cout << " at layer: " << layer_idx << " removing object part " << part_id << std::endl; + } + prev_island_to_object_part_mapping = next_island_to_object_part_mapping; + next_island_to_object_part_mapping.clear(); + prev_island_to_weakest_connection = next_island_to_weakest_connection; + next_island_to_weakest_connection.clear(); + + } + return issues; +} + +/* + + // islands_graph.back() refers to the top most (current) layer + for (size_t island_idx = 0; island_idx < islands_graph.back().islands.size(); ++island_idx) { + Island &island = islands_graph.back().islands[island_idx]; + + std::vector island_external_lines; + for (size_t lidx : islands_lines[island_idx]) { + island_external_lines.push_back(layer_lines[lidx]); + } + LinesDistancer island_lines_dist(island_external_lines); + Accumulator acc = island; // in acc, we accumulate the mass and other properties of the object part as we traverse the islands down to bed + // There is one object part for each island at the top most layer, and each one is computed individually - + // Some of the calculations will be done multiple times + int layer_idx = islands_graph.size() - 1; + // traverse the islands graph down, and for each connection area, calculate if it holds or breaks + while (acc.connected_islands.size() > 0) { + //test for break between layer_idx and layer_idx -1; + LayerIslands below = islands_graph[layer_idx - 1]; // must exist, see while condition + layer_idx--; + // initialize variables that we will accumulate over all islands, which are connected to the current object part + std::vector pivot_points; + Vec2f sticking_centroid; + float connection_area = 0; + for (const auto &pair : acc.connected_islands) { + const Island &below_i = below.islands[pair.first]; + Vec2f centroid = (below_i.volume_centroid_accumulator / below_i.volume).head<2>(); // centroid of the island 'below_i'; TODO it should be centroid of the connection area + pivot_points.push_back(centroid); // for object parts, we also consider breaking pivots in the centroids of the islands + sticking_centroid += centroid * pair.second; // pair.second is connection area in mm^2 + connection_area += pair.second; + } + sticking_centroid /= connection_area; //normalize to get final sticking centroid + for (const Vec3f &p_point : acc.pivot_points) { + pivot_points.push_back(p_point.head<2>()); + } + // Now we have accumulated pivot points, connection area and sticking centroid of the whole layer to the current object part + + // create KD tree over current pivot points + auto coord_fn = [&pivot_points](size_t idx, size_t dim) { + return pivot_points[idx][dim]; + }; + KDTreeIndirect<2, float, decltype(coord_fn)> pivot_points_tree(coord_fn, pivot_points.size()); + + // iterate over extrusions at top layer island, check each for stability + for (const ExtrusionLine &line : island_external_lines) { + Vec2f line_dir = (line.b - line.a).normalized(); + Vec2f pivot_site_search_point = line.b + line_dir * 300.0f; + size_t pivot_idx = find_closest_point(pivot_points_tree, pivot_site_search_point); + const Vec2f &pivot = pivot_points[pivot_idx]; + + float sticking_arm = (pivot - sticking_centroid).norm(); + float sticking_torque = sticking_arm * connection_area * params.tensile_strength; // For breakage in between layers, we compute with tensile strength, not bed adhesion + + float mass = acc.volume * params.filament_density; + const Vec3f &mass_centorid = acc.volume_centroid_accumulator / acc.volume; + float weight = mass * params.gravity_constant; + float weight_arm = (pivot - mass_centorid.head<2>()).norm(); + float weight_torque = weight_arm * weight; + + float bed_movement_arm = mass_centorid.z(); + float bed_movement_force = params.max_acceleration * mass; + float bed_movement_torque = bed_movement_force * bed_movement_arm; + + Vec3f extruder_pressure_direction = to_3d(line_dir, 0.0f); + extruder_pressure_direction.z() = -0.2 - line.malformation * 0.5; + extruder_pressure_direction.normalize(); + float conflict_torque_arm = (to_3d(Vec2f(pivot - line.b), print_z).cross( + extruder_pressure_direction)).norm(); + float extruder_conflict_force = params.tolerable_extruder_conflict_force + + std::min(line.malformation, 1.0f) * params.malformations_additive_conflict_extruder_force; + float extruder_conflict_torque = extruder_conflict_force * conflict_torque_arm; + + float total_torque = bed_movement_torque + extruder_conflict_torque - weight_torque - sticking_torque; + + if (total_torque > 0) { + Vec2f target_point { }; + size_t _idx { }; + island_lines_dist.signed_distance_from_lines(pivot_site_search_point, _idx, target_point); + if (!supports_presence_grid.position_taken(to_3d(target_point, print_z))) { + float area = params.support_points_interface_radius * params.support_points_interface_radius + * float(PI); + float sticking_force = area * params.support_adhesion; + Vec3f support_point = to_3d(target_point, print_z); + island.pivot_points.push_back(support_point); + island.sticking_force += sticking_force; + island.sticking_centroid_accumulator += sticking_force * support_point; + issues.support_points.emplace_back(support_point, + extruder_conflict_torque - sticking_torque, extruder_pressure_direction); + supports_presence_grid.take_position(to_3d(target_point, print_z)); + } + } + #if 0 + BOOST_LOG_TRIVIAL(debug) + << "SSG: sticking_arm: " << sticking_arm; + BOOST_LOG_TRIVIAL(debug) + << "SSG: sticking_torque: " << sticking_torque; + BOOST_LOG_TRIVIAL(debug) + << "SSG: weight_arm: " << sticking_arm; + BOOST_LOG_TRIVIAL(debug) + << "SSG: weight_torque: " << weight_torque; + BOOST_LOG_TRIVIAL(debug) + << "SSG: bed_movement_arm: " << bed_movement_arm; + BOOST_LOG_TRIVIAL(debug) + << "SSG: bed_movement_torque: " << bed_movement_torque; + BOOST_LOG_TRIVIAL(debug) + << "SSG: conflict_torque_arm: " << conflict_torque_arm; + BOOST_LOG_TRIVIAL(debug) + << "SSG: extruder_conflict_torque: " << extruder_conflict_torque; + BOOST_LOG_TRIVIAL(debug) + << "SSG: total_torque: " << total_torque << " printz: " << print_z; + #endif + } + + std::unordered_map tmp = acc.connected_islands; + acc.connected_islands.clear(); + // finally, add gathered islands to accumulator, and continue down to next layer + for (const auto &pair : tmp) { + const Island &below_i = below.islands[pair.first]; + for (const auto &below_islands : below_i.connected_islands) { + acc.connected_islands[below_islands.first] += below_islands.second; + } + for (const Vec3f &pivot_p : below_i.pivot_points) { + acc.pivot_points.push_back(pivot_p); + } + acc.sticking_centroid_accumulator += below_i.sticking_centroid_accumulator; + acc.sticking_force += below_i.sticking_force; + acc.volume += below_i.volume; + acc.volume_centroid_accumulator += below_i.volume_centroid_accumulator; + } + } + + // We have arrived to the bed level, now check for stability of the object part on the bed + std::vector pivot_points; + for (const Vec3f &p_point : acc.pivot_points) { + pivot_points.push_back(p_point.head<2>()); + } + auto coord_fn = [&pivot_points](size_t idx, size_t dim) { + return pivot_points[idx][dim]; + }; + KDTreeIndirect<2, float, decltype(coord_fn)> pivot_points_tree(coord_fn, pivot_points.size()); + + for (const ExtrusionLine &line : island_external_lines) { + Vec2f line_dir = (line.b - line.a).normalized(); + Vec2f pivot_site_search_point = line.b + line_dir * 300.0f; + size_t pivot_idx = find_closest_point(pivot_points_tree, pivot_site_search_point); + const Vec2f &pivot = pivot_points[pivot_idx]; + + const Vec2f &sticking_centroid = acc.sticking_centroid_accumulator.head<2>() / acc.sticking_force; + float sticking_arm = (pivot - sticking_centroid).norm(); + float sticking_torque = sticking_arm * acc.sticking_force; + + float mass = acc.volume * params.filament_density; + const Vec3f &mass_centorid = acc.volume_centroid_accumulator / acc.volume; + float weight = mass * params.gravity_constant; + float weight_arm = (pivot - mass_centorid.head<2>()).norm(); + float weight_torque = weight_arm * weight; + + float bed_movement_arm = mass_centorid.z(); + float bed_movement_force = params.max_acceleration * mass; + float bed_movement_torque = bed_movement_force * bed_movement_arm; + + Vec3f extruder_pressure_direction = to_3d(line_dir, 0.0f); + extruder_pressure_direction.z() = -0.2 - line.malformation * 0.5; + extruder_pressure_direction.normalize(); + float conflict_torque_arm = (to_3d(Vec2f(pivot - line.b), print_z).cross( + extruder_pressure_direction)).norm(); + float extruder_conflict_force = params.tolerable_extruder_conflict_force + + std::min(line.malformation, 1.0f) * params.malformations_additive_conflict_extruder_force; + float extruder_conflict_torque = extruder_conflict_force * conflict_torque_arm; + + float total_torque = bed_movement_torque + extruder_conflict_torque - weight_torque - sticking_torque; + + if (total_torque > 0) { + Vec2f target_point; + size_t _idx; + island_lines_dist.signed_distance_from_lines(pivot_site_search_point, _idx, target_point); + if (!supports_presence_grid.position_taken(to_3d(target_point, print_z))) { + float area = params.support_points_interface_radius * params.support_points_interface_radius + * float(PI); + float sticking_force = area * params.support_adhesion; + Vec3f support_point = to_3d(target_point, print_z); + island.pivot_points.push_back(support_point); + island.sticking_force += sticking_force; + island.sticking_centroid_accumulator += sticking_force * support_point; + issues.support_points.emplace_back(support_point, + extruder_conflict_torque - sticking_torque, extruder_pressure_direction); + supports_presence_grid.take_position(to_3d(target_point, print_z)); + } + } + #if 0 + BOOST_LOG_TRIVIAL(debug) + << "SSG: sticking_arm: " << sticking_arm; + BOOST_LOG_TRIVIAL(debug) + << "SSG: sticking_torque: " << sticking_torque; + BOOST_LOG_TRIVIAL(debug) + << "SSG: weight_arm: " << sticking_arm; + BOOST_LOG_TRIVIAL(debug) + << "SSG: weight_torque: " << weight_torque; + BOOST_LOG_TRIVIAL(debug) + << "SSG: bed_movement_arm: " << bed_movement_arm; + BOOST_LOG_TRIVIAL(debug) + << "SSG: bed_movement_torque: " << bed_movement_torque; + BOOST_LOG_TRIVIAL(debug) + << "SSG: conflict_torque_arm: " << conflict_torque_arm; + BOOST_LOG_TRIVIAL(debug) + << "SSG: extruder_conflict_torque: " << extruder_conflict_torque; + BOOST_LOG_TRIVIAL(debug) + << "SSG: total_torque: " << total_torque << " printz: " << print_z; + #endif + } + } + + return issues; + */ + +std::tuple> check_extrusions_and_build_graph(const PrintObject *po, + const Params ¶ms) { #ifdef DEBUG_FILES - FILE *debug_acc = boost::nowide::fopen(debug_out_path("accumulators.obj").c_str(), "w"); + FILE *segmentation_f = boost::nowide::fopen(debug_out_path("segmentation.obj").c_str(), "w"); FILE *malform_f = boost::nowide::fopen(debug_out_path("malformations.obj").c_str(), "w"); #endif - StabilityAccumulators stability_accs; - LayerLinesDistancer prev_layer_lines { { } }; + Issues issues { }; - std::vector checked_lines; - VoxelGrid supports_presence_grid { po, params.min_distance_between_support_points }; + std::vector islands_graph; + std::vector layer_lines; + float flow_width = get_flow_width(po->layers()[po->layer_count() - 1]->regions()[0], erExternalPerimeter); + PixelGrid prev_layer_grid(po, flow_width); // PREPARE BASE LAYER - float max_flow_width = 0.0f; const Layer *layer = po->layers()[0]; - float base_print_z = layer->print_z; for (const LayerRegion *layer_region : layer->regions()) { for (const ExtrusionEntity *ex_entity : layer_region->perimeters.entities) { for (const ExtrusionEntity *perimeter : static_cast(ex_entity)->entities) { - const float flow_width = get_flow_width(layer_region, perimeter->role()); - max_flow_width = std::max(flow_width, max_flow_width); - const float mm3_per_mm = float(perimeter->min_mm3_per_mm()); - int id = stability_accs.create_accumulator(); - StabilityAccumulator &acc = stability_accs.access(id); Points points { }; perimeter->collect_points(points); for (int point_idx = 0; point_idx < int(points.size() - 1); ++point_idx) { Vec2f start = unscaled(points[point_idx]).cast(); Vec2f next = unscaled(points[point_idx + 1]).cast(); - ExtrusionLine line { start, next, perimeter->role() == erExternalPerimeter }; - line.stability_accumulator_id = id; - float line_sticking_force = line.len * flow_width * params.base_adhesion; - acc.add_base_extrusion(line, line_sticking_force, base_print_z, mm3_per_mm); - checked_lines.push_back(line); + ExtrusionLine line { start, next, perimeter }; + layer_lines.push_back(line); } if (perimeter->is_loop()) { Vec2f start = unscaled(points[points.size() - 1]).cast(); Vec2f next = unscaled(points[0]).cast(); - ExtrusionLine line { start, next, perimeter->role() == erExternalPerimeter }; - line.stability_accumulator_id = id; - float line_sticking_force = line.len * flow_width * params.base_adhesion; - acc.add_base_extrusion(line, line_sticking_force, base_print_z, mm3_per_mm); - checked_lines.push_back(line); + ExtrusionLine line { start, next, perimeter }; + layer_lines.push_back(line); } } // perimeter } // ex_entity for (const ExtrusionEntity *ex_entity : layer_region->fills.entities) { for (const ExtrusionEntity *fill : static_cast(ex_entity)->entities) { - const float flow_width = get_flow_width(layer_region, fill->role()); - max_flow_width = std::max(flow_width, max_flow_width); - const float mm3_per_mm = float(fill->min_mm3_per_mm()); - int id = stability_accs.create_accumulator(); - StabilityAccumulator &acc = stability_accs.access(id); Points points { }; fill->collect_points(points); for (int point_idx = 0; point_idx < int(points.size() - 1); ++point_idx) { Vec2f start = unscaled(points[point_idx]).cast(); Vec2f next = unscaled(points[point_idx + 1]).cast(); - ExtrusionLine line { start, next, false }; - line.stability_accumulator_id = id; - float line_sticking_force = line.len * flow_width * params.base_adhesion; - acc.add_base_extrusion(line, line_sticking_force, base_print_z, mm3_per_mm); - checked_lines.push_back(line); + ExtrusionLine line { start, next, fill }; + layer_lines.push_back(line); } } // fill } // ex_entity } // region - //MERGE BASE LAYER STABILITY ACCS - prev_layer_lines = LayerLinesDistancer { std::move(checked_lines) }; - for (const ExtrusionLine &l : prev_layer_lines.get_lines()) { - size_t nearest_line_idx; - Vec2f nearest_pt; - Vec2f line_dir = (l.b - l.a).normalized(); - Vec2f site_search_location = l.a + Vec2f(line_dir.y(), -line_dir.x()) * max_flow_width; - float dist = prev_layer_lines.signed_distance_from_lines(site_search_location, nearest_line_idx, nearest_pt); - if (std::abs(dist) < max_flow_width) { - size_t other_line_acc_id = prev_layer_lines.get_line(nearest_line_idx).stability_accumulator_id; - stability_accs.merge_accumulators(other_line_acc_id, l.stability_accumulator_id); + auto [layer_islands, layer_grid] = reckon_islands(layer, true, 0, prev_layer_grid, + layer_lines, params); + islands_graph.push_back(std::move(layer_islands)); +#ifdef DEBUG_FILES + for (size_t x = 0; x < size_t(layer_grid.get_pixel_count().x()); ++x) { + for (size_t y = 0; y < size_t(layer_grid.get_pixel_count().y()); ++y) { + Vec2i coords = Vec2i(x, y); + size_t island_idx = layer_grid.get_pixel(coords); + if (layer_grid.get_pixel(coords) != NULL_ISLAND) { + Vec2f pos = layer_grid.get_pixel_center(coords); + size_t pseudornd = ((island_idx + 127) * 33331 + 6907) % 23; + Vec3f color = value_to_rgbf(0.0f, float(23), float(pseudornd)); + fprintf(segmentation_f, "v %f %f %f %f %f %f\n", pos[0], + pos[1], layer->print_z, color[0], color[1], color[2]); + } } } - -#ifdef DEBUG_FILES - for (const auto &line : prev_layer_lines.get_lines()) { - Vec3f color = stability_accs.get_accumulator_color(line.stability_accumulator_id); - fprintf(debug_acc, "v %f %f %f %f %f %f\n", line.b[0], - line.b[1], base_print_z, color[0], color[1], color[2]); + for (const auto &line : layer_lines) { + if (line.malformation > 0.0f) { + Vec3f color = value_to_rgbf(0, 1.0f, line.malformation); + fprintf(malform_f, "v %f %f %f %f %f %f\n", line.b[0], + line.b[1], layer->print_z, color[0], color[1], color[2]); + } } - - stability_accs.log_accumulators(); #endif + LinesDistancer external_lines(layer_lines); + layer_lines.clear(); + prev_layer_grid = layer_grid; - //CHECK STABILITY OF ALL LAYERS for (size_t layer_idx = 1; layer_idx < po->layer_count(); ++layer_idx) { const Layer *layer = po->layers()[layer_idx]; - checked_lines = std::vector { }; - std::vector> fill_points; - float max_fill_flow_width = 0.0f; - - float print_z = layer->print_z; for (const LayerRegion *layer_region : layer->regions()) { for (const ExtrusionEntity *ex_entity : layer_region->perimeters.entities) { for (const ExtrusionEntity *perimeter : static_cast(ex_entity)->entities) { - check_extrusion_entity_stability(perimeter, stability_accs, issues, checked_lines, print_z, - layer_region, - prev_layer_lines, params); + check_extrusion_entity_stability(perimeter, layer_lines, layer->print_z, layer_region, + external_lines, issues, params); } // perimeter } // ex_entity for (const ExtrusionEntity *ex_entity : layer_region->fills.entities) { for (const ExtrusionEntity *fill : static_cast(ex_entity)->entities) { if (fill->role() == ExtrusionRole::erGapFill || fill->role() == ExtrusionRole::erBridgeInfill) { - check_extrusion_entity_stability(fill, stability_accs, issues, checked_lines, print_z, - layer_region, - prev_layer_lines, params); + check_extrusion_entity_stability(fill, layer_lines, layer->print_z, layer_region, + external_lines, issues, params); } else { - const float flow_width = get_flow_width(layer_region, fill->role()); - max_fill_flow_width = std::max(max_fill_flow_width, flow_width); - Vec2f start = unscaled(fill->first_point()).cast(); - size_t nearest_line_idx; - Vec2f nearest_pt; - float dist = prev_layer_lines.signed_distance_from_lines(start, nearest_line_idx, nearest_pt); - if (dist < flow_width) { - size_t acc_id = prev_layer_lines.get_line(nearest_line_idx).stability_accumulator_id; - StabilityAccumulator &acc = stability_accs.access(acc_id); - Points points { }; - const float mm3_per_mm = float(fill->min_mm3_per_mm()); - fill->collect_points(points); - for (int point_idx = 0; point_idx < int(points.size() - 1); ++point_idx) { - Vec2f start = unscaled(points[point_idx]).cast(); - Vec2f next = unscaled(points[point_idx + 1]).cast(); - ExtrusionLine line { start, next, false }; - line.stability_accumulator_id = acc_id; - acc.add_extrusion(line, print_z, mm3_per_mm); - } - fill_points.emplace_back(start, acc_id); - } else { - BOOST_LOG_TRIVIAL(debug) - << "SSG: ERROR: seem that infill starts in the air? on printz: " << print_z; + Points points { }; + fill->collect_points(points); + for (int point_idx = 0; point_idx < int(points.size() - 1); ++point_idx) { + Vec2f start = unscaled(points[point_idx]).cast(); + Vec2f next = unscaled(points[point_idx + 1]).cast(); + ExtrusionLine line { start, next, fill }; + layer_lines.push_back(line); } } } // fill } // ex_entity } // region - prev_layer_lines = LayerLinesDistancer { std::move(checked_lines) }; - - for (const std::pair &fill_point : fill_points) { - size_t nearest_line_idx; - Vec2f nearest_pt; - float dist = prev_layer_lines.signed_distance_from_lines(fill_point.first, nearest_line_idx, nearest_pt); - if (dist < max_fill_flow_width) { - size_t other_line_acc_id = prev_layer_lines.get_line(nearest_line_idx).stability_accumulator_id; - stability_accs.merge_accumulators(other_line_acc_id, fill_point.second); - } else { - BOOST_LOG_TRIVIAL(debug) - << "SSG: ERROR: seem that infill starts in the air? on printz: " << print_z; - } - } - - float flow_width = get_flow_width(layer->regions()[0], erExternalPerimeter); - - check_layer_global_stability(stability_accs, - supports_presence_grid, - issues, - flow_width, - prev_layer_lines.get_lines(), - print_z, - params); - - reckon_thin_islands(stability_accs, flow_width, flow_width*layer->height, print_z, prev_layer_lines, params); + auto [layer_islands, layer_grid] = reckon_islands(layer, true, 0, prev_layer_grid, + layer_lines, params); + islands_graph.push_back(std::move(layer_islands)); #ifdef DEBUG_FILES - for (const auto &line : prev_layer_lines.get_lines()) { - Vec3f color = value_to_rgbf(0, 1.0f, line.malformation); - fprintf(malform_f, "v %f %f %f %f %f %f\n", line.b[0], - line.b[1], print_z, color[0], color[1], color[2]); + for (size_t x = 0; x < size_t(layer_grid.get_pixel_count().x()); ++x) { + for (size_t y = 0; y < size_t(layer_grid.get_pixel_count().y()); ++y) { + Vec2i coords = Vec2i(x, y); + size_t island_idx = layer_grid.get_pixel(coords); + if (layer_grid.get_pixel(coords) != NULL_ISLAND) { + Vec2f pos = layer_grid.get_pixel_center(coords); + size_t pseudornd = ((island_idx + 127) * 33331 + 6907) % 23; + Vec3f color = value_to_rgbf(0.0f, float(23), float(pseudornd)); + fprintf(segmentation_f, "v %f %f %f %f %f %f\n", pos[0], + pos[1], layer->print_z, color[0], color[1], color[2]); + } + } } - for (const auto &line : prev_layer_lines.get_lines()) { - Vec3f color = stability_accs.get_accumulator_color(line.stability_accumulator_id); - fprintf(debug_acc, "v %f %f %f %f %f %f\n", line.b[0], - line.b[1], print_z, color[0], color[1], color[2]); + for (const auto &line : layer_lines) { + if (line.malformation > 0.0f) { + Vec3f color = value_to_rgbf(0, 1.0f, line.malformation); + fprintf(malform_f, "v %f %f %f %f %f %f\n", line.b[0], + line.b[1], layer->print_z, color[0], color[1], color[2]); + } } - stability_accs.log_accumulators(); #endif + external_lines = LinesDistancer(layer_lines); + layer_lines.clear(); + prev_layer_grid = layer_grid; } #ifdef DEBUG_FILES - fclose(debug_acc); + fclose(segmentation_f); fclose(malform_f); #endif - std::cout << " SUPP: " << issues.supports_nedded.size() << std::endl; - return issues; + return {issues, islands_graph}; } #ifdef DEBUG_FILES @@ -907,46 +1066,35 @@ void debug_export(Issues issues, std::string file_name) { return; } - for (size_t i = 0; i < issues.supports_nedded.size(); ++i) { - fprintf(fp, "v %f %f %f %f %f %f\n", issues.supports_nedded[i].position(0), - issues.supports_nedded[i].position(1), - issues.supports_nedded[i].position(2), 1.0, 0.0, 1.0); + for (size_t i = 0; i < issues.support_points.size(); ++i) { + fprintf(fp, "v %f %f %f %f %f %f\n", issues.support_points[i].position(0), + issues.support_points[i].position(1), + issues.support_points[i].position(2), 1.0, 0.0, 1.0); } fclose(fp); } - { - FILE *fp = boost::nowide::fopen(debug_out_path((file_name + "_curling.obj").c_str()).c_str(), "w"); - if (fp == nullptr) { - BOOST_LOG_TRIVIAL(error) - << "Debug files: Couldn't open " << file_name << " for writing"; - return; - } - - for (size_t i = 0; i < issues.curling_up.size(); ++i) { - fprintf(fp, "v %f %f %f %f %f %f\n", issues.curling_up[i].position(0), - issues.curling_up[i].position(1), - issues.curling_up[i].position(2), 0.0, 1.0, 0.0); - } - fclose(fp); - } } #endif std::vector quick_search(const PrintObject *po, const Params ¶ms) { - check_object_stability(po, params); return {}; } -Issues -full_search(const PrintObject *po, const Params ¶ms) { - auto issues = check_object_stability(po, params); +Issues full_search(const PrintObject *po, const Params ¶ms) { + auto [local_issues, graph] = check_extrusions_and_build_graph(po, params); + Issues global_issues = check_global_stability(graph, params); #ifdef DEBUG_FILES - debug_export(issues, "issues"); + debug_export(local_issues, "local_issues"); + debug_export(global_issues, "global_issues"); #endif - return issues; + global_issues.support_points.insert(global_issues.support_points.end(), + local_issues.support_points.begin(), local_issues.support_points.end()); + + return global_issues; } + } //SupportableIssues End } diff --git a/src/libslic3r/SupportSpotsGeneratorRefactoring.cpp b/src/libslic3r/SupportSpotsGeneratorRefactoring.cpp deleted file mode 100644 index a2275c6372..0000000000 --- a/src/libslic3r/SupportSpotsGeneratorRefactoring.cpp +++ /dev/null @@ -1,972 +0,0 @@ -#include "SupportSpotsGenerator.hpp" - -#include "tbb/parallel_for.h" -#include "tbb/blocked_range.h" -#include "tbb/blocked_range2d.h" -#include "tbb/parallel_reduce.h" -#include -#include -#include -#include - -#include "AABBTreeLines.hpp" -#include "KDTreeIndirect.hpp" -#include "libslic3r/Layer.hpp" -#include "libslic3r/ClipperUtils.hpp" -#include "Geometry/ConvexHull.hpp" - -#define DEBUG_FILES - -#ifdef DEBUG_FILES -#include -#include "libslic3r/Color.hpp" -#endif - -namespace Slic3r { - -class ExtrusionLine -{ -public: - ExtrusionLine() : - a(Vec2f::Zero()), b(Vec2f::Zero()), len(0.0f), origin_entity(nullptr) { - } - ExtrusionLine(const Vec2f &_a, const Vec2f &_b, const ExtrusionEntity *origin_entity) : - a(_a), b(_b), len((_a - _b).norm()), origin_entity(origin_entity) { - } - - float length() { - return (a - b).norm(); - } - - bool is_external_perimeter() const { - assert(origin_entity != nullptr); - return origin_entity->role() == erExternalPerimeter; - } - - Vec2f a; - Vec2f b; - float len; - const ExtrusionEntity *origin_entity; - - bool support_point_generated = false; - float malformation = 0.0f; - - static const constexpr int Dim = 2; - using Scalar = Vec2f::Scalar; -}; - -auto get_a(ExtrusionLine &&l) { - return l.a; -} -auto get_b(ExtrusionLine &&l) { - return l.b; -} - -namespace SupportSpotsGenerator { - -SupportPoint::SupportPoint(const Vec3f &position, float force, const Vec3f &direction) : - position(position), force(force), direction(direction) { -} - -class LinesDistancer { -private: - std::vector lines; - AABBTreeIndirect::Tree<2, float> tree; - -public: - explicit LinesDistancer(std::vector &lines) : - lines(lines) { - tree = AABBTreeLines::build_aabb_tree_over_indexed_lines(this->lines); - } - - // negative sign means inside - float signed_distance_from_lines(const Vec2f &point, size_t &nearest_line_index_out, - Vec2f &nearest_point_out) const { - auto distance = AABBTreeLines::squared_distance_to_indexed_lines(lines, tree, point, nearest_line_index_out, - nearest_point_out); - if (distance < 0) - return std::numeric_limits::infinity(); - - distance = sqrt(distance); - const ExtrusionLine &line = lines[nearest_line_index_out]; - Vec2f v1 = line.b - line.a; - Vec2f v2 = point - line.a; - if ((v1.x() * v2.y()) - (v1.y() * v2.x()) > 0.0) { - distance *= -1; - } - return distance; - } - - const ExtrusionLine& get_line(size_t line_idx) const { - return lines[line_idx]; - } - - const std::vector& get_lines() const { - return lines; - } -}; - -static const size_t NULL_ISLAND = std::numeric_limits::max(); - -class PixelGrid { - Vec2f pixel_size; - Vec2f origin; - Vec2f size; - Vec2i pixel_count; - - std::vector pixels { }; - -public: - PixelGrid(const PrintObject *po, float resolution) { - pixel_size = Vec2f(resolution, resolution); - - Vec2crd size_half = po->size().head<2>().cwiseQuotient(Vec2crd(2, 2)) + Vec2crd::Ones(); - Vec2f min = unscale(Vec2crd(-size_half.x(), -size_half.y())).cast(); - Vec2f max = unscale(Vec2crd(size_half.x(), size_half.y())).cast(); - - origin = min; - size = max - min; - pixel_count = size.cwiseQuotient(pixel_size).cast() + Vec2i::Ones(); - - pixels.resize(pixel_count.y() * pixel_count.x()); - clear(); - } - - void distribute_edge(const Vec2f &p1, const Vec2f &p2, size_t value) { - Vec2f dir = (p2 - p1); - float length = dir.norm(); - if (length < 0.1) { - return; - } - float step_size = this->pixel_size.x() / 2.0; - - float distributed_length = 0; - while (distributed_length < length) { - float next_len = std::min(length, distributed_length + step_size); - Vec2f location = p1 + ((next_len / length) * dir); - this->access_pixel(location) = value; - - distributed_length = next_len; - } - } - - void clear() { - for (size_t &val : pixels) { - val = NULL_ISLAND; - } - } - - float pixel_area() const { - return this->pixel_size.x() * this->pixel_size.y(); - } - - size_t get_pixel(const Vec2i &coords) const { - return pixels[this->to_pixel_index(coords)]; - } - - Vec2i get_pixel_count() { - return pixel_count; - } - - Vec2f get_pixel_center(const Vec2i &coords) const { - return origin + coords.cast().cwiseProduct(this->pixel_size) - + this->pixel_size.cwiseQuotient(Vec2f(2.0f, 2.0f)); - } - -private: - Vec2i to_pixel_coords(const Vec2f &position) const { - Vec2i pixel_coords = (position - this->origin).cwiseQuotient(this->pixel_size).cast(); - return pixel_coords; - } - - size_t to_pixel_index(const Vec2i &pixel_coords) const { - assert(pixel_coords.x() >= 0); - assert(pixel_coords.x() < pixel_count.x()); - assert(pixel_coords.y() >= 0); - assert(pixel_coords.y() < pixel_count.y()); - - return pixel_coords.y() * pixel_count.x() + pixel_coords.x(); - } - - size_t& access_pixel(const Vec2f &position) { - return pixels[this->to_pixel_index(this->to_pixel_coords(position))]; - } -}; - -struct SupportGridFilter { -private: - Vec3f cell_size; - Vec3f origin; - Vec3f size; - Vec3i cell_count; - - std::unordered_set taken_cells { }; - -public: - SupportGridFilter(const PrintObject *po, float voxel_size) { - cell_size = Vec3f(voxel_size, voxel_size, voxel_size); - - Vec2crd size_half = po->size().head<2>().cwiseQuotient(Vec2crd(2, 2)) + Vec2crd::Ones(); - Vec3f min = unscale(Vec3crd(-size_half.x(), -size_half.y(), 0)).cast() - cell_size; - Vec3f max = unscale(Vec3crd(size_half.x(), size_half.y(), po->height())).cast() + cell_size; - - origin = min; - size = max - min; - cell_count = size.cwiseQuotient(cell_size).cast() + Vec3i::Ones(); - } - - Vec3i to_cell_coords(const Vec3f &position) const { - Vec3i cell_coords = (position - this->origin).cwiseQuotient(this->cell_size).cast(); - return cell_coords; - } - - size_t to_cell_index(const Vec3i &cell_coords) const { - assert(cell_coords.x() >= 0); - assert(cell_coords.x() < cell_count.x()); - assert(cell_coords.y() >= 0); - assert(cell_coords.y() < cell_count.y()); - assert(cell_coords.z() >= 0); - assert(cell_coords.z() < cell_count.z()); - - return cell_coords.z() * cell_count.x() * cell_count.y() - + cell_coords.y() * cell_count.x() - + cell_coords.x(); - } - - Vec3f get_cell_center(const Vec3i &cell_coords) const { - return origin + cell_coords.cast().cwiseProduct(this->cell_size) - + this->cell_size.cwiseQuotient(Vec3f(2.0f, 2.0f, 2.0)); - } - - void take_position(const Vec3f &position) { - taken_cells.insert(to_cell_index(to_cell_coords(position))); - } - - bool position_taken(const Vec3f &position) const { - return taken_cells.find(to_cell_index(to_cell_coords(position))) != taken_cells.end(); - } - -}; - -struct Island { - std::unordered_map islands_under_with_connection_area; - std::vector pivot_points; - float volume; - Vec3f volume_centroid_accumulator; - float sticking_force; // for support points present on this layer (or bed extrusions) - Vec3f sticking_centroid_accumulator; -}; - -struct LayerIslands { - std::vector islands; -}; - -float get_flow_width(const LayerRegion *region, ExtrusionRole role) { - switch (role) { - case ExtrusionRole::erBridgeInfill: - return region->flow(FlowRole::frExternalPerimeter).width(); - case ExtrusionRole::erExternalPerimeter: - return region->flow(FlowRole::frExternalPerimeter).width(); - case ExtrusionRole::erGapFill: - return region->flow(FlowRole::frInfill).width(); - case ExtrusionRole::erPerimeter: - return region->flow(FlowRole::frPerimeter).width(); - case ExtrusionRole::erSolidInfill: - return region->flow(FlowRole::frSolidInfill).width(); - case ExtrusionRole::erInternalInfill: - return region->flow(FlowRole::frInfill).width(); - case ExtrusionRole::erTopSolidInfill: - return region->flow(FlowRole::frTopSolidInfill).width(); - default: - return region->flow(FlowRole::frPerimeter).width(); - } -} - -// Accumulator of current extruion path properties -// It remembers unsuported distance and maximum accumulated curvature over that distance. -// Used to determine local stability issues (too long bridges, extrusion curves into air) -struct ExtrusionPropertiesAccumulator { - float distance = 0; //accumulated distance - float curvature = 0; //accumulated signed ccw angles - float max_curvature = 0; //max absolute accumulated value - - void add_distance(float dist) { - distance += dist; - } - - void add_angle(float ccw_angle) { - curvature += ccw_angle; - max_curvature = std::max(max_curvature, std::abs(curvature)); - } - - void reset() { - distance = 0; - curvature = 0; - max_curvature = 0; - } -}; - -void check_extrusion_entity_stability(const ExtrusionEntity *entity, - std::vector &checked_lines_out, - float print_z, - const LayerRegion *layer_region, - const LinesDistancer &prev_layer_lines, - Issues &issues, - const Params ¶ms) { - - if (entity->is_collection()) { - for (const auto *e : static_cast(entity)->entities) { - check_extrusion_entity_stability(e, checked_lines_out, print_z, layer_region, prev_layer_lines, - issues, params); - } - } else { //single extrusion path, with possible varying parameters - const auto to_vec3f = [print_z](const Vec2f &point) { - return Vec3f(point.x(), point.y(), print_z); - }; - Points points { }; - entity->collect_points(points); - std::vector lines; - lines.reserve(points.size() * 1.5); - lines.emplace_back(unscaled(points[0]).cast(), unscaled(points[0]).cast(), entity); - for (int point_idx = 0; point_idx < int(points.size() - 1); ++point_idx) { - Vec2f start = unscaled(points[point_idx]).cast(); - Vec2f next = unscaled(points[point_idx + 1]).cast(); - Vec2f v = next - start; // vector from next to current - float dist_to_next = v.norm(); - v.normalize(); - int lines_count = int(std::ceil(dist_to_next / params.bridge_distance)); - float step_size = dist_to_next / lines_count; - for (int i = 0; i < lines_count; ++i) { - Vec2f a(start + v * (i * step_size)); - Vec2f b(start + v * ((i + 1) * step_size)); - lines.emplace_back(a, b, entity); - } - } - - ExtrusionPropertiesAccumulator bridging_acc { }; - ExtrusionPropertiesAccumulator malformation_acc { }; - bridging_acc.add_distance(params.bridge_distance + 1.0f); // Initialise unsupported distance with larger than tolerable distance -> - // -> it prevents extruding perimeter starts and short loops into air. - const float flow_width = get_flow_width(layer_region, entity->role()); - - for (size_t line_idx = 0; line_idx < lines.size(); ++line_idx) { - ExtrusionLine ¤t_line = lines[line_idx]; - float curr_angle = 0; - if (line_idx + 1 < lines.size()) { - const Vec2f v1 = current_line.b - current_line.a; - const Vec2f v2 = lines[line_idx + 1].b - lines[line_idx + 1].a; - curr_angle = angle(v1, v2); - } - bridging_acc.add_angle(curr_angle); - malformation_acc.add_angle(std::max(0.0f, curr_angle)); - - size_t nearest_line_idx; - Vec2f nearest_point; - float dist_from_prev_layer = prev_layer_lines.signed_distance_from_lines(current_line.b, nearest_line_idx, - nearest_point); - - if (fabs(dist_from_prev_layer) < flow_width) { - bridging_acc.reset(); - } else { - bridging_acc.add_distance(current_line.len); - if (bridging_acc.distance // if unsupported distance is larger than bridge distance linearly decreased by curvature, enforce supports. - > params.bridge_distance - / (1.0f + (bridging_acc.max_curvature - * params.bridge_distance_decrease_by_curvature_factor / PI))) { - issues.support_points.emplace_back(to_vec3f(current_line.b), 0.0f, Vec3f(0.f, 0.0f, -1.0f)); - current_line.support_point_generated = true; - bridging_acc.reset(); - } - } - - //malformation - if (fabs(dist_from_prev_layer) < flow_width * 2.0f) { - const ExtrusionLine &nearest_line = prev_layer_lines.get_line(nearest_line_idx); - current_line.malformation += 0.9 * nearest_line.malformation; - } - if (dist_from_prev_layer > flow_width * 0.3) { - malformation_acc.add_distance(current_line.len); - current_line.malformation += 0.15 - * (0.8 + 0.2 * malformation_acc.max_curvature / (1.0f + 0.5f * malformation_acc.distance)); - } else { - malformation_acc.reset(); - } - } - checked_lines_out.insert(checked_lines_out.end(), lines.begin(), lines.end()); - } -} - -std::tuple> reckon_islands( - const Layer *layer, bool first_layer, - size_t prev_layer_islands_count, - const PixelGrid &prev_layer_grid, - const std::vector &layer_lines, - const Params ¶ms) { - - //extract extrusions (connected paths from multiple lines) from the layer_lines. belonging to single polyline is determined by origin_entity ptr. - // result is a vector of [start, end) index pairs into the layer_lines vector - std::vector> extrusions; //start and end idx (one beyond last extrusion) [start,end) - const ExtrusionEntity *current_ex = nullptr; - for (size_t lidx = 0; lidx < layer_lines.size(); ++lidx) { - const ExtrusionLine &line = layer_lines[lidx]; - if (line.origin_entity == current_ex) { - extrusions.back().second = lidx + 1; - } else { - extrusions.emplace_back(lidx, lidx + 1); - current_ex = line.origin_entity; - } - } - - std::vector islands; // these search trees will be used to determine to which island does the extrusion begin - std::vector> island_extrusions; //final assigment of each extrusion to an island - // initliaze the search from external perimeters - at the beginning, there is island candidate for each external perimeter. - // some of them will disappear (e.g. holes) - for (size_t e = 0; e < extrusions.size(); ++e) { - if (layer_lines[extrusions[e].first].is_external_perimeter()) { - std::vector copy(extrusions[e].second - extrusions[e].first); - for (size_t ex_line_idx = extrusions[e].first; ex_line_idx < extrusions[e].second; ++ex_line_idx) { - copy[ex_line_idx - extrusions[e].first] = layer_lines[ex_line_idx]; - } - islands.emplace_back(copy); - island_extrusions.push_back( { e }); - } - } - // backup code if islands not found - this can currently happen, as external perimeters may be also pure overhang perimeters, and there is no - // way to distinguish external extrusions with total certainty. - // If that happens, just make the first extrusion into island - it may be wrong, but it won't crash. - if (islands.empty() && !extrusions.empty()) { - std::vector copy(extrusions[0].second - extrusions[0].first); - for (size_t ex_line_idx = extrusions[0].first; ex_line_idx < extrusions[0].second; ++ex_line_idx) { - copy[ex_line_idx - extrusions[0].first] = layer_lines[ex_line_idx]; - } - islands.emplace_back(copy); - island_extrusions.push_back( { 0 }); - } - - // assign non external extrusions to islands - for (size_t e = 0; e < extrusions.size(); ++e) { - if (!layer_lines[extrusions[e].first].is_external_perimeter()) { - bool island_assigned = false; - for (size_t i = 0; i < islands.size(); ++i) { - size_t _idx; - Vec2f _pt; - if (islands[i].signed_distance_from_lines(layer_lines[extrusions[e].first].a, _idx, _pt) < 0) { - island_extrusions[i].push_back(e); - island_assigned = true; - break; - } - } - if (!island_assigned) { // If extrusion is not assigned for some reason, push it into the first island. As with the previous backup code, - // it may be wrong, but it won't crash - island_extrusions[0].push_back(e); - } - } - } - // merge islands which are embedded within each other (mainly holes) - for (size_t i = 0; i < islands.size(); ++i) { - if (islands[i].get_lines().empty()) { - continue; - } - for (size_t j = 0; j < islands.size(); ++j) { - if (islands[j].get_lines().empty() || i == j) { - continue; - } - size_t _idx; - Vec2f _pt; - if (islands[i].signed_distance_from_lines(islands[j].get_line(0).a, _idx, _pt) < 0) { - island_extrusions[i].insert(island_extrusions[i].end(), island_extrusions[j].begin(), - island_extrusions[j].end()); - island_extrusions[j].clear(); - } - } - } - - float flow_width = get_flow_width(layer->regions()[0], erExternalPerimeter); - // after filtering the layer lines into islands, build the result LayerIslands structure. - LayerIslands result { }; - std::vector line_to_island_mapping(layer_lines.size(), NULL_ISLAND); - for (const std::vector &island_ex : island_extrusions) { - if (island_ex.empty()) { - continue; - } - - Island island { }; - for (size_t extrusion_idx : island_ex) { - for (size_t lidx = extrusions[extrusion_idx].first; lidx < extrusions[extrusion_idx].second; ++lidx) { - line_to_island_mapping[lidx] = result.islands.size(); - const ExtrusionLine &line = layer_lines[lidx]; - float volume = line.origin_entity->min_mm3_per_mm() * line.len; - island.volume += volume; - island.volume_centroid_accumulator += to_3d(Vec2f((line.a + line.b) / 2.0f), float(layer->print_z)) - * volume; - - if (first_layer) { - float sticking_force = line.len * flow_width * params.base_adhesion; - island.sticking_force += sticking_force; - island.sticking_centroid_accumulator += sticking_force - * to_3d(Vec2f((line.a + line.b) / 2.0f), float(layer->print_z)); - if (line.is_external_perimeter()) { - island.pivot_points.push_back(to_3d(Vec2f(line.b), float(layer->print_z))); - } - } else if (layer_lines[lidx].support_point_generated) { - float support_interface_area = params.support_points_interface_radius - * params.support_points_interface_radius - * float(PI); - float sticking_force = support_interface_area * params.support_adhesion; - island.sticking_force += sticking_force; - island.sticking_centroid_accumulator += sticking_force - * to_3d(Vec2f(line.b), float(layer->print_z)); - island.pivot_points.push_back(to_3d(Vec2f(line.b), float(layer->print_z))); - } - } - } - result.islands.push_back(island); - } - - //LayerIslands structure built. Now determine connections and their areas to the previous layer using raterization. - PixelGrid current_layer_grid = prev_layer_grid; - current_layer_grid.clear(); - // build index image of current layer - tbb::parallel_for(tbb::blocked_range(0, layer_lines.size()), - [&layer_lines, ¤t_layer_grid, &line_to_island_mapping]( - tbb::blocked_range r) { - for (size_t i = r.begin(); i < r.end(); ++i) { - size_t island = line_to_island_mapping[i]; - const ExtrusionLine &line = layer_lines[i]; - current_layer_grid.distribute_edge(line.a, line.b, island); - } - }); - - //compare the image of previous layer with the current layer. For each pair of overlapping valid pixels, add pixel area to the respecitve island connection - for (size_t x = 0; x < size_t(current_layer_grid.get_pixel_count().x()); ++x) { - for (size_t y = 0; y < size_t(current_layer_grid.get_pixel_count().y()); ++y) { - Vec2i coords = Vec2i(x, y); - if (current_layer_grid.get_pixel(coords) != NULL_ISLAND - && prev_layer_grid.get_pixel(coords) != NULL_ISLAND) { - result.islands[current_layer_grid.get_pixel(coords)].islands_under_with_connection_area[prev_layer_grid.get_pixel( - coords)] += - current_layer_grid.pixel_area(); - } - } - } - return {result, current_layer_grid, line_to_island_mapping}; -} - -void check_global_stability( - float print_z, - std::vector &islands_graph, - SupportGridFilter &supports_presence_grid, - const std::vector &layer_lines, - const std::vector &line_to_island_mapping, - Issues &issues, - const Params ¶ms - ) { - // vector of islands, where each contains vector of line indices (to layer_lines vector) - // basically reverse of line_to_island_mapping - std::vector> islands_lines(islands_graph.back().islands.size()); - for (size_t lidx = 0; lidx < layer_lines.size(); ++lidx) { - if (layer_lines[lidx].origin_entity->role() == erExternalPerimeter) { - islands_lines[line_to_island_mapping[lidx]].push_back(lidx); - } - } - - using Accumulator = Island; - - // islands_graph.back() refers to the top most (current) layer - for (size_t island_idx = 0; island_idx < islands_graph.back().islands.size(); ++island_idx) { - Island &island = islands_graph.back().islands[island_idx]; - - std::vector island_external_lines; - for (size_t lidx : islands_lines[island_idx]) { - island_external_lines.push_back(layer_lines[lidx]); - } - LinesDistancer island_lines_dist(island_external_lines); - Accumulator acc = island; // in acc, we accumulate the mass and other properties of the object part as we traverse the islands down to bed - // There is one object part for each island at the top most layer, and each one is computed individually - - // Some of the calculations will be done multiple times - int layer_idx = islands_graph.size() - 1; - // traverse the islands graph down, and for each connection area, calculate if it holds or breaks - while (acc.islands_under_with_connection_area.size() > 0) { - //test for break between layer_idx and layer_idx -1; - LayerIslands below = islands_graph[layer_idx - 1]; // must exist, see while condition - layer_idx--; - // initialize variables that we will accumulate over all islands, which are connected to the current object part - std::vector pivot_points; - Vec2f sticking_centroid; - float connection_area = 0; - for (const auto &pair : acc.islands_under_with_connection_area) { - const Island &below_i = below.islands[pair.first]; - Vec2f centroid = (below_i.volume_centroid_accumulator / below_i.volume).head<2>(); // centroid of the island 'below_i'; TODO it should be centroid of the connection area - pivot_points.push_back(centroid); // for object parts, we also consider breaking pivots in the centroids of the islands - sticking_centroid += centroid * pair.second; // pair.second is connection area in mm^2 - connection_area += pair.second; - } - sticking_centroid /= connection_area; //normalize to get final sticking centroid - for (const Vec3f &p_point : acc.pivot_points) { - pivot_points.push_back(p_point.head<2>()); - } - // Now we have accumulated pivot points, connection area and sticking centroid of the whole layer to the current object part - - // create KD tree over current pivot points - auto coord_fn = [&pivot_points](size_t idx, size_t dim) { - return pivot_points[idx][dim]; - }; - KDTreeIndirect<2, float, decltype(coord_fn)> pivot_points_tree(coord_fn, pivot_points.size()); - - // iterate over extrusions at top layer island, check each for stability - for (const ExtrusionLine &line : island_external_lines) { - Vec2f line_dir = (line.b - line.a).normalized(); - Vec2f pivot_site_search_point = line.b + line_dir * 300.0f; - size_t pivot_idx = find_closest_point(pivot_points_tree, pivot_site_search_point); - const Vec2f &pivot = pivot_points[pivot_idx]; - - float sticking_arm = (pivot - sticking_centroid).norm(); - float sticking_torque = sticking_arm * connection_area * params.tensile_strength; // For breakage in between layers, we compute with tensile strength, not bed adhesion - - float mass = acc.volume * params.filament_density; - const Vec3f &mass_centorid = acc.volume_centroid_accumulator / acc.volume; - float weight = mass * params.gravity_constant; - float weight_arm = (pivot - mass_centorid.head<2>()).norm(); - float weight_torque = weight_arm * weight; - - float bed_movement_arm = mass_centorid.z(); - float bed_movement_force = params.max_acceleration * mass; - float bed_movement_torque = bed_movement_force * bed_movement_arm; - - Vec3f extruder_pressure_direction = to_3d(line_dir, 0.0f); - extruder_pressure_direction.z() = -0.2 - line.malformation * 0.5; - extruder_pressure_direction.normalize(); - float conflict_torque_arm = (to_3d(Vec2f(pivot - line.b), print_z).cross( - extruder_pressure_direction)).norm(); - float extruder_conflict_force = params.tolerable_extruder_conflict_force + - std::min(line.malformation, 1.0f) * params.malformations_additive_conflict_extruder_force; - float extruder_conflict_torque = extruder_conflict_force * conflict_torque_arm; - - float total_torque = bed_movement_torque + extruder_conflict_torque - weight_torque - sticking_torque; - - if (total_torque > 0) { - Vec2f target_point { }; - size_t _idx { }; - island_lines_dist.signed_distance_from_lines(pivot_site_search_point, _idx, target_point); - if (!supports_presence_grid.position_taken(to_3d(target_point, print_z))) { - float area = params.support_points_interface_radius * params.support_points_interface_radius - * float(PI); - float sticking_force = area * params.support_adhesion; - Vec3f support_point = to_3d(target_point, print_z); - island.pivot_points.push_back(support_point); - island.sticking_force += sticking_force; - island.sticking_centroid_accumulator += sticking_force * support_point; - issues.support_points.emplace_back(support_point, - extruder_conflict_torque - sticking_torque, extruder_pressure_direction); - supports_presence_grid.take_position(to_3d(target_point, print_z)); - } - } -#if 0 - BOOST_LOG_TRIVIAL(debug) - << "SSG: sticking_arm: " << sticking_arm; - BOOST_LOG_TRIVIAL(debug) - << "SSG: sticking_torque: " << sticking_torque; - BOOST_LOG_TRIVIAL(debug) - << "SSG: weight_arm: " << sticking_arm; - BOOST_LOG_TRIVIAL(debug) - << "SSG: weight_torque: " << weight_torque; - BOOST_LOG_TRIVIAL(debug) - << "SSG: bed_movement_arm: " << bed_movement_arm; - BOOST_LOG_TRIVIAL(debug) - << "SSG: bed_movement_torque: " << bed_movement_torque; - BOOST_LOG_TRIVIAL(debug) - << "SSG: conflict_torque_arm: " << conflict_torque_arm; - BOOST_LOG_TRIVIAL(debug) - << "SSG: extruder_conflict_torque: " << extruder_conflict_torque; - BOOST_LOG_TRIVIAL(debug) - << "SSG: total_torque: " << total_torque << " printz: " << print_z; - #endif - } - - std::unordered_map tmp = acc.islands_under_with_connection_area; - acc.islands_under_with_connection_area.clear(); - // finally, add gathered islands to accumulator, and continue down to next layer - for (const auto &pair : tmp) { - const Island &below_i = below.islands[pair.first]; - for (const auto &below_islands : below_i.islands_under_with_connection_area) { - acc.islands_under_with_connection_area[below_islands.first] += below_islands.second; - } - for (const Vec3f &pivot_p : below_i.pivot_points) { - acc.pivot_points.push_back(pivot_p); - } - acc.sticking_centroid_accumulator += below_i.sticking_centroid_accumulator; - acc.sticking_force += below_i.sticking_force; - acc.volume += below_i.volume; - acc.volume_centroid_accumulator += below_i.volume_centroid_accumulator; - } - } - - // We have arrived to the bed level, now check for stability of the object part on the bed - std::vector pivot_points; - for (const Vec3f &p_point : acc.pivot_points) { - pivot_points.push_back(p_point.head<2>()); - } - auto coord_fn = [&pivot_points](size_t idx, size_t dim) { - return pivot_points[idx][dim]; - }; - KDTreeIndirect<2, float, decltype(coord_fn)> pivot_points_tree(coord_fn, pivot_points.size()); - - for (const ExtrusionLine &line : island_external_lines) { - Vec2f line_dir = (line.b - line.a).normalized(); - Vec2f pivot_site_search_point = line.b + line_dir * 300.0f; - size_t pivot_idx = find_closest_point(pivot_points_tree, pivot_site_search_point); - const Vec2f &pivot = pivot_points[pivot_idx]; - - const Vec2f &sticking_centroid = acc.sticking_centroid_accumulator.head<2>() / acc.sticking_force; - float sticking_arm = (pivot - sticking_centroid).norm(); - float sticking_torque = sticking_arm * acc.sticking_force; - - float mass = acc.volume * params.filament_density; - const Vec3f &mass_centorid = acc.volume_centroid_accumulator / acc.volume; - float weight = mass * params.gravity_constant; - float weight_arm = (pivot - mass_centorid.head<2>()).norm(); - float weight_torque = weight_arm * weight; - - float bed_movement_arm = mass_centorid.z(); - float bed_movement_force = params.max_acceleration * mass; - float bed_movement_torque = bed_movement_force * bed_movement_arm; - - Vec3f extruder_pressure_direction = to_3d(line_dir, 0.0f); - extruder_pressure_direction.z() = -0.2 - line.malformation * 0.5; - extruder_pressure_direction.normalize(); - float conflict_torque_arm = (to_3d(Vec2f(pivot - line.b), print_z).cross( - extruder_pressure_direction)).norm(); - float extruder_conflict_force = params.tolerable_extruder_conflict_force + - std::min(line.malformation, 1.0f) * params.malformations_additive_conflict_extruder_force; - float extruder_conflict_torque = extruder_conflict_force * conflict_torque_arm; - - float total_torque = bed_movement_torque + extruder_conflict_torque - weight_torque - sticking_torque; - - if (total_torque > 0) { - Vec2f target_point; - size_t _idx; - island_lines_dist.signed_distance_from_lines(pivot_site_search_point, _idx, target_point); - if (!supports_presence_grid.position_taken(to_3d(target_point, print_z))) { - float area = params.support_points_interface_radius * params.support_points_interface_radius - * float(PI); - float sticking_force = area * params.support_adhesion; - Vec3f support_point = to_3d(target_point, print_z); - island.pivot_points.push_back(support_point); - island.sticking_force += sticking_force; - island.sticking_centroid_accumulator += sticking_force * support_point; - issues.support_points.emplace_back(support_point, - extruder_conflict_torque - sticking_torque, extruder_pressure_direction); - supports_presence_grid.take_position(to_3d(target_point, print_z)); - } - } -#if 0 - BOOST_LOG_TRIVIAL(debug) - << "SSG: sticking_arm: " << sticking_arm; - BOOST_LOG_TRIVIAL(debug) - << "SSG: sticking_torque: " << sticking_torque; - BOOST_LOG_TRIVIAL(debug) - << "SSG: weight_arm: " << sticking_arm; - BOOST_LOG_TRIVIAL(debug) - << "SSG: weight_torque: " << weight_torque; - BOOST_LOG_TRIVIAL(debug) - << "SSG: bed_movement_arm: " << bed_movement_arm; - BOOST_LOG_TRIVIAL(debug) - << "SSG: bed_movement_torque: " << bed_movement_torque; - BOOST_LOG_TRIVIAL(debug) - << "SSG: conflict_torque_arm: " << conflict_torque_arm; - BOOST_LOG_TRIVIAL(debug) - << "SSG: extruder_conflict_torque: " << extruder_conflict_torque; - BOOST_LOG_TRIVIAL(debug) - << "SSG: total_torque: " << total_torque << " printz: " << print_z; - #endif - } - } -} - -Issues check_object_stability(const PrintObject *po, const Params ¶ms) { -#ifdef DEBUG_FILES - FILE *segmentation_f = boost::nowide::fopen(debug_out_path("segmentation.obj").c_str(), "w"); - FILE *malform_f = boost::nowide::fopen(debug_out_path("malformations.obj").c_str(), "w"); -#endif - - Issues issues { }; - std::vector islands_graph; - std::vector layer_lines; - float flow_width = get_flow_width(po->layers()[po->layer_count() - 1]->regions()[0], erExternalPerimeter); - PixelGrid prev_layer_grid(po, flow_width); - SupportGridFilter supports_presence_grid { po, params.min_distance_between_support_points }; - - // PREPARE BASE LAYER - const Layer *layer = po->layers()[0]; - for (const LayerRegion *layer_region : layer->regions()) { - for (const ExtrusionEntity *ex_entity : layer_region->perimeters.entities) { - for (const ExtrusionEntity *perimeter : static_cast(ex_entity)->entities) { - Points points { }; - perimeter->collect_points(points); - for (int point_idx = 0; point_idx < int(points.size() - 1); ++point_idx) { - Vec2f start = unscaled(points[point_idx]).cast(); - Vec2f next = unscaled(points[point_idx + 1]).cast(); - ExtrusionLine line { start, next, perimeter }; - layer_lines.push_back(line); - } - if (perimeter->is_loop()) { - Vec2f start = unscaled(points[points.size() - 1]).cast(); - Vec2f next = unscaled(points[0]).cast(); - ExtrusionLine line { start, next, perimeter }; - layer_lines.push_back(line); - } - } // perimeter - } // ex_entity - for (const ExtrusionEntity *ex_entity : layer_region->fills.entities) { - for (const ExtrusionEntity *fill : static_cast(ex_entity)->entities) { - Points points { }; - fill->collect_points(points); - for (int point_idx = 0; point_idx < int(points.size() - 1); ++point_idx) { - Vec2f start = unscaled(points[point_idx]).cast(); - Vec2f next = unscaled(points[point_idx + 1]).cast(); - ExtrusionLine line { start, next, fill }; - layer_lines.push_back(line); - } - } // fill - } // ex_entity - } // region - - auto [layer_islands, layer_grid, line_to_island_mapping] = reckon_islands(layer, true, 0, prev_layer_grid, - layer_lines, params); - islands_graph.push_back(std::move(layer_islands)); -#ifdef DEBUG_FILES - for (size_t x = 0; x < size_t(layer_grid.get_pixel_count().x()); ++x) { - for (size_t y = 0; y < size_t(layer_grid.get_pixel_count().y()); ++y) { - Vec2i coords = Vec2i(x, y); - size_t island_idx = layer_grid.get_pixel(coords); - if (layer_grid.get_pixel(coords) != NULL_ISLAND) { - Vec2f pos = layer_grid.get_pixel_center(coords); - size_t pseudornd = ((island_idx + 127) * 33331 + 6907) % 23; - Vec3f color = value_to_rgbf(0.0f, float(23), float(pseudornd)); - fprintf(segmentation_f, "v %f %f %f %f %f %f\n", pos[0], - pos[1], layer->print_z, color[0], color[1], color[2]); - } - } - } - for (const auto &line : layer_lines) { - if (line.malformation > 0.0f) { - Vec3f color = value_to_rgbf(0, 1.0f, line.malformation); - fprintf(malform_f, "v %f %f %f %f %f %f\n", line.b[0], - line.b[1], layer->print_z, color[0], color[1], color[2]); - } - } -#endif - LinesDistancer external_lines(layer_lines); - layer_lines.clear(); - prev_layer_grid = layer_grid; - - for (size_t layer_idx = 1; layer_idx < po->layer_count(); ++layer_idx) { - const Layer *layer = po->layers()[layer_idx]; - for (const LayerRegion *layer_region : layer->regions()) { - for (const ExtrusionEntity *ex_entity : layer_region->perimeters.entities) { - for (const ExtrusionEntity *perimeter : static_cast(ex_entity)->entities) { - check_extrusion_entity_stability(perimeter, layer_lines, layer->print_z, layer_region, - external_lines, issues, params); - } // perimeter - } // ex_entity - for (const ExtrusionEntity *ex_entity : layer_region->fills.entities) { - for (const ExtrusionEntity *fill : static_cast(ex_entity)->entities) { - if (fill->role() == ExtrusionRole::erGapFill - || fill->role() == ExtrusionRole::erBridgeInfill) { - check_extrusion_entity_stability(fill, layer_lines, layer->print_z, layer_region, - external_lines, issues, params); - } else { - Points points { }; - fill->collect_points(points); - for (int point_idx = 0; point_idx < int(points.size() - 1); ++point_idx) { - Vec2f start = unscaled(points[point_idx]).cast(); - Vec2f next = unscaled(points[point_idx + 1]).cast(); - ExtrusionLine line { start, next, fill }; - layer_lines.push_back(line); - } - } - } // fill - } // ex_entity - } // region - - auto [layer_islands, layer_grid, line_to_island_mapping] = reckon_islands(layer, true, 0, prev_layer_grid, - layer_lines, params); - islands_graph.push_back(std::move(layer_islands)); - - check_global_stability(layer->print_z, islands_graph, supports_presence_grid, layer_lines, - line_to_island_mapping, issues, params); - -#ifdef DEBUG_FILES - for (size_t x = 0; x < size_t(layer_grid.get_pixel_count().x()); ++x) { - for (size_t y = 0; y < size_t(layer_grid.get_pixel_count().y()); ++y) { - Vec2i coords = Vec2i(x, y); - size_t island_idx = layer_grid.get_pixel(coords); - if (layer_grid.get_pixel(coords) != NULL_ISLAND) { - Vec2f pos = layer_grid.get_pixel_center(coords); - size_t pseudornd = ((island_idx + 127) * 33331 + 6907) % 23; - Vec3f color = value_to_rgbf(0.0f, float(23), float(pseudornd)); - fprintf(segmentation_f, "v %f %f %f %f %f %f\n", pos[0], - pos[1], layer->print_z, color[0], color[1], color[2]); - } - } - } - for (const auto &line : layer_lines) { - if (line.malformation > 0.0f) { - Vec3f color = value_to_rgbf(0, 1.0f, line.malformation); - fprintf(malform_f, "v %f %f %f %f %f %f\n", line.b[0], - line.b[1], layer->print_z, color[0], color[1], color[2]); - } - } -#endif - external_lines = LinesDistancer(layer_lines); - layer_lines.clear(); - prev_layer_grid = layer_grid; - } - -#ifdef DEBUG_FILES - fclose(segmentation_f); - fclose(malform_f); -#endif - - return issues; -} - -#ifdef DEBUG_FILES -void debug_export(Issues issues, std::string file_name) { - Slic3r::CNumericLocalesSetter locales_setter; - { - FILE *fp = boost::nowide::fopen(debug_out_path((file_name + "_supports.obj").c_str()).c_str(), "w"); - if (fp == nullptr) { - BOOST_LOG_TRIVIAL(error) - << "Debug files: Couldn't open " << file_name << " for writing"; - return; - } - - for (size_t i = 0; i < issues.support_points.size(); ++i) { - fprintf(fp, "v %f %f %f %f %f %f\n", issues.support_points[i].position(0), - issues.support_points[i].position(1), - issues.support_points[i].position(2), 1.0, 0.0, 1.0); - } - - fclose(fp); - } -} -#endif - -std::vector quick_search(const PrintObject *po, const Params ¶ms) { - check_object_stability(po, params); - return {}; -} - -Issues -full_search(const PrintObject *po, const Params ¶ms) { - auto issues = check_object_stability(po, params); -#ifdef DEBUG_FILES - debug_export(issues, "issues"); -#endif - return issues; -} - -} //SupportableIssues End -} - diff --git a/src/slic3r/GUI/BonjourDialog.hpp b/src/slic3r/GUI/BonjourDialog.hpp index 8bfc076c44..5bb61131d0 100644 --- a/src/slic3r/GUI/BonjourDialog.hpp +++ b/src/slic3r/GUI/BonjourDialog.hpp @@ -8,6 +8,8 @@ #include #include +#include +#include #include "libslic3r/PrintConfig.hpp" From 8723fb22bb47f26d24d320be30939b0fc53d7774 Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Wed, 20 Jul 2022 17:21:36 +0200 Subject: [PATCH 048/100] add pivot tree into ObjectPart struct --- src/libslic3r/SupportSpotsGenerator.cpp | 31 +++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/src/libslic3r/SupportSpotsGenerator.cpp b/src/libslic3r/SupportSpotsGenerator.cpp index 4512c795b4..61e078d8b6 100644 --- a/src/libslic3r/SupportSpotsGenerator.cpp +++ b/src/libslic3r/SupportSpotsGenerator.cpp @@ -566,13 +566,33 @@ std::tuple reckon_islands( return {result, current_layer_grid}; } -struct ObjectPart { +struct CoordinateFunctor { + const std::vector *coordinates; + CoordinateFunctor(const std::vector *coords) : + coordinates(coords) { + } + CoordinateFunctor() : + coordinates(nullptr) { + } + + const float& operator()(size_t idx, size_t dim) const { + return coordinates->operator [](idx)[dim]; + } +}; + +//TODO make pivot tree part of this structure, with cached invalidation, then recompute manually when needed +class ObjectPart { float volume { }; Vec3f volume_centroid_accumulator = Vec3f::Zero(); float sticking_force { }; Vec3f sticking_centroid_accumulator = Vec3f::Zero(); std::vector pivot_points { }; + CoordinateFunctor pivots_coordinate_functor; + bool is_pivot_tree_valid = false; + KDTreeIndirect<3, float, CoordinateFunctor> pivot_tree { CoordinateFunctor { } }; + +public: void add(const ObjectPart &other) { this->volume_centroid_accumulator += other.volume_centroid_accumulator; this->volume += other.volume; @@ -587,9 +607,12 @@ struct ObjectPart { this->sticking_force = island.sticking_force; this->sticking_centroid_accumulator = island.sticking_centroid_accumulator; this->pivot_points = island.pivot_points; + this->pivots_coordinate_functor = CoordinateFunctor(&this->pivot_points); } - ObjectPart() = default; + ObjectPart() { + this->pivots_coordinate_functor = CoordinateFunctor(&this->pivot_points); + }; }; struct WeakestConnection { @@ -691,6 +714,10 @@ Issues check_global_stability(const std::vector &islands_graph, co prev_island_to_weakest_connection = next_island_to_weakest_connection; next_island_to_weakest_connection.clear(); + // All object parts updated, inactive parts removed and weakest point of each island updated as well. + // Now compute the stability of each active object part, adding supports where necessary, and also + // check each island whether the weakest point is strong enough. If not, add supports as well. + } return issues; } From 07049b849e44bd0ee4c712f619aeb574d9e6ad8d Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Thu, 21 Jul 2022 14:41:53 +0200 Subject: [PATCH 049/100] fixed various bugs --- src/libslic3r/SupportSpotsGenerator.cpp | 254 +++++++++++++++++++----- 1 file changed, 201 insertions(+), 53 deletions(-) diff --git a/src/libslic3r/SupportSpotsGenerator.cpp b/src/libslic3r/SupportSpotsGenerator.cpp index 61e078d8b6..b4ddc97c2e 100644 --- a/src/libslic3r/SupportSpotsGenerator.cpp +++ b/src/libslic3r/SupportSpotsGenerator.cpp @@ -74,7 +74,7 @@ private: AABBTreeIndirect::Tree<2, float> tree; public: - explicit LinesDistancer(std::vector &lines) : + explicit LinesDistancer(const std::vector &lines) : lines(lines) { tree = AABBTreeLines::build_aabb_tree_over_indexed_lines(this->lines); } @@ -249,17 +249,17 @@ public: }; struct IslandConnection { - float area; - Vec3f centroid_accumulator; + float area{}; + Vec3f centroid_accumulator = Vec3f::Zero(); }; struct Island { - std::unordered_map connected_islands; - std::vector pivot_points; // for support points present on this layer (or bed extrusions) - float volume; - Vec3f volume_centroid_accumulator; - float sticking_force; // for support points present on this layer (or bed extrusions) - Vec3f sticking_centroid_accumulator; + std::unordered_map connected_islands{}; + std::vector pivot_points{}; // for support points present on this layer (or bed extrusions) + float volume{}; + Vec3f volume_centroid_accumulator = Vec3f::Zero(); + float sticking_force{}; // for support points present on this layer (or bed extrusions) + Vec3f sticking_centroid_accumulator = Vec3f::Zero(); std::vector external_lines; }; @@ -316,7 +316,7 @@ struct ExtrusionPropertiesAccumulator { void check_extrusion_entity_stability(const ExtrusionEntity *entity, std::vector &checked_lines_out, - float print_z, + float layer_z, const LayerRegion *layer_region, const LinesDistancer &prev_layer_lines, Issues &issues, @@ -324,12 +324,12 @@ void check_extrusion_entity_stability(const ExtrusionEntity *entity, if (entity->is_collection()) { for (const auto *e : static_cast(entity)->entities) { - check_extrusion_entity_stability(e, checked_lines_out, print_z, layer_region, prev_layer_lines, + check_extrusion_entity_stability(e, checked_lines_out, layer_z, layer_region, prev_layer_lines, issues, params); } } else { //single extrusion path, with possible varying parameters - const auto to_vec3f = [print_z](const Vec2f &point) { - return Vec3f(point.x(), point.y(), print_z); + const auto to_vec3f = [layer_z](const Vec2f &point) { + return Vec3f(point.x(), point.y(), layer_z); }; Points points { }; entity->collect_points(points); @@ -509,16 +509,16 @@ std::tuple reckon_islands( const ExtrusionLine &line = layer_lines[lidx]; float volume = line.origin_entity->min_mm3_per_mm() * line.len; island.volume += volume; - island.volume_centroid_accumulator += to_3d(Vec2f((line.a + line.b) / 2.0f), float(layer->print_z)) + island.volume_centroid_accumulator += to_3d(Vec2f((line.a + line.b) / 2.0f), float(layer->slice_z)) * volume; if (first_layer) { float sticking_force = line.len * flow_width * params.base_adhesion; island.sticking_force += sticking_force; island.sticking_centroid_accumulator += sticking_force - * to_3d(Vec2f((line.a + line.b) / 2.0f), float(layer->print_z)); + * to_3d(Vec2f((line.a + line.b) / 2.0f), float(layer->slice_z)); if (line.is_external_perimeter()) { - island.pivot_points.push_back(to_3d(Vec2f(line.b), float(layer->print_z))); + island.pivot_points.push_back(to_3d(Vec2f(line.b), float(layer->slice_z))); } } else if (layer_lines[lidx].support_point_generated) { float support_interface_area = params.support_points_interface_radius @@ -527,8 +527,8 @@ std::tuple reckon_islands( float sticking_force = support_interface_area * params.support_adhesion; island.sticking_force += sticking_force; island.sticking_centroid_accumulator += sticking_force - * to_3d(Vec2f(line.b), float(layer->print_z)); - island.pivot_points.push_back(to_3d(Vec2f(line.b), float(layer->print_z))); + * to_3d(Vec2f(line.b), float(layer->slice_z)); + island.pivot_points.push_back(to_3d(Vec2f(line.b), float(layer->slice_z))); } } } @@ -555,10 +555,11 @@ std::tuple reckon_islands( Vec2i coords = Vec2i(x, y); if (current_layer_grid.get_pixel(coords) != NULL_ISLAND && prev_layer_grid.get_pixel(coords) != NULL_ISLAND) { - IslandConnection& connection = result.islands[current_layer_grid.get_pixel(coords)] - .connected_islands[prev_layer_grid.get_pixel(coords)]; + IslandConnection &connection = result.islands[current_layer_grid.get_pixel(coords)] + .connected_islands[prev_layer_grid.get_pixel(coords)]; connection.area += current_layer_grid.pixel_area(); - connection.centroid_accumulator += to_3d(current_layer_grid.get_pixel_center(coords), result.layer_z) * current_layer_grid.pixel_area(); + connection.centroid_accumulator += to_3d(current_layer_grid.get_pixel_center(coords), result.layer_z) + * current_layer_grid.pixel_area(); } } } @@ -580,7 +581,6 @@ struct CoordinateFunctor { } }; -//TODO make pivot tree part of this structure, with cached invalidation, then recompute manually when needed class ObjectPart { float volume { }; Vec3f volume_centroid_accumulator = Vec3f::Zero(); @@ -592,6 +592,15 @@ class ObjectPart { bool is_pivot_tree_valid = false; KDTreeIndirect<3, float, CoordinateFunctor> pivot_tree { CoordinateFunctor { } }; + void check_pivot_tree() { + if (!is_pivot_tree_valid) { + this->pivots_coordinate_functor = CoordinateFunctor(&this->pivot_points); + this->pivot_tree = { this->pivots_coordinate_functor }; + pivot_tree.build(pivot_points.size()); + is_pivot_tree_valid = true; + } + } + public: void add(const ObjectPart &other) { this->volume_centroid_accumulator += other.volume_centroid_accumulator; @@ -599,6 +608,7 @@ public: this->sticking_force += other.sticking_force; this->sticking_centroid_accumulator += other.sticking_centroid_accumulator; this->pivot_points.insert(this->pivot_points.end(), other.pivot_points.begin(), other.pivot_points.end()); + this->is_pivot_tree_valid = this->is_pivot_tree_valid && other.pivot_points.empty(); } ObjectPart(const Island &island) { @@ -607,25 +617,121 @@ public: this->sticking_force = island.sticking_force; this->sticking_centroid_accumulator = island.sticking_centroid_accumulator; this->pivot_points = island.pivot_points; - this->pivots_coordinate_functor = CoordinateFunctor(&this->pivot_points); } - ObjectPart() { - this->pivots_coordinate_functor = CoordinateFunctor(&this->pivot_points); - }; + ObjectPart() = default; + + std::tuple is_stable_while_extruding(const ExtrusionLine &extruded_line, float layer_z, + const Params ¶ms) { + check_pivot_tree(); + + Vec2f line_dir = (extruded_line.b - extruded_line.a).normalized(); + Vec3f pivot_site_search_point = to_3d(Vec2f(extruded_line.b + line_dir * 300.0f), layer_z); + size_t pivot_idx = find_closest_point(this->pivot_tree, pivot_site_search_point); + const Vec3f &pivot = pivot_points[pivot_idx]; + + const Vec3f &sticking_centroid = this->sticking_centroid_accumulator / this->sticking_force; + float sticking_arm = (pivot - sticking_centroid).norm(); + float sticking_torque = sticking_arm * this->sticking_force; + + float mass = this->volume * params.filament_density; + const Vec3f &mass_centorid = this->volume_centroid_accumulator / this->volume; + float weight = mass * params.gravity_constant; + float weight_arm = (pivot.head<2>() - mass_centorid.head<2>()).norm(); + float weight_torque = weight_arm * weight; + + float bed_movement_arm = mass_centorid.z(); + float bed_movement_force = params.max_acceleration * mass; + float bed_movement_torque = bed_movement_force * bed_movement_arm; + + Vec3f extruder_pressure_direction = to_3d(line_dir, 0.0f); + extruder_pressure_direction.z() = -0.1f - extruded_line.malformation * 0.5f; + extruder_pressure_direction.normalize(); + Vec3d endpoint = (to_3d(extruded_line.b, layer_z)).cast(); + float conflict_torque_arm = line_alg::distance_to(Linef3(endpoint, endpoint + extruder_pressure_direction.cast()), pivot.cast()); +// float conflict_torque_arm = (to_3d(Vec2f(pivot.head<2>() - extruded_line.b), layer_z).cross( +// extruder_pressure_direction)).norm(); + float extruder_conflict_force = params.tolerable_extruder_conflict_force + + std::min(extruded_line.malformation, 1.0f) * params.malformations_additive_conflict_extruder_force; + float extruder_conflict_torque = extruder_conflict_force * conflict_torque_arm; + + float total_torque = bed_movement_torque + extruder_conflict_torque - weight_torque - sticking_torque; + +#if 1 + BOOST_LOG_TRIVIAL(debug) + << "pivot: " << pivot.x() << " " << pivot.y() << " " << pivot.z(); + BOOST_LOG_TRIVIAL(debug) + << "sticking_centroid: " << sticking_centroid.x() << " " << sticking_centroid.y() << " " << sticking_centroid.z(); + BOOST_LOG_TRIVIAL(debug) + << "SSG: sticking_force: " << sticking_force; + BOOST_LOG_TRIVIAL(debug) + << "SSG: sticking_arm: " << sticking_arm; + BOOST_LOG_TRIVIAL(debug) + << "SSG: sticking_torque: " << sticking_torque; + BOOST_LOG_TRIVIAL(debug) + << "SSG: weight_arm: " << sticking_arm; + BOOST_LOG_TRIVIAL(debug) + << "SSG: weight_torque: " << weight_torque; + BOOST_LOG_TRIVIAL(debug) + << "SSG: bed_movement_arm: " << bed_movement_arm; + BOOST_LOG_TRIVIAL(debug) + << "SSG: bed_movement_torque: " << bed_movement_torque; + BOOST_LOG_TRIVIAL(debug) + << "SSG: conflict_torque_arm: " << conflict_torque_arm; + BOOST_LOG_TRIVIAL(debug) + << "SSG: extruder_conflict_torque: " << extruder_conflict_torque; + BOOST_LOG_TRIVIAL(debug) + << "SSG: total_torque: " << total_torque << " layer_z: " << layer_z; +#endif + + return {total_torque / conflict_torque_arm, pivot_site_search_point}; + } + + void add_pivot_point(const Vec3f pivot_point, float sticking_force) { + this->pivot_points.push_back(pivot_point); + this->sticking_force += sticking_force; + this->sticking_centroid_accumulator += sticking_force * pivot_point; + this->is_pivot_tree_valid = false; + } + + void print() const { + std::cout << "sticking_force: " << sticking_force << std::endl; + std::cout << "volume: " << volume << std::endl; + } + }; struct WeakestConnection { float area = 0.0f; Vec3f centroid_accumulator = Vec3f::Zero(); - void add(const WeakestConnection& other) { + void add(const WeakestConnection &other) { this->area += other.area; this->centroid_accumulator += other.centroid_accumulator; } }; -Issues check_global_stability(const std::vector &islands_graph, const Params ¶ms) { +void debug_print_graph(const std::vector& islands_graph) { + std::cout << "BUILT ISLANDS GRAPH:" << std::endl; + for (size_t layer_idx = 0; layer_idx < islands_graph.size(); ++layer_idx) { + std::cout << "ISLANDS AT LAYER: " << layer_idx << " AT HEIGHT: " << islands_graph[layer_idx].layer_z << std::endl; + for (size_t island_idx = 0; island_idx < islands_graph[layer_idx].islands.size(); ++island_idx) { + const Island &island = islands_graph[layer_idx].islands[island_idx]; + std::cout << " ISLAND " << island_idx << std::endl; + std::cout << " volume: " << island.volume << std::endl; + std::cout << " sticking_force: " << island.sticking_force << std::endl; + std::cout << " pivot_points count: " << island.pivot_points.size() << std::endl; + std::cout << " connected_islands count: " << island.connected_islands.size() << std::endl; + } + } + std::cout << "END OF GRAPH" << std::endl; + + +} + +Issues check_global_stability(SupportGridFilter supports_presence_grid, + const std::vector &islands_graph, const Params ¶ms) { + debug_print_graph(islands_graph); Issues issues { }; size_t next_part_idx = 0; std::unordered_map active_object_parts; @@ -641,7 +747,8 @@ Issues check_global_stability(const std::vector &islands_graph, co std::cout << "at layer: " << layer_idx << " the following island to object mapping is used:" << std::endl; for (const auto &m : prev_island_to_object_part_mapping) { std::cout << "island " << m.first << " maps to part " << m.second << std::endl; - Vec3f connection_center = prev_island_to_weakest_connection[m.first].centroid_accumulator / prev_island_to_weakest_connection[m.first].area; + Vec3f connection_center = prev_island_to_weakest_connection[m.first].centroid_accumulator + / prev_island_to_weakest_connection[m.first].area; std::cout << " island has weak point with connection area: " << prev_island_to_weakest_connection[m.first].area << " and center: " << connection_center.x() << " " << connection_center.y() << " " << connection_center.z() << std::endl; @@ -655,12 +762,12 @@ Issues check_global_stability(const std::vector &islands_graph, co active_object_parts.emplace(part_idx, ObjectPart(island)); next_island_to_object_part_mapping.emplace(island_idx, part_idx); next_island_to_weakest_connection.emplace(island_idx, - WeakestConnection{INFINITY, Vec3f::Zero()}); + WeakestConnection { INFINITY, Vec3f::Zero() }); layer_active_parts.insert(part_idx); } else { - size_t final_part_idx{}; - WeakestConnection transfered_weakest_connection{}; - WeakestConnection new_weakest_connection{}; + size_t final_part_idx { }; + WeakestConnection transfered_weakest_connection { }; + WeakestConnection new_weakest_connection { }; // MERGE parts { std::unordered_set part_indices; @@ -680,7 +787,7 @@ Issues check_global_stability(const std::vector &islands_graph, co } } } - auto estimate_strength = [layer_z](const WeakestConnection& conn){ + auto estimate_strength = [layer_z](const WeakestConnection &conn) { float radius = fsqrt(conn.area / PI); float arm_len_estimate = std::max(0.001f, layer_z - (conn.centroid_accumulator.z() / conn.area)); return radius * conn.area / arm_len_estimate; @@ -703,6 +810,7 @@ Issues check_global_stability(const std::vector &islands_graph, co parts_to_delete.insert(part.first); } else { std::cout << "at layer " << layer_idx << " part is still active: " << part.first << std::endl; + part.second.print(); } } for (size_t part_id : parts_to_delete) { @@ -718,11 +826,50 @@ Issues check_global_stability(const std::vector &islands_graph, co // Now compute the stability of each active object part, adding supports where necessary, and also // check each island whether the weakest point is strong enough. If not, add supports as well. + for (size_t island_idx = 0; island_idx < islands_graph[layer_idx].islands.size(); ++island_idx) { + const Island &island = islands_graph[layer_idx].islands[island_idx]; + ObjectPart &part = active_object_parts.at(prev_island_to_object_part_mapping[island_idx]); + + std::vector dummy { }; + LinesDistancer island_lines_dist(dummy); + float unchecked_dist = params.min_distance_between_support_points + 1.0f; + + for (const ExtrusionLine &line : island.external_lines) { + if (unchecked_dist + line.len < params.min_distance_between_support_points + && line.malformation < 0.3f) { + unchecked_dist += line.len; + } else { + unchecked_dist = line.len; + auto [force, pivot_site_search_point] = part.is_stable_while_extruding(line, layer_z, params); + if (force > 0) { + if (island_lines_dist.get_lines().empty()) { + island_lines_dist = LinesDistancer(island.external_lines); + } + Vec2f target_point; + size_t _idx; + island_lines_dist.signed_distance_from_lines(pivot_site_search_point.head<2>(), _idx, + target_point); + Vec3f support_point = to_3d(target_point, layer_z); + if (!supports_presence_grid.position_taken(support_point)) { + float area = params.support_points_interface_radius * params.support_points_interface_radius + * float(PI); + float sticking_force = area * params.support_adhesion; + part.add_pivot_point(support_point, sticking_force); + issues.support_points.emplace_back(support_point, force, + to_3d(Vec2f(line.b - line.a).normalized(), 0.0f)); + supports_presence_grid.take_position(support_point); + } + } + } + } + } + //end of iteration over layer } return issues; } /* + void a() { // islands_graph.back() refers to the top most (current) layer for (size_t island_idx = 0; island_idx < islands_graph.back().islands.size(); ++island_idx) { @@ -788,7 +935,7 @@ Issues check_global_stability(const std::vector &islands_graph, co Vec3f extruder_pressure_direction = to_3d(line_dir, 0.0f); extruder_pressure_direction.z() = -0.2 - line.malformation * 0.5; extruder_pressure_direction.normalize(); - float conflict_torque_arm = (to_3d(Vec2f(pivot - line.b), print_z).cross( + float conflict_torque_arm = (to_3d(Vec2f(pivot - line.b), layer_z).cross( extruder_pressure_direction)).norm(); float extruder_conflict_force = params.tolerable_extruder_conflict_force + std::min(line.malformation, 1.0f) * params.malformations_additive_conflict_extruder_force; @@ -800,17 +947,17 @@ Issues check_global_stability(const std::vector &islands_graph, co Vec2f target_point { }; size_t _idx { }; island_lines_dist.signed_distance_from_lines(pivot_site_search_point, _idx, target_point); - if (!supports_presence_grid.position_taken(to_3d(target_point, print_z))) { + if (!supports_presence_grid.position_taken(to_3d(target_point, layer_z))) { float area = params.support_points_interface_radius * params.support_points_interface_radius * float(PI); float sticking_force = area * params.support_adhesion; - Vec3f support_point = to_3d(target_point, print_z); + Vec3f support_point = to_3d(target_point, layer_z); island.pivot_points.push_back(support_point); island.sticking_force += sticking_force; island.sticking_centroid_accumulator += sticking_force * support_point; issues.support_points.emplace_back(support_point, extruder_conflict_torque - sticking_torque, extruder_pressure_direction); - supports_presence_grid.take_position(to_3d(target_point, print_z)); + supports_presence_grid.take_position(to_3d(target_point, layer_z)); } } #if 0 @@ -831,7 +978,7 @@ Issues check_global_stability(const std::vector &islands_graph, co BOOST_LOG_TRIVIAL(debug) << "SSG: extruder_conflict_torque: " << extruder_conflict_torque; BOOST_LOG_TRIVIAL(debug) - << "SSG: total_torque: " << total_torque << " printz: " << print_z; + << "SSG: total_torque: " << total_torque << " layer_z: " << layer_z; #endif } @@ -886,7 +1033,7 @@ Issues check_global_stability(const std::vector &islands_graph, co Vec3f extruder_pressure_direction = to_3d(line_dir, 0.0f); extruder_pressure_direction.z() = -0.2 - line.malformation * 0.5; extruder_pressure_direction.normalize(); - float conflict_torque_arm = (to_3d(Vec2f(pivot - line.b), print_z).cross( + float conflict_torque_arm = (to_3d(Vec2f(pivot - line.b), layer_z).cross( extruder_pressure_direction)).norm(); float extruder_conflict_force = params.tolerable_extruder_conflict_force + std::min(line.malformation, 1.0f) * params.malformations_additive_conflict_extruder_force; @@ -898,17 +1045,17 @@ Issues check_global_stability(const std::vector &islands_graph, co Vec2f target_point; size_t _idx; island_lines_dist.signed_distance_from_lines(pivot_site_search_point, _idx, target_point); - if (!supports_presence_grid.position_taken(to_3d(target_point, print_z))) { + if (!supports_presence_grid.position_taken(to_3d(target_point, layer_z))) { float area = params.support_points_interface_radius * params.support_points_interface_radius * float(PI); float sticking_force = area * params.support_adhesion; - Vec3f support_point = to_3d(target_point, print_z); + Vec3f support_point = to_3d(target_point, layer_z); island.pivot_points.push_back(support_point); island.sticking_force += sticking_force; island.sticking_centroid_accumulator += sticking_force * support_point; issues.support_points.emplace_back(support_point, extruder_conflict_torque - sticking_torque, extruder_pressure_direction); - supports_presence_grid.take_position(to_3d(target_point, print_z)); + supports_presence_grid.take_position(to_3d(target_point, layer_z)); } } #if 0 @@ -929,12 +1076,13 @@ Issues check_global_stability(const std::vector &islands_graph, co BOOST_LOG_TRIVIAL(debug) << "SSG: extruder_conflict_torque: " << extruder_conflict_torque; BOOST_LOG_TRIVIAL(debug) - << "SSG: total_torque: " << total_torque << " printz: " << print_z; + << "SSG: total_torque: " << total_torque << " layer_z: " << layer_z; #endif } } return issues; + } */ std::tuple> check_extrusions_and_build_graph(const PrintObject *po, @@ -950,7 +1098,7 @@ std::tuple> check_extrusions_and_build_graph(c float flow_width = get_flow_width(po->layers()[po->layer_count() - 1]->regions()[0], erExternalPerimeter); PixelGrid prev_layer_grid(po, flow_width); - // PREPARE BASE LAYER +// PREPARE BASE LAYER const Layer *layer = po->layers()[0]; for (const LayerRegion *layer_region : layer->regions()) { for (const ExtrusionEntity *ex_entity : layer_region->perimeters.entities) { @@ -998,7 +1146,7 @@ std::tuple> check_extrusions_and_build_graph(c size_t pseudornd = ((island_idx + 127) * 33331 + 6907) % 23; Vec3f color = value_to_rgbf(0.0f, float(23), float(pseudornd)); fprintf(segmentation_f, "v %f %f %f %f %f %f\n", pos[0], - pos[1], layer->print_z, color[0], color[1], color[2]); + pos[1], layer->slice_z, color[0], color[1], color[2]); } } } @@ -1006,7 +1154,7 @@ std::tuple> check_extrusions_and_build_graph(c if (line.malformation > 0.0f) { Vec3f color = value_to_rgbf(0, 1.0f, line.malformation); fprintf(malform_f, "v %f %f %f %f %f %f\n", line.b[0], - line.b[1], layer->print_z, color[0], color[1], color[2]); + line.b[1], layer->slice_z, color[0], color[1], color[2]); } } #endif @@ -1019,7 +1167,7 @@ std::tuple> check_extrusions_and_build_graph(c for (const LayerRegion *layer_region : layer->regions()) { for (const ExtrusionEntity *ex_entity : layer_region->perimeters.entities) { for (const ExtrusionEntity *perimeter : static_cast(ex_entity)->entities) { - check_extrusion_entity_stability(perimeter, layer_lines, layer->print_z, layer_region, + check_extrusion_entity_stability(perimeter, layer_lines, layer->slice_z, layer_region, external_lines, issues, params); } // perimeter } // ex_entity @@ -1027,7 +1175,7 @@ std::tuple> check_extrusions_and_build_graph(c for (const ExtrusionEntity *fill : static_cast(ex_entity)->entities) { if (fill->role() == ExtrusionRole::erGapFill || fill->role() == ExtrusionRole::erBridgeInfill) { - check_extrusion_entity_stability(fill, layer_lines, layer->print_z, layer_region, + check_extrusion_entity_stability(fill, layer_lines, layer->slice_z, layer_region, external_lines, issues, params); } else { Points points { }; @@ -1043,7 +1191,7 @@ std::tuple> check_extrusions_and_build_graph(c } // ex_entity } // region - auto [layer_islands, layer_grid] = reckon_islands(layer, true, 0, prev_layer_grid, + auto [layer_islands, layer_grid] = reckon_islands(layer, false, 0, prev_layer_grid, layer_lines, params); islands_graph.push_back(std::move(layer_islands)); @@ -1057,7 +1205,7 @@ std::tuple> check_extrusions_and_build_graph(c size_t pseudornd = ((island_idx + 127) * 33331 + 6907) % 23; Vec3f color = value_to_rgbf(0.0f, float(23), float(pseudornd)); fprintf(segmentation_f, "v %f %f %f %f %f %f\n", pos[0], - pos[1], layer->print_z, color[0], color[1], color[2]); + pos[1], layer->slice_z, color[0], color[1], color[2]); } } } @@ -1065,7 +1213,7 @@ std::tuple> check_extrusions_and_build_graph(c if (line.malformation > 0.0f) { Vec3f color = value_to_rgbf(0, 1.0f, line.malformation); fprintf(malform_f, "v %f %f %f %f %f %f\n", line.b[0], - line.b[1], layer->print_z, color[0], color[1], color[2]); + line.b[1], layer->slice_z, color[0], color[1], color[2]); } } #endif @@ -1110,7 +1258,7 @@ std::vector quick_search(const PrintObject *po, const Params ¶ms) { Issues full_search(const PrintObject *po, const Params ¶ms) { auto [local_issues, graph] = check_extrusions_and_build_graph(po, params); - Issues global_issues = check_global_stability(graph, params); + Issues global_issues = check_global_stability( { po, params.min_distance_between_support_points }, graph, params); #ifdef DEBUG_FILES debug_export(local_issues, "local_issues"); debug_export(global_issues, "global_issues"); From 3b029cef05da0cdc2655144c9f94e12877b6597a Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Thu, 21 Jul 2022 14:58:47 +0200 Subject: [PATCH 050/100] another bulk of fixes GLOBAL STABILITY check works --- src/libslic3r/SupportSpotsGenerator.cpp | 28 ++++++++++++++----------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/src/libslic3r/SupportSpotsGenerator.cpp b/src/libslic3r/SupportSpotsGenerator.cpp index b4ddc97c2e..4abf4dd952 100644 --- a/src/libslic3r/SupportSpotsGenerator.cpp +++ b/src/libslic3r/SupportSpotsGenerator.cpp @@ -249,16 +249,16 @@ public: }; struct IslandConnection { - float area{}; + float area { }; Vec3f centroid_accumulator = Vec3f::Zero(); }; struct Island { - std::unordered_map connected_islands{}; - std::vector pivot_points{}; // for support points present on this layer (or bed extrusions) - float volume{}; + std::unordered_map connected_islands { }; + std::vector pivot_points { }; // for support points present on this layer (or bed extrusions) + float volume { }; Vec3f volume_centroid_accumulator = Vec3f::Zero(); - float sticking_force{}; // for support points present on this layer (or bed extrusions) + float sticking_force { }; // for support points present on this layer (or bed extrusions) Vec3f sticking_centroid_accumulator = Vec3f::Zero(); std::vector external_lines; @@ -623,6 +623,10 @@ public: std::tuple is_stable_while_extruding(const ExtrusionLine &extruded_line, float layer_z, const Params ¶ms) { + if (pivot_points.empty()) { + return {this->volume * params.filament_density*params.gravity_constant,Vec3f {0.0f,0.0f,-1.0f}}; + } + check_pivot_tree(); Vec2f line_dir = (extruded_line.b - extruded_line.a).normalized(); @@ -648,9 +652,8 @@ public: extruder_pressure_direction.z() = -0.1f - extruded_line.malformation * 0.5f; extruder_pressure_direction.normalize(); Vec3d endpoint = (to_3d(extruded_line.b, layer_z)).cast(); - float conflict_torque_arm = line_alg::distance_to(Linef3(endpoint, endpoint + extruder_pressure_direction.cast()), pivot.cast()); -// float conflict_torque_arm = (to_3d(Vec2f(pivot.head<2>() - extruded_line.b), layer_z).cross( -// extruder_pressure_direction)).norm(); + float conflict_torque_arm = line_alg::distance_to( + Linef3(endpoint, endpoint + extruder_pressure_direction.cast()), pivot.cast()); float extruder_conflict_force = params.tolerable_extruder_conflict_force + std::min(extruded_line.malformation, 1.0f) * params.malformations_additive_conflict_extruder_force; float extruder_conflict_torque = extruder_conflict_force * conflict_torque_arm; @@ -661,7 +664,8 @@ public: BOOST_LOG_TRIVIAL(debug) << "pivot: " << pivot.x() << " " << pivot.y() << " " << pivot.z(); BOOST_LOG_TRIVIAL(debug) - << "sticking_centroid: " << sticking_centroid.x() << " " << sticking_centroid.y() << " " << sticking_centroid.z(); + << "sticking_centroid: " << sticking_centroid.x() << " " << sticking_centroid.y() << " " + << sticking_centroid.z(); BOOST_LOG_TRIVIAL(debug) << "SSG: sticking_force: " << sticking_force; BOOST_LOG_TRIVIAL(debug) @@ -711,10 +715,11 @@ struct WeakestConnection { } }; -void debug_print_graph(const std::vector& islands_graph) { +void debug_print_graph(const std::vector &islands_graph) { std::cout << "BUILT ISLANDS GRAPH:" << std::endl; for (size_t layer_idx = 0; layer_idx < islands_graph.size(); ++layer_idx) { - std::cout << "ISLANDS AT LAYER: " << layer_idx << " AT HEIGHT: " << islands_graph[layer_idx].layer_z << std::endl; + std::cout << "ISLANDS AT LAYER: " << layer_idx << " AT HEIGHT: " << islands_graph[layer_idx].layer_z + << std::endl; for (size_t island_idx = 0; island_idx < islands_graph[layer_idx].islands.size(); ++island_idx) { const Island &island = islands_graph[layer_idx].islands[island_idx]; std::cout << " ISLAND " << island_idx << std::endl; @@ -726,7 +731,6 @@ void debug_print_graph(const std::vector& islands_graph) { } std::cout << "END OF GRAPH" << std::endl; - } Issues check_global_stability(SupportGridFilter supports_presence_grid, From ed1c4d99a7285db6008b43ba12a9483771dd8355 Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Thu, 21 Jul 2022 17:40:18 +0200 Subject: [PATCH 051/100] Weakest connection break check also implemented. Tensile force however might be too low approximation. --- src/libslic3r/SupportSpotsGenerator.cpp | 338 ++++++++---------------- 1 file changed, 106 insertions(+), 232 deletions(-) diff --git a/src/libslic3r/SupportSpotsGenerator.cpp b/src/libslic3r/SupportSpotsGenerator.cpp index 4abf4dd952..31ac358a0b 100644 --- a/src/libslic3r/SupportSpotsGenerator.cpp +++ b/src/libslic3r/SupportSpotsGenerator.cpp @@ -567,6 +567,16 @@ std::tuple reckon_islands( return {result, current_layer_grid}; } +struct WeakestConnection { + float area = 0.0f; + Vec3f centroid_accumulator = Vec3f::Zero(); + + void add(const WeakestConnection &other) { + this->area += other.area; + this->centroid_accumulator += other.centroid_accumulator; + } +}; + struct CoordinateFunctor { const std::vector *coordinates; CoordinateFunctor(const std::vector *coords) : @@ -639,12 +649,12 @@ public: float sticking_torque = sticking_arm * this->sticking_force; float mass = this->volume * params.filament_density; - const Vec3f &mass_centorid = this->volume_centroid_accumulator / this->volume; + const Vec3f &mass_centroid = this->volume_centroid_accumulator / this->volume; float weight = mass * params.gravity_constant; - float weight_arm = (pivot.head<2>() - mass_centorid.head<2>()).norm(); + float weight_arm = (pivot.head<2>() - mass_centroid.head<2>()).norm(); float weight_torque = weight_arm * weight; - float bed_movement_arm = mass_centorid.z(); + float bed_movement_arm = mass_centroid.z(); float bed_movement_force = params.max_acceleration * mass; float bed_movement_torque = bed_movement_force * bed_movement_arm; @@ -673,7 +683,7 @@ public: BOOST_LOG_TRIVIAL(debug) << "SSG: sticking_torque: " << sticking_torque; BOOST_LOG_TRIVIAL(debug) - << "SSG: weight_arm: " << sticking_arm; + << "SSG: weight_arm: " << weight_arm; BOOST_LOG_TRIVIAL(debug) << "SSG: weight_torque: " << weight_torque; BOOST_LOG_TRIVIAL(debug) @@ -691,6 +701,73 @@ public: return {total_torque / conflict_torque_arm, pivot_site_search_point}; } + float is_strong_enough_while_extruding( + const WeakestConnection &connection, + const ExtrusionLine &extruded_line, + float layer_z, + const Params ¶ms) const { + + Vec2f line_dir = (extruded_line.b - extruded_line.a).normalized(); + Vec3f pivot = connection.centroid_accumulator / connection.area; + + float tensile_force = connection.area * params.tensile_strength; + float tensile_arm = fsqrt(connection.area / float(PI)); + float tensile_torque = tensile_force * tensile_arm; + + const Vec3f &mass_centroid = this->volume_centroid_accumulator / this->volume; + float mass = this->volume * params.filament_density + * ((2.0f * layer_z - pivot.z() - mass_centroid.z()) / (2.0f * layer_z)); + + float weight = mass * params.gravity_constant; + float weight_arm = (pivot.head<2>() - mass_centroid.head<2>()).norm(); + float weight_torque = weight_arm * weight; + + float bed_movement_arm = mass_centroid.z(); + float bed_movement_force = params.max_acceleration * mass; + float bed_movement_torque = bed_movement_force * bed_movement_arm; + + Vec3f extruder_pressure_direction = to_3d(line_dir, 0.0f); + extruder_pressure_direction.z() = -0.1f - extruded_line.malformation * 0.5f; + extruder_pressure_direction.normalize(); + Vec3d endpoint = (to_3d(extruded_line.b, layer_z)).cast(); + float conflict_torque_arm = line_alg::distance_to( + Linef3(endpoint, endpoint + extruder_pressure_direction.cast()), pivot.cast()); + float extruder_conflict_force = params.tolerable_extruder_conflict_force + + std::min(extruded_line.malformation, 1.0f) * params.malformations_additive_conflict_extruder_force; + float extruder_conflict_torque = extruder_conflict_force * conflict_torque_arm; + + float total_torque = bed_movement_torque + extruder_conflict_torque + weight_torque - tensile_torque; +#if 1 + BOOST_LOG_TRIVIAL(debug) + << "pivot: " << pivot.x() << " " << pivot.y() << " " << pivot.z(); + BOOST_LOG_TRIVIAL(debug) + << "mass_centroid: " << mass_centroid.x() << " " << mass_centroid.y() << " " + << mass_centroid.z(); + BOOST_LOG_TRIVIAL(debug) + << "SSG: tensile_force: " << tensile_force; + BOOST_LOG_TRIVIAL(debug) + << "SSG: tensile_arm: " << tensile_arm; + BOOST_LOG_TRIVIAL(debug) + << "SSG: tensile_torque: " << tensile_torque; + BOOST_LOG_TRIVIAL(debug) + << "SSG: weight_arm: " << weight_arm; + BOOST_LOG_TRIVIAL(debug) + << "SSG: weight_torque: " << weight_torque; + BOOST_LOG_TRIVIAL(debug) + << "SSG: bed_movement_arm: " << bed_movement_arm; + BOOST_LOG_TRIVIAL(debug) + << "SSG: bed_movement_torque: " << bed_movement_torque; + BOOST_LOG_TRIVIAL(debug) + << "SSG: conflict_torque_arm: " << conflict_torque_arm; + BOOST_LOG_TRIVIAL(debug) + << "SSG: extruder_conflict_torque: " << extruder_conflict_torque; + BOOST_LOG_TRIVIAL(debug) + << "SSG: total_torque: " << total_torque << " layer_z: " << layer_z; +#endif + + return total_torque / conflict_torque_arm; + } + void add_pivot_point(const Vec3f pivot_point, float sticking_force) { this->pivot_points.push_back(pivot_point); this->sticking_force += sticking_force; @@ -705,16 +782,6 @@ public: }; -struct WeakestConnection { - float area = 0.0f; - Vec3f centroid_accumulator = Vec3f::Zero(); - - void add(const WeakestConnection &other) { - this->area += other.area; - this->centroid_accumulator += other.centroid_accumulator; - } -}; - void debug_print_graph(const std::vector &islands_graph) { std::cout << "BUILT ISLANDS GRAPH:" << std::endl; for (size_t layer_idx = 0; layer_idx < islands_graph.size(); ++layer_idx) { @@ -793,10 +860,21 @@ Issues check_global_stability(SupportGridFilter supports_presence_grid, } auto estimate_strength = [layer_z](const WeakestConnection &conn) { float radius = fsqrt(conn.area / PI); - float arm_len_estimate = std::max(0.001f, layer_z - (conn.centroid_accumulator.z() / conn.area)); + float arm_len_estimate = std::max(1.3f, layer_z - (conn.centroid_accumulator.z() / conn.area)); return radius * conn.area / arm_len_estimate; }; + std::cout << "new_weakest_connection info: " << std::endl; + std::cout << "area: " << new_weakest_connection.area << std::endl; + std::cout << "centroid acc: " << new_weakest_connection.centroid_accumulator.x() << " " + << new_weakest_connection.centroid_accumulator.y() << " " + << new_weakest_connection.centroid_accumulator.z() << std::endl; + std::cout << "transfered_weakest_connection info: " << std::endl; + std::cout << "area: " << transfered_weakest_connection.area << std::endl; + std::cout << "centroid acc: " << transfered_weakest_connection.centroid_accumulator.x() << " " + << transfered_weakest_connection.centroid_accumulator.y() << " " + << transfered_weakest_connection.centroid_accumulator.z() << std::endl; + if (estimate_strength(transfered_weakest_connection) < estimate_strength(new_weakest_connection)) { new_weakest_connection = transfered_weakest_connection; } @@ -833,6 +911,12 @@ Issues check_global_stability(SupportGridFilter supports_presence_grid, for (size_t island_idx = 0; island_idx < islands_graph[layer_idx].islands.size(); ++island_idx) { const Island &island = islands_graph[layer_idx].islands[island_idx]; ObjectPart &part = active_object_parts.at(prev_island_to_object_part_mapping[island_idx]); + WeakestConnection &weakest_conn = prev_island_to_weakest_connection[island_idx]; + std::cout << "Weakest connection info: " << std::endl; + std::cout << "area: " << weakest_conn.area << std::endl; + std::cout << "centroid acc: " << weakest_conn.centroid_accumulator.x() << " " + << weakest_conn.centroid_accumulator.y() << " " + << weakest_conn.centroid_accumulator.z() << std::endl; std::vector dummy { }; LinesDistancer island_lines_dist(dummy); @@ -845,6 +929,10 @@ Issues check_global_stability(SupportGridFilter supports_presence_grid, } else { unchecked_dist = line.len; auto [force, pivot_site_search_point] = part.is_stable_while_extruding(line, layer_z, params); + if (force <= 0) { + force = part.is_strong_enough_while_extruding(weakest_conn, line, layer_z, params); + } + if (force > 0) { if (island_lines_dist.get_lines().empty()) { island_lines_dist = LinesDistancer(island.external_lines); @@ -862,6 +950,9 @@ Issues check_global_stability(SupportGridFilter supports_presence_grid, issues.support_points.emplace_back(support_point, force, to_3d(Vec2f(line.b - line.a).normalized(), 0.0f)); supports_presence_grid.take_position(support_point); + + weakest_conn.area += area; + weakest_conn.centroid_accumulator += support_point * area; } } } @@ -872,223 +963,6 @@ Issues check_global_stability(SupportGridFilter supports_presence_grid, return issues; } -/* - void a() { - - // islands_graph.back() refers to the top most (current) layer - for (size_t island_idx = 0; island_idx < islands_graph.back().islands.size(); ++island_idx) { - Island &island = islands_graph.back().islands[island_idx]; - - std::vector island_external_lines; - for (size_t lidx : islands_lines[island_idx]) { - island_external_lines.push_back(layer_lines[lidx]); - } - LinesDistancer island_lines_dist(island_external_lines); - Accumulator acc = island; // in acc, we accumulate the mass and other properties of the object part as we traverse the islands down to bed - // There is one object part for each island at the top most layer, and each one is computed individually - - // Some of the calculations will be done multiple times - int layer_idx = islands_graph.size() - 1; - // traverse the islands graph down, and for each connection area, calculate if it holds or breaks - while (acc.connected_islands.size() > 0) { - //test for break between layer_idx and layer_idx -1; - LayerIslands below = islands_graph[layer_idx - 1]; // must exist, see while condition - layer_idx--; - // initialize variables that we will accumulate over all islands, which are connected to the current object part - std::vector pivot_points; - Vec2f sticking_centroid; - float connection_area = 0; - for (const auto &pair : acc.connected_islands) { - const Island &below_i = below.islands[pair.first]; - Vec2f centroid = (below_i.volume_centroid_accumulator / below_i.volume).head<2>(); // centroid of the island 'below_i'; TODO it should be centroid of the connection area - pivot_points.push_back(centroid); // for object parts, we also consider breaking pivots in the centroids of the islands - sticking_centroid += centroid * pair.second; // pair.second is connection area in mm^2 - connection_area += pair.second; - } - sticking_centroid /= connection_area; //normalize to get final sticking centroid - for (const Vec3f &p_point : acc.pivot_points) { - pivot_points.push_back(p_point.head<2>()); - } - // Now we have accumulated pivot points, connection area and sticking centroid of the whole layer to the current object part - - // create KD tree over current pivot points - auto coord_fn = [&pivot_points](size_t idx, size_t dim) { - return pivot_points[idx][dim]; - }; - KDTreeIndirect<2, float, decltype(coord_fn)> pivot_points_tree(coord_fn, pivot_points.size()); - - // iterate over extrusions at top layer island, check each for stability - for (const ExtrusionLine &line : island_external_lines) { - Vec2f line_dir = (line.b - line.a).normalized(); - Vec2f pivot_site_search_point = line.b + line_dir * 300.0f; - size_t pivot_idx = find_closest_point(pivot_points_tree, pivot_site_search_point); - const Vec2f &pivot = pivot_points[pivot_idx]; - - float sticking_arm = (pivot - sticking_centroid).norm(); - float sticking_torque = sticking_arm * connection_area * params.tensile_strength; // For breakage in between layers, we compute with tensile strength, not bed adhesion - - float mass = acc.volume * params.filament_density; - const Vec3f &mass_centorid = acc.volume_centroid_accumulator / acc.volume; - float weight = mass * params.gravity_constant; - float weight_arm = (pivot - mass_centorid.head<2>()).norm(); - float weight_torque = weight_arm * weight; - - float bed_movement_arm = mass_centorid.z(); - float bed_movement_force = params.max_acceleration * mass; - float bed_movement_torque = bed_movement_force * bed_movement_arm; - - Vec3f extruder_pressure_direction = to_3d(line_dir, 0.0f); - extruder_pressure_direction.z() = -0.2 - line.malformation * 0.5; - extruder_pressure_direction.normalize(); - float conflict_torque_arm = (to_3d(Vec2f(pivot - line.b), layer_z).cross( - extruder_pressure_direction)).norm(); - float extruder_conflict_force = params.tolerable_extruder_conflict_force + - std::min(line.malformation, 1.0f) * params.malformations_additive_conflict_extruder_force; - float extruder_conflict_torque = extruder_conflict_force * conflict_torque_arm; - - float total_torque = bed_movement_torque + extruder_conflict_torque - weight_torque - sticking_torque; - - if (total_torque > 0) { - Vec2f target_point { }; - size_t _idx { }; - island_lines_dist.signed_distance_from_lines(pivot_site_search_point, _idx, target_point); - if (!supports_presence_grid.position_taken(to_3d(target_point, layer_z))) { - float area = params.support_points_interface_radius * params.support_points_interface_radius - * float(PI); - float sticking_force = area * params.support_adhesion; - Vec3f support_point = to_3d(target_point, layer_z); - island.pivot_points.push_back(support_point); - island.sticking_force += sticking_force; - island.sticking_centroid_accumulator += sticking_force * support_point; - issues.support_points.emplace_back(support_point, - extruder_conflict_torque - sticking_torque, extruder_pressure_direction); - supports_presence_grid.take_position(to_3d(target_point, layer_z)); - } - } - #if 0 - BOOST_LOG_TRIVIAL(debug) - << "SSG: sticking_arm: " << sticking_arm; - BOOST_LOG_TRIVIAL(debug) - << "SSG: sticking_torque: " << sticking_torque; - BOOST_LOG_TRIVIAL(debug) - << "SSG: weight_arm: " << sticking_arm; - BOOST_LOG_TRIVIAL(debug) - << "SSG: weight_torque: " << weight_torque; - BOOST_LOG_TRIVIAL(debug) - << "SSG: bed_movement_arm: " << bed_movement_arm; - BOOST_LOG_TRIVIAL(debug) - << "SSG: bed_movement_torque: " << bed_movement_torque; - BOOST_LOG_TRIVIAL(debug) - << "SSG: conflict_torque_arm: " << conflict_torque_arm; - BOOST_LOG_TRIVIAL(debug) - << "SSG: extruder_conflict_torque: " << extruder_conflict_torque; - BOOST_LOG_TRIVIAL(debug) - << "SSG: total_torque: " << total_torque << " layer_z: " << layer_z; - #endif - } - - std::unordered_map tmp = acc.connected_islands; - acc.connected_islands.clear(); - // finally, add gathered islands to accumulator, and continue down to next layer - for (const auto &pair : tmp) { - const Island &below_i = below.islands[pair.first]; - for (const auto &below_islands : below_i.connected_islands) { - acc.connected_islands[below_islands.first] += below_islands.second; - } - for (const Vec3f &pivot_p : below_i.pivot_points) { - acc.pivot_points.push_back(pivot_p); - } - acc.sticking_centroid_accumulator += below_i.sticking_centroid_accumulator; - acc.sticking_force += below_i.sticking_force; - acc.volume += below_i.volume; - acc.volume_centroid_accumulator += below_i.volume_centroid_accumulator; - } - } - - // We have arrived to the bed level, now check for stability of the object part on the bed - std::vector pivot_points; - for (const Vec3f &p_point : acc.pivot_points) { - pivot_points.push_back(p_point.head<2>()); - } - auto coord_fn = [&pivot_points](size_t idx, size_t dim) { - return pivot_points[idx][dim]; - }; - KDTreeIndirect<2, float, decltype(coord_fn)> pivot_points_tree(coord_fn, pivot_points.size()); - - for (const ExtrusionLine &line : island_external_lines) { - Vec2f line_dir = (line.b - line.a).normalized(); - Vec2f pivot_site_search_point = line.b + line_dir * 300.0f; - size_t pivot_idx = find_closest_point(pivot_points_tree, pivot_site_search_point); - const Vec2f &pivot = pivot_points[pivot_idx]; - - const Vec2f &sticking_centroid = acc.sticking_centroid_accumulator.head<2>() / acc.sticking_force; - float sticking_arm = (pivot - sticking_centroid).norm(); - float sticking_torque = sticking_arm * acc.sticking_force; - - float mass = acc.volume * params.filament_density; - const Vec3f &mass_centorid = acc.volume_centroid_accumulator / acc.volume; - float weight = mass * params.gravity_constant; - float weight_arm = (pivot - mass_centorid.head<2>()).norm(); - float weight_torque = weight_arm * weight; - - float bed_movement_arm = mass_centorid.z(); - float bed_movement_force = params.max_acceleration * mass; - float bed_movement_torque = bed_movement_force * bed_movement_arm; - - Vec3f extruder_pressure_direction = to_3d(line_dir, 0.0f); - extruder_pressure_direction.z() = -0.2 - line.malformation * 0.5; - extruder_pressure_direction.normalize(); - float conflict_torque_arm = (to_3d(Vec2f(pivot - line.b), layer_z).cross( - extruder_pressure_direction)).norm(); - float extruder_conflict_force = params.tolerable_extruder_conflict_force + - std::min(line.malformation, 1.0f) * params.malformations_additive_conflict_extruder_force; - float extruder_conflict_torque = extruder_conflict_force * conflict_torque_arm; - - float total_torque = bed_movement_torque + extruder_conflict_torque - weight_torque - sticking_torque; - - if (total_torque > 0) { - Vec2f target_point; - size_t _idx; - island_lines_dist.signed_distance_from_lines(pivot_site_search_point, _idx, target_point); - if (!supports_presence_grid.position_taken(to_3d(target_point, layer_z))) { - float area = params.support_points_interface_radius * params.support_points_interface_radius - * float(PI); - float sticking_force = area * params.support_adhesion; - Vec3f support_point = to_3d(target_point, layer_z); - island.pivot_points.push_back(support_point); - island.sticking_force += sticking_force; - island.sticking_centroid_accumulator += sticking_force * support_point; - issues.support_points.emplace_back(support_point, - extruder_conflict_torque - sticking_torque, extruder_pressure_direction); - supports_presence_grid.take_position(to_3d(target_point, layer_z)); - } - } - #if 0 - BOOST_LOG_TRIVIAL(debug) - << "SSG: sticking_arm: " << sticking_arm; - BOOST_LOG_TRIVIAL(debug) - << "SSG: sticking_torque: " << sticking_torque; - BOOST_LOG_TRIVIAL(debug) - << "SSG: weight_arm: " << sticking_arm; - BOOST_LOG_TRIVIAL(debug) - << "SSG: weight_torque: " << weight_torque; - BOOST_LOG_TRIVIAL(debug) - << "SSG: bed_movement_arm: " << bed_movement_arm; - BOOST_LOG_TRIVIAL(debug) - << "SSG: bed_movement_torque: " << bed_movement_torque; - BOOST_LOG_TRIVIAL(debug) - << "SSG: conflict_torque_arm: " << conflict_torque_arm; - BOOST_LOG_TRIVIAL(debug) - << "SSG: extruder_conflict_torque: " << extruder_conflict_torque; - BOOST_LOG_TRIVIAL(debug) - << "SSG: total_torque: " << total_torque << " layer_z: " << layer_z; - #endif - } - } - - return issues; - } - */ - std::tuple> check_extrusions_and_build_graph(const PrintObject *po, const Params ¶ms) { #ifdef DEBUG_FILES From 9ff0d49fae61323a9c77f0be84afa2175292f6bf Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Fri, 22 Jul 2022 16:51:44 +0200 Subject: [PATCH 052/100] Implemented calculation of elastic section modulus --- src/libslic3r/PrintObject.cpp | 2 +- src/libslic3r/SupportSpotsGenerator.cpp | 116 ++++++++++++------------ src/libslic3r/SupportSpotsGenerator.hpp | 4 +- 3 files changed, 60 insertions(+), 62 deletions(-) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 674aaec26a..b5848cabac 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -434,7 +434,7 @@ void PrintObject::generate_support_spots() Vec3f point = Vec3f(inv_transform * support_point.position); Vec3f origin = Vec3f( inv_transform * Vec3f(support_point.position.x(), support_point.position.y(), 0.0f)); - selector.enforce_spot(point, origin, 0.3f); + selector.enforce_spot(point, origin, 0.6f); } model_volume->supported_facets.set(selector.selector); diff --git a/src/libslic3r/SupportSpotsGenerator.cpp b/src/libslic3r/SupportSpotsGenerator.cpp index 31ac358a0b..63f8f21000 100644 --- a/src/libslic3r/SupportSpotsGenerator.cpp +++ b/src/libslic3r/SupportSpotsGenerator.cpp @@ -251,6 +251,23 @@ public: struct IslandConnection { float area { }; Vec3f centroid_accumulator = Vec3f::Zero(); + Vec2f second_moment_of_area_accumulator = Vec2f::Zero(); + + void add(const IslandConnection &other) { + this->area += other.area; + this->centroid_accumulator += other.centroid_accumulator; + this->second_moment_of_area_accumulator += other.second_moment_of_area_accumulator; + } + + void print_info(const std::string &tag) { + Vec3f centroid = centroid_accumulator / area; + Vec2f variance = + (second_moment_of_area_accumulator / area - centroid.head<2>().cwiseProduct(centroid.head<2>())); + std::cout << tag << std::endl; + std::cout << "area: " << area << std::endl; + std::cout << "centroid: " << centroid.x() << " " << centroid.y() << " " << centroid.z() << std::endl; + std::cout << "variance: " << variance.x() << " " << variance.y() << std::endl; + } }; struct Island { @@ -557,8 +574,11 @@ std::tuple reckon_islands( && prev_layer_grid.get_pixel(coords) != NULL_ISLAND) { IslandConnection &connection = result.islands[current_layer_grid.get_pixel(coords)] .connected_islands[prev_layer_grid.get_pixel(coords)]; + Vec2f current_coords = current_layer_grid.get_pixel_center(coords); connection.area += current_layer_grid.pixel_area(); - connection.centroid_accumulator += to_3d(current_layer_grid.get_pixel_center(coords), result.layer_z) + connection.centroid_accumulator += to_3d(current_coords, result.layer_z) + * current_layer_grid.pixel_area(); + connection.second_moment_of_area_accumulator += current_coords.cwiseProduct(current_coords) * current_layer_grid.pixel_area(); } } @@ -567,16 +587,6 @@ std::tuple reckon_islands( return {result, current_layer_grid}; } -struct WeakestConnection { - float area = 0.0f; - Vec3f centroid_accumulator = Vec3f::Zero(); - - void add(const WeakestConnection &other) { - this->area += other.area; - this->centroid_accumulator += other.centroid_accumulator; - } -}; - struct CoordinateFunctor { const std::vector *coordinates; CoordinateFunctor(const std::vector *coords) : @@ -702,24 +712,25 @@ public: } float is_strong_enough_while_extruding( - const WeakestConnection &connection, + const IslandConnection &connection, const ExtrusionLine &extruded_line, float layer_z, const Params ¶ms) const { Vec2f line_dir = (extruded_line.b - extruded_line.a).normalized(); - Vec3f pivot = connection.centroid_accumulator / connection.area; - - float tensile_force = connection.area * params.tensile_strength; - float tensile_arm = fsqrt(connection.area / float(PI)); - float tensile_torque = tensile_force * tensile_arm; + Vec3f centroid = connection.centroid_accumulator / connection.area; + Vec2f variance = (connection.second_moment_of_area_accumulator / connection.area + - centroid.head<2>().cwiseProduct(centroid.head<2>())); + float extreme_fiber_dist = variance.cwiseSqrt().norm(); + float elastic_section_modulus = connection.area * (variance.x() + variance.y()) / extreme_fiber_dist; + float yield_torque = elastic_section_modulus * params.yield_strength; const Vec3f &mass_centroid = this->volume_centroid_accumulator / this->volume; float mass = this->volume * params.filament_density - * ((2.0f * layer_z - pivot.z() - mass_centroid.z()) / (2.0f * layer_z)); + * ((2.0f * layer_z - centroid.z() - mass_centroid.z()) / (2.0f * layer_z)); float weight = mass * params.gravity_constant; - float weight_arm = (pivot.head<2>() - mass_centroid.head<2>()).norm(); + float weight_arm = (centroid.head<2>() - mass_centroid.head<2>()).norm(); float weight_torque = weight_arm * weight; float bed_movement_arm = mass_centroid.z(); @@ -731,24 +742,24 @@ public: extruder_pressure_direction.normalize(); Vec3d endpoint = (to_3d(extruded_line.b, layer_z)).cast(); float conflict_torque_arm = line_alg::distance_to( - Linef3(endpoint, endpoint + extruder_pressure_direction.cast()), pivot.cast()); + Linef3(endpoint, endpoint + extruder_pressure_direction.cast()), centroid.cast()); float extruder_conflict_force = params.tolerable_extruder_conflict_force + std::min(extruded_line.malformation, 1.0f) * params.malformations_additive_conflict_extruder_force; float extruder_conflict_torque = extruder_conflict_force * conflict_torque_arm; - float total_torque = bed_movement_torque + extruder_conflict_torque + weight_torque - tensile_torque; + float total_torque = bed_movement_torque + extruder_conflict_torque + weight_torque - yield_torque; #if 1 BOOST_LOG_TRIVIAL(debug) - << "pivot: " << pivot.x() << " " << pivot.y() << " " << pivot.z(); + << "centroid: " << centroid.x() << " " << centroid.y() << " " << centroid.z(); BOOST_LOG_TRIVIAL(debug) << "mass_centroid: " << mass_centroid.x() << " " << mass_centroid.y() << " " << mass_centroid.z(); BOOST_LOG_TRIVIAL(debug) - << "SSG: tensile_force: " << tensile_force; + << "variance: " << variance.x() << " " << variance.y(); BOOST_LOG_TRIVIAL(debug) - << "SSG: tensile_arm: " << tensile_arm; + << "SSG: elastic_section_modulus: " << elastic_section_modulus; BOOST_LOG_TRIVIAL(debug) - << "SSG: tensile_torque: " << tensile_torque; + << "SSG: yield_torque: " << yield_torque; BOOST_LOG_TRIVIAL(debug) << "SSG: weight_arm: " << weight_arm; BOOST_LOG_TRIVIAL(debug) @@ -809,8 +820,8 @@ Issues check_global_stability(SupportGridFilter supports_presence_grid, std::unordered_map prev_island_to_object_part_mapping; std::unordered_map next_island_to_object_part_mapping; - std::unordered_map prev_island_to_weakest_connection; - std::unordered_map next_island_to_weakest_connection; + std::unordered_map prev_island_to_weakest_connection; + std::unordered_map next_island_to_weakest_connection; for (size_t layer_idx = 0; layer_idx < islands_graph.size(); ++layer_idx) { float layer_z = islands_graph[layer_idx].layer_z; @@ -818,11 +829,7 @@ Issues check_global_stability(SupportGridFilter supports_presence_grid, std::cout << "at layer: " << layer_idx << " the following island to object mapping is used:" << std::endl; for (const auto &m : prev_island_to_object_part_mapping) { std::cout << "island " << m.first << " maps to part " << m.second << std::endl; - Vec3f connection_center = prev_island_to_weakest_connection[m.first].centroid_accumulator - / prev_island_to_weakest_connection[m.first].area; - std::cout << " island has weak point with connection area: " << - prev_island_to_weakest_connection[m.first].area << " and center: " << - connection_center.x() << " " << connection_center.y() << " " << connection_center.z() << std::endl; + prev_island_to_weakest_connection[m.first].print_info("connection info:"); } for (size_t island_idx = 0; island_idx < islands_graph[layer_idx].islands.size(); ++island_idx) { @@ -833,20 +840,19 @@ Issues check_global_stability(SupportGridFilter supports_presence_grid, active_object_parts.emplace(part_idx, ObjectPart(island)); next_island_to_object_part_mapping.emplace(island_idx, part_idx); next_island_to_weakest_connection.emplace(island_idx, - WeakestConnection { INFINITY, Vec3f::Zero() }); + IslandConnection { 1.0f, Vec3f::Zero(), Vec2f { INFINITY, INFINITY } }); layer_active_parts.insert(part_idx); } else { size_t final_part_idx { }; - WeakestConnection transfered_weakest_connection { }; - WeakestConnection new_weakest_connection { }; + IslandConnection transfered_weakest_connection { }; + IslandConnection new_weakest_connection { }; // MERGE parts { std::unordered_set part_indices; for (const auto &connection : island.connected_islands) { part_indices.insert(prev_island_to_object_part_mapping.at(connection.first)); transfered_weakest_connection.add(prev_island_to_weakest_connection.at(connection.first)); - new_weakest_connection.area += connection.second.area; - new_weakest_connection.centroid_accumulator += connection.second.centroid_accumulator; + new_weakest_connection.add(connection.second); } final_part_idx = *part_indices.begin(); for (size_t part_idx : part_indices) { @@ -858,22 +864,16 @@ Issues check_global_stability(SupportGridFilter supports_presence_grid, } } } - auto estimate_strength = [layer_z](const WeakestConnection &conn) { - float radius = fsqrt(conn.area / PI); - float arm_len_estimate = std::max(1.3f, layer_z - (conn.centroid_accumulator.z() / conn.area)); - return radius * conn.area / arm_len_estimate; + auto estimate_strength = [layer_z](const IslandConnection &conn) { + Vec3f centroid = conn.centroid_accumulator / conn.area; + float min_variance = (conn.second_moment_of_area_accumulator / conn.area + - centroid.head<2>().cwiseProduct(centroid.head<2>())).minCoeff(); + float arm_len_estimate = std::max(1.1f, layer_z - (conn.centroid_accumulator.z() / conn.area)); + return min_variance / arm_len_estimate; }; - std::cout << "new_weakest_connection info: " << std::endl; - std::cout << "area: " << new_weakest_connection.area << std::endl; - std::cout << "centroid acc: " << new_weakest_connection.centroid_accumulator.x() << " " - << new_weakest_connection.centroid_accumulator.y() << " " - << new_weakest_connection.centroid_accumulator.z() << std::endl; - std::cout << "transfered_weakest_connection info: " << std::endl; - std::cout << "area: " << transfered_weakest_connection.area << std::endl; - std::cout << "centroid acc: " << transfered_weakest_connection.centroid_accumulator.x() << " " - << transfered_weakest_connection.centroid_accumulator.y() << " " - << transfered_weakest_connection.centroid_accumulator.z() << std::endl; + new_weakest_connection.print_info("new_weakest_connection"); + transfered_weakest_connection.print_info("transfered_weakest_connection"); if (estimate_strength(transfered_weakest_connection) < estimate_strength(new_weakest_connection)) { new_weakest_connection = transfered_weakest_connection; @@ -911,20 +911,16 @@ Issues check_global_stability(SupportGridFilter supports_presence_grid, for (size_t island_idx = 0; island_idx < islands_graph[layer_idx].islands.size(); ++island_idx) { const Island &island = islands_graph[layer_idx].islands[island_idx]; ObjectPart &part = active_object_parts.at(prev_island_to_object_part_mapping[island_idx]); - WeakestConnection &weakest_conn = prev_island_to_weakest_connection[island_idx]; - std::cout << "Weakest connection info: " << std::endl; - std::cout << "area: " << weakest_conn.area << std::endl; - std::cout << "centroid acc: " << weakest_conn.centroid_accumulator.x() << " " - << weakest_conn.centroid_accumulator.y() << " " - << weakest_conn.centroid_accumulator.z() << std::endl; + IslandConnection &weakest_conn = prev_island_to_weakest_connection[island_idx]; + weakest_conn.print_info("weakest connection info: "); std::vector dummy { }; LinesDistancer island_lines_dist(dummy); float unchecked_dist = params.min_distance_between_support_points + 1.0f; for (const ExtrusionLine &line : island.external_lines) { - if (unchecked_dist + line.len < params.min_distance_between_support_points - && line.malformation < 0.3f) { + if ((unchecked_dist + line.len < params.min_distance_between_support_points + && line.malformation < 0.3f) || line.len == 0) { unchecked_dist += line.len; } else { unchecked_dist = line.len; @@ -953,6 +949,8 @@ Issues check_global_stability(SupportGridFilter supports_presence_grid, weakest_conn.area += area; weakest_conn.centroid_accumulator += support_point * area; + weakest_conn.second_moment_of_area_accumulator += area + * support_point.head<2>().cwiseProduct(support_point.head<2>()); } } } diff --git a/src/libslic3r/SupportSpotsGenerator.hpp b/src/libslic3r/SupportSpotsGenerator.hpp index 2a9b48fddc..15b5802895 100644 --- a/src/libslic3r/SupportSpotsGenerator.hpp +++ b/src/libslic3r/SupportSpotsGenerator.hpp @@ -19,11 +19,11 @@ struct Params { const float base_adhesion = 3.0f * gravity_constant; // adhesion per mm^2 of first layer const float support_adhesion = 1.0f * gravity_constant; // adhesion per mm^2 of support interface layer - const float support_points_interface_radius = 0.3f; // mm + const float support_points_interface_radius = 0.6f; // mm const float max_acceleration = 9*1000.0f; // mm/s^2 ; max acceleration of object (bed) in XY (NOTE: The max hit is received by the object in the jerk phase, so the usual machine limits are too low) const float filament_density = 1.25f * 0.001f; // g/mm^3 ; Common filaments are very lightweight, so precise number is not that important - const float tensile_strength = 33000.0f; // mN/mm^2; 33 MPa is tensile strength of ABS, which has the lowest tensile strength from common materials. + const float yield_strength = 33000.0f; // mN/mm^2; 33 MPa is yield strength of ABS, which has the lowest yield strength from common materials. const float tolerable_extruder_conflict_force = 50.0f * gravity_constant; // force that can occasionally push the model due to various factors (filament leaks, small curling, ... ); current value corresponds to weight of X grams const float malformations_additive_conflict_extruder_force = 100.0f * gravity_constant; // for areas with possible high layered curled filaments From 50e7cc9d4c95523341adefcb1a0f47eb182aaa70 Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Mon, 25 Jul 2022 12:32:48 +0200 Subject: [PATCH 053/100] fix bug with removed object parts which were still referenced by other islands --- src/libslic3r/SupportSpotsGenerator.cpp | 109 ++++++++++++++---------- 1 file changed, 65 insertions(+), 44 deletions(-) diff --git a/src/libslic3r/SupportSpotsGenerator.cpp b/src/libslic3r/SupportSpotsGenerator.cpp index 63f8f21000..b7067fd9c3 100644 --- a/src/libslic3r/SupportSpotsGenerator.cpp +++ b/src/libslic3r/SupportSpotsGenerator.cpp @@ -811,56 +811,90 @@ void debug_print_graph(const std::vector &islands_graph) { } +class ActiveObjectParts { + size_t next_part_idx = 0; + std::unordered_map active_object_parts; + std::unordered_map active_object_parts_id_mapping; + +public: + size_t get_flat_id(size_t id) { + size_t index = active_object_parts_id_mapping.at(id); + while (index != active_object_parts_id_mapping.at(index)) { + index = active_object_parts_id_mapping.at(index); + } + size_t i = id; + while (index != active_object_parts_id_mapping.at(i)) { + size_t next = active_object_parts_id_mapping[i]; + active_object_parts_id_mapping[i] = index; + i = next; + } + return index; + } + + ObjectPart& access(size_t id) { + return this->active_object_parts.at(this->get_flat_id(id)); + } + + size_t insert(const Island &island) { + this->active_object_parts.emplace(next_part_idx, ObjectPart(island)); + this->active_object_parts_id_mapping.emplace(next_part_idx, next_part_idx); + return next_part_idx++; + } + + void merge(size_t from, size_t to) { + size_t to_flat = this->get_flat_id(to); + size_t from_flat = this->get_flat_id(from); + active_object_parts.at(to_flat).add(active_object_parts.at(from_flat)); + active_object_parts.erase(from_flat); + active_object_parts_id_mapping[from] = to_flat; + } +}; + Issues check_global_stability(SupportGridFilter supports_presence_grid, const std::vector &islands_graph, const Params ¶ms) { debug_print_graph(islands_graph); Issues issues { }; - size_t next_part_idx = 0; - std::unordered_map active_object_parts; + ActiveObjectParts active_object_parts { }; std::unordered_map prev_island_to_object_part_mapping; std::unordered_map next_island_to_object_part_mapping; - std::unordered_map prev_island_to_weakest_connection; - std::unordered_map next_island_to_weakest_connection; + std::unordered_map prev_island_weakest_connection; + std::unordered_map next_island_weakest_connection; for (size_t layer_idx = 0; layer_idx < islands_graph.size(); ++layer_idx) { float layer_z = islands_graph[layer_idx].layer_z; - std::unordered_set layer_active_parts; std::cout << "at layer: " << layer_idx << " the following island to object mapping is used:" << std::endl; for (const auto &m : prev_island_to_object_part_mapping) { std::cout << "island " << m.first << " maps to part " << m.second << std::endl; - prev_island_to_weakest_connection[m.first].print_info("connection info:"); + prev_island_weakest_connection[m.first].print_info("connection info:"); } for (size_t island_idx = 0; island_idx < islands_graph[layer_idx].islands.size(); ++island_idx) { const Island &island = islands_graph[layer_idx].islands[island_idx]; if (island.connected_islands.empty()) { //new object part emerging - size_t part_idx = next_part_idx; - next_part_idx++; - active_object_parts.emplace(part_idx, ObjectPart(island)); - next_island_to_object_part_mapping.emplace(island_idx, part_idx); - next_island_to_weakest_connection.emplace(island_idx, + size_t part_id = active_object_parts.insert(island); + next_island_to_object_part_mapping.emplace(island_idx, part_id); + next_island_weakest_connection.emplace(island_idx, IslandConnection { 1.0f, Vec3f::Zero(), Vec2f { INFINITY, INFINITY } }); - layer_active_parts.insert(part_idx); } else { - size_t final_part_idx { }; + size_t final_part_id { }; IslandConnection transfered_weakest_connection { }; IslandConnection new_weakest_connection { }; // MERGE parts { - std::unordered_set part_indices; + std::unordered_set parts_ids; for (const auto &connection : island.connected_islands) { - part_indices.insert(prev_island_to_object_part_mapping.at(connection.first)); - transfered_weakest_connection.add(prev_island_to_weakest_connection.at(connection.first)); + size_t part_id = active_object_parts.get_flat_id(prev_island_to_object_part_mapping.at(connection.first)); + parts_ids.insert(part_id); + transfered_weakest_connection.add(prev_island_weakest_connection.at(connection.first)); new_weakest_connection.add(connection.second); } - final_part_idx = *part_indices.begin(); - for (size_t part_idx : part_indices) { - if (final_part_idx != part_idx) { - std::cout << "at layer: " << layer_idx << " merging object part: " << part_idx - << " into final part: " << final_part_idx << std::endl; - active_object_parts.at(final_part_idx).add(active_object_parts.at(part_idx)); - active_object_parts.erase(part_idx); + final_part_id = *parts_ids.begin(); + for (size_t part_id : parts_ids) { + if (final_part_id != part_id) { + std::cout << "at layer: " << layer_idx << " merging object part: " << part_id + << " into final part: " << final_part_id << std::endl; + active_object_parts.merge(part_id, final_part_id); } } } @@ -878,31 +912,17 @@ Issues check_global_stability(SupportGridFilter supports_presence_grid, if (estimate_strength(transfered_weakest_connection) < estimate_strength(new_weakest_connection)) { new_weakest_connection = transfered_weakest_connection; } - next_island_to_weakest_connection.emplace(island_idx, new_weakest_connection); - next_island_to_object_part_mapping.emplace(island_idx, final_part_idx); - ObjectPart &part = active_object_parts[final_part_idx]; + next_island_weakest_connection.emplace(island_idx, new_weakest_connection); + next_island_to_object_part_mapping.emplace(island_idx, final_part_id); + ObjectPart &part = active_object_parts.access(final_part_id); part.add(ObjectPart(island)); - layer_active_parts.insert(final_part_idx); } } - std::unordered_set parts_to_delete; - for (const auto &part : active_object_parts) { - if (layer_active_parts.find(part.first) == layer_active_parts.end()) { - parts_to_delete.insert(part.first); - } else { - std::cout << "at layer " << layer_idx << " part is still active: " << part.first << std::endl; - part.second.print(); - } - } - for (size_t part_id : parts_to_delete) { - active_object_parts.erase(part_id); - std::cout << " at layer: " << layer_idx << " removing object part " << part_id << std::endl; - } prev_island_to_object_part_mapping = next_island_to_object_part_mapping; next_island_to_object_part_mapping.clear(); - prev_island_to_weakest_connection = next_island_to_weakest_connection; - next_island_to_weakest_connection.clear(); + prev_island_weakest_connection = next_island_weakest_connection; + next_island_weakest_connection.clear(); // All object parts updated, inactive parts removed and weakest point of each island updated as well. // Now compute the stability of each active object part, adding supports where necessary, and also @@ -910,8 +930,9 @@ Issues check_global_stability(SupportGridFilter supports_presence_grid, for (size_t island_idx = 0; island_idx < islands_graph[layer_idx].islands.size(); ++island_idx) { const Island &island = islands_graph[layer_idx].islands[island_idx]; - ObjectPart &part = active_object_parts.at(prev_island_to_object_part_mapping[island_idx]); - IslandConnection &weakest_conn = prev_island_to_weakest_connection[island_idx]; + ObjectPart &part = active_object_parts.access(prev_island_to_object_part_mapping[island_idx]); + part.print(); + IslandConnection &weakest_conn = prev_island_weakest_connection[island_idx]; weakest_conn.print_info("weakest connection info: "); std::vector dummy { }; From 3f7f5ec0ede76a72fceb4f798fc4f4fd24017238 Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Mon, 25 Jul 2022 13:36:50 +0200 Subject: [PATCH 054/100] Lowered default extrusion conflict force - it probably needs more adjusting, after the bed adhesion is reworked with elastic section modulus --- src/libslic3r/SupportSpotsGenerator.cpp | 9 +++++---- src/libslic3r/SupportSpotsGenerator.hpp | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/libslic3r/SupportSpotsGenerator.cpp b/src/libslic3r/SupportSpotsGenerator.cpp index b7067fd9c3..28ea8d17c1 100644 --- a/src/libslic3r/SupportSpotsGenerator.cpp +++ b/src/libslic3r/SupportSpotsGenerator.cpp @@ -674,7 +674,7 @@ public: Vec3d endpoint = (to_3d(extruded_line.b, layer_z)).cast(); float conflict_torque_arm = line_alg::distance_to( Linef3(endpoint, endpoint + extruder_pressure_direction.cast()), pivot.cast()); - float extruder_conflict_force = params.tolerable_extruder_conflict_force + + float extruder_conflict_force = params.standard_extruder_conflict_force + std::min(extruded_line.malformation, 1.0f) * params.malformations_additive_conflict_extruder_force; float extruder_conflict_torque = extruder_conflict_force * conflict_torque_arm; @@ -721,6 +721,7 @@ public: Vec3f centroid = connection.centroid_accumulator / connection.area; Vec2f variance = (connection.second_moment_of_area_accumulator / connection.area - centroid.head<2>().cwiseProduct(centroid.head<2>())); + variance = variance.cwiseProduct(line_dir.cwiseAbs()); float extreme_fiber_dist = variance.cwiseSqrt().norm(); float elastic_section_modulus = connection.area * (variance.x() + variance.y()) / extreme_fiber_dist; float yield_torque = elastic_section_modulus * params.yield_strength; @@ -733,17 +734,17 @@ public: float weight_arm = (centroid.head<2>() - mass_centroid.head<2>()).norm(); float weight_torque = weight_arm * weight; - float bed_movement_arm = mass_centroid.z(); + float bed_movement_arm = std::max(0.0f, mass_centroid.z() - centroid.z()); float bed_movement_force = params.max_acceleration * mass; float bed_movement_torque = bed_movement_force * bed_movement_arm; Vec3f extruder_pressure_direction = to_3d(line_dir, 0.0f); - extruder_pressure_direction.z() = -0.1f - extruded_line.malformation * 0.5f; + extruder_pressure_direction.z() = -extruded_line.malformation * 0.5f; extruder_pressure_direction.normalize(); Vec3d endpoint = (to_3d(extruded_line.b, layer_z)).cast(); float conflict_torque_arm = line_alg::distance_to( Linef3(endpoint, endpoint + extruder_pressure_direction.cast()), centroid.cast()); - float extruder_conflict_force = params.tolerable_extruder_conflict_force + + float extruder_conflict_force = params.standard_extruder_conflict_force + std::min(extruded_line.malformation, 1.0f) * params.malformations_additive_conflict_extruder_force; float extruder_conflict_torque = extruder_conflict_force * conflict_torque_arm; diff --git a/src/libslic3r/SupportSpotsGenerator.hpp b/src/libslic3r/SupportSpotsGenerator.hpp index 15b5802895..0813663696 100644 --- a/src/libslic3r/SupportSpotsGenerator.hpp +++ b/src/libslic3r/SupportSpotsGenerator.hpp @@ -24,7 +24,7 @@ struct Params { const float max_acceleration = 9*1000.0f; // mm/s^2 ; max acceleration of object (bed) in XY (NOTE: The max hit is received by the object in the jerk phase, so the usual machine limits are too low) const float filament_density = 1.25f * 0.001f; // g/mm^3 ; Common filaments are very lightweight, so precise number is not that important const float yield_strength = 33000.0f; // mN/mm^2; 33 MPa is yield strength of ABS, which has the lowest yield strength from common materials. - const float tolerable_extruder_conflict_force = 50.0f * gravity_constant; // force that can occasionally push the model due to various factors (filament leaks, small curling, ... ); current value corresponds to weight of X grams + const float standard_extruder_conflict_force = 1.0f * gravity_constant; // force that can occasionally push the model due to various factors (filament leaks, small curling, ... ); current value corresponds to weight of X grams const float malformations_additive_conflict_extruder_force = 100.0f * gravity_constant; // for areas with possible high layered curled filaments }; From 2808e41238dded9c09841a252db3b56df0fd558d Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Mon, 25 Jul 2022 17:48:42 +0200 Subject: [PATCH 055/100] reworked bed adhesion model to use elastic section modulus fixed units updated bed adhesion value --- src/libslic3r/SupportSpotsGenerator.cpp | 317 ++++++++++-------------- src/libslic3r/SupportSpotsGenerator.hpp | 23 +- 2 files changed, 144 insertions(+), 196 deletions(-) diff --git a/src/libslic3r/SupportSpotsGenerator.cpp b/src/libslic3r/SupportSpotsGenerator.cpp index 28ea8d17c1..b313030ac2 100644 --- a/src/libslic3r/SupportSpotsGenerator.cpp +++ b/src/libslic3r/SupportSpotsGenerator.cpp @@ -272,11 +272,11 @@ struct IslandConnection { struct Island { std::unordered_map connected_islands { }; - std::vector pivot_points { }; // for support points present on this layer (or bed extrusions) float volume { }; Vec3f volume_centroid_accumulator = Vec3f::Zero(); - float sticking_force { }; // for support points present on this layer (or bed extrusions) + float sticking_area { }; // for support points present on this layer (or bed extrusions) Vec3f sticking_centroid_accumulator = Vec3f::Zero(); + Vec2f sticking_second_moment_of_area_accumulator = Vec2f::Zero(); std::vector external_lines; }; @@ -473,8 +473,8 @@ std::tuple reckon_islands( if (!layer_lines[extrusions[e].first].is_external_perimeter()) { bool island_assigned = false; for (size_t i = 0; i < islands.size(); ++i) { - size_t _idx; - Vec2f _pt; + size_t _idx = 0; + Vec2f _pt = Vec2f::Zero(); if (islands[i].signed_distance_from_lines(layer_lines[extrusions[e].first].a, _idx, _pt) < 0) { island_extrusions[i].push_back(e); island_assigned = true; @@ -530,22 +530,16 @@ std::tuple reckon_islands( * volume; if (first_layer) { - float sticking_force = line.len * flow_width * params.base_adhesion; - island.sticking_force += sticking_force; - island.sticking_centroid_accumulator += sticking_force - * to_3d(Vec2f((line.a + line.b) / 2.0f), float(layer->slice_z)); - if (line.is_external_perimeter()) { - island.pivot_points.push_back(to_3d(Vec2f(line.b), float(layer->slice_z))); - } + float sticking_area = line.len * flow_width; + island.sticking_area += sticking_area; + Vec2f middle = Vec2f((line.a + line.b) / 2.0f); + island.sticking_centroid_accumulator += sticking_area * to_3d(middle, float(layer->slice_z)); + island.sticking_second_moment_of_area_accumulator += sticking_area * middle.cwiseProduct(middle); } else if (layer_lines[lidx].support_point_generated) { - float support_interface_area = params.support_points_interface_radius - * params.support_points_interface_radius - * float(PI); - float sticking_force = support_interface_area * params.support_adhesion; - island.sticking_force += sticking_force; - island.sticking_centroid_accumulator += sticking_force - * to_3d(Vec2f(line.b), float(layer->slice_z)); - island.pivot_points.push_back(to_3d(Vec2f(line.b), float(layer->slice_z))); + float sticking_area = line.len * flow_width; + island.sticking_area += sticking_area; + island.sticking_centroid_accumulator += sticking_area * to_3d(line.b, float(layer->slice_z)); + island.sticking_second_moment_of_area_accumulator += sticking_area * line.b.cwiseProduct(line.b); } } } @@ -604,194 +598,157 @@ struct CoordinateFunctor { class ObjectPart { float volume { }; Vec3f volume_centroid_accumulator = Vec3f::Zero(); - float sticking_force { }; + float sticking_area { }; Vec3f sticking_centroid_accumulator = Vec3f::Zero(); - std::vector pivot_points { }; - - CoordinateFunctor pivots_coordinate_functor; - bool is_pivot_tree_valid = false; - KDTreeIndirect<3, float, CoordinateFunctor> pivot_tree { CoordinateFunctor { } }; - - void check_pivot_tree() { - if (!is_pivot_tree_valid) { - this->pivots_coordinate_functor = CoordinateFunctor(&this->pivot_points); - this->pivot_tree = { this->pivots_coordinate_functor }; - pivot_tree.build(pivot_points.size()); - is_pivot_tree_valid = true; - } - } + Vec2f sticking_second_moment_of_area_accumulator = Vec2f::Zero(); public: - void add(const ObjectPart &other) { - this->volume_centroid_accumulator += other.volume_centroid_accumulator; - this->volume += other.volume; - this->sticking_force += other.sticking_force; - this->sticking_centroid_accumulator += other.sticking_centroid_accumulator; - this->pivot_points.insert(this->pivot_points.end(), other.pivot_points.begin(), other.pivot_points.end()); - this->is_pivot_tree_valid = this->is_pivot_tree_valid && other.pivot_points.empty(); - } + ObjectPart() = default; ObjectPart(const Island &island) { this->volume = island.volume; this->volume_centroid_accumulator = island.volume_centroid_accumulator; - this->sticking_force = island.sticking_force; + this->sticking_area = island.sticking_area; this->sticking_centroid_accumulator = island.sticking_centroid_accumulator; - this->pivot_points = island.pivot_points; + this->sticking_second_moment_of_area_accumulator = island.sticking_second_moment_of_area_accumulator; } - ObjectPart() = default; - - std::tuple is_stable_while_extruding(const ExtrusionLine &extruded_line, float layer_z, - const Params ¶ms) { - if (pivot_points.empty()) { - return {this->volume * params.filament_density*params.gravity_constant,Vec3f {0.0f,0.0f,-1.0f}}; - } - - check_pivot_tree(); - - Vec2f line_dir = (extruded_line.b - extruded_line.a).normalized(); - Vec3f pivot_site_search_point = to_3d(Vec2f(extruded_line.b + line_dir * 300.0f), layer_z); - size_t pivot_idx = find_closest_point(this->pivot_tree, pivot_site_search_point); - const Vec3f &pivot = pivot_points[pivot_idx]; - - const Vec3f &sticking_centroid = this->sticking_centroid_accumulator / this->sticking_force; - float sticking_arm = (pivot - sticking_centroid).norm(); - float sticking_torque = sticking_arm * this->sticking_force; - - float mass = this->volume * params.filament_density; - const Vec3f &mass_centroid = this->volume_centroid_accumulator / this->volume; - float weight = mass * params.gravity_constant; - float weight_arm = (pivot.head<2>() - mass_centroid.head<2>()).norm(); - float weight_torque = weight_arm * weight; - - float bed_movement_arm = mass_centroid.z(); - float bed_movement_force = params.max_acceleration * mass; - float bed_movement_torque = bed_movement_force * bed_movement_arm; - - Vec3f extruder_pressure_direction = to_3d(line_dir, 0.0f); - extruder_pressure_direction.z() = -0.1f - extruded_line.malformation * 0.5f; - extruder_pressure_direction.normalize(); - Vec3d endpoint = (to_3d(extruded_line.b, layer_z)).cast(); - float conflict_torque_arm = line_alg::distance_to( - Linef3(endpoint, endpoint + extruder_pressure_direction.cast()), pivot.cast()); - float extruder_conflict_force = params.standard_extruder_conflict_force + - std::min(extruded_line.malformation, 1.0f) * params.malformations_additive_conflict_extruder_force; - float extruder_conflict_torque = extruder_conflict_force * conflict_torque_arm; - - float total_torque = bed_movement_torque + extruder_conflict_torque - weight_torque - sticking_torque; - -#if 1 - BOOST_LOG_TRIVIAL(debug) - << "pivot: " << pivot.x() << " " << pivot.y() << " " << pivot.z(); - BOOST_LOG_TRIVIAL(debug) - << "sticking_centroid: " << sticking_centroid.x() << " " << sticking_centroid.y() << " " - << sticking_centroid.z(); - BOOST_LOG_TRIVIAL(debug) - << "SSG: sticking_force: " << sticking_force; - BOOST_LOG_TRIVIAL(debug) - << "SSG: sticking_arm: " << sticking_arm; - BOOST_LOG_TRIVIAL(debug) - << "SSG: sticking_torque: " << sticking_torque; - BOOST_LOG_TRIVIAL(debug) - << "SSG: weight_arm: " << weight_arm; - BOOST_LOG_TRIVIAL(debug) - << "SSG: weight_torque: " << weight_torque; - BOOST_LOG_TRIVIAL(debug) - << "SSG: bed_movement_arm: " << bed_movement_arm; - BOOST_LOG_TRIVIAL(debug) - << "SSG: bed_movement_torque: " << bed_movement_torque; - BOOST_LOG_TRIVIAL(debug) - << "SSG: conflict_torque_arm: " << conflict_torque_arm; - BOOST_LOG_TRIVIAL(debug) - << "SSG: extruder_conflict_torque: " << extruder_conflict_torque; - BOOST_LOG_TRIVIAL(debug) - << "SSG: total_torque: " << total_torque << " layer_z: " << layer_z; -#endif - - return {total_torque / conflict_torque_arm, pivot_site_search_point}; + void add(const ObjectPart &other) { + this->volume_centroid_accumulator += other.volume_centroid_accumulator; + this->volume += other.volume; + this->sticking_area += other.sticking_area; + this->sticking_centroid_accumulator += other.sticking_centroid_accumulator; + this->sticking_second_moment_of_area_accumulator += other.sticking_second_moment_of_area_accumulator; } - float is_strong_enough_while_extruding( + void add_support_point(const Vec3f &position, float sticking_area) { + this->sticking_area += sticking_area; + this->sticking_centroid_accumulator += sticking_area * position; + this->sticking_second_moment_of_area_accumulator += sticking_area + * position.head<2>().cwiseProduct(position.head<2>()); + } + + float is_stable_while_extruding( const IslandConnection &connection, const ExtrusionLine &extruded_line, float layer_z, const Params ¶ms) const { Vec2f line_dir = (extruded_line.b - extruded_line.a).normalized(); - Vec3f centroid = connection.centroid_accumulator / connection.area; - Vec2f variance = (connection.second_moment_of_area_accumulator / connection.area - - centroid.head<2>().cwiseProduct(centroid.head<2>())); - variance = variance.cwiseProduct(line_dir.cwiseAbs()); - float extreme_fiber_dist = variance.cwiseSqrt().norm(); - float elastic_section_modulus = connection.area * (variance.x() + variance.y()) / extreme_fiber_dist; - float yield_torque = elastic_section_modulus * params.yield_strength; + + auto compute_elastic_section_modulus = [&line_dir]( + const Vec3f ¢roid_accumulator, const Vec2f &second_moment_of_area_accumulator, const float &area) { + Vec3f centroid = centroid_accumulator / area; + Vec2f variance = (second_moment_of_area_accumulator / area + - centroid.head<2>().cwiseProduct(centroid.head<2>())); + variance = variance.cwiseProduct(line_dir.cwiseAbs()); + float extreme_fiber_dist = variance.cwiseSqrt().norm(); + float elastic_section_modulus = area * (variance.x() + variance.y()) / extreme_fiber_dist; + return elastic_section_modulus; + }; const Vec3f &mass_centroid = this->volume_centroid_accumulator / this->volume; - float mass = this->volume * params.filament_density - * ((2.0f * layer_z - centroid.z() - mass_centroid.z()) / (2.0f * layer_z)); - + float mass = this->volume * params.filament_density; float weight = mass * params.gravity_constant; - float weight_arm = (centroid.head<2>() - mass_centroid.head<2>()).norm(); - float weight_torque = weight_arm * weight; - float bed_movement_arm = std::max(0.0f, mass_centroid.z() - centroid.z()); - float bed_movement_force = params.max_acceleration * mass; - float bed_movement_torque = bed_movement_force * bed_movement_arm; + float movement_force = params.max_acceleration * mass; Vec3f extruder_pressure_direction = to_3d(line_dir, 0.0f); extruder_pressure_direction.z() = -extruded_line.malformation * 0.5f; extruder_pressure_direction.normalize(); Vec3d endpoint = (to_3d(extruded_line.b, layer_z)).cast(); - float conflict_torque_arm = line_alg::distance_to( - Linef3(endpoint, endpoint + extruder_pressure_direction.cast()), centroid.cast()); float extruder_conflict_force = params.standard_extruder_conflict_force + std::min(extruded_line.malformation, 1.0f) * params.malformations_additive_conflict_extruder_force; - float extruder_conflict_torque = extruder_conflict_force * conflict_torque_arm; - float total_torque = bed_movement_torque + extruder_conflict_torque + weight_torque - yield_torque; + // section for bed calculations + { + Vec3f bed_centroid = this->sticking_centroid_accumulator / this->sticking_area; + float bed_yield_torque = compute_elastic_section_modulus(this->sticking_centroid_accumulator, + this->sticking_second_moment_of_area_accumulator, this->sticking_area) + * params.bed_adhesion_yield_strength; + + float bed_weight_arm = (bed_centroid.head<2>() - mass_centroid.head<2>()).norm(); + float bed_weight_torque = bed_weight_arm * weight; + + float bed_movement_arm = std::max(0.0f, mass_centroid.z() - bed_centroid.z()); + float bed_movement_torque = movement_force * bed_movement_arm; + + float bed_conflict_torque_arm = line_alg::distance_to( + Linef3(endpoint, endpoint + extruder_pressure_direction.cast()), + bed_centroid.cast()); + float bed_extruder_conflict_torque = extruder_conflict_force * bed_conflict_torque_arm; + + float bed_total_torque = bed_movement_torque + bed_extruder_conflict_torque + bed_weight_torque + - bed_yield_torque; #if 1 - BOOST_LOG_TRIVIAL(debug) - << "centroid: " << centroid.x() << " " << centroid.y() << " " << centroid.z(); - BOOST_LOG_TRIVIAL(debug) - << "mass_centroid: " << mass_centroid.x() << " " << mass_centroid.y() << " " - << mass_centroid.z(); - BOOST_LOG_TRIVIAL(debug) - << "variance: " << variance.x() << " " << variance.y(); - BOOST_LOG_TRIVIAL(debug) - << "SSG: elastic_section_modulus: " << elastic_section_modulus; - BOOST_LOG_TRIVIAL(debug) - << "SSG: yield_torque: " << yield_torque; - BOOST_LOG_TRIVIAL(debug) - << "SSG: weight_arm: " << weight_arm; - BOOST_LOG_TRIVIAL(debug) - << "SSG: weight_torque: " << weight_torque; - BOOST_LOG_TRIVIAL(debug) - << "SSG: bed_movement_arm: " << bed_movement_arm; - BOOST_LOG_TRIVIAL(debug) - << "SSG: bed_movement_torque: " << bed_movement_torque; - BOOST_LOG_TRIVIAL(debug) - << "SSG: conflict_torque_arm: " << conflict_torque_arm; - BOOST_LOG_TRIVIAL(debug) - << "SSG: extruder_conflict_torque: " << extruder_conflict_torque; - BOOST_LOG_TRIVIAL(debug) - << "SSG: total_torque: " << total_torque << " layer_z: " << layer_z; + BOOST_LOG_TRIVIAL(debug) + << "bed_centroid: " << bed_centroid.x() << " " << bed_centroid.y() << " " << bed_centroid.z(); + BOOST_LOG_TRIVIAL(debug) + << "SSG: bed_yield_torque: " << bed_yield_torque; + BOOST_LOG_TRIVIAL(debug) + << "SSG: bed_weight_arm: " << bed_weight_arm; + BOOST_LOG_TRIVIAL(debug) + << "SSG: bed_weight_torque: " << bed_weight_torque; + BOOST_LOG_TRIVIAL(debug) + << "SSG: bed_movement_arm: " << bed_movement_arm; + BOOST_LOG_TRIVIAL(debug) + << "SSG: bed_movement_torque: " << bed_movement_torque; + BOOST_LOG_TRIVIAL(debug) + << "SSG: bed_conflict_torque_arm: " << bed_conflict_torque_arm; + BOOST_LOG_TRIVIAL(debug) + << "SSG: bed_extruder_conflict_torque: " << bed_extruder_conflict_torque; + BOOST_LOG_TRIVIAL(debug) + << "SSG: total_torque: " << bed_total_torque << " layer_z: " << layer_z; #endif - return total_torque / conflict_torque_arm; - } + if (bed_total_torque > 0) + return bed_total_torque / bed_conflict_torque_arm; + } - void add_pivot_point(const Vec3f pivot_point, float sticking_force) { - this->pivot_points.push_back(pivot_point); - this->sticking_force += sticking_force; - this->sticking_centroid_accumulator += sticking_force * pivot_point; - this->is_pivot_tree_valid = false; - } + //section for weak connection calculations + { + Vec3f conn_centroid = connection.centroid_accumulator / connection.area; + float conn_yield_torque = compute_elastic_section_modulus(connection.centroid_accumulator, + connection.second_moment_of_area_accumulator, connection.area) * params.material_yield_strength; - void print() const { - std::cout << "sticking_force: " << sticking_force << std::endl; - std::cout << "volume: " << volume << std::endl; - } + float conn_weight_arm = (conn_centroid.head<2>() - mass_centroid.head<2>()).norm(); + float conn_weight_torque = conn_weight_arm * weight * (conn_centroid.z() / layer_z); + float conn_movement_arm = std::max(0.0f, mass_centroid.z() - conn_centroid.z()); + float conn_movement_torque = movement_force * conn_movement_arm; + + float conn_conflict_torque_arm = line_alg::distance_to( + Linef3(endpoint, endpoint + extruder_pressure_direction.cast()), + conn_centroid.cast()); + float conn_extruder_conflict_torque = extruder_conflict_force * conn_conflict_torque_arm; + + float conn_total_torque = conn_movement_torque + conn_extruder_conflict_torque + conn_weight_torque + - conn_yield_torque; + +#if 1 + BOOST_LOG_TRIVIAL(debug) + << "bed_centroid: " << conn_centroid.x() << " " << conn_centroid.y() << " " << conn_centroid.z(); + BOOST_LOG_TRIVIAL(debug) + << "SSG: conn_yield_torque: " << conn_yield_torque; + BOOST_LOG_TRIVIAL(debug) + << "SSG: conn_weight_arm: " << conn_weight_arm; + BOOST_LOG_TRIVIAL(debug) + << "SSG: conn_weight_torque: " << conn_weight_torque; + BOOST_LOG_TRIVIAL(debug) + << "SSG: conn_movement_arm: " << conn_movement_arm; + BOOST_LOG_TRIVIAL(debug) + << "SSG: conn_movement_torque: " << conn_movement_torque; + BOOST_LOG_TRIVIAL(debug) + << "SSG: conn_conflict_torque_arm: " << conn_conflict_torque_arm; + BOOST_LOG_TRIVIAL(debug) + << "SSG: conn_extruder_conflict_torque: " << conn_extruder_conflict_torque; + BOOST_LOG_TRIVIAL(debug) + << "SSG: total_torque: " << conn_total_torque << " layer_z: " << layer_z; +#endif + + return conn_total_torque / conn_conflict_torque_arm; + } + } }; void debug_print_graph(const std::vector &islands_graph) { @@ -803,8 +760,7 @@ void debug_print_graph(const std::vector &islands_graph) { const Island &island = islands_graph[layer_idx].islands[island_idx]; std::cout << " ISLAND " << island_idx << std::endl; std::cout << " volume: " << island.volume << std::endl; - std::cout << " sticking_force: " << island.sticking_force << std::endl; - std::cout << " pivot_points count: " << island.pivot_points.size() << std::endl; + std::cout << " sticking_area: " << island.sticking_area << std::endl; std::cout << " connected_islands count: " << island.connected_islands.size() << std::endl; } } @@ -885,7 +841,8 @@ Issues check_global_stability(SupportGridFilter supports_presence_grid, { std::unordered_set parts_ids; for (const auto &connection : island.connected_islands) { - size_t part_id = active_object_parts.get_flat_id(prev_island_to_object_part_mapping.at(connection.first)); + size_t part_id = active_object_parts.get_flat_id( + prev_island_to_object_part_mapping.at(connection.first)); parts_ids.insert(part_id); transfered_weakest_connection.add(prev_island_weakest_connection.at(connection.first)); new_weakest_connection.add(connection.second); @@ -932,7 +889,6 @@ Issues check_global_stability(SupportGridFilter supports_presence_grid, for (size_t island_idx = 0; island_idx < islands_graph[layer_idx].islands.size(); ++island_idx) { const Island &island = islands_graph[layer_idx].islands[island_idx]; ObjectPart &part = active_object_parts.access(prev_island_to_object_part_mapping[island_idx]); - part.print(); IslandConnection &weakest_conn = prev_island_weakest_connection[island_idx]; weakest_conn.print_info("weakest connection info: "); @@ -946,25 +902,22 @@ Issues check_global_stability(SupportGridFilter supports_presence_grid, unchecked_dist += line.len; } else { unchecked_dist = line.len; - auto [force, pivot_site_search_point] = part.is_stable_while_extruding(line, layer_z, params); - if (force <= 0) { - force = part.is_strong_enough_while_extruding(weakest_conn, line, layer_z, params); - } - + auto force = part.is_stable_while_extruding(weakest_conn, line, layer_z, params); if (force > 0) { if (island_lines_dist.get_lines().empty()) { island_lines_dist = LinesDistancer(island.external_lines); } Vec2f target_point; size_t _idx; + Vec3f pivot_site_search_point = to_3d(Vec2f(line.b + (line.b - line.a).normalized() * 300.0f), + layer_z); island_lines_dist.signed_distance_from_lines(pivot_site_search_point.head<2>(), _idx, target_point); Vec3f support_point = to_3d(target_point, layer_z); if (!supports_presence_grid.position_taken(support_point)) { float area = params.support_points_interface_radius * params.support_points_interface_radius * float(PI); - float sticking_force = area * params.support_adhesion; - part.add_pivot_point(support_point, sticking_force); + part.add_support_point(support_point, area); issues.support_points.emplace_back(support_point, force, to_3d(Vec2f(line.b - line.a).normalized(), 0.0f)); supports_presence_grid.take_position(support_point); diff --git a/src/libslic3r/SupportSpotsGenerator.hpp b/src/libslic3r/SupportSpotsGenerator.hpp index 0813663696..531f2768bf 100644 --- a/src/libslic3r/SupportSpotsGenerator.hpp +++ b/src/libslic3r/SupportSpotsGenerator.hpp @@ -8,29 +8,24 @@ namespace Slic3r { namespace SupportSpotsGenerator { struct Params { - const float gravity_constant = 9806.65f; // mm/s^2; gravity acceleration on Earth's surface, algorithm assumes that printer is in upwards position. - + // the algorithm should use the following units for all computations: distance [mm], mass [g], time [s], force [N] const float bridge_distance = 12.0f; //mm const float bridge_distance_decrease_by_curvature_factor = 5.0f; // allowed bridge distance = bridge_distance / (this factor * (curvature / PI) ) - const float min_distance_between_support_points = 3.0f; - - // Adhesion computation : from experiment, PLA holds about 3g per mm^2 of base area (with reserve); So it can withstand about 3*gravity_constant force per mm^2 - const float base_adhesion = 3.0f * gravity_constant; // adhesion per mm^2 of first layer - const float support_adhesion = 1.0f * gravity_constant; // adhesion per mm^2 of support interface layer - + const float min_distance_between_support_points = 3.0f; //mm const float support_points_interface_radius = 0.6f; // mm + const float gravity_constant = 9806.65f; // mm/s^2; gravity acceleration on Earth's surface, algorithm assumes that printer is in upwards position. const float max_acceleration = 9*1000.0f; // mm/s^2 ; max acceleration of object (bed) in XY (NOTE: The max hit is received by the object in the jerk phase, so the usual machine limits are too low) - const float filament_density = 1.25f * 0.001f; // g/mm^3 ; Common filaments are very lightweight, so precise number is not that important - const float yield_strength = 33000.0f; // mN/mm^2; 33 MPa is yield strength of ABS, which has the lowest yield strength from common materials. - const float standard_extruder_conflict_force = 1.0f * gravity_constant; // force that can occasionally push the model due to various factors (filament leaks, small curling, ... ); current value corresponds to weight of X grams - const float malformations_additive_conflict_extruder_force = 100.0f * gravity_constant; // for areas with possible high layered curled filaments - + const float filament_density = 1.25e-3f ; // g/mm^3 ; Common filaments are very lightweight, so precise number is not that important + const float bed_adhesion_yield_strength = 0.128f * 1e6f; //MPa * 1e^6 = (g*mm/s^2)/mm^2 = g/(mm*s^2); yield strength of the bed surface + const float material_yield_strength = 33.0f * 1e6f; // (g*mm/s^2)/mm^2; 33 MPa is yield strength of ABS, which has the lowest yield strength from common materials. + const float standard_extruder_conflict_force = 20.0f * gravity_constant; // force that can occasionally push the model due to various factors (filament leaks, small curling, ... ); + const float malformations_additive_conflict_extruder_force = 300.0f * gravity_constant; // for areas with possible high layered curled filaments }; struct SupportPoint { - SupportPoint(const Vec3f &position, float force,const Vec3f& direction); + SupportPoint(const Vec3f &position, float force, const Vec3f& direction); Vec3f position; float force; Vec3f direction; From cdf68039f7281207ecc4c1257b1afda8b66108e6 Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Tue, 26 Jul 2022 10:30:13 +0200 Subject: [PATCH 056/100] fixed bug with zero area section modulus returning nans --- src/libslic3r/SupportSpotsGenerator.cpp | 1 + src/libslic3r/SupportSpotsGenerator.hpp | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libslic3r/SupportSpotsGenerator.cpp b/src/libslic3r/SupportSpotsGenerator.cpp index b313030ac2..a0b84b285c 100644 --- a/src/libslic3r/SupportSpotsGenerator.cpp +++ b/src/libslic3r/SupportSpotsGenerator.cpp @@ -638,6 +638,7 @@ public: auto compute_elastic_section_modulus = [&line_dir]( const Vec3f ¢roid_accumulator, const Vec2f &second_moment_of_area_accumulator, const float &area) { + if (area < EPSILON) return 0.0f; Vec3f centroid = centroid_accumulator / area; Vec2f variance = (second_moment_of_area_accumulator / area - centroid.head<2>().cwiseProduct(centroid.head<2>())); diff --git a/src/libslic3r/SupportSpotsGenerator.hpp b/src/libslic3r/SupportSpotsGenerator.hpp index 531f2768bf..7be312bcb4 100644 --- a/src/libslic3r/SupportSpotsGenerator.hpp +++ b/src/libslic3r/SupportSpotsGenerator.hpp @@ -10,7 +10,7 @@ namespace SupportSpotsGenerator { struct Params { // the algorithm should use the following units for all computations: distance [mm], mass [g], time [s], force [N] const float bridge_distance = 12.0f; //mm - const float bridge_distance_decrease_by_curvature_factor = 5.0f; // allowed bridge distance = bridge_distance / (this factor * (curvature / PI) ) + const float bridge_distance_decrease_by_curvature_factor = 3.0f; // allowed bridge distance = bridge_distance / (this factor * (curvature / PI) ) const float min_distance_between_support_points = 3.0f; //mm const float support_points_interface_radius = 0.6f; // mm From 90e77f9135f55e57cd12a6d9b25cdeb1598bcdfc Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Tue, 26 Jul 2022 14:45:36 +0200 Subject: [PATCH 057/100] integration into FDM supports painter gizmo --- src/libslic3r/PrintObject.cpp | 7 +- src/libslic3r/SupportSpotsGenerator.hpp | 2 +- src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp | 97 +++++++++++++++++++- src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp | 4 + 4 files changed, 100 insertions(+), 10 deletions(-) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index b5848cabac..0f85891ee2 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -398,7 +398,6 @@ void PrintObject::ironing() } } - void PrintObject::generate_support_spots() { if (this->set_started(posSupportSpotsSearch)) { @@ -421,8 +420,6 @@ void PrintObject::generate_support_spots() } } else { SupportSpotsGenerator::Issues issues = SupportSpotsGenerator::full_search(this); - //TODO fix -// if (!issues.supports_nedded.empty()) { auto obj_transform = this->trafo_centered(); for (ModelVolume *model_volume : this->model_object()->volumes) { if (model_volume->type() == ModelVolumeType::MODEL_PART) { @@ -438,7 +435,7 @@ void PrintObject::generate_support_spots() } model_volume->supported_facets.set(selector.selector); -#if 1 +#if 0 //DEBUG export indexed_triangle_set copy = model_volume->mesh().its; its_transform(copy, obj_transform * model_transformation); its_write_obj(copy, @@ -446,7 +443,6 @@ void PrintObject::generate_support_spots() #endif } } -// } } m_print->throw_if_canceled(); @@ -456,7 +452,6 @@ void PrintObject::generate_support_spots() } } - void PrintObject::generate_support_material() { if (this->set_started(posSupportMaterial)) { diff --git a/src/libslic3r/SupportSpotsGenerator.hpp b/src/libslic3r/SupportSpotsGenerator.hpp index 7be312bcb4..cc11c27c7e 100644 --- a/src/libslic3r/SupportSpotsGenerator.hpp +++ b/src/libslic3r/SupportSpotsGenerator.hpp @@ -8,7 +8,7 @@ namespace Slic3r { namespace SupportSpotsGenerator { struct Params { - // the algorithm should use the following units for all computations: distance [mm], mass [g], time [s], force [N] + // the algorithm should use the following units for all computations: distance [mm], mass [g], time [s], force [g*mm/s^2] const float bridge_distance = 12.0f; //mm const float bridge_distance_decrease_by_curvature_factor = 3.0f; // allowed bridge distance = bridge_distance / (this factor * (curvature / PI) ) diff --git a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp index 49e97ee1f1..36f48d0087 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp @@ -10,6 +10,8 @@ #include "slic3r/GUI/GUI_ObjectList.hpp" #include "slic3r/GUI/format.hpp" #include "slic3r/Utils/UndoRedo.hpp" +#include "libslic3r/Print.hpp" +#include "slic3r/GUI/MsgDialog.hpp" #include @@ -39,6 +41,7 @@ bool GLGizmoFdmSupports::on_init() { m_shortcut_key = WXK_CONTROL_L; + m_desc["auto_generate"] = _L("Auto-generate supports"); m_desc["clipping_of_view"] = _L("Clipping of view") + ": "; m_desc["reset_direction"] = _L("Reset direction"); m_desc["cursor_size"] = _L("Brush size") + ": "; @@ -91,7 +94,7 @@ void GLGizmoFdmSupports::on_render_input_window(float x, float y, float bottom_l if (! m_c->selection_info()->model_object()) return; - const float approx_height = m_imgui->scaled(23.f); + const float approx_height = m_imgui->scaled(25.f); y = std::min(y, bottom_limit - approx_height); m_imgui->set_next_window_pos(x, y, ImGuiCond_Always); @@ -153,6 +156,12 @@ void GLGizmoFdmSupports::on_render_input_window(float x, float y, float bottom_l ImGui::Separator(); + bool generate = m_imgui->button(m_desc.at("auto_generate")); + if (generate) + auto_generate(); + + ImGui::Separator(); + float position_before_text_y = ImGui::GetCursorPos().y; ImGui::AlignTextToFramePadding(); m_imgui->text_wrapped(m_desc["highlight_by_angle"] + ":", autoset_slider_label_max_width); @@ -369,6 +378,50 @@ void GLGizmoFdmSupports::select_facets_by_angle(float threshold_deg, bool block) m_parent.set_as_dirty(); } +void GLGizmoFdmSupports::data_changed() +{ + GLGizmoPainterBase::data_changed(); + if (! m_c->selection_info()) + return; + + ModelObject* mo = m_c->selection_info()->model_object(); + if (mo && this->waiting_for_autogenerated_supports) { + get_data_from_backend(); + } +} + +void GLGizmoFdmSupports::get_data_from_backend() +{ + if (! has_backend_supports()) + return; + ModelObject* mo = m_c->selection_info()->model_object(); + + // find the respective PrintObject, we need a pointer to it + for (const PrintObject* po : m_parent.fff_print()->objects()) { + if (po->model_object()->id() == mo->id()) { + std::unordered_map mvs; + for (const ModelVolume* mv : po->model_object()->volumes) { + if (mv->is_model_part()) { + mvs.emplace(mv->id().id, mv); + } + } + // NOTE: Copying the data into ModelVolumes stops the background processing. + int mesh_id = -1.0f; + for (ModelVolume* mv : mo->volumes){ + if (mv->is_model_part()){ + mesh_id++; + mv->supported_facets.assign(mvs[mv->id().id]->supported_facets); + m_triangle_selectors[mesh_id]->deserialize(mv->supported_facets.get_data(), true); + m_triangle_selectors[mesh_id]->request_update_render_data(); + } + } + this->waiting_for_autogenerated_supports = false; + m_parent.post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS)); + m_parent.set_as_dirty(); + + } + } +} void GLGizmoFdmSupports::update_model_object() const @@ -391,8 +444,6 @@ void GLGizmoFdmSupports::update_model_object() const } } - - void GLGizmoFdmSupports::update_from_model_object() { wxBusyCursor wait; @@ -417,6 +468,46 @@ void GLGizmoFdmSupports::update_from_model_object() } } +bool GLGizmoFdmSupports::has_backend_supports() const +{ + const ModelObject* mo = m_c->selection_info()->model_object(); + if (! mo) + return false; + + // find SlaPrintObject with this ID + for (const PrintObject* po : m_parent.fff_print()->objects()) { + if (po->model_object()->id() == mo->id()) + return po->is_step_done(posSupportSpotsSearch); + } + return false; +} + +void GLGizmoFdmSupports::reslice_FDM_supports(bool postpone_error_messages) const { + wxGetApp().CallAfter( + [this, postpone_error_messages]() { + wxGetApp().plater()->reslice_FFF_until_step(posSupportSpotsSearch, + *m_c->selection_info()->model_object(), postpone_error_messages); + }); +} + +void GLGizmoFdmSupports::auto_generate() +{ + ModelObject *mo = m_c->selection_info()->model_object(); + bool not_painted = std::all_of(mo->volumes.begin(), mo->volumes.end(), [](const ModelVolume* vol){ + return vol->type() != ModelVolumeType::MODEL_PART || vol->supported_facets.empty(); + }); + + MessageDialog dlg(GUI::wxGetApp().plater(), + _L("Autogeneration will erase all currently painted areas.") + "\n\n" + + _L("Are you sure you want to do it?") + "\n", + _L("Warning"), wxICON_WARNING | wxYES | wxNO); + + if (not_painted || dlg.ShowModal() == wxID_YES) { + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Autogenerate support points")); + this->waiting_for_autogenerated_supports = true; + wxGetApp().CallAfter([this]() { reslice_FDM_supports(); }); + } +} PainterGizmoType GLGizmoFdmSupports::get_painter_type() const diff --git a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp index f4c21a174b..2820298b6f 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp @@ -26,6 +26,7 @@ protected: private: bool on_init() override; + void data_changed() override; void update_model_object() const override; void update_from_model_object() override; @@ -41,8 +42,11 @@ private: std::map m_desc; + bool waiting_for_autogenerated_supports = false; bool has_backend_supports() const; void reslice_FDM_supports(bool postpone_error_messages = false) const; + void auto_generate(); + void get_data_from_backend(); }; From 9cfde724f100a01543b6e2421c9021bdcf794019 Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Tue, 26 Jul 2022 16:50:08 +0200 Subject: [PATCH 058/100] fix numerical issues in stability calculations --- src/libslic3r/SupportSpotsGenerator.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/libslic3r/SupportSpotsGenerator.cpp b/src/libslic3r/SupportSpotsGenerator.cpp index a0b84b285c..6a1aada0a1 100644 --- a/src/libslic3r/SupportSpotsGenerator.cpp +++ b/src/libslic3r/SupportSpotsGenerator.cpp @@ -638,12 +638,14 @@ public: auto compute_elastic_section_modulus = [&line_dir]( const Vec3f ¢roid_accumulator, const Vec2f &second_moment_of_area_accumulator, const float &area) { - if (area < EPSILON) return 0.0f; Vec3f centroid = centroid_accumulator / area; Vec2f variance = (second_moment_of_area_accumulator / area - centroid.head<2>().cwiseProduct(centroid.head<2>())); variance = variance.cwiseProduct(line_dir.cwiseAbs()); float extreme_fiber_dist = variance.cwiseSqrt().norm(); + if (extreme_fiber_dist < EPSILON) { + return 0.0f; + } float elastic_section_modulus = area * (variance.x() + variance.y()) / extreme_fiber_dist; return elastic_section_modulus; }; @@ -663,6 +665,9 @@ public: // section for bed calculations { + if (this->sticking_area < EPSILON) + return 1.0f; + Vec3f bed_centroid = this->sticking_centroid_accumulator / this->sticking_area; float bed_yield_torque = compute_elastic_section_modulus(this->sticking_centroid_accumulator, this->sticking_second_moment_of_area_accumulator, this->sticking_area) @@ -708,6 +713,9 @@ public: //section for weak connection calculations { + if (connection.area < EPSILON) + return 1.0f; + Vec3f conn_centroid = connection.centroid_accumulator / connection.area; float conn_yield_torque = compute_elastic_section_modulus(connection.centroid_accumulator, connection.second_moment_of_area_accumulator, connection.area) * params.material_yield_strength; @@ -763,6 +771,7 @@ void debug_print_graph(const std::vector &islands_graph) { std::cout << " volume: " << island.volume << std::endl; std::cout << " sticking_area: " << island.sticking_area << std::endl; std::cout << " connected_islands count: " << island.connected_islands.size() << std::endl; + std::cout << " lines count: " << island.external_lines.size() << std::endl; } } std::cout << "END OF GRAPH" << std::endl; From dbe864ea8a0c838026c030b15b0a3e2968f96a97 Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Wed, 27 Jul 2022 11:29:31 +0200 Subject: [PATCH 059/100] refactor to use covariance to best estimate XY variance of the connection and thus second moment of area --- src/libslic3r/SupportSpotsGenerator.cpp | 99 ++++++++++++++++++++----- 1 file changed, 79 insertions(+), 20 deletions(-) diff --git a/src/libslic3r/SupportSpotsGenerator.cpp b/src/libslic3r/SupportSpotsGenerator.cpp index 6a1aada0a1..def0217e52 100644 --- a/src/libslic3r/SupportSpotsGenerator.cpp +++ b/src/libslic3r/SupportSpotsGenerator.cpp @@ -15,6 +15,7 @@ #include "libslic3r/ClipperUtils.hpp" #include "Geometry/ConvexHull.hpp" +#define DETAILED_DEBUG_LOGS #define DEBUG_FILES #ifdef DEBUG_FILES @@ -252,21 +253,25 @@ struct IslandConnection { float area { }; Vec3f centroid_accumulator = Vec3f::Zero(); Vec2f second_moment_of_area_accumulator = Vec2f::Zero(); + float second_moment_of_area_covariance_accumulator { }; void add(const IslandConnection &other) { this->area += other.area; this->centroid_accumulator += other.centroid_accumulator; this->second_moment_of_area_accumulator += other.second_moment_of_area_accumulator; + this->second_moment_of_area_covariance_accumulator += other.second_moment_of_area_covariance_accumulator; } void print_info(const std::string &tag) { Vec3f centroid = centroid_accumulator / area; Vec2f variance = (second_moment_of_area_accumulator / area - centroid.head<2>().cwiseProduct(centroid.head<2>())); + float covariance = second_moment_of_area_covariance_accumulator / area - centroid.x() * centroid.y(); std::cout << tag << std::endl; std::cout << "area: " << area << std::endl; std::cout << "centroid: " << centroid.x() << " " << centroid.y() << " " << centroid.z() << std::endl; std::cout << "variance: " << variance.x() << " " << variance.y() << std::endl; + std::cout << "covariance: " << covariance << std::endl; } }; @@ -277,6 +282,7 @@ struct Island { float sticking_area { }; // for support points present on this layer (or bed extrusions) Vec3f sticking_centroid_accumulator = Vec3f::Zero(); Vec2f sticking_second_moment_of_area_accumulator = Vec2f::Zero(); + float sticking_second_moment_of_area_covariance_accumulator { }; std::vector external_lines; }; @@ -535,11 +541,15 @@ std::tuple reckon_islands( Vec2f middle = Vec2f((line.a + line.b) / 2.0f); island.sticking_centroid_accumulator += sticking_area * to_3d(middle, float(layer->slice_z)); island.sticking_second_moment_of_area_accumulator += sticking_area * middle.cwiseProduct(middle); + island.sticking_second_moment_of_area_covariance_accumulator += sticking_area * middle.x() + * middle.y(); } else if (layer_lines[lidx].support_point_generated) { float sticking_area = line.len * flow_width; island.sticking_area += sticking_area; island.sticking_centroid_accumulator += sticking_area * to_3d(line.b, float(layer->slice_z)); island.sticking_second_moment_of_area_accumulator += sticking_area * line.b.cwiseProduct(line.b); + island.sticking_second_moment_of_area_covariance_accumulator += sticking_area * line.b.x() + * line.b.y(); } } } @@ -574,6 +584,8 @@ std::tuple reckon_islands( * current_layer_grid.pixel_area(); connection.second_moment_of_area_accumulator += current_coords.cwiseProduct(current_coords) * current_layer_grid.pixel_area(); + connection.second_moment_of_area_covariance_accumulator += current_coords.x() * current_coords.y() + * current_layer_grid.pixel_area(); } } } @@ -601,6 +613,7 @@ class ObjectPart { float sticking_area { }; Vec3f sticking_centroid_accumulator = Vec3f::Zero(); Vec2f sticking_second_moment_of_area_accumulator = Vec2f::Zero(); + float sticking_second_moment_of_area_covariance_accumulator { }; public: ObjectPart() = default; @@ -611,6 +624,8 @@ public: this->sticking_area = island.sticking_area; this->sticking_centroid_accumulator = island.sticking_centroid_accumulator; this->sticking_second_moment_of_area_accumulator = island.sticking_second_moment_of_area_accumulator; + this->sticking_second_moment_of_area_covariance_accumulator = + island.sticking_second_moment_of_area_covariance_accumulator; } void add(const ObjectPart &other) { @@ -619,6 +634,8 @@ public: this->sticking_area += other.sticking_area; this->sticking_centroid_accumulator += other.sticking_centroid_accumulator; this->sticking_second_moment_of_area_accumulator += other.sticking_second_moment_of_area_accumulator; + this->sticking_second_moment_of_area_covariance_accumulator += + other.sticking_second_moment_of_area_covariance_accumulator; } void add_support_point(const Vec3f &position, float sticking_area) { @@ -626,6 +643,51 @@ public: this->sticking_centroid_accumulator += sticking_area * position; this->sticking_second_moment_of_area_accumulator += sticking_area * position.head<2>().cwiseProduct(position.head<2>()); + this->sticking_second_moment_of_area_covariance_accumulator += sticking_area + * position.x() * position.y(); + } + + float compute_elastic_section_modulus( + const Vec2f &line_dir, + const Vec3f ¢roid_accumulator, + const Vec2f &second_moment_of_area_accumulator, + const float &second_moment_of_area_covariance_accumulator, + const float &area) const { + assert(area > 0); + Vec3f centroid = centroid_accumulator / area; + Vec2f variance = (second_moment_of_area_accumulator / area + - centroid.head<2>().cwiseProduct(centroid.head<2>())); + float covariance = second_moment_of_area_covariance_accumulator / area - centroid.x() * centroid.y(); + // Var(aX+bY)=a^2*Var(X)+b^2*Var(Y)+2*a*b*Cov(X,Y) + float directional_xy_variance = line_dir.x() * line_dir.x() * variance.x() + + line_dir.y() * line_dir.y() * variance.y() + + 2.0f * line_dir.x() * line_dir.y() * covariance; + +#ifdef DETAILED_DEBUG_LOGS + BOOST_LOG_TRIVIAL(debug) + << "centroid: " << centroid.x() << " " << centroid.y() << " " << centroid.z(); + BOOST_LOG_TRIVIAL(debug) + << "variance: " << variance.x() << " " << variance.y(); + BOOST_LOG_TRIVIAL(debug) + << "covariance: " << covariance; + BOOST_LOG_TRIVIAL(debug) + << "directional_xy_variance: " << directional_xy_variance; +#endif + + if (directional_xy_variance < EPSILON) { + return 0.0f; + } + float extreme_fiber_dist = sqrt(area * directional_xy_variance); + float elastic_section_modulus = area * directional_xy_variance / extreme_fiber_dist; + +#ifdef DETAILED_DEBUG_LOGS + BOOST_LOG_TRIVIAL(debug) + << "extreme_fiber_dist: " << extreme_fiber_dist; + BOOST_LOG_TRIVIAL(debug) + << "elastic_section_modulus: " << elastic_section_modulus; +#endif + + return elastic_section_modulus; } float is_stable_while_extruding( @@ -636,20 +698,6 @@ public: Vec2f line_dir = (extruded_line.b - extruded_line.a).normalized(); - auto compute_elastic_section_modulus = [&line_dir]( - const Vec3f ¢roid_accumulator, const Vec2f &second_moment_of_area_accumulator, const float &area) { - Vec3f centroid = centroid_accumulator / area; - Vec2f variance = (second_moment_of_area_accumulator / area - - centroid.head<2>().cwiseProduct(centroid.head<2>())); - variance = variance.cwiseProduct(line_dir.cwiseAbs()); - float extreme_fiber_dist = variance.cwiseSqrt().norm(); - if (extreme_fiber_dist < EPSILON) { - return 0.0f; - } - float elastic_section_modulus = area * (variance.x() + variance.y()) / extreme_fiber_dist; - return elastic_section_modulus; - }; - const Vec3f &mass_centroid = this->volume_centroid_accumulator / this->volume; float mass = this->volume * params.filament_density; float weight = mass * params.gravity_constant; @@ -669,8 +717,12 @@ public: return 1.0f; Vec3f bed_centroid = this->sticking_centroid_accumulator / this->sticking_area; - float bed_yield_torque = compute_elastic_section_modulus(this->sticking_centroid_accumulator, - this->sticking_second_moment_of_area_accumulator, this->sticking_area) + float bed_yield_torque = compute_elastic_section_modulus( + line_dir, + this->sticking_centroid_accumulator, + this->sticking_second_moment_of_area_accumulator, + this->sticking_second_moment_of_area_covariance_accumulator, + this->sticking_area) * params.bed_adhesion_yield_strength; float bed_weight_arm = (bed_centroid.head<2>() - mass_centroid.head<2>()).norm(); @@ -686,7 +738,8 @@ public: float bed_total_torque = bed_movement_torque + bed_extruder_conflict_torque + bed_weight_torque - bed_yield_torque; -#if 1 + +#ifdef DETAILED_DEBUG_LOGS BOOST_LOG_TRIVIAL(debug) << "bed_centroid: " << bed_centroid.x() << " " << bed_centroid.y() << " " << bed_centroid.z(); BOOST_LOG_TRIVIAL(debug) @@ -717,8 +770,12 @@ public: return 1.0f; Vec3f conn_centroid = connection.centroid_accumulator / connection.area; - float conn_yield_torque = compute_elastic_section_modulus(connection.centroid_accumulator, - connection.second_moment_of_area_accumulator, connection.area) * params.material_yield_strength; + float conn_yield_torque = compute_elastic_section_modulus( + line_dir, + connection.centroid_accumulator, + connection.second_moment_of_area_accumulator, + connection.second_moment_of_area_covariance_accumulator, + connection.area) * params.material_yield_strength; float conn_weight_arm = (conn_centroid.head<2>() - mass_centroid.head<2>()).norm(); float conn_weight_torque = conn_weight_arm * weight * (conn_centroid.z() / layer_z); @@ -734,7 +791,7 @@ public: float conn_total_torque = conn_movement_torque + conn_extruder_conflict_torque + conn_weight_torque - conn_yield_torque; -#if 1 +#ifdef DETAILED_DEBUG_LOGS BOOST_LOG_TRIVIAL(debug) << "bed_centroid: " << conn_centroid.x() << " " << conn_centroid.y() << " " << conn_centroid.z(); BOOST_LOG_TRIVIAL(debug) @@ -936,6 +993,8 @@ Issues check_global_stability(SupportGridFilter supports_presence_grid, weakest_conn.centroid_accumulator += support_point * area; weakest_conn.second_moment_of_area_accumulator += area * support_point.head<2>().cwiseProduct(support_point.head<2>()); + weakest_conn.second_moment_of_area_covariance_accumulator += area * support_point.x() + * support_point.y(); } } } From a6cf309020acc571b50468f3c58d50979a624e1f Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Wed, 27 Jul 2022 14:29:30 +0200 Subject: [PATCH 060/100] updated weakest connection strength estimation, fixed various issues --- src/libslic3r/PrintObject.cpp | 4 +- src/libslic3r/SupportSpotsGenerator.cpp | 44 ++++++++++---------- src/libslic3r/SupportSpotsGenerator.hpp | 2 +- src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp | 2 - 4 files changed, 25 insertions(+), 27 deletions(-) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 0f85891ee2..b085c7a07a 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -422,7 +422,7 @@ void PrintObject::generate_support_spots() SupportSpotsGenerator::Issues issues = SupportSpotsGenerator::full_search(this); auto obj_transform = this->trafo_centered(); for (ModelVolume *model_volume : this->model_object()->volumes) { - if (model_volume->type() == ModelVolumeType::MODEL_PART) { + if (model_volume->is_model_part()) { Transform3d model_transformation = model_volume->get_matrix(); Transform3f inv_transform = (obj_transform * model_transformation).inverse().cast(); TriangleSelectorWrapper selector { model_volume->mesh() }; @@ -439,7 +439,7 @@ void PrintObject::generate_support_spots() indexed_triangle_set copy = model_volume->mesh().its; its_transform(copy, obj_transform * model_transformation); its_write_obj(copy, - debug_out_path("model.obj").c_str()); + debug_out_path(("model"+std::to_string(model_volume->id().id)+".obj").c_str()).c_str()); #endif } } diff --git a/src/libslic3r/SupportSpotsGenerator.cpp b/src/libslic3r/SupportSpotsGenerator.cpp index def0217e52..2669ac5434 100644 --- a/src/libslic3r/SupportSpotsGenerator.cpp +++ b/src/libslic3r/SupportSpotsGenerator.cpp @@ -313,7 +313,7 @@ float get_flow_width(const LayerRegion *region, ExtrusionRole role) { } } -// Accumulator of current extruion path properties +// Accumulator of current extrusion path properties // It remembers unsuported distance and maximum accumulated curvature over that distance. // Used to determine local stability issues (too long bridges, extrusion curves into air) struct ExtrusionPropertiesAccumulator { @@ -417,7 +417,7 @@ void check_extrusion_entity_stability(const ExtrusionEntity *entity, } if (dist_from_prev_layer > flow_width * 0.3) { malformation_acc.add_distance(current_line.len); - current_line.malformation += 0.15f + current_line.malformation += 0.3f * layer_region->layer()->height * (0.8f + 0.2f * malformation_acc.max_curvature / (1.0f + 0.5f * malformation_acc.distance)); } else { malformation_acc.reset(); @@ -704,10 +704,6 @@ public: float movement_force = params.max_acceleration * mass; - Vec3f extruder_pressure_direction = to_3d(line_dir, 0.0f); - extruder_pressure_direction.z() = -extruded_line.malformation * 0.5f; - extruder_pressure_direction.normalize(); - Vec3d endpoint = (to_3d(extruded_line.b, layer_z)).cast(); float extruder_conflict_force = params.standard_extruder_conflict_force + std::min(extruded_line.malformation, 1.0f) * params.malformations_additive_conflict_extruder_force; @@ -731,9 +727,7 @@ public: float bed_movement_arm = std::max(0.0f, mass_centroid.z() - bed_centroid.z()); float bed_movement_torque = movement_force * bed_movement_arm; - float bed_conflict_torque_arm = line_alg::distance_to( - Linef3(endpoint, endpoint + extruder_pressure_direction.cast()), - bed_centroid.cast()); + float bed_conflict_torque_arm = layer_z - bed_centroid.z(); float bed_extruder_conflict_torque = extruder_conflict_force * bed_conflict_torque_arm; float bed_total_torque = bed_movement_torque + bed_extruder_conflict_torque + bed_weight_torque @@ -783,9 +777,7 @@ public: float conn_movement_arm = std::max(0.0f, mass_centroid.z() - conn_centroid.z()); float conn_movement_torque = movement_force * conn_movement_arm; - float conn_conflict_torque_arm = line_alg::distance_to( - Linef3(endpoint, endpoint + extruder_pressure_direction.cast()), - conn_centroid.cast()); + float conn_conflict_torque_arm = layer_z - conn_centroid.z(); float conn_extruder_conflict_torque = extruder_conflict_force * conn_conflict_torque_arm; float conn_total_torque = conn_movement_torque + conn_extruder_conflict_torque + conn_weight_torque @@ -817,6 +809,7 @@ public: } }; +#ifdef DETAILED_DEBUG_LOGS void debug_print_graph(const std::vector &islands_graph) { std::cout << "BUILT ISLANDS GRAPH:" << std::endl; for (size_t layer_idx = 0; layer_idx < islands_graph.size(); ++layer_idx) { @@ -832,8 +825,8 @@ void debug_print_graph(const std::vector &islands_graph) { } } std::cout << "END OF GRAPH" << std::endl; - } +#endif class ActiveObjectParts { size_t next_part_idx = 0; @@ -876,7 +869,10 @@ public: Issues check_global_stability(SupportGridFilter supports_presence_grid, const std::vector &islands_graph, const Params ¶ms) { +#ifdef DETAILED_DEBUG_LOGS debug_print_graph(islands_graph); +#endif + Issues issues { }; ActiveObjectParts active_object_parts { }; std::unordered_map prev_island_to_object_part_mapping; @@ -887,11 +883,13 @@ Issues check_global_stability(SupportGridFilter supports_presence_grid, for (size_t layer_idx = 0; layer_idx < islands_graph.size(); ++layer_idx) { float layer_z = islands_graph[layer_idx].layer_z; - std::cout << "at layer: " << layer_idx << " the following island to object mapping is used:" << std::endl; + +#ifdef DETAILED_DEBUG_LOGS for (const auto &m : prev_island_to_object_part_mapping) { std::cout << "island " << m.first << " maps to part " << m.second << std::endl; prev_island_weakest_connection[m.first].print_info("connection info:"); } +#endif for (size_t island_idx = 0; island_idx < islands_graph[layer_idx].islands.size(); ++island_idx) { const Island &island = islands_graph[layer_idx].islands[island_idx]; @@ -917,24 +915,25 @@ Issues check_global_stability(SupportGridFilter supports_presence_grid, final_part_id = *parts_ids.begin(); for (size_t part_id : parts_ids) { if (final_part_id != part_id) { - std::cout << "at layer: " << layer_idx << " merging object part: " << part_id - << " into final part: " << final_part_id << std::endl; active_object_parts.merge(part_id, final_part_id); } } } - auto estimate_strength = [layer_z](const IslandConnection &conn) { + auto estimate_conn_strength = [layer_z](const IslandConnection &conn) { Vec3f centroid = conn.centroid_accumulator / conn.area; - float min_variance = (conn.second_moment_of_area_accumulator / conn.area - - centroid.head<2>().cwiseProduct(centroid.head<2>())).minCoeff(); + Vec2f variance = (conn.second_moment_of_area_accumulator / conn.area + - centroid.head<2>().cwiseProduct(centroid.head<2>())); + float xy_variance = variance.x() + variance.y(); float arm_len_estimate = std::max(1.1f, layer_z - (conn.centroid_accumulator.z() / conn.area)); - return min_variance / arm_len_estimate; + return conn.area * sqrt(xy_variance) / arm_len_estimate; }; +#ifdef DETAILED_DEBUG_LOGS new_weakest_connection.print_info("new_weakest_connection"); transfered_weakest_connection.print_info("transfered_weakest_connection"); +#endif - if (estimate_strength(transfered_weakest_connection) < estimate_strength(new_weakest_connection)) { + if (estimate_conn_strength(transfered_weakest_connection) < estimate_conn_strength(new_weakest_connection)) { new_weakest_connection = transfered_weakest_connection; } next_island_weakest_connection.emplace(island_idx, new_weakest_connection); @@ -957,8 +956,9 @@ Issues check_global_stability(SupportGridFilter supports_presence_grid, const Island &island = islands_graph[layer_idx].islands[island_idx]; ObjectPart &part = active_object_parts.access(prev_island_to_object_part_mapping[island_idx]); IslandConnection &weakest_conn = prev_island_weakest_connection[island_idx]; +#ifdef DETAILED_DEBUG_LOGS weakest_conn.print_info("weakest connection info: "); - +#endif std::vector dummy { }; LinesDistancer island_lines_dist(dummy); float unchecked_dist = params.min_distance_between_support_points + 1.0f; diff --git a/src/libslic3r/SupportSpotsGenerator.hpp b/src/libslic3r/SupportSpotsGenerator.hpp index cc11c27c7e..6b94ceeaa2 100644 --- a/src/libslic3r/SupportSpotsGenerator.hpp +++ b/src/libslic3r/SupportSpotsGenerator.hpp @@ -21,7 +21,7 @@ struct Params { const float bed_adhesion_yield_strength = 0.128f * 1e6f; //MPa * 1e^6 = (g*mm/s^2)/mm^2 = g/(mm*s^2); yield strength of the bed surface const float material_yield_strength = 33.0f * 1e6f; // (g*mm/s^2)/mm^2; 33 MPa is yield strength of ABS, which has the lowest yield strength from common materials. const float standard_extruder_conflict_force = 20.0f * gravity_constant; // force that can occasionally push the model due to various factors (filament leaks, small curling, ... ); - const float malformations_additive_conflict_extruder_force = 300.0f * gravity_constant; // for areas with possible high layered curled filaments + const float malformations_additive_conflict_extruder_force = 150.0f * gravity_constant; // for areas with possible high layered curled filaments }; struct SupportPoint { diff --git a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp index 36f48d0087..7e8f0954e0 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp @@ -405,7 +405,6 @@ void GLGizmoFdmSupports::get_data_from_backend() mvs.emplace(mv->id().id, mv); } } - // NOTE: Copying the data into ModelVolumes stops the background processing. int mesh_id = -1.0f; for (ModelVolume* mv : mo->volumes){ if (mv->is_model_part()){ @@ -418,7 +417,6 @@ void GLGizmoFdmSupports::get_data_from_backend() this->waiting_for_autogenerated_supports = false; m_parent.post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS)); m_parent.set_as_dirty(); - } } } From ff73cd253edc168957ddfbc9131ed18f87c9d1fd Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Wed, 27 Jul 2022 15:33:10 +0200 Subject: [PATCH 061/100] fix extreme fibre distance calculation --- src/libslic3r/SupportSpotsGenerator.cpp | 10 +++++----- src/libslic3r/SupportSpotsGenerator.hpp | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/libslic3r/SupportSpotsGenerator.cpp b/src/libslic3r/SupportSpotsGenerator.cpp index 2669ac5434..e90693f6d2 100644 --- a/src/libslic3r/SupportSpotsGenerator.cpp +++ b/src/libslic3r/SupportSpotsGenerator.cpp @@ -677,7 +677,7 @@ public: if (directional_xy_variance < EPSILON) { return 0.0f; } - float extreme_fiber_dist = sqrt(area * directional_xy_variance); + float extreme_fiber_dist = sqrt(area/PI); float elastic_section_modulus = area * directional_xy_variance / extreme_fiber_dist; #ifdef DETAILED_DEBUG_LOGS @@ -924,7 +924,7 @@ Issues check_global_stability(SupportGridFilter supports_presence_grid, Vec2f variance = (conn.second_moment_of_area_accumulator / conn.area - centroid.head<2>().cwiseProduct(centroid.head<2>())); float xy_variance = variance.x() + variance.y(); - float arm_len_estimate = std::max(1.1f, layer_z - (conn.centroid_accumulator.z() / conn.area)); + float arm_len_estimate = std::max(1.0f, layer_z - (conn.centroid_accumulator.z() / conn.area)); return conn.area * sqrt(xy_variance) / arm_len_estimate; }; @@ -933,10 +933,10 @@ Issues check_global_stability(SupportGridFilter supports_presence_grid, transfered_weakest_connection.print_info("transfered_weakest_connection"); #endif - if (estimate_conn_strength(transfered_weakest_connection) < estimate_conn_strength(new_weakest_connection)) { - new_weakest_connection = transfered_weakest_connection; + if (estimate_conn_strength(transfered_weakest_connection) > estimate_conn_strength(new_weakest_connection)) { + transfered_weakest_connection = new_weakest_connection; } - next_island_weakest_connection.emplace(island_idx, new_weakest_connection); + next_island_weakest_connection.emplace(island_idx, transfered_weakest_connection); next_island_to_object_part_mapping.emplace(island_idx, final_part_id); ObjectPart &part = active_object_parts.access(final_part_id); part.add(ObjectPart(island)); diff --git a/src/libslic3r/SupportSpotsGenerator.hpp b/src/libslic3r/SupportSpotsGenerator.hpp index 6b94ceeaa2..10c7abe70f 100644 --- a/src/libslic3r/SupportSpotsGenerator.hpp +++ b/src/libslic3r/SupportSpotsGenerator.hpp @@ -19,9 +19,9 @@ struct Params { const float max_acceleration = 9*1000.0f; // mm/s^2 ; max acceleration of object (bed) in XY (NOTE: The max hit is received by the object in the jerk phase, so the usual machine limits are too low) const float filament_density = 1.25e-3f ; // g/mm^3 ; Common filaments are very lightweight, so precise number is not that important const float bed_adhesion_yield_strength = 0.128f * 1e6f; //MPa * 1e^6 = (g*mm/s^2)/mm^2 = g/(mm*s^2); yield strength of the bed surface - const float material_yield_strength = 33.0f * 1e6f; // (g*mm/s^2)/mm^2; 33 MPa is yield strength of ABS, which has the lowest yield strength from common materials. + const float material_yield_strength = 15.0f * 1e6f; // (g*mm/s^2)/mm^2; 33 MPa is yield strength of ABS, which has the lowest yield strength from common materials. const float standard_extruder_conflict_force = 20.0f * gravity_constant; // force that can occasionally push the model due to various factors (filament leaks, small curling, ... ); - const float malformations_additive_conflict_extruder_force = 150.0f * gravity_constant; // for areas with possible high layered curled filaments + const float malformations_additive_conflict_extruder_force = 300.0f * gravity_constant; // for areas with possible high layered curled filaments }; struct SupportPoint { From 2401556193208955eee1aa852e1bd33df8eae2e7 Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Wed, 27 Jul 2022 17:15:23 +0200 Subject: [PATCH 062/100] most extreme fiber is now taken from the current island.. this is not correct, but from all aproximations it gives best results --- src/libslic3r/SupportSpotsGenerator.cpp | 40 ++++++++++++++----------- src/libslic3r/SupportSpotsGenerator.hpp | 2 +- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/src/libslic3r/SupportSpotsGenerator.cpp b/src/libslic3r/SupportSpotsGenerator.cpp index e90693f6d2..59ed818b37 100644 --- a/src/libslic3r/SupportSpotsGenerator.cpp +++ b/src/libslic3r/SupportSpotsGenerator.cpp @@ -649,6 +649,7 @@ public: float compute_elastic_section_modulus( const Vec2f &line_dir, + const Vec3f &extreme_point, const Vec3f ¢roid_accumulator, const Vec2f &second_moment_of_area_accumulator, const float &second_moment_of_area_covariance_accumulator, @@ -673,11 +674,13 @@ public: BOOST_LOG_TRIVIAL(debug) << "directional_xy_variance: " << directional_xy_variance; #endif - if (directional_xy_variance < EPSILON) { return 0.0f; } - float extreme_fiber_dist = sqrt(area/PI); + + float extreme_fiber_dist = line_alg::distance_to( + Linef(centroid.head<2>().cast(), (centroid.head<2>() + Vec2f(line_dir.y(), -line_dir.x())).cast()), + extreme_point.head<2>().cast()); float elastic_section_modulus = area * directional_xy_variance / extreme_fiber_dist; #ifdef DETAILED_DEBUG_LOGS @@ -693,11 +696,11 @@ public: float is_stable_while_extruding( const IslandConnection &connection, const ExtrusionLine &extruded_line, + const Vec3f &extreme_point, float layer_z, const Params ¶ms) const { Vec2f line_dir = (extruded_line.b - extruded_line.a).normalized(); - const Vec3f &mass_centroid = this->volume_centroid_accumulator / this->volume; float mass = this->volume * params.filament_density; float weight = mass * params.gravity_constant; @@ -715,6 +718,7 @@ public: Vec3f bed_centroid = this->sticking_centroid_accumulator / this->sticking_area; float bed_yield_torque = compute_elastic_section_modulus( line_dir, + extreme_point, this->sticking_centroid_accumulator, this->sticking_second_moment_of_area_accumulator, this->sticking_second_moment_of_area_covariance_accumulator, @@ -764,8 +768,13 @@ public: return 1.0f; Vec3f conn_centroid = connection.centroid_accumulator / connection.area; + + if (layer_z - conn_centroid.z() < 3.0f) { + return -1.0f; + } float conn_yield_torque = compute_elastic_section_modulus( line_dir, + extreme_point, connection.centroid_accumulator, connection.second_moment_of_area_accumulator, connection.second_moment_of_area_covariance_accumulator, @@ -933,7 +942,8 @@ Issues check_global_stability(SupportGridFilter supports_presence_grid, transfered_weakest_connection.print_info("transfered_weakest_connection"); #endif - if (estimate_conn_strength(transfered_weakest_connection) > estimate_conn_strength(new_weakest_connection)) { + if (estimate_conn_strength(transfered_weakest_connection) + > estimate_conn_strength(new_weakest_connection)) { transfered_weakest_connection = new_weakest_connection; } next_island_weakest_connection.emplace(island_idx, transfered_weakest_connection); @@ -959,8 +969,7 @@ Issues check_global_stability(SupportGridFilter supports_presence_grid, #ifdef DETAILED_DEBUG_LOGS weakest_conn.print_info("weakest connection info: "); #endif - std::vector dummy { }; - LinesDistancer island_lines_dist(dummy); + LinesDistancer island_lines_dist(island.external_lines); float unchecked_dist = params.min_distance_between_support_points + 1.0f; for (const ExtrusionLine &line : island.external_lines) { @@ -969,18 +978,15 @@ Issues check_global_stability(SupportGridFilter supports_presence_grid, unchecked_dist += line.len; } else { unchecked_dist = line.len; - auto force = part.is_stable_while_extruding(weakest_conn, line, layer_z, params); + Vec2f target_point; + size_t _idx; + Vec3f pivot_site_search_point = to_3d(Vec2f(line.b + (line.b - line.a).normalized() * 300.0f), + layer_z); + island_lines_dist.signed_distance_from_lines(pivot_site_search_point.head<2>(), _idx, + target_point); + Vec3f support_point = to_3d(target_point, layer_z); + auto force = part.is_stable_while_extruding(weakest_conn, line, support_point, layer_z, params); if (force > 0) { - if (island_lines_dist.get_lines().empty()) { - island_lines_dist = LinesDistancer(island.external_lines); - } - Vec2f target_point; - size_t _idx; - Vec3f pivot_site_search_point = to_3d(Vec2f(line.b + (line.b - line.a).normalized() * 300.0f), - layer_z); - island_lines_dist.signed_distance_from_lines(pivot_site_search_point.head<2>(), _idx, - target_point); - Vec3f support_point = to_3d(target_point, layer_z); if (!supports_presence_grid.position_taken(support_point)) { float area = params.support_points_interface_radius * params.support_points_interface_radius * float(PI); diff --git a/src/libslic3r/SupportSpotsGenerator.hpp b/src/libslic3r/SupportSpotsGenerator.hpp index 10c7abe70f..7052105612 100644 --- a/src/libslic3r/SupportSpotsGenerator.hpp +++ b/src/libslic3r/SupportSpotsGenerator.hpp @@ -21,7 +21,7 @@ struct Params { const float bed_adhesion_yield_strength = 0.128f * 1e6f; //MPa * 1e^6 = (g*mm/s^2)/mm^2 = g/(mm*s^2); yield strength of the bed surface const float material_yield_strength = 15.0f * 1e6f; // (g*mm/s^2)/mm^2; 33 MPa is yield strength of ABS, which has the lowest yield strength from common materials. const float standard_extruder_conflict_force = 20.0f * gravity_constant; // force that can occasionally push the model due to various factors (filament leaks, small curling, ... ); - const float malformations_additive_conflict_extruder_force = 300.0f * gravity_constant; // for areas with possible high layered curled filaments + const float malformations_additive_conflict_extruder_force = 150.0f * gravity_constant; // for areas with possible high layered curled filaments }; struct SupportPoint { From 1d4f41a2fd91823c03f724611066faf35d721cfb Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Thu, 28 Jul 2022 14:46:16 +0200 Subject: [PATCH 063/100] improved option logic, custom setting for object soe that it uses the painted supports --- src/libslic3r/PrintObject.cpp | 33 +++++++++++--------- src/libslic3r/SupportSpotsGenerator.cpp | 2 +- src/libslic3r/SupportSpotsGenerator.hpp | 2 +- src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp | 18 ++++++++--- 4 files changed, 34 insertions(+), 21 deletions(-) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index b085c7a07a..2d473415c5 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -398,6 +398,21 @@ void PrintObject::ironing() } } + +/* +std::vector problematic_layers = SupportSpotsGenerator::quick_search(this); + if (!problematic_layers.empty()) { + std::cout << "Object needs supports" << std::endl; + this->active_step_add_warning(PrintStateBase::WarningLevel::CRITICAL, + L("Supportable issues found. Consider enabling supports for this object")); + this->active_step_add_warning(PrintStateBase::WarningLevel::CRITICAL, + L("Supportable issues found. Consider enabling supports for this object")); + for (size_t index = 0; index < std::min(problematic_layers.size(), size_t(4)); ++index) { + this->active_step_add_warning(PrintStateBase::WarningLevel::CRITICAL, + format(L("Layer with issues: %1%"), problematic_layers[index] + 1)); + } + } + */ void PrintObject::generate_support_spots() { if (this->set_started(posSupportSpotsSearch)) { @@ -405,20 +420,10 @@ void PrintObject::generate_support_spots() << "Searching support spots - start"; m_print->set_status(75, L("Searching support spots")); - if (!this->m_config.support_material) { - std::vector problematic_layers = SupportSpotsGenerator::quick_search(this); - if (!problematic_layers.empty()) { - std::cout << "Object needs supports" << std::endl; - this->active_step_add_warning(PrintStateBase::WarningLevel::CRITICAL, - L("Supportable issues found. Consider enabling supports for this object")); - this->active_step_add_warning(PrintStateBase::WarningLevel::CRITICAL, - L("Supportable issues found. Consider enabling supports for this object")); - for (size_t index = 0; index < std::min(problematic_layers.size(), size_t(4)); ++index) { - this->active_step_add_warning(PrintStateBase::WarningLevel::CRITICAL, - format(L("Layer with issues: %1%"), problematic_layers[index] + 1)); - } - } - } else { + if (this->m_config.support_material && !this->m_config.support_material_auto && + std::all_of(this->model_object()->volumes.begin(), this->model_object()->volumes.end(), + [](const ModelVolume* mv){return mv->supported_facets.empty();}) + ) { SupportSpotsGenerator::Issues issues = SupportSpotsGenerator::full_search(this); auto obj_transform = this->trafo_centered(); for (ModelVolume *model_volume : this->model_object()->volumes) { diff --git a/src/libslic3r/SupportSpotsGenerator.cpp b/src/libslic3r/SupportSpotsGenerator.cpp index 59ed818b37..b75efd6ca0 100644 --- a/src/libslic3r/SupportSpotsGenerator.cpp +++ b/src/libslic3r/SupportSpotsGenerator.cpp @@ -434,7 +434,7 @@ std::tuple reckon_islands( const std::vector &layer_lines, const Params ¶ms) { - //extract extrusions (connected paths from multiple lines) from the layer_lines. belonging to single polyline is determined by origin_entity ptr. + //extract extrusions (connected paths from multiple lines) from the layer_lines. Grouping by the same polyline is determined by common origin_entity ptr. // result is a vector of [start, end) index pairs into the layer_lines vector std::vector> extrusions; //start and end idx (one beyond last extrusion) [start,end) const ExtrusionEntity *current_ex = nullptr; diff --git a/src/libslic3r/SupportSpotsGenerator.hpp b/src/libslic3r/SupportSpotsGenerator.hpp index 7052105612..6b94ceeaa2 100644 --- a/src/libslic3r/SupportSpotsGenerator.hpp +++ b/src/libslic3r/SupportSpotsGenerator.hpp @@ -19,7 +19,7 @@ struct Params { const float max_acceleration = 9*1000.0f; // mm/s^2 ; max acceleration of object (bed) in XY (NOTE: The max hit is received by the object in the jerk phase, so the usual machine limits are too low) const float filament_density = 1.25e-3f ; // g/mm^3 ; Common filaments are very lightweight, so precise number is not that important const float bed_adhesion_yield_strength = 0.128f * 1e6f; //MPa * 1e^6 = (g*mm/s^2)/mm^2 = g/(mm*s^2); yield strength of the bed surface - const float material_yield_strength = 15.0f * 1e6f; // (g*mm/s^2)/mm^2; 33 MPa is yield strength of ABS, which has the lowest yield strength from common materials. + const float material_yield_strength = 33.0f * 1e6f; // (g*mm/s^2)/mm^2; 33 MPa is yield strength of ABS, which has the lowest yield strength from common materials. const float standard_extruder_conflict_force = 20.0f * gravity_constant; // force that can occasionally push the model due to various factors (filament leaks, small curling, ... ); const float malformations_additive_conflict_extruder_force = 150.0f * gravity_constant; // for areas with possible high layered curled filaments }; diff --git a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp index 7e8f0954e0..494adb55ad 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp @@ -42,6 +42,7 @@ bool GLGizmoFdmSupports::on_init() m_shortcut_key = WXK_CONTROL_L; m_desc["auto_generate"] = _L("Auto-generate supports"); + m_desc["generating"] = _L("Generating supports..."); m_desc["clipping_of_view"] = _L("Clipping of view") + ": "; m_desc["reset_direction"] = _L("Reset direction"); m_desc["cursor_size"] = _L("Brush size") + ": "; @@ -156,10 +157,13 @@ void GLGizmoFdmSupports::on_render_input_window(float x, float y, float bottom_l ImGui::Separator(); - bool generate = m_imgui->button(m_desc.at("auto_generate")); - if (generate) - auto_generate(); - + if (waiting_for_autogenerated_supports) { + m_imgui->text(m_desc.at("generating")); + } else { + bool generate = m_imgui->button(m_desc.at("auto_generate")); + if (generate) + auto_generate(); + } ImGui::Separator(); float position_before_text_y = ImGui::GetCursorPos().y; @@ -410,6 +414,7 @@ void GLGizmoFdmSupports::get_data_from_backend() if (mv->is_model_part()){ mesh_id++; mv->supported_facets.assign(mvs[mv->id().id]->supported_facets); + mv->supported_facets.touch(); m_triangle_selectors[mesh_id]->deserialize(mv->supported_facets.get_data(), true); m_triangle_selectors[mesh_id]->request_update_render_data(); } @@ -472,7 +477,7 @@ bool GLGizmoFdmSupports::has_backend_supports() const if (! mo) return false; - // find SlaPrintObject with this ID + // find PrintObject with this ID for (const PrintObject* po : m_parent.fff_print()->objects()) { if (po->model_object()->id() == mo->id()) return po->is_step_done(posSupportSpotsSearch); @@ -495,6 +500,9 @@ void GLGizmoFdmSupports::auto_generate() return vol->type() != ModelVolumeType::MODEL_PART || vol->supported_facets.empty(); }); + mo->config.set("support_material", true); + mo->config.set("support_material_auto", false); + MessageDialog dlg(GUI::wxGetApp().plater(), _L("Autogeneration will erase all currently painted areas.") + "\n\n" + _L("Are you sure you want to do it?") + "\n", From ea769776027ef6233af5963bfa699e4c62f4e473 Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Thu, 28 Jul 2022 15:47:34 +0200 Subject: [PATCH 064/100] Quick fix for invalidation of support spots search --- src/libslic3r/PrintApply.cpp | 4 +++- src/libslic3r/PrintObject.cpp | 2 +- src/libslic3r/SupportSpotsGenerator.hpp | 2 +- src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp | 11 ++++++++++- 4 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/libslic3r/PrintApply.cpp b/src/libslic3r/PrintApply.cpp index 4c085728c7..2a5b14c7e3 100644 --- a/src/libslic3r/PrintApply.cpp +++ b/src/libslic3r/PrintApply.cpp @@ -1194,8 +1194,10 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ update_apply_status(false); } // Invalidate just the supports step. - for (const PrintObjectStatus &print_object_status : print_objects_range) + for (const PrintObjectStatus &print_object_status : print_objects_range) { + update_apply_status(print_object_status.print_object->invalidate_step(posSupportSpotsSearch)); update_apply_status(print_object_status.print_object->invalidate_step(posSupportMaterial)); + } if (supports_differ) { // Copy just the support volumes. model_volume_list_update_supports(model_object, model_object_new); diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 2d473415c5..59fbcc99d9 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -436,7 +436,7 @@ void PrintObject::generate_support_spots() Vec3f point = Vec3f(inv_transform * support_point.position); Vec3f origin = Vec3f( inv_transform * Vec3f(support_point.position.x(), support_point.position.y(), 0.0f)); - selector.enforce_spot(point, origin, 0.6f); + selector.enforce_spot(point, origin, 1.0f); } model_volume->supported_facets.set(selector.selector); diff --git a/src/libslic3r/SupportSpotsGenerator.hpp b/src/libslic3r/SupportSpotsGenerator.hpp index 6b94ceeaa2..6e435faa31 100644 --- a/src/libslic3r/SupportSpotsGenerator.hpp +++ b/src/libslic3r/SupportSpotsGenerator.hpp @@ -13,7 +13,7 @@ struct Params { const float bridge_distance_decrease_by_curvature_factor = 3.0f; // allowed bridge distance = bridge_distance / (this factor * (curvature / PI) ) const float min_distance_between_support_points = 3.0f; //mm - const float support_points_interface_radius = 0.6f; // mm + const float support_points_interface_radius = 1.0f; // mm const float gravity_constant = 9806.65f; // mm/s^2; gravity acceleration on Earth's surface, algorithm assumes that printer is in upwards position. const float max_acceleration = 9*1000.0f; // mm/s^2 ; max acceleration of object (bed) in XY (NOTE: The max hit is received by the object in the jerk phase, so the usual machine limits are too low) diff --git a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp index 494adb55ad..c193dbf908 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp @@ -414,7 +414,6 @@ void GLGizmoFdmSupports::get_data_from_backend() if (mv->is_model_part()){ mesh_id++; mv->supported_facets.assign(mvs[mv->id().id]->supported_facets); - mv->supported_facets.touch(); m_triangle_selectors[mesh_id]->deserialize(mv->supported_facets.get_data(), true); m_triangle_selectors[mesh_id]->request_update_render_data(); } @@ -510,6 +509,16 @@ void GLGizmoFdmSupports::auto_generate() if (not_painted || dlg.ShowModal() == wxID_YES) { Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Autogenerate support points")); + int mesh_id = -1.0f; + for (ModelVolume *mv : mo->volumes) { + if (mv->is_model_part()) { + mesh_id++; + mv->supported_facets.reset(); + m_triangle_selectors[mesh_id]->reset(); + m_triangle_selectors[mesh_id]->request_update_render_data(); + } + } + this->waiting_for_autogenerated_supports = true; wxGetApp().CallAfter([this]() { reslice_FDM_supports(); }); } From 14f109e703bb7afe9317654b4d64fa26bfe1b0be Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Thu, 28 Jul 2022 17:46:03 +0200 Subject: [PATCH 065/100] refactored local issues to use overhang distance --- src/libslic3r/PrintObject.cpp | 2 +- src/libslic3r/SupportSpotsGenerator.cpp | 24 +++++++++++++++--------- src/libslic3r/SupportSpotsGenerator.hpp | 5 +++-- 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 59fbcc99d9..2d473415c5 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -436,7 +436,7 @@ void PrintObject::generate_support_spots() Vec3f point = Vec3f(inv_transform * support_point.position); Vec3f origin = Vec3f( inv_transform * Vec3f(support_point.position.x(), support_point.position.y(), 0.0f)); - selector.enforce_spot(point, origin, 1.0f); + selector.enforce_spot(point, origin, 0.6f); } model_volume->supported_facets.set(selector.selector); diff --git a/src/libslic3r/SupportSpotsGenerator.cpp b/src/libslic3r/SupportSpotsGenerator.cpp index b75efd6ca0..bb60605bc9 100644 --- a/src/libslic3r/SupportSpotsGenerator.cpp +++ b/src/libslic3r/SupportSpotsGenerator.cpp @@ -354,10 +354,12 @@ void check_extrusion_entity_stability(const ExtrusionEntity *entity, const auto to_vec3f = [layer_z](const Vec2f &point) { return Vec3f(point.x(), point.y(), layer_z); }; + float overhang_dist = tan(params.overhang_angle_deg * PI / 180.0f)*layer_region->layer()->height; + Points points { }; entity->collect_points(points); std::vector lines; - lines.reserve(points.size() * 1.5); + lines.reserve(points.size() * 1.5f); lines.emplace_back(unscaled(points[0]).cast(), unscaled(points[0]).cast(), entity); for (int point_idx = 0; point_idx < int(points.size() - 1); ++point_idx) { Vec2f start = unscaled(points[point_idx]).cast(); @@ -396,14 +398,18 @@ void check_extrusion_entity_stability(const ExtrusionEntity *entity, float dist_from_prev_layer = prev_layer_lines.signed_distance_from_lines(current_line.b, nearest_line_idx, nearest_point); - if (fabs(dist_from_prev_layer) < flow_width) { + if (fabs(dist_from_prev_layer) < overhang_dist) { bridging_acc.reset(); } else { bridging_acc.add_distance(current_line.len); - if (bridging_acc.distance // if unsupported distance is larger than bridge distance linearly decreased by curvature, enforce supports. - > params.bridge_distance - / (1.0f + (bridging_acc.max_curvature - * params.bridge_distance_decrease_by_curvature_factor / PI))) { + // if unsupported distance is larger than bridge distance linearly decreased by curvature, enforce supports. + bool in_layer_dist_condition = bridging_acc.distance + > params.bridge_distance / (1.0f + (bridging_acc.max_curvature + * params.bridge_distance_decrease_by_curvature_factor / PI)); + bool between_layers_condition = fabs(dist_from_prev_layer) > 5.0f*overhang_dist || + prev_layer_lines.get_line(nearest_line_idx).malformation > 0.3f; + + if (in_layer_dist_condition && between_layers_condition) { issues.support_points.emplace_back(to_vec3f(current_line.b), 0.0f, Vec3f(0.f, 0.0f, -1.0f)); current_line.support_point_generated = true; bridging_acc.reset(); @@ -411,13 +417,13 @@ void check_extrusion_entity_stability(const ExtrusionEntity *entity, } //malformation - if (fabs(dist_from_prev_layer) < flow_width * 2.0f) { + if (fabs(dist_from_prev_layer) < overhang_dist * 5.0f) { const ExtrusionLine &nearest_line = prev_layer_lines.get_line(nearest_line_idx); current_line.malformation += 0.9 * nearest_line.malformation; } - if (dist_from_prev_layer > flow_width * 0.3) { + if (dist_from_prev_layer > overhang_dist) { malformation_acc.add_distance(current_line.len); - current_line.malformation += 0.3f * layer_region->layer()->height + current_line.malformation += 0.1f * (0.8f + 0.2f * malformation_acc.max_curvature / (1.0f + 0.5f * malformation_acc.distance)); } else { malformation_acc.reset(); diff --git a/src/libslic3r/SupportSpotsGenerator.hpp b/src/libslic3r/SupportSpotsGenerator.hpp index 6e435faa31..e8ad8c6249 100644 --- a/src/libslic3r/SupportSpotsGenerator.hpp +++ b/src/libslic3r/SupportSpotsGenerator.hpp @@ -10,10 +10,11 @@ namespace SupportSpotsGenerator { struct Params { // the algorithm should use the following units for all computations: distance [mm], mass [g], time [s], force [g*mm/s^2] const float bridge_distance = 12.0f; //mm - const float bridge_distance_decrease_by_curvature_factor = 3.0f; // allowed bridge distance = bridge_distance / (this factor * (curvature / PI) ) + const float bridge_distance_decrease_by_curvature_factor = 5.0f; // allowed bridge distance = bridge_distance / (this factor * (curvature / PI) ) + const float overhang_angle_deg = 55.0f; const float min_distance_between_support_points = 3.0f; //mm - const float support_points_interface_radius = 1.0f; // mm + const float support_points_interface_radius = 0.6f; // mm const float gravity_constant = 9806.65f; // mm/s^2; gravity acceleration on Earth's surface, algorithm assumes that printer is in upwards position. const float max_acceleration = 9*1000.0f; // mm/s^2 ; max acceleration of object (bed) in XY (NOTE: The max hit is received by the object in the jerk phase, so the usual machine limits are too low) From 62c3ca5e991576e54d84bd355ceb692148a5b5d1 Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Fri, 29 Jul 2022 13:08:27 +0200 Subject: [PATCH 066/100] gui integration, overhang angle hack --- src/libslic3r/PrintObject.cpp | 4 +++- src/libslic3r/SupportSpotsGenerator.cpp | 12 ++++++------ src/libslic3r/SupportSpotsGenerator.hpp | 6 +++--- src/slic3r/GUI/ConfigManipulation.cpp | 2 +- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 2d473415c5..631ad7c3c2 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -424,7 +424,9 @@ void PrintObject::generate_support_spots() std::all_of(this->model_object()->volumes.begin(), this->model_object()->volumes.end(), [](const ModelVolume* mv){return mv->supported_facets.empty();}) ) { - SupportSpotsGenerator::Issues issues = SupportSpotsGenerator::full_search(this); + SupportSpotsGenerator::Params params{}; + params.overhang_angle_deg = 90.001f - this->m_config.support_material_threshold; + SupportSpotsGenerator::Issues issues = SupportSpotsGenerator::full_search(this, params); auto obj_transform = this->trafo_centered(); for (ModelVolume *model_volume : this->model_object()->volumes) { if (model_volume->is_model_part()) { diff --git a/src/libslic3r/SupportSpotsGenerator.cpp b/src/libslic3r/SupportSpotsGenerator.cpp index bb60605bc9..2e5c8dbd42 100644 --- a/src/libslic3r/SupportSpotsGenerator.cpp +++ b/src/libslic3r/SupportSpotsGenerator.cpp @@ -15,8 +15,8 @@ #include "libslic3r/ClipperUtils.hpp" #include "Geometry/ConvexHull.hpp" -#define DETAILED_DEBUG_LOGS -#define DEBUG_FILES +//#define DETAILED_DEBUG_LOGS +//#define DEBUG_FILES #ifdef DEBUG_FILES #include @@ -406,8 +406,8 @@ void check_extrusion_entity_stability(const ExtrusionEntity *entity, bool in_layer_dist_condition = bridging_acc.distance > params.bridge_distance / (1.0f + (bridging_acc.max_curvature * params.bridge_distance_decrease_by_curvature_factor / PI)); - bool between_layers_condition = fabs(dist_from_prev_layer) > 5.0f*overhang_dist || - prev_layer_lines.get_line(nearest_line_idx).malformation > 0.3f; + bool between_layers_condition = fabs(dist_from_prev_layer) > 3.0f*flow_width || + prev_layer_lines.get_line(nearest_line_idx).malformation > 0.6f; if (in_layer_dist_condition && between_layers_condition) { issues.support_points.emplace_back(to_vec3f(current_line.b), 0.0f, Vec3f(0.f, 0.0f, -1.0f)); @@ -417,13 +417,13 @@ void check_extrusion_entity_stability(const ExtrusionEntity *entity, } //malformation - if (fabs(dist_from_prev_layer) < overhang_dist * 5.0f) { + if (fabs(dist_from_prev_layer) < 3.0f*flow_width) { const ExtrusionLine &nearest_line = prev_layer_lines.get_line(nearest_line_idx); current_line.malformation += 0.9 * nearest_line.malformation; } if (dist_from_prev_layer > overhang_dist) { malformation_acc.add_distance(current_line.len); - current_line.malformation += 0.1f + current_line.malformation += 0.3f * (0.8f + 0.2f * malformation_acc.max_curvature / (1.0f + 0.5f * malformation_acc.distance)); } else { malformation_acc.reset(); diff --git a/src/libslic3r/SupportSpotsGenerator.hpp b/src/libslic3r/SupportSpotsGenerator.hpp index e8ad8c6249..87f1f48c39 100644 --- a/src/libslic3r/SupportSpotsGenerator.hpp +++ b/src/libslic3r/SupportSpotsGenerator.hpp @@ -11,7 +11,7 @@ struct Params { // the algorithm should use the following units for all computations: distance [mm], mass [g], time [s], force [g*mm/s^2] const float bridge_distance = 12.0f; //mm const float bridge_distance_decrease_by_curvature_factor = 5.0f; // allowed bridge distance = bridge_distance / (this factor * (curvature / PI) ) - const float overhang_angle_deg = 55.0f; + float overhang_angle_deg = 50.0f; const float min_distance_between_support_points = 3.0f; //mm const float support_points_interface_radius = 0.6f; // mm @@ -21,8 +21,8 @@ struct Params { const float filament_density = 1.25e-3f ; // g/mm^3 ; Common filaments are very lightweight, so precise number is not that important const float bed_adhesion_yield_strength = 0.128f * 1e6f; //MPa * 1e^6 = (g*mm/s^2)/mm^2 = g/(mm*s^2); yield strength of the bed surface const float material_yield_strength = 33.0f * 1e6f; // (g*mm/s^2)/mm^2; 33 MPa is yield strength of ABS, which has the lowest yield strength from common materials. - const float standard_extruder_conflict_force = 20.0f * gravity_constant; // force that can occasionally push the model due to various factors (filament leaks, small curling, ... ); - const float malformations_additive_conflict_extruder_force = 150.0f * gravity_constant; // for areas with possible high layered curled filaments + const float standard_extruder_conflict_force = 50.0f * gravity_constant; // force that can occasionally push the model due to various factors (filament leaks, small curling, ... ); + const float malformations_additive_conflict_extruder_force = 200.0f * gravity_constant; // for areas with possible high layered curled filaments }; struct SupportPoint { diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp index 96bf2fe020..d591297dfb 100644 --- a/src/slic3r/GUI/ConfigManipulation.cpp +++ b/src/slic3r/GUI/ConfigManipulation.cpp @@ -282,7 +282,7 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig* config) "dont_support_bridges", "support_material_extrusion_width", "support_material_contact_distance", "support_material_xy_spacing" }) toggle_field(el, have_support_material); - toggle_field("support_material_threshold", have_support_material_auto); +// toggle_field("support_material_threshold", have_support_material_auto); toggle_field("support_material_bottom_contact_distance", have_support_material && ! have_support_soluble); toggle_field("support_material_closing_radius", have_support_material && support_material_style == smsSnug); From 4eaa863ba4c892dfa59523a8b58343a1ad2943c8 Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Fri, 12 Aug 2022 15:27:00 +0200 Subject: [PATCH 067/100] make supports bigger, improve malformations, TODO: do not support small extrusions, check part size --- src/libslic3r/PrintObject.cpp | 6 ++-- src/libslic3r/SupportSpotsGenerator.cpp | 27 ++++++++++++---- src/libslic3r/SupportSpotsGenerator.hpp | 41 ++++++++++++++++++++----- 3 files changed, 56 insertions(+), 18 deletions(-) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 631ad7c3c2..918c4a3259 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -419,13 +419,11 @@ void PrintObject::generate_support_spots() BOOST_LOG_TRIVIAL(debug) << "Searching support spots - start"; m_print->set_status(75, L("Searching support spots")); - if (this->m_config.support_material && !this->m_config.support_material_auto && std::all_of(this->model_object()->volumes.begin(), this->model_object()->volumes.end(), [](const ModelVolume* mv){return mv->supported_facets.empty();}) ) { - SupportSpotsGenerator::Params params{}; - params.overhang_angle_deg = 90.001f - this->m_config.support_material_threshold; + SupportSpotsGenerator::Params params{90.001f - this->m_config.support_material_threshold, this->print()->m_config.filament_type.values}; SupportSpotsGenerator::Issues issues = SupportSpotsGenerator::full_search(this, params); auto obj_transform = this->trafo_centered(); for (ModelVolume *model_volume : this->model_object()->volumes) { @@ -438,7 +436,7 @@ void PrintObject::generate_support_spots() Vec3f point = Vec3f(inv_transform * support_point.position); Vec3f origin = Vec3f( inv_transform * Vec3f(support_point.position.x(), support_point.position.y(), 0.0f)); - selector.enforce_spot(point, origin, 0.6f); + selector.enforce_spot(point, origin, 1.5f); } model_volume->supported_facets.set(selector.selector); diff --git a/src/libslic3r/SupportSpotsGenerator.cpp b/src/libslic3r/SupportSpotsGenerator.cpp index 2e5c8dbd42..584eaf58c2 100644 --- a/src/libslic3r/SupportSpotsGenerator.cpp +++ b/src/libslic3r/SupportSpotsGenerator.cpp @@ -15,8 +15,8 @@ #include "libslic3r/ClipperUtils.hpp" #include "Geometry/ConvexHull.hpp" -//#define DETAILED_DEBUG_LOGS -//#define DEBUG_FILES +#define DETAILED_DEBUG_LOGS +#define DEBUG_FILES #ifdef DEBUG_FILES #include @@ -337,6 +337,15 @@ struct ExtrusionPropertiesAccumulator { } }; +// base function: ((e^(((1)/(x^(2)+1)))-1)/(e-1)) +// checkout e.g. here: https://www.geogebra.org/calculator +float gauss(float value, float mean_x_coord, float mean_value, float falloff_speed) { + float shifted = value - mean_x_coord; + float denominator = falloff_speed * shifted * shifted + 1.0f; + float exponent = 1.0f / denominator; + return mean_value * (std::exp(exponent) - 1.0f) / (std::exp(1.0f) - 1.0f); +} + void check_extrusion_entity_stability(const ExtrusionEntity *entity, std::vector &checked_lines_out, float layer_z, @@ -407,7 +416,7 @@ void check_extrusion_entity_stability(const ExtrusionEntity *entity, > params.bridge_distance / (1.0f + (bridging_acc.max_curvature * params.bridge_distance_decrease_by_curvature_factor / PI)); bool between_layers_condition = fabs(dist_from_prev_layer) > 3.0f*flow_width || - prev_layer_lines.get_line(nearest_line_idx).malformation > 0.6f; + prev_layer_lines.get_line(nearest_line_idx).malformation > 3.0f * layer_region->layer()->height; if (in_layer_dist_condition && between_layers_condition) { issues.support_points.emplace_back(to_vec3f(current_line.b), 0.0f, Vec3f(0.f, 0.0f, -1.0f)); @@ -423,8 +432,8 @@ void check_extrusion_entity_stability(const ExtrusionEntity *entity, } if (dist_from_prev_layer > overhang_dist) { malformation_acc.add_distance(current_line.len); - current_line.malformation += 0.3f - * (0.8f + 0.2f * malformation_acc.max_curvature / (1.0f + 0.5f * malformation_acc.distance)); + current_line.malformation += layer_region->layer()->height * (0.5f + + 1.5f * (malformation_acc.max_curvature / PI) * gauss(malformation_acc.distance, 5.0f, 1.0f, 0.2f)); } else { malformation_acc.reset(); } @@ -729,7 +738,7 @@ public: this->sticking_second_moment_of_area_accumulator, this->sticking_second_moment_of_area_covariance_accumulator, this->sticking_area) - * params.bed_adhesion_yield_strength; + * params.get_bed_adhesion_yield_strength(); float bed_weight_arm = (bed_centroid.head<2>() - mass_centroid.head<2>()).norm(); float bed_weight_torque = bed_weight_arm * weight; @@ -759,6 +768,10 @@ public: BOOST_LOG_TRIVIAL(debug) << "SSG: bed_conflict_torque_arm: " << bed_conflict_torque_arm; BOOST_LOG_TRIVIAL(debug) + << "SSG: extruded_line.malformation: " << extruded_line.malformation; + BOOST_LOG_TRIVIAL(debug) + << "SSG: extruder_conflict_force: " << extruder_conflict_force; + BOOST_LOG_TRIVIAL(debug) << "SSG: bed_extruder_conflict_torque: " << bed_extruder_conflict_torque; BOOST_LOG_TRIVIAL(debug) << "SSG: total_torque: " << bed_total_torque << " layer_z: " << layer_z; @@ -971,6 +984,8 @@ Issues check_global_stability(SupportGridFilter supports_presence_grid, for (size_t island_idx = 0; island_idx < islands_graph[layer_idx].islands.size(); ++island_idx) { const Island &island = islands_graph[layer_idx].islands[island_idx]; ObjectPart &part = active_object_parts.access(prev_island_to_object_part_mapping[island_idx]); + + IslandConnection &weakest_conn = prev_island_weakest_connection[island_idx]; #ifdef DETAILED_DEBUG_LOGS weakest_conn.print_info("weakest connection info: "); diff --git a/src/libslic3r/SupportSpotsGenerator.hpp b/src/libslic3r/SupportSpotsGenerator.hpp index 87f1f48c39..b139ae0980 100644 --- a/src/libslic3r/SupportSpotsGenerator.hpp +++ b/src/libslic3r/SupportSpotsGenerator.hpp @@ -2,27 +2,52 @@ #define SRC_LIBSLIC3R_SUPPORTABLEISSUESSEARCH_HPP_ #include "libslic3r/Print.hpp" +#include namespace Slic3r { namespace SupportSpotsGenerator { struct Params { + Params(float overhang_angle_deg, const std::vector& filament_types) : overhang_angle_deg(overhang_angle_deg) + { + if (filament_types.size() > 1) { + BOOST_LOG_TRIVIAL(warning) << "SupportSpotsGenerator does not currently handle different materials properly, only first will be used"; + } + if (filament_types.empty() || filament_types[0].empty()) { + BOOST_LOG_TRIVIAL(error) << "SupportSpotsGenerator error: empty filament_type"; + filament_type = std::string("PLA"); + }else { + filament_type = filament_types[0]; + } + } + // the algorithm should use the following units for all computations: distance [mm], mass [g], time [s], force [g*mm/s^2] - const float bridge_distance = 12.0f; //mm + const float bridge_distance = 15.0f; //mm const float bridge_distance_decrease_by_curvature_factor = 5.0f; // allowed bridge distance = bridge_distance / (this factor * (curvature / PI) ) - float overhang_angle_deg = 50.0f; + const float overhang_angle_deg = 50.0f; const float min_distance_between_support_points = 3.0f; //mm - const float support_points_interface_radius = 0.6f; // mm + const float support_points_interface_radius = 1.5f; // mm + std::string filament_type; const float gravity_constant = 9806.65f; // mm/s^2; gravity acceleration on Earth's surface, algorithm assumes that printer is in upwards position. const float max_acceleration = 9*1000.0f; // mm/s^2 ; max acceleration of object (bed) in XY (NOTE: The max hit is received by the object in the jerk phase, so the usual machine limits are too low) const float filament_density = 1.25e-3f ; // g/mm^3 ; Common filaments are very lightweight, so precise number is not that important - const float bed_adhesion_yield_strength = 0.128f * 1e6f; //MPa * 1e^6 = (g*mm/s^2)/mm^2 = g/(mm*s^2); yield strength of the bed surface const float material_yield_strength = 33.0f * 1e6f; // (g*mm/s^2)/mm^2; 33 MPa is yield strength of ABS, which has the lowest yield strength from common materials. - const float standard_extruder_conflict_force = 50.0f * gravity_constant; // force that can occasionally push the model due to various factors (filament leaks, small curling, ... ); - const float malformations_additive_conflict_extruder_force = 200.0f * gravity_constant; // for areas with possible high layered curled filaments + const float standard_extruder_conflict_force = 20.0f * gravity_constant; // force that can occasionally push the model due to various factors (filament leaks, small curling, ... ); + const float malformations_additive_conflict_extruder_force = 300.0f * gravity_constant; // for areas with possible high layered curled filaments + + // MPa * 1e^6 = (g*mm/s^2)/mm^2 = g/(mm*s^2); yield strength of the bed surface + float get_bed_adhesion_yield_strength() const { + if (filament_type == "PLA"){ + return 0.018f * 1e6f; + } else if (filament_type == "PET" || filament_type == "PETG") { + return 0.3f * 1e6f; + } else { //PLA default value - defensive approach, PLA has quite low adhesion + return 0.018f * 1e6f; + } + } }; struct SupportPoint { @@ -36,8 +61,8 @@ struct Issues { std::vector support_points; }; -std::vector quick_search(const PrintObject *po, const Params ¶ms = Params { }); -Issues full_search(const PrintObject *po, const Params ¶ms = Params { }); +std::vector quick_search(const PrintObject *po, const Params ¶ms); +Issues full_search(const PrintObject *po, const Params ¶ms); } From 970c9e033d9594244d6dfc8735d29f5fd85caddc Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Mon, 15 Aug 2022 13:03:37 +0200 Subject: [PATCH 068/100] fix triangle selector painting on models with transformation --- src/libslic3r/PrintObject.cpp | 10 +++++----- src/libslic3r/SupportSpotsGenerator.cpp | 2 -- src/libslic3r/TriangleSelectorWrapper.cpp | 9 ++++----- src/libslic3r/TriangleSelectorWrapper.hpp | 4 +++- 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 918c4a3259..256cf70270 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -428,14 +428,14 @@ void PrintObject::generate_support_spots() auto obj_transform = this->trafo_centered(); for (ModelVolume *model_volume : this->model_object()->volumes) { if (model_volume->is_model_part()) { - Transform3d model_transformation = model_volume->get_matrix(); - Transform3f inv_transform = (obj_transform * model_transformation).inverse().cast(); - TriangleSelectorWrapper selector { model_volume->mesh() }; + Transform3d mesh_transformation = obj_transform * model_volume->get_matrix(); + Transform3d inv_transform = mesh_transformation.inverse(); + TriangleSelectorWrapper selector { model_volume->mesh(), mesh_transformation}; for (const SupportSpotsGenerator::SupportPoint &support_point : issues.support_points) { - Vec3f point = Vec3f(inv_transform * support_point.position); + Vec3f point = Vec3f(inv_transform.cast() * support_point.position); Vec3f origin = Vec3f( - inv_transform * Vec3f(support_point.position.x(), support_point.position.y(), 0.0f)); + inv_transform.cast() * Vec3f(support_point.position.x(), support_point.position.y(), 0.0f)); selector.enforce_spot(point, origin, 1.5f); } diff --git a/src/libslic3r/SupportSpotsGenerator.cpp b/src/libslic3r/SupportSpotsGenerator.cpp index 584eaf58c2..a1ecd9652a 100644 --- a/src/libslic3r/SupportSpotsGenerator.cpp +++ b/src/libslic3r/SupportSpotsGenerator.cpp @@ -984,8 +984,6 @@ Issues check_global_stability(SupportGridFilter supports_presence_grid, for (size_t island_idx = 0; island_idx < islands_graph[layer_idx].islands.size(); ++island_idx) { const Island &island = islands_graph[layer_idx].islands[island_idx]; ObjectPart &part = active_object_parts.access(prev_island_to_object_part_mapping[island_idx]); - - IslandConnection &weakest_conn = prev_island_weakest_connection[island_idx]; #ifdef DETAILED_DEBUG_LOGS weakest_conn.print_info("weakest connection info: "); diff --git a/src/libslic3r/TriangleSelectorWrapper.cpp b/src/libslic3r/TriangleSelectorWrapper.cpp index ec22ed5dd1..3a5f44d843 100644 --- a/src/libslic3r/TriangleSelectorWrapper.cpp +++ b/src/libslic3r/TriangleSelectorWrapper.cpp @@ -1,11 +1,10 @@ -#include "Model.hpp" #include "TriangleSelectorWrapper.hpp" #include namespace Slic3r { -TriangleSelectorWrapper::TriangleSelectorWrapper(const TriangleMesh &mesh) : - mesh(mesh), selector(mesh), triangles_tree( +TriangleSelectorWrapper::TriangleSelectorWrapper(const TriangleMesh &mesh, const Transform3d& mesh_transform) : + mesh(mesh), mesh_transform(mesh_transform), selector(mesh), triangles_tree( AABBTreeIndirect::build_aabb_tree_over_indexed_triangle_set(mesh.its.vertices, mesh.its.indices)) { } @@ -23,7 +22,7 @@ void TriangleSelectorWrapper::enforce_spot(const Vec3f &point, const Vec3f &orig Vec3f face_normal = its_face_normal(mesh.its, hit.id); if ((point - pos).norm() < radius && face_normal.dot(dir) < 0) { std::unique_ptr cursor = std::make_unique( - pos, origin, radius, Transform3d::Identity(), TriangleSelector::ClippingPlane { }); + pos, origin, radius, this->mesh_transform, TriangleSelector::ClippingPlane { }); selector.select_patch(hit.id, std::move(cursor), EnforcerBlockerType::ENFORCER, Transform3d::Identity(), true, 0.0f); break; @@ -36,7 +35,7 @@ void TriangleSelectorWrapper::enforce_spot(const Vec3f &point, const Vec3f &orig triangles_tree, point, hit_idx_out, hit_point_out); if (dist < radius) { std::unique_ptr cursor = std::make_unique( - point, origin, radius, Transform3d::Identity(), TriangleSelector::ClippingPlane { }); + point, origin, radius, this->mesh_transform, TriangleSelector::ClippingPlane { }); selector.select_patch(hit_idx_out, std::move(cursor), EnforcerBlockerType::ENFORCER, Transform3d::Identity(), true, 0.0f); diff --git a/src/libslic3r/TriangleSelectorWrapper.hpp b/src/libslic3r/TriangleSelectorWrapper.hpp index 10707cc257..22c61d6279 100644 --- a/src/libslic3r/TriangleSelectorWrapper.hpp +++ b/src/libslic3r/TriangleSelectorWrapper.hpp @@ -2,6 +2,7 @@ #define SRC_LIBSLIC3R_TRIANGLESELECTORWRAPPER_HPP_ #include "TriangleSelector.hpp" +#include "Model.hpp" #include "AABBTreeIndirect.hpp" namespace Slic3r { @@ -15,10 +16,11 @@ namespace Slic3r { class TriangleSelectorWrapper { public: const TriangleMesh &mesh; + const Transform3d& mesh_transform; TriangleSelector selector; AABBTreeIndirect::Tree<3, float> triangles_tree; - TriangleSelectorWrapper(const TriangleMesh &mesh); + TriangleSelectorWrapper(const TriangleMesh &mesh, const Transform3d& mesh_transform); void enforce_spot(const Vec3f &point, const Vec3f& origin, float radius); From 6114b04594ac2071181951a94e0e038ff6cc2571 Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Mon, 15 Aug 2022 17:27:06 +0200 Subject: [PATCH 069/100] improve bed adhesion estimation, comupute weight factor sign --- src/libslic3r/SupportSpotsGenerator.cpp | 52 +++++++++++++++++++------ 1 file changed, 41 insertions(+), 11 deletions(-) diff --git a/src/libslic3r/SupportSpotsGenerator.cpp b/src/libslic3r/SupportSpotsGenerator.cpp index a1ecd9652a..518c8cacc3 100644 --- a/src/libslic3r/SupportSpotsGenerator.cpp +++ b/src/libslic3r/SupportSpotsGenerator.cpp @@ -555,9 +555,18 @@ std::tuple reckon_islands( island.sticking_area += sticking_area; Vec2f middle = Vec2f((line.a + line.b) / 2.0f); island.sticking_centroid_accumulator += sticking_area * to_3d(middle, float(layer->slice_z)); - island.sticking_second_moment_of_area_accumulator += sticking_area * middle.cwiseProduct(middle); - island.sticking_second_moment_of_area_covariance_accumulator += sticking_area * middle.x() - * middle.y(); + // Bottom infill lines can be quite long, and algined, so the middle approximaton used above does not work + Vec2f dir = (line.b - line.a).normalized(); + float segment_length = flow_width; // segments of size flow_width + for (float segment_middle_dist = std::min(line.len, segment_length * 0.5f); segment_middle_dist < line.len; + segment_middle_dist += segment_length) { + Vec2f segment_middle = line.a + segment_middle_dist * dir; + island.sticking_second_moment_of_area_accumulator += segment_length * flow_width + * segment_middle.cwiseProduct(segment_middle); + island.sticking_second_moment_of_area_covariance_accumulator += segment_length * flow_width + * segment_middle.x() + * segment_middle.y(); + } } else if (layer_lines[lidx].support_point_generated) { float sticking_area = line.len * flow_width; island.sticking_area += sticking_area; @@ -662,9 +671,8 @@ public: * position.x() * position.y(); } - float compute_elastic_section_modulus( + float compute_directional_xy_variance( const Vec2f &line_dir, - const Vec3f &extreme_point, const Vec3f ¢roid_accumulator, const Vec2f &second_moment_of_area_accumulator, const float &second_moment_of_area_covariance_accumulator, @@ -678,7 +686,6 @@ public: float directional_xy_variance = line_dir.x() * line_dir.x() * variance.x() + line_dir.y() * line_dir.y() * variance.y() + 2.0f * line_dir.x() * line_dir.y() * covariance; - #ifdef DETAILED_DEBUG_LOGS BOOST_LOG_TRIVIAL(debug) << "centroid: " << centroid.x() << " " << centroid.y() << " " << centroid.z(); @@ -689,10 +696,27 @@ public: BOOST_LOG_TRIVIAL(debug) << "directional_xy_variance: " << directional_xy_variance; #endif + return directional_xy_variance; + } + + float compute_elastic_section_modulus( + const Vec2f &line_dir, + const Vec3f &extreme_point, + const Vec3f ¢roid_accumulator, + const Vec2f &second_moment_of_area_accumulator, + const float &second_moment_of_area_covariance_accumulator, + const float &area) const { + + float directional_xy_variance = compute_directional_xy_variance( + line_dir, + centroid_accumulator, + second_moment_of_area_accumulator, + second_moment_of_area_covariance_accumulator, + area); if (directional_xy_variance < EPSILON) { return 0.0f; } - + Vec3f centroid = centroid_accumulator / area; float extreme_fiber_dist = line_alg::distance_to( Linef(centroid.head<2>().cast(), (centroid.head<2>() + Vec2f(line_dir.y(), -line_dir.x())).cast()), extreme_point.head<2>().cast()); @@ -731,7 +755,7 @@ public: return 1.0f; Vec3f bed_centroid = this->sticking_centroid_accumulator / this->sticking_area; - float bed_yield_torque = compute_elastic_section_modulus( + float bed_yield_torque = - compute_elastic_section_modulus( line_dir, extreme_point, this->sticking_centroid_accumulator, @@ -740,8 +764,14 @@ public: this->sticking_area) * params.get_bed_adhesion_yield_strength(); - float bed_weight_arm = (bed_centroid.head<2>() - mass_centroid.head<2>()).norm(); - float bed_weight_torque = bed_weight_arm * weight; + Vec2f bed_weight_arm = (mass_centroid.head<2>() - bed_centroid.head<2>()); + float bed_weight_arm_len = bed_weight_arm.norm(); + float bed_weight_dir_xy_variance = compute_directional_xy_variance(bed_weight_arm, this->sticking_centroid_accumulator, + this->sticking_second_moment_of_area_accumulator, + this->sticking_second_moment_of_area_covariance_accumulator, + this->sticking_area); + float bed_weight_sign = bed_weight_arm_len < 2.0f * sqrt(bed_weight_dir_xy_variance) ? -1.0f : 1.0f; + float bed_weight_torque = bed_weight_sign * bed_weight_arm_len * weight; float bed_movement_arm = std::max(0.0f, mass_centroid.z() - bed_centroid.z()); float bed_movement_torque = movement_force * bed_movement_arm; @@ -750,7 +780,7 @@ public: float bed_extruder_conflict_torque = extruder_conflict_force * bed_conflict_torque_arm; float bed_total_torque = bed_movement_torque + bed_extruder_conflict_torque + bed_weight_torque - - bed_yield_torque; + + bed_yield_torque; #ifdef DETAILED_DEBUG_LOGS BOOST_LOG_TRIVIAL(debug) From 3773de29575acb5880cbc9d7a1d6364c2ec01052 Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Tue, 16 Aug 2022 16:14:22 +0200 Subject: [PATCH 070/100] hardcode overhang angles, remove volumetric filtering (does not work correctly) --- src/libslic3r/PrintObject.cpp | 2 +- src/libslic3r/SupportSpotsGenerator.cpp | 11 +++++++--- src/libslic3r/SupportSpotsGenerator.hpp | 22 +++++++++++--------- src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp | 2 ++ 4 files changed, 23 insertions(+), 14 deletions(-) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 256cf70270..c96e23fe06 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -423,7 +423,7 @@ void PrintObject::generate_support_spots() std::all_of(this->model_object()->volumes.begin(), this->model_object()->volumes.end(), [](const ModelVolume* mv){return mv->supported_facets.empty();}) ) { - SupportSpotsGenerator::Params params{90.001f - this->m_config.support_material_threshold, this->print()->m_config.filament_type.values}; + SupportSpotsGenerator::Params params{this->print()->m_config.filament_type.values}; SupportSpotsGenerator::Issues issues = SupportSpotsGenerator::full_search(this, params); auto obj_transform = this->trafo_centered(); for (ModelVolume *model_volume : this->model_object()->volumes) { diff --git a/src/libslic3r/SupportSpotsGenerator.cpp b/src/libslic3r/SupportSpotsGenerator.cpp index 518c8cacc3..c7fd7e882c 100644 --- a/src/libslic3r/SupportSpotsGenerator.cpp +++ b/src/libslic3r/SupportSpotsGenerator.cpp @@ -364,6 +364,8 @@ void check_extrusion_entity_stability(const ExtrusionEntity *entity, return Vec3f(point.x(), point.y(), layer_z); }; float overhang_dist = tan(params.overhang_angle_deg * PI / 180.0f)*layer_region->layer()->height; + float min_malformation_dist = tan(params.malformation_angle_span_deg.first * PI / 180.0f)*layer_region->layer()->height; + float max_malformation_dist = tan(params.malformation_angle_span_deg.second * PI / 180.0f)*layer_region->layer()->height; Points points { }; entity->collect_points(points); @@ -387,8 +389,7 @@ void check_extrusion_entity_stability(const ExtrusionEntity *entity, ExtrusionPropertiesAccumulator bridging_acc { }; ExtrusionPropertiesAccumulator malformation_acc { }; - bridging_acc.add_distance(params.bridge_distance + 1.0f); // Initialise unsupported distance with larger than tolerable distance -> - // -> it prevents extruding perimeter starts and short loops into air. + bridging_acc.add_distance(params.bridge_distance); const float flow_width = get_flow_width(layer_region, entity->role()); for (size_t line_idx = 0; line_idx < lines.size(); ++line_idx) { @@ -430,7 +431,7 @@ void check_extrusion_entity_stability(const ExtrusionEntity *entity, const ExtrusionLine &nearest_line = prev_layer_lines.get_line(nearest_line_idx); current_line.malformation += 0.9 * nearest_line.malformation; } - if (dist_from_prev_layer > overhang_dist) { + if (dist_from_prev_layer > min_malformation_dist && dist_from_prev_layer < max_malformation_dist) { malformation_acc.add_distance(current_line.len); current_line.malformation += layer_region->layer()->height * (0.5f + 1.5f * (malformation_acc.max_curvature / PI) * gauss(malformation_acc.distance, 5.0f, 1.0f, 0.2f)); @@ -652,6 +653,10 @@ public: island.sticking_second_moment_of_area_covariance_accumulator; } + float get_volume() const { + return volume; + } + void add(const ObjectPart &other) { this->volume_centroid_accumulator += other.volume_centroid_accumulator; this->volume += other.volume; diff --git a/src/libslic3r/SupportSpotsGenerator.hpp b/src/libslic3r/SupportSpotsGenerator.hpp index b139ae0980..a88b7a7be0 100644 --- a/src/libslic3r/SupportSpotsGenerator.hpp +++ b/src/libslic3r/SupportSpotsGenerator.hpp @@ -9,15 +9,16 @@ namespace Slic3r { namespace SupportSpotsGenerator { struct Params { - Params(float overhang_angle_deg, const std::vector& filament_types) : overhang_angle_deg(overhang_angle_deg) - { + Params(const std::vector &filament_types) { if (filament_types.size() > 1) { - BOOST_LOG_TRIVIAL(warning) << "SupportSpotsGenerator does not currently handle different materials properly, only first will be used"; + BOOST_LOG_TRIVIAL(warning) + << "SupportSpotsGenerator does not currently handle different materials properly, only first will be used"; } if (filament_types.empty() || filament_types[0].empty()) { - BOOST_LOG_TRIVIAL(error) << "SupportSpotsGenerator error: empty filament_type"; + BOOST_LOG_TRIVIAL(error) + << "SupportSpotsGenerator error: empty filament_type"; filament_type = std::string("PLA"); - }else { + } else { filament_type = filament_types[0]; } } @@ -25,22 +26,23 @@ struct Params { // the algorithm should use the following units for all computations: distance [mm], mass [g], time [s], force [g*mm/s^2] const float bridge_distance = 15.0f; //mm const float bridge_distance_decrease_by_curvature_factor = 5.0f; // allowed bridge distance = bridge_distance / (this factor * (curvature / PI) ) - const float overhang_angle_deg = 50.0f; + const float overhang_angle_deg = 80.0f; + const std::pair malformation_angle_span_deg = std::pair { 45.0f, 80.0f }; const float min_distance_between_support_points = 3.0f; //mm const float support_points_interface_radius = 1.5f; // mm std::string filament_type; const float gravity_constant = 9806.65f; // mm/s^2; gravity acceleration on Earth's surface, algorithm assumes that printer is in upwards position. - const float max_acceleration = 9*1000.0f; // mm/s^2 ; max acceleration of object (bed) in XY (NOTE: The max hit is received by the object in the jerk phase, so the usual machine limits are too low) - const float filament_density = 1.25e-3f ; // g/mm^3 ; Common filaments are very lightweight, so precise number is not that important + const float max_acceleration = 9 * 1000.0f; // mm/s^2 ; max acceleration of object (bed) in XY (NOTE: The max hit is received by the object in the jerk phase, so the usual machine limits are too low) + const float filament_density = 1.25e-3f; // g/mm^3 ; Common filaments are very lightweight, so precise number is not that important const float material_yield_strength = 33.0f * 1e6f; // (g*mm/s^2)/mm^2; 33 MPa is yield strength of ABS, which has the lowest yield strength from common materials. const float standard_extruder_conflict_force = 20.0f * gravity_constant; // force that can occasionally push the model due to various factors (filament leaks, small curling, ... ); const float malformations_additive_conflict_extruder_force = 300.0f * gravity_constant; // for areas with possible high layered curled filaments // MPa * 1e^6 = (g*mm/s^2)/mm^2 = g/(mm*s^2); yield strength of the bed surface float get_bed_adhesion_yield_strength() const { - if (filament_type == "PLA"){ + if (filament_type == "PLA") { return 0.018f * 1e6f; } else if (filament_type == "PET" || filament_type == "PETG") { return 0.3f * 1e6f; @@ -51,7 +53,7 @@ struct Params { }; struct SupportPoint { - SupportPoint(const Vec3f &position, float force, const Vec3f& direction); + SupportPoint(const Vec3f &position, float force, const Vec3f &direction); Vec3f position; float force; Vec3f direction; diff --git a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp index c193dbf908..2ed7ae6792 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp @@ -332,6 +332,7 @@ void GLGizmoFdmSupports::on_render_input_window(float x, float y, float bottom_l } update_model_object(); + this->waiting_for_autogenerated_supports = false; m_parent.set_as_dirty(); } @@ -379,6 +380,7 @@ void GLGizmoFdmSupports::select_facets_by_angle(float threshold_deg, bool block) Plater::TakeSnapshot snapshot(wxGetApp().plater(), block ? _L("Block supports by angle") : _L("Add supports by angle")); update_model_object(); + this->waiting_for_autogenerated_supports = false; m_parent.set_as_dirty(); } From 15d0c55d54121d02409ff861025ca7af8e4760e7 Mon Sep 17 00:00:00 2001 From: Pavel Mikus Date: Fri, 19 Aug 2022 14:21:36 +0200 Subject: [PATCH 071/100] improve GUI responsivenes, turn off debug info --- src/libslic3r/SupportSpotsGenerator.cpp | 4 ++-- src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/libslic3r/SupportSpotsGenerator.cpp b/src/libslic3r/SupportSpotsGenerator.cpp index c7fd7e882c..eced07b2c2 100644 --- a/src/libslic3r/SupportSpotsGenerator.cpp +++ b/src/libslic3r/SupportSpotsGenerator.cpp @@ -15,8 +15,8 @@ #include "libslic3r/ClipperUtils.hpp" #include "Geometry/ConvexHull.hpp" -#define DETAILED_DEBUG_LOGS -#define DEBUG_FILES +//#define DETAILED_DEBUG_LOGS +//#define DEBUG_FILES #ifdef DEBUG_FILES #include diff --git a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp index 2ed7ae6792..513a655ec0 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp @@ -393,6 +393,8 @@ void GLGizmoFdmSupports::data_changed() ModelObject* mo = m_c->selection_info()->model_object(); if (mo && this->waiting_for_autogenerated_supports) { get_data_from_backend(); + } else { + this->waiting_for_autogenerated_supports = false; } } From f17e3f2c8b723a6b329f6dd8dbdfe4442a466b54 Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Tue, 23 Aug 2022 14:46:08 +0200 Subject: [PATCH 072/100] Added support for ignoring of tiny extrusion drops which are usually not worth the supports. However, it is disabled, as it can currently result in unsupported large columns --- src/libslic3r/SupportSpotsGenerator.cpp | 83 ++++++++++++++----------- src/libslic3r/SupportSpotsGenerator.hpp | 7 ++- 2 files changed, 50 insertions(+), 40 deletions(-) diff --git a/src/libslic3r/SupportSpotsGenerator.cpp b/src/libslic3r/SupportSpotsGenerator.cpp index eced07b2c2..a405031681 100644 --- a/src/libslic3r/SupportSpotsGenerator.cpp +++ b/src/libslic3r/SupportSpotsGenerator.cpp @@ -41,7 +41,7 @@ public: bool is_external_perimeter() const { assert(origin_entity != nullptr); - return origin_entity->role() == erExternalPerimeter; + return origin_entity->role() == erExternalPerimeter || origin_entity->role() == erOverhangPerimeter; } Vec2f a; @@ -363,9 +363,11 @@ void check_extrusion_entity_stability(const ExtrusionEntity *entity, const auto to_vec3f = [layer_z](const Vec2f &point) { return Vec3f(point.x(), point.y(), layer_z); }; - float overhang_dist = tan(params.overhang_angle_deg * PI / 180.0f)*layer_region->layer()->height; - float min_malformation_dist = tan(params.malformation_angle_span_deg.first * PI / 180.0f)*layer_region->layer()->height; - float max_malformation_dist = tan(params.malformation_angle_span_deg.second * PI / 180.0f)*layer_region->layer()->height; + float overhang_dist = tan(params.overhang_angle_deg * PI / 180.0f) * layer_region->layer()->height; + float min_malformation_dist = tan(params.malformation_angle_span_deg.first * PI / 180.0f) + * layer_region->layer()->height; + float max_malformation_dist = tan(params.malformation_angle_span_deg.second * PI / 180.0f) + * layer_region->layer()->height; Points points { }; entity->collect_points(points); @@ -387,9 +389,14 @@ void check_extrusion_entity_stability(const ExtrusionEntity *entity, } } + if (entity->total_volume() < params.supportable_volume_threshold) { + checked_lines_out.insert(checked_lines_out.end(), lines.begin(), lines.end()); + return; + } + ExtrusionPropertiesAccumulator bridging_acc { }; ExtrusionPropertiesAccumulator malformation_acc { }; - bridging_acc.add_distance(params.bridge_distance); + bridging_acc.add_distance(params.bridge_distance + 1.0f); const float flow_width = get_flow_width(layer_region, entity->role()); for (size_t line_idx = 0; line_idx < lines.size(); ++line_idx) { @@ -416,7 +423,7 @@ void check_extrusion_entity_stability(const ExtrusionEntity *entity, bool in_layer_dist_condition = bridging_acc.distance > params.bridge_distance / (1.0f + (bridging_acc.max_curvature * params.bridge_distance_decrease_by_curvature_factor / PI)); - bool between_layers_condition = fabs(dist_from_prev_layer) > 3.0f*flow_width || + bool between_layers_condition = fabs(dist_from_prev_layer) > 3.0f * flow_width || prev_layer_lines.get_line(nearest_line_idx).malformation > 3.0f * layer_region->layer()->height; if (in_layer_dist_condition && between_layers_condition) { @@ -427,14 +434,17 @@ void check_extrusion_entity_stability(const ExtrusionEntity *entity, } //malformation - if (fabs(dist_from_prev_layer) < 3.0f*flow_width) { + if (fabs(dist_from_prev_layer) < 3.0f * flow_width) { const ExtrusionLine &nearest_line = prev_layer_lines.get_line(nearest_line_idx); current_line.malformation += 0.9 * nearest_line.malformation; } if (dist_from_prev_layer > min_malformation_dist && dist_from_prev_layer < max_malformation_dist) { malformation_acc.add_distance(current_line.len); - current_line.malformation += layer_region->layer()->height * (0.5f + - 1.5f * (malformation_acc.max_curvature / PI) * gauss(malformation_acc.distance, 5.0f, 1.0f, 0.2f)); + current_line.malformation += layer_region->layer()->height + * (0.5f + + + 1.5f * (malformation_acc.max_curvature / PI) + * gauss(malformation_acc.distance, 5.0f, 1.0f, 0.2f)); } else { malformation_acc.reset(); } @@ -464,12 +474,13 @@ std::tuple reckon_islands( } } - std::vector islands; // these search trees will be used to determine to which island does the extrusion begin - std::vector> island_extrusions; //final assigment of each extrusion to an island + std::vector islands; // these search trees will be used to determine to which island does the extrusion belong. + std::vector> island_extrusions; //final assigment of each extrusion to an island. // initliaze the search from external perimeters - at the beginning, there is island candidate for each external perimeter. // some of them will disappear (e.g. holes) for (size_t e = 0; e < extrusions.size(); ++e) { - if (layer_lines[extrusions[e].first].is_external_perimeter()) { + if (layer_lines[extrusions[e].first].origin_entity->is_loop() && + layer_lines[extrusions[e].first].is_external_perimeter()) { std::vector copy(extrusions[e].second - extrusions[e].first); for (size_t ex_line_idx = extrusions[e].first; ex_line_idx < extrusions[e].second; ++ex_line_idx) { copy[ex_line_idx - extrusions[e].first] = layer_lines[ex_line_idx]; @@ -478,8 +489,8 @@ std::tuple reckon_islands( island_extrusions.push_back( { e }); } } - // backup code if islands not found - this can currently happen, as external perimeters may be also pure overhang perimeters, and there is no - // way to distinguish external extrusions with total certainty. + + // backup code if islands not found // If that happens, just make the first extrusion into island - it may be wrong, but it won't crash. if (islands.empty() && !extrusions.empty()) { std::vector copy(extrusions[0].second - extrusions[0].first); @@ -492,9 +503,13 @@ std::tuple reckon_islands( // assign non external extrusions to islands for (size_t e = 0; e < extrusions.size(); ++e) { - if (!layer_lines[extrusions[e].first].is_external_perimeter()) { + if (!layer_lines[extrusions[e].first].origin_entity->is_loop() || + !layer_lines[extrusions[e].first].is_external_perimeter()) { bool island_assigned = false; for (size_t i = 0; i < islands.size(); ++i) { + if (island_extrusions[i].empty()) { + continue; + } size_t _idx = 0; Vec2f _pt = Vec2f::Zero(); if (islands[i].signed_distance_from_lines(layer_lines[extrusions[e].first].a, _idx, _pt) < 0) { @@ -509,24 +524,6 @@ std::tuple reckon_islands( } } } - // merge islands which are embedded within each other (mainly holes) - for (size_t i = 0; i < islands.size(); ++i) { - if (islands[i].get_lines().empty()) { - continue; - } - for (size_t j = 0; j < islands.size(); ++j) { - if (islands[j].get_lines().empty() || i == j) { - continue; - } - size_t _idx; - Vec2f _pt; - if (islands[i].signed_distance_from_lines(islands[j].get_line(0).a, _idx, _pt) < 0) { - island_extrusions[i].insert(island_extrusions[i].end(), island_extrusions[j].begin(), - island_extrusions[j].end()); - island_extrusions[j].clear(); - } - } - } float flow_width = get_flow_width(layer->regions()[0], erExternalPerimeter); // after filtering the layer lines into islands, build the result LayerIslands structure. @@ -559,7 +556,8 @@ std::tuple reckon_islands( // Bottom infill lines can be quite long, and algined, so the middle approximaton used above does not work Vec2f dir = (line.b - line.a).normalized(); float segment_length = flow_width; // segments of size flow_width - for (float segment_middle_dist = std::min(line.len, segment_length * 0.5f); segment_middle_dist < line.len; + for (float segment_middle_dist = std::min(line.len, segment_length * 0.5f); + segment_middle_dist < line.len; segment_middle_dist += segment_length) { Vec2f segment_middle = line.a + segment_middle_dist * dir; island.sticking_second_moment_of_area_accumulator += segment_length * flow_width @@ -723,7 +721,8 @@ public: } Vec3f centroid = centroid_accumulator / area; float extreme_fiber_dist = line_alg::distance_to( - Linef(centroid.head<2>().cast(), (centroid.head<2>() + Vec2f(line_dir.y(), -line_dir.x())).cast()), + Linef(centroid.head<2>().cast(), + (centroid.head<2>() + Vec2f(line_dir.y(), -line_dir.x())).cast()), extreme_point.head<2>().cast()); float elastic_section_modulus = area * directional_xy_variance / extreme_fiber_dist; @@ -760,7 +759,7 @@ public: return 1.0f; Vec3f bed_centroid = this->sticking_centroid_accumulator / this->sticking_area; - float bed_yield_torque = - compute_elastic_section_modulus( + float bed_yield_torque = -compute_elastic_section_modulus( line_dir, extreme_point, this->sticking_centroid_accumulator, @@ -771,7 +770,8 @@ public: Vec2f bed_weight_arm = (mass_centroid.head<2>() - bed_centroid.head<2>()); float bed_weight_arm_len = bed_weight_arm.norm(); - float bed_weight_dir_xy_variance = compute_directional_xy_variance(bed_weight_arm, this->sticking_centroid_accumulator, + float bed_weight_dir_xy_variance = compute_directional_xy_variance(bed_weight_arm, + this->sticking_centroid_accumulator, this->sticking_second_moment_of_area_accumulator, this->sticking_second_moment_of_area_covariance_accumulator, this->sticking_area); @@ -1019,6 +1019,13 @@ Issues check_global_stability(SupportGridFilter supports_presence_grid, for (size_t island_idx = 0; island_idx < islands_graph[layer_idx].islands.size(); ++island_idx) { const Island &island = islands_graph[layer_idx].islands[island_idx]; ObjectPart &part = active_object_parts.access(prev_island_to_object_part_mapping[island_idx]); + + //skip small drops of material - if they grow in size, they will be supported in next layers; + // if they dont grow, they are not worthy + if (part.get_volume() < params.supportable_volume_threshold) { + continue; + } + IslandConnection &weakest_conn = prev_island_weakest_connection[island_idx]; #ifdef DETAILED_DEBUG_LOGS weakest_conn.print_info("weakest connection info: "); @@ -1132,7 +1139,7 @@ std::tuple> check_extrusions_and_build_graph(c } for (const auto &line : layer_lines) { if (line.malformation > 0.0f) { - Vec3f color = value_to_rgbf(0, 1.0f, line.malformation); + Vec3f color = value_to_rgbf(-EPSILON, 1.0f, line.malformation); fprintf(malform_f, "v %f %f %f %f %f %f\n", line.b[0], line.b[1], layer->slice_z, color[0], color[1], color[2]); } diff --git a/src/libslic3r/SupportSpotsGenerator.hpp b/src/libslic3r/SupportSpotsGenerator.hpp index a88b7a7be0..b059755e38 100644 --- a/src/libslic3r/SupportSpotsGenerator.hpp +++ b/src/libslic3r/SupportSpotsGenerator.hpp @@ -24,14 +24,17 @@ struct Params { } // the algorithm should use the following units for all computations: distance [mm], mass [g], time [s], force [g*mm/s^2] - const float bridge_distance = 15.0f; //mm - const float bridge_distance_decrease_by_curvature_factor = 5.0f; // allowed bridge distance = bridge_distance / (this factor * (curvature / PI) ) + const float bridge_distance = 12.0f; //mm + const float bridge_distance_decrease_by_curvature_factor = 5.0f; // allowed bridge distance = bridge_distance / (1 + this factor * (curvature / PI) ) const float overhang_angle_deg = 80.0f; const std::pair malformation_angle_span_deg = std::pair { 45.0f, 80.0f }; const float min_distance_between_support_points = 3.0f; //mm const float support_points_interface_radius = 1.5f; // mm + // NOTE: Currently disabled, does not work correctly due to inability of the algorithm to correctly detect islands at each layer + const float supportable_volume_threshold = 0.0f; // mm^3 + std::string filament_type; const float gravity_constant = 9806.65f; // mm/s^2; gravity acceleration on Earth's surface, algorithm assumes that printer is in upwards position. const float max_acceleration = 9 * 1000.0f; // mm/s^2 ; max acceleration of object (bed) in XY (NOTE: The max hit is received by the object in the jerk phase, so the usual machine limits are too low) From 905c602995979f3f775b7a359abc4d01de2e5de3 Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Tue, 6 Sep 2022 12:23:42 +0200 Subject: [PATCH 073/100] remove underscore from varaibles, its not C++ friendly practice --- .clang-format | 2 +- src/libslic3r/SupportSpotsGenerator.cpp | 22 +++++++++++----------- src/libslic3r/SupportSpotsGenerator.hpp | 2 +- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.clang-format b/.clang-format index 440c89ec57..6ec205af84 100644 --- a/.clang-format +++ b/.clang-format @@ -46,7 +46,7 @@ BreakConstructorInitializersBeforeComma: false BreakConstructorInitializers: BeforeComma BreakAfterJavaFieldAnnotations: false BreakStringLiterals: true -ColumnLimit: 78 +ColumnLimit: 140 CommentPragmas: '^ IWYU pragma:' CompactNamespaces: true ConstructorInitializerAllOnOneLineOrOnePerLine: true diff --git a/src/libslic3r/SupportSpotsGenerator.cpp b/src/libslic3r/SupportSpotsGenerator.cpp index a405031681..ce071a766c 100644 --- a/src/libslic3r/SupportSpotsGenerator.cpp +++ b/src/libslic3r/SupportSpotsGenerator.cpp @@ -31,8 +31,8 @@ public: ExtrusionLine() : a(Vec2f::Zero()), b(Vec2f::Zero()), len(0.0f), origin_entity(nullptr) { } - ExtrusionLine(const Vec2f &_a, const Vec2f &_b, const ExtrusionEntity *origin_entity) : - a(_a), b(_b), len((_a - _b).norm()), origin_entity(origin_entity) { + ExtrusionLine(const Vec2f &a, const Vec2f &b, const ExtrusionEntity *origin_entity) : + a(a), b(b), len((a - b).norm()), origin_entity(origin_entity) { } float length() { @@ -510,9 +510,9 @@ std::tuple reckon_islands( if (island_extrusions[i].empty()) { continue; } - size_t _idx = 0; - Vec2f _pt = Vec2f::Zero(); - if (islands[i].signed_distance_from_lines(layer_lines[extrusions[e].first].a, _idx, _pt) < 0) { + size_t idx = 0; + Vec2f pt = Vec2f::Zero(); + if (islands[i].signed_distance_from_lines(layer_lines[extrusions[e].first].a, idx, pt) < 0) { island_extrusions[i].push_back(e); island_assigned = true; break; @@ -579,7 +579,7 @@ std::tuple reckon_islands( result.islands.push_back(island); } - //LayerIslands structure built. Now determine connections and their areas to the previous layer using raterization. + //LayerIslands structure built. Now determine connections and their areas to the previous layer using rasterization. PixelGrid current_layer_grid = prev_layer_grid; current_layer_grid.clear(); // build index image of current layer @@ -1040,10 +1040,10 @@ Issues check_global_stability(SupportGridFilter supports_presence_grid, } else { unchecked_dist = line.len; Vec2f target_point; - size_t _idx; + size_t idx; Vec3f pivot_site_search_point = to_3d(Vec2f(line.b + (line.b - line.a).normalized() * 300.0f), layer_z); - island_lines_dist.signed_distance_from_lines(pivot_site_search_point.head<2>(), _idx, + island_lines_dist.signed_distance_from_lines(pivot_site_search_point.head<2>(), idx, target_point); Vec3f support_point = to_3d(target_point, layer_z); auto force = part.is_stable_while_extruding(weakest_conn, line, support_point, layer_z, params); @@ -1239,9 +1239,9 @@ void debug_export(Issues issues, std::string file_name) { } #endif -std::vector quick_search(const PrintObject *po, const Params ¶ms) { - return {}; -} +// std::vector quick_search(const PrintObject *po, const Params ¶ms) { +// return {}; +// } Issues full_search(const PrintObject *po, const Params ¶ms) { auto [local_issues, graph] = check_extrusions_and_build_graph(po, params); diff --git a/src/libslic3r/SupportSpotsGenerator.hpp b/src/libslic3r/SupportSpotsGenerator.hpp index b059755e38..b991b4f6b1 100644 --- a/src/libslic3r/SupportSpotsGenerator.hpp +++ b/src/libslic3r/SupportSpotsGenerator.hpp @@ -66,7 +66,7 @@ struct Issues { std::vector support_points; }; -std::vector quick_search(const PrintObject *po, const Params ¶ms); +// std::vector quick_search(const PrintObject *po, const Params ¶ms); Issues full_search(const PrintObject *po, const Params ¶ms); } From 8a1a31992ae9678716d482577fad300ab6dd6276 Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Tue, 6 Sep 2022 16:29:17 +0200 Subject: [PATCH 074/100] use Polyline instead of Points, so that there are no duplicate points --- src/libslic3r/SupportSpotsGenerator.cpp | 110 +++++++++++------------- src/libslic3r/SupportSpotsGenerator.hpp | 7 +- 2 files changed, 58 insertions(+), 59 deletions(-) diff --git a/src/libslic3r/SupportSpotsGenerator.cpp b/src/libslic3r/SupportSpotsGenerator.cpp index ce071a766c..f976bac1ac 100644 --- a/src/libslic3r/SupportSpotsGenerator.cpp +++ b/src/libslic3r/SupportSpotsGenerator.cpp @@ -1,5 +1,6 @@ #include "SupportSpotsGenerator.hpp" +#include "ExtrusionEntity.hpp" #include "tbb/parallel_for.h" #include "tbb/blocked_range.h" #include "tbb/blocked_range2d.h" @@ -346,6 +347,41 @@ float gauss(float value, float mean_x_coord, float mean_value, float falloff_spe return mean_value * (std::exp(exponent) - 1.0f) / (std::exp(1.0f) - 1.0f); } +void push_lines(const ExtrusionEntity *e, std::vector& destination) +{ + assert(!e->is_collection()); + Polyline pl = e->as_polyline(); + for (int point_idx = 0; point_idx < int(pl.points.size() - 1); ++point_idx) { + Vec2f start = unscaled(pl.points[point_idx]).cast(); + Vec2f next = unscaled(pl.points[point_idx + 1]).cast(); + ExtrusionLine line{start, next, e}; + destination.push_back(line); + } +} + +std::vector to_short_lines(const ExtrusionEntity *e, float length_limit) +{ + assert(!e->is_collection()); + Polyline pl = e->as_polyline(); + std::vector lines; + lines.reserve(pl.points.size() * 1.5f); + for (int point_idx = 0; point_idx < int(pl.points.size() - 1); ++point_idx) { + Vec2f start = unscaled(pl.points[point_idx]).cast(); + Vec2f next = unscaled(pl.points[point_idx + 1]).cast(); + Vec2f v = next - start; // vector from next to current + float dist_to_next = v.norm(); + v.normalize(); + int lines_count = int(std::ceil(dist_to_next / length_limit)); + float step_size = dist_to_next / lines_count; + for (int i = 0; i < lines_count; ++i) { + Vec2f a(start + v * (i * step_size)); + Vec2f b(start + v * ((i + 1) * step_size)); + lines.emplace_back(a, b, e); + } + } + return lines; +} + void check_extrusion_entity_stability(const ExtrusionEntity *entity, std::vector &checked_lines_out, float layer_z, @@ -371,23 +407,8 @@ void check_extrusion_entity_stability(const ExtrusionEntity *entity, Points points { }; entity->collect_points(points); - std::vector lines; - lines.reserve(points.size() * 1.5f); - lines.emplace_back(unscaled(points[0]).cast(), unscaled(points[0]).cast(), entity); - for (int point_idx = 0; point_idx < int(points.size() - 1); ++point_idx) { - Vec2f start = unscaled(points[point_idx]).cast(); - Vec2f next = unscaled(points[point_idx + 1]).cast(); - Vec2f v = next - start; // vector from next to current - float dist_to_next = v.norm(); - v.normalize(); - int lines_count = int(std::ceil(dist_to_next / params.bridge_distance)); - float step_size = dist_to_next / lines_count; - for (int i = 0; i < lines_count; ++i) { - Vec2f a(start + v * (i * step_size)); - Vec2f b(start + v * ((i + 1) * step_size)); - lines.emplace_back(a, b, entity); - } - } + std::vector lines = to_short_lines(entity, params.bridge_distance); + if (lines.empty()) return; if (entity->total_volume() < params.supportable_volume_threshold) { checked_lines_out.insert(checked_lines_out.end(), lines.begin(), lines.end()); @@ -440,11 +461,9 @@ void check_extrusion_entity_stability(const ExtrusionEntity *entity, } if (dist_from_prev_layer > min_malformation_dist && dist_from_prev_layer < max_malformation_dist) { malformation_acc.add_distance(current_line.len); - current_line.malformation += layer_region->layer()->height - * (0.5f - + - 1.5f * (malformation_acc.max_curvature / PI) - * gauss(malformation_acc.distance, 5.0f, 1.0f, 0.2f)); + current_line.malformation += layer_region->layer()->height * + (0.5f + 1.5f * (malformation_acc.max_curvature / PI) * + gauss(malformation_acc.distance, 5.0f, 1.0f, 0.2f)); } else { malformation_acc.reset(); } @@ -1049,18 +1068,20 @@ Issues check_global_stability(SupportGridFilter supports_presence_grid, auto force = part.is_stable_while_extruding(weakest_conn, line, support_point, layer_z, params); if (force > 0) { if (!supports_presence_grid.position_taken(support_point)) { - float area = params.support_points_interface_radius * params.support_points_interface_radius - * float(PI); + float area = std::min(float(unscaled(line.origin_entity->length())), + params.support_points_interface_radius * params.support_points_interface_radius + * float(PI)); + float altered_area = area * params.get_support_spots_adhesion_strength() / params.get_bed_adhesion_yield_strength(); part.add_support_point(support_point, area); issues.support_points.emplace_back(support_point, force, to_3d(Vec2f(line.b - line.a).normalized(), 0.0f)); supports_presence_grid.take_position(support_point); - weakest_conn.area += area; - weakest_conn.centroid_accumulator += support_point * area; - weakest_conn.second_moment_of_area_accumulator += area + weakest_conn.area += altered_area; + weakest_conn.centroid_accumulator += support_point * altered_area; + weakest_conn.second_moment_of_area_accumulator += altered_area * support_point.head<2>().cwiseProduct(support_point.head<2>()); - weakest_conn.second_moment_of_area_covariance_accumulator += area * support_point.x() + weakest_conn.second_moment_of_area_covariance_accumulator += altered_area * support_point.x() * support_point.y(); } } @@ -1090,32 +1111,12 @@ std::tuple> check_extrusions_and_build_graph(c for (const LayerRegion *layer_region : layer->regions()) { for (const ExtrusionEntity *ex_entity : layer_region->perimeters.entities) { for (const ExtrusionEntity *perimeter : static_cast(ex_entity)->entities) { - Points points { }; - perimeter->collect_points(points); - for (int point_idx = 0; point_idx < int(points.size() - 1); ++point_idx) { - Vec2f start = unscaled(points[point_idx]).cast(); - Vec2f next = unscaled(points[point_idx + 1]).cast(); - ExtrusionLine line { start, next, perimeter }; - layer_lines.push_back(line); - } - if (perimeter->is_loop()) { - Vec2f start = unscaled(points[points.size() - 1]).cast(); - Vec2f next = unscaled(points[0]).cast(); - ExtrusionLine line { start, next, perimeter }; - layer_lines.push_back(line); - } + push_lines(perimeter, layer_lines); } // perimeter } // ex_entity for (const ExtrusionEntity *ex_entity : layer_region->fills.entities) { for (const ExtrusionEntity *fill : static_cast(ex_entity)->entities) { - Points points { }; - fill->collect_points(points); - for (int point_idx = 0; point_idx < int(points.size() - 1); ++point_idx) { - Vec2f start = unscaled(points[point_idx]).cast(); - Vec2f next = unscaled(points[point_idx + 1]).cast(); - ExtrusionLine line { start, next, fill }; - layer_lines.push_back(line); - } + push_lines(fill, layer_lines); } // fill } // ex_entity } // region @@ -1165,14 +1166,7 @@ std::tuple> check_extrusions_and_build_graph(c check_extrusion_entity_stability(fill, layer_lines, layer->slice_z, layer_region, external_lines, issues, params); } else { - Points points { }; - fill->collect_points(points); - for (int point_idx = 0; point_idx < int(points.size() - 1); ++point_idx) { - Vec2f start = unscaled(points[point_idx]).cast(); - Vec2f next = unscaled(points[point_idx + 1]).cast(); - ExtrusionLine line { start, next, fill }; - layer_lines.push_back(line); - } + push_lines(fill, layer_lines); } } // fill } // ex_entity diff --git a/src/libslic3r/SupportSpotsGenerator.hpp b/src/libslic3r/SupportSpotsGenerator.hpp index b991b4f6b1..9a45c38d98 100644 --- a/src/libslic3r/SupportSpotsGenerator.hpp +++ b/src/libslic3r/SupportSpotsGenerator.hpp @@ -30,7 +30,7 @@ struct Params { const std::pair malformation_angle_span_deg = std::pair { 45.0f, 80.0f }; const float min_distance_between_support_points = 3.0f; //mm - const float support_points_interface_radius = 1.5f; // mm + const float support_points_interface_radius = 2.0f; // mm // NOTE: Currently disabled, does not work correctly due to inability of the algorithm to correctly detect islands at each layer const float supportable_volume_threshold = 0.0f; // mm^3 @@ -53,6 +53,11 @@ struct Params { return 0.018f * 1e6f; } } + + //just return PLA adhesion value as value for supports + float get_support_spots_adhesion_strength() const { + return 0.018f * 1e6f; + } }; struct SupportPoint { From a6a723928c6f5eda7b51b634fdf25cff64dac376 Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Wed, 7 Sep 2022 17:11:58 +0200 Subject: [PATCH 075/100] create cradle around small parts, break tiny connections in the model graph, fix PETG support --- src/libslic3r/PrintObject.cpp | 2 +- src/libslic3r/SupportSpotsGenerator.cpp | 61 ++++++++++++------------- src/libslic3r/SupportSpotsGenerator.hpp | 27 +++++------ 3 files changed, 44 insertions(+), 46 deletions(-) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 71ec15e07d..f4b69bb812 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -435,7 +435,7 @@ void PrintObject::generate_support_spots() Vec3f point = Vec3f(inv_transform.cast() * support_point.position); Vec3f origin = Vec3f( inv_transform.cast() * Vec3f(support_point.position.x(), support_point.position.y(), 0.0f)); - selector.enforce_spot(point, origin, 1.5f); + selector.enforce_spot(point, origin, support_point.spot_radius); } model_volume->supported_facets.set(selector.selector); diff --git a/src/libslic3r/SupportSpotsGenerator.cpp b/src/libslic3r/SupportSpotsGenerator.cpp index f976bac1ac..0a6ab5e9da 100644 --- a/src/libslic3r/SupportSpotsGenerator.cpp +++ b/src/libslic3r/SupportSpotsGenerator.cpp @@ -16,8 +16,8 @@ #include "libslic3r/ClipperUtils.hpp" #include "Geometry/ConvexHull.hpp" -//#define DETAILED_DEBUG_LOGS -//#define DEBUG_FILES +// #define DETAILED_DEBUG_LOGS +// #define DEBUG_FILES #ifdef DEBUG_FILES #include @@ -66,8 +66,8 @@ auto get_b(ExtrusionLine &&l) { namespace SupportSpotsGenerator { -SupportPoint::SupportPoint(const Vec3f &position, float force, const Vec3f &direction) : - position(position), force(force), direction(direction) { +SupportPoint::SupportPoint(const Vec3f &position, float force, float spot_radius, const Vec3f &direction) : + position(position), force(force), spot_radius(spot_radius), direction(direction) { } class LinesDistancer { @@ -405,16 +405,9 @@ void check_extrusion_entity_stability(const ExtrusionEntity *entity, float max_malformation_dist = tan(params.malformation_angle_span_deg.second * PI / 180.0f) * layer_region->layer()->height; - Points points { }; - entity->collect_points(points); std::vector lines = to_short_lines(entity, params.bridge_distance); if (lines.empty()) return; - if (entity->total_volume() < params.supportable_volume_threshold) { - checked_lines_out.insert(checked_lines_out.end(), lines.begin(), lines.end()); - return; - } - ExtrusionPropertiesAccumulator bridging_acc { }; ExtrusionPropertiesAccumulator malformation_acc { }; bridging_acc.add_distance(params.bridge_distance + 1.0f); @@ -429,6 +422,7 @@ void check_extrusion_entity_stability(const ExtrusionEntity *entity, curr_angle = angle(v1, v2); } bridging_acc.add_angle(curr_angle); + // malformation in concave angles does not happen malformation_acc.add_angle(std::max(0.0f, curr_angle)); size_t nearest_line_idx; @@ -444,11 +438,11 @@ void check_extrusion_entity_stability(const ExtrusionEntity *entity, bool in_layer_dist_condition = bridging_acc.distance > params.bridge_distance / (1.0f + (bridging_acc.max_curvature * params.bridge_distance_decrease_by_curvature_factor / PI)); - bool between_layers_condition = fabs(dist_from_prev_layer) > 3.0f * flow_width || + bool between_layers_condition = fabs(dist_from_prev_layer) > flow_width || prev_layer_lines.get_line(nearest_line_idx).malformation > 3.0f * layer_region->layer()->height; if (in_layer_dist_condition && between_layers_condition) { - issues.support_points.emplace_back(to_vec3f(current_line.b), 0.0f, Vec3f(0.f, 0.0f, -1.0f)); + issues.support_points.emplace_back(to_vec3f(current_line.b), 0.0f, params.support_points_interface_radius, Vec3f(0.f, 0.0f, -1.0f)); current_line.support_point_generated = true; bridging_acc.reset(); } @@ -562,7 +556,7 @@ std::tuple reckon_islands( for (size_t lidx = extrusions[extrusion_idx].first; lidx < extrusions[extrusion_idx].second; ++lidx) { line_to_island_mapping[lidx] = result.islands.size(); const ExtrusionLine &line = layer_lines[lidx]; - float volume = line.origin_entity->min_mm3_per_mm() * line.len; + float volume = line.len * layer->height * flow_width * PI / 4.0f; island.volume += volume; island.volume_centroid_accumulator += to_3d(Vec2f((line.a + line.b) / 2.0f), float(layer->slice_z)) * volume; @@ -632,6 +626,15 @@ std::tuple reckon_islands( } } + // filter out very small connection areas, they brake the graph building + for (Island &island : result.islands) { + std::vector conns_to_remove; + for (const auto &conn : island.connected_islands) { + if (conn.second.area < params.connections_min_considerable_area) { conns_to_remove.push_back(conn.first); } + } + for (size_t conn : conns_to_remove) { island.connected_islands.erase(conn); } + } + return {result, current_layer_grid}; } @@ -1039,12 +1042,6 @@ Issues check_global_stability(SupportGridFilter supports_presence_grid, const Island &island = islands_graph[layer_idx].islands[island_idx]; ObjectPart &part = active_object_parts.access(prev_island_to_object_part_mapping[island_idx]); - //skip small drops of material - if they grow in size, they will be supported in next layers; - // if they dont grow, they are not worthy - if (part.get_volume() < params.supportable_volume_threshold) { - continue; - } - IslandConnection &weakest_conn = prev_island_weakest_connection[island_idx]; #ifdef DETAILED_DEBUG_LOGS weakest_conn.print_info("weakest connection info: "); @@ -1068,21 +1065,21 @@ Issues check_global_stability(SupportGridFilter supports_presence_grid, auto force = part.is_stable_while_extruding(weakest_conn, line, support_point, layer_z, params); if (force > 0) { if (!supports_presence_grid.position_taken(support_point)) { - float area = std::min(float(unscaled(line.origin_entity->length())), - params.support_points_interface_radius * params.support_points_interface_radius - * float(PI)); - float altered_area = area * params.get_support_spots_adhesion_strength() / params.get_bed_adhesion_yield_strength(); - part.add_support_point(support_point, area); - issues.support_points.emplace_back(support_point, force, - to_3d(Vec2f(line.b - line.a).normalized(), 0.0f)); + float orig_area = params.support_points_interface_radius * params.support_points_interface_radius * float(PI); + // artifically lower the area for materials that have strong bed adhesion, as this adhesion does not apply for support points + float altered_area = orig_area * params.get_support_spots_adhesion_strength() / params.get_bed_adhesion_yield_strength(); + part.add_support_point(support_point, altered_area); + + float radius = part.get_volume() < params.small_parts_threshold ? params.small_parts_support_points_interface_radius : params.support_points_interface_radius; + issues.support_points.emplace_back(support_point, force, radius, to_3d(Vec2f(line.b - line.a).normalized(), 0.0f)); supports_presence_grid.take_position(support_point); weakest_conn.area += altered_area; weakest_conn.centroid_accumulator += support_point * altered_area; - weakest_conn.second_moment_of_area_accumulator += altered_area - * support_point.head<2>().cwiseProduct(support_point.head<2>()); - weakest_conn.second_moment_of_area_covariance_accumulator += altered_area * support_point.x() - * support_point.y(); + weakest_conn.second_moment_of_area_accumulator += altered_area * + support_point.head<2>().cwiseProduct(support_point.head<2>()); + weakest_conn.second_moment_of_area_covariance_accumulator += altered_area * support_point.x() * + support_point.y(); } } } @@ -1104,7 +1101,7 @@ std::tuple> check_extrusions_and_build_graph(c std::vector islands_graph; std::vector layer_lines; float flow_width = get_flow_width(po->layers()[po->layer_count() - 1]->regions()[0], erExternalPerimeter); - PixelGrid prev_layer_grid(po, flow_width); + PixelGrid prev_layer_grid(po, flow_width*2.0f); // PREPARE BASE LAYER const Layer *layer = po->layers()[0]; diff --git a/src/libslic3r/SupportSpotsGenerator.hpp b/src/libslic3r/SupportSpotsGenerator.hpp index 9a45c38d98..61a1bc5385 100644 --- a/src/libslic3r/SupportSpotsGenerator.hpp +++ b/src/libslic3r/SupportSpotsGenerator.hpp @@ -30,40 +30,41 @@ struct Params { const std::pair malformation_angle_span_deg = std::pair { 45.0f, 80.0f }; const float min_distance_between_support_points = 3.0f; //mm - const float support_points_interface_radius = 2.0f; // mm - - // NOTE: Currently disabled, does not work correctly due to inability of the algorithm to correctly detect islands at each layer - const float supportable_volume_threshold = 0.0f; // mm^3 + const float support_points_interface_radius = 1.5f; // mm + const float connections_min_considerable_area = 1.5f; //mm^2 + const float small_parts_threshold = 5.0f; //mm^3 + const float small_parts_support_points_interface_radius = 3.0f; // mm std::string filament_type; const float gravity_constant = 9806.65f; // mm/s^2; gravity acceleration on Earth's surface, algorithm assumes that printer is in upwards position. const float max_acceleration = 9 * 1000.0f; // mm/s^2 ; max acceleration of object (bed) in XY (NOTE: The max hit is received by the object in the jerk phase, so the usual machine limits are too low) - const float filament_density = 1.25e-3f; // g/mm^3 ; Common filaments are very lightweight, so precise number is not that important - const float material_yield_strength = 33.0f * 1e6f; // (g*mm/s^2)/mm^2; 33 MPa is yield strength of ABS, which has the lowest yield strength from common materials. + const double filament_density = 1.25e-3f; // g/mm^3 ; Common filaments are very lightweight, so precise number is not that important + const double material_yield_strength = 33.0f * 1e6f; // (g*mm/s^2)/mm^2; 33 MPa is yield strength of ABS, which has the lowest yield strength from common materials. const float standard_extruder_conflict_force = 20.0f * gravity_constant; // force that can occasionally push the model due to various factors (filament leaks, small curling, ... ); const float malformations_additive_conflict_extruder_force = 300.0f * gravity_constant; // for areas with possible high layered curled filaments // MPa * 1e^6 = (g*mm/s^2)/mm^2 = g/(mm*s^2); yield strength of the bed surface - float get_bed_adhesion_yield_strength() const { + double get_bed_adhesion_yield_strength() const { if (filament_type == "PLA") { - return 0.018f * 1e6f; + return 0.018 * 1e6; } else if (filament_type == "PET" || filament_type == "PETG") { - return 0.3f * 1e6f; + return 0.3 * 1e6; } else { //PLA default value - defensive approach, PLA has quite low adhesion - return 0.018f * 1e6f; + return 0.018 * 1e6; } } //just return PLA adhesion value as value for supports - float get_support_spots_adhesion_strength() const { - return 0.018f * 1e6f; + double get_support_spots_adhesion_strength() const { + return 0.018f * 1e6; } }; struct SupportPoint { - SupportPoint(const Vec3f &position, float force, const Vec3f &direction); + SupportPoint(const Vec3f &position, float force, float spot_radius, const Vec3f &direction); Vec3f position; float force; + float spot_radius; Vec3f direction; }; From 9e2a555f1bf6b2cac6e6388fe4d4b5e36f0340c9 Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Wed, 7 Sep 2022 18:00:04 +0200 Subject: [PATCH 076/100] fix supporting of start and end of extrusion line --- src/libslic3r/SupportSpotsGenerator.cpp | 4 ++++ src/slic3r/GUI/ConfigManipulation.cpp | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/libslic3r/SupportSpotsGenerator.cpp b/src/libslic3r/SupportSpotsGenerator.cpp index 0a6ab5e9da..697518bd08 100644 --- a/src/libslic3r/SupportSpotsGenerator.cpp +++ b/src/libslic3r/SupportSpotsGenerator.cpp @@ -365,6 +365,7 @@ std::vector to_short_lines(const ExtrusionEntity *e, float length Polyline pl = e->as_polyline(); std::vector lines; lines.reserve(pl.points.size() * 1.5f); + lines.emplace_back(unscaled(pl.points[0]).cast(), unscaled(pl.points[0]).cast(), e); for (int point_idx = 0; point_idx < int(pl.points.size() - 1); ++point_idx) { Vec2f start = unscaled(pl.points[point_idx]).cast(); Vec2f next = unscaled(pl.points[point_idx + 1]).cast(); @@ -415,6 +416,9 @@ void check_extrusion_entity_stability(const ExtrusionEntity *entity, for (size_t line_idx = 0; line_idx < lines.size(); ++line_idx) { ExtrusionLine ¤t_line = lines[line_idx]; + if (line_idx + 1 == lines.size() && current_line.b != lines.begin()->a) { + bridging_acc.add_distance(params.bridge_distance + 1.0f); + } float curr_angle = 0; if (line_idx + 1 < lines.size()) { const Vec2f v1 = current_line.b - current_line.a; diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp index 9df53caeba..c571d1d2af 100644 --- a/src/slic3r/GUI/ConfigManipulation.cpp +++ b/src/slic3r/GUI/ConfigManipulation.cpp @@ -282,7 +282,7 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig* config) "dont_support_bridges", "support_material_extrusion_width", "support_material_contact_distance", "support_material_xy_spacing" }) toggle_field(el, have_support_material); -// toggle_field("support_material_threshold", have_support_material_auto); + toggle_field("support_material_threshold", have_support_material_auto); toggle_field("support_material_bottom_contact_distance", have_support_material && ! have_support_soluble); toggle_field("support_material_closing_radius", have_support_material && support_material_style == smsSnug); From 997d0a48a8b24027b21c361e3e97e47f55ba5814 Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Thu, 8 Sep 2022 13:45:18 +0200 Subject: [PATCH 077/100] Improved time estimation for gcode files produced by KISSSlicer --- src/libslic3r/GCode/GCodeProcessor.cpp | 86 +++++++++++++++++++++++++- src/libslic3r/GCode/GCodeProcessor.hpp | 2 + 2 files changed, 86 insertions(+), 2 deletions(-) diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index 3868c87dfa..8de1815ae3 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -1289,6 +1289,7 @@ void GCodeProcessor::reset() m_options_z_corrector.reset(); m_spiral_vase_active = false; + m_kissslicer_toolchange_time_correction = 0.0f; #if ENABLE_GCODE_VIEWER_DATA_CHECKING m_mm3_per_mm_compare.reset(); @@ -1347,6 +1348,8 @@ void GCodeProcessor::process_file(const std::string& filename, std::function elements; + boost::split(elements, comment, boost::is_any_of("=")); + if (elements.size() == 2) { + try + { + switch (std::stoi(elements[1])) + { + default: { break; } + case 1: + case 2: + case 3: { m_flavor = gcfMarlinLegacy; break; } + } + return true; + } + catch (...) + { + // invalid data, do nothing + } + } + } + return false; + }; + + auto detect_printer = [this](const std::string_view comment) { + static const std::string search_str = "printer_name"; + const size_t pos = comment.find(search_str); + if (pos != comment.npos) { + std::vector elements; + boost::split(elements, comment, boost::is_any_of("=")); + if (elements.size() == 2) { + elements[1] = boost::to_upper_copy(elements[1]); + if (boost::contains(elements[1], "MK2.5") || boost::contains(elements[1], "MK3")) + m_kissslicer_toolchange_time_correction = 18.0f; // MMU2 + else if (boost::contains(elements[1], "MK2")) + m_kissslicer_toolchange_time_correction = 5.0f; // MMU + } + return true; + } + + return false; + }; + + begin = skip_whitespaces(begin, end); + if (begin != end) { + if (*begin == ';') { + // Comment. + begin = skip_whitespaces(++begin, end); + end = remove_eols(begin, end); + if (begin != end) { + const std::string_view comment(begin, end - begin); + if (detect_flavor(comment) || detect_printer(comment)) + ++found_counter; + } + + // we got the data, + // force early exit to avoid parsing the entire file + if (found_counter == 2) + m_parser.quit_parsing(); + } + else if (*begin == 'M' || *begin == 'G') + // the header has been fully parsed, quit search + m_parser.quit_parsing(); + } + } + ); + m_parser.reset(); +} + std::vector GCodeProcessor::get_layers_time(PrintEstimatedStatistics::ETimeMode mode) const { return (mode < PrintEstimatedStatistics::ETimeMode::Count) ? @@ -2559,7 +2638,7 @@ bool GCodeProcessor::process_bambustudio_tags(const std::string_view comment) bool GCodeProcessor::detect_producer(const std::string_view comment) { for (const auto& [id, search_string] : Producers) { - size_t pos = comment.find(search_string); + const size_t pos = comment.find(search_string); if (pos != comment.npos) { m_producer = id; BOOST_LOG_TRIVIAL(info) << "Detected gcode producer: " << search_string; @@ -3615,7 +3694,8 @@ void GCodeProcessor::process_T(const std::string_view command) // T-1 is a valid gcode line for RepRap Firmwares (used to deselects all tools) see https://github.com/prusa3d/PrusaSlicer/issues/5677 if ((m_flavor != gcfRepRapFirmware && m_flavor != gcfRepRapSprinter) || eid != -1) BOOST_LOG_TRIVIAL(error) << "GCodeProcessor encountered an invalid toolchange (" << command << ")."; - } else { + } + else { unsigned char id = static_cast(eid); if (m_extruder_id != id) { if (id >= m_result.extruder_colors.size()) @@ -3631,6 +3711,8 @@ void GCodeProcessor::process_T(const std::string_view command) float extra_time = get_filament_unload_time(static_cast(old_extruder_id)); m_time_processor.extruder_unloaded = false; extra_time += get_filament_load_time(static_cast(m_extruder_id)); + if (m_producer == EProducer::KissSlicer && m_flavor == gcfMarlinLegacy) + extra_time += m_kissslicer_toolchange_time_correction; simulate_st_synchronize(extra_time); m_result.extruders_count = std::max(m_result.extruders_count, m_extruder_id + 1); diff --git a/src/libslic3r/GCode/GCodeProcessor.hpp b/src/libslic3r/GCode/GCodeProcessor.hpp index 3edea61f42..484f416163 100644 --- a/src/libslic3r/GCode/GCodeProcessor.hpp +++ b/src/libslic3r/GCode/GCodeProcessor.hpp @@ -575,6 +575,7 @@ namespace Slic3r { OptionsZCorrector m_options_z_corrector; size_t m_last_default_color_id; bool m_spiral_vase_active; + float m_kissslicer_toolchange_time_correction; #if ENABLE_GCODE_VIEWER_STATISTICS std::chrono::time_point m_start_time; #endif // ENABLE_GCODE_VIEWER_STATISTICS @@ -648,6 +649,7 @@ namespace Slic3r { void apply_config(const DynamicPrintConfig& config); void apply_config_simplify3d(const std::string& filename); void apply_config_superslicer(const std::string& filename); + void apply_config_kissslicer(const std::string& filename); void process_gcode_line(const GCodeReader::GCodeLine& line, bool producers_enabled); // Process tags embedded into comments From 70d0e9eb448857c3f2bcf3e69b8b89ed594eb65b Mon Sep 17 00:00:00 2001 From: YuSanka Date: Thu, 8 Sep 2022 13:58:46 +0200 Subject: [PATCH 078/100] Fix for #8819 - Height Range modifier incorrectly displays height in Viewport --- src/slic3r/GUI/GLCanvas3D.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 18fd091d0d..66759f4c74 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -4173,7 +4173,7 @@ void GLCanvas3D::handle_sidebar_focus_event(const std::string& opt_key, bool foc void GLCanvas3D::handle_layers_data_focus_event(const t_layer_height_range range, const EditorType type) { - std::string field = "layer_" + std::to_string(type) + "_" + std::to_string(range.first) + "_" + std::to_string(range.second); + std::string field = "layer_" + std::to_string(type) + "_" + float_to_string_decimal_point(range.first) + "_" + float_to_string_decimal_point(range.second); handle_sidebar_focus_event(field, true); } From 380f8ea8d5f09032d89796a1c99c61f33db744bc Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Fri, 9 Sep 2022 09:31:34 +0200 Subject: [PATCH 079/100] Follow-up of db31995310c1743faa0c1ce9d3135af6cb7e2c71 - Fixed opening of gcode files when dragging and dropping them into GCodeViewer --- src/slic3r/GUI/Plater.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 7d939f1a1e..85978ed6b5 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -1609,7 +1609,8 @@ bool PlaterDropTarget::OnDropFiles(wxCoord x, wxCoord y, const wxArrayString &fi m_mainframe.Raise(); m_mainframe.select_tab(size_t(0)); - m_plater.select_view_3D("3D"); + if (wxGetApp().is_editor()) + m_plater.select_view_3D("3D"); bool res = m_plater.load_files(filenames); m_mainframe.update_title(); return res; From bee57e46d4f2b2330c6a159746b933a29515b69d Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Fri, 9 Sep 2022 11:18:14 +0200 Subject: [PATCH 080/100] remove old build fix for gcc --- src/slic3r/GUI/BonjourDialog.hpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/slic3r/GUI/BonjourDialog.hpp b/src/slic3r/GUI/BonjourDialog.hpp index 5bb61131d0..8bfc076c44 100644 --- a/src/slic3r/GUI/BonjourDialog.hpp +++ b/src/slic3r/GUI/BonjourDialog.hpp @@ -8,8 +8,6 @@ #include #include -#include -#include #include "libslic3r/PrintConfig.hpp" From 670629d883a8ee63894052ded7414055211244b1 Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Fri, 9 Sep 2022 15:34:48 +0200 Subject: [PATCH 081/100] Fix compilation - missing include for boost string conv, set supports flags for object AFTER the dialog window and snapshot --- src/libslic3r/GCode/GCodeProcessor.cpp | 1 + src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp | 5 ++--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index 8de1815ae3..ec57313f41 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -5,6 +5,7 @@ #include "libslic3r/format.hpp" #include "GCodeProcessor.hpp" +#include #include #include #include diff --git a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp index 513a655ec0..b30c29db1f 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp @@ -503,9 +503,6 @@ void GLGizmoFdmSupports::auto_generate() return vol->type() != ModelVolumeType::MODEL_PART || vol->supported_facets.empty(); }); - mo->config.set("support_material", true); - mo->config.set("support_material_auto", false); - MessageDialog dlg(GUI::wxGetApp().plater(), _L("Autogeneration will erase all currently painted areas.") + "\n\n" + _L("Are you sure you want to do it?") + "\n", @@ -523,6 +520,8 @@ void GLGizmoFdmSupports::auto_generate() } } + mo->config.set("support_material", true); + mo->config.set("support_material_auto", false); this->waiting_for_autogenerated_supports = true; wxGetApp().CallAfter([this]() { reslice_FDM_supports(); }); } From 819c42e4adc42e5fc806bc0c05fd4bfd1e512c18 Mon Sep 17 00:00:00 2001 From: David Kocik Date: Fri, 9 Sep 2022 17:01:15 +0200 Subject: [PATCH 082/100] Fix of bug when uploading SLA with folder on target printer. Until now, the exported file contained the same folder inside archive. --- src/slic3r/GUI/BackgroundSlicingProcess.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/slic3r/GUI/BackgroundSlicingProcess.cpp b/src/slic3r/GUI/BackgroundSlicingProcess.cpp index 53790e076b..173d345704 100644 --- a/src/slic3r/GUI/BackgroundSlicingProcess.cpp +++ b/src/slic3r/GUI/BackgroundSlicingProcess.cpp @@ -738,7 +738,7 @@ void BackgroundSlicingProcess::prepare_upload() ThumbnailsList thumbnails = this->render_thumbnails( ThumbnailsParams{current_print()->full_print_config().option("thumbnails")->values, true, true, true, true}); - m_sla_print->export_print(source_path.string(),thumbnails, m_upload_job.upload_data.upload_path.string()); + m_sla_print->export_print(source_path.string(),thumbnails, m_upload_job.upload_data.upload_path.filename().string()); } m_print->set_status(100, (boost::format(_utf8(L("Scheduling upload to `%1%`. See Window -> Print Host Upload Queue"))) % m_upload_job.printhost->get_host()).str()); From c440e5f80b5ff814cf15515a2301c7086ef7dbfa Mon Sep 17 00:00:00 2001 From: rtyr <36745189+rtyr@users.noreply.github.com> Date: Tue, 13 Sep 2022 11:42:08 +0200 Subject: [PATCH 083/100] Initial Rigid3D bundle https://github.com/prusa3d/PrusaSlicer/pull/8832 --- resources/profiles/Rigid3D.idx | 3 + resources/profiles/Rigid3D.ini | 469 ++++++++++++++++++ .../profiles/Rigid3D/Mucit2_thumbnail.png | Bin 0 -> 54448 bytes .../profiles/Rigid3D/Mucit_thumbnail.png | Bin 0 -> 55165 bytes .../profiles/Rigid3D/Zero2_thumbnail.png | Bin 0 -> 60344 bytes .../profiles/Rigid3D/Zero3_thumbnail.png | Bin 0 -> 47050 bytes resources/profiles/Rigid3D/mucit2_bed.png | Bin 0 -> 10363 bytes resources/profiles/Rigid3D/mucit2_bed.stl | Bin 0 -> 763384 bytes resources/profiles/Rigid3D/mucit_bed.png | Bin 0 -> 9332 bytes resources/profiles/Rigid3D/mucit_bed.stl | Bin 0 -> 220684 bytes resources/profiles/Rigid3D/zero2_bed.png | Bin 0 -> 9707 bytes resources/profiles/Rigid3D/zero3_bed.png | Bin 0 -> 9708 bytes resources/profiles/Rigid3D/zero_bed.stl | Bin 0 -> 874084 bytes 13 files changed, 472 insertions(+) create mode 100644 resources/profiles/Rigid3D.idx create mode 100644 resources/profiles/Rigid3D.ini create mode 100644 resources/profiles/Rigid3D/Mucit2_thumbnail.png create mode 100644 resources/profiles/Rigid3D/Mucit_thumbnail.png create mode 100644 resources/profiles/Rigid3D/Zero2_thumbnail.png create mode 100644 resources/profiles/Rigid3D/Zero3_thumbnail.png create mode 100644 resources/profiles/Rigid3D/mucit2_bed.png create mode 100644 resources/profiles/Rigid3D/mucit2_bed.stl create mode 100644 resources/profiles/Rigid3D/mucit_bed.png create mode 100644 resources/profiles/Rigid3D/mucit_bed.stl create mode 100644 resources/profiles/Rigid3D/zero2_bed.png create mode 100644 resources/profiles/Rigid3D/zero3_bed.png create mode 100644 resources/profiles/Rigid3D/zero_bed.stl diff --git a/resources/profiles/Rigid3D.idx b/resources/profiles/Rigid3D.idx new file mode 100644 index 0000000000..21c2f4b0c2 --- /dev/null +++ b/resources/profiles/Rigid3D.idx @@ -0,0 +1,3 @@ +min_slic3r_version = 2.6.0-alpha0 +1.0.0 Initial Rigid3D bundle + diff --git a/resources/profiles/Rigid3D.ini b/resources/profiles/Rigid3D.ini new file mode 100644 index 0000000000..d6cbd372a5 --- /dev/null +++ b/resources/profiles/Rigid3D.ini @@ -0,0 +1,469 @@ +# Print profiles for the Creality printers. + +[vendor] +# Vendor name will be shown by the Config Wizard. +name = Rigid3D +# Configuration version of this file. Config file will only be installed, if the config_version differs. +# This means, the server may force the PrusaSlicer configuration to be downgraded. +config_version = 1.0.0 +# Where to get the updates from? +config_update_url = https://files.prusa3d.com/wp-content/uploads/repository/PrusaSlicer-settings-master/live/Rigid3D/ +# changelog_url = https://files.prusa3d.com/?latest=slicer-profiles&lng=%1% + +# The printer models will be shown by the Configuration Wizard in this order, +# also the first model installed & the first nozzle installed will be activated after install. +# Printer model name will be shown by the installation wizard. + +[printer_model:Zero2] +name = Rigid3D Zero2 +variants = 0.4 +technology = FFF +family = Zero +bed_model = zero_bed.stl +bed_texture = zero2_bed.png +default_materials = Generic PLA @Rigid3D; Generic PETG @Rigid3D; Generic ABS @Rigid3D; Rigid3D PLA @Rigid3D; Generic Nylon @Rigid3D; Generic FLEX @Rigid3D + +[printer_model:Zero3] +name = Rigid3D Zero3 +variants = 0.4 +technology = FFF +family = Zero +bed_model = zero_bed.stl +bed_texture = zero3_bed.png +default_materials =Generic PLA @Rigid3D; Generic PETG @Rigid3D; Generic ABS @Rigid3D; Rigid3D PLA @Rigid3D; Generic Nylon @Rigid3D; Generic FLEX @Rigid3D + +[printer_model:Mucit] +name = Rigid3D Mucit +variants = 0.4 +technology = FFF +family = Mucit +bed_model = mucit_bed.stl +bed_texture = mucit_bed.png +default_materials = Generic PLA @Rigid3D; Rigid3D PLA @Rigid3D + +[printer_model:Mucit2] +name = Rigid3D Mucit2 +variants = 0.4 +technology = FFF +family = Mucit +bed_model = mucit2_bed.stl +bed_texture = mucit2_bed.png +default_materials = Generic PLA @Rigid3D; Generic PETG @Rigid3D; Generic ABS @Rigid3D; Rigid3D PLA @Rigid3D; Generic Nylon @Rigid3D; Generic FLEX @Rigid3D + +# All presets starting with asterisk, for example *common*, are intermediate and they will +# not make it into the user interface. + +# Common print preset +[print:*common*] +avoid_crossing_perimeters = 0 +avoid_crossing_perimeters_max_detour = 0 +bottom_fill_pattern = monotonic +bottom_solid_min_thickness = 0.8 +bridge_acceleration = 0 +bridge_angle = 0 +bridge_flow_ratio = 1 +bridge_speed = 60 +brim_separation = 0 +brim_type = no_brim +brim_width = 0 +clip_multipart_objects = 1 +compatible_printers = +compatible_printers_condition = printer_notes=~/.*PRINTER_VENDOR_RIGID3D.*/ +complete_objects = 0 +default_acceleration = 0 +dont_support_bridges = 1 +draft_shield = disabled +elefant_foot_compensation = 0 +ensure_vertical_shell_thickness = 0 +external_perimeter_extrusion_width = 0.45 +external_perimeter_speed = 50% +external_perimeters_first = 1 +extra_perimeters = 1 +extruder_clearance_height = 20 +extruder_clearance_radius = 20 +extrusion_width = 0.45 +fill_angle = 45 +fill_density = 10% +fill_pattern = line +first_layer_acceleration = 0 +first_layer_acceleration_over_raft = 0 +first_layer_extrusion_width = 0.42 +first_layer_height = 0.2 +first_layer_speed = 50% +first_layer_speed_over_raft = 30 +fuzzy_skin = none +fuzzy_skin_point_dist = 0.8 +fuzzy_skin_thickness = 0.3 +gap_fill_enabled = 1 +gap_fill_speed = 20 +gcode_comments = 0 +gcode_label_objects = 0 +gcode_resolution = 0.0125 +gcode_substitutions = +infill_acceleration = 0 +infill_anchor = 2.5 +infill_anchor_max = 12 +infill_every_layers = 1 +infill_extruder = 1 +infill_extrusion_width = 0.45 +infill_first = 1 +infill_only_where_needed = 0 +infill_overlap = 25% +infill_speed = 60 +inherits = +interface_shells = 0 +ironing = 0 +ironing_flowrate = 15% +ironing_spacing = 0.1 +ironing_speed = 15 +ironing_type = top +max_print_speed = 80 +max_volumetric_speed = 0 +min_skirt_length = 0 +mmu_segmented_region_max_width = 0 +notes = +only_retract_when_crossing_perimeters = 0 +ooze_prevention = 0 +output_filename_format = {input_filename_base}_{layer_height}mm_{initial_filament_type}_{printer_model}_{print_time}.gcode +overhangs = 1 +perimeter_acceleration = 0 +perimeter_extruder = 1 +perimeter_extrusion_width = 0.45 +perimeter_speed = 60 +perimeters = 2 +post_process = +print_settings_id = +raft_contact_distance = 0.1 +raft_expansion = 1.5 +raft_first_layer_density = 90% +raft_first_layer_expansion = 3 +raft_layers = 0 +resolution = 0 +seam_position = rear +single_extruder_multi_material_priming = 1 +skirt_distance = 6 +skirt_height = 1 +skirts = 0 +slice_closing_radius = 0.049 +slicing_mode = regular +small_perimeter_speed = 15 +solid_infill_below_area = 70 +solid_infill_every_layers = 0 +solid_infill_extruder = 1 +solid_infill_extrusion_width = 0.45 +solid_infill_speed = 100% +spiral_vase = 0 +standby_temperature_delta = -5 +support_material = 0 +support_material_angle = 0 +support_material_auto = 1 +support_material_bottom_contact_distance = 0 +support_material_bottom_interface_layers = -1 +support_material_buildplate_only = 0 +support_material_closing_radius = 2 +support_material_contact_distance = 0.2 +support_material_enforce_layers = 0 +support_material_extruder = 1 +support_material_extrusion_width = 0.35 +support_material_interface_contact_loops = 0 +support_material_interface_extruder = 1 +support_material_interface_layers = 3 +support_material_interface_pattern = rectilinear +support_material_interface_spacing = 0 +support_material_interface_speed = 100% +support_material_pattern = rectilinear +support_material_spacing = 2.5 +support_material_speed = 60 +support_material_style = grid +support_material_synchronize_layers = 0 +support_material_threshold = 0 +support_material_with_sheath = 1 +support_material_xy_spacing = 50% +thick_bridges = 0 +thin_walls = 1 +top_fill_pattern = monotonic +top_infill_extrusion_width = 0.4 +top_solid_infill_speed = 100% +top_solid_min_thickness = 0.8 +travel_speed = 80 +travel_speed_z = 0 +wipe_tower = 0 +wipe_tower_bridging = 10 +wipe_tower_brim_width = 2 +wipe_tower_no_sparse_layers = 0 +wipe_tower_rotation_angle = 0 +wipe_tower_width = 60 +wipe_tower_x = 180 +wipe_tower_y = 140 +xy_size_compensation = 0 + +[print:0.06mm - Ultra @Rigid3D] +inherits = *common* +layer_height = 0.06 +bottom_solid_layers = 10 +top_solid_layers = 14 + +[print:0.12mm - Super @Rigid3D] +inherits = *common* +layer_height = 0.12 +bottom_solid_layers = 7 +top_solid_layers = 7 + +[print:0.16mm - Good @Rigid3D] +inherits = *common* +layer_height = 0.16 +bottom_solid_layers = 5 +top_solid_layers = 5 + +[print:0.20mm - Standard @Rigid3D] +inherits = *common* +layer_height = 0.20 +bottom_solid_layers = 4 +top_solid_layers = 4 + +[print:0.24mm - Draft @Rigid3D] +inherits = *common* +layer_height = 0.24 +bottom_solid_layers = 3 +top_solid_layers = 4 + +[print:0.28mm - Low @Rigid3D] +inherits = *common* +layer_height = 0.28 +bottom_solid_layers = 3 +top_solid_layers = 4 + +[filament:*common*] +disable_fan_first_layers = 3 +end_filament_gcode = "; Filament-specific end gcode \n;END gcode for filament\n" +extrusion_multiplier = 1 +fan_below_layer_time = 60 +filament_cooling_final_speed = 3.4 +filament_cooling_initial_speed = 2.2 +filament_cooling_moves = 4 +filament_cost = 0 +filament_deretract_speed = nil +filament_diameter = 1.75 +filament_minimal_purge_on_wipe_tower = 15 +filament_notes = "" +filament_retract_before_travel = nil +filament_retract_before_wipe = nil +filament_retract_layer_change = nil +filament_retract_length = nil +filament_retract_lift = nil +filament_retract_lift_above = nil +filament_retract_lift_below = nil +filament_retract_restart_extra = nil +filament_retract_speed = nil +filament_settings_id = "" +filament_soluble = 0 +filament_spool_weight = 0 +filament_wipe = nil +min_print_speed = 10 +slowdown_below_layer_time = 10 +start_filament_gcode = "; Filament gcode\n" + +[filament:*PLA*] +inherits = *common* +compatible_printers_condition = printer_notes=~/.*PRINTER_VENDOR_RIGID3D.*/ +bridge_fan_speed = 100 +cooling = 1 +fan_always_on = 1 +filament_type = PLA +full_fan_speed_layer = 4 +max_fan_speed = 100 +min_fan_speed = 100 + +[filament:*ABS*] +inherits = *common* +compatible_printers_condition = printer_notes=~/.*PRINTER_VENDOR_RIGID3D.*/ and printer_notes=~/.*PRINTER_HAS_HEATEDBED.*/ +cooling = 1 +fan_always_on = 0 +filament_type = ABS +max_fan_speed = 0 +min_fan_speed = 0 + +[filament:*PETG*] +inherits = *common* +compatible_printers_condition = printer_notes=~/.*PRINTER_VENDOR_RIGID3D.*/ and printer_notes=~/.*PRINTER_HAS_HEATEDBED.*/ +cooling = 1 +fan_always_on = 0 +filament_type = PETG +max_fan_speed = 50 +min_fan_speed = 30 + +[filament:*FLEX*] +inherits = *common* +compatible_printers_condition = printer_notes=~/.*PRINTER_VENDOR_RIGID3D.*/ and printer_notes=~/.*PRINTER_HAS_HEATEDBED.*/ +bridge_fan_speed = 100 +cooling = 1 +fan_always_on = 1 +filament_type = FLEX +full_fan_speed_layer = 4 +max_fan_speed = 100 +min_fan_speed = 100 + +[filament:*NYLON*] +inherits = *common* +compatible_printers_condition = printer_notes=~/.*PRINTER_VENDOR_RIGID3D.*/ and printer_notes=~/.*PRINTER_HAS_HEATEDBED.*/ +cooling = 1 +fan_always_on = 0 +filament_type = NYLON +max_fan_speed = 0 +min_fan_speed = 0 + +[filament:Generic PLA @Rigid3D] +inherits = *PLA* +bed_temperature = 55 +filament_colour = #FFFF00 +filament_density = 1.24 +filament_max_volumetric_speed = 12 +first_layer_temperature = 215 +temperature = 215 +first_layer_bed_temperature = 60 + +[filament:Rigid3D PLA @Rigid3D] +inherits = *PLA* +filament_colour = #FFFF00 +filament_density = 1.24 +first_layer_temperature = 210 +temperature = 205 +first_layer_bed_temperature = 60 +bed_temperature = 55 +filament_max_volumetric_speed = 12 + +[filament:Generic ABS @Rigid3D] +inherits = *ABS* +filament_colour = #49B928 +filament_density = 1.04 +first_layer_temperature = 235 +temperature = 235 +first_layer_bed_temperature = 100 +bed_temperature = 100 +filament_max_volumetric_speed = 8 + +[filament:Generic PETG @Rigid3D] +inherits = *PETG* +filament_colour = #FF8000 +filament_density = 1.27 +first_layer_temperature = 240 +temperature = 240 +first_layer_bed_temperature = 70 +bed_temperature = 70 +filament_max_volumetric_speed = 8 + +[filament:Generic FLEX @Rigid3D] +inherits = *FLEX* +filament_colour = #C80000 +filament_density = 1.21 +first_layer_temperature = 225 +temperature = 225 +first_layer_bed_temperature = 70 +bed_temperature = 70 +filament_max_volumetric_speed = 2.5 + +[filament:Generic Nylon @Rigid3D] +inherits = *NYLON* +filament_colour = #0AF1CE +filament_density = 1.12 +first_layer_temperature = 250 +temperature = 250 +first_layer_bed_temperature = 100 +bed_temperature = 100 +filament_max_volumetric_speed = 8 + +# Common printer preset +[printer:*common*] +before_layer_gcode = ;BEFORE_LAYER_CHANGE\n;[layer_z]\n\n +between_objects_gcode = +color_change_gcode = M600 +default_filament_profile = "Rigid3D PLA @Rigid3D" +default_print_profile = "0.20mm - Standard @Rigid3D" +deretract_speed = 0 +extruder_offset = 0x0 +gcode_flavor = marlin2 +layer_gcode = ;AFTER_LAYER_CHANGE\n;[layer_z]\n\n +machine_limits_usage = time_estimate_only +machine_max_acceleration_e = 1000 +machine_max_acceleration_extruding = 500 +machine_max_acceleration_retracting = 1000 +machine_max_acceleration_travel = 1000 +machine_max_acceleration_x = 500 +machine_max_acceleration_y = 500 +machine_max_acceleration_z = 100 +machine_max_feedrate_e = 25 +machine_max_feedrate_x = 120 +machine_max_feedrate_y = 120 +machine_max_feedrate_z = 20 +machine_max_jerk_e = 5 +machine_max_jerk_x = 10 +machine_max_jerk_y = 10 +machine_max_jerk_z = 0.3 +machine_min_extruding_rate = 0 +machine_min_travel_rate = 0 +max_layer_height = 0.32 +min_layer_height = 0.05 +nozzle_diameter = 0.4 +pause_print_gcode = M0 +printer_settings_id = +printer_technology = FFF +printer_variant = 0.4 +remaining_times = 1 +retract_before_travel = 2 +retract_before_wipe = 0% +retract_layer_change = 0 +retract_length = 1 +retract_length_toolchange = 10 +retract_lift = 0 +retract_lift_above = 0 +retract_lift_below = 0 +retract_restart_extra = 0 +retract_restart_extra_toolchange = 0 +retract_speed = 25 +silent_mode = 0 +single_extruder_multi_material = 0 +template_custom_gcode = +thumbnails = +toolchange_gcode = +use_firmware_retraction = 0 +use_relative_e_distances = 0 +use_volumetric_e = 0 +variable_layer_height = 1 +wipe = 0 +z_offset = 0 + +[printer:Rigid3D Zero2] +inherits = *common* +bed_shape = 0x0,200x0,200x200,0x200 +max_print_height = 190 +printer_model = Zero2 +printer_notes = Don't remove the following keywords! These keywords are used in the "compatible printer" condition of the print and filament profiles to link the particular print and filament profiles to this printer profile.\nPRINTER_VENDOR_RIGID3D\nPRINTER_MODEL_ZERO2\nPRINTER_HAS_HEATEDBED\n +start_gcode = G21\nG92 E0\nG28\nM420 S1\nM107\nG90\nG1 X10.0 Y0.1 Z0.3 F3000.0\nG1 X190.0 Y0.1 Z0.3 F1500.0 E15\nG1 X190 Y0.4 Z0.3 F3000.0\nG1 X10.0 Y0.4 Z0.3 F1500.0 E30\nG1 Z2.0 F1500.0\nG92 E0\n +end_gcode = G1 X0 Y180\nM107\nG91\nG0 Z20\nT0\nG1 E-1\nM104 T0 S0\nG90\nG92 E0\nM140 S0\nM84\nM300 S2093 P150\nM300 S2637 P150\nM300 S3135 P150\nM300 S4186 P150\nM300 S3135 P150\nM300 S2637 P150\nM300 S2793 P150\nM300 S2349 P150\nM300 S1975 P150\nM300 S2093 P450\n + +[printer:Rigid3D Zero3] +inherits = *common* +bed_shape = 0x0,200x0,200x200,0x200 +max_print_height = 200 +printer_model = Zero3 +printer_notes = Don't remove the following keywords! These keywords are used in the "compatible printer" condition of the print and filament profiles to link the particular print and filament profiles to this printer profile.\nPRINTER_VENDOR_RIGID3D\nPRINTER_MODEL_ZERO3\nPRINTER_HAS_HEATEDBED\n +start_gcode = G21\nG92 E0\nG28\nM420 S1\nM107\nG90\nG1 X10.0 Y0.1 Z0.3 F3000.0\nG1 X190.0 Y0.1 Z0.3 F1500.0 E15\nG1 X190 Y0.4 Z0.3 F3000.0\nG1 X10.0 Y0.4 Z0.3 F1500.0 E30\nG1 Z2.0 F1500.0\nG92 E0\n +end_gcode = G92 E0\nT0\nG1 F1800 E-2\nG27 P2\nM107\nM104 T0 S0\nM140 S0\nG90\nG92 E0\nM18\n + +[printer:Rigid3D Mucit] +inherits = *common* +bed_shape = 0x0,150x0,150x150,0x150 +max_print_height = 150 +printer_model = Mucit +printer_notes = Don't remove the following keywords! These keywords are used in the "compatible printer" condition of the print and filament profiles to link the particular print and filament profiles to this printer profile.\nPRINTER_VENDOR_RIGID3D\nPRINTER_MODEL_MUCIT\n +start_gcode = G21\nG92 E0\nG28\nM420 S1\nM107\nG90\nG1 X10.0 Y0.1 Z0.3 F3000.0\nG1 X140.0 Y0.1 Z0.3 F1500.0 E10\nG1 X140 Y0.4 Z0.3 F3000.0\nG1 X10.0 Y0.4 Z0.3 F1500.0 E20\nG1 Z2.0 F1500.0\nG92 E0\n +end_gcode = G1 X0 Y140\nM107\nG91\nG0 Z20\nT0\nG1 E-2\nM104 T0 S0\nG90\nG92 E0\nM140 S0\nM84\nM300 S2093 P150\nM300 S2637 P150\nM300 S3135 P150\nM300 S4186 P150\nM300 S3135 P150\nM300 S2637 P150\nM300 S2793 P150\nM300 S2349 P150\nM300 S1975 P150\nM300 S2093 P450\n + +[printer:Rigid3D Mucit2] +inherits = *common* +bed_shape = 0x0,150x0,150x150,0x150 +max_print_height = 150 +printer_model = Mucit2 +printer_notes = Don't remove the following keywords! These keywords are used in the "compatible printer" condition of the print and filament profiles to link the particular print and filament profiles to this printer profile.\nPRINTER_VENDOR_RIGID3D\nPRINTER_MODEL_MUCIT2\nPRINTER_HAS_HEATEDBED\n +start_gcode = G21\nG92 E0\nG28\nM420 S1\nM107\nG90\nG1 X10.0 Y0.1 Z0.3 F3000.0\nG1 X140.0 Y0.1 Z0.3 F1500.0 E10\nG1 X140 Y0.4 Z0.3 F3000.0\nG1 X10.0 Y0.4 Z0.3 F1500.0 E20\nG1 Z2.0 F1500.0\nG92 E0\n +end_gcode = G92 E0\nT0\nG1 F1800 E-2\nG27 P2\nM107\nM104 T0 S0\nM140 S0\nG90\nG92 E0\nM18\n diff --git a/resources/profiles/Rigid3D/Mucit2_thumbnail.png b/resources/profiles/Rigid3D/Mucit2_thumbnail.png new file mode 100644 index 0000000000000000000000000000000000000000..c4eb8d5260ecbdf543602cd2f02a79a2f07faace GIT binary patch literal 54448 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�Zxni!n~_vO+C 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 Bvn2ok literal 0 HcmV?d00001 diff --git a/resources/profiles/Rigid3D/Mucit_thumbnail.png b/resources/profiles/Rigid3D/Mucit_thumbnail.png new file mode 100644 index 0000000000000000000000000000000000000000..0796709d11a28f08a913d35999fd618ebf62b69f GIT binary patch literal 55165 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

!Zz{8I{w)l!mF0rbFK4CCO!ayJ)I7e-58b_Ym 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

l2@@5StDO5YI0GOoonU-)gxq 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 uQ;g) zuQ(Y1x-+k@NQv*E4|EwktfBvA@k3o~BA?4v4n@)Ao@d z{4mFg5Yoy2$hkAtF>$_o&fsD;PsL|@^~TIUZPF!7V0=rZs8y2G7HiJ)ZN(5cn(v5k6$N3F2?eZ}?g9?(=Ei}&&^MMmT2#mN<1kqRx`15d9y}<$KK?wf;po^CqmVCNn_R_iQ zMYF>ub?)BYPi{rkA}@cgjPE#??fQLP+oWn4q3Tbg^tUz-1O^IolhG#ANZ4Nc%v#{J z68X$g9i2eQAu;Vq!)$${>4n7Q<2VbrFNN|z+|9~B2Z-q9_cm7p-KHFd>}dgdKTHPa zH>)Hue~P5M!}$WETWEStVKFuB_^oo~F&l?n2DiW_w5BUg#%n^gh=|`H>Fb_r>RLar zD2wPC5!4bahB7=cH{Wf&ZI0FSAuEvzj!L2q*{4gnewfP3rEfd@7tCeIg@gW05v%)P zAP$2v>-@T%xx%c)clAX#jEh4eMylQVaJM^xrV-|R)B}Ht{kqkGxLKeh0oASe#CWpL z$~vk+vmvgIOpJKHURk+C_EK$}?CXh}Y^v%*`o`s^h<#i|xVaq+-^C{+=xk;RE%~ z3G=H#C$Z#ghkRoQe6YqwmvT@0q+Cxv73~<{X_CTH+ipzyeb z=UFwtf%%Is)=*oMCM@jxwt+PQIV1uJ_-8}Sie8M(HZc8?#5%T|T8>cS{bu?nkNR=7 zJte#iH7lgaVR@c#6zmxXXWP^2a)+HYj`QF8XLc1)`F|l7`1D??=)BIe=RdGoakH8t zZh}@=1{0PR^ukOK@caJ1ZJ#yYD9@hQ0DFChezi?P zcojnm?YeEPXnad5&K?N6Z|@neRfuYqCYpz@aI6hfYvlg{eo~8U zd5`s(zj&#_(GV(8CcSQ?lN8ttZA0xp5c&fQ?<GpQI%;zpokt!x14v%Xi8nZMRAQt&d>d^F_g%u2Y zq7kB$YC$4x&c|xeQ_5KhXz5M8m-)OKK)zaGU zYjDXF`;4*@)mk#!2rz&J+QQQK92th39D72IqP`cpFV&jm8TG;1e;r0AC(%{N;l!C2 znxME1VOk>`(!|`u67I9Hw%^1wULQV^Rdh9YaHGQJ7zL|&^Vw~zVw7DV+ot25P~J=u z%|W2loV}fsyF27w(Q>WXSst(2hhs9qNvWS<#SWI1j?1)q%t`n96iZvXBmMNXSZv(Z z;sdSdlm6`-ag11t6~}upG;6_PdFCC({|XN)gZF`Ec0!k%PSv!X@aSCaVTUJ(`fcNU zCXq+^KPHl}FtFHw2U3;@GSW%Q7UiY#;Pn>ARh{3Y>|deyrs@8M>La-+0po@xfy`Hd zg~>IQ#Exl}UJUTRDNP6{xsh7e2dQYPHx_7d!^mdKJ&zpY*&^Xc55L(f!~bLTenbxgF; In$Ge61H^3Ur~m)} literal 0 HcmV?d00001 diff --git a/resources/profiles/Rigid3D/Zero2_thumbnail.png b/resources/profiles/Rigid3D/Zero2_thumbnail.png new file mode 100644 index 0000000000000000000000000000000000000000..af2606198de9e5798f8f943cefef6047942f843a GIT binary patch literal 60344 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 Y9o}dYx04jJ2yde(tNNu*+U)250C|~;c>n+a literal 0 HcmV?d00001 diff --git a/resources/profiles/Rigid3D/Zero3_thumbnail.png b/resources/profiles/Rigid3D/Zero3_thumbnail.png new file mode 100644 index 0000000000000000000000000000000000000000..7f65192f2ca503879267e22944ca95f208190372 GIT binary patch literal 47050 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 z zIEt~>P&viUgE61}iD!BLzx^H<*IcVKe@2hso`d}Yk{sq!Fp|}0SK-l|AAP0IkABF& zAVPo3@IU|IBzRg!#7g=A%%VVtd~pKlQh)B07*naRPIg#4lO!) z)Z);=uEinqmd-+`>+a7XFtzqmv=YoKLmDgQ^8%?9bew&ftm~%7sjR@5njRUolIYq{ zo0>>SM$@TZChdDXTRY2aGNYQ$Fs-+Ajj3yj`Mia#=4C+~astylAy$&>k80lk%qs8y z&=x#6fbG)1gY%tmMaGc!VgCW#--VSm*el@y7%fmM6+2VNSNlv3H2n-#hCR|GCW%wx zC?-)Vz^#9CJAzvmps-{R5lQd0 zL15*i^O_my7UqElhe0x(PKlC~gSlot+4pRM)jV!@p0;?Nas4QoFu*$V(HRms$XI@zb!#)S&l4@M?6F>eEJiP^PJcKv} zslW|kvI{B&N{Ad(G0aN1kV2|Z+m?EN&bhOWFa79~T>IblIT&s8hp&E-D3ZMA#ix1r zXpgLyk@YgtB<1Fv`#9r>!Vs7x3EF6sl!!!eu)W7CA9_D=B>COn{Ug%EZ@HvtgtLNL z&q?#-m?O5ZXo#9=xxte=>c1oHMW+HU|C?<+vzvt4Q$eh_Wr0>Z>`BoS)kT$V5}_I| z{Z@})4W_Q?B{7CJI?8XSgz2OmUm8B1S-hJE&XDW=wZ zyvT=ta0On!4=VN->A?ZaM$nI8R>AB(h#KMqW*UY9RvajZ;~|V{u3p^d=YP23FaM0< zw}0FUV|BO^Xz-dSNhs!Xt({|goNe2M@20VB zn~lxJPGdDzV_S_e;bdalwj0}N*ccPreCPQO@AcvO1?P5Rt#$0HWDfy6UE_RsM!V&8 zYajkj+RbGd=ov4k+R(8!LLgy(hOwd{%Nk!jP@-a{{h(tm!Tk0SS7|LWBbyzZvIniH zuiv>5Oxdc`w8oy7>wy{?G2-?o@%<6{S^G(3z2D>GWKJs9uVbdU(aQT^945!l6ib`H z(CeE2!an|q>|-Rie{uM7_ke-7c5A!2wr9AjjGjp;LmFaKscIm@3A27?P0#8TI{ou1 zdS)UsJfQnnNX`4^r+|TGY>QMPcvbxBbWVJy@HCE{v-SI5--;MzaG)&ADJlny*_6?X z&Lmm(=`A>b_TZr~c;kJ&%CU-bEtfMRCT&ImGV`liFuDMuf6!>G+ccXNm>8FjH+sJl zJ$My);Rt*c{UnN!!P@M3#x?S~=gtY(F%bIf*9^G1d}l6{f$28%DL9U^Rh(Ma_y=?9 z1-%O2D($Nn&ywXQV|#Fk;tV_segZVVIWSQYSze(pF%aFS@s|8Lw#NAOUvXFD@S`0i zJq9%9-;6f%cT%JG7b0xOxaaX&|C|V4Uhl_7ZSQ4Hm8<^Ft*NyWM6?SLu_!*dg4WLo4s_NK8|PbUc`){%Vqmky?g;hrkyRo#TOJ$7k(k!l0iK$)xxZuOTFZ zyw%g#(g37pL9dM>hBU*^0`dm{5cM zu^ir?e0Fipa>pLIz2eC4X$d%odwu(-@<}PBdVNsa9R!LFu^zHDddA(emIE>aQmdMa z

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 literal 0 HcmV?d00001 diff --git a/resources/profiles/Rigid3D/mucit2_bed.png b/resources/profiles/Rigid3D/mucit2_bed.png new file mode 100644 index 0000000000000000000000000000000000000000..d01c0b82d7b390de22de64eacb85cb4bf0f8cbe2 GIT binary patch literal 10363 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

Q5jfvU0wl^i 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⪚iw_wQG*iFO8AmtVI>LF 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 literal 0 HcmV?d00001 diff --git a/resources/profiles/Rigid3D/mucit_bed.png b/resources/profiles/Rigid3D/mucit_bed.png new file mode 100644 index 0000000000000000000000000000000000000000..2f94cb8f265d80b5382d353bc3c96b2f2474f578 GIT binary patch literal 9332 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 gpFuudJB(D;;hA4-WBYXATM literal 0 HcmV?d00001 diff --git a/resources/profiles/Rigid3D/zero2_bed.png b/resources/profiles/Rigid3D/zero2_bed.png new file mode 100644 index 0000000000000000000000000000000000000000..4e6e04e652513cb20401c701229b44dd096234b8 GIT binary patch literal 9707 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 UqdJLc5e8taZ*~QD>Gp&F06;YmUjP6A literal 0 HcmV?d00001 diff --git a/resources/profiles/Rigid3D/zero3_bed.png b/resources/profiles/Rigid3D/zero3_bed.png new file mode 100644 index 0000000000000000000000000000000000000000..3c5dd5f1933029f5ec1fc1e253250046b0b2bc81 GIT binary patch literal 9708 zcmeHtcT`hZzjns@I*vFq8W})fW-O?PGy~GHAR--sP=r8$0EP~sCs7$hBSk@^6A|eN z7)n40K?OoD8c0G(5JC|GVJIOG2;AuV&b#it>%0HWtoyCI=8v;>_Sxsz&$EB~*=Mi( z_IYGsW_ajZ$!`Gwz#*d>*Q@}5-B8ix`j6eBJC#%GVxnS?=VjB&001HB;FjxNQT=DP z8&;+OK;$U^;BE{6u(2a5X8{0&5&$sg0sv^H0|4Uw$&l9T0KhJN3lnRDot>Riza9sH zKtn@ATU%T6^Yc_H6^Fz5`}-#*ChF>Kp+eZ3~)Fcfk4pP+soteW@cv6($ciF zv^+dK;Ba_zb2FdMuc)Z7v9Xz)ob>he-Q3)~bLY;-j~^dDe*FCT^NERxD_5@6)zxip zZ$qKbf`WnE#si|{wb0sAu0RaJAE|*9ozI*pBGcz+KB_${*sIRYYc6OG{X8ZW~2!%o&9UVL# zUszZ;Jv~h#kv@F*(B9sjpP#?HyliDOdpP%2-(o%JGb!cel;NW0?e}7L;kENxhwzhU{ZSBIs z!rQlR%gf6zU%t#_GDk;8J3BiK3=AS8BLf2i-Q3(tOG}N7jf;wkZr{FbWMsr*u^v8r z7!eWS?ChMIn_FC5+}hge)92S9u;;Ww{3 z0N_^_qidI~!-p4`n=vPCA}C8RI~V=q$MwJYA*2=b{l)X)zhGXMT`u2s|L((Ye{FyH z@E!2L#NLORZYFA&hcA>nTt>s~6YP)BjZL=bl~qnUs!XbX&MhWybp*j$oT1NY&U90S zCSjtG@FM}Q61Dukkw|R*esMEjDp4yDal61M`Lr=~FSFTll zlUxAJy`iRGY2AIKW8aPEn22A803BA!=6{3!4#fZ0j?e(^R#N^8*Uz86ApfEctqDLa zvU-NJTz2?JXv~rs3q`%<96HyPW|kGO4R$wGFKhC1rf@2u@mb{_= zdTCW}E^IEivM_59qOQnB*p~pKco+FgmNVqj{-l-bGKXfGLOUNp(o~ta?fM9b3hGTf zqpOgV5~URiOW+Ig9G!lD;6&}+q-4*+c=VpG@EqGat=m4sT5sqLN}i<0MAqj%#oWk` zRMxZq+=T&ktfNugGoCN*=oj3JYd5X=@@sC^=Ni!JBWRH(aVj((h4rj8iJ_Hu@R1<_Lm8pb@V9k9g@5{ zEE+D==i11!(V+IsRb-_t)#Ljy3*IwktSrezW{eccv()^)n$V^_JW@`U$;%N|5tW4%YNL!fOR|wXJW40|4hdmeh#bO*0 zZ8O%d;{=cG z2(3ijv=VX~I+eg%-402T%+k{$l?rEPb<#0l6;A0OS4rXc}jCwAN>=SeoV12;?d^4U%X#__6y!c^Mj*PlE9*heHm5#OAR`2mwtLo3OvTQerv8Ylknb#G=&6Of zGj!{hAbdxPLMdcdRk@XB5bq>MdZch}m;a+#++!!?2!?I%7RRK&y6+R9AE=6#DE|r# zTb7j|W~oEy+Mc76@3yBGC9}*Wooxh|(J)U_Pv7k)z_t~NSla2BZoL=jPoKLWoh+SO z$!Ws&*E-cxkYYr^*tX(51(J&o+1<NSMry&-{{N$J0;|NlL5SCqi%_>HUQ8K;I~gjXo7BTYEZNaAs}sezP^SPd$&KXPW-TmCR-iltEU zv#jqidYNY?|Q`geyVvgi-M9yz-zTNQ%}H{%H#*W?o+4pgBE&^={0 z#?VQb!$TN)1FI6-b^M%y*1mCz(=yjk%jMlDBfEM@H$^x9URusd=N_Sm4N{$6%1yzvGTt_v&skKt0Dp|xGfwB;s{+*fqB<|_~E2~Xr}#AgN#BIQ_S-m~4s@PT~j7U=Uu z`!i0wMm&pJgZu^r--w%bN3ImV#@NSY_5@Fszv*V=?_5seIgh{GW~Kd<#TNzgJtI#r zDzD5O$u#3DyTutPzJMt-X+C8kk-;UeF~ZtFY(~>Q{xP?>u~l`)qD2;UEZVB#6y4J_ zEl?#eckXk`QzFHYhO784)}~vqO^&F%%hR^b8S-lculu4`sJ=RdGJTm`rmra&DO|}T zQnZ|_$SOxg^SrrP>!5YA*Qbfj*lO6GZWxI)$w>h*8xtrvODBDNs((fKW(f@Gai$E4 zRpVAVfgPhpVe=pK-g_d)DS}DUph?7lll$gUceTnz0Id~0djnyAiIU8xc7{G??qx5pl942INPD#Eu7l-N^s z2EDo^2hrK)L=ek3lLvf>!zf7chtzS>xS52x9~VR&V`u&S7J^??z)ZtJ^OfPNeK|^s z2qpuW%u9V)jA*`hG*9|c8(O;z>DQ%QO6?KLPL86y_(T8+U??O`83;a@f^p#4o z)lnP5m?VGb7ELT|QElxrR@Xzg@4fFJSG{^Qy|la_C7o*_;@BeTu~w)_1NTvw6S-K{8|BZDZQ+i2)4-UQ=R%(b z7706t-)YV{OMk3H?H-@J6UMo=`CuPd-`IqIJO8q8%%UNxx?ri-z8_a{)WD`&^V%95 z9(OB2x|CXkQ@p($_!=$X+p0wS2l#SZLKK|HT*A9Jvs!aKTK!|$ub@*l^QJWMlUTyq zYw|zF^pKtPE~AO6`#04$Qnk9s?+%-sEQHT4Bfw&)x8vQ?`1mju_(&Hv1lf9~1gfXP zt>N_OpuQOCD{w(-Q%_51glP~vw9nXt-aEw!^|^hOhnd~)SgTsIWkd(ZC!d2Mm8SyX z`#GL@Wg?LWDLS%@ah~t%k5NQL)nM7?1lBQo>}rmVL&76(CojEgrWp3?vL=e z{a+)pCYg2ES7M^Hg!N&b73qp=35k-5hcoz<3RE3kVYpU#m)-U&j{J2aU={giZxrag zW;-P3>)iHBE$1YloLw2S=uu58XgyWxkWTL#y#ZdIBwpj6At{E^{Eyv+$?Vnj z8l}hGX_I=BIoOdRV>E)G?$ziT12<`myOGXy8`wBaq^y4 z)+0Zq4i46I>~qw#I28CQAr&uk<7B^2UK|+QmQ?wyW>d)G*CzD?;p3JhKlg#%WgTHV zh??Sk-rl~(WouZy4_6M;zPseJ^r^jkvvJX1*4iiXs@nybT))?IKSZOA;aUP2ufBCX z)N(8r((yRV!@a3l)ZyWCYK;mf%1wlVavmlD?PXru-|boXw1@y&+#J~t%wkR7<si@35 z^!TRL>c|hGK7PM;zLfzqw%1@7YclkH%UNIN1vG<=YJgIkUUQ zs`ErDsF-od8b`So4^Fme{xRwI6Q)Ie&V*QDP1k5$hA>)vJN0(Yk&Vw{JZzeJ?~CuP zyPpTUNA$QCjCCbYATED{e`&a!VHGs$5v%~}ORQN` zs_g-;c*kQTg3OdOlOs4&(^PXSLcN=63?FKkT7h;eZ2jWT@z0)e8P8RkK7EXf-zx5xd1v? zgMBjRtU@R6VbM$%HwX6urQDQfs*~`CJA^#-oKFD=Dn@~(=o;`UZ`wA+2Dg?v@mcR& zZfk!UV)x=^0>dE`vlZ0xY?A$pF4Eq_mAQAa|TK`XIClOC|X zr?Dj=$>#iw>fP)UXd5ZXoR+%kWarQBz%q^Gs$myQvL(UtkvNhq`Mmzyw*<^o&M908 zBR4+Q%$C5qx$OWYo4tbO2a)~sA{#3Qu68#-YbVc#yvN`TpGM4~`pZa`gkLmfgnmuG zx|Hhl0!LD6`VYtxx1)2x1f7G*mIQoI?{T-s1p=>`R9JX01wPPm^ z9`DPTKi>L6R!;qLE6pQ^@3``Fw}v4hW0ZQA(`PV5BRnDtY`}!{B5AcNt;d4&D0qz$ z?ZA(Ndmskgx>WME!sO?rzRXyzI^9a$pj%Nz{Yq<)o@=RA94N33eII;l0|N6NAY{5O z%;eVxckV*7QXh+EfS+|qD7Tb>SaI8Rh^$B}c3Zo$PywTpFo}Y$G?CFr*rA)zTPejQ zYs;5RHEpXbI6L~x5(ko*$zx{%N%7`sblk~61`5qafCRMQn8d;J1ChDq5z^ADmA*ZD zD(q+xO(W*`vGN6D3sA#rec{fdJaF*Z6}-9|C}!p4^It++k5!>M{LZow&J*6aB{#x& zjnw&N6aMBBf~*5G?Sec^Tns zP^(IlSn@`N=F(DdbDSIdF=;BS+xhdyHT0eoXUY9iyTIocM@}^gJF3`a<5r5AN$%NR zO`2ZjE1Wa1Fq~hWadTOTjzxV-SIhZ(^0bdh27cPnBO9(=*|IOC9SJR zC7q6<<#om2zhO-MS67P%y`5uiWYq>yFD+IYGh5`}=7m|vm)RF39dgWfIkdOjjVVkR8iJcQIi+_P*Rd;Gqw9K0AGK1 zZ&<|t190h|fJ+65Z$yAI|7HmB_Vox3arF)O?-*50CD1=HH)I)^vipR= z-L-Xf^;?$?|M_`CcKqczBZi-T);U9V{Qv#)gQkXt>t=oPmDARYI;tW4nV78C&>7u3 zX0K-(8nX23)2Ej|>N7EUZ(T_@lqS(zYPepheS?@2k5^OwORf=}cqqCq8|(^#3vqS~M*h(?(8APF<-QAqG*J&NLqS@kZl&zkPLD4q7xV z8rR)AG1+XnYJ?a>X*$yw@%Xmmx85t$pheT7F>u_(WXMw02r-D#bf$6q_}#~E8IMfM zL5rqE!yhdM{j+YS5x-(Kvg;#AM(i)d(?&(sZVA>8{1` zSN-dksRk{Y7L8BeotTVUpc)|tQJT&)9vgS+_y-sNBGsTp)1vXSc@vXlu4;rBL}@zH zIAQzY;}2SRWvW4orbXkW6%&)YXRAhtL6oL5jT?@S$N%W>SEL%WXj(Kj?KLUs_n~Tp z7({70)A+_aH;ljOsmoIhS~M*hg}#%LlipK}5Q8X9XBw?PyJP&lSNvD1L5rqEW5^zp zlEOQx5n>Rf=}cqp>@nk4eebeVgBDGT#_+=@B_F@88X*Qzn$9$4j(KQ&u{FQ*Sr zHE7YaXsoX*$z5Veqo?JFhq+)u2VwqA{X#QgYuY)d(?&(sZVAORv@ApZNP}sRk{Y z7LA66$;q3St44_7qcTRO@lMBj<;U(kCDl+=W^wtR+k0~I>V>KiVi2Y2ET^+4Y*gOr zn4hH@v}jt&@$i7j$Rf=}hCCyLy$kne)?BgBDGT#tVB*PNx4%H9`!cG@WT| zbm$i4=3oCf)u2VwqVd4tlatOtsu5xkrRhv#_6B{*%_~k!HE7YaXq>A$s2!EjSzzX*$z5^}6Qrwjb`CYS5x-(U{(^ExGV|)d(?&(sZV=)lYs@)EgHiQZ%gJ3R*evYC{1S?o1ZtR-2X4%N;PQFv}nvYrY-sD391od5T)r% z42kS^1W~ZIo)zqG{21^}@E~ z+}^4YVi2Y2OyiZ?Pbm*rx?ZY5i>5{6&CA=8h6dFLF^JN1rt#&B)5=HpU45i1x6`6& z(YSw9n_25yR2^$W45BohXa=U4RGgKqQAWGAj#;EPjDL*}QPO3qRrbXkl(MIDn)d(?&(sZWr$1R7J zH@f6ssRk{Y7LDHH+L8%RsYZxFl%_L{O}-pfPL6*!)u2VwqH*P;ZON!{su5xkrRhxL zt_kOtKiqSAszHmUMPtkpZAt&zRU^b8O4FIfAFlm*`J*KhQVm)(EgBa;-n`+RaY0)?;X-f`0 zO*KLcqBNap+%f2q^8C;Kl4{VRY0)^ay)8MbMKwYUqBNapoH=nsdE`|Or5dzoS~Na> zy)C);aMcJgh|+YX@z32aEAOyiOsYYPrbQ!qr!6^q57h`Uh|+YX@gM*7U*#WPa7U^^ zi>5{6$M3ZzNB32Y5Q8X9XBwYBd3pKff8LO4(4uM4=>K6`vS}~X2r-D#bf)p{dsmdN z*gH-&XwkH2+&#N3d1=MMYTq7W5T)r%W5}W_%P;mCo@&sdY0;?6ZA%8vQ;iUVC{1S? z=KV_9yfsNRXwkH2j9bu_%zIZgLJXobooOuk;FsmI_AaIxv}js11}{FGH{$~ zgcw9=I@5>-UR^%^MjSgD6dB8pGbcrhLcI6H^UZ zG%Xr^SDU_Muxf-DL}@zHn0>&g^0F7lr5dzoS~PyOrY#wMtZIZ9L}@zH`0&@G%6BaN zMXEuIrbT1S+P37L15_i#AWGAj#+b^e^6#JBH`Sm;)1q<4Ix|n)MKwYUqBNap+`3>? z`Kh!2ak$)bS~M*h?-`AM_fd@ygD6dB8oydIs@$}4%T$9FO^ZfDL%W%`t44_7qcTRO zA^tZs#Obrax^?R+x=%*anZ|M0KSM@CgR|r@GIofeY(ee+?BIZgZ^`t?sMR1t(}G&~ z!(Rk6Xvt&LYLKC6L7npE(*X@y@))%mWN11=aX*uPb)6-Tp&GHNHw|(2%i6%Y)SleC z-TdmFDFYTp^@o;PM!ts2D`RIUVNVZeI7=-{{Ks8dUDQ_)>hmn>Ob(cMkF|IR15R=k6|T;G@sS=pTi%R$CF zH#a61f2C}v;@=OVSf?Fcyy7+4d2wU%zP&!7lG7_P`mAV7mRq({@xAgC;XmeBIoU7Aq)>hX$emM} z`7O#`tIN)Bvi;Rwmo}kh)Z-NyA70gjS{d?+sOzt9La$^P)+<_ex}^zyR6Smi@yy*# zf&Pj$k*I^mHYLAUiuPf>^4oTE^4y1ts>iE_2FG}Iljh{250o9cRz#iHyE)nJ{X9nh zHH6V>88IWTHYmQ2?Fr-pBFjDrtrPF|kUg<(n`mqlGiXYtC7mdB;DjN0*BuCB}( z#NsoW?MUr>5)F>}&&-aJNqclRs&RBC!g3Jx?LnreW;v`^v@AHe zsfX8!jAzbl!hFv8<$FaGd)E|iy<&U*V4LRT*?;up4w6wC(3~9cuCha35%t(^rYEv> z%rLB1(gQRdsCCq}VCgZm6{HH5;aeO-dL?ZuDMS7kmRu{>14xhR=J1Yf8A3|$x%MSw z$h>RC_Uf(U_za>(%x_BO^~+6cXf)*Tv?`PI2rxWA%> zjN7(t^!vm(Q;N%E5XH9XdqoQw5A|*g^weY!<*)8ImAK5g+DFr({ez!5%V7{X2xmIMnws&Ll?*+X`2pQQ?(4-l>s)ZA#WI z&b*plZc0vBq#hc9I1`D>v~1blgwobi4l<6K(NxFPCF;!gns7Ji6)huXndc8%GwSh* z44Ef(UlWPq8en#nGSA{N$EeXiX}0sk`o<43J}or|o*`z-)OL8eRxzEK zSYJ8F*yguiSKK#>)SzYOrmrjdm0^7vWQ<+!>x%n+kQ&U3sN2h5RovHrWE?ksX+`&D zzjgc4iteFiP7W_umrq#tTpiijGUpOLgBCKTTw!Ja|1a-ZjSQmtj);;ur{ytD-zG}l zKRz2Lw%RUAwl8IA=Hz=d<;*C#@(;DB(WgYoKkmecQ>DdaTA1&RKaG;_-j>Ip#&$!a zWUG5?^?a|GFB!l4d6eA$K%NE}M9H=4_h24_7P-2&{&!EjB17)x(ns?&=oL{?dy4;? z$BwnN_XEnZt zj8Ue&rE@rRT$;^yQZPiHmbvdf#3(NA=#C9(8yeC`*NTY;+QX`2ku)77uq za_F-&QMqdXh8maoj`r-wR#!AqM7XD>g^bbXuCBNk5Y8`utR;%0sKbj_98*{SVO2%r zB~aUE&_c!~C$6fvNC=MOdqosSZHE`HI8I2Pc+Ocpc|IpY`rwXp@f3|d}qiIS7A?1@)o>~x&zwMXV@&?}-w zH$}<1QF)A|>qW^^3o^@5CcW$8o_IyZvkg&l-;z8HdPUSA!|1ajk3q|~4C;neJ@JZ+ z2E$&jp-b;$DzU%9LX@zt*f5Vli+I?%X@+9?{PsZ$8FCfQ-6Bte45H*pf6yn7A@_Oa zb(uwK$)A7Q6R*gS8hX)9c^dSJsPK%8^ACA%pd$ct3STQSafmzICl9Ow2&d=ht8tH45BzDT1G52@vFU~(lq8uW@N_W9wG%N4!nkgPX2pm&rkJ_0?1xqI0g z-p#IFS!=WGp@`O5XZy zceS0UaG%J2Y5kp}B>L~3ctytS0a0?vpStsksJZ>3aK4!WV) zt~2>w(K7FrW~^D&;}sc~-_;ygBcoSDeLA|?uF3gc(Q@$EX1h|zoWj?NjA0L(HHX~w zMCL`*m47lbz}xcf`4(%NlR?{LmWoNcnRh)a6J!+EH3w$;;+3vU5cQN{=#zzCa#|J| zlzU2XIr$7~_>5x$Peo)jG&q#7^(oA1#L@<75f9xHT|HirQ8euQCrElllw4_jVzpkG zL|o=d{07&2Rnho)9J_o5Eo3Y`{HuzKjORH1=uA|2Cc_c3@SWuqjV?uPr_goIKt{{r zFDou?l;ikb5w*)-zpS_@P$9-2u3lErm_BC|msMQsoe-n4>k{)FOBRjd*7cT{ZyogO zWin{F^rJ5-8uy{Tl9O@K_%A9hq60OU7g4_(YZx{@0~xeT81+R(BP=i{hwpkn$k_g@ zFDfo}g5&u2gQ)N~KOBW+iYS>m=}Nlw$|T}4`|v68mlch;S6_cc#*rs{S#h!SoQmIH5%s%mzO1;& zcx2G>_{YmC8r!ZuugG}j$z>H6wT>Fhiztp)4lmb=qt}1*UsllwZ_LSO&_c#@3zt@0 z>^8^ouN6^z+wbt=72o~04p~~!IA_%M8MKfw<=~|i7s1SNe6NUV+;VBf#UmqwZ}R2& zbMTLP($2|{XWZL<+NISRDStE|ian9@Nf_(_WX1E4i+kb~8L|d&$YtGmMHHVbtyi&l z#nvfLMQ)!&Jv{>%@NkyZtQQP6A9JG+J*>OuMF0Pv6_za>rFLij);MJA!L-(-A zxT1SoEF%`izh5vd|JiJ30QA=3yOx}c7iXH0I(L@Ayog%+ zVY8j1_!_)Zdn`2HDBAU;dfFfvw=QnBYgxVqy&`H_XS2T+UZurlT4pb4wyWpNDXc-p zjAdq*MeeMRc@Z^wxtY~eXPLeREr+aZ#*DR|YemL0Uzsn%a%af&il~iNHQO1v?-eb3 zuWrWdz8FMMdMlZLp%VqVd`eJF>Il zVmdjMxJ(Nf|M^j8#YJKx<1b@7D;js~_M18@8Xt@~IlQz%TFChGFFGqO@|WSn*>7BC zUow1~&WgrAVlUxq&_c%Dy^XgvvXSG&Wip6b-K(?W;uK{b2`?^lg--fJ_jbCTHA^z2 z57sy1&L{1!%&3#@3iDC*)I>65l+ZWvwHmtCN7S&Fn**!P?E7|{-HbU^JzkN~Z%(tF z&xO4riesW>#B!~;-X^U`caE^#ScXaX;~*K*9`oN?GB2W}&FfoItC4y|i}Wk*jcYyE ziVW$y^55{%E25--)Hlk$SF}hUtnay*lVzBMf33)ne!k{ycnyOn_FYyZ77g}fGOFp? zQhi>LA){#iyCUXA6rbCzS2g93ny+u-WHyk$bAT3^Ww;#)_1tqZWQL==A;Mk}wdssz zyI;d+a300pj{ULEpoNU1|MQEAi(%?Ge&0?MM=V zaZxrM$M=dTj$J7}Hh;};TxPF7cB9UUMv>!HNEy{X{`f)0HS2d)Tzojk@fk!th&W6R zFJ7@nexq!@maV=~WS#aIw2;wfVzYfW8DjrmLTzpZ-@oPI#jVE+gT(raOm~U{0>wU26#$(r)s1m@+S-_)KjzQm+`fLB7<}@12>GWtfCtcgc{i1XpjH_x^)< z5hY(A?z{~c#-psiqD8)H{Mok@RnHwHL%yi|pKmJLmBcRxQSvRodVRpKUda~2JG$qZdDvq&_nuNoA!{}a-|{t> z7f}*jF25YS&l2ZQYk18)uez44Cu(gVL%ykYt;?0fXAmV{WNX{B_*hCVUps04K~Lm6 zrkb81Wza%~e8=SV3{@dtL%vwoUX5()l}Y&JAVa>8_j*y|k^J}!q8M``!)AZuvP5Y5 z&IVaKkeE)_tgmgt)i+PIhahdf;;sI#?QQsXTL%wixeHS&D7g4$ODwccB5kO*IjoC==pzG(If-pD&kRj2t zp5IW}j^i_YR6VtX42kJAr&`~fLgtmrizwDmpTRpN(Mff@;+^#ww2&bYP`j_YL`ftz z*H)ZLT&6{0w`uF5w!?QVIT;cwuKR06l*FbB-9dMC@B7gwl{2=PnvMpyZ1G8D#y7Kc z>Q!w1n%}tmyT(r|_YYT#V=ue*)5?ce=>KQvjNw}xV{+xw$~iF@h9p06dCid7mAM;a zmcMR4yK>F?igN4>oBj32K{8JJ$E?cG^_1;6K7*)bx6Z1JYXBoQEb|+e|5jL7nR?;0 zn$dEf;R`EwU8Vn@p)-bWalSuw-@?jmH-ceE@?-YUKe(W>-y50wT|*aC?wp{WICh53 z{`%!0>^>hX$d2=d{eOK8buZY@d*EyBBJLfUjXEe8c zT)DNkqU!OAj3oZJa{uPa4tYh?=7)YJ%TFzveeCpSiB1vh(8C)7lWP>8LdQ zD@)hR04(F;wTnzkot`p|_+XLQBb=pc7)C;V;xa8{u+Pu1*aMj(=NG_1q6KUT$1eIctKlT}k|M5Os9FMU_84m&c&xF`1cE~HD9(wlk z%EQgwF%BN}S>^kyvNrL-4?e5h(UI3aHom8$^5}N8HGI?~9hJU&WdHAclDbO^>v8$H zj>`Rq=P`(S_3w)-L*uM{ob}S;%ADa@I@O5HU-KK6X_@`N;>xd1?@5ayD47t8BE1vO``G#X9EjQV!O*r~78^4zIwbOqw@ZO z%D3KEwo~!16;XTwNb#}xYkuQ0`*Rbqta9Ge+M1s-s(<{Rnv9K?cU10pvAfz%)Onp9 zmDe5t!+4Z=wex!&m1{1^EO))$QJHaiPrM?dtOew_`_T>@!(A zShZlIt)?FG|P_wI>TWZbsn;!4vF-FZb6 z$9dl?-rL(2bX1oAz9)B(j9<*|s2p%zcXyB|)*jz0_U$MArlWGqrakeBjEgSosH|Mx z-L)d>q%%!zp9_Y)gS2ea(ouQf-JaY*GHyJmqjLKMXr#rLcFuZ56zg;qoLw?+)fG2l8p=g!8RouRh(y(`5JATv>+mW3(m^uqEV$u-%g8Tw5%-R zoDQerGl*jOQhc1T;_`t}$woBpcU;LvU=Nq$)*V{xJK9^fZ9#--dK>bJj0bzS1Y$?C z?GkWSDyP4wCWFh!&hmLk;oD)W)OuKe^Rj_h-&*wRf-h~O8%J^ zRhoZwiQ_2x1#N@+ zR7A$N2boz$?)-{*5yiPgs$snvG0XI?)iXb&O{nk8DeM&)(z^1m74ss>pBWex>lL5- zrSH=E%ACSpk@47WX8yo)`>Hg*7bS{4cnY>&?R{#=j`ZS*_K$x#yX24OmO%>{o1I?@ zoSa3C(LX8Kc?sFoB4o~`vnBO}j899Yz_S7KB1%>%+_Okk#grv3(;}mco@>aQ!d{Uf zn%n5E#jCHP5)tI?uObq8SGbfKF-X@sN}!{7=-kSmMK>h5znw5 zugLhr)x|pEClYntbw$K$q*rVQGT+ft-*}%KUhX+9WXR0PJN2zf^BF|(`O4SexGn3j zIwn%vXV5~%AHUn;pS6|i6;YMFTLR~K(X!;A7TZfuJM0x1`ybsB=u4OvQP=;V#qUev zOerq&S^w!rTJ32b?1_A@Xdz?!CtCxjd5}R=O%G?iVlR61j8=O^nN!#+GR}IhHE;)+ z7g3x=`5G(kF539!^4#CENaQlj*v1h(Xp0S-PfRn44DV%s2yeyCG)QQt1F|WMxLeKWtW$1G=5)$ z7BX5cF4achkEMs>h%9V9!A00QxAQeGM+Q+{8KQb4Ql-%O0Kkv#Y?X^Zr^-k5jDJ?o`H;KZ!`0&>e)q3 zq3d`~6#L4Q5of%(%+@K-RN9NOb@>{!kRi1_|GWt@h{|oXMl3FK9anr`b$n0W&P5vuBkUDXvj!9c-_eUj)5ztlp}qz!WE^xu3A293@mD{Hns-adf10pf(Q^4+CHs^@ZKojZgBCJA9bLi` zlH>Rcq7E8c@}In{SF{X!*o?1tW^3=%b_!j+BIAlbnSSLqeM{;%zE?z@`?z@@W!D_Y z*kWzT#`Ue3wDz^)mG;LDuU@HzjFxq!K;O>1h;YC8of z2Q6gC8kxp!3^Rxt-Bb+3vS9x@w_mY`5l_hIy`AYrZQNq#mtPK|PT!_zqaHiFl$@5o zZ(T%Grg}ykGDh?%A|jMi@x3DIfZk^Inj2x9mL0Yz+E{wLPfnrh9VBDmH;Q${zanZ_ zFLTeU=Suk+w6twfMC`75t`!-r8yD+{6-Lym4U33P=KPAww7j)}*&8!UTTwk;k@3d* z<}KM=WmALKohUy0IK12s&b9@m5#O1U&!B}2(aVqcP6km@rclIpXOZis5#O0p*ef#R z-gF<&k|??V`4Qh~kvisLan^GO$&h;7ee6%7q^;;lU4F@Fk@o1$@T$ivGNjGxnLoY; zuRBrFuei9ZjuV$TgOu5_MwOSGbjIoM+B2wy40+Pek1Ef+h+4UKF%bWXmbdpSdNH{vtzasGbw)Ysekc`4v%I3G*3joicxLaj#uYK7$%$$Q(r@Xond-s-AXE zhRmEaCVj1jwgaO0yz4Xed!}UHzJBzAc>`X3f66;+8F5?oXb$JiuZtAlEa|&yaAy2~jt2oSyC_Z0>H8z=1vZKr^@00>#6fG_P zEZK38+F=bcUYu#(bLYlZXI@0D{jh{JAg3aytGMg_p9{^-^=fQ&=H&2lKWHK2*2Sg3 zoPi9YrgfGuM{z0=x7Jx^FDcpi4s&w&u3nKbW0`qlk{er{c@Z^wxp}4z#8!8fgIAX9 z+>kkiy&~h8ugvb0+}P^Oi>M7(l`!{pDsh>Xy;qm)JeoNy45?%Jk%w8UcbHwWE1qO${-{^9kkM~WDKO%YK@|Hg zt6|C_QM27@ja(m%n$4Un!zBC`MTT7I{HHtSMU>o4_cU2mNxh;)YK%tBW=>(R$dFo@ zA2pkK5hXQ0KWa8D((d#ro;iiRB12kme$;H{MU?as8u8z1q}PfT>4|j3gE?7-N%(h= z4C(D$%x%Z<8ARpQ_S7q>?JiPw$-Zf*&np=XG`6;kB`$(?$Sa~`?8=Xt%{kRDGjq~s zj{3YJC+;ZrC>da>y&9I2*9|SZWFT zMCnU3f^vOcks*CoegtLaMHFj~?-lQ@_^!_!%*irL!mqnz$W?I9JC5Tsh>|O~m6%5CWlok+Qw}oZ%NLFP8)gtC-_6u~O`dwi63VVHjmue|S7gYJ zw3-O9HRT|R*Uc}7e0`)5DdpQG7gMyJxSV9j*HxhyrYiY4lgxPo`kg~k|EzKhN1#HhL38sF@;l&*!(rWahb8M{g~ACtbTN5sJE$x{7E(V zM9(tG7q0n{5NMI_wq5P3$15`AJMsM5POpfP@6xq(*>Xrx(l6H4BHy^XmRgTjWXL!5 z+U9)?dPS7%xvOa(>D84zqOR|Ap2$v6?d_~a%J40YA$tn*dpK&yj#k$@JGRv@3I8n_ z8M13uM~0MPJdz)uK@?*uWZ3MlL{M(EF<#~C8W$DTwE~~P+DC?b*%OLF>+&LseN0yk z*Vg1ade>52PF)$!LWX>|?|KQx@p}fMBu;?#F%BwXaFI=s*L%Wln) zenqon&*n(K#}S6Uv+xOCzgB8 zx=VIxKyx%{kGx$L%uBA7?s_0Q>=hYupYnG=FfXDuSk)Z4-4ISCF4OYtSIv>GG&3iM zmy**$hB?(P%HIJ&22uAcZ;tX;&uMvUS#zYT^2{mh6&dEdyC{DL1oI+lYG-rgc0)Lo zxJ=8B=>D!YqgQ$lVG)L}DtmByb@nLhMdv;%%X?oG>+aB>M zznx!sS95g#1GT-uywT0kR`+KA?-ZmQv@qWT$2Lb-{vnUC)BerTi0PR{`gV;VLAI~K zKFXY96P?j*f5p7WnA*1~dZfzsHOL_9!#+*X`?(BS%sDpE@*a6b#=3QlQA5LLIg#<` z6;b9K8>{D+Lu!V;=a71(>&B8({>VuS8B(Wp&Dz%>gD7b$HEZF98kcF2HmGat%qgrv zhO~cO-w!j0l76M;y+P`gIms;2mESkZrY2U`jm4|{_CeISFE>ZJP90{@LWViXEYe*B zwdGKCqBx848JtsH@h5YAsyn->?K5a0 z+|m^N$MQT4dPS631B#Aak;kCLoSYU7S=ke>$k^l3rs(3Y@-*lbQRZ}=XxP_z3|h?j zIFZIxV@}~~MaKM-o1*@!^E8+jQQHhM{cbLUmcp@3(RFKja;?Z1dSp|yZFL?TE(cMZ zIj4*`v||R#7uFzSzqyTdj8Q}#|F1?nCi-5Te18<__hT#Wjw1aUY`unt zKo3U?8BgC9MfqR2kwMf=H%8IuC-Uw&8SPg@(XIcTq56%8qJ9tNF;44jidJl&&7BWf z*%W=d8a+v_Zug@a8S6KgC(E7lX5q|>DCx)U*fozqi}YO@D~dVQ-gEVr4C&!CPF0ve zRQPFv=jXbPV^a^r7oS<9O>@A`n)2e|D5J1e_tQ-B1-;W zdvMqiQ&_c$m?=(mGJD15I%A6n@<)7Tk znf|<6nr*vdPGPUeIOK+A+k(RkqJ~}D9O-v{VaBJW=19Ma?|+<`olVT^8TbStPu?2; zsJ?QLAuUQ{FNM7#Dm*V~n;1nJt%JSYtZ9*b_VG1nA*1&@k$pl+8F9vo%hZ_qY7}W~ z5wd*^TF99Dg3-vmx?~X5_o>L<8{aGT`qy6EXJx55tPf_nZ<%`aPd`qPy-J8pt*tSq(CX`pr+HNWT&^`0OVZjTsL`QCm|sLJs|N z6jcg3T6WP;l6hb2EM!Q%ntr6Rt8nv=tc5!UQBo`O<9N{`wO!+QF(;?cRf7y^Y58%y zm={sfn)Bm$9Tb_Bv_rBIO0TBAOMj*J-}j0ZGNgaZzgB-f+dNwyn`vxzesiSX>NBUX z1{uYRo1^@1^_dq@mtWo-<$tR$Puq6?O?z{sJ9d~;*sJ})*n7HJLAWiimM|}(WE|8T zNxp`R`nrQkW*NHMlsWkfTF8(Yj<+YZO7qu6iQ-y~ufcI!%AtG5M7#T$3>k9W^1mx$ zUPN(K#P>?>vpxezEzze5=HxRth7)z~Pnx6rm^B>frLE|wMz*g(3mJzV)@;Y%FoP(r zO!^G2WyrI^b021XEwz0HEo8{Eh>OwbI9V;xdrOo&`{c)(q-DhXrby#iQrjs=4@V0b zm(DUz!qv!_VFpoWzSk6OFu%))W2olTH&~9E(G>lAK~Js~8C$eBMUQ@-r@?X%HND&v zow6v8LCaNtZHm?|?ul1q?C@k$bVp~N2E8JRD^uy!jk9YUmtT9a(XL;7`lMMksIGW$ zF5xrQt&42!E3J&|3R?{$lQ8##7BZgbi~{R$WDvFMg2-Qov*n=Wnc0z@M>D6YLs@Y= zugLh(OjEwx7^cjNsPGJ#vs9U7Xph63!d{UfGo1X0S&woeHd7_WXnYSCUxXfJ-GA`+CQu-Lx_PwHo3>o3_XRl-s#W||4!8xal z;X2c2PCkPcGWI&v%mAy;3}FUQoYnda&N;6hY2Fo7X#N-FVXKgnN!#+GGwhL|9ej6MU=di&HqB07I}B0&x6b<>=haE7D%5g!wjP2O;gPi zq@l)TTI6k(eqULiS7gXrGX1tQ>=jY+j!nPL^%=CtyF1;@U!PZG$a};5uLqbHQG8nR zz2dWsto7*^HO$Fp&_af+E#`m0Lk3Z@mZ{(W_!_jxTBv?w#GHHvEo8{5YW`O#WDvz? zxKty~l;SeqjGXySV;wu4L_=rcWL!11(e9b@HOL^UzVV!ldn=8B_fgD?D9#^!uQ;3B z?$Jj3_Ki7ZS4poGEo79&Hrh9TVFpp=e7(RMMUM2pTEDaK$}T;bGmtTMqt3!T2Vl-n zbxOM1@xe<(;6eA;}v>U5(;3 z3~P`fb821J3Nwh}%A{q)QgW`)$rFU`Fs;ukGUQoA_o`YAlkl%DQS!W#zY~yeMr2;1 ztLN-vd<|O2kU3HQ+gCD(;_8mC!L=Tlh3Ok*=HxSIAw%YK`Oi^g5LL5M6lY3t`PLmw zZ&_WlxNX};yZXU?EX7*JGrLFj?c1@tnR|YE_7q>k$Rx}iB!j4?J)^*zxYM_Z>~1_V zw%jhV?>wv7XU>&rA!Ba8DDa+>&u7;T?kvoEwMU&9YmvVnE*7uO?PdDGYK*pl--zr!vie#r89Qzf z1@@{jFQN|WZQj;&jarv_Maw09BD?2~Ir-O$7Bc>^b!2z%g&9OieXCh@PQ7}2KXZMm ztK_dAWZux+)ZO?YZ^`s2Mc$>k=bC!P4>IJfT+MS->J|GwSv7LI9_rB`Lsq7A$3^&B z5hbf%H9I*{uV|4KHn&5h9NO)Wf|S?cNrj(RzNP-Jiyu;o}jFft_cx zka6`@jsDIvQ)>B%%j`MD6J2R0JHuvw{q;mLq)hoM+sun7u4SgV$Ev~Jd4ruJyNj7Q zWmsjq$MANN+mcg5dOO_( zPqtrjTF7YMBns@hCxfWgjm=xq+;}qVk^eHbvv73ZtmN#$eXnRCW6d+2g|qkQ(zhFG zTqc94U$l1?E4{flY_OuUaOi1y8uW^&K3jcJSaA^;_73tYe!XF2 zzpmg__pcQ#WW2RORQolD(J|j5qxKOsuK9~X|J&hJRVXggLI%rcy-GF6AnNG<{Gu>s zR2PkuK?@l**UEauXR^_~O}!f2lbH+|7j4s7*zAJ5Sq7g6iTd?`&cd{jc??=U_-<$6 z+FN_#6&V-o+gTVmE>DAA5yf?Q|9~8rW~C_giu0Y$!y>!ypgyn2*!SotuyV(|h~jg* zEl27V_ut44A-6(X&n$xs*^!jLB22G{;@r;nigPvDf2FJE%*irL!mn3k$lkB~y&uer zD6S2p8gZr+m$`=Y_RWoUPdi8L466)U$k_GzM!TOr%pfW}TjGqY?aW5|jRCd88f26P zH`=ci!VIE59@OZ+caAfjoD=8X>d7jOersEwS7gWvQvO#d%!??g?fMmrEr;& zF;Vv+vo2VMN%-}O44I?kf5F4Nh~jgi)kwW!pCR)^U8iPFmQmvs88S!BUkhhmM9FGG z&3b$46)m#1plkZfDeM&)U+oBA?w=uAXg?SOhHK$a=dc}4iqk(=) z#+<@lks+gxew7wx5GA9Xx5vwRMT^{L-A7iRS7b(C=M2tMyAx3mLL@qTkwv8AR1Qc~xn?SGSD)qA=&RS`U2xy!kocbUaud#v=9x;(!H);S=i-OUYk3c#ZKrvm7>RO<~B= z?n=&ULdF-9))Y2do~J>}-#1@dICW)DG{|`WM{5gbf89kRy;k&!s15I1Tj;yG3nMK# zEysMkws7s5o_IyZxbLkij9J@7qsA+u`dqTEaK<_?j7Rd5J%8xe@4Wx)l`^crg+U7$ zydst%B#%MVwJ)wKyhpF9w75(Q88zjwjK@A+R=9UoR{MUl&6kA*3$yh9yuG|I;~11& z7_oS@+i@!j(|?v(M*Mn3Vd#)7{lt^Y3R}OMv8&mq|HNfl$QY8WD7<=M7Y&i|7(}ht zu(I&x&p+=v(x{%DxW3Y##UT)8jdPNHvl{rfaKRqGuevm;FdnJ3VV!0ozC9D~|vzC#1 zMGF}>ZLy-zIVi8>WDv#r=4+f$Tvj;srL2Twu3lC+sVz&hd_IE~GN>J95Ovs*D+~9H z$}0ygWQ6N3>(AKrzAjwye3mcortcLkWNhE`bz$|(d0vr0RNvozT{tr7!ieRbUwq=K zLgAgvvg!}33MakS6R*ga^TDb@zYn`;m;%LRdPNl5iYcYT9-FZb6$6)JKS`LokKOC~OaLMI8sflFFxP58i!t1+hwL~2^ere&rJG(H_YsGsz za_ZNG1KN9Xt;lHHa#i8u*Sot`M6vex*NXl=yKrgYjX(C}T9L6;|7C@Nk9R3~dOz6C ziDHl8do^hBmxb5+WhFfHgf9!LR%ZF`^4Bj57j2bSa$3lk5`S5Ed&4e_^nOs|=pmpm$O`pP;lf!rQiVTTBq&bBdL`lS>nsUUVArYGNx{D`Tmn8O*UPG&qGH4+~ zVlU~HwhWUndvQzz5{*bGkx4YxB-uWL7BVFIiHno#IB}U4iK?RUaG8_C>($lLk|7aY zGy-mzL6k&_adC1TN7hAmd|umutO#rDPgzf{iF9j-eod5>Z7qkai|3b|7Fm1e)m3fh zQ&$Z#WGATJr!a#k*=_0~RXUDeuO#*gdj`Gg4zFIRg$#+}qJ2!5L6k&_$&br9th1x= z+9R*0?QY}c9fdny%+j(_sJ*Mzh{Y>fWChYi1$KAVGEBn1=VZw0r;B0iI6i|YSyj!C zZcK};*1Cws^>{^wtQ3c0%DTLWVm-E{h^6GL)$+c@MSpfVS%yjYB_~66!*t)Gh>~{R zeQhU0-W7%0x#mR_+oN9&wgcI}tubEf^NI}F7oJ~tnHNzUgMF{q4kY#xz0#KEGuT^_ zA@QB^+c_CTvCsE4*jq|eF0CcZ$!E|)hC~Dl_o7-tqU3u{?SrjGEH$z7)>(yd4O7#e z;mm)`DhypOOS1>J43qHdE*URxKf7?v`pR}3pFz}`A+rl}H^^hK_qpbgjzZr(vi_?4 zTu0&l!?Uzg5U=Dtht3V-H64Yfe`Tm4>pBWMFV17|n#i}W+8gky`x>;6A>Z1%p2%_h znI%#3ZEsBvXNcw`0a)ZaVeL7o?G%JT3mNiVvg@@S$7c}5xwgaWJzuwON_F1$Q9;x7 z)snUA9Anh6vkJRc>szgc_C7?hhFV4}Ua|h{{@Coo0~={ws;A#2V~?e?3lD9gY^!24 zSVE$TovwT(|WjuM`!oqDgW~lw!78b_e^M5c5)xTC`jQ?U`q5mVw zwklm1L>=94QQ^W=Z#GtFFUZiX6hSw~^zaxmgd$@hvgGWKJ>SG15}-j)>3o0`|dkwFyuN2_5x zi^~t2{j~bTEBkES^Kw1=eaN7;)i4REB`z-?C0|Itk=|L$NWBtoi7KEoFl?z&3Zwj^C@J5g+D z4lmbg=?jYr_jYD^9sa>0a}~0>#5(OWXd&aWwTnz&f|^*R`8AO!wlrUZqi|cLqwxLK ztoBjcXV5~%A71Y$%s4%-?vg>&ZtryzuDK+SLCeV59fbp~>xoxn+_|8mu>ALV8uW@N zK41AIXYFIHyzuCGg`q#}QWFg|E`P9NM`7$U*`2!KppL@r6MAyb$=I-^qwv7H-Q9Dd z*mwEmU{7|^Z#oLcY}%8nOU6d`bQB)luDh#C6lZ|et5~iT$GfpV`lN8iHhM+tX|-gW zH|CSVk$shIRl2lVqUQbklfnf9x-e3&XxV<(Ifc19E2{JogIq{z3%8q zEg@s=rX7XJ4|cid<^uS&ohXjl_DZGqgV*HNU(PR#xlc=5&;20d-52H;_WvJcTa_-Y zmMC6ni%-4cRlI7!XN7II)+<_%S7e;D{pW>qcTl!f>Eabpd=|C%)GOA8TZb+v+&Mu@ zTaQ;{Z1muQ!hUZk+p2W&im2D#Ur_kg`&}5RSF}9c=d;3oa}-sNS7iMA2cH%0=umdZ zE23EQ9bRhUzR#5GzWHH~mu!@*{hl3cO2*XhF%wxbPWkJ}lX9wf{EL z)ZJ=?4e?5&D*22n?kd@6+(gYAT?)k6riF|{#+Ljz+f|{sY))vlUx0o8(pLK|8QH#9 zzdoSVMtd@+9QQD86d5n<((0ehVZD+!4SG$8dcAL}fBr|DQR4DX?=RZeE6eXL{@?L; z4NIbb7j2BTwlj-$M0=BKqNCS(4W=imo|W;;CZ?9;o`NAYLr?IVLI&WZezbB1*JpUm~Co;byvd8YP_-xqI$nmwC%Bm zkx7`oofa}anpO<7K{AMHn^^QCzFV(24hmaGxZnM>7>GzJ?M_kB=5;s3i21E}k5?^( z`N!-}#0dGL-X$BGh&efY*HMj(54R~HE|TN;;~-JtnLfuNnIY>}*!6iuhRn`uVnC!N zXI@0{sn3-{O3oSNl6fT?8I3vl3|h$OKc^Ikl12tm^8Y&Ts?|^n83SgQ0x^V`7g3xC z`x=}TxBjza=*~icGBG+>kKKGWAJq?OEg}ox<;2TPT z)1a6aQNyk+1;uKY76nHN#gf2$#W#_lrII{J5MaDB{w)$~v<4no-iYSgNzE`x6G5_RN8(BBZAc`}!lo4k< zInT?DUQ=H!_VDBrGUmKs#@*cZ!EzAQ_oUstqINJn2?v@Ml6WhKyZ&_YK0 z6~#c$Kn79$MigyNY{1TF6+oUNNvb$h?R;+A#80AZdBFp=eh*nN!#+GEOw?{PkVtMby@Yk-vuQ zTZ(p_nmL8NYB2fg%C_W`zZTBCh!PKL*4ty5mq?q?6S<^C=}BG8DeM&)(we(Jb%7|E z$?7Q&z6R%b2S?fD2`x=be zK7$q+!<8Xpd*~!Hvu0sWGAr5G(ab5VvDx{KQM$Nfqfmz#ME%$0r9d<*nZ0OiRGIr| z%+&h4B17g~`Ek6M7g4hM>EedDlEh_MThjB@_G>TMc(2UK;k!P6kn#QLX1y%;d^P4p zl*}wc=c|=$R5zKoYkWB76!wY?d1k1IbZe;oQxQ>Ine;U{6P9vlTpaOA=PN#g7LQ@B zv=@7)O7mwKa&;BOH9%iOYM(n_tz_4CsqHg3D<*2pPfGT*BcH*!v$SA6UyW>^K?@m& z99Ht5wyQM15xaz?9-G*zEhWNgvi z>PNH6y;ej`E4KzBX3=uhUt4WduKK(pW5AQGenh!kuZXHyfs8XsT;>|mm`aOXLzIy- zzeUkP#%nLO*!9b*L+L;MN)b`>o;2%lJS*OM#d+<}KNoGApmtb;j5i-D))Bd$D88-9 z3XmzqWxl(4`nIBN^YwW}#!WXC>xlAC6zB84SDe?%dYPU}R-ad7$f}yY5eQ#hqDG%s zv^y>=BbL6MbG2v9yiL#M5U=v*KD3Y_^WglmImjSN=GQfQ4N|Y<9_X1CayRu%3+80Y zVG{mVLS#rS(HTydL6p?7{FAe|TR{3+olQ#5kbkXcAwzoY{MjoRL~%CgmxHrA8JG0T zFy`blXdz?Zsb*G_duA9JME&_}H-GdsIJ+A;(yaBI)RWmO8M1y=6YJiH`STK@IDhoL z;_Ob|bLr_^ET7Myg$#LPmVY`I8AQojx%|_)XpuL6dNK@i3VTI{yiv?Qor`%9CGR8M z$u~~Lyw!(A-gD~dT+GSgyWS5n0Hc2neX%2gJelPPDX<&wG z-=t`F9_5Y(Mkg){L-!Qy{f(mCoe*ZoikCiT${LwIf95hwf&3AN3|W(_>910+II_sx zM|T}0jw>Pbz^NeS|=Uve_!-DLhrP0WiZdH)$Y zsmZ;ykkLR-YLXU}Uw3ICLq?+flbXmNiqDCD$z{buPvw@ePfu!MPCkPcGGr9ZKdFff zqH3Nw<4nnXSA@3q_RTGJ|CL?Gc9Ufr@QB%)s@%MgMuh`=}-dVJ7lyTI-A^byP|z}>oaH}V@|(f;C(GM{;_q@?nxv&)u{gQ+c_B*_bJ-_kYNT<2lg)3 z?z4=gFS)m=XlE8XA7^^)>M4A=UZo7~vy=A*x>}dZFz)%~AVc0$U=?qN7~H#0g{cl@$^X#uK#;xgOX4qFuMj_5Fh7BXb! zq~|gD8e|Z~nVQewysc$p^X8{|4ia)jO@x7tdh~=Jh1w!5r>X|Rh z$udmB?@P!Kjr=oTm={svVaW}q`J@P(BoBbXmyL(Ed1{ocP744q%tjt<+GKhNpprU=773DL z&_c$?1B!t?cVrNiJF|?X4YDVbo?7=nGN-UtWXLFyzq6lt5ydBctC4!e=YAR0^g9OT zWEm#m_g7@dh?@W11oI+_J$R~NJd&Td%zOLRe&#-3+>?4m##eirdX>A|P`uKz7s|^@dvyQs-#}gBK?)_hG0%%ugH-8F@Hsvc@f3uM5~c{CEqb<)LdCR(Wq?lwL(q&-IPHK z8S>?V#{0DzCJ~qA`!S8PD_@^!EM8&P^t-7BpEG2H)7VrjpRYj+88V9M?)Wf+C>g2q zXkufr;_mA%6v{^ zCbIUVjOri17bQdHj2f>o%pgkUrCuD!*znA6T$XvXM(mUsxyJjf&nq%y6;ET0hP@(6 zRtoc@&@!Gj<6hTSuevg7+PM_oi%HJX%BpHjB7(IP9iJ&OcQhP-d-J`yxh@=nK# z5NkZj?jY~2JiTayP?pJH?T=rt$dD%_jZSLeCgC%PV(qbvSQzwo-jgj}Y+P;smSHv2 zLWb;TsV8EuL$R;)H8=vu*Lm8DvIYALTF8*^{Iov~Gl-IJ3G>S#`{uPa$Uc3oSJVz` zkRjg))YM%=_1}yVCEpup40K#F76&dnHM1DJGUPQ?k82NpP>;%)kPxg^% zFUp+!a?nDC>|yi9+BoC+*NP~9?UrJNVSeK>dop=@qSvavHb{oNp~=4=%!?@YN~u@X zKXI8guJ52$+Xi_>d4G{0UE#X6mWWAoSi#^qDqd^*wRK}oxjY@+@Dlp%jo zMni+M_>A_Xp4jmhh#Gys#H8lL!qh8TUfpG4;!Y{7$15^E>N7EUFZVP--a#MbT7h4VA-A_B zdIIS#H?$>sw&=Isot)&K4M>fbR!mIvguI{4o0!y`ke8Nx&fv+3o=bG{36m3dN?|=E zC*y&`Cnq&$(WM%+ys+2gM9({cKA>-izlM_85G0Y(9+}@Luno|)|uU?%sDbdqG zKbbKp(UVZA9o8UY&TEqrJv%kbAZo%>lM+2s(K1r6Xo<&7O5E9r_1q6K`rke&$v;Pt zB_xV{rSBDcnL^)5$w}|&NmliEMaHJRCMEf&*U>AYSdV?LSgSvMcVeQa*D)u{FbTik zCFAS~6O;VY>zEf&9Ep65zN_1kqX%cB*O0HjFHKJLywrh<+L9p?x|G~d&8P+o8T&jwInh(*e2ojQH`l#iR$ni^ zd~%}a#4#t!sL>#U_sKHiu3iy!NoQN~_S0R;kuv;#X>#JuVXdb{H8eQJh|Wn#{@IzV zyF{_)v*j=e^W`K)1F~5rpW$1a@9(W^Oa9%bOUY9WGKf0EFkaaukHL1(Yk6C8?X4M# zIfcC<+acY$xAD;G|T5Ri0c1gTe4}do?Klre*B&pCHiJ;UxVsIMenpF zXYY~6I4WsN4m~Zita-UDS#eQMydvY`=i8Doqw+N96;Z5jemRB?X-o8+`{9S1ajAMj z`y2bTC3-@;&v<9UwnWb_<@j;YR&7bmnXHBymuVqmdcU@$=G@m<7}Qv`a&j{8@oXd` z+xLnVGUhLwoalMUVFppT<6v42-rJ8~Z%ZydyeD^%j04--lCxU6yMsir_W0%C9i0BN zw&eM}vTMb==`&~{qhnB8GWPJiIRhC)%{Znl$v3+J71iZT(f?A zIybC5GA)_AL6(;F8C~)A8MKhG{P)w62R6#sVFpnLTsJLwXp=k!EmvPMEm_&CCti^; zPokEQpyW5U#Ahka{( zf8gDz$@o3HXrv5U=wW&8)Z~A^+l67o%=b7Ov?s4sPnH_BetUA)i&-3UVY@ZCs$Z!F z8ARQ`QG2rD_jyswnXi=gys7_(M(P#wC8K)X{I$Ls22s14E4|a=Jgc=3vLRPLs2HV;Lhfhmpe6zN# z_~oF#WNf#`wB(F!^4bR(L_Mn}MOre9B_7q6}D^n|DZ`?e=CQq(e}C{YH8pu?Xr2bWB5M1+rzZCe&tuTixWm-s!z+5?6&dFI z&tyoPr$MiLl<1AnIH1w3ztMv3$Qdd3_1*AW@tR`uBq^$BZz^ud1iRvWEH$TF5XXT4KhmD7y-d<3D*3 zb^Gz{$+`A)HZo}0u&F&UV{e)-wH;ocKeR=WA@_UIgWY*Wl$mWLW)6{44z|u}`{;pJ zWJr75|GK<(&Jq$8em-aGl)glstK=R?PWqf!d#%($hV)(XT$O(Zl|hvBmGWFw%b-R2 zU72at?-dyr3}{berdg}O5)#GvykByuS27cmys9-ZO|y^j8MKfgHDBglwO)}yRJEn$ zFleccYCZ6ZjQfYTCo=c?zr7;L%*+y*Z`Cr)lV~E(nqpy@%=2v`Pp!2KTF7X;)2y(V z=jU1mHTpc+p2!TXt6unikTLLY?TO6LYBlHyQ5?H`4L*5QpC)_Y6&cm}RX1J{C9^Y` z5!RN2mg=mp2VRj;opI)9@LCZi&+6i3tyi>E*8qCp6&doZD_6c&gI*COqngY#YZfGKl%^t5j2E8Kt zlNs-u{a$$tTF9u*A9G4h22pcfdq0sk;0?FZ8|XCJ;-w2)ElQB$_h;5D0jGbf)x3mNi+B+rMnB`1R@ zc_NdRP|Ki2o^Peb*Y6b>GAEHXQma9)h>|&w%xr2Iv{c^$^uQ}Js&5@~H0TvkGJlhK zT&-8WWooj|jXk+m@{UCr)m6M44SGdXb)_(eL5s|YWzJjw9VA0mJY|kuTXK3ul&q@C zoW7PpOLgV82VRj;T^-KRpjSl6bC0ycTCZg6(>0RnoFNiac)f%cGN|phS~7@|Rdwkp zYQ3U`jBq))awEN_uINTs-0Y(Ju!rWDFhhK_Yw6Ys*0fQPq78 zISg9Jc=f^$%nq154Kj#&^YRbO_>sq$-tPmmYbvwI8(n#4E52W})dz{}KCCSVEo8j2 z;Rj|XS03YA@6JqQSBm6y->jL5?1GW>{Dm_U+0R?6A@^C{H;d)8mu4oiqe{|0Zkw6N zj;dM)=gyqNbMEWkL0ZTN&-BS4ir)Gfw2-mS<1@|9<-Br`K@_ioufdt{!vkiTZzZys z9%r&XgBCJg*vmZom_1*$SCUolHkh}dW6 zl>=q%t-0mo;x5DMpjDHXt_U|DqTD}Sw<6r3$mv+P`-*T=D^k||$clk(DaaC{y$E;mo*e+=_%4Jgify%P48Ou zlrvX_JobnRt3K7?Av1-uV^a$uOf?V{}Jc1M=4%{w0k8qEs+j1hv#b~70K_7>n6FmcuAcct0 z{3~*?54{dji1_=1*AH|nrrUBN$W`_ee{tlBFq6eq=2M?tG0-g@$=&?S6$9PR>FVJ8 z+Rk>BxHupCc924Z_Jte0DmPOTL9X_^rP1egD26M4RF=eC@c294)XdG)JYR0}q1x6C zcQ0NC_ccT)vr5saZeI~WE}f|VbRuQq{(NzQ8f9~p_Ri5jHu;Szp zoL?WlXw^XX^(B`pgYPR+h*&sSHPAi&u2n>kYwIgkg_{;R1pTP@-nwF-Z2Ks8=nZsH^(`0mKs6lU{rwkDq1%_S| zV#w9*mKuE?K@W1-BdZ3g@F1%=6L|zFMC={rE4?Y*x19)b`MGPTrFK5<`VXxd$n%$6 zT07)dUy(w@(GRa0sJ2DdDk8|mHhCR-+w^Wpmflspjry&(Uhi^O2j{+}& zcSnTI=XJK;)js$NyU&~ma= zRYYj7qaDnetsZ&iIRt0qctd`i)qn_PZYdwDYZd#4T*~`Wrd1a~it??LDij_%n2(C-9^1?V)5upr!og;NyPOHcjYk=es%F;UU{5U?^9Ir}p%j2Wn zVK-du{(K$;$1~qCT3db|M8s+|xi^ur$ffgO#rwK_rBhr*I_JI_Iww{Pvx^{w`gEGC zQ{^rq)+~wru6#T7@LYZ6mJ^}OJ)Ny}bdjVrS9|hR5fSU+%=ea%i*Kmc!8s#VAd2hKXjT!SNTi9M(-;k6id`x*44olk}Fo|${}L?yhzcBwfw`;ts+8xs65TCRf@yB zVfX0ow5OJpxXQkdr-VH1L*ppo*!0ibQ-+8(Q@YVtMvzM@^CrjTMlC5?nWs-0$0{P) zEQ@)pB3GNEkwMTmY;!akF5e@=w%2a9B%+OlXLL{&xj5SmHONoo+^6yI)!WADD;8an;u;+2ijY$0{P)(}8*Vid=21KZ77e<8On9#<7YBjlcTg`TB}n z+D)GNNDe`YcB~(06Qh9^QoKydB zUGY|tLWDB(l!MppT_VU8E6wE)T)lLzt#LTIRYb(fn7K8IR*{SAqi;D^GR5SsI&!`p zJy$g%+GtzV?JFY4#c0ADt4>-qxcQftHL}`lk>)ogc?2m$Xr~=Ug14&Hh-z zuYF^jKiaHWj|j@@SyG5-b0FtIkgLt0%pkZz$`3vKo8!!)M6~&r86BLj$i=;=?_Ivt zx(}y&aihPxM8q0a&%_?rLE?B9_(Er+SB)pRYZ`B zJ49O+RpjD4=B;Y)$279q z`#8<-Ci?i*IpaJ)RTVh+n=OS1#jZ}@E{AAyQX9GQ*>BlBaw*&Nz(aEgQiy1CM>8$w zT*A4l&AIf5TJ7MB)8>vgTtxf6B83Qf%73~&*A61c)!tCaSVf9%zWn+zdcU7xR|H*??<+V_wqxmhg5tbzgsMR2 z>j+a8xk8nr%GE-ugPQPPJxhwJK!=+bWAqgfssbHy%;xDUa;XZmtKF8Y+UvnpmF<_q zKDr9`SQ@#iCKsw6&7&jKAE>M{L8w?zg$(c^BIzJSHKeU}0{bxZQu^bc2N9t*Ko#~T zLme%~BgmzCnVpJRwVp?)W>i^yFx8o~ihHukz3oYd>Vbu-NsV?@`m&mtfjb~_87ZU? zp$cI6x}2@;1C2qo+^VUTts+HFOnF5@hssu!@^rfUK~nT)C~rnwl$0Ju_z`>8`hU+VmdX^(q^n{Dz4&T8$-Fb{&V zh|ua|)vdh_BFLqh&bl?!Z8<5b8*MfED5d`d=wOH%R3Qtyk`B!TR!RQUM^;r9LGUdng@}&Npmv1+ zn%e_I&*Gs~m7PI%5UrwmmXr`%NQe+~s6q~RV{Oaa{NoYiqO~4D--r4-Gj+0x6e2WZ zYaZ@Cb0WyaRU$SZ{9UWBo&&6tA*aAFcH|NFgHZaVm=%mJ#G)n_`p0-*xj|ZML$~7*St^u~Au;ruHFv zl^Zctb5$zn4v5K`m96_cs6$_Mu&*P8 zx|3D@iwNAA>I(?=Z3apzx10#cm!36SMJ`!q-=6R~R1H1Viw?Eat&VirDrM5B7NE4Z zd)el9$WN{ES*)ZGp&Xff7Aq-4C||}hT75f6A)?)RHx`x=`dKVHXjiYE{S+twd48z`h?9;$>CH z25tVow~7=ZLS(A4n5;)U^SbM+kOA7rI{n7$D?4uq(Wolq`DJt{=3`YZ_WJSll|?o~ z#Ip)H0vW`9$6R08*+hsZRu**((a6f;m)HiYw`T=vqYtxo){+ zWu^BwA?lzh{8!J0_qNjeE6QIFSXt?9)Ni7_RiqGc>QO5z%bG4D$hE;qD=WRf$tuBW zS;ZMSREnvrhD>?(A%zI-VziraI;6%$2gt>FUIsOPY9Tm>YnImBE4kF&@<))P5TQt9 z{#y`~MJ~oNla9tJ&f)qFgnd_tk`f~P@GU1I)N8ErmFp>sT>1{g)7wh(5kc$z$Mn@er)vBsd>14MV6X68es%>a4 zR+YA{4pKBT8$$EF&WqNJAeUAJ%YgA#h5MD2-N+0#HA4)s>!ZXLSUcz^5TR@&cW1QD zA;`t{c^#w>5$>o~IuT!U%gM!FZ*(N9=#l9rm`-UVm+kXbkwQecaa!f?r4m6d_PV!< z9*Z)mlpiIl=;3&)NFgHJ5vY`#)$J=H$W`{?7+YXuf|ANs5fSc~RJz@AI<9sa9lL!Mn_nRy}8CCpb*F0E}4?O(nfM4Wogs<2nfSw&goVw)QA!J=rD&SZ5) zq`nHde3j+$QBrGA1iIcOB4hkfio3Y49JMO!`(tZqt+$F4 zA~rcLL~(D-^%W81Vw=2GI`P)&nEGmFYE@XtqJ@-Hwu*?0_g@uO(cCVTvdG0A@m7WW zlHg55Ss|w+oRY=gwANci3K3VocUACka#j&RF1E>A#i&8Zld3F(itzw%6)8kqy~V2F z4Z^BA;KF~ki$;)()_NVJ5D~I^D$DmOBgn;GZxF$vXcZ$6jB7Ar;dPKgL?`-@bPz!< z_Mz873K0)IczqS_UN-M^89}a26en55xCo;yjH7rRq!7`G$s`>_kc&Oyb&x_t$YHC( z?T$>#i69r_OpT6Y)sJ3reI-v-Geg)HRPy%qo6#0;6)8l#`z_a3^5cc}I*1?_d&KJ? zg@{{rzdoFpK}QR#XNe#eqhH=CM!fcY%Jr31ilDXLDpH6D`+`cIe`ROPO1CHt7+h5TU#;-As~&4Wf>? zFlv!yq5ITUXMnK*uY(jKRFxuM*MJCeF~;F_=(e&|4G6a-E8S+6?co+>rTfpR9WAV$ zglAFN4f60TDm@eRZg>`zo=Nw4=oDWgR}^KM zX&jFJJcv+co5o~U2cHMIm|y1G!R$8q*YGJN+@E2Ps5^JL4e-HaGhaL9UKft)hCC zSvukAh1ofdB94D=6)8kqyzz?4>L`>Ei1s>2A!3U&S5$gFNk}z|vUtr$@9L*{s+WhvRj%+)~)n>;J5y>h&XMID+-YfggWXGz6P?n^9BU8B;`T~{E z9YoeDA{0HauY0!4%|HHmkc*yLWR19bmLo^|bA8)%bgPJH;{n6IYEAQ2k&An{mN^>d z;vz&SVxh00cE=!Pk&79dQFzo;&+0@*^%EpZCo-y;px@jZdIY(23Zv>RSwvJ3)+ov% zLg!MdHPLMc5#-WYlqyvuL~ZTR%_G&P5k)tTLN59kRuQ2af_sL_I$a$co!YC}*HuP) zcZtw$GXDh^$|9F`ukI@`spX`!=gy7ol+-?o<{$qi649P>4_n^CJc3+0SJxMiyM0AU zdqzKoRYWK!K;JMN9i+Y|v}UWwrTwRU%hp?^ z_0?w1cBLMpuQCX4RV~kwEo^@`T%3=+RUB(N<5o=>wU%Qu_G|pxk8>h)Dy}*Z-M*qM za_J;oRU{H3v_XIBS!Frc*9vq;Lv>QJR@Dt4AlUD1)SyAIM&x28%rYiZEzMnS&XG@lf?_)a&yl&s?0pj-yk zei_{=B9zOJuj)vv$fe8#)g9^f6)DOAQLT~Dts+8MBdSP}bi|ir&iEh~M}hAv&dAEW zQdOYQts+9XSNSg@u&>C)H{V;uaiB~yRZtq;Dk7A7H~jKV+fm^kKhDX;mB?GgaiCkJ zsx~#cRYd5nsw!Qr`3aJ%e2&A_Qa2h^pKNrih|rBjRc~3dRpipWPSt+tB6JJWzG|i0 znf479ML!p`@iO1Ll%*S=szNh*JBU!_f}lTR72ia1ahEZCNA#Ao=lzW=`VTsLaqVbf z@l?hrL@-w4bx;<$I(?ONkU|7wHC_i1 z%;+Gc&CDD_2NBAev@a76ZPF8@z2s_hE;ClKPulakF{~m&_qOfZcV(-{)!zEfSVf9% zliSzp*uJt3BH9``86A{GF5NP>FGYJDq_p?M$LK2}+BzfiSVb<*G2SY+L-)4rOHFNd zbVdi~sP?Y*80{cJx4!e=cA{0}qJ_R4v{zZ)_8mP+@(5ChPzHAXOMgU=i{szx;K*rn zoyX`aB9v8ZUkCR(Xcf7XlbrwFHYv&rwr_eVmR6Cg&3nxtNKvM&eKDJo%6&ydo0*%@L0RN#PcSnGa_QVrU#U=Q z+mpBqg8ighhdNqV?Ru14ZMNwUk*wld-JUx)Tzq@HRiqH1 zb8h=Wdl^BlHhU*y7421St-h)=dS4OI=JgJBWc!L-x+kvh@TB)D^%d=Hvyd7t_DH$q zMCi0%-MPq-V~zp{+U4aIx3@Gbe?J zHlKQ^BRz8>$kk?J4-v^Kz75Kzv9E3N?eSKTLWJ^i^4}9Cf?S=IGg+nE9Jg;8d$i5E zZ)`6=4^oIwmcPDc*nNUTc&^fWMXTCd35{Ip^L<4M5qx{f2y(Ue4KnQ@g^2Q-$hSe6 z{rZBT`iF0iw~7=Z+8p|fRYZ`h&A=Zb(laNe&Fmk;Dk7Bqsqd6^pE<1}mvTq-ow6>1 ztFWrW=xcA{;;QZYiWDNKyNn>0a$xmcw60a85Rq+1=;3WD2P;_@qd)jQkYz8%Y+(XaN{A#y$V?0$8^!uVS`o=!)wNPp_t z`!DEKZ~Vn=jaB=d->a^@GX9oTYb{va{Qt-s|Bg2;j6%ds=k}^MwdjVoHpIu4^{U_f zJjz}7i@j>)rEAa;P$81~k3rUb(e^XK?;qY3e`ouuL6Ga#fY|iTH6Vgvu^rYYMC|a` zUUlkUB09Dp{_PRu`g%aD`=>dG#wt=ad|a=p|1geKM0`D<@B9569i4WNYrDtyssnGB zgJ`VU;ly5gOa9Nvy=t57=dV)NU5RFx&JnhN7>iIt($0{OT{FGkxqf6%K=vYNAt}B5t{;Acw`#!T*{q4I^)^*S7 zRp0;iI93s{>vO_;_3!gp_3Y<`x&5n=>*|d{J5J9bF8f2Us$IR_@u#pRz9w>I=aSGG z#oLFg7ZEr8rC0s9MYrv4{;6k4`N*BUYKyJL(Lu!hclD|ZTeR0ftH^caeZ6Y8t#d8E z>xKR5h((dI&5QfhpBIl~6%mKOv|mlWF{gu8J?+)~>g8{VTw8AyX4kjQhnU>4Uv2-M z$hGsE`_Ji|J<*hHyM9Zce$^K*y%m}YUy-Nhh}H5k5 z5n5MlJbJ50`Ohx~Z>b&Iqv;^xCtvCp<`T*x*IB3b)7<5)BIU4U{WK3!l1Gq2#2ZfQ z7v^0e$aP;p*gWrbkn-g(_mgKqNghE85wAJDU+`pzAlHur!aOIhgOn|TXJK9#C3yrX zM9446dwoQZ>#l$>FVpLgRYz|a*H^V;-512)^gcX-6e84bCv62coTwLPrGE?b`WvQ z#ihQYc5-n~9a~`S_}T%z>ILU@d-Sq{dewD5$i2IyP~Y8$^s2*u2t;bRys%C8j+BEp z=v9B+qpPp6Cm}9;TCbYfDRLy#9lAzRmXmDKC8&J{U+f}JY^1|vFe{*)~k;Dw@BIU z<-HU&VEa-#!VmwJ5b?fO_EP*IAsQWLe4>~9=v$8tF|yXb9!&=kuQ|3?J+I2OLy;}p z11REUyNS_s5TThNzhj{+a%o<%9g=T3DVncrPen-y5q|jIB|>xM53=z@$|4t|kzU7B zR}N#7n$Im}NiWJHNFhQiL*6PP$iFMeD&HnZ%Kgq zT`IXAezaG8^mRD|DTl2cB3|1>E~gu2c^yRD@z{R#%Gc*~P!_rNUbkPpXxkiul&hW; zeBABEv5E-!J{DUZZgYy<5;x-2>D&#ZPDd+kc&}~22n>`i0KC}+~Otg zJ7*}D?JMgbV#R+9+f+u7>s{ZR(^ttVQdXWddlwu)TkGf3@gY~*N% zWMi;nRcZ&h_B^7O#&!qMnih8xHa>{hm{{T{bpUEH!lTnM%Qj$lILWE9b^ATYp$feU>i$HrFr05jcqS=(>5u^~I6YPBSoCtC; zg5-5DBBb08%VD4-k06DJTSMHZ<1GcK-=C99xh*?AIrmqai>E?Y%};!H1j zTuujNkxQ{G%P2@XYPEyeO^Pj^{=?|wQc^;MAHL;8tn-X8=bxAJkSU8?Jo8RE8moAQ zz4E!e>UG~3$0{NeRekJP^I1hMo{T4}IxRnBk6t=`o!P5b*blPhiVoWeuV&HSPIIhk zTF%H;|F~Xt^&7@%IT6Y)v7EAUJIKXcql9RzV&;)@W-QLY_9aC4;olM>l%tc6Wl$Em zv~qUhC5=_2XsxxF3?-GVB0_6>KJG(VDxgH z5g*?#!~=S{m6NdyMb%C^YaAU!D57`n*K;~(6}cGK@a9j7thtbRmzmy`5jS~ z{Lg>fZ5*qJkSF`2x6Wr3xp;P;T3&1Ac3qtRh8mA3GnUq_R~+DCU&Ul%*_k zaTk_!G*)p>rihpMqLh>n;fL=lA{1fEXZ%tYxtJM|bTn4+{*YFKZQHYdN=k^1RYYhd z+H&5^0dmnB^g2i(LTjQ~SVoYGIS3v>PxvSMgfsI4$MM36*nYZS{p6sWPtA6a>j(Sx zt9KldL+~7M*-_yf=ag}*BI2;4`-SWsT1Bp%KGjdzLcXs^S@+nGfqnWoRuM7V3+FBE zy#TL+R*`G^xPBqRvi1r6bdO=_XZz`X10|Jp5OK|k;be4fY^l~5RHga@cZ@p|)wciA zR`GXvH?cJAGc5KNzeExd`ew%Nx6aw)G&)qB{@3S5O04o9e^Zig2Ps6Tg8e5yo6`}$ z?|Wyn!_rs9-u;&Ndz)ZwKAuPl5&yf}^6HH3BHCL;MEurod#r#^01w)YdIhv2<5-dD?>Z3kubEGb09YN+v?+gn8hxp=Og z5Vdr~+K*9ItQ;ACQ&K{NAKoe=V!giktRmOx+g)C*-fcdFDtX>^dwkBSpLp7hxo1v_ zDlR_#y2v$JJBUz)$9FEDqodPua;c)^`+qzK(e!St>K9w6`hZ(q5r6Yt@V!e45vn`5 z!ywmJM375W3m>{@4x+J&6jeh!==^c4A|h5|oTH=ZD_TV^-rDpn*UV}A8qLAB-{GFx zBS<0QxZ}f3n|I~*I9dTLb6M+$?awHwtb+)xUA8y$2vW2T+Iih*I*8D^YbSqR2W62< z>-_Nt=2}jQGK7vjIC4=^SqBlyNILvqaylrBT*`p@z+pKADaxRlIAR>Dh)^!pvp$j2 zL958c_?&OKe5xg%jk4sC{pzG~tRh0*;M-1_&nj{$s(Qs2fT*KwAv+EY?R!u?5x+Qi#y5FP|Gl1i8vl97c+? z$F+PfN-A4Lg!a+-tT4(Vm-gx|D=hWZdtV#!3)(oYA_W!^rlhh}L@4H9vE?#?T#9SB z@sX@jB*n(FA}lsDjAj)PisVZRZdo{`;*T&oR3cEY*`=0mVev&)d zj)e%FG+3Kb%fkH_NNZ%_2qLT7JX&@i!%vts-K>-z~2mzs?*Topz9magC&-v5HX+ePi_2>*8}B z%_<`F4accVb8iV{k&AH@Zxv$}`a1jhuZVj$oNRPZ7P)v^-s|8lHCC067OFn|2EWVgYq_`78fAV7b$cD85TW(a zZqAkwRur`6aJDbsVdRP;SfT zPMgmva&_)z)X}zjmj1a;lI>EG^Fm;!8 z5TRSG`Q0{Uk&8Qkq@&iT#w`KUO5j23|;Vyy-G4qk5d zp)7JSitDXnj5U6bWgM%B(6?X?*ls?n$fa-0T)o8{MDt#8HHcpfiCmnIQ#-l0yDl)sKee6*@7l$$pp0V`5&AaE;FkHUA{SR`Gb+|j;JU4E z;=FUkI93s%uj@?wW7 zL}=Z$9N4l|J4~Srq{sg}S zew5xAi?P$giM;Xe_$FGEoCv*@7T+%;$fY;mP6rx9Z8}KN=rTWgG^>bE+|PXYvQ^|_ zY&ju^4~ye$=x!uD46|(giX8dG-3cRJ*so zw>)q65R%WZ3iht?EAJW zDt+m?i+IM$D+VWi`ac`Wme*V{(6^(-wa2yR4)X0FWiG-j^z9%*zm4nmw;f*4LCRc& zp}h_w^xL>zbKdR+9i+@f7~1O~Lcfjc;2U=t{Q0?vx7QI@&yqsKL4V$5aO(y@2oO;9 z><^EeOqC-yIBGIQf%(Qn>4x~znUg7N>g=;8Q~p&JA<*pmj-5^wl%M;V>2xQGZ%oqB zAYQQJOsZhC=UZk*sD*UVr)Sbl&-3~-?#`&qm0rgI&zw&6c23@KI#tifT2-qZQ~OS) z+7kDL-&W7C+;UQg`1%JY3-uF;AlD%unoRW?Q#%@~NZIGZld1M2B_%}oQO}Y>#L|z1 z=bx>bO$52l+;1|~woE!2tJu5j(KkPNx=@Xs&)~d`r^l?&M8vf(o9=`G|q{^dvpE8r40o&(w{M)jb zbYE#%_-(h3%7}FWmu2ale(Frhfh!|OA>s)EJ^vkEa=q_MGwIH-ZwD!#`Ql89o>I3* zkV3>6UzkZX@5%^ro%H#cR58yZIOCjf>`bb-C*AqkhZG{-{Hd8WRQNMlL2JJfUun*kq3|JGXT)Hv8u=Qrn6b9Yh^*_3X)inM$=L5BS4Ws$r=a z!>si>NFie98>UkI%m$Hkkg~;Zr&29WN-FCh;+fY>6>5J{mg8DJm1>Q8t9CeUD%C~V z@93#iFNKoIRuS>_f1OG-Q_Bc){pq7qDSyTzG&@^WN6qb4)lpWN|Bw*1R)a%lCezID z_3+!O8D$Xx>emk<9th}s^(4w77gtno6<3zq_n%C&9VI11$0{QBIB>F1*O{`&b?ef} zR7X1LsMT^(PC9rp)ug7Rgy>jB#QP7KEY!fJEOIS5bTZZ4PC6Q^NZI+Y$yC3ak`kh0 z6%il(*U3~nyo?|h{l|o;)mJy1HI;63|Mow^sBP;2j%F1R*PlC;?r?h@r2PJZskB;- zrh|yvXQv9QGi5ohFf&+fOK;TzP&AiV4NFQ&i15RYC?Yg>b?RAm5aiNaX|*lA4tf)t z?lPTvRC9Z0W@vPfLWEWZdzxMc5#-XklE2+g%5x{D$E+Yg#M&R8?p7)2w1ZrKUOHW< zgh0yRW7A_+TOi`|pO`LGb!1{lA_r zRDrtUvwHE3jyh@z z--?HKr@7rKra3gsiZiuYLWI_meC;;MBA3=Ks}<+1B1P+^_xh zw^34Tfwkk_O{dZuPOf!dF;#fmNg?7jTTZ1KYu+k(NLF=S2wPv}ExG#bsU4&cp_rVV zxA}I^yH^az@`#A`2vUepoXIM5l@a7pEUUAl4{Zrw$m>m}TZX^-^klkyNZn-}L@fWz zWH(c`(Lu@$Cr+kp+|hIp@x)Um3pXbzi(K#dVrco?+nJ43q+Gfz_|feRP)aJdoQNe~ znJnBgr7UtWCYy9LR%uMwmy?w680f)pa;Wz7HeI&v|V>2=VLQryqJI7HnZ zK?)IyEn2j`j3Afdm-Zb^k03=cQj2&{w?~jd#QOr;qAg_vxfsRu2*z~9WtE?(+apLJ zLi+MmpokzBBgI|^eNMe4R`rsSJc1M=^oCl+%`$>ql4L$n7hx14G*9zhBbnk(~lwuvAYW6NI0 zY2TPmUf2`PoKD^uC3yrXL>#zmdW5@u>)<{^yKSp(P2C^XI!Xy5aq?y?Rdbeg#{>yafLltnI`nA%sn8s<7e z>e+u-Fr8|fUH{-zs*FZSt@sFRPI#>siIBIHuV6-5&X+e{VaM9LypXFu2~s%N<$RJ`3P zT1%4c4ZBtug$Tv^^A)Wri(EQG$ggUo=xoB)S4t{dMTE{pY;7+i$ffg^d_`+gbdFTKV){#2pPHtkis%=XKg6Ro;j(oyRPGM@0d{ijkrWJ*ei@WVfIB6J#_ zud_{Aq8-fZf?}MX*p$)OEHK1R~KG&=uC>ruXFfJiq=z7 zx#dJ$c0`DNWmjiLJY<#Csge%cmvnU)g$VV6RkkW4$faJ-SF>UyWqCD|YRHUc6%lXW zVP=GyHRQT{o0(K2$G780Pn=2jcHi{4Ah-Pr+h|r1ars&^Bh>dHmqwJ;6Y^GRoLg0C z%?wt}Xf&&c&`e~Nl)MhgA{Wn~J%U+`C!RN*c8!$e5u^~Ic{l$>HX_KyxR%$!ox9cy z+hJ?%vUl1eNFhS2PrmLs5#(ZQpwUrBP2s#2IfC+*ti~24m8~K|o?5=f7G;r(r?TEE zp1$7pf~oXoP*T|{BIJwOTUkbsOP;K~)7~mlwtvc0s=z*)RYcV5OjTCby=)b^ezW$J zAA@zo)w9|gST!r{Cajv(XjT!S-Alfj6=ji2yQO@+KApZ=4Jw`2T5Ur~%6nv_5TU)A zRY^>J6Jc1M= zvc zPBpncx7kd(!|ko&9zeTo^Ik@?iU{rQ^WR;fEOK#|(O6YS+u~YlI}@#?RSdxTijq8n6e1LV$a{T6kc)RV z8Xa}i6!$nbm*`Z+a(vY5^UrWZ=oHAZg?u|Gi(K4UCPc8Tp5^Z6^lK+m{awBb2@!ty z{Ra^TUN>2&@k?3c+GFKps@&ss(64>k(`HgF9QL|LkV3?JpFNZ6@RSkc`nL^dy7hgU zmXq?e7tW-*LDXH=LB!dc&J)1sp8q}$?*q#hwHZf}^6wQXM92@e*{<6TlSMB2 zs2)N8L+!AtGt})7q!6Kgo4?8px%8Z@s*cw|ik`VutEX;{AcY9MWAoQfBp2hpjgC5M zs%Pnc+OwW*9C*P(xy?Dkc#te)lQ-1C1X)7#F`+|^+-H4)O$ zshAa@el8)GEVSxbNk?N9DQdY@$r{ZnBGl{oy)$KzD;vdW+QHE&FWjo5>+P`~N$qG5 zq!1ymKVQY12y$_!-{`2Lrf}06&sNXFMiJkh$ZH5vh|s%WBe;wpm*#n^A=Myi(?N>X z533bL-DMp_Xzj9Z5|k0->UiNTte!n(!|B1!ZT_CB_1J8;^qJEGm!+2wq!96zCr=OT zM8I1`u0JiE9$1cO$Q>QH4AVN|>e>4)oE{wh!&o~ucou`}eh`0ClGhPV*#~wm|AV`x z26k71)qy->#ko@h`(pNg|Hst8Zu*uHq!1A@2L}0DzeJGhw$rBu`Rs(-J~}n99FMK{ zpBk8FNl8sR>ew3p1Vd)#z;ZdueMJfpzkFGEg1?A8?fZ%da)q0S19wL;Sw+fP+e{Da zULqxxbr2CUS_k>OSIQ#S3wE3yxNKZ03YLYlqVV=v=1IsZ9$0>fB<1HTQiyo=Hvd9(gf2Aku z5u(_*6T)UxNwOK$BS;}a7Ut(&BFLp4$AdXT?OPFdu7&S$0vc6;4h72*j4yICC0{s+&#sIb?@SLY zzks@9o2})f5b@dbrU%csV7?WHT=WclJD$99dSG9*de-l!2bPOL-QKF59yc?vi0K7S zoEcbEc_BKhL9LnLg%jcoEFviL^DYr5Z8kH=?`tWGTw!lG=zLqYu}aU` zvOM*cSS}|e`IeJHgx=76E+-M>;)a8xZb_$w^K>3=KRk1XsjZIh*0Y%waM!sf?SVYKGp53Mh7W(%uc0e zKuKjCM1(38>G_ut&eBk0kGJmlnAj%F1R&pmM}`KVq8Wsz&)aZ`m+tG+VNTWDL6 z)eCu_niL|`3mu<2m>0e>1|52WwjUt6+YYmY2)!}+{Rd@{i+)$5qmG*DS?&Qe=Iw5u zMuF+}R*^!4<`t_0K}jy$(`{_{P6ZrS-9lAQyMx4Wf>?a6${h^YPseT%P`aYVKNrDF?rGD)lZUm30siPRY}ADkI2s{=%t2J`+%$g+*xPm)P!C zR@wOP>M#ls@`-HcT}F^gF;d&Ndju(pz1oUAnpH%|hcj=$>!2)hb$p*X;_6w(rayoA z^uTIM=uNa9@j6H$LSxYCT9gswD*FuF%WAf>^SaT-2N9YR?X0kCmCfPgI&Q<6LH-PK z_hmB!yWJb=ung=D?`T#Laaus<@9Pve4?(P|_M&4#c|Qx7Elg zBgl0c*2jcstUC7HGlN4B#$74h}-X&^!vdU=1=>`^{qcocK3q;RnL<0 zu|G_v`JB4TRuQq+|4gQ}q>La}$TjJn0yI|r{>sU8HWBhY(%QFXt88Y_e!#LUv`?_? z3vZQXGCLpA3}@#?(qWQ3f)pY&qvoR@@)ImaR9=Qa13lsrAFq(ez%9Yn}y%vaK(EOOD;PC9D6yNslSx|0K|-%Uvg5q|g? zj)<>(VP=r87fxB^I`Q)}1FKJ-bTn3x@_|!k1_$0S&YVcZ(@&lm+_7T5nVMYRIB90k zspQ#Mb>Dt71FJ3Y%@2hYr>#uD_N8`&AHJ`M*kNX7U{w*y2y$`l@(7Nor#^EkMeD-d z#T57R2##k(4eZ2~l01SGA{4Q()7~y6*~u3 zKke+}!+v?I;iYnoI0EKa}JV zq!6LG%W8+05#*xBnGm(cIp>T}y>)Qsug00Lh&bScnZdtYGv9ngE< zXgei&1Sv%PzZXmu_69_dOY4X2MZ6CBoJ+5pOnEDm8mBl@a99D9E3NlcEu2 z5p(J;>mWiS*rN4i1i83F@(9K(l;dTY{hXCNf)pYYfy`(26G1M;FWvpxR#81misGVn zZg)^o}tmwNa6 z-_9Yp-f14RXtw5(`8@z4G^6ID=afY*M%#SL>D|k7vajo~eI7vy5%SKg{&E>XF76o; zqSo_ZFY4Vr_}^oXQg=dxAN~mvp|N7sPs#{#aTo3pjB@Yz3vgD5V7q~CcEcR zO*@Dn7rhU!L(xGyGgUm%PG2P{?`4oegyNT$lk9a6L9Viw!C6M*!z!$d_5_L0c+B6H zr7Uul=c~nsPNrST`wp2*`x^GTZ#n%o#YOFGf<599q!6JvZ2qL02y*esdL5x&bUKj^ z^`g_+cDWt&SpW5Vlj)4Mj39-G{l7h#&WF8KM3C!UnU7y_JfC&;WICy&q_PeoLRFn~N?JycOXJ+mSUrN{ zJbV`-MTbVSiio!!Hd%;9Q5Lzlu6V1s%6#tN$rO8|B#$74i13w%6!$A5$i-R7BRE^C zcWqxI-Sf}8MCfVSp33W>EOP1X$)8{mq4mm6;^h0-37ywLio7M;HBypCkV1sKGuyG2 z5#*9LnBNJLqIi<+u&KMOg9t^YY zTw2@nrx~QkXRs3!N-A4Lgk}aidnqHxrFkX4Kj*W({=~_&+ZxR(B9?zt?SxO3*a|S3RYc5uXV<|2$LFF^ltr#BzrX9?-}^ZP*9q+=EKh>^d^<=XLc15s z(I_Lx#ns0n*o*Q2EaJiTc?2m$$m6hROBq2fJ|~YLMY|U}yJe4f1Sv%5-Ob1Ph#;5V z{QUl0Bh_M)8s`?}q@=P{L}-r5pHx#8xwy0RI%u!PxkV-@$syBh|B)aOI0J@ z`KN$rYp-+=QAHb7Gg(C3^p{?`ciA#WRZVRXLy&8~JA3J7Xk?AJdbWP3mu^=)@@OyJ z$56{nlGi~B5g%K-U%3B41i9{bY(L#MNjhrn)!y`;e#-vS^RPQuo#)ZQLZ%a*6cKu| z`8!xdkn10I>!*B3Z`EGw_S0Qm*>3e@C@CSr4{sF_>V-~4pjrr0)Vp?%el#6K=*i~q z(Nh+=9(}M^xJOUQk@xk|J!(oSx15Ok?&_sG++_s0^zP>G(UYRL-R{wkW)%?{UHNm%o9`YJ%{SyFC3clZpbyR3tV zJAIl>RGNCXWcMFP%(Y|Sw#vFnw9c*`-mVHS39p`a>stkt$gd7`zf=6 z+x8%K|R%hUU5_e)70K?)H+4rtqX zdju)3{z^Z^DJjV#NFicbKm^UEj3Cz&zSt{N zRpA`I`!z$aU%K=26)6t!tDdi`)|wX2&rR0t;NmW$VXh-2d=C=7v;0!K!>#wqB(>rr zu13M1;$TnvD+$hFma{ej(G>*}!gN8`ipm~6dOzmR{$@x09${dC?k+V~)1 zX+Y;Akd#HPWdUIkPu~twz8APGMoLK@K?)HU1a$uPG7;nwx^se6Yn(6rY(GWP9&80WXlM0-FSTP;TO(%O9IsAEuH~7P+)f$mh3{a_B@q<@=eNH-R>0or=`=1X!ZhNb3G^>a>>68#b_({&ope%Cjb#jQETsQ~O zSVeDJ@0I0TQBrD0_~Bblgx+bZgi%J2YuQo#lv9=vwRC6{SuGrmL91+&wW>kzEJYD7 zt67sp1ZBP*L@3&p&$_29a%oPqEPvk)#&@(DST#IKN{EhCL}(?liZ^8hxfnG{h*~XY z4C%bh`l&wLXjT!Sl`~&`kFv=E1p9KU-n#i=F9b{QT)3K56w(Mu8UGJ;$)d-V!_ ziJphWdhR-;m+Fa7cUcD!n;h0l6${D;a_xCUuiz)rgVgx2N(g-4ybe-`(Adn^NFjn; zjDD4M9JyccUVCwEzvh526Mbq<9In=gVnj%hDa($g?y?RdmIib_1DmqQ#Z^1$XsqIz zsuKa*FH=%NgdcupAVQ}O`5b!6BA2`js~VDYG*-#?vHSV*ob0weB_%}1Dk6URrGC1h zQbv%Arw$2GtL0qjH~L)I*R;3PN3)8EYflVMFq;oYS>$5W&|AeQ;3J>zr`$nGN{H~o zKS3g9j_VgPOeu?8j4dV|wRA9Us4-zx{U|9RI#v;((PiInC?m-Aybt%gH`W`g=O{~X347X!$;M$ z;+Ws2waYv}kKii2-OGFFc0bogk06DJm%gl*Zda8NyF;y30C< z*nX3+CeB+?j$HIOypFFwHpD5P6W0l@ucKK-1gp(^9h5~b)vC@{LZEN(fqi-@`aykN z9pfqUUueVs6zb6>ql_KI--eQixEjKYyY~1i2W$ zZgkX9Q$5SGM(qd8Ld{C{_IRsEAwoNgeBCG_$iFyGBmvs>F-NS>Y|1Y@_MOoxJ|A-J_IXH(P<(!ZA)4eN7DqBT_Vlw%AUX(>H z#xj$RS~CNEXGQL;ejX(yMEEhsF9AZ4!cHx{4uV|t&XbN>Iv90Q%dH;vXjT!SUbhO` zNk_*ja_MR2t7em;x5TPuk7gASdPDP7vnh*QJWEgQX!?p?s>Zo}S&WhrqSJCBG{8pF)e1@j4D2rT-GkG11@7(iu>6KjU?s$l4U#e(z(+H6TNh5RYs6P#J3iOJ=H0>6^96NEqr6Ia1uv< zuyKO&hO6R@uBnF22l01SGBD62DFZh%ZMfoWHgni6ECwl zc?2m$$k(zwhcbd(ntk&7D4rHywNWqSP*ZnV2NBPCUhsX}Z0s_ETnBB?E1b&Eua(zt z_k3j4{4+HX+D+suY)}@tv=^}(O1>T1FIhxL`zVVoQIbcHLWK5howL*c^>I;haqaRt zn3<&$EV~KMna?9gAwnl+b_>6ZAeT<)><+(2kfO|fy9rO-9zhBbIv=!K_+hzp+b&)E)g9kyOLLzN!jtKA;QvTv{908 z2Ps6n_$gtwn_DBk(Ln^cxG(WKIA`1(<`2tBl%)CRDX5O7__-hBh_Y6w$EEd3K1If`I(vsa%o=4uLh)O?y?n!lFC*Q zA#Wg`RZLmrlE;zHTqZ?cjeR$TlFC*Qp|fqvvo0gZrIYde`G-#O?Yu^L09LhbG^>bE z4nn@p9c7V={)%r0$ARWud)q0=BS;}a^L&1-C4yXhr@apLqOudL4j?6Y1Sv!)Ga`TH zMFhF{oScq@3l>~4Sg_#s>a=CAI8VRjC+N40=J@}In0n!>A6`-o*1xUcdeS3Hs@=3FuYpMYKoU_=~DKVasUA@}F1L;;rNF1q)VBws{y%ki z$`J9TJ*sN6{c{NR;?9pQsb-!u&hsGRy8*rW@ww;0CrGY40^(zj$stHtYrz{U#oqKT zQPQy8bKepoB>3s}MtA4e17&$ja%C-McRomZUT%+h9;@-8Y>U*~tnmYId z$u$uWn}jp3G{e;qRWCf`!;7mA{w=<*n|*X~we+vUcUS&yLbPh)3}+M~es{#;YU;t< z_|Us+2yz{B%;IXt2Y^Uck@EZ#7FW-_e;libIPVLKt6$y=9ocq}YwI%>SBKvNM6&81 z7A&cL@ViL){^OQZ2do;$Dk9$TlqJ=3S3*bDDsruS){^SH|C)mcmemVizwMIhqW|n# z^1|0FsSds>haiQ|=I)m-sW!ZJ4kB1qFZ|WLORCq0FTLq)_|pALsui0L_2t(n&R0Zi z^Wc)|keBCl5J9f<{<@@k(o1s)Qr7Ye+4Bx-0sxI3sTJnzTt7_+0+H5e& z>_fy;eiM9#%?&+liC+!KMGt4lulI^G@-d&Ps`K7ya!0X>h-DwIswal;hIi*H&WYsW zobRpT9KON6RrSBqCU+F8h*FQHN28ct+5X_hh8Q5T2__o>S6Ls~uWjH~2qsbn2;n_U!n3tpy9x zx{?s#N4<~~BJ}+4`DSjM%cr(klxTU&cVC`EkV1sKk*y8YyqSVNtbr z`&;AR)<@qCQi#~{mPOS!T6BY`Bd%V^bz;|zmQ>qc7xht6SqBmS@#3)B{VLZE$|6^1 z{YX~bdg_wusi#F*Tc5F{I_!%Ds}h1-A3keI^Qax|i9D*c6olB~!${|R(a`lqx&h5vsiin%;UQ&H&o16|>MK1ahzU4FPRMktajkDzy zkE^QB{KjU^%)A@klzO3NPJ5R$@7g=Y@#uAsLWJi0)4!5y2NC4rtdw*#R&kcm8uixy z8|6|`LWCcFz9K^Fq@N9!vhhZo5`XKRo`0q$ z;)p93Rj0P-&_(HwpRdTpRU)y6AN4}c`Y*V1QMGcc^@E7@9#~Y}&-J5Kc=lma!2bcB7V2t;?$KmpZUZ7&MalSI@o`I)<~xuAcixAd*$2?74bzweHR1SVhD(4=%3$^n2*YT176#9~$Ol z6=MLet(GK@b+j=^#D+VC7(mv0r7Uv&Y1bvy)<2uC9UuJUlH_@BbKH{T!~1s7^V;(J zODcUa!gloY&$~qY&kvVW*M~dqc^}SXk!$9nCDnpDhj`0>FR30o6)8Wyeo1x9UPG(Q zQ)?eyY|g^ZK7Ud5=k|2r#Ah$6ZrxxFo=m{_y&4f`J!Mh#!1D|})KV`b<%-8Gs-E)# zLeyXYL>=2wFFfNP zS5JItt9Y)aID?(a{C%C(69?{S=WT&IAxI(O)TgYTc*E{ML=|Ct9Dc>>iKW}!(b)U& z_g7C$ZWezlE^a4uEpwx%UU<#URhmC^Hf1|HdV?N8E}c!;t}%-UQ17=Bp(u{+Sd)$p zf?V9mBt$J8^qqAov%^p0>@%8GMCe2)KT}f{xjM0oV3GdR3+dzRyF*xC$MTSgnAmY~ z^0lM%__z0D$n~tnP=F0Nf=9YlQp1B>>-~I5wfSN5Jr8od=87fNHizU8ZVmt4 zl4|ySL#y)Zxms?NM}XMqrX|&h@6PEUf?SLm`Ia+wuhRuPAEYFYAcY8>Q`kvk89^?c zh3tI;=Jpm=FVxwM#bgw@x%|q=Mcri`L?{OI;mdP6D2rT*Gr1KfSw)IsS+c%fcuJ6CJs{Zz`4) z3?g=XaaFBUA2j6G?8BA*1y8E*)JCz2h|Ql+r8mawpjG5z4A3JO5!~gm zRdwHgTWd$Liip2GrYgMcw2E9jyZ2Vn|MiQR9_ z`97q4a`)8}w_Z1n4kD_zte%*M+#eR|9f! zeH`-ZP2@VEQy|*|j%F1R@)?Hv&!HA?6}jYb$iagBB8=fu?HAKXaLWEAO zT_#|VuRqZ$JEnU3la* z1n2ONyn6M-=4VE^obx?`6e3o=Wc9=+KMSi`SiO)4avi(T>WN<*IR{azcWJwF8!S7v z90MSQ21ZAYVnYv0-rte$xNo@-bK8w7m@ zB3}Ia)f4;d9MRDpvx*3EF)HE_jFJ4u86m2&{y0_wNnJ-zi<`a{q z9lLyZQOb_g%3y2z@((QXIgnD+AV?u%*WDMT(=0LDpL*dY$1F;Dp}+pjqA@c}i8$d4 zi&Czt$=AQ$TOt=dwFXf)e+L9lElzvak`z^An|=bI-N|$Xi7&#)BH^ zwdr7V=&egb)=PVmPu-E%5Tp=s=%GsrnFBMPpxZ}QHW6F zCBI9hEON=~8}5pSTIz+Q$WOF=G$jrB?Rl70M943_Hq^}Q#=wQczYeBt>Ul=7mvr*(xG*#y0c-hZygRl1pcL!+Rk_ zkn*vRrDeB7Mze|t<$L9Cj8GQ2lwX$5%v3DY^8Xc8wR-`SG;GD(_lgKbaq~H;8mV@7 zL1W%-L5!w@2+bw=n--KsF3m*w+Z;O8wc9{C6SjLrlvHjx5ju|^-eVeK>V@2AC|A)= z_(sz~gtFs@r;F4#d%e*V;w6rHl$X(%NPHOzhHMCiPJcoI9r`0+t5yyEstWTi5#(aL)ay9t zr*}>K@t4s{J>iV-`;z!uk-L0m{~2rjZQ`KwqyPE&*}qOKJ172DoUwC5p;lipBBb+L z%lM?H=B*-y2%QgG?r0f7E}chrP8u4kn8l_OaLf3m?y?Rdbe8V!m<0Ly<9%v!X?&Q^ z7dd{Rq2Fxv^kLootRm^SJDxF_&JeWI+e3* zv4m)>B1J2i#lJ_hiU_TV`3OB_k&AJZWL47+&dAE%8t+yk5y}v=`;lIU#)qvF0B;nYnopT$i+B{ZwI3)gI6s|nX7D{N036qy{}y~LZ&RawtB;& zl$q;wkkWh8qLk%J-5x;-5jXF)XoRd{a+TvgoZ&urz}*vPZ`7Ubyj7fih8*U@{{eG|9c9%X&|=6fejyD|ROId}fHEGa~s^WA$V zo_<}{xJ0Eb-s;KbBPb5?@@ z4R_o@S?A*qPHcQ!#%4!Gjam9tuPqHPs70dBLL-D;|v$ zu5g^ey;YwGm5u^}t z@Uag}>^qg~DsrOGDuw70EWs&RNJ@22mdW#(5$SeOgvDFokvVQ+>6FUszZ}y?LiWDNAxA<=p z4_%bgK?J!NP4GGxTX^K-f1Nn&o8wqT#Qod-b>gXK&Sw?5I6r!;IQy)3(4!N3XJ#Ug z;HV|y%zt`xV%Z}(tB4@i8*hGO;*$^O5Trcj%a2T~|4`(jq_R~+tbE5K6O(_->7Xof z{q~8EOuYZW9DVxyG&g7y`8JNhY<=>4&x2fi7ra%R zE&o4d=K*F{Rj&V`Nl^p@Q9>1LSCHPkl*8G{fauH|L@X#(N)V;WMfiiV1)KquE}GD! zT;X~zNJom4vq>^ZD2C#_Nd`25BoIiWg&-yW_3pLT+P^htZRVVL9>aP3-gkZ9T6^tp zeXD(Yug@>7FZjHzExl{VpoEP57QIz}V#8QlkwMfJe|f8Z{Ms=FB~#M3>QArSiB)9$ z``6#9zq>5fR?-%`y1Jgrv?r*$qz4xNf5@Ojb1KKs{n2a5 zuqsR}%GG||C3H8##CCg=1=y+zGRl#m`-<+4Sv7+w-HCkfQ?VUI-N$%&1uN0bjbBC2W%oRE_qluq?d*S$q1+kq=fRx@qI3&8s7Gq+pVrul zHFJA)^YzOfGx8H8L-%06w_fadus0B;yS9J)1Q^~b-3@+yO)JqY8I9=kAqZ zRY9}$aF!6I`#*D50wcAQwD!9v7T2d8`b553JJ?)Y-@4EKrpL&!p1~)j`_(I~Q`TeT zUP6ZMaG$gmtg=QKU&2--ihZE~TjOf1q3%k3zNjtRFl10dhVEnie8noOTw=g8HfMxtoqm$9J}ObpR+8Bs3jN9s$bg` zW6%RAKZEm$^cbZEtus(UhVnr;?^?|uN~gTzt*aAFu2<`%lY2j?yIKbsI$7o3&1wcw z+9kVk)z~wqMEh#@ujWwgcxp1Vj~Doo!nCk;iPFAd++R_mb8F5gw%1l*Xp(+ zN^7LSTbo*5TBEmAoGFzqmf;Im-^~L2pTvnWxo%I28O; z>eBg(>q~zNhPR3%z{7Vfu3xo#CsvX1^BWh}M{NlmQLBhr<@by0H*FAO{N(UO^^Ize z=TGpWy7|40{Y?*E6rcYm90$qx)n<$8V>Y$yY6elKe6X;7;aV}q*Y|t1zHrOOa;py5 z_|^K+o7&&iRvow3y!zO0SdV7-Lix14LuRJE^+t>d07wXGaw~{U2 zm|eeqRfj4^iLmAzKgjsv&9m!Quj1GxCmeBz;%HamHGXg`I%}U<^)LEysU54x_+a@z z>&ur<$y+_t!Fq(K&F+pfI2P$9l8YJI^P*(v=2H+~6{dy0ohXiWVLj+ywS(p2tM;rS zL%U}|OdYj~DDBF{<9154+vny6?O8>Jb{d1ZT58P-+lnaW90}WsepmUHR`>I>(&vW^ zjs|2X*VFRlFGc%RSyqT@CjuZtS*J=1OS{6F6UBLAj!!Kst~FORb-4Ae8G&)F&(rjM71;GkTEfRp?=lJVmdhD5XCV#Z{5_kaatSNY;paX zJM6RlV70~dN!NAK6UkVzcu{@zOJco*eU~V{1>rMijI;aDLK7_)7Gm8mWUzI~c;lXh z^(&UxIH;OI)VNC))(@H;V^Ff=j~3Ry_Vh^c6|ExU;n550KX^2zgAAhf+Hhh0Pj`*P z=v{X~efz^K>g5+-txwxO#<*~$m+Ci+wf*O#kIt(vIkc1BPR5F7&#Mo7H`Wtb526@x zhR=g<=f$_osXw$;CsvWM$`9w%Z~nqaRuQ%FXXn)CZXRP$GVdQR)_=4~CsvX1!|^ZH z_Z$_|!L}lbUMX*@)V5e!$X zy*;(Ov_=N?9rWe-^=!1}*0(+1N+va5svnhB=qOA}Yec<$_Dl8We>xI_mXLA5 z=$GnOoe;B%45H{O!?xlK`;7Uo)IYUuCr^-!hwpl&zRId2Jwc*&J?oYF*6+sZ!KZfN z|9iPU{>o0;ij2$mc)33M(wGjm6;XV$VO!DOe|+_o`X!4yv5Jh_*LkJB!<><O4_xN0TD#@Me z&0E)?^_SMZxmLs9^LT#!hQC*`!<`3)3`;PKe?HdZ&*l$K=<)Z*q$9p#vfcX)pZo7) zA2fWv9qEWOSk~_k>hX65fAyUnfBVo@z?zlTw*E<Euu6pLoE#?{Cm>JGyQa#<9#v_{E7gL3&YSW>kPGP=6GC;_A-q4muM)tv#ZB{m4qdg z^_FL#gpBN4fJ9{N*1=fgQ;qM`$awe zM*q^wd#dw58nph*dI>zvx##xzTWxyN0>5hshCvA#n_kf8Z`2jw{68Uss5>t2^SL2B zBb8NO*{$Kfl|p@=*|FjC_lFEh$arn5Mn%s4r`~M%GtIwS+VI&y#E$!RO32uCS)=tn zr!mjg+?7Sto$oX%&d^h`(z^|RQ;WKN&0BPkvF&?}ihHRniztqEp$__2`FZyiv80ee z2^kto-1n-g8ANHkihBlnvax^a>F_on8GpOJr{cys-)Ew}b5&2pt$0fG4t93)o(#Rs z?hb$0RQnd~;&i`>w>n+n%y`6sPPUE9omge?! zkRhM%qMB-}h|*XR&xR;_-FH`HiSufs#`gHHxr90>A!G3H9{(*E&&X?j#?)T_J%+=c z>GfZ8Aloy{4?9LVOUT&wxnBPziE0K>JIw73zfd7sntLBeKD(&bfBB+4tH@aA?Oy-Y zjcTiiTCuCI;%grC`a56K=d%FGW4IOxbx=aa9XIw>WCS9Es7-F|YrTDzx7A_O`+PPd zviF?U=kMTot8xYy| z7rvwA(bUAe)!UYMwnmJGbkbB`uUBD{G!w`bxFhLNhRCZvzT22qT=LIy{A&2im#Cfl=$3`H_7yLG68Wf7$~$jyC1#tm!r`s^u#tM>Zb zEApc5*@O&A$XLGI%s-=F!61XEzGXcX`D7@$Wl4|EQ^S&~ts-NK`8_^IPBnul&Z$EN zFF?Q1R3Lg)>Dz$h&3dN5l(0oBNmPAon_b{LlHncrxUFrifcfjRmZO0x|DkfD0Szc)b!QPt5BBQiyn z?#r7j$=BTc2%{x3^vvD2I;$B(smH{>&M_gx}dqFF}#n--Lip_zsIhK9E)RXwOf zt2pkvpJaQ6`4QHf46Pu!uZdPOh|+45`}Un@q|!l&*00<*?%K1846T>N$8(lNlzfTH zCzM-dEVAPuBgn<8_4)XdCFP9bpRldSn6*xykA15dL_IO8FN~>E%S+Aq_CBAD`;No= ze3ovFEAcmq5;9IXwyz@BBpF0;r8u|8TgABcjKB2x{0uC~*1*++5;7ipxUV7y1Q|qC z$Gcp`**R(W-p-P$ts-Niea$M)gsK%`*X~5kYcwj}T53P$?1uLSyPnqweTi>f#%kN& z(eV2eEUCI4WITUQqaqh8%OdL72O2*2Yp7$B*$tnsYoj?0pY2N}#p4o6$asBT!)F=` zb&x?+b!5qX4vju8FQeYtxK)&pp&g--^FR`{`Mzd^i)Q~mWc?ohMGflv_y%V6Jo@D< zO30YAVNXS-bn5uub$k3bKgbSSml85gT&Kr>Z={+*)Xr;}m2G}GOSDwJo#UP2EBB>X z>2~i;s}3h2L$O`_OR+4AD6U3(9l2Fpo7Q@n`*sma@(lAM^zCG5O>X4exI|Tt&cC_5 z*L&o?YkIwJZ_g?+K7CWKA0?{mLDalk&HBvn?=<9XMad)g_WCbbwPzI>b{d81esQCvaF?eSLq=?Jp|;lBXZ z@H;;&$=1O9YgqaMSUL6)b>^83?Nwx;A z9+Z%=>NI0j^jj%p5cSt*8b0$|s6%^D?&~Ak!E)c-Qc3aAnG!OzisQc273v^^D6Ij- zzs^O8)|T8z#*(UakfC)g_oc3C22ol&i+`O<{nce@R?l}?nyaniD7@*KJ-)W`iSgE@ zgbdB@iEKq@@ozg(LWZI<_bn-3kG$q9ZQkRvMeg%C z6My1czizb483wIClg$ca(O&!X`3OKgF<@ANVNgQGsQt`LczBeN>mY+D&Uf?TQ>!Gc z(aZGxU&CjlWJwn57?hB4-^_+@g=z*-D#`VQR)&+1@&5A--|E#2qBy?_8C)yYj=TG= zVS84Qq1}G>EvZlk%OZ*%JY>+5X@2IuAjpzJ1|?)@rWcd@|+TkY3A-lsRSC#zUe$l(0-iZMO@OAGWXA%hY!w%e-*x!g-m=tYTIyjxF2{&q^9 z*u|Wa81b8vw2F+Ub}*w_@%75mrs2MwsCTyMsd(m;jM=KE!*6qtarYN{D!%{0dJrYA z9skA%CGzu~eaD217dGi>-})E30c)L*5>sfO7EciQd5-S zt#W0Nq4z)jr6!g|6i-e2n&(z=U0r*3F6%x^@{AU%$k1M4JfA+xB1(IcF2{4Vj=lqX z{ppRVhxaC@zcFlmH?IdJeaH6t^McgNqBG}Fs~JQo0*rfVN)(H_ zFF>_t6&Z?y-It-Nts<&Al3}!@bq4oEmG-P6L+cvl*IY`wY)^EpOO)18;@@_nM0*o% zjlDgq$k1L$d`+Ka5v9Esw}(*HLhm3YI$7hs?ZlEo1|?+ZBv1U?PGk_JlS9EdfYfSA zYkCLWiBGcib_HiSJ%bW5^mfI+n<4+d5E2w;})4%EU_ok|?B8vMvA%pL;R+`-jA@Av&Qo>gS%%w_zWFD#2F zo$++ve+jLkgSLu}g1wTD?#d)`O@W_v&puA(UH1$uGa!>rWxIXB8PQ?AP0RPTyD-T16CRlSLKs z|Jb&2d9KvDE>4hDBS!#a=xN5|5|%}jdXI~PLaW4ZnMZZf&E>q(9g28vD#ke4gLZY& z`|nB$b!gwfWd+sVw#zSC&7gz~?WG5~iVH<{HsQ1rr5*lwK4eOCiooSurtYvFl#rnl zdoFu(HG?R&ddQ%J44os4KMyj9;?oRu@Y(8ijkBF4g$zo_(48OGZq*E;RMN=3oebSE zia$Y?MU?I_#e0eF;JECjy20b}r1Hsz^`L|d-5!ePVkLtp-8hZsd!U5JUOL4TbihUu} zK?xa($m2DqM3KI0QI=G#gAC0w+|#UP5T$j$crT%^)txVwak0JUL55Ds#5}n_5*(q64SaV9q(AnW2k8nW?eLGQnn?oJ!KiWlhnLMdGWKcqecCF+2smUOU zqg|+jen}_V+&I`?zam3tR?$!>HJ;19_(>i*>+xyB~|MnLu=&DqgFGBQncj! zamb*(I(g`xV0%`Pq0^0VU&6A8qK^r!qP>dxTz_RrA%hY!6lJ=1rJ6yMqT2X7NQok3 z_ij^nwGJ{AkvqRq%^*rM02cvuj(9+<@=07}UC##)Wc%~(Sn zWDqs9ej{ty4Nn!ftK4!=sFAtZol0^mVXZox;%vorDba{P@l&%OWa#3kof{ReEdPJ7HGRGq--hlB%sD*^$fK8*HG( zU-&$T`tncuvJ>BlF+ROtU*^ue)%WSk&UxRV%D35-um6VcITe;21upS|*FYD@hrOdOPT9z24WbLYql~qKU2+)r!Io?}k<~7+d|FV7QncwuH_qAgc z8D{oU(c3xV5M^dB{_P5_qU4_^_hPhc$0{;RlL#rquLq52yQ^^TO z=MYs!Y9(H6#W+gil{=G2-64Y#GH(8y(Xq8Xd05RLDmtF0vP$EtyM?LI+1*~2ZZ|%* z>TnV=6dA;CFS9J76n(^RFK>KnUv|k7`wrfIV_&xB>-INGs&o+vl3#a!XiX&=n zkGG26^2%l}M$2}rBIBWZdofbmTI=6%R3qx;Tg`~WdU&fS`P5ClsBJq|kzr=3;qxdA z3#}rGqeKDD|C81ZnAzjsPIF??NL)+*=8h~m5?)WO(!+_xK< zJ8Po8>q-h4l#sE_!Hq1w!%GHH|9wOwb34kR4ob`(bmmUku%wVd2^qa7HnQL>PC*Mj zHBqOW+Q?eY;-qR_`XzZzmpehao#$({%1OwO502+fU|B?QCgXMFRxzU2e9-MXvLw$i zKSEzZ#t)|VWh=c5?^Do122q?dcpa&9a0c*sJG<6Bqt_+I_=^*wke+F1i2@jm({tq(9CXspX}$Q789gE(_4Fe%zC} ze2r|8PzNPsY%sPbTl*~1C0jmJayEI<_w}V;;ejk zCmxOrjdt!ncUW_3CrYDe%SrOwDoQjuyWB)9sagjaiVWhpkXRN`oHK+v_&m=2YOnX_ zW>wvf3^~KpL4Rh%f#mgFdi`n?OR`w|Z|JGXFe~YPU8|Zwlv!^N$HCO;pya|$O}y*R z_>}K|wGJ{gOKn+)Gt{siL~-oOb);648EfG)^q!Y(&aURPim37Xo3Uhg#zjL)Ym_|E+mroizkO&or8TYyH4g0c^K-UHsDlzRUfi$O&-AMqMA5J0jMVbd8vCds z8Mm%M-PJnCP!tvSi7bmKMQ6?jhgMOdsIT+xgBa7ckS@VNN=JJ*{&2N{voF!H3AftDABa3HBXIVtuW*9Crd#Hnw1qS85X~2>~ z1|?*eZ*!P=t@Dp1C!9MIC3egA4O01CN@St?rU7-A8uADLo{S8&g3C5u%^*rG9nUZ> z?QVXq=j_G|J^z*wC$EQmpX=4~iSGSiNv&2n9c0J{yEm@=dc?`)?R!knx-Idwd*R%^->%BV@d>LQm%MwAak<@)=Z(YO>djUS!1z z$slS!!*G2pw2G4T465v}rS6bH9U-Hq;{71QeE-9v#CGpvsDlz&SialsSVhKNh8_1M zw2COTwDVn|Rfn$MleydyYi(d=jXr-~l~s%l=3UUpTvna--g7cey{wVNZ`ZIaqBs}z z^+@%8@O{>3;9}AC-dZv=61B`Ajk0i!j3`-EJxg`HU9IcxtEtspW*T3QT!+S27pv*L zb5S3kY{;O5486heRZiNg)^+pq_S%XJJxw>$53OQZMA5r?Myl3bwoOk~-ZCp=k*9a) z$=scmkP%8s#*@1k#wi%pO!@i?b(kA&S$PkxWLvELH}s-p-1Wtt%-zAOW`wA!H;PY? zjIVyqypM;X9)%)nmFrhTt+;tl_VD*(3`%Bg(vy|<65FwgjJX^3WbvDmtOrr-`C(h} zJ^1^&<}}sb&h~b!BID$Bda{-bO-5PRRz&TzR!TOp&=<7OY=b4@$_`_M;~1 z-^tafl6mH2UJ(IYH^1Q{gPf6Cp2;~BF|HVR)>@BcNtReZ9cRg{o%@61L;&B-9@gXbH* z-9jDgKPH=5=JGPKq>w=g85b^UWbr(ZWDxb(5;J!moZkeHlsnak?QlB%sD|@c^B%S zWSjTQ3EWwotjUnE{rl#W?%Y_-X%$gic?qqOk8*J|+3Iicn3@tY)W^z*!##|AtbL&2 z*Kz-Ok2%9|j`eV2JFgU4MF|-@+|j@abjb;46GU;9qQuK8&f+xoxs@q;jF3SI85%|7 z&w~u2G*Y|gAL^h)(LmXk+8NczP{a}MyR?cZMK$hS39X_;kyH8JwPO_-iq7KiIjtg! zqfclR$0Eg|rB`dmDl!!NI*%IapjAY1rXMmm?^oPj=6-C)Dl#-ra2cUO9khxlj=Ld) ztFn4O+>Qy~;E+KH8G0Yx-b*!usAJ6gAJ3k|@ov-E4PRU8uGT@uhI1Nd>5>z!y%NRf z%;Qse2Cjd~bGkLT_N*d9J~$qQvn-Ub|+F;Qpli$44oWt zXIrWnMCo)&e9e+Ru@ZKC|Omm`+2*J zRqSz^rMf4`-c#aL4@$_;3^_i2kUC|d6kqjg{CLi;()p6$Ho3P-r(WEh zeBF<7xBFR=t%0itC1mJ6Ry;F>ZiBfT8M;eWX4Tl&-0gcVyJGy8@61d0R*@ln4y7Ao zLB0&nU=4M$#@)Q9wYCQK-|(4}p_4rEoC7S2D7Im!Lw8JE76;wbb6IEfez-b?3`)q* z&A@n;8#0K}O~eDcqAL@S=vJf4@57R+ts+CWDC1d&SXPMY%UlkrP>1f+x@Rl4TffxJ zS=W0)1|?+ZPE*;(N{+dC4;{Jz?D{L&CEodVX9*d)QB}SxB`1vZiPC*6_okJ2>Ck-x z=M89yZW6@bb4tk2O@iP(FBFA3bo0UaD6&JVC?P|4C<33DT3%RlqIlCk)S=r>E^~+O zN4X3n(mnF~L5A*Q#WS9;ETZ^shF0ljvFoF{i|l$hO9~m3kfFQJ?(M2(5T(1;@wb)| zWpr@ffV!)7kf96`&J$HLh|=9~=f^?@d$sN;I?pMq+9N^RCRCX zD5G0#E(;XLl2U_<09=nFL-*rcKB;O3QSChsGIVP%o`HsC5ykf=w2DtkxBJ~Y!IDA- zC1fa@fO{XS8AR!>rF)x01|_1s>eA%hY!bQ?7OJjftQcT~&gUusEfw=PML@g(sX zPs}{68SlGaSvCfDr@N8mq>Khj4X$66WtEIOSMB!tsu@J>zgD-m)-zJ+ki^{|5~H)5 zNFk%U^=4SEW7liO_&djUTs{W3?aSRvZdYE>yy=04XNBf>}<{6{Wm#VBfdDa*|YdrboG04f#&U;SAp-aa2 zTn$A_<^KuiA4G*drsUZ7+~v6%TE83jsN1oMjK(J2xWi2y!5jyun?GlK*Ve;k6RCY3 zrO%MupLwHryI-W-DoT=l#vp4zsk_uLyyj#awdWXQEGRjlRYcJ%<#=O}wTf@fj&nzM z=u61h_qoxn{z_`{^+eaYL>>0bX!u8Kk+q6%>a3GTW6Wr$t;lE`I~wDa(NX+`RuMJj z@X>y>b7=iHt@SP|66#P}b#|+f3_Tl{tD$@v!`~>P)E8RrZKq;T zq7k68yWwPLED7$PyUGXeIZ=#JbFi_7;sslw5H6XvClGSVhJ& zzZu;|yh{|LsIcaDUX=I~pGz-K{P|7#v5-Lt8Q;Dt@%bl0#sM=EAAj62JMqyAO9~m3 zkny?s2_mPG6Z&1EI8Q9`dLHyLAI5M_AxEt9vN-cJbax+Mx{^yA9ingL; z^<%sJ^PsORHK-nxkkNZ$w|}zL45CgswY%bO24gkF*PY!zC*$zxW5PS`g=yh8$ znAUsrscKHi$0v{J@UA`?k3T#n%m)y)il{UHGR9{U$ic=U{iQXH^ez%s1mI%o<;#~h zO}{R)AzFfAP(p@c6qm2F0O$V+8AK_UC?zMfim1sGl8V^prE%T<{Kc-ll=f@$qMpiuj5QqW<|I@wW`CdkH0E{OZSvzu{<0xBrIkAW<8PO)Bnla@_venI^g~a&j;-2z5|G z#<6F2`+J_%45D^9&&-gbIXSNR!e~FzU%07>?u(o?Y>`k0C1hOmsnM+~MTQ!-6;T|6 zLml+z$J}5dq#~mOTO?#qLdI$T)!oJ&ETYEV(v3S`rIxg|<_F#W+MBuUmp9}KMyh8{i7YH{ z+qYvC89jy_&*(s_h*C?J*#u~n-UFAPLGOpl&!Ccw7mq}Jl#rqKF`l1+45H}sbDK;% z*_klc!WsvTMr6^BRb;%l-)KZNMa`uqn9&n;?pH^*M##o8v-1PV4ttG8Ci-OUQU+o6)WF4?_zXMA7r*<)>ClT4Vg7_`$7ZwPzI>ie%#} zUM!0!Ma#j8m$!-%MdWU@WJ$ILt{#+-p{TzcsY_1SRzzuL5kvsqs;5>;{0aUiS4{l* zf5k*yT?-;_)P7xw_Xg{k-_Cz9iYjf=6QqQUF2j!dD>8@@yY!Exmb6BREOfp$EQt(C z$hgz6;~6%{AgZ=Pg0X};IBw6opu5BKyJVbtS$D=PLPKL($c)kplMU<=x@?{u{^q1DyM>UUjF)rT%i?#oTy`2oLmzD8#$q5-m zX?@N`=Q%!gI`~e!VV<+QU8BAbe?KT8L%pPBR5NPq`{BwWiv7*n7GYoT4D%!GS7fN?$8(FYETR}0oi*x^a(*Ex@>(AKV z>^&jl!Cl{Pp1j1O_B!kR=8*-;<1_o3_wG!)hy6rv;_uzD z_k;|huGl~Ek#{wN5;D&JPErx+lR?zDgUqVBpIH=krF!Q7xW}yg7WZ~{xT71laNCM~ zz!+<06E1I9SabG7qUc>i#x*Pat9jDh_Ni??=3mYC9;kSNrVjcuy=1Gz-&eWwi;2JO zq84!;!$R|a)!Tvk>gUXgN0CFOg<%PXK?xZvZl3siItAFO3K&GOk6EbYr8V}rwKquo z9d7Eb)n9aAy;&Af|GTc)kM_A`jF!|{b@DoizlAHd+e6rCEfYU5gMepHLdK?R zCO(^hE#2vm2Pktonf1M5)qw1fL5X~cW5^S=yW)NnN11zPb|V`_J9ADlK6t*nA~OZ8B5JeQ-TpprSdZ)G zcl%r@TQ2Cv&FgloBICkE-M+ttI%pM9k1a8)2JDFiO}Q*e$?Vs=(fiu5ii~M*cDHfI zov3k3%`>maWl>7LvaB2T^V_kCj62@xZX+K6QLDV$jr;>;`=&KYwt3H-dMI*Pu+>Yv z-Y81Q*#3QUCLo&2f()X#<`nAS2(bRZznXhy6{CTF=JE{A>yvTX)PFU1nrzvj4l;;h z^ij?D)MwsnUOwBFJMQTBn%Uf#4#py{Pkg_*#?nseLB`V``=EL7vX~CmgQ%I`{GfTn zzhVqZR=)Cs=BW2Nv5Jg+UihGS_WLm%w2G)}|8045{0A`xC2O9vym{F2POKv1?0c6t zrx)z%CrA`y)SQu8p80wN*1V@2p7wO1Ax|Sx+i*+qGvE8ON+{BK>V+>(nfZsL87)VMYgU6(xNi zF}u@6ZabD#U2`(tTRHJrDykVoaqTQ*FtXIsblF;Dm5VY%1|?)Dl65&+s~JQoT6THd zLdK%q5}#XO`@Ir>$DO)E1|?*u@5b-AlR*?`mZ1(tUK`HoMqZS5Vg@o^n`h2I`W&{U zmT){Lim{!?r}94BgU}f6)_pk6dxrTD>L5eWKs>7o%OXkqde7Dg(Vig%D zz5ZTv+l3=pMHE-tLaSh-;jK%CR`lYzKUo%0?`)G)l+<7RE4u&TW=ij38!byw7!Wf8?a3vX4b=Rt3& zb*3_JP&!f3UyR7o5-c^~iN_#3Aa0aZ8)W?+(A4T3%XHUvl=UC%T%G z9qOQj47~*{t(#j#22os94|Onh)|$1;hNzN8j@!x5T6jDgBFiF*o+z}6`y85WyPRh% zDP&MWhGzQloM&VZrL%EWInT;<+$SII_Ol7To1s;dkfEL!&-O+JQS>ohN2=b=$V<_E znH9F3sE-WI6I`xYufzNZBmEH7&Ra`{W-l&#R;v#4O!9Y-D8`STk;*E*&uZN=S5!Mz zk)aj(c-|;_IHI`U=B+Y6(i-E>f7|!%=HuV%B!(m7C%=5VIrd2Sl0s3qgGJOa+1t$r zkBc!V`C$3m&A*-6iB)9my63Xym1o6t=#6sTlBhL)wye3uc`*hhr~GwU^T>-kv5Jfn z-dol@cw9^ets?4;-QQ`heJvQKPw7t?-^+C>tpT`x#gZ)6{u@3IG88k!M;w+#6i10r z2mO*x{JP9|Drsa-O@>Yz2WM*wMPYAPesJ7j6|iS5u63q@hSBB~|I@C^3qm45zC^R$~f zv5Jg~AAYAf{?8*>MHF}Y!?t3(U%0~S=2S-~&zy|rXJ0oyCiXn|%!yiK-`AT*uN7lV z+4YTPa*UPq9r;G{q*Lv0mK4^55;C5;_>Jaq=f-r9K@=m-PzU4BPj2*PbK6@wv5Jht zdfsdveE&#R5yhS#TE+L^mKWY^UNNf^tH}7(`foMoFBr)xqW1jyTg`Wu#Tb-KN#ANd zy>chLos2F1@>cWswMXjhMDhI(+loEzsN-L6{^us0v=tdo{Py+c-CvH?oc)TZ(SLot zdCl%I1|`{?*PGw@dM8$qG5X_gH0K-`(?P3<;+qz>6~_#+uj+oSxzWWjPfZCK*O_tf zAM3=`d#-C1K(!Bp|E4ldBOPZ^#Zrf+KJ(o0}zCFgEgpB{b#?)_AtaZsC>YY25 zG>_PFB*sc_FKPb!Ar`gf+OIX=K0d}^yI-@)lIHJsvbM8Ds#}+g_f}uhJYk<$%~=*v ze6pbq*6{SnOPVL{Zf$3agbYf^_|iX@G=I<=t2r4&vE4!)> zU|SKj*Oy*vjz23_4@$_We&(aUzNC5Nqt^Cw4qVcl_pJR*c4!qPWW0CGlIDT)V$Ylm zqWIo~I=Z?xyW3^>Q?3`6y;GTDTprMnL5Z@_xa_1XDP&MWhH@vwbC{Arlrk^GbC~Mx zipvVBTQFt5&$52iR*|8bHt~#$%Btb=Jt{v(nJu!+#XDR_DIr6-eq5%?Y6eltClt?s zNr`e6m6}~pgAM8JaZohd5^>V#QRocz~s8maE9xltyY!u zzOuH;auv^QNQtu6m3^rltH@B6ysEximPM4Z@|EvOSrePD&qIfyJ?YhHXzKHeV0Da(>dn`~Q^mXM*li}7d9vWViy5b97)AD7)!*@4R3 zsO9?@GW0y$`%H#15V^dp)eNHOkB2i#zohJorB^F;4`-ATGL+%5^r$7rt_Zt&5Tz`V zaZgQ&GF6r%K&iXb;Cj2$L58wnmZML}39TYZdG6fURpOj`$7|RsGh|GrWJ~?B?8+ z+`C=suC|Jd&)z({dG#ueU2^mU%d&{t>W$gW>sO62DEZjGztCK^xyF590%L6 ziVWSac3JaFn}$Dgq8K@c3`X0^l2*o4?N~*IGPK3>Q?s{+sCIlJ8OjysGH8eO;D|#M z$F7h;Z>emd<+#+2Rb(hTX*njAT0&1vl(M0^5w*nY2~wiWs%354v5E}keyw^QWm!Zq zat^KHyY}ioUTpqo6W7Dr=_O=*=CgB}b2oSFQcLK4h~nE^;?-7fp8Znu`JdXdCN*Db z9+kRstE{@OzMwhvxX1DlhY{J=)?Lut{xJLhhdL-BW8sUhHmB_$V^GI-CoE`Qc1Ekd zPzTE;rZO5vvu97i+ zkC&UHFLmrvOZWtd`t|>NxjFvI7=x1O_q^QP@sAGGj#Xrw`ufYwwQqIoDyxV(?920; z+uj#r+`i5$%^l`g$y_pztH$11dYK1a zThQF-#!h?*8GCKGu=%IEVmfFQQS@VF+p1r&6)zh*zj^7SuD0zwK{D33ZhmuzCmp-g z680;iXl;p?Rg{p?oH4(--i(+IGKgYd2zBsDoi_iK=BL)}+gKEc|&(h2iuCM>d~2F(H=t!%`W>$BMx!{+}i2dyHCky_Zg>_6|WwzxUzx=ySjWBO)`o7dbi zl2t^phlW;t%SJ#vIo7T#35;CfLBHx^g<}Yq8{cR_$OU4hE z7|(FyNUcj0-_5Wd^fJevvbecyX(v6Aj05@?H}_s`B&&#Ge+#V|zurJ{(4O`=@4U`H z@|8X8@8!z}llU{=^P+ywnEmViHXS3@{R9}WYyBw`jh*1vZx;|>QA;Sep7dKTahvQmHy=Z9b;Be^39j} zll==+dpgLN_+o$ZuN`7KSQb$`&FW9?iZXT_>`%@pB-cM$qN?90qCW9RX-PGM5;D&H zYuO56>ykm#(tE2~my#dfT|PmURIP)I*|(Q{p_)O|_y63V?6-65c}zU1KiTT*R&xFE z{mJYe`@7nz7gp;}7EZUQ-OX>;!>bveU!^}e?mf%c)ckh&C8LZ~OvWlqF#t$Tmo{BLj zxnbNu@*huj(pF?V`1^t6gQ+nctOrq3em9U@Fc@P{GV$_(WYYa@<>bv zts;u!eE2*#3ZHo4K=Q9!JF$w4l`a@a?znj*tB5-9*8|DPe*z=5wxzYZ&lpJl-vzCf z-14)5+tdRgIiv5XDF>)WK1B`hS;JMaMxyO>2~p@$H+cYEA}GGj6DA zT}lqW){M2q6J$wYJt!gLm8<%zdI@GfME!Jpe??EEWR2hT``%97)mD*l-KG70-d4>Z zigUP-!3e4UNBzlv_SxRoo>gR=c#L@-`^EY$%OdKiqf9Kie~h8%#myiU`?zt5C582% zgbc-9ZcGdrjP(AvYQLWev80ee2^kw1wwr}jGl=@*NBUc5cd6cwu_l_lZw(vWU1NUl z`|&bPjL*+$6&dpkd&WA$;>27BC3_l6e)!2wbdaHXy!Gi}I!qIU?>SLwgP>DISCn>x2;mJE!O@U zGKkXL@Rr9=k5mezgA&a@_byQFSw)8CxPO}p9oDE}^&m=~`m1EfD)X1tG|ITv)fjc} zBwOwZT@%8#N^^o?XndXVs6}a{o|IKEEWt2X78zP|`eQRz4>E{a_9+v?JrZMRHDZm; ztVC-Wt}k)yDh$g16OMxq_nSKX!FrrEere*}t76`V5zEPYnzgLLN42MejH7oOsPKs_ zi>PnxI^cb9SPx2e-f6(kYgtmrpoEN1Y(L;<$khy@=59TZ^zR#MtFL@+AbEL=mAv+u zf#i(Q_BVA`>mcKXO$L&Ulb8;!r0AV@YbWjLAY<(r#;-)zWLOqaKmU7wvRjm~;Hmy( zheGn*CrcFlaaeOo$e1*>$|^F5;tEozW8N=J`xMXMnxFUkXTy?01|?)1az=l0#NM&z z!7=sYYs#-!6icesLB^<$^;gVu{~GAJQqPs4VrM%4_W zK5rOq&B-%TjZusZ6kj<%Pu-qjehiB;z));=@fNZ1gJlt=C%E1hhB0!hDA6;&{7V+a zlB%sDLw)QMTMyIGQV*ikr+>5EFh*|Gj1Bse!2_-2abxtyj6}t)DxTZCtp8e7$dhzYp>7VWhJkl(x04u&xcr*GX~D@|G@6lo!3Fqd1WadF_cK#)(8}lz5yBW0C&M_i+$~Y0cr8iMACO z6IrbNH;k{y(7ML+6D+%$K@?-VoRK;m8ui^1RLrp1ot;>v6{z|dmZ6oY!=stMIM!+n z(XD1{ZPBfZhxK5jr~NNC=U_=8gAy{d$L8ic)eNFuGmMsbl%b|It~9=X{y?(pjh)0k zWE^$gK=P3rVq-1qK@=m|upW$S_dRF8>@0L*6&a`fVj%hD??$qUsOojmi@#-}yCZB_ zf7^c``QO7jv5Jfp_cQCFhm2$uQQVsh+lq1Rom-lf|9v{Kij330I8YHS@d*;e$T_r% zk#J+ff#iw3I*i{%GH`5{Gi{j>9#ixl5zB*{S|A-EQ_d9 z4(j)7)}d9D{OZ7dzwgYFLIx#d-2Cl+zk^-PAnNbm?62sFdwkZ6eZ~I3p_}*nw}`r{ zb&zr4X8ryR4jGhaM0Ru8_H>Y;vE9vzLme!OD8&zPfA#G8{eHh@>?h4mb+PlqlB#u( zvHE%@F1>Tud!EYga>rHC6lVuUb^*LXU+$zRunz5`IG547n41Z; zXB8P*VRN&vTu1Rw*j7Ywtv?5+=3n!_w8k}W?OM8?(VkUgbiGsdO0Pp|g0>>6y}240 z+Q)S}8@Z0+pKx|Z6j$y;tF(H0?qBU&q-gR}57^)BSw)88(yQ)+hfA$l=G!%x#p!gy z>HpD*4l;E5;+H2wN3`Zd>GZ{~PXNPPMTzDd&Y!neb22o0iH{#Fizv;I+<28&!COU% zW?^oPge4V~w!~n^gbdB@;%hQ2izv+v-5OA}4xN*65r9tFxX2)6a2~D`Lhjre=L{i( z5;Al;DIUp?LDYl4AMlY=sDl!nrgH0IEGcAALWWLq#n<7;AWA31+`3<=gA$!Qb8Cn! zDP&MWhEBc3*DT2(ihE(94z5|~oSoZ6W=SD~5;AmR&+SK7Glf*1P`T0??C-|#v`~7Kt>dUt8_kAqXK?xbF?bKhfqaL^g_00m>*$eNruj4xwZFd22tF5@r+bDw5#i4LwOt*Gqz_H8S-6o z9z;Y|C<^OAl>DRH-^szoBK@T`#++J7UGH97E+ZU^wf}|@G8tOMbvsSf45GNpkuy@K zgL@u2ap9g|dsdO5Qynd<21ZR-52AE(#H~4mR#BqUDUbe7Cv8QBPUcSeQ|t+{hD7P~ z?}VGc@NGqjP8i?*$4;ywLo4(b-vAx)X>CtF=$_ZR9QL;GLOYdf_58#0LE3bbdWV(?^zB7HYs* zK82#tDxw%mG~^6vRKEUgbdAI-MdoFAd06Uaz^TOaP38RrJTp1?rI%m=i(9BuZdqV=F;RNdTNDcXu9 zg}sClGOpOOzv6k2LDUwz_qRUtRGxue|9iWbSy6u{zMYKwzS5t3^!$;0J5jRg=3ftE zGt2y?-I4|cL)3!oMTh-ulZkE<0&n^D=of64D)C5QzNb=L%-ek$}HCY+sbgL>4p*i z#>nxzPfT8T!hS#Ih%F~3^=IsF>JD}AeSY(M1OBb0g&~6yGSpV_)hIHE;t8lw2j3j^ zUFUcCriBbj$k4kIU*#l&sOlB3E6$pjoOHKs(PhU^Ox}B7q}H8w`-J4=B^I^qwG)yf z-}n$ls$RmkQ@*78!cIH`8S-6ET|DePH;ooXGDOK&e&e!XjNB?pUAgUf1~T*(xH{R=oeq{ol*YuMt-Mu~XsmV5fF)Jy zAVcH4d;Zl7q7*;0_;y20YxEh18~@?HI8^N!C?R9q2ZM?GR!(RY8APS;4<_;N<505U zdxMGl+7U~t);nhFo}N?iIUZp4kqq9NbOlg#!+t!Chl7~)m9P3 zRgsYK+4Ia(E4~^?OOCpDFo}PEkPxaZ5068ELh8#bAgxbJ?F9a=>R z8Aq=*DT#jrlnkO)=$e$c?}3IoUYIs9ao^;e`^dz^ePNR&g$zo_m~h*~B>389K{InT z`19QknV9UEeTb(vtZ;ND;|IMHlbt48cC}SRan!D6Jn;5}6_ zqX*FZ!^MW}c^@(~_i=G)XcfyMiZjcQ!RPV2*@KDue%MCO4<_#WVIgDup9d57@_U@#&lX2DVlaeL-p(hrmh3^MZ z^f6&Q=mAb%GBI)A_ieA{WIQ-)ViNz>FUul|J|?t^J|n$$V&cAK%926`C1h-L@x&zl zEmJaxV#FEhV06FNsENq|(>r;BWURLO#AL(gM|y%p(G!JM(J#I9?1bdn`JGrr#*vRs zNVZ)#l2t@8)()*=%=NDj^AfOa`yW%9qd;`{eGtj$@mXq3`!o{VnTA*@=mNGpv8mPOR!{RWfx*Jxi_Fe!20Ccbjkq{Mx1nc`$Kb@nlIMgAy`o*dEZ}6ZehmP{(oSPDeD{Fm=MYSSsn&AZy)tlzNKHTOzfzLf<}l^IsJsva^ZA){Ic8AR#-cVA#= z)!`&$M4v~h^^!u8spiEK%zumR+A!2X2^s1!$4q@JXIC?bQV+fK-eHW~s{i@Iv6Vp`*W58RnYv4>zEB6tC1c`sQ$$#Ayvr7KkeRWtPf&0oW*`W?f$WSzhw&RTU)LWb5w9Xr%P z22u9^pB~YnQC=gBYadsx#&wNftyWP&t2CBtTx?}fhvE>8Q^WOz^88SZP(-C7c!`W3`GEn{Nqp1l|>YzWv?Su z496Hp(el;1cM|oHp|^H}Er&%*Mp+mq5~X+k#tmW&&f+yPYrJj0t;o6JbzNJo5t8 zj#Xr+zg>L3t5aE%(EAWo^y7$CI>~X=8$-E7=RHnZVt?C{Tr)yO{>*ir@~PWxBy7)F z+TVr`qOZWS4tt(q}1Y^#=f5LN7dMHrN5R(kQ* zT)FL7MTVl-mB%=CS(C7>h*IR*+#|-Iq*x1Y=`p2-;cZ2RR)aU$KBj|jEm6f(TZ1zbbG{*Dq^gHK zXKCLxofcSjy!{=0qw;4?2^l(fu-hTAx0Vuno}m*RWauo#3A+!|(P9-*_LR*qMqYFJ z51sNp=fV&1B`rG0uqVu8^K-TpQTF8eFh*__B{~sbJFpY0$k1v06HJt zorW=Tt0>X!fd{wf#40j$bK#%s4AaqK6;aizAB^-F4QMA*tk}p|O2{bYxaIp{YOO!{ zl9EBx^5y^6sv{LcGsyqh)r}LSS1aa*rH)*I7@?$O6m!Fnky}L_#hPw`D%mRD{e?b} zjAC^-qJ#d5sOoW$W0B&}9iDW4upO(&P#k*6qmJ!rVV_4?7ExLc+V;K}gZ{PHE9juD z$k2M_oXul8*j7Yo?ea&P#2A!ljj}e%q00J|^&VbxGG;6vGH=v`5uRX(dd@`1F@}l2 zOnhY}CNeX(UF`4Gmkt^K7-MX+aLB~CR#L3i<(B+m{!n6007tB%gbc0wU9^^Kg`)1M zYQE9)L*`z=#JsG_XAhbC2DaQN!w}ONC1f<`4kh247qf~CqG+vu9=TOo*}U>&*53Lv zL&=d_S_@f{XS7&F#=w z>OsZ}PYju{Hf9wWM6pg@M{bqoOINL8+v?~ehZ1wQD1Vy8YILNRP(sFEj~g;)Qinw{ zc|FJ=>TjnGnchB(ky|xw@1expe#o^i-fzgPK-%B5*4HD~K?xa8d~YZ*Hz6YRAcH8@ zDb(@!hC}8wo-Ipr&Uq`@b~}FUp~RdFjOd_*jAvFJGUxAxG4h%-9%Pis_%XDK5;CH( zXs&|{qG(~LgAy{{TQ)N>XD}l*CxfVH&5g3O_TzJBCXa78n3uK7r8CWGO#A!EKg>*? zS=s&$ttwU{a>nRmW+vv`W&ZobQ)imnfgRSPy%H!>(WGF36Gp`jEAZydInz*jouUD%Nd<@6C%v$XrR$dg7Gh zTBFR+D#j&bXf1R5!yLQhgmEHK)v+ieFP$J*=T_U>sk_vmcaRb?it`g;zslQ+45ErN z8W9F1ItMcP(oU=*LnjutJKwR(nuPTrsyMe6T9vBi#d*P8GVX&RGuyJU2;;d@gBX;M zahQqv%>A5DN8VOs5EVv%g_hhZ(^nJIXY;Z~y*Fg$oW*Z@9x|eX^&sOH?+=+dXUr-x zh+>_5&2y_5Su!qV%;*{BM_QwVjA-mY+DS{v%1gp5O%4VnArBh{QJwz}7mTcsWM zw^z44?!>!?%njL2VmLD1dtk_{=!_J@5ykj1v}&zuhs>>5Th?he4JGC-WZpAqt!J1Y zX^j#x#{GH7+y;(WMFvroYsj|WE?zh$lRkJ$tt4QBB51`Ij=rz z$lThp<X=2N^^$4i0rtLdFTZo9A)hFdey7WDv!8-s{M%Vywmpkg=U- zI>;c3ad4=E5;C^CWv0359jiGRL~*Ux>&UJ8%a={O`#l@uT)q3u#N7MJ-&#h1o?(8Z zHA=|%&evy}o8_@+i43CHBB2gS$e4TJOf%;krX#nC45Ap3dmX9j(S;~{>{>HT{)$d& zPR0$J%rv<(Vpg%{M6uOFs~DxG*Um8a&uzJkYCXgJNNbdkk=-^UF*yh#qca&qRmbi1 zQ8N;AgEcSf$Sr3iX4SkX_wkB&SL>oK5+=i}&YPT7j$PCwwa=qu5JhXNYfi@Dy)zQC zD=G(>&UG#k#^FYYs-4z?dc{bhby za0a3K=Z7pCX*R(aVyRxBGHmiay%Vt~X#40kB-QlXUhFO)Wb-7=o zJJqI*a@#pu3az4q411q@*v^lk+Wc^4Mv<3G6lY;xM{X78WV(O;(zBiHe~_WO?`B3B zc^<3>QQY(PR^|0jjH`Wf?Q6XfJOomQ-yO z88&B8Ob5#%if@|Nk*XeCf#AvneT-+AAECb@!}MX3(PJckMHGEys6&}T%&K|bJ~p4o zNNq)wvXPh-?G9R(3}rw0*7T8DmndahNuC*rp)62lHk4bXd{XZ}FpQBu4`t{vYn3@l zIRLJlWq;dTSi>0k^Pq$bWfNHMS<4Pv*WUDQEmwEAO=O$v4jGhCpS{C9jFH!aI_zEj zR(+ukmP>|m1AO=E!*m#GS|fufYv9mK|A?mCdY^=H;?}rfB~c)|?ERH!3zpu`Hr&)~OhSYh=Y*Px9fTfnqA1 zl@O|k;qu?y=PoOuC-~Tz(@iX5ea5*LPB)Q-{mu1K&oDpI8YN_GckOg@_ABOn$RLWV zxS2ZP_9rgAy`qW~-5`B8nqwsKe$Ov}M_hV)nOkXPCX5$n&6t4CN*>J980+ zGHI97D&@#9s|9&&ZH5i|o4P%tMF$zm?qPP#BCX4^h^iiO=wEHtp-!wK!|bV<6}Fg9 zq*X-GgNHTe%*1A}v(K4tLCBzl4C=0C5TzW1W`-YWT}sG^js~fENq7^XjP9ep-I$lw zLP^P}o=uQJRI&PzXj}*(rklOOVLA*o965>N80>ZA zR?)-R974A5(sy}Au7eUX=!rZd*FgqRHj8A;CsIO2)Klj=7zryYmpQGFsmHMAd#lWk z&?l0ij6jFnKa#&9ioP<`!I-MpSIc`Mdw$5EgbeL@De7o_Kgb}e*fEPRDA7)w;t`e0 zlB%sDqu7s%=wMkyX*Wx2O|A8yq}V0vz$!9|Q)m$#w2COMii9<%Un*8>^Kz@b4<%%1 zby(w9Yt6|BQD*hrWZ-LM=#+)#1djMQMzPi#(LrBZtc7-L_+!Tl7`egADC+YCWsfDq2F6c1g9K)ygPN zp5+qlTFLhcRqS9!7?hAv?0-cV)UMr7t)@u_t*x#(8Pr|PAWFMgTH9!?IVEHiyIB#2 zcCeh!C{BpwD84tLRg{pS(_)HwTdg94D0-z(2fd8;qZFH}T$U6vC?TWR3md^Iq8LAh zI=FsO?5lKO6&b}2%?MTzMc)-#MNg)cRP6+`-&SO3tvSmgiX&XuuQ=Z6w7S;ZRW3^k8I+Kr zljK@I`*5p>(m8aknYA)F;?VCFrTTwzr zagscugAAgIbLbHUB|6`(Iht%|N!3=7QJlqz=wMky>4syTMcL>Jbx=Y^aYj6%gT0}+ z(~zUs-$EUfkWrk6i|8POsOpGJ=ZLf(rWVzSB<<4ZH~Uy<6(wX8r(PmfkwH{-l)*Sk zr;xPX*S=L`=sdmF{=}}X2T^R1kiqAyGhSMEP`TC5oDwpMGi4F0$RMh?Nf2RBLWa)9 zX|=eu=9CoY?{XAN3TsXY8O1G+hz>G{(k%$BxU^bjZY*cY%p-|12r3hpejoI=+cS4E zBD9JqbE7VEc}uGql#rqPtnnPCWDsR;)Md*3;_H!G+srs?5ntK76 z%g{x3$e@G_<;{raPb7mVKBrJe*zQv^<(G-pyr88u>QIgv$1vw-GUc#otvMM)nKLt) z^5nEKC{cDE<>G1IDl*K;noRkAT6NGWqD*#&OgV*G8I&j|l5!)pZxtCP(?g~_Nv%3) z6;aBZqk)d2<%BJ<< zRuL8TSE=R(%8c!DK+r!b$F|AClLb0*1|?*e)2*4iyB5}5`>5J&P+7{>>$2yV(I?YB zLZHJ?(;6jYXm>Y0M(He!b^=t^mF859iw(`FooTPLRflTsas_GC->ofa|3JF}t&HQ& zote3aO#7NHdNEN==Hj5d=BAzWm)7PSI5X2}7nPM9Gc(h<8T~HWFrtGJGIVmsMS!++ z`){a&Y@Mo!uPsqRhMu6ye-`Q>gD4Z#W{dYj43|2qbmGOWA!)Z&`>Coq*`W?f$S~_# znOoPYW)OAsS~IiwdONM6KAp*NtLmW+O2{zhkTaJzu9`vAoJVG4I-BGBRcd)@&8(Pb z%6cM+GO8#~ihh$F>Y#*-@n_A*+>U8AgQ$%zo{?!!+FMo7(we-2ZV*T!Z|Aa>$t&o5 zeJg_!GUOFZ+m(#eDhYMy=CbaC4A&RdgXNN;`{KIk(P|YLM461InQp~+hPR47QQpVh zsiwCJt)hesWfylh*{c~uDZ98V3#>AhrL`g|&LxVkUEUz-&UF~H{?Zyf8T}(YpT*jL z?P%b9A{i}SG>tKcqSp>}P(sGL%VuUeq2g;^(9#+iM74|t-YR-adTn~U&?-vEsP^q- z5Y=AmlA+T!+DUG$Im;r7<5gY{Uvv6ConX`%MU_k671o>*GIV-UXChmzB7>;v(STk? zXI9)U0R32K6(wXmdfSXl=U7^;B7>-Ezsva&=TR)F+A1=B&^sej=BW?2iYWU0Tt{j( zrM3Ito}RhA&DB<)k?GEXEtm7Q(ysi>wx7k3q1zC;cRyGRz)Q7T*mg zgDCd5P>0zi$=tq**>A~|ZA`YC9hyw}$$UKuTKLSBok!VigfgounQl+FGL-d6xtt_X zWf8 z$RJ9+C7xTI5;Al))vb7jR*^xJSuf3W(yG-e#vkT>b>?oCw?r9*qHtA>43qaKa~Y39 zt5_CM`u|&UIvHxHL-Sgf|4Ossc*ZO~bLFmZSv}1=p6M<2HTU%>-tAl>w99zGlEQi@ zpQKg`RF*PDy04v7Gbka$tgdESIcT+t45AeMxv#l|R;kU^swzuQPtQcZSyITLgbc-@ zEnmAa)NsX%D0;hGM=H1&y6dR1NtWpDs74)I+k9jwe&?M0AB}3|+N9a%%uMc;L=5`M z5-%N;kfHp8ZYQ>yL6q_uR_^e6iP;m*+|Ds|SL+~y-nE)Rl-Z-tbidEGo8hFEm*2GT z9$A?w-5x#Jp$^)l=9;KqWGFis6#np+}CdM>@!8y zeU=n5lsU(JgGadxm03rYR5K_cL%9=`9r45KK@>;r&?-hnChugXT#)TsMTW^cnJM?= zhg(II@@Oj8Wh;Xc6SHN?q1nDwWEA5^LpV8~))|5ych>tzsOddqnO81g$M9ZHYBX z$awsFLz!+SwOU06QLK}}U6YB;72HfEqiyNE7xIdxa-Aj8ZHvUsL+mPHib=1>PEWGKH{najQ8gwYaFj7v+r zo*?5-mFaS~QFq9ogp6Xo6M2GU5T#ZQa`vZ2llh;?I`qfMnf6W98_3p3RDQX@qAi0G zGK$eBVwFZn_jQ$GnJ!OgDB%TjBKUPo#wV*aOWx`Ub4^(0X)OP3v=?4r{7fAa4lYb~sWpAyQ47Wx` zl*TGE0!0{qXVnR&^JMpizWWd-4i5Ad0v#srIo>XjX4*Ux$ly( zeqlW*A)^?HB09(*inFCq2jg>%v&wwx-Vay5kUl`K)z+GmK~(kGDCG~)?R%A_ z)g1R7#ry_-)t#Tr-3B6qGl0AvsjW>~;~c=;;K_93Uiw-XMfLL>jecfA#`r%E8SfK| z^jQ{BE%oqLDI#;LfW`T{Oja>cD-6p26Rr-DQJi|o+0_i9xVC5uH~*&cqTF#U&iMR) z%FY8$s_OdV7(*27C9#*NF&Z^uZ@A-*y~Y~F*y8LCHbB&%h{~vOS7QS#5k*7PXp+Ar z)>wC(he{DkED5k08+Mu1( zua)ErnnyAZZRg?U&J~F)cXSc8VpNs`Sts&Z(IYK;d3rW~U6GJ1O=<1_?_ZEn>>Ik@ zF8@<0aO_^jWbFG|e7ff0&hT3RB(m|KK&@C7qq1?P06~w8UkN2g%aY=?B9V=q1vFR| zqc~^bYp@rWjn?&`RwS}k57J2NWYqvuSe>~bslFF&KISJY2R&tDTjo^_piX#?TaQYv=R`LZ5nctLh zWe-TnH)+(6y>DcWP&~3njbx07P1@IBPf_+3b#1Jo5WLfTf*ulWl}U+fZ>4mcU=-)u z^pKTHzi^cIwj^gpejlk-b1gCwSsqzR`?X?OjFQZ{@tK}qEA|IvpGn!XO=`uSp07a< z3E3M?c7GE(PB6-?k4(a=W%*9c&Te|N{GEIadProyX5d_rU=-ilz6SHGB#T+HD4V~o zNbu?EYp_;+)RU3SGx>&^jVF+hudz*A5ZYTr_D+;sE2+EeooL^jl${V|20s4&JKMsM6Rj2heZ9^m1~HMd*hH`)T?gh7pwcFR_>1ybx)n( zGe%?eU!P!k{Hk{oI zb6@(yn~a3)`X;$Kd98v3qxiNo!`r@{Ax?3iqO zGVu4;SR?2mAv-kNeh_hjQL-bn?DL%ef)&TPzmgSJKcn)sO1I-z56%?{`M#xnf!wbZ z&lRJ1O{avbk^FJreRW?lm%a0)zH-&ep88_*jA*R>>$git$bI7Nt5y*K(U5OA%Nh_V zSH28w`$e<1TDt!0UnL~uThcdVdsW8?M#&eaWpzj6@4Yz`?n(JicbLfCfV5UdvdNw$T!K{ zUWfG~PlZH-wPI95Ysu9}{-oc1&7S~n*O|ojRcr2!A|ZRzhj+PQS&WjWx9tAb_zPz5 zyYvXNwKCb!@*S+lpvS?tGl^_%PT*XTV3f?A+jneIg^K&v{jC%@Iw0Q;lC?$6U+pC1 zoAKd2Gx-ZLicclKR(xj27d&OHSM#+ZA>T0#?*z?SF-pFbDyxnge?i^{@-1PxQkt(7 z3Hg?=Ts65ydiV=p?Tli}AU{{mkd}V^ij($&ea(YcXjZ&}^5x#VUrR#XhxT2NIKe2s zyR>lSq)lCa7fG8$zDy%WOYOJ@32Bqq?xb;oQPL)nz1H)yTXCHGTPbAguF~@^V=(nE z4lQ&K<^-<>`MR#{1kS6+m7f3cpI0R0t7)0mcYpZ= z_ceS^oyb}^Lsrnaz7rH-!8YHr{mW!wrqq3Enbt117 z?*sX=pR8YM{%R*7U+tDm-Nx&RcMhZE+uo9$n-h7h=#g)DOGa+Du ze1aYl*_$D7u1GLS-bCRqtje2AdUbN%yjCQ#ckm)=#VC3E%lp0YHyqml*^lBs;TV;zeGU-38nV7~8pSh`6`%g6;=K(>WNn{-2Fqd; zuQy+VSA(=FrLEZfZ%!f`{a8e;7{%+&uNAL`Y~G*;wIY$tSuCPfjFL8qeZSbRmGtxN zd%`keVBg;5S@#KgNXU4cefKv`Fe+Q25;zXt2iY!1X;ijdAd&6wlSYyD zYef%5rseXN^$B`NWVwogT9IHBuQy+VSA)DM(FY&%Q7gnb>Dcbb2$=pm8q&l0E=2}bdW;n#|9VVOa!eDwkCOKb? z1U)1qBTRC>a-!nu=Ki{QPF=;HdL(PWzP!l0**^|?NMw7x)HO29Z-q08*K``}YNWN| zH7awp_T4RNXT>Y%%~X?+8U66rxmXsX{CRb)k%-7h(ZuAxE6ghDS(ID@wPpO_*)8T~ z|39rz`cI`Gy~oq$sYhoQEvin}<3&TW9!%p&H_JXREmvnVjF##s=e}(1sn^SuD}8W( z5SfsC0Zj5p)T1*d7FDOS;PgL!tw`t$jnQ?+Cm1C&LKl1&BIwasWQ%I1RwQKBy7aZt zv05=o`u|7I4-xdp9Btn(EUKAWk&xNmwoi?Y)rwKPdg}3VmFR4rmL>Cp+f1@@o2eBE znU8EqjIOK5UXyi?jOl*+s6{oSK|=azLmx1j8ob&Wr6bUbB6P+?qh#)7jn_hij*F>B zN7XDUb{xD@bzWsr1doG+jNl~i7#%xuM)8>H@$#GV2xa`|&}mj~>^N8}5;9`c`jXLg z75}bf6tA~>ywr-nl+HR>xy{syg!I{$A8&MB#jh2kbcR1f(4$!d7S&9xNa&cQ(Xm=F zN_u%Wj0_RH`lPKh??8)crdA|$1ls6Wtr!)58}R)hZQZsB`u345v={HOHxV`bBa{(g zt4TA*p<~EO=xDUj)Zn$tDBjKWc)1dJ96C#;<_0{~;o}4QZ(p%kS0FFGb z70(r;q=!0W&k(`mkPMMSC+gJ~zdz_9A#)T*KZ0vFuN4VKNj^)zJ3|CLnlsarT9J@` z+O}gt8mtwgbp20=(49imBjdS;@2j?~uDPi{i2OI_)gU`ld^xx$S0V{nF|y+^xUTZE z%U_UDy50mtUMpS=(ii;WHa)o#Nl0JtvGqgO6|Y1_`Mo}O5P7X+UTf$uJ&Us1>DIN? zj{hF?kkFM@_&xG-MS@Yh7P8>3Cq)Ih+&ZdUJmr-|)rwK8Us*2B z`B#Xb=hVx~#os;GlUk9u<`NR-G&wZWc;yO?Eq*f#@zo?vF zG5+ttS}}@G44*jaH|65+tF$H;9$qdE8`YCqk+}Mha`EiTLr2b9G3ugUm5UeuAw=-b zX}P$qxM6>dir*jfkT~W1w)_tEkDLUf_^eEcigK00`=_=SUwiU{^tqdy* z8QWf5s~FN)d6)L$!`oz@9omaqWHxIX|2=wv_-Kpv;v-v!G*}j+p8rLA@!%jq&wcB+ z7r)-BCr3_V%$n`R`!hOz9E=+LV|S*5#1^l#<@M#gvj3}Xc@2FHdPod@yDd^HYW(=Q zw)~8e_BH4s(Q9H`{%ghwM$M|W)vpC0u5SR(>kqXR58PIN4{8H&qiXb!m~l^A@r#U( z6O4N0?`_39w+mfY^t^gYTk-Vmdr~VB?~Q9K?z2NkgU7)rwrc$2C=Y1M@7i|{ZY!4d z=t-^aE3_3i-B*c|c4#ZUxnHPOr=DEOTX)BwRLa|ltgT-wdPtmcVky!VCBdk_oKVW! zzPUz4MpC~lmx>Sf)i*%fz*2GVWz}X$IpGZdDk1T?qo=Q+bWWri^hixk{Bch-NXRjL zzUo364YgvFoVWAWT!=_(wf|YA;+s3EM_RA@6#hr8QbK;`m+#l8rT1tn4u7ytG`u-e zLhgw^lQl~2sJk;;&TfR5-&TBTsuDlw)n1&M*+C*ba(PncWw~ej^HY?Tx5QqV?E>{r zMSelH07x%>QhV{Gr<6{^U9|kE6xdFX-h>+mPTLQpHCF$v6zCx#{gS`bppjud!Kl?P zYcHPmM2Pr<>p@(#xO&Dqd$C^iT7`R%QY-V2kdg*{8@g*rFp51H|H#>5dGz?QJ5O3J zOY#YNNUU~jxp>jFi>ei)*beqJ*kU=jrCeNlWKU{E;^`gA#owH>s9G^<{cXy{TZed=y;WQB z)&qJ{D-wM-b1m5eLr(_Qicz0!==voGhlmw^+?GG1c3!Tnc+inOsTGMzpSI$uM};(4 zD@MJvcw7EF_RrOO<4gIwX!jdS`CE-8`2;;ACX96>g~1*@2}T`pt!r6k;{m<~M;)a- zXQNRp$tUO`vHP%6WWeB_QL#cM_4-BU}E(Lt8QsH29IA|sIW z%o$ziyjCQ}pHhm9IgS}S_w8oTBfJ*gE5xsF%-ksAxqhwUAcZ|?VEp!|tWJK@Ig=-}hOGk?A$QdQACHwzb z@#%m3-W>@UTl9`RuNBMkqR_|9H7Zi8YoBf}u6v)JMbgrSw|=`%&_hC6`qsP831_$? zU#cU&;z)>Xy^Nfw2tf}Cne$wF5B%mC<`ayP`Hwq`iy~y+$nGMUX|nql5^lkonK>m<(ri+s`W($33WR5tbC!Ao0`)H&=LHs3pU)7{xgYUxRZH z8=P7$Zhu=(YDMC+Q_963-Lj}!F^V%Aey!LOu6)s+x7&DpK0yzO3G>?XHe;M%6nn)! z!9MaTueTRl4(-XgBC+&zH*XMZ@A5bp^~H;>cNc6^(=&Byd){tmN&a!rLqeVy;n7+W zjN*8JuffrQEAMO1`#3DgC+H!u`kn3h2y~oa6la}$f-@ugEZd&r($X`En`v{%AUIC^e4zmz{ko2wOxHAcG8^DN6IUMog1!y_ju^8R2eRa&n$4%=L< zNJwk>=Szg%YAlOUoU`z2#rB}|!WP>`%VkM9;S6ob*n69V^z_cyeo?hz6z3Cijf!Zn z{V7+;{)g0S6>cp_ea%BcuF%VW9jlcQjC%FTQgPydg^09P^elZ7Q?S z+(H`p*|l0RN_zMEmKGw?TG1n;2JPjZ)QW_RSd2JvA&rJwG3wS^-BaR}g^09P^86V7 zJN4Xrf2sJ}vwBi15^p?IDz=UYX~?MBDvzp1M$&%&SWh%aj4rnoXFk4=MtbCuQ)2lZ zyv8JR#O8EdVE(64pofIymo&`dROD)>hGf84t_kT}z!`pTf`rW1dh<~k<`ayP+1`hq zUg*fvTG1o3$d^y-$=M|#GuVetT1cbeI2grQ!0Zs*Vat)T-zQlF*4t|CT#=Bh1?%bM z8V$8#l#K6MZ!#y+TG1mT!q)$6u2v*u1p3ab|Cwtv)QVB^KDYVLlyEhYKQ4b4^O-Vw zZgZ*9Ua@D4#_GR*pOb`)*4wOXoM4p94tT$L#Wd)VnFhPAnyVEFnb~N#cAboW9E^(3 z*Ge9dJwK#vXwQ{~TII*VC}~-adswgV20{;fp<0oUmh8$8h3*fQ#i)kfc3vy?=%w9$ z^gTVP6$$AxTz2=OYQ-q&i`;eRLWHZC+i}tL&qm$SSQF_}-Sd|aK@ZP@^t_(DeIde~ z;YxvheaV5e>;$2$y^{SD>3{rD0}_&RX?YHDf>Dy=8E*Sk@(FrKNRDtr4vRzmv&$&SFt(f-UxOaWT(&G5YWoB|BqY}$ z+(IV7C}u|Z8nADN&I2Wa`CQH7B%|8s3TV?7k4-*L@=9w z`$28_{JFew!dE*zB&6*V&YC2_sQ9cU=jc9Iw=Ez2U`hVjrH91ltG7jR4M;GGvw*$^ z=je9)zLe)luq2{?=ncI6?X56g> zAhorU;u<7mw%=w4;sm3Z@0Sy4t=Pkor_YvEt$TB|A|X${aCRHZVpK!V%hgDG6U-Ns zQ8jBvv!whu1oGx_NXWR~qTB6^l5s}sC!`t`RjL%&j*=1K3IC_(O5R=JUyvRWG8)}* zm88c(f>90atGrfh!^zlt>CZi>6$!~m+xSllUAqmnVwB{ny*&;@UMtCzJGw(Xl0kRt zg*~Yi3CY-dy8;bYBl+X^CKx3dfX@ONPYgP0GbX&IrBLc!Z2%E6T4G+c+z(SS~&{uqU-5@z15p zk#{X?#i(EoWJP{+<~PfjuFWFKlfllqe;o9XkkR4rtR)FXHTU!(A!F9%y+c5z@Gm(V6=Px>KQMFe$zz#i6|*`@sbA=lNSpVTC7 zd%qOvZL=&!4ST!P*rRvnOKWAnn)C;)_0rtcPD1*h;not%ViccB>DhHPl0TIKTP(6V z)8iyWk~8DYLFTohNAhxPZnL>sk&so!Hm@456{93W z$g%=`g7?8O|1ITXWV~;Df*uk_O)ur6ZE=E8LtZXL+M@J~d%l#ncc~rMAo1E1*8<>r zoD9?VxjkhV6&#bP$UVr`gb>zy;oaO=E9>`>kQy#^37*tmt&CuloRK>&U5H3)MUR}_ z-LKH7=4wSke$9W4T1dmy%y0WJO0J%Rt^(m|B!4OezKdjB)S{ZZ+DXVbY`9h|i&4^J zv9qAnN&j`VqDT5Tc1Br}#u`Bn3F%XXe?bzAVt?1K74J^T>$6;5mK3iQ3CSZ2_a;~t zqnLA)PdPvB< z;*E=D2U96LP-shR*8ta>Ol(yf&xn8I0|&Gvwzr6zwMuNQU7VnY#Heyx5}qaF{VZp} zRv)q?zgF~+=+&z|3FmT>U=)8FU&Hs*%XMScc@5(PqZ)qm4677)UAa{d`4iRGuB*5m zHgNTkc;OfAk*l2qqulz2{O+nN$g`cE#dc`V?^KrL6ZDX9`7@Cx0|`d0yJvg;B=R-n zXf4}F&fET3j*tAZi@rZd$h{f9KUfx{C^8 zq(ntIx6%Sn0D0H;>|BwMXQG|;c&!*EPgKh(^J_(qJgIFB4$nwjgM_>#!mFKF7NgiQ z@M|USSIeN1H?`$Mu_T|6rg14sJ`lakz>i=^SKYbC92G z*p}EBVqAmI^V9ZgOKiQf5Q}b+k&s&1ZZz2;r2p~X=ZxZ8!YBCD-*!-2V!ISHS1S@9 zobG<7LTJW>Wijfuv)dBeyV0)|J%i3~OKhh?mgE!kkSJW-me?K#ae`57ar(s2vF-UA zlF!GFo*x0G1knorPk=G&#j z^3$m86ZDYy>-(i7oU2BHQL|>35}WPyHR##%(^6s?R4mCS=ppgX`K2Vh%MA%e?eu*q z@pjV5gj~yV-L}Nm^T}JyR_C##c&$jdS+XR&QjgC;m+PGOwpS^&NBZaVxcugP41gv1 zwW5c_d#ASNqX}_>QEtbZ#`i}>o(vLYPcqT4XQF7^lP}ju2}a2=SxY8JI8=KRRu&04 zZ`LNtH5v#;ZG4#P*K&n|tC9Rk*SP27Ff!&~S@80ko3_U4zy33kgp6x=8X4vjjAGj^ z4X&65pRdx&uvyFIYDGd?IN@1Kmc=NxAN^XfJt!?Qo8e_idIWYH^pKF&V0bk-2}a4- zfVX13Vj9c;xGk})xx#X7iOsUJqaV@9#r`QS$rSenNh&_zZVhlZowx z#FBi19ulYQ(3XVPRFhy7N5Xs!jzDa5KzlxV%4fb$&_hC=iQ&;&5{%;e%Gcm~;;Qr7 z^BxOJ@(FrK9C=ZDq*p|OQNi9sMV{?$1yA1lb-5UMuUJY7KilacA$^~Qr)a9dH;bHI zdw;N`#v?a%67tL1j6h1b^5u{J9%PhUJyw#&>c9RQhlGq}*$hOSU=({~DN!*EK9S|x zwM^pXYUL5`?y@ZAc&+4)vM9MXZTul8T+J#4X%*Y79%~});D-6Zl%R)%w0&(pGS_fM zrNF1!+5addw%57P_C)mwdPt1Fzm$Zt?ny8z-Ya7NT&`Uk3+H{~*NPqza^KjfdYoX? z8h5rw#$=wI+MYi-sdx3$?U839JtW*d6Zz~wyjB;lS}?wqV?!mQsa?ZeM!jk*c&y-F#lP?D7NR*U{@pg zQz=Nl#>V%ghh*7?yjL_<|Mj0fBwXHN63$jG>+`?cb;+Sj2fL|-O@*!`rl0N^7_DEZl9umpC_QtkoMee~-o0JpFLUGwC`5R7X zyZ`fArG(Vo?pir=ds_wxhx$(j5^~n`^RZrEgPxUE zEa&6KEXgP6Au(W;a%7yE1f$qy^flOml;^IE%CjV&pofG!!EHt$PB2PJYIwFg)PGWw zaCwB0xe58rZJdhb%CBi7Q+}=JAtAqJ!)R1RYDEos!`Vn0XrWFO}Lk;SB%ml8`&f8-dI)pJ0^Sj}4=oX|3py``kuSo2wNGdB%8i z^uby&ilZWFu&a^$@%zp)A7w2Lc~*w6M0!ZbTv#~ol?0dba8@F@$p^0@7#*R#P%?yc3gwRj=y(3q~IQ=EQ?WY-?${a ziy-?oa%I?8LReC~RwQn@wUmVS9AH_D;)u7e!Cu4C@3iMVA-T^N-D4pkPdMv^`5G*X zQSsh{y!))jD{oHgJ2&_AAt7&VZ>Axun*U5>RQ&zHXSf`>jTAIjD-v?nZIq(%T-lS1 zQS2@G#JqQZm8|tEo#&9>-)LS7IZ=`46+I;6in(~zlam`kZ}8GB;julkYE(YAM#pNAPNZ7W05(krGxkBp1kno*Xdu}08CLY|`GHKQaL z#e2oq;An(giMF;+N(w)B=^-Iket2yk2}a4C=B@3^s^(TZxn8{OJuba_+bfJE`Nu&I z2^m2M?-fRZQ8G$lyPNwO^vFnv?R(CWe1aYlGTIW}_nZWy*zWQ*_@;mUl2T${SY=5* zK@W+Emz9$67gk9yiqA@4A!hwm)U;#P<52wolMQLdIuo7yCHD zC|++q(NZqwBf__zSk9m4@#CO}#OJ4!^LL4_am7VWBm4gK{hj4}FlgY$m>yOnZ2{>_qnf*ulkUs#Tex07HL z-z&Zb@1oycRnBK(SdvfBL*mM--7&o$nwucOD85&G4c<8e++Ae(cPz;#=piBR5-&?H z!~FLLqxdGuH7fGPk*ANfMcs~KdCP*@IpGXngT&y=%XzCNCsGY*Cs@l{t|glvY)*rO zT$MJ{m}@lDN}em$+mdIn_1k=c?Q?1M*$x|gulNK#B&3xY-eH3TqxjbLHP~X2GitMM z&7CU}^4o-G`&bsE_$JD0RgvFZt|c2clq=CjAX!pQIK#h6NXT6f9_3_NjFPuc!}}wx z6LM^FFSxK~LY|<$U~`CHVwBB%U7O+Hk?~ zb`p%@dp_5w$nU|YzC62Zj)f)VgfskKkc9NxY_=v&FiQG{4Sj>OR?@PtaZ#ze^=q1| z6$v>D)~CvAm1^)^B<(AkgKAELgtYC#Bf>0;QPNIq7$Zw-MUS*BZ5)mz<;UR+|Mwsv zEm?1@FT;F-QPOg57*9-VMUS+?ZOpQ{T9J@ezc(%#tQDi!8pwj(Vat*8`jOV8^-`Ow z6$xpV+Sq%p;SB#=F^X4cPNcQs6(@JC_3oRi6$yD_gxe)7i&1<_t|Sy?Q-~QSwPiuU@mXyCWab`MI+f-3mWzY)8I6ZmDZK{zo>h zY24(>wLbZ~&bHpYUn_cAe$*}bmU_Wae`6(H|>awWysZM`$l~@uA_-Hql`Lkd`G?(m9>r6io}dt zI`UPmae`5Y-`){fiA2w|zt`{W=4wSk?s?loDqbr_$rB^IZxlTzKhV*`9cV~=@lZ!( zmm3}-qb_``BeIhYJu5!Z(Zk((NZdT3Bi~Odey$kBH-k^`iCp=%{yXyt?i2Ko_N1oT$=b(-}m#?{6k=XNqj>fz`C*#+OQCIHU5y}1F8?xZu zWE}Cbx1An=9S5&=5(6C_9(5+cD85B~4ZbI?>(h~sy|W~rpoheh1ay z=g{}+&nULed<}X?y!m15nMi_Br_8B8gMAHpKL5D>JZDKhK@W*@K99XWNHB^|K3{{+ zrP)8~%-_yZlK1qGPN(UI>Jml74_-2LHNcXk$~)n~r}ONwidkQS%?{&9j)(vtPA ztGrh9NDJAn*XC+PLR#l`uf%J`DE2>7BCi#Dqc@M~NP4!)ltjyw9gQnlotWQdVALCz zc0^XoN*~b1)ulISqw;dD?2X!Ze4_kg_pZHLXD46&Nk=|z+}yb$@vBwc@A0?LJSY2>NBy!Rf1Wp| zLE@5r9r>HVKMt0~s0mwkL~CKYj8YBvPboi@;D#~e_+IApe zxzbwp_KnI3dPqoXIlMO&+ff&t+!0(MA3qKfHxKTJtfOa581?kfj>tNCxl?UNLb=av z-$9lXuN4V-#)S7BWLb>jh=s4Q!u0w`<1Me&M1qF?Qw7{H*&LEQ?WJuF~1Ke_}_zHTX`Ib7ecvlJ*ID zNXYLL-i4L~qei~ck?%^SHlAx{ZgALnbFaa2dX zzV6Zi9r@}!mK4_@@qfSW$X5o&2}W^@BPS~9)!xz(TnR6430q0v6ZEj|@@9yy9oR5UVZSk!Q!U zB%h#%#InmsdpFc(Ai<~=mhH&1e|-&lezu$&mCy2pS&~oCLt@Y6-I&AUi>ei)&RW5> z6NBrv+5YIWO-G*bN$q&8Nc?6y_ihhfs$aB5QcrVtH$?jvlU%CLtqtw&P~} zI2aXg$;jPlttGi@ZTB@EUtEKPJSDHz zxv1-kQOAsMW3qdM2zvf>UPr#iQggK;@$UJq9kgpmgJm<BYJ)v=|~dlPm8*`@R87 zik~ZwaBu4H_YGKisJQMR)U zwc{Ejqz`Ai>cj~~wY9q*>z&v$vtqU4T#7uYd$uB%guEqeZEw6*jQZbgANy_P9c5X1 z@`elN*7BYD@QUs?e^8$|a&{Np_93z4%5KCnnyY4IF^Z!i{*iN(WBO_xUS6M-YIU=;HXd=0+o*YDqv?+(I}e1aYlV_iGgb|#4vjN&+oPw<^uKDMKWE7nQ; z_V|v-j#)eoM#=qQGfjT2`1b0!xFi4G088=-dPv9!WcWJ`BpAg}5ntoupLXVN;q8Cc znZM8ZmhcIBNDSY|y+49&1`;c5*_n^rY`aZoWZZ|u=`Ef486DQz8Tmc-+ov;cdC7ap z+Hfo>ejFs^4QH*vIKe2+I{5_Wp!Pb#)v8^eCC$}}#OFtL=B;I4gJm)5x})8Q&Pk!x z5l7^Sv(Y%xB+y zV$@acF3S3r_g>wZ_flDsPi!@=GtYs0#M!puG)~Y%V#^ylBUySR7&X=rwy$1ZtBSPY z9=fSBpHcXSvu!?tCHVwfsq&PtU73P}L;dzHiPD~(`3O?3(LgY2=x&|)9w2c-o{2W% z_3ke_^O3NWs3=z{u;nFh9NQg4p83|o@ipinA#XU_nIujyieqOfQBlr~)?)lh-siU8 z61C$RB&02|=v5GmVtdZl;K-Kb%-Bj4z8`&p9uktJX{%u31fw_$=o1|CK5&iByamwQ zl}O^&wOwvQuqVv27{&LzUn{=1@7ts^A1Po-K0yx&xpr-oB2F-htpT54+u-2s-J9Wn zo;-I+ly~Zke_L&!=TcK0yzOu_N7mepTr0OoCB-&ifjC>MwPX zd$y13$+;r&M2G8bXBo!625ZHr^2MEz-oy_t@62a)PrafupY3HyaSamBU)dSyNAXSe z)HR*?2yJs3B&J;J=A(im!YqqXU4QJ%N1*-V;M?nC_a?I}c$VZ7^pM!Wz2PjMK29*| zR7W&q8&>44#&?mlqwW1c?YIUBxj(!yfDH5BoQ#sYt6`iWtrb0T*V+@x7ZLd0H#!*VyQgJUMODsJU8^kY}*>)dg3+{HYX}M840nKXe=o!UcmzUWO9w!PT9?!D-zOj3Xg@eEJm@{=huq; zjQa+5<|7j<$tUO`vDQhQk=YCqjABp5*WlCg`%60WQ5=@!6ZDX{_|ncu)(Z(n@pMV!y=K;PdsC>pJt?W0vF-^pF@n*0pN>wWxP3 zqYBq|=DF3r2A{8E-Luy6!l~^O^pKF=OLz?y2}ZH+lWSDuSt3!^6A}%}@nT6i;SB#t zO+t>(vc=*Aqjqpa!whd)D|+OuX0yo6)ry3?QN!77EQ?Xnn{3FSOKT-=3W-yx5AR)aBn>~rwicx$A=R{g7zEh=FWN*&qYDGeNWZ^HLuq;M#W-G6i zGyHe0tWdBu)v~_9+xNpAgZ!x!=#e!PJ=o?SxP1G zWx8ZQ*t+(33xFOHl5t^c>f^Oyl$5mS*$fhr6Jq<(HELK{jA90iPw=V#xqAcHicDS$ zK0yzOaW`~ER)CUV6i3@~jfz|)e3Hqinq?KUq?~Yu|7<5A8kV0NCm1C)43BZdV3Nir0#S^up|_j1!EKo}OLPIg!>%`m%QCNKe@A!Cf`k+Ee-9Eewrslz4Lb;=wW3Exwrvlg=4wSkM$*GGqb!S2d|&0q zk=BZDFKJWT=pakV31|4{iiEW1?VD(Ef>C@w=0rs__`Z`F6Pu}Su2v*uh9!qGB47X>a>5$n12_ zc61;i^V|))kvJLuI2gsgs9!7gOeG)A_OEE}T#=CcJKH-$kJ*l#Wig8H$DF9haqwv= zZvz{3=CzO$&hW2x67nVrXJ)c2M)4V(YoxW}vrop$>}Xk1PBhetg!I+yOvedENl(v4 z5OSg-M=s-gb}dQI)UL7SYDGf&xVAf#ufgZJjFDO^tT_!5GWKfixm?2;{x8TV_A*kU zqFklG%oEAdv?n#MH;pxd9ul(Nq~UGgApeeHl&op--Z&N0V0%!qMQrzHYHO?!^pKED zm4@HLL4K_m#b->eQ4xZ_t&DzHt(yAiR# zJR=&b|N2KxLUzIk_Xk-PqxhD{H7Zgo-fJ=@W21P@ohuSD9u$63vn)pOZq94vj7s73 zHx4MiznYG{Y<1s&Vy|AWx*6)t?i1Tu=RQFXiP;AY&*_w?C|4HApau`$PKYiXIZDpL9Snws&Y8js&ANJN#o1d^;e^GNY z-Zk5m$?RvRPObjg{r~^2FsmqbuU;{NQD)0k*{j#24@MR?*zc88gC4V`-1B}gulCsb zrV%3;WwvT;UmIE2wy@RCavb!SEgCoVo>%+hHl`6H7-hCY%kQD&>g2``>k7`@ArsRlh} zi^hqWV;V7nQD&>gQZJla_-ah= z7WvKTFn3Q=rLO~c50bdd+{FAh!Ko3TQ#0~U_{}cckY;K&||h}?7Hu~TH6HEh!Ko3 zTQz2PpItb9^Zil{ddwD$3kS@rz5B9h#0W;2tr~xxGrVxe_@h$|ddwD$Z%>$48}P1a z#0W;2ot>+>XBC!uYjCPTkJ+N}x8d_@k9=twPelkunXMWxPdl@4=H``DgC4U*W7MVd zYD@N+ojrGB1f$GWjgRggR=Dgx7pEHZm@OKAzsWsIRyU0p!6>s;s;CCx#>W$f z6qY^WzEp!AvqfW>`2py5sWfh zHNHIml)|f@Pe?WBF;~35DDDe>v5l$86D=pm({;bHRv&0 zG-?}s;@aACvS)jYV3gUaG5^Y=3MYNGSV7u8^q4IgefR#PR$k6DVg#eiR*hfndt~9o zM|!6k^q4Ig?Snt5-MY4E#0W;2ts1Y-JiPFWF?~`EddwD$XRrRG_P}PQ5hEC7wrV_d z?qP)?yDyV!&||h}?E2UzwVV2zMvP#T*{U(|lY{NM?RgJA7Z$h!Ko3 zTQz<;=)l53x2~LO&|`Lb9Ls)Mn>oNVV#Kb9GFvq+oVb5s<-J$c-d&&8wjE;{F@jNMt48Uc#li(!ZkTG& zW437AKkL)lbAK|87{MsBRpYaJ3WY=N*f`ao$86E~@tU92`rT<7F@jNMtH!Qf{R^jj zx@oFGkJ+Me+cWsPdl-a5=^1uxXPv2NfHRv&0G}iy; z=e5Dhn?{Ual-a7W!@t)n{OFS1Qw@5|7LEPqd|rEY4bzAbj51p_9zJZH!pvj#N;T*) zTQpiX{Gv8%L(_;6j51p_2LF4FLSglNQw@5|7LEDGd{KLL3)6@Zj51p_`tGu7Ve9Ai zPc`TI%*`jgTGGEr_9cUUcf>CCx#);c6Q#h>SuvCK{vqfX$y}ztYJ<>E{1f$GW zji;~aQyBBs;i(2aW{bw@7kyb9*k&3rf>CCx#xk|uh4c12GS#5RY|%I&`Lg!-Ak&Bu zj51p_q&+RIWNA~=W435~zu?Q-^TSLdMli~3)mXOow=JLj`kZ?;nkM&mmHXC&||h} zeA@r3+VbN~BStXFY}Gh-+m~Bz{py5NgC4U*WAXF8s-1PaX~YOdnXMW!jv=EHa_!P% zwrGs1epMTLmubWZMwzV||5^OWmK8^yl4{UnwrJe6*w?k8512-bV3gUaarR}8w!F3V z;8cSivqfXQ-M+4U`>1Ke2u7K$8s``8Yw4UiB-NnDY|;2h=hwBC#57_Aqs&%~#pnFJ zrT2kDQw@5|7LBu}d|lgSl4-;UMwzV|Q?I+ZWq9?pRD&L~MPs*Rzp1_TtZBpuMwzV| z@4Pj(<)w|!NHyp&TQr6q{7tR(CDVuzj51p_ioY4%GVILXr5f~@EgIim`%Uf8X{He) z7-hC<9Pz@%Eg$`3SgJvf*`o1g_cygQUNemt!6>s;o?mpVg#ei&VG+uc5LbY;D}U%9SUD%vpvP>{*k$B*wYE=9BStXFY}FWZ?7rGKdgJ!Xr>4{!Lsw)(fG5hEC7wrc$S zt?@gbwav)%IOs83H1>S!``QKHnMRCYl-a8B%CcwgoVa`gxd-VnTQtURyr4Grd((&! zj51p_mjCsZJ9oRh3DKa(Y|&U|=z`jD3rr(MFv@Jzc;@g|`akXRKSYBbvqj^AhZfYP zg1+TBz{}`9YA{z9VEgB0JET}EkYhE@65F;36wrZUJ-N@>0^OsF?SX@Sn z%j=n%Wi+WR8XLL)|I;~|oz$xr3E$I52;HmK6(5YO{-gBoRD*=s5;fjw?E0!{kf0|- zsK(gZ$m-SiZJFj}kT6@K1}-)!IdO_9=d13<fOaBB~RRD8YJim5vuX?7tgECI!yCqNSG~A<9bg@zP#Er zNYE1^RO8(v&#Nx+%zNp%B4M^f-Mqx4Wa|q}g9JSxLNzvc{@m)>cWIss39}{Y%RZBm zbBCG+33@_=Y83mOTixdf&66QvwnW{q^rYm4V@-nuJt0ChW}S0Rb>lu8r}Jtg%$BGt zmz$LIJHRwZ&=Vq5eLyUCqu$)iTb~v zyJyKera^+95TP1B{@w4Yhn=i>G9=8FsIE;XB{#lo8YJim5vuXRhG$g2{ao{8NSG~A zYi~Iz88g8&NYE1^RO61PPpdvNNb_V!m@QFLwwsjPbgyZUpeIDA#^?iwR^R_n^JGYv zEm5_dCnZn)*)&Md6CzaOCzFR%KRQVBWJs7TQ9s;mQnKV|(;z`lh)@le^-{Gw84_ko z)Tn(XCFP1~kf0|-sK&JyoKk)L6U~z$VYWmKJZMrfbBJk>peIDA##Iwes=l_V=E;yS zTcSFTn3SA||84_ko)W^q8N_PI0X^@~NM5u<#dZ}8T3<Xw0%k~IsaL4uwTp&H{?ZLjV$Li1!um@QG`hD=J<-rO`u&=Vq5439}_? zi%TXY3l>byp6w*)2@$IC*r=nbYk#VFG9=8Fs9ncQO0J)08YJim5vp;c95TJQ)&ZOVn1+O-j1{Xc{Ew2@$H{ zvR!oUWG9=8Fs4;UVB~u2O1_^pXglcSaZ?SsS_L?U{!fc7!?3+o+7RQ+e33@_=YV7-9 zp?b>gnkPfTY>Dc<#N_1pgH3}3Jt0Ch+8*y;J@zxrlObWYM6J5g$j~wb+YEkkT6@KW^FP#aeuS>oCG}~LNzu$ zb*t(Q*J_>&39}{Y|F)l;xDqlA67+-!)mUSTe$~4l(mWXwW=oX&Et1t%G7S>+gb3C6 z`PZ9PSA9tHWJs7TQSO>b+%K9P2MKyYglY`DY2)f1U4`^MCtp_9_o-g ze2?mpP>p>DeAczzmzpO-!fc6heW=9sU@{F7^n?i2`03fRx_-2r=E;ySTcXz5d`fb| z5Yr$*Pl!;BWlo&gwNaKQL&9u{a(&w5=7FX`f}Rke8hicv)vg6AYn}`Vvn9&)&67`# zH4PHyW23Plklq66MB9 z5;szjX^@~NM5xAYGbVK1)v0+hB+Qnmft4xA=zUCs1U(@_H9mao(XPWc(L5OvW=oVC zyGj<{%``~R6CzY&h5q+-ZThU{$&fHxqMrWqlqA{NG)T}BB2?prkN)0uLY5~(!fc6h zV~L3y$;*BZ67+-!)wpEf&0P;o)I1pyW=quQ2~(1FwlobA^n?i2n6%RMU2CnUc`_u- zmMAy2o4AqH>^Ml!6CzaOfFnnDU7zL2kT6@Ks_#rm+z4@|L4uwTp&CoQbaB_ES)L3D zvn9%nwI}baXBs5v2@$Gs{Jxd0?Xo->5@t)3+vOoSX>HRWK~IQKjR)2k()FuZnkPfT zY>B#UwP%u+)lGv0Jt0ChHoW=huFJAK84_ko)M}eNleig_?Drr+Pl!+rm-W(Rc`_u- zmZ%&1Ka;qbpiF}VJt0Ch3OnuCwc?|iCqu$)iQ4>tXOdCNnFa}ZLWF8;xXH#{l@XdJ zL&9u{a&v>pxTQ^l1U(@_H5PAOrR%3#XbuYrvn9&SaVGQom<9=YLWF8Omh|o#{+w!% zFk7Owx$v39&6H-p2MKyYglg38e(&+Uvpg9RW=qr;S3i^7*xNKn&=Vq5W8(c!KK_F& zPlklq5>>nHndF_tO@jnIAwo6!y*B>wi?Tc!5@t)(n1`N8&ijFBkf0|-sK%hB&VKyq zEKi1n*%EcalxGr`Ns#>>B9F?8Ofh|nFa}ZLWF9}Kj@Xm zCT4juB+Qm5ms^s!%#Q3hNYE1^RO7-oSA5L!WJs8;QB&P``@~Fxgv)rzJt0DA*`LUz zeN-g(!{yt!HLlO4WtlBeVsj^*ln@{#%o8HQ8YILv4cp5`O7=C(V>KaR8l>t+Zf8NG zrsZcuqUDe2(4#a7wV8#qV8QD(o98fFqR!d#joL=NY>tHlJt0ChWZxaxO-JfR!fc89 z?20#P<8uuX^n?i2n0oA%JHMRWA0*6{sAK;9Ms46?b{r(=2@$Hn>^2f+OVmHtdb4)k z4@`puJt0Ch-kUyt=PR=36$!H?>btf#Yws*>8YJim5vsB7BTw%9$qSmFNWyH1+Te~i zYd7{b4HEQ(2-RTLBnh)6YST~OtlhkXX^@~NM5u;)tF_pBkc8P1we*f})#mpx4HEQ( z2-TR~f0dTKviCU&vn492yj2^wv}ur_Cq$?QvvEn7Em6Bqe5>Yq_}Md&1U(@_HDuo% z*-c08K@w(5)aA>(U3+0g(;z`lh)@k?N0TsHqHZ|k?b>laF%1&*gb3AOmNyBrC8|93 z?OMy~ra^+95TP0$|71wZ*sR@7!fc89=)Z5*PF~wINYE1^RD)UfB+QnmpKdU-_Re~y zL4uwTp&GL9j_jr*zc~rBCF%z!&aB<>bJHL}Pl!+ru7V(8wnSZi=geByrlvuHo)DoL zT+KnkY>9g09f~(RXS;+|4vd&=Vq5!;Rv!*tib~vn8tS`*&)i z_c09;^n?i2`1-X8Egy8~_!SAWC2H_4@74w$WEv#s2@$Gs&pK0EPJLR(;YgS*Q47w0 zx7K-tX^@~NM5spJ)|Xp${!+&iNti8BOHFvUHZRXxB0*1xP>snWUu{`!DIFIjVYWnF zwfK9rTL#*3kf0|-sK(2u&1`vSbzKKZ!fc89!>;eu#tktI67+-!)!^z(5@t)(XBWIz zd+kiqAVE)vPz|m!C1JKiJzaaR)}H6Xk)S6;s0LTdk}zAN-mmq$#EyeRuU>Ueh){aJ zIg1spx=QCiNSG~AV|RYPw(A(vAmMvdkA!OMHL-W$j$L&ghlJS@b?Mpf*T&vp8YJim z5vnod%07j0@9KOM39}_?+Jo=cw*HH0kf0|-s0LS)lQ3JNhJEsWZTY)Rg9JSxLN&N5 zo`l&Fb=+nj)cQSa8YJim5vsw}{UpqmsH+BkP^;(GlAtFZspw&iiAL4uwTp&BFpQ7nwzQSTt?Q4bL4uwTp&H!njfB|}_2|d5YHQ}}?nuxRB2;5^`M|=pcWVAF39}_?mo;YB z{&v3|2MKyYglhcitAh*g_tO3PNSK`-$F8$$$4@W~5|6?YB2cqP94Bc5R;* zO@jnIAwo5{8zKp_C2H*G*|nqIGz}8;gb3B(u8t(kmZ;AjoL#$qo@tPvCq$?Qcc&y_ zwnXjRJ=@JE*t%O1^n?i2;4Yga%$BHAzn|^K`b>iaJt0ChI=(utF!DfMH$lQ|iMn## zIknx^F%1&*gb3Am-{li^4b$}>B+QnmQLS@oYi@2DB523Aai1U(@_HMmPN39}_?-rPC0KBG;81U(@_ zHMrX}39}{YnYJDb33@_=YVfNI zB+Qnm>iu(TdkT6@Ku6lNE?S*4a zg9JSxLN#RmQDzn8Hz#4XM6EY%Ztc9Gra^+95TP3VT|UvblXZUv5@t)(@Hgkywz<$W zNYE1^RO6>EpXkX)be{ z5@ne@x}K8+Jt0Chj(=;Z$8OB__vzJ(nKf!l)axruOQyNJrT_H95dfEVg#eiR*ilWdUyRU%NM1` zY|*&zifPG`UzkRWV3gUaQQ2jcuAgOjGW3`&8vVvkOWHp*jTpfwvsL4@Uu@iUVV1v3 zkJ+N}uY0B?L+6=Bj9`@6sg+!rtI zx-wgDK#$p?(YpHdCCx##(Nj*Mr$QFM7-tjWtJ2PsZM58Zm-VW~;{DKbzXM zL$+Rx9_*No(aVWtry{3s<s+_LtJWMG?V#0W;2ts2v=>Qj9-TenS**`o2_W;2qhN18^AV3gUaaj08= zeqOfzoF219(n8Zm-VW~;{cC$3X{@&L_~p~q~|81(xY$un!1MvP#T z*{X5(E9+Iy%)To_kJ+NJ-Iy84pyf>?Mli~3)tJzK!|Kg7I)4_zM21YHU&;>M)KY)(})p_ zGFvqk^y^ohob4w=kJ+Me*u)vh4R4r6j9`@6s_|>LFV6DWzBu%lEgCDoJR@28Wz&ce zj51p_j(7X}%**!op~q~|xZ#Z%$)OWXBStXFY}NRK+o$O99s8xv5_-%QjrV8GNRD~L zG-3p!%vOy}-F{2svi+9mFKfh2 zpx>KDj9`@6sxjxdy{a1>qIoj(m@OK!HtbG@3^R=w!6>s;deet(c2vqj^% zExME938oPv7-hCBgH%odHQM|LL< zZe|)Wf>CCx#y)QU#8ocV{S)aiTQml?btkv1Z5lCxQD&>g=58Oz)3bdb=`mY09v{@5 zl$SG&7{MsBRpUpakE;Ip6Wz~}9VRBMvP#T*{ZSr%BAW#WzCbJ$86DP8`Yh> z^@3@{2u7K$8aufCUT2)6`@Pa*wrCuEox9JUFpU_&D6>^#%Vh^vpFK^#?@W)`qOsig z?&QgTm`03Xl-a7WiQ9j7-E99|ddwD$vu^KBD&tKfMli~3)i}fLBYe_kQ`5OrddwD$ zv3GSRYmPFF7{MsBRpSb`AM)a#>VC-dm@OKoKG2<9KhiW}1f$GWjXT`F&QFcg@1N6S zwrG6!Xm>JyuxZ2yMwzV|o4Ng|PtEqHrpIj2C?wsgm2Thjv0rMQ z3_WIxM)9TYq_l}?#0W;2tr}~%{o6On_HU=hY|%JuT6eO`>ZTDR7-hChKXsr2Kcd}$3(})p_GFvt7cl+rN>Q_ncL3+#*EFt-5R5WgHLh^qH~8w$`h5d>%odIDA9N@0y=)pWf>CCx z#&hmF4gcI?zw}(uW437Yo89g9LpF^V!6>s;qv*aj@t4c>dlU4SEgD1Tbtf;}V;V7n zQD&>gNA9~Bt=V@o=rLO~+CS}1`i(b@7{MsBRbx;0{g0;(*E|_|%odG4UvwuIUSS$B zf>CCx#v1NBCggEcZQ@!zXK=3_WIxM%&ljNuQHV zBStXFY}HuJeOG4xBlWv7^q4IgtAFdBB?C+&Mlj0kOv8O2r+TyI$8F48@ODt5G57SGSC?i*ZBCZ>@R*#XhhNT|j*_j|nZ{!;1h(W}?IR@8@HFt@dy8%4tRC{02&mUh?G9aE;I8YIk?D3@8)TF>z!K~IQK4VM*GwLCHsW=oXI z5NoaHs*#{4M5u<#ZmW*|bhzcJ*}npeIDA##0ZBsLF0S@@ODownVuM;K~IQKjqP4Nv)b}w?SGIkTcS=oV{U7GEdU96LWF9#)dN-8O-IfZ39}{Y z@ZY*;Nxr6m1U(@_HU2Z?ch!5Z(C>MXFk7O2xc}VN`q~T<^n?i2aH~10w*G^J*%Ec} zc5_?nYfMPc6CzaO9M?a8Z?N{yNti8B-RsV6t*@0KK~IQK4Y#_cYU^=Gm@QG?FFCih zzUGGnJt0Chj+`*0I`&lko)-zTCCaTBYOSwbB0*1xP>p2U!PSp1&~YCUW=mA?{HNSG~AZ`?Si zwZ0~f1U(@_HQe{^tMV0l(I8>AM731rwAR=5k)S6;sK)tA4y^v|4>~SN!fc5;{kL;k z>uVHA&=Vq5!>wwpo_myz?~*WEqONRp8u?mF67+-!)!3|WshV6iD?L{v%$BG>tUIT* zzGjpJJt0ChcIo}w>YhK-b*UuGmZ&qnpWRwt`$~eI5TP1hPC2G}>gM03$3enuiR#}y z+qJcAofipuLWF9#Rln7fKGXRR5@t)(rw`6qS799%u2udbsfVYWoM74oh1wfZFJ2@$IC%RvWL54u(7IZ2o;QC%O; zYOU`%K!TnSp&Bnev43@~gY|n}B+Qm5w_`zTeeVPk^n?i2;MciGm@QFv-!QASz6S&e zdP0P1xLqTvw$B6!vn9&yAkkXi%YpFW? zWj7sp50WrjqW*l}N3Hd}UP#arB2>feyHl0jbVP%M*%CGIf{$A3d)APkCq$@*+vTPz zyXlAq39}_?-vd8tt?#`s-LDs>z((7a>_M-&4EO+d|qcOk8R+QnO` zpr}Y4JyfD*nu+YKn-4y@sHkWl>JbIe!bDN3Pj_KZiD^1u zXoDK54tKm+uV%MtL!DT|88+>$xnJ1#stszSt{(Aby_!L%4RvA-XV}!b=6k33{#|WQ zBXv^GH|y0bJ#DBHYdFJRFEs5{K>1i{gBmHD#80~?$fDx(=YuzFKtjG zweZ0=>eXyRZKxA#IAf({TSS2HuUp-!yf44bmqc<*^X&r2KBNZG{A zdNun~8|uUw&R8?zy+-%dL(A*X1~pP^FJDowW|(S2omj&eHubczM`u6JOB>Wk{pOGr z^=cNYHq?nVoMC@&HZV;G&xbask-B>074>SStTxn%HJo98H#h9BVgI_4wb(|=Ceqfc z*|^$JC)RMr#QqB#&%HOYWN3pLsV`jqdcB(Qs||Hx4QHIc&HTpH$9;~lHmH#r_1)L& z)vRJ|s1s{A!=@WII1gDH)JP5cba}m+xvUL!Vhv}EojtuVwuhhRr44GNUYowWUd@iy zhB~o^GkWyAzw!Q4ex8>$sF7N6!SZ@F16v#F#2U`9_xT!_rh~6tZBQfi^Zl3Ct6ARK zP$$-K#zA*XX>@M*d0yI}Mry`8%j(rka&4#+YdB;1s}mc~_VDw(v_Xy3wRbP8SF_c% zp-!yf44cN@7#r?O)dn?E9nM%*uV%z+L!DT|8N1K9vhlC`d=9-fsFB)f_ht2J*1a~= zi8Y*I)A1W)!hOQpphoKTmtU(_GyAonPORaKlX{J5Z1pd{-&q^fNS!h9wR&~-fHu^L zHJo9euQV`C2Vc9|phoJp<6o;+XDDbxomj&eKi^?MqvJ~#l?-iABlYV}uhpxwAhe-Q ztl$sF9jA{?&SQHitIU zi8Y*IpFcJ*O$X11HmH&6eDJIF>WmR>s1s{A(yB)+E6Ff zaE5&zJOk5oa2?v9M(P`XURtlte9?wFv4%57-_tguOPH6T4Qiz9#F%<@c8xaFi8Y*I zpU=<0G#y-rHmH%BzSGirbq0?%)QL5mVW;-YXod5*)^Nso!=8I&W;l0A8`MaRJM5Kub+(f>)QL5mamhcb z^Srb{jTCqC_&ynJs1s{A1G|te|8Vn1>?t3Qy*SmL8`nsMJxr|)8djq>(X&wKPTT zjG4AiX1smwbUp|P}A&rbFtfeVx zXY{lEiMRG|D;erggAw*BhU<_<#uV1l6ty!Nw$Jjd=T?*qb*RC>p38Zc@H(WCF@?1> zMeU4ZY`iQdgMq!gPv6JKN*WncSW8pX&ba2mi<{FX9b7Whp#}qcl84PC zLmC-VSW8pX&KUCIMa{>%4=5SxP=gWnR)@z*8W~eqOHV2}9V*LfY% z$e6-fnxb~bw`{k1z0;VIp$;_|*z5lAS~8@OF@?1>MeU3Uwokv`KI2Py$AKKvjRgJ8B(X&wKPTTjKgM~+uU{Rl#-zi zH5lQZmhf0fBV!6{X^PqzU5-7cx!D_kEE(!hgAwk{2@GjuOkpieQ9I)nyRT@E@84fC z)S(6=++!3N(#V*?TAHGE#-_bSH+TA<=_NxQYA|rG)WmUoKBSQ`g|#$A?Tq8EIIDT| z+cQgsI@DlH@DtpUCB^~8VuZPn`N^D zLmC-VSW8pX&UpB@Cp9$2$xw$HjBpQkU`Qil3TtVK+8JkEctUe!|4t=C9cnOe zFZ!*0_yn`k zH5ix;uxt>oLmC-VSW8pX&Un=38g%NneaTRV8jLXGAY6wuGN!PWrl_6KbBF%TXKnrt zzIN521_QGi&K}F_kVeK7*3uNUGoCo{@aCyQb}1R^P=gU>PK4`_M#dD@(iF8bZ0=6; zUYoyz>rjUpj4(SRFr<+&g|#$A?Tlxp7R}R(?j=JVYA`SZw=)XrFa;{nYvWA-f>>QI9bX5@tHkVeK7*3uNU zGtRa3R~ZLiSL#rM5oY}ahBPv!u$HE%opH83PIuem1%^7*U|?p^8!P!(Nh4zlYiWww z83)_*eRiWy$xw$Hj4*pCT!%C=rm&W#sGaeHDLtA`Je-#db*RC>45?=~X%7r(WK3Z# zO;J1J>3a9(v41Q|hC0+>U>4Ts&SXd?fzh|5F`xXp!sKLO@y9NFDSV<#e z3TtVK+8OV!@7!EtuN%NnhZ>A9yDwaaG|K6FpkpmfQ9I-HBRe&_UUWjqP=^|fFoQ5K zq>(X&wKPTTjLE<0*lb%duwZLIyh z28KG+U|?qI@N3DCM#dD@(iF8bj{fgujjr~0Cm8BbgAr!GhQ~@88BQIA$S-cnA#m7n-8Bw=)Xw;Vy&v-Y~-^a;B`nNV+w0& zirN{Y?wa2C?cSqHhC0+>gc;A_I;4>?g|#$A?Tj~$zQ1voy-$YgP=^|fFsnK+q>(X& zwKPTTjQ;jM-=_9{9~kOTgArzK2Zl551_NgVJhq$+X=F@cElp87Bb-)HGSs0417{sfev=Go zWK3Z#O;I}|oVrjl)S(6gXEyYDiwtRGOkpieQ9I+9I}UD~XrFtPUsqwR|7?V_C&F_s zjZD|l6ty$HY@gd5WS`%Gp$;{!Bb=cT7}Cg?!djZ5cE-#9wO!+@_BkXN>QIA$vp_no z;bSF@j47<8DQaijdsm0X5c@n840Wi%2xppv>ySpq6xPxdwKL`}S~25(`&<|db*RAz zXR`!`G%}{JmZqqkF`{4FjDhy~HW=zqgMl+0o|2&sH5fSaXX2-1NF!qkYiWww8QV?kJ>yo}H-YO= zhZ+o=T{Lt(8Pdp@!djZ5c1FK5pMM0?bih!D8jNrTQ+Up$kuim}G)3)SsBh5#B`XIg)>&wZ{FdJ@ZUqj3izKo z8#cWCpNwiPZ1OFOM)6px`E1zFJR=o1h8sqKGUU4>TI74?KZIoq& zodQ-qCe%n>{?PjRqV;cw$1T;+6t(}WbT^ec)L?9R&ieY$&lstOrl_5vyTa6=24ixM z_4SFL@;XutO;I~TceklS4Mx9LKdoQ>2^pz|rl_5vyY$qd24j<3KCO3MM@FilDQaiv zZbNmb!B~9!r*)eU6rS@`LsQhw&|Q=2P=oQ@S3a!|`GAa6LsQhwxcsONjUPPYds5Y* z2IHMspVT*7Lq@8hDQah&^2hBOy?Q!B9cnPXGwzf6@YQ6b8k(YZhVI5yhZ>AWzWqu4 z+*M?x8k(YZhVCj>hZ>CY-d$Jk^%fbahNh^Up}V8ip$22vpV!qVzez@_p($!-=q_({ zsKJSu#=$ zO;I~zx!nJu4mB8uZ2ocm)hEbEH8e%-481Ew9cnNxnfpQIBRrq74<&XdVVH8e%-480pr9cnPnUH3tK$OJM{4NXxyL+@%-hZ>CY9{Hd? z{8}BImwr(H+LdIa8k(YZhTdhW4mB9xseMqtWgHo)hNh^Up?Axw zLk-48*$4Hf#*&e0Xo}hy-+E)CX3v}aK4EpJ!5H3JTc0tCj8sEY)Xq4(y+iZQhYT*i z2i2hlwe$1I@Dn7am3pC{J~_T8k(YZ#^PG1W|zzT z{&RJx!T43Dwe?lUkdbO=irN`^SG+pZVEoTBYw8R7k&$X>irN`d>~qdxXZSn-b*RDk z@q{(?(S68BH8e%-44qP-4mB9>oxG<0#6D!C8k(YZMz5E;H9K$P^BL5k2IITk*VI>b zCnMF+6ty#SYJ@t}U>x)B_v=q|BO}$&6ty#S`h_~wV0`Uw@7G6nCL`6*6ty#SiibMX zV9Xi+e*KwE$VfFbMeU3UH}+_LYnIP1QHL6ggZsZ<|M-J{hrb6?4NXxy!{+WZZJJK` zvqT+gF#g}R@7Ld4Nk*!nDQaivbQpE0!T9s5@6~U9g^W}~Q`F9|xjRjprc+*rI@DlH zyXU=nmxW}c8k(YZ#+*A3XdZKl&-YP>8jOjTzE}T2I~l2lrl_5vQ-{={24m=e_v%9) zCL`6*6ty$@|E*8+iy!;EC3UF5=)B{5^!B}wZ>iVdkk&$X>irN`ET}&Nn zFpeF*x_-`BGExmqQ9DDYq^Uy<#uMLOT`xwGk!om)+8H|SO&w}5ip^KomklB#)zB2R zGjwX5I@Dl1w(Q;d!2V>U8k(YZhECs8hq}CuN8YX9)`yH#qmUG}GjxieI@Dmy|J}Rw z2YQl`YG{hu89EJ79cnN}j()fPhn>ktH8e%-44vwz4mB8?AMtMe^exCpH8e%-44qD? z4mB9Fc7NACLm(s7&=j>ZbjqeW)L{H(qj&ASzPG~f+EhbR)Xw;S9}aDfJI3dWszVLN znE!jH{>Uq2q#Bx{cE<0w8P@#!P@j{k4mB9(G~TH%TR=vtp($!-==4@~sLRLdHe)WBqP<(6ty$-)CF~@!6^E#st=z@MyjDHYG=%n z^Cr}x2IJt`s`@kck&$X>irN`^3Whq=VEnlEs(P=)m7(d^6RejziWTYCJqIQO!uA&Y#7^AmcRp0E#WTYCJqIQO!5~B_^ z7!$UzpCyCHNHsJ??F>EbMjdJ}KJK`xKISkoQVmT}J3~+HQHL6g+d8bO-?Tp&sfMPg zouQ`>sY4CMPrtCL{>azJNHsJ??F>CdNgZl1ZnYgZFKQIBheL${L zjnAii?~gM^zFX~E`uxrs-(`b!cm4Og);Db5(uR%CI~z9qU-io~s{L2mphoJD7w5Gm zf5y;;Ickq(_{r`A8W-FA9ejUigBqzF_MF!m zx-A*nP$$-K#?${hq|wvPsRBbA)JWaA!@O4Bl?-jD6Kgo*#6iIus1s{Aqsyol8`s+W9Xub} zphjxPd~WOaBgoK(I1Z&sy2IWh=j* zQybJs9kR>Z)=TG;p$&Cn4QF8P4yNhgvC;-LQZ}=;HGDi7+E6FfaE9D1J6;>qo|@a* z=PEL^k!6(=Yk2z?cAxOs;XYw)P$PBosyVGgt|vnqpLd+64QKrDu@22+5A*w-wLy*4 zkfn246K^3y8|uUw&Uo17?)=c^@8J2+1~pPgJ~OAa-~W)I4RvA-XB;x1Q?s|t-vL7# z)JP4RIj6PdJ!EJ@omj&eZ#-Av^sogPIwPM8|uUw&KPfVSoR5XShPWn)RaT#w6^a;hBnlRHJow&w|h13 zzt88%XoDK5KYe>n>$F|S(1tp(hBHnZy>Iiwn|v;gHmH$Wx#ye~{?Fep(S|y)hBL4} zT6jLRL5&oy0V_B|8|uUw&cNe@s|Q0H)JWmEz$4)dZKxA#I0Mf&o<%UUL5&o?rtpk9 zLmTSE8qUDiExsDT&;~V9_};)*rZcpmPORY!eE;FQ3Jh&fBZcpAe1|zh8|uUw&cM$R z{B!_A8`MbQXBB=@I71ui#2U`P&qMr_14A3sNa1HLe&RVp8|uUw&cM%g{Imu`8`MbQ zcL#oQJ3|}l#2U`P?jk_D07DzpNZ~aJUJ*D$8|uUw&cN#)yqW<+8`MbQwH033I71ui#2U`P>o>eA z14A3sNZ~aiUZFWd8|uUw&cN$Xyt)KK8`MaZuXXJ;uQRlvPORY!yq?CZWH7WrjTBz9 z<2AN3w4qL{;S9{(!K-~Rv_Xv&{`SBte`jbzomj&eGA(qxHmH%p-%$7q#u?gBC)RKV z{!YVRU$_o!P$PxE1@V`dGqj;jtlQNu`{%xPORY!{QZx=>cP+kHBxww0e|5;LmTSE8qUD`7_@XnDlw4qL{ z;S9V#hIhTd&;~V9cn=QmfH^}O>ckq(!25i7_YMqgP$Px+67f!+Gqj;jtl$g~8AUHBxww z8t)K0LmTSE8qUD`;COc%3~f*&h4{<1~pRn%mDA$J3|}l z#2U`P=Mwm|0Ss+WBZbdC@X3QSw4qL{;S7Aff=^Ar&;~V9_zVZ1pg2Pt>ckq(z~@Bx z^al)WP$PxUqVP$OGqj;jtlVU4f6n!>QYY3RLuVmX&mf;oPvO5W3eUMc7x>FA{7)Tk zYs0nerpljtFpQd#RCHWdFU|CGAGH_NUd~iQ zQ`FADIgK_IsAQ-^4aUhIv=`N$)Ko)L)XuP7togb*9cp;a)u9Gs(>3iywYN6a&=j>Z zaBilZhFLPyp$6l$)$K*KM>y5c6ty#KS8?9jrANt7hZ>A?R<#$^UguOpQ`F9|-O;&C z1u7ZpP=m4eTkS=)XFJu<6ty#Kmv{c7y$6&Gb*RC({mu5G+IyaAXo}hyI9JxD0+kGP zsKI!2d3#aqp-(k5MeU4t-n}5-?e5VfLmg@`Zd}@4RQCd;8k(YZ2G04l(|b#XI@Dn7 z_)>dO-P4e2Xo}hyb{9nc>wjEXGSs04P=m4biT0wpS0>fa6ty$#?wWkX=qV*b9cnNRc)Y!+ z?)gbIG)3(Uoab#*fl7ut)L`VT_M*CXDb>&vwKIM(e@y<<{Qi=m4mB8`Om8o$d$3Xs zO;J1Jpq``iq4!TO8R}4jG3J5xqPmwX)zB2RGjLA4o&H`j)S(7r;Z z>@K=|WsmtKLmg@`F1)L~sP64cH8e%-jBVc@nJ*Z=uwb_$&nwl?6ty$#ZrPmo*Q!Ge#-wrWMRgBvs-Y=rXT10P$+=Ah zDjzF#sKHn;7O%B=FLJ7(DQaif-OKrz?`>56t)>n&7>!ZwMRiYgs-Y=rXV_iX`7;l9 zC>iQdgE3-wdr{pRo@!`{+8I~AJTTvBQpb{^4mB9_2e%j1J?^Q7rl_4^cg5#p4((Jj z)S(9B?PJ=D>R$a+LsQhwu)F(nn+jAi)S(9BsebK6HFF@<&=j>ZY)V0X>>su+8R}4j zF{Y1w4}LZ~A=S_nwKMj6@~GUV0+kGPsKI!0pZ21f0g-BGirN`AH6p+Jsct1h9cnP% z>fT;dvn)~#O;J1JAN>!{|1@fslA#VY81uTd7u8ITR6|qL&af#S`F1buRx;F~24hs` z_M)0Cl4@v*+8H*DB)`0O_mZIwH5kuq(q2?EQc?{~Q9Hw?s^px*q7F3}AAc~rsAj#S z8k(YZhE0dbIZs9%YA{x;oLy8iYf=qOQ9EPb2M@~szHhJcu~LT`j9XrrT~xDoQVmT} zI|K8BY${O6P=^|ftryNNsu@D5hNh^Uf%Q43NF8c0e$YO_pYU4;kCi&qU@RYG&&Ox8^imB?Q9EPFgInkCo#ONQ)S(9B#Qw93Y9?T+p($!- z4Em&V{@BOImDiyTH5iln%r2_ghN*_8sGVWc5OdB?REHXj2Yb#gsu_)`hNh^UVN)G* z&Vf{i8jRoXJiDl7O{N-}qIQN&r_4FeQXOhAPTyj7QO(RuH8e%-47{Gkt7Lp#sY4CM zM%nD5n*Et-Xo}hy_`3kF_Q6nx8jN#S%_^!Hrm2ResGWhoZ}3+O80t`iG2@k4MKy~x z)zB2RGi-WmZI8~wN`^YrV7$6uR#DBAO*J${?Tm%zE~`D%^|X?q4mB9dADvZHvvE@m zO;J0;rs>vfDp1K#hZ>BwduJ8ZjNep4Q`FA5^nismn+jAi)S(7r{N!0hHLEz)&=j>Z z@ID6KoxpRh4mB7%UOTI(W-g~1nxb}wO=+%iuCqGSVB9=zR#DB4PBk<|?TmLWpI)=6 zK;?C)Lk-3Uqh}S>4D3`xQ`FAD`(t?53y+mL)L?w)U12;{>QIC6-LA8WYSw+Kp($!-;C*nsyA6gq)L?AdaaK{y z>`yf`MePi{pO1Ix!BB@9jOlCJit6lvR6|qL&al%8YCPXS9cnNJz1~(-XDFl^nxb|F zK3~D7Cb$lDsKHpVu&t=hf=D$qMePiHPJ~Z?z)*)8jNvoeit0>@R6|qL&X_v!;M)BU zUR*NNp$22YeQow$ALs9;8k(YZhMne7n>Fr|lA#VY7`-O971bFdsfMPgonfbv)Oe1G zI@DlXadlf!ot2VmXo}hy@Bdea+6zznSt;sJgE9Y-wxT-oCDqUrwKFE&v7+y#_PH>g zb9Jb}==kHdqB^@K)zB2RGZwbB^QI9*`mna5I!h?k&=j>Z?DV0&E!%H^$4VV)FmBqvt*FjKN;Nb^?Tq=S^zLg@fl7ut z)L_i`T3b<_?UZV0irN|2_krCUxDIuw!T7%|+luOps#HT$)XunS#a4YzwtXyMs6!2g z?nuhAR6|qL&cJReoMvW-@xK3tdv1J}4H%2;{5G6&2AyTnuu`=bM^n_!z^*Wy$_9oy z)L0j1#No6yXGkMs3TtVK+8Nm0hSTN1P=^}p;;cKII_C^&WK3Z#O;I}oyYz5M9vJFS zV_lrthtu<%A&rbFtfeVxXW(2woc0HXI@DMfXAj~OKxat9o*SoQElp871Lqs!)Iu=S zp~kwlpQVM<44om3j47<8DQainoJO3!2!=Y;Sl9Nmv~Vh;Go+C*g|#$A?F^hZiBlxO zP=^}p;!I1NF6j(uU}sH9$6A`Ab_UMP#A%pds6&l)Z9hv3r(`-q8W~eqOH;p$;|H#hI@-ebpJ#$e6-fnxb~bZMMH1r^SMy4mH-r*|j)D))~^sn8I3`qIL$( z$Hl3)V5mclb#VqSPQ!JEG%}{JmZqqkfpdOwdM_C2P-9)&&(gxFzRr+F#uV1l6ty$1 zvHK=)3NaY!P-9)&&(gx_#LkdL#uV1l6ty#O?lMkO216ZctZVyOS~z9d8Pdp@!djZ5 zb_ULm#;MR?s6&l)aYi*xi*|-IGN!PWrl_5PbFgu`H5lqpV_n(X&wKPTTj6H3B&{u7a z5T0{&sKJ;tuDx~cDl(*zF@?1>MePi|>sK9WFrFIQZu{F`2wzvy$e6-fnxb~b`F6ka z#o>Nub*RCZF{-_F`W9qJBV!6{X^PqzulF0=?9lT2w$-5q!}hbZe!nvr(#V*?TAHGE zM%#*k&B?#<`_I*(2E+EVv>xn9hBPv!u$HE%ozeB86Pm9d>G#pALk-5;$F$r1YGg`#U?GN!PWrl_6Kw7CYmhPejnP=jInSz5~m zks*zYDXgU_YG+Kc`3$vwKA%AyYA|d+ORE@3hBPv!u$HE%oiTs={>|eSb+f-Ev+?Rs zgR!!Ed+Y46WJn`p3TtVK+8LkNyo^1d*n z^SK}DP=hhLb9?K+>&cKt#uV1l6ty#swfQAa*L{A8I@Dl1yGeWN$=k`0M#dD@(iF8b zo_IKK{$NUv^7&AQ8VuXd(%R>4GNh3)g|#$A?ToW+p3K2vo{T!wV7#$%cI)Ja$&g0I z6xPxdwKMMi%R$Yv5A?Y>>QIAW`&n8iw38u?j47<8DQah|y77SKxnq33k2=&~*nXDQ z)(gpyM#dD@(iF8b#@PC+jf3wGb*RBOp?!AimRHD-M#dD@(iF8ba(kSX*y9C;I@Dm; zewNl7E6I>X#uV1l6ty#E*z^72j6Nkp9cnOaKTGT556F;4#uV1l6ty!hu=zpPg!xzM zP=oR0?Xz3YZn7l&u9Zf{6xPxdwKGn)`9aT2Ez0XqhZ+pq&(a#*nG9)UOkpieQ9I*a z`#FN24tPG)p$5bDv$W=SBSRY4SyR%nmZqqkajN}1d?L(MQ->Oiv&YVEt?W*QG%}{J zmZqqk@vO}c>KW$4sY4Bh?PqB{xepoA$e6-fnxb|_C;R>MXqa=S4mB9d2FC82kOp?vlyqN9Q?C8LZ@tU9#sr%qgzMm1|JktpEUii7$dHDWskOp?vlyt16DQag7w!hO(v%lAH9qLen@!En}t>M>_A&rbF ztfeVxXIyaJvc@s?cPAL?P=m4T(OIn_6UdN8#uV1l6ty$HaMp{Br^EbRb*RCZdGD;& z7L&=4M#dD@(iF8bHal)%<79gu0N0@oH5j&^rFFqwWJn`p3TtVK+8Mv=F~4zSn1`$m zH5j&^r8RLH8PdScnv#ySG)3)<7q@M#dD@(iF8bcKV;` zjZMRRYIUf=u>CBpPo|S0jf^R*r73D>?D75k8@Gfx+v-q*Vf$HHd5a8bU}sH9$6A`A zcE)D*K3^AmzYov3I@Dm;ewNk&kCPz{?5rv2SW8pX&e(PAl*VCU?zuYDV2tTAtM%#= zWJn`p3TtVK+8GnqOl&-4@4whBPv!u$HE%oiYB|D;pPwdHU*5gJJtwT06c(hBPv!u$HE%ozc(c2Tcg) z0;od`#=~pdS~n~uLmJpwQ_``Prl_4!?=+_It#H19I@Dkce!Z>r*m5$Ykuim}G)3)< z5APV#xH_EEpbj+{wx6Xn`AssUkuim}G)3)<$GZ<`3<>8=s6!3Lu$gVGUT={h4eYEb z=~zos)XtbT>EOmS;oJ;$sKI#pzP8pmtH_W>#uV1l6ty$Xvd``I59fcVLk))QXK9_b znha@VOkpieQ9I-L!P_;y8O||LhZ+pq&(i9+h74(BOkpieQ9Gmi)DDef!g(y}P=hh= zlD5{#ACMu9j47<8DQaiD_1uaXjc~4vI@Dm;ewNnokI0Zl#uV1l6ty#M?cX+|UpOB} z9cnQ0L2a$B>&TEs#uV1l6ty$jr%j&mQ8?#E9cnOaKTGSUpO7Jqj47<8DQaho-~6H( zGi|>Ce(tJ64aS81+gcMpB|{q6SyR%nmZqqkap%52E+EVw1%!HLmC-V zSW8pX&baEF=O6i%?f(En9cnOaKTB)TXAEgzXH7}RTAHGE#^oPY=U}Nr4Mvu=+57D; zhW7xZft@ua-Ph8TYya=|&(CoU_J3iX%zT+AGrs@>vt@4H;f?aYBj@vj*r4MRXg=TY zf8l>Jv;j>9XW4AXpu>NOHpnPjcs}fZdWF@MpY+sysR&7ut_2`}R zisc)Vp$&Cn4QJ?XTy0Pzwf1-Oid#F7p$&Cn4QJ@CVr@_(wci!<3Y)+fo)2xP6KgmF zbEz*#sKwOf#(4RvA-XI$3v;MxPzeV@KIsF9k~XI^0wO2hM^4RvA-XXxDn z+Mq_N>z?xpo3t7j+E6FfaE9Imp$%%J?%H8qQR_;EHq?nVoS}DfXoDIlo4s3{xicBs zP$$-KhTfH;4QixphH$ZbH!`%LPORY!y}L#m)JQ$Ld~RWrh{N-t4RvA-XXsr*+Mq_t zW;z!(Q8_TQp-!yfjJey~T${hE-yfw7YNW28Ik(uQHyPScC)RMrRwqxX9W}@A^U?-2 zQZ{3|unE`UI<%oqtlV}8QM@M)^G;qQeoN@7}}sl%4X0PHUU07AKFkS)^LX2 zov96Kq<(tJ+`=Z=2ZlD(i8Y*oxm1`oh3n7;HBxpaK=JM=WN1U3Si>2ZOND7uU}%FH zDLdPsm@+Mq_t&MGRdpGk%`)QL5mp;J7xL5-B1xm5Iik_>IA6KgmFbEzckq(xay=`@(TwYUNW>njg*~%RqVTr3~i_rYdAxv!)SvVskx)(6xY2? zhBnlRHJouz(LMj|RG*8Z4QizJ89Jx<*2iRML!DT|8JJ6jX;XMUv_XxOovl{b$!6hu zP#fyR8qUC6DomRKLmSjc*%@)g4qeF5hB~o^GccD5)26`C1~pQD{`Q>0PMQnXp$&Cn z4QJ>SC~Z(9WvAs8_&3QuyA$SEe(xp-!yf41E9Ly9!@d+Mq@X z-{bfWbA~q5i8Y*|Q|q)rjTC-X;U|SNw4qL{;SBsd#7{X~hc>8@!p~g%#B+u=)QL5m zp;H94L5&oCci<ID)HBxv@f>#92(1tp(hBNTG2d`$p&;~V9cx{DOHqOw7I1QrCA%)Na1hu zzTYQ98|uUw&d_Po+Mq@X?=k%J6f(4-PORY!otmu;YNYU93Ervj$4VRO#2U`f>EGI* zMhfrw;2jrdXhWS?!x=ioTpQF#;k_%obL0$ds1s{AL#MH8gBmHk2ZwjSoS_YMVhv~L zRC#SsBZc=8@lKvIw4qL{;S9WAiFYCK{hckq(z~^@O zG!6`HP$PxU2Jy+9Gqj;jtlckq(z+5VP%8SQJ8`MbQGiQ9_>kMtE6KgmFbE)uYH5l5UMhc&u@p$%%J zuqOjMBAlTObz%)?=qY2`phgONOR%%U8QM@M)^G;qQejsLu0tEtNO7l|Gqj;jtl^9k zvn~2y+7uYNvxXWe+r3v*dk0euO;P*5>cBIf?}KSmV5mb4hV4SMSz+7*nQCZ?+8NDz zdiR~=`z*t(uq7=xOo8FE$;e1GG)3)$UFrLf zvE#Q~3l23HnB7MeU4w#~!tRU2;kJ{h9Vmkf2N!SLCZ{JKgtG)3)?# zF#FS=bEd$+tkLS;j8sEY)XrG(#WA&O{Qi$}hAGy9!xR`kGnLnoYG{hu8KZiQul?2U zW69K^2E*=FDXM#AQVmT}J7c3oSJsx?Gq!v_)S(6gGiheVG0bNDa_|1)zB2RGp3$3rS{_N zbIZp{9cnP_Zmy!bmn_xL6ty!>{MR3Am)W`Z_&ul&H5hhRT2b8-muhH=+8Jjbdw;E` z-*;Eej>cMWm;%G@t}Cj0`%(=}Q9EPSr0KOk?=!l*4t1!(z|8IH9>r8cQ`FAbc=gQM z)t8@DGSs04!|rypndE%VQw>c~JLAlr^K0kLJhNn|Lk))AwP~}}$w)OcMeU5Q9=Wi# z=lkV-sbRLdKOanifm!a=y|1Z;rl_6q#JMlldT&0Wybg7!!SI>%ypB{uQ`F9Q;gV&w zgSId46VBA32E%9TlaXp@irN{MoW8R5wXMthoilZ)!LYlU?d$aRs zZfN;fsY4Bh-JM-j_lBn$nxb~b4PDpOURqGze_o!UfVJQ-1%{v5!0SjgG)3)@yZPLYOdV=4Y`RBL%@#>DG)3)%RKWyEF+sKKylL`5|VDb>&vwKHZM+$;a(J>|S5Og1Xlfsd6ni)L__@ ztD>5*m1<~;+8J{mIw)V?tDNJ7iCN`ZaHzq+*>}~fuv9}+)Xq5Zp+5Q25BDm+uGFCh z!={cE)y%V0LsQhw=-0^e5x13d)iA-VTni307=9KZzjjj%O;I~z%CsV%*{7Tjm#ISy z#wa_hv6?}bYG{hu88(+H4|DD^b*RDcvmtpMsfMPgozZsm;rXX$>{31->QIA$GcK!{ zfT@P2sGaeL9s1`7*?cPejiU}V7&cwd&H&|gq#Bx{cE)5ozin(@&QHXo!g4J*)L__@ zL_5orj8sEY)XrFH=cS(T`>o6CP=^|fr~0)Q)vU=>LsQhw_}SXy@}F>4&`H|4mB8lwlJUbR6|qL&iL!~C*|#LZ&WhW zp$5axNG2oI&=j>ZragCZ{+o{L8u{fivB!8Q7_Yrl_59(5y4_BlalgY-j3FgJILO?aX^#N2;MIYG*w7i?i~bUMS~< zV={NS7946Y#yrqoR5Qs_4NXxy4&_>KsKK~#X?szfX_0DZirN{I-n}57_LtG+^Pvtk z7>_P*FRHUSQVmT}JHt-%$iw*`nL5;9-2P^JQJpc8YG{hu8Fng39?mhz)S(7r@3-u0 z;j^<+QVmT}J7ZHjzpcyvmgli#>QIAWr>+#$nJ=k^rl_59h3%7h>6_)bGC0AdTni30 z7-+8G-?eOZ3k z1Le6(IN_*V3l23H-PW}i)!9y|hNh^U@z(>#<=Q->Oi@t?F8)frW(hNh^U zVW(Q<;T$YG0L0gVLk-4+Puq*?tgTc-Q`FA5{n2sxc024>UWYo=V4Q06mZ~$uQVmT} zJEPssZ~Mz}&*5CMOdV=478#>D`z+Pa6ty#MwS6)_diBdCLmg@`d^Zfw5=u2RMePjy z`#EiD{rZ1=Hcy5c49=F>u%S+@;qCvNO_gPSP8|Ow)pg7-KToLsMNpn{C)RMrBzvr$ zd*jRHo+6vCQ(&Ku|9f2{wRrs!dp_FASy)+?YG{huSNe)Q=YO8NsAQ-^4TkOYv9BvK zQVmT}J41JDsY4CMgin_g*qh^5o@!`{+8Mg@OdV>hYkPg{`<#qaLsQhw&>eK@P=jG} z^a||F@#{!6G)3)L<8k(YZhVEQehZ>CA-&|s^6Uj(5 zG)3(U-GQwRH5j%(r@-DEf2>jsO;I~Tcap0^jdg9FY=ONw&PX*hMeU3OUpzmrZ8N@n zKGdPcy0+KH{$`lP=RDES6ty#SXTLhsVEk~A{VcIPKYkslhNh^U@sBb8o&V+P>&oj; zhZ^hFpIu^q!;z6{Xo}hyI5Qk4f#Yje9cnOa-g1GxIes0fhNh^Up?8d^LydJ0e0+)h zO-)9sp($!-=$$X>P=jInbL_nVGExmqQ9EPs%rW`Iv+pk-D|M*BST}u%y~jaDs-Y=r zXS5C&oxgMW^pc?tH5j(nr@-DE=qyV$G({aAt4q(yAL-OqGSs2Qx)Y`?vG<(F=#Xe= zirN`^XP7$FVAx(CdmoOBR6|qL&iKQJBlAlSSXf?%I@Dm;UY`PcbNsPNH8e%-jGt~g zBA@!p7fXgZ)L7T%^%vNi4|S-) zu=6bJwf5|A9jS(MMkQjDQagt_VID~ zpilfBcXg=2u)RL^`5+mohNh^U@h$rd^4LH4z541eU9I?>QH0dC-zxlpRtmWYG{hu8J)N3pZBsoKVYas4ThbERA6t8Uq`B;DQahY zfB50~8%O&L2z98juI=?Hus6pUsfMPgouQK{)SXE zQ3kCM(_+yo7Xo}hyI)O$VYOK4>!WV3h4jHM2rl_5v zlXTRf2E+FH*uEt)QVmT}J3}V|sY4CMDG$F;U~i5;R;h-jsGXscjntvWy0+KH_I{C( zYG{hu8Th)zS0jFws6!2gozqxgZ;oF_s-Y=rXW;t}-&J6!LydK9uTL?mGa0Fdrl_6q zUr+6p-*fe$B|{x*FwXkf3kCM(_;sWjnxb~bpGNJH|D*rmB|{x*tZRFH3hd2sMyjDH zYG>f*I(}N?u~LT`>mD-l1=}x5MyjDHYG>#qHg%}MSU%_l`<#=ER6|qL&e-mzt@BA^ zk14N19cnNJ_J5(k-W-3dQVmT}J7eT#TjuTW9#=Bdp~kwl*T?oDlaXp@irN`E;ZGfE zF#gu_1=~+eMyjDHYG+_34kq2;Iah}o4BP8t`@+dcH8e%-44t^B4mB7fws@hy-W=#G zOEok_?Tl}|u~FXhrjyF+P>0&reWAeK9M`FaoiZ&c*Ur!hm`u4-HO^VJ*zPwVBh}Cp zwXd{h#Cx^wtB01?p$;|v?TuF!7ucKQ*O6*yirN`EkyIUOtZRFH3hd2sMyjDHYG>$V zRduMb?(#<$7ucKQj8sEY)Xo@q*o(Dmw)dH`>QH0d*1e1EejGAV4NXxyZ&fjK!ZR+D^mJD^Mv2M3(7u)?&WTYCJqIQN(9#)4M zj0xiw7ucKQ*O6*yirN`tXHTzp^nSE}_P=^`}+v`(c zZw_>pr5c)|c7{$ESBDzw4&QUJ-Oouzs-Y=rXXvDKb*RCxy*_qdDjBJUrl_5<`yd-nX2p$;_|)7LI4ur~)f%Tf(ZQ9EO| zyGPgFu{}TdU8@c?*0sGpHbaPvR6|qL&d`$t)S(8$_WBgqo8#A!YG{hu89(1)K&|6T z7nRqc4mH-by*>r@<~SqO&=j>Z{&x4lwOJ2bTr$+5#=5rG$L^yiBh}CpwKMdD3U#Q# z*k|%0n+HHfs-Y=rXXr^A>QIC6v#S>s*qh^zRjQ#WYG+(Fy+iH6aK?x_)L7T{`q+F1 zGExmqQ9GmU;T3&%wLL%h+Es@d4BP8tvmwYxH8e%-jM4YB_3dJNe!x(N8VuX(Q($k7 zKUS%Rrl_6Kdy~n1@3K8VV5mclb#1RtfxS7-NHsJ??TqJaCJuHFfT0dG*0sGp1@`7R zBh}CpwKLXiwqM`>wLL#zs6&l)ANks%0(*0uk!om)+8O5!d#=w++y4QEI@DO#_WIa7 z88T80O;J1J*Nea02RnGcP=^|f&+oEHH8e%-3{29wV(C`po}vwR*355#5vJ;dy*S!X zhiz>@XBl=neQ>PrjnW1+Qeiq!U}!^~Si>3ruvtj|9^AX!x| z%lC3>gBqzY%`80U+E6FfaK^5-Cw1ex?@84LHBwxAq}J8|uUw&bZk2I{#$%0S)}T(grnB znD)2Bo@8i4omj&ezqUQw-KPvG8QP#m3R4U3+<^>js1s{ArmTkR(1tp(hBHRmy)p~Jy)xRMMheqnXWzl=(1tp(hBNlEdw#YI z_xxys8mTb#He81`)QL5mG0^T^`e(RzNgLEiVS4X~@w^Ugs1s{AW18KA^}TQpmNux7 z3R8%~b!bDKSi>2|+r4B@?(X-JX@eRmOj90z3a>*O>ckq(_;}^WywhmECr%sGNQJ4; z;X1UTPORaK^;?g~4-fbDX@eT6Fx@&Zw4qL{;f!zWbXwl&48KQF8`MZ)O7~8C^Rd!~ zI2=w0n^!wftUWZBQeH zY5YT$&It@{s1s{A;|9B@y4Uago@#ASBNa{+2n=ng6KgnQxZN9makw{J8`MbQbcA6u zc^%qNC)RMrYnvROZ@#Lt{k7xgG;l4pkqW0ggzM0TIs1s{ALnno3gBq!Dx=whkw4qL{;f(EVM#|Se+N*r5 zv_XwjI3*}Bw4qL{;f#B1)=Rg2eAbILsFA{HM}PV@A1iIB6Kgo*Ae&jUSD0C&4Qiyq zsZHTJw4qL{;fz@}N$1yL_Kr5FkqW0z1%@`%i8Y+D?RR?R2ZR|y+Mq@XXGd+{g^!gs z)QL5maf;1C8XjgLX@eT6a2i;+4sECtYdGVqyL#mPAI!_ID{W9Cg|nl2e*Ab~XhWS? z!x_K*cK3Y$zxZq_ZBQeHv!kwmn+$EJ6KgmloGe#fhc>8@!r4*#Eh9r4>ckq(cxLD> zdFLPctT1g*BZaf0wq8tzHq?nVoDoj?E3ZQv)JWm%sC}O#LmTSE8qRpyX1DDfX18gB z8Y!F|b=^!dw4qL{;fx*sxOM)OFoRAT)JWm%s9t|3LmTSE8qUzkc-o*w3TH=cc@G)d zP$$-K#`eeB*->F8pf;$H3a4U*?+ckq(_|C=4YLmk(R&7utg|nl6KY|Qxs1s{A3JHXc)(6K3{ngBmHE9hG+_LmTSE8qOGa*N|HA?+eSzCn(|h(1tp(hBJP&_kfz+C0#PKL5&p7j#2U`n{nrQAjt^%+XoDIl zoE>$|7G!8comj&eJ?u=2l|0iz)?yneoE>$`SIE$YI zM>4dbPORaKi>7s`?H zw4qL{;f$3hwDtXZIP*mt)JWm%sMj|pLmTSE8qWC1U6cEE3TM}7gBmHE9X0L?WN1U3 zSi>1%*Gu{Rp$%%JaCVgKBnZ!kHq?nVobkK+d-t6f&JxlFHBvY`YH~(~Hq?nVoN@24 z=lfh8&P37%HBvY`%64{y>(GWev4%6g_rd0$o$aIzYER9#_uI+P24>dKi8Z{fGixwU zCd=Bwzt^>=bj}Q?=-?gU|C~UBwl>H>a(wxm>%WQ`DeT_izZPlyuWXF}yyMrT4QJf> z#^!x459eTMgBq!@zbO0*Qw>c~`<|kmE`GkxRiB-MRqlZ*A1kK7z&@#=>)Qh()zB2R zGrCOf-S@8WwW|&_7})PM@l!HV4NXxyL%*Zcp#}r{)_(d48L5V*sGafNLzDY1-G5y9 zoU20(2KL`|T}MW$p($!-T=(6!z9YlWM0Kda2>S@b^O0(3irN|bE?d#}m*Ho-I@Dl< z{g8o?YG{hu8S8KFP`fSsj#7sj4D9RdxQ35as-Y=rXXtNEb*RAz`%}Ypq#Bx{c80zJ zP=^|fu+KIyQVmT}JLA1O53X(V;Kk+Fl{(a5V83v$xA<748k(YZM%cMrGSs041N)vQ zzez@_p($!-gdNx=Lmg@`uz&lpQIA$edsqVB_q|)6ty!p z?K{4<)j!TJuR|SbFv5QN@SLX_nxb|_*x6q))S(6=+!qiSsfMPgouPkusY4A$xW6GV zQVmT}JL8)lPOKdh{^nGN8Vua0@#+(NK2i-$Q9I+3i>K61ZS!-m)S(6=+;0-DBh}Cp zwKI-h{Kwio$NM>0>QI9b?wbjWR6|qL&d_%z)S(6=-2W38sfMPgopI=W(`&!)>E~dn zLk$M*V>Ht;6Gf4wgF9VBo&83-01| zq#Bx{4%czY!rFnWcn+4Y1&1jx!u@f)jzmLK)Xq5kzh108*~Qqs>|pYnz?UVVL| z{GFTpvk9i$sn*5)yfa3Tk!om)+E>y~Ow^&qb%gtp!()|dXo}hy-@dA2e*Hgr4pw>Z zboq6~6d2+D>cB`fG)3)aLr4BV1;Xd)eNHsJ??TkfV+%iA-eLn|F9cnNJ z+I{p3`tkWlH8e%-jMpY@onLhBG3D=bb*RAz_w9%4NHsJ??F{{7R2^zC!u)~2NHsJ? z?TjN{?v{T$oP(tfH5izqu(CTJt5ic%)XvaPs@0(e1M?u}cOxU!&=j>Z!eol_`A~-% z49v9{-IUS5Fa-wYb3D5V8L5V*sGafin|tIBJmz~Q)S(6= z%oz#Kd8(l)YG>$94t1!(z`T?{z z<=+YCV5vh5MwllQt|QgZ6ty#S$Cf(OV1&6yfstxxirN{v^GqFTFfiZg$=mr@r5c)| zcE+#l9IThaIaumYgAwLbh3iN)G)3)<>9%)p$aB7TP#tP8!o01(NHsJ??F`+~s17w4 zm>YKXSUy&%hNh^Up*u6xp#~$&KMU89YG{hu8RtB9RDRg8eh!v8)L>wa+p>Mm#3l39Ygn4w~I#LZyQ9I*MI|plNI0s7|YA`TY@76xNj#NWa)XvbI z%j!^<&xg+)4A+rrgeih$irN`%s|M!d#`&IYb*RAza}EO|)zB2RGdkPe^I_o}EOn^C z2=f{PBh}CpwKH@_ygJliVD4m=@%czKG)3)<`{taSzrC-YgQX5N7-4>9xQV4mq~3uXmIs-Y=rXFO%+VErVVgQX5N z7?{ht>`^jO4NXxyqs>;MeU4sn;m`BXg>!_9cnPb9O=MFH8e%- zjGOJ=z6bxwbFj);*X7@}Oo0*RVFyO4p($!-=pBdZP=kTF-edak`A9W1MeU4_F_V1h z#PT^;hZ+pbCm+5i8L5V*r~|`ht2ch*=U}Nr4F=|{*SmhkNHjD>?F_w>RUK+D!o2wK ze54whqIO0%n_R!`wmb)`oM~S^=S+ctx%UsR<)^)=hNh^UaiHCc+kLsQhw&^x`=p#}rzDGZy*>qs>;MeU4@>@0}; zKRXA@*Mh?o7&sT=>HEk?H8e%-488MS9cnPb`4-`^N;Nb^?Tl~Q%z;mGo`Y4M!BKu) zF$G3Arz0>@4NXxy;~blv@P}{?mO9j6;JlG}m+-MlH8e%-jPKhFh`p!zIaumYgAvY6 z3D=QoXo}hyQ|-)`!HZ^=^D@+-1_S56X}95JQ;PU z!C36)E(J!ap($!-U?$EU;T$Y=sKMYK9oMPG=To|?=f}U^B_F)6-z(GJHlUb3=gk6Z zQR81ScgBF?-P!)%vrHT6#2S7b8&BIU-{X&mmhUHOgBq!24-P1HY$HP(>ckq(Skt$A zKIv|MKT#XhNOiq;KykohWN1U3Si>1-wD-tAnCR~(YJ(c75mN^gwFVj5P$$-K#*dqO zckq(m^!3ae#+JUexf$0ks3H@K=Jj5$k2v5v4%4? zzGUD0hF|*oiQ1q>YRwG;icKCMLmTSE8qWB|@dxCq?)Uc-wLy*4eb)>qp1+R_ZKxA# zIO8WX56U0g!|%1!1~pQ@x@tf%VHz3QP$$-KhCSc8J&WaYt_^CWes;xxV!)rs(1tp( zM!1fr^ZajD_9z+JphjxKr2~r9caiZEX{ZxxIOF)SMSk05eh;rUsF8a7`~k()e;`8} z>ckq(u%9Ej{d6d=LmSjcbvt)J(d`a0w4qL{;SBqEnA=aelA#T1q^>$^Kr!i7GPI#i ztl^A#-|L^RyscZw&;~V9T}KQkI^Rr&Hq?nVobmBJN98Y!^!F3BL5)=VsRN1&ZX`n+ z>ckq(c>Wl>SAV|WtFH}eq;5LNz6Y--LmTSE8qRom&*SsHfAg6G+Mq`2mjmtR)iq>j zL!DT|8Cky*^2;W6DxVK+P$M-*`Mt;c`-$41M(W9<1{70& zL54Qei8Y+@rT@>?xyM^Om-&A=Q%O1?l133jQ9@7Hcdh5yQ)=(Ml&LgHbi_31Nhlsx zF{O!2)2|$ABsH3jiq2Sfc8Vm2bW$0I94m*B2`PTx&-eO%p7r+X`tN%AT<_=YUTg2Q z?{(ex{r&FxPu+!A$66L7T&rvI-RFkCI@S&nOfn)Ir+$9#Zo7+Dsjn3Y*XlZL+1zk2 zwu1zdj0ndMzP(Sk#oJ<@NW!(cp7X+T&Ty0+B$#AGIBxvwzTF`&j5Sh7xK`I)yUY!@ zKf(?YOfn)I+pO`%?txFnJduQJb^Unfx#5yS?I6J>(T5a>WVgFh?NHEEWaO`*W!QD?cj(H*p*Xp|Q zsdK}VUTp^nCK(ZqH_yDiJK*JCh&deoEz|0{X_L9(YxcB*1e1&i$8P5>@1AtdnbkqU zwYnbrE4!nL}tTy1W+!)A7nV3HBx zIAfoqyRTk!OP%$RaILP(SDhO!Zfpk$CK(ZqNtq{}b5+a}Nw`+my{pU(_t?M=5==58 z9BpUodK@HNtLufu@$2>NAi*Rf!twKCkLiB6+ELX(!nL~29n1~ivaTH@m}EpacDv}9 z?uj=(yE;g?R@WH8Ofn)IA20L7 zx3Be>>LB4dy5`EhmUfUB6uXm*2wD@!f4H>nM?WUv+Q)JHgSXW=<6uBS|L!yiABk{` z%WL)T_g}ZG9tkAjT3vb-*1yjV5==589PJ(4O%4*S)pc%heEc3eNHEEWa14(-ru+Rz zkE{+7uGOWV%4ghZ2MHz_5f1DwKPKT?UHa|%%I$WLV3HBxXrB!AI7qlwmp&zSyV(vB zOfn)I?Gvs#NVry)KJ6~M-VPE>G9nxgeSbx_-S^g<(R)t9wYv1Fz3*@AAi*Rf!qNWX zR0j#y>e64I|NDgeAW3 zGq13N1e1&iM>~_L4ic`_rL&qJTxtgiCK(Zq#hc#M9r22Ns)K}Ub?NNvz#rH_f=Nb% zWA64Nx*O&Xt_~8e)upq{^%w0R!6YNX(aw;ogM@2!>1=s&fgL26WJEaL{mH|+o991J z9VA?j(q{%>)un0610T181e1&i$LD`}VE5f0{ir%fxK@{@M%SNg2MHz_ z5snS++P}Ne96S znMc_{f=Nb%qg`pJ4ic`_rK=8m9bpFvCK(ZqcEzMRNVry)uAY44P&-I4$%t@lwf`%- zhu&P)n=G#t3D@eHveJ=2__j44(*St zU2k<9BwVXYSLNP+fE^^5WJEaH6~*cx;aXj~x_H-{?I6J7?v7V(RvjcLB4-UAi0MfS21rf=NcS&6lZ*(*S$A*V?edPgAN`nwYjx>vx^L4rv}grnUFSdW8* zYjx?az-2qxL4rv}griIc`!-K3Z^<5Y0vSovx>xazhevg4__xF(lfNb6uE`^QcT9iS zgI8&uI3VF#UFQ^s=854T!6YNXu}^)Z!Mb)Faw5==589A!G#w|U}#glly@|Nc>(Cx(Lr zlZ*&QnGW{l8e}~V60X(tu|JLKJTV+3m}Epa%5<Oo6T?A*Nk)XD zOb7cmPaKeNt*#sH8P$1WI7l$bh;WqYVBhA60}`&)b?M#Z^Qz1f!$E>cMuekG2m5l3 zzP?r@T&wG(J4bb%7!DFlG9nyhI@q^);(&x}b^ZMxMs=PT4iZc0saHi31X@)%BE{Ms=PT z4iZcU&PYwYr{q!>G;^!$E>cMuekG2m3Zp9FTCWuD@P8s`JEfkYJJ# z;V9F=zReQ{BwVZO+rJvsd15$7Fv*B;-2T3|^?$zhd-b&<;aXh_zZlhdVmL@J$%t@V zaQ1=y!|%VWI!L%y*ZEhE>O3(VB$#AGILdUeZ}Y?f3D@dc>&j7`Cx(LrlZ*&QnGW{l z-obhtBwVZO*_V~i-7-%M2MHz_5soq)?8`ln)j`6wx^}o^ROgA|Ai*Rf!cnG!eVZo^ zNVrzl<1QN2d15$7Fv*B;l<8pK=7|FmuGMu#KdSS@aFAe<5#jjy?fdj!UVN~=RwP`j z>%j9yb)Faw5==5892f1qcfZZcR@HZd0SVXYy7n98?|GRghJyr?j0i`W4)*0<=jtHg zT3ydQXH@5j;UK{zBf?RpgMGPYyE;g?R@eSt8r6AXI7l$bh;WqYVBhA60}`&)b=ZH8 z>O3(VB$#AGILdUeZ}Y?f3D@d6;M1czPi(Ih2qqa3jxrtW+dOeV!nL}VoIa}a#Bh*c zk`du3)4{$x(@@_J60X&C%PFHePYeeMCK(ZqG9B#OJaIt6wYm=f$f(W}!$E>cMuekG z2m3Zp9FTCWuA5IB)p=q#NHEEWaFpp_uP5pBJ4nK{x}JahsLm6^L4rv}griIc`|`|B zb&znauKnLTs`JEfkYJJ#;VAQ%UQ-qw2MO2eI_juVohOEa1e1&iN0|=xZJszF;aXkq zJZx0wiQyo@BqQ2!d~Zo#o+YctLBh4VMu(K&!7@*5#{q&#MubDp+_ib)fP`yxJ?g+w zohOEa1e1&iN0|=xZJszF;aXjvd*i6i6T?A*Nk)XDOb7e&jADJQNVrzl_OB~{&&xb9 z93+@zL^#THuy6Ck0SVXYI`vhfI!_D-2__j44n1?%=7|FmuGMwRsLUD4JTV+3m}Epa z%5<cMuekG2m3Zp9FTCWuAPRXI!_D-2__j44n1?H zC+YNiPQtaimh3XB^TcqFV3HBx&@*>>l1>g1uGRH-&l?R-+`+FE2__j4j{kVtM*ZP8 z&sPTt*XsJ;ZOi+yy&WW&WJEah%$=U3({Ye+t*$R`F&fT4+YS;;G9nx=Eo*pvsjTHC z2MO2e8a;J1{Qfp}kYJJ#;W+PQ>-HzU>y_0(!nL}t-C#6a*xC*fOfn)IdwzPI{;q3Y zT^%G`t835oM#EdSw1Whbj0nf?Kea}G(m(xEb&znau5;EJ4L|%0J4i6eh;Wqa^S!RR z>-`|%T3xFQ%6q=K9VD1!L^$-!o$elxgM@2!ExCVTxY5(>Ai*Rf!co58Omr8793)(; z>veZ84DZ|24iZc&R~|3|Cp-4iZcN@AFh2cHx+ChRzMug*-|N7MANw+Pp4ic`_wbSVf!{Z-g2MHz_ z5sp(XKW*~ws~=h&BwW|y_~^oLyGPqW;v6Iy5sorHp6D)9y;dY#t823p7lyxI#|{!q zG9n!JuQ-0P=lO?K2MO2eI_|iI;dj=yg9MX|2*;*Jk0<|i`Qg<;!nL}tJ#u0A@wMzA z!6YNXp=a*&B%NL>60X(t>~|~-Pg>Is5==589Bc2s_vAlUd3SY?aILOG4_p|Yx`rJj zm}Epa?m2F5^5I{EgM@2!owMJ<@awDFL4rv}ghS8V>8|+tbA^A)w7PEFYhifPYIcxd zk`dw1Gk1EDP7V^T)wS^+3&Sl}I!G|dh;R&_v%zGyT~<^F3D@d+;foiBN3ZJRAi*Rf z!cnfHPV}^ej)R12b-iV#{H*ogll!3vF*a}fq@+)m}Epa%JumR^c0UABwVZO8&6*-``y_=f=Nb%L(kmlNjf=5 zxK`JXH(nUNW~GA!lZ*(5p1IRgRdSGUt*-0WUnqO(`8Y^0$%t_1nL9m6CkF}F>bh&~ zh2c6Y9VD1!L^$-!ot~tVgM@2!tvXm3KK$^l?fW?iCK(Y9-6NwX>Es~cT3zekH;>~W z!6YNXp=a*&B%K^2T&wFDch2MeAi*Rf!g19{pS|1L+j~yJwYqk=c^*GkB$#AGILc4r zZvNRN;aXk0|7IS)A0(J$L^yPhjDH77xK`J`SI^`3oCK4M2#4;G@n;DM*Xla*(s_Jd zkzkS$;m|!Y{!Ap{T3x4JIFHX=5==589J)uwpY0@EtLwYpn#bP{5==589OW<0jQ>WF zaILOe&Ys8LK@vl5nlAjXpb%zvm>FWJEZ0kBrY5NVrzl!s+ukFCoDs zBf?S6WM+KsL&CMXPWb3N&aX%?$%t^2GqM?#{D@i{dK*Xlaz$a$Q%lVFk&;V5VNGyZNs!nL|?SU!*MA0(J$ zL^yPhjKAZMaILO|1LyHQiUgC42uJzmHRJDUBwVZO(*5S~eUJo`j0i{h207#JoFrV= z<9N+HzSojiK#~#R&^U!P_=P@rK!6YNXp?hRJlOf?+U4Ptp9&;ZOOfn)IdgjivB@(XHb>VjN zm|u}#k`dw1Ju;q=k#Mc9Pd;-Vb2t)AG9nzhN5->060X&C;%4PiW=z^O%d0V3HBx&^*0szFmETpBqPG1XYM@HC*fLM3-^~bomO5qAi*Rf!cnd?biOu0 z!nL}-boU&te~@635#cCTOgdjmqGPrrQ**KtTN$%t^2D?gpD6_IeQuCLxOhwD)! zm}Epa{%z;=yKh|`*Stu$R@bY3Ro?TJ*VRZc$%t_19vMAJr|(fDT&runtIP8rE3Xfd zV3HBxC|49aUxOs!T3y%uXb#soNifNXaFi>fov)>maIG#su-DRwA@M zwf-&FK91#=#eW|Rnxhi>cb#NJICMXe?h=xNgll!_Rd~u3c939_5#ea>V0DmituDQr zU%Sc<5==589PKBuI!L%ymwqaD`>7oym}Epa+HX{KkZ`Ro{dS%Eb2~^d$%t_1emmVo zr}u+|Yjx>U;>p+8L4rv}grj}JRR;;z>e8p(LBFp60X&yPwlT= zYX=D?84(WM$EmwBD>veXJV3HBxc;iX;&Kz=E>@7>ewYv0I^ien3L4rv} zgyUl``@_uBH+f?{4ic`_rL%!2-(&{~CK(Zqb|zCDBwVXYXEht%Vh0H(84-?lMphjp zT&qiGXRF<22MHz_5sr3dSsf%?t4n8@|8u(?B$#AGICS5+?k3mI6$#hs(%JH7e{Tl~ zCK(ZqcBWq)BwVXYXZ?HMVFw8&84(UW2cWzAbsQvIt4rTr9=g*G5==589RF{tugzR@ z#=+G=!nL~eE$qX0+d+a!Mug+|fBXE*f9@B1tdnr9E`6I^_Z~Y)Fv*B;==l;oHKNyw zgll!_Tk*U8Xa@-<84(UWC!?od)L~(s7V*tu9SlUVNV&B$#AGIQ0CLo~n|AgllzaYV@Z2?I6JrymHCRSH6E_b&znaE=^UR@)tWuFv*B;w25$akZ`T8Th5*zUbE6cf=Nb% zLB4-UAhXi?@9*= zCK(Zqb_J_CNVry)u4bLJ(m{erMug*OAKG!Zlgo4N`n)3HT3xy-cg;!%2__j44n3!@ zr|;z;;aXj~y7;G+4iZcVxIilv=r~BYR+p~6Pk*jRFv*B; zl%GW1Yfv2|T&qiW5lnwSNHEEWaFpLD-7`@gBwVXYcSB5n&q*-Jh;Woo2HpEn9VA?< zOLuilKd(qI$%t^2PdMGfQXM2*t4nvMOh0!?Fv*B;luznO`)nVRaIG%gWi$QzL4rv} zgroe$nY6!AV-l{_rMrcue+Nl0$%t^2znr=^s=ihvT&qiWJx%|flVFk&;V5SSlXlK9 zCgECLx_fJSUP6LNMuelB$>?6SdK@HNt4nvGP0z1LFv*B;Xr23Z4mT#@T3x!EZhD?b zf=Nb%qnugl-obhtBwVXYcLh$*cS$hGh;WoM$VbMMG z_2-I&Yjx@F`03n-1e1&iN0}5&+H7e|!nL|)%JT-(`4tH!84->$@zOI5^*BhlR+pYu zn9kuyFv*B;l*yf*&8Q9%uGOWdE~fKD5==589AyHjXH2Stgll!_>5=JNlmwHE2uGQu zPTK5sOv1Ig^c2i=zDt5hMuekGgePr=JSO238)Nk)XDTxrm=Wc58K;aXjKN^E-lg9MX| z2#5BeYu8xDBwVXYPrFU8FLAibu|)9G9n!9$;s*<;aXjKigJ2=kOY&A2#5A*Y}X*iBwVXYPlNhu=3qdANk)XD zJ;_>+gM@2!>8VyN!jE`{w-QV;A{^TPQ!5qd zJ0}U(>e5rj+D|wfB$#AGINB4^)j`6wy7aWP_7e^V2__j4jYd_)ket=+-5#eZ0-q+WPgll!_DSYiG91appG9nyh zO@m%573e)D;aXi zI7qlwmsWSse!}4(!6YNXq4&G(Cp;$MT3uSDLi-7ag9MX|XvYz2a@6A>;aXiUtOPz z`q?GnT3uSjMf(Ydg9MX|2!}qU_4zIb3D@eab!n9&?I#=#5==58 z9A$UlzU?PGCgECLTCGX@35SCOlZ*&QTWPAkRwP`jORG+4KjCnYV3HBxXe(w_2MO2e z(&|~-PdFSTm}Epa&Ux3~{q`HLS{)=@t4phZX+Pm`kYJJ#;b<$EP4sg`!nL}znwjZ+_(LN$0S^&NVry)R{PU_!r>snBqPF6b_ed;e!^oC zuGOVg3$>qcI7l$bh;W>E;eq`&J6%?fgM@2!Y4t_zCmaqEOfn)IZRN=7AmLhFT18U( z35SCOlZ*&Q*&VoV`w5RpxK@`|!_uW{AwYs!AsrC~N2MHz_5stDuaNqV59+PmbF0Hbv{e;6of=Nb%qwEgcxBZ02 zBwVXYtHo+R;c$>(k`du3y94)aKjAS6*Xq)$x7trQ93+@zL^#Usz<-+w{e;IPT&qi~ziU6?aFAe<5#eYn*I%I5iiB%*X%&0zCmaqEOfn)IZ3X`7AmLhF zT8&@(35SCOlZ*&Q+ex50NVry)b`{Wm!r>snBqPG1`wVpVfL<#SuGOX85wxFhI7l$b zh;X!>9jb$bYjtUt2kj>u4iZc#h_z zNVtwJ?%6?tyLn_p(B*q`r*Ff>QJ()8*4d;I*ZP0I^nziXC;lzLBqQR;%XF}7^TYuO z*Xp|Y8^bzJ3$9qh^)jrBN4xK`K3Um4bUVmL@J$%t^2>0sC9i31X@)%DiT z4eLBH93+@zL^#S^scZAZ0SVXYy7J6nohP>U0|b+d2uGO?c4h6)`dX22t**U3F|6~% zaFAe<5#i7pscoJ(AmLhF_kDC&=ZWDU!6YNXQKo}knWX2VC${4N!6YNXQKo}kS*y4@NVrzlC5I2|JTV+3m}Epa%I8R@s|`9160X&? z$?{>HCx(LrlZ*(5)<|vh!~qG{>iWRjhIO784iZcMMqIo)``iOfn)IWjfgD zDz6+QT&wHy`C*+WhJyr?j0i`W4t8bD{^}s%T3t`yeOTv-;UK{zBf?RpgI${^4oJ9G z*U8IFxo&RwP`j>$xv1=L}_@7!DFlG9nyhI@q;&;(&x}b=|$o zu+9_1L4rv}griIcyEac8kZ`T8AMZS@^TcqFV3HBx(0T*9D@Csr3D@fS$qvO)=854T z!6YNXQKo}knwf=Nb%qf7_8HcuRoaILOo+YakIF&reAWJEa1 zbg*ml!~qG{>N;nuVVx(2g9MX|2#40=&|O=4KS;P%*Ne9p)_Gz$NHEEWaFpp_*XD@> z60X&?+UCPLPYeeMCK(ZqG9B#NJaIt6wYqM6>afle!$E>cMubD_ylCZ9y;dY#tLx@X zhIO784iZc$ z9qe@1uikSKuGO{kdc!(T327kpRwP`j>n-aH>pU?WB$#AGILiI=o$iX4gM@2! zU9#4&&J)8yf=Nb%L+iG+dE$VCYjrKGIjr-F9t5*jJ*Xp`zwPBqnhJyr?j0lI;1JqL^dOt|GR@W7)4(mKI z93+@zL^!mrp`Lz`gM@2!-Mh-L&J)8yf=Nb%L+dl@DIPgUxK`JTibM0naFAe<5#i7} zlWm?jAmLhFUmFbTJTV+3m}Epa%5<>PQ&l<+60X&?P#l^khJyr?j0i`W4t8yxI3VF# zT~90y%@e~xf=Nb%Lu;hAdE$VC>*yNRJ%GbOVo>a>48oofL2HdvJ*`(>sxb-IK91ub z^q$2e^zTlS@R0~dc|Ngg`w5RpxK@{51??vs$3cQgMueligS+WCNVrzl*NQ{?2|waJ zuLP5f2uIl+xNG|fk4d;zmwqarai<+5m}Epa+HX|-zarsUUHa|Pe!_7aB$#AGINB#e zb&znaE`3VucC#HMm}Epa+9zCfkZ`RoecD}iy&WW&WJEa1Gk2YylGb}p!nL~eslD%S z>>$A;Bf`=C;#3C-*Xq(=pa1)X9VD1!L^#Usz+Kx zg9MX|2uIl+xNG|fk4d;zm(DV^pK$yRl3>b^|d15T3tF@o?KuD2__j4j`GZ1 zr&Tue`$59Bx^&im$hYkv!6YNXp*2#qT8JDZT&qjpUbg(Y9VD1!L^#Sbcb!%}k%NS5 zb?IB!&0n#D1e1&iM|tM1D^Jo@2MO2e(znUae$fsROfn)I?VE6QkZ`RoeJg(Zf7?NV zNk)W2You!BQ@vIsT&ruf&CA!p&)7kNNk)XDJagA+RUkP?xK@{@6dRvm2MHz_5so%t zsSXmZ)un0610T181e1&iM|tM1(<)6m4ic`_rK!>NC)+`SNk)W2YouzmDLF{ER+pw< zSA4_{5==589Bpz}9VA?yf6xvROfn)IS|e2} zpX#+D;aXjqs$TVeJ4i6eh;V3)RIPj}2MO2e(scN@$J#-HNk)XDO}49pgllza%Kh*$ zc939_5#cD$+;!zix=zPI!nL|It=E3S@t%`lk`du(R~o8=gll!_s>5DK*g=9xMuekX zF{us`uGOWhCm%V~4iZc&eAgQ55Ck65==589A$UluI(p0CgECLy36KQFSdgO zlZ*&QyF;iR2MO2ey7J8O_k5WhB$#AGILhw8UE5E1Ov1Igbl1~`FS3IKlZ*(5es0@- z!ebJy)up?)w4ZRi=Omb9L^#@=XZ5ur;aXj~3r+h8hl2!@j0i{B9k^@z36Dv*R+sLk zTX%^aB$#AGINH5~^*BhlR+sJyT(*-PB$#AGILdUeZ}Y^mU+O>938WI&dVTJAc(zUr z|CV5q5%KDlJ(qgzq*Q(f2P9mp>+IstJTV+3m}Epa%063t**#11s{sku>Y6VO%@e~x zf=Nb%qf7_;HcuRoaILQIJ~UhBiQyo@BqPF6rh~m!$J23;aILO&i$n9oaFAe<5#cD) z!CtHU$w9)kx}N*j**Z@Q2MHz_5soq)?6q2<93)(;Yx}><)_Gz$NHEEWaFjildhMhn z2MO2eTJ6EvI!_D-2__j4j&?_Ub&znau5%xlt@FfikYJJ#;V9F=zReQ{BwVX&$^El+ zo)``iOfn)I?Fof?93)(;>*PPp)_Gz$NHEEWaI_~as)K}UbzODuY@H{Dg9MX|2uGO? z_HCXxAmLhFH{3H@=ZWDU!6YNXQKo}^nwX=1e7!DFlG9nyW z!>i2`2P9mp>pQ=it@FfikYJJ#;h2B@+xqK2aZ!D(NVrzl%YHFi=ZWDU!6YNXQTANw zwUd(G4-&4`b-~rMb)Faw5==5899qMx%@YSCT&rvCD`)FGF&reAWJEa1bWqPC*W)1J zT3y>+Rz7#jJTV+3m}Epa%5<>TZUTC(NVrzl^DddK^TcqFV3HBxDAU2d%@YSCT&rvS zi)QOQF&reAWJEa1bg*yp!~qG{>bjzzt@FfikYJJ#;V649_1Z~EuN4W`>Uzt0vvr;r z4iZcbm8W**Z@Q2MHz_5soq)?AttXK*F`Uj`+xIohOEa1e1&iN0|=x zZJszF;kq8jiL-TT*j_6Tw;;)gaFpp_uP5pBT9I(At|iCM)_Gz$NHEEWaFpp_U!M7? z4ic`_wf}o(>pU?WB$#AGILbVxZ}Y?f3D@dcanx*`Cx(LrlZ*&QnGW`Co;V=kT3v@7 zHe2V3;UK{zBf{~C4=(BdW8dxTYemAfy5cMucOX=RK?c^Lux#4ic`_b=s?D>pU?WB$#AGILgm$-{y$}60X&C>!{2b$~-X~ zB$#AGILdUeFVDW#;~?Q$T~FD)eE%r(#Bh*ck`du3)4{&Y69*(*t83@sY@H{Dg9MX| z2#21z)01?1&q=se*9&%;t@FfikYJJ#;b?0#RtE{!>iWCq&0^m^5==589Ods*@4W#@ zxK`Ia+m`oZ<^F>tm}Epa%5}TmdmNH*t**~+F^hc^NifNXaFpwhz4vM);aXjzr_N$O zND@pkA{^zsr`Od{z2_uctLvH#X0fj&2__j4jtzglPJjHjUtJv}T&ru(^=7d@CkZAQ z5stP(Y;}-ut*&#{n#De&B$#AGILi5S@4ZY(xK`JygYuql?%#6~Ofn)I<$J-TobA`+ zAmLhFOYUEWeP2m1$%t^2?>7_O1)=wxgll!}bN4drUrT~XMuek$Pn+m&4mn7;R@d^| zmSG=W5==589Bn1&>LB4-UGKPl8TJzwp>EykNk)XD%*Q6Wi%#!33D@e{?CfRO|C|Jqj0lIG zxzpW%a*%MXu5->>hJD;gFv*B;l%3xv-YcGjYjy2(`ZDZCPl8ECgrn?WKk=UTBwVZO z+>b6BZue+^CX!&15#eY%JJi>TgllzecH*+(@7J+|1e1&iN1209%5*#45ByuE)pgu) z%ZA@s+YS;;G9nyho9*scYClf=Nb%qwU~Q9VA?<>zw_T4Zps+ z9VD1!L^v*AW0y&}id7vXT&wH0y_OAcTFnj;Ofn)I<+|OZT#c&^60X&?!5+(oTds7F zV3HBx*k$LB4- zUDvL^Y`E1*2MHz_5sq@-$NA-Mj_M%cT3vUqy==J7N(Tuh84-?h|H}D#%1y5o3D@d+ z_~E6)habMReLp9`BqPF6p6lCHPwUA+!nL~Azi%myg9MX|2*-~;cHOQYYp)dv*XnxO zolEh4kYJJ#;m|!Ye$Pp`R@V+UFU8Ll2__j4j`FU!yKmg4t=1e1&i zhwhQllXP;BaILQWu3n1Ya}rE4A{@F$Mo-eoLBh4Vj=Xd!KCehH$%t_19vMAJCkF}F z>N@ShrTE+>!6YNXp?hTf*-pZ>y1x6ZrTF_nf=Nb%WB8m6W_H^p{(g{ft*%?oUW&hi zB$#AGILhU#2Lm*Vd^2__j4j&cSt<8uZQuGKXRv84(WMBjfJ|BwVZO#^p=#{euLP zj0lJBk@8=|#WJEZ0kBnykBwVZO zeJ@#xIRgnM84->$nV9h`gM@2!?fAl_n3s@Xk`du36P6i0NvEGH60X&C@6Jmx_aVU~ zBf_D3WIS6U;aXjb+bzZXiUgC42#4;G(UWvK4ic`_b;dK7Vh%@wNk)W2_sDqGN5Zwb zKD1f+Tl-{xHz2_zBf_D3Wb`DRj)R12b)B@~Qp`n3Fv*B;=pGr*UP-uC*QeK8iuo=H zCK(Y9-6P`}G6~n}x@4`Tm{XHrk`dw1Ju;q!lW?uBhaVnd-cEu^MubE6$atnt!nL|a z_m?%Dp5gbL1e1&iN4e6_mHqDOdrrc&y1sn(5Z6CQFv*B;=pLDJ=Sy{vaILOq+&;v0 z91=`2A{=FX#!jm)>SvdPYjvG-!w}b_NHEEWaFlf>JFOlm2MO2e+VfZCJ%6^}4-!l= zA{=GC%ucId%0a@ly4Jh8JpZxs`XC7=84(WMBjan3BwVZO`X3E(os$HUj0i`$GTLcX zQXK~g*Xr_99&x>v1e1(-grk>3g!j&K?c>Bf?SENbSl~hkrwuR+nCd z>HdQxm}Epa+B;YsBwVXY@8)zLMG{OhA{^}}u{ub&R+oM%r~5&YV3HBxXuna_LBh4V z^xHMv*OCO2j0i_rBeg3}e8q7biY&*Ofn)IWsTIC^0aq74ic`_rN2JYeP2m1$%t^2onmL&-m+!w zw&}m+T3z}pI^DmP1e1&iN7?aqrtOhCAmLhFIvbeo!%KomMuelC$<)`1gll!_tY*5O zFbO6Z5sr37RvjcN*pOavc5#cC1rq8rJ)(0e9t4rS|y<7WWK!Qm|grlsH zI-{MG^jeW{tuB2lp6*9af=Nb%qpXoSqn(uGAmLhF`gVWiz1}y21e1&iN1IGk2MO2e z(v;#!f3kxFlZ*(5)pohhrBRR;;z>e5v8DSxqp1e1&iN1F&&2MO2e(scNml@1b2 zG9nx&-Lm1#U4Or#I!L%ym!{mu{?*4pf=Nb%L+j|3)$yu>gllzaTEF2#c939_5#eZ8 z8mfbYYjx?W!zn8rB$#AGINBAH>LB4-UAlU*=EFV?5==589PP?ab&znaE?ouMccp^_ zlZ*&QyMk36BwVXYSF_Gq=^()*Bf?SENZqZhn^_$sT&qi0<*r%jAi*Rf!co>p-Bmj& z>9d4{Yjx@B;-6MJNHEEWaI`C<)j`6wx^$IvF!)`&UQ2>WMuekX;gw@d!nL||wRd_P zB$#AGILaES=a-dFtAm7Vb?K`0^!*^gBqPG1b#dE!J|^K>UAp={{kbB+BqPGn?i{Gc zLBh4VbQi(&_k#qJj0i{hjk-W973lpS;aXj~8)Euvq3Pd25==589PLi0>LB4-UApUO`uCg!lZ*(5)>Uuk3}X_m)up?) zrspLjm}Epa%9+fhT(7OiLBh4VbQjw6{E7sVj0i_r(_o^N3iNYD!nL||H{J9+kpz>B z2uHgUusTS%R+sJyoSyHJV3HBx(7prhoO(>cwYqe7*4Yjx>v)9Lpp5==589A!;|iB>9*gM@2!>8{!7_dya& zG9nyhO@oP6Dv*POYjx@F-|6>S5==589Oawvq70QClZ*&QnM_RDEMrWKS;P%m+p?A z&V5KQ$%t^2NztUumc}Gpt4nwJd*)Q`Xam6{Bf?Q8UK6cUpr0!euGOWd6{d4I5==58 z9A$E+XEUmUgll!_sf+16kpz>B2uGPfPP9^ij)R12b?NDm>0FcqlZ*&QnWRqI>~&1S zwYv7Xdl}}tB$#AGILbtL(q_nG60X&yr+IF@&flX*Fv*B;Xuq&F3m=nktu8&4G@ZAT zV3HBxC=>d=&Gg44T&qh@S52=QkYJJ#;V4%c`f|OtzUL%dt4mLbO|O5DV3HBxIQV_* z^n3l`)zv}5wYv1Q+w?jP2__j4j`pNpb&znaEtH~FNk)XDJ&{`-BwVXYPv_qA1OJ|rV3HBx(EguVsX(8JBwVXYPZ>Yu zB0ESh$%t^YC#0)`gll!_Y3ZFVw1Whbj0i{B9k_4%36Dv*R+paoo|)J|f=Nb%qpWGr zYo!9cRwP`jOHZ%A;5<7>Fv*B;v?uSYgM@2!=_&kYe#;ILOfn)IWsTIn?I%1Y;aXi< zO` zwYs!w$cH{-2MHz_5stRvNp+BLtuC#8vdO3HAi*Rf!lC`u^(m*{K@zUjrBz%$@t<~( zV3HBxDEHy?`m~mVgzI`7WnG-don{9KtyAM9Bf`;E(uv~$VOm{URp*G0*+GIyMug*< zP4?(F-rz~qLBh4Vv^tRX6OMa|NHEEWaQx3Ud-l71^KsQd!nL}z%8~XH4hIP)84-?h zpITq;e5no+uGOX0nhyG)9VD1!L^#??Q`JGjwYs$G)aTxB2MHz_5stQER&|hYtuC#e z^{Zp;Ai*Rf!g1>7_wKj5c$MlP;aXi<1#B?3g9MX|2uEANY@*)}60X&y)y%Y?aJ=Uv zm}Epa%I?5@+fR5*!nL}zD%gvTUYtLu4}%+~#c<2Xn#$%t^Y zmGSCpMZ&eZv`U`#6AlLnCK(Zqw!&X^kZ`Rot@fw=gu_9CNk)XD><-+w{e;IPT&qi~ z7XID-c939_5#e~PE4ic`_rBx(ve!U$e zm}Epa%I?5@+fR5*!nL}z8fO1mJ4i6eh;WqMf%~?f@R)>ab!k=4lU{8H2__j4j>$A;Bf_D3WZHhhV-l{_rPX4u zd8r*Fm}Epa%I?5@+fR5*!nL}z>g|XB!448kG9nzyE;_7#@3Q0T`$59By0m()_7jfZ za}rE4A{=ey;p!maT3uR&_|DzzAi*Rf!cm?h>&u-l)j`6wy0n_|$xG}Y!6YNXp?hT7 ze!^oCuGOVgp|ziIyjCQbWJEaHiq`cwNVry)R=57;_I8k9k`du(D|1%|3D@e>$A;Bf`;EuD?L%J|tYLORLx~Zfpk$CK(ZqwgP{3kZ`Ro zt;WB{26m8Ok`du(I|)<=3D@ehPUuGMwQt452OCx(LrlZ*(*-ap>9+w_JyPaKeNt*-4}H(Jy@ zF&reAWJEZ=y~Fn14&PW(kAsA3b$$MgqeaaV!$E>cMug+9AK0P$#Zfy|2MO2edeniV zMa>h#L4rv}gyXn_ck14Ea-AoZbyBDQmTPs*A2M3hJTV+3m}Epa+KyB8I7qlw*P(}v z7Bx={2MHz_5st@Qv}?EF9otq13D@dcanxv0^TcqFV3HB-IAYJR>LB4-T?f2(w5WMv zI}Q*`G9ny*`tY!O`951#2MO2e+WGj=qUMR=Ai*Rf!g2P8X1mv3v3YfnaILOeP8=<2 zo)``iOfn)IXDqy=+xEaZPb}*?Pya2~>N@NrqeaaV!$E>cMug*sTfeN^X0gr_2P9mp z>(*07i<%mSg9MX|2**ahobNvW+>PpMMZ&eZc0PTysCi;INHEEWaGbn!kM6_&^Tc>R z@Nbz`*Z!X_zk_9-7!DFlG9ny5Sbxv%rPtPZ;(&x}bshfS<@2h{6T?A*Nk)X@^KW}) z_ng<)dE$VCYjwTlOQS{26T?A*Nk)X@6Sus&JNTq^>T5;9wYqjVXSAq!VmL@J$%t^2 z>0npZ?5_?IuGMwjH%5z^Cx(LrlZ*(*zNhcq-TUi0PaKeNt*-sg8!c*{7!DFlG9nz` zxptp!gF7D7dHaBbYjs`Tmve?PPYeeMCK(ZqH{7ys_rD*zw>n6;R@dV%8ZBy`7!DFl zG9nz0d&(QTwO;av>LB4-UC+H_w7BF@J4i6eh;V#s<2QFdU4Bz_kZ`T8?Jg^hGEa=- zAi*Rf!l88&ba##34-&4`wbqs8wJP((aFAe<5#cD)!LH2{2P9mp>-?)ni<&2fg9MX| z2*>?DJFwgCmvx>vAmLhF^S>A^YMvMl5==589BpUo`dX22t*&qVYP6_%VmL@J$%t@l z`P75D%O5_kI!L%y*F)Ei7Bx={2MHz_5soq)?AkoB?DI4Iw_K}hvl~W>nkR;X1e1&i z#~ORQy<6?h&)4H1;aXjr+%#I$JTV+3m}EpazVN-}U4P7()j`6wy6(PZw5WMvI7l$b zh;aO7%R{?!|L@f5AmLhFpSpdtsCi;INHEEWaNM)}on8N~4_5~X*XsKFKa3VNPYeeM zCK(Zq+rEBS_qs3EdE$VCYju6-&e5XgiQyo@BqPFc@XrqK&fa}okAsA3b^Yk>(W2&w z;UK{zBf{~)vPSBUcCYiq0SVXYy5XMDqUMR=Ai*Rf!g2Rq@9Nh1!0YO9kZ`T8EAK7e z)yh0E93+@zL^#&D_TAmTK6$P>NVrzlDSsL*YMvMl5==589A7{0$nNN8y`VZsxK`Km z?;kB{o)``iOfn)IXTR&H?wD8AdE$VCYjvITz-UqP#Bh*ck`dwf!uBh=7te24kAsA3 zb*=v3Xi@XTaFAe<5#iYWiWS{zUs|I&NVrzlc7GWyYMvMl5==5894lUXboYr*-KzQ3 zfP`yxJ@>DpMa>h#L4rv}gyRWU9o-#t_C?h}!nL~AeQ30(d15$7Fv*B;XnjUK#iQdO z;aXi67Ki4E;UK{zBf@dwzaG;aGW+i8AmLhF^AC>}HBSr&2__j4j-Ot5On2GqpIaRy zT&wFV#i4m(I7l$bh;aPm_G7w_Zu&FL`UWIitLqQNp?P9BNHEEWaO_>yFa6S5PpA$O zuA^&@#Z?A&kQfZ6Nk#%U#gE^$9LAW0Yahq)4|>mH68d+iN%%;FV^UtLOWysn z>LB4-U3wL?pK!cZB$#AGINCe7n~sBoYjx?})PBN`c+V@rBqPFcfBD(n^Ox`X8^W}@ z^i%naJMAFBBqPGnexs^`gll!_w@dp8$8nHgk`du(pA6MO!nL~eDY4tlc939_5#eZ` zaMeM=wYv0aciHuJkYJJ#;dt?nS9Gh~y2gy&a}uuArBCgBe`5y;CK(Zq_7|r*NVry) z{`&miFYF+}BqPExIqInH-`3r_I!L%ym;Q>LaJ3yIm}Epa+8ID~kZ`Rooeezm3Oh(J z$%t^YGnwij;aXiftNFpDc939_5#jjC@7~p&d+Y0}gM@2!>Fn&lAJ{>HNk)X@Gj|=) zJ?o}}s)K}Ub?Gcq`w6$-K@dzbA{^}uxjIO=R+r9}Cl}a3f=Nb%xZj@gll!_+sl?;w}S+ej0nfKpK@q-=Z>dU2MO2e z(zmdizhVaoCK(Zq-(SAGTfXa=)j`6wy7X=GvtP7>1e1&iNBbsR9VA?E>)`Ba~YBwVXY-|mN>v4aGYj0ne)XC2h-d+vGFLBh4VG^N=13_D0L$%t^Y z2}^a5aIG#)TORnh9VD1!L^yW7=)mp|TVGZkBwVXYQ={uowu1zdj0lI;NY!dndOt|G zR+pwyf6xvROfn)I z&t3hE-8rBBLv@gFtu9SfuX?{7B$#AGIDY%^zTKmj+*=(aT&wHyezf@8W9=ZpBqPGn zCfn6P!nL|I<$m}WJ4i6eh;V%1J$rYzJ!aKTKUXANt4q^*?I#@XISD2i5sr4Hp*l#o zR+p|i>~(}4B$#AGINBAH>LB4-UAlVmkwfhu!6YNXasG#1*?n!Bb*qDfYjx==&@T_M zg9MX|2*>qzyrTQy`s-H*3D@e<)vRs)#SRioG9nyrJYbLR%+J;RgvTUYt4mkq-hY4{ zB$#AGINBA(dK@HNt4miG?|QQxB$#AGIOYZ~>t1mGCe=a0wYqeb^v!Rug9MX|2*Z<$t? zuD(BJFFQyu$%t@hKi0ON@R)>ab?GjGbN|r}5==589Dn%tUAt?Z@vM3rBwVXYcS9WT zayv*c$%u9wWq;iABwcloaIG%g)uH`_<1?|{`%wud84->ToV8Q;;n&ywgvTUYt4nvM zJor*ONHEEWaIE+89lDFZwp0DTBH>zHy36KQFSdgOlZ*&QyF;itNVry)?iSL1!tq*> zV3HBxc>bfF)xGX>yH*DY*XlauRiniVUt|XfCK(ZqBR{iscgwfB>LB4-UAlWq`w7Q! zkYJJ#;b?cBRR;;z>e5|k+D|wfB$#AGIQD&L^X`_n&Q=Er*Xq*Ubn7m$g9MX|2uHhj zusTS%R+sJyT(*-PB$#AGILdUeZ}Y@*YBE=6laWNN*XN1lziVpvx5Oipza`?;Ez`li z%@YSCT&ru3;?O)X93+@zL^#Sku`e@Z&94R|T&wGA#i4m(I7l$bh;WqYVBhA60}`&) z)vYpD=ZWDU!6YNXQKo}^nd#T#AmLhFe=H8o6T?A*Nk)XD-ML&HBwVZO@>S>RJh8nW zWz{dO3#eC@Nk)X@ReK%XAAR&~e?yp7*Hx>{)p=q#NHEEWaNK(3ivG7p)Oq57glly@ zxcXe3Cx(LrlZ*(*N48zjfB!oh>bt>!gllyztT|WbiQyo@BqPGH`+JV+zk9}3)j`6w zx-MR8uFey~L4rv}gyYg>!RbML-uGO{PljrI@F&reA zWJEa1bg*yp!~qG{>biN8xjIj5uN4R;84->$9qh}!IQ2M4xK`KoPo1mt#Bh*ck`duJ z;{1dA*M79l69*(*t83NG=juE$93+@zL^u|AI;j8O_ttshfP`yxy=05II!_D-2__j4 zjx|=itv`R=i|T7d!nL}-w$)snC$`rL1e1&iN0|=xZJszF;aXkGwwbifsxjIh_2MHz_5svE~|JHu{*Z-!zRwP`j>nA&u&)qUl3$9qh|JkkvuLwYo0fd9KbA!$E>cMuekG2m3Zp9FTCWuDf=btMkNgkYJJ#;rPU( z_v?SQZk;C%NVrzlb6+@D=ZWDU!6YNX(O0n>(b=LQf0SVXY`tkf+ohOEa1e1&i$L@c8 zb^n1Q*QpK?uGMwwE9UAvF&reAWJEa1bg*yp!~qG{>N@__b9J5=4iZc`p|BC*E zXViJ(fP`yxowD~_ohOEa1e1&i$0rWkqc1CS)z^xIYjs_+-&~z1hJyr?j0nfKFP-l% zShi7hkZ`T8C%<*B&J)8yf=Nb%qf7_;HcuRoaILO?d)r){Cx(LrlZ*&QnGW`Co;V=k zT3s72pR4o4aFAe<5#cD)!M;2zQ(r3*uGRI!!{_QeF&reAWJEZA_{m|v#ln`=LBh4V zKDJ`6&J)8yf=Nb%<0Iee`U6hcx;jX>R@bq|&eeHhI7l$bh;Uqg`mX(1Ppb380SVXY zI^hFzb)Faw5==5897lb9N&oKW)p_E8gll!3ds6uwEc3*0kYJJ#;V9F=zReQ{BwVZO zzK_n;d15$7Fv*B;JofcF^pE;%ohJ@RxK`IIKQUM5iQyo@BqPGnRxqpYISJS5`pKE) z?|GRghJyr?j0i`W4)$%HI3VF#UHgA-uFey~L4rv}griIc`!-J;kZ`T8Cx4~P8Ol5{ z93+@zL^z(l{g(X!-yYW2iiB%*-Smy}{iDni!$E>cMug+6yEpH5c}JZm4oJ9G*QFQC z)p=q#NHEEWaFpp_U!Fy-$3eoix-PkBuFey~L4rv}gyUbnxJmzx7sQ@=BwVZOx*yFg zp16ZwD-ujHA{_TTYoq@E{d&G02MO2edhFHZ{aCsGAPFWJ5ssIC=865Y|7DNrAmLhF z|M;u9#pynZB$#AGINq}9`u#U=j=dU5xK`I!Z|Nh;60W0besO+t9|wuSV47q^(0_N_UnYM( zXP@dI;aXi6uQk88(bMc8@yH~6B*Jmj`u9#wf7E`}LBh4VKDFNb;(eRiL4rv}gyZrb z|6%g{2i{N}BwVZOLmSR7-m-}uB$#AGI6k-CO_M{nd2@A;a9tgn%`Z;(6Mh7NBqPG{ zgxCFMvhNe#8V(YzR@Wz=IluUq4SgIWn0zQ9!qHadt_~8e)pg-^^NX8rUbhjV`NiWOV+RQ)84-@xe)+V? z7uPwoI!L%y*TRAGi`zZg4iZc*>eMFMfP2J4i6eh;TgX zw1X$#xao-MAmLhFSD!Gyc+#47kYJJ#;W%O6y(cdpyt_I`xK`H*ADv%3bqza6Fv*B; zv>jZkgM@2!Eu22T`1RH8Ai*Rf!g1@9cbR&ZVKepGdkaILP}&YoY~a;1X=lZ*(*F3UHVl$E)vgM@2!O};h1c=W114iZcN@h$`Ng-cbdX?@5#eY% zPE`j9*Xr8$>iNY720jiFOfn)IpTF^p3yvty-f1pM!nL|~|IPg387mzmm}EpaF2C%^ z3m$V^I7qlw*Yj?kUwq9<2MHz_5strJ{_G20dQmt?xK`KZcg`qtE}(@?4zW4-&4UYhm%>hi`4)&q)jh(PzA z>N;cF1^k|qV3HBxSmoC5?RM1F@p(nUwYrYmaRHxKB$#AGINFY$^*BhlR@Yl*7Vx=C zf=Nb%&7&{yfgFNVrzlI}TjH`4tH!84-?hMmFPfI1;Ya^{jU+;5?B8lZ*(*x5o$1%-j;^ zq9k0a>)In1aK1}|Nk)XDoFUKnoSKAdbscxy0?yk>Fv*B;eC*)kXZD!HcLNfx)%CO! z7x4Xq1e1&iNBPDwxrC=yIEA{^zL*Nnfbk#Mc9oljrD_dya&G9nyn zUi_(<_uLrYIZ3##$8pvIzSolYDw2!{NBJf^GTZ0=OiAPgpWixUUKmo-4~a}wFwfg)%EiA zM!5b#f=Nb%W6w{o)0N#%>dzGk*XsK91|wX@A;Bag!g0;ay4@4r8`p|RxK`J~Q%ATS zMS@92gyYqZTfaN^&ba19!nL}-xJ7xTpY8X91e1&iN4XN$`Pv-`*Xp`w+w%O!%Ikw9 zm}Epa$`!@V*C0u_R@dJ>Z-ncdB$#AGILejL&eu{&xK@{+@`&rTB$#BxBOJXxcSU&b zJl8&s>AroLB4-UHYk3D@e z-RR>W!6YNXap9(an0f3o-&h?aT&qiG1JiwYNifNXaI`a->LB4-T{^3o?k7xwNk)XD zosm@s3D@e<+1YeoWD-m=A{_0^vN}k(R+r8)r~5mTV3HBxC_6&Wv^}Kj`L6#h)9TXM z@^qhS5==589PLcM9tR26>e5;NbiZv9Ofn)IWryyWwx@Uf{^5Viw7T@|Wx8)T2__j4 zj`LUh+RXPqdvHAt60X&yZ(-B@&q*-Jh;Wo0(`VWq>jM(5)unHf(|z1YFv*B;?ECL$ z&fNIj<@Gp7xK@|G6;Jo0C&45m!g2GBr_Nlx^`X^4!gW24GH1B*UVoO5(D!~P84->) znTX>6VOm|9QatHTc939_5#czb`@qbKPaRerBwVXY)0P+CX9o!;84-@t_Bv+fySE=+ z9VA?DM6-*g=9xMug)PkKSkI8@IiyI!L%ym!^38 z|Je=_Ofn)IZ30;xBwVXY)5!S;?I6JF_lx9VD1!L^yuD;YKqXY`vm7NVry)rrgK=)yF}CNk)X@_B&ReIqYZQ zAmLhFn$~alkR2qLWJEaHm4LB4-UAmfe)=CEnCK(Zqf4+9d-S+8^sSXmZ z)upR)*Q|7qV3HBx_}GuH+VwrZ3kM0;>eAK4Kdp3-V3HBxXjew-agcBwT?>nY!SCAj zS`xbA>LeqAZdZ8a7?W_VE?w=N9tVj>CgCFyj$?1S>inbPwHlLftu9@)p1vO>m}Epa z{^N%`UhpsRo{vemR+p~6Pk*jRFv*B;l%GW1YfxV+60X&yy9lPgA0(J$L^#?V5Y<7# zwYqdS#Ps)^1e1&iN4t}wI!L%ym+tDAeqNDak`du(ccfGY3D@e<-6_-0T@p+(A{_6% zZuQBX@!38m;aXj~%Vzrbg9MX|2*++)Z8SMztNQn2Ov1Igbhps-?;r^#84-?lr&E2c zNVry)?s}U3Jtx5=Bf>E_V9DhB7u9oyF$ve|(%oCr^AZwFG9n!9&a-+PBwVXYccD$s zuShV-h;WoMvPnCK8M;q|>eAhf)AM!`Ofn)I4{!g0$uXa<-wnnjT&qiWX->a?kYJJ#;V9o&bWdu1tw^|5 zm+m(8x21CR4Fr>n2uJzmHEG|~#w1*;OLxsqzYmgNk`dup@~JZ?kDJu*oMRHM)up?C zr{8NyFv*B;lyAb5_T70*!nL||7xVP{ISD2i5sp{=@41sDU#K&HF$ve|(%sn8IRgnM z84-@h?s(qh@&9m8ea}g_R+sK7_l#pOAi*Rf!citHlXAVbI!L%ym+p?A&V5KQ$%t^2 zNztUumc}Gpt4nwJd*(D4kYJJ#;V2WYNx5EIkAsA3b?Iq^=^Ty(lZ*&QncV5wjOrla zT3veTVmePG!6YNX(ViHo4ic`bW5apOMIS*R$%t^2N$RA{UdMV8#WPmd>e5p%)A=q5 zCK(ZqS5EGoeBsx1hCC+WT3vdY=ho}|J&FXAj0i`WY){%Od`!Z1bj`1vst*Pv^n{b= z?HLhtnb7xbreDt6r-W;D>FKKJbpsNQOoAp6j`n0&ea}g_R+pXe5rj+D|wfB$#AGINB4^_5X^5Yjx>qY3(N*4iZcsnBqPGnp1iNG6$#hs(o^`_PdFSTm}Epa z?l1fNKI!0{s)K}Ub!jyL?I#=#5==589A$UlzU?PGCgECLS`|V235SCOlZ*&QThXB& z2MO2e(&`S{PdFSTm}Epa9=dAR{)TJoe!^oCuGOVgDzu+)I7l$bh;XzOHtKPZaIG$_ zwxRum!$E>cMuelS6jB`|T&qi~hG;+GaFAe<5#eYno>T`3*Xq*hC)!Uq93+@zL^#Us zz%> z)umN+w4ZP|NHEEWaQu9aJ^HmSdt!BvaIG$_4y65r!$E>cMzrHteb0XG?#ERJ3D@d6 zzO1*T{e;_bfMAjl;V8QU_iaDnF$ve|(rQiGPdFSTm}Epa+DcRPI7qlwmsXw9e!}4( z!6YNX(N@f=4ic`_rPZ^vpKv%xFv*B;l-+^*wx95rgllza6)^2591appG9nyp1+$6f z?Ic{QORJe_KjCnYV3HBx*!-S-`>*|T-A{N-!nL}zDx3Bb4hIP)84-@MJ8<9j6CRUr ztuC!Dr~QP(L4rv}grlvDS6?d(k`du3y94)aKjAS6*Xq*hi`q{(93+@zL^#@7 zll3@AxK@`|k<@;|;UK{zBf{~`XCBnQ{EE7t@R)>ab!jzB?I#=#5==5896MinaDUUw z&aKBm!nL}zs;Bl74hIP)84-@QB58GyaIG$_POANc+iMl8o>qcMMuem64&1l>gvTUY zt4ph_YCqv{kYJJ#;kfSEhxWf%tosR%Nw`**R*Ths!r>snBqPGH$3K^~alcge6CRUr ztuC#4tNnz-L4rv}grn>Z+_(LN$0S^( zjyiksoU=(p6GBqzxdakwBeKX03aI}?&>pMuowYszlvGx-V2MHz_5stDu z@X+=X9+PmbF0H1l{e;6of=Nb%qwEemwEcv~BwVXYt3qo(;c$>(k`du(D_YmriiB%* z?YmA{YqIPo91appG9nypW$x-A;aXisnBqPGnR!Xl960X&yRnxVfa5zXX$%t@#>wk|O9@^LagvTUYt4piDYd_&|kYJJ# z;b<$@uhO{>3D@eI2cMuem64m`B|gvTUYt4q5hXg}d_kYJJ#;b=QM)Z-xGT3tI8hxQW=2MHz_ z5stDu@X+=X9+PmbuJwyU`w54G1e1&iN7)^CX!{9|Nw|(K?%6?tyLn_p&~3*Meg7!W ze@xfeq!QQqe}C3!y3P~-lVFk&apYw>*tdCNN5ZwbHh$4`ohOEa1e1&iN0|=xWsSyq z93)(;>z0>I*Lh+%NHEEWaFpp_-{y%O3D@d6^A*!|o)``iOfn)IWjffmd16PxwYs)^ z?R1?dhJyr?j0i`W4)$dyrTSWtaILQYnVGKh#Bh*ck`du3)4{&Y6FU;F)wRxk({-L0 z4iZcgohOEa1e1&iN0|=x zZJyYXaILOqTsvLoiQyo@BqPF6rh|Rio1?x~BwVX&%WoD(nJ0#W1e1&iN0|=xWsi~S zAmLhF_x(3I7l$bh;XzW)9Y(R!nL{{`K#$VPi(K%P6zj8 z7o|!t$%t^2>0saHi5&^o>iXl&({-L04iZc0saHi5&^o>iYSgrt3U093+@zL^#THuy6Ckj)ZG< zedqS_U9HR$!$E>cMuekG2m7+8UVW`dxK`JhcTU%NVmL@J$%t^2>0n>>2CNPeuGO{V zuIV~Y3$9qijYu_NJHU7!2wbe$)Lg9MX|2uGO?_GPcedK@HNt84AQPuF>3 zI7l$bh;WqYVBhA69SPU!dfGpx>pU?WB$#AGILdUeFMDTde$|n1t*&SMbGptG!$E>c zMuekG2YWsJA_ocA>RR_-({-L04iZcnp{fd15$7Fv*B;l<8pK z=7}8%*Xnx7zo+XwF&reAWJEa1bg(ab%hux{;aXjvDGtpO!$E>cMuekG2m3Zp>`1s) z*YAo$^TcqFV3HBxDAU2d%@aEkuA}P(br0Zhkm$Nek`Y0d{e=6rpYWK3YafU96AlLn z{kxMSd?dnAb_ed;e!^oCuGOViLHh}Zg9MX|2uFJdmzH0TF$ve|dP#9;KjCnYV3HBx zD7ypqZ9m~L3D@eCetuB4qee-8_kYJJ#;V8QU_iaDnF$ve|(x>)bH`qafNk)XD z{l%Hm&lL&R>e64IJAP;f2__j4jp$pHJ4i6eh;WqMf%~?f@R)>ab?Mv7W?!&_1e1&iN7)^?Z~F<4Nw`**zJ=ZN z89PWY$%t^2{e=6rpYWK3Yjx?{n$g9MX|2uJ(wT;FpNuGOV)#Rvbl9VD1!L^#Us zz>$A;Bf?R32kzT`!ebJy)upM?&)#nb2__j4j%k1e1&iN7)^?Z~F<4Nw`+mb;ESsPdHvH5==589Br~)Un>%>)uk!-zmNBE zkYJJ#;V8QU_iaDnF$ve|(zIUt3CD4eV3HBxXjd9~o%@h*tu9@4*!>7QNHEEWaI`BX z)j`6wx^(sA-G|yif=Nb%qwEgcxBZ02BwVXYSAl+XkR2qLWJEa1?!bN9Pk2niwYqdQ z>nU%vg9MX|2uIl+xNrLjk4d;zm#)gqzrhX?Ofn)I?TTW3KS;P%m#!}Uzm;~7V3HBx zD7ypqZ9m~L3D@epE*f=Nb%qwEgcxBZ02BwVXYciH^-g?5l&k`du(cL>$jiiB%*>24wI zCmer2NHEEWaFpGF`?jC(n1pL}>8_`*Jl_ryOfn)IWq07d?I%1Y;aXj~drSKX$8nHg zk`du(cb?VPiiB%*=`J+wCmaqEOfn)IWq07d?I%1Y;aXj~n{M4Dc939_5#ea}4%Xu! z;aXj~D{%R<>>$A;Bf?RpgF~AqmeYn&olQm(wO*g~%YWC@@IQ&WC;v&rt6QdnLz^dd zBwVX&r{d5&F&reAWJEa1bZ{s$WX-QS60X&CadBv#7!DFlG9nyhIykg>Vn@QYy83&J z>O3(VB$#AGILdTz(CTc)pgxHN2@hY z3~N1;*4BA@N5Zwb{&BBS zohOEa1e1&i$Ny|~?C`=JH>?g4uGRI@dyncoF&reAWJEX?jyYyHdEL#cgM@2!U3;HV zohOEa1e1&i#}_U;dN}T>&#ev;uGO{gI-@#I3$9URKF)aoGNT3uV+e^lp* z;UK{zBf?RpgF~Aqb|hS@Ym;?Hb)Faw5==589A%z3w0UAj!nL}#c~JShD)Yo}kYJJ# z;W+U7hYuIMXk1?_60X&C$acMug)p zN56Skv)vi>I7qlw*XuSI)p=q#NHEEWa9q0ap~G$4oLwCxT&wHB4EO`ji5&^o>bmKXqdHG)uN4R;84-^5Y+pSN60X(t^GA>BJTV+3m}EpaK6%xF!*(k! zt_~8e)pgHJM|GYU4iZcLBB()HO+ z!nL|Ce$uGU6T?A*Nk)XDOb3THPwYszR@d?^Ms=PT4iZcl=arD$3eoiy3X8VROgA|Ai*Rf z!ZGuPoraHoa{cNc;aXi^+j~^!iQyo@BqPGH(T{c$9URKD zGWE40;aXi^JA72cMzrH7&)jM1 zrOzu8uGMw%Y2|mY%oE#jfMAjl;V9F=q0JLJ60X&C=X*zWo)``iOfn)I588R_;hN7s zs~!gl*Xnx3IiosH3l`jREpLBh4V)_k=5JumabaFAe<5#cD)!J*9) zI})zdweKfKb)Faw5==589A!E5}sOqs$Y-L4rv}gk!INZ93e%&vbQ=aILPdts2#NVmL@J$%t^2>EQ6JsqL$S zgllzOd*!Ik6T?A*Nk)XDt+-enBwVZOr{5T1-#!vdG9nyhr@z5_1Cns9t_Oa%ydR7E z50YS#5#eYnVAkUx;aXk0{dk0Z6iG12h;WqM>jv-DNW!(cKKJtx_JbtBBqPF6cCQ<} z=OhW&>UzShBkXHQf=Nb%qwIt^c<)RSuGMwXA4b@plLV8D2uInyZtxzUBwVX&$Gb+@ zXOsk!j0i{BnRM`8rX*ZP*D~yvN}}r~Nk#-+c355To~$HXtLvKkEW^I9B<`Msk3=}i zPO%H#Tb6`tb$#?f%dmef2__j4j z_0CO}VLxFKOfn)IZAI()drrc&y3T##GVF^?f=Nb%qpi$c9VA?<>x!o>!~V`Bm}Epa z%8t+r-b0#%YjyqU*~_p`H3=pe5stP}dOZ#juGO{m^Oj-1Z4yi}A{=Fh?gj7ZO~SRh zPS}1K_6;Y&BqPF6b{b#s-sB`)tLv9LEyMojB$#AGILeOc3*KX$gll!Z>{ZLKk2?t_ z84-@M^ZSDLiYMV(U0>UK8TO+m!6YNXQFgFj@SgW1T-W2+Z`tan-QVXvBwmIjBf`;k zcBnsBBwVZOXNN3X{o8fyAi*Rf!tuTXPh8k(;jro;;aXkC9KCGym+xx_2__j4jz=9g zUO4}{!>faYYjr*Dgk`Hga34EJFv*B;l-=tVrfxo>I!L%y*LUBsZ1rjPwu1zdj0nd& z_S$1%=kBQLAmLhFr@nXD>a*6ig9MX|2uInyZsE`$goA`@b-nc5Wvjn%FFQyu$%t@V zxAwLR7k&2VdK@HNt81fAEL;7nwd^3lBqPF6cCTCb_+iIX2MO2e`t?Q2R&TcGAi*Rf z!g1H$8!Q~K$}TV|F!dR zkYJJ#;m|X8dWuI560X(tq8pa2{_vuM1e1&iN816cI!L%y*E4Tgw)$0z4iZcB!kZ>Jc%UA#V-?y~y=OoH?nZ+a{f-bw)ZTG?EAmLhFf4J{*90!TJC*dOz zjtzhI)9vQkYemAfy8dsy<#<0xFv*B;{PVgkm+sKsa}uuA_02~t$IlfBCK(Zq@{_o< z>~~k+4-&4`^~J|6$L|LTCK(ZqvU}aqBiipE3D@fS@D|JQdrpE$Muek0zq?dV(&^`l zgll!ZeVgU@yduFQBf?R3uUk6ZJ`+i}R@cg@<@nqs!6YNXG5h{|PaSeie72Ktt*+4v zm*ejT2__j4j%^RwVCs9%2?q(+>e^_h<@h^Df=Nb%qwHQc^9A^AI7;43D@d6bid^|zaqgT zBf?S6$fkS_N5Zwbp8BTcI8P+OBqPF6cCVYd9CJ7lOfn)IWpX!F_PeVN60X&C z$j{2(+K2nQ0SP7<5stEZ-PEB^Sy>$=^}0VS z$9$Ir6VoKZam;$RPn~n2G? z1YIWdy=VF)T&wHi`;;}E9`E;@#NCtdkqAfGy{>=Ri(g$|D-y2NwabI1as7h?lZ*(* zJwCHefBl+QRtE{!>iWqB)3}a9f=Nb%qwHSSzyIWyR|g5#>U!Cur*S=s1e1&iN7=ou z|Kd%%R0j#y>bl?w<(1yj?*|Dc84-?hC9W^~-Bkw(*XsJy7UlVm#p{D4m}Epa$`!@l z*C0u_R@Y;nIgRU_B$#AGILejL{!Pz)aXk(auGQtIJmPvS2__kFH^-pwI3m1vo@*b+ zWZ%B#sD%DqCm9irveRGR_6F=oxK@{5g~|SdB$#AGINCc{|F1~6R+rw*$v%oCm}Epa z+D~G2kZ`Ro{Zvl&gCxNuBf`;sqpE|1Yjx?jYqGB;2__j4jGyxK@`w?I!z-l3~V~mIRZG2uIoR zcB<`>+mUdsE}ac{@7%5)uprL$v)L2m}Epa z+L?Ym4ic`_rL+FYe%mCNWJEa14&762Pw$R|Yjx?{%Vghh5==589A&5RskS$HN5Zwb z^et?%|2YXJ84-@MWBOFvW4$BcT3z}!IoZdZ1e1&iN7?y(s_hluk#MapeJh^qM^A!D zMuem6U_aINyf5p8PyQ{}>e9FS@7(UsL=sFgA{=cpQGc#TxK@{@6c4-84iZcZGaT&pWI-NVry)rY$f0iyb7GWJEa5-u?KgD{ei!I!L%ym!?K5?y`dflZ*&Qn|M_R z3D@e<^y{F%+ChRzMucOR`|mk*$*+&B4ic`_r77ONf3t%GlZ*&Qn?P0v3D@e$X zqp2gFbZm8yaIG#)x!?9r9|s9084-@R{`6i`w|zexBwVXY)A|knWd{i+84-?lrJ)`N z3D@eT~=^KaRtAm7Vb?K_y^@|P?Ofn)IZ~N7n?T-0P zI7qlwm#!|}x#%FlBqPGnu8h{>AmKW?map!*-?ZzsBy`2qNk#EYR`Dj$0S^vh{^9c2__j4j&>(Ub&zna zF5T5J`Me^*BqPGn?ntQ)60X&yyHh5gyCj%oL^y8!{k;|ri_i8k3D@eFWJEX~`tl_U zpSh}@GmJ^NR+sMHnw*!AV3HBxXm_5~;~?Q$UAhZxa(+dENk)XDoRKZGbGR`H*Xq*U zbd&Q$5==589PLiPdK@HNt4ntUPR@5pFv*B;lr!Xoc1}Ge;aXj~yK!>fPJ&5BgyW$8 zq=jQYQokFFNw`**?$Vrm{~*C6Bf?R>u`HD9we__k;aXj~+jR0hiUgC42uJzmwa~t+ zjY+sxm+qRKd>+=g&ed54+93)(;OLt>W<_sj5WJEabx#?vK-+s{n)j`6w zx^!3hWL`poNk)XDOjs7QQi0A(NVry)?v9_#eMm6Lh;Woi(L$RojY+sxm+tbP%&$l= z$%t^2iPwTwD$sF|aIG#qtuUFxkzkS$;V6^4g>t>NI!L%ym!7(q%o9m4$%t^YCq}A+ zgll!_>5<7?lmwHE2uGQuF0|R}n1pL}=_#1We3t~1j0ne%KYshd1AnnsJq{AC)upF- zZuzOdN0DHX5#cD4?S(cAACqt$UCS1y>Rm@dPdItro)JNp3H{J!`sKWRLbz6!p01i) zHz0BMBxn-hXitXK*NTK|b?GUw$@LEsOfn)IpV@Vt;iC`Oy*fy^R+pZ3n_R~s!6YNX z(Vo<+4ic`_rKk2L*P}=<$%t^YCl0HFgll!_>BGr&H4;oRA{^yP+)%F9RtE{!>e5q` zlk0;dm}Epa$`!?-U4tBxaIG#q4eG0zT}OgRMuek1$y$$tgll!_sa9WO?K%=nG9n!9 ziQMWS;aXjKI`>ak`}dp#lZ*(*aM`BA56+#g4ic`_rKgNHzS0g7Ofn)I?Fs4XAmLhF zdRqEfU$KJ(lZ*&Q*&TRj`w5RpxK@{*`kq>_g9MX|2uIl+cxd|xk4d;zm!4jK?qzn6 zV3HBxXiwhP*NTK|b?GVmCw|Ee5==589RECN>*0sTJgYiLxK@`|6S&tG>>$A;Bf?R3 z2Oipf!ebJy)umMte(+g4NHEEWaA@DeA{!+D|y{DI&onBf@d@db!nL}z+Mo6l4hIP)84-@J|771`=F;y~2MO2e(yE1z-PaBh zOfn)IWq07A?I%1Y;aXi>$A;Bf`;Ej;zN)!nL}zisVhNvx5Yaj0neOk3L|y z?%$VH2MO2e(rTE)YwRGwBqPGH_jeB*zW0cWtAm7Vb!k=4(_U!@2__j4jvyn&1e1&i$Da;)^DrEFMs<*ItuC#4d&UdwAi*Rf!cley9@>7wV-l{_bx2u9cr>zu z1e1&iM_YNg9tR26>e4F2w=K1U1e1&iN7)^CX!{9|Nw`**R#Sfe5<5sR$%t^2-GPU; zpYWK3YjtT=XzeE)pNS-xWJEaHiq`eDBH>zH`>s>gn%v3`5==589BpOp>LB4-UDuZN z!VlfT4iZcknV60X(t(z5RPx=*r$1e1&iM_VbqI!L%ymsU;x=;Q1l z!6YNXvDLSa9oD*V?dl-mT3uTGed|r^Ai*Rf!qHZ)U!`*&60X&yRqR(k!VVHlG9nyp z1^((F;aXi>$A;Bf`;k5~vOmuGOVo1+<@V{C<#Nk`du3y8{nxKjAS6*Xq*l z2(#+LzYjy2Z9NJGfj)Me~j0i{B9e8N_36Dv*R+o0G(0;<7wV-l{Ti+gsE;BFon5%kG3oAkHry1l-Cbl=}~ZZuQAhbwWd|M&auJU80> zL*5se1e1)2BY)RjoA#gGzs?go60X(t!k5mCetC`^B$#AGIPP4!S%1!jqk0@9T&wH! z7tf8(DBsWH$Vo8Ch;V%5s?Ga%e4wum60X(ti|yw|&p6W#5==589H*YQMZep9mR1J| z*XrtDFgLpPy>^gbk`dwf#m-yxr=PW?I!L%y*T+Gqy(eyj*Ai*Rf!cktQz8rmZkZ`T8$89$^x}eM% z;^&G4lZ*(*yRX@bi8v-01!%*+GIyMuemM-1g%QB}jXqXp8Sz??V3HBxD4!#J`E;lb60X&?`WfZ3 zq|7qHL4rv}grhri`~F+&Zc-g2T&wHkt>#7>9A^g!CK(ZqtA-u=hrDFt>LB4-U9Z`4 zZuI=4?I6Je}>4bECH(Vh0H(84->by!GY%sb#HH z{T(FXT3t`xY;JVQfp(B!k`dw1^SkA#-0C3VT3t&YKQ}tD%w*#IAi*Rf!ttn=zPj%( zS-Uz&xK`KeHk}(C{RTToFv*B;Jn>z7^q2hM9@Rm@wYuKLB4-T`zd>+~~qGzX}HlCK(ZqTW>g||G=+5QXM2* ztLq65nj7uV+d+a!MucOxvgiG(wa=~&60X(tkO$6OXhzj@3cJwYq+B-?`EHPqc#slZ*(* zhrV)jzx#zts)K}Ub!}H1r-_u7jqdjtJ4i6e zh;W>>$+7(>ANug>AmLhF8yCl0HnxKVlZ*(*3%+}7|B0>YJh3C;x*o^9=SFvIWCw}! zkz_c)phgQbEE6a ztS{bk5==589MfAL-#_XrAE^!!uGMvJalGh(c939_5#f0I$;bD%KmN_tLBh4V_9>1_ z?`H=ICK(ZqO%{&tpMK7>tAm7Vbxjq=6YpaO2__j4j?;c~eE-`M*IcgmoP=w2ZB`ub zUds*=Ofn)IA1LdW9(e{3@?$Oynf=Nb%rcu z*ZqXYBwVXYpAu*O#tsrpG9nyX?{Qqe$ANV};V}u<>e8p({cf>?1e1&i$1UGEw*Tc3 zbwA-T3D@eY?e_?P~tI!L%ym%bI>aj6|7 zm}Epa+Bf>@AmLhF`gVWLC3cWtk`dw1elKMgm+BzlT3wn_Y=5yGB$#AGIDQcmmg*qk zT3wp9{Ouw;NHEEW=7`x+b&znaE=`R-cfq0q1e1&iN1J$62MO2e()8<)Puf9(Nk)XD zP423Lgllzaiuat4+d+a!MuekHAghCfYjtTFdGGV=Ai*Rf!qFzF)j`6wx-?b2{zG<< zV3HBxIPBWJ`Wx?cM|F^Jtu9T6zj%%vB$#AGIR5({uj{||#=4*In1pL}Y07=}S$2?M zk`du(6Z&4~B_v#{OVj$dzt0X5Ofn)IH$Cyy{Ta8{{e;IPT&qi09gcjr9VD1!L^#?N zlX@H^T&qi0PYyfX4iZcab?NHj6(`t1f=Nb%1ywzkGF#alZ*(*4(Ctzw_aTL6CRUrtu9@)eofiaJ7zK@m}Epa$~`i@?tIbj zISJS5($)8m9cc#%CK(ZqcIQBKkZ`Ro-9_-1!|WizBqPF6?vd$r=ZlVmgll!_ZirnE zwSxqcj0nf0U%90J^84$4!ebJy)up>SRvl~y2__j4j&pwdto|_%-?|lh zXa@-<84-?lXH9jGaIG%gW%KF%?I6JF#->v!;KV4ECBwVXYcMC1q&khnyG9nxw z`+r;XU;fH=)j`6wx^&mmb^F*sf=Nb%V~fK#@BjR>zB)*_R+sMHI%uUGB$#AGIIei- zX8l|Dt@{a&Nw`**?m~Oij2$GHWJEaH9dz|LNVry)?xy>}UUraRk`du3_sI0R^QC^4 z;NLQ>F5MM)?w)p#V3HBxDAU2A%@eyXojYG=lS*9cbMQ0eziVpvp9GVPh)>Zn9UR&` zu_NJHU1t@C=854T!6YNXQKo}KnIUTq*O73ou7iq0^TcqFV3HBxDAU2A%@aEkuGO`q zI5bZT2MHz_5soq)9Lh|;9tR26>iXjc=j%K%93+@zL^#THaA@0VK_)I$%t?q`^IC2V~%)ob&znat`B@*zRnZFL4rv}griIc zhc-{_NVrzl9cRzid15$7Fv*B;{P5cMuekG2ZuIK>`1s) z*Q)o=*Lh+%NHEEWaFprb(B_F93D@fS+WY3~JTV+3m}Epa%5-pO^TdvXYjs`q-uXID z3$9UR&`u_NJHU7vf;e4QtTg9MX|2uGO?4sD*;k#Mc9Gu}O4=ZWDU!6YNX zQKo}KnN@e1`8rPw2MHz_5soq)9Ll|e)j`6w zy8e06e4QtTg9MX|2uGO?4&@%m>LB4-T`NzVuk*xkkYJJ#;V9F=q1?+^9VA?pU@zg9MX|2uGO?4sD*;k#Mc9vyPvy^TcqFV3HBx zDAU0~J1OZnNVrzlg~!gJq{AC)%BSp=j%K%93+@zL^$?&;ZDQ0*VlPsN5Zwb&OUs;&J)8yf=Nb%qf7^f zHc#wGxK`KEZ=SF7#Bh*ck`dw9;n^=9PW{Xy>uW{AwYpyRrujNg3A0Kp_9!l7sG+B~r% z;aXkC?mu7WiQyo@BqPF6rh|i?q|@&p3D@d+)PD1Io)``iOfn)IdgiXp6FU;F)%E#( z=IcB$93+@zL^#THaA@*wn{vArK4 zm}Epa^vs=}q|^IB!nL}#+H1bf6T?A*Nk)XDOb3THPwYszR@a%Yov-u6aFAe<5#cD) z!J*9)I})zdb^EL5>pU?WB$#AGI6i&(=EG6v)p=q^!nL|ywEKLWCx(LrlZ*(*@S@Fz zQ_idN#Eyh(b)Ek5`8rPw2MHz_5e_|brzh$3o|AB`u5a%$U+0P8Ai*Rf!cm{Oi+N&4 z!nL~Yxl{T3QRa!^Ai*Rf!cndh4Z8ZH;~?Q$T~FDuydQhmL4rv}grod@8gvy)4ic`_ zwc`%+qn%%E2MHz_5sq@*ZqU^@IY_ux*USs&M@x6Ng9MX|2uHd8IOwXP93)(;>)_?{ zqi4Os4iZcXj~i@h^RxK`I8Q{}t! zE_RS$k`du3=b?kHR_i!OxK`Kfw)3Mm?PLcDCK(Zqubp_0q5t+C)j`6wx^{j}`CI!^ zJ4i6eh;ZC^=|2`OKX1?KAmLhF&)Q~w^z)b4L4rv}ghS8V=}9`hRwP`j>mkpWA8qks zJ4i6eh;ZncJKfD82MO2ex@pV#(cFvdAi*Rf!l7sGbXSTTBwVZO;-{2Z-wW*^!6YNX zp=a)Nca0n*T&wHIC(Vy$USJ0aCK(Y9J#(kKgybOMT3t(?FhBbBayv*c$%t_1nL9m6 zCkF}F>iXkj=SN2`vx5Yaj0lIGxzk-+a*%MXuJayU{tk}pAi*Rf!cpc*3%dJE4ic`_ z^|D9KkG}OhJ4i6eh;XcZ(!~qcEX3aABwVZOt_{mvw6}u^`4bShLT;^*Bhl zR@YhU&yV()vV#PZj0nd*zxwFHw?21Bb&znau4k=R&f%8YL4rv}ghS8V=}9`hA0%9> z>znJA>rva;L4rv}ghS8V=}9^{NVrzlUiX_HZMv-;B$#AGIP}b&o}`n5gll#E{@&%h z{ke9KV3HBxIAERe!orP*R|g5#>N;Yr`Oywb>>$A;Bf_!OV-H$*&kaXZ2MO2e`scsr zMsIqK9VD1!L^$-!ot~uAYemAfy591S^33S7?I6Ju&o{!qb$3cQg zMuek0&#<7U6yzY`T3!Ej+uZ2siw+V@G9n!1I_iR+wvdB_YjyqnH*=#eKhwuSf=Nb% zqg)SOrKd*ZAmLhFyWUdn*FF0aNVrzlRll4Y{cUR>2MHz_5spvn z`@vO@pE|xeNVrzlHb0vieSXnFf=Nb%gR}aILP_e42mH zNifNXaOfTxJxQnEa}uuA^`lSmyo3akj0ne{*V=Jv!!>d4L&CMXUhxT@Uy)#v5#cCj zWK%weBjH+I*PqApL=sFgA{;;b@Ig~Q{a&1ll5nlAy+2&m{a8HTCBY;k!g2U1$4?!3 zbDUF?aILQ2oKx=KT|93m!6YNXQO@+I{M~?rYjqud)*QZnkYJJ#;V9o&ru-d;gll!( z?|tPt;l=M!B$#AGILbG#DSuZZ;aXiEc~`kdc=7un2__j4j`9t1%HKIjxK`KHX><5q zOM*#8gk!hiQ&ZdT9^aiwxK`KCPbtqqE`C2J!6YNXp?hTXB%OZGNw`+m{E2gzGmv1C z5#i81GM;6SaILOqzP;Rsyf`l*!6YNXp?hRJlOf?+T{pj_{5@Zs`;cIg5#i81GM+7w zaILO$kDJ5%iUgC42#4;G@r;awYjy2;bXh-jaSlgA5&kwDrVs%T&rvDzsj>Ri}Q98Ofn)IWkTP3rcc7P zx(?d6{2g4pZa{)bMubE6$oSd>3D@dcxw4!mE?)m2!6YNXp?hTXB%R(560X(t`q$6l zIt~dY84(WMBcmtj*QCJ^>-Jq50YS#5#i81GQI{$!nL|Cctv@RW$`*E2__j44n1?{YpEn$tIJP$ z#PwPdOfn)Ix(`Qp*XTVLq4lZtZ@KnyoPM6y*(RZX*GWc%;}!4NsK4O7J5~n?*Xq)% z@Y(b2Ai*Rf!qMKr>LB4-U3xdKT-?u#1e1&ihwksvU0ZssNVry)ek#BANgoFZCK(Zq zgI~99KQ4PO$w9)ky7b%i)y2KfNHEEWaP0q*b^7P6w|jMvaIG$VN__f49|s9084-^5 z30EB?T&qi;cJH{z4iZcLB4-UHa?u zn2UWJB$#AGI6k(;?NiTRYwzkH;aXk#EBd|9+d+a!MuelC0aOPG*Xq*Qz>$~OL4rv} zgk$YD+%&b-jVr2ygll!_tY-Z$*+GIyMug+Ag`Z44W=Y+%xFg|OT{=5E=TbXJFv*B; zv@^?k93)(;OJ|uIF77!=f=Nb%qn#mF2MO2e(%JF}m-{$KFv*B;Jn*C|r}m%OzdA^` zR+rBDf3?aE5==589B=*Om#22yweIQNk#MapeS3MvU<3D@fSdHFtg z$Q5>wV3HBx&~q7j+CrZtBwVXY-zLvr+%J^`lZ*&QStE6-tbAG>BwVXY--@qZ?c*T9 zBqPGnzR_0)3D@enUzsyp|5ZCkFv*B;w8=zukZ`RoO(|}?(hd?#G9ny5Ir^ljhn{s< zb&znaE=^m$dzBp|m}Epa+N7vDNVry)rbb`7+71#-G9ny$j!aL7>F0`sYjtV*b?P;C zkYJJ#;dtbOUpMuPA0JsABwVXYQ@q`-wSxqcj0nfaf4jrfD}Q%Xb&znaE=?oX`I;Rh zm}Epa+9b6)NVry)rm7b$I!G|dh;ZmROFccQ_k)CMb!j@h{nvdQB$#AGIP|=zo&uGF zgllza%Khy{2MHz_5so&YuMQHf)un0u3%=pwAi*Rf!qKiYR0j#y>e5w*3l|+Em}Epa z+7*-PAmLhFx_YwqH+>u=m}Epa^gOkmlGgh{!nL||6==sr2MHz_5e_{UuBW}_AmLhF zx|(&uqJspJj0lIGZ`V`na*%MXE?t$oaM3}6Nk)W2&*|&wdpStBR@d&=&5f>FbdX?@ z5#eZ8MyrE_Yjx==={FZ0B$#AGINB9nImRSht4mjVC&xj8Nk)XD9Qoz#wHlLftu9@) zp1dC$XbCK(Zq@(HJVSgM1BYjx@F zl*#8V2__j4j`B&ZdvU6Rgll!_E}O~U4-!l=A{^x}&O-YeH74O&UAkLn@^_E~lZ*&Q z`OCS`{??93xK@|$dYb$_C&45m!qM*7s;?CZ*R|`fJTD=kJHdR$k`du3YZ@$Qr2>7H zkZ`Ro-Gw$ezanwBr1?mMqnwc~v~##I3D@e<-E@=lL=sFgA{<(;zpTPvkAsA3b?L6a z$@wk`CK(Zqc1L4%kZ`Ro-Q74jZzsVdBf?S6^cUK9gE0x$>e5}BlkXoSm}Epa$~P9> zlUk31gll!_Zqv#4C=yIEA{^zLm+q~t4ic`_rMqS)-v>!B$%t@hUyk;jb4{3aIG%gRX&-QkYJJ#;V2W9g*KBJlW?sr-5o!f`;cIg5#cD4B0URGUn>%>)up@q zC-W;3Ofn)IW#Xl08mfbYYjx>qg~=R_1e1&iN15C$v{~Pngll!_sf)=xkpz>B2uGPf zF0`5Dn1pL}>FJTlT$BWpj0i`Wr0Q9j`dX22tu8$UGnwy_V3HBx*zJpdUbx@m>I`{I z!nL~eG|yyCO@c{Agrhx)RF8v%Yjx?Vq{+OU1e1&ihxR>dGyO3M*Xq*KRg>!mB$#AG zILeg-#~% zwYu~a<>dMx2__j4j&enDXxAXeBwVXYPlNhuX4jEmk`du3S4M|+Ep<%7wYv0FtFN(k z9SJ5G5stDu@X+=X9+PmbEcMubDJQ`=8?Ov1Igw7P@#6AlLnCK(Zq zvODn5_7fhHaIG$_Qlb5X!$E>cMuelSuu)$t60X&y)i$)Ba5zXX$%t_1_pj|IJSO2< zU0O9n`w54G1e1&ihxR4cr-R;e60X(tu(A$|_7e^V2__j44t*Z#Q%(*NuGOVgT(qBX zI7l$bh;SUZ){BQH-2TYwAmLhFT8&2g35SCOlZ*&Q*&TRj`w5RpxK@`|)zN;!;UK{z zBf_!KMmr6=Kk{MqI7qlwmsSVTe!}4(!6YNX@!=)A4x69%;OZdZT3uS@Nc#zgg9MX| z2uE4dU}*aZk4d;zmsV@ie!}4(!6YNX(N>zO$3eoiy0q%liTm3@f=Nb%qwEemwEcv~ zBwVXYt7mCH;W!QwOfn)IZRM|e93)(;ORIosKjCnYV3HBxD7ym>Z9m~L3D@esnBqPGn zR>rFi60X&yRr0i-a5zXX$%t^Y75=J&gllzawLk4A91appG9nyprNZhU;aXi(k`du(D|y!AAmLhFTGdng35SCOlZ*(5=6r2G;V}u<>eA|@ z+D|wfB$#AGI5e-+6jJXw3D@esn zBqPGnc6O-8LBh4V&MFS=CmaqEOfn)IWq07A?I%1Y;aXjvDGu!?91appG9nyhO@l#q zrRe=2;aXkZbt4=km}Epa%J=5J%@e!tuC=1hCY89>zx&*O&D43~e-cbGBL1(+_y1ns z>dSF-BwVZOOLxuGd15$7Fv*B;XpK~@d@2VC*XnxFpJ(bkF&reAWJEZ$MyjSPa*%MX zuG@Y;Q|F1{Ai*Rf!l5-%+dQ!&;aXiE|Mg6rCx(LrlZ*&QnUD2tp4gFat**m$z9Y)Olh!NHEEWaA=LxHc#wGxK`Jv zzfwL+$~-X~B$#AGIJD9?2>S;uBEGH>O3(VB$#AGIIbFY=pXWujqCeC!nL}7 z_NAFRPYeeMCK(ZqG9B#OJh3C;T3sLg{7juEhJyr?j0nezU%gX*?ZW!?I7qlw*U_Ju zsq@5ekYJJ#;W*~5UHXmhxn6aUaILQWFPN$G#Bh*ck`dw18mVob*pYCpu4Dgurp^`1s)*Y59|sq@5ekYJJ#;V9F=UUzfowIbnKT@QHoOr0l&g9MX|2#3}i(8{NBkZ`T8 z8%~?4^TcqFV3HBx(7Fj)`BV-PuGMwTJ7(%UF&reAWJEZ${)6rkl7ob6b*(vRrp^N>=ZWDU!6YNXp>>ejJh3C;T3xR_bf(S|!$E>cMuekG z2m3Zp>`1s)*Xf7M)Olh!NHEEWaFpp_-{y%O3D@en`JkCPPYeeMCK(Y9t*@n(PxX04 z!nL~gIB=%U6T?A*Nk)W2You!BQ#nYuR@d*|I8*0|;UK{zBf_CIQrkSSBjH+IAKrha z&J)8yf=Nb%qf7^T-QBO_AmLhF2mRMfohOEa1e1&iN0|=xZJyYXaILP_?>AHDiQyo@ zBqPF6rh|Q(Cw3%UtLxA=%+z^eI7l$bh;WqYVBhA69SPU!I)C4pI!_D-2__j4jxrtW z+dQ!&;aXjH>@!p6iQyo@BqPF6rh~nn;?a9f!nL|q6o=-C;UK{zBf?RpgMFJPb|hS@ z>o2o2b)Faw5==589A!G#>!~Uo2MO2eI=?tHPYeeMCK(ZqG9B#OJh3C;T3v?~hvtdl zAi*Rf!l5-%+dQ!&;aXk$6vsU}J4i6eh;V3)R6VU%UaBz(*FFyICmhE?LjSIlj0i`0 z=C0RMhjNf`tuDO^+D|wfB$#AGINCe7R1OlZ)une+`w8FeJ+B0lj0i`0=C0RMpmLCK ztuFmkYCqv{kYJJ#;b^~6)j`6wy7b$%*==@^V3HBx&>E?FDp$ur!nL~eDWUy@<2Xn# z$%t@hjZ{5dT;C7;Tc*{ePdn`=91appG9nyWBUMjH%R$1my7Z|%{-qrxm}Epa$}@Mp zp7xf5gll!_uaEW`@&gT&qiGXSc1fg9MX| z2uE4}p>O*Mk4d;zm(DWZe4QO6m}Epa%I?5@+fR5*!nL||w*0TJ+d+a!Muem64&1l> zgvTUYt4nA7r(Rc z*cY#`g9MX|2uIl+xNrLjk4d;zm%dHzvdRt;Ofn)IWxbld?I%1Y;aXk#R;>Mm>$A;Bf`-psntQkwYoG_ zz5YXXkYJJ#;m{hXZ9m~L3D@ehQsSXmZ)upSwk3QZG5==5896Ovp-M9UO$0S^< zOINL5bBrA%m}Epa$~`iD+fR5*!nL||_5EW<+ChRzMuel?IZ$6K60X&yy9oYrm>ndT zWJEa1JugvTUYt4nuvtUA~Z5==589A!V@ zzU?PGCgECLx;tgd1MMKeBqPGn?yRY=6$#hs(p@&6-ro)qOfn)IWq07d?I%1Y;aXj~ zTWHCCc939_5#cDi1NUt|;V}u<>e5|L*X?5m2__j4jx7$~y#Mph`ubXtaIG%gy>-w^ zJ4i6eh;Ur-&dvI_?pya09+PmbF5QLps2Mv*Fv*B;v^(hPagcDWF5OM{gT3q^!6YNX zQSOoHb?1wI2T8bAm+lJGe!_8HLV`&~griIchc-{_&N^$h&L)+(*1vml`R|$<{wKjC zBjW!`Yk0MJVn@QYx?WQpnkR;X1e1&iN4vvR^Q(@8Yjv$t9GWMFg9MX|2#412YV*X7 zgll!3bLMQFCx(LrlZ*&QnGOzG9Z$zW!nL~YUmThzhJyr?j0lI;@M`nKj)ZG`1s)*SYVVt@FfikYJJ# z;m{ghZJyYXaILP>-!oh1iQyo@BqPF6rh|i4!PI+B!nL~gdiQLdCx(LrlZ*&QnGOzZ zp4gFat*-UoHCyM2;UK{zBf?RpgG0HNT91Q-Yju6>^w~O33$9UR&`u_NJH zU5_|zw$2m7L4rv}ghT7ews~Sl!nL{%I(4?r6T?A*Nk)XDOb3THPwYszR@Zr_%+`5g zI7l$bh;WqY;LzrY9SPU!y6mLcI!_D-2__j4jxrq_+B~r%;aXi6&(GF*VmL@J$%t^2 z>EO`ji5&^o>N@r9vvr;r4iZcI=%oD>wf=Nb%qf7^fHc#wGxK`IXN6yxHVmL@J$%y8NXJ6}UMZ&eZo_5%5ohLR2 z2qqa3jxrq_$~|}0LBh4Vmb_`U&J)8yf=Nb%qf7?}?WCmloP=w2J^rBCI!_D-2__j4 zjxrq_w3Ct?BwVZO_Wfr^nkR;X1e1&iN0|-|+7&?#60X(t-*1?$^TcqFV3HBxDAU2A z%@aEkuGO`4wtSY9d15$7Fv*B;lt^dbF^+=-lZ*(5*6?cc#Eyh(b?xz***Z@Q2MHz_5soq)9JG^?j)R12b-nGC zvvr;r4iZccMzrHNe|l*1 z#Eyh(bzQw=w$2mVae!cw5#i7?cWs{7k#Mc9$89rP=ZWDU!6YNXQKo}~o}|<7APLv% zTCvq^ohOEa1e1&ihn~4>^TdvXYjvIRl-W8@3$9UR&`u_NJHT_4+Qw$2m7 zL4rv}griIchc-{_NVrzlM;|*|=ZWq80Kp_9!l7sG^dz0$4-&4`b@Ilub)Faw5==58 z9A!EcMuekG2ZuIK z>`1s)*B2i!Tjz=4Ai*Rf!cnG!Lz^ddBwVX&`}@q+d15$7Fv*B;=$Sh`NvHRmgllzO zeUI5XPYeeMCK(Zq`pjL-6FU;F)%DbWtgQ3IaFAe<5#cD;i3abfN5Zwb#&@p7zI`N^ zWJEa1-={%WvGiJzaILPd{$VBdA0)vfBf`;Ez^o1uuGRJbezOw$D3V~35#cE33xlpI z>NrTaR@eP+T8aH2NifNXaOjykJxM1A3D@e{@TV)WuO$g484->P4q9i}`fss!CJEQ- zdcgH7u|FpXCK(ZqwnA(@4ic`_b?5)C#6F`Wm}Epa%Fd*N_cA5nT3z4#=1T0BN`gs7 zgrlw4TaSZ;YjwTv>XqF0_0oSVTz+2c;>si=!l7sG^dz0$a}uuA^^z-AV*gqaOfn)I zZ3X4(AmLhFw_myv`|y%rk`dw1Gk3ZxMaMzHwYpBbcqR4|Ccz{l!l7sGba#y$BwVZO z5f`q+zQ`n)WJEaH%G}jK!nL~2{`gAl?@WS8MuelSFkT%bT&wH;A6|)ls!1@(h;Wp7 z%!2OP(rZP+wYm;GYbEyECcz{l!cpc*3%dJE4ic`_b?v)WV&8BQOfn)IZRPsvAmLhF z4}Zr>?0-&zNk)WYcArlztl8(_>LB4-U9WljO6=oKf=Nb%qwM^?;JxBWxK`JDj$4WS z=t(fih;XzW5$bV}aILP@hp!xMww-4gB$#AGINHt*)j`6wx_*7o%F(9V+ChRzMubDp z-04X={alf7t**89TRGbDxpt6Xk`du(J8e`43D@d+_{_@D4omDH!6YNXvDIS_T6oV5 zM^pz1*Xnxw9_8=gbL=3&BqPG1XYTYQon9*vuGRIFm#-XM_-s2!Fv*B;{CTY%7dBjT zRCSPWt**^px^i^qv+N+jBqPF6u6Hfyu6P{>3D@d+>WEV z{cUR>2MHz_5spvn`@vO@pE|xeNVrzlyEa}q`uw7U1e1&i$5Fc;yXxe(hJ%D_bsg}q z^7nkvL4rv}gk$|*Y`JRbmEj=aT3tIlaOG&XMF$Bc84-@J9{$VAFS|J$BwVX&v-_+Z zZM*0o!6YNX(RP-r?*|Fj>bhsQat*&?dW(9t(NHEEWaFn0KrHeni__s`}>wsUb z!0!hMCK(Zq@*B0(zk?)Pt82#_R^a!X1e1&ihwhQllXUv~LBh4Vp8ow6_`D*)BqPG1 zdt~$^og5@wtLtIct-$9l2__j44&5W8C+XxM;ktHRvjTrVNOVXtA{^x}&XoT~k#Mc9 z-wiA9caX&0lI9~3j`Ejt%71G~xK`IszqA5>&q*-Jh;Zm08J{zdaILN%e|81VOGq%u zh;WoMnJJ(9kZ`T8?|*6q&aX%?$%t^2GqNe4!;x^Ut{cu1e1&iM>#{DTDbA>`m;;IwYvWK-W51+C&45m!corjr~KW3gllzeblM7h z{~*C6Bf?R>u}o>T5d994aILPX6IbAS6bU975svcBYf7u0$U(xjy7qs|3Va_V!6YNX zQNBS=`8y{G*Xla|s1^8LOM*#8grj^Dp7M8R60X&C%bQl<`#A|F84-^1jehEyg#+W~ z3jdaAbv@^eD==pu!6YNXp?hRJ%OK%eU1!X$z`TS6lZ*(5?ve3KhJI-}vB$#AG zICPJUXMH4GtLxyGtiU{x1e1&ihwhQ_%#ws_bv@t(D=-%&!6YNXp?hTXB%NL>60X&C zalZodT@p+(A{=ERJmncO3D@e{Z^;VGsYx)&h;Woy_>?ExBwVZOnOm>GyqyG-j0i`W z(D$C{lW?uB`#rV%9o)%3S0tEZL^yPhjGm;^`$59Bx^CaBoG0#L2MHz_5sq@jr1v!z z60X&C>tj~nIt~dY84-?h<)`OCjnT3!0op6r)Of=Nb%qy5FH4ic`_rN2IteP2m1$%t^2 zonoij-m)DD*Xq(=(aHX`B$#AGINBLNJq{AC)upq6$v(U!m}Epa%Fex0ZLi;YUgCet zw7PUwGucm=1e1&iN7;dRs_j|ak#Mapot;hgMJB-{Bf`U}dvFv*B;w8=zu zkZ`RoO(|}?(hd?#G9ny5Ir^ljhn{s%?=VwG9nypl3E=kT&qh{)r%G#B$#AGIP{#Qo}Sce zMZ&eZG#%dl>pl(=Ofn)IdR|mdfyzO`b?qus?r$$TNN8g2BqPGnCiK-o!nL|It$)Ec zd>kb1mNXxUaI`B8)j`6wx^&gy!bJxOCK(ZqcEzMRNVry)uAZ#@O&uGN}NVry)u4bLE=pex)Bf?SENL{L@*5x4KT3xy- zcj2Og1e1&ihn~~d)Aw?aaIG#~UA$`1L4rv}gri* zk`du3KZ&c_&+eFnYjx=^g30d(2__j44y{w%eh0@ST&qiWLri|pNifNXaFkDmRa&V) z?>PzA>e5{ulg}#>Ofn)IeAhGlk-FpOfn)ITCcyIi;hXSR+sJyoSg5HV3HBxC}+qE<$7&>KS;P%m+o$y zoVSx;k`du3XZj26yTO=*Yjx=^&B^x<5==589OWC!f_975?;r`+>eAh&lkZU^m}Epa z$~UhC?RqN*3D@eCgECLy8CzXy_N)%j0lJJ7ir&}$0S^< zOLs9(zMqp|k`du3-{=?0_1gHk!oOu&UAh~4GG`#cBqPF6CKC&7mN6#bT3x!Ud@?T~ z!6YNXQ6?-4Z6-4&;aXj~JKnRJt|P%DBf?Q8MGM-cTR&GMT&qiW`A_CoB$#AGILgFp zK`Rx=LBh4V^t8fc4o8AXMuekG?iSjtZ%o3qy7bh=WS&TZNk)XDOduE9%yLY^wYv25 zh-aByM}kR4griJS7qn7=UMmu=)upFkCi7hqOfn)IWg@)LX2@d_uGOWdc_wpe5==58 z9A&btCz0xLkZ`RoJ(V<>x07I!5#i9jXKkiGCgECLdb(j0i`$(lBVH0=*w3 zT&qh@iA}D5kYJJ#;V4&3hIWl*Ov1Ig^t9XLIt~dY84-?h1j}3&Fnf7Ofn)I<;v*LuBDDixK@{*YSnesxL!+wNk)XD>?b_5{e;IP zT&qh@=WhNXJ4i6eh;Xzgi|hMA!nL~el<_amv4aGYj0i_r(_qj_1$xg(xK@{*mOkTb zJ4i6eh;V2>R;^SZ2MO2e(o^5hIMWUiOfn)IWle)YD;3B=!nL~e^!l~$wSxqcj0lJJ z$8Gxwk4d;zm!87k`Q3JqV3HBxXe$WR;~?Q$U0O|G&FOZKV3HBx(0;|D^jeW{tuC#K zF#S$DNHEEWaOibv`w5RpxK@`|cevmbJ4i6eh;WqMfrqxA@R)>ab!nA~`=4Y72__j4 zj<&)^eXU5iR+m=Wcude2F?R+m<5I^{q+{(oC%9w%2> z>zzIE=cdq^;e2y>Jt zO=SlOuhpfePA#6{4iZcv!W?Nka8vdP*Cf1Fm!6(A|7drRU=k7LC{O;%$3entb?GT! zgG25h!6YKgk+uUjWuI_O!fSQuX=Zy4xPt_fh%iUGAGOhw3iO(j@LFAZD%+-qyMqLi zh%iTaqFi>6@LFAeO3%6b%T#xeU=k7LC{M=A4ia9gOHavr@KASTb43BBedyjGWLoUaLz_k-TlM5eEn+5n+zB9k?m`gliIBt4mMA9NNPjB$z~mIm(kf^Kp>y zT3vdo=ZAK62MHz-VGhmtG`-X7LBeZw>FK05?BWg*Od`S@npbKHDF+F!)upGbe!g-C z2__L?4$WOPO_hU$*Xq*KVqeK#y5?-rIPraSJlRHQ-i3oF~?=3ZY zQh^*KyjGW<-aBdscaUHb5#~tSft#{VxF+GXy7Uy{uW#!P5=q<12394iZcv!W?Nka8vdP z*Cf1Fm!6t_+Pdx_!6YKgk+uUjWuI_O!fSQu>F@81bq5J15n+zB9k?m`gliIBt4mL@ zKl&r?Ai*Re%#pSOH)Wr2O~Pw+>1q7WuI3IBOd`S@Ws|^C{eF<}T3y;IpnbyjS&amf zh%iUm4&0P|!Ziu6)urtScdY0R5=E})YClC93+@TggNw#)G|+Ok?>kwA6|W^r+K0|NHB>Aa|}GNWwr2) ziTP(m!fSOMy4Fxn^F(uyU=k7LIA~y->aU9@WCsba)wOutp`PZ6<{-f&BFu5_GC8{c2{OC+az=BmXV0)s?1rJ&1}B$z~mIc~jrVl{b{k7ox7uho^NcsN;%Cp`PZ6<{-f& zBFvGdgH@R)wn%ubuFgI~JxjtBt?2LOu=>UaKoj@p_sknu7$B zh%m&1}B$z~mIgY<#YIWb!qq2j9*Xp|C%R@cQ6U{+_ zNko|AzFVeMFQ4(AK7X`Gc&)DX&`?kFM01c}5)tOOXY>)(tc~(Ku|>jbbzOV(P*3wj zbC6&X5#~5}&4Fsnw72qcknmbv@5~tLX`W~f5=Ai*Re%yGo;j;i*Z@l+AV3l?WWd{kb)%AKk)YClC z93+@TggJh>->mBTSFXGnkSlr1e1s`$DvD4tm^MynH?m&R@XPq9O`MF zXbuugBElR~wwhgS{J=%oLBeZw{djn&r+K0|NHB>AbLja&WuDj~;kCNb6tAawqB%$~ zi3oG7^W!{I|ST*VShY^)yd32MHz-VUABPKe_two^}3Nk?>kw$Dcja z(>&1}B$z~mIp)1JxB9@^Cu9c+uho^NcsAi*Re%(3mWr&Q|??v))RyjItT&mZb(o@fpdOd`S@<9;%~y78`0W(Ntc)pf)L zLp{wC%|U`mM402sQx{YRy|`I+knmbv=X`Uhr+K0|NHB>AbF8rS!s`9=*2)eNUaRZU zZw>V{Pc#P!CJ|wd-eU`^f4XyYc98H|U1^Hf(>&1}B$z~mIns2nD)U6`wH*0xd9ALg z-x=y@o@fpdOd`S@TP{Df`s$SXG$U(~@LF9fUO3d#JkcB^m_&p*{(1bOYNa1uo*g8- zR@cSJp?RV?NHB>Ab8LRbqUy$*=4S^9uk&$SG}O~P(HtaJL=q9^SlU}uUBBwKb{rs_ zR@b5Ff7d+G93+@TggNehb5V8VF2B^QuSLRZb)_j@PxC}`kYExK=19}Qs>~C$hj-+^ z<+ZvlPXGI;mODr=i3oF~eZrN#t(Oj?CgHUohxQ5Eagfk|*OQ1a$5rWP^^-e(nH?m& zR+oMX+9zxd5=ZEQolWQe1L3r~^sn-# zZ@Ys8lZY@!`HjjB5?<%yNDl22w&Nh7-#bqt!W^$Xd203Svis~fKsc=~y-Qs9TX&FP z5)tOmGg9^KV!a+DyjGXq?N)rl9VD1UggK@?y0BXOzAR zpIBJ!{;{>PgM`=W(qEr8xhPa(9qm5)tP3%e(WcH&*ykc98H|T{=7a z{S)pW!6YKg@zkqxtL-j4Av;KTtuCEqp7fYINHB>AbBuZ6J4i5z z2y;x_Wmfgj&Ogi!5?-rIpNh3l*xuVoFo_6rluz{8LBeZw>C^osKXnHQCJ|wd?{0m3 z_1SCh$_^4yjGW{cspI=4iZcv!W?A+nH?m& zR+pxc+9zzUISD2aVU99M%?=V?t4mYWXTRqT5=E#BFxc!YFhQ{551QiB)nFarrg>mYz`7kBElSHLSO0JhlJPa(zO1}Z@Ys8lZY_K zq8+DHn>;rFUXxbKOCLNko|A`mY~UJ-yXR z*+If2FSR2MHz-VUFvz+P&KQ{BhYq!fSQuYVZ1s+(Cj#M403A-~U`y_6h5IEF=Fduk&$C z80vj-fjda(dbTGKVU7bY*`?ZM|4r>UKsc=~U46fLo;ye|i3oF)I|s6ZgxBiQT?GF< z#~mb?M1(oce05@V)z7xf4ia9gOLs#YFxwp@m_&p*);MKCHR>zdWCsba)up>Smd7G8^9VD1UggIW?dR+DHg59%&gxBiQU4fTPa|a0~5n+xr9c;=xv32CoK%Px1@ml9Q zbJG8=so_5oOd`V0md^dnqUO6(p4L3EMZ#-!U6UM|Cz^u9$bmdhGzSSL5n+xr9cH zLsLU@kYExK=Fl^|^prpStVnpRuJ0c;kmrf!Ai*Re%%Nv^m3d-|gxBhtee^({Cz^u< zlZY^fp5djZF6uZ)c&)B4A2X2WiRK`|BqGd_rh`qHC$>m?#JWn(S2__L?j-S0X zw|VTtd7juJ;kCNXJ8>Y-6U{+_Nko_zgTO_jbb$#-T z^tU$66U{+_Nko_U!hyfjmz%2MHz-VU9E%Y|1>bMZ#-!U3K+9o+p}v1e1s` zN16^c>7MO;93;F}*Zk`S@;uQTB$z~mIns2nDf7e@39r?4+>ZwGJkcB^m_&p*(sZyX z^TZYjuhn(R%>#L!XbuugBElSchF6&3=Xqj_gxBi2?#+QbPc#P!CJ|wdXE*wI z^NaKHJh4T>YjwT#yMa7UGzSSL5n+xr9c;=xu|>jbb?x@Yfjmz%2MHz-VU9E%Y|1>b zMZ#-!-S+2!JWn(S2__L?jx-%?(sy|CYfi#zb?yAOfjmz%2MHz-VUDNQAJ;7WTAnAi zNO-NTr~YYro+p}v1e1s`$7i=#xA~7}KcA0-gxBg?w8He>%&C6vLxM>}nB(96Z>^^L zzmv0rgxBh-R+--0Z;CreFo_6rOuBK6=3jd6+c~Ewc@LFB}Z-eQ*_JQso!6YKgG3w?O zn)GC@>>%N_y8f`~^j?2ICkZAIVU8~^AKiRlXWIcv!fSP3A^BElT2TsW%P zvm(JHBFyo~VefT!T{nd%R@ZT-P47*ZAa~yHW;_mvl&&duFUaKo@nd@!!Nq3N75)tP3>$d%b#c zc98H|UB9_xdhhcS+(Cj#M401`htKTZ^^3XLLBeZwr7d&4uk7Rw5=!0eLi z?%&MM4ia9gD{YzU?K|Qi!6YKgvG~)Qb$2;)L3WVvT3x?uruXjH!HlZY_K);GSf zbe}Jtnva8o*Xr7Oc{)!VagbmV5#~6hS+?}0|1bv$uho^d%=P|fJ3kH*Od`S@I~{Q8 z(nH#d@^O&xT3u<&T<^yt4iZcv!W`#cxNzyNUo{5_uhn(#Z`0rN5eEq-5n+xsUfycy zq-EwH;kCMc{Py(Tfg=tQOd`S@KbY{!-9LNX93;F}SK2by+j+!6f=NV}W1E*gc=x60 zdvW@EPQq*J8tiR1;vms#^^=GgaZGdv39r?aw#?x;l4FGU`^0=C%yG*huT1QfpA`wO z)wSo!gLpkiFo_6rq*ri~zvd*oR@cmr4C3Dv2__L?j!|?>Px35n+z>&M?W}OGtRFuK(C#5bswcm_&p*(mUKF ze@`UgwYt)lIlS+ZU=k7LNbl5be{Uz@wYo-47{uQX5=~Adzuhq4|ZiD!HPJ&59m}8FzCbZ|>W9JMcyjIs{dk^Bg zgang_Fvl&kCb!d5N%Q*^39r?)`9Xs?zaqgTBFu4GGqwGZzuGw*39r?aw#?x?kpz>7 zFh@GGZ2P$=39r?)*1#ancS$gb2y={md{KM7m+hRIgxBgyTjp@yPJ&59m?NF(xBasL z39r@l#>_!{{vg35BFyoXgU@S^nR`-x%}IEzuC!$ipQA`Hi3oE%w)VyCEz)=I^mmYi z*Xp|9)Ioe6B*7#i%#l7pw*50F39r>P<;+2Rt|h@FBFu5^?)s?o)VeUhMNko|AvOOMc@4wV$OC-EjSK2a%`4tH!5n+yZ z*Lte`otDkWNO-NTb#6${Pu*#UE61UB)nGFH*OuoJdp&Gh%m)S_3w`kV%|=INko|AkZVU(`z*DYJ_)bY_0G@J-@$$T-xUca z5n+yWrJ?d`6C}J=*UHbP^Thq#L4rv{nB!xYuTZ`7oLyrf;kCNf{PiHN^04c*B$z~mIZn8Do$Amn_H1;{Ai{U&dF{v1-`ht*|6NZa!W=IgGqyVQ#>v@1 z!fSQur_kR&NPJ(fb1aQwYqdR(BH#Lf=NV}W94mM zYj6MmrrYa*|CZD0(pgP^pD+m~5n+xCpM9=eAVAe@`_DCJ|wd>%N_x^&jx-?vSI zNko_7o!%`HUaL!=Uiy2(Nic~BbCgeB`8Y^;tuB2E>+e4&!6YKg@ytm-Y`?YR ztn47+wYv0avcJcj1e1s`$E8nR(LU!k1e1s`$LPzKv_J68IoUzNYjtVb^5p&QAi*Re z%uyyq*+If7b@LFA(;vM*qJ4i5z2y;w3WY6|9ub+|~B)nFarjaZB%pD|{M1(oYBsDuoc&#o? zRc{(`kYExK=2$Ryi?%i?>3o-j*Xq)Ac=w0>I7l#w2y^W7;F|60cUYJmB)nFarreK@ zI7l#w2y>JPeRh!WT3wpf@A`-z2MHz-VUBX8Av;KTtu9@4xM9RWf=NV}qg*k`4ia9g zOIJ@uKkCOpf=NV}W4(K?n50cgdd*3Atu9>!nmpnl!6YKgaq~U1C%u+-FUdi|Yjx>r z))^xX5=e5xY8%7)?m_&p*9(>`6i3`*2B{@iVtu9?%ynn<& zf=NV}qg)xy4ia8l*I@6_5eErfarGo3MjW~(EJsbkYjx>rZ~r*THQ`MBePTWm=J;su ziMvm?pH)r5Yjx?Wb^rAs!6YKgkzT>229I^-$4>gBElR?j@Y7m(((CkZB4>!b?L6B{@-&FOd`S@<&Ld< z93;F}m+s!`pO=te5)tOuWYV79o!-pnJ~aui)up@8`sY_9m_&p*zVwG_-RGXm=WsO% zuhpfy>H6o1B$z~mIntSBx15XCB)nFa?h5Rm?~-5=5#}g&H0IZXgxBiQ-HrY8b`ne? z!W`*Lzgs>V)FixCm+sQ+fBqoBBqGdF?l8^ALBeZw>2A~h=O_|PBElTyPTA}r;kCMS z*KGguAPFWBVUF|(vRgiL)+D@Em+t=Uf379LBqGf5=2>Z9@%H(%b4|i)b?GkV{^xTN zOd`S@#+B)&0TV$7cr#uhpfy%KP&Y5=kaaR+sMb@6WGDFo_6rly4|x2MMp$rEe=> z4rkwj$OMy!D2`{J>y}wxO~Pw+>01~5d19LNjSx&C!W?5Jzt-LE)jYGTNqDU;eS4%o z7bU?YBFvE{srs%=K5`OXt4rU4>CbmbFo_6rq=|61%#dpmUaL#r=IPI=Nic~BbEL_( zzPprDyKP>jos4M1(ogl?Hv6EFT96 zuhsR3O{e4f2MHz-VU8Jltk^g@LFB^_F@0J8VM#5VUF_6$?PEEwYu~z%Kr615=e9Db{TgelMS@90 znB%Cuw4@-!6YKgQNCH6Uk?&qt4rT9);{6C`z~@Om_&p* z(stmc>=Uj@c&#pdTUz^s%|U`mM3^IO2X4wf;hKck>e9EqwNKa_B$z~mIi7fC%jRp( zOw314!fSQu+w0mVYz`7kBElRm@3&2J?nM)_gM`=W(zo!nPuLtJm_&p*$`b^#gM`=W z($fUAPuLtJm_&p*RykvQbIn(`%MKD=t4mKs&^}>vkYExK=1ALto3c;1CgHWZ^mGU9 z6E+74CJ|wdv>mu9`-E!}UaLz_sn9-QbC6&X5#}gQ*vLOC5?-rIPutKwVRMjR5)tM| z+ku;nZJ%&W!fSQusUcUN?hX=6BElSLJ8)C>3D+dNR+pZBqJ6@493+@TggO3j((cVq zcN&*}RwTSum!9IHeZuA-!6YKgk+uUjWuI_O!fSQuX*Aj=Yz`7kBElShd}ea<+$YB7 z;~?R+y7W{X?GrW!2__L?j8T3vc7oAwEtg9MX^Fh_Z!Tvu~Z5?-rIPnXj^ zVRMjR5)tMoPsYm*5?-rIPs!6hVRMjR5)tMoPx#9Y5?-rIPy5q8VRMjR5)tMoPb$m~ z5?-rIPc76wVRMjR5)tMoPh89n5?-rIPhZqNVRMjR5)tMoPmatE5?-rIPm$CZyIg<{-f&BFvGt12<)# za81H%b?ND(+9zxd5=@LFAZ%BuDWn}Y28_#YuP1Wx? z39r?qr^RZYusKLDi3oF~?ZAzu!g7%CT3veTt@a6*(Y3+@LFAZ3bFPHn}Y83 zHNRQBecmTrlki$ym(Li;`-IIwf=NV}<3H`0e5rwwNKa_B$z~mId*+=VRP*^d7p4i!fSQu>F?SnYz`7kBElSLJ8)C>3D+dN zR+pY)uYJPiAi*Re%<+pSPHhf4VcAl>myqyUU3wb7_6eJV1e1s`N7*Eh9VEO~m$nLM zpRhSdFo_6r?3H#XoO;^4>>%N_y0jfZ`-IIwf=NV}qilA_4ia9gOIseaPuLtJm_&p* zu1h;DZhCFG_6gS{yjIto+AU{&Uc zt@rIaoM)3tyw-pBvHgehJnmI)0AbEN5DRpyB;5?-t8 z)+2}WJkcB^m_&p*(sZyY^TZYjuhq5QF~fPDXbuugBElSLI#{J={N~q#gxBg?Fk?8+ z6U{+_Nko_Qn3Hd18x%*XsK2$-{Y`XbuugBElTqJ@&2sw0w>H?}~)i>bhy(aGoccg9MX^ zFvr20A5i`N&Q-I6gxBi&$^79wPc#P!CJ|wdG##wUJh4T>YjxeYa5&Es%|U`mM3^H@ z2dng~`+OWEyjIsmi-z+&(Htb0M1(ok+j~k?{d9D8knmbvb59%2^F(uyU=k7LNYlY8 z?HpVc&)C#Eg8=9 zM01c}5)tMo8;G-mgxBg?eAaNDCz^uU#W~;XF??2MHz-VU9aCJHFcX z;k&YfgxBg?|GeQmPc#P!CJ|wdh09K;PCMq->>%N_x(+*kIL{N!L4rv{nB$qLv#PIu z|N878;kCNXyjbb=~sK;XF??2MHz-VU8(V&8{|n z;G%pSB)nGF{ofkS^F(uyU=k7L_|dT^RsV5)o+q|Qc&)C7zdfAiiRK`|BqGeQ*3voE zthtNxaggv@UES{t=Xs(zNHB>AbKLvr$<@Doz0M92UaRY>3y1SO(Htb0M1(ogbg(M( z#1;v!)iv*;;XF??2MHz-VU9E%tjavGMZ#-!RTmHEd7?Q;Fo_6rjCt{t>b@8D%s(p< zUaRY!0njni7gUdtLx6>&^*x`B$z~mIeL#Rtp4fFJWp(q@LFA; zxO6zr6U{+_Nko_pBH^{VZcYx(6U{+_Nko_AbEJL3RoN$8lknP)L;Hm7I7sNf>q$hIBW(w+%0A(mgxBiQPeJ>H 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%0A(mgxBiQQ;yC&(H$h1M1(nB-|wL2yy2Dd z&x(ZC>eADi&N{&zB$z~mIm(l!vV(-z>e5rE7SC`82__L?jvsz$O0)cuyid3$;kCN- z^sMf0%5?-rIPXQYoat8?}5n+zB9k?m`gliIBt4mKa+jGDjB$z~m zIns9ErtA~0NqDU;J(X?K!`(rGNko{VJW;Nz>joseR+pYG_m`>eAi*Re%u$|(YFgr+itu8&aaPk4}Ai*Re z%u$}Wm>nd%R+pZ>IC@`qkYExK<|t2&%nlM>t4mLjylpRckYExK=D2U&r{J4i5z2y>Jtd1eO*uhpfedVXkEcaUHb5#~tSft#{VxF+GXy7Y9?8+LIA z2__L?jQ}&nx^XaoP^iv($ivJ+Swf>m_&p* zwm<2lX3;TupKwjWYjx?Vx3hP02MHz-VUBfvJf}JMj=WE}CgHWZ^z`0QJGg@clZY_K zrVD-vF->*WumnPu81Z0EZqm_&p*(stmc>=Uj@ zc&#oyrTg5C+(Cj#M3|#IVLTrP39r?qr;V?;fjdYri3oF~?Z8dhCtQ>8T3vc-y7md% z&x!<-h%iUm4&0P|!Ziu6)upGuzcbbyB$z~mIns9ErtA~0NqDU;J;h%8gzY#;Fo_6r zr0u{>*(Y3+@LFAZ8o%}ln}Y*(Y3+ z@LF9*CWrP3+i{R!5)tMon;r6Tknmbv+VY@%!sZ~sBqGd_wgWe1pKwjWYju4rIkZpM i93+@TggMf7;HK;ou1R>Uu5rnseZuA-!6YKg@&5tO9E4f` literal 0 HcmV?d00001 From c22c05cafa8eee2591fed93fbfd90e2bfa7a1de4 Mon Sep 17 00:00:00 2001 From: rtyr <36745189+rtyr@users.noreply.github.com> Date: Tue, 13 Sep 2022 11:43:07 +0200 Subject: [PATCH 084/100] fixed description --- resources/profiles/Rigid3D.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/profiles/Rigid3D.ini b/resources/profiles/Rigid3D.ini index d6cbd372a5..0583862939 100644 --- a/resources/profiles/Rigid3D.ini +++ b/resources/profiles/Rigid3D.ini @@ -1,4 +1,4 @@ -# Print profiles for the Creality printers. +# Print profiles for the Rigid3D printers. [vendor] # Vendor name will be shown by the Config Wizard. From 9f59941498bcab52548e487c62ad1541df57b3bc Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Tue, 13 Sep 2022 13:34:28 +0200 Subject: [PATCH 085/100] #8844 and #8837 - Fixed crash in legend due to missing data for used filament --- src/slic3r/GUI/GCodeViewer.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 54b4eb1eca..d4cf8632e6 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -3860,14 +3860,16 @@ void GCodeViewer::render_legend(float& legend_height) if (m_view_type == EViewType::Tool) { // calculate used filaments data + used_filaments_m = std::vector(m_extruder_ids.size(), 0.0); + used_filaments_g = std::vector(m_extruder_ids.size(), 0.0); for (size_t extruder_id : m_extruder_ids) { if (m_print_statistics.volumes_per_extruder.find(extruder_id) == m_print_statistics.volumes_per_extruder.end()) continue; double volume = m_print_statistics.volumes_per_extruder.at(extruder_id); auto [used_filament_m, used_filament_g] = get_used_filament_from_volume(volume, extruder_id); - used_filaments_m.push_back(used_filament_m); - used_filaments_g.push_back(used_filament_g); + used_filaments_m[extruder_id] = used_filament_m; + used_filaments_g[extruder_id] = used_filament_g; } std::string longest_used_filament_string; @@ -4002,7 +4004,8 @@ void GCodeViewer::render_legend(float& legend_height) // shows only extruders actually used size_t i = 0; for (unsigned char extruder_id : m_extruder_ids) { - append_item(EItemType::Rect, m_tool_colors[extruder_id], _u8L("Extruder") + " " + std::to_string(extruder_id + 1), + if (used_filaments_m[i] > 0.0 && used_filaments_g[i] > 0.0) + append_item(EItemType::Rect, m_tool_colors[extruder_id], _u8L("Extruder") + " " + std::to_string(extruder_id + 1), true, "", 0.0f, 0.0f, offsets, used_filaments_m[i], used_filaments_g[i]); ++i; } From 0559332ca84f421a29f10f1c8bbe2a02bac39832 Mon Sep 17 00:00:00 2001 From: rtyr <36745189+rtyr@users.noreply.github.com> Date: Tue, 13 Sep 2022 13:35:55 +0200 Subject: [PATCH 086/100] Added thumbnail for Ender 5 Pro This printer model will be commented for now. https://github.com/prusa3d/PrusaSlicer/pull/8848 --- .../profiles/Creality/ENDER5PRO_thumbnail.png | Bin 0 -> 28977 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 resources/profiles/Creality/ENDER5PRO_thumbnail.png diff --git a/resources/profiles/Creality/ENDER5PRO_thumbnail.png b/resources/profiles/Creality/ENDER5PRO_thumbnail.png new file mode 100644 index 0000000000000000000000000000000000000000..ce4e5b044276404175404f51edd975b72923f4fc GIT binary patch literal 28977 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 literal 0 HcmV?d00001 From 0b8d7380ff4b360e3aefa8d1153cb1641668844d Mon Sep 17 00:00:00 2001 From: YuSanka Date: Thu, 8 Sep 2022 16:02:26 +0200 Subject: [PATCH 087/100] DiffDialog: Implemented a transfer of options from one preset to another Related to [Feature Request] #5384 - Copy values in Profile comparaison dialog --- src/slic3r/GUI/ConfigWizard.cpp | 4 +- src/slic3r/GUI/GUI_App.cpp | 4 +- src/slic3r/GUI/MainFrame.cpp | 47 +++- src/slic3r/GUI/MainFrame.hpp | 1 + src/slic3r/GUI/Plater.cpp | 5 +- src/slic3r/GUI/Tab.cpp | 50 ++++- src/slic3r/GUI/Tab.hpp | 6 +- src/slic3r/GUI/UnsavedChangesDialog.cpp | 286 +++++++++++++++++++----- src/slic3r/GUI/UnsavedChangesDialog.hpp | 81 ++++--- 9 files changed, 379 insertions(+), 105 deletions(-) diff --git a/src/slic3r/GUI/ConfigWizard.cpp b/src/slic3r/GUI/ConfigWizard.cpp index d95848300e..490e8e54ab 100644 --- a/src/slic3r/GUI/ConfigWizard.cpp +++ b/src/slic3r/GUI/ConfigWizard.cpp @@ -2564,9 +2564,9 @@ bool ConfigWizard::priv::apply_config(AppConfig *app_config, PresetBundle *prese bool check_unsaved_preset_changes = page_welcome->reset_user_profile(); if (check_unsaved_preset_changes) header = _L("All user presets will be deleted."); - int act_btns = UnsavedChangesDialog::ActionButtons::KEEP; + int act_btns = ActionButtons::KEEP; if (!check_unsaved_preset_changes) - act_btns |= UnsavedChangesDialog::ActionButtons::SAVE; + act_btns |= ActionButtons::SAVE; // Install bundles from resources if needed: std::vector install_bundles; diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 901a3c0ff1..c054a2e3bc 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -2521,9 +2521,9 @@ bool GUI_App::check_and_save_current_preset_changes(const wxString& caption, con { if (has_current_preset_changes()) { const std::string app_config_key = remember_choice ? "default_action_on_close_application" : ""; - int act_buttons = UnsavedChangesDialog::ActionButtons::SAVE; + int act_buttons = ActionButtons::SAVE; if (dont_save_insted_of_discard) - act_buttons |= UnsavedChangesDialog::ActionButtons::DONT_SAVE; + act_buttons |= ActionButtons::DONT_SAVE; UnsavedChangesDialog dlg(caption, header, app_config_key, act_buttons); std::string act = app_config_key.empty() ? "none" : wxGetApp().app_config->get(app_config_key); if (act == "none" && dlg.ShowModal() == wxID_CANCEL) diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index d4198b4c54..3875d8cdf7 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -277,8 +277,52 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, wxDEFAULT_FRAME_S preferences_dialog = new PreferencesDialog(this); } + + // bind events from DiffDlg + + bind_diff_dialog(); } +void MainFrame::bind_diff_dialog() +{ + auto get_tab = [](Preset::Type type) { + Tab* null_tab = nullptr; + for (Tab* tab : wxGetApp().tabs_list) + if (tab->type() == type) + return tab; + return null_tab; + }; + + auto transfer = [this, get_tab](Preset::Type type) { + get_tab(type)->transfer_options(diff_dialog.get_left_preset_name(type), + diff_dialog.get_right_preset_name(type), + std::move(diff_dialog.get_selected_options(type))); + }; + + auto save = [this, get_tab](Preset::Type type) { + get_tab(type)->save_options(diff_dialog.get_left_preset_name(type), + diff_dialog.get_right_preset_name(type), + std::move(diff_dialog.get_selected_options(type))); + }; + + auto process_options = [this](std::function process) { + const Preset::Type diff_dlg_type = diff_dialog.view_type(); + if (diff_dlg_type == Preset::TYPE_INVALID) { + for (const Preset::Type& type : diff_dialog.printer_technology() == ptFFF ? + std::initializer_list{Preset::TYPE_PRINTER, Preset::TYPE_PRINT, Preset::TYPE_FILAMENT} : + std::initializer_list{ Preset::TYPE_PRINTER, Preset::TYPE_SLA_PRINT, Preset::TYPE_SLA_MATERIAL } ) + process(type); + } + else + process(diff_dlg_type); + }; + + diff_dialog.Bind(EVT_DIFF_DIALOG_TRANSFER, [this, process_options, transfer](SimpleEvent&) { process_options(transfer); }); + + diff_dialog.Bind(EVT_DIFF_DIALOG_SAVE, [this, process_options, save](SimpleEvent&) { process_options(save); }); +} + + #ifdef _MSW_DARK_MODE static wxString pref() { return " [ "; } static wxString suff() { return " ] "; } @@ -2130,9 +2174,6 @@ void MainFrame::add_to_recent_projects(const wxString& filename) void MainFrame::technology_changed() { - // upadte DiffDlg - diff_dialog.update_presets(); - // update menu titles PrinterTechnology pt = plater()->printer_technology(); if (int id = m_menubar->FindMenu(pt == ptFFF ? _L("Material Settings") : _L("Filament Settings")); id != wxNOT_FOUND) diff --git a/src/slic3r/GUI/MainFrame.hpp b/src/slic3r/GUI/MainFrame.hpp index 893febf4bd..66adf806aa 100644 --- a/src/slic3r/GUI/MainFrame.hpp +++ b/src/slic3r/GUI/MainFrame.hpp @@ -107,6 +107,7 @@ class MainFrame : public DPIFrame bool can_delete() const; bool can_delete_all() const; bool can_reslice() const; + void bind_diff_dialog(); // MenuBar items changeable in respect to printer technology enum MenuItems diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 85978ed6b5..6837892151 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -5328,10 +5328,9 @@ void Plater::new_project() (saved_project == wxID_YES ? _L("You can keep presets modifications to the new project or discard them") : _L("You can keep presets modifications to the new project, discard them or save changes as new presets.\n" "Note, if changes will be saved then new project wouldn't keep them")); - using ab = UnsavedChangesDialog::ActionButtons; - int act_buttons = ab::KEEP; + int act_buttons = ActionButtons::KEEP; if (saved_project == wxID_NO) - act_buttons |= ab::SAVE; + act_buttons |= ActionButtons::SAVE; if (!wxGetApp().check_and_keep_current_preset_changes(_L("Creating a new project"), header, act_buttons)) return; } diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 5f92f98a37..f57a68cae1 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -1183,9 +1183,9 @@ void Tab::activate_option(const std::string& opt_key, const wxString& category) m_highlighter.init(get_custom_ctrl_with_blinking_ptr(opt_key)); } -void Tab::cache_config_diff(const std::vector& selected_options) +void Tab::cache_config_diff(const std::vector& selected_options, const DynamicPrintConfig* config/* = nullptr*/) { - m_cache_config.apply_only(m_presets->get_edited_preset().config, selected_options); + m_cache_config.apply_only(config ? *config : m_presets->get_edited_preset().config, selected_options); } void Tab::apply_config_from_cache() @@ -3583,6 +3583,42 @@ void Tab::compare_preset() wxGetApp().mainframe->diff_dialog.show(m_type); } +void Tab::transfer_options(const std::string &name_from, const std::string &name_to, std::vector options) +{ + if (options.empty()) + return; + + Preset* preset_from = m_presets->find_preset(name_from); + Preset* preset_to = m_presets->find_preset(name_to); + + if (m_type == Preset::TYPE_PRINTER) { + auto it = std::find(options.begin(), options.end(), "extruders_count"); + if (it != options.end()) { + // erase "extruders_count" option from the list + options.erase(it); + // cache the extruders count + static_cast(this)->cache_extruder_cnt(&preset_from->config); + } + } + cache_config_diff(options, &preset_from->config); + + if (name_to != m_presets->get_edited_preset().name ) + select_preset(preset_to->name); + + apply_config_from_cache(); + load_current_preset(); +} + +void Tab::save_options(const std::string &name_from, const std::string &name_to, std::vector options) +{ + if (options.empty()) + return; + + Preset* preset_from = m_presets->find_preset(name_from); + Preset* preset_to = m_presets->find_preset(name_to); + +} + // Save the current preset into file. // This removes the "dirty" flag of the preset, possibly creates a new preset under a new name, // and activates the new preset. @@ -4267,12 +4303,15 @@ wxSizer* TabPrinter::create_bed_shape_widget(wxWindow* parent) return sizer; } -void TabPrinter::cache_extruder_cnt() +void TabPrinter::cache_extruder_cnt(const DynamicPrintConfig* config/* = nullptr*/) { - if (m_presets->get_edited_preset().printer_technology() == ptSLA) + const DynamicPrintConfig& cached_config = config ? *config : m_presets->get_edited_preset().config; + if (Preset::printer_technology(cached_config) == ptSLA) return; - m_cache_extruder_count = m_extruders_count; + // get extruders count + auto* nozzle_diameter = dynamic_cast(cached_config.option("nozzle_diameter")); + m_cache_extruder_count = nozzle_diameter->values.size(); //m_extruders_count; } bool TabPrinter::apply_extruder_cnt_from_cache() @@ -4282,6 +4321,7 @@ bool TabPrinter::apply_extruder_cnt_from_cache() if (m_cache_extruder_count > 0) { m_presets->get_edited_preset().set_num_extruders(m_cache_extruder_count); +// extruders_count_changed(m_cache_extruder_count); m_cache_extruder_count = 0; return true; } diff --git a/src/slic3r/GUI/Tab.hpp b/src/slic3r/GUI/Tab.hpp index 8ed11bf9d0..9c4b101407 100644 --- a/src/slic3r/GUI/Tab.hpp +++ b/src/slic3r/GUI/Tab.hpp @@ -322,6 +322,8 @@ public: void OnKeyDown(wxKeyEvent& event); void compare_preset(); + void transfer_options(const std::string&name_from, const std::string&name_to, std::vector options); + void save_options(const std::string &name_from, const std::string &name_to, std::vector options); void save_preset(std::string name = std::string(), bool detach = false); void rename_preset(); void delete_preset(); @@ -374,7 +376,7 @@ public: void update_wiping_button_visibility(); void activate_option(const std::string& opt_key, const wxString& category); - void cache_config_diff(const std::vector& selected_options); + void cache_config_diff(const std::vector& selected_options, const DynamicPrintConfig* config = nullptr); void apply_config_from_cache(); const std::map& get_category_icon_map() { return m_category_icon; } @@ -503,7 +505,7 @@ public: bool supports_printer_technology(const PrinterTechnology /* tech */) const override { return true; } wxSizer* create_bed_shape_widget(wxWindow* parent); - void cache_extruder_cnt(); + void cache_extruder_cnt(const DynamicPrintConfig* config = nullptr); bool apply_extruder_cnt_from_cache(); }; diff --git a/src/slic3r/GUI/UnsavedChangesDialog.cpp b/src/slic3r/GUI/UnsavedChangesDialog.cpp index 225d92ec64..9bcb9def0b 100644 --- a/src/slic3r/GUI/UnsavedChangesDialog.cpp +++ b/src/slic3r/GUI/UnsavedChangesDialog.cpp @@ -5,7 +5,6 @@ #include #include #include -#include #include @@ -22,10 +21,6 @@ #include "MainFrame.hpp" #include "MsgDialog.hpp" -//#define FTS_FUZZY_MATCH_IMPLEMENTATION -//#include "fts_fuzzy_match.h" - -#include "BitmapCache.hpp" #include "PresetComboBoxes.hpp" using boost::optional; @@ -40,6 +35,10 @@ namespace Slic3r { namespace GUI { +wxDEFINE_EVENT(EVT_DIFF_DIALOG_TRANSFER, SimpleEvent); +wxDEFINE_EVENT(EVT_DIFF_DIALOG_SAVE, SimpleEvent); + + // ---------------------------------------------------------------------------- // ModelNode: a node inside DiffModel // ---------------------------------------------------------------------------- @@ -104,8 +103,8 @@ ModelNode::ModelNode(ModelNode* parent, const wxString& text, const std::string& ModelNode::ModelNode(ModelNode* parent, const wxString& text) : m_parent_win(parent->m_parent_win), m_parent(parent), - m_text(text), - m_icon_name("dot_small") + m_icon_name("dot_small"), + m_text(text) { UpdateIcons(); } @@ -135,12 +134,12 @@ ModelNode::ModelNode(ModelNode* parent, const wxString& text, const wxString& ol m_old_color(old_value.StartsWith("#") ? old_value : ""), m_mod_color(mod_value.StartsWith("#") ? mod_value : ""), m_new_color(new_value.StartsWith("#") ? new_value : ""), - m_container(false), - m_text(text), m_icon_name("empty"), + m_text(text), m_old_value(old_value), m_mod_value(mod_value), - m_new_value(new_value) + m_new_value(new_value), + m_container(false) { // check if old/new_value is color if (m_old_color.IsEmpty()) { @@ -505,7 +504,7 @@ unsigned int DiffModel::GetChildren(const wxDataViewItem& parent, wxDataViewItem for (const std::unique_ptr& child : children) array.Add(wxDataViewItem((void*)child.get())); - return array.size(); + return array.Count(); } @@ -591,7 +590,7 @@ void DiffModel::Clear() static std::string get_pure_opt_key(std::string opt_key) { - int pos = opt_key.find("#"); + const int pos = opt_key.find("#"); if (pos > 0) boost::erase_tail(opt_key, opt_key.size() - pos); return opt_key; @@ -1472,34 +1471,11 @@ static std::string get_selection(PresetComboBox* preset_combo) return into_u8(preset_combo->GetString(preset_combo->GetSelection())); } -DiffPresetDialog::DiffPresetDialog(MainFrame* mainframe) - : DPIDialog(mainframe, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER), - m_pr_technology(wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology()) -{ -#if defined(__WXMSW__) - // ys_FIXME! temporary workaround for correct font scaling - // Because of from wxWidgets 3.1.3 auto rescaling is implemented for the Fonts, - // From the very beginning set dialog font to the wxSYS_DEFAULT_GUI_FONT - this->SetFont(wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT)); -#endif // __WXMSW__ +void DiffPresetDialog::create_presets_sizer() +{ + m_presets_sizer = new wxBoxSizer(wxVERTICAL); - int border = 10; - int em = em_unit(); - - assert(wxGetApp().preset_bundle); - - m_preset_bundle_left = std::make_unique(*wxGetApp().preset_bundle); - m_preset_bundle_right = std::make_unique(*wxGetApp().preset_bundle); - - m_top_info_line = new wxStaticText(this, wxID_ANY, _L("Select presets to compare")); - m_top_info_line->SetFont(wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT).Bold()); - - m_bottom_info_line = new wxStaticText(this, wxID_ANY, ""); - m_bottom_info_line->SetFont(wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT).Bold()); - - wxBoxSizer* presets_sizer = new wxBoxSizer(wxVERTICAL); - - for (auto new_type : { Preset::TYPE_PRINT, Preset::TYPE_FILAMENT, Preset::TYPE_SLA_PRINT, Preset::TYPE_SLA_MATERIAL, Preset::TYPE_PRINTER }) + for (auto new_type : { Preset::TYPE_PRINT, Preset::TYPE_SLA_PRINT, Preset::TYPE_FILAMENT, Preset::TYPE_SLA_MATERIAL, Preset::TYPE_PRINTER }) { const PresetCollection* collection = get_preset_collection(new_type); wxBoxSizer* sizer = new wxBoxSizer(wxHORIZONTAL); @@ -1507,13 +1483,13 @@ DiffPresetDialog::DiffPresetDialog(MainFrame* mainframe) PresetComboBox* presets_right; ScalableButton* equal_bmp = new ScalableButton(this, wxID_ANY, "equal"); - auto add_preset_combobox = [collection, sizer, new_type, em, this](PresetComboBox** cb_, PresetBundle* preset_bundle) { - *cb_ = new PresetComboBox(this, new_type, wxSize(em * 35, -1), preset_bundle); - PresetComboBox* cb = (*cb_); + auto add_preset_combobox = [collection, sizer, new_type, this](PresetComboBox** cb_, PresetBundle* preset_bundle) { + *cb_ = new PresetComboBox(this, new_type, wxSize(em_unit() * 35, -1), preset_bundle); + PresetComboBox*cb = (*cb_); cb->show_modif_preset_separately(); cb->set_selection_changed_function([this, new_type, preset_bundle, cb](int selection) { if (m_view_type == Preset::TYPE_INVALID) { - std::string preset_name = cb->GetString(selection).ToUTF8().data(); + std::string preset_name = Preset::remove_suffix_modified(cb->GetString(selection).ToUTF8().data()); update_compatibility(preset_name, new_type, preset_bundle); } update_tree(); @@ -1527,7 +1503,7 @@ DiffPresetDialog::DiffPresetDialog(MainFrame* mainframe) add_preset_combobox(&presets_left, m_preset_bundle_left.get()); sizer->Add(equal_bmp, 0, wxRIGHT | wxLEFT | wxALIGN_CENTER_VERTICAL, 5); add_preset_combobox(&presets_right, m_preset_bundle_right.get()); - presets_sizer->Add(sizer, 1, wxTOP, 5); + m_presets_sizer->Add(sizer, 1, wxTOP, 5); equal_bmp->Show(new_type == Preset::TYPE_PRINTER); m_preset_combos.push_back({ presets_left, equal_bmp, presets_right }); @@ -1540,7 +1516,10 @@ DiffPresetDialog::DiffPresetDialog(MainFrame* mainframe) update_tree(); }); } +} +void DiffPresetDialog::create_show_all_presets_chb() +{ m_show_all_presets = new wxCheckBox(this, wxID_ANY, _L("Show all presets (including incompatible)")); m_show_all_presets->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent&) { bool show_all = m_show_all_presets->GetValue(); @@ -1553,26 +1532,171 @@ DiffPresetDialog::DiffPresetDialog(MainFrame* mainframe) if (m_view_type == Preset::TYPE_INVALID) update_tree(); }); +} - m_tree = new DiffViewCtrl(this, wxSize(em * 65, em * 40)); +void DiffPresetDialog::create_info_lines() +{ + const wxFont font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT).Bold(); + + m_top_info_line = new wxStaticText(this, wxID_ANY, _L("Select presets to compare")); + m_top_info_line->SetFont(font); + + m_bottom_info_line = new wxStaticText(this, wxID_ANY, ""); + m_bottom_info_line->SetFont(font); +} + +void DiffPresetDialog::create_tree() +{ + m_tree = new DiffViewCtrl(this, wxSize(em_unit() * 65, em_unit() * 40)); + m_tree->AppendToggleColumn_(L"\u2714", DiffModel::colToggle, wxLinux ? 9 : 6); m_tree->AppendBmpTextColumn("", DiffModel::colIconText, 35); m_tree->AppendBmpTextColumn(_L("Left Preset Value"), DiffModel::colOldValue, 15); m_tree->AppendBmpTextColumn(_L("Right Preset Value"),DiffModel::colModValue, 15); m_tree->Hide(); + m_tree->GetColumn(DiffModel::colToggle)->SetHidden(true); +} - wxBoxSizer* topSizer = new wxBoxSizer(wxVERTICAL); +void DiffPresetDialog::create_buttons() +{ + wxFont font = this->GetFont().Scaled(1.4f); + m_buttons = new wxBoxSizer(wxHORIZONTAL); - topSizer->Add(m_top_info_line, 0, wxEXPAND | wxLEFT | wxTOP | wxRIGHT, 2 * border); - topSizer->Add(presets_sizer, 0, wxEXPAND | wxLEFT | wxTOP | wxRIGHT, border); - topSizer->Add(m_show_all_presets, 0, wxEXPAND | wxALL, border); - topSizer->Add(m_tree, 1, wxEXPAND | wxALL, border); - topSizer->Add(m_bottom_info_line, 0, wxEXPAND | wxALL, 2 * border); + auto show_in_bottom_info = [this](const wxString& ext_line, wxMouseEvent& e) { + m_bottom_info_line->SetLabel(ext_line); + m_bottom_info_line->Show(true); + Layout(); + e.Skip(); + }; - this->SetMinSize(wxSize(80 * em, 30 * em)); + // Transfer + m_transfer_btn = new ScalableButton(this, wxID_ANY, "paste_menu", _L("Transfer"), wxDefaultSize, wxDefaultPosition, wxBORDER_DEFAULT, 24); + m_transfer_btn->Bind(wxEVT_BUTTON, [this](wxEvent&) { button_event(Action::Transfer);}); + + + auto enable_transfer = [this](const Preset::Type& type) { + const Preset& main_edited_preset = get_preset_collection(type, wxGetApp().preset_bundle)->get_edited_preset(); + if (main_edited_preset.is_dirty) + return main_edited_preset.name == get_right_preset_name(type); + return true; + }; + m_transfer_btn->Bind(wxEVT_UPDATE_UI, [this, enable_transfer](wxUpdateUIEvent& evt) { + bool enable = m_tree->has_selection(); + if (enable) { + if (m_view_type == Preset::TYPE_INVALID) { + for (const Preset::Type& type : (m_pr_technology == ptFFF ? std::initializer_list{Preset::TYPE_PRINTER, Preset::TYPE_PRINT, Preset::TYPE_FILAMENT} : + std::initializer_list{ Preset::TYPE_PRINTER, Preset::TYPE_SLA_PRINT, Preset::TYPE_SLA_MATERIAL })) + if (!enable_transfer(type)) { + enable = false; + break; + } + } + else + enable = enable_transfer(m_view_type); + } + evt.Enable(enable); + }); + m_transfer_btn->Bind(wxEVT_ENTER_WINDOW, [this, show_in_bottom_info](wxMouseEvent& e) { + show_in_bottom_info(_L("Transfer the selected options from left preset to the right.\n" + "Note: New modified presets will be selected in setting stabs after close this dialog."), e); }); + + // Save + m_save_btn = new ScalableButton(this, wxID_ANY, "save", _L("Save"), wxDefaultSize, wxDefaultPosition, wxBORDER_DEFAULT, 24); + m_save_btn->Bind(wxEVT_BUTTON, [this](wxEvent&) { button_event(Action::Save); }); + m_save_btn->Bind(wxEVT_UPDATE_UI, [this](wxUpdateUIEvent& evt) { evt.Enable(m_tree->has_selection()); }); + m_save_btn->Bind(wxEVT_ENTER_WINDOW, [this, show_in_bottom_info](wxMouseEvent& e) { + show_in_bottom_info(_L("Save the selected options from left preset to the right."), e); }); + + // Cancel + m_cancel_btn = new ScalableButton(this, wxID_CANCEL, "cross", _L("Cancel"), wxDefaultSize, wxDefaultPosition, wxBORDER_DEFAULT, 24); + m_cancel_btn->Bind(wxEVT_BUTTON, [this](wxEvent&) { button_event(Action::Discard);}); + + for (ScalableButton* btn : { m_transfer_btn, m_save_btn, m_cancel_btn }) { + btn->Bind(wxEVT_LEAVE_WINDOW, [this](wxMouseEvent& e) { update_bottom_info(); Layout(); e.Skip(); }); + m_buttons->Add(btn, 1, wxLEFT, 5); + btn->SetFont(font); + } + + m_buttons->Show(false); +} + +void DiffPresetDialog::create_edit_sizer() +{ + // Add check box for the edit mode + m_use_for_transfer = new wxCheckBox(this, wxID_ANY, _L("Transfer values from left to right")); + m_use_for_transfer->SetToolTip(_L("If enabled, this dialog can be used for transver selected values from left to right preset.")); + m_use_for_transfer->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent&) { + bool use = m_use_for_transfer->GetValue(); + m_tree->GetColumn(DiffModel::colToggle)->SetHidden(!use); + if (m_tree->IsShown()) { + m_buttons->Show(use); + Fit(); + Refresh(); + } + else + this->Layout(); + }); + + // Add Buttons + create_buttons(); + + // Create and fill edit sizer + m_edit_sizer = new wxBoxSizer(wxHORIZONTAL); + m_edit_sizer->Add(m_use_for_transfer, 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxRIGHT, 5); + m_edit_sizer->AddSpacer(em_unit() * 10); + m_edit_sizer->Add(m_buttons, 1, wxLEFT, 5); + m_edit_sizer->Show(false); +} + +void DiffPresetDialog::complete_dialog_creation() +{ + wxBoxSizer*topSizer = new wxBoxSizer(wxVERTICAL); + + int border = 10; + topSizer->Add(m_top_info_line, 0, wxEXPAND | wxLEFT | wxTOP | wxRIGHT, 2 * border); + topSizer->Add(m_presets_sizer, 0, wxEXPAND | wxLEFT | wxTOP | wxRIGHT, border); + topSizer->Add(m_show_all_presets, 0, wxEXPAND | wxALL, border); + topSizer->Add(m_tree, 1, wxEXPAND | wxALL, border); + topSizer->Add(m_bottom_info_line, 0, wxEXPAND | wxALL, 2 * border); + topSizer->Add(m_edit_sizer, 0, wxEXPAND | wxLEFT | wxBOTTOM | wxRIGHT, 2 * border); + + this->SetMinSize(wxSize(80 * em_unit(), 30 * em_unit())); this->SetSizer(topSizer); topSizer->SetSizeHints(this); } +DiffPresetDialog::DiffPresetDialog(MainFrame* mainframe) + : DPIDialog(mainframe, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER), + m_pr_technology(wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology()) +{ +#if defined(__WXMSW__) + // ys_FIXME! temporary workaround for correct font scaling + // Because of from wxWidgets 3.1.3 auto rescaling is implemented for the Fonts, + // From the very beginning set dialog font to the wxSYS_DEFAULT_GUI_FONT + this->SetFont(wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT)); +#endif // __WXMSW__ + + // Init bundles + + assert(wxGetApp().preset_bundle); + + m_preset_bundle_left = std::make_unique(*wxGetApp().preset_bundle); + m_preset_bundle_right = std::make_unique(*wxGetApp().preset_bundle); + + // Create UI items + + create_info_lines(); + + create_presets_sizer(); + + create_show_all_presets_chb(); + + create_tree(); + + create_edit_sizer(); + + complete_dialog_creation(); +} + void DiffPresetDialog::update_controls_visibility(Preset::Type type /* = Preset::TYPE_INVALID*/) { for (auto preset_combos : m_preset_combos) { @@ -1598,6 +1722,8 @@ void DiffPresetDialog::update_bundles_from_app() { *m_preset_bundle_left = *wxGetApp().preset_bundle; *m_preset_bundle_right = *wxGetApp().preset_bundle; + + m_pr_technology = m_preset_bundle_left.get()->printers.get_edited_preset().printer_technology(); } void DiffPresetDialog::show(Preset::Type type /* = Preset::TYPE_INVALID*/) @@ -1620,8 +1746,6 @@ void DiffPresetDialog::show(Preset::Type type /* = Preset::TYPE_INVALID*/) void DiffPresetDialog::update_presets(Preset::Type type) { - m_pr_technology = m_preset_bundle_left.get()->printers.get_edited_preset().printer_technology(); - update_bundles_from_app(); update_controls_visibility(type); @@ -1645,9 +1769,20 @@ void DiffPresetDialog::update_presets(Preset::Type type) update_tree(); } +void DiffPresetDialog::update_bottom_info(wxString bottom_info) +{ + if (m_tree->has_long_strings()) + bottom_info = _L("Some fields are too long to fit. Right mouse click reveals the full text."); + + const bool show_bottom_info = !m_tree->IsShown() || m_tree->has_long_strings(); + if (show_bottom_info) + m_bottom_info_line->SetLabel(bottom_info); + m_bottom_info_line->Show(show_bottom_info); +} + void DiffPresetDialog::update_tree() { - // update searcher befofre update of tree + // update searcher before update of tree wxGetApp().sidebar().check_and_update_searcher(); Search::OptionsSearcher& searcher = wxGetApp().sidebar().get_searcher(); searcher.sort_options_by_key(); @@ -1739,17 +1874,15 @@ void DiffPresetDialog::update_tree() left_val, right_val, "", category_icon_map.at(option.category)); } } - - if (m_tree->has_long_strings()) - bottom_info = _L("Some fields are too long to fit. Right mouse click reveals the full text."); bool tree_was_shown = m_tree->IsShown(); m_tree->Show(show_tree); - bool show_bottom_info = !show_tree || m_tree->has_long_strings(); - if (show_bottom_info) - m_bottom_info_line->SetLabel(bottom_info); - m_bottom_info_line->Show(show_bottom_info); + bool can_transfer_options = m_view_type == Preset::TYPE_INVALID || get_left_preset_name(m_view_type) != get_right_preset_name(m_view_type); + m_edit_sizer->Show(show_tree && can_transfer_options); + m_buttons->Show(m_edit_sizer->IsShown(size_t(0)) && m_use_for_transfer->GetValue()); + + update_bottom_info(bottom_info); if (tree_was_shown == m_tree->IsShown()) Layout(); @@ -1802,6 +1935,10 @@ void DiffPresetDialog::on_sys_color_changed() preset_combos.equal_bmp->sys_color_changed(); preset_combos.presets_right->sys_color_changed(); } + + for (ScalableButton* btn : { m_transfer_btn, m_save_btn, m_cancel_btn }) + btn->sys_color_changed(); + // msw_rescale updates just icons, so use it m_tree->Rescale(); Refresh(); @@ -1865,6 +2002,31 @@ void DiffPresetDialog::update_compatibility(const std::string& preset_name, Pres } } +void DiffPresetDialog::button_event(Action act) +{ + if (act == Action::Save) { + wxPostEvent(this, SimpleEvent(EVT_DIFF_DIALOG_SAVE)); + } + else { + Hide(); + if (act == Action::Transfer) + wxPostEvent(this, SimpleEvent(EVT_DIFF_DIALOG_TRANSFER)); + } +} + +std::string DiffPresetDialog::get_left_preset_name(Preset::Type type) +{ + PresetComboBox* cb = m_preset_combos[int(type - Preset::TYPE_PRINT)].presets_left; + return Preset::remove_suffix_modified(get_selection(cb)); +} + +std::string DiffPresetDialog::get_right_preset_name(Preset::Type type) +{ + + PresetComboBox* cb = m_preset_combos[int(type - Preset::TYPE_PRINT)].presets_right; + return Preset::remove_suffix_modified(get_selection(cb)); +} + } } // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/UnsavedChangesDialog.hpp b/src/slic3r/GUI/UnsavedChangesDialog.hpp index 0d98129326..2397c419e2 100644 --- a/src/slic3r/GUI/UnsavedChangesDialog.hpp +++ b/src/slic3r/GUI/UnsavedChangesDialog.hpp @@ -15,6 +15,9 @@ class wxStaticText; namespace Slic3r { namespace GUI{ +wxDECLARE_EVENT(EVT_DIFF_DIALOG_TRANSFER, SimpleEvent); +wxDECLARE_EVENT(EVT_DIFF_DIALOG_SAVE, SimpleEvent); + // ---------------------------------------------------------------------------- // ModelNode: a node inside DiffModel // ---------------------------------------------------------------------------- @@ -103,7 +106,7 @@ public: ModelNode* GetParent() { return m_parent; } ModelNodePtrArray& GetChildren() { return m_children; } ModelNode* GetNthChild(unsigned int n) { return m_children[n].get(); } - unsigned int GetChildCount() const { return m_children.size(); } + unsigned int GetChildCount() const { return (unsigned int)(m_children.size()); } void Append(std::unique_ptr child) { m_children.emplace_back(std::move(child)); } @@ -154,7 +157,7 @@ public: }; DiffModel(wxWindow* parent); - ~DiffModel() {} + ~DiffModel() override = default; void SetAssociatedControl(wxDataViewCtrl* ctrl) { m_ctrl = ctrl; } @@ -242,6 +245,20 @@ public: std::vector selected_options(); }; +// Discard and Cancel buttons are always but next buttons are optional +enum ActionButtons { + TRANSFER = 1, + KEEP = 2, + SAVE = 4, + DONT_SAVE = 8, +}; + +enum class Action { + Undef, + Transfer, + Discard, + Save +}; //------------------------------------------ // UnsavedChangesDialog @@ -262,13 +279,6 @@ class UnsavedChangesDialog : public DPIDialog std::string m_app_config_key; - enum class Action { - Undef, - Transfer, - Discard, - Save - }; - static constexpr char ActTransfer[] = "transfer"; static constexpr char ActDiscard[] = "discard"; static constexpr char ActSave[] = "save"; @@ -281,19 +291,12 @@ class UnsavedChangesDialog : public DPIDialog int m_buttons { ActionButtons::TRANSFER | ActionButtons::SAVE }; public: - // Discard and Cancel buttons are always but next buttons are optional - enum ActionButtons { - TRANSFER = 1, - KEEP = 2, - SAVE = 4, - DONT_SAVE = 8, - }; // show unsaved changes when preset is switching UnsavedChangesDialog(Preset::Type type, PresetCollection* dependent_presets, const std::string& new_selected_preset = std::string()); // show unsaved changes for all another cases UnsavedChangesDialog(const wxString& caption, const wxString& header, const std::string& app_config_key, int act_buttons); - ~UnsavedChangesDialog() {} + ~UnsavedChangesDialog() override = default; void build(Preset::Type type, PresetCollection* dependent_presets, const std::string& new_selected_preset, const wxString& header = ""); void update(Preset::Type type, PresetCollection* dependent_presets, const std::string& new_selected_preset, const wxString& header); @@ -332,7 +335,7 @@ class FullCompareDialog : public wxDialog public: FullCompareDialog(const wxString& option_name, const wxString& old_value, const wxString& mod_value, const wxString& new_value, const wxString& old_value_header, const wxString& mod_value_header, const wxString& new_value_header); - ~FullCompareDialog() {} + ~FullCompareDialog() override = default; }; @@ -342,19 +345,37 @@ public: class DiffPresetDialog : public DPIDialog { DiffViewCtrl* m_tree { nullptr }; + wxBoxSizer* m_presets_sizer { nullptr }; wxStaticText* m_top_info_line { nullptr }; wxStaticText* m_bottom_info_line { nullptr }; wxCheckBox* m_show_all_presets { nullptr }; + wxCheckBox* m_use_for_transfer { nullptr }; + ScalableButton* m_transfer_btn { nullptr }; + ScalableButton* m_save_btn { nullptr }; + ScalableButton* m_cancel_btn { nullptr }; + wxBoxSizer* m_buttons { nullptr }; + wxBoxSizer* m_edit_sizer { nullptr }; Preset::Type m_view_type { Preset::TYPE_INVALID }; PrinterTechnology m_pr_technology; std::unique_ptr m_preset_bundle_left; std::unique_ptr m_preset_bundle_right; - void update_tree(); - void update_bundles_from_app(); - void update_controls_visibility(Preset::Type type = Preset::TYPE_INVALID); - void update_compatibility(const std::string& preset_name, Preset::Type type, PresetBundle* preset_bundle); + void create_buttons(); + void create_edit_sizer(); + void complete_dialog_creation(); + void create_presets_sizer(); + void create_info_lines(); + void create_tree(); + void create_show_all_presets_chb(); + + void update_bottom_info(wxString bottom_info = ""); + void update_tree(); + void update_bundles_from_app(); + void update_controls_visibility(Preset::Type type = Preset::TYPE_INVALID); + void update_compatibility(const std::string& preset_name, Preset::Type type, PresetBundle* preset_bundle); + + void button_event(Action act); struct DiffPresets { @@ -366,11 +387,19 @@ class DiffPresetDialog : public DPIDialog std::vector m_preset_combos; public: - DiffPresetDialog(MainFrame* mainframe); - ~DiffPresetDialog() {} + DiffPresetDialog(MainFrame*mainframe); + ~DiffPresetDialog() override = default; - void show(Preset::Type type = Preset::TYPE_INVALID); - void update_presets(Preset::Type type = Preset::TYPE_INVALID); + void show(Preset::Type type = Preset::TYPE_INVALID); + void update_presets(Preset::Type type = Preset::TYPE_INVALID); + + Preset::Type view_type() const { return m_view_type; } + PrinterTechnology printer_technology() const { return m_pr_technology; } + + std::string get_left_preset_name(Preset::Type type); + std::string get_right_preset_name(Preset::Type type); + + std::vector get_selected_options(Preset::Type type) const { return std::move(m_tree->options(type, true)); } protected: void on_dpi_changed(const wxRect& suggested_rect) override; From 10cc836e3fafcca29d002f0feed8a7af5f64d198 Mon Sep 17 00:00:00 2001 From: YuSanka Date: Tue, 13 Sep 2022 17:30:03 +0200 Subject: [PATCH 088/100] Fix for #8850 - Incorrect display STL file name if not western coding page characters used --- src/slic3r/GUI/GLCanvas3D.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 66759f4c74..152402e68d 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -834,14 +834,14 @@ void GLCanvas3D::Labels::render(const std::vector& sorted_ imgui.begin(owner.title, ImGuiWindowFlags_NoMouseInputs | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove); ImGui::BringWindowToDisplayFront(ImGui::GetCurrentWindow()); float win_w = ImGui::GetWindowWidth(); - float label_len = imgui.calc_text_size(owner.label).x; + float label_len = ImGui::CalcTextSize(owner.label.c_str()).x; ImGui::SetCursorPosX(0.5f * (win_w - label_len)); ImGui::AlignTextToFramePadding(); imgui.text(owner.label); if (!owner.print_order.empty()) { ImGui::Separator(); - float po_len = imgui.calc_text_size(owner.print_order).x; + float po_len = ImGui::CalcTextSize(owner.print_order.c_str()).x; ImGui::SetCursorPosX(0.5f * (win_w - po_len)); ImGui::AlignTextToFramePadding(); imgui.text(owner.print_order); From 60cad081e6fb476fab3a25680281aad799933d0d Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Wed, 14 Sep 2022 09:13:22 +0200 Subject: [PATCH 089/100] Rework of 850b590c31641055de3bf2e5a97a7c4260161a0a - The previous fix resulted in wrong colors for toolpaths in Tool view mode --- src/libslic3r/GCode/GCodeProcessor.cpp | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index ec57313f41..c4a5e2fa21 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -778,8 +778,7 @@ void GCodeProcessorResult::reset() { max_print_height = 0.0f; settings_ids.reset(); extruders_count = 0; - extruder_colors = DEFAULT_EXTRUDER_COLORS; - assert(extruder_colors.size() == MIN_EXTRUDERS_COUNT); + extruder_colors = std::vector(); filament_diameters = std::vector(MIN_EXTRUDERS_COUNT, DEFAULT_FILAMENT_DIAMETER); filament_densities = std::vector(MIN_EXTRUDERS_COUNT, DEFAULT_FILAMENT_DENSITY); #if ENABLE_USED_FILAMENT_POST_PROCESS @@ -871,6 +870,7 @@ void GCodeProcessor::apply_config(const PrintConfig& config) { m_parser.apply_config(config); + m_producer = EProducer::PrusaSlicer; m_flavor = config.gcode_flavor; size_t extruders_count = config.nozzle_diameter.values.size(); @@ -1345,12 +1345,15 @@ void GCodeProcessor::process_file(const std::string& filename, std::function(eid); if (m_extruder_id != id) { - if (id >= m_result.extruder_colors.size()) + if (((m_producer == EProducer::PrusaSlicer || m_producer == EProducer::Slic3rPE || m_producer == EProducer::Slic3r) && id >= m_result.extruders_count) || + ((m_producer != EProducer::PrusaSlicer && m_producer != EProducer::Slic3rPE && m_producer != EProducer::Slic3r) && id >= m_result.extruder_colors.size())) BOOST_LOG_TRIVIAL(error) << "GCodeProcessor encountered an invalid toolchange, maybe from a custom gcode."; else { unsigned char old_extruder_id = m_extruder_id; From 430408f535eb014f8f65b5edc4433c9ff409db4a Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Wed, 14 Sep 2022 09:16:21 +0200 Subject: [PATCH 090/100] Fixed typo --- src/libslic3r/GCode/GCodeProcessor.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index c4a5e2fa21..96d2af236a 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -1354,6 +1354,7 @@ void GCodeProcessor::process_file(const std::string& filename, std::function Date: Wed, 14 Sep 2022 09:21:29 +0200 Subject: [PATCH 091/100] Follow-up of 9f59941498bcab52548e487c62ad1541df57b3bc - More robust fix --- src/slic3r/GUI/GCodeViewer.cpp | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index d4cf8632e6..b2b4b25eae 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -3860,8 +3860,8 @@ void GCodeViewer::render_legend(float& legend_height) if (m_view_type == EViewType::Tool) { // calculate used filaments data - used_filaments_m = std::vector(m_extruder_ids.size(), 0.0); - used_filaments_g = std::vector(m_extruder_ids.size(), 0.0); + used_filaments_m = std::vector(m_extruders_count, 0.0); + used_filaments_g = std::vector(m_extruders_count, 0.0); for (size_t extruder_id : m_extruder_ids) { if (m_print_statistics.volumes_per_extruder.find(extruder_id) == m_print_statistics.volumes_per_extruder.end()) continue; @@ -4002,12 +4002,10 @@ void GCodeViewer::render_legend(float& legend_height) #endif // ENABLE_PREVIEW_LAYER_TIME case EViewType::Tool: { // shows only extruders actually used - size_t i = 0; for (unsigned char extruder_id : m_extruder_ids) { - if (used_filaments_m[i] > 0.0 && used_filaments_g[i] > 0.0) + if (used_filaments_m[extruder_id] > 0.0 && used_filaments_g[extruder_id] > 0.0) append_item(EItemType::Rect, m_tool_colors[extruder_id], _u8L("Extruder") + " " + std::to_string(extruder_id + 1), - true, "", 0.0f, 0.0f, offsets, used_filaments_m[i], used_filaments_g[i]); - ++i; + true, "", 0.0f, 0.0f, offsets, used_filaments_m[extruder_id], used_filaments_g[extruder_id]); } break; } From ec2e783615be2afc10fd66d45a9b32f2030d9292 Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Wed, 14 Sep 2022 10:02:39 +0200 Subject: [PATCH 092/100] Fixed GCodeViewer to take care of parking volume when calculating used filament (similar as in PrusaSlicer) --- src/libslic3r/GCode/GCodeProcessor.cpp | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index 96d2af236a..3c773c89a3 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -1094,6 +1094,20 @@ void GCodeProcessor::apply_config(const DynamicPrintConfig& config) } } + // With MM setups like Prusa MMU2, the filaments may be expected to be parked at the beginning. + // Remember the parking position so the initial load is not included in filament estimate. + const ConfigOptionBool* single_extruder_multi_material = config.option("single_extruder_multi_material"); + const ConfigOptionBool* wipe_tower = config.option("wipe_tower"); + const ConfigOptionFloat* parking_pos_retraction = config.option("parking_pos_retraction"); + const ConfigOptionFloat* extra_loading_move = config.option("extra_loading_move"); + + if (single_extruder_multi_material != nullptr && wipe_tower != nullptr && parking_pos_retraction != nullptr && extra_loading_move != nullptr) { + if (single_extruder_multi_material->value && m_result.extruders_count > 1 && wipe_tower->value) { + m_parking_position = float(parking_pos_retraction->value); + m_extra_loading_move = float(extra_loading_move->value); + } + } + bool use_machine_limits = false; const ConfigOptionEnum* machine_limits_usage = config.option>("machine_limits_usage"); if (machine_limits_usage != nullptr) From 369e08aed19cf746590d3e2c7c9983ba8d25e838 Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Wed, 14 Sep 2022 10:46:05 +0200 Subject: [PATCH 093/100] Small optimization in rendering of selection rectangle --- src/slic3r/GUI/GLSelectionRectangle.cpp | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/slic3r/GUI/GLSelectionRectangle.cpp b/src/slic3r/GUI/GLSelectionRectangle.cpp index 32dc9ad18f..5959c19510 100644 --- a/src/slic3r/GUI/GLSelectionRectangle.cpp +++ b/src/slic3r/GUI/GLSelectionRectangle.cpp @@ -171,7 +171,7 @@ namespace GUI { GLModel::Geometry init_data; #if ENABLE_GL_CORE_PROFILE || ENABLE_OPENGL_ES init_data.format = { GLModel::Geometry::EPrimitiveType::Lines, GLModel::Geometry::EVertexLayout::P4 }; - init_data.reserve_vertices(8); + init_data.reserve_vertices(5); init_data.reserve_indices(8); #else init_data.format = { GLModel::Geometry::EPrimitiveType::LineLoop, GLModel::Geometry::EVertexLayout::P2 }; @@ -187,25 +187,19 @@ namespace GUI { init_data.add_vertex(Vec4f(left, bottom, 0.0f, perimeter)); perimeter += width; - init_data.add_vertex(Vec4f(right, bottom, 0.0f, perimeter)); - init_data.add_vertex(Vec4f(right, bottom, 0.0f, perimeter)); perimeter += height; - init_data.add_vertex(Vec4f(right, top, 0.0f, perimeter)); - init_data.add_vertex(Vec4f(right, top, 0.0f, perimeter)); perimeter += width; - init_data.add_vertex(Vec4f(left, top, 0.0f, perimeter)); - init_data.add_vertex(Vec4f(left, top, 0.0f, perimeter)); perimeter += height; init_data.add_vertex(Vec4f(left, bottom, 0.0f, perimeter)); // indices init_data.add_line(0, 1); + init_data.add_line(1, 2); init_data.add_line(2, 3); - init_data.add_line(4, 5); - init_data.add_line(6, 7); + init_data.add_line(3, 4); #else init_data.add_vertex(Vec2f(left, bottom)); init_data.add_vertex(Vec2f(right, bottom)); From 82716cd78c1af35069518147dfb62a35aeaab2af Mon Sep 17 00:00:00 2001 From: YuSanka Date: Wed, 14 Sep 2022 15:09:33 +0200 Subject: [PATCH 094/100] Follow-up https://github.com/Prusa-Development/PrusaSlicerPrivate/commit/3b1f1d944410d3f9d4fd30ef7fe88909aa73de01 fixes: Tab: Fixed rename_preset(). * Presets weren't sorted after preset renaming. * New selected preset wasn't updated on the Plater. Preset: Fixed delete_preset(). * Selected preset wasn't updated after preset deletion. --- src/libslic3r/Preset.cpp | 9 +++++++ src/slic3r/GUI/Tab.cpp | 53 ++++++++++++++++++++++++++++------------ 2 files changed, 46 insertions(+), 16 deletions(-) diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 2741e9146c..3803c3c7a3 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -947,6 +947,11 @@ bool PresetCollection::delete_current_preset() bool PresetCollection::delete_preset(const std::string& name) { + if (name == this->get_selected_preset().name) + return delete_current_preset(); + + const std::string selected_preset_name = this->get_selected_preset_name(); + auto it = this->find_preset_internal(name); const Preset& preset = *it; @@ -957,6 +962,10 @@ bool PresetCollection::delete_preset(const std::string& name) boost::nowide::remove(preset.file.c_str()); } m_presets.erase(it); + + // update selected preset + this->select_preset_by_name(selected_preset_name, true); + return true; } diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index f57a68cae1..917c9edaa9 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -30,6 +30,8 @@ #include #include #include +#include + #include "wxExtensions.hpp" #include "PresetComboBoxes.hpp" #include @@ -3710,12 +3712,11 @@ void Tab::rename_preset() if (m_presets_choice->is_selected_physical_printer()) return; - Preset& selected_preset = m_presets->get_selected_preset(); wxString msg; if (m_type == Preset::TYPE_PRINTER && !m_preset_bundle->physical_printers.empty()) { // Check preset for rename in physical printers - std::vector ph_printers = m_preset_bundle->physical_printers.get_printers_with_preset(selected_preset.name); + std::vector ph_printers = m_preset_bundle->physical_printers.get_printers_with_preset(m_presets->get_selected_preset().name); if (!ph_printers.empty()) { msg += _L_PLURAL("The physical printer below is based on the preset, you are going to rename.", "The physical printers below are based on the preset, you are going to rename.", ph_printers.size()); @@ -3732,31 +3733,51 @@ void Tab::rename_preset() SavePresetDialog dlg(m_parent, m_type, true, msg); if (dlg.ShowModal() != wxID_OK) return; - std::string new_name = into_u8(dlg.get_name()); + const std::string new_name = into_u8(dlg.get_name()); if (new_name.empty() || new_name == m_presets->get_selected_preset().name) return; - // rename selected and edited presets + // Note: selected preset can be changed, if in SavePresetDialog was selected name of existing preset + Preset& selected_preset = m_presets->get_selected_preset(); + Preset& edited_preset = m_presets->get_edited_preset(); - std::string old_name = selected_preset.name; - std::string old_file_name = selected_preset.file; + const std::string old_name = selected_preset.name; + const std::string old_file_name = selected_preset.file; - selected_preset.name = new_name; - boost::replace_last(selected_preset.file, old_name, new_name); + assert(old_name == edited_preset.name); - Preset& edited_preset = m_presets->get_edited_preset(); - edited_preset.name = new_name; - boost::replace_last(edited_preset.file, old_name, new_name); + using namespace boost; + try { + // rename selected and edited presets - // rename file with renamed preset configuration - boost::filesystem::rename(old_file_name, selected_preset.file); + selected_preset.name = new_name; + replace_last(selected_preset.file, old_name, new_name); - // rename selected preset in printers, if it's needed - if (!msg.IsEmpty()) - m_preset_bundle->physical_printers.rename_preset_in_printers(old_name, new_name); + edited_preset.name = new_name; + replace_last(edited_preset.file, old_name, new_name); + + // rename file with renamed preset configuration + + filesystem::rename(old_file_name, selected_preset.file); + + // rename selected preset in printers, if it's needed + + if (!msg.IsEmpty()) + m_preset_bundle->physical_printers.rename_preset_in_printers(old_name, new_name); + } + catch (const exception& ex) { + const std::string exception = diagnostic_information(ex); + printf("Can't rename a preset : %s", exception.c_str()); + } + + // sort presets after renaming + std::sort(m_presets->begin(), m_presets->end()); + // update selection + m_presets->select_preset_by_name(new_name, true); m_presets_choice->update(); + on_presets_changed(); } // Called for a currently selected preset. From 2781f716f48a87e7027a0b77ec8dd1042560cbb0 Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Fri, 9 Sep 2022 12:50:10 +0200 Subject: [PATCH 095/100] Fixed short edge collapse algortihm, so that it does not decimate all triangles on very high detailed models Relevant issue 8834 Access Error when slicing --- src/libslic3r/GCode/SeamPlacer.cpp | 4 ++-- src/libslic3r/GCode/SeamPlacer.hpp | 1 + src/libslic3r/ShortEdgeCollapse.cpp | 6 +++++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/libslic3r/GCode/SeamPlacer.cpp b/src/libslic3r/GCode/SeamPlacer.cpp index 4df95d7552..a54e9f819e 100644 --- a/src/libslic3r/GCode/SeamPlacer.cpp +++ b/src/libslic3r/GCode/SeamPlacer.cpp @@ -642,8 +642,8 @@ void compute_global_occlusion(GlobalModelInfo &result, const PrintObject *po, BOOST_LOG_TRIVIAL(debug) << "SeamPlacer: decimate: start"; - its_short_edge_collpase(triangle_set, 25000); - its_short_edge_collpase(negative_volumes_set, 25000); + its_short_edge_collpase(triangle_set, SeamPlacer::fast_decimation_triangle_count_target); + its_short_edge_collpase(negative_volumes_set, SeamPlacer::fast_decimation_triangle_count_target); size_t negative_volumes_start_index = triangle_set.indices.size(); its_merge(triangle_set, negative_volumes_set); diff --git a/src/libslic3r/GCode/SeamPlacer.hpp b/src/libslic3r/GCode/SeamPlacer.hpp index 12db7c72bc..671f6bcce8 100644 --- a/src/libslic3r/GCode/SeamPlacer.hpp +++ b/src/libslic3r/GCode/SeamPlacer.hpp @@ -110,6 +110,7 @@ class SeamPlacer { public: // Number of samples generated on the mesh. There are sqr_rays_per_sample_point*sqr_rays_per_sample_point rays casted from each samples static constexpr size_t raycasting_visibility_samples_count = 30000; + static constexpr size_t fast_decimation_triangle_count_target = 16000; //square of number of rays per sample point static constexpr size_t sqr_rays_per_sample_point = 5; diff --git a/src/libslic3r/ShortEdgeCollapse.cpp b/src/libslic3r/ShortEdgeCollapse.cpp index b36278c37f..0c940cb475 100644 --- a/src/libslic3r/ShortEdgeCollapse.cpp +++ b/src/libslic3r/ShortEdgeCollapse.cpp @@ -97,7 +97,8 @@ void its_short_edge_collpase(indexed_triangle_set &mesh, size_t target_triangle_ //shuffle the faces and traverse in random order, this MASSIVELY improves the quality of the result std::shuffle(face_indices.begin(), face_indices.end(), generator); - + + int allowed_face_removals = int(face_indices.size()) - int(target_triangle_count); for (const size_t &face_idx : face_indices) { if (face_removal_flags[face_idx]) { // if face already removed from previous collapses, skip (each collapse removes two triangles [at least] ) @@ -130,10 +131,13 @@ void its_short_edge_collpase(indexed_triangle_set &mesh, size_t target_triangle_ // remove faces remove_face(face_idx, neighbor_to_remove_face_idx); remove_face(neighbor_to_remove_face_idx, face_idx); + allowed_face_removals-=2; // break. this triangle is done break; } + + if (allowed_face_removals <= 0) { break; } } // filter face_indices, remove those that have been collapsed From 63222eb5299c8e69bfc600cc2bf2785e4aa1c18b Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Mon, 12 Sep 2022 10:53:17 +0200 Subject: [PATCH 096/100] Reduce curling of Rear seams, improve its quality Relevant issue: 8841 Rear Seam Not Aligned, Not Rear of Model --- src/libslic3r/GCode/SeamPlacer.cpp | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/libslic3r/GCode/SeamPlacer.cpp b/src/libslic3r/GCode/SeamPlacer.cpp index a54e9f819e..0f4e43a076 100644 --- a/src/libslic3r/GCode/SeamPlacer.cpp +++ b/src/libslic3r/GCode/SeamPlacer.cpp @@ -1,5 +1,7 @@ #include "SeamPlacer.hpp" +#include "Point.hpp" +#include "libslic3r.h" #include "tbb/parallel_for.h" #include "tbb/blocked_range.h" #include "tbb/parallel_reduce.h" @@ -729,8 +731,9 @@ void gather_enforcers_blockers(GlobalModelInfo &result, const PrintObject *po) { struct SeamComparator { SeamPosition setup; float angle_importance; - explicit SeamComparator(SeamPosition setup) : - setup(setup) { + Vec2f rear_attractor; + explicit SeamComparator(SeamPosition setup, const Vec2f& rear_attractor = Vec2f::Zero()) : + setup(setup), rear_attractor(rear_attractor) { angle_importance = setup == spNearest ? SeamPlacer::angle_importance_nearest : SeamPlacer::angle_importance_aligned; } @@ -761,8 +764,9 @@ struct SeamComparator { return false; } - if (setup == SeamPosition::spRear && a.position.y() != b.position.y()) { - return a.position.y() > b.position.y(); + if (setup == SeamPosition::spRear) { + return (a.position.head<2>() - rear_attractor).squaredNorm() < + (b.position.head<2>() - rear_attractor).squaredNorm(); } float distance_penalty_a = 0.0f; @@ -824,7 +828,8 @@ struct SeamComparator { } if (setup == SeamPosition::spRear) { - return a.position.y() + SeamPlacer::seam_align_score_tolerance * 5.0f > b.position.y(); + return (a.position.head<2>() - rear_attractor).squaredNorm() - a.perimeter.flow_width < + (b.position.head<2>() - rear_attractor).squaredNorm(); } float penalty_a = a.overhang + a.visibility @@ -1452,7 +1457,9 @@ void SeamPlacer::init(const Print &print, std::function throw_if_can for (const PrintObject *po : print.objects()) { throw_if_canceled_func(); SeamPosition configured_seam_preference = po->config().seam_position.value; - SeamComparator comparator { configured_seam_preference }; + Vec2f rear_attractor = unscaled(po->bounding_box().center()).cast() + + 1.5f * Vec2f(0.0f, unscale(po->bounding_box().max.y())); + SeamComparator comparator{configured_seam_preference, rear_attractor}; { GlobalModelInfo global_model_info { }; From 30831af8a5f9c640598a0149bdf70deae7444dcb Mon Sep 17 00:00:00 2001 From: YuSanka Date: Thu, 15 Sep 2022 16:48:02 +0200 Subject: [PATCH 097/100] DiffDialog: Implemented a transfer of the selected options from left preset to the right and save them to the new preset * Related to #6130 - Feature Request: Profile settings, Save AND Transfer + SavePresetDialog: Refactoring --- src/libslic3r/Preset.cpp | 49 +++++++++ src/libslic3r/Preset.hpp | 4 + src/libslic3r/PresetBundle.cpp | 67 ++++++++++-- src/libslic3r/PresetBundle.hpp | 8 +- src/slic3r/GUI/GUI_App.cpp | 6 +- src/slic3r/GUI/MainFrame.cpp | 10 +- src/slic3r/GUI/PresetComboBoxes.cpp | 1 - src/slic3r/GUI/SavePresetDialog.cpp | 134 +++++++++++++----------- src/slic3r/GUI/SavePresetDialog.hpp | 19 +++- src/slic3r/GUI/UnsavedChangesDialog.cpp | 54 ++++++++-- src/slic3r/GUI/UnsavedChangesDialog.hpp | 15 ++- 11 files changed, 271 insertions(+), 96 deletions(-) diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 3803c3c7a3..5d152ea17c 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -924,6 +924,55 @@ void PresetCollection::save_current_preset(const std::string &new_name, bool det this->get_selected_preset().save(); } +Preset& PresetCollection::get_preset_with_name(const std::string& new_name, const Preset* initial_preset) +{ + // 1) Find the preset with a new_name or create a new one, + // initialize it with the preset_to config. + auto it = this->find_preset_internal(new_name); + if (it != m_presets.end() && it->name == new_name) { + // Preset with the same name found. + Preset& preset = *it; + if (!preset.is_default && !preset.is_external && !preset.is_system) { + // Overwriting an existing preset if it isn't default/external/system + preset.config = initial_preset->config; + // The newly saved preset can be activated -> make it visible. + preset.is_visible = true; + } + return preset; + } + + // Creating a new preset. + Preset& preset = *m_presets.insert(it, *initial_preset); + std::string& inherits = preset.inherits(); + std::string old_name = preset.name; + preset.name = new_name; + preset.file = this->path_from_name(new_name); + preset.vendor = nullptr; + preset.alias.clear(); + preset.renamed_from.clear(); + if (preset.is_system) { + // Inheriting from a system preset. + inherits = old_name; + } + else if (inherits.empty()) { + // Inheriting from a user preset. Link the new preset to the old preset. + // inherits = old_name; + } + else { + // Inherited from a user preset. Just maintain the "inherited" flag, + // meaning it will inherit from either the system preset, or the inherited user preset. + } + preset.is_default = false; + preset.is_system = false; + preset.is_external = false; + // The newly saved preset can be activated -> make it visible. + preset.is_visible = true; + // Just system presets have aliases + preset.alias.clear(); + + return preset; +} + bool PresetCollection::delete_current_preset() { const Preset &selected = this->get_selected_preset(); diff --git a/src/libslic3r/Preset.hpp b/src/libslic3r/Preset.hpp index 839ebca460..aa801a84fd 100644 --- a/src/libslic3r/Preset.hpp +++ b/src/libslic3r/Preset.hpp @@ -341,6 +341,10 @@ public: // All presets are marked as not modified and the new preset is activated. void save_current_preset(const std::string &new_name, bool detach = false); + // Find the preset with a new_name or create a new one, + // initialize it with the initial_preset config. + Preset& PresetCollection::get_preset_with_name(const std::string& new_name, const Preset* initial_preset); + // Delete the current preset, activate the first visible preset. // returns true if the preset was deleted successfully. bool delete_current_preset(); diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index d98998cc40..176b098761 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -425,16 +425,24 @@ void PresetBundle::load_installed_printers(const AppConfig &config) preset.set_visible_from_appconfig(config); } -const std::string& PresetBundle::get_preset_name_by_alias( const Preset::Type& preset_type, const std::string& alias) const +PresetCollection& PresetBundle::get_presets(Preset::Type type) +{ + assert(type >= Preset::TYPE_PRINT && type <= Preset::TYPE_PRINTER); + + return type == Preset::TYPE_PRINT ? prints : + type == Preset::TYPE_SLA_PRINT ? sla_prints : + type == Preset::TYPE_FILAMENT ? filaments : + type == Preset::TYPE_SLA_MATERIAL ? sla_materials : printers; +} + + +const std::string& PresetBundle::get_preset_name_by_alias( const Preset::Type& preset_type, const std::string& alias) { // there are not aliases for Printers profiles if (preset_type == Preset::TYPE_PRINTER || preset_type == Preset::TYPE_INVALID) return alias; - const PresetCollection& presets = preset_type == Preset::TYPE_PRINT ? prints : - preset_type == Preset::TYPE_SLA_PRINT ? sla_prints : - preset_type == Preset::TYPE_FILAMENT ? filaments : - sla_materials; + const PresetCollection& presets = get_presets(preset_type); return presets.get_preset_name_by_alias(alias); } @@ -442,10 +450,7 @@ const std::string& PresetBundle::get_preset_name_by_alias( const Preset::Type& p void PresetBundle::save_changes_for_preset(const std::string& new_name, Preset::Type type, const std::vector& unselected_options) { - PresetCollection& presets = type == Preset::TYPE_PRINT ? prints : - type == Preset::TYPE_SLA_PRINT ? sla_prints : - type == Preset::TYPE_FILAMENT ? filaments : - type == Preset::TYPE_SLA_MATERIAL ? sla_materials : printers; + PresetCollection& presets = get_presets(type); // if we want to save just some from selected options if (!unselected_options.empty()) { @@ -468,6 +473,50 @@ void PresetBundle::save_changes_for_preset(const std::string& new_name, Preset:: } } +bool PresetBundle::transfer_and_save(Preset::Type type, const std::string& preset_from_name, const std::string& preset_to_name, + const std::string& preset_new_name, const std::vector& options) +{ + if (options.empty()) + return false; + + PresetCollection& presets = get_presets(type); + + const Preset* preset_from = presets.find_preset(preset_from_name, false, false); + const Preset* preset_to = presets.find_preset(preset_to_name, false, false); + if (!preset_from || !preset_to) + return false; + + // Find the preset with a new_name or create a new one, + // initialize it with the preset_to config. + Preset& preset = presets.get_preset_with_name(preset_new_name, preset_to); + if (preset.is_default || preset.is_external || preset.is_system) + // Cannot overwrite the default preset. + return false; + + // Apply options from the preset_from_name. + preset.config.apply_only(preset_from->config, options); + + // Store new_name preset to disk. + preset.save(); + + // update selection + presets.select_preset_by_name(preset_new_name, true); + + // Mark the print & filament enabled if they are compatible with the currently selected preset. + // If saving the preset changes compatibility with other presets, keep the now incompatible dependent presets selected, however with a "red flag" icon showing that they are no more compatible. + update_compatible(PresetSelectCompatibleType::Never); + + if (type == Preset::TYPE_PRINTER) + copy_bed_model_and_texture_if_needed(preset.config); + + if (type == Preset::TYPE_FILAMENT) { + // synchronize the first filament presets. + set_filament_preset(0, filaments.get_selected_preset_name()); + } + + return true; +} + void PresetBundle::load_installed_filaments(AppConfig &config) { if (! config.has_section(AppConfig::SECTION_FILAMENTS)) { diff --git a/src/libslic3r/PresetBundle.hpp b/src/libslic3r/PresetBundle.hpp index 0213069679..549a132866 100644 --- a/src/libslic3r/PresetBundle.hpp +++ b/src/libslic3r/PresetBundle.hpp @@ -54,6 +54,8 @@ public: // extruders.size() should be the same as printers.get_edited_preset().config.nozzle_diameter.size() std::vector filament_presets; + PresetCollection& get_presets(Preset::Type preset_type); + // The project configuration values are kept separated from the print/filament/printer preset, // they are being serialized / deserialized from / to the .amf, .3mf, .config, .gcode, // and they are being used by slicing core. @@ -142,11 +144,15 @@ public: // If the "vendor" section is missing, enable all models and variants of the particular vendor. void load_installed_printers(const AppConfig &config); - const std::string& get_preset_name_by_alias(const Preset::Type& preset_type, const std::string& alias) const; + const std::string& get_preset_name_by_alias(const Preset::Type& preset_type, const std::string& alias); // Save current preset of a provided type under a new name. If the name is different from the old one, // Unselected option would be reverted to the beginning values void save_changes_for_preset(const std::string& new_name, Preset::Type type, const std::vector& unselected_options); + // Transfer options form preset_from_name preset to preset_to_name preset and save preset_to_name preset as new new_name preset + // Return false, if new preset wasn't saved + bool transfer_and_save(Preset::Type type, const std::string& preset_from_name, const std::string& preset_to_name, + const std::string& new_name, const std::vector& options); static const char *PRUSA_BUNDLE; private: diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index c054a2e3bc..b03010b17f 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -2540,8 +2540,7 @@ bool GUI_App::check_and_save_current_preset_changes(const wxString& caption, con // synchronize config.ini with the current selections. preset_bundle->export_selections(*app_config); - MessageDialog(nullptr, _L_PLURAL("The preset modifications are successfully saved", - "The presets modifications are successfully saved", dlg.get_names_and_types().size())).ShowModal(); + MessageDialog(nullptr, dlg.msg_success_saved_modifications(dlg.get_names_and_types().size())).ShowModal(); } } @@ -2601,8 +2600,7 @@ bool GUI_App::check_and_keep_current_preset_changes(const wxString& caption, con // synchronize config.ini with the current selections. preset_bundle->export_selections(*app_config); - wxString text = _L_PLURAL("The preset modifications are successfully saved", - "The presets modifications are successfully saved", preset_names_and_types.size()); + wxString text = dlg.msg_success_saved_modifications(preset_names_and_types.size()); if (!is_called_from_configwizard) text += "\n\n" + _L("For new project all modifications will be reseted"); diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 3875d8cdf7..0c83551769 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -296,13 +296,7 @@ void MainFrame::bind_diff_dialog() auto transfer = [this, get_tab](Preset::Type type) { get_tab(type)->transfer_options(diff_dialog.get_left_preset_name(type), diff_dialog.get_right_preset_name(type), - std::move(diff_dialog.get_selected_options(type))); - }; - - auto save = [this, get_tab](Preset::Type type) { - get_tab(type)->save_options(diff_dialog.get_left_preset_name(type), - diff_dialog.get_right_preset_name(type), - std::move(diff_dialog.get_selected_options(type))); + diff_dialog.get_selected_options(type)); }; auto process_options = [this](std::function process) { @@ -318,8 +312,6 @@ void MainFrame::bind_diff_dialog() }; diff_dialog.Bind(EVT_DIFF_DIALOG_TRANSFER, [this, process_options, transfer](SimpleEvent&) { process_options(transfer); }); - - diff_dialog.Bind(EVT_DIFF_DIALOG_SAVE, [this, process_options, save](SimpleEvent&) { process_options(save); }); } diff --git a/src/slic3r/GUI/PresetComboBoxes.cpp b/src/slic3r/GUI/PresetComboBoxes.cpp index 7f781e243e..65469e6a03 100644 --- a/src/slic3r/GUI/PresetComboBoxes.cpp +++ b/src/slic3r/GUI/PresetComboBoxes.cpp @@ -38,7 +38,6 @@ #include "../Utils/UndoRedo.hpp" #include "BitmapCache.hpp" #include "PhysicalPrinterDialog.hpp" -#include "SavePresetDialog.hpp" #include "MsgDialog.hpp" // A workaround for a set of issues related to text fitting into gtk widgets: diff --git a/src/slic3r/GUI/SavePresetDialog.cpp b/src/slic3r/GUI/SavePresetDialog.cpp index 974d771b95..89a5d7a8de 100644 --- a/src/slic3r/GUI/SavePresetDialog.cpp +++ b/src/slic3r/GUI/SavePresetDialog.cpp @@ -8,7 +8,6 @@ #include #include #include -#include #include "libslic3r/PresetBundle.hpp" @@ -22,25 +21,23 @@ using Slic3r::GUI::format_wxstr; namespace Slic3r { namespace GUI { -#define BORDER_W 10 - +constexpr auto BORDER_W = 10; //----------------------------------------------- // SavePresetDialog::Item //----------------------------------------------- -SavePresetDialog::Item::Item(Preset::Type type, const std::string& suffix, wxBoxSizer* sizer, SavePresetDialog* parent): - m_type(type), - m_parent(parent) +std::string SavePresetDialog::Item::get_init_preset_name(const std::string &suffix) { - Tab* tab = wxGetApp().get_tab(m_type); - assert(tab); - m_presets = tab->get_presets(); + PresetBundle* preset_bundle = m_parent->get_preset_bundle(); + if (!preset_bundle) + preset_bundle = wxGetApp().preset_bundle; + m_presets = &preset_bundle->get_presets(m_type); const Preset& sel_preset = m_presets->get_selected_preset(); - std::string preset_name = sel_preset.is_default ? "Untitled" : - sel_preset.is_system ? (boost::format(("%1% - %2%")) % sel_preset.name % suffix).str() : - sel_preset.name; + std::string preset_name = sel_preset.is_default ? "Untitled" : + sel_preset.is_system ? (boost::format(("%1% - %2%")) % sel_preset.name % suffix).str() : + sel_preset.name; // if name contains extension if (boost::iends_with(preset_name, ".ini")) { @@ -48,18 +45,11 @@ SavePresetDialog::Item::Item(Preset::Type type, const std::string& suffix, wxBox preset_name.resize(len); } - std::vector values; - for (const Preset& preset : *m_presets) { - if (preset.is_default || preset.is_system || preset.is_external) - continue; - values.push_back(preset.name); - } - - std::string label_str = m_parent->is_for_rename() ?_utf8(L("Rename %s to:")) : _utf8(L("Save %s as:")); - wxStaticText* label_top = new wxStaticText(m_parent, wxID_ANY, from_u8((boost::format(label_str) % into_u8(tab->title())).str())); - - m_valid_bmp = new wxStaticBitmap(m_parent, wxID_ANY, *get_bmp_bundle("tick_mark")); + return preset_name; +} +void SavePresetDialog::Item::init_input_name_ctrl(wxBoxSizer *input_name_sizer, const std::string preset_name) +{ if (m_parent->is_for_rename()) { #ifdef _WIN32 long style = wxBORDER_SIMPLE; @@ -68,10 +58,19 @@ SavePresetDialog::Item::Item(Preset::Type type, const std::string& suffix, wxBox #endif m_text_ctrl = new wxTextCtrl(m_parent, wxID_ANY, from_u8(preset_name), wxDefaultPosition, wxSize(35 * wxGetApp().em_unit(), -1), style); m_text_ctrl->Bind(wxEVT_TEXT, [this](wxCommandEvent&) { update(); }); + + input_name_sizer->Add(m_text_ctrl,1, wxEXPAND, BORDER_W); } else { + std::vector values; + for (const Preset&preset : *m_presets) { + if (preset.is_default || preset.is_system || preset.is_external) + continue; + values.push_back(preset.name); + } + m_combo = new wxComboBox(m_parent, wxID_ANY, from_u8(preset_name), wxDefaultPosition, wxSize(35 * wxGetApp().em_unit(), -1)); - for (const std::string& value : values) + for (const std::string&value : values) m_combo->Append(from_u8(value)); m_combo->Bind(wxEVT_TEXT, [this](wxCommandEvent&) { update(); }); @@ -80,20 +79,34 @@ SavePresetDialog::Item::Item(Preset::Type type, const std::string& suffix, wxBox // So process wxEVT_COMBOBOX too m_combo->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent&) { update(); }); #endif //__WXOSX__ - } - m_valid_label = new wxStaticText(m_parent, wxID_ANY, ""); + input_name_sizer->Add(m_combo, 1, wxEXPAND, BORDER_W); + } +} + +wxString SavePresetDialog::Item::get_top_label_text() const +{ + const std::string label_str = m_parent->is_for_rename() ?_u8L("Rename %s to:") : _u8L("Save %s as:"); + Tab* tab = wxGetApp().get_tab(m_type); + return from_u8((boost::format(label_str) % into_u8(tab->title())).str()); +} + +SavePresetDialog::Item::Item(Preset::Type type, const std::string& suffix, wxBoxSizer* sizer, SavePresetDialog* parent): + m_type(type), + m_parent(parent), + m_valid_bmp(new wxStaticBitmap(m_parent, wxID_ANY, *get_bmp_bundle("tick_mark"))), + m_valid_label(new wxStaticText(m_parent, wxID_ANY, "")) +{ m_valid_label->SetFont(wxGetApp().bold_font()); - wxBoxSizer* combo_sizer = new wxBoxSizer(wxHORIZONTAL); - combo_sizer->Add(m_valid_bmp, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, BORDER_W); - if (m_parent->is_for_rename()) - combo_sizer->Add(m_text_ctrl,1, wxEXPAND, BORDER_W); - else - combo_sizer->Add(m_combo, 1, wxEXPAND, BORDER_W); + wxStaticText* label_top = new wxStaticText(m_parent, wxID_ANY, get_top_label_text()); + + wxBoxSizer* input_name_sizer = new wxBoxSizer(wxHORIZONTAL); + input_name_sizer->Add(m_valid_bmp, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, BORDER_W); + init_input_name_ctrl(input_name_sizer, get_init_preset_name(suffix)); sizer->Add(label_top, 0, wxEXPAND | wxTOP| wxBOTTOM, BORDER_W); - sizer->Add(combo_sizer, 0, wxEXPAND | wxBOTTOM, BORDER_W); + sizer->Add(input_name_sizer,0, wxEXPAND | wxBOTTOM, BORDER_W); sizer->Add(m_valid_label, 0, wxEXPAND | wxLEFT, 3*BORDER_W); if (m_type == Preset::TYPE_PRINTER) @@ -107,7 +120,7 @@ void SavePresetDialog::Item::update() bool rename = m_parent->is_for_rename(); m_preset_name = into_u8(rename ? m_text_ctrl->GetValue() : m_combo->GetValue()); - m_valid_type = Valid; + m_valid_type = ValidationType::Valid; wxString info_line; const char* unusable_symbols = "<>[]:/\\|?*\""; @@ -117,44 +130,44 @@ void SavePresetDialog::Item::update() if (m_preset_name.find_first_of(unusable_symbols[i]) != std::string::npos) { info_line = _L("The supplied name is not valid;") + "\n" + _L("the following characters are not allowed:") + " " + unusable_symbols; - m_valid_type = NoValid; + m_valid_type = ValidationType::NoValid; break; } } - if (m_valid_type == Valid && m_preset_name.find(unusable_suffix) != std::string::npos) { + if (m_valid_type == ValidationType::Valid && m_preset_name.find(unusable_suffix) != std::string::npos) { info_line = _L("The supplied name is not valid;") + "\n" + _L("the following suffix is not allowed:") + "\n\t" + from_u8(PresetCollection::get_suffix_modified()); - m_valid_type = NoValid; + m_valid_type = ValidationType::NoValid; } - if (m_valid_type == Valid && m_preset_name == "- default -") { + if (m_valid_type == ValidationType::Valid && m_preset_name == "- default -") { info_line = _L("The supplied name is not available."); - m_valid_type = NoValid; + m_valid_type = ValidationType::NoValid; } const Preset* existing = m_presets->find_preset(m_preset_name, false); - if (m_valid_type == Valid && existing && (existing->is_default || existing->is_system)) { + if (m_valid_type == ValidationType::Valid && existing && (existing->is_default || existing->is_system)) { info_line = rename ? _L("The supplied name is used for a system profile.") : _L("Cannot overwrite a system profile."); - m_valid_type = NoValid; + m_valid_type = ValidationType::NoValid; } - if (m_valid_type == Valid && existing && (existing->is_external)) { + if (m_valid_type == ValidationType::Valid && existing && (existing->is_external)) { info_line = rename ? _L("The supplied name is used for a external profile.") : _L("Cannot overwrite an external profile."); - m_valid_type = NoValid; + m_valid_type = ValidationType::NoValid; } - if (m_valid_type == Valid && existing) + if (m_valid_type == ValidationType::Valid && existing) { if (m_preset_name == m_presets->get_selected_preset_name()) { if (!rename && m_presets->get_edited_preset().is_dirty) info_line = _L("Just save preset modifications"); else info_line = _L("Nothing changed"); - m_valid_type = Valid; + m_valid_type = ValidationType::Valid; } else { if (existing->is_compatible) @@ -162,31 +175,31 @@ void SavePresetDialog::Item::update() else info_line = from_u8((boost::format(_u8L("Preset with name \"%1%\" already exists and is incompatible with selected printer.")) % m_preset_name).str()); info_line += "\n" + _L("Note: This preset will be replaced after saving"); - m_valid_type = Warning; + m_valid_type = ValidationType::Warning; } } - if (m_valid_type == Valid && m_preset_name.empty()) { + if (m_valid_type == ValidationType::Valid && m_preset_name.empty()) { info_line = _L("The name cannot be empty."); - m_valid_type = NoValid; + m_valid_type = ValidationType::NoValid; } - if (m_valid_type == Valid && m_preset_name.find_first_of(' ') == 0) { + if (m_valid_type == ValidationType::Valid && m_preset_name.find_first_of(' ') == 0) { info_line = _L("The name cannot start with space character."); - m_valid_type = NoValid; + m_valid_type = ValidationType::NoValid; } - if (m_valid_type == Valid && m_preset_name.find_last_of(' ') == m_preset_name.length()-1) { + if (m_valid_type == ValidationType::Valid && m_preset_name.find_last_of(' ') == m_preset_name.length()-1) { info_line = _L("The name cannot end with space character."); - m_valid_type = NoValid; + m_valid_type = ValidationType::NoValid; } - if (m_valid_type == Valid && m_presets->get_preset_name_by_alias(m_preset_name) != m_preset_name) { + if (m_valid_type == ValidationType::Valid && m_presets->get_preset_name_by_alias(m_preset_name) != m_preset_name) { info_line = _L("The name cannot be the same as a preset alias name."); - m_valid_type = NoValid; + m_valid_type = ValidationType::NoValid; } - if (!m_parent->get_info_line_extention().IsEmpty() && m_valid_type != NoValid) + if (!m_parent->get_info_line_extention().IsEmpty() && m_valid_type != ValidationType::NoValid) info_line += "\n\n" + m_parent->get_info_line_extention(); m_valid_label->SetLabel(info_line); @@ -202,14 +215,14 @@ void SavePresetDialog::Item::update() void SavePresetDialog::Item::update_valid_bmp() { - std::string bmp_name = m_valid_type == Warning ? "exclamation" : - m_valid_type == NoValid ? "cross" : "tick_mark" ; + std::string bmp_name = m_valid_type == ValidationType::Warning ? "exclamation" : + m_valid_type == ValidationType::NoValid ? "cross" : "tick_mark" ; m_valid_bmp->SetBitmap(*get_bmp_bundle(bmp_name)); } void SavePresetDialog::Item::accept() { - if (m_valid_type == Warning) + if (m_valid_type == ValidationType::Warning) m_presets->delete_preset(m_preset_name); } @@ -224,8 +237,9 @@ SavePresetDialog::SavePresetDialog(wxWindow* parent, Preset::Type type, std::str build(std::vector{type}, suffix); } -SavePresetDialog::SavePresetDialog(wxWindow* parent, std::vector types, std::string suffix) - : DPIDialog(parent, wxID_ANY, _L("Save presets"), wxDefaultPosition, wxSize(45 * wxGetApp().em_unit(), 5 * wxGetApp().em_unit()), wxDEFAULT_DIALOG_STYLE | wxICON_WARNING | wxRESIZE_BORDER) +SavePresetDialog::SavePresetDialog(wxWindow* parent, std::vector types, std::string suffix, PresetBundle* preset_bundle/* = nullptr*/) + : DPIDialog(parent, wxID_ANY, _L("Save presets"), wxDefaultPosition, wxSize(45 * wxGetApp().em_unit(), 5 * wxGetApp().em_unit()), wxDEFAULT_DIALOG_STYLE | wxICON_WARNING | wxRESIZE_BORDER), + m_preset_bundle(preset_bundle) { build(types, suffix); } diff --git a/src/slic3r/GUI/SavePresetDialog.hpp b/src/slic3r/GUI/SavePresetDialog.hpp index 1ecda7b7fd..aec58ac7a3 100644 --- a/src/slic3r/GUI/SavePresetDialog.hpp +++ b/src/slic3r/GUI/SavePresetDialog.hpp @@ -29,7 +29,7 @@ class SavePresetDialog : public DPIDialog struct Item { - enum ValidationType + enum class ValidationType { Valid, NoValid, @@ -41,15 +41,15 @@ class SavePresetDialog : public DPIDialog void update_valid_bmp(); void accept(); - bool is_valid() const { return m_valid_type != NoValid; } + bool is_valid() const { return m_valid_type != ValidationType::NoValid; } Preset::Type type() const { return m_type; } std::string preset_name() const { return m_preset_name; } private: Preset::Type m_type; - ValidationType m_valid_type; std::string m_preset_name; + ValidationType m_valid_type {ValidationType::NoValid}; SavePresetDialog* m_parent {nullptr}; wxStaticBitmap* m_valid_bmp {nullptr}; wxComboBox* m_combo {nullptr}; @@ -58,7 +58,11 @@ class SavePresetDialog : public DPIDialog PresetCollection* m_presets {nullptr}; - void update(); + std::string get_init_preset_name(const std::string &suffix); + void init_input_name_ctrl(wxBoxSizer *input_name_sizer, std::string preset_name); + wxString get_top_label_text() const ; + + void update(); }; std::vector m_items; @@ -73,17 +77,22 @@ class SavePresetDialog : public DPIDialog bool m_use_for_rename{false}; wxString m_info_line_extention{wxEmptyString}; + PresetBundle* m_preset_bundle{ nullptr }; + public: const wxString& get_info_line_extention() { return m_info_line_extention; } SavePresetDialog(wxWindow* parent, Preset::Type type, std::string suffix = ""); - SavePresetDialog(wxWindow* parent, std::vector types, std::string suffix = ""); + SavePresetDialog(wxWindow* parent, std::vector types, std::string suffix = "", PresetBundle* preset_bundle = nullptr); SavePresetDialog(wxWindow* parent, Preset::Type type, bool rename, const wxString& info_line_extention); ~SavePresetDialog(); void AddItem(Preset::Type type, const std::string& suffix); + void set_preset_bundle(PresetBundle* preset_bundle) { m_preset_bundle = preset_bundle; } + PresetBundle* get_preset_bundle() const { return m_preset_bundle; } + std::string get_name(); std::string get_name(Preset::Type type); diff --git a/src/slic3r/GUI/UnsavedChangesDialog.cpp b/src/slic3r/GUI/UnsavedChangesDialog.cpp index 9bcb9def0b..7fdec251ce 100644 --- a/src/slic3r/GUI/UnsavedChangesDialog.cpp +++ b/src/slic3r/GUI/UnsavedChangesDialog.cpp @@ -36,7 +36,6 @@ namespace Slic3r { namespace GUI { wxDEFINE_EVENT(EVT_DIFF_DIALOG_TRANSFER, SimpleEvent); -wxDEFINE_EVENT(EVT_DIFF_DIALOG_SAVE, SimpleEvent); // ---------------------------------------------------------------------------- @@ -1336,6 +1335,12 @@ void UnsavedChangesDialog::update_tree(Preset::Type type, PresetCollection* pres searcher.sort_options_by_label(); } +wxString UnsavedChangesDialog::msg_success_saved_modifications(size_t saved_presets_cnt) +{ + return _L_PLURAL("The preset modifications are successfully saved", + "The presets modifications are successfully saved", static_cast(saved_presets_cnt)); +} + void UnsavedChangesDialog::on_dpi_changed(const wxRect& suggested_rect) { int em = em_unit(); @@ -1488,10 +1493,11 @@ void DiffPresetDialog::create_presets_sizer() PresetComboBox*cb = (*cb_); cb->show_modif_preset_separately(); cb->set_selection_changed_function([this, new_type, preset_bundle, cb](int selection) { - if (m_view_type == Preset::TYPE_INVALID) { - std::string preset_name = Preset::remove_suffix_modified(cb->GetString(selection).ToUTF8().data()); + std::string preset_name = Preset::remove_suffix_modified(cb->GetString(selection).ToUTF8().data()); + if (m_view_type == Preset::TYPE_INVALID) update_compatibility(preset_name, new_type, preset_bundle); - } + // update selection inside of related presets + preset_bundle->get_presets(new_type).select_preset_by_name(preset_name, true); update_tree(); }); if (collection->get_selected_idx() != (size_t)-1) @@ -2002,10 +2008,47 @@ void DiffPresetDialog::update_compatibility(const std::string& preset_name, Pres } } +bool DiffPresetDialog::save() +{ + presets_to_saves.clear(); + + std::vector types_for_save; + + for (const Preset::Type& type : m_pr_technology == ptFFF ? std::initializer_list{Preset::TYPE_PRINTER, Preset::TYPE_PRINT, Preset::TYPE_FILAMENT} : + std::initializer_list{Preset::TYPE_PRINTER, Preset::TYPE_SLA_PRINT, Preset::TYPE_SLA_MATERIAL }) + if (!m_tree->options(type, true).empty()) { + types_for_save.emplace_back(type); + presets_to_saves.emplace_back(PresetToSave{ type, get_left_preset_name(type), get_right_preset_name(type), get_right_preset_name(type) }); + } + + if (!types_for_save.empty()) { + SavePresetDialog save_dlg(this, types_for_save, _u8L("Modified"), m_preset_bundle_right.get()); + if (save_dlg.ShowModal() != wxID_OK) + return false; + + for (auto& preset : presets_to_saves) { + const std::string& name = save_dlg.get_name(preset.type); + if (!name.empty()) + preset.new_name = name; + } + } + return true; +} + void DiffPresetDialog::button_event(Action act) { if (act == Action::Save) { - wxPostEvent(this, SimpleEvent(EVT_DIFF_DIALOG_SAVE)); + if (save()) { + size_t saved_cnt = 0; + for (const auto& preset : presets_to_saves) + if (wxGetApp().preset_bundle->transfer_and_save(preset.type, preset.from_name, preset.to_name, preset.new_name, m_tree->options(preset.type, true))) + saved_cnt++; + + if (saved_cnt > 0) + MessageDialog(nullptr, UnsavedChangesDialog::msg_success_saved_modifications(saved_cnt)).ShowModal(); + + update_presets(); + } } else { Hide(); @@ -2022,7 +2065,6 @@ std::string DiffPresetDialog::get_left_preset_name(Preset::Type type) std::string DiffPresetDialog::get_right_preset_name(Preset::Type type) { - PresetComboBox* cb = m_preset_combos[int(type - Preset::TYPE_PRINT)].presets_right; return Preset::remove_suffix_modified(get_selection(cb)); } diff --git a/src/slic3r/GUI/UnsavedChangesDialog.hpp b/src/slic3r/GUI/UnsavedChangesDialog.hpp index 2397c419e2..a303593ca8 100644 --- a/src/slic3r/GUI/UnsavedChangesDialog.hpp +++ b/src/slic3r/GUI/UnsavedChangesDialog.hpp @@ -16,7 +16,6 @@ namespace Slic3r { namespace GUI{ wxDECLARE_EVENT(EVT_DIFF_DIALOG_TRANSFER, SimpleEvent); -wxDECLARE_EVENT(EVT_DIFF_DIALOG_SAVE, SimpleEvent); // ---------------------------------------------------------------------------- // ModelNode: a node inside DiffModel @@ -321,6 +320,8 @@ public: std::vector get_selected_options() { return m_tree->selected_options(); } bool has_unselected_options() { return m_tree->has_unselected_options(); } + static wxString msg_success_saved_modifications(size_t saved_presets_cnt); + protected: void on_dpi_changed(const wxRect& suggested_rect) override; void on_sys_color_changed() override; @@ -376,6 +377,7 @@ class DiffPresetDialog : public DPIDialog void update_compatibility(const std::string& preset_name, Preset::Type type, PresetBundle* preset_bundle); void button_event(Action act); + bool save(); struct DiffPresets { @@ -386,6 +388,17 @@ class DiffPresetDialog : public DPIDialog std::vector m_preset_combos; + // attributes witch are used for save preset + struct PresetToSave + { + Preset::Type type; + std::string from_name; + std::string to_name; + std::string new_name; + }; + + std::vector presets_to_saves; + public: DiffPresetDialog(MainFrame*mainframe); ~DiffPresetDialog() override = default; From 97ab4cae4fa0c927d5e83485613087bb1d846033 Mon Sep 17 00:00:00 2001 From: YuSanka Date: Fri, 16 Sep 2022 09:54:01 +0200 Subject: [PATCH 098/100] Tab: Fixed visibility for "Rename preset" button --- src/slic3r/GUI/Tab.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 917c9edaa9..ed73e5552d 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -41,7 +41,6 @@ #include "Plater.hpp" #include "MainFrame.hpp" #include "format.hpp" -#include "PhysicalPrinterDialog.hpp" #include "UnsavedChangesDialog.hpp" #include "SavePresetDialog.hpp" #include "MsgDialog.hpp" @@ -3187,7 +3186,8 @@ void Tab::update_btns_enabling() const Preset& preset = m_presets->get_edited_preset(); m_btn_delete_preset->Show((m_type == Preset::TYPE_PRINTER && m_preset_bundle->physical_printers.has_selection()) || (!preset.is_default && !preset.is_system)); - m_btn_rename_preset->Show(!preset.is_default && !preset.is_system && !m_presets_choice->is_selected_physical_printer()); + m_btn_rename_preset->Show(!preset.is_default && !preset.is_system && !preset.is_external && + !wxGetApp().preset_bundle->physical_printers.has_selection()); if (m_btn_edit_ph_printer) m_btn_edit_ph_printer->SetToolTip( m_preset_bundle->physical_printers.has_selection() ? From 1bd0c831217c25475d8587547789a7910ee3208e Mon Sep 17 00:00:00 2001 From: YuSanka Date: Fri, 16 Sep 2022 15:03:27 +0200 Subject: [PATCH 099/100] Follow-up https://github.com/Prusa-Development/PrusaSlicerPrivate/commit/97ab4cae4fa0c927d5e83485613087bb1d846033 : typo fix --- src/libslic3r/Preset.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libslic3r/Preset.hpp b/src/libslic3r/Preset.hpp index aa801a84fd..f0a81a71ad 100644 --- a/src/libslic3r/Preset.hpp +++ b/src/libslic3r/Preset.hpp @@ -343,7 +343,7 @@ public: // Find the preset with a new_name or create a new one, // initialize it with the initial_preset config. - Preset& PresetCollection::get_preset_with_name(const std::string& new_name, const Preset* initial_preset); + Preset& get_preset_with_name(const std::string& new_name, const Preset* initial_preset); // Delete the current preset, activate the first visible preset. // returns true if the preset was deleted successfully. From 84f651f85d3deca795443d1d3908c1ec71d2db7a Mon Sep 17 00:00:00 2001 From: YuSanka Date: Fri, 16 Sep 2022 17:25:00 +0200 Subject: [PATCH 100/100] DiffDialog: Save preset * Fixed a crash after save the preset with existing name * Added update of the PresetComboBoxes on SettingsTabs and Sidebar * Some code refactoring --- src/libslic3r/Preset.cpp | 19 +++++++-- src/libslic3r/PresetBundle.cpp | 9 ++--- src/slic3r/GUI/MainFrame.cpp | 9 ++++- src/slic3r/GUI/SavePresetDialog.cpp | 5 ++- src/slic3r/GUI/SavePresetDialog.hpp | 10 ++--- src/slic3r/GUI/Tab.cpp | 14 +------ src/slic3r/GUI/Tab.hpp | 1 - src/slic3r/GUI/UnsavedChangesDialog.cpp | 51 ++++++++++++++++++------- src/slic3r/GUI/UnsavedChangesDialog.hpp | 12 +++--- 9 files changed, 81 insertions(+), 49 deletions(-) diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 5d152ea17c..aba428f8b0 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -932,8 +932,8 @@ Preset& PresetCollection::get_preset_with_name(const std::string& new_name, cons if (it != m_presets.end() && it->name == new_name) { // Preset with the same name found. Preset& preset = *it; - if (!preset.is_default && !preset.is_external && !preset.is_system) { - // Overwriting an existing preset if it isn't default/external/system + if (!preset.is_default && !preset.is_external && !preset.is_system && initial_preset->name != new_name) { + // Overwriting an existing preset if it isn't default/external/system or isn't an initial_preset preset.config = initial_preset->config; // The newly saved preset can be activated -> make it visible. preset.is_visible = true; @@ -941,10 +941,12 @@ Preset& PresetCollection::get_preset_with_name(const std::string& new_name, cons return preset; } + const std::string selected_preset_name = this->get_selected_preset_name(); + // Creating a new preset. Preset& preset = *m_presets.insert(it, *initial_preset); std::string& inherits = preset.inherits(); - std::string old_name = preset.name; + std::string old_name = preset.name; preset.name = new_name; preset.file = this->path_from_name(new_name); preset.vendor = nullptr; @@ -970,7 +972,16 @@ Preset& PresetCollection::get_preset_with_name(const std::string& new_name, cons // Just system presets have aliases preset.alias.clear(); - return preset; + // sort printers and get new it + std::sort(m_presets.begin(), m_presets.end()); + + // set initial preset selection + this->select_preset_by_name(selected_preset_name, true); + + it = this->find_preset_internal(new_name); + assert(it != m_presets.end()); + + return *it; } bool PresetCollection::delete_current_preset() diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index 176b098761..b783c8aebc 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -481,9 +481,8 @@ bool PresetBundle::transfer_and_save(Preset::Type type, const std::string& prese PresetCollection& presets = get_presets(type); - const Preset* preset_from = presets.find_preset(preset_from_name, false, false); const Preset* preset_to = presets.find_preset(preset_to_name, false, false); - if (!preset_from || !preset_to) + if (!preset_to) return false; // Find the preset with a new_name or create a new one, @@ -494,14 +493,14 @@ bool PresetBundle::transfer_and_save(Preset::Type type, const std::string& prese return false; // Apply options from the preset_from_name. + const Preset* preset_from = presets.find_preset(preset_from_name, false, false); + if (!preset_from) + return false; preset.config.apply_only(preset_from->config, options); // Store new_name preset to disk. preset.save(); - // update selection - presets.select_preset_by_name(preset_new_name, true); - // Mark the print & filament enabled if they are compatible with the currently selected preset. // If saving the preset changes compatibility with other presets, keep the now incompatible dependent presets selected, however with a "red flag" icon showing that they are no more compatible. update_compatible(PresetSelectCompatibleType::Never); diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 0c83551769..df531c5629 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -299,6 +299,11 @@ void MainFrame::bind_diff_dialog() diff_dialog.get_selected_options(type)); }; + auto update_presets = [this, get_tab](Preset::Type type) { + get_tab(type)->update_preset_choice(); + m_plater->sidebar().update_presets(type); + }; + auto process_options = [this](std::function process) { const Preset::Type diff_dlg_type = diff_dialog.view_type(); if (diff_dlg_type == Preset::TYPE_INVALID) { @@ -311,7 +316,9 @@ void MainFrame::bind_diff_dialog() process(diff_dlg_type); }; - diff_dialog.Bind(EVT_DIFF_DIALOG_TRANSFER, [this, process_options, transfer](SimpleEvent&) { process_options(transfer); }); + diff_dialog.Bind(EVT_DIFF_DIALOG_TRANSFER, [this, process_options, transfer](SimpleEvent&) { process_options(transfer); }); + + diff_dialog.Bind(EVT_DIFF_DIALOG_UPDATE_PRESETS,[this, process_options, update_presets](SimpleEvent&) { process_options(update_presets); }); } diff --git a/src/slic3r/GUI/SavePresetDialog.cpp b/src/slic3r/GUI/SavePresetDialog.cpp index 89a5d7a8de..57aa5da983 100644 --- a/src/slic3r/GUI/SavePresetDialog.cpp +++ b/src/slic3r/GUI/SavePresetDialog.cpp @@ -163,8 +163,9 @@ void SavePresetDialog::Item::update() if (m_valid_type == ValidationType::Valid && existing) { if (m_preset_name == m_presets->get_selected_preset_name()) { - if (!rename && m_presets->get_edited_preset().is_dirty) - info_line = _L("Just save preset modifications"); + if (!rename && m_presets->get_edited_preset().is_dirty || + m_parent->get_preset_bundle()) // means that we save modifications from the DiffDialog + info_line = _L("Save preset modifications to existing user profile"); else info_line = _L("Nothing changed"); m_valid_type = ValidationType::Valid; diff --git a/src/slic3r/GUI/SavePresetDialog.hpp b/src/slic3r/GUI/SavePresetDialog.hpp index aec58ac7a3..c40a5896f2 100644 --- a/src/slic3r/GUI/SavePresetDialog.hpp +++ b/src/slic3r/GUI/SavePresetDialog.hpp @@ -86,15 +86,13 @@ public: SavePresetDialog(wxWindow* parent, Preset::Type type, std::string suffix = ""); SavePresetDialog(wxWindow* parent, std::vector types, std::string suffix = "", PresetBundle* preset_bundle = nullptr); SavePresetDialog(wxWindow* parent, Preset::Type type, bool rename, const wxString& info_line_extention); - ~SavePresetDialog(); + ~SavePresetDialog() override; void AddItem(Preset::Type type, const std::string& suffix); - void set_preset_bundle(PresetBundle* preset_bundle) { m_preset_bundle = preset_bundle; } - PresetBundle* get_preset_bundle() const { return m_preset_bundle; } - - std::string get_name(); - std::string get_name(Preset::Type type); + PresetBundle* get_preset_bundle() const { return m_preset_bundle; } + std::string get_name(); + std::string get_name(Preset::Type type); bool enable_ok_btn() const; void add_info_for_edit_ph_printer(wxBoxSizer *sizer); diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index ed73e5552d..d27bfc9315 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -47,14 +47,14 @@ #include "Notebook.hpp" #ifdef WIN32 - #include + #include #endif // WIN32 namespace Slic3r { namespace GUI { Tab::Tab(wxBookCtrlBase* parent, const wxString& title, Preset::Type type) : - m_parent(parent), m_title(title), m_type(type) + m_parent(parent), m_type(type), m_title(title) { Create(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBK_LEFT | wxTAB_TRAVERSAL/*, name*/); this->SetFont(Slic3r::GUI::wxGetApp().normal_font()); @@ -3611,16 +3611,6 @@ void Tab::transfer_options(const std::string &name_from, const std::string &name load_current_preset(); } -void Tab::save_options(const std::string &name_from, const std::string &name_to, std::vector options) -{ - if (options.empty()) - return; - - Preset* preset_from = m_presets->find_preset(name_from); - Preset* preset_to = m_presets->find_preset(name_to); - -} - // Save the current preset into file. // This removes the "dirty" flag of the preset, possibly creates a new preset under a new name, // and activates the new preset. diff --git a/src/slic3r/GUI/Tab.hpp b/src/slic3r/GUI/Tab.hpp index 9c4b101407..448e4be050 100644 --- a/src/slic3r/GUI/Tab.hpp +++ b/src/slic3r/GUI/Tab.hpp @@ -323,7 +323,6 @@ public: void compare_preset(); void transfer_options(const std::string&name_from, const std::string&name_to, std::vector options); - void save_options(const std::string &name_from, const std::string &name_to, std::vector options); void save_preset(std::string name = std::string(), bool detach = false); void rename_preset(); void delete_preset(); diff --git a/src/slic3r/GUI/UnsavedChangesDialog.cpp b/src/slic3r/GUI/UnsavedChangesDialog.cpp index 7fdec251ce..c04a536efb 100644 --- a/src/slic3r/GUI/UnsavedChangesDialog.cpp +++ b/src/slic3r/GUI/UnsavedChangesDialog.cpp @@ -35,7 +35,8 @@ namespace Slic3r { namespace GUI { -wxDEFINE_EVENT(EVT_DIFF_DIALOG_TRANSFER, SimpleEvent); +wxDEFINE_EVENT(EVT_DIFF_DIALOG_TRANSFER, SimpleEvent); +wxDEFINE_EVENT(EVT_DIFF_DIALOG_UPDATE_PRESETS, SimpleEvent); // ---------------------------------------------------------------------------- @@ -1384,7 +1385,7 @@ FullCompareDialog::FullCompareDialog(const wxString& option_name, const wxString wxFlexGridSizer* grid_sizer = new wxFlexGridSizer(2, has_new_value_column ? 3 : 2, 1, 0); grid_sizer->SetFlexibleDirection(wxBOTH); - for (size_t col = 0 ; col < grid_sizer->GetCols(); col++) + for (int col = 0 ; col < grid_sizer->GetCols(); col++) grid_sizer->AddGrowableCol(col, 1); grid_sizer->AddGrowableRow(1,1); @@ -1750,9 +1751,10 @@ void DiffPresetDialog::show(Preset::Type type /* = Preset::TYPE_INVALID*/) Show(); } -void DiffPresetDialog::update_presets(Preset::Type type) +void DiffPresetDialog::update_presets(Preset::Type type, bool update_preset_bundles_from_app/* = true */) { - update_bundles_from_app(); + if (update_preset_bundles_from_app) + update_bundles_from_app(); update_controls_visibility(type); if (type == Preset::TYPE_INVALID) @@ -2010,7 +2012,7 @@ void DiffPresetDialog::update_compatibility(const std::string& preset_name, Pres bool DiffPresetDialog::save() { - presets_to_saves.clear(); + presets_to_save.clear(); std::vector types_for_save; @@ -2018,7 +2020,7 @@ bool DiffPresetDialog::save() std::initializer_list{Preset::TYPE_PRINTER, Preset::TYPE_SLA_PRINT, Preset::TYPE_SLA_MATERIAL }) if (!m_tree->options(type, true).empty()) { types_for_save.emplace_back(type); - presets_to_saves.emplace_back(PresetToSave{ type, get_left_preset_name(type), get_right_preset_name(type), get_right_preset_name(type) }); + presets_to_save.emplace_back(PresetToSave{ type, get_left_preset_name(type), get_right_preset_name(type), get_right_preset_name(type) }); } if (!types_for_save.empty()) { @@ -2026,7 +2028,7 @@ bool DiffPresetDialog::save() if (save_dlg.ShowModal() != wxID_OK) return false; - for (auto& preset : presets_to_saves) { + for (auto& preset : presets_to_save) { const std::string& name = save_dlg.get_name(preset.type); if (!name.empty()) preset.new_name = name; @@ -2035,25 +2037,48 @@ bool DiffPresetDialog::save() return true; } +std::vector DiffPresetDialog::get_options_to_save(Preset::Type type) +{ + auto options = m_tree->options(type, true); + + // erase "inherits" option from the list if it exists there + if (const auto it = std::find(options.begin(), options.end(), "inherits"); it != options.end()) + options.erase(it); + + if (type == Preset::TYPE_PRINTER) { + // erase "extruders_count" option from the list if it exists there + if (const auto it = std::find(options.begin(), options.end(), "extruders_count"); it != options.end()) + options.erase(it); + } + return options; +} + void DiffPresetDialog::button_event(Action act) { if (act == Action::Save) { if (save()) { size_t saved_cnt = 0; - for (const auto& preset : presets_to_saves) - if (wxGetApp().preset_bundle->transfer_and_save(preset.type, preset.from_name, preset.to_name, preset.new_name, m_tree->options(preset.type, true))) + for (const auto& preset : presets_to_save) + if (wxGetApp().preset_bundle->transfer_and_save(preset.type, preset.from_name, preset.to_name, preset.new_name, get_options_to_save(preset.type))) saved_cnt++; - if (saved_cnt > 0) - MessageDialog(nullptr, UnsavedChangesDialog::msg_success_saved_modifications(saved_cnt)).ShowModal(); - - update_presets(); + if (saved_cnt > 0) { + MessageDialog(this, UnsavedChangesDialog::msg_success_saved_modifications(saved_cnt)).ShowModal(); + update_bundles_from_app(); + for (const auto& preset : presets_to_save) { + m_preset_bundle_left->get_presets(preset.type).select_preset_by_name(preset.from_name, true); + m_preset_bundle_right->get_presets(preset.type).select_preset_by_name(preset.new_name, true); + } + update_presets(m_view_type, false); + } } } else { Hide(); if (act == Action::Transfer) wxPostEvent(this, SimpleEvent(EVT_DIFF_DIALOG_TRANSFER)); + else if (!presets_to_save.empty()) + wxPostEvent(this, SimpleEvent(EVT_DIFF_DIALOG_UPDATE_PRESETS)); } } diff --git a/src/slic3r/GUI/UnsavedChangesDialog.hpp b/src/slic3r/GUI/UnsavedChangesDialog.hpp index a303593ca8..1592914fce 100644 --- a/src/slic3r/GUI/UnsavedChangesDialog.hpp +++ b/src/slic3r/GUI/UnsavedChangesDialog.hpp @@ -15,7 +15,8 @@ class wxStaticText; namespace Slic3r { namespace GUI{ -wxDECLARE_EVENT(EVT_DIFF_DIALOG_TRANSFER, SimpleEvent); +wxDECLARE_EVENT(EVT_DIFF_DIALOG_TRANSFER, SimpleEvent); +wxDECLARE_EVENT(EVT_DIFF_DIALOG_UPDATE_PRESETS, SimpleEvent); // ---------------------------------------------------------------------------- // ModelNode: a node inside DiffModel @@ -376,8 +377,9 @@ class DiffPresetDialog : public DPIDialog void update_controls_visibility(Preset::Type type = Preset::TYPE_INVALID); void update_compatibility(const std::string& preset_name, Preset::Type type, PresetBundle* preset_bundle); - void button_event(Action act); - bool save(); + std::vector get_options_to_save(Preset::Type type); + void button_event(Action act); + bool save(); struct DiffPresets { @@ -397,14 +399,14 @@ class DiffPresetDialog : public DPIDialog std::string new_name; }; - std::vector presets_to_saves; + std::vector presets_to_save; public: DiffPresetDialog(MainFrame*mainframe); ~DiffPresetDialog() override = default; void show(Preset::Type type = Preset::TYPE_INVALID); - void update_presets(Preset::Type type = Preset::TYPE_INVALID); + void update_presets(Preset::Type type = Preset::TYPE_INVALID, bool update_preset_bundles_from_app = true); Preset::Type view_type() const { return m_view_type; } PrinterTechnology printer_technology() const { return m_pr_technology; }