//Copyright (c) 2021 Ultimaker B.V. //CuraEngine is released under the terms of the AGPLv3 or higher. #include "SkeletalTrapezoidation.hpp" #include #include #include #include #include #include #include "utils/linearAlg2D.hpp" #include "Utils.hpp" #include "SVG.hpp" #include "Geometry/VoronoiVisualUtils.hpp" #include "Geometry/VoronoiUtilsCgal.hpp" #include "../EdgeGrid.hpp" #include "Geometry/VoronoiUtils.hpp" #define SKELETAL_TRAPEZOIDATION_BEAD_SEARCH_MAX 1000 //A limit to how long it'll keep searching for adjacent beads. Increasing will re-use beadings more often (saving performance), but search longer for beading (costing performance). namespace Slic3r::Arachne { #ifdef ARACHNE_DEBUG static void export_graph_to_svg(const std::string &path, SkeletalTrapezoidationGraph &graph, const Polygons &polys, const std::vector> &edge_junctions = {}, const bool beat_count = true, const bool transition_middles = true, const bool transition_ends = true) { const std::vector colors = {"blue", "cyan", "red", "orange", "magenta", "pink", "purple", "green", "yellow"}; coordf_t stroke_width = scale_(0.03); BoundingBox bbox = get_extents(polys); for (const auto &node : graph.nodes) bbox.merge(node.p); bbox.offset(scale_(1.)); ::Slic3r::SVG svg(path.c_str(), bbox); for (const auto &line : to_lines(polys)) svg.draw(line, "gray", stroke_width); for (const auto &edge : graph.edges) svg.draw(Line(edge.from->p, edge.to->p), (edge.data.centralIsSet() && edge.data.isCentral()) ? "blue" : "cyan", stroke_width); for (const auto &line_junction : edge_junctions) for (const auto &extrusion_junction : *line_junction) svg.draw(extrusion_junction.p, "orange", coord_t(stroke_width * 2.)); if (beat_count) { for (const auto &node : graph.nodes) { svg.draw(node.p, "red", coord_t(stroke_width * 1.6)); svg.draw_text(node.p, std::to_string(node.data.bead_count).c_str(), "black", 1); } } if (transition_middles) { for (auto &edge : graph.edges) { if (std::shared_ptr> transitions = edge.data.getTransitions(); transitions) { for (auto &transition : *transitions) { Line edge_line = Line(edge.to->p, edge.from->p); double edge_length = edge_line.length(); Point pt = edge_line.a + (edge_line.vector().cast() * (double(transition.pos) / edge_length)).cast(); svg.draw(pt, "magenta", coord_t(stroke_width * 1.5)); svg.draw_text(pt, std::to_string(transition.lower_bead_count).c_str(), "black", 1); } } } } if (transition_ends) { for (auto &edge : graph.edges) { if (std::shared_ptr> transitions = edge.data.getTransitionEnds(); transitions) { for (auto &transition : *transitions) { Line edge_line = Line(edge.to->p, edge.from->p); double edge_length = edge_line.length(); Point pt = edge_line.a + (edge_line.vector().cast() * (double(transition.pos) / edge_length)).cast(); svg.draw(pt, transition.is_lower_end ? "green" : "lime", coord_t(stroke_width * 1.5)); svg.draw_text(pt, std::to_string(transition.lower_bead_count).c_str(), "black", 1); } } } } } #endif SkeletalTrapezoidation::node_t &SkeletalTrapezoidation::makeNode(const VD::vertex_type &vd_node, Point p) { auto he_node_it = vd_node_to_he_node.find(&vd_node); if (he_node_it == vd_node_to_he_node.end()) { graph.nodes.emplace_front(SkeletalTrapezoidationJoint(), p); node_t& node = graph.nodes.front(); vd_node_to_he_node.emplace(&vd_node, &node); return node; } else { return *he_node_it->second; } } void SkeletalTrapezoidation::transferEdge(Point from, Point to, const VD::edge_type &vd_edge, edge_t *&prev_edge, Point &start_source_point, Point &end_source_point, const std::vector &segments) { auto he_edge_it = vd_edge_to_he_edge.find(vd_edge.twin()); if (he_edge_it != vd_edge_to_he_edge.end()) { // Twin segment(s) have already been made edge_t* source_twin = he_edge_it->second; assert(source_twin); auto end_node_it = vd_node_to_he_node.find(vd_edge.vertex1()); assert(end_node_it != vd_node_to_he_node.end()); node_t* end_node = end_node_it->second; for (edge_t* twin = source_twin; ;twin = twin->prev->twin->prev) { if(!twin) { BOOST_LOG_TRIVIAL(warning) << "Encountered a voronoi edge without twin."; continue; //Prevent reading unallocated memory. } assert(twin); graph.edges.emplace_front(SkeletalTrapezoidationEdge()); edge_t* edge = &graph.edges.front(); edge->from = twin->to; edge->to = twin->from; edge->twin = twin; twin->twin = edge; edge->from->incident_edge = edge; if (prev_edge) { edge->prev = prev_edge; prev_edge->next = edge; } prev_edge = edge; if (prev_edge->to == end_node) { return; } if (!twin->prev || !twin->prev->twin || !twin->prev->twin->prev) { BOOST_LOG_TRIVIAL(error) << "Discretized segment behaves oddly!"; return; } assert(twin->prev); // Forth rib assert(twin->prev->twin); // Back rib assert(twin->prev->twin->prev); // Prev segment along parabola constexpr bool is_not_next_to_start_or_end = false; // Only ribs at the end of a cell should be skipped graph.makeRib(prev_edge, start_source_point, end_source_point, is_not_next_to_start_or_end); } assert(prev_edge); } else { Points discretized = discretize(vd_edge, segments); assert(discretized.size() >= 2); if(discretized.size() < 2) { BOOST_LOG_TRIVIAL(warning) << "Discretized Voronoi edge is degenerate."; } assert(!prev_edge || prev_edge->to); if(prev_edge && !prev_edge->to) { BOOST_LOG_TRIVIAL(warning) << "Previous edge doesn't go anywhere."; } node_t* v0 = (prev_edge)? prev_edge->to : &makeNode(*vd_edge.vertex0(), from); // TODO: investigate whether boost:voronoi can produce multiple verts and violates consistency Point p0 = discretized.front(); for (size_t p1_idx = 1; p1_idx < discretized.size(); p1_idx++) { Point p1 = discretized[p1_idx]; node_t* v1; if (p1_idx < discretized.size() - 1) { graph.nodes.emplace_front(SkeletalTrapezoidationJoint(), p1); v1 = &graph.nodes.front(); } else { v1 = &makeNode(*vd_edge.vertex1(), to); } graph.edges.emplace_front(SkeletalTrapezoidationEdge()); edge_t* edge = &graph.edges.front(); edge->from = v0; edge->to = v1; edge->from->incident_edge = edge; if (prev_edge) { edge->prev = prev_edge; prev_edge->next = edge; } prev_edge = edge; p0 = p1; v0 = v1; if (p1_idx < discretized.size() - 1) { // Rib for last segment gets introduced outside this function! constexpr bool is_not_next_to_start_or_end = false; // Only ribs at the end of a cell should be skipped graph.makeRib(prev_edge, start_source_point, end_source_point, is_not_next_to_start_or_end); } } assert(prev_edge); vd_edge_to_he_edge.emplace(&vd_edge, prev_edge); } } Points SkeletalTrapezoidation::discretize(const VD::edge_type& vd_edge, const std::vector& segments) { assert(Geometry::VoronoiUtils::is_in_range(vd_edge)); /*Terminology in this function assumes that the edge moves horizontally from left to right. This is not necessarily the case; the edge can go in any direction, but it helps to picture it in a certain direction in your head.*/ const VD::cell_type *left_cell = vd_edge.cell(); const VD::cell_type *right_cell = vd_edge.twin()->cell(); Point start = Geometry::VoronoiUtils::to_point(vd_edge.vertex0()).cast(); Point end = Geometry::VoronoiUtils::to_point(vd_edge.vertex1()).cast(); bool point_left = left_cell->contains_point(); bool point_right = right_cell->contains_point(); if ((!point_left && !point_right) || vd_edge.is_secondary()) // Source vert is directly connected to source segment { return Points({ start, end }); } else if (point_left != point_right) //This is a parabolic edge between a point and a line. { Point p = Geometry::VoronoiUtils::get_source_point(*(point_left ? left_cell : right_cell), segments.begin(), segments.end()); const Segment& s = Geometry::VoronoiUtils::get_source_segment(*(point_left ? right_cell : left_cell), segments.begin(), segments.end()); return Geometry::VoronoiUtils::discretize_parabola(p, s, start, end, discretization_step_size, transitioning_angle); } else //This is a straight edge between two points. { /*While the edge is straight, it is still discretized since the part becomes narrower between the two points. As such it may need different beadings along the way.*/ Point left_point = Geometry::VoronoiUtils::get_source_point(*left_cell, segments.begin(), segments.end()); Point right_point = Geometry::VoronoiUtils::get_source_point(*right_cell, segments.begin(), segments.end()); coord_t d = (right_point - left_point).cast().norm(); Point middle = (left_point + right_point) / 2; Point x_axis_dir = perp(Point(right_point - left_point)); coord_t x_axis_length = x_axis_dir.cast().norm(); const auto projected_x = [x_axis_dir, x_axis_length, middle](Point from) //Project a point on the edge. { Point vec = from - middle; assert(( vec.cast().dot(x_axis_dir.cast())/ int64_t(x_axis_length)) <= std::numeric_limits::max()); coord_t x = vec.cast().dot(x_axis_dir.cast()) / int64_t(x_axis_length); return x; }; coord_t start_x = projected_x(start); coord_t end_x = projected_x(end); //Part of the edge will be bound to the markings on the endpoints of the edge. Calculate how far that is. float bound = 0.5 / tan((M_PI - transitioning_angle) * 0.5); int64_t marking_start_x = - int64_t(d) * bound; int64_t marking_end_x = int64_t(d) * bound; assert((middle.cast() + x_axis_dir.cast() * marking_start_x / int64_t(x_axis_length)).x() <= std::numeric_limits::max()); assert((middle.cast() + x_axis_dir.cast() * marking_start_x / int64_t(x_axis_length)).y() <= std::numeric_limits::max()); assert((middle.cast() + x_axis_dir.cast() * marking_end_x / int64_t(x_axis_length)).x() <= std::numeric_limits::max()); assert((middle.cast() + x_axis_dir.cast() * marking_end_x / int64_t(x_axis_length)).y() <= std::numeric_limits::max()); Point marking_start = middle + (x_axis_dir.cast() * marking_start_x / int64_t(x_axis_length)).cast(); Point marking_end = middle + (x_axis_dir.cast() * marking_end_x / int64_t(x_axis_length)).cast(); int64_t direction = 1; if (start_x > end_x) //Oops, the Voronoi edge is the other way around. { direction = -1; std::swap(marking_start, marking_end); std::swap(marking_start_x, marking_end_x); } //Start generating points along the edge. Point a = start; Point b = end; Points ret; ret.emplace_back(a); //Introduce an extra edge at the borders of the markings? bool add_marking_start = marking_start_x * direction > int64_t(start_x) * direction; bool add_marking_end = marking_end_x * direction > int64_t(start_x) * direction; //The edge's length may not be divisible by the step size, so calculate an integer step count and evenly distribute the vertices among those. Point ab = b - a; coord_t ab_size = ab.cast().norm(); coord_t step_count = (ab_size + discretization_step_size / 2) / discretization_step_size; if (step_count % 2 == 1) { step_count++; // enforce a discretization point being added in the middle } for (coord_t step = 1; step < step_count; step++) { Point here = a + (ab.cast() * int64_t(step) / int64_t(step_count)).cast(); //Now simply interpolate the coordinates to get the new vertices! coord_t x_here = projected_x(here); //If we've surpassed the position of the extra markings, we may need to insert them first. if (add_marking_start && marking_start_x * direction < int64_t(x_here) * direction) { ret.emplace_back(marking_start); add_marking_start = false; } if (add_marking_end && marking_end_x * direction < int64_t(x_here) * direction) { ret.emplace_back(marking_end); add_marking_end = false; } ret.emplace_back(here); } if (add_marking_end && marking_end_x * direction < int64_t(end_x) * direction) { ret.emplace_back(marking_end); } ret.emplace_back(b); return ret; } } bool SkeletalTrapezoidation::computePointCellRange(const VD::cell_type &cell, Point &start_source_point, Point &end_source_point, const VD::edge_type *&starting_vd_edge, const VD::edge_type *&ending_vd_edge, const std::vector &segments) { if (cell.incident_edge()->is_infinite()) return false; //Infinite edges only occur outside of the polygon. Don't copy any part of this cell. // Check if any point of the cell is inside or outside polygon // Copy whole cell into graph or not at all // If the cell.incident_edge()->vertex0() is far away so much that it doesn't even fit into Vec2i64, then there is no way that it will be inside the input polygon. if (const VD::vertex_type &vert = *cell.incident_edge()->vertex0(); vert.x() >= double(std::numeric_limits::max()) || vert.x() <= double(std::numeric_limits::lowest()) || vert.y() >= double(std::numeric_limits::max()) || vert.y() <= double(std::numeric_limits::lowest())) return false; // Don't copy any part of this cell const Point source_point = Geometry::VoronoiUtils::get_source_point(cell, segments.begin(), segments.end()); const PolygonsPointIndex source_point_index = Geometry::VoronoiUtils::get_source_point_index(cell, segments.begin(), segments.end()); Vec2i64 some_point = Geometry::VoronoiUtils::to_point(cell.incident_edge()->vertex0()); if (some_point == source_point.cast()) some_point = Geometry::VoronoiUtils::to_point(cell.incident_edge()->vertex1()); //Test if the some_point is even inside the polygon. //The edge leading out of a polygon must have an endpoint that's not in the corner following the contour of the polygon at that vertex. //So if it's inside the corner formed by the polygon vertex, it's all fine. //But if it's outside of the corner, it must be a vertex of the Voronoi diagram that goes outside of the polygon towards infinity. if (!LinearAlg2D::isInsideCorner(source_point_index.prev().p(), source_point_index.p(), source_point_index.next().p(), some_point)) return false; // Don't copy any part of this cell const VD::edge_type* vd_edge = cell.incident_edge(); do { assert(vd_edge->is_finite()); if (Vec2i64 p1 = Geometry::VoronoiUtils::to_point(vd_edge->vertex1()); p1 == source_point.cast()) { start_source_point = source_point; end_source_point = source_point; starting_vd_edge = vd_edge->next(); ending_vd_edge = vd_edge; } else { assert((Geometry::VoronoiUtils::to_point(vd_edge->vertex0()) == source_point.cast() || !vd_edge->is_secondary()) && "point cells must end in the point! They cannot cross the point with an edge, because collinear edges are not allowed in the input."); } } while (vd_edge = vd_edge->next(), vd_edge != cell.incident_edge()); assert(starting_vd_edge && ending_vd_edge); assert(starting_vd_edge != ending_vd_edge); return true; } SkeletalTrapezoidation::SkeletalTrapezoidation(const Polygons& polys, const BeadingStrategy& beading_strategy, double transitioning_angle, coord_t discretization_step_size, coord_t transition_filter_dist, coord_t allowed_filter_deviation, coord_t beading_propagation_transition_dist ): transitioning_angle(transitioning_angle), discretization_step_size(discretization_step_size), transition_filter_dist(transition_filter_dist), allowed_filter_deviation(allowed_filter_deviation), beading_propagation_transition_dist(beading_propagation_transition_dist), beading_strategy(beading_strategy) { constructFromPolygons(polys); } void SkeletalTrapezoidation::constructFromPolygons(const Polygons& polys) { #ifdef ARACHNE_DEBUG this->outline = polys; #endif // Check self intersections. assert([&polys]() -> bool { EdgeGrid::Grid grid; grid.set_bbox(get_extents(polys)); grid.create(polys, scaled(10.)); return !grid.has_intersecting_edges(); }()); vd_edge_to_he_edge.clear(); vd_node_to_he_node.clear(); std::vector segments; for (size_t poly_idx = 0; poly_idx < polys.size(); poly_idx++) for (size_t point_idx = 0; point_idx < polys[poly_idx].size(); point_idx++) segments.emplace_back(&polys, poly_idx, point_idx); #ifdef ARACHNE_DEBUG { static int iRun = 0; BoundingBox bbox = get_extents(polys); SVG svg(debug_out_path("arachne_voronoi-input-%d.svg", iRun++).c_str(), bbox); svg.draw_outline(polys, "black", scaled(0.03f)); } #endif VD voronoi_diagram; voronoi_diagram.construct_voronoi(segments.cbegin(), segments.cend()); #ifdef ARACHNE_DEBUG_VORONOI { static int iRun = 0; dump_voronoi_to_svg(debug_out_path("arachne_voronoi-diagram-%d.svg", iRun++).c_str(), voronoi_diagram, to_points(polys), to_lines(polys)); } #endif assert(this->graph.edges.empty() && this->graph.nodes.empty() && this->vd_edge_to_he_edge.empty() && this->vd_node_to_he_node.empty()); for (const VD::cell_type &cell : voronoi_diagram.cells()) { if (!cell.incident_edge()) continue; // There is no spoon Point start_source_point; Point end_source_point; const VD::edge_type *starting_voronoi_edge = nullptr; const VD::edge_type *ending_voronoi_edge = nullptr; // Compute and store result in above variables if (cell.contains_point()) { const bool keep_going = computePointCellRange(cell, start_source_point, end_source_point, starting_voronoi_edge, ending_voronoi_edge, segments); if (!keep_going) continue; } else { assert(cell.contains_segment()); Geometry::SegmentCellRange cell_range = Geometry::VoronoiUtils::compute_segment_cell_range(cell, segments.cbegin(), segments.cend()); assert(cell_range.is_valid()); start_source_point = cell_range.segment_start_point; end_source_point = cell_range.segment_end_point; starting_voronoi_edge = cell_range.edge_begin; ending_voronoi_edge = cell_range.edge_end; } if (!starting_voronoi_edge || !ending_voronoi_edge) { assert(false && "Each cell should start / end in a polygon vertex"); continue; } // Copy start to end edge to graph assert(Geometry::VoronoiUtils::is_in_range(*starting_voronoi_edge)); edge_t *prev_edge = nullptr; transferEdge(start_source_point, Geometry::VoronoiUtils::to_point(starting_voronoi_edge->vertex1()).cast(), *starting_voronoi_edge, prev_edge, start_source_point, end_source_point, segments); node_t *starting_node = vd_node_to_he_node[starting_voronoi_edge->vertex0()]; starting_node->data.distance_to_boundary = 0; constexpr bool is_next_to_start_or_end = true; graph.makeRib(prev_edge, start_source_point, end_source_point, is_next_to_start_or_end); for (const VD::edge_type* vd_edge = starting_voronoi_edge->next(); vd_edge != ending_voronoi_edge; vd_edge = vd_edge->next()) { assert(vd_edge->is_finite()); assert(Geometry::VoronoiUtils::is_in_range(*vd_edge)); Point v1 = Geometry::VoronoiUtils::to_point(vd_edge->vertex0()).cast(); Point v2 = Geometry::VoronoiUtils::to_point(vd_edge->vertex1()).cast(); transferEdge(v1, v2, *vd_edge, prev_edge, start_source_point, end_source_point, segments); graph.makeRib(prev_edge, start_source_point, end_source_point, vd_edge->next() == ending_voronoi_edge); } transferEdge(Geometry::VoronoiUtils::to_point(ending_voronoi_edge->vertex0()).cast(), end_source_point, *ending_voronoi_edge, prev_edge, start_source_point, end_source_point, segments); prev_edge->to->data.distance_to_boundary = 0; } #ifdef ARACHNE_DEBUG assert(Geometry::VoronoiUtilsCgal::is_voronoi_diagram_planar_intersection(voronoi_diagram)); #endif separatePointyQuadEndNodes(); graph.collapseSmallEdges(); // Set [incident_edge] the first possible edge that way we can iterate over all reachable edges from node.incident_edge, // without needing to iterate backward for (edge_t& edge : graph.edges) if (!edge.prev) edge.from->incident_edge = &edge; } void SkeletalTrapezoidation::separatePointyQuadEndNodes() { NodeSet visited_nodes; for (edge_t& edge : graph.edges) { if (edge.prev) { continue; } edge_t* quad_start = &edge; if (visited_nodes.find(quad_start->from) == visited_nodes.end()) { visited_nodes.emplace(quad_start->from); } else { // Needs to be duplicated graph.nodes.emplace_back(*quad_start->from); node_t* new_node = &graph.nodes.back(); new_node->incident_edge = quad_start; quad_start->from = new_node; quad_start->twin->to = new_node; } } } // // ^^^^^^^^^^^^^^^^^^^^^ // INITIALIZATION // ===================== // // ===================== // TRANSTISIONING // vvvvvvvvvvvvvvvvvvvvv // void SkeletalTrapezoidation::generateToolpaths(std::vector &generated_toolpaths, bool filter_outermost_central_edges) { #ifdef ARACHNE_DEBUG static int iRun = 0; #endif p_generated_toolpaths = &generated_toolpaths; updateIsCentral(); #ifdef ARACHNE_DEBUG export_graph_to_svg(debug_out_path("ST-updateIsCentral-final-%d.svg", iRun), this->graph, this->outline); #endif filterCentral(central_filter_dist); #ifdef ARACHNE_DEBUG export_graph_to_svg(debug_out_path("ST-filterCentral-final-%d.svg", iRun), this->graph, this->outline); #endif if (filter_outermost_central_edges) filterOuterCentral(); updateBeadCount(); #ifdef ARACHNE_DEBUG export_graph_to_svg(debug_out_path("ST-updateBeadCount-final-%d.svg", iRun), this->graph, this->outline); #endif filterNoncentralRegions(); #ifdef ARACHNE_DEBUG export_graph_to_svg(debug_out_path("ST-filterNoncentralRegions-final-%d.svg", iRun), this->graph, this->outline); #endif generateTransitioningRibs(); #ifdef ARACHNE_DEBUG export_graph_to_svg(debug_out_path("ST-generateTransitioningRibs-final-%d.svg", iRun), this->graph, this->outline); #endif generateExtraRibs(); #ifdef ARACHNE_DEBUG export_graph_to_svg(debug_out_path("ST-generateExtraRibs-final-%d.svg", iRun), this->graph, this->outline); #endif generateSegments(); #ifdef ARACHNE_DEBUG export_graph_to_svg(debug_out_path("ST-generateSegments-final-%d.svg", iRun), this->graph, this->outline); #endif #ifdef ARACHNE_DEBUG ++iRun; #endif } void SkeletalTrapezoidation::updateIsCentral() { // _.-'^` A and B are the endpoints of an edge we're checking. // _.-'^` Part of the line AB will be used as a cap, // _.-'^` \ because the polygon is too narrow there. // _.-'^` \ If |AB| minus the cap is still bigger than dR, // _.-'^` \ R2 the edge AB is considered central. It's then // _.-'^` \ _.-'\`\ significant compared to the edges around it. // _.-'^` \R1 _.-'^` '`\ dR // _.-'^`a/2 \_.-'^`a \ Line AR2 is parallel to the polygon contour. // `^'-._````````````````A```````````v````````B``````` dR is the remaining diameter at B. // `^'-._ dD = |AB| As a result, AB is less often central if the polygon // `^'-._ corner is obtuse. // sin a = dR / dD coord_t outer_edge_filter_length = beading_strategy.getTransitionThickness(0) / 2; float cap = sin(beading_strategy.getTransitioningAngle() * 0.5); // = cos(bisector_angle / 2) for (edge_t& edge: graph.edges) { assert(edge.twin); if(!edge.twin) { BOOST_LOG_TRIVIAL(warning) << "Encountered a Voronoi edge without twin!"; continue; } if(edge.twin->data.centralIsSet()) { edge.data.setIsCentral(edge.twin->data.isCentral()); } else if(edge.data.type == SkeletalTrapezoidationEdge::EdgeType::EXTRA_VD) { edge.data.setIsCentral(false); } else if(std::max(edge.from->data.distance_to_boundary, edge.to->data.distance_to_boundary) < outer_edge_filter_length) { edge.data.setIsCentral(false); } else { Point a = edge.from->p; Point b = edge.to->p; Point ab = b - a; coord_t dR = std::abs(edge.to->data.distance_to_boundary - edge.from->data.distance_to_boundary); coord_t dD = ab.cast().norm(); edge.data.setIsCentral(dR < dD * cap); } } } void SkeletalTrapezoidation::filterCentral(coord_t max_length) { for (edge_t& edge : graph.edges) { if (isEndOfCentral(edge) && edge.to->isLocalMaximum() && !edge.to->isLocalMaximum()) { filterCentral(edge.twin, 0, max_length); } } } bool SkeletalTrapezoidation::filterCentral(edge_t* starting_edge, coord_t traveled_dist, coord_t max_length) { coord_t length = (starting_edge->from->p - starting_edge->to->p).cast().norm(); if (traveled_dist + length > max_length) { return false; } bool should_dissolve = true; //Should we unmark this as central and propagate that? for (edge_t* next_edge = starting_edge->next; next_edge && next_edge != starting_edge->twin; next_edge = next_edge->twin->next) { if (next_edge->data.isCentral()) { should_dissolve &= filterCentral(next_edge, traveled_dist + length, max_length); } } should_dissolve &= !starting_edge->to->isLocalMaximum(); // Don't filter central regions with a local maximum! if (should_dissolve) { starting_edge->data.setIsCentral(false); starting_edge->twin->data.setIsCentral(false); } return should_dissolve; } void SkeletalTrapezoidation::filterOuterCentral() { for (edge_t& edge : graph.edges) { if (!edge.prev) { edge.data.setIsCentral(false); edge.twin->data.setIsCentral(false); } } } void SkeletalTrapezoidation::updateBeadCount() { for (edge_t& edge : graph.edges) { if (edge.data.isCentral()) { edge.to->data.bead_count = beading_strategy.getOptimalBeadCount(edge.to->data.distance_to_boundary * 2); } } // Fix bead count at locally maximal R, also for central regions!! See TODO s in generateTransitionEnd(.) for (node_t& node : graph.nodes) { if (node.isLocalMaximum()) { if (node.data.distance_to_boundary < 0) { BOOST_LOG_TRIVIAL(warning) << "Distance to boundary not yet computed for local maximum!"; node.data.distance_to_boundary = std::numeric_limits::max(); edge_t* edge = node.incident_edge; do { node.data.distance_to_boundary = std::min(node.data.distance_to_boundary, edge->to->data.distance_to_boundary + coord_t((edge->from->p - edge->to->p).cast().norm())); } while (edge = edge->twin->next, edge != node.incident_edge); } coord_t bead_count = beading_strategy.getOptimalBeadCount(node.data.distance_to_boundary * 2); node.data.bead_count = bead_count; } } } void SkeletalTrapezoidation::filterNoncentralRegions() { for (edge_t& edge : graph.edges) { if (!isEndOfCentral(edge)) { continue; } if(edge.to->data.bead_count < 0 && edge.to->data.distance_to_boundary != 0) { BOOST_LOG_TRIVIAL(warning) << "Encountered an uninitialized bead at the boundary!"; } assert(edge.to->data.bead_count >= 0 || edge.to->data.distance_to_boundary == 0); constexpr coord_t max_dist = scaled(0.4); filterNoncentralRegions(&edge, edge.to->data.bead_count, 0, max_dist); } } bool SkeletalTrapezoidation::filterNoncentralRegions(edge_t* to_edge, coord_t bead_count, coord_t traveled_dist, coord_t max_dist) { coord_t r = to_edge->to->data.distance_to_boundary; edge_t* next_edge = to_edge->next; for (; next_edge && next_edge != to_edge->twin; next_edge = next_edge->twin->next) { if (next_edge->to->data.distance_to_boundary >= r || shorter_then(next_edge->to->p - next_edge->from->p, scaled(0.01))) { break; // Only walk upward } } if (next_edge == to_edge->twin || ! next_edge) { return false; } const coord_t length = (next_edge->to->p - next_edge->from->p).cast().norm(); bool dissolve = false; if (next_edge->to->data.bead_count == bead_count) { dissolve = true; } else if (next_edge->to->data.bead_count < 0) { dissolve = filterNoncentralRegions(next_edge, bead_count, traveled_dist + length, max_dist); } else // Upward bead count is different { // Dissolve if two central regions with different bead count are closer together than the max_dist (= transition distance) dissolve = (traveled_dist + length < max_dist) && std::abs(next_edge->to->data.bead_count - bead_count) == 1; } if (dissolve) { next_edge->data.setIsCentral(true); next_edge->twin->data.setIsCentral(true); next_edge->to->data.bead_count = beading_strategy.getOptimalBeadCount(next_edge->to->data.distance_to_boundary * 2); next_edge->to->data.transition_ratio = 0; } return dissolve; // Dissolving only depend on the one edge going upward. There cannot be multiple edges going upward. } void SkeletalTrapezoidation::generateTransitioningRibs() { // Store the upward edges to the transitions. // We only store the halfedge for which the distance_to_boundary is higher at the end than at the beginning. ptr_vector_t> edge_transitions; generateTransitionMids(edge_transitions); for (edge_t& edge : graph.edges) { // Check if there is a transition in between nodes with different bead counts if (edge.data.isCentral() && edge.from->data.bead_count != edge.to->data.bead_count) { assert(edge.data.hasTransitions() || edge.twin->data.hasTransitions()); } } filterTransitionMids(); #ifdef ARACHNE_DEBUG static int iRun = 0; export_graph_to_svg(debug_out_path("ST-generateTransitioningRibs-mids-%d.svg", iRun++), this->graph, this->outline); #endif ptr_vector_t> edge_transition_ends; // We only map the half edge in the upward direction. mapped items are not sorted generateAllTransitionEnds(edge_transition_ends); #ifdef ARACHNE_DEBUG export_graph_to_svg(debug_out_path("ST-generateTransitioningRibs-ends-%d.svg", iRun++), this->graph, this->outline); #endif applyTransitions(edge_transition_ends); // Note that the shared pointer lists will be out of scope and thus destroyed here, since the remaining refs are weak_ptr. #ifdef ARACHNE_DEBUG ++iRun; #endif } void SkeletalTrapezoidation::generateTransitionMids(ptr_vector_t>& edge_transitions) { for (edge_t& edge : graph.edges) { assert(edge.data.centralIsSet()); if (!edge.data.isCentral()) { // Only central regions introduce transitions continue; } coord_t start_R = edge.from->data.distance_to_boundary; coord_t end_R = edge.to->data.distance_to_boundary; int start_bead_count = edge.from->data.bead_count; int end_bead_count = edge.to->data.bead_count; if (start_R == end_R) { // No transitions occur when both end points have the same distance_to_boundary assert(edge.from->data.bead_count == edge.to->data.bead_count); if(edge.from->data.bead_count != edge.to->data.bead_count) { BOOST_LOG_TRIVIAL(warning) << "Bead count " << edge.from->data.bead_count << " is different from " << edge.to->data.bead_count << " even though distance to boundary is the same."; } continue; } else if (start_R > end_R) { // Only consider those half-edges which are going from a lower to a higher distance_to_boundary continue; } if (edge.from->data.bead_count == edge.to->data.bead_count) { // No transitions should occur according to the enforced bead counts continue; } if (start_bead_count > beading_strategy.getOptimalBeadCount(start_R * 2) || end_bead_count > beading_strategy.getOptimalBeadCount(end_R * 2)) { // Wasn't the case earlier in this function because of already introduced transitions BOOST_LOG_TRIVIAL(error) << "transitioning segment overlap! (?)"; } assert(start_R < end_R); if(start_R >= end_R) { BOOST_LOG_TRIVIAL(warning) << "Transitioning the wrong way around! This function expects to transition from small R to big R, but was transitioning from " << start_R << " to " << end_R; } coord_t edge_size = (edge.from->p - edge.to->p).cast().norm(); for (int transition_lower_bead_count = start_bead_count; transition_lower_bead_count < end_bead_count; transition_lower_bead_count++) { coord_t mid_R = beading_strategy.getTransitionThickness(transition_lower_bead_count) / 2; if (mid_R > end_R) { BOOST_LOG_TRIVIAL(error) << "transition on segment lies outside of segment!"; mid_R = end_R; } if (mid_R < start_R) { BOOST_LOG_TRIVIAL(error) << "transition on segment lies outside of segment!"; mid_R = start_R; } coord_t mid_pos = int64_t(edge_size) * int64_t(mid_R - start_R) / int64_t(end_R - start_R); assert(mid_pos >= 0); assert(mid_pos <= edge_size); if(mid_pos < 0 || mid_pos > edge_size) { BOOST_LOG_TRIVIAL(warning) << "Transition mid is out of bounds of the edge."; } auto transitions = edge.data.getTransitions(); constexpr bool ignore_empty = true; assert((! edge.data.hasTransitions(ignore_empty)) || mid_pos >= transitions->back().pos); if (! edge.data.hasTransitions(ignore_empty)) { edge_transitions.emplace_back(std::make_shared>()); edge.data.setTransitions(edge_transitions.back()); // initialization transitions = edge.data.getTransitions(); } transitions->emplace_back(mid_pos, transition_lower_bead_count, mid_R); } assert((edge.from->data.bead_count == edge.to->data.bead_count) || edge.data.hasTransitions()); } } void SkeletalTrapezoidation::filterTransitionMids() { for (edge_t& edge : graph.edges) { if (! edge.data.hasTransitions()) { continue; } auto& transitions = *edge.data.getTransitions(); // This is how stuff should be stored in transitions assert(transitions.front().lower_bead_count <= transitions.back().lower_bead_count); assert(edge.from->data.distance_to_boundary <= edge.to->data.distance_to_boundary); const Point a = edge.from->p; const Point b = edge.to->p; Point ab = b - a; coord_t ab_size = ab.cast().norm(); bool going_up = true; std::list to_be_dissolved_back = dissolveNearbyTransitions(&edge, transitions.back(), ab_size - transitions.back().pos, transition_filter_dist, going_up); bool should_dissolve_back = !to_be_dissolved_back.empty(); for (TransitionMidRef& ref : to_be_dissolved_back) { dissolveBeadCountRegion(&edge, transitions.back().lower_bead_count + 1, transitions.back().lower_bead_count); ref.edge->data.getTransitions()->erase(ref.transition_it); } { coord_t trans_bead_count = transitions.back().lower_bead_count; coord_t upper_transition_half_length = (1.0 - beading_strategy.getTransitionAnchorPos(trans_bead_count)) * beading_strategy.getTransitioningLength(trans_bead_count); should_dissolve_back |= filterEndOfCentralTransition(&edge, ab_size - transitions.back().pos, upper_transition_half_length, trans_bead_count); } if (should_dissolve_back) { transitions.pop_back(); } if (transitions.empty()) { // FilterEndOfCentralTransition gives inconsistent new bead count when executing for the same transition in two directions. continue; } going_up = false; std::list to_be_dissolved_front = dissolveNearbyTransitions(edge.twin, transitions.front(), transitions.front().pos, transition_filter_dist, going_up); bool should_dissolve_front = !to_be_dissolved_front.empty(); for (TransitionMidRef& ref : to_be_dissolved_front) { dissolveBeadCountRegion(edge.twin, transitions.front().lower_bead_count, transitions.front().lower_bead_count + 1); ref.edge->data.getTransitions()->erase(ref.transition_it); } { coord_t trans_bead_count = transitions.front().lower_bead_count; coord_t lower_transition_half_length = beading_strategy.getTransitionAnchorPos(trans_bead_count) * beading_strategy.getTransitioningLength(trans_bead_count); should_dissolve_front |= filterEndOfCentralTransition(edge.twin, transitions.front().pos, lower_transition_half_length, trans_bead_count + 1); } if (should_dissolve_front) { transitions.pop_front(); } if (transitions.empty()) { // FilterEndOfCentralTransition gives inconsistent new bead count when executing for the same transition in two directions. continue; } } } std::list SkeletalTrapezoidation::dissolveNearbyTransitions(edge_t* edge_to_start, TransitionMiddle& origin_transition, coord_t traveled_dist, coord_t max_dist, bool going_up) { std::list to_be_dissolved; if (traveled_dist > max_dist) return to_be_dissolved; bool should_dissolve = true; for (edge_t* edge = edge_to_start->next; edge && edge != edge_to_start->twin; edge = edge->twin->next){ if (!edge->data.isCentral()) continue; Point a = edge->from->p; Point b = edge->to->p; Point ab = b - a; coord_t ab_size = ab.cast().norm(); bool is_aligned = edge->isUpward(); edge_t* aligned_edge = is_aligned? edge : edge->twin; bool seen_transition_on_this_edge = false; const coord_t origin_radius = origin_transition.feature_radius; const coord_t radius_here = edge->from->data.distance_to_boundary; const bool dissolve_result_is_odd = bool(origin_transition.lower_bead_count % 2) == going_up; const coord_t width_deviation = std::abs(origin_radius - radius_here) * 2; // times by two because the deviation happens at both sides of the significant edge const coord_t line_width_deviation = dissolve_result_is_odd ? width_deviation : width_deviation / 2; // assume the deviation will be split over either 1 or 2 lines, i.e. assume wall_distribution_count = 1 if (line_width_deviation > allowed_filter_deviation) should_dissolve = false; if (should_dissolve && aligned_edge->data.hasTransitions()) { auto& transitions = *aligned_edge->data.getTransitions(); for (auto transition_it = transitions.begin(); transition_it != transitions.end(); ++ transition_it) { // Note: this is not necessarily iterating in the traveling direction! // Check whether we should dissolve coord_t pos = is_aligned? transition_it->pos : ab_size - transition_it->pos; if (traveled_dist + pos < max_dist && transition_it->lower_bead_count == origin_transition.lower_bead_count) { // Only dissolve local optima if (traveled_dist + pos < beading_strategy.getTransitioningLength(transition_it->lower_bead_count)) { // Consecutive transitions both in/decreasing in bead count should never be closer together than the transition distance assert(going_up != is_aligned || transition_it->lower_bead_count == 0); } to_be_dissolved.emplace_back(aligned_edge, transition_it); seen_transition_on_this_edge = true; } } } if (should_dissolve && !seen_transition_on_this_edge) { std::list to_be_dissolved_here = dissolveNearbyTransitions(edge, origin_transition, traveled_dist + ab_size, max_dist, going_up); if (to_be_dissolved_here.empty()) { // The region is too long to be dissolved in this direction, so it cannot be dissolved in any direction. to_be_dissolved.clear(); return to_be_dissolved; } to_be_dissolved.splice(to_be_dissolved.end(), to_be_dissolved_here); // Transfer to_be_dissolved_here into to_be_dissolved should_dissolve = should_dissolve && !to_be_dissolved.empty(); } } if (!should_dissolve) to_be_dissolved.clear(); return to_be_dissolved; } void SkeletalTrapezoidation::dissolveBeadCountRegion(edge_t* edge_to_start, coord_t from_bead_count, coord_t to_bead_count) { assert(from_bead_count != to_bead_count); if (edge_to_start->to->data.bead_count != from_bead_count) return; edge_to_start->to->data.bead_count = to_bead_count; for (edge_t* edge = edge_to_start->next; edge && edge != edge_to_start->twin; edge = edge->twin->next) { if (!edge->data.isCentral()) { continue; } dissolveBeadCountRegion(edge, from_bead_count, to_bead_count); } } bool SkeletalTrapezoidation::filterEndOfCentralTransition(edge_t* edge_to_start, coord_t traveled_dist, coord_t max_dist, coord_t replacing_bead_count) { if (traveled_dist > max_dist) { return false; } bool is_end_of_central = true; bool should_dissolve = false; for (edge_t* next_edge = edge_to_start->next; next_edge && next_edge != edge_to_start->twin; next_edge = next_edge->twin->next) { if (next_edge->data.isCentral()) { coord_t length = (next_edge->to->p - next_edge->from->p).cast().norm(); should_dissolve |= filterEndOfCentralTransition(next_edge, traveled_dist + length, max_dist, replacing_bead_count); is_end_of_central = false; } } if (is_end_of_central && traveled_dist < max_dist) { should_dissolve = true; } if (should_dissolve) { edge_to_start->to->data.bead_count = replacing_bead_count; } return should_dissolve; } void SkeletalTrapezoidation::generateAllTransitionEnds(ptr_vector_t>& edge_transition_ends) { for (edge_t& edge : graph.edges) { if (! edge.data.hasTransitions()) { continue; } auto& transition_positions = *edge.data.getTransitions(); assert(edge.from->data.distance_to_boundary <= edge.to->data.distance_to_boundary); for (TransitionMiddle& transition_middle : transition_positions) { assert(transition_positions.front().pos <= transition_middle.pos); assert(transition_middle.pos <= transition_positions.back().pos); generateTransitionEnds(edge, transition_middle.pos, transition_middle.lower_bead_count, edge_transition_ends); } } } void SkeletalTrapezoidation::generateTransitionEnds(edge_t& edge, coord_t mid_pos, coord_t lower_bead_count, ptr_vector_t>& edge_transition_ends) { const Point a = edge.from->p; const Point b = edge.to->p; const Point ab = b - a; const coord_t ab_size = ab.cast().norm(); const coord_t transition_length = beading_strategy.getTransitioningLength(lower_bead_count); const float transition_mid_position = beading_strategy.getTransitionAnchorPos(lower_bead_count); constexpr float inner_bead_width_ratio_after_transition = 1.0; constexpr coord_t start_rest = 0; const float mid_rest = transition_mid_position * inner_bead_width_ratio_after_transition; constexpr float end_rest = inner_bead_width_ratio_after_transition; { // Lower bead count transition end const coord_t start_pos = ab_size - mid_pos; const coord_t transition_half_length = transition_mid_position * int64_t(transition_length); const coord_t end_pos = start_pos + transition_half_length; generateTransitionEnd(*edge.twin, start_pos, end_pos, transition_half_length, mid_rest, start_rest, lower_bead_count, edge_transition_ends); } { // Upper bead count transition end const coord_t start_pos = mid_pos; const coord_t transition_half_length = (1.0 - transition_mid_position) * transition_length; const coord_t end_pos = mid_pos + transition_half_length; #ifdef DEBUG if (! generateTransitionEnd(edge, start_pos, end_pos, transition_half_length, mid_rest, end_rest, lower_bead_count, edge_transition_ends)) { BOOST_LOG_TRIVIAL(warning) << "There must have been at least one direction in which the bead count is increasing enough for the transition to happen!"; } #else generateTransitionEnd(edge, start_pos, end_pos, transition_half_length, mid_rest, end_rest, lower_bead_count, edge_transition_ends); #endif } } bool SkeletalTrapezoidation::generateTransitionEnd(edge_t& edge, coord_t start_pos, coord_t end_pos, coord_t transition_half_length, double start_rest, double end_rest, coord_t lower_bead_count, ptr_vector_t>& edge_transition_ends) { Point a = edge.from->p; Point b = edge.to->p; Point ab = b - a; coord_t ab_size = ab.cast().norm(); // TODO: prevent recalculation of these values assert(start_pos <= ab_size); if(start_pos > ab_size) { BOOST_LOG_TRIVIAL(warning) << "Start position of edge is beyond edge range."; } bool going_up = end_rest > start_rest; assert(edge.data.isCentral()); if (!edge.data.isCentral()) { BOOST_LOG_TRIVIAL(warning) << "This function shouldn't generate ends in or beyond non-central regions."; return false; } if (end_pos > ab_size) { // Recurse on all further edges float rest = end_rest - (start_rest - end_rest) * (end_pos - ab_size) / (start_pos - end_pos); assert(rest >= 0); assert(rest <= std::max(end_rest, start_rest)); assert(rest >= std::min(end_rest, start_rest)); coord_t central_edge_count = 0; for (edge_t* outgoing = edge.next; outgoing && outgoing != edge.twin; outgoing = outgoing->twin->next) { if (!outgoing->data.isCentral()) continue; central_edge_count++; } bool is_only_going_down = true; bool has_recursed = false; for (edge_t* outgoing = edge.next; outgoing && outgoing != edge.twin;) { edge_t* next = outgoing->twin->next; // Before we change the outgoing edge itself if (!outgoing->data.isCentral()) { outgoing = next; continue; // Don't put transition ends in non-central regions } if (central_edge_count > 1 && going_up && isGoingDown(outgoing, 0, end_pos - ab_size + transition_half_length, lower_bead_count)) { // We're after a 3-way_all-central_junction-node and going in the direction of lower bead count // don't introduce a transition end along this central direction, because this direction is the downward direction // while we are supposed to be [going_up] outgoing = next; continue; } bool is_going_down = generateTransitionEnd(*outgoing, 0, end_pos - ab_size, transition_half_length, rest, end_rest, lower_bead_count, edge_transition_ends); is_only_going_down &= is_going_down; outgoing = next; has_recursed = true; } if (!going_up || (has_recursed && !is_only_going_down)) { edge.to->data.transition_ratio = rest; edge.to->data.bead_count = lower_bead_count; } return is_only_going_down; } else // end_pos < ab_size { // Add transition end point here bool is_lower_end = end_rest == 0; // TODO collapse this parameter into the bool for which it is used here! coord_t pos = -1; edge_t* upward_edge = nullptr; if (edge.isUpward()) { upward_edge = &edge; pos = end_pos; } else { upward_edge = edge.twin; pos = ab_size - end_pos; } if(!upward_edge->data.hasTransitionEnds()) { //This edge doesn't have a data structure yet for the transition ends. Make one. edge_transition_ends.emplace_back(std::make_shared>()); upward_edge->data.setTransitionEnds(edge_transition_ends.back()); } auto transitions = upward_edge->data.getTransitionEnds(); //Add a transition to it (on the correct side). assert(ab_size == (edge.twin->from->p - edge.twin->to->p).cast().norm()); assert(pos <= ab_size); if (transitions->empty() || pos < transitions->front().pos) { // Preorder so that sorting later on is faster transitions->emplace_front(pos, lower_bead_count, is_lower_end); } else { transitions->emplace_back(pos, lower_bead_count, is_lower_end); } return false; } } bool SkeletalTrapezoidation::isGoingDown(edge_t* outgoing, coord_t traveled_dist, coord_t max_dist, coord_t lower_bead_count) const { // NOTE: the logic below is not fully thought through. // TODO: take transition mids into account if (outgoing->to->data.distance_to_boundary == 0) { return true; } bool is_upward = outgoing->to->data.distance_to_boundary >= outgoing->from->data.distance_to_boundary; edge_t* upward_edge = is_upward? outgoing : outgoing->twin; if (outgoing->to->data.bead_count > lower_bead_count + 1) { assert(upward_edge->data.hasTransitions() && "If the bead count is going down there has to be a transition mid!"); if(!upward_edge->data.hasTransitions()) { BOOST_LOG_TRIVIAL(warning) << "If the bead count is going down there has to be a transition mid!"; } return false; } coord_t length = (outgoing->to->p - outgoing->from->p).cast().norm(); if (upward_edge->data.hasTransitions()) { auto& transition_mids = *upward_edge->data.getTransitions(); TransitionMiddle& mid = is_upward? transition_mids.front() : transition_mids.back(); if ( mid.lower_bead_count == lower_bead_count && ((is_upward && mid.pos + traveled_dist < max_dist) || (!is_upward && length - mid.pos + traveled_dist < max_dist)) ) { return true; } } if (traveled_dist + length > max_dist) { return false; } if (outgoing->to->data.bead_count <= lower_bead_count && !(outgoing->to->data.bead_count == lower_bead_count && outgoing->to->data.transition_ratio > 0.0)) { return true; } bool is_only_going_down = true; bool has_recursed = false; for (edge_t* next = outgoing->next; next && next != outgoing->twin; next = next->twin->next) { if (!next->data.isCentral()) { continue; } bool is_going_down = isGoingDown(next, traveled_dist + length, max_dist, lower_bead_count); is_only_going_down &= is_going_down; has_recursed = true; } return has_recursed && is_only_going_down; } static inline Point normal(const Point& p0, coord_t len) { int64_t _len = p0.cast().norm(); if (_len < 1) return Point(len, 0); return (p0.cast() * int64_t(len) / _len).cast(); }; void SkeletalTrapezoidation::applyTransitions(ptr_vector_t>& edge_transition_ends) { for (edge_t& edge : graph.edges) { if (edge.twin->data.hasTransitionEnds()) { coord_t length = (edge.from->p - edge.to->p).cast().norm(); auto& twin_transition_ends = *edge.twin->data.getTransitionEnds(); if (! edge.data.hasTransitionEnds()) { edge_transition_ends.emplace_back(std::make_shared>()); edge.data.setTransitionEnds(edge_transition_ends.back()); } auto& transition_ends = *edge.data.getTransitionEnds(); for (TransitionEnd& end : twin_transition_ends) { transition_ends.emplace_back(length - end.pos, end.lower_bead_count, end.is_lower_end); } twin_transition_ends.clear(); } } for (edge_t& edge : graph.edges) { if (! edge.data.hasTransitionEnds()) { continue; } assert(edge.data.isCentral()); auto& transitions = *edge.data.getTransitionEnds(); transitions.sort([](const TransitionEnd& a, const TransitionEnd& b) { return a.pos < b.pos; } ); node_t* from = edge.from; node_t* to = edge.to; Point a = from->p; Point b = to->p; Point ab = b - a; coord_t ab_size = (ab).cast().norm(); edge_t* last_edge_replacing_input = &edge; for (TransitionEnd& transition_end : transitions) { coord_t new_node_bead_count = transition_end.is_lower_end? transition_end.lower_bead_count : transition_end.lower_bead_count + 1; coord_t end_pos = transition_end.pos; node_t* close_node = (end_pos < ab_size / 2)? from : to; if ((end_pos < snap_dist || end_pos > ab_size - snap_dist) && close_node->data.bead_count == new_node_bead_count ) { assert(end_pos <= ab_size); close_node->data.transition_ratio = 0; continue; } Point mid = a + normal(ab, end_pos); assert(last_edge_replacing_input->data.isCentral()); assert(last_edge_replacing_input->data.type != SkeletalTrapezoidationEdge::EdgeType::EXTRA_VD); last_edge_replacing_input = graph.insertNode(last_edge_replacing_input, mid, new_node_bead_count); assert(last_edge_replacing_input->data.type != SkeletalTrapezoidationEdge::EdgeType::EXTRA_VD); assert(last_edge_replacing_input->data.isCentral()); } } } bool SkeletalTrapezoidation::isEndOfCentral(const edge_t& edge_to) const { if (!edge_to.data.isCentral()) { return false; } if (!edge_to.next) { return true; } for (const edge_t* edge = edge_to.next; edge && edge != edge_to.twin; edge = edge->twin->next) { if (edge->data.isCentral()) { return false; } assert(edge->twin); } return true; } void SkeletalTrapezoidation::generateExtraRibs() { for (auto edge_it = graph.edges.begin(); edge_it != graph.edges.end(); ++edge_it) { edge_t& edge = *edge_it; if (!edge.data.isCentral() || shorter_then(edge.to->p - edge.from->p, discretization_step_size) || edge.from->data.distance_to_boundary >= edge.to->data.distance_to_boundary) { continue; } std::vector rib_thicknesses = beading_strategy.getNonlinearThicknesses(edge.from->data.bead_count); if (rib_thicknesses.empty()) continue; // Preload some variables before [edge] gets changed node_t* from = edge.from; node_t* to = edge.to; Point a = from->p; Point b = to->p; Point ab = b - a; coord_t ab_size = ab.cast().norm(); coord_t a_R = edge.from->data.distance_to_boundary; coord_t b_R = edge.to->data.distance_to_boundary; edge_t* last_edge_replacing_input = &edge; for (coord_t rib_thickness : rib_thicknesses) { if (rib_thickness / 2 <= a_R) { continue; } if (rib_thickness / 2 >= b_R) { break; } coord_t new_node_bead_count = std::min(edge.from->data.bead_count, edge.to->data.bead_count); coord_t end_pos = int64_t(ab_size) * int64_t(rib_thickness / 2 - a_R) / int64_t(b_R - a_R); assert(end_pos > 0); assert(end_pos < ab_size); node_t* close_node = (end_pos < ab_size / 2)? from : to; if ((end_pos < snap_dist || end_pos > ab_size - snap_dist) && close_node->data.bead_count == new_node_bead_count ) { assert(end_pos <= ab_size); close_node->data.transition_ratio = 0; continue; } Point mid = a + normal(ab, end_pos); assert(last_edge_replacing_input->data.isCentral()); assert(last_edge_replacing_input->data.type != SkeletalTrapezoidationEdge::EdgeType::EXTRA_VD); last_edge_replacing_input = graph.insertNode(last_edge_replacing_input, mid, new_node_bead_count); assert(last_edge_replacing_input->data.type != SkeletalTrapezoidationEdge::EdgeType::EXTRA_VD); assert(last_edge_replacing_input->data.isCentral()); } } } // // ^^^^^^^^^^^^^^^^^^^^^ // TRANSTISIONING // ===================== // TOOLPATH GENERATION // vvvvvvvvvvvvvvvvvvvvv // void SkeletalTrapezoidation::generateSegments() { std::vector upward_quad_mids; for (edge_t& edge : graph.edges) { if (edge.prev && edge.next && edge.isUpward()) { upward_quad_mids.emplace_back(&edge); } } std::sort(upward_quad_mids.begin(), upward_quad_mids.end(), [](edge_t* a, edge_t* b) { if (a->to->data.distance_to_boundary == b->to->data.distance_to_boundary) { // Ordering between two 'upward' edges of the same distance is important when one of the edges is flat and connected to the other if (a->from->data.distance_to_boundary == a->to->data.distance_to_boundary && b->from->data.distance_to_boundary == b->to->data.distance_to_boundary) { coord_t max = std::numeric_limits::max(); coord_t a_dist_from_up = std::min(a->distToGoUp().value_or(max), a->twin->distToGoUp().value_or(max)) - (a->to->p - a->from->p).cast().norm(); coord_t b_dist_from_up = std::min(b->distToGoUp().value_or(max), b->twin->distToGoUp().value_or(max)) - (b->to->p - b->from->p).cast().norm(); return a_dist_from_up < b_dist_from_up; } else if (a->from->data.distance_to_boundary == a->to->data.distance_to_boundary) { return true; // Edge a might be 'above' edge b } else if (b->from->data.distance_to_boundary == b->to->data.distance_to_boundary) { return false; // Edge b might be 'above' edge a } else { // Ordering is not important } } return a->to->data.distance_to_boundary > b->to->data.distance_to_boundary; }); ptr_vector_t node_beadings; { // Store beading for (node_t& node : graph.nodes) { if (node.data.bead_count <= 0) { continue; } if (node.data.transition_ratio == 0) { node_beadings.emplace_back(new BeadingPropagation(beading_strategy.compute(node.data.distance_to_boundary * 2, node.data.bead_count))); node.data.setBeading(node_beadings.back()); assert(node_beadings.back()->beading.total_thickness == node.data.distance_to_boundary * 2); if(node_beadings.back()->beading.total_thickness != node.data.distance_to_boundary * 2) { BOOST_LOG_TRIVIAL(warning) << "If transitioning to an endpoint (ratio 0), the node should be exactly in the middle."; } } else { Beading low_count_beading = beading_strategy.compute(node.data.distance_to_boundary * 2, node.data.bead_count); Beading high_count_beading = beading_strategy.compute(node.data.distance_to_boundary * 2, node.data.bead_count + 1); Beading merged = interpolate(low_count_beading, 1.0 - node.data.transition_ratio, high_count_beading); node_beadings.emplace_back(new BeadingPropagation(merged)); node.data.setBeading(node_beadings.back()); assert(merged.total_thickness == node.data.distance_to_boundary * 2); if(merged.total_thickness != node.data.distance_to_boundary * 2) { BOOST_LOG_TRIVIAL(warning) << "If merging two beads, the new bead must be exactly in the middle."; } } } } #ifdef ARACHNE_DEBUG static int iRun = 0; export_graph_to_svg(debug_out_path("ST-generateSegments-before-propagation-%d.svg", iRun), this->graph, this->outline); #endif propagateBeadingsUpward(upward_quad_mids, node_beadings); #ifdef ARACHNE_DEBUG export_graph_to_svg(debug_out_path("ST-generateSegments-upward-propagation-%d.svg", iRun), this->graph, this->outline); #endif propagateBeadingsDownward(upward_quad_mids, node_beadings); #ifdef ARACHNE_DEBUG export_graph_to_svg(debug_out_path("ST-generateSegments-downward-propagation-%d.svg", iRun), this->graph, this->outline); #endif ptr_vector_t edge_junctions; // junctions ordered high R to low R generateJunctions(node_beadings, edge_junctions); #ifdef ARACHNE_DEBUG export_graph_to_svg(debug_out_path("ST-generateSegments-junctions-%d.svg", iRun), this->graph, this->outline, edge_junctions); #endif connectJunctions(edge_junctions); generateLocalMaximaSingleBeads(); #ifdef ARACHNE_DEBUG ++iRun; #endif } SkeletalTrapezoidation::edge_t* SkeletalTrapezoidation::getQuadMaxRedgeTo(edge_t* quad_start_edge) { assert(quad_start_edge->prev == nullptr); assert(quad_start_edge->from->data.distance_to_boundary == 0); coord_t max_R = -1; edge_t* ret = nullptr; for (edge_t* edge = quad_start_edge; edge; edge = edge->next) { coord_t r = edge->to->data.distance_to_boundary; if (r > max_R) { max_R = r; ret = edge; } } if (!ret->next && ret->to->data.distance_to_boundary - scaled(0.005) < ret->from->data.distance_to_boundary) { ret = ret->prev; } assert(ret); assert(ret->next); return ret; } void SkeletalTrapezoidation::propagateBeadingsUpward(std::vector& upward_quad_mids, ptr_vector_t& node_beadings) { for (auto upward_quad_mids_it = upward_quad_mids.rbegin(); upward_quad_mids_it != upward_quad_mids.rend(); ++upward_quad_mids_it) { edge_t* upward_edge = *upward_quad_mids_it; if (upward_edge->to->data.bead_count >= 0) { // Don't override local beading continue; } if (! upward_edge->from->data.hasBeading()) { // Only propagate if we have something to propagate continue; } BeadingPropagation& lower_beading = *upward_edge->from->data.getBeading(); if (upward_edge->to->data.hasBeading()) { // Only propagate to places where there is place continue; } assert((upward_edge->from->data.distance_to_boundary != upward_edge->to->data.distance_to_boundary || shorter_then(upward_edge->to->p - upward_edge->from->p, central_filter_dist)) && "zero difference R edges should always be central"); coord_t length = (upward_edge->to->p - upward_edge->from->p).cast().norm(); BeadingPropagation upper_beading = lower_beading; upper_beading.dist_to_bottom_source += length; upper_beading.is_upward_propagated_only = true; node_beadings.emplace_back(new BeadingPropagation(upper_beading)); upward_edge->to->data.setBeading(node_beadings.back()); assert(upper_beading.beading.total_thickness <= upward_edge->to->data.distance_to_boundary * 2); } } void SkeletalTrapezoidation::propagateBeadingsDownward(std::vector& upward_quad_mids, ptr_vector_t& node_beadings) { for (edge_t* upward_quad_mid : upward_quad_mids) { // Transfer beading information to lower nodes if (!upward_quad_mid->data.isCentral()) { // for equidistant edge: propagate from known beading to node with unknown beading if (upward_quad_mid->from->data.distance_to_boundary == upward_quad_mid->to->data.distance_to_boundary && upward_quad_mid->from->data.hasBeading() && ! upward_quad_mid->to->data.hasBeading() ) { propagateBeadingsDownward(upward_quad_mid->twin, node_beadings); } else { propagateBeadingsDownward(upward_quad_mid, node_beadings); } } } } void SkeletalTrapezoidation::propagateBeadingsDownward(edge_t* edge_to_peak, ptr_vector_t& node_beadings) { coord_t length = (edge_to_peak->to->p - edge_to_peak->from->p).cast().norm(); BeadingPropagation& top_beading = *getOrCreateBeading(edge_to_peak->to, node_beadings); assert(top_beading.beading.total_thickness >= edge_to_peak->to->data.distance_to_boundary * 2); if(top_beading.beading.total_thickness < edge_to_peak->to->data.distance_to_boundary * 2) { BOOST_LOG_TRIVIAL(warning) << "Top bead is beyond the center of the total width."; } assert(!top_beading.is_upward_propagated_only); if(!edge_to_peak->from->data.hasBeading()) { // Set new beading if there is no beading associated with the node yet BeadingPropagation propagated_beading = top_beading; propagated_beading.dist_from_top_source += length; node_beadings.emplace_back(new BeadingPropagation(propagated_beading)); edge_to_peak->from->data.setBeading(node_beadings.back()); assert(propagated_beading.beading.total_thickness >= edge_to_peak->from->data.distance_to_boundary * 2); if(propagated_beading.beading.total_thickness < edge_to_peak->from->data.distance_to_boundary * 2) { BOOST_LOG_TRIVIAL(warning) << "Propagated bead is beyond the center of the total width."; } } else { BeadingPropagation& bottom_beading = *edge_to_peak->from->data.getBeading(); coord_t total_dist = top_beading.dist_from_top_source + length + bottom_beading.dist_to_bottom_source; double ratio_of_top = static_cast(bottom_beading.dist_to_bottom_source) / std::min(total_dist, beading_propagation_transition_dist); ratio_of_top = std::max(0.0, ratio_of_top); if (ratio_of_top >= 1.0) { bottom_beading = top_beading; bottom_beading.dist_from_top_source += length; } else { Beading merged_beading = interpolate(top_beading.beading, ratio_of_top, bottom_beading.beading, edge_to_peak->from->data.distance_to_boundary); bottom_beading = BeadingPropagation(merged_beading); bottom_beading.is_upward_propagated_only = false; assert(merged_beading.total_thickness >= edge_to_peak->from->data.distance_to_boundary * 2); if(merged_beading.total_thickness < edge_to_peak->from->data.distance_to_boundary * 2) { BOOST_LOG_TRIVIAL(warning) << "Merged bead is beyond the center of the total width."; } } } } SkeletalTrapezoidation::Beading SkeletalTrapezoidation::interpolate(const Beading& left, double ratio_left_to_whole, const Beading& right, coord_t switching_radius) const { assert(ratio_left_to_whole >= 0.0 && ratio_left_to_whole <= 1.0); Beading ret = interpolate(left, ratio_left_to_whole, right); // TODO: don't use toolpath locations past the middle! // TODO: stretch bead widths and locations of the higher bead count beading to fit in the left over space coord_t next_inset_idx; for (next_inset_idx = left.toolpath_locations.size() - 1; next_inset_idx >= 0; next_inset_idx--) { if (switching_radius > left.toolpath_locations[next_inset_idx]) { break; } } if (next_inset_idx < 0) { // There is no next inset, because there is only one assert(left.toolpath_locations.empty() || left.toolpath_locations.front() >= switching_radius); return ret; } if (next_inset_idx + 1 == coord_t(left.toolpath_locations.size())) { // We cant adjust to fit the next edge because there is no previous one?! return ret; } assert(next_inset_idx < coord_t(left.toolpath_locations.size())); assert(left.toolpath_locations[next_inset_idx] <= switching_radius); assert(left.toolpath_locations[next_inset_idx + 1] >= switching_radius); if (ret.toolpath_locations[next_inset_idx] > switching_radius) { // One inset disappeared between left and the merged one // solve for ratio f: // f*l + (1-f)*r = s // f*l + r - f*r = s // f*(l-r) + r = s // f*(l-r) = s - r // f = (s-r) / (l-r) float new_ratio = static_cast(switching_radius - right.toolpath_locations[next_inset_idx]) / static_cast(left.toolpath_locations[next_inset_idx] - right.toolpath_locations[next_inset_idx]); new_ratio = std::min(1.0, new_ratio + 0.1); return interpolate(left, new_ratio, right); } return ret; } SkeletalTrapezoidation::Beading SkeletalTrapezoidation::interpolate(const Beading& left, double ratio_left_to_whole, const Beading& right) const { assert(ratio_left_to_whole >= 0.0 && ratio_left_to_whole <= 1.0); float ratio_right_to_whole = 1.0 - ratio_left_to_whole; Beading ret = (left.total_thickness > right.total_thickness)? left : right; for (size_t inset_idx = 0; inset_idx < std::min(left.bead_widths.size(), right.bead_widths.size()); inset_idx++) { if(left.bead_widths[inset_idx] == 0 || right.bead_widths[inset_idx] == 0) { ret.bead_widths[inset_idx] = 0; //0-width wall markers stay 0-width. } else { ret.bead_widths[inset_idx] = ratio_left_to_whole * left.bead_widths[inset_idx] + ratio_right_to_whole * right.bead_widths[inset_idx]; } ret.toolpath_locations[inset_idx] = ratio_left_to_whole * left.toolpath_locations[inset_idx] + ratio_right_to_whole * right.toolpath_locations[inset_idx]; } return ret; } void SkeletalTrapezoidation::generateJunctions(ptr_vector_t& node_beadings, ptr_vector_t& edge_junctions) { for (edge_t& edge_ : graph.edges) { edge_t* edge = &edge_; if (edge->from->data.distance_to_boundary > edge->to->data.distance_to_boundary) { // Only consider the upward half-edges continue; } coord_t start_R = edge->to->data.distance_to_boundary; // higher R coord_t end_R = edge->from->data.distance_to_boundary; // lower R if ((edge->from->data.bead_count == edge->to->data.bead_count && edge->from->data.bead_count >= 0) || end_R >= start_R) { // No beads to generate continue; } Beading* beading = &getOrCreateBeading(edge->to, node_beadings)->beading; edge_junctions.emplace_back(std::make_shared()); edge_.data.setExtrusionJunctions(edge_junctions.back()); // initialization LineJunctions& ret = *edge_junctions.back(); assert(beading->total_thickness >= edge->to->data.distance_to_boundary * 2); if(beading->total_thickness < edge->to->data.distance_to_boundary * 2) { BOOST_LOG_TRIVIAL(warning) << "Generated junction is beyond the center of total width."; } Point a = edge->to->p; Point b = edge->from->p; Point ab = b - a; const size_t num_junctions = beading->toolpath_locations.size(); size_t junction_idx; // Compute starting junction_idx for this segment for (junction_idx = (std::max(size_t(1), beading->toolpath_locations.size()) - 1) / 2; junction_idx < num_junctions; junction_idx--) { coord_t bead_R = beading->toolpath_locations[junction_idx]; // toolpath_locations computed inside DistributedBeadingStrategy could be off by 1 because of rounding errors. // In GH issue #8472, these roundings errors caused missing the middle extrusion. // Adding small epsilon should help resolve those cases. if (bead_R <= start_R + 1) { // Junction coinciding with start node is used in this function call break; } } // Robustness against odd segments which might lie just slightly outside of the range due to rounding errors // not sure if this is really needed (TODO) if (junction_idx + 1 < num_junctions && beading->toolpath_locations[junction_idx + 1] <= start_R + scaled(0.005) && beading->total_thickness < start_R + scaled(0.005) ) { junction_idx++; } for (; junction_idx < num_junctions; junction_idx--) //When junction_idx underflows, it'll be more than num_junctions too. { coord_t bead_R = beading->toolpath_locations[junction_idx]; assert(bead_R >= 0); if (bead_R < end_R) { // Junction coinciding with a node is handled by the next segment break; } Point junction(a + (ab.cast() * int64_t(bead_R - start_R) / int64_t(end_R - start_R)).cast()); if (bead_R > start_R - scaled(0.005)) { // Snap to start node if it is really close, in order to be able to see 3-way intersection later on more robustly junction = a; } ret.emplace_back(junction, beading->bead_widths[junction_idx], junction_idx); } } } std::shared_ptr SkeletalTrapezoidation::getOrCreateBeading(node_t* node, ptr_vector_t& node_beadings) { if (! node->data.hasBeading()) { if (node->data.bead_count == -1) { // This bug is due to too small central edges constexpr coord_t nearby_dist = scaled(0.1); auto nearest_beading = getNearestBeading(node, nearby_dist); if (nearest_beading) { return nearest_beading; } // Else make a new beading: bool has_central_edge = false; bool first = true; coord_t dist = std::numeric_limits::max(); for (edge_t* edge = node->incident_edge; edge && (first || edge != node->incident_edge); edge = edge->twin->next) { if (edge->data.isCentral()) { has_central_edge = true; } assert(edge->to->data.distance_to_boundary >= 0); dist = std::min(dist, edge->to->data.distance_to_boundary + coord_t((edge->to->p - edge->from->p).cast().norm())); first = false; } if (!has_central_edge) { BOOST_LOG_TRIVIAL(error) << "Unknown beading for non-central node!"; } assert(dist != std::numeric_limits::max()); node->data.bead_count = beading_strategy.getOptimalBeadCount(dist * 2); } assert(node->data.bead_count != -1); node_beadings.emplace_back(new BeadingPropagation(beading_strategy.compute(node->data.distance_to_boundary * 2, node->data.bead_count))); node->data.setBeading(node_beadings.back()); } assert(node->data.hasBeading()); return node->data.getBeading(); } std::shared_ptr SkeletalTrapezoidation::getNearestBeading(node_t* node, coord_t max_dist) { struct DistEdge { edge_t* edge_to; coord_t dist; DistEdge(edge_t* edge_to, coord_t dist) : edge_to(edge_to), dist(dist) {} }; auto compare = [](const DistEdge& l, const DistEdge& r) -> bool { return l.dist > r.dist; }; std::priority_queue, decltype(compare)> further_edges(compare); bool first = true; for (edge_t* outgoing = node->incident_edge; outgoing && (first || outgoing != node->incident_edge); outgoing = outgoing->twin->next) { further_edges.emplace(outgoing, (outgoing->to->p - outgoing->from->p).cast().norm()); first = false; } for (coord_t counter = 0; counter < SKELETAL_TRAPEZOIDATION_BEAD_SEARCH_MAX; counter++) { // Prevent endless recursion if (further_edges.empty()) return nullptr; DistEdge here = further_edges.top(); further_edges.pop(); if (here.dist > max_dist) return nullptr; if (here.edge_to->to->data.hasBeading()) { return here.edge_to->to->data.getBeading(); } else { // recurse for (edge_t* further_edge = here.edge_to->next; further_edge && further_edge != here.edge_to->twin; further_edge = further_edge->twin->next) { further_edges.emplace(further_edge, here.dist + (further_edge->to->p - further_edge->from->p).cast().norm()); } } } return nullptr; } void SkeletalTrapezoidation::addToolpathSegment(const ExtrusionJunction& from, const ExtrusionJunction& to, bool is_odd, bool force_new_path, bool from_is_3way, bool to_is_3way) { if (from == to) return; std::vector &generated_toolpaths = *p_generated_toolpaths; size_t inset_idx = from.perimeter_index; if (inset_idx >= generated_toolpaths.size()) { generated_toolpaths.resize(inset_idx + 1); } assert((generated_toolpaths[inset_idx].empty() || !generated_toolpaths[inset_idx].back().junctions.empty()) && "empty extrusion lines should never have been generated"); if (generated_toolpaths[inset_idx].empty() || generated_toolpaths[inset_idx].back().is_odd != is_odd || generated_toolpaths[inset_idx].back().junctions.back().perimeter_index != inset_idx // inset_idx should always be consistent ) { force_new_path = true; } if (!force_new_path && shorter_then(generated_toolpaths[inset_idx].back().junctions.back().p - from.p, scaled(0.010)) && std::abs(generated_toolpaths[inset_idx].back().junctions.back().w - from.w) < scaled(0.010) && ! from_is_3way // force new path at 3way intersection ) { generated_toolpaths[inset_idx].back().junctions.push_back(to); } else if (!force_new_path && shorter_then(generated_toolpaths[inset_idx].back().junctions.back().p - to.p, scaled(0.010)) && std::abs(generated_toolpaths[inset_idx].back().junctions.back().w - to.w) < scaled(0.010) && ! to_is_3way // force new path at 3way intersection ) { if ( ! is_odd) { BOOST_LOG_TRIVIAL(error) << "Reversing even wall line causes it to be printed CCW instead of CW!"; } generated_toolpaths[inset_idx].back().junctions.push_back(from); } else { generated_toolpaths[inset_idx].emplace_back(inset_idx, is_odd); generated_toolpaths[inset_idx].back().junctions.push_back(from); generated_toolpaths[inset_idx].back().junctions.push_back(to); } }; void SkeletalTrapezoidation::connectJunctions(ptr_vector_t& edge_junctions) { EdgeSet unprocessed_quad_starts(graph.edges.size() * 5 / 2); for (edge_t& edge : graph.edges) { if (!edge.prev) { unprocessed_quad_starts.emplace(&edge); } } EdgeSet passed_odd_edges; while (!unprocessed_quad_starts.empty()) { edge_t* poly_domain_start = *unprocessed_quad_starts.begin(); edge_t* quad_start = poly_domain_start; bool new_domain_start = true; do { edge_t* quad_end = quad_start; while (quad_end->next) { quad_end = quad_end->next; } edge_t* edge_to_peak = getQuadMaxRedgeTo(quad_start); // walk down on both sides and connect junctions edge_t* edge_from_peak = edge_to_peak->next; assert(edge_from_peak); unprocessed_quad_starts.erase(quad_start); if (! edge_to_peak->data.hasExtrusionJunctions()) { edge_junctions.emplace_back(std::make_shared()); edge_to_peak->data.setExtrusionJunctions(edge_junctions.back()); } // The junctions on the edge(s) from the start of the quad to the node with highest R LineJunctions from_junctions = *edge_to_peak->data.getExtrusionJunctions(); if (! edge_from_peak->twin->data.hasExtrusionJunctions()) { edge_junctions.emplace_back(std::make_shared()); edge_from_peak->twin->data.setExtrusionJunctions(edge_junctions.back()); } // The junctions on the edge(s) from the end of the quad to the node with highest R LineJunctions to_junctions = *edge_from_peak->twin->data.getExtrusionJunctions(); if (edge_to_peak->prev) { LineJunctions from_prev_junctions = *edge_to_peak->prev->data.getExtrusionJunctions(); while (!from_junctions.empty() && !from_prev_junctions.empty() && from_junctions.back().perimeter_index <= from_prev_junctions.front().perimeter_index) { from_junctions.pop_back(); } from_junctions.reserve(from_junctions.size() + from_prev_junctions.size()); from_junctions.insert(from_junctions.end(), from_prev_junctions.begin(), from_prev_junctions.end()); assert(!edge_to_peak->prev->prev); if(edge_to_peak->prev->prev) { BOOST_LOG_TRIVIAL(warning) << "The edge we're about to connect is already connected."; } } if (edge_from_peak->next) { LineJunctions to_next_junctions = *edge_from_peak->next->twin->data.getExtrusionJunctions(); while (!to_junctions.empty() && !to_next_junctions.empty() && to_junctions.back().perimeter_index <= to_next_junctions.front().perimeter_index) { to_junctions.pop_back(); } to_junctions.reserve(to_junctions.size() + to_next_junctions.size()); to_junctions.insert(to_junctions.end(), to_next_junctions.begin(), to_next_junctions.end()); assert(!edge_from_peak->next->next); if(edge_from_peak->next->next) { BOOST_LOG_TRIVIAL(warning) << "The edge we're about to connect is already connected!"; } } assert(std::abs(int(from_junctions.size()) - int(to_junctions.size())) <= 1); // at transitions one end has more beads if(std::abs(int(from_junctions.size()) - int(to_junctions.size())) > 1) { BOOST_LOG_TRIVIAL(warning) << "Can't create a transition when connecting two perimeters where the number of beads differs too much! " << from_junctions.size() << " vs. " << to_junctions.size(); } size_t segment_count = std::min(from_junctions.size(), to_junctions.size()); for (size_t junction_rev_idx = 0; junction_rev_idx < segment_count; junction_rev_idx++) { ExtrusionJunction& from = from_junctions[from_junctions.size() - 1 - junction_rev_idx]; ExtrusionJunction& to = to_junctions[to_junctions.size() - 1 - junction_rev_idx]; assert(from.perimeter_index == to.perimeter_index); if(from.perimeter_index != to.perimeter_index) { BOOST_LOG_TRIVIAL(warning) << "Connecting two perimeters with different indices! Perimeter " << from.perimeter_index << " and " << to.perimeter_index; } const bool from_is_odd = quad_start->to->data.bead_count > 0 && quad_start->to->data.bead_count % 2 == 1 // quad contains single bead segment && quad_start->to->data.transition_ratio == 0 // We're not in a transition && junction_rev_idx == segment_count - 1 // Is single bead segment && shorter_then(from.p - quad_start->to->p, scaled(0.005)); const bool to_is_odd = quad_end->from->data.bead_count > 0 && quad_end->from->data.bead_count % 2 == 1 // quad contains single bead segment && quad_end->from->data.transition_ratio == 0 // We're not in a transition && junction_rev_idx == segment_count - 1 // Is single bead segment && shorter_then(to.p - quad_end->from->p, scaled(0.005)); const bool is_odd_segment = from_is_odd && to_is_odd; if (is_odd_segment && passed_odd_edges.count(quad_start->next->twin) > 0) // Only generate toolpath for odd segments once { continue; // Prevent duplication of single bead segments } bool from_is_3way = from_is_odd && quad_start->to->isMultiIntersection(); bool to_is_3way = to_is_odd && quad_end->from->isMultiIntersection(); passed_odd_edges.emplace(quad_start->next); addToolpathSegment(from, to, is_odd_segment, new_domain_start, from_is_3way, to_is_3way); } new_domain_start = false; } while(quad_start = quad_start->getNextUnconnected(), quad_start != poly_domain_start); } } void SkeletalTrapezoidation::generateLocalMaximaSingleBeads() { std::vector &generated_toolpaths = *p_generated_toolpaths; for (auto& node : graph.nodes) { if (! node.data.hasBeading()) { continue; } Beading& beading = node.data.getBeading()->beading; if (beading.bead_widths.size() % 2 == 1 && node.isLocalMaximum(true) && !node.isCentral()) { const size_t inset_index = beading.bead_widths.size() / 2; constexpr bool is_odd = true; if (inset_index >= generated_toolpaths.size()) { generated_toolpaths.resize(inset_index + 1); } generated_toolpaths[inset_index].emplace_back(inset_index, is_odd); ExtrusionLine& line = generated_toolpaths[inset_index].back(); const coord_t width = beading.bead_widths[inset_index]; // total area to be extruded is pi*(w/2)^2 = pi*w*w/4 // Width a constant extrusion width w, that would be a length of pi*w/4 // If we make a small circle to fill up the hole, then that circle would have a circumference of 2*pi*r // So our circle needs to be such that r=w/8 const coord_t r = width / 8; constexpr coord_t n_segments = 6; for (coord_t segment = 0; segment < n_segments; segment++) { float a = 2.0 * M_PI / n_segments * segment; line.junctions.emplace_back(node.p + Point(r * cos(a), r * sin(a)), width, inset_index); } } } } // // ^^^^^^^^^^^^^^^^^^^^^ // TOOLPATH GENERATION // ===================== // } // namespace Slic3r::Arachne