diff --git a/resources/data/sla_support.svg b/resources/data/sla_support.svg new file mode 100644 index 0000000000..d131315194 --- /dev/null +++ b/resources/data/sla_support.svg @@ -0,0 +1,361 @@ + + + + + + + + + + + + + + + + 201. - 250. layer + + + + + + + + + + island supports + For loading is used only first red curveUse only line segments (no curve)Y coordinate of points must only increase.First point.y must be zeroLast virtual point of curve is [last.x, ∞] + Supported radius[in mm] + + Headinterface + + + + z [in mm]Print Direction + + + Last curve pointdefine stability requirements + 6. - 10. layer + Guid for layers height 0.05mm + 16. - 20. layer + 26. - 30. layer + 36. - 40. layer + 46. - 50. layer + 101. - 150. layer + + + diff --git a/resources/icons/sphere_blueish.svg b/resources/icons/sphere_blueish.svg new file mode 100644 index 0000000000..39aba38de2 --- /dev/null +++ b/resources/icons/sphere_blueish.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + diff --git a/resources/icons/sphere_cyan.svg b/resources/icons/sphere_cyan.svg new file mode 100644 index 0000000000..5402fc301a --- /dev/null +++ b/resources/icons/sphere_cyan.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + diff --git a/resources/icons/sphere_lightgray.svg b/resources/icons/sphere_lightgray.svg new file mode 100644 index 0000000000..fcf383f4bf --- /dev/null +++ b/resources/icons/sphere_lightgray.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + diff --git a/resources/icons/sphere_orange.svg b/resources/icons/sphere_orange.svg new file mode 100644 index 0000000000..2a4382d3f6 --- /dev/null +++ b/resources/icons/sphere_orange.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + diff --git a/resources/icons/sphere_redish.svg b/resources/icons/sphere_redish.svg new file mode 100644 index 0000000000..83a26f4a14 --- /dev/null +++ b/resources/icons/sphere_redish.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + diff --git a/resources/icons/support_structure.svg b/resources/icons/support_structure.svg new file mode 100644 index 0000000000..efa7d455cd --- /dev/null +++ b/resources/icons/support_structure.svg @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/icons/support_structure_invisible.svg b/resources/icons/support_structure_invisible.svg new file mode 100644 index 0000000000..585db7c0ac --- /dev/null +++ b/resources/icons/support_structure_invisible.svg @@ -0,0 +1,228 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index d8c8d1e069..f7773673ae 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -452,7 +452,37 @@ set(SLIC3R_SOURCES SLA/BranchingTreeSLA.hpp SLA/BranchingTreeSLA.cpp SLA/ZCorrection.hpp - SLA/ZCorrection.cpp + SLA/ZCorrection.cpp + SLA/SupportIslands/EvaluateNeighbor.cpp + SLA/SupportIslands/EvaluateNeighbor.hpp + SLA/SupportIslands/ExpandNeighbor.cpp + SLA/SupportIslands/ExpandNeighbor.hpp + SLA/SupportIslands/IStackFunction.hpp + SLA/SupportIslands/LineUtils.cpp + SLA/SupportIslands/LineUtils.hpp + SLA/SupportIslands/NodeDataWithResult.hpp + SLA/SupportIslands/Parabola.hpp + SLA/SupportIslands/ParabolaUtils.cpp + SLA/SupportIslands/ParabolaUtils.hpp + SLA/SupportIslands/PointUtils.cpp + SLA/SupportIslands/PointUtils.hpp + SLA/SupportIslands/PolygonUtils.cpp + SLA/SupportIslands/PolygonUtils.hpp + SLA/SupportIslands/PostProcessNeighbor.cpp + SLA/SupportIslands/PostProcessNeighbor.hpp + SLA/SupportIslands/PostProcessNeighbors.cpp + SLA/SupportIslands/PostProcessNeighbors.hpp + SLA/SupportIslands/SampleConfig.hpp + SLA/SupportIslands/SampleConfigFactory.cpp + SLA/SupportIslands/SampleConfigFactory.hpp + SLA/SupportIslands/SupportIslandPoint.cpp + SLA/SupportIslands/SupportIslandPoint.hpp + SLA/SupportIslands/UniformSupportIsland.cpp + SLA/SupportIslands/UniformSupportIsland.hpp + SLA/SupportIslands/VectorUtils.hpp + SLA/SupportIslands/VoronoiGraph.hpp + SLA/SupportIslands/VoronoiGraphUtils.cpp + SLA/SupportIslands/VoronoiGraphUtils.hpp BranchingTree/BranchingTree.cpp BranchingTree/BranchingTree.hpp BranchingTree/PointCloud.cpp @@ -524,6 +554,9 @@ foreach(_source IN ITEMS ${SLIC3R_SOURCES}) source_group("${_group_path}" FILES "${_source}") endforeach() +# Create the source groups for source tree with root at CMAKE_CURRENT_SOURCE_DIR. +source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${SOURCE_LIST_SLA}) + if (SLIC3R_STATIC) set(CGAL_Boost_USE_STATIC_LIBS ON CACHE BOOL "" FORCE) endif () @@ -541,6 +574,7 @@ add_library(libslic3r_cgal STATIC MeshBoolean.hpp MeshBoolean.cpp TryCatchSignal.hpp TryCatchSignal.cpp Triangulation.hpp Triangulation.cpp + SLA/SupportIslands/VoronoiDiagramCGAL.hpp SLA/SupportIslands/VoronoiDiagramCGAL.cpp ) target_include_directories(libslic3r_cgal PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) target_include_directories(libslic3r_cgal PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/..) diff --git a/src/libslic3r/ClipperUtils.cpp b/src/libslic3r/ClipperUtils.cpp index a5ff5a2591..cc2b49be2f 100644 --- a/src/libslic3r/ClipperUtils.cpp +++ b/src/libslic3r/ClipperUtils.cpp @@ -726,6 +726,8 @@ Slic3r::Polygons diff(const Slic3r::Surfaces &subject, const Slic3r::Polygons &c { return _clipper(ClipperLib::ctDifference, ClipperUtils::SurfacesProvider(subject), ClipperUtils::PolygonsProvider(clip), do_safety_offset); } Slic3r::Polygons intersection(const Slic3r::Polygon &subject, const Slic3r::Polygon &clip, ApplySafetyOffset do_safety_offset) { return _clipper(ClipperLib::ctIntersection, ClipperUtils::SinglePathProvider(subject.points), ClipperUtils::SinglePathProvider(clip.points), do_safety_offset); } +Slic3r::Polygons intersection(const Slic3r::Polygon &subject, const Slic3r::ExPolygon &clip, ApplySafetyOffset do_safety_offset) + { return _clipper(ClipperLib::ctIntersection, ClipperUtils::SinglePathProvider(subject.points), ClipperUtils::ExPolygonProvider(clip), do_safety_offset); } Slic3r::Polygons intersection_clipped(const Slic3r::Polygons &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset) { return intersection(subject, ClipperUtils::clip_clipper_polygons_with_subject_bbox(clip, get_extents(subject).inflated(SCALED_EPSILON)), do_safety_offset); } Slic3r::Polygons intersection(const Slic3r::Polygons &subject, const Slic3r::ExPolygon &clip, ApplySafetyOffset do_safety_offset) @@ -778,6 +780,8 @@ Slic3r::ExPolygons diff_ex(const Slic3r::ExPolygon &subject, const Slic3r::Polyg { return _clipper_ex(ClipperLib::ctDifference, ClipperUtils::ExPolygonProvider(subject), ClipperUtils::SinglePathProvider(clip.points), do_safety_offset); } Slic3r::ExPolygons diff_ex(const Slic3r::ExPolygon &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset) { return _clipper_ex(ClipperLib::ctDifference, ClipperUtils::ExPolygonProvider(subject), ClipperUtils::PolygonsProvider(clip), do_safety_offset); } +Slic3r::ExPolygons diff_ex(const Slic3r::ExPolygon &subject, const Slic3r::ExPolygons &clip, ApplySafetyOffset do_safety_offset) + { return _clipper_ex(ClipperLib::ctDifference, ClipperUtils::ExPolygonProvider(subject), ClipperUtils::ExPolygonsProvider(clip), do_safety_offset); } Slic3r::ExPolygons diff_ex(const Slic3r::ExPolygons &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset) { return _clipper_ex(ClipperLib::ctDifference, ClipperUtils::ExPolygonsProvider(subject), ClipperUtils::PolygonsProvider(clip), do_safety_offset); } Slic3r::ExPolygons diff_ex(const Slic3r::ExPolygons &subject, const Slic3r::ExPolygons &clip, ApplySafetyOffset do_safety_offset) @@ -1037,8 +1041,7 @@ Polygons union_parallel_reduce(const Polygons &subject) }); } -Polygons simplify_polygons(const Polygons &subject) -{ +Polygons simplify_polygons(const Polygons &subject) { CLIPPER_UTILS_TIME_LIMIT_MILLIS(CLIPPER_UTILS_TIME_LIMIT_DEFAULT); ClipperLib::Paths output; @@ -1048,27 +1051,9 @@ Polygons simplify_polygons(const Polygons &subject) c.StrictlySimple(true); c.AddPaths(ClipperUtils::PolygonsProvider(subject), ClipperLib::ptSubject, true); c.Execute(ClipperLib::ctUnion, output, ClipperLib::pftNonZero, ClipperLib::pftNonZero); - - // convert into Slic3r polygons return to_polygons(std::move(output)); } -ExPolygons simplify_polygons_ex(const Polygons &subject, bool preserve_collinear) -{ - CLIPPER_UTILS_TIME_LIMIT_MILLIS(CLIPPER_UTILS_TIME_LIMIT_DEFAULT); - - ClipperLib::PolyTree polytree; - ClipperLib::Clipper c; -// c.PreserveCollinear(true); - //FIXME StrictlySimple is very expensive! Is it needed? - c.StrictlySimple(true); - c.AddPaths(ClipperUtils::PolygonsProvider(subject), ClipperLib::ptSubject, true); - c.Execute(ClipperLib::ctUnion, polytree, ClipperLib::pftNonZero, ClipperLib::pftNonZero); - - // convert into ExPolygons - return PolyTreeToExPolygons(std::move(polytree)); -} - Polygons top_level_islands(const Slic3r::Polygons &polygons) { CLIPPER_UTILS_TIME_LIMIT_MILLIS(CLIPPER_UTILS_TIME_LIMIT_DEFAULT); diff --git a/src/libslic3r/ClipperUtils.hpp b/src/libslic3r/ClipperUtils.hpp index d38bd2aa83..2b6fad550b 100644 --- a/src/libslic3r/ClipperUtils.hpp +++ b/src/libslic3r/ClipperUtils.hpp @@ -454,6 +454,7 @@ Slic3r::ExPolygons diff_ex(const Slic3r::Polygons &subject, const Slic3r::Surfac Slic3r::ExPolygons diff_ex(const Slic3r::Polygon &subject, const Slic3r::ExPolygons &clip, ApplySafetyOffset do_safety_offset = ApplySafetyOffset::No); Slic3r::ExPolygons diff_ex(const Slic3r::ExPolygon &subject, const Slic3r::Polygon &clip, ApplySafetyOffset do_safety_offset = ApplySafetyOffset::No); Slic3r::ExPolygons diff_ex(const Slic3r::ExPolygon &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset = ApplySafetyOffset::No); +Slic3r::ExPolygons diff_ex(const Slic3r::ExPolygon &subject, const Slic3r::ExPolygons &clip, ApplySafetyOffset do_safety_offset = ApplySafetyOffset::No); Slic3r::ExPolygons diff_ex(const Slic3r::ExPolygons &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset = ApplySafetyOffset::No); Slic3r::ExPolygons diff_ex(const Slic3r::ExPolygons &subject, const Slic3r::ExPolygons &clip, ApplySafetyOffset do_safety_offset = ApplySafetyOffset::No); Slic3r::ExPolygons diff_ex(const Slic3r::Surfaces &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset = ApplySafetyOffset::No); @@ -477,6 +478,7 @@ inline Slic3r::Lines diff_ln(const Slic3r::Lines &subject, const Slic3r::Polygon // Safety offset is applied to the clipping polygons only. Slic3r::Polygons intersection(const Slic3r::Polygon &subject, const Slic3r::Polygon &clip, ApplySafetyOffset do_safety_offset = ApplySafetyOffset::No); +Slic3r::Polygons intersection(const Slic3r::Polygon &subject, const Slic3r::ExPolygon &clip, ApplySafetyOffset do_safety_offset = ApplySafetyOffset::No); Slic3r::Polygons intersection(const Slic3r::Polygons &subject, const Slic3r::ExPolygon &clip, ApplySafetyOffset do_safety_offset = ApplySafetyOffset::No); Slic3r::Polygons intersection(const Slic3r::Polygons &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset = ApplySafetyOffset::No); // Optimized version clipping the "clipping" polygon using clip_clipper_polygon_with_subject_bbox(). @@ -639,7 +641,6 @@ void traverse_pt(const ClipperLib::PolyNodes &nodes, ExOrJustPolygons *retval) /* OTHER */ Slic3r::Polygons simplify_polygons(const Slic3r::Polygons &subject); -Slic3r::ExPolygons simplify_polygons_ex(const Slic3r::Polygons &subject); Polygons top_level_islands(const Slic3r::Polygons &polygons); diff --git a/src/libslic3r/Format/3mf.cpp b/src/libslic3r/Format/3mf.cpp index 5c608067cb..e8fde1fbb1 100644 --- a/src/libslic3r/Format/3mf.cpp +++ b/src/libslic3r/Format/3mf.cpp @@ -1419,21 +1419,31 @@ namespace Slic3r { std::vector sla_support_points; if (version == 0) { + assert(object_data_points.size() % 3 == 0); for (unsigned int i=0; isla::SupportPointType{ + return (std::abs(val - 1.) < EPSILON) ? sla::SupportPointType::island : + (std::abs(val - 2.) < EPSILON) ? sla::SupportPointType::manual_add : + //(std::abs(val - 3.) < EPSILON) ? sla::SupportPointType::slope : + sla::SupportPointType::slope; // default for previous version of store points + }; + assert(object_data_points.size() % 5 == 0); for (unsigned int i=0; i float { + switch (t) { + case Slic3r::sla::SupportPointType::manual_add: return 2.f; + case Slic3r::sla::SupportPointType::island: return 1.f; + case Slic3r::sla::SupportPointType::slope: return 3.f; + default: assert(false); return 0.f; + } + }; // Store the layer height profile as a single space separated list. for (size_t i = 0; i < sla_support_points.size(); ++i) { - sprintf(buffer, (i==0 ? "%f %f %f %f %f" : " %f %f %f %f %f"), sla_support_points[i].pos(0), sla_support_points[i].pos(1), sla_support_points[i].pos(2), sla_support_points[i].head_front_radius, (float)sla_support_points[i].is_new_island); + sprintf(buffer, (i==0 ? "%f %f %f %f %f" : " %f %f %f %f %f"), + sla_support_points[i].pos(0), + sla_support_points[i].pos(1), + sla_support_points[i].pos(2), + sla_support_points[i].head_front_radius, + support_point_type_to_float(sla_support_points[i].type)); out += buffer; } out += "\n"; diff --git a/src/libslic3r/Format/3mf.hpp b/src/libslic3r/Format/3mf.hpp index 78f95b7db0..b2e3746fd2 100644 --- a/src/libslic3r/Format/3mf.hpp +++ b/src/libslic3r/Format/3mf.hpp @@ -19,7 +19,18 @@ namespace Slic3r { * version 1 : ThreeMF_support_points_version=1 object_id=1|-12.055421 -2.658771 10.000000 0.4 0.0 object_id=2|-14.051745 -3.570338 5.000000 0.6 1.0 - // introduced header with version number; x,y,z,head_size,is_new_island) + // introduced header with version number; x,y,z,head_size,type) + // before 2.9.1 fifth float means is_island (bool flag) -> value from 0.9999f to 1.0001f means it is support for island otherwise not. User edited points has always value zero. + // since 2.9.1 fifth float means type -> starts show user edited points + // type range value meaning + // (float is used only for compatibility, string will be better) + // from | to | meaning + // -------------------------------- + // 0.9999f | 1.0001 | island (no change) + // 1.9999f | 2.0001 | manual edited points loose info about island + // 2.9999f | 3.0001 | generated point by slope ration + // all other values are readed also as slope type + */ enum { diff --git a/src/libslic3r/Format/AMF.cpp b/src/libslic3r/Format/AMF.cpp index 797d083caa..f595e22736 100644 --- a/src/libslic3r/Format/AMF.cpp +++ b/src/libslic3r/Format/AMF.cpp @@ -774,7 +774,7 @@ void AMFParserContext::endElement(const char * /* name */) point(coord_idx) = float(atof(p)); if (++coord_idx == 5) { - m_object->sla_support_points.push_back(sla::SupportPoint(point)); + m_object->sla_support_points.push_back(sla::SupportPoint{Vec3f(point[0], point[1], point[2]), point[3]}); coord_idx = 0; } if (end == nullptr) diff --git a/src/libslic3r/Geometry/Circle.hpp b/src/libslic3r/Geometry/Circle.hpp index 94f756133a..6bce435bb9 100644 --- a/src/libslic3r/Geometry/Circle.hpp +++ b/src/libslic3r/Geometry/Circle.hpp @@ -239,7 +239,7 @@ int ray_circle_intersections(T r, T a, T b, T c, std::pair &get_nodes() const { return m_nodes; } + // NOTE: Copy constructor cause failing FDM tests but not each run only from time to time. + // KDTreeIndirect(const KDTreeIndirect &rhs) : m_nodes(rhs.m_nodes), coordinate(rhs.coordinate) {} + KDTreeIndirect get_copy() const { KDTreeIndirect copy(coordinate); copy.m_nodes = m_nodes; return copy; } void build(size_t num_indices) { std::vector indices; @@ -63,10 +66,10 @@ public: } template - unsigned int descent_mask(const CoordType &point_coord, const CoordType &search_radius, size_t idx, size_t dimension) const + unsigned int descent_mask(const CoordType &point_coord, const double &search_radius, size_t idx, size_t dimension) const { CoordType dist = point_coord - this->coordinate(idx, dimension); - return (dist * dist < search_radius + CoordType(EPSILON)) ? + return (double(dist) * dist < search_radius + EPSILON) ? // The plane intersects a hypersphere centered at point_coord of search_radius. ((unsigned int)(VisitorReturnMask::CONTINUE_LEFT) | (unsigned int)(VisitorReturnMask::CONTINUE_RIGHT)) : // The plane does not intersect the hypersphere. @@ -213,44 +216,43 @@ std::array find_closest_points( const Tree &kdtree; const PointType &point; const FilterFn filter; - - std::array, K> results; + struct Result { + size_t index; + double distance_sq; + }; + std::array results; Visitor(const Tree &kdtree, const PointType &point, FilterFn filter) : kdtree(kdtree), point(point), filter(filter) { - results.fill(std::make_pair(Tree::npos, - std::numeric_limits::max())); + results.fill(Result{Tree::npos, std::numeric_limits::max()}); } unsigned int operator()(size_t idx, size_t dimension) { if (this->filter(idx)) { - auto dist = CoordT(0); + double distance_sq = 0.; for (size_t i = 0; i < D; ++i) { CoordT d = point[i] - kdtree.coordinate(idx, i); - dist += d * d; + distance_sq += double(d) * d; } - auto res = std::make_pair(idx, dist); - auto it = std::lower_bound(results.begin(), results.end(), - res, [](auto &r1, auto &r2) { - return r1.second < r2.second; - }); - + Result res{idx, distance_sq}; + auto lower_distance = [](const Result &r1, const Result &r2) { + return r1.distance_sq < r2.distance_sq; }; + auto it = std::lower_bound(results.begin(), results.end(), res, lower_distance); if (it != results.end()) { std::rotate(it, std::prev(results.end()), results.end()); *it = res; } } - return kdtree.descent_mask(point[dimension], - results.front().second, idx, - dimension); + return kdtree.descent_mask(point[dimension], results.front().distance_sq, idx, dimension); } } visitor(kdtree, point, filter); kdtree.visit(visitor); std::array ret; - for (size_t i = 0; i < K; i++) ret[i] = visitor.results[i].first; + for (size_t i = 0; i < K; i++) + ret[i] = visitor.results[i].index; return ret; } @@ -290,20 +292,20 @@ std::vector find_nearby_points(const KDTreeIndirectType &kdtree, const P struct Visitor { const KDTreeIndirectType &kdtree; const PointType center; - const CoordType max_distance_squared; + const double max_distance_squared; const FilterFn filter; std::vector result; Visitor(const KDTreeIndirectType &kdtree, const PointType& center, const CoordType &max_distance, FilterFn filter) : - kdtree(kdtree), center(center), max_distance_squared(max_distance*max_distance), filter(filter) { + kdtree(kdtree), center(center), max_distance_squared(double(max_distance)*max_distance), filter(filter) { } unsigned int operator()(size_t idx, size_t dimension) { if (this->filter(idx)) { - auto dist = CoordType(0); + double dist = 0.; for (size_t i = 0; i < KDTreeIndirectType::NumDimensions; ++i) { CoordType d = center[i] - kdtree.coordinate(idx, i); - dist += d * d; + dist += double(d) * d; } if (dist < max_distance_squared) { result.push_back(idx); diff --git a/src/libslic3r/Line.cpp b/src/libslic3r/Line.cpp index 535fb9b1bc..74f4879c8b 100644 --- a/src/libslic3r/Line.cpp +++ b/src/libslic3r/Line.cpp @@ -64,6 +64,18 @@ double Line::perp_distance_to(const Point &point) const return std::abs(cross2(v, va)) / v.norm(); } +double Line::perp_signed_distance_to(const Point &point) const { + // Sign is dependent on the line orientation. + // For CCW oriented polygon is possitive distace into shape and negative outside. + // For Line({0,0},{0,2}) and point {1,1} the distance is negative one(-1). + const Line &line = *this; + const Vec2d v = (line.b - line.a).cast(); + const Vec2d va = (point - line.a).cast(); + if (line.a == line.b) + return va.norm(); + return cross2(v, va) / v.norm(); +} + double Line::orientation() const { double angle = this->atan2_(); diff --git a/src/libslic3r/Line.hpp b/src/libslic3r/Line.hpp index a32d6eac75..14b77c44d9 100644 --- a/src/libslic3r/Line.hpp +++ b/src/libslic3r/Line.hpp @@ -216,6 +216,7 @@ public: double distance_to(const Point &point) const { return distance_to(point, this->a, this->b); } double distance_to_infinite_squared(const Point &point, Point *closest_point) const { return line_alg::distance_to_infinite_squared(*this, point, closest_point); } double perp_distance_to(const Point &point) const; + double perp_signed_distance_to(const Point &point) const; bool parallel_to(double angle) const; bool parallel_to(const Line& line) const; bool perpendicular_to(double angle) const; diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 375da5550a..a34edb5206 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -604,7 +604,6 @@ static std::vector s_Preset_sla_print_options { "branchingsupport_object_elevation", "support_points_density_relative", - "support_points_minimal_distance", "slice_closing_radius", "slicing_mode", "pad_enable", diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 2f6836986c..c93eadfadd 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -4564,14 +4564,6 @@ void PrintConfigDef::init_sla_params() def->min = 0; def->set_default_value(new ConfigOptionInt(100)); - def = this->add("support_points_minimal_distance", coFloat); - def->label = L("Minimal distance of the support points"); - def->category = L("Supports"); - def->tooltip = L("No support points will be placed closer than this threshold."); - def->sidetext = L("mm"); - def->min = 0; - def->set_default_value(new ConfigOptionFloat(1.)); - def = this->add("pad_enable", coBool); def->label = L("Use pad"); def->category = L("Pad"); @@ -4994,7 +4986,8 @@ static std::set PrintConfigDef_ignore = { "infill_only_where_needed", "gcode_binary", // Introduced in 2.7.0-alpha1, removed in 2.7.1 (replaced by binary_gcode). "wiping_volumes_extruders", // Removed in 2.7.3-alpha1. - "wipe_tower_x", "wipe_tower_y", "wipe_tower_rotation_angle" // Removed in 2.9.0 + "wipe_tower_x", "wipe_tower_y", "wipe_tower_rotation_angle", // Removed in 2.9.0 + "support_points_minimal_distance", // End of the using in 2.9.1 (change algorithm for the support generator) }; void PrintConfigDef::handle_legacy(t_config_option_key &opt_key, std::string &value) diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 3106819feb..11f8df180b 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -1143,11 +1143,8 @@ PRINT_CONFIG_CLASS_DEFINE( // and the model object's bounding box bottom. Units in mm. ((ConfigOptionFloat, branchingsupport_object_elevation))/*= 5.0*/ - - /////// Following options influence automatic support points placement: ((ConfigOptionInt, support_points_density_relative)) - ((ConfigOptionFloat, support_points_minimal_distance)) // Now for the base pool (pad) ///////////////////////////////////////////// diff --git a/src/libslic3r/SLA/SupportIslands/EvaluateNeighbor.cpp b/src/libslic3r/SLA/SupportIslands/EvaluateNeighbor.cpp new file mode 100644 index 0000000000..d6f9fcee2b --- /dev/null +++ b/src/libslic3r/SLA/SupportIslands/EvaluateNeighbor.cpp @@ -0,0 +1,23 @@ +#include "EvaluateNeighbor.hpp" +#include "ExpandNeighbor.hpp" + +using namespace Slic3r::sla; + +EvaluateNeighbor::EvaluateNeighbor(VoronoiGraph::ExPath & result, + const VoronoiGraph::Node *node, + double distance_to_node, + const VoronoiGraph::Path &prev_path) + : post_process_neighbor( + std::make_unique(result, + node, + distance_to_node, + prev_path)) +{} + +void EvaluateNeighbor::process(CallStack &call_stack) +{ + NodeDataWithResult &data = *post_process_neighbor; + call_stack.emplace(std::move(post_process_neighbor)); + for (const VoronoiGraph::Node::Neighbor &neighbor : data.node->neighbors) + call_stack.emplace(std::make_unique(data, neighbor)); +} \ No newline at end of file diff --git a/src/libslic3r/SLA/SupportIslands/EvaluateNeighbor.hpp b/src/libslic3r/SLA/SupportIslands/EvaluateNeighbor.hpp new file mode 100644 index 0000000000..b233ccb612 --- /dev/null +++ b/src/libslic3r/SLA/SupportIslands/EvaluateNeighbor.hpp @@ -0,0 +1,36 @@ +#ifndef slic3r_SLA_SuppotstIslands_EvaluateNeighbor_hpp_ +#define slic3r_SLA_SuppotstIslands_EvaluateNeighbor_hpp_ + +#include + +#include "IStackFunction.hpp" +#include "PostProcessNeighbors.hpp" +#include "VoronoiGraph.hpp" + +namespace Slic3r::sla { + +/// +/// create on stack +/// 1 * PostProcessNeighbors +/// N * ExpandNode +/// +class EvaluateNeighbor : public IStackFunction +{ + std::unique_ptr post_process_neighbor; +public: + EvaluateNeighbor( + VoronoiGraph::ExPath & result, + const VoronoiGraph::Node *node, + double distance_to_node = 0., + const VoronoiGraph::Path &prev_path = VoronoiGraph::Path({}, 0.)); + + /// + /// create on stack + /// 1 * PostProcessNeighbors + /// N * ExpandNode + /// + virtual void process(CallStack &call_stack); +}; + +} // namespace Slic3r::sla +#endif // slic3r_SLA_SuppotstIslands_EvaluateNeighbor_hpp_ diff --git a/src/libslic3r/SLA/SupportIslands/ExpandNeighbor.cpp b/src/libslic3r/SLA/SupportIslands/ExpandNeighbor.cpp new file mode 100644 index 0000000000..260ab27436 --- /dev/null +++ b/src/libslic3r/SLA/SupportIslands/ExpandNeighbor.cpp @@ -0,0 +1,44 @@ +#include "ExpandNeighbor.hpp" +#include "VoronoiGraphUtils.hpp" + +using namespace Slic3r::sla; + +ExpandNeighbor::ExpandNeighbor( + NodeDataWithResult & data, + const VoronoiGraph::Node::Neighbor &neighbor) + : data(data) + , neighbor(neighbor) +{} + +void ExpandNeighbor::process(CallStack &call_stack) +{ + if (data.skip_nodes.find(neighbor.node) != data.skip_nodes.end()) return; + + // detection of circle + auto circle_opt = VoronoiGraphUtils::create_circle(data.act_path, + neighbor); + if (circle_opt.has_value()) { + size_t circle_index = data.result.circles.size(); + data.circle_indexes.push_back(circle_index); + data.result.circles.push_back(*circle_opt); + return; + } + + // create copy of path(not circles, not side_branches) + const VoronoiGraph::Node &next_node = *neighbor.node; + // is next node leaf ? + if (next_node.neighbors.size() == 1) { + VoronoiGraph::Path side_branch({&next_node}, neighbor.length()); + data.side_branches.push(std::move(side_branch)); + return; + } + + auto post_process_neighbor = std::make_unique(data); + VoronoiGraph::ExPath &neighbor_path = post_process_neighbor->neighbor_path; + + call_stack.emplace(std::move(post_process_neighbor)); + call_stack.emplace( + std::make_unique(neighbor_path, neighbor.node, + neighbor.length(), + data.act_path)); +} \ No newline at end of file diff --git a/src/libslic3r/SLA/SupportIslands/ExpandNeighbor.hpp b/src/libslic3r/SLA/SupportIslands/ExpandNeighbor.hpp new file mode 100644 index 0000000000..061963c46f --- /dev/null +++ b/src/libslic3r/SLA/SupportIslands/ExpandNeighbor.hpp @@ -0,0 +1,35 @@ +#ifndef slic3r_SLA_SuppotstIslands_ExpandNeighbor_hpp_ +#define slic3r_SLA_SuppotstIslands_ExpandNeighbor_hpp_ + +#include "IStackFunction.hpp" +#include "VoronoiGraph.hpp" +#include "PostProcessNeighbor.hpp" +#include "EvaluateNeighbor.hpp" + +namespace Slic3r::sla { + +/// +/// Expand neighbor to +/// - PostProcessNeighbor +/// - EvaluateNeighbor +/// +class ExpandNeighbor : public IStackFunction +{ + NodeDataWithResult & data; + const VoronoiGraph::Node::Neighbor &neighbor; + +public: + ExpandNeighbor(NodeDataWithResult & data, + const VoronoiGraph::Node::Neighbor &neighbor); + + /// + /// Expand neighbor to + /// - PostProcessNeighbor + /// - EvaluateNeighbor + /// + /// Output callStack + virtual void process(CallStack &call_stack); +}; + +} // namespace Slic3r::sla +#endif // slic3r_SLA_SuppotstIslands_ExpandNeighbor_hpp_ diff --git a/src/libslic3r/SLA/SupportIslands/IStackFunction.hpp b/src/libslic3r/SLA/SupportIslands/IStackFunction.hpp new file mode 100644 index 0000000000..6763116a70 --- /dev/null +++ b/src/libslic3r/SLA/SupportIslands/IStackFunction.hpp @@ -0,0 +1,23 @@ +#ifndef slic3r_SLA_SuppotstIslands_IStackFunction_hpp_ +#define slic3r_SLA_SuppotstIslands_IStackFunction_hpp_ + +#include +#include + +namespace Slic3r::sla { + +/// +/// Interface for objects inside of CallStack. +/// It is way to prevent stack overflow inside recurrent functions. +/// +class IStackFunction +{ +public: + virtual ~IStackFunction() = default; + virtual void process(std::stack> &call_stack) = 0; +}; + +using CallStack = std::stack>; + +} // namespace Slic3r::sla +#endif // slic3r_SLA_SuppotstIslands_IStackFunction_hpp_ diff --git a/src/libslic3r/SLA/SupportIslands/LineUtils.cpp b/src/libslic3r/SLA/SupportIslands/LineUtils.cpp new file mode 100644 index 0000000000..f8f3236273 --- /dev/null +++ b/src/libslic3r/SLA/SupportIslands/LineUtils.cpp @@ -0,0 +1,458 @@ +#include "LineUtils.hpp" +#include +#include +#include +#include "VectorUtils.hpp" +#include "PointUtils.hpp" + +using namespace Slic3r::sla; + +// sort counter clock wise lines +void LineUtils::sort_CCW(Lines &lines, const Point& center) +{ + std::function calc = [¢er](const Line &line) { + Point p = line.a - center; + return std::atan2(p.y(), p.x()); + }; + VectorUtils::sort_by(lines, calc); +} + +bool LineUtils::is_parallel_y(const Line &line) { + coord_t x_change = line.a.x() - line.b.x(); + return (x_change == 0); +} +bool LineUtils::is_parallel_y(const Linef &line) +{ + double x_change = line.a.x() - line.b.x(); + return (fabs(x_change) < std::numeric_limits::epsilon()); +} + +std::optional LineUtils::crop_ray(const Line & ray, + const Point ¢er, + double radius) +{ + if (is_parallel_y(ray)) { + coord_t x = ray.a.x(); + coord_t diff = x - center.x(); + coord_t abs_diff = abs(diff); + if (abs_diff > radius) return {}; + // create cross points + double move_y = sqrt(radius * radius - static_cast(x) * x); + coord_t y = static_cast(std::round(move_y)); + coord_t cy = center.y(); + Point first(x, cy + y); + Point second(x,cy - y); + return Line(first, second); + } else { + Line moved_line(ray.a - center, ray.b - center); + double a, b, c; + std::tie(a, b, c) = get_param(moved_line); + std::pair points; + int count = Slic3r::Geometry::ray_circle_intersections( + radius, a, b, c, points); + if (count != 2) return {}; + return Line(points.first.cast() + center, + points.second.cast() + center); + } +} +std::optional LineUtils::crop_ray(const Linef &ray, + const Point ¢er, + double radius) +{ + Vec2d center_d = center.cast(); + if (is_parallel_y(ray)) { + double x = ray.a.x(); + double diff = x - center_d.x(); + double abs_diff = fabs(diff); + if (abs_diff > radius) return {}; + // create cross points + double y = sqrt(radius * radius - x * x); + Vec2d first(x, y); + Vec2d second(x, -y); + return Linef(first + center_d, + second + center_d); + } else { + Linef moved_line(ray.a - center_d, ray.b - center_d); + double a, b, c; + std::tie(a, b, c) = get_param(moved_line); + std::pair points; + int count = Slic3r::Geometry::ray_circle_intersections(radius, a, b, + c, points); + if (count != 2) return {}; + return Linef(points.first + center_d, points.second + center_d); + } +} + +std::optional LineUtils::crop_half_ray(const Line & half_ray, + const Point ¢er, + double radius) +{ + std::optional segment = crop_ray(half_ray, center, radius); + if (!segment.has_value()) return {}; + Point dir = LineUtils::direction(half_ray); + using fnc = std::function; + fnc use_point_x = [&half_ray, &dir](const Point &p) -> bool { + return (p.x() > half_ray.a.x()) == (dir.x() > 0); + }; + fnc use_point_y = [&half_ray, &dir](const Point &p) -> bool { + return (p.y() > half_ray.a.y()) == (dir.y() > 0); + }; + bool use_x = PointUtils::is_majorit_x(dir); + fnc use_point = (use_x) ? use_point_x : use_point_y; + bool use_a = use_point(segment->a); + bool use_b = use_point(segment->b); + if (!use_a && !use_b) return {}; + if (use_a && use_b) return segment; + return Line(half_ray.a, (use_a)?segment->a : segment->b); +} + +std::optional LineUtils::crop_half_ray(const Linef & half_ray, + const Point ¢er, + double radius) +{ + std::optional segment = crop_ray(half_ray, center, radius); + if (!segment.has_value()) return {}; + Vec2d dir = half_ray.b - half_ray.a; + using fnc = std::function; + fnc use_point_x = [&half_ray, &dir](const Vec2d &p) -> bool { + return (p.x() > half_ray.a.x()) == (dir.x() > 0); + }; + fnc use_point_y = [&half_ray, &dir](const Vec2d &p) -> bool { + return (p.y() > half_ray.a.y()) == (dir.y() > 0); + }; + bool use_x = PointUtils::is_majorit_x(dir); + fnc use_point = (use_x) ? use_point_x : use_point_y; + bool use_a = use_point(segment->a); + bool use_b = use_point(segment->b); + if (!use_a && !use_b) return {}; + if (use_a && use_b) return segment; + return Linef(half_ray.a, (use_a) ? segment->a : segment->b); +} + +std::optional LineUtils::crop_line(const Line & line, + const Point ¢er, + double radius) +{ + std::optional segment = crop_ray(line, center, radius); + if (!segment.has_value()) return {}; + + Point dir = line.b - line.a; + using fnc = std::function; + fnc use_point_x = [&line, &dir](const Point &p) -> bool { + return (dir.x() > 0) ? (p.x() > line.a.x()) && (p.x() < line.b.x()) : + (p.x() < line.a.x()) && (p.x() > line.b.x()); + }; + fnc use_point_y = [&line, &dir](const Point &p) -> bool { + return (dir.y() > 0) ? (p.y() > line.a.y()) && (p.y() < line.b.y()) : + (p.y() < line.a.y()) && (p.y() > line.b.y()); + }; + bool use_x = PointUtils::is_majorit_x(dir); + fnc use_point = (use_x) ? use_point_x : use_point_y; + bool use_a = use_point(segment->a); + bool use_b = use_point(segment->b); + if (!use_a && !use_b) return {}; + if (use_a && use_b) return segment; + bool same_dir = (use_x) ? + ((dir.x() > 0) == ((segment->b.x() - segment->a.x()) > 0)) : + ((dir.y() > 0) == ((segment->b.y() - segment->a.y()) > 0)) ; + if (use_a) { + if (same_dir) + return Line(segment->a, line.b); + else + return Line(line.a, segment->a); + } else { // use b + if (same_dir) + return Line(line.a, segment->b); + else + return Line(segment->b, line.b); + } +} + +std::optional LineUtils::crop_line(const Linef & line, + const Point ¢er, + double radius) +{ + std::optional segment = crop_ray(line, center, radius); + if (!segment.has_value()) return {}; + + Vec2d dir = line.b - line.a; + using fnc = std::function; + fnc use_point_x = [&line, &dir](const Vec2d &p) -> bool { + return (dir.x() > 0) ? (p.x() > line.a.x()) && (p.x() < line.b.x()) : + (p.x() < line.a.x()) && (p.x() > line.b.x()); + }; + fnc use_point_y = [&line, &dir](const Vec2d &p) -> bool { + return (dir.y() > 0) ? (p.y() > line.a.y()) && (p.y() < line.b.y()) : + (p.y() < line.a.y()) && (p.y() > line.b.y()); + }; + bool use_x = PointUtils::is_majorit_x(dir); + fnc use_point = (use_x) ? use_point_x : use_point_y; + bool use_a = use_point(segment->a); + bool use_b = use_point(segment->b); + if (!use_a && !use_b) return {}; + if (use_a && use_b) return segment; + bool same_dir = (use_x) ? ((dir.x() > 0) == + ((segment->b.x() - segment->a.x()) > 0)) : + ((dir.y() > 0) == + ((segment->b.y() - segment->a.y()) > 0)); + if (use_a) { + if (same_dir) + return Linef(segment->a, line.b); + else + return Linef(line.a, segment->a); + } else { // use b + if (same_dir) + return Linef(line.a, segment->b); + else + return Linef(segment->b, line.b); + } +} + + +std::tuple LineUtils::get_param(const Line &line) { + Vector normal = line.normal(); + double a = normal.x(); + double b = normal.y(); + double c = -a * line.a.x() - b * line.a.y(); + return {a, b, c}; +} + +std::tuple LineUtils::get_param(const Linef &line) +{ + Vec2d direction = line.b - line.a; + Vec2d normal(-direction.y(), direction.x()); + double a = normal.x(); + double b = normal.y(); + double c = -a * line.a.x() - b * line.a.y(); + return {a, b, c}; +} + +void LineUtils::draw(SVG & svg, + const Line &line, + const char *color, + coordf_t stroke_width, + const char *name, + bool side_points, + const char *color_a, + const char *color_b) +{ + svg.draw(line, color, stroke_width); + bool use_name = name != nullptr; + if (use_name) { + Point middle = line.a/2 + line.b/2; + svg.draw_text(middle, name, color); + } + if (side_points) { + std::string name_a = (use_name) ? "A" : (std::string("A_") + name); + std::string name_b = (use_name) ? "B" : (std::string("B_") + name); + svg.draw_text(line.a, name_a.c_str(), color_a); + svg.draw_text(line.b, name_b.c_str(), color_b); + } +} + +double LineUtils::perp_distance(const Linef &line, Vec2d p) +{ + Vec2d v = line.b - line.a; // direction + Vec2d va = p - line.a; + return std::abs(cross2(v, va)) / v.norm(); +} + +bool LineUtils::is_parallel(const Line &first, const Line &second) +{ + Vec2i64 dir1 = direction(first).cast(); + Vec2i64 dir2 = direction(second).cast(); + return Slic3r::cross2(dir1, dir2) == 0; +} + +std::optional LineUtils::intersection(const Line &ray1, const Line &ray2) +{ + const Vec2d v1 = direction(ray1).cast(); + const Vec2d v2 = direction(ray2).cast(); + double denom = cross2(v1, v2); + if (fabs(denom) < std::numeric_limits::epsilon()) return {}; + + const Vec2d v12 = (ray1.a - ray2.a).cast(); + double nume = cross2(v2, v12); + double t = nume / denom; + return (ray1.a.cast() + t * v1); +} + +bool LineUtils::belongs(const Line &line, const Point &point, double benevolence) +{ + const Point &a = line.a; + const Point &b = line.b; + auto is_in_interval = [](coord_t value, coord_t from, coord_t to) -> bool + { + if (from < to) { + // from < value < to + if (from > value || to < value) return false; + } else { + // to < value < from + if (from < value || to > value) return false; + } + return true; + }; + + if (!is_in_interval(point.x(), a.x(), b.x()) || + !is_in_interval(point.y(), a.y(), b.y()) ) + { // out of interval + return false; + } + double distance = line.perp_distance_to(point); + if (distance < benevolence) return true; + return false; +} + +Slic3r::Point LineUtils::direction(const Line &line) +{ + return line.b - line.a; +} + +Slic3r::Point LineUtils::middle(const Line &line) { + // division before adding to prevent data type overflow + return line.a / 2 + line.b / 2; +} + +double LineUtils::foot(const Line &line, const Point &point) +{ + Vec2d a = line.a.cast(); + Vec2d vec = point.cast() - a; + Vec2d b = line.b.cast(); + Vec2d dir = b - a; + double l2 = dir.squaredNorm(); + return vec.dot(dir) / l2; +} + +LineUtils::LineConnection LineUtils::create_line_connection( + const Slic3r::Lines &lines) +{ + LineConnection line_connection; + static const size_t bad_index = -1; + auto insert = [&](size_t line_index, size_t connected, bool connect_by_a){ + auto item = line_connection.find(line_index); + if (item == line_connection.end()) { + // create new + line_connection[line_index] = (connect_by_a) ? + std::pair(connected, bad_index) : + std::pair(bad_index, connected); + } else { + std::pair &pair = item->second; + size_t &ref_index = (connect_by_a) ? pair.first : pair.second; + assert(ref_index == bad_index); + ref_index = connected; + } + }; + + auto inserts = [&](size_t i1, size_t i2)->bool{ + bool is_l1_a_connect = true; // false => l1_b_connect + const Slic3r::Line &l1 = lines[i1]; + const Slic3r::Line &l2 = lines[i2]; + if (!PointUtils::is_equal(l1.a, l2.b)) return false; + if (!PointUtils::is_equal(l1.b, l2.a)) return false; + else is_l1_a_connect = false; + insert(i1, i2, is_l1_a_connect); + insert(i2, i1, !is_l1_a_connect); + return true; + }; + + std::vector not_finished; + size_t prev_index = lines.size() - 1; + for (size_t index = 0; index < lines.size(); ++index) { + if (!inserts(prev_index, index)) { + bool found_index = false; + bool found_prev_index = false; + not_finished.erase(std::remove_if(not_finished.begin(), + not_finished.end(), + [&](const size_t ¬_finished_index) { + if (!found_index && inserts(index, not_finished_index)) { + found_index = true; + return true; + } + if (!found_prev_index && inserts(prev_index, not_finished_index)) { + found_prev_index = true; + return true; + } + return false; + }), + not_finished.end()); + if (!found_index) not_finished.push_back(index); + if (!found_prev_index) not_finished.push_back(prev_index); + } + prev_index = index; + } + assert(not_finished.empty()); + return line_connection; +} + +Slic3r::BoundingBox LineUtils::create_bounding_box(const Lines &lines) { + Points pts; + pts.reserve(lines.size()*2); + for (const Line &line : lines) { + pts.push_back(line.a); + pts.push_back(line.b); + } + return BoundingBox(pts); +} + +std::map LineUtils::create_line_connection_over_b(const Lines &lines) +{ + std::map line_connection; + auto inserts = [&](size_t i1, size_t i2) -> bool { + const Line &l1 = lines[i1]; + const Line &l2 = lines[i2]; + if (!PointUtils::is_equal(l1.b, l2.a)) + return false; + assert(line_connection.find(i1) == line_connection.end()); + line_connection[i1] = i2; + return true; + }; + + std::vector not_finished_a; + std::vector not_finished_b; + size_t prev_index = lines.size() - 1; + for (size_t index = 0; index < lines.size(); ++index) { + if (!inserts(prev_index, index)) { + bool found_b = false; + not_finished_b.erase(std::remove_if(not_finished_b.begin(), not_finished_b.end(), + [&](const size_t ¬_finished_index) { + if (!found_b && inserts(prev_index, not_finished_index)) { + found_b = true; + return true; + } + return false; + }),not_finished_b.end()); + if (!found_b) not_finished_a.push_back(prev_index); + + bool found_a = false; + not_finished_a.erase(std::remove_if(not_finished_a.begin(), not_finished_a.end(), + [&](const size_t ¬_finished_index) { + if (!found_a && inserts(not_finished_index, index)) { + found_a = true; + return true; + } + return false; + }),not_finished_a.end()); + if (!found_a) not_finished_b.push_back(index); + } + prev_index = index; + } + assert(not_finished_a.empty()); + assert(not_finished_b.empty()); + return line_connection; +} + +void LineUtils::draw(SVG & svg, + const Lines &lines, + const char * color, + coordf_t stroke_width, + bool ord, + bool side_points, + const char * color_a, + const char * color_b) +{ + for (const auto &line : lines) { + draw(svg, line, color, stroke_width, + (ord) ? std::to_string(&line - &lines.front()).c_str() : nullptr, + side_points, color_a, color_b); + } +} \ No newline at end of file diff --git a/src/libslic3r/SLA/SupportIslands/LineUtils.hpp b/src/libslic3r/SLA/SupportIslands/LineUtils.hpp new file mode 100644 index 0000000000..d48d988d9c --- /dev/null +++ b/src/libslic3r/SLA/SupportIslands/LineUtils.hpp @@ -0,0 +1,227 @@ +#ifndef slic3r_SLA_SuppotstIslands_LineUtils_hpp_ +#define slic3r_SLA_SuppotstIslands_LineUtils_hpp_ + +#include +#include +#include +#include +#include +#include +#include "PointUtils.hpp" + +namespace Slic3r::sla { + +/// +/// Class which contain collection of static function +/// for work with Line and Lines. +/// QUESTION: Is it only for SLA? +/// +class LineUtils +{ +public: + LineUtils() = delete; + + /// + /// Sort lines to be in counter clock wise order only by point Line::a and function std::atan2 + /// + /// Lines to be sort + /// Center for CCW order + static void sort_CCW(Lines &lines, const Point ¢er); + + /// + /// Create line segment intersection of line and circle + /// + /// Input line. + /// Circle center. + /// Circle radius. + /// Chord -> line segment inside circle + static std::optional crop_line(const Line & line, + const Point ¢er, + double radius); + static std::optional crop_line(const Linef & line, + const Point ¢er, + double radius); + /// + /// Create line segment intersection of ray and circle, when exist + /// + /// Input ray. + /// Circle center. + /// Circle radius. + /// Chord -> line segment inside circle + static std::optional crop_ray(const Line & ray, + const Point ¢er, + double radius); + static std::optional crop_ray(const Linef & ray, + const Point ¢er, + double radius); + /// + /// Create line segment intersection of half ray(start point and direction) and circle, when exist + /// + /// Use Line::a as start point and Line::b as direction but no limit + /// Circle center. + /// Circle radius. + /// Chord -> line segment inside circle + static std::optional crop_half_ray(const Line & half_ray, + const Point ¢er, + double radius); + static std::optional crop_half_ray(const Linef & half_ray, + const Point ¢er, + double radius); + + /// + /// check if line is parallel to Y + /// + /// Input line + /// True when parallel otherwise FALSE + static bool is_parallel_y(const Line &line); + static bool is_parallel_y(const Linef &line); + + /// + /// Create parametric coeficient + /// ax + by + c = 0 + /// Can't be parallel to Y axis - check by function is_parallel_y + /// + /// Input line - cant be parallel with y axis + /// a, b, c + static std::tuple get_param(const Line &line); + static std::tuple get_param(const Linef &line); + + /// + /// Calculate distance between point and ray + /// + /// a and b are only for direction, not limit + /// Point in space + /// Euclid perpedicular distance + static double perp_distance(const Linef &line, Vec2d p); + + /// + /// Create cross product of line direction. + /// When zero than they are parallel. + /// + /// First line + /// Second line + /// True when direction are same(scaled or oposit or combination) otherwise FALSE + static bool is_parallel(const Line &first, const Line &second); + + /// + /// Intersection of line - no matter on line limitation + /// + /// first line + /// second line + /// intersection point when exist + static std::optional intersection(const Line &ray1, const Line &ray2); + + /// + /// Check when point lays on line and belongs line range(from point a to point b) + /// + /// source line + /// point to check + /// maximal distance from line to belongs line + /// True when points lay between line.a and line.b + static bool belongs(const Line & line, + const Point &point, + double benevolence = 1.); + + /// + /// Create direction from point a to point b + /// Direction vector is represented as point + /// + /// input line + /// line direction | b - a + static Point direction(const Line &line); + + /// + /// middle point, center of line + /// + /// + /// ceneter of line | a+b / 2 + static Point middle(const Line &line); + + /// + /// Calculate foot point in maner of Geometry::foot_pt + /// - without unnecessary conversion + /// + /// input line + /// point to search foot on line + /// ration betwen point line.a and line.b (in range from 0. to 1.) + static double foot(const Line &line, const Point& point); + + // line index, + using LineConnection = std::map>; + /// + /// Create data structure from exPolygon lines to find if two lines are connected + /// !! not tested + /// + /// Lines created from ExPolygon + /// map of connected lines. + static LineConnection create_line_connection(const Lines &lines); + + /// + /// create bounding box around lines + /// + /// input lines + /// Bounding box + static BoundingBox create_bounding_box(const Lines &lines); + + /// + /// Create data structure from exPolygon lines to store connected line indexes + /// + /// Lines created from ExPolygon + /// map of connected lines over point line::b + static std::map create_line_connection_over_b(const Lines &lines); + + /// + /// Comparator to sort points laying on line from point a to point b + /// + struct SortFromAToB + { + std::function compare; + SortFromAToB(const Line &line) + { + Point dir = LineUtils::direction(line); + compare = (PointUtils::is_majorit_x(dir)) ? + ((dir.x() < 0) ? is_x_grater : is_x_smaller) : + ((dir.y() < 0) ? is_y_grater : is_y_smaller); + } + static bool is_x_grater(const Point &left, const Point &right) + { + return left.x() > right.x(); + } + static bool is_x_smaller(const Point &left, const Point &right) + { + return left.x() < right.x(); + } + static bool is_y_grater(const Point &left, const Point &right) + { + return left.y() > right.y(); + } + static bool is_y_smaller(const Point &left, const Point &right) + { + return left.y() < right.y(); + } + bool operator()(const Point &left, const Point &right) + { + return compare(left, right); + } + }; + + static void draw(SVG & svg, + const Line &line, + const char *color = "gray", + coordf_t stroke_width = 0, + const char *name = nullptr, + bool side_points = false, + const char *color_a = "lightgreen", + const char *color_b = "lightblue"); + static void draw(SVG & svg, + const Lines &lines, + const char *color = "gray", + coordf_t stroke_width = 0, + bool ord = false, // write order as text + bool side_points = false, + const char *color_a = "lightgreen", + const char *color_b = "lightblue"); +}; + +} // namespace Slic3r::sla +#endif // slic3r_SLA_SuppotstIslands_LineUtils_hpp_ diff --git a/src/libslic3r/SLA/SupportIslands/NodeDataWithResult.hpp b/src/libslic3r/SLA/SupportIslands/NodeDataWithResult.hpp new file mode 100644 index 0000000000..0db4c6725f --- /dev/null +++ b/src/libslic3r/SLA/SupportIslands/NodeDataWithResult.hpp @@ -0,0 +1,73 @@ +#ifndef slic3r_SLA_SuppotstIslands_NodeDataWithResult_hpp_ +#define slic3r_SLA_SuppotstIslands_NodeDataWithResult_hpp_ + +#include +#include +#include "VoronoiGraph.hpp" + +namespace Slic3r::sla { + +/// +/// DTO for process node during depth search +/// which create longest path in voronoi graph +/// +struct NodeDataWithResult +{ + // result for this node + VoronoiGraph::ExPath &result; + + // actual proccessed node + const VoronoiGraph::Node *node; + // distance to this node from input node + double distance_to_node; + + // path from start point to this node + // last one is actual node + VoronoiGraph::Path act_path; + + // skip node when circle start - must end at this node + // set --> multiple cirle could start at same node + // previous node should be skiped to so it is initialized with it + std::set skip_nodes; // circle + + // store all circle indexes this node is lay on + // used to create connected circles structure + std::vector circle_indexes; + // When circle_indexes is not empty node lays on circle + // and in this node is not searching for longest path only store side + // branches(not part of circle) + + // indexes of circle ending in this node(could be more than one) + std::vector end_circle_indexes; + // When end_circle_indexes == circle_indexes + // than it is end of circle (multi circle structure) and it is processed + + // contain possible continue path + // possible empty + VoronoiGraph::ExPath::SideBranches side_branches; + +public: + // append node to act path + NodeDataWithResult( + VoronoiGraph::ExPath & result, + const VoronoiGraph::Node *node, + double distance_to_node, + VoronoiGraph::Path &&act_path, + std::set &&skip_nodes + ) + : result(result) + , node(node) + , distance_to_node(distance_to_node) + , act_path(std::move(act_path)) // copy prev and append actual node + , skip_nodes(std::move(skip_nodes)) + { + //prev_path.extend(node, distance_to_node) + //const VoronoiGraph::Node *prev_node = (prev_path.nodes.size() >= 1) ? + // prev_path.nodes.back() : + // nullptr; + //skip_nodes = {prev_node}; + } +}; + +} // namespace Slic3r::sla +#endif // slic3r_SLA_SuppotstIslands_NodeDataWithResult_hpp_ diff --git a/src/libslic3r/SLA/SupportIslands/Parabola.hpp b/src/libslic3r/SLA/SupportIslands/Parabola.hpp new file mode 100644 index 0000000000..d6b69a1d5f --- /dev/null +++ b/src/libslic3r/SLA/SupportIslands/Parabola.hpp @@ -0,0 +1,43 @@ +#ifndef slic3r_SLA_SuppotstIslands_Parabola_hpp_ +#define slic3r_SLA_SuppotstIslands_Parabola_hpp_ + +#include +#include + +namespace Slic3r::sla { + +/// +/// DTO store prabola params +/// A parabola can be defined geometrically as a set of points (locus of points) in the Euclidean plane: +/// Where distance from focus point is same as distance from line(directrix). +/// +struct Parabola +{ + Line directrix; + Point focus; + + Parabola(Line directrix, Point focus) + : directrix(std::move(directrix)), focus(std::move(focus)) + {} +}; + + +/// +/// DTO store segment of parabola +/// Parabola with start(from) and end(to) point lay on parabola +/// +struct ParabolaSegment: public Parabola +{ + Point from; + Point to; + + ParabolaSegment(Parabola parabola, Point from, Point to) : + Parabola(std::move(parabola)), from(from), to(to) + {} + ParabolaSegment(Line directrix, Point focus, Point from, Point to) + : Parabola(directrix, focus), from(from), to(to) + {} +}; + +} // namespace Slic3r::sla +#endif // slic3r_SLA_SuppotstIslands_Parabola_hpp_ diff --git a/src/libslic3r/SLA/SupportIslands/ParabolaUtils.cpp b/src/libslic3r/SLA/SupportIslands/ParabolaUtils.cpp new file mode 100644 index 0000000000..6ba513a7e9 --- /dev/null +++ b/src/libslic3r/SLA/SupportIslands/ParabolaUtils.cpp @@ -0,0 +1,109 @@ +#include "ParabolaUtils.hpp" +#include "PointUtils.hpp" +#include "VoronoiGraphUtils.hpp" + +// sampling parabola +#include +#include +#include + +using namespace Slic3r::sla; + +double ParabolaUtils::length(const ParabolaSegment ¶bola) +{ + const Point &point = parabola.focus; + const Line & line = parabola.directrix; + Line norm_line(point, point + line.normal()); + + // sign of distance is resolved by dot product in function is_over_zero() + double scaled_x1 = norm_line.perp_distance_to(parabola.from); + double scaled_x2 = norm_line.perp_distance_to(parabola.to); + + double parabola_scale = 1. / (4. * focal_length(parabola)); + + double x1 = scaled_x1 * parabola_scale; + double x2 = scaled_x2 * parabola_scale; + + double length_x1 = parabola_arc_length(x1) / parabola_scale; + double length_x2 = parabola_arc_length(x2) / parabola_scale; + + return (is_over_zero(parabola)) ? + (length_x1 + length_x2) : // interval is over zero + fabs(length_x1 - length_x2); // interval is on same side of parabola +} +double ParabolaUtils::length_by_sampling( + const ParabolaSegment ¶bola, + double discretization_step) +{ + using VD = Slic3r::Geometry::VoronoiDiagram; + std::vector parabola_samples({ + VoronoiGraphUtils::to_point(parabola.from), + VoronoiGraphUtils::to_point(parabola.to)}); + VD::point_type source_point = VoronoiGraphUtils::to_point(parabola.focus); + VD::segment_type source_segment = VoronoiGraphUtils::to_segment(parabola.directrix); + ::boost::polygon::voronoi_visual_utils::discretize( + source_point, source_segment, discretization_step, ¶bola_samples); + + double sumLength = 0; + for (size_t index = 1; index < parabola_samples.size(); ++index) { + double diffX = parabola_samples[index - 1].x() - + parabola_samples[index].x(); + double diffY = parabola_samples[index - 1].y() - + parabola_samples[index].y(); + double length = sqrt(diffX * diffX + diffY * diffY); + sumLength += length; + } + return sumLength; +} + +double ParabolaUtils::focal_length(const Parabola ¶bola) +{ + // https://en.wikipedia.org/wiki/Parabola + // p = 2f; y = 1/(4f) * x^2; y = 1/(2p) * x^2 + double p = parabola.directrix.perp_distance_to(parabola.focus); + double f = p / 2.; + return f; +} + +bool ParabolaUtils::is_over_zero(const ParabolaSegment ¶bola) +{ + Vec2i64 line_direction = (parabola.directrix.b - parabola.directrix.a).cast(); + Vec2i64 focus_from = (parabola.focus - parabola.from).cast(); + Vec2i64 focus_to = (parabola.focus - parabola.to).cast();; + bool is_positive_x1 = line_direction.dot(focus_from) > 0; + bool is_positive_x2 = line_direction.dot(focus_to) > 0; + return is_positive_x1 != is_positive_x2; +} + +void ParabolaUtils::draw(SVG & svg, + const ParabolaSegment ¶bola, + const char * color, + coord_t width, + double discretization_step) +{ + using VD = Slic3r::Geometry::VoronoiDiagram; + if (PointUtils::is_equal(parabola.from, parabola.to)) return; + + std::vector parabola_samples( + {VoronoiGraphUtils::to_point(parabola.from), + VoronoiGraphUtils::to_point(parabola.to)}); + VD::point_type source_point = VoronoiGraphUtils::to_point(parabola.focus); + VD::segment_type source_segment = VoronoiGraphUtils::to_segment(parabola.directrix); + + ::boost::polygon::voronoi_visual_utils::discretize( + source_point, source_segment, discretization_step, ¶bola_samples); + + for (size_t index = 1; index < parabola_samples.size(); ++index) { + const auto& s0 = parabola_samples[index - 1]; + const auto& s1 = parabola_samples[index]; + Line l(Point(s0.x(), s0.y()), Point(s1.x(), s1.y())); + svg.draw(l, color, width); + } +} + +// PRIVATE +double ParabolaUtils::parabola_arc_length(double x) +{ + double sqrtRes = sqrt(1 + 4 * x * x); + return 1 / 4. * log(2 * x + sqrtRes) + 1 / 2. * x * sqrtRes; +}; diff --git a/src/libslic3r/SLA/SupportIslands/ParabolaUtils.hpp b/src/libslic3r/SLA/SupportIslands/ParabolaUtils.hpp new file mode 100644 index 0000000000..460914fd2f --- /dev/null +++ b/src/libslic3r/SLA/SupportIslands/ParabolaUtils.hpp @@ -0,0 +1,73 @@ +#ifndef slic3r_SLA_SuppotstIslands_ParabolaUtils_hpp_ +#define slic3r_SLA_SuppotstIslands_ParabolaUtils_hpp_ + +#include "Parabola.hpp" +#include + +namespace Slic3r::sla { + +/// +/// Class which contain collection of static function +/// for work with Parabola. +/// +class ParabolaUtils +{ +public: + ParabolaUtils() = delete; + + /// + /// Integrate length over interval defined by points from and to + /// + /// Input segment of parabola + /// Length of parabola arc + static double length(const ParabolaSegment ¶bola); + + /// + /// Sample parabola between points from and to by step. + /// + /// Input segment of parabola + /// Define sampling + /// Length of parabola arc + static double length_by_sampling(const ParabolaSegment ¶bola, + double discretization_step = 200); + + /// + /// calculate focal length of parabola + /// + /// input parabola + /// Focal length + static double focal_length(const Parabola ¶bola); + + /// + /// Check if parabola interval (from, to) contains top of parabola + /// + /// Input segment of parabola + /// True when interval contain top of parabola otherwise False + static bool is_over_zero(const ParabolaSegment ¶bola); + + /// + /// Connvert parabola to svg by sampling + /// + /// outputfile + /// parabola to draw + /// color + /// width + /// step between discretized lines + static void draw(SVG & svg, + const ParabolaSegment ¶bola, + const char * color, + coord_t width, + double discretization_step = 1e3); + +private: + /// + /// Integral of parabola: y = x^2 from zero to point x + /// https://ocw.mit.edu/courses/mathematics/18-01sc-single-variable-calculus-fall-2010/unit-4-techniques-of-integration/part-b-partial-fractions-integration-by-parts-arc-length-and-surface-area/session-78-computing-the-length-of-a-curve/MIT18_01SCF10_Ses78d.pdf + /// + /// x coordinate of parabola, Positive number + /// Length of parabola from zero to x + static double parabola_arc_length(double x); +}; + +} // namespace Slic3r::sla +#endif // slic3r_SLA_SuppotstIslands_ParabolaUtils_hpp_ diff --git a/src/libslic3r/SLA/SupportIslands/PointUtils.cpp b/src/libslic3r/SLA/SupportIslands/PointUtils.cpp new file mode 100644 index 0000000000..1e828eda14 --- /dev/null +++ b/src/libslic3r/SLA/SupportIslands/PointUtils.cpp @@ -0,0 +1,40 @@ +#include "PointUtils.hpp" + +using namespace Slic3r::sla; + +bool PointUtils::is_equal(const Point &p1, const Point &p2) { + return p1.x() == p2.x() && p1.y() == p2.y(); +} + +bool PointUtils::is_majorit_x(const Point &point) +{ + return abs(point.x()) > abs(point.y()); +} + +bool PointUtils::is_majorit_x(const Vec2d &point) +{ + return fabs(point.x()) > fabs(point.y()); +} + +Slic3r::Point PointUtils::perp(const Point &vector) +{ + return Point(-vector.y(), vector.x()); +} + +bool PointUtils::is_same_direction(const Point &dir1, const Point &dir2) +{ + // (is_majorit_x(dir1)) ? (dir1.x() > 0) == (dir2.x() > 0) : + // (dir1.y() > 0) == (dir2.y() > 0); + // Cant use majorit direction: + // [2] 750000 544907 + // [2] 463525 -1426583 + // !! bad idea + + // Cant use dot product for int value ==> dir1.dot(dir2) + // diferent int result for input + //[2] - 128707028 93448506 + //[2] 10475487 1662574 + // may be overflow ?? + + return dir1.cast().dot(dir2.cast()) > 0; +} diff --git a/src/libslic3r/SLA/SupportIslands/PointUtils.hpp b/src/libslic3r/SLA/SupportIslands/PointUtils.hpp new file mode 100644 index 0000000000..2a640ea678 --- /dev/null +++ b/src/libslic3r/SLA/SupportIslands/PointUtils.hpp @@ -0,0 +1,53 @@ +#ifndef slic3r_SLA_SuppotstIslands_PointUtils_hpp_ +#define slic3r_SLA_SuppotstIslands_PointUtils_hpp_ + +#include + +namespace Slic3r::sla { + +/// +/// Class which contain collection of static function +/// for work with Point and Points. +/// QUESTION: Is it only for SLA? +/// +class PointUtils +{ +public: + PointUtils() = delete; + /// + /// Is equal p1 == p2 + /// + /// first point + /// second point + /// True when equal otherwise FALSE + static bool is_equal(const Point &p1, const Point &p2); + + /// + /// check if absolut value of x is grater than absolut value of y + /// + /// input direction + /// True when mayorit vaule is X other wise FALSE(mayorit value is y) + static bool is_majorit_x(const Point &point); + static bool is_majorit_x(const Vec2d &point); + + /// + /// Create perpendicular vector[-y,x] + /// + /// input vector from zero to point coordinate + /// Perpendicular[-vector.y, vector.x] + static Point perp(const Point &vector); + + /// + /// Check if both direction are on same half of the circle + /// alpha = atan2 of direction1 + /// beta = atan2 of direction2 + /// beta is in range from(alpha - Pi/2) to (alpha + Pi/2) + /// + /// first direction + /// second direction + /// True when on same half otherwise false + static bool is_same_direction(const Point &dir1, const Point &dir2); +}; + +} // namespace Slic3r::sla +#endif // slic3r_SLA_SuppotstIslands_PointUtils_hpp_ diff --git a/src/libslic3r/SLA/SupportIslands/PolygonUtils.cpp b/src/libslic3r/SLA/SupportIslands/PolygonUtils.cpp new file mode 100644 index 0000000000..e20526e0d0 --- /dev/null +++ b/src/libslic3r/SLA/SupportIslands/PolygonUtils.cpp @@ -0,0 +1,87 @@ +#include "PolygonUtils.hpp" +#include "libslic3r/Geometry.hpp" + +using namespace Slic3r::sla; + +inline bool is_in_coord_limits(const double& value) { + return (value < std::numeric_limits::max()) && + (value > std::numeric_limits::min()); +} + +Slic3r::Polygon PolygonUtils::create_regular(size_t count_points, + double radius, + const Point ¢er) +{ + assert(radius >= 1.); + assert(count_points >= 3); + Points points; + points.reserve(count_points); + double increase_angle = 2 * M_PI / count_points; + for (size_t i = 0; i < count_points; ++i) { + double angle = i * increase_angle; + double x = cos(angle) * radius + center.x(); + assert(is_in_coord_limits(x)); + double y = sin(angle) * radius + center.y(); + assert(is_in_coord_limits(y)); + points.emplace_back(x, y); + } + return Polygon(points); +} + +Slic3r::Polygon PolygonUtils::create_equilateral_triangle(double edge_size) +{ + coord_t x = edge_size / 2; + coord_t y = sqrt(edge_size * edge_size - edge_size * edge_size / 4) / 2; + return {{-x, -y}, {x, -y}, {0, y}}; +} + +Slic3r::Polygon PolygonUtils::create_isosceles_triangle(double side, double height) +{ + return {{-side / 2, 0.}, {side / 2, 0.}, {.0, height}}; +} + +Slic3r::Polygon PolygonUtils::create_square(double size) +{ + double size_2 = size / 2; + return {{-size_2, size_2}, + {-size_2, -size_2}, + {size_2, -size_2}, + {size_2, size_2}}; +} + +Slic3r::Polygon PolygonUtils::create_rect(double width, double height) +{ + double x_2 = width / 2; + double y_2 = height / 2; + return {{-x_2, y_2}, {-x_2, -y_2}, {x_2, -y_2}, {x_2, y_2}}; +} + +bool PolygonUtils::is_ccw(const Polygon &polygon, const Point ¢er) { + const Point *prev = &polygon.points.back(); + for (const Point &point : polygon.points) { + Geometry::Orientation o = Geometry::orient(center, *prev, point); + if (o != Geometry::Orientation::ORIENTATION_CCW) return false; + prev = &point; + } + return true; +} + +bool PolygonUtils::is_not_self_intersect(const Polygon &polygon, + const Point & center) +{ + auto get_angle = [¢er](const Point &point) { + Point diff_point = point - center; + return atan2(diff_point.y(), diff_point.x()); + }; + bool found_circle_end = false; // only one can be on polygon + double prev_angle = get_angle(polygon.points.back()); + for (const Point &point : polygon.points) { + double angle = get_angle(point); + if (angle < prev_angle) { + if (found_circle_end) return false; + found_circle_end = true; + } + prev_angle = angle; + } + return true; +} diff --git a/src/libslic3r/SLA/SupportIslands/PolygonUtils.hpp b/src/libslic3r/SLA/SupportIslands/PolygonUtils.hpp new file mode 100644 index 0000000000..314e7257d3 --- /dev/null +++ b/src/libslic3r/SLA/SupportIslands/PolygonUtils.hpp @@ -0,0 +1,84 @@ +#ifndef slic3r_SLA_SuppotstIslands_PolygonUtils_hpp_ +#define slic3r_SLA_SuppotstIslands_PolygonUtils_hpp_ + +#include + +namespace Slic3r::sla { +/// +/// Class which contain collection of static function +/// for work with Polygon. +/// +class PolygonUtils +{ +public: + PolygonUtils() = delete; + + /// + /// Create regular polygon with N points + /// + /// Count points of regular polygon + /// Radius around center + /// Center point + /// Regular Polygon with CCW points + static Polygon create_regular(size_t count_points, double radius = 10., const Point& center = Point(0,0)); + + /// + /// Create circle with N points + /// alias for create regular + /// + /// Radius of circle + /// Count points of circle + /// Center point + /// Regular Polygon with CCW points + static Polygon create_circle(double radius, size_t count_points = 10, const Point& center = Point(0,0)){ + return create_regular(count_points, radius, center); + } + + /// + /// Create triangle with same length for all sides + /// + /// triangel edge size + /// Equilateral triangle + static Polygon create_equilateral_triangle(double edge_size); + + /// + /// Create triangle with two side with same size + /// + /// Size of unique side + /// triangle height + /// Isosceles Triangle + static Polygon create_isosceles_triangle(double side, double height); + + /// + /// Create squar with center in [0,0] + /// + /// + /// Square + static Polygon create_square(double size); + + /// + /// Create rect with center in [0,0] + /// + /// width + /// height + /// Rectangle + static Polygon create_rect(double width, double height); + + /// + /// check if all pairs on polygon create with center ccw triangle + /// + /// input polygon to check + /// center point inside polygon + /// True when all points in polygon are CCW with center + static bool is_ccw(const Polygon &polygon, const Point ¢er); + + /// + /// ! Only for polygon around point, like Voronoi diagram cell + /// + /// Polygon to check + /// Center inside polygon, points create circle around center + /// True when valid without self intersection otherwise FALSE + static bool is_not_self_intersect(const Polygon &polygon, const Point ¢er); +}; +} // namespace Slic3r::sla +#endif // slic3r_SLA_SuppotstIslands_PolygonUtils_hpp_ diff --git a/src/libslic3r/SLA/SupportIslands/PostProcessNeighbor.cpp b/src/libslic3r/SLA/SupportIslands/PostProcessNeighbor.cpp new file mode 100644 index 0000000000..1ca9e98d7c --- /dev/null +++ b/src/libslic3r/SLA/SupportIslands/PostProcessNeighbor.cpp @@ -0,0 +1,40 @@ +#include "PostProcessNeighbor.hpp" + +#include "VoronoiGraphUtils.hpp" + +using namespace Slic3r::sla; + +void PostProcessNeighbor::process() +{ + bool is_circle_neighbor = false; + if (neighbor_path.nodes.empty()) { // neighbor node is on circle + for (VoronoiGraph::Circle &circle : neighbor_path.circles) { + const auto &circle_item = std::find(circle.nodes.begin(), + circle.nodes.end(), data.node); + if (circle_item == circle.nodes.end()) + continue; // node is NOT on circle + + size_t next_circle_index = &circle - + &neighbor_path.circles.front(); + size_t circle_index = data.result.circles.size() + + next_circle_index; + data.circle_indexes.push_back(circle_index); + + // check if this node is end of circle + if (circle_item == circle.nodes.begin()) { + data.end_circle_indexes.push_back(circle_index); + + // !! this FIX circle lenght because at detection of + // circle it will cost time to calculate it + circle.length -= data.act_path.length; + + // skip twice checking of circle + data.skip_nodes.insert(circle.nodes.back()); + } + is_circle_neighbor = true; + } + } + VoronoiGraphUtils::append_neighbor_branch(data.result, neighbor_path); + if (!is_circle_neighbor) + data.side_branches.push(std::move(neighbor_path)); +} \ No newline at end of file diff --git a/src/libslic3r/SLA/SupportIslands/PostProcessNeighbor.hpp b/src/libslic3r/SLA/SupportIslands/PostProcessNeighbor.hpp new file mode 100644 index 0000000000..fbd63a8b53 --- /dev/null +++ b/src/libslic3r/SLA/SupportIslands/PostProcessNeighbor.hpp @@ -0,0 +1,36 @@ +#ifndef slic3r_SLA_SuppotstIslands_PostProcessNeighbor_hpp_ +#define slic3r_SLA_SuppotstIslands_PostProcessNeighbor_hpp_ + +#include "IStackFunction.hpp" +#include "NodeDataWithResult.hpp" +#include "VoronoiGraph.hpp" +#include "NodeDataWithResult.hpp" + +namespace Slic3r::sla { + +/// +/// Decimate data from Ex path to path +/// Done after ONE neighbor is procceessed. +/// Check if node is on circle. +/// Remember ended circle +/// Merge side branches and circle information into result +/// +class PostProcessNeighbor : public IStackFunction +{ + NodeDataWithResult &data; + +public: + VoronoiGraph::ExPath neighbor_path; // data filled in EvaluateNeighbor + PostProcessNeighbor(NodeDataWithResult &data) : data(data) {} + + virtual void process([[maybe_unused]] CallStack &call_stack) + { + process(); + } + +private: + void process(); +}; + +} // namespace Slic3r::sla +#endif // slic3r_SLA_SuppotstIslands_PostProcessNeighbor_hpp_ diff --git a/src/libslic3r/SLA/SupportIslands/PostProcessNeighbors.cpp b/src/libslic3r/SLA/SupportIslands/PostProcessNeighbors.cpp new file mode 100644 index 0000000000..10842a3381 --- /dev/null +++ b/src/libslic3r/SLA/SupportIslands/PostProcessNeighbors.cpp @@ -0,0 +1,50 @@ +#include "PostProcessNeighbors.hpp" + +#include "VoronoiGraphUtils.hpp" + +using namespace Slic3r::sla; + +void PostProcessNeighbors::process() +{ + // remember connected circle + if (circle_indexes.size() > 1) { + for (size_t circle_index : circle_indexes) { + for (size_t circle_index2 : circle_indexes) { + if (circle_index == circle_index2) continue; + result.connected_circle[circle_index].insert(circle_index2); + } + } + } + + // detect end of circles in this node + if (!end_circle_indexes.empty() && + end_circle_indexes.size() == circle_indexes.size()) { + size_t circle_index = circle_indexes.front(); // possible any of them + side_branches.push( + VoronoiGraphUtils::find_longest_path_on_circles(*node, + circle_index, + result)); + + circle_indexes.clear(); // resolved circles + } + + // simple node on circle --> only input and output neighbor + if (side_branches.empty()) return; + + // is node on unresolved circle? + if (!circle_indexes.empty()) { + // not search for longest path, it will eval on end of circle + result.side_branches[node] = side_branches; + return; + } + + // create result longest path from longest side branch + VoronoiGraph::Path longest_path(std::move(side_branches.top())); + side_branches.pop(); + if (!side_branches.empty()) { + result.side_branches[node] = side_branches; + } + longest_path.nodes.insert(longest_path.nodes.begin(), node); + result.nodes = std::move(longest_path.nodes); + result.length = distance_to_node + longest_path.length; +} \ No newline at end of file diff --git a/src/libslic3r/SLA/SupportIslands/PostProcessNeighbors.hpp b/src/libslic3r/SLA/SupportIslands/PostProcessNeighbors.hpp new file mode 100644 index 0000000000..01d6a2d2bb --- /dev/null +++ b/src/libslic3r/SLA/SupportIslands/PostProcessNeighbors.hpp @@ -0,0 +1,47 @@ +#ifndef slic3r_SLA_SuppotstIslands_PostProcessNeighbors_hpp_ +#define slic3r_SLA_SuppotstIslands_PostProcessNeighbors_hpp_ + +#include "IStackFunction.hpp" +#include "VoronoiGraph.hpp" +#include "NodeDataWithResult.hpp" + +namespace Slic3r::sla { + +/// +/// call after all neighbors are processed +/// +class PostProcessNeighbors : public NodeDataWithResult, public IStackFunction +{ +public: + PostProcessNeighbors(VoronoiGraph::ExPath & result, + const VoronoiGraph::Node *node, + double distance_to_node = 0., + const VoronoiGraph::Path &prev_path = + VoronoiGraph::Path({}, 0.) // make copy + ) + : NodeDataWithResult( + result, node, distance_to_node, + prev_path.extend(node, distance_to_node), + prepare_skip_nodes(prev_path) + ) + {} + + virtual void process([[maybe_unused]] CallStack &call_stack) + { + process(); + } + +private: + static std::set prepare_skip_nodes( + const VoronoiGraph::Path &prev_path) + { + if (prev_path.nodes.empty()) return {}; + const VoronoiGraph::Node *prev_node = prev_path.nodes.back(); + return {prev_node}; + } + + void process(); +}; + +} // namespace Slic3r::sla +#endif // slic3r_SLA_SuppotstIslands_PostProcessNeighbors_hpp_ diff --git a/src/libslic3r/SLA/SupportIslands/SampleConfig.hpp b/src/libslic3r/SLA/SupportIslands/SampleConfig.hpp new file mode 100644 index 0000000000..a177e36da0 --- /dev/null +++ b/src/libslic3r/SLA/SupportIslands/SampleConfig.hpp @@ -0,0 +1,119 @@ +#ifndef slic3r_SLA_SuppotstIslands_SampleConfig_hpp_ +#define slic3r_SLA_SuppotstIslands_SampleConfig_hpp_ + +#include + +#define OPTION_TO_STORE_ISLAND + +namespace Slic3r::sla { + +/// +/// Configure how to prepare data for SupportPointGenerator +/// +struct PrepareSupportConfig +{ + // Size of the steps between discretized samples on the overhanging part of layer + // Smaller value means more point to investigate in support process, + // but smaller divergence of support distances + double discretize_overhang_step = 2.; // [in mm] + + // Detection of peninsula(half island) + // Peninsula contain wider one layer overhang than this value + float peninsula_min_width = scale_(2); // [in scaled mm] + + // Distance from previous layer part to still supported + float peninsula_self_supported_width = scale_(1.5); // [in scaled mm] + + // To be able support same 2d area multipletimes, + // It is neccessary to remove support point form near KDTree structure + // Must be greater than surface texture and lower than self supporting area + // May be use maximal island distance + float removing_delta = scale_(5.); + + // Define minimal size of separable model part which will be filtered out + // Half of support head diameter is impossible to print other than sphere from support head + float minimal_bounding_sphere_radius = 0.2f; // [in mm] +}; + +/// +/// Configuration DTO +/// Define where is neccessary to put support point on island +/// Mainly created by SampleConfigFactory +/// +struct SampleConfig +{ + // Maximal distance of support points on thin island's part + // MUST be bigger than zero + coord_t thin_max_distance = static_cast(scale_(5.)); + + // Maximal distance of support points inside of thick island's part + // MUST be bigger than zero + coord_t thick_inner_max_distance = static_cast(scale_(5.)); + + // Maximal distance of support points on outline of thick island's part + // Sample outline of Field by this value + // MUST be bigger than zero + coord_t thick_outline_max_distance = static_cast(scale_(5 * 3 / 4.)); + + // Support point head radius + // MUST be bigger than zero + coord_t head_radius = static_cast(scale_(.4)); // [nano meter] + + // When it is possible, there will be this minimal distance from outline. + // zero when head should be on outline + coord_t minimal_distance_from_outline = 0; // [nano meter] + + // Measured as sum of VD edge length from outline + // Used only when there is no space for outline offset on first/last point + // Must be bigger than minimal_distance_from_outline + coord_t maximal_distance_from_outline = static_cast(scale_(1.));// [nano meter] + + // Maximal length of longest path in voronoi diagram to be island + // supported only by one single support point this point will be in center of path. + coord_t max_length_for_one_support_point = static_cast(scale_(1.)); + + // Maximal length of island supported by 2 points + coord_t max_length_for_two_support_points = static_cast(scale_(1.)); + // Maximal ratio of path length for island supported by 2 points + // Used only in case when maximal_distance_from_outline is bigger than + // current island longest_path * this ratio + // Note: Preven for tiny island to contain overlapped support points + // must be smaller than 0.5 and bigger than zero + float max_length_ratio_for_two_support_points = 0.25; // |--25%--Sup----50%----Sup--25%--| + + // Maximal width of line island supported in the middle of line + // Must be greater or equal to thick_min_width + coord_t thin_max_width = static_cast(scale_(1.)); + + // Minimal width to be supported by outline + // Must be smaller or equal to thin_max_width + coord_t thick_min_width = static_cast(scale_(1.)); + + // Minimal length of island's part to create tiny&thick interface + coord_t min_part_length = static_cast(scale_(1.)); + + // Term criteria for end of alignment + // Minimal change in manhatn move of support position before termination + coord_t minimal_move = static_cast(scale_(.01)); // devide from print resolution to quater pixel + + // Maximal count of align iteration + size_t count_iteration = 100; + + // Maximal distance over Voronoi diagram edges to find closest point during aligning Support point + coord_t max_align_distance = 0; // [scaled mm -> nanometers] + + // There is no need to calculate with precisse island + // NOTE: Slice of Cylinder bottom has tip of trinagles on contour + // (neighbor coordinate - create issue in voronoi) + double simplification_tolerance = scale_(0.05 /*mm*/); + +#ifdef OPTION_TO_STORE_ISLAND + // Only for debug purposes + std::string path = ""; // when set to empty string, no debug output is generated +#endif // OPTION_TO_STORE_ISLAND + + // Configuration for data preparation + PrepareSupportConfig prepare_config; +}; +} // namespace Slic3r::sla +#endif // slic3r_SLA_SuppotstIslands_SampleConfig_hpp_ diff --git a/src/libslic3r/SLA/SupportIslands/SampleConfigFactory.cpp b/src/libslic3r/SLA/SupportIslands/SampleConfigFactory.cpp new file mode 100644 index 0000000000..e73d636c3b --- /dev/null +++ b/src/libslic3r/SLA/SupportIslands/SampleConfigFactory.cpp @@ -0,0 +1,137 @@ +#include "SampleConfigFactory.hpp" + +using namespace Slic3r::sla; + +bool SampleConfigFactory::verify(SampleConfig &cfg) { + auto verify_max = [](coord_t &c, coord_t max) { + assert(c <= max); + if (c > max) { + c = max; + return false; + } + return true; + }; + auto verify_min = [](coord_t &c, coord_t min) { + assert(c >= min); + if (c < min) { + c = min; + return false; + } + return true; + }; + auto verify_min_max = [](coord_t &min, coord_t &max) { + // min must be smaller than max + assert(min < max); + if (min > max) { + std::swap(min, max); + return false; + } else if (min == max) { + min /= 2; // cut in half + return false; + } + return true; + }; + bool res = true; + res &= verify_min_max(cfg.max_length_for_one_support_point, cfg.max_length_for_two_support_points); + res &= verify_min_max(cfg.thick_min_width, cfg.thin_max_width); // check histeresis + res &= verify_max(cfg.max_length_for_one_support_point, + 2 * cfg.thin_max_distance + + 2 * cfg.head_radius + + 2 * cfg.minimal_distance_from_outline); + res &= verify_min(cfg.max_length_for_one_support_point, + 2 * cfg.head_radius + 2 * cfg.minimal_distance_from_outline); + res &= verify_max(cfg.max_length_for_two_support_points, + 2 * cfg.thin_max_distance + + 2 * 2 * cfg.head_radius + + 2 * cfg.minimal_distance_from_outline); + res &= verify_min(cfg.thin_max_width, + 2 * cfg.head_radius + 2 * cfg.minimal_distance_from_outline); + res &= verify_max(cfg.thin_max_width, + 2 * cfg.thin_max_distance + 2 * cfg.head_radius); + if (!res) while (!verify(cfg)); + return res; +} + +SampleConfig SampleConfigFactory::create(float support_head_diameter_in_mm) { + SampleConfig result; + result.head_radius = static_cast(scale_(support_head_diameter_in_mm/2)); + + assert(result.minimal_distance_from_outline < result.maximal_distance_from_outline); + + // https://cfl.prusa3d.com/display/SLA/Single+Supporty + // head 0.4mm cca 1.65 mm + // head 0.5mm cca 1.85 mm + // This values are used for solvig equation(to find 2.9 and 1.3) + double head_area = M_PI * sqr(support_head_diameter_in_mm / 2); // Pi r^2 + result.max_length_for_one_support_point = static_cast(scale_(head_area * 2.9 + 1.3)); + + // https://cfl.prusa3d.com/display/SLA/Double+Supports+-+Rectangles + // head 0.4mm cca 6.5 mm + // Use linear dependency to max_length_for_one_support_point + result.max_length_for_two_support_points = + static_cast(result.max_length_for_one_support_point * 3.9); + + // https://cfl.prusa3d.com/display/SLA/Double+Supports+-+Squares + // head 0.4mm cca (4.168 to 4.442) => from 3.6 to 4.2 + result.thin_max_width = static_cast(result.max_length_for_one_support_point * 2.5); + result.thick_min_width = static_cast(result.max_length_for_one_support_point * 2.15); + + // guessed from max_length_for_two_support_points to value 5.2mm + result.thin_max_distance = static_cast(result.max_length_for_two_support_points * 0.8); + + // guess from experiments documented above __(not verified values)__ + result.thick_inner_max_distance = result.max_length_for_two_support_points; // 6.5mm + result.thick_outline_max_distance = static_cast(result.max_length_for_two_support_points * 0.75); // 4.875mm + + result.minimal_distance_from_outline = result.head_radius; // 0.2mm + result.maximal_distance_from_outline = result.thin_max_distance / 3; // 1.73mm + result.min_part_length = result.thin_max_distance; // 5.2mm + + // Align support points + // TODO: propagate print resolution + result.minimal_move = scale_(0.1); // 0.1 mm is enough + // [in nanometers --> 0.01mm ], devide from print resolution to quater pixel is too strict + result.count_iteration = 30; // speed VS precission + result.max_align_distance = result.max_length_for_two_support_points / 2; + + verify(result); + return result; +} + +SampleConfig SampleConfigFactory::apply_density(const SampleConfig ¤t, float density) { + if (is_approx(density, 1.f)) + return current; + if (density < .1f) + density = .1f; // minimal 10% + + SampleConfig result = current; // copy + result.thin_max_distance = static_cast(current.thin_max_distance / density); // linear + result.thick_inner_max_distance = static_cast( // controll radius - quadratic + std::sqrt(sqr((double) current.thick_inner_max_distance) / density) + ); + result.thick_outline_max_distance = static_cast( + current.thick_outline_max_distance / density + ); // linear + // result.head_radius .. no change + // result.minimal_distance_from_outline .. no change + // result.maximal_distance_from_outline .. no change + // result.max_length_for_one_support_point .. no change + // result.max_length_for_two_support_points .. no change + verify(result); + return result; +} + +#ifdef USE_ISLAND_GUI_FOR_SETTINGS +std::optional SampleConfigFactory::gui_sample_config_opt; +SampleConfig &SampleConfigFactory::get_sample_config() { + // init config + if (!gui_sample_config_opt.has_value()) + // create default configuration + gui_sample_config_opt = sla::SampleConfigFactory::create(.4f); + return *gui_sample_config_opt; +} + +SampleConfig SampleConfigFactory::get_sample_config(float density) { + return apply_density(get_sample_config(), density); +} +#endif // USE_ISLAND_GUI_FOR_SETTINGS \ No newline at end of file diff --git a/src/libslic3r/SLA/SupportIslands/SampleConfigFactory.hpp b/src/libslic3r/SLA/SupportIslands/SampleConfigFactory.hpp new file mode 100644 index 0000000000..16286f4baf --- /dev/null +++ b/src/libslic3r/SLA/SupportIslands/SampleConfigFactory.hpp @@ -0,0 +1,43 @@ +#ifndef slic3r_SLA_SuppotstIslands_SampleConfigFactory_hpp_ +#define slic3r_SLA_SuppotstIslands_SampleConfigFactory_hpp_ + +#include +#include "SampleConfig.hpp" +#include "libslic3r/PrintConfig.hpp" + +//#define USE_ISLAND_GUI_FOR_SETTINGS + +namespace Slic3r::sla { + +/// +/// Factory to create configuration +/// +class SampleConfigFactory +{ +public: + SampleConfigFactory() = delete; + + static bool verify(SampleConfig &cfg); + static SampleConfig create(float support_head_diameter_in_mm); + static SampleConfig apply_density(const SampleConfig& cfg, float density); +#ifdef USE_ISLAND_GUI_FOR_SETTINGS +private: + // TODO: REMOVE IT. Do not use in production + // Global variable to temporary set configuration from GUI into SLA print steps + static std::optional gui_sample_config_opt; +public: + static SampleConfig &get_sample_config(); + + /// + /// Create scaled copy of sample config + /// + /// Scale for config values(minimal value is .1f) + /// 1.f .. no scale + /// .9f .. less support points (approx 90%) + /// 1.1f.. extend count of supports (approx to 110%) + /// Scaled configuration + static SampleConfig get_sample_config(float density); +#endif // USE_ISLAND_GUI_FOR_SETTINGS +}; +} // namespace Slic3r::sla +#endif // slic3r_SLA_SuppotstIslands_SampleConfigFactory_hpp_ diff --git a/src/libslic3r/SLA/SupportIslands/SupportIslandPoint.cpp b/src/libslic3r/SLA/SupportIslands/SupportIslandPoint.cpp new file mode 100644 index 0000000000..dc1282454f --- /dev/null +++ b/src/libslic3r/SLA/SupportIslands/SupportIslandPoint.cpp @@ -0,0 +1,202 @@ +#include "SupportIslandPoint.hpp" +#include "VoronoiGraphUtils.hpp" +#include "LineUtils.hpp" + +using namespace Slic3r::sla; + +SupportIslandPoint::SupportIslandPoint(Slic3r::Point point, Type type) + : point(std::move(point)), type(type) +{} + +bool SupportIslandPoint::can_move(const Type &type) +{ + static const std::set cant_move({ + Type::one_bb_center_point, + Type::one_center_point, + Type::two_points, + }); + return cant_move.find(type) == cant_move.end(); +} + +bool SupportIslandPoint::can_move() const { return can_move(type); } + +coord_t SupportIslandPoint::move(const Point &destination) +{ + Point diff = destination - point; + point = destination; + return abs(diff.x()) + abs(diff.y()); // Manhatn distance +} + +std::string SupportIslandPoint::to_string(const Type &type) +{ + static std::map type_to_string= + {{Type::one_center_point, "one_center_point"}, + {Type::two_points, "two_points"}, + {Type::two_points_backup, "two_points_backup"}, + {Type::one_bb_center_point,"one_bb_center_point"}, + {Type::thin_part, "thin_part"}, + {Type::thin_part_change, "thin_part_change"}, + {Type::thin_part_loop, "thin_part_loop"}, + {Type::thick_part_outline, "thick_part_outline"}, + {Type::thick_part_inner, "thick_part_inner"}, + {Type::bad_shape_for_vd, "bad_shape_for_vd"}, + {Type::permanent, "permanent"}, + {Type::undefined, "undefined"}}; + auto it = type_to_string.find(type); + if (it == type_to_string.end()) + return to_string(Type::undefined); + return it->second; +} + +/////////////// +// Point on VD +/////////////// + +SupportCenterIslandPoint::SupportCenterIslandPoint( + VoronoiGraph::Position position, + const SampleConfig * configuration, + Type type) + : SupportIslandPoint(VoronoiGraphUtils::create_edge_point(position), type) + , configuration(configuration) + , position(position) +{} + +coord_t SupportCenterIslandPoint::move(const Point &destination) +{ + // move only along VD + // TODO: Start respect minimum distance from outline !! + position = + VoronoiGraphUtils::align(position, destination, + configuration->max_align_distance); + Point new_point = VoronoiGraphUtils::create_edge_point(position); + return SupportIslandPoint::move(new_point); +} + +/////////////// +// Point on Outline +/////////////// + +SupportOutlineIslandPoint::SupportOutlineIslandPoint( + Position position, std::shared_ptr restriction, Type type) + : SupportIslandPoint(calc_point(position, *restriction), type) + , position(position) + , restriction(std::move(restriction)) +{} + +bool SupportOutlineIslandPoint::can_move() const { return true; } + +coord_t SupportOutlineIslandPoint::move(const Point &destination) +{ + size_t index = position.index; + MoveResult closest = create_result(index, destination); + + const double &length = restriction->lengths[position.index]; + double distance = (1.0 - position.ratio) * length; + while (distance < restriction->max_align_distance) { + auto next_index = restriction->next_index(index); + if (!next_index.has_value()) break; + index = *next_index; + update_result(closest, index, destination); + distance += restriction->lengths[index]; + } + + index = position.index; + distance = static_cast(position.ratio) * length; + while (distance < restriction->max_align_distance) { + auto prev_index = restriction->prev_index(index); + if (!prev_index.has_value()) break; + index = *prev_index; + update_result(closest, index, destination); + distance += restriction->lengths[index]; + } + + // apply closest result of move + this->position = closest.position; + return SupportIslandPoint::move(closest.point); +} + +Slic3r::Point SupportOutlineIslandPoint::calc_point(const Position &position, const Restriction &restriction) +{ + const Line &line = restriction.lines[position.index]; + Point direction = LineUtils::direction(line); + return line.a + direction * position.ratio; +} + +SupportOutlineIslandPoint::MoveResult SupportOutlineIslandPoint::create_result( + size_t index, const Point &destination) +{ + const Line &line = restriction->lines[index]; + double line_ratio_full = LineUtils::foot(line, destination); + double line_ratio = std::clamp(line_ratio_full, 0., 1.); + Position position(index, line_ratio); + Point point = calc_point(position, *restriction); + double distance_double = (point - destination).cast().norm(); + coord_t distance = static_cast(distance_double); + return MoveResult(position, point, distance); +} + +void SupportOutlineIslandPoint::update_result(MoveResult & result, + size_t index, + const Point &destination) +{ + const Line &line = restriction->lines[index]; + double line_ratio_full = LineUtils::foot(line, destination); + double line_ratio = std::clamp(line_ratio_full, 0., 1.); + Position position(index, line_ratio); + Point point = calc_point(position, *restriction); + Point diff = point - destination; + if (abs(diff.x()) > result.distance) return; + if (abs(diff.y()) > result.distance) return; + double distance_double = diff.cast().norm(); + coord_t distance = static_cast(distance_double); + if (result.distance > distance) { + result.distance = distance; + result.position = position; + result.point = point; + } +} + +//////////////////// +/// Inner Point +/////////////////////// + +SupportIslandInnerPoint::SupportIslandInnerPoint( + Point point, std::shared_ptr inner, Type type) + : SupportIslandPoint(point, type), inner(std::move(inner)) +{} + +coord_t SupportIslandInnerPoint::move(const Point &destination) { + + // IMPROVE: Do not move over island hole if there is no connected island. + // Can cause bad supported area in very special case. + for (const ExPolygon& inner_expolygon: *inner) + if (inner_expolygon.contains(destination)) + return SupportIslandPoint::move(destination); + + // find closest line cross area border + Vec2d v1 = (destination-point).cast(); + double closest_ratio = 1.; + Lines lines = to_lines(*inner); + for (const Line &line : lines) { + // line intersection + const Vec2d v2 = LineUtils::direction(line).cast(); + double denom = cross2(v1, v2); + // is line parallel + if (fabs(denom) < std::numeric_limits::epsilon()) continue; + + const Vec2d v12 = (point - line.a).cast(); + double nume1 = cross2(v2, v12); + double t1 = nume1 / denom; + if (t1 < 0. || t1 > closest_ratio) continue; // out of line + + double nume2 = cross2(v1, v12); + double t2 = nume2 / denom; + if (t2 < 0. || t2 > 1.0) continue; // out of contour + + closest_ratio = t1; + } + // no correct closest point --> almost parallel cross + if (closest_ratio >= 1.) return 0; + Point new_point = point + (closest_ratio * v1).cast(); + return SupportIslandPoint::move(new_point); +} \ No newline at end of file diff --git a/src/libslic3r/SLA/SupportIslands/SupportIslandPoint.hpp b/src/libslic3r/SLA/SupportIslands/SupportIslandPoint.hpp new file mode 100644 index 0000000000..5ca2b9393e --- /dev/null +++ b/src/libslic3r/SLA/SupportIslands/SupportIslandPoint.hpp @@ -0,0 +1,311 @@ +#ifndef slic3r_SLA_SuppotstIslands_SupportIslandPoint_hpp_ +#define slic3r_SLA_SuppotstIslands_SupportIslandPoint_hpp_ + +#include +#include +#include +#include "VoronoiGraph.hpp" +#include "SampleConfig.hpp" + +namespace Slic3r::sla { + +/// +/// DTO position with information about source of support point +/// +class SupportIslandPoint +{ +public: + enum class Type: unsigned char { + one_bb_center_point, // for island smaller than head radius + one_center_point, // small enough to support only by one support point + two_points, // island stretched between two points + two_points_backup, // same as before but forced after divide to thin&thick with small amoutn of points + thin_part, // point on thin part of island lay on VD + thin_part_change, // on the first edge -> together with change to thick part of island + thin_part_loop, // on the last edge -> loop into itself part of island + thick_part_outline, // keep position align with island outline + thick_part_inner, // point inside wide part, without restriction on move + + bad_shape_for_vd, // can't make a Voronoi diagram on the shape + + permanent, // permanent support point with static position + undefined + }; + + Type type; + Point point; + +public: + /// + /// constructor + /// + /// coordinate point inside a layer (in one slice) + /// type of support point + SupportIslandPoint(Point point, Type type = Type::undefined); + + /// + /// virtual destructor to be inheritable + /// + virtual ~SupportIslandPoint() = default; + + /// + /// static function to decide if type is possible to move or not + /// + /// type to distinguish + /// True when is possible to move, otherwise FALSE + static bool can_move(const Type &type); + + /// + /// static function to decide if type is possible to move or not + /// + /// type to distinguish + /// True when is possible to move, otherwise FALSE + virtual bool can_move() const; + + /// + /// Move position of support point close to destination + /// with support point restrictions + /// + /// Wanted position + /// Move distance + virtual coord_t move(const Point &destination); + + /// + /// Convert type to string value + /// + /// Input type + /// String type + static std::string to_string(const Type &type); +}; + +using SupportIslandPointPtr = std::unique_ptr; +using SupportIslandPoints = std::vector; + +/// +/// Support point with no move during aligning +/// +class SupportIslandNoMovePoint : public SupportIslandPoint +{ +public: + //constructor + using SupportIslandPoint::SupportIslandPoint; + + /// + /// Can move? + /// + /// FALSE + bool can_move() const override { return false; } + + /// + /// No move! + /// Should not be call. + /// + /// Wanted position + /// No move means zero distance + coord_t move(const Point &destination) override { return 0; } +}; + +/// +/// DTO Support point laying on voronoi graph edge +/// Restriction to move only on Voronoi graph +/// +class SupportCenterIslandPoint : public SupportIslandPoint +{ +public: + // Define position on voronoi graph + // FYI: Lose data when voronoi graph does NOT exist + VoronoiGraph::Position position; + + // hold pointer to configuration + // FYI: Lose data when configuration destruct + const SampleConfig *configuration; +public: + SupportCenterIslandPoint(VoronoiGraph::Position position, + const SampleConfig *configuration, + Type type = Type::thin_part); + + bool can_move() const override{ return true; } + coord_t move(const Point &destination) override; +}; + +/// +/// Support point laying on Outline of island +/// Restriction to move only on outline +/// +class SupportOutlineIslandPoint : public SupportIslandPoint +{ +public: + // definition of restriction + class Restriction; + + struct Position + { + // index of line form island outline - index into Restriction + // adress line inside inner polygon --> SupportOutline + size_t index; + + // define position on line by ratio + // from 0 (line point a) + // to 1 (line point b) + float ratio; + + Position(size_t index, float ratio) : index(index), ratio(ratio) {} + }; + Position position; + + + // store lines for allowed move - with distance from island source lines + std::shared_ptr restriction; + +public: + SupportOutlineIslandPoint(Position position, + std::shared_ptr restriction, + Type type = Type::thick_part_outline); + // return true + bool can_move() const override; + + /// + /// Move nearest to destination point + /// only along restriction lines + /// + change current position + /// + /// Wanted support position + /// move distance manhatn + coord_t move(const Point &destination) override; + + /// + /// Calculate 2d point belong to line position + /// + /// Define position on line from restriction + /// Hold lines + /// Position in 2d + static Point calc_point(const Position & position, + const Restriction &restriction); + + /// + /// Keep data for align support point on bordred of island + /// Define possible move of point along outline + /// IMPROVE: Should contain list of Points on outline. + /// (to keep maximal distance of neighbor points on outline) + /// + class Restriction + { + public: + // line restriction + // must be connected line.a == prev_line.b && line.b == next_line.a + Lines lines; + + // keep stored line lengths + // same size as lines + std::vector lengths; + + // maximal distance for search nearest line to destination point during aligning + coord_t max_align_distance; + + Restriction(Lines lines, + std::vector lengths, + coord_t max_align_distance) + : lines(lines) + , lengths(lengths) + , max_align_distance(max_align_distance) + { + assert(lines.size() == lengths.size()); + } + virtual ~Restriction() = default; + virtual std::optional next_index(size_t index) const = 0; + virtual std::optional prev_index(size_t index) const = 0; + }; + + class RestrictionLineSequence: public Restriction + { + public: + // inherit constructors + using Restriction::Restriction; + + virtual std::optional next_index(size_t index) const override + { + assert(index < lines.size()); + ++index; + if (index >= lines.size()) return {}; // index out of range + return index; + } + + virtual std::optional prev_index(size_t index) const override + { + assert(index < lines.size()); + if (index >= lines.size()) return {}; // index out of range + if (index == 0) return {}; // no prev line + return index - 1; + } + }; + + class RestrictionCircleSequence : public Restriction + { + public: + // inherit constructors + using Restriction::Restriction; + + virtual std::optional next_index(size_t index) const override + { + assert(index < lines.size()); + if (index >= lines.size()) return {}; // index out of range + ++index; + if (index == lines.size()) return 0; + return index; + } + + virtual std::optional prev_index(size_t index) const override + { + assert(index < lines.size()); + if (index >= lines.size()) return {}; // index out of range + if (index == 0) return lines.size() - 1; + return index - 1; + } + }; + +private: + // DTO for result of move + struct MoveResult + { + // define position on restriction line + Position position; + // point laying on restricted line + Point point; + // distance point on restricted line from destination point + coord_t distance; + + MoveResult(Position position, Point point, coord_t distance) + : position(position), point(point), distance(distance) + {} + }; + MoveResult create_result(size_t index, const Point &destination); + void update_result(MoveResult& result, size_t index, const Point &destination); +}; + +/// +/// Store pointer to inner ExPolygon for allowed move across island area +/// Give an option to move with point +/// +class SupportIslandInnerPoint: public SupportIslandPoint +{ + // define inner area of island where inner point could move during aligning + std::shared_ptr inner; +public: + SupportIslandInnerPoint(Point point, + std::shared_ptr inner, + Type type = Type::thick_part_inner); + + bool can_move() const override { return true; }; + + /// + /// Move nearest to destination point + /// only inside inner area + /// + change current position + /// + /// Wanted support position + /// move distance euclidean + coord_t move(const Point &destination) override; +}; + +} // namespace Slic3r::sla +#endif // slic3r_SLA_SuppotstIslands_SupportIslandPoint_hpp_ diff --git a/src/libslic3r/SLA/SupportIslands/UniformSupportIsland.cpp b/src/libslic3r/SLA/SupportIslands/UniformSupportIsland.cpp new file mode 100644 index 0000000000..67793d04df --- /dev/null +++ b/src/libslic3r/SLA/SupportIslands/UniformSupportIsland.cpp @@ -0,0 +1,2637 @@ +#include "UniformSupportIsland.hpp" + +#include +#include +#include +#include +#include + +#include + +#include // allign +#include // closest point +#include +#include "libslic3r/Geometry/Voronoi.hpp" +#include +#include +#include +#include +#include +#include +#include + +#include "VoronoiGraph.hpp" +#include "Parabola.hpp" +#include "IStackFunction.hpp" +#include "EvaluateNeighbor.hpp" +#include "ParabolaUtils.hpp" +#include "VoronoiGraphUtils.hpp" +#include "VectorUtils.hpp" +#include "LineUtils.hpp" +#include "PointUtils.hpp" + +#include "VoronoiDiagramCGAL.hpp" // aligning of points + +// comment definition of NDEBUG to enable assert() +//#define NDEBUG +//#define SLA_SAMPLE_ISLAND_UTILS_STORE_FIELD_TO_SVG_PATH "C:/data/temp/fields/island_<>.svg" +//#define SLA_SAMPLE_ISLAND_UTILS_STORE_PENINSULA_FIELD_TO_SVG_PATH "C:/data/temp/fields/peninsula_<>.svg" +//#define SLA_SAMPLE_ISLAND_UTILS_STORE_ALIGNED_TO_SVG_PATH "C:/data/temp/align/island_<>_aligned.svg" +//#define SLA_SAMPLE_ISLAND_UTILS_STORE_ALIGN_ONCE_TO_SVG_PATH "C:/data/temp/align_once/iter_<>.svg" +//#define SLA_SAMPLE_ISLAND_UTILS_DEBUG_CELL_DISTANCE_PATH "C:/data/temp/island_cell.svg" + +namespace { +using namespace Slic3r; +using namespace Slic3r::sla; + +/// +/// Replace first occurence of string +/// TODO: Generalize and Move into string utils +/// +/// +/// +/// +/// +std::string replace_first( + std::string s, + const std::string& toReplace, + const std::string& replaceWith +) { + std::size_t pos = s.find(toReplace); + if (pos == std::string::npos) return s; + s.replace(pos, toReplace.length(), replaceWith); + return s; +} + +/// +/// IMPROVE: use Slic3r::BoundingBox +/// Search for reference to an Expolygon with biggest contour +/// +/// Input +/// reference into expolygons +const ExPolygon &get_expolygon_with_biggest_contour(const ExPolygons &expolygons) { + assert(!expolygons.empty()); + const ExPolygon *biggest = &expolygons.front(); + for (size_t index = 1; index < expolygons.size(); ++index) { + const ExPolygon *current = &expolygons[index]; + if (biggest->contour.size() < current->contour.size()) + biggest = current; + } + return *biggest; +} + +/// +/// When radius of all points is smaller than max radius set output center and return true +/// +/// +/// +/// +/// True when Bounding box of points is smaller than max radius +bool get_center(const Points &points, coord_t max_radius, Point& output_center){ + if (points.size()<=2) + return false; + auto it = points.begin(); + Point min = *it; + Point max = *it; + for (++it; it != points.end(); ++it) { + if (min.x() > it->x()) { + min.x() = it->x(); + if (max.x() - min.x() > max_radius) + return false; + } else if(max.x() < it->x()) { + max.x() = it->x(); + if (max.x() - min.x() > max_radius) + return false; + } + if (min.y() > it->y()) { + min.y() = it->y(); + if (max.y() - min.y() > max_radius) + return false; + } else if (max.y() < it->y()) { + max.y() = it->y(); + if (max.y() - min.y() > max_radius) + return false; + } + } + + // prevent overflow of point range, no care about 1 size + output_center = min/2 + max/2; + return true; +} + +/// +/// Decrease level of detail +/// +/// Polygon to reduce count of points +/// Define progressivness of reduction +/// Simplified island +ExPolygon get_simplified(const ExPolygon &island, const SampleConfig &config) { + //// closing similar to FDM arachne do before voronoi inspiration in make_expolygons inside TriangleMeshSlicer + //float closing_radius = scale_(0.0499f); + //float offset_out = closing_radius; + //float offset_in = -closing_radius; + //ExPolygons closed_expolygons = offset2_ex({island}, offset_out, offset_in); // mitter + //ExPolygon closed_expolygon = get_expolygon_with_biggest_contour(closed_expolygons); + //// "Close" operation still create neighbor pixel for sharp triangle tip - cause VD issues + + ExPolygons simplified_expolygons = island.simplify(config.simplification_tolerance); + if (simplified_expolygons.empty()) + return island; + + ExPolygon biggest = get_expolygon_with_biggest_contour(simplified_expolygons); + + // NOTE: Order of polygon is different for Windows and Linux + // to unify behavior one have to sort holes + std::sort(biggest.holes.begin(), biggest.holes.end(), + // first sort by size of polygons than by coordinates of points + [](const Polygon &polygon1, const Polygon &polygon2) { + if (polygon1.size() > polygon2.size()) + return true; + if (polygon1.size() < polygon2.size()) + return false; + // NOTE: polygon1.size() == polygon2.size() + for (size_t point_index = 0; point_index < polygon1.size(); ++point_index) { + const Point &p1 = polygon1[point_index]; + const Point &p2 = polygon2[point_index]; + if (p1.x() > p2.x()) + return true; + if (p1.x() < p2.x()) + return false; + // NOTE: p1.x() == p2.x() + if (p1.y() > p2.y()) + return true; + if (p1.y() < p2.y()) + return false; + // NOTE: p1 == p2 check next point + } + return true; + }); + + return biggest; +} + +/// +/// Transform support point to slicer points +/// +Points to_points(const SupportIslandPoints &support_points){ + Points result; + result.reserve(support_points.size()); + std::transform(support_points.begin(), support_points.end(), std::back_inserter(result), + [](const std::unique_ptr &p) { return p->point; }); + return result; +} + +#ifdef OPTION_TO_STORE_ISLAND +SVG draw_island(const std::string &path, const ExPolygon &island, const ExPolygon &simplified_island) { + SVG svg(path, BoundingBox{island.contour.points}); + svg.draw_original(island); + svg.draw(island, "lightgray"); + svg.draw(simplified_island, "gray"); + return svg; +} +SVG draw_island_graph(const std::string &path, const ExPolygon &island, + const ExPolygon &simplified_island, const VoronoiGraph& skeleton, + const VoronoiGraph::ExPath& longest_path, const Lines& lines, const SampleConfig &config) { + SVG svg = draw_island(path, island, simplified_island); + VoronoiGraphUtils::draw(svg, skeleton, lines, config, true /*print Pointer address*/); + coord_t width = config.head_radius / 10; + VoronoiGraphUtils::draw(svg, longest_path.nodes, width, "orange"); + return svg; +} +#endif // OPTION_TO_STORE_ISLAND + +/// +/// keep same distances between support points +/// call once align +/// +/// In/Out support points to be alligned(min 3 points) +/// Area for sampling, border for position of samples +/// Sampling configuration +/// Maximal distance between neighbor points + +/// Term criteria for align: Minimal sample move and Maximal count of iteration +void align_samples(SupportIslandPoints &samples, const ExPolygon &island, const SampleConfig &config); + +void draw(SVG &svg, const SupportIslandPoints &supportIslandPoints, coord_t radius, bool write_type = true); + +/// +/// Create unique static support point +/// +/// Define position on VD +/// Type of support point +/// new created support point +SupportIslandPointPtr create_no_move_point( + const VoronoiGraph::Position &position, + SupportIslandPoint::Type type) +{ + Point point = VoronoiGraphUtils::create_edge_point(position); + return std::make_unique(point, type); +} + +/// +/// Find point lay on path with distance from first point on path +/// +/// Neighbor connected Nodes +/// Distance to final point +/// Position on VG with distance to first node when exists. +/// When distance is out of path return null optional +std::optional create_position_on_path( + const VoronoiGraph::Nodes &path, + double distance) +{ + const VoronoiGraph::Node *prev_node = nullptr; + double actual_distance = 0.; + for (const VoronoiGraph::Node *node : path) { + if (prev_node == nullptr) { // first call + prev_node = node; + continue; + } + const VoronoiGraph::Node::Neighbor *neighbor = + VoronoiGraphUtils::get_neighbor(prev_node, node); + actual_distance += neighbor->length(); + if (actual_distance >= distance) { + // over half point is on + double behind_position = actual_distance - distance; + double ratio = 1. - behind_position / neighbor->length(); + return VoronoiGraph::Position(neighbor, ratio); + } + prev_node = node; + } + + // distance must be inside path + // this means bad input params + assert(false); + return {}; // unreachable +} + +/// +/// Find first point lay on sequence of node +/// where widht are equal second params OR +/// distance from first node is exactly max distance +/// Depends which occure first +/// +/// Sequence of nodes, should be longer than max distance +/// Source lines for VG --> params for parabola. +/// Width of island(2x distance to outline) +/// Maximal distance from first node on path. +/// At end is set to actual distance from first node. +/// Position when exists +std::optional create_position_on_path( + const VoronoiGraph::Nodes &path, const Lines &lines, coord_t width, coord_t &max_distance) +{ + const VoronoiGraph::Node *prev_node = nullptr; + coord_t actual_distance = 0; + for (const VoronoiGraph::Node *node : path) { + if (prev_node == nullptr) { // first call + prev_node = node; + continue; + } + const VoronoiGraph::Node::Neighbor *neighbor = + VoronoiGraphUtils::get_neighbor(prev_node, node); + + if (width <= neighbor->max_width()) { + VoronoiGraph::Position position = VoronoiGraphUtils::get_position_with_width(neighbor, width, lines); + // set max distance to actual distance + coord_t rest_distance = position.calc_distance(); + coord_t distance = actual_distance + rest_distance; + if (max_distance > distance) { + max_distance = distance; + return position; + } + } + + actual_distance += static_cast(neighbor->length()); + if (actual_distance >= max_distance) { + // over half point is on + coord_t behind_position = actual_distance - max_distance; + double ratio = 1. - behind_position / neighbor->length(); + return VoronoiGraph::Position(neighbor, ratio); + } + prev_node = node; + } + + // distance must be inside path + // this means bad input params + assert(false); + return {}; // unreachable +} + +/// +/// Find point lay in center of path +/// Distance from this point to front of path +/// is same as distance to back of path +/// +/// Queue of neighbor nodes.(must be neighbor) +/// Type of result island point +/// Point laying on voronoi diagram +SupportIslandPointPtr create_middle_path_point( + const VoronoiGraph::Path &path, SupportIslandPoint::Type type) +{ + auto position_opt = create_position_on_path(path.nodes, path.length / 2); + if (!position_opt.has_value()) return nullptr; + return create_no_move_point(*position_opt, type); +} + +#ifndef NDEBUG +bool is_points_in_distance(const Point & p, + const Points &points, + double max_distance) +{ + return std::all_of(points.begin(), points.end(), + [p, max_distance](const Point &point) { + double d = (p - point).cast().norm(); + return d <= max_distance; + }); +} +#endif // NDEBUG + +void move_duplicit_positions(SupportIslandPoints &supports, const Points &prev_position) { + // remove duplicit points when exist + Points aligned = to_points(supports); + std::vector sorted(aligned.size()); + std::iota(sorted.begin(), sorted.end(), 0); + auto cmp_index = [&aligned](size_t a_index, size_t b_index) { + // sort by x and than by y + const Point &a = aligned[a_index]; + const Point &b = aligned[b_index]; + return a.x() < b.x() || (a.x() == b.x() && a.y() < b.y()); + }; + std::sort(sorted.begin(), sorted.end(), cmp_index); + + auto get_duplicit_index = [](const std::vector &sorted, const Points& aligned) { + const Point *prev_p = &aligned[sorted.front()]; + for (size_t i = 1; i < sorted.size(); ++i){ + if (const Point &p = aligned[sorted[i]]; *prev_p == p) { + return sorted[i]; + } else { + prev_p = &p; + } + } + return sorted.size(); + }; + + do { + size_t duplicit_index = get_duplicit_index(sorted, aligned); + if (duplicit_index >= sorted.size()) + return; // without duplicit points + + // divide last move to half + Point new_pos = prev_position[duplicit_index] / 2 + aligned[duplicit_index] / 2; + coord_t move_distance = supports[duplicit_index]->move(new_pos); + assert(move_distance > 0); // It must move + aligned[duplicit_index] = supports[duplicit_index]->point; // update aligned position + // IMPROVE: Resort duplicit index use std::rotate + std::sort(sorted.begin(), sorted.end(), cmp_index); + } while (true); // end when no duplicit index +} + +/// +/// once align +/// +/// In/Out support points to be alligned(min 3 points) +/// Area for sampling, border for position of samples +/// Sampling configuration +/// Maximal distance between neighbor points + +/// Term criteria for align: Minimal sample move and Maximal count of iteration +/// Maximal distance of move during aligning. +coord_t align_once( + SupportIslandPoints &supports, + const ExPolygon &island, + const SampleConfig &config) +{ + // IMPROVE: Do not calculate voronoi diagram out of island(only triangulate island) + // https://stackoverflow.com/questions/23823345/how-to-construct-a-voronoi-diagram-inside-a-polygon + // IMPROVE1: add accessor to point coordinate do not copy points + // IMPROVE2: add filter for create cell polygon only for moveable samples + Points points = to_points(supports); + coord_t max_distance = std::max(std::max( + config.thin_max_distance, + config.thick_inner_max_distance), + config.thick_outline_max_distance); + Polygons cell_polygons = create_voronoi_cells_cgal(points, max_distance); + +#ifdef SLA_SAMPLE_ISLAND_UTILS_STORE_ALIGN_ONCE_TO_SVG_PATH + std::string color_of_island = "#FF8080"; // LightRed. Should not be visible - cell color should overlap + std::string color_point_cell = "lightgray"; // bigger than island but NOT self overlap + std::string color_island_cell_intersection = "gray"; // Should full overlap island !! + std::string color_old_point = "lightblue"; // Center of island cell intersection + std::string color_wanted_point = "darkblue"; // Center of island cell intersection + std::string color_new_point = "blue"; // Center of island cell intersection + std::string color_static_point = "black"; + BoundingBox bbox(island.contour.points); + static int counter = 0; + Slic3r::SVG svg(replace_first(SLA_SAMPLE_ISLAND_UTILS_STORE_ALIGN_ONCE_TO_SVG_PATH, + "<>", std::to_string(counter++)).c_str(), bbox); + svg.draw(island, color_of_island); +#endif // SLA_SAMPLE_ISLAND_UTILS_STORE_ALIGN_ONCE_TO_SVG_PATH + + // Maximal move during align each loop of align it should decrease + coord_t max_move = 0; + for (size_t i = 0; i < supports.size(); i++) { + const Polygon &cell_polygon = cell_polygons[i]; + SupportIslandPointPtr &support = supports[i]; + +#ifdef SLA_SAMPLE_ISLAND_UTILS_STORE_ALIGN_ONCE_TO_SVG_PATH + if (!support->can_move()) { // draww freezed support points + svg.draw(support->point, color_static_point, config.head_radius); + svg.draw_text(support->point + Point(config.head_radius, 0), + SupportIslandPoint::to_string(support->type).c_str(), color_static_point.c_str()); + } +#endif // SLA_SAMPLE_ISLAND_UTILS_STORE_ALIGN_ONCE_TO_SVG_PATH + if (!support->can_move()) + continue; + + // polygon must be at least triangle + assert(cell_polygon.points.size() >= 3); + if (cell_polygon.points.size() < 3) + continue; // do not align point with invalid cell + + // IMPROVE: add intersection polygon with expolygon + Polygons intersections = intersection(cell_polygon, island); + const Polygon *island_cell = nullptr; + if (intersections.size() == 1) { + island_cell = &intersections.front(); + // intersection island and cell made by suppot point + // must generate polygon containing initial source for voronoi cell + // otherwise it is invalid voronoi diagram + assert(island_cell->contains(support->point)); + } else { + for (const Polygon &intersection : intersections) { + if (intersection.contains(support->point)) { + island_cell = &intersection; + break; + } + } + // intersection island and cell made by suppot point + // must generate polygon containing initial source for voronoi cell + // otherwise it is invalid voronoi diagram + assert(island_cell != nullptr); + if (island_cell == nullptr) + continue; + } + + // new aligned position for sample + Point island_cell_center = island_cell->centroid(); + +#ifdef SLA_SAMPLE_ISLAND_UTILS_DEBUG_CELL_DISTANCE_PATH + {SVG cell_svg(SLA_SAMPLE_ISLAND_UTILS_DEBUG_CELL_DISTANCE_PATH, island_cell->points); + cell_svg.draw(island, "lightgreen"); + cell_svg.draw(cell_polygon, "lightgray"); + cell_svg.draw(points, "darkgray", config.head_radius); + cell_svg.draw(*island_cell, "gray"); + cell_svg.draw(sample->point, "green", config.head_radius); + cell_svg.draw(island_cell_center, "black", config.head_radius);} +#endif //SLA_SAMPLE_ISLAND_UTILS_DEBUG_CELL_DISTANCE_PATH + // Check that still points do not have bigger distance from each other + assert(is_points_in_distance(island_cell_center, island_cell->points, + std::max(std::max(config.thick_inner_max_distance, config.thick_outline_max_distance), config.thin_max_distance))); + +#ifdef SLA_SAMPLE_ISLAND_UTILS_STORE_ALIGN_ONCE_TO_SVG_PATH + svg.draw(cell_polygon, color_point_cell); + svg.draw(*island_cell, color_island_cell_intersection); + svg.draw(Line(support->point, island_cell_center), color_wanted_point, config.head_radius / 5); + svg.draw(support->point, color_old_point, config.head_radius); + svg.draw(island_cell_center, color_wanted_point, config.head_radius); // wanted position +#endif // SLA_SAMPLE_ISLAND_UTILS_STORE_ALIGN_ONCE_TO_SVG_PATH + + // say samples to use its restriction to change posion close to center + coord_t act_move = support->move(island_cell_center); + if (max_move < act_move) + max_move = act_move; + +#ifdef SLA_SAMPLE_ISLAND_UTILS_STORE_ALIGN_ONCE_TO_SVG_PATH + svg.draw(support->point, color_new_point, config.head_radius); + svg.draw_text(support->point + Point(config.head_radius, 0), + SupportIslandPoint::to_string(support->type).c_str(), color_new_point.c_str() + ); +#endif // SLA_SAMPLE_ISLAND_UTILS_STORE_ALIGN_ONCE_TO_SVG_PATH + } + + move_duplicit_positions(supports, points); + return max_move; +} + +void align_samples(SupportIslandPoints &samples, const ExPolygon &island, const SampleConfig & config) +{ + if (samples.size() == 1) + return; // Do not align one support + + // Can't create voronoi for duplicit points + // Fix previous algo to not produce duplicit points + assert(!has_duplicate_points(to_points(samples))); + + bool exist_moveable = false; + for (const auto &sample : samples) { + if (sample->can_move()) { + exist_moveable = true; + break; + } + } + if (!exist_moveable) + return; // no support to align + + size_t count_iteration = config.count_iteration; // copy + coord_t max_move = 0; + while (--count_iteration > 1) { + max_move = align_once(samples, island, config); + if (max_move < config.minimal_move) break; + } + +#ifdef SLA_SAMPLE_ISLAND_UTILS_STORE_ALIGNED_TO_SVG_PATH + static int counter = 0; + SVG svg(replace_first(SLA_SAMPLE_ISLAND_UTILS_STORE_ALIGNED_TO_SVG_PATH, + "<>", std::to_string(counter++)).c_str(),BoundingBox(island.contour.points)); + svg.draw(island); + draw(svg, samples, config.head_radius); + svg.Close(); + std::cout << "Align use " << config.count_iteration - count_iteration + << " iteration and finish with precision " << unscale(max_move,0)[0] << + " mm" << std::endl; +#endif // SLA_SAMPLE_ISLAND_UTILS_STORE_ALIGNED_TO_SVG_PATH + +} + +void align_samples_with_permanent( + SupportIslandPoints &samples, const ExPolygon &island, const Points& permanent, const SampleConfig &config) +{ + assert(!permanent.empty()); + if (permanent.empty()) + return align_samples(samples, island, config); + + // detect whether add adding support points + size_t tolerance = 1 + size_t(permanent.size() * 0.1); // 1 + 10% of permanent points + bool extend_permanent = samples.size() > (permanent.size() + tolerance); + if (!extend_permanent) // use only permanent support points + return samples.clear(); + + // find closest samples to permanent support points + Points points; + points.reserve(samples.size()); + for (const SupportIslandPointPtr &p : samples) + points.push_back(p->point); + auto point_accessor = [&points](size_t idx, size_t dim) -> coord_t & { + return points[idx][dim]; }; + KDTreeIndirect<2, coord_t, decltype(point_accessor)> tree(point_accessor, samples.size()); + for (size_t i = 0; i < permanent.size(); ++i) { + std::array closests = find_closest_points<5>(tree, permanent[i]); + bool found_closest = false; + for (size_t idx : closests) { + if (idx >= samples.size()) + continue; // closest function return also size_t::max() + SupportIslandPointPtr &sample = samples[idx]; + if (sample->type == SupportIslandPoint::Type::permanent) + continue; // already used + sample->type = SupportIslandPoint::Type::permanent; + found_closest = true; + break; + } + if (!found_closest) { // backup solution when closest 5 fails, took first non permanent + for (const auto &sample : samples) + if (sample->type != SupportIslandPoint::Type::permanent) { + sample->type = SupportIslandPoint::Type::permanent; + break; + } + } + } + + // remove samples marked as permanent + samples.erase(std::remove_if(samples.begin(), samples.end(), [](const SupportIslandPointPtr &sample) { + return sample->type == SupportIslandPoint::Type::permanent; }), samples.end()); + + // add permanent into samples + for (const Point&p: permanent) + samples.push_back( + std::make_unique(p, SupportIslandPoint::Type::permanent)); + + align_samples(samples, island, config); + + // remove permanent samples inserted for aligning + samples.erase(std::remove_if(samples.begin(), samples.end(), [](const SupportIslandPointPtr &sample) { + return sample->type == SupportIslandPoint::Type::permanent; }), samples.end()); +} + +/// +/// Separation of thin and thick part of island +/// + +using VD = Slic3r::Geometry::VoronoiDiagram; +using Position = VoronoiGraph::Position; +using Positions = std::vector; +using Neighbor = VoronoiGraph::Node::Neighbor; + +/// +/// Define narrow part of island along voronoi skeleton +/// +struct ThinPart +{ + // Ceneter of longest path inside island part + // longest path is choosen from: + // shortest connection path between ends + // OR farest path to node from end + // OR farest path between nodes(only when ends are empty) + Position center; + + // Transition from tiny to thick part + // sorted by address of neighbor + Positions ends; +}; +using ThinParts = std::vector; + +/// +/// Define wide(fat) part of island along voronoi skeleton +/// +struct ThickPart +{ + // neighbor from thick part (twin of end with smallest source line index) + // edge from thin to thick, start.node is inside of thick part + const Neighbor* start; + + // Transition from thick to thin part + // sorted by address of neighbor + Positions ends; +}; +using ThickParts = std::vector; + +/// +/// Generate support points for thin part of island +/// +/// One thin part of island +/// [OUTPUT]Set of support points +/// Define density of support points +void create_supports_for_thin_part( + const ThinPart &part, SupportIslandPoints &results, const SampleConfig &config +) { + struct SupportIn + { + // want to create support in + coord_t support_in; // [nano meters] + // Neighbor to continue is not sampled yet + const Neighbor *neighbor; + }; + using SupportIns = std::vector; + + coord_t support_distance = config.thin_max_distance; + coord_t half_support_distance = support_distance / 2; + + // Current neighbor + SupportIn curr{half_support_distance + part.center.calc_distance(), part.center.neighbor}; + const Neighbor *twin_start = VoronoiGraphUtils::get_twin(*curr.neighbor); + coord_t twin_support_in = static_cast(twin_start->length()) - curr.support_in + + support_distance; + + // Process queue + SupportIns process; + process.push_back(SupportIn{twin_support_in, twin_start}); + bool is_first_neighbor = true; // help to skip checking first neighbor exist in process + + // Loop over thin part of island to create support points on the voronoi skeleton. + while (curr.neighbor != nullptr || !process.empty()) { + if (curr.neighbor == nullptr) { // need to pop next one from process + curr = process.back(); // copy + process.pop_back(); + } + + auto part_end_it = std::lower_bound(part.ends.begin(), part.ends.end(), curr.neighbor, + [](const Position &end, const Neighbor *n) { return end.neighbor < n; }); + bool is_end_neighbor = part_end_it != part.ends.end() && + curr.neighbor == part_end_it->neighbor; + + // add support on current neighbor + coord_t edge_length = (is_end_neighbor) ? part_end_it->calc_distance() : + static_cast(curr.neighbor->length()); + while (edge_length >= curr.support_in) { + double ratio = curr.support_in / curr.neighbor->length(); + VoronoiGraph::Position position(curr.neighbor, ratio); + results.push_back(std::make_unique( + position, &config, SupportIslandPoint::Type::thin_part_change)); + curr.support_in += support_distance; + } + curr.support_in -= edge_length; + + if (is_end_neighbor) { + // on the current neighbor lay part end(transition into neighbor Thick part) + if (curr.support_in < half_support_distance) + results.push_back(std::make_unique( + *part_end_it, &config, SupportIslandPoint::Type::thin_part)); + curr.neighbor = nullptr; + continue; + } + + // Voronoi has zero width only on contour of island + // IMPROVE: Add supports for edges, but not for + // * sharp corner + // * already near supported (How to decide which one to support?) + // if (curr.neighbor->min_width() == 0) create_edge_support(); + // OLD function name was create_sample_center_end() + + // detect loop on island part + const Neighbor *twin = VoronoiGraphUtils::get_twin(*curr.neighbor); + if (!is_first_neighbor) { // not first neighbor + if (auto process_it = std::find_if(process.begin(), process.end(), + [twin](const SupportIn &p) { return p.neighbor == twin; }); + process_it != process.end()) { // self loop detected + if (curr.support_in < half_support_distance) { + Position position{curr.neighbor, 1.}; // fine tune position by alignment + results.push_back(std::make_unique( + position, &config, SupportIslandPoint::Type::thin_part_loop)); + } + process.erase(process_it); + curr.neighbor = nullptr; + continue; + } + } else { + is_first_neighbor = false; + } + + // next neighbor is short cut to not push back and pop new_starts + const Neighbor *next_neighbor = nullptr; + for (const Neighbor &node_neighbor : curr.neighbor->node->neighbors) { + // Check wheather node is not previous one + if (twin == &node_neighbor) + continue; + if (next_neighbor == nullptr) { + next_neighbor = &node_neighbor; + continue; + } + process.push_back(SupportIn{curr.support_in, &node_neighbor}); + } + // NOTE: next_neighbor is null when no next neighbor + curr.neighbor = next_neighbor; + } +} + +// Data type object represents one island change from wide to tiny part +// It is stored inside map under source line index +// Help to create field from thick part of island +struct WideTinyChange{ + // new coordinate for line.b point + Point new_b; + // new coordinate for next line.a point + Point next_new_a; + // index to lines + size_t next_line_index; + + WideTinyChange(Point new_b, Point next_new_a, size_t next_line_index) + : new_b(new_b) + , next_new_a(next_new_a) + , next_line_index(next_line_index) + {} + + // is used only when multi wide tiny change are on same Line + struct SortFromAToB + { + LineUtils::SortFromAToB compare; + SortFromAToB(const Line &line) : compare(line) {} + bool operator()(const WideTinyChange &left, + const WideTinyChange &right) + { + return compare.compare(left.new_b, right.new_b); + } + }; +}; +using WideTinyChanges = std::vector; + +/// +/// Collect all source line indices from Voronoi Graph part +/// +/// input.node lay inside of part +/// Limits of part, should be accesibly only from one side +/// Source line indices of island part +std::vector get_line_indices(const Neighbor* input, const Positions& ends) { + std::vector indices; + // Process queue + std::vector process; + const Neighbor *current = input; + // Loop over thin part of island to create support points on the voronoi skeleton. + while (current != nullptr || !process.empty()) { + if (current == nullptr) { // need to pop next one from process + current = process.back(); // copy + process.pop_back(); + } + + const VD::edge_type *edge = current->edge; + indices.push_back(edge->cell()->source_index()); + indices.push_back(edge->twin()->cell()->source_index()); + + // Is current neighbor one of ends? + if(auto end_it = std::lower_bound(ends.begin(), ends.end(), current, + [](const Position &end, const Neighbor *n) { return end.neighbor < n; }); + end_it != ends.end() && current == end_it->neighbor){ + current = nullptr; + continue; + } + + // Exist current neighbor in process queue + const Neighbor *twin = VoronoiGraphUtils::get_twin(*current); + if (auto process_it = std::find_if(process.begin(), process.end(), + [&twin](const Neighbor *n) { return n == twin; }); + process_it != process.end()) { + process.erase(process_it); + current = nullptr; + continue; + } + + // search for next neighbor + const std::vector &node_neighbors = current->node->neighbors; + current = nullptr; + for (const Neighbor &node_neighbor : node_neighbors) { + // Check wheather node is not previous one + if (twin == &node_neighbor) continue; + if (current == nullptr) { + current = &node_neighbor; + continue; + } + process.push_back(&node_neighbor); + } + } + return indices; +} + +/// +/// Fix expolygon with hole bigger than contour +/// NOTE: when change contour and index it is neccesary also fix source indices +/// +/// [In/Out] expolygon +/// [OUT] source indices of island contour line creating field +/// True when contour is changed +bool set_biggest_hole_as_contour(ExPolygon &shape, std::vector &ids) { + Point contour_size = BoundingBox(shape.contour.points).size(); + Polygons &holes = shape.holes; + size_t contour_index = holes.size(); + for (size_t hole_index = 0; hole_index < holes.size(); ++hole_index) { + Point hole_size = BoundingBox(holes[hole_index].points).size(); + if (hole_size.x() < contour_size.x()) // X size should be enough + continue; // size is smaller it is really hole + contour_size = hole_size; + contour_index = hole_index; + } + if (contour_index == holes.size()) + return false; // contour is set correctly + + // some hole is bigger than contour and become contour + + // swap source indices + size_t contour_count = shape.contour.size(); + size_t hole_index_offset = contour_count; + for (size_t i = 0; i < contour_index; i++) + hole_index_offset += shape.holes[i].size(); + size_t hole_index_end = hole_index_offset + shape.holes[contour_index].size(); + + // swap contour with hole + Polygon tmp = holes[contour_index]; // copy + std::swap(tmp, shape.contour); + holes[contour_index] = std::move(tmp); + + // Temp copy of the old hole(newly contour) indices + std::vector contour_indices(ids.begin() + hole_index_offset, + ids.begin() + hole_index_end); // copy + ids.erase(ids.begin() + hole_index_offset, // remove copied contour + ids.begin() + hole_index_end); + ids.insert(ids.begin() + hole_index_offset, // insert old contour(newly hole) + ids.begin(), ids.begin() + contour_count); + ids.erase(ids.begin(), ids.begin() + contour_count); // remove old contour + ids.insert(ids.begin(), contour_indices.begin(), contour_indices.end()); + return true; +} + +/// +/// DTO represents Wide parts of island to sample +/// extend polygon with information about source lines +/// +struct Field { + // inner part of field, offseted border(island outline) by minimal_distance_from_outline + ExPolygons inner; + + // Flag for each line from inner, whether this line needs to be supported + // Converted from needs of border lines + // same size as to_lines(inner).size() + std::vector is_inner_outline; +}; + +/// +/// Create field +/// Offset island shape to inner part and transfer is_outline flags onto inner lines +/// +/// source field +/// distance from outline +/// When True than island line should be supported +/// So this information must be propagated to inner line +/// NOTE: same size as to_lines(island).size() +/// Field +Field create_field(const Slic3r::ExPolygon &island, float offset_delta, const std::vector& is_outline) +{ + ExPolygons inner = offset_ex(island, -offset_delta, ClipperLib::jtSquare); + if (inner.empty()) return {}; // no place for support point + + // TODO: Connect indexes for convert during creation of offset + // !! this implementation was fast for develop BUT NOT for running !! + // Use offset with Z coordinate and then connect by Z coordinate + const double angle_tolerace = 1e-4; + const double distance_tolerance = 20.; + Lines island_lines = to_lines(island); + Lines inner_lines = to_lines(inner); + size_t inner_line_index = 0; // continue where prev seach stop + // Convert index map from island index to inner index + size_t invalid_conversion = island_lines.size(); + std::vector inner_2_island(inner_lines.size(), invalid_conversion); + for (size_t island_line_index = 0; island_line_index < island_lines.size(); ++island_line_index) { + const Line &island_line = island_lines[island_line_index]; + Vec2d dir1 = LineUtils::direction(island_line).cast(); + dir1.normalize(); + size_t majorit_axis = (fabs(dir1.x()) > fabs(dir1.y())) ? 0 : 1; + coord_t start1 = island_line.a[majorit_axis]; + coord_t end1 = island_line.b[majorit_axis]; + if (start1 > end1) std::swap(start1, end1); + + size_t stop_inner_index = inner_line_index; + do { + ++inner_line_index; + if (inner_line_index == inner_lines.size()) + inner_line_index = 0; + const Line &inner_line = inner_lines[inner_line_index]; + + // check that line overlap its interval + coord_t start2 = inner_line.a[majorit_axis]; + coord_t end2 = inner_line.b[majorit_axis]; + if (start2 > end2) std::swap(start2, end2); + if (start1 > end2 || start2 > end1) continue; // not overlaped intervals + + Vec2d dir2 = LineUtils::direction(inner_line).cast(); + dir2.normalize(); + double angle = acos(dir1.dot(dir2)); + if (fabs(angle) > angle_tolerace) continue; // not similar direction + + // Improve: use only one side of offest !! + Point offset_middle = LineUtils::middle(inner_line); + double distance = island_line.perp_signed_distance_to(offset_middle); + if (fabs(distance - offset_delta) > distance_tolerance) + continue; // only parallel line with big distance + + // found first inner line + inner_2_island[inner_line_index] = island_line_index; + + // There could be also liar but we ignor that fact and accept first one + break; + } while (inner_line_index != stop_inner_index); + } + + // Create outline flags for inner lines + enum class Outline { // extend bool with unknown value + yes, + no, + unknown}; + std::vector inner_outline(inner_2_island.size(), Outline::unknown); + for (size_t inner_index = 0; inner_index < inner_2_island.size(); ++inner_index) { + size_t border_index = inner_2_island[inner_index]; + if (border_index == invalid_conversion) + continue; // inner_outline[inner_index] = Outline::unknown + inner_outline[inner_index] = is_outline[border_index]? Outline::yes : Outline::no; + } + + // limit unknown state + ExPolygonsIndices border_indices(ExPolygons{island}); + + size_t inner_offset = 0; // offset of current inner polygon inside of the lambda + auto remove_unknown = [&inner_offset, &inner_outline, &inner_2_island, &border_indices, invalid_conversion] + (size_t polygon_size) { + ScopeGuard offset_increase([&inner_offset, polygon_size] + { inner_offset += polygon_size; }); // increase offset for next polygon + + // collect sequence of unknown + size_t first_yes = 0; + while (first_yes < polygon_size && + inner_outline[first_yes + inner_offset] != Outline::yes) + ++first_yes; + + // polygon do not contain outline for sampling + if (first_yes == polygon_size) { + for (size_t i = 0; i < polygon_size; ++i) + inner_outline[i + inner_offset] = Outline::no; + return; + } + auto loop_increment = [polygon_size](size_t &i) { // loop incrementation of index + if (++i == polygon_size) i = 0; }; + auto set_to = [&inner_outline, inner_offset, loop_increment] + (size_t from, size_t to, Outline value) { + for (size_t i = from; i != to; loop_increment(i)) { + inner_outline[i + inner_offset] = value; + } + }; + + bool is_prev_outline = true; + int32_t first_polygon = border_indices.cvt(inner_2_island[first_yes + inner_offset]).polygon_index; + int32_t prev_polygon = first_polygon; + size_t start_unknown = polygon_size; // invalid value, current index is not Outline::unknown + size_t i = first_yes; + loop_increment(i); // one after first_yes + + // resolve sequence of unknown outline from start_unknown to end_unknown(polygon indices) + // + auto resolve_unknown = [&start_unknown, &is_prev_outline, &prev_polygon, &set_to] + (size_t end_unknown, bool is_current_outline, int32_t border_polygon_index) { + Outline value = (is_current_outline && // is current(after unknown) outline + is_prev_outline && // was (before unknown) outline + border_polygon_index == prev_polygon // is same border polygon + )? Outline::yes : Outline::no; + set_to(start_unknown, end_unknown, value); // change sequence of unknown to value + }; + for (; i != first_yes; loop_increment(i)) { + size_t inner_index = i + inner_offset; + Outline outline = inner_outline[inner_index]; + if (outline == Outline::unknown){ + if (start_unknown == polygon_size) + start_unknown = i; + continue; + } + size_t border_line_index = inner_2_island[inner_index]; + int32_t border_polygon_index = (border_line_index == invalid_conversion) ? -1 : + border_indices.cvt(static_cast(border_line_index)).polygon_index; + bool is_current_outline = outline == Outline::yes; + if (start_unknown != polygon_size) { + resolve_unknown(i, is_current_outline, border_polygon_index); + start_unknown = polygon_size; + } + prev_polygon = border_polygon_index; + is_prev_outline = is_current_outline; + } + if (start_unknown != polygon_size) // last unknown sequence + resolve_unknown(i, true, first_polygon); + }; + for (const ExPolygon &inner_expoly: inner) { + remove_unknown(inner_expoly.contour.size()); + for (const Polygon& hole: inner_expoly.holes) + remove_unknown(hole.size()); + } + assert(inner_offset == inner_lines.size()); + assert(std::none_of(inner_outline.begin(), inner_outline.end(), [](const Outline &o) { + return o == Outline::unknown; })); + std::vector is_inner_outline(inner_2_island.size(), false); + for (const Outline &o : inner_outline) + if (o == Outline::yes) + is_inner_outline[&o - &inner_outline.front()] = true; + return Field{inner, is_inner_outline}; +} + +#if defined(SLA_SAMPLE_ISLAND_UTILS_STORE_FIELD_TO_SVG_PATH) || defined(SLA_SAMPLE_ISLAND_UTILS_STORE_PENINSULA_FIELD_TO_SVG_PATH) +void draw(SVG &svg, const Field &field, const ExPolygon& border, bool draw_border_line_indexes = false, bool draw_field_source_indexes = true) { + const char *field_color = "red"; + const char *border_line_color = "blue"; + const char *inner_line_color = "lightgreen"; + const char *inner_line_outline_color = "darkgreen"; + svg.draw(border, field_color); + Lines border_lines = to_lines(border); + LineUtils::draw(svg, border_lines, border_line_color, 0., draw_border_line_indexes); + if (field.inner.empty()) + return; + // draw inner + Lines inner_lines = to_lines(field.inner); + LineUtils::draw(svg, inner_lines, inner_line_color, 0., draw_border_line_indexes); + if (draw_field_source_indexes) + for (auto &line : inner_lines) { + size_t index = &line - &inner_lines.front(); + Point middle_point = LineUtils::middle(line); + std::string text = std::to_string(index); + const char *color = inner_line_color; + if (field.is_inner_outline[&line - &inner_lines.front()]) { + LineUtils::draw(svg, line, inner_line_outline_color); + color = inner_line_outline_color; + } + svg.draw_text(middle_point, text.c_str(), color); + } +} +#endif // SLA_SAMPLE_ISLAND_UTILS_STORE_FIELD_TO_SVG_PATH || SLA_SAMPLE_ISLAND_UTILS_STORE_PENINSULA_FIELD_TO_SVG_PATH + +std::map create_wide_tiny_changes(const Positions& part_ends, const Lines &lines) { + std::map wide_tiny_changes; + // part_ends are already oriented + for (const Position &position : part_ends) { + Point p1, p2; + std::tie(p2, p1) = VoronoiGraphUtils::point_on_lines(position, lines); + const VD::edge_type *edge = position.neighbor->edge; + size_t i1 = edge->twin()->cell()->source_index(); + size_t i2 = edge->cell()->source_index(); + + // add sorted change from wide to tiny + // stored uder line index or line shorten in point b + WideTinyChange change(p1, p2, i2); + auto item = wide_tiny_changes.find(i1); + if (item == wide_tiny_changes.end()) { + wide_tiny_changes[i1] = {change}; + } else { + WideTinyChange::SortFromAToB pred(lines[i1]); + VectorUtils::insert_sorted(item->second, change, pred); + } + } + return wide_tiny_changes; +} + +// IMPROVE do not use pointers on node but pointers on Neighbor +Field create_thick_field(const ThickPart& part, const Lines &lines, const SampleConfig &config) +{ + // store shortening of outline segments + // line index, vector + std::map wide_tiny_changes = create_wide_tiny_changes(part.ends, lines); + + // connection of line on island + std::map b_connection = LineUtils::create_line_connection_over_b(lines); + + std::vector source_indices; + auto inser_point_b = [&lines, &b_connection, &source_indices] + (size_t &index, Points &points, std::set &done) + { + const Line &line = lines[index]; + points.push_back(line.b); + const auto &connection_item = b_connection.find(index); + assert(connection_item != b_connection.end()); + done.insert(index); + index = connection_item->second; + source_indices.push_back(index); + }; + + size_t source_index_for_change = lines.size(); + + /// + /// Insert change into + /// NOTE: separate functionality to be able force break from second loop + /// + /// island(ExPolygon) converted to lines + /// ... + /// False when change lead to close loop(into first change) otherwise True + auto insert_changes = [&wide_tiny_changes, &lines, &source_indices, source_index_for_change] + (size_t &index, Points &points, std::set &done, size_t input_index)->bool { + auto change_item = wide_tiny_changes.find(index); + while (change_item != wide_tiny_changes.end()) { + const WideTinyChanges &changes = change_item->second; + assert(!changes.empty()); + size_t change_index = 0; + if (!points.empty()) { // Not first point, could lead to termination + const Point &last_point = points.back(); + LineUtils::SortFromAToB pred(lines[index]); + bool no_change = false; + while (pred.compare(changes[change_index].new_b, last_point)) { + ++change_index; + if (change_index >= changes.size()) { + no_change = true; + break; + } + } + if (no_change) break; + + // Field ends with change into first index + if (change_item->first == input_index && + change_index == 0) { + return false; + } + } + const WideTinyChange &change = changes[change_index]; + // prevent double points + if (points.empty() || + !PointUtils::is_equal(points.back(), change.new_b)) { + points.push_back(change.new_b); + source_indices.push_back(source_index_for_change); + } else { + source_indices.back() = source_index_for_change; + } + // prevent double points + if (!PointUtils::is_equal(lines[change.next_line_index].b, + change.next_new_a)) { + points.push_back(change.next_new_a); + source_indices.push_back(change.next_line_index); + } + done.insert(index); + + auto is_before_first_change = [&wide_tiny_changes, input_index, &lines] + (const Point& point_on_input_line) { + // is current change into first index line lay before first change? + auto input_change_item = wide_tiny_changes.find(input_index); + if(input_change_item == wide_tiny_changes.end()) + return true; + + const WideTinyChanges &changes = input_change_item->second; + LineUtils::SortFromAToB pred(lines[input_index]); + for (const WideTinyChange &change : changes) { + if (pred.compare(change.new_b, point_on_input_line)) + // Exist input change before + return false; + } + // It is before first index + return true; + }; + + // change into first index - loop is finished by change + if (index != input_index && + input_index == change.next_line_index && + is_before_first_change(change.next_new_a)) { + return false; + } + + index = change.next_line_index; + change_item = wide_tiny_changes.find(index); + } + return true; + }; + + // all source line indices belongs to thick part of island + std::vector field_line_indices = get_line_indices(part.start, part.ends); + + // Collect outer points of field + Points points; + points.reserve(field_line_indices.size()); + std::vector outline_indexes; + outline_indexes.reserve(field_line_indices.size()); + size_t input_index1 = part.start->edge->cell()->source_index(); + size_t input_index2 = part.start->edge->twin()->cell()->source_index(); + size_t input_index = std::min(input_index1, input_index2); // Why select min index? + size_t outline_index = input_index; + // Done indexes is used to detect holes in field + std::set done_indices; // IMPROVE: use vector(size of lines count) with bools + do { + if (!insert_changes(outline_index, points, done_indices, input_index)) + break; + inser_point_b(outline_index, points, done_indices); + } while (outline_index != input_index); + + assert(points.size() >= 3); + ExPolygon border{Polygon{points}}; + // finding holes(another closed polygon) + if (done_indices.size() < field_line_indices.size()) { + for (const size_t &index : field_line_indices) { + if(done_indices.find(index) != done_indices.end()) continue; + // new hole + Points hole_points; + size_t hole_index = index; + do { + inser_point_b(hole_index, hole_points, done_indices); + } while (hole_index != index); + border.holes.emplace_back(hole_points); + } + // Set largest polygon as contour + set_biggest_hole_as_contour(border, source_indices); + } + std::vector is_border_outline; + is_border_outline.reserve(source_indices.size()); + for (size_t source_index : source_indices) + is_border_outline.push_back(source_index != source_index_for_change); + float delta = static_cast(config.minimal_distance_from_outline); + Field field = create_field(border, delta, is_border_outline); +#ifdef SLA_SAMPLE_ISLAND_UTILS_STORE_FIELD_TO_SVG_PATH + { + const char *source_line_color = "black"; + bool draw_source_line_indexes = true; + bool draw_border_line_indexes = false; + bool draw_field_source_indexes = true; + static int counter = 0; + SVG svg(replace_first(SLA_SAMPLE_ISLAND_UTILS_STORE_FIELD_TO_SVG_PATH, + "<>", std::to_string(counter++)).c_str(),LineUtils::create_bounding_box(lines)); + LineUtils::draw(svg, lines, source_line_color, 0., draw_source_line_indexes); + draw(svg, field, border, draw_border_line_indexes, draw_field_source_indexes); + } +#endif //SLA_SAMPLE_ISLAND_UTILS_STORE_FIELD_TO_SVG_PATH + assert(!field.inner.empty()); + return field; +} + +/// +/// Uniform sample expolygon area by points inside Equilateral triangle center +/// +/// Input area to sample.(scaled) +/// Distance between samples. +/// Uniform samples(scaled) +Slic3r::Points sample_expolygon(const ExPolygon &expoly, coord_t triangle_side){ + const Points &points = expoly.contour.points; + assert(!points.empty()); + // get y range + coord_t min_y = points.front().y(); + coord_t max_y = min_y; + for (const Point &point : points) { + if (min_y > point.y()) + min_y = point.y(); + else if (max_y < point.y()) + max_y = point.y(); + } + coord_t half_triangle_side = triangle_side / 2; + static const float coef2 = sqrt(3.) / 2.; + coord_t triangle_height = static_cast(std::round(triangle_side * coef2)); + + // IMPROVE: use line end y + Lines lines = to_lines(expoly); + // remove lines paralel with axe x + lines.erase(std::remove_if(lines.begin(), lines.end(), + [](const Line &l) { + return l.a.y() == l.b.y(); + }), lines.end()); + + // change line direction from top to bottom + for (Line &line : lines) + if (line.a.y() > line.b.y()) std::swap(line.a, line.b); + + // sort by a.y() + std::sort(lines.begin(), lines.end(), + [](const Line &l1, const Line &l2) -> bool { + return l1.a.y() < l2.a.y(); + }); + // IMPROVE: guess size and reserve points + Points result; + size_t start_index = 0; + bool is_odd = false; + for (coord_t y = min_y + triangle_height / 2; y < max_y; y += triangle_height) { + is_odd = !is_odd; + std::vector intersections; + bool increase_start_index = true; + for (auto line = std::begin(lines)+start_index; line != std::end(lines); ++line) { + const Point &b = line->b; + if (b.y() <= y) { + // removing lines is slow, start index is faster + // line = lines.erase(line); + if (increase_start_index) ++start_index; + continue; + } + increase_start_index = false; + const Point &a = line->a; + if (a.y() >= y) break; + float y_range = static_cast(b.y() - a.y()); + float x_range = static_cast(b.x() - a.x()); + float ratio = (y - a.y()) / y_range; + coord_t intersection = a.x() + + static_cast(x_range * ratio); + intersections.push_back(intersection); + } + assert(intersections.size() % 2 == 0); + std::sort(intersections.begin(), intersections.end()); + for (size_t index = 0; index + 1 < intersections.size(); index += 2) { + coord_t start_x = intersections[index]; + coord_t end_x = intersections[index + 1]; + if (is_odd) start_x += half_triangle_side; + coord_t div = start_x / triangle_side; + if (start_x > 0) div += 1; + coord_t x = div * triangle_side; + if (is_odd) x -= half_triangle_side; + while (x < end_x) { + result.emplace_back(x, y); + x += triangle_side; + } + } + } + return result; +} + +/// +/// Same as sample_expolygon but offseted by centroid and rotate by farrest point from centroid +/// +Slic3r::Points sample_expolygons_with_centering(const ExPolygons &expolys, coord_t triangle_side) { + Points result; + for (const ExPolygon &expoly : expolys) { + assert(!expoly.contour.empty()); + if (expoly.contour.size() < 3) + continue; + // to unify sampling of rotated expolygon offset and rotate pattern by centroid and farrest point + Point center = expoly.contour.centroid(); + Point extrem = expoly.contour.front(); // the farest point from center + // NOTE: ignore case with multiple same distance points + double extrem_distance_sq = -1.; + for (const Point &point : expoly.contour.points) { + Point from_center = point - center; + double distance_sq = from_center.cast().squaredNorm(); + if (extrem_distance_sq < distance_sq) { + extrem_distance_sq = distance_sq; + extrem = point; + } + } + double angle = atan2(extrem.y() - center.y(), extrem.x() - center.x()); + ExPolygon expoly_tr = expoly; // copy + expoly_tr.rotate(angle, center); + Points samples = sample_expolygon(expoly_tr, triangle_side); + for (Point &sample : samples) + sample.rotate(-angle, center); + append(result, samples); + } + return result; +} + +/// +/// create support points on border of field +/// +/// Input field +/// Parameters for sampling. +/// support for outline +SupportIslandPoints sample_outline(const Field &field, const SampleConfig &config){ + coord_t max_align_distance = config.max_align_distance; + coord_t sample_distance = config.thick_outline_max_distance; + SupportIslandPoints result; + + using RestrictionPtr = std::shared_ptr; + auto add_sample = [&result, sample_distance] + (size_t index, const RestrictionPtr& restriction, coord_t &last_support) { + const double &line_length_double = restriction->lengths[index]; + coord_t line_length = static_cast(std::round(line_length_double)); + while (last_support + line_length > sample_distance){ + float ratio = static_cast((sample_distance - last_support) / line_length_double); + SupportOutlineIslandPoint::Position position(index, ratio); + result.emplace_back(std::make_unique( + position, restriction, SupportIslandPoint::Type::thick_part_outline)); + last_support -= sample_distance; + } + last_support += line_length; + }; + auto add_circle_sample = [max_align_distance, sample_distance, &add_sample] + (const Polygon &polygon) { + // IMPROVE: find interesting points to start sampling + Lines lines = to_lines(polygon); + std::vector lengths; + lengths.reserve(lines.size()); + double sum_lengths = 0; + for (const Line &line : lines) { + double length = line.length(); + sum_lengths += length; + lengths.push_back(length); + } + + using Restriction = SupportOutlineIslandPoint::RestrictionCircleSequence; + auto restriction = std::make_shared(lines, lengths, max_align_distance); + coord_t last_support = std::min(static_cast(sum_lengths), sample_distance) / 2; + for (size_t index = 0; index < lines.size(); ++index) + add_sample(index, restriction, last_support); + }; + + // sample line sequence + auto add_lines_samples = [&add_sample, max_align_distance, sample_distance] + (const Lines &inner_lines, size_t first_index, size_t last_index) { + if (first_index >= inner_lines.size() || + last_index >= inner_lines.size()) { + // Invalid state caused by bad pairing of inner lines with outline contour + // Observed on field created from peninsula (not separated tiny parts) + // and different way to create the change for connection to land. + assert(false); + return; + } + + ++last_index; // index after last item + Lines lines; + // is over start ? + if (first_index > last_index) { + size_t count = last_index + (inner_lines.size() - first_index); + lines.reserve(count); + std::copy(inner_lines.begin() + first_index, + inner_lines.end(), + std::back_inserter(lines)); + std::copy(inner_lines.begin(), + inner_lines.begin() + last_index, + std::back_inserter(lines)); + } else { + size_t count = last_index - first_index; + lines.reserve(count); + std::copy(inner_lines.begin() + first_index, + inner_lines.begin() + last_index, + std::back_inserter(lines)); + } + + // IMPROVE: find interesting points to start sampling + std::vector lengths; + lengths.reserve(lines.size()); + double sum_lengths = 0; + for (const Line &line : lines) { + double length = line.length(); + sum_lengths += length; + lengths.push_back(length); + } + + using Restriction = SupportOutlineIslandPoint::RestrictionLineSequence; + auto restriction = std::make_shared(lines, lengths, max_align_distance); + + // CHECK: Is correct to has always one support on outline sequence? + // or no sample small sequence at all? + coord_t last_support = std::min(static_cast(sum_lengths), sample_distance) / 2; + for (size_t index = 0; index < lines.size(); ++index) { + add_sample(index, restriction, last_support); + } + }; + + auto sample_polygon = [&add_circle_sample, &add_lines_samples, + &is_outline = field.is_inner_outline] + (const Polygon &inner_polygon, size_t inner_offset) { + // weird inner shape to sample, + // investigate field.border offseting + assert(inner_polygon.size() >= 3); + if (inner_polygon.size() < 3) + return; // no shape to sample + + // contain polygon tiny wide change? + size_t first_change_index = inner_polygon.size(); + for (size_t polygon_index = 0; polygon_index < inner_polygon.size(); ++polygon_index) + if (!is_outline[polygon_index + inner_offset]) { + // found change from wide to tiny part + first_change_index = polygon_index; + break; + } + + // is polygon without change + if (first_change_index == inner_polygon.size()) + return add_circle_sample(inner_polygon); + + // exist change create line sequences + // initialize with non valid values + size_t inner_invalid = inner_polygon.size(); + // first and last index to inner lines + size_t inner_first = inner_invalid; // + size_t inner_last = inner_invalid; + size_t stop_index = first_change_index; + if (stop_index == 0) // when check inner_index contain index after last item + stop_index = inner_polygon.size(); + + size_t inner_index = first_change_index; + do { // search for first outline index after change + ++inner_index; + if (inner_index == inner_polygon.size()) { + inner_index = 0; + // Detect that whole polygon is not peninsula outline(coast) + if (first_change_index == 0) + return; // Polygon do not contain edge to support. + } + } while (!is_outline[inner_index + inner_offset]); + + const Lines inner_lines = to_lines(inner_polygon); + for (;inner_index != stop_index; ++inner_index) { + if (inner_index == inner_lines.size()) + inner_index = 0; + + // not all inner lines has corresponding field line + // same has more than one field line + if (!is_outline[inner_index + inner_offset]) { // non outline part + if (inner_first == inner_invalid) continue; + // create Restriction object + add_lines_samples(inner_lines, inner_first, inner_last); + inner_first = inner_invalid; + inner_last = inner_invalid; + continue; + } + + inner_last = inner_index; + // initialize first index + if (inner_first == inner_invalid) inner_first = inner_last; + } + if (inner_first != inner_invalid) + add_lines_samples(inner_lines, inner_first, inner_last); + }; + + // No inner space to sample + if (field.inner.empty() || field.inner.front().contour.size() < 3) + return result; + + // Sample inner outlines + size_t index_offset = 0; + for (const ExPolygon & inner: field.inner) { + sample_polygon(inner.contour, index_offset); + index_offset += inner.contour.size(); + for (const Polygon &hole: inner.holes) { + sample_polygon(hole, index_offset); + index_offset += hole.size(); + } + } + return result; +} + +/// +/// Create field from thick part of island +/// Add support points on field contour(uniform step) +/// Add support points into inner part of field (grind) +/// +/// Define thick part of VG +/// OUTPUT support points +/// Island contour(with holes) +/// Define support density (by grid size and contour step) +void create_supports_for_thick_part(const ThickPart &part, SupportIslandPoints &results, + const Lines &lines, const SampleConfig &config) { + // Create field for thick part of island + Field field = create_thick_field(part, lines, config); + if (field.inner.empty()) + return; // no inner part + SupportIslandPoints outline_support = sample_outline(field, config); + results.insert(results.end(), + std::move_iterator(outline_support.begin()), + std::move_iterator(outline_support.end())); + // Inner must survive after sample field for aligning supports(move along outline) + auto inner = std::make_shared(field.inner); + Points inner_points = sample_expolygons_with_centering(*inner, config.thick_inner_max_distance); + std::transform(inner_points.begin(), inner_points.end(), std::back_inserter(results), + [&](const Point &point) { + return std::make_unique( + point, inner, SupportIslandPoint::Type::thick_part_inner); + }); +} + +// Search for interfaces +// 1. thin to min_wide +// 2. min_wide to max_center +// 3. max_center to Thick +enum class IslandPartType { thin, middle, thick }; + +// transition into neighbor part +struct IslandPartChange { + // position on the way out of island part + // Position::Neighbor::node is target(twin neighbor has source) + // Position::ration define position on connection between nodes + Position position; + size_t part_index; +}; +using IslandPartChanges = std::vector; + +/// +/// Part of island with interfaces defined by positions +/// +struct IslandPart { + // type of island part { thin | middle | thick } + IslandPartType type; + + // Positions and index of island part change + IslandPartChanges changes; + + // sum of all lengths inside of part + // IMPROVE1: Separate calculation localy into function merge_middle_parts_into_biggest_neighbor + // IMPROVE2: better will be length of longest path + // Used as rule to connect(merge) middle part of island to its biggest neighbour + // NOTE: No solution for island with 2 biggest neighbors with same sum_lengths. + coord_t sum_lengths = 0; +}; +using IslandParts = std::vector; + +/// +/// Data for process island parts' separation +/// +struct ProcessItem { + // previously processed island node + const VoronoiGraph::Node *prev_node = nullptr; + + // current island node to investigate neighbors + const VoronoiGraph::Node *node = nullptr; + + // index of island part stored in island_parts + // NOTE: Can't use reference because of vector reallocation + size_t i = std::numeric_limits::max(); +}; +using ProcessItems = std::vector; + +/// +/// Add new island part +/// +/// Already existing island parts +/// Source part index +/// Type for new added part +/// Edge where appear change from one state to another +/// min or max(thick_min_width, thin_max_width) +/// Island border +/// Minimal Island part length +/// index of new part inside island_parts +size_t add_part( + IslandParts &island_parts, + size_t part_index, + IslandPartType to_type, + const Neighbor *neighbor, + coord_t limit, + const Lines &lines, + const SampleConfig &config +) { + Position position = VoronoiGraphUtils::get_position_with_width(neighbor, limit, lines); + + // Do not create part, when it is too close to island contour + if (VoronoiGraphUtils::ends_in_distanace(position, config.min_part_length)) + return part_index; // too close to border to add part, nothing to add + + size_t new_part_index = island_parts.size(); + const Neighbor *twin = VoronoiGraphUtils::get_twin(*neighbor); + Position twin_position(twin, 1. - position.ratio); + + if (new_part_index == 1 && + VoronoiGraphUtils::ends_in_distanace(twin_position, config.min_part_length)) { + // Exist only initial island + // NOTE: First island part is from start shorter than SampleConfig::min_part_length + // Which is different to rest of island. + assert(island_parts.size() == 1); + assert(island_parts.front().changes.empty()); + // First island is too close to border to create new island part + // First island is initialy set set thin, + // but correct type is same as type in short length distance from start + island_parts.front().type = to_type; + return part_index; + } + + island_parts[part_index].changes.push_back({position, new_part_index}); + // NOTE: ignore multiple position on same neighbor + island_parts[part_index].sum_lengths += position.calc_distance(); + + coord_t sum_lengths = twin_position.calc_distance(); + IslandPartChanges changes{IslandPartChange{twin_position, part_index}}; + island_parts.push_back({to_type, changes, sum_lengths}); + return new_part_index; +} + +/// +/// Detect interface between thin, middle and thick part of island +/// +/// Already existing parts +/// current part index +/// current neigbor to investigate +/// Island contour +/// Configuration of hysterezis +/// Next part index +size_t detect_interface(IslandParts &island_parts, size_t part_index, const Neighbor *neighbor, const Lines &lines, const SampleConfig &config) { + // Range for of hysterezis between thin and thick part of island + coord_t min = config.thick_min_width; + coord_t max = config.thin_max_width; + + size_t next_part_index = part_index; + switch (island_parts[part_index].type) { + case IslandPartType::thin: + // Near contour is type permanent no matter of width + // assert(neighbor->min_width() <= min); + if (neighbor->max_width() < min) break; // still thin part + next_part_index = add_part(island_parts, part_index, IslandPartType::middle, neighbor, min, lines, config); + if (neighbor->max_width() < max) return next_part_index; // no thick part + return add_part(island_parts, next_part_index, IslandPartType::thick, neighbor, max, lines, config); + case IslandPartType::middle: + // assert(neighbor->min_width() >= min || neighbor->max_width() <= max); + if (neighbor->min_width() < min) { + return add_part(island_parts, part_index, IslandPartType::thin, neighbor, min, lines, config); + } else if (neighbor->max_width() > max) { + return add_part(island_parts, part_index, IslandPartType::thick, neighbor, max, lines, config); + } + break;// still middle part + case IslandPartType::thick: + //assert(neighbor->max_width() >= max); + if (neighbor->max_width() > max) break; // still thick part + next_part_index = add_part(island_parts, part_index, IslandPartType::middle, neighbor, max, lines, config); + if (neighbor->min_width() > min) return next_part_index; // no thin part + return add_part(island_parts, next_part_index, IslandPartType::thin, neighbor, min, lines, config); + default: assert(false); // unknown part type + } + + // without new interface between island parts + island_parts[part_index].sum_lengths += static_cast(neighbor->length()); + return part_index; +} + +/// +/// Merge two island parts defined by index +/// NOTE: Do not sum IslandPart::sum_lengths on purpose to be independent on the merging order +/// +/// All parts +/// Merge into +/// Merge from +void merge_island_parts(IslandParts &island_parts, size_t index, size_t remove_index){ + // It is better to remove bigger index, not neccessary + assert(index < remove_index); + // merge part interfaces + IslandPartChanges &changes = island_parts[index].changes; + IslandPartChanges &remove_changes = island_parts[remove_index].changes; + + // remove changes back to merged part + auto remove_changes_end = std::remove_if(remove_changes.begin(), remove_changes.end(), + [i=index](const IslandPartChange &change) { return change.part_index == i; }); + + // remove changes into removed part + changes.erase(std::remove_if(changes.begin(), changes.end(), + [i=remove_index](const IslandPartChange &change) { return change.part_index == i; }), + changes.end()); + + // move changes from remove part to merged part + changes.insert(changes.end(), + std::move_iterator(remove_changes.begin()), + std::move_iterator(remove_changes_end)); + + // remove island part + island_parts.erase(island_parts.begin() + remove_index); + + // fix indices inside island part changes + for (IslandPart &island_part : island_parts) { + for (IslandPartChange &change : island_part.changes) { + if (change.part_index == remove_index) + change.part_index = index; + else if (change.part_index > remove_index) + --change.part_index; + } + } +} + +/// +/// When apper loop back to already processed part of island graph this function merge island parts +/// +/// All island parts +/// To fix index +/// Index into island parts to merge +/// Index into island parts to merge +/// Queue of future processing +void merge_parts_and_fix_process(IslandParts &island_parts, + ProcessItem &item, size_t index, size_t remove_index, ProcessItems &process) { + if (remove_index == index) return; // nothing to merge, loop connect to itself + if (remove_index < index) // remove part with bigger index + std::swap(remove_index, index); + + // Merged parts should be the same state, it is essential for alhorithm + // Only first island part changes its type, but only before first change + assert(island_parts[index].type == island_parts[remove_index].type); + island_parts[index].sum_lengths += island_parts[remove_index].sum_lengths; + merge_island_parts(island_parts, index, remove_index); + + // fix indices in process queue + for (ProcessItem &p : process) + if (p.i == remove_index) + p.i = index; // swap to new index + else if (p.i > remove_index) + --p.i; // decrease index + + // fix index for current item + if (item.i > remove_index) + --item.i; // decrease index +} + +void merge_middle_parts_into_biggest_neighbor(IslandParts& island_parts) { + // Connect parts till there is no middle parts + for (size_t index = 0; index < island_parts.size(); ++index) { + const IslandPart &island_part = island_parts[index]; + if (island_part.type != IslandPartType::middle) continue; // only middle parts + // there must be change into middle part island always start as thin part + assert(!island_part.changes.empty()); + if (island_part.changes.empty()) continue; // weird situation + // find biggest neighbor island part + auto max_change = std::max_element(island_part.changes.begin(), island_part.changes.end(), + [&island_parts](const IslandPartChange &a, const IslandPartChange &b) { + return island_parts[a.part_index].sum_lengths < + island_parts[b.part_index].sum_lengths;}); + + // set island type by merged one (Thin OR Thick) + island_parts[index].type = island_parts[max_change->part_index].type; + + size_t merged_index = index; + size_t remove_index = max_change->part_index; + if (merged_index > remove_index) + std::swap(merged_index, remove_index); + + // NOTE: be carefull, function remove island part inside island_parts + merge_island_parts(island_parts, merged_index, remove_index); + --index; // on current index could be different island part + } +} + +void merge_same_neighbor_type_parts(IslandParts &island_parts) { + // connect neighbor parts with same type + for (size_t island_part_index = 0; island_part_index < island_parts.size(); ++island_part_index) { + while (true) { + const IslandPart &island_part = island_parts[island_part_index]; + assert(island_part.type != IslandPartType::middle); // only thin or thick parts + const IslandPartChanges &changes = island_part.changes; + auto change_it = std::find_if(changes.begin(), changes.end(), + [&island_parts, type = island_part.type](const IslandPartChange &change) { + assert(change.part_index < island_parts.size()); + return island_parts[change.part_index].type == type;}); + if (change_it == changes.end()) break; // no more changes + merge_island_parts(island_parts, island_part_index, change_it->part_index); + } + } +} + +/// +/// Find shortest distances between changes (combination of changes) +/// and choose the longest distance or farest node distance from changes +/// +/// transition into different part island +/// [optional]Center of longest path +/// Length of island part defined as longest distance on graph inside part +coord_t get_longest_distance(const IslandPartChanges& changes, Position* center = nullptr) { + const Neighbor *front_twin = VoronoiGraphUtils::get_twin(*changes.front().position.neighbor); + if (changes.size() == 2 && front_twin == changes.back().position.neighbor) { + // Special case when part lay only on one neighbor + if (center != nullptr) { + *center = changes.front().position;// copy + center->ratio = (center->ratio + changes.back().position.ratio)/2; + } + return static_cast(changes.front().position.neighbor->length() * + (1 - changes.front().position.ratio - changes.back().position.ratio)); + } + + struct ShortestDistance{ + coord_t distance; + size_t prev_node_distance_index; + }; + using ShortestDistances = std::vector; + // for each island part node find distance to changes + struct NodeDistance { + // island part node + const VoronoiGraph::Node *node; + // shortest distance to node from change + ShortestDistances shortest_distances; // size == changes.size() + }; + using NodeDistances = std::vector; + NodeDistances node_distances; + + const coord_t no_distance = std::numeric_limits::max(); + const size_t no_index = std::numeric_limits::max(); + size_t count = changes.size(); + for (const IslandPartChange &change : changes) { + const VoronoiGraph::Node *node = VoronoiGraphUtils::get_twin(*change.position.neighbor)->node; + size_t change_index = &change - &changes.front(); + coord_t distance = change.position.calc_distance(); + if (auto node_distance_it = std::find_if(node_distances.begin(), node_distances.end(), + [&node](const NodeDistance &node_distance) { return node_distance.node == node;}); + node_distance_it != node_distances.end()) { // multiple changes has same nearest node + ShortestDistance &shortest_distance = node_distance_it->shortest_distances[change_index]; + assert(shortest_distance.distance == no_distance); + assert(shortest_distance.prev_node_distance_index == no_index); + shortest_distance.distance = distance; + continue; // Do not add twice into node_distances + } + ShortestDistances shortest_distances(count, ShortestDistance{no_distance, no_index}); + shortest_distances[change_index].distance = distance; + node_distances.push_back(NodeDistance{node, std::move(shortest_distances)}); + } + + // use sorted changes for faster check of neighbors + IslandPartChanges sorted_changes = changes; // copy + std::sort(sorted_changes.begin(), sorted_changes.end(), + [](const IslandPartChange &a, const IslandPartChange &b) { + return a.position.neighbor < b.position.neighbor; + }); + auto exist_part_change_for_neighbor = [&sorted_changes](const Neighbor *neighbor) { + auto it = std::lower_bound(sorted_changes.begin(), sorted_changes.end(), neighbor, + [](const IslandPartChange &change, const Neighbor *neighbor_) { + return change.position.neighbor < neighbor_; }); + if (it == sorted_changes.end()) return false; + return it->position.neighbor == neighbor; + }; + + // Queue of island nodes to propagate shortest distance into their neigbors + // contain indices into node_distances + std::vector process; + for (size_t i = 1; i < node_distances.size(); i++) process.push_back(i); // zero index is start + size_t next_distance_index = 0; // zero index is start + size_t current_node_distance_index = -1; + const Neighbor *prev_neighbor = front_twin; + // propagate distances into neighbors + while (true /* next_distance_index < node_distances.size()*/) { + current_node_distance_index = next_distance_index; + next_distance_index = -1; // set to no value ... index > node_distances.size() + for (const Neighbor &neighbor : node_distances[current_node_distance_index].node->neighbors) { + if (&neighbor == prev_neighbor) continue; + if (exist_part_change_for_neighbor(&neighbor)) + continue; // change is search graph limit + + // IMPROVE: use binary search + auto node_distance_it = std::find_if(node_distances.begin(), node_distances.end(), + [node = neighbor.node](const NodeDistance& d) { + return d.node == node;} ); + if (node_distance_it == node_distances.end()) { + // create new node distance + ShortestDistances new_shortest_distances = + node_distances[current_node_distance_index].shortest_distances; // copy + for (ShortestDistance &d : new_shortest_distances) + if (d.distance != no_distance) { + d.distance += static_cast(neighbor.length()); + d.prev_node_distance_index = current_node_distance_index; + } + if (next_distance_index < node_distances.size()) + process.push_back(next_distance_index); // store for next processing + next_distance_index = node_distances.size(); + prev_neighbor = VoronoiGraphUtils::get_twin(neighbor); + // extend node distances (NOTE: invalidate addresing into node_distances) + node_distances.push_back(NodeDistance{neighbor.node, new_shortest_distances}); + continue; + } + + bool exist_distance_change = false; + // update distances + for (size_t i = 0; i < count; ++i) { + const ShortestDistance &d = node_distances[current_node_distance_index] + .shortest_distances[i]; + if (d.distance == no_distance) continue; + coord_t new_distance = d.distance + static_cast(neighbor.length()); + if (ShortestDistance ¤t_distance = node_distance_it->shortest_distances[i]; + current_distance.distance > new_distance) { + current_distance.distance = new_distance; + current_distance.prev_node_distance_index = current_node_distance_index; + exist_distance_change = true; + } + } + if (!exist_distance_change) + continue; // no change in distances + + size_t item_index = node_distance_it - node_distances.begin(); + // process store unique indices into node_distances + if(std::find(process.begin(), process.end(), item_index) != process.end()) + continue; // already in process + + if (next_distance_index < node_distances.size()) + process.push_back(next_distance_index); // store for next processing + next_distance_index = item_index; + prev_neighbor = VoronoiGraphUtils::get_twin(neighbor); + } + + if (next_distance_index >= node_distances.size()){ + if (process.empty()) + break; // no more nodes to process + next_distance_index = process.back(); + process.pop_back(); + prev_neighbor = nullptr; // do not know previous neighbor + continue; + } + } + + // find farest distance node from changes + coord_t farest_from_change = 0; + size_t change_index = 0; + const NodeDistance *farest_distnace = &node_distances.front(); + for (const NodeDistance &node_distance : node_distances) + for (const ShortestDistance& d : node_distance.shortest_distances) + if (farest_from_change < d.distance) { + farest_from_change = d.distance; + change_index = &d - &node_distance.shortest_distances.front(); + farest_distnace = &node_distance; + } + + // farest distance between changes + // till node distances do not change order than index of change is index of closest node of change + size_t source_change = count; + for (size_t i = 0; i < (count-1); ++i) { + const NodeDistance &node_distance = node_distances[i]; + const ShortestDistance &distance_to_change = node_distance.shortest_distances[i]; + for (size_t j = i+1; j < count; ++j) { + coord_t distance = node_distance.shortest_distances[j].distance + distance_to_change.distance; + if (farest_from_change < distance) { + // this change is farest from other changes + farest_from_change = distance; + change_index = j; + source_change = i; + farest_distnace = &node_distance; + } + } + } + + // center is not needed so return only farest distance + if (center == nullptr) + return farest_from_change; + + // Next lines are for calculation of center for longest path + coord_t half_distance = farest_from_change / 2; + + // check if center is on change neighbor + auto is_ceneter_on_change_neighbor = [&changes, center, half_distance](size_t change_index) { + if (change_index >= changes.size()) + return false; + const Position &position = changes[change_index].position; + if (position.calc_distance() < half_distance) + return false; + // center lay on neighbour with change + center->neighbor = position.neighbor; + center->ratio = position.ratio - half_distance / position.neighbor->length(); + return true; + }; + if (is_ceneter_on_change_neighbor(source_change) || + is_ceneter_on_change_neighbor(change_index)) + return farest_from_change; + + const NodeDistance *prev_node_distance = farest_distnace; + const NodeDistance *node_distance = nullptr; + // iterate over longest path to find center(half distance) + while (prev_node_distance->shortest_distances[change_index].distance >= half_distance) { + node_distance = prev_node_distance; + size_t prev_index = node_distance->shortest_distances[change_index].prev_node_distance_index; + // case with center on change neighbor is already handled, so prev_index should be valid + assert(prev_index != no_index && prev_indexshortest_distances[change_index].distance >= half_distance); + assert(prev_node_distance->shortest_distances[change_index].distance <= half_distance); + coord_t to_half_distance = half_distance - node_distance->shortest_distances[change_index].distance; + // find neighbor between node_distance and prev_node_distance + for (const Neighbor &n : node_distance->node->neighbors) { + if (n.node != prev_node_distance->node) + continue; + center->neighbor = &n; + center->ratio = to_half_distance / n.length(); + return farest_from_change; + } + + // weird situation when center is not found + assert(false); + return farest_from_change; +} + +/// +/// Remove island part with index +/// Merge all neighbors of deleted part together and create merged part on lowest index of merged parts +/// +/// All existing island parts with type only thin OR thick +/// index of island part to remove +/// modified part index and all removed part indices +std::pair> merge_negihbor(IslandParts &island_parts, size_t index) { + // merge all neighbors into one part + std::vector remove_indices; + const IslandPartChanges &changes = island_parts[index].changes; + // all neighbor should be the same type which is different to current one. + assert(std::find_if(changes.begin(), changes.end(), [&island_parts, type = island_parts[index].type] + (const IslandPartChange &c) { return island_parts[c.part_index].type == type; }) == changes.end()); + remove_indices.reserve(changes.size()); + + // collect changes from neighbors for result part + indices of neighbor parts + IslandPartChanges modified_changes; + for (const IslandPartChange &change : changes) { + remove_indices.push_back(change.part_index); + // iterate neighbor changes and collect only changes to other neighbors + for (const IslandPartChange &n_change : island_parts[change.part_index].changes) { + if (n_change.part_index == index) + continue; // skip back to removed part + + // Till it is made only on thick+thin parts and neighbor are different type + // It should never appear + assert(std::find_if(changes.begin(), changes.end(), [i = n_change.part_index] + (const IslandPartChange &change){ return change.part_index == i;}) == changes.end()); + //if(std::find_if(changes.begin(), changes.end(), [polygon_index = n_change.part_index] + //(const IslandPartChange &change){ return change.part_index == polygon_index;}) != changes.end()) + // continue; // skip removed part changes + modified_changes.push_back(n_change); + } + } + + // Modified index is smallest from index or remove_indices + std::sort(remove_indices.begin(), remove_indices.end()); + remove_indices.erase( // Remove duplicit inidices + std::unique(remove_indices.begin(), remove_indices.end()), remove_indices.end()); + size_t modified_index = index; + if (remove_indices.front() < index) { + std::swap(remove_indices.front(), modified_index); + std::sort(remove_indices.begin(), remove_indices.end()); + } + + // Set result part after merge + IslandPart& merged_part = island_parts[modified_index]; + merged_part.type = island_parts[changes.front().part_index].type; // type of neighbor + merged_part.changes = modified_changes; + merged_part.sum_lengths = 0; // invalid value after merge + + // remove parts from island parts, from high index to low + for (auto it = remove_indices.rbegin(); it < remove_indices.rend(); ++it) + island_parts.erase(island_parts.begin() + *it); + + // For all parts and their changes fix indices + for (IslandPart &island_part : island_parts) + for (IslandPartChange &change : island_part.changes){ + auto it = std::lower_bound(remove_indices.begin(), remove_indices.end(), change.part_index); + if (it != remove_indices.end() && *it == change.part_index) { // index from removed indices set to modified index + change.part_index = modified_index; // Set neighbors neighbors to point on modified_index + } else { // index bigger than some of removed index decrease by the amount of smaller removed indices + change.part_index -= it - remove_indices.begin(); + } + } + + return std::make_pair(modified_index, remove_indices); +} + +/// +/// Calculate all distances between changes(combination of changes) +/// Choose the longest distance between change for each island part(part_length). +/// Merge island parts in order from shortest path_length +/// Till path_length is smaller than config::min_part_length +/// +/// Only thin or thick parts +/// Minimal length of part to not be merged into neighbors +void merge_short_parts(IslandParts &island_parts, coord_t min_part_length) { + // should be called only for multiple island parts, at least 2 + assert(island_parts.size() > 1); + if (island_parts.size() <= 1) return; // nothing to merge + + // only thin OR thick parts + assert(std::find_if(island_parts.begin(), island_parts.end(), [](const IslandPart &i) + {return i.type != IslandPartType::thin && i.type != IslandPartType::thick; }) == island_parts.end()); + + // same size as island_parts + std::vector part_lengths; + part_lengths.reserve(island_parts.size()); + for (const IslandPart& island_part: island_parts) + part_lengths.push_back(get_longest_distance(island_part.changes)); + + // Merge island parts in order from shortest length + while(true){ + // find smallest part + size_t smallest_part_index = std::min_element(part_lengths.begin(), part_lengths.end()) - part_lengths.begin(); + if (part_lengths[smallest_part_index] >= min_part_length) + break; // all parts are long enough + + auto [index, remove_indices] = merge_negihbor(island_parts, smallest_part_index); + if (island_parts.size() == 1) + return; // only longest part left + + // update part lengths + part_lengths[index] = get_longest_distance(island_parts[index].changes); + for (auto remove_index_it = remove_indices.rbegin(); + remove_index_it != remove_indices.rend(); + ++remove_index_it) + // remove lengths for removed parts + part_lengths.erase(part_lengths.begin() + *remove_index_it); + } +} + +ThinPart create_only_thin_part(const VoronoiGraph::ExPath &path) { + std::optional path_center_opt = create_position_on_path(path.nodes, path.length / 2); + assert(path_center_opt.has_value()); + return ThinPart{*path_center_opt, /*ends*/ {}}; +} + +const VoronoiGraph::Node::Neighbor *get_smallest_source_index(const Positions& positions){ + // do not call with empty positions + assert(!positions.empty()); + if (positions.size() == 1) + return positions.front().neighbor; + + const VoronoiGraph::Node::Neighbor *smallest = nullptr; + size_t smallest_index = std::numeric_limits::max(); + for (const Position &position : positions) { + const VD::edge_type *e = position.neighbor->edge; + size_t min_index = std::min( + e->cell()->source_index(), + e->twin()->cell()->source_index()); + if (smallest_index > min_index) { + smallest_index = min_index; + smallest = position.neighbor; + } + } + return smallest; +} + +std::pair convert_island_parts_to_thin_thick( + const IslandParts& island_parts, const VoronoiGraph::ExPath &path) +{ + // always must be at least one island part + assert(!island_parts.empty()); + // when exist only one change there can't be any changes + assert(island_parts.size() != 1 || island_parts.front().changes.empty()); + // convert island parts into result + if (island_parts.size() == 1) + return island_parts.front().type == IslandPartType::thin ? + std::make_pair(ThinParts{create_only_thin_part(path)}, ThickParts{}) : + std::make_pair(ThinParts{}, ThickParts{ + ThickPart{&path.nodes.front()->neighbors.front()}}); + + std::pair result; + ThinParts& thin_parts = result.first; + ThickParts& thick_parts = result.second; + for (const IslandPart& i:island_parts) { + // Only one island item is solved earlier, soo each part has to have changes + assert(!i.changes.empty()); + Positions ends; + ends.reserve(i.changes.size()); + std::transform(i.changes.begin(), i.changes.end(), std::back_inserter(ends), + [](const IslandPartChange &change) { return change.position; }); + std::sort(ends.begin(), ends.end(), + [](const Position &a, const Position &b) { return a.neighbor < b.neighbor; }); + if (i.type == IslandPartType::thin) { + // Calculate center of longest distance, discard distance + Position center; + get_longest_distance(i.changes, ¢er); + thin_parts.push_back(ThinPart{center, std::move(ends)}); + } else { + assert(i.type == IslandPartType::thick); + const Neighbor *start = VoronoiGraphUtils::get_twin(*get_smallest_source_index(ends)); + // NOTE: VD could contain different order of edges each run. + // To unify behavior as a start index is selected edge with smallest index of source line + thick_parts.push_back(ThickPart {start, std::move(ends)}); + } + } + return result; +} + +/// +/// Separate thin(narrow) and thick(wide) part of island +/// +/// Longest path over island +/// Island border +/// Define border between thin and thick part +/// and minimal length of separable part +/// Thin and thick parts +std::pair separate_thin_thick( + const VoronoiGraph::ExPath &path, const Lines &lines, const SampleConfig &config +) { + // Check input + assert(!path.nodes.empty()); + assert(lines.size() >= 3); // at least triangle + + // Start dividing on some border of island + const VoronoiGraph::Node *start_node = path.nodes.front(); + + // CHECK that front of path is outline node + assert(start_node->neighbors.size() == 1); + // first neighbor must be from outline node + assert(start_node->neighbors.front().min_width() == 0); + + IslandParts island_parts{IslandPart{IslandPartType::thin, /*changes*/{}, /*sum_lengths*/0}}; + ProcessItem item = {/*prev_node*/ nullptr, start_node, 0}; // current processing item + ProcessItems process; // queue of nodes to process + do { // iterate over all nodes in graph and collect interfaces into island_parts + assert(item.node != nullptr); + ProcessItem next_item = {nullptr, nullptr, std::numeric_limits::max()}; + for (const Neighbor &neighbor: item.node->neighbors) { + if (neighbor.node == item.prev_node) continue; // already done + if (next_item.node != nullptr) // already prepared item is stored into queue + process.push_back(next_item); + + size_t next_part_index = detect_interface(island_parts, item.i, &neighbor, lines, config); + next_item = ProcessItem{item.node, neighbor.node, next_part_index}; + + // exist loop back? + auto is_oposit_item = [&next_item](const ProcessItem &p) { + return p.node == next_item.prev_node && p.prev_node == next_item.node;}; + if (auto process_it = std::find_if(process.begin(), process.end(), is_oposit_item); + process_it != process.end()) { + // solve loop back + merge_parts_and_fix_process(island_parts, item, process_it->i, next_item.i, process); + // branch is already processed + process.erase(process_it); + next_item.node = nullptr; // do not use item as next one + continue; + } + } + // Select next node to process + if (next_item.node != nullptr) { + item = next_item; // copy + } else { + if (process.empty()) + break; // no more nodes to process + item = process.back(); // copy + process.pop_back(); + } + } while (item.node != nullptr); // loop should end by break with empty process + + merge_middle_parts_into_biggest_neighbor(island_parts); + if (island_parts.size() != 1) + merge_same_neighbor_type_parts(island_parts); + if (island_parts.size() != 1) + merge_short_parts(island_parts, config.min_part_length); + + return convert_island_parts_to_thin_thick(island_parts, path); +} + +/// +/// create points on both ends of path with side distance from border +/// +/// Longest path over island. +/// Source lines for VG --> outline of island. +/// Wanted width on position +/// Maximal distance from side +/// 2x Static Support point(lay os sides of path) +SupportIslandPoints create_side_points( + const VoronoiGraph::ExPath &path, const Lines& lines, const SampleConfig &config, + SupportIslandPoint::Type type = SupportIslandPoint::Type::two_points) +{ + coord_t max_distance_by_length = static_cast(path.length * config.max_length_ratio_for_two_support_points); + coord_t max_distance = std::min(config.maximal_distance_from_outline, max_distance_by_length); + + VoronoiGraph::Nodes reverse_path = path.nodes; // copy + std::reverse(reverse_path.begin(), reverse_path.end()); + + coord_t width = 2 * config.head_radius; + coord_t side_distance1 = max_distance; // copy + coord_t side_distance2 = max_distance; // copy + auto pos1 = create_position_on_path(path.nodes, lines, width, side_distance1); + auto pos2 = create_position_on_path(reverse_path, lines, width, side_distance2); + assert(pos1.has_value()); + assert(pos2.has_value()); + SupportIslandPoints result; + result.reserve(2); + result.push_back(create_no_move_point(*pos1, type)); + result.push_back(create_no_move_point(*pos2, type)); + return result; +} + +void draw(SVG &svg, const SupportIslandPoints &supportIslandPoints, coord_t radius, bool write_type) { + const char *color = nullptr; + for (const auto &p : supportIslandPoints) { + switch (p->type) { + case SupportIslandPoint::Type::thin_part: + case SupportIslandPoint::Type::thin_part_change: + case SupportIslandPoint::Type::thin_part_loop: color = "lightred"; break; + case SupportIslandPoint::Type::thick_part_outline: color = "lightblue"; break; + case SupportIslandPoint::Type::thick_part_inner: color = "lightgreen"; break; + case SupportIslandPoint::Type::one_bb_center_point: color = "red"; break; + case SupportIslandPoint::Type::one_center_point: + case SupportIslandPoint::Type::two_points: + case SupportIslandPoint::Type::two_points_backup: + default: color = "black"; + } + svg.draw(p->point, color, radius); + if (write_type && p->type != SupportIslandPoint::Type::undefined) { + auto type_name = SupportIslandPoint::to_string(p->type); + Point start = p->point + Point(radius, 0); + svg.draw_text(start, std::string(type_name).c_str(), color, 8); + } + } +} +} // namespace + +////////////////////////////// +/// uniform support island /// +////////////////////////////// +namespace Slic3r::sla { +SupportIslandPoints uniform_support_island( + const ExPolygon &island, const Points& permanent, const SampleConfig &config){ + ExPolygon simplified_island = get_simplified(island, config); +#ifdef OPTION_TO_STORE_ISLAND + std::string path; + if (!config.path.empty()) { + static int counter = 0; + path = replace_first(config.path, "<>", std::to_string(++counter)); + draw_island(path, island, simplified_island); + // need to save svg in case of infinite loop so no store SVG into variable + } +#endif // OPTION_TO_STORE_ISLAND + + // 0) When island is smaller than minimal-head diameter, + // it will be supported whole by support poin in center + if (Point center; get_center(simplified_island.contour.points, config.head_radius, center)) { + SupportIslandPoints supports; + supports.push_back(std::make_unique( + center, SupportIslandInnerPoint::Type::one_bb_center_point)); +#ifdef OPTION_TO_STORE_ISLAND + if (!path.empty()){ // add center support point into image + SVG svg = draw_island(path, island, simplified_island); + svg.draw_text(Point{0, 0}, "one center support point", "black"); + draw(svg, supports, config.head_radius); + } +#endif // OPTION_TO_STORE_ISLAND + return supports; + } + + Geometry::VoronoiDiagram vd; + Lines lines = to_lines(simplified_island); + vd.construct_voronoi(lines.begin(), lines.end()); + assert(vd.get_issue_type() == Geometry::VoronoiDiagram::IssueType::NO_ISSUE_DETECTED); + if (vd.get_issue_type() != Geometry::VoronoiDiagram::IssueType::NO_ISSUE_DETECTED) { + // error state suppport island by one point + Point center = BoundingBox{island.contour.points}.center(); + SupportIslandPoints supports; + supports.push_back(std::make_unique( + center, SupportIslandInnerPoint::Type::bad_shape_for_vd)); +#ifdef OPTION_TO_STORE_ISLAND + if (!path.empty()) { // add center support point into image + SVG svg = draw_island(path, island, simplified_island); + svg.draw_text(Point{0, 0}, "Can't create Voronoi Diagram for the shape", "red"); + draw(svg, supports, config.head_radius); + } +#endif // OPTION_TO_STORE_ISLAND + return supports; + } + Voronoi::annotate_inside_outside(vd, lines); + VoronoiGraph skeleton = VoronoiGraphUtils::create_skeleton(vd, lines); + VoronoiGraph::ExPath longest_path; + + const VoronoiGraph::Node *start_node = VoronoiGraphUtils::getFirstContourNode(skeleton); + // every island has to have a point on contour + assert(start_node != nullptr); + longest_path = VoronoiGraphUtils::create_longest_path(start_node); + +#ifdef OPTION_TO_STORE_ISLAND // add voronoi diagram with longest path into image + if (!path.empty()) draw_island_graph(path, island, simplified_island, skeleton, longest_path, lines, config); +#endif // OPTION_TO_STORE_ISLAND + + // 1) One support point + if (longest_path.length < config.max_length_for_one_support_point) { + // create only one point in center + SupportIslandPoints supports; + supports.push_back(create_middle_path_point( + longest_path, SupportIslandPoint::Type::one_center_point)); +#ifdef OPTION_TO_STORE_ISLAND + if (!path.empty()){ + SVG svg = draw_island(path, island, simplified_island); + draw(svg, supports, config.head_radius); + } +#endif // OPTION_TO_STORE_ISLAND + return supports; + } + + // 2) Two support points have to stretch island even if haed is not fully under island. + if (VoronoiGraphUtils::get_max_width(longest_path) < config.thin_max_width && + longest_path.length < config.max_length_for_two_support_points) { + SupportIslandPoints supports = create_side_points(longest_path, lines, config); +#ifdef OPTION_TO_STORE_ISLAND + if (!path.empty()){ + SVG svg = draw_island(path, island, simplified_island); + draw(svg, supports, config.head_radius); + } +#endif // OPTION_TO_STORE_ISLAND + return supports; + } + + // TODO: 3) Triangle aligned support points + // eval outline and find three point create almost equilateral triangle to stretch island + + // 4) Divide island on Thin & Thick part and support by parts + SupportIslandPoints supports; + auto [thin, thick] = separate_thin_thick(longest_path, lines, config); + assert(!thin.empty() || !thick.empty()); + for (const ThinPart &part : thin) create_supports_for_thin_part(part, supports, config); + for (const ThickPart &part : thick) create_supports_for_thick_part(part, supports, lines, config); + + // At least 2 support points are neccessary after thin/thick sampling heuristic + if (supports.size() <= 2){ + SupportIslandInnerPoint::Type type = SupportIslandInnerPoint::Type::two_points_backup; + SupportIslandPoints two_supports = create_side_points(longest_path, lines, config, type); +#ifdef OPTION_TO_STORE_ISLAND + if (!path.empty()) { + SVG svg = draw_island(path, island, simplified_island); + draw(svg, two_supports, config.head_radius); + } +#endif // OPTION_TO_STORE_ISLAND + return two_supports; + } + +#ifdef OPTION_TO_STORE_ISLAND + Points supports_before_align = ::to_points(supports); + if (!path.empty()) { + SVG svg = draw_island_graph(path, island, simplified_island, skeleton, longest_path, lines, config); + draw(svg, supports, config.head_radius); + } +#endif // OPTION_TO_STORE_ISLAND + + // allign samples + if (permanent.empty()) + align_samples(supports, island, config); + else + align_samples_with_permanent(supports, island, permanent, config); + +#ifdef OPTION_TO_STORE_ISLAND + if (!path.empty()) { + SVG svg = draw_island(path, island, simplified_island); + coord_t width = config.head_radius / 5; + VoronoiGraphUtils::draw(svg, longest_path.nodes, width, "darkorange"); + VoronoiGraphUtils::draw(svg, skeleton, lines, config, false /*print Pointer address*/); + + Lines align_moves; + align_moves.reserve(supports.size()); + for (size_t i = 0; i < supports.size(); ++i) + align_moves.push_back(Line(supports[i]->point, supports_before_align[i])); + svg.draw(align_moves, "lightgray", width); + draw(svg, supports, config.head_radius); + } +#endif // OPTION_TO_STORE_ISLAND + return supports; +} + +// Follow implementation "create_supports_for_thick_part(" +SupportIslandPoints uniform_support_peninsula( + const Peninsula &peninsula, const Points& permanent, const SampleConfig &config){ + // create_peninsula_field + float delta = static_cast(config.minimal_distance_from_outline); + Field field = create_field(peninsula.unsuported_area, delta, peninsula.is_outline); + assert(!field.inner.empty()); + if (field.inner.empty()) + return {}; // no inner part + +#ifdef SLA_SAMPLE_ISLAND_UTILS_STORE_PENINSULA_FIELD_TO_SVG_PATH + { + Lines lines = to_lines(peninsula.unsuported_area); + const char *source_line_color = "black"; + bool draw_source_line_indexes = true; + bool draw_border_line_indexes = false; + bool draw_field_source_indexes = true; + static int counter = 0; + SVG svg(replace_first(SLA_SAMPLE_ISLAND_UTILS_STORE_PENINSULA_FIELD_TO_SVG_PATH, + "<>", std::to_string(counter++)).c_str(), + LineUtils::create_bounding_box(lines)); + LineUtils::draw(svg, lines, source_line_color, 0., draw_source_line_indexes); + draw(svg, field, peninsula.unsuported_area, draw_border_line_indexes, draw_field_source_indexes); + } +#endif // SLA_SAMPLE_ISLAND_UTILS_STORE_PENINSULA_FIELD_TO_SVG_PATH + + SupportIslandPoints results = sample_outline(field, config); + // Inner must survive after sample field for aligning supports(move along outline) + auto inner = std::make_shared(field.inner); + Points inner_points = sample_expolygons_with_centering(*inner, config.thick_inner_max_distance); + std::transform(inner_points.begin(), inner_points.end(), std::back_inserter(results), + [&inner](const Point &point) { return std::make_unique( + point, inner, SupportIslandPoint::Type::thick_part_inner);}); + + // allign samples + if (permanent.empty()) + align_samples(results, peninsula.unsuported_area, config); + else + align_samples_with_permanent(results, peninsula.unsuported_area, permanent, config); + return results; +} + +bool is_uniform_support_island_visualization_disabled() { +#ifndef NDEBUG + return false; +#endif +#ifdef SLA_SAMPLE_ISLAND_UTILS_STORE_FIELD_TO_SVG_PATH + return false; +#endif +#ifdef SLA_SAMPLE_ISLAND_UTILS_STORE_PENINSULA_FIELD_TO_SVG_PATH + return false; +#endif +#ifdef SLA_SAMPLE_ISLAND_UTILS_STORE_ALIGN_ONCE_TO_SVG_PATH + return false; +#endif +#ifdef SLA_SAMPLE_ISLAND_UTILS_STORE_ALIGNED_TO_SVG_PATH + return false; +#endif + return true; +} + +} // namespace Slic3r::sla diff --git a/src/libslic3r/SLA/SupportIslands/UniformSupportIsland.hpp b/src/libslic3r/SLA/SupportIslands/UniformSupportIsland.hpp new file mode 100644 index 0000000000..0a07385e71 --- /dev/null +++ b/src/libslic3r/SLA/SupportIslands/UniformSupportIsland.hpp @@ -0,0 +1,37 @@ +#ifndef slic3r_SLA_SuppotstIslands_UniformSupportIsland_hpp_ +#define slic3r_SLA_SuppotstIslands_UniformSupportIsland_hpp_ + +#include +#include "SampleConfig.hpp" +#include "SupportIslandPoint.hpp" +#include "libslic3r/SLA/SupportPointGenerator.hpp" // Peninsula + +namespace Slic3r::sla { + +/// +/// Distribute support points across island area defined by ExPolygon. +/// +/// Shape of island +/// Place supported by already existing supports +/// Configuration of support density +/// Support points laying inside of the island +SupportIslandPoints uniform_support_island( + const ExPolygon &island, const Points &permanent, const SampleConfig &config); + +/// +/// Distribute support points across peninsula +/// +/// half island with anotation of the coast and land outline +/// Place supported by already existing supports +/// Density distribution parameters +/// Support points laying inside of the peninsula +SupportIslandPoints uniform_support_peninsula( + const Peninsula &peninsula, const Points& permanent, const SampleConfig &config); + +/// +/// Check for tests that developer do not forget disable visualization after debuging. +/// +bool is_uniform_support_island_visualization_disabled(); + +} // namespace Slic3r::sla +#endif // slic3r_SLA_SuppotstIslands_UniformSupportIsland_hpp_ diff --git a/src/libslic3r/SLA/SupportIslands/VectorUtils.hpp b/src/libslic3r/SLA/SupportIslands/VectorUtils.hpp new file mode 100644 index 0000000000..29f6d09c6a --- /dev/null +++ b/src/libslic3r/SLA/SupportIslands/VectorUtils.hpp @@ -0,0 +1,152 @@ +#ifndef slic3r_SLA_SuppotstIslands_VectorUtils_hpp_ +#define slic3r_SLA_SuppotstIslands_VectorUtils_hpp_ + +#include +#include +#include // iota +#include + +namespace Slic3r::sla { + +/// +/// Class which contain collection of static function +/// for work with std vector +/// QUESTION: Is it only for SLA? +/// +class VectorUtils +{ +public: + VectorUtils() = delete; + + /// + /// For sorting a vector by calculated value + /// CACHE for calculated values + /// + /// vetor to be sorted + /// function to calculate sorting value + template + static void sort_by(std::vector &data, std::function &calc) + { + assert(!data.empty()); + if (data.size() <= 1) return; + + // initialize original index locations + std::vector idx(data.size()); + std::iota(idx.begin(), idx.end(), 0); + + // values used for sort + std::vector v; + v.reserve(data.size()); + for (const T1 &d : data) v.emplace_back(calc(d)); + + // sort indexes based on comparing values in v + // using std::stable_sort instead of std::sort + // to avoid unnecessary index re-orderings + // when v contains elements of equal values + std::stable_sort(idx.begin(), idx.end(), [&v](size_t i1, size_t i2) { + return v[i1] < v[i2]; + }); + + reorder_destructive(idx.begin(), idx.end(), data.begin()); + } + + /// + /// shortcut to use std::transform with alocation for result + /// + /// vetor to be transformed + /// lambda to transform data types + /// result vector + template + static std::vector transform(const std::vector &data, std::function &transform_func) + { + std::vector result; + result.reserve(data.size()); + std::transform(data.begin(), data.end(), std::back_inserter(result), transform_func); + return result; + } + + /// + /// Reorder vector by indexes given by iterators + /// + /// Start index + /// Last index + /// data to reorder. e.g. vector::begin() + template + static void reorder(order_iterator order_begin, + order_iterator order_end, + value_iterator v) + { + typedef typename std::iterator_traits::value_type value_t; + typedef typename std::iterator_traits::value_type index_t; + typedef typename std::iterator_traits::difference_type diff_t; + + diff_t remaining = order_end - 1 - order_begin; + for (index_t s = index_t(), d; remaining > 0; ++s) { + for (d = order_begin[s]; d > s; d = order_begin[d]) + ; + if (d == s) { + --remaining; + value_t temp = v[s]; + while (d = order_begin[d], d != s) { + std::swap(temp, v[d]); + --remaining; + } + v[s] = temp; + } + } + } + + /// + /// Same as function 'reorder' but destroy order vector for speed + /// + /// Start index + /// Last index + /// data to reorder. e.g. vector::begin() + template + static void reorder_destructive(order_iterator order_begin, + order_iterator order_end, + value_iterator v) + { + typedef typename std::iterator_traits::value_type value_t; + typedef typename std::iterator_traits::value_type index_t; + typedef typename std::iterator_traits::difference_type diff_t; + static const index_t done_index = static_cast(-1); + diff_t remaining = order_end - 1 - order_begin; + // s = start sequence index + for (index_t s = index_t(); remaining > 0; ++s) { + // d = index1 for swap + index_t d = order_begin[s]; + if (d == done_index) continue; + --remaining; + if (s == d) continue; // correct order + value_t temp = v[s]; + do { + std::swap(temp, v[d]); + index_t d2 = done_index; + std::swap(order_begin[d], d2); + d = d2; + --remaining; + } while (d != s); + v[s] = temp; + } + } + + /// + /// Insert item into sorted vector of items + /// + /// Sorted vector with items + /// Item to insert + /// Predicate for sorting + /// now inserted item + template + static typename std::vector::iterator insert_sorted( + std::vector &data, const T &item, Pred pred) + { + auto iterator = std::upper_bound(data.begin(), data.end(), item, pred); + return data.insert(iterator, item); + } + +}; + +} // namespace Slic3r::sla +#endif // slic3r_SLA_SuppotstIslands_VectorUtils_hpp_ diff --git a/src/libslic3r/SLA/SupportIslands/VoronoiDiagramCGAL.cpp b/src/libslic3r/SLA/SupportIslands/VoronoiDiagramCGAL.cpp new file mode 100644 index 0000000000..69203ddeab --- /dev/null +++ b/src/libslic3r/SLA/SupportIslands/VoronoiDiagramCGAL.cpp @@ -0,0 +1,294 @@ +#include "VoronoiDiagramCGAL.hpp" + +#include + +// includes for defining the Voronoi diagram adaptor +#include +#include +#include +#include +#include + +#include "libslic3r/Geometry.hpp" +#include "libslic3r/Line.hpp" +#include "libslic3r/SLA/SupportIslands/LineUtils.hpp" +#include "libslic3r/SLA/SupportIslands/VoronoiGraphUtils.hpp" + +using namespace Slic3r; +using namespace Slic3r::sla; + +// typedefs for defining the adaptor +using K = CGAL::Exact_predicates_inexact_constructions_kernel; +using DT = CGAL::Delaunay_triangulation_2; +using AT = CGAL::Delaunay_triangulation_adaptation_traits_2
; +using AP = CGAL::Delaunay_triangulation_caching_degeneracy_removal_policy_2
; +using VD = CGAL::Voronoi_diagram_2; + +// typedef for the result type of the point location +using Site_2 = AT::Site_2; +using Point_2 = AT::Point_2; +using Locate_result = VD::Locate_result; +using Vertex_handle = VD::Vertex_handle; +using Face_handle = VD::Face_handle; +using Halfedge_handle = VD::Halfedge_handle; +using Ccb_halfedge_circulator = VD::Ccb_halfedge_circulator; + +namespace{ +// conversion from double to coor_t +Slic3r::Point to_point(const Site_2 &s) { return Slic3r::Point(s.x(), s.y()); } + +/// +/// Create line segment lay between given points with distance limited by maximal_distance +/// +/// +/// +/// limits for result segment +/// line perpendicular to line between point1 and point2 +Slic3r::Line create_line_between_points( + const Point &point1, const Point &point2, double maximal_distance +) { + Point middle = point1/2 + point2/2; + Point diff = point1 - point2; // direction from point2 to point1 + coord_t manhatn_distance = abs(diff.x()) + abs(diff.y()); + // alligned points should not be too close + assert(manhatn_distance >= 1); + // it is not neccesary to know exact distance + // One need to know minimal distance between points. + // worst case is diagonal: sqrt(2*(0.5 * manhatn_distance)^2) = + double min_distance = manhatn_distance * .7; // 1 / sqrt(2) + double scale = maximal_distance / min_distance; + Point side_dir(-diff.y() * scale, diff.x() * scale); + return Line(middle - side_dir, middle + side_dir); +} + +/// +/// Crop line which is no too far away(compare to maximal_distance) from v1(or v2) +/// +/// +/// +/// +/// +/// +/// +std::optional crop_line( + const Point_2 &a, const Point_2 &b, + const Point &v1, const Point &v2, double maximal_distance +) { + Point diff = v1 - v2; + Point dir(-diff.y(), diff.x()); + coord_t abs_x = abs(dir.x()); + coord_t abs_y = abs(dir.y()); + Point middle = v1 / 2 + v2 / 2; + + double a_t, b_t; // [units is size of dir] + if (abs_x > abs_y) { + a_t = (a.x() - middle.x()) / (double) dir.x(); + b_t = (b.x() - middle.x()) / (double) dir.x(); + } else { + a_t = (a.y() - middle.y()) / (double) dir.y(); + b_t = (b.y() - middle.y()) / (double) dir.y(); + } + + // calculate approx distance from middle point to detect too far from middle + coord_t manhatn_distance = abs_x + abs_y; + // alligned points should not be too close + assert(manhatn_distance >= 1); + double min_distance = manhatn_distance * .7; + double a_dist = a_t * min_distance; + double b_dist = b_t * min_distance; + + double scale = maximal_distance / min_distance; + Line l(to_point(a), to_point(b)); + if (a_dist > maximal_distance) { + if (b_dist > maximal_distance) + return {}; // out of range + l.a = middle + (scale * dir.cast()).cast(); + } else if (a_dist < -maximal_distance) { + if (b_dist < -maximal_distance) + return {}; // out of range + l.a = middle - (scale * dir.cast()).cast(); + } + if (b_dist > maximal_distance) { + l.b = middle + (scale * dir.cast()).cast(); + } else if (b_dist < -maximal_distance) + l.b = middle - (scale * dir.cast()).cast(); + return l; +} + +/// +/// Crop ray to line which is no too far away(compare to maximal_distance) from v1(or v2) +/// +/// ray start +/// +/// +/// Limits for line +/// +std::optional crop_ray(const Point_2 &ray_point, const Point &v1, const Point &v2, double maximal_distance) { + assert(maximal_distance > 0); + Point diff = v2 - v1; + Point ray_dir(-diff.y(), diff.x()); + + // bounds are similar as for line between points + Point middle = v1/2 + v2/2; + coord_t abs_x = abs(ray_dir.x()); + coord_t abs_y = abs(ray_dir.y()); + coord_t manhatn_dist = abs_x + abs_y; // maximal distance + + // alligned points should not be too close + assert(manhatn_dist >= 1); + + double min_distance = manhatn_dist * .7; + assert(min_distance > 0); + + // count of dir from ray point to middle + double middle_t = (abs_x > abs_y) ? + // use x coor + (middle.x() - ray_point.x()) / (double) ray_dir.x() : + // use y coor + (middle.y() - ray_point.y()) / (double) ray_dir.y(); + + // minimal distance from ray point to middle point + double min_middle_dist = middle_t * min_distance; + if (min_middle_dist < -maximal_distance) + // ray start out of area of interest + return {}; + + double scale = maximal_distance / min_distance; + Point side_dir = (ray_dir.cast() * scale).cast(); + return Line(min_middle_dist > maximal_distance? + (middle - side_dir) : to_point(ray_point), + middle + side_dir); +} + +std::optional to_line( + const Halfedge_handle &edge, + double maximal_distance +) { + // validation slow down a lot, Never appear during algorithm tunning + assert(edge->is_valid()); + //if (!edge->is_valid()) return {}; + + if (edge->has_source()) { + // source point of edge + if (edge->has_target()) { // Line segment + assert(edge->is_segment()); + return crop_line( + edge->source()->point(), + edge->target()->point(), + to_point(edge->up()->point()), + to_point(edge->down()->point()), + maximal_distance); + } else { // ray from source + assert(edge->is_ray()); + return crop_ray( + edge->source()->point(), + to_point(edge->up()->point()), + to_point(edge->down()->point()), + maximal_distance); + } + } else if (edge->has_target()) { // ray from target + assert(edge->is_ray()); + return crop_ray( + edge->target()->point(), + to_point(edge->down()->point()), + to_point(edge->up()->point()), + maximal_distance); + } + // infinite line between points + assert(edge->is_bisector()); + return create_line_between_points( + to_point(edge->up()->point()), + to_point(edge->down()->point()), + maximal_distance + ); +} + +} // namespace + +Polygons Slic3r::sla::create_voronoi_cells_cgal(const Points &points, coord_t max_distance) { + assert(points.size() > 1); + + // Different way to fill points into VD + // delaunary triangulation + std::vector dt_points; + dt_points.reserve(points.size()); + for (const Point &p : points) + dt_points.emplace_back(p.x(), p.y()); + DT dt(dt_points.begin(), dt_points.end()); + assert(dt.is_valid()); + VD vd(dt); + assert(vd.is_valid()); + + // voronoi diagram seems that points face order is same as inserted points + //VD vd; + //for (const Point& p: points) { + // Site_2 t(p.x(), p.y()); + // vd.insert(t); + //} + + Polygons cells(points.size()); + size_t fit_index = 0; + // Loop over the faces of the Voronoi diagram in order of given points + for (VD::Face_iterator fit = vd.faces_begin(); fit != vd.faces_end(); ++fit, ++fit_index) { + // get source point index + // TODO: do it without search !!! + Point_2 source_point = fit->dual()->point(); + Slic3r::Point source_pt(source_point.x(), source_point.y()); + auto it = std::find(points.begin(), points.end(), source_pt); + assert(it != points.end()); + if (it == points.end()) + continue; + size_t index = it - points.begin(); + assert(source_pt.x() == points[index].x()); + assert(source_pt.y() == points[index].y()); + + // origin of voronoi face + const Point& origin = points[index]; + Lines lines; + // collect croped lines of field + Ccb_halfedge_circulator ec_start = fit->ccb(); + Ccb_halfedge_circulator ec = ec_start; + do { + assert(ec->is_valid()); + std::optional line_opt = to_line(ec, max_distance); + if (!line_opt.has_value()) + continue; + Line &line = *line_opt; + Geometry::Orientation orientation = Geometry::orient(origin, line.a, line.b); + // Can be rich on circle over source point edge + if (orientation == Geometry::Orientation::ORIENTATION_COLINEAR) continue; + if (orientation == Geometry::Orientation::ORIENTATION_CW) std::swap(line.a, line.b); + lines.push_back(std::move(line)); + } while (++ec != ec_start); + assert(!lines.empty()); + if (lines.size() > 1) + LineUtils::sort_CCW(lines, origin); + + + //Slic3r::SVG + // svg("C:/data/temp/CGAL_VD_face.svg", + // {origin - Point(max_distance, max_distance), + // origin + Point(max_distance, max_distance)}); + //svg.draw(lines, "green"); + //do { + // if (ec->has_source()) + // svg.draw(to_point(ec->source()->point())); + // if (ec->has_target()) + // svg.draw(to_point(ec->target()->point())); + // if (ec->is_segment()) + // svg.draw(Line(to_point(ec->source()->point()), to_point(ec->target()->point()))); + //} while (++ec != ec_start); + //svg.draw(origin, "red"); + //svg.Close(); + + // preccission to decide when not connect neighbor points + double min_distance = max_distance / 1000.; + size_t count_point = 6; // count added points + // cell for current processed face + cells[index] = + VoronoiGraphUtils::to_polygon(lines, origin, max_distance, min_distance, count_point); + } + // Point_2 p; + // Locate_result lr = vd.locate(p); // Could locate face of VD - potentionaly could iterate input points + return cells; +} \ No newline at end of file diff --git a/src/libslic3r/SLA/SupportIslands/VoronoiDiagramCGAL.hpp b/src/libslic3r/SLA/SupportIslands/VoronoiDiagramCGAL.hpp new file mode 100644 index 0000000000..02781e5810 --- /dev/null +++ b/src/libslic3r/SLA/SupportIslands/VoronoiDiagramCGAL.hpp @@ -0,0 +1,26 @@ +///|/ Copyright (c) Prusa Research 2024 Filip Sykala @Jony01 +///|/ +///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher +///|/ +#ifndef slic3r_SLA_SuppotstIslands_VoronoiDiagramCGAL_hpp_ +#define slic3r_SLA_SuppotstIslands_VoronoiDiagramCGAL_hpp_ + +#include +#include + +namespace Slic3r::sla { + +/// +/// +/// IMPROVE1: use accessor to point coordinate instead of points +/// IMPROVE2: add filter for create cell polygon only for moveable samples +/// +/// Input points for voronoi diagram +/// Limit for polygon made by point +/// NOTE: prerequisities input points are in max_distance only outer have infinite cell +/// which are croped to max_distance +/// Polygon cell for input point +Polygons create_voronoi_cells_cgal(const Points &points, coord_t max_distance); + +} +#endif // slic3r_SLA_SuppotstIslands_VoronoiDiagramCGAL_hpp_ diff --git a/src/libslic3r/SLA/SupportIslands/VoronoiGraph.hpp b/src/libslic3r/SLA/SupportIslands/VoronoiGraph.hpp new file mode 100644 index 0000000000..325a1eee32 --- /dev/null +++ b/src/libslic3r/SLA/SupportIslands/VoronoiGraph.hpp @@ -0,0 +1,212 @@ +#ifndef slic3r_SLA_SuppotstIslands_VoronoiGraph_hpp_ +#define slic3r_SLA_SuppotstIslands_VoronoiGraph_hpp_ + +#include +#include +#include +#include + +namespace Slic3r::sla { + +/// +/// DTO store skeleton With longest path +/// +struct VoronoiGraph +{ + using VD = Slic3r::Geometry::VoronoiDiagram; + struct Node; + using Nodes = std::vector; + struct Path; + struct ExPath; + using Circle = Path; + struct Position; + std::map data; +}; + +/// +/// Node data structure for Voronoi Graph. +/// Extend information about Voronoi vertex. +/// +struct VoronoiGraph::Node +{ + // reference to Voronoi diagram VertexCategory::Inside OR + // VertexCategory::OnContour but NOT VertexCategory::Outside + const VD::vertex_type *vertex; + // longest distance to edge sum of line segment size (no euclid because of shape U) + double longestDistance; + + // actual distance to edge + double distance; + + struct Neighbor; + std::vector neighbors; + + // constructor + Node(const VD::vertex_type *vertex, double distance) + : vertex(vertex), longestDistance(0.), distance(distance), neighbors() + {} +}; + +/// +/// Surrond GraphNode data type. +/// Extend information about voronoi edge. +/// TODO IMPROVE: extends neighbors for store more edges +/// (cumulate Nodes with 2 neighbors - No cross) +/// +struct VoronoiGraph::Node::Neighbor +{ + const VD::edge_type *edge; + // pointer on graph node structure + const Node *node; + + /// + /// DTO represents size property of one Neighbor + /// + struct Size{ + // length edge between vertices + double length; + + // widht is distance between outlines + // maximal width + coord_t min_width; + // minimal widht + coord_t max_width; + + Size(double length, coord_t min_width, coord_t max_width) + : length(length), min_width(min_width), max_width(max_width) + {} + }; + std::shared_ptr size; + +public: + Neighbor(const VD::edge_type * edge, + const Node * node, + std::shared_ptr size) + : edge(edge) + , node(node) + , size(std::move(size)) + {} + // accessor to member + double length() const { return size->length; } + coord_t min_width() const { return size->min_width; } + coord_t max_width() const { return size->max_width; } +}; + +/// +/// DTO represents path over nodes of VoronoiGraph +/// store queue of Nodes +/// store length of path +/// +struct VoronoiGraph::Path +{ + // row of neighbor Nodes + VoronoiGraph::Nodes nodes; + + // length of path + // when circle contain length from back to front; + double length; + +public: + Path() : nodes(), length(0.) {} + Path(const VoronoiGraph::Node *node) : nodes({node}), length(0.) {} + Path(VoronoiGraph::Nodes nodes, double length) + : nodes(std::move(nodes)), length(length) + {} + + void append(const VoronoiGraph::Node *node, double length) + { + nodes.push_back(node); + this->length += length; + } + + Path extend(const VoronoiGraph::Node *node, double length) const { + Path result(*this); // make copy + result.append(node, length); + return result; + } + + struct OrderLengthFromShortest{ + bool operator()(const VoronoiGraph::Path &path1, + const VoronoiGraph::Path &path2){ + return path1.length > path2.length; + } + }; + + struct OrderLengthFromLongest{ + bool operator()(const VoronoiGraph::Path &path1, + const VoronoiGraph::Path &path2){ + return path1.length < path2.length; + } + }; +}; + + +/// +/// DTO +/// extends path with side branches and circles(connection of circles) +/// +struct VoronoiGraph::ExPath : public VoronoiGraph::Path +{ + // not main path is stored in secondary paths + // key is pointer to source node + using SideBranches = std::priority_queue, + OrderLengthFromLongest>; + using SideBranchesMap = std::map; + + // All side branches in Graph under node + // Map contains only node, which has side branche(s) + // There is NOT empty SideBranches in map !!! + SideBranchesMap side_branches; + + // All circles in Graph under node + std::vector circles; + + // alone circle does'n have record in connected_circle + // every connected circle have at least two records(firs to second & + // second to first) EXAMPLE with 3 circles(index to circles stored in + // this->circles are: c1, c2 and c3) connected together + // connected_circle[c1] = {c2, c3}; connected_circle[c2] = {c1, c3}; + // connected_circle[c3] = {c1, c2}; + using ConnectedCircles = std::map>; + ConnectedCircles connected_circle; + +public: + ExPath() = default; +}; + +/// +/// DTO +/// Extend neighbor with ratio to edge +/// For point position on VoronoiGraph use VoronoiGraphUtils::get_edge_point +/// +struct VoronoiGraph::Position +{ + // neighbor is stored inside of voronoi diagram + const VoronoiGraph::Node::Neighbor* neighbor; + + // define position on neighbor edge + // Value should be in range from 0. to 1. (shrinked when used) + // Value 0 means position of edge->vertex0 + // Value 0.5 is on half edge way between edge->vertex0 and edge->vertex1 + // Value 1 means position of edge->vertex1 + double ratio; + + Position(const VoronoiGraph::Node::Neighbor *neighbor, double ratio) + : neighbor(neighbor), ratio(ratio) + {} + + Position(): neighbor(nullptr), ratio(0.) {} + + coord_t calc_distance() const { + return static_cast(neighbor->length() * ratio); + } + + coord_t calc_rest_distance() const + { + return static_cast(neighbor->length() * (1. - ratio)); + } +}; + +} // namespace Slic3r::sla +#endif // slic3r_SLA_SuppotstIslands_VoronoiGraph_hpp_ diff --git a/src/libslic3r/SLA/SupportIslands/VoronoiGraphUtils.cpp b/src/libslic3r/SLA/SupportIslands/VoronoiGraphUtils.cpp new file mode 100644 index 0000000000..78d89dbdec --- /dev/null +++ b/src/libslic3r/SLA/SupportIslands/VoronoiGraphUtils.cpp @@ -0,0 +1,1456 @@ +#include "VoronoiGraphUtils.hpp" + +#include +#include +#include +#include "IStackFunction.hpp" +#include "EvaluateNeighbor.hpp" +#include "ParabolaUtils.hpp" +#include "LineUtils.hpp" +#include "PointUtils.hpp" +#include "PolygonUtils.hpp" + +#include + +// comment definition of NDEBUG to enable assert() +//#define NDEBUG +#include + +//#define SLA_SVG_VISUALIZATION_CELL_2_POLYGON + +using namespace Slic3r::sla; + +coord_t VoronoiGraphUtils::to_coord(const VD::coordinate_type &coord) +{ + static const VD::coordinate_type min_val = + static_cast(std::numeric_limits::min()); + static const VD::coordinate_type max_val = + static_cast(std::numeric_limits::max()); + if (coord > max_val) return std::numeric_limits::max(); + if (coord < min_val) return std::numeric_limits::min(); + return static_cast(std::round(coord)); +} + +Slic3r::Point VoronoiGraphUtils::to_point(const VD::vertex_type *vertex) +{ + return Point(to_coord(vertex->x()), to_coord(vertex->y())); +} + +VoronoiGraphUtils::VD::point_type VoronoiGraphUtils::to_point(const Point &point) +{ + return VD::point_type(point.x(), point.y()); +} + +Slic3r::Vec2d VoronoiGraphUtils::to_point_d(const VD::vertex_type *vertex) +{ + return Vec2d(vertex->x(), vertex->y()); +} + +VoronoiGraphUtils::VD::segment_type VoronoiGraphUtils::to_segment(const Line &line) +{ + return VD::segment_type(to_point(line.a), to_point(line.b)); +} + +Slic3r::Point VoronoiGraphUtils::to_direction(const VD::edge_type *edge) +{ + return to_direction_d(edge).cast(); +} + +Slic3r::Vec2d VoronoiGraphUtils::to_direction_d(const VD::edge_type *edge) +{ + const VD::vertex_type *v0 = edge->vertex0(); + const VD::vertex_type *v1 = edge->vertex1(); + return Vec2d(v1->x() - v0->x(), v1->y() - v0->y()); +} + +bool VoronoiGraphUtils::is_coord_in_limits(const VD::coordinate_type &coord, + const coord_t & source, + double max_distance) +{ + VD::coordinate_type min_val = source - max_distance; + VD::coordinate_type max_val = source + max_distance; + if (coord > max_val) return false; + if (coord < min_val) return false; + return true; +} + +bool VoronoiGraphUtils::is_point_in_limits(const VD::vertex_type *vertex, + const Point & source, + double max_distance) +{ + if (vertex == nullptr) return false; + return is_coord_in_limits(vertex->x(), source.x(), max_distance) && + is_coord_in_limits(vertex->y(), source.y(), max_distance); +} + +// create line segment between (in the middle) points. With size depend on their distance +Slic3r::Line VoronoiGraphUtils::create_line_between_source_points( + const Point &point1, const Point &point2, double maximal_distance) +{ + Point middle = (point1 + point2) / 2; + Point diff = point1 - point2; + double distance_2 = diff.x() * static_cast(diff.x()) + + diff.y() * static_cast(diff.y()); + double half_distance = sqrt(distance_2) / 2.; + double half_distance_2 = distance_2 / 4; + double size = sqrt(maximal_distance * maximal_distance - half_distance_2); + // normalized direction to side multiplied by size/2 + double scale = size / half_distance / 2; + Point side_dir(-diff.y() * scale, diff.x() * scale); + return Line(middle - side_dir, middle + side_dir); +} + +std::optional VoronoiGraphUtils::to_line( + const VD::edge_type &edge, const Points &points, double maximal_distance) +{ + assert(edge.is_linear()); + assert(edge.is_primary()); + const Point &p1 = retrieve_point(points, *edge.cell()); + const Point &p2 = retrieve_point(points, *edge.twin()->cell()); + const VD::vertex_type *v0 = edge.vertex0(); + const VD::vertex_type *v1 = edge.vertex1(); + + bool use_v1 = false; // v0 == nullptr or out of limits + bool use_double_precision = false; + bool use_both = false; + if (edge.is_finite()) { + bool is_v0_in_limit = is_point_in_limits(v0, p1, maximal_distance); + bool is_v1_in_limit = is_point_in_limits(v1, p1, maximal_distance); + if (!is_v0_in_limit) { + use_v1 = true; + if (!is_v1_in_limit) { + use_double_precision = true; + use_both = true; + } + } else if (is_v1_in_limit) { + // normal full edge line segment + return Line(to_point(v0), to_point(v1)); + } + } else if (v0 == nullptr) { + if (v1 == nullptr) + {// both vertex are nullptr, create edge between points + return create_line_between_source_points(p1, p2, maximal_distance); + } + if (!is_point_in_limits(v1, p1, maximal_distance)) + use_double_precision = true; + use_v1 = true; + } else if (!is_point_in_limits(v0, p1, maximal_distance)) { + use_double_precision = true; + if (v1 != nullptr) + use_v1 = true; // v1 is in + } + + Point direction = (use_v1) ? + Point(p2.y() - p1.y(), p1.x() - p2.x()) : + Point(p1.y() - p2.y(), p2.x() - p1.x()); + const VD::vertex_type* edge_vertex = (use_v1) ? v1 : v0; + // koeficient for crop line + if (!use_double_precision) { + Point ray_point = to_point(edge_vertex); + Line ray(ray_point, ray_point + direction); + return LineUtils::crop_half_ray(ray, p1, maximal_distance); + } + std::optional segment; + if (use_both) { + Linef edge_segment(to_point_d(v0), to_point_d(v1)); + segment = LineUtils::crop_line(edge_segment, p1, maximal_distance); + } else { + // Vertex can't be used as start point because data type limitation + // Explanation for shortening line is in Test::bad_vertex + Vec2d middle = (p1.cast() + p2.cast()) / 2.; + Vec2d vertex = to_point_d(edge_vertex); + Vec2d vertex_direction = (vertex - middle); + Vec2d vertex_dir_abs(fabs(vertex_direction.x()), fabs(vertex_direction.y())); + double divider = (vertex_dir_abs.x() > vertex_dir_abs.y()) ? + vertex_dir_abs.x() / maximal_distance : + vertex_dir_abs.y() / maximal_distance; + Vec2d vertex_dir_short = vertex_direction / divider; + Vec2d start_point = middle + vertex_dir_short; + Linef line_short(start_point, start_point + direction.cast()); + segment = LineUtils::crop_half_ray(line_short, p1, maximal_distance); + } + if (!segment.has_value()) return {}; + return Line(segment->a.cast(), segment->b.cast()); +} + + +Slic3r::Polygon VoronoiGraphUtils::to_polygon(const Lines &lines, + const Point ¢er, + double maximal_distance, + double minimal_distance, + size_t count_points) +{ + assert(minimal_distance > 0.); + assert(maximal_distance > minimal_distance); + assert(count_points >= 3); + if (lines.empty()) + return PolygonUtils::create_regular(count_points, maximal_distance, center); + + Points points; + points.reserve(std::max(lines.size(), count_points)); + const Line *prev_line = &lines.back(); + double max_angle = 2 * M_PI / count_points; + for (const Line &line : lines) { + const Point &p1 = prev_line->b; + const Point &p2 = line.a; + prev_line = &line; + Point diff = p1-p2; + if (abs(diff.x()) < minimal_distance && + abs(diff.y()) < minimal_distance) { + Point avg = (p1 + p2) / 2; + points.push_back(avg); + continue; + } + Point v1 = p1 - center; + Point v2 = p2 - center; + double a1 = std::atan2(v1.y(), v1.x()); + double a2 = std::atan2(v2.y(), v2.x()); + + double diff_angle = a2 - a1; + if(diff_angle < 0.) diff_angle += 2 * M_PI; + if(diff_angle > 2 * M_PI) diff_angle -= 2 * M_PI; + + size_t count_segment = std::floor(fabs(diff_angle) / max_angle) + 1; + double increase_angle = diff_angle / count_segment; + points.push_back(p1); + for (size_t i = 1; i < count_segment; i++) { + double angle = a1 + i*increase_angle; + double x = cos(angle) * maximal_distance + center.x(); + assert(x < std::numeric_limits::max()); + assert(x > std::numeric_limits::min()); + double y = sin(angle) * maximal_distance + center.y(); + assert(y < std::numeric_limits::max()); + assert(y > std::numeric_limits::min()); + points.emplace_back(x,y); + } + points.push_back(p2); + } + Polygon polygon(points); + if (!polygon.contains(center)) { + draw(polygon, lines, center); + } + assert(polygon.is_valid()); + assert(polygon.contains(center)); + assert(PolygonUtils::is_not_self_intersect(polygon, center)); + return polygon; +} + +Slic3r::Polygon VoronoiGraphUtils::to_polygon(const VD::cell_type & cell, + const Slic3r::Points &points, + double maximal_distance) +{ + Lines lines; + Point center = points[cell.source_index()]; + // Convenient way to iterate edges around Voronoi cell. + const VD::edge_type *edge = cell.incident_edge(); + do { + assert(edge->is_linear()); + if (!edge->is_primary()) continue; + std::optional line = to_line(*edge, points, maximal_distance); + if (!line.has_value()) continue; + Geometry::Orientation orientation = Geometry::orient(center, line->a, line->b); + // Can be rich on circle over source point edge + if (orientation == Geometry::Orientation::ORIENTATION_COLINEAR) + continue; + if (orientation == Geometry::Orientation::ORIENTATION_CW) + std::swap(line->a, line->b); + lines.push_back(*line); + } while ((edge = edge->next()) && edge != cell.incident_edge()); + assert(!lines.empty()); + if (lines.size() > 1) + LineUtils::sort_CCW(lines, center); + // preccission to decide when not connect neighbor points + double min_distance = maximal_distance / 1000.; + size_t count_point = 6; // count added points + Slic3r::Polygon polygon = to_polygon(lines, center, maximal_distance, min_distance, count_point); +#ifdef SLA_SVG_VISUALIZATION_CELL_2_POLYGON + { + std::cout << "cell " << cell.source_index() << " has " << lines.size() << "edges" << std::endl; + BoundingBox bbox(center - Point(maximal_distance, maximal_distance), + center + Point(maximal_distance, maximal_distance)); + static int counter = 0; + std::string filename = "polygon" + std::to_string(counter++) + ".svg"; + SVG svg(filename.c_str(), bbox); + svg.draw(center, "lightgreen", maximal_distance); + svg.draw(polygon, "lightblue"); + int index = 0; + for (auto &line : lines) { + svg.draw(line); + svg.draw_text(line.a, ("A"+std::to_string(++index)).c_str(), "green"); + svg.draw_text(line.b, ("B" + std::to_string(index)).c_str(), "blue"); + } + svg.draw(center, "red", maximal_distance / 100); + } +#endif /* SLA_SVG_VISUALIZATION_CELL_2_POLYGON */ + return polygon; +} + +VoronoiGraph::Node *VoronoiGraphUtils::getNode(VoronoiGraph & graph, + const VD::vertex_type *vertex, + const VD::edge_type * edge, + const Lines & lines) +{ + std::map &data = graph.data; + auto mapItem = data.find(vertex); + // return when exists + if (mapItem != data.end()) return &mapItem->second; + + // is new vertex (first edge to this vertex) + // calculate distance to islad border + fill item0 + const VD::cell_type *cell = edge->cell(); + // const VD::cell_type * cell2 = edge.twin()->cell(); + const Line &line = lines[cell->source_index()]; + // const Line & line1 = lines[cell2->source_index()]; + Point point = to_point(vertex); + double distance = line.distance_to(point); + + auto [iterator, success] = data.emplace(vertex, VoronoiGraph::Node(vertex, distance)); + + assert(success); + if (!success) return nullptr; + + return &iterator->second; +} + +Slic3r::Point VoronoiGraphUtils::retrieve_point(const Lines & lines, + const VD::cell_type &cell) +{ + using namespace boost::polygon; + assert(cell.source_category() == SOURCE_CATEGORY_SEGMENT_START_POINT || + cell.source_category() == SOURCE_CATEGORY_SEGMENT_END_POINT); + return (cell.source_category() == SOURCE_CATEGORY_SEGMENT_START_POINT) ? + lines[cell.source_index()].a : + lines[cell.source_index()].b; +} + +const Slic3r::Point &VoronoiGraphUtils::retrieve_point( + const Points &points, const VD::cell_type &cell) +{ + assert(cell.contains_point()); + assert(cell.source_category() == boost::polygon::SOURCE_CATEGORY_SINGLE_POINT); + return points[cell.source_index()]; +} + +Slic3r::Point VoronoiGraphUtils::get_parabola_point( + const VD::edge_type ¶bola, const Slic3r::Lines &lines) +{ + using namespace boost::polygon; + assert(parabola.is_curved()); + const VD::cell_type& cell = (parabola.cell()->contains_point())? + *parabola.cell() : *parabola.twin()->cell(); + assert(cell.contains_point()); + assert(cell.source_category() == SOURCE_CATEGORY_SEGMENT_START_POINT || + cell.source_category() == SOURCE_CATEGORY_SEGMENT_END_POINT); + return (cell.source_category() == SOURCE_CATEGORY_SEGMENT_START_POINT) ? + lines[cell.source_index()].a : + lines[cell.source_index()].b; +} + +Slic3r::Line VoronoiGraphUtils::get_parabola_line( + const VD::edge_type ¶bola, const Slic3r::Lines &lines) +{ + assert(parabola.is_curved()); + const VD::cell_type& cell = (parabola.cell()->contains_segment())? + *parabola.cell() : *parabola.twin()->cell(); + assert(cell.contains_segment()); + return lines[cell.source_index()]; +} + +Parabola VoronoiGraphUtils::get_parabola( + const VD::edge_type &edge, const Lines &lines) +{ + Point point = get_parabola_point(edge, lines); + Line line = get_parabola_line(edge, lines); + return Parabola(line, point); +} + +double VoronoiGraphUtils::calculate_length_of_parabola( + const VD::edge_type & edge, + const Lines & lines) +{ + Point v0 = to_point(edge.vertex0()); + Point v1 = to_point(edge.vertex1()); + ParabolaSegment parabola(get_parabola(edge, lines), v0, v1); + return ParabolaUtils::length(parabola); +} + +double VoronoiGraphUtils::calculate_length( + const VD::edge_type &edge, const Lines &lines) +{ + if (edge.is_linear()) { + const VD::vertex_type* v0 = edge.vertex0(); + const VD::vertex_type* v1 = edge.vertex1(); + double diffX = v0->x() - v1->x(); + double diffY = v0->y() - v1->y(); + return sqrt(diffX * diffX + diffY * diffY); + } + assert(edge.is_curved()); + return calculate_length_of_parabola(edge, lines); +} + +double VoronoiGraphUtils::calculate_max_width( + const VD::edge_type &edge, const Lines &lines) +{ + auto get_squared_distance = [&](const VD::vertex_type *vertex, + const Point &point) -> double { + Point point_v = to_point(vertex); + Vec2d vector = (point - point_v).cast(); + return vector.x() * vector.x() + vector.y() * vector.y(); + }; + auto max_width = [&](const Point& point)->double{ + return 2. * + sqrt(std::max(get_squared_distance(edge.vertex0(), point), + get_squared_distance(edge.vertex1(), point))); + }; + + if (edge.is_linear()) { + // edge line could be initialized by 2 points + if (edge.cell()->contains_point()) { + const Line &source_line = lines[edge.cell()->source_index()]; + Point source_point; + if (edge.cell()->source_category() == + boost::polygon::SOURCE_CATEGORY_SEGMENT_START_POINT) + source_point = source_line.a; + else { + assert(edge.cell()->source_category() == + boost::polygon::SOURCE_CATEGORY_SEGMENT_END_POINT); + source_point = source_line.b; + } + return max_width(source_point); + } + assert(edge.cell()->contains_segment()); + assert(!edge.twin()->cell()->contains_point()); + assert(edge.twin()->cell()->contains_segment()); + + const Line &line = lines[edge.cell()->source_index()]; + + Point v0 = to_point(edge.vertex0()); + Point v1 = to_point(edge.vertex1()); + double distance0 = line.perp_distance_to(v0); + double distance1 = line.perp_distance_to(v1); + return 2 * std::max(distance0, distance1); + } + assert(edge.is_curved()); + Parabola parabola = get_parabola(edge, lines); + // distance to point and line is same + // vector from edge vertex to parabola focus point + return max_width(parabola.focus); +} + +std::pair VoronoiGraphUtils::calculate_width( + const VD::edge_type &edge, const Lines &lines) +{ + if (edge.is_linear()) + return calculate_width_for_line(edge, lines); + return calculate_width_for_parabola(edge, lines); +} + +std::pair VoronoiGraphUtils::calculate_width_for_line( + const VD::edge_type &line_edge, const Lines &lines) +{ + assert(line_edge.is_linear()); + // edge line could be initialized by 2 points + if (line_edge.cell()->contains_point()) { + const Line &source_line = lines[line_edge.cell()->source_index()]; + Point source_point; + if (line_edge.cell()->source_category() == + boost::polygon::SOURCE_CATEGORY_SEGMENT_START_POINT) + source_point = source_line.a; + else { + assert(line_edge.cell()->source_category() == + boost::polygon::SOURCE_CATEGORY_SEGMENT_END_POINT); + source_point = source_line.b; + } + return min_max_width(line_edge, source_point); + } + assert(line_edge.cell()->contains_segment()); + assert(!line_edge.twin()->cell()->contains_point()); + assert(line_edge.twin()->cell()->contains_segment()); + const Line & line = lines[line_edge.cell()->source_index()]; + Point v0 = to_point(line_edge.vertex0()); + Point v1 = to_point(line_edge.vertex1()); + double distance0 = line.perp_distance_to(v0); + double distance1 = line.perp_distance_to(v1); + std::pair min_max(2 * static_cast(distance0), + 2 * static_cast(distance1)); + if (min_max.first > min_max.second) + std::swap(min_max.first, min_max.second); + return min_max; +} + +std::pair VoronoiGraphUtils::calculate_width_for_parabola( + const VD::edge_type ¶bola_edge, const Lines &lines) +{ + assert(parabola_edge.is_curved()); + // distance to point and line on parabola is same + Parabola parabola = get_parabola(parabola_edge, lines); + Point v0 = to_point(parabola_edge.vertex0()); + Point v1 = to_point(parabola_edge.vertex1()); + ParabolaSegment parabola_segment(parabola, v0, v1); + std::pair min_max = min_max_width(parabola_edge, parabola.focus); + if (ParabolaUtils::is_over_zero(parabola_segment)) { + min_max.first = parabola.directrix.perp_distance_to(parabola.focus); + } + return min_max; +} + +std::pair VoronoiGraphUtils::min_max_width( + const VD::edge_type &edge, const Point &point) +{ + auto distance = [](const VD::vertex_type *vertex, + const Point & point) -> coord_t { + Vec2d point_d = point.cast(); + Vec2d diff = point_d - to_point_d(vertex); + double distance = diff.norm(); + return static_cast(std::round(distance)); + }; + std::pair result(2 * distance(edge.vertex0(), point), + 2 * distance(edge.vertex1(), point)); + if (result.first > result.second) std::swap(result.first, result.second); + return result; +}; + +VoronoiGraph VoronoiGraphUtils::create_skeleton(const VD &vd, const Lines &lines) +{ + // vd should be annotated. + // assert(Voronoi::debug::verify_inside_outside_annotations(vd)); + + VoronoiGraph skeleton; + for (const VD::edge_type &edge : vd.edges()) { + if ( + // Ignore secondary and unbounded edges, they shall never be part + // of the skeleton. + edge.is_secondary() || edge.is_infinite() || + // Skip the twin edge of an edge, that has already been processed. + &edge > edge.twin() || + // Ignore outer edges. + (Voronoi::edge_category(edge) != + Voronoi::EdgeCategory::PointsInside && + Voronoi::edge_category(edge.twin()) != + Voronoi::EdgeCategory::PointsInside)) + continue; + + const VD::vertex_type * v0 = edge.vertex0(); + const VD::vertex_type * v1 = edge.vertex1(); + Voronoi::VertexCategory category0 = Voronoi::vertex_category(*v0); + Voronoi::VertexCategory category1 = Voronoi::vertex_category(*v1); + if (category0 == Voronoi::VertexCategory::Outside || + category1 == Voronoi::VertexCategory::Outside) + continue; + // only debug check annotation + if (category0 == Voronoi::VertexCategory::Unknown || + category1 == Voronoi::VertexCategory::Unknown) + return {}; // vd must be annotated + + double length = calculate_length(edge, lines); + coord_t min_width, max_width; + std::tie(min_width, max_width) = calculate_width(edge, lines); + auto neighbor_size = std::make_shared( + length, min_width, max_width); + + VoronoiGraph::Node *node0 = getNode(skeleton, v0, &edge, lines); + VoronoiGraph::Node *node1 = getNode(skeleton, v1, &edge, lines); + // add extended Edge to graph, both side + node0->neighbors.emplace_back(&edge, node1, neighbor_size); + node1->neighbors.emplace_back(edge.twin(), node0, std::move(neighbor_size)); + } + return skeleton; +} + +const VoronoiGraph::Node::Neighbor *VoronoiGraphUtils::get_neighbor( + const VoronoiGraph::Node *from, const VoronoiGraph::Node *to) +{ + for (const VoronoiGraph::Node::Neighbor &neighbor : from->neighbors) + if (neighbor.node == to) return &neighbor; + return nullptr; +} + +double VoronoiGraphUtils::get_neighbor_distance(const VoronoiGraph::Node *from, + const VoronoiGraph::Node *to) +{ + const VoronoiGraph::Node::Neighbor *neighbor = get_neighbor(from, to); + assert(neighbor != nullptr); + return neighbor->length(); +} + +VoronoiGraph::Path VoronoiGraphUtils::find_longest_path_on_circle( + const VoronoiGraph::Circle & circle, + const VoronoiGraph::ExPath::SideBranchesMap &side_branches) +{ + double half_circle_length = circle.length / 2.; + double distance_on_circle = 0; + + bool is_longest_revers_direction = false; + const VoronoiGraph::Node *longest_circle_node = nullptr; + const VoronoiGraph::Path *longest_circle_branch = nullptr; + double longest_branch_length = 0; + + bool is_short_revers_direction = false; + // find longest side branch + const VoronoiGraph::Node *prev_circle_node = nullptr; + for (const VoronoiGraph::Node *circle_node : circle.nodes) { + if (prev_circle_node != nullptr) + distance_on_circle += get_neighbor_distance(circle_node, + prev_circle_node); + prev_circle_node = circle_node; + + auto side_branches_item = side_branches.find(circle_node); + if (side_branches_item != side_branches.end()) { + // side_branches should be sorted by length + if (distance_on_circle > half_circle_length) + is_short_revers_direction = true; + const auto &longest_node_branch = side_branches_item->second.top(); + double circle_branch_length = longest_node_branch.length + + ((is_short_revers_direction) ? + (circle.length - + distance_on_circle) : + distance_on_circle); + if (longest_branch_length < circle_branch_length) { + longest_branch_length = circle_branch_length; + is_longest_revers_direction = is_short_revers_direction; + longest_circle_node = circle_node; + longest_circle_branch = &longest_node_branch; + } + } + } + assert(longest_circle_node != + nullptr); // only circle with no side branches + assert(longest_circle_branch != nullptr); + // almost same - double preccission + // distance_on_circle += get_neighbor_distance(circle.path.back(), + // circle.path.front()); assert(distance_on_circle == circle.length); + + // circlePath + auto circle_iterator = std::find(circle.nodes.begin(), circle.nodes.end(), + longest_circle_node); + VoronoiGraph::Nodes circle_path; + if (is_longest_revers_direction) { + circle_path = VoronoiGraph::Nodes(circle_iterator, circle.nodes.end()); + std::reverse(circle_path.begin(), circle_path.end()); + } else { + if (longest_circle_node != circle.nodes.front()) + circle_path = VoronoiGraph::Nodes(circle.nodes.begin() + 1, + circle_iterator + 1); + } + // append longest side branch + circle_path.insert(circle_path.end(), + longest_circle_branch->nodes.begin(), + longest_circle_branch->nodes.end()); + return {circle_path, longest_branch_length}; +} + +VoronoiGraph::Path VoronoiGraphUtils::find_longest_path_on_circles( + const VoronoiGraph::Node & input_node, + size_t finished_circle_index, + const VoronoiGraph::ExPath &ex_path) +{ + const std::vector &circles = ex_path.circles; + const auto &circle = circles[finished_circle_index]; + auto connected_circle_item = ex_path.connected_circle.find( + finished_circle_index); + // is only one circle + if (connected_circle_item == ex_path.connected_circle.end()) { + // find longest path over circle and store it into next_path + return find_longest_path_on_circle(circle, ex_path.side_branches); + } + + // multi circle + // find longest path over circles + const std::set &connected_circles = connected_circle_item->second; + + // collect all circle ndoes + std::set nodes; + nodes.insert(circle.nodes.begin(), circle.nodes.end()); + for (size_t circle_index : connected_circles) { + const auto &circle = circles[circle_index]; + nodes.insert(circle.nodes.begin(), circle.nodes.end()); + } + + // nodes are path throw circles + // length is sum path throw circles PLUS length of longest side_branch + VoronoiGraph::Path longest_path; + + // wide search by shortest distance for path over circle's node + // !! Do NOT use recursion, may cause stack overflow + std::set done; // all ready checked + // on top is shortest path + std::priority_queue, + VoronoiGraph::Path::OrderLengthFromShortest> + search_queue; + VoronoiGraph::Path start_path({&input_node}, 0.); + search_queue.emplace(start_path); + while (!search_queue.empty()) { + // shortest path from input_node + VoronoiGraph::Path path(std::move(search_queue.top())); + search_queue.pop(); + const VoronoiGraph::Node &node = *path.nodes.back(); + if (done.find(&node) != done.end()) { // already checked + continue; + } + done.insert(&node); + for (const VoronoiGraph::Node::Neighbor &neighbor : node.neighbors) { + if (nodes.find(neighbor.node) == nodes.end()) + continue; // out of circles + if (done.find(neighbor.node) != done.end()) continue; + VoronoiGraph::Path neighbor_path = path; // make copy + neighbor_path.append(neighbor.node, neighbor.length()); + search_queue.push(neighbor_path); + + auto branches_item = ex_path.side_branches.find(neighbor.node); + // exist side from this neighbor node ? + if (branches_item == ex_path.side_branches.end()) continue; + const VoronoiGraph::Path &longest_branch = branches_item->second + .top(); + double length = longest_branch.length + neighbor_path.length; + if (longest_path.length < length) { + longest_path.length = length; + longest_path.nodes = neighbor_path.nodes; // copy path + } + } + } + + // create result path + assert(!longest_path.nodes.empty()); + longest_path.nodes.erase(longest_path.nodes.begin()); // remove input_node + assert(!longest_path.nodes.empty()); + auto branches_item = ex_path.side_branches.find(longest_path.nodes.back()); + if (branches_item == ex_path.side_branches.end()) { + // longest path ends on circle + return longest_path; + } + const VoronoiGraph::Path &longest_branch = branches_item->second.top(); + longest_path.nodes.insert(longest_path.nodes.end(), + longest_branch.nodes.begin(), + longest_branch.nodes.end()); + return longest_path; +} + +std::optional VoronoiGraphUtils::create_circle( + const VoronoiGraph::Path & path, + const VoronoiGraph::Node::Neighbor &neighbor) +{ + VoronoiGraph::Nodes passed_nodes = path.nodes; + // detection of circle + // not neccesary to check last one in path + auto end_find = passed_nodes.end() - 1; + const auto &path_item = std::find(passed_nodes.begin(), end_find, + neighbor.node); + if (path_item == end_find) return {}; // circle not detected + // separate Circle: + VoronoiGraph::Nodes circle_path(path_item, passed_nodes.end()); + // !!! Real circle lenght is calculated on detection of end circle + // now circle_length contain also lenght of path before circle + double circle_length = path.length + neighbor.length(); + // solve of branch length will be at begin of cirlce + return VoronoiGraph::Circle(std::move(circle_path), circle_length); +}; + +void VoronoiGraphUtils::merge_connected_circle( + VoronoiGraph::ExPath::ConnectedCircles &dst, + VoronoiGraph::ExPath::ConnectedCircles &src, + size_t dst_circle_count) +{ + std::set done; + for (const auto &item : src) { + size_t dst_index = dst_circle_count + item.first; + if (done.find(dst_index) != done.end()) continue; + done.insert(dst_index); + + std::set connected_circle; + for (const size_t &src_index : item.second) + connected_circle.insert(dst_circle_count + src_index); + + auto &dst_set = dst[dst_index]; + dst_set.merge(connected_circle); + + // write same information into connected circles + connected_circle = dst_set; // copy + connected_circle.insert(dst_index); + for (size_t prev_connection_idx : dst_set) { + done.insert(prev_connection_idx); + for (size_t connected_circle_idx : connected_circle) { + if (connected_circle_idx == prev_connection_idx) continue; + dst[prev_connection_idx].insert(connected_circle_idx); + } + } + } +} + +void VoronoiGraphUtils::append_neighbor_branch(VoronoiGraph::ExPath &dst, + VoronoiGraph::ExPath &src) +{ + // move side branches + if (!src.side_branches.empty()) + dst.side_branches + .insert(std::make_move_iterator(src.side_branches.begin()), + std::make_move_iterator(src.side_branches.end())); + + // move circles + if (!src.circles.empty()) { + // copy connected circles indexes + if (!src.connected_circle.empty()) { + merge_connected_circle(dst.connected_circle, src.connected_circle, + dst.circles.size()); + } + dst.circles.insert(dst.circles.end(), + std::make_move_iterator(src.circles.begin()), + std::make_move_iterator(src.circles.end())); + } +} + +void VoronoiGraphUtils::reshape_longest_path(VoronoiGraph::ExPath &path) +{ + assert(path.nodes.size() >= 1); + + double actual_length = 0.; + const VoronoiGraph::Node *prev_node = nullptr; + VoronoiGraph::Nodes origin_path = path.nodes; // make copy + // index to path + size_t path_index = 0; + for (const VoronoiGraph::Node *node : origin_path) { + if (prev_node != nullptr) { + ++path_index; + actual_length += get_neighbor_distance(prev_node, node); + } + prev_node = node; + // increase actual length + + auto side_branches_item = path.side_branches.find(node); + if (side_branches_item == path.side_branches.end()) + continue; // no side branches + VoronoiGraph::ExPath::SideBranches &branches = side_branches_item + ->second; + if (actual_length >= branches.top().length) + continue; // no longer branch + + auto end_path = path.nodes.begin() + path_index; + VoronoiGraph::Path side_branch({path.nodes.begin(), end_path}, + actual_length); + std::reverse(side_branch.nodes.begin(), side_branch.nodes.end()); + VoronoiGraph::Path new_main_branch(std::move(branches.top())); + branches.pop(); + std::reverse(new_main_branch.nodes.begin(), + new_main_branch.nodes.end()); + // add old main path store into side branches - may be it is not neccessary + branches.push(std::move(side_branch)); + + // swap side branch with main branch + path.nodes.erase(path.nodes.begin(), end_path); + path.nodes.insert(path.nodes.begin(), new_main_branch.nodes.begin(), + new_main_branch.nodes.end()); + + path.length += new_main_branch.length; + path.length -= actual_length; + path_index = new_main_branch.nodes.size(); + actual_length = new_main_branch.length; + } +} + +VoronoiGraph::ExPath VoronoiGraphUtils::create_longest_path( + const VoronoiGraph::Node *start_node) +{ + VoronoiGraph::ExPath longest_path; + CallStack call_stack; + call_stack.emplace( + std::make_unique(longest_path, start_node)); + + // depth search for longest path in graph + while (!call_stack.empty()) { + std::unique_ptr stack_function = std::move( + call_stack.top()); + call_stack.pop(); + stack_function->process(call_stack); + // stack function deleted + } + + reshape_longest_path(longest_path); + // after reshape it shoud be longest path for whole Voronoi Graph + return longest_path; +} + +const VoronoiGraph::Node::Neighbor *VoronoiGraphUtils::get_twin(const VoronoiGraph::Node::Neighbor& neighbor) +{ + auto twin_edge = neighbor.edge->twin(); + for (const VoronoiGraph::Node::Neighbor &twin_neighbor : neighbor.node->neighbors) { + if (twin_neighbor.edge == twin_edge) return &twin_neighbor; + } + assert(false); + return nullptr; +} + +const VoronoiGraph::Node *VoronoiGraphUtils::get_twin_node(const VoronoiGraph::Node::Neighbor &neighbor) +{ + return get_twin(neighbor)->node; +} + +bool VoronoiGraphUtils::is_opposit_direction(const VD::edge_type *edge, const Line &line) +{ + Point dir_line = LineUtils::direction(line); + Point dir_edge = VoronoiGraphUtils::to_direction(edge); + return !PointUtils::is_same_direction(dir_line, dir_edge); +} + +Slic3r::Point VoronoiGraphUtils::create_edge_point( + const VoronoiGraph::Position &position) +{ + return create_edge_point(position.neighbor->edge, position.ratio); +} + +Slic3r::Point VoronoiGraphUtils::create_edge_point(const VD::edge_type *edge, + double ratio) +{ + const VD::vertex_type *v0 = edge->vertex0(); + const VD::vertex_type *v1 = edge->vertex1(); + if (ratio <= std::numeric_limits::epsilon()) + return Point(v0->x(), v0->y()); + if (ratio >= 1. - std::numeric_limits::epsilon()) + return Point(v1->x(), v1->y()); + + if (edge->is_linear()) { + Point dir(v1->x() - v0->x(), v1->y() - v0->y()); + // normalize + dir *= ratio; + return Point(v0->x() + dir.x(), v0->y() + dir.y()); + } + + assert(edge->is_curved()); + // TODO: distance on curve + + // approx by line + Point dir(v1->x() - v0->x(), v1->y() - v0->y()); + dir *= ratio; + return Point(v0->x() + dir.x(), v0->y() + dir.y()); +} + +// NOTE: Heuristic is bad -> Width is not linear on edge e.g. VD of hexagon +// Solution: Edge has to know width changes. +VoronoiGraph::Position VoronoiGraphUtils::get_position_with_width( + const VoronoiGraph::Node::Neighbor *neighbor, coord_t width, const Slic3r::Lines &lines) +{ + VoronoiGraph::Position result(neighbor, 0.); + const VD::edge_type *edge = neighbor->edge; + if (edge->is_curved()) { + // Every point on curve has same distance from outline + // !!! NOT TRUE !!! + // Only same distance from point and line !!! + // TODO: Fix it + return result; + } + assert(edge->is_finite()); + Slic3r::Line edge_line(to_point(edge->vertex0()), to_point(edge->vertex1())); + const Slic3r::Line &source_line = lines[edge->cell()->source_index()]; + if (LineUtils::is_parallel(edge_line, source_line)) { + // Every point on parallel lines has same distance + return result; + } + + double half_width = width / 2.; + + double a_dist = source_line.perp_distance_to(edge_line.a); + double b_dist = source_line.perp_distance_to(edge_line.b); + + // check if half_width is in range from a_dist to b_dist + if (a_dist > b_dist) { + if (b_dist >= half_width) { + // vertex1 is closer to width + result.ratio = 1.; + return result; + } else if (a_dist <= half_width) { + // vertex0 is closer to width + return result; + } + } else { + // a_dist < b_dist + if (a_dist >= half_width) { + // vertex0 is closer to width + return result; + } else if (b_dist <= half_width) { + // vertex1 is closer to width + result.ratio = 1.; + return result; + } + } + result.ratio = fabs((a_dist - half_width) / (a_dist - b_dist)); + return result; +} + +std::pair VoronoiGraphUtils::point_on_lines( + const VoronoiGraph::Position &position, const Lines &lines) +{ + const VD::edge_type *edge = position.neighbor->edge; + + // TODO: solve point on parabola + //assert(edge->is_linear()); + + Point edge_point = create_edge_point(position); + auto point_on_line = [&](const VD::edge_type *edge) -> Point { + assert(edge->is_finite()); + const VD::cell_type *cell = edge->cell(); + size_t line_index = cell->source_index(); + const Line &line = lines[line_index]; + using namespace boost::polygon; + if (cell->source_category() == SOURCE_CATEGORY_SEGMENT_START_POINT) { + return line.a; + } + if (cell->source_category() == SOURCE_CATEGORY_SEGMENT_END_POINT) { + return line.b; + } + + Point dir = LineUtils::direction(line); + Line intersecting_line(edge_point, edge_point + PointUtils::perp(dir)); + std::optional intersection = LineUtils::intersection(line, intersecting_line); + assert(intersection.has_value()); + return intersection->cast(); + }; + + return {point_on_line(edge), point_on_line(edge->twin())}; +} + +namespace{ +using namespace Slic3r; +using VD = Slic3r::Geometry::VoronoiDiagram; +double get_distance_sq(const VD::edge_type &edge, const Point &point, double &edge_ratio) { + // TODO: find closest point on curve edge + // if (edge.is_linear()) { + + // get line foot point, inspired Geometry::foot_pt + Vec2d v0 = VoronoiGraphUtils::to_point_d(edge.vertex0()); + Vec2d v = point.cast() - v0; + Vec2d v1 = VoronoiGraphUtils::to_point_d(edge.vertex1()); + Vec2d edge_dir = v1 - v0; + double l2 = edge_dir.squaredNorm(); + edge_ratio = v.dot(edge_dir) / l2; + // IMPROVE: not neccesary to calculate point if (edge_ratio > 1 || edge_ratio < 0) + Point edge_point; + if (edge_ratio > 1.) + edge_point = v1.cast(); + else if (edge_ratio < 0.) + edge_point = v0.cast(); + else { // foot point + edge_point = (v0 + edge_dir * edge_ratio).cast(); + } + return (point - edge_point).cast().squaredNorm(); +} +} + +VoronoiGraph::Position VoronoiGraphUtils::align( + const VoronoiGraph::Position &position, const Point &to, double max_distance) +{ + // for each neighbor in max distance try align edge + struct NodeDistance + { + const VoronoiGraph::Node *node; + double distance; // distance to search for closest point + NodeDistance(const VoronoiGraph::Node *node, double distance) + : node(node), distance(distance) + {} + }; + std::queue process; + const VoronoiGraph::Node::Neighbor* neighbor = position.neighbor; + double from_distance = neighbor->length() * position.ratio; + if (from_distance < max_distance) { + const VoronoiGraph::Node *from_node = VoronoiGraphUtils::get_twin_node(*neighbor); + process.emplace(from_node, from_distance); + } + double to_distance = neighbor->length() * (1 - position.ratio); + if (to_distance < max_distance) { + const VoronoiGraph::Node *to_node = neighbor->node; + process.emplace(to_node, to_distance); + } + if (process.empty()) { + const VoronoiGraph::Node *node = (position.ratio < 0.5) ? + VoronoiGraphUtils::get_twin_node(*neighbor) : neighbor->node; + process.emplace(node, max_distance); + } + + double closest_distance_sq = std::numeric_limits::max(); + VoronoiGraph::Position closest; + + std::set done; + while (!process.empty()) { + NodeDistance nd = process.front(); // copy + process.pop(); + if (done.find(nd.node) != done.end()) continue; + done.insert(nd.node); + for (const auto &neighbor : nd.node->neighbors) { + if (done.find(neighbor.node) != done.end()) continue; + double ratio; + double distance_sq = get_distance_sq(*neighbor.edge, to, ratio); + if (closest_distance_sq > distance_sq) { + closest_distance_sq = distance_sq; + closest = VoronoiGraph::Position(&neighbor, ratio); + } + double from_start = nd.distance + neighbor.length(); + if (from_start < max_distance) + process.emplace(neighbor.node, from_start); + } + } + return closest; +} + +const VoronoiGraph::Node *VoronoiGraphUtils::getFirstContourNode( + const VoronoiGraph &graph) +{ + for (const auto &[key, value] : graph.data) { + const VD::vertex_type & vertex = *key; + Voronoi::VertexCategory category = Voronoi::vertex_category(vertex); + if (category == Voronoi::VertexCategory::OnContour) { + return &value; + } + } + return nullptr; +} + +coord_t VoronoiGraphUtils::get_max_width(const VoronoiGraph::Nodes &path) +{ + coord_t max = 0; + const VoronoiGraph::Node *prev_node = nullptr; + for (const VoronoiGraph::Node *node : path) { + if (prev_node == nullptr) { + prev_node = node; + continue; + } + const VoronoiGraph::Node::Neighbor *neighbor = get_neighbor(prev_node, node); + if (max < neighbor->max_width()) max = neighbor->max_width(); + prev_node = node; + } + return max; +} + +coord_t VoronoiGraphUtils::get_max_width( + const VoronoiGraph::ExPath &longest_path) +{ + coord_t max = get_max_width(longest_path.nodes); + for (const auto &side_branches_item : longest_path.side_branches) { + const VoronoiGraph::Node *prev_node = side_branches_item.first; + VoronoiGraph::ExPath::SideBranches side_branches = side_branches_item.second; // !!! copy + while (!side_branches.empty()) { + const VoronoiGraph::Path &side_path = side_branches.top(); + const VoronoiGraph::Node::Neighbor *first_neighbor = + get_neighbor(prev_node, side_path.nodes.front()); + coord_t max_side_branch = std::max( + get_max_width(side_path.nodes), first_neighbor->max_width()); + if (max < max_side_branch) max = max_side_branch; + side_branches.pop(); + } + } + + for (const VoronoiGraph::Circle &circle : longest_path.circles) { + const VoronoiGraph::Node::Neighbor *first_neighbor = + get_neighbor(circle.nodes.front(), circle.nodes.back()); + double max_circle = std::max( + first_neighbor->max_width(), get_max_width(circle.nodes)); + if (max < max_circle) max = max_circle; + } + + return max; +} + +// !!! is slower than go along path +coord_t VoronoiGraphUtils::get_max_width(const VoronoiGraph::Node *node) +{ + coord_t max = 0; + std::set done; + std::queue process; + process.push(node); + while (!process.empty()) { + const VoronoiGraph::Node *actual_node = process.front(); + process.pop(); + if (done.find(actual_node) != done.end()) continue; + for (const VoronoiGraph::Node::Neighbor& neighbor: actual_node->neighbors) { + if (done.find(neighbor.node) != done.end()) continue; + process.push(neighbor.node); + if (max < neighbor.max_width()) max = neighbor.max_width(); + } + done.insert(actual_node); + } + return max; +} + +bool VoronoiGraphUtils::ends_in_distanace(const VoronoiGraph::Position &position, coord_t max_distance) { + const VoronoiGraph::Node *node = position.neighbor->node; + coord_t rest_distance = max_distance - position.calc_rest_distance(); + if (rest_distance < 0) + return false; + + // speed up - end of gpraph is no need investigate further + if (node->neighbors.size() == 1) + return true; + + // Already processed nodes + std::set done; + done.insert(get_twin_node(*position.neighbor)); + + struct Next{ + const VoronoiGraph::Node *node; + coord_t rest_distance; + }; + // sorted by distance from position from biggest + std::vector process_queue; + do { + done.insert(node); + for (const VoronoiGraph::Node::Neighbor &neighbor: node->neighbors){ + const VoronoiGraph::Node *neighbor_node = neighbor.node; + // Check whether node is already done + // Nodes are processed from closer to position + // soo done neighbor have to has bigger rest_distance + if (done.find(neighbor_node) != done.end()) + // node is already explore + continue; + + coord_t neighbor_rest = rest_distance - static_cast(neighbor.length()); + if (neighbor_rest < 0) + // exist node far than max distance + return false; + + // speed up - end of gpraph is no need to add to the process queue + if (neighbor_node->neighbors.size() == 1) + continue; + + // check whether exist in queue this node with farer path and fix it + auto it = std::find_if(process_queue.begin(), process_queue.end(), + [neighbor_node](const Next &n) { return n.node == neighbor_node;}); + if (it == process_queue.end()){ + process_queue.emplace_back(Next{neighbor_node, neighbor_rest}); + } else if (it->rest_distance < neighbor_rest) { + // found shorter path to node + it->rest_distance = neighbor_rest; + } + } + + if (process_queue.empty()) + return true; + + // find biggest rest distance -> closest to input position + auto next = std::max_element(process_queue.begin(), process_queue.end(), + [](const Next& n1, const Next& n2){ + return n1.rest_distance < n2.rest_distance; + }); + rest_distance = next->rest_distance; + node = next->node; + process_queue.erase(next); // process queue pop + } while (true); +} + +void VoronoiGraphUtils::for_neighbor_at_distance( + const VoronoiGraph::Position &position, + coord_t max_distance, + std::function fnc) +{ + coord_t act_distance = position.calc_distance(); + const VoronoiGraph::Node *act_node = position.neighbor->node; + const VoronoiGraph::Node *twin_node = get_twin_node(*position.neighbor); + std::set done; + done.insert(twin_node); + done.insert(act_node); + std::queue> process; + coord_t distance = position.calc_rest_distance(); + if (distance < max_distance) process.push({twin_node, distance}); + + while (true) { + const VoronoiGraph::Node *next_node = nullptr; + coord_t next_distance = 0; + for (const auto &neighbor : act_node->neighbors) { + if (done.find(neighbor.node) != done.end()) + continue; // already checked + done.insert(neighbor.node); + + fnc(neighbor, act_distance); + + coord_t length = static_cast(neighbor.length()); + coord_t distance = act_distance + length; + if (distance >= max_distance) continue; + if (next_node == nullptr) { + next_node = neighbor.node; + next_distance = distance; + } else { + process.push({neighbor.node, distance}); + } + } + if (next_node != nullptr) { // exist next node + act_node = next_node; + act_distance = next_distance; + } else if (!process.empty()) { // exist next process + act_node = process.front().first; + act_distance = process.front().second; + process.pop(); + } else { // no next node neither process + break; + } + } +} + +double VoronoiGraphUtils::outline_angle(const VoronoiGraph::Node::Neighbor &neighbor, const Lines& lines) +{ + assert(neighbor.edge->is_linear()); + assert(neighbor.min_width() == 0); + const VD::cell_type *c1 = neighbor.edge->cell(); + const VD::cell_type *c2 = neighbor.edge->twin()->cell(); + + const Line &l1 = lines[c1->source_index()]; + const Line &l2 = lines[c2->source_index()]; + + Vec2d d1 = LineUtils::direction(l1).cast(); + Vec2d d2 = LineUtils::direction(l2).cast(); + + double dot = d1.dot(-d2); + return std::acos(dot/d1.norm() / d2.norm()); +} + +void VoronoiGraphUtils::draw(SVG & svg, + const VoronoiGraph &graph, + const Lines & lines, + const SampleConfig &config, + bool pointer_caption) +{ + coord_t width = config.head_radius / 10; + LineUtils::draw(svg, lines, "black", width, false); + + auto print_address = [&](const Point& p, const char* prefix, void * addr, const char* color){ + if (pointer_caption) { + std::stringstream ss; + ss << prefix << std::hex << reinterpret_cast(addr); + std::string s = ss.str(); + svg.draw_text(p, s.c_str(), color, 6); + } + }; + + std::vector skeleton_colors{ + "yellow", // thin (min+max belowe thin) + "yellowgreen", // on way to thin (max is above thin) + "limegreen", // between (inside histerezis) + "forestgreen", // on way to thick (min is belove thick) + "darkgreen" // thick (min+max above thick) + }; + auto get_color = [&](const VoronoiGraph::Node::Neighbor &n) { + if (n.min_width() > config.thin_max_width){ + return skeleton_colors[4]; + } else if (n.max_width() < config.thick_min_width){ + return skeleton_colors[0]; + } else if (n.min_width() < config.thin_max_width && + n.max_width() > config.thick_min_width){ + return skeleton_colors[2]; + } else if (n.min_width() < config.thick_min_width){ + return skeleton_colors[1]; + } else if (n.max_width() > config.thin_max_width) { + return skeleton_colors[3]; + } + assert(false); + return "gray"; + }; + + for (const auto &[key, value] : graph.data) { + Point p(key->x(), key->y()); + svg.draw(p, "lightgray", width); + print_address(p, "vertex ptr ",(void*)key, "lightgray"); + for (const auto &n : value.neighbors) { + Point from = to_point(n.edge->vertex0()); + Point to = to_point(n.edge->vertex1()); + bool is_second = n.edge->vertex0() > n.edge->vertex1(); + Point center = (from + to) / 2; + Point p = center + ((is_second) ? Point(0., -2e6) : + Point(0., 2e6)); + print_address(p, "neighbor ptr ", (void *) &n, "gray"); + if (is_second) continue; + const char *color = get_color(n); + if (pointer_caption) { + std::string width_str = "width min=" + std::to_string(n.min_width()) + + " max=" + std::to_string(n.max_width()); + svg.draw_text(center + Point(-6e6, 0.), width_str.c_str(), color, 6); + } + draw(svg, *n.edge, lines, color, width); + } + } +} + +void VoronoiGraphUtils::draw(SVG & svg, + const VD::edge_type &edge, + const Lines & lines, + const char * color, + coord_t width) +{ + Point from = to_point(edge.vertex0()); + Point to = to_point(edge.vertex1()); + if (edge.is_curved()) { + Parabola p = get_parabola(edge, lines); + ParabolaSegment ps(p, from, to); + ParabolaUtils::draw(svg, ps, color, width); + return; + } + svg.draw(Line(from, to), color, width); +} + + +void VoronoiGraphUtils::draw(SVG & svg, + const VoronoiGraph::Nodes &path, + coord_t width, + const char * color, + bool finish, + bool caption) +{ + const VoronoiGraph::Node *prev_node = (finish) ? path.back() : nullptr; + int index = 0; + for (auto &node : path) { + ++index; + if (prev_node == nullptr) { + prev_node = node; + continue; + } + + Point from = to_point(prev_node->vertex); + Point to = to_point(node->vertex); + svg.draw(Line(from, to), color, width); + if (caption) { + svg.draw_text(from, std::to_string(index - 1).c_str(), color, 6); + svg.draw_text(to, std::to_string(index).c_str(), color, 6); + } + prev_node = node; + } +} + +void VoronoiGraphUtils::draw(SVG & svg, + const VoronoiGraph::ExPath &path, + coord_t width) +{ + const char *circlePathColor = "green"; + const char *sideBranchesColor = "blue"; + const char *mainPathColor = "red"; + + for (auto &circle : path.circles) { + draw(svg, circle.nodes, width, circlePathColor, true); + Point center(0, 0); + for (auto p : circle.nodes) { + center += to_point(p->vertex); + } + center.x() /= circle.nodes.size(); + center.y() /= circle.nodes.size(); + + svg.draw_text(center, + ("C" + std::to_string(&circle - &path.circles.front())) + .c_str(), + circlePathColor); + } + + for (const auto &branches : path.side_branches) { + auto tmp = branches.second; // copy + while (!tmp.empty()) { + const auto &branch = tmp.top(); + auto path = branch.nodes; + path.insert(path.begin(), branches.first); + draw(svg, path, width, sideBranchesColor); + tmp.pop(); + } + } + + draw(svg, path.nodes, width, mainPathColor); +} + +void VoronoiGraphUtils::draw(const Polygon &polygon, + const Lines & lines, + const Point & center) +{ + SVG svg("Bad_polygon.svg", {polygon.points}); + svg.draw(polygon, "orange"); + LineUtils::draw(svg, lines, "red", 0., true, true); + svg.draw(center); +} \ No newline at end of file diff --git a/src/libslic3r/SLA/SupportIslands/VoronoiGraphUtils.hpp b/src/libslic3r/SLA/SupportIslands/VoronoiGraphUtils.hpp new file mode 100644 index 0000000000..d1c6f1f6cc --- /dev/null +++ b/src/libslic3r/SLA/SupportIslands/VoronoiGraphUtils.hpp @@ -0,0 +1,506 @@ +#ifndef slic3r_SLA_SuppotstIslands_VoronoiGraphUtils_hpp_ +#define slic3r_SLA_SuppotstIslands_VoronoiGraphUtils_hpp_ + +#include +#include +#include +#include +#include +#include + +#include "VoronoiGraph.hpp" +#include "Parabola.hpp" +#include "SampleConfig.hpp" +#include "SupportIslandPoint.hpp" + +namespace Slic3r::sla { + +/// +/// Class which contain collection of static function +/// for work with Voronoi Graph. +/// +class VoronoiGraphUtils +{ + using VD = Slic3r::Geometry::VoronoiDiagram; + +public: + VoronoiGraphUtils() = delete; + + /// + /// Convert coordinate type between voronoi and slicer format + /// + /// Coordinate + /// When it is possible than cast it otherwise empty optional + static coord_t to_coord(const VD::coordinate_type &coord); + + /// + /// Convert Vodonoi diagram vertex type to Slicer Point + /// decrease precission by rounding + /// + /// Input point pointer(double precission) + /// Converted point(int preccission) + static Point to_point(const VD::vertex_type *vertex); + + /// + /// Convert Slic3r point to Vodonoi point type + /// extend precission + /// + /// Input point(int preccission) + /// Converted vertex(double precission) + static VD::point_type to_point(const Point &point); + + /// + /// Convert point type between voronoi and slicer format + /// + /// Input vertex + /// created vector + static Vec2d to_point_d(const VD::vertex_type* vertex); + + /// + /// Convert Slic3r Line to Voronoi segment type + /// + /// input line(int preccission) + /// output segment(double precission) + static VD::segment_type to_segment(const Line &line); + + /// + /// create direction from Voronoi edge + /// + /// input + /// direction --> (vertex1 - vertex0) + static Point to_direction(const VD::edge_type *edge); + + /// + /// create direction from Voronoi edge + /// + /// input + /// direction --> (vertex1 - vertex0) + static Vec2d to_direction_d(const VD::edge_type *edge); + + /// + /// check if coord is in limits for coord_t + /// + /// input value + /// VD source point coordinate + /// Maximal distance from source + /// True when coord is in +- max_distance from source otherwise FALSE. + static bool is_coord_in_limits(const VD::coordinate_type &coord, + const coord_t & source, + double max_distance); + + /// + /// Check x and y values of vertex + /// + /// input vertex + /// VD source point + /// Maximal distance from source + /// True when both coord are in limits given by source and max distance otherwise FALSE + static bool is_point_in_limits(const VD::vertex_type *vertex, + const Point & source, + double max_distance); +private: + /// + /// PRIVATE: function to help convert edge without vertex to line + /// + /// VD source point + /// VD source point + /// Maximal distance from source point + /// Line segment between lines + static Line create_line_between_source_points( + const Point &point1, const Point &point2, double maximal_distance); + +public: + /// + /// Convert edge to line + /// only for line edge + /// crop infinite edge by maximal distance from source point + /// inspiration in VoronoiVisualUtils::clip_infinite_edge + /// + /// VD edge + /// Source point for voronoi diagram + /// Maximal distance from source point + /// Croped line, when all line segment is out of max distance return empty optional + static std::optional to_line(const VD::edge_type &edge, + const Points & points, + double maximal_distance); + /// + /// close polygon defined by lines + /// close points will convert to their center + /// Mainly for convert to polygon + /// + /// Border of polygon, sorted lines CCW + /// Center point of polygon + /// Radius around center point + /// Merge points closer than minimal_distance + /// Count checking points, create help points for result polygon + /// Valid CCW polygon with center inside of polygon + static Polygon to_polygon(const Lines &lines, + const Point ¢er, + double maximal_distance, + double minimal_distance, + size_t count_points); + /// + /// Convert cell to polygon + /// Source for VD must be only point to create VD with only line segments + /// infinite cell are croped by maximal distance from source point + /// + /// cell from VD created only by points + /// source points for VD + /// maximal distance from source point - only for infinite edges(cells) + /// polygon created by cell + static Polygon to_polygon(const VD::cell_type &cell, + const Points & points, + double maximal_distance); + + // return node from graph by vertex, when no exists create one + static VoronoiGraph::Node *getNode(VoronoiGraph & graph, + const VD::vertex_type *vertex, + const VD::edge_type * edge, + const Lines & lines); + + /// + /// Extract point from lines, belongs to cell + /// ! Source for VD must be only lines + /// Main purpose parabola focus point from lines belongs to cell + /// inspiration in VoronoiVisualUtils::retrieve_point + /// + /// Source of Voronoi diagram + /// Cell inside of Voronoi diagram + /// Point from source lines. + static Point retrieve_point(const Lines &lines, const VD::cell_type &cell); + + /// + /// Extract point from lines + /// ! Source for VD must be only points + /// inspiration in VoronoiVisualUtils::retrieve_point + /// + /// Source of Voronoi diagram + /// Cell inside of Voronoi diagram + /// Point from source points. + static const Point& retrieve_point(const Points &points, const VD::cell_type &cell); + +private: + /// + /// PRIVATE: function to get parabola focus point + /// + /// curved edge + /// source lines + /// Parabola focus point + static Point get_parabola_point(const VD::edge_type ¶bola, const Lines &lines); + + /// + /// PRIVATE: function to get parabola diretrix + /// + /// curved edge + /// source lines + /// Parabola diretrix + static Line get_parabola_line(const VD::edge_type ¶bola, const Lines &lines); + +public: + /// + /// Construct parabola from curved edge + /// + /// curved edge + /// Voronoi diagram source lines + /// Parabola represented shape of edge + static Parabola get_parabola(const VD::edge_type &edge, const Lines &lines); + + /// + /// Calculate length of curved edge + /// + /// curved edge + /// Voronoi diagram source lines + /// edge length + static double calculate_length_of_parabola(const VD::edge_type &edge, const Lines &lines); + + /// + /// Calculate length of edge line segment or curve - parabola. + /// + /// Input edge to calcuate length + /// Source for Voronoi diagram. It contains parabola parameters + /// The length of edge + static double calculate_length(const VD::edge_type &edge, const Lines &lines); + + /// + /// Calculate maximal distance to outline and multiply by two(must be similar on both side) + /// ! not used + /// + /// Input edge. + /// Source for Voronoi diagram. It contains parabola parameters + /// Maximal island width along edge + static double calculate_max_width(const VD::edge_type &edge, const Lines &lines); + + /// + /// Calculate width limit(min, max) and round value to coord_t + /// + /// Input edge + /// Source for Voronoi diagram. It contains parabola parameters + /// Width range for edge. + /// First is minimal width on edge. + /// Second is maximal width on edge. + static std::pair calculate_width(const VD::edge_type &edge, const Lines &lines); + +private: + static std::pair calculate_width_for_line( + const VD::edge_type &line_edge, const Lines &lines); + static std::pair calculate_width_for_parabola( + const VD::edge_type ¶bola_edge, const Lines &lines); + static std::pair min_max_width(const VD::edge_type &edge, const Point &point); + +public: + /// + /// calculate distances to border of island and length on skeleton + /// + /// Input anotated voronoi diagram + /// (use function Slic3r::Voronoi::annotate_inside_outside) + /// Source lines for voronoi diagram + /// Extended voronoi graph by distances and length + static VoronoiGraph create_skeleton(const VD &vd, const Lines &lines); + + /// + /// find neighbor and return distance between nodes + /// + /// source of neighborse + /// neighbor node + /// When neighbor return distance between neighbor Nodes + /// otherwise no value + static const VoronoiGraph::Node::Neighbor *get_neighbor( + const VoronoiGraph::Node *from, const VoronoiGraph::Node *to); + + /// + /// use function get_neighbor + /// when not neighbor assert + /// + /// source Node + /// destination Node + /// distance between Nodes or Assert when not neighbor + static double get_neighbor_distance(const VoronoiGraph::Node *from, + const VoronoiGraph::Node *to); + + /// + /// Create longest node path over circle together with side branches + /// + /// Source circle, can't be connected with another + /// circle Circle side branches from + /// nodes of circle Path before circle - + /// defince input point to circle Longest nodes path and + /// its length + static VoronoiGraph::Path find_longest_path_on_circle( + const VoronoiGraph::Circle & circle, + const VoronoiGraph::ExPath::SideBranchesMap &side_branches); + + /// + /// Serach longest path from input_node throw Nodes in connected circles, + /// when circle is alone call find_longest_path_on_circle. + /// + /// Node on circle + /// index of circle with input + /// node Hold Circles, connection of circles + /// and Side branches Longest path from input + /// node + static VoronoiGraph::Path find_longest_path_on_circles( + const VoronoiGraph::Node & input_node, + size_t finished_circle_index, + const VoronoiGraph::ExPath &ex_path); + + /// + /// Function for detection circle in passed path. + /// + /// Already passed path in Graph + /// Actual neighbor possible created circle + /// Circle when exists + static std::optional create_circle( + const VoronoiGraph::Path & path, + const VoronoiGraph::Node::Neighbor &neighbor); + + /// + /// Move source connected circles into destination + /// + /// In/Out param + /// Input possible modified, do not use it after this + /// function Count of destination + /// circles before merge Source circle are append afted destination, therfore + /// all src indexes must be increased by destination circle count + static void merge_connected_circle( + VoronoiGraph::ExPath::ConnectedCircles &dst, + VoronoiGraph::ExPath::ConnectedCircles &src, + size_t dst_circle_count); + + /// + /// move data from source to destination + /// side_branches + circles + connected_circle + /// + /// destination extended path - append data from + /// source source extended path - data will be + /// moved to dst + static void append_neighbor_branch(VoronoiGraph::ExPath &dst, + VoronoiGraph::ExPath &src); + + /// + /// Heal starting from random point. + /// Compare length of all starting path with side branches + /// when side branch is longer than swap with start path + /// + /// IN/OUT path to be fixed after creating longest path + /// from one point + static void reshape_longest_path(VoronoiGraph::ExPath &path); + + /// + /// Extract the longest path from voronoi graph + /// by own function call stack(IStackFunction). + /// Restructuralize path by branch created from random point. + /// + /// Random point from outline. + static VoronoiGraph::ExPath create_longest_path( + const VoronoiGraph::Node *start_node); + + /// + /// Find twin neighbor + /// + /// neighbor + /// Twin neighbor + static const VoronoiGraph::Node::Neighbor *get_twin(const VoronoiGraph::Node::Neighbor& neighbor); + + /// + /// Find source node of neighbor + /// + /// neighbor + /// start node + static const VoronoiGraph::Node *get_twin_node(const VoronoiGraph::Node::Neighbor& neighbor); + + /// + /// Check if neighbor is in opposit direction to line direction + /// + /// edge has direction from vertex0 to vertex1 + /// line has direction from point a to point b + /// True when oposit direction otherwise FALSE + static bool is_opposit_direction(const VD::edge_type *edge, const Line &line); + + /// + /// Create point on edge defined by neighbor + /// in distance defined by edge length ratio + /// + /// Containe neighbor and position ratio on neighbor + /// Point laying on neighbor edge + static Point create_edge_point(const VoronoiGraph::Position& position); + static Point create_edge_point(const VD::edge_type *edge, double ratio); + + /// + /// Find position on VD edge with width + /// + /// Edge for searching position + /// Specify place on edge + /// Source lines for voronoi diagram + /// Position on given edge + static VoronoiGraph::Position get_position_with_width( + const VoronoiGraph::Node::Neighbor *neighbor, + coord_t width, + const Lines & lines); + + /// + /// Calculate both point on source lines correspond to edge postion + /// Faster way to get both point_on_line + /// + /// Position on edge + /// Source lines of VD + /// pair of point lay on outline lines correspond to position on edge + /// first -> source line of edge cell + /// second -> source line of edge twin cell + static std::pair point_on_lines( + const VoronoiGraph::Position &position, + const Lines & lines); + + /// + /// align "position" close to point "to" + /// + /// input position on VD + /// point to align + /// maximal distance on VD for search point + /// Position on VD + static VoronoiGraph::Position align(const VoronoiGraph::Position &position, + const Point & to, + double max_distance); + + /// + /// Calc position by ratio to closest point laying on edge + /// + /// edge to align + /// point to align + /// output: ratio between vertex0 and vertex1 closest to point, + /// when less than zero vertex0 is closest point on edge + /// when grater than one vertex1 is closest point on edge + /// distance edge to "to" point + /// only when result ratio is in range from 0 to 1 + static double get_distance(const VD::edge_type &edge, + const Point & point, + double & edge_ratio); + + static const VoronoiGraph::Node *getFirstContourNode( + const VoronoiGraph &graph); + + /// + /// Get max width from edge in voronoi graph + /// + /// Input point to voronoi graph + /// Maximal widht in graph + static coord_t get_max_width(const VoronoiGraph::ExPath &longest_path); + static coord_t get_max_width(const VoronoiGraph::Nodes &path); + static coord_t get_max_width(const VoronoiGraph::Node *node); + + /// + /// Check wheather VG ends in smaller distance than given one + /// + /// Position in direction to checked end + /// distance to explore + /// True when there is only smaller VD path to edge + static bool ends_in_distanace(const VoronoiGraph::Position &position, coord_t max_distance); + + /// + /// only line created VG + /// only last neighbor + /// Calculate angle of outline(source lines) at end of voronoi diagram + /// + /// Neighbor to calculate angle + /// Angle of source lines in radians + static double outline_angle(const VoronoiGraph::Node::Neighbor &neighbor, const Lines& lines); + + /// + /// Loop over neighbor in max distance from position + /// + /// Start of loop + /// Termination of loop + /// function to process neighbor with actual distance + static void for_neighbor_at_distance( + const VoronoiGraph::Position &position, + coord_t max_distance, + std::function fnc); + +public: // draw function for debug + static void draw(SVG & svg, + const VoronoiGraph &graph, + const Lines & lines, + const SampleConfig &config, + bool pointer_caption = false); + static void draw(SVG & svg, + const VD::edge_type &edge, + const Lines & lines, + const char * color, + coord_t width); + static void draw(SVG & svg, + const VoronoiGraph::Nodes &path, + coord_t width, + const char * color, + bool finish = false, + bool caption = false); + static void draw(SVG & svg, + const VoronoiGraph::ExPath &path, + coord_t width); + + // draw polygon when convert from cell + static void draw(const Slic3r::Polygon &polygon, + const Slic3r::Lines & lines, + const Slic3r::Point & center); +}; + +} // namespace Slic3r::sla +#endif // slic3r_SLA_SuppotstIslands_VoronoiGraphUtils_hpp_ diff --git a/src/libslic3r/SLA/SupportPoint.hpp b/src/libslic3r/SLA/SupportPoint.hpp index 66ed674028..d5d7f22ac5 100644 --- a/src/libslic3r/SLA/SupportPoint.hpp +++ b/src/libslic3r/SLA/SupportPoint.hpp @@ -7,11 +7,7 @@ #include -namespace Slic3r { - -class ModelObject; - -namespace sla { +namespace Slic3r::sla { // An enum to keep track of where the current points on the ModelObject came from. enum class PointsStatus { @@ -21,58 +17,47 @@ enum class PointsStatus { UserModified // User has done some edits. }; -struct SupportPoint -{ - Vec3f pos; - float head_front_radius; - bool is_new_island; - - SupportPoint() - : pos(Vec3f::Zero()), head_front_radius(0.f), is_new_island(false) - {} - - SupportPoint(float pos_x, - float pos_y, - float pos_z, - float head_radius, - bool new_island = false) - : pos(pos_x, pos_y, pos_z) - , head_front_radius(head_radius) - , is_new_island(new_island) - {} - - SupportPoint(Vec3f position, float head_radius, bool new_island = false) - : pos(position) - , head_front_radius(head_radius) - , is_new_island(new_island) - {} - - SupportPoint(Eigen::Matrix data) - : pos(data(0), data(1), data(2)) - , head_front_radius(data(3)) - , is_new_island(data(4) != 0.f) - {} - - bool operator==(const SupportPoint &sp) const - { - float rdiff = std::abs(head_front_radius - sp.head_front_radius); - return (pos == sp.pos) && rdiff < float(EPSILON) && - is_new_island == sp.is_new_island; - } - - bool operator!=(const SupportPoint &sp) const { return !(sp == (*this)); } - - template void serialize(Archive &ar) - { - ar(pos, head_front_radius, is_new_island); - } +// Reason of automatic support placement usage +enum class SupportPointType { + manual_add, + island, // no move, island should be grouped + slope, + //thin, + //stability, + //edge }; +/// +/// Stereolithography(SLA) support point +/// +struct SupportPoint +{ + // Position on model surface + Vec3f pos = Vec3f::Zero(); // [in mm] + + // radius of the touching interface + // Also define force it must keep + float head_front_radius = 0.f; // [in mm] + + // type + SupportPointType type{SupportPointType::manual_add}; + + bool is_island() const { return type == SupportPointType::island; } + template void serialize(Archive &ar){ + ar(pos, head_front_radius, type); + } + + // unsaved changes + cache invalidation + bool operator==(const SupportPoint &sp) const { + float rdiff = std::abs(head_front_radius - sp.head_front_radius); + return (pos == sp.pos) && rdiff < float(EPSILON) && type == sp.type; + } + + bool operator!=(const SupportPoint &sp) const { return !(sp == (*this)); } + +}; using SupportPoints = std::vector; -SupportPoints transformed_support_points(const ModelObject &mo, - const Transform3d &trafo); - -}} // namespace Slic3r::sla +} // namespace Slic3r::sla #endif // SUPPORTPOINT_HPP diff --git a/src/libslic3r/SLA/SupportPointGenerator.cpp b/src/libslic3r/SLA/SupportPointGenerator.cpp index 227cc61c16..47760dc00c 100644 --- a/src/libslic3r/SLA/SupportPointGenerator.cpp +++ b/src/libslic3r/SLA/SupportPointGenerator.cpp @@ -1,678 +1,1609 @@ -///|/ Copyright (c) Prusa Research 2019 - 2023 Tomáš Mészáros @tamasmeszaros, Vojtěch Bubník @bubnikv, Lukáš Matěna @lukasmatena +///|/ Copyright (c) Prusa Research 2024 Filip Sykala @Jony01 ///|/ ///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher ///|/ -#include -#include -#include -#include -#include -#include #include "SupportPointGenerator.hpp" -#include "libslic3r/Execution/ExecutionTBB.hpp" -#include "libslic3r/Geometry/ConvexHull.hpp" -#include "libslic3r/Model.hpp" -#include "libslic3r/ExPolygon.hpp" -#include "libslic3r/Point.hpp" -#include "libslic3r/ClipperUtils.hpp" -#include "libslic3r/Tesselate.hpp" -#include "libslic3r/MinAreaBoundingBox.hpp" -#include "libslic3r/libslic3r.h" -#include "libslic3r/AABBMesh.hpp" + +#include "libslic3r/Execution/ExecutionTBB.hpp" // parallel preparation of data for sampling #include "libslic3r/Execution/Execution.hpp" -#include "libslic3r/SLA/SupportPoint.hpp" -#include "libslic3r/BoundingBox.hpp" +#include "libslic3r/KDTreeIndirect.hpp" +#include "libslic3r/ClipperUtils.hpp" +#include "libslic3r/AABBTreeLines.hpp" // closest point to layer part +#include "libslic3r/AABBMesh.hpp" // move_on_mesh_surface Should be in another file +// SupportIslands +#include "libslic3r/SLA/SupportIslands/UniformSupportIsland.hpp" +#include "libslic3r/SLA/SupportIslands/SampleConfigFactory.hpp" -namespace Slic3r { -namespace sla { +using namespace Slic3r; +using namespace Slic3r::sla; -/*float SupportPointGenerator::approximate_geodesic_distance(const Vec3d& p1, const Vec3d& p2, Vec3d& n1, Vec3d& n2) +namespace { +#ifndef NDEBUG +bool exist_point_in_distance(const Vec3f &p, float distance, const LayerSupportPoints &pts) { + float distance_sq = sqr(distance); + return std::any_of(pts.begin(), pts.end(), [&p, distance_sq](const LayerSupportPoint &sp) { + return (sp.pos - p).squaredNorm() < distance_sq; }); +} +#endif // NDEBUG + +/// +/// Struct to store support points in KD tree to fast search for nearest ones. +/// +class NearPoints { - n1.normalize(); - n2.normalize(); + /// + /// Structure made for KDTree as function to + /// acess support point coordinate by index into global support point storage + /// + struct PointAccessor { + // multiple trees points into same data storage of all support points + LayerSupportPoints *m_supports_ptr; + explicit PointAccessor(LayerSupportPoints *supports_ptr) : m_supports_ptr(supports_ptr) {} + // accessor to coordinate for kd tree + const coord_t &operator()(size_t idx, size_t dimension) const { + return m_supports_ptr->at(idx).position_on_layer[dimension]; + } + }; - Vec3d v = (p2-p1); - v.normalize(); + PointAccessor m_points; + using Tree = KDTreeIndirect<2, coord_t, PointAccessor>; + Tree m_tree; +public: + /// + /// Constructor get pointer on the global storage of support points + /// + /// Pointer on Support vector + explicit NearPoints(LayerSupportPoints* supports_ptr) + : m_points(supports_ptr), m_tree(m_points) {} - float c1 = n1.dot(v); - float c2 = n2.dot(v); - float result = pow(p1(0)-p2(0), 2) + pow(p1(1)-p2(1), 2) + pow(p1(2)-p2(2), 2); - // Check for division by zero: - if(fabs(c1 - c2) > 0.0001) - result *= (asin(c1) - asin(c2)) / (c1 - c2); + NearPoints get_copy(){ + NearPoints copy(m_points.m_supports_ptr); + copy.m_tree = m_tree.get_copy(); // copy tree + return copy; + } + + /// + /// Remove support points from KD-Tree which lay out of expolygons + /// + /// Define area where could be support points + /// Current layer z coordinate + /// to prevent remove permanent points in prev layer influence + void remove_out_of(const ExPolygons &shapes, float current_z) { + std::vector indices = get_indices(); + auto it = std::remove_if(indices.begin(), indices.end(), + [&pts = *m_points.m_supports_ptr, &shapes, current_z](size_t point_index) { + const LayerSupportPoint& lsp = pts.at(point_index); + if (lsp.is_permanent && lsp.pos.z() >= current_z) + return false; + return !std::any_of(shapes.begin(), shapes.end(), + [&p = lsp.position_on_layer](const ExPolygon &shape) { + return shape.contains(p); + }); + }); + if (it == indices.end()) + return; // no point to remove + indices.erase(it, indices.end()); + m_tree.clear(); + m_tree.build(indices); // consume indices + } + + /// + /// Add point new support point into global storage of support points + /// and pointer into tree structure of nearest points + /// + /// New added point + void add(LayerSupportPoint &&point) { + // IMPROVE: only add to existing tree, do not reconstruct tree + std::vector indices = get_indices(); + LayerSupportPoints &pts = *m_points.m_supports_ptr; + assert(!exist_point_in_distance(point.pos, point.head_front_radius, pts)); + size_t index = pts.size(); + pts.emplace_back(std::move(point)); + indices.push_back(index); + m_tree.clear(); + m_tree.build(indices); // consume indices + } + + using CheckFnc = std::function; + /// + /// Iterate over support points in 2d radius and search any of fnc with True. + /// Made for check wheather surrounding supports support current point p. + /// + /// Center of search circle + /// Search circle radius + /// Function to check supports point + /// True wheny any of check function return true, otherwise False + bool exist_true_in_radius(const Point &pos, coord_t radius, const CheckFnc &fnc) const { + std::vector point_indices = find_nearby_points(m_tree, pos, radius); + return std::any_of(point_indices.begin(), point_indices.end(), + [&points = *m_points.m_supports_ptr, &pos, &fnc](size_t point_index){ + return fnc(points.at(point_index), pos); + }); + } + + /// + /// Merge another tree structure into current one. + /// Made for connection of two mesh parts. + /// + /// Another near points + void merge(NearPoints &&near_point) { + // need to have same global storage of support points + assert(m_points.m_supports_ptr == near_point.m_points.m_supports_ptr); + + // IMPROVE: merge trees instead of rebuild + std::vector indices = get_indices(); + std::vector indices2 = near_point.get_indices(); + // merge + indices.insert(indices.end(), + std::move_iterator(indices2.begin()), + std::move_iterator(indices2.end())); + // remove duplicit indices - Diamond case + std::sort(indices.begin(), indices.end()); + auto it = std::unique(indices.begin(), indices.end()); + indices.erase(it, indices.end()); + // rebuild tree + m_tree.clear(); + m_tree.build(indices); // consume indices + } + + /// + /// Getter on used indices + /// + /// Current used Indices into m_points + std::vector get_indices() const { + std::vector indices = m_tree.get_nodes(); // copy + // nodes in tree contain "max values for size_t" on unused leaf of tree, + // when count of indices is not exactly power of 2 + auto it = std::remove_if(indices.begin(), indices.end(), + [max_index = m_points.m_supports_ptr->size()](size_t i) { return i >= max_index; }); + indices.erase(it, indices.end()); + return indices; + } +}; +using NearPointss = std::vector; + +/// +/// Intersection of line segment and circle +/// +/// Line segment point A, Point lay inside circle +/// Line segment point B, Point lay outside or on circle +/// Circle center point +/// squared value of Circle Radius (r2 = r*r) +/// Intersection point +Point intersection_line_circle(const Point &p1, const Point &p2, const Point &cnt, double r2) { + // Vector from p1 to p2 + Vec2d dp_d((p2 - p1).cast()); + // Vector from circle center to p1 + Vec2d f_d((p1 - cnt).cast()); + + double a = dp_d.squaredNorm(); + double b = 2 * (f_d.x() * dp_d.x() + f_d.y() * dp_d.y()); + double c = f_d.squaredNorm() - r2; + + // Discriminant of the quadratic equation + double discriminant = b * b - 4 * a * c; + + // No intersection if discriminant is negative + assert(discriminant >= 0); + if (discriminant < 0) + return {}; // No intersection + + // Calculate the two possible values of t (parametric parameter) + discriminant = sqrt(discriminant); + double t1 = (-b - discriminant) / (2 * a); + + // Check for valid intersection points within the line segment + if (t1 >= 0 && t1 <= 1) { + return {p1.x() + t1 * dp_d.x(), p1.y() + t1 * dp_d.y()}; + } + + // should not be in use + double t2 = (-b + discriminant) / (2 * a); + if (t2 >= 0 && t2 <= 1 && t1 != t2) { + return {p1.x() + t2 * dp_d.x(), p1.y() + t2 * dp_d.y()}; + } + return {}; +} + +/// +/// Move grid from previous layer to current one +/// for given part +/// +/// Grids generated in previous layer +/// Current layer part to process +/// Grids which will be moved to current grid +/// Grid for given part +NearPoints create_near_points( + const LayerParts &prev_layer_parts, + const LayerPart &part, + NearPointss &prev_grids +) { + const LayerParts::const_iterator &prev_part_it = part.prev_parts.front(); + size_t index_of_prev_part = prev_part_it - prev_layer_parts.begin(); + NearPoints near_points = (prev_part_it->next_parts.size() == 1)? + std::move(prev_grids[index_of_prev_part]) : + // Need a copy there are multiple parts above previus one + prev_grids[index_of_prev_part].get_copy(); // copy + + // merge other grid in case of multiple previous parts + for (size_t i = 1; i < part.prev_parts.size(); ++i) { + const LayerParts::const_iterator &prev_part_it = part.prev_parts[i]; + size_t index_of_prev_part = prev_part_it - prev_layer_parts.begin(); + if (prev_part_it->next_parts.size() == 1) { + near_points.merge(std::move(prev_grids[index_of_prev_part])); + } else { // Need a copy there are multiple parts above previus one + NearPoints grid_ = prev_grids[index_of_prev_part].get_copy(); // copy + near_points.merge(std::move(grid_)); + } + } + return near_points; +} + +/// +/// Add support point to near_points when it is neccessary +/// +/// Current part - keep samples +/// Configuration to sample +/// Keep previous sampled suppport points +/// current z coordinate of part +/// Max distance to seach support for sample +void support_part_overhangs( + const LayerPart &part, + const SupportPointGeneratorConfig &config, + NearPoints &near_points, + float part_z, + coord_t maximal_radius +) { + NearPoints::CheckFnc is_supported = [] + (const LayerSupportPoint &support_point, const Point &p) -> bool { + // Debug visualization of all sampled outline + //return false; + coord_t r = support_point.current_radius; + Point dp = support_point.position_on_layer - p; + if (std::abs(dp.x()) > r) return false; + if (std::abs(dp.y()) > r) return false; + double r2 = sqr(static_cast(r)); + return dp.cast().squaredNorm() < r2; + }; + + for (const Point &p : part.samples) { + if (!near_points.exist_true_in_radius(p, maximal_radius, is_supported)) { + // not supported sample, soo create new support point + near_points.add(LayerSupportPoint{ + SupportPoint{ + Vec3f{unscale(p.x()), unscale(p.y()), part_z}, + /* head_front_radius */ config.head_diameter / 2, + SupportPointType::slope + }, + /* position_on_layer */ p, + /* radius_curve_index */ 0, + /* current_radius */ static_cast(scale_(config.support_curve.front().x())) + }); + } + } +} + +/// +/// Sample part as Island +/// Result store to grid +/// +/// Island to support +/// OUT place to store new supports +/// z coordinate of part +/// z coordinate of part +/// +void support_island(const LayerPart &part, NearPoints& near_points, float part_z, + const Points &permanent, const SupportPointGeneratorConfig &cfg) { + SupportIslandPoints samples = uniform_support_island(*part.shape, permanent, cfg.island_configuration); + for (const SupportIslandPointPtr &sample : samples) + near_points.add(LayerSupportPoint{ + SupportPoint{ + Vec3f{ + unscale(sample->point.x()), + unscale(sample->point.y()), + part_z + }, + /* head_front_radius */ cfg.head_diameter / 2, + SupportPointType::island + }, + /* position_on_layer */ sample->point, + /* radius_curve_index */ 0, + /* current_radius */ static_cast(scale_(cfg.support_curve.front().x())) + }); +} + +void support_peninsulas(const Peninsulas& peninsulas, NearPoints& near_points, float part_z, + const Points &permanent, const SupportPointGeneratorConfig &cfg) { + for (const Peninsula& peninsula: peninsulas) { + SupportIslandPoints peninsula_supports = + uniform_support_peninsula(peninsula, permanent, cfg.island_configuration); + for (const SupportIslandPointPtr &support : peninsula_supports) + near_points.add(LayerSupportPoint{ + SupportPoint{ + Vec3f{ + unscale(support->point.x()), + unscale(support->point.y()), + part_z + }, + /* head_front_radius */ cfg.head_diameter / 2, + SupportPointType::island + }, + /* position_on_layer */ support->point, + /* radius_curve_index */ 0, + /* current_radius */ static_cast(scale_(cfg.support_curve.front().x())) + }); + } +} + +/// +/// Copy parts shapes from link to output +/// +/// Links between part of mesh +/// Collected expolygons from links +ExPolygons get_shapes(const PartLinks& part_links) { + ExPolygons out; + out.reserve(part_links.size()); + for (const PartLink &part_link : part_links) + out.push_back(*part_link->shape); // copy + return out; +} + +/// +/// Uniformly sample Polyline, +/// Use first point and each next point is first crosing radius from last added +/// +/// Begin of polyline points to sample +/// End of polyline points to sample +/// Squared distance for sampling +/// Uniformly distributed points laying on input polygon +/// with exception of first and last point(they are closer than dist2) +Slic3r::Points sample(Points::const_iterator b, Points::const_iterator e, double dist2) { + assert(e-b >= 2); + if (e - b < 2) + return {}; // at least one line(2 points) + + // IMPROVE1: start of sampling e.g. center of Polyline + // IMPROVE2: Random offset(To remove align of point between slices) + // IMPROVE3: Sample small overhangs with memory for last sample(OR in center) + Slic3r::Points r; + r.push_back(*b); + + //Points::const_iterator prev_pt = e; + const Point *prev_pt = nullptr; + for (Points::const_iterator it = b; it+1 < e; ++it){ + const Point &pt = *(it+1); + double p_dist2 = (r.back() - pt).cast().squaredNorm(); + while (p_dist2 > dist2) { // line segment goes out of radius + if (prev_pt == nullptr) + prev_pt = &(*it); + r.push_back(intersection_line_circle(*prev_pt, pt, r.back(), dist2)); + p_dist2 = (r.back() - pt).cast().squaredNorm(); + prev_pt = &r.back(); + } + prev_pt = nullptr; + } + return r; +} + +bool contain_point(const Point &p, const Points &sorted_points) { + auto it = std::lower_bound(sorted_points.begin(), sorted_points.end(), p); + if (it == sorted_points.end()) + return false; + ++it; // next point should be same as searched + if (it == sorted_points.end()) + return false; + return it->x() == p.x() && it->y() == p.y(); +}; + +#ifndef NDEBUG +bool exist_same_points(const ExPolygon &shape, const Points& prev_points) { + auto shape_points = to_points(shape); + return shape_points.end() != + std::find_if(shape_points.begin(), shape_points.end(), [&prev_points](const Point &p) { + return contain_point(p, prev_points); + }); +} +#endif // NDEBUG + +Points sample_overhangs(const LayerPart& part, double dist2) { + const ExPolygon &shape = *part.shape; + + // Collect previous expolygons by links collected in loop before + ExPolygons prev_shapes = get_shapes(part.prev_parts); + assert(!prev_shapes.empty()); + ExPolygons overhangs = diff_ex(shape, prev_shapes, ApplySafetyOffset::Yes); + if (overhangs.empty()) // above part is smaller in whole contour + return {}; + + Points prev_points = to_points(prev_shapes); + std::sort(prev_points.begin(), prev_points.end()); + + // TODO: solve case when shape and prev points has same point + assert(!exist_same_points(shape, prev_points)); + + auto sample_overhang = [&prev_points, dist2](const Polygon &polygon, Points &samples) { + const Points &pts = polygon.points; + // first point which is not part of shape + Points::const_iterator first_bad = pts.end(); + Points::const_iterator start_it = pts.end(); + for (auto it = pts.begin(); it != pts.end(); ++it) { + const Point &p = *it; + if (contain_point(p, prev_points)) { + if (first_bad == pts.end()) { + // remember begining + first_bad = it; + } + if (start_it != pts.end()) { + // finish sampling + append(samples, sample(start_it, it, dist2)); + // prepare for new start + start_it = pts.end(); + } + } else if (start_it == pts.end()) { + start_it = it; + } + } + + // sample last segment + if (start_it == pts.end()) { // tail is without points + if (first_bad != pts.begin()) // only begining + append(samples, sample(pts.begin(), first_bad, dist2)); + } else { // tail contain points + if (first_bad == pts.begin()) { // only tail + append(samples, sample(start_it, pts.end(), dist2)); + } else if (start_it == pts.begin()) { // whole polygon is overhang + assert(first_bad == pts.end()); + Points pts2 = pts; // copy + pts2.push_back(pts.front()); + append(samples, sample(pts2.begin(), pts2.end(), dist2)); + } else { // need connect begining and tail + Points pts2; + pts2.reserve((pts.end() - start_it) + + (first_bad - pts.begin())); + for (auto it = start_it; it < pts.end(); ++it) + pts2.push_back(*it); + for (auto it = pts.begin(); it < first_bad; ++it) + pts2.push_back(*it); + append(samples, sample(pts2.begin(), pts2.end(), dist2)); + } + } + }; + + Points samples; + for (const ExPolygon &overhang : overhangs) { + sample_overhang(overhang.contour, samples); + for (const Polygon &hole : overhang.holes) { + sample_overhang(hole, samples); + } + } + return samples; +} + +coord_t calc_influence_radius(float z_distance, const SupportPointGeneratorConfig &config) { + float island_support_distance_sq = sqr(config.support_curve.front().x()); + if (!is_approx(config.density_relative, 1.f, 1e-4f)) // exist relative density + island_support_distance_sq /= config.density_relative; + float z_distance_sq = sqr(z_distance); + if (z_distance_sq >= island_support_distance_sq) + return 0.f; + // IMPROVE: use curve interpolation instead of sqrt(stored in config). + // shape of supported area before permanent supports is sphere with radius of island_support_distance + return static_cast(scale_(std::sqrt(island_support_distance_sq - z_distance_sq))); +} + +void prepare_supports_for_layer(LayerSupportPoints &supports, float layer_z, + const NearPointss& activ_points, const SupportPointGeneratorConfig &config) { + auto set_radius = [&config](LayerSupportPoint &support, float radius) { + if (!is_approx(config.density_relative, 1.f, 1e-4f)) // exist relative density + radius = std::sqrt(sqr(radius) / config.density_relative); + support.current_radius = static_cast(scale_(radius)); + }; + + std::vector is_active(supports.size(), {false}); + for (const NearPoints& pts : activ_points) { + std::vector indices = pts.get_indices(); + for (size_t i : indices) + is_active[i] = true; + } + + const std::vector& curve = config.support_curve; + // calculate support area for each support point as radius + // IMPROVE: use some offsets of previous supported island + for (LayerSupportPoint &support : supports) { + size_t &index = support.radius_curve_index; + if (index + 1 >= curve.size()) + continue; // already contain maximal radius + + if (!is_active[&support - &supports.front()]) + continue; // Point is not used in any part of the current layer + + // find current segment + float diff_z = layer_z - support.pos.z(); + if (diff_z < 0.) { + // permanent support influence distribution of support points printed before. + support.current_radius = calc_influence_radius(-diff_z, config); + continue; + } + while ((index + 1) < curve.size() && diff_z > curve[index + 1].y()) + ++index; + + if ((index+1) >= curve.size()) { + // set maximal radius + set_radius(support, curve.back().x()); + continue; + } + // interpolate radius on input curve + Vec2f a = curve[index]; + Vec2f b = curve[index+1]; + assert(a.y() <= diff_z && diff_z <= b.y()); + float t = (diff_z - a.y()) / (b.y() - a.y()); + assert(0 <= t && t <= 1); + set_radius(support, a.x() + t * (b.x() - a.x())); + } +} + +/// +/// Near points do not have to contain support points out of part. +/// Due to be able support in same area again(overhang above another overhang) +/// Wanted Side effect, it supports thiny part of overhangs +/// +/// +/// current +/// Current layer z coordinate +/// to prevent remove permanent points in prev layer influence +void remove_supports_out_of_part(NearPoints &near_points, const LayerPart &part, float current_z) { + // Offsetting is made in data preparation - speed up caused by paralelization + //ExPolygons extend_shape = offset_ex(*part.shape, config.removing_delta, ClipperLib::jtSquare); + near_points.remove_out_of(part.extend_shape, current_z); +} + +/// +/// Detect existence of peninsula on current layer part +/// +/// IN/OUT island part containing peninsulas +/// minimal width of overhang to become peninsula +/// supported distance from mainland +void create_peninsulas(LayerPart &part, const PrepareSupportConfig &config) { + assert(config.peninsula_min_width > config.peninsula_self_supported_width); + const ExPolygons below_shapes = get_shapes(part.prev_parts); + const ExPolygons below_expanded = offset_ex(below_shapes, config.peninsula_min_width, ClipperLib::jtSquare); + const ExPolygon &part_shape = *part.shape; + ExPolygons over_peninsula = diff_ex(part_shape, below_expanded); + if (over_peninsula.empty()) + return; // only tiny overhangs + + ExPolygons below_self_supported = offset_ex(below_shapes, config.peninsula_self_supported_width, ClipperLib::jtSquare); + // exist weird edge case, where expand function return empty result + assert(below_self_supported.empty()); + + // exist layer part over peninsula limit + ExPolygons peninsulas_shape = diff_ex(part_shape, below_self_supported); + + // IMPROVE: Anotate source of diff by ClipperLib_Z + Lines below_lines = to_lines(below_self_supported); + auto get_angle = [](const Line &l) { + Point diff = l.b - l.a; + if (diff.x() < 0) // Only positive direction X + diff = -diff; + return atan2(diff.y(), diff.x()); + }; + std::vector belowe_line_angle; // define direction of line with positive X + belowe_line_angle.reserve(below_lines.size()); + for (const Line& l : below_lines) + belowe_line_angle.push_back(get_angle(l)); + std::vector idx(below_lines.size()); + std::iota(idx.begin(), idx.end(), 0); + auto is_lower = [&belowe_line_angle](size_t i1, size_t i2) { + return belowe_line_angle[i1] < belowe_line_angle[i2]; }; + std::sort(idx.begin(), idx.end(), is_lower); + + // Check, wheather line exist in set of belowe lines + // True .. line exist in previous layer (or partialy overlap previous line), connection to land + // False .. line is made by border of current layer part(peninsula coast) + auto exist_belowe = [&get_angle, &idx, &below_lines, &belowe_line_angle] + (const Line &l) { + // It is edge case of expand + if (below_lines.empty()) + return false; + // allowed angle epsilon + const double angle_epsilon = 1e-3; // < 0.06 DEG + const double paralel_epsilon = scale_(1e-2); // 10 um + double angle = get_angle(l); + double low_angle = angle - angle_epsilon; + bool is_over = false; + if (low_angle <= -M_PI_2) { + low_angle += M_PI; + is_over = true; + } + double hi_angle = angle + angle_epsilon; + if (hi_angle >= M_PI_2) { + hi_angle -= M_PI; + is_over = true; + } + int mayorit_idx = 0; + if (Point d = l.a - l.b; + abs(d.x()) < abs(d.y())) + mayorit_idx = 1; + + coord_t low = l.a[mayorit_idx]; + coord_t high = l.b[mayorit_idx]; + if (low > high) + std::swap(low, high); + + auto is_lower_angle = [&belowe_line_angle](size_t index, double angle) { + return belowe_line_angle[index] < angle; }; + auto it_idx = std::lower_bound(idx.begin(), idx.end(), low_angle, is_lower_angle); + if (it_idx == idx.end()) { + if (is_over) { + it_idx = idx.begin(); + is_over = false; + } else { + return false; + } + } + while (is_over || belowe_line_angle[*it_idx] < hi_angle) { + const Line &l2 = below_lines[*it_idx]; + coord_t l2_low = l2.a[mayorit_idx]; + coord_t l2_high = l2.b[mayorit_idx]; + if (low > high) + std::swap(low, high); + if ((l2_high >= low && l2_low <= high) && ( + ((l2.a == l.a && l2.b == l.b) ||(l2.a == l.b && l2.b == l.a)) || // speed up - same line + l.perp_distance_to(l2.a) < paralel_epsilon)) // check distance of parallel lines + return true; + ++it_idx; + if (it_idx == idx.end()){ + if (is_over) { + it_idx = idx.begin(); + is_over = false; + } else { + break; + } + } + } + return false; + }; + + // anotate source of peninsula: overhang VS previous layer + for (const ExPolygon &peninsula : peninsulas_shape) { + // Check that peninsula is wide enough(min_peninsula_width) + if (intersection_ex(ExPolygons{peninsula}, over_peninsula).empty()) + continue; + + // need to know shape and edges of peninsula + Lines lines = to_lines(peninsula); + std::vector is_outline(lines.size()); + // when line overlap with belowe lines it is not outline + for (size_t i = 0; i < lines.size(); i++) + is_outline[i] = !exist_belowe(lines[i]); + part.peninsulas.push_back(Peninsula{peninsula, is_outline}); + } +} + +struct LayerPartIndex { + size_t layer_index; // index into layers + size_t part_index; // index into layer parts + bool operator<(const LayerPartIndex &other) const { + return layer_index < other.layer_index || + (layer_index == other.layer_index && part_index < other.part_index); + } + bool operator==(const LayerPartIndex &other) const { + return layer_index == other.layer_index && part_index == other.part_index; + } +}; +using SmallPart = std::vector; +using SmallParts = std::vector; + +std::optional create_small_part( + const Layers &layers, const LayerPartIndex& island, float radius_in_mm) { + // Search for BB with diameter(2*r) distance from part and collect parts + const LayerPart &part = layers[island.layer_index].parts[island.part_index]; + coord_t radius = static_cast(scale_(radius_in_mm)); + + // only island part could be source of small part + assert(part.prev_parts.empty()); + // Island bounding box Should be already checked out of function + assert(part.shape_extent.size().x() <= 2*radius && + part.shape_extent.size().y() <= 2*radius ); + + Point range{radius, radius}; + Point center = part.shape_extent.center(); + BoundingBox range_bb{center - range,center + range}; + // BoundingBox range_bb{ // check exist sphere with radius to overlap model part + // part.shape_extent.min - range, + // part.shape_extent.max + range}; + + /// + /// Check parts in layers if they are in range of bounding box + /// Recursive fast check function without storing any already searched data + /// NOTE: Small part with holes could slow down checking + /// + /// Index of layer with part + /// Index of part in layer.parts + /// Recursion protection + std::function check_parts; + check_parts = [&range_bb, &check_parts, &layers, &island, radius_in_mm] + (const LayerPartIndex& check, size_t allowed_depth) -> bool { + const Layer &check_layer = layers[check.layer_index]; + const LayerPart &check_part = check_layer.parts[check.part_index]; + for (const PartLink &link : check_part.next_parts) + // part bound is far than diameter from source part + if (!range_bb.contains(link->shape_extent.min) || + !range_bb.contains(link->shape_extent.max)) + return false; // part is too large + + --allowed_depth; + if (allowed_depth == 0) + return true; // break recursion + + // recursively check next layers + size_t next_layer_i = check.layer_index + 1; + for (const PartLink& link : check_part.next_parts) { + size_t next_part_i = link - layers[next_layer_i].parts.cbegin(); + if (!check_parts({next_layer_i, next_part_i}, allowed_depth)) + return false; + } + + if (check.layer_index == island.layer_index) { + if (!check_part.prev_parts.empty()) + return false; // prev layers are already checked + + // When model part start from multi islands on the same layer, + // Investigate only the first island(lower index into parts). + if (check.part_index < island.part_index) + return false; // part is already checked + } + + + if (float max_z = radius_in_mm + layers[island.layer_index].print_z; + check_layer.print_z > max_z) + return false; // too far in Z + + // NOTE: multiple investigation same part seems more relevant + // instead of store and search for already checked parts + + // check previous parts + for (const PartLink &link : check_part.prev_parts) { + // part bound is far than diameter from source part + if (!range_bb.contains(link->shape_extent.min) || + !range_bb.contains(link->shape_extent.max)) + return false; // part is too large + } + + for (const PartLink &link : check_part.prev_parts) { + size_t prev_layer_i = check.layer_index - 1; // exist only when exist prev parts - cant be negative + size_t prev_part_i = link - layers[prev_layer_i].parts.cbegin(); + // Improve: check only parts which are not already checked + if (!check_parts({prev_layer_i, prev_part_i}, allowed_depth)) + return false; + } + return true; + }; + + float layer_height = layers[1].print_z - layers[0].print_z; + assert(layer_height > 0.f); + float safe_multiplicator = 1.3f; // 30% more layers than radius for zigzag(up & down layer) search + size_t allowed_depth = static_cast(std::ceil(radius_in_mm / layer_height * safe_multiplicator)); + // Check Bounding boxes and do not create any data - FAST CHECK + // NOTE: it could check model parts multiple times those there is allowed_depth + if (!check_parts(island, allowed_depth)) + return {}; + + SmallPart collected; // sorted by layer_i, part_i + std::vector queue_next; + LayerPartIndex curr = island; + // create small part by flood fill neighbor parts + do { + if (curr.layer_index >= layers.size()){ + if (queue_next.empty()) + break; // finish collecting + curr = queue_next.back(); + queue_next.pop_back(); + } + auto collected_it = std::lower_bound(collected.begin(), collected.end(), curr); + if (collected_it != collected.end() && + *collected_it == curr ) // already processed + continue; + collected.insert(collected_it, curr); // insert sorted + + const LayerPart &curr_part = layers[curr.layer_index].parts[curr.part_index]; + LayerPartIndex next{layers.size(), 0}; // set to uninitialized value(layer index is out of layers) + for (const PartLink &link : curr_part.next_parts) { + size_t next_layer_i = curr.layer_index + 1; + size_t part_i = link - layers[next_layer_i].parts.begin(); + LayerPartIndex next_{next_layer_i, part_i}; + auto it = std::lower_bound(collected.begin(), collected.end(), next_); + if (it != collected.end() && *it == next_) + continue; // already collected + + if (next.layer_index >= layers.size()) // next is uninitialized + next = next_; + else + queue_next.push_back(next_); // insert sorted + } + for (const PartLink &link : curr_part.prev_parts) { + size_t prev_layer_i = curr.layer_index - 1; + size_t part_i = link - layers[prev_layer_i].parts.begin(); + LayerPartIndex next_{prev_layer_i, part_i}; + auto it = std::lower_bound(collected.begin(), collected.end(), next_); + if (it != collected.end() && *it == next_) + continue; // already collected + + if (next.layer_index >= layers.size()) // next is uninitialized + next = next_; + else + queue_next.push_back(next_); // insert sorted + } + curr = next; + } while (true); // NOTE: It is break when queue is empty && curr is invalid + + // Check that whole model part is inside support head sphere + float print_z = layers[island.layer_index].print_z; + std::vector vertices; + for (const LayerPartIndex &part_id : collected) { + const Layer &layer = layers[part_id.layer_index]; + double radius_sq = (sqr(radius_in_mm) - sqr(layer.print_z - print_z)) / sqr(SCALING_FACTOR); + const LayerPart &layer_part = layer.parts[part_id.part_index]; + for (const Point &p : layer_part.shape->contour.points) { + Vec2d diff2d = (p - center).cast(); + if (sqr(diff2d.x()) + sqr(diff2d.y()) > radius_sq) + return {}; // part is too large + } + } + return collected; +} + +/// +/// Detection of small parts of support +/// +SmallParts get_small_parts(const Layers &layers, float radius_in_mm) { + // collect islands + coord_t diameter = static_cast(2 * scale_(radius_in_mm)); + std::vector islands; + for (size_t layer_i = 0; layer_i < layers.size(); ++layer_i) { + const Layer &layer = layers[layer_i]; + for (size_t part_i = 0; part_i < layer.parts.size(); ++part_i) { + const LayerPart &part = layer.parts[part_i]; + if (!part.prev_parts.empty()) + continue; // not island + if (const Point size = part.shape_extent.size(); + size.x() > diameter || size.y() > diameter) + continue; // big island + islands.push_back({layer_i, part_i}); + } + } + + // multithreaded investigate islands + std::mutex m; // write access into result + SmallParts result; + execution::for_each(ex_tbb, size_t(0), islands.size(), + [&layers, radius_in_mm, &islands, &result, &m](size_t island_i) { + std::optional small_part_opt = create_small_part(layers, islands[island_i], radius_in_mm); + if (!small_part_opt.has_value()) + return; // no small part + std::lock_guard lock(m); + result.push_back(*small_part_opt); + }, 8 /* gransize */); return result; } +void erase(const SmallParts &small_parts, Layers &layers) { + // be carefull deleting small parts could invalidate const reference into vector with parts + // whole layer must be threated at once + std::vector to_erase; + for (const SmallPart &small_part : small_parts) + to_erase.insert(to_erase.end(), small_part.begin(), small_part.end()); -float SupportPointGenerator::get_required_density(float angle) const -{ - // calculation would be density_0 * cos(angle). To provide one more degree of freedom, we will scale the angle - // to get the user-set density for 45 deg. So it ends up as density_0 * cos(K * angle). - float K = 4.f * float(acos(m_config.density_at_45/m_config.density_at_horizontal) / M_PI); - return std::max(0.f, float(m_config.density_at_horizontal * cos(K*angle))); + auto cmp = [](const LayerPartIndex &a, const LayerPartIndex &b) { + return a.layer_index < b.layer_index || + (a.layer_index == b.layer_index && a.part_index > b.part_index);}; + std::sort(to_erase.begin(), to_erase.end(), cmp); // sort by layer index and part index + assert(std::unique(to_erase.begin(), to_erase.end()) == to_erase.end()); + size_t erase_to; // without this index + for (size_t erase_from = 0; erase_from < to_erase.size(); erase_from = erase_to) { + erase_to = erase_from + 1; + size_t layer_index = to_erase[erase_from].layer_index; + while (erase_to < to_erase.size() && + to_erase[erase_to].layer_index == layer_index) + ++erase_to; + + Layer &layer = layers[layer_index]; + LayerParts& parts_ = layer.parts; + LayerParts layer_parts = parts_; // copy current + + // https://stackoverflow.com/questions/11021764/does-moving-a-vector-invalidate-iterators + // use swap where const iterator should be guaranteed, instead of move on the end of loop + std::swap(layer_parts, parts_); // swap copy into layer parts + + // NOTE: part indices are sorted descent + for (size_t i = erase_from; i < erase_to; ++i) + parts_.erase(parts_.begin() + to_erase[i].part_index); // remove parts + + if (layer_index > 0) { // not first layer + Layer& prev_layer = layers[layer_index - 1]; + for (LayerPart& prev_part: prev_layer.parts){ + for (PartLink &next_part : prev_part.next_parts) { + size_t part_i = next_part - layer_parts.cbegin(); + for (size_t i = erase_from; i < erase_to; ++i) + if (part_i >= to_erase[i].part_index) + --part_i; // index after removed part + assert(part_i < parts_.size()); + next_part = parts_.begin() + part_i; + } + } + } + if (layer_index < layers.size() - 1) { // not last layer + Layer& next_layer = layers[layer_index + 1]; + for (LayerPart &next_part : next_layer.parts) { + for (PartLink &prev_part : next_part.prev_parts) { + size_t part_i = prev_part - layer_parts.cbegin(); + for (size_t i = erase_from; i < erase_to; ++i) + if (part_i >= to_erase[i].part_index) + --part_i; // index after removed part + assert(part_i < parts_.size()); + prev_part = parts_.begin() + part_i; + } + } + } + } } -float SupportPointGenerator::distance_limit(float angle) const -{ - return 1./(2.4*get_required_density(angle)); -}*/ +} // namespace -SupportPointGenerator::SupportPointGenerator( - const AABBMesh &emesh, - const std::vector &slices, - const std::vector & heights, - const Config & config, - std::function throw_on_cancel, - std::function statusfn) - : SupportPointGenerator(emesh, config, throw_on_cancel, statusfn) -{ - std::random_device rd; - m_rng.seed(rd()); - execute(slices, heights); -} - -SupportPointGenerator::SupportPointGenerator( - const AABBMesh &emesh, - const SupportPointGenerator::Config &config, - std::function throw_on_cancel, - std::function statusfn) - : m_config(config) - , m_emesh(emesh) - , m_throw_on_cancel(throw_on_cancel) - , m_statusfn(statusfn) -{ -} - -void SupportPointGenerator::execute(const std::vector &slices, - const std::vector & heights) -{ - process(slices, heights); - project_onto_mesh(m_output); -} - -void SupportPointGenerator::project_onto_mesh(std::vector& points) const -{ - // The function makes sure that all the points are really exactly placed on the mesh. - - // Use a reasonable granularity to account for the worker thread synchronization cost. - static constexpr size_t gransize = 64; - - execution::for_each(ex_tbb, size_t(0), points.size(), [this, &points](size_t idx) - { - if ((idx % 16) == 0) - // Don't call the following function too often as it flushes CPU write caches due to synchronization primitves. - m_throw_on_cancel(); - - Vec3f& p = points[idx].pos; - // Project the point upward and downward and choose the closer intersection with the mesh. - AABBMesh::hit_result hit_up = m_emesh.query_ray_hit(p.cast(), Vec3d(0., 0., 1.)); - AABBMesh::hit_result hit_down = m_emesh.query_ray_hit(p.cast(), Vec3d(0., 0., -1.)); - - bool up = hit_up.is_hit(); - bool down = hit_down.is_hit(); - - if (!up && !down) - return; - - AABBMesh::hit_result& hit = (!down || (hit_up.distance() < hit_down.distance())) ? hit_up : hit_down; - p = p + (hit.distance() * hit.direction()).cast(); - }, gransize); -} - -static std::vector make_layers( - const std::vector& slices, const std::vector& heights, - std::function throw_on_cancel) -{ +SupportPointGeneratorData Slic3r::sla::prepare_generator_data( + std::vector &&slices, + const std::vector &heights, + const PrepareSupportConfig &config, + ThrowOnCancel throw_on_cancel, + StatusFunction statusfn +) { + // check input + assert(!slices.empty()); assert(slices.size() == heights.size()); + if (slices.empty() || slices.size() != heights.size()) + return SupportPointGeneratorData{}; + + // Move input into result + SupportPointGeneratorData result; + result.slices = std::move(slices); // Allocate empty layers. - std::vector layers; - layers.reserve(slices.size()); - for (size_t i = 0; i < slices.size(); ++ i) - layers.emplace_back(i, heights[i]); + result.layers = Layers(result.slices.size()); - // FIXME: calculate actual pixel area from printer config: - //const float pixel_area = pow(wxGetApp().preset_bundle->project_config.option("display_width") / wxGetApp().preset_bundle->project_config.option("display_pixels_x"), 2.f); // - const float pixel_area = pow(0.047f, 2.f); - - execution::for_each(ex_tbb, size_t(0), layers.size(), - [&layers, &slices, &heights, pixel_area, throw_on_cancel](size_t layer_id) - { - if ((layer_id % 8) == 0) + // Generate Extents and SampleLayers + execution::for_each(ex_tbb, size_t(0), result.slices.size(), + [&result, &heights, throw_on_cancel](size_t layer_id) { + if ((layer_id % 128) == 0) // Don't call the following function too often as it flushes // CPU write caches due to synchronization primitves. throw_on_cancel(); - SupportPointGenerator::MyLayer &layer = layers[layer_id]; - const ExPolygons & islands = slices[layer_id]; - // FIXME WTF? - const float height = (layer_id > 2 ? - heights[layer_id - 3] : - heights[0] - (heights[1] - heights[0])); - layer.islands.reserve(islands.size()); + Layer &layer = result.layers[layer_id]; + layer.print_z = heights[layer_id]; // copy + const ExPolygons &islands = result.slices[layer_id]; + layer.parts.reserve(islands.size()); for (const ExPolygon &island : islands) { - float area = float(island.area() * SCALING_FACTOR * SCALING_FACTOR); - if (area >= pixel_area) - // FIXME this is not a correct centroid of a polygon with holes. - layer.islands.emplace_back(layer, island, get_extents(island.contour), - unscaled(island.contour.centroid()), area, height); + layer.parts.push_back(LayerPart{ + &island, {}, + get_extents(island.contour) + // sample - only hangout part of expolygon could be known after linking + }); + } + }, 4 /*gransize*/); + + // Link parts by intersections + execution::for_each(ex_tbb, size_t(1), result.slices.size(), + [&result, throw_on_cancel](size_t layer_id) { + if ((layer_id % 16) == 0) + throw_on_cancel(); + + LayerParts &parts_above = result.layers[layer_id].parts; + LayerParts &parts_below = result.layers[layer_id-1].parts; + for (auto it_above = parts_above.begin(); it_above < parts_above.end(); ++it_above) { + for (auto it_below = parts_below.begin(); it_below < parts_below.end(); ++it_below) { + // Improve: do some sort of parts + skip some of them + if (!it_above->shape_extent.overlap(it_below->shape_extent)) + continue; // no bounding box overlap + + // Improve: test could be done faster way + Polygons polys = intersection(*it_above->shape, *it_below->shape); + if (polys.empty()) + continue; // no intersection + + // TODO: check minimal intersection! + + it_above->prev_parts.push_back(it_below); + it_below->next_parts.push_back(it_above); + } } - }, 32 /*gransize*/); - - // Calculate overlap of successive layers. Link overlapping islands. - execution::for_each(ex_tbb, size_t(1), layers.size(), - [&layers, &heights, throw_on_cancel] (size_t layer_id) - { - if ((layer_id % 2) == 0) - // Don't call the following function too often as it flushes CPU write caches due to synchronization primitves. - throw_on_cancel(); - SupportPointGenerator::MyLayer &layer_above = layers[layer_id]; - SupportPointGenerator::MyLayer &layer_below = layers[layer_id - 1]; - //FIXME WTF? - const float layer_height = (layer_id!=0 ? heights[layer_id]-heights[layer_id-1] : heights[0]); - const float safe_angle = 35.f * (float(M_PI)/180.f); // smaller number - less supports - const float between_layers_offset = scaled(layer_height * std::tan(safe_angle)); - const float slope_angle = 75.f * (float(M_PI)/180.f); // smaller number - less supports - const float slope_offset = scaled(layer_height * std::tan(slope_angle)); - //FIXME This has a quadratic time complexity, it will be excessively slow for many tiny islands. - for (SupportPointGenerator::Structure &top : layer_above.islands) { - for (SupportPointGenerator::Structure &bottom : layer_below.islands) { - float overlap_area = top.overlap_area(bottom); - if (overlap_area > 0) { - top.islands_below.emplace_back(&bottom, overlap_area); - bottom.islands_above.emplace_back(&top, overlap_area); - } - } - if (! top.islands_below.empty()) { - Polygons bottom_polygons = top.polygons_below(); - top.overhangs = diff_ex(*top.polygon, bottom_polygons); - if (! top.overhangs.empty()) { - - // Produce 2 bands around the island, a safe band for dangling overhangs - // and an unsafe band for sloped overhangs. - // These masks include the original island - auto dangl_mask = expand(bottom_polygons, between_layers_offset, ClipperLib::jtSquare); - auto overh_mask = expand(bottom_polygons, slope_offset, ClipperLib::jtSquare); - - // Absolutely hopeless overhangs are those outside the unsafe band - top.overhangs = diff_ex(*top.polygon, overh_mask); - - // Now cut out the supported core from the safe band - // and cut the safe band from the unsafe band to get distinct - // zones. - overh_mask = diff(overh_mask, dangl_mask); - dangl_mask = diff(dangl_mask, bottom_polygons); - - top.dangling_areas = intersection_ex(*top.polygon, dangl_mask); - top.overhangs_slopes = intersection_ex(*top.polygon, overh_mask); - - top.overhangs_area = 0.f; - std::vector> expolys_with_areas; - for (ExPolygon &ex : top.overhangs) { - float area = float(ex.area()); - expolys_with_areas.emplace_back(&ex, area); - top.overhangs_area += area; - } - std::sort(expolys_with_areas.begin(), expolys_with_areas.end(), - [](const std::pair &p1, const std::pair &p2) - { return p1.second > p2.second; }); - ExPolygons overhangs_sorted; - for (auto &p : expolys_with_areas) - overhangs_sorted.emplace_back(std::move(*p.first)); - top.overhangs = std::move(overhangs_sorted); - top.overhangs_area *= float(SCALING_FACTOR * SCALING_FACTOR); - } - } - } }, 8 /* gransize */); - return layers; + // erase unsupportable model parts + SmallParts small_parts = get_small_parts(result.layers, config.minimal_bounding_sphere_radius); + if(!small_parts.empty()) erase(small_parts, result.layers); + + // Sample overhangs part of island + double sample_distance_in_um = scale_(config.discretize_overhang_step); + double sample_distance_in_um2 = sample_distance_in_um * sample_distance_in_um; + execution::for_each(ex_tbb, size_t(1), result.layers.size(), + [&result, sample_distance_in_um2, throw_on_cancel](size_t layer_id) { + if ((layer_id % 32) == 0) + throw_on_cancel(); + + LayerParts &parts = result.layers[layer_id].parts; + for (auto it_part = parts.begin(); it_part < parts.end(); ++it_part) { + if (it_part->prev_parts.empty()) + continue; // island + + // IMPROVE: overhangs could be calculated with Z coordninate + // soo one will know source shape of point and do not have to search this + // information Get inspiration at + // https://github.com/Prusa-Development/PrusaSlicerPrivate/blob/e00c46f070ec3d6fc325640b0dd10511f8acf5f7/src/libslic3r/PerimeterGenerator.cpp#L399 + it_part->samples = sample_overhangs(*it_part, sample_distance_in_um2); + } + }, 8 /* gransize */); + + // Detect peninsula + execution::for_each(ex_tbb, size_t(1), result.layers.size(), + [&layers = result.layers, &config, throw_on_cancel](size_t layer_id) { + if ((layer_id % 32) == 0) + throw_on_cancel(); + LayerParts &parts = layers[layer_id].parts; + for (auto it_part = parts.begin(); it_part < parts.end(); ++it_part) { + if (it_part->prev_parts.empty()) + continue; // island + create_peninsulas(*it_part, config); + } + }, 8 /* gransize */); + + // calc extended parts, more info PrepareSupportConfig::removing_delta + execution::for_each(ex_tbb, size_t(1), result.layers.size(), + [&layers = result.layers, delta = config.removing_delta, throw_on_cancel](size_t layer_id) { + if ((layer_id % 16) == 0) + throw_on_cancel(); + LayerParts &parts = layers[layer_id].parts; + for (auto it_part = parts.begin(); it_part < parts.end(); ++it_part) + it_part->extend_shape = offset_ex(*it_part->shape, delta, ClipperLib::jtSquare); + }, 8 /* gransize */); + return result; } -void SupportPointGenerator::process(const std::vector& slices, const std::vector& heights) -{ -#ifdef SLA_SUPPORTPOINTGEN_DEBUG - std::vector> islands; -#endif /* SLA_SUPPORTPOINTGEN_DEBUG */ - - std::vector layers = make_layers(slices, heights, m_throw_on_cancel); - - PointGrid3D point_grid; - point_grid.cell_size = Vec3f(10.f, 10.f, 10.f); - - double increment = 100.0 / layers.size(); - double status = 0; - - for (unsigned int layer_id = 0; layer_id < layers.size(); ++ layer_id) { - SupportPointGenerator::MyLayer *layer_top = &layers[layer_id]; - SupportPointGenerator::MyLayer *layer_bottom = (layer_id > 0) ? &layers[layer_id - 1] : nullptr; - std::vector support_force_bottom; - if (layer_bottom != nullptr) { - support_force_bottom.assign(layer_bottom->islands.size(), 0.f); - for (size_t i = 0; i < layer_bottom->islands.size(); ++ i) - support_force_bottom[i] = layer_bottom->islands[i].supports_force_total(); - } - for (Structure &top : layer_top->islands) - for (Structure::Link &bottom_link : top.islands_below) { - Structure &bottom = *bottom_link.island; - //float centroids_dist = (bottom.centroid - top.centroid).norm(); - // Penalization resulting from centroid offset: -// bottom.supports_force *= std::min(1.f, 1.f - std::min(1.f, (1600.f * layer_height) * centroids_dist * centroids_dist / bottom.area)); - float &support_force = support_force_bottom[&bottom - layer_bottom->islands.data()]; -//FIXME this condition does not reflect a bifurcation into a one large island and one tiny island well, it incorrectly resets the support force to zero. -// One should rather work with the overlap area vs overhang area. -// support_force *= std::min(1.f, 1.f - std::min(1.f, 0.1f * centroids_dist * centroids_dist / bottom.area)); - // Penalization resulting from increasing polygon area: - support_force *= std::min(1.f, 20.f * bottom.area / top.area); - } - // Let's assign proper support force to each of them: - if (layer_id > 0) { - for (Structure &below : layer_bottom->islands) { - float below_support_force = support_force_bottom[&below - layer_bottom->islands.data()]; - float above_overlap_area = 0.f; - for (Structure::Link &above_link : below.islands_above) - above_overlap_area += above_link.overlap_area; - for (Structure::Link &above_link : below.islands_above) - above_link.island->supports_force_inherited += below_support_force * above_link.overlap_area / above_overlap_area; - } - } - // Now iterate over all polygons and append new points if needed. - for (Structure &s : layer_top->islands) { - // Penalization resulting from large diff from the last layer: - s.supports_force_inherited /= std::max(1.f, 0.17f * (s.overhangs_area) / s.area); - - add_support_points(s, point_grid); - } - - m_throw_on_cancel(); - - status += increment; - m_statusfn(int(std::round(status))); +#ifdef USE_ISLAND_GUI_FOR_SETTINGS +#include "libslic3r/NSVGUtils.hpp" +#include "libslic3r/Utils.hpp" +std::vector load_curve_from_file() { + std::string filePath = Slic3r::resources_dir() + "/data/sla_support.svg"; + EmbossShape::SvgFile svg_file{filePath}; + NSVGimage *image = init_image(svg_file); + if (image == nullptr) { + // In test is not known resource_dir! + // File is not located soo return DEFAULT permanent radius 5mm is returned + return {Vec2f{5.f,.0f}, Vec2f{5.f, 1.f}}; } + for (NSVGshape *shape_ptr = image->shapes; shape_ptr != NULL; shape_ptr = shape_ptr->next) { + const NSVGshape &shape = *shape_ptr; + if (!(shape.flags & NSVG_FLAGS_VISIBLE)) continue; // is visible + if (shape.fill.type != NSVG_PAINT_NONE) continue; // is not used fill + if (shape.stroke.type == NSVG_PAINT_NONE) continue; // exist stroke + if (shape.strokeWidth < 1e-5f) continue; // is visible stroke width + if (shape.stroke.color != 4278190261) continue; // is red + + // use only first path + const NSVGpath *path = shape.paths; + size_t count_points = path->npts; + assert(count_points > 1); + --count_points; + std::vector points; + points.reserve(count_points/3+1); + points.push_back({path->pts[0], path->pts[1]}); + for (size_t i = 0; i < count_points; i += 3) { + const float *p = &path->pts[i * 2]; + points.push_back({p[6], p[7]}); + } + assert(points.size() >= 2); + return points; + } + + // red curve line is not found + assert(false); + return {}; +} +#endif // USE_ISLAND_GUI_FOR_SETTINGS + +// Processing permanent support points +// Permanent are manualy edited points by user +namespace { +size_t get_index_of_closest_part(const Point &coor, const LayerParts &parts, double max_allowed_distance_sq) { + size_t count_lines = 0; + std::vector part_lines_ends; + part_lines_ends.reserve(parts.size()); + for (const LayerPart &part : parts) { + count_lines += count_points(*part.shape); + part_lines_ends.push_back(count_lines); + } + Linesf lines; + lines.reserve(count_lines); + for (const LayerPart &part : parts) + append(lines, to_linesf({*part.shape})); + AABBTreeIndirect::Tree<2, double> tree = + AABBTreeLines::build_aabb_tree_over_indexed_lines(lines); + + size_t line_idx = std::numeric_limits::max(); + Vec2d coor_d = coor.cast(); + Vec2d hit_point; + [[maybe_unused]] double distance_sq = + AABBTreeLines::squared_distance_to_indexed_lines(lines, tree, coor_d, line_idx, hit_point); + + if (distance_sq >= max_allowed_distance_sq) // point is farer than 1mm from any layer part + return parts.size(); // this support point should not be used any more + + // Find part index of closest line + for (size_t part_index = 0; part_index < part_lines_ends.size(); ++part_index) + if (line_idx < part_lines_ends[part_index]) { + // check point lais inside prev or next part shape + // When assert appear check that part index is really the correct one + assert(union_ex( + get_shapes(parts[part_index].prev_parts), + get_shapes(parts[part_index].next_parts))[0].contains(coor)); + return part_index; + } + + assert(false); // not found + return parts.size(); } -void SupportPointGenerator::add_support_points(SupportPointGenerator::Structure &s, SupportPointGenerator::PointGrid3D &grid3d) -{ - // Select each type of surface (overrhang, dangling, slope), derive the support - // force deficit for it and call uniformly conver with the right params +/// +/// Guess range of layers by its centers +/// NOTE: not valid range for variable layer height but divide space +/// +/// +/// +/// Range of layers +MinMax get_layer_range(const Layers &layers, size_t layer_id) { + assert(layer_id < layers.size()); + if (layer_id >= layers.size()) + return MinMax{0., 0.}; - float tp = m_config.tear_pressure(); - float current = s.supports_force_total(); - - if (s.islands_below.empty()) { - // completely new island - needs support no doubt - // deficit is full, there is nothing below that would hold this island - uniformly_cover({ *s.polygon }, s, s.area * tp, grid3d, IslandCoverageFlags(icfIsNew | icfWithBoundary) ); - return; - } - - if (! s.overhangs.empty()) { - uniformly_cover(s.overhangs, s, s.overhangs_area * tp, grid3d); - } - - auto areafn = [](double sum, auto &p) { return sum + p.area() * SCALING_FACTOR * SCALING_FACTOR; }; - - current = s.supports_force_total(); - if (! s.dangling_areas.empty()) { - // Let's see if there's anything that overlaps enough to need supports: - // What we now have in polygons needs support, regardless of what the forces are, so we can add them. - - double a = std::accumulate(s.dangling_areas.begin(), s.dangling_areas.end(), 0., areafn); - uniformly_cover(s.dangling_areas, s, a * tp - a * current * s.area, grid3d, icfWithBoundary); - } - - current = s.supports_force_total(); - if (! s.overhangs_slopes.empty()) { - double a = std::accumulate(s.overhangs_slopes.begin(), s.overhangs_slopes.end(), 0., areafn); - uniformly_cover(s.overhangs_slopes, s, a * tp - a * current / s.area, grid3d, icfWithBoundary); - } + float print_z = layers[layer_id].print_z; + float min = (layer_id == 0) ? 0.f : (layers[layer_id - 1].print_z + print_z) / 2.f; + float max = ((layer_id + 1) < layers.size()) ? + (layers[layer_id + 1].print_z + print_z) / 2.f : + print_z + (print_z - min); // last layer guess height by prev layer center + return MinMax{min, max}; } -std::vector sample_expolygon(const ExPolygon &expoly, float samples_per_mm2, std::mt19937 &rng) -{ - // Triangulate the polygon with holes into triplets of 3D points. - std::vector triangles = Slic3r::triangulate_expolygon_2f(expoly); - - std::vector out; - if (! triangles.empty()) - { - // Calculate area of each triangle. - auto areas = reserve_vector(triangles.size() / 3); - double aback = 0.; - for (size_t i = 0; i < triangles.size(); ) { - const Vec2f &a = triangles[i ++]; - const Vec2f v1 = triangles[i ++] - a; - const Vec2f v2 = triangles[i ++] - a; - - // Prefix sum of the areas. - areas.emplace_back(aback + 0.5f * std::abs(cross2(v1, v2))); - aback = areas.back(); - } - - size_t num_samples = size_t(ceil(areas.back() * samples_per_mm2)); - std::uniform_real_distribution<> random_triangle(0., double(areas.back())); - std::uniform_real_distribution<> random_float(0., 1.); - for (size_t i = 0; i < num_samples; ++ i) { - double r = random_triangle(rng); - size_t idx_triangle = std::min(std::upper_bound(areas.begin(), areas.end(), (float)r) - areas.begin(), areas.size() - 1) * 3; - // Select a random point on the triangle. - const Vec2f &a = triangles[idx_triangle ++]; - const Vec2f &b = triangles[idx_triangle++]; - const Vec2f &c = triangles[idx_triangle]; -#if 1 - // https://www.cs.princeton.edu/~funk/tog02.pdf - // page 814, formula 1. - double u = float(std::sqrt(random_float(rng))); - double v = float(random_float(rng)); - out.emplace_back(a * (1.f - u) + b * (u * (1.f - v)) + c * (v * u)); -#else - // Greg Turk, Graphics Gems - // https://devsplorer.wordpress.com/2019/08/07/find-a-random-point-on-a-plane-using-barycentric-coordinates-in-unity/ - double u = float(random_float(rng)); - double v = float(random_float(rng)); - if (u + v >= 1.f) { - u = 1.f - u; - v = 1.f - v; - } - out.emplace_back(a + u * (b - a) + v * (c - a)); -#endif +size_t get_index_of_layer_part(const Point& coor, const LayerParts& parts, double max_allowed_distance_sq) { + size_t part_index = parts.size(); + // find part for support point + for (const LayerPart &part : parts) { + if (part.shape_extent.contains(coor) && part.shape->contains(coor)) { + // parts do not overlap each other + assert(part_index >= parts.size()); + part_index = &part - &parts.front(); } } - return out; -} - - -std::vector sample_expolygon(const ExPolygons &expolys, float samples_per_mm2, std::mt19937 &rng) -{ - std::vector out; - for (const ExPolygon &expoly : expolys) - append(out, sample_expolygon(expoly, samples_per_mm2, rng)); - - return out; -} - -void sample_expolygon_boundary(const ExPolygon & expoly, - float samples_per_mm, - std::vector &out, - std::mt19937 & /*rng*/) -{ - double point_stepping_scaled = scale_(1.f) / samples_per_mm; - for (size_t i_contour = 0; i_contour <= expoly.holes.size(); ++ i_contour) { - const Polygon &contour = (i_contour == 0) ? expoly.contour : - expoly.holes[i_contour - 1]; - - const Points pts = contour.equally_spaced_points(point_stepping_scaled); - for (size_t i = 0; i < pts.size(); ++ i) - out.emplace_back(unscale(pts[i].x()), - unscale(pts[i].y())); + if (part_index >= parts.size()) { // support point is not in any part + part_index = get_index_of_closest_part(coor, parts, max_allowed_distance_sq); + // if (part_index >= parts.size()) // support is too far from any part } + return part_index; } -std::vector sample_expolygon_with_boundary(const ExPolygon &expoly, float samples_per_mm2, float samples_per_mm_boundary, std::mt19937 &rng) -{ - std::vector out = sample_expolygon(expoly, samples_per_mm2, rng); - sample_expolygon_boundary(expoly, samples_per_mm_boundary, out, rng); - return out; -} +LayerParts::const_iterator get_closest_part(const PartLinks &links, Vec2d &coor) { + if (links.size() == 1) + return links.front(); -std::vector sample_expolygon_with_boundary(const ExPolygons &expolys, float samples_per_mm2, float samples_per_mm_boundary, std::mt19937 &rng) -{ - std::vector out; - for (const ExPolygon &expoly : expolys) - append(out, sample_expolygon_with_boundary(expoly, samples_per_mm2, samples_per_mm_boundary, rng)); - return out; -} - -template -static inline std::vector poisson_disk_from_samples(const std::vector &raw_samples, float radius, REFUSE_FUNCTION refuse_function) -{ - Vec2f corner_min(std::numeric_limits::max(), std::numeric_limits::max()); - for (const Vec2f &pt : raw_samples) { - corner_min.x() = std::min(corner_min.x(), pt.x()); - corner_min.y() = std::min(corner_min.y(), pt.y()); + Point coor_p = coor.cast(); + // Note: layer part MUST not overlap each other + for (const PartLink &link : links) { + LayerParts::const_iterator part_it = link; + if (part_it->shape_extent.contains(coor_p) && + part_it->shape->contains(coor_p)) { + return part_it; + } } - // Assign the raw samples to grid cells, sort the grid cells lexicographically. - struct RawSample - { - Vec2f coord; - Vec2i cell_id; - RawSample(const Vec2f &crd = {}, const Vec2i &id = {}): coord{crd}, cell_id{id} {} + size_t count_lines = 0; + std::vector part_lines_ends; + part_lines_ends.reserve(links.size()); + for (const PartLink &link: links) { + count_lines += count_points(*link->shape); + part_lines_ends.push_back(count_lines); + } + Linesf lines; + lines.reserve(count_lines); + for (const PartLink &link : links) + append(lines, to_linesf({*link->shape})); + AABBTreeIndirect::Tree<2, double> tree = + AABBTreeLines::build_aabb_tree_over_indexed_lines(lines); + + size_t line_idx = std::numeric_limits::max(); + Vec2d hit_point; + [[maybe_unused]] double distance_sq = + AABBTreeLines::squared_distance_to_indexed_lines(lines, tree, coor, line_idx, hit_point); + + // Find part index of closest line + for (size_t part_index = 0; part_index < part_lines_ends.size(); ++part_index) { + if (line_idx >= part_lines_ends[part_index]) + continue; + + // check point lais inside prev or next part shape + // When assert appear check that part index is really the correct one + assert(union_ex( + get_shapes(links[part_index]->prev_parts), + get_shapes(links[part_index]->next_parts))[0].contains(coor.cast())); + coor = hit_point; // update closest point + return links[part_index]; + } + + assert(false); // not found + return links.front(); +} + +struct PartId { + // index into layers + size_t layer_id; + // index into parts of the previously addresed layer. + size_t part_id; +}; + +/// +/// Dive into previous layers a trace influence over layer parts before support point +/// +/// Index of part that point will appear +/// Index of layer where point will appear +/// Permanent support point +/// All layers +/// +/// First influence: Layer_index + Part_index +PartId get_index_of_first_influence( + const PartId& partid, + const SupportPoint &p, + const Point& coor, + const Layers &layers, + const SupportPointGeneratorConfig &config) { + // find layer part for support + + float max_influence_distance = std::max( + 2 * p.head_front_radius, + config.support_curve.front().x()); + + const LayerParts& parts = layers[partid.layer_id].parts; + LayerParts::const_iterator current_part_it = parts.cbegin() + partid.part_id; + LayerParts::const_iterator prev_part_it = current_part_it; // stop influence just before island + Vec2d coor_d = coor.cast(); + + auto get_part_id = [&layers](size_t layer_index, const LayerParts::const_iterator& part_it) { + const LayerParts &parts = layers[layer_index].parts; + size_t part_index = part_it - parts.cbegin(); + assert(part_index < parts.size()); + return PartId{layer_index, part_index}; }; - - auto raw_samples_sorted = reserve_vector(raw_samples.size()); - for (const Vec2f &pt : raw_samples) - raw_samples_sorted.emplace_back(pt, ((pt - corner_min) / radius).cast()); - - std::sort(raw_samples_sorted.begin(), raw_samples_sorted.end(), [](const RawSample &lhs, const RawSample &rhs) - { return lhs.cell_id.x() < rhs.cell_id.x() || (lhs.cell_id.x() == rhs.cell_id.x() && lhs.cell_id.y() < rhs.cell_id.y()); }); - - struct PoissonDiskGridEntry { - // Resulting output sample points for this cell: - enum { - max_positions = 4 - }; - Vec2f poisson_samples[max_positions]; - int num_poisson_samples = 0; - - // Index into raw_samples: - int first_sample_idx; - int sample_cnt; - }; - - struct CellIDHash { - std::size_t operator()(const Vec2i &cell_id) const { - return std::hash()(cell_id.x()) ^ std::hash()(cell_id.y() * 593); + // Detect not propagate into island + // Island supports has different behavior + // p.pos.z() - p.head_front_radius >= layer.print_z + for (size_t i = 0; i <= partid.layer_id; ++i) { + size_t current_layer_id = partid.layer_id - i; + const Layer &layer = layers[current_layer_id]; + float z_distance = p.pos.z() - layer.print_z; + if (z_distance >= max_influence_distance) + return get_part_id(current_layer_id, current_part_it); // above layer index + + const PartLinks &prev_parts = current_part_it->prev_parts; + if (prev_parts.empty()){ + // Island support + return (z_distance < p.head_front_radius) ? + get_part_id(current_layer_id, current_part_it) : + get_part_id(current_layer_id + 1, prev_part_it); } - }; - // Map from cell IDs to hash_data. Each hash_data points to the range in raw_samples corresponding to that cell. - // (We could just store the samples in hash_data. This implementation is an artifact of the reference paper, which - // is optimizing for GPU acceleration that we haven't implemented currently.) - typedef std::unordered_map Cells; - Cells cells; - { - typename Cells::iterator last_cell_id_it; - Vec2i last_cell_id(-1, -1); - for (size_t i = 0; i < raw_samples_sorted.size(); ++ i) { - const RawSample &sample = raw_samples_sorted[i]; - if (sample.cell_id == last_cell_id) { - // This sample is in the same cell as the previous, so just increase the count. Cells are - // always contiguous, since we've sorted raw_samples_sorted by cell ID. - ++ last_cell_id_it->second.sample_cnt; - } else { - // This is a new cell. - PoissonDiskGridEntry data; - data.first_sample_idx = int(i); - data.sample_cnt = 1; - auto result = cells.insert({sample.cell_id, data}); - last_cell_id = sample.cell_id; - last_cell_id_it = result.first; - } + prev_part_it = current_part_it; + current_part_it = get_closest_part(prev_parts, coor_d); + } + + // It is unreachable! + // The first layer is always island + assert(false); + return PartId{std::numeric_limits::max(), std::numeric_limits::max()}; +} + +struct PermanentSupport { + SupportPoints::const_iterator point_it; // reference to permanent + + // Define wheere layer part when start influene support area + // When part is island also affect distribution of supports on island + PartId influence; + + // Position of support point in layer + PartId part; + Point layer_position; +}; +using PermanentSupports = std::vector; + +/// +/// Prepare permanent supports for layer's parts +/// +/// Permanent supports +/// Define heights of layers +/// Define how to propagate to previous layers +/// Supoorts to add into layer parts +PermanentSupports prepare_permanent_supports( + const SupportPoints &permanent_supports, + const Layers &layers, + const SupportPointGeneratorConfig &config +) { + // How to propagate permanent support position into previous layers? and how deep? requirements + // are chained. IMHO it should start togetjer from islands and permanent than propagate over surface + + if (permanent_supports.empty()) + return {}; + + // permanent supports MUST be sorted by z + assert(std::is_sorted(permanent_supports.begin(), permanent_supports.end(), + [](const SupportPoint &a, const SupportPoint &b) { return a.pos.z() < b.pos.z(); })); + + size_t permanent_index = 0; + PermanentSupports result; + for (size_t layer_id = 0; layer_id < layers.size(); ++layer_id) { + float layer_max_z = get_layer_range(layers, layer_id).max; + if (permanent_index >= permanent_supports.size()) + break; // no more permanent supports + + if (permanent_supports[permanent_index].pos.z() >= layer_max_z) + continue; // no permanent support for this layer + + const Layer &layer = layers[layer_id]; + for (; permanent_index < permanent_supports.size(); ++permanent_index) { + SupportPoints::const_iterator point_it = permanent_supports.begin()+permanent_index; + if (point_it->pos.z() > layer_max_z) + // support point belongs to another layer + // Points are sorted by z + break; + + // find layer part for support + Point coor(static_cast(scale_(point_it->pos.x())), + static_cast(scale_(point_it->pos.y()))); + + double allowed_distance_sq = std::max(config.max_allowed_distance_sq, + sqr(scale_(point_it->head_front_radius))); + size_t part_index = get_index_of_layer_part(coor, layer.parts, allowed_distance_sq); + if (part_index >= layer.parts.size()) + continue; // support point is not in any part + + PartId part_id{layer_id, part_index}; + // find part of first inlfuenced layer and part for this support point + PartId influence = get_index_of_first_influence(part_id, *point_it, coor, layers, config); + result.push_back(PermanentSupport{point_it, influence, part_id, coor}); } } - const int max_trials = 5; - const float radius_squared = radius * radius; - for (int trial = 0; trial < max_trials; ++ trial) { - // Create sample points for each entry in cells. - for (auto &it : cells) { - const Vec2i &cell_id = it.first; - PoissonDiskGridEntry &cell_data = it.second; - // This cell's raw sample points start at first_sample_idx. On trial 0, try the first one. On trial 1, try first_sample_idx + 1. - int next_sample_idx = cell_data.first_sample_idx + trial; - if (trial >= cell_data.sample_cnt) - // There are no more points to try for this cell. + // sort by layer index and part index + std::sort(result.begin(), result.end(), [](const PermanentSupport& s1, const PermanentSupport& s2) { + return s1.influence.layer_id != s2.influence.layer_id ? + s1.influence.layer_id < s2.influence.layer_id : + s1.influence.part_id < s2.influence.part_id; }); + + return result; +} + +bool exist_permanent_support(const PermanentSupports& supports, size_t current_support_index, + size_t layer_index, size_t part_index) { + if (current_support_index >= supports.size()) + return false; + + const PartId &influence = supports[current_support_index].influence; + assert(influence.layer_id >= layer_index); + return influence.layer_id == layer_index && + influence.part_id == part_index; +} + +/// +/// copy permanent supports into near points +/// which has influence into current layer part +/// +/// OUTPUT for all permanent supports for this layer and part +/// source for Copy +/// current index into supports +/// current layer index +/// current layer index +/// current part index +void copy_permanent_supports(NearPoints& near_points, const PermanentSupports& supports, size_t& support_index, + float print_z, size_t layer_index, size_t part_index, const SupportPointGeneratorConfig &config) { + while (exist_permanent_support(supports, support_index, layer_index, part_index)) { + const PermanentSupport &support = supports[support_index]; + near_points.add(LayerSupportPoint{ + /* SupportPoint */ *support.point_it, + /* position_on_layer */ support.layer_position, + /* radius_curve_index */ 0, // before support point - earlier influence on point distribution + /* current_radius */ calc_influence_radius(fabs(support.point_it->pos.z() - print_z), config), + /* active_in_part */ true, + /* is_permanent */ true + }); + + // NOTE: increment index globaly + ++support_index; + } +} + +Points get_permanents(const PermanentSupports &supports, size_t support_index, + size_t layer_index, size_t part_index) { + Points result; + while (exist_permanent_support(supports, support_index, layer_index, part_index)) { + result.push_back(supports[support_index].layer_position); // copy + ++support_index; // only local(temporary) increment + } + return result; +} + +} // namespace + +namespace Slic3r::sla { +using namespace Slic3r; + +std::vector create_default_support_curve(){ +#ifdef USE_ISLAND_GUI_FOR_SETTINGS + return {}; +#else // USE_ISLAND_GUI_FOR_SETTINGS + return std::vector{ + Vec2f{3.2f, 0.f}, + Vec2f{4.f, 3.9f}, + Vec2f{5.f, 15.f}, + Vec2f{6.f, 40.f}, + }; +#endif // USE_ISLAND_GUI_FOR_SETTINGS +} + +SampleConfig create_default_island_configuration(float head_diameter_in_mm) { + return SampleConfigFactory::create(head_diameter_in_mm); +} + +LayerSupportPoints generate_support_points( + const SupportPointGeneratorData &data, + const SupportPointGeneratorConfig &config, + ThrowOnCancel throw_on_cancel, + StatusFunction statusfn +) { + const Layers &layers = data.layers; + double increment = 100.0 / static_cast(layers.size()); + double status = 0; // current progress + int status_int = 0; +#ifdef USE_ISLAND_GUI_FOR_SETTINGS + // Hack to set curve for testing + if (config.support_curve.empty()) + const_cast(config).support_curve = load_curve_from_file(); +#endif // USE_ISLAND_GUI_FOR_SETTINGS + + // Maximal radius of supported area of one support point + double max_support_radius = config.support_curve.back().x(); + // check distance to nearest support points from grid + coord_t maximal_radius = static_cast(scale_(max_support_radius)); + + // Storage for support points used by grid + LayerSupportPoints result; + + // Index into data.permanent_supports + size_t permanent_index = 0; + PermanentSupports permanent_supports = + prepare_permanent_supports(data.permanent_supports, layers, config); + + // grid index == part in layer index + NearPointss prev_grids; // same count as previous layer item size + for (size_t layer_id = 0; layer_id < layers.size(); ++layer_id) { + const Layer &layer = layers[layer_id]; + prepare_supports_for_layer(result, layer.print_z, prev_grids, config); + + // grid index == part in layer index + NearPointss grids; + grids.reserve(layer.parts.size()); + + for (const LayerPart &part : layer.parts) { + size_t part_id = &part - &layer.parts.front(); + if (part.prev_parts.empty()) { // Island ? + grids.emplace_back(&result); // only island add new grid + Points permanent = + get_permanents(permanent_supports, permanent_index, layer_id, part_id); + support_island(part, grids.back(), layer.print_z, permanent, config); + copy_permanent_supports( + grids.back(), permanent_supports, permanent_index, layer.print_z, layer_id, + part_id, config + ); continue; - const RawSample &candidate = raw_samples_sorted[next_sample_idx]; - // See if this point conflicts with any other points in this cell, or with any points in - // neighboring cells. Note that it's possible to have more than one point in the same cell. - bool conflict = refuse_function(candidate.coord); - for (int i = -1; i < 2 && ! conflict; ++ i) { - for (int j = -1; j < 2; ++ j) { - const auto &it_neighbor = cells.find(cell_id + Vec2i(i, j)); - if (it_neighbor != cells.end()) { - const PoissonDiskGridEntry &neighbor = it_neighbor->second; - for (int i_sample = 0; i_sample < neighbor.num_poisson_samples; ++ i_sample) - if ((neighbor.poisson_samples[i_sample] - candidate.coord).squaredNorm() < radius_squared) { - conflict = true; - break; - } - } - } } - if (! conflict) { - // Store the new sample. - assert(cell_data.num_poisson_samples < cell_data.max_positions); - if (cell_data.num_poisson_samples < cell_data.max_positions) - cell_data.poisson_samples[cell_data.num_poisson_samples ++] = candidate.coord; + + // first layer should have empty prev_part + assert(layer_id != 0); + const LayerParts &prev_layer_parts = layers[layer_id - 1].parts; + NearPoints near_points = create_near_points(prev_layer_parts, part, prev_grids); + remove_supports_out_of_part(near_points, part, layer.print_z); + assert(!near_points.get_indices().empty()); + if (!part.peninsulas.empty()) { + // only get copy of points do not modify permanent_index + Points permanent = + get_permanents(permanent_supports, permanent_index, layer_id, part_id); + support_peninsulas(part.peninsulas, near_points, layer.print_z, permanent, config); } + copy_permanent_supports( + near_points, permanent_supports, permanent_index, layer.print_z, layer_id, part_id, + config + ); + support_part_overhangs(part, config, near_points, layer.print_z, maximal_radius); + grids.push_back(std::move(near_points)); } - } + prev_grids = std::move(grids); - // Copy the results to the output. - std::vector out; - for (const auto& it : cells) - for (int i = 0; i < it.second.num_poisson_samples; ++ i) - out.emplace_back(it.second.poisson_samples[i]); - return out; + throw_on_cancel(); + + int old_status_int = status_int; + status += increment; + status_int = static_cast(std::round(status)); + if (old_status_int < status_int) + statusfn(status_int); + } + // Remove permanent supports from result + // To preserve permanent 3d position it is necessary to append points after move_on_mesh_surface + result.erase( + std::remove_if( + result.begin(), result.end(), [](const LayerSupportPoint &p) { return p.is_permanent; } + ), + result.end() + ); + return result; } +SupportPoints move_on_mesh_surface( + const LayerSupportPoints &points, + const AABBMesh &mesh, + double allowed_move, + ThrowOnCancel throw_on_cancel +) { + SupportPoints pts; + pts.reserve(points.size()); + for (const LayerSupportPoint &p : points) + pts.push_back(static_cast(p)); -void SupportPointGenerator::uniformly_cover(const ExPolygons& islands, Structure& structure, float deficit, PointGrid3D &grid3d, IslandCoverageFlags flags) -{ - //int num_of_points = std::max(1, (int)((island.area()*pow(SCALING_FACTOR, 2) * m_config.tear_pressure)/m_config.support_force)); + // The function makes sure that all the points are really exactly placed on the mesh. + execution::for_each( + ex_tbb, size_t(0), pts.size(), + [&pts, &mesh, &throw_on_cancel, allowed_move](size_t idx) { + if ((idx % 16) == 0) + // Don't call the following function too often as it flushes CPU write caches due to + // synchronization primitves. + throw_on_cancel(); - float support_force_deficit = deficit; -// auto bb = get_extents(islands); + Vec3f &p = pts[idx].pos; + Vec3d p_double = p.cast(); + const Vec3d up_vec(0., 0., 1.); + const Vec3d down_vec(0., 0., -1.); + // Project the point upward and downward and choose the closer intersection with the mesh. + AABBMesh::hit_result hit_up = mesh.query_ray_hit(p_double, up_vec); + AABBMesh::hit_result hit_down = mesh.query_ray_hit(p_double, down_vec); - if (flags & icfIsNew) { - auto chull = Geometry::convex_hull(islands); - auto rotbox = MinAreaBoundigBox{chull, MinAreaBoundigBox::pcConvex}; - Vec2d bbdim = {unscaled(rotbox.width()), unscaled(rotbox.height())}; + bool up = hit_up.is_hit(); + bool down = hit_down.is_hit(); + // no hit means support points lay exactly on triangle surface + if (!up && !down) + return; - if (bbdim.x() > bbdim.y()) std::swap(bbdim.x(), bbdim.y()); - double aspectr = bbdim.y() / bbdim.x(); + AABBMesh::hit_result &hit = (!down || hit_up.distance() < hit_down.distance()) ? + hit_up : + hit_down; + if (hit.distance() <= allowed_move) { + p[2] += static_cast(hit.distance() * hit.direction()[2]); + return; + } - support_force_deficit *= (1 + aspectr / 2.); - } - - if (support_force_deficit < 0) - return; - - // Number of newly added points. - const size_t poisson_samples_target = size_t(ceil(support_force_deficit / m_config.support_force())); - - const float density_horizontal = m_config.tear_pressure() / m_config.support_force(); - //FIXME why? - float poisson_radius = std::max(m_config.minimal_distance, 1.f / (5.f * density_horizontal)); -// const float poisson_radius = 1.f / (15.f * density_horizontal); - const float samples_per_mm2 = 30.f / (float(M_PI) * poisson_radius * poisson_radius); - // Minimum distance between samples, in 3D space. -// float min_spacing = poisson_radius / 3.f; - float min_spacing = poisson_radius; - - //FIXME share the random generator. The random generator may be not so cheap to initialize, also we don't want the random generator to be restarted for each polygon. - - std::vector raw_samples = - flags & icfWithBoundary ? - sample_expolygon_with_boundary(islands, samples_per_mm2, - 5.f / poisson_radius, m_rng) : - sample_expolygon(islands, samples_per_mm2, m_rng); - - std::vector poisson_samples; - for (size_t iter = 0; iter < 4; ++ iter) { - poisson_samples = poisson_disk_from_samples(raw_samples, poisson_radius, - [&structure, &grid3d, min_spacing](const Vec2f &pos) { - return grid3d.collides_with(pos, structure.layer->print_z, min_spacing); - }); - if (poisson_samples.size() >= poisson_samples_target || m_config.minimal_distance > poisson_radius-EPSILON) - break; - float coeff = 0.5f; - if (poisson_samples.size() * 2 > poisson_samples_target) - coeff = float(poisson_samples.size()) / float(poisson_samples_target); - poisson_radius = std::max(m_config.minimal_distance, poisson_radius * coeff); - min_spacing = std::max(m_config.minimal_distance, min_spacing * coeff); - } - -#ifdef SLA_SUPPORTPOINTGEN_DEBUG - { - static int irun = 0; - Slic3r::SVG svg(debug_out_path("SLA_supports-uniformly_cover-%d.svg", irun ++), get_extents(islands)); - for (const ExPolygon &island : islands) - svg.draw(island); - for (const Vec2f &pt : raw_samples) - svg.draw(Point(scale_(pt.x()), scale_(pt.y())), "red"); - for (const Vec2f &pt : poisson_samples) - svg.draw(Point(scale_(pt.x()), scale_(pt.y())), "blue"); - } -#endif /* NDEBUG */ - -// assert(! poisson_samples.empty()); - if (poisson_samples_target < poisson_samples.size()) { - std::shuffle(poisson_samples.begin(), poisson_samples.end(), m_rng); - poisson_samples.erase(poisson_samples.begin() + poisson_samples_target, poisson_samples.end()); - } - for (const Vec2f &pt : poisson_samples) { - m_output.emplace_back(float(pt(0)), float(pt(1)), structure.zlevel, m_config.head_diameter/2.f, flags & icfIsNew); - structure.supports_force_this_layer += m_config.support_force(); - grid3d.insert(pt, &structure); - } + // big distance means that ray fly over triangle side (space between triangles) + int triangle_index; + Vec3d closest_point; + double distance = mesh.squared_distance(p_double, triangle_index, closest_point); + if (distance <= std::numeric_limits::epsilon()) + return; // correct coordinate + p = closest_point.cast(); + }, + 64 /* gransize */ + ); + return pts; } - -void remove_bottom_points(std::vector &pts, float lvl) -{ - // get iterator to the reorganized vector end - auto endit = std::remove_if(pts.begin(), pts.end(), [lvl] - (const sla::SupportPoint &sp) { - return sp.pos.z() <= lvl; - }); - - // erase all elements after the new end - pts.erase(endit, pts.end()); -} - -#ifdef SLA_SUPPORTPOINTGEN_DEBUG -void SupportPointGenerator::output_structures(const std::vector& structures) -{ - for (unsigned int i=0 ; i{*structures[i].polygon}, ss.str()); - } -} - -void SupportPointGenerator::output_expolygons(const ExPolygons& expolys, const std::string &filename) -{ - BoundingBox bb(Point(-30000000, -30000000), Point(30000000, 30000000)); - Slic3r::SVG svg_cummulative(filename, bb); - for (size_t i = 0; i < expolys.size(); ++ i) { - /*Slic3r::SVG svg("single"+std::to_string(i)+".svg", bb); - svg.draw(expolys[i]); - svg.draw_outline(expolys[i].contour, "black", scale_(0.05)); - svg.draw_outline(expolys[i].holes, "blue", scale_(0.05)); - svg.Close();*/ - - svg_cummulative.draw(expolys[i]); - svg_cummulative.draw_outline(expolys[i].contour, "black", scale_(0.05)); - svg_cummulative.draw_outline(expolys[i].holes, "blue", scale_(0.05)); - } -} -#endif - -SupportPoints transformed_support_points(const ModelObject &mo, - const Transform3d &trafo) -{ - auto spts = mo.sla_support_points; - Transform3f tr = trafo.cast(); - for (sla::SupportPoint& suppt : spts) { - suppt.pos = tr * suppt.pos; - } - - return spts; -} - -} // namespace sla -} // namespace Slic3r +} // namespace Slic3r::sla diff --git a/src/libslic3r/SLA/SupportPointGenerator.hpp b/src/libslic3r/SLA/SupportPointGenerator.hpp index 734c09bef1..9db992be50 100644 --- a/src/libslic3r/SLA/SupportPointGenerator.hpp +++ b/src/libslic3r/SLA/SupportPointGenerator.hpp @@ -1,250 +1,232 @@ -///|/ Copyright (c) Prusa Research 2020 - 2022 Vojtěch Bubník @bubnikv, Tomáš Mészáros @tamasmeszaros, Lukáš Matěna @lukasmatena +///|/ Copyright (c) Prusa Research 2024 Filip Sykala @Jony01 ///|/ ///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher ///|/ #ifndef SLA_SUPPORTPOINTGENERATOR_HPP #define SLA_SUPPORTPOINTGENERATOR_HPP -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include #include -#include +#include +#include + +#include "libslic3r/Point.hpp" #include "libslic3r/ExPolygon.hpp" -#include "libslic3r/Polygon.hpp" -#include "libslic3r/libslic3r.h" +#include "libslic3r/SLA/SupportPoint.hpp" +#include "libslic3r/SLA/SupportIslands/SampleConfig.hpp" -namespace Slic3r { -class AABBMesh; -} // namespace Slic3r +namespace Slic3r::sla { -// #define SLA_SUPPORTPOINTGEN_DEBUG +std::vector create_default_support_curve(); +SampleConfig create_default_island_configuration(float head_diameter_in_mm); -namespace Slic3r { namespace sla { +/// +/// Configuration for automatic support placement +/// +struct SupportPointGeneratorConfig{ + /// + /// 0 mean only one support point for each island + /// lower than one mean less amount of support points + /// 1 mean fine tuned sampling + /// more than one mean bigger amout of support points + /// + float density_relative{1.f}; -class SupportPointGenerator { -public: - struct Config { - float density_relative {1.f}; - float minimal_distance {1.f}; - float head_diameter {0.4f}; + /// + /// Size range for support point interface (head) + /// + float head_diameter = 0.4f; // [in mm] - // Originally calibrated to 7.7f, reduced density by Tamas to 70% which is 11.1 (7.7 / 0.7) to adjust for new algorithm changes in tm_suppt_gen_improve - inline float support_force() const { return 11.1f / density_relative; } // a force one point can support (arbitrary force unit) - inline float tear_pressure() const { return 1.f; } // pressure that the display exerts (the force unit per mm2) - }; - - SupportPointGenerator(const AABBMesh& emesh, const std::vector& slices, - const std::vector& heights, const Config& config, std::function throw_on_cancel, std::function statusfn); - - SupportPointGenerator(const AABBMesh& emesh, const Config& config, std::function throw_on_cancel, std::function statusfn); - - const std::vector& output() const { return m_output; } - std::vector& output() { return m_output; } - - struct MyLayer; - - struct Structure { - Structure(MyLayer &layer, const ExPolygon& poly, const BoundingBox &bbox, const Vec2f ¢roid, float area, float h) : - layer(&layer), polygon(&poly), bbox(bbox), centroid(centroid), area(area), zlevel(h) -#ifdef SLA_SUPPORTPOINTGEN_DEBUG - , unique_id(std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch())) -#endif /* SLA_SUPPORTPOINTGEN_DEBUG */ - {} - MyLayer *layer; - const ExPolygon* polygon = nullptr; - const BoundingBox bbox; - const Vec2f centroid = Vec2f::Zero(); - const float area = 0.f; - float zlevel = 0; - // How well is this ExPolygon held to the print base? - // Positive number, the higher the better. - float supports_force_this_layer = 0.f; - float supports_force_inherited = 0.f; - float supports_force_total() const { return this->supports_force_this_layer + this->supports_force_inherited; } -#ifdef SLA_SUPPORTPOINTGEN_DEBUG - std::chrono::milliseconds unique_id; -#endif /* SLA_SUPPORTPOINTGEN_DEBUG */ - - struct Link { - Link(Structure *island, float overlap_area) : island(island), overlap_area(overlap_area) {} - Structure *island; - float overlap_area; - }; + // maximal distance to nearest support point(define radiuses per layer) + // x axis .. mean distance on layer(XY) + // y axis .. mean difference of height(Z) + // Points of lines [in mm] + std::vector support_curve = create_default_support_curve(); -#ifdef NDEBUG - // In release mode, use the optimized container. - boost::container::small_vector islands_above; - boost::container::small_vector islands_below; -#else - // In debug mode, use the standard vector, which is well handled by debugger visualizer. - std::vector islands_above; - std::vector islands_below; -#endif - // Overhangs, that are dangling considerably. - ExPolygons dangling_areas; - // Complete overhands. - ExPolygons overhangs; - // Overhangs, where the surface must slope. - ExPolygons overhangs_slopes; - float overhangs_area = 0.f; - - bool overlaps(const Structure &rhs) const { - return this->bbox.overlap(rhs.bbox) && this->polygon->overlaps(*rhs.polygon); - } - float overlap_area(const Structure &rhs) const { - double out = 0.; - if (this->bbox.overlap(rhs.bbox)) { - Polygons polys = intersection(*this->polygon, *rhs.polygon); - for (const Polygon &poly : polys) - out += poly.area(); - } - return float(out); - } - float area_below() const { - float area = 0.f; - for (const Link &below : this->islands_below) - area += below.island->area; - return area; - } - Polygons polygons_below() const { - size_t cnt = 0; - for (const Link &below : this->islands_below) - cnt += 1 + below.island->polygon->holes.size(); - Polygons out; - out.reserve(cnt); - for (const Link &below : this->islands_below) { - out.emplace_back(below.island->polygon->contour); - append(out, below.island->polygon->holes); - } - return out; - } - ExPolygons expolygons_below() const { - ExPolygons out; - out.reserve(this->islands_below.size()); - for (const Link &below : this->islands_below) - out.emplace_back(*below.island->polygon); - return out; - } - // Positive deficit of the supports. If negative, this area is well supported. If positive, more supports need to be added. - float support_force_deficit(const float tear_pressure) const { return this->area * tear_pressure - this->supports_force_total(); } - }; - - struct MyLayer { - MyLayer(const size_t layer_id, coordf_t print_z) : layer_id(layer_id), print_z(print_z) {} - size_t layer_id; - coordf_t print_z; - std::vector islands; - }; - - struct RichSupportPoint { - Vec3f position; - Structure *island; - }; - - struct PointGrid3D { - struct GridHash { - std::size_t operator()(const Vec3i &cell_id) const { - return std::hash()(cell_id.x()) ^ std::hash()(cell_id.y() * 593) ^ std::hash()(cell_id.z() * 7919); - } - }; - typedef std::unordered_multimap Grid; - - Vec3f cell_size; - Grid grid; - - Vec3i cell_id(const Vec3f &pos) { - return Vec3i(int(floor(pos.x() / cell_size.x())), - int(floor(pos.y() / cell_size.y())), - int(floor(pos.z() / cell_size.z()))); - } - - void insert(const Vec2f &pos, Structure *island) { - RichSupportPoint pt; - pt.position = Vec3f(pos.x(), pos.y(), float(island->layer->print_z)); - pt.island = island; - grid.emplace(cell_id(pt.position), pt); - } - - bool collides_with(const Vec2f &pos, float print_z, float radius) { - Vec3f pos3d(pos.x(), pos.y(), print_z); - Vec3i cell = cell_id(pos3d); - std::pair it_pair = grid.equal_range(cell); - if (collides_with(pos3d, radius, it_pair.first, it_pair.second)) - return true; - for (int i = -1; i < 2; ++ i) - for (int j = -1; j < 2; ++ j) - for (int k = -1; k < 1; ++ k) { - if (i == 0 && j == 0 && k == 0) - continue; - it_pair = grid.equal_range(cell + Vec3i(i, j, k)); - if (collides_with(pos3d, radius, it_pair.first, it_pair.second)) - return true; - } - return false; - } - - private: - bool collides_with(const Vec3f &pos, float radius, Grid::const_iterator it_begin, Grid::const_iterator it_end) { - for (Grid::const_iterator it = it_begin; it != it_end; ++ it) { - float dist2 = (it->second.position - pos).squaredNorm(); - if (dist2 < radius * radius) - return true; - } - return false; - } - }; - - void execute(const std::vector &slices, - const std::vector & heights); - - void seed(std::mt19937::result_type s) { m_rng.seed(s); } -private: - std::vector m_output; - - SupportPointGenerator::Config m_config; - - void process(const std::vector& slices, const std::vector& heights); + // Configuration for sampling island + SampleConfig island_configuration = create_default_island_configuration(head_diameter); -public: - enum IslandCoverageFlags : uint8_t { icfNone = 0x0, icfIsNew = 0x1, icfWithBoundary = 0x2 }; - -private: - - void uniformly_cover(const ExPolygons& islands, Structure& structure, float deficit, PointGrid3D &grid3d, IslandCoverageFlags flags = icfNone); - - void add_support_points(Structure& structure, PointGrid3D &grid3d); - - void project_onto_mesh(std::vector& points) const; - -#ifdef SLA_SUPPORTPOINTGEN_DEBUG - static void output_expolygons(const ExPolygons& expolys, const std::string &filename); - static void output_structures(const std::vector &structures); -#endif // SLA_SUPPORTPOINTGEN_DEBUG - - const AABBMesh& m_emesh; - std::function m_throw_on_cancel; - std::function m_statusfn; - - std::mt19937 m_rng; + // maximal allowed distance to layer part for permanent(manual edited) support + // helps to identify not wanted support points during automatic support generation. + double max_allowed_distance_sq = scale_(1) * scale_(1); // 1mm }; -void remove_bottom_points(std::vector &pts, float lvl); +struct LayerPart; // forward decl. +using LayerParts = std::vector; -std::vector sample_expolygon(const ExPolygon &expoly, float samples_per_mm2, std::mt19937 &rng); -void sample_expolygon_boundary(const ExPolygon &expoly, float samples_per_mm, std::vector &out, std::mt19937 &rng); +using PartLink = LayerParts::const_iterator; +#ifdef NDEBUG +// In release mode, use the optimized container. +using PartLinks = boost::container::small_vector; +#else +// In debug mode, use the standard vector, which is well handled by debugger visualizer. +using PartLinks = std::vector; +#endif -}} // namespace Slic3r::sla +// Large one layer overhang that needs to be supported on the overhanging side +struct Peninsula{ + // shape of peninsula + //ExPolygon shape; -#endif // SUPPORTPOINTGENERATOR_HPP + // Prev layer is extended by self support const and subtracted from current one. + // This part of layer is supported as island + ExPolygon unsuported_area; + + // Flag for unsuported_area line about source + // Same size as Slic3r::to_lines(unsuported_area) + // True .. peninsula outline(coast) + // False .. connection to land(already supported by previous layer) + std::vector is_outline; +}; +using Peninsulas = std::vector; + +// Part on layer is defined by its shape +struct LayerPart { + // Pointer to expolygon stored in input + const ExPolygon *shape; + + // To detect irelevant support poinst for part + ExPolygons extend_shape; + + // rectangular bounding box of shape + BoundingBox shape_extent; + + // uniformly sampled shape contour + Points samples; + + // Parts from previous printed layer, which is connected to current part + PartLinks prev_parts; + PartLinks next_parts; + + // half island is supported as special case + Peninsulas peninsulas; +}; + +/// +/// Extend support point with information from layer +/// +struct LayerSupportPoint: public SupportPoint +{ + // 2d coordinate on layer + // use only when part is not nullptr + Point position_on_layer; // [scaled_ unit] + + // index into curve to faster found radius for current layer + size_t radius_curve_index = 0; + coord_t current_radius = 0; // [in scaled mm] + + // information whether support point is active in current investigated layer + // Flagged false when no part on layer in Radius 'r' around support point + // Tool to support overlapped overhang area multiple times + bool active_in_part = true; + + // When true support point position is not generated by algorithm + bool is_permanent = false; +}; +using LayerSupportPoints = std::vector; + +/// +/// One slice divided into +/// +struct Layer +{ + // Absolute distance from Zero - copy value from heights + // It represents center of layer(range of layer is half layer size above and belowe) + float print_z; // [in mm] + + // data for one expolygon + LayerParts parts; +}; +using Layers = std::vector; + +/// +/// Keep state of Support Point generation +/// Used for resampling with different configuration +/// +struct SupportPointGeneratorData +{ + // Input slices of mesh + std::vector slices; + + // Keep information about layer and its height + // and connection between layers for its part + // NOTE: contain links into slices + Layers layers; + + // Manualy edited supports by user should be permanent + SupportPoints permanent_supports; +}; + +// call during generation of support points to check cancel event +using ThrowOnCancel = std::function; +// call to say progress of generation into gui in range from 0 to 100 +using StatusFunction= std::function; + +struct PrepareGeneratorDataConfig +{ + // Discretization of overhangs outline, + // smaller will slow down the process but will be more precise + double discretize_overhang_sample_in_mm = 2.; // [in mm] + + // Define minimal width of overhang to be considered as peninsula + // (partialy island - sampled not on edge) + coord_t peninsula_width = scale_(2.); // [in scaled mm] +}; + +/// +/// Prepare data for generate support points +/// Used for interactive resampling to store permanent data between configuration changes., +/// Everything which could be prepared are stored into result. +/// Need to regenerate on mesh change(Should be connected with ObjectId) OR change of slicing heights +/// +/// Countour cut from mesh +/// Heights of the slices - Same size as slices +/// Preparation parameters +/// Call in meanwhile to check cancel event +/// Say progress of generation into gui +/// Data prepared for generate support points +SupportPointGeneratorData prepare_generator_data( + std::vector &&slices, + const std::vector &heights, + const PrepareSupportConfig &config = {}, + ThrowOnCancel throw_on_cancel = []() {}, + StatusFunction statusfn = [](int) {} +); + +/// +/// Generate support points on islands by configuration parameters +/// +/// Preprocessed data needed for sampling +/// Define density of samples +/// Call in meanwhile to check cancel event +/// Progress of generation into gui +/// Generated support points +LayerSupportPoints generate_support_points( + const SupportPointGeneratorData &data, + const SupportPointGeneratorConfig &config, + ThrowOnCancel throw_on_cancel = []() {}, + StatusFunction statusfn = [](int) {} +); +} // namespace Slic3r::sla + +// TODO: Not sure if it is neccessary & Should be in another file +namespace Slic3r{ +class AABBMesh; +namespace sla { +/// +/// Move support points on surface of mesh +/// +/// Support points to move on surface +/// Define surface for move points +/// Call in meanwhile to check cancel event +/// Support points laying on mesh surface +SupportPoints move_on_mesh_surface( + const LayerSupportPoints &points, + const AABBMesh &mesh, + double allowed_move, + ThrowOnCancel throw_on_cancel = []() {} +); + +}} + +#endif // SLA_SUPPORTPOINTGENERATOR_HPP diff --git a/src/libslic3r/SLAPrint.cpp b/src/libslic3r/SLAPrint.cpp index 954aed7a2a..1d5c41041e 100644 --- a/src/libslic3r/SLAPrint.cpp +++ b/src/libslic3r/SLAPrint.cpp @@ -1015,7 +1015,6 @@ bool SLAPrintObject::invalidate_state_by_config_options(const std::vectorsla_support_points; + Transform3f tr = trafo().cast(); + for (sla::SupportPoint &suppt : spts) { + suppt.pos = tr * suppt.pos; + } + return spts; } sla::DrainHoles SLAPrintObject::transformed_drainhole_points() const diff --git a/src/libslic3r/SLAPrint.hpp b/src/libslic3r/SLAPrint.hpp index abb76b6b7c..c931fa4105 100644 --- a/src/libslic3r/SLAPrint.hpp +++ b/src/libslic3r/SLAPrint.hpp @@ -26,6 +26,7 @@ #include "PrintBase.hpp" #include "SLA/SupportTree.hpp" +#include "SLA/SupportPointGenerator.hpp" // SupportPointGeneratorData #include "Point.hpp" #include "Format/SLAArchiveWriter.hpp" #include "libslic3r/GCode/ThumbnailData.hpp" @@ -367,6 +368,9 @@ private: std::vector m_model_height_levels; + // Precalculated data needed for interactive automatic support placement. + sla::SupportPointGeneratorData m_support_point_generator_data; + struct SupportData { sla::SupportableMesh input; // the input diff --git a/src/libslic3r/SLAPrintSteps.cpp b/src/libslic3r/SLAPrintSteps.cpp index 3b92cd861c..0ea737db9d 100644 --- a/src/libslic3r/SLAPrintSteps.cpp +++ b/src/libslic3r/SLAPrintSteps.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -51,9 +52,9 @@ #include "libslic3r/SLA/Hollowing.hpp" #include "libslic3r/SLA/JobController.hpp" #include "libslic3r/SLA/RasterBase.hpp" -#include "libslic3r/SLA/SupportPoint.hpp" #include "libslic3r/SLA/SupportTree.hpp" #include "libslic3r/SLA/SupportTreeStrategies.hpp" +#include "libslic3r/SLA/SupportIslands/SampleConfigFactory.hpp" #include "libslic3r/SLAPrint.hpp" #include "libslic3r/TriangleMesh.hpp" @@ -105,8 +106,77 @@ std::string PRINT_STEP_LABELS(size_t idx) assert(false); return "Out of bounds!"; } +using namespace sla; + +/// +/// Copy permanent support points from model to permanent_supports +/// +/// OUTPUT +/// +/// +void prepare_permanent_support_points( + SupportPoints &permanent_supports, + const SupportPoints &object_supports, + const Transform3d &object_trafo, + const AABBMesh &emesh) { + // update permanent support points + permanent_supports.clear(); // previous supports are irelevant + for (const SupportPoint &p : object_supports) { + if (p.type != SupportPointType::manual_add) + continue; + + // TODO: remove transformation + // ?? Why need transform the position? + Vec3f pos = (object_trafo * p.pos.cast()).cast(); + double dist_sq = emesh.squared_distance(pos.cast()); + if (dist_sq >= sqr(p.head_front_radius)) { + // TODO: inform user about skipping points, which are far from surface + assert(false); + continue; // skip points outside the mesh + } + permanent_supports.push_back(p); // copy + permanent_supports.back().pos = pos; // ?? Why need transform the position? + } + + // Prevent overlapped permanent supports + auto point_accessor = [&permanent_supports](size_t idx, size_t dim) -> float & { + return permanent_supports[idx].pos[dim]; }; + std::vector indices(permanent_supports.size()); + std::iota(indices.begin(), indices.end(), 0); + KDTreeIndirect<3, float, decltype(point_accessor)> tree(point_accessor, indices); + for (SupportPoint &p : permanent_supports) { + if (p.head_front_radius < 0.f) + continue; // already marked for erase + + std::vector near_indices = find_nearby_points(tree, p.pos, p.head_front_radius); + assert(!near_indices.empty()); + if (near_indices.size() == 1) + continue; // only support itself + + size_t index = &p - &permanent_supports.front(); + for (size_t near_index : near_indices) { + if (near_index == index) + continue; // support itself + SupportPoint p_near = permanent_supports[near_index]; + if ((p.pos - p_near.pos).squaredNorm() > sqr(p.head_front_radius)) + continue; // not near point + // IMPROVE: investigate neighbors of the near point + + // TODO: inform user about skip near point + assert(false); + permanent_supports[near_index].head_front_radius = -1.0f; // mark for erase + } + } + + permanent_supports.erase(std::remove_if(permanent_supports.begin(), permanent_supports.end(), + [](const SupportPoint &p) { return p.head_front_radius < 0.f; }),permanent_supports.end()); + + std::sort(permanent_supports.begin(), permanent_supports.end(), + [](const SupportPoint& p1,const SupportPoint& p2){ return p1.pos.z() < p2.pos.z(); }); } +} // namespace + SLAPrint::Steps::Steps(SLAPrint *print) : m_print{print} , objcount{m_print->m_objects.size()} @@ -469,6 +539,29 @@ template BoundingBoxf3 csgmesh_positive_bb(const Cont &csg) return bb3d; } +void SLAPrint::Steps::prepare_for_generate_supports(SLAPrintObject &po) { + using namespace sla; + std::vector slices = po.get_model_slices(); // copy + const std::vector &heights = po.m_model_height_levels; +#ifdef USE_ISLAND_GUI_FOR_SETTINGS + const PrepareSupportConfig &prepare_cfg = SampleConfigFactory::get_sample_config(po.config().support_head_front_diameter).prepare_config; // use configuration edited by GUI +#else // USE_ISLAND_GUI_FOR_SETTINGS + const PrepareSupportConfig prepare_cfg; // use Default values of the configuration +#endif // USE_ISLAND_GUI_FOR_SETTINGS + ThrowOnCancel cancel = [this]() { throw_if_canceled(); }; + + // scaling for the sub operations + double d = objectstep_scale * OBJ_STEP_LEVELS[slaposSupportPoints] / 200.0; + double init = current_status(); + StatusFunction status = [this, d, init](unsigned st) { + double current = init + st * d; + if (std::round(current_status()) < std::round(current)) + report_status(current, OBJ_STEP_LABELS(slaposSupportPoints)); + }; + po.m_support_point_generator_data = + prepare_generator_data(std::move(slices), heights, prepare_cfg, cancel, status); +} + // The slicing will be performed on an imaginary 1D grid which starts from // the bottom of the bounding box created around the supported model. So // the first layer which is usually thicker will be part of the supports @@ -543,6 +636,10 @@ void SLAPrint::Steps::slice_model(SLAPrintObject &po) // We apply the printer correction offset here. apply_printer_corrections(po, soModel); + // Prepare data for the support point generator only when supports are enabled + if (po.m_config.supports_enable.getBool()) + // We need to prepare data in previous step to create interactive support point generation + prepare_for_generate_supports(po); // po.m_preview_meshes[slaposObjectSlice] = po.get_mesh_to_print(); // report_status(-2, "", SlicingStatus::RELOAD_SLA_PREVIEW); } @@ -610,6 +707,7 @@ static void filter_support_points_by_modifiers(sla::SupportPoints &pts, // support points. Then we sprinkle the rest of the mesh. void SLAPrint::Steps::support_points(SLAPrintObject &po) { + using namespace sla; // If supports are disabled, we can skip the model scan. if(!po.m_config.supports_enable.getBool()) return; @@ -628,86 +726,115 @@ void SLAPrint::Steps::support_points(SLAPrintObject &po) BOOST_LOG_TRIVIAL(debug) << "Support point count " << mo.sla_support_points.size(); - // Unless the user modified the points or we already did the calculation, - // we will do the autoplacement. Otherwise we will just blindly copy the - // frontend data into the backend cache. - if (mo.sla_points_status != sla::PointsStatus::UserModified) { - - // calculate heights of slices (slices are calculated already) - const std::vector& heights = po.m_model_height_levels; - - throw_if_canceled(); - sla::SupportPointGenerator::Config config; - const SLAPrintObjectConfig& cfg = po.config(); - - // the density config value is in percents: - config.density_relative = float(cfg.support_points_density_relative / 100.f); - config.minimal_distance = float(cfg.support_points_minimal_distance); - switch (cfg.support_tree_type) { - case sla::SupportTreeType::Default: - case sla::SupportTreeType::Organic: - config.head_diameter = float(cfg.support_head_front_diameter); - break; - case sla::SupportTreeType::Branching: - config.head_diameter = float(cfg.branchingsupport_head_front_diameter); - break; - } - - // scaling for the sub operations - double d = objectstep_scale * OBJ_STEP_LEVELS[slaposSupportPoints] / 100.0; - double init = current_status(); - - auto statuscb = [this, d, init](unsigned st) - { - double current = init + st * d; - if(std::round(current_status()) < std::round(current)) - report_status(current, OBJ_STEP_LABELS(slaposSupportPoints)); - }; - - // Construction of this object does the calculation. - throw_if_canceled(); - sla::SupportPointGenerator auto_supports( - po.m_supportdata->input.emesh, po.get_model_slices(), - heights, config, [this]() { throw_if_canceled(); }, statuscb); - - // Now let's extract the result. - std::vector& points = auto_supports.output(); - throw_if_canceled(); - - MeshSlicingParamsEx params; - params.closing_radius = float(po.config().slice_closing_radius.value); - std::vector blockers = - slice_volumes(po.model_object()->volumes, - po.m_model_height_levels, po.trafo(), params, - [](const ModelVolume *vol) { - return vol->is_support_blocker(); - }); - - std::vector enforcers = - slice_volumes(po.model_object()->volumes, - po.m_model_height_levels, po.trafo(), params, - [](const ModelVolume *vol) { - return vol->is_support_enforcer(); - }); - - SuppPtMask mask{blockers, enforcers, po.config().support_enforcers_only.getBool()}; - filter_support_points_by_modifiers(points, mask, po.m_model_height_levels); - - po.m_supportdata->input.pts = points; - - BOOST_LOG_TRIVIAL(debug) - << "Automatic support points: " - << po.m_supportdata->input.pts.size(); - - // Using RELOAD_SLA_SUPPORT_POINTS to tell the Plater to pass - // the update status to GLGizmoSlaSupports - report_status(-1, _u8L("Generating support points"), - SlicingStatus::RELOAD_SLA_SUPPORT_POINTS); - } else { + if (mo.sla_points_status == PointsStatus::UserModified) { // There are either some points on the front-end, or the user // removed them on purpose. No calculation will be done. po.m_supportdata->input.pts = po.transformed_support_points(); + return; } + // Unless the user modified the points or we already did the calculation, + // we will do the autoplacement. Otherwise we will just blindly copy the + // frontend data into the backend cache. + // if (mo.sla_points_status != PointsStatus::UserModified) + + throw_if_canceled(); + const SLAPrintObjectConfig& cfg = po.config(); + + // the density config value is in percents: + SupportPointGeneratorConfig config; + config.density_relative = float(cfg.support_points_density_relative / 100.f); + + switch (cfg.support_tree_type) { + case SupportTreeType::Default: + case SupportTreeType::Organic: + config.head_diameter = float(cfg.support_head_front_diameter); + break; + case SupportTreeType::Branching: + config.head_diameter = float(cfg.branchingsupport_head_front_diameter); + break; + } + + // copy current configuration for sampling islands +#ifdef USE_ISLAND_GUI_FOR_SETTINGS + // use static variable to propagate data from GUI + config.island_configuration = SampleConfigFactory::get_sample_config(config.density_relative); +#else // USE_ISLAND_GUI_FOR_SETTINGS + config.island_configuration = SampleConfigFactory::apply_density( + SampleConfigFactory::create(config.head_diameter), config.density_relative); +#endif // USE_ISLAND_GUI_FOR_SETTINGS + + // scaling for the sub operations + double d = objectstep_scale * OBJ_STEP_LEVELS[slaposSupportPoints] / 100.0; + double init = current_status(); + + auto statuscb = [this, d, init](unsigned st) + { + double current = init + st * d; + if(std::round(current_status()) < std::round(current)) + report_status(current, OBJ_STEP_LABELS(slaposSupportPoints)); + }; + + // Construction of this object does the calculation. + throw_if_canceled(); + + // TODO: filter small unprintable islands in slices + // (Island with area smaller than 1 pixel was skipped in support generator) + + SupportPointGeneratorData &data = po.m_support_point_generator_data; + SupportPoints &permanent_supports = data.permanent_supports; + const SupportPoints &object_supports = po.model_object()->sla_support_points; + const Transform3d& object_trafo = po.trafo(); + const AABBMesh& emesh = po.m_supportdata->input.emesh; + prepare_permanent_support_points(permanent_supports, object_supports, object_trafo, emesh); + + ThrowOnCancel cancel = [this]() { throw_if_canceled(); }; + StatusFunction status = statuscb; + LayerSupportPoints layer_support_points = generate_support_points(data, config, cancel, status); + + // Maximal move of support point to mesh surface, + // no more than height of layer + assert(po.m_model_height_levels.size() > 1); + double allowed_move = (po.m_model_height_levels[1] - po.m_model_height_levels[0]) + + std::numeric_limits::epsilon(); + SupportPoints support_points = + move_on_mesh_surface(layer_support_points, emesh, allowed_move, cancel); + + // The Generator count with permanent support positions but do not convert to LayerSupportPoints. + // To preserve permanent 3d position it is necessary to append points after move_on_mesh_surface + support_points.insert(support_points.end(), + permanent_supports.begin(), permanent_supports.end()); + + throw_if_canceled(); + + MeshSlicingParamsEx params; + params.closing_radius = float(po.config().slice_closing_radius.value); + std::vector blockers = + slice_volumes(po.model_object()->volumes, + po.m_model_height_levels, po.trafo(), params, + [](const ModelVolume *vol) { + return vol->is_support_blocker(); + }); + + std::vector enforcers = + slice_volumes(po.model_object()->volumes, + po.m_model_height_levels, po.trafo(), params, + [](const ModelVolume *vol) { + return vol->is_support_enforcer(); + }); + + SuppPtMask mask{blockers, enforcers, po.config().support_enforcers_only.getBool()}; + filter_support_points_by_modifiers(support_points, mask, po.m_model_height_levels); + + po.m_supportdata->input.pts = support_points; + + BOOST_LOG_TRIVIAL(debug) + << "Automatic support points: " + << po.m_supportdata->input.pts.size(); + + // Using RELOAD_SLA_SUPPORT_POINTS to tell the Plater to pass + // the update status to GLGizmoSlaSupports + report_status(-1, _u8L("Generating support points"), + SlicingStatus::RELOAD_SLA_SUPPORT_POINTS); } void SLAPrint::Steps::support_tree(SLAPrintObject &po) @@ -717,10 +844,17 @@ void SLAPrint::Steps::support_tree(SLAPrintObject &po) // If the zero elevation mode is engaged, we have to filter out all the // points that are on the bottom of the object if (is_zero_elevation(po.config())) { - remove_bottom_points(po.m_supportdata->input.pts, - float( - po.m_supportdata->input.zoffset + - EPSILON)); + // remove_bottom_points + std::vector &pts = po.m_supportdata->input.pts; + float lvl(po.m_supportdata->input.zoffset + EPSILON); + + // get iterator to the reorganized vector end + auto endit = std::remove_if(pts.begin(), pts.end(), + [lvl](const sla::SupportPoint &sp) { + return sp.pos.z() <= lvl; }); + + // erase all elements after the new end + pts.erase(endit, pts.end()); } po.m_supportdata->input.cfg = make_support_cfg(po.m_config); diff --git a/src/libslic3r/SLAPrintSteps.hpp b/src/libslic3r/SLAPrintSteps.hpp index a1e50842c7..de1f256f5e 100644 --- a/src/libslic3r/SLAPrintSteps.hpp +++ b/src/libslic3r/SLAPrintSteps.hpp @@ -56,6 +56,8 @@ private: void generate_preview(SLAPrintObject &po, SLAPrintObjectStep step); indexed_triangle_set generate_preview_vdb(SLAPrintObject &po, SLAPrintObjectStep step); + void prepare_for_generate_supports(SLAPrintObject &po); + public: explicit Steps(SLAPrint *print); diff --git a/src/libslic3r/SVG.cpp b/src/libslic3r/SVG.cpp index 01909354ef..5027627ccf 100644 --- a/src/libslic3r/SVG.cpp +++ b/src/libslic3r/SVG.cpp @@ -19,6 +19,7 @@ #include "libslic3r/Line.hpp" #include "libslic3r/Surface.hpp" #include "libslic3r/libslic3r.h" +#include "libslic3r/Utils.hpp" namespace Slic3r { @@ -110,6 +111,19 @@ void SVG::draw(const ExPolygon &expolygon, std::string fill, const float fill_op this->path(d, true, 0, fill_opacity); } +void SVG::draw_original(const ExPolygon &expolygon) { + std::ostringstream d; + auto write_d = [&d](const Points &pts) { + d << "M "; + for (const Point& p: pts) + d << p.x() << " " << p.y() << " "; + d << "z "; // closed path + }; + for (const Polygon &p : to_polygons(expolygon)) + write_d(p.points); + path(d.str(), false /*fill*/, 1 /*stroke_width*/, 0.f /*fill opacity*/); +} + void SVG::draw_outline(const ExPolygon &expolygon, std::string stroke_outer, std::string stroke_holes, coordf_t stroke_width) { draw_outline(expolygon.contour, stroke_outer, stroke_width); diff --git a/src/libslic3r/SVG.hpp b/src/libslic3r/SVG.hpp index 1de7f495c6..8d32b07604 100644 --- a/src/libslic3r/SVG.hpp +++ b/src/libslic3r/SVG.hpp @@ -96,6 +96,9 @@ public: void draw_text(const Point &pt, const char *text, const char *color, coordf_t font_size = 20.f); void draw_legend(const Point &pt, const char *text, const char *color, coordf_t font_size = 10.f); + // Draw no scaled expolygon coordinates + void draw_original(const ExPolygon &exPoly); + void Close(); private: diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp index 888a9906c7..b838a267d8 100644 --- a/src/slic3r/GUI/ConfigManipulation.cpp +++ b/src/slic3r/GUI/ConfigManipulation.cpp @@ -478,7 +478,6 @@ void ConfigManipulation::toggle_print_sla_options(DynamicPrintConfig* config) toggle_field("branchingsupport_max_weight_on_model", supports_en && is_branching_tree); toggle_field("support_points_density_relative", supports_en); - toggle_field("support_points_minimal_distance", supports_en); bool pad_en = config->opt_bool("pad_enable"); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp b/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp index fdba58a62b..025849839f 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp @@ -17,21 +17,97 @@ #include "slic3r/GUI/GUI.hpp" #include "slic3r/GUI/GUI_ObjectSettings.hpp" #include "slic3r/GUI/GUI_ObjectList.hpp" +#include "slic3r/GUI/format.hpp" #include "slic3r/GUI/NotificationManager.hpp" #include "slic3r/GUI/MsgDialog.hpp" #include "libslic3r/PresetBundle.hpp" #include "libslic3r/SLAPrint.hpp" +#include "libslic3r/SLA/SupportIslands/SampleConfigFactory.hpp" +#include "imgui/imgui_stdlib.h" // string input for ImGui static const double CONE_RADIUS = 0.25; static const double CONE_HEIGHT = 0.75; -namespace Slic3r { -namespace GUI { +using namespace Slic3r; +using namespace Slic3r::GUI; + +namespace { + +enum class IconType : unsigned { + show_support_points_selected, + show_support_points_unselected, + show_support_points_hovered, + show_support_structure_selected, + show_support_structure_unselected, + show_support_structure_hovered, + // automatic calc of icon's count + _count +}; + +IconManager::Icons init_icons(IconManager &mng, ImVec2 size = ImVec2{50, 50}) { + mng.release(); + + // icon order has to match the enum IconType + IconManager::InitTypes init_types { + {"support_structure_invisible.svg", size, IconManager::RasterType::color}, // show_support_points_selected + {"support_structure_invisible.svg", size, IconManager::RasterType::gray_only_data}, // show_support_points_unselected + {"support_structure_invisible.svg", size, IconManager::RasterType::color}, // show_support_points_hovered + + {"support_structure.svg", size, IconManager::RasterType::color}, // show_support_structure_selected + {"support_structure.svg", size, IconManager::RasterType::gray_only_data}, // show_support_structure_unselected + {"support_structure.svg", size, IconManager::RasterType::color}, // show_support_structure_hovered + }; + + assert(init_types.size() == static_cast(IconType::_count)); + std::string path = resources_dir() + "/icons/"; + for (IconManager::InitType &init_type : init_types) + init_type.filepath = path + init_type.filepath; + + return mng.init(init_types); +} +const IconManager::Icon &get_icon(const IconManager::Icons &icons, IconType type) { + return *icons[static_cast(type)]; } + +/// +/// Draw icon buttons to swap between show structure and only supports points +/// +/// In|Out view mode +/// all loaded icons +/// True when change is made +bool draw_view_mode(bool &show_support_structure, const IconManager::Icons &icons) { + ImGui::PushStyleVar(ImGuiStyleVar_ChildBorderSize, 8.); + ScopeGuard sg([] { ImGui::PopStyleVar(); }); + if (show_support_structure) { + draw(get_icon(icons, IconType::show_support_structure_selected)); + if(ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", _u8L("Visible support structure").c_str()); + ImGui::SameLine(); + if (clickable(get_icon(icons, IconType::show_support_points_unselected), + get_icon(icons, IconType::show_support_points_hovered))) { + show_support_structure = false; + return true; + } else if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", _u8L("Click to show support points without support structure").c_str()); + } else { // !show_support_structure + if (clickable(get_icon(icons, IconType::show_support_structure_unselected), + get_icon(icons, IconType::show_support_structure_hovered))) { + show_support_structure = true; + return true; + } else if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", _u8L("Click to show support structure with pad").c_str()); + ImGui::SameLine(); + draw(get_icon(icons, IconType::show_support_points_selected)); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", _u8L("Visible support points without support structure").c_str()); + } + return false; +} +} // namespace + GLGizmoSlaSupports::GLGizmoSlaSupports(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id) -: GLGizmoSlaBase(parent, icon_filename, sprite_id, slaposDrillHoles) -{ - show_sla_supports(true); + : GLGizmoSlaBase(parent, icon_filename, sprite_id, slaposDrillHoles /*slaposSupportPoints*/) { + show_sla_supports(false); } bool GLGizmoSlaSupports::on_init() @@ -44,8 +120,7 @@ bool GLGizmoSlaSupports::on_init() m_desc["remove_all"] = _u8L("Remove all points"); m_desc["apply_changes"] = _u8L("Apply changes"); m_desc["discard_changes"] = _u8L("Discard changes"); - m_desc["minimal_distance"] = _u8L("Minimal points distance") + ": "; - m_desc["points_density"] = _u8L("Support points density") + ": "; + m_desc["points_density"] = _u8L("Support points density"); m_desc["auto_generate"] = _u8L("Auto-generate points"); m_desc["manual_editing"] = _u8L("Manual editing"); m_desc["clipping_of_view"] = _u8L("Clipping of view")+ ": "; @@ -141,8 +216,6 @@ void GLGizmoSlaSupports::on_render() glsafe(::glEnable(GL_BLEND)); glsafe(::glEnable(GL_DEPTH_TEST)); - show_sla_supports(!m_editing_mode); - render_volumes(); render_points(selection); @@ -195,10 +268,15 @@ void GLGizmoSlaSupports::render_points(const Selection& selection) const Transform3d& view_matrix = camera.get_view_matrix(); shader->set_uniform("projection_matrix", camera.get_projection_matrix()); + const ColorRGBA selected_color = ColorRGBA::REDISH(); + const ColorRGBA hovered_color = ColorRGBA::CYAN(); + const ColorRGBA island_color = ColorRGBA::BLUEISH(); + const ColorRGBA inactive_color = ColorRGBA::LIGHT_GRAY(); + const ColorRGBA manual_color = ColorRGBA::ORANGE(); + ColorRGBA render_color; for (size_t i = 0; i < cache_size; ++i) { const sla::SupportPoint& support_point = m_editing_mode ? m_editing_cache[i].support_point : m_normal_cache[i]; - const bool point_selected = m_editing_mode ? m_editing_cache[i].selected : false; const bool clipped = is_mesh_point_clipped(support_point.pos.cast()); if (i < m_point_raycasters.size()) { @@ -208,22 +286,16 @@ void GLGizmoSlaSupports::render_points(const Selection& selection) if (clipped) continue; + render_color = + support_point.type == sla::SupportPointType::manual_add ? manual_color : + support_point.type == sla::SupportPointType::island ? island_color : + inactive_color; // First decide about the color of the point. - if (size_t(m_hover_id) == i && m_editing_mode) // ignore hover state unless editing mode is active - render_color = { 0.f, 1.f, 1.f, 1.f }; - else { // neigher hover nor picking - bool supports_new_island = m_lock_unique_islands && support_point.is_new_island; - if (m_editing_mode) { - if (point_selected) - render_color = { 1.f, 0.3f, 0.3f, 1.f}; - else - if (supports_new_island) - render_color = { 0.3f, 0.3f, 1.f, 1.f }; - else - render_color = { 0.7f, 0.7f, 0.7f, 1.f }; - } - else - render_color = { 0.5f, 0.5f, 0.5f, 1.f }; + if (m_editing_mode) { + if (size_t(m_hover_id) == i) // ignore hover state unless editing mode is active + render_color = hovered_color; + else if (m_editing_cache[i].selected) + render_color = selected_color; } m_cone.model.set_color(render_color); @@ -324,7 +396,7 @@ bool GLGizmoSlaSupports::gizmo_event(SLAGizmoEventType action, const Vec2d& mous std::pair pos_and_normal; if (unproject_on_mesh(mouse_position, pos_and_normal)) { // we got an intersection Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Add support point")); - m_editing_cache.emplace_back(sla::SupportPoint(pos_and_normal.first, m_new_point_head_diameter/2.f, false), false, pos_and_normal.second); + m_editing_cache.emplace_back(sla::SupportPoint{pos_and_normal.first, m_new_point_head_diameter/2.f}, false, pos_and_normal.second); m_parent.set_as_dirty(); m_wait_for_up_event = true; unregister_point_raycasters_for_picking(); @@ -479,7 +551,7 @@ void GLGizmoSlaSupports::delete_selected_points(bool force) Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Delete support point")); for (unsigned int idx=0; idx GLGizmoSlaSupports::get_config_options(const st return out; } - - -/* -void GLGizmoSlaSupports::find_intersecting_facets(const igl::AABB* aabb, const Vec3f& normal, double offset, std::vector& idxs) const -{ - if (aabb->is_leaf()) { // this is a facet - // corner.dot(normal) - offset - idxs.push_back(aabb->m_primitive); - } - else { // not a leaf - using CornerType = Eigen::AlignedBox::CornerType; - bool sign = std::signbit(offset - normal.dot(aabb->m_box.corner(CornerType(0)))); - for (unsigned int i=1; i<8; ++i) - if (std::signbit(offset - normal.dot(aabb->m_box.corner(CornerType(i)))) != sign) { - find_intersecting_facets(aabb->m_left, normal, offset, idxs); - find_intersecting_facets(aabb->m_right, normal, offset, idxs); - } - } -} - - - -void GLGizmoSlaSupports::make_line_segments() const -{ - TriangleMeshSlicer tms(&m_c->m_model_object->volumes.front()->mesh); - Vec3f normal(0.f, 1.f, 1.f); - double d = 0.; - - std::vector lines; - find_intersections(&m_AABB, normal, d, lines); - ExPolygons expolys; - tms.make_expolygons_simple(lines, &expolys); - - SVG svg("slice_loops.svg", get_extents(expolys)); - svg.draw(expolys); - //for (const IntersectionLine &l : lines[i]) - // svg.draw(l, "red", 0); - //svg.draw_outline(expolygons, "black", "blue", 0); - svg.Close(); -} -*/ - - void GLGizmoSlaSupports::on_render_input_window(float x, float y, float bottom_limit) { + // Keep resolution of icons for + static float rendered_line_height; + if (float line_height = ImGui::GetTextLineHeightWithSpacing(); + m_icons.empty() || + rendered_line_height != line_height) { // change of view resolution + rendered_line_height = line_height; + + // need regeneration when change resolution(move between monitors) + float width = std::round(line_height / 8 + 1) * 8; + ImVec2 icon_size{width, width}; + m_icons = init_icons(m_icon_manager, icon_size); + } + static float last_y = 0.0f; static float last_h = 0.0f; @@ -595,7 +637,7 @@ RENDER_AGAIN: // First calculate width of all the texts that are could possibly be shown. We will decide set the dialog width based on that: - const float settings_sliders_left = std::max(ImGuiPureWrap::calc_text_size(m_desc.at("minimal_distance")).x, ImGuiPureWrap::calc_text_size(m_desc.at("points_density")).x) + m_imgui->scaled(1.f); + const float settings_sliders_left = ImGuiPureWrap::calc_text_size(m_desc.at("points_density")).x + m_imgui->scaled(1.f); const float clipping_slider_left = std::max(ImGuiPureWrap::calc_text_size(m_desc.at("clipping_of_view")).x, ImGuiPureWrap::calc_text_size(m_desc.at("reset_direction")).x) + m_imgui->scaled(1.5f); const float diameter_slider_left = ImGuiPureWrap::calc_text_size(m_desc.at("head_diameter")).x + m_imgui->scaled(1.f); const float minimal_slider_width = m_imgui->scaled(4.f); @@ -677,51 +719,91 @@ RENDER_AGAIN: } else { // not in editing mode: m_imgui->disabled_begin(!is_input_enabled()); - - ImGui::AlignTextToFramePadding(); - ImGuiPureWrap::text(m_desc.at("minimal_distance")); - ImGui::SameLine(settings_sliders_left); - ImGui::PushItemWidth(window_width - settings_sliders_left); - - std::vector opts = get_config_options({"support_points_density_relative", "support_points_minimal_distance"}); - float density = static_cast(opts[0])->value; - float minimal_point_distance = static_cast(opts[1])->value; - - m_imgui->slider_float("##minimal_point_distance", &minimal_point_distance, 0.f, 20.f, "%.f mm"); - bool slider_clicked = m_imgui->get_last_slider_status().clicked; // someone clicked the slider - bool slider_edited = m_imgui->get_last_slider_status().edited; // someone is dragging the slider - bool slider_released = m_imgui->get_last_slider_status().deactivated_after_edit; // someone has just released the slider - - ImGui::AlignTextToFramePadding(); ImGuiPureWrap::text(m_desc.at("points_density")); - ImGui::SameLine(settings_sliders_left); + ImGui::SameLine(); - m_imgui->slider_float("##points_density", &density, 0.f, 200.f, "%.f %%"); - slider_clicked |= m_imgui->get_last_slider_status().clicked; - slider_edited |= m_imgui->get_last_slider_status().edited; - slider_released |= m_imgui->get_last_slider_status().deactivated_after_edit; + if (draw_view_mode(m_show_support_structure, m_icons)){ + show_sla_supports(m_show_support_structure); + if (m_show_support_structure) { + if (m_normal_cache.empty()) { + // first click also have to generate point + auto_generate(); + } else { + reslice_until_step(slaposPad); + } + } + } - if (slider_clicked) { // stash the values of the settings so we know what to revert to after undo - m_minimal_point_distance_stash = minimal_point_distance; - m_density_stash = density; + const char *support_points_density = "support_points_density_relative"; + float density = static_cast(get_config_options({support_points_density})[0])->value; + float old_density = density; + wxString tooltip = _L("Change amount of generated support points."); + if (m_imgui->slider_float("##density", &density, 50.f, 200.f, "%.f %%", 1.f, false, tooltip)){ + if (density < 10.f) // not neccessary, but lower value seems pointless. Zero cause issues inside algorithms. + density = 10.f; + mo->config.set(support_points_density, (int) density); } - if (slider_edited) { - mo->config.set("support_points_minimal_distance", minimal_point_distance); - mo->config.set("support_points_density_relative", (int)density); - } - if (slider_released) { - mo->config.set("support_points_minimal_distance", m_minimal_point_distance_stash); - mo->config.set("support_points_density_relative", (int)m_density_stash); + + const ImGuiWrapper::LastSliderStatus &density_status = m_imgui->get_last_slider_status(); + static std::optional density_stash; // Value for undo/redo stack is written on stop dragging + if (!density_stash.has_value() && !is_approx(density, old_density)) // stash the values of the settings so we know what to revert to after undo + density_stash = (int)old_density; + if (density_status.deactivated_after_edit && density_stash.has_value()) { // slider released + // set configuration to value before slide + // to store this value on undo redo snapshot stack + mo->config.set(support_points_density, *density_stash); + density_stash.reset(); Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Support parameter change")); - mo->config.set("support_points_minimal_distance", minimal_point_distance); - mo->config.set("support_points_density_relative", (int)density); + mo->config.set(support_points_density, (int) density); wxGetApp().obj_list()->update_and_show_object_settings_item(); - } - - bool generate = ImGuiPureWrap::button(m_desc.at("auto_generate")); - - if (generate) auto_generate(); + } + + const sla::SupportPoints &supports = m_normal_cache; + int count_user_edited = 0; + int count_island = 0; + for (const sla::SupportPoint &support : supports) + switch (support.type) { + case sla::SupportPointType::manual_add: ++count_user_edited; break; + case sla::SupportPointType::island: ++count_island; break; + //case sla::SupportPointType::slope: + default: assert(support.type == sla::SupportPointType::slope); } + + std::string stats; + if (supports.empty()) { + stats = "No support points generated yet."; + } else if (count_user_edited == 0) { + stats = GUI::format("%d support points generated (%d on islands)", + (int) supports.size(), count_island); + } else { + stats = GUI::format("%d(%d manual) support points (%d on islands)", + (int) supports.size(), count_user_edited, count_island); + } + ImVec4 light_gray{0.4f, 0.4f, 0.4f, 1.0f}; + ImGui::TextColored(light_gray, "%s", stats.c_str()); + +#ifdef USE_ISLAND_GUI_FOR_SETTINGS + ImGui::Separator(); + ImGui::Text("Between delimiters is temporary GUI"); + sla::SampleConfig &sample_config = sla::SampleConfigFactory::get_sample_config(); + if (float overhang_sample_distance = sample_config.prepare_config.discretize_overhang_step; + m_imgui->slider_float("overhang discretization", &overhang_sample_distance, 2e-5f, 10.f, "%.2f mm")){ + sample_config.prepare_config.discretize_overhang_step = overhang_sample_distance; + } else if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Smaller will slow down. Step for discretization overhang outline for test of support need"); + + draw_island_config(); + ImGui::Text("Distribution depends on './resources/data/sla_support.svg'\ninstruction for edit are in file"); + ImGui::Separator(); +#endif // USE_ISLAND_GUI_FOR_SETTINGS + + if (ImGuiPureWrap::button(m_desc.at("auto_generate"))) + auto_generate(); + ImGui::SameLine(); + + m_imgui->disabled_begin(!is_input_enabled() || m_normal_cache.empty()); + remove_all = ImGuiPureWrap::button(m_desc.at("remove_all")); + m_imgui->disabled_end(); ImGui::Separator(); if (ImGuiPureWrap::button(m_desc.at("manual_editing"))) @@ -729,10 +811,6 @@ RENDER_AGAIN: m_imgui->disabled_end(); - m_imgui->disabled_begin(!is_input_enabled() || m_normal_cache.empty()); - remove_all = ImGuiPureWrap::button(m_desc.at("remove_all")); - m_imgui->disabled_end(); - // ImGuiPureWrap::text(""); // ImGuiPureWrap::text(m_c->m_model_object->sla_points_status == sla::PointsStatus::NoPoints ? _(L("No points (will be autogenerated)")) : // (m_c->m_model_object->sla_points_status == sla::PointsStatus::AutoGenerated ? _(L("Autogenerated points (no modifications)")) : @@ -798,6 +876,139 @@ RENDER_AGAIN: m_parent.set_as_dirty(); } +#ifdef USE_ISLAND_GUI_FOR_SETTINGS +void GLGizmoSlaSupports::draw_island_config() { + if (!ImGui::TreeNode("Support islands:")) + return; // no need to draw configuration for islands + sla::SampleConfig &sample_config = sla::SampleConfigFactory::get_sample_config(); + + ImGui::SameLine(); + ImGui::Text("head radius %.2f mm", unscale(sample_config.head_radius)); + + bool exist_change = false; + + if (float max_for_one = unscale(sample_config.max_length_for_one_support_point); // [in mm] + ImGui::InputFloat("One support", &max_for_one, .1f, 1.f, "%.2f mm")) { + sample_config.max_length_for_one_support_point = scale_(max_for_one); + exist_change = true; + } else if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Maximal island length (longest voronoi path)\n" + "for support island by exactly one point.\n" + "Point will be on the longest path center"); + + if (float max_for_two = unscale(sample_config.max_length_for_two_support_points); // [in mm] + ImGui::InputFloat("Two supports", &max_for_two, .1f, 1.f, "%.2f mm")) { + sample_config.max_length_for_two_support_points = scale_(max_for_two); + exist_change = true; + } else if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Maximal island length (longest voronoi path)\n" + "for support by 2 points on path sides\n" + "To stretch the island."); + if (float thin_max_width = unscale(sample_config.thin_max_width); // [in mm] + ImGui::InputFloat("Thin max width", &thin_max_width, .1f, 1.f, "%.2f mm")) { + sample_config.thin_max_width = scale_(thin_max_width); + exist_change = true; + } else if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Maximal width of line island supported in the middle of line\n" + "Must be greater than thick min width(to make hysteresis)"); + if (float thick_min_width = unscale(sample_config.thick_min_width); // [in mm] + ImGui::InputFloat("Thick min width", &thick_min_width, .1f, 1.f, "%.2f mm")) { + sample_config.thick_min_width = scale_(thick_min_width); + exist_change = true; + } else if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Minimal width to be supported by outline\n" + "Must be smaller than thin max width(to make hysteresis)"); + if (float max_distance = unscale(sample_config.thin_max_distance); // [in mm] + ImGui::InputFloat("Thin max distance", &max_distance, .1f, 1.f, "%.2f mm")) { + sample_config.thin_max_distance = scale_(max_distance); + exist_change = true; + } else if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Maximal distance of supports on thin island's part"); + if (float max_distance = unscale(sample_config.thick_inner_max_distance); // [in mm] + ImGui::InputFloat("Thick inner max distance", &max_distance, .1f, 1.f, "%.2f mm")) { + sample_config.thick_inner_max_distance = scale_(max_distance); + exist_change = true; + } else if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Maximal distance of supports inside thick island's part"); + if (float max_distance = unscale(sample_config.thick_outline_max_distance); // [in mm] + ImGui::InputFloat("Thick outline max distance", &max_distance, .1f, 1.f, "%.2f mm")) { + sample_config.thick_outline_max_distance = scale_(max_distance); + exist_change = true; + } else if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Maximal distance of supports on thick island's part outline"); + + if (float minimal_distance_from_outline = unscale(sample_config.minimal_distance_from_outline); // [in mm] + ImGui::InputFloat("From outline", &minimal_distance_from_outline, .1f, 1.f, "%.2f mm")) { + sample_config.minimal_distance_from_outline = scale_(minimal_distance_from_outline); + exist_change = true; + } else if (ImGui::IsItemHovered()) + ImGui::SetTooltip("When it is possible, there will be this minimal distance from outline.\n" + "ZERO mean head center will lay on island outline\n" + "IMHO value should be bigger than head radius"); + ImGui::SameLine(); + if (float maximal_distance_from_outline = unscale(sample_config.maximal_distance_from_outline); // [in mm] + ImGui::InputFloat("Max", &maximal_distance_from_outline, .1f, 1.f, "%.2f mm")) { + sample_config.maximal_distance_from_outline = scale_(maximal_distance_from_outline); + exist_change = true; + } else if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Measured as sum of VD edge length from outline\n" + "Used only when there is no space for outline offset on first/last point\n" + "Must be bigger than value 'From outline'"); + + if (float simplification_tolerance = unscale(sample_config.simplification_tolerance); // [in mm] + ImGui::InputFloat("Simplify", &simplification_tolerance, .1f, 1.f, "%.2f mm")) { + sample_config.simplification_tolerance = scale_(simplification_tolerance); + exist_change = true; + } else if (ImGui::IsItemHovered()) + ImGui::SetTooltip("There is no need to calculate with precisse island Voronoi\n" + "NOTE: Slice of Cylinder bottom has tip of trinagles on contour\n" + "(neighbor coordinate -> create issue in boost::voronoi)\n" + "Bigger value will speed up"); + ImGui::Text("Aligning termination criteria:"); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("After initial support placement on island, supports are aligned\n" + "to more uniformly support area of irregular island shape"); + if (int count = static_cast(sample_config.count_iteration); + ImGui::SliderInt("max iteration", &count, 0, 100, "%d loops" )){ + sample_config.count_iteration = count; + exist_change = true; + } else if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Align termination condition, max count of aligning calls"); + if (float minimal_move = unscale(sample_config.minimal_move); // [in mm] + ImGui::InputFloat("minimal move", &minimal_move, .1f, 1.f, "%.2f mm")) { + sample_config.minimal_move = scale_(minimal_move); + exist_change = true; + } else if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Align termination condition, when support points after align did not change their position more,\n" + "than this distance it is deduce that supports are aligned enough.\n" + "Bigger value mean speed up of aligning"); + + if (exist_change){ + sla::SampleConfigFactory::verify(sample_config); + } + + +#ifdef OPTION_TO_STORE_ISLAND + bool store_islands = !sample_config.path.empty(); + if (ImGui::Checkbox("StoreIslands", &store_islands)) { + if (store_islands == true) + sample_config.path = "C:/data/temp/island<>.svg"; + else + sample_config.path.clear(); + } else if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Store islands into files\n<> is replaced by island order number"); + if (store_islands) { + ImGui::SameLine(); + std::string path; + ImGui::InputText("path", &sample_config.path); + } +#endif // OPTION_TO_STORE_ISLAND + + // end of tree node + ImGui::TreePop(); +} +#endif // USE_ISLAND_GUI_FOR_SETTINGS + bool GLGizmoSlaSupports::on_is_activable() const { const Selection& selection = m_parent.get_selection(); @@ -914,7 +1125,7 @@ void GLGizmoSlaSupports::on_dragging(const UpdateData &data) { assert(m_hover_id != -1); if (!m_editing_mode) return; - if (m_editing_cache[m_hover_id].support_point.is_new_island && m_lock_unique_islands) + if (m_editing_cache[m_hover_id].support_point.is_island() && m_lock_unique_islands) return; std::pair pos_and_normal; @@ -922,7 +1133,7 @@ void GLGizmoSlaSupports::on_dragging(const UpdateData &data) return; m_editing_cache[m_hover_id].support_point.pos = pos_and_normal.first; - m_editing_cache[m_hover_id].support_point.is_new_island = false; + m_editing_cache[m_hover_id].support_point.type = sla::SupportPointType::manual_add; m_editing_cache[m_hover_id].normal = pos_and_normal.second; } @@ -1021,7 +1232,7 @@ void GLGizmoSlaSupports::editing_mode_apply_changes() mo->sla_support_points.clear(); mo->sla_support_points = m_normal_cache; - reslice_until_step(slaposPad); + reslice_until_step(m_show_support_structure ? slaposPad : slaposSupportPoints); } } @@ -1121,10 +1332,10 @@ void GLGizmoSlaSupports::get_data_from_backend() for (const SLAPrintObject* po : m_parent.sla_print()->objects()) { if (po->model_object()->id() == mo->id()) { m_normal_cache.clear(); - const std::vector& points = po->get_support_points(); - auto mat = po->trafo().inverse().cast(); - for (unsigned int i=0; itrafo().inverse().cast(); // TODO: WTF trafo????? !!!!!! + for (const sla::SupportPoint &p : po->get_support_points()) + m_normal_cache.emplace_back(sla::SupportPoint{mat * p.pos, p.head_front_radius, p.type}); mo->sla_points_status = sla::PointsStatus::AutoGenerated; break; @@ -1138,27 +1349,18 @@ void GLGizmoSlaSupports::get_data_from_backend() void GLGizmoSlaSupports::auto_generate() { - //wxMessageDialog dlg(GUI::wxGetApp().plater(), - MessageDialog dlg(GUI::wxGetApp().plater(), - _L("Autogeneration will erase all manually edited points.") + "\n\n" + - _L("Are you sure you want to do it?") + "\n", - _L("Warning"), wxICON_WARNING | wxYES | wxNO); - + Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Autogenerate support points")); + wxGetApp().CallAfter([this]() { reslice_until_step( + m_show_support_structure ? slaposPad : slaposSupportPoints); }); ModelObject* mo = m_c->selection_info()->model_object(); - - if (mo->sla_points_status != sla::PointsStatus::UserModified || m_normal_cache.empty() || dlg.ShowModal() == wxID_YES) { - Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Autogenerate support points")); - wxGetApp().CallAfter([this]() { reslice_until_step(slaposPad); }); - mo->sla_points_status = sla::PointsStatus::Generating; - } + mo->sla_points_status = sla::PointsStatus::Generating; } - - void GLGizmoSlaSupports::switch_to_editing_mode() { wxGetApp().plater()->enter_gizmos_stack(); m_editing_mode = true; + show_sla_supports(false); m_editing_cache.clear(); for (const sla::SupportPoint& sp : m_normal_cache) m_editing_cache.emplace_back(sp); @@ -1172,6 +1374,7 @@ void GLGizmoSlaSupports::disable_editing_mode() { if (m_editing_mode) { m_editing_mode = false; + show_sla_supports(m_show_support_structure); wxGetApp().plater()->leave_gizmos_stack(); m_parent.set_as_dirty(); unregister_point_raycasters_for_picking(); @@ -1308,11 +1511,20 @@ SlaGizmoHelpDialog::SlaGizmoHelpDialog() gridsizer->Add(desc, -1, wxALIGN_CENTRE_VERTICAL); } + std::vector> point_types; + point_types.push_back(std::make_pair("sphere_lightgray",_L("Generated point"))); + point_types.push_back(std::make_pair("sphere_redish", _L("Selected support point"))); + point_types.push_back(std::make_pair("sphere_orange", _L("User edited point"))); + point_types.push_back(std::make_pair("sphere_blueish", _L("Island support point"))); + point_types.push_back(std::make_pair("sphere_cyan", _L("Mouse hovered point"))); + for (const auto &[icon_name, description] : point_types) { + auto desc = new wxStaticText(this, wxID_ANY, description); + desc->SetFont(font); + gridsizer->Add(new wxStaticBitmap(this, wxID_ANY, ScalableBitmap(this, icon_name).bmp()), + -1, wxALIGN_CENTRE_VERTICAL); + gridsizer->Add(desc, -1, wxALIGN_CENTRE_VERTICAL); + } + SetSizer(hsizer); hsizer->SetSizeHints(this); } - - - -} // namespace GUI -} // namespace Slic3r diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.hpp b/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.hpp index 13087bf0fe..735b4e9edd 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.hpp @@ -8,6 +8,7 @@ #include "GLGizmoSlaBase.hpp" #include "slic3r/GUI/GLSelectionRectangle.hpp" #include "slic3r/GUI/I18N.hpp" +#include "slic3r/GUI/IconManager.hpp" #include "libslic3r/SLA/SupportPoint.hpp" #include "libslic3r/ObjectID.hpp" @@ -91,17 +92,21 @@ private: void unregister_point_raycasters_for_picking(); void update_point_raycasters_for_picking_transform(); + void draw_island_config(); + + bool m_show_support_structure = false; bool m_lock_unique_islands = false; bool m_editing_mode = false; // Is editing mode active? float m_new_point_head_diameter; // Size of a new point. CacheEntry m_point_before_drag; // undo/redo - so we know what state was edited float m_old_point_head_diameter = 0.; // the same - float m_minimal_point_distance_stash = 0.f; // and again - float m_density_stash = 0.f; // and again mutable std::vector m_editing_cache; // a support point and whether it is currently selected std::vector m_normal_cache; // to restore after discarding changes or undo/redo ObjectID m_old_mo_id; + IconManager m_icon_manager; + IconManager::Icons m_icons; + PickingModel m_sphere; PickingModel m_cone; std::vector, std::shared_ptr>> m_point_raycasters; diff --git a/src/slic3r/GUI/IconManager.cpp b/src/slic3r/GUI/IconManager.cpp index 8343530801..1adf887ad5 100644 --- a/src/slic3r/GUI/IconManager.cpp +++ b/src/slic3r/GUI/IconManager.cpp @@ -288,7 +288,7 @@ std::vector IconManager::init(const std::vector } void IconManager::release() { - BOOST_LOG_TRIVIAL(error) << "Not implemented yet"; + BOOST_LOG_TRIVIAL(warning) << "Not implemented yet"; } void priv::clear(IconManager::Icons &icons) { diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 64a43294a1..54378bb2c9 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -5953,7 +5953,6 @@ void TabSLAPrint::build() optgroup = page->new_optgroup(L("Automatic generation")); optgroup->append_single_option_line("support_points_density_relative"); - optgroup->append_single_option_line("support_points_minimal_distance"); page = add_options_page(L("Pad"), "pad"); optgroup = page->new_optgroup(L("Pad")); diff --git a/tests/data/sla_islands/SPE-2674.svg b/tests/data/sla_islands/SPE-2674.svg new file mode 100644 index 0000000000..c548517c22 --- /dev/null +++ b/tests/data/sla_islands/SPE-2674.svg @@ -0,0 +1,5 @@ + + + diff --git a/tests/data/sla_islands/SPE-2674_2.svg b/tests/data/sla_islands/SPE-2674_2.svg new file mode 100644 index 0000000000..96c36acaae --- /dev/null +++ b/tests/data/sla_islands/SPE-2674_2.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/tests/data/sla_islands/lm_issue.svg b/tests/data/sla_islands/lm_issue.svg new file mode 100644 index 0000000000..1424e64c7c --- /dev/null +++ b/tests/data/sla_islands/lm_issue.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/tests/libslic3r/test_kdtreeindirect.cpp b/tests/libslic3r/test_kdtreeindirect.cpp index 946c241435..c1f83ea631 100644 --- a/tests/libslic3r/test_kdtreeindirect.cpp +++ b/tests/libslic3r/test_kdtreeindirect.cpp @@ -86,6 +86,23 @@ TEST_CASE("Test kdtree query for a Box", "[KDTreeIndirect]") REQUIRE(call_count < pgrid.point_count()); } +TEST_CASE("Test kdtree closests points", "[KDTreeIndirect]") { + Points pts{ + Point{-9000000, 9000000}, + Point{-9000000, -9000000}, + Point{ 9000000, -9000000}, + Point{ 9000000, 9000000}, + Point{25, 25} + }; + auto point_accessor = [&pts](size_t idx, size_t dim) -> coord_t & { + return pts[idx][dim]; + }; + KDTreeIndirect<2, coord_t, decltype(point_accessor)> tree(point_accessor, pts.size()); + + std::array closest = find_closest_points<5>(tree, Point{0, 0}); + CHECK(closest[0] == 4); +} + //TEST_CASE("Test kdtree query for a Sphere", "[KDTreeIndirect]") { // auto vol = BoundingBox3Base{{0.f, 0.f, 0.f}, {10.f, 10.f, 10.f}}; diff --git a/tests/libslic3r/test_point.cpp b/tests/libslic3r/test_point.cpp index 8f453a92b6..0b16a88aa7 100644 --- a/tests/libslic3r/test_point.cpp +++ b/tests/libslic3r/test_point.cpp @@ -31,6 +31,13 @@ TEST_CASE("Distance to line", "[Point]") { CHECK(line.distance_to(Point{50, 50}) == Approx(50)); CHECK(line.perp_distance_to(Point{50, 50}) == Approx(50)); CHECK(line.perp_distance_to(Point{150, 50}) == Approx(50)); + + // possitive values are on the left side WRT line direction + CHECK(line.perp_signed_distance_to(Point{50, 50}) == Approx(50)); + CHECK(line.perp_signed_distance_to(Point{50, -50}) == Approx(-50)); + const Line line2{{0, 0}, {0, 100}}; + CHECK(line2.perp_signed_distance_to(Point{50, 50}) == Approx(-50)); + CHECK(line2.perp_signed_distance_to(Point{-50, 50}) == Approx(50)); } TEST_CASE("Distance to diagonal line", "[Point]") { diff --git a/tests/libslic3r/test_voronoi.cpp b/tests/libslic3r/test_voronoi.cpp index 1caaf7e955..18c0d7b1f7 100644 --- a/tests/libslic3r/test_voronoi.cpp +++ b/tests/libslic3r/test_voronoi.cpp @@ -2283,3 +2283,81 @@ TEST_CASE("Voronoi cell doesn't contain a source point - SPE-2298", "[VoronoiCel REQUIRE(vd.is_valid()); } +// */ + +/* +//#include +TEST_CASE("bad vertex cause overflow of data type precisin when use VD result", "[VoronoiDiagram]") +{ + // Points are almost in line + Points points = { + {-106641371, 61644934}, + {-56376476, 32588892}, + {0, 0} + }; + + //auto perp_distance = sla::LineUtils::perp_distance; + auto perp_distance = [](const Linef &line, Vec2d p) { + Vec2d v = line.b - line.a; // direction + Vec2d va = p - line.a; + return std::abs(cross2(v, va)) / v.norm(); + }; + + using VD = Slic3r::Geometry::VoronoiDiagram; + VD vd; + vd.construct_voronoi(points.begin(), points.end()); + + // edge between source index 0 and 1 has bad vertex + size_t bad_index0 = 0; + size_t bad_index1 = 1; + for (auto &edge : vd.edges()) { + size_t i1 = edge.cell()->source_index(); + size_t i2 = edge.twin()->cell()->source_index(); + if ((i1 == bad_index0 && i2 == bad_index1) || + (i1 == bad_index1 && i2 == bad_index0)) { + Vec2d p1 = points[bad_index0].cast(); + Vec2d p2 = points[bad_index1].cast(); + Vec2d middle = (p1 + p2) / 2; + // direction for edge is perpendicular point connection + Vec2d direction(p2.y() - p1.y(), p1.x() - p2.x()); + const VD::vertex_type *vrtx = (edge.vertex0() == nullptr) ? + edge.vertex1() : + edge.vertex0(); + if (vrtx == nullptr) continue; + Vec2d vertex(vrtx->x(), vrtx->y()); + + double point_distance = (p1 - p2).norm(); + [[maybe_unused]] double half_point_distance = point_distance / 2; + + Linef line_from_middle(middle, middle + direction); // line between source points + double distance_vertex = perp_distance(line_from_middle, vertex); + [[maybe_unused]] double distance_p1 = perp_distance(line_from_middle, p1); + [[maybe_unused]] double distance_p2 = perp_distance(line_from_middle, p2); + + Linef line_from_vertex(vertex, vertex + direction); + [[maybe_unused]] double distance_middle = perp_distance(line_from_vertex, middle); + [[maybe_unused]] double distance_p1_ = perp_distance(line_from_vertex, p1); + [[maybe_unused]] double distance_p2_ = perp_distance(line_from_vertex, p2); + + double maximal_distance = 9e6; + Vec2d vertex_direction = (vertex - middle); + Vec2d vertex_dir_abs(fabs(vertex_direction.x()), + fabs(vertex_direction.y())); + double divider = (vertex_dir_abs.x() > vertex_dir_abs.y()) ? + vertex_dir_abs.x() / maximal_distance : + vertex_dir_abs.y() / maximal_distance; + Vec2d vertex_dir_short = vertex_direction / divider; + Vec2d start_point = middle + vertex_dir_short; + Linef line_short(start_point, start_point + direction); + double distance_short_vertex = perp_distance(line_short, vertex); + double distance_short_middle = perp_distance(line_short, middle); + [[maybe_unused]] double distance_p1_short = perp_distance(line_short, p1); + [[maybe_unused]] double distance_p2_short = perp_distance(line_short, p2); + + CHECK(distance_vertex < 10); + //CHECK(distance_middle < 10); // This is bad + CHECK(distance_short_vertex < 10); + CHECK(distance_short_middle < 10); + } + } +}*/ diff --git a/tests/sla_print/CMakeLists.txt b/tests/sla_print/CMakeLists.txt index b244e76ac8..e599c66c8c 100644 --- a/tests/sla_print/CMakeLists.txt +++ b/tests/sla_print/CMakeLists.txt @@ -4,6 +4,10 @@ add_executable(${_TEST_NAME}_tests ${_TEST_NAME}_tests_main.cpp sla_test_utils.hpp sla_test_utils.cpp sla_supptgen_tests.cpp sla_raycast_tests.cpp + sla_parabola_tests.cpp + sla_voronoi_graph_tests.cpp + sla_vectorUtils_tests.cpp + sla_lineUtils_tests.cpp sla_supptreeutils_tests.cpp sla_archive_readwrite_tests.cpp sla_zcorrection_tests.cpp) @@ -18,3 +22,11 @@ endif() # catch_discover_tests(${_TEST_NAME}_tests TEST_PREFIX "${_TEST_NAME}: ") add_test(${_TEST_NAME}_tests ${_TEST_NAME}_tests ${CATCH_EXTRA_ARGS}) + +if (WIN32) + # Adds a post-build copy of libgmp-10.dll + add_custom_command(TARGET ${_TEST_NAME}_tests POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${CMAKE_PREFIX_PATH}/bin/libgmp-10.dll" + $) +endif() \ No newline at end of file diff --git a/tests/sla_print/sla_lineUtils_tests.cpp b/tests/sla_print/sla_lineUtils_tests.cpp new file mode 100644 index 0000000000..10616de2d2 --- /dev/null +++ b/tests/sla_print/sla_lineUtils_tests.cpp @@ -0,0 +1,59 @@ +#include +#include + +using namespace Slic3r; +using namespace Slic3r::sla; + +TEST_CASE("Intersection point", "[Utils], [LineUtils]") +{ + Point a1(0, 0); + Point b1(3, 6); + Line l1(a1, b1); + auto intersection = LineUtils::intersection(l1, Line(Point(0, 4), + Point(5, 4))); + CHECK(intersection.has_value()); + Point i_point = intersection->cast(); + CHECK(PointUtils::is_equal(i_point, Point(2, 4))); + + // same line + auto bad_intersection = LineUtils::intersection(l1, l1); + CHECK(!bad_intersection.has_value()); + + // oposit direction + bad_intersection = LineUtils::intersection(l1, Line(b1, a1)); + CHECK(!bad_intersection.has_value()); + + // parallel line + bad_intersection = LineUtils::intersection(l1, Line(a1 + Point(0, 1), + b1 + Point(0, 1))); + CHECK(!bad_intersection.has_value()); + + // out of line segment, but ray has intersection + Line l2(Point(0, 8), Point(6, 8)); + intersection = LineUtils::intersection(l1, l2); + auto intersection2 = LineUtils::intersection(l2, l1); + CHECK(intersection.has_value()); + CHECK(intersection2.has_value()); + i_point = intersection->cast(); + CHECK(PointUtils::is_equal(i_point, Point(4, 8))); + CHECK(PointUtils::is_equal(i_point, intersection2->cast())); + + Line l3(Point(-2, -2), Point(1, -2)); + intersection = LineUtils::intersection(l1, l3); + intersection2 = LineUtils::intersection(l3, l1); + CHECK(intersection.has_value()); + CHECK(intersection2.has_value()); + i_point = intersection->cast(); + CHECK(PointUtils::is_equal(i_point, Point(-1, -2))); + CHECK(PointUtils::is_equal(i_point, intersection2->cast())); +} + +TEST_CASE("Point belongs to line", "[Utils], [LineUtils]") +{ + Line l(Point(10, 10), Point(50, 30)); + CHECK(LineUtils::belongs(l, Point(30, 20))); + CHECK(!LineUtils::belongs(l, Point(30, 30))); + CHECK(LineUtils::belongs(l, Point(30, 30), 10.)); + CHECK(!LineUtils::belongs(l, Point(30, 10))); + CHECK(!LineUtils::belongs(l, Point(70, 40))); +} diff --git a/tests/sla_print/sla_parabola_tests.cpp b/tests/sla_print/sla_parabola_tests.cpp new file mode 100644 index 0000000000..f3ab2ca267 --- /dev/null +++ b/tests/sla_print/sla_parabola_tests.cpp @@ -0,0 +1,50 @@ +#include "sla_test_utils.hpp" + +#include + +using namespace Slic3r; +using namespace Slic3r::sla; + +void parabola_check_length(const ParabolaSegment ¶bola) +{ + auto diffPoint = parabola.to - parabola.from; + double min = sqrt(diffPoint.x() * diffPoint.x() + + diffPoint.y() * diffPoint.y()); + double max = static_cast(diffPoint.x()) + diffPoint.y(); + double len = ParabolaUtils::length(parabola); + double len2 = ParabolaUtils::length_by_sampling(parabola, 1.); + CHECK(fabs(len2 - len) < 1.); + CHECK(len >= min); + CHECK(len <= max); +} + +// after generalization put to ParabolaUtils +double getParabolaY(const Parabola ¶bola, double x) +{ + double f = ParabolaUtils::focal_length(parabola); + Vec2d perp = parabola.directrix.normal().cast(); + // work only for test cases + if (perp.y() > 0.) perp *= -1.; + perp.normalize(); + Vec2d v = parabola.focus.cast() + perp * f; + return 1 / (4 * f) * (x - v.x()) * (x - v.x()) + v.y(); +} + +TEST_CASE("Parabola length", "[SupGen][Voronoi][Parabola]") +{ + using namespace Slic3r::sla; + double scale = 1e6; + // U shape parabola + Parabola parabola_x2(Line({-1. * scale, -.25 * scale}, + {1. * scale, -.25 * scale}), + Point(0. * scale, .25 * scale)); + + double from_x = 1 * scale; + double to_x = 3 * scale; + Point from(from_x, getParabolaY(parabola_x2, from_x)); + Point to(to_x, getParabolaY(parabola_x2, to_x)); + ParabolaSegment parabola_segment(parabola_x2, from, to); + parabola_check_length(parabola_segment); +} + + diff --git a/tests/sla_print/sla_print_tests.cpp b/tests/sla_print/sla_print_tests.cpp index a733e77cc5..49b38452c4 100644 --- a/tests/sla_print/sla_print_tests.cpp +++ b/tests/sla_print/sla_print_tests.cpp @@ -31,52 +31,6 @@ const char *const SUPPORT_TEST_MODELS[] = { } // namespace -TEST_CASE("Support point generator should be deterministic if seeded", - "[SLASupportGeneration], [SLAPointGen]") { - TriangleMesh mesh = load_model("A_upsidedown.obj"); - - AABBMesh emesh{mesh}; - - sla::SupportTreeConfig supportcfg; - sla::SupportPointGenerator::Config autogencfg; - autogencfg.head_diameter = float(2 * supportcfg.head_front_radius_mm); - sla::SupportPointGenerator point_gen{emesh, autogencfg, [] {}, [](int) {}}; - - auto bb = mesh.bounding_box(); - double zmin = bb.min.z(); - double zmax = bb.max.z(); - double gnd = zmin - supportcfg.object_elevation_mm; - auto layer_h = 0.05f; - - auto slicegrid = grid(float(gnd), float(zmax), layer_h); - std::vector slices = slice_mesh_ex(mesh.its, slicegrid, CLOSING_RADIUS); - - point_gen.seed(0); - point_gen.execute(slices, slicegrid); - - auto get_chksum = [](const std::vector &pts){ - int64_t chksum = 0; - for (auto &pt : pts) { - auto p = scaled(pt.pos); - chksum += p.x() + p.y() + p.z(); - } - - return chksum; - }; - - int64_t checksum = get_chksum(point_gen.output()); - size_t ptnum = point_gen.output().size(); - REQUIRE(point_gen.output().size() > 0); - - for (int i = 0; i < 20; ++i) { - point_gen.output().clear(); - point_gen.seed(0); - point_gen.execute(slices, slicegrid); - REQUIRE(point_gen.output().size() == ptnum); - REQUIRE(checksum == get_chksum(point_gen.output())); - } -} - TEST_CASE("Flat pad geometry is valid", "[SLASupportGeneration]") { sla::PadConfig padcfg; diff --git a/tests/sla_print/sla_supptgen_tests.cpp b/tests/sla_print/sla_supptgen_tests.cpp index 226c9cedad..962a71a177 100644 --- a/tests/sla_print/sla_supptgen_tests.cpp +++ b/tests/sla_print/sla_supptgen_tests.cpp @@ -4,17 +4,29 @@ #include #include #include +#include +#include +#include +#include +#include +#include +#include +#include "nanosvg/nanosvg.h" // load SVG file #include "sla_test_utils.hpp" -namespace Slic3r { namespace sla { +using namespace Slic3r; +using namespace Slic3r::sla; + +//#define STORE_SAMPLE_INTO_SVG_FILES "C:/data/temp/test_islands/sample_" +//#define STORE_ISLAND_ISSUES "C:/data/temp/issues/" TEST_CASE("Overhanging point should be supported", "[SupGen]") { // Pyramid with 45 deg slope TriangleMesh mesh = make_pyramid(10.f, 10.f); mesh.rotate_y(float(PI)); - mesh.WriteOBJFile("Pyramid.obj"); + //mesh.WriteOBJFile("Pyramid.obj"); sla::SupportPoints pts = calc_support_pts(mesh); @@ -54,18 +66,11 @@ double min_point_distance(const sla::SupportPoints &pts) TEST_CASE("Overhanging horizontal surface should be supported", "[SupGen]") { double width = 10., depth = 10., height = 1.; - TriangleMesh mesh = make_cube(width, depth, height); + TriangleMesh mesh = make_cube(width, depth, height); mesh.translate(0., 0., 5.); // lift up - mesh.WriteOBJFile("Cuboid.obj"); - - sla::SupportPointGenerator::Config cfg; - sla::SupportPoints pts = calc_support_pts(mesh, cfg); - - double mm2 = width * depth; - + // mesh.WriteOBJFile("Cuboid.obj"); + sla::SupportPoints pts = calc_support_pts(mesh); REQUIRE(!pts.empty()); - REQUIRE(pts.size() * cfg.support_force() > mm2 * cfg.tear_pressure()); - REQUIRE(min_point_distance(pts) >= cfg.minimal_distance); } template auto&& center_around_bb(M &&mesh) @@ -84,8 +89,7 @@ TEST_CASE("Overhanging edge should be supported", "[SupGen]") { mesh.translate(0., 0., height); mesh.WriteOBJFile("Prism.obj"); - sla::SupportPointGenerator::Config cfg; - sla::SupportPoints pts = calc_support_pts(mesh, cfg); + sla::SupportPoints pts = calc_support_pts(mesh); Linef3 overh{ {0.f, -depth / 2.f, 0.f}, {0.f, depth / 2.f, 0.f}}; @@ -97,9 +101,8 @@ TEST_CASE("Overhanging edge should be supported", "[SupGen]") { return line_alg::distance_to(overh, Vec3d{pt.pos.cast()}) < 1.; }); - REQUIRE(overh_pts.size() * cfg.support_force() > overh.length() * cfg.tear_pressure()); - double ddiff = min_point_distance(pts) - cfg.minimal_distance; - REQUIRE(ddiff > - 0.1 * cfg.minimal_distance); + //double ddiff = min_point_distance(pts) - cfg.minimal_distance; + //REQUIRE(ddiff > - 0.1 * cfg.minimal_distance); } TEST_CASE("Hollowed cube should be supported from the inside", "[SupGen][Hollowed]") { @@ -114,9 +117,8 @@ TEST_CASE("Hollowed cube should be supported from the inside", "[SupGen][Hollowe Vec3f mv = bb.center().cast() - Vec3f{0.f, 0.f, 0.5f * h}; mesh.translate(-mv); - sla::SupportPointGenerator::Config cfg; - sla::SupportPoints pts = calc_support_pts(mesh, cfg); - sla::remove_bottom_points(pts, mesh.bounding_box().min.z() + EPSILON); + sla::SupportPoints pts = calc_support_pts(mesh); + //sla::remove_bottom_points(pts, mesh.bounding_box().min.z() + EPSILON); REQUIRE(!pts.empty()); } @@ -132,11 +134,471 @@ TEST_CASE("Two parallel plates should be supported", "[SupGen][Hollowed]") mesh.WriteOBJFile("parallel_plates.obj"); - sla::SupportPointGenerator::Config cfg; - sla::SupportPoints pts = calc_support_pts(mesh, cfg); - sla::remove_bottom_points(pts, mesh.bounding_box().min.z() + EPSILON); + sla::SupportPoints pts = calc_support_pts(mesh); + //sla::remove_bottom_points(pts, mesh.bounding_box().min.z() + EPSILON); REQUIRE(!pts.empty()); } -}} // namespace Slic3r::sla + +Slic3r::Polygon create_cross_roads(double size, double width) +{ + auto r1 = PolygonUtils::create_rect(5.3 * size, width); + r1.rotate(3.14/4); + r1.translate(2 * size, width / 2); + auto r2 = PolygonUtils::create_rect(6.1 * size, 3 / 4. * width); + r2.rotate(-3.14 / 5); + r2.translate(3 * size, width / 2); + auto r3 = PolygonUtils::create_rect(7.9 * size, 4 / 5. * width); + r3.translate(2*size, width/2); + auto r4 = PolygonUtils::create_rect(5 / 6. * width, 5.7 * size); + r4.translate(-size,3*size); + Polygons rr = union_(Polygons({r1, r2, r3, r4})); + return rr.front(); +} + +ExPolygon create_trinagle_with_hole(double size) +{ + auto hole = PolygonUtils::create_equilateral_triangle(size / 3); + hole.reverse(); + hole.rotate(3.14 / 4); + return ExPolygon(PolygonUtils::create_equilateral_triangle(size), hole); +} + +ExPolygon create_square_with_hole(double size, double hole_size) +{ + assert(sqrt(hole_size *hole_size / 2) < size); + auto hole = PolygonUtils::create_square(hole_size); + hole.rotate(M_PI / 4.); // 45 + hole.reverse(); + return ExPolygon(PolygonUtils::create_square(size), hole); +} + +ExPolygon create_square_with_4holes(double size, double hole_size) { + auto hole = PolygonUtils::create_square(hole_size); + hole.reverse(); + double size_4 = size / 4; + auto h1 = hole; + h1.translate(size_4, size_4); + auto h2 = hole; + h2.translate(-size_4, size_4); + auto h3 = hole; + h3.translate(size_4, -size_4); + auto h4 = hole; + h4.translate(-size_4, -size_4); + ExPolygon result(PolygonUtils::create_square(size)); + result.holes = Polygons({h1, h2, h3, h4}); + return result; +} + +// boudary of circle +ExPolygon create_disc(double radius, double width, size_t count_line_segments) +{ + double width_2 = width / 2; + auto hole = PolygonUtils::create_circle(radius - width_2, + count_line_segments); + hole.reverse(); + return ExPolygon(PolygonUtils::create_circle(radius + width_2, + count_line_segments), + hole); +} + +Slic3r::Polygon create_V_shape(double height, double line_width, double angle = M_PI/4) { + double angle_2 = angle / 2; + auto left_side = PolygonUtils::create_rect(line_width, height); + auto right_side = left_side; + right_side.rotate(-angle_2); + double small_move = cos(angle_2) * line_width / 2; + double side_move = sin(angle_2) * height / 2 + small_move; + right_side.translate(side_move,0); + left_side.rotate(angle_2); + left_side.translate(-side_move, 0); + auto bottom = PolygonUtils::create_rect(4 * small_move, line_width); + bottom.translate(0., -cos(angle_2) * height / 2 + line_width/2); + Polygons polygons = union_(Polygons({left_side, right_side, bottom})); + return polygons.front(); +} + +ExPolygon create_tiny_wide_test_1(double wide, double tiny) +{ + double hole_size = wide; + double width = 2 * wide + hole_size; + double height = wide + hole_size + tiny; + auto outline = PolygonUtils::create_rect(width, height); + auto hole = PolygonUtils::create_rect(hole_size, hole_size); + hole.reverse(); + int hole_move_y = height/2 - (hole_size/2 + tiny); + hole.translate(0, hole_move_y); + + ExPolygon result(outline); + result.holes = {hole}; + return result; +} + +ExPolygon create_tiny_wide_test_2(double wide, double tiny) +{ + double hole_size = wide; + double width = (3 + 1) * wide + 3 * hole_size; + double height = 2*wide + 2*tiny + 3*hole_size; + auto outline = PolygonUtils::create_rect( width, height); + auto hole = PolygonUtils::create_rect(hole_size, hole_size); + hole.reverse(); + auto hole2 = hole;// copy + auto hole3 = hole; // copy + auto hole4 = hole; // copy + + int hole_move_x = wide + hole_size; + int hole_move_y = wide + hole_size; + hole.translate(hole_move_x, hole_move_y); + hole2.translate(-hole_move_x, hole_move_y); + hole3.translate(hole_move_x, -hole_move_y); + hole4.translate(-hole_move_x, -hole_move_y); + + auto hole5 = PolygonUtils::create_circle(hole_size / 2, 16); + hole5.reverse(); + auto hole6 = hole5; // copy + hole5.translate(0, hole_move_y); + hole6.translate(0, -hole_move_y); + + auto hole7 = PolygonUtils::create_equilateral_triangle(hole_size); + hole7.reverse(); + auto hole8 = PolygonUtils::create_circle(hole_size/2, 7, Point(hole_move_x,0)); + hole8.reverse(); + auto hole9 = PolygonUtils::create_circle(hole_size/2, 5, Point(-hole_move_x,0)); + hole9.reverse(); + + ExPolygon result(outline); + result.holes = {hole, hole2, hole3, hole4, hole5, hole6, hole7, hole8, hole9}; + return result; +} + +ExPolygon create_tiny_between_holes(double wide, double tiny) +{ + double hole_size = wide; + double width = 2 * wide + 2*hole_size + tiny; + double height = 2 * wide + hole_size; + auto outline = PolygonUtils::create_rect(width, height); + auto holeL = PolygonUtils::create_rect(hole_size, hole_size); + holeL.reverse(); + auto holeR = holeL; + int hole_move_x = (hole_size + tiny)/2; + holeL.translate(-hole_move_x, 0); + holeR.translate(hole_move_x, 0); + + ExPolygon result(outline); + result.holes = {holeL, holeR}; + return result; +} + +// stress test for longest path +// needs reshape +ExPolygon create_mountains(double size) { + return ExPolygon({{0., 0.}, + {size, 0.}, + {5 * size / 6, size}, + {4 * size / 6, size / 6}, + {3 * size / 7, 2 * size}, + {2 * size / 7, size / 6}, + {size / 7, size}}); +} + +/// Neighbor points create trouble for voronoi - test of neccessary offseting(closing) of contour +ExPolygon create_cylinder_bottom_slice() { + indexed_triangle_set its_cylinder = its_make_cylinder(6.6551999999999998, 11.800000000000001); + MeshSlicingParams param; + Polygons polygons = slice_mesh(its_cylinder, 0.0125000002, param); + return ExPolygon{polygons.front()}; +} + +ExPolygon load_frog(){ + TriangleMesh mesh = load_model("frog_legs.obj"); + std::vector slices = slice_mesh_ex(mesh.its, {0.1f}); + return slices.front()[1]; +} + +ExPolygon load_svg(const std::string& svg_filepath) { + struct NSVGimage *image = nsvgParseFromFile(svg_filepath.c_str(), "px", 96); + ScopeGuard sg_image([&image] { nsvgDelete(image); }); + + auto to_polygon = [](NSVGpath *path) { + Polygon r; + r.points.reserve(path->npts); + for (int i = 0; i < path->npts; i++) + r.points.push_back(Point(path->pts[2 * i], path->pts[2 * i + 1])); + return r; + }; + + for (NSVGshape *shape_ptr = image->shapes; shape_ptr != NULL; shape_ptr = shape_ptr->next) { + const NSVGshape &shape = *shape_ptr; + if (!(shape.flags & NSVG_FLAGS_VISIBLE)) continue; // is visible + if (shape.fill.type != NSVG_PAINT_NONE) continue; // is not used fill + if (shape.stroke.type == NSVG_PAINT_NONE) continue; // exist stroke + //if (shape.strokeWidth < 1e-5f) continue; // is visible stroke width + //if (shape.stroke.color != 4278190261) continue; // is red + ExPolygon result; + for (NSVGpath *path = shape.paths; path != NULL; path = path->next) { + // Path order is reverse to path in file + if (path->next == NULL) // last path is contour + result.contour = to_polygon(path); + else + result.holes.push_back(to_polygon(path)); + } + return result; + } + REQUIRE(false); + return {}; +} + +ExPolygons createTestIslands(double size) +{ + std::string dir = std::string(TEST_DATA_DIR PATH_SEPARATOR) + "sla_islands/"; + bool useFrogLeg = false; + // need post reorganization of longest path + ExPolygons result = { + // one support point + ExPolygon(PolygonUtils::create_equilateral_triangle(size)), + ExPolygon(PolygonUtils::create_square(size)), + ExPolygon(PolygonUtils::create_rect(size / 2, size)), + ExPolygon(PolygonUtils::create_isosceles_triangle(size / 2, 3 * size / 2)), // small sharp triangle + ExPolygon(PolygonUtils::create_circle(size / 2, 10)), + create_square_with_4holes(size, size / 4), + create_disc(size/4, size / 4, 10), + ExPolygon(create_V_shape(2*size/3, size / 4)), + + // two support points + ExPolygon(PolygonUtils::create_isosceles_triangle(size / 2, 3 * size)), // small sharp triangle + ExPolygon(PolygonUtils::create_rect(size / 2, 3 * size)), + ExPolygon(create_V_shape(1.5*size, size/3)), + + // tiny line support points + ExPolygon(PolygonUtils::create_rect(size / 2, 10 * size)), // long line + ExPolygon(create_V_shape(size*4, size / 3)), + ExPolygon(create_cross_roads(size, size / 3)), + create_disc(3*size, size / 4, 30), + create_disc(2*size, size, 12), // 3 points + create_square_with_4holes(5 * size, 5 * size / 2 - size / 3), + + // Tiny and wide part together with holes + ExPolygon(PolygonUtils::create_isosceles_triangle(5. * size, 40. * size)), + create_tiny_wide_test_1(3 * size, 2 / 3. * size), + create_tiny_wide_test_2(3 * size, 2 / 3. * size), + create_tiny_between_holes(3 * size, 2 / 3. * size), + + ExPolygon(PolygonUtils::create_equilateral_triangle(scale_(18.6))), + create_cylinder_bottom_slice(), + load_svg(dir + "lm_issue.svg"), // change from thick to thin and vice versa on circle + load_svg(dir + "SPE-2674.svg"), // center of longest path lay inside of the VD node + load_svg(dir + "SPE-2674_2.svg"), // missing Voronoi vertex even after the rotation of input. + + // still problem + // three support points + ExPolygon(PolygonUtils::create_equilateral_triangle(3 * size)), + ExPolygon(PolygonUtils::create_circle(size, 20)), + + create_mountains(size), + create_trinagle_with_hole(size), + create_square_with_hole(size, size / 2), + create_square_with_hole(size, size / 3) + }; + if (useFrogLeg) + result.push_back(load_frog()); + return result; +} + +Points createNet(const BoundingBox& bounding_box, double distance) +{ + Point size = bounding_box.size(); + double distance_2 = distance / 2; + int cols1 = static_cast(floor(size.x() / distance))+1; + int cols2 = static_cast(floor((size.x() - distance_2) / distance))+1; + // equilateral triangle height with side distance + double h = sqrt(distance * distance - distance_2 * distance_2); + int rows = static_cast(floor(size.y() / h)) +1; + int rows_2 = rows / 2; + size_t count_points = rows_2 * (cols1 + static_cast(cols2)); + if (rows % 2 == 1) count_points += cols2; + Points result; + result.reserve(count_points); + bool isOdd = true; + Point offset = bounding_box.min; + double x_max = offset.x() + static_cast(size.x()); + double y_max = offset.y() + static_cast(size.y()); + for (double y = offset.y(); y <= y_max; y += h) { + double x_offset = offset.x(); + if (isOdd) x_offset += distance_2; + isOdd = !isOdd; + for (double x = x_offset; x <= x_max; x += distance) { + result.emplace_back(x, y); + } + } + assert(result.size() == count_points); + return result; +} + +// create uniform triangle net and return points laying inside island +Points rasterize(const ExPolygon &island, double distance) { + BoundingBox bb; + for (const Point &pt : island.contour.points) bb.merge(pt); + Points fullNet = createNet(bb, distance); + Points result; + result.reserve(fullNet.size()); + std::copy_if(fullNet.begin(), fullNet.end(), std::back_inserter(result), + [&island](const Point &p) { return island.contains(p); }); + return result; +} + +SupportIslandPoints test_island_sampling(const ExPolygon & island, + const SampleConfig &config) +{ + auto points = uniform_support_island(island, {}, config); + + Points chck_points = rasterize(island, config.head_radius); // TODO: Use resolution of printer + bool is_island_supported = true; // Check rasterized island points that exist support point in max_distance + double max_distance = config.thick_inner_max_distance; + std::vector point_distances(chck_points.size(), {max_distance + 1}); + for (size_t index = 0; index < chck_points.size(); ++index) { + const Point &chck_point = chck_points[index]; + double &min_distance = point_distances[index]; + bool exist_close_support_point = false; + for (const auto &island_point : points) { + const Point& p = island_point->point; + Point abs_diff(fabs(p.x() - chck_point.x()), + fabs(p.y() - chck_point.y())); + if (abs_diff.x() < min_distance && abs_diff.y() < min_distance) { + double distance = sqrt((double) abs_diff.x() * abs_diff.x() + + (double) abs_diff.y() * abs_diff.y()); + if (min_distance > distance) { + min_distance = distance; + exist_close_support_point = true; + } + } + } + if (!exist_close_support_point) is_island_supported = false; + } + + bool is_all_points_inside_island = true; + for (const auto &point : points) + if (!island.contains(point->point)) + is_all_points_inside_island = false; + +#ifdef STORE_ISLAND_ISSUES + if (!is_island_supported || !is_all_points_inside_island) { // visualize + static int counter = 0; + BoundingBox bb; + for (const Point &pt : island.contour.points) bb.merge(pt); + SVG svg(STORE_ISLAND_ISSUES + std::string("Error") + std::to_string(++counter) + ".svg", bb); + svg.draw(island, "blue", 0.5f); + for (auto& p : points) + svg.draw(p->point, island.contains(p->point) ? "lightgreen" : "red", config.head_radius); + for (size_t index = 0; index < chck_points.size(); ++index) { + const Point &chck_point = chck_points[index]; + double distance = point_distances[index]; + bool isOk = distance < max_distance; + std::string color = (isOk) ? "gray" : "red"; + svg.draw(chck_point, color, config.head_radius / 4); + } + } +#endif // STORE_ISLAND_ISSUES + + CHECK(!points.empty()); + CHECK(is_all_points_inside_island); + // CHECK(is_island_supported); // TODO: skip special cases with one point and 2 points + + return points; +} + +SampleConfig create_sample_config(double size) { + float head_diameter = .4f; + return SampleConfigFactory::create(head_diameter); + + //coord_t max_distance = 3 * size + 0.1; + //SampleConfig cfg; + //cfg.head_radius = size / 4; + //cfg.minimal_distance_from_outline = cfg.head_radius; + //cfg.maximal_distance_from_outline = max_distance/4; + //cfg.max_length_for_one_support_point = 2*size; + //cfg.max_length_for_two_support_points = 4*size; + //cfg.thin_max_width = size; + //cfg.thick_min_width = cfg.thin_max_width; + //cfg.thick_outline_max_distance = max_distance; + + //cfg.minimal_move = static_cast(size/30); + //cfg.count_iteration = 100; + //cfg.max_align_distance = 0; + //return cfg; +} + +#ifdef STORE_SAMPLE_INTO_SVG_FILES +namespace { +void store_sample(const SupportIslandPoints &samples, const ExPolygon &island) { + static int counter = 0; + BoundingBox bb(island.contour.points); + SVG svg((STORE_SAMPLE_INTO_SVG_FILES + std::to_string(counter++) + ".svg").c_str(), bb); + + double mm = scale_(1); + svg.draw(island, "lightgray"); + for (const auto &s : samples) + svg.draw(s->point, "blue", 0.2*mm); + + + // draw resolution + Point p(bb.min.x() + 1e6, bb.max.y() - 2e6); + svg.draw_text(p, (std::to_string(samples.size()) + " samples").c_str(), "black"); + svg.draw_text(p - Point(0., 1.8e6), "Scale 1 cm ", "black"); + Point start = p - Point(0., 2.3e6); + svg.draw(Line(start + Point(0., 5e5), start + Point(10*mm, 5e5)), "black", 2e5); + svg.draw(Line(start + Point(0., -5e5), start + Point(10*mm, -5e5)), "black", 2e5); + svg.draw(Line(start + Point(10*mm, 5e5), start + Point(10*mm, -5e5)), "black", 2e5); + for (int i=0; i<10;i+=2) + svg.draw(Line(start + Point(i*mm, 0.), start + Point((i+1)*mm, 0.)), "black", 1e6); +} +} // namespace +#endif // STORE_SAMPLE_INTO_SVG_FILES + +/// +/// Check for correct sampling of island +/// +TEST_CASE("Uniform sample test islands", "[SupGen], [VoronoiSkeleton]") +{ + //set_logging_level(5); + float head_diameter = .4f; + SampleConfig cfg = SampleConfigFactory::create(head_diameter); + //cfg.path = "C:/data/temp/islands/<>.svg"; + ExPolygons islands = createTestIslands(7 * scale_(head_diameter)); + for (ExPolygon &island : islands) { + // information for debug which island cause problem + [[maybe_unused]] size_t debug_index = &island - &islands.front(); + + SupportIslandPoints points = test_island_sampling(island, cfg); +#ifdef STORE_SAMPLE_INTO_SVG_FILES + store_sample(points, island); +#endif // STORE_SAMPLE_INTO_SVG_FILES + + double angle = 3.14 / 3; // cca 60 degree + + island.rotate(angle); + SupportIslandPoints pointsR = test_island_sampling(island, cfg); +#ifdef STORE_SAMPLE_INTO_SVG_FILES + store_sample(pointsR, island); +#endif // STORE_SAMPLE_INTO_SVG_FILES + + // points count should be the same + //CHECK(points.size() == pointsR.size()) + } +} + +TEST_CASE("Disable visualization", "[hide]") +{ + CHECK(true); +#ifdef STORE_SAMPLE_INTO_SVG_FILES + CHECK(false); +#endif // STORE_SAMPLE_INTO_SVG_FILES +#ifdef STORE_ISLAND_ISSUES + CHECK(false); +#endif // STORE_ISLAND_ISSUES +#ifdef USE_ISLAND_GUI_FOR_SETTINGS + CHECK(false); +#endif // USE_ISLAND_GUI_FOR_SETTINGS + CHECK(is_uniform_support_island_visualization_disabled()); +} + diff --git a/tests/sla_print/sla_test_utils.cpp b/tests/sla_print/sla_test_utils.cpp index f294abccb8..01eb222969 100644 --- a/tests/sla_print/sla_test_utils.cpp +++ b/tests/sla_print/sla_test_utils.cpp @@ -128,29 +128,27 @@ void test_supports(const std::string &obj_filename, // TODO: do the cgal hole cutting... // Create the support point generator - sla::SupportPointGenerator::Config autogencfg; - autogencfg.head_diameter = float(2 * supportcfg.head_front_radius_mm); - sla::SupportPointGenerator point_gen{sm.emesh, autogencfg, [] {}, [](int) {}}; - - point_gen.seed(0); // Make the test repeatable - point_gen.execute(out.model_slices, out.slicegrid); - + sla::SupportPointGeneratorConfig autogencfg; + sla::SupportPointGeneratorData gen_data = sla::prepare_generator_data(std::move(out.model_slices), out.slicegrid); + sla::LayerSupportPoints layer_support_points = sla::generate_support_points(gen_data, autogencfg); + double allowed_move = (out.slicegrid[1] - out.slicegrid[0]) + std::numeric_limits::epsilon(); // Get the calculated support points. - sm.pts = point_gen.output(); - + sm.pts = sla::move_on_mesh_surface(layer_support_points, sm.emesh, allowed_move); + out.model_slices = std::move(gen_data.slices); // return ownership + int validityflags = ASSUME_NO_REPAIR; // If there is no elevation, support points shall be removed from the // bottom of the object. - if (std::abs(supportcfg.object_elevation_mm) < EPSILON) { - sla::remove_bottom_points(sm.pts, zmin + supportcfg.base_height_mm); - } else { - // Should be support points at least on the bottom of the model - REQUIRE_FALSE(sm.pts.empty()); + //if (std::abs(supportcfg.object_elevation_mm) < EPSILON) { + // sla::remove_bottom_points(sm.pts, zmin + supportcfg.base_height_mm); + //} else { + // // Should be support points at least on the bottom of the model + // REQUIRE_FALSE(sm.pts.empty()); - // Also the support mesh should not be empty. - validityflags |= ASSUME_NO_EMPTY; - } + // // Also the support mesh should not be empty. + // validityflags |= ASSUME_NO_EMPTY; + //} // Generate the actual support tree sla::SupportTreeBuilder treebuilder; @@ -465,7 +463,7 @@ double predict_error(const ExPolygon &p, const sla::PixelDim &pd) sla::SupportPoints calc_support_pts( const TriangleMesh & mesh, - const sla::SupportPointGenerator::Config &cfg) + const sla::SupportPointGeneratorConfig &cfg) { // Prepare the slice grid and the slices auto bb = cast(mesh.bounding_box()); @@ -473,12 +471,11 @@ sla::SupportPoints calc_support_pts( std::vector slices = slice_mesh_ex(mesh.its, heights, CLOSING_RADIUS); // Prepare the support point calculator + sla::SupportPointGeneratorData gen_data = sla::prepare_generator_data(std::move(slices), heights); + sla::LayerSupportPoints layer_support_points = sla::generate_support_points(gen_data, cfg); + AABBMesh emesh{mesh}; - sla::SupportPointGenerator spgen{emesh, cfg, []{}, [](int){}}; - - // Calculate the support points - spgen.seed(0); - spgen.execute(slices, heights); - - return spgen.output(); + double allowed_move = (heights[1] - heights[0]) + std::numeric_limits::epsilon(); + // Get the calculated support points. + return sla::move_on_mesh_surface(layer_support_points, emesh, allowed_move); } diff --git a/tests/sla_print/sla_test_utils.hpp b/tests/sla_print/sla_test_utils.hpp index 99f4862b48..73e3c0eb58 100644 --- a/tests/sla_print/sla_test_utils.hpp +++ b/tests/sla_print/sla_test_utils.hpp @@ -136,6 +136,6 @@ double predict_error(const ExPolygon &p, const sla::PixelDim &pd); sla::SupportPoints calc_support_pts( const TriangleMesh & mesh, - const sla::SupportPointGenerator::Config &cfg = {}); + const sla::SupportPointGeneratorConfig &cfg = {}); #endif // SLA_TEST_UTILS_HPP diff --git a/tests/sla_print/sla_vectorUtils_tests.cpp b/tests/sla_print/sla_vectorUtils_tests.cpp new file mode 100644 index 0000000000..aa0f4aa622 --- /dev/null +++ b/tests/sla_print/sla_vectorUtils_tests.cpp @@ -0,0 +1,25 @@ +#include +#include + +using namespace Slic3r::sla; + +TEST_CASE("Reorder", "[Utils], [VectorUtils]") +{ + std::vector data{0, 1, 3, 2, 4, 7, 6, 5, 8}; + std::vector order{0, 1, 3, 2, 4, 7, 6, 5, 8}; + + VectorUtils::reorder(order.begin(), order.end(), data.begin()); + for (size_t i = 0; i < data.size() - 1; ++i) { + CHECK(data[i] < data[i + 1]); + } +} + +TEST_CASE("Reorder destructive", "[Utils], [VectorUtils]"){ + std::vector data {0, 1, 3, 2, 4, 7, 6, 5, 8}; + std::vector order{0, 1, 3, 2, 4, 7, 6, 5, 8}; + + VectorUtils::reorder_destructive(order.begin(), order.end(), data.begin()); + for (size_t i = 0; i < data.size() - 1;++i) { + CHECK(data[i] < data[i + 1]); + } +} diff --git a/tests/sla_print/sla_voronoi_graph_tests.cpp b/tests/sla_print/sla_voronoi_graph_tests.cpp new file mode 100644 index 0000000000..26fdf93a0b --- /dev/null +++ b/tests/sla_print/sla_voronoi_graph_tests.cpp @@ -0,0 +1,91 @@ +#include "sla_test_utils.hpp" +#include +#include + +using namespace Slic3r; +using namespace Slic3r::sla; + +TEST_CASE("Convert coordinate datatype", "[Voronoi]") +{ + using VD = Slic3r::Geometry::VoronoiDiagram; + VD::coordinate_type coord = 101197493902.64694; + coord_t coord2 = VoronoiGraphUtils::to_coord(coord); + CHECK(coord2 > 100); + + coord = -101197493902.64694; + coord2 = VoronoiGraphUtils::to_coord(coord); + CHECK(coord2 < -100); + + coord = 12345.1; + coord2 = VoronoiGraphUtils::to_coord(coord); + CHECK(coord2 == 12345); + + coord = -12345.1; + coord2 = VoronoiGraphUtils::to_coord(coord); + CHECK(coord2 == -12345); + + coord = 12345.9; + coord2 = VoronoiGraphUtils::to_coord(coord); + CHECK(coord2 == 12346); + + coord = -12345.9; + coord2 = VoronoiGraphUtils::to_coord(coord); + CHECK(coord2 == -12346); +} + +void check(Slic3r::Points points, double max_distance) { + using VD = Slic3r::Geometry::VoronoiDiagram; + VD vd; + vd.construct_voronoi(points.begin(), points.end()); + double max_area = M_PI * max_distance*max_distance; // circle = Pi * r^2 + for (const VD::cell_type &cell : vd.cells()) { + Slic3r::Polygon polygon = VoronoiGraphUtils::to_polygon(cell, points, max_distance); + CHECK(polygon.area() < max_area); + CHECK(polygon.contains(points[cell.source_index()])); + } +} + +TEST_CASE("Polygon from cell", "[Voronoi]") +{ + // for debug #define SLA_SVG_VISUALIZATION_CELL_2_POLYGON in VoronoiGraphUtils + double max_distance = 1e7; + coord_t size = (int) (4e6); + coord_t half_size = size/2; + + Slic3r::Points two_cols({Point(0, 0), Point(size, 0)}); + check(two_cols, max_distance); + + Slic3r::Points two_rows({Point(0, 0), Point(0, size)}); + check(two_rows, max_distance); + + Slic3r::Points two_diag({Point(0, 0), Point(size, size)}); + check(two_diag, max_distance); + + Slic3r::Points three({Point(0, 0), Point(size, 0), Point(half_size, size)}); + check(three, max_distance); + + Slic3r::Points middle_point({Point(0, 0), Point(size, half_size), + Point(-size, half_size), Point(0, -size)}); + check(middle_point, max_distance); + + Slic3r::Points middle_point2({Point(half_size, half_size), Point(-size, -size), Point(-size, size), + Point(size, -size), Point(size, size)}); + check(middle_point2, max_distance); + + Slic3r::Points diagonal_points({{-123473762, 71287970}, + {-61731535, 35684428}, + {0, 0}, + {61731535, -35684428}, + {123473762, -71287970}}); + double diagonal_max_distance = 5e7; + check(diagonal_points, diagonal_max_distance); + + int scale = 10; + Slic3r::Points diagonal_points2; + std::transform(diagonal_points.begin(), diagonal_points.end(), + std::back_inserter(diagonal_points2), + [&](const Slic3r::Point &p) { return p/scale; }); + check(diagonal_points2, diagonal_max_distance / scale); +} + +